PMFlow/backend/app/services/llm_service/mixin_structured_extract_ob...

126 lines
5.1 KiB
Python

from __future__ import annotations
from .shared import *
class LLMStructuredExtractObjectMixin:
def _extract_structured_object_from_response(
self,
*,
function_name: str,
response: Any,
fallback_text: str,
provider: str,
model: str,
) -> StructuredExtractionResult:
message = {}
choice: dict[str, Any] = {}
if isinstance(response, dict):
choices = response.get("choices") if isinstance(response.get("choices"), list) else []
choice = choices[0] if choices and isinstance(choices[0], dict) else {}
message = choice.get("message") if isinstance(choice.get("message"), dict) else {}
last_nonempty_field = ""
last_nonempty_text = ""
for field, raw_value in self._iter_structured_candidates(message=message, choice=choice, response=response):
text = self._sanitize_visible_answer(self._extract_text_from_value(raw_value))
normalized = self._strip_code_fence(text)
if normalized:
last_nonempty_field = field
last_nonempty_text = normalized[:4000]
if not self._looks_like_json_container(normalized):
continue
candidate = self._extract_json_candidate(normalized)
if not candidate:
continue
try:
parsed = json.loads(candidate)
except Exception:
return StructuredExtractionResult(
payload=None,
source_field=field,
provider=provider,
model=model,
error_code="structured_json_invalid",
raw_text=candidate[:4000],
validation="invalid",
)
parsed = self._unwrap_structured_payload_once(parsed)
validation_error = self._validate_structured_payload(function_name=function_name, payload=parsed)
if validation_error is not None:
return StructuredExtractionResult(
payload=None,
source_field=field,
provider=provider,
model=model,
error_code=validation_error,
raw_text=candidate[:4000],
validation="invalid",
)
return StructuredExtractionResult(
payload=parsed if isinstance(parsed, dict) else None,
source_field=field,
provider=provider,
model=model,
raw_text=candidate[:4000],
validation="ok",
)
fallback_normalized = self._strip_code_fence(fallback_text)
fallback_candidate = (
self._extract_json_candidate(fallback_normalized)
if self._looks_like_json_container(fallback_normalized)
else ""
)
if fallback_candidate:
try:
parsed = json.loads(fallback_candidate)
except Exception:
return StructuredExtractionResult(
payload=None,
source_field="generated_text",
provider=provider,
model=model,
error_code="structured_json_invalid",
raw_text=fallback_candidate[:4000],
validation="invalid",
)
parsed = self._unwrap_structured_payload_once(parsed)
validation_error = self._validate_structured_payload(function_name=function_name, payload=parsed)
if validation_error is None and isinstance(parsed, dict):
return StructuredExtractionResult(
payload=parsed,
source_field="generated_text",
provider=provider,
model=model,
raw_text=fallback_candidate[:4000],
validation="ok",
)
if not last_nonempty_field and str(fallback_text or "").strip():
last_nonempty_field = "generated_text"
last_nonempty_text = str(fallback_text or "").strip()[:4000]
error_code = "structured_json_not_found" if last_nonempty_field else "structured_empty"
return StructuredExtractionResult(
payload=None,
source_field=last_nonempty_field,
provider=provider,
model=model,
error_code=error_code,
raw_text=last_nonempty_text,
validation="invalid",
)
@staticmethod
def _classify_structured_exception(exc: Exception) -> str:
if isinstance(exc, (asyncio.TimeoutError, httpx.TimeoutException)):
return "structured_timeout"
if isinstance(exc, (httpx.HTTPError, httpx.RequestError)):
return "structured_http_error"
lowered = str(exc or "").strip().lower()
if "timeout" in lowered:
return "structured_timeout"
if "status" in lowered or "http" in lowered:
return "structured_http_error"
return "structured_empty"