"""AI routing and provider governance aggregation service.""" from __future__ import annotations import json import os from datetime import datetime, timedelta from decimal import Decimal from typing import Any from sqlalchemy import and_, select from sqlalchemy.ext.asyncio import AsyncSession from ..models import AICallLog, Integration from ..services.ai_function_runtime_profile_service import AIFunctionRuntimeProfileService from ..services.external_query_service.registry import ExternalProviderRegistry from ..services.ai_routing_scene_contracts import AI_SCENE_REGISTRY, scene_contract from ..services.model.model_config import ( build_routing_config_snapshot, find_matching_rule, get_tier_defaults, load_model_config, ) from ..utils.timezone import now_cn_naive TEAM_POLICY_INTEGRATION_TYPE = "ai_team_policy" LLM_ENGINE_INTEGRATION_TYPE = "llm_engine" MODEL_ROUTING_INTEGRATION_TYPE = "model_routing" CAPABILITY_SLOT_INTEGRATION_TYPE = "ai_capability_slots" PLATFORM_RUNTIME_TENANT_ID = int(os.getenv("PMFLOW_PLATFORM_RUNTIME_TENANT_ID", "1") or "1") CAPABILITY_PROBE_EVIDENCE_TTL_DAYS = int(os.getenv("AI_CAPABILITY_PROBE_EVIDENCE_TTL_DAYS", "90") or "90") CAPABILITY_PROBE_KEYS = ( "basic_chat", "model_self_report", "structured_output", "json_schema", "tool_calling", "streaming", "long_context_32k", "long_context_128k", "vision_input", "audio_input", "private_deployment", "external_search_ready", "long_context", ) CAPABILITY_PROBE_STATUSES = {"untested", "running", "passed", "failed", "partial"} CAPABILITY_EVIDENCE_STATUSES = {"verified", "failed", "skipped", "not_probeable", "declared_only"} LIVE_EVIDENCE_STRENGTHS = {"live_probe", "smoke_probe", "provider_contract", "operator_verified"} DECLARED_EVIDENCE_STRENGTHS = {"declared_only", "manual", "inferred", "model_self_report"} DECLARED_PROBE_REQUIREMENT_FLAGS = { "structured_output": "structured_output", "json_schema": "json_schema", "tool_calling": "tool_calling", "long_context": "long_context", "vision_input": "vision", "audio_input": "audio", "private_deployment": "private_deployment", "external_search_ready": "external_search_ready", } CAPABILITY_DECLARED_FIELDS_META = "capability_declared_fields" PROVIDER_MODEL_CONTEXT_CONTRACTS = { ("dashscope", "qwen-plus"): { "context_tokens": 1_000_000, "max_input_tokens": 997_952, "source": "aliyun_model_studio_official_spec", "label": "阿里云百炼 Qwen-Plus 官方模型规格", }, } def _provider_model_context_contract(provider: Any, model: Any) -> dict[str, Any] | None: normalized_provider = str(provider or "").strip().lower() normalized_model = str(model or "").strip().lower() if not normalized_provider or not normalized_model: return None exact = PROVIDER_MODEL_CONTEXT_CONTRACTS.get((normalized_provider, normalized_model)) if exact: return dict(exact) if normalized_provider == "dashscope" and normalized_model.startswith("qwen-plus-"): return dict(PROVIDER_MODEL_CONTEXT_CONTRACTS[("dashscope", "qwen-plus")]) return None def _explicit_declared_capability_fields(source: dict[str, Any]) -> set[str]: raw_fields = source.get(CAPABILITY_DECLARED_FIELDS_META) allowed = set(DECLARED_PROBE_REQUIREMENT_FLAGS.values()) if isinstance(raw_fields, list): return {str(item) for item in raw_fields if str(item) in allowed} return {field for field in allowed if field in source} def _probe_status_declared_scope(declared: dict[str, Any], explicit_fields: set[str]) -> dict[str, Any]: if not explicit_fields: return {} scoped = dict(declared) for declared_flag in DECLARED_PROBE_REQUIREMENT_FLAGS.values(): if declared_flag not in explicit_fields: scoped[declared_flag] = False return scoped PROVIDER_LABELS = { "dashscope": "Qwen / DashScope", "openai": "OpenAI 兼容网关", "deepseek": "DeepSeek", "ollama": "本地 Ollama", } SLOT_LABELS = { "low": "快速槽", "high": "深度槽", "structured": "结构化槽", "long_context": "长上下文槽", "tool": "工具槽", "external_query": "外部查询槽", } OBSERVABLE_RUNTIME_SLOTS = ("low", "high", "structured", "long_context", "tool", "external_query") SLOT_RECOMMENDATION_FAILURE_RATE_THRESHOLD = float(os.getenv("AI_SLOT_RECOMMENDATION_FAILURE_RATE_THRESHOLD", "0.3") or "0.3") SLOT_RECOMMENDATION_FALLBACK_RATE_THRESHOLD = float(os.getenv("AI_SLOT_RECOMMENDATION_FALLBACK_RATE_THRESHOLD", "0.3") or "0.3") SLOT_RECOMMENDATION_DEGRADED_RATE_THRESHOLD = float(os.getenv("AI_SLOT_RECOMMENDATION_DEGRADED_RATE_THRESHOLD", "0.2") or "0.2") SLOT_RECOMMENDATION_MIN_CALLS = int(os.getenv("AI_SLOT_RECOMMENDATION_MIN_CALLS", "3") or "3") LOW_SLOT_LATENCY_WARNING_MS = int(os.getenv("AI_LOW_SLOT_LATENCY_WARNING_MS", "3000") or "3000") LOW_SLOT_COST_WARNING = float(os.getenv("AI_LOW_SLOT_COST_WARNING", "1.0") or "1.0") SLOT_SPECS = { "low": { "label": "快速槽", "summary": "轻量理解、润色、低风险草案与快速分流;失败后可升级到深度槽。", "configurable": True, "protected": False, "default_failure_policy": "upgrade_to_high", }, "high": { "label": "深度槽", "summary": "结构化、复杂推理、人员匹配和保护场景;默认不允许客户空间策略降级。", "configurable": True, "protected": True, "default_failure_policy": "explicit_fail", }, "structured": { "label": "结构化槽", "summary": "面向 JSON/DSL/抽取类输出,发布后由结构化生成 runtime 消费。", "configurable": True, "protected": True, "default_failure_policy": "explicit_fail", }, "long_context": { "label": "长上下文槽", "summary": "面向长文档、长线程和跨项目上下文,发布后仅由显式长上下文调用消费。", "configurable": True, "protected": True, "default_failure_policy": "try_fallback_then_fail", }, "tool": { "label": "工具槽", "summary": "面向工具调用和 MCP 执行编排,发布后仅由显式工具规划调用消费。", "configurable": True, "protected": True, "default_failure_policy": "explicit_fail", }, "external_query": { "label": "外部查询槽", "summary": "正式外部查询 runtime 槽;绑定 external provider registry / MCP adapter / bounded live search fallback 的受控执行链,只在 route_mode=external 后消费。", "configurable": True, "protected": True, "default_failure_policy": "explicit_fail", "runtime_binding": "external_provider_registry", }, "private": { "label": "私有槽", "summary": "候选槽,不接 runtime;面向私有部署或客户隔离运行时,等待租户隔离和合规门禁接入。", "configurable": False, "protected": True, "default_failure_policy": "explicit_fail", }, "multimodal": { "label": "多模态槽", "summary": "候选槽,不接 runtime;面向图片、语音、文件理解,等待多模态能力矩阵接入。", "configurable": False, "protected": True, "default_failure_policy": "try_fallback_then_fail", }, } CAPABILITY_LABELS = { "structured_output": "结构化输出", "json_schema": "JSON Schema", "tool_calling": "工具调用", "long_context": "长上下文", "vision": "视觉理解", "audio": "音频理解", "local_only": "本地运行", "private_deployment": "私有部署", "external_search_ready": "外部查询链路", } SLOT_REQUIREMENTS = { "low": { "summary": "低成本、低延迟、可用连接;不要求强结构化能力。", "required_flags": [], "forbidden_flags": [], "min_context_tokens": None, "any_flags": [], }, "high": { "summary": "复杂推理和保护场景;禁止仅本地运行时。", "required_flags": [], "forbidden_flags": ["local_only"], "min_context_tokens": None, "any_flags": [], }, "structured": { "summary": "必须同时具备结构化输出和 JSON Schema 能力。", "required_flags": ["structured_output", "json_schema"], "forbidden_flags": [], "min_context_tokens": None, "any_flags": [], }, "long_context": { "summary": "必须支持至少 32K 上下文窗口。", "required_flags": ["long_context"], "forbidden_flags": [], "min_context_tokens": 32768, "any_flags": [], }, "tool": { "summary": "必须支持工具调用或 function calling。", "required_flags": ["tool_calling"], "forbidden_flags": [], "min_context_tokens": None, "any_flags": [], }, "external_query": { "summary": "必须接入外部查询 provider registry 或等价实时检索适配器。", "required_flags": ["external_search_ready"], "forbidden_flags": [], "min_context_tokens": None, "any_flags": [], }, "private": { "summary": "必须满足私有部署或本地隔离运行要求。", "required_flags": ["private_deployment"], "forbidden_flags": [], "min_context_tokens": None, "any_flags": [], }, "multimodal": { "summary": "必须至少具备视觉或音频一种多模态理解能力。", "required_flags": [], "forbidden_flags": [], "min_context_tokens": None, "any_flags": ["vision", "audio"], }, } FAILURE_POLICY_LABELS = { "upgrade_to_high": "失败后升级深度槽", "try_fallback_then_fail": "先走备用链路,仍失败则显式失败", "explicit_fail": "显式失败,不静默降级", } PROTECTED_FUNCTIONS = { "adaptive_rag_answer_deep", "pmflow_launch_draft_v1", "pmflow_requirement_plan_draft", "pmflow_requirement_plan_round", "project_requirement_extraction", "generate_task_breakdown", } SCENE_FUNCTIONS = { scene_key: list(scene.get("function_names", [])) for scene_key, scene in AI_SCENE_REGISTRY.items() } SCENE_LABELS = { scene_key: str(scene.get("label") or scene_key) for scene_key, scene in AI_SCENE_REGISTRY.items() } SCENE_SLOT_REQUIREMENTS = { scene_key: list(scene.get("required_slots", [])) for scene_key, scene in AI_SCENE_REGISTRY.items() } SCENE_DEFAULT_SLOT = { scene_key: str(scene.get("default_slot") or "") for scene_key, scene in AI_SCENE_REGISTRY.items() } PROTECTED_SCENE_KEYS = { scene_key for scene_key, scene in AI_SCENE_REGISTRY.items() if bool(scene.get("protected")) and scene.get("lifecycle") == "active" } SCENE_FUNCTION_SLOT_ALLOWLIST = { scene_key: set(scene.get("allowed_slots", [])) for scene_key, scene in AI_SCENE_REGISTRY.items() if scene.get("allowed_slots") } DEFAULT_SCENE_POLICIES = { scene_key: str(scene.get("summary") or "") for scene_key, scene in AI_SCENE_REGISTRY.items() } def _safe_dict(value: Any) -> dict[str, Any]: if isinstance(value, dict): try: return json.loads(json.dumps(value)) except Exception: return dict(value) if isinstance(value, str): try: parsed = json.loads(value) return parsed if isinstance(parsed, dict) else {} except Exception: return {} return {} def _mask_secret(secret: str) -> str: value = str(secret or "").strip() if not value: return "" if len(value) <= 8: return "*" * len(value) return f"{value[:4]}...{value[-4:]}" def _normalize_tier(value: Any) -> str: text = str(value or "").strip().lower() return text if text in {"low", "high"} else "" def _iso(value: Any) -> str | None: if isinstance(value, datetime): return value.isoformat() text = str(value or "").strip() return text or None def _json_clone(value: Any) -> Any: try: return json.loads(json.dumps(value, ensure_ascii=False)) except Exception: return value def _release_version() -> str: return f"slot-v{now_cn_naive().strftime('%Y%m%d%H%M%S')}" def _empty_capability_probe(source: str = "inferred") -> dict[str, Any]: normalized_source = str(source or "inferred").strip().lower() if normalized_source not in {"inferred", "manual", "probed"}: normalized_source = "inferred" return { "status": "untested", "last_verified_at": None, "verified_capabilities": {key: False for key in CAPABILITY_PROBE_KEYS}, "evidence": {}, "source": normalized_source, } def _evidence_strength_from_row(row: dict[str, Any], *, key: str) -> str: strength = str(row.get("evidence_strength") or "").strip().lower() if strength: return strength source = str(row.get("source") or "").strip().lower() status = str(row.get("status") or "").strip().lower() message = str(row.get("message") or row.get("summary") or "").strip() if source in {"manual", "default", "inferred"}: return source if key in {"long_context", "long_context_32k", "long_context_128k"} and ("声明" in message or "declared" in message.lower()): return "declared_only" if status == "verified" and source in {"probe", "probed"}: return "live_probe" return "unknown" def _probe_row_is_verified(row: dict[str, Any], *, key: str) -> bool: if not isinstance(row, dict): return False status = str(row.get("status") or "").strip().lower() ok = row.get("ok") if ok is not True and status != "verified": return False return _evidence_strength_from_row(row, key=key) in LIVE_EVIDENCE_STRENGTHS def _derive_effective_capabilities(*, declared: dict[str, Any], probe: dict[str, Any]) -> dict[str, bool]: evidence = probe.get("evidence") if isinstance(probe.get("evidence"), dict) else {} def _row_for(key: str) -> dict[str, Any] | None: row = evidence.get(key) if isinstance(row, dict): return row flat = declared.get("capability_evidence") if isinstance(declared.get("capability_evidence"), dict) else {} flat_row = flat.get(key) return flat_row if isinstance(flat_row, dict) else None def _verified(key: str) -> bool: row = _row_for(key) if isinstance(row, dict) and _probe_row_is_verified(row, key=key): return True return False def _verified_min_tokens(key: str, min_tokens: int) -> bool: row = _row_for(key) if not isinstance(row, dict) or not _probe_row_is_verified(row, key=key): return False try: tested_tokens = int(row.get("tested_tokens") or row.get("max_context_tokens") or 0) except (TypeError, ValueError): tested_tokens = 0 return tested_tokens >= min_tokens long_context_verified = _verified("long_context") or _verified("long_context_32k") or _verified("long_context_128k") long_32k_verified = _verified_min_tokens("long_context", 32768) or _verified_min_tokens("long_context_32k", 32768) long_128k_verified = _verified_min_tokens("long_context", 128000) or _verified_min_tokens("long_context_128k", 128000) return { "basic_chat": _verified("basic_chat"), "structured_output": _verified("structured_output"), "json_schema": _verified("json_schema"), "tool_calling": _verified("tool_calling"), "streaming": _verified("streaming"), "long_context": long_context_verified, "long_context_32k": long_32k_verified, "long_context_128k": long_128k_verified, "vision_input": _verified("vision_input"), "audio_input": _verified("audio_input"), "private_deployment": _verified("private_deployment"), "external_search_ready": _verified("external_search_ready"), "vision": _verified("vision_input"), "audio": _verified("audio_input"), } def _capability_evidence_is_in_scope(key: str, row: dict[str, Any], *, declared: dict[str, Any]) -> bool: normalized_key = str(key or "").strip() if normalized_key == "model_self_report": return False if normalized_key == "basic_chat": return True if normalized_key == "structured_output": return bool(declared.get("structured_output")) if normalized_key == "json_schema": return bool(declared.get("json_schema")) if normalized_key == "tool_calling": return bool(declared.get("tool_calling")) if normalized_key == "streaming": return _probe_row_is_verified(row, key=normalized_key) if normalized_key == "long_context": return bool(declared.get("long_context")) or _probe_row_is_verified(row, key=normalized_key) if normalized_key in {"long_context_32k", "long_context_128k"}: try: declared_tokens = int(declared.get("max_context_tokens") or 0) except (TypeError, ValueError): declared_tokens = 0 required_tokens = 128000 if normalized_key == "long_context_128k" else 32768 return (bool(declared.get("long_context")) and declared_tokens >= required_tokens) or _probe_row_is_verified(row, key=normalized_key) if normalized_key == "vision_input": return bool(declared.get("vision")) or _probe_row_is_verified(row, key=normalized_key) if normalized_key == "audio_input": return bool(declared.get("audio")) or _probe_row_is_verified(row, key=normalized_key) return True def _summarize_capability_probe_status( *, declared: dict[str, Any], probe: dict[str, Any], missing_scope_declared: dict[str, Any] | None = None, ) -> str: evidence = probe.get("evidence") if isinstance(probe.get("evidence"), dict) else {} long_context_row = evidence.get("long_context") if isinstance(evidence.get("long_context"), dict) else None covered_long_context_thresholds: set[str] = set() if isinstance(long_context_row, dict) and _probe_row_is_verified(long_context_row, key="long_context"): try: verified_tokens = int(long_context_row.get("tested_tokens") or long_context_row.get("max_context_tokens") or 0) except (TypeError, ValueError): verified_tokens = 0 if verified_tokens >= 32768: covered_long_context_thresholds.add("long_context_32k") if verified_tokens >= 128000: covered_long_context_thresholds.add("long_context_128k") scoped = { str(key): value for key, value in evidence.items() if isinstance(value, dict) and str(key) not in covered_long_context_thresholds and _capability_evidence_is_in_scope(str(key), value, declared=declared) } placeholder_scope = missing_scope_declared if isinstance(missing_scope_declared, dict) else declared for key, declared_flag in DECLARED_PROBE_REQUIREMENT_FLAGS.items(): if bool(placeholder_scope.get(declared_flag)) and key not in scoped: scoped[key] = { "status": "declared_only", "ok": None, "evidence_strength": "declared_only", "summary": "已声明但尚未获得真实探测或人工确认依据", } if not scoped: return "untested" verified = sum(1 for key, value in scoped.items() if _probe_row_is_verified(value, key=key)) failed = sum( 1 for value in scoped.values() if str(value.get("status") or "").strip().lower() == "failed" or (value.get("ok") is False and str(value.get("status") or "").strip().lower() not in {"skipped", "not_probeable", "declared_only"}) ) neutral = len(scoped) - verified - failed if verified and not failed and not neutral: return "passed" if verified and (failed or neutral): return "partial" if failed: return "failed" return "untested" def _normalize_legacy_long_context_probe(*, declared: dict[str, Any], probe: dict[str, Any], flat_evidence: dict[str, Any]) -> None: if not bool(declared.get("long_context")): return try: declared_tokens = int(declared.get("max_context_tokens") or 0) except (TypeError, ValueError): declared_tokens = 0 if declared_tokens <= 0: return evidence = probe.get("evidence") if isinstance(probe.get("evidence"), dict) else {} row = evidence.get("long_context") if not isinstance(row, dict): return status = str(row.get("status") or "").strip().lower() failure_code = str(row.get("failure_code") or "").strip() text = " ".join(str(row.get(key) or "") for key in ("summary", "error_summary", "request_summary", "response_summary")) if status != "failed" or failure_code != "declared_window_too_small" or "32768" not in text: return message = f"长上下文真实 smoke 需要高成本调用,本次只记录声明窗口 {declared_tokens} tokens,不作为槽位发布 verified 证据" normalized_row = dict(row) normalized_row.update( { "ok": False, "status": "declared_only", "summary": message, "error_summary": "", "evidence_strength": "declared_only", "failure_code": None, "tested_tokens": declared_tokens, } ) evidence["long_context"] = normalized_row flat_evidence["long_context"] = { **dict(flat_evidence.get("long_context") or {}), "status": "declared_only", "source": "probe", "message": message, "evidence_strength": "declared_only", "failure_code": None, "max_context_tokens": declared_tokens, } def _probe_source_from_flat(source: Any) -> str: normalized = str(source or "").strip().lower() if normalized in {"probe", "mixed", "probed"}: return "probed" if normalized == "manual": return "manual" return "inferred" def _flat_source_from_probe(source: Any, *, fallback: str = "default") -> str: normalized = str(source or "").strip().lower() if normalized == "probed": return "probe" if normalized == "manual": return "manual" return fallback if fallback in {"default", "manual", "probe", "mixed"} else "default" def _normalize_capability_probe( raw: Any, *, flat_evidence: dict[str, Any], flat_status: Any, flat_last_verified_at: Any, flat_source: Any, ) -> dict[str, Any]: source = raw if isinstance(raw, dict) else {} probe = _empty_capability_probe(_probe_source_from_flat(flat_source)) raw_source = str(source.get("source") or "").strip().lower() if raw_source in {"inferred", "manual", "probed"}: probe["source"] = raw_source raw_status = str(source.get("status") or flat_status or "").strip().lower() if raw_status in CAPABILITY_PROBE_STATUSES: probe["status"] = raw_status raw_last_verified_at = str(source.get("last_verified_at") or flat_last_verified_at or "").strip() probe["last_verified_at"] = raw_last_verified_at or None verified_capabilities = source.get("verified_capabilities") if isinstance(source.get("verified_capabilities"), dict) else {} for key in CAPABILITY_PROBE_KEYS: probe["verified_capabilities"][key] = bool(verified_capabilities.get(key, False)) nested_evidence = source.get("evidence") if isinstance(source.get("evidence"), dict) else {} evidence_keys = list(dict.fromkeys([*CAPABILITY_PROBE_KEYS, *nested_evidence.keys(), *flat_evidence.keys()])) normalized_evidence: dict[str, dict[str, Any]] = {} for key in evidence_keys: nested_row = nested_evidence.get(key) if isinstance(nested_evidence.get(key), dict) else {} flat_row = flat_evidence.get(key) if isinstance(flat_evidence.get(key), dict) else {} if not nested_row and not flat_row: continue raw_status = str(nested_row.get("status") or flat_row.get("status") or "").strip().lower() ok_value = nested_row.get("ok") if not isinstance(ok_value, bool): ok_value = raw_status == "verified" checked_at = str(nested_row.get("checked_at") or flat_row.get("verified_at") or probe["last_verified_at"] or "").strip() or None summary = str(nested_row.get("summary") or flat_row.get("message") or "").strip() error_summary = str(nested_row.get("error_summary") or ("" if ok_value else flat_row.get("message") or "")).strip() evidence_strength = _evidence_strength_from_row({**flat_row, **nested_row, "status": raw_status}, key=str(key)) if evidence_strength == "unknown" and ok_value is True and probe.get("source") == "probed": message = str(summary or error_summary or "") evidence_strength = ( "declared_only" if str(key) in {"long_context", "long_context_32k", "long_context_128k"} and ("声明" in message or "declared" in message.lower()) else "live_probe" ) row = { "ok": bool(ok_value), "status": raw_status if raw_status in CAPABILITY_EVIDENCE_STATUSES else ("verified" if ok_value else "failed"), "method": str(nested_row.get("method") or flat_row.get("method") or "").strip() or None, "evidence_strength": evidence_strength, "checked_at": checked_at, "summary": summary[:300], "error_summary": error_summary[:300], "failure_code": str(nested_row.get("failure_code") or flat_row.get("failure_code") or "").strip() or None, "request_summary": str(nested_row.get("request_summary") or flat_row.get("request_summary") or "").strip()[:500], "response_summary": str(nested_row.get("response_summary") or flat_row.get("response_summary") or "").strip()[:500], "latency_ms": nested_row.get("latency_ms", flat_row.get("latency_ms")), "source": str(nested_row.get("source") or flat_row.get("source") or "").strip() or None, } for passthrough_key in ("source_type", "source_url", "source_note", "verified_by", "verified_at", "expires_at"): passthrough_value = nested_row.get(passthrough_key, flat_row.get(passthrough_key)) if passthrough_value not in (None, ""): row[passthrough_key] = passthrough_value if key in {"long_context", "long_context_32k", "long_context_128k"}: tested_tokens = nested_row.get("tested_tokens", flat_row.get("max_context_tokens")) try: row["tested_tokens"] = int(tested_tokens) if tested_tokens is not None and tested_tokens != "" else None except (TypeError, ValueError): row["tested_tokens"] = None model_self_report = nested_row.get("model_self_report", flat_row.get("model_self_report")) if str(key) == "model_self_report" and isinstance(model_self_report, dict): row["model_self_report"] = model_self_report normalized_evidence[key] = row probe["verified_capabilities"][key] = _probe_row_is_verified(row, key=str(key)) probe["evidence"] = normalized_evidence if probe["status"] == "untested" and normalized_evidence: ok_count = sum(1 for item in normalized_evidence.values() if item.get("ok") is True) failed_count = sum(1 for item in normalized_evidence.values() if item.get("ok") is not True) probe["status"] = "passed" if ok_count and not failed_count else ("partial" if ok_count else "failed") return probe def _flat_evidence_from_probe(probe: dict[str, Any]) -> dict[str, dict[str, Any]]: evidence = probe.get("evidence") if isinstance(probe.get("evidence"), dict) else {} flattened: dict[str, dict[str, Any]] = {} for key, value in evidence.items(): if not isinstance(value, dict): continue flattened[str(key)] = { "status": str(value.get("status") or ("verified" if value.get("ok") is True else "failed")), "source": value.get("source") or "probe", "verified_at": value.get("checked_at"), "message": str(value.get("summary") or value.get("error_summary") or "")[:300], "latency_ms": value.get("latency_ms"), "method": value.get("method"), "evidence_strength": value.get("evidence_strength"), "failure_code": value.get("failure_code"), "request_summary": value.get("request_summary"), "response_summary": value.get("response_summary"), } if value.get("tested_tokens") is not None: flattened[str(key)]["max_context_tokens"] = value.get("tested_tokens") for passthrough_key in ("source_type", "source_url", "source_note", "verified_by", "expires_at"): if value.get(passthrough_key) not in (None, ""): flattened[str(key)][passthrough_key] = value.get(passthrough_key) if str(key) == "model_self_report" and isinstance(value.get("model_self_report"), dict): flattened[str(key)]["model_self_report"] = value.get("model_self_report") return flattened def _parse_iso_datetime(value: Any) -> datetime | None: text = str(value or "").strip() if not text: return None try: parsed = datetime.fromisoformat(text.replace("Z", "+00:00")) except ValueError: return None if parsed.tzinfo is not None: parsed = parsed.replace(tzinfo=None) return parsed def _default_engine_capabilities(provider: str, model: str = "") -> dict[str, Any]: normalized_provider = str(provider or "").strip().lower() normalized_model = str(model or "").strip().lower() is_local = normalized_provider == "ollama" is_high_reasoning = normalized_provider in {"openai", "deepseek"} or "glm-5" in normalized_model is_dashscope = normalized_provider == "dashscope" context_contract = _provider_model_context_contract(normalized_provider, normalized_model) default_context_tokens = ( int(context_contract["context_tokens"]) if context_contract else (8192 if is_local else (128000 if is_high_reasoning else 32768)) ) return { "structured_output": not is_local, "json_schema": normalized_provider in {"openai", "deepseek"} or is_dashscope, "tool_calling": normalized_provider in {"openai", "deepseek", "dashscope"}, "long_context": not is_local, "max_context_tokens": default_context_tokens, "vision": False, "audio": False, "local_only": is_local, "private_deployment": is_local, "external_search_ready": False, "avg_latency_ms": None, "cost_tier": "low" if normalized_provider in {"dashscope", "ollama"} else "medium", "reliability_tier": "local" if is_local else ("high" if is_high_reasoning else "standard"), "capability_source": "default", "capability_probe_status": "untested", "capability_last_verified_at": None, "capability_blocking_reasons": [], "capability_evidence": {}, "capability_probe": _empty_capability_probe("inferred"), "declared_capabilities": {}, "effective_capabilities": {}, } def _normalize_engine_capabilities(raw: Any, *, provider: str, model: str = "") -> dict[str, Any]: defaults = _default_engine_capabilities(provider, model) source = raw if isinstance(raw, dict) else {} def _bool(key: str) -> bool: return bool(source[key]) if key in source else bool(defaults[key]) def _int_or_none(key: str) -> int | None: value = source.get(key, defaults.get(key)) if value is None or value == "": return None try: return max(0, int(value)) except (TypeError, ValueError): return defaults.get(key) def _tier(key: str, allowed: set[str]) -> str: value = str(source.get(key) or defaults.get(key) or "").strip().lower() return value if value in allowed else str(defaults.get(key) or "") evidence = source.get("capability_evidence") if isinstance(source.get("capability_evidence"), dict) else {} blocking_reasons = source.get("capability_blocking_reasons") if isinstance(source.get("capability_blocking_reasons"), list) else [] metadata_keys = { "capability_source", "capability_probe_status", "capability_last_verified_at", "capability_blocking_reasons", "capability_evidence", "capability_probe", "declared_capabilities", "effective_capabilities", CAPABILITY_DECLARED_FIELDS_META, } has_saved_capability_fields = any(key in source for key in defaults if key not in metadata_keys) capability_source = str(source.get("capability_source") or "").strip().lower() if capability_source not in {"default", "manual", "probe", "mixed"}: capability_source = "manual" if has_saved_capability_fields else "default" probe_status = str(source.get("capability_probe_status") or "").strip().lower() if probe_status not in {"untested", "running", "passed", "partial", "failed"}: probe_status = "untested" capability_probe = _normalize_capability_probe( source.get("capability_probe"), flat_evidence=evidence, flat_status=probe_status, flat_last_verified_at=source.get("capability_last_verified_at"), flat_source=capability_source, ) declared_capabilities = { "structured_output": _bool("structured_output"), "json_schema": _bool("json_schema"), "tool_calling": _bool("tool_calling"), "long_context": _bool("long_context"), "max_context_tokens": _int_or_none("max_context_tokens"), "vision": _bool("vision"), "audio": _bool("audio"), "local_only": _bool("local_only"), "private_deployment": _bool("private_deployment"), "external_search_ready": _bool("external_search_ready"), "avg_latency_ms": _int_or_none("avg_latency_ms"), "cost_tier": _tier("cost_tier", {"low", "medium", "high", "unknown"}), "reliability_tier": _tier("reliability_tier", {"standard", "high", "local", "unknown"}), } explicit_declared_fields = _explicit_declared_capability_fields(source) _normalize_legacy_long_context_probe(declared=declared_capabilities, probe=capability_probe, flat_evidence=evidence) if not evidence and capability_probe.get("evidence"): evidence = _flat_evidence_from_probe(capability_probe) if not blocking_reasons: blocking_reasons = [ f"{key}: {str(value.get('error_summary') or '')[:180]}" for key, value in (capability_probe.get("evidence") or {}).items() if ( isinstance(value, dict) and _capability_evidence_is_in_scope(str(key), value, declared=declared_capabilities) and value.get("ok") is not True and str(value.get("error_summary") or "").strip() ) ] missing_scope_declared = _probe_status_declared_scope(declared_capabilities, explicit_declared_fields) scoped_probe_status = _summarize_capability_probe_status( declared=declared_capabilities, probe=capability_probe, missing_scope_declared=missing_scope_declared, ) if scoped_probe_status != "untested" or probe_status == "untested": probe_status = scoped_probe_status capability_probe["status"] = probe_status last_verified_at = str(source.get("capability_last_verified_at") or capability_probe.get("last_verified_at") or "").strip() or None if capability_source == "default" and capability_probe.get("source") in {"manual", "probed"}: capability_source = _flat_source_from_probe(capability_probe.get("source"), fallback=capability_source) normalized = { **declared_capabilities, "capability_source": capability_source, "capability_probe_status": probe_status, "capability_last_verified_at": last_verified_at, "capability_blocking_reasons": [str(item)[:220] for item in blocking_reasons if str(item or "").strip()][:8], "capability_evidence": { str(key): value for key, value in evidence.items() if str(key or "").strip() and isinstance(value, dict) }, "capability_probe": capability_probe, "declared_capabilities": { "structured_output": declared_capabilities["structured_output"], "json_schema": declared_capabilities["json_schema"], "tool_calling": declared_capabilities["tool_calling"], "long_context": declared_capabilities["long_context"], "max_context_tokens": declared_capabilities["max_context_tokens"], "vision": declared_capabilities["vision"], "audio": declared_capabilities["audio"], "private_deployment": declared_capabilities["private_deployment"], "external_search_ready": declared_capabilities["external_search_ready"], }, CAPABILITY_DECLARED_FIELDS_META: sorted(explicit_declared_fields), } normalized["effective_capabilities"] = _derive_effective_capabilities(declared=normalized, probe=capability_probe) return normalized class AIRoutingGovernanceService: def __init__(self, db: AsyncSession, *, tenant_id: int): self.db = db self.tenant_id = int(tenant_id) async def get_overview(self) -> dict[str, Any]: bundle = load_model_config() engine_bundle = await self._load_integration_config(LLM_ENGINE_INTEGRATION_TYPE) routing_bundle = await self._load_integration_config(MODEL_ROUTING_INTEGRATION_TYPE) slot_bundle = self._normalize_capability_slot_bundle( await self._load_integration_config(CAPABILITY_SLOT_INTEGRATION_TYPE) ) capability_slots = self._build_capability_slots( engine_bundle=engine_bundle, slot_bundle=slot_bundle, config_bundle=bundle, stage="active", ) slots = [slot for slot in capability_slots if slot.get("slot_key") in {"low", "high"}] metrics = await self._build_metrics() diagnostics = await self._build_diagnostics( engine_bundle=engine_bundle, routing_bundle=routing_bundle, slots=slots, metrics=metrics, ) snapshot = build_routing_config_snapshot(bundle=bundle) return { "config_version": snapshot.get("routing_config_version") or "", "config_updated_at": snapshot.get("routing_config_updated_at") or "", "source_of_truth": "PMFlow 本地路由层 baseline", "slots": slots, "capability_slots": capability_slots, "metrics": metrics, "diagnostics": diagnostics, "latency_status": "暂未采集平均耗时", } async def get_admin_console(self) -> dict[str, Any]: overview = await self.get_overview() engine_bundle = await self._load_integration_config(LLM_ENGINE_INTEGRATION_TYPE) routing_bundle = await self._load_integration_config(MODEL_ROUTING_INTEGRATION_TYPE) config_bundle = load_model_config() capability_slot_release = await self.get_capability_slots() function_runtime_profiles = await AIFunctionRuntimeProfileService(self.db).get_profiles() function_observability = await self._function_runtime_observability(hours=24 * 7) scene_routes = self._build_scene_routes( routing_bundle=routing_bundle, config_bundle=config_bundle, function_runtime_profiles=function_runtime_profiles, function_observability=function_observability, ) scene_failure_rates = await self._scene_failure_rates(hours=24 * 7) scene_slot_matrix = self._build_scene_slot_matrix( scene_routes=scene_routes, capability_slot_release=capability_slot_release, scene_failure_rates=scene_failure_rates, ) slot_observability = await self._build_slot_observability( capability_slot_release=capability_slot_release, ) slot_recommendations = self._build_slot_recommendations( slots=overview.get("capability_slots", []), slot_observability=slot_observability, ) return { "overview": overview, "providers": self._build_providers(engine_bundle=engine_bundle), "capability_slots": overview.get("capability_slots", []), "capability_slot_release": capability_slot_release, "function_runtime_profiles": function_runtime_profiles, "scene_routes": scene_routes, "scene_slot_matrix": scene_slot_matrix, "slot_observability": slot_observability, "slot_recommendations": slot_recommendations, "protected_functions": sorted(PROTECTED_FUNCTIONS), "recent_failures": await self._recent_failures(limit=8), "diagnostics": overview["diagnostics"], "audit_hint": "供应商、槽位、路由和团队策略变更都会写入操作日志。", } async def get_capability_slots(self) -> dict[str, Any]: bundle = load_model_config() engine_bundle = await self._load_integration_config(LLM_ENGINE_INTEGRATION_TYPE) slot_bundle = self._normalize_capability_slot_bundle( await self._load_integration_config(CAPABILITY_SLOT_INTEGRATION_TYPE) ) active_slots = self._build_capability_slots( engine_bundle=engine_bundle, slot_bundle=slot_bundle, config_bundle=bundle, stage="active", ) draft_slots = self._build_capability_slots( engine_bundle=engine_bundle, slot_bundle=slot_bundle, config_bundle=bundle, stage="draft", ) validation = self.validate_capability_slot_bundle( engine_bundle=engine_bundle, slot_bundle=slot_bundle, stage="draft", ) return { "version": str(slot_bundle.get("active_version") or slot_bundle.get("version") or "unpublished").strip(), "active_version": str(slot_bundle.get("active_version") or "unpublished").strip(), "draft_version": str(slot_bundle.get("draft_version") or "draft").strip(), "published_at": _iso((slot_bundle.get("active") or {}).get("published_at") if isinstance(slot_bundle.get("active"), dict) else None), "published_by": (slot_bundle.get("active") or {}).get("published_by") if isinstance(slot_bundle.get("active"), dict) else None, "updated_at": _iso((slot_bundle.get("draft") or {}).get("updated_at") if isinstance(slot_bundle.get("draft"), dict) else None), "updated_by": (slot_bundle.get("draft") or {}).get("updated_by") if isinstance(slot_bundle.get("draft"), dict) else None, "dirty": self._capability_slot_dirty(slot_bundle), "validation": validation, "history": self._capability_slot_history(slot_bundle), "slots": draft_slots, "active_slots": active_slots, "draft_slots": draft_slots, } async def update_capability_slot( self, *, slot_key: str, payload: dict[str, Any], updated_by: int, ) -> dict[str, Any]: key = str(slot_key or "").strip().lower() if key not in SLOT_SPECS: raise ValueError("能力槽位不存在") if not bool((SLOT_SPECS.get(key) or {}).get("configurable", False)): raise ValueError("该能力槽当前为候选槽,不接 runtime,不能配置运行时") if key == "external_query": return await self._update_external_query_slot(payload=payload, updated_by=updated_by) engine_bundle = await self._load_integration_config(LLM_ENGINE_INTEGRATION_TYPE) engines = [item for item in engine_bundle.get("engines", []) if isinstance(item, dict)] engine_by_id = {str(item.get("id") or ""): item for item in engines if str(item.get("id") or "")} primary_engine_id = str(payload.get("primary_engine_id") or "").strip() if primary_engine_id and primary_engine_id not in engine_by_id: raise ValueError("主运行时不存在,请先在供应商与凭据中新增连接") if primary_engine_id and not bool(engine_by_id[primary_engine_id].get("enabled", True)): raise ValueError("主运行时已停用,不能绑定到能力槽") fallback_engine_ids: list[str] = [] for item in payload.get("fallback_engine_ids") or []: engine_id = str(item or "").strip() if not engine_id or engine_id == primary_engine_id or engine_id in fallback_engine_ids: continue if engine_id not in engine_by_id: raise ValueError("备用运行时不存在,请刷新后重试") if not bool(engine_by_id[engine_id].get("enabled", True)): raise ValueError("备用运行时已停用,不能绑定到能力槽") fallback_engine_ids.append(engine_id) failure_policy = str(payload.get("failure_policy") or SLOT_SPECS[key]["default_failure_policy"]).strip() if failure_policy not in FAILURE_POLICY_LABELS: raise ValueError("失败策略不支持") allow_scene_downgrade = bool(payload.get("allow_scene_downgrade", False)) if key == "high" and allow_scene_downgrade: raise ValueError("深度槽属于保护槽,不允许客户空间策略降级") integration, slot_bundle = await self._get_or_init_capability_slot_bundle() slots = self._slot_config_map(slot_bundle, stage="draft") slots[key] = { "primary_engine_id": primary_engine_id or None, "fallback_engine_ids": fallback_engine_ids, "failure_policy": failure_policy, "allow_scene_downgrade": allow_scene_downgrade, "updated_at": now_cn_naive().isoformat(), "updated_by": int(updated_by), } draft = slot_bundle.get("draft") if isinstance(slot_bundle.get("draft"), dict) else {} draft["slots"] = slots draft["updated_at"] = now_cn_naive().isoformat() draft["updated_by"] = int(updated_by) slot_bundle["draft"] = draft slot_bundle["draft_version"] = "draft" integration.config = json.loads(json.dumps(slot_bundle, ensure_ascii=False)) integration.is_active = True await self.db.flush() return await self.get_capability_slots() async def _update_external_query_slot(self, *, payload: dict[str, Any], updated_by: int) -> dict[str, Any]: registry = self._external_query_registry_status(binding=payload) provider_ids = {str(item.get("id") or "") for item in registry.get("providers", []) if str(item.get("id") or "")} primary_provider_id = str(payload.get("primary_provider_id") or payload.get("primary_engine_id") or "").strip() if primary_provider_id and primary_provider_id not in provider_ids: raise ValueError("外部查询主 provider 不存在,请刷新 provider registry 后重试") fallback_provider_ids: list[str] = [] raw_fallback_provider_ids = payload.get("fallback_provider_ids") if not isinstance(raw_fallback_provider_ids, list): raw_fallback_provider_ids = payload.get("fallback_engine_ids") for item in raw_fallback_provider_ids or []: provider_id = str(item or "").strip() if not provider_id or provider_id == primary_provider_id or provider_id in fallback_provider_ids: continue if provider_id not in provider_ids: raise ValueError("外部查询备用 provider 不存在,请刷新 provider registry 后重试") fallback_provider_ids.append(provider_id) failure_policy = str(payload.get("failure_policy") or "explicit_fail").strip() if failure_policy != "explicit_fail": raise ValueError("外部查询槽失败策略必须为显式失败,禁止回退到普通 LLM 编造答案") integration, slot_bundle = await self._get_or_init_capability_slot_bundle() slots = self._slot_config_map(slot_bundle, stage="draft") slots["external_query"] = { "primary_provider_id": primary_provider_id or None, "fallback_provider_ids": fallback_provider_ids, "verifier_required": True, "source_filtering_required": True, "failure_policy": "explicit_fail", "allow_scene_downgrade": False, "updated_at": now_cn_naive().isoformat(), "updated_by": int(updated_by), } draft = slot_bundle.get("draft") if isinstance(slot_bundle.get("draft"), dict) else {} draft["slots"] = slots draft["updated_at"] = now_cn_naive().isoformat() draft["updated_by"] = int(updated_by) slot_bundle["draft"] = draft slot_bundle["draft_version"] = "draft" integration.config = json.loads(json.dumps(slot_bundle, ensure_ascii=False)) integration.is_active = True await self.db.flush() return await self.get_capability_slots() async def validate_capability_slots(self) -> dict[str, Any]: engine_bundle = await self._load_integration_config(LLM_ENGINE_INTEGRATION_TYPE) slot_bundle = self._normalize_capability_slot_bundle( await self._load_integration_config(CAPABILITY_SLOT_INTEGRATION_TYPE) ) return self.validate_capability_slot_bundle( engine_bundle=engine_bundle, slot_bundle=slot_bundle, stage="draft", ) async def publish_capability_slots(self, *, published_by: int, summary: str | None = None) -> dict[str, Any]: engine_bundle = await self._load_integration_config(LLM_ENGINE_INTEGRATION_TYPE) integration, slot_bundle = await self._get_or_init_capability_slot_bundle() validation = self.validate_capability_slot_bundle( engine_bundle=engine_bundle, slot_bundle=slot_bundle, stage="draft", ) blocking = [item for item in validation.get("items", []) if item.get("severity") == "critical"] if blocking: raise ValueError("草稿存在阻塞项,不能发布") version = _release_version() now = now_cn_naive().isoformat() draft_slots = self._slot_config_map(slot_bundle, stage="draft") slot_bundle["active"] = { "slots": _json_clone(draft_slots), "published_at": now, "published_by": int(published_by), } slot_bundle["active_version"] = version slot_bundle["version"] = version history = self._capability_slot_history(slot_bundle) history.insert( 0, { "version": version, "published_at": now, "published_by": int(published_by), "summary": str(summary or "发布能力槽位草稿").strip()[:200], "slots": _json_clone(draft_slots), }, ) slot_bundle["history"] = history[:10] integration.config = json.loads(json.dumps(slot_bundle, ensure_ascii=False)) integration.is_active = True await self.db.flush() return await self.get_capability_slots() async def rollback_capability_slots(self, *, target_version: str | None, rolled_back_by: int) -> dict[str, Any]: integration, slot_bundle = await self._get_or_init_capability_slot_bundle() history = self._capability_slot_history(slot_bundle) current_version = str(slot_bundle.get("active_version") or "").strip() candidates = [item for item in history if str(item.get("version") or "").strip() != current_version] if target_version: target = next((item for item in history if str(item.get("version") or "").strip() == str(target_version).strip()), None) else: target = candidates[0] if candidates else None if not isinstance(target, dict) or not isinstance(target.get("slots"), dict): raise ValueError("没有可回滚的已发布版本") version = _release_version() now = now_cn_naive().isoformat() restored_slots = _json_clone(target["slots"]) slot_bundle["active"] = { "slots": restored_slots, "published_at": now, "published_by": int(rolled_back_by), "rolled_back_from": str(target.get("version") or ""), } slot_bundle["draft"] = { "slots": _json_clone(restored_slots), "updated_at": now, "updated_by": int(rolled_back_by), } slot_bundle["active_version"] = version slot_bundle["draft_version"] = "draft" slot_bundle["version"] = version history.insert( 0, { "version": version, "published_at": now, "published_by": int(rolled_back_by), "summary": f"回滚到 {target.get('version') or '上一版本'}", "slots": _json_clone(restored_slots), }, ) slot_bundle["history"] = history[:10] integration.config = json.loads(json.dumps(slot_bundle, ensure_ascii=False)) integration.is_active = True await self.db.flush() return await self.get_capability_slots() async def run_diagnostics(self) -> dict[str, Any]: overview = await self.get_overview() return { "checked_at": now_cn_naive().isoformat(), "items": overview["diagnostics"], "summary": self._diagnostic_summary(overview["diagnostics"]), } async def get_team_policy(self) -> dict[str, Any]: saved = await self._load_integration_config(TEAM_POLICY_INTEGRATION_TYPE) return self._merge_team_policy(saved) async def update_team_policy(self, *, payload: dict[str, Any], updated_by: int) -> dict[str, Any]: normalized = self._merge_team_policy(payload) normalized["updated_by"] = int(updated_by) normalized["updated_at"] = now_cn_naive().isoformat() row = await self._get_or_init_integration(TEAM_POLICY_INTEGRATION_TYPE, default_config={}) row.config = json.loads(json.dumps(normalized, ensure_ascii=False)) row.is_active = True await self.db.flush() return normalized async def _load_integration_config(self, integration_type: str) -> dict[str, Any]: tenant_id = ( PLATFORM_RUNTIME_TENANT_ID if integration_type in {LLM_ENGINE_INTEGRATION_TYPE, MODEL_ROUTING_INTEGRATION_TYPE, CAPABILITY_SLOT_INTEGRATION_TYPE} else self.tenant_id ) row = ( await self.db.execute( select(Integration).where( and_( Integration.tenant_id == tenant_id, Integration.type == integration_type, ) ) ) ).scalar_one_or_none() return _safe_dict(row.config if row else {}) async def _get_or_init_integration(self, integration_type: str, *, default_config: dict[str, Any]) -> Integration: row = ( await self.db.execute( select(Integration).where( and_( Integration.tenant_id == self.tenant_id, Integration.type == integration_type, ) ) ) ).scalar_one_or_none() if row is None: row = Integration( tenant_id=self.tenant_id, type=integration_type, config=json.loads(json.dumps(default_config, ensure_ascii=False)), is_active=True, ) self.db.add(row) await self.db.flush() return row async def _get_or_init_capability_slot_bundle(self) -> tuple[Integration, dict[str, Any]]: row = ( await self.db.execute( select(Integration).where( and_( Integration.tenant_id == PLATFORM_RUNTIME_TENANT_ID, Integration.type == CAPABILITY_SLOT_INTEGRATION_TYPE, ) ) ) ).scalar_one_or_none() if row is None: row = Integration( tenant_id=PLATFORM_RUNTIME_TENANT_ID, type=CAPABILITY_SLOT_INTEGRATION_TYPE, config=self._normalize_capability_slot_bundle({}), is_active=True, ) self.db.add(row) await self.db.flush() bundle = self._normalize_capability_slot_bundle(_safe_dict(row.config)) return row, bundle @staticmethod def _normalize_capability_slot_bundle(raw: dict[str, Any]) -> dict[str, Any]: bundle = _safe_dict(raw) legacy_slots = bundle.get("slots") if isinstance(bundle.get("slots"), dict) else {} active = bundle.get("active") if isinstance(bundle.get("active"), dict) else {} draft = bundle.get("draft") if isinstance(bundle.get("draft"), dict) else {} active_slots = active.get("slots") if isinstance(active.get("slots"), dict) else legacy_slots draft_slots = draft.get("slots") if isinstance(draft.get("slots"), dict) else active_slots active_version = str(bundle.get("active_version") or bundle.get("version") or "").strip() if not active_version: active_version = "unpublished" normalized = { "version": active_version, "active_version": active_version, "draft_version": str(bundle.get("draft_version") or "draft").strip() or "draft", "active": { **{key: value for key, value in active.items() if key != "slots"}, "slots": _json_clone(active_slots), }, "draft": { **{key: value for key, value in draft.items() if key != "slots"}, "slots": _json_clone(draft_slots), }, "history": bundle.get("history") if isinstance(bundle.get("history"), list) else [], } if "published_at" not in normalized["active"] and bundle.get("updated_at"): normalized["active"]["published_at"] = bundle.get("updated_at") if "updated_at" not in normalized["draft"] and bundle.get("updated_at"): normalized["draft"]["updated_at"] = bundle.get("updated_at") return normalized @staticmethod def _slot_config_map(slot_bundle: dict[str, Any], *, stage: str) -> dict[str, Any]: normalized_stage = "draft" if stage == "draft" else "active" stage_bundle = slot_bundle.get(normalized_stage) if isinstance(slot_bundle.get(normalized_stage), dict) else {} slots = stage_bundle.get("slots") if isinstance(stage_bundle.get("slots"), dict) else {} return _json_clone(slots) if isinstance(slots, dict) else {} @staticmethod def _capability_slot_dirty(slot_bundle: dict[str, Any]) -> bool: return _json_clone(AIRoutingGovernanceService._slot_config_map(slot_bundle, stage="active")) != _json_clone( AIRoutingGovernanceService._slot_config_map(slot_bundle, stage="draft") ) @staticmethod def _capability_slot_history(slot_bundle: dict[str, Any]) -> list[dict[str, Any]]: history = slot_bundle.get("history") if isinstance(slot_bundle.get("history"), list) else [] return [item for item in history if isinstance(item, dict)] def validate_capability_slot_bundle( self, *, engine_bundle: dict[str, Any], slot_bundle: dict[str, Any], stage: str, ) -> dict[str, Any]: engines = [item for item in engine_bundle.get("engines", []) if isinstance(item, dict)] engine_by_id = {str(item.get("id") or ""): item for item in engines if str(item.get("id") or "")} slots = self._slot_config_map(slot_bundle, stage=stage) items: list[dict[str, Any]] = [] enabled_reserved_slots = [ key for key in SLOT_SPECS if key not in {"low", "high"} and isinstance(slots.get(key), dict) and ( str((slots.get(key) or {}).get("primary_engine_id") or "").strip() or str((slots.get(key) or {}).get("primary_provider_id") or "").strip() or bool((slots.get(key) or {}).get("fallback_engine_ids")) or bool((slots.get(key) or {}).get("fallback_provider_ids")) ) ] for slot_key in ["low", "high", *enabled_reserved_slots]: saved = slots.get(slot_key) if isinstance(slots.get(slot_key), dict) else {} primary_engine_id = str(saved.get("primary_engine_id") or "").strip() slot_label = str((SLOT_SPECS.get(slot_key) or {}).get("label") or SLOT_LABELS.get(slot_key, slot_key)) if slot_key == "external_query": items.extend(self._external_query_slot_risks(saved=saved, slot_label=slot_label)) continue if not primary_engine_id: items.append(self._risk(f"{slot_key}_primary_missing", "critical", f"{slot_label}未选择主运行时", "必须先从供应商与凭据中选择已启用运行时。")) continue primary_engine = engine_by_id.get(primary_engine_id) if not primary_engine: items.append(self._risk(f"{slot_key}_primary_missing_provider", "critical", f"{slot_label}主运行时不存在", "请刷新供应商列表或重新选择运行时。")) continue if not bool(primary_engine.get("enabled", True)): items.append(self._risk(f"{slot_key}_primary_disabled", "critical", f"{slot_label}主运行时已停用", "发布前必须启用运行时或重新选择。")) status = str(primary_engine.get("connection_status") or "untested").strip() if status == "test_failed": items.append(self._risk(f"{slot_key}_primary_test_failed", "critical", f"{slot_label}主运行时测试失败", "发布前必须先修复供应商连接。")) elif status != "available": items.append(self._risk(f"{slot_key}_primary_untested", "warning", f"{slot_label}主运行时未完成连通测试", "建议先在供应商与凭据中测试通过再发布。")) provider = str(primary_engine.get("provider") or "").strip().lower() if slot_key == "high" and provider == "ollama": items.append(self._risk("high_slot_ollama_blocked", "critical", "深度槽不能绑定本地 Ollama", "深度槽承载结构化、复杂推理和保护场景,不能使用仅知识库问答运行时。")) items.extend( self._engine_requirement_risks( slot_key=slot_key, slot_label=slot_label, engine=primary_engine, role_label="主运行时", ) ) if slot_key == "low": primary_coverage = self._engine_capability_coverage(slot_key=slot_key, engine=primary_engine) primary_probe = (primary_coverage.get("capabilities") or {}).get("capability_probe") primary_probe_status = str((primary_probe or {}).get("status") or "untested").strip().lower() if isinstance(primary_probe, dict) else "untested" primary_probe_source = str((primary_probe or {}).get("source") or "inferred").strip().lower() if isinstance(primary_probe, dict) else "inferred" if primary_probe_status == "untested" or primary_probe_source != "probed": items.append( self._risk( "low_primary_capability_evidence_untested", "warning", "快速槽主运行时缺少 capability probe", "快速槽可先发布,但当前能力仅来自默认推断或人工标注,建议补一次供应商能力探测。", ) ) fallback_ids = saved.get("fallback_engine_ids") if isinstance(saved.get("fallback_engine_ids"), list) else [] for fallback_id_raw in fallback_ids: fallback_id = str(fallback_id_raw or "").strip() if not fallback_id: continue if fallback_id == primary_engine_id: items.append(self._risk(f"{slot_key}_fallback_duplicate_primary", "warning", f"{slot_label}备用链路重复主运行时", "重复运行时不会提供真实容灾能力。")) continue fallback_engine = engine_by_id.get(fallback_id) if not fallback_engine: items.append(self._risk(f"{slot_key}_fallback_missing", "critical", f"{slot_label}备用运行时不存在", "请移除失效备用项。")) continue if not bool(fallback_engine.get("enabled", True)): items.append(self._risk(f"{slot_key}_fallback_disabled", "critical", f"{slot_label}备用运行时已停用", "请启用备用运行时或移除该备用项。")) items.extend( self._engine_requirement_risks( slot_key=slot_key, slot_label=slot_label, engine=fallback_engine, role_label="备用运行时", ) ) if self._same_engine_identity(primary_engine, fallback_engine): items.append(self._risk(f"{slot_key}_fallback_same_runtime", "warning", f"{slot_label}备用链路与主运行时相同", "相同 provider/model/base_url 只能规避少量连接状态问题,不能提升模型能力。")) if slot_key == "high" and bool(saved.get("allow_scene_downgrade", False)): items.append(self._risk("high_downgrade_blocked", "critical", "深度槽不允许被场景策略降级", "保护场景必须显式失败或走高阶修复,不能静默降级。")) if not items: items.append(self._risk("ok", "ok", "草稿可发布", "当前草稿未触发阻塞项。")) return { "stage": stage, "can_publish": not any(item.get("severity") == "critical" for item in items), "summary": self._diagnostic_summary(items), "items": items, } def _external_query_slot_risks(self, *, saved: dict[str, Any], slot_label: str) -> list[dict[str, Any]]: registry = self._external_query_registry_status(binding=saved) providers = [item for item in registry.get("providers", []) if isinstance(item, dict)] provider_by_id = {str(item.get("id") or ""): item for item in providers if str(item.get("id") or "")} primary_provider_id = str(saved.get("primary_provider_id") or saved.get("primary_engine_id") or "").strip() fallback_provider_ids = [ str(item or "").strip() for item in list(saved.get("fallback_provider_ids") or saved.get("fallback_engine_ids") or []) if str(item or "").strip() ] risks: list[dict[str, Any]] = [] if not primary_provider_id: risks.append(self._risk("external_query_primary_provider_missing", "critical", f"{slot_label}未选择主外部 provider", "必须先绑定 MCP adapter 或 bounded live search fallback。")) return risks primary = provider_by_id.get(primary_provider_id) if not primary: risks.append(self._risk("external_query_primary_provider_unknown", "critical", f"{slot_label}主 provider 不存在", "请刷新 provider registry 后重新选择外部查询 provider。")) elif not bool(primary.get("available")): risks.append(self._risk("external_query_primary_provider_unavailable", "critical", f"{slot_label}主 provider 不可用", "发布前必须恢复 MCP adapter 或 bounded live search fallback。")) for provider_id in fallback_provider_ids: if provider_id == primary_provider_id: risks.append(self._risk("external_query_fallback_duplicate_primary", "warning", f"{slot_label}备用 provider 重复主 provider", "重复 provider 不会提供真实外部查询容灾。")) continue provider = provider_by_id.get(provider_id) if not provider: risks.append(self._risk("external_query_fallback_provider_unknown", "critical", f"{slot_label}备用 provider 不存在", "请移除失效备用 provider。")) elif not bool(provider.get("available")): risks.append(self._risk("external_query_fallback_provider_unavailable", "critical", f"{slot_label}备用 provider 不可用", "请恢复该 provider 或从 fallback 链移除。")) selected_ids = [primary_provider_id, *fallback_provider_ids] selected_available = [ provider_by_id[item] for item in selected_ids if item in provider_by_id and bool(provider_by_id[item].get("available")) ] if not selected_available: risks.append(self._risk("external_query_provider_unavailable", "critical", f"{slot_label}没有可用外部 provider", "至少需要一个可用的 MCP adapter 或 bounded live search fallback。")) if not any(bool(item.get("available")) for item in providers): risks.append(self._risk("external_query_registry_empty", "critical", "外部 provider registry 不可用", "发布前必须至少启用一个受控外部查询 provider。")) if not (bool(registry.get("mcp_adapter_available")) or bool(registry.get("live_search_fallback_available"))): risks.append(self._risk("external_query_adapter_missing", "critical", "外部查询执行链不可用", "必须启用 MCP adapter 或 bounded live search fallback 中至少一个执行链。")) verifier = registry.get("verifier") if isinstance(registry.get("verifier"), dict) else {} if not bool(saved.get("verifier_required", True)): risks.append(self._risk("external_query_verifier_not_required", "critical", "外部查询未强制 verifier", "外部查询槽必须开启 verifier_required,禁止未验证来源直接出答案。")) if not bool(verifier.get("available")): risks.append(self._risk("external_query_verifier_unavailable", "critical", "外部查询 verifier 不可用", "发布前必须恢复答案验证器。")) source_filtering = registry.get("source_filtering") if isinstance(registry.get("source_filtering"), dict) else {} if not bool(saved.get("source_filtering_required", True)): risks.append(self._risk("external_query_source_filtering_not_required", "critical", "外部查询未强制来源过滤", "外部查询槽必须开启 source_filtering_required,禁止内部来源混入外部答案。")) if not bool(source_filtering.get("fail_closed")): risks.append(self._risk("external_query_source_filtering_open", "critical", "外部来源过滤未 fail-closed", "来源过滤必须 fail-closed,无法确认公开来源时只能显式失败或 freshness guard。")) if str(saved.get("failure_policy") or "explicit_fail").strip() != "explicit_fail": risks.append(self._risk("external_query_failure_policy_invalid", "critical", "外部查询失败策略不合规", "外部查询失败必须显式失败或 freshness guard,禁止回退普通 LLM。")) return risks def _engine_requirement_risks( self, *, slot_key: str, slot_label: str, engine: dict[str, Any], role_label: str, ) -> list[dict[str, Any]]: coverage = self._engine_capability_coverage(slot_key=slot_key, engine=engine) if bool(coverage.get("satisfied", True)): return [] risks: list[dict[str, Any]] = [] missing = "、".join(str(item) for item in coverage.get("missing_labels", []) if str(item or "").strip()) missing_evidence = "、".join(str(item) for item in coverage.get("missing_evidence_labels", []) if str(item or "").strip()) stale_evidence = "、".join(str(item) for item in coverage.get("stale_evidence_labels", []) if str(item or "").strip()) if missing: risks.append( self._risk( f"{slot_key}_{role_label}_capability_missing", "critical", f"{slot_label}{role_label}缺少能力", f"缺少:{missing};请在供应商与凭据中选择符合要求的运行时,或先修正供应商能力配置。", ) ) if missing_evidence: risks.append( self._risk( f"{slot_key}_{role_label}_capability_evidence_missing", "critical", f"{slot_label}{role_label}缺少能力探测证据", f"缺少探测证据:{missing_evidence};请先对供应商执行 capability probe,再发布能力槽位。", ) ) if stale_evidence: risks.append( self._risk( f"{slot_key}_{role_label}_capability_evidence_stale", "critical", f"{slot_label}{role_label}能力探测证据已过期", f"证据过期:{stale_evidence};请重新执行 capability probe 后再发布能力槽位。", ) ) if risks: return risks return [ self._risk( f"{slot_key}_{role_label}_capability_missing", "critical", f"{slot_label}{role_label}不满足能力要求", "能力矩阵未满足;请在供应商与凭据中补齐能力标签,或选择符合要求的运行时。", ) ] def _merge_team_policy(self, saved: dict[str, Any]) -> dict[str, Any]: strategy = str(saved.get("strategy") or "balanced").strip() legacy_strategy_map = {"quality": "quality_first", "speed": "speed_first"} strategy = legacy_strategy_map.get(strategy, strategy) if strategy not in {"quality_first", "balanced", "speed_first"}: strategy = "balanced" saved_scenes = { str(item.get("key") or ""): item for item in saved.get("scenes", []) if isinstance(item, dict) and str(item.get("key") or "") in SCENE_LABELS } scenes: list[dict[str, Any]] = [] for key, label in SCENE_LABELS.items(): item = saved_scenes.get(key, {}) scenes.append( { "key": key, "label": label, "policy": str(item.get("policy") or DEFAULT_SCENE_POLICIES[key]).strip(), "mode": str(item.get("mode") or self._default_scene_mode(key)).strip(), "human_confirmation": bool(item.get("human_confirmation", key in {"project_launch", "requirement_structuring", "task_breakdown"})), "protected": bool(item.get("protected", key in {"project_launch", "requirement_structuring", "task_breakdown", "assignee_matching"})), } ) budget_guard = saved.get("budget_guard") if isinstance(saved.get("budget_guard"), dict) else {} return { "strategy": strategy, "strategy_label": {"quality_first": "质量优先", "balanced": "成本均衡", "speed_first": "速度优先"}[strategy], "scenes": scenes, "automation_boundary": str( saved.get("automation_boundary") or "高风险写操作只生成待确认草案,禁止模型直接改生产状态。" ).strip(), "budget_guard": { "enabled": bool(budget_guard.get("enabled", False)), "monthly_budget_cny": budget_guard.get("monthly_budget_cny"), "alert_threshold_ratio": float(budget_guard.get("alert_threshold_ratio", 0.8) or 0.8), }, "quality_targets": self._normalize_quality_targets(saved.get("quality_targets")), "updated_at": str(saved.get("updated_at") or "") or None, "updated_by": saved.get("updated_by"), } @staticmethod def _normalize_quality_targets(value: Any) -> list[str]: if isinstance(value, list): items = [str(item).strip() for item in value if str(item or "").strip()] return items[:8] if items else AIRoutingGovernanceService._default_quality_targets() return AIRoutingGovernanceService._default_quality_targets() @staticmethod def _default_quality_targets() -> list[str]: return [ "项目启动、需求结构化、任务拆解失败时必须显式进入待确认或失败态", "内部问答必须保留来源合同,外部查询必须经过实时来源校验", "保护场景不得被租户覆盖降级到快速槽", ] @staticmethod def _default_scene_mode(key: str) -> str: if key in {"requirement_structuring", "assignee_matching"}: return "深度优先" if key == "external_query": return "外部校验" if key in {"feishu_understanding", "feishu_reply"}: return "快速处理" return "快速起草,失败升级" def _build_slots(self, *, engine_bundle: dict[str, Any], config_bundle: dict[str, Any]) -> list[dict[str, Any]]: return [ slot for slot in self._build_capability_slots( engine_bundle=engine_bundle, slot_bundle={}, config_bundle=config_bundle, stage="active", ) if slot.get("slot_key") in {"low", "high"} ] def _build_capability_slots( self, *, engine_bundle: dict[str, Any], slot_bundle: dict[str, Any], config_bundle: dict[str, Any], stage: str = "active", ) -> list[dict[str, Any]]: engines = [item for item in engine_bundle.get("engines", []) if isinstance(item, dict)] engine_by_id = {str(item.get("id") or ""): item for item in engines if str(item.get("id") or "")} normalized_bundle = self._normalize_capability_slot_bundle(slot_bundle) saved_slots = self._slot_config_map(normalized_bundle, stage=stage) tier_defaults = config_bundle.get("tier_defaults") if isinstance(config_bundle.get("tier_defaults"), dict) else get_tier_defaults() slots: list[dict[str, Any]] = [] for slot_key, spec in SLOT_SPECS.items(): saved = saved_slots.get(slot_key) if isinstance(saved_slots.get(slot_key), dict) else {} if slot_key == "external_query": slots.append( self._build_external_query_slot_summary( saved=saved, spec=spec, stage=stage, ) ) continue if slot_key in {"low", "high"}: defaults = tier_defaults.get(slot_key) if isinstance(tier_defaults.get(slot_key), dict) else {} inferred_engine = self._infer_engine_for_tier(engines=engines, defaults=defaults) else: defaults = {} inferred_engine = None primary_engine_id = str(saved.get("primary_engine_id") or "").strip() primary_engine = engine_by_id.get(primary_engine_id) if primary_engine_id else inferred_engine fallback_engine_ids = [ str(item or "").strip() for item in saved.get("fallback_engine_ids", []) if str(item or "").strip() in engine_by_id ] if isinstance(saved.get("fallback_engine_ids"), list) else [] fallback_engines = [self._slot_engine_summary(engine_by_id[item]) for item in fallback_engine_ids] provider = str((primary_engine or {}).get("provider") or defaults.get("provider") or "").strip().lower() model = str((primary_engine or {}).get("model") or defaults.get("model") or "").strip() base_url = str((primary_engine or {}).get("base_url") or defaults.get("base_url") or "").strip() configured = bool(primary_engine and (bool(primary_engine.get("enabled", True)) or provider == "ollama")) source = "槽位绑定" if primary_engine_id else ("供应商配置" if primary_engine else "未配置") api_key_masked = _mask_secret(str((primary_engine or {}).get("api_key") or "")) if primary_engine else "" failure_policy = str(saved.get("failure_policy") or spec["default_failure_policy"]).strip() if failure_policy not in FAILURE_POLICY_LABELS: failure_policy = str(spec["default_failure_policy"]) candidate_engines: list[dict[str, Any]] = [] blocked_candidate_engines: list[dict[str, Any]] = [] for item in engines: if not bool(item.get("enabled", True)): continue summary = self._slot_engine_summary(item) coverage = self._engine_capability_coverage(slot_key=slot_key, engine=item) if not summary: continue if bool(coverage.get("satisfied", True)): candidate_engines.append(summary) continue blocked_candidate_engines.append( { **summary, "gap_reasons": self._coverage_gap_reasons(coverage), } ) slots.append( { "slot_key": slot_key, "tier": slot_key, "label": str(spec["label"]), "summary": str(spec["summary"]), "configurable": bool(spec["configurable"]), "protected": bool(spec["protected"]), "provider": provider, "provider_label": PROVIDER_LABELS.get(provider, provider or "未配置"), "model": model, "base_url": base_url, "configured": configured, "config_source": source, "api_key_masked": api_key_masked, "primary_engine_id": str((primary_engine or {}).get("id") or "") or None, "primary_engine": self._slot_engine_summary(primary_engine), "fallback_engine_ids": fallback_engine_ids, "fallback_engines": fallback_engines, "requirements": self._slot_requirement_summary(slot_key), "capability_coverage": self._engine_capability_coverage(slot_key=slot_key, engine=primary_engine), "candidate_engine_count": len([item for item in candidate_engines if item]), "candidate_engines": [item for item in candidate_engines if item][:5], "blocked_candidate_engine_count": len([item for item in blocked_candidate_engines if item]), "blocked_candidate_engines": [item for item in blocked_candidate_engines if item][:5], "failure_policy": failure_policy, "failure_policy_label": FAILURE_POLICY_LABELS[failure_policy], "allow_scene_downgrade": bool(saved.get("allow_scene_downgrade", False)), "updated_at": _iso(saved.get("updated_at")), "updated_by": saved.get("updated_by"), } ) return slots def _build_external_query_slot_summary( self, *, saved: dict[str, Any], spec: dict[str, Any], stage: str, ) -> dict[str, Any]: registry = self._external_query_registry_status(binding=saved) provider_by_id = {str(item.get("id") or ""): item for item in registry.get("providers", []) if isinstance(item, dict) and str(item.get("id") or "")} primary_provider_id = str(saved.get("primary_provider_id") or saved.get("primary_engine_id") or "").strip() fallback_provider_ids = [ str(item or "").strip() for item in list(saved.get("fallback_provider_ids") or saved.get("fallback_engine_ids") or []) if str(item or "").strip() in provider_by_id and str(item or "").strip() != primary_provider_id ] primary_provider = provider_by_id.get(primary_provider_id) if primary_provider_id else None fallback_providers = [provider_by_id[item] for item in fallback_provider_ids if item in provider_by_id] configured = bool(primary_provider_id and primary_provider and bool(primary_provider.get("available"))) coverage = self._external_query_capability_coverage(saved=saved, registry=registry) failure_policy = str(saved.get("failure_policy") or spec["default_failure_policy"]).strip() if failure_policy != "explicit_fail": failure_policy = "explicit_fail" return { "slot_key": "external_query", "tier": "external_query", "label": str(spec["label"]), "summary": str(spec["summary"]), "configurable": bool(spec["configurable"]), "protected": bool(spec["protected"]), "provider": "external_registry", "provider_label": "External Provider Registry", "model": str((primary_provider or {}).get("label") or ""), "base_url": "external_query_service", "configured": configured, "config_source": "槽位绑定" if primary_provider_id else "未配置", "api_key_masked": "", "primary_engine_id": None, "primary_engine": None, "fallback_engine_ids": [], "fallback_engines": [], "primary_provider_id": primary_provider_id or None, "primary_provider": primary_provider, "fallback_provider_ids": fallback_provider_ids, "fallback_providers": fallback_providers, "verifier_required": True, "source_filtering_required": True, "external_provider_registry": registry, "requirements": self._slot_requirement_summary("external_query"), "capability_coverage": coverage, "candidate_engine_count": int(registry.get("available_provider_count") or 0), "candidate_engines": [], "blocked_candidate_engine_count": len([item for item in registry.get("providers", []) if isinstance(item, dict) and not bool(item.get("available"))]), "blocked_candidate_engines": [], "candidate_external_providers": [item for item in registry.get("providers", []) if isinstance(item, dict) and bool(item.get("available"))], "blocked_external_providers": [item for item in registry.get("providers", []) if isinstance(item, dict) and not bool(item.get("available"))], "failure_policy": failure_policy, "failure_policy_label": FAILURE_POLICY_LABELS[failure_policy], "allow_scene_downgrade": False, "updated_at": _iso(saved.get("updated_at")), "updated_by": saved.get("updated_by"), "stage": stage, } @staticmethod def _external_query_registry_status(*, binding: dict[str, Any] | None = None) -> dict[str, Any]: return ExternalProviderRegistry.status_snapshot(binding=binding) def _external_query_capability_coverage(self, *, saved: dict[str, Any], registry: dict[str, Any]) -> dict[str, Any]: missing: list[str] = [] missing_labels: list[str] = [] primary_provider_id = str(saved.get("primary_provider_id") or saved.get("primary_engine_id") or "").strip() providers = [item for item in registry.get("providers", []) if isinstance(item, dict)] provider_by_id = {str(item.get("id") or ""): item for item in providers if str(item.get("id") or "")} if not primary_provider_id: missing.append("primary_provider") missing_labels.append("未选择主外部 provider") elif primary_provider_id not in provider_by_id: missing.append("primary_provider_unknown") missing_labels.append("主外部 provider 不存在") elif not bool(provider_by_id[primary_provider_id].get("available")): missing.append("primary_provider_unavailable") missing_labels.append("主外部 provider 不可用") if not (bool(registry.get("mcp_adapter_available")) or bool(registry.get("live_search_fallback_available"))): missing.append("external_adapter") missing_labels.append("MCP adapter / bounded live search fallback 不可用") verifier = registry.get("verifier") if isinstance(registry.get("verifier"), dict) else {} if not bool(verifier.get("available")): missing.append("verifier") missing_labels.append("外部答案 verifier 不可用") source_filtering = registry.get("source_filtering") if isinstance(registry.get("source_filtering"), dict) else {} if not bool(source_filtering.get("fail_closed")): missing.append("source_filtering") missing_labels.append("来源过滤未 fail-closed") if str(saved.get("failure_policy") or "explicit_fail").strip() != "explicit_fail": missing.append("failure_policy") missing_labels.append("失败策略必须显式失败") return { "satisfied": len(missing) == 0, "missing": missing, "missing_labels": missing_labels, "missing_evidence": [], "missing_evidence_labels": [], "stale_evidence": [], "stale_evidence_labels": [], "capabilities": { "external_search_ready": len(missing) == 0, "capability_source": "registry", "capability_probe_status": "not_required", "capability_last_verified_at": None, "capability_blocking_reasons": missing_labels, }, } @staticmethod def _infer_engine_for_tier(*, engines: list[dict[str, Any]], defaults: dict[str, Any]) -> dict[str, Any] | None: provider = str(defaults.get("provider") or "").strip().lower() model = str(defaults.get("model") or "").strip().lower() for item in engines: if not bool(item.get("enabled", True)): continue if str(item.get("provider") or "").strip().lower() != provider: continue item_model = str(item.get("model") or "").strip().lower() if model and item_model and item_model != model: continue return item for item in engines: if bool(item.get("enabled", True)) and str(item.get("provider") or "").strip().lower() == provider: return item return None @staticmethod def _slot_engine_summary(engine: dict[str, Any] | None) -> dict[str, Any] | None: if not isinstance(engine, dict): return None provider = str(engine.get("provider") or "").strip().lower() connection_status = str(engine.get("connection_status") or "untested").strip() or "untested" capabilities = _normalize_engine_capabilities( engine.get("capabilities"), provider=provider, model=str(engine.get("model") or ""), ) return { "id": str(engine.get("id") or ""), "name": str(engine.get("name") or PROVIDER_LABELS.get(provider, provider)).strip(), "provider": provider, "provider_label": PROVIDER_LABELS.get(provider, provider), "model": str(engine.get("model") or ""), "base_url": str(engine.get("base_url") or ""), "enabled": bool(engine.get("enabled", True)), "connection_status": connection_status, "api_key_masked": _mask_secret(str(engine.get("api_key") or "")), "capabilities": capabilities, } @staticmethod def _slot_requirement_summary(slot_key: str) -> dict[str, Any]: requirement = SLOT_REQUIREMENTS.get(slot_key, {}) required_flags = [str(item) for item in requirement.get("required_flags", []) if str(item or "").strip()] forbidden_flags = [str(item) for item in requirement.get("forbidden_flags", []) if str(item or "").strip()] any_flags = [str(item) for item in requirement.get("any_flags", []) if str(item or "").strip()] min_context = requirement.get("min_context_tokens") labels = [CAPABILITY_LABELS.get(item, item) for item in required_flags] if any_flags: labels.append(" / ".join(CAPABILITY_LABELS.get(item, item) for item in any_flags)) if min_context: labels.append(f"上下文 >= {int(min_context):,} tokens") if forbidden_flags: labels.extend(f"禁止 {CAPABILITY_LABELS.get(item, item)}" for item in forbidden_flags) return { "summary": str(requirement.get("summary") or ""), "required_flags": required_flags, "forbidden_flags": forbidden_flags, "any_flags": any_flags, "min_context_tokens": min_context, "labels": labels, } @classmethod def _engine_capability_coverage(cls, *, slot_key: str, engine: dict[str, Any] | None) -> dict[str, Any]: requirement = SLOT_REQUIREMENTS.get(slot_key, {}) if not isinstance(engine, dict): return { "satisfied": False, "missing": ["runtime"], "missing_labels": ["未选择运行时"], "missing_evidence": [], "missing_evidence_labels": [], "stale_evidence": [], "stale_evidence_labels": [], "capabilities": _normalize_engine_capabilities({}, provider="", model=""), } provider = str(engine.get("provider") or "").strip().lower() capabilities = _normalize_engine_capabilities( engine.get("capabilities"), provider=provider, model=str(engine.get("model") or ""), ) missing: list[str] = [] missing_labels: list[str] = [] missing_evidence: list[str] = [] missing_evidence_labels: list[str] = [] stale_evidence: list[str] = [] stale_evidence_labels: list[str] = [] def _has_verified_evidence(key: str) -> bool: effective_key = "long_context_32k" if key == "long_context" else key effective = capabilities.get("effective_capabilities") if isinstance(capabilities.get("effective_capabilities"), dict) else {} if bool(effective.get(effective_key) or effective.get(key)): return True probe = capabilities.get("capability_probe") if isinstance(probe, dict): probe_source = str(probe.get("source") or "").strip().lower() probe_evidence = probe.get("evidence") if isinstance(probe.get("evidence"), dict) else {} probe_row = probe_evidence.get(effective_key) or probe_evidence.get(key) if isinstance(probe_row, dict) and probe_source == "probed" and _probe_row_is_verified(probe_row, key=effective_key): return True evidence = capabilities.get("capability_evidence") if not isinstance(evidence, dict): return False row = evidence.get(effective_key) or evidence.get(key) if not isinstance(row, dict): return False return _probe_row_is_verified(row, key=effective_key) def _evidence_row(key: str) -> dict[str, Any] | None: probe = capabilities.get("capability_probe") evidence_key = "long_context_32k" if key == "long_context" else key if isinstance(probe, dict): probe_evidence = probe.get("evidence") if isinstance(probe.get("evidence"), dict) else {} probe_row = probe_evidence.get(evidence_key) or probe_evidence.get(key) if isinstance(probe_row, dict): return probe_row evidence = capabilities.get("capability_evidence") if isinstance(evidence, dict): row = evidence.get(evidence_key) or evidence.get(key) if isinstance(row, dict): return row return None def _evidence_checked_at(key: str) -> str | None: row = _evidence_row(key) if not isinstance(row, dict): return None checked_at = str(row.get("checked_at") or row.get("verified_at") or "").strip() if checked_at: return checked_at return None def _has_stale_evidence(key: str) -> bool: row = _evidence_row(key) if isinstance(row, dict): expires_at = str(row.get("expires_at") or "").strip() parsed_expires_at = _parse_iso_datetime(expires_at) if expires_at else None if parsed_expires_at is not None and parsed_expires_at < now_cn_naive(): return True checked_at = _evidence_checked_at(key) if not checked_at: return False parsed = _parse_iso_datetime(checked_at) if parsed is None: return False return parsed < (now_cn_naive() - timedelta(days=CAPABILITY_PROBE_EVIDENCE_TTL_DAYS)) def _requires_probe_evidence(key: str) -> bool: if slot_key not in {"structured", "tool", "long_context"}: return False return key in {"structured_output", "json_schema", "tool_calling", "long_context", "max_context_tokens"} def _record_capability_gap(key: str, label: str) -> None: if not bool(capabilities.get(key)): missing.append(key) missing_labels.append(label) return evidence_key = "long_context" if key == "max_context_tokens" else key if _requires_probe_evidence(key) and not _has_verified_evidence(evidence_key): missing_evidence.append(evidence_key) missing_evidence_labels.append(label) elif _requires_probe_evidence(key) and _has_stale_evidence(evidence_key): stale_evidence.append(evidence_key) stale_evidence_labels.append(label) for flag in requirement.get("required_flags", []) or []: key = str(flag) _record_capability_gap(key, CAPABILITY_LABELS.get(key, key)) any_flags = [str(item) for item in requirement.get("any_flags", []) or [] if str(item or "").strip()] if any_flags and not any(bool(capabilities.get(item)) for item in any_flags): missing.append("any:" + ",".join(any_flags)) missing_labels.append(" 或 ".join(CAPABILITY_LABELS.get(item, item) for item in any_flags)) elif any_flags and slot_key in {"structured", "tool", "long_context"}: verified_any = any(bool(capabilities.get(item)) and _has_verified_evidence(item) for item in any_flags) if not verified_any: missing_evidence.append("any:" + ",".join(any_flags)) missing_evidence_labels.append(" 或 ".join(CAPABILITY_LABELS.get(item, item) for item in any_flags)) elif any(_has_stale_evidence(item) for item in any_flags if bool(capabilities.get(item))): stale_evidence.append("any:" + ",".join(any_flags)) stale_evidence_labels.append(" 或 ".join(CAPABILITY_LABELS.get(item, item) for item in any_flags)) for flag in requirement.get("forbidden_flags", []) or []: key = str(flag) if bool(capabilities.get(key)): missing.append(f"forbidden:{key}") missing_labels.append(f"禁止 {CAPABILITY_LABELS.get(key, key)}") min_context = requirement.get("min_context_tokens") if min_context: actual = int(capabilities.get("max_context_tokens") or 0) if actual < int(min_context): missing.append("max_context_tokens") missing_labels.append(f"上下文窗口不足 {int(min_context):,} tokens") elif _requires_probe_evidence("max_context_tokens") and not _has_verified_evidence("long_context"): missing_evidence.append("long_context") missing_evidence_labels.append(f"上下文 >= {int(min_context):,} tokens") elif _requires_probe_evidence("max_context_tokens") and _has_stale_evidence("long_context"): stale_evidence.append("long_context") stale_evidence_labels.append(f"上下文 >= {int(min_context):,} tokens") return { "satisfied": len(missing) == 0 and len(missing_evidence) == 0 and len(stale_evidence) == 0, "missing": missing, "missing_labels": missing_labels, "missing_evidence": missing_evidence, "missing_evidence_labels": missing_evidence_labels, "stale_evidence": stale_evidence, "stale_evidence_labels": stale_evidence_labels, "capabilities": capabilities, } @staticmethod def _coverage_gap_reasons(coverage: dict[str, Any]) -> list[str]: reasons: list[str] = [] missing_labels = coverage.get("missing_labels") if isinstance(coverage.get("missing_labels"), list) else [] missing_evidence_labels = coverage.get("missing_evidence_labels") if isinstance(coverage.get("missing_evidence_labels"), list) else [] stale_evidence_labels = coverage.get("stale_evidence_labels") if isinstance(coverage.get("stale_evidence_labels"), list) else [] if missing_labels: reasons.append("缺能力:" + "、".join(str(item) for item in missing_labels if str(item or "").strip())) if missing_evidence_labels: reasons.append("缺证据:" + "、".join(str(item) for item in missing_evidence_labels if str(item or "").strip())) if stale_evidence_labels: reasons.append("证据过期:" + "、".join(str(item) for item in stale_evidence_labels if str(item or "").strip())) return [item for item in reasons if item][:6] def _build_providers(self, *, engine_bundle: dict[str, Any]) -> list[dict[str, Any]]: default_fallback_id = str(engine_bundle.get("default_fallback_engine_id") or engine_bundle.get("active_engine_id") or "") engines = [item for item in engine_bundle.get("engines", []) if isinstance(item, dict)] providers = [] for item in engines: provider = str(item.get("provider") or "").strip().lower() enabled = bool(item.get("enabled", True)) is_default_fallback = str(item.get("id") or "") == default_fallback_id connection_status = str(item.get("connection_status") or "untested").strip() or "untested" status_label = self._provider_status_label( enabled=enabled, is_default_fallback=is_default_fallback, connection_status=connection_status, ) providers.append( { "id": str(item.get("id") or ""), "name": str(item.get("name") or PROVIDER_LABELS.get(provider, provider)).strip(), "provider": provider, "provider_label": PROVIDER_LABELS.get(provider, provider), "model": str(item.get("model") or ""), "base_url": str(item.get("base_url") or ""), "api_key_masked": _mask_secret(str(item.get("api_key") or "")), "active": is_default_fallback, "enabled": enabled, "is_default_fallback": is_default_fallback, "connection_status": connection_status, "last_test_status": str(item.get("last_test_status") or ""), "last_test_at": _iso(item.get("last_test_at")), "last_error_summary": str(item.get("last_error_summary") or ""), "status_label": status_label, "updated_at": _iso(item.get("updated_at")), "available_actions": self._provider_action_permissions( enabled=enabled, is_default_fallback=is_default_fallback, connection_status=connection_status, ), "capabilities": _normalize_engine_capabilities( item.get("capabilities"), provider=provider, model=str(item.get("model") or ""), ), } ) return providers @staticmethod def _provider_action_permissions(*, enabled: bool, is_default_fallback: bool, connection_status: str) -> dict[str, bool]: connection_available = str(connection_status or "untested").strip().lower() == "available" return { "enable": not enabled, "disable": enabled, "edit": True, "test": enabled, "capability_probe": enabled and connection_available, "set_default_fallback": enabled and connection_available and not is_default_fallback, "delete": (not enabled) and not is_default_fallback, } @staticmethod def _provider_status_label(*, enabled: bool, is_default_fallback: bool, connection_status: str) -> str: _ = is_default_fallback if not enabled: return "已停用" if connection_status == "available": return "可用" if connection_status == "test_failed": return "测试失败" return "未测试" def _build_scene_routes( self, *, routing_bundle: dict[str, Any], config_bundle: dict[str, Any], function_runtime_profiles: dict[str, Any] | None = None, function_observability: dict[str, Any] | None = None, ) -> list[dict[str, Any]]: mappings = routing_bundle.get("mappings") if isinstance(routing_bundle.get("mappings"), dict) else {} profile_release = function_runtime_profiles if isinstance(function_runtime_profiles, dict) else {} active_profiles = profile_release.get("active_profiles") if isinstance(profile_release.get("active_profiles"), dict) else {} draft_profiles = profile_release.get("draft_profiles") if isinstance(profile_release.get("draft_profiles"), dict) else {} draft_smoke = profile_release.get("draft_last_smoke") if isinstance(profile_release.get("draft_last_smoke"), dict) else {} observability = function_observability if isinstance(function_observability, dict) else {} routes: list[dict[str, Any]] = [] for scene_key, functions in SCENE_FUNCTIONS.items(): contract = scene_contract(scene_key) function_rows: list[dict[str, Any]] = [] tiers: list[str] = [] for function_name in functions: override = mappings.get(function_name) if isinstance(mappings.get(function_name), dict) else None override_tier = _normalize_tier((override or {}).get("tier_alias") or (override or {}).get("tier")) rule = find_matching_rule(function_name, tier_filter={override_tier} if override_tier else None, bundle=config_bundle) tier = override_tier or _normalize_tier((rule or {}).get("tier")) or "low" tiers.append(tier) function_rows.append( { "function_name": function_name, "display_name": self._function_display_name(function_name), "tier": tier, "slot_label": SLOT_LABELS.get(tier, tier), "source": "override" if override_tier else "default", "rule_name": str((rule or {}).get("name") or "") or None, "protected": function_name in PROTECTED_FUNCTIONS, "runtime_profile": self._function_runtime_profile_summary( function_name=function_name, active_profile=active_profiles.get(function_name) if isinstance(active_profiles.get(function_name), dict) else None, draft_profile=draft_profiles.get(function_name) if isinstance(draft_profiles.get(function_name), dict) else None, active_version=str(profile_release.get("active_version") or ""), draft_version=str(profile_release.get("draft_version") or "draft"), dirty=bool(profile_release.get("dirty", False)), draft_smoke=draft_smoke.get(function_name) if isinstance(draft_smoke.get(function_name), dict) else None, observability=observability.get(function_name) if isinstance(observability.get(function_name), dict) else None, ), } ) default_slot = str(SCENE_DEFAULT_SLOT.get(scene_key) or "").strip().lower() primary_tier = "mixed" if len(set(tiers)) > 1 else (tiers[0] if tiers else default_slot) routes.append( { "scene_key": scene_key, "label": contract["label"], "summary": DEFAULT_SCENE_POLICIES[scene_key], "primary_slot_label": ( "混合策略" if primary_tier == "mixed" else str((SLOT_SPECS.get(primary_tier) or {}).get("label") or SLOT_LABELS.get(primary_tier, contract["expected_slot_label"])) ), "scene_contract": contract, "lifecycle": contract["lifecycle"], "scene_family": contract["scene_family"], "functions": function_rows, } ) return routes def _build_scene_slot_matrix( self, *, scene_routes: list[dict[str, Any]], capability_slot_release: dict[str, Any], scene_failure_rates: dict[str, dict[str, Any]] | None = None, ) -> list[dict[str, Any]]: active_version = str(capability_slot_release.get("active_version") or "unpublished") draft_version = str(capability_slot_release.get("draft_version") or "draft") active_slots = [ item for item in capability_slot_release.get("active_slots", []) if isinstance(item, dict) ] draft_slots = [ item for item in capability_slot_release.get("draft_slots", []) if isinstance(item, dict) ] slot_by_key = { str(item.get("slot_key") or item.get("tier") or "").strip().lower(): item for item in active_slots if str(item.get("slot_key") or item.get("tier") or "").strip() } draft_slot_by_key = { str(item.get("slot_key") or item.get("tier") or "").strip().lower(): item for item in draft_slots if str(item.get("slot_key") or item.get("tier") or "").strip() } rows: list[dict[str, Any]] = [] for route in scene_routes: scene_key = str(route.get("scene_key") or "").strip() contract = route.get("scene_contract") if isinstance(route.get("scene_contract"), dict) else scene_contract(scene_key) lifecycle = str(contract.get("lifecycle") or route.get("lifecycle") or "active") current_slot = str(SCENE_DEFAULT_SLOT.get(scene_key) or "").strip().lower() required_slots = [ item for item in SCENE_SLOT_REQUIREMENTS.get(scene_key, []) if str(item or "").strip() ] function_slots = [ str(item.get("tier") or "").strip().lower() for item in route.get("functions", []) if isinstance(item, dict) and str(item.get("tier") or "").strip() ] base_slots = [] if not current_slot or current_slot == "mixed" else [current_slot] ordered_slots = list(dict.fromkeys([*base_slots, *required_slots, *function_slots])) slot_rows = [ self._build_scene_slot_matrix_slot( scene_key=scene_key, slot_key=slot_key, slot=slot_by_key.get(slot_key), runtime_version=active_version, is_required=slot_key in required_slots, ) for slot_key in ordered_slots ] route_risks = self._scene_route_slot_risks(scene_key=scene_key, route=route) for risk in route_risks: slot_key = str(risk.get("slot_key") or "").strip().lower() target = next((item for item in slot_rows if item.get("slot_key") == slot_key), None) if target is None: target = self._build_scene_slot_matrix_slot( scene_key=scene_key, slot_key=slot_key, slot=slot_by_key.get(slot_key), runtime_version=active_version, is_required=False, ) slot_rows.append(target) target["risk_level"] = "critical" target["risk_label"] = "保护场景降级" target["risk_reasons"] = list(dict.fromkeys([*target.get("risk_reasons", []), str(risk.get("message") or "")])) risk_order = {"critical": 3, "warning": 2, "ok": 1} scene_risk_level = max( (str(item.get("risk_level") or "ok") for item in slot_rows), key=lambda value: risk_order.get(value, 0), default="ok", ) active_slot = slot_by_key.get(current_slot) if current_slot != "mixed" else None draft_slot = draft_slot_by_key.get(current_slot) if current_slot != "mixed" else None failure_stats = (scene_failure_rates or {}).get(scene_key) if isinstance(scene_failure_rates, dict) else {} required_capabilities = self._scene_required_capabilities(scene_key=scene_key, current_slot=current_slot) scene_label = str(route.get("label") or SCENE_LABELS.get(scene_key, scene_key)) active_runtime = self._scene_runtime_summary( slot_key=current_slot, slot=active_slot, version=active_version, source="active", ) draft_runtime = self._scene_runtime_summary( slot_key=current_slot, slot=draft_slot, version=draft_version, source="draft", ) capability_gap = self._scene_capability_gap(slot_rows) recent_failed_calls = int((failure_stats or {}).get("failed_calls") or 0) recent_total_calls = int((failure_stats or {}).get("total_calls") or 0) failure_rate = float((failure_stats or {}).get("failure_rate") or 0.0) scene_health = self._scene_health( lifecycle=lifecycle, active_runtime=active_runtime, draft_dirty=bool(capability_slot_release.get("dirty", False)), capability_gap=capability_gap, risk_level=scene_risk_level, recent_failed_calls=recent_failed_calls, recent_total_calls=recent_total_calls, failure_rate=failure_rate, ) release_gate = self._scene_release_gate( lifecycle=lifecycle, protected=scene_key in PROTECTED_SCENE_KEYS, active_runtime=active_runtime, draft_runtime=draft_runtime, draft_dirty=bool(capability_slot_release.get("dirty", False)), capability_gap=capability_gap, risk_level=scene_risk_level, recent_failed_calls=recent_failed_calls, ) impact_summary = ( f"{scene_label} 是候选场景,当前仅展示运行合同,不参与 active 槽位发布影响。" if lifecycle != "active" else self._scene_impact_summary( scene_label=scene_label, current_slot=current_slot, active_runtime=active_runtime, draft_runtime=draft_runtime, risk_level=scene_risk_level, failure_rate=failure_rate, ) ) rows.append( { "scene_key": scene_key, "scene_label": scene_label, "label": scene_label, "summary": str(route.get("summary") or DEFAULT_SCENE_POLICIES.get(scene_key, "")), "scene_contract": contract, "lifecycle": lifecycle, "scene_family": str(contract.get("scene_family") or route.get("scene_family") or "unknown"), "protected": scene_key in PROTECTED_SCENE_KEYS, "required_slot": current_slot, "current_slot": current_slot, "current_slot_label": ( "复杂度动态分流" if current_slot == "mixed" else str((SLOT_SPECS.get(current_slot) or {}).get("label") or SLOT_LABELS.get(current_slot, current_slot or contract.get("expected_slot_label") or "-")) ), "required_capabilities": required_capabilities, "active_slot_runtime": active_runtime, "active_runtime": active_runtime, "draft_slot_runtime": draft_runtime, "draft_runtime": draft_runtime, "active_version": active_version, "draft_dirty": bool(capability_slot_release.get("dirty", False)), "runtime_version": active_version, "capability_gap": capability_gap, "recent_failure_rate": float((failure_stats or {}).get("failure_rate") or 0.0), "recent_failure_count": recent_failed_calls, "recent_failed_calls": recent_failed_calls, "recent_total_calls": recent_total_calls, "scene_health": scene_health, "release_gate": release_gate, "primary_slot_label": str(route.get("primary_slot_label") or ""), "risk_level": scene_risk_level, "risk_label": self._scene_risk_label(scene_risk_level), "impact_summary": impact_summary, "slots": slot_rows, "functions": route.get("functions", []), "function_runtime_profiles": [ item.get("runtime_profile") for item in route.get("functions", []) if isinstance(item, dict) and isinstance(item.get("runtime_profile"), dict) ], } ) return rows @staticmethod def _scene_capability_gap(slot_rows: list[dict[str, Any]]) -> dict[str, Any]: missing: list[str] = [] missing_evidence: list[str] = [] stale_evidence: list[str] = [] reasons: list[str] = [] for slot in slot_rows: coverage = slot.get("capability_coverage") if isinstance(slot.get("capability_coverage"), dict) else {} missing.extend(str(item) for item in coverage.get("missing_labels", []) if str(item or "").strip()) missing_evidence.extend(str(item) for item in coverage.get("missing_evidence_labels", []) if str(item or "").strip()) stale_evidence.extend(str(item) for item in coverage.get("stale_evidence_labels", []) if str(item or "").strip()) reasons.extend(str(item) for item in slot.get("risk_reasons", []) if str(item or "").strip()) unique_missing = list(dict.fromkeys(missing)) unique_missing_evidence = list(dict.fromkeys(missing_evidence)) unique_stale_evidence = list(dict.fromkeys(stale_evidence)) unique_reasons = list(dict.fromkeys(reasons)) return { "missing": unique_missing, "missing_evidence": unique_missing_evidence, "stale_evidence": unique_stale_evidence, "reasons": unique_reasons, "has_gap": bool(unique_missing or unique_missing_evidence or unique_stale_evidence or unique_reasons), "summary": ";".join(unique_reasons[:3]) or ( "、".join( [ *[f"缺能力:{item}" for item in unique_missing], *[f"缺证据:{item}" for item in unique_missing_evidence], *[f"证据过期:{item}" for item in unique_stale_evidence], ][:3] ) ) or "无能力缺口", } @staticmethod def _scene_health( *, lifecycle: str, active_runtime: dict[str, Any], draft_dirty: bool, capability_gap: dict[str, Any], risk_level: str, recent_failed_calls: int, recent_total_calls: int, failure_rate: float, ) -> dict[str, Any]: if lifecycle != "active": return { "status": "unknown", "score": 0, "blockers": [], "warnings": ["候选场景仅展示运行合同,暂不参与 active 发布门禁。"], "evidence_summary": "candidate scene,无运行样本要求。", "last_observed_at": None, } blockers: list[str] = [] warnings: list[str] = [] if not bool(active_runtime.get("configured")): blockers.append("Active runtime 未绑定") if bool(capability_gap.get("has_gap")): blockers.extend(str(item) for item in capability_gap.get("reasons", []) if str(item or "").strip()) for label in capability_gap.get("missing", []) or []: blockers.append(f"缺能力:{label}") for label in capability_gap.get("missing_evidence", []) or []: blockers.append(f"缺证据:{label}") for label in capability_gap.get("stale_evidence", []) or []: blockers.append(f"证据过期:{label}") if risk_level == "critical": blockers.append("保护场景存在 critical 路由或槽位风险") if recent_total_calls == 0: warnings.append("暂无 7d 运行样本") elif failure_rate >= 0.3: warnings.append(f"7d 失败率 {round(failure_rate * 100)}%") elif recent_failed_calls > 0: warnings.append(f"7d 失败样本 {recent_failed_calls} 次") if draft_dirty: warnings.append("存在未发布 draft 影响") unique_blockers = list(dict.fromkeys(blockers)) unique_warnings = list(dict.fromkeys(warnings)) if unique_blockers: status = "blocked" score = 30 elif recent_total_calls == 0: status = "unknown" score = 55 elif unique_warnings: status = "warning" score = 70 else: status = "healthy" score = 95 evidence_summary = ( ";".join(unique_blockers[:2] or unique_warnings[:2]) or f"7d 样本 {recent_total_calls} 次,失败 {recent_failed_calls} 次。" ) return { "status": status, "score": score, "blockers": unique_blockers, "warnings": unique_warnings, "evidence_summary": evidence_summary, "last_observed_at": None, } @staticmethod def _scene_release_gate( *, lifecycle: str, protected: bool, active_runtime: dict[str, Any], draft_runtime: dict[str, Any], draft_dirty: bool, capability_gap: dict[str, Any], risk_level: str, recent_failed_calls: int, ) -> dict[str, Any]: affected_by_draft = bool(draft_dirty) active_label = str(active_runtime.get("runtime_label") or "") draft_label = str(draft_runtime.get("runtime_label") or "") if draft_label and draft_label != active_label: affected_by_draft = True if lifecycle != "active": return { "publish_blocked": False, "blocking_reason": None, "affected_by_draft": False, "protected_scene_regression": False, "suggested_action": "none", } protected_scene_regression = bool(protected and (risk_level == "critical" or capability_gap.get("has_gap") or not active_runtime.get("configured"))) publish_blocked = bool(protected_scene_regression) blocking_reason = None suggested_action = "none" if not bool(active_runtime.get("configured")): blocking_reason = "Active runtime 未绑定" suggested_action = "configure_slot" elif (capability_gap.get("missing_evidence") or capability_gap.get("stale_evidence")): blocking_reason = capability_gap.get("summary") or "能力证据缺失或过期" suggested_action = "probe_capability" elif capability_gap.get("missing"): blocking_reason = capability_gap.get("summary") or "缺少场景要求的 runtime 能力" suggested_action = "add_provider" elif recent_failed_calls > 0: suggested_action = "inspect_failures" return { "publish_blocked": publish_blocked, "blocking_reason": blocking_reason, "affected_by_draft": affected_by_draft, "protected_scene_regression": protected_scene_regression, "suggested_action": suggested_action, } def _scene_required_capabilities(self, *, scene_key: str, current_slot: str) -> list[str]: slot_keys = list(SCENE_SLOT_REQUIREMENTS.get(scene_key, [])) if current_slot and current_slot != "mixed": slot_keys.insert(0, current_slot) labels: list[str] = [] for slot_key in dict.fromkeys(slot_keys): requirement_labels = self._slot_requirement_summary(str(slot_key)).get("labels") or [] if requirement_labels: labels.extend(str(item) for item in requirement_labels if str(item or "").strip()) else: slot_label = str((SLOT_SPECS.get(str(slot_key)) or {}).get("label") or SLOT_LABELS.get(str(slot_key), str(slot_key))) labels.append(f"{slot_label}可用连接") return list(dict.fromkeys(labels)) def _scene_runtime_summary( self, *, slot_key: str, slot: dict[str, Any] | None, version: str, source: str, ) -> dict[str, Any]: normalized_slot = str(slot_key or "").strip().lower() slot_label = "复杂度动态分流" if normalized_slot == "mixed" else str((SLOT_SPECS.get(normalized_slot) or {}).get("label") or SLOT_LABELS.get(normalized_slot, normalized_slot or "-")) if normalized_slot == "mixed": return { "slot_key": normalized_slot, "slot_label": slot_label, "version": version, "configured": True, "runtime_label": "按问题复杂度在快速槽/深度槽之间分流", "runtime_source": source, "primary_engine_id": None, } primary_engine = (slot or {}).get("primary_engine") if isinstance((slot or {}).get("primary_engine"), dict) else None primary_provider = (slot or {}).get("primary_provider") if isinstance((slot or {}).get("primary_provider"), dict) else None runtime_label = ( f"{primary_engine.get('name')} · {primary_engine.get('provider_label')} / {primary_engine.get('model')}" if primary_engine else ( f"{primary_provider.get('label')} · external_query_service" if primary_provider else ( f"{(slot or {}).get('provider_label')} / {(slot or {}).get('model')}" if bool((slot or {}).get("configured")) else "未绑定运行时" ) ) ) return { "slot_key": normalized_slot, "slot_label": slot_label, "version": version, "configured": bool((slot or {}).get("configured")), "runtime_label": runtime_label, "runtime_source": str((slot or {}).get("config_source") or source), "primary_engine_id": str((slot or {}).get("primary_engine_id") or "") or None, "primary_provider_id": str((slot or {}).get("primary_provider_id") or "") or None, } @staticmethod def _function_runtime_profile_summary( *, function_name: str, active_profile: dict[str, Any] | None, draft_profile: dict[str, Any] | None, active_version: str, draft_version: str, dirty: bool, draft_smoke: dict[str, Any] | None, observability: dict[str, Any] | None, ) -> dict[str, Any]: active = _safe_dict(active_profile) draft = _safe_dict(draft_profile) or active profile_dirty = active != draft return { "function_name": function_name, "label": str(draft.get("label") or function_name), "scene_key": str(draft.get("scene_key") or active.get("scene_key") or ""), "prompt_key": str(draft.get("prompt_key") or active.get("prompt_key") or ""), "required_slot": str(draft.get("required_slot") or active.get("required_slot") or ""), "response_contract": str(draft.get("response_contract") or active.get("response_contract") or ""), "active_version": active_version, "draft_version": draft_version, "dirty": bool(dirty or profile_dirty), "active": { "max_tokens": int(active.get("max_tokens") or 0), "repair_max_tokens": int(active.get("repair_max_tokens") or 0), "temperature": float(active.get("temperature") or 0.0), "foreground_wait_seconds": int(active.get("foreground_wait_seconds") or 0), "provider_timeout_seconds": int(active.get("provider_timeout_seconds") or 0), "job_timeout_seconds": int(active.get("job_timeout_seconds") or 0), "max_retries": int(active.get("max_retries") or 0), "large_input_policy": str(active.get("large_input_policy") or ""), "response_mode": str(active.get("response_mode") or ""), "response_format": str(active.get("response_format") or ""), }, "draft": { "max_tokens": int(draft.get("max_tokens") or active.get("max_tokens") or 0), "repair_max_tokens": int(draft.get("repair_max_tokens") or active.get("repair_max_tokens") or 0), "temperature": float(draft.get("temperature") or active.get("temperature") or 0.0), "foreground_wait_seconds": int(draft.get("foreground_wait_seconds") or active.get("foreground_wait_seconds") or 0), "provider_timeout_seconds": int(draft.get("provider_timeout_seconds") or active.get("provider_timeout_seconds") or 0), "job_timeout_seconds": int(draft.get("job_timeout_seconds") or active.get("job_timeout_seconds") or 0), "max_retries": int(draft.get("max_retries") or active.get("max_retries") or 0), "large_input_policy": str(draft.get("large_input_policy") or active.get("large_input_policy") or ""), "response_mode": str(draft.get("response_mode") or active.get("response_mode") or ""), "response_format": str(draft.get("response_format") or active.get("response_format") or ""), }, "limits": { "max_tokens_min": int(draft.get("max_tokens_min") or active.get("max_tokens_min") or 0), "max_tokens_max": int(draft.get("max_tokens_max") or active.get("max_tokens_max") or 0), "recommended_min_tokens": int(draft.get("recommended_min_tokens") or active.get("recommended_min_tokens") or 0), }, "retry_policy": _safe_dict(draft.get("retry_policy") or active.get("retry_policy")), "health_policy": _safe_dict(draft.get("health_policy") or active.get("health_policy")), "cost_policy": _safe_dict(draft.get("cost_policy") or active.get("cost_policy")), "draft_smoke": _safe_dict(draft_smoke), "observability": _safe_dict(observability), } @staticmethod def _scene_impact_summary( *, scene_label: str, current_slot: str, active_runtime: dict[str, Any], draft_runtime: dict[str, Any], risk_level: str, failure_rate: float, ) -> str: slot_label = str(active_runtime.get("slot_label") or current_slot or "-") active_label = str(active_runtime.get("runtime_label") or "未绑定运行时") draft_label = str(draft_runtime.get("runtime_label") or "未绑定运行时") if risk_level == "critical": return f"{scene_label} 当前依赖 {slot_label},存在阻塞风险;发布前必须先修复槽位能力或运行时绑定。" if active_label != draft_label: return f"发布草稿会把 {scene_label} 的 {slot_label} 从 {active_label} 切换为 {draft_label}。" if failure_rate > 0: return f"{scene_label} 当前由 {slot_label} 承载,近 7 天失败率 {round(failure_rate * 100)}%,发布前建议复核证据。" return f"{scene_label} 当前由 {slot_label} 承载,active runtime 为 {active_label}。" def _build_scene_slot_matrix_slot( self, *, scene_key: str, slot_key: str, slot: dict[str, Any] | None, runtime_version: str, is_required: bool, ) -> dict[str, Any]: slot_key = str(slot_key or "").strip().lower() slot_label = str((slot or {}).get("label") or (SLOT_SPECS.get(slot_key) or {}).get("label") or SLOT_LABELS.get(slot_key, slot_key)) coverage = (slot or {}).get("capability_coverage") if isinstance((slot or {}).get("capability_coverage"), dict) else {} risk_reasons: list[str] = [] configured = bool((slot or {}).get("configured")) if not configured: risk_reasons.append("未发布 active slot 绑定") if coverage and not bool(coverage.get("satisfied", True)): missing = [str(item) for item in coverage.get("missing_labels", []) if str(item or "").strip()] missing_evidence = [str(item) for item in coverage.get("missing_evidence_labels", []) if str(item or "").strip()] stale_evidence = [str(item) for item in coverage.get("stale_evidence_labels", []) if str(item or "").strip()] if missing: risk_reasons.append(f"缺能力:{'、'.join(missing)}") if missing_evidence: risk_reasons.append(f"缺证据:{'、'.join(missing_evidence)}") if stale_evidence: risk_reasons.append(f"证据过期:{'、'.join(stale_evidence)}") risk_level = "ok" if risk_reasons: risk_level = "critical" if scene_key in PROTECTED_SCENE_KEYS or is_required else "warning" primary_engine = (slot or {}).get("primary_engine") if isinstance((slot or {}).get("primary_engine"), dict) else None primary_provider = (slot or {}).get("primary_provider") if isinstance((slot or {}).get("primary_provider"), dict) else None return { "slot_key": slot_key, "slot_label": slot_label, "required": bool(is_required), "configured": configured, "runtime_version": runtime_version, "runtime_source": str((slot or {}).get("config_source") or "未发布"), "primary_engine_id": str((slot or {}).get("primary_engine_id") or "") or None, "primary_provider_id": str((slot or {}).get("primary_provider_id") or "") or None, "primary_engine_label": ( f"{primary_engine.get('name')} · {primary_engine.get('provider_label')} / {primary_engine.get('model')}" if primary_engine else ( f"{primary_provider.get('label')} · external_query_service" if primary_provider else ( f"{(slot or {}).get('provider_label')} / {(slot or {}).get('model')}" if configured else "未发布 active runtime" ) ) ), "risk_level": risk_level, "risk_label": self._scene_risk_label(risk_level), "risk_reasons": risk_reasons, "capability_coverage": coverage, } @staticmethod def _scene_route_slot_risks(*, scene_key: str, route: dict[str, Any]) -> list[dict[str, Any]]: allowed = SCENE_FUNCTION_SLOT_ALLOWLIST.get(scene_key) if not allowed: return [] risks: list[dict[str, Any]] = [] for item in route.get("functions", []): if not isinstance(item, dict) or not bool(item.get("protected")): continue slot_key = str(item.get("tier") or "").strip().lower() if slot_key and slot_key not in allowed: risks.append( { "slot_key": slot_key, "message": f"{item.get('display_name') or item.get('function_name')} 被路由到 {SLOT_LABELS.get(slot_key, slot_key)},不满足该保护场景的能力要求。", } ) return risks @staticmethod def _scene_risk_label(level: str) -> str: if level == "critical": return "阻塞" if level == "warning": return "需关注" return "正常" async def _build_metrics(self) -> dict[str, Any]: return { "last_24h": await self._window_metrics(hours=24), "last_7d": await self._window_metrics(hours=24 * 7), } def _build_slot_recommendations( self, *, slots: list[dict[str, Any]], slot_observability: list[dict[str, Any]], ) -> list[dict[str, Any]]: observability_by_slot = { str(item.get("slot_key") or "").strip().lower(): item for item in slot_observability if isinstance(item, dict) } recommendations: list[dict[str, Any]] = [] for slot in slots: if not isinstance(slot, dict): continue slot_key = str(slot.get("slot_key") or slot.get("tier") or "").strip().lower() if slot_key not in OBSERVABLE_RUNTIME_SLOTS: continue recommendations.extend( self._slot_evidence_recommendations(slot_key=slot_key, slot=slot) ) recommendations.extend( self._slot_observability_recommendations( slot_key=slot_key, slot=slot, observability=observability_by_slot.get(slot_key) or {}, ) ) if slot_key in {"structured", "tool", "long_context"} and int(slot.get("candidate_engine_count") or 0) == 0: recommendations.append( self._slot_recommendation( severity="warning", slot_key=slot_key, code=f"{slot_key}_no_candidate_runtime", reason="该槽位当前没有满足能力要求和证据要求的候选 runtime。", evidence_summary="候选 runtime 数量为 0;不满足要求的 runtime 需要补能力、补证据或启用连接。", suggested_action="回到供应商与凭据补齐能力标签、API Key、连接状态,或执行 capability probe;必要时接入新的 runtime。", ) ) return recommendations[:20] def _slot_evidence_recommendations(self, *, slot_key: str, slot: dict[str, Any]) -> list[dict[str, Any]]: coverage = slot.get("capability_coverage") if isinstance(slot.get("capability_coverage"), dict) else {} missing_evidence_labels = [str(item) for item in coverage.get("missing_evidence_labels", []) if str(item or "").strip()] stale_evidence_labels = [str(item) for item in coverage.get("stale_evidence_labels", []) if str(item or "").strip()] items: list[dict[str, Any]] = [] if missing_evidence_labels: items.append( self._slot_recommendation( severity="critical" if slot_key in {"structured", "tool", "long_context"} else "warning", slot_key=slot_key, code=f"{slot_key}_probe_evidence_missing", reason="槽位主 runtime 缺少 verified capability evidence。", evidence_summary="缺证据:" + "、".join(missing_evidence_labels), suggested_action="先在供应商与凭据执行 capability probe,再回到槽位页校验发布。", target_engine_id=str(slot.get("primary_engine_id") or "") or None, ) ) if stale_evidence_labels: items.append( self._slot_recommendation( severity="warning", slot_key=slot_key, code=f"{slot_key}_probe_evidence_stale", reason="槽位主 runtime 的 capability evidence 已过期。", evidence_summary="证据过期:" + "、".join(stale_evidence_labels), suggested_action="重新执行 capability probe,确认 runtime 仍满足该槽位能力要求。", target_engine_id=str(slot.get("primary_engine_id") or "") or None, ) ) return items def _slot_observability_recommendations( self, *, slot_key: str, slot: dict[str, Any], observability: dict[str, Any], ) -> list[dict[str, Any]]: windows = observability.get("windows") if isinstance(observability.get("windows"), dict) else {} last_7d = windows.get("last_7d") if isinstance(windows.get("last_7d"), dict) else {} if last_7d.get("status") != "sampled": return [] total_calls = int(last_7d.get("total_calls") or 0) recommendations: list[dict[str, Any]] = [] if total_calls >= SLOT_RECOMMENDATION_MIN_CALLS: failure_rate = float(last_7d.get("failure_rate") or 0) if failure_rate >= SLOT_RECOMMENDATION_FAILURE_RATE_THRESHOLD: target = self._first_candidate_engine_id(slot) recommendations.append( self._slot_recommendation( severity="warning", slot_key=slot_key, code=f"{slot_key}_high_failure_rate", reason="该 active slot 近 7 天失败率偏高。", evidence_summary=f"7d 调用 {total_calls} 次,失败率 {round(failure_rate * 100)}%。", suggested_action="优先补 fallback;若已有候选 runtime,评估切换主 runtime 或调整 fallback chain。", target_engine_id=target, ) ) fallback_hits = int(last_7d.get("fallback_hits") or 0) fallback_rate = float(last_7d.get("fallback_rate") or 0) if fallback_hits > 0 and fallback_rate >= SLOT_RECOMMENDATION_FALLBACK_RATE_THRESHOLD: target = self._first_fallback_engine_id(slot) recommendations.append( self._slot_recommendation( severity="info", slot_key=slot_key, code=f"{slot_key}_fallback_primary_candidate", reason="该 active slot 近 7 天频繁命中 fallback。", evidence_summary=f"7d fallback 命中 {fallback_hits} 次,占比 {round(fallback_rate * 100)}%。", suggested_action="评估将表现稳定的 fallback runtime 提升为 primary,或保留当前主运行时并补充备用链路。", target_engine_id=target, ) ) degraded_calls = int(last_7d.get("degraded_calls") or 0) degraded_rate = float(last_7d.get("degraded_rate") or 0) if degraded_calls > 0 and degraded_rate >= SLOT_RECOMMENDATION_DEGRADED_RATE_THRESHOLD: recommendations.append( self._slot_recommendation( severity="critical", slot_key=slot_key, code=f"{slot_key}_active_slot_degraded", reason="该 active slot 发生 degraded runtime 路径。", evidence_summary=f"7d degraded 调用 {degraded_calls} 次,占比 {round(degraded_rate * 100)}%。", suggested_action="修复当前 active slot 的 runtime/证据,必要时回滚到上一稳定 active 版本。", ) ) if slot_key == "low": recommendations.extend(self._low_slot_cost_latency_recommendations(slot=slot, window=last_7d)) return recommendations def _low_slot_cost_latency_recommendations(self, *, slot: dict[str, Any], window: dict[str, Any]) -> list[dict[str, Any]]: total_calls = int(window.get("total_calls") or 0) if total_calls < SLOT_RECOMMENDATION_MIN_CALLS: return [] average_latency_ms = window.get("average_latency_ms") estimated_cost = float(window.get("estimated_cost") or 0) latency_high = isinstance(average_latency_ms, (int, float)) and float(average_latency_ms) >= LOW_SLOT_LATENCY_WARNING_MS cost_high = estimated_cost >= LOW_SLOT_COST_WARNING if not latency_high and not cost_high: return [] target = self._low_cost_latency_candidate(slot) if not target: return [] reasons = [] if latency_high: reasons.append(f"平均延迟 {round(float(average_latency_ms))}ms") if cost_high: reasons.append(f"7d 估算成本 ¥{estimated_cost:.2f}") return [ self._slot_recommendation( severity="info", slot_key="low", code="low_cost_latency_candidate", reason="快速槽的成本或延迟出现异常。", evidence_summary=";".join(reasons) + ";成本/延迟来自 ai_call_logs 估算。", suggested_action="评估切换到低成本或低延迟候选 runtime;先保存 draft 并通过发布校验,不要直接发布。", target_engine_id=target, ) ] @staticmethod def _slot_recommendation( *, severity: str, slot_key: str, code: str, reason: str, evidence_summary: str, suggested_action: str, target_engine_id: str | None = None, ) -> dict[str, Any]: normalized_severity = str(severity or "info").strip().lower() if normalized_severity not in {"critical", "warning", "info"}: normalized_severity = "info" slot_label = str((SLOT_SPECS.get(slot_key) or {}).get("label") or SLOT_LABELS.get(slot_key, slot_key)) return { "severity": normalized_severity, "slot_key": slot_key, "slot_label": slot_label, "code": code, "reason": reason, "evidence_summary": evidence_summary, "suggested_action": suggested_action, "target_engine_id": target_engine_id, } @staticmethod def _first_candidate_engine_id(slot: dict[str, Any]) -> str | None: candidates = slot.get("candidate_engines") if isinstance(slot.get("candidate_engines"), list) else [] current = str(slot.get("primary_engine_id") or "") for candidate in candidates: if not isinstance(candidate, dict): continue candidate_id = str(candidate.get("id") or "").strip() if candidate_id and candidate_id != current: return candidate_id return None @staticmethod def _first_fallback_engine_id(slot: dict[str, Any]) -> str | None: fallback_ids = slot.get("fallback_engine_ids") if isinstance(slot.get("fallback_engine_ids"), list) else [] for fallback_id in fallback_ids: value = str(fallback_id or "").strip() if value: return value fallback_engines = slot.get("fallback_engines") if isinstance(slot.get("fallback_engines"), list) else [] for fallback in fallback_engines: if isinstance(fallback, dict) and str(fallback.get("id") or "").strip(): return str(fallback.get("id") or "").strip() return None def _low_cost_latency_candidate(self, slot: dict[str, Any]) -> str | None: candidates = slot.get("candidate_engines") if isinstance(slot.get("candidate_engines"), list) else [] current = str(slot.get("primary_engine_id") or "") for candidate in candidates: if not isinstance(candidate, dict): continue candidate_id = str(candidate.get("id") or "").strip() if not candidate_id or candidate_id == current: continue capabilities = candidate.get("capabilities") if isinstance(candidate.get("capabilities"), dict) else {} cost_tier = str(capabilities.get("cost_tier") or "").strip().lower() avg_latency = capabilities.get("avg_latency_ms") low_latency = isinstance(avg_latency, (int, float)) and float(avg_latency) < LOW_SLOT_LATENCY_WARNING_MS if cost_tier == "low" or low_latency: return candidate_id return self._first_candidate_engine_id(slot) async def _build_slot_observability(self, *, capability_slot_release: dict[str, Any]) -> list[dict[str, Any]]: active_slots = { str(slot.get("slot_key") or slot.get("tier") or "").strip().lower(): slot for slot in capability_slot_release.get("active_slots", []) if isinstance(slot, dict) } active_version = str(capability_slot_release.get("active_version") or "unpublished").strip() return [ { "slot_key": slot_key, "slot_label": str((SLOT_SPECS.get(slot_key) or {}).get("label") or SLOT_LABELS.get(slot_key, slot_key)), "active_version": active_version, "active_runtime": self._scene_runtime_summary( slot_key=slot_key, slot=active_slots.get(slot_key), version=active_version, source="active", ), "windows": { "last_24h": await self._slot_observability_window(slot_key=slot_key, hours=24, sample_limit=4), "last_7d": await self._slot_observability_window(slot_key=slot_key, hours=24 * 7, sample_limit=4), }, } for slot_key in OBSERVABLE_RUNTIME_SLOTS ] async def _slot_observability_window(self, *, slot_key: str, hours: int, sample_limit: int) -> dict[str, Any]: since = now_cn_naive() - timedelta(hours=hours) rows = ( await self.db.execute( select(AICallLog) .where( and_( AICallLog.tenant_id == self.tenant_id, AICallLog.created_at >= since, ) ) .order_by(AICallLog.created_at.desc(), AICallLog.id.desc()) ) ).scalars().all() matched: list[tuple[AICallLog, dict[str, Any]]] = [] for row in rows: signal = self._row_slot_observability_signal(row) if signal.get("slot_key") == slot_key: matched.append((row, signal)) if not matched: return self._empty_slot_observability_window(hours=hours) total = len(matched) failed = sum(1 for row, _signal in matched if self._is_failed_status(str(row.status or ""))) fallback_hits = sum(1 for _row, signal in matched if int(signal.get("slot_fallback_index") or 0) > 0) degraded_calls = sum( 1 for _row, signal in matched if bool(signal.get("slot_degraded")) or signal.get("slot_runtime_source") == "active_slot_degraded" ) active_slot_calls = sum(1 for _row, signal in matched if signal.get("slot_runtime_source") == "active_slot") degraded_source_calls = sum(1 for _row, signal in matched if signal.get("slot_runtime_source") == "active_slot_degraded") total_cost = sum(float(row.cost or Decimal("0")) for row, _signal in matched) latencies = [ latency for latency in (self._row_latency_ms(row) for row, _signal in matched) if latency is not None ] failure_samples: list[dict[str, Any]] = [] for row, signal in matched: if not self._is_failed_status(str(row.status or "")): continue failure_samples.append(self._slot_failure_sample(row=row, signal=signal)) if len(failure_samples) >= sample_limit: break return { "window_hours": hours, "status": "sampled", "sample_state_label": "已采集运行样本", "total_calls": total, "failed_calls": failed, "failure_rate": round(failed / total, 4) if total else 0.0, "fallback_hits": fallback_hits, "fallback_rate": round(fallback_hits / total, 4) if total else 0.0, "degraded_calls": degraded_calls, "degraded_rate": round(degraded_calls / total, 4) if total else 0.0, "active_slot_calls": active_slot_calls, "active_slot_degraded_calls": degraded_source_calls, "average_latency_ms": round(sum(latencies) / len(latencies), 2) if latencies else None, "estimated_cost": round(total_cost, 6), "estimated_cost_source": "ai_call_logs.cost 估算值,非供应商精确账单", "latency_source": "ai_call_logs.latency_ms" if latencies else "当前调用日志未记录延迟", "recent_failure_samples": failure_samples, } @staticmethod def _empty_slot_observability_window(*, hours: int) -> dict[str, Any]: return { "window_hours": hours, "status": "empty", "sample_state_label": "暂无运行样本", "total_calls": 0, "failed_calls": 0, "failure_rate": 0.0, "fallback_hits": 0, "fallback_rate": 0.0, "degraded_calls": 0, "degraded_rate": 0.0, "active_slot_calls": 0, "active_slot_degraded_calls": 0, "average_latency_ms": None, "estimated_cost": 0.0, "estimated_cost_source": "ai_call_logs.cost 估算值,非供应商精确账单", "latency_source": "当前调用日志未记录延迟", "recent_failure_samples": [], } def _row_slot_observability_signal(self, row: AICallLog) -> dict[str, Any]: meta = self._row_routing_meta(row) slot_key = str( meta.get("slot_key") or getattr(row, "slot_key", None) or row.executed_tier or row.planned_tier or "" ).strip().lower() if slot_key not in OBSERVABLE_RUNTIME_SLOTS: return {} runtime_source = str(meta.get("slot_runtime_source") or getattr(row, "slot_runtime_source", None) or "").strip().lower() routing_decision = str(row.routing_decision or "").strip().lower() degraded = bool(meta.get("slot_degraded", False) or getattr(row, "slot_degraded", False)) if routing_decision == "structured_slot_degraded": runtime_source = "active_slot_degraded" degraded = True if runtime_source not in {"active_slot", "active_slot_degraded"}: runtime_source = "active_slot" fallback_index = self._safe_int(meta.get("slot_fallback_index") if "slot_fallback_index" in meta else getattr(row, "slot_fallback_index", None)) return { "slot_key": slot_key, "slot_runtime_source": runtime_source, "slot_fallback_index": fallback_index, "slot_degraded": degraded, } @staticmethod def _row_routing_meta(row: AICallLog) -> dict[str, Any]: raw = getattr(row, "routing_meta", None) if isinstance(raw, dict): return raw if isinstance(raw, str) and raw.strip(): try: parsed = json.loads(raw) return parsed if isinstance(parsed, dict) else {} except Exception: return {} return {} @staticmethod def _row_latency_ms(row: AICallLog) -> float | None: raw = getattr(row, "latency_ms", None) or getattr(row, "duration_ms", None) try: value = float(raw) except (TypeError, ValueError): return None return value if value >= 0 else None @staticmethod def _safe_int(raw: Any) -> int: try: return int(raw) except (TypeError, ValueError): return 0 def _slot_failure_sample(self, *, row: AICallLog, signal: dict[str, Any]) -> dict[str, Any]: return { "function_label": self._function_display_name(str(row.function_name or "")), "function_name": str(row.function_name or ""), "status_label": self._status_label(str(row.status or "")), "slot_runtime_source": str(signal.get("slot_runtime_source") or "active_slot"), "slot_fallback_index": int(signal.get("slot_fallback_index") or 0), "created_at": _iso(row.created_at), } async def _window_metrics(self, *, hours: int) -> dict[str, Any]: since = now_cn_naive() - timedelta(hours=hours) rows = ( await self.db.execute( select(AICallLog).where( and_( AICallLog.tenant_id == self.tenant_id, AICallLog.created_at >= since, ) ) ) ).scalars().all() total = len(rows) failed = sum(1 for row in rows if self._is_failed_status(str(row.status or ""))) upgraded = sum( 1 for row in rows if str(row.planned_tier or "").strip() and str(row.executed_tier or "").strip() and str(row.planned_tier or "").strip() != str(row.executed_tier or "").strip() ) structured_failed = sum(1 for row in rows if self._is_structured_failure(str(row.status or ""))) total_cost = sum(float(row.cost or Decimal("0")) for row in rows) return { "total_calls": total, "failed_calls": failed, "failure_rate": round(failed / total, 4) if total else 0.0, "upgraded_calls": upgraded, "upgrade_rate": round(upgraded / total, 4) if total else 0.0, "structured_failed_calls": structured_failed, "estimated_cost": round(total_cost, 6), "average_latency_ms": None, } async def _scene_failure_rates(self, *, hours: int) -> dict[str, dict[str, Any]]: since = now_cn_naive() - timedelta(hours=hours) rows = ( await self.db.execute( select(AICallLog).where( and_( AICallLog.tenant_id == self.tenant_id, AICallLog.created_at >= since, ) ) ) ).scalars().all() function_to_scene = { function_name: scene_key for scene_key, functions in SCENE_FUNCTIONS.items() for function_name in functions } stats: dict[str, dict[str, int]] = { scene_key: {"total_calls": 0, "failed_calls": 0} for scene_key in SCENE_LABELS } for row in rows: scene_key = function_to_scene.get(str(row.function_name or "")) if not scene_key: continue stats[scene_key]["total_calls"] += 1 if self._is_failed_status(str(row.status or "")): stats[scene_key]["failed_calls"] += 1 return { scene_key: { "total_calls": values["total_calls"], "failed_calls": values["failed_calls"], "failure_rate": round(values["failed_calls"] / values["total_calls"], 4) if values["total_calls"] else 0.0, } for scene_key, values in stats.items() } async def _function_runtime_observability(self, *, hours: int) -> dict[str, dict[str, Any]]: since = now_cn_naive() - timedelta(hours=hours) function_names = sorted({function for functions in SCENE_FUNCTIONS.values() for function in functions}) rows = ( await self.db.execute( select(AICallLog).where( and_( AICallLog.tenant_id.in_([self.tenant_id, PLATFORM_RUNTIME_TENANT_ID]), AICallLog.function_name.in_(function_names), AICallLog.created_at >= since, ) ) ) ).scalars().all() grouped: dict[str, list[AICallLog]] = {} for row in rows: grouped.setdefault(str(row.function_name or ""), []).append(row) def _completion_tokens(row: AICallLog) -> int: payload = row.provider_response_payload if isinstance(row.provider_response_payload, dict) else {} usage = payload.get("usage") if isinstance(payload.get("usage"), dict) else {} return int(usage.get("completion_tokens") or 0) def _finish_reason(row: AICallLog) -> str: payload = row.provider_response_payload if isinstance(row.provider_response_payload, dict) else {} choices = payload.get("choices") if isinstance(payload.get("choices"), list) else [] choice = choices[0] if choices and isinstance(choices[0], dict) else {} return str(choice.get("finish_reason") or "").strip() def _percentile(values: list[int], percentile: float) -> int: ordered = sorted(value for value in values if value > 0) if not ordered: return 0 index = min(len(ordered) - 1, max(0, int(round((len(ordered) - 1) * percentile)))) return ordered[index] result: dict[str, dict[str, Any]] = {} for function_name, function_rows in grouped.items(): latest = max(function_rows, key=lambda item: (item.created_at, item.id)) failed = [row for row in function_rows if self._is_failed_status(str(row.status or ""))] finish_reasons = [_finish_reason(row) for row in function_rows] completion_tokens = [_completion_tokens(row) for row in function_rows] result[function_name] = { "window_hours": hours, "total_calls": len(function_rows), "failed_calls": len(failed), "failure_rate": round(len(failed) / len(function_rows), 4) if function_rows else 0.0, "length_finish_count": sum(1 for item in finish_reasons if item in {"length", "max_tokens"}), "structured_json_invalid_count": sum(1 for row in function_rows if str(row.status or "") in {"structured_empty", "structured_json_invalid"}), "completion_tokens_p50": _percentile(completion_tokens, 0.5), "completion_tokens_p95": _percentile(completion_tokens, 0.95), "latest_status": str(latest.status or ""), "latest_finish_reason": _finish_reason(latest), "latest_call_id": int(latest.id), "latest_created_at": latest.created_at.isoformat() if latest.created_at else None, } return result async def _build_diagnostics( self, *, engine_bundle: dict[str, Any], routing_bundle: dict[str, Any], slots: list[dict[str, Any]], metrics: dict[str, Any], ) -> list[dict[str, Any]]: items: list[dict[str, Any]] = [] if any(not slot.get("configured") for slot in slots if slot.get("tier") == "high"): items.append(self._risk("high_unavailable", "critical", "深度槽未配置可用供应商", "项目启动、需求结构化和复杂问答可能显式失败。")) if len(slots) == 2 and self._same_slot_runtime(slots[0], slots[1]): items.append(self._risk("same_slot_runtime", "warning", "快速槽和深度槽指向同一运行时", "低阶失败后升级无法获得更强模型能力。")) items.extend(self._protected_downgrade_diagnostics(routing_bundle=routing_bundle)) last_24h = metrics.get("last_24h") if isinstance(metrics.get("last_24h"), dict) else {} if int(last_24h.get("total_calls") or 0) >= 3 and float(last_24h.get("failure_rate") or 0) >= 0.3: items.append(self._risk("high_failure_rate", "warning", "最近 24 小时 AI 调用失败率偏高", "建议先检查供应商 Key、额度、结构化输出和高阶槽位。")) if int(last_24h.get("structured_failed_calls") or 0) >= 3: items.append(self._risk("structured_failure_rate", "warning", "结构化输出失败偏多", "项目启动、任务拆解和语义事实抽取可能进入待补充或失败状态。")) if self._ollama_runtime_risk(engine_bundle=engine_bundle, slots=slots): items.append(self._risk("ollama_scope_risk", "warning", "本地 Ollama 只能用于知识库问答", "若被绑定到项目创建或任务拆解,会被运行边界拦截。")) if not items: items.append({"code": "ok", "severity": "ok", "title": "未发现明显配置风险", "message": "当前供应商、槽位和路由配置未触发内置风险规则。"}) return items def _protected_downgrade_diagnostics(self, *, routing_bundle: dict[str, Any]) -> list[dict[str, Any]]: mappings = routing_bundle.get("mappings") if isinstance(routing_bundle.get("mappings"), dict) else {} downgraded = [] for function_name in PROTECTED_FUNCTIONS: override = mappings.get(function_name) if isinstance(mappings.get(function_name), dict) else None tier = _normalize_tier((override or {}).get("tier_alias") or (override or {}).get("tier")) if tier == "low": downgraded.append(function_name) if not downgraded: return [] return [ self._risk( "protected_function_downgraded", "critical", "保护场景被覆盖到快速槽", f"涉及 {len(downgraded)} 个保护函数;这些场景应保持深度或 low-first 高阶修复。", ) ] @staticmethod def _risk(code: str, severity: str, title: str, message: str) -> dict[str, Any]: return {"code": code, "severity": severity, "title": title, "message": message} @staticmethod def _same_slot_runtime(a: dict[str, Any], b: dict[str, Any]) -> bool: return ( str(a.get("provider") or "").strip().lower() == str(b.get("provider") or "").strip().lower() and str(a.get("model") or "").strip().lower() == str(b.get("model") or "").strip().lower() and str(a.get("base_url") or "").strip().rstrip("/").lower() == str(b.get("base_url") or "").strip().rstrip("/").lower() ) @staticmethod def _same_engine_identity(a: dict[str, Any], b: dict[str, Any]) -> bool: return ( str(a.get("provider") or "").strip().lower() == str(b.get("provider") or "").strip().lower() and str(a.get("model") or "").strip().lower() == str(b.get("model") or "").strip().lower() and str(a.get("base_url") or "").strip().rstrip("/").lower() == str(b.get("base_url") or "").strip().rstrip("/").lower() ) @staticmethod def _ollama_runtime_risk(*, engine_bundle: dict[str, Any], slots: list[dict[str, Any]]) -> bool: settings = engine_bundle.get("settings") if isinstance(engine_bundle.get("settings"), dict) else {} if bool(settings.get("enable_local_kb_qa")): return False return any(str(slot.get("provider") or "").strip().lower() == "ollama" for slot in slots) @staticmethod def _is_failed_status(status: str) -> bool: normalized = str(status or "").strip().lower() if not normalized: return True return not ( normalized.startswith("success") or normalized in {"ok", "success_cache_hit", "success_failover"} ) @staticmethod def _is_structured_failure(status: str) -> bool: normalized = str(status or "").strip().lower() return "structured" in normalized and any(marker in normalized for marker in ("error", "empty", "invalid", "failed")) async def _recent_failures(self, *, limit: int) -> list[dict[str, Any]]: rows = ( await self.db.execute( select(AICallLog) .where(AICallLog.tenant_id == self.tenant_id) .order_by(AICallLog.created_at.desc(), AICallLog.id.desc()) .limit(80) ) ).scalars().all() function_to_scene = { function_name: scene_key for scene_key, functions in SCENE_FUNCTIONS.items() for function_name in functions } failures: list[dict[str, Any]] = [] for row in rows: if not self._is_failed_status(str(row.status or "")): continue scene_key = function_to_scene.get(str(row.function_name or "")) failures.append( { "function_label": self._function_display_name(str(row.function_name or "")), "function_name": str(row.function_name or ""), "scene_key": scene_key, "scene_label": SCENE_LABELS.get(scene_key or "", ""), "status_label": self._status_label(str(row.status or "")), "planned_slot": SLOT_LABELS.get(str(row.planned_tier or "").strip().lower(), str(row.planned_tier or "") or "-"), "executed_slot": SLOT_LABELS.get(str(row.executed_tier or "").strip().lower(), str(row.executed_tier or "") or "-"), "created_at": _iso(row.created_at), } ) if len(failures) >= limit: break return failures @staticmethod def _function_display_name(function_name: str) -> str: labels = { "pmflow_launch_draft_v1": "项目启动草案", "pmflow_semantic_context_decision": "语义上下文决策", "adaptive_rag_answer_deep": "深度问答生成", "adaptive_rag_answer_normal": "常规问答生成", "generate_task_breakdown": "任务拆解", "rag_db_project_semantic_retrieval": "项目语义检索", "feishu_humanized_reply": "飞书回复润色", "signal_planner_analyze": "信号规划分析", } return labels.get(str(function_name or "").strip(), str(function_name or "").strip() or "未知调用") @staticmethod def _status_label(status: str) -> str: normalized = str(status or "").strip().lower() if "http" in normalized: return "供应商请求失败" if "empty" in normalized: return "模型返回为空" if "invalid" in normalized: return "结构化结果不合法" if "cache" in normalized: return "缓存命中" if normalized.startswith("success"): return "成功" return "调用失败" @staticmethod def _diagnostic_summary(items: list[dict[str, Any]]) -> dict[str, int]: return { "critical": sum(1 for item in items if item.get("severity") == "critical"), "warning": sum(1 for item in items if item.get("severity") == "warning"), "ok": sum(1 for item in items if item.get("severity") == "ok"), }