97 lines
4.3 KiB
Python
97 lines
4.3 KiB
Python
from __future__ import annotations
|
|
|
|
from .shared import *
|
|
|
|
class LLMStructuredCandidatesMixin:
|
|
def _iter_structured_candidates(
|
|
self,
|
|
*,
|
|
message: Any,
|
|
choice: Any,
|
|
response: Any,
|
|
) -> list[tuple[str, Any]]:
|
|
candidates: list[tuple[str, Any]] = []
|
|
if isinstance(message, dict):
|
|
tool_calls = message.get("tool_calls")
|
|
if isinstance(tool_calls, list) and tool_calls:
|
|
first = tool_calls[0] if isinstance(tool_calls[0], dict) else {}
|
|
fn = first.get("function") if isinstance(first, dict) else {}
|
|
candidates.append(("tool_calls.function.arguments", fn.get("arguments") if isinstance(fn, dict) else None))
|
|
candidates.append(("message.content", message.get("content")))
|
|
if isinstance(choice, dict):
|
|
candidates.append(("choice.text", choice.get("text")))
|
|
if isinstance(response, dict):
|
|
candidates.append(("response.output_text", response.get("output_text")))
|
|
if isinstance(message, dict):
|
|
candidates.append(("message.reasoning_content", message.get("reasoning_content")))
|
|
candidates.append(("message.reasoning_content", message.get("reasoning")))
|
|
if isinstance(choice, dict):
|
|
candidates.append(("choice.reasoning_content", choice.get("reasoning_content")))
|
|
candidates.append(("choice.reasoning_content", choice.get("reasoning")))
|
|
if isinstance(response, dict):
|
|
candidates.append(("response.reasoning_content", response.get("reasoning_content")))
|
|
candidates.append(("response.reasoning_content", response.get("reasoning")))
|
|
return candidates
|
|
|
|
@classmethod
|
|
def _unwrap_structured_payload_once(cls, payload: Any) -> Any:
|
|
if not isinstance(payload, dict):
|
|
return payload
|
|
for key in ("json", "data", "result", "arguments"):
|
|
if key not in payload:
|
|
continue
|
|
inner = payload.get(key)
|
|
if isinstance(inner, str):
|
|
candidate = cls._extract_json_candidate(cls._strip_code_fence(inner))
|
|
if not candidate:
|
|
continue
|
|
try:
|
|
return json.loads(candidate)
|
|
except Exception:
|
|
return inner
|
|
if isinstance(inner, (dict, list)):
|
|
return inner
|
|
return payload
|
|
|
|
@staticmethod
|
|
def _matches_expected_type(value: Any, expected: str) -> bool:
|
|
if expected == "bool":
|
|
return isinstance(value, bool)
|
|
if expected == "str":
|
|
return isinstance(value, str)
|
|
if expected == "list":
|
|
return isinstance(value, list)
|
|
if expected == "number":
|
|
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
if expected == "number_or_none":
|
|
return value is None or (isinstance(value, (int, float)) and not isinstance(value, bool))
|
|
if expected == "str_or_none":
|
|
return value is None or isinstance(value, str)
|
|
if expected == "dict_or_none":
|
|
return value is None or isinstance(value, dict)
|
|
return True
|
|
|
|
def _validate_structured_payload(self, *, function_name: str, payload: Any) -> Optional[str]:
|
|
fn = str(function_name or "").strip().lower()
|
|
schema = STRUCTURED_OBJECT_SCHEMAS.get(fn)
|
|
if schema is None:
|
|
return None if isinstance(payload, (dict, list)) else "structured_json_invalid"
|
|
if not isinstance(payload, dict):
|
|
return "structured_schema_invalid"
|
|
required_keys = schema.get("required_keys") or ()
|
|
for key in required_keys:
|
|
if key not in payload:
|
|
return "structured_schema_invalid"
|
|
type_checks = schema.get("type_checks") or {}
|
|
for key, expected in type_checks.items():
|
|
if not self._matches_expected_type(payload.get(key), str(expected)):
|
|
return "structured_schema_invalid"
|
|
optional_type_checks = schema.get("optional_type_checks") or {}
|
|
for key, expected_values in optional_type_checks.items():
|
|
if key not in payload:
|
|
continue
|
|
if not any(self._matches_expected_type(payload.get(key), str(expected)) for expected in expected_values):
|
|
return "structured_schema_invalid"
|
|
return None
|
|
|