Skip to content

JSON Schema generation

conatus.actions.json_schema

Types and methods to generate the JSON Schema of Actions.

Generating a JSONSchema for an Action is important since LLM APIs tend to requite a JSON Schema for the input parameters of the function.

The main method here is generate_openai_json_schema , which generates the JSON Schema for an Action. We do this by first generating Pydantic models from the Action 's FunctionInfo , one of which will be a Pydantic model specifically for the JSON Schema. It is that model that is the input to generate_openai_json_schema .

In practice, it will be used like this:

from conatus.actions.utils.schema_extraction import (
    extract_function_info,
    generate_pydantic_models,
)
from conatus.actions.json_schema import generate_openai_json_schema

def func(a: int, b: str) -> str:
    ...

function_info = extract_function_info(func)
pydantic_models = generate_pydantic_models(function_info)
json_schema = generate_openai_json_schema(pydantic_models.json_schema)

Strict vs lenient mode

We can generate the JSON Schema in "lenient" mode or "strict" mode. The "strict" mode is more restrictive, is tailor made for OpenAI's Structured Outputs feature, and guarantees us that what will be returned by the LLM corresponds exactly to the specifications of the action.

Reference

Note that OpenAI tend to deprecate their documentation quite aggressively. Sorry if the links are broken.

PropertyConstPydanticJSON module-attribute

PropertyConstPydanticJSON = (
    Annotated[StringPydanticJSON, Tag("string")]
    | Annotated[ObjectPydanticJSON, Tag("object")]
    | Annotated[ArrayPydanticJSON, Tag("array")]
    | Annotated[NumberPydanticJSON, Tag("number")]
    | Annotated[NumberPydanticJSON, Tag("integer")]
    | Annotated[BooleanPydanticJSON, Tag("boolean")]
    | Annotated[NullPydanticJSON, Tag("null")]
    | Annotated[AnyPydanticJSON, Tag("any")]
)

Pydantic JSON schema for a constant value.

PropertyPydanticJSON module-attribute

Pydantic JSON schema for a property.

PropertyConstOpenAIStrictJSON module-attribute

PropertyConstOpenAIStrictJSON = (
    Annotated[StringOpenAIStrictJSON, Tag("string")]
    | Annotated[ObjectOpenAIStrictJSON, Tag("object")]
    | Annotated[ArrayOpenAIStrictJSON, Tag("array")]
    | Annotated[NumberOpenAIStrictJSON, Tag("number")]
    | Annotated[NumberOpenAIStrictJSON, Tag("integer")]
    | Annotated[BooleanOpenAIStrictJSON, Tag("boolean")]
    | Annotated[NullOpenAIStrictJSON, Tag("null")]
    | Annotated[AnyPydanticJSON, Tag("any")]
)

OpenAI JSON schema for a constant value (strict mode).

PropertyOpenAIStrictJSON module-attribute

OpenAI JSON schema for a property (strict mode).

AnyPydanticJSON

Bases: NoExtraBaseModel

Base model for a JSON schema representing any type.

ATTRIBUTE DESCRIPTION
title

The title of the schema.

TYPE: str | None

description

The description of the schema.

TYPE: str | None

remove_refs

remove_refs(defs: dict[str, PropertyPydanticJSON]) -> Self

Remove references from the JSON schema.

PARAMETER DESCRIPTION
defs

The definitions of the schema.

TYPE: dict[str, PropertyPydanticJSON]

RETURNS DESCRIPTION
Self

The JSON schema without references.

Source code in conatus/actions/json_schema.py
def remove_refs(self, defs: dict[str, PropertyPydanticJSON]) -> Self:
    """Remove references from the JSON schema.

    Args:
        defs: The definitions of the schema.

    Returns:
        The JSON schema without references.
    """
    _ = defs
    return self

StringPydanticJSON

Bases: NoExtraBaseModel

Pydantic JSON schema for a string.

ATTRIBUTE DESCRIPTION
title

The title of the schema.

TYPE: str | None

description

The description of the schema.

TYPE: str | None

enum

The possible values of the string.

TYPE: list[str] | None

default

The default value of the string.

TYPE: str | None

max_length

The maximum length of the string.

TYPE: int | None

min_length

The minimum length of the string.

TYPE: int | None

pattern

The pattern of the string.

TYPE: str | None

format

The format of the string.

TYPE: str | None

type_

The type of the string.

TYPE: Literal['string']

remove_refs

remove_refs(defs: dict[str, PropertyPydanticJSON]) -> Self

Remove references from the JSON schema.

PARAMETER DESCRIPTION
defs

The definitions of the schema.

TYPE: dict[str, PropertyPydanticJSON]

RETURNS DESCRIPTION
Self

The JSON schema without references.

Source code in conatus/actions/json_schema.py
def remove_refs(self, defs: dict[str, PropertyPydanticJSON]) -> Self:
    """Remove references from the JSON schema.

    Args:
        defs: The definitions of the schema.

    Returns:
        The JSON schema without references.
    """
    _ = defs
    return self

NumberPydanticJSON

Bases: NoExtraBaseModel

Pydantic JSON schema for a number (int or float).

ATTRIBUTE DESCRIPTION
title

The title of the schema.

TYPE: str | None

description

The description of the schema.

TYPE: str | None

enum

The possible values of the number.

TYPE: list[int | float] | None

default

The default value of the number.

TYPE: int | float | None

type_

The type of the number.

TYPE: Literal['number', 'integer'] | tuple[Literal['number', 'integer'], Literal['null']]

exclusive_maximum

The exclusive maximum value of the number.

TYPE: int | None

exclusive_minimum

The exclusive minimum value of the number.

TYPE: int | None

maximum

The maximum value of the number.

TYPE: int | float | None

minimum

The minimum value of the number.

TYPE: int | float | None

remove_refs

remove_refs(defs: dict[str, PropertyPydanticJSON]) -> Self

Remove references from the JSON schema.

PARAMETER DESCRIPTION
defs

The definitions of the schema.

TYPE: dict[str, PropertyPydanticJSON]

RETURNS DESCRIPTION
Self

The JSON schema without references.

Source code in conatus/actions/json_schema.py
def remove_refs(self, defs: dict[str, PropertyPydanticJSON]) -> Self:
    """Remove references from the JSON schema.

    Args:
        defs: The definitions of the schema.

    Returns:
        The JSON schema without references.
    """
    _ = defs
    return self

BooleanPydanticJSON

Bases: NoExtraBaseModel

Pydantic JSON schema for a boolean.

ATTRIBUTE DESCRIPTION
description

The description of the schema.

TYPE: str | None

default

The default value of the boolean.

TYPE: bool | None

title

The title of the schema.

TYPE: str | None

remove_refs

remove_refs(defs: dict[str, PropertyPydanticJSON]) -> Self

Remove references from the JSON schema.

PARAMETER DESCRIPTION
defs

The definitions of the schema.

TYPE: dict[str, PropertyPydanticJSON]

RETURNS DESCRIPTION
Self

The JSON schema without references.

Source code in conatus/actions/json_schema.py
def remove_refs(self, defs: dict[str, PropertyPydanticJSON]) -> Self:
    """Remove references from the JSON schema.

    Args:
        defs: The definitions of the schema.

    Returns:
        The JSON schema without references.
    """
    _ = defs
    return self

ObjectPydanticJSON

Bases: NoExtraBaseModel

Pydantic JSON schema for an object.

ATTRIBUTE DESCRIPTION
description

The description of the schema.

TYPE: str | None

properties

The properties of the object.

TYPE: dict[str, PropertyPydanticJSON] | AnyPydanticJSON | None

additional_properties

Whether additional properties are allowed.

TYPE: PropertyPydanticJSON | bool | None

default

The default value of the object.

TYPE: dict[str, ConstJSONSchema] | None

required

The required properties of the object.

TYPE: list[str] | None

title

The title of the schema.

TYPE: str | None

remove_refs

remove_refs(defs: dict[str, PropertyPydanticJSON]) -> Self

Remove references from the JSON schema.

PARAMETER DESCRIPTION
defs

The definitions of the schema.

TYPE: dict[str, PropertyPydanticJSON]

RETURNS DESCRIPTION
Self

The JSON schema without references.

Source code in conatus/actions/json_schema.py
def remove_refs(self, defs: dict[str, PropertyPydanticJSON]) -> Self:
    """Remove references from the JSON schema.

    Args:
        defs: The definitions of the schema.

    Returns:
        The JSON schema without references.
    """
    if isinstance(self.properties, dict):
        self.properties = {
            key: prop.remove_refs(defs)
            for key, prop in self.properties.items()
        }
    elif isinstance(self.properties, AnyPydanticJSON):  # pragma: no cover
        self.properties = self.properties.remove_refs(defs)
    if self.additional_properties and not isinstance(  # pragma: no cover
        self.additional_properties, bool
    ):
        self.additional_properties = self.additional_properties.remove_refs(
            defs
        )
    return self

ArrayPydanticJSON

Bases: NoExtraBaseModel

Pydantic JSON schema for an array.

ATTRIBUTE DESCRIPTION
title

The title of the schema.

TYPE: str | None

description

The description of the schema.

TYPE: str | None

items

The items of the array.

TYPE: PropertyPydanticJSON

max_items

The maximum number of items in the array.

TYPE: int | None

min_items

The minimum number of items in the array.

TYPE: int | None

prefix_items

The prefix items of the array.

TYPE: list[PropertyPydanticJSON] | None

default

The default value of the array.

TYPE: list[ConstJSONSchema] | None

unique_items

Whether the items in the array are unique.

TYPE: bool | None

type_

The type of the array.

TYPE: Literal['array']

remove_refs

remove_refs(defs: dict[str, PropertyPydanticJSON]) -> Self

Remove references from the JSON schema.

PARAMETER DESCRIPTION
defs

The definitions of the schema.

TYPE: dict[str, PropertyPydanticJSON]

RETURNS DESCRIPTION
Self

The JSON schema without references.

Source code in conatus/actions/json_schema.py
def remove_refs(self, defs: dict[str, PropertyPydanticJSON]) -> Self:
    """Remove references from the JSON schema.

    Args:
        defs: The definitions of the schema.

    Returns:
        The JSON schema without references.
    """
    self.items = self.items.remove_refs(defs)
    if self.prefix_items:
        self.prefix_items = [
            item.remove_refs(defs) for item in self.prefix_items
        ]
    return self

NullPydanticJSON

Bases: NoExtraBaseModel

Pydantic JSON schema for a null value.

ATTRIBUTE DESCRIPTION
title

The title of the schema.

TYPE: str | None

description

The description of the schema.

TYPE: str | None

type_

The type of the schema.

TYPE: Literal['null']

default

The default value of the schema.

TYPE: None

remove_refs

remove_refs(defs: dict[str, PropertyPydanticJSON]) -> Self

Remove references from the JSON schema.

PARAMETER DESCRIPTION
defs

The definitions of the schema.

TYPE: dict[str, PropertyPydanticJSON]

RETURNS DESCRIPTION
Self

The JSON schema without references.

Source code in conatus/actions/json_schema.py
def remove_refs(self, defs: dict[str, PropertyPydanticJSON]) -> Self:
    """Remove references from the JSON schema.

    Args:
        defs: The definitions of the schema.

    Returns:
        The JSON schema without references.
    """
    _ = defs
    return self

RefPydanticJSON

Bases: NoExtraBaseModel

Pydantic JSON schema for a reference.

ATTRIBUTE DESCRIPTION
title

The title of the schema.

TYPE: str | None

description

The description of the schema.

TYPE: str | None

ref

The reference of the schema.

TYPE: str

default

The default value of the schema.

TYPE: ConstJSONSchema | None

remove_refs

remove_refs(
    defs: dict[str, PropertyPydanticJSON],
) -> Self | PropertyPydanticJSON

Remove references from the JSON schema.

PARAMETER DESCRIPTION
defs

The definitions of the schema.

TYPE: dict[str, PropertyPydanticJSON]

RETURNS DESCRIPTION
Self | PropertyPydanticJSON

The JSON schema without references.

RAISES DESCRIPTION
JSONSchemaIncorrectRefsError

If the reference is not found in the definitions.

Source code in conatus/actions/json_schema.py
def remove_refs(
    self, defs: dict[str, PropertyPydanticJSON]
) -> Self | PropertyPydanticJSON:
    """Remove references from the JSON schema.

    Args:
        defs: The definitions of the schema.

    Returns:
        The JSON schema without references.

    Raises:
        JSONSchemaIncorrectRefsError: If the reference is not found in the
            definitions.
    """
    ref_name = self.ref.split("/")[-1]
    if ref_name in defs:
        return defs[ref_name].remove_refs(defs)
    msg = (
        f"Reference {self.ref} not found in definitions: "
        f"[{', '.join(defs.keys())}]"
    )
    raise JSONSchemaIncorrectRefsError(msg)

AnyOfPydanticJSON

Bases: NoExtraBaseModel

Pydantic JSON schema for a union of types.

ATTRIBUTE DESCRIPTION
title

The title of the schema.

TYPE: str | None

description

The description of the schema.

TYPE: str | None

any_of

The union of types.

TYPE: list[PropertyPydanticJSON]

default

The default value of the schema.

TYPE: ConstJSONSchema | None

remove_refs

remove_refs(defs: dict[str, PropertyPydanticJSON]) -> Self

Remove references from the JSON schema.

PARAMETER DESCRIPTION
defs

The definitions of the schema.

TYPE: dict[str, PropertyPydanticJSON]

RETURNS DESCRIPTION
Self

The JSON schema without references.

Source code in conatus/actions/json_schema.py
def remove_refs(self, defs: dict[str, PropertyPydanticJSON]) -> Self:
    """Remove references from the JSON schema.

    Args:
        defs: The definitions of the schema.

    Returns:
        The JSON schema without references.
    """
    self.any_of = [prop.remove_refs(defs) for prop in self.any_of]
    return self

PydanticJSONSchema

Bases: NoExtraBaseModel

Pydantic JSON schema.

This should be what Pydantic returns with model_json_schema().

ATTRIBUTE DESCRIPTION
properties

The properties of the schema.

TYPE: dict[str, PropertyPydanticJSON]

required

The required properties of the schema.

TYPE: list[str] | None

title

The title of the schema.

TYPE: str

description

The description of the schema.

TYPE: str | None

type_

The type of the schema.

TYPE: str

defs

The definitions of the schema.

TYPE: dict[str, PropertyPydanticJSON] | None

remove_refs

remove_refs() -> None

Remove references from the JSON schema.

Note this is happening in-place, so the original JSON schema is modified.

Source code in conatus/actions/json_schema.py
def remove_refs(self) -> None:
    """Remove references from the JSON schema.

    Note this is happening in-place, so the original JSON schema is
    modified.
    """
    defs = self.defs or {}
    self.properties = {
        key: prop.remove_refs(defs) for key, prop in self.properties.items()
    }
    if self.defs:
        self.defs = None

OpenAIJSONSchemaParameters

Bases: IgnoreExtraBaseModel

OpenAI JSON schema for function parameters (lenient mode).

ATTRIBUTE DESCRIPTION
properties

The properties of the schema.

TYPE: dict[str, PropertyPydanticJSON]

required

The required properties of the schema.

TYPE: list[str] | None

type_

The type of the schema.

TYPE: str

defs

The definitions of the schema.

TYPE: dict[str, PropertyPydanticJSON] | None

OpenAIJSONSchema

Bases: NoExtraBaseModel

OpenAI JSON schema for functions (lenient mode).

This represents an individual function.

ATTRIBUTE DESCRIPTION
name

The name of the function.

TYPE: str

description

The description of the function.

TYPE: str | None

parameters

The parameters of the function.

TYPE: OpenAIJSONSchemaParameters

strict

Whether the function is strict.

TYPE: bool

ArrayOpenAIStrictJSON

Bases: NoExtraBaseModel

OpenAI JSON schema for an array (strict mode).

ATTRIBUTE DESCRIPTION
title

The title of the schema.

TYPE: str | None

description

The description of the schema.

TYPE: str | None

items

The items of the array.

TYPE: PropertyOpenAIStrictJSON

type_

The type of the array.

TYPE: Literal['array'] | tuple[Literal['array'], Literal['null']]

StringOpenAIStrictJSON

Bases: NoExtraBaseModel

OpenAI JSON schema for a string (strict mode).

ATTRIBUTE DESCRIPTION
title

The title of the schema.

TYPE: str | None

description

The description of the schema.

TYPE: str | None

enum

The possible values of the string.

TYPE: list[str] | None

type_

The type of the string.

TYPE: Literal['string'] | tuple[Literal['string'], Literal['null']]

ObjectOpenAIStrictJSON

Bases: NoExtraBaseModel

OpenAI JSON schema for an object (strict mode).

ATTRIBUTE DESCRIPTION
title

The title of the schema.

TYPE: str | None

description

The description of the schema.

TYPE: str | None

properties

The properties of the object.

TYPE: dict[str, PropertyOpenAIStrictJSON]

additional_properties

Whether additional properties are allowed.

TYPE: PropertyOpenAIStrictJSON | bool | None

required

The required properties of the object.

TYPE: list[str] | None

NumberOpenAIStrictJSON

Bases: NoExtraBaseModel

OpenAI JSON schema for a number (strict mode).

ATTRIBUTE DESCRIPTION
title

The title of the schema.

TYPE: str | None

description

The description of the schema.

TYPE: str | None

enum

The possible values of the number.

TYPE: list[int | float] | None

type_

The type of the number.

TYPE: Literal['number', 'integer'] | tuple[Literal['number', 'integer'], Literal['null']]

RefOpenAIStrictJSON

Bases: NoExtraBaseModel

OpenAI JSON schema for a reference (strict mode).

ATTRIBUTE DESCRIPTION
ref

The reference of the schema.

TYPE: str

AnyOfOpenAIStrictJSON

Bases: NoExtraBaseModel

OpenAI JSON schema for a union of types (strict mode).

ATTRIBUTE DESCRIPTION
title

The title of the schema.

TYPE: str | None

description

The description of the schema.

TYPE: str | None

any_of

The union of types.

TYPE: list[PropertyOpenAIStrictJSON]

OpenAIJSONStrictSchemaParameters

Bases: IgnoreExtraBaseModel

OpenAI JSON schema for function parameters (strict mode).

ATTRIBUTE DESCRIPTION
properties

The properties of the schema.

TYPE: dict[str, PropertyOpenAIStrictJSON]

required

The required properties of the schema.

TYPE: list[str] | None

type_

The type of the schema.

TYPE: Literal['object']

defs

The definitions of the schema.

TYPE: dict[str, PropertyOpenAIStrictJSON] | None

OpenAIJSONStrictFnSchema

Bases: NoExtraBaseModel

OpenAI JSON schema for functions (strict mode).

ATTRIBUTE DESCRIPTION
name

The name of the function.

TYPE: str

description

The description of the function.

TYPE: str | None

parameters

The parameters of the function.

TYPE: OpenAIJSONStrictSchemaParameters

strict

Whether the function is strict.

TYPE: Literal[True]

get_pyd_json_schema_type

get_pyd_json_schema_type(
    v: dict[str, object] | BaseModel,
) -> str

Get the type of a PydJSONSchema object.

JSONSchema objects generally have a 'type' element associated with them, but if they don't, it's generally a sign that they are a union (anyOf) or a reference ($ref). This function helps Pydantic determine which type of JSONSchema object it is.

For more information, see the Pydantic documentation on unions and discriminators

PARAMETER DESCRIPTION
v

The JSONSchema object.

TYPE: dict | BaseModel

RETURNS DESCRIPTION
str

The tag of the Pydantic JSONSchema object.

TYPE: str

Source code in conatus/actions/json_schema.py
def get_pyd_json_schema_type(v: dict[str, object] | BaseModel) -> str:  # noqa: PLR0911
    """Get the type of a PydJSONSchema object.

    JSONSchema objects generally have a 'type' element associated with
    them, but if they don't, it's generally a sign that they are a union
    (`anyOf`) or a reference (`$ref`). This function helps Pydantic
    determine which type of JSONSchema object it is.

    For more information, see the [Pydantic documentation on unions and
    discriminators](https://docs.pydantic.dev/2.10/concepts/unions/#discriminated-unions-with-callable-discriminator)

    Args:
        v (dict | BaseModel): The JSONSchema object.

    Returns:
        str: The tag of the Pydantic JSONSchema object.
    """
    if isinstance(v, dict):
        if "anyOf" in v:
            return "anyOf"
        if "$ref" in v:
            return "ref"
        type_val_dict: list[str] | tuple[str] | str = cast(
            "list[str] | tuple[str] | str", v.get("type", "any")
        )
        # In practice, Pydantic never returns a JSON Schema where `type` is
        # a list or a tuple, but rather uses `anyOf` for that. So this
        # shouldn't happen.
        if isinstance(type_val_dict, list | tuple):  # pragma: no cover
            return type_val_dict[0]
        return type_val_dict

    if getattr(v, "any_of", None):
        return "anyOf"
    if getattr(v, "ref", None):
        return "ref"
    type_val: list[str] | tuple[str] | str = getattr(v, "type_", "any")
    # Check for a type_val like ["string", "null"]
    # In practice, Pydantic prefers to use `anyOf` anyway, so we shouldn't
    # encounter this.
    if isinstance(type_val, list | tuple):  # pragma: no cover
        return type_val[0]
    return type_val

cast_strict_prop

cast_strict_prop(
    jsc: AnyOfPydanticJSON, key_reference: list[str]
) -> tuple[AnyOfOpenAIStrictJSON, list[str]]
cast_strict_prop(
    jsc: AnyPydanticJSON, key_reference: list[str]
) -> tuple[AnyPydanticJSON, list[str]]
cast_strict_prop(
    jsc: RefPydanticJSON, key_reference: list[str]
) -> tuple[RefOpenAIStrictJSON, list[str]]
cast_strict_prop(
    jsc: StringPydanticJSON, key_reference: list[str]
) -> tuple[StringOpenAIStrictJSON, list[str]]
cast_strict_prop(
    jsc: ObjectPydanticJSON, key_reference: list[str]
) -> tuple[ObjectOpenAIStrictJSON, list[str]]
cast_strict_prop(
    jsc: ArrayPydanticJSON, key_reference: list[str]
) -> tuple[ArrayOpenAIStrictJSON, list[str]]
cast_strict_prop(
    jsc: NumberPydanticJSON, key_reference: list[str]
) -> tuple[NumberOpenAIStrictJSON, list[str]]
cast_strict_prop(
    jsc: BooleanPydanticJSON, key_reference: list[str]
) -> tuple[BooleanOpenAIStrictJSON, list[str]]
cast_strict_prop(
    jsc: NullPydanticJSON, key_reference: list[str]
) -> tuple[NullOpenAIStrictJSON, list[str]]
cast_strict_prop(
    jsc: PropertyPydanticJSON, key_reference: list[str]
) -> tuple[PropertyOpenAIStrictJSON, list[str]]

Cast a Pydantic JSON schema to a strict OpenAI-compatible one.

PARAMETER DESCRIPTION
jsc

The Pydantic JSON schema.

TYPE: PropertyPydanticJSON

key_reference

The key reference.

TYPE: list[str]

RETURNS DESCRIPTION
tuple[PropertyOpenAIStrictJSON, list[str]]

The OpenAI-compatible JSON schema and any errors.

Source code in conatus/actions/json_schema.py
def cast_strict_prop(  # noqa: C901, PLR0911, PLR0912, PLR0915
    jsc: PropertyPydanticJSON, key_reference: list[str]
) -> tuple[PropertyOpenAIStrictJSON, list[str]]:
    """Cast a Pydantic JSON schema to a strict OpenAI-compatible one.

    Args:
        jsc: The Pydantic JSON schema.
        key_reference: The key reference.

    Returns:
        The OpenAI-compatible JSON schema and any errors.
    """
    errors: list[str] = []
    msg: str = ""
    desc = jsc.description or NO_DESC
    if getattr(jsc, "default", None):
        desc += f" -- Default: {getattr(jsc, 'default', None)}"

    match jsc:
        case AnyPydanticJSON():
            msg = "Missing 'type', '$ref', or 'anyOf' in "
            msg += f"{'.'.join(key_reference)}"
            msg += " This likely means you didn't specify a type."
            return jsc, [msg]

        case RefPydanticJSON():
            # We don't put a description here because we will add it in the
            # backend.
            if jsc.default is not None:
                # This happens with `Existing` types.
                return AnyOfOpenAIStrictJSON(
                    anyOf=[
                        RefOpenAIStrictJSON(**{"$ref": jsc.ref}),
                        NullOpenAIStrictJSON(type="null"),
                    ],
                ), errors
            return RefOpenAIStrictJSON(**{"$ref": jsc.ref}), errors

        case AnyOfPydanticJSON():
            any_of_res = [
                cast_strict_prop(sub_jsc, [*key_reference, f"anyOf[{i}]"])
                for i, sub_jsc in enumerate(jsc.any_of)
            ]
            any_of = [res[0] for res in any_of_res]
            err_delta = [err for res in any_of_res for err in res[1]]
            errors.extend(err_delta)
            if jsc.default is not None and not any(
                isinstance(element, NullOpenAIStrictJSON) for element in any_of
            ):
                any_of.append(NullOpenAIStrictJSON(type="null"))
            return AnyOfOpenAIStrictJSON(
                anyOf=any_of,
                title=jsc.title,
                description=desc if desc != NO_DESC else None,
            ), errors

        case StringPydanticJSON():
            if jsc.format:
                desc += f" -- Format: {jsc.format}"
            if jsc.pattern:
                desc += f" -- Pattern: {jsc.pattern}"
            if jsc.min_length or jsc.max_length:
                desc += f" -- Length: {jsc.min_length or 0} to "
                desc += f"{jsc.max_length or 'infinity'}"
            return StringOpenAIStrictJSON(
                title=jsc.title,
                description=desc if desc != NO_DESC else None,
                enum=jsc.enum,
                type="string" if jsc.default is None else ("string", "null"),
            ), errors

        case ObjectPydanticJSON():
            properties: dict[str, PropertyOpenAIStrictJSON] = {}
            # Because Pydantic is relatively aggressive with 'defs', you
            # generally don't see nested properties. So this is fallback.
            if (
                isinstance(jsc.properties, dict) and len(jsc.properties) > 0
            ):  # pragma: no cover
                for arg_name, sub_jsc in jsc.properties.items():
                    properties[arg_name], err_delta = cast_strict_prop(
                        sub_jsc, [*key_reference, "properties", arg_name]
                    )
                    errors.extend(err_delta)
            else:
                msg = f"Empty 'properties' in {'.'.join(key_reference)}."
                msg += " This means you expect a dict with unspecified keys."
                msg += " But in strict mode, the LLM will return '{}'."
                errors.append(msg)
                properties = {}
            return ObjectOpenAIStrictJSON(
                title=jsc.title,
                description=desc if desc != NO_DESC else None,
                type="object" if jsc.default is None else ("object", "null"),
                properties=properties,
                required=list(properties.keys()),
                additionalProperties=False,
            ), errors

        case ArrayPydanticJSON():
            if jsc.unique_items:
                desc += " -- Items NEED to be distinct. NO DUPLICATES."
            if jsc.max_items or jsc.min_items:
                desc += f" -- Number of items: {jsc.min_items or 0}"
                desc += f" to {jsc.max_items or 'infinity'}"
            if isinstance(jsc.items, AnyPydanticJSON) and jsc.prefix_items:
                jsc.items = AnyOfPydanticJSON(anyOf=jsc.prefix_items)
            items, err_delta = cast_strict_prop(
                jsc.items, [*key_reference, "items"]
            )
            errors.extend(err_delta)
            return ArrayOpenAIStrictJSON(
                title=jsc.title,
                description=desc if desc != NO_DESC else None,
                items=items,
                type="array" if jsc.default is None else ("array", "null"),
            ), errors

        case NumberPydanticJSON():
            display_range = any(
                [
                    jsc.minimum,
                    jsc.maximum,
                    jsc.exclusive_maximum,
                    jsc.exclusive_minimum,
                ]
            )
            if display_range:
                desc += " -- Range: "
                desc += f"{jsc.minimum or jsc.exclusive_minimum or 0} to "
                desc += f"{jsc.maximum or jsc.exclusive_maximum or 'infinity'}"
            return NumberOpenAIStrictJSON(
                title=jsc.title,
                description=desc if desc != NO_DESC else None,
                enum=jsc.enum,
                type="number" if jsc.default is None else ("number", "null"),
            ), errors

        case BooleanPydanticJSON():
            return BooleanOpenAIStrictJSON(
                title=jsc.title,
                description=desc if desc != NO_DESC else None,
                type="boolean" if jsc.default is None else ("boolean", "null"),
            ), errors

        # Should be comprehensive pattern matching
        case NullPydanticJSON():  # pragma: no branch
            return NullOpenAIStrictJSON(
                title=jsc.title,
                description=desc if desc != NO_DESC else None,
                type="null",
            ), errors

cast_strict_mode

cast_strict_mode(
    oai_jsc: OpenAIJSONSchema,
) -> OpenAIJSONStrictFnSchema

Cast the OpenAI JSON schema to strict mode.

PARAMETER DESCRIPTION
oai_jsc

The OpenAI JSON schema.

TYPE: OpenAIJSONSchema

RETURNS DESCRIPTION
OpenAIJSONStrictFnSchema

The OpenAI JSON schema in strict mode.

RAISES DESCRIPTION
OpenAIStrictCastError

If the schema is incorrect.

Source code in conatus/actions/json_schema.py
def cast_strict_mode(oai_jsc: OpenAIJSONSchema) -> OpenAIJSONStrictFnSchema:  # noqa: C901, PLR0912
    """Cast the OpenAI JSON schema to strict mode.

    Args:
        oai_jsc: The OpenAI JSON schema.

    Returns:
        The OpenAI JSON schema in strict mode.

    Raises:
        OpenAIStrictCastError: If the schema is incorrect.
    """
    errors: list[str] = []
    props = oai_jsc.parameters.properties
    defs = oai_jsc.parameters.defs

    new_props: dict[str, PropertyOpenAIStrictJSON] = {}
    for arg, arg_schema in props.items():
        # Process the properties
        new_props[arg], err_delta = cast_strict_prop(
            arg_schema, ["properties", arg]
        )
        errors.extend(err_delta)

    new_defs: dict[str, PropertyOpenAIStrictJSON] = {}
    oai_strict_jsc = OpenAIJSONStrictFnSchema(
        name=oai_jsc.name,
        description=oai_jsc.description,
        strict=True,
        parameters=OpenAIJSONStrictSchemaParameters(
            properties=new_props,
            required=list(new_props.keys()),
            additionalProperties=False,
            type="object",
            **{"$defs": None},
        ),
    )
    if not defs:
        if errors:
            msg = CAST_STRICT_ERR_MSG + str(oai_jsc) + "\n"
            msg += "\n".join(errors)
            raise OpenAIStrictCastError(msg)
        return oai_strict_jsc

    for arg_name, arg_schema in props.items():
        # Transfer the description and title to the ref
        if (
            isinstance(arg_schema, RefPydanticJSON)
            and defs
            and ((arg_schema_ref := arg_schema.ref.split("/")[-1]) in defs)
        ):
            def_desc = (
                f"Description for parameter '{arg_name}': "
                f"{arg_schema.description}"
            )
            if isinstance(defs[arg_schema_ref].description, str):
                # NOTE: This awkward cast is here to make type-checking happy.
                defs[arg_schema_ref].description = (
                    str(defs[arg_schema_ref].description) + f" -- {def_desc}"
                )
            else:  # pragma: no cover
                defs[arg_schema_ref].description = def_desc
            # Pydantic always provides a title, but just in case
            if not defs[arg_schema_ref].title:  # pragma: no cover
                defs[arg_schema_ref].title = arg_schema.title
    for def_key, def_ in defs.items():
        desc = def_.description or NO_DESC
        # Doesn't seem like there's ever a default for $defs, but keeping
        # this in case.
        if getattr(def_, "default", None):  # pragma: no cover
            desc += f" -- Default: {getattr(def_, 'default', None)}"
        if isinstance(def_, ObjectPydanticJSON) and def_.properties:
            new_def_prop_dict: dict[str, PropertyOpenAIStrictJSON] = {}
            if isinstance(def_.properties, AnyPydanticJSON):  # pragma: no cover
                msg = f"Empty 'properties' in '$defs.{def_key}'."
                msg += " This means you expect a dict with unspecified keys."
                msg += " But in strict mode, the LLM will return '{}'."
                errors.append(msg)
                continue
            for arg, arg_schema in def_.properties.items():
                new_def_prop_dict[arg], err_delta = cast_strict_prop(
                    arg_schema, ["$defs", def_key, "properties", arg]
                )
                errors.extend(err_delta)
            new_defs[def_key] = ObjectOpenAIStrictJSON(
                title=def_.title,
                description=def_.description,
                type="object",
                properties=new_def_prop_dict,
                required=list(new_def_prop_dict.keys()),
                additionalProperties=False,
            )
        else:
            new_defs[def_key], err_delta = cast_strict_prop(
                def_, ["$defs", def_key]
            )

    if errors:
        msg = CAST_STRICT_ERR_MSG + str(oai_jsc) + "\n"
        msg += "\n".join(errors)
        raise OpenAIStrictCastError(msg)

    oai_strict_jsc.parameters.defs = new_defs
    return oai_strict_jsc

generate_openai_json_schema

generate_openai_json_schema(
    json_schema_pydantic_model: (
        type[BaseModel] | TypeAdapter[ParamType]
    ),
    *,
    strict_mode: bool = False,
    convert_non_objects_to_objects: Literal[False] = False,
    remove_refs: bool = False
) -> FunctionDefinition
generate_openai_json_schema(
    json_schema_pydantic_model: (
        type[BaseModel] | TypeAdapter[ParamType]
    ),
    *,
    strict_mode: bool = False,
    convert_non_objects_to_objects: Literal[True],
    remove_refs: bool = False
) -> tuple[FunctionDefinition, bool]
generate_openai_json_schema(
    json_schema_pydantic_model: (
        type[BaseModel] | TypeAdapter[ParamType]
    ),
    *,
    strict_mode: bool = False,
    convert_non_objects_to_objects: bool = False,
    remove_refs: bool = False
) -> FunctionDefinition | tuple[FunctionDefinition, bool]

Transform the Pydantic JSON Schema into an OpenAI-compatible one.

PARAMETER DESCRIPTION
json_schema_pydantic_model

The Pydantic model (or TypeAdapter) representing the JSON schema of the action.

TYPE: type[BaseModel] | TypeAdapter[ParamType]

strict_mode

Whether to generate the schema in strict mode.

TYPE: bool DEFAULT: False

convert_non_objects_to_objects

Flag to allow the conversion of non- object JSON schemas to object JSON schemas. This is only relevant for TypeAdapters, which can sometimes generate JSON schemas for lists, strings, etc. If set to True , the function will convert these non-object JSON schemas before validating them. It will also return a boolean indicating whether the conversion was necessary.

TYPE: bool DEFAULT: False

remove_refs

Whether to remove references from the JSON schema.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
FunctionDefinition | tuple[FunctionDefinition, bool]

If convert_non_objects_to_objects is True, a tuple containing the OpenAI-compatible JSON schema and a boolean indicating whether the conversion was necessary. In this instance, the boolean is True if the conversion was necessary and False otherwise. Otherwise, the OpenAI-compatible JSON schema.

Source code in conatus/actions/json_schema.py
def generate_openai_json_schema(
    json_schema_pydantic_model: type[BaseModel] | TypeAdapter[ParamType],
    *,
    strict_mode: bool = False,
    convert_non_objects_to_objects: bool = False,
    remove_refs: bool = False,
) -> FunctionDefinition | tuple[FunctionDefinition, bool]:
    """Transform the Pydantic JSON Schema into an OpenAI-compatible one.

    Args:
        json_schema_pydantic_model: The Pydantic model (or TypeAdapter)
            representing the JSON schema of the action.
        strict_mode: Whether to generate the schema in strict mode.
        convert_non_objects_to_objects: Flag to allow the conversion of non-
            object JSON schemas to object JSON schemas. This is only relevant
            for [`TypeAdapter`][pydantic.TypeAdapter]s, which can sometimes
            generate JSON schemas for lists, strings, etc. If set to [`True`
            ][True], the function will convert these non-object JSON schemas
            before validating them. It will also return a boolean indicating
            whether the conversion was necessary.
        remove_refs: Whether to remove references from the JSON schema.

    Returns:
        If `convert_non_objects_to_objects` is [`True`][True], a tuple
            containing the OpenAI-compatible JSON schema and a boolean
            indicating whether the conversion was necessary. In this instance,
            the boolean is [`True`][True] if the conversion was necessary and
            [`False`][False] otherwise. Otherwise, the OpenAI-compatible
            JSON schema.
    """
    json_schema, conversion_was_necessary = get_complete_json_schema(
        json_schema_pydantic_model,
        convert_non_objects_to_objects=convert_non_objects_to_objects,
    )

    # Validation of the Pydantic model before the transformation to a dict
    pyd_jsc = PydanticJSONSchema(**json_schema)
    desc = pyd_jsc.description or NO_DESC
    name = pyd_jsc.title.split("JSONSchema")[0]

    if remove_refs:
        pyd_jsc.remove_refs()

    oai_jsc = OpenAIJSONSchema(
        name=name,
        description=desc,
        parameters=OpenAIJSONSchemaParameters(
            **pyd_jsc.model_dump(by_alias=True)
        ),
    )

    fn_def = cast(  # pyright: ignore[reportInvalidCast]
        "FunctionDefinition",
        (
            cast_strict_mode(oai_jsc).model_dump(
                by_alias=True, exclude_none=True
            )
            if strict_mode
            else oai_jsc.model_dump(by_alias=True, exclude_none=True)
        ),
    )
    if convert_non_objects_to_objects:
        return fn_def, conversion_was_necessary
    return fn_def

get_complete_json_schema

get_complete_json_schema(
    json_schema_pydantic_model: (
        type[BaseModel] | TypeAdapter[ParamType]
    ),
    *,
    convert_non_objects_to_objects: bool = False
) -> tuple[dict[str, Any], bool]

Get the complete JSON schema for a Pydantic model.

PARAMETER DESCRIPTION
json_schema_pydantic_model

The Pydantic model (or TypeAdapter) representing the JSON schema of the action.

TYPE: type[BaseModel] | TypeAdapter[ParamType]

convert_non_objects_to_objects

Flag to allow the conversion of non- object JSON schemas to object JSON schemas. This is only relevant for TypeAdapters, which can sometimes generate JSON schemas for lists, strings, etc.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
dict[str, Any]

The complete JSON schema.

bool

Whether the schema was converted to an item object.

Source code in conatus/actions/json_schema.py
def get_complete_json_schema(
    json_schema_pydantic_model: type[BaseModel] | TypeAdapter[ParamType],
    *,
    convert_non_objects_to_objects: bool = False,
) -> tuple[dict[str, Any], bool]:  # pyright: ignore[reportExplicitAny]
    """Get the complete JSON schema for a Pydantic model.

    Args:
        json_schema_pydantic_model: The Pydantic model (or TypeAdapter)
            representing the JSON schema of the action.
        convert_non_objects_to_objects: Flag to allow the conversion of non-
            object JSON schemas to object JSON schemas. This is only relevant
            for [`TypeAdapter`][pydantic.TypeAdapter]s, which can sometimes
            generate JSON schemas for lists, strings, etc.

    Returns:
        (dict[str, Any]): The complete JSON schema.
        (bool): Whether the schema was converted to an item object.
    """
    if isinstance(json_schema_pydantic_model, TypeAdapter):
        json_schema = json_schema_pydantic_model.json_schema()
    else:
        json_schema = json_schema_pydantic_model.model_json_schema()

    conversion_was_necessary = False
    # If we get a JSON schema (say, from a TypeAdapter) that is not already
    # an object, we need to wrap it in an object.
    if (
        json_schema.get("type") != "object"
        or json_schema.get("additionalProperties", None) is not None
    ) and convert_non_objects_to_objects:
        old_json_schema = json_schema.copy()
        json_schema = {
            "type": "object",
            "title": "itemJSONSchema",
            "properties": {"item": old_json_schema},
        }
        if "$defs" in old_json_schema:
            defs = old_json_schema.pop("$defs")
            json_schema["$defs"] = defs
        conversion_was_necessary = True

    return json_schema, conversion_was_necessary

generate_structured_output_openai_json_schema

generate_structured_output_openai_json_schema(
    json_schema_pydantic_model: (
        type[BaseModel] | TypeAdapter[ParamType]
    ),
    *,
    strict_mode: bool = False
) -> tuple[StructuredOutputDefinition, bool]

Transform the Pydantic JSON Schema into an OpenAI-compatible one.

PARAMETER DESCRIPTION
json_schema_pydantic_model

The Pydantic model representing the JSON schema of the action.

TYPE: type[BaseModel] | TypeAdapter[ParamType]

strict_mode

Whether to generate the schema in strict mode.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
StructuredOutputDefinition

The OpenAI-compatible JSON schema.

bool

Whether the schema was converted to an item object.

Source code in conatus/actions/json_schema.py
def generate_structured_output_openai_json_schema(
    json_schema_pydantic_model: type[BaseModel] | TypeAdapter[ParamType],
    *,
    strict_mode: bool = False,
) -> tuple[StructuredOutputDefinition, bool]:
    """Transform the Pydantic JSON Schema into an OpenAI-compatible one.

    Args:
        json_schema_pydantic_model: The Pydantic model representing the JSON
            schema of the action.
        strict_mode: Whether to generate the schema in strict mode.

    Returns:
        (StructuredOutputDefinition): The OpenAI-compatible JSON schema.
        (bool): Whether the schema was converted to an item object.
    """
    schema, conversion_was_necessary = generate_openai_json_schema(
        json_schema_pydantic_model,
        strict_mode=strict_mode,
        convert_non_objects_to_objects=True,
    )
    return {
        "name": schema["name"],
        "description": schema["description"],
        "schema": schema["parameters"],
        "strict": schema["strict"],
    }, conversion_was_necessary