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, ) from core.chatbot import get_chatbot_response 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"), } async def _send_typing(room_id: str, show: bool) -> None: await broadcast(room_id, {"type": "bot_typing", "show": show, "room_id": room_id}) @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) # ── chatbot 전용 채널 ─────────────────────────────── if room_id == "chatbot": await _send_typing(room_id, True) try: reply_text = await asyncio.wait_for( get_chatbot_response(client_id, text), timeout=10.0, ) except asyncio.TimeoutError: reply_text = "응답 시간이 초과되었습니다. 잠시 후 다시 시도해주세요." finally: await _send_typing(room_id, False) bot_msg = _make_bot_msg(room_id, {"content": reply_text, "is_widget": False}) store_message(room_id, bot_msg) await broadcast(room_id, bot_msg) continue # ── 일반 채널: @bot 명령 처리 ────────────────────── 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)