support multi-step conversations, improve prompts, summarize room more often
This commit is contained in:
parent
560291d609
commit
6a44fd9174
|
@ -1,14 +1,13 @@
|
|||
from json import loads
|
||||
from logging import getLogger
|
||||
|
||||
from packit.utils import could_be_json
|
||||
|
||||
from adventure.context import (
|
||||
action_context,
|
||||
broadcast,
|
||||
get_actor_agent_for_name,
|
||||
get_agent_for_actor,
|
||||
world_context,
|
||||
)
|
||||
from adventure.utils.conversation import loop_conversation
|
||||
from adventure.utils.search import (
|
||||
find_actor_in_room,
|
||||
find_item_in_actor,
|
||||
|
@ -20,6 +19,8 @@ from adventure.utils.world import describe_entity
|
|||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
MAX_CONVERSATION_STEPS = 3
|
||||
|
||||
|
||||
def action_look(target: str) -> str:
|
||||
"""
|
||||
|
@ -90,7 +91,9 @@ def action_move(direction: str) -> str:
|
|||
action_room.actors.remove(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:
|
||||
|
@ -116,11 +119,11 @@ def action_ask(character: str, question: str) -> str:
|
|||
Ask another character a question.
|
||||
|
||||
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.
|
||||
"""
|
||||
# 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
|
||||
question_actor, question_agent = get_actor_agent_for_name(character)
|
||||
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."
|
||||
|
||||
broadcast(f"{action_actor.name} asks {character}: {question}")
|
||||
answer = question_agent(
|
||||
f"{action_actor.name} asks you: {question}. Reply with your response to them. "
|
||||
f"Do not include the question or any JSON. Only include your answer for {action_actor.name}."
|
||||
first_prompt = (
|
||||
"{last_actor.name} asks you: {response}\n"
|
||||
"Reply with your response to them. Reply with 'END' to end the conversation. "
|
||||
"Do not include the question or any JSON. Only include your answer for {last_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:
|
||||
answer = loads(answer).get("parameters", {}).get("message", "")
|
||||
action_agent = get_agent_for_actor(action_actor)
|
||||
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}")
|
||||
return f"{character} responds: {answer}"
|
||||
|
||||
|
@ -153,12 +174,12 @@ def action_tell(character: str, message: str) -> str:
|
|||
Tell another character a message.
|
||||
|
||||
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.
|
||||
"""
|
||||
# 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
|
||||
question_actor, question_agent = get_actor_agent_for_name(character)
|
||||
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."
|
||||
|
||||
broadcast(f"{action_actor.name} tells {character}: {message}")
|
||||
answer = question_agent(
|
||||
f"{action_actor.name} tells you: {message}. Reply with your response to them. "
|
||||
f"Do not include the message or any JSON. Only include your reply to {action_actor.name}."
|
||||
first_prompt = (
|
||||
"{last_actor.name} starts a conversation with you. They say: {response}\n"
|
||||
"Reply with your response to them. "
|
||||
"Do not include the message or any JSON. Only include your reply to {last_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:
|
||||
answer = loads(answer).get("parameters", {}).get("message", "")
|
||||
action_agent = get_agent_for_actor(action_actor)
|
||||
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}")
|
||||
return f"{character} responds: {answer}"
|
||||
|
||||
|
|
|
@ -124,6 +124,7 @@ def action_use(item: str, target: str) -> str:
|
|||
target_actor = action_actor
|
||||
target = action_actor.name
|
||||
else:
|
||||
# TODO: allow targeting the room itself and items in the room
|
||||
target_actor = find_actor_in_room(action_room, target)
|
||||
if not target_actor:
|
||||
return f"The {target} character is not in the room."
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from adventure.context import action_context, get_current_step
|
||||
from adventure.models.planning import CalendarEvent
|
||||
from adventure.utils.planning import get_recent_notes
|
||||
|
||||
|
||||
def take_note(fact: str):
|
||||
|
@ -13,10 +14,10 @@ def take_note(fact: str):
|
|||
|
||||
with action_context() as (_, action_actor):
|
||||
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)
|
||||
return "You make a note of that."
|
||||
return "You make a note of that fact."
|
||||
|
||||
|
||||
def read_notes(unused: bool, count: int = 10):
|
||||
|
@ -27,7 +28,8 @@ def read_notes(unused: bool, count: int = 10):
|
|||
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):
|
||||
facts = get_recent_notes(action_actor, count=count)
|
||||
return "\n".join(facts)
|
||||
|
||||
|
||||
|
@ -74,7 +76,8 @@ def replace_note(old: str, new: str) -> str:
|
|||
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
|
||||
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:
|
||||
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."
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
@ -103,33 +106,3 @@ def read_calendar(unused: bool, count: int = 10):
|
|||
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:]
|
||||
|
|
|
@ -176,15 +176,14 @@ def load_or_initialize_system_data(args, systems: List[GameSystem], world: World
|
|||
if system.data:
|
||||
system_data_file = f"{args.world}.{system.name}.json"
|
||||
|
||||
data = None
|
||||
if path.exists(system_data_file):
|
||||
logger.info(f"loading system data from {system_data_file}")
|
||||
data = system.data.load(system_data_file)
|
||||
set_system_data(system.name, data)
|
||||
else:
|
||||
logger.info(f"no system data found at {system_data_file}")
|
||||
if system.initialize:
|
||||
data = system.initialize(world)
|
||||
|
||||
set_system_data(system.name, data)
|
||||
|
||||
|
||||
|
|
|
@ -40,6 +40,11 @@ class ServerConfig:
|
|||
websocket: WebsocketServerConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorldActorConfig:
|
||||
conversation_limit: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorldSizeConfig:
|
||||
actor_items: IntRange
|
||||
|
@ -52,6 +57,7 @@ class WorldSizeConfig:
|
|||
|
||||
@dataclass
|
||||
class WorldConfig:
|
||||
actor: WorldActorConfig
|
||||
size: WorldSizeConfig
|
||||
|
||||
|
||||
|
@ -80,6 +86,9 @@ DEFAULT_CONFIG = Config(
|
|||
),
|
||||
server=ServerConfig(websocket=WebsocketServerConfig(host="localhost", port=8001)),
|
||||
world=WorldConfig(
|
||||
actor=WorldActorConfig(
|
||||
conversation_limit=3,
|
||||
),
|
||||
size=WorldSizeConfig(
|
||||
actor_items=IntRange(min=0, max=2),
|
||||
item_effects=IntRange(min=1, max=2),
|
||||
|
@ -87,6 +96,6 @@ DEFAULT_CONFIG = Config(
|
|||
rooms=IntRange(min=3, max=6),
|
||||
room_actors=IntRange(min=1, max=3),
|
||||
room_items=IntRange(min=1, max=3),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@ from math import inf
|
|||
from typing import Callable, Sequence
|
||||
|
||||
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.results import multi_function_or_str_result
|
||||
from packit.toolbox import Toolbox
|
||||
|
@ -20,10 +20,9 @@ from adventure.actions.base import (
|
|||
action_tell,
|
||||
)
|
||||
from adventure.actions.planning import (
|
||||
check_calendar,
|
||||
erase_notes,
|
||||
get_recent_notes,
|
||||
get_upcoming_events,
|
||||
read_calendar,
|
||||
read_notes,
|
||||
replace_note,
|
||||
schedule_event,
|
||||
|
@ -44,9 +43,10 @@ from adventure.context import (
|
|||
from adventure.game_system import GameSystem
|
||||
from adventure.models.entity import Actor, Room, World
|
||||
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.planning import expire_events, get_upcoming_events
|
||||
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
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
@ -72,8 +72,12 @@ def world_result_parser(value, 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
|
||||
notes_prompt, events_prompt = get_notes_events(actor, current_turn)
|
||||
|
||||
room_actors = [actor.name for actor in room.actors]
|
||||
room_items = [item.name for item in room.items]
|
||||
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(
|
||||
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 items: {visible_items}. "
|
||||
"Your inventory contains the following items: {actor_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?"
|
||||
),
|
||||
|
@ -119,6 +124,8 @@ def prompt_actor_action(room, actor, agent, action_names, action_toolbox) -> str
|
|||
"room_description": describe_entity(room),
|
||||
"visible_actors": room_actors,
|
||||
"visible_items": room_items,
|
||||
"notes_prompt": notes_prompt,
|
||||
"events_prompt": events_prompt,
|
||||
},
|
||||
result_parser=result_parser,
|
||||
toolbox=action_toolbox,
|
||||
|
@ -132,11 +139,9 @@ def prompt_actor_action(room, actor, agent, action_names, action_toolbox) -> str
|
|||
return result
|
||||
|
||||
|
||||
def prompt_actor_think(
|
||||
room: Room, actor: Actor, agent: Agent, planner_toolbox: Toolbox
|
||||
) -> str:
|
||||
recent_notes = get_recent_notes()
|
||||
upcoming_events = get_upcoming_events()
|
||||
def get_notes_events(actor: Actor, current_turn: int):
|
||||
recent_notes = get_recent_notes(actor)
|
||||
upcoming_events = get_upcoming_events(actor, current_turn)
|
||||
|
||||
if len(recent_notes) > 0:
|
||||
notes = "\n".join(recent_notes)
|
||||
|
@ -155,27 +160,30 @@ def prompt_actor_think(
|
|||
else:
|
||||
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)
|
||||
note_count = len(actor.planner.notes)
|
||||
|
||||
logger.info("starting planning for actor: %s", actor.name)
|
||||
set_end, condition_end = make_flag_condition()
|
||||
|
||||
def result_parser(value, **kwargs):
|
||||
if normalize_name(value) == "end":
|
||||
set_end()
|
||||
|
||||
return multi_function_or_str_result(value, **kwargs)
|
||||
|
||||
_, condition_end, result_parser = make_keyword_condition("You are done planning.")
|
||||
stop_condition = condition_or(condition_end, partial(condition_threshold, max=3))
|
||||
|
||||
result = loop_reduce(
|
||||
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. "
|
||||
"Try to keep your notes accurate. Replace or erase old notes if they are no longer accurate or useful. "
|
||||
"If you have upcoming events with other characters, schedule them on your calendar. You have {event_count} calendar events. "
|
||||
"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={
|
||||
|
@ -183,6 +191,7 @@ def prompt_actor_think(
|
|||
"events_prompt": events_prompt,
|
||||
"note_count": note_count,
|
||||
"notes_prompt": notes_prompt,
|
||||
"room_summary": summarize_room(room, actor),
|
||||
},
|
||||
result_parser=result_parser,
|
||||
stop_condition=stop_condition,
|
||||
|
@ -227,7 +236,7 @@ def simulate_world(
|
|||
replace_note,
|
||||
erase_notes,
|
||||
schedule_event,
|
||||
read_calendar,
|
||||
check_calendar,
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -253,17 +262,21 @@ def simulate_world(
|
|||
|
||||
# decrement effects on the actor and remove any that have expired
|
||||
expire_effects(actor)
|
||||
# TODO: expire calendar events
|
||||
expire_events(actor, current_step)
|
||||
|
||||
# give the actor a chance to think and check their planner
|
||||
if agent.memory and len(agent.memory) > 0:
|
||||
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}")
|
||||
except Exception:
|
||||
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)
|
||||
broadcast(result_event)
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ logger = getLogger(__name__)
|
|||
|
||||
@dataclass
|
||||
class LogicLabel:
|
||||
backstory: str
|
||||
backstory: str | None = None
|
||||
description: str | None = None
|
||||
match: Optional[Attributes] = None
|
||||
rule: Optional[str] = None
|
||||
|
@ -134,7 +134,7 @@ def update_logic(
|
|||
data: Any | None = None,
|
||||
*,
|
||||
rules: LogicTable,
|
||||
triggers: TriggerTable
|
||||
triggers: TriggerTable,
|
||||
) -> None:
|
||||
for room in world.rooms:
|
||||
update_attributes(room, rules=rules, triggers=triggers)
|
||||
|
@ -157,7 +157,7 @@ def format_logic(
|
|||
|
||||
for label in rules.labels:
|
||||
if match_logic(entity, label):
|
||||
if perspective == FormatPerspective.SECOND_PERSON:
|
||||
if perspective == FormatPerspective.SECOND_PERSON and label.backstory:
|
||||
labels.append(label.backstory)
|
||||
elif perspective == FormatPerspective.THIRD_PERSON and label.description:
|
||||
labels.append(label.description)
|
||||
|
@ -171,7 +171,8 @@ def format_logic(
|
|||
|
||||
|
||||
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)
|
||||
|
||||
with open(filename) as file:
|
||||
|
@ -188,7 +189,7 @@ def load_logic(filename: str):
|
|||
|
||||
logger.info("initialized logic system")
|
||||
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)
|
||||
)
|
||||
system_simulate = wraps(update_logic)(
|
||||
|
|
|
@ -194,18 +194,20 @@ def simulate_quests(world: World, step: int, data: QuestData | None = None) -> N
|
|||
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
|
||||
raise ValueError("Quest data is required for simulation")
|
||||
|
||||
for room in world.rooms:
|
||||
for actor in room.actors:
|
||||
active_quest = get_active_quest(data, actor)
|
||||
active_quest = get_active_quest(quests, actor)
|
||||
if active_quest:
|
||||
logger.info(f"simulating quest for {actor.name}: {active_quest.name}")
|
||||
if is_quest_complete(world, active_quest):
|
||||
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:
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
]
|
|
@ -4,7 +4,9 @@ from adventure.utils.file import load_yaml, save_yaml
|
|||
|
||||
|
||||
def load_system_data(cls, file):
|
||||
with load_yaml(file) as data:
|
||||
with open(file, "r") as f:
|
||||
data = load_yaml(f)
|
||||
|
||||
return cls(**data)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue