- core/chatbot.py: 세션 기반 대화 엔진 (컨텍스트 기억, Ollama sLLM 연동 + 폴백) - #AI챗봇 전용 채널: @bot 없이 자연어 자유 대화 - 타이핑 인디케이터 (봇 응답 생성 중 애니메이션) - 인텐트 분류: 재기동/배포/로그/DB/SSL/디스크/크론/보안 - Ollama(sLLM) 우선, 미설치 시 지식베이스 자동 폴백 - 대화 초기화 명령 지원 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
130 lines
4.4 KiB
Python
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)
|