PMFlow/backend/app/services/llm_service/mixin_generate_db_structure...

366 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from .shared import *
class LLMGenerateDbStructuredMixin:
async def _generate_structured_with_db(
self,
db: AsyncSession,
prompt: str,
**kwargs: Any,
) -> StructuredExtractionResult:
tenant_id = int(kwargs.get("tenant_id") or 1)
user_id = self._safe_int(kwargs.get("user_id"))
function_name = str(kwargs.get("function_name") or "llm_generate").strip().lower() or "llm_generate"
temperature = float(kwargs.get("temperature") or 0.0)
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"))
routing_meta["response_contract"] = "structured"
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 = prompt_with_context
provider = ""
model = ""
engine_meta: dict[str, Any] = {}
status = "success"
response_text = ""
provider_request_payload: Any = None
provider_response_payload: Any = None
provider_attempts: Any = None
started_at_perf = time.perf_counter()
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 "")
cache_key = ""
if self._structured_cache_enabled(function_name):
cache_key = self._structured_cache_key(
function_name=function_name,
model=model or "unknown",
prompt=merged_prompt,
)
cached = await self._structured_cache_get(key=cache_key)
if cached is not None:
status = "success_cache_hit"
result = StructuredExtractionResult(
payload=cached,
source_field="cache",
provider=provider,
model=model,
raw_text=json.dumps(cached, ensure_ascii=False),
validation="ok",
from_cache=True,
)
_runtime_log(
"LLM结构化",
f"function={function_name}, structured_source=cache, validation=ok, fallback=false",
)
return result
effective_routing_meta = _merge_call_routing_meta(
caller_meta=routing_meta,
engine_meta=_read_engine_routing_meta(engine),
)
completion = await self._chat_completion_raw(
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=effective_routing_meta,
)
response_text = completion.text
provider_request_payload = completion.raw_request
provider_response_payload = completion.raw_response
provider_attempts = completion.provider_attempts
extraction = self._extract_structured_object_from_response(
function_name=function_name,
response=completion.raw_response,
fallback_text=completion.text,
provider=provider,
model=model,
)
_runtime_log(
"LLM结构化",
(
f"function={function_name}, structured_source={extraction.source_field or 'none'}, "
f"validation={extraction.validation}, fallback=false"
),
)
if extraction.payload is None:
if self._should_attempt_model_repair(function_name):
repaired = await self._repair_structured_payload_with_model(
function_name=function_name,
provider=provider,
model=model,
api_key=str(engine.get("api_key") or ""),
base_url=str(engine.get("base_url") or ""),
system_prompt=prompt_profile.get("system_prompt") or "",
original_prompt=prompt_with_context,
extraction=extraction,
raw_response=completion.raw_response,
fallback_text=completion.text,
max_tokens=max_tokens,
routing_meta=routing_meta,
)
if isinstance(repaired, StructuredExtractionResult) and repaired.payload is not None:
extraction = repaired
response_text = repaired.raw_text or response_text
else:
raise ValueError(str(extraction.error_code or "structured_schema_invalid"))
else:
raise ValueError(str(extraction.error_code or "structured_schema_invalid"))
if extraction.payload is not None and cache_key:
await self._structured_cache_set(key=cache_key, value=extraction.payload)
status = extraction.error_code or "success"
return extraction
except Exception as exc:
provider_request_payload = provider_request_payload or getattr(exc, "provider_request_payload", None)
provider_response_payload = provider_response_payload or getattr(exc, "provider_response_payload", None)
provider_attempts = provider_attempts or getattr(exc, "provider_attempts", None)
engine_routing_meta = _read_engine_routing_meta(engine_meta)
if str(engine_routing_meta.get("planned_tier") or "").strip().lower() == "structured":
failover_result, failover_engine = await self._retry_structured_with_slot_fallback_after_primary_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 isinstance(failover_result, StructuredExtractionResult) and isinstance(failover_engine, dict):
engine_meta = failover_engine
provider = str(failover_engine.get("provider") or "")
model = str(failover_engine.get("model") or "")
response_text = failover_result.raw_text or json.dumps(failover_result.payload or {}, ensure_ascii=False)
status = failover_result.error_code or "success_structured_slot_fallback"
provider_request_payload = failover_engine.get("_provider_request_payload")
provider_response_payload = failover_engine.get("_provider_response_payload")
provider_attempts = failover_engine.get("_provider_attempts")
return failover_result
error_code = self._classify_structured_exception(exc)
status = error_code
reason_text = self._build_model_error_message(
function_name=function_name,
routing_meta=_merge_call_routing_meta(caller_meta=routing_meta, engine_meta=engine_routing_meta),
error=exc,
)
response_text = reason_text
_runtime_log(
"LLM结构化",
"function={function_name}, structured_source=none, validation=invalid, fallback=structured_slot_exhausted".format(
function_name=function_name
),
)
raise RuntimeError(reason_text) from exc
failover_result, failover_engine = await self._retry_structured_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 isinstance(failover_result, StructuredExtractionResult) and isinstance(failover_engine, dict):
engine_meta = failover_engine
provider = str(failover_engine.get("provider") or "")
model = str(failover_engine.get("model") or "")
response_text = failover_result.raw_text or json.dumps(failover_result.payload or {}, ensure_ascii=False)
status = failover_result.error_code or "success_failover"
provider_request_payload = failover_engine.get("_provider_request_payload")
provider_response_payload = failover_engine.get("_provider_response_payload")
provider_attempts = failover_engine.get("_provider_attempts")
return failover_result
error_code = self._classify_structured_exception(exc)
status = error_code
reason_text = self._build_model_error_message(
function_name=function_name,
routing_meta=_merge_call_routing_meta(
caller_meta=routing_meta,
engine_meta=_read_engine_routing_meta(engine_meta),
),
error=exc,
)
response_text = reason_text
_runtime_log(
"LLM结构化",
f"function={function_name}, structured_source=none, validation=invalid, fallback=false",
)
raise RuntimeError(reason_text) from exc
finally:
try:
log_payload = response_text or status
duration_ms = int((time.perf_counter() - started_at_perf) * 1000)
call_routing_meta = _merge_call_routing_meta(
caller_meta=routing_meta,
engine_meta=_read_engine_routing_meta(engine_meta),
)
call_routing_meta["duration_ms"] = duration_ms
await self._log_ai_call(
tenant_id=tenant_id,
function_name=function_name,
prompt=merged_prompt,
response=log_payload,
status=status,
provider=provider,
model=model,
routing_meta=call_routing_meta,
provider_request_payload=provider_request_payload or engine_meta.get("_provider_request_payload"),
provider_response_payload=provider_response_payload or engine_meta.get("_provider_response_payload"),
provider_attempts=provider_attempts or engine_meta.get("_provider_attempts"),
user_id=user_id,
)
except Exception as log_exc:
_runtime_log("LLM", f"结构化调用日志写入失败:{log_exc}")
@staticmethod
def _build_structured_schema_hint(function_name: str) -> str:
fn = str(function_name or "").strip().lower()
schema = STRUCTURED_OBJECT_SCHEMAS.get(fn)
if not isinstance(schema, dict):
return "{}"
schema_hint = {
"required_keys": list(schema.get("required_keys") or []),
"type_checks": dict(schema.get("type_checks") or {}),
"optional_type_checks": dict(schema.get("optional_type_checks") or {}),
}
return json.dumps(schema_hint, ensure_ascii=False)
@staticmethod
def _should_attempt_model_repair(function_name: str) -> bool:
fn = str(function_name or "").strip().lower()
if fn == "pmflow_semantic_context_decision":
return _env_bool("LLM_INTENT_STRUCTURED_REPAIR_ENABLED", True)
return True
async def _repair_structured_payload_with_model(
self,
*,
function_name: str,
provider: str,
model: str,
api_key: str,
base_url: str,
system_prompt: str,
original_prompt: str,
extraction: StructuredExtractionResult,
raw_response: Any,
fallback_text: str,
max_tokens: int,
routing_meta: dict[str, Any] | None = None,
) -> Optional[StructuredExtractionResult]:
source_text = str(extraction.raw_text or "").strip()
if source_text.startswith("[") and source_text.endswith("]"):
source_text = ""
if not source_text:
source_text = str(fallback_text or "").strip()
if not source_text:
return None
schema_hint = self._build_structured_schema_hint(function_name)
repair_prompt = (
"首字符必须是 {,末字符必须是 }。\n"
"你是 JSON 修复器。把模型输出整理为满足目标 schema 的 JSON 对象。\n"
f"function_name: {function_name}\n"
f"schema_hint_json: {schema_hint}\n"
f"original_task_prompt: {str(original_prompt or '')[:2000]}\n"
f"model_output: {source_text[:2800]}\n"
"要求:\n"
"1. 只输出 JSON 对象。\n"
"2. 必须包含 required_keys。\n"
"3. 若字段缺失,填 null/空字符串/空数组,并保持类型合法。\n"
"4. 禁止输出解释、Markdown、代码块。"
)
try:
normalized_function = str(function_name or "").strip().lower()
configured_repair_tokens = int(_normalize_routing_meta(routing_meta).get("function_runtime_repair_max_tokens") or 0)
if configured_repair_tokens > 0:
repair_max_tokens = max(180, min(int(max_tokens or 0), configured_repair_tokens))
elif normalized_function == "pmflow_launch_draft_v1":
repair_max_tokens = max(1200, min(int(max_tokens or 0), 3600))
else:
repair_max_tokens = max(180, min(int(max_tokens or 0), 420))
if normalized_function == "pmflow_semantic_context_decision":
repair_max_tokens = min(repair_max_tokens, 220)
repair_completion = await self._chat_completion_raw(
provider=provider,
prompt=repair_prompt,
system_prompt=system_prompt,
api_key=api_key,
base_url=base_url,
model=model,
temperature=0.0,
max_tokens=repair_max_tokens,
function_name=function_name,
response_mode="fast",
routing_meta=_normalize_routing_meta(routing_meta),
)
except Exception as exc:
_runtime_log(
"LLM结构化",
(
"模型结构化修复失败:"
f"function={function_name}, error={type(exc).__name__}:{str(exc)[:180]}"
),
)
return None
repair_extraction = self._extract_structured_object_from_response(
function_name=function_name,
response=repair_completion.raw_response,
fallback_text=repair_completion.text,
provider=provider,
model=model,
)
if repair_extraction.payload is None:
_runtime_log(
"LLM结构化",
(
"模型结构化修复未产出有效 JSON:"
f"function={function_name}, source={repair_extraction.source_field or 'none'}, "
f"error={repair_extraction.error_code or 'unknown'}"
),
)
return None
_runtime_log(
"LLM结构化",
(
f"function={function_name}, structured_source={repair_extraction.source_field or 'none'}, "
f"validation={repair_extraction.validation}, fallback=model_repair"
),
)
return repair_extraction