diff --git a/taleweave/actions/base.py b/taleweave/actions/base.py index 6bef3c8..bb98231 100644 --- a/taleweave/actions/base.py +++ b/taleweave/actions/base.py @@ -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, + ], + ) diff --git a/taleweave/actions/optional.py b/taleweave/actions/optional.py index 4a27e1a..e1c39f4 100644 --- a/taleweave/actions/optional.py +++ b/taleweave/actions/optional.py @@ -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, + ], + ) diff --git a/taleweave/actions/planning.py b/taleweave/actions/planning.py index 5321193..f44cbce 100644 --- a/taleweave/actions/planning.py +++ b/taleweave/actions/planning.py @@ -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, + ], + ) diff --git a/taleweave/context.py b/taleweave/context.py index 970d4c3..3e13ac4 100644 --- a/taleweave/context.py +++ b/taleweave/context.py @@ -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 diff --git a/taleweave/editor.py b/taleweave/editor.py index 1b6b37c..ca3a400 100644 --- a/taleweave/editor.py +++ b/taleweave/editor.py @@ -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) diff --git a/taleweave/engine.py b/taleweave/engine.py new file mode 100644 index 0000000..5899dea --- /dev/null +++ b/taleweave/engine.py @@ -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 diff --git a/taleweave/main.py b/taleweave/main.py index 3083212..37dc5ad 100644 --- a/taleweave/main.py +++ b/taleweave/main.py @@ -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__": diff --git a/taleweave/plugins.py b/taleweave/plugins.py index fab5687..965952e 100644 --- a/taleweave/plugins.py +++ b/taleweave/plugins.py @@ -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) diff --git a/taleweave/systems/action.py b/taleweave/systems/action.py index e023cdf..bd95d35 100644 --- a/taleweave/systems/action.py +++ b/taleweave/systems/action.py @@ -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 + ) ] diff --git a/taleweave/systems/planning.py b/taleweave/systems/planning.py index 6178708..3868ac0 100644 --- a/taleweave/systems/planning.py +++ b/taleweave/systems/planning.py @@ -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: diff --git a/taleweave/utils/file.py b/taleweave/utils/file.py index 0dc6a6e..2630c4e 100644 --- a/taleweave/utils/file.py +++ b/taleweave/utils/file.py @@ -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)