1
0
Fork 0

split up entry points and engine components, group all actions
Run Docker Build / build (push) Successful in 12s Details
Run Python Build / build (push) Successful in 26s Details

This commit is contained in:
Sean Sube 2024-06-16 17:52:15 -05:00
parent 60fb4c94cf
commit 2f26d4e883
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
11 changed files with 382 additions and 321 deletions

View File

@ -2,6 +2,7 @@ from logging import getLogger
from taleweave.context import ( from taleweave.context import (
action_context, action_context,
add_extra_actions,
broadcast, broadcast,
get_agent_for_character, get_agent_for_character,
get_character_agent_for_name, get_character_agent_for_name,
@ -10,6 +11,7 @@ from taleweave.context import (
world_context, world_context,
) )
from taleweave.errors import ActionError from taleweave.errors import ActionError
from taleweave.systems.action import ACTION_SYSTEM_NAME
from taleweave.utils.conversation import loop_conversation from taleweave.utils.conversation import loop_conversation
from taleweave.utils.search import ( from taleweave.utils.search import (
find_character_in_room, find_character_in_room,
@ -337,3 +339,18 @@ def action_drop(item: str) -> str:
action_room.items.append(action_item) action_room.items.append(action_item)
return format_prompt("action_drop_result", item=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,
],
)

View File

@ -5,6 +5,7 @@ from packit.agent import Agent, agent_easy_connect
from taleweave.context import ( from taleweave.context import (
action_context, action_context,
add_extra_actions,
broadcast, broadcast,
get_agent_for_character, get_agent_for_character,
get_current_turn, get_current_turn,
@ -21,6 +22,7 @@ from taleweave.generate import (
generate_room, generate_room,
link_rooms, link_rooms,
) )
from taleweave.systems.action import ACTION_SYSTEM_NAME
from taleweave.utils.effect import apply_effects, is_effect_ready from taleweave.utils.effect import apply_effects, is_effect_ready
from taleweave.utils.search import find_character_in_room from taleweave.utils.search import find_character_in_room
from taleweave.utils.string import normalize_name from taleweave.utils.string import normalize_name
@ -256,8 +258,11 @@ def init_optional() -> List[Callable]:
""" """
Initialize the custom actions. Initialize the custom actions.
""" """
return [ return add_extra_actions(
ACTION_SYSTEM_NAME,
[
action_explore, action_explore,
action_search, action_search,
action_use, action_use,
] ],
)

View File

@ -1,5 +1,6 @@
from taleweave.context import ( from taleweave.context import (
action_context, action_context,
add_extra_actions,
get_agent_for_character, get_agent_for_character,
get_current_turn, get_current_turn,
get_game_config, get_game_config,
@ -7,6 +8,7 @@ from taleweave.context import (
) )
from taleweave.errors import ActionError from taleweave.errors import ActionError
from taleweave.models.planning import CalendarEvent 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.planning import get_recent_notes
from taleweave.utils.template import format_prompt from taleweave.utils.template import format_prompt
@ -194,3 +196,18 @@ def check_calendar(count: int):
for event in events 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,
],
)

View File

@ -34,10 +34,10 @@ current_character: Character | None = None
dungeon_master: Agent | None = None dungeon_master: Agent | None = None
# game context # 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]] = {} character_agents: Dict[str, Tuple[Character, Agent]] = {}
event_emitter = EventEmitter() event_emitter = EventEmitter()
extra_actions: List[Callable[..., str]] = []
game_config: Config = DEFAULT_CONFIG game_config: Config = DEFAULT_CONFIG
game_systems: List[GameSystem] = [] game_systems: List[GameSystem] = []
prompt_library: PromptLibrary = PromptLibrary(prompts={}) prompt_library: PromptLibrary = PromptLibrary(prompts={})
@ -186,8 +186,8 @@ def get_system_data(system: str) -> Any | None:
return system_data.get(system) return system_data.get(system)
def get_extra_actions() -> List[Callable[..., str]]: def get_action_group(name: str) -> List[Callable[..., str]]:
return extra_actions return action_groups.get(name, [])
# endregion # endregion
@ -242,9 +242,9 @@ def set_system_data(system: str, data: Any):
system_data[system] = data system_data[system] = data
def set_extra_actions(actions: List[Callable[..., str]]): def add_extra_actions(group: str, actions: List[Callable[..., str]]):
global extra_actions action_groups.setdefault(group, []).extend(actions)
extra_actions = actions return group, actions
# endregion # endregion

View File

@ -1,8 +1,12 @@
import argparse import argparse
from os import path from os import environ, path
from typing import List, Tuple 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.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.game_system import GameSystem
from taleweave.generate import ( from taleweave.generate import (
generate_character, generate_character,
@ -11,7 +15,7 @@ from taleweave.generate import (
generate_room, generate_room,
link_rooms, 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.base import dump_model
from taleweave.models.entity import World, WorldState from taleweave.models.entity import World, WorldState
from taleweave.plugins import load_plugin from taleweave.plugins import load_plugin
@ -30,9 +34,16 @@ from taleweave.utils.world import describe_entity
ENTITY_TYPES = ["room", "portal", "item", "character"] 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(): def parse_args():
parser = argparse.ArgumentParser(description="Taleweave Editor") 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("--state", type=str, help="State file to edit")
parser.add_argument("--world", type=str, help="World 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") parser.add_argument("--systems", type=str, nargs="*", help="Game systems to load")
@ -41,11 +52,9 @@ def parse_args():
subparsers.required = True subparsers.required = True
# Set up the 'list' command # Set up the 'list' command
list_parser = subparsers.add_parser( list_parser = subparsers.add_parser("list", help="List entities of a specific type")
"list", help="List all entities or entities of a specific type"
)
list_parser.add_argument( 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 # Set up the 'describe' command
@ -107,7 +116,7 @@ def parse_args():
def load_world(state_file, world_file) -> Tuple[World, WorldState | None]: def load_world(state_file, world_file) -> Tuple[World, WorldState | None]:
systems = [] systems = get_game_systems()
if state_file and path.exists(state_file): if state_file and path.exists(state_file):
with open(state_file, "r") as f: 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): 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: if state:
print(f"Saving world {world.name} to {state_file}") logger.warning(f"Saving world {world.name} to {state_file}")
return return
with open(state_file, "w") as f: with open(state_file, "w") as f:
save_yaml(f, dump_model(WorldState, state)) save_yaml(f, dump_model(WorldState, state))
else: else:
print(f"Saving world {world.name} to {world_file}") logger.warning(f"Saving world {world.name} to {world_file}")
return return
with open(world_file, "w") as f: 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): def command_list(args):
print(f"Listing {args.type}s")
world, _ = load_world(args.state, args.world) 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": if args.type == "room":
for room in list_rooms(world): for room in list_rooms(world):
print(room.name) logger.info(room.name)
if args.type == "portal": if args.type == "portal":
for portal in list_portals(world): for portal in list_portals(world):
print(portal.name) logger.info(portal.name)
if args.type == "item": if args.type == "item":
for item in list_items( for item in list_items(
world, include_character_inventory=True, include_item_inventory=True world, include_character_inventory=True, include_item_inventory=True
): ):
print(item.name) logger.info(item.name)
if args.type == "character": if args.type == "character":
for character in list_characters(world): for character in list_characters(world):
print(character.name) logger.info(character.name)
def command_describe(args): def command_describe(args):
print(f"Describing {args.entity}")
world, _ = load_world(args.state, args.world) world, _ = load_world(args.state, args.world)
print(world.name) logger.info(f"Describing {args.entity} from world {world.name}")
if args.type == "room": if args.type == "room":
room = find_room(world, args.entity) room = find_room(world, args.entity)
if not room: if not room:
print(f"Room {args.entity} not found") logger.error(f"Room {args.entity} not found")
else: else:
print(describe_entity(room)) logger.info(describe_entity(room))
if args.type == "portal": if args.type == "portal":
portal = find_portal(world, args.entity) portal = find_portal(world, args.entity)
if not portal: if not portal:
print(f"Portal {args.entity} not found") logger.error(f"Portal {args.entity} not found")
else: else:
print(describe_entity(portal)) logger.info(describe_entity(portal))
if args.type == "item": if args.type == "item":
item = find_item( item = find_item(
@ -194,22 +206,21 @@ def command_describe(args):
include_item_inventory=True, include_item_inventory=True,
) )
if not item: if not item:
print(f"Item {args.entity} not found") logger.error(f"Item {args.entity} not found")
else: else:
print(describe_entity(item)) logger.info(describe_entity(item))
if args.type == "character": if args.type == "character":
character = find_character(world, args.entity) character = find_character(world, args.entity)
if not character: if not character:
print(f"Character {args.entity} not found") logger.error(f"Character {args.entity} not found")
else: else:
print(describe_entity(character)) logger.info(describe_entity(character))
def command_create(args): def command_create(args):
print(f"Create {args.type} named {args.name}")
world, state = load_world(args.state, args.world) 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 # TODO: Create the entity
@ -217,9 +228,10 @@ def command_create(args):
def command_generate(args): def command_generate(args):
print(f"Generate {args.type} with prompt: {args.prompt}")
world, state = load_world(args.state, args.world) 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() dungeon_master = get_dungeon_master()
systems = get_game_systems() systems = get_game_systems()
@ -231,12 +243,12 @@ def command_generate(args):
if args.type == "portal": if args.type == "portal":
source_room = find_room(world, args.room) source_room = find_room(world, args.room)
if not source_room: if not source_room:
print(f"Room {args.room} not found") logger.error(f"Room {args.room} not found")
return return
destination_room = find_room(world, args.dest_room) destination_room = find_room(world, args.dest_room)
if not destination_room: if not destination_room:
print(f"Room {args.dest_room} not found") logger.error(f"Room {args.dest_room} not found")
return return
outgoing_portal, incoming_portal = generate_portals( outgoing_portal, incoming_portal = generate_portals(
@ -249,7 +261,7 @@ def command_generate(args):
# TODO: add item to character or container inventory # TODO: add item to character or container inventory
room = find_room(world, args.room) room = find_room(world, args.room)
if not room: if not room:
print(f"Room {args.room} not found") logger.error(f"Room {args.room} not found")
return return
item = generate_item(dungeon_master, world, systems) item = generate_item(dungeon_master, world, systems)
@ -258,7 +270,7 @@ def command_generate(args):
if args.type == "character": if args.type == "character":
room = find_room(world, args.room) room = find_room(world, args.room)
if not room: if not room:
print(f"Room {args.room} not found") logger.error(f"Room {args.room} not found")
return return
character = generate_character( character = generate_character(
@ -270,9 +282,8 @@ def command_generate(args):
def command_delete(args): def command_delete(args):
print(f"Delete {args.entity}")
world, state = load_world(args.state, args.world) 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 # TODO: Delete the entity
@ -280,23 +291,22 @@ def command_delete(args):
def command_update(args): def command_update(args):
print(f"Update {args.entity}")
world, state = load_world(args.state, args.world) 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": if args.type == "room":
room = find_room(world, args.entity) room = find_room(world, args.entity)
if not room: if not room:
print(f"Room {args.entity} not found") logger.error(f"Room {args.entity} not found")
else: else:
print(describe_entity(room)) logger.info(describe_entity(room))
if args.type == "portal": if args.type == "portal":
portal = find_portal(world, args.entity) portal = find_portal(world, args.entity)
if not portal: if not portal:
print(f"Portal {args.entity} not found") logger.error(f"Portal {args.entity} not found")
else: else:
print(describe_entity(portal)) logger.info(describe_entity(portal))
if args.type == "item": if args.type == "item":
item = find_item( item = find_item(
@ -306,14 +316,14 @@ def command_update(args):
include_item_inventory=True, include_item_inventory=True,
) )
if not item: if not item:
print(f"Item {args.entity} not found") logger.error(f"Item {args.entity} not found")
else: else:
print(describe_entity(item)) logger.info(describe_entity(item))
if args.type == "character": if args.type == "character":
character = find_character(world, args.entity) character = find_character(world, args.entity)
if not character: if not character:
print(f"Character {args.entity} not found") logger.error(f"Character {args.entity} not found")
else: else:
if args.backstory: if args.backstory:
character.backstory = args.backstory character.backstory = args.backstory
@ -321,15 +331,14 @@ def command_update(args):
if args.description: if args.description:
character.description = args.description character.description = args.description
print(describe_entity(character)) logger.info(describe_entity(character))
save_world(args.state, args.world, world, state) save_world(args.state, args.world, world, state)
def command_link(args): def command_link(args):
print(f"Linking rooms {args.rooms}")
world, state = load_world(args.state, args.world) 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() dungeon_master = get_dungeon_master()
systems = get_game_systems() systems = get_game_systems()
@ -352,14 +361,16 @@ COMMAND_TABLE = {
def main(): def main():
args = parse_args() args = parse_args()
print(args) logger.debug(f"running with args: {args}")
load_prompt_library(args)
# load game systems before executing commands # load game systems before executing commands
systems: List[GameSystem] = [] systems: List[GameSystem] = []
for system_name in args.systems or []: 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) 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) systems.extend(module_systems)
set_game_systems(systems) set_game_systems(systems)

158
taleweave/engine.py Normal file
View File

@ -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

View File

@ -1,73 +1,67 @@
import argparse import argparse
import atexit import atexit
from functools import partial
from glob import glob from glob import glob
from itertools import count
from logging.config import dictConfig from logging.config import dictConfig
from os import environ, path from os import environ, path
from typing import List from typing import List
from dotenv import load_dotenv 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 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 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: try:
if path.exists(LOG_PATH): if path.exists(LOG_PATH):
with open(LOG_PATH, "r") as f: with open(LOG_PATH, "r") as f:
config_logging = load_yaml(f) config_logging = load_yaml(f)
dictConfig(config_logging) dictConfig(config_logging)
else: else:
print("logging config not found") print(f"logging config not found at {LOG_PATH}")
except Exception as err: 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 # 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 import debugpy
debugpy.listen(5679) debugpy.listen(5679)
logger.info("waiting for debugger to attach...") logger.warning("waiting for debugger to attach...")
debugpy.wait_for_client() debugpy.wait_for_client()
else:
logger = logger_with_colors(__name__)
if True: if True:
from taleweave.context import ( from taleweave.context import (
get_current_turn,
get_prompt_library, get_prompt_library,
get_system_data,
set_current_turn,
set_current_world, set_current_world,
set_dungeon_master,
set_extra_actions,
set_game_config, set_game_config,
set_game_systems, set_game_systems,
set_system_data,
subscribe, subscribe,
) )
from taleweave.engine import load_or_generate_world, simulate_world
from taleweave.game_system import GameSystem 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.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.event import GenerateEvent
from taleweave.models.files import TemplateFile, WorldPrompt from taleweave.models.files import TemplateFile, WorldPrompt
from taleweave.models.prompt import PromptLibrary from taleweave.models.prompt import PromptLibrary
from taleweave.plugins import load_plugin 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.action import init_action
from taleweave.systems.planning import init_planning from taleweave.systems.planning import init_planning
from taleweave.utils.template import format_prompt
def int_or_inf(value: str) -> float | int: def int_or_inf(value: str) -> float | int:
@ -82,18 +76,8 @@ def parse_args():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Generate and simulate a text adventure world" description="Generate and simulate a text adventure world"
) )
parser.add_argument(
"--actions", # config arguments
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",
)
parser.add_argument( parser.add_argument(
"--config", "--config",
type=str, type=str,
@ -104,28 +88,11 @@ def parse_args():
action="store_true", action="store_true",
help="Whether to run the simulation in a Discord bot", 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( parser.add_argument(
"--player", "--player",
type=str, type=str,
help="The name of the character to play as", 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( parser.add_argument(
"--render", "--render",
action="store_true", action="store_true",
@ -136,26 +103,24 @@ def parse_args():
action="store_true", action="store_true",
help="Whether to render entities as they are generated", 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( parser.add_argument(
"--server", "--server",
action="store_true", action="store_true",
help="Whether to run the websocket server", help="Whether to run the websocket server",
) )
# data and plugin arguments
parser.add_argument( parser.add_argument(
"--state", "--actions",
type=str, 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( parser.add_argument(
"--turns", "--prompts",
type=int_or_inf, type=str,
default=10, nargs="*",
help="The number of simulation turns to run", help="The file to load game prompts from",
) )
parser.add_argument( parser.add_argument(
"--systems", "--systems",
@ -163,23 +128,57 @@ def parse_args():
nargs="*", nargs="*",
help="Extra systems to run in the simulation", help="Extra systems to run in the simulation",
) )
# generation arguments
parser.add_argument( parser.add_argument(
"--theme", "--add-rooms",
type=str, default=0,
default="fantasy", type=int,
help="The theme of the generated world", 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( parser.add_argument(
"--world", "--world",
type=str, type=str,
default="world", default="world",
help="The file to save the generated world to", 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( parser.add_argument(
"--world-template", "--world-template",
type=str, type=str,
help="The template file to load the world prompt from", 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() return parser.parse_args()
@ -196,8 +195,8 @@ def get_world_prompt(args) -> WorldPrompt:
return WorldPrompt( return WorldPrompt(
name=args.world, name=args.world,
theme=args.theme, theme=args.world_theme,
flavor=args.flavor, flavor=args.world_flavor,
) )
@ -217,111 +216,6 @@ def load_prompt_library(args) -> None:
return 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(): def main():
args = parse_args() args = parse_args()
@ -365,28 +259,13 @@ def main():
atexit.register(shutdown_threads) 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 # load extra actions from plugins
for action_name in args.actions or []: for action_name in args.actions or []:
logger.info(f"loading extra actions from {action_name}") 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( 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 # set up the game systems
systems: List[GameSystem] = [] systems: List[GameSystem] = []
@ -411,7 +290,14 @@ def main():
# load or generate the world # load or generate the world
world_prompt = get_world_prompt(args) world_prompt = get_world_prompt(args)
world, world_state_file, world_turn = load_or_generate_world( 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) set_current_world(world)
@ -426,21 +312,7 @@ def main():
if args.server: if args.server:
server_system(world, world_turn) server_system(world, world_turn)
# run game systems for each turn simulate_world(world, systems, args.turns)
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
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -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): 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) plugin_module = import_module(module_name)
return getattr(plugin_module, override_function or function_name) return getattr(plugin_module, override_function or function_name)

View File

@ -7,20 +7,12 @@ from packit.loops import loop_retry
from packit.results import function_result from packit.results import function_result
from packit.toolbox import Toolbox 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 ( from taleweave.context import (
broadcast, broadcast,
get_action_group,
get_character_agent_for_name, get_character_agent_for_name,
get_character_for_agent, get_character_for_agent,
get_current_world, get_current_world,
get_extra_actions,
set_current_character, set_current_character,
set_current_room, set_current_room,
) )
@ -35,6 +27,8 @@ from taleweave.utils.world import format_attributes
from .planning import get_notes_events from .planning import get_notes_events
ACTION_SYSTEM_NAME = "action"
logger = getLogger(__name__) logger = getLogger(__name__)
@ -172,18 +166,7 @@ action_tools: Toolbox | None = None
def initialize_action(world: World): def initialize_action(world: World):
global action_tools global action_tools
extra_actions = get_extra_actions() action_tools = Toolbox(get_action_group(ACTION_SYSTEM_NAME))
action_tools = Toolbox(
[
action_ask,
action_give,
action_examine,
action_move,
action_take,
action_tell,
*extra_actions,
]
)
def simulate_action(world: World, turn: int, data: Any | None = None): 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(): def init_action():
return [ return [
GameSystem("action", initialize=initialize_action, simulate=simulate_action) GameSystem(
ACTION_SYSTEM_NAME, initialize=initialize_action, simulate=simulate_action
)
] ]

View File

@ -9,17 +9,8 @@ from packit.loops import loop_retry
from packit.results import function_result from packit.results import function_result
from packit.toolbox import Toolbox 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 ( from taleweave.context import (
get_action_group,
get_character_agent_for_name, get_character_agent_for_name,
get_current_turn, get_current_turn,
get_game_config, get_game_config,
@ -31,24 +22,26 @@ from taleweave.errors import ActionError
from taleweave.game_system import GameSystem from taleweave.game_system import GameSystem
from taleweave.models.entity import Character, Room, World from taleweave.models.entity import Character, Room, World
from taleweave.utils.conversation import make_keyword_condition, summarize_room 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.search import find_containing_room
from taleweave.utils.template import format_prompt from taleweave.utils.template import format_prompt
logger = getLogger(__name__) logger = getLogger(__name__)
PLANNING_SYSTEM_NAME = "planning"
# build a toolbox for the planners # build a toolbox for the planners
planner_toolbox = Toolbox( planning_tools: Toolbox | None = None
[
check_calendar,
erase_notes, def initialize_planning(world: World):
read_notes, global planning_tools
edit_note,
schedule_event, planning_tools = Toolbox(get_action_group(PLANNING_SYSTEM_NAME))
summarize_notes,
take_note,
]
)
def get_notes_events(character: Character, current_turn: int): def get_notes_events(character: Character, current_turn: int):
@ -87,7 +80,7 @@ def prompt_character_planning(
room: Room, room: Room,
character: Character, character: Character,
agent: Agent, agent: Agent,
planner_toolbox: Toolbox, toolbox: Toolbox,
current_turn: int, current_turn: int,
max_steps: int | None = None, max_steps: int | None = None,
) -> str: ) -> str:
@ -121,14 +114,14 @@ def prompt_character_planning(
raise ActionError( raise ActionError(
format_prompt( format_prompt(
"world_simulate_character_planning_error_unknown_tool", "world_simulate_character_planning_error_unknown_tool",
actions=planner_toolbox.list_tools(), actions=toolbox.list_tools(),
) )
) )
else: else:
raise ActionError( raise ActionError(
format_prompt( format_prompt(
"world_simulate_character_planning_error_json", "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, result_parser=result_parser,
stop_condition=stop_condition, stop_condition=stop_condition,
toolbox=planner_toolbox, toolbox=toolbox,
) )
if agent.memory: 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: if agent.memory and len(agent.memory) > 0:
try: try:
thoughts = prompt_character_planning( thoughts = prompt_character_planning(
room, character, agent, planner_toolbox, turn room, character, agent, planning_tools, turn
) )
logger.debug(f"{character.name} thinks: {thoughts}") logger.debug(f"{character.name} thinks: {thoughts}")
except Exception: except Exception:

View File

@ -1,5 +1,7 @@
from yaml import Loader, dump, load 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): def load_yaml(file):
return load(file, Loader=Loader) return load(file, Loader=Loader)