1
0
Fork 0

hack in a message broadcast system

This commit is contained in:
Sean Sube 2024-05-04 15:35:42 -05:00
parent ebf4ccf1c4
commit 8706badec5
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
5 changed files with 198 additions and 93 deletions

View File

@ -3,7 +3,7 @@ from logging import getLogger
from packit.utils import could_be_json 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__) logger = getLogger(__name__)
@ -16,29 +16,29 @@ def action_look(target: str) -> str:
target: The name of the target to look at. target: The name of the target to look at.
""" """
_, action_room, action_actor = get_current_context() _, 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: 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 return action_room.description
for actor in action_room.actors: for actor in action_room.actors:
if actor.name == target: if actor.name == target:
logger.info( broadcast(
f"{action_actor.name} saw the {actor.name} actor in the {action_room.name} room" f"{action_actor.name} saw the {actor.name} actor in the {action_room.name} room"
) )
return actor.description return actor.description
for item in action_room.items: for item in action_room.items:
if item.name == target: if item.name == target:
logger.info( broadcast(
f"{action_actor.name} saw the {item.name} item in the {action_room.name} room" f"{action_actor.name} saw the {item.name} item in the {action_room.name} room"
) )
return item.description return item.description
for item in action_actor.items: for item in action_actor.items:
if item.name == target: if item.name == target:
logger.info( broadcast(
f"{action_actor.name} saw the {item.name} item in their inventory" f"{action_actor.name} saw the {item.name} item in their inventory"
) )
return item.description return item.description
@ -65,7 +65,7 @@ def action_move(direction: str) -> str:
if not destination_room: if not destination_room:
return f"The {destination_name} room does not exist." 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) action_room.actors.remove(action_actor)
destination_room.actors.append(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) item = next((item for item in action_room.items if item.name == item_name), None)
if item: 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_room.items.remove(item)
action_actor.items.append(item) action_actor.items.append(item)
return "You take the {item_name} item and put it in your inventory." 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: if not question_agent:
return f"The {character} character does not exist." 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( answer = question_agent(
f"{action_actor.name} asks you: {question}. Reply with your response to them. " 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}." 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", "") answer = loads(answer).get("parameters", {}).get("message", "")
if len(answer.strip()) > 0: 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} responds: {answer}"
return f"{character} does not respond." return f"{character} does not respond."
@ -161,7 +161,7 @@ def action_tell(character: str, message: str) -> str:
if not question_agent: if not question_agent:
return f"The {character} character does not exist." 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( answer = question_agent(
f"{action_actor.name} tells you: {message}. Reply with your response to them. " 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}." 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", "") answer = loads(answer).get("parameters", {}).get("message", "")
if len(answer.strip()) > 0: 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} responds: {answer}"
return f"{character} does not respond." return f"{character} does not respond."
@ -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."
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) action_actor.items.remove(item)
destination_actor.items.append(item) destination_actor.items.append(item)
@ -218,7 +218,7 @@ def action_drop(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."
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_actor.items.remove(item)
action_room.items.append(item) action_room.items.append(item)

View File

@ -1,12 +1,13 @@
from typing import Dict, Tuple from typing import Callable, Dict, Tuple
from packit.agent import Agent from packit.agent import Agent
from adventure.models import Actor from adventure.models import Actor, Room, World
current_world = None current_broadcast: Callable[[str], None] | None = None
current_room = None current_world: World | None = None
current_actor = None current_room: Room | None = None
current_actor: Actor | None = None
current_step = 0 current_step = 0
@ -41,6 +42,20 @@ def get_current_actor():
return 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): def set_current_world(world):
global current_world global current_world
current_world = world current_world = world

View File

@ -1,6 +1,6 @@
from logging import getLogger from logging import getLogger
from random import choice, randint from random import choice, randint
from typing import List from typing import Callable, List
from packit.agent import Agent from packit.agent import Agent
@ -9,7 +9,9 @@ from adventure.models import Actor, Item, Room, World
logger = getLogger(__name__) 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( name = agent(
"Generate one room, area, or location that would make sense in the world of {world_theme}. " "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. " "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, world_theme=world_theme,
existing_rooms=existing_rooms, existing_rooms=existing_rooms,
) )
logger.info(f"Generating room: {name}") callback(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? "
"What does it smell like? What can be seen or heard?", "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( def generate_item(
agent: Agent, agent: Agent,
world_theme: str, world_theme: str,
existing_items: List[str],
callback,
dest_room: str | None = None, dest_room: str | None = None,
dest_actor: str | None = None, dest_actor: str | None = None,
existing_items: List[str] = [],
) -> Item: ) -> Item:
if dest_actor: if dest_actor:
dest_note = "The item will be held by the {dest_actor} character" dest_note = "The item will be held by the {dest_actor} character"
@ -56,7 +59,7 @@ def generate_item(
existing_items=existing_items, existing_items=existing_items,
world_theme=world_theme, world_theme=world_theme,
) )
logger.info(f"Generating item: {name}") callback(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?",
name=name, name=name,
@ -68,7 +71,7 @@ def generate_item(
def generate_actor( 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: ) -> Actor:
name = agent( 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. " "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, existing_actors=existing_actors,
world_theme=world_theme, world_theme=world_theme,
) )
logger.info(f"Generating actor: {name}") callback(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? "
"What are they doing? Describe their appearance from the perspective of an outside observer." "What are they doing? Describe their appearance from the perspective of an outside observer."
@ -106,9 +109,10 @@ 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] = lambda x: None,
) -> World: ) -> World:
room_count = room_count or randint(3, max_rooms) 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_actors: List[str] = []
existing_items: List[str] = [] existing_items: List[str] = []
@ -117,30 +121,48 @@ def generate_world(
rooms = [] rooms = []
for i in range(room_count): for i in range(room_count):
existing_rooms = [room.name for room in rooms] 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) rooms.append(room)
item_count = randint(0, 3) item_count = randint(0, 3)
callback(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(
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) room.items.append(item)
existing_items.append(item.name) existing_items.append(item.name)
actor_count = randint(0, 3) actor_count = randint(0, 3)
callback(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(
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) room.actors.append(actor)
existing_actors.append(actor.name) existing_actors.append(actor.name)
# generate the actor's inventory # generate the actor's inventory
item_count = randint(0, 3) item_count = randint(0, 3)
callback(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(
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) actor.items.append(item)
existing_items.append(item.name) existing_items.append(item.name)

View File

@ -1,5 +1,6 @@
from importlib import import_module from importlib import import_module
from json import load from json import load
from logging.config import dictConfig
from os import environ, path from os import environ, path
from typing import Callable, Dict, Sequence, Tuple 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.toolbox import Toolbox
from packit.utils import logger_with_colors from packit.utils import logger_with_colors
from adventure.actions import ( from adventure.context import set_current_broadcast
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__) # 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) load_dotenv(environ.get("ADVENTURE_ENV", ".env"), override=True)
@ -65,12 +83,21 @@ def simulate_world(
systems: Sequence[ systems: Sequence[
Tuple[Callable[[World, int], None], Callable[[Dict[str, str]], str] | None] 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]] = [], input_callbacks: Sequence[Callable[[Room, Actor, str], None]] = [],
result_callbacks: Sequence[Callable[[Room, Actor, str], None]] = [], result_callbacks: Sequence[Callable[[Room, Actor, str], None]] = [],
): ):
logger.info("Simulating the world") logger.info("Simulating the world")
set_current_world(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 # build a toolbox for the actions
action_tools = Toolbox( action_tools = Toolbox(
[ [
@ -222,6 +249,25 @@ def main():
if args.player: if args.player:
players.append(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 = {} 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}")
@ -246,12 +292,23 @@ def main():
{}, {},
llm, 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( world = generate_world(
agent, agent,
args.world, args.world,
args.theme, args.theme,
room_count=args.rooms, room_count=args.rooms,
max_rooms=args.max_rooms, max_rooms=args.max_rooms,
callback=broadcast_callback,
) )
save_world(world, world_file) save_world(world, world_file)
@ -281,22 +338,9 @@ def main():
) )
extra_systems.append(module_systems) extra_systems.append(module_systems)
# make sure the server system is last # make sure the server system runs after any updates
input_callbacks = []
result_callbacks = []
if args.server: if args.server:
from adventure.server import (
launch_server,
server_input,
server_result,
server_system,
)
launch_server()
extra_systems.append((server_system, None)) extra_systems.append((server_system, None))
input_callbacks.append(server_input)
result_callbacks.append(server_result)
# start the sim # start the sim
logger.debug("Simulating world: %s", world) logger.debug("Simulating world: %s", world)

View File

@ -1,60 +1,75 @@
import asyncio import asyncio
from collections import deque
from json import dumps from json import dumps
from logging import getLogger from logging import getLogger
from threading import Thread from threading import Thread
import websockets import websockets
from flask import Flask, send_from_directory
from adventure.models import Actor, Room, World from adventure.models import Actor, Room, World
from adventure.state import snapshot_world, world_json from adventure.state import snapshot_world, world_json
logger = getLogger(__name__) logger = getLogger(__name__)
app = Flask(__name__)
connected = set() connected = set()
recent_events = deque(maxlen=10)
recent_world = None
@app.route("/<path:page>")
def send_report(page: str):
print(f"Sending {page}")
return send_from_directory(
"/home/ssube/code/github/ssube/llm-adventure/web-ui", page
)
async def handler(websocket): async def handler(websocket):
logger.info("Client connected")
connected.add(websocket) 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: while True:
try: try:
# await websocket.wait_closed()
message = await websocket.recv() message = await websocket.recv()
print(message) print(message)
except websockets.ConnectionClosedOK: except websockets.ConnectionClosedOK:
break break
connected.remove(websocket) connected.remove(websocket)
logger.info("Client disconnected")
socket_thread = None socket_thread = None
static_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(): def launch_server():
global socket_thread, static_thread global socket_thread, static_thread
def run_sockets(): def run_sockets():
asyncio.run(server_main()) asyncio.run(server_main())
def run_static():
app.run(port=8000)
socket_thread = Thread(target=run_sockets) socket_thread = Thread(target=run_sockets)
socket_thread.start() socket_thread.start()
static_thread = Thread(target=run_static)
static_thread.start()
async def server_main(): async def server_main():
async with websockets.serve(handler, "", 8001): async with websockets.serve(handler, "", 8001):
@ -63,28 +78,37 @@ async def server_main():
def server_system(world: World, step: int): def server_system(world: World, step: int):
global recent_world
json_state = { json_state = {
**snapshot_world(world, step), **snapshot_world(world, step),
"type": "world", "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): def server_result(room: Room, actor: Actor, action: str):
json_action = { json_action = {
"actor": actor.name, "actor": actor,
"result": action, "result": action,
"room": room.name, "room": room,
"type": "result", "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 = { json_input = {
"actor": actor.name, "actor": actor,
"input": message, "input": message,
"room": room.name, "room": room,
"type": "input", "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)