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