start moving prompts into data files
This commit is contained in:
parent
d37de8a5ab
commit
90d81929e9
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Text World</title>
|
||||
<title>TaleWeave AI</title>
|
||||
<link href="./bundle/main.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
prompts:
|
||||
discord_help: |
|
||||
**Commands:**
|
||||
- `!help` - Show this help message
|
||||
- `!{{ bot_name }}` - Show the active world
|
||||
- `!join <character>` - 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 <character>` 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.
|
|
@ -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
|
|
@ -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}}".
|
|
@ -5,10 +5,12 @@ from taleweave.context import (
|
|||
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 @@ from taleweave.utils.search import (
|
|||
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.")
|
||||
raise ActionError(
|
||||
format_prompt("action_give_error_target", character=character)
|
||||
)
|
||||
|
||||
if destination_character == action_character:
|
||||
raise ActionError(
|
||||
"You cannot give an item to yourself. Try another action."
|
||||
)
|
||||
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)
|
||||
|
|
|
@ -15,11 +15,17 @@ from taleweave.context import (
|
|||
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 @@ if not has_dungeon_master():
|
|||
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)
|
||||
|
|
|
@ -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
|
||||
]
|
||||
)
|
||||
|
|
|
@ -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 @@ from taleweave.systems.quest import (
|
|||
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,
|
||||
)
|
||||
|
|
|
@ -34,6 +34,7 @@ from taleweave.player import (
|
|||
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 @@ class AdventureClient(Client):
|
|||
):
|
||||
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 @@ class AdventureClient(Client):
|
|||
)
|
||||
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)
|
||||
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
|
||||
|
||||
if isinstance(event, ActionEvent):
|
||||
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
|
||||
|
|
|
@ -20,6 +20,7 @@ from pyee.base import EventEmitter
|
|||
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 @@ dungeon_master: Agent | None = None
|
|||
# game context
|
||||
event_emitter = EventEmitter()
|
||||
game_systems: List[GameSystem] = []
|
||||
prompt_library: PromptLibrary = PromptLibrary(prompts={})
|
||||
system_data: Dict[str, Any] = {}
|
||||
|
||||
|
||||
|
@ -44,7 +46,7 @@ STRING_EVENT_TYPE = "message"
|
|||
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from packit.loops import loop_retry
|
|||
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.entity import Character, Item, Portal, Room, World, WorldE
|
|||
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 duplicate_name_parser(existing_names: List[str]):
|
|||
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 generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
|
|||
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)
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ load_dotenv(environ.get("ADVENTURE_ENV", ".env"), override=True)
|
|||
|
||||
if True:
|
||||
from taleweave.context import (
|
||||
get_prompt_library,
|
||||
get_system_data,
|
||||
set_current_turn,
|
||||
set_dungeon_master,
|
||||
|
@ -43,6 +44,7 @@ if True:
|
|||
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 @@ if True:
|
|||
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 main():
|
|||
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,
|
||||
|
|
|
@ -10,6 +10,7 @@ class WorldPrompt:
|
|||
flavor: str = ""
|
||||
|
||||
|
||||
# TODO: rename to WorldTemplates
|
||||
@dataclass
|
||||
class PromptFile:
|
||||
prompts: List[WorldPrompt]
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
from typing import Dict
|
||||
|
||||
from .base import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class PromptLibrary:
|
||||
prompts: Dict[str, str]
|
|
@ -22,10 +22,10 @@ from taleweave.actions.base import (
|
|||
)
|
||||
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 @@ from taleweave.context import (
|
|||
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.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.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 prompt_character_action(
|
|||
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 prompt_character_action(
|
|||
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 prompt_character_action(
|
|||
|
||||
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}")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -12,6 +12,7 @@ from taleweave.context import broadcast
|
|||
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 loop_conversation(
|
|||
# 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 loop_conversation(
|
|||
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)
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue