231 lines
8.2 KiB
Python
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}
|