145 lines
6.2 KiB
Python
145 lines
6.2 KiB
Python
from __future__ import annotations
|
|
|
|
from .shared import *
|
|
|
|
class LLMGenerateDbChatMixin:
|
|
async def _generate_with_db(self, db: AsyncSession, prompt: str, **kwargs: Any) -> str:
|
|
started_at = time.perf_counter()
|
|
tenant_id = int(kwargs.get("tenant_id") or 1)
|
|
function_name = str(kwargs.get("function_name") or "llm_generate")
|
|
temperature = float(kwargs.get("temperature") or 0.2)
|
|
max_tokens = int(kwargs.get("max_tokens") or 1024)
|
|
use_prompt_profile = bool(kwargs.get("use_prompt_profile", True))
|
|
response_mode = str(kwargs.get("response_mode") or "complete").strip().lower() or "complete"
|
|
routing_meta = _normalize_routing_meta(kwargs.get("routing_meta"))
|
|
response_text = ""
|
|
status = "success"
|
|
routing_decision = "primary_engine"
|
|
engine_meta: dict[str, Any] = {}
|
|
prompt_profile = {"role_name": "", "system_prompt": ""}
|
|
prompt_with_context = await self._inject_model_context_bundle_prompt(
|
|
db=db,
|
|
tenant_id=tenant_id,
|
|
function_name=function_name,
|
|
prompt=prompt,
|
|
)
|
|
merged_prompt = self._merge_prompt_with_profile(
|
|
prompt=prompt_with_context,
|
|
prompt_profile=prompt_profile,
|
|
function_name=function_name,
|
|
)
|
|
|
|
def _log_stub_failure(error_obj: Exception, *, used_stub: bool = False) -> None:
|
|
elapsed_ms = (time.perf_counter() - started_at) * 1000
|
|
routing_meta = _read_engine_routing_meta(engine_meta)
|
|
outcome = "兜底 stub" if used_stub else "显式报错"
|
|
logger.warning(
|
|
"[LLM_FAIL] 生成失败 → %s function=%s route=%s error=%r duration_ms=%.0f",
|
|
outcome,
|
|
function_name,
|
|
_routing_meta_log_text(routing_meta) or "unknown",
|
|
error_obj,
|
|
elapsed_ms,
|
|
)
|
|
|
|
try:
|
|
if use_prompt_profile:
|
|
prompt_profile = await self._resolve_active_prompt_profile(db, tenant_id=tenant_id)
|
|
merged_prompt = self._merge_prompt_with_profile(
|
|
prompt=prompt_with_context,
|
|
prompt_profile=prompt_profile,
|
|
function_name=function_name,
|
|
)
|
|
engine = await self._resolve_engine_for_call(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
function_name=function_name,
|
|
routing_meta=routing_meta,
|
|
)
|
|
engine_meta = engine
|
|
provider = str(engine.get("provider") or "")
|
|
model = str(engine.get("model") or "")
|
|
response_text = await self._chat_completion(
|
|
provider=provider,
|
|
prompt=prompt_with_context,
|
|
system_prompt=prompt_profile.get("system_prompt") or "",
|
|
api_key=str(engine.get("api_key") or ""),
|
|
base_url=str(engine.get("base_url") or ""),
|
|
model=model,
|
|
temperature=temperature,
|
|
max_tokens=max_tokens,
|
|
function_name=function_name,
|
|
response_mode=response_mode,
|
|
routing_meta=routing_meta,
|
|
)
|
|
if not response_text.strip():
|
|
raise ValueError("primary_engine_empty_response")
|
|
|
|
if self._call_is_invalid_structured_output(
|
|
function_name=function_name,
|
|
response_text=response_text,
|
|
routing_meta=routing_meta,
|
|
):
|
|
raise ValueError("invalid_structured_output")
|
|
return response_text
|
|
except Exception as exc:
|
|
failover_response, failover_engine = await self._retry_with_high_tier_after_low_model_failure(
|
|
db=db,
|
|
tenant_id=tenant_id,
|
|
function_name=function_name,
|
|
failed_engine=engine_meta,
|
|
prompt=prompt_with_context,
|
|
system_prompt=str(prompt_profile.get("system_prompt") or ""),
|
|
temperature=temperature,
|
|
max_tokens=max_tokens,
|
|
response_mode=response_mode,
|
|
error=exc,
|
|
routing_meta=routing_meta,
|
|
)
|
|
if failover_response and isinstance(failover_engine, dict):
|
|
routing_decision = "low_to_high_escalated"
|
|
status = "success_failover"
|
|
response_text = failover_response
|
|
engine_meta = failover_engine
|
|
return failover_response
|
|
status = "failed"
|
|
reason_text = self._build_model_error_message(
|
|
function_name=function_name,
|
|
routing_meta=_read_engine_routing_meta(engine_meta),
|
|
error=exc,
|
|
)
|
|
response_text = reason_text
|
|
_log_stub_failure(exc, used_stub=False)
|
|
raise RuntimeError(reason_text) from exc
|
|
finally:
|
|
try:
|
|
merged_prompt = self._merge_prompt_with_profile(
|
|
prompt=prompt_with_context,
|
|
prompt_profile=prompt_profile,
|
|
function_name=function_name,
|
|
)
|
|
await self._log_ai_call(
|
|
tenant_id=tenant_id,
|
|
function_name=function_name,
|
|
prompt=merged_prompt,
|
|
response=response_text,
|
|
status=status,
|
|
provider=str(engine_meta.get("provider") or ""),
|
|
model=str(engine_meta.get("model") or ""),
|
|
routing_meta=_read_engine_routing_meta(engine_meta),
|
|
)
|
|
routing_meta = _read_engine_routing_meta(engine_meta)
|
|
if (
|
|
routing_meta.get("planned_tier")
|
|
and routing_meta.get("executed_tier")
|
|
and routing_meta.get("planned_tier") != routing_meta.get("executed_tier")
|
|
):
|
|
_runtime_log(
|
|
"LLM路由",
|
|
(
|
|
f"function={function_name}, {_routing_meta_log_text(routing_meta)}"
|
|
),
|
|
)
|
|
except Exception as log_exc:
|
|
_runtime_log("LLM", f"调用日志写入失败:{log_exc}")
|