1
0
Fork 0

start moving prompts into data files
Run Python Build / build (push) Failing after 22s Details
Run Docker Build / build (push) Successful in 25s Details

This commit is contained in:
Sean Sube 2024-05-31 18:58:01 -05:00
parent d37de8a5ab
commit 90d81929e9
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
20 changed files with 983 additions and 328 deletions

View File

@ -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>

24
prompts/discord-en-us.yml Normal file
View File

@ -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.

378
prompts/llama-base.yml Normal file
View File

@ -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

18
prompts/llama-quest.yml Normal file
View File

@ -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}}".

View File

@ -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)

View File

@ -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)

View File

@ -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
] ]
) )

View File

@ -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,
)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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]

View File

@ -0,0 +1,8 @@
from typing import Dict
from .base import dataclass
@dataclass
class PromptLibrary:
prompts: Dict[str, str]

View File

@ -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}")

View File

@ -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:

View File

@ -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)

27
taleweave/utils/prompt.py Normal file
View File

@ -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)

View File

@ -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