PMFlow/backend/app/services/ai_routing_governance_servi...

3484 lines
173 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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 官方模型规格",
},
("zhipu", "glm-5.1"): {
"context_tokens": 200_000,
"max_input_tokens": 200_000,
"max_output_tokens": 128_000,
"source": "zhipu_bigmodel_official_spec",
"label": "智谱 BigModel GLM-5.1 官方模型规格",
},
}
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 兼容网关",
"zhipu": "智谱 BigModel",
"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_zhipu = normalized_provider == "zhipu"
is_high_reasoning = normalized_provider in {"openai", "deepseek", "zhipu"} 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", "zhipu"} or is_dashscope,
"tool_calling": normalized_provider in {"openai", "deepseek", "dashscope", "zhipu"},
"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": "high" if is_zhipu and "glm-5.1" in normalized_model else ("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"))
or bool(registry.get("dashscope_responses_available"))
):
risks.append(self._risk("external_query_adapter_missing", "critical", "外部查询执行链不可用", "必须启用 DashScope Responses、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 {"pmflow_ai_route_planning", "pmflow_ai_context_governance"}:
return "语义治理"
if key == "pmflow_ai_answer_presentation":
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"))
or bool(registry.get("dashscope_responses_available"))
):
missing.append("external_adapter")
missing_labels.append("DashScope Responses / 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)
legacy_tier = override_tier or _normalize_tier((rule or {}).get("tier")) or "low"
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,
)
profile_slot = str(runtime_profile.get("required_slot") or "").strip().lower()
tier = profile_slot or legacy_tier
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": "runtime_profile" if profile_slot else ("override" if override_tier else "default"),
"rule_name": "function_runtime_profile" if profile_slot else (str((rule or {}).get("name") or "") or None),
"legacy_tier": legacy_tier,
"legacy_rule_name": str((rule or {}).get("name") or "") or None,
"protected": function_name in PROTECTED_FUNCTIONS,
"runtime_profile": runtime_profile,
}
)
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
scene_key = str(draft.get("scene_key") or active.get("scene_key") or "")
scene = scene_contract(scene_key)
slot_keys = [
str(item or "").strip()
for item in list(scene.get("allowed_slots") or scene.get("required_slots") or [])
if str(item or "").strip()
]
required_slot = str(draft.get("required_slot") or active.get("required_slot") or "")
if required_slot and required_slot not in slot_keys:
slot_keys.insert(0, required_slot)
health_policy = _safe_dict(draft.get("health_policy") or active.get("health_policy"))
return {
"function_name": function_name,
"label": str(draft.get("label") or function_name),
"scene_key": scene_key,
"prompt_key": str(draft.get("prompt_key") or active.get("prompt_key") or ""),
"required_slot": required_slot,
"slot_options": [
{
"slot_key": slot_key,
"label": str((SLOT_SPECS.get(slot_key) or {}).get("label") or SLOT_LABELS.get(slot_key, slot_key)),
}
for slot_key in dict.fromkeys(slot_keys)
],
"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 ""),
"runtime_mode": str(active.get("runtime_mode") or "active"),
"quality_gate_mode": str(active.get("quality_gate_mode") or "strict"),
},
"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 ""),
"runtime_mode": str(draft.get("runtime_mode") or active.get("runtime_mode") or "active"),
"quality_gate_mode": str(draft.get("quality_gate_mode") or active.get("quality_gate_mode") or "strict"),
},
"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": health_policy,
"smoke_supported": AIFunctionRuntimeProfileService.smoke_supported(function_name),
"smoke_required_for_publish": bool(health_policy.get("smoke_required_for_publish")),
"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", "快速槽和深度槽指向同一运行时", "低阶失败后升级无法获得更强模型能力。"))
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
@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": "语义上下文决策",
"pmflow_ai_route_planner": "旧 PMFlow AI 路由规划(已下线)",
"rag_query_semantics": "问答语义与来源策略",
"rag_followup_anchor_semantics": "续问锚点归位",
"rag_pending_clarification_selection_semantics": "待澄清项选择",
"customer_answer_composer": "客户答案表达生成",
"customer_answer_verifier": "客户答案质检",
"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"),
}