From be986b8b97e21b666f5830e1cac7b95092d3ee41 Mon Sep 17 00:00:00 2001 From: OoO Date: Tue, 5 May 2026 14:19:09 +0800 Subject: [PATCH] =?UTF-8?q?fix(observability):=20=E7=BC=BA=E8=A1=A8?= =?UTF-8?q?=E6=99=82=E6=94=B9=E7=82=BA=E5=AE=89=E5=85=A8=E7=A9=BA=E7=8B=80?= =?UTF-8?q?=E6=85=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/admin_observability_routes.py | 123 ++++++++++++++++++--------- static/css/observability-system.css | 14 ++- templates/admin/business_intel.html | 9 +- 3 files changed, 91 insertions(+), 55 deletions(-) diff --git a/routes/admin_observability_routes.py b/routes/admin_observability_routes.py index d4e1493..6553c12 100644 --- a/routes/admin_observability_routes.py +++ b/routes/admin_observability_routes.py @@ -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() diff --git a/static/css/observability-system.css b/static/css/observability-system.css index 9fbf183..40e69f2 100644 --- a/static/css/observability-system.css +++ b/static/css/observability-system.css @@ -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; } diff --git a/templates/admin/business_intel.html b/templates/admin/business_intel.html index d2667cf..544abd7 100644 --- a/templates/admin/business_intel.html +++ b/templates/admin/business_intel.html @@ -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;