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

512 lines
21 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.

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 _is_low_tier_function(self, function_name: str) -> bool:
rule = self._match_openclaw_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) == "low"
@staticmethod
def _looks_like_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) -> bool:
if not self._low_to_high_escalation_enabled():
return False
if not self._is_low_tier_function(function_name):
return False
return self._looks_like_unanswerable_response(response_text)
def _should_retry_low_tier_with_high_tier(
self,
*,
function_name: str,
failed_engine: dict[str, Any],
) -> bool:
if not self._low_to_high_escalation_enabled():
return False
_ = failed_engine
return self._is_low_tier_function(function_name)
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_openclaw_high_tier_rule_for_escalation(self, function_name: str) -> Optional[dict[str, Any]]:
bundle = self._load_openclaw_routing_bundle()
rule = find_matching_rule(
function_name,
tier_filter={"high"},
bundle=bundle,
)
if not isinstance(rule, dict):
_runtime_log("LLM路由", f"OpenClaw高阶规则未命中:function={function_name}")
return None
_runtime_log(
"LLM路由",
(
"OpenClaw高阶规则候选:"
f"function={function_name}, rule={rule.get('name') or 'unnamed'}, priority={rule.get('priority')}"
),
)
return rule
async def _resolve_openclaw_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],
) -> Optional[dict[str, Any]]:
rule = self._match_openclaw_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,
)
if not isinstance(routed, dict):
_runtime_log(
"LLM路由",
(
"OpenClaw高阶规则命中但未解析到可用引擎:"
f"function={function_name}, rule={rule.get('name') or 'unnamed'}"
),
)
return None
routed = self._ensure_openclaw_control_plane_engine(routed)
if self._same_runtime_engine(routed, failed_engine):
_runtime_log(
"LLM路由",
(
"OpenClaw高阶升级命中但与失败引擎同源,跳过:"
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="openclaw_high_tier_failover",
)
except Exception as exc:
_runtime_log(
"LLM路由",
(
"OpenClaw高阶升级被拦截:"
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_openclaw_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路由",
(
"OpenClaw高阶升级命中:"
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,
) -> 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):
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)]
active_engine = self._resolve_active_engine_from_bundle(bundle)
_runtime_log(
"LLM路由",
(
"低阶模型失败准备升级高阶:"
f"function={function_name}, from_tier=low, to_tier=high, reason={str(error)[:120]}"
),
)
high_engine = await self._resolve_openclaw_high_tier_engine_for_escalation(
bundle=bundle,
engines=engines,
active_engine=active_engine,
function_name=function_name,
failed_engine=failed_engine,
)
if not isinstance(high_engine, dict):
_runtime_log(
"LLM路由",
(
"OpenClaw高阶升级未命中,回退通用 high tier:"
f"function={function_name}, reason={str(error)[:120]}"
),
)
high_engine = self._resolve_tier_engine(
tier="high",
engines=engines,
active_engine=active_engine,
)
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]}"
),
)
text = await self._chat_completion(
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,
)
if not text.strip():
raise ValueError("high_tier_empty_response")
if self._is_invalid_structured_output(function_name=function_name, response_text=text):
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",
)
_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,
) -> 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,
):
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)]
active_engine = self._resolve_active_engine_from_bundle(bundle)
_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_openclaw_high_tier_engine_for_escalation(
bundle=bundle,
engines=engines,
active_engine=active_engine,
function_name=function_name,
failed_engine=failed_engine,
)
if not isinstance(high_engine, dict):
_runtime_log(
"LLM路由",
(
"OpenClaw高阶结构化升级未命中,回退通用 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,
)
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,
)
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,
)
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",
)
_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