guardia-itsm/routers/smart_ux.py
2026-06-03 09:16:57 +09:00

222 lines
8.0 KiB
Python

"""
GUARDiA 스마트 UX — 다음명령 제시 + 음성처리
엔드포인트:
POST /api/ux/next-commands — Ollama 다음명령 3개 예측
GET /api/ux/quick-commands — 자주 쓰는 명령 목록
POST /api/ux/learn — 명령 사용 학습
GET /api/ux/suggestions — 컨텍스트 기반 추천
POST /api/ux/voice-process — 음성 텍스트→명령 매핑
"""
from __future__ import annotations
import json, logging
from datetime import datetime, timedelta
from typing import List, Optional
import httpx
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlalchemy import select, desc, func
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user
from database import get_db
from models import User, CommandHistory
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/ux", tags=["스마트 UX"])
OLLAMA_URL = "http://localhost:11434"
TEXT_MODEL = "llama3"
# GUARDiA 기본 명령어 (빠른 접근용)
DEFAULT_COMMANDS = [
"/sr create", "/server status", "/deploy",
"/incident create", "/dashboard", "/kb search",
"/cmdb server", "/assign me", "/sr list",
"/server check all", "/alert list", "/report generate",
]
# 음성 명령어 매핑 (한국어 → ITSM 명령)
VOICE_COMMAND_MAP = {
"서버 상태": "/server status", "서버 확인": "/server status",
"SR 만들어": "/sr create", "서비스 요청": "/sr create",
"배포 시작": "/deploy", "배포해줘": "/deploy",
"장애 보고": "/incident create", "인시던트": "/incident create",
"대시보드": "/dashboard", "현황 보기": "/dashboard",
"담당자 배정": "/assign", "KB 검색": "/kb search",
"보고서": "/report generate", "리포트": "/report generate",
"경보 목록": "/alert list", "알림 확인": "/alert list",
}
class NextCommandIn(BaseModel):
recent_messages: List[str] = []
context: str = "" # 현재 방/작업 컨텍스트
user_role: str = ""
class LearnIn(BaseModel):
command: str
room: str = "general"
success: bool = True
class VoiceIn(BaseModel):
text: str # 음성→텍스트 변환 결과
context: str = ""
@router.post("/next-commands")
async def next_commands(body: NextCommandIn, db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user)):
"""Ollama로 다음 명령어 3개 예측."""
# 1. 사용자 자주 쓰는 명령 조회
rows = await db.execute(
select(CommandHistory.command, func.count(CommandHistory.id).label("cnt"))
.where(CommandHistory.user_id == user.id,
CommandHistory.used_at > datetime.utcnow() - timedelta(days=7))
.group_by(CommandHistory.command)
.order_by(desc("cnt")).limit(5)
)
frequent = [r[0] for r in rows.all()]
# 2. Ollama 컨텍스트 분석
recent = body.recent_messages[-5:] if body.recent_messages else []
context_str = "\n".join(recent) if recent else "대화 없음"
try:
async with httpx.AsyncClient(timeout=15) as c:
r = await c.post(f"{OLLAMA_URL}/api/generate", json={
"model": TEXT_MODEL,
"system": """GUARDiA ITSM 운영 어시스턴트.
사용 가능 명령: /sr create, /server status, /deploy, /incident create,
/kb search, /dashboard, /assign, /cmdb, /report, /alert
JSON 배열로만 답변: ["/cmd1", "/cmd2", "/cmd3"]""",
"prompt": f"최근 대화:\n{context_str}\n\n컨텍스트: {body.context}\n다음에 유용한 명령 3개:",
"stream": False,
})
resp = r.json().get("response", "[]")
# JSON 파싱 시도
import re
arr_match = re.search(r'\[([^\]]+)\]', resp)
if arr_match:
raw = arr_match.group(0)
suggested = json.loads(raw)
else:
suggested = DEFAULT_COMMANDS[:3]
except Exception:
suggested = DEFAULT_COMMANDS[:3]
# 3. 자주 쓰는 명령 우선 포함
all_cmds = frequent[:2] + [c for c in suggested if c not in frequent]
final = all_cmds[:3] if all_cmds else DEFAULT_COMMANDS[:3]
return {
"commands": final,
"source": "ai+history",
"frequent": frequent[:3],
}
@router.get("/quick-commands")
async def quick_commands(db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user)):
"""자주 쓰는 명령 + 기본 명령 목록."""
rows = await db.execute(
select(CommandHistory.command, func.count(CommandHistory.id).label("cnt"))
.where(CommandHistory.user_id == user.id)
.group_by(CommandHistory.command)
.order_by(desc("cnt")).limit(8)
)
user_frequent = [{"command": r[0], "use_count": r[1]} for r in rows.all()]
return {
"frequent": user_frequent,
"defaults": [{"command": c, "use_count": 0} for c in DEFAULT_COMMANDS[:6]],
}
@router.post("/learn")
async def learn_command(body: LearnIn, db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user)):
"""명령 사용 기록 학습 (클릭/실행 시 호출)."""
log = CommandHistory(
user_id=user.id,
command=body.command,
room=body.room,
success=body.success,
used_at=datetime.utcnow(),
)
db.add(log); await db.commit()
return {"ok": True, "learned": body.command}
@router.get("/suggestions")
async def get_suggestions(context: str = "", db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user)):
"""현재 컨텍스트 기반 명령 추천."""
ctx_lower = context.lower()
# 키워드 기반 규칙 추천
rules = {
"서버": ["/server status", "/server check all", "/cmdb server"],
"배포": ["/deploy", "/deploy status", "/deploy rollback"],
"sr": ["/sr create", "/sr list", "/sr assign"],
"인시던트": ["/incident create", "/incident list", "/incident assign"],
"보고서": ["/report generate", "/report monthly", "/sla report"],
}
for keyword, cmds in rules.items():
if keyword in ctx_lower:
return {"suggestions": cmds, "matched_context": keyword}
# 기본 추천
return {"suggestions": DEFAULT_COMMANDS[:5], "matched_context": "default"}
@router.post("/voice-process")
async def voice_process(body: VoiceIn, db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user)):
"""음성 텍스트→ITSM 명령 매핑 + Ollama 자연어 처리."""
text = body.text.strip()
# 1. 직접 매핑 확인
for korean, cmd in VOICE_COMMAND_MAP.items():
if korean in text:
return {
"original_text": text,
"mapped_command": cmd,
"confidence": "high",
"method": "keyword_mapping",
}
# 2. Ollama 자연어 처리
try:
async with httpx.AsyncClient(timeout=15) as c:
r = await c.post(f"{OLLAMA_URL}/api/generate", json={
"model": TEXT_MODEL,
"system": """ITSM 명령어 매퍼.
사용자 음성을 ITSM 슬래시 명령으로 변환.
가능한 명령: /sr create, /server status, /deploy, /incident create, /dashboard
슬래시 명령만 출력 (설명 없음).""",
"prompt": f"음성: '{text}'\n→ ITSM 명령:",
"stream": False,
})
resp = r.json().get("response", "").strip()
import re
cmd_match = re.search(r'/\w+(?:\s+\w+)*', resp)
if cmd_match:
return {"original_text": text, "mapped_command": cmd_match.group(),
"confidence": "medium", "method": "ollama"}
except Exception:
pass
return {
"original_text": text,
"mapped_command": None,
"confidence": "low",
"method": "failed",
"suggestion": "텍스트를 직접 입력하세요",
}