split up entry points and engine components, group all actions
This commit is contained in:
parent
60fb4c94cf
commit
2f26d4e883
|
@ -2,6 +2,7 @@ from logging import getLogger
|
|||
|
||||
from taleweave.context import (
|
||||
action_context,
|
||||
add_extra_actions,
|
||||
broadcast,
|
||||
get_agent_for_character,
|
||||
get_character_agent_for_name,
|
||||
|
@ -10,6 +11,7 @@ from taleweave.context import (
|
|||
world_context,
|
||||
)
|
||||
from taleweave.errors import ActionError
|
||||
from taleweave.systems.action import ACTION_SYSTEM_NAME
|
||||
from taleweave.utils.conversation import loop_conversation
|
||||
from taleweave.utils.search import (
|
||||
find_character_in_room,
|
||||
|
@ -337,3 +339,18 @@ def action_drop(item: str) -> str:
|
|||
action_room.items.append(action_item)
|
||||
|
||||
return format_prompt("action_drop_result", item=item)
|
||||
|
||||
|
||||
def init():
|
||||
return add_extra_actions(
|
||||
ACTION_SYSTEM_NAME,
|
||||
[
|
||||
action_examine,
|
||||
action_move,
|
||||
action_take,
|
||||
action_tell,
|
||||
action_ask,
|
||||
action_give,
|
||||
action_drop,
|
||||
],
|
||||
)
|
||||
|
|
|
@ -5,6 +5,7 @@ from packit.agent import Agent, agent_easy_connect
|
|||
|
||||
from taleweave.context import (
|
||||
action_context,
|
||||
add_extra_actions,
|
||||
broadcast,
|
||||
get_agent_for_character,
|
||||
get_current_turn,
|
||||
|
@ -21,6 +22,7 @@ from taleweave.generate import (
|
|||
generate_room,
|
||||
link_rooms,
|
||||
)
|
||||
from taleweave.systems.action import ACTION_SYSTEM_NAME
|
||||
from taleweave.utils.effect import apply_effects, is_effect_ready
|
||||
from taleweave.utils.search import find_character_in_room
|
||||
from taleweave.utils.string import normalize_name
|
||||
|
@ -256,8 +258,11 @@ def init_optional() -> List[Callable]:
|
|||
"""
|
||||
Initialize the custom actions.
|
||||
"""
|
||||
return [
|
||||
action_explore,
|
||||
action_search,
|
||||
action_use,
|
||||
]
|
||||
return add_extra_actions(
|
||||
ACTION_SYSTEM_NAME,
|
||||
[
|
||||
action_explore,
|
||||
action_search,
|
||||
action_use,
|
||||
],
|
||||
)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from taleweave.context import (
|
||||
action_context,
|
||||
add_extra_actions,
|
||||
get_agent_for_character,
|
||||
get_current_turn,
|
||||
get_game_config,
|
||||
|
@ -7,6 +8,7 @@ from taleweave.context import (
|
|||
)
|
||||
from taleweave.errors import ActionError
|
||||
from taleweave.models.planning import CalendarEvent
|
||||
from taleweave.systems.planning import PLANNING_SYSTEM_NAME
|
||||
from taleweave.utils.planning import get_recent_notes
|
||||
from taleweave.utils.template import format_prompt
|
||||
|
||||
|
@ -194,3 +196,18 @@ def check_calendar(count: int):
|
|||
for event in events
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def init():
|
||||
return add_extra_actions(
|
||||
PLANNING_SYSTEM_NAME,
|
||||
[
|
||||
take_note,
|
||||
read_notes,
|
||||
erase_notes,
|
||||
edit_note,
|
||||
summarize_notes,
|
||||
schedule_event,
|
||||
check_calendar,
|
||||
],
|
||||
)
|
||||
|
|
|
@ -34,10 +34,10 @@ current_character: Character | None = None
|
|||
dungeon_master: Agent | None = None
|
||||
|
||||
# game context
|
||||
# TODO: wrap this into a class that can be passed around
|
||||
# TODO: wrap these into a class that can be passed around
|
||||
action_groups: Dict[str, List[Callable[..., str]]] = {}
|
||||
character_agents: Dict[str, Tuple[Character, Agent]] = {}
|
||||
event_emitter = EventEmitter()
|
||||
extra_actions: List[Callable[..., str]] = []
|
||||
game_config: Config = DEFAULT_CONFIG
|
||||
game_systems: List[GameSystem] = []
|
||||
prompt_library: PromptLibrary = PromptLibrary(prompts={})
|
||||
|
@ -186,8 +186,8 @@ def get_system_data(system: str) -> Any | None:
|
|||
return system_data.get(system)
|
||||
|
||||
|
||||
def get_extra_actions() -> List[Callable[..., str]]:
|
||||
return extra_actions
|
||||
def get_action_group(name: str) -> List[Callable[..., str]]:
|
||||
return action_groups.get(name, [])
|
||||
|
||||
|
||||
# endregion
|
||||
|
@ -242,9 +242,9 @@ def set_system_data(system: str, data: Any):
|
|||
system_data[system] = data
|
||||
|
||||
|
||||
def set_extra_actions(actions: List[Callable[..., str]]):
|
||||
global extra_actions
|
||||
extra_actions = actions
|
||||
def add_extra_actions(group: str, actions: List[Callable[..., str]]):
|
||||
action_groups.setdefault(group, []).extend(actions)
|
||||
return group, actions
|
||||
|
||||
|
||||
# endregion
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import argparse
|
||||
from os import path
|
||||
from os import environ, path
|
||||
from typing import List, Tuple
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from packit.utils import logger_with_colors
|
||||
|
||||
from taleweave.context import get_dungeon_master, get_game_systems, set_game_systems
|
||||
from taleweave.engine import load_or_initialize_system_data
|
||||
from taleweave.game_system import GameSystem
|
||||
from taleweave.generate import (
|
||||
generate_character,
|
||||
|
@ -11,7 +15,7 @@ from taleweave.generate import (
|
|||
generate_room,
|
||||
link_rooms,
|
||||
)
|
||||
from taleweave.main import load_or_initialize_system_data
|
||||
from taleweave.main import load_prompt_library
|
||||
from taleweave.models.base import dump_model
|
||||
from taleweave.models.entity import World, WorldState
|
||||
from taleweave.plugins import load_plugin
|
||||
|
@ -30,9 +34,16 @@ from taleweave.utils.world import describe_entity
|
|||
|
||||
ENTITY_TYPES = ["room", "portal", "item", "character"]
|
||||
|
||||
logger = logger_with_colors(__name__)
|
||||
|
||||
|
||||
# load environment variables before anything else
|
||||
load_dotenv(environ.get("TALEWEAVE_ENV", ".env"), override=True)
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="Taleweave Editor")
|
||||
parser.add_argument("--prompts", type=str, nargs="*", help="Prompt files to load")
|
||||
parser.add_argument("--state", type=str, help="State file to edit")
|
||||
parser.add_argument("--world", type=str, help="World file to edit")
|
||||
parser.add_argument("--systems", type=str, nargs="*", help="Game systems to load")
|
||||
|
@ -41,11 +52,9 @@ def parse_args():
|
|||
subparsers.required = True
|
||||
|
||||
# Set up the 'list' command
|
||||
list_parser = subparsers.add_parser(
|
||||
"list", help="List all entities or entities of a specific type"
|
||||
)
|
||||
list_parser = subparsers.add_parser("list", help="List entities of a specific type")
|
||||
list_parser.add_argument(
|
||||
"type", help="Type of entity to list", choices=ENTITY_TYPES, nargs="?"
|
||||
"type", help="Type of entity to list", choices=ENTITY_TYPES
|
||||
)
|
||||
|
||||
# Set up the 'describe' command
|
||||
|
@ -107,7 +116,7 @@ def parse_args():
|
|||
|
||||
|
||||
def load_world(state_file, world_file) -> Tuple[World, WorldState | None]:
|
||||
systems = []
|
||||
systems = get_game_systems()
|
||||
|
||||
if state_file and path.exists(state_file):
|
||||
with open(state_file, "r") as f:
|
||||
|
@ -129,14 +138,19 @@ def load_world(state_file, world_file) -> Tuple[World, WorldState | None]:
|
|||
|
||||
|
||||
def save_world(state_file, world_file, world: World, state: WorldState | None):
|
||||
"""
|
||||
Save the world to the given files.
|
||||
|
||||
This is intentionally a noop stub until the editor is more stable.
|
||||
"""
|
||||
if state:
|
||||
print(f"Saving world {world.name} to {state_file}")
|
||||
logger.warning(f"Saving world {world.name} to {state_file}")
|
||||
return
|
||||
|
||||
with open(state_file, "w") as f:
|
||||
save_yaml(f, dump_model(WorldState, state))
|
||||
else:
|
||||
print(f"Saving world {world.name} to {world_file}")
|
||||
logger.warning(f"Saving world {world.name} to {world_file}")
|
||||
return
|
||||
|
||||
with open(world_file, "w") as f:
|
||||
|
@ -144,47 +158,45 @@ def save_world(state_file, world_file, world: World, state: WorldState | None):
|
|||
|
||||
|
||||
def command_list(args):
|
||||
print(f"Listing {args.type}s")
|
||||
world, _ = load_world(args.state, args.world)
|
||||
print(world.name)
|
||||
logger.info(f"Listing {args.type}s from world {world.name}")
|
||||
|
||||
if args.type == "room":
|
||||
for room in list_rooms(world):
|
||||
print(room.name)
|
||||
logger.info(room.name)
|
||||
|
||||
if args.type == "portal":
|
||||
for portal in list_portals(world):
|
||||
print(portal.name)
|
||||
logger.info(portal.name)
|
||||
|
||||
if args.type == "item":
|
||||
for item in list_items(
|
||||
world, include_character_inventory=True, include_item_inventory=True
|
||||
):
|
||||
print(item.name)
|
||||
logger.info(item.name)
|
||||
|
||||
if args.type == "character":
|
||||
for character in list_characters(world):
|
||||
print(character.name)
|
||||
logger.info(character.name)
|
||||
|
||||
|
||||
def command_describe(args):
|
||||
print(f"Describing {args.entity}")
|
||||
world, _ = load_world(args.state, args.world)
|
||||
print(world.name)
|
||||
logger.info(f"Describing {args.entity} from world {world.name}")
|
||||
|
||||
if args.type == "room":
|
||||
room = find_room(world, args.entity)
|
||||
if not room:
|
||||
print(f"Room {args.entity} not found")
|
||||
logger.error(f"Room {args.entity} not found")
|
||||
else:
|
||||
print(describe_entity(room))
|
||||
logger.info(describe_entity(room))
|
||||
|
||||
if args.type == "portal":
|
||||
portal = find_portal(world, args.entity)
|
||||
if not portal:
|
||||
print(f"Portal {args.entity} not found")
|
||||
logger.error(f"Portal {args.entity} not found")
|
||||
else:
|
||||
print(describe_entity(portal))
|
||||
logger.info(describe_entity(portal))
|
||||
|
||||
if args.type == "item":
|
||||
item = find_item(
|
||||
|
@ -194,22 +206,21 @@ def command_describe(args):
|
|||
include_item_inventory=True,
|
||||
)
|
||||
if not item:
|
||||
print(f"Item {args.entity} not found")
|
||||
logger.error(f"Item {args.entity} not found")
|
||||
else:
|
||||
print(describe_entity(item))
|
||||
logger.info(describe_entity(item))
|
||||
|
||||
if args.type == "character":
|
||||
character = find_character(world, args.entity)
|
||||
if not character:
|
||||
print(f"Character {args.entity} not found")
|
||||
logger.error(f"Character {args.entity} not found")
|
||||
else:
|
||||
print(describe_entity(character))
|
||||
logger.info(describe_entity(character))
|
||||
|
||||
|
||||
def command_create(args):
|
||||
print(f"Create {args.type} named {args.name}")
|
||||
world, state = load_world(args.state, args.world)
|
||||
print(world.name)
|
||||
logger.info(f"Create {args.type} named {args.name} in world {world.name}")
|
||||
|
||||
# TODO: Create the entity
|
||||
|
||||
|
@ -217,9 +228,10 @@ def command_create(args):
|
|||
|
||||
|
||||
def command_generate(args):
|
||||
print(f"Generate {args.type} with prompt: {args.prompt}")
|
||||
world, state = load_world(args.state, args.world)
|
||||
print(world.name)
|
||||
logger.info(
|
||||
f"Generating {args.type} for world {world.name} using prompt: {args.prompt}"
|
||||
)
|
||||
|
||||
dungeon_master = get_dungeon_master()
|
||||
systems = get_game_systems()
|
||||
|
@ -231,12 +243,12 @@ def command_generate(args):
|
|||
if args.type == "portal":
|
||||
source_room = find_room(world, args.room)
|
||||
if not source_room:
|
||||
print(f"Room {args.room} not found")
|
||||
logger.error(f"Room {args.room} not found")
|
||||
return
|
||||
|
||||
destination_room = find_room(world, args.dest_room)
|
||||
if not destination_room:
|
||||
print(f"Room {args.dest_room} not found")
|
||||
logger.error(f"Room {args.dest_room} not found")
|
||||
return
|
||||
|
||||
outgoing_portal, incoming_portal = generate_portals(
|
||||
|
@ -249,7 +261,7 @@ def command_generate(args):
|
|||
# TODO: add item to character or container inventory
|
||||
room = find_room(world, args.room)
|
||||
if not room:
|
||||
print(f"Room {args.room} not found")
|
||||
logger.error(f"Room {args.room} not found")
|
||||
return
|
||||
|
||||
item = generate_item(dungeon_master, world, systems)
|
||||
|
@ -258,7 +270,7 @@ def command_generate(args):
|
|||
if args.type == "character":
|
||||
room = find_room(world, args.room)
|
||||
if not room:
|
||||
print(f"Room {args.room} not found")
|
||||
logger.error(f"Room {args.room} not found")
|
||||
return
|
||||
|
||||
character = generate_character(
|
||||
|
@ -270,9 +282,8 @@ def command_generate(args):
|
|||
|
||||
|
||||
def command_delete(args):
|
||||
print(f"Delete {args.entity}")
|
||||
world, state = load_world(args.state, args.world)
|
||||
print(world.name)
|
||||
logger.info(f"Delete {args.entity} from world {world.name}")
|
||||
|
||||
# TODO: Delete the entity
|
||||
|
||||
|
@ -280,23 +291,22 @@ def command_delete(args):
|
|||
|
||||
|
||||
def command_update(args):
|
||||
print(f"Update {args.entity}")
|
||||
world, state = load_world(args.state, args.world)
|
||||
print(world.name)
|
||||
logger.info(f"Update {args.entity} in world {world.name}")
|
||||
|
||||
if args.type == "room":
|
||||
room = find_room(world, args.entity)
|
||||
if not room:
|
||||
print(f"Room {args.entity} not found")
|
||||
logger.error(f"Room {args.entity} not found")
|
||||
else:
|
||||
print(describe_entity(room))
|
||||
logger.info(describe_entity(room))
|
||||
|
||||
if args.type == "portal":
|
||||
portal = find_portal(world, args.entity)
|
||||
if not portal:
|
||||
print(f"Portal {args.entity} not found")
|
||||
logger.error(f"Portal {args.entity} not found")
|
||||
else:
|
||||
print(describe_entity(portal))
|
||||
logger.info(describe_entity(portal))
|
||||
|
||||
if args.type == "item":
|
||||
item = find_item(
|
||||
|
@ -306,14 +316,14 @@ def command_update(args):
|
|||
include_item_inventory=True,
|
||||
)
|
||||
if not item:
|
||||
print(f"Item {args.entity} not found")
|
||||
logger.error(f"Item {args.entity} not found")
|
||||
else:
|
||||
print(describe_entity(item))
|
||||
logger.info(describe_entity(item))
|
||||
|
||||
if args.type == "character":
|
||||
character = find_character(world, args.entity)
|
||||
if not character:
|
||||
print(f"Character {args.entity} not found")
|
||||
logger.error(f"Character {args.entity} not found")
|
||||
else:
|
||||
if args.backstory:
|
||||
character.backstory = args.backstory
|
||||
|
@ -321,15 +331,14 @@ def command_update(args):
|
|||
if args.description:
|
||||
character.description = args.description
|
||||
|
||||
print(describe_entity(character))
|
||||
logger.info(describe_entity(character))
|
||||
|
||||
save_world(args.state, args.world, world, state)
|
||||
|
||||
|
||||
def command_link(args):
|
||||
print(f"Linking rooms {args.rooms}")
|
||||
world, state = load_world(args.state, args.world)
|
||||
print(world.name)
|
||||
logger.info(f"Linking rooms {args.rooms} in world {world.name}")
|
||||
|
||||
dungeon_master = get_dungeon_master()
|
||||
systems = get_game_systems()
|
||||
|
@ -352,14 +361,16 @@ COMMAND_TABLE = {
|
|||
|
||||
def main():
|
||||
args = parse_args()
|
||||
print(args)
|
||||
logger.debug(f"running with args: {args}")
|
||||
|
||||
load_prompt_library(args)
|
||||
|
||||
# load game systems before executing commands
|
||||
systems: List[GameSystem] = []
|
||||
for system_name in args.systems or []:
|
||||
print(f"loading extra systems from {system_name}")
|
||||
logger.info(f"loading extra systems from {system_name}")
|
||||
module_systems = load_plugin(system_name)
|
||||
print(f"loaded extra systems: {module_systems}")
|
||||
logger.info(f"loaded extra systems: {module_systems}")
|
||||
systems.extend(module_systems)
|
||||
|
||||
set_game_systems(systems)
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
from functools import partial
|
||||
from itertools import count
|
||||
from logging import getLogger
|
||||
from os import path
|
||||
from typing import List
|
||||
|
||||
from packit.agent import Agent, agent_easy_connect
|
||||
from packit.memory import make_limited_memory
|
||||
|
||||
from taleweave.context import (
|
||||
get_current_turn,
|
||||
get_system_data,
|
||||
set_current_turn,
|
||||
set_dungeon_master,
|
||||
set_system_data,
|
||||
)
|
||||
from taleweave.game_system import GameSystem
|
||||
from taleweave.generate import generate_room, generate_world, link_rooms
|
||||
from taleweave.models.config import Config
|
||||
from taleweave.models.entity import World, WorldState
|
||||
from taleweave.models.files import WorldPrompt
|
||||
from taleweave.state import create_agents, save_world
|
||||
from taleweave.utils.file import load_yaml
|
||||
from taleweave.utils.template import format_prompt
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
def load_or_initialize_system_data(
|
||||
world_path: str, systems: List[GameSystem], world: World
|
||||
):
|
||||
for system in systems:
|
||||
if system.data:
|
||||
system_data_file = f"{world_path}.{system.name}.json"
|
||||
|
||||
if path.exists(system_data_file):
|
||||
logger.info(f"loading system data from {system_data_file}")
|
||||
data = system.data.load(system_data_file)
|
||||
set_system_data(system.name, data)
|
||||
continue
|
||||
else:
|
||||
logger.info(f"no system data found at {system_data_file}")
|
||||
|
||||
if system.initialize:
|
||||
logger.info(f"initializing system data for {system.name}")
|
||||
data = system.initialize(world)
|
||||
set_system_data(system.name, data)
|
||||
|
||||
|
||||
def save_system_data(world_path: str, systems: List[GameSystem]):
|
||||
for system in systems:
|
||||
if system.data:
|
||||
system_data_file = f"{world_path}.{system.name}.json"
|
||||
logger.info(f"saving system data to {system_data_file}")
|
||||
system.data.save(system_data_file, get_system_data(system.name))
|
||||
|
||||
|
||||
def load_or_generate_world(
|
||||
world_path: str,
|
||||
state_path: str | None,
|
||||
config: Config,
|
||||
players, # TODO: type me
|
||||
systems: List[GameSystem],
|
||||
world_prompt: WorldPrompt,
|
||||
add_rooms: int = 0,
|
||||
room_count: int | None = None,
|
||||
):
|
||||
world_file = world_path + ".json"
|
||||
world_state_file = state_path or (world_path + ".state.json")
|
||||
|
||||
memory = {}
|
||||
turn = 0
|
||||
|
||||
# prepare an agent for the world builder
|
||||
llm = agent_easy_connect()
|
||||
memory_factory = partial(
|
||||
make_limited_memory, limit=config.world.character.memory_limit
|
||||
)
|
||||
world_builder = Agent(
|
||||
"World Builder",
|
||||
format_prompt(
|
||||
"world_generate_dungeon_master",
|
||||
flavor=world_prompt.flavor,
|
||||
theme=world_prompt.theme,
|
||||
),
|
||||
{},
|
||||
llm,
|
||||
memory_factory=memory_factory,
|
||||
)
|
||||
set_dungeon_master(world_builder)
|
||||
|
||||
if path.exists(world_state_file):
|
||||
logger.info(f"loading world state from {world_state_file}")
|
||||
with open(world_state_file, "r") as f:
|
||||
state = WorldState(**load_yaml(f))
|
||||
|
||||
set_current_turn(state.turn)
|
||||
load_or_initialize_system_data(world_path, systems, state.world)
|
||||
|
||||
memory = state.memory
|
||||
turn = state.turn
|
||||
world = state.world
|
||||
elif path.exists(world_file):
|
||||
logger.info(f"loading world from {world_file}")
|
||||
with open(world_file, "r") as f:
|
||||
world = World(**load_yaml(f))
|
||||
|
||||
load_or_initialize_system_data(world_path, systems, world)
|
||||
else:
|
||||
logger.info(f"generating a new world using theme: {world_prompt.theme}")
|
||||
world = generate_world(
|
||||
world_builder,
|
||||
world_path,
|
||||
world_prompt.theme,
|
||||
systems,
|
||||
room_count=room_count,
|
||||
)
|
||||
load_or_initialize_system_data(world_path, systems, world)
|
||||
|
||||
# TODO: check if there have been any changes before saving
|
||||
save_world(world, world_file)
|
||||
save_system_data(world_path, systems)
|
||||
|
||||
if add_rooms:
|
||||
new_rooms = []
|
||||
for i in range(add_rooms):
|
||||
logger.info(f"generating room {i + 1} of {add_rooms}")
|
||||
room = generate_room(
|
||||
world_builder, world, systems, current_room=i, total_rooms=add_rooms
|
||||
)
|
||||
new_rooms.append(room)
|
||||
world.rooms.append(room)
|
||||
|
||||
# if the world was already full, no new rooms will be added
|
||||
if new_rooms:
|
||||
link_rooms(world_builder, world, systems, new_rooms)
|
||||
|
||||
# create agents for each character after adding any new rooms
|
||||
create_agents(world, memory=memory, players=players)
|
||||
return (world, world_state_file, turn)
|
||||
|
||||
|
||||
def simulate_world(world: World, systems: List[GameSystem], turns: int):
|
||||
# run game systems for each turn
|
||||
logger.info(f"simulating the world for {turns} turns using systems: {systems}")
|
||||
for i in count():
|
||||
current_turn = get_current_turn()
|
||||
logger.info(f"simulating turn {i} of {turns} (world turn {current_turn})")
|
||||
|
||||
for system in systems:
|
||||
if system.simulate:
|
||||
logger.info(f"running system {system.name}")
|
||||
system.simulate(world, current_turn)
|
||||
|
||||
set_current_turn(current_turn + 1)
|
||||
if i >= turns:
|
||||
logger.info("reached turn limit at world turn %s", current_turn + 1)
|
||||
break
|
|
@ -1,73 +1,67 @@
|
|||
import argparse
|
||||
import atexit
|
||||
from functools import partial
|
||||
from glob import glob
|
||||
from itertools import count
|
||||
from logging.config import dictConfig
|
||||
from os import environ, path
|
||||
from typing import List
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from packit.agent import Agent, agent_easy_connect
|
||||
from packit.memory import make_limited_memory
|
||||
from packit.utils import logger_with_colors
|
||||
|
||||
# configure logging
|
||||
# this is the only taleweave import allowed before the logger has been created
|
||||
# this is the ONLY taleweave import allowed before the logger has been created
|
||||
from taleweave.utils.file import load_yaml
|
||||
|
||||
LOG_PATH = "logging.json"
|
||||
# load environment variables before anything else
|
||||
load_dotenv(environ.get("TALEWEAVE_ENV", ".env"), override=True)
|
||||
|
||||
# configure logging
|
||||
# TODO: move this to a separate module
|
||||
|
||||
LOG_PATH = environ.get("TALEWEAVE_LOGGING", "logging.json")
|
||||
try:
|
||||
if path.exists(LOG_PATH):
|
||||
with open(LOG_PATH, "r") as f:
|
||||
config_logging = load_yaml(f)
|
||||
dictConfig(config_logging)
|
||||
else:
|
||||
print("logging config not found")
|
||||
print(f"logging config not found at {LOG_PATH}")
|
||||
|
||||
except Exception as err:
|
||||
print("error loading logging config: %s" % (err))
|
||||
print(f"error loading logging config: {err}")
|
||||
|
||||
|
||||
logger = logger_with_colors(__name__) # , level="DEBUG")
|
||||
|
||||
load_dotenv(environ.get("TALEWEAVE_ENV", ".env"), override=True)
|
||||
|
||||
# start the debugger, if needed
|
||||
if environ.get("DEBUG", "false").lower() == "true":
|
||||
if environ.get("DEBUG", "false").lower() in ["true", "1", "yes", "t", "y"]:
|
||||
logger = logger_with_colors(__name__, level="DEBUG")
|
||||
|
||||
import debugpy
|
||||
|
||||
debugpy.listen(5679)
|
||||
logger.info("waiting for debugger to attach...")
|
||||
logger.warning("waiting for debugger to attach...")
|
||||
debugpy.wait_for_client()
|
||||
else:
|
||||
logger = logger_with_colors(__name__)
|
||||
|
||||
|
||||
if True:
|
||||
from taleweave.context import (
|
||||
get_current_turn,
|
||||
get_prompt_library,
|
||||
get_system_data,
|
||||
set_current_turn,
|
||||
set_current_world,
|
||||
set_dungeon_master,
|
||||
set_extra_actions,
|
||||
set_game_config,
|
||||
set_game_systems,
|
||||
set_system_data,
|
||||
subscribe,
|
||||
)
|
||||
from taleweave.engine import load_or_generate_world, simulate_world
|
||||
from taleweave.game_system import GameSystem
|
||||
from taleweave.generate import generate_room, generate_world, link_rooms
|
||||
from taleweave.models.config import DEFAULT_CONFIG, Config
|
||||
from taleweave.models.entity import World, WorldState
|
||||
from taleweave.models.entity import World
|
||||
from taleweave.models.event import GenerateEvent
|
||||
from taleweave.models.files import TemplateFile, WorldPrompt
|
||||
from taleweave.models.prompt import PromptLibrary
|
||||
from taleweave.plugins import load_plugin
|
||||
from taleweave.state import create_agents, save_world, save_world_state
|
||||
from taleweave.state import save_world_state
|
||||
from taleweave.systems.action import init_action
|
||||
from taleweave.systems.planning import init_planning
|
||||
from taleweave.utils.template import format_prompt
|
||||
|
||||
|
||||
def int_or_inf(value: str) -> float | int:
|
||||
|
@ -82,18 +76,8 @@ def parse_args():
|
|||
parser = argparse.ArgumentParser(
|
||||
description="Generate and simulate a text adventure world"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--actions",
|
||||
type=str,
|
||||
nargs="*",
|
||||
help="Extra actions to include in the simulation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--add-rooms",
|
||||
default=0,
|
||||
type=int,
|
||||
help="The number of new rooms to generate before starting the simulation",
|
||||
)
|
||||
|
||||
# config arguments
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
|
@ -104,28 +88,11 @@ def parse_args():
|
|||
action="store_true",
|
||||
help="Whether to run the simulation in a Discord bot",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--flavor",
|
||||
type=str,
|
||||
default="",
|
||||
help="Some additional flavor text for the generated world",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--optional-actions",
|
||||
action="store_true",
|
||||
help="Whether to include optional actions in the simulation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--player",
|
||||
type=str,
|
||||
help="The name of the character to play as",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prompts",
|
||||
type=str,
|
||||
nargs="*",
|
||||
help="The file to load game prompts from",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--render",
|
||||
action="store_true",
|
||||
|
@ -136,26 +103,24 @@ def parse_args():
|
|||
action="store_true",
|
||||
help="Whether to render entities as they are generated",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rooms",
|
||||
type=int,
|
||||
help="The number of rooms to generate",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--server",
|
||||
action="store_true",
|
||||
help="Whether to run the websocket server",
|
||||
)
|
||||
|
||||
# data and plugin arguments
|
||||
parser.add_argument(
|
||||
"--state",
|
||||
"--actions",
|
||||
type=str,
|
||||
help="The file to save the world state to. Defaults to $world.state.json, if not set",
|
||||
nargs="*",
|
||||
help="Extra actions to include in the simulation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--turns",
|
||||
type=int_or_inf,
|
||||
default=10,
|
||||
help="The number of simulation turns to run",
|
||||
"--prompts",
|
||||
type=str,
|
||||
nargs="*",
|
||||
help="The file to load game prompts from",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--systems",
|
||||
|
@ -163,23 +128,57 @@ def parse_args():
|
|||
nargs="*",
|
||||
help="Extra systems to run in the simulation",
|
||||
)
|
||||
|
||||
# generation arguments
|
||||
parser.add_argument(
|
||||
"--theme",
|
||||
type=str,
|
||||
default="fantasy",
|
||||
help="The theme of the generated world",
|
||||
"--add-rooms",
|
||||
default=0,
|
||||
type=int,
|
||||
help="The number of new rooms to generate before starting the simulation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rooms",
|
||||
type=int,
|
||||
help="The number of rooms to generate",
|
||||
)
|
||||
|
||||
# simulation arguments
|
||||
parser.add_argument(
|
||||
"--turns",
|
||||
type=int_or_inf,
|
||||
default=10,
|
||||
help="The number of simulation turns to run",
|
||||
)
|
||||
|
||||
# world arguments
|
||||
parser.add_argument(
|
||||
"--world",
|
||||
type=str,
|
||||
default="world",
|
||||
help="The file to save the generated world to",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--world-flavor",
|
||||
type=str,
|
||||
default="",
|
||||
help="Some additional flavor text for the generated world",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--world-state",
|
||||
type=str,
|
||||
help="The file to save the world state to. Defaults to $world.state.json, if not set",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--world-template",
|
||||
type=str,
|
||||
help="The template file to load the world prompt from",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--world-theme",
|
||||
type=str,
|
||||
default="fantasy",
|
||||
help="The theme of the generated world",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
|
@ -196,8 +195,8 @@ def get_world_prompt(args) -> WorldPrompt:
|
|||
|
||||
return WorldPrompt(
|
||||
name=args.world,
|
||||
theme=args.theme,
|
||||
flavor=args.flavor,
|
||||
theme=args.world_theme,
|
||||
flavor=args.world_flavor,
|
||||
)
|
||||
|
||||
|
||||
|
@ -217,111 +216,6 @@ def load_prompt_library(args) -> None:
|
|||
return None
|
||||
|
||||
|
||||
def load_or_initialize_system_data(
|
||||
world_path: str, systems: List[GameSystem], world: World
|
||||
):
|
||||
for system in systems:
|
||||
if system.data:
|
||||
system_data_file = f"{world_path}.{system.name}.json"
|
||||
|
||||
if path.exists(system_data_file):
|
||||
logger.info(f"loading system data from {system_data_file}")
|
||||
data = system.data.load(system_data_file)
|
||||
set_system_data(system.name, data)
|
||||
continue
|
||||
else:
|
||||
logger.info(f"no system data found at {system_data_file}")
|
||||
|
||||
if system.initialize:
|
||||
logger.info(f"initializing system data for {system.name}")
|
||||
data = system.initialize(world)
|
||||
set_system_data(system.name, data)
|
||||
|
||||
|
||||
def save_system_data(args, systems: List[GameSystem]):
|
||||
for system in systems:
|
||||
if system.data:
|
||||
system_data_file = f"{args.world}.{system.name}.json"
|
||||
logger.info(f"saving system data to {system_data_file}")
|
||||
system.data.save(system_data_file, get_system_data(system.name))
|
||||
|
||||
|
||||
def load_or_generate_world(
|
||||
args, config: Config, players, systems: List[GameSystem], world_prompt: WorldPrompt
|
||||
):
|
||||
world_file = args.world + ".json"
|
||||
world_state_file = args.state or (args.world + ".state.json")
|
||||
add_rooms = args.add_rooms
|
||||
|
||||
memory = {}
|
||||
turn = 0
|
||||
|
||||
# prepare an agent for the world builder
|
||||
llm = agent_easy_connect()
|
||||
memory_factory = partial(
|
||||
make_limited_memory, limit=config.world.character.memory_limit
|
||||
)
|
||||
world_builder = Agent(
|
||||
"World Builder",
|
||||
format_prompt(
|
||||
"world_generate_dungeon_master",
|
||||
flavor=world_prompt.flavor,
|
||||
theme=world_prompt.theme,
|
||||
),
|
||||
{},
|
||||
llm,
|
||||
memory_factory=memory_factory,
|
||||
)
|
||||
set_dungeon_master(world_builder)
|
||||
|
||||
if path.exists(world_state_file):
|
||||
logger.info(f"loading world state from {world_state_file}")
|
||||
with open(world_state_file, "r") as f:
|
||||
state = WorldState(**load_yaml(f))
|
||||
|
||||
set_current_turn(state.turn)
|
||||
load_or_initialize_system_data(args.world, systems, state.world)
|
||||
|
||||
memory = state.memory
|
||||
turn = state.turn
|
||||
world = state.world
|
||||
elif path.exists(world_file):
|
||||
logger.info(f"loading world from {world_file}")
|
||||
with open(world_file, "r") as f:
|
||||
world = World(**load_yaml(f))
|
||||
|
||||
load_or_initialize_system_data(args.world, systems, world)
|
||||
else:
|
||||
logger.info(f"generating a new world using theme: {world_prompt.theme}")
|
||||
world = generate_world(
|
||||
world_builder,
|
||||
args.world,
|
||||
world_prompt.theme,
|
||||
systems,
|
||||
room_count=args.rooms,
|
||||
)
|
||||
load_or_initialize_system_data(args.world, systems, world)
|
||||
|
||||
# TODO: check if there have been any changes before saving
|
||||
save_world(world, world_file)
|
||||
save_system_data(args, systems)
|
||||
|
||||
new_rooms = []
|
||||
for i in range(add_rooms):
|
||||
logger.info(f"generating room {i + 1} of {add_rooms}")
|
||||
room = generate_room(
|
||||
world_builder, world, systems, current_room=i, total_rooms=add_rooms
|
||||
)
|
||||
new_rooms.append(room)
|
||||
world.rooms.append(room)
|
||||
|
||||
if new_rooms:
|
||||
link_rooms(world_builder, world, systems, new_rooms)
|
||||
|
||||
create_agents(world, memory=memory, players=players)
|
||||
return (world, world_state_file, turn)
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
|
@ -365,28 +259,13 @@ def main():
|
|||
|
||||
atexit.register(shutdown_threads)
|
||||
|
||||
# load built-in but optional actions
|
||||
extra_actions = []
|
||||
if args.optional_actions:
|
||||
logger.info("loading optional actions")
|
||||
from taleweave.actions.optional import init_optional
|
||||
|
||||
optional_actions = init_optional()
|
||||
logger.info(
|
||||
f"loaded optional actions: {[action.__name__ for action in optional_actions]}"
|
||||
)
|
||||
extra_actions.extend(optional_actions)
|
||||
|
||||
# load extra actions from plugins
|
||||
for action_name in args.actions or []:
|
||||
logger.info(f"loading extra actions from {action_name}")
|
||||
module_actions = load_plugin(action_name)
|
||||
action_group, module_actions = load_plugin(action_name)
|
||||
logger.info(
|
||||
f"loaded extra actions: {[action.__name__ for action in module_actions]}"
|
||||
f"loaded extra actions to group '{action_group}': {[action.__name__ for action in module_actions]}"
|
||||
)
|
||||
extra_actions.extend(module_actions)
|
||||
|
||||
set_extra_actions(extra_actions)
|
||||
|
||||
# set up the game systems
|
||||
systems: List[GameSystem] = []
|
||||
|
@ -411,7 +290,14 @@ def main():
|
|||
# load or generate the world
|
||||
world_prompt = get_world_prompt(args)
|
||||
world, world_state_file, world_turn = load_or_generate_world(
|
||||
args, config, players, systems, world_prompt=world_prompt
|
||||
args.world,
|
||||
args.world_state,
|
||||
config,
|
||||
players,
|
||||
systems,
|
||||
world_prompt=world_prompt,
|
||||
room_count=args.rooms,
|
||||
add_rooms=args.add_rooms,
|
||||
)
|
||||
set_current_world(world)
|
||||
|
||||
|
@ -426,21 +312,7 @@ def main():
|
|||
if args.server:
|
||||
server_system(world, world_turn)
|
||||
|
||||
# run game systems for each turn
|
||||
logger.info(f"simulating the world for {args.turns} turns using systems: {systems}")
|
||||
for i in count():
|
||||
current_turn = get_current_turn()
|
||||
logger.info(f"simulating turn {i} of {args.turns} (world turn {current_turn})")
|
||||
|
||||
for system in systems:
|
||||
if system.simulate:
|
||||
logger.info(f"running system {system.name}")
|
||||
system.simulate(world, current_turn)
|
||||
|
||||
set_current_turn(current_turn + 1)
|
||||
if i >= args.turns:
|
||||
logger.info("reached turn limit at world turn %s", current_turn + 1)
|
||||
break
|
||||
simulate_world(world, systems, args.turns)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -7,6 +7,7 @@ def load_plugin(name: str, override_function: str | None = None):
|
|||
|
||||
|
||||
def get_plugin_function(name: str, override_function: str | None = None):
|
||||
module_name, function_name = name.rsplit(":", 1)
|
||||
module_name, *rest = name.rsplit(":", 1)
|
||||
function_name = rest[0] if rest else "init"
|
||||
plugin_module = import_module(module_name)
|
||||
return getattr(plugin_module, override_function or function_name)
|
||||
|
|
|
@ -7,20 +7,12 @@ from packit.loops import loop_retry
|
|||
from packit.results import function_result
|
||||
from packit.toolbox import Toolbox
|
||||
|
||||
from taleweave.actions.base import (
|
||||
action_ask,
|
||||
action_examine,
|
||||
action_give,
|
||||
action_move,
|
||||
action_take,
|
||||
action_tell,
|
||||
)
|
||||
from taleweave.context import (
|
||||
broadcast,
|
||||
get_action_group,
|
||||
get_character_agent_for_name,
|
||||
get_character_for_agent,
|
||||
get_current_world,
|
||||
get_extra_actions,
|
||||
set_current_character,
|
||||
set_current_room,
|
||||
)
|
||||
|
@ -35,6 +27,8 @@ from taleweave.utils.world import format_attributes
|
|||
|
||||
from .planning import get_notes_events
|
||||
|
||||
ACTION_SYSTEM_NAME = "action"
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -172,18 +166,7 @@ action_tools: Toolbox | None = None
|
|||
def initialize_action(world: World):
|
||||
global action_tools
|
||||
|
||||
extra_actions = get_extra_actions()
|
||||
action_tools = Toolbox(
|
||||
[
|
||||
action_ask,
|
||||
action_give,
|
||||
action_examine,
|
||||
action_move,
|
||||
action_take,
|
||||
action_tell,
|
||||
*extra_actions,
|
||||
]
|
||||
)
|
||||
action_tools = Toolbox(get_action_group(ACTION_SYSTEM_NAME))
|
||||
|
||||
|
||||
def simulate_action(world: World, turn: int, data: Any | None = None):
|
||||
|
@ -215,5 +198,7 @@ def simulate_action(world: World, turn: int, data: Any | None = None):
|
|||
|
||||
def init_action():
|
||||
return [
|
||||
GameSystem("action", initialize=initialize_action, simulate=simulate_action)
|
||||
GameSystem(
|
||||
ACTION_SYSTEM_NAME, initialize=initialize_action, simulate=simulate_action
|
||||
)
|
||||
]
|
||||
|
|
|
@ -9,17 +9,8 @@ from packit.loops import loop_retry
|
|||
from packit.results import function_result
|
||||
from packit.toolbox import Toolbox
|
||||
|
||||
from taleweave.actions.planning import (
|
||||
check_calendar,
|
||||
edit_note,
|
||||
erase_notes,
|
||||
get_recent_notes,
|
||||
read_notes,
|
||||
schedule_event,
|
||||
summarize_notes,
|
||||
take_note,
|
||||
)
|
||||
from taleweave.context import (
|
||||
get_action_group,
|
||||
get_character_agent_for_name,
|
||||
get_current_turn,
|
||||
get_game_config,
|
||||
|
@ -31,24 +22,26 @@ from taleweave.errors import ActionError
|
|||
from taleweave.game_system import GameSystem
|
||||
from taleweave.models.entity import Character, Room, World
|
||||
from taleweave.utils.conversation import make_keyword_condition, summarize_room
|
||||
from taleweave.utils.planning import expire_events, get_upcoming_events
|
||||
from taleweave.utils.planning import (
|
||||
expire_events,
|
||||
get_recent_notes,
|
||||
get_upcoming_events,
|
||||
)
|
||||
from taleweave.utils.search import find_containing_room
|
||||
from taleweave.utils.template import format_prompt
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
PLANNING_SYSTEM_NAME = "planning"
|
||||
|
||||
# build a toolbox for the planners
|
||||
planner_toolbox = Toolbox(
|
||||
[
|
||||
check_calendar,
|
||||
erase_notes,
|
||||
read_notes,
|
||||
edit_note,
|
||||
schedule_event,
|
||||
summarize_notes,
|
||||
take_note,
|
||||
]
|
||||
)
|
||||
planning_tools: Toolbox | None = None
|
||||
|
||||
|
||||
def initialize_planning(world: World):
|
||||
global planning_tools
|
||||
|
||||
planning_tools = Toolbox(get_action_group(PLANNING_SYSTEM_NAME))
|
||||
|
||||
|
||||
def get_notes_events(character: Character, current_turn: int):
|
||||
|
@ -87,7 +80,7 @@ def prompt_character_planning(
|
|||
room: Room,
|
||||
character: Character,
|
||||
agent: Agent,
|
||||
planner_toolbox: Toolbox,
|
||||
toolbox: Toolbox,
|
||||
current_turn: int,
|
||||
max_steps: int | None = None,
|
||||
) -> str:
|
||||
|
@ -121,14 +114,14 @@ def prompt_character_planning(
|
|||
raise ActionError(
|
||||
format_prompt(
|
||||
"world_simulate_character_planning_error_unknown_tool",
|
||||
actions=planner_toolbox.list_tools(),
|
||||
actions=toolbox.list_tools(),
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise ActionError(
|
||||
format_prompt(
|
||||
"world_simulate_character_planning_error_json",
|
||||
actions=planner_toolbox.list_tools(),
|
||||
actions=toolbox.list_tools(),
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -155,7 +148,7 @@ def prompt_character_planning(
|
|||
),
|
||||
result_parser=result_parser,
|
||||
stop_condition=stop_condition,
|
||||
toolbox=planner_toolbox,
|
||||
toolbox=toolbox,
|
||||
)
|
||||
|
||||
if agent.memory:
|
||||
|
@ -189,7 +182,7 @@ def simulate_planning(world: World, turn: int, data: Any | None = None):
|
|||
if agent.memory and len(agent.memory) > 0:
|
||||
try:
|
||||
thoughts = prompt_character_planning(
|
||||
room, character, agent, planner_toolbox, turn
|
||||
room, character, agent, planning_tools, turn
|
||||
)
|
||||
logger.debug(f"{character.name} thinks: {thoughts}")
|
||||
except Exception:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from yaml import Loader, dump, load
|
||||
|
||||
# this module MUST NOT import any other taleweave modules, since it is used to initialize the logger
|
||||
|
||||
|
||||
def load_yaml(file):
|
||||
return load(file, Loader=Loader)
|
||||
|
|
Loading…
Reference in New Issue