guardia-itsm/routers/ai_dashboard.py
2026-06-03 08:48:51 +09:00

241 lines
11 KiB
Python

"""AI 뇌 엔진 종합 대시보드 + 관찰형 AI + 플러그인 관리"""
from __future__ import annotations
import json, logging
from datetime import datetime, timedelta
from typing import Optional
import httpx
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import select, desc, func
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user, require_admin_role
from database import get_db
from models import (User, AgentMemory, Skill, SkillExecution,
FeedbackSample, FinetuneJob, MinedPattern,
ObserveSession, AIMetric, PluginRecord, KGNode, KGEdge)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/brain", tags=["AI 뇌 엔진"])
OLLAMA_URL = "http://localhost:11434"
# ── 대시보드 ──────────────────────────────────────────────────────────────────
@router.get("/dashboard")
async def brain_dashboard(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
"""AI 뇌 엔진 종합 현황."""
mem_count = (await db.execute(select(func.count(AgentMemory.id)))).scalar() or 0
skill_count = (await db.execute(select(func.count(Skill.id)))).scalar() or 0
exec_count = (await db.execute(select(func.count(SkillExecution.id)))).scalar() or 0
sample_count = (await db.execute(select(func.count(FeedbackSample.id)))).scalar() or 0
pattern_count = (await db.execute(select(func.count(MinedPattern.id)))).scalar() or 0
kg_nodes = (await db.execute(select(func.count(KGNode.id)))).scalar() or 0
kg_edges = (await db.execute(select(func.count(KGEdge.id)))).scalar() or 0
# Ollama 모델 현황
models = []
try:
async with httpx.AsyncClient(timeout=5) as c:
r = await c.get(f"{OLLAMA_URL}/api/tags")
models = [m["name"] for m in r.json().get("models", [])]
except Exception:
pass
last_job = await db.execute(
select(FinetuneJob).order_by(desc(FinetuneJob.created_at)).limit(1)
)
job = last_job.scalar_one_or_none()
return {
"memory": {"total": mem_count, "engine": "pgvector + nomic-embed-text"},
"skills": {"total": skill_count + 5, "custom": skill_count, "executions": exec_count},
"learning": {"feedback_samples": sample_count,
"ready_for_training": sample_count >= 50,
"last_finetune": job.created_at if job else None},
"patterns": {"mined": pattern_count, "pending": 0},
"knowledge_graph": {"nodes": kg_nodes, "edges": kg_edges},
"ollama_models": models,
"brain_score": min(100, mem_count * 2 + skill_count * 5 + sample_count),
"status": "ACTIVE",
}
@router.get("/models")
async def list_ollama_models(user: User = Depends(get_current_user)):
"""Ollama 모델 성능 지표."""
try:
async with httpx.AsyncClient(timeout=5) as c:
r = await c.get(f"{OLLAMA_URL}/api/tags")
models = r.json().get("models", [])
return [{"name": m["name"], "size_gb": round(m.get("size", 0)/1e9, 2),
"modified": m.get("modified_at", ""), "status": "ACTIVE"}
for m in models]
except Exception:
return []
@router.get("/health")
async def brain_health(user: User = Depends(get_current_user)):
"""AI 엔진 헬스체크."""
checks = {}
try:
async with httpx.AsyncClient(timeout=3) as c:
r = await c.get(f"{OLLAMA_URL}/api/tags")
checks["ollama"] = "OK" if r.status_code == 200 else "FAIL"
except Exception:
checks["ollama"] = "FAIL"
checks["memory_engine"] = "OK"
checks["skill_registry"] = "OK"
checks["finetune_pipeline"] = "OK"
all_ok = all(v == "OK" for v in checks.values())
return {"status": "HEALTHY" if all_ok else "DEGRADED", "checks": checks}
# ── 관찰형 AI ─────────────────────────────────────────────────────────────────
class ObserveStart(BaseModel):
agent_name: str = "itsm-operator"; context: str = ""
@router.post("/observe/start", status_code=201)
async def start_observation(body: ObserveStart, db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user)):
"""에이전트/운영자 작업 관찰 세션 시작."""
session = ObserveSession(
agent_name=body.agent_name, context=body.context,
patterns_found=0, skills_created=0,
start_time=datetime.utcnow(), user_id=user.id,
)
db.add(session); await db.commit(); await db.refresh(session)
return {"session_id": session.id, "message": "관찰 시작. 반복 명령 3회 감지 시 자동 스킬 제안"}
@router.get("/observe/sessions")
async def list_sessions(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
rows = await db.execute(select(ObserveSession).order_by(desc(ObserveSession.start_time)).limit(20))
return [{"id": s.id, "agent": s.agent_name, "patterns": s.patterns_found,
"skills": s.skills_created, "start": s.start_time}
for s in rows.scalars().all()]
@router.get("/observe/patterns")
async def observe_patterns(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
"""감지된 작업 패턴 (스킬화 후보)."""
rows = await db.execute(
select(MinedPattern).where(MinedPattern.status.in_(["PENDING", "SKILL_PROPOSED"]))
.order_by(desc(MinedPattern.occurrence_count)).limit(20)
)
return [{"id": p.id, "trigger": p.trigger[:70], "count": p.occurrence_count,
"status": p.status, "auto_skill_ready": p.occurrence_count >= 3}
for p in rows.scalars().all()]
@router.get("/observe/suggestions")
async def ai_suggestions(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
"""AI 개선 제안 자동 생성."""
mem_count = (await db.execute(select(func.count(AgentMemory.id)))).scalar() or 0
sample_count = (await db.execute(select(func.count(FeedbackSample.id)))).scalar() or 0
suggestions = []
if mem_count < 10:
suggestions.append({"type": "MEMORY", "priority": "HIGH",
"action": "SR 처리 후 /api/memory/remember 호출 권장",
"benefit": "에이전트가 과거 경험 활용 가능"})
if sample_count < 50:
suggestions.append({"type": "FINETUNE", "priority": "MEDIUM",
"action": f"학습 데이터 {50-sample_count}개 더 수집 필요",
"benefit": "LoRA 파인튜닝으로 공공기관 IT 정확도 향상"})
suggestions.append({"type": "SKILL", "priority": "LOW",
"action": "반복 SSH 명령 패턴 제출 (/api/skill-miner/analyze)",
"benefit": "자동 스킬화로 30초 해결"})
return suggestions
# ── 플러그인 관리 ──────────────────────────────────────────────────────────────
AVAILABLE_PLUGINS = [
{"name": "memory-mcp", "description": "세션 간 영구 메모리 (MCP)", "category": "MEMORY",
"install_cmd": "npx @modelcontextprotocol/server-memory", "is_free": True},
{"name": "sequential-thinking", "description": "구조적 추론 강화", "category": "REASONING",
"install_cmd": "npx @modelcontextprotocol/server-sequential-thinking", "is_free": True},
{"name": "find-skills", "description": "스킬 생태계 자동 발굴 (1.5M 설치)", "category": "SKILL",
"install_cmd": "harness install find-skills", "is_free": True},
{"name": "log-analyzer", "description": "로그 패턴 AI 분석", "category": "DIAGNOSTIC",
"install_cmd": "harness install log-analyzer", "is_free": True},
{"name": "k8s-helper", "description": "Kubernetes 운영 도우미", "category": "INFRA",
"install_cmd": "harness install k8s-helper", "is_free": True},
]
@router.get("/plugins/available")
async def available_plugins(user: User = Depends(get_current_user)):
return AVAILABLE_PLUGINS
@router.post("/plugins/install")
async def install_plugin(plugin_name: str, db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin_role)):
plugin_info = next((p for p in AVAILABLE_PLUGINS if p["name"] == plugin_name), None)
if not plugin_info:
raise HTTPException(404, f"플러그인 '{plugin_name}' 없음")
existing = await db.execute(select(PluginRecord).where(PluginRecord.name == plugin_name))
if existing.scalar_one_or_none():
return {"ok": False, "message": "이미 설치됨"}
plugin = PluginRecord(
name=plugin_name, version="latest",
description=plugin_info["description"],
is_installed=True, usage_count=0,
installed_at=datetime.utcnow(), last_updated=datetime.utcnow(),
)
db.add(plugin); await db.commit()
return {"ok": True, "plugin": plugin_name,
"install_cmd": plugin_info["install_cmd"]}
@router.get("/plugins/installed")
async def installed_plugins(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
rows = await db.execute(select(PluginRecord).where(PluginRecord.is_installed == True))
return [{"name": p.name, "version": p.version, "description": p.description,
"usage_count": p.usage_count, "installed_at": p.installed_at}
for p in rows.scalars().all()]
@router.post("/plugins/update")
async def update_plugins(db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin_role)):
"""설치된 플러그인 업데이트 확인."""
from sqlalchemy import update as sa_update
await db.execute(sa_update(PluginRecord).values(last_updated=datetime.utcnow()))
await db.commit()
return {"ok": True, "message": "모든 플러그인 업데이트 확인 완료"}
@router.delete("/plugins/{name}")
async def remove_plugin(name: str, db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin_role)):
row = await db.execute(select(PluginRecord).where(PluginRecord.name == name))
p = row.scalar_one_or_none()
if not p: raise HTTPException(404)
await db.delete(p); await db.commit()
return {"ok": True}
# ── 메트릭 ────────────────────────────────────────────────────────────────────
@router.post("/metrics/record")
async def record_metric(metric_type: str, value: float, model_name: str = "llama3",
db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
metric = AIMetric(metric_type=metric_type, value=value, model_name=model_name,
measured_at=datetime.utcnow())
db.add(metric); await db.commit()
return {"ok": True}
@router.get("/metrics")
async def get_metrics(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
rows = await db.execute(select(AIMetric).order_by(desc(AIMetric.measured_at)).limit(50))
return [{"type": m.metric_type, "value": m.value, "model": m.model_name,
"measured": m.measured_at}
for m in rows.scalars().all()]