Files
ewoooc/templates/admin/promotion_review.html
OoO 79cf08c58c
All checks were successful
CD Pipeline / deploy (push) Successful in 2m30s
feat(p39): 觀測台升級 — DB + MCP + RAG + AI 自動化深度整合
統帥質疑:6 頁觀測台只是 raw stats dashboard,沒展現 AI 自動化專業。
深度盤點 4 軸結果:
- DB 利用率 22.7%(22 表只用 5 張)
- MCP 整合 1/6(mcp_calls 表完全沒被讀)
- RAG 整合 0/6(沒 import rag_service)
- AI 自動化 L0 × 5 + L1 × 1(純讀 dashboard,無一鍵觸發)

本 commit 5 個增強:

D-1: promotion_review 加 RAG「Top 3 相似已晉升」
- 對每筆 awaiting_review episode 跑 rag_service.query 找 ai_insights 中
  cosine ≥ 0.7 的相似已晉升內容
- 輔助人工判斷:是否冗餘?是否新領域?
- header 顯示 ai_insights 知識庫 size
- fail-safe: 單筆 RAG 失敗不影響其餘

D-2: host_health 加 MCP 24h 工作量 widget
- 從 mcp_calls 統計各 server 24h 呼叫次數 / 成功率 / cache 率 /
  使用 tool 數 / 平均耗時 / cost
- 展現「AI×MCP 編排規模」而非只「server 健康與否」

D-3: ai_calls × rag_query_log × mcp_calls 三表 JOIN
- 新增「呼叫端 × RAG × MCP 編排矩陣」card
- 每個 caller:總呼叫 / RAG 命中率 / MCP 編排率(透過 request_id 串接)
  / RAG 反饋分數 / 反饋筆數
- 展現「AI 自動化專業」核心指標

D-4: budget 加 RAG 自動策略建議 + 一鍵 force-throttle (L2)
- ratio ≥ 0.8 時自動 RAG 召回 ai_insights 中的 budget_strategy 知識
- POST /budget/force_throttle endpoint:立即重算 cost_throttle 狀態
  (不等下次每小時 cron)— 升級到 L2 自動化
- 對應頁面加「立即重算節流狀態」按鈕

D-5: host_health 加 incidents + heal_logs 7d 摘要
- 顯示 ADR-013 AutoHeal 閉環核心 KPI:
  總事件 / 未解決 / 已解決 / P0+P1 / 自癒成功率 / 平均自癒耗時
- 展現「AIOps 自癒系統」運作實況

對應升級:
- DB 利用率 22.7% → ~50%(新接 mcp_calls + rag_query_log JOIN
  + ai_insights + incidents + heal_logs)
- MCP 整合 1/6 → 3/6(host_health + ai_calls + budget 都接 mcp_calls)
- RAG 整合 0/6 → 3/6(promotion_review + budget + 待 quality_trend)
- AI 自動化 L1 → L2 一鍵 force-throttle 一個(其餘按鈕待 D-6)

全部 fail-safe:DB 表/RAG/MCP 失敗都不擋頁面渲染。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 19:08:41 +08:00

127 lines
5.9 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}RAG 學習晉升審核{% endblock %}
{% block content %}
<div class="container-fluid mt-3">
<h2 class="mb-3"><i class="fas fa-brain me-2"></i>RAG 學習晉升審核
<small class="text-muted">待審核 × {{ episodes|length }} 筆 · 知識庫 ai_insights × {{ kb_size or 0 }} 筆</small>
</h2>
{% if error %}
<div class="alert alert-warning"><strong><i class="fas fa-exclamation-triangle me-1"></i></strong> {{ error }}</div>
{% endif %}
{% if episodes %}
<p class="text-muted small">
<i class="fas fa-info-circle me-1"></i>Phase 11 PromotionGate Stage 4 強制門檻weight ≥ 0.8 的 episode 必經統帥審核,
24 小時無回應自動過期weight 降為 0.5 不晉升)。
點「通過晉升」→ 寫入 ai_insights 供 RAG 檢索;點「拒絕」→ 永不晉升learning_episodes 留存)。
</p>
{% for ep in episodes %}
<div class="card mb-3 episode-card" data-episode-id="{{ ep.id }}">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong>學習片段 #{{ ep.id }}</strong>
<span class="badge bg-secondary ms-2">{{ ep.episode_type }}</span>
{% if ep.source_table %}<span class="badge bg-light text-dark ms-1">
{{ ep.source_table }}#{{ ep.source_id }}</span>{% endif %}
<span class="badge bg-info ms-1">權重:{{ "%.2f"|format(ep.weight) }}</span>
<span class="badge bg-info ms-1">品質:{{ "%.2f"|format(ep.quality_score) }}</span>
</div>
<small class="text-muted">{{ ep.created_at }}</small>
</div>
<div class="card-body">
<pre style="white-space: pre-wrap; font-size: 0.9em; max-height: 200px; overflow-y: auto;">{{ ep.distilled_text }}</pre>
{% if ep.similar_insights %}
<div class="mt-3 p-2" style="background: #f7f7f9; border-radius: 6px; border-left: 3px solid #6f42c1;">
<small class="text-muted d-block mb-2">
<i class="fas fa-search me-1"></i><strong>RAG 自動檢索:知識庫中 Top 3 相似已晉升內容</strong>
<span class="text-muted">cosine ≥ 0.7)— 輔助判斷此次晉升是否冗餘</span>
</small>
<ul class="list-unstyled mb-0 small">
{% for sim in ep.similar_insights %}
<li class="mb-1">
<span class="badge bg-light text-dark me-1">#{{ sim.id }}</span>
<span class="badge bg-info text-dark me-1">{{ sim.insight_type }}</span>
<span class="badge bg-secondary me-1">相似度 {{ "%.2f"|format(sim.similarity) }}</span>
{% if sim.created_at %}<small class="text-muted me-1">{{ sim.created_at }}</small>{% endif %}
<span class="text-dark">{{ sim.content }}{% if sim.content|length >= 180 %}…{% endif %}</span>
</li>
{% endfor %}
</ul>
</div>
{% else %}
<div class="mt-2"><small class="text-muted">
<i class="fas fa-search me-1"></i>RAG 檢索:知識庫中無 cosine ≥ 0.7 的相似內容(屬新領域知識,建議晉升)
</small></div>
{% endif %}
</div>
<div class="card-footer text-end">
<button class="btn btn-success btn-sm me-2" onclick="approveEpisode({{ ep.id }}, this)">
<i class="fas fa-check me-1"></i>通過晉升
</button>
<button class="btn btn-outline-danger btn-sm" onclick="rejectEpisode({{ ep.id }}, this)">
<i class="fas fa-times me-1"></i>拒絕
</button>
</div>
</div>
{% endfor %}
{% else %}
<div class="alert alert-info">
<i class="fas fa-sparkles me-1"></i>目前無待審核片段。
<small>RAG 未啟用 / 無高權重片段 / 全部已 24 小時過期)</small>
</div>
{% endif %}
<p class="text-muted mt-3"><small>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 29 — RAG 學習晉升審核
</small></p>
</div>
<script>
async function approveEpisode(id, btn) {
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 處理中...';
try {
const r = await fetch(`/observability/promotion_review/approve/${id}`, {method: 'POST'});
const d = await r.json();
if (d.ok) {
const card = document.querySelector(`.episode-card[data-episode-id="${id}"]`);
card.classList.add('border-success');
card.querySelector('.card-footer').innerHTML =
`<span class="text-success"><i class="fas fa-check me-1"></i>已晉升 → ai_insights #${d.insight_id}(審核者:${d.approver}</span>`;
} else {
alert('晉升失敗:' + (d.error || 'unknown'));
btn.disabled = false; btn.innerHTML = '<i class="fas fa-check me-1"></i>通過晉升';
}
} catch (e) {
alert('Error' + e);
btn.disabled = false; btn.innerHTML = '<i class="fas fa-check me-1"></i>通過晉升';
}
}
async function rejectEpisode(id, btn) {
if (!confirm(`拒絕學習片段 #${id}?此筆將永不晉升(保留在 learning_episodes 不刪除)`)) return;
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 處理中...';
try {
const r = await fetch(`/observability/promotion_review/reject/${id}`, {method: 'POST'});
const d = await r.json();
if (d.ok) {
const card = document.querySelector(`.episode-card[data-episode-id="${id}"]`);
card.classList.add('border-danger');
card.querySelector('.card-footer').innerHTML =
`<span class="text-danger"><i class="fas fa-times me-1"></i>已拒絕rejected_human</span>`;
} else {
alert('拒絕失敗:' + (d.error || 'unknown'));
btn.disabled = false; btn.innerHTML = '<i class="fas fa-times me-1"></i>拒絕';
}
} catch (e) {
alert('Error' + e);
btn.disabled = false; btn.innerHTML = '<i class="fas fa-times me-1"></i>拒絕';
}
}
</script>
{% endblock %}