diff --git a/adventure/discord_bot.py b/adventure/discord_bot.py index 4c1d64b..c158a4e 100644 --- a/adventure/discord_bot.py +++ b/adventure/discord_bot.py @@ -221,7 +221,7 @@ def launch_bot(): continue event = event_queue.get() - logger.info("broadcasting event %s", event.type) + logger.debug("broadcasting %s event", event.type) if client: event_task = client.loop.create_task(broadcast_event(event)) @@ -343,7 +343,7 @@ def embed_from_player(event: PlayerEvent): description = f"{event.client} is now playing as {event.character}" else: title = "Player Left" - description = f"{event.client} has left the game, {event.character} will be played by the AI" + description = f"{event.client} has left the game. {event.character} is now controlled by an LLM" player_embed = Embed(title=title, description=description) return player_embed diff --git a/adventure/server.py b/adventure/server.py index b83ba92..b18af62 100644 --- a/adventure/server.py +++ b/adventure/server.py @@ -3,7 +3,7 @@ from collections import deque from json import dumps, loads from logging import getLogger from threading import Thread -from typing import Literal +from typing import Dict, Literal from uuid import uuid4 import websockets @@ -27,6 +27,11 @@ logger = getLogger(__name__) connected = set() recent_events = deque(maxlen=100) last_snapshot = None +player_names: Dict[str, str] = {} + + +def get_player_name(client_id: str) -> str: + return player_names.get(client_id, client_id) async def handler(websocket): @@ -70,45 +75,74 @@ async def handler(websocket): try: # if this socket is attached to a character and that character's turn is active, wait for input message = await websocket.recv() - logger.info(f"Received message for {id}: {message}") + player_name = get_player_name(id) + logger.info(f"Received message for {player_name}: {message}") try: data = loads(message) message_type = data.get("type", None) 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? - remove_player(id) - - actor, llm_agent = get_actor_agent_for_name(character_name) - if not actor: - logger.error(f"Failed to find actor {character_name}") - continue - - # prevent any recursive fallback bugs - if isinstance(llm_agent, RemotePlayer): - logger.warning( - "patching recursive fallback for %s", character_name + if "name" in data: + new_player_name = data["name"] + existing_id = next( + ( + k + for k, v in player_names.items() + if v == new_player_name + ), + None, ) - llm_agent = llm_agent.fallback_agent + if existing_id is not None: + logger.error( + f"Name {new_player_name} is already in use by {existing_id}" + ) + continue - # player_name = data["player"] - player = RemotePlayer( - actor.name, actor.backstory, sync_turn, fallback_agent=llm_agent - ) - set_player(id, player) - logger.info(f"Client {id} is now character {character_name}") + logger.info( + f"changing player name for {id} to {new_player_name}" + ) + player_names[id] = new_player_name - # swap out the LLM agent - set_actor_agent(actor.name, actor, player) + elif "become" in data: + character_name = data["become"] + if has_player(character_name): + logger.error( + f"Character {character_name} is already in use" + ) + continue - # notify all clients that this character is now active - player_event(character_name, id, "join") - player_list() + # TODO: should this always remove? + remove_player(id) + + actor, llm_agent = get_actor_agent_for_name(character_name) + if not actor: + logger.error(f"Failed to find actor {character_name}") + continue + + # prevent any recursive fallback bugs + if isinstance(llm_agent, RemotePlayer): + logger.warning( + "patching recursive fallback for %s", character_name + ) + llm_agent = llm_agent.fallback_agent + + player = RemotePlayer( + actor.name, + actor.backstory, + sync_turn, + fallback_agent=llm_agent, + ) + set_player(id, player) + logger.info( + f"Client {player_name} is now character {character_name}" + ) + + # swap out the LLM agent + set_actor_agent(actor.name, actor, player) + + # notify all clients that this character is now active + player_event(character_name, player_name, "join") + player_list() elif message_type == "input": player = get_player(id) if player and isinstance(player, RemotePlayer): @@ -129,8 +163,9 @@ async def handler(websocket): if player and isinstance(player, RemotePlayer): remove_player(id) - logger.info("Disconnecting player for %s", player.name) - player_event(player.name, id, "leave") + player_name = get_player_name(id) + logger.info("Disconnecting player %s from %s", player_name, player.name) + player_event(player.name, player_name, "leave") player_list() actor, _ = get_actor_agent_for_name(player.name) @@ -138,7 +173,7 @@ async def handler(websocket): logger.info("Restoring LLM agent for %s", player.name) set_actor_agent(player.name, actor, player.fallback_agent) - logger.info("Client disconnected: %s", id) + logger.info("Client disconnected: %s", player_name) socket_thread = None diff --git a/client/src/app.tsx b/client/src/app.tsx index fecd7ae..c3b4ca9 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -20,6 +20,8 @@ import { Switch, FormGroup, FormControlLabel, + TextField, + IconButton, } from '@mui/material'; import { Allotment } from 'allotment'; @@ -30,6 +32,8 @@ import { PlayerPanel } from './player.js'; import 'allotment/dist/style.css'; import './main.css'; import { HistoryPanel } from './history.js'; +import { set } from 'lodash'; +import { CheckBox, Save } from '@mui/icons-material'; const useWebSocket = (useWebSocketModule as any).default; @@ -74,15 +78,23 @@ export function DetailDialog(props: { setDetails: SetDetails; details: Maybe(false); const [ autoScroll, setAutoScroll ] = useState(true); + const [ themeMode, setThemeMode ] = useState('light'); + + // client identity - slice 2 + const [ clientId, setClientId ] = useState(''); + const [ clientName, setClientName ] = useState(''); + + // world state - slice 3 const [ detailEntity, setDetailEntity ] = useState>(undefined); const [ character, setCharacter ] = useState>(undefined); - const [ clientId, setClientId ] = useState(''); const [ world, setWorld ] = useState>(undefined); - const [ themeMode, setThemeMode ] = useState('light'); - const [ history, setHistory ] = useState>([]); const [ players, setPlayers ] = useState>({}); + + // socket stuff + const [ history, setHistory ] = useState>([]); const { lastMessage, readyState, sendMessage } = useWebSocket(props.socketUrl); function setPlayer(actor: Maybe) { @@ -99,6 +111,11 @@ export function App(props: AppProps) { } } + function sendName(name: string) { + sendMessage(JSON.stringify({ type: 'player', name: clientName })); + setClientName(name); + } + const theme = createTheme({ palette: { mode: themeMode as PaletteMode, @@ -175,6 +192,22 @@ export function App(props: AppProps) { sx={{ marginLeft: 'auto' }} />} label="Auto Scroll" /> + + setClientName(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + sendName(clientName); + } + }} + sx={{ marginLeft: 'auto' }} + /> + sendName(clientName)}> + + + diff --git a/docs/events.md b/docs/events.md index 5c032a4..9ce3e1b 100644 --- a/docs/events.md +++ b/docs/events.md @@ -16,6 +16,12 @@ - [Reply Events](#reply-events) - [Result Events](#result-events) - [Status Events](#status-events) + - [Server-specific Events](#server-specific-events) + - [Websocket Server Events](#websocket-server-events) + - [Websocket New Client](#websocket-new-client) + - [Websocket Player Become Character](#websocket-player-become-character) + - [Websocket Player Input](#websocket-player-input) + - [Websocket Player Name](#websocket-player-name) ## Event Types @@ -88,3 +94,53 @@ more frequent progress updates when generating with slow models. ### Result Events ### Status Events + +## Server-specific Events + +### Websocket Server Events + +The websocket server has a few unique message types that it uses to communicate metadata with socket clients. + +#### Websocket New Client + +Notify a new client of its unique ID. + +```yaml +type: "id" +id: str +``` + +This is an outgoing event from the server to clients. + +#### Websocket Player Become Character + +A socket client wants to play as a character in the world. + +```yaml +type: "player" +become: str +``` + +This is an incoming event from clients to the server. + +#### Websocket Player Input + +A socket client has sent some input, usually in response to a prompt. + +```yaml +type: "input" +input: str +``` + +This is an incoming event from clients to the server. + +#### Websocket Player Name + +Update the player name attached to a socket client. + +```yaml +type: "player" +name: str +``` + +This is an incoming event from clients to the server.