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

280 lines
12 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)
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"
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 = ""
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,
)
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
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,
)
response_text = completion.text
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,
)
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:
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,
)
if isinstance(failover_result, StructuredExtractionResult) and isinstance(failover_engine, dict):
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"
return failover_result
error_code = self._classify_structured_exception(exc)
status = error_code
reason_text = self._build_model_error_message(
function_name=function_name,
provider=provider,
model=model,
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
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,
)
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 in {"classify_intent_ai_pmo_kernel", "classify_intent_complex"}:
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,
) -> 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:
repair_max_tokens = max(180, min(int(max_tokens or 0), 420))
if str(function_name or "").strip().lower() in {"classify_intent_ai_pmo_kernel", "classify_intent_complex"}:
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",
)
except Exception as exc:
_runtime_log(
"LLM结构化",
(
"模型结构化修复失败:"
f"function={function_name}, provider={provider}, model={model}, "
f"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