- itsm/ -> workspace/guardia-itsm/ - manager/ -> workspace/guardia-manager/ - app/ -> workspace/guardia-messenger/ - manual/ -> workspace/guardia-docs/ workspace/zioinfo-web/ unchanged. git mv preserves full commit history. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
306 lines
9.7 KiB
Python
306 lines
9.7 KiB
Python
"""
|
|
B-4: KB 자동 업데이트 에이전트 API 라우터
|
|
|
|
엔드포인트:
|
|
POST /api/kb-agent/run — 에이전트 즉시 실행 (해결 SR 일괄 처리)
|
|
POST /api/kb-agent/analyze/{sr_id} — 특정 SR → KB 분석 및 생성
|
|
GET /api/kb-agent/candidates — KB 후보 목록 (미처리 완료 SR)
|
|
GET /api/kb-agent/stats — 에이전트 처리 통계
|
|
POST /api/kb/ — KB 수동 생성
|
|
PUT /api/kb/{kb_id} — KB 수정
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
from datetime import datetime, timedelta
|
|
from typing import List, Optional
|
|
|
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import desc, select, and_, func
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from database import get_db
|
|
from models import KBDocument, KBDocumentOut, SRRequest, SRStatus
|
|
from core.kb_agent import (
|
|
auto_create_kb_from_sr,
|
|
run_kb_agent_batch,
|
|
classify_category,
|
|
extract_tags_rule,
|
|
compute_similarity,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/kb-agent", tags=["kb-agent"])
|
|
|
|
|
|
# ── doc_id 생성 헬퍼 ─────────────────────────────────────────────────────────
|
|
|
|
async def _next_doc_id(db: AsyncSession) -> str:
|
|
"""KB-YYYYMMDD-NNNN 형식의 문서 ID 생성."""
|
|
today = datetime.utcnow().strftime("%Y%m%d")
|
|
prefix = f"KB-{today}-"
|
|
# 오늘 생성된 최대 번호 조회
|
|
q = select(KBDocument.doc_id).where(
|
|
KBDocument.doc_id.like(f"{prefix}%")
|
|
).order_by(desc(KBDocument.doc_id)).limit(1)
|
|
last = (await db.execute(q)).scalar()
|
|
if last:
|
|
try:
|
|
seq = int(last.split("-")[-1]) + 1
|
|
except ValueError:
|
|
seq = 1
|
|
else:
|
|
seq = 1
|
|
return f"{prefix}{seq:04d}"
|
|
|
|
|
|
# ── 에이전트 실행 ──────────────────────────────────────────────────────────────
|
|
|
|
class RunAgentRequest(BaseModel):
|
|
days_back: int = 7
|
|
max_sr: int = 20
|
|
use_llm: bool = True
|
|
model: str = "llama3"
|
|
|
|
|
|
@router.post("/run")
|
|
async def run_kb_agent(
|
|
body: RunAgentRequest,
|
|
background_tasks: BackgroundTasks,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
KB 자동 업데이트 에이전트 실행.
|
|
최근 N일 완료된 SR을 분석하여 KB 문서 자동 생성.
|
|
"""
|
|
# 동기적 실행 (최대 20건이므로 백그라운드 불필요)
|
|
try:
|
|
result = await run_kb_agent_batch(
|
|
db = db,
|
|
days_back= body.days_back,
|
|
max_sr = body.max_sr,
|
|
use_llm = body.use_llm,
|
|
model = body.model,
|
|
)
|
|
return {
|
|
"status": "completed",
|
|
"run_at": datetime.utcnow().isoformat(),
|
|
**result,
|
|
}
|
|
except Exception as e:
|
|
logger.error("[B-4] 에이전트 실행 오류: %s", e)
|
|
raise HTTPException(500, f"에이전트 실행 오류: {str(e)[:100]}")
|
|
|
|
|
|
@router.post("/analyze/{sr_id}")
|
|
async def analyze_sr(
|
|
sr_id: str,
|
|
use_llm: bool = Query(True),
|
|
model: str = Query("llama3"),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""특정 SR을 분석하여 KB 문서 생성."""
|
|
result = await auto_create_kb_from_sr(
|
|
db=db, sr_id=sr_id, use_llm=use_llm, model=model
|
|
)
|
|
if result is None:
|
|
raise HTTPException(404, f"SR '{sr_id}'를 찾을 수 없습니다.")
|
|
|
|
# KB ID가 있으면 상세 반환
|
|
if result.get("kb_id"):
|
|
kb = (await db.execute(
|
|
select(KBDocument).where(KBDocument.id == result["kb_id"])
|
|
)).scalars().first()
|
|
if kb:
|
|
result["kb_detail"] = {
|
|
"doc_id": kb.doc_id,
|
|
"title": kb.title,
|
|
"category": kb.category,
|
|
"tags": kb.tags,
|
|
}
|
|
|
|
return result
|
|
|
|
|
|
# ── KB 후보 목록 ──────────────────────────────────────────────────────────────
|
|
|
|
@router.get("/candidates")
|
|
async def list_kb_candidates(
|
|
days_back: int = Query(30, ge=1, le=90),
|
|
limit: int = Query(20, ge=1, le=100),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""KB 미처리 완료 SR 목록 (KB 후보)."""
|
|
since = datetime.utcnow() - timedelta(days=days_back)
|
|
|
|
# 이미 처리된 SR ID
|
|
processed = (await db.execute(
|
|
select(KBDocument.source_sr_id).where(KBDocument.source_sr_id.isnot(None))
|
|
)).scalars().all()
|
|
processed_set = set(processed)
|
|
|
|
q = (
|
|
select(SRRequest)
|
|
.where(
|
|
and_(
|
|
SRRequest.status == "COMPLETED",
|
|
SRRequest.updated_at >= since,
|
|
)
|
|
)
|
|
.order_by(desc(SRRequest.updated_at))
|
|
.limit(limit * 2)
|
|
)
|
|
srs = (await db.execute(q)).scalars().all()
|
|
|
|
candidates = []
|
|
for sr in srs:
|
|
if sr.sr_id in processed_set:
|
|
continue
|
|
candidates.append({
|
|
"sr_id": sr.sr_id,
|
|
"title": sr.title,
|
|
"sr_type": str(sr.sr_type) if sr.sr_type else None,
|
|
"priority": str(sr.priority) if sr.priority else None,
|
|
"completed_at": sr.updated_at.isoformat() if sr.updated_at else None,
|
|
})
|
|
if len(candidates) >= limit:
|
|
break
|
|
|
|
return {
|
|
"total": len(candidates),
|
|
"candidates": candidates,
|
|
}
|
|
|
|
|
|
# ── KB 수동 생성 / 수정 ───────────────────────────────────────────────────────
|
|
|
|
class KBCreateRequest(BaseModel):
|
|
title: str
|
|
category: Optional[str] = "일반"
|
|
symptoms: Optional[str] = None
|
|
cause: Optional[str] = None
|
|
solution: Optional[str] = None
|
|
commands: Optional[str] = None
|
|
tags: Optional[str] = None
|
|
sr_type: Optional[str] = None
|
|
source_sr_id: Optional[str] = None
|
|
|
|
|
|
@router.post("/kb", response_model=KBDocumentOut, status_code=201,
|
|
tags=["kb"])
|
|
async def create_kb(
|
|
body: KBCreateRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""KB 문서 수동 생성."""
|
|
doc_id = await _next_doc_id(db)
|
|
|
|
# 자동 태그 보강
|
|
auto_tags = extract_tags_rule(
|
|
body.title, body.symptoms or "", body.solution or ""
|
|
)
|
|
existing_tags = body.tags or ""
|
|
merged_tags = existing_tags + (" " if existing_tags and auto_tags else "") + " ".join(auto_tags)
|
|
|
|
kb = KBDocument(
|
|
doc_id = doc_id,
|
|
title = body.title,
|
|
category = body.category or classify_category(body.title, body.symptoms or ""),
|
|
symptoms = body.symptoms,
|
|
cause = body.cause,
|
|
solution = body.solution,
|
|
commands = body.commands,
|
|
tags = merged_tags.strip(),
|
|
sr_type = body.sr_type,
|
|
source_sr_id = body.source_sr_id,
|
|
author = "user",
|
|
published = False,
|
|
created_at= datetime.utcnow(),
|
|
)
|
|
db.add(kb)
|
|
await db.commit()
|
|
await db.refresh(kb)
|
|
return kb
|
|
|
|
|
|
class KBUpdateRequest(BaseModel):
|
|
title: Optional[str] = None
|
|
category: Optional[str] = None
|
|
symptoms: Optional[str] = None
|
|
cause: Optional[str] = None
|
|
solution: Optional[str] = None
|
|
commands: Optional[str] = None
|
|
tags: Optional[str] = None
|
|
published: Optional[bool] = None
|
|
|
|
|
|
@router.put("/kb/{kb_id}", response_model=KBDocumentOut, tags=["kb"])
|
|
async def update_kb(
|
|
kb_id: int,
|
|
body: KBUpdateRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""KB 문서 수정."""
|
|
kb = (await db.execute(
|
|
select(KBDocument).where(KBDocument.id == kb_id)
|
|
)).scalars().first()
|
|
if not kb:
|
|
raise HTTPException(404, f"KB {kb_id}를 찾을 수 없습니다.")
|
|
|
|
for field, val in body.model_dump(exclude_none=True).items():
|
|
setattr(kb, field, val)
|
|
kb.updated_at = datetime.utcnow()
|
|
|
|
await db.commit()
|
|
await db.refresh(kb)
|
|
return kb
|
|
|
|
|
|
# ── 에이전트 통계 ─────────────────────────────────────────────────────────────
|
|
|
|
@router.get("/stats")
|
|
async def get_kb_agent_stats(
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""KB 에이전트 처리 통계."""
|
|
total_kb = (await db.execute(
|
|
select(func.count()).select_from(KBDocument)
|
|
)).scalar() or 0
|
|
|
|
agent_created = (await db.execute(
|
|
select(func.count()).select_from(KBDocument)
|
|
.where(KBDocument.author == "kb-agent")
|
|
)).scalar() or 0
|
|
|
|
user_created = (await db.execute(
|
|
select(func.count()).select_from(KBDocument)
|
|
.where(KBDocument.author != "kb-agent")
|
|
)).scalar() or 0
|
|
|
|
published = (await db.execute(
|
|
select(func.count()).select_from(KBDocument)
|
|
.where(KBDocument.published == True)
|
|
)).scalar() or 0
|
|
|
|
# 완료 SR 중 KB 미생성 수
|
|
total_completed = (await db.execute(
|
|
select(func.count()).select_from(SRRequest)
|
|
.where(SRRequest.status == "COMPLETED")
|
|
)).scalar() or 0
|
|
|
|
processed_count = (await db.execute(
|
|
select(func.count()).select_from(KBDocument)
|
|
.where(KBDocument.source_sr_id.isnot(None))
|
|
)).scalar() or 0
|
|
|
|
return {
|
|
"total_kb_documents": total_kb,
|
|
"agent_created": agent_created,
|
|
"user_created": user_created,
|
|
"published": published,
|
|
"completed_sr_total": total_completed,
|
|
"kb_coverage_pct": round(processed_count / max(total_completed, 1) * 100, 1),
|
|
}
|