Skip to content

About

LLM-friendly annotated Julia types.

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Latest commit

 

History

35 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DescribedTypes

Build Status codecov Stable Docs Dev Docs

This package provides a way to annotate Julia types with descriptions, which are then used to generate JSON Schemas compatible with LLM provider APIs (for structured-output functionality).

OpenAI uses JSON Schema in two different places, and the package supports both:

Example — Response Format (OPENAI)

using DescribedTypes
using JSON

struct Person
    name::String
    age::Int
end

DescribedTypes.annotate(::Type{Person}) = Annotation(
    name="Person",
    description="A schema for a person.",
    parameters=Dict(
        :name => Annotation(name="name", description="The name of the person", enum=["Alice", "Bob"]),
        :age  => Annotation(name="age", description="The age of the person")
    )
)

schema_dict = schema(Person, llm_adapter=OPENAI)
println(JSON.json(schema_dict, 2))

Output (generated automatically by docs/generate_readme.jl):

{
  "name": "Person",
  "description": "A schema for a person.",
  "strict": true,
  "schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "The name of the person",
        "enum": [
          "Alice",
          "Bob"
        ]
      },
      "age": {
        "type": "integer",
        "description": "The age of the person"
      }
    },
    "required": [
      "name",
      "age"
    ],
    "additionalProperties": false
  }
}

Example — Tool / Function Calling (OPENAI_TOOLS)

DescribedTypes.annotate(::Type{Person}) = Annotation(
    name="get_person",
    description="Fetches information about a person.",
    parameters=Dict(
        :name => Annotation(name="name", description="The name of the person", enum=["Alice", "Bob"]),
        :age  => Annotation(name="age", description="The age of the person")
    )
)

schema_dict = schema(Person, llm_adapter=OPENAI_TOOLS)
println(JSON.json(schema_dict, 2))

Output:

{
  "type": "function",
  "name": "get_person",
  "description": "Fetches information about a person.",
  "strict": true,
  "parameters": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "The name of the person",
        "enum": [
          "Alice",
          "Bob"
        ]
      },
      "age": {
        "type": "integer",
        "description": "The age of the person"
      }
    },
    "required": [
      "name",
      "age"
    ],
    "additionalProperties": false
  }
}

Function Method Tools (function -> schema, JSON -> call)

The package also supports direct function-method extraction and invocation:

# Weather lookup helper.
function weather(city::String, days::Int=3; unit::String="celsius", include_humidity::Bool=false)
    return (; city, days, unit, include_humidity)
end

DescribedTypes.annotate(::typeof(weather), ms::MethodSignature) = MethodAnnotation(
    name=:weather_tool,
    description="Weather lookup tool.",
    argsannot=Dict(
        :city => ArgAnnotation(name=:city, description="City name", required=true),
        :days => ArgAnnotation(name=:days, description="Forecast horizon", required=false),
        :unit => ArgAnnotation(name=:unit, description="Temperature unit", enum=["celsius", "fahrenheit"], required=false),
        :include_humidity => ArgAnnotation(name=:include_humidity, description="Include humidity signal", required=false),
    ),
)

tool_schema = schema(weather, llm_adapter=OPENAI_TOOLS)
result = callfunction(weather, Dict("city" => "Paris", "unit" => "fahrenheit"))

Tool schema output:

{
  "type": "function",
  "name": "weather_tool",
  "description": "Weather lookup tool.",
  "strict": true,
  "parameters": {
    "type": "object",
    "properties": {
      "city": {
        "type": "string",
        "description": "City name"
      },
      "days": {
        "type": [
          "integer",
          "null"
        ],
        "description": "Forecast horizon"
      },
      "unit": {
        "type": [
          "string",
          "null"
        ],
        "description": "Temperature unit",
        "enum": [
          "celsius",
          "fahrenheit",
          null
        ]
      },
      "include_humidity": {
        "type": [
          "boolean",
          "null"
        ],
        "description": "Include humidity signal"
      }
    },
    "required": [
      "city",
      "days",
      "unit",
      "include_humidity"
    ],
    "additionalProperties": false
  }
}

Function-call output:

{
  "city": "Paris",
  "days": 3,
  "unit": "fahrenheit",
  "include_humidity": false
}

Optional Fields

Both OPENAI and OPENAI_TOOLS modes enforce that all fields are listed in "required". Optional fields (Union{Nothing, T}) are represented as ["type", "null"], following the OpenAI structured-output requirement:

Although all fields must be required (and the model will return a value for each parameter), it is possible to emulate an optional parameter by using a union type with null.

Standard schema (optional field excluded from required):

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string"
    },
    "nickname": {
      "type": "string"
    }
  },
  "required": [
    "name"
  ]
}

OpenAI schema (optional field uses ["type", "null"]):

{
  "name": "PersonOpt",
  "description": "A person with an optional nickname.",
  "strict": true,
  "schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "Full name"
      },
      "nickname": {
        "type": [
          "string",
          "null"
        ],
        "description": "Optional nickname"
      }
    },
    "required": [
      "name",
      "nickname"
    ],
    "additionalProperties": false
  }
}

Symbol Enums and Duplicate Handling

Field-level annotation enums accept both String and Symbol values. Schema output still emits strings (JSON-compatible).

struct PersonSymbolEnum
    name::String
end

DescribedTypes.annotate(::Type{PersonSymbolEnum}) = Annotation(
    name="PersonSymbolEnum",
    description="A person with symbol-based enum annotations.",
    parameters=Dict(
        :name => Annotation(name="name", description="The name", enum=[:Alice, "Alice", :Bob]),
    ),
)

# Default duplicate policy is :dedupe
schema(PersonSymbolEnum, llm_adapter=OPENAI)

Output (:dedupe, default):

{
  "name": "PersonSymbolEnum",
  "description": "A person with symbol-based enum annotations.",
  "strict": true,
  "schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "The name",
        "enum": [
          "Alice",
          "Bob"
        ]
      }
    },
    "required": [
      "name"
    ],
    "additionalProperties": false
  }
}

To enforce strict duplicate handling, pass enum_duplicate_policy=:error:

schema(PersonSymbolEnum, llm_adapter=OPENAI, enum_duplicate_policy=:error)

Raises:

ArgumentError("Field `name` of PersonSymbolEnum: Duplicate enum value after normalization: \"Alice\".")

Output blocks in this README were generated by running julia docs/generate_readme.jl.

About

LLM-friendly annotated Julia types.

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages