-
-
Notifications
You must be signed in to change notification settings - Fork 69
Open
Description
There are use cases to have arrays of objects in a JSON schema for structured output, which are supported by OpenAI: https://platform.openai.com/docs/guides/structured-outputs#definitions-are-supported
It appears this works with ActiveRecord, but not with ActiveModel:
activeagent/lib/active_agent/schema_generator.rb
Lines 188 to 199 in d8fb8f0
| when :has_many, :has_and_belongs_to_many | |
| schema[:properties][association.name.to_s] = { | |
| type: "array", | |
| items: { "$ref": "#/$defs/#{association.name.to_s.singularize}" } | |
| } | |
| if options[:nested_associations] | |
| nested_schema = json_schema_from_model( | |
| association.klass, | |
| options.merge(include_associations: false) | |
| ) | |
| schema[:$defs][association.name.to_s.singularize] = nested_schema | |
| end |
Could we add support for this to ActiveAgent::SchemaGenerator for ActiveModel?
I have a hacky prototype / monkeypatch, but would be great to have upstream support for this.
class Schemas::ApplicationSchema
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveAgent::SchemaGenerator
# ActiveModel doesn't support associations like ActiveRecord.
#
# This adds a simple `has_many` declaration for nested schemas, enabling
# JSON Schema generation with $defs/$ref for AI agent structured output.
class << self
def associations
@associations ||= {}
end
def has_many(name, class_name:)
klass = class_name.classify.constantize
associations[name.to_s] = {
type: :array,
class: klass
}
attribute name, default: -> { [] }
end
def to_json_schema(options = {})
result = super
if associations.any?
schema = options[:strict] ? result[:schema] : result
schema[:$defs] ||= {}
associations.each do |association_name, config|
klass = config[:class]
singular_name = association_name.to_s.singularize
nested_schema = klass.to_json_schema(strict: options[:strict], name: singular_name)
schema_to_store = options[:strict] ? nested_schema[:schema] : nested_schema
schema[:$defs][singular_name] = schema_to_store
schema[:properties][association_name] = {
type: "array",
items: {"$ref": "#/$defs/#{singular_name}"}
}
end
end
result
end
end
endrequire "rails_helper"
class Schemas::ExampleSchema < Schemas::ApplicationSchema
class Address < Schemas::ApplicationSchema
attribute :street, :string
attribute :city, :string
attribute :state, :string
attribute :zip, :string
end
attribute :name, :string
attribute :age, :integer
attribute :email, :string
has_many :addresses, class_name: Address.name
validates :name, presence: true
validates :age, presence: true
validates :email, presence: true
end
RSpec.describe Schemas::ApplicationSchema, type: :model do
describe "#to_json_schema" do
let(:strict) { true }
subject { Schemas::ExampleSchema.to_json_schema(strict:, name: "example_schema") }
context "when strict" do
let(:strict) { true }
it "returns a valid JSON schema" do
expect(subject).to eq(
{
name: "example_schema",
schema: {
type: "object",
properties: {
"name" => {type: "string"},
"age" => {type: "integer"},
"email" => {type: "string"},
"addresses" => {
type: "array",
items: {"$ref": "#/$defs/address"}
}
},
required: ["addresses", "age", "email", "name"],
additionalProperties: false,
"$defs": {
"address" => {
type: "object",
properties: {
"street" => {type: "string"},
"city" => {type: "string"},
"state" => {type: "string"},
"zip" => {type: "string"}
},
required: ["city", "state", "street", "zip"],
additionalProperties: false
}
}
},
strict: true
}
)
end
end
context "when not strict" do
let(:strict) { false }
it "returns a valid JSON schema" do
expect(subject).to eq({
type: "object",
properties: {
"name" => {type: "string"},
"age" => {type: "integer"},
"email" => {type: "string"},
"addresses" => {
type: "array",
items: {"$ref": "#/$defs/address"}
}
},
required: ["name", "age", "email"],
additionalProperties: false,
"$defs": {
"address" => {
type: "object",
properties: {
"street" => {type: "string"},
"city" => {type: "string"},
"state" => {type: "string"},
"zip" => {type: "string"}
},
required: [],
additionalProperties: false
}
}
})
end
end
end
endMetadata
Metadata
Assignees
Labels
No labels