from __future__ import annotations from .shared import * class LLMTierEscalationMixin: def _low_to_high_escalation_enabled(self) -> bool: return _env_bool("LLM_LOW_TO_HIGH_ESCALATION_ENABLED", True) @staticmethod def _same_runtime_engine(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() ) def _call_is_low_tier_function( self, function_name: str, *, routing_meta: dict[str, Any] | None = None, ) -> bool: try: return bool(self._is_low_tier_function(function_name, routing_meta=routing_meta)) except TypeError: return bool(self._is_low_tier_function(function_name)) def _call_is_invalid_structured_output_for_retry( self, *, function_name: str, response_text: str, routing_meta: dict[str, Any] | None = None, ) -> bool: wrapper = getattr(self, "_call_is_invalid_structured_output", None) if callable(wrapper): return bool( wrapper( function_name=function_name, response_text=response_text, routing_meta=routing_meta, ) ) checker = getattr(self, "_is_invalid_structured_output", None) if not callable(checker): return False try: return bool( checker( function_name=function_name, response_text=response_text, routing_meta=routing_meta, ) ) except TypeError: return bool( checker( function_name=function_name, response_text=response_text, ) ) def _is_low_tier_function(self, function_name: str, routing_meta: dict[str, Any] | None = None) -> bool: rule = self._match_control_plane_rule(function_name) if rule is not None: tier = str(rule.get("tier") or "").strip().lower() if tier: return tier == "low" return self._resolve_function_tier(function_name, routing_meta=routing_meta) == "low" @staticmethod def _is_unanswerable_response(text: str) -> bool: cleaned = str(text or "").strip() lowered = cleaned.lower() if not lowered: return True if len(cleaned) <= 8: return True return any(marker in lowered for marker in LOW_TO_HIGH_ESCALATION_FALLBACK_MARKERS) @staticmethod def _normalize_model_name(model_name: str) -> str: return re.sub(r"[\s_]+", "", str(model_name or "")).lower() @staticmethod def _is_tier_default_model(model_name: str, *, tier: str) -> bool: normalized = LLMTierEscalationMixin._normalize_model_name(model_name) if not normalized: return False tier_name = str(tier or "").strip().lower() expected = LLMTierEscalationMixin._normalize_model_name( str(TIER_DEFAULTS.get(tier_name, {}).get("model") or "") ) if not expected: return False return normalized == expected def _should_escalate_low_to_high( self, *, function_name: str, response_text: str, routing_meta: dict[str, Any] | None = None, ) -> bool: if not self._low_to_high_escalation_enabled(): return False if not self._call_is_low_tier_function(function_name, routing_meta=routing_meta): return False return self._is_unanswerable_response(response_text) def _should_retry_low_tier_with_high_tier( self, *, function_name: str, failed_engine: dict[str, Any], routing_meta: dict[str, Any] | None = None, ) -> bool: if not self._low_to_high_escalation_enabled(): return False _ = failed_engine return self._call_is_low_tier_function(function_name, routing_meta=routing_meta) def _prefer_high_tier_retry_engine( self, *, bundle: dict[str, Any], engines: list[dict[str, Any]], function_name: str, failed_engine: dict[str, Any], current_engine: dict[str, Any], ) -> Optional[dict[str, Any]]: if self._is_tier_default_model(str(current_engine.get("model") or ""), tier="high"): return current_engine settings = self._normalize_engine_settings(bundle.get("settings")) for item in engines: candidate = dict(item) if not self._is_tier_default_model(str(candidate.get("model") or ""), tier="high"): continue if self._same_runtime_engine(candidate, failed_engine): continue try: candidate = self._ensure_engine_allowed_for_function( engine=candidate, function_name=function_name, settings=settings, engine_label="forced_high_tier_retry", ) except Exception as exc: _runtime_log( "LLM路由", ( "高阶重试候选被拦截:" f"function={function_name}, error={type(exc).__name__}:{str(exc)[:180]}" ), ) continue _runtime_log( "LLM路由", ( "高阶重试候选命中:" f"function={function_name}, tier=high, decision=forced_high_retry" ), ) return candidate return None def _match_control_plane_high_tier_rule_for_escalation(self, function_name: str) -> Optional[dict[str, Any]]: bundle = self._load_control_plane_routing_bundle() rule = find_matching_rule( function_name, tier_filter={"high"}, bundle=bundle, ) if not isinstance(rule, dict): _runtime_log("LLM路由", f"PMFlow 本地路由层高阶规则未命中:function={function_name}") return None _runtime_log( "LLM路由", ( "PMFlow 本地路由层高阶规则候选:" f"function={function_name}, rule={rule.get('name') or 'unnamed'}, priority={rule.get('priority')}" ), ) return rule async def _resolve_control_plane_high_tier_engine_for_escalation( self, *, bundle: dict[str, Any], engines: list[dict[str, Any]], active_engine: dict[str, Any], function_name: str, failed_engine: dict[str, Any], slot_bundle: dict[str, Any] | None = None, ) -> Optional[dict[str, Any]]: rule = self._match_control_plane_high_tier_rule_for_escalation(function_name) if not isinstance(rule, dict): return None routed = self._resolve_engine_from_rule( rule=rule, engines=engines, active_engine=active_engine, slot_bundle=slot_bundle, ) if not isinstance(routed, dict): _runtime_log( "LLM路由", ( "PMFlow 本地路由层高阶规则命中但未解析到可用引擎:" f"function={function_name}, rule={rule.get('name') or 'unnamed'}" ), ) return None routed = self._ensure_control_plane_engine(routed) if self._same_runtime_engine(routed, failed_engine): _runtime_log( "LLM路由", ( "PMFlow 本地路由层高阶升级命中但与失败引擎同源,跳过:" f"function={function_name}, tier=high" ), ) return None try: routed = self._ensure_engine_allowed_for_function( engine=routed, function_name=function_name, settings=self._normalize_engine_settings(bundle.get("settings")), engine_label="control_plane_high_tier_failover", ) except Exception as exc: _runtime_log( "LLM路由", ( "PMFlow 本地路由层高阶升级被拦截:" f"function={function_name}, rule={rule.get('name') or 'unnamed'}, " f"error={type(exc).__name__}:{str(exc)[:180]}" ), ) return None config_snapshot = build_routing_config_snapshot(bundle=self._load_control_plane_routing_bundle()) routed = _merge_engine_routing_meta( routed, tier_alias="high", planned_tier="high", executed_tier="high", route_source="default", route_rule_name=str(rule.get("name") or "").strip(), rule_priority=int(rule.get("priority") or 0), routing_decision="high_tier_retry_candidate", routing_config_version=config_snapshot.get("routing_config_version"), routing_config_updated_at=config_snapshot.get("routing_config_updated_at"), ) _runtime_log( "LLM路由", ( "PMFlow 本地路由层高阶升级命中:" f"function={function_name}, rule={rule.get('name') or 'unnamed'}, tier=high" ), ) return routed async def _retry_with_high_tier_after_low_model_failure( self, *, db: AsyncSession, tenant_id: int, function_name: str, failed_engine: dict[str, Any], prompt: str, system_prompt: str, temperature: float, max_tokens: int, response_mode: str, error: Exception, routing_meta: dict[str, Any] | None = None, ) -> tuple[Optional[str], Optional[dict[str, Any]]]: if not self._should_retry_low_tier_with_high_tier( function_name=function_name, failed_engine=failed_engine, routing_meta=routing_meta, ): return None, None try: _, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id) engines = [ item for item in bundle.get("engines", []) if isinstance(item, dict) and self._engine_enabled(item) ] active_engine = self._resolve_active_engine_from_bundle(bundle) slot_bundle = await self._load_capability_slot_bundle(db) _runtime_log( "LLM路由", ( "低阶模型失败准备升级高阶:" f"function={function_name}, from_tier=low, to_tier=high, reason={str(error)[:120]}" ), ) high_engine = await self._resolve_control_plane_high_tier_engine_for_escalation( bundle=bundle, engines=engines, active_engine=active_engine, function_name=function_name, failed_engine=failed_engine, slot_bundle=slot_bundle, ) if not isinstance(high_engine, dict): _runtime_log( "LLM路由", ( "PMFlow 本地路由层高阶升级未命中,回退通用 high tier:" f"function={function_name}, reason={str(error)[:120]}" ), ) high_engine = self._resolve_tier_engine( tier="high", engines=engines, active_engine=active_engine, slot_bundle=slot_bundle, ) if not isinstance(high_engine, dict): return None, None high_engine = self._ensure_engine_allowed_for_function( engine=high_engine, function_name=function_name, settings=self._normalize_engine_settings(bundle.get("settings")), engine_label="high_tier_failover", ) forced_high_tier_engine = self._prefer_high_tier_retry_engine( bundle=bundle, engines=engines, function_name=function_name, failed_engine=failed_engine, current_engine=high_engine, ) if isinstance(forced_high_tier_engine, dict): high_engine = forced_high_tier_engine failed_provider = str(failed_engine.get("provider") or "").strip().lower() failed_model = str(failed_engine.get("model") or "").strip() high_provider = str(high_engine.get("provider") or "").strip().lower() high_model = str(high_engine.get("model") or "").strip() if failed_provider == high_provider and failed_model == high_model: _runtime_log( "LLM路由", ( "高阶升级引擎与失败引擎相同,跳过:" f"function={function_name}, tier=high" ), ) return None, None _runtime_log( "LLM路由", ( "低阶模型失败自动升级:" f"function={function_name}, planned_tier=low, executed_tier=high, " f"decision=low_to_high_escalated, reason={str(error)[:120]}" ), ) completion = await self._chat_completion_raw( provider=high_provider, prompt=prompt, system_prompt=system_prompt, api_key=str(high_engine.get("api_key") or ""), base_url=str(high_engine.get("base_url") or ""), model=high_model, temperature=temperature, max_tokens=max_tokens, function_name=function_name, response_mode=response_mode, routing_meta=routing_meta, ) text = completion.text if not text.strip(): raise ValueError("high_tier_empty_response") if self._call_is_invalid_structured_output_for_retry( function_name=function_name, response_text=text, routing_meta=routing_meta, ): raise ValueError("high_tier_invalid_structured_output") high_engine = _merge_engine_routing_meta( high_engine, tier_alias="low", planned_tier="low", executed_tier="high", routing_decision="low_to_high_escalated", ) high_engine["_provider_request_payload"] = completion.raw_request high_engine["_provider_response_payload"] = completion.raw_response high_engine["_provider_attempts"] = completion.provider_attempts _runtime_log( "LLM路由", ( "高阶升级成功:" f"function={function_name}, {_routing_meta_log_text(_read_engine_routing_meta(high_engine))}, text_len={len(text)}" ), ) return text, high_engine except Exception as retry_exc: _runtime_log( "LLM路由", ( "低阶模型失败升级高阶也失败:" f"function={function_name}, error={type(retry_exc).__name__}:{str(retry_exc)[:180]}" ), ) return None, None async def _retry_structured_with_high_tier_after_low_model_failure( self, *, db: AsyncSession, tenant_id: int, function_name: str, failed_engine: dict[str, Any], prompt: str, system_prompt: str, temperature: float, max_tokens: int, response_mode: str, error: Exception, routing_meta: dict[str, Any] | None = None, ) -> tuple[Optional[StructuredExtractionResult], Optional[dict[str, Any]]]: fn = str(function_name or "").strip().lower() force_intent_retry = False if not self._should_retry_low_tier_with_high_tier( function_name=function_name, failed_engine=failed_engine, routing_meta=routing_meta, ): return None, None try: _, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id) engines = [ item for item in bundle.get("engines", []) if isinstance(item, dict) and self._engine_enabled(item) ] active_engine = self._resolve_active_engine_from_bundle(bundle) slot_bundle = await self._load_capability_slot_bundle(db) _runtime_log( "LLM路由", ( "低阶结构化失败准备升级高阶:" f"function={function_name}, from_tier=low, to_tier=high, reason={str(error)[:120]}, " f"force_intent_retry={force_intent_retry}" ), ) high_engine = await self._resolve_control_plane_high_tier_engine_for_escalation( bundle=bundle, engines=engines, active_engine=active_engine, function_name=function_name, failed_engine=failed_engine, slot_bundle=slot_bundle, ) if not isinstance(high_engine, dict): _runtime_log( "LLM路由", ( "PMFlow 本地路由层高阶结构化升级未命中,回退通用 high tier:" f"function={function_name}, reason={str(error)[:120]}, " f"force_intent_retry={force_intent_retry}" ), ) high_engine = self._resolve_tier_engine( tier="high", engines=engines, active_engine=active_engine, slot_bundle=slot_bundle, ) if not isinstance(high_engine, dict): return None, None high_engine = self._ensure_engine_allowed_for_function( engine=high_engine, function_name=function_name, settings=self._normalize_engine_settings(bundle.get("settings")), engine_label="high_tier_structured_failover", ) forced_high_tier_engine = self._prefer_high_tier_retry_engine( bundle=bundle, engines=engines, function_name=function_name, failed_engine=failed_engine, current_engine=high_engine, ) if isinstance(forced_high_tier_engine, dict): high_engine = forced_high_tier_engine failed_provider = str(failed_engine.get("provider") or "").strip().lower() failed_model = str(failed_engine.get("model") or "").strip() high_provider = str(high_engine.get("provider") or "").strip().lower() high_model = str(high_engine.get("model") or "").strip() if failed_provider == high_provider and failed_model == high_model: if not force_intent_retry: _runtime_log( "LLM路由", ( "高阶结构化升级引擎与失败引擎相同,跳过:" f"function={function_name}, tier=high" ), ) return None, None fallback_engine: Optional[dict[str, Any]] = None for fallback_tier in ("low",): candidate = self._resolve_tier_engine( tier=fallback_tier, engines=engines, active_engine=active_engine, slot_bundle=slot_bundle, ) if not isinstance(candidate, dict): continue candidate = self._ensure_engine_allowed_for_function( engine=candidate, function_name=function_name, settings=self._normalize_engine_settings(bundle.get("settings")), engine_label=f"intent_structured_failover_{fallback_tier}", ) candidate_provider = str(candidate.get("provider") or "").strip().lower() candidate_model = str(candidate.get("model") or "").strip() if candidate_provider == failed_provider and candidate_model == failed_model: continue fallback_engine = candidate break if fallback_engine is None: return None, None high_engine = fallback_engine high_provider = str(high_engine.get("provider") or "").strip().lower() high_model = str(high_engine.get("model") or "").strip() _runtime_log( "LLM路由", ( "低阶结构化失败自动升级:" f"function={function_name}, planned_tier=low, executed_tier=high, " f"decision=low_to_high_escalated, reason={str(error)[:120]}, " f"force_intent_retry={force_intent_retry}" ), ) completion = await self._chat_completion_raw( provider=high_provider, prompt=prompt, system_prompt=system_prompt, api_key=str(high_engine.get("api_key") or ""), base_url=str(high_engine.get("base_url") or ""), model=high_model, temperature=temperature, max_tokens=max_tokens, function_name=( "intent_structured_runtime_failover" if force_intent_retry else function_name ), response_mode=response_mode, routing_meta=routing_meta, ) extraction = self._extract_structured_object_from_response( function_name=function_name, response=completion.raw_response, fallback_text=completion.text, provider=high_provider, model=high_model, ) if extraction.payload is None: raise ValueError(str(extraction.error_code or "structured_schema_invalid")) _runtime_log( "LLM结构化", ( f"function={function_name}, structured_source={extraction.source_field or 'none'}, " f"validation={extraction.validation}, fallback=true" ), ) high_engine = _merge_engine_routing_meta( high_engine, tier_alias="low", planned_tier="low", executed_tier="high", routing_decision="low_to_high_escalated", ) high_engine["_provider_request_payload"] = completion.raw_request high_engine["_provider_response_payload"] = completion.raw_response high_engine["_provider_attempts"] = completion.provider_attempts _runtime_log( "LLM路由", ( "高阶结构化升级成功:" f"function={function_name}, {_routing_meta_log_text(_read_engine_routing_meta(high_engine))}, " f"structured_source={extraction.source_field or 'none'}" ), ) return extraction, high_engine except Exception as retry_exc: _runtime_log( "LLM路由", ( "低阶结构化失败升级高阶也失败:" f"function={function_name}, error={type(retry_exc).__name__}:{str(retry_exc)[:180]}" ), ) return None, None async def _retry_structured_with_slot_fallback_after_primary_failure( self, *, db: AsyncSession, tenant_id: int, function_name: str, failed_engine: dict[str, Any], prompt: str, system_prompt: str, temperature: float, max_tokens: int, response_mode: str, error: Exception, routing_meta: dict[str, Any] | None = None, ) -> tuple[Optional[StructuredExtractionResult], Optional[dict[str, Any]]]: failed_meta = _read_engine_routing_meta(failed_engine) if str(failed_meta.get("planned_tier") or "").strip().lower() != "structured": return None, None try: _, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id) settings = self._normalize_engine_settings(bundle.get("settings")) engines = [ item for item in bundle.get("engines", []) if isinstance(item, dict) and self._engine_enabled(item) ] slot_bundle = await self._load_capability_slot_bundle(db) slot_version = self._slot_bundle_active_version(slot_bundle) failed_engine_id = str(failed_engine.get("id") or "").strip() failed_provider = str(failed_engine.get("provider") or "").strip().lower() failed_model = str(failed_engine.get("model") or "").strip() candidates = self._resolve_engines_from_slot_binding( tier="structured", engines=engines, slot_bundle=slot_bundle, ) for candidate in candidates: candidate_id = str(candidate.get("id") or "").strip() candidate_provider = str(candidate.get("provider") or "").strip().lower() candidate_model = str(candidate.get("model") or "").strip() if candidate_id and candidate_id == failed_engine_id: continue if candidate_provider == failed_provider and candidate_model == failed_model: continue candidate = self._ensure_engine_allowed_for_function( engine=candidate, function_name=function_name, settings=settings, engine_label="structured_slot_fallback", ) candidate = _merge_engine_routing_meta( candidate, tier_alias="structured", planned_tier="structured", executed_tier="structured", route_source="slot_binding", routing_decision="capability_slot_fallback", slot_key="structured", slot_version=slot_version, slot_runtime_source="active_slot", slot_fallback_index=candidate.get("_slot_fallback_index", 0), slot_degraded=False, ) _runtime_log( "LLM路由", ( "结构化槽 fallback 重试:" f"function={function_name}, failed={failed_engine_id or failed_provider}, " f"fallback={candidate_id or candidate_provider}, reason={str(error)[:120]}" ), ) completion = await self._chat_completion_raw( provider=candidate_provider, prompt=prompt, system_prompt=system_prompt, api_key=str(candidate.get("api_key") or ""), base_url=str(candidate.get("base_url") or ""), model=candidate_model, temperature=temperature, max_tokens=max_tokens, function_name=function_name, response_mode=response_mode, routing_meta=_merge_call_routing_meta( caller_meta=routing_meta, engine_meta=_read_engine_routing_meta(candidate), ), ) extraction = self._extract_structured_object_from_response( function_name=function_name, response=completion.raw_response, fallback_text=completion.text, provider=candidate_provider, model=candidate_model, ) if extraction.payload is None: _runtime_log( "LLM结构化", ( "结构化槽 fallback 返回无效结构:" f"function={function_name}, fallback={candidate_id or candidate_provider}, " f"error={extraction.error_code or 'structured_schema_invalid'}" ), ) continue candidate["_provider_request_payload"] = completion.raw_request candidate["_provider_response_payload"] = completion.raw_response candidate["_provider_attempts"] = completion.provider_attempts _runtime_log( "LLM结构化", ( f"function={function_name}, structured_source={extraction.source_field or 'none'}, " "validation=ok, fallback=structured_slot" ), ) return extraction, candidate return None, None except Exception as retry_exc: _runtime_log( "LLM路由", ( "结构化槽 fallback 链失败:" f"function={function_name}, error={type(retry_exc).__name__}:{str(retry_exc)[:180]}" ), ) return None, None