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 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}"
|
||||||
|
|
||||||
|
|
|
@ -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."
|
||||||
|
|
|
@ -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,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.
|
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)
|
return "\n".join(facts)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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:]
|
|
||||||
|
|
|
@ -176,15 +176,14 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)(
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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):
|
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)
|
return cls(**data)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue