""" 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": "텍스트를 직접 입력하세요", }