diff --git a/app/api/dashboard.py b/app/api/dashboard.py index 777f67e..d591437 100644 --- a/app/api/dashboard.py +++ b/app/api/dashboard.py @@ -49,6 +49,22 @@ class QueueLag(BaseModel): oldest_pending_age_sec: int | None # 가장 오래된 pending 의 created_at 기준 경과 (초) +class TierHealthStack(BaseModel): + """PR-B B-3 — tier 관측성 3종 카드 소스 (24h 윈도우). + + 대시보드 카드: + - "에스컬레이션 비율": escalated_total / triage_total (>20% 적색, <1% 회색) + - "triage JSON 건강도": triage_json_invalid / triage_total (>5% 적색) + - "Backlog Suppression": suppressed_total / triage_total (>10% 주황) + """ + triage_total: int = 0 + escalated_total: int = 0 + escalation_by_reason: dict[str, int] = {} # long_context / low_confidence / deep_requested / self_declare + escalation_by_domain: dict[str, int] = {} # safety_reference / news_item / ... + triage_json_invalid: int = 0 # error_code='triage_json_invalid' + suppressed_total: int = 0 # suppressed_reason IS NOT NULL + + class DashboardResponse(BaseModel): today_added: int today_by_domain: list[DomainCount] @@ -66,6 +82,8 @@ class DashboardResponse(BaseModel): category_counts: dict[str, int] = {} library_pending_suggestions: int = 0 queue_lag: list[QueueLag] = [] + # PR-B B-3 — tier 관측성 + tier_health: TierHealthStack = TierHealthStack() @router.get("/", response_model=DashboardResponse) @@ -198,6 +216,47 @@ async def get_dashboard( for row in lag_result.all() ] + # ─── PR-B B-3 — tier 관측성 (24h) ─── + tier_rows = (await session.execute(text(""" + SELECT + COUNT(*) FILTER (WHERE mode = 'summary_triage') AS triage_total, + COUNT(*) FILTER (WHERE mode = 'summary_triage' AND escalated_to_26b = true) AS escalated_total, + COUNT(*) FILTER (WHERE mode = 'summary_triage' AND error_code = 'triage_json_invalid') AS json_invalid, + COUNT(*) FILTER (WHERE mode = 'summary_triage' AND suppressed_reason IS NOT NULL) AS suppressed_total + FROM analyze_events + WHERE created_at > NOW() - INTERVAL '24 hours' + """))).one() + + reason_rows = await session.execute(text(""" + SELECT unnest(escalation_reasons) AS reason, COUNT(*) AS n + FROM analyze_events + WHERE created_at > NOW() - INTERVAL '24 hours' + AND mode = 'summary_triage' + AND escalated_to_26b = true + GROUP BY 1 ORDER BY 2 DESC + """)) + escalation_by_reason = {r[0]: r[1] for r in reason_rows if r[0]} + + domain_rows = await session.execute(text(""" + SELECT subject_domain, COUNT(*) AS n + FROM analyze_events + WHERE created_at > NOW() - INTERVAL '24 hours' + AND mode = 'summary_triage' + AND escalated_to_26b = true + AND subject_domain IS NOT NULL + GROUP BY 1 ORDER BY 2 DESC + """)) + escalation_by_domain = {r[0]: r[1] for r in domain_rows} + + tier_health = TierHealthStack( + triage_total=int(tier_rows.triage_total or 0), + escalated_total=int(tier_rows.escalated_total or 0), + triage_json_invalid=int(tier_rows.json_invalid or 0), + suppressed_total=int(tier_rows.suppressed_total or 0), + escalation_by_reason=escalation_by_reason, + escalation_by_domain=escalation_by_domain, + ) + return DashboardResponse( today_added=today_added, today_by_domain=[ @@ -227,4 +286,5 @@ async def get_dashboard( category_counts=category_counts, library_pending_suggestions=library_pending_suggestions, queue_lag=queue_lag, + tier_health=tier_health, ) diff --git a/frontend/src/lib/components/AnalysisPanel.svelte b/frontend/src/lib/components/AnalysisPanel.svelte index 3fed308..557c644 100644 --- a/frontend/src/lib/components/AnalysisPanel.svelte +++ b/frontend/src/lib/components/AnalysisPanel.svelte @@ -1,36 +1,69 @@
+ {tldr} +
+ {/if} + + {#if bullets && bullets.length > 0} ++ 상세 +
++ {detail} +
++ 상세 요약 대기 중 (긴 문서/핵심 결정 문서일 때만 26B 생성). +
+ {/if} + + {#if inconsistencies && inconsistencies.length > 0} ++ 일관성 이슈 +
+ {#each inconsistencies as it} + {@const Icon = INC_ICON[it.kind] ?? HelpCircle} +
- 문서 앞부분(최대 12,000자)을 Gemma 4로 구조화합니다. 약 10~40초 소요.
-
- ※ 전체 분석은 추후 제공 예정.
+ 문서 앞부분(최대 12,000자)을 4층 구조 (evidence / explanation / examples /
+ summary) 로 구조화합니다. 약 10~40초 소요.
에스컬레이션 비율 (24h)
++ {(esc_rate * 100).toFixed(1)}% +
++ {th.escalated_total} / {th.triage_total} + {#if esc_rate > 0.20}(튜닝 필요){/if} + {#if esc_rate < 0.01}(false negative?){/if} +
+ {#if Object.keys(th.escalation_by_reason).length > 0} +triage JSON 건강도 (24h)
++ {(json_rate * 100).toFixed(1)}% +
++ 깨짐 {th.triage_json_invalid} 건 + {#if json_rate > 0.05}(프롬프트 이슈 의심){/if} +
+5% 초과 시 4B 프롬프트·모델 재검토
+Backlog Suppression (24h)
++ {(sup_rate * 100).toFixed(1)}% +
++ 억제 {th.suppressed_total} 건 + {#if sup_rate > 0.10}(임계치 재조정 신호){/if} +
+10% 초과 시 ratio/pending threshold 조정
+