from __future__ import annotations from .shared import * class LLMGuardrailsPromptMixin: @staticmethod def _requires_ddl_api_guardrail(function_name: str) -> bool: fn = str(function_name or "").strip().lower() if not fn: return False return fn in DDL_API_GUARDRAIL_FUNCTION_EXACT @classmethod def _inject_function_guardrails(cls, *, system_prompt: str, function_name: str) -> str: base_prompt = str(system_prompt or "").strip() if not cls._requires_ddl_api_guardrail(function_name): return base_prompt extra = str(DDL_API_GUARDRAIL_SYSTEM_PROMPT or "").strip() if not extra: return base_prompt if extra in base_prompt: return base_prompt if not base_prompt: return extra return f"{base_prompt}\n\n{extra}" @classmethod def _merge_prompt_with_profile( cls, *, prompt: str, prompt_profile: dict[str, str], function_name: str = "", ) -> str: system_prompt = cls._inject_function_guardrails( system_prompt=str(prompt_profile.get("system_prompt") or "").strip(), function_name=function_name, ) if not system_prompt: return prompt role_name = str(prompt_profile.get("role_name") or "unknown").strip() or "unknown" return f"[system_role:{role_name}]\n{system_prompt}\n\n{prompt}" def _direct_emergency_enabled(self) -> bool: return _env_bool("LLM_DIRECT_EMERGENCY_ENABLED", False) @staticmethod def _should_raise_model_error(function_name: str) -> bool: fn = str(function_name or "").strip().lower() if not fn: return False if fn in STRICT_MODEL_ERROR_FUNCTION_EXACT: return True return any(prefix and fn.startswith(prefix) for prefix in STRICT_MODEL_ERROR_FUNCTION_PREFIXES) @staticmethod def _build_model_error_message( *, function_name: str, routing_meta: dict[str, Any] | None = None, error: Exception, ) -> str: clean_error = re.sub(r"\s+", " ", str(error or "")).strip() if len(clean_error) > 160: clean_error = f"{clean_error[:160]}..." fn_text = str(function_name or "llm_generate").strip() or "llm_generate" meta = _normalize_routing_meta(routing_meta) if not clean_error: clean_error = "upstream_empty_or_invalid_response" route_bits = _routing_meta_log_text(meta) if route_bits: return f"模型调用失败:function={fn_text}, {route_bits}, reason={clean_error}" return f"模型调用失败:function={fn_text}, reason={clean_error}" @staticmethod def _structured_cache_enabled(function_name: str) -> bool: fn = str(function_name or "").strip().lower() return fn in { "signal_source_onboarding_plan", } def _request_timeout_for_function( self, function_name: str, *, response_mode: str = "complete", routing_meta: dict[str, Any] | None = None, ) -> float: meta = _normalize_routing_meta(routing_meta) if meta.get("fallback_policy") != "fast_local": return self.request_timeout env_key = "LLM_FAST_LOCAL_FALLBACK_TIMEOUT_COMPLETE" default_timeout = FAST_LOCAL_FALLBACK_COMPLETE_TIMEOUT_SECONDS if meta.get("execution_mode") == "local_planner" and str(response_mode or "").strip().lower() == "fast": env_key = "LLM_FAST_LOCAL_FALLBACK_TIMEOUT_FAST" default_timeout = FAST_LOCAL_FALLBACK_FAST_TIMEOUT_SECONDS try: configured = float(os.getenv(env_key, str(default_timeout))) except Exception: configured = default_timeout if configured <= 0: return self.request_timeout return min(self.request_timeout, configured) @staticmethod def _is_openclaw_gateway_request(*, provider_name: str, base_url: str) -> bool: if str(provider_name or "").strip().lower() != "openai": return False normalized_base_url = str(base_url or "").strip().lower() if not normalized_base_url: return False return "openclaw" in normalized_base_url @staticmethod def _openclaw_agents_timeout_seconds() -> int: raw = os.getenv( "OPENCLAW_AGENTS_DEFAULT_TIMEOUT_SECONDS", str(OPENCLAW_AGENTS_DEFAULT_TIMEOUT_SECONDS), ) try: configured = int(str(raw).strip()) except Exception: configured = OPENCLAW_AGENTS_DEFAULT_TIMEOUT_SECONDS if configured <= 0: configured = OPENCLAW_AGENTS_DEFAULT_TIMEOUT_SECONDS return min(configured, OPENCLAW_AGENTS_TIMEOUT_SECONDS_MAX) @staticmethod def _http_status_retry_budget_for_function(function_name: str) -> int: fn = str(function_name or "").strip().lower() is_intent_function = fn in { "pmflow_semantic_context_decision", "intent_structured_runtime_failover", } env_key = "LLM_INTENT_HTTP_RETRY_BUDGET" if is_intent_function else "LLM_HTTP_STATUS_RETRY_BUDGET" default_budget = "0" if is_intent_function else "2" raw = str(os.getenv(env_key, default_budget) or default_budget).strip() try: parsed = int(raw) except Exception: parsed = int(default_budget) return max(0, min(parsed, 3)) @staticmethod def _empty_chat_retry_budget(*, function_name: str, current_max_tokens: int) -> int: fn = str(function_name or "").strip().lower() current = max(int(current_max_tokens or 0), 1) if current >= 2800: return 0 if fn == "pmflow_semantic_context_decision": raw_budget = str(os.getenv("LLM_INTENT_EMPTY_RETRY_BUDGET", "2") or "2").strip() try: parsed_budget = int(raw_budget) except Exception: parsed_budget = 2 return max(0, min(parsed_budget, 3)) if fn.startswith("adaptive_rag_answer_"): # RAG 问答在 GLM 系列上经常先返回 reasoning_content,需至少升档两次才能拿到最终 content。 return 2 if any(token in fn for token in ("qa", "query", "reply")): return 1 return 1 @staticmethod def _should_retry_empty_chat_response( *, structured_output: bool, message: Any, choice: Any, current_max_tokens: int, ) -> bool: if int(current_max_tokens or 0) >= 2800: return False finish_reason = str(choice.get("finish_reason") or "").strip().lower() if isinstance(choice, dict) else "" if finish_reason not in {"length", "max_tokens"}: return False reasoning_text = "" if isinstance(message, dict): reasoning_text = str(message.get("reasoning_content") or message.get("reasoning") or "").strip() if not reasoning_text and isinstance(choice, dict): reasoning_text = str(choice.get("reasoning_content") or choice.get("reasoning") or "").strip() return bool(reasoning_text) @staticmethod def _next_retry_max_tokens_for_empty_chat(current_max_tokens: int) -> int: current = max(int(current_max_tokens or 0), 1) grown = max(current + 512, int(current * 1.8)) return min(grown, 3000) def _use_provider_circuit_breaker(self, function_name: str, routing_meta: dict[str, Any] | None = None) -> bool: fn = str(function_name or "").strip().lower() if any(fn.startswith(prefix) for prefix in CIRCUIT_BREAKER_EXEMPT_FUNCTION_PREFIXES): return False meta = _normalize_routing_meta(routing_meta) return meta.get("fallback_policy") != "fast_local" def _structured_output_required( self, function_name: str, routing_meta: dict[str, Any] | None = None, ) -> bool: meta = _normalize_routing_meta(routing_meta) if meta.get("response_contract") == "structured": return True if meta.get("response_contract") == "freeform": return False fn = str(function_name or "").strip().lower() if not fn: return False return fn in STRUCTURED_OBJECT_SCHEMAS