Files
ewoooc/templates/admin/ai_calls_dashboard.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

312 lines
13 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 %}AI 呼叫總覽{% endblock %}
{% block ewooo_content %}
<div class="container-fluid mt-3">
<h2 class="mb-3"><i class="fas fa-chart-bar me-2"></i>AI 呼叫總覽
<small class="text-muted">過去 {{ hours }} 小時</small>
</h2>
{% if error %}
<div class="alert alert-warning"><strong><i class="fas fa-exclamation-triangle me-1"></i></strong> {{ error }}</div>
{% endif %}
<!-- Phase 40 D-7: 一鍵觸發 Code Review (L2 自動化) -->
<div class="mb-3">
<button class="btn btn-warning btn-sm" onclick="triggerCodeReview()">
<i class="fas fa-microscope me-1"></i>觸發 Code Review Pipeline (5 step)
</button>
<small class="text-muted ms-2">
看到錯誤率飆高?一鍵觸發 Hermes→OpenClaw→EA→NemoTron 5 步審查最新 commit
在背景執行,完成後 Telegram 通知。
</small>
</div>
<!-- 篩選 bar -->
<form method="get" class="row g-2 mb-3">
<div class="col-auto">
<select name="hours" class="form-select form-select-sm">
{% for h in [1, 6, 24, 72, 168] %}
<option value="{{ h }}" {% if hours == h %}selected{% endif %}>
{% if h < 24 %}{{ h }} 小時{% else %}{{ (h//24) }} {% endif %}
</option>
{% endfor %}
</select>
</div>
<div class="col-auto">
<select name="caller" class="form-select form-select-sm">
<option value="">全部呼叫端</option>
{% for c in callers %}
<option value="{{ c }}" {% if caller_filter == c %}selected{% endif %}>{{ c }}</option>
{% endfor %}
</select>
</div>
<div class="col-auto">
<select name="provider" class="form-select form-select-sm">
<option value="">全部供應商</option>
{% for p in ['gcp_ollama','ollama_secondary','ollama_111','gemini','claude','nim','openrouter','nim_via_elephant'] %}
<option value="{{ p }}" {% if provider_filter == p %}selected{% endif %}>{{ p }}</option>
{% endfor %}
</select>
</div>
<div class="col-auto">
<button class="btn btn-primary btn-sm">篩選</button>
</div>
</form>
<!-- 總覽 KPI -->
<div class="row g-2 mb-3">
<div class="col-lg-2 col-md-4 col-sm-6"><div class="card p-2"><small>呼叫次數</small><h4>{{ "{:,}".format(summary.total_calls or 0) }}</h4></div></div>
<div class="col-lg-2 col-md-4 col-sm-6"><div class="card p-2"><small>Token 用量</small><h4>{{ "{:,}".format(summary.total_tokens or 0) }}</h4></div></div>
<div class="col-lg-2 col-md-4 col-sm-6"><div class="card p-2"><small>成本 (USD)</small><h4>${{ "%.2f"|format(summary.total_cost or 0) }}</h4></div></div>
<div class="col-lg-2 col-md-4 col-sm-6"><div class="card p-2"><small>平均耗時</small><h4>{{ summary.avg_duration or 0 }} ms</h4></div></div>
<div class="col-lg-2 col-md-4 col-sm-6"><div class="card p-2"><small>RAG 命中</small><h4 class="text-success">{{ summary.rag_hits or 0 }}</h4></div></div>
<div class="col-lg-2 col-md-4 col-sm-6"><div class="card p-2"><small>錯誤次數</small><h4 class="{% if (summary.error_calls or 0) > 0 %}text-danger{% endif %}">{{ summary.error_calls or 0 }}</h4></div></div>
</div>
<!-- Phase 39 D-3: caller × RAG × MCP 編排矩陣 -->
{% if caller_richness %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-network-wired me-2"></i>呼叫端 × RAG × MCP 編排矩陣</strong>
<small class="text-muted">資料來源ai_calls × mcp_calls × rag_query_log{{ hours }}h 內呼叫 ≥ 5 次的 caller</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0">
<thead class="table-light">
<tr>
<th>呼叫端</th>
<th class="text-end">總呼叫</th>
<th class="text-end">RAG 命中率</th>
<th class="text-end">MCP 編排率</th>
<th class="text-end">RAG 反饋分數</th>
<th class="text-end">反饋筆數</th>
</tr>
</thead>
<tbody>
{% for c in caller_richness %}
<tr>
<td><code>{{ c.caller }}</code></td>
<td class="text-end">{{ "{:,}".format(c.total_calls) }}</td>
<td class="text-end">
<strong class="{% if c.rag_hit_rate >= 50 %}text-success{% elif c.rag_hit_rate >= 20 %}text-warning{% else %}text-muted{% endif %}">
{{ "%.1f"|format(c.rag_hit_rate) }}%
</strong>
<small class="text-muted">({{ c.rag_hits }})</small>
</td>
<td class="text-end">
<strong class="{% if c.mcp_rate >= 30 %}text-info{% elif c.mcp_rate >= 10 %}text-warning{% endif %}">
{{ "%.1f"|format(c.mcp_rate) }}%
</strong>
<small class="text-muted">({{ c.mcp_orchestrated }})</small>
</td>
<td class="text-end">
{% if c.feedback_count > 0 %}
<strong class="{% if c.avg_rag_feedback >= 4 %}text-success{% elif c.avg_rag_feedback >= 3 %}text-warning{% else %}text-danger{% endif %}">
{{ "%.2f"|format(c.avg_rag_feedback) }}/5
</strong>
{% else %}
<small class="text-muted"></small>
{% endif %}
</td>
<td class="text-end">{{ c.feedback_count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card-footer small text-muted">
<i class="fas fa-info-circle me-1"></i>
<strong>RAG 命中率</strong>caller 在此期間有多少呼叫吃到 RAG 召回;
<strong>MCP 編排率</strong>:有多少呼叫透過 request_id 串接到 MCP tool
<strong>反饋分數</strong>RAG 召回後人工反饋1-5
</div>
</div>
{% endif %}
<!-- Phase 47 K-2: 24h 每小時呼叫趨勢 -->
{% if hourly_trend %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-chart-area me-2"></i>過去 24h 每小時呼叫趨勢</strong>
<small class="text-muted">每小時 bucket呼叫數 · 成本 · 錯誤</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0" style="font-size: 0.85em;">
<thead class="table-light">
<tr>
<th>時段</th><th>呼叫數</th><th>成本 USD</th><th>錯誤</th><th style="width: 50%;">流量分布</th>
</tr>
</thead>
<tbody>
{% set max_calls = (hourly_trend | map(attribute='calls') | max) or 1 %}
{% for h in hourly_trend %}
<tr>
<td><code>{{ h.hour }}</code></td>
<td><strong>{{ "{:,}".format(h.calls) }}</strong></td>
<td>${{ "%.3f"|format(h.cost) }}</td>
<td>
{% if h.errors > 0 %}<span class="text-danger">{{ h.errors }}</span>
{% else %}<small class="text-muted">0</small>{% endif %}
</td>
<td>
<div class="progress" style="height: 8px;">
<div class="progress-bar {% if h.errors > 0 %}bg-warning{% else %}bg-info{% endif %}"
style="width: {{ (h.calls / max_calls * 100) | round | int }}%"></div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- Phase 47 K-2: by model 細分 -->
{% if by_model %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-microchip me-2"></i>依模型細分</strong>
<small class="text-muted">資料來源ai_calls.model細到模型版本— Top 15</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 class="text-end">呼叫</th><th class="text-end">Token</th>
<th class="text-end">成本 USD</th><th class="text-end">平均耗時</th>
<th class="text-end">錯誤</th>
</tr>
</thead>
<tbody>
{% for m in by_model %}
<tr>
<td><code>{{ m.model[:35] }}</code></td>
<td><span class="badge bg-secondary">{{ m.provider }}</span></td>
<td class="text-end">{{ "{:,}".format(m.calls) }}</td>
<td class="text-end">{{ "{:,}".format(m.tokens) }}</td>
<td class="text-end">${{ "%.4f"|format(m.cost) }}</td>
<td class="text-end">{{ m.avg_ms }} ms</td>
<td class="text-end">
{% if m.errors > 0 %}<span class="text-danger">{{ m.errors }}</span>
{% else %}<small class="text-muted">0</small>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- Phase 47 K-2: agent_context 最近 10 筆 -->
{% if recent_contexts %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-comments-dollar me-2"></i>Agent 對話上下文(最近 10 筆)</strong>
<small class="text-muted">資料來源agent_contextHermes/OpenClaw/NemoTron 工作 session</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0" style="font-size: 0.85em;">
<thead class="table-light">
<tr>
<th>時間</th><th>Agent</th><th>Key</th>
<th class="text-end">TTL min</th><th>預覽</th>
</tr>
</thead>
<tbody>
{% for c in recent_contexts %}
<tr>
<td><small>{{ c.created_at }}</small></td>
<td><span class="badge bg-info text-dark">{{ c.agent_name }}</span></td>
<td><code>{{ c.context_key }}</code></td>
<td class="text-end">{{ c.ttl_minutes }}</td>
<td><small class="text-muted">{{ c.preview }}{% if c.preview|length >= 120 %}…{% endif %}</small></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- by provider -->
<div class="card mb-3">
<div class="card-header"><strong>依供應商分組</strong></div>
<div class="card-body p-0">
<table class="table table-sm mb-0">
<thead class="table-light">
<tr><th>供應商</th><th class="text-end">呼叫數</th><th class="text-end">Token 用量</th><th class="text-end">成本 (USD)</th></tr>
</thead>
<tbody>
{% for row in by_provider %}
<tr>
<td><code>{{ row.provider }}</code></td>
<td class="text-end">{{ "{:,}".format(row.calls) }}</td>
<td class="text-end">{{ "{:,}".format(row.tokens) }}</td>
<td class="text-end">${{ "%.2f"|format(row.cost) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- recent calls -->
<div class="card">
<div class="card-header"><strong>最近呼叫(最新 100 筆)</strong></div>
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0" style="font-size: 0.85em;">
<thead class="table-light">
<tr>
<th>編號</th><th>時間</th><th>呼叫端</th><th>供應商</th><th>模型</th>
<th class="text-end">輸入</th><th class="text-end">輸出</th><th class="text-end">耗時 ms</th>
<th>狀態</th><th class="text-end">成本 $</th><th>標記</th>
</tr>
</thead>
<tbody>
{% for r in recent %}
<tr {% if r.status not in ['ok','cache_only'] %}class="table-warning"{% endif %}>
<td>{{ r.id }}</td>
<td><small>{{ r.called_at }}</small></td>
<td><code>{{ r.caller }}</code></td>
<td><small>{{ r.provider }}</small></td>
<td><small>{{ r.model[:25] }}</small></td>
<td class="text-end">{{ r.in_tokens }}</td>
<td class="text-end">{{ r.out_tokens }}</td>
<td class="text-end">{{ r.duration_ms }}</td>
<td><small>{{ r.status }}</small></td>
<td class="text-end">${{ "%.4f"|format(r.cost) }}</td>
<td>
{% if r.cache_hit %}<span class="badge bg-success">快取</span>{% endif %}
{% if r.rag_hit %}<span class="badge bg-info">RAG</span>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<p class="text-muted mt-2"><small>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 40 — AI 呼叫總覽(含 RAG/MCP 編排矩陣)
</small></p>
</div>
<script>
async function triggerCodeReview() {
if (!confirm('觸發 Code Review Pipeline\n\n會對最新 commit 跑 5 step 審查Hermes 掃描 → OpenClaw 摘要 → EA 決策 → NemoTron 行動),背景執行。')) return;
try {
const r = await fetch('/observability/ai_calls/trigger_code_review', {method: 'POST'});
const d = await r.json();
if (d.ok) {
alert(`${d.message}\n\nPipeline ID: ${d.pipeline_id}\nCommit: ${d.commit_sha}\n變更檔案: ${d.changed_files_count}`);
} else {
alert('❌ ' + (d.error || '觸發失敗'));
}
} catch (e) {
alert('Error: ' + e);
}
}
</script>
{% endblock %}