This commit is contained in:
@@ -297,6 +297,23 @@ def rag_queries_dashboard():
|
||||
|
||||
session = get_session()
|
||||
try:
|
||||
rag_query_log_exists = bool(session.execute(
|
||||
sa_text("SELECT to_regclass('public.rag_query_log') IS NOT NULL")
|
||||
).scalar())
|
||||
if not rag_query_log_exists:
|
||||
return render_template(
|
||||
'admin/rag_queries.html',
|
||||
active_page='obs_rag_queries',
|
||||
hours=hours,
|
||||
caller_filter=caller_filter,
|
||||
saved_only=saved_only,
|
||||
summary={},
|
||||
callers=[],
|
||||
by_caller=[],
|
||||
queries=[],
|
||||
error='rag_query_log 尚未建立,RAG 召回資料待接入。',
|
||||
)
|
||||
|
||||
# 整體統計
|
||||
summary_row = session.execute(
|
||||
sa_text("""
|
||||
@@ -417,7 +434,7 @@ def rag_queries_dashboard():
|
||||
active_page='obs_rag_queries', hours=hours,
|
||||
caller_filter=caller_filter, saved_only=saved_only,
|
||||
summary={}, callers=[], by_caller=[], queries=[],
|
||||
error=f'查詢失敗: {type(e).__name__}: {str(e)[:200]}',
|
||||
error='RAG 召回資料暫時不可用,已切換安全空狀態。',
|
||||
)
|
||||
finally:
|
||||
session.close()
|
||||
@@ -1067,31 +1084,53 @@ def ai_calls_dashboard():
|
||||
).fetchall()
|
||||
|
||||
# 5. Phase 39 D-3: caller × RAG 命中率 × MCP 編排率(跨表 JOIN)
|
||||
# 展現「AI 自動化專業」核心:每個 caller 多大比例走了 RAG / MCP
|
||||
caller_richness = session.execute(
|
||||
sa_text("""
|
||||
SELECT a.caller,
|
||||
COUNT(*) AS total_calls,
|
||||
COUNT(*) FILTER (WHERE a.rag_hit) AS rag_hits,
|
||||
COUNT(DISTINCT m.request_id) AS mcp_orchestrated,
|
||||
COALESCE(AVG(rl.feedback_score) FILTER (WHERE rl.feedback_score IS NOT NULL), 0)
|
||||
AS avg_rag_feedback,
|
||||
COUNT(rl.feedback_score) AS feedback_count
|
||||
FROM ai_calls a
|
||||
LEFT JOIN mcp_calls m
|
||||
ON m.request_id = a.request_id
|
||||
AND m.called_at >= :since
|
||||
LEFT JOIN rag_query_log rl
|
||||
ON rl.caller = a.caller
|
||||
AND rl.queried_at >= :since
|
||||
WHERE a.called_at >= :since
|
||||
GROUP BY a.caller
|
||||
HAVING COUNT(*) >= 5
|
||||
ORDER BY total_calls DESC
|
||||
LIMIT 12
|
||||
"""),
|
||||
{'since': since},
|
||||
).fetchall()
|
||||
# mcp_calls / rag_query_log 尚未 migration 時安全降級,不曝露 DB exception。
|
||||
mcp_calls_table_exists = bool(session.execute(
|
||||
sa_text("SELECT to_regclass('public.mcp_calls') IS NOT NULL")
|
||||
).scalar())
|
||||
rag_query_log_exists = bool(session.execute(
|
||||
sa_text("SELECT to_regclass('public.rag_query_log') IS NOT NULL")
|
||||
).scalar())
|
||||
if mcp_calls_table_exists and rag_query_log_exists:
|
||||
caller_richness = session.execute(
|
||||
sa_text("""
|
||||
SELECT a.caller,
|
||||
COUNT(*) AS total_calls,
|
||||
COUNT(*) FILTER (WHERE a.rag_hit) AS rag_hits,
|
||||
COUNT(DISTINCT m.request_id) AS mcp_orchestrated,
|
||||
COALESCE(AVG(rl.feedback_score) FILTER (WHERE rl.feedback_score IS NOT NULL), 0)
|
||||
AS avg_rag_feedback,
|
||||
COUNT(rl.feedback_score) AS feedback_count
|
||||
FROM ai_calls a
|
||||
LEFT JOIN mcp_calls m
|
||||
ON m.request_id = a.request_id
|
||||
AND m.called_at >= :since
|
||||
LEFT JOIN rag_query_log rl
|
||||
ON rl.caller = a.caller
|
||||
AND rl.queried_at >= :since
|
||||
WHERE a.called_at >= :since
|
||||
GROUP BY a.caller
|
||||
HAVING COUNT(*) >= 5
|
||||
ORDER BY total_calls DESC
|
||||
LIMIT 12
|
||||
"""),
|
||||
{'since': since},
|
||||
).fetchall()
|
||||
else:
|
||||
caller_richness = session.execute(
|
||||
sa_text("""
|
||||
SELECT caller,
|
||||
COUNT(*) AS total_calls,
|
||||
COUNT(*) FILTER (WHERE rag_hit) AS rag_hits
|
||||
FROM ai_calls
|
||||
WHERE called_at >= :since
|
||||
GROUP BY caller
|
||||
HAVING COUNT(*) >= 5
|
||||
ORDER BY total_calls DESC
|
||||
LIMIT 12
|
||||
"""),
|
||||
{'since': since},
|
||||
).fetchall()
|
||||
|
||||
return render_template(
|
||||
'admin/ai_calls_dashboard.html',
|
||||
@@ -1156,11 +1195,11 @@ def ai_calls_dashboard():
|
||||
'caller': r[0],
|
||||
'total_calls': int(r[1] or 0),
|
||||
'rag_hits': int(r[2] or 0),
|
||||
'mcp_orchestrated': int(r[3] or 0),
|
||||
'avg_rag_feedback': round(float(r[4] or 0), 2),
|
||||
'feedback_count': int(r[5] or 0),
|
||||
'mcp_orchestrated': int(r[3] or 0) if mcp_calls_table_exists and rag_query_log_exists else 0,
|
||||
'avg_rag_feedback': round(float(r[4] or 0), 2) if mcp_calls_table_exists and rag_query_log_exists else 0,
|
||||
'feedback_count': int(r[5] or 0) if mcp_calls_table_exists and rag_query_log_exists else 0,
|
||||
'rag_hit_rate': (float(r[2] or 0) / float(r[1]) * 100) if r[1] else 0,
|
||||
'mcp_rate': (float(r[3] or 0) / float(r[1]) * 100) if r[1] else 0,
|
||||
'mcp_rate': (float(r[3] or 0) / float(r[1]) * 100) if mcp_calls_table_exists and rag_query_log_exists and r[1] else 0,
|
||||
}
|
||||
for r in caller_richness
|
||||
],
|
||||
@@ -1174,7 +1213,7 @@ def ai_calls_dashboard():
|
||||
provider_filter=provider_filter,
|
||||
summary={}, by_provider=[], recent=[], callers=[], caller_richness=[],
|
||||
by_model=[], hourly_trend=[], recent_contexts=[],
|
||||
error=f'查詢失敗: {type(e).__name__}: {str(e)[:200]}',
|
||||
error='AI 呼叫資料暫時不可用,已切換安全空狀態。',
|
||||
)
|
||||
finally:
|
||||
session.close()
|
||||
@@ -1530,13 +1569,19 @@ def budget_dashboard():
|
||||
|
||||
session = get_session()
|
||||
try:
|
||||
budgets = session.execute(
|
||||
sa_text("""
|
||||
SELECT id, period, provider, budget_usd, alert_pct, updated_at
|
||||
FROM ai_call_budgets
|
||||
ORDER BY period, provider NULLS FIRST
|
||||
"""),
|
||||
).fetchall()
|
||||
ai_call_budgets_exists = bool(session.execute(
|
||||
sa_text("SELECT to_regclass('public.ai_call_budgets') IS NOT NULL")
|
||||
).scalar())
|
||||
if ai_call_budgets_exists:
|
||||
budgets = session.execute(
|
||||
sa_text("""
|
||||
SELECT id, period, provider, budget_usd, alert_pct, updated_at
|
||||
FROM ai_call_budgets
|
||||
ORDER BY period, provider NULLS FIRST
|
||||
"""),
|
||||
).fetchall()
|
||||
else:
|
||||
budgets = []
|
||||
|
||||
spent_rows = session.execute(
|
||||
sa_text("""
|
||||
@@ -1687,7 +1732,7 @@ def budget_dashboard():
|
||||
budget_strategies=[], cost_trend_30d=[],
|
||||
top_cost_callers=[], price_rec_7d=[],
|
||||
provider_cost_month=[],
|
||||
error=f'查詢失敗: {type(e).__name__}: {str(e)[:200]}')
|
||||
error='預算資料暫時不可用,已切換安全空狀態。')
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@@ -140,10 +140,10 @@
|
||||
.momo-observability-mode .ppt-title {
|
||||
max-width: 780px;
|
||||
font-family: 'Noto Sans TC', 'Inter', sans-serif !important;
|
||||
font-size: clamp(1.9rem, 3.2vw, 2.75rem) !important;
|
||||
line-height: 1.12 !important;
|
||||
letter-spacing: -0.045em !important;
|
||||
font-weight: 860 !important;
|
||||
font-size: clamp(2.1rem, 4vw, 3.35rem) !important;
|
||||
line-height: 1.05 !important;
|
||||
letter-spacing: -0.055em !important;
|
||||
font-weight: 900 !important;
|
||||
}
|
||||
|
||||
.momo-observability-mode .obs-hero,
|
||||
@@ -161,8 +161,6 @@
|
||||
border: 1px solid rgba(201, 100, 66, 0.16) !important;
|
||||
border-radius: 28px !important;
|
||||
box-shadow: var(--obs-shadow) !important;
|
||||
padding-top: clamp(1.05rem, 2vw, 1.65rem) !important;
|
||||
padding-bottom: clamp(1.05rem, 2vw, 1.65rem) !important;
|
||||
}
|
||||
|
||||
.momo-observability-mode .obs-hero::after,
|
||||
@@ -226,8 +224,8 @@
|
||||
.momo-observability-mode .quality-subtitle,
|
||||
.momo-observability-mode .ppt-subtitle {
|
||||
color: color-mix(in srgb, var(--obs-ink) 62%, var(--obs-muted)) !important;
|
||||
font-size: 0.94rem !important;
|
||||
line-height: 1.68 !important;
|
||||
font-size: 0.98rem !important;
|
||||
line-height: 1.75 !important;
|
||||
letter-spacing: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -300,7 +300,7 @@
|
||||
|
||||
.biz-decision-card {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(82px, .36fr) minmax(240px, 1.15fr) minmax(130px, .46fr);
|
||||
grid-template-columns: minmax(82px, .35fr) minmax(180px, 1fr) minmax(120px, .45fr) minmax(160px, .7fr);
|
||||
gap: .8rem;
|
||||
align-items: center;
|
||||
padding: .95rem;
|
||||
@@ -329,12 +329,9 @@
|
||||
}
|
||||
|
||||
.biz-decision-reason {
|
||||
grid-column: 2 / 4;
|
||||
color: var(--biz-muted);
|
||||
font-size: .82rem;
|
||||
line-height: 1.5;
|
||||
padding-top: .55rem;
|
||||
border-top: 1px dashed rgba(201, 100, 66, 0.18);
|
||||
}
|
||||
|
||||
.biz-price-stack {
|
||||
@@ -404,10 +401,6 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.biz-decision-reason {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.biz-alert-strip {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user