split up simulation into planning and action systems
This commit is contained in:
parent
d0ddeca211
commit
3d833c683f
8
Makefile
8
Makefile
|
@ -36,16 +36,16 @@ lint-check:
|
||||||
black --check tests/
|
black --check tests/
|
||||||
flake8 taleweave
|
flake8 taleweave
|
||||||
flake8 tests
|
flake8 tests
|
||||||
isort --check-only --skip __init__.py --filter-files taleweave
|
isort --check-only --filter-files taleweave
|
||||||
isort --check-only --skip __init__.py --filter-files tests
|
isort --check-only --filter-files tests
|
||||||
|
|
||||||
lint-fix:
|
lint-fix:
|
||||||
black taleweave/
|
black taleweave/
|
||||||
black tests/
|
black tests/
|
||||||
flake8 taleweave
|
flake8 taleweave
|
||||||
flake8 tests
|
flake8 tests
|
||||||
isort --skip __init__.py --filter-files taleweave
|
isort --filter-files taleweave
|
||||||
isort --skip __init__.py --filter-files tests
|
isort --filter-files tests
|
||||||
|
|
||||||
style: lint-fix
|
style: lint-fix
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,5 @@
|
||||||
prompts:
|
prompts:
|
||||||
# digest system
|
# action digest
|
||||||
digest_action_move_other_enter: |
|
|
||||||
{{event.character | name}} entered the room through the {{source_portal | name}}.
|
|
||||||
digest_action_move_other_exit: |
|
|
||||||
{{event.character | name}} left the room, heading through the {{destination_portal | name}}.
|
|
||||||
digest_action_move_self_enter: |
|
|
||||||
You entered the room through the {{source_portal | name}}.
|
|
||||||
digest_action_move_self_exit: |
|
|
||||||
You left the room, heading through the {{destination_portal | name}}.
|
|
||||||
digest_action_move: |
|
digest_action_move: |
|
||||||
{{event.character | name}} entered the room.
|
{{event.character | name}} entered the room.
|
||||||
digest_action_take: |
|
digest_action_take: |
|
||||||
|
@ -22,3 +14,13 @@ prompts:
|
||||||
{{event.character | name}} told {{event.parameters[character]}} about something.
|
{{event.character | name}} told {{event.parameters[character]}} about something.
|
||||||
digest_action_examine: |
|
digest_action_examine: |
|
||||||
{{event.character | name}} examined the {{event.parameters[target]}}.
|
{{event.character | name}} examined the {{event.parameters[target]}}.
|
||||||
|
|
||||||
|
# movement digest
|
||||||
|
digest_move_other_enter: |
|
||||||
|
{{event.character | name}} entered the room through the {{source_portal | name}}.
|
||||||
|
digest_move_other_exit: |
|
||||||
|
{{event.character | name}} left the room, heading through the {{destination_portal | name}}.
|
||||||
|
digest_move_self_enter: |
|
||||||
|
You entered the room through the {{source_portal | name}}.
|
||||||
|
digest_move_self_exit: |
|
||||||
|
You left the room, heading through the {{destination_portal | name}}.
|
|
@ -252,7 +252,7 @@ def action_use(item: str, target: str) -> str:
|
||||||
return outcome
|
return outcome
|
||||||
|
|
||||||
|
|
||||||
def init() -> List[Callable]:
|
def init_optional() -> List[Callable]:
|
||||||
"""
|
"""
|
||||||
Initialize the custom actions.
|
Initialize the custom actions.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -37,6 +37,7 @@ dungeon_master: Agent | None = None
|
||||||
# TODO: wrap this into a class that can be passed around
|
# TODO: wrap this into a class that can be passed around
|
||||||
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={})
|
||||||
|
@ -185,6 +186,10 @@ 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]]:
|
||||||
|
return extra_actions
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
@ -237,6 +242,11 @@ def set_system_data(system: str, data: Any):
|
||||||
system_data[system] = data
|
system_data[system] = data
|
||||||
|
|
||||||
|
|
||||||
|
def set_extra_actions(actions: List[Callable[..., str]]):
|
||||||
|
global extra_actions
|
||||||
|
extra_actions = actions
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ from taleweave.generate import (
|
||||||
generate_item,
|
generate_item,
|
||||||
generate_portals,
|
generate_portals,
|
||||||
generate_room,
|
generate_room,
|
||||||
|
link_rooms,
|
||||||
)
|
)
|
||||||
from taleweave.main import load_or_initialize_system_data
|
from taleweave.main import load_or_initialize_system_data
|
||||||
from taleweave.models.base import dump_model
|
from taleweave.models.base import dump_model
|
||||||
|
@ -27,7 +28,6 @@ from taleweave.utils.search import (
|
||||||
)
|
)
|
||||||
from taleweave.utils.world import describe_entity
|
from taleweave.utils.world import describe_entity
|
||||||
|
|
||||||
|
|
||||||
ENTITY_TYPES = ["room", "portal", "item", "character"]
|
ENTITY_TYPES = ["room", "portal", "item", "character"]
|
||||||
|
|
||||||
|
|
||||||
|
@ -72,6 +72,9 @@ def parse_args():
|
||||||
"prompt", type=str, help="Prompt to generate the entity"
|
"prompt", type=str, help="Prompt to generate the entity"
|
||||||
)
|
)
|
||||||
generate_parser.add_argument("--room", type=str, help="Room the entity is in")
|
generate_parser.add_argument("--room", type=str, help="Room the entity is in")
|
||||||
|
generate_parser.add_argument(
|
||||||
|
"--dest-room", type=str, help="Destination room for portals"
|
||||||
|
)
|
||||||
|
|
||||||
# Set up the 'delete' command
|
# Set up the 'delete' command
|
||||||
delete_parser = subparsers.add_parser("delete", help="Delete an entity")
|
delete_parser = subparsers.add_parser("delete", help="Delete an entity")
|
||||||
|
@ -91,6 +94,15 @@ def parse_args():
|
||||||
"--description", type=str, help="Description of the entity"
|
"--description", type=str, help="Description of the entity"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Set up the 'link' command
|
||||||
|
link_parser = subparsers.add_parser("link", help="Link rooms")
|
||||||
|
link_parser.add_argument(
|
||||||
|
"rooms",
|
||||||
|
type=str,
|
||||||
|
nargs="*",
|
||||||
|
help="Rooms to link. Leave blank to link all rooms.",
|
||||||
|
)
|
||||||
|
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
@ -212,24 +224,47 @@ def command_generate(args):
|
||||||
dungeon_master = get_dungeon_master()
|
dungeon_master = get_dungeon_master()
|
||||||
systems = get_game_systems()
|
systems = get_game_systems()
|
||||||
|
|
||||||
# TODO: Generate the entity
|
|
||||||
if args.type == "room":
|
if args.type == "room":
|
||||||
room = generate_room(dungeon_master, world, systems)
|
room = generate_room(dungeon_master, world, systems)
|
||||||
world.rooms.append(room)
|
world.rooms.append(room)
|
||||||
|
|
||||||
if args.type == "portal":
|
if args.type == "portal":
|
||||||
portal = generate_portals(dungeon_master, world, "TODO", "TODO", systems)
|
source_room = find_room(world, args.room)
|
||||||
# TODO: Add portal to room and generate reverse portal from destination room
|
if not source_room:
|
||||||
|
print(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")
|
||||||
|
return
|
||||||
|
|
||||||
|
outgoing_portal, incoming_portal = generate_portals(
|
||||||
|
dungeon_master, world, source_room, destination_room, systems
|
||||||
|
)
|
||||||
|
source_room.portals.append(outgoing_portal)
|
||||||
|
destination_room.portals.append(incoming_portal)
|
||||||
|
|
||||||
if args.type == "item":
|
if args.type == "item":
|
||||||
|
# TODO: add item to character or container inventory
|
||||||
|
room = find_room(world, args.room)
|
||||||
|
if not room:
|
||||||
|
print(f"Room {args.room} not found")
|
||||||
|
return
|
||||||
|
|
||||||
item = generate_item(dungeon_master, world, systems)
|
item = generate_item(dungeon_master, world, systems)
|
||||||
# TODO: Add item to room or character inventory
|
room.items.append(item)
|
||||||
|
|
||||||
if args.type == "character":
|
if args.type == "character":
|
||||||
|
room = find_room(world, args.room)
|
||||||
|
if not room:
|
||||||
|
print(f"Room {args.room} not found")
|
||||||
|
return
|
||||||
|
|
||||||
character = generate_character(
|
character = generate_character(
|
||||||
dungeon_master, world, systems, "TODO", args.prompt
|
dungeon_master, world, systems, room, args.prompt
|
||||||
)
|
)
|
||||||
# TODO: Add character to room
|
room.characters.append(character)
|
||||||
|
|
||||||
save_world(args.state, args.world, world, state)
|
save_world(args.state, args.world, world, state)
|
||||||
|
|
||||||
|
@ -291,6 +326,19 @@ def command_update(args):
|
||||||
save_world(args.state, args.world, world, state)
|
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)
|
||||||
|
|
||||||
|
dungeon_master = get_dungeon_master()
|
||||||
|
systems = get_game_systems()
|
||||||
|
|
||||||
|
link_rooms(dungeon_master, world, systems)
|
||||||
|
|
||||||
|
save_world(args.state, args.world, world, state)
|
||||||
|
|
||||||
|
|
||||||
COMMAND_TABLE = {
|
COMMAND_TABLE = {
|
||||||
"list": command_list,
|
"list": command_list,
|
||||||
"describe": command_describe,
|
"describe": command_describe,
|
||||||
|
@ -298,6 +346,7 @@ COMMAND_TABLE = {
|
||||||
"generate": command_generate,
|
"generate": command_generate,
|
||||||
"delete": command_delete,
|
"delete": command_delete,
|
||||||
"update": command_update,
|
"update": command_update,
|
||||||
|
"link": command_link,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
import argparse
|
||||||
import atexit
|
import atexit
|
||||||
from functools import partial
|
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
|
||||||
|
@ -42,11 +44,15 @@ if environ.get("DEBUG", "false").lower() == "true":
|
||||||
|
|
||||||
if True:
|
if True:
|
||||||
from taleweave.context import (
|
from taleweave.context import (
|
||||||
|
get_current_turn,
|
||||||
get_prompt_library,
|
get_prompt_library,
|
||||||
get_system_data,
|
get_system_data,
|
||||||
set_current_turn,
|
set_current_turn,
|
||||||
|
set_current_world,
|
||||||
set_dungeon_master,
|
set_dungeon_master,
|
||||||
|
set_extra_actions,
|
||||||
set_game_config,
|
set_game_config,
|
||||||
|
set_game_systems,
|
||||||
set_system_data,
|
set_system_data,
|
||||||
subscribe,
|
subscribe,
|
||||||
)
|
)
|
||||||
|
@ -58,8 +64,9 @@ if True:
|
||||||
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.simulate import simulate_world
|
|
||||||
from taleweave.state import create_agents, save_world, save_world_state
|
from taleweave.state import create_agents, save_world, save_world_state
|
||||||
|
from taleweave.systems.action import init_action
|
||||||
|
from taleweave.systems.planning import init_planning
|
||||||
from taleweave.utils.template import format_prompt
|
from taleweave.utils.template import format_prompt
|
||||||
|
|
||||||
|
|
||||||
|
@ -72,8 +79,6 @@ def int_or_inf(value: str) -> float | int:
|
||||||
|
|
||||||
# main
|
# main
|
||||||
def parse_args():
|
def parse_args():
|
||||||
import argparse
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Generate and simulate a text adventure world"
|
description="Generate and simulate a text adventure world"
|
||||||
)
|
)
|
||||||
|
@ -364,9 +369,9 @@ def main():
|
||||||
extra_actions = []
|
extra_actions = []
|
||||||
if args.optional_actions:
|
if args.optional_actions:
|
||||||
logger.info("loading optional actions")
|
logger.info("loading optional actions")
|
||||||
from taleweave.actions.optional import init as init_optional_actions
|
from taleweave.actions.optional import init_optional
|
||||||
|
|
||||||
optional_actions = init_optional_actions()
|
optional_actions = init_optional()
|
||||||
logger.info(
|
logger.info(
|
||||||
f"loaded optional actions: {[action.__name__ for action in optional_actions]}"
|
f"loaded optional actions: {[action.__name__ for action in optional_actions]}"
|
||||||
)
|
)
|
||||||
|
@ -381,63 +386,61 @@ def main():
|
||||||
)
|
)
|
||||||
extra_actions.extend(module_actions)
|
extra_actions.extend(module_actions)
|
||||||
|
|
||||||
|
set_extra_actions(extra_actions)
|
||||||
|
|
||||||
|
# set up the game systems
|
||||||
|
systems: List[GameSystem] = []
|
||||||
|
systems.extend(init_planning())
|
||||||
|
systems.extend(init_action())
|
||||||
|
|
||||||
# load extra systems from plugins
|
# load extra systems from plugins
|
||||||
extra_systems: List[GameSystem] = []
|
|
||||||
for system_name in args.systems or []:
|
for system_name in args.systems or []:
|
||||||
logger.info(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)
|
||||||
logger.info(f"loaded extra systems: {module_systems}")
|
logger.info(f"loaded extra systems: {module_systems}")
|
||||||
extra_systems.extend(module_systems)
|
systems.extend(module_systems)
|
||||||
|
|
||||||
# make sure the server system runs after any updates
|
# make sure the server system runs after any updates
|
||||||
if args.server:
|
if args.server:
|
||||||
from taleweave.server.websocket import server_system
|
from taleweave.server.websocket import server_system
|
||||||
|
|
||||||
extra_systems.append(GameSystem(name="server", simulate=server_system))
|
systems.append(GameSystem(name="server", simulate=server_system))
|
||||||
|
|
||||||
|
set_game_systems(systems)
|
||||||
|
|
||||||
# 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, extra_systems, world_prompt=world_prompt
|
args, config, players, systems, world_prompt=world_prompt
|
||||||
)
|
)
|
||||||
|
set_current_world(world)
|
||||||
|
|
||||||
# make sure the snapshot system runs last
|
# make sure the snapshot system runs last
|
||||||
def snapshot_system(world: World, turn: int, data: None = None) -> None:
|
def snapshot_system(world: World, turn: int, data: None = None) -> None:
|
||||||
logger.info("taking snapshot of world state")
|
logger.info("taking snapshot of world state")
|
||||||
save_world_state(world, turn, world_state_file)
|
save_world_state(world, turn, world_state_file)
|
||||||
|
|
||||||
extra_systems.append(GameSystem(name="snapshot", simulate=snapshot_system))
|
systems.append(GameSystem(name="snapshot", simulate=snapshot_system))
|
||||||
|
|
||||||
# hack: send a snapshot to the websocket server
|
# hack: send a snapshot to the websocket server
|
||||||
if args.server:
|
if args.server:
|
||||||
server_system(world, world_turn)
|
server_system(world, world_turn)
|
||||||
|
|
||||||
# create the DM
|
# run game systems for each turn
|
||||||
llm = agent_easy_connect()
|
logger.info(f"simulating the world for {args.turns} turns using systems: {systems}")
|
||||||
memory_factory = partial(
|
for i in count():
|
||||||
make_limited_memory, limit=config.world.character.memory_limit
|
current_turn = get_current_turn()
|
||||||
)
|
logger.info(f"simulating turn {i} of {args.turns} (world turn {current_turn})")
|
||||||
world_builder = Agent(
|
|
||||||
"dungeon master",
|
|
||||||
format_prompt(
|
|
||||||
"world_generate_dungeon_master",
|
|
||||||
flavor=world_prompt.flavor,
|
|
||||||
theme=world_prompt.theme,
|
|
||||||
),
|
|
||||||
{},
|
|
||||||
llm,
|
|
||||||
memory_factory=memory_factory,
|
|
||||||
)
|
|
||||||
set_dungeon_master(world_builder)
|
|
||||||
|
|
||||||
# start the sim
|
for system in systems:
|
||||||
logger.debug("simulating world: %s", world.name)
|
if system.simulate:
|
||||||
simulate_world(
|
logger.info(f"running system {system.name}")
|
||||||
world,
|
system.simulate(world, current_turn)
|
||||||
turns=args.turns,
|
|
||||||
actions=extra_actions,
|
set_current_turn(current_turn + 1)
|
||||||
systems=extra_systems,
|
if i >= args.turns:
|
||||||
)
|
logger.info("reached turn limit at world turn %s", current_turn + 1)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -1,393 +0,0 @@
|
||||||
from functools import partial
|
|
||||||
from itertools import count
|
|
||||||
from json import loads
|
|
||||||
from logging import getLogger
|
|
||||||
from math import inf
|
|
||||||
from typing import Callable, Sequence
|
|
||||||
|
|
||||||
from packit.agent import Agent
|
|
||||||
from packit.conditions import condition_or, condition_threshold
|
|
||||||
from packit.errors import ToolError
|
|
||||||
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.actions.planning import (
|
|
||||||
check_calendar,
|
|
||||||
edit_note,
|
|
||||||
erase_notes,
|
|
||||||
get_recent_notes,
|
|
||||||
read_notes,
|
|
||||||
schedule_event,
|
|
||||||
summarize_notes,
|
|
||||||
take_note,
|
|
||||||
)
|
|
||||||
from taleweave.context import (
|
|
||||||
broadcast,
|
|
||||||
get_character_agent_for_name,
|
|
||||||
get_character_for_agent,
|
|
||||||
get_current_turn,
|
|
||||||
get_current_world,
|
|
||||||
get_game_config,
|
|
||||||
get_prompt,
|
|
||||||
set_current_character,
|
|
||||||
set_current_room,
|
|
||||||
set_current_turn,
|
|
||||||
set_current_world,
|
|
||||||
set_game_systems,
|
|
||||||
)
|
|
||||||
from taleweave.errors import ActionError
|
|
||||||
from taleweave.game_system import GameSystem
|
|
||||||
from taleweave.models.entity import Character, Room, World
|
|
||||||
from taleweave.models.event import ActionEvent, ResultEvent
|
|
||||||
from taleweave.utils.conversation import make_keyword_condition, summarize_room
|
|
||||||
from taleweave.utils.effect import expire_effects
|
|
||||||
from taleweave.utils.planning import expire_events, get_upcoming_events
|
|
||||||
from taleweave.utils.search import find_containing_room
|
|
||||||
from taleweave.utils.template import format_prompt
|
|
||||||
from taleweave.utils.world import format_attributes
|
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def world_result_parser(value, agent, **kwargs):
|
|
||||||
current_world = get_current_world()
|
|
||||||
if not current_world:
|
|
||||||
raise ValueError(
|
|
||||||
"The current world must be set before calling world_result_parser"
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug(f"parsing action for {agent.name}: {value}")
|
|
||||||
|
|
||||||
current_character = get_character_for_agent(agent)
|
|
||||||
current_room = next(
|
|
||||||
(room for room in current_world.rooms if current_character in room.characters),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
set_current_room(current_room)
|
|
||||||
set_current_character(current_character)
|
|
||||||
|
|
||||||
return function_result(value, agent=agent, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def prompt_character_action(
|
|
||||||
room, character, agent, action_toolbox, current_turn
|
|
||||||
) -> str:
|
|
||||||
action_names = action_toolbox.list_tools()
|
|
||||||
|
|
||||||
# collect data for the prompt
|
|
||||||
notes_prompt, events_prompt = get_notes_events(character, current_turn)
|
|
||||||
|
|
||||||
room_characters = [character.name for character in room.characters]
|
|
||||||
room_items = [item.name for item in room.items]
|
|
||||||
room_directions = [portal.name for portal in room.portals]
|
|
||||||
|
|
||||||
character_attributes = format_attributes(character)
|
|
||||||
# character_effects = [effect.name for effect in character.active_effects]
|
|
||||||
character_items = [item.name for item in character.items]
|
|
||||||
|
|
||||||
# set up a result parser for the agent
|
|
||||||
def result_parser(value, **kwargs):
|
|
||||||
if not room or not character:
|
|
||||||
raise ValueError("Room and character must be set before parsing results")
|
|
||||||
|
|
||||||
# trim suffixes that are used elsewhere
|
|
||||||
value = value.removesuffix("END").strip()
|
|
||||||
|
|
||||||
# fix the "action_ move" whitespace issue
|
|
||||||
if '"action_ ' in value:
|
|
||||||
value = value.replace('"action_ ', '"action_')
|
|
||||||
|
|
||||||
# fix unbalanced curly braces
|
|
||||||
if value.startswith("{") and not value.endswith("}"):
|
|
||||||
open_count = value.count("{")
|
|
||||||
close_count = value.count("}")
|
|
||||||
|
|
||||||
if open_count > close_count:
|
|
||||||
fixed_value = value + ("}" * (open_count - close_count))
|
|
||||||
try:
|
|
||||||
loads(fixed_value)
|
|
||||||
value = fixed_value
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = world_result_parser(value, **kwargs)
|
|
||||||
|
|
||||||
# TODO: try to avoid parsing the JSON twice
|
|
||||||
event = ActionEvent.from_json(value, room, character)
|
|
||||||
broadcast(event)
|
|
||||||
|
|
||||||
return result
|
|
||||||
except ToolError as e:
|
|
||||||
e_str = str(e)
|
|
||||||
if e_str and "Error running tool" in e_str:
|
|
||||||
# extract the tool name and rest of the message from the error
|
|
||||||
# the format is: "Error running tool: <action_name>: <message>"
|
|
||||||
action_name, message = e_str.split(":", 1)
|
|
||||||
action_name = action_name.removeprefix("Error running tool").strip()
|
|
||||||
message = message.strip()
|
|
||||||
raise ActionError(
|
|
||||||
format_prompt(
|
|
||||||
"world_simulate_character_action_error_action",
|
|
||||||
action=action_name,
|
|
||||||
message=message,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif e_str and "Unknown tool" in e_str:
|
|
||||||
raise ActionError(
|
|
||||||
format_prompt(
|
|
||||||
"world_simulate_character_action_error_unknown_tool",
|
|
||||||
actions=action_names,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ActionError(
|
|
||||||
format_prompt(
|
|
||||||
"world_simulate_character_action_error_json",
|
|
||||||
actions=action_names,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# prompt and act
|
|
||||||
logger.info("starting turn for character: %s", character.name)
|
|
||||||
result = loop_retry(
|
|
||||||
agent,
|
|
||||||
format_prompt(
|
|
||||||
"world_simulate_character_action",
|
|
||||||
actions=action_names,
|
|
||||||
character_items=character_items,
|
|
||||||
attributes=character_attributes,
|
|
||||||
directions=room_directions,
|
|
||||||
room=room,
|
|
||||||
visible_characters=room_characters,
|
|
||||||
visible_items=room_items,
|
|
||||||
notes_prompt=notes_prompt,
|
|
||||||
events_prompt=events_prompt,
|
|
||||||
),
|
|
||||||
result_parser=result_parser,
|
|
||||||
toolbox=action_toolbox,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug(f"{character.name} action result: {result}")
|
|
||||||
if agent.memory:
|
|
||||||
agent.memory.append(result)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def get_notes_events(character: Character, current_turn: int):
|
|
||||||
recent_notes = get_recent_notes(character)
|
|
||||||
upcoming_events = get_upcoming_events(character, current_turn)
|
|
||||||
|
|
||||||
if len(recent_notes) > 0:
|
|
||||||
notes = "\n".join(recent_notes)
|
|
||||||
notes_prompt = format_prompt(
|
|
||||||
"world_simulate_character_planning_notes_some", notes=notes
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
notes_prompt = format_prompt("world_simulate_character_planning_notes_none")
|
|
||||||
|
|
||||||
if len(upcoming_events) > 0:
|
|
||||||
current_turn = get_current_turn()
|
|
||||||
events = [
|
|
||||||
format_prompt(
|
|
||||||
"world_simulate_character_planning_events_item",
|
|
||||||
event=event,
|
|
||||||
turns=event.turn - current_turn,
|
|
||||||
)
|
|
||||||
for event in upcoming_events
|
|
||||||
]
|
|
||||||
events = "\n".join(events)
|
|
||||||
events_prompt = format_prompt(
|
|
||||||
"world_simulate_character_planning_events_some", events=events
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
events_prompt = format_prompt("world_simulate_character_planning_events_none")
|
|
||||||
|
|
||||||
return notes_prompt, events_prompt
|
|
||||||
|
|
||||||
|
|
||||||
def prompt_character_planning(
|
|
||||||
room: Room,
|
|
||||||
character: Character,
|
|
||||||
agent: Agent,
|
|
||||||
planner_toolbox: Toolbox,
|
|
||||||
current_turn: int,
|
|
||||||
max_steps: int | None = None,
|
|
||||||
) -> str:
|
|
||||||
config = get_game_config()
|
|
||||||
max_steps = max_steps or config.world.turn.planning_steps
|
|
||||||
|
|
||||||
notes_prompt, events_prompt = get_notes_events(character, current_turn)
|
|
||||||
|
|
||||||
event_count = len(character.planner.calendar.events)
|
|
||||||
note_count = len(character.planner.notes)
|
|
||||||
|
|
||||||
def result_parser(value, **kwargs):
|
|
||||||
try:
|
|
||||||
return function_result(value, **kwargs)
|
|
||||||
except ToolError as e:
|
|
||||||
e_str = str(e)
|
|
||||||
if e_str and "Error running tool" in e_str:
|
|
||||||
# extract the tool name and rest of the message from the error
|
|
||||||
# the format is: "Error running tool: <action_name>: <message>"
|
|
||||||
action_name, message = e_str.split(":", 2)
|
|
||||||
action_name = action_name.removeprefix("Error running tool").strip()
|
|
||||||
message = message.strip()
|
|
||||||
raise ActionError(
|
|
||||||
format_prompt(
|
|
||||||
"world_simulate_character_planning_error_action",
|
|
||||||
action=action_name,
|
|
||||||
message=message,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif e_str and "Unknown tool" in e_str:
|
|
||||||
raise ActionError(
|
|
||||||
format_prompt(
|
|
||||||
"world_simulate_character_planning_error_unknown_tool",
|
|
||||||
actions=planner_toolbox.list_tools(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ActionError(
|
|
||||||
format_prompt(
|
|
||||||
"world_simulate_character_planning_error_json",
|
|
||||||
actions=planner_toolbox.list_tools(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("starting planning for character: %s", character.name)
|
|
||||||
_, condition_end, result_parser = make_keyword_condition(
|
|
||||||
get_prompt("world_simulate_character_planning_done"),
|
|
||||||
result_parser=result_parser,
|
|
||||||
)
|
|
||||||
stop_condition = condition_or(
|
|
||||||
condition_end, partial(condition_threshold, max=max_steps)
|
|
||||||
)
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
while not stop_condition(current=i):
|
|
||||||
result = loop_retry(
|
|
||||||
agent,
|
|
||||||
format_prompt(
|
|
||||||
"world_simulate_character_planning",
|
|
||||||
event_count=event_count,
|
|
||||||
events_prompt=events_prompt,
|
|
||||||
note_count=note_count,
|
|
||||||
notes_prompt=notes_prompt,
|
|
||||||
room_summary=summarize_room(room, character),
|
|
||||||
),
|
|
||||||
result_parser=result_parser,
|
|
||||||
stop_condition=stop_condition,
|
|
||||||
toolbox=planner_toolbox,
|
|
||||||
)
|
|
||||||
|
|
||||||
if agent.memory:
|
|
||||||
agent.memory.append(result)
|
|
||||||
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def simulate_world(
|
|
||||||
world: World,
|
|
||||||
turns: float | int = inf,
|
|
||||||
actions: Sequence[Callable[..., str]] = [],
|
|
||||||
systems: Sequence[GameSystem] = [],
|
|
||||||
):
|
|
||||||
logger.info("simulating the world")
|
|
||||||
set_current_world(world)
|
|
||||||
set_game_systems(systems)
|
|
||||||
|
|
||||||
# build a toolbox for the actions
|
|
||||||
action_tools = Toolbox(
|
|
||||||
[
|
|
||||||
action_ask,
|
|
||||||
action_give,
|
|
||||||
action_examine,
|
|
||||||
action_move,
|
|
||||||
action_take,
|
|
||||||
action_tell,
|
|
||||||
*actions,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# build a toolbox for the planners
|
|
||||||
planner_toolbox = Toolbox(
|
|
||||||
[
|
|
||||||
check_calendar,
|
|
||||||
erase_notes,
|
|
||||||
read_notes,
|
|
||||||
edit_note,
|
|
||||||
schedule_event,
|
|
||||||
summarize_notes,
|
|
||||||
take_note,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# simulate each character
|
|
||||||
for i in count():
|
|
||||||
current_turn = get_current_turn()
|
|
||||||
logger.info(f"simulating turn {i} of {turns} (world turn {current_turn})")
|
|
||||||
|
|
||||||
for character_name in world.order:
|
|
||||||
character, agent = get_character_agent_for_name(character_name)
|
|
||||||
if not agent or not character:
|
|
||||||
logger.error(f"agent or character not found for name {character_name}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
room = find_containing_room(world, character)
|
|
||||||
if not room:
|
|
||||||
logger.error(f"character {character_name} is not in a room")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# prep context
|
|
||||||
set_current_room(room)
|
|
||||||
set_current_character(character)
|
|
||||||
|
|
||||||
# decrement effects on the character and remove any that have expired
|
|
||||||
expire_effects(character)
|
|
||||||
expire_events(character, current_turn)
|
|
||||||
|
|
||||||
# give the character a chance to think and check their planner
|
|
||||||
if agent.memory and len(agent.memory) > 0:
|
|
||||||
try:
|
|
||||||
thoughts = prompt_character_planning(
|
|
||||||
room, character, agent, planner_toolbox, current_turn
|
|
||||||
)
|
|
||||||
logger.debug(f"{character.name} thinks: {thoughts}")
|
|
||||||
except Exception:
|
|
||||||
logger.exception(
|
|
||||||
f"error during planning for character {character.name}"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = prompt_character_action(
|
|
||||||
room, character, agent, action_tools, current_turn
|
|
||||||
)
|
|
||||||
result_event = ResultEvent(
|
|
||||||
result=result, room=room, character=character
|
|
||||||
)
|
|
||||||
broadcast(result_event)
|
|
||||||
except Exception:
|
|
||||||
logger.exception(f"error during action for character {character.name}")
|
|
||||||
|
|
||||||
for system in systems:
|
|
||||||
if system.simulate:
|
|
||||||
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
|
|
|
@ -0,0 +1,218 @@
|
||||||
|
from json import loads
|
||||||
|
from logging import getLogger
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from packit.errors import ToolError
|
||||||
|
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_character_agent_for_name,
|
||||||
|
get_character_for_agent,
|
||||||
|
get_current_world,
|
||||||
|
get_extra_actions,
|
||||||
|
set_current_character,
|
||||||
|
set_current_room,
|
||||||
|
)
|
||||||
|
from taleweave.errors import ActionError
|
||||||
|
from taleweave.game_system import GameSystem
|
||||||
|
from taleweave.models.entity import World
|
||||||
|
from taleweave.models.event import ActionEvent, ResultEvent
|
||||||
|
from taleweave.utils.effect import expire_effects
|
||||||
|
from taleweave.utils.search import find_containing_room
|
||||||
|
from taleweave.utils.template import format_prompt
|
||||||
|
from taleweave.utils.world import format_attributes
|
||||||
|
|
||||||
|
from .planning import get_notes_events
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def world_result_parser(value, agent, **kwargs):
|
||||||
|
current_world = get_current_world()
|
||||||
|
if not current_world:
|
||||||
|
raise ValueError(
|
||||||
|
"The current world must be set before calling world_result_parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(f"parsing action for {agent.name}: {value}")
|
||||||
|
|
||||||
|
current_character = get_character_for_agent(agent)
|
||||||
|
current_room = next(
|
||||||
|
(room for room in current_world.rooms if current_character in room.characters),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
set_current_room(current_room)
|
||||||
|
set_current_character(current_character)
|
||||||
|
|
||||||
|
return function_result(value, agent=agent, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_character_action(
|
||||||
|
room, character, agent, action_toolbox, current_turn
|
||||||
|
) -> str:
|
||||||
|
action_names = action_toolbox.list_tools()
|
||||||
|
|
||||||
|
# collect data for the prompt
|
||||||
|
notes_prompt, events_prompt = get_notes_events(character, current_turn)
|
||||||
|
|
||||||
|
room_characters = [character.name for character in room.characters]
|
||||||
|
room_items = [item.name for item in room.items]
|
||||||
|
room_directions = [portal.name for portal in room.portals]
|
||||||
|
|
||||||
|
character_attributes = format_attributes(character)
|
||||||
|
# character_effects = [effect.name for effect in character.active_effects]
|
||||||
|
character_items = [item.name for item in character.items]
|
||||||
|
|
||||||
|
# set up a result parser for the agent
|
||||||
|
def result_parser(value, **kwargs):
|
||||||
|
if not room or not character:
|
||||||
|
raise ValueError("Room and character must be set before parsing results")
|
||||||
|
|
||||||
|
# trim suffixes that are used elsewhere
|
||||||
|
value = value.removesuffix("END").strip()
|
||||||
|
|
||||||
|
# fix the "action_ move" whitespace issue
|
||||||
|
if '"action_ ' in value:
|
||||||
|
value = value.replace('"action_ ', '"action_')
|
||||||
|
|
||||||
|
# fix unbalanced curly braces
|
||||||
|
if value.startswith("{") and not value.endswith("}"):
|
||||||
|
open_count = value.count("{")
|
||||||
|
close_count = value.count("}")
|
||||||
|
|
||||||
|
if open_count > close_count:
|
||||||
|
fixed_value = value + ("}" * (open_count - close_count))
|
||||||
|
try:
|
||||||
|
loads(fixed_value)
|
||||||
|
value = fixed_value
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = world_result_parser(value, **kwargs)
|
||||||
|
|
||||||
|
# TODO: try to avoid parsing the JSON twice
|
||||||
|
event = ActionEvent.from_json(value, room, character)
|
||||||
|
broadcast(event)
|
||||||
|
|
||||||
|
return result
|
||||||
|
except ToolError as e:
|
||||||
|
e_str = str(e)
|
||||||
|
if e_str and "Error running tool" in e_str:
|
||||||
|
# extract the tool name and rest of the message from the error
|
||||||
|
# the format is: "Error running tool: <action_name>: <message>"
|
||||||
|
action_name, message = e_str.split(":", 1)
|
||||||
|
action_name = action_name.removeprefix("Error running tool").strip()
|
||||||
|
message = message.strip()
|
||||||
|
raise ActionError(
|
||||||
|
format_prompt(
|
||||||
|
"world_simulate_character_action_error_action",
|
||||||
|
action=action_name,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif e_str and "Unknown tool" in e_str:
|
||||||
|
raise ActionError(
|
||||||
|
format_prompt(
|
||||||
|
"world_simulate_character_action_error_unknown_tool",
|
||||||
|
actions=action_names,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ActionError(
|
||||||
|
format_prompt(
|
||||||
|
"world_simulate_character_action_error_json",
|
||||||
|
actions=action_names,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# prompt and act
|
||||||
|
logger.info("starting turn for character: %s", character.name)
|
||||||
|
result = loop_retry(
|
||||||
|
agent,
|
||||||
|
format_prompt(
|
||||||
|
"world_simulate_character_action",
|
||||||
|
actions=action_names,
|
||||||
|
character_items=character_items,
|
||||||
|
attributes=character_attributes,
|
||||||
|
directions=room_directions,
|
||||||
|
room=room,
|
||||||
|
visible_characters=room_characters,
|
||||||
|
visible_items=room_items,
|
||||||
|
notes_prompt=notes_prompt,
|
||||||
|
events_prompt=events_prompt,
|
||||||
|
),
|
||||||
|
result_parser=result_parser,
|
||||||
|
toolbox=action_toolbox,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(f"{character.name} action result: {result}")
|
||||||
|
if agent.memory:
|
||||||
|
agent.memory.append(result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def simulate_action(world: World, turn: int, data: Any | None = None):
|
||||||
|
for character_name in world.order:
|
||||||
|
character, agent = get_character_agent_for_name(character_name)
|
||||||
|
if not agent or not character:
|
||||||
|
logger.error(f"agent or character not found for name {character_name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
room = find_containing_room(world, character)
|
||||||
|
if not room:
|
||||||
|
logger.error(f"character {character_name} is not in a room")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# prep context
|
||||||
|
set_current_room(room)
|
||||||
|
set_current_character(character)
|
||||||
|
|
||||||
|
# decrement effects on the character and remove any that have expired
|
||||||
|
expire_effects(character)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = prompt_character_action(room, character, agent, action_tools, turn)
|
||||||
|
result_event = ResultEvent(result=result, room=room, character=character)
|
||||||
|
broadcast(result_event)
|
||||||
|
except Exception:
|
||||||
|
logger.exception(f"error during action for character {character.name}")
|
||||||
|
|
||||||
|
|
||||||
|
def init_action():
|
||||||
|
return [
|
||||||
|
GameSystem("action", initialize=initialize_action, simulate=simulate_action)
|
||||||
|
]
|
|
@ -6,7 +6,7 @@ from taleweave.game_system import FormatPerspective, GameSystem
|
||||||
from taleweave.models.entity import Character, Room, World, WorldEntity
|
from taleweave.models.entity import Character, Room, World, WorldEntity
|
||||||
from taleweave.models.event import ActionEvent, GameEvent
|
from taleweave.models.event import ActionEvent, GameEvent
|
||||||
from taleweave.utils.search import find_containing_room, find_portal, find_room
|
from taleweave.utils.search import find_containing_room, find_portal, find_room
|
||||||
from taleweave.utils.template import format_str
|
from taleweave.utils.template import format_prompt, format_str
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
@ -44,11 +44,12 @@ def create_move_digest(
|
||||||
character_mode = "self" if (event.character == active_character) else "other"
|
character_mode = "self" if (event.character == active_character) else "other"
|
||||||
direction_mode = "enter" if (destination_room == active_room) else "exit"
|
direction_mode = "enter" if (destination_room == active_room) else "exit"
|
||||||
|
|
||||||
message = format_str(
|
message = format_prompt(
|
||||||
f"digest_move_{character_mode}_{direction_mode}",
|
f"digest_move_{character_mode}_{direction_mode}",
|
||||||
destination_portal=destination_portal,
|
destination_portal=destination_portal,
|
||||||
destination_room=destination_room,
|
destination_room=destination_room,
|
||||||
direction=direction,
|
direction=direction,
|
||||||
|
event=event,
|
||||||
source_portal=source_portal,
|
source_portal=source_portal,
|
||||||
source_room=source_room,
|
source_room=source_room,
|
||||||
)
|
)
|
||||||
|
@ -118,6 +119,9 @@ def format_digest(
|
||||||
if not isinstance(entity, Character):
|
if not isinstance(entity, Character):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
if perspective != FormatPerspective.SECOND_PERSON:
|
||||||
|
return ""
|
||||||
|
|
||||||
buffer = character_buffers[entity.name]
|
buffer = character_buffers[entity.name]
|
||||||
|
|
||||||
world = get_current_world()
|
world = get_current_world()
|
||||||
|
|
|
@ -0,0 +1,202 @@
|
||||||
|
from functools import partial
|
||||||
|
from logging import getLogger
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from packit.agent import Agent
|
||||||
|
from packit.conditions import condition_or, condition_threshold
|
||||||
|
from packit.errors import ToolError
|
||||||
|
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_character_agent_for_name,
|
||||||
|
get_current_turn,
|
||||||
|
get_game_config,
|
||||||
|
get_prompt,
|
||||||
|
set_current_character,
|
||||||
|
set_current_room,
|
||||||
|
)
|
||||||
|
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.search import find_containing_room
|
||||||
|
from taleweave.utils.template import format_prompt
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
# build a toolbox for the planners
|
||||||
|
planner_toolbox = Toolbox(
|
||||||
|
[
|
||||||
|
check_calendar,
|
||||||
|
erase_notes,
|
||||||
|
read_notes,
|
||||||
|
edit_note,
|
||||||
|
schedule_event,
|
||||||
|
summarize_notes,
|
||||||
|
take_note,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_notes_events(character: Character, current_turn: int):
|
||||||
|
recent_notes = get_recent_notes(character)
|
||||||
|
upcoming_events = get_upcoming_events(character, current_turn)
|
||||||
|
|
||||||
|
if len(recent_notes) > 0:
|
||||||
|
notes = "\n".join(recent_notes)
|
||||||
|
notes_prompt = format_prompt(
|
||||||
|
"world_simulate_character_planning_notes_some", notes=notes
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
notes_prompt = format_prompt("world_simulate_character_planning_notes_none")
|
||||||
|
|
||||||
|
if len(upcoming_events) > 0:
|
||||||
|
current_turn = get_current_turn()
|
||||||
|
events = [
|
||||||
|
format_prompt(
|
||||||
|
"world_simulate_character_planning_events_item",
|
||||||
|
event=event,
|
||||||
|
turns=event.turn - current_turn,
|
||||||
|
)
|
||||||
|
for event in upcoming_events
|
||||||
|
]
|
||||||
|
events = "\n".join(events)
|
||||||
|
events_prompt = format_prompt(
|
||||||
|
"world_simulate_character_planning_events_some", events=events
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
events_prompt = format_prompt("world_simulate_character_planning_events_none")
|
||||||
|
|
||||||
|
return notes_prompt, events_prompt
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_character_planning(
|
||||||
|
room: Room,
|
||||||
|
character: Character,
|
||||||
|
agent: Agent,
|
||||||
|
planner_toolbox: Toolbox,
|
||||||
|
current_turn: int,
|
||||||
|
max_steps: int | None = None,
|
||||||
|
) -> str:
|
||||||
|
config = get_game_config()
|
||||||
|
max_steps = max_steps or config.world.turn.planning_steps
|
||||||
|
|
||||||
|
notes_prompt, events_prompt = get_notes_events(character, current_turn)
|
||||||
|
|
||||||
|
event_count = len(character.planner.calendar.events)
|
||||||
|
note_count = len(character.planner.notes)
|
||||||
|
|
||||||
|
def result_parser(value, **kwargs):
|
||||||
|
try:
|
||||||
|
return function_result(value, **kwargs)
|
||||||
|
except ToolError as e:
|
||||||
|
e_str = str(e)
|
||||||
|
if e_str and "Error running tool" in e_str:
|
||||||
|
# extract the tool name and rest of the message from the error
|
||||||
|
# the format is: "Error running tool: <action_name>: <message>"
|
||||||
|
action_name, message = e_str.split(":", 2)
|
||||||
|
action_name = action_name.removeprefix("Error running tool").strip()
|
||||||
|
message = message.strip()
|
||||||
|
raise ActionError(
|
||||||
|
format_prompt(
|
||||||
|
"world_simulate_character_planning_error_action",
|
||||||
|
action=action_name,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif e_str and "Unknown tool" in e_str:
|
||||||
|
raise ActionError(
|
||||||
|
format_prompt(
|
||||||
|
"world_simulate_character_planning_error_unknown_tool",
|
||||||
|
actions=planner_toolbox.list_tools(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ActionError(
|
||||||
|
format_prompt(
|
||||||
|
"world_simulate_character_planning_error_json",
|
||||||
|
actions=planner_toolbox.list_tools(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("starting planning for character: %s", character.name)
|
||||||
|
_, condition_end, result_parser = make_keyword_condition(
|
||||||
|
get_prompt("world_simulate_character_planning_done"),
|
||||||
|
result_parser=result_parser,
|
||||||
|
)
|
||||||
|
stop_condition = condition_or(
|
||||||
|
condition_end, partial(condition_threshold, max=max_steps)
|
||||||
|
)
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while not stop_condition(current=i):
|
||||||
|
result = loop_retry(
|
||||||
|
agent,
|
||||||
|
format_prompt(
|
||||||
|
"world_simulate_character_planning",
|
||||||
|
event_count=event_count,
|
||||||
|
events_prompt=events_prompt,
|
||||||
|
note_count=note_count,
|
||||||
|
notes_prompt=notes_prompt,
|
||||||
|
room_summary=summarize_room(room, character),
|
||||||
|
),
|
||||||
|
result_parser=result_parser,
|
||||||
|
stop_condition=stop_condition,
|
||||||
|
toolbox=planner_toolbox,
|
||||||
|
)
|
||||||
|
|
||||||
|
if agent.memory:
|
||||||
|
agent.memory.append(result)
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def simulate_planning(world: World, turn: int, data: Any | None = None):
|
||||||
|
for character_name in world.order:
|
||||||
|
character, agent = get_character_agent_for_name(character_name)
|
||||||
|
if not agent or not character:
|
||||||
|
logger.error(f"agent or character not found for name {character_name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
room = find_containing_room(world, character)
|
||||||
|
if not room:
|
||||||
|
logger.error(f"character {character_name} is not in a room")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# prep context
|
||||||
|
set_current_room(room)
|
||||||
|
set_current_character(character)
|
||||||
|
|
||||||
|
# decrement effects on the character and remove any that have expired
|
||||||
|
expire_events(character, turn) # TODO: move to planning
|
||||||
|
|
||||||
|
# give the character a chance to think and check their planner
|
||||||
|
if agent.memory and len(agent.memory) > 0:
|
||||||
|
try:
|
||||||
|
thoughts = prompt_character_planning(
|
||||||
|
room, character, agent, planner_toolbox, turn
|
||||||
|
)
|
||||||
|
logger.debug(f"{character.name} thinks: {thoughts}")
|
||||||
|
except Exception:
|
||||||
|
logger.exception(
|
||||||
|
f"error during planning for character {character.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def init_planning():
|
||||||
|
return [GameSystem("planning", simulate=simulate_planning)]
|
|
@ -1,9 +1,9 @@
|
||||||
|
from taleweave.systems.logic import load_logic
|
||||||
|
|
||||||
from .hunger_actions import action_cook, action_eat
|
from .hunger_actions import action_cook, action_eat
|
||||||
from .hygiene_actions import action_wash
|
from .hygiene_actions import action_wash
|
||||||
from .sleeping_actions import action_sleep
|
from .sleeping_actions import action_sleep
|
||||||
|
|
||||||
from taleweave.systems.logic import load_logic
|
|
||||||
|
|
||||||
LOGIC_FILES = [
|
LOGIC_FILES = [
|
||||||
"./taleweave/systems/sim/environment_logic.yaml",
|
"./taleweave/systems/sim/environment_logic.yaml",
|
||||||
"./taleweave/systems/sim/hunger_logic.yaml",
|
"./taleweave/systems/sim/hunger_logic.yaml",
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
from logging import getLogger
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from taleweave.game_system import GameSystem
|
||||||
|
from taleweave.models.entity import World
|
||||||
|
from taleweave.state import save_world_state
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def simulate_snapshot(world: World, turn: int, data: Any | None = None):
|
||||||
|
logger.info("taking snapshot of world state")
|
||||||
|
world_state_file = "TODO" # TODO: get world state file from somewhere
|
||||||
|
save_world_state(world, turn, world_state_file)
|
||||||
|
|
||||||
|
|
||||||
|
def init():
|
||||||
|
return [GameSystem("snapshot", simulate=simulate_snapshot)]
|
|
@ -1,16 +1,17 @@
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import List
|
|
||||||
from taleweave.context import get_dungeon_master
|
|
||||||
from taleweave.models.base import dataclass
|
|
||||||
from taleweave.models.entity import World
|
|
||||||
from taleweave.systems.logic import load_logic
|
|
||||||
from taleweave.game_system import GameSystem
|
|
||||||
from packit.agent import Agent
|
|
||||||
from taleweave.models.entity import Room, WorldEntity
|
|
||||||
from taleweave.utils.string import or_list
|
|
||||||
from packit.results import enum_result
|
|
||||||
from packit.loops import loop_retry
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from packit.agent import Agent
|
||||||
|
from packit.loops import loop_retry
|
||||||
|
from packit.results import enum_result
|
||||||
|
|
||||||
|
from taleweave.context import get_dungeon_master
|
||||||
|
from taleweave.game_system import GameSystem
|
||||||
|
from taleweave.models.base import dataclass
|
||||||
|
from taleweave.models.entity import Room, World, WorldEntity
|
||||||
|
from taleweave.systems.logic import load_logic
|
||||||
|
from taleweave.utils.string import or_list
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,11 @@ def format_attributes(
|
||||||
for system in systems
|
for system in systems
|
||||||
if system.format
|
if system.format
|
||||||
]
|
]
|
||||||
|
attribute_descriptions = [
|
||||||
|
description
|
||||||
|
for description in attribute_descriptions
|
||||||
|
if len(description.strip()) > 0
|
||||||
|
]
|
||||||
|
|
||||||
return f"{'. '.join(attribute_descriptions)}"
|
return f"{'. '.join(attribute_descriptions)}"
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,12 @@ templates:
|
||||||
theme: post-apocalyptic world where the only survivors are sentient robots
|
theme: post-apocalyptic world where the only survivors are sentient robots
|
||||||
flavor: create a world where the only survivors of a nuclear apocalypse are sentient robots, who must now rebuild society from scratch
|
flavor: create a world where the only survivors of a nuclear apocalypse are sentient robots, who must now rebuild society from scratch
|
||||||
- name: haunted-house
|
- name: haunted-house
|
||||||
theme: haunted house in the middle of nowhere
|
theme: group of unexpecting friends find a haunted house in the middle of nowhere
|
||||||
flavor: create a spooky and suspenseful world where a group of people are trapped in a haunted house in the middle of nowhere
|
flavor: |
|
||||||
|
create a spooky and suspenseful world where a group of innocent, unexpecting people find and become trapped in a
|
||||||
|
haunted house in the middle of nowhere. make sure you create some rooms for the road up to the house as well as
|
||||||
|
the interior of the house itself. include a variety of characters and make sure they will fully utilize all of the
|
||||||
|
actions available to them in this world, exploring and interacting with each other and the environment.
|
||||||
- name: magical-kingdom
|
- name: magical-kingdom
|
||||||
theme: dangerous magical fantasy world
|
theme: dangerous magical fantasy world
|
||||||
flavor: make a strange and dangerous world where magic winds its way through everything and incredibly powerful beings drink, fight, and wander the halls
|
flavor: make a strange and dangerous world where magic winds its way through everything and incredibly powerful beings drink, fight, and wander the halls
|
||||||
|
|
Loading…
Reference in New Issue