170 lines
6.4 KiB
Python
170 lines
6.4 KiB
Python
"""독립 지원 — 자립도 점수 측정·추적·로드맵."""
|
|
from __future__ import annotations
|
|
import logging
|
|
from datetime import datetime, timedelta
|
|
from fastapi import APIRouter, BackgroundTasks, Depends
|
|
from sqlalchemy import select, desc, func as sqlfunc
|
|
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, IndependenceScore, HealthCheckResult, AutonomousAction, SelfReport
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/independence", tags=["독립지원-자립도"])
|
|
|
|
# 자립도 차원별 가중치 (합산 100%)
|
|
DIMENSIONS = {
|
|
"health": 0.25, # 건강검진 자동화
|
|
"sr_auto": 0.30, # SR AI 자동처리율
|
|
"self_heal": 0.20, # 자가수복 성공률
|
|
"report": 0.10, # 자율 보고서 발송
|
|
"finetune": 0.15, # AI 파인튜닝 자동화
|
|
}
|
|
|
|
ROADMAP = [
|
|
{"milestone": "현재", "target": 30, "description": "대부분 수동 개입 필요"},
|
|
{"milestone": "3개월", "target": 50, "description": "SR의 절반 AI 자동처리"},
|
|
{"milestone": "6개월", "target": 70, "description": "장애 자동수복 + 자동 파인튜닝"},
|
|
{"milestone": "1년", "target": 85, "description": "GUARDiA가 알아서 다 했습니다"},
|
|
]
|
|
|
|
|
|
async def _measure_independence() -> dict:
|
|
"""각 차원별 점수 계산 → 자립도 점수 산출."""
|
|
from database import AsyncSessionLocal
|
|
async with AsyncSessionLocal() as db:
|
|
since = datetime.utcnow() - timedelta(days=30)
|
|
|
|
# 1. 건강검진 자동화 (최근 30일 실행 횟수 기준, 30회 이상 = 100점)
|
|
hc_count = (await db.execute(
|
|
select(sqlfunc.count()).select_from(HealthCheckResult)
|
|
.where(HealthCheckResult.created_at >= since,
|
|
HealthCheckResult.triggered_by.like("schedule%"))
|
|
)).scalar() or 0
|
|
health_score = min(hc_count / 30 * 100, 100)
|
|
|
|
# 2. SR 자동처리율
|
|
try:
|
|
from models import ServiceRequest
|
|
sr_total = (await db.execute(
|
|
select(sqlfunc.count()).select_from(ServiceRequest)
|
|
.where(ServiceRequest.created_at >= since)
|
|
)).scalar() or 0
|
|
sr_auto = (await db.execute(
|
|
select(sqlfunc.count()).select_from(ServiceRequest)
|
|
.where(ServiceRequest.created_at >= since, ServiceRequest.resolved_by_ai == True)
|
|
)).scalar() or 0
|
|
sr_score = (sr_auto / sr_total * 100) if sr_total else 0
|
|
except Exception:
|
|
sr_score = 0
|
|
|
|
# 3. 자가수복 성공률
|
|
heal_total = (await db.execute(
|
|
select(sqlfunc.count()).select_from(AutonomousAction)
|
|
.where(AutonomousAction.executed_at >= since, AutonomousAction.action_type == "restart")
|
|
)).scalar() or 0
|
|
heal_ok = (await db.execute(
|
|
select(sqlfunc.count()).select_from(AutonomousAction)
|
|
.where(AutonomousAction.executed_at >= since,
|
|
AutonomousAction.action_type == "restart",
|
|
AutonomousAction.success == True)
|
|
)).scalar() or 0
|
|
heal_score = (heal_ok / heal_total * 100) if heal_total else 0
|
|
|
|
# 4. 자율 보고서 발송 (월 4회 이상 = 100점)
|
|
report_count = (await db.execute(
|
|
select(sqlfunc.count()).select_from(SelfReport)
|
|
.where(SelfReport.created_at >= since)
|
|
)).scalar() or 0
|
|
report_score = min(report_count / 4 * 100, 100)
|
|
|
|
# 5. 파인튜닝 자동화 (최근 완료 작업 수)
|
|
from models import AutoFinetuneJob
|
|
ft_count = (await db.execute(
|
|
select(sqlfunc.count()).select_from(AutoFinetuneJob)
|
|
.where(AutoFinetuneJob.created_at >= since, AutoFinetuneJob.status == "success")
|
|
)).scalar() or 0
|
|
ft_score = min(ft_count / 1 * 100, 100)
|
|
|
|
dim_scores = {
|
|
"health": health_score,
|
|
"sr_auto": sr_score,
|
|
"self_heal": heal_score,
|
|
"report": report_score,
|
|
"finetune": ft_score,
|
|
}
|
|
overall = sum(dim_scores[d] * DIMENSIONS[d] for d in DIMENSIONS)
|
|
|
|
# 저장
|
|
entry = IndependenceScore(
|
|
score=round(overall, 2),
|
|
dimension="overall",
|
|
details=str(dim_scores),
|
|
target_score=85.0,
|
|
measured_at=datetime.utcnow(),
|
|
)
|
|
db.add(entry)
|
|
for dim, score in dim_scores.items():
|
|
db.add(IndependenceScore(
|
|
score=round(score, 2), dimension=dim,
|
|
target_score=100.0, measured_at=datetime.utcnow(),
|
|
))
|
|
await db.commit()
|
|
|
|
return {"overall": round(overall, 2), "dimensions": dim_scores}
|
|
|
|
|
|
@router.post("/measure")
|
|
async def measure(
|
|
background_tasks: BackgroundTasks,
|
|
user: User = Depends(require_admin_role),
|
|
):
|
|
"""자립도 즉시 측정."""
|
|
background_tasks.add_task(_measure_independence)
|
|
return {"ok": True, "message": "자립도 측정 시작됨 (백그라운드)"}
|
|
|
|
|
|
@router.get("/score")
|
|
async def get_score(
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
"""현재 자립도 점수."""
|
|
row = await db.execute(
|
|
select(IndependenceScore)
|
|
.where(IndependenceScore.dimension == "overall")
|
|
.order_by(desc(IndependenceScore.measured_at)).limit(1)
|
|
)
|
|
latest = row.scalar_one_or_none()
|
|
return {
|
|
"score": latest.score if latest else 30.0,
|
|
"target": latest.target_score if latest else 85.0,
|
|
"measured_at": latest.measured_at if latest else None,
|
|
"roadmap": ROADMAP,
|
|
}
|
|
|
|
|
|
@router.get("/history")
|
|
async def score_history(
|
|
limit: int = 30,
|
|
dimension: str = "overall",
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
"""자립도 추이 (차트용)."""
|
|
rows = await db.execute(
|
|
select(IndependenceScore)
|
|
.where(IndependenceScore.dimension == dimension)
|
|
.order_by(desc(IndependenceScore.measured_at)).limit(limit)
|
|
)
|
|
return [{
|
|
"score": s.score, "dimension": s.dimension,
|
|
"measured_at": s.measured_at,
|
|
} for s in reversed(rows.scalars().all())]
|
|
|
|
|
|
@router.get("/roadmap")
|
|
async def roadmap(user: User = Depends(get_current_user)):
|
|
"""자립도 로드맵."""
|
|
return {"roadmap": ROADMAP, "dimensions": DIMENSIONS}
|