zioinfo-mail/messenger/routers/ws_relay.py
DESKTOP-TKLFCPR\ython a3b3aaf29e feat(chatbot): AI 챗봇 채널 추가
- core/chatbot.py: 세션 기반 대화 엔진 (컨텍스트 기억, Ollama sLLM 연동 + 폴백)
- #AI챗봇 전용 채널: @bot 없이 자연어 자유 대화
- 타이핑 인디케이터 (봇 응답 생성 중 애니메이션)
- 인텐트 분류: 재기동/배포/로그/DB/SSL/디스크/크론/보안
- Ollama(sLLM) 우선, 미설치 시 지식베이스 자동 폴백
- 대화 초기화 명령 지원

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:13:54 +09:00

130 lines
4.4 KiB
Python

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)