import asyncio import json from datetime import datetime from typing import Dict, List from uuid import uuid4 from fastapi import APIRouter, WebSocket, WebSocketDisconnect from core.bot import ( ROOMS, check_proactive_context, get_bot_response, message_store, store_message, ) router = APIRouter() # room_id → list of connected WebSocket clients room_channels: Dict[str, List[WebSocket]] = {} def _new_msg_id() -> str: return f"MSG-{datetime.now().strftime('%Y%m%d')}-{str(uuid4())[:8].upper()}" async def broadcast(room_id: str, payload: dict) -> None: dead: List[WebSocket] = [] for ws in room_channels.get(room_id, []): try: await ws.send_text(json.dumps(payload, ensure_ascii=False)) except Exception: dead.append(ws) for ws in dead: room_channels[room_id].remove(ws) def _make_bot_msg(room_id: str, reply: dict) -> dict: return { "message_id": _new_msg_id(), "timestamp": datetime.now().isoformat(), "room_id": room_id, "sender": "GUARDiA-Bot", "sender_type": "BOT", "msg_type": "CHAT", "content": reply.get("content", ""), "is_widget": reply.get("is_widget", False), "interactive_action": reply.get("interactive_action"), } @router.websocket("/ws/chat/{room_id}/{client_id}") async def chat_endpoint(ws: WebSocket, room_id: str, client_id: str): await ws.accept() room_channels.setdefault(room_id, []).append(ws) # 접속 시 최근 메시지 + 방 목록 전달 history = message_store.get(room_id, [])[-50:] await ws.send_text(json.dumps( {"type": "init", "messages": history, "rooms": ROOMS}, ensure_ascii=False, )) try: while True: raw = await ws.receive_text() data = json.loads(raw) text = data.get("content", "").strip() if not text: continue # 사용자 메시지 브로드캐스트 user_msg = { "message_id": _new_msg_id(), "timestamp": datetime.now().isoformat(), "room_id": room_id, "sender": client_id, "sender_type": "HUMAN", "msg_type": "CHAT", "content": text, "is_widget": False, "interactive_action": None, } store_message(room_id, user_msg) await broadcast(room_id, user_msg) # 봇 명령 응답 bot_reply = get_bot_response(text) if bot_reply: await asyncio.sleep(0.4) bot_msg = _make_bot_msg(room_id, bot_reply) store_message(room_id, bot_msg) await broadcast(room_id, bot_msg) continue # 선제적 맥락 분석 proactive = check_proactive_context(text) if proactive: await asyncio.sleep(1.2) pro_msg = _make_bot_msg(room_id, proactive) store_message(room_id, pro_msg) await broadcast(room_id, pro_msg) except WebSocketDisconnect: lst = room_channels.get(room_id, []) if ws in lst: lst.remove(ws)