修正 Code Review Gemini 備援遙測
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
This commit is contained in:
@@ -132,6 +132,7 @@
|
||||
- Phase 62 candidate queue writer run receipt:新增 `services/market_intel/candidate_queue_writer_run_receipt.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_writer_run_receipt` 與 UI receipt 按鈕,審核 CLI 寫入後的 writer output、post-write smoke、dedupe key 一致性與 artifact 路徑;API/UI 不回吐 receipt 原文、不讀 approval token、不執行 CLI、不連 DB、不寫 queue、不掛 scheduler;版本同步至 V10.247。
|
||||
- Phase 63 candidate queue writer run closeout:新增 `services/market_intel/candidate_queue_writer_run_closeout.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_writer_run_closeout` 與 UI closeout 按鈕,在 receipt 通過後檢查 closeout artifact、操作員人工 queue review/read-only inventory 確認與安全 promotion gate;API/UI 不回吐原始 receipt、不讀 approval token、不執行 CLI、不連 DB、不寫 queue、不掛 scheduler;版本同步至 V10.248。
|
||||
- V10.248 補市場情報 390px preview panel QA:sample review 工具列改為 textarea + 可換行 action rail,移除舊的硬編 8 欄 grid;`check_responsive_overflow` 新增 `--screenshot-all`,本機 390x844 `/market_intel` 真頁面 QA 通過且 overflow=0。
|
||||
- V10.250 補 Code Review Gemini 備援遙測護欄:Ollama 主路徑失敗時 `fallback_to` 明確指向 `code_review_openclaw_gemini`,測試鎖住「Gemini 不得記成 `code_review_openclaw` 主 caller」;AI Calls 觀測台會把 legacy `code_review_openclaw + gemini` 顯示成 Gemini 備援,避免誤判 Gemini-first。
|
||||
- Schema smoke:`tests/test_market_intel_skeleton.py` 檢查 `Base.metadata` 內含 ADR-035 八張 `market_*` tables。
|
||||
- Desktop UI QA:本機只註冊 `market_intel_bp` 的 Flask harness 載入 `/market_intel`,確認 Phase 15、候選預覽、writer preview、安全 flags、點陣暖紙視覺正常,console error 0。
|
||||
- API QA:`/api/market_intel/schema_smoke` 通過 7 張表與 `market_platforms` 必要欄位檢查;`/api/market_intel/platform_seed_writer_plan` 回傳 4 筆 dry-run upsert preview,`writes_executed=false`,四平台皆 `blocked_dry_run_only`。
|
||||
|
||||
@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.249"
|
||||
SYSTEM_VERSION = "V10.250"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -39,11 +39,57 @@ _PPT_AIDER_HEAL_LOCK = threading.Lock()
|
||||
_PPT_AIDER_HEAL_ACTIVE = {}
|
||||
|
||||
|
||||
_GEMINI_BACKUP_CALLER_DISPLAY = {
|
||||
# 專用備援 caller 落地前的舊資料仍會存在;顯示時標成備援,避免誤判 Gemini-first。
|
||||
'code_review_openclaw': 'code_review_openclaw_gemini',
|
||||
}
|
||||
_GEMINI_BACKUP_CALLERS = {
|
||||
'code_review_openclaw_gemini',
|
||||
'openclaw_bot_gemini',
|
||||
'openclaw_bot_image_gemini',
|
||||
}
|
||||
|
||||
|
||||
def _list_ppt_aider_heal_active_jobs():
|
||||
with _PPT_AIDER_HEAL_LOCK:
|
||||
return [dict(job) for job in _PPT_AIDER_HEAL_ACTIVE.values()]
|
||||
|
||||
|
||||
def _build_ai_call_recent_row(row):
|
||||
"""整理最近 ai_calls 列表,讓 Ollama-first 備援語意可被辨識。"""
|
||||
caller = row[2] or ''
|
||||
provider = row[3] or ''
|
||||
caller_display = caller
|
||||
route_badges = []
|
||||
|
||||
if provider == 'gemini':
|
||||
if caller in _GEMINI_BACKUP_CALLER_DISPLAY:
|
||||
caller_display = _GEMINI_BACKUP_CALLER_DISPLAY[caller]
|
||||
route_badges.append('Gemini 備援')
|
||||
route_badges.append('舊 caller')
|
||||
elif caller in _GEMINI_BACKUP_CALLERS or caller.endswith('_gemini'):
|
||||
route_badges.append('Gemini 備援')
|
||||
else:
|
||||
route_badges.append('ADR-028 鎖定/升級')
|
||||
|
||||
return {
|
||||
'id': row[0],
|
||||
'called_at': row[1].strftime('%H:%M:%S'),
|
||||
'caller': caller,
|
||||
'caller_display': caller_display,
|
||||
'provider': provider,
|
||||
'model': row[4],
|
||||
'in_tokens': int(row[5] or 0),
|
||||
'out_tokens': int(row[6] or 0),
|
||||
'duration_ms': int(row[7] or 0),
|
||||
'status': row[8],
|
||||
'cost': float(row[9] or 0),
|
||||
'cache_hit': bool(row[10]),
|
||||
'rag_hit': bool(row[11]),
|
||||
'route_badges': route_badges,
|
||||
}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# /observability/overview — Phase 45 總覽(單頁聚合 6 項 KPI)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -716,7 +762,7 @@ _AGENT_CALLER_GROUPS = {
|
||||
'openclaw_qa', 'openclaw_daily', 'openclaw_daily_insight',
|
||||
'openclaw_meta', 'openclaw_monthly', 'openclaw_weekly',
|
||||
'openclaw_bot_main', 'openclaw_bot_gemini', 'openclaw_bot_nim',
|
||||
'sales_copy', 'code_review_openclaw',
|
||||
'sales_copy', 'code_review_openclaw', 'code_review_openclaw_gemini',
|
||||
],
|
||||
'hermes': [
|
||||
'hermes_analyst', 'hermes_intent', 'code_review_hermes',
|
||||
@@ -1165,15 +1211,7 @@ def ai_calls_dashboard():
|
||||
'tokens': int(r[2] or 0), 'cost': float(r[3] or 0)}
|
||||
for r in by_provider
|
||||
],
|
||||
recent=[
|
||||
{'id': r[0], 'called_at': r[1].strftime('%H:%M:%S'),
|
||||
'caller': r[2], 'provider': r[3], 'model': r[4],
|
||||
'in_tokens': int(r[5] or 0), 'out_tokens': int(r[6] or 0),
|
||||
'duration_ms': int(r[7] or 0), 'status': r[8],
|
||||
'cost': float(r[9] or 0), 'cache_hit': bool(r[10]),
|
||||
'rag_hit': bool(r[11])}
|
||||
for r in recent
|
||||
],
|
||||
recent=[_build_ai_call_recent_row(r) for r in recent],
|
||||
callers=[r[0] for r in callers],
|
||||
by_model=[
|
||||
{
|
||||
|
||||
@@ -336,7 +336,12 @@ class CodeReviewPipeline:
|
||||
if resp.success and (resp.content or '').strip():
|
||||
return resp.content or ""
|
||||
_ctx.set_error(resp.error or 'ollama generate failed')
|
||||
_ctx.fallback_to_caller('code_review_openclaw')
|
||||
fallback_caller = (
|
||||
'code_review_openclaw'
|
||||
if CODE_REVIEW_USE_CLAUDE
|
||||
else 'code_review_openclaw_gemini'
|
||||
)
|
||||
_ctx.fallback_to_caller(fallback_caller)
|
||||
logger.warning(
|
||||
"[CodeReview] OpenClaw Ollama 三主機皆失敗,才啟用雲端備援: %s",
|
||||
resp.error,
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
|
||||
<section class="calls-table-shell">
|
||||
<div class="calls-table-title"><div><div class="calls-label">最近呼叫</div><h3>最近呼叫 100 筆</h3></div></div>
|
||||
<div class="table-responsive"><table class="table table-sm table-striped mb-0"><thead class="table-light"><tr><th>編號</th><th>時間</th><th>呼叫端</th><th>供應商</th><th>模型</th><th class="text-end">輸入</th><th class="text-end">輸出</th><th class="text-end">耗時</th><th>狀態</th><th class="text-end">成本</th><th>標記</th></tr></thead><tbody>{% for r in recent %}<tr {% if r.status not in ['ok','cache_only'] %}class="table-warning"{% endif %}><td>{{ r.id }}</td><td><small>{{ r.called_at }}</small></td><td><code>{{ r.caller }}</code></td><td><small>{{ obs_label.provider(r.provider) }}</small></td><td><small>{{ r.model[:25] }}</small></td><td class="text-end">{{ r.in_tokens }}</td><td class="text-end">{{ r.out_tokens }}</td><td class="text-end">{{ r.duration_ms }}</td><td><small>{{ obs_label.status(r.status, '-') }}</small></td><td class="text-end">${{ "%.4f"|format(r.cost) }}</td><td>{% if r.cache_hit %}<span class="badge bg-success">快取</span>{% endif %}{% if r.rag_hit %}<span class="badge bg-info">RAG</span>{% endif %}</td></tr>{% endfor %}</tbody></table></div>
|
||||
<div class="table-responsive"><table class="table table-sm table-striped mb-0"><thead class="table-light"><tr><th>編號</th><th>時間</th><th>呼叫端</th><th>供應商</th><th>模型</th><th class="text-end">輸入</th><th class="text-end">輸出</th><th class="text-end">耗時</th><th>狀態</th><th class="text-end">成本</th><th>標記</th></tr></thead><tbody>{% for r in recent %}<tr {% if r.status not in ['ok','cache_only'] %}class="table-warning"{% endif %}><td>{{ r.id }}</td><td><small>{{ r.called_at }}</small></td><td><code>{{ r.caller_display or r.caller }}</code>{% if r.caller_display and r.caller_display != r.caller %}<br><small class="text-muted">原始:{{ r.caller }}</small>{% endif %}</td><td><small>{{ obs_label.provider(r.provider) }}</small></td><td><small>{{ r.model[:25] }}</small></td><td class="text-end">{{ r.in_tokens }}</td><td class="text-end">{{ r.out_tokens }}</td><td class="text-end">{{ r.duration_ms }}</td><td><small>{{ obs_label.status(r.status, '-') }}</small></td><td class="text-end">${{ "%.4f"|format(r.cost) }}</td><td>{% for badge in r.route_badges %}<span class="badge bg-secondary me-1">{{ badge }}</span>{% endfor %}{% if r.cache_hit %}<span class="badge bg-success">快取</span>{% endif %}{% if r.rag_hit %}<span class="badge bg-info">RAG</span>{% endif %}</td></tr>{% endfor %}</tbody></table></div>
|
||||
</section>
|
||||
|
||||
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Ollama 優先策略 v5.0 — AI 流量控制塔</small></p>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
@@ -99,6 +100,33 @@ def test_ai_calls_dashboard_db_error_falls_back(client, monkeypatch):
|
||||
assert r.status_code == 200 # 失敗安全:仍 render,不 500
|
||||
|
||||
|
||||
def test_ai_calls_recent_row_marks_legacy_code_review_gemini_as_backup():
|
||||
from routes.admin_observability_routes import _build_ai_call_recent_row
|
||||
|
||||
row = (
|
||||
1452,
|
||||
datetime(2026, 5, 19, 12, 2, 49),
|
||||
'code_review_openclaw',
|
||||
'gemini',
|
||||
'gemini-2.5-flash',
|
||||
181,
|
||||
56,
|
||||
9158,
|
||||
'ok',
|
||||
0,
|
||||
False,
|
||||
False,
|
||||
)
|
||||
|
||||
data = _build_ai_call_recent_row(row)
|
||||
|
||||
assert data['caller'] == 'code_review_openclaw'
|
||||
assert data['caller_display'] == 'code_review_openclaw_gemini'
|
||||
assert data['provider'] == 'gemini'
|
||||
assert 'Gemini 備援' in data['route_badges']
|
||||
assert '舊 caller' in data['route_badges']
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# /observability/promotion_review
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -77,6 +77,27 @@ def _stub_logger(monkeypatch):
|
||||
monkeypatch.setenv('AI_CALL_LOGGING_ENABLED', 'false')
|
||||
|
||||
|
||||
def _capture_ai_call_states(monkeypatch):
|
||||
"""攔截 ai_call_logger 寫入,保留 caller/provider/fallback_to 供路由斷言。"""
|
||||
import services.ai_call_logger as logger_mod
|
||||
|
||||
states = []
|
||||
|
||||
def _capture(state):
|
||||
states.append({
|
||||
"caller": state.caller,
|
||||
"provider": state.provider,
|
||||
"model": state.model,
|
||||
"status": state.status,
|
||||
"fallback_to": state.fallback_to,
|
||||
"error": state.error,
|
||||
})
|
||||
|
||||
monkeypatch.setattr(logger_mod, '_async_write', _capture)
|
||||
monkeypatch.setenv('AI_CALL_LOGGING_ENABLED', 'true')
|
||||
return states
|
||||
|
||||
|
||||
def _stub_ollama(monkeypatch, *, success: bool = True,
|
||||
content: str = "OLLAMA-RESULT",
|
||||
error: str = "all hosts failed"):
|
||||
@@ -245,6 +266,46 @@ def test_ollama_failure_flag_false_uses_gemini_backup(monkeypatch):
|
||||
fake_elephant.generate.assert_not_called()
|
||||
|
||||
|
||||
def test_gemini_backup_uses_dedicated_caller_in_telemetry(monkeypatch):
|
||||
"""Ollama 失敗後的 Gemini 必須記為 code_review_openclaw_gemini。"""
|
||||
monkeypatch.setenv('CODE_REVIEW_USE_CLAUDE', 'false')
|
||||
monkeypatch.setenv('GEMINI_API_KEY', 'test-key')
|
||||
captured = _capture_ai_call_states(monkeypatch)
|
||||
|
||||
svc_mod = _reload_pipeline()
|
||||
_stub_ollama(monkeypatch, success=False)
|
||||
_stub_anthropic(monkeypatch, svc_mod, available=True)
|
||||
fake_genai, fake_elephant = _stub_gemini_and_elephant(monkeypatch)
|
||||
|
||||
pipeline = _make_pipeline(svc_mod)
|
||||
result = pipeline._openclaw_assess(
|
||||
files={"services/foo.py": "def x(): pass"},
|
||||
findings=[],
|
||||
)
|
||||
|
||||
assert result == "GEMINI-RESULT"
|
||||
fake_genai.GenerativeModel.assert_called_once()
|
||||
fake_elephant.generate.assert_not_called()
|
||||
assert any(
|
||||
state["caller"] == "code_review_openclaw"
|
||||
and state["provider"] == "gcp_ollama"
|
||||
and state["status"] == "fallback"
|
||||
and state["fallback_to"] == "code_review_openclaw_gemini"
|
||||
for state in captured
|
||||
)
|
||||
assert any(
|
||||
state["caller"] == "code_review_openclaw_gemini"
|
||||
and state["provider"] == "gemini"
|
||||
and state["status"] == "ok"
|
||||
for state in captured
|
||||
)
|
||||
assert not any(
|
||||
state["caller"] == "code_review_openclaw"
|
||||
and state["provider"] == "gemini"
|
||||
for state in captured
|
||||
)
|
||||
|
||||
|
||||
def test_ollama_failure_claude_unavailable_uses_gemini_backup(monkeypatch):
|
||||
"""Ollama 失敗 + flag=true 但 Claude unavailable → Gemini 才作備援"""
|
||||
monkeypatch.setenv('CODE_REVIEW_USE_CLAUDE', 'true')
|
||||
|
||||
Reference in New Issue
Block a user