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