dspy 0.30.0 → 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: 5e59fde98eb918cca54822bc6af704100c51b71081d8ac77e17e8df3cddcb016
4
- data.tar.gz: f8ff81ef1873fc1bffac8a209ddb4172586295e6ef1f46d6bb598045148fc3eb
3
+ metadata.gz: 972e09f00d8d5417d5c1af255eb01503fc33ab370264345b4c7de880b4f99fda
4
+ data.tar.gz: 21f6f7952a9caaf8398a24a69516147bf68c88e510b88aa2f45239786bbfd31b
5
5
  SHA512:
6
- metadata.gz: dc880712e317a8e88c188527b5e1ff906da8c03fe1cbc4251e265bd87c141e52a16b4498f2f9316c33543f3428fb3067fd80a0728ce33de81575f111b93d4553
7
- data.tar.gz: 7d1899b7e8a61b5d2c80fb8f237085171e9b64407f99135b22c341076e11867ee0c323d15c67c5428337606c869702ec9e6ada13edf27c38a6a40d8c83ef4f3b
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
 
@@ -47,28 +43,12 @@ and
47
43
  bundle install
48
44
  ```
49
45
 
50
- ### Optional Sibling Gems
51
-
52
- DSPy.rb ships multiple gems from this monorepo so you only install what you need. Add these alongside `dspy`:
53
-
54
- | Gem | Description | Status |
55
- | --- | --- | --- |
56
- | `dspy-schema` | Exposes `DSPy::TypeSystem::SorbetJsonSchema` for downstream reuse. | **Stable** (v1.0.0) |
57
- | `dspy-code_act` | Think-Code-Observe agents that synthesize and execute Ruby safely. | Preview (0.x) |
58
- | `dspy-datasets` | Dataset helpers plus Parquet/Polars tooling for richer evaluation corpora. | Preview (0.x) |
59
- | `dspy-evals` | High-throughput evaluation harness with metrics, callbacks, and regression fixtures. | Preview (0.x) |
60
- | `dspy-miprov2` | Bayesian optimization + Gaussian Process backend for the MIPROv2 teleprompter. | Preview (0.x) |
61
- | `dspy-gepa` | `DSPy::Teleprompt::GEPA`, reflection loops, experiment tracking, telemetry adapters. | Preview (mirrors `dspy` version) |
62
- | `gepa` | GEPA optimizer core (Pareto engine, telemetry, reflective proposer). | Preview (mirrors `dspy` version) |
63
- | `dspy-o11y` | Core observability APIs: `DSPy::Observability`, async span processor, observation types. | **Stable** (v1.0.0) |
64
- | `dspy-o11y-langfuse` | Auto-configures DSPy observability to stream spans to Langfuse via OTLP. | **Stable** (v1.0.0) |
65
-
66
- 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 `docs/core-concepts/dependency-tree.md` for the full dependency map and roadmap.
67
46
  ### Your First Reliable Predictor
68
47
 
69
48
  ```ruby
49
+ require 'dspy'
70
50
 
71
- # 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).
72
52
  DSPy.configure do |c|
73
53
  c.lm = DSPy::LM.new('openai/gpt-4o-mini',
74
54
  api_key: ENV['OPENAI_API_KEY'],
@@ -99,7 +79,7 @@ class Classify < DSPy::Signature
99
79
  end
100
80
  end
101
81
 
102
- # Wire it to the simplest prompting technique - a Predictn.
82
+ # Wire it to the simplest prompting technique: a prediction loop.
103
83
  classify = DSPy::Predict.new(Classify)
104
84
  # it may raise an error if you mess the inputs or your LLM messes the outputs.
105
85
  result = classify.call(sentence: "This book was super fun to read!")
@@ -108,6 +88,37 @@ puts result.sentiment # => #<Sentiment::Positive>
108
88
  puts result.confidence # => 0.85
109
89
  ```
110
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
+
97
+ ### Sibling Gems
98
+
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`:
100
+
101
+ | Gem | Description | Status |
102
+ | --- | --- | --- |
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) |
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) |
108
+ | `dspy-datasets` | Dataset helpers plus Parquet/Polars tooling for richer evaluation corpora. (Toggle via `DSPY_WITH_DATASETS`.) | **Stable** (v1.0.0) |
109
+ | `dspy-evals` | High-throughput evaluation harness with metrics, callbacks, and regression fixtures. (Toggle via `DSPY_WITH_EVALS`.) | **Stable** (v1.0.0) |
110
+ | `dspy-miprov2` | Bayesian optimization + Gaussian Process backend for the MIPROv2 teleprompter. (Install or export `DSPY_WITH_MIPROV2=1` before requiring the teleprompter.) | **Stable** (v1.0.0) |
111
+ | `dspy-gepa` | `DSPy::Teleprompt::GEPA`, reflection loops, experiment tracking, telemetry adapters. (Install or set `DSPY_WITH_GEPA=1`.) | **Stable** (v1.0.0) |
112
+ | `gepa` | GEPA optimizer core (Pareto engine, telemetry, reflective proposer). | **Stable** (v1.0.0) |
113
+ | `dspy-o11y` | Core observability APIs: `DSPy::Observability`, async span processor, observation types. (Install or set `DSPY_WITH_O11Y=1`.) | **Stable** (v1.0.0) |
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) |
118
+
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.
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.
111
122
  ### Access to 200+ Models Across 5 Providers
112
123
 
113
124
  DSPy.rb provides unified access to major LLM providers with provider-specific optimizations:
@@ -148,7 +159,10 @@ end
148
159
 
149
160
  ## What You Get
150
161
 
151
- **Developer Experience:**
162
+ **Developer Experience:** Official clients, multimodal coverage, and observability baked in.
163
+ <details>
164
+ <summary>Expand for everything included</summary>
165
+
152
166
  - LLM provider support using official Ruby clients:
153
167
  - [OpenAI Ruby](https://github.com/openai/openai-ruby) with vision model support
154
168
  - [Anthropic Ruby SDK](https://github.com/anthropics/anthropic-sdk-ruby) with multimodal capabilities
@@ -158,21 +172,33 @@ end
158
172
  - Runtime type checking with [Sorbet](https://sorbet.org/) including T::Enum and union types
159
173
  - Type-safe tool definitions for ReAct agents
160
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>
161
180
 
162
- **Core Building Blocks:**
163
181
  - **Signatures** - Define input/output schemas using Sorbet types with T::Enum and union type support
164
182
  - **Predict** - LLM completion with structured data extraction and multimodal support
165
183
  - **Chain of Thought** - Step-by-step reasoning for complex problems with automatic prompt optimization
166
184
  - **ReAct** - Tool-using agents with type-safe tool definitions and error recovery
167
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>
168
191
 
169
- **Optimization & Evaluation:**
170
192
  - **Prompt Objects** - Manipulate prompts as first-class objects instead of strings
171
193
  - **Typed Examples** - Type-safe training data with automatic validation
172
194
  - **Evaluation Framework** - Advanced metrics beyond simple accuracy with error-resilient pipelines
173
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>
174
201
 
175
- **Production Features:**
176
202
  - **Reliable JSON Extraction** - Native structured outputs for OpenAI and Gemini, Anthropic tool-based extraction, and automatic strategy selection with fallback
177
203
  - **Type-Safe Configuration** - Strategy enums with automatic provider optimization (Strict/Compatible modes)
178
204
  - **Smart Retry Logic** - Progressive fallback with exponential backoff for handling transient failures
@@ -180,10 +206,13 @@ end
180
206
  - **Performance Caching** - Schema and capability caching for faster repeated operations
181
207
  - **File-based Storage** - Optimization result persistence with versioning
182
208
  - **Structured Logging** - JSON and key=value formats with span tracking
209
+ </details>
183
210
 
184
211
  ## Recent Achievements
185
212
 
186
- 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>
187
216
 
188
217
  ### Foundation
189
218
  - ✅ **JSON Parsing Reliability** - Native OpenAI structured outputs with adaptive retry logic and schema-aware fallbacks
@@ -199,8 +228,11 @@ DSPy.rb has rapidly evolved from experimental to production-ready:
199
228
  - ✅ **Optimizer Utilities Parity (v0.29.0)** - Bootstrap strategies, dataset summaries, and Layer 3 utilities unlock multi-predictor programs on Ruby
200
229
  - ✅ **Observability Hardening (v0.29.0)** - OTLP exporter runs on a single-thread executor preventing frozen SSL contexts without blocking spans
201
230
  - ✅ **Documentation Refresh (v0.29.x)** - New GEPA guide plus ADE optimization docs covering presets, stratified splits, and error-handling defaults
231
+ </details>
202
232
 
203
- **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>
204
236
 
205
237
  ### Production Readiness
206
238
  - 🚧 **Production Patterns** - Real-world usage validation and performance optimization
@@ -210,10 +242,9 @@ DSPy.rb has rapidly evolved from experimental to production-ready:
210
242
  - 🚧 **Community Examples** - Real-world applications and case studies
211
243
  - 🚧 **Contributor Experience** - Making it easier to contribute and extend
212
244
  - 🚧 **Performance Benchmarks** - Comparative analysis vs other frameworks
245
+ </details>
213
246
 
214
- **v1.0 Philosophy:**
215
- v1.0 will be released after extensive production battle-testing, not after checking off features.
216
- 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.
217
248
 
218
249
 
219
250
  ## Documentation
@@ -259,15 +259,34 @@ module DSPy
259
259
  # Executes method with around callbacks
260
260
  def execute_with_around_callbacks(method_name, original_method, *args, **kwargs, &block)
261
261
  callbacks = self.class.send(:callbacks_for, method_name)[:around]
262
+ args_copy = args.dup
263
+ kwargs_copy = kwargs.dup
262
264
 
263
265
  # Build callback chain from innermost (original method) to outermost
264
266
  chain = callbacks.reverse.inject(
265
267
  -> { original_method.bind(self).call(*args, **kwargs, &block) }
266
268
  ) do |inner, callback|
267
269
  if callback.is_a?(Proc)
268
- -> { instance_exec(-> { inner.call }, &callback) }
270
+ -> do
271
+ next_proc = -> { inner.call }
272
+ proc_arity = callback.arity
273
+ expects_extra = proc_arity.abs > 1
274
+
275
+ if expects_extra
276
+ instance_exec(next_proc, args_copy, kwargs_copy, &callback)
277
+ else
278
+ instance_exec(next_proc, &callback)
279
+ end
280
+ end
269
281
  else
270
- -> { send(callback) { inner.call } }
282
+ -> do
283
+ method_obj = method(callback)
284
+ if method_obj.arity.zero?
285
+ send(callback) { inner.call }
286
+ else
287
+ send(callback, args_copy, kwargs_copy) { inner.call }
288
+ end
289
+ end
271
290
  end
272
291
  end
273
292
 
data/lib/dspy/context.rb CHANGED
@@ -33,7 +33,8 @@ module DSPy
33
33
  context = {
34
34
  trace_id: SecureRandom.uuid,
35
35
  span_stack: [],
36
- otel_span_stack: []
36
+ otel_span_stack: [],
37
+ module_stack: []
37
38
  }
38
39
 
39
40
  # Set in both Thread and Fiber storage
@@ -158,6 +159,48 @@ module DSPy
158
159
  end
159
160
  end
160
161
  end
162
+
163
+ def with_module(module_instance, label: nil)
164
+ stack = module_stack
165
+ entry = build_module_entry(module_instance, label)
166
+ stack.push(entry)
167
+ yield
168
+ ensure
169
+ if stack.last.equal?(entry)
170
+ stack.pop
171
+ else
172
+ stack.delete(entry)
173
+ end
174
+ end
175
+
176
+ def module_stack
177
+ current[:module_stack] ||= []
178
+ end
179
+
180
+ def module_context_attributes
181
+ stack = module_stack
182
+ return {} if stack.empty?
183
+
184
+ path = stack.map do |entry|
185
+ {
186
+ id: entry[:id],
187
+ class: entry[:class],
188
+ label: entry[:label]
189
+ }
190
+ end
191
+
192
+ ancestry_token = path.map { |node| node[:id] }.join('>')
193
+
194
+ {
195
+ module_path: path,
196
+ module_root: path.first,
197
+ module_leaf: path.last,
198
+ module_scope: {
199
+ ancestry_token: ancestry_token,
200
+ depth: path.length
201
+ }
202
+ }
203
+ end
161
204
 
162
205
  def clear!
163
206
  # Clear both the thread-specific key and the legacy key
@@ -218,6 +261,14 @@ module DSPy
218
261
  end
219
262
  end
220
263
  end
264
+
265
+ def build_module_entry(module_instance, explicit_label)
266
+ {
267
+ id: (module_instance.respond_to?(:module_scope_id) ? module_instance.module_scope_id : SecureRandom.uuid),
268
+ class: module_instance.class.name,
269
+ label: explicit_label || (module_instance.respond_to?(:module_scope_label) ? module_instance.module_scope_label : nil)
270
+ }
271
+ end
221
272
  end
222
273
  end
223
274
  end
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