feat(governance): 新增 AI 技術雷達滾動監控
Some checks failed
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / tests (push) Successful in 1m39s
CD Pipeline / build-and-deploy (push) Successful in 4m35s
CD Pipeline / post-deploy-checks (push) Successful in 1m51s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
Some checks failed
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / tests (push) Successful in 1m39s
CD Pipeline / build-and-deploy (push) Successful in 4m35s
CD Pipeline / post-deploy-checks (push) Successful in 1m51s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
This commit is contained in:
@@ -41,6 +41,9 @@ from src.services.agent_market_governance_snapshot import (
|
||||
from src.services.ai_agent_market_radar_readback import (
|
||||
load_latest_ai_agent_market_radar_readback,
|
||||
)
|
||||
from src.services.ai_technology_radar_readback import (
|
||||
load_latest_ai_technology_radar_readback,
|
||||
)
|
||||
from src.services.agent_service import (
|
||||
AgentService,
|
||||
TaskState,
|
||||
@@ -718,6 +721,35 @@ async def get_ai_agent_market_radar_readback() -> dict[str, Any]:
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/ai-technology-radar-readback",
|
||||
response_model=dict[str, Any],
|
||||
summary="取得 AI 技術雷達與滾動更新讀回",
|
||||
description=(
|
||||
"讀取最新已提交的 AI 技術雷達 readback;"
|
||||
"此端點只呈現 AI 技術 primary sources、技術領域、審核佇列、Agent 分工與滾動更新 Gate。"
|
||||
"它不呼叫外部來源、不安裝 SDK、不呼叫付費 API、不切換模型、不送 Telegram、"
|
||||
"不改主機、不修改 production routing、不替換 OpenClaw。"
|
||||
),
|
||||
)
|
||||
async def get_ai_technology_radar_readback() -> dict[str, Any]:
|
||||
"""回傳最新 AI 技術雷達與滾動更新只讀快照。"""
|
||||
try:
|
||||
payload = await asyncio.to_thread(load_latest_ai_technology_radar_readback)
|
||||
return redact_public_lan_topology(payload)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
logger.error("ai_technology_radar_readback_invalid", error=str(exc))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="AI 技術雷達 readback 無效",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/automation-inventory-snapshot",
|
||||
response_model=dict[str, Any],
|
||||
|
||||
@@ -291,6 +291,16 @@ def _parse_source(source_type: str, body: bytes) -> dict[str, str | None]:
|
||||
"version": str(version) if version else None,
|
||||
"published_at": str(published_at) if published_at else None,
|
||||
}
|
||||
if source_type == "github_tags":
|
||||
payload = _loads_json(body)
|
||||
if isinstance(payload, list) and payload:
|
||||
first = payload[0]
|
||||
if isinstance(first, dict):
|
||||
version = first.get("name")
|
||||
return {
|
||||
"version": str(version) if version else None,
|
||||
"published_at": None,
|
||||
}
|
||||
return {"version": None, "published_at": None}
|
||||
|
||||
|
||||
|
||||
68
apps/api/src/services/ai_technology_radar_readback.py
Normal file
68
apps/api/src/services/ai_technology_radar_readback.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
AI technology radar readback.
|
||||
|
||||
Loads the committed read-only AI technology radar artifact. The radar is an
|
||||
operator decision surface only; it does not approve SDK installs, paid API
|
||||
calls, production routing, Telegram sends, host writes, or OpenClaw replacement.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from src.services.snapshot_paths import default_operations_dir
|
||||
|
||||
_DEFAULT_OPERATIONS_DIR = default_operations_dir(Path(__file__))
|
||||
_SNAPSHOT_NAME = "ai-technology-radar-readback.snapshot.json"
|
||||
|
||||
|
||||
def load_latest_ai_technology_radar_readback(
|
||||
operations_dir: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Load the committed AI technology radar readback snapshot."""
|
||||
directory = operations_dir or _DEFAULT_OPERATIONS_DIR
|
||||
snapshot_path = directory / _SNAPSHOT_NAME
|
||||
with snapshot_path.open(encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError(f"{snapshot_path}: expected JSON object")
|
||||
if payload.get("schema_version") != "ai_technology_radar_readback_v1":
|
||||
raise ValueError(f"{snapshot_path}: unexpected schema_version")
|
||||
|
||||
policy = payload.get("policy") or {}
|
||||
forbidden_true = [
|
||||
key
|
||||
for key in [
|
||||
"sdk_installation_approved",
|
||||
"paid_api_calls_approved",
|
||||
"production_routing_approved",
|
||||
"telegram_send_approved",
|
||||
"model_provider_switch_approved",
|
||||
"host_write_approved",
|
||||
"openclaw_replacement_approved",
|
||||
]
|
||||
if policy.get(key) is not False
|
||||
]
|
||||
if forbidden_true:
|
||||
raise ValueError(f"{snapshot_path}: unsafe policy flags: {forbidden_true}")
|
||||
if policy.get("read_only") is not True:
|
||||
raise ValueError(f"{snapshot_path}: read_only policy must be true")
|
||||
|
||||
serialized = json.dumps(payload, ensure_ascii=False)
|
||||
forbidden_fragments = [
|
||||
"/Users/",
|
||||
".claude/projects",
|
||||
".codex",
|
||||
"192.168.",
|
||||
"auth.json",
|
||||
"conversations",
|
||||
"sessions",
|
||||
]
|
||||
leaked = [fragment for fragment in forbidden_fragments if fragment in serialized]
|
||||
if leaked:
|
||||
raise ValueError(f"{snapshot_path}: forbidden local or raw-history fragment: {leaked}")
|
||||
|
||||
return payload
|
||||
183
apps/api/src/services/ai_technology_watch.py
Normal file
183
apps/api/src/services/ai_technology_watch.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
AI technology watch service.
|
||||
|
||||
Builds a read-only cross-domain AI technology radar from primary sources. This
|
||||
wraps the existing market-watch fetcher so the broader radar uses the same
|
||||
no-SDK, no-paid-API, no-production-change boundary as the Agent market watch.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from collections import Counter
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
FetchSource = Callable[[str, int], Any]
|
||||
|
||||
_AGENT_MARKET_WATCH_MODULE = None
|
||||
|
||||
|
||||
def run_ai_technology_watch(
|
||||
registry: dict[str, Any],
|
||||
*,
|
||||
registry_path: str,
|
||||
mode: str = "live",
|
||||
previous_report: dict[str, Any] | None = None,
|
||||
timeout_seconds: int = 12,
|
||||
fetcher: FetchSource | None = None,
|
||||
generated_at: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a broad read-only AI technology watch report."""
|
||||
run_agent_market_watch = _load_agent_market_watch_runner()
|
||||
agent_previous = _to_agent_previous_report(previous_report or {})
|
||||
raw_report = run_agent_market_watch(
|
||||
registry,
|
||||
registry_path=registry_path,
|
||||
mode=mode,
|
||||
previous_report=agent_previous,
|
||||
timeout_seconds=timeout_seconds,
|
||||
fetcher=fetcher,
|
||||
generated_at=generated_at,
|
||||
)
|
||||
registry_by_id = {
|
||||
str(item.get("candidate_id", "")).strip(): item
|
||||
for item in registry.get("candidates") or []
|
||||
if str(item.get("candidate_id", "")).strip()
|
||||
}
|
||||
technologies = [
|
||||
_technology_row(raw_candidate, registry_by_id.get(raw_candidate["candidate_id"], {}))
|
||||
for raw_candidate in raw_report.get("candidates") or []
|
||||
]
|
||||
area_counts = Counter(row["technology_area"] for row in technologies)
|
||||
priority_counts = Counter(row["evaluation_priority"] for row in technologies)
|
||||
changed = [row for row in technologies if row["changed"]]
|
||||
|
||||
return {
|
||||
"schema_version": "ai_technology_watch_report_v1",
|
||||
"generated_at": generated_at or datetime.now(timezone.utc).isoformat(),
|
||||
"mode": mode,
|
||||
"registry": {
|
||||
"path": registry_path,
|
||||
"schema_version": str(registry.get("schema_version", "")),
|
||||
"updated_at": str(registry.get("updated_at", "")),
|
||||
},
|
||||
"cadence": dict(registry.get("cadence") or {}),
|
||||
"policy": _policy(registry),
|
||||
"summary": {
|
||||
"technology_count": len(technologies),
|
||||
"technology_area_count": len(area_counts),
|
||||
"source_count": raw_report["summary"]["source_count"],
|
||||
"changed_technologies": len(changed),
|
||||
"watch_only_technologies": len(technologies) - len(changed),
|
||||
"review_queue_count": len(changed),
|
||||
"source_failure_count": raw_report["summary"]["failure_count"],
|
||||
"high_priority_count": priority_counts.get("p0", 0)
|
||||
+ priority_counts.get("p1", 0),
|
||||
},
|
||||
"technology_area_counts": dict(sorted(area_counts.items())),
|
||||
"technologies": technologies,
|
||||
"review_queue": [_review_queue_item(row) for row in changed],
|
||||
"new_technology_discovery": raw_report.get("new_candidate_discovery") or [],
|
||||
"failures": raw_report.get("failures") or [],
|
||||
}
|
||||
|
||||
|
||||
def _load_agent_market_watch_runner() -> Any:
|
||||
global _AGENT_MARKET_WATCH_MODULE
|
||||
if _AGENT_MARKET_WATCH_MODULE is not None:
|
||||
return _AGENT_MARKET_WATCH_MODULE.run_agent_market_watch
|
||||
|
||||
module_name = "awoooi_agent_market_watch_service_for_ai_technology"
|
||||
service_path = Path(__file__).with_name("agent_market_watch.py")
|
||||
spec = importlib.util.spec_from_file_location(module_name, service_path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"cannot load agent market watch service from {service_path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
_AGENT_MARKET_WATCH_MODULE = module
|
||||
return module.run_agent_market_watch
|
||||
|
||||
|
||||
def _policy(registry: dict[str, Any]) -> dict[str, Any]:
|
||||
policy = dict(registry.get("policy") or {})
|
||||
policy.setdefault("read_only", True)
|
||||
policy.setdefault("sdk_installation_approved", False)
|
||||
policy.setdefault("paid_api_calls_approved", False)
|
||||
policy.setdefault("production_routing_approved", False)
|
||||
policy.setdefault("workflow_modification_approved", False)
|
||||
policy.setdefault("telegram_send_approved", False)
|
||||
policy.setdefault("model_provider_switch_approved", False)
|
||||
policy.setdefault("host_write_approved", False)
|
||||
return policy
|
||||
|
||||
|
||||
def _technology_row(raw_candidate: dict[str, Any], registry_candidate: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"technology_id": raw_candidate["candidate_id"],
|
||||
"display_name": raw_candidate["display_name"],
|
||||
"technology_area": str(registry_candidate.get("technology_area") or "uncategorized"),
|
||||
"integration_surface": str(registry_candidate.get("integration_surface") or "watch_only"),
|
||||
"awoooi_role": str(registry_candidate.get("awoooi_role") or raw_candidate.get("recommended_role") or ""),
|
||||
"evaluation_priority": raw_candidate.get("evaluation_priority") or "watch",
|
||||
"requires_cost_approval": bool(raw_candidate.get("requires_cost_approval")),
|
||||
"requires_dependency_approval": bool(raw_candidate.get("requires_dependency_approval")),
|
||||
"requires_security_review": bool(registry_candidate.get("requires_security_review", True)),
|
||||
"sources": raw_candidate.get("sources") or [],
|
||||
"changed": bool(raw_candidate.get("changed")),
|
||||
"decision": raw_candidate.get("decision"),
|
||||
"recommended_actions": _recommended_actions(raw_candidate, registry_candidate),
|
||||
}
|
||||
|
||||
|
||||
def _recommended_actions(
|
||||
raw_candidate: dict[str, Any],
|
||||
registry_candidate: dict[str, Any],
|
||||
) -> list[str]:
|
||||
if raw_candidate.get("changed"):
|
||||
return [
|
||||
"refresh_ai_technology_scorecard",
|
||||
"classify_business_applicability",
|
||||
"prepare_no_install_integration_note",
|
||||
"route_high_risk_items_to_human_review",
|
||||
]
|
||||
if any(source.get("error") for source in raw_candidate.get("sources") or []):
|
||||
return ["retry_primary_source_fetch", "keep_current_runtime_status"]
|
||||
actions = list(registry_candidate.get("steady_state_actions") or [])
|
||||
return actions or ["keep_watch_only_status"]
|
||||
|
||||
|
||||
def _review_queue_item(row: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"technology_id": row["technology_id"],
|
||||
"technology_area": row["technology_area"],
|
||||
"reason": "primary_source_version_or_content_changed",
|
||||
"required_next_gate": "scorecard_then_sandbox_or_replay_plan",
|
||||
"requires_cost_approval": row["requires_cost_approval"],
|
||||
"requires_dependency_approval": row["requires_dependency_approval"],
|
||||
"requires_security_review": row["requires_security_review"],
|
||||
}
|
||||
|
||||
|
||||
def _to_agent_previous_report(report: dict[str, Any]) -> dict[str, Any] | None:
|
||||
if not report:
|
||||
return None
|
||||
if report.get("schema_version") == "agent_market_watch_report_v1":
|
||||
return report
|
||||
if report.get("schema_version") != "ai_technology_watch_report_v1":
|
||||
return None
|
||||
return {
|
||||
"schema_version": "agent_market_watch_report_v1",
|
||||
"candidates": [
|
||||
{
|
||||
"candidate_id": row.get("technology_id"),
|
||||
"sources": row.get("sources") or [],
|
||||
}
|
||||
for row in report.get("technologies") or []
|
||||
if row.get("technology_id")
|
||||
],
|
||||
}
|
||||
@@ -225,6 +225,48 @@ def test_docs_hash_ignores_dynamic_script_noise():
|
||||
assert second_report["candidates"][0]["sources"][0]["changed_since_reference"] is False
|
||||
|
||||
|
||||
def test_market_watch_parses_github_tags_for_projects_without_releases():
|
||||
registry = {
|
||||
"schema_version": "agent_market_watch_sources_v1",
|
||||
"policy": {"replacement_decision_allowed": False},
|
||||
"candidates": [
|
||||
{
|
||||
"candidate_id": "pgvector",
|
||||
"display_name": "pgvector",
|
||||
"evaluation_priority": "watch",
|
||||
"recommended_role": "vector store",
|
||||
"sources": [
|
||||
{
|
||||
"source_id": "pgvector_tags",
|
||||
"type": "github_tags",
|
||||
"url": "https://api.github.com/repos/pgvector/pgvector/tags",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
def fetcher(_url: str, _timeout: int) -> FetchedSource:
|
||||
return FetchedSource(
|
||||
status="ok",
|
||||
http_status=200,
|
||||
body=json.dumps([{"name": "v0.8.0"}]).encode(),
|
||||
)
|
||||
|
||||
report = run_agent_market_watch(
|
||||
registry,
|
||||
registry_path="registry.json",
|
||||
mode="live",
|
||||
fetcher=fetcher,
|
||||
generated_at="2026-06-25T00:00:00+00:00",
|
||||
)
|
||||
|
||||
source = report["candidates"][0]["sources"][0]
|
||||
assert source["status"] == "ok"
|
||||
assert source["version"] == "v0.8.0"
|
||||
assert report["summary"]["failure_count"] == 0
|
||||
|
||||
|
||||
def test_versioned_source_ignores_metadata_hash_noise_when_version_is_unchanged():
|
||||
registry = {
|
||||
"schema_version": "agent_market_watch_sources_v1",
|
||||
|
||||
61
apps/api/tests/test_ai_technology_radar_readback.py
Normal file
61
apps/api/tests/test_ai_technology_radar_readback.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from src.services.ai_technology_radar_readback import (
|
||||
load_latest_ai_technology_radar_readback,
|
||||
)
|
||||
|
||||
|
||||
def test_ai_technology_radar_readback_committed_snapshot_is_safe():
|
||||
payload = load_latest_ai_technology_radar_readback()
|
||||
|
||||
assert payload["schema_version"] == "ai_technology_radar_readback_v1"
|
||||
assert payload["summary"]["overall_completion_percent"] == 42.2
|
||||
assert payload["summary"]["ai_technology_radar_completion_percent"] == 100.0
|
||||
assert payload["summary"]["technology_count"] == 20
|
||||
assert payload["summary"]["technology_area_count"] == 6
|
||||
assert payload["summary"]["source_count"] == 47
|
||||
assert payload["summary"]["source_failures"] == 0
|
||||
assert payload["summary"]["high_priority_count"] == 14
|
||||
assert payload["summary"]["rolling_update_status"] == (
|
||||
"near_real_time_watch_ready_integration_gated"
|
||||
)
|
||||
assert payload["source_scope"]["gitea_main_evidence_basis_commit"] == "683428bd"
|
||||
|
||||
policy = payload["policy"]
|
||||
assert policy["read_only"] is True
|
||||
assert policy["raw_chat_history_synced"] is False
|
||||
assert policy["sdk_installation_approved"] is False
|
||||
assert policy["paid_api_calls_approved"] is False
|
||||
assert policy["production_routing_approved"] is False
|
||||
assert policy["telegram_send_approved"] is False
|
||||
assert policy["model_provider_switch_approved"] is False
|
||||
assert policy["host_write_approved"] is False
|
||||
assert policy["openclaw_replacement_approved"] is False
|
||||
|
||||
serialized = json.dumps(payload, ensure_ascii=False)
|
||||
assert "/Users/" not in serialized
|
||||
assert ".claude/projects" not in serialized
|
||||
assert ".codex" not in serialized
|
||||
assert "192.168." not in serialized
|
||||
assert "auth.json" not in serialized
|
||||
|
||||
|
||||
def test_ai_technology_radar_readback_contains_roles_and_cadence():
|
||||
payload = load_latest_ai_technology_radar_readback()
|
||||
|
||||
roles = {row["agent"]: row for row in payload["professional_agent_roles"]}
|
||||
assert "OpenClaw" in roles
|
||||
assert "NemoTron" in roles
|
||||
assert "Hermes" in roles
|
||||
assert "MarketRadar" in roles
|
||||
assert "生產路由" in roles["OpenClaw"]["review_boundary"]
|
||||
assert "離線回放評估者" in roles["NemoTron"]["professional_role"]
|
||||
|
||||
controls = {row["cadence"]: row for row in payload["rolling_update_controls"]}
|
||||
assert "每 6 小時" in controls
|
||||
assert controls["每 6 小時"]["gate"] == "read_only_only"
|
||||
assert payload["report_contract"]["api_endpoint"] == (
|
||||
"/api/v1/agents/ai-technology-radar-readback"
|
||||
)
|
||||
35
apps/api/tests/test_ai_technology_radar_readback_api.py
Normal file
35
apps/api/tests/test_ai_technology_radar_readback_api.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.api.v1.agents import router
|
||||
|
||||
|
||||
def test_ai_technology_radar_readback_endpoint_returns_committed_snapshot():
|
||||
app = FastAPI()
|
||||
app.include_router(router, prefix="/api/v1")
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/api/v1/agents/ai-technology-radar-readback")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["schema_version"] == "ai_technology_radar_readback_v1"
|
||||
assert data["summary"]["overall_completion_percent"] == 42.2
|
||||
assert data["summary"]["ai_technology_radar_completion_percent"] == 100.0
|
||||
assert data["summary"]["technology_count"] == 20
|
||||
assert data["summary"]["source_count"] == 47
|
||||
assert data["summary"]["source_failures"] == 0
|
||||
assert data["policy"]["read_only"] is True
|
||||
assert data["policy"]["sdk_installation_approved"] is False
|
||||
assert data["policy"]["telegram_send_approved"] is False
|
||||
assert data["policy"]["openclaw_replacement_approved"] is False
|
||||
|
||||
serialized = json.dumps(data, ensure_ascii=False)
|
||||
assert "/Users/" not in serialized
|
||||
assert ".claude/projects" not in serialized
|
||||
assert ".codex" not in serialized
|
||||
assert "192.168." not in serialized
|
||||
148
apps/api/tests/test_ai_technology_watch.py
Normal file
148
apps/api/tests/test_ai_technology_watch.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from src.services.agent_market_watch import FetchedSource
|
||||
from src.services.ai_technology_watch import run_ai_technology_watch
|
||||
|
||||
|
||||
def test_ai_technology_watch_groups_areas_and_blocks_integration():
|
||||
registry = {
|
||||
"schema_version": "ai_technology_watch_sources_v1",
|
||||
"updated_at": "2026-06-25",
|
||||
"cadence": {
|
||||
"near_real_time_watch": "每 6 小時",
|
||||
"daily_triage": "每日",
|
||||
"weekly_scorecard": "每週",
|
||||
"monthly_strategy_review": "每月",
|
||||
},
|
||||
"policy": {
|
||||
"read_only": True,
|
||||
"sdk_installation_approved": False,
|
||||
"paid_api_calls_approved": False,
|
||||
"production_routing_approved": False,
|
||||
"telegram_send_approved": False,
|
||||
"model_provider_switch_approved": False,
|
||||
"host_write_approved": False,
|
||||
},
|
||||
"candidates": [
|
||||
{
|
||||
"candidate_id": "langgraph_runtime",
|
||||
"display_name": "LangGraph",
|
||||
"technology_area": "agent_frameworks",
|
||||
"integration_surface": "durable_workflow_human_in_loop",
|
||||
"awoooi_role": "事件工作流候選",
|
||||
"evaluation_priority": "p0",
|
||||
"requires_cost_approval": False,
|
||||
"requires_dependency_approval": True,
|
||||
"requires_security_review": True,
|
||||
"sources": [
|
||||
{
|
||||
"source_id": "langgraph_pypi",
|
||||
"type": "pypi",
|
||||
"url": "https://pypi.org/pypi/langgraph/json",
|
||||
"reference_version": "1.0.0",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"candidate_id": "ragas_eval",
|
||||
"display_name": "Ragas",
|
||||
"technology_area": "evaluation_and_observability",
|
||||
"integration_surface": "rag_evaluation",
|
||||
"awoooi_role": "RAG 品質評測候選",
|
||||
"evaluation_priority": "p1",
|
||||
"requires_cost_approval": False,
|
||||
"requires_dependency_approval": True,
|
||||
"requires_security_review": True,
|
||||
"sources": [
|
||||
{
|
||||
"source_id": "ragas_pypi",
|
||||
"type": "pypi",
|
||||
"url": "https://pypi.org/pypi/ragas/json",
|
||||
"reference_version": "0.1.0",
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
def fetcher(url: str, _timeout: int) -> FetchedSource:
|
||||
version = "1.1.0" if "langgraph" in url else "0.1.0"
|
||||
payload = {
|
||||
"info": {"version": version},
|
||||
"releases": {
|
||||
version: [{"upload_time_iso_8601": "2026-06-25T01:02:03Z"}]
|
||||
},
|
||||
}
|
||||
return FetchedSource(status="ok", http_status=200, body=json.dumps(payload).encode())
|
||||
|
||||
report = run_ai_technology_watch(
|
||||
registry,
|
||||
registry_path="docs/ai/ai-technology-watch-sources.v1.json",
|
||||
mode="live",
|
||||
fetcher=fetcher,
|
||||
generated_at="2026-06-25T00:00:00+00:00",
|
||||
)
|
||||
|
||||
assert report["schema_version"] == "ai_technology_watch_report_v1"
|
||||
assert report["summary"]["technology_count"] == 2
|
||||
assert report["summary"]["technology_area_count"] == 2
|
||||
assert report["summary"]["changed_technologies"] == 1
|
||||
assert report["summary"]["review_queue_count"] == 1
|
||||
assert report["summary"]["high_priority_count"] == 2
|
||||
assert report["technology_area_counts"]["agent_frameworks"] == 1
|
||||
assert report["technology_area_counts"]["evaluation_and_observability"] == 1
|
||||
assert report["policy"]["read_only"] is True
|
||||
assert report["policy"]["sdk_installation_approved"] is False
|
||||
assert report["policy"]["paid_api_calls_approved"] is False
|
||||
assert report["policy"]["production_routing_approved"] is False
|
||||
assert report["policy"]["telegram_send_approved"] is False
|
||||
assert report["review_queue"][0]["technology_id"] == "langgraph_runtime"
|
||||
|
||||
|
||||
def test_ai_technology_watch_reuses_previous_report_for_change_detection():
|
||||
registry = {
|
||||
"schema_version": "ai_technology_watch_sources_v1",
|
||||
"updated_at": "2026-06-25",
|
||||
"policy": {"read_only": True},
|
||||
"candidates": [
|
||||
{
|
||||
"candidate_id": "mcp_sdk",
|
||||
"display_name": "MCP SDK",
|
||||
"technology_area": "mcp_and_a2a",
|
||||
"evaluation_priority": "p0",
|
||||
"sources": [
|
||||
{
|
||||
"source_id": "mcp_npm",
|
||||
"type": "npm",
|
||||
"url": "https://registry.npmjs.org/%40modelcontextprotocol%2Fsdk",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
def fetcher(_url: str, _timeout: int) -> FetchedSource:
|
||||
payload = {"dist-tags": {"latest": "1.2.3"}, "time": {"1.2.3": "2026-06-25"}}
|
||||
return FetchedSource(status="ok", http_status=200, body=json.dumps(payload).encode())
|
||||
|
||||
first_report = run_ai_technology_watch(
|
||||
registry,
|
||||
registry_path="registry.json",
|
||||
mode="live",
|
||||
fetcher=fetcher,
|
||||
generated_at="2026-06-25T00:00:00+00:00",
|
||||
)
|
||||
second_report = run_ai_technology_watch(
|
||||
registry,
|
||||
registry_path="registry.json",
|
||||
mode="live",
|
||||
previous_report=first_report,
|
||||
fetcher=fetcher,
|
||||
generated_at="2026-06-25T01:00:00+00:00",
|
||||
)
|
||||
|
||||
assert first_report["summary"]["changed_technologies"] == 0
|
||||
assert second_report["summary"]["changed_technologies"] == 0
|
||||
assert second_report["technologies"][0]["sources"][0]["version"] == "1.2.3"
|
||||
Reference in New Issue
Block a user