126 lines
5.1 KiB
Python
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"
|
|
|