216 lines
8.7 KiB
Python
216 lines
8.7 KiB
Python
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 _http_status_retry_budget_for_function(function_name: str) -> int:
|
||
fn = str(function_name or "").strip().lower()
|
||
is_intent_function = fn in {
|
||
"classify_intent_ai_pmo_kernel",
|
||
"classify_intent_complex",
|
||
"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 in {"classify_intent_ai_pmo_kernel", "classify_intent_complex"}:
|
||
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) -> 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)
|