Source code for qstn.inference.dynamic_pydantic
import re
from enum import Enum
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, create_model
from .response_generation import JSONItem, JSONObject
def _safe_name(text: str, prefix: str = "field") -> str:
name = re.sub(r"\W+", "_", text).strip("_").lower()
if not name or name[0].isdigit():
name = f"{prefix}_{name}"
return name
def _model_name(text: str | None, fallback: str) -> str:
if text is None:
return fallback
safe = _safe_name(text, fallback.lower())
return "".join(part.capitalize() for part in safe.split("_")) or fallback
def _enum_member_name(value: Any, index: int) -> str:
base_name = re.sub(r"\W+", "_", str(value)).strip("_").upper()
if not base_name:
base_name = "VALUE"
if base_name[0].isdigit():
base_name = f"VALUE_{base_name}"
return f"{base_name}_{index}"
def _create_enum_type(name: str, values: list[Any]) -> type[Enum]:
members = {
_enum_member_name(value, index): value for index, value in enumerate(values, start=1)
}
return Enum(name, members)
def _base_type_for_item(item: JSONItem) -> type:
if item.constraints.enum is not None:
return _create_enum_type(
_model_name(item.json_field, "EnumValue"),
item.constraints.enum,
)
if item.value_type == "string":
return str
if item.value_type == "float":
return float
if item.value_type == "int":
return int
if item.value_type == "bool":
return bool
raise ValueError(f"Unsupported value_type: {item.value_type}")
def _type_for_item(item: JSONItem) -> type:
base_type = _base_type_for_item(item)
if item.constraints.nullable:
return base_type | None
return base_type
def _field_for_item(item: JSONItem) -> tuple[type, Any]:
field_kwargs: dict[str, Any] = {
"alias": item.json_field,
}
if item.value_type in {"float", "int"}:
if item.constraints.ge is not None:
field_kwargs["ge"] = item.constraints.ge
if item.constraints.le is not None:
field_kwargs["le"] = item.constraints.le
if item.value_type == "string":
if item.constraints.min_length is not None:
field_kwargs["min_length"] = item.constraints.min_length
if item.constraints.max_length is not None:
field_kwargs["max_length"] = item.constraints.max_length
if item.constraints.pattern is not None:
field_kwargs["pattern"] = item.constraints.pattern
return _type_for_item(item), Field(..., **field_kwargs)
[docs]
def build_pydantic_model_from_json_object(
json_object: JSONObject,
model_name: str = "StructuredOutput",
) -> type[BaseModel]:
model_fields: dict[str, tuple[type, Any]] = {}
for child in json_object.children:
if isinstance(child, JSONItem):
internal_name = _safe_name(child.json_field, "field")
model_fields[internal_name] = _field_for_item(child)
continue
if child.json_field is None:
raise ValueError("Nested JSONObject entries must define `json_field`.")
nested_model = build_pydantic_model_from_json_object(
json_object=child,
model_name=_model_name(child.json_field, "NestedObject"),
)
internal_name = _safe_name(child.json_field, "object")
model_fields[internal_name] = (nested_model, Field(..., alias=child.json_field))
return create_model(
model_name,
__config__=ConfigDict(
populate_by_name=True,
extra="forbid",
),
**model_fields,
)