import asyncio import logging from datetime import datetime import socketio from app.models.database import async_session_maker from app.models.orm import Character, Clue, Message, Vote from app.models.schemas import GamePhase from app.services.agent_scheduler import agent_scheduler from app.services.game_state_manager import game_state_manager from sqlalchemy import select logger = logging.getLogger(__name__) sio = socketio.AsyncServer( async_mode="asgi", cors_allowed_origins="*", logger=False, ) connected_clients: dict[str, dict] = {} _tracked_tasks: set[asyncio.Task] = set() @sio.event async def connect(sid, environ, auth): connected_clients[sid] = {"sid": sid, "role": "viewer"} await sio.emit("system", {"message": "Connected to Hermes Live Show"}, to=sid) @sio.event async def disconnect(sid): connected_clients.pop(sid, None) @sio.event async def set_role(sid, data): if sid in connected_clients: connected_clients[sid]["role"] = data.get("role", "viewer") @sio.event async def send_message(sid, data): character_id = data.get("character_id") content = data.get("content", "") msg_type = data.get("msg_type", "character") game_phase = data.get("game_phase", "") target_character_id = data.get("target_character_id") if async_session_maker: async with async_session_maker() as session: msg = Message( character_id=character_id, game_phase=game_phase, msg_type=msg_type, content=content, target_character_id=target_character_id, ) session.add(msg) await session.commit() await sio.emit("new_message", { "character_id": character_id, "content": content, "msg_type": msg_type, "game_phase": game_phase, "target_character_id": target_character_id, }) @sio.event async def dm_command(sid, data): command = data.get("command", "") args = data.get("args", {}) if async_session_maker: async with async_session_maker() as session: if command == "next_phase": state = await game_state_manager.next_phase(session) await sio.emit("state_change", { "phase": state.current_phase, "progress": game_state_manager.get_progress_percent(state), }) elif command == "prev_phase": state = await game_state_manager.prev_phase(session) await sio.emit("state_change", { "phase": state.current_phase, "progress": game_state_manager.get_progress_percent(state), }) elif command == "unlock_clue": clue_id = args.get("clue_id") character_id = args.get("character_id") if clue_id and character_id: result = await session.execute(select(Clue).where(Clue.id == clue_id)) clue = result.scalars().first() if clue: clue.is_unlocked = True clue.unlocked_by = character_id clue.unlocked_at = datetime.utcnow() await session.commit() await sio.emit("clue_unlocked", { "clue_id": clue.id, "clue_name": clue.name, "content": clue.content, "unlocked_by": character_id, }) elif command == "start_auto": state = await game_state_manager.get_state(session) script_id = args.get("script_id") or (state.active_script_id if state else None) if script_id: interval = float(args.get("interval", 8.0)) rounds = int(args.get("rounds", 1)) async def emit(event, data): await sio.emit(event, data) task = asyncio.create_task( agent_scheduler.run_auto_speaking(session, script_id, interval, rounds, emit) ) _tracked_tasks.add(task) task.add_done_callback(_tracked_tasks.discard) await sio.emit("system", {"message": "Auto speaking started"}) elif command == "stop_auto": agent_scheduler.stop() await sio.emit("system", {"message": "Auto speaking stopped"}) elif command == "trigger_vote": state = await game_state_manager.get_state(session) script_id = args.get("script_id") or (state.active_script_id if state else None) if script_id: async def emit(event, data): await sio.emit(event, data) votes = await agent_scheduler.trigger_voting(session, script_id, emit) current_state = await game_state_manager.get_or_create_state(session) round_num = current_state.round_number if hasattr(current_state, 'round_number') else 1 for v in votes: session.add(Vote( voter_id=v["voter_id"], target_id=v["target_id"], round_number=round_num, reason=v.get("reason", ""), )) await session.commit() await sio.emit("voting_results", {"votes": votes}) @sio.event async def request_state(sid, data): if async_session_maker: async with async_session_maker() as session: state = await game_state_manager.get_or_create_state(session) await sio.emit("state_change", { "phase": state.current_phase, "progress": game_state_manager.get_progress_percent(state), "is_running": state.is_running, "is_paused": state.is_paused, }, to=sid) def create_socket_app(app): return socketio.ASGIApp(sio, other_app=app)