""" Self-Improving Learning Loop — API Router 엔드포인트: GET /api/learning/stats — 학습 현황 통계 GET /api/learning/patterns — 재발 패턴 목록 GET /api/learning/patterns/{id} — 패턴 상세 GET /api/learning/lessons — 검증된 교훈 목록 GET /api/learning/thresholds — 적응형 임계값 현황 POST /api/learning/feedback — KB 사용 피드백 기록 POST /api/learning/feedback/{id}/check — 피드백 효과 즉시 검증 POST /api/learning/thresholds/outcome — 이상 탐지 결과 기록 POST /api/learning/thresholds/missed — 누락 탐지 기록 POST /api/learning/thresholds/calibrate — 임계값 즉시 보정 POST /api/learning/mine — 패턴 마이닝 즉시 실행 POST /api/learning/detect-recurrence — SR 재발 감지 (수동 호출) """ from datetime import datetime from typing import List, Optional from fastapi import APIRouter, Depends, Query from pydantic import BaseModel from sqlalchemy import select, desc from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user from database import get_db from models import ( RecurrencePattern, SolutionFeedback, AdaptiveThreshold, LessonLearned, User ) router = APIRouter(prefix="/api/learning", tags=["learning"]) # ── Pydantic I/O ─────────────────────────────────────────────────────────────── class FeedbackCreate(BaseModel): sr_id: str kb_doc_id: str kb_id: Optional[int] = None class OutcomeRecord(BaseModel): source: str metric_type: str was_actual_incident: bool base_threshold: Optional[float] = None class MissedRecord(BaseModel): source: str metric_type: str class CalibrateRequest(BaseModel): source: str metric_type: str class RecurrenceRequest(BaseModel): sr_id: str title: str description: str sr_type: str inst_id: Optional[int] = None class MineRequest(BaseModel): days_back: int = 30 min_occurrences: int = 3 # ── 통계 요약 ────────────────────────────────────────────────────────────────── @router.get("/stats") async def get_stats( db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): from core.learning import get_learning_stats return await get_learning_stats(db) # ── 재발 패턴 ────────────────────────────────────────────────────────────────── @router.get("/patterns") async def list_patterns( limit: int = Query(50, le=200), offset: int = Query(0, ge=0), sr_type: Optional[str] = None, escalated_only: bool = False, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): q = select(RecurrencePattern) if sr_type: q = q.where(RecurrencePattern.sr_type == sr_type) if escalated_only: q = q.where(RecurrencePattern.escalated == True) q = q.order_by(desc(RecurrencePattern.occurrence_count)).offset(offset).limit(limit) rows = (await db.execute(q)).scalars().all() return [ { "id": p.id, "sr_type": p.sr_type, "tech_keywords": p.tech_keywords, "occurrence_count": p.occurrence_count, "first_seen_at": p.first_seen_at.isoformat() if p.first_seen_at else None, "last_seen_at": p.last_seen_at.isoformat() if p.last_seen_at else None, "escalated": p.escalated, "problem_id": p.problem_id, "sr_ids": (p.sr_ids or [])[-5:], } for p in rows ] @router.get("/patterns/{pattern_id}") async def get_pattern( pattern_id: int, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): p = (await db.execute( select(RecurrencePattern).where(RecurrencePattern.id == pattern_id) )).scalars().first() if not p: from fastapi import HTTPException raise HTTPException(status_code=404, detail="패턴을 찾을 수 없습니다") return { "id": p.id, "pattern_key": p.pattern_key, "sr_type": p.sr_type, "inst_id": p.inst_id, "keyword_signature": p.keyword_signature, "tech_keywords": p.tech_keywords, "occurrence_count": p.occurrence_count, "first_seen_at": p.first_seen_at.isoformat() if p.first_seen_at else None, "last_seen_at": p.last_seen_at.isoformat() if p.last_seen_at else None, "escalated": p.escalated, "problem_id": p.problem_id, "sr_ids": p.sr_ids or [], } # ── 재발 감지 ────────────────────────────────────────────────────────────────── @router.post("/detect-recurrence") async def detect_recurrence( body: RecurrenceRequest, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): from core.learning import detect_recurrence as _detect return await _detect( db = db, sr_id = body.sr_id, title = body.title, description = body.description, sr_type = body.sr_type, inst_id = body.inst_id, ) # ── 솔루션 효과 피드백 ───────────────────────────────────────────────────────── @router.post("/feedback") async def create_feedback( body: FeedbackCreate, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): from core.learning import record_kb_usage return await record_kb_usage(db, body.sr_id, body.kb_doc_id, body.kb_id) @router.post("/feedback/{feedback_id}/check") async def check_feedback( feedback_id: int, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): from core.learning import check_solution_effectiveness return await check_solution_effectiveness(db, feedback_id) @router.get("/feedback") async def list_feedback( limit: int = Query(50, le=200), offset: int = Query(0, ge=0), db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): rows = (await db.execute( select(SolutionFeedback) .order_by(desc(SolutionFeedback.applied_at)) .offset(offset).limit(limit) )).scalars().all() return [ { "id": f.id, "sr_id": f.sr_id, "kb_doc_id": f.kb_doc_id, "applied_at": f.applied_at.isoformat() if f.applied_at else None, "effectiveness_score": f.effectiveness_score, "recurred_within_days": f.recurred_within_days, "checked_at": f.checked_at.isoformat() if f.checked_at else None, } for f in rows ] # ── 교훈 목록 ────────────────────────────────────────────────────────────────── @router.get("/lessons") async def list_lessons( limit: int = Query(50, le=200), offset: int = Query(0, ge=0), verified_only: bool = False, category: Optional[str] = None, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): q = select(LessonLearned) if verified_only: q = q.where(LessonLearned.is_verified == True) if category: q = q.where(LessonLearned.category == category) q = q.order_by(desc(LessonLearned.confidence_score)).offset(offset).limit(limit) rows = (await db.execute(q)).scalars().all() return [ { "id": l.id, "lesson_id": l.lesson_id, "title": l.title, "category": l.category, "problem_pattern": l.problem_pattern, "root_cause": l.root_cause, "effective_solution": l.effective_solution, "prevention": l.prevention, "confidence_score": l.confidence_score, "is_verified": l.is_verified, "created_at": l.created_at.isoformat() if l.created_at else None, } for l in rows ] # ── 적응형 임계값 ────────────────────────────────────────────────────────────── @router.get("/thresholds") async def list_thresholds( source: Optional[str] = None, metric_type: Optional[str] = None, adapted_only: bool = False, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): q = select(AdaptiveThreshold) if source: q = q.where(AdaptiveThreshold.source == source) if metric_type: q = q.where(AdaptiveThreshold.metric_type == metric_type) if adapted_only: q = q.where(AdaptiveThreshold.adaptation_count > 0) q = q.order_by(desc(AdaptiveThreshold.adaptation_count)).limit(200) rows = (await db.execute(q)).scalars().all() return [ { "id": t.id, "source": t.source, "metric_type": t.metric_type, "base_threshold": t.base_threshold, "adapted_threshold": t.adapted_threshold, "change_pct": round( (t.adapted_threshold - t.base_threshold) / t.base_threshold * 100, 1 ) if t.base_threshold else 0, "true_positive": t.true_positive, "false_positive": t.false_positive, "missed_count": t.missed_count, "adaptation_count": t.adaptation_count, "last_adapted_at": t.last_adapted_at.isoformat() if t.last_adapted_at else None, } for t in rows ] @router.post("/thresholds/outcome") async def record_outcome( body: OutcomeRecord, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): from core.learning import record_anomaly_outcome return await record_anomaly_outcome( db, body.source, body.metric_type, body.was_actual_incident, body.base_threshold, ) @router.post("/thresholds/missed") async def record_missed( body: MissedRecord, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): from core.learning import record_missed_detection return await record_missed_detection(db, body.source, body.metric_type) @router.post("/thresholds/calibrate") async def calibrate( body: CalibrateRequest, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): from core.learning import calibrate_threshold return await calibrate_threshold(db, body.source, body.metric_type) @router.post("/thresholds/calibrate-all") async def calibrate_all( db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): """모든 적응형 임계값 일괄 보정.""" rows = (await db.execute(select(AdaptiveThreshold))).scalars().all() results = [] from core.learning import calibrate_threshold for rec in rows: r = await calibrate_threshold(db, rec.source, rec.metric_type) if r.get("adjusted"): results.append(r) return {"calibrated": len(results), "results": results} # ── 패턴 마이닝 ──────────────────────────────────────────────────────────────── @router.post("/mine") async def mine_lessons( body: MineRequest, db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): from core.learning import run_lesson_mining return await run_lesson_mining(db, body.days_back, body.min_occurrences)