diff --git a/taleweave/actions/optional.py b/taleweave/actions/optional.py index 63987d6..740eadf 100644 --- a/taleweave/actions/optional.py +++ b/taleweave/actions/optional.py @@ -184,7 +184,9 @@ def action_use(item: str, target: str) -> str: ) outcome = dungeon_master( f"{action_character.name} uses the {chosen_name} effect of {item} on {target}. " - f"{describe_character(action_character)}. {describe_character(target_character)}. {describe_entity(action_item)}. " + f"{describe_character(action_character)}. " + f"{describe_character(target_character)}. " + f"{describe_entity(action_item)}. " f"What happens? How does {target} react? Be creative with the results. The outcome can be good, bad, or neutral." "Decide based on the characters involved and the item being used." "Specify the outcome of the action. Do not include the question or any JSON. Only include the outcome of the action." diff --git a/taleweave/player.py b/taleweave/player.py index d412720..afb9870 100644 --- a/taleweave/player.py +++ b/taleweave/player.py @@ -6,6 +6,7 @@ from typing import Any, Callable, Dict, List, Optional, Sequence from langchain_core.messages import AIMessage, BaseMessage, HumanMessage from packit.agent import Agent +from packit.toolbox import Toolbox from taleweave.context import action_context from taleweave.models.event import PromptEvent @@ -89,7 +90,28 @@ class BasePlayer: Ask the player for input. """ - return self(prompt, **context) + return self(prompt, **context, **kwargs) + + def format_psuedo_functions(self, toolbox: Toolbox) -> str: + """ + Format pseudo functions for the player prompt. + """ + functions = [] + for tool in toolbox.list_definitions(): + function_data = tool["function"] + function_name = f"~{function_data['name']}" + function_args = [] + for name, info in ( + function_data.get("parameters", {}).get("properties", {}).items() + ): + function_args.append(f"{name}={info['type']}") + + if function_args: + functions.append(function_name + ":" + ",".join(function_args)) + else: + functions.append(function_name) + + return "\n".join(functions) def parse_pseudo_function(self, reply: str): # turn other replies into a JSON function call @@ -170,12 +192,15 @@ class RemotePlayer(BasePlayer): self.input_queue = Queue() self.send_prompt = send_prompt - def __call__(self, prompt: str, **kwargs) -> str: + def __call__(self, prompt: str, toolbox: Toolbox | None = None, **kwargs) -> str: """ Ask the player for input. """ formatted_prompt = prompt.format(**kwargs) + if toolbox: + formatted_prompt += self.format_psuedo_functions(toolbox) + self.memory.append(HumanMessage(content=formatted_prompt)) with action_context() as (current_room, current_character): @@ -193,7 +218,7 @@ class RemotePlayer(BasePlayer): logger.exception("error getting reply from remote player") if self.fallback_agent: - logger.info("prompting fallback agent: {self.fallback_agent.name}") + logger.info(f"prompting fallback agent: {self.fallback_agent.name}") return self.fallback_agent(prompt, **kwargs) return "" diff --git a/taleweave/render/prompt.py b/taleweave/render/prompt.py index 767fe8a..7f2c0da 100644 --- a/taleweave/render/prompt.py +++ b/taleweave/render/prompt.py @@ -4,6 +4,7 @@ from random import shuffle from typing import List from taleweave.context import get_current_world, get_dungeon_master +from taleweave.game_system import FormatPerspective from taleweave.models.entity import Room, WorldEntity from taleweave.models.event import ( ActionEvent, @@ -32,7 +33,11 @@ def prompt_from_parameters( if target_character: logger.debug("adding character to prompt: %s", target_character.name) pre.append(f"with {target_character.name}") - post.append(describe_entity(target_character)) + post.append( + describe_entity( + target_character, perspective=FormatPerspective.THIRD_PERSON + ) + ) if "item" in parameters: # look up the item @@ -47,7 +52,9 @@ def prompt_from_parameters( if target_item: logger.debug("adding item to prompt: %s", target_item.name) pre.append(f"using the {target_item.name}") - post.append(describe_entity(target_item)) + post.append( + describe_entity(target_item, perspective=FormatPerspective.THIRD_PERSON) + ) if "target" in parameters: # could be a room, character, or item @@ -60,13 +67,21 @@ def prompt_from_parameters( if target_room: logger.debug("adding room to prompt: %s", target_room.name) pre.append(f"in the {target_room.name}") - post.append(describe_entity(target_room)) + post.append( + describe_entity( + target_room, perspective=FormatPerspective.THIRD_PERSON + ) + ) target_character = find_character_in_room(action_room, target_name) if target_character: logger.debug("adding character to prompt: %s", target_character.name) pre.append(f"with {target_character.name}") - post.append(describe_entity(target_character)) + post.append( + describe_entity( + target_character, perspective=FormatPerspective.THIRD_PERSON + ) + ) target_item = find_item_in_room( action_room, @@ -77,7 +92,11 @@ def prompt_from_parameters( if target_item: logger.debug("adding item to prompt: %s", target_item.name) pre.append(f"using the {target_item.name}") - post.append(describe_entity(target_item)) + post.append( + describe_entity( + target_item, perspective=FormatPerspective.THIRD_PERSON + ) + ) return (" and ".join(pre) if pre else "", " and ".join(post) if post else "") diff --git a/taleweave/simulate.py b/taleweave/simulate.py index ce25aea..5b46160 100644 --- a/taleweave/simulate.py +++ b/taleweave/simulate.py @@ -115,9 +115,10 @@ def prompt_character_action( pass if could_be_json(value): + # TODO: only emit valid actions that parse and run correctly event = ActionEvent.from_json(value, room, character) else: - # TODO: this should be removed and throw + # TODO: this path should be removed and throw event = ResultEvent(value, room, character) broadcast(event) diff --git a/taleweave/systems/digest.py b/taleweave/systems/digest.py new file mode 100644 index 0000000..158e347 --- /dev/null +++ b/taleweave/systems/digest.py @@ -0,0 +1,90 @@ +from typing import Dict, List + +from taleweave.context import get_current_world, subscribe +from taleweave.game_system import FormatPerspective, GameSystem +from taleweave.models.entity import Character, Room, World, WorldEntity +from taleweave.models.event import ActionEvent, GameEvent +from taleweave.utils.search import find_containing_room + + +def create_turn_digest( + active_room: Room, active_character: Character, turn_events: List[GameEvent] +) -> List[str]: + messages = [] + for event in turn_events: + if isinstance(event, ActionEvent): + if event.character == active_character or event.room == active_room: + if event.action == "move": + # TODO: differentiate between entering and leaving + messages.append(f"{event.character.name} entered the room.") + elif event.action == "take": + messages.append( + f"{event.character.name} picked up the {event.parameters['item']}." + ) + elif event.action == "give": + messages.append( + f"{event.character.name} gave {event.parameters['item']} to {event.parameters['character']}." + ) + elif event.action == "ask": + messages.append( + f"{event.character.name} asked {event.parameters['character']} about something." + ) + elif event.action == "tell": + messages.append( + f"{event.character.name} told {event.parameters['character']} something." + ) + elif event.action == "examine": + messages.append( + f"{event.character.name} examined the {event.parameters['target']}." + ) + + return messages + + +character_buffers: Dict[str, List[GameEvent]] = {} + + +def digest_listener(event: GameEvent): + if isinstance(event, ActionEvent): + character = event.character.name + + # append the event to every character's buffer except the one who triggered it + # the actor should have their buffer reset, because they can only act on their turn + + for name, buffer in character_buffers.items(): + if name == character: + buffer.clear() + else: + buffer.append(event) + + +def format_digest( + entity: WorldEntity, + perspective: FormatPerspective = FormatPerspective.SECOND_PERSON, +) -> str: + if not isinstance(entity, Character): + return "" + + buffer = character_buffers[entity.name] + + world = get_current_world() + if not world: + raise ValueError("No world found") + + room = find_containing_room(world, entity) + if not room: + raise ValueError("Character not found in any room") + + digest = create_turn_digest(room, entity, buffer) + return "\n".join(digest) + + +def initialize_digest(world: World): + for room in world.rooms: + for character in room.characters: + character_buffers[character.name] = [] + + +def init(): + subscribe(GameEvent, digest_listener) + return [GameSystem("digest", format=format_digest, initialize=initialize_digest)] diff --git a/taleweave/systems/sim/hygiene_actions.py b/taleweave/systems/sim/hygiene_actions.py index eae946c..0588181 100644 --- a/taleweave/systems/sim/hygiene_actions.py +++ b/taleweave/systems/sim/hygiene_actions.py @@ -12,7 +12,8 @@ def action_wash(unused: bool) -> str: dungeon_master = get_dungeon_master() outcome = dungeon_master( - f"{action_character.name} washes themselves in the {action_room.name}. {describe_entity(action_room)}. {describe_entity(action_character)}" + f"{action_character.name} washes themselves in the {action_room.name}. " + f"{describe_entity(action_room)}. {describe_entity(action_character)}" f"{action_character.name} was {hygiene} to start with. How clean are they after washing? Respond with 'clean' or 'dirty'." "If the room has a shower or running water, they should be cleaner. If the room is dirty, they should end up dirtier." ) diff --git a/taleweave/utils/world.py b/taleweave/utils/world.py index bf63b32..141e3af 100644 --- a/taleweave/utils/world.py +++ b/taleweave/utils/world.py @@ -31,7 +31,7 @@ def describe_static(entity: WorldEntity) -> str: def describe_entity( entity: WorldEntity, - perspective: FormatPerspective = FormatPerspective.SECOND_PERSON, + perspective: FormatPerspective = FormatPerspective.THIRD_PERSON, ) -> str: if isinstance(entity, Character): return describe_character(entity, perspective)