This commit is contained in:
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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", "審核"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}}}}}});})();
|
||||
|
||||
@@ -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 4:weight ≥ 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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user