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 _prefer_fast_local_fallback(function_name: str) -> bool: fn = str(function_name or "").strip().lower() if not fn: return False return any(hint and hint in fn for hint in FAST_LOCAL_FALLBACK_FUNCTION_HINTS) @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, provider: str, model: str, error: Exception, ) -> str: clean_error = re.sub(r"\s+", " ", str(error or "")).strip() if len(clean_error) > 160: clean_error = f"{clean_error[:160]}..." provider_text = str(provider or "unknown").strip() or "unknown" model_text = str(model or "unknown").strip() or "unknown" fn_text = str(function_name or "llm_generate").strip() or "llm_generate" if not clean_error: clean_error = "upstream_empty_or_invalid_response" return ( f"模型调用失败:function={fn_text}, provider={provider_text}, model={model_text}, " f"reason={clean_error}" ) @staticmethod def _is_fast_local_planner_function(function_name: str) -> bool: fn = str(function_name or "").strip().lower() return any(hint and hint in fn for hint in FAST_LOCAL_PLANNER_FUNCTION_HINTS) @staticmethod def _is_fast_local_worker_function(function_name: str) -> bool: fn = str(function_name or "").strip().lower() return any(hint and hint in fn for hint in FAST_LOCAL_WORKER_FUNCTION_HINTS) @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") -> float: if not self._prefer_fast_local_fallback(function_name): return self.request_timeout env_key = "LLM_FAST_LOCAL_FALLBACK_TIMEOUT_COMPLETE" default_timeout = FAST_LOCAL_FALLBACK_COMPLETE_TIMEOUT_SECONDS if self._is_fast_local_planner_function(function_name) 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 _should_retry_empty_chat_response( *, structured_output: bool, message: Any, choice: Any, current_max_tokens: int, ) -> bool: if structured_output: return False 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) -> bool: fn = str(function_name or "").strip().lower() if any(fn.startswith(prefix) for prefix in CIRCUIT_BREAKER_EXEMPT_FUNCTION_PREFIXES): return False return not self._prefer_fast_local_fallback(function_name) def _structured_output_required(self, function_name: str) -> bool: fn = str(function_name or "").strip().lower() if not fn: return False custom_hints = _parse_csv_items(os.getenv("LLM_STRUCTURED_OUTPUT_FUNCTIONS", "")) hint_pool = custom_hints or STRUCTURED_OUTPUT_FUNCTION_HINTS return any(hint and hint in fn for hint in hint_pool)