dspy 0.30.1 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0bdf656ce5715a455d3fb36427878ae507d8d4bd9a7f092a6b9752196f9ebc4b
4
- data.tar.gz: cd21540e0eea82c1567c085b9ef2fd5c2d9742cd1ff08f855a16f4109893ebb2
3
+ metadata.gz: 972e09f00d8d5417d5c1af255eb01503fc33ab370264345b4c7de880b4f99fda
4
+ data.tar.gz: 21f6f7952a9caaf8398a24a69516147bf68c88e510b88aa2f45239786bbfd31b
5
5
  SHA512:
6
- metadata.gz: 992c191cbc6bbdd1bc770a83d4a1151869c4f57a20d7236a19b0add9c50329ec2dea901fe440488b2194fe96951d729c4509d33a9df8fb6c2dc7154c570e9c00
7
- data.tar.gz: 82e67a674db952801adf40407d34daf0e2808f4c1b4d99da60f63c33fd2033b7f5a2deaefe63ea91bf4bcc7129e7580772877df3ca3b69bf9894d2454a5bdf38
6
+ metadata.gz: 780f786797df9d50950c1526296c8bc7a0db87dab29078e14de2d32a0ec608ae30585963a4dda3a5261b60530b3dd59e0b9e5915eab6c8d60360c4d1b6e1d8af
7
+ data.tar.gz: 3fb5d69bb58d9b57905a6f9985ecdd5d792be0aa91fb5e980f437f5d8887144564697d1151ed9bb8ec742b4ae4c1efff5303b53d937603ac7719959ffb11cfd9
data/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
  [![Total Downloads](https://img.shields.io/gem/dt/dspy)](https://rubygems.org/gems/dspy)
5
5
  [![Build Status](https://img.shields.io/github/actions/workflow/status/vicentereig/dspy.rb/ruby.yml?branch=main&label=build)](https://github.com/vicentereig/dspy.rb/actions/workflows/ruby.yml)
6
6
  [![Documentation](https://img.shields.io/badge/docs-vicentereig.github.io%2Fdspy.rb-blue)](https://vicentereig.github.io/dspy.rb/)
7
+ [![Discord](https://img.shields.io/discord/1161519468141355160?label=discord&logo=discord&logoColor=white)](https://discord.gg/zWBhrMqn)
7
8
 
8
9
  > [!NOTE]
9
10
  > The core Prompt Engineering Framework is production-ready with
@@ -17,18 +18,13 @@
17
18
 
18
19
  **Build reliable LLM applications in idiomatic Ruby using composable, type-safe modules.**
19
20
 
20
- The Ruby framework for programming with large language models. DSPy.rb brings structured LLM programming to Ruby developers, programmatic Prompt Engineering and Context Engineering.
21
- Instead of wrestling with prompt strings and parsing responses, you define typed signatures using idiomatic Ruby to compose and decompose AI Worklows and AI Agents.
21
+ DSPy.rb is the Ruby-first surgical port of Stanford's [DSPy framework](https://github.com/stanfordnlp/dspy). It delivers structured LLM programming, prompt engineering, and context engineering in the language we love. Instead of wrestling with brittle prompt strings, you define typed signatures in idiomatic Ruby and compose workflows and agents that actually behave.
22
22
 
23
- **Prompts are the just Functions.** Traditional prompting is like writing code with string concatenation: it works until it doesn't. DSPy.rb brings you
24
- the programming approach pioneered by [dspy.ai](https://dspy.ai/): instead of crafting fragile prompts, you define modular
25
- signatures and let the framework handle the messy details.
23
+ **Prompts are just functions.** Traditional prompting is like writing code with string concatenation: it works until it doesn't. DSPy.rb brings you the programming approach pioneered by [dspy.ai](https://dspy.ai/): define modular signatures and let the framework deal with the messy bits.
26
24
 
27
- DSPy.rb is an idiomatic Ruby surgical port of Stanford's [DSPy framework](https://github.com/stanfordnlp/dspy). While implementing
28
- the core concepts of signatures, predictors, and the main optimization algorithms from the original Python library, DSPy.rb embraces Ruby
29
- conventions and adds Ruby-specific innovations like Sorbet-base Typed system, ReAct loops, and production-ready integrations like non-blocking Open Telemetry Instrumentation.
25
+ While we implement the same signatures, predictors, and optimization algorithms as the original library, DSPy.rb leans hard into Ruby conventions with Sorbet-based typing, ReAct loops, and production-ready integrations like non-blocking OpenTelemetry instrumentation.
30
26
 
31
- **What you get?** Ruby LLM applications that actually scale and don't break when you sneeze.
27
+ **What you get?** Ruby LLM applications that scale and don't break when you sneeze.
32
28
 
33
29
  Check the [examples](examples/) and take them for a spin!
34
30
 
@@ -50,8 +46,9 @@ bundle install
50
46
  ### Your First Reliable Predictor
51
47
 
52
48
  ```ruby
49
+ require 'dspy'
53
50
 
54
- # Configure DSPy globablly to use your fave LLM - you can override this on an instance levle.
51
+ # Configure DSPy globally to use your fave LLM (you can override per predictor).
55
52
  DSPy.configure do |c|
56
53
  c.lm = DSPy::LM.new('openai/gpt-4o-mini',
57
54
  api_key: ENV['OPENAI_API_KEY'],
@@ -82,7 +79,7 @@ class Classify < DSPy::Signature
82
79
  end
83
80
  end
84
81
 
85
- # Wire it to the simplest prompting technique - a Predictn.
82
+ # Wire it to the simplest prompting technique: a prediction loop.
86
83
  classify = DSPy::Predict.new(Classify)
87
84
  # it may raise an error if you mess the inputs or your LLM messes the outputs.
88
85
  result = classify.call(sentence: "This book was super fun to read!")
@@ -91,6 +88,12 @@ puts result.sentiment # => #<Sentiment::Positive>
91
88
  puts result.confidence # => 0.85
92
89
  ```
93
90
 
91
+ Save this as `examples/first_predictor.rb` and run it with:
92
+
93
+ ```bash
94
+ bundle exec ruby examples/first_predictor.rb
95
+ ```
96
+
94
97
  ### Sibling Gems
95
98
 
96
99
  DSPy.rb ships multiple gems from this monorepo so you can opt into features with heavier dependency trees (e.g., datasets pull in Polars/Arrow, MIPROv2 requires `numo-*` BLAS bindings) only when you need them. Add these alongside `dspy`:
@@ -98,6 +101,9 @@ DSPy.rb ships multiple gems from this monorepo so you can opt into features with
98
101
  | Gem | Description | Status |
99
102
  | --- | --- | --- |
100
103
  | `dspy-schema` | Exposes `DSPy::TypeSystem::SorbetJsonSchema` for downstream reuse. (Still required by the core `dspy` gem; extraction lets other projects depend on it directly.) | **Stable** (v1.0.0) |
104
+ | `dspy-openai` | Packages the OpenAI/OpenRouter/Ollama adapters plus the official SDK guardrails. Install whenever you call `openai/*`, `openrouter/*`, or `ollama/*`. [Adapter README](https://github.com/vicentereig/dspy.rb/blob/main/lib/dspy/openai/README.md) | **Stable** (v1.0.0) |
105
+ | `dspy-anthropic` | Claude adapters, streaming, and structured-output helpers behind the official `anthropic` SDK. [Adapter README](https://github.com/vicentereig/dspy.rb/blob/main/lib/dspy/anthropic/README.md) | **Stable** (v1.0.0) |
106
+ | `dspy-gemini` | Gemini adapters with multimodal + tool-call support via `gemini-ai`. [Adapter README](https://github.com/vicentereig/dspy.rb/blob/main/lib/dspy/gemini/README.md) | **Stable** (v1.0.0) |
101
107
  | `dspy-code_act` | Think-Code-Observe agents that synthesize and execute Ruby safely. (Add the gem or set `DSPY_WITH_CODE_ACT=1` before requiring `dspy/code_act`.) | **Stable** (v1.0.0) |
102
108
  | `dspy-datasets` | Dataset helpers plus Parquet/Polars tooling for richer evaluation corpora. (Toggle via `DSPY_WITH_DATASETS`.) | **Stable** (v1.0.0) |
103
109
  | `dspy-evals` | High-throughput evaluation harness with metrics, callbacks, and regression fixtures. (Toggle via `DSPY_WITH_EVALS`.) | **Stable** (v1.0.0) |
@@ -106,52 +112,13 @@ DSPy.rb ships multiple gems from this monorepo so you can opt into features with
106
112
  | `gepa` | GEPA optimizer core (Pareto engine, telemetry, reflective proposer). | **Stable** (v1.0.0) |
107
113
  | `dspy-o11y` | Core observability APIs: `DSPy::Observability`, async span processor, observation types. (Install or set `DSPY_WITH_O11Y=1`.) | **Stable** (v1.0.0) |
108
114
  | `dspy-o11y-langfuse` | Auto-configures DSPy observability to stream spans to Langfuse via OTLP. (Install or set `DSPY_WITH_O11Y_LANGFUSE=1`.) | **Stable** (v1.0.0) |
115
+ | `dspy-deep_search` | Production DeepSearch loop with Exa-backed search/read, token budgeting, and instrumentation (Issue #163). | **Stable** (v1.0.0) |
116
+ | `dspy-deep_research` | Planner/QA orchestration atop DeepSearch plus the memory supervisor used by the CLI example. | **Stable** (v1.0.0) |
117
+ | `sorbet-toon` | Token-Oriented Object Notation (TOON) codec, prompt formatter, and Sorbet mixins for BAML/TOON Enhanced Prompting. [Sorbet::Toon README](https://github.com/vicentereig/dspy.rb/blob/main/lib/sorbet/toon/README.md) | **Alpha** (v0.1.0) |
109
118
 
110
- Set the matching `DSPY_WITH_*` environment variables (see `Gemfile`) to include or exclude each sibling gem when running Bundler locally (for example `DSPY_WITH_GEPA=1` or `DSPY_WITH_O11Y_LANGFUSE=1`). Refer to `adr/013-dependency-tree.md` for the full dependency map and roadmap.
111
- ### Your First Reliable Predictor
112
-
113
- ```ruby
114
-
115
- # Configure DSPy globablly to use your fave LLM - you can override this on an instance levle.
116
- DSPy.configure do |c|
117
- c.lm = DSPy::LM.new('openai/gpt-4o-mini',
118
- api_key: ENV['OPENAI_API_KEY'],
119
- structured_outputs: true) # Enable OpenAI's native JSON mode
120
- end
121
-
122
- # Define a signature for sentiment classification - instead of writing a full prompt!
123
- class Classify < DSPy::Signature
124
- description "Classify sentiment of a given sentence." # sets the goal of the underlying prompt
125
-
126
- class Sentiment < T::Enum
127
- enums do
128
- Positive = new('positive')
129
- Negative = new('negative')
130
- Neutral = new('neutral')
131
- end
132
- end
133
-
134
- # Structured Inputs: makes sure you are sending only valid prompt inputs to your model
135
- input do
136
- const :sentence, String, description: 'The sentence to analyze'
137
- end
138
-
139
- # Structured Outputs: your predictor will validate the output of the model too.
140
- output do
141
- const :sentiment, Sentiment, description: 'The sentiment of the sentence'
142
- const :confidence, Float, description: 'A number between 0.0 and 1.0'
143
- end
144
- end
145
-
146
- # Wire it to the simplest prompting technique - a Predictn.
147
- classify = DSPy::Predict.new(Classify)
148
- # it may raise an error if you mess the inputs or your LLM messes the outputs.
149
- result = classify.call(sentence: "This book was super fun to read!")
150
-
151
- puts result.sentiment # => #<Sentiment::Positive>
152
- puts result.confidence # => 0.85
153
- ```
119
+ **Provider adapters:** Add `dspy-openai`, `dspy-anthropic`, and/or `dspy-gemini` next to `dspy` in your Gemfile depending on which `DSPy::LM` providers you call. Each gem already depends on the official SDK (`openai`, `anthropic`, `gemini-ai`), and DSPy auto-loads the adapters when the gem is present—no extra `require` needed.
154
120
 
121
+ Set the matching `DSPY_WITH_*` environment variables (see `Gemfile`) to include or exclude each sibling gem when running Bundler locally (for example `DSPY_WITH_GEPA=1` or `DSPY_WITH_O11Y_LANGFUSE=1`). Refer to `adr/013-dependency-tree.md` for the full dependency map and roadmap.
155
122
  ### Access to 200+ Models Across 5 Providers
156
123
 
157
124
  DSPy.rb provides unified access to major LLM providers with provider-specific optimizations:
@@ -192,7 +159,10 @@ end
192
159
 
193
160
  ## What You Get
194
161
 
195
- **Developer Experience:**
162
+ **Developer Experience:** Official clients, multimodal coverage, and observability baked in.
163
+ <details>
164
+ <summary>Expand for everything included</summary>
165
+
196
166
  - LLM provider support using official Ruby clients:
197
167
  - [OpenAI Ruby](https://github.com/openai/openai-ruby) with vision model support
198
168
  - [Anthropic Ruby SDK](https://github.com/anthropics/anthropic-sdk-ruby) with multimodal capabilities
@@ -202,21 +172,33 @@ end
202
172
  - Runtime type checking with [Sorbet](https://sorbet.org/) including T::Enum and union types
203
173
  - Type-safe tool definitions for ReAct agents
204
174
  - Comprehensive instrumentation and observability
175
+ </details>
176
+
177
+ **Core Building Blocks:** Predictors, agents, and pipelines wired through type-safe signatures.
178
+ <details>
179
+ <summary>Expand for everything included</summary>
205
180
 
206
- **Core Building Blocks:**
207
181
  - **Signatures** - Define input/output schemas using Sorbet types with T::Enum and union type support
208
182
  - **Predict** - LLM completion with structured data extraction and multimodal support
209
183
  - **Chain of Thought** - Step-by-step reasoning for complex problems with automatic prompt optimization
210
184
  - **ReAct** - Tool-using agents with type-safe tool definitions and error recovery
211
185
  - **Module Composition** - Combine multiple LLM calls into production-ready workflows
186
+ </details>
187
+
188
+ **Optimization & Evaluation:** Treat prompt optimization like a real ML workflow.
189
+ <details>
190
+ <summary>Expand for everything included</summary>
212
191
 
213
- **Optimization & Evaluation:**
214
192
  - **Prompt Objects** - Manipulate prompts as first-class objects instead of strings
215
193
  - **Typed Examples** - Type-safe training data with automatic validation
216
194
  - **Evaluation Framework** - Advanced metrics beyond simple accuracy with error-resilient pipelines
217
195
  - **MIPROv2 Optimization** - Advanced Bayesian optimization with Gaussian Processes, multiple optimization strategies, auto-config presets, and storage persistence
196
+ </details>
197
+
198
+ **Production Features:** Hardened behaviors for teams shipping actual products.
199
+ <details>
200
+ <summary>Expand for everything included</summary>
218
201
 
219
- **Production Features:**
220
202
  - **Reliable JSON Extraction** - Native structured outputs for OpenAI and Gemini, Anthropic tool-based extraction, and automatic strategy selection with fallback
221
203
  - **Type-Safe Configuration** - Strategy enums with automatic provider optimization (Strict/Compatible modes)
222
204
  - **Smart Retry Logic** - Progressive fallback with exponential backoff for handling transient failures
@@ -224,10 +206,13 @@ end
224
206
  - **Performance Caching** - Schema and capability caching for faster repeated operations
225
207
  - **File-based Storage** - Optimization result persistence with versioning
226
208
  - **Structured Logging** - JSON and key=value formats with span tracking
209
+ </details>
227
210
 
228
211
  ## Recent Achievements
229
212
 
230
- DSPy.rb has rapidly evolved from experimental to production-ready:
213
+ DSPy.rb has gone from experimental to production-ready in three fast releases.
214
+ <details>
215
+ <summary>Expand for the full changelog highlights</summary>
231
216
 
232
217
  ### Foundation
233
218
  - ✅ **JSON Parsing Reliability** - Native OpenAI structured outputs with adaptive retry logic and schema-aware fallbacks
@@ -243,8 +228,11 @@ DSPy.rb has rapidly evolved from experimental to production-ready:
243
228
  - ✅ **Optimizer Utilities Parity (v0.29.0)** - Bootstrap strategies, dataset summaries, and Layer 3 utilities unlock multi-predictor programs on Ruby
244
229
  - ✅ **Observability Hardening (v0.29.0)** - OTLP exporter runs on a single-thread executor preventing frozen SSL contexts without blocking spans
245
230
  - ✅ **Documentation Refresh (v0.29.x)** - New GEPA guide plus ADE optimization docs covering presets, stratified splits, and error-handling defaults
231
+ </details>
246
232
 
247
- **Current Focus Areas:**
233
+ **Current Focus Areas:** Closing the loop on production patterns and community adoption ahead of v1.0.
234
+ <details>
235
+ <summary>Expand for the roadmap</summary>
248
236
 
249
237
  ### Production Readiness
250
238
  - 🚧 **Production Patterns** - Real-world usage validation and performance optimization
@@ -254,10 +242,9 @@ DSPy.rb has rapidly evolved from experimental to production-ready:
254
242
  - 🚧 **Community Examples** - Real-world applications and case studies
255
243
  - 🚧 **Contributor Experience** - Making it easier to contribute and extend
256
244
  - 🚧 **Performance Benchmarks** - Comparative analysis vs other frameworks
245
+ </details>
257
246
 
258
- **v1.0 Philosophy:**
259
- v1.0 will be released after extensive production battle-testing, not after checking off features.
260
- The API is already stable - v1.0 represents confidence in production reliability backed by real-world validation.
247
+ **v1.0 Philosophy:** v1.0 lands after battle-testing, not checkbox bingo. The API is already stable; the milestone marks production confidence.
261
248
 
262
249
 
263
250
  ## Documentation
data/lib/dspy/evals.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
- require 'polars'
5
4
  require 'concurrent'
6
5
  require 'sorbet-runtime'
7
6
  require_relative 'example'
@@ -111,8 +110,14 @@ module DSPy
111
110
  }
112
111
  end
113
112
 
114
- sig { returns(Polars::DataFrame) }
113
+ if defined?(Polars::DataFrame)
114
+ sig { returns(Polars::DataFrame) }
115
+ else
116
+ sig { returns(T.untyped) }
117
+ end
115
118
  def to_polars
119
+ ensure_polars!
120
+
116
121
  rows = @results.each_with_index.map do |result, index|
117
122
  {
118
123
  "index" => index,
@@ -130,6 +135,20 @@ module DSPy
130
135
 
131
136
  private
132
137
 
138
+ POLARS_MISSING_ERROR = <<~MSG
139
+ Polars is required to export evaluation results. Add `gem 'polars'`
140
+ (or enable the `dspy-datasets` gem / `DSPY_WITH_DATASETS=1`) before
141
+ calling `DSPy::Evals::BatchEvaluationResult#to_polars`.
142
+ MSG
143
+
144
+ def ensure_polars!
145
+ return if defined?(Polars::DataFrame)
146
+
147
+ require 'polars'
148
+ rescue LoadError => e
149
+ raise LoadError, "#{POLARS_MISSING_ERROR}\n\n#{e.message}"
150
+ end
151
+
133
152
  def serialize_for_polars(value)
134
153
  case value
135
154
  when NilClass, TrueClass, FalseClass, Numeric, String
@@ -6,15 +6,27 @@ module DSPy
6
6
  class AdapterFactory
7
7
  # Maps provider prefixes to adapter classes
8
8
  ADAPTER_MAP = {
9
- 'openai' => 'OpenAIAdapter',
10
- 'anthropic' => 'AnthropicAdapter',
11
- 'ollama' => 'OllamaAdapter',
12
- 'gemini' => 'GeminiAdapter',
13
- 'openrouter' => 'OpenrouterAdapter'
9
+ 'openai' => { class_name: 'DSPy::OpenAI::LM::Adapters::OpenAIAdapter', gem_name: 'dspy-openai' },
10
+ 'anthropic' => { class_name: 'DSPy::Anthropic::LM::Adapters::AnthropicAdapter', gem_name: 'dspy-anthropic' },
11
+ 'ollama' => { class_name: 'DSPy::OpenAI::LM::Adapters::OllamaAdapter', gem_name: 'dspy-openai' },
12
+ 'gemini' => { class_name: 'DSPy::Gemini::LM::Adapters::GeminiAdapter', gem_name: 'dspy-gemini' },
13
+ 'openrouter' => { class_name: 'DSPy::OpenAI::LM::Adapters::OpenRouterAdapter', gem_name: 'dspy-openai' }
14
14
  }.freeze
15
15
 
16
16
  PROVIDERS_WITH_EXTRA_OPTIONS = %w[openai anthropic ollama gemini openrouter].freeze
17
17
 
18
+ class AdapterData < Data.define(:class_name, :gem_name)
19
+ def self.from_prefix(provider_prefix)
20
+ if ADAPTER_MAP.key?(provider_prefix)
21
+ new(**ADAPTER_MAP[provider_prefix])
22
+ end
23
+ end
24
+
25
+ def require_path
26
+ gem_name.tr('-', '/')
27
+ end
28
+ end
29
+
18
30
  class << self
19
31
  # Creates an adapter instance based on model_id
20
32
  # @param model_id [String] Full model identifier (e.g., "openai/gpt-4")
@@ -46,21 +58,32 @@ module DSPy
46
58
  end
47
59
 
48
60
  def get_adapter_class(provider)
49
- adapter_class_name = ADAPTER_MAP[provider]
50
-
51
- unless adapter_class_name
61
+ ensure_adapter_supported!(provider)
62
+ ensure_adapter_loaded!(provider)
63
+
64
+ Object.const_get(adapter_data(provider).class_name)
65
+ end
66
+
67
+ def adapter_data(provider)
68
+ AdapterData.from_prefix(provider)
69
+ end
70
+
71
+ def ensure_adapter_supported!(provider)
72
+ if adapter_data(provider).nil?
52
73
  available_providers = ADAPTER_MAP.keys.join(', ')
53
- raise UnsupportedProviderError,
54
- "Unsupported provider: #{provider}. Available: #{available_providers}"
74
+ raise UnsupportedProviderError, "Unsupported provider: #{provider}. Available: #{available_providers}"
55
75
  end
76
+ end
56
77
 
57
- begin
58
- Object.const_get("DSPy::LM::#{adapter_class_name}")
59
- rescue NameError
60
- raise UnsupportedProviderError,
61
- "Adapter not found: DSPy::LM::#{adapter_class_name}. " \
62
- "Make sure the corresponding gem is installed."
63
- end
78
+ def ensure_adapter_loaded!(provider)
79
+ adapter_data = adapter_data(provider)
80
+ require adapter_data.require_path
81
+ msg = <<~ERROR
82
+ Adapter not found: #{adapter_data.class_name}.
83
+ Install the #{adapter_data.gem_name} gem and try again.
84
+ ERROR
85
+ rescue LoadError
86
+ raise MissingAdapterError, msg
64
87
  end
65
88
  end
66
89
  end
@@ -6,6 +6,9 @@ module DSPy
6
6
  class AdapterError < Error; end
7
7
  class UnsupportedProviderError < Error; end
8
8
  class ConfigurationError < Error; end
9
+ class MissingAdapterError < Error; end
10
+ class UnsupportedVersionError < Error; end
11
+ class MissingOfficialSDKError < Error; end
9
12
 
10
13
  # Raised when API key is missing or invalid
11
14
  class MissingAPIKeyError < Error
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "sorbet-runtime"
4
- require_relative "adapters/openai/schema_converter"
5
- require_relative "adapters/gemini/schema_converter"
6
4
 
7
5
  module DSPy
8
6
  class LM
@@ -72,10 +70,19 @@ module DSPy
72
70
  # OpenAI/Ollama preparation
73
71
  sig { params(request_params: T::Hash[Symbol, T.untyped]).void }
74
72
  def prepare_openai_request(request_params)
73
+ begin
74
+ require "dspy/openai"
75
+ rescue LoadError
76
+ msg = <<~MSG
77
+ OpenAI adapter is optional; structured output helpers will be unavailable until the gem is installed.
78
+ Add `gem 'dspy-openai'` to your Gemfile and run `bundle install`.
79
+ MSG
80
+ raise DSPy::LM::MissingAdapterError, msg
81
+ end
82
+
75
83
  # Check if structured outputs are supported
76
- if adapter.instance_variable_get(:@structured_outputs_enabled) &&
77
- DSPy::LM::Adapters::OpenAI::SchemaConverter.supports_structured_outputs?(adapter.model)
78
- response_format = DSPy::LM::Adapters::OpenAI::SchemaConverter.to_openai_format(signature_class)
84
+ if adapter.instance_variable_get(:@structured_outputs_enabled) && DSPy::OpenAI::LM::SchemaConverter.supports_structured_outputs?(adapter.model)
85
+ response_format = DSPy::OpenAI::LM::SchemaConverter.to_openai_format(signature_class)
79
86
  request_params[:response_format] = response_format
80
87
  end
81
88
  end
@@ -107,10 +114,19 @@ module DSPy
107
114
  # Gemini preparation
108
115
  sig { params(request_params: T::Hash[Symbol, T.untyped]).void }
109
116
  def prepare_gemini_request(request_params)
117
+ begin
118
+ require "dspy/gemini"
119
+ rescue LoadError
120
+ msg = <<~MSG
121
+ Gemini adapter is optional; structured output helpers will be unavailable until the gem is installed.
122
+ Add `gem 'dspy-gemini'` to your Gemfile and run `bundle install`.
123
+ MSG
124
+ raise DSPy::LM::MissingAdapterError, msg
125
+ end
126
+
110
127
  # Check if structured outputs are supported
111
- if adapter.instance_variable_get(:@structured_outputs_enabled) &&
112
- DSPy::LM::Adapters::Gemini::SchemaConverter.supports_structured_outputs?(adapter.model)
113
- schema = DSPy::LM::Adapters::Gemini::SchemaConverter.to_gemini_format(signature_class)
128
+ if adapter.instance_variable_get(:@structured_outputs_enabled) && DSPy::Gemini::LM::SchemaConverter.supports_structured_outputs?(adapter.model)
129
+ schema = DSPy::Gemini::LM::SchemaConverter.to_gemini_format(signature_class)
114
130
 
115
131
  request_params[:generation_config] = {
116
132
  response_mime_type: "application/json",
data/lib/dspy/lm.rb CHANGED
@@ -12,13 +12,6 @@ require_relative 'lm/adapter_factory'
12
12
 
13
13
  # Load instrumentation
14
14
 
15
- # Load adapters
16
- require_relative 'lm/adapters/openai_adapter'
17
- require_relative 'lm/adapters/anthropic_adapter'
18
- require_relative 'lm/adapters/ollama_adapter'
19
- require_relative 'lm/adapters/gemini_adapter'
20
- require_relative 'lm/adapters/openrouter_adapter'
21
-
22
15
  # Load strategy system
23
16
  require_relative 'lm/chat_strategy'
24
17
  require_relative 'lm/json_strategy'
@@ -27,16 +20,18 @@ require_relative 'lm/json_strategy'
27
20
  require_relative 'lm/message'
28
21
  require_relative 'lm/message_builder'
29
22
  require_relative 'structured_outputs_prompt'
23
+ require_relative 'schema/sorbet_toon_adapter'
30
24
 
31
25
  module DSPy
32
26
  class LM
33
27
  extend T::Sig
34
- attr_reader :model_id, :api_key, :model, :provider, :adapter, :schema_format
28
+ attr_reader :model_id, :api_key, :model, :provider, :adapter, :schema_format, :data_format
35
29
 
36
- def initialize(model_id, api_key: nil, schema_format: :json, **options)
30
+ def initialize(model_id, api_key: nil, schema_format: :json, data_format: :json, **options)
37
31
  @model_id = model_id
38
32
  @api_key = api_key
39
33
  @schema_format = schema_format
34
+ @data_format = data_format
40
35
 
41
36
  # Parse provider and model from model_id
42
37
  @provider, @model = parse_model_id(model_id)
@@ -209,12 +204,42 @@ module DSPy
209
204
  adapter_class_name = adapter.class.name
210
205
 
211
206
  if adapter_class_name.include?('OpenAIAdapter') || adapter_class_name.include?('OllamaAdapter')
207
+ begin
208
+ require "dspy/openai"
209
+ rescue LoadError
210
+ msg = <<~MSG
211
+ Install the openai gem to enable support for this adapter.
212
+ Add `gem 'dspy-openai'` to your Gemfile and run `bundle install`.
213
+ MSG
214
+ raise DSPy::LM::MissingAdapterError, msg
215
+ end
216
+
212
217
  adapter.instance_variable_get(:@structured_outputs_enabled) &&
213
- DSPy::LM::Adapters::OpenAI::SchemaConverter.supports_structured_outputs?(adapter.model)
218
+ DSPy::OpenAI::LM::SchemaConverter.supports_structured_outputs?(adapter.model)
214
219
  elsif adapter_class_name.include?('GeminiAdapter')
220
+ begin
221
+ require "dspy/gemini"
222
+ rescue LoadError
223
+ msg = <<~MSG
224
+ Install the gem to enable Gemini support.
225
+ Add `gem 'dspy-gemini'` to your Gemfile and run `bundle install`.
226
+ MSG
227
+ raise DSPy::LM::MissingAdapterError, msg
228
+ end
229
+
215
230
  adapter.instance_variable_get(:@structured_outputs_enabled) &&
216
- DSPy::LM::Adapters::Gemini::SchemaConverter.supports_structured_outputs?(adapter.model)
231
+ DSPy::Gemini::LM::SchemaConverter.supports_structured_outputs?(adapter.model)
217
232
  elsif adapter_class_name.include?('AnthropicAdapter')
233
+ begin
234
+ require "dspy/anthropic"
235
+ rescue LoadError
236
+ msg = <<~MSG
237
+ Install the gem to enable Claude support.
238
+ Add `gem 'dspy-anthropic'` to your Gemfile and run `bundle install`.
239
+ MSG
240
+ raise DSPy::LM::MissingAdapterError, msg
241
+ end
242
+
218
243
  structured_outputs_enabled = adapter.instance_variable_get(:@structured_outputs_enabled)
219
244
  structured_outputs_enabled.nil? ? true : structured_outputs_enabled
220
245
  else
@@ -223,28 +248,46 @@ module DSPy
223
248
  end
224
249
 
225
250
  def parse_response(response, input_values, signature_class)
226
- # Try to parse the response as JSON
251
+ if data_format == :toon
252
+ payload = DSPy::Schema::SorbetToonAdapter.parse_output(signature_class, response.content.to_s)
253
+ return normalize_output_payload(payload)
254
+ end
255
+
227
256
  content = response.content
228
257
 
229
258
  begin
230
- json_payload = JSON.parse(content)
231
-
232
- # For Sorbet signatures, just return the parsed JSON
233
- # The Predict will handle validation
234
- json_payload
259
+ JSON.parse(content)
235
260
  rescue JSON::ParserError => e
236
- # Enhanced error message with debugging information
237
261
  error_details = {
238
262
  original_content: response.content,
239
263
  provider: provider,
240
264
  model: model
241
265
  }
242
-
266
+
243
267
  DSPy.logger.debug("JSON parsing failed: #{error_details}")
244
268
  raise "Failed to parse LLM response as JSON: #{e.message}. Original content length: #{response.content&.length || 0} chars"
245
269
  end
246
270
  end
247
271
 
272
+ def normalize_output_payload(payload)
273
+ case payload
274
+ when T::Struct
275
+ payload.class.props.each_with_object({}) do |(name, _), memo|
276
+ memo[name.to_s] = normalize_output_payload(payload.send(name))
277
+ end
278
+ when Hash
279
+ payload.each_with_object({}) do |(key, value), memo|
280
+ memo[key.to_s] = normalize_output_payload(value)
281
+ end
282
+ when Array
283
+ payload.map { |item| normalize_output_payload(item) }
284
+ when Set
285
+ payload.map { |item| normalize_output_payload(item) }
286
+ else
287
+ payload
288
+ end
289
+ end
290
+
248
291
  # Common instrumentation method for LM requests
249
292
  def instrument_lm_request(messages, signature_class_name, &execution_block)
250
293
  # Prepare input for tracing - convert messages to JSON for input tracking
data/lib/dspy/module.rb CHANGED
@@ -5,6 +5,8 @@ require 'dry-configurable'
5
5
  require 'securerandom'
6
6
  require_relative 'context'
7
7
  require_relative 'callbacks'
8
+ require_relative 'type_serializer'
9
+ require 'json'
8
10
 
9
11
  module DSPy
10
12
  class Module
@@ -209,17 +211,15 @@ module DSPy
209
211
  {}
210
212
  end
211
213
 
212
- payload.to_json
214
+ serialized = DSPy::TypeSerializer.serialize(payload)
215
+ JSON.generate(serialized)
213
216
  rescue StandardError
214
217
  payload.to_s
215
218
  end
216
219
 
217
220
  def serialize_module_output(result)
218
- if result.respond_to?(:to_h)
219
- result.to_h.to_json
220
- else
221
- result.to_json
222
- end
221
+ serialized = DSPy::TypeSerializer.serialize(result)
222
+ JSON.generate(serialized)
223
223
  rescue StandardError
224
224
  result.to_s
225
225
  end