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

190 lines
6.9 KiB
Python

from __future__ import annotations
from .shared_part_01 import *
def _normalize_provider(raw: Optional[str]) -> str:
provider = str(raw or "").strip().lower()
provider = PROVIDER_ALIASES.get(provider, provider)
if provider not in SUPPORTED_PROVIDERS:
raise ValueError("provider 仅支持 deepseek/glm/openai/ollama")
return provider
def _mask_secret(secret: str) -> str:
value = str(secret or "").strip()
if not value:
return ""
if len(value) <= 8:
return "*" * len(value)
return f"{value[:4]}...{value[-4:]}"
def _safe_json(value: Any) -> dict[str, Any]:
if isinstance(value, dict):
try:
# 返回深拷贝,避免原地修改导致 SQLAlchemy 不感知 JSON 字段变更
return json.loads(json.dumps(value))
except Exception:
return dict(value)
if isinstance(value, str):
try:
parsed = json.loads(value)
return parsed if isinstance(parsed, dict) else {}
except Exception:
return {}
return {}
def _optional_tier_alias(raw: Any) -> str:
value = str(raw or "").strip().lower()
return value if value in VALID_TIER_ALIASES else ""
def _optional_response_contract(raw: Any) -> str:
value = str(raw or "").strip().lower()
return value if value in {"structured", "freeform"} else ""
def _optional_fallback_policy(raw: Any) -> str:
value = str(raw or "").strip().lower()
return value if value in {"fast_local", "default", "disabled"} else ""
def _optional_execution_mode(raw: Any) -> str:
value = str(raw or "").strip().lower()
return value if value in {"remote", "local_planner", "local_worker"} else ""
def _normalize_routing_meta(raw: Any) -> dict[str, Any]:
payload = _safe_json(raw)
planned_tier = _optional_tier_alias(payload.get("planned_tier"))
executed_tier = _optional_tier_alias(payload.get("executed_tier")) or planned_tier
tier_alias = _optional_tier_alias(payload.get("tier_alias")) or planned_tier or executed_tier
route_source = str(payload.get("route_source") or payload.get("source") or "").strip().lower()
if route_source not in {"default", "override", "active_engine"}:
route_source = "default" if tier_alias else ""
rule_name = str(payload.get("route_rule_name") or payload.get("rule_name") or "").strip()
decision = str(payload.get("routing_decision") or payload.get("decision") or "").strip()
config_version = str(payload.get("routing_config_version") or "").strip()
config_updated_at = str(payload.get("routing_config_updated_at") or "").strip()
rule_priority_raw = payload.get("rule_priority")
try:
rule_priority = int(rule_priority_raw) if rule_priority_raw is not None else None
except (TypeError, ValueError):
rule_priority = None
response_contract = _optional_response_contract(payload.get("response_contract"))
fallback_policy = _optional_fallback_policy(payload.get("fallback_policy"))
execution_mode = _optional_execution_mode(payload.get("execution_mode"))
return {
"tier_alias": tier_alias,
"planned_tier": planned_tier or tier_alias,
"executed_tier": executed_tier or planned_tier or tier_alias,
"route_source": route_source,
"route_rule_name": rule_name,
"routing_decision": decision or "primary_tier",
"routing_config_version": config_version,
"routing_config_updated_at": config_updated_at,
"rule_priority": rule_priority,
"response_contract": response_contract,
"fallback_policy": fallback_policy,
"execution_mode": execution_mode,
}
def _overlay_engine_api_key_from_env(engine: dict[str, Any]) -> dict[str, Any]:
runtime = dict(engine or {})
provider = str(runtime.get("provider") or runtime.get("source_provider") or "").strip().lower()
if provider not in PROVIDER_DEFAULTS:
return runtime
defaults = dict(PROVIDER_DEFAULTS.get(provider) or {})
api_env = str(runtime.get("api_env") or defaults.get("api_env") or "").strip()
if not api_env:
return runtime
env_api_key = str(os.getenv(api_env, "") or "").strip()
if env_api_key:
runtime["api_key"] = env_api_key
runtime["api_env"] = api_env
return runtime
def _read_engine_routing_meta(engine: Any) -> dict[str, Any]:
payload = engine.get("_routing_meta") if isinstance(engine, dict) else {}
return _normalize_routing_meta(payload)
def _merge_engine_routing_meta(engine: dict[str, Any], **updates: Any) -> dict[str, Any]:
runtime = dict(engine or {})
current = _read_engine_routing_meta(runtime)
for key, value in updates.items():
if value is None:
continue
current[key] = value
runtime["_routing_meta"] = _normalize_routing_meta(current)
return runtime
def _routing_meta_log_text(meta: Any) -> str:
data = _normalize_routing_meta(meta)
parts = []
if data.get("planned_tier"):
parts.append(f"planned_tier={data['planned_tier']}")
if data.get("executed_tier"):
parts.append(f"executed_tier={data['executed_tier']}")
if data.get("route_source"):
parts.append(f"source={data['route_source']}")
if data.get("route_rule_name"):
parts.append(f"rule={data['route_rule_name']}")
if data.get("routing_decision"):
parts.append(f"decision={data['routing_decision']}")
return ", ".join(parts)
def _env_bool(name: str, default: bool = False) -> bool:
raw = os.getenv(name)
if raw is None:
return default
return str(raw).strip().lower() in BOOL_TRUE_VALUES
def _parse_csv_items(raw: str) -> tuple[str, ...]:
rows = [item.strip().lower() for item in str(raw or "").split(",") if item.strip()]
return tuple(rows)
def _cn_now_text() -> str:
cn_tz = timezone(timedelta(hours=8))
return datetime.now(cn_tz).strftime("%Y-%m-%d %H:%M:%S")
def _cn_now_text_ms() -> str:
cn_tz = timezone(timedelta(hours=8))
return datetime.now(cn_tz).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
def _runtime_log(scope: str, message: str) -> None:
print(f"[{_cn_now_text()}][{scope}] {message}")
def _is_openclaw_timeout_message(text: str) -> bool:
lowered = str(text or "").strip().lower()
if not lowered:
return False
timeout_signals = (
"request timed out before a response was generated",
"agents.defaults.timeoutseconds",
"timeoutseconds",
"before a response was generated",
)
return any(signal in lowered for signal in timeout_signals)
@dataclass
class ChatCompletionResult:
text: str
raw_response: Any = None
@dataclass
class StructuredExtractionResult:
payload: Optional[dict[str, Any]]
source_field: str
provider: str
model: str
error_code: Optional[str] = None
raw_text: str = ""
validation: str = "invalid"
from_cache: bool = False
__all__ = [name for name in globals() if not name.startswith("__")]