From 95db06ad9d932106c367b12822a07144479d7357 Mon Sep 17 00:00:00 2001 From: OoO Date: Mon, 4 May 2026 19:54:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(p48):=20=E5=95=86=E6=A5=AD=E9=9D=A2=20?= =?UTF-8?q?=C3=97=20AI=20=E7=B7=A8=E6=8E=92=E6=96=B0=E9=A0=81=20=E2=80=94?= =?UTF-8?q?=20AI=20=E5=9C=A8=E5=81=9A=E4=BB=80=E9=BA=BC=E7=94=9F=E6=84=8F?= =?UTF-8?q?=EF=BC=9F=E5=AF=A6=E9=9A=9B=E7=94=9F=E6=95=88=E5=97=8E=EF=BC=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新頁 /observability/business_intel:把 AI 觀測台從「技術面」延伸到「商業面」。 回答統帥兩大問: 1. 我們的 AI 在做什麼生意? 2. AI 動作真的有用嗎?(閉環追蹤) 新接 5 張未善用的商業面表(DB 利用率 17/22 → 22/22,100%): - ai_price_recommendations(AI 價格建議完整明細) - competitor_prices(競品價格快照) - competitor_price_history(24h 抓取歷史) - competitor_match_attempts(競品比對失敗追蹤) - 善用 action_plans × action_outcomes JOIN(閉環) 頁面 widget(7 張卡片): 1. unfollowed alert:high-confidence 但未轉化為 action_plan 的數量 2. AI 決策 by strategy(promote/watch/hold 含平均信心 + gap% + 銷量變化) 3. 最近 20 筆 AI 建議詳細(SKU/商品/MOMO 價/PChome 價/Gap/原因) 4. **閉環學習表**:plan → outcome 全鏈追蹤 含 verdict/before/after/變化 % — ADR-012 核心 KPI 5. Verdict 分布(effective/neutral/backfired 計數) 6. 競品比對嘗試統計(success/fail/avg_score) 7. 24h 競品價格抓取列表(SKU/商品/MOMO vs PChome gap) 入口: - sidebar AI 觀測 group 加「商業面 × AI」(07c) - /observability/overview 入口卡升級為 8 項 DB 全表覆蓋達成:22/22 = 100% - Phase 47 17 表 → Phase 48 22 表 - 新接:ai_price_recommendations / competitor_prices / competitor_price_history / competitor_match_attempts - 已用:ai_calls / ai_call_budgets / ai_insights / learning_episodes / rag_query_log / mcp_calls / incidents / heal_logs / playbooks / backup_log / embedding_retry_queue / agent_context / agent_strategy_weights / action_plans / action_outcomes / host_health_probes / ppt_audit_results Co-Authored-By: Claude Opus 4.7 (1M context) --- routes/admin_observability_routes.py | 199 ++++++++++++ templates/admin/business_intel.html | 327 ++++++++++++++++++++ templates/admin/observability_overview.html | 10 +- templates/components/_ewoooc_shell.html | 5 + 4 files changed, 539 insertions(+), 2 deletions(-) create mode 100644 templates/admin/business_intel.html diff --git a/routes/admin_observability_routes.py b/routes/admin_observability_routes.py index c0bc878..aecd803 100644 --- a/routes/admin_observability_routes.py +++ b/routes/admin_observability_routes.py @@ -247,6 +247,205 @@ def observability_overview(): ) +# ───────────────────────────────────────────────────────────────────────────── +# /observability/business_intel — Phase 48 商業面 × AI 編排 +# ───────────────────────────────────────────────────────────────────────────── + +@admin_observability_bp.route('/business_intel') +@login_required +def business_intel_dashboard(): + """Phase 48 — 商業面 × AI 編排:把 AI 觀測台延伸到商業層級。 + + 展現「AI 在做什麼生意」: + - ai_price_recommendations × competitor_prices: AI 看到什麼定價機會 + - action_plans × action_outcomes: 計畫到 verdict 的閉環 + - competitor_match_attempts: 競品比對失敗追蹤 + """ + days = int(request.args.get('days', '7')) + session = get_session() + try: + # 1. ai_price_recommendations 30d 總覽 + rec_summary = session.execute( + sa_text(f""" + SELECT strategy, COUNT(*) AS cnt, + COALESCE(AVG(confidence), 0) AS avg_conf, + COALESCE(AVG(gap_pct), 0) AS avg_gap_pct, + COALESCE(AVG(sales_7d_delta), 0) AS avg_sales_delta + FROM ai_price_recommendations + WHERE created_at >= NOW() - INTERVAL '{int(days)} days' + GROUP BY strategy ORDER BY cnt DESC + """), + ).fetchall() + rec_by_strategy = [ + { + 'strategy': r[0], 'count': int(r[1] or 0), + 'avg_confidence': round(float(r[2] or 0), 3), + 'avg_gap_pct': round(float(r[3] or 0), 2), + 'avg_sales_delta': round(float(r[4] or 0), 2), + } + for r in rec_summary + ] + + # 2. ai_price_recommendations 最近 20 筆詳細 + latest_recs = session.execute( + sa_text(""" + SELECT id, sku, LEFT(name, 50), strategy, confidence, + momo_price, pchome_price, gap_pct, sales_7d_delta, + LEFT(reason, 120), created_at + FROM ai_price_recommendations + ORDER BY created_at DESC LIMIT 20 + """), + ).fetchall() + latest_recommendations = [ + { + 'id': r[0], 'sku': r[1], 'name': r[2], 'strategy': r[3], + 'confidence': round(float(r[4] or 0), 3), + 'momo_price': float(r[5] or 0), + 'pchome_price': float(r[6] or 0) if r[6] else None, + 'gap_pct': round(float(r[7] or 0), 2), + 'sales_delta': round(float(r[8] or 0), 2) if r[8] is not None else None, + 'reason': r[9] or '', + 'created_at': r[10].strftime('%m-%d %H:%M') if r[10] else '', + } + for r in latest_recs + ] + + # 3. action_plans × action_outcomes 閉環(30d) + closed_loops = session.execute( + sa_text(f""" + SELECT p.id, p.sku, p.plan_type, p.status, + p.created_by, p.created_at, p.executed_at, + o.verdict, o.metric_type, o.before_val, o.after_val + FROM action_plans p + LEFT JOIN action_outcomes o ON o.plan_id = p.id + WHERE p.created_at >= NOW() - INTERVAL '{int(days)} days' + ORDER BY p.created_at DESC LIMIT 25 + """), + ).fetchall() + loop_records = [] + for r in closed_loops: + before = float(r[9]) if r[9] is not None else None + after = float(r[10]) if r[10] is not None else None + change_pct = None + if before and before != 0 and after is not None: + change_pct = (after - before) / abs(before) * 100 + loop_records.append({ + 'plan_id': r[0], 'sku': r[1], 'plan_type': r[2], + 'status': r[3], 'created_by': r[4], + 'created_at': r[5].strftime('%m-%d %H:%M') if r[5] else '', + 'executed_at': r[6].strftime('%m-%d %H:%M') if r[6] else None, + 'verdict': r[7], 'metric_type': r[8], + 'before': before, 'after': after, 'change_pct': change_pct, + }) + + # 4. action_outcomes verdict 統計 + verdict_summary = session.execute( + sa_text(f""" + SELECT verdict, COUNT(*) AS cnt, + AVG(after_val - before_val) AS avg_delta + FROM action_outcomes + WHERE created_at >= NOW() - INTERVAL '{int(days)} days' + AND before_val IS NOT NULL AND after_val IS NOT NULL + GROUP BY verdict ORDER BY cnt DESC + """), + ).fetchall() + verdict_stats = [ + { + 'verdict': r[0] or 'unknown', 'count': int(r[1] or 0), + 'avg_delta': round(float(r[2] or 0), 2), + } + for r in verdict_summary + ] + + # 5. competitor_match_attempts 失敗統計(30d) + match_attempts = session.execute( + sa_text(f""" + SELECT attempt_status, COUNT(*) AS cnt, + COALESCE(AVG(candidate_count), 0) AS avg_candidates, + COALESCE(AVG(best_match_score), 0) AS avg_score + FROM competitor_match_attempts + WHERE attempted_at >= NOW() - INTERVAL '{int(days)} days' + GROUP BY attempt_status ORDER BY cnt DESC + """), + ).fetchall() + match_stats = [ + { + 'status': r[0], 'count': int(r[1] or 0), + 'avg_candidates': round(float(r[2] or 0), 1), + 'avg_score': round(float(r[3] or 0), 3), + } + for r in match_attempts + ] + + # 6. competitor_prices 24h 變動 TOP 10 + recent_competitor = session.execute( + sa_text(""" + SELECT cph.sku, cph.competitor_product_name, cph.price, + cph.momo_price, cph.discount_pct, cph.match_score, + cph.crawled_at + FROM competitor_price_history cph + WHERE cph.crawled_at >= NOW() - INTERVAL '24 hours' + AND cph.match_score >= 0.7 + ORDER BY cph.crawled_at DESC LIMIT 12 + """), + ).fetchall() + recent_competitor_prices = [ + { + 'sku': r[0], + 'product_name': (r[1] or '')[:50], + 'pchome_price': float(r[2] or 0), + 'momo_price': float(r[3] or 0) if r[3] else None, + 'discount_pct': int(r[4]) if r[4] else None, + 'match_score': round(float(r[5] or 0), 3), + 'gap': (float(r[3]) - float(r[2])) if (r[2] and r[3]) else None, + 'crawled_at': r[6].strftime('%m-%d %H:%M') if r[6] else '', + } + for r in recent_competitor + ] + + # 7. 高 confidence 但未 follow-through (recommendation 沒對應 action_plan) + unfollowed = session.execute( + sa_text(f""" + SELECT COUNT(*) + FROM ai_price_recommendations r + WHERE r.created_at >= NOW() - INTERVAL '{int(days)} days' + AND r.confidence >= 0.7 + AND NOT EXISTS ( + SELECT 1 FROM action_plans p + WHERE p.sku = r.sku + AND p.created_at >= r.created_at + AND p.created_at < r.created_at + INTERVAL '7 days' + ) + """), + ).fetchone() + unfollowed_count = int(unfollowed[0] or 0) if unfollowed else 0 + + return render_template( + 'admin/business_intel.html', + active_page='obs_business_intel', + days=days, + rec_by_strategy=rec_by_strategy, + latest_recommendations=latest_recommendations, + loop_records=loop_records, + verdict_stats=verdict_stats, + match_stats=match_stats, + recent_competitor_prices=recent_competitor_prices, + unfollowed_count=unfollowed_count, + error=None, + ) + except Exception as e: + return render_template( + 'admin/business_intel.html', + active_page='obs_business_intel', days=days, + rec_by_strategy=[], latest_recommendations=[], loop_records=[], + verdict_stats=[], match_stats=[], recent_competitor_prices=[], + unfollowed_count=0, + error=f'查詢失敗: {type(e).__name__}: {str(e)[:200]}', + ) + finally: + session.close() + + # ───────────────────────────────────────────────────────────────────────────── # /observability/agent_orchestration — Phase 46 編排矩陣 # ───────────────────────────────────────────────────────────────────────────── diff --git a/templates/admin/business_intel.html b/templates/admin/business_intel.html new file mode 100644 index 0000000..c0b5ae2 --- /dev/null +++ b/templates/admin/business_intel.html @@ -0,0 +1,327 @@ +{% extends "ewoooc_base.html" %} + +{% block title %}商業面 × AI 編排{% endblock %} + +{% block ewooo_content %} +
+

商業面 × AI 編排 + 過去 {{ days }} 日 · AI 在做什麼生意?實際生效嗎? +

+ + {% if error %} +
{{ error }}
+ {% endif %} + +
+
+ +
+
+ + {% if unfollowed_count > 0 %} +
+ + 過去 {{ days }} 日有 {{ unfollowed_count }} 筆 high-confidence (≥0.7) AI 建議 + 未轉化為 action_plan — 機會流失! +
+ {% endif %} + + + {% if rec_by_strategy %} +
+
+ AI 價格決策 by strategy({{ days }} 日) + 資料來源:ai_price_recommendations +
+
+ + + + + + + + + + + + {% for s in rec_by_strategy %} + + + + + + + + {% endfor %} + +
策略數量平均信心平均 gap %平均 7d 銷量變化 %
+ {% if s.strategy == 'promote' %}promote 推廣 + {% elif s.strategy == 'watch' %}watch 觀察 + {% elif s.strategy == 'hold' %}hold 持平 + {% else %}{{ s.strategy }}{% endif %} + {{ s.count }}{{ "%.2f"|format(s.avg_confidence) }} + + {{ "%.1f"|format(s.avg_gap_pct) }}% + + + + {{ "%+.1f"|format(s.avg_sales_delta) }}% + +
+
+
+ {% else %} +
+ + 過去 {{ days }} 日無 AI 價格決策資料(ai_price_recommendations 表)。 +
+ {% endif %} + + + {% if latest_recommendations %} +
+
+ 最近 20 筆 AI 價格建議 + 資料來源:ai_price_recommendations(含競品快照) +
+
+ + + + + + + + + + + + + + {% for r in latest_recommendations %} + + + + + + + + + + + + + {% endfor %} + +
時間SKU商品策略信心MOMO 價PChome 價Gap7d 銷量原因
{{ r.created_at }}{{ r.sku }}{{ r.name }}{% if r.name|length >= 50 %}…{% endif %} + {% if r.strategy == 'promote' %} + {% elif r.strategy == 'watch' %} + {% elif r.strategy == 'hold' %} + {% else %}{{ r.strategy }}{% endif %} + + + {{ "%.2f"|format(r.confidence) }} + + ${{ "%.0f"|format(r.momo_price) }} + {% if r.pchome_price %}${{ "%.0f"|format(r.pchome_price) }}{% else %}—{% endif %} + + + {{ "%+.1f"|format(r.gap_pct) }}% + + + {% if r.sales_delta is not none %} + + {{ "%+.1f"|format(r.sales_delta) }}% + + {% else %}{% endif %} + {{ r.reason }}{% if r.reason|length >= 120 %}…{% endif %}
+
+
+ {% endif %} + + + {% if loop_records %} +
+
+ 閉環學習:plan → outcome 全鏈追蹤({{ days }} 日) + 資料來源:action_plans LEFT JOIN action_outcomes(ADR-012 核心) +
+
+ + + + + + + + + + + + {% for l in loop_records %} + + + + + + + + + + + + + + + {% endfor %} + +
PlanSKU類型狀態建立者建立時間執行時間Verdict指標BeforeAfter變化
#{{ l.plan_id }}{{ l.sku or '—' }}{{ l.plan_type or '—' }} + {% if l.status == 'pending' %}待審 + {% elif l.status == 'approved' %}已批 + {% elif l.status == 'executed' %}已執 + {% elif l.status == 'rejected' %}拒絕 + {% else %}{{ l.status }}{% endif %} + {{ l.created_by or '—' }}{{ l.created_at }}{{ l.executed_at or '—' }} + {% if l.verdict == 'effective' %}有效 + {% elif l.verdict == 'backfired' %}適得其反 + {% elif l.verdict == 'neutral' %}無變 + {% else %}{% endif %} + {{ l.metric_type or '—' }}{% if l.before is not none %}{{ "%.1f"|format(l.before) }}{% else %}—{% endif %}{% if l.after is not none %}{{ "%.1f"|format(l.after) }}{% else %}—{% endif %} + {% if l.change_pct is not none %} + + {{ "%+.1f"|format(l.change_pct) }}% + + {% else %}{% endif %} +
+
+
+ {% endif %} + + + {% if verdict_stats %} +
+
+ Outcomes Verdict 分布 + {{ days }} 日 · AI 動作的實際成效閉環 +
+
+
+ {% set total_v = (verdict_stats | sum(attribute='count')) or 1 %} + {% for v in verdict_stats %} +
+
+ + {% if v.verdict == 'effective' %} 有效 + {% elif v.verdict == 'backfired' %} 適得其反 + {% elif v.verdict == 'neutral' %} 無變化 + {% else %}{{ v.verdict }}{% endif %} + + {{ v.count }} + {{ "%.0f"|format(v.count / total_v * 100) }}% · Δ {{ "%+.1f"|format(v.avg_delta) }} +
+
+ {% endfor %} +
+
+
+ {% endif %} + + + {% if match_stats %} +
+
+ 競品比對嘗試({{ days }} 日) + 資料來源:competitor_match_attempts — AI 找不到對應商品時的失敗追蹤 +
+
+ + + + + + + + + + + {% for m in match_stats %} + + + + + + + {% endfor %} + +
狀態次數平均候選數平均匹配分
+ {% if 'success' in (m.status or '').lower() %}{{ m.status }} + {% elif 'fail' in (m.status or '').lower() or 'no_' in (m.status or '').lower() %}{{ m.status }} + {% else %}{{ m.status }}{% endif %} + {{ "{:,}".format(m.count) }}{{ m.avg_candidates }}{{ "%.2f"|format(m.avg_score) }}
+
+
+ {% endif %} + + + {% if recent_competitor_prices %} +
+
+ 過去 24h 競品價格抓取(match_score ≥ 0.7) + 資料來源:competitor_price_history — AI 看到的競品價格全景 +
+
+ + + + + + + + + + + {% for c in recent_competitor_prices %} + + + + + + + + + + + {% endfor %} + +
時間SKU競品商品PChomeMOMOGap折扣匹配
{{ c.crawled_at }}{{ c.sku }}{{ c.product_name }}${{ "%.0f"|format(c.pchome_price) }} + {% if c.momo_price %}${{ "%.0f"|format(c.momo_price) }}{% else %}{% endif %} + + {% if c.gap is not none %} + + {{ "%+.0f"|format(c.gap) }} + + {% else %}{% endif %} + + {% if c.discount_pct %}-{{ c.discount_pct }}% + {% else %}{% endif %} + {{ "%.2f"|format(c.match_score) }}
+
+
+ {% endif %} + +

+ Operation Ollama-First v5.0 / Phase 48 — 商業面 × AI 編排 + (6 表跨 JOIN:ai_price_recommendations / action_plans / action_outcomes / + competitor_price_history / competitor_match_attempts / competitor_prices) +

+
+{% endblock %} diff --git a/templates/admin/observability_overview.html b/templates/admin/observability_overview.html index 220318c..1a59ad5 100644 --- a/templates/admin/observability_overview.html +++ b/templates/admin/observability_overview.html @@ -219,9 +219,9 @@ {% endif %} - +