""" 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") ], }