GitHub
ESC

Structured Output

Overview

fm.cr supports generating structured output that conforms to a JSON schema. You can either define Crystal structs with the Generable module for type-safe deserialization, or work with raw JSON schemas directly.

Using Generable

Include both JSON::Serializable and Fm::Generable in a struct to automatically generate a JSON schema and deserialize responses:

struct Person
  include JSON::Serializable
  include Fm::Generable

  getter name : String
  getter age : Int32
  getter occupation : String
end

person = session.respond_structured(Person, "Generate a fictional software engineer.")
puts "#{person.name}, age #{person.age} -- #{person.occupation}"

Generable automatically maps Crystal types to JSON Schema types:

Crystal Type JSON Schema Type
String string
Int32, Int64, etc. integer
Float32, Float64 number
Bool boolean
Array(T) array
Nested Generable types object

Guide Annotations

Use the @[Fm::Guide] annotation to add constraints that guide the model's generation:

struct MovieReview
  include JSON::Serializable
  include Fm::Generable

  @[Fm::Guide(description: "The movie title")]
  getter title : String

  @[Fm::Guide(any_of: ["positive", "negative", "neutral"])]
  getter sentiment : String

  @[Fm::Guide(minimum: 1, maximum: 10)]
  getter rating : Int32

  @[Fm::Guide(min_items: 1, max_items: 5)]
  getter tags : Array(String)
end

Available Guide Constraints

Constraint Type Description
description String Human-readable field description
any_of Array Restrict string to specific values (JSON Schema enum)
constant Any Fix field to a single constant value
minimum Number Minimum numeric value
maximum Number Maximum numeric value
pattern String Regex pattern for string values
min_items Int Minimum array length
max_items Int Maximum array length
count Int Exact array length (sets both minItems and maxItems)

Raw JSON Schema

For more control, use respond_json with a JSON schema string directly:

schema = %({"type":"object","properties":{"city":{"type":"string"},"population":{"type":"integer"}},"required":["city","population"]})

json = session.respond_json("Largest city in Japan", schema)
puts json  # => {"city":"Tokyo","population":13960000}

The return value is a raw JSON string. Parse it with JSON.parse if needed:

data = JSON.parse(json)
puts data["city"]        # => Tokyo
puts data["population"]  # => 13960000

Streaming Structured Output

Combine structured output with streaming using stream_json:

schema = %({"type":"object","properties":{"name":{"type":"string"},"bio":{"type":"string"}},"required":["name","bio"]})

session.stream_json("Generate a character profile.", schema) do |chunk|
  print chunk
  STDOUT.flush
end
puts