PMFlow/backend/app/services/llm_service/shared_part_02.py

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("__")]