All checks were successful
CD Pipeline / deploy (push) Successful in 2m30s
統帥質疑: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>
127 lines
5.9 KiB
HTML
127 lines
5.9 KiB
HTML
{% 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 %}
|