-
-
Notifications
You must be signed in to change notification settings - Fork 343
Structured output & json mode #122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 37 commits
39a594d
290764a
1a766d7
16ce84a
9816968
2d30f10
a651e2d
0513cea
5a749d2
a1e01d4
376156e
96a9d9c
87ddf79
642b3c9
39c3902
fb39411
126bebf
21dea58
44a77d5
e7ee70d
68d39eb
320c611
98ff547
65c2215
15dc0e4
2d19063
78ba898
a20c1b7
370ef1d
b8cc7ec
932bc11
cc13503
6993978
3de661f
09d78a6
eb2f95b
0f1e4d8
17b179d
acfc00c
f93bed3
90b57f7
5dfe022
4a66560
2eb7790
f778328
02af5b2
1897092
a9ee1c5
0ac9a3d
2c72684
0dc953c
837e951
ad061b5
43f9c95
fc64702
629c29c
7153695
fa06863
3a9c515
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # CLAUDE.md | ||
|
|
||
| This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||
|
|
||
| ## Build & Test Commands | ||
| - Build: `bundle exec rake build` | ||
| - Install dependencies: `bundle install` | ||
| - Run all tests: `bundle exec rspec` | ||
| - Run specific test: `bundle exec rspec spec/ruby_llm/chat_spec.rb` | ||
| - Run specific test by description: `bundle exec rspec -e "description"` | ||
| - Re-record VCR cassettes: `bundle exec rake vcr:record[all]` or `bundle exec rake vcr:record[openai,anthropic]` | ||
| - Check style: `bundle exec rubocop` | ||
| - Auto-fix style: `bundle exec rubocop -A` | ||
|
|
||
| ## Code Style Guidelines | ||
| - Follow [Standard Ruby](https://github.com/testdouble/standard) style | ||
| - Use frozen_string_literal comment at the top of each file | ||
| - Follow model naming conventions from CONTRIBUTING.md when adding providers | ||
| - Use RSpec for tests with descriptive test names that form clean VCR cassettes | ||
| - Handle errors with specific error classes from RubyLLM::Error | ||
| - Use method keyword arguments with Ruby 3+ syntax | ||
| - Document public APIs with YARD comments | ||
| - Maintain backward compatibility for minor version changes | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| --- | ||
| layout: default | ||
| title: Structured Output | ||
| parent: Guides | ||
| nav_order: 7 | ||
| --- | ||
|
|
||
| # Structured Output | ||
|
|
||
| RubyLLM allows you to request structured data from language models by providing a JSON schema. When you use the `with_response_format` method, RubyLLM will ensure the model returns data matching your schema instead of free-form text. | ||
|
||
|
|
||
| ## Schema-Based Output (Recommended) | ||
|
|
||
| We recommend providing a schema for structured data: | ||
|
|
||
| ```ruby | ||
| # Define a JSON schema | ||
| schema = { | ||
| type: "object", | ||
| properties: { | ||
| name: { type: "string" }, | ||
| age: { type: "integer" }, | ||
| interests: { type: "array", items: { type: "string" } } | ||
| }, | ||
| required: ["name", "age", "interests"] | ||
| } | ||
|
|
||
| response = RubyLLM.chat(model: "gpt-4o") | ||
| .with_response_format(schema) | ||
| .ask("Create a profile for a Ruby developer") | ||
| ``` | ||
|
|
||
| RubyLLM intelligently handles your schema based on the model's capabilities: | ||
|
|
||
| - For models with native schema support (like GPT-4o): Uses API-level schema validation | ||
| - For models without schema support: Automatically adds schema instructions to the system message | ||
|
|
||
| ## Simple JSON Mode (Alternative) | ||
|
|
||
| For cases where you just need well-formed JSON: | ||
|
|
||
| ```ruby | ||
| response = RubyLLM.chat(model: "gpt-4.1-nano") | ||
| .with_response_format(:json) | ||
| .ask("Create a profile for a Ruby developer") | ||
| ``` | ||
|
|
||
| This uses OpenAI's `response_format: {type: "json_object"}` parameter, works with most OpenAI models, and guarantees valid JSON without enforcing a specific structure. | ||
|
|
||
| ## Strict and Non-Strict Modes | ||
|
||
|
|
||
| By default, RubyLLM operates in "strict mode" which only allows models that officially support the requested output format. If you try to use a schema with a model that doesn't support schema validation, RubyLLM will raise an `UnsupportedStructuredOutputError`. | ||
|
|
||
| For broader compatibility, you can disable strict mode: | ||
|
|
||
| ```ruby | ||
| # Use schema with a model that doesn't currently support schema validation on RubyLLM | ||
| response = RubyLLM.chat(model: "gemini-2.0-flash") | ||
| .with_response_format(schema, strict: false) | ||
| .ask("Create a profile for a Ruby developer") | ||
| ``` | ||
|
|
||
| In non-strict mode: | ||
|
|
||
| - RubyLLM doesn't validate if the model supports the requested format | ||
| - The schema is automatically added to the system message | ||
| - JSON parsing is handled automatically | ||
| - Works with most models that can produce JSON output, including Claude and Gemini | ||
|
|
||
| This allows you to use schema-based output with a wider range of models, though without API-level schema validation. | ||
|
|
||
| ## Error Handling | ||
|
|
||
| RubyLLM provides two main error types for structured output: | ||
|
|
||
| 1. **UnsupportedStructuredOutputError**: Raised when using schema-based output with a model that doesn't support it in strict mode: | ||
| 2. **InvalidStructuredOutput**: Raised if the model returns invalid JSON: | ||
|
|
||
| Note: RubyLLM checks that responses are valid JSON but doesn't verify conformance to the schema structure. For full schema validation, use a library like `json-schema`. | ||
|
|
||
| ## With ActiveRecord and Rails | ||
|
||
|
|
||
| The structured output feature works seamlessly with RubyLLM's Rails integration. Message content can be either a String or a Hash. | ||
|
|
||
| If you're storing message content in your database, ensure your messages table can store JSON. PostgreSQL's `jsonb` column type is ideal: | ||
|
|
||
| ```ruby | ||
| # In a migration | ||
| create_table :messages do |t| | ||
| t.references :chat | ||
| t.string :role | ||
| t.jsonb :content # Use jsonb for efficient JSON storage | ||
| # other fields... | ||
| end | ||
| ``` | ||
|
|
||
| If you have an existing application with a text-based content column, add serialization: | ||
|
|
||
| ```ruby | ||
| # In your Message model | ||
| class Message < ApplicationRecord | ||
| serialize :content, JSON | ||
| acts_as_message | ||
| end | ||
| ``` | ||
|
|
||
| ## Tips for Effective Schemas | ||
|
|
||
| 1. **Be specific**: Provide clear property descriptions to guide the model. | ||
| 2. **Start simple**: Begin with basic schemas and add complexity gradually. | ||
| 3. **Include required fields**: Specify which properties are required. | ||
| 4. **Use appropriate types**: Match JSON Schema types to your expected data. | ||
| 5. **Validate locally**: Consider using a gem like `json-schema` for additional validation. | ||
| 6. **Test model compatibility**: Different models have different levels of schema support. | ||
|
|
||
| ## Example: Complex Schema | ||
|
|
||
| ```ruby | ||
| schema = { | ||
| type: "object", | ||
| properties: { | ||
| products: { | ||
| type: "array", | ||
| items: { | ||
| type: "object", | ||
| properties: { | ||
| name: { type: "string" }, | ||
| price: { type: "number" }, | ||
| in_stock: { type: "boolean" }, | ||
| categories: { | ||
| type: "array", | ||
| items: { type: "string" } | ||
| } | ||
| }, | ||
| required: ["name", "price", "in_stock"] | ||
| } | ||
| }, | ||
| total_products: { type: "integer" }, | ||
| store_info: { | ||
| type: "object", | ||
| properties: { | ||
| name: { type: "string" }, | ||
| location: { type: "string" } | ||
| } | ||
| } | ||
| }, | ||
| required: ["products", "total_products"] | ||
| } | ||
|
|
||
| inventory = chat.with_response_format(schema) # Let RubyLLM handle the schema formatting | ||
| .ask("Create an inventory for a Ruby gem store") | ||
| ``` | ||
|
|
||
| ### Limitations | ||
|
|
||
| - Schema validation is only available at the API level for certain OpenAI models | ||
| - No enforcement of required fields or data types without external validation | ||
| - For full schema validation, use a library like `json-schema` to verify the output | ||
|
|
||
| RubyLLM handles all the complexity of supporting different model capabilities, so you can focus on your application logic. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: should we include or not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels like yet another thing to manage IMO, but if you don't think it'll need to be modified often then sure, why not I guess?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no