guardia-itsm/routers/ai_insights.py
2026-06-02 06:07:36 +09:00

231 lines
8.2 KiB
Python

"""
AI 인사이트 — SR 패턴 분석 + 반복 장애 예측 + 주간 운영 리포트
엔드포인트:
GET /api/insights/weekly — 주간 AI 인사이트 리포트
GET /api/insights/patterns — 반복 SR 패턴 분석
GET /api/insights/anomalies — 이상 패턴 감지
GET /api/insights/recommendations — AI 운영 개선 권고
POST /api/insights/ask — 운영 데이터 자연어 질의
"""
from __future__ import annotations
import logging
from datetime import date, datetime, timedelta
from typing import Optional
import httpx
from fastapi import APIRouter, Depends, Query
from sqlalchemy import select, func, desc
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user
from database import get_db
from models import User, SRRequest, SRStatus
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/insights", tags=["AI Insights"])
OLLAMA_URL = "http://localhost:11434"
MODEL = "llama3"
async def _llm(prompt: str, system: str = "") -> str:
try:
async with httpx.AsyncClient(timeout=30) as c:
r = await c.post(f"{OLLAMA_URL}/api/generate", json={
"model": MODEL, "prompt": prompt,
"system": system or "GUARDiA ITSM 전문 분석가. 한국어로 핵심만 간결하게.",
"stream": False,
})
return r.json().get("response", "").strip() if r.status_code == 200 else ""
except Exception as e:
logger.warning(f"LLM 호출 실패: {e}")
return ""
@router.get("/weekly")
async def weekly_insights(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""주간 AI 인사이트 리포트."""
today = date.today()
week_start = today - timedelta(days=7)
# 이번 주 SR 통계
total_r = await db.execute(
select(func.count(SRRequest.id)).where(SRRequest.created_at >= week_start)
)
done_r = await db.execute(
select(func.count(SRRequest.id)).where(
SRRequest.status == SRStatus.DONE, SRRequest.updated_at >= week_start
)
)
open_r = await db.execute(
select(func.count(SRRequest.id)).where(
SRRequest.status.in_([SRStatus.OPEN, SRStatus.IN_PROGRESS])
)
)
total = total_r.scalar() or 0
done = done_r.scalar() or 0
open_count = open_r.scalar() or 0
# 카테고리별 분포
cat_rows = await db.execute(
select(SRRequest.category, func.count(SRRequest.id).label("cnt"))
.where(SRRequest.created_at >= week_start)
.group_by(SRRequest.category).order_by(desc("cnt")).limit(5)
)
top_categories = [(r.category or "기타", r.cnt) for r in cat_rows.all()]
# Ollama 인사이트 생성
stats_summary = (
f"이번 주 신규 SR {total}건, 완료 {done}건, 미처리 {open_count}건. "
f"상위 카테고리: {', '.join(f'{c}({n}건)' for c, n in top_categories[:3])}"
)
insight = await _llm(
f"운영 현황: {stats_summary}\n운영팀을 위한 핵심 인사이트 3가지를 번호 매겨 제시하세요."
)
return {
"period": {"start": week_start.isoformat(), "end": today.isoformat()},
"stats": {"total": total, "done": done, "open": open_count,
"completion_rate": round(done / total * 100, 1) if total else 0},
"top_categories": [{"category": c, "count": n} for c, n in top_categories],
"ai_insight": insight,
"generated_at": datetime.utcnow(),
}
@router.get("/patterns")
async def sr_patterns(
days: int = Query(30, ge=7, le=90),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""반복 SR 패턴 — 같은 카테고리/서버에서 반복 발생하는 SR."""
since = date.today() - timedelta(days=days)
# 카테고리별 반복 패턴
cat_rows = await db.execute(
select(
SRRequest.category, SRRequest.priority,
func.count(SRRequest.id).label("cnt"),
func.avg(
func.extract('epoch', SRRequest.updated_at - SRRequest.created_at) / 3600
).label("avg_hours")
).where(SRRequest.created_at >= since)
.group_by(SRRequest.category, SRRequest.priority)
.order_by(desc("cnt")).limit(10)
)
patterns = [
{
"category": r.category or "기타",
"priority": r.priority or "MEDIUM",
"count": r.cnt,
"avg_resolution_hours": round(r.avg_hours or 0, 1),
"is_recurring": r.cnt >= 3,
}
for r in cat_rows.all()
]
recurring = [p for p in patterns if p["is_recurring"]]
insight = ""
if recurring:
summary = ", ".join(f"{p['category']}({p['count']}건)" for p in recurring[:3])
insight = await _llm(
f"반복 발생 카테고리: {summary}. 근본 원인과 재발 방지 방안을 제시하세요."
)
return {"period_days": days, "patterns": patterns, "recurring_count": len(recurring), "insight": insight}
@router.get("/anomalies")
async def detect_anomalies(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""이상 패턴 감지 — 오늘 SR이 7일 평균보다 2배 이상이거나 미처리가 급증."""
today = date.today()
today_count_r = await db.execute(
select(func.count(SRRequest.id)).where(func.date(SRRequest.created_at) == today)
)
today_count = today_count_r.scalar() or 0
# 7일 평균
daily_counts = []
for i in range(1, 8):
d = today - timedelta(days=i)
r = await db.execute(select(func.count(SRRequest.id)).where(func.date(SRRequest.created_at) == d))
daily_counts.append(r.scalar() or 0)
avg_7d = sum(daily_counts) / len(daily_counts) if daily_counts else 0
anomalies = []
if avg_7d > 0 and today_count >= avg_7d * 2:
anomalies.append({
"type": "SR_SURGE", "severity": "HIGH",
"message": f"오늘 SR {today_count}건 — 7일 평균({avg_7d:.0f}건) 대비 {today_count/avg_7d:.1f}",
})
# 미처리 SR 급증
open_r = await db.execute(
select(func.count(SRRequest.id)).where(SRRequest.status.in_([SRStatus.OPEN, SRStatus.IN_PROGRESS]))
)
open_count = open_r.scalar() or 0
if open_count > 20:
anomalies.append({
"type": "BACKLOG_HIGH", "severity": "MEDIUM",
"message": f"미처리 SR {open_count}건 — 임계값(20건) 초과",
})
return {"anomalies": anomalies, "today_sr": today_count, "avg_7d": round(avg_7d, 1), "open_sr": open_count}
@router.get("/recommendations")
async def ai_recommendations(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""AI 운영 개선 권고사항."""
weekly = await weekly_insights(db, user)
patterns = await sr_patterns(30, db, user)
anomalies = await detect_anomalies(db, user)
context = (
f"완료율 {weekly['stats']['completion_rate']}%, "
f"미처리 {weekly['stats']['open']}건, "
f"반복 카테고리 {patterns['recurring_count']}개, "
f"이상 감지 {len(anomalies['anomalies'])}"
)
recommendations = await _llm(
f"운영 현황: {context}\n개선 권고사항 5가지를 우선순위 순으로 제시하세요.",
"GUARDiA ITSM 운영 컨설턴트. 구체적이고 실행 가능한 권고사항을 제시."
)
return {
"summary": context,
"recommendations": recommendations,
"generated_at": datetime.utcnow(),
}
class AskRequest(__import__('pydantic', fromlist=['BaseModel']).BaseModel):
question: str
@router.post("/ask")
async def ask_operations(
req: AskRequest,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""운영 데이터 자연어 질의."""
weekly = await weekly_insights(db, user)
context = (
f"이번 주 SR 현황: 신규 {weekly['stats']['total']}건, "
f"완료 {weekly['stats']['done']}건, 미처리 {weekly['stats']['open']}"
)
answer = await _llm(f"운영 현황: {context}\n\n질문: {req.question}")
return {"question": req.question, "answer": answer}