- Text World
+ TaleWeave AI
diff --git a/prompts/discord-en-us.yml b/prompts/discord-en-us.yml
new file mode 100644
index 0000000..c65dd7a
--- /dev/null
+++ b/prompts/discord-en-us.yml
@@ -0,0 +1,24 @@
+prompts:
+ discord_help: |
+ **Commands:**
+ - `!help` - Show this help message
+ - `!{{ bot_name }}` - Show the active world
+ - `!join ` - Join the game as the specified character
+ - `!leave` - Leave the game
+ discord_join_error_none: You must specify a character!
+ discord_join_error_not_found: Character {{ character }} was not found!
+ discord_join_error_taken: Someone is already playing as {{ character }}!
+ discord_join_result: |
+ {{ event.client }} is now playing as {{ event.character }}!
+ discord_join_title: |
+ Player Joined
+ discord_leave_error_none: You are not playing the game yet!
+ discord_leave_result: |
+ {{ event.client }} has left the game! {{ event.character }} is now being played by an LLM.
+ discord_leave_title: |
+ Player Left
+ discord_user_new: |
+ You are not playing the game yet! Use `!join ` to start playing.
+ discord_world_active: |
+ Hello! Welcome to {{ bot_name }}. The active world is `{{ world.name }}` (theme: {{ world.theme }})
+ discord_world_none: Hello! Welcome to {{ bot_name }}. There is no active world yet.
\ No newline at end of file
diff --git a/prompts/llama-base.yml b/prompts/llama-base.yml
new file mode 100644
index 0000000..0f652eb
--- /dev/null
+++ b/prompts/llama-base.yml
@@ -0,0 +1,378 @@
+prompts:
+ # base actions
+ action_examine_error_target: |
+ You cannot examine the {{target}} because it is not in the room.
+ action_examine_broadcast_action: |
+ {{action_character | name}} looks at {{target}}.
+ action_examine_broadcast_character: |
+ {{action_character | name}} saw {{target_character | name}} in the {{action_room | name}} room.
+ action_examine_broadcast_inventory: |
+ {{action_character | name}} saw the {{target_item | name}} item in their inventory.
+ action_examine_broadcast_item: |
+ {{action_character | name}} saw the {{target_item | name}} item in the {{action_room | name}} room.
+ action_examine_broadcast_room: |
+ {{action_character | name}} saw the {{action_room | name}} room.
+ action_examine_result_character: |
+ You examine the {{target_character | name}}. {{ target_character | describe }}.
+ action_examine_result_inventory: |
+ You examine the {{target_item | name}}. {{target_item | describe}}.
+ action_examine_result_item: |
+ You examine the {{target_item | name}}. {{target_item | describe}}.
+ action_examine_result_room: |
+ You examine the {{target_room | name}}. {{target_room | describe}}.
+
+ action_move_error_direction: |
+ {{direction}} is not an exit from this room. Please choose a valid direction: {{portals}}.
+ action_move_error_room: |
+ You cannot move through {{direction}}, it does not lead anywhere.
+ action_move_broadcast: |
+ {{action_character | name}} moves through {{direction}} to {{dest_room | name}}.
+ action_move_result: |
+ You move through {{direction}} to {{dest_room | name}}.
+
+ action_take_error_item: |
+ You cannot take the {{item}} item because it is not in the room.
+ action_take_broadcast: |
+ {{action_character | name}} picks up the {{item}} item.
+ action_take_result: |
+ You pick up the {{item}} item and put it in your inventory.
+
+ action_ask_error_self: |
+ You cannot ask yourself a question. Stop talking to yourself. Try another action or a different character.
+ action_ask_error_target: |
+ You cannot ask {{character}} a question because they are not in the room.
+ action_ask_error_agent: |
+ You cannot ask {{character}} a question because they are not a character.
+ action_ask_broadcast: |
+ {{action_character | name}} asks {{character}}: {{question}}.
+ action_ask_conversation_first: |
+ {{last_character | name}} asks you: {{response}}
+ Reply with your response to them. Reply with 'END' to end the conversation.
+ Do not include the question or any JSON. Only include your answer for {{last_character | name}}.
+ action_ask_conversation_reply: |
+ {{last_character | name}} continues the conversation with you. They reply: {{response}}
+ Reply with your response to them. Reply with 'END' to end the conversation.
+ Do not include the question or any JSON. Only include your answer for {{last_character | name}}.
+ action_ask_conversation_end: |
+ {{last_character | name}} ends the conversation for now.
+ action_ask_ignore: |
+ {{character}} does not respond.
+
+ action_tell_error_self: |
+ You cannot tell yourself a message. Stop talking to yourself. Try taking notes during your planning phase instead.
+ action_tell_error_target: |
+ You cannot tell {{character}} a message because they are not in the room.
+ action_tell_error_agent: |
+ You cannot tell {{character}} a message because they are not a character.
+ action_tell_broadcast: |
+ {{action_character | name}} tells {{character}}: {{message}}.
+ action_tell_conversation_first: |
+ {{last_character | name}} starts a conversation with you. They say: {{response}}
+ Reply with your response to them. Reply with 'END' to end the conversation.
+ Do not include the question or any JSON. Only include your answer for {{last_character | name}}.
+ action_tell_conversation_reply: |
+ {{last_character | name}} continues the conversation with you. They reply: {{response}}
+ Reply with your response to them. Reply with 'END' to end the conversation.
+ Do not include the question or any JSON. Only include your answer for {{last_character | name}}.
+ action_tell_conversation_end: |
+ {{last_character | name}} ends the conversation for now.
+ action_tell_ignore: |
+ {{character}} does not respond.
+
+ action_give_error_target: |
+ You cannot give the {{item}} item to {{character}} because they are not in the room.
+ action_give_error_self: |
+ You cannot give the {{item}} item to yourself. Try giving it to another character in the room.
+ action_give_error_item: |
+ You cannot give the {{item}} item because it is not in your inventory or in the room.
+ action_give_broadcast: |
+ {{action_character | name}} gives the {{item}} item to {{character}}.
+ action_give_result: |
+ You give the {{item}} item to {{character}}.
+
+ action_drop_error_item: |
+ You cannot drop the {{item}} item because it is not in your inventory.
+ action_drop_broadcast: |
+ {{action_character | name}} drops the {{item}} item.
+ action_drop_result: |
+ You drop the {{item}} item.
+
+ # optional actions
+ action_explore_error_direction: |
+ You cannot explore {{direction}} from here, that direction already leads to {{dest_room}}. Please use action_move to go there.
+ action_explore_error_generating: |
+ You cannot explore {{direction}} from here, something strange happened and nothing exists in that direction.
+ action_explore_broadcast: |
+ {{action_character | name}} explores {{direction}} from {{action_room | name}} and finds a new room: {{new_room | name}}.
+ action_explore_result: |
+ You explore {{direction}} and find a new room: {{new_room | name}}.
+
+ action_search_error_full: |
+ You find nothing hidden in the room. There is no room for more items.
+ action_search_error_generating: |
+ You find nothing hidden in the room. Something strange happened and the item you were looking for is not there.
+ action_search_broadcast: |
+ {{action_character | name}} searches the room and finds a new item: {{new_item | name}}.
+ action_search_result: |
+ You search the room and find a new item: {{new_item | name}}.
+
+ action_use_error_cooldown: |
+ You cannot use the {{item}} item again so soon. Please wait a bit before trying again.
+ action_use_error_exhausted: |
+ You cannot use the {{item}} item anymore. It has been used too many times.
+ action_use_error_item: |
+ The {{item}} item is not available in your inventory or in the room.
+ action_use_error_target: |
+ The {{target}} is not in the room, so you cannot use the {{item}} item on it.
+ action_use_broadcast: |
+ {{action_character | name}} uses {{item}} on {{target}} and applies the {{effect}} effect.
+ action_use_dm_effect: |
+ {{action_character | name}} uses {{item}} on {{target}}. {{item}} can apply any of the following effects: {{effect_names}}.
+ Which effect should be applied? Specify the effect. Do not include the question or any JSON. Only reply with the effect name.
+ action_use_dm_outcome: |
+ {{action_character | name}} uses {{item}} on {{target}} and applies the {{effect | name}} effect.
+ {{action_character | describe}}. {{target_character | describe}}.
+ {{action_item | describe}}. What happens? How does {{target_character | name}} react? What is the outcome?
+ Be creative with the results. The outcome can be positive, negative, or neutral. Describe one possible outcome
+ based on the characters, items, and effects involved. Do not include the question or any JSON. Only reply with the outcome.
+
+ # planning actions
+ action_take_note_error_limit: |
+ You have reached the maximum number of notes. Please delete or summarize some of your existing notes before adding more.
+ action_take_note_error_length: |
+ The note is too long. Please keep notes under 200 characters.
+ action_take_note_error_duplicate: |
+ You already have a note about that fact. If you want to update the note, please edit or summarize the existing note.
+ action_take_note_result: |
+ You make a note of that fact.
+
+ action_erase_notes_error_empty: |
+ You have no notes to erase.
+ action_erase_notes_error_match: |
+ You have no notes that match that text.
+ action_erase_notes_result: |
+ You erased {{count}} notes.
+
+ action_edit_note_error_empty: |
+ You have no notes to edit.
+ action_edit_note_error_match: |
+ You have no notes that match that text.
+ action_edit_note_result: |
+ You edited that note.
+
+ action_summarize_notes_error_empty: |
+ You have no notes to summarize.
+ action_summarize_notes_error_limit: |
+ You still have too many notes. Please condense them further, you can only have up to {{limit}} notes.
+ action_summarize_notes_prompt: |
+ Please summarize your notes. Remove any duplicates and combine similar notes.
+ 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. Do not number the lines
+ in your response. Do not include any JSON or other information.
+ Your notes are:\n{notes}
+ action_summarize_notes_result: |
+ You summarized your notes.
+
+ action_schedule_event_error_name: |
+ The event must have a name.
+ action_schedule_event_result: |
+ You scheduled an event that will happen in {{turns}} turns.
+
+ action_check_calendar_empty: |
+ You have no upcoming events on your calendar. You can plan events with other characters during your turn.
+ Make sure you inform the other characters about the event so they can plan accordingly.
+ action_check_calendar_each: |
+ {{event.name}} will happen in {{turns}} turn
+
+ # digest system
+ digest_action_move: |
+ {{event.character | name}} entered the room.
+ digest_action_take: |
+ {{event.character | name}} picked up the {{event.parameters[item]}}.
+ digest_action_give: |
+ {{event.character | name}} gave the {{event.parameters[item]}} to {{event.parameters[character]}}.
+ digest_action_drop: |
+ {{event.character | name}} dropped the {{event.parameters[item]}}.
+ digest_action_ask: |
+ {{event.character | name}} asked {{event.parameters[character]}} about something.
+ digest_action_tell: |
+ {{event.character | name}} told {{event.parameters[character]}} about something.
+ digest_action_examine: |
+ {{event.character | name}} examined the {{event.parameters[target]}}.
+
+ # world defaults
+ world_default_dungeon_master: |
+ You are the dungeon master in charge of creating an engaging fantasy world full of interesting characters who
+ interact with each other and explore their environment. Be creative and original, creating a world that is
+ visually detailed and full of curious details. Do not repeat yourself unless you are given the same prompt with
+ the same characters, room, and context.
+
+ # world generation
+ world_generate_dungeon_master: |
+ You are an experienced dungeon master creating a visually detailed world for a new adventure. Be creative and
+ original, creating a world that is visually detailed and full of curious details. Do not repeat yourself unless you
+ are given the same prompt with the same characters, room, and context. {{flavor}}. The theme is:
+ {{theme}}.
+
+ world_generate_world_broadcast_theme: |
+ Generating a {{theme}} with {{room_count}} rooms
+
+ world_generate_room_name: |
+ Generate one room, area, or location that would make sense in the world of {{world_theme}}.
+ Only respond with the room name in title case, do not include the description or any other text.
+ Do not prefix the name with "the", do not wrap it in quotes. The existing rooms are: {{existing_rooms}}
+ world_generate_room_description: |
+ Generate a detailed description of the {{name}} area. What does it look like?
+ What does it smell like? What can be seen or heard?
+ world_generate_room_broadcast_room: |
+ Generating room: {{name}}
+ world_generate_room_broadcast_items: |
+ Generating {{item_count}} items for room: {{name}}
+ world_generate_room_broadcast_characters: |
+ Generating {{character_count}} characters for room: {{name}}
+
+ world_generate_portal_name_outgoing: |
+ Generate the name of a portal that leads from the {{source_room}} room to the {{dest_room}} room and fits the world theme of {{world_theme}}.
+ Some example portal names are: 'door', 'gate', 'archway', 'staircase', 'trapdoor', 'mirror', and 'magic circle'.
+ Only respond with the portal name in title case, do not include a description or any other text.
+ Do not prefix the name with "the", do not wrap it in quotes. Use a unique name.
+ Do not create any duplicate portals in the same room. The existing portals are: {{existing_portals}}
+ world_generate_portal_name_incoming: |
+ Generate the opposite name of the portal that leads from the {{dest_room}} room to the {{source_room}} room.
+ The name should be the opposite of the {{outgoing_name}} portal and should fit the world theme of {{world_theme}}.
+ Some example portal names are: 'door', 'gate', 'archway', 'staircase', 'trapdoor', 'mirror', and 'magic circle'.
+ Only respond with the portal name in title case, do not include a description or any other text.
+ Do not prefix the name with "the", do not wrap it in quotes. Use a unique name.
+ Do not create any duplicate portals in the same room. The existing portals are: {{existing_portals}}
+ world_generate_portal_broadcast_outgoing: |
+ Generating portal: {{outgoing_name}}
+ world_generate_portal_broadcast_incoming: |
+ Linking {{outgoing_name}} to {{incoming_name}}
+
+ world_generate_item_name: |
+ Generate a new item or object that would make sense in the world of {{world_theme}}. {{dest_note}}.
+ Only respond with the item name in title case, do not include the description or any other text.
+ Do not prefix the name with "the", do not wrap it in quotes. Use a unique name.
+ Do not create any duplicate items in the same room. Do not give characters the same item more than once.
+ The existing items are: {{existing_items}}
+ world_generate_item_description: |
+ Generate a detailed description of the {{name}} item. What does it look like?
+ What is it made of? What is its purpose or function?
+ world_generate_item_broadcast_item: |
+ Generating item: {{name}}
+ world_generate_item_broadcast_effects: |
+ Generating {{effect_count}} effects for item: {{name}}
+
+ world_generate_effect_name: |
+ Generate one effect for an {{entity_type}} named {{entity_name}} that would make sense in the world of {{theme}}.
+ Only respond with the effect name in title case, do not include a description or any other text.
+ Do not prefix the name with "the", do not wrap it in quotes. Use a unique name.
+ Do not create any duplicate effects on the same item. The existing effects are: {{existing_effects}}.
+ Some example effects are: 'fire', 'poison', 'frost', 'haste', 'slow', and 'heal'.
+ world_generate_effect_description: |
+ Generate a detailed description of the {{name}} effect. What does it look like?
+ What does it do? How does it affect the target? Describe the effect from the perspective of an outside observer.
+ world_generate_effect_application: |
+ How should the {{name}} effect be applied? Respond with 'temporary' for a temporary effect that lasts for a duration,
+ or 'permanent' for a permanent effect that immediately modifies the target.
+ For example, a healing potion would be a permanent effect that increases health every turn,
+ while bleeding would be a temporary effect that decreases health every turn.
+ A haste potion would be a temporary effect that increases speed for a duration,
+ while a slow spell would be a temporary effect that decreases speed for a duration.
+ Do not include any other text. Do not use JSON.
+ world_generate_effect_cooldown: |
+ How many turns should the {{name}} effect wait before it can be used again?
+ Enter a positive number to set a cooldown, or 0 for no cooldown.
+ Do not include any other text. Do not use JSON.
+ world_generate_effect_duration: |
+ How many turns does the {{name}} effect last? Enter a positive number to set a duration, or 0 for an instant effect.
+ Do not include any other text. Do not use JSON.
+ world_generate_effect_uses: |
+ How many times can the {{name}} effect be used before it is exhausted?
+ Enter a positive number to set a limit, or -1 for unlimited uses.
+ Do not include any other text. Do not use JSON.
+ world_generate_effect_attribute_names: |
+ Generate a short list of attributes that the {{name}} effect modifies. Include 1 to 3 attributes.
+ For example, 'heal' increases the target's 'health' attribute, while 'poison' decreases it.
+ Use a comma-separated list of attribute names, such as 'health, strength, speed'.
+ Only include the attribute names, do not include the question or any JSON.
+ world_generate_effect_attribute_value: |
+ How much does the {{name}} effect modify the {{attribute_name}} attribute?
+ For example, heal might add 10 to the health attribute, while poison might remove -5 from it.
+ Enter a positive number to increase the attribute or a negative number to decrease it.
+ Do not include any other text. Do not use JSON.
+ world_generate_effect_broadcast_effect: |
+ Generating effect: {{name}}
+ world_generate_effect_error_application: |
+ The application must be either 'temporary' or 'permanent'.
+
+ world_generate_character_name: |
+ Generate a new character that would make sense in the world of {{world_theme}}.
+ Characters can be a person, creature, or some other intelligent entity.
+ The character will be placed in the {{dest_room}} room. {{additional_prompt}}.
+ Only respond with the character name in title case, do not include a description or any other text.
+ Do not prefix the name with "the", do not wrap it in quotes.
+ Do not include the name of the room. Do not give characters any duplicate names.
+ Do not create any duplicate characters. The existing characters are: {{existing_characters}}
+ world_generate_character_description: |
+ Generate a detailed description of {{name}}. {{detail_prompt}}. What do they look like? What are they wearing?
+ What are they doing? Describe their appearance and demeanor from the perspective of an outside observer.
+ Do not include the room or any other characters in the description, because they will change over time.
+ world_generate_character_backstory: |
+ Generate a backstory for the {{name}} character. {{additional_prompt}}. {{detail_prompt}}. Where are they from?
+ What are they doing here? What are their goals?
+ Make sure to phrase the backstory in the second person, starting with "you are" and speaking directly to {{name}}.
+ world_generate_character_broadcast_name: |
+ Generating character: {{name}}
+ world_generate_character_broadcast_items: |
+ Generating {{item_count}} items for character: {{name}}
+
+ world_generate_link_broadcast_portals: |
+ Generating {{portal_count}} portals for room: {{name}}
+
+ world_generate_error_name_exists: |
+ The name '{{name}}' already exists in the world. Please generate a unique name.
+ world_generate_error_name_json: |
+ The name '{{name}}' is not valid. The name cannot contain any JSON or function calls.
+ world_generate_error_name_punctuation: |
+ The name '{{name}}' is not valid. The name cannot contain any quotes, colons, or other sentence punctuation.
+ Apostrophes are allowed in names like "O'Connell" or "D'Artagnan".
+ world_generate_error_name_length: |
+ The name '{{name}}' is too long. Please generate a shorter name with fewer than 50 characters.
+
+ # world simulation
+ world_simulate_character_action: |
+ You are currently in the {{room_name}} room. {{room_description}}. {{attributes}}.
+ The room contains the following characters: {{visible_characters}}.
+ The room contains the following items: {{visible_items}}.
+ Your inventory contains the following items: {{character_items}}.
+ You can take the following actions: {{actions}}.
+ You can move in the following directions: {{directions}}.
+ {{notes_prompt}} {{events_prompt}}
+ What will you do next? Reply with a JSON function call, calling one of the actions.
+ You can only perform one action per turn. What is your next action?
+
+ world_simulate_character_planning: |
+ You are about to start your turn. Plan your next action carefully. Take notes and schedule events to help keep track of your goals.
+ You can check your notes for important facts or check your calendar for upcoming events. You have {{note_count}} notes.
+ If you have plans with other characters, schedule them on your calendar. You have {{event_count}} events on your calendar.
+ {{room_summary}}
+ Think about your goals and any quests that you are working on, and plan your next action accordingly.
+ Try to keep your notes accurate and up-to-date. Replace or erase old notes when they are no longer accurate or useful.
+ Do not keeps notes about upcoming events, use your calendar for that.
+ You can perform up to 3 planning actions in a single turn. When you are done planning, reply with 'END'.
+ {{notes_prompt}} {{events_prompt}}
+ world_simulate_character_planning_done: |
+ You are done planning your turn.
+ world_simulate_character_planning_notes_some: |
+ Your recent notes are: {{notes}}
+ world_simulate_character_planning_notes_none: |
+ You have no recent notes.
+ world_simulate_character_planning_events_some: |
+ Your upcoming events are: {{events}}
+ world_simulate_character_planning_events_none: |
+ You have no upcoming events.
+ world_simulate_character_planning_events_item: |
+ {{event.name}} in {{turns}} turns
\ No newline at end of file
diff --git a/prompts/llama-quest.yml b/prompts/llama-quest.yml
new file mode 100644
index 0000000..d5c79ca
--- /dev/null
+++ b/prompts/llama-quest.yml
@@ -0,0 +1,18 @@
+prompts:
+ action_accept_quest_error_none: No quests are available at the moment.
+ action_accept_quest_error_name: |
+ {{character}} does not have a quest named "{{quest_name}}".
+ action_accept_quest_error_room: |
+ {{character}} is not in the room.
+ action_accept_quest_result: |
+ You have started the quest "{{quest_name}}".
+
+ action_submit_quest_error_active: |
+ You do not have any active quests.
+ action_submit_quest_error_none: No quests are available at the moment.
+ action_submit_quest_error_name: |
+ {{character}} does not have a quest named "{{quest_name}}".
+ action_submit_quest_error_room: |
+ {{character}} is not in the room.
+ action_submit_quest_result: |
+ You have completed the quest "{{quest_name}}".
\ No newline at end of file
diff --git a/taleweave/actions/base.py b/taleweave/actions/base.py
index 104a099..a852fee 100644
--- a/taleweave/actions/base.py
+++ b/taleweave/actions/base.py
@@ -5,10 +5,12 @@
broadcast,
get_agent_for_character,
get_character_agent_for_name,
+ get_prompt,
world_context,
)
from taleweave.errors import ActionError
from taleweave.utils.conversation import loop_conversation
+from taleweave.utils.prompt import format_prompt
from taleweave.utils.search import (
find_character_in_room,
find_item_in_character,
@@ -17,7 +19,6 @@
find_room,
)
from taleweave.utils.string import normalize_name
-from taleweave.utils.world import describe_entity
logger = getLogger(__name__)
@@ -33,34 +34,65 @@ def action_examine(target: str) -> str:
"""
with action_context() as (action_room, action_character):
- broadcast(f"{action_character.name} looks at {target}")
+ broadcast(
+ format_prompt(
+ "action_examine_broadcast_action",
+ action_character=action_character,
+ target=target,
+ )
+ )
if normalize_name(target) == normalize_name(action_room.name):
- broadcast(f"{action_character.name} saw the {action_room.name} room")
- return describe_entity(action_room)
+ broadcast(
+ format_prompt(
+ "action_examine_broadcast_room",
+ action_character=action_character,
+ action_room=action_room,
+ )
+ )
+ return format_prompt("action_examine_result_room", action_room=action_room)
target_character = find_character_in_room(action_room, target)
if target_character:
broadcast(
- f"{action_character.name} saw the {target_character.name} character in the {action_room.name} room"
+ format_prompt(
+ "action_examine_broadcast_character",
+ action_character=action_character,
+ action_room=action_room,
+ target_character=target_character,
+ )
+ )
+ return format_prompt(
+ "action_examine_result_character", target_character=target_character
)
- return describe_entity(target_character)
target_item = find_item_in_room(action_room, target)
if target_item:
broadcast(
- f"{action_character.name} saw the {target_item.name} item in the {action_room.name} room"
+ format_prompt(
+ "action_examine_broadcast_item",
+ action_character=action_character,
+ action_room=action_room,
+ target_item=target_item,
+ )
)
- return describe_entity(target_item)
+ return format_prompt("action_examine_result_item", target_item=target_item)
target_item = find_item_in_character(action_character, target)
if target_item:
broadcast(
- f"{action_character.name} saw the {target_item.name} item in their inventory"
+ format_prompt(
+ "action_examine_broadcast_inventory",
+ action_character=action_character,
+ action_room=action_room,
+ target_item=target_item,
+ )
+ )
+ return format_prompt(
+ "action_examine_result_inventory", target_item=target_item
)
- return describe_entity(target_item)
- return "You do not see that item or character in the room."
+ return format_prompt("action_examine_error_target", target=target)
def action_move(direction: str) -> str:
@@ -74,20 +106,36 @@ def action_move(direction: str) -> str:
with world_context() as (action_world, action_room, action_character):
portal = find_portal_in_room(action_room, direction)
if not portal:
- raise ActionError(f"You cannot move {direction} from here.")
+ portals = [p.name for p in action_room.portals]
+ raise ActionError(
+ format_prompt(
+ "action_move_error_direction", direction=direction, portals=portals
+ )
+ )
- destination_room = find_room(action_world, portal.destination)
- if not destination_room:
- raise ActionError(f"The {portal.destination} room does not exist.")
+ dest_room = find_room(action_world, portal.destination)
+ if not dest_room:
+ raise ActionError(
+ format_prompt(
+ "action_move_error_room",
+ direction=direction,
+ destination=portal.destination,
+ )
+ )
broadcast(
- f"{action_character.name} moves through {direction} to {destination_room.name}"
+ format_prompt(
+ "action_move_broadcast",
+ action_character=action_character,
+ dest_room=dest_room,
+ direction=direction,
+ )
)
action_room.characters.remove(action_character)
- destination_room.characters.append(action_character)
+ dest_room.characters.append(action_character)
- return (
- f"You move through the {direction} and arrive at {destination_room.name}."
+ return format_prompt(
+ "action_move_result", direction=direction, dest_room=dest_room
)
@@ -101,12 +149,20 @@ def action_take(item: str) -> str:
with action_context() as (action_room, action_character):
action_item = find_item_in_room(action_room, item)
if not action_item:
- raise ActionError(f"The {item} item is not in the room.")
+ raise ActionError(format_prompt("action_take_error_item", item=item))
- broadcast(f"{action_character.name} takes the {item} item")
+ broadcast(
+ format_prompt(
+ "action_take_broadcast",
+ action_character=action_character,
+ action_room=action_room,
+ item=item,
+ )
+ )
action_room.items.remove(action_item)
action_character.items.append(action_item)
- return f"You take the {item} item and put it in your inventory."
+
+ return format_prompt("action_take_result", item=item)
def action_ask(character: str, question: str) -> str:
@@ -122,27 +178,31 @@ def action_ask(character: str, question: str) -> str:
# sanity checks
question_character, question_agent = get_character_agent_for_name(character)
if question_character == action_character:
- raise ActionError(
- "You cannot ask yourself a question. Stop talking to yourself. Try another action."
- )
+ raise ActionError(format_prompt("action_ask_error_self"))
if not question_character:
- raise ActionError(f"The {character} character is not in the room.")
+ raise ActionError(
+ format_prompt("action_ask_error_target", character=character)
+ )
if not question_agent:
- raise ActionError(f"The {character} character does not exist.")
+ raise ActionError(
+ format_prompt("action_ask_error_agent", character=character)
+ )
- broadcast(f"{action_character.name} asks {character}: {question}")
- first_prompt = (
- "{last_character.name} asks you: {response}\n"
- "Reply with your response to them. Reply with 'END' to end the conversation. "
- "Do not include the question or any JSON. Only include your answer for {last_character.name}."
- )
- reply_prompt = (
- "{last_character.name} continues the conversation with you. They reply: {response}\n"
- "Reply with your response to them. Reply with 'END' to end the conversation. "
- "Do not include the question or any JSON. Only include your answer for {last_character.name}."
+ # TODO: make sure they are in the same room
+
+ broadcast(
+ format_prompt(
+ "action_ask_broadcast",
+ action_character=action_character,
+ character=character,
+ question=question,
+ )
)
+ first_prompt = get_prompt("action_ask_conversation_first")
+ reply_prompt = get_prompt("action_ask_conversation_reply")
+ end_prompt = get_prompt("action_ask_conversation_end")
action_agent = get_agent_for_character(action_character)
result = loop_conversation(
@@ -153,7 +213,7 @@ def action_ask(character: str, question: str) -> str:
first_prompt,
reply_prompt,
question,
- "Goodbye",
+ end_prompt,
echo_function=action_tell.__name__,
echo_parameter="message",
max_length=MAX_CONVERSATION_STEPS,
@@ -162,7 +222,7 @@ def action_ask(character: str, question: str) -> str:
if result:
return result
- return f"{character} does not respond."
+ return format_prompt("action_ask_ignore", character=character)
def action_tell(character: str, message: str) -> str:
@@ -179,27 +239,22 @@ def action_tell(character: str, message: str) -> str:
# sanity checks
question_character, question_agent = get_character_agent_for_name(character)
if question_character == action_character:
- raise ActionError(
- "You cannot tell yourself a message. Stop talking to yourself. Try another action."
- )
+ raise ActionError(format_prompt("action_tell_error_self"))
if not question_character:
- raise ActionError(f"The {character} character is not in the room.")
+ raise ActionError(
+ format_prompt("action_tell_error_target", character=character)
+ )
if not question_agent:
- raise ActionError(f"The {character} character does not exist.")
+ raise ActionError(
+ format_prompt("action_tell_error_agent", character=character)
+ )
broadcast(f"{action_character.name} tells {character}: {message}")
- first_prompt = (
- "{last_character.name} starts a conversation with you. They say: {response}\n"
- "Reply with your response to them. "
- "Do not include the message or any JSON. Only include your reply to {last_character.name}."
- )
- reply_prompt = (
- "{last_character.name} continues the conversation with you. They reply: {response}\n"
- "Reply with your response to them. "
- "Do not include the message or any JSON. Only include your reply to {last_character.name}."
- )
+ first_prompt = get_prompt("action_tell_conversation_first")
+ reply_prompt = get_prompt("action_tell_conversation_reply")
+ end_prompt = get_prompt("action_tell_conversation_end")
action_agent = get_agent_for_character(action_character)
result = loop_conversation(
@@ -210,7 +265,7 @@ def action_tell(character: str, message: str) -> str:
first_prompt,
reply_prompt,
message,
- "Goodbye",
+ end_prompt,
echo_function=action_tell.__name__,
echo_parameter="message",
max_length=MAX_CONVERSATION_STEPS,
@@ -219,7 +274,7 @@ def action_tell(character: str, message: str) -> str:
if result:
return result
- return f"{character} does not respond."
+ return format_prompt("action_tell_ignore", character=character)
def action_give(character: str, item: str) -> str:
@@ -233,22 +288,29 @@ def action_give(character: str, item: str) -> str:
with action_context() as (action_room, action_character):
destination_character = find_character_in_room(action_room, character)
if not destination_character:
- raise ActionError(f"The {character} character is not in the room.")
-
- if destination_character == action_character:
raise ActionError(
- "You cannot give an item to yourself. Try another action."
+ format_prompt("action_give_error_target", character=character)
)
+ if destination_character == action_character:
+ raise ActionError(format_prompt("action_give_error_self"))
+
action_item = find_item_in_character(action_character, item)
if not action_item:
- raise ActionError(f"You do not have the {item} item in your inventory.")
+ raise ActionError(format_prompt("action_give_error_item", item=item))
- broadcast(f"{action_character.name} gives {character} the {item} item.")
+ broadcast(
+ format_prompt(
+ "action_give_broadcast",
+ action_character=action_character,
+ character=character,
+ item=item,
+ )
+ )
action_character.items.remove(action_item)
destination_character.items.append(action_item)
- return f"You give the {item} item to {character}."
+ return format_prompt("action_give_result", character=character, item=item)
def action_drop(item: str) -> str:
@@ -262,10 +324,14 @@ def action_drop(item: str) -> str:
with action_context() as (action_room, action_character):
action_item = find_item_in_character(action_character, item)
if not action_item:
- raise ActionError(f"You do not have the {item} item in your inventory.")
+ raise ActionError(format_prompt("action_drop_error_item", item=item))
- broadcast(f"{action_character.name} drops the {item} item")
+ broadcast(
+ format_prompt(
+ "action_drop_broadcast", action_character=action_character, item=item
+ )
+ )
action_character.items.remove(action_item)
action_room.items.append(action_item)
- return f"You drop the {item} item."
+ return format_prompt("action_drop_result", item=item)
diff --git a/taleweave/actions/optional.py b/taleweave/actions/optional.py
index 740eadf..0883ef1 100644
--- a/taleweave/actions/optional.py
+++ b/taleweave/actions/optional.py
@@ -15,11 +15,17 @@
world_context,
)
from taleweave.errors import ActionError
-from taleweave.generate import generate_item, generate_room, link_rooms
+from taleweave.generate import (
+ generate_item,
+ generate_portals,
+ generate_room,
+ link_rooms,
+)
from taleweave.utils.effect import apply_effects, is_effect_ready
+from taleweave.utils.prompt import format_prompt
from taleweave.utils.search import find_character_in_room
from taleweave.utils.string import normalize_name
-from taleweave.utils.world import describe_character, describe_entity
+from taleweave.utils.world import describe_entity
logger = getLogger(__name__)
@@ -29,7 +35,7 @@
set_dungeon_master(
Agent(
"dungeon master",
- "You are the dungeon master in charge of a fantasy world.",
+ format_prompt("world_default_dungeon_master"),
{},
llm,
)
@@ -50,8 +56,11 @@ def action_explore(direction: str) -> str:
if direction in action_room.portals:
dest_room = action_room.portals[direction]
raise ActionError(
- f"You cannot explore {direction} from here, that direction already leads to {dest_room}. "
- "Please use the move action to go there."
+ format_prompt(
+ "action_explore_error_direction",
+ direction=direction,
+ dest_room=dest_room,
+ )
)
try:
@@ -59,16 +68,34 @@ def action_explore(direction: str) -> str:
new_room = generate_room(dungeon_master, action_world, systems)
action_world.rooms.append(new_room)
- # link the rooms together
+ # link the rooms together, starting with the current room
+ outgoing_portal, incoming_portal = generate_portals(
+ dungeon_master,
+ action_world,
+ action_room,
+ new_room,
+ systems,
+ outgoing_name=direction,
+ )
+ action_room.portals.append(outgoing_portal)
+ new_room.portals.append(incoming_portal)
link_rooms(dungeon_master, action_world, systems, [new_room])
broadcast(
- f"{action_character.name} explores {direction} of {action_room.name} and finds a new room: {new_room.name}"
+ format_prompt(
+ "action_explore_broadcast",
+ action_character=action_character,
+ action_room=action_room,
+ direction=direction,
+ new_room=new_room,
+ )
+ )
+ return format_prompt(
+ "action_explore_result", direction=direction, new_room=new_room
)
- return f"You explore {direction} and find a new room: {new_room.name}"
except Exception:
logger.exception("error generating room")
- return f"You cannot explore {direction} from here, there is no room in that direction."
+ return format_prompt("action_explore_error_generating", direction=direction)
def action_search(unused: bool) -> str:
@@ -80,9 +107,7 @@ def action_search(unused: bool) -> str:
dungeon_master = get_dungeon_master()
if len(action_room.items) > 2:
- return (
- "You find nothing hidden in the room. There is no room for more items."
- )
+ return format_prompt("action_search_error_full")
try:
systems = get_game_systems()
@@ -95,12 +120,17 @@ def action_search(unused: bool) -> str:
action_room.items.append(new_item)
broadcast(
- f"{action_character.name} searches {action_room.name} and finds a new item: {new_item.name}"
+ format_prompt(
+ "action_search_broadcast",
+ action_character=action_character,
+ action_room=action_room,
+ new_item=new_item,
+ )
)
- return f"You search the room and find a new item: {new_item.name}"
+ return format_prompt("action_search_result", new_item=new_item)
except Exception:
logger.exception("error generating item")
- return "You find nothing hidden in the room."
+ return format_prompt("action_search_error_generating")
def action_use(item: str, target: str) -> str:
@@ -118,12 +148,12 @@ def action_use(item: str, target: str) -> str:
(
search_item
for search_item in (action_character.items + action_room.items)
- if search_item.name == item
+ if normalize_name(search_item.name) == normalize_name(item)
),
None,
)
if not action_item:
- raise ActionError(f"The {item} item is not available to use.")
+ raise ActionError(format_prompt("action_use_error_item", item=item))
if target == "self":
target_character = action_character
@@ -132,19 +162,22 @@ def action_use(item: str, target: str) -> str:
# TODO: allow targeting the room itself and items in the room
target_character = find_character_in_room(action_room, target)
if not target_character:
- return f"The {target} character is not in the room."
+ return format_prompt("action_use_error_target", target=target)
effect_names = [effect.name for effect in action_item.effects]
# TODO: should use a retry loop and enum result parser
chosen_name = dungeon_master(
- f"{action_character.name} uses {item} on {target}. "
- f"{item} has the following effects: {effect_names}. "
- "Which effect should be applied? Specify the name of the effect to apply."
- "Do not include the question or any JSON. Only include the name of the effect to apply."
+ format_prompt(
+ "action_use_dm_effect",
+ action_character=action_character,
+ item=item,
+ target=target,
+ effect_names=effect_names,
+ )
)
chosen_name = normalize_name(chosen_name)
- chosen_effect = next(
+ effect = next(
(
search_effect
for search_effect in action_item.effects
@@ -152,46 +185,56 @@ def action_use(item: str, target: str) -> str:
),
None,
)
- if not chosen_effect:
+ if not effect:
raise ValueError(f"The {chosen_name} effect is not available to apply.")
current_turn = get_current_turn()
- effect_ready = is_effect_ready(chosen_effect, current_turn)
+ effect_ready = is_effect_ready(effect, current_turn)
if effect_ready == "cooldown":
raise ActionError(
- f"The {chosen_name} effect of {item} is still cooling down and is not ready to use yet."
+ format_prompt("action_use_error_cooldown", effect=effect, item=item)
)
elif effect_ready == "exhausted":
raise ActionError(
- f"The {chosen_name} effect of {item} has no uses remaining."
+ format_prompt("action_use_error_exhausted", effect=effect, item=item)
)
- elif chosen_effect.uses is not None:
- chosen_effect.uses -= 1
+ elif effect.uses is not None:
+ effect.uses -= 1
- chosen_effect.last_used = current_turn
+ effect.last_used = current_turn
try:
- apply_effects(target_character, [chosen_effect])
+ apply_effects(target_character, [effect])
except Exception:
- logger.exception("error applying effect: %s", chosen_effect)
+ logger.exception("error applying effect: %s", effect)
raise ValueError(
f"There was a problem applying the {chosen_name} effect while using the {item} item."
)
broadcast(
- f"{action_character.name} uses the {chosen_name} effect of {item} on {target}"
+ format_prompt(
+ "action_use_broadcast",
+ action_character=action_character,
+ effect=effect,
+ item=item,
+ target=target,
+ )
)
outcome = dungeon_master(
- f"{action_character.name} uses the {chosen_name} effect of {item} on {target}. "
- 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."
+ format_prompt(
+ "action_use_dm_outcome",
+ action_character=action_character,
+ action_item=action_item,
+ describe_entity=describe_entity,
+ effect=effect,
+ item=item,
+ target_character=target_character,
+ )
)
- broadcast(f"The action resulted in: {outcome}")
+ broadcast(
+ f"The action resulted in: {outcome}"
+ ) # TODO: should this be removed or moved to the prompt library?
# make sure both agents remember the outcome
target_agent = get_agent_for_character(target_character)
diff --git a/taleweave/actions/planning.py b/taleweave/actions/planning.py
index b1e0793..fd98459 100644
--- a/taleweave/actions/planning.py
+++ b/taleweave/actions/planning.py
@@ -1,8 +1,14 @@
-from taleweave.context import action_context, get_agent_for_character, get_current_turn
+from taleweave.context import (
+ action_context,
+ get_agent_for_character,
+ get_current_turn,
+ get_prompt,
+)
from taleweave.errors import ActionError
from taleweave.models.config import DEFAULT_CONFIG
from taleweave.models.planning import CalendarEvent
from taleweave.utils.planning import get_recent_notes
+from taleweave.utils.prompt import format_prompt
character_config = DEFAULT_CONFIG.world.character
@@ -18,19 +24,14 @@ 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. You do not need to take duplicate notes. "
- "If you have too many notes, consider erasing, replacing, or summarizing them."
- )
+ raise ActionError(get_prompt("action_take_note_error_duplicate"))
if len(action_character.planner.notes) >= character_config.note_limit:
- raise ActionError(
- "You have reached the limit of notes you can take. Please erase, replace, or summarize some notes."
- )
+ raise ActionError(get_prompt("action_take_note_error_limit"))
action_character.planner.notes.append(fact)
- return "You make a note of that fact."
+ return get_prompt("action_take_note_result")
def read_notes(unused: bool, count: int = 10):
@@ -55,21 +56,25 @@ def erase_notes(prefix: str) -> str:
"""
with action_context() as (_, action_character):
+ if len(action_character.planner.notes) == 0:
+ raise ActionError(get_prompt("action_erase_notes_error_empty"))
+
matches = [
note for note in action_character.planner.notes if note.startswith(prefix)
]
if not matches:
- return "No notes found with that prefix."
+ raise ActionError(get_prompt("action_erase_notes_error_match"))
action_character.planner.notes[:] = [
note for note in action_character.planner.notes if note not in matches
]
- return f"Erased {len(matches)} notes."
+
+ return format_prompt("action_erase_notes_result", count=len(matches))
-def replace_note(old: str, new: str) -> str:
+def edit_note(old: str, new: str) -> str:
"""
- Replace a note with a new note.
+ Modify a note with new details.
Args:
old: The old note to replace.
@@ -77,13 +82,17 @@ def replace_note(old: str, new: str) -> str:
"""
with action_context() as (_, action_character):
+ if len(action_character.planner.notes) == 0:
+ raise ActionError(get_prompt("action_edit_note_error_empty"))
+
if old not in action_character.planner.notes:
- return "Note not found."
+ raise ActionError(get_prompt("action_edit_note_error_match"))
action_character.planner.notes[:] = [
new if note == old else note for note in action_character.planner.notes
]
- return "Note replaced."
+
+ return get_prompt("action_edit_note_result")
def summarize_notes(limit: int) -> str:
@@ -96,19 +105,16 @@ def summarize_notes(limit: int) -> str:
with action_context() as (_, action_character):
notes = action_character.planner.notes
+ if len(notes) == 0:
+ raise ActionError(get_prompt("action_summarize_notes_error_empty"))
+
action_agent = get_agent_for_character(action_character)
if not action_agent:
raise ActionError("Agent missing for character {action_character.name}")
summary = action_agent(
- "Please summarize your notes. Remove any duplicates and combine similar notes. "
- "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. Do not number the lines "
- "in your response. Do not include any JSON or other information. "
- "Your notes are:\n{notes}",
+ get_prompt("action_summarize_notes_prompt"),
limit=limit,
notes=notes,
)
@@ -116,11 +122,14 @@ def summarize_notes(limit: int) -> str:
new_notes = [note.strip() for note in summary.split("\n") if note.strip()]
if len(new_notes) > character_config.note_limit:
raise ActionError(
- f"Too many notes. You can only have up to {character_config.note_limit} notes."
+ format_prompt(
+ "action_summarize_notes_error_limit",
+ limit=character_config.note_limit,
+ )
)
action_character.planner.notes[:] = new_notes
- return "Notes were summarized successfully."
+ return get_prompt("action_summarize_notes_result")
def schedule_event(name: str, turns: int):
@@ -138,9 +147,12 @@ def schedule_event(name: str, turns: int):
# TODO: limit the number of events that can be scheduled
with action_context() as (_, action_character):
+ if not name:
+ raise ActionError(get_prompt("action_schedule_event_error_name"))
+
event = CalendarEvent(name, turns)
action_character.planner.calendar.events.append(event)
- return f"{name} is scheduled to happen in {turns} turns."
+ return format_prompt("action_schedule_event_result", name=name, turns=turns)
def check_calendar(count: int):
@@ -156,15 +168,16 @@ def check_calendar(count: int):
with action_context() as (_, action_character):
if len(action_character.planner.calendar.events) == 0:
- return (
- "You have no upcoming events scheduled. You can plan events with other characters or on your own. "
- "Make sure to inform others about events that involve them."
- )
+ return get_prompt("action_check_calendar_empty")
events = action_character.planner.calendar.events[:count]
return "\n".join(
[
- f"{event.name} will happen in {event.turn - current_turn} turns"
+ format_prompt(
+ "action_check_calendar_each",
+ name=event.name,
+ turn=event.turn - current_turn,
+ )
for event in events
]
)
diff --git a/taleweave/actions/quest.py b/taleweave/actions/quest.py
index 7f9422a..6b42803 100644
--- a/taleweave/actions/quest.py
+++ b/taleweave/actions/quest.py
@@ -1,4 +1,5 @@
from taleweave.context import action_context, get_system_data
+from taleweave.errors import ActionError
from taleweave.systems.quest import (
QUEST_SYSTEM,
complete_quest,
@@ -6,6 +7,7 @@
get_quests_for_character,
set_active_quest,
)
+from taleweave.utils.prompt import format_prompt
from taleweave.utils.search import find_character_in_room
@@ -17,20 +19,30 @@ def accept_quest(character: str, quest: str) -> str:
with action_context() as (action_room, action_character):
quests = get_system_data(QUEST_SYSTEM)
if not quests:
- return "No quests available."
+ raise ActionError(
+ format_prompt("action_accept_quest_error_none", character=character)
+ )
target_character = find_character_in_room(action_room, character)
if not target_character:
- return f"{character} is not in the room."
+ raise ActionError(
+ format_prompt("action_accept_quest_error_room", character=character)
+ )
available_quests = get_quests_for_character(quests, target_character)
for available_quest in available_quests:
if available_quest.name == quest:
set_active_quest(quests, action_character, available_quest)
- return f"You have accepted the quest: {quest}"
+ return format_prompt(
+ "action_accept_quest_result", character=character, quest=quest
+ )
- return f"{character} does not have the quest: {quest}"
+ raise ActionError(
+ format_prompt(
+ "action_accept_quest_error_name", character=character, quest=quest
+ )
+ )
def submit_quest(character: str) -> str:
@@ -41,18 +53,32 @@ def submit_quest(character: str) -> str:
with action_context() as (action_room, action_character):
quests = get_system_data(QUEST_SYSTEM)
if not quests:
- return "No quests available."
+ raise ActionError(
+ format_prompt("action_submit_quest_error_none", character=character)
+ )
active_quest = get_active_quest(quests, action_character)
if not active_quest:
- return "You do not have an active quest."
+ raise ActionError(
+ format_prompt("action_submit_quest_error_active", character=character)
+ )
target_character = find_character_in_room(action_room, character)
if not target_character:
- return f"{character} is not in the room."
+ raise ActionError(
+ format_prompt("action_submit_quest_error_room", character=character)
+ )
if active_quest.giver.character == target_character.name:
complete_quest(quests, action_character, active_quest)
- return f"You have completed the quest: {active_quest.name}"
+ return format_prompt(
+ "action_submit_quest_result",
+ character=character,
+ quest=active_quest.name,
+ )
- return f"{character} is not the quest giver for your active quest."
+ return format_prompt(
+ "action_submit_quest_error_name",
+ character=character,
+ quest=active_quest.name,
+ )
diff --git a/taleweave/bot/discord.py b/taleweave/bot/discord.py
index 7e1b147..c34e147 100644
--- a/taleweave/bot/discord.py
+++ b/taleweave/bot/discord.py
@@ -34,6 +34,7 @@
set_player,
)
from taleweave.render.comfy import render_event
+from taleweave.utils.prompt import format_prompt
logger = getLogger(__name__)
client = None
@@ -86,28 +87,38 @@ async def on_message(self, message):
):
world = get_current_world()
if world:
- active_world = f"Active world: {world.name} (theme: {world.theme})"
+ world_message = format_prompt(
+ "discord_world_active", bot_name=bot_config.name_title, world=world
+ )
else:
- active_world = "No active world"
+ world_message = format_prompt(
+ "discord_world_none", bot_name=bot_config.name_title
+ )
- await message.channel.send(
- f"Hello! Welcome to {bot_config.name_title}! {active_world}"
- )
+ await message.channel.send(world_message)
return
if message.content.startswith("!help"):
- await message.channel.send("Type `!join` to start playing!")
+ await message.channel.send(
+ format_prompt("discord_help", bot_name=bot_config.name_command)
+ )
return
if message.content.startswith("!join"):
character_name = remove_tags(message.content).replace("!join", "").strip()
if has_player(character_name):
- await channel.send(f"{character_name} has already been taken!")
+ await channel.send(
+ format_prompt("discord_join_error_taken", character=character_name)
+ )
return
character, agent = get_character_agent_for_name(character_name)
if not character:
- await channel.send(f"Character `{character_name}` not found!")
+ await channel.send(
+ format_prompt(
+ "discord_join_error_not_found", character=character_name
+ )
+ )
return
def prompt_player(event: PromptEvent):
@@ -156,9 +167,7 @@ def prompt_player(event: PromptEvent):
)
return
- await message.channel.send(
- "You are not currently playing Adventure! Type `!join` to start playing!"
- )
+ await message.channel.send(format_prompt("discord_user_new"))
return
@@ -317,8 +326,10 @@ def embed_from_event(event: GameEvent) -> Embed | None:
return embed_from_generate(event)
elif isinstance(event, ResultEvent):
return embed_from_result(event)
- elif isinstance(event, (ActionEvent, ReplyEvent)):
+ elif isinstance(event, ActionEvent):
return embed_from_action(event)
+ elif isinstance(event, ReplyEvent):
+ return embed_from_reply(event)
elif isinstance(event, StatusEvent):
return embed_from_status(event)
elif isinstance(event, PlayerEvent):
@@ -329,23 +340,25 @@ def embed_from_event(event: GameEvent) -> Embed | None:
logger.warning("unknown event type: %s", event)
-def embed_from_action(event: ActionEvent | ReplyEvent):
- action_embed = Embed(title=event.room.name, description=event.speaker.name)
-
- if isinstance(event, ActionEvent):
- action_name = event.action.replace("action_", "").title()
- action_parameters = event.parameters
+def embed_from_action(event: ActionEvent):
+ action_embed = Embed(title=event.room.name, description=event.character.name)
+ action_name = event.action.replace("action_", "").title()
+ action_parameters = event.parameters
- action_embed.add_field(name="Action", value=action_name)
+ action_embed.add_field(name="Action", value=action_name)
- for key, value in action_parameters.items():
- action_embed.add_field(name=key.replace("_", " ").title(), value=value)
- else:
- action_embed.add_field(name="Message", value=event.text)
+ for key, value in action_parameters.items():
+ action_embed.add_field(name=key.replace("_", " ").title(), value=value)
return action_embed
+def embed_from_reply(event: ReplyEvent):
+ reply_embed = Embed(title=event.room.name, description=event.speaker.name)
+ reply_embed.add_field(name="Reply", value=event.text)
+ return reply_embed
+
+
def embed_from_generate(event: GenerateEvent) -> Embed:
generate_embed = Embed(title="Generating", description=event.name)
return generate_embed
@@ -363,11 +376,11 @@ def embed_from_result(event: ResultEvent):
def embed_from_player(event: PlayerEvent):
if event.status == "join":
- title = "Player Joined"
- description = f"{event.client} is now playing as {event.character}"
+ title = format_prompt("discord_join_title", event=event)
+ description = format_prompt("discord_join_result", event=event)
else:
- title = "Player Left"
- description = f"{event.client} has left the game. {event.character} is now controlled by an LLM"
+ title = format_prompt("discord_leave_title", event=event)
+ description = format_prompt("discord_leave_result", event=event)
player_embed = Embed(title=title, description=description)
return player_embed
diff --git a/taleweave/context.py b/taleweave/context.py
index 54af28e..3fdfa81 100644
--- a/taleweave/context.py
+++ b/taleweave/context.py
@@ -20,6 +20,7 @@
from taleweave.game_system import GameSystem
from taleweave.models.entity import Character, Room, World
from taleweave.models.event import GameEvent, StatusEvent
+from taleweave.models.prompt import PromptLibrary
from taleweave.utils.string import normalize_name
logger = getLogger(__name__)
@@ -34,6 +35,7 @@
# game context
event_emitter = EventEmitter()
game_systems: List[GameSystem] = []
+prompt_library: PromptLibrary = PromptLibrary(prompts={})
system_data: Dict[str, Any] = {}
@@ -44,7 +46,7 @@
def get_event_name(event: GameEvent | Type[GameEvent]):
- return f"event:{event.type}"
+ return f"event.{event.type}"
def broadcast(message: str | GameEvent):
@@ -162,6 +164,14 @@ def get_game_systems() -> List[GameSystem]:
return game_systems
+def get_prompt(name: str) -> str:
+ return prompt_library.prompts[name]
+
+
+def get_prompt_library() -> PromptLibrary:
+ return prompt_library
+
+
def get_system_data(system: str) -> Any | None:
return system_data.get(system)
@@ -204,6 +214,11 @@ def set_game_systems(systems: Sequence[GameSystem]):
game_systems = list(systems)
+def set_prompt_library(library: PromptLibrary):
+ global prompt_library
+ prompt_library = library
+
+
def set_system_data(system: str, data: Any):
system_data[system] = data
diff --git a/taleweave/generate.py b/taleweave/generate.py
index a89826b..cbc9d09 100644
--- a/taleweave/generate.py
+++ b/taleweave/generate.py
@@ -7,7 +7,7 @@
from packit.results import enum_result, int_result
from packit.utils import could_be_json
-from taleweave.context import broadcast, set_current_world, set_system_data
+from taleweave.context import broadcast, get_prompt, set_current_world, set_system_data
from taleweave.game_system import GameSystem
from taleweave.models.config import DEFAULT_CONFIG, WorldConfig
from taleweave.models.effect import (
@@ -20,6 +20,7 @@
from taleweave.models.event import GenerateEvent
from taleweave.utils import try_parse_float, try_parse_int
from taleweave.utils.effect import resolve_int_range
+from taleweave.utils.prompt import format_prompt
from taleweave.utils.search import (
list_characters,
list_characters_in_room,
@@ -40,16 +41,24 @@ def name_parser(value: str, **kwargs):
logger.debug(f"validating generated name: {value}")
if value in existing_names:
- raise ValueError(f'"{value}" has already been used.')
+ raise ValueError(
+ format_prompt("world_generate_error_name_exists", name=value)
+ )
if could_be_json(value):
- raise ValueError("The name cannot contain JSON or other commands.")
+ raise ValueError(
+ format_prompt("world_generate_error_name_json", name=value)
+ )
if '"' in value or ":" in value:
- raise ValueError("The name cannot contain quotes or colons.")
+ raise ValueError(
+ format_prompt("world_generate_error_name_punctuation", name=value)
+ )
if len(value) > 50:
- raise ValueError("The name cannot be longer than 50 characters.")
+ raise ValueError(
+ format_prompt("world_generate_error_name_length", name=value)
+ )
return value
@@ -88,9 +97,7 @@ def generate_room(
name = loop_retry(
agent,
- "Generate one room, area, or location that would make sense in the world of {world_theme}. "
- "Only respond with the room name in title case, do not include the description or any other text. "
- 'Do not prefix the name with "the", do not wrap it in quotes. The existing rooms are: {existing_rooms}',
+ get_prompt("world_generate_room_name"),
context={
"world_theme": world.theme,
"existing_rooms": existing_rooms,
@@ -99,18 +106,18 @@ def generate_room(
toolbox=None,
)
- broadcast_generated(message=f"Generating room: {name}")
- desc = agent(
- "Generate a detailed description of the {name} area. What does it look like? "
- "What does it smell like? What can be seen or heard?",
- name=name,
- )
+ broadcast_generated(format_prompt("world_generate_room_broadcast_room", name=name))
+ desc = agent(get_prompt("world_generate_room_description"), name=name)
actions = {}
room = Room(name=name, description=desc, items=[], characters=[], actions=actions)
item_count = resolve_int_range(world_config.size.room_items) or 0
- broadcast_generated(f"Generating {item_count} items for room: {name}")
+ broadcast_generated(
+ format_prompt(
+ "world_generate_room_broadcast_items", item_count=item_count, name=name
+ )
+ )
for _ in range(item_count):
try:
@@ -128,7 +135,11 @@ def generate_room(
character_count = resolve_int_range(world_config.size.room_characters) or 0
broadcast_generated(
- message=f"Generating {character_count} characters for room: {name}"
+ format_prompt(
+ "world_generate_room_broadcast_characters",
+ character_count=character_count,
+ name=name,
+ )
)
for _ in range(character_count):
@@ -155,17 +166,14 @@ def generate_portals(
source_room: Room,
dest_room: Room,
systems: List[GameSystem],
+ outgoing_name: str | None = None,
) -> Tuple[Portal, Portal]:
existing_source_portals = [portal.name for portal in source_room.portals]
existing_dest_portals = [portal.name for portal in dest_room.portals]
- outgoing_name = loop_retry(
+ outgoing_name = outgoing_name or loop_retry(
agent,
- "Generate the name of a portal that leads from the {source_room} room to the {dest_room} room and fits the world theme of {world_theme}. "
- "Some example portal names are: 'door', 'gate', 'archway', 'staircase', 'trapdoor', 'mirror', and 'magic circle'. "
- "Only respond with the portal name in title case, do not include a description or any other text. "
- 'Do not prefix the name with "the", do not wrap it in quotes. Use a unique name. '
- "Do not create any duplicate portals in the same room. The existing portals are: {existing_portals}",
+ get_prompt("world_generate_portal_name_outgoing"),
context={
"source_room": source_room.name,
"dest_room": dest_room.name,
@@ -175,16 +183,15 @@ def generate_portals(
result_parser=duplicate_name_parser(existing_source_portals),
toolbox=None,
)
- broadcast_generated(message=f"Generating portal: {outgoing_name}")
+ broadcast_generated(
+ message=format_prompt(
+ "world_generate_portal_broadcast_outgoing", outgoing_name=outgoing_name
+ )
+ )
incoming_name = loop_retry(
agent,
- "Generate the opposite name of the portal that leads from the {dest_room} room to the {source_room} room. "
- "The name should be the opposite of the {outgoing_name} portal and should fit the world theme of {world_theme}. "
- "Some example portal names are: 'door', 'gate', 'archway', 'staircase', 'trapdoor', 'mirror', and 'magic circle'. "
- "Only respond with the portal name in title case, do not include a description or any other text. "
- 'Do not prefix the name with "the", do not wrap it in quotes. Use a unique name. '
- "Do not create any duplicate portals in the same room. The existing portals are: {existing_portals}",
+ get_prompt("world_generate_portal_name_incoming"),
context={
"source_room": source_room.name,
"dest_room": dest_room.name,
@@ -196,7 +203,15 @@ def generate_portals(
toolbox=None,
)
- broadcast_generated(message=f"Linking {outgoing_name} to {incoming_name}")
+ broadcast_generated(
+ message=format_prompt(
+ "world_generate_portal_broadcast_incoming",
+ incoming_name=incoming_name,
+ outgoing_name=outgoing_name,
+ )
+ )
+
+ # TODO: generate descriptions for the portals
outgoing_portal = Portal(
name=outgoing_name,
@@ -242,11 +257,7 @@ def generate_item(
name = loop_retry(
agent,
- "Generate one item or object that would make sense in the world of {world_theme}. {dest_note}. "
- "Only respond with the item name in title case, do not include a description or any other text. Do not prefix the "
- 'name with "the", do not wrap it in quotes. Do not include the name of the room. Use a unique name. '
- "Do not create any duplicate items in the same room. Do not give characters any duplicate items. "
- "The existing items are: {existing_items}",
+ get_prompt("world_generate_item_name"),
context={
"dest_note": dest_note,
"existing_items": existing_items,
@@ -256,18 +267,23 @@ def generate_item(
toolbox=None,
)
- broadcast_generated(message=f"Generating item: {name}")
- desc = agent(
- "Generate a detailed description of the {name} item. What does it look like? What is it made of? What does it do?",
- name=name,
+ broadcast_generated(
+ message=format_prompt("world_generate_item_broadcast_item", name=name)
)
+ desc = agent(get_prompt("world_generate_item_description"), name=name)
actions = {}
item = Item(name=name, description=desc, actions=actions)
generate_system_attributes(agent, world, item, systems)
effect_count = resolve_int_range(world_config.size.item_effects) or 0
- broadcast_generated(message=f"Generating {effect_count} effects for item: {name}")
+ broadcast_generated(
+ message=format_prompt(
+ "world_generate_item_broadcast_effects",
+ effect_count=effect_count,
+ name=name,
+ )
+ )
for _ in range(effect_count):
try:
@@ -294,12 +310,7 @@ def generate_character(
name = loop_retry(
agent,
- "Generate a new character that would make sense in the world of {world_theme}. Characters can be a person, creature, or some other intelligent entity."
- "The character will be placed in the {dest_room} room. {additional_prompt}. "
- "Only respond with the character name in title case, do not include a description or any other text. "
- 'Do not prefix the name with "the", do not wrap it in quotes. '
- "Do not include the name of the room. Do not give characters any duplicate names."
- "Do not create any duplicate characters. The existing characters are: {existing_characters}",
+ get_prompt("world_generate_character_name"),
context={
"additional_prompt": additional_prompt,
"dest_room": dest_room.name,
@@ -310,18 +321,17 @@ def generate_character(
toolbox=None,
)
- broadcast_generated(message=f"Generating character: {name}")
+ broadcast_generated(
+ message=format_prompt("world_generate_character_broadcast_name", name=name)
+ )
description = agent(
- "Generate a detailed description of the {name} character. {additional_prompt}. {detail_prompt}. What do they look like? What are they wearing? "
- "What are they doing? Describe their appearance from the perspective of an outside observer."
- "Do not include the room or any other characters in the description, because they will move around.",
+ get_prompt("world_generate_character_description"),
additional_prompt=additional_prompt,
detail_prompt=detail_prompt,
name=name,
)
backstory = agent(
- "Generate a backstory for the {name} character. {additional_prompt}. {detail_prompt}. Where are they from? What are they doing here? What are their "
- 'goals? Make sure to phrase the backstory in the second person, starting with "you are" and speaking directly to {name}.',
+ get_prompt("world_generate_character_backstory"),
additional_prompt=additional_prompt,
detail_prompt=detail_prompt,
name=name,
@@ -334,7 +344,11 @@ def generate_character(
# generate the character's inventory
item_count = resolve_int_range(world_config.size.character_items) or 0
- broadcast_generated(f"Generating {item_count} items for character {name}")
+ broadcast_generated(
+ message=format_prompt(
+ "world_generate_character_broadcast_items", item_count=item_count, name=name
+ )
+ )
for k in range(item_count):
try:
@@ -352,6 +366,7 @@ def generate_character(
logger.exception("error generating item")
if add_to_world_order:
+ # TODO: make sure characters have an agent
logger.info(f"adding character {name} to end of world turn order")
world.order.append(name)
@@ -364,11 +379,7 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
name = loop_retry(
agent,
- "Generate one effect for an {entity_type} named {entity_name} that would make sense in the world of {theme}. "
- "Only respond with the effect name in title case, do not include a description or any other text. "
- 'Do not prefix the name with "the", do not wrap it in quotes. Use a unique name. '
- "Do not create any duplicate effects on the same item. The existing effects are: {existing_effects}. "
- "Some example effects are: 'fire', 'poison', 'frost', 'haste', 'slow', and 'heal'.",
+ get_prompt("world_generate_effect_name"),
context={
"entity_name": entity.name,
"entity_type": entity_type,
@@ -378,18 +389,18 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
result_parser=duplicate_name_parser(existing_effects),
toolbox=None,
)
- broadcast_generated(message=f"Generating effect: {name}")
+ broadcast_generated(
+ message=format_prompt("world_generate_effect_broadcast_effect", name=name)
+ )
description = agent(
- "Generate a detailed description of the {name} effect. What does it look like? What does it do? "
- "How does it affect the target? Describe the effect from the perspective of an outside observer.",
+ get_prompt("world_generate_effect_description"),
name=name,
)
cooldown = loop_retry(
agent,
- f"How many turns should the {name} effect wait before it can be used again? Enter a positive number to set a cooldown, or 0 for no cooldown. "
- "Do not include any other text. Do not use JSON.",
+ get_prompt("world_generate_effect_cooldown"),
context={
"name": name,
},
@@ -399,8 +410,7 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
uses = loop_retry(
agent,
- f"How many times can the {name} effect be used before it is exhausted? Enter a positive number to set a limit, or -1 for unlimited uses. "
- "Do not include any other text. Do not use JSON.",
+ get_prompt("world_generate_effect_uses"),
context={
"name": name,
},
@@ -412,10 +422,7 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
uses = None
attribute_names = agent(
- "Generate a short list of attributes that the {name} effect modifies. Include 1 to 3 attributes. "
- "For example, 'heal' increases the target's 'health' attribute, while 'poison' decreases it. "
- "Use a comma-separated list of attribute names, such as 'health, strength, speed'. "
- "Only include the attribute names, do not include the question or any JSON.",
+ get_prompt("world_generate_effect_attribute_names"),
name=name,
)
@@ -424,10 +431,7 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
attribute_name = normalize_name(attribute_name)
if attribute_name:
value = agent(
- f"How much does the {name} effect modify the {attribute_name} attribute? "
- "For example, heal might add 10 to the health attribute, while poison might remove -5 from it."
- "Enter a positive number to increase the attribute or a negative number to decrease it. "
- "Do not include any other text. Do not use JSON.",
+ get_prompt("world_generate_effect_attribute_value"),
name=name,
attribute_name=attribute_name,
)
@@ -452,8 +456,7 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
duration = loop_retry(
agent,
- f"How many turns does the {name} effect last? Enter a positive number to set a duration, or 0 for an instant effect. "
- "Do not include any other text. Do not use JSON.",
+ get_prompt("world_generate_effect_duration"),
context={
"name": name,
},
@@ -466,19 +469,11 @@ def parse_application(value: str, **kwargs) -> str:
if value:
return value
- raise ValueError("The application must be 'temporary' or 'permanent'.")
+ raise ValueError(get_prompt("world_generate_effect_error_application"))
application = loop_retry(
agent,
- (
- f"How should the {name} effect be applied? Respond with 'temporary' for a temporary effect that lasts for a duration, "
- "or 'permanent' for a permanent effect that immediately modifies the target. "
- "For example, a healing potion would be a permanent effect that increases health every turn, "
- "while bleeding would be a temporary effect that decreases health every turn. "
- "A haste potion would be a temporary effect that increases speed for a duration, "
- "while a slow spell would be a temporary effect that decreases speed for a duration. "
- "Do not include any other text. Do not use JSON."
- ),
+ get_prompt("world_generate_effect_application"),
context={
"name": name,
},
@@ -513,7 +508,11 @@ def link_rooms(
continue
broadcast_generated(
- message=f"Generating {num_portals} portals for room: {room.name}"
+ format_prompt(
+ "world_generate_room_broadcast_portals",
+ num_portals=num_portals,
+ name=room.name,
+ )
)
for _ in range(num_portals):
@@ -553,7 +552,7 @@ def generate_world(
) -> World:
room_count = room_count or resolve_int_range(world_config.size.rooms) or 0
- broadcast_generated(message=f"Generating a {theme} with {room_count} rooms")
+ broadcast_generated(message=format_prompt("world_generate_world_broadcast_theme"))
world = World(name=name, rooms=[], theme=theme, order=[])
set_current_world(world)
diff --git a/taleweave/main.py b/taleweave/main.py
index 04253ba..9e918be 100644
--- a/taleweave/main.py
+++ b/taleweave/main.py
@@ -31,6 +31,7 @@
if True:
from taleweave.context import (
+ get_prompt_library,
get_system_data,
set_current_turn,
set_dungeon_master,
@@ -43,6 +44,7 @@
from taleweave.models.entity import World, WorldState
from taleweave.models.event import GenerateEvent
from taleweave.models.files import PromptFile, WorldPrompt
+ from taleweave.models.prompt import PromptLibrary
from taleweave.plugins import load_plugin
from taleweave.simulate import simulate_world
from taleweave.state import (
@@ -51,6 +53,7 @@
save_world,
save_world_state,
)
+ from taleweave.utils.prompt import format_prompt
# start the debugger, if needed
if environ.get("DEBUG", "false").lower() == "true":
@@ -116,6 +119,12 @@ def parse_args():
type=str,
help="The name of the character to play as",
)
+ parser.add_argument(
+ "--prompts",
+ type=str,
+ nargs="*",
+ help="The file to load game prompts from",
+ )
parser.add_argument(
"--render",
action="store_true",
@@ -191,6 +200,18 @@ def get_world_prompt(args) -> WorldPrompt:
)
+def load_prompt_library(args) -> None:
+ if args.prompts:
+ for prompt_file in args.prompts:
+ with open(prompt_file, "r") as f:
+ new_library = PromptLibrary(**load_yaml(f))
+ logger.info(f"loaded prompt library from {args.prompts}")
+ library = get_prompt_library()
+ library.prompts.update(new_library.prompts)
+
+ return None
+
+
def load_or_initialize_system_data(args, systems: List[GameSystem], world: World):
for system in systems:
if system.data:
@@ -232,8 +253,11 @@ def load_or_generate_world(
llm = agent_easy_connect()
world_builder = Agent(
"World Builder",
- f"You are an experienced game master creating a visually detailed world for a new adventure. "
- f"{world_prompt.flavor}. The theme is: {world_prompt.theme}.",
+ format_prompt(
+ "world_generate_dungeon_master",
+ flavor=world_prompt.flavor,
+ theme=world_prompt.theme,
+ ),
{},
llm,
memory_factory=memory_factory,
@@ -291,6 +315,8 @@ def main():
else:
config = DEFAULT_CONFIG
+ load_prompt_library(args)
+
players = []
if args.player:
players.append(args.player)
@@ -378,10 +404,8 @@ def snapshot_system(world: World, turn: int, data: None = None) -> None:
llm = agent_easy_connect()
world_builder = Agent(
"dungeon master",
- (
- f"You are the dungeon master in charge of a {world.theme} world. Be creative and original, and come up with "
- f"interesting events that will keep players interested. {args.flavor}"
- "Do not to repeat yourself unless you are given the same prompt with the same characters and actions."
+ format_prompt(
+ "world_generate_dungeon_master", flavor=args.flavor, theme=world.theme
),
{},
llm,
diff --git a/taleweave/models/files.py b/taleweave/models/files.py
index 5ce6b17..655b13c 100644
--- a/taleweave/models/files.py
+++ b/taleweave/models/files.py
@@ -10,6 +10,7 @@ class WorldPrompt:
flavor: str = ""
+# TODO: rename to WorldTemplates
@dataclass
class PromptFile:
prompts: List[WorldPrompt]
diff --git a/taleweave/models/prompt.py b/taleweave/models/prompt.py
new file mode 100644
index 0000000..971b603
--- /dev/null
+++ b/taleweave/models/prompt.py
@@ -0,0 +1,8 @@
+from typing import Dict
+
+from .base import dataclass
+
+
+@dataclass
+class PromptLibrary:
+ prompts: Dict[str, str]
diff --git a/taleweave/simulate.py b/taleweave/simulate.py
index 5b46160..2306e0e 100644
--- a/taleweave/simulate.py
+++ b/taleweave/simulate.py
@@ -22,10 +22,10 @@
)
from taleweave.actions.planning import (
check_calendar,
+ edit_note,
erase_notes,
get_recent_notes,
read_notes,
- replace_note,
schedule_event,
summarize_notes,
take_note,
@@ -36,6 +36,7 @@
get_character_for_agent,
get_current_turn,
get_current_world,
+ get_prompt,
set_current_character,
set_current_room,
set_current_turn,
@@ -49,6 +50,7 @@
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.prompt import format_prompt
from taleweave.utils.search import find_containing_room
from taleweave.utils.world import describe_entity, format_attributes
@@ -115,10 +117,13 @@ def result_parser(value, **kwargs):
pass
if could_be_json(value):
- # TODO: only emit valid actions that parse and run correctly
+ # TODO: only emit valid actions that parse and run correctly, and try to avoid parsing the JSON twice
event = ActionEvent.from_json(value, room, character)
else:
# TODO: this path should be removed and throw
+ logger.warning(
+ "invalid action, emitting as result event - this is a bug somewhere"
+ )
event = ResultEvent(value, room, character)
broadcast(event)
@@ -129,17 +134,7 @@ def result_parser(value, **kwargs):
logger.info("starting turn for character: %s", character.name)
result = loop_retry(
agent,
- (
- "You are currently in the {room_name} room. {room_description}. {attributes}. "
- "The room contains the following characters: {visible_characters}. "
- "The room contains the following items: {visible_items}. "
- "Your inventory contains the following items: {character_items}."
- "You can take the following actions: {actions}. "
- "You can move in the following directions: {directions}. "
- "{notes_prompt} {events_prompt}"
- "What will you do next? Reply with a JSON function call, calling one of the actions."
- "You can only perform one action per turn. What is your next action?"
- ),
+ get_prompt("world_simulate_character_action"),
context={
"actions": action_names,
"character_items": character_items,
@@ -158,7 +153,6 @@ def result_parser(value, **kwargs):
logger.debug(f"{character.name} action result: {result}")
if agent.memory:
- # TODO: make sure this is not duplicating memories and wasting space
agent.memory.append(result)
return result
@@ -170,25 +164,33 @@ def get_notes_events(character: Character, current_turn: int):
if len(recent_notes) > 0:
notes = "\n".join(recent_notes)
- notes_prompt = f"Your recent notes are: {notes}\n"
+ notes_prompt = format_prompt(
+ "world_simulate_character_planning_notes_some", notes=notes
+ )
else:
- notes_prompt = "You have no recent notes.\n"
+ notes_prompt = format_prompt("world_simulate_character_planning_notes_none")
if len(upcoming_events) > 0:
current_turn = get_current_turn()
events = [
- f"{event.name} in {event.turn - current_turn} turns"
+ 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 = f"Upcoming events are: {events}\n"
+ events_prompt = format_prompt(
+ "world_simulate_character_planning_events_some", events=events
+ )
else:
- events_prompt = "You have no upcoming events.\n"
+ events_prompt = format_prompt("world_simulate_character_planning_events_none")
return notes_prompt, events_prompt
-def prompt_character_think(
+def prompt_character_planning(
room: Room,
character: Character,
agent: Agent,
@@ -204,7 +206,9 @@ def prompt_character_think(
note_count = len(character.planner.notes)
logger.info("starting planning for character: %s", character.name)
- _, condition_end, result_parser = make_keyword_condition("You are done planning.")
+ _, condition_end, result_parser = make_keyword_condition(
+ get_prompt("world_simulate_character_planning_done")
+ )
stop_condition = condition_or(
condition_end, partial(condition_threshold, max=max_steps)
)
@@ -213,15 +217,7 @@ def prompt_character_think(
while not stop_condition(current=i):
result = loop_retry(
agent,
- "You are about to start your turn. Plan your next action carefully. Take notes and schedule events to help keep track of your goals. "
- "You can check your notes for important facts or check your calendar for upcoming events. You have {note_count} notes. "
- "If you have plans with other characters, schedule them on your calendar. You have {event_count} events on your calendar. "
- "{room_summary}"
- "Think about your goals and any quests that you are working on, and plan your next action accordingly. "
- "Try to keep your notes accurate and up-to-date. Replace or erase old notes when they are no longer accurate or useful. "
- "Do not keeps notes about upcoming events, use your calendar for that. "
- "You can perform up to 3 planning actions in a single turn. When you are done planning, reply with 'END'."
- "{notes_prompt} {events_prompt}",
+ get_prompt("world_simulate_character_planning"),
context={
"event_count": event_count,
"events_prompt": events_prompt,
@@ -272,7 +268,7 @@ def simulate_world(
check_calendar,
erase_notes,
read_notes,
- replace_note,
+ edit_note,
schedule_event,
summarize_notes,
take_note,
@@ -306,7 +302,7 @@ def simulate_world(
# give the character a chance to think and check their planner
if agent.memory and len(agent.memory) > 0:
try:
- thoughts = prompt_character_think(
+ thoughts = prompt_character_planning(
room, character, agent, planner_toolbox, current_turn
)
logger.debug(f"{character.name} thinks: {thoughts}")
diff --git a/taleweave/systems/digest.py b/taleweave/systems/digest.py
index 158e347..bd3462d 100644
--- a/taleweave/systems/digest.py
+++ b/taleweave/systems/digest.py
@@ -1,42 +1,31 @@
+from logging import getLogger
from typing import Dict, List
-from taleweave.context import get_current_world, subscribe
+from taleweave.context import get_current_world, get_prompt_library, 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
+logger = getLogger(__name__)
+
def create_turn_digest(
active_room: Room, active_character: Character, turn_events: List[GameEvent]
) -> List[str]:
+ library = get_prompt_library()
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']}."
- )
+ prompt_key = f"digest_{event.action}"
+ if prompt_key in library.prompts:
+ try:
+ template = library.prompts[prompt_key]
+ message = template.format(event=event)
+ messages.append(message)
+ except Exception:
+ logger.exception("error formatting digest event: %s", event)
return messages
@@ -48,8 +37,8 @@ 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
+ # append the event to every character's buffer except the one who triggered it. the
+ # acting character should have their buffer reset, because they can only act on their turn
for name, buffer in character_buffers.items():
if name == character:
diff --git a/taleweave/utils/conversation.py b/taleweave/utils/conversation.py
index 6cd5643..5cea46e 100644
--- a/taleweave/utils/conversation.py
+++ b/taleweave/utils/conversation.py
@@ -12,6 +12,7 @@
from taleweave.models.config import DEFAULT_CONFIG
from taleweave.models.entity import Character, Room
from taleweave.models.event import ReplyEvent
+from taleweave.utils.prompt import format_str
from .string import and_list, normalize_name
@@ -143,7 +144,12 @@ def result_parser(value: str, **kwargs) -> str:
# summarize the room and present the last response
summary = summarize_room(room, character)
response = agent(
- prompt, response=response, summary=summary, last_character=last_character
+ format_str(
+ prompt,
+ response=response,
+ summary=summary,
+ last_character=last_character,
+ )
)
response = result_parser(response)
@@ -155,4 +161,4 @@ def result_parser(value: str, **kwargs) -> str:
i += 1
last_character = character
- return f"{last_character.name} ends the conversation for now"
+ return format_str(end_message, response=response, last_character=last_character)
diff --git a/taleweave/utils/prompt.py b/taleweave/utils/prompt.py
new file mode 100644
index 0000000..c01141a
--- /dev/null
+++ b/taleweave/utils/prompt.py
@@ -0,0 +1,27 @@
+from logging import getLogger
+
+from jinja2 import Environment
+
+from taleweave.context import get_prompt_library
+from taleweave.utils.world import describe_entity, name_entity
+
+logger = getLogger(__name__)
+
+
+def format_prompt(prompt_key: str, **kwargs) -> str:
+ try:
+ library = get_prompt_library()
+ template_str = library.prompts[prompt_key]
+ return format_str(template_str, **kwargs)
+ except Exception as e:
+ logger.exception("error formatting prompt: %s", prompt_key)
+ raise e
+
+
+def format_str(template_str: str, **kwargs) -> str:
+ env = Environment()
+ env.filters["describe"] = describe_entity
+ env.filters["name"] = name_entity
+
+ template = env.from_string(template_str)
+ return template.render(**kwargs)
diff --git a/taleweave/utils/world.py b/taleweave/utils/world.py
index 141e3af..6aa2161 100644
--- a/taleweave/utils/world.py
+++ b/taleweave/utils/world.py
@@ -51,3 +51,12 @@ def format_attributes(
]
return f"{'. '.join(attribute_descriptions)}"
+
+
+def name_entity(
+ entity: str | WorldEntity,
+) -> str:
+ if isinstance(entity, str):
+ return entity
+
+ return entity.name
diff --git a/taleweave/prompts.yml b/worlds.yml
similarity index 100%
rename from taleweave/prompts.yml
rename to worlds.yml