fix: hide caller keys in observability UI
Some checks failed
CD Pipeline / deploy (push) Failing after 1m11s

This commit is contained in:
ogt
2026-06-26 19:10:12 +08:00
parent fbce41cf02
commit f823439496
6 changed files with 30 additions and 11 deletions

View File

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

View File

@@ -2,7 +2,7 @@
> **最後更新**: 2026-06-26 (台北時間)
> **狀態**: 🟢 四 AI Agent 自動化閉環已落地LLM 路由紅線升級為 Ollama-first 三主機級聯PChome 後台業績匯入韌性已補強產品定位正名為「PChome 業績成長自動化作戰系統」外部市場來源正規化層、自動同步、作戰清單與價格參考表優先讀取、CSV 備援預檢、前台操作入口、高可見頁面繁中化守門、比價/作戰 UI 工作台化、跨平台來源治理與商品身份 UI 契約已建立GCP embedding 熔斷延後處理、110 proxy rescue 與 direct host health skip 已建立
> **適用版本**: V10.717
> **適用版本**: V10.718
---
@@ -802,3 +802,4 @@ POSTGRES_HOST=momo-db
| 2026-06-26 | AI 銷售建議前端不得保留 raw content 路徑 | V10.715 起 `/ai_recommend` 只使用已整理的外部訊號摘要與可用線索;搜尋或商品洞察結果若未整理成結構,前端只顯示可理解的待整理狀態,不再把 raw content 當上下文或 fallback 分支保留。 |
| 2026-06-26 | 首頁覆核候選必須標示為待確認商品 | V10.716 起首頁 PChome 覆核區不再使用「PChome 候選」作為可見主語統一改為「PChome 待確認商品」與「開 PChome 待確認商品」,避免使用者把未確認同款誤認為正式比價結果。 |
| 2026-06-26 | 供貨風險頁不得使用資料表或英文模組名作為主語 | V10.717 起缺貨清單與補貨通知頁統一使用「供貨風險、缺貨處理清單、補貨通知紀錄」等營運語言不再顯示「缺貨資料表、缺貨資料、Vendor Stockout」等資料庫或英文模組感文案。 |
| 2026-06-26 | AI 觀測頁不得外露 caller key | V10.718 起 AI 品質診斷與知識召回頁使用「使用情境」作為可見主語,並透過 `obs_label.caller()` 顯示營運名稱;前台不得直接顯示 `<code>{{ caller }}</code>``top_k` 或「全部呼叫端」等工程語言。 |

View File

@@ -588,7 +588,7 @@ def rag_queries_dashboard():
'queried_at': r[1].strftime('%Y-%m-%d %H:%M:%S') if r[1] else '',
'caller': r[2],
'query_text': r[3] or '',
'top_k': int(r[4] or 0),
'take_count': int(r[4] or 0),
'threshold': round(float(r[5] or 0), 3),
'hit_count': int(r[6] or 0),
'used_results': used_ids,

View File

@@ -18,12 +18,12 @@
{% 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> 品質診斷 · {{ days }} 日視窗</div><h1 class="quality-title">AI 品質診斷台</h1><p class="quality-subtitle">用反饋、知識分數與行動閉環檢查 AI 建議是否可靠。</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">知識評分</div><span class="quality-value">{{ rag_total }}</span><small class="text-muted">已回饋知識查詢</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 建議是否可靠。</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">知識評分</div><span class="quality-value">{{ rag_total }}</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="quality-grid">
<div class="quality-stack">
<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>
<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><span>{{ obs_label.caller(caller) }}</span></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 %}">{{ obs_label.status(a.status) }}</span></td><td><code>{{ obs_label.plan_type(a.plan_type) }}</code></td><td class="text-end">{{ a.count }}</td></tr>{% endfor %}</tbody></table></div></article>{% endif %}
</div>
<aside class="quality-stack">
@@ -32,8 +32,8 @@
</aside>
</section>
{% 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">知識根因建議</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">{{ obs_label.insight(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 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">知識根因建議</h2></div></div><div class="quality-panel-body">{% for rc in rag_root_causes %}<div class="root-card"><strong>{{ obs_label.caller(rc.caller) }}</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">{{ obs_label.insight(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 %}<strong>{{ obs_label.caller(rec.caller) }}</strong>{{ 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">{{ obs_label.verdict(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>AI 品質診斷台</small></p>
</div>

View File

@@ -15,23 +15,24 @@
</style>
{% set total = summary.total if summary else 0 %}
{% import "admin/_observability_labels.html" as obs_label %}
<div class="container-fluid mt-3">
<section class="qa-hero">
<div class="qa-kicker"><i class="fas fa-magnifying-glass-chart me-1"></i> 知識召回雷達 · {{ hours }} 小時視窗</div>
<h1 class="qa-title">知識召回雷達</h1>
<p class="qa-subtitle">追蹤知識是否命中、是否省下模型呼叫,避免業績建議缺少根據。</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 %}
<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 %}>{{ obs_label.caller(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()">僅看已省下模型呼叫</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 }} 次省下模型呼叫</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">查詢串流</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 }} 小時無符合條件的知識查詢紀錄。</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">取用數</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><span>{{ obs_label.caller(q.caller) }}</span></td><td><small>{{ q.query_text }}{% if q.query_text|length >= 200 %}…{% endif %}</small></td><td class="text-end">{{ q.take_count }}</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 }} 小時無符合條件的知識查詢紀錄。</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">呼叫端品質</div><h2 class="qa-panel-title">呼叫端知識表現</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 %}
{% if by_caller %}<article class="qa-panel"><div class="qa-panel-head"><div><div class="qa-label">使用情境品質</div><h2 class="qa-panel-title">使用情境知識表現</h2></div></div><div class="qa-panel-body">{% for c in by_caller %}<div class="caller-card"><div class="caller-top"><span>{{ obs_label.caller(c.caller) }}</span><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>

View File

@@ -709,6 +709,8 @@ def test_utility_pages_keep_operator_copy_professional():
stockout_list = (ROOT / "templates/vendor_stockout_list_v2.html").read_text(encoding="utf-8")
stockout_send_email = (ROOT / "templates/vendor_stockout_send_email_v2.html").read_text(encoding="utf-8")
stockout_vendor_management = (ROOT / "templates/vendor_stockout_vendor_management_v2.html").read_text(encoding="utf-8")
quality_trend = (ROOT / "templates/admin/quality_trend.html").read_text(encoding="utf-8")
rag_queries = (ROOT / "templates/admin/rag_queries.html").read_text(encoding="utf-8")
ai_calls = (ROOT / "templates/admin/ai_calls_dashboard.html").read_text(encoding="utf-8")
observability_labels = (ROOT / "templates/admin/_observability_labels.html").read_text(encoding="utf-8")
host_health = (ROOT / "templates/admin/host_health.html").read_text(encoding="utf-8")
@@ -730,6 +732,8 @@ def test_utility_pages_keep_operator_copy_professional():
stockout_list,
stockout_send_email,
stockout_vendor_management,
quality_trend,
rag_queries,
ai_calls,
observability_labels,
host_health,
@@ -761,6 +765,17 @@ def test_utility_pages_keep_operator_copy_professional():
assert "供應商窗口" in stockout_vendor_management
assert "維護正確窗口" in stockout_vendor_management
assert "Vendor Stockout" not in stockout_vendor_management
assert "使用情境反饋分佈" in quality_trend
assert "最低使用情境平均分" in quality_trend
assert "<code>{{ caller }}</code>" not in quality_trend
assert "<code>{{ rc.caller }}</code>" not in quality_trend
assert "<code>{{ rec.caller }}</code>" not in quality_trend
assert "全部使用情境" in rag_queries
assert "各使用情境知識表現" in rag_queries
assert "僅看已省下模型呼叫" in rag_queries
assert "top_k" not in rag_queries
assert "<code>{{ q.caller }}</code>" not in rag_queries
assert "<code>{{ c.caller }}</code>" not in rag_queries
assert "使用情境" in ai_calls
assert "全部使用情境" in ai_calls
assert "情境 × 知識命中矩陣" in ai_calls
@@ -807,6 +822,8 @@ def test_utility_pages_keep_operator_copy_professional():
"全部呼叫端",
"呼叫端 ×",
"<code>{{ c.caller }}</code>",
"<code>{{ q.caller }}</code>",
"<code>{{ caller }}</code>",
"NIM Elephant",
"OpenRouter",
"GitLab 部署紀錄",