316 lines
13 KiB
Python
316 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
from .shared_part_01 import *
|
|
|
|
def _normalize_provider(raw: Optional[str]) -> str:
|
|
provider = str(raw or "").strip().lower()
|
|
provider = PROVIDER_ALIASES.get(provider, provider)
|
|
if provider not in SUPPORTED_PROVIDERS:
|
|
raise ValueError("provider 仅支持 deepseek/dashscope/openai/ollama")
|
|
return provider
|
|
|
|
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:]}"
|
|
|
|
|
|
PROVIDER_DISPLAY_LABELS = {
|
|
"dashscope": "Qwen / DashScope",
|
|
"openai": "OpenAI 兼容网关",
|
|
"deepseek": "DeepSeek",
|
|
"ollama": "本地 Ollama",
|
|
}
|
|
|
|
|
|
def _provider_display_label(raw: Any) -> str:
|
|
provider = str(raw or "").strip().lower()
|
|
provider = PROVIDER_ALIASES.get(provider, provider)
|
|
return PROVIDER_DISPLAY_LABELS.get(provider, provider)
|
|
|
|
def _safe_json(value: Any) -> dict[str, Any]:
|
|
if isinstance(value, dict):
|
|
try:
|
|
# 返回深拷贝,避免原地修改导致 SQLAlchemy 不感知 JSON 字段变更
|
|
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 {}
|
|
|
|
|
|
CAPABILITY_SLOT_ALIASES = {
|
|
"structured",
|
|
"long_context",
|
|
"tool",
|
|
"external_query",
|
|
"private",
|
|
"multimodal",
|
|
}
|
|
|
|
|
|
def _optional_tier_alias(raw: Any) -> str:
|
|
value = str(raw or "").strip().lower()
|
|
return value if value in VALID_TIER_ALIASES or value in CAPABILITY_SLOT_ALIASES else ""
|
|
|
|
|
|
def _optional_response_contract(raw: Any) -> str:
|
|
value = str(raw or "").strip().lower()
|
|
return value if value in {"structured", "freeform"} else ""
|
|
|
|
|
|
def _optional_fallback_policy(raw: Any) -> str:
|
|
value = str(raw or "").strip().lower()
|
|
return value if value in {"fast_local", "default", "disabled"} else ""
|
|
|
|
|
|
def _optional_execution_mode(raw: Any) -> str:
|
|
value = str(raw or "").strip().lower()
|
|
return value if value in {"remote", "local_planner", "local_worker"} else ""
|
|
|
|
|
|
def _optional_meta_text(raw: Any) -> str:
|
|
return str(raw or "").strip()
|
|
|
|
|
|
def _optional_meta_int(raw: Any) -> int | None:
|
|
if raw is None or raw == "":
|
|
return None
|
|
try:
|
|
return int(raw)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def _optional_meta_float(raw: Any) -> float | None:
|
|
if raw is None or raw == "":
|
|
return None
|
|
try:
|
|
return float(raw)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def _normalize_routing_meta(raw: Any) -> dict[str, Any]:
|
|
payload = _safe_json(raw)
|
|
planned_tier = _optional_tier_alias(payload.get("planned_tier"))
|
|
executed_tier = _optional_tier_alias(payload.get("executed_tier")) or planned_tier
|
|
tier_alias = _optional_tier_alias(payload.get("tier_alias")) or planned_tier or executed_tier
|
|
route_source = str(payload.get("route_source") or payload.get("source") or "").strip().lower()
|
|
if route_source not in {"default", "override", "active_engine", "slot_binding", "provider_console", "probe_runner"}:
|
|
route_source = "default" if tier_alias else ""
|
|
rule_name = str(payload.get("route_rule_name") or payload.get("rule_name") or "").strip()
|
|
decision = str(payload.get("routing_decision") or payload.get("decision") or "").strip()
|
|
config_version = str(payload.get("routing_config_version") or "").strip()
|
|
config_updated_at = str(payload.get("routing_config_updated_at") or "").strip()
|
|
rule_priority_raw = payload.get("rule_priority")
|
|
try:
|
|
rule_priority = int(rule_priority_raw) if rule_priority_raw is not None else None
|
|
except (TypeError, ValueError):
|
|
rule_priority = None
|
|
response_contract = _optional_response_contract(payload.get("response_contract"))
|
|
fallback_policy = _optional_fallback_policy(payload.get("fallback_policy"))
|
|
execution_mode = _optional_execution_mode(payload.get("execution_mode"))
|
|
slot_key = _optional_tier_alias(payload.get("slot_key"))
|
|
slot_version = str(payload.get("slot_version") or "").strip()
|
|
slot_runtime_source = str(payload.get("slot_runtime_source") or "").strip().lower()
|
|
if slot_runtime_source not in {"active_slot", "active_slot_degraded"}:
|
|
slot_runtime_source = ""
|
|
slot_fallback_index_raw = payload.get("slot_fallback_index")
|
|
try:
|
|
slot_fallback_index = int(slot_fallback_index_raw) if slot_fallback_index_raw is not None else None
|
|
except (TypeError, ValueError):
|
|
slot_fallback_index = None
|
|
slot_degraded = bool(payload.get("slot_degraded", False))
|
|
provider_code = _optional_meta_text(payload.get("provider_code") or payload.get("provider")).lower()
|
|
prompt_key = _optional_meta_text(payload.get("prompt_key"))
|
|
prompt_source = _optional_meta_text(payload.get("prompt_source"))
|
|
prompt_release_id = _optional_meta_int(payload.get("prompt_release_id"))
|
|
prompt_format_error = bool(payload.get("prompt_format_error", False))
|
|
is_direct_operation = bool(payload.get("direct_operation", False))
|
|
normalized_decision = decision
|
|
if not normalized_decision and (tier_alias or slot_key or planned_tier or executed_tier):
|
|
normalized_decision = "primary_tier"
|
|
return {
|
|
"trace_id": _optional_meta_text(payload.get("trace_id")),
|
|
"run_id": _optional_meta_text(payload.get("run_id")),
|
|
"scene_key": _optional_meta_text(payload.get("scene_key")),
|
|
"provider": provider_code,
|
|
"provider_code": provider_code,
|
|
"provider_label": _optional_meta_text(payload.get("provider_label")) or _provider_display_label(provider_code),
|
|
"model": _optional_meta_text(payload.get("model")),
|
|
"engine_id": _optional_meta_text(payload.get("engine_id")),
|
|
"engine_name": _optional_meta_text(payload.get("engine_name")),
|
|
"latency_ms": _optional_meta_int(payload.get("latency_ms")),
|
|
"duration_ms": _optional_meta_int(payload.get("duration_ms")),
|
|
"tier_alias": tier_alias,
|
|
"planned_tier": planned_tier or tier_alias,
|
|
"executed_tier": executed_tier or planned_tier or tier_alias,
|
|
"route_source": route_source,
|
|
"route_rule_name": rule_name,
|
|
"routing_decision": normalized_decision,
|
|
"routing_config_version": config_version,
|
|
"routing_config_updated_at": config_updated_at,
|
|
"rule_priority": rule_priority,
|
|
"response_contract": response_contract,
|
|
"fallback_policy": fallback_policy,
|
|
"execution_mode": execution_mode,
|
|
"slot_key": slot_key,
|
|
"slot_version": slot_version,
|
|
"slot_runtime_source": slot_runtime_source,
|
|
"slot_fallback_index": slot_fallback_index,
|
|
"slot_degraded": slot_degraded,
|
|
"prompt_key": prompt_key,
|
|
"prompt_release_id": prompt_release_id,
|
|
"prompt_source": prompt_source,
|
|
"prompt_format_error": prompt_format_error,
|
|
"direct_operation": is_direct_operation,
|
|
"probe_run_id": _optional_meta_text(payload.get("probe_run_id")),
|
|
"probe_mode": _optional_meta_text(payload.get("probe_mode")),
|
|
"probe_target": _optional_meta_text(payload.get("probe_target")),
|
|
"probe_targets": payload.get("probe_targets") if isinstance(payload.get("probe_targets"), list) else [],
|
|
"probe_method": _optional_meta_text(payload.get("probe_method")),
|
|
"probe_status": _optional_meta_text(payload.get("probe_status")),
|
|
"probe_summary": payload.get("probe_summary") if isinstance(payload.get("probe_summary"), dict) else {},
|
|
"evidence_strength": _optional_meta_text(payload.get("evidence_strength")),
|
|
"failure_code": _optional_meta_text(payload.get("failure_code")),
|
|
"upstream_request_sent": payload.get("upstream_request_sent") if isinstance(payload.get("upstream_request_sent"), bool) else None,
|
|
"upstream_missing_reason": _optional_meta_text(payload.get("upstream_missing_reason")),
|
|
"scene_timeout_policy_id": _optional_meta_text(payload.get("scene_timeout_policy_id")),
|
|
"scene_timeout_policy_version": _optional_meta_text(payload.get("scene_timeout_policy_version")),
|
|
"input_bucket": _optional_meta_text(payload.get("input_bucket")),
|
|
"input_tokens_estimate": _optional_meta_int(payload.get("input_tokens_estimate")),
|
|
"foreground_wait_seconds": _optional_meta_float(payload.get("foreground_wait_seconds")),
|
|
"provider_timeout_seconds": _optional_meta_float(payload.get("provider_timeout_seconds")),
|
|
"job_timeout_seconds": _optional_meta_float(payload.get("job_timeout_seconds")),
|
|
"max_retries": _optional_meta_int(payload.get("max_retries")),
|
|
"function_runtime_profile_version": _optional_meta_text(payload.get("function_runtime_profile_version")),
|
|
"function_runtime_profile_source": _optional_meta_text(payload.get("function_runtime_profile_source")),
|
|
"function_runtime_max_tokens": _optional_meta_int(payload.get("function_runtime_max_tokens")),
|
|
"function_runtime_repair_max_tokens": _optional_meta_int(payload.get("function_runtime_repair_max_tokens")),
|
|
"provider_usage": payload.get("provider_usage") if isinstance(payload.get("provider_usage"), dict) else {},
|
|
"scene_fallback_policy": _optional_meta_text(payload.get("scene_fallback_policy")),
|
|
"large_input_policy": _optional_meta_text(payload.get("large_input_policy")),
|
|
"timeout_policy_source": _optional_meta_text(payload.get("timeout_policy_source")),
|
|
"timeout_policy_reason": _optional_meta_text(payload.get("timeout_policy_reason")),
|
|
}
|
|
|
|
|
|
def _read_engine_routing_meta(engine: Any) -> dict[str, Any]:
|
|
payload = engine.get("_routing_meta") if isinstance(engine, dict) else {}
|
|
return _normalize_routing_meta(payload)
|
|
|
|
|
|
def _merge_call_routing_meta(*, caller_meta: Any, engine_meta: Any) -> dict[str, Any]:
|
|
caller = _normalize_routing_meta(caller_meta)
|
|
engine = _normalize_routing_meta(engine_meta)
|
|
merged = dict(caller)
|
|
for key, value in engine.items():
|
|
if value is None or value == "":
|
|
continue
|
|
merged[key] = value
|
|
if engine.get("slot_degraded"):
|
|
merged["slot_degraded"] = True
|
|
if engine.get("prompt_format_error"):
|
|
merged["prompt_format_error"] = True
|
|
return _normalize_routing_meta(merged)
|
|
|
|
|
|
def _merge_engine_routing_meta(engine: dict[str, Any], **updates: Any) -> dict[str, Any]:
|
|
runtime = dict(engine or {})
|
|
current = _read_engine_routing_meta(runtime)
|
|
for key, value in updates.items():
|
|
if value is None:
|
|
continue
|
|
current[key] = value
|
|
runtime["_routing_meta"] = _normalize_routing_meta(current)
|
|
return runtime
|
|
|
|
|
|
def _routing_meta_log_text(meta: Any) -> str:
|
|
data = _normalize_routing_meta(meta)
|
|
parts = []
|
|
if data.get("planned_tier"):
|
|
parts.append(f"planned_tier={data['planned_tier']}")
|
|
if data.get("executed_tier"):
|
|
parts.append(f"executed_tier={data['executed_tier']}")
|
|
if data.get("route_source"):
|
|
parts.append(f"source={data['route_source']}")
|
|
if data.get("route_rule_name"):
|
|
parts.append(f"rule={data['route_rule_name']}")
|
|
if data.get("routing_decision"):
|
|
parts.append(f"decision={data['routing_decision']}")
|
|
if data.get("slot_key"):
|
|
parts.append(f"slot_key={data['slot_key']}")
|
|
if data.get("slot_version"):
|
|
parts.append(f"slot_version={data['slot_version']}")
|
|
if data.get("slot_runtime_source"):
|
|
parts.append(f"slot_runtime_source={data['slot_runtime_source']}")
|
|
if data.get("slot_fallback_index") is not None:
|
|
parts.append(f"slot_fallback_index={data['slot_fallback_index']}")
|
|
if data.get("slot_degraded"):
|
|
parts.append("slot_degraded=true")
|
|
return ", ".join(parts)
|
|
|
|
def _env_bool(name: str, default: bool = False) -> bool:
|
|
raw = os.getenv(name)
|
|
if raw is None:
|
|
return default
|
|
return str(raw).strip().lower() in BOOL_TRUE_VALUES
|
|
|
|
def _parse_csv_items(raw: str) -> tuple[str, ...]:
|
|
rows = [item.strip().lower() for item in str(raw or "").split(",") if item.strip()]
|
|
return tuple(rows)
|
|
|
|
def _cn_now_text() -> str:
|
|
cn_tz = timezone(timedelta(hours=8))
|
|
return datetime.now(cn_tz).strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
def _cn_now_text_ms() -> str:
|
|
cn_tz = timezone(timedelta(hours=8))
|
|
return datetime.now(cn_tz).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
|
|
|
def _runtime_log(scope: str, message: str) -> None:
|
|
print(f"[{_cn_now_text()}][{scope}] {message}")
|
|
|
|
def _is_control_plane_timeout_message(text: str) -> bool:
|
|
lowered = str(text or "").strip().lower()
|
|
if not lowered:
|
|
return False
|
|
timeout_signals = (
|
|
"request timed out before a response was generated",
|
|
"agents.defaults.timeoutseconds",
|
|
"timeoutseconds",
|
|
"before a response was generated",
|
|
)
|
|
return any(signal in lowered for signal in timeout_signals)
|
|
|
|
@dataclass
|
|
class ChatCompletionResult:
|
|
text: str
|
|
raw_request: Any = None
|
|
raw_response: Any = None
|
|
provider_attempts: Optional[list[dict[str, Any]]] = None
|
|
|
|
@dataclass
|
|
class StructuredExtractionResult:
|
|
payload: Optional[dict[str, Any]]
|
|
source_field: str
|
|
provider: str
|
|
model: str
|
|
error_code: Optional[str] = None
|
|
raw_text: str = ""
|
|
validation: str = "invalid"
|
|
from_cache: bool = False
|
|
|
|
|
|
__all__ = [name for name in globals() if not name.startswith("__")]
|