feat(p39): 觀測台升級 — DB + MCP + RAG + AI 自動化深度整合
All checks were successful
CD Pipeline / deploy (push) Successful in 2m30s
All checks were successful
CD Pipeline / deploy (push) Successful in 2m30s
統帥質疑:6 頁觀測台只是 raw stats dashboard,沒展現 AI 自動化專業。 深度盤點 4 軸結果: - DB 利用率 22.7%(22 表只用 5 張) - MCP 整合 1/6(mcp_calls 表完全沒被讀) - RAG 整合 0/6(沒 import rag_service) - AI 自動化 L0 × 5 + L1 × 1(純讀 dashboard,無一鍵觸發) 本 commit 5 個增強: D-1: promotion_review 加 RAG「Top 3 相似已晉升」 - 對每筆 awaiting_review episode 跑 rag_service.query 找 ai_insights 中 cosine ≥ 0.7 的相似已晉升內容 - 輔助人工判斷:是否冗餘?是否新領域? - header 顯示 ai_insights 知識庫 size - fail-safe: 單筆 RAG 失敗不影響其餘 D-2: host_health 加 MCP 24h 工作量 widget - 從 mcp_calls 統計各 server 24h 呼叫次數 / 成功率 / cache 率 / 使用 tool 數 / 平均耗時 / cost - 展現「AI×MCP 編排規模」而非只「server 健康與否」 D-3: ai_calls × rag_query_log × mcp_calls 三表 JOIN - 新增「呼叫端 × RAG × MCP 編排矩陣」card - 每個 caller:總呼叫 / RAG 命中率 / MCP 編排率(透過 request_id 串接) / RAG 反饋分數 / 反饋筆數 - 展現「AI 自動化專業」核心指標 D-4: budget 加 RAG 自動策略建議 + 一鍵 force-throttle (L2) - ratio ≥ 0.8 時自動 RAG 召回 ai_insights 中的 budget_strategy 知識 - POST /budget/force_throttle endpoint:立即重算 cost_throttle 狀態 (不等下次每小時 cron)— 升級到 L2 自動化 - 對應頁面加「立即重算節流狀態」按鈕 D-5: host_health 加 incidents + heal_logs 7d 摘要 - 顯示 ADR-013 AutoHeal 閉環核心 KPI: 總事件 / 未解決 / 已解決 / P0+P1 / 自癒成功率 / 平均自癒耗時 - 展現「AIOps 自癒系統」運作實況 對應升級: - DB 利用率 22.7% → ~50%(新接 mcp_calls + rag_query_log JOIN + ai_insights + incidents + heal_logs) - MCP 整合 1/6 → 3/6(host_health + ai_calls + budget 都接 mcp_calls) - RAG 整合 0/6 → 3/6(promotion_review + budget + 待 quality_trend) - AI 自動化 L1 → L2 一鍵 force-throttle 一個(其餘按鈕待 D-6) 全部 fail-safe:DB 表/RAG/MCP 失敗都不擋頁面渲染。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -109,6 +109,33 @@ def ai_calls_dashboard():
|
||||
{'since': since},
|
||||
).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()
|
||||
|
||||
return render_template(
|
||||
'admin/ai_calls_dashboard.html',
|
||||
active_page='obs_ai_calls',
|
||||
@@ -140,6 +167,19 @@ def ai_calls_dashboard():
|
||||
for r in recent
|
||||
],
|
||||
callers=[r[0] for r in callers],
|
||||
caller_richness=[
|
||||
{
|
||||
'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),
|
||||
'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,
|
||||
}
|
||||
for r in caller_richness
|
||||
],
|
||||
error=None,
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -148,7 +188,7 @@ def ai_calls_dashboard():
|
||||
active_page='obs_ai_calls',
|
||||
hours=hours, caller_filter=caller_filter,
|
||||
provider_filter=provider_filter,
|
||||
summary={}, by_provider=[], recent=[], callers=[],
|
||||
summary={}, by_provider=[], recent=[], callers=[], caller_richness=[],
|
||||
error=f'查詢失敗: {type(e).__name__}: {str(e)[:200]}',
|
||||
)
|
||||
finally:
|
||||
@@ -162,7 +202,11 @@ def ai_calls_dashboard():
|
||||
@admin_observability_bp.route('/promotion_review')
|
||||
@login_required
|
||||
def promotion_review_list():
|
||||
"""awaiting_review episodes 列表(24h 內 reviewed_at IS NULL)"""
|
||||
"""awaiting_review episodes 列表(24h 內 reviewed_at IS NULL)
|
||||
|
||||
Phase 39(D-1):每筆 episode 自動跑 RAG 找 Top 3 相似已晉升 ai_insights,
|
||||
輔助人工判斷晉升價值。RAG fail-safe:失敗則 similar_insights=[],不擋頁面。
|
||||
"""
|
||||
session = get_session()
|
||||
try:
|
||||
rows = session.execute(
|
||||
@@ -177,20 +221,59 @@ def promotion_review_list():
|
||||
"""),
|
||||
).fetchall()
|
||||
|
||||
# ai_insights 全表大小(給「晉升後 KB 增長」對照)
|
||||
kb_size = 0
|
||||
try:
|
||||
kb_row = session.execute(
|
||||
sa_text("SELECT COUNT(*) FROM ai_insights"),
|
||||
).fetchone()
|
||||
kb_size = int(kb_row[0] or 0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
episodes = [
|
||||
{'id': r[0], 'created_at': r[1].strftime('%Y-%m-%d %H:%M'),
|
||||
'episode_type': r[2], 'source_table': r[3], 'source_id': r[4],
|
||||
'distilled_text': (r[5] or '')[:600],
|
||||
'quality_score': float(r[6] or 0),
|
||||
'weight': float(r[7] or 0),
|
||||
'status': r[8]}
|
||||
'status': r[8],
|
||||
'similar_insights': []}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
# Phase 39 D-1:對每筆 episode 跑 RAG 找 Top 3 相似已晉升
|
||||
try:
|
||||
from services.rag_service import rag_service
|
||||
for ep in episodes:
|
||||
try:
|
||||
rag_result = rag_service.query(
|
||||
text=ep['distilled_text'][:500],
|
||||
caller='admin_promotion_review',
|
||||
top_k=3,
|
||||
threshold=0.7,
|
||||
)
|
||||
ep['similar_insights'] = [
|
||||
{
|
||||
'id': h.get('id'),
|
||||
'insight_type': h.get('insight_type'),
|
||||
'content': (h.get('content') or '')[:180],
|
||||
'similarity': round(float(h.get('similarity', 0)), 3),
|
||||
'created_at': h.get('created_at').strftime('%Y-%m-%d')
|
||||
if h.get('created_at') else '',
|
||||
}
|
||||
for h in rag_result.hits[:3]
|
||||
]
|
||||
except Exception:
|
||||
pass # 單筆 RAG 失敗不影響其餘
|
||||
except Exception:
|
||||
pass # rag_service import 失敗(feature flag OFF)→ 略過
|
||||
|
||||
return render_template(
|
||||
'admin/promotion_review.html',
|
||||
active_page='obs_promotion_review',
|
||||
episodes=episodes,
|
||||
kb_size=kb_size,
|
||||
error=None,
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -198,6 +281,7 @@ def promotion_review_list():
|
||||
'admin/promotion_review.html',
|
||||
active_page='obs_promotion_review',
|
||||
episodes=[],
|
||||
kb_size=0,
|
||||
error=f'查詢失敗: {type(e).__name__}: {str(e)[:200]}',
|
||||
)
|
||||
finally:
|
||||
@@ -328,14 +412,79 @@ def budget_dashboard():
|
||||
'updated_at': b[5].strftime('%Y-%m-%d %H:%M') if b[5] else '-',
|
||||
})
|
||||
|
||||
return render_template('admin/budget.html', active_page='obs_budget', rows=rows, error=None)
|
||||
# Phase 39 D-4: RAG 自動建議策略(針對超 80% 的 row)
|
||||
budget_strategies = []
|
||||
over_threshold_rows = [r for r in rows if r.get('ratio', 0) >= 0.8]
|
||||
if over_threshold_rows:
|
||||
try:
|
||||
from services.rag_service import rag_service
|
||||
top_breach = max(over_threshold_rows, key=lambda r: r.get('ratio', 0))
|
||||
query_text = (
|
||||
f"預算超出 alert_pct provider={top_breach['provider']} "
|
||||
f"ratio={int(top_breach['ratio']*100)}% 應採取什麼節流策略"
|
||||
)
|
||||
rag_result = rag_service.query(
|
||||
text=query_text,
|
||||
caller='admin_budget_dashboard',
|
||||
top_k=3,
|
||||
threshold=0.65,
|
||||
)
|
||||
budget_strategies = [
|
||||
{
|
||||
'id': h.get('id'),
|
||||
'insight_type': h.get('insight_type'),
|
||||
'content': (h.get('content') or '')[:240],
|
||||
'similarity': round(float(h.get('similarity', 0)), 3),
|
||||
}
|
||||
for h in rag_result.hits[:3]
|
||||
]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return render_template(
|
||||
'admin/budget.html',
|
||||
active_page='obs_budget',
|
||||
rows=rows,
|
||||
budget_strategies=budget_strategies,
|
||||
error=None,
|
||||
)
|
||||
except Exception as e:
|
||||
return render_template('admin/budget.html', active_page='obs_budget', rows=[],
|
||||
budget_strategies=[],
|
||||
error=f'查詢失敗: {type(e).__name__}: {str(e)[:200]}')
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@admin_observability_bp.route('/budget/force_throttle', methods=['POST'])
|
||||
@login_required
|
||||
def budget_force_throttle():
|
||||
"""Phase 39 D-4 (L2 自動化):立即強制執行 cost_throttle evaluate(不等 hourly cron)。
|
||||
|
||||
用途:admin 在觀測台看到 ratio 飆超 110% 時不需等下次 cron,
|
||||
直接點按鈕強制 re-evaluate 三主機 throttle 狀態(claude→gemini fallback 立即生效)。
|
||||
"""
|
||||
try:
|
||||
from services.cost_throttle_service import (
|
||||
evaluate_throttle_status, is_cost_throttle_enabled,
|
||||
)
|
||||
if not is_cost_throttle_enabled():
|
||||
return jsonify({
|
||||
'ok': False,
|
||||
'error': 'COST_THROTTLE_ENABLED=false(先設環境變數)',
|
||||
}), 400
|
||||
new_state = evaluate_throttle_status()
|
||||
throttled = [p for p, s in new_state.items() if s.get('throttled')]
|
||||
return jsonify({
|
||||
'ok': True,
|
||||
'throttled_providers': throttled,
|
||||
'state': new_state,
|
||||
'message': f'已立即重算 throttle 狀態,被節流的 provider:{throttled or "(無)"}',
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'ok': False, 'error': f'{type(e).__name__}: {str(e)[:200]}'}), 500
|
||||
|
||||
|
||||
@admin_observability_bp.route('/budget/update/<int:budget_id>', methods=['POST'])
|
||||
@login_required
|
||||
def budget_update(budget_id: int):
|
||||
@@ -543,6 +692,8 @@ def host_health_dashboard():
|
||||
|
||||
# Phase 38:讀過去 24h 三主機健康歷史(給趨勢卡片)
|
||||
health_history = []
|
||||
mcp_24h = [] # Phase 39 D-2: MCP 24h 各 server 工作量
|
||||
aiops_summary = {} # Phase 39 D-5: incidents + heal_logs 7d 統計
|
||||
try:
|
||||
_session2 = get_session()
|
||||
try:
|
||||
@@ -570,6 +721,79 @@ def host_health_dashboard():
|
||||
}
|
||||
for r in history_rows
|
||||
]
|
||||
|
||||
# Phase 39 D-5:incidents + heal_logs 過去 7d 統計
|
||||
try:
|
||||
inc_rows = _session2.execute(
|
||||
sa_text("""
|
||||
SELECT
|
||||
COUNT(*) AS total_incidents,
|
||||
COUNT(*) FILTER (WHERE status = 'open') AS open_count,
|
||||
COUNT(*) FILTER (WHERE status = 'resolved') AS resolved_count,
|
||||
COUNT(*) FILTER (WHERE severity = 'P0') AS p0_count,
|
||||
COUNT(*) FILTER (WHERE severity = 'P1') AS p1_count
|
||||
FROM incidents
|
||||
WHERE created_at >= NOW() - INTERVAL '7 days'
|
||||
"""),
|
||||
).fetchone()
|
||||
heal_rows = _session2.execute(
|
||||
sa_text("""
|
||||
SELECT
|
||||
COUNT(*) AS total_heals,
|
||||
COUNT(*) FILTER (WHERE result = 'success') AS heal_success,
|
||||
COUNT(*) FILTER (WHERE result = 'failed') AS heal_failed,
|
||||
COALESCE(AVG(duration_ms) FILTER (WHERE result = 'success'), 0) AS avg_ms
|
||||
FROM heal_logs
|
||||
WHERE created_at >= NOW() - INTERVAL '7 days'
|
||||
"""),
|
||||
).fetchone()
|
||||
aiops_summary = {
|
||||
'incidents_total': int(inc_rows[0] or 0),
|
||||
'incidents_open': int(inc_rows[1] or 0),
|
||||
'incidents_resolved': int(inc_rows[2] or 0),
|
||||
'incidents_p0': int(inc_rows[3] or 0),
|
||||
'incidents_p1': int(inc_rows[4] or 0),
|
||||
'heals_total': int(heal_rows[0] or 0),
|
||||
'heals_success': int(heal_rows[1] or 0),
|
||||
'heals_failed': int(heal_rows[2] or 0),
|
||||
'heals_avg_ms': int(heal_rows[3] or 0),
|
||||
'heal_success_rate': (
|
||||
float(heal_rows[1] or 0) / float(heal_rows[0]) * 100
|
||||
) if heal_rows[0] else 0,
|
||||
}
|
||||
except Exception:
|
||||
aiops_summary = {}
|
||||
|
||||
# Phase 39 D-2:MCP 24h 工作量(每個 server)
|
||||
mcp_rows = _session2.execute(
|
||||
sa_text("""
|
||||
SELECT server,
|
||||
COUNT(*) AS total_calls,
|
||||
COUNT(*) FILTER (WHERE status = 'ok') AS ok_calls,
|
||||
COUNT(*) FILTER (WHERE cache_hit) AS cache_hits,
|
||||
COALESCE(SUM(cost_usd), 0) AS total_cost,
|
||||
COALESCE(AVG(duration_ms), 0) AS avg_ms,
|
||||
COUNT(DISTINCT tool) AS tools_used
|
||||
FROM mcp_calls
|
||||
WHERE called_at >= NOW() - INTERVAL '24 hours'
|
||||
GROUP BY server
|
||||
ORDER BY total_calls DESC
|
||||
"""),
|
||||
).fetchall()
|
||||
mcp_24h = [
|
||||
{
|
||||
'server': r[0],
|
||||
'total_calls': int(r[1] or 0),
|
||||
'ok_calls': int(r[2] or 0),
|
||||
'cache_hits': int(r[3] or 0),
|
||||
'total_cost': float(r[4] or 0),
|
||||
'avg_ms': int(r[5] or 0),
|
||||
'tools_used': int(r[6] or 0),
|
||||
'success_rate': (float(r[2] or 0) / float(r[1]) * 100) if r[1] else 0,
|
||||
'cache_rate': (float(r[3] or 0) / float(r[1]) * 100) if r[1] else 0,
|
||||
}
|
||||
for r in mcp_rows
|
||||
]
|
||||
finally:
|
||||
_session2.close()
|
||||
except Exception:
|
||||
@@ -582,4 +806,6 @@ def host_health_dashboard():
|
||||
mcp_status=mcp_status,
|
||||
throttle_state=throttle_state,
|
||||
health_history=health_history,
|
||||
mcp_24h=mcp_24h,
|
||||
aiops_summary=aiops_summary,
|
||||
)
|
||||
|
||||
@@ -54,6 +54,65 @@
|
||||
<div class="col-lg-2 col-md-4 col-sm-6"><div class="card p-2"><small>錯誤次數</small><h4 class="{% if (summary.error_calls or 0) > 0 %}text-danger{% endif %}">{{ summary.error_calls or 0 }}</h4></div></div>
|
||||
</div>
|
||||
|
||||
<!-- Phase 39 D-3: caller × RAG × MCP 編排矩陣 -->
|
||||
{% if caller_richness %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><strong><i class="fas fa-network-wired me-2"></i>呼叫端 × RAG × MCP 編排矩陣</strong>
|
||||
<small class="text-muted">資料來源:ai_calls × mcp_calls × rag_query_log({{ hours }}h 內呼叫 ≥ 5 次的 caller)</small>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<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 %}text-success{% elif c.rag_hit_rate >= 20 %}text-warning{% 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 %}text-info{% elif c.mcp_rate >= 10 %}text-warning{% endif %}">
|
||||
{{ "%.1f"|format(c.mcp_rate) }}%
|
||||
</strong>
|
||||
<small class="text-muted">({{ c.mcp_orchestrated }})</small>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
{% if c.feedback_count > 0 %}
|
||||
<strong class="{% if c.avg_rag_feedback >= 4 %}text-success{% elif c.avg_rag_feedback >= 3 %}text-warning{% else %}text-danger{% 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>
|
||||
<div class="card-footer small text-muted">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
<strong>RAG 命中率</strong>:caller 在此期間有多少呼叫吃到 RAG 召回;
|
||||
<strong>MCP 編排率</strong>:有多少呼叫透過 request_id 串接到 MCP tool;
|
||||
<strong>反饋分數</strong>:RAG 召回後人工反饋(1-5)。
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- by provider -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><strong>依供應商分組</strong></div>
|
||||
|
||||
@@ -16,6 +16,35 @@
|
||||
手動編輯預算後立即生效(不需重啟)。
|
||||
</p>
|
||||
|
||||
<!-- Phase 39 D-4: 一鍵立即重算 throttle 狀態(L2 自動化)-->
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-warning btn-sm" onclick="forceThrottle()">
|
||||
<i class="fas fa-bolt me-1"></i>立即重算節流狀態(不等 cron)
|
||||
</button>
|
||||
<small class="text-muted ms-2">用途:發現某 provider 飆超 110% 時立即 evaluate,毋需等下次每小時 cron。</small>
|
||||
</div>
|
||||
|
||||
<!-- Phase 39 D-4: RAG 自動建議策略 -->
|
||||
{% if budget_strategies %}
|
||||
<div class="card mb-3" style="border-left: 4px solid #6f42c1;">
|
||||
<div class="card-header bg-light">
|
||||
<strong><i class="fas fa-lightbulb me-2"></i>RAG 自動策略建議</strong>
|
||||
<small class="text-muted">— 從知識庫 ai_insights 召回過去類似超支情境的應對策略</small>
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
<ul class="list-unstyled small mb-0">
|
||||
{% for s in budget_strategies %}
|
||||
<li class="mb-2 p-2" style="background: #fafafa; border-radius: 4px;">
|
||||
<span class="badge bg-info text-dark 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>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<table class="table table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
@@ -76,6 +105,24 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function forceThrottle() {
|
||||
if (!confirm('立即重算所有 provider 的 throttle 狀態?\n(不等下次每小時 cron)')) return;
|
||||
try {
|
||||
const r = await fetch('/observability/budget/force_throttle', {method: 'POST'});
|
||||
const d = await r.json();
|
||||
if (d.ok) {
|
||||
const list = (d.throttled_providers && d.throttled_providers.length > 0)
|
||||
? d.throttled_providers.join(', ') : '(無)';
|
||||
alert(`✅ 已重算:被節流的 provider = ${list}`);
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('❌ ' + (d.error || '重算失敗'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveBudget(id) {
|
||||
const budgetInput = document.querySelector(`.budget-input[data-budget-id="${id}"]`);
|
||||
const alertInput = document.querySelector(`.alert-input[data-budget-id="${id}"]`);
|
||||
|
||||
@@ -112,6 +112,112 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AIOps 7d 摘要(Phase 39 D-5 新增) -->
|
||||
{% if aiops_summary %}
|
||||
<div class="card mb-3" style="border-left: 4px solid #0d6efd;">
|
||||
<div class="card-header">
|
||||
<strong><i class="fas fa-shield-virus me-2"></i>AIOps 自癒系統 7 日摘要</strong>
|
||||
<small class="text-muted">資料來源:incidents + heal_logs(ADR-013 AutoHeal 閉環)</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-2 col-sm-4">
|
||||
<div class="border rounded p-2 text-center">
|
||||
<small class="text-muted d-block">總事件</small>
|
||||
<strong style="font-size: 1.4em;">{{ aiops_summary.incidents_total }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-4">
|
||||
<div class="border rounded p-2 text-center">
|
||||
<small class="text-muted d-block">未解決</small>
|
||||
<strong class="{% if aiops_summary.incidents_open > 0 %}text-danger{% endif %}" style="font-size: 1.4em;">
|
||||
{{ aiops_summary.incidents_open }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-4">
|
||||
<div class="border rounded p-2 text-center">
|
||||
<small class="text-muted d-block">已解決</small>
|
||||
<strong class="text-success" style="font-size: 1.4em;">{{ aiops_summary.incidents_resolved }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-4">
|
||||
<div class="border rounded p-2 text-center">
|
||||
<small class="text-muted d-block">P0/P1</small>
|
||||
<strong class="{% if (aiops_summary.incidents_p0 + aiops_summary.incidents_p1) > 0 %}text-danger{% endif %}" style="font-size: 1.4em;">
|
||||
{{ aiops_summary.incidents_p0 + aiops_summary.incidents_p1 }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-4">
|
||||
<div class="border rounded p-2 text-center">
|
||||
<small class="text-muted d-block">自癒成功率</small>
|
||||
<strong class="{% if aiops_summary.heal_success_rate >= 80 %}text-success{% elif aiops_summary.heal_success_rate >= 50 %}text-warning{% else %}text-danger{% endif %}" style="font-size: 1.4em;">
|
||||
{{ "%.0f"|format(aiops_summary.heal_success_rate) }}%
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-4">
|
||||
<div class="border rounded p-2 text-center">
|
||||
<small class="text-muted d-block">平均自癒耗時</small>
|
||||
<strong style="font-size: 1.4em;">{{ aiops_summary.heals_avg_ms }} ms</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 small text-muted">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
7d 共 {{ aiops_summary.heals_total }} 次自癒嘗試
|
||||
(成功 {{ aiops_summary.heals_success }} · 失敗 {{ aiops_summary.heals_failed }})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- MCP 24h 工作量(Phase 39 D-2 新增) -->
|
||||
{% if mcp_24h %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><strong><i class="fas fa-bolt me-2"></i>MCP 服務 24h 工作量</strong>
|
||||
<small class="text-muted">資料來源:mcp_calls 表 — 展現 AI×MCP 編排規模</small>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<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">使用 Tool 數</th>
|
||||
<th class="text-end">平均耗時</th>
|
||||
<th class="text-end">成本 (USD)</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 %}text-success{% elif s.success_rate >= 80 %}text-warning{% else %}text-danger{% endif %}">
|
||||
{{ "%.1f"|format(s.success_rate) }}%
|
||||
</strong>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span class="{% if s.cache_rate >= 30 %}text-success{% endif %}">
|
||||
{{ "%.1f"|format(s.cache_rate) }}%
|
||||
</span>
|
||||
</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>
|
||||
{% endif %}
|
||||
|
||||
<!-- 過去 24h 健康趨勢(Phase 38 新增) -->
|
||||
{% if health_history %}
|
||||
<div class="card mb-3">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block content %}
|
||||
<div class="container-fluid mt-3">
|
||||
<h2 class="mb-3"><i class="fas fa-brain me-2"></i>RAG 學習晉升審核
|
||||
<small class="text-muted">待審核 × {{ episodes|length }} 筆</small>
|
||||
<small class="text-muted">待審核 × {{ episodes|length }} 筆 · 知識庫 ai_insights × {{ kb_size or 0 }} 筆</small>
|
||||
</h2>
|
||||
|
||||
{% if error %}
|
||||
@@ -34,6 +34,30 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<pre style="white-space: pre-wrap; font-size: 0.9em; max-height: 200px; overflow-y: auto;">{{ ep.distilled_text }}</pre>
|
||||
|
||||
{% if ep.similar_insights %}
|
||||
<div class="mt-3 p-2" style="background: #f7f7f9; border-radius: 6px; border-left: 3px solid #6f42c1;">
|
||||
<small class="text-muted d-block mb-2">
|
||||
<i class="fas fa-search me-1"></i><strong>RAG 自動檢索:知識庫中 Top 3 相似已晉升內容</strong>
|
||||
<span class="text-muted">(cosine ≥ 0.7)— 輔助判斷此次晉升是否冗餘</span>
|
||||
</small>
|
||||
<ul class="list-unstyled mb-0 small">
|
||||
{% for sim in ep.similar_insights %}
|
||||
<li class="mb-1">
|
||||
<span class="badge bg-light text-dark me-1">#{{ sim.id }}</span>
|
||||
<span class="badge bg-info text-dark me-1">{{ sim.insight_type }}</span>
|
||||
<span class="badge bg-secondary me-1">相似度 {{ "%.2f"|format(sim.similarity) }}</span>
|
||||
{% if sim.created_at %}<small class="text-muted me-1">{{ sim.created_at }}</small>{% endif %}
|
||||
<span class="text-dark">{{ sim.content }}{% if sim.content|length >= 180 %}…{% endif %}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mt-2"><small class="text-muted">
|
||||
<i class="fas fa-search me-1"></i>RAG 檢索:知識庫中無 cosine ≥ 0.7 的相似內容(屬新領域知識,建議晉升)
|
||||
</small></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<button class="btn btn-success btn-sm me-2" onclick="approveEpisode({{ ep.id }}, this)">
|
||||
|
||||
Reference in New Issue
Block a user