From 80e98482e0a2cded2b177ae7a90c9afe38aa3384 Mon Sep 17 00:00:00 2001 From: Sean Sube Date: Tue, 28 May 2024 19:55:32 -0500 Subject: [PATCH] fix conversation events, rename look to examine, generate indoor/outdoor attributes --- client/src/events.tsx | 29 +++++++++++- taleweave/actions/base.py | 30 +++++-------- taleweave/actions/planning.py | 8 +++- taleweave/bot/discord.py | 2 +- taleweave/main.py | 9 ++-- taleweave/models/event.py | 13 ++---- taleweave/player.py | 1 + taleweave/render/comfy.py | 2 +- taleweave/render/prompt.py | 2 +- taleweave/simulate.py | 21 ++++----- taleweave/systems/weather/__init__.py | 46 +++++++++++++++++++- taleweave/systems/weather/weather_logic.yaml | 10 ++--- taleweave/utils/conversation.py | 4 +- taleweave/utils/search.py | 23 +++++----- 14 files changed, 131 insertions(+), 69 deletions(-) diff --git a/client/src/events.tsx b/client/src/events.tsx index 05cc99a..459e770 100644 --- a/client/src/events.tsx +++ b/client/src/events.tsx @@ -119,6 +119,32 @@ export function SnapshotEventItem(props: EventItemProps) { } export function ReplyEventItem(props: EventItemProps) { + const { event } = props; + const { audience, speaker, text } = event; + + return + + + + + + + {speaker.name} replies to {audience.name}: {text} + + } + /> + ; +} + +export function StatusEventItem(props: EventItemProps) { const { event } = props; const { text } = event; @@ -295,8 +321,9 @@ export function EventItem(props: EventItemProps) { case 'result': return ; case 'reply': - case 'status': // TODO: should have a different component return ; + case 'status': + return ; case 'player': return ; case 'render': diff --git a/taleweave/actions/base.py b/taleweave/actions/base.py index 51d9574..104a099 100644 --- a/taleweave/actions/base.py +++ b/taleweave/actions/base.py @@ -13,6 +13,7 @@ from taleweave.utils.search import ( find_character_in_room, find_item_in_character, find_item_in_room, + find_portal_in_room, find_room, ) from taleweave.utils.string import normalize_name @@ -20,12 +21,12 @@ from taleweave.utils.world import describe_entity logger = getLogger(__name__) -MAX_CONVERSATION_STEPS = 3 +MAX_CONVERSATION_STEPS = 2 -def action_look(target: str) -> str: +def action_examine(target: str) -> str: """ - Look at a target in the room or your inventory. + Examine the room, a character, or an item (in the room or in your inventory). Args: target: The name of the target to look at. @@ -71,14 +72,7 @@ def action_move(direction: str) -> str: """ with world_context() as (action_world, action_room, action_character): - portal = next( - ( - p - for p in action_room.portals - if normalize_name(p.name) == normalize_name(direction) - ), - None, - ) + portal = find_portal_in_room(action_room, direction) if not portal: raise ActionError(f"You cannot move {direction} from here.") @@ -151,7 +145,7 @@ def action_ask(character: str, question: str) -> str: ) action_agent = get_agent_for_character(action_character) - answer = loop_conversation( + result = loop_conversation( action_room, [question_character, action_character], [question_agent, action_agent], @@ -165,9 +159,8 @@ def action_ask(character: str, question: str) -> str: max_length=MAX_CONVERSATION_STEPS, ) - if answer: - broadcast(f"{character} responds to {action_character.name}: {answer}") - return f"{character} responds: {answer}" + if result: + return result return f"{character} does not respond." @@ -209,7 +202,7 @@ def action_tell(character: str, message: str) -> str: ) action_agent = get_agent_for_character(action_character) - answer = loop_conversation( + result = loop_conversation( action_room, [question_character, action_character], [question_agent, action_agent], @@ -223,9 +216,8 @@ def action_tell(character: str, message: str) -> str: max_length=MAX_CONVERSATION_STEPS, ) - if answer: - broadcast(f"{character} responds to {action_character.name}: {answer}") - return f"{character} responds: {answer}" + if result: + return result return f"{character} does not respond." diff --git a/taleweave/actions/planning.py b/taleweave/actions/planning.py index 6e56d42..b1e0793 100644 --- a/taleweave/actions/planning.py +++ b/taleweave/actions/planning.py @@ -18,7 +18,10 @@ def take_note(fact: str): with action_context() as (_, action_character): if fact in action_character.planner.notes: - raise ActionError("You already have a note about that fact.") + raise ActionError( + "You already have a note about that fact. You do not need to take duplicate notes. " + "If you have too many notes, consider erasing, replacing, or summarizing them." + ) if len(action_character.planner.notes) >= character_config.note_limit: raise ActionError( @@ -103,7 +106,8 @@ def summarize_notes(limit: int) -> str: "If a newer note contradicts an older note, keep the newer note. " "Clean up your notes so you can focus on the most important facts. " "Respond with one note per line. You can have up to {limit} notes, " - "so make sure you reply with less than {limit} lines. " + "so make sure you reply with less than {limit} lines. Do not number the lines " + "in your response. Do not include any JSON or other information. " "Your notes are:\n{notes}", limit=limit, notes=notes, diff --git a/taleweave/bot/discord.py b/taleweave/bot/discord.py index 9bad720..7e1b147 100644 --- a/taleweave/bot/discord.py +++ b/taleweave/bot/discord.py @@ -330,7 +330,7 @@ def embed_from_event(event: GameEvent) -> Embed | None: def embed_from_action(event: ActionEvent | ReplyEvent): - action_embed = Embed(title=event.room.name, description=event.character.name) + action_embed = Embed(title=event.room.name, description=event.speaker.name) if isinstance(event, ActionEvent): action_name = event.action.replace("action_", "").title() diff --git a/taleweave/main.py b/taleweave/main.py index fe9722d..04253ba 100644 --- a/taleweave/main.py +++ b/taleweave/main.py @@ -200,11 +200,14 @@ def load_or_initialize_system_data(args, systems: List[GameSystem], world: World 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: - data = system.initialize(world) - set_system_data(system.name, data) + + 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]): diff --git a/taleweave/models/event.py b/taleweave/models/event.py index 6d2da41..5b5b12f 100644 --- a/taleweave/models/event.py +++ b/taleweave/models/event.py @@ -71,22 +71,15 @@ class PromptEvent(BaseModel): class ReplyEvent(BaseModel): """ A character has replied with text. - - This is the non-JSON version of an ActionEvent. - - TODO: add the character being replied to. """ - text: str room: Room - character: Character + speaker: Character + audience: Character | Room + text: str id: str = Field(default_factory=uuid) type: Literal["reply"] = "reply" - @staticmethod - def from_text(text: str, room: Room, character: Character) -> "ReplyEvent": - return ReplyEvent(text=text, room=room, character=character) - @dataclass class ResultEvent(BaseModel): diff --git a/taleweave/player.py b/taleweave/player.py index 519fad8..d412720 100644 --- a/taleweave/player.py +++ b/taleweave/player.py @@ -193,6 +193,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}") return self.fallback_agent(prompt, **kwargs) return "" diff --git a/taleweave/render/comfy.py b/taleweave/render/comfy.py index fffb039..ced7dad 100644 --- a/taleweave/render/comfy.py +++ b/taleweave/render/comfy.py @@ -230,7 +230,7 @@ def get_image_prefix(event: GameEvent | WorldEntity) -> str: if isinstance(event, ReplyEvent): return sanitize_name( - f"event-reply-{event.character.name}-{fast_hash(event.text)}" + f"event-reply-{event.speaker.name}-{fast_hash(event.text)}" ) if isinstance(event, ResultEvent): diff --git a/taleweave/render/prompt.py b/taleweave/render/prompt.py index 924abdf..767fe8a 100644 --- a/taleweave/render/prompt.py +++ b/taleweave/render/prompt.py @@ -97,7 +97,7 @@ def scene_from_event(event: GameEvent) -> str | None: ) if isinstance(event, ReplyEvent): - return f"{event.character.name} replies: {event.text}. {describe_entity(event.character)}. {describe_entity(event.room)}." + return f"{event.speaker.name} replies: {event.text}. {describe_entity(event.speaker)}. {describe_entity(event.room)}." if isinstance(event, ResultEvent): return f"{event.result}. {describe_entity(event.character)}. {describe_entity(event.room)}." diff --git a/taleweave/simulate.py b/taleweave/simulate.py index 08e404b..ce25aea 100644 --- a/taleweave/simulate.py +++ b/taleweave/simulate.py @@ -8,14 +8,14 @@ from typing import Callable, Sequence from packit.agent import Agent from packit.conditions import condition_or, condition_threshold from packit.loops import loop_retry -from packit.results import multi_function_or_str_result +from packit.results import function_result from packit.toolbox import Toolbox from packit.utils import could_be_json from taleweave.actions.base import ( action_ask, + action_examine, action_give, - action_look, action_move, action_take, action_tell, @@ -45,11 +45,11 @@ from taleweave.context import ( from taleweave.game_system import GameSystem from taleweave.models.config import DEFAULT_CONFIG from taleweave.models.entity import Character, Room, World -from taleweave.models.event import ActionEvent, ReplyEvent, ResultEvent +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_room_with_character +from taleweave.utils.search import find_containing_room from taleweave.utils.world import describe_entity, format_attributes logger = getLogger(__name__) @@ -76,7 +76,7 @@ def world_result_parser(value, agent, **kwargs): set_current_room(current_room) set_current_character(current_character) - return multi_function_or_str_result(value, agent=agent, **kwargs) + return function_result(value, agent=agent, **kwargs) def prompt_character_action( @@ -94,7 +94,7 @@ def prompt_character_action( character_items = [item.name for item in character.items] # set up a result parser for the agent - def result_parser(value, agent, **kwargs): + def result_parser(value, **kwargs): if not room or not character: raise ValueError("Room and character must be set before parsing results") @@ -117,11 +117,12 @@ def prompt_character_action( if could_be_json(value): event = ActionEvent.from_json(value, room, character) else: - event = ReplyEvent.from_text(value, room, character) + # TODO: this should be removed and throw + event = ResultEvent(value, room, character) broadcast(event) - return world_result_parser(value, agent, **kwargs) + return world_result_parser(value, **kwargs) # prompt and act logger.info("starting turn for character: %s", character.name) @@ -255,7 +256,7 @@ def simulate_world( [ action_ask, action_give, - action_look, + action_examine, action_move, action_take, action_tell, @@ -288,7 +289,7 @@ def simulate_world( logger.error(f"agent or character not found for name {character_name}") continue - room = find_room_with_character(world, character) + room = find_containing_room(world, character) if not room: logger.error(f"character {character_name} is not in a room") continue diff --git a/taleweave/systems/weather/__init__.py b/taleweave/systems/weather/__init__.py index 3d74ee8..3d5b368 100644 --- a/taleweave/systems/weather/__init__.py +++ b/taleweave/systems/weather/__init__.py @@ -1,8 +1,19 @@ +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 + +logger = getLogger(__name__) + LOGIC_FILES = [ "./taleweave/systems/weather/weather_logic.yaml", @@ -39,10 +50,36 @@ def get_time_of_day(turn: int) -> TimeOfDay: def initialize_weather(world: World): time_of_day = get_time_of_day(0) for room in world.rooms: + logger.info(f"initializing weather for {room.name}") room.attributes["time"] = time_of_day.name + if "environment" not in room.attributes: + dungeon_master = get_dungeon_master() + generate_room_weather(dungeon_master, world.theme, room) -# TODO: generate indoor/outdoor attributes + +def generate_room_weather(agent: Agent, theme: str, entity: Room) -> None: + environment_options = ["indoor", "outdoor"] + environment_result = partial(enum_result, enum=environment_options) + environment = loop_retry( + agent, + "Is this room indoors or outdoors?" + "Reply with a single word: {environment_list}.\n\n" + "{description}", + context={ + "environment_list": or_list(environment_options), + "description": entity.description, + }, + result_parser=environment_result, + ) + entity.attributes["environment"] = environment + logger.info(f"generated environment for {entity.name}: {environment}") + + +def generate_weather(agent: Agent, theme: str, entity: WorldEntity) -> None: + if isinstance(entity, Room): + if "environment" not in entity.attributes: + generate_room_weather(agent, theme, entity) def simulate_weather(world: World, turn: int, data: None = None): @@ -55,5 +92,10 @@ def init(): logic_systems = [load_logic(filename) for filename in LOGIC_FILES] return [ *logic_systems, - GameSystem("weather", initialize=initialize_weather, simulate=simulate_weather), + GameSystem( + "weather", + generate=generate_weather, + initialize=initialize_weather, + simulate=simulate_weather, + ), ] diff --git a/taleweave/systems/weather/weather_logic.yaml b/taleweave/systems/weather/weather_logic.yaml index d4f1a4b..1aa0cd5 100644 --- a/taleweave/systems/weather/weather_logic.yaml +++ b/taleweave/systems/weather/weather_logic.yaml @@ -3,7 +3,7 @@ rules: - group: weather match: type: room - outdoor: true + environment: outdoor weather: clear chance: 0.1 set: @@ -12,7 +12,7 @@ rules: - group: weather match: type: room - outdoor: true + environment: outdoor weather: clouds chance: 0.1 set: @@ -21,7 +21,7 @@ rules: - group: weather match: type: room - outdoor: true + environment: outdoor weather: rain chance: 0.1 set: @@ -30,7 +30,7 @@ rules: - group: weather match: type: room - outdoor: true + environment: outdoor weather: clouds chance: 0.1 set: @@ -40,7 +40,7 @@ rules: - group: weather match: type: room - outdoor: true + environment: outdoor rule: | "weather" not in attributes set: diff --git a/taleweave/utils/conversation.py b/taleweave/utils/conversation.py index 9b0e6bc..6cd5643 100644 --- a/taleweave/utils/conversation.py +++ b/taleweave/utils/conversation.py @@ -148,11 +148,11 @@ def loop_conversation( response = result_parser(response) logger.info(f"{character.name} responds: {response}") - reply_event = ReplyEvent.from_text(response, room, character) + reply_event = ReplyEvent(room, character, last_character, response) broadcast(reply_event) # increment the step counter i += 1 last_character = character - return response + return f"{last_character.name} ends the conversation for now" diff --git a/taleweave/utils/search.py b/taleweave/utils/search.py index 1b85e5e..4b57acc 100644 --- a/taleweave/utils/search.py +++ b/taleweave/utils/search.py @@ -23,9 +23,9 @@ def find_room(world: World, room_name: str) -> Room | None: def find_portal(world: World, portal_name: str) -> Portal | None: for room in world.rooms: - for portal in room.portals: - if normalize_name(portal.name) == normalize_name(portal_name): - return portal + portal = find_portal_in_room(room, portal_name) + if portal: + return portal return None @@ -47,6 +47,14 @@ def find_character_in_room(room: Room, character_name: str) -> Character | None: return None +def find_portal_in_room(room: Room, portal_name: str) -> Portal | None: + for portal in room.portals: + if normalize_name(portal.name) == normalize_name(portal_name): + return portal + + return None + + # TODO: allow item or str def find_item( world: World, @@ -109,15 +117,6 @@ def find_item_in_room( return None -def find_room_with_character(world: World, character: Character) -> Room | None: - for room in world.rooms: - for room_character in room.characters: - if normalize_name(character.name) == normalize_name(room_character.name): - return room - - return None - - def find_containing_room(world: World, entity: Room | Character | Item) -> Room | None: if isinstance(entity, Room): return entity