feat(ai): add external mcp rag integration readback

This commit is contained in:
ogt
2026-07-09 22:18:34 +08:00
parent df5593c1fb
commit cc3c0697a7
5 changed files with 436 additions and 0 deletions

View File

@@ -26,6 +26,8 @@
- Telegram 失敗必須可暫存與 replay。
- EventRouter / AutoHeal 變更必須更新 `services/ai_automation_metrics.py` 指標或確認既有指標已覆蓋。
- AI 自動化閉環變更必須確認 `/api/ai-automation/smoke``/ai_automation_smoke` 仍能反映新狀態。
- 外部 MCP / RAG 能力導入內部治理時,必須確認 `/api/ai-automation/external-mcp-rag-integration`
`python scripts/ops/report_external_mcp_rag_integration.py` 可讀回每個能力的內部落點、狀態、資料邊界與下一個機器動作。
- AI 自動化 Prometheus 指標變更必須同步檢查 `docker/grafana/provisioning/dashboards/json/ai-automation-overview.json` 是否需要新增 panel 或查詢。
- 188 線上 active monitoring stack 以 `monitoring/prometheus.yml` 為準110 gateway 另有 `/home/wooo/monitoring/prometheus.yml`。若 dashboard 無資料,先確認 Prometheus `momo-app` target 與 `momo-network` 連線;所有 Blackbox HTTP target 必須打 `/health`,不可打 Dashboard 首頁 `/`
- Smoke dashboard 會保存 JSONL 趨勢;若新增檢查項目,要確保 history compact record 仍保持小而可讀。

View File

@@ -689,6 +689,19 @@ def ai_automation_pixelrag_visual_evidence_readback_api():
))
@system_public_bp.route('/api/ai-automation/external-mcp-rag-integration')
@login_required
def ai_automation_external_mcp_rag_integration_api():
"""Read-only external MCP/RAG absorption status for internal MCP/RAG planes."""
from services.external_mcp_rag_integration_service import (
build_external_mcp_rag_integration_readback,
)
return jsonify(build_external_mcp_rag_integration_readback(
target_family=str(request.args.get('family') or '').strip() or None,
))
@system_public_bp.route('/api/ai-automation/smoke/history/export')
@login_required
def ai_automation_smoke_history_export():

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Report external MCP/RAG absorption into internal MCP/RAG planes."""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from services.external_mcp_rag_integration_service import ( # noqa: E402
build_external_mcp_rag_integration_readback,
)
def main() -> int:
parser = argparse.ArgumentParser(
description="輸出外部 MCP/RAG 導入內部 MCP/RAG 治理面的機器可讀狀態。"
)
parser.add_argument(
"--family",
choices=["external_mcp", "internal_mcp", "external_rag", "internal_rag"],
help="只輸出指定 capability family。",
)
args = parser.parse_args()
payload = build_external_mcp_rag_integration_readback(
target_family=args.family,
)
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
return 0 if payload.get("success") else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,308 @@
"""Read-only inventory for external MCP/RAG absorption into internal control planes."""
from __future__ import annotations
import os
from copy import deepcopy
from datetime import datetime, timezone
from typing import Any
POLICY = "read_only_external_mcp_rag_integration_readback_v1"
def _enabled(value: str | None) -> bool:
return str(value or "").strip().lower() in {"1", "true", "yes", "on"}
def _controlled_apply_boundary() -> dict[str, bool]:
return {
"network_call": False,
"db_write": False,
"secret_read": False,
"production_price_write": False,
"external_side_effect": False,
}
def _status_counts(items: list[dict[str, Any]]) -> dict[str, int]:
counts: dict[str, int] = {}
for item in items:
status = str(item.get("status") or "unknown")
counts[status] = counts.get(status, 0) + 1
return counts
def _mcp_runtime_snapshot() -> dict[str, Any]:
try:
from services.mcp_router import MCP_BASE_HOSTS, TOOL_REGISTRY, is_mcp_router_enabled
return {
"enabled": is_mcp_router_enabled(),
"servers": deepcopy(MCP_BASE_HOSTS),
"caller_count": len(TOOL_REGISTRY),
"tool_registry": {
caller: {
server: list(tools)
for server, tools in servers.items()
}
for caller, servers in TOOL_REGISTRY.items()
},
}
except Exception as exc:
return {
"enabled": False,
"servers": {},
"caller_count": 0,
"tool_registry": {},
"error": str(exc)[:300],
}
def _rag_runtime_snapshot() -> dict[str, Any]:
try:
from services.rag_service import (
RAG_DEFAULT_THRESHOLD,
RAG_DEFAULT_TOP_K,
RAG_EMBED_DIM,
RAG_EMBED_MODEL,
get_embedding_signature,
is_rag_enabled,
)
return {
"enabled": is_rag_enabled(),
"vector_store": "pgvector",
"embedding_model": RAG_EMBED_MODEL,
"embedding_dim": RAG_EMBED_DIM,
"embedding_signature": get_embedding_signature(),
"default_top_k": RAG_DEFAULT_TOP_K,
"default_threshold": RAG_DEFAULT_THRESHOLD,
}
except Exception as exc:
return {
"enabled": False,
"vector_store": "pgvector",
"embedding_model": "",
"embedding_dim": 0,
"embedding_signature": "",
"error": str(exc)[:300],
}
def _pixelrag_snapshot() -> dict[str, Any]:
try:
from services.pixelrag_crawler_integration_service import ECOMMERCE_PLATFORM_PROFILES
platforms = sorted(
platform
for platform in ECOMMERCE_PLATFORM_PROFILES
if platform not in {"market_intel", "external_market"}
)
return {
"enabled": True,
"platform_count": len(platforms),
"platforms": platforms,
"visual_rag_stage": "phase1_visual_evidence_receipts",
}
except Exception as exc:
return {
"enabled": False,
"platform_count": 0,
"platforms": [],
"visual_rag_stage": "unavailable",
"error": str(exc)[:300],
}
def _capability_inventory() -> list[dict[str, Any]]:
return [
{
"id": "mcp.omnisearch.tavily_exa",
"family": "external_mcp",
"external_capability": "Tavily / Exa style public web search through omnisearch MCP",
"internal_plane": "mcp_router",
"internal_entrypoint": "services.mcp_router.TOOL_REGISTRY[mcp_collector][omnisearch]",
"status": "integrated_self_hosted_gateway",
"writes": ["mcp_calls_log_async"],
"data_boundary": "public_search_summary_only",
"next_machine_action": "keep_mcp_router_health_and_cache_readback",
},
{
"id": "mcp.firecrawl.scrape",
"family": "external_mcp",
"external_capability": "Firecrawl style public page scrape",
"internal_plane": "mcp_router",
"internal_entrypoint": "services.mcp_router.TOOL_REGISTRY[*][firecrawl].scrape_url",
"status": "integrated_self_hosted_gateway",
"writes": ["mcp_calls_log_async"],
"data_boundary": "approved_public_url_only",
"next_machine_action": "enforce_source_contract_before_scheduler_attach",
},
{
"id": "mcp.postgres.query",
"family": "internal_mcp",
"external_capability": "SQL read interface exposed as MCP tool",
"internal_plane": "mcp_router",
"internal_entrypoint": "postgres.query for approved callers",
"status": "integrated_read_only_contract",
"writes": ["mcp_calls_log_async"],
"data_boundary": "read_only_query_allowlist",
"next_machine_action": "keep_disallowing_unknown_callers_and_write_tools",
},
{
"id": "mcp.filesystem.readonly",
"family": "internal_mcp",
"external_capability": "Filesystem MCP read diagnostics",
"internal_plane": "mcp_router",
"internal_entrypoint": "ops_diagnostics filesystem read-only tools",
"status": "integrated_read_only_contract",
"writes": ["mcp_calls_log_async"],
"data_boundary": "allowed_directories_read_only",
"next_machine_action": "keep_write_file_and_mutation_tools_rejected",
},
{
"id": "rag.pixelrag.visual_evidence",
"family": "external_rag",
"external_capability": "PixelRAG page screenshot and tile retrieval pattern",
"internal_plane": "pixelrag_visual_evidence + internal RAG candidate lane",
"internal_entrypoint": "services.pixelrag_crawler_integration_service",
"status": "integrated_phase1_visual_receipts",
"writes": ["artifact_file_only"],
"data_boundary": "public_page_screenshot_tiles_no_price_write",
"next_machine_action": "add_ocr_vlm_replay_before_promoting_to_pgvector_rag",
},
{
"id": "rag.pgvector.bge_m3",
"family": "internal_rag",
"external_capability": "General text RAG retrieval pattern",
"internal_plane": "rag_service",
"internal_entrypoint": "services.rag_service.RAGService.query",
"status": "internal_primary_ready",
"writes": ["rag_query_log_async"],
"data_boundary": "ai_insights_pgvector_embedding_signature_guard",
"next_machine_action": "keep_embedding_signature_and_feedback_guardrails",
},
{
"id": "rag.qwen3_vl_embedding",
"family": "external_rag",
"external_capability": "Multimodal visual embedding model for tile retrieval",
"internal_plane": "ollama_first_visual_embedding_benchmark",
"internal_entrypoint": "not_enabled_in_production",
"status": "deferred_until_ollama_first_verified",
"writes": [],
"data_boundary": "no_hosted_visual_embedding_api",
"next_machine_action": "benchmark_local_multimodal_embedding_then_design_pgvector_metadata",
},
{
"id": "rag.faiss_visual_index",
"family": "external_rag",
"external_capability": "FAISS visual retrieval index used by some PixelRAG pipelines",
"internal_plane": "pgvector_first_policy",
"internal_entrypoint": "not_enabled_in_production",
"status": "rejected_for_production_without_adr",
"writes": [],
"data_boundary": "pgvector_is_the_only_production_vector_store",
"next_machine_action": "use_pgvector_compatible_metadata_or_write_adr_before_any_faiss_trial",
},
{
"id": "mcp.gemini_grounding",
"family": "external_mcp",
"external_capability": "Gemini grounding as hosted external search fallback",
"internal_plane": "mcp_collector_final_fallback",
"internal_entrypoint": "services.mcp_collector_service guarded fallback",
"status": "disabled_by_default_fallback_only",
"writes": ["mcp_cache_only_when_explicitly_enabled"],
"data_boundary": "gemini_api_hard_disabled_by_default",
"next_machine_action": "prefer_self_hosted_mcp_or_ollama_static_fallback",
},
]
def build_external_mcp_rag_integration_readback(
*,
target_family: str | None = None,
) -> dict[str, Any]:
"""Describe how external MCP/RAG capabilities are absorbed internally."""
family_filter = str(target_family or "").strip().lower()
inventory = _capability_inventory()
if family_filter:
inventory = [
item
for item in inventory
if str(item.get("family") or "").lower() == family_filter
]
counts = _status_counts(inventory)
unresolved = [
item
for item in inventory
if str(item.get("status") or "").startswith(("deferred", "rejected", "disabled"))
]
absorbed = [
item
for item in inventory
if item not in unresolved
]
all_absorbed = len(unresolved) == 0
return {
"success": True,
"policy": POLICY,
"generated_at": datetime.now(timezone.utc).isoformat(),
"status": "partially_integrated" if not all_absorbed else "fully_integrated",
"answer_to_owner": (
"不是全部完成;已把主要外部 MCP/RAG 放進內部可治理 registry"
"其中 self-hosted MCP、pgvector RAG、PixelRAG phase1 已整合,"
"多模態 embedding / FAISS / Gemini grounding 仍需條件或維持停用。"
if not all_absorbed
else "目前 registry 內的外部 MCP/RAG 都已有內部治理落點。"
),
"completion": {
"total_capabilities": len(inventory),
"absorbed_count": len(absorbed),
"unresolved_count": len(unresolved),
"all_absorbed": all_absorbed,
"status_counts": counts,
},
"runtime": {
"mcp": _mcp_runtime_snapshot(),
"rag": _rag_runtime_snapshot(),
"pixelrag": _pixelrag_snapshot(),
"env_flags": {
"MCP_ROUTER_ENABLED": _enabled(os.getenv("MCP_ROUTER_ENABLED")),
"RAG_ENABLED": _enabled(os.getenv("RAG_ENABLED")),
"GEMINI_API_HARD_DISABLED": _enabled(os.getenv("GEMINI_API_HARD_DISABLED", "true")),
},
},
"inventory": inventory,
"next_machine_actions": [
{
"priority": "P0",
"action": "wire_pixelrag_receipts_to_internal_rag_candidate_replay",
"status": "ready_after_ocr_vlm_replay_contract",
},
{
"priority": "P0",
"action": "add_mcp_router_health_readback_to_ai_automation_smoke",
"status": "ready",
},
{
"priority": "P1",
"action": "benchmark_ollama_first_visual_embedding",
"status": "blocked_until_model_runtime_verified",
},
{
"priority": "P1",
"action": "promote_successful_marketplace_visual_receipts_to_source_contracts",
"status": "ready_for_shopee_warning_for_coupang",
},
],
"controlled_apply": _controlled_apply_boundary(),
}
__all__ = [
"POLICY",
"build_external_mcp_rag_integration_readback",
]

View File

@@ -0,0 +1,73 @@
import json
import subprocess
import sys
def test_external_mcp_rag_readback_reports_partial_truth():
from services.external_mcp_rag_integration_service import (
POLICY,
build_external_mcp_rag_integration_readback,
)
payload = build_external_mcp_rag_integration_readback()
inventory = {item["id"]: item for item in payload["inventory"]}
assert payload["policy"] == POLICY
assert payload["status"] == "partially_integrated"
assert payload["completion"]["all_absorbed"] is False
assert payload["completion"]["unresolved_count"] >= 1
assert "不是全部完成" in payload["answer_to_owner"]
assert inventory["rag.pixelrag.visual_evidence"]["status"] == "integrated_phase1_visual_receipts"
assert inventory["rag.qwen3_vl_embedding"]["status"] == "deferred_until_ollama_first_verified"
assert inventory["rag.faiss_visual_index"]["status"] == "rejected_for_production_without_adr"
assert inventory["mcp.gemini_grounding"]["status"] == "disabled_by_default_fallback_only"
assert payload["controlled_apply"]["db_write"] is False
assert payload["controlled_apply"]["network_call"] is False
def test_external_mcp_rag_readback_can_filter_family():
from services.external_mcp_rag_integration_service import (
build_external_mcp_rag_integration_readback,
)
payload = build_external_mcp_rag_integration_readback(target_family="external_rag")
assert payload["completion"]["total_capabilities"] == 3
assert {item["family"] for item in payload["inventory"]} == {"external_rag"}
def test_external_mcp_rag_cli_outputs_machine_readable_json():
completed = subprocess.run(
[
sys.executable,
"scripts/ops/report_external_mcp_rag_integration.py",
"--family",
"external_rag",
],
capture_output=True,
check=False,
text=True,
)
assert completed.returncode == 0
payload = json.loads(completed.stdout)
assert payload["success"] is True
assert payload["completion"]["total_capabilities"] == 3
assert payload["inventory"][0]["family"] == "external_rag"
def test_external_mcp_rag_ai_automation_route_returns_readback():
from flask import Flask
from routes import system_public_routes as routes
app = Flask(__name__)
with app.test_request_context(
"/api/ai-automation/external-mcp-rag-integration?family=external_mcp"
):
response = routes.ai_automation_external_mcp_rag_integration_api.__wrapped__()
payload = response.get_json()
assert payload["policy"] == "read_only_external_mcp_rag_integration_readback_v1"
assert payload["success"] is True
assert {item["family"] for item in payload["inventory"]} == {"external_mcp"}
assert payload["runtime"]["mcp"]["caller_count"] >= 1