switch to events, close threads
This commit is contained in:
parent
7d14e9ee76
commit
cf61060450
|
@ -197,7 +197,7 @@ def action_give(character: str, item_name: str) -> str:
|
||||||
if not item:
|
if not item:
|
||||||
return f"You do not have the {item_name} item in your inventory."
|
return f"You do not have the {item_name} item in your inventory."
|
||||||
|
|
||||||
broadcast(f"{action_actor.name} gives {character} the {item_name} item")
|
broadcast(f"{action_actor.name} gives {character} the {item_name} item.")
|
||||||
action_actor.items.remove(item)
|
action_actor.items.remove(item)
|
||||||
destination_actor.items.append(item)
|
destination_actor.items.append(item)
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ from typing import Callable, Dict, Tuple
|
||||||
|
|
||||||
from packit.agent import Agent
|
from packit.agent import Agent
|
||||||
|
|
||||||
from adventure.models import Actor, Room, World
|
from adventure.models.entity import Actor, Room, World
|
||||||
|
|
||||||
current_broadcast: Callable[[str], None] | None = None
|
current_broadcast: Callable[[str], None] | None = None
|
||||||
current_world: World | None = None
|
current_world: World | None = None
|
||||||
|
@ -16,6 +16,16 @@ dungeon_master: Agent | None = None
|
||||||
actor_agents: Dict[str, Tuple[Actor, Agent]] = {}
|
actor_agents: Dict[str, Tuple[Actor, Agent]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def broadcast(message: str):
|
||||||
|
if current_broadcast:
|
||||||
|
current_broadcast(message)
|
||||||
|
|
||||||
|
|
||||||
|
def has_dungeon_master():
|
||||||
|
return dungeon_master is not None
|
||||||
|
|
||||||
|
|
||||||
|
# region context getters
|
||||||
def get_current_context() -> Tuple[World, Room, Actor]:
|
def get_current_context() -> Tuple[World, Room, Actor]:
|
||||||
if not current_world:
|
if not current_world:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
@ -47,11 +57,23 @@ def get_current_broadcast():
|
||||||
return current_broadcast
|
return current_broadcast
|
||||||
|
|
||||||
|
|
||||||
def broadcast(message: str):
|
def get_current_step() -> int:
|
||||||
if current_broadcast:
|
return current_step
|
||||||
current_broadcast(message)
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_dungeon_master() -> Agent:
|
||||||
|
if not dungeon_master:
|
||||||
|
raise ValueError(
|
||||||
|
"The dungeon master must be set before calling action functions"
|
||||||
|
)
|
||||||
|
|
||||||
|
return dungeon_master
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
# region context setters
|
||||||
def set_current_broadcast(broadcast):
|
def set_current_broadcast(broadcast):
|
||||||
global current_broadcast
|
global current_broadcast
|
||||||
current_broadcast = broadcast
|
current_broadcast = broadcast
|
||||||
|
@ -72,15 +94,24 @@ def set_current_actor(actor: Actor | None):
|
||||||
current_actor = actor
|
current_actor = actor
|
||||||
|
|
||||||
|
|
||||||
def get_current_step() -> int:
|
|
||||||
return current_step
|
|
||||||
|
|
||||||
|
|
||||||
def set_current_step(step: int):
|
def set_current_step(step: int):
|
||||||
global current_step
|
global current_step
|
||||||
current_step = step
|
current_step = step
|
||||||
|
|
||||||
|
|
||||||
|
def set_actor_agent(name, actor, agent):
|
||||||
|
actor_agents[name] = (actor, agent)
|
||||||
|
|
||||||
|
|
||||||
|
def set_dungeon_master(agent):
|
||||||
|
global dungeon_master
|
||||||
|
dungeon_master = agent
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
# region search functions
|
||||||
def get_actor_for_agent(agent):
|
def get_actor_for_agent(agent):
|
||||||
return next(
|
return next(
|
||||||
(
|
(
|
||||||
|
@ -114,27 +145,8 @@ def get_actor_agent_for_name(name):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def set_actor_agent_for_name(name, actor, agent):
|
|
||||||
actor_agents[name] = (actor, agent)
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_actor_agents():
|
def get_all_actor_agents():
|
||||||
return list(actor_agents.values())
|
return list(actor_agents.values())
|
||||||
|
|
||||||
|
|
||||||
def set_dungeon_master(agent):
|
# endregion
|
||||||
global dungeon_master
|
|
||||||
dungeon_master = agent
|
|
||||||
|
|
||||||
|
|
||||||
def get_dungeon_master() -> Agent:
|
|
||||||
if not dungeon_master:
|
|
||||||
raise ValueError(
|
|
||||||
"The dungeon master must be set before calling action functions"
|
|
||||||
)
|
|
||||||
|
|
||||||
return dungeon_master
|
|
||||||
|
|
||||||
|
|
||||||
def has_dungeon_master():
|
|
||||||
return dungeon_master is not None
|
|
||||||
|
|
|
@ -1,27 +1,34 @@
|
||||||
# from functools import cache
|
|
||||||
from json import loads
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from os import environ
|
from os import environ
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from re import sub
|
from re import sub
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from typing import Literal
|
from typing import Tuple
|
||||||
|
|
||||||
from discord import Client, Embed, File, Intents
|
from discord import Client, Embed, File, Intents
|
||||||
from packit.utils import could_be_json
|
|
||||||
|
|
||||||
from adventure.context import (
|
from adventure.context import (
|
||||||
get_actor_agent_for_name,
|
get_actor_agent_for_name,
|
||||||
get_current_world,
|
get_current_world,
|
||||||
set_actor_agent_for_name,
|
set_actor_agent,
|
||||||
|
)
|
||||||
|
from adventure.models.event import (
|
||||||
|
ActionEvent,
|
||||||
|
GameEvent,
|
||||||
|
GenerateEvent,
|
||||||
|
PromptEvent,
|
||||||
|
ReplyEvent,
|
||||||
|
ResultEvent,
|
||||||
|
StatusEvent,
|
||||||
)
|
)
|
||||||
from adventure.models import Actor, Room
|
|
||||||
from adventure.player import RemotePlayer, get_player, has_player, set_player
|
from adventure.player import RemotePlayer, get_player, has_player, set_player
|
||||||
from adventure.render_comfy import generate_image_tool
|
from adventure.render_comfy import generate_image_tool
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
client = None
|
client = None
|
||||||
prompt_queue: Queue = Queue()
|
|
||||||
|
active_tasks = set()
|
||||||
|
prompt_queue: Queue[Tuple[GameEvent, Embed | str]] = Queue()
|
||||||
|
|
||||||
|
|
||||||
def remove_tags(text: str) -> str:
|
def remove_tags(text: str) -> str:
|
||||||
|
@ -32,6 +39,54 @@ def remove_tags(text: str) -> str:
|
||||||
return sub(r"<[^>]*>", "", text)
|
return sub(r"<[^>]*>", "", text)
|
||||||
|
|
||||||
|
|
||||||
|
def find_embed_field(embed: Embed, name: str) -> str | None:
|
||||||
|
return next((field.value for field in embed.fields if field.name == name), None)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: becomes prompt from event
|
||||||
|
def prompt_from_embed(embed: Embed) -> str | None:
|
||||||
|
room_name = embed.title
|
||||||
|
actor_name = embed.description
|
||||||
|
|
||||||
|
world = get_current_world()
|
||||||
|
if not world:
|
||||||
|
return
|
||||||
|
|
||||||
|
room = next((room for room in world.rooms if room.name == room_name), None)
|
||||||
|
if not room:
|
||||||
|
return
|
||||||
|
|
||||||
|
actor = next((actor for actor in room.actors if actor.name == actor_name), None)
|
||||||
|
if not actor:
|
||||||
|
return
|
||||||
|
|
||||||
|
item_field = find_embed_field(embed, "Item")
|
||||||
|
|
||||||
|
action_field = find_embed_field(embed, "Action")
|
||||||
|
if action_field:
|
||||||
|
if item_field:
|
||||||
|
item = next(
|
||||||
|
(
|
||||||
|
item
|
||||||
|
for item in (room.items + actor.items)
|
||||||
|
if item.name == item_field
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if item:
|
||||||
|
return f"{actor.name} {action_field} the {item.name}. {item.description}. {actor.description}. {room.description}."
|
||||||
|
|
||||||
|
return f"{actor.name} {action_field} the {item_field}. {actor.description}. {room.description}."
|
||||||
|
|
||||||
|
return f"{actor.name} {action_field}. {actor.description}. {room.name}."
|
||||||
|
|
||||||
|
result_field = find_embed_field(embed, "Result")
|
||||||
|
if result_field:
|
||||||
|
return f"{result_field}. {actor.description}. {room.description}."
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
class AdventureClient(Client):
|
class AdventureClient(Client):
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
logger.info(f"Logged in as {self.user}")
|
logger.info(f"Logged in as {self.user}")
|
||||||
|
@ -46,31 +101,14 @@ class AdventureClient(Client):
|
||||||
# TODO: look up event that caused this message, get the room and actors
|
# TODO: look up event that caused this message, get the room and actors
|
||||||
if len(reaction.message.embeds) > 0:
|
if len(reaction.message.embeds) > 0:
|
||||||
embed = reaction.message.embeds[0]
|
embed = reaction.message.embeds[0]
|
||||||
room_name = embed.title
|
prompt = prompt_from_embed(embed)
|
||||||
actor_name = embed.description
|
|
||||||
prompt = f"{room_name}. {actor_name}."
|
|
||||||
await reaction.message.channel.send(f"Generating image for: {prompt}")
|
|
||||||
|
|
||||||
world = get_current_world()
|
|
||||||
if not world:
|
|
||||||
return
|
|
||||||
|
|
||||||
room = next(
|
|
||||||
(room for room in world.rooms if room.name == room_name), None
|
|
||||||
)
|
|
||||||
if not room:
|
|
||||||
return
|
|
||||||
|
|
||||||
actor = next(
|
|
||||||
(actor for actor in room.actors if actor.name == actor_name), None
|
|
||||||
)
|
|
||||||
if not actor:
|
|
||||||
return
|
|
||||||
|
|
||||||
prompt = f"{room.name}. {actor.name}."
|
|
||||||
else:
|
else:
|
||||||
prompt = remove_tags(reaction.message.content)
|
prompt = remove_tags(reaction.message.content)
|
||||||
|
if prompt.startswith("Generating"):
|
||||||
|
# TODO: get the entity from the message
|
||||||
|
pass
|
||||||
|
|
||||||
|
await reaction.message.add_reaction("📸")
|
||||||
paths = generate_image_tool(prompt, 2)
|
paths = generate_image_tool(prompt, 2)
|
||||||
logger.info(f"Generated images: {paths}")
|
logger.info(f"Generated images: {paths}")
|
||||||
|
|
||||||
|
@ -110,20 +148,22 @@ class AdventureClient(Client):
|
||||||
await channel.send(f"Character `{character_name}` not found!")
|
await channel.send(f"Character `{character_name}` not found!")
|
||||||
return
|
return
|
||||||
|
|
||||||
def prompt_player(character: str, prompt: str):
|
def prompt_player(event: PromptEvent):
|
||||||
logger.info(
|
logger.info(
|
||||||
"append prompt for character %s (user %s) to queue: %s",
|
"append prompt for character %s (user %s) to queue: %s",
|
||||||
character,
|
event.actor.name,
|
||||||
user_name,
|
user_name,
|
||||||
prompt,
|
event.prompt,
|
||||||
)
|
)
|
||||||
prompt_queue.put((character, prompt))
|
|
||||||
|
# TODO: build an embed from the prompt
|
||||||
|
prompt_queue.put((event, event.prompt))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
player = RemotePlayer(
|
player = RemotePlayer(
|
||||||
actor.name, actor.backstory, prompt_player, fallback_agent=agent
|
actor.name, actor.backstory, prompt_player, fallback_agent=agent
|
||||||
)
|
)
|
||||||
set_actor_agent_for_name(character_name, actor, player)
|
set_actor_agent(character_name, actor, player)
|
||||||
set_player(user_name, player)
|
set_player(user_name, player)
|
||||||
|
|
||||||
logger.info(f"{user_name} has joined the game as {actor.name}!")
|
logger.info(f"{user_name} has joined the game as {actor.name}!")
|
||||||
|
@ -153,9 +193,6 @@ class AdventureClient(Client):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
active_tasks = set()
|
|
||||||
|
|
||||||
|
|
||||||
def launch_bot():
|
def launch_bot():
|
||||||
def bot_main():
|
def bot_main():
|
||||||
global client
|
global client
|
||||||
|
@ -170,27 +207,29 @@ def launch_bot():
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
sleep(0.5)
|
sleep(0.1)
|
||||||
if prompt_queue.empty():
|
if prompt_queue.empty():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if len(active_tasks) > 0:
|
if len(active_tasks) > 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
character, prompt = prompt_queue.get()
|
event, prompt = prompt_queue.get()
|
||||||
logger.info("Prompting character %s: %s", character, prompt)
|
logger.info("Prompting for event %s: %s", event, prompt)
|
||||||
|
|
||||||
if client:
|
if client:
|
||||||
prompt_task = client.loop.create_task(broadcast_event(prompt))
|
prompt_task = client.loop.create_task(broadcast_event(prompt))
|
||||||
active_tasks.add(prompt_task)
|
active_tasks.add(prompt_task)
|
||||||
prompt_task.add_done_callback(active_tasks.discard)
|
prompt_task.add_done_callback(active_tasks.discard)
|
||||||
|
|
||||||
bot_thread = Thread(target=bot_main)
|
bot_thread = Thread(target=bot_main, daemon=True)
|
||||||
bot_thread.start()
|
bot_thread.start()
|
||||||
|
|
||||||
prompt_thread = Thread(target=prompt_main)
|
prompt_thread = Thread(target=prompt_main, daemon=True)
|
||||||
prompt_thread.start()
|
prompt_thread.start()
|
||||||
|
|
||||||
|
return [bot_thread, prompt_thread]
|
||||||
|
|
||||||
|
|
||||||
def stop_bot():
|
def stop_bot():
|
||||||
global client
|
global client
|
||||||
|
@ -238,39 +277,48 @@ async def broadcast_event(message: str | Embed):
|
||||||
await channel.send(embed=message)
|
await channel.send(embed=message)
|
||||||
|
|
||||||
|
|
||||||
def bot_action(room: Room, actor: Actor, message: str):
|
def bot_event(event: GameEvent):
|
||||||
try:
|
if isinstance(event, GenerateEvent):
|
||||||
action_embed = Embed(title=room.name, description=actor.name)
|
bot_generate(event)
|
||||||
|
elif isinstance(event, ResultEvent):
|
||||||
|
bot_result(event)
|
||||||
|
elif isinstance(event, (ActionEvent, ReplyEvent)):
|
||||||
|
bot_action(event)
|
||||||
|
elif isinstance(event, StatusEvent):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
logger.warning("Unknown event type: %s", event)
|
||||||
|
|
||||||
if could_be_json(message):
|
|
||||||
action_data = loads(message)
|
def bot_action(event: ActionEvent | ReplyEvent):
|
||||||
action_name = action_data["function"].replace("action_", "").title()
|
try:
|
||||||
action_parameters = action_data.get("parameters", {})
|
action_embed = Embed(title=event.room.name, description=event.actor.name)
|
||||||
|
|
||||||
|
if isinstance(event, ActionEvent):
|
||||||
|
action_name = event.action.replace("action_", "").title()
|
||||||
|
action_parameters = event.parameters
|
||||||
|
|
||||||
action_embed.add_field(name="Action", value=action_name)
|
action_embed.add_field(name="Action", value=action_name)
|
||||||
|
|
||||||
for key, value in action_parameters.items():
|
for key, value in action_parameters.items():
|
||||||
action_embed.add_field(name=key.replace("_", " ").title(), value=value)
|
action_embed.add_field(name=key.replace("_", " ").title(), value=value)
|
||||||
else:
|
else:
|
||||||
action_embed.add_field(name="Message", value=message)
|
action_embed.add_field(name="Message", value=event.text)
|
||||||
|
|
||||||
prompt_queue.put((actor.name, action_embed))
|
prompt_queue.put((event, action_embed))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to broadcast action: %s", e)
|
logger.error("Failed to broadcast action: %s", e)
|
||||||
|
|
||||||
|
|
||||||
def bot_event(message: str):
|
def bot_generate(event: GenerateEvent):
|
||||||
prompt_queue.put((None, message))
|
prompt_queue.put((event, event.name))
|
||||||
|
|
||||||
|
|
||||||
def bot_result(room: Room, actor: Actor, action: str):
|
def bot_result(event: ResultEvent):
|
||||||
result_embed = Embed(title=room.name, description=actor.name)
|
text = event.result
|
||||||
result_embed.add_field(name="Result", value=action)
|
if len(text) > 1000:
|
||||||
prompt_queue.put((actor.name, result_embed))
|
text = text[:1000] + "..."
|
||||||
|
|
||||||
|
result_embed = Embed(title=event.room.name, description=event.actor.name)
|
||||||
def player_event(character: str, id: str, event: Literal["join", "leave"]):
|
result_embed.add_field(name="Result", value=text)
|
||||||
if event == "join":
|
prompt_queue.put((event, result_embed))
|
||||||
prompt_queue.put((character, f"{character} has joined the game!"))
|
|
||||||
elif event == "leave":
|
|
||||||
prompt_queue.put((character, f"{character} has left the game!"))
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from random import choice, randint
|
from random import choice, randint
|
||||||
from typing import Callable, List
|
from typing import List
|
||||||
|
|
||||||
from packit.agent import Agent
|
from packit.agent import Agent
|
||||||
from packit.loops import loop_retry
|
from packit.loops import loop_retry
|
||||||
|
|
||||||
from adventure.models import Actor, Item, Room, World
|
from adventure.models.entity import Actor, Item, Room, World
|
||||||
|
from adventure.models.event import EventCallback, GenerateEvent
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
@ -17,13 +18,10 @@ OPPOSITE_DIRECTIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
GenerateCallback = Callable[[str], None]
|
|
||||||
|
|
||||||
|
|
||||||
def generate_room(
|
def generate_room(
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
world_theme: str,
|
world_theme: str,
|
||||||
callback: GenerateCallback | None = None,
|
callback: EventCallback | None = None,
|
||||||
existing_rooms: List[str] = [],
|
existing_rooms: List[str] = [],
|
||||||
) -> Room:
|
) -> Room:
|
||||||
def unique_name(name: str, **kwargs):
|
def unique_name(name: str, **kwargs):
|
||||||
|
@ -45,7 +43,7 @@ def generate_room(
|
||||||
)
|
)
|
||||||
|
|
||||||
if callable(callback):
|
if callable(callback):
|
||||||
callback(f"Generating room: {name}")
|
callback(GenerateEvent.from_name(f"Generating room: {name}"))
|
||||||
|
|
||||||
desc = agent(
|
desc = agent(
|
||||||
"Generate a detailed description of the {name} area. What does it look like? "
|
"Generate a detailed description of the {name} area. What does it look like? "
|
||||||
|
@ -65,7 +63,7 @@ def generate_room(
|
||||||
def generate_item(
|
def generate_item(
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
world_theme: str,
|
world_theme: str,
|
||||||
callback: Callable[[str], None] | None = None,
|
callback: EventCallback | None = None,
|
||||||
dest_room: str | None = None,
|
dest_room: str | None = None,
|
||||||
dest_actor: str | None = None,
|
dest_actor: str | None = None,
|
||||||
existing_items: List[str] = [],
|
existing_items: List[str] = [],
|
||||||
|
@ -99,7 +97,7 @@ def generate_item(
|
||||||
)
|
)
|
||||||
|
|
||||||
if callable(callback):
|
if callable(callback):
|
||||||
callback(f"Generating item: {name}")
|
callback(GenerateEvent.from_name(f"Generating item: {name}"))
|
||||||
|
|
||||||
desc = agent(
|
desc = agent(
|
||||||
"Generate a detailed description of the {name} item. What does it look like? What is it made of? What does it do?",
|
"Generate a detailed description of the {name} item. What does it look like? What is it made of? What does it do?",
|
||||||
|
@ -115,7 +113,7 @@ def generate_actor(
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
world_theme: str,
|
world_theme: str,
|
||||||
dest_room: str,
|
dest_room: str,
|
||||||
callback: GenerateCallback | None = None,
|
callback: EventCallback | None = None,
|
||||||
existing_actors: List[str] = [],
|
existing_actors: List[str] = [],
|
||||||
) -> Actor:
|
) -> Actor:
|
||||||
def unique_name(name: str, **kwargs):
|
def unique_name(name: str, **kwargs):
|
||||||
|
@ -141,7 +139,7 @@ def generate_actor(
|
||||||
)
|
)
|
||||||
|
|
||||||
if callable(callback):
|
if callable(callback):
|
||||||
callback(f"Generating actor: {name}")
|
callback(GenerateEvent.from_name(f"Generating actor: {name}"))
|
||||||
|
|
||||||
description = agent(
|
description = agent(
|
||||||
"Generate a detailed description of the {name} character. What do they look like? What are they wearing? "
|
"Generate a detailed description of the {name} character. What do they look like? What are they wearing? "
|
||||||
|
@ -169,12 +167,14 @@ def generate_world(
|
||||||
theme: str,
|
theme: str,
|
||||||
room_count: int | None = None,
|
room_count: int | None = None,
|
||||||
max_rooms: int = 5,
|
max_rooms: int = 5,
|
||||||
callback: Callable[[str], None] | None = None,
|
callback: EventCallback | None = None,
|
||||||
) -> World:
|
) -> World:
|
||||||
room_count = room_count or randint(3, max_rooms)
|
room_count = room_count or randint(3, max_rooms)
|
||||||
|
|
||||||
if callable(callback):
|
if callable(callback):
|
||||||
callback(f"Generating a {theme} with {room_count} rooms")
|
callback(
|
||||||
|
GenerateEvent.from_name(f"Generating a {theme} with {room_count} rooms")
|
||||||
|
)
|
||||||
|
|
||||||
existing_actors: List[str] = []
|
existing_actors: List[str] = []
|
||||||
existing_items: List[str] = []
|
existing_items: List[str] = []
|
||||||
|
@ -192,7 +192,11 @@ def generate_world(
|
||||||
item_count = randint(1, 3)
|
item_count = randint(1, 3)
|
||||||
|
|
||||||
if callable(callback):
|
if callable(callback):
|
||||||
callback(f"Generating {item_count} items for room: {room.name}")
|
callback(
|
||||||
|
GenerateEvent.from_name(
|
||||||
|
f"Generating {item_count} items for room: {room.name}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
for j in range(item_count):
|
for j in range(item_count):
|
||||||
item = generate_item(
|
item = generate_item(
|
||||||
|
@ -208,7 +212,11 @@ def generate_world(
|
||||||
actor_count = randint(1, 3)
|
actor_count = randint(1, 3)
|
||||||
|
|
||||||
if callable(callback):
|
if callable(callback):
|
||||||
callback(f"Generating {actor_count} actors for room: {room.name}")
|
callback(
|
||||||
|
GenerateEvent.from_name(
|
||||||
|
f"Generating {actor_count} actors for room: {room.name}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
for j in range(actor_count):
|
for j in range(actor_count):
|
||||||
actor = generate_actor(
|
actor = generate_actor(
|
||||||
|
@ -225,7 +233,11 @@ def generate_world(
|
||||||
item_count = randint(0, 2)
|
item_count = randint(0, 2)
|
||||||
|
|
||||||
if callable(callback):
|
if callable(callback):
|
||||||
callback(f"Generating {item_count} items for actor {actor.name}")
|
callback(
|
||||||
|
GenerateEvent.from_name(
|
||||||
|
f"Generating {item_count} items for actor {actor.name}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
for k in range(item_count):
|
for k in range(item_count):
|
||||||
item = generate_item(
|
item = generate_item(
|
||||||
|
|
|
@ -7,7 +7,7 @@ from pydantic import Field
|
||||||
from rule_engine import Rule
|
from rule_engine import Rule
|
||||||
from yaml import Loader, load
|
from yaml import Loader, load
|
||||||
|
|
||||||
from adventure.models import (
|
from adventure.models.entity import (
|
||||||
Actor,
|
Actor,
|
||||||
Attributes,
|
Attributes,
|
||||||
AttributeValue,
|
AttributeValue,
|
||||||
|
|
|
@ -1,26 +1,33 @@
|
||||||
from json import load
|
import atexit
|
||||||
from logging.config import dictConfig
|
from logging.config import dictConfig
|
||||||
from os import environ, path
|
from os import environ, path
|
||||||
from typing import Callable, Sequence, Tuple
|
from typing import List
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from packit.agent import Agent, agent_easy_connect
|
from packit.agent import Agent, agent_easy_connect
|
||||||
from packit.loops import loop_retry
|
|
||||||
from packit.results import multi_function_or_str_result
|
|
||||||
from packit.toolbox import Toolbox
|
|
||||||
from packit.utils import logger_with_colors
|
from packit.utils import logger_with_colors
|
||||||
|
from yaml import Loader, load
|
||||||
|
|
||||||
from adventure.context import set_current_broadcast, set_dungeon_master
|
from adventure.context import set_current_step, set_dungeon_master
|
||||||
from adventure.models import Attributes
|
from adventure.generate import generate_world
|
||||||
|
from adventure.models.entity import World, WorldState
|
||||||
|
from adventure.models.event import EventCallback, GameEvent
|
||||||
|
from adventure.models.files import PromptFile, WorldPrompt
|
||||||
from adventure.plugins import load_plugin
|
from adventure.plugins import load_plugin
|
||||||
|
from adventure.simulate import simulate_world
|
||||||
|
from adventure.state import create_agents, save_world, save_world_state
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
|
def load_yaml(file):
|
||||||
|
return load(file, Loader=Loader)
|
||||||
|
|
||||||
|
|
||||||
|
# configure logging
|
||||||
LOG_PATH = "logging.json"
|
LOG_PATH = "logging.json"
|
||||||
# LOG_PATH = "dev-logging.json"
|
|
||||||
try:
|
try:
|
||||||
if path.exists(LOG_PATH):
|
if path.exists(LOG_PATH):
|
||||||
with open(LOG_PATH, "r") as f:
|
with open(LOG_PATH, "r") as f:
|
||||||
config_logging = load(f)
|
config_logging = load_yaml(f)
|
||||||
dictConfig(config_logging)
|
dictConfig(config_logging)
|
||||||
else:
|
else:
|
||||||
print("logging config not found")
|
print("logging config not found")
|
||||||
|
@ -28,166 +35,12 @@ try:
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
print("error loading logging config: %s" % (err))
|
print("error loading logging config: %s" % (err))
|
||||||
|
|
||||||
if True:
|
|
||||||
from adventure.actions import (
|
|
||||||
action_ask,
|
|
||||||
action_give,
|
|
||||||
action_look,
|
|
||||||
action_move,
|
|
||||||
action_take,
|
|
||||||
action_tell,
|
|
||||||
)
|
|
||||||
from adventure.context import (
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
from adventure.generate import generate_world
|
|
||||||
from adventure.models import Actor, Room, World, WorldState
|
|
||||||
from adventure.state import create_agents, save_world, save_world_state
|
|
||||||
|
|
||||||
logger = logger_with_colors(__name__, level="DEBUG")
|
logger = logger_with_colors(__name__, level="DEBUG")
|
||||||
|
|
||||||
load_dotenv(environ.get("ADVENTURE_ENV", ".env"), override=True)
|
load_dotenv(environ.get("ADVENTURE_ENV", ".env"), override=True)
|
||||||
|
|
||||||
|
|
||||||
# simulation
|
|
||||||
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 simulate_world(
|
|
||||||
world: World,
|
|
||||||
steps: int = 10,
|
|
||||||
actions: Sequence[Callable[..., str]] = [],
|
|
||||||
systems: Sequence[
|
|
||||||
Tuple[Callable[[World, int], None], Callable[[Attributes], str] | None]
|
|
||||||
] = [],
|
|
||||||
event_callbacks: Sequence[Callable[[str], None]] = [],
|
|
||||||
input_callbacks: Sequence[Callable[[Room, Actor, str], None]] = [],
|
|
||||||
result_callbacks: Sequence[Callable[[Room, Actor, str], None]] = [],
|
|
||||||
):
|
|
||||||
logger.info("Simulating the world")
|
|
||||||
set_current_world(world)
|
|
||||||
|
|
||||||
# set up a broadcast callback
|
|
||||||
def broadcast_callback(message):
|
|
||||||
logger.info(message)
|
|
||||||
for callback in event_callbacks:
|
|
||||||
callback(message)
|
|
||||||
|
|
||||||
set_current_broadcast(broadcast_callback)
|
|
||||||
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
# simulate each actor
|
|
||||||
for i in range(steps):
|
|
||||||
current_step = get_current_step()
|
|
||||||
logger.info(f"Simulating 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 = next((room for room in world.rooms if actor in room.actors), None)
|
|
||||||
if not room:
|
|
||||||
logger.error(f"Actor {actor_name} is not in a room")
|
|
||||||
continue
|
|
||||||
|
|
||||||
room_actors = [actor.name for actor in room.actors]
|
|
||||||
room_items = [item.name for item in room.items]
|
|
||||||
room_directions = list(room.portals.keys())
|
|
||||||
|
|
||||||
actor_attributes = " ".join(
|
|
||||||
system_format(actor.attributes)
|
|
||||||
for _, system_format in systems
|
|
||||||
if system_format
|
|
||||||
)
|
|
||||||
actor_items = [item.name for item in actor.items]
|
|
||||||
|
|
||||||
def result_parser(value, agent, **kwargs):
|
|
||||||
for callback in input_callbacks:
|
|
||||||
logger.info(
|
|
||||||
f"calling input callback for {actor_name}: {callback.__name__}"
|
|
||||||
)
|
|
||||||
callback(room, actor, value)
|
|
||||||
|
|
||||||
return world_result_parser(value, agent, **kwargs)
|
|
||||||
|
|
||||||
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?"
|
|
||||||
# Pick the most important action and save the rest for later."
|
|
||||||
),
|
|
||||||
context={
|
|
||||||
"actions": action_names,
|
|
||||||
"actor_items": actor_items,
|
|
||||||
"attributes": actor_attributes,
|
|
||||||
"directions": room_directions,
|
|
||||||
"room_name": room.name,
|
|
||||||
"room_description": room.description,
|
|
||||||
"visible_actors": room_actors,
|
|
||||||
"visible_items": room_items,
|
|
||||||
},
|
|
||||||
result_parser=result_parser,
|
|
||||||
toolbox=action_tools,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug(f"{actor.name} step result: {result}")
|
|
||||||
agent.memory.append(result)
|
|
||||||
|
|
||||||
for callback in result_callbacks:
|
|
||||||
callback(room, actor, result)
|
|
||||||
|
|
||||||
for system_update, _ in systems:
|
|
||||||
system_update(world, current_step)
|
|
||||||
|
|
||||||
set_current_step(current_step + 1)
|
|
||||||
|
|
||||||
|
|
||||||
# main
|
# main
|
||||||
def parse_args():
|
def parse_args():
|
||||||
import argparse
|
import argparse
|
||||||
|
@ -249,84 +102,72 @@ def parse_args():
|
||||||
default="world",
|
default="world",
|
||||||
help="The file to save the generated world to",
|
help="The file to save the generated world to",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--world-prompt",
|
||||||
|
type=str,
|
||||||
|
help="The file to load the world prompt from",
|
||||||
|
)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def get_world_prompt(args) -> WorldPrompt:
|
||||||
args = parse_args()
|
if args.world_prompt:
|
||||||
|
prompt_file, prompt_name = args.world_prompt.split(":")
|
||||||
|
with open(prompt_file, "r") as f:
|
||||||
|
prompts = PromptFile(**load_yaml(f))
|
||||||
|
for prompt in prompts.prompts:
|
||||||
|
if prompt.name == prompt_name:
|
||||||
|
return prompt
|
||||||
|
|
||||||
|
logger.warning(f"prompt {prompt_name} not found in {prompt_file}")
|
||||||
|
|
||||||
|
return WorldPrompt(
|
||||||
|
name=args.world,
|
||||||
|
theme=args.theme,
|
||||||
|
flavor=args.flavor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_or_generate_world(args, players, callbacks, world_prompt: WorldPrompt):
|
||||||
world_file = args.world + ".json"
|
world_file = args.world + ".json"
|
||||||
world_state_file = args.state or (args.world + ".state.json")
|
world_state_file = args.state or (args.world + ".state.json")
|
||||||
|
|
||||||
players = []
|
|
||||||
if args.player:
|
|
||||||
players.append(args.player)
|
|
||||||
|
|
||||||
# set up callbacks
|
|
||||||
event_callbacks = []
|
|
||||||
input_callbacks = []
|
|
||||||
result_callbacks = []
|
|
||||||
|
|
||||||
if args.discord:
|
|
||||||
from adventure.discord_bot import bot_action, bot_event, bot_result, launch_bot
|
|
||||||
|
|
||||||
launch_bot()
|
|
||||||
event_callbacks.append(bot_event)
|
|
||||||
input_callbacks.append(bot_action)
|
|
||||||
result_callbacks.append(bot_result)
|
|
||||||
|
|
||||||
if args.server:
|
|
||||||
from adventure.server import (
|
|
||||||
launch_server,
|
|
||||||
server_action,
|
|
||||||
server_event,
|
|
||||||
server_result,
|
|
||||||
server_system,
|
|
||||||
)
|
|
||||||
|
|
||||||
launch_server()
|
|
||||||
event_callbacks.append(server_event)
|
|
||||||
input_callbacks.append(server_action)
|
|
||||||
result_callbacks.append(server_result)
|
|
||||||
|
|
||||||
memory = {}
|
memory = {}
|
||||||
if path.exists(world_state_file):
|
if path.exists(world_state_file):
|
||||||
logger.info(f"Loading world state from {world_state_file}")
|
logger.info(f"loading world state from {world_state_file}")
|
||||||
with open(world_state_file, "r") as f:
|
with open(world_state_file, "r") as f:
|
||||||
state = WorldState(**load(f))
|
state = WorldState(**load_yaml(f))
|
||||||
|
|
||||||
set_current_step(state.step)
|
set_current_step(state.step)
|
||||||
|
|
||||||
memory = state.memory
|
memory = state.memory
|
||||||
world = state.world
|
world = state.world
|
||||||
world.name = args.world
|
|
||||||
elif path.exists(world_file):
|
elif path.exists(world_file):
|
||||||
logger.info(f"Loading world from {world_file}")
|
logger.info(f"loading world from {world_file}")
|
||||||
with open(world_file, "r") as f:
|
with open(world_file, "r") as f:
|
||||||
world = World(**load(f))
|
world = World(**load_yaml(f))
|
||||||
else:
|
else:
|
||||||
logger.info(f"Generating a new {args.theme} world")
|
logger.info(f"generating a new world using theme: {world_prompt.theme}")
|
||||||
llm = agent_easy_connect()
|
llm = agent_easy_connect()
|
||||||
world_builder = Agent(
|
world_builder = Agent(
|
||||||
"World Builder",
|
"World Builder",
|
||||||
f"You are an experienced game master creating a visually detailed {args.theme} world for a new adventure. {args.flavor}",
|
f"You are an experienced game master creating a visually detailed world for a new adventure. "
|
||||||
|
f"{world_prompt.flavor}. The theme is: {world_prompt.theme}.",
|
||||||
{},
|
{},
|
||||||
llm,
|
llm,
|
||||||
)
|
)
|
||||||
|
|
||||||
world = None
|
world = None
|
||||||
|
|
||||||
def broadcast_callback(message):
|
def broadcast_callback(event: GameEvent):
|
||||||
logger.info(message)
|
logger.info(event)
|
||||||
for callback in event_callbacks:
|
for callback in callbacks:
|
||||||
callback(message)
|
callback(event)
|
||||||
if args.server and world:
|
|
||||||
server_system(world, 0)
|
|
||||||
|
|
||||||
world = generate_world(
|
world = generate_world(
|
||||||
world_builder,
|
world_builder,
|
||||||
args.world,
|
args.world,
|
||||||
args.theme,
|
world_prompt.theme,
|
||||||
room_count=args.rooms,
|
room_count=args.rooms,
|
||||||
max_rooms=args.max_rooms,
|
max_rooms=args.max_rooms,
|
||||||
callback=broadcast_callback,
|
callback=broadcast_callback,
|
||||||
|
@ -334,40 +175,68 @@ def main():
|
||||||
save_world(world, world_file)
|
save_world(world, world_file)
|
||||||
|
|
||||||
create_agents(world, memory=memory, players=players)
|
create_agents(world, memory=memory, players=players)
|
||||||
|
return (world, world_state_file)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
players = []
|
||||||
|
if args.player:
|
||||||
|
players.append(args.player)
|
||||||
|
|
||||||
|
# set up callbacks
|
||||||
|
callbacks: List[EventCallback] = []
|
||||||
|
|
||||||
|
# launch other threads
|
||||||
|
threads = []
|
||||||
|
if args.discord:
|
||||||
|
from adventure.discord_bot import bot_event, launch_bot
|
||||||
|
|
||||||
|
threads.extend(launch_bot())
|
||||||
|
callbacks.append(bot_event)
|
||||||
|
|
||||||
if args.server:
|
if args.server:
|
||||||
server_system(world, 0)
|
from adventure.server import launch_server, server_event, server_system
|
||||||
|
|
||||||
# load extra actions
|
threads.extend(launch_server())
|
||||||
|
callbacks.append(server_event)
|
||||||
|
|
||||||
|
# register the thread shutdown handler
|
||||||
|
def shutdown_threads():
|
||||||
|
for thread in threads:
|
||||||
|
thread.join(1.0)
|
||||||
|
|
||||||
|
atexit.register(shutdown_threads)
|
||||||
|
|
||||||
|
# load built-in but optional actions
|
||||||
extra_actions = []
|
extra_actions = []
|
||||||
for action_name in args.actions or []:
|
|
||||||
logger.info(f"Loading extra actions from {action_name}")
|
|
||||||
module_actions = load_plugin(action_name)
|
|
||||||
logger.info(
|
|
||||||
f"Loaded extra actions: {[action.__name__ for action in module_actions]}"
|
|
||||||
)
|
|
||||||
extra_actions.extend(module_actions)
|
|
||||||
|
|
||||||
if args.optional_actions:
|
if args.optional_actions:
|
||||||
logger.info("Loading optional actions")
|
logger.info("loading optional actions")
|
||||||
from adventure.optional_actions import init as init_optional_actions
|
from adventure.optional_actions import init as init_optional_actions
|
||||||
|
|
||||||
optional_actions = init_optional_actions()
|
optional_actions = init_optional_actions()
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Loaded optional actions: {[action.__name__ for action in optional_actions]}"
|
f"loaded optional actions: {[action.__name__ for action in optional_actions]}"
|
||||||
)
|
)
|
||||||
extra_actions.extend(optional_actions)
|
extra_actions.extend(optional_actions)
|
||||||
|
|
||||||
# load extra systems
|
# load extra actions from plugins
|
||||||
def snapshot_system(world: World, step: int) -> None:
|
for action_name in args.actions or []:
|
||||||
logger.debug("Snapshotting world state")
|
logger.info(f"loading extra actions from {action_name}")
|
||||||
save_world_state(world, step, world_state_file)
|
module_actions = load_plugin(action_name)
|
||||||
|
logger.info(
|
||||||
|
f"loaded extra actions: {[action.__name__ for action in module_actions]}"
|
||||||
|
)
|
||||||
|
extra_actions.extend(module_actions)
|
||||||
|
|
||||||
extra_systems = [(snapshot_system, None)]
|
# load extra systems from plugins
|
||||||
|
extra_systems = []
|
||||||
for system_name in args.systems or []:
|
for system_name in args.systems or []:
|
||||||
logger.info(f"Loading extra systems from {system_name}")
|
logger.info(f"loading extra systems from {system_name}")
|
||||||
module_systems = load_plugin(system_name)
|
module_systems = load_plugin(system_name)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Loaded extra systems: {[component.__name__ for system in module_systems for component in system]}"
|
f"loaded extra systems: {[component.__name__ for system in module_systems for component in system]}"
|
||||||
)
|
)
|
||||||
extra_systems.extend(module_systems)
|
extra_systems.extend(module_systems)
|
||||||
|
|
||||||
|
@ -375,6 +244,23 @@ def main():
|
||||||
if args.server:
|
if args.server:
|
||||||
extra_systems.append((server_system, None))
|
extra_systems.append((server_system, None))
|
||||||
|
|
||||||
|
# load or generate the world
|
||||||
|
world_prompt = get_world_prompt(args)
|
||||||
|
world, world_state_file = load_or_generate_world(
|
||||||
|
args, players, callbacks, world_prompt=world_prompt
|
||||||
|
)
|
||||||
|
|
||||||
|
# make sure the snapshot system runs last
|
||||||
|
def snapshot_system(world: World, step: int) -> None:
|
||||||
|
logger.info("taking snapshot of world state")
|
||||||
|
save_world_state(world, step, world_state_file)
|
||||||
|
|
||||||
|
extra_systems.append((snapshot_system, None))
|
||||||
|
|
||||||
|
# run the systems once to initialize everything
|
||||||
|
for system_update, _ in extra_systems:
|
||||||
|
system_update(world, 0)
|
||||||
|
|
||||||
# create the DM
|
# create the DM
|
||||||
llm = agent_easy_connect()
|
llm = agent_easy_connect()
|
||||||
world_builder = Agent(
|
world_builder = Agent(
|
||||||
|
@ -390,14 +276,13 @@ def main():
|
||||||
set_dungeon_master(world_builder)
|
set_dungeon_master(world_builder)
|
||||||
|
|
||||||
# start the sim
|
# start the sim
|
||||||
logger.debug("Simulating world: %s", world)
|
logger.debug("simulating world: %s", world)
|
||||||
simulate_world(
|
simulate_world(
|
||||||
world,
|
world,
|
||||||
steps=args.steps,
|
steps=args.steps,
|
||||||
actions=extra_actions,
|
actions=extra_actions,
|
||||||
systems=extra_systems,
|
systems=extra_systems,
|
||||||
input_callbacks=input_callbacks,
|
callbacks=callbacks,
|
||||||
result_callbacks=result_callbacks,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from dataclasses import dataclass
|
||||||
|
else:
|
||||||
|
from pydantic.dataclasses import dataclass as dataclass # noqa
|
|
@ -1,12 +1,8 @@
|
||||||
from typing import TYPE_CHECKING, Callable, Dict, List
|
from typing import Callable, Dict, List
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
from .base import dataclass
|
||||||
from dataclasses import dataclass
|
|
||||||
else:
|
|
||||||
from pydantic.dataclasses import dataclass as dataclass # noqa
|
|
||||||
|
|
||||||
|
|
||||||
Actions = Dict[str, Callable]
|
Actions = Dict[str, Callable]
|
||||||
AttributeValue = bool | int | str
|
AttributeValue = bool | int | str
|
||||||
|
@ -55,3 +51,6 @@ class WorldState:
|
||||||
memory: Dict[str, List[str | Dict[str, str]]]
|
memory: Dict[str, List[str | Dict[str, str]]]
|
||||||
step: int
|
step: int
|
||||||
world: World
|
world: World
|
||||||
|
|
||||||
|
|
||||||
|
WorldEntity = Room | Actor | Item
|
|
@ -0,0 +1,137 @@
|
||||||
|
from json import loads
|
||||||
|
from typing import Callable, Dict, Literal
|
||||||
|
|
||||||
|
from .base import dataclass
|
||||||
|
from .entity import Actor, Item, Room, WorldEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BaseEvent:
|
||||||
|
"""
|
||||||
|
A base event class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
event: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GenerateEvent:
|
||||||
|
"""
|
||||||
|
A new entity has been generated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
event = "generate"
|
||||||
|
name: str
|
||||||
|
entity: WorldEntity | None = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_name(name: str) -> "GenerateEvent":
|
||||||
|
return GenerateEvent(name=name)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_entity(entity: WorldEntity) -> "GenerateEvent":
|
||||||
|
return GenerateEvent(name=entity.name, entity=entity)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ActionEvent:
|
||||||
|
"""
|
||||||
|
An actor has taken an action.
|
||||||
|
"""
|
||||||
|
|
||||||
|
event = "action"
|
||||||
|
action: str
|
||||||
|
parameters: Dict[str, str]
|
||||||
|
|
||||||
|
room: Room
|
||||||
|
actor: Actor
|
||||||
|
item: Item | None = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_json(json: str, room: Room, actor: Actor) -> "ActionEvent":
|
||||||
|
openai_json = loads(json)
|
||||||
|
return ActionEvent(
|
||||||
|
action=openai_json["function"],
|
||||||
|
parameters=openai_json["parameters"],
|
||||||
|
room=room,
|
||||||
|
actor=actor,
|
||||||
|
item=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PromptEvent:
|
||||||
|
"""
|
||||||
|
A prompt for an actor to take an action.
|
||||||
|
"""
|
||||||
|
|
||||||
|
event = "prompt"
|
||||||
|
prompt: str
|
||||||
|
room: Room
|
||||||
|
actor: Actor
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_text(prompt: str, room: Room, actor: Actor) -> "PromptEvent":
|
||||||
|
return PromptEvent(prompt=prompt, room=room, actor=actor)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ReplyEvent:
|
||||||
|
"""
|
||||||
|
An actor has replied with text.
|
||||||
|
|
||||||
|
This is the non-JSON version of an ActionEvent.
|
||||||
|
"""
|
||||||
|
|
||||||
|
event = "text"
|
||||||
|
text: str
|
||||||
|
room: Room
|
||||||
|
actor: Actor
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_text(text: str, room: Room, actor: Actor) -> "ReplyEvent":
|
||||||
|
return ReplyEvent(text=text, room=room, actor=actor)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ResultEvent:
|
||||||
|
"""
|
||||||
|
A result of an action.
|
||||||
|
"""
|
||||||
|
|
||||||
|
event = "result"
|
||||||
|
result: str
|
||||||
|
room: Room
|
||||||
|
actor: Actor
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StatusEvent:
|
||||||
|
"""
|
||||||
|
A status broadcast event with text.
|
||||||
|
"""
|
||||||
|
|
||||||
|
event = "status"
|
||||||
|
text: str
|
||||||
|
room: Room | None = None
|
||||||
|
actor: Actor | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlayerEvent:
|
||||||
|
"""
|
||||||
|
A player joining or leaving the game.
|
||||||
|
"""
|
||||||
|
|
||||||
|
event = "player"
|
||||||
|
status: Literal["join", "leave"]
|
||||||
|
character: str
|
||||||
|
client: str
|
||||||
|
|
||||||
|
|
||||||
|
# event types
|
||||||
|
WorldEvent = ActionEvent | PromptEvent | ReplyEvent | ResultEvent | StatusEvent
|
||||||
|
GameEvent = GenerateEvent | PlayerEvent | WorldEvent
|
||||||
|
|
||||||
|
# callback types
|
||||||
|
EventCallback = Callable[[GameEvent], None]
|
|
@ -0,0 +1,15 @@
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from .base import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WorldPrompt:
|
||||||
|
name: str
|
||||||
|
theme: str
|
||||||
|
flavor: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PromptFile:
|
||||||
|
prompts: List[WorldPrompt]
|
|
@ -8,6 +8,8 @@ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
|
||||||
from packit.agent import Agent
|
from packit.agent import Agent
|
||||||
from packit.utils import could_be_json
|
from packit.utils import could_be_json
|
||||||
|
|
||||||
|
from adventure.models.event import PromptEvent
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -158,13 +160,13 @@ class LocalPlayer(BasePlayer):
|
||||||
class RemotePlayer(BasePlayer):
|
class RemotePlayer(BasePlayer):
|
||||||
fallback_agent: Agent | None
|
fallback_agent: Agent | None
|
||||||
input_queue: Queue[str]
|
input_queue: Queue[str]
|
||||||
send_prompt: Callable[[str, str], bool]
|
send_prompt: Callable[[PromptEvent], bool]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
backstory: str,
|
backstory: str,
|
||||||
send_prompt: Callable[[str, str], bool],
|
send_prompt: Callable[[PromptEvent], bool],
|
||||||
fallback_agent=None,
|
fallback_agent=None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(name, backstory)
|
super().__init__(name, backstory)
|
||||||
|
@ -180,9 +182,11 @@ class RemotePlayer(BasePlayer):
|
||||||
formatted_prompt = prompt.format(**kwargs)
|
formatted_prompt = prompt.format(**kwargs)
|
||||||
self.memory.append(HumanMessage(content=formatted_prompt))
|
self.memory.append(HumanMessage(content=formatted_prompt))
|
||||||
|
|
||||||
|
prompt_event = PromptEvent.from_text(formatted_prompt, None, None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info(f"prompting remote player: {self.name}")
|
logger.info(f"prompting remote player: {self.name}")
|
||||||
if self.send_prompt(self.name, formatted_prompt):
|
if self.send_prompt(prompt_event):
|
||||||
reply = self.input_queue.get(timeout=60)
|
reply = self.input_queue.get(timeout=60)
|
||||||
logger.info(f"got reply from remote player: {reply}")
|
logger.info(f"got reply from remote player: {reply}")
|
||||||
return self.parse_input(reply)
|
return self.parse_input(reply)
|
||||||
|
|
|
@ -1,32 +1,43 @@
|
||||||
prompts:
|
prompts:
|
||||||
- theme: talking animal truckers in the Australian outback
|
- name: outback-animals
|
||||||
|
theme: talking animal truckers in the Australian outback
|
||||||
flavor: create a fun and happy world where rough and tumble talking animals drive trucks and run saloons in the outback
|
flavor: create a fun and happy world where rough and tumble talking animals drive trucks and run saloons in the outback
|
||||||
- theme: grimdark future where humans wage a desperate war for survival against hedgehogs
|
- name: grimdark-hedgehogs
|
||||||
|
theme: grimdark future where humans wage a desperate war for survival against hedgehogs
|
||||||
flavor: create a grim, dark story where a ragtag group of humans is barely surviving in a world overrun by vicious mutant hedgehogs
|
flavor: create a grim, dark story where a ragtag group of humans is barely surviving in a world overrun by vicious mutant hedgehogs
|
||||||
- theme: crowded apartment building in New York City
|
- name: nyc-apartment
|
||||||
|
theme: crowded apartment building in New York City
|
||||||
flavor: |
|
flavor: |
|
||||||
create a crowded apartment building in NYC in the late 90s with a cast of wild and wacky characters.
|
create a crowded apartment building in NYC in the late 90s with a cast of wild and wacky characters.
|
||||||
include colorful characters and make sure they will fully utilize all of the actions available to them
|
include colorful characters and make sure they will fully utilize all of the actions available to them
|
||||||
in this world, exploring and interacting with each other
|
in this world, exploring and interacting with each other
|
||||||
- theme: opening scenes from Jurassic Park
|
- name: jurassic-park
|
||||||
|
theme: opening scenes from Jurassic Park
|
||||||
flavor: |
|
flavor: |
|
||||||
follow the script of the film Jurassic Park exactly. do not deviate from the script in any way.
|
follow the script of the film Jurassic Park exactly. do not deviate from the script in any way.
|
||||||
include accurate characters and make sure they will fully utilize all of the actions available to them in this world
|
include accurate characters and make sure they will fully utilize all of the actions available to them in this world
|
||||||
- theme: opening scenes from Star Wars
|
- name: star-wars
|
||||||
|
theme: opening scenes from Star Wars
|
||||||
flavor: |
|
flavor: |
|
||||||
follow the script of the 1977 film Star Wars exactly. do not deviate from the script in any way.
|
follow the script of the 1977 film Star Wars exactly. do not deviate from the script in any way.
|
||||||
include accurate characters and make sure they will fully utilize all of the actions available to them in this world
|
include accurate characters and make sure they will fully utilize all of the actions available to them in this world
|
||||||
- theme: wealthy cyberpunk utopia with a dark secret
|
- name: cyberpunk-utopia
|
||||||
|
theme: wealthy cyberpunk utopia with a dark secret
|
||||||
flavor: make a strange and dangerous world where technology is pervasive and scarcity is unheard of - for the upper class, at least
|
flavor: make a strange and dangerous world where technology is pervasive and scarcity is unheard of - for the upper class, at least
|
||||||
- theme: post-apocalyptic world where the only survivors are sentient robots
|
- name: post-apocalyptic-robots
|
||||||
|
theme: post-apocalyptic world where the only survivors are sentient robots
|
||||||
flavor: create a world where the only survivors of a nuclear apocalypse are sentient robots, who must now rebuild society from scratch
|
flavor: create a world where the only survivors of a nuclear apocalypse are sentient robots, who must now rebuild society from scratch
|
||||||
- theme: haunted house in the middle of nowhere
|
- name: haunted-house
|
||||||
|
theme: haunted house in the middle of nowhere
|
||||||
flavor: create a spooky and suspenseful world where a group of people are trapped in a haunted house in the middle of nowhere
|
flavor: create a spooky and suspenseful world where a group of people are trapped in a haunted house in the middle of nowhere
|
||||||
- theme: dangerous magical fantasy world
|
- name: magical-kingdom
|
||||||
|
theme: dangerous magical fantasy world
|
||||||
flavor: make a strange and dangerous world where magic winds its way through everything and incredibly powerful beings drink, fight, and wander the halls
|
flavor: make a strange and dangerous world where magic winds its way through everything and incredibly powerful beings drink, fight, and wander the halls
|
||||||
- theme: underwater city of mermaids
|
- name: underwater-mermaids
|
||||||
|
theme: underwater city of mermaids
|
||||||
flavor: create a beautiful and mysterious world where mermaids live in an underwater city, exploring the depths and interacting with each other
|
flavor: create a beautiful and mysterious world where mermaids live in an underwater city, exploring the depths and interacting with each other
|
||||||
- theme: a mysterious town in the Pacific Northwest filled with strange cryptids and private investigators searching for them
|
- name: cryptid-town
|
||||||
|
theme: a mysterious town in the Pacific Northwest filled with strange cryptids and private investigators searching for them
|
||||||
flavor: |
|
flavor: |
|
||||||
make a strange and creepy world where terrifying creatures that you could never imagine in daylight roam the back
|
make a strange and creepy world where terrifying creatures that you could never imagine in daylight roam the back
|
||||||
alleys and hard-bitten private investigators with rough voices search for answers. do not use the word cryptid in any names
|
alleys and hard-bitten private investigators with rough voices search for answers. do not use the word cryptid in any names
|
|
@ -150,8 +150,26 @@ def generate_images(
|
||||||
"inputs": {"batch_size": count, "height": height, "width": width},
|
"inputs": {"batch_size": count, "height": height, "width": width},
|
||||||
},
|
},
|
||||||
"6": {
|
"6": {
|
||||||
"class_type": "CLIPTextEncode",
|
"class_type": "smZ CLIPTextEncode",
|
||||||
"inputs": {"text": prompt, "clip": ["4", 1]},
|
"inputs": {
|
||||||
|
"text": prompt,
|
||||||
|
"parser": "compel",
|
||||||
|
"mean_normalization": True,
|
||||||
|
"multi_conditioning": True,
|
||||||
|
"use_old_emphasis_implementation": False,
|
||||||
|
"with_SDXL": False,
|
||||||
|
"ascore": 6,
|
||||||
|
"width": width,
|
||||||
|
"height": height,
|
||||||
|
"crop_w": 0,
|
||||||
|
"crop_h": 0,
|
||||||
|
"target_width": width,
|
||||||
|
"target_height": height,
|
||||||
|
"text_g": "",
|
||||||
|
"text_l": "",
|
||||||
|
"smZ_steps": 1,
|
||||||
|
"clip": ["4", 1],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"7": {"class_type": "CLIPTextEncode", "inputs": {"text": "", "clip": ["4", 1]}},
|
"7": {"class_type": "CLIPTextEncode", "inputs": {"text": "", "clip": ["4", 1]}},
|
||||||
"8": {
|
"8": {
|
||||||
|
|
|
@ -8,8 +8,17 @@ from uuid import uuid4
|
||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
from adventure.context import get_actor_agent_for_name, set_actor_agent_for_name
|
from adventure.context import get_actor_agent_for_name, set_actor_agent
|
||||||
from adventure.models import Actor, Room, World
|
from adventure.models.entity import Actor, Room, World
|
||||||
|
from adventure.models.event import (
|
||||||
|
ActionEvent,
|
||||||
|
GameEvent,
|
||||||
|
GenerateEvent,
|
||||||
|
PromptEvent,
|
||||||
|
ReplyEvent,
|
||||||
|
ResultEvent,
|
||||||
|
StatusEvent,
|
||||||
|
)
|
||||||
from adventure.player import (
|
from adventure.player import (
|
||||||
RemotePlayer,
|
RemotePlayer,
|
||||||
get_player,
|
get_player,
|
||||||
|
@ -24,7 +33,7 @@ logger = getLogger(__name__)
|
||||||
|
|
||||||
connected = set()
|
connected = set()
|
||||||
recent_events = deque(maxlen=100)
|
recent_events = deque(maxlen=100)
|
||||||
recent_world = None
|
last_snapshot = None
|
||||||
|
|
||||||
|
|
||||||
async def handler(websocket):
|
async def handler(websocket):
|
||||||
|
@ -45,10 +54,10 @@ async def handler(websocket):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def sync_turn(character: str, prompt: str) -> bool:
|
def sync_turn(event: PromptEvent) -> bool:
|
||||||
player = get_player(id)
|
player = get_player(id)
|
||||||
if player and player.name == character:
|
if player and player.name == event.actor.name:
|
||||||
asyncio.run(next_turn(character, prompt))
|
asyncio.run(next_turn(event.actor.name, event.prompt))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -56,8 +65,8 @@ async def handler(websocket):
|
||||||
try:
|
try:
|
||||||
await websocket.send(dumps({"type": "id", "id": id}))
|
await websocket.send(dumps({"type": "id", "id": id}))
|
||||||
|
|
||||||
if recent_world:
|
if last_snapshot:
|
||||||
await websocket.send(recent_world)
|
await websocket.send(last_snapshot)
|
||||||
|
|
||||||
for message in recent_events:
|
for message in recent_events:
|
||||||
await websocket.send(message)
|
await websocket.send(message)
|
||||||
|
@ -74,10 +83,14 @@ async def handler(websocket):
|
||||||
data = loads(message)
|
data = loads(message)
|
||||||
message_type = data.get("type", None)
|
message_type = data.get("type", None)
|
||||||
if message_type == "player":
|
if message_type == "player":
|
||||||
|
character_name = data["become"]
|
||||||
|
if has_player(character_name):
|
||||||
|
logger.error(f"Character {character_name} is already in use")
|
||||||
|
continue
|
||||||
|
|
||||||
# TODO: should this always remove?
|
# TODO: should this always remove?
|
||||||
remove_player(id)
|
remove_player(id)
|
||||||
|
|
||||||
character_name = data["become"]
|
|
||||||
actor, llm_agent = get_actor_agent_for_name(character_name)
|
actor, llm_agent = get_actor_agent_for_name(character_name)
|
||||||
if not actor:
|
if not actor:
|
||||||
logger.error(f"Failed to find actor {character_name}")
|
logger.error(f"Failed to find actor {character_name}")
|
||||||
|
@ -90,10 +103,6 @@ async def handler(websocket):
|
||||||
)
|
)
|
||||||
llm_agent = llm_agent.fallback_agent
|
llm_agent = llm_agent.fallback_agent
|
||||||
|
|
||||||
if has_player(character_name):
|
|
||||||
logger.error(f"Character {character_name} is already in use")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# player_name = data["player"]
|
# player_name = data["player"]
|
||||||
player = RemotePlayer(
|
player = RemotePlayer(
|
||||||
actor.name, actor.backstory, sync_turn, fallback_agent=llm_agent
|
actor.name, actor.backstory, sync_turn, fallback_agent=llm_agent
|
||||||
|
@ -102,7 +111,7 @@ async def handler(websocket):
|
||||||
logger.info(f"Client {id} is now character {character_name}")
|
logger.info(f"Client {id} is now character {character_name}")
|
||||||
|
|
||||||
# swap out the LLM agent
|
# swap out the LLM agent
|
||||||
set_actor_agent_for_name(actor.name, actor, player)
|
set_actor_agent(actor.name, actor, player)
|
||||||
|
|
||||||
# notify all clients that this character is now active
|
# notify all clients that this character is now active
|
||||||
player_event(character_name, id, "join")
|
player_event(character_name, id, "join")
|
||||||
|
@ -134,7 +143,7 @@ async def handler(websocket):
|
||||||
actor, _ = get_actor_agent_for_name(player.name)
|
actor, _ = get_actor_agent_for_name(player.name)
|
||||||
if actor and player.fallback_agent:
|
if actor and player.fallback_agent:
|
||||||
logger.info("Restoring LLM agent for %s", player.name)
|
logger.info("Restoring LLM agent for %s", player.name)
|
||||||
set_actor_agent_for_name(player.name, actor, player.fallback_agent)
|
set_actor_agent(player.name, actor, player.fallback_agent)
|
||||||
|
|
||||||
logger.info("Client disconnected: %s", id)
|
logger.info("Client disconnected: %s", id)
|
||||||
|
|
||||||
|
@ -166,9 +175,11 @@ def launch_server():
|
||||||
def run_sockets():
|
def run_sockets():
|
||||||
asyncio.run(server_main())
|
asyncio.run(server_main())
|
||||||
|
|
||||||
socket_thread = Thread(target=run_sockets)
|
socket_thread = Thread(target=run_sockets, daemon=True)
|
||||||
socket_thread.start()
|
socket_thread.start()
|
||||||
|
|
||||||
|
return [socket_thread]
|
||||||
|
|
||||||
|
|
||||||
async def server_main():
|
async def server_main():
|
||||||
async with websockets.serve(handler, "", 8001):
|
async with websockets.serve(handler, "", 8001):
|
||||||
|
@ -177,12 +188,12 @@ async def server_main():
|
||||||
|
|
||||||
|
|
||||||
def server_system(world: World, step: int):
|
def server_system(world: World, step: int):
|
||||||
global recent_world
|
global last_snapshot
|
||||||
json_state = {
|
json_state = {
|
||||||
**snapshot_world(world, step),
|
**snapshot_world(world, step),
|
||||||
"type": "world",
|
"type": "world",
|
||||||
}
|
}
|
||||||
recent_world = send_and_append(json_state)
|
last_snapshot = send_and_append(json_state)
|
||||||
|
|
||||||
|
|
||||||
def server_result(room: Room, actor: Actor, action: str):
|
def server_result(room: Room, actor: Actor, action: str):
|
||||||
|
@ -205,14 +216,29 @@ def server_action(room: Room, actor: Actor, message: str):
|
||||||
send_and_append(json_input)
|
send_and_append(json_input)
|
||||||
|
|
||||||
|
|
||||||
def server_event(message: str):
|
def server_generate(event: GenerateEvent):
|
||||||
json_broadcast = {
|
json_broadcast = {
|
||||||
"message": message,
|
"name": event.name,
|
||||||
"type": "event",
|
"type": "generate",
|
||||||
}
|
}
|
||||||
send_and_append(json_broadcast)
|
send_and_append(json_broadcast)
|
||||||
|
|
||||||
|
|
||||||
|
def server_event(event: GameEvent):
|
||||||
|
if isinstance(event, GenerateEvent):
|
||||||
|
return server_generate(event)
|
||||||
|
elif isinstance(event, ActionEvent):
|
||||||
|
return server_action(event.room, event.actor, event.action)
|
||||||
|
elif isinstance(event, ReplyEvent):
|
||||||
|
return server_action(event.room, event.actor, event.text)
|
||||||
|
elif isinstance(event, ResultEvent):
|
||||||
|
return server_result(event.room, event.actor, event.result)
|
||||||
|
elif isinstance(event, StatusEvent):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
logger.warning("Unknown event type: %s", event)
|
||||||
|
|
||||||
|
|
||||||
def player_event(character: str, id: str, event: Literal["join", "leave"]):
|
def player_event(character: str, id: str, event: Literal["join", "leave"]):
|
||||||
json_broadcast = {
|
json_broadcast = {
|
||||||
"type": "player",
|
"type": "player",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from adventure.models import Attributes, Room
|
from adventure.models.entity import Attributes, Room
|
||||||
|
|
||||||
|
|
||||||
def hot_room(room: Room, attributes: Attributes):
|
def hot_room(room: Room, attributes: Attributes):
|
||||||
|
|
|
@ -0,0 +1,177 @@
|
||||||
|
from logging import getLogger
|
||||||
|
from typing import Callable, Sequence, Tuple
|
||||||
|
|
||||||
|
from packit.loops import 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 import (
|
||||||
|
action_ask,
|
||||||
|
action_give,
|
||||||
|
action_look,
|
||||||
|
action_move,
|
||||||
|
action_take,
|
||||||
|
action_tell,
|
||||||
|
)
|
||||||
|
from adventure.context import (
|
||||||
|
get_actor_agent_for_name,
|
||||||
|
get_actor_for_agent,
|
||||||
|
get_current_step,
|
||||||
|
get_current_world,
|
||||||
|
set_current_actor,
|
||||||
|
set_current_broadcast,
|
||||||
|
set_current_room,
|
||||||
|
set_current_step,
|
||||||
|
set_current_world,
|
||||||
|
)
|
||||||
|
from adventure.models.entity import Attributes, World
|
||||||
|
from adventure.models.event import (
|
||||||
|
ActionEvent,
|
||||||
|
EventCallback,
|
||||||
|
ReplyEvent,
|
||||||
|
ResultEvent,
|
||||||
|
StatusEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
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 simulate_world(
|
||||||
|
world: World,
|
||||||
|
steps: int = 10,
|
||||||
|
actions: Sequence[Callable[..., str]] = [],
|
||||||
|
systems: Sequence[
|
||||||
|
Tuple[Callable[[World, int], None], Callable[[Attributes], str] | None]
|
||||||
|
] = [],
|
||||||
|
callbacks: Sequence[EventCallback] = [],
|
||||||
|
):
|
||||||
|
logger.info("Simulating the world")
|
||||||
|
set_current_world(world)
|
||||||
|
|
||||||
|
# set up a broadcast callback
|
||||||
|
def broadcast_callback(message):
|
||||||
|
logger.info(message)
|
||||||
|
event = StatusEvent(text=message)
|
||||||
|
for callback in callbacks:
|
||||||
|
callback(event)
|
||||||
|
|
||||||
|
set_current_broadcast(broadcast_callback)
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
# simulate each actor
|
||||||
|
for i in range(steps):
|
||||||
|
current_step = get_current_step()
|
||||||
|
logger.info(f"Simulating 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 = next((room for room in world.rooms if actor in room.actors), None)
|
||||||
|
if not room:
|
||||||
|
logger.error(f"Actor {actor_name} is not in a room")
|
||||||
|
continue
|
||||||
|
|
||||||
|
room_actors = [actor.name for actor in room.actors]
|
||||||
|
room_items = [item.name for item in room.items]
|
||||||
|
room_directions = list(room.portals.keys())
|
||||||
|
|
||||||
|
actor_attributes = " ".join(
|
||||||
|
system_format(actor.attributes)
|
||||||
|
for _, system_format in systems
|
||||||
|
if system_format
|
||||||
|
)
|
||||||
|
actor_items = [item.name for item in actor.items]
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
for callback in callbacks:
|
||||||
|
logger.info(
|
||||||
|
f"calling input callback for {actor_name}: {callback.__name__}"
|
||||||
|
)
|
||||||
|
callback(event)
|
||||||
|
|
||||||
|
return world_result_parser(value, agent, **kwargs)
|
||||||
|
|
||||||
|
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": room.description,
|
||||||
|
"visible_actors": room_actors,
|
||||||
|
"visible_items": room_items,
|
||||||
|
},
|
||||||
|
result_parser=result_parser,
|
||||||
|
toolbox=action_tools,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(f"{actor.name} step result: {result}")
|
||||||
|
agent.memory.append(result)
|
||||||
|
|
||||||
|
result_event = ResultEvent(result=result, room=room, actor=actor)
|
||||||
|
for callback in callbacks:
|
||||||
|
callback(result_event)
|
||||||
|
|
||||||
|
for system_update, _ in systems:
|
||||||
|
system_update(world, current_step)
|
||||||
|
|
||||||
|
set_current_step(current_step + 1)
|
|
@ -7,8 +7,8 @@ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, System
|
||||||
from packit.agent import Agent, agent_easy_connect
|
from packit.agent import Agent, agent_easy_connect
|
||||||
from pydantic import RootModel
|
from pydantic import RootModel
|
||||||
|
|
||||||
from adventure.context import get_all_actor_agents, set_actor_agent_for_name
|
from adventure.context import get_all_actor_agents, set_actor_agent
|
||||||
from adventure.models import World
|
from adventure.models.entity import World
|
||||||
from adventure.player import LocalPlayer
|
from adventure.player import LocalPlayer
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ def create_agents(
|
||||||
else:
|
else:
|
||||||
agent = Agent(actor.name, actor.backstory, {}, llm)
|
agent = Agent(actor.name, actor.backstory, {}, llm)
|
||||||
agent.memory = restore_memory(memory.get(actor.name, []))
|
agent.memory = restore_memory(memory.get(actor.name, []))
|
||||||
set_actor_agent_for_name(actor.name, actor, agent)
|
set_actor_agent(actor.name, actor, agent)
|
||||||
|
|
||||||
|
|
||||||
def graph_world(world: World, step: int):
|
def graph_world(world: World, step: int):
|
||||||
|
|
Loading…
Reference in New Issue