Files
ewoooc/templates/admin/promotion_review.html
OoO 2e124db602
All checks were successful
CD Pipeline / deploy (push) Successful in 2m36s
feat(p47): 6 頁深挖資料庫 — 從 5 表 → 17 表,每頁加 3-5 個 widget
統帥質疑:「6 頁內容太空洞,要更貼近資料庫裡所有數據」
盤點:DB 有 22 表,6 頁原本只用 5 表(22.7% 利用率)。
本 commit 新接 12 張既有但未用的表。

K-1 host_health(接 +5 表 → 8 表)
- 加 incidents 最近 10 筆詳細(task_name/error_type/嚴重度/狀態/重試/錯誤訊息)
- 加 heal_logs 最近 10 筆(action_type/result/耗時/incident 關聯)
- 加 playbooks 庫排行 TOP 12(success_count/fail_count/成功率/啟用狀態)
- 加 backup_log 7 日歷史(type/status/size/duration/error)
- 加 embedding_retry_queue pending/failed 警示

K-2 ai_calls(接 +3 表)
- 加 24h 每小時呼叫趨勢 bucket(含成本+錯誤+流量分布條)
- 加 by model 細分(不只 provider,到 model 版本級別)TOP 15
- 加 agent_context 最近 10 筆(OpenClaw/Hermes 對話 session preview)

K-3 budget(接 +3 表)
- 加當月 Top 5 燒錢呼叫端(caller × cost ranking)
- 加過去 30 日每日成本 by provider 趨勢表
- 加 ai_price_recommendations 7 日統計(strategy 分布 + 平均信心度)

K-4 promotion_review(接 +2 表)
- 加蒸餾池 30 日 status 分布(不只 awaiting,看 8 種完整流動)
- 加 ai_insights 最近 10 筆已晉升內容預覽
- 加 agent_strategy_weights TOP 12(OpenClaw 學習權重 + 成功率)

K-5 quality_trend(接 +3 表)
- 加 RAG 整體 feedback 1-5 分分布(過去 N 日,星等視覺)
- 加 action_plans status 分布(pending/approved/executed/rejected)
- 加 action_outcomes verdict 分布(effective/neutral/backfired
  — ADR-012 閉環學習核心 KPI)

K-6 ppt_audit_history(接 +0 表,但 deeper SQL)
- 加 30 日統計卡(total/passed/failed/error/通過率/總 issue 數/平均信心度)
- 加 Top 10 反覆失敗檔案(30d 失敗次數 + total issues)
- 加 empty state 說明(PPT_VISION_ENABLED=false 時顯示啟用步驟)

DB 利用率對應:
- Phase 38 起點:5 表(22.7%)
- Phase 39-46 累計:12 表(54.5%)
- Phase 47 收官:17 表(77.3%)
  新接:playbooks / backup_log / embedding_retry_queue / agent_context /
        ai_price_recommendations / agent_strategy_weights / action_plans /
        action_outcomes(之前已接:incidents/heal_logs/ai_insights/learning_episodes/
        rag_query_log/mcp_calls/ai_calls/host_health_probes/ppt_audit_results)

每頁 widget 數對應:
- host_health: 5 卡 → 11 卡
- ai_calls: 5 卡 → 8 卡
- budget: 4 卡 → 7 卡
- promotion_review: 2 卡 → 5 卡
- quality_trend: 4 卡 → 7 卡
- ppt_audit: 3 卡 → 6 卡

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

228 lines
10 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 "ewoooc_base.html" %}
{% block title %}RAG 學習晉升審核{% endblock %}
{% block ewooo_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 %}
<!-- Phase 47 K-4: 蒸餾池 30d 分布 -->
{% if episode_distribution_30d %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-flask me-2"></i>蒸餾池 30 日狀態分布</strong>
<small class="text-muted">資料來源learning_episodes不只看 awaiting看完整流動</small>
</div>
<div class="card-body">
<div class="row g-2">
{% for status, cnt in episode_distribution_30d.items() %}
<div class="col-md-2 col-sm-4">
<div class="border rounded p-2 text-center">
<small class="text-muted d-block">
{% if status == 'pending' %}<i class="fas fa-hourglass-start"></i> 待處理
{% elif status == 'awaiting_review' %}<i class="fas fa-user-clock"></i> 待審核
{% elif status == 'approved' %}<i class="fas fa-check-circle text-success"></i> 已晉升
{% elif status == 'rejected_quality' %}<i class="fas fa-times text-danger"></i> 品質拒
{% elif status == 'rejected_hallucination' %}<i class="fas fa-times text-danger"></i> 幻覺拒
{% elif status == 'rejected_duplicate' %}<i class="fas fa-clone text-warning"></i> 重複拒
{% elif status == 'rejected_human' %}<i class="fas fa-user-times text-danger"></i> 人工拒
{% elif status == 'expired' %}<i class="fas fa-clock text-muted"></i> 已過期
{% else %}{{ status }}{% endif %}
</small>
<strong style="font-size: 1.4em;">{{ cnt }}</strong>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<!-- Phase 47 K-4: 最近 10 筆已晉升 ai_insights -->
{% if latest_insights %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-book me-2"></i>知識庫最近 10 筆 ai_insights</strong>
<small class="text-muted">資料來源ai_insights — 已晉升內容預覽</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0" style="font-size: 0.9em;">
<thead class="table-light">
<tr><th>#</th><th>類型</th><th>期間</th><th>SKU</th><th>建立時間</th><th>預覽</th></tr>
</thead>
<tbody>
{% for i in latest_insights %}
<tr>
<td><code>#{{ i.id }}</code></td>
<td><span class="badge bg-info text-dark">{{ i.insight_type }}</span></td>
<td><small>{{ i.period or '—' }}</small></td>
<td><small>{{ i.product_sku or '—' }}</small></td>
<td><small>{{ i.created_at }}</small></td>
<td><small class="text-muted">{{ i.preview }}{% if i.preview|length >= 160 %}…{% endif %}</small></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- Phase 47 K-4: agent_strategy_weights TOP 12 -->
{% if strategy_weights %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-balance-scale me-2"></i>OpenClaw 學習策略權重 TOP 12</strong>
<small class="text-muted">資料來源agent_strategy_weightsADR-012 closed-loop learning</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0" style="font-size: 0.9em;">
<thead class="table-light">
<tr>
<th>策略 Key</th>
<th class="text-end">權重</th>
<th class="text-end">成功</th>
<th class="text-end">失敗</th>
<th class="text-end">成功率</th>
<th>更新時間</th>
</tr>
</thead>
<tbody>
{% for s in strategy_weights %}
<tr>
<td><code>{{ s.strategy_key }}</code></td>
<td class="text-end"><strong>{{ "%.2f"|format(s.weight) }}</strong></td>
<td class="text-end text-success">{{ s.success }}</td>
<td class="text-end text-danger">{{ s.fail }}</td>
<td class="text-end">
{% if (s.success + s.fail) > 0 %}
<span class="{% if s.success_rate >= 70 %}text-success{% elif s.success_rate >= 40 %}text-warning{% else %}text-danger{% endif %}">
{{ "%.0f"|format(s.success_rate) }}%
</span>
{% else %}<small class="text-muted"></small>{% endif %}
</td>
<td><small>{{ s.updated_at }}</small></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<p class="text-muted mt-3"><small>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 47 — RAG 學習晉升審核
4 表深挖learning_episodes / ai_insights / agent_strategy_weights / rag_query_log
</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 %}