From 8706badec5473092bebd68f80d7e02809dd790db Mon Sep 17 00:00:00 2001 From: Sean Sube Date: Sat, 4 May 2024 15:35:42 -0500 Subject: [PATCH] hack in a message broadcast system --- adventure/actions.py | 28 +++++----- adventure/context.py | 25 +++++++-- adventure/generate.py | 46 ++++++++++++----- adventure/main.py | 116 +++++++++++++++++++++++++++++------------- adventure/server.py | 76 +++++++++++++++++---------- 5 files changed, 198 insertions(+), 93 deletions(-) diff --git a/adventure/actions.py b/adventure/actions.py index bda2c1a..50f28ed 100644 --- a/adventure/actions.py +++ b/adventure/actions.py @@ -3,7 +3,7 @@ from logging import getLogger from packit.utils import could_be_json -from adventure.context import get_actor_agent_for_name, get_current_context +from adventure.context import broadcast, get_actor_agent_for_name, get_current_context logger = getLogger(__name__) @@ -16,29 +16,29 @@ def action_look(target: str) -> str: target: The name of the target to look at. """ _, action_room, action_actor = get_current_context() - logger.info(f"{action_actor.name} looks at {target}") + broadcast(f"{action_actor.name} looks at {target}") if target == action_room.name: - logger.info(f"{action_actor.name} saw the {action_room.name} room") + broadcast(f"{action_actor.name} saw the {action_room.name} room") return action_room.description for actor in action_room.actors: if actor.name == target: - logger.info( + broadcast( f"{action_actor.name} saw the {actor.name} actor in the {action_room.name} room" ) return actor.description for item in action_room.items: if item.name == target: - logger.info( + broadcast( f"{action_actor.name} saw the {item.name} item in the {action_room.name} room" ) return item.description for item in action_actor.items: if item.name == target: - logger.info( + broadcast( f"{action_actor.name} saw the {item.name} item in their inventory" ) return item.description @@ -65,7 +65,7 @@ def action_move(direction: str) -> str: if not destination_room: return f"The {destination_name} room does not exist." - logger.info(f"{action_actor.name} moves {direction} to {destination_name}") + broadcast(f"{action_actor.name} moves {direction} to {destination_name}") action_room.actors.remove(action_actor) destination_room.actors.append(action_actor) @@ -83,7 +83,7 @@ def action_take(item_name: str) -> str: item = next((item for item in action_room.items if item.name == item_name), None) if item: - logger.info(f"{action_actor.name} takes the {item_name} item") + broadcast(f"{action_actor.name} takes the {item_name} item") action_room.items.remove(item) action_actor.items.append(item) return "You take the {item_name} item and put it in your inventory." @@ -118,7 +118,7 @@ def action_ask(character: str, question: str) -> str: if not question_agent: return f"The {character} character does not exist." - logger.info(f"{action_actor.name} asks {character}: {question}") + broadcast(f"{action_actor.name} asks {character}: {question}") answer = question_agent( f"{action_actor.name} asks you: {question}. Reply with your response to them. " f"Do not include the question or any JSON. Only include your answer for {action_actor.name}." @@ -128,7 +128,7 @@ def action_ask(character: str, question: str) -> str: answer = loads(answer).get("parameters", {}).get("message", "") if len(answer.strip()) > 0: - logger.info(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} does not respond." @@ -161,7 +161,7 @@ def action_tell(character: str, message: str) -> str: if not question_agent: return f"The {character} character does not exist." - logger.info(f"{action_actor.name} tells {character}: {message}") + broadcast(f"{action_actor.name} tells {character}: {message}") answer = question_agent( f"{action_actor.name} tells you: {message}. Reply with your response to them. " f"Do not include the message or any JSON. Only include your reply to {action_actor.name}." @@ -171,7 +171,7 @@ def action_tell(character: str, message: str) -> str: answer = loads(answer).get("parameters", {}).get("message", "") if len(answer.strip()) > 0: - logger.info(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} does not respond." @@ -197,7 +197,7 @@ def action_give(character: str, item_name: str) -> str: if not item: return f"You do not have the {item_name} item in your inventory." - logger.info(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) destination_actor.items.append(item) @@ -218,7 +218,7 @@ def action_drop(item_name: str) -> str: if not item: return f"You do not have the {item_name} item in your inventory." - logger.info(f"{action_actor.name} drops the {item_name} item") + broadcast(f"{action_actor.name} drops the {item_name} item") action_actor.items.remove(item) action_room.items.append(item) diff --git a/adventure/context.py b/adventure/context.py index e7f7116..50be354 100644 --- a/adventure/context.py +++ b/adventure/context.py @@ -1,12 +1,13 @@ -from typing import Dict, Tuple +from typing import Callable, Dict, Tuple from packit.agent import Agent -from adventure.models import Actor +from adventure.models import Actor, Room, World -current_world = None -current_room = None -current_actor = None +current_broadcast: Callable[[str], None] | None = None +current_world: World | None = None +current_room: Room | None = None +current_actor: Actor | None = None current_step = 0 @@ -41,6 +42,20 @@ def get_current_actor(): return current_actor +def get_current_broadcast(): + return current_broadcast + + +def broadcast(message): + if current_broadcast: + current_broadcast(message) + + +def set_current_broadcast(broadcast): + global current_broadcast + current_broadcast = broadcast + + def set_current_world(world): global current_world current_world = world diff --git a/adventure/generate.py b/adventure/generate.py index 9dbfe3a..c9c01a8 100644 --- a/adventure/generate.py +++ b/adventure/generate.py @@ -1,6 +1,6 @@ from logging import getLogger from random import choice, randint -from typing import List +from typing import Callable, List from packit.agent import Agent @@ -9,7 +9,9 @@ from adventure.models import Actor, Item, Room, World logger = getLogger(__name__) -def generate_room(agent: Agent, world_theme: str, existing_rooms: List[str]) -> Room: +def generate_room( + agent: Agent, world_theme: str, existing_rooms: List[str], callback +) -> Room: name = agent( "Generate one room, area, or location that would make sense in the world of {world_theme}. " "Only respond with the room name, do not include the description or any other text. " @@ -17,7 +19,7 @@ def generate_room(agent: Agent, world_theme: str, existing_rooms: List[str]) -> world_theme=world_theme, existing_rooms=existing_rooms, ) - logger.info(f"Generating room: {name}") + callback(f"Generating room: {name}") desc = agent( "Generate a detailed description of the {name} area. What does it look like? " "What does it smell like? What can be seen or heard?", @@ -36,9 +38,10 @@ def generate_room(agent: Agent, world_theme: str, existing_rooms: List[str]) -> def generate_item( agent: Agent, world_theme: str, + existing_items: List[str], + callback, dest_room: str | None = None, dest_actor: str | None = None, - existing_items: List[str] = [], ) -> Item: if dest_actor: dest_note = "The item will be held by the {dest_actor} character" @@ -56,7 +59,7 @@ def generate_item( existing_items=existing_items, world_theme=world_theme, ) - logger.info(f"Generating item: {name}") + callback(f"Generating item: {name}") desc = agent( "Generate a detailed description of the {name} item. What does it look like? What is it made of? What does it do?", name=name, @@ -68,7 +71,7 @@ def generate_item( def generate_actor( - agent: Agent, world_theme: str, dest_room: str, existing_actors: List[str] = [] + agent: Agent, world_theme: str, dest_room: str, existing_actors: List[str], callback ) -> Actor: name = agent( "Generate one person or creature that would make sense in the world of {world_theme}. The character will be placed in the {dest_room} room. " @@ -79,7 +82,7 @@ def generate_actor( existing_actors=existing_actors, world_theme=world_theme, ) - logger.info(f"Generating actor: {name}") + callback(f"Generating actor: {name}") description = agent( "Generate a detailed description of the {name} character. What do they look like? What are they wearing? " "What are they doing? Describe their appearance from the perspective of an outside observer." @@ -106,9 +109,10 @@ def generate_world( theme: str, room_count: int | None = None, max_rooms: int = 5, + callback: Callable[[str], None] = lambda x: None, ) -> World: room_count = room_count or randint(3, max_rooms) - logger.info(f"Generating a {theme} with {room_count} rooms") + callback(f"Generating a {theme} with {room_count} rooms") existing_actors: List[str] = [] existing_items: List[str] = [] @@ -117,30 +121,48 @@ def generate_world( rooms = [] for i in range(room_count): existing_rooms = [room.name for room in rooms] - room = generate_room(agent, theme, existing_rooms) + room = generate_room(agent, theme, existing_rooms, callback=callback) rooms.append(room) item_count = randint(0, 3) + callback(f"Generating {item_count} items for room {room.name}") + for j in range(item_count): item = generate_item( - agent, theme, dest_room=room.name, existing_items=existing_items + agent, + theme, + dest_room=room.name, + existing_items=existing_items, + callback=callback, ) room.items.append(item) existing_items.append(item.name) actor_count = randint(0, 3) + callback(f"Generating {actor_count} actors for room {room.name}") + for j in range(actor_count): actor = generate_actor( - agent, theme, dest_room=room.name, existing_actors=existing_actors + agent, + theme, + dest_room=room.name, + existing_actors=existing_actors, + callback=callback, ) room.actors.append(actor) existing_actors.append(actor.name) # generate the actor's inventory item_count = randint(0, 3) + callback(f"Generating {item_count} items for actor {actor.name}") + for k in range(item_count): item = generate_item( - agent, theme, dest_room=room.name, existing_items=existing_items + agent, + theme, + dest_room=room.name, + existing_items=existing_items, + callback=callback, ) actor.items.append(item) existing_items.append(item.name) diff --git a/adventure/main.py b/adventure/main.py index 70effd6..95e3c9b 100644 --- a/adventure/main.py +++ b/adventure/main.py @@ -1,5 +1,6 @@ from importlib import import_module from json import load +from logging.config import dictConfig from os import environ, path from typing import Callable, Dict, Sequence, Tuple @@ -10,29 +11,46 @@ from packit.results import multi_function_or_str_result from packit.toolbox import Toolbox from packit.utils import logger_with_colors -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_world, - get_step, - set_current_actor, - set_current_room, - set_current_world, - set_step, -) -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 +from adventure.context import set_current_broadcast -logger = logger_with_colors(__name__) +# Configure logging +LOG_PATH = "logging.json" +# LOG_PATH = "dev-logging.json" +try: + if path.exists(LOG_PATH): + with open(LOG_PATH, "r") as f: + config_logging = load(f) + dictConfig(config_logging) + else: + print("logging config not found") + +except Exception as 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_world, + get_step, + set_current_actor, + set_current_room, + set_current_world, + set_step, + ) + 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="INFO") load_dotenv(environ.get("ADVENTURE_ENV", ".env"), override=True) @@ -65,12 +83,21 @@ def simulate_world( systems: Sequence[ Tuple[Callable[[World, int], None], Callable[[Dict[str, str]], 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( [ @@ -222,6 +249,25 @@ def main(): if args.player: players.append(args.player) + # set up callbacks + event_callbacks = [] + input_callbacks = [] + result_callbacks = [] + + 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 = {} if path.exists(world_state_file): logger.info(f"Loading world state from {world_state_file}") @@ -246,12 +292,23 @@ def main(): {}, llm, ) + + world = None + + def broadcast_callback(message): + logger.info(message) + for callback in event_callbacks: + callback(message) + if args.server and world: + server_system(world, 0) + world = generate_world( agent, args.world, args.theme, room_count=args.rooms, max_rooms=args.max_rooms, + callback=broadcast_callback, ) save_world(world, world_file) @@ -281,22 +338,9 @@ def main(): ) extra_systems.append(module_systems) - # make sure the server system is last - input_callbacks = [] - result_callbacks = [] - + # make sure the server system runs after any updates if args.server: - from adventure.server import ( - launch_server, - server_input, - server_result, - server_system, - ) - - launch_server() extra_systems.append((server_system, None)) - input_callbacks.append(server_input) - result_callbacks.append(server_result) # start the sim logger.debug("Simulating world: %s", world) diff --git a/adventure/server.py b/adventure/server.py index 6cfcf99..98c021f 100644 --- a/adventure/server.py +++ b/adventure/server.py @@ -1,60 +1,75 @@ import asyncio +from collections import deque from json import dumps from logging import getLogger from threading import Thread import websockets -from flask import Flask, send_from_directory from adventure.models import Actor, Room, World from adventure.state import snapshot_world, world_json logger = getLogger(__name__) -app = Flask(__name__) connected = set() - - -@app.route("/") -def send_report(page: str): - print(f"Sending {page}") - return send_from_directory( - "/home/ssube/code/github/ssube/llm-adventure/web-ui", page - ) +recent_events = deque(maxlen=10) +recent_world = None async def handler(websocket): + logger.info("Client connected") connected.add(websocket) + + try: + if recent_world: + await websocket.send(recent_world) + + for message in recent_events: + await websocket.send(message) + except Exception: + logger.exception("Failed to send recent messages to new client") + while True: try: - # await websocket.wait_closed() message = await websocket.recv() print(message) except websockets.ConnectionClosedOK: break connected.remove(websocket) + logger.info("Client disconnected") socket_thread = None static_thread = None +def server_json(obj): + if isinstance(obj, Actor): + return obj.name + + if isinstance(obj, Room): + return obj.name + + return world_json(obj) + + +def send_and_append(message): + json_message = dumps(message, default=server_json) + recent_events.append(json_message) + websockets.broadcast(connected, json_message) + return json_message + + def launch_server(): global socket_thread, static_thread def run_sockets(): asyncio.run(server_main()) - def run_static(): - app.run(port=8000) - socket_thread = Thread(target=run_sockets) socket_thread.start() - static_thread = Thread(target=run_static) - static_thread.start() - async def server_main(): async with websockets.serve(handler, "", 8001): @@ -63,28 +78,37 @@ async def server_main(): def server_system(world: World, step: int): + global recent_world json_state = { **snapshot_world(world, step), "type": "world", } - websockets.broadcast(connected, dumps(json_state, default=world_json)) + recent_world = send_and_append(json_state) def server_result(room: Room, actor: Actor, action: str): json_action = { - "actor": actor.name, + "actor": actor, "result": action, - "room": room.name, + "room": room, "type": "result", } - websockets.broadcast(connected, dumps(json_action)) + send_and_append(json_action) -def server_input(room: Room, actor: Actor, message: str): +def server_action(room: Room, actor: Actor, message: str): json_input = { - "actor": actor.name, + "actor": actor, "input": message, - "room": room.name, - "type": "input", + "room": room, + "type": "action", } - websockets.broadcast(connected, dumps(json_input)) + send_and_append(json_input) + + +def server_event(message: str): + json_broadcast = { + "message": message, + "type": "event", + } + send_and_append(json_broadcast)