統一觀測台新版工作台規範
All checks were successful
CD Pipeline / deploy (push) Successful in 59s

This commit is contained in:
OoO
2026-05-13 19:39:33 +08:00
parent bc47f79a77
commit 250dd58172
15 changed files with 441 additions and 140 deletions

View File

@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.121"
SYSTEM_VERSION = "V10.122"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -25,7 +25,6 @@ WEB_CSS_PATH = Path("web/static/css/observability-system.css")
SHELL_PATH = Path("templates/components/_ewoooc_shell.html")
BASE_PATH = Path("templates/ewoooc_base.html")
ROUTE_PATH = Path("routes/admin_observability_routes.py")
OVERVIEW_PATH = Path("templates/admin/observability_overview.html")
@dataclass(frozen=True)
@@ -79,6 +78,7 @@ REQUIRED_CSS_SNIPPETS = [
"--obs-title-size",
"--obs-value-size",
"--obs-matrix-dot",
"v3.11 V2 workbench normalization",
"v3.10 terminal dot-matrix layer",
".momo-observability-mode",
".obs-chart-frame",
@@ -137,16 +137,55 @@ FORBIDDEN_BASE_PATTERNS = [
),
]
FORBIDDEN_OVERVIEW_COPY = [
FORBIDDEN_OBSERVABILITY_COPY = [
"AI Observability Command Room",
"Business Intelligence",
"Agent Command Matrix",
"AI Traffic Control",
"AI Cost Governance",
"RAG Recall Radar",
"Quality Diagnostics",
"PPT Visual QA Pipeline",
"RAG Promotion Gate",
"Infrastructure Lifeline",
"Risk Signals",
"AI Calls / 24h",
"Host Cascade",
"AI Runtime",
"Learning Loop",
">Command<",
">Runtime<",
">Quality<",
"Total Calls",
"Ollama Share",
"Paid Cost",
"Provider Split",
"Caller Orchestration",
"Model Economics",
"Recent Calls",
"Budget Ratio",
"Budget Lines",
"Provider Mix",
"Burn Rate",
"Saved Call",
"Query Stream",
"Caller Quality",
"Worst Avg",
"RAG Scores",
"Caller Feedback",
"RAG Feedback",
"Learning Pool",
"Root Cause",
"Action Outcomes",
"Audit History",
"Generated Files",
"30d Audit Mix",
"Failure Hotspots",
"Awaiting Review",
"Review Queue",
"Ollama Down",
"AIOps Open",
"Heal Rate",
"Cost Throttle",
"MCP Workload",
"Operation Ollama-First",
"RAG hits",
"Cache hits",
"30d episodes",
@@ -258,18 +297,20 @@ def scan_base_topbar() -> list[str]:
return findings
def scan_overview_copy() -> list[str]:
path = ROOT / OVERVIEW_PATH
if not path.exists():
return [f"{OVERVIEW_PATH}: missing required overview page"]
text = path.read_text(encoding="utf-8")
def scan_observability_copy() -> list[str]:
findings: list[str] = []
for snippet in FORBIDDEN_OVERVIEW_COPY:
if snippet in text:
findings.append(
f"{OVERVIEW_PATH}: legacy English overview copy `{snippet}` must be localized to the V2 workbench language"
)
for template_path in TEMPLATE_PATHS:
path = ROOT / Path(template_path)
if not path.exists():
findings.append(f"{template_path}: missing required observability page")
continue
text = path.read_text(encoding="utf-8")
for snippet in FORBIDDEN_OBSERVABILITY_COPY:
if snippet in text:
findings.append(
f"{template_path}: legacy English observability copy `{snippet}` must be localized to the V2 workbench language"
)
return findings
@@ -357,7 +398,7 @@ def main() -> int:
findings.extend(scan_css())
findings.extend(scan_shell())
findings.extend(scan_base_topbar())
findings.extend(scan_overview_copy())
findings.extend(scan_observability_copy())
findings.extend(scan_nav_contract())
if findings:
@@ -371,7 +412,7 @@ def main() -> int:
print(f"- css guardrails checked: {len(REQUIRED_CSS_SNIPPETS)}")
print(f"- sidebar/nav guardrails checked: {len(REQUIRED_SHELL_SNIPPETS)}")
print(f"- base/topbar guardrails checked: {len(REQUIRED_BASE_SNIPPETS) + len(FORBIDDEN_BASE_PATTERNS)}")
print(f"- overview copy guardrails checked: {len(FORBIDDEN_OVERVIEW_COPY)}")
print(f"- observability copy guardrails checked: {len(TEMPLATE_PATHS)} pages × {len(FORBIDDEN_OBSERVABILITY_COPY)} terms")
print(f"- nav contract checked: {len(OBSERVABILITY_PAGES)} pages")
return 0

View File

@@ -65,7 +65,7 @@ OBSERVABILITY_PAGES = (
"/observability/ai_calls",
"AI 呼叫",
"AI 呼叫",
("AI 呼叫", "Provider", "RAG"),
("AI 呼叫", "供應商", "RAG"),
),
ObservabilityPage(
"templates/admin/budget.html",
@@ -73,7 +73,7 @@ OBSERVABILITY_PAGES = (
"/observability/budget",
"預算控管",
"預算",
("預算控管", "force", "throttle"),
("預算控管", "預算線", "節流"),
),
ObservabilityPage(
"templates/admin/promotion_review.html",
@@ -81,7 +81,7 @@ OBSERVABILITY_PAGES = (
"/observability/promotion_review",
"RAG 晉升審核",
"晉升",
("RAG 晉升審核", "Promotion", "ai_insights"),
("RAG 晉升審核", "晉升", "ai_insights"),
),
ObservabilityPage(
"templates/admin/rag_queries.html",
@@ -89,7 +89,7 @@ OBSERVABILITY_PAGES = (
"/observability/rag_queries",
"RAG 召回詳情",
"RAG",
("RAG 召回詳情", "最近 50", "hits"),
("RAG 召回詳情", "最近 50", "命中"),
),
ObservabilityPage(
"templates/admin/quality_trend.html",
@@ -97,7 +97,7 @@ OBSERVABILITY_PAGES = (
"/observability/quality_trend",
"反饋趨勢",
"品質",
("反饋趨勢", "Caller", "蒸餾"),
("反饋趨勢", "呼叫端", "蒸餾"),
),
ObservabilityPage(
"templates/admin/ppt_audit_history.html",
@@ -105,7 +105,7 @@ OBSERVABILITY_PAGES = (
"/observability/ppt_audit_history",
"PPT 視覺審核",
"PPT",
("PPT 視覺審核", "AiderHeal", "audit"),
("PPT 視覺審核", "AiderHeal", "審核"),
),
)

View File

@@ -2379,6 +2379,136 @@
}
}
/* v3.11 V2 workbench normalization: unify legacy observability page skins before the terminal dot layer. */
.momo-observability-mode :is(
.obs-hero,
.agent-hero,
.biz-command,
.runtime-hero,
.calls-hero,
.gov-hero,
.gate-hero,
.rag-hero,
.qa-hero,
.quality-hero,
.ppt-hero
) {
border-color: var(--obs-line) !important;
border-radius: var(--momo-radius-lg, 8px) !important;
background-color: var(--momo-bg-surface, #faf7f0) !important;
background-image: var(--obs-matrix-dot) !important;
background-size: var(--obs-matrix-size) !important;
box-shadow: var(--momo-shadow-md, 0 2px 8px rgba(42, 37, 32, 0.06)) !important;
}
.momo-observability-mode :is(
.obs-panel,
.agent-panel,
.biz-panel,
.runtime-panel,
.calls-panel,
.gov-panel,
.gate-panel,
.rag-panel,
.qa-panel,
.quality-panel,
.ppt-panel,
.obs-signal,
.agent-signal,
.biz-signal,
.runtime-signal,
.calls-signal,
.gov-signal,
.gate-signal,
.rag-signal,
.qa-signal,
.quality-signal,
.ppt-signal,
.agent-card,
.rec-card,
.host-lane,
.runtime-mini,
.calls-mini,
.gov-mini,
.gate-mini,
.quality-mini,
.ppt-mini,
.strategy-card,
.episode-card,
.similar-box,
.fix-card,
.root-card,
.caller-card,
.biz-filter-card,
.biz-alert-strip,
.biz-chart-shell,
.biz-strategy-card,
.biz-mini-metric,
.biz-decision-card,
.obs-route-card
) {
border-color: var(--obs-line) !important;
border-radius: var(--momo-radius-lg, 8px) !important;
background-color: var(--momo-bg-elevated, #fdfaf3) !important;
background-image: var(--obs-matrix-dot-soft) !important;
background-size: var(--obs-matrix-size) !important;
box-shadow: var(--momo-shadow-md, 0 2px 8px rgba(42, 37, 32, 0.06)) !important;
}
.momo-observability-mode :is(
.obs-kicker,
.agent-kicker,
.biz-kicker,
.runtime-kicker,
.calls-kicker,
.gov-kicker,
.gate-kicker,
.rag-kicker,
.qa-kicker,
.quality-kicker,
.ppt-kicker,
.obs-signal-label,
.agent-label,
.biz-signal .label,
.runtime-label,
.calls-label,
.gov-label,
.gate-label,
.rag-label,
.qa-label,
.quality-label,
.ppt-label,
.obs-section-eyebrow
) {
font-family: var(--momo-font-family-mono, "JetBrains Mono", ui-monospace, monospace) !important;
font-size: var(--momo-text-label, 0.6875rem) !important;
letter-spacing: 0.06em !important;
text-transform: none !important;
color: color-mix(in srgb, var(--obs-accent) 76%, var(--obs-muted)) !important;
}
.momo-observability-mode :is(
.btn,
.badge,
.obs-pill,
.biz-badge,
[class$="-pill"],
.model-chip
) {
border-radius: var(--momo-radius-lg, 8px) !important;
}
.momo-observability-mode :is(
.agent-meter,
.caller-meter,
.progress,
.progress-bar,
.obs-progress-xs,
.obs-progress-sm
) {
border-radius: var(--momo-radius-sm, 3px) !important;
}
/* v3.10 terminal dot-matrix layer: this must stay at EOF to win the cascade. */
.momo-observability-mode {
--obs-matrix-dot: radial-gradient(color-mix(in srgb, var(--obs-accent) 14%, transparent) 0.85px, transparent 0.95px);

View File

@@ -9,20 +9,20 @@
</style>
<div class="container-fluid mt-3">
<section class="agent-hero"><div class="agent-kicker"><i class="fas fa-network-wired me-1"></i> Agent Command Matrix · {{ hours }}h Window</div><h1 class="agent-title">Agent 指揮矩陣</h1><p class="agent-subtitle">這頁回答 AI 中樞如何分工:誰在用 Ollama、誰還在吃付費 LLM、哪些 Agent 有 RAG 命中、哪些工作流已經接上 MCP。這不是列表是指揮官視角。</p><form method="get" class="agent-filter"><select name="hours" class="form-select form-select-sm" onchange="this.form.submit()">{% 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></form>{% if overall %}<div class="agent-command"><div class="agent-signal"><div class="agent-label">Total Calls</div><span class="agent-value">{{ "{:,}".format(overall.total_calls) }}</span><small class="text-muted">{{ "{:,}".format(overall.total_tokens) }} tokens</small></div><div class="agent-signal"><div class="agent-label">Ollama Share</div><span class="agent-value status-good">{{ "%.0f"|format(overall.local_pct) }}%</span><small class="text-muted">{{ "{:,}".format(overall.local_calls) }} local calls</small></div><div class="agent-signal"><div class="agent-label">Paid Cost</div><span class="agent-value {% if overall.total_cost > 0 %}status-warn{% else %}status-good{% endif %}">${{ "%.2f"|format(overall.total_cost) }}</span><small class="text-muted">{{ "{:,}".format(overall.paid_calls) }} paid calls</small></div><div class="agent-signal"><div class="agent-label">RAG Rate</div><span class="agent-value status-blue">{{ "%.0f"|format(overall.rag_rate) }}%</span><small class="text-muted">{{ "{:,}".format(overall.rag_hits) }} hits</small></div></div>{% endif %}</section>
<section class="agent-hero"><div class="agent-kicker"><i class="fas fa-network-wired me-1"></i> Agent 指揮矩陣 · {{ hours }} 小時視窗</div><h1 class="agent-title">Agent 指揮矩陣</h1><p class="agent-subtitle">這頁回答 AI 中樞如何分工:誰在用 Ollama、誰還在吃付費 LLM、哪些 Agent 有 RAG 命中、哪些工作流已經接上 MCP。這不是列表是指揮官視角。</p><form method="get" class="agent-filter"><select name="hours" class="form-select form-select-sm" onchange="this.form.submit()">{% 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></form>{% if overall %}<div class="agent-command"><div class="agent-signal"><div class="agent-label">呼叫總量</div><span class="agent-value">{{ "{:,}".format(overall.total_calls) }}</span><small class="text-muted">{{ "{:,}".format(overall.total_tokens) }} 權杖</small></div><div class="agent-signal"><div class="agent-label">Ollama 占比</div><span class="agent-value status-good">{{ "%.0f"|format(overall.local_pct) }}%</span><small class="text-muted">{{ "{:,}".format(overall.local_calls) }} 次本地呼叫</small></div><div class="agent-signal"><div class="agent-label">付費成本</div><span class="agent-value {% if overall.total_cost > 0 %}status-warn{% else %}status-good{% endif %}">${{ "%.2f"|format(overall.total_cost) }}</span><small class="text-muted">{{ "{:,}".format(overall.paid_calls) }} 次付費呼叫</small></div><div class="agent-signal"><div class="agent-label">RAG 命中率</div><span class="agent-value status-blue">{{ "%.0f"|format(overall.rag_rate) }}%</span><small class="text-muted">{{ "{:,}".format(overall.rag_hits) }} 次命中</small></div></div>{% endif %}</section>
{% if error %}<div class="alert alert-warning mt-3"><strong><i class="fas fa-triangle-exclamation me-1"></i></strong>{{ error }}</div>{% endif %}
<section class="agent-grid">
<div class="agent-stack">
<article class="agent-table-shell"><div class="agent-table-title"><div><div class="agent-label">4 Agent Matrix</div><h3>LLM × MCP × RAG 編排矩陣</h3></div></div><div class="table-responsive"><table class="table mb-0"><thead class="table-light"><tr><th>Agent</th><th class="text-end">呼叫</th><th class="text-end">成本</th><th class="text-end">Ollama</th><th class="text-end">付費</th><th class="text-end">MCP</th><th class="text-end">RAG</th><th class="text-end">錯誤</th><th class="text-end">耗時</th></tr></thead><tbody>{% for ag in agent_matrix %}<tr><td><strong>{{ ag.label }}</strong><small class="d-block text-muted">{{ ag.desc }}</small></td><td class="text-end">{% if ag.calls > 0 %}<strong>{{ "{:,}".format(ag.calls) }}</strong><small class="d-block text-muted">{{ "{:,}".format(ag.tokens) }} tk</small>{% else %}<small class="text-muted"></small>{% endif %}</td><td class="text-end">{% if ag.calls > 0 %}${{ "%.2f"|format(ag.cost) }}{% else %}<small class="text-muted"></small>{% endif %}</td><td class="text-end">{% if ag.calls > 0 %}<strong class="status-good">{{ "%.0f"|format(ag.ollama_pct) }}%</strong><small class="d-block text-muted">A {{ ag.ollama_gcp_a }} · B {{ ag.ollama_gcp_b }} · 111 {{ ag.ollama_111 }}</small>{% else %}<small class="text-muted"></small>{% endif %}</td><td class="text-end">{% if ag.calls > 0 %}<strong class="{% if ag.paid_pct > 50 %}status-bad{% elif ag.paid_pct > 20 %}status-warn{% endif %}">{{ "%.0f"|format(ag.paid_pct) }}%</strong><small class="d-block text-muted">Gemini {{ ag.gemini }}{% if ag.other_paid %} · 其他 {{ ag.other_paid }}{% endif %}</small>{% else %}<small class="text-muted"></small>{% endif %}</td><td class="text-end">{% if ag.calls > 0 %}<strong class="{% if ag.mcp_rate >= 30 %}status-blue{% elif ag.mcp_rate >= 10 %}status-warn{% else %}text-muted{% endif %}">{{ "%.1f"|format(ag.mcp_rate) }}%</strong><small class="d-block text-muted">{{ ag.mcp_calls }}</small>{% else %}<small class="text-muted"></small>{% endif %}</td><td class="text-end">{% if ag.calls > 0 %}<strong class="status-blue">{{ "%.1f"|format(ag.rag_rate) }}%</strong><small class="d-block text-muted">{{ ag.rag_hits }}</small>{% else %}<small class="text-muted"></small>{% endif %}</td><td class="text-end">{% if ag.calls > 0 %}<strong class="{% if ag.error_rate >= 15 %}status-bad{% elif ag.error_rate >= 5 %}status-warn{% else %}status-good{% endif %}">{{ "%.1f"|format(ag.error_rate) }}%</strong><small class="d-block text-muted">{{ ag.errors }}</small>{% else %}<small class="text-muted"></small>{% endif %}</td><td class="text-end">{% if ag.calls > 0 %}{{ ag.avg_ms }} ms{% else %}<small class="text-muted"></small>{% endif %}</td></tr>{% endfor %}</tbody></table></div></article>
<article class="agent-table-shell"><div class="agent-table-title"><div><div class="agent-label"> Agent 矩陣</div><h3>LLM × MCP × RAG 編排矩陣</h3></div></div><div class="table-responsive"><table class="table mb-0"><thead class="table-light"><tr><th>Agent</th><th class="text-end">呼叫</th><th class="text-end">成本</th><th class="text-end">Ollama</th><th class="text-end">付費</th><th class="text-end">MCP</th><th class="text-end">RAG</th><th class="text-end">錯誤</th><th class="text-end">耗時</th></tr></thead><tbody>{% for ag in agent_matrix %}<tr><td><strong>{{ ag.label }}</strong><small class="d-block text-muted">{{ ag.desc }}</small></td><td class="text-end">{% if ag.calls > 0 %}<strong>{{ "{:,}".format(ag.calls) }}</strong><small class="d-block text-muted">{{ "{:,}".format(ag.tokens) }} 權杖</small>{% else %}<small class="text-muted"></small>{% endif %}</td><td class="text-end">{% if ag.calls > 0 %}${{ "%.2f"|format(ag.cost) }}{% else %}<small class="text-muted"></small>{% endif %}</td><td class="text-end">{% if ag.calls > 0 %}<strong class="status-good">{{ "%.0f"|format(ag.ollama_pct) }}%</strong><small class="d-block text-muted">A {{ ag.ollama_gcp_a }} · B {{ ag.ollama_gcp_b }} · 111 {{ ag.ollama_111 }}</small>{% else %}<small class="text-muted"></small>{% endif %}</td><td class="text-end">{% if ag.calls > 0 %}<strong class="{% if ag.paid_pct > 50 %}status-bad{% elif ag.paid_pct > 20 %}status-warn{% endif %}">{{ "%.0f"|format(ag.paid_pct) }}%</strong><small class="d-block text-muted">Gemini {{ ag.gemini }}{% if ag.other_paid %} · 其他 {{ ag.other_paid }}{% endif %}</small>{% else %}<small class="text-muted"></small>{% endif %}</td><td class="text-end">{% if ag.calls > 0 %}<strong class="{% if ag.mcp_rate >= 30 %}status-blue{% elif ag.mcp_rate >= 10 %}status-warn{% else %}text-muted{% endif %}">{{ "%.1f"|format(ag.mcp_rate) }}%</strong><small class="d-block text-muted">{{ ag.mcp_calls }}</small>{% else %}<small class="text-muted"></small>{% endif %}</td><td class="text-end">{% if ag.calls > 0 %}<strong class="status-blue">{{ "%.1f"|format(ag.rag_rate) }}%</strong><small class="d-block text-muted">{{ ag.rag_hits }}</small>{% else %}<small class="text-muted"></small>{% endif %}</td><td class="text-end">{% if ag.calls > 0 %}<strong class="{% if ag.error_rate >= 15 %}status-bad{% elif ag.error_rate >= 5 %}status-warn{% else %}status-good{% endif %}">{{ "%.1f"|format(ag.error_rate) }}%</strong><small class="d-block text-muted">{{ ag.errors }}</small>{% else %}<small class="text-muted"></small>{% endif %}</td><td class="text-end">{% if ag.calls > 0 %}{{ ag.avg_ms }} ms{% else %}<small class="text-muted"></small>{% endif %}</td></tr>{% endfor %}</tbody></table></div></article>
</div>
<aside class="agent-stack">
<article class="agent-panel"><div class="agent-panel-head"><div><div class="agent-label">Agent Cards</div><h2 class="agent-panel-title">分工健康速覽</h2></div></div><div class="agent-panel-body">{% for ag in agent_matrix %}<div class="agent-card"><div class="agent-card-top"><div><strong>{{ ag.label }}</strong><small class="d-block text-muted">{{ ag.desc }}</small></div><span class="badge {% if ag.error_rate >= 15 %}bg-danger{% elif ag.calls == 0 %}bg-secondary{% else %}bg-success{% endif %}">{{ ag.calls }} calls</span></div><div class="agent-meter"><span style="width:{{ ag.ollama_pct|round|int if ag.calls > 0 else 0 }}%"></span></div><small class="text-muted">Ollama {{ "%.0f"|format(ag.ollama_pct) if ag.calls > 0 else 0 }}% · RAG {{ "%.0f"|format(ag.rag_rate) if ag.calls > 0 else 0 }}% · MCP {{ "%.0f"|format(ag.mcp_rate) if ag.calls > 0 else 0 }}%</small></div>{% endfor %}</div></article>
<article class="agent-panel"><div class="agent-panel-head"><div><div class="agent-label">分工卡片</div><h2 class="agent-panel-title">分工健康速覽</h2></div></div><div class="agent-panel-body">{% for ag in agent_matrix %}<div class="agent-card"><div class="agent-card-top"><div><strong>{{ ag.label }}</strong><small class="d-block text-muted">{{ ag.desc }}</small></div><span class="badge {% if ag.error_rate >= 15 %}bg-danger{% elif ag.calls == 0 %}bg-secondary{% else %}bg-success{% endif %}">{{ ag.calls }} 次呼叫</span></div><div class="agent-meter"><span style="width:{{ ag.ollama_pct|round|int if ag.calls > 0 else 0 }}%"></span></div><small class="text-muted">Ollama {{ "%.0f"|format(ag.ollama_pct) if ag.calls > 0 else 0 }}% · RAG {{ "%.0f"|format(ag.rag_rate) if ag.calls > 0 else 0 }}% · MCP {{ "%.0f"|format(ag.mcp_rate) if ag.calls > 0 else 0 }}%</small></div>{% endfor %}</div></article>
</aside>
</section>
{% if recommendations %}<section class="agent-panel mt-3"><div class="agent-panel-head"><div><div class="agent-label">Rules</div><h2 class="agent-panel-title">編排策略自動建議</h2></div></div><div class="agent-panel-body">{% for r in recommendations %}<div class="rec-card"><span class="badge {% if r.severity == 'high' %}bg-danger{% elif r.severity == 'med' %}bg-warning{% else %}bg-info{% endif %} me-1">{{ r.severity|upper }}</span><strong>{{ r.agent }}</strong><div class="small mt-1"><i class="fas fa-search me-1"></i><strong>發現:</strong>{{ r.finding }}</div><div class="small text-muted"><i class="fas fa-arrow-right me-1"></i><strong>建議:</strong>{{ r.suggestion }}</div></div>{% endfor %}</div></section>{% endif %}
{% if mcp_matrix %}<section class="agent-table-shell"><div class="agent-table-title"><div><div class="agent-label">MCP Detail</div><h3>MCP server × caller 工作量</h3></div></div><div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>MCP Server</th><th>Caller</th><th class="text-end">tool 呼叫</th><th class="text-end">cache</th><th class="text-end">cache </th><th class="text-end">成本</th></tr></thead><tbody>{% for m in mcp_matrix %}<tr><td><code>{{ m.server }}</code></td><td><code>{{ m.caller }}</code></td><td class="text-end">{{ "{:,}".format(m.calls) }}</td><td class="text-end">{{ m.cache_hits }}</td><td class="text-end"><span class="{% if m.cache_rate >= 50 %}status-good{% elif m.cache_rate >= 20 %}status-warn{% endif %}">{{ "%.0f"|format(m.cache_rate) }}%</span></td><td class="text-end">${{ "%.4f"|format(m.cost) }}</td></tr>{% endfor %}</tbody></table></div></section>{% endif %}
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 — Agent 指揮矩陣</small></p>
{% if recommendations %}<section class="agent-panel mt-3"><div class="agent-panel-head"><div><div class="agent-label">策略規則</div><h2 class="agent-panel-title">編排策略自動建議</h2></div></div><div class="agent-panel-body">{% for r in recommendations %}<div class="rec-card"><span class="badge {% if r.severity == 'high' %}bg-danger{% elif r.severity == 'med' %}bg-warning{% else %}bg-info{% endif %} me-1">{{ r.severity|upper }}</span><strong>{{ r.agent }}</strong><div class="small mt-1"><i class="fas fa-search me-1"></i><strong>發現:</strong>{{ r.finding }}</div><div class="small text-muted"><i class="fas fa-arrow-right me-1"></i><strong>建議:</strong>{{ r.suggestion }}</div></div>{% endfor %}</div></section>{% endif %}
{% if mcp_matrix %}<section class="agent-table-shell"><div class="agent-table-title"><div><div class="agent-label">MCP 明細</div><h3>MCP 服務 × 呼叫端工作量</h3></div></div><div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>MCP 服務</th><th>呼叫端</th><th class="text-end">tool 呼叫</th><th class="text-end">快取</th><th class="text-end">快取</th><th class="text-end">成本</th></tr></thead><tbody>{% for m in mcp_matrix %}<tr><td><code>{{ m.server }}</code></td><td><code>{{ m.caller }}</code></td><td class="text-end">{{ "{:,}".format(m.calls) }}</td><td class="text-end">{{ m.cache_hits }}</td><td class="text-end"><span class="{% if m.cache_rate >= 50 %}status-good{% elif m.cache_rate >= 20 %}status-warn{% endif %}">{{ "%.0f"|format(m.cache_rate) }}%</span></td><td class="text-end">${{ "%.4f"|format(m.cost) }}</td></tr>{% endfor %}</tbody></table></div></section>{% endif %}
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Ollama 優先策略 v5.0 — Agent 指揮矩陣</small></p>
</div>
{% endblock %}

View File

@@ -50,11 +50,11 @@
<div class="container-fluid mt-3">
<section class="calls-hero">
<div class="calls-kicker"><i class="fas fa-chart-bar me-1"></i> AI Traffic Control · {{ hours }}h Window</div>
<div class="calls-kicker"><i class="fas fa-chart-bar me-1"></i> AI 流量管制 · {{ hours }} 小時視窗</div>
<h1 class="calls-title">AI 流量控制塔</h1>
<p class="calls-subtitle">這裡不是流水帳,而是 AI 中樞的飛航管制台:看呼叫量、Token、成本、錯誤率、RAG 命中與 MCP 編排,並在異常時一鍵派出 Code Review Pipeline</p>
<p class="calls-subtitle">這裡不是流水帳,而是 AI 中樞的飛航管制台:看呼叫量、權杖量、成本、錯誤率、RAG 命中與 MCP 編排,並在異常時一鍵派出程式碼審查管線</p>
<div class="calls-actions">
<button class="btn btn-warning btn-sm" onclick="triggerCodeReview()"><i class="fas fa-microscope me-1"></i>觸發 Code Review Pipeline</button>
<button class="btn btn-warning btn-sm" onclick="triggerCodeReview()"><i class="fas fa-microscope me-1"></i>觸發程式碼審查管線</button>
<form method="get" class="calls-filter">
<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>
<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>
@@ -67,26 +67,26 @@
{% if error %}<div class="alert alert-warning mt-3"><strong><i class="fas fa-triangle-exclamation me-1"></i></strong>{{ error }}</div>{% endif %}
<section class="calls-command">
<div class="calls-signal"><div class="calls-label">Calls</div><span class="calls-value">{{ "{:,}".format(total) }}</span>{% if hourly_trend %}<canvas data-spark="calls" height="26"></canvas>{% endif %}</div>
<div class="calls-signal"><div class="calls-label">Tokens</div><span class="calls-value">{{ "{:,}".format(summary.total_tokens or 0) }}</span><div class="calls-note">{{ avg_tokens }} tk/call</div></div>
<div class="calls-signal"><div class="calls-label">Cost</div><span class="calls-value">${{ "%.2f"|format(summary.total_cost or 0) }}</span>{% if hourly_trend %}<canvas data-spark="cost" height="26"></canvas>{% endif %}</div>
<div class="calls-signal"><div class="calls-label">Latency</div><span class="calls-value">{{ summary.avg_duration or 0 }}ms</span><div class="calls-note">{{ summary.cache_hits or 0 }} cache hits</div></div>
<div class="calls-signal"><div class="calls-label">RAG Hit</div><span class="calls-value status-blue">{{ "%.1f"|format(rag_rate) }}%</span><div class="calls-note">{{ summary.rag_hits or 0 }} hits</div></div>
<div class="calls-signal"><div class="calls-label">Errors</div><span class="calls-value {% if error_rate >= 15 %}status-bad{% elif error_rate >= 5 %}status-warn{% else %}status-good{% endif %}">{{ errors }}</span>{% if hourly_trend %}<canvas data-spark="errors" height="26"></canvas>{% endif %}</div>
<div class="calls-signal"><div class="calls-label">呼叫總量</div><span class="calls-value">{{ "{:,}".format(total) }}</span>{% if hourly_trend %}<canvas data-spark="calls" height="26"></canvas>{% endif %}</div>
<div class="calls-signal"><div class="calls-label">權杖量</div><span class="calls-value">{{ "{:,}".format(summary.total_tokens or 0) }}</span><div class="calls-note">{{ avg_tokens }} 權杖/次</div></div>
<div class="calls-signal"><div class="calls-label">成本</div><span class="calls-value">${{ "%.2f"|format(summary.total_cost or 0) }}</span>{% if hourly_trend %}<canvas data-spark="cost" height="26"></canvas>{% endif %}</div>
<div class="calls-signal"><div class="calls-label">延遲</div><span class="calls-value">{{ summary.avg_duration or 0 }}ms</span><div class="calls-note">{{ summary.cache_hits or 0 }} 次快取命中</div></div>
<div class="calls-signal"><div class="calls-label">RAG 命中</div><span class="calls-value status-blue">{{ "%.1f"|format(rag_rate) }}%</span><div class="calls-note">{{ summary.rag_hits or 0 }} 次命中</div></div>
<div class="calls-signal"><div class="calls-label">錯誤</div><span class="calls-value {% if error_rate >= 15 %}status-bad{% elif error_rate >= 5 %}status-warn{% else %}status-good{% endif %}">{{ errors }}</span>{% if hourly_trend %}<canvas data-spark="errors" height="26"></canvas>{% endif %}</div>
</section>
<section class="calls-grid">
<div class="calls-stack">
{% if hourly_trend %}
<article class="calls-panel">
<div class="calls-panel-head"><div><div class="calls-label">Hourly Traffic</div><h2 class="calls-panel-title">每小時呼叫趨勢</h2></div></div>
<div class="calls-panel-head"><div><div class="calls-label">每小時流量</div><h2 class="calls-panel-title">每小時呼叫趨勢</h2></div></div>
<div class="calls-panel-body"><div class="calls-chart"><canvas id="hourlyTrendChart"></canvas></div></div>
</article>
{% endif %}
{% if caller_richness %}
<article class="calls-table-shell">
<div class="calls-table-title"><div><div class="calls-label">Caller Orchestration</div><h3>呼叫端 × RAG × MCP 編排矩陣</h3></div></div>
<div class="calls-table-title"><div><div class="calls-label">呼叫端編排</div><h3>呼叫端 × RAG × MCP 編排矩陣</h3></div></div>
<div class="table-responsive"><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 %}status-good{% elif c.rag_hit_rate >= 20 %}status-warn{% 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 %}status-blue{% elif c.mcp_rate >= 10 %}status-warn{% endif %}">{{ "%.1f"|format(c.mcp_rate) }}%</strong></td><td class="text-end">{% if c.feedback_count > 0 %}<strong class="{% if c.avg_rag_feedback >= 4 %}status-good{% elif c.avg_rag_feedback >= 3 %}status-warn{% else %}status-bad{% 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>
</article>
{% endif %}
@@ -94,19 +94,19 @@
<aside class="calls-stack">
<article class="calls-panel">
<div class="calls-panel-head"><div><div class="calls-label">Provider Split</div><h2 class="calls-panel-title">供應商分布</h2></div></div>
<div class="calls-panel-head"><div><div class="calls-label">供應商分布</div><h2 class="calls-panel-title">供應商分布</h2></div></div>
<div class="calls-panel-body">
<div class="calls-mini-grid">
{% for row in by_provider[:4] %}
<div class="calls-mini"><span class="calls-label">{{ row.provider }}</span><strong>{{ "{:,}".format(row.calls) }}</strong><small class="text-muted">${{ "%.2f"|format(row.cost) }} · {{ "{:,}".format(row.tokens) }} tk</small></div>
{% else %}<div class="text-muted small">尚無 provider 資料</div>{% endfor %}
{% else %}<div class="text-muted small">尚無供應商資料</div>{% endfor %}
</div>
</div>
</article>
{% if recent_contexts %}
<article class="calls-panel">
<div class="calls-panel-head"><div><div class="calls-label">Agent Context</div><h2 class="calls-panel-title">最近上下文</h2></div></div>
<div class="calls-panel-head"><div><div class="calls-label">Agent 上下文</div><h2 class="calls-panel-title">最近上下文</h2></div></div>
<div class="calls-panel-body">
{% for c in recent_contexts[:5] %}<div class="mb-2 pb-2 border-bottom"><span class="badge bg-info">{{ c.agent_name }}</span> <code>{{ c.context_key }}</code><div class="text-muted small mt-1">{{ c.preview }}{% if c.preview|length >= 120 %}…{% endif %}</div></div>{% endfor %}
</div>
@@ -117,17 +117,17 @@
{% if by_model %}
<section class="calls-table-shell">
<div class="calls-table-title"><div><div class="calls-label">Model Economics</div><h3>依模型細分</h3></div></div>
<div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>模型</th><th>供應商</th><th class="text-end">呼叫</th><th class="text-end">Token</th><th class="text-end">成本</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="status-bad">{{ m.errors }}</span>{% else %}<small class="text-muted">0</small>{% endif %}</td></tr>{% endfor %}</tbody></table></div>
<div class="calls-table-title"><div><div class="calls-label">模型成本</div><h3>依模型細分</h3></div></div>
<div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>模型</th><th>供應商</th><th class="text-end">呼叫</th><th class="text-end">權杖</th><th class="text-end">成本</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="status-bad">{{ m.errors }}</span>{% else %}<small class="text-muted">0</small>{% endif %}</td></tr>{% endfor %}</tbody></table></div>
</section>
{% endif %}
<section class="calls-table-shell">
<div class="calls-table-title"><div><div class="calls-label">Recent Calls</div><h3>最近呼叫 100 筆</h3></div></div>
<div class="calls-table-title"><div><div class="calls-label">最近呼叫</div><h3>最近呼叫 100 筆</h3></div></div>
<div class="table-responsive"><table class="table table-sm table-striped mb-0"><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">耗時</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>
</section>
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 — AI 流量控制塔</small></p>
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Ollama 優先策略 v5.0 — AI 流量控制塔</small></p>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
@@ -143,6 +143,6 @@
const el = document.getElementById('hourlyTrendChart'); if (!el || !labels.length) return;
new Chart(el, { data: { labels, datasets: [ { type: 'line', label: '呼叫數', data: calls, borderColor: '#c96442', backgroundColor: 'rgba(201,100,66,.12)', tension: .35, fill: true, yAxisID: 'y' }, { type: 'line', label: '錯誤', data: errors, borderColor: '#b94b45', backgroundColor: 'rgba(185,75,69,.1)', tension: .35, yAxisID: 'y' }, { type: 'bar', label: '成本 USD', data: costs, backgroundColor: 'rgba(184,121,47,.38)', borderColor: '#b8792f', yAxisID: 'y1' } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, scales: { y: { beginAtZero: true, title: { display: true, text: '次數' } }, y1: { position: 'right', beginAtZero: true, grid: { drawOnChartArea: false }, title: { display: true, text: 'USD' } } } } });
})();
async function triggerCodeReview() { if (!confirm('觸發 Code Review Pipeline\n\n會對最新 commit 跑 5 step 審查,背景執行。')) 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) { console.warn('code_review_trigger_failed', e); alert('操作暫時無法完成,請稍後再試或查看系統日誌。'); } }
async function triggerCodeReview() { if (!confirm('觸發程式碼審查管線\n\n會對最新 commit 跑 5 步驟審查,背景執行。')) 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\n管線 ID: ${d.pipeline_id}\nCommit: ${d.commit_sha}\n變更檔案: ${d.changed_files_count}`); } else { alert('❌ ' + (d.error || '觸發失敗')); } } catch (e) { console.warn('code_review_trigger_failed', e); alert('操作暫時無法完成,請稍後再試或查看系統日誌。'); } }
</script>
{% endblock %}

View File

@@ -37,15 +37,15 @@
<div class="container-fluid mt-3">
<section class="gov-hero">
<div class="gov-kicker"><i class="fas fa-wallet me-1"></i> AI Cost Governance · Budget / Throttle / RAG Strategy</div>
<div class="gov-kicker"><i class="fas fa-wallet me-1"></i> AI 成本治理 · 預算 / 節流 / RAG 策略</div>
<h1 class="gov-title">AI 成本治理艙</h1>
<p class="gov-subtitle">這頁回答一個問題AI 中樞花錢是否仍在治理邊界內?預算、實際支出、月底推估、節流狀態與 RAG 策略建議集中在同一個 cockpit</p>
<div class="gov-actions"><button class="btn btn-warning btn-sm" onclick="forceThrottle()"><i class="fas fa-bolt me-1"></i>立即重算節流狀態</button><span class="text-muted small">超過 110% 時不用等 cron直接 evaluate provider throttle</span></div>
<p class="gov-subtitle">這頁回答一個問題AI 中樞花錢是否仍在治理邊界內?預算、實際支出、月底推估、節流狀態與 RAG 策略建議集中在同一個工作台</p>
<div class="gov-actions"><button class="btn btn-warning btn-sm" onclick="forceThrottle()"><i class="fas fa-bolt me-1"></i>立即重算節流狀態</button><span class="text-muted small">超過 110% 時不用等 排程,直接重算供應商節流</span></div>
<div class="gov-command">
<div class="gov-signal"><div class="gov-label">Month Spend</div><span class="gov-value">${{ "%.2f"|format(total_spent.value) }}</span><div class="gov-note">預算 ${{ "%.2f"|format(total_budget.value) }}</div></div>
<div class="gov-signal"><div class="gov-label">Budget Ratio</div><span class="gov-value {% if total_ratio >= 110 %}status-bad{% elif total_ratio >= 80 %}status-warn{% else %}status-good{% endif %}">{{ "%.0f"|format(total_ratio) }}%</span><div class="gov-note"> provider 加總</div></div>
<div class="gov-signal"><div class="gov-label">Warnings</div><span class="gov-value {% if warn_count.value > 0 %}status-warn{% else %}status-good{% endif %}">{{ warn_count.value }}</span><div class="gov-note">使用率 ≥ 80%</div></div>
<div class="gov-signal"><div class="gov-label">Throttled</div><span class="gov-value {% if throttled_count.value > 0 %}status-bad{% else %}status-good{% endif %}">{{ throttled_count.value }}</span><div class="gov-note">已啟動成本節流</div></div>
<div class="gov-signal"><div class="gov-label">當月花費</div><span class="gov-value">${{ "%.2f"|format(total_spent.value) }}</span><div class="gov-note">預算 ${{ "%.2f"|format(total_budget.value) }}</div></div>
<div class="gov-signal"><div class="gov-label">預算使用率</div><span class="gov-value {% if total_ratio >= 110 %}status-bad{% elif total_ratio >= 80 %}status-warn{% else %}status-good{% endif %}">{{ "%.0f"|format(total_ratio) }}%</span><div class="gov-note">供應商加總</div></div>
<div class="gov-signal"><div class="gov-label">預警</div><span class="gov-value {% if warn_count.value > 0 %}status-warn{% else %}status-good{% endif %}">{{ warn_count.value }}</span><div class="gov-note">使用率 ≥ 80%</div></div>
<div class="gov-signal"><div class="gov-label">已節流</div><span class="gov-value {% if throttled_count.value > 0 %}status-bad{% else %}status-good{% endif %}">{{ throttled_count.value }}</span><div class="gov-note">已啟動成本節流</div></div>
</div>
</section>
@@ -54,42 +54,42 @@
<section class="gov-grid">
<div class="gov-stack">
<article class="gov-table-shell">
<div class="gov-table-title"><div><div class="gov-label">Budget Lines</div><h3>預算線與節流狀態</h3></div></div>
<div class="gov-table-title"><div><div class="gov-label">預算線</div><h3>預算線與節流狀態</h3></div></div>
<div class="table-responsive"><table class="table table-hover mb-0"><thead class="table-light"><tr><th>週期</th><th>供應商</th><th class="text-end">已花費</th><th>預算</th><th>閾值</th><th class="text-end">使用率</th><th>狀態</th><th>動作</th></tr></thead><tbody>{% for r in rows %}<tr {% if r.throttled %}class="table-danger"{% elif r.ratio >= 0.8 %}class="table-warning"{% endif %}><td><span class="badge bg-secondary">{{ r.period }}</span></td><td><code>{{ r.provider }}</code></td><td class="text-end">${{ "%.2f"|format(r.spent) }}</td><td><input type="number" step="0.01" min="0.01" value="{{ "%.2f"|format(r.budget_usd) }}" class="form-control form-control-sm budget-input" data-budget-id="{{ r.id }}" ></td><td><input type="number" min="1" max="100" value="{{ r.alert_pct }}" class="form-control form-control-sm alert-input" data-budget-id="{{ r.id }}" ></td><td class="text-end"><strong class="{% if r.ratio >= 1.10 %}status-bad{% elif r.ratio >= 0.8 %}status-warn{% else %}status-good{% endif %}">{{ "%.0f"|format(r.ratio * 100) }}%</strong></td><td>{% if r.throttled %}<span class="badge bg-danger">已節流</span>{% elif r.ratio >= 0.8 %}<span class="badge bg-warning">接近上限</span>{% else %}<span class="badge bg-success">正常</span>{% endif %}</td><td><button class="btn btn-primary btn-sm save-budget-btn" data-budget-id="{{ r.id }}" onclick="saveBudget({{ r.id }})"><i class="fas fa-save me-1"></i>儲存</button></td></tr>{% else %}<tr><td colspan="8" class="text-center text-muted">無預算資料(需先跑 migrations/025</td></tr>{% endfor %}</tbody></table></div>
</article>
{% if cost_trend_30d %}
<article class="gov-panel"><div class="gov-panel-head"><div><div class="gov-label">30d Cost Trend</div><h2 class="gov-panel-title">每日成本堆疊趨勢</h2></div></div><div class="gov-panel-body"><div class="gov-chart"><canvas id="costTrend30dChart"></canvas></div></div></article>
<article class="gov-panel"><div class="gov-panel-head"><div><div class="gov-label">30 日成本趨勢</div><h2 class="gov-panel-title">每日成本堆疊趨勢</h2></div></div><div class="gov-panel-body"><div class="gov-chart"><canvas id="costTrend30dChart"></canvas></div></div></article>
{% endif %}
</div>
<aside class="gov-stack">
{% if provider_cost_month %}
<article class="gov-panel"><div class="gov-panel-head"><div><div class="gov-label">Provider Mix</div><h2 class="gov-panel-title">當月成本分布</h2></div></div><div class="gov-panel-body"><div class="obs-chart-frame"><canvas id="providerCostPieChart"></canvas></div></div></article>
<article class="gov-panel"><div class="gov-panel-head"><div><div class="gov-label">供應商分布</div><h2 class="gov-panel-title">當月成本分布</h2></div></div><div class="gov-panel-body"><div class="obs-chart-frame"><canvas id="providerCostPieChart"></canvas></div></div></article>
{% endif %}
{% if top_cost_callers %}
<article class="gov-panel"><div class="gov-panel-head"><div><div class="gov-label">Burn Rate</div><h2 class="gov-panel-title">Top 5 燒錢 caller</h2></div></div><div class="gov-panel-body">{% set max_cost = (top_cost_callers | map(attribute='cost') | max) or 1 %}{% for c in top_cost_callers %}<div class="gov-mini mb-2"><div class="d-flex justify-content-between"><code>{{ c.caller }}</code><strong>${{ "%.2f"|format(c.cost) }}</strong></div><div class="progress mt-2 obs-progress-xs"><div class="progress-bar" style="width: {{ (c.cost / max_cost * 100) | round | int }}%;"></div></div><small class="text-muted">{{ "{:,}".format(c.calls) }} calls · {{ "{:,}".format(c.tokens) }} tokens</small></div>{% endfor %}</div></article>
<article class="gov-panel"><div class="gov-panel-head"><div><div class="gov-label">燃燒率</div><h2 class="gov-panel-title">Top 5 燒錢呼叫端</h2></div></div><div class="gov-panel-body">{% set max_cost = (top_cost_callers | map(attribute='cost') | max) or 1 %}{% for c in top_cost_callers %}<div class="gov-mini mb-2"><div class="d-flex justify-content-between"><code>{{ c.caller }}</code><strong>${{ "%.2f"|format(c.cost) }}</strong></div><div class="progress mt-2 obs-progress-xs"><div class="progress-bar" style="width: {{ (c.cost / max_cost * 100) | round | int }}%;"></div></div><small class="text-muted">{{ "{:,}".format(c.calls) }} 次呼叫 · {{ "{:,}".format(c.tokens) }} 權杖</small></div>{% endfor %}</div></article>
{% endif %}
</aside>
</section>
{% if budget_strategies %}
<section class="gov-panel mt-3"><div class="gov-panel-head"><div><div class="gov-label">RAG Strategy</div><h2 class="gov-panel-title">RAG 自動策略建議</h2></div></div><div class="gov-panel-body">{% for s in budget_strategies %}<div class="strategy-card"><span class="badge bg-info me-1">{{ s.insight_type }}</span><span class="badge bg-secondary me-1">相似度 {{ "%.2f"|format(s.similarity) }}</span><span>{{ s.content }}{% if s.content|length >= 240 %}…{% endif %}</span></div>{% endfor %}</div></section>
<section class="gov-panel mt-3"><div class="gov-panel-head"><div><div class="gov-label">RAG 策略</div><h2 class="gov-panel-title">RAG 自動策略建議</h2></div></div><div class="gov-panel-body">{% for s in budget_strategies %}<div class="strategy-card"><span class="badge bg-info me-1">{{ s.insight_type }}</span><span class="badge bg-secondary me-1">相似度 {{ "%.2f"|format(s.similarity) }}</span><span>{{ s.content }}{% if s.content|length >= 240 %}…{% endif %}</span></div>{% endfor %}</div></section>
{% endif %}
{% if price_rec_7d %}
<section class="gov-panel mt-3"><div class="gov-panel-head"><div><div class="gov-label">Business Output</div><h2 class="gov-panel-title">AI 價格決策 7 日</h2></div></div><div class="gov-panel-body"><div class="gov-mini-grid">{% for p in price_rec_7d %}<div class="gov-mini"><span class="gov-label">{{ p.strategy }}</span><strong>{{ p.count }}</strong><small class="text-muted">信心 {{ "%.2f"|format(p.avg_confidence) }}</small></div>{% endfor %}</div></div></section>
<section class="gov-panel mt-3"><div class="gov-panel-head"><div><div class="gov-label">商業產出</div><h2 class="gov-panel-title">AI 價格決策 7 日</h2></div></div><div class="gov-panel-body"><div class="gov-mini-grid">{% for p in price_rec_7d %}<div class="gov-mini"><span class="gov-label">{{ p.strategy }}</span><strong>{{ p.count }}</strong><small class="text-muted">信心 {{ "%.2f"|format(p.avg_confidence) }}</small></div>{% endfor %}</div></div></section>
{% endif %}
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 — AI 成本治理艙</small></p>
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Ollama 優先策略 v5.0 — AI 成本治理艙</small></p>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
(function() { const data = {{ provider_cost_month | default([]) | tojson }}; const el = document.getElementById('providerCostPieChart'); if (!el || !data.length) return; const colors = {'gcp_ollama':'#4f8a5b','ollama_secondary':'#7aaa82','ollama_111':'#a3cfa8','gemini':'#b8792f','claude':'#4f6f8f','nim':'#6aa6a6','openrouter':'#8b8077','nim_via_elephant':'#c96442'}; new Chart(el,{type:'doughnut',data:{labels:data.map(d=>d.provider),datasets:[{data:data.map(d=>d.cost),backgroundColor:data.map((d,i)=>colors[d.provider]||`hsl(${(i*47)%360},55%,55%)`),borderWidth:1,borderColor:'#fff'}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{font:{size:11}}}}}}); })();
(function() { const raw = {{ cost_trend_30d | tojson }}; if (!raw || !raw.length) return; const dateSet = [...new Set(raw.map(r=>r.date))].sort(); const providerSet = [...new Set(raw.map(r=>r.provider))]; const palette = ['#c96442','#b8792f','#4f8a5b','#4f6f8f','#6aa6a6','#8b8077','#a66a4a']; const datasets = providerSet.map((p,i)=>({label:p,data:dateSet.map(d=>{const row=raw.find(r=>r.date===d&&r.provider===p);return row?row.cost:0;}),backgroundColor:palette[i%palette.length]})); const el=document.getElementById('costTrend30dChart'); if(!el)return; new Chart(el,{type:'bar',data:{labels:dateSet,datasets},options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},scales:{x:{stacked:true},y:{stacked:true,beginAtZero:true,title:{display:true,text:'USD'}}}}}); })();
async function forceThrottle(){if(!confirm('立即重算所有 provider 的 throttle 狀態?'))return;try{const r=await fetch('/observability/budget/force_throttle',{method:'POST'});const d=await r.json();if(d.ok){alert(`✅ 已重算:被節流的 provider = ${(d.throttled_providers&&d.throttled_providers.length>0)?d.throttled_providers.join(', '):'(無)'}`);window.location.reload();}else{alert('❌ '+(d.error||'重算失敗'));}}catch(e){console.warn('budget_force_throttle_failed',e);alert('操作暫時無法完成,請稍後再試或查看系統日誌。');}}
async function forceThrottle(){if(!confirm('立即重算所有供應商的節流狀態?'))return;try{const r=await fetch('/observability/budget/force_throttle',{method:'POST'});const d=await r.json();if(d.ok){alert(`✅ 已重算:被節流的供應商 = ${(d.throttled_providers&&d.throttled_providers.length>0)?d.throttled_providers.join(', '):'(無)'}`);window.location.reload();}else{alert('❌ '+(d.error||'重算失敗'));}}catch(e){console.warn('budget_force_throttle_failed',e);alert('操作暫時無法完成,請稍後再試或查看系統日誌。');}}
async function saveBudget(id){const budgetInput=document.querySelector(`.budget-input[data-budget-id="${id}"]`);const alertInput=document.querySelector(`.alert-input[data-budget-id="${id}"]`);const btn=document.querySelector(`.save-budget-btn[data-budget-id="${id}"]`);btn.disabled=true;btn.innerHTML='<i class="fas fa-spinner fa-spin"></i>';try{const r=await fetch(`/observability/budget/update/${id}`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({budget_usd:parseFloat(budgetInput.value),alert_pct:parseInt(alertInput.value)})});const d=await r.json();if(d.ok){btn.innerHTML='<i class="fas fa-check"></i> 已儲存';setTimeout(()=>{btn.innerHTML='<i class="fas fa-save me-1"></i>儲存';btn.disabled=false;},1500);}else{alert('更新失敗:'+(d.error||'請稍後再試'));btn.disabled=false;btn.innerHTML='<i class="fas fa-save me-1"></i>儲存';}}catch(e){console.warn('budget_save_failed',e);alert('操作暫時無法完成,請稍後再試或查看系統日誌。');btn.disabled=false;btn.innerHTML='<i class="fas fa-save me-1"></i>儲存';}}
</script>
{% endblock %}

View File

@@ -439,7 +439,7 @@
<div class="biz-warroom">
<div class="biz-hero">
<section class="biz-command">
<span class="biz-kicker"><i class="fas fa-store"></i> Business Intelligence</span>
<span class="biz-kicker"><i class="fas fa-store"></i> 商業情報</span>
<h1 class="biz-title">商業 AI 戰果室</h1>
<p>
這一頁不再只是資料列表,而是把價格建議、未跟進警示、閉環學習與競品監測收成一個商業決策控制台。
@@ -476,7 +476,7 @@
<article class="biz-signal">
<div class="label">高信心未跟進</div>
<div class="value">{{ unfollowed_count }}</div>
<div class="note">confidence >= 0.8 且仍未轉 action_plan</div>
<div class="note">信心分 >= 0.8 且仍未轉行動計畫</div>
</article>
<article class="biz-signal">
<div class="label">平均信心分</div>
@@ -486,7 +486,7 @@
<article class="biz-signal">
<div class="label">有效率</div>
<div class="value">{{ '%.0f'|format(effective_rate) }}%</div>
<div class="note">effective / 已回收 verdict</div>
<div class="note">有效 / 已回收結論</div>
</article>
<article class="biz-signal">
<div class="label">競品監測</div>
@@ -559,7 +559,7 @@
</div>
<div class="biz-price-stack">
{{ r.momo_price or '-' }} / {{ r.pchome_price or '-' }}
<small>gap {{ '%.1f'|format(r.gap_pct or 0) }}%</small>
<small>差距 {{ '%.1f'|format(r.gap_pct or 0) }}%</small>
</div>
<div class="biz-decision-reason">{{ r.reason or '尚無原因摘要' }}</div>
</article>
@@ -575,20 +575,20 @@
<div class="biz-panel-head">
<div>
<h3>閉環學習紀錄</h3>
<p>追蹤 action_plan 到 outcome 的真實效果,這是 AI 能不能變聰明的核心證據。</p>
<p>追蹤行動計畫到實際結果的真實效果,這是 AI 能不能變聰明的核心證據。</p>
</div>
<span class="biz-badge good">{{ loop_records|length }} records</span>
<span class="biz-badge good">{{ loop_records|length }} 筆紀錄</span>
</div>
<div class="biz-panel-body table-responsive">
{% if loop_records %}
<table class="biz-table">
<thead>
<tr>
<th>Plan</th>
<th>計畫</th>
<th>SKU</th>
<th>狀態</th>
<th>建立 / 執行</th>
<th>Verdict</th>
<th>結論</th>
<th>指標</th>
<th>變化</th>
</tr>
@@ -608,7 +608,7 @@
</tbody>
</table>
{% else %}
<div class="biz-empty">尚未形成 action_plan → outcome 閉環紀錄。</div>
<div class="biz-empty">尚未形成行動計畫到實際結果的閉環紀錄。</div>
{% endif %}
</div>
</section>
@@ -618,7 +618,7 @@
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>Verdict 戰果分布</h3>
<h3>結論戰果分布</h3>
<p>用結果反校正 AI 建議,不讓漂亮信心分掩蓋真實成效。</p>
</div>
</div>
@@ -626,7 +626,7 @@
<div class="biz-chart-shell"><canvas id="verdictPieChart"></canvas></div>
{% if verdict_stats %}
<table class="biz-table mt-2">
<thead><tr><th>Verdict</th><th>Count</th><th>Avg Δ</th></tr></thead>
<thead><tr><th>結論</th><th>數量</th><th>平均變化</th></tr></thead>
<tbody>
{% for v in verdict_stats %}
<tr><td>{{ v.verdict or '未分類' }}</td><td>{{ v.count }}</td><td>{{ '%.1f'|format(v.avg_delta or 0) }}%</td></tr>
@@ -634,7 +634,7 @@
</tbody>
</table>
{% else %}
<div class="biz-empty">尚無 verdict 統計。</div>
<div class="biz-empty">尚無結論統計。</div>
{% endif %}
</div>
</section>
@@ -649,7 +649,7 @@
<div class="biz-panel-body table-responsive">
{% if match_stats %}
<table class="biz-table">
<thead><tr><th>Status</th><th>Count</th><th>Candidates</th><th>Score</th></tr></thead>
<thead><tr><th>狀態</th><th>數量</th><th>候選數</th><th>分數</th></tr></thead>
<tbody>
{% for m in match_stats %}
<tr>
@@ -683,8 +683,8 @@
<tr>
<td>{{ r.crawled_at or '-' }}</td>
<td><strong>{{ r.sku }}</strong><br><span class="text-muted small">{{ r.product_name or '-' }}</span></td>
<td>{{ r.pchome_price or '-' }} / {{ r.momo_price or '-' }}<br><span class="text-muted small">gap {{ r.gap or '-' }}</span></td>
<td>{{ '%.1f'|format(r.discount_pct or 0) }}%<br><span class="text-muted small">score {{ '%.2f'|format(r.match_score or 0) }}</span></td>
<td>{{ r.pchome_price or '-' }} / {{ r.momo_price or '-' }}<br><span class="text-muted small">差距 {{ r.gap or '-' }}</span></td>
<td>{{ '%.1f'|format(r.discount_pct or 0) }}%<br><span class="text-muted small">分數 {{ '%.2f'|format(r.match_score or 0) }}</span></td>
</tr>
{% endfor %}
</tbody>
@@ -697,7 +697,7 @@
</aside>
</div>
<div class="text-muted small mt-3">資料來源:ai_price_recommendations / action_plans / action_outcomes / competitor_match_attempts / competitor_price_history</div>
<div class="text-muted small mt-3">資料來源:AI 價格建議、行動計畫、實際結果、競品比對與競品價格歷史。</div>
</div>
{% endblock %}

View File

@@ -59,14 +59,14 @@
<div class="container-fluid mt-3">
<section class="runtime-hero">
<div class="runtime-kicker"><i class="fas fa-heartbeat me-1"></i> Infrastructure Lifeline · Ollama / MCP / AIOps</div>
<div class="runtime-kicker"><i class="fas fa-heartbeat me-1"></i> 基礎設施生命線 · Ollama / MCP / AIOps</div>
<h1 class="runtime-title">基礎設施生命線</h1>
<p class="runtime-subtitle">這頁是 AI 中樞的底盤監控:三主機 Ollama 級聯、MCP 工具層、成本節流與 ADR-013 AutoHeal 閉環。先看能不能活,再看要不要修。</p>
<div class="runtime-command">
<div class="runtime-signal"><div class="runtime-label">Ollama Down</div><span class="runtime-value {% if down.count > 0 %}status-bad{% else %}status-good{% endif %}">{{ down.count }}</span><small class="text-muted">{{ ollama_hosts|length }} 台即時 probe</small></div>
<div class="runtime-signal"><div class="runtime-label">AIOps Open</div><span class="runtime-value {% if aiops_summary and aiops_summary.incidents_open > 0 %}status-bad{% else %}status-good{% endif %}">{{ aiops_summary.incidents_open if aiops_summary else '—' }}</span><small class="text-muted">7 日 incident 未解決</small></div>
<div class="runtime-signal"><div class="runtime-label">Heal Rate</div><span class="runtime-value {% if aiops_summary and aiops_summary.heal_success_rate >= 80 %}status-good{% elif aiops_summary and aiops_summary.heal_success_rate >= 50 %}status-warn{% else %}status-bad{% endif %}">{{ "%.0f"|format(aiops_summary.heal_success_rate) if aiops_summary else '—' }}{% if aiops_summary %}%{% endif %}</span><small class="text-muted">ADR-013 自癒成功率</small></div>
<div class="runtime-signal"><div class="runtime-label">Throttled</div><span class="runtime-value {% if throttled.count > 0 %}status-warn{% else %}status-good{% endif %}">{{ throttled.count }}</span><small class="text-muted">成本節流供應商</small></div>
<div class="runtime-signal"><div class="runtime-label">Ollama 離線</div><span class="runtime-value {% if down.count > 0 %}status-bad{% else %}status-good{% endif %}">{{ down.count }}</span><small class="text-muted">{{ ollama_hosts|length }} 台即時探測</small></div>
<div class="runtime-signal"><div class="runtime-label">AIOps 未解</div><span class="runtime-value {% if aiops_summary and aiops_summary.incidents_open > 0 %}status-bad{% else %}status-good{% endif %}">{{ aiops_summary.incidents_open if aiops_summary else '—' }}</span><small class="text-muted">7 日事件未解決</small></div>
<div class="runtime-signal"><div class="runtime-label">自癒成功率</div><span class="runtime-value {% if aiops_summary and aiops_summary.heal_success_rate >= 80 %}status-good{% elif aiops_summary and aiops_summary.heal_success_rate >= 50 %}status-warn{% else %}status-bad{% endif %}">{{ "%.0f"|format(aiops_summary.heal_success_rate) if aiops_summary else '—' }}{% if aiops_summary %}%{% endif %}</span><small class="text-muted">ADR-013 自癒成功率</small></div>
<div class="runtime-signal"><div class="runtime-label">節流供應商</div><span class="runtime-value {% if throttled.count > 0 %}status-warn{% else %}status-good{% endif %}">{{ throttled.count }}</span><small class="text-muted">成本節流供應商</small></div>
</div>
</section>
@@ -74,7 +74,7 @@
<div class="runtime-stack">
<article class="runtime-panel">
<div class="runtime-panel-head">
<div><div class="runtime-label">Host Cascade</div><h2 class="runtime-panel-title">Ollama 三主機</h2></div>
<div><div class="runtime-label">主機級聯</div><h2 class="runtime-panel-title">Ollama 三主機</h2></div>
<span class="badge {% if down.count > 0 %}bg-danger{% else %}bg-success{% endif %}">{{ '需要處理' if down.count > 0 else '全部在線' }}</span>
</div>
<div class="runtime-panel-body">
@@ -104,7 +104,7 @@
{% if health_history %}
<article class="runtime-table-shell">
<div class="runtime-table-title"><div><div class="runtime-label">24h Probe History</div><h3>健康趨勢摘要</h3></div></div>
<div class="runtime-table-title"><div><div class="runtime-label">24 小時探測歷史</div><h3>健康趨勢摘要</h3></div></div>
<div class="table-responsive"><table class="table mb-0"><thead class="table-light"><tr><th>角色</th><th class="text-end">總探針</th><th class="text-end">正常</th><th class="text-end">離線</th><th class="text-end">在線率</th><th class="text-end">平均 ms</th></tr></thead><tbody>{% for h in health_history %}<tr><td><strong>{{ h.host_label }}</strong></td><td class="text-end">{{ h.total }}</td><td class="text-end status-good">{{ h.up_count }}</td><td class="text-end status-bad">{{ h.down_count }}</td><td class="text-end"><strong class="{% if h.uptime_pct >= 99 %}status-good{% elif h.uptime_pct >= 90 %}status-warn{% else %}status-bad{% endif %}">{{ "%.1f"|format(h.uptime_pct) }}%</strong></td><td class="text-end">{{ h.avg_ms }}</td></tr>{% endfor %}</tbody></table></div>
</article>
{% endif %}
@@ -113,7 +113,7 @@
<aside class="runtime-stack">
{% if aiops_summary %}
<article class="runtime-panel">
<div class="runtime-panel-head"><div><div class="runtime-label">AIOps Loop</div><h2 class="runtime-panel-title">自癒閉環 7 日</h2></div></div>
<div class="runtime-panel-head"><div><div class="runtime-label">AIOps 閉環</div><h2 class="runtime-panel-title">自癒閉環 7 日</h2></div></div>
<div class="runtime-panel-body">
<div class="runtime-mini-grid">
<div class="runtime-mini"><span class="runtime-label">事件總數</span><strong>{{ aiops_summary.incidents_total }}</strong></div>
@@ -130,10 +130,10 @@
<div class="runtime-panel-head"><div><div class="runtime-label">MCP / Budget</div><h2 class="runtime-panel-title">工具層與節流</h2></div></div>
<div class="runtime-panel-body">
<div class="runtime-mini-grid">
<div class="runtime-mini"><span class="runtime-label">MCP Servers</span><strong>{{ mcp_status|length }}</strong></div>
<div class="runtime-mini"><span class="runtime-label">MCP 24h Calls</span><strong>{{ "{:,}".format(mcp_24h|sum(attribute='total_calls')) if mcp_24h else 0 }}</strong></div>
<div class="runtime-mini"><span class="runtime-label">Playbooks</span><strong>{{ active_playbooks.count }}/{{ playbook_ranking|length }}</strong></div>
<div class="runtime-mini"><span class="runtime-label">Embed Queue</span><strong class="{% if embed_queue_failed > 0 %}status-bad{% elif embed_queue_pending > 0 %}status-warn{% else %}status-good{% endif %}">{{ embed_queue_pending }}/{{ embed_queue_failed }}</strong></div>
<div class="runtime-mini"><span class="runtime-label">MCP 服務</span><strong>{{ mcp_status|length }}</strong></div>
<div class="runtime-mini"><span class="runtime-label">MCP 24 小時呼叫</span><strong>{{ "{:,}".format(mcp_24h|sum(attribute='total_calls')) if mcp_24h else 0 }}</strong></div>
<div class="runtime-mini"><span class="runtime-label">自癒劇本</span><strong>{{ active_playbooks.count }}/{{ playbook_ranking|length }}</strong></div>
<div class="runtime-mini"><span class="runtime-label">嵌入佇列</span><strong class="{% if embed_queue_failed > 0 %}status-bad{% elif embed_queue_pending > 0 %}status-warn{% else %}status-good{% endif %}">{{ embed_queue_pending }}/{{ embed_queue_failed }}</strong></div>
</div>
</div>
</article>
@@ -142,32 +142,32 @@
{% if throttle_state %}
<section class="runtime-table-shell">
<div class="runtime-table-title"><div><div class="runtime-label">Cost Throttle</div><h3>成本節流狀態</h3></div></div>
<div class="runtime-table-title"><div><div class="runtime-label">成本節流</div><h3>成本節流狀態</h3></div></div>
<div class="table-responsive"><table class="table mb-0"><thead class="table-light"><tr><th>供應商</th><th>已花費</th><th>預算</th><th>月底推估</th><th>使用率</th><th>狀態</th></tr></thead><tbody>{% for provider, info in throttle_state.items() %}<tr><td><code>{{ provider }}</code></td><td>${{ "%.2f"|format(info.spent) }}</td><td>${{ "%.2f"|format(info.budget) }}</td><td>${{ "%.2f"|format(info.projected) }}</td><td>{{ "%.0f"|format(info.ratio * 100) }}%</td><td>{% if info.throttled %}<span class="badge bg-danger">已節流</span>{% else %}<span class="badge bg-success">正常</span>{% endif %}</td></tr>{% endfor %}</tbody></table></div>
</section>
{% endif %}
{% if mcp_24h %}
<section class="runtime-table-shell">
<div class="runtime-table-title"><div><div class="runtime-label">MCP Workload</div><h3>MCP 服務 24h 工作量</h3></div></div>
<div class="table-responsive"><table class="table mb-0"><thead class="table-light"><tr><th>服務</th><th class="text-end">呼叫</th><th class="text-end">成功率</th><th class="text-end">快取</th><th class="text-end">Tools</th><th class="text-end">平均</th><th class="text-end">成本</th></tr></thead><tbody>{% for s in mcp_24h %}<tr><td><code>{{ s.server }}</code></td><td class="text-end">{{ "{:,}".format(s.total_calls) }}</td><td class="text-end"><strong class="{% if s.success_rate >= 95 %}status-good{% elif s.success_rate >= 80 %}status-warn{% else %}status-bad{% endif %}">{{ "%.1f"|format(s.success_rate) }}%</strong></td><td class="text-end">{{ "%.1f"|format(s.cache_rate) }}%</td><td class="text-end">{{ s.tools_used }}</td><td class="text-end">{{ s.avg_ms }} ms</td><td class="text-end">${{ "%.4f"|format(s.total_cost) }}</td></tr>{% endfor %}</tbody></table></div>
<div class="runtime-table-title"><div><div class="runtime-label">MCP 工作量</div><h3>MCP 服務 24h 工作量</h3></div></div>
<div class="table-responsive"><table class="table mb-0"><thead class="table-light"><tr><th>服務</th><th class="text-end">呼叫</th><th class="text-end">成功率</th><th class="text-end">快取</th><th class="text-end">工具</th><th class="text-end">平均</th><th class="text-end">成本</th></tr></thead><tbody>{% for s in mcp_24h %}<tr><td><code>{{ s.server }}</code></td><td class="text-end">{{ "{:,}".format(s.total_calls) }}</td><td class="text-end"><strong class="{% if s.success_rate >= 95 %}status-good{% elif s.success_rate >= 80 %}status-warn{% else %}status-bad{% endif %}">{{ "%.1f"|format(s.success_rate) }}%</strong></td><td class="text-end">{{ "%.1f"|format(s.cache_rate) }}%</td><td class="text-end">{{ s.tools_used }}</td><td class="text-end">{{ s.avg_ms }} ms</td><td class="text-end">${{ "%.4f"|format(s.total_cost) }}</td></tr>{% endfor %}</tbody></table></div>
</section>
{% endif %}
<section class="runtime-main">
<div class="runtime-stack">
<article class="runtime-table-shell"><div class="runtime-table-title"><div><div class="runtime-label">Incidents</div><h3>最近 10 筆事件</h3></div></div><div class="table-responsive">{% if recent_incidents %}<table class="table table-sm mb-0"><thead class="table-light"><tr><th>時間</th><th>任務</th><th>錯誤</th><th>等級</th><th>狀態</th><th>訊息</th></tr></thead><tbody>{% for i in recent_incidents %}<tr><td><small>{{ i.created_at }}</small></td><td><code>{{ i.task_name }}</code></td><td><span class="badge bg-secondary">{{ i.error_type }}</span></td><td><span class="badge {% if i.severity in ('P0','P1') %}bg-danger{% elif i.severity == 'P2' %}bg-warning{% else %}bg-info{% endif %}">{{ i.severity }}</span></td><td>{{ i.status }}</td><td><small class="text-muted">{{ i.error_message }}</small></td></tr>{% endfor %}</tbody></table>{% else %}<div class="text-muted text-center p-3 small">尚無 incident 紀錄</div>{% endif %}</div></article>
<article class="runtime-table-shell"><div class="runtime-table-title"><div><div class="runtime-label">Heal Logs</div><h3>最近 10 筆自癒</h3></div></div><div class="table-responsive">{% if recent_heals %}<table class="table table-sm mb-0"><thead class="table-light"><tr><th>時間</th><th>動作</th><th>結果</th><th class="text-end">耗時</th><th>細節</th></tr></thead><tbody>{% for h in recent_heals %}<tr><td><small>{{ h.created_at }}</small></td><td><span class="badge bg-info">{{ h.action_type or '—' }}</span></td><td>{% if h.result == 'success' %}<span class="badge bg-success">成功</span>{% elif h.result == 'failed' %}<span class="badge bg-danger">失敗</span>{% else %}<span class="badge bg-secondary">{{ h.result }}</span>{% endif %}</td><td class="text-end">{{ h.duration_ms }} ms</td><td><small class="text-muted">{{ h.action_detail }}</small></td></tr>{% endfor %}</tbody></table>{% else %}<div class="text-muted text-center p-3 small">尚無 heal log</div>{% endif %}</div></article>
<article class="runtime-table-shell"><div class="runtime-table-title"><div><div class="runtime-label">事件紀錄</div><h3>最近 10 筆事件</h3></div></div><div class="table-responsive">{% if recent_incidents %}<table class="table table-sm mb-0"><thead class="table-light"><tr><th>時間</th><th>任務</th><th>錯誤</th><th>等級</th><th>狀態</th><th>訊息</th></tr></thead><tbody>{% for i in recent_incidents %}<tr><td><small>{{ i.created_at }}</small></td><td><code>{{ i.task_name }}</code></td><td><span class="badge bg-secondary">{{ i.error_type }}</span></td><td><span class="badge {% if i.severity in ('P0','P1') %}bg-danger{% elif i.severity == 'P2' %}bg-warning{% else %}bg-info{% endif %}">{{ i.severity }}</span></td><td>{{ i.status }}</td><td><small class="text-muted">{{ i.error_message }}</small></td></tr>{% endfor %}</tbody></table>{% else %}<div class="text-muted text-center p-3 small">尚無事件紀錄</div>{% endif %}</div></article>
<article class="runtime-table-shell"><div class="runtime-table-title"><div><div class="runtime-label">自癒紀錄</div><h3>最近 10 筆自癒</h3></div></div><div class="table-responsive">{% if recent_heals %}<table class="table table-sm mb-0"><thead class="table-light"><tr><th>時間</th><th>動作</th><th>結果</th><th class="text-end">耗時</th><th>細節</th></tr></thead><tbody>{% for h in recent_heals %}<tr><td><small>{{ h.created_at }}</small></td><td><span class="badge bg-info">{{ h.action_type or '—' }}</span></td><td>{% if h.result == 'success' %}<span class="badge bg-success">成功</span>{% elif h.result == 'failed' %}<span class="badge bg-danger">失敗</span>{% else %}<span class="badge bg-secondary">{{ h.result }}</span>{% endif %}</td><td class="text-end">{{ h.duration_ms }} ms</td><td><small class="text-muted">{{ h.action_detail }}</small></td></tr>{% endfor %}</tbody></table>{% else %}<div class="text-muted text-center p-3 small">尚無自癒紀錄</div>{% endif %}</div></article>
</div>
<aside class="runtime-stack">
<article class="runtime-table-shell"><div class="runtime-table-title"><div><div class="runtime-label">Playbooks</div><h3>AutoHeal Playbook</h3></div></div><div class="table-responsive">{% if playbook_ranking %}<table class="table table-sm mb-0"><thead class="table-light"><tr><th>名稱</th><th>成功率</th><th>狀態</th><th>切換</th></tr></thead><tbody>{% for p in playbook_ranking %}<tr><td><strong>{{ p.name }}</strong><br><small class="text-muted"><code>{{ p.error_type }}</code> · {{ p.action_type }}</small></td><td>{% if (p.success + p.fail) > 0 %}<strong>{{ "%.0f"|format(p.success_rate) }}%</strong>{% else %}<span class="text-muted"></span>{% endif %}</td><td>{% if p.is_active %}<span class="badge bg-success">啟用</span>{% else %}<span class="badge bg-secondary">停用</span>{% endif %}</td><td><button class="btn btn-sm btn-outline-secondary" onclick="togglePlaybook({{ p.id }}, {{ p.name|tojson }})">切換</button></td></tr>{% endfor %}</tbody></table>{% else %}<div class="text-muted text-center p-3 small">尚無 playbook 資料</div>{% endif %}</div></article>
<article class="runtime-table-shell"><div class="runtime-table-title"><div><div class="runtime-label">Backup</div><h3>備份歷史 7 日</h3></div></div><div class="table-responsive">{% if backup_history %}<table class="table table-sm mb-0"><thead class="table-light"><tr><th>時間</th><th>狀態</th><th class="text-end">MB</th></tr></thead><tbody>{% for b in backup_history %}<tr><td><small>{{ b.created_at }}</small></td><td>{% if b.status == 'success' %}<span class="badge bg-success">成功</span>{% else %}<span class="badge bg-danger">{{ b.status }}</span>{% endif %}</td><td class="text-end">{{ b.size_mb }}</td></tr>{% endfor %}</tbody></table>{% else %}<div class="text-muted text-center p-3 small">過去 7 日無備份紀錄</div>{% endif %}</div></article>
<article class="runtime-table-shell"><div class="runtime-table-title"><div><div class="runtime-label">自癒劇本</div><h3>AutoHeal 劇本</h3></div></div><div class="table-responsive">{% if playbook_ranking %}<table class="table table-sm mb-0"><thead class="table-light"><tr><th>名稱</th><th>成功率</th><th>狀態</th><th>切換</th></tr></thead><tbody>{% for p in playbook_ranking %}<tr><td><strong>{{ p.name }}</strong><br><small class="text-muted"><code>{{ p.error_type }}</code> · {{ p.action_type }}</small></td><td>{% if (p.success + p.fail) > 0 %}<strong>{{ "%.0f"|format(p.success_rate) }}%</strong>{% else %}<span class="text-muted"></span>{% endif %}</td><td>{% if p.is_active %}<span class="badge bg-success">啟用</span>{% else %}<span class="badge bg-secondary">停用</span>{% endif %}</td><td><button class="btn btn-sm btn-outline-secondary" onclick="togglePlaybook({{ p.id }}, {{ p.name|tojson }})">切換</button></td></tr>{% endfor %}</tbody></table>{% else %}<div class="text-muted text-center p-3 small">尚無劇本資料</div>{% endif %}</div></article>
<article class="runtime-table-shell"><div class="runtime-table-title"><div><div class="runtime-label">備份</div><h3>備份歷史 7 日</h3></div></div><div class="table-responsive">{% if backup_history %}<table class="table table-sm mb-0"><thead class="table-light"><tr><th>時間</th><th>狀態</th><th class="text-end">MB</th></tr></thead><tbody>{% for b in backup_history %}<tr><td><small>{{ b.created_at }}</small></td><td>{% if b.status == 'success' %}<span class="badge bg-success">成功</span>{% else %}<span class="badge bg-danger">{{ b.status }}</span>{% endif %}</td><td class="text-end">{{ b.size_mb }}</td></tr>{% endfor %}</tbody></table>{% else %}<div class="text-muted text-center p-3 small">過去 7 日無備份紀錄</div>{% endif %}</div></article>
</aside>
</section>
{% if embed_queue_pending > 0 or embed_queue_failed > 0 %}<div class="alert alert-warning mt-3"><strong>Embedding 重試佇列:</strong>待處理 {{ embed_queue_pending }} 筆 · 失敗 {{ embed_queue_failed }} 筆</div>{% endif %}
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 — 基礎設施生命線</small></p>
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Ollama 優先策略 v5.0 — 基礎設施生命線</small></p>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>

View File

@@ -608,9 +608,9 @@
<div class="obs-section-eyebrow">RAG 品質</div>
<h3>RAG 與品質</h3>
<div class="obs-route-list">
<a class="obs-route-card" href="/observability/promotion_review"><span class="obs-route-icon"><i class="fas fa-brain"></i></span><span><span class="obs-route-title">RAG 晉升審核</span><span class="obs-route-desc">Promotion Gate 與人工審核。</span></span><span class="obs-route-code">07</span></a>
<a class="obs-route-card" href="/observability/promotion_review"><span class="obs-route-icon"><i class="fas fa-brain"></i></span><span><span class="obs-route-title">RAG 晉升審核</span><span class="obs-route-desc">晉升守門與人工審核。</span></span><span class="obs-route-code">07</span></a>
<a class="obs-route-card" href="/observability/rag_queries"><span class="obs-route-icon"><i class="fas fa-magnifying-glass-chart"></i></span><span><span class="obs-route-title">RAG 召回詳情</span><span class="obs-route-desc">Query hits、節省呼叫、反饋追蹤。</span></span><span class="obs-route-code">08</span></a>
<a class="obs-route-card" href="/observability/quality_trend"><span class="obs-route-icon"><i class="fas fa-comments"></i></span><span><span class="obs-route-title">反饋趨勢</span><span class="obs-route-desc">Caller 品質、蒸餾池、根因建議。</span></span><span class="obs-route-code">09</span></a>
<a class="obs-route-card" href="/observability/quality_trend"><span class="obs-route-icon"><i class="fas fa-comments"></i></span><span><span class="obs-route-title">反饋趨勢</span><span class="obs-route-desc">呼叫端品質、蒸餾池、根因建議。</span></span><span class="obs-route-code">09</span></a>
<a class="obs-route-card" href="/observability/ppt_audit_history"><span class="obs-route-icon"><i class="fas fa-search"></i></span><span><span class="obs-route-title">PPT 視覺審核</span><span class="obs-route-desc">PPT audit、RAG 修法、AiderHeal。</span></span><span class="obs-route-code">10</span></a>
</div>
</div>

View File

@@ -9,23 +9,23 @@
</style>
<div class="container-fluid mt-3">
<section class="ppt-hero"><div class="ppt-kicker"><i class="fas fa-search me-1"></i> PPT Visual QA Pipeline · minicpm-v / AiderHeal / RAG Fixes</div><h1 class="ppt-title">PPT 視覺 QA 產線</h1><p class="ppt-subtitle">這頁追蹤每份自動簡報是否通過視覺審核檔案產出、minicpm-v audit、Telegram 推送、RAG 修法建議與 AiderHeal 自動修 generator。</p><div class="ppt-command"><div class="ppt-signal"><div class="ppt-label">Vision</div><span class="ppt-value {% if vision_enabled %}status-good{% else %}status-warn{% endif %}">{{ 'ON' if vision_enabled else 'OFF' }}</span><small class="text-muted">PPT_VISION_ENABLED</small></div><div class="ppt-signal"><div class="ppt-label">30d Total</div><span class="ppt-value">{{ audit_30d_stats.total if audit_30d_stats else 0 }}</span><small class="text-muted">audit records</small></div><div class="ppt-signal"><div class="ppt-label">Pass Rate</div><span class="ppt-value {% if audit_30d_stats and audit_30d_stats.pass_rate >= 80 %}status-good{% elif audit_30d_stats and audit_30d_stats.pass_rate >= 60 %}status-warn{% else %}status-bad{% endif %}">{{ "%.0f"|format(audit_30d_stats.pass_rate) if audit_30d_stats else '—' }}{% if audit_30d_stats %}%{% endif %}</span><small class="text-muted">過去 30 日</small></div><div class="ppt-signal"><div class="ppt-label">Issues</div><span class="ppt-value {% if audit_30d_stats and audit_30d_stats.total_issues > 0 %}status-warn{% else %}status-good{% endif %}">{{ audit_30d_stats.total_issues if audit_30d_stats else 0 }}</span><small class="text-muted">視覺問題數</small></div></div></section>
<section class="ppt-hero"><div class="ppt-kicker"><i class="fas fa-search me-1"></i> PPT 視覺 QA 產線 · minicpm-v / AiderHeal / RAG 修法</div><h1 class="ppt-title">PPT 視覺 QA 產線</h1><p class="ppt-subtitle">這頁追蹤每份自動簡報是否通過視覺審核檔案產出、minicpm-v 審核、Telegram 推送、RAG 修法建議與 AiderHeal 自動修 generator。</p><div class="ppt-command"><div class="ppt-signal"><div class="ppt-label">視覺模型</div><span class="ppt-value {% if vision_enabled %}status-good{% else %}status-warn{% endif %}">{{ '啟用' if vision_enabled else '停用' }}</span><small class="text-muted">PPT_VISION_ENABLED</small></div><div class="ppt-signal"><div class="ppt-label">30 日總量</div><span class="ppt-value">{{ audit_30d_stats.total if audit_30d_stats else 0 }}</span><small class="text-muted">審核紀錄</small></div><div class="ppt-signal"><div class="ppt-label">通過率</div><span class="ppt-value {% if audit_30d_stats and audit_30d_stats.pass_rate >= 80 %}status-good{% elif audit_30d_stats and audit_30d_stats.pass_rate >= 60 %}status-warn{% else %}status-bad{% endif %}">{{ "%.0f"|format(audit_30d_stats.pass_rate) if audit_30d_stats else '—' }}{% if audit_30d_stats %}%{% endif %}</span><small class="text-muted">過去 30 日</small></div><div class="ppt-signal"><div class="ppt-label">問題數</div><span class="ppt-value {% if audit_30d_stats and audit_30d_stats.total_issues > 0 %}status-warn{% else %}status-good{% endif %}">{{ audit_30d_stats.total_issues if audit_30d_stats else 0 }}</span><small class="text-muted">視覺問題數</small></div></div></section>
{% if error %}<div class="alert alert-warning mt-3"><strong><i class="fas fa-triangle-exclamation me-1"></i></strong>{{ error }}</div>{% endif %}
<section class="ppt-grid">
<div class="ppt-stack">
<article class="ppt-table-shell"><div class="ppt-table-title"><div><div class="ppt-label">Audit History</div><h3>視覺審核歷史 100 筆</h3></div></div><div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>時間</th><th>檔名</th><th>結果</th><th class="text-end">問題</th><th class="text-end">信心</th><th class="text-end">耗時</th><th>錯誤</th><th>動作</th></tr></thead><tbody>{% for r in audit_records %}<tr><td><small>{{ r.audited_at }}</small></td><td><code>{{ r.pptx_filename }}</code></td><td>{% if r.audit_status == 'passed' %}<span class="badge bg-success">通過</span>{% elif r.audit_status == 'failed' %}<span class="badge bg-warning">有問題</span>{% elif r.audit_status == 'error' %}<span class="badge bg-danger">錯誤</span>{% elif r.audit_status == 'skipped' %}<span class="badge bg-secondary">跳過</span>{% else %}<span class="badge bg-light text-dark">{{ r.audit_status }}</span>{% endif %}</td><td class="text-end">{{ r.issues_count }}</td><td class="text-end">{{ "%.2f"|format(r.confidence) }}</td><td class="text-end">{{ r.duration_ms }}</td><td><small class="text-muted">{{ (r.error_msg or '')[:80] }}</small></td><td>{% if r.audit_status in ('failed','error') %}<button class="btn btn-sm btn-outline-warning" onclick="triggerAiderHeal({{ r.pptx_filename|tojson }}, {{ (r.error_msg or '')|tojson }})"><i class="fas fa-wrench me-1"></i>AiderHeal</button>{% endif %}</td></tr>{% else %}<tr><td colspan="8" class="text-center text-muted">尚無審核紀錄</td></tr>{% endfor %}</tbody></table></div></article>
<article class="ppt-table-shell"><div class="ppt-table-title"><div><div class="ppt-label">Generated Files</div><h3>過去 7 日 PPT 檔案</h3></div></div><div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>檔名</th><th class="text-end">KB</th><th>修改時間</th><th>狀態</th></tr></thead><tbody>{% for f in files %}<tr><td><code>{{ f.name }}</code></td><td class="text-end">{{ f.size_kb }}</td><td><small>{{ f.mtime }}</small></td><td><small class="text-muted">22:00 cron 自動審核</small></td></tr>{% else %}<tr><td colspan="4" class="text-center text-muted">過去 7 日無 PPT 生成</td></tr>{% endfor %}</tbody></table></div></article>
<article class="ppt-table-shell"><div class="ppt-table-title"><div><div class="ppt-label">審核歷史</div><h3>視覺審核歷史 100 筆</h3></div></div><div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>時間</th><th>檔名</th><th>結果</th><th class="text-end">問題</th><th class="text-end">信心</th><th class="text-end">耗時</th><th>錯誤</th><th>動作</th></tr></thead><tbody>{% for r in audit_records %}<tr><td><small>{{ r.audited_at }}</small></td><td><code>{{ r.pptx_filename }}</code></td><td>{% if r.audit_status == 'passed' %}<span class="badge bg-success">通過</span>{% elif r.audit_status == 'failed' %}<span class="badge bg-warning">有問題</span>{% elif r.audit_status == 'error' %}<span class="badge bg-danger">錯誤</span>{% elif r.audit_status == 'skipped' %}<span class="badge bg-secondary">跳過</span>{% else %}<span class="badge bg-light text-dark">{{ r.audit_status }}</span>{% endif %}</td><td class="text-end">{{ r.issues_count }}</td><td class="text-end">{{ "%.2f"|format(r.confidence) }}</td><td class="text-end">{{ r.duration_ms }}</td><td><small class="text-muted">{{ (r.error_msg or '')[:80] }}</small></td><td>{% if r.audit_status in ('failed','error') %}<button class="btn btn-sm btn-outline-warning" onclick="triggerAiderHeal({{ r.pptx_filename|tojson }}, {{ (r.error_msg or '')|tojson }})"><i class="fas fa-wrench me-1"></i>AiderHeal</button>{% endif %}</td></tr>{% else %}<tr><td colspan="8" class="text-center text-muted">尚無審核紀錄</td></tr>{% endfor %}</tbody></table></div></article>
<article class="ppt-table-shell"><div class="ppt-table-title"><div><div class="ppt-label">已產檔案</div><h3>過去 7 日 PPT 檔案</h3></div></div><div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>檔名</th><th class="text-end">KB</th><th>修改時間</th><th>狀態</th></tr></thead><tbody>{% for f in files %}<tr><td><code>{{ f.name }}</code></td><td class="text-end">{{ f.size_kb }}</td><td><small>{{ f.mtime }}</small></td><td><small class="text-muted">22:00 cron 自動審核</small></td></tr>{% else %}<tr><td colspan="4" class="text-center text-muted">過去 7 日無 PPT 生成</td></tr>{% endfor %}</tbody></table></div></article>
</div>
<aside class="ppt-stack">
{% if audit_30d_stats and audit_30d_stats.total > 0 %}<article class="ppt-panel"><div class="ppt-panel-head"><div><div class="ppt-label">30d Audit Mix</div><h2 class="ppt-panel-title">審核結果分布</h2></div></div><div class="ppt-panel-body"><div class="obs-chart-frame"><canvas id="pptAuditPieChart"></canvas></div><div class="ppt-mini-grid mt-3"><div class="ppt-mini"><span class="ppt-label">Passed</span><strong class="status-good">{{ audit_30d_stats.passed }}</strong></div><div class="ppt-mini"><span class="ppt-label">Failed</span><strong class="{% if audit_30d_stats.failed > 0 %}status-warn{% endif %}">{{ audit_30d_stats.failed }}</strong></div><div class="ppt-mini"><span class="ppt-label">Error</span><strong class="{% if audit_30d_stats.error > 0 %}status-bad{% endif %}">{{ audit_30d_stats.error }}</strong></div><div class="ppt-mini"><span class="ppt-label">Confidence</span><strong>{{ "%.2f"|format(audit_30d_stats.avg_confidence) }}</strong></div></div></div></article>{% endif %}
{% if top_failure_files %}<article class="ppt-panel"><div class="ppt-panel-head"><div><div class="ppt-label">Failure Hotspots</div><h2 class="ppt-panel-title">Top 失敗檔案</h2></div></div><div class="ppt-panel-body">{% for f in top_failure_files %}<div class="fix-card"><code>{{ f.filename }}</code><div class="d-flex justify-content-between mt-1"><small class="text-muted">{{ f.last_audit }}</small><span class="badge bg-warning">{{ f.attempts }} 次</span></div><small class="text-muted">issues {{ f.total_issues }}</small></div>{% endfor %}</div></article>{% endif %}
{% if audit_30d_stats and audit_30d_stats.total > 0 %}<article class="ppt-panel"><div class="ppt-panel-head"><div><div class="ppt-label">30 日審核分布</div><h2 class="ppt-panel-title">審核結果分布</h2></div></div><div class="ppt-panel-body"><div class="obs-chart-frame"><canvas id="pptAuditPieChart"></canvas></div><div class="ppt-mini-grid mt-3"><div class="ppt-mini"><span class="ppt-label">通過</span><strong class="status-good">{{ audit_30d_stats.passed }}</strong></div><div class="ppt-mini"><span class="ppt-label">失敗</span><strong class="{% if audit_30d_stats.failed > 0 %}status-warn{% endif %}">{{ audit_30d_stats.failed }}</strong></div><div class="ppt-mini"><span class="ppt-label">錯誤</span><strong class="{% if audit_30d_stats.error > 0 %}status-bad{% endif %}">{{ audit_30d_stats.error }}</strong></div><div class="ppt-mini"><span class="ppt-label">信心分</span><strong>{{ "%.2f"|format(audit_30d_stats.avg_confidence) }}</strong></div></div></div></article>{% endif %}
{% if top_failure_files %}<article class="ppt-panel"><div class="ppt-panel-head"><div><div class="ppt-label">失敗熱點</div><h2 class="ppt-panel-title">Top 失敗檔案</h2></div></div><div class="ppt-panel-body">{% for f in top_failure_files %}<div class="fix-card"><code>{{ f.filename }}</code><div class="d-flex justify-content-between mt-1"><small class="text-muted">{{ f.last_audit }}</small><span class="badge bg-warning">{{ f.attempts }} 次</span></div><small class="text-muted">問題 {{ f.total_issues }}</small></div>{% endfor %}</div></article>{% endif %}
</aside>
</section>
{% if rag_fixes %}<section class="ppt-panel mt-3"><div class="ppt-panel-head"><div><div class="ppt-label">RAG Fix Suggestions</div><h2 class="ppt-panel-title">RAG 自動修法建議</h2></div></div><div class="ppt-panel-body">{% for fix in rag_fixes %}<div class="fix-card"><strong><code>{{ fix.pptx_filename }}</code></strong><small class="text-muted ms-2">{{ fix.audited_at }}</small><div class="small status-bad mt-1">{{ fix.error_msg }}</div><ul class="list-unstyled mt-2 mb-0 small">{% for h in fix.hits %}<li class="mb-1"><span class="badge bg-info me-1">{{ h.insight_type }}</span><span class="badge bg-light text-dark me-1">相似度 {{ "%.2f"|format(h.similarity) }}</span>{{ h.content }}{% if h.content|length >= 200 %}…{% endif %}</li>{% endfor %}</ul></div>{% endfor %}</div></section>{% endif %}
{% if rag_fixes %}<section class="ppt-panel mt-3"><div class="ppt-panel-head"><div><div class="ppt-label">RAG 修法建議</div><h2 class="ppt-panel-title">RAG 自動修法建議</h2></div></div><div class="ppt-panel-body">{% for fix in rag_fixes %}<div class="fix-card"><strong><code>{{ fix.pptx_filename }}</code></strong><small class="text-muted ms-2">{{ fix.audited_at }}</small><div class="small status-bad mt-1">{{ fix.error_msg }}</div><ul class="list-unstyled mt-2 mb-0 small">{% for h in fix.hits %}<li class="mb-1"><span class="badge bg-info me-1">{{ h.insight_type }}</span><span class="badge bg-light text-dark me-1">相似度 {{ "%.2f"|format(h.similarity) }}</span>{{ h.content }}{% if h.content|length >= 200 %}…{% endif %}</li>{% endfor %}</ul></div>{% endfor %}</div></section>{% endif %}
{% if (not audit_30d_stats or audit_30d_stats.total == 0) and not vision_enabled %}<div class="alert alert-info mt-3"><strong>為什麼這頁空?</strong><ul class="mb-0 small mt-2"><li>PPT_VISION_ENABLED=false</li><li>188 主機需安裝 LibreOffice</li><li>需 Ollama 拉取 minicpm-v 模型</li><li>啟用後每日 22:00 cron 寫入 ppt_audit_results</li></ul></div>{% endif %}
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 — PPT 視覺 QA 產線</small></p>
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Ollama 優先策略 v5.0 — PPT 視覺 QA 產線</small></p>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script><script>(function(){const stats={{ audit_30d_stats | default({}) | tojson }};const el=document.getElementById('pptAuditPieChart');if(!el||!stats.total)return;const data=[{label:'通過',value:stats.passed||0,color:'#4f8a5b'},{label:'失敗',value:stats.failed||0,color:'#b8792f'},{label:'錯誤',value:stats.error||0,color:'#b94b45'},{label:'跳過',value:stats.skipped||0,color:'#8b8077'}].filter(d=>d.value>0);if(!data.length)return;new Chart(el,{type:'doughnut',data:{labels:data.map(d=>d.label),datasets:[{data:data.map(d=>d.value),backgroundColor:data.map(d=>d.color),borderWidth:1,borderColor:'#fff'}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{font:{size:12}}}}}});})();

View File

@@ -1,6 +1,6 @@
{% extends "ewoooc_base.html" %}
{% block title %}RAG Promotion Gate{% endblock %}
{% block title %}RAG 知識晉升閘{% endblock %}
{% block ewooo_content %}
<style>
@@ -40,14 +40,14 @@
<div class="container-fluid mt-3">
<section class="gate-hero">
<div class="gate-kicker"><i class="fas fa-brain me-1"></i> RAG Promotion Gate · Human Review / Dedup / Anti-pollution</div>
<div class="gate-kicker"><i class="fas fa-brain me-1"></i> RAG 知識晉升閘 · 人工審核 / 去重 / 防污染</div>
<h1 class="gate-title">RAG 知識晉升閘</h1>
<p class="gate-subtitle">這頁是 RAG 不被污染的最後關卡。高權重 learning episode 不能直接進知識庫,必須先看品質、相似知識、人工拒絕與晉升分布,再決定是否寫入 ai_insights。</p>
<p class="gate-subtitle">這頁是 RAG 不被污染的最後關卡。高權重 學習片段 不能直接進知識庫,必須先看品質、相似知識、人工拒絕與晉升分布,再決定是否寫入 ai_insights。</p>
<div class="gate-command">
<div class="gate-signal"><div class="gate-label">Awaiting Review</div><span class="gate-value {% if episodes|length > 0 %}status-warn{% else %}status-good{% endif %}">{{ episodes|length }}</span><small class="text-muted">高權重待審片段</small></div>
<div class="gate-signal"><div class="gate-label">Knowledge Base</div><span class="gate-value">{{ kb_size or 0 }}</span><small class="text-muted">ai_insights 已晉升</small></div>
<div class="gate-signal"><div class="gate-label">30d Approval</div><span class="gate-value status-blue">{{ "%.0f"|format(approval_rate) }}%</span><small class="text-muted">{{ approved_30d }}/{{ total_dist }} episodes</small></div>
<div class="gate-signal"><div class="gate-label">Rejected 30d</div><span class="gate-value {% if rejected_30d.value > 0 %}status-bad{% else %}status-good{% endif %}">{{ rejected_30d.value }}</span><small class="text-muted">品質 / 幻覺 / 重複 / 人工拒</small></div>
<div class="gate-signal"><div class="gate-label">待審核</div><span class="gate-value {% if episodes|length > 0 %}status-warn{% else %}status-good{% endif %}">{{ episodes|length }}</span><small class="text-muted">高權重待審片段</small></div>
<div class="gate-signal"><div class="gate-label">知識庫</div><span class="gate-value">{{ kb_size or 0 }}</span><small class="text-muted">ai_insights 已晉升</small></div>
<div class="gate-signal"><div class="gate-label">30 日通過率</div><span class="gate-value status-blue">{{ "%.0f"|format(approval_rate) }}%</span><small class="text-muted">{{ approved_30d }}/{{ total_dist }} 個片段</small></div>
<div class="gate-signal"><div class="gate-label">30 日拒絕</div><span class="gate-value {% if rejected_30d.value > 0 %}status-bad{% else %}status-good{% endif %}">{{ rejected_30d.value }}</span><small class="text-muted">品質 / 幻覺 / 重複 / 人工拒</small></div>
</div>
</section>
@@ -56,9 +56,9 @@
<section class="gate-grid">
<div class="gate-stack">
<article class="gate-panel">
<div class="gate-panel-head"><div><div class="gate-label">Review Queue</div><h2 class="gate-panel-title">待審核片段</h2></div><span class="badge {% if episodes %}bg-warning{% else %}bg-success{% endif %}">{{ episodes|length }} 筆</span></div>
<div class="gate-panel-head"><div><div class="gate-label">審核佇列</div><h2 class="gate-panel-title">待審核片段</h2></div><span class="badge {% if episodes %}bg-warning{% else %}bg-success{% endif %}">{{ episodes|length }} 筆</span></div>
<div class="gate-panel-body">
<p class="text-muted small mb-0"><i class="fas fa-shield-halved me-1"></i>PromotionGate Stage 4weight ≥ 0.8 必經統帥審核24 小時無回應自動降權,不直接污染知識庫。</p>
<p class="text-muted small mb-0"><i class="fas fa-shield-halved me-1"></i>晉升守門第 4 階段:權重 ≥ 0.8 必經統帥審核24 小時無回應自動降權,不直接污染知識庫。</p>
</div>
</article>
@@ -83,19 +83,19 @@
<aside class="gate-stack">
{% if episode_distribution_30d %}
<article class="gate-panel"><div class="gate-panel-head"><div><div class="gate-label">Distillation Pool</div><h2 class="gate-panel-title">30 日狀態分布</h2></div></div><div class="gate-panel-body"><div class="obs-chart-frame obs-chart-frame-tall"><canvas id="episodeDistChart"></canvas></div></div></article>
<article class="gate-panel"><div class="gate-panel-head"><div><div class="gate-label">蒸餾池</div><h2 class="gate-panel-title">30 日狀態分布</h2></div></div><div class="gate-panel-body"><div class="obs-chart-frame obs-chart-frame-tall"><canvas id="episodeDistChart"></canvas></div></div></article>
{% endif %}
{% if strategy_weights %}
<article class="gate-panel"><div class="gate-panel-head"><div><div class="gate-label">OpenClaw Weights</div><h2 class="gate-panel-title">策略權重 Top</h2></div></div><div class="gate-panel-body"><div class="gate-mini-grid">{% for s in strategy_weights[:6] %}<div class="gate-mini"><span class="gate-label">{{ s.strategy_key[:22] }}</span><strong>{{ "%.2f"|format(s.weight) }}</strong><small class="text-muted">成功 {{ s.success }} · 失敗 {{ s.fail }}</small></div>{% endfor %}</div></div></article>
<article class="gate-panel"><div class="gate-panel-head"><div><div class="gate-label">OpenClaw 權重</div><h2 class="gate-panel-title">策略權重 Top</h2></div></div><div class="gate-panel-body"><div class="gate-mini-grid">{% for s in strategy_weights[:6] %}<div class="gate-mini"><span class="gate-label">{{ s.strategy_key[:22] }}</span><strong>{{ "%.2f"|format(s.weight) }}</strong><small class="text-muted">成功 {{ s.success }} · 失敗 {{ s.fail }}</small></div>{% endfor %}</div></div></article>
{% endif %}
</aside>
</section>
{% if latest_insights %}
<section class="gate-table-shell"><div class="gate-table-title"><div><div class="gate-label">Knowledge Base</div><h3>最近 10 筆 ai_insights</h3></div></div><div class="table-responsive"><table class="table table-sm mb-0"><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">{{ 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></section>
<section class="gate-table-shell"><div class="gate-table-title"><div><div class="gate-label">知識庫</div><h3>最近 10 筆 ai_insights</h3></div></div><div class="table-responsive"><table class="table table-sm mb-0"><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">{{ 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></section>
{% endif %}
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 — RAG 知識晉升閘</small></p>
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Ollama 優先策略 v5.0 — RAG 知識晉升閘</small></p>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>

View File

@@ -17,24 +17,24 @@
{% set rag_total = (rag_overall_dist | sum(attribute='count')) if rag_overall_dist else 0 %}
<div class="container-fluid mt-3">
<section class="quality-hero"><div class="quality-kicker"><i class="fas fa-comments me-1"></i> Quality Diagnostics · {{ days }}d Window</div><h1 class="quality-title">AI 品質診斷台</h1><p class="quality-subtitle">這裡看 AI 的回答到底有沒有變好:caller 反饋、RAG 分數、learning episode 流量、action plan 與 outcome 閉環全部聚合到同一張品質雷達。</p><form method="get" class="quality-filter"><select name="days" class="form-select form-select-sm">{% for d in [7,14,30,90] %}<option value="{{ d }}" {% if days == d %}selected{% endif %}>{{ d }} 日</option>{% endfor %}</select><button class="btn btn-primary btn-sm">查詢</button></form><div class="quality-command"><div class="quality-signal"><div class="quality-label">Feedback</div><span class="quality-value">{{ total_feedback.value }}</span><small class="text-muted">caller feedback 總量</small></div><div class="quality-signal"><div class="quality-label">Worst Avg</div><span class="quality-value {% if worst_avg.value >= 4 %}status-good{% elif worst_avg.value >= 3 %}status-warn{% else %}status-bad{% endif %}">{{ "%.2f"|format(worst_avg.value) }}</span><small class="text-muted">最差 caller 平均分</small></div><div class="quality-signal"><div class="quality-label">Episodes</div><span class="quality-value status-blue">{{ episode_total }}</span><small class="text-muted">蒸餾池 {{ days }} 日</small></div><div class="quality-signal"><div class="quality-label">RAG Scores</div><span class="quality-value">{{ rag_total }}</span><small class="text-muted">已回饋 RAG query</small></div></div></section>
<section class="quality-hero"><div class="quality-kicker"><i class="fas fa-comments me-1"></i> 品質診斷 · {{ days }} 日視窗</div><h1 class="quality-title">AI 品質診斷台</h1><p class="quality-subtitle">這裡看 AI 的回答到底有沒有變好:呼叫端反饋、RAG 分數、學習片段流量、行動計畫與結果閉環全部聚合到同一張品質雷達。</p><form method="get" class="quality-filter"><select name="days" class="form-select form-select-sm">{% for d in [7,14,30,90] %}<option value="{{ d }}" {% if days == d %}selected{% endif %}>{{ d }} 日</option>{% endfor %}</select><button class="btn btn-primary btn-sm">查詢</button></form><div class="quality-command"><div class="quality-signal"><div class="quality-label">反饋總量</div><span class="quality-value">{{ total_feedback.value }}</span><small class="text-muted">呼叫端反饋總量</small></div><div class="quality-signal"><div class="quality-label">最差均分</div><span class="quality-value {% if worst_avg.value >= 4 %}status-good{% elif worst_avg.value >= 3 %}status-warn{% else %}status-bad{% endif %}">{{ "%.2f"|format(worst_avg.value) }}</span><small class="text-muted">最差呼叫端平均分</small></div><div class="quality-signal"><div class="quality-label">蒸餾樣本</div><span class="quality-value status-blue">{{ episode_total }}</span><small class="text-muted">蒸餾池 {{ days }} 日</small></div><div class="quality-signal"><div class="quality-label">RAG 評分</div><span class="quality-value">{{ rag_total }}</span><small class="text-muted">已回饋 RAG 查詢</small></div></div></section>
{% if error %}<div class="alert alert-warning mt-3"><strong><i class="fas fa-triangle-exclamation me-1"></i></strong>{{ error }}</div>{% endif %}
<section class="quality-grid">
<div class="quality-stack">
<article class="quality-table-shell"><div class="quality-table-title"><div><div class="quality-label">Caller Feedback</div><h3>呼叫端 × 反饋分佈</h3></div></div><div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>呼叫端</th><th class="text-end">平均</th><th class="text-end"></th><th class="text-end">倒讚</th><th class="text-end">總數</th><th>趨勢</th><th>分布</th></tr></thead><tbody>{% for caller, info in trends %}<tr><td><code>{{ caller }}</code></td><td class="text-end"><strong class="{% if info.avg_score >= 4 %}status-good{% elif info.avg_score >= 3 %}status-warn{% else %}status-bad{% endif %}">{{ "%.2f"|format(info.avg_score) }}</strong>/5</td><td class="text-end status-good">{{ info.thumbs_up }}</td><td class="text-end status-bad">{{ info.thumbs_down }}</td><td class="text-end">{{ info.total_feedback }}</td><td>{% if info.trend == 'positive' %}<span class="badge bg-success">正向</span>{% elif info.trend == 'negative' %}<span class="badge bg-danger">負向</span>{% elif info.trend == 'neutral' %}<span class="badge bg-secondary">中性</span>{% else %}<span class="badge bg-light text-dark">無資料</span>{% endif %}</td><td class="quality-distribution-cell"><div class="progress obs-progress-sm"><div class="progress-bar" style="width:{{ (info.avg_score / 5 * 100)|int }}%"></div></div></td></tr>{% else %}<tr><td colspan="7" class="text-center text-muted">無反饋資料</td></tr>{% endfor %}</tbody></table></div></article>
{% if action_plans_status %}<article class="quality-table-shell"><div class="quality-table-title"><div><div class="quality-label">Action Plans</div><h3>Action Plans 狀態分布</h3></div></div><div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>狀態</th><th>計畫類型</th><th class="text-end">數量</th></tr></thead><tbody>{% for a in action_plans_status %}<tr><td><span class="badge {% if a.status == 'approved' %}bg-success{% elif a.status == 'pending' %}bg-warning{% elif a.status == 'rejected' %}bg-danger{% else %}bg-secondary{% endif %}">{{ a.status }}</span></td><td><code>{{ a.plan_type }}</code></td><td class="text-end">{{ a.count }}</td></tr>{% endfor %}</tbody></table></div></article>{% endif %}
<article class="quality-table-shell"><div class="quality-table-title"><div><div class="quality-label">呼叫端反饋</div><h3>呼叫端 × 反饋分佈</h3></div></div><div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>呼叫端</th><th class="text-end">平均</th><th class="text-end"></th><th class="text-end">倒讚</th><th class="text-end">總數</th><th>趨勢</th><th>分布</th></tr></thead><tbody>{% for caller, info in trends %}<tr><td><code>{{ caller }}</code></td><td class="text-end"><strong class="{% if info.avg_score >= 4 %}status-good{% elif info.avg_score >= 3 %}status-warn{% else %}status-bad{% endif %}">{{ "%.2f"|format(info.avg_score) }}</strong>/5</td><td class="text-end status-good">{{ info.thumbs_up }}</td><td class="text-end status-bad">{{ info.thumbs_down }}</td><td class="text-end">{{ info.total_feedback }}</td><td>{% if info.trend == 'positive' %}<span class="badge bg-success">正向</span>{% elif info.trend == 'negative' %}<span class="badge bg-danger">負向</span>{% elif info.trend == 'neutral' %}<span class="badge bg-secondary">中性</span>{% else %}<span class="badge bg-light text-dark">無資料</span>{% endif %}</td><td class="quality-distribution-cell"><div class="progress obs-progress-sm"><div class="progress-bar" style="width:{{ (info.avg_score / 5 * 100)|int }}%"></div></div></td></tr>{% else %}<tr><td colspan="7" class="text-center text-muted">無反饋資料</td></tr>{% endfor %}</tbody></table></div></article>
{% if action_plans_status %}<article class="quality-table-shell"><div class="quality-table-title"><div><div class="quality-label">行動計畫</div><h3>行動計畫 狀態分布</h3></div></div><div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>狀態</th><th>計畫類型</th><th class="text-end">數量</th></tr></thead><tbody>{% for a in action_plans_status %}<tr><td><span class="badge {% if a.status == 'approved' %}bg-success{% elif a.status == 'pending' %}bg-warning{% elif a.status == 'rejected' %}bg-danger{% else %}bg-secondary{% endif %}">{{ a.status }}</span></td><td><code>{{ a.plan_type }}</code></td><td class="text-end">{{ a.count }}</td></tr>{% endfor %}</tbody></table></div></article>{% endif %}
</div>
<aside class="quality-stack">
{% if rag_overall_dist %}<article class="quality-panel"><div class="quality-panel-head"><div><div class="quality-label">RAG Feedback</div><h2 class="quality-panel-title">RAG 分數分布</h2></div></div><div class="quality-panel-body"><div class="obs-chart-frame"><canvas id="ragFeedbackPieChart"></canvas></div></div></article>{% endif %}
{% if episode_distribution %}<article class="quality-panel"><div class="quality-panel-head"><div><div class="quality-label">Learning Pool</div><h2 class="quality-panel-title">蒸餾池狀態</h2></div></div><div class="quality-panel-body"><div class="quality-mini-grid">{% for status, cnt in episode_distribution.items() %}<div class="quality-mini"><span class="quality-label">{{ status }}</span><strong>{{ cnt }}</strong></div>{% endfor %}</div></div></article>{% endif %}
{% if rag_overall_dist %}<article class="quality-panel"><div class="quality-panel-head"><div><div class="quality-label">RAG 反饋總量</div><h2 class="quality-panel-title">RAG 分數分布</h2></div></div><div class="quality-panel-body"><div class="obs-chart-frame"><canvas id="ragFeedbackPieChart"></canvas></div></div></article>{% endif %}
{% if episode_distribution %}<article class="quality-panel"><div class="quality-panel-head"><div><div class="quality-label">學習池</div><h2 class="quality-panel-title">蒸餾池狀態</h2></div></div><div class="quality-panel-body"><div class="quality-mini-grid">{% for status, cnt in episode_distribution.items() %}<div class="quality-mini"><span class="quality-label">{{ status }}</span><strong>{{ cnt }}</strong></div>{% endfor %}</div></div></article>{% endif %}
</aside>
</section>
{% if rag_root_causes %}<section class="quality-panel mt-3"><div class="quality-panel-head"><div><div class="quality-label">Root Cause</div><h2 class="quality-panel-title">RAG 自動根因建議</h2></div></div><div class="quality-panel-body">{% for rc in rag_root_causes %}<div class="root-card"><strong><code>{{ rc.caller }}</code></strong><span class="badge bg-danger ms-1">{{ "%.2f"|format(rc.avg_score) }}/5</span><span class="badge bg-secondary ms-1">{{ rc.feedback_n }} 筆</span><ul class="list-unstyled mt-2 mb-0 small">{% for h in rc.hits %}<li class="mb-1"><span class="badge bg-info me-1">{{ h.insight_type }}</span><span class="badge bg-light text-dark me-1">相似度 {{ "%.2f"|format(h.similarity) }}</span>{{ h.content }}{% if h.content|length >= 200 %}…{% endif %}</li>{% endfor %}</ul></div>{% endfor %}</div></section>{% endif %}
{% if recommendations %}<section class="quality-panel mt-3"><div class="quality-panel-head"><div><div class="quality-label">Recommendations</div><h2 class="quality-panel-title">智能建議</h2></div></div><div class="quality-panel-body"><ul class="mb-0">{% for rec in recommendations %}<li>{% if rec.action == 'review' %}<i class="fas fa-triangle-exclamation status-warn me-1"></i>{% else %}<i class="fas fa-check status-good me-1"></i>{% endif %}<code>{{ rec.caller }}</code>{{ rec.reason }}</li>{% endfor %}</ul></div></section>{% endif %}
{% if action_outcomes_stats %}<section class="quality-panel mt-3"><div class="quality-panel-head"><div><div class="quality-label">Action Outcomes</div><h2 class="quality-panel-title">實際動作成效</h2></div></div><div class="quality-panel-body"><div class="quality-mini-grid">{% set total_ao = (action_outcomes_stats | sum(attribute='count')) or 1 %}{% for r in action_outcomes_stats %}<div class="quality-mini"><span class="quality-label">{{ r.verdict }}</span><strong class="{% if r.verdict == 'effective' %}status-good{% elif r.verdict == 'backfired' %}status-bad{% endif %}">{{ r.count }}</strong><small class="text-muted">{{ "%.1f"|format(r.count / total_ao * 100) }}%</small></div>{% endfor %}</div></div></section>{% endif %}
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 — AI 品質診斷台</small></p>
{% if rag_root_causes %}<section class="quality-panel mt-3"><div class="quality-panel-head"><div><div class="quality-label">根因分析</div><h2 class="quality-panel-title">RAG 自動根因建議</h2></div></div><div class="quality-panel-body">{% for rc in rag_root_causes %}<div class="root-card"><strong><code>{{ rc.caller }}</code></strong><span class="badge bg-danger ms-1">{{ "%.2f"|format(rc.avg_score) }}/5</span><span class="badge bg-secondary ms-1">{{ rc.feedback_n }} 筆</span><ul class="list-unstyled mt-2 mb-0 small">{% for h in rc.hits %}<li class="mb-1"><span class="badge bg-info me-1">{{ h.insight_type }}</span><span class="badge bg-light text-dark me-1">相似度 {{ "%.2f"|format(h.similarity) }}</span>{{ h.content }}{% if h.content|length >= 200 %}…{% endif %}</li>{% endfor %}</ul></div>{% endfor %}</div></section>{% endif %}
{% if recommendations %}<section class="quality-panel mt-3"><div class="quality-panel-head"><div><div class="quality-label">智能建議</div><h2 class="quality-panel-title">智能建議</h2></div></div><div class="quality-panel-body"><ul class="mb-0">{% for rec in recommendations %}<li>{% if rec.action == 'review' %}<i class="fas fa-triangle-exclamation status-warn me-1"></i>{% else %}<i class="fas fa-check status-good me-1"></i>{% endif %}<code>{{ rec.caller }}</code>{{ rec.reason }}</li>{% endfor %}</ul></div></section>{% endif %}
{% if action_outcomes_stats %}<section class="quality-panel mt-3"><div class="quality-panel-head"><div><div class="quality-label">動作成效</div><h2 class="quality-panel-title">實際動作成效</h2></div></div><div class="quality-panel-body"><div class="quality-mini-grid">{% set total_ao = (action_outcomes_stats | sum(attribute='count')) or 1 %}{% for r in action_outcomes_stats %}<div class="quality-mini"><span class="quality-label">{{ r.verdict }}</span><strong class="{% if r.verdict == 'effective' %}status-good{% elif r.verdict == 'backfired' %}status-bad{% endif %}">{{ r.count }}</strong><small class="text-muted">{{ "%.1f"|format(r.count / total_ao * 100) }}%</small></div>{% endfor %}</div></div></section>{% endif %}
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Ollama 優先策略 v5.0 — AI 品質診斷台</small></p>
</div>
{% if rag_overall_dist %}<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script><script>(function(){const data={{ rag_overall_dist | tojson }};const el=document.getElementById('ragFeedbackPieChart');if(!el||!data.length)return;const colorMap={1:'#b94b45',2:'#c96442',3:'#b8792f',4:'#7aaa82',5:'#4f8a5b'};new Chart(el,{type:'doughnut',data:{labels:data.map(r=>`${r.score}`),datasets:[{data:data.map(r=>r.count),backgroundColor:data.map(r=>colorMap[r.score]||'#8b8077'),borderWidth:1,borderColor:'#fff'}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{font:{size:12}}}}}});})();</script>{% endif %}

View File

@@ -18,29 +18,29 @@
<div class="container-fluid mt-3">
<section class="qa-hero">
<div class="qa-kicker"><i class="fas fa-magnifying-glass-chart me-1"></i> RAG Recall Radar · {{ hours }}h Window</div>
<div class="qa-kicker"><i class="fas fa-magnifying-glass-chart me-1"></i> RAG 召回雷達 · {{ hours }} 小時視窗</div>
<h1 class="qa-title">RAG 召回雷達</h1>
<p class="qa-subtitle">這裡追蹤每次 RAG 查詢是否真的命中、是否省下 LLM call、哪些 caller 用得好、哪些 query 沒找到知識。RAG 如果要成為武器,這裡就是雷達螢幕。</p>
<form method="get" class="qa-filter"><select name="hours" class="form-select form-select-sm" onchange="this.form.submit()">{% 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><select name="caller" class="form-select form-select-sm" onchange="this.form.submit()"><option value="">全部呼叫端</option>{% for c in callers %}<option value="{{ c }}" {% if caller_filter == c %}selected{% endif %}>{{ c }}</option>{% endfor %}</select><label class="form-check-label small d-flex align-items-center gap-2"><input class="form-check-input" type="checkbox" name="saved_only" value="1" {% if saved_only %}checked{% endif %} onchange="this.form.submit()">僅看 saved_call=true</label></form>
{% if summary and summary.total > 0 %}<div class="qa-command"><div class="qa-signal"><div class="qa-label">Queries</div><span class="qa-value">{{ "{:,}".format(summary.total) }}</span><div class="qa-note">{{ summary.distinct_callers }} callers</div></div><div class="qa-signal"><div class="qa-label">Hit Rate</div><span class="qa-value {% if summary.hit_rate >= 70 %}status-good{% elif summary.hit_rate >= 40 %}status-warn{% else %}status-bad{% endif %}">{{ "%.1f"|format(summary.hit_rate) }}%</span><div class="qa-note">{{ summary.with_hits }} hit · {{ summary.no_hits }} miss</div></div><div class="qa-signal"><div class="qa-label">Saved Call</div><span class="qa-value status-blue">{{ "%.1f"|format(summary.saved_rate) }}%</span><div class="qa-note">{{ summary.saved }} 次省下 LLM</div></div><div class="qa-signal"><div class="qa-label">Feedback</div><span class="qa-value {% if summary.avg_score >= 4 %}status-good{% elif summary.avg_score >= 3 %}status-warn{% else %}status-bad{% endif %}">{{ "%.2f"|format(summary.avg_score) }}</span><div class="qa-note">{{ summary.feedback_count }} 筆 · 平均 {{ summary.avg_hits }} hits</div></div></div>{% endif %}
<p class="qa-subtitle">這裡追蹤每次 RAG 查詢是否真的命中、是否省下 LLM 呼叫、哪些呼叫端用得好、哪些查詢沒找到知識。RAG 如果要成為武器,這裡就是雷達螢幕。</p>
<form method="get" class="qa-filter"><select name="hours" class="form-select form-select-sm" onchange="this.form.submit()">{% 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><select name="caller" class="form-select form-select-sm" onchange="this.form.submit()"><option value="">全部呼叫端</option>{% for c in callers %}<option value="{{ c }}" {% if caller_filter == c %}selected{% endif %}>{{ c }}</option>{% endfor %}</select><label class="form-check-label small d-flex align-items-center gap-2"><input class="form-check-input" type="checkbox" name="saved_only" value="1" {% if saved_only %}checked{% endif %} onchange="this.form.submit()">僅看已省下 LLM 呼叫</label></form>
{% if summary and summary.total > 0 %}<div class="qa-command"><div class="qa-signal"><div class="qa-label">查詢數</div><span class="qa-value">{{ "{:,}".format(summary.total) }}</span><div class="qa-note">{{ summary.distinct_callers }} 個呼叫端</div></div><div class="qa-signal"><div class="qa-label">命中率</div><span class="qa-value {% if summary.hit_rate >= 70 %}status-good{% elif summary.hit_rate >= 40 %}status-warn{% else %}status-bad{% endif %}">{{ "%.1f"|format(summary.hit_rate) }}%</span><div class="qa-note">{{ summary.with_hits }} 次命中 · {{ summary.no_hits }} 次未命中</div></div><div class="qa-signal"><div class="qa-label">省下呼叫</div><span class="qa-value status-blue">{{ "%.1f"|format(summary.saved_rate) }}%</span><div class="qa-note">{{ summary.saved }} 次省下 LLM</div></div><div class="qa-signal"><div class="qa-label">反饋分</div><span class="qa-value {% if summary.avg_score >= 4 %}status-good{% elif summary.avg_score >= 3 %}status-warn{% else %}status-bad{% endif %}">{{ "%.2f"|format(summary.avg_score) }}</span><div class="qa-note">{{ summary.feedback_count }} 筆 · 平均 {{ summary.avg_hits }} 次命中</div></div></div>{% endif %}
</section>
{% if error %}<div class="alert alert-warning mt-3"><strong><i class="fas fa-triangle-exclamation me-1"></i></strong>{{ error }}</div>{% endif %}
<section class="qa-grid">
<div class="qa-stack">
<article class="qa-table-shell"><div class="qa-table-title"><div><div class="qa-label">Query Stream</div><h3>最近 50 筆查詢詳情</h3></div></div><div class="table-responsive">{% if queries %}<table class="table table-sm mb-0"><thead class="table-light"><tr><th>時間</th><th>呼叫端</th><th>查詢</th><th class="text-end">top_k</th><th class="text-end">門檻</th><th class="text-end">命中</th><th>saved</th><th>反饋</th><th>動作</th></tr></thead><tbody>{% for q in queries %}<tr><td><small>{{ q.queried_at }}</small></td><td><code>{{ q.caller }}</code></td><td><small>{{ q.query_text }}{% if q.query_text|length >= 200 %}…{% endif %}</small></td><td class="text-end">{{ q.top_k }}</td><td class="text-end">{{ q.threshold }}</td><td class="text-end">{% if q.hit_count > 0 %}<strong class="status-good">{{ q.hit_count }}</strong>{% else %}<small class="text-muted">0</small>{% endif %}</td><td>{% if q.saved_call %}<span class="badge bg-success">saved</span>{% else %}<small class="text-muted"></small>{% endif %}</td><td>{% if q.feedback_score is not none %}<span class="badge {% if q.feedback_score >= 4 %}bg-success{% elif q.feedback_score >= 3 %}bg-warning{% else %}bg-danger{% endif %}">{{ q.feedback_score }}/5</span>{% else %}<small class="text-muted"></small>{% endif %}</td><td>{% if q.hit_count > 0 %}<button class="btn btn-sm btn-outline-info" onclick="showHits({{ q.id }})"><i class="fas fa-eye me-1"></i> hits</button>{% endif %}</td></tr>{% endfor %}</tbody></table>{% else %}<div class="alert alert-info m-3">過去 {{ hours }} 小時無符合條件的 RAG 查詢紀錄。</div>{% endif %}</div></article>
<article class="qa-table-shell"><div class="qa-table-title"><div><div class="qa-label">查詢串流</div><h3>最近 50 筆查詢詳情</h3></div></div><div class="table-responsive">{% if queries %}<table class="table table-sm mb-0"><thead class="table-light"><tr><th>時間</th><th>呼叫端</th><th>查詢</th><th class="text-end">top_k</th><th class="text-end">門檻</th><th class="text-end">命中</th><th>已省下</th><th>反饋</th><th>動作</th></tr></thead><tbody>{% for q in queries %}<tr><td><small>{{ q.queried_at }}</small></td><td><code>{{ q.caller }}</code></td><td><small>{{ q.query_text }}{% if q.query_text|length >= 200 %}…{% endif %}</small></td><td class="text-end">{{ q.top_k }}</td><td class="text-end">{{ q.threshold }}</td><td class="text-end">{% if q.hit_count > 0 %}<strong class="status-good">{{ q.hit_count }}</strong>{% else %}<small class="text-muted">0</small>{% endif %}</td><td>{% if q.saved_call %}<span class="badge bg-success">已省下</span>{% else %}<small class="text-muted"></small>{% endif %}</td><td>{% if q.feedback_score is not none %}<span class="badge {% if q.feedback_score >= 4 %}bg-success{% elif q.feedback_score >= 3 %}bg-warning{% else %}bg-danger{% endif %}">{{ q.feedback_score }}/5</span>{% else %}<small class="text-muted"></small>{% endif %}</td><td>{% if q.hit_count > 0 %}<button class="btn btn-sm btn-outline-info" onclick="showHits({{ q.id }})"><i class="fas fa-eye me-1"></i>命中</button>{% endif %}</td></tr>{% endfor %}</tbody></table>{% else %}<div class="alert alert-info m-3">過去 {{ hours }} 小時無符合條件的 RAG 查詢紀錄。</div>{% endif %}</div></article>
</div>
<aside class="qa-stack">
{% if by_caller %}<article class="qa-panel"><div class="qa-panel-head"><div><div class="qa-label">Caller Quality</div><h2 class="qa-panel-title">各呼叫端 RAG 表現</h2></div></div><div class="qa-panel-body">{% for c in by_caller %}<div class="caller-card"><div class="caller-top"><code>{{ c.caller }}</code><strong class="{% if c.hit_rate >= 70 %}status-good{% elif c.hit_rate >= 40 %}status-warn{% else %}text-muted{% endif %}">{{ "%.1f"|format(c.hit_rate) }}%</strong></div><div class="caller-meter"><span style="width: {{ c.hit_rate|round|int }}%"></span></div><small class="text-muted">{{ c.total }} queries · saved {{ "%.1f"|format(c.saved_rate) }}% · feedback {{ c.fb_count }}</small></div>{% endfor %}</div></article>{% endif %}
{% if by_caller %}<article class="qa-panel"><div class="qa-panel-head"><div><div class="qa-label">呼叫端品質</div><h2 class="qa-panel-title">各呼叫端 RAG 表現</h2></div></div><div class="qa-panel-body">{% for c in by_caller %}<div class="caller-card"><div class="caller-top"><code>{{ c.caller }}</code><strong class="{% if c.hit_rate >= 70 %}status-good{% elif c.hit_rate >= 40 %}status-warn{% else %}text-muted{% endif %}">{{ "%.1f"|format(c.hit_rate) }}%</strong></div><div class="caller-meter"><span style="width: {{ c.hit_rate|round|int }}%"></span></div><small class="text-muted">{{ c.total }} 次查詢 · 省下 {{ "%.1f"|format(c.saved_rate) }}% · 反饋 {{ c.fb_count }}</small></div>{% endfor %}</div></article>{% endif %}
</aside>
</section>
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 — RAG 召回雷達</small></p>
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Ollama 優先策略 v5.0 — RAG 召回雷達</small></p>
</div>
<div class="modal fade" id="hitsModal" tabindex="-1"><div class="modal-dialog modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title"><i class="fas fa-eye me-2"></i>RAG 命中內容</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body" id="hitsModalBody"><div class="text-center"><i class="fas fa-spinner fa-spin"></i> 載入中...</div></div></div></div></div>
<script>
async function showHits(queryId){const modalEl=document.getElementById('hitsModal');const body=document.getElementById('hitsModalBody');body.innerHTML='<div class="text-center"><i class="fas fa-spinner fa-spin"></i> 載入中...</div>';const modal=new bootstrap.Modal(modalEl);modal.show();try{const r=await fetch(`/observability/rag_queries/${queryId}/hits`);const d=await r.json();if(!d.ok){body.innerHTML=`<div class="alert alert-danger">❌ ${d.error||'載入失敗'}</div>`;return;}let html=`<div class="mb-3"><small class="text-muted">查詢 #${d.query_id} · 門檻 ${d.threshold} · 命中 ${d.hit_count}</small><div class="p-2 mt-1 obs-modal-preview"><small><strong>查詢內容:</strong></small><br><code>${escapeHtml(d.query_text||'')}</code></div></div>`;if(d.hits.length===0){html+='<div class="alert alert-warning">無 hits 詳細資料</div>';}else{html+='<h6 class="mb-2">Top hits 內容預覽:</h6>';d.hits.forEach(h=>{html+=`<div class="mb-2 p-2 obs-modal-preview"><div class="mb-1"><span class="badge bg-light text-dark me-1">#${h.id}</span><span class="badge bg-info me-1">${escapeHtml(h.insight_type||'')}</span>${h.period?`<span class="badge bg-secondary me-1">${escapeHtml(h.period)}</span>`:''}${h.product_sku?`<small class="text-muted me-1">SKU: ${escapeHtml(h.product_sku)}</small>`:''}<small class="text-muted">${h.created_at}</small></div><small>${escapeHtml(h.content||'')}${h.content&&h.content.length>=300?'…':''}</small></div>`;});}body.innerHTML=html;}catch(e){console.warn('rag_query_hits_load_failed',e);body.innerHTML='<div class="alert alert-danger">❌ 召回詳情暫時無法載入,請稍後再試或查看系統日誌。</div>';}}
async function showHits(queryId){const modalEl=document.getElementById('hitsModal');const body=document.getElementById('hitsModalBody');body.innerHTML='<div class="text-center"><i class="fas fa-spinner fa-spin"></i> 載入中...</div>';const modal=new bootstrap.Modal(modalEl);modal.show();try{const r=await fetch(`/observability/rag_queries/${queryId}/hits`);const d=await r.json();if(!d.ok){body.innerHTML=`<div class="alert alert-danger">❌ ${d.error||'載入失敗'}</div>`;return;}let html=`<div class="mb-3"><small class="text-muted">查詢 #${d.query_id} · 門檻 ${d.threshold} · 命中 ${d.hit_count}</small><div class="p-2 mt-1 obs-modal-preview"><small><strong>查詢內容:</strong></small><br><code>${escapeHtml(d.query_text||'')}</code></div></div>`;if(d.hits.length===0){html+='<div class="alert alert-warning">無命中詳細資料</div>';}else{html+='<h6 class="mb-2">Top 命中內容預覽:</h6>';d.hits.forEach(h=>{html+=`<div class="mb-2 p-2 obs-modal-preview"><div class="mb-1"><span class="badge bg-light text-dark me-1">#${h.id}</span><span class="badge bg-info me-1">${escapeHtml(h.insight_type||'')}</span>${h.period?`<span class="badge bg-secondary me-1">${escapeHtml(h.period)}</span>`:''}${h.product_sku?`<small class="text-muted me-1">SKU: ${escapeHtml(h.product_sku)}</small>`:''}<small class="text-muted">${h.created_at}</small></div><small>${escapeHtml(h.content||'')}${h.content&&h.content.length>=300?'…':''}</small></div>`;});}body.innerHTML=html;}catch(e){console.warn('rag_query_hits_load_failed',e);body.innerHTML='<div class="alert alert-danger">❌ 召回詳情暫時無法載入,請稍後再試或查看系統日誌。</div>';}}
function escapeHtml(s){if(!s)return'';return s.replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
</script>
{% endblock %}

View File

@@ -2379,6 +2379,136 @@
}
}
/* v3.11 V2 workbench normalization: unify legacy observability page skins before the terminal dot layer. */
.momo-observability-mode :is(
.obs-hero,
.agent-hero,
.biz-command,
.runtime-hero,
.calls-hero,
.gov-hero,
.gate-hero,
.rag-hero,
.qa-hero,
.quality-hero,
.ppt-hero
) {
border-color: var(--obs-line) !important;
border-radius: var(--momo-radius-lg, 8px) !important;
background-color: var(--momo-bg-surface, #faf7f0) !important;
background-image: var(--obs-matrix-dot) !important;
background-size: var(--obs-matrix-size) !important;
box-shadow: var(--momo-shadow-md, 0 2px 8px rgba(42, 37, 32, 0.06)) !important;
}
.momo-observability-mode :is(
.obs-panel,
.agent-panel,
.biz-panel,
.runtime-panel,
.calls-panel,
.gov-panel,
.gate-panel,
.rag-panel,
.qa-panel,
.quality-panel,
.ppt-panel,
.obs-signal,
.agent-signal,
.biz-signal,
.runtime-signal,
.calls-signal,
.gov-signal,
.gate-signal,
.rag-signal,
.qa-signal,
.quality-signal,
.ppt-signal,
.agent-card,
.rec-card,
.host-lane,
.runtime-mini,
.calls-mini,
.gov-mini,
.gate-mini,
.quality-mini,
.ppt-mini,
.strategy-card,
.episode-card,
.similar-box,
.fix-card,
.root-card,
.caller-card,
.biz-filter-card,
.biz-alert-strip,
.biz-chart-shell,
.biz-strategy-card,
.biz-mini-metric,
.biz-decision-card,
.obs-route-card
) {
border-color: var(--obs-line) !important;
border-radius: var(--momo-radius-lg, 8px) !important;
background-color: var(--momo-bg-elevated, #fdfaf3) !important;
background-image: var(--obs-matrix-dot-soft) !important;
background-size: var(--obs-matrix-size) !important;
box-shadow: var(--momo-shadow-md, 0 2px 8px rgba(42, 37, 32, 0.06)) !important;
}
.momo-observability-mode :is(
.obs-kicker,
.agent-kicker,
.biz-kicker,
.runtime-kicker,
.calls-kicker,
.gov-kicker,
.gate-kicker,
.rag-kicker,
.qa-kicker,
.quality-kicker,
.ppt-kicker,
.obs-signal-label,
.agent-label,
.biz-signal .label,
.runtime-label,
.calls-label,
.gov-label,
.gate-label,
.rag-label,
.qa-label,
.quality-label,
.ppt-label,
.obs-section-eyebrow
) {
font-family: var(--momo-font-family-mono, "JetBrains Mono", ui-monospace, monospace) !important;
font-size: var(--momo-text-label, 0.6875rem) !important;
letter-spacing: 0.06em !important;
text-transform: none !important;
color: color-mix(in srgb, var(--obs-accent) 76%, var(--obs-muted)) !important;
}
.momo-observability-mode :is(
.btn,
.badge,
.obs-pill,
.biz-badge,
[class$="-pill"],
.model-chip
) {
border-radius: var(--momo-radius-lg, 8px) !important;
}
.momo-observability-mode :is(
.agent-meter,
.caller-meter,
.progress,
.progress-bar,
.obs-progress-xs,
.obs-progress-sm
) {
border-radius: var(--momo-radius-sm, 3px) !important;
}
/* v3.10 terminal dot-matrix layer: this must stay at EOF to win the cascade. */
.momo-observability-mode {
--obs-matrix-dot: radial-gradient(color-mix(in srgb, var(--obs-accent) 14%, transparent) 0.85px, transparent 0.95px);