1
0
Fork 0

support multi-step conversations, improve prompts, summarize room more often

This commit is contained in:
Sean Sube 2024-05-26 15:59:12 -05:00
parent 560291d609
commit 6a44fd9174
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
11 changed files with 331 additions and 96 deletions

View File

@ -1,14 +1,13 @@
from json import loads
from logging import getLogger from logging import getLogger
from packit.utils import could_be_json
from adventure.context import ( from adventure.context import (
action_context, action_context,
broadcast, broadcast,
get_actor_agent_for_name, get_actor_agent_for_name,
get_agent_for_actor,
world_context, world_context,
) )
from adventure.utils.conversation import loop_conversation
from adventure.utils.search import ( from adventure.utils.search import (
find_actor_in_room, find_actor_in_room,
find_item_in_actor, find_item_in_actor,
@ -20,6 +19,8 @@ from adventure.utils.world import describe_entity
logger = getLogger(__name__) logger = getLogger(__name__)
MAX_CONVERSATION_STEPS = 3
def action_look(target: str) -> str: def action_look(target: str) -> str:
""" """
@ -90,7 +91,9 @@ def action_move(direction: str) -> str:
action_room.actors.remove(action_actor) action_room.actors.remove(action_actor)
destination_room.actors.append(action_actor) destination_room.actors.append(action_actor)
return f"You move {direction} and arrive at {destination_room.name}." return (
f"You move through the {direction} and arrive at {destination_room.name}."
)
def action_take(item: str) -> str: def action_take(item: str) -> str:
@ -116,11 +119,11 @@ def action_ask(character: str, question: str) -> str:
Ask another character a question. Ask another character a question.
Args: Args:
character: The name of the character to ask. character: The name of the character to ask. You cannot ask yourself questions.
question: The question to ask them. question: The question to ask them.
""" """
# capture references to the current actor and room, because they will be overwritten # capture references to the current actor and room, because they will be overwritten
with action_context() as (_, action_actor): with action_context() as (action_room, action_actor):
# sanity checks # sanity checks
question_actor, question_agent = get_actor_agent_for_name(character) question_actor, question_agent = get_actor_agent_for_name(character)
if question_actor == action_actor: if question_actor == action_actor:
@ -133,15 +136,33 @@ def action_ask(character: str, question: str) -> str:
return f"The {character} character does not exist." return f"The {character} character does not exist."
broadcast(f"{action_actor.name} asks {character}: {question}") broadcast(f"{action_actor.name} asks {character}: {question}")
answer = question_agent( first_prompt = (
f"{action_actor.name} asks you: {question}. Reply with your response to them. " "{last_actor.name} asks you: {response}\n"
f"Do not include the question or any JSON. Only include your answer for {action_actor.name}." "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_actor.name}."
)
reply_prompt = (
"{last_actor.name} continues the conversation with you. They reply: {response}\n"
"Reply with your response to them. Reply with 'END' to end the conversation. "
"Do not include the question or any JSON. Only include your answer for {last_actor.name}."
) )
if could_be_json(answer) and action_tell.__name__ in answer: action_agent = get_agent_for_actor(action_actor)
answer = loads(answer).get("parameters", {}).get("message", "") answer = loop_conversation(
action_room,
[question_actor, action_actor],
[question_agent, action_agent],
action_actor,
first_prompt,
reply_prompt,
question,
"Goodbye",
echo_function=action_tell.__name__,
echo_parameter="message",
max_length=MAX_CONVERSATION_STEPS,
)
if len(answer.strip()) > 0: if answer:
broadcast(f"{character} responds to {action_actor.name}: {answer}") broadcast(f"{character} responds to {action_actor.name}: {answer}")
return f"{character} responds: {answer}" return f"{character} responds: {answer}"
@ -153,12 +174,12 @@ def action_tell(character: str, message: str) -> str:
Tell another character a message. Tell another character a message.
Args: Args:
character: The name of the character to tell. character: The name of the character to tell. You cannot talk to yourself.
message: The message to tell them. message: The message to tell them.
""" """
# capture references to the current actor and room, because they will be overwritten # capture references to the current actor and room, because they will be overwritten
with action_context() as (_, action_actor): with action_context() as (action_room, action_actor):
# sanity checks # sanity checks
question_actor, question_agent = get_actor_agent_for_name(character) question_actor, question_agent = get_actor_agent_for_name(character)
if question_actor == action_actor: if question_actor == action_actor:
@ -171,15 +192,33 @@ def action_tell(character: str, message: str) -> str:
return f"The {character} character does not exist." return f"The {character} character does not exist."
broadcast(f"{action_actor.name} tells {character}: {message}") broadcast(f"{action_actor.name} tells {character}: {message}")
answer = question_agent( first_prompt = (
f"{action_actor.name} tells you: {message}. Reply with your response to them. " "{last_actor.name} starts a conversation with you. They say: {response}\n"
f"Do not include the message or any JSON. Only include your reply to {action_actor.name}." "Reply with your response to them. "
"Do not include the message or any JSON. Only include your reply to {last_actor.name}."
)
reply_prompt = (
"{last_actor.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_actor.name}."
) )
if could_be_json(answer) and action_tell.__name__ in answer: action_agent = get_agent_for_actor(action_actor)
answer = loads(answer).get("parameters", {}).get("message", "") answer = loop_conversation(
action_room,
[question_actor, action_actor],
[question_agent, action_agent],
action_actor,
first_prompt,
reply_prompt,
message,
"Goodbye",
echo_function=action_tell.__name__,
echo_parameter="message",
max_length=MAX_CONVERSATION_STEPS,
)
if len(answer.strip()) > 0: if answer:
broadcast(f"{character} responds to {action_actor.name}: {answer}") broadcast(f"{character} responds to {action_actor.name}: {answer}")
return f"{character} responds: {answer}" return f"{character} responds: {answer}"

View File

@ -124,6 +124,7 @@ def action_use(item: str, target: str) -> str:
target_actor = action_actor target_actor = action_actor
target = action_actor.name target = action_actor.name
else: else:
# TODO: allow targeting the room itself and items in the room
target_actor = find_actor_in_room(action_room, target) target_actor = find_actor_in_room(action_room, target)
if not target_actor: if not target_actor:
return f"The {target} character is not in the room." return f"The {target} character is not in the room."

View File

@ -1,5 +1,6 @@
from adventure.context import action_context, get_current_step from adventure.context import action_context, get_current_step
from adventure.models.planning import CalendarEvent from adventure.models.planning import CalendarEvent
from adventure.utils.planning import get_recent_notes
def take_note(fact: str): def take_note(fact: str):
@ -13,10 +14,10 @@ def take_note(fact: str):
with action_context() as (_, action_actor): with action_context() as (_, action_actor):
if fact in action_actor.planner.notes: if fact in action_actor.planner.notes:
return "You already know that." return "You already have a note about that fact."
action_actor.planner.notes.append(fact) action_actor.planner.notes.append(fact)
return "You make a note of that." return "You make a note of that fact."
def read_notes(unused: bool, count: int = 10): def read_notes(unused: bool, count: int = 10):
@ -27,8 +28,9 @@ def read_notes(unused: bool, count: int = 10):
count: The number of recent notes to read. 10 is usually a good number. count: The number of recent notes to read. 10 is usually a good number.
""" """
facts = get_recent_notes(count=count) with action_context() as (_, action_actor):
return "\n".join(facts) facts = get_recent_notes(action_actor, count=count)
return "\n".join(facts)
def erase_notes(prefix: str) -> str: def erase_notes(prefix: str) -> str:
@ -74,7 +76,8 @@ def replace_note(old: str, new: str) -> str:
def schedule_event(name: str, turns: int): def schedule_event(name: str, turns: int):
""" """
Schedule an event to happen at a specific turn. Events are important occurrences that can affect the world in Schedule an event to happen at a specific turn. Events are important occurrences that can affect the world in
significant ways. You will be notified about upcoming events so you can plan accordingly. significant ways. You will be notified about upcoming events so you can plan accordingly. Make sure you inform
other characters about events that involve them, and give them enough time to prepare.
Args: Args:
name: The name of the event. name: The name of the event.
@ -88,7 +91,7 @@ def schedule_event(name: str, turns: int):
return f"{name} is scheduled to happen in {turns} turns." return f"{name} is scheduled to happen in {turns} turns."
def read_calendar(unused: bool, count: int = 10): def check_calendar(unused: bool, count: int = 10):
""" """
Read your calendar to see upcoming events that you have scheduled. Read your calendar to see upcoming events that you have scheduled.
""" """
@ -103,33 +106,3 @@ def read_calendar(unused: bool, count: int = 10):
for event in events for event in events
] ]
) )
def get_upcoming_events(turns: int = 3):
"""
Get a list of upcoming events within a certain number of turns.
Args:
turns: The number of turns to look ahead for events.
"""
current_turn = get_current_step()
with action_context() as (_, action_actor):
calendar = action_actor.planner.calendar
# TODO: sort events by turn
return [
event for event in calendar.events if event.turn - current_turn <= turns
]
def get_recent_notes(count: int = 3):
"""
Get the most recent facts from your notes.
Args:
history: The number of recent facts to retrieve.
"""
with action_context() as (_, action_actor):
return action_actor.planner.notes[-count:]

View File

@ -176,16 +176,15 @@ def load_or_initialize_system_data(args, systems: List[GameSystem], world: World
if system.data: if system.data:
system_data_file = f"{args.world}.{system.name}.json" system_data_file = f"{args.world}.{system.name}.json"
data = None
if path.exists(system_data_file): if path.exists(system_data_file):
logger.info(f"loading system data from {system_data_file}") logger.info(f"loading system data from {system_data_file}")
data = system.data.load(system_data_file) data = system.data.load(system_data_file)
set_system_data(system.name, data)
else: else:
logger.info(f"no system data found at {system_data_file}") logger.info(f"no system data found at {system_data_file}")
if system.initialize: if system.initialize:
data = system.initialize(world) data = system.initialize(world)
set_system_data(system.name, data)
set_system_data(system.name, data)
def save_system_data(args, systems: List[GameSystem]): def save_system_data(args, systems: List[GameSystem]):

View File

@ -40,6 +40,11 @@ class ServerConfig:
websocket: WebsocketServerConfig websocket: WebsocketServerConfig
@dataclass
class WorldActorConfig:
conversation_limit: int
@dataclass @dataclass
class WorldSizeConfig: class WorldSizeConfig:
actor_items: IntRange actor_items: IntRange
@ -52,6 +57,7 @@ class WorldSizeConfig:
@dataclass @dataclass
class WorldConfig: class WorldConfig:
actor: WorldActorConfig
size: WorldSizeConfig size: WorldSizeConfig
@ -80,6 +86,9 @@ DEFAULT_CONFIG = Config(
), ),
server=ServerConfig(websocket=WebsocketServerConfig(host="localhost", port=8001)), server=ServerConfig(websocket=WebsocketServerConfig(host="localhost", port=8001)),
world=WorldConfig( world=WorldConfig(
actor=WorldActorConfig(
conversation_limit=3,
),
size=WorldSizeConfig( size=WorldSizeConfig(
actor_items=IntRange(min=0, max=2), actor_items=IntRange(min=0, max=2),
item_effects=IntRange(min=1, max=2), item_effects=IntRange(min=1, max=2),
@ -87,6 +96,6 @@ DEFAULT_CONFIG = Config(
rooms=IntRange(min=3, max=6), rooms=IntRange(min=3, max=6),
room_actors=IntRange(min=1, max=3), room_actors=IntRange(min=1, max=3),
room_items=IntRange(min=1, max=3), room_items=IntRange(min=1, max=3),
) ),
), ),
) )

View File

@ -5,7 +5,7 @@ from math import inf
from typing import Callable, Sequence from typing import Callable, Sequence
from packit.agent import Agent from packit.agent import Agent
from packit.conditions import condition_or, condition_threshold, make_flag_condition from packit.conditions import condition_or, condition_threshold
from packit.loops import loop_reduce, loop_retry from packit.loops import loop_reduce, loop_retry
from packit.results import multi_function_or_str_result from packit.results import multi_function_or_str_result
from packit.toolbox import Toolbox from packit.toolbox import Toolbox
@ -20,10 +20,9 @@ from adventure.actions.base import (
action_tell, action_tell,
) )
from adventure.actions.planning import ( from adventure.actions.planning import (
check_calendar,
erase_notes, erase_notes,
get_recent_notes, get_recent_notes,
get_upcoming_events,
read_calendar,
read_notes, read_notes,
replace_note, replace_note,
schedule_event, schedule_event,
@ -44,9 +43,10 @@ from adventure.context import (
from adventure.game_system import GameSystem from adventure.game_system import GameSystem
from adventure.models.entity import Actor, Room, World from adventure.models.entity import Actor, Room, World
from adventure.models.event import ActionEvent, ReplyEvent, ResultEvent from adventure.models.event import ActionEvent, ReplyEvent, ResultEvent
from adventure.utils.conversation import make_keyword_condition, summarize_room
from adventure.utils.effect import expire_effects from adventure.utils.effect import expire_effects
from adventure.utils.planning import expire_events, get_upcoming_events
from adventure.utils.search import find_room_with_actor from adventure.utils.search import find_room_with_actor
from adventure.utils.string import normalize_name
from adventure.utils.world import describe_entity, format_attributes from adventure.utils.world import describe_entity, format_attributes
logger = getLogger(__name__) logger = getLogger(__name__)
@ -72,8 +72,12 @@ def world_result_parser(value, agent, **kwargs):
return multi_function_or_str_result(value, agent=agent, **kwargs) return multi_function_or_str_result(value, agent=agent, **kwargs)
def prompt_actor_action(room, actor, agent, action_names, action_toolbox) -> str: def prompt_actor_action(
room, actor, agent, action_names, action_toolbox, current_turn
) -> str:
# collect data for the prompt # collect data for the prompt
notes_prompt, events_prompt = get_notes_events(actor, current_turn)
room_actors = [actor.name for actor in room.actors] room_actors = [actor.name for actor in room.actors]
room_items = [item.name for item in room.items] room_items = [item.name for item in room.items]
room_directions = [portal.name for portal in room.portals] room_directions = [portal.name for portal in room.portals]
@ -101,12 +105,13 @@ def prompt_actor_action(room, actor, agent, action_names, action_toolbox) -> str
result = loop_retry( result = loop_retry(
agent, agent,
( (
"You are currently in {room_name}. {room_description}. {attributes}. " "You are currently in the {room_name} room. {room_description}. {attributes}. "
"The room contains the following characters: {visible_actors}. " "The room contains the following characters: {visible_actors}. "
"The room contains the following items: {visible_items}. " "The room contains the following items: {visible_items}. "
"Your inventory contains the following items: {actor_items}." "Your inventory contains the following items: {actor_items}."
"You can take the following actions: {actions}. " "You can take the following actions: {actions}. "
"You can move in the following directions: {directions}. " "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." "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?" "You can only perform one action per turn. What is your next action?"
), ),
@ -119,6 +124,8 @@ def prompt_actor_action(room, actor, agent, action_names, action_toolbox) -> str
"room_description": describe_entity(room), "room_description": describe_entity(room),
"visible_actors": room_actors, "visible_actors": room_actors,
"visible_items": room_items, "visible_items": room_items,
"notes_prompt": notes_prompt,
"events_prompt": events_prompt,
}, },
result_parser=result_parser, result_parser=result_parser,
toolbox=action_toolbox, toolbox=action_toolbox,
@ -132,11 +139,9 @@ def prompt_actor_action(room, actor, agent, action_names, action_toolbox) -> str
return result return result
def prompt_actor_think( def get_notes_events(actor: Actor, current_turn: int):
room: Room, actor: Actor, agent: Agent, planner_toolbox: Toolbox recent_notes = get_recent_notes(actor)
) -> str: upcoming_events = get_upcoming_events(actor, current_turn)
recent_notes = get_recent_notes()
upcoming_events = get_upcoming_events()
if len(recent_notes) > 0: if len(recent_notes) > 0:
notes = "\n".join(recent_notes) notes = "\n".join(recent_notes)
@ -155,27 +160,30 @@ def prompt_actor_think(
else: else:
events_prompt = "You have no upcoming events.\n" events_prompt = "You have no upcoming events.\n"
return notes_prompt, events_prompt
def prompt_actor_think(
room: Room, actor: Actor, agent: Agent, planner_toolbox: Toolbox, current_turn: int
) -> str:
notes_prompt, events_prompt = get_notes_events(actor, current_turn)
event_count = len(actor.planner.calendar.events) event_count = len(actor.planner.calendar.events)
note_count = len(actor.planner.notes) note_count = len(actor.planner.notes)
logger.info("starting planning for actor: %s", actor.name) logger.info("starting planning for actor: %s", actor.name)
set_end, condition_end = make_flag_condition() _, condition_end, result_parser = make_keyword_condition("You are done planning.")
def result_parser(value, **kwargs):
if normalize_name(value) == "end":
set_end()
return multi_function_or_str_result(value, **kwargs)
stop_condition = condition_or(condition_end, partial(condition_threshold, max=3)) stop_condition = condition_or(condition_end, partial(condition_threshold, max=3))
result = loop_reduce( result = loop_reduce(
agent, agent,
"You are about to take your turn. Plan your next action carefully. " "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. " "You can check your notes for important facts or check your calendar for upcoming events. You have {note_count} notes. "
"Try to keep your notes accurate. Replace or erase old notes if they are no longer accurate or useful. " "If you have plans with other characters, schedule them on your calendar. You have {event_count} events on your calendar. "
"If you have upcoming events with other characters, schedule them on your calendar. You have {event_count} calendar events. " "{room_summary}"
"Think about your goals and any quests that you are working on, and plan your next action accordingly. " "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'." "You can perform up to 3 planning actions in a single turn. When you are done planning, reply with 'END'."
"{notes_prompt} {events_prompt}", "{notes_prompt} {events_prompt}",
context={ context={
@ -183,6 +191,7 @@ def prompt_actor_think(
"events_prompt": events_prompt, "events_prompt": events_prompt,
"note_count": note_count, "note_count": note_count,
"notes_prompt": notes_prompt, "notes_prompt": notes_prompt,
"room_summary": summarize_room(room, actor),
}, },
result_parser=result_parser, result_parser=result_parser,
stop_condition=stop_condition, stop_condition=stop_condition,
@ -227,7 +236,7 @@ def simulate_world(
replace_note, replace_note,
erase_notes, erase_notes,
schedule_event, schedule_event,
read_calendar, check_calendar,
] ]
) )
@ -253,17 +262,21 @@ def simulate_world(
# decrement effects on the actor and remove any that have expired # decrement effects on the actor and remove any that have expired
expire_effects(actor) expire_effects(actor)
# TODO: expire calendar events expire_events(actor, current_step)
# give the actor a chance to think and check their planner # give the actor 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_actor_think(room, actor, agent, planner_toolbox) thoughts = prompt_actor_think(
room, actor, agent, planner_toolbox, current_step
)
logger.debug(f"{actor.name} thinks: {thoughts}") logger.debug(f"{actor.name} thinks: {thoughts}")
except Exception: except Exception:
logger.exception(f"error during planning for actor {actor.name}") logger.exception(f"error during planning for actor {actor.name}")
result = prompt_actor_action(room, actor, agent, action_names, action_tools) result = prompt_actor_action(
room, actor, agent, action_names, action_tools, current_step
)
result_event = ResultEvent(result=result, room=room, actor=actor) result_event = ResultEvent(result=result, room=room, actor=actor)
broadcast(result_event) broadcast(result_event)

View File

@ -17,7 +17,7 @@ logger = getLogger(__name__)
@dataclass @dataclass
class LogicLabel: class LogicLabel:
backstory: str backstory: str | None = None
description: str | None = None description: str | None = None
match: Optional[Attributes] = None match: Optional[Attributes] = None
rule: Optional[str] = None rule: Optional[str] = None
@ -134,7 +134,7 @@ def update_logic(
data: Any | None = None, data: Any | None = None,
*, *,
rules: LogicTable, rules: LogicTable,
triggers: TriggerTable triggers: TriggerTable,
) -> None: ) -> None:
for room in world.rooms: for room in world.rooms:
update_attributes(room, rules=rules, triggers=triggers) update_attributes(room, rules=rules, triggers=triggers)
@ -157,7 +157,7 @@ def format_logic(
for label in rules.labels: for label in rules.labels:
if match_logic(entity, label): if match_logic(entity, label):
if perspective == FormatPerspective.SECOND_PERSON: if perspective == FormatPerspective.SECOND_PERSON and label.backstory:
labels.append(label.backstory) labels.append(label.backstory)
elif perspective == FormatPerspective.THIRD_PERSON and label.description: elif perspective == FormatPerspective.THIRD_PERSON and label.description:
labels.append(label.description) labels.append(label.description)
@ -171,7 +171,8 @@ def format_logic(
def load_logic(filename: str): def load_logic(filename: str):
system_name = "logic-" + path.splitext(path.basename(filename))[0] basename = path.splitext(path.basename(filename))[0]
system_name = f"logic_{basename}"
logger.info("loading logic from file %s as system %s", filename, system_name) logger.info("loading logic from file %s as system %s", filename, system_name)
with open(filename) as file: with open(filename) as file:
@ -188,7 +189,7 @@ def load_logic(filename: str):
logger.info("initialized logic system") logger.info("initialized logic system")
system_format = wraps(format_logic)(partial(format_logic, rules=logic_rules)) system_format = wraps(format_logic)(partial(format_logic, rules=logic_rules))
system_initialize = wraps(load_logic)( system_initialize = wraps(update_logic)(
partial(update_logic, step=0, rules=logic_rules, triggers=logic_triggers) partial(update_logic, step=0, rules=logic_rules, triggers=logic_triggers)
) )
system_simulate = wraps(update_logic)( system_simulate = wraps(update_logic)(

View File

@ -194,18 +194,20 @@ def simulate_quests(world: World, step: int, data: QuestData | None = None) -> N
3. Generate any new quests. 3. Generate any new quests.
""" """
if not data: # TODO: switch to using data parameter
quests: QuestData | None = get_system_data(QUEST_SYSTEM)
if not quests:
# TODO: initialize quest data for worlds that don't have it # TODO: initialize quest data for worlds that don't have it
raise ValueError("Quest data is required for simulation") raise ValueError("Quest data is required for simulation")
for room in world.rooms: for room in world.rooms:
for actor in room.actors: for actor in room.actors:
active_quest = get_active_quest(data, actor) active_quest = get_active_quest(quests, actor)
if active_quest: if active_quest:
logger.info(f"simulating quest for {actor.name}: {active_quest.name}") logger.info(f"simulating quest for {actor.name}: {active_quest.name}")
if is_quest_complete(world, active_quest): if is_quest_complete(world, active_quest):
logger.info(f"quest complete for {actor.name}: {active_quest.name}") logger.info(f"quest complete for {actor.name}: {active_quest.name}")
complete_quest(data, actor, active_quest) complete_quest(quests, actor, active_quest)
def load_quest_data(file: str) -> QuestData: def load_quest_data(file: str) -> QuestData:

View File

@ -0,0 +1,159 @@
from functools import partial
from json import loads
from logging import getLogger
from typing import List
from packit.agent import Agent
from packit.conditions import condition_and, condition_threshold, make_flag_condition
from packit.results import multi_function_or_str_result
from packit.utils import could_be_json
from adventure.context import broadcast
from adventure.models.config import DEFAULT_CONFIG
from adventure.models.entity import Actor, Room
from .string import normalize_name
logger = getLogger(__name__)
actor_config = DEFAULT_CONFIG.world.actor
def make_keyword_condition(end_message: str, keywords=["end", "stop"]):
set_end, condition_end = make_flag_condition()
def result_parser(value, **kwargs):
normalized_value = normalize_name(value)
if normalized_value in keywords:
logger.debug(f"found keyword, setting stop condition: {normalized_value}")
set_end()
return end_message
# sometimes the models will make up a tool named after the keyword
keyword_functions = [f'"function": "{kw}"' for kw in keywords]
if could_be_json(normalized_value) and any(
kw in normalized_value for kw in keyword_functions
):
logger.debug(
f"found keyword function, setting stop condition: {normalized_value}"
)
set_end()
return end_message
return multi_function_or_str_result(value, **kwargs)
return set_end, condition_end, result_parser
def and_list(items: List[str]) -> str:
"""
Convert a list of items into a human-readable list.
"""
if not items:
return "nothing"
if len(items) == 1:
return items[0]
return f"{', '.join(items[:-1])}, and {items[-1]}"
def or_list(items: List[str]) -> str:
"""
Convert a list of items into a human-readable list.
"""
if not items:
return "nothing"
if len(items) == 1:
return items[0]
return f"{', '.join(items[:-1])}, or {items[-1]}"
def summarize_room(room: Room, player: Actor) -> str:
"""
Summarize a room for the player.
"""
actor_names = and_list(
[actor.name for actor in room.actors if actor.name != player.name]
)
item_names = and_list([item.name for item in room.items])
inventory_names = and_list([item.name for item in player.items])
return (
f"You are in the {room.name} room with {actor_names}. "
f"You see the {item_names} around the room. "
f"You are carrying the {inventory_names}."
)
def loop_conversation(
room: Room,
actors: List[Actor],
agents: List[Agent],
first_actor: Actor,
first_prompt: str,
reply_prompt: str,
first_message: str,
end_message: str,
echo_function: str | None = None,
echo_parameter: str | None = None,
max_length: int | None = None,
) -> str | None:
"""
Loop through a conversation between a series of agents, using metadata from their actors.
"""
if max_length is None:
max_length = actor_config.conversation_limit
if len(actors) != len(agents):
raise ValueError("The number of actors and agents must match.")
_, condition_end, parse_end = make_keyword_condition(end_message)
stop_length = partial(condition_threshold, max=max_length)
stop_condition = condition_and(condition_end, stop_length)
def result_parser(value: str, **kwargs) -> str:
value = parse_end(value, **kwargs)
if condition_end():
return value
if echo_function and could_be_json(value) and echo_function in value:
value = loads(value).get("parameters", {}).get(echo_parameter, "")
return value.strip()
i = 0
last_actor = first_actor
response = first_message
while not stop_condition(current=i):
if i == 0:
logger.debug(f"starting conversation with {first_actor.name}")
prompt = first_prompt
else:
logger.debug(f"continuing conversation with {last_actor.name} on step {i}")
prompt = reply_prompt
# loop through the actors and agents
actor = actors[i % len(actors)]
agent = agents[i % len(agents)]
# summarize the room and present the last response
summary = summarize_room(room, actor)
response = agent(
prompt, response=response, summary=summary, last_actor=last_actor
)
response = result_parser(response)
broadcast(f"{actor.name} responds: {response}")
# increment the step counter
i += 1
last_actor = actor
return response

View File

@ -0,0 +1,37 @@
from adventure.models.entity import Actor
def expire_events(actor: Actor, current_turn: int):
"""
Expire events that have already happened.
"""
events = actor.planner.calendar.events
expired_events = [event for event in events if event.turn < current_turn]
actor.planner.calendar.events[:] = [
event for event in events if event not in expired_events
]
return expired_events
def get_recent_notes(actor: Actor, count: int = 3):
"""
Get the most recent facts from your notes.
"""
return actor.planner.notes[-count:]
def get_upcoming_events(actor: Actor, current_turn: int, upcoming_turns: int = 3):
"""
Get a list of upcoming events within a certain number of turns.
"""
calendar = actor.planner.calendar
# TODO: sort events by turn
return [
event
for event in calendar.events
if event.turn - current_turn <= upcoming_turns
]

View File

@ -4,8 +4,10 @@ from adventure.utils.file import load_yaml, save_yaml
def load_system_data(cls, file): def load_system_data(cls, file):
with load_yaml(file) as data: with open(file, "r") as f:
return cls(**data) data = load_yaml(f)
return cls(**data)
def save_system_data(cls, file, model): def save_system_data(cls, file, model):