start moving prompts into data files
This commit is contained in:
parent
d37de8a5ab
commit
90d81929e9
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Text World</title>
|
<title>TaleWeave AI</title>
|
||||||
<link href="./bundle/main.css" rel="stylesheet">
|
<link href="./bundle/main.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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,
|
broadcast,
|
||||||
get_agent_for_character,
|
get_agent_for_character,
|
||||||
get_character_agent_for_name,
|
get_character_agent_for_name,
|
||||||
|
get_prompt,
|
||||||
world_context,
|
world_context,
|
||||||
)
|
)
|
||||||
from taleweave.errors import ActionError
|
from taleweave.errors import ActionError
|
||||||
from taleweave.utils.conversation import loop_conversation
|
from taleweave.utils.conversation import loop_conversation
|
||||||
|
from taleweave.utils.prompt import format_prompt
|
||||||
from taleweave.utils.search import (
|
from taleweave.utils.search import (
|
||||||
find_character_in_room,
|
find_character_in_room,
|
||||||
find_item_in_character,
|
find_item_in_character,
|
||||||
|
@ -17,7 +19,6 @@ from taleweave.utils.search import (
|
||||||
find_room,
|
find_room,
|
||||||
)
|
)
|
||||||
from taleweave.utils.string import normalize_name
|
from taleweave.utils.string import normalize_name
|
||||||
from taleweave.utils.world import describe_entity
|
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
@ -33,34 +34,65 @@ def action_examine(target: str) -> str:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with action_context() as (action_room, action_character):
|
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):
|
if normalize_name(target) == normalize_name(action_room.name):
|
||||||
broadcast(f"{action_character.name} saw the {action_room.name} room")
|
broadcast(
|
||||||
return describe_entity(action_room)
|
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)
|
target_character = find_character_in_room(action_room, target)
|
||||||
if target_character:
|
if target_character:
|
||||||
broadcast(
|
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)
|
target_item = find_item_in_room(action_room, target)
|
||||||
if target_item:
|
if target_item:
|
||||||
broadcast(
|
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)
|
target_item = find_item_in_character(action_character, target)
|
||||||
if target_item:
|
if target_item:
|
||||||
broadcast(
|
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:
|
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):
|
with world_context() as (action_world, action_room, action_character):
|
||||||
portal = find_portal_in_room(action_room, direction)
|
portal = find_portal_in_room(action_room, direction)
|
||||||
if not portal:
|
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)
|
dest_room = find_room(action_world, portal.destination)
|
||||||
if not destination_room:
|
if not dest_room:
|
||||||
raise ActionError(f"The {portal.destination} room does not exist.")
|
raise ActionError(
|
||||||
|
format_prompt(
|
||||||
|
"action_move_error_room",
|
||||||
|
direction=direction,
|
||||||
|
destination=portal.destination,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
broadcast(
|
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)
|
action_room.characters.remove(action_character)
|
||||||
destination_room.characters.append(action_character)
|
dest_room.characters.append(action_character)
|
||||||
|
|
||||||
return (
|
return format_prompt(
|
||||||
f"You move through the {direction} and arrive at {destination_room.name}."
|
"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):
|
with action_context() as (action_room, action_character):
|
||||||
action_item = find_item_in_room(action_room, item)
|
action_item = find_item_in_room(action_room, item)
|
||||||
if not action_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_room.items.remove(action_item)
|
||||||
action_character.items.append(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:
|
def action_ask(character: str, question: str) -> str:
|
||||||
|
@ -122,27 +178,31 @@ def action_ask(character: str, question: str) -> str:
|
||||||
# sanity checks
|
# sanity checks
|
||||||
question_character, question_agent = get_character_agent_for_name(character)
|
question_character, question_agent = get_character_agent_for_name(character)
|
||||||
if question_character == action_character:
|
if question_character == action_character:
|
||||||
raise ActionError(
|
raise ActionError(format_prompt("action_ask_error_self"))
|
||||||
"You cannot ask yourself a question. Stop talking to yourself. Try another action."
|
|
||||||
)
|
|
||||||
|
|
||||||
if not question_character:
|
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:
|
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}")
|
# TODO: make sure they are in the same room
|
||||||
first_prompt = (
|
|
||||||
"{last_character.name} asks you: {response}\n"
|
broadcast(
|
||||||
"Reply with your response to them. Reply with 'END' to end the conversation. "
|
format_prompt(
|
||||||
"Do not include the question or any JSON. Only include your answer for {last_character.name}."
|
"action_ask_broadcast",
|
||||||
)
|
action_character=action_character,
|
||||||
reply_prompt = (
|
character=character,
|
||||||
"{last_character.name} continues the conversation with you. They reply: {response}\n"
|
question=question,
|
||||||
"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}."
|
|
||||||
)
|
)
|
||||||
|
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)
|
action_agent = get_agent_for_character(action_character)
|
||||||
result = loop_conversation(
|
result = loop_conversation(
|
||||||
|
@ -153,7 +213,7 @@ def action_ask(character: str, question: str) -> str:
|
||||||
first_prompt,
|
first_prompt,
|
||||||
reply_prompt,
|
reply_prompt,
|
||||||
question,
|
question,
|
||||||
"Goodbye",
|
end_prompt,
|
||||||
echo_function=action_tell.__name__,
|
echo_function=action_tell.__name__,
|
||||||
echo_parameter="message",
|
echo_parameter="message",
|
||||||
max_length=MAX_CONVERSATION_STEPS,
|
max_length=MAX_CONVERSATION_STEPS,
|
||||||
|
@ -162,7 +222,7 @@ def action_ask(character: str, question: str) -> str:
|
||||||
if result:
|
if result:
|
||||||
return 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:
|
def action_tell(character: str, message: str) -> str:
|
||||||
|
@ -179,27 +239,22 @@ def action_tell(character: str, message: str) -> str:
|
||||||
# sanity checks
|
# sanity checks
|
||||||
question_character, question_agent = get_character_agent_for_name(character)
|
question_character, question_agent = get_character_agent_for_name(character)
|
||||||
if question_character == action_character:
|
if question_character == action_character:
|
||||||
raise ActionError(
|
raise ActionError(format_prompt("action_tell_error_self"))
|
||||||
"You cannot tell yourself a message. Stop talking to yourself. Try another action."
|
|
||||||
)
|
|
||||||
|
|
||||||
if not question_character:
|
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:
|
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}")
|
broadcast(f"{action_character.name} tells {character}: {message}")
|
||||||
first_prompt = (
|
first_prompt = get_prompt("action_tell_conversation_first")
|
||||||
"{last_character.name} starts a conversation with you. They say: {response}\n"
|
reply_prompt = get_prompt("action_tell_conversation_reply")
|
||||||
"Reply with your response to them. "
|
end_prompt = get_prompt("action_tell_conversation_end")
|
||||||
"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}."
|
|
||||||
)
|
|
||||||
|
|
||||||
action_agent = get_agent_for_character(action_character)
|
action_agent = get_agent_for_character(action_character)
|
||||||
result = loop_conversation(
|
result = loop_conversation(
|
||||||
|
@ -210,7 +265,7 @@ def action_tell(character: str, message: str) -> str:
|
||||||
first_prompt,
|
first_prompt,
|
||||||
reply_prompt,
|
reply_prompt,
|
||||||
message,
|
message,
|
||||||
"Goodbye",
|
end_prompt,
|
||||||
echo_function=action_tell.__name__,
|
echo_function=action_tell.__name__,
|
||||||
echo_parameter="message",
|
echo_parameter="message",
|
||||||
max_length=MAX_CONVERSATION_STEPS,
|
max_length=MAX_CONVERSATION_STEPS,
|
||||||
|
@ -219,7 +274,7 @@ def action_tell(character: str, message: str) -> str:
|
||||||
if result:
|
if result:
|
||||||
return 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:
|
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):
|
with action_context() as (action_room, action_character):
|
||||||
destination_character = find_character_in_room(action_room, character)
|
destination_character = find_character_in_room(action_room, character)
|
||||||
if not destination_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:
|
if destination_character == action_character:
|
||||||
raise ActionError(
|
raise ActionError(format_prompt("action_give_error_self"))
|
||||||
"You cannot give an item to yourself. Try another action."
|
|
||||||
)
|
|
||||||
|
|
||||||
action_item = find_item_in_character(action_character, item)
|
action_item = find_item_in_character(action_character, item)
|
||||||
if not action_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)
|
action_character.items.remove(action_item)
|
||||||
destination_character.items.append(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:
|
def action_drop(item: str) -> str:
|
||||||
|
@ -262,10 +324,14 @@ def action_drop(item: str) -> str:
|
||||||
with action_context() as (action_room, action_character):
|
with action_context() as (action_room, action_character):
|
||||||
action_item = find_item_in_character(action_character, item)
|
action_item = find_item_in_character(action_character, item)
|
||||||
if not action_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_character.items.remove(action_item)
|
||||||
action_room.items.append(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,
|
world_context,
|
||||||
)
|
)
|
||||||
from taleweave.errors import ActionError
|
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.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.search import find_character_in_room
|
||||||
from taleweave.utils.string import normalize_name
|
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__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
@ -29,7 +35,7 @@ if not has_dungeon_master():
|
||||||
set_dungeon_master(
|
set_dungeon_master(
|
||||||
Agent(
|
Agent(
|
||||||
"dungeon master",
|
"dungeon master",
|
||||||
"You are the dungeon master in charge of a fantasy world.",
|
format_prompt("world_default_dungeon_master"),
|
||||||
{},
|
{},
|
||||||
llm,
|
llm,
|
||||||
)
|
)
|
||||||
|
@ -50,8 +56,11 @@ def action_explore(direction: str) -> str:
|
||||||
if direction in action_room.portals:
|
if direction in action_room.portals:
|
||||||
dest_room = action_room.portals[direction]
|
dest_room = action_room.portals[direction]
|
||||||
raise ActionError(
|
raise ActionError(
|
||||||
f"You cannot explore {direction} from here, that direction already leads to {dest_room}. "
|
format_prompt(
|
||||||
"Please use the move action to go there."
|
"action_explore_error_direction",
|
||||||
|
direction=direction,
|
||||||
|
dest_room=dest_room,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -59,16 +68,34 @@ def action_explore(direction: str) -> str:
|
||||||
new_room = generate_room(dungeon_master, action_world, systems)
|
new_room = generate_room(dungeon_master, action_world, systems)
|
||||||
action_world.rooms.append(new_room)
|
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])
|
link_rooms(dungeon_master, action_world, systems, [new_room])
|
||||||
|
|
||||||
broadcast(
|
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:
|
except Exception:
|
||||||
logger.exception("error generating room")
|
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:
|
def action_search(unused: bool) -> str:
|
||||||
|
@ -80,9 +107,7 @@ def action_search(unused: bool) -> str:
|
||||||
dungeon_master = get_dungeon_master()
|
dungeon_master = get_dungeon_master()
|
||||||
|
|
||||||
if len(action_room.items) > 2:
|
if len(action_room.items) > 2:
|
||||||
return (
|
return format_prompt("action_search_error_full")
|
||||||
"You find nothing hidden in the room. There is no room for more items."
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
systems = get_game_systems()
|
systems = get_game_systems()
|
||||||
|
@ -95,12 +120,17 @@ def action_search(unused: bool) -> str:
|
||||||
action_room.items.append(new_item)
|
action_room.items.append(new_item)
|
||||||
|
|
||||||
broadcast(
|
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:
|
except Exception:
|
||||||
logger.exception("error generating item")
|
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:
|
def action_use(item: str, target: str) -> str:
|
||||||
|
@ -118,12 +148,12 @@ def action_use(item: str, target: str) -> str:
|
||||||
(
|
(
|
||||||
search_item
|
search_item
|
||||||
for search_item in (action_character.items + action_room.items)
|
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,
|
None,
|
||||||
)
|
)
|
||||||
if not action_item:
|
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":
|
if target == "self":
|
||||||
target_character = action_character
|
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
|
# TODO: allow targeting the room itself and items in the room
|
||||||
target_character = find_character_in_room(action_room, target)
|
target_character = find_character_in_room(action_room, target)
|
||||||
if not target_character:
|
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]
|
effect_names = [effect.name for effect in action_item.effects]
|
||||||
# TODO: should use a retry loop and enum result parser
|
# TODO: should use a retry loop and enum result parser
|
||||||
chosen_name = dungeon_master(
|
chosen_name = dungeon_master(
|
||||||
f"{action_character.name} uses {item} on {target}. "
|
format_prompt(
|
||||||
f"{item} has the following effects: {effect_names}. "
|
"action_use_dm_effect",
|
||||||
"Which effect should be applied? Specify the name of the effect to apply."
|
action_character=action_character,
|
||||||
"Do not include the question or any JSON. Only include the name of the effect to apply."
|
item=item,
|
||||||
|
target=target,
|
||||||
|
effect_names=effect_names,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
chosen_name = normalize_name(chosen_name)
|
chosen_name = normalize_name(chosen_name)
|
||||||
|
|
||||||
chosen_effect = next(
|
effect = next(
|
||||||
(
|
(
|
||||||
search_effect
|
search_effect
|
||||||
for search_effect in action_item.effects
|
for search_effect in action_item.effects
|
||||||
|
@ -152,46 +185,56 @@ def action_use(item: str, target: str) -> str:
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if not chosen_effect:
|
if not effect:
|
||||||
raise ValueError(f"The {chosen_name} effect is not available to apply.")
|
raise ValueError(f"The {chosen_name} effect is not available to apply.")
|
||||||
|
|
||||||
current_turn = get_current_turn()
|
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":
|
if effect_ready == "cooldown":
|
||||||
raise ActionError(
|
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":
|
elif effect_ready == "exhausted":
|
||||||
raise ActionError(
|
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:
|
elif effect.uses is not None:
|
||||||
chosen_effect.uses -= 1
|
effect.uses -= 1
|
||||||
|
|
||||||
chosen_effect.last_used = current_turn
|
effect.last_used = current_turn
|
||||||
|
|
||||||
try:
|
try:
|
||||||
apply_effects(target_character, [chosen_effect])
|
apply_effects(target_character, [effect])
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("error applying effect: %s", chosen_effect)
|
logger.exception("error applying effect: %s", effect)
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"There was a problem applying the {chosen_name} effect while using the {item} item."
|
f"There was a problem applying the {chosen_name} effect while using the {item} item."
|
||||||
)
|
)
|
||||||
|
|
||||||
broadcast(
|
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(
|
outcome = dungeon_master(
|
||||||
f"{action_character.name} uses the {chosen_name} effect of {item} on {target}. "
|
format_prompt(
|
||||||
f"{describe_character(action_character)}. "
|
"action_use_dm_outcome",
|
||||||
f"{describe_character(target_character)}. "
|
action_character=action_character,
|
||||||
f"{describe_entity(action_item)}. "
|
action_item=action_item,
|
||||||
f"What happens? How does {target} react? Be creative with the results. The outcome can be good, bad, or neutral."
|
describe_entity=describe_entity,
|
||||||
"Decide based on the characters involved and the item being used."
|
effect=effect,
|
||||||
"Specify the outcome of the action. Do not include the question or any JSON. Only include the outcome of the action."
|
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
|
# make sure both agents remember the outcome
|
||||||
target_agent = get_agent_for_character(target_character)
|
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.errors import ActionError
|
||||||
from taleweave.models.config import DEFAULT_CONFIG
|
from taleweave.models.config import DEFAULT_CONFIG
|
||||||
from taleweave.models.planning import CalendarEvent
|
from taleweave.models.planning import CalendarEvent
|
||||||
from taleweave.utils.planning import get_recent_notes
|
from taleweave.utils.planning import get_recent_notes
|
||||||
|
from taleweave.utils.prompt import format_prompt
|
||||||
|
|
||||||
character_config = DEFAULT_CONFIG.world.character
|
character_config = DEFAULT_CONFIG.world.character
|
||||||
|
|
||||||
|
@ -18,19 +24,14 @@ def take_note(fact: str):
|
||||||
|
|
||||||
with action_context() as (_, action_character):
|
with action_context() as (_, action_character):
|
||||||
if fact in action_character.planner.notes:
|
if fact in action_character.planner.notes:
|
||||||
raise ActionError(
|
raise ActionError(get_prompt("action_take_note_error_duplicate"))
|
||||||
"You already have a note about that fact. You do not need to take duplicate notes. "
|
|
||||||
"If you have too many notes, consider erasing, replacing, or summarizing them."
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(action_character.planner.notes) >= character_config.note_limit:
|
if len(action_character.planner.notes) >= character_config.note_limit:
|
||||||
raise ActionError(
|
raise ActionError(get_prompt("action_take_note_error_limit"))
|
||||||
"You have reached the limit of notes you can take. Please erase, replace, or summarize some notes."
|
|
||||||
)
|
|
||||||
|
|
||||||
action_character.planner.notes.append(fact)
|
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):
|
def read_notes(unused: bool, count: int = 10):
|
||||||
|
@ -55,21 +56,25 @@ def erase_notes(prefix: str) -> str:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with action_context() as (_, action_character):
|
with action_context() as (_, action_character):
|
||||||
|
if len(action_character.planner.notes) == 0:
|
||||||
|
raise ActionError(get_prompt("action_erase_notes_error_empty"))
|
||||||
|
|
||||||
matches = [
|
matches = [
|
||||||
note for note in action_character.planner.notes if note.startswith(prefix)
|
note for note in action_character.planner.notes if note.startswith(prefix)
|
||||||
]
|
]
|
||||||
if not matches:
|
if not matches:
|
||||||
return "No notes found with that prefix."
|
raise ActionError(get_prompt("action_erase_notes_error_match"))
|
||||||
|
|
||||||
action_character.planner.notes[:] = [
|
action_character.planner.notes[:] = [
|
||||||
note for note in action_character.planner.notes if note not in matches
|
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:
|
Args:
|
||||||
old: The old note to replace.
|
old: The old note to replace.
|
||||||
|
@ -77,13 +82,17 @@ def replace_note(old: str, new: str) -> str:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with action_context() as (_, action_character):
|
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:
|
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[:] = [
|
action_character.planner.notes[:] = [
|
||||||
new if note == old else note for note in 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:
|
def summarize_notes(limit: int) -> str:
|
||||||
|
@ -96,19 +105,16 @@ def summarize_notes(limit: int) -> str:
|
||||||
|
|
||||||
with action_context() as (_, action_character):
|
with action_context() as (_, action_character):
|
||||||
notes = action_character.planner.notes
|
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)
|
action_agent = get_agent_for_character(action_character)
|
||||||
|
|
||||||
if not action_agent:
|
if not action_agent:
|
||||||
raise ActionError("Agent missing for character {action_character.name}")
|
raise ActionError("Agent missing for character {action_character.name}")
|
||||||
|
|
||||||
summary = action_agent(
|
summary = action_agent(
|
||||||
"Please summarize your notes. Remove any duplicates and combine similar notes. "
|
get_prompt("action_summarize_notes_prompt"),
|
||||||
"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}",
|
|
||||||
limit=limit,
|
limit=limit,
|
||||||
notes=notes,
|
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()]
|
new_notes = [note.strip() for note in summary.split("\n") if note.strip()]
|
||||||
if len(new_notes) > character_config.note_limit:
|
if len(new_notes) > character_config.note_limit:
|
||||||
raise ActionError(
|
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
|
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):
|
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
|
# TODO: limit the number of events that can be scheduled
|
||||||
|
|
||||||
with action_context() as (_, action_character):
|
with action_context() as (_, action_character):
|
||||||
|
if not name:
|
||||||
|
raise ActionError(get_prompt("action_schedule_event_error_name"))
|
||||||
|
|
||||||
event = CalendarEvent(name, turns)
|
event = CalendarEvent(name, turns)
|
||||||
action_character.planner.calendar.events.append(event)
|
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):
|
def check_calendar(count: int):
|
||||||
|
@ -156,15 +168,16 @@ def check_calendar(count: int):
|
||||||
|
|
||||||
with action_context() as (_, action_character):
|
with action_context() as (_, action_character):
|
||||||
if len(action_character.planner.calendar.events) == 0:
|
if len(action_character.planner.calendar.events) == 0:
|
||||||
return (
|
return get_prompt("action_check_calendar_empty")
|
||||||
"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."
|
|
||||||
)
|
|
||||||
|
|
||||||
events = action_character.planner.calendar.events[:count]
|
events = action_character.planner.calendar.events[:count]
|
||||||
return "\n".join(
|
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
|
for event in events
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from taleweave.context import action_context, get_system_data
|
from taleweave.context import action_context, get_system_data
|
||||||
|
from taleweave.errors import ActionError
|
||||||
from taleweave.systems.quest import (
|
from taleweave.systems.quest import (
|
||||||
QUEST_SYSTEM,
|
QUEST_SYSTEM,
|
||||||
complete_quest,
|
complete_quest,
|
||||||
|
@ -6,6 +7,7 @@ from taleweave.systems.quest import (
|
||||||
get_quests_for_character,
|
get_quests_for_character,
|
||||||
set_active_quest,
|
set_active_quest,
|
||||||
)
|
)
|
||||||
|
from taleweave.utils.prompt import format_prompt
|
||||||
from taleweave.utils.search import find_character_in_room
|
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):
|
with action_context() as (action_room, action_character):
|
||||||
quests = get_system_data(QUEST_SYSTEM)
|
quests = get_system_data(QUEST_SYSTEM)
|
||||||
if not quests:
|
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)
|
target_character = find_character_in_room(action_room, character)
|
||||||
if not target_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)
|
available_quests = get_quests_for_character(quests, target_character)
|
||||||
|
|
||||||
for available_quest in available_quests:
|
for available_quest in available_quests:
|
||||||
if available_quest.name == quest:
|
if available_quest.name == quest:
|
||||||
set_active_quest(quests, action_character, available_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:
|
def submit_quest(character: str) -> str:
|
||||||
|
@ -41,18 +53,32 @@ def submit_quest(character: str) -> str:
|
||||||
with action_context() as (action_room, action_character):
|
with action_context() as (action_room, action_character):
|
||||||
quests = get_system_data(QUEST_SYSTEM)
|
quests = get_system_data(QUEST_SYSTEM)
|
||||||
if not quests:
|
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)
|
active_quest = get_active_quest(quests, action_character)
|
||||||
if not active_quest:
|
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)
|
target_character = find_character_in_room(action_room, character)
|
||||||
if not target_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:
|
if active_quest.giver.character == target_character.name:
|
||||||
complete_quest(quests, action_character, active_quest)
|
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,
|
set_player,
|
||||||
)
|
)
|
||||||
from taleweave.render.comfy import render_event
|
from taleweave.render.comfy import render_event
|
||||||
|
from taleweave.utils.prompt import format_prompt
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
client = None
|
client = None
|
||||||
|
@ -86,28 +87,38 @@ class AdventureClient(Client):
|
||||||
):
|
):
|
||||||
world = get_current_world()
|
world = get_current_world()
|
||||||
if 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:
|
else:
|
||||||
active_world = "No active world"
|
world_message = format_prompt(
|
||||||
|
"discord_world_none", bot_name=bot_config.name_title
|
||||||
|
)
|
||||||
|
|
||||||
await message.channel.send(
|
await message.channel.send(world_message)
|
||||||
f"Hello! Welcome to {bot_config.name_title}! {active_world}"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if message.content.startswith("!help"):
|
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
|
return
|
||||||
|
|
||||||
if message.content.startswith("!join"):
|
if message.content.startswith("!join"):
|
||||||
character_name = remove_tags(message.content).replace("!join", "").strip()
|
character_name = remove_tags(message.content).replace("!join", "").strip()
|
||||||
if has_player(character_name):
|
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
|
return
|
||||||
|
|
||||||
character, agent = get_character_agent_for_name(character_name)
|
character, agent = get_character_agent_for_name(character_name)
|
||||||
if not character:
|
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
|
return
|
||||||
|
|
||||||
def prompt_player(event: PromptEvent):
|
def prompt_player(event: PromptEvent):
|
||||||
|
@ -156,9 +167,7 @@ class AdventureClient(Client):
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
await message.channel.send(
|
await message.channel.send(format_prompt("discord_user_new"))
|
||||||
"You are not currently playing Adventure! Type `!join` to start playing!"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@ -317,8 +326,10 @@ def embed_from_event(event: GameEvent) -> Embed | None:
|
||||||
return embed_from_generate(event)
|
return embed_from_generate(event)
|
||||||
elif isinstance(event, ResultEvent):
|
elif isinstance(event, ResultEvent):
|
||||||
return embed_from_result(event)
|
return embed_from_result(event)
|
||||||
elif isinstance(event, (ActionEvent, ReplyEvent)):
|
elif isinstance(event, ActionEvent):
|
||||||
return embed_from_action(event)
|
return embed_from_action(event)
|
||||||
|
elif isinstance(event, ReplyEvent):
|
||||||
|
return embed_from_reply(event)
|
||||||
elif isinstance(event, StatusEvent):
|
elif isinstance(event, StatusEvent):
|
||||||
return embed_from_status(event)
|
return embed_from_status(event)
|
||||||
elif isinstance(event, PlayerEvent):
|
elif isinstance(event, PlayerEvent):
|
||||||
|
@ -329,23 +340,25 @@ def embed_from_event(event: GameEvent) -> Embed | None:
|
||||||
logger.warning("unknown event type: %s", event)
|
logger.warning("unknown event type: %s", event)
|
||||||
|
|
||||||
|
|
||||||
def embed_from_action(event: ActionEvent | ReplyEvent):
|
def embed_from_action(event: ActionEvent):
|
||||||
action_embed = Embed(title=event.room.name, description=event.speaker.name)
|
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_embed.add_field(name="Action", value=action_name)
|
||||||
action_name = event.action.replace("action_", "").title()
|
|
||||||
action_parameters = event.parameters
|
|
||||||
|
|
||||||
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)
|
||||||
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)
|
|
||||||
|
|
||||||
return action_embed
|
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:
|
def embed_from_generate(event: GenerateEvent) -> Embed:
|
||||||
generate_embed = Embed(title="Generating", description=event.name)
|
generate_embed = Embed(title="Generating", description=event.name)
|
||||||
return generate_embed
|
return generate_embed
|
||||||
|
@ -363,11 +376,11 @@ def embed_from_result(event: ResultEvent):
|
||||||
|
|
||||||
def embed_from_player(event: PlayerEvent):
|
def embed_from_player(event: PlayerEvent):
|
||||||
if event.status == "join":
|
if event.status == "join":
|
||||||
title = "Player Joined"
|
title = format_prompt("discord_join_title", event=event)
|
||||||
description = f"{event.client} is now playing as {event.character}"
|
description = format_prompt("discord_join_result", event=event)
|
||||||
else:
|
else:
|
||||||
title = "Player Left"
|
title = format_prompt("discord_leave_title", event=event)
|
||||||
description = f"{event.client} has left the game. {event.character} is now controlled by an LLM"
|
description = format_prompt("discord_leave_result", event=event)
|
||||||
|
|
||||||
player_embed = Embed(title=title, description=description)
|
player_embed = Embed(title=title, description=description)
|
||||||
return player_embed
|
return player_embed
|
||||||
|
|
|
@ -20,6 +20,7 @@ from pyee.base import EventEmitter
|
||||||
from taleweave.game_system import GameSystem
|
from taleweave.game_system import GameSystem
|
||||||
from taleweave.models.entity import Character, Room, World
|
from taleweave.models.entity import Character, Room, World
|
||||||
from taleweave.models.event import GameEvent, StatusEvent
|
from taleweave.models.event import GameEvent, StatusEvent
|
||||||
|
from taleweave.models.prompt import PromptLibrary
|
||||||
from taleweave.utils.string import normalize_name
|
from taleweave.utils.string import normalize_name
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
@ -34,6 +35,7 @@ dungeon_master: Agent | None = None
|
||||||
# game context
|
# game context
|
||||||
event_emitter = EventEmitter()
|
event_emitter = EventEmitter()
|
||||||
game_systems: List[GameSystem] = []
|
game_systems: List[GameSystem] = []
|
||||||
|
prompt_library: PromptLibrary = PromptLibrary(prompts={})
|
||||||
system_data: Dict[str, Any] = {}
|
system_data: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,7 +46,7 @@ STRING_EVENT_TYPE = "message"
|
||||||
|
|
||||||
|
|
||||||
def get_event_name(event: GameEvent | Type[GameEvent]):
|
def get_event_name(event: GameEvent | Type[GameEvent]):
|
||||||
return f"event:{event.type}"
|
return f"event.{event.type}"
|
||||||
|
|
||||||
|
|
||||||
def broadcast(message: str | GameEvent):
|
def broadcast(message: str | GameEvent):
|
||||||
|
@ -162,6 +164,14 @@ def get_game_systems() -> List[GameSystem]:
|
||||||
return game_systems
|
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:
|
def get_system_data(system: str) -> Any | None:
|
||||||
return system_data.get(system)
|
return system_data.get(system)
|
||||||
|
|
||||||
|
@ -204,6 +214,11 @@ def set_game_systems(systems: Sequence[GameSystem]):
|
||||||
game_systems = list(systems)
|
game_systems = list(systems)
|
||||||
|
|
||||||
|
|
||||||
|
def set_prompt_library(library: PromptLibrary):
|
||||||
|
global prompt_library
|
||||||
|
prompt_library = library
|
||||||
|
|
||||||
|
|
||||||
def set_system_data(system: str, data: Any):
|
def set_system_data(system: str, data: Any):
|
||||||
system_data[system] = data
|
system_data[system] = data
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ from packit.loops import loop_retry
|
||||||
from packit.results import enum_result, int_result
|
from packit.results import enum_result, int_result
|
||||||
from packit.utils import could_be_json
|
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.game_system import GameSystem
|
||||||
from taleweave.models.config import DEFAULT_CONFIG, WorldConfig
|
from taleweave.models.config import DEFAULT_CONFIG, WorldConfig
|
||||||
from taleweave.models.effect import (
|
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.models.event import GenerateEvent
|
||||||
from taleweave.utils import try_parse_float, try_parse_int
|
from taleweave.utils import try_parse_float, try_parse_int
|
||||||
from taleweave.utils.effect import resolve_int_range
|
from taleweave.utils.effect import resolve_int_range
|
||||||
|
from taleweave.utils.prompt import format_prompt
|
||||||
from taleweave.utils.search import (
|
from taleweave.utils.search import (
|
||||||
list_characters,
|
list_characters,
|
||||||
list_characters_in_room,
|
list_characters_in_room,
|
||||||
|
@ -40,16 +41,24 @@ def duplicate_name_parser(existing_names: List[str]):
|
||||||
logger.debug(f"validating generated name: {value}")
|
logger.debug(f"validating generated name: {value}")
|
||||||
|
|
||||||
if value in existing_names:
|
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):
|
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:
|
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:
|
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
|
return value
|
||||||
|
|
||||||
|
@ -88,9 +97,7 @@ def generate_room(
|
||||||
|
|
||||||
name = loop_retry(
|
name = loop_retry(
|
||||||
agent,
|
agent,
|
||||||
"Generate one room, area, or location that would make sense in the world of {world_theme}. "
|
get_prompt("world_generate_room_name"),
|
||||||
"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}',
|
|
||||||
context={
|
context={
|
||||||
"world_theme": world.theme,
|
"world_theme": world.theme,
|
||||||
"existing_rooms": existing_rooms,
|
"existing_rooms": existing_rooms,
|
||||||
|
@ -99,18 +106,18 @@ def generate_room(
|
||||||
toolbox=None,
|
toolbox=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
broadcast_generated(message=f"Generating room: {name}")
|
broadcast_generated(format_prompt("world_generate_room_broadcast_room", name=name))
|
||||||
desc = agent(
|
desc = agent(get_prompt("world_generate_room_description"), name=name)
|
||||||
"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,
|
|
||||||
)
|
|
||||||
|
|
||||||
actions = {}
|
actions = {}
|
||||||
room = Room(name=name, description=desc, items=[], characters=[], actions=actions)
|
room = Room(name=name, description=desc, items=[], characters=[], actions=actions)
|
||||||
|
|
||||||
item_count = resolve_int_range(world_config.size.room_items) or 0
|
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):
|
for _ in range(item_count):
|
||||||
try:
|
try:
|
||||||
|
@ -128,7 +135,11 @@ def generate_room(
|
||||||
|
|
||||||
character_count = resolve_int_range(world_config.size.room_characters) or 0
|
character_count = resolve_int_range(world_config.size.room_characters) or 0
|
||||||
broadcast_generated(
|
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):
|
for _ in range(character_count):
|
||||||
|
@ -155,17 +166,14 @@ def generate_portals(
|
||||||
source_room: Room,
|
source_room: Room,
|
||||||
dest_room: Room,
|
dest_room: Room,
|
||||||
systems: List[GameSystem],
|
systems: List[GameSystem],
|
||||||
|
outgoing_name: str | None = None,
|
||||||
) -> Tuple[Portal, Portal]:
|
) -> Tuple[Portal, Portal]:
|
||||||
existing_source_portals = [portal.name for portal in source_room.portals]
|
existing_source_portals = [portal.name for portal in source_room.portals]
|
||||||
existing_dest_portals = [portal.name for portal in dest_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,
|
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}. "
|
get_prompt("world_generate_portal_name_outgoing"),
|
||||||
"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}",
|
|
||||||
context={
|
context={
|
||||||
"source_room": source_room.name,
|
"source_room": source_room.name,
|
||||||
"dest_room": dest_room.name,
|
"dest_room": dest_room.name,
|
||||||
|
@ -175,16 +183,15 @@ def generate_portals(
|
||||||
result_parser=duplicate_name_parser(existing_source_portals),
|
result_parser=duplicate_name_parser(existing_source_portals),
|
||||||
toolbox=None,
|
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(
|
incoming_name = loop_retry(
|
||||||
agent,
|
agent,
|
||||||
"Generate the opposite name of the portal that leads from the {dest_room} room to the {source_room} room. "
|
get_prompt("world_generate_portal_name_incoming"),
|
||||||
"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}",
|
|
||||||
context={
|
context={
|
||||||
"source_room": source_room.name,
|
"source_room": source_room.name,
|
||||||
"dest_room": dest_room.name,
|
"dest_room": dest_room.name,
|
||||||
|
@ -196,7 +203,15 @@ def generate_portals(
|
||||||
toolbox=None,
|
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(
|
outgoing_portal = Portal(
|
||||||
name=outgoing_name,
|
name=outgoing_name,
|
||||||
|
@ -242,11 +257,7 @@ def generate_item(
|
||||||
|
|
||||||
name = loop_retry(
|
name = loop_retry(
|
||||||
agent,
|
agent,
|
||||||
"Generate one item or object that would make sense in the world of {world_theme}. {dest_note}. "
|
get_prompt("world_generate_item_name"),
|
||||||
"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}",
|
|
||||||
context={
|
context={
|
||||||
"dest_note": dest_note,
|
"dest_note": dest_note,
|
||||||
"existing_items": existing_items,
|
"existing_items": existing_items,
|
||||||
|
@ -256,18 +267,23 @@ def generate_item(
|
||||||
toolbox=None,
|
toolbox=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
broadcast_generated(message=f"Generating item: {name}")
|
broadcast_generated(
|
||||||
desc = agent(
|
message=format_prompt("world_generate_item_broadcast_item", name=name)
|
||||||
"Generate a detailed description of the {name} item. What does it look like? What is it made of? What does it do?",
|
|
||||||
name=name,
|
|
||||||
)
|
)
|
||||||
|
desc = agent(get_prompt("world_generate_item_description"), name=name)
|
||||||
|
|
||||||
actions = {}
|
actions = {}
|
||||||
item = Item(name=name, description=desc, actions=actions)
|
item = Item(name=name, description=desc, actions=actions)
|
||||||
generate_system_attributes(agent, world, item, systems)
|
generate_system_attributes(agent, world, item, systems)
|
||||||
|
|
||||||
effect_count = resolve_int_range(world_config.size.item_effects) or 0
|
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):
|
for _ in range(effect_count):
|
||||||
try:
|
try:
|
||||||
|
@ -294,12 +310,7 @@ def generate_character(
|
||||||
|
|
||||||
name = loop_retry(
|
name = loop_retry(
|
||||||
agent,
|
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."
|
get_prompt("world_generate_character_name"),
|
||||||
"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}",
|
|
||||||
context={
|
context={
|
||||||
"additional_prompt": additional_prompt,
|
"additional_prompt": additional_prompt,
|
||||||
"dest_room": dest_room.name,
|
"dest_room": dest_room.name,
|
||||||
|
@ -310,18 +321,17 @@ def generate_character(
|
||||||
toolbox=None,
|
toolbox=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
broadcast_generated(message=f"Generating character: {name}")
|
broadcast_generated(
|
||||||
|
message=format_prompt("world_generate_character_broadcast_name", name=name)
|
||||||
|
)
|
||||||
description = agent(
|
description = agent(
|
||||||
"Generate a detailed description of the {name} character. {additional_prompt}. {detail_prompt}. What do they look like? What are they wearing? "
|
get_prompt("world_generate_character_description"),
|
||||||
"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.",
|
|
||||||
additional_prompt=additional_prompt,
|
additional_prompt=additional_prompt,
|
||||||
detail_prompt=detail_prompt,
|
detail_prompt=detail_prompt,
|
||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
backstory = agent(
|
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 "
|
get_prompt("world_generate_character_backstory"),
|
||||||
'goals? Make sure to phrase the backstory in the second person, starting with "you are" and speaking directly to {name}.',
|
|
||||||
additional_prompt=additional_prompt,
|
additional_prompt=additional_prompt,
|
||||||
detail_prompt=detail_prompt,
|
detail_prompt=detail_prompt,
|
||||||
name=name,
|
name=name,
|
||||||
|
@ -334,7 +344,11 @@ def generate_character(
|
||||||
|
|
||||||
# generate the character's inventory
|
# generate the character's inventory
|
||||||
item_count = resolve_int_range(world_config.size.character_items) or 0
|
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):
|
for k in range(item_count):
|
||||||
try:
|
try:
|
||||||
|
@ -352,6 +366,7 @@ def generate_character(
|
||||||
logger.exception("error generating item")
|
logger.exception("error generating item")
|
||||||
|
|
||||||
if add_to_world_order:
|
if add_to_world_order:
|
||||||
|
# TODO: make sure characters have an agent
|
||||||
logger.info(f"adding character {name} to end of world turn order")
|
logger.info(f"adding character {name} to end of world turn order")
|
||||||
world.order.append(name)
|
world.order.append(name)
|
||||||
|
|
||||||
|
@ -364,11 +379,7 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
|
||||||
|
|
||||||
name = loop_retry(
|
name = loop_retry(
|
||||||
agent,
|
agent,
|
||||||
"Generate one effect for an {entity_type} named {entity_name} that would make sense in the world of {theme}. "
|
get_prompt("world_generate_effect_name"),
|
||||||
"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'.",
|
|
||||||
context={
|
context={
|
||||||
"entity_name": entity.name,
|
"entity_name": entity.name,
|
||||||
"entity_type": entity_type,
|
"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),
|
result_parser=duplicate_name_parser(existing_effects),
|
||||||
toolbox=None,
|
toolbox=None,
|
||||||
)
|
)
|
||||||
broadcast_generated(message=f"Generating effect: {name}")
|
broadcast_generated(
|
||||||
|
message=format_prompt("world_generate_effect_broadcast_effect", name=name)
|
||||||
|
)
|
||||||
|
|
||||||
description = agent(
|
description = agent(
|
||||||
"Generate a detailed description of the {name} effect. What does it look like? What does it do? "
|
get_prompt("world_generate_effect_description"),
|
||||||
"How does it affect the target? Describe the effect from the perspective of an outside observer.",
|
|
||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
|
|
||||||
cooldown = loop_retry(
|
cooldown = loop_retry(
|
||||||
agent,
|
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. "
|
get_prompt("world_generate_effect_cooldown"),
|
||||||
"Do not include any other text. Do not use JSON.",
|
|
||||||
context={
|
context={
|
||||||
"name": name,
|
"name": name,
|
||||||
},
|
},
|
||||||
|
@ -399,8 +410,7 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
|
||||||
|
|
||||||
uses = loop_retry(
|
uses = loop_retry(
|
||||||
agent,
|
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. "
|
get_prompt("world_generate_effect_uses"),
|
||||||
"Do not include any other text. Do not use JSON.",
|
|
||||||
context={
|
context={
|
||||||
"name": name,
|
"name": name,
|
||||||
},
|
},
|
||||||
|
@ -412,10 +422,7 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
|
||||||
uses = None
|
uses = None
|
||||||
|
|
||||||
attribute_names = agent(
|
attribute_names = agent(
|
||||||
"Generate a short list of attributes that the {name} effect modifies. Include 1 to 3 attributes. "
|
get_prompt("world_generate_effect_attribute_names"),
|
||||||
"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.",
|
|
||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -424,10 +431,7 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
|
||||||
attribute_name = normalize_name(attribute_name)
|
attribute_name = normalize_name(attribute_name)
|
||||||
if attribute_name:
|
if attribute_name:
|
||||||
value = agent(
|
value = agent(
|
||||||
f"How much does the {name} effect modify the {attribute_name} attribute? "
|
get_prompt("world_generate_effect_attribute_value"),
|
||||||
"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.",
|
|
||||||
name=name,
|
name=name,
|
||||||
attribute_name=attribute_name,
|
attribute_name=attribute_name,
|
||||||
)
|
)
|
||||||
|
@ -452,8 +456,7 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
|
||||||
|
|
||||||
duration = loop_retry(
|
duration = loop_retry(
|
||||||
agent,
|
agent,
|
||||||
f"How many turns does the {name} effect last? Enter a positive number to set a duration, or 0 for an instant effect. "
|
get_prompt("world_generate_effect_duration"),
|
||||||
"Do not include any other text. Do not use JSON.",
|
|
||||||
context={
|
context={
|
||||||
"name": name,
|
"name": name,
|
||||||
},
|
},
|
||||||
|
@ -466,19 +469,11 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
|
||||||
if value:
|
if value:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
raise ValueError("The application must be 'temporary' or 'permanent'.")
|
raise ValueError(get_prompt("world_generate_effect_error_application"))
|
||||||
|
|
||||||
application = loop_retry(
|
application = loop_retry(
|
||||||
agent,
|
agent,
|
||||||
(
|
get_prompt("world_generate_effect_application"),
|
||||||
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."
|
|
||||||
),
|
|
||||||
context={
|
context={
|
||||||
"name": name,
|
"name": name,
|
||||||
},
|
},
|
||||||
|
@ -513,7 +508,11 @@ def link_rooms(
|
||||||
continue
|
continue
|
||||||
|
|
||||||
broadcast_generated(
|
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):
|
for _ in range(num_portals):
|
||||||
|
@ -553,7 +552,7 @@ def generate_world(
|
||||||
) -> World:
|
) -> World:
|
||||||
room_count = room_count or resolve_int_range(world_config.size.rooms) or 0
|
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=[])
|
world = World(name=name, rooms=[], theme=theme, order=[])
|
||||||
set_current_world(world)
|
set_current_world(world)
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ load_dotenv(environ.get("ADVENTURE_ENV", ".env"), override=True)
|
||||||
|
|
||||||
if True:
|
if True:
|
||||||
from taleweave.context import (
|
from taleweave.context import (
|
||||||
|
get_prompt_library,
|
||||||
get_system_data,
|
get_system_data,
|
||||||
set_current_turn,
|
set_current_turn,
|
||||||
set_dungeon_master,
|
set_dungeon_master,
|
||||||
|
@ -43,6 +44,7 @@ if True:
|
||||||
from taleweave.models.entity import World, WorldState
|
from taleweave.models.entity import World, WorldState
|
||||||
from taleweave.models.event import GenerateEvent
|
from taleweave.models.event import GenerateEvent
|
||||||
from taleweave.models.files import PromptFile, WorldPrompt
|
from taleweave.models.files import PromptFile, WorldPrompt
|
||||||
|
from taleweave.models.prompt import PromptLibrary
|
||||||
from taleweave.plugins import load_plugin
|
from taleweave.plugins import load_plugin
|
||||||
from taleweave.simulate import simulate_world
|
from taleweave.simulate import simulate_world
|
||||||
from taleweave.state import (
|
from taleweave.state import (
|
||||||
|
@ -51,6 +53,7 @@ if True:
|
||||||
save_world,
|
save_world,
|
||||||
save_world_state,
|
save_world_state,
|
||||||
)
|
)
|
||||||
|
from taleweave.utils.prompt import format_prompt
|
||||||
|
|
||||||
# start the debugger, if needed
|
# start the debugger, if needed
|
||||||
if environ.get("DEBUG", "false").lower() == "true":
|
if environ.get("DEBUG", "false").lower() == "true":
|
||||||
|
@ -116,6 +119,12 @@ def parse_args():
|
||||||
type=str,
|
type=str,
|
||||||
help="The name of the character to play as",
|
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(
|
parser.add_argument(
|
||||||
"--render",
|
"--render",
|
||||||
action="store_true",
|
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):
|
def load_or_initialize_system_data(args, systems: List[GameSystem], world: World):
|
||||||
for system in systems:
|
for system in systems:
|
||||||
if system.data:
|
if system.data:
|
||||||
|
@ -232,8 +253,11 @@ def load_or_generate_world(
|
||||||
llm = agent_easy_connect()
|
llm = agent_easy_connect()
|
||||||
world_builder = Agent(
|
world_builder = Agent(
|
||||||
"World Builder",
|
"World Builder",
|
||||||
f"You are an experienced game master creating a visually detailed world for a new adventure. "
|
format_prompt(
|
||||||
f"{world_prompt.flavor}. The theme is: {world_prompt.theme}.",
|
"world_generate_dungeon_master",
|
||||||
|
flavor=world_prompt.flavor,
|
||||||
|
theme=world_prompt.theme,
|
||||||
|
),
|
||||||
{},
|
{},
|
||||||
llm,
|
llm,
|
||||||
memory_factory=memory_factory,
|
memory_factory=memory_factory,
|
||||||
|
@ -291,6 +315,8 @@ def main():
|
||||||
else:
|
else:
|
||||||
config = DEFAULT_CONFIG
|
config = DEFAULT_CONFIG
|
||||||
|
|
||||||
|
load_prompt_library(args)
|
||||||
|
|
||||||
players = []
|
players = []
|
||||||
if args.player:
|
if args.player:
|
||||||
players.append(args.player)
|
players.append(args.player)
|
||||||
|
@ -378,10 +404,8 @@ def main():
|
||||||
llm = agent_easy_connect()
|
llm = agent_easy_connect()
|
||||||
world_builder = Agent(
|
world_builder = Agent(
|
||||||
"dungeon master",
|
"dungeon master",
|
||||||
(
|
format_prompt(
|
||||||
f"You are the dungeon master in charge of a {world.theme} world. Be creative and original, and come up with "
|
"world_generate_dungeon_master", flavor=args.flavor, theme=world.theme
|
||||||
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."
|
|
||||||
),
|
),
|
||||||
{},
|
{},
|
||||||
llm,
|
llm,
|
||||||
|
|
|
@ -10,6 +10,7 @@ class WorldPrompt:
|
||||||
flavor: str = ""
|
flavor: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: rename to WorldTemplates
|
||||||
@dataclass
|
@dataclass
|
||||||
class PromptFile:
|
class PromptFile:
|
||||||
prompts: List[WorldPrompt]
|
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 (
|
from taleweave.actions.planning import (
|
||||||
check_calendar,
|
check_calendar,
|
||||||
|
edit_note,
|
||||||
erase_notes,
|
erase_notes,
|
||||||
get_recent_notes,
|
get_recent_notes,
|
||||||
read_notes,
|
read_notes,
|
||||||
replace_note,
|
|
||||||
schedule_event,
|
schedule_event,
|
||||||
summarize_notes,
|
summarize_notes,
|
||||||
take_note,
|
take_note,
|
||||||
|
@ -36,6 +36,7 @@ from taleweave.context import (
|
||||||
get_character_for_agent,
|
get_character_for_agent,
|
||||||
get_current_turn,
|
get_current_turn,
|
||||||
get_current_world,
|
get_current_world,
|
||||||
|
get_prompt,
|
||||||
set_current_character,
|
set_current_character,
|
||||||
set_current_room,
|
set_current_room,
|
||||||
set_current_turn,
|
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.conversation import make_keyword_condition, summarize_room
|
||||||
from taleweave.utils.effect import expire_effects
|
from taleweave.utils.effect import expire_effects
|
||||||
from taleweave.utils.planning import expire_events, get_upcoming_events
|
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.search import find_containing_room
|
||||||
from taleweave.utils.world import describe_entity, format_attributes
|
from taleweave.utils.world import describe_entity, format_attributes
|
||||||
|
|
||||||
|
@ -115,10 +117,13 @@ def prompt_character_action(
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if could_be_json(value):
|
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)
|
event = ActionEvent.from_json(value, room, character)
|
||||||
else:
|
else:
|
||||||
# TODO: this path should be removed and throw
|
# 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)
|
event = ResultEvent(value, room, character)
|
||||||
|
|
||||||
broadcast(event)
|
broadcast(event)
|
||||||
|
@ -129,17 +134,7 @@ def prompt_character_action(
|
||||||
logger.info("starting turn for character: %s", character.name)
|
logger.info("starting turn for character: %s", character.name)
|
||||||
result = loop_retry(
|
result = loop_retry(
|
||||||
agent,
|
agent,
|
||||||
(
|
get_prompt("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?"
|
|
||||||
),
|
|
||||||
context={
|
context={
|
||||||
"actions": action_names,
|
"actions": action_names,
|
||||||
"character_items": character_items,
|
"character_items": character_items,
|
||||||
|
@ -158,7 +153,6 @@ def prompt_character_action(
|
||||||
|
|
||||||
logger.debug(f"{character.name} action result: {result}")
|
logger.debug(f"{character.name} action result: {result}")
|
||||||
if agent.memory:
|
if agent.memory:
|
||||||
# TODO: make sure this is not duplicating memories and wasting space
|
|
||||||
agent.memory.append(result)
|
agent.memory.append(result)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -170,25 +164,33 @@ def get_notes_events(character: Character, current_turn: int):
|
||||||
|
|
||||||
if len(recent_notes) > 0:
|
if len(recent_notes) > 0:
|
||||||
notes = "\n".join(recent_notes)
|
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:
|
else:
|
||||||
notes_prompt = "You have no recent notes.\n"
|
notes_prompt = format_prompt("world_simulate_character_planning_notes_none")
|
||||||
|
|
||||||
if len(upcoming_events) > 0:
|
if len(upcoming_events) > 0:
|
||||||
current_turn = get_current_turn()
|
current_turn = get_current_turn()
|
||||||
events = [
|
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
|
for event in upcoming_events
|
||||||
]
|
]
|
||||||
events = "\n".join(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:
|
else:
|
||||||
events_prompt = "You have no upcoming events.\n"
|
events_prompt = format_prompt("world_simulate_character_planning_events_none")
|
||||||
|
|
||||||
return notes_prompt, events_prompt
|
return notes_prompt, events_prompt
|
||||||
|
|
||||||
|
|
||||||
def prompt_character_think(
|
def prompt_character_planning(
|
||||||
room: Room,
|
room: Room,
|
||||||
character: Character,
|
character: Character,
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
|
@ -204,7 +206,9 @@ def prompt_character_think(
|
||||||
note_count = len(character.planner.notes)
|
note_count = len(character.planner.notes)
|
||||||
|
|
||||||
logger.info("starting planning for character: %s", character.name)
|
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(
|
stop_condition = condition_or(
|
||||||
condition_end, partial(condition_threshold, max=max_steps)
|
condition_end, partial(condition_threshold, max=max_steps)
|
||||||
)
|
)
|
||||||
|
@ -213,15 +217,7 @@ def prompt_character_think(
|
||||||
while not stop_condition(current=i):
|
while not stop_condition(current=i):
|
||||||
result = loop_retry(
|
result = loop_retry(
|
||||||
agent,
|
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. "
|
get_prompt("world_simulate_character_planning"),
|
||||||
"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}",
|
|
||||||
context={
|
context={
|
||||||
"event_count": event_count,
|
"event_count": event_count,
|
||||||
"events_prompt": events_prompt,
|
"events_prompt": events_prompt,
|
||||||
|
@ -272,7 +268,7 @@ def simulate_world(
|
||||||
check_calendar,
|
check_calendar,
|
||||||
erase_notes,
|
erase_notes,
|
||||||
read_notes,
|
read_notes,
|
||||||
replace_note,
|
edit_note,
|
||||||
schedule_event,
|
schedule_event,
|
||||||
summarize_notes,
|
summarize_notes,
|
||||||
take_note,
|
take_note,
|
||||||
|
@ -306,7 +302,7 @@ def simulate_world(
|
||||||
# give the character a chance to think and check their planner
|
# give the character a chance to think and check their planner
|
||||||
if agent.memory and len(agent.memory) > 0:
|
if agent.memory and len(agent.memory) > 0:
|
||||||
try:
|
try:
|
||||||
thoughts = prompt_character_think(
|
thoughts = prompt_character_planning(
|
||||||
room, character, agent, planner_toolbox, current_turn
|
room, character, agent, planner_toolbox, current_turn
|
||||||
)
|
)
|
||||||
logger.debug(f"{character.name} thinks: {thoughts}")
|
logger.debug(f"{character.name} thinks: {thoughts}")
|
||||||
|
|
|
@ -1,42 +1,31 @@
|
||||||
|
from logging import getLogger
|
||||||
from typing import Dict, List
|
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.game_system import FormatPerspective, GameSystem
|
||||||
from taleweave.models.entity import Character, Room, World, WorldEntity
|
from taleweave.models.entity import Character, Room, World, WorldEntity
|
||||||
from taleweave.models.event import ActionEvent, GameEvent
|
from taleweave.models.event import ActionEvent, GameEvent
|
||||||
from taleweave.utils.search import find_containing_room
|
from taleweave.utils.search import find_containing_room
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def create_turn_digest(
|
def create_turn_digest(
|
||||||
active_room: Room, active_character: Character, turn_events: List[GameEvent]
|
active_room: Room, active_character: Character, turn_events: List[GameEvent]
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
|
library = get_prompt_library()
|
||||||
messages = []
|
messages = []
|
||||||
for event in turn_events:
|
for event in turn_events:
|
||||||
if isinstance(event, ActionEvent):
|
if isinstance(event, ActionEvent):
|
||||||
if event.character == active_character or event.room == active_room:
|
if event.character == active_character or event.room == active_room:
|
||||||
if event.action == "move":
|
prompt_key = f"digest_{event.action}"
|
||||||
# TODO: differentiate between entering and leaving
|
if prompt_key in library.prompts:
|
||||||
messages.append(f"{event.character.name} entered the room.")
|
try:
|
||||||
elif event.action == "take":
|
template = library.prompts[prompt_key]
|
||||||
messages.append(
|
message = template.format(event=event)
|
||||||
f"{event.character.name} picked up the {event.parameters['item']}."
|
messages.append(message)
|
||||||
)
|
except Exception:
|
||||||
elif event.action == "give":
|
logger.exception("error formatting digest event: %s", event)
|
||||||
messages.append(
|
|
||||||
f"{event.character.name} gave {event.parameters['item']} to {event.parameters['character']}."
|
|
||||||
)
|
|
||||||
elif event.action == "ask":
|
|
||||||
messages.append(
|
|
||||||
f"{event.character.name} asked {event.parameters['character']} about something."
|
|
||||||
)
|
|
||||||
elif event.action == "tell":
|
|
||||||
messages.append(
|
|
||||||
f"{event.character.name} told {event.parameters['character']} something."
|
|
||||||
)
|
|
||||||
elif event.action == "examine":
|
|
||||||
messages.append(
|
|
||||||
f"{event.character.name} examined the {event.parameters['target']}."
|
|
||||||
)
|
|
||||||
|
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
@ -48,8 +37,8 @@ def digest_listener(event: GameEvent):
|
||||||
if isinstance(event, ActionEvent):
|
if isinstance(event, ActionEvent):
|
||||||
character = event.character.name
|
character = event.character.name
|
||||||
|
|
||||||
# append the event to every character's buffer except the one who triggered it
|
# append the event to every character's buffer except the one who triggered it. the
|
||||||
# the actor should have their buffer reset, because they can only act on their turn
|
# acting character should have their buffer reset, because they can only act on their turn
|
||||||
|
|
||||||
for name, buffer in character_buffers.items():
|
for name, buffer in character_buffers.items():
|
||||||
if name == character:
|
if name == character:
|
||||||
|
|
|
@ -12,6 +12,7 @@ from taleweave.context import broadcast
|
||||||
from taleweave.models.config import DEFAULT_CONFIG
|
from taleweave.models.config import DEFAULT_CONFIG
|
||||||
from taleweave.models.entity import Character, Room
|
from taleweave.models.entity import Character, Room
|
||||||
from taleweave.models.event import ReplyEvent
|
from taleweave.models.event import ReplyEvent
|
||||||
|
from taleweave.utils.prompt import format_str
|
||||||
|
|
||||||
from .string import and_list, normalize_name
|
from .string import and_list, normalize_name
|
||||||
|
|
||||||
|
@ -143,7 +144,12 @@ def loop_conversation(
|
||||||
# summarize the room and present the last response
|
# summarize the room and present the last response
|
||||||
summary = summarize_room(room, character)
|
summary = summarize_room(room, character)
|
||||||
response = agent(
|
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)
|
response = result_parser(response)
|
||||||
|
|
||||||
|
@ -155,4 +161,4 @@ def loop_conversation(
|
||||||
i += 1
|
i += 1
|
||||||
last_character = character
|
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)}"
|
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