from functools import partial from itertools import count from logging import getLogger 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.loops import loop_reduce, loop_retry from packit.results import multi_function_or_str_result from packit.toolbox import Toolbox from packit.utils import could_be_json from adventure.actions.base import ( action_ask, action_give, action_look, action_move, action_take, action_tell, ) from adventure.actions.planning import ( erase_notes, get_recent_notes, get_upcoming_events, read_calendar, read_notes, replace_note, schedule_event, take_note, ) from adventure.context import ( broadcast, get_actor_agent_for_name, get_actor_for_agent, get_current_step, get_current_world, set_current_actor, set_current_room, set_current_step, set_current_world, set_game_systems, ) 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.effect import expire_effects 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__) def world_result_parser(value, agent, **kwargs): current_world = get_current_world() if not current_world: raise ValueError( "The current world must be set before calling world_result_parser" ) logger.debug(f"parsing action for {agent.name}: {value}") current_actor = get_actor_for_agent(agent) current_room = next( (room for room in current_world.rooms if current_actor in room.actors), None ) set_current_room(current_room) set_current_actor(current_actor) return multi_function_or_str_result(value, agent=agent, **kwargs) def prompt_actor_action(room, actor, agent, action_names, action_toolbox) -> str: # collect data for the prompt 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] actor_attributes = format_attributes(actor) # actor_effects = [effect.name for effect in actor.active_effects] actor_items = [item.name for item in actor.items] # set up a result parser for the agent def result_parser(value, agent, **kwargs): if not room or not actor: raise ValueError("Room and actor must be set before parsing results") if could_be_json(value): event = ActionEvent.from_json(value, room, actor) else: event = ReplyEvent.from_text(value, room, actor) broadcast(event) return world_result_parser(value, agent, **kwargs) # prompt and act logger.info("starting turn for actor: %s", actor.name) result = loop_retry( agent, ( "You are currently in {room_name}. {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}. " "What will you do next? Reply with a JSON function call, calling one of the actions." "You can only perform one action per turn. What is your next action?" ), context={ "actions": action_names, "actor_items": actor_items, "attributes": actor_attributes, "directions": room_directions, "room_name": room.name, "room_description": describe_entity(room), "visible_actors": room_actors, "visible_items": room_items, }, result_parser=result_parser, toolbox=action_toolbox, ) logger.debug(f"{actor.name} step result: {result}") if agent.memory: # TODO: make sure this is not duplicating memories and wasting space agent.memory.append(result) 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() if len(recent_notes) > 0: notes = "\n".join(recent_notes) notes_prompt = f"Your recent notes are: {notes}\n" else: notes_prompt = "You have no recent notes.\n" if len(upcoming_events) > 0: current_step = get_current_step() events = [ f"{event.name} in {event.turn - current_step} turns" for event in upcoming_events ] events = "\n".join(events) events_prompt = f"Upcoming events are: {events}\n" else: events_prompt = "You have no upcoming events.\n" 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) 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 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. " "Think about your goals and any quests that you are working on, and plan your next action accordingly. " "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={ "event_count": event_count, "events_prompt": events_prompt, "note_count": note_count, "notes_prompt": notes_prompt, }, result_parser=result_parser, stop_condition=stop_condition, toolbox=planner_toolbox, ) if agent.memory: agent.memory.append(result) return result def simulate_world( world: World, steps: float | int = inf, actions: Sequence[Callable[..., str]] = [], systems: Sequence[GameSystem] = [], ): logger.info("simulating the world") set_current_world(world) set_game_systems(systems) # build a toolbox for the actions action_tools = Toolbox( [ action_ask, action_give, action_look, action_move, action_take, action_tell, *actions, ] ) action_names = action_tools.list_tools() # build a toolbox for the planners planner_toolbox = Toolbox( [ take_note, read_notes, replace_note, erase_notes, schedule_event, read_calendar, ] ) # simulate each actor for i in count(): current_step = get_current_step() logger.info(f"simulating step {i} of {steps} (world step {current_step})") for actor_name in world.order: actor, agent = get_actor_agent_for_name(actor_name) if not agent or not actor: logger.error(f"agent or actor not found for name {actor_name}") continue room = find_room_with_actor(world, actor) if not room: logger.error(f"actor {actor_name} is not in a room") continue # prep context set_current_room(room) set_current_actor(actor) # decrement effects on the actor and remove any that have expired expire_effects(actor) # TODO: expire calendar events # 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) 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_event = ResultEvent(result=result, room=room, actor=actor) broadcast(result_event) for system in systems: if system.simulate: system.simulate(world, current_step) set_current_step(current_step + 1) if i >= steps: logger.info("reached step limit at world step %s", current_step + 1) break