dspy 0.19.1 → 0.20.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: d76e6cfecee3a85c7d8cd389b39eb0401e2051644e0f3ada98ab03645414f35e
4
- data.tar.gz: fe078c869c9ee45237916dd10bd12a5d9bb80ea629c1bb6b0a29ae6ff5249b73
3
+ metadata.gz: 44cf35be07e90187237ccdc79533d0ca76dbe2cb1040f0e841bff69afd7e71fc
4
+ data.tar.gz: 443a4d5dafe1fc7c90335e2b6294a4b1e1dd77b77cb24c19c7f8237f1824b2d0
5
5
  SHA512:
6
- metadata.gz: 56565be338d06d517daa931d7ebfb32f259b92d9b8a55140783bcf34f635ace57213909d74003ad33b6e8761d5c1fae950f1f6332ee55131d8a7ffd5c4a58183
7
- data.tar.gz: 3e36f838fbba8e06428397611f1f2e2476f789c7f4714a98ce2198f38bf5632a0f4e12cf5be56282c48d31071c9b784959f0095f27bc8f42c44472ad43111d41
6
+ metadata.gz: 6e2e2e6098773c599190e0ab5e43ecd883e04359268557f28b0350219b98995c8fbe6633db2a5d1a34be1d250ae10ce05cd4aaaebefd6c13f5e855fcf587b5ef
7
+ data.tar.gz: b14760bb9cf7075991fdfc274bab60e1d55a4547a8b0da2b78c37bb76c69c4963a899f8ab26f26612210957f6184999b769adc590f90bd60714f992ab1c20230
data/README.md CHANGED
@@ -2,12 +2,17 @@
2
2
 
3
3
  [![Gem Version](https://img.shields.io/gem/v/dspy)](https://rubygems.org/gems/dspy)
4
4
  [![Total Downloads](https://img.shields.io/gem/dt/dspy)](https://rubygems.org/gems/dspy)
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
+ [![Documentation](https://img.shields.io/badge/docs-vicentereig.github.io%2Fdspy.rb-blue)](https://vicentereig.github.io/dspy.rb/)
5
7
 
6
8
  **Build reliable LLM applications in Ruby using composable, type-safe modules.**
7
9
 
8
- DSPy.rb brings structured LLM programming to Ruby developers. Instead of wrestling with prompt strings and parsing responses, you define typed signatures and compose them into pipelines that just work.
10
+ DSPy.rb brings structured LLM programming to Ruby developers. Instead of wrestling with prompt strings and parsing
11
+ responses, you define typed signatures and compose them into pipelines that just work.
9
12
 
10
- 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/): instead of crafting fragile prompts, you define modular signatures and let the framework handle the messy details.
13
+ Traditional prompting is like writing code with string concatenation: it works until it doesn't. DSPy.rb brings you
14
+ the programming approach pioneered by [dspy.ai](https://dspy.ai/): instead of crafting fragile prompts, you define modular
15
+ signatures and let the framework handle the messy details.
11
16
 
12
17
  The result? LLM applications that actually scale and don't break when you sneeze.
13
18
 
@@ -54,18 +59,18 @@ puts result.confidence # => 0.85
54
59
  ## What You Get
55
60
 
56
61
  **Core Building Blocks:**
57
- - **Signatures** - Define input/output schemas using Sorbet types
58
- - **Predict** - Basic LLM completion with structured data
59
- - **Chain of Thought** - Step-by-step reasoning for complex problems
60
- - **ReAct** - Tool-using agents with basic tool integration
62
+ - **Signatures** - Define input/output schemas using Sorbet types with T::Enum and union type support
63
+ - **Predict** - LLM completion with structured data extraction and multimodal support
64
+ - **Chain of Thought** - Step-by-step reasoning for complex problems with automatic prompt optimization
65
+ - **ReAct** - Tool-using agents with type-safe tool definitions and error recovery
61
66
  - **CodeAct** - Dynamic code execution agents for programming tasks
62
- - **Manual Composition** - Combine multiple LLM calls into workflows
67
+ - **Module Composition** - Combine multiple LLM calls into production-ready workflows
63
68
 
64
69
  **Optimization & Evaluation:**
65
70
  - **Prompt Objects** - Manipulate prompts as first-class objects instead of strings
66
71
  - **Typed Examples** - Type-safe training data with automatic validation
67
- - **Evaluation Framework** - Basic testing with simple metrics
68
- - **Basic Optimization** - Simple prompt optimization techniques
72
+ - **Evaluation Framework** - Advanced metrics beyond simple accuracy with error-resilient pipelines
73
+ - **MIPROv2 Optimization** - Automatic prompt optimization with storage and persistence
69
74
 
70
75
  **Production Features:**
71
76
  - **Reliable JSON Extraction** - Native OpenAI structured outputs, Anthropic extraction patterns, and automatic strategy selection with fallback
@@ -78,28 +83,64 @@ puts result.confidence # => 0.85
78
83
 
79
84
  **Developer Experience:**
80
85
  - LLM provider support using official Ruby clients:
81
- - [OpenAI Ruby](https://github.com/openai/openai-ruby)
82
- - [Anthropic Ruby SDK](https://github.com/anthropics/anthropic-sdk-ruby)
83
- - [Ollama](https://ollama.com/) via OpenAI compatibility layer
84
- - Runtime type checking with [Sorbet](https://sorbet.org/)
86
+ - [OpenAI Ruby](https://github.com/openai/openai-ruby) with vision model support
87
+ - [Anthropic Ruby SDK](https://github.com/anthropics/anthropic-sdk-ruby) with multimodal capabilities
88
+ - [Ollama](https://ollama.com/) via OpenAI compatibility layer for local models
89
+ - **Multimodal Support** - Complete image analysis with DSPy::Image, type-safe bounding boxes, vision-capable models
90
+ - Runtime type checking with [Sorbet](https://sorbet.org/) including T::Enum and union types
85
91
  - Type-safe tool definitions for ReAct agents
86
92
  - Comprehensive instrumentation and observability
87
93
 
88
94
  ## Development Status
89
95
 
90
- DSPy.rb is actively developed and approaching stability at **v0.15.4**. The core framework is production-ready with comprehensive documentation, but I'm battle-testing features through the 0.x series before committing to a stable v1.0 API.
96
+ DSPy.rb is actively developed and approaching stability. The core framework is production-ready with
97
+ comprehensive documentation, but I'm battle-testing features through the 0.x series before committing
98
+ to a stable v1.0 API.
91
99
 
92
100
  Real-world usage feedback is invaluable - if you encounter issues or have suggestions, please open a GitHub issue!
93
101
 
102
+ ## Documentation
103
+
104
+ 📖 **[Complete Documentation Website](https://vicentereig.github.io/dspy.rb/)**
105
+
106
+ ### LLM-Friendly Documentation
107
+
108
+ For LLMs and AI assistants working with DSPy.rb:
109
+ - **[llms.txt](https://vicentereig.github.io/dspy.rb/llms.txt)** - Concise reference optimized for LLMs
110
+ - **[llms-full.txt](https://vicentereig.github.io/dspy.rb/llms-full.txt)** - Comprehensive API documentation
111
+
112
+ ### Getting Started
113
+ - **[Installation & Setup](docs/src/getting-started/installation.md)** - Detailed installation and configuration
114
+ - **[Quick Start Guide](docs/src/getting-started/quick-start.md)** - Your first DSPy programs
115
+ - **[Core Concepts](docs/src/getting-started/core-concepts.md)** - Understanding signatures, predictors, and modules
116
+
117
+ ### Core Features
118
+ - **[Signatures & Types](docs/src/core-concepts/signatures.md)** - Define typed interfaces for LLM operations
119
+ - **[Predictors](docs/src/core-concepts/predictors.md)** - Predict, ChainOfThought, ReAct, and more
120
+ - **[Modules & Pipelines](docs/src/core-concepts/modules.md)** - Compose complex multi-stage workflows
121
+ - **[Multimodal Support](docs/src/core-concepts/multimodal.md)** - Image analysis with vision-capable models
122
+ - **[Examples & Validation](docs/src/core-concepts/examples.md)** - Type-safe training data
123
+
124
+ ### Optimization
125
+ - **[Evaluation Framework](docs/src/optimization/evaluation.md)** - Advanced metrics beyond simple accuracy
126
+ - **[Prompt Optimization](docs/src/optimization/prompt-optimization.md)** - Manipulate prompts as objects
127
+ - **[MIPROv2 Optimizer](docs/src/optimization/miprov2.md)** - Automatic optimization algorithms
128
+
129
+ ### Production Features
130
+ - **[Storage System](docs/src/production/storage.md)** - Persistence and optimization result storage
131
+ - **[Observability](docs/src/production/observability.md)** - Zero-config Langfuse integration and structured logging
132
+
133
+ ### Advanced Usage
134
+ - **[Complex Types](docs/src/advanced/complex-types.md)** - Sorbet type integration with automatic coercion for structs, enums, and arrays
135
+ - **[Manual Pipelines](docs/src/advanced/pipelines.md)** - Manual module composition patterns
136
+ - **[RAG Patterns](docs/src/advanced/rag.md)** - Manual RAG implementation with external services
137
+ - **[Custom Metrics](docs/src/advanced/custom-metrics.md)** - Proc-based evaluation logic
138
+
94
139
  ## Quick Start
95
140
 
96
141
  ### Installation
97
142
 
98
- ```ruby
99
- gem 'dspy', '~> 0.15'
100
- ```
101
-
102
- Or add to your Gemfile:
143
+ Add to your Gemfile:
103
144
 
104
145
  ```ruby
105
146
  gem 'dspy'
@@ -135,68 +176,51 @@ sudo apt-get install cmake
135
176
 
136
177
  **Note**: The `polars-df` gem compilation can take 15-20 minutes. Pre-built binaries are available for most platforms, so compilation is only needed if a pre-built binary isn't available for your system.
137
178
 
138
- ## Documentation
139
-
140
- 📖 **[Complete Documentation Website](https://vicentereig.github.io/dspy.rb/)**
141
-
142
- ### LLM-Friendly Documentation
143
-
144
- For LLMs and AI assistants working with DSPy.rb:
145
- - **[llms.txt](https://vicentereig.github.io/dspy.rb/llms.txt)** - Concise reference optimized for LLMs
146
- - **[llms-full.txt](https://vicentereig.github.io/dspy.rb/llms-full.txt)** - Comprehensive API documentation
147
-
148
- ### Getting Started
149
- - **[Installation & Setup](docs/src/getting-started/installation.md)** - Detailed installation and configuration
150
- - **[Quick Start Guide](docs/src/getting-started/quick-start.md)** - Your first DSPy programs
151
- - **[Core Concepts](docs/src/getting-started/core-concepts.md)** - Understanding signatures, predictors, and modules
152
-
153
- ### Core Features
154
- - **[Signatures & Types](docs/src/core-concepts/signatures.md)** - Define typed interfaces for LLM operations
155
- - **[Predictors](docs/src/core-concepts/predictors.md)** - Predict, ChainOfThought, ReAct, and more
156
- - **[Modules & Pipelines](docs/src/core-concepts/modules.md)** - Compose complex multi-stage workflows
157
- - **[Examples & Validation](docs/src/core-concepts/examples.md)** - Type-safe training data
158
-
159
- ### Optimization
160
- - **[Evaluation Framework](docs/src/optimization/evaluation.md)** - Basic testing with simple metrics
161
- - **[Prompt Optimization](docs/src/optimization/prompt-optimization.md)** - Manipulate prompts as objects
162
- - **[MIPROv2 Optimizer](docs/src/optimization/miprov2.md)** - Basic automatic optimization
163
-
164
- ### Production Features
165
- - **[Storage System](docs/src/production/storage.md)** - Basic file-based persistence
166
- - **[Observability](docs/src/production/observability.md)** - Zero-config Langfuse integration and structured logging
167
-
168
- ### Advanced Usage
169
- - **[Complex Types](docs/src/advanced/complex-types.md)** - Sorbet type integration with automatic coercion for structs, enums, and arrays
170
- - **[Manual Pipelines](docs/src/advanced/pipelines.md)** - Manual module composition patterns
171
- - **[RAG Patterns](docs/src/advanced/rag.md)** - Manual RAG implementation with external services
172
- - **[Custom Metrics](docs/src/advanced/custom-metrics.md)** - Proc-based evaluation logic
173
-
174
179
  ## Recent Achievements
175
180
 
176
181
  DSPy.rb has rapidly evolved from experimental to production-ready:
177
182
 
178
- - ✅ **JSON Parsing Reliability** (v0.8.0) - Native OpenAI structured outputs, strategy selection, retry logic
179
- - ✅ **Type-Safe Strategy Configuration** (v0.9.0) - Provider-optimized automatic strategy selection
180
- - ✅ **Documentation Website** (v0.6.4) - Comprehensive docs at [vicentereig.github.io/dspy.rb](https://vicentereig.github.io/dspy.rb)
183
+ ### Foundation
184
+ - ✅ **JSON Parsing Reliability** - Native OpenAI structured outputs, strategy selection, retry logic
185
+ - ✅ **Type-Safe Strategy Configuration** - Provider-optimized automatic strategy selection
186
+ - ✅ **Core Module System** - Predict, ChainOfThought, ReAct, CodeAct with type safety
181
187
  - ✅ **Production Observability** - OpenTelemetry, New Relic, and Langfuse integration
182
188
  - ✅ **Optimization Framework** - MIPROv2 algorithm with storage & persistence
183
- - ✅ **Core Module System** - Predict, ChainOfThought, ReAct, CodeAct with type safety
184
189
 
185
- ## Roadmap - Battle-Testing Toward v1.0
190
+ ### Recent Advances
191
+ - ✅ **Comprehensive Multimodal Framework** - Complete image analysis with `DSPy::Image`, type-safe bounding boxes, vision model integration
192
+ - ✅ **Advanced Type System** - `T::Enum` integration, union types for agentic workflows, complex type coercion
193
+ - ✅ **Production-Ready Evaluation** - Multi-factor metrics beyond accuracy, error-resilient evaluation pipelines
194
+ - ✅ **Documentation Ecosystem** - `llms.txt` for AI assistants, ADRs, blog articles, comprehensive examples
195
+ - ✅ **API Maturation** - Simplified idiomatic patterns, better error handling, production-proven designs
186
196
 
187
- DSPy.rb is currently at **v0.15.2** and approaching stability. I'm focusing on real-world usage and refinement through the 0.16+ series before committing to a stable v1.0 API.
197
+ ## Roadmap - Production Battle-Testing Toward v1.0
198
+
199
+ DSPy.rb has transitioned from **feature building** to **production validation**. The core framework is
200
+ feature-complete and stable - now I'm focusing on real-world usage patterns, performance optimization,
201
+ and ecosystem integration.
188
202
 
189
203
  **Current Focus Areas:**
190
- - ✅ **Ollama Support** - Local model integration (completed in v0.15.0)
191
- - **Agentic Memory** - Persistent agent state management with Memory module
192
- - 🚧 **Google Gemini Support** - Integration with Gemini models (#52)
193
- - 🚧 **Context Engineering** - Advanced prompt optimization techniques
194
- - 🚧 **MCP Support** - Model Context Protocol integration
195
- - 🚧 **Additional Optimizer Support** - Expanding teleprompt capabilities
196
- - 🚧 **Performance Optimization** - Based on production usage patterns
204
+
205
+ ### Production Readiness
206
+ - 🚧 **Production Patterns** - Real-world usage validation and performance optimization
207
+ - 🚧 **Ruby Ecosystem Integration** - Rails integration, Sidekiq compatibility, deployment patterns
208
+ - 🚧 **Scale Testing** - High-volume usage, memory management, connection pooling
209
+ - 🚧 **Error Recovery** - Robust failure handling patterns for production environments
210
+
211
+ ### Ecosystem Expansion
212
+ - 🚧 **Model Context Protocol (MCP)** - Integration with MCP ecosystem
213
+ - 🚧 **Additional Provider Support** - Google Gemini, Azure OpenAI, local models beyond Ollama
214
+ - 🚧 **Tool Ecosystem** - Expanded tool integrations for ReAct agents
215
+
216
+ ### Community & Adoption
217
+ - 🚧 **Community Examples** - Real-world applications and case studies
218
+ - 🚧 **Contributor Experience** - Making it easier to contribute and extend
219
+ - 🚧 **Performance Benchmarks** - Comparative analysis vs other frameworks
197
220
 
198
221
  **v1.0 Philosophy:**
199
- v1.0 will be released after extensive production battle-testing, not after checking off features. This ensures a stable, reliable API backed by real-world validation.
222
+ v1.0 will be released after extensive production battle-testing, not after checking off features.
223
+ The API is already stable - v1.0 represents confidence in production reliability backed by real-world validation.
200
224
 
201
225
  ## License
202
226
 
@@ -25,6 +25,7 @@ module DSPy
25
25
  @signature_class = enhanced_signature
26
26
  end
27
27
 
28
+
28
29
  # Override prompt-based methods to maintain ChainOfThought behavior
29
30
  sig { override.params(new_prompt: Prompt).returns(ChainOfThought) }
30
31
  def with_prompt(new_prompt)
data/lib/dspy/code_act.rb CHANGED
@@ -129,8 +129,16 @@ module DSPy
129
129
  # Use the enhanced output struct with CodeAct fields
130
130
  @output_struct_class = enhanced_output_struct
131
131
 
132
+ # Store original signature name
133
+ @original_signature_name = signature_class.name
134
+
132
135
  class << self
133
- attr_reader :input_struct_class, :output_struct_class
136
+ attr_reader :input_struct_class, :output_struct_class, :original_signature_name
137
+
138
+ # Override name to return the original signature name
139
+ def name
140
+ @original_signature_name || super
141
+ end
134
142
  end
135
143
  end
136
144
 
@@ -140,8 +148,6 @@ module DSPy
140
148
 
141
149
  sig { params(kwargs: T.untyped).returns(T.untyped).override }
142
150
  def forward(**kwargs)
143
- lm = config.lm || DSPy.config.lm
144
-
145
151
  # Validate input and serialize all fields as task context
146
152
  input_struct = @original_signature_class.input_struct_class.new(**kwargs)
147
153
  task = DSPy::TypeSerializer.serialize(input_struct).to_json
data/lib/dspy/image.rb CHANGED
@@ -19,6 +19,10 @@ module DSPy
19
19
  'anthropic' => {
20
20
  sources: %w[base64 data],
21
21
  parameters: []
22
+ },
23
+ 'gemini' => {
24
+ sources: %w[base64 data], # Gemini supports inline base64 data, not URLs
25
+ parameters: []
22
26
  }
23
27
  }.freeze
24
28
 
@@ -99,6 +103,27 @@ module DSPy
99
103
  end
100
104
  end
101
105
 
106
+ def to_gemini_format
107
+ if url
108
+ # Gemini requires base64 for inline data, URLs not supported for inline_data
109
+ raise NotImplementedError, "URL fetching for Gemini not yet implemented. Use base64 or data instead."
110
+ elsif base64
111
+ {
112
+ inline_data: {
113
+ mime_type: content_type,
114
+ data: base64
115
+ }
116
+ }
117
+ elsif data
118
+ {
119
+ inline_data: {
120
+ mime_type: content_type,
121
+ data: to_base64
122
+ }
123
+ }
124
+ end
125
+ end
126
+
102
127
  def to_base64
103
128
  return base64 if base64
104
129
  return Base64.strict_encode64(data.pack('C*')) if data
@@ -139,6 +164,11 @@ module DSPy
139
164
  raise DSPy::LM::IncompatibleImageFeatureError,
140
165
  "Anthropic doesn't support image URLs. Please provide base64 or raw data instead."
141
166
  end
167
+ when 'gemini'
168
+ if current_source == 'url'
169
+ raise DSPy::LM::IncompatibleImageFeatureError,
170
+ "Gemini doesn't support image URLs for inline data. Please provide base64 or raw data instead."
171
+ end
142
172
  end
143
173
  end
144
174
 
@@ -148,6 +178,9 @@ module DSPy
148
178
  when 'anthropic'
149
179
  raise DSPy::LM::IncompatibleImageFeatureError,
150
180
  "Anthropic doesn't support the 'detail' parameter. This feature is OpenAI-specific."
181
+ when 'gemini'
182
+ raise DSPy::LM::IncompatibleImageFeatureError,
183
+ "Gemini doesn't support the 'detail' parameter. This feature is OpenAI-specific."
151
184
  end
152
185
  end
153
186
  end
@@ -8,7 +8,8 @@ module DSPy
8
8
  ADAPTER_MAP = {
9
9
  'openai' => 'OpenAIAdapter',
10
10
  'anthropic' => 'AnthropicAdapter',
11
- 'ollama' => 'OllamaAdapter'
11
+ 'ollama' => 'OllamaAdapter',
12
+ 'gemini' => 'GeminiAdapter'
12
13
  }.freeze
13
14
 
14
15
  class << self
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gemini-ai'
4
+ require 'json'
5
+ require_relative '../vision_models'
6
+
7
+ module DSPy
8
+ class LM
9
+ class GeminiAdapter < Adapter
10
+ def initialize(model:, api_key:)
11
+ super
12
+ validate_api_key!(api_key, 'gemini')
13
+
14
+ @client = Gemini.new(
15
+ credentials: {
16
+ service: 'generative-language-api',
17
+ api_key: api_key
18
+ },
19
+ options: {
20
+ model: model,
21
+ server_sent_events: true
22
+ }
23
+ )
24
+ end
25
+
26
+ def chat(messages:, signature: nil, **extra_params, &block)
27
+ normalized_messages = normalize_messages(messages)
28
+
29
+ # Validate vision support if images are present
30
+ if contains_images?(normalized_messages)
31
+ VisionModels.validate_vision_support!('gemini', model)
32
+ # Convert messages to Gemini format with proper image handling
33
+ normalized_messages = format_multimodal_messages(normalized_messages)
34
+ end
35
+
36
+ # Convert DSPy message format to Gemini format
37
+ gemini_messages = convert_messages_to_gemini_format(normalized_messages)
38
+
39
+ request_params = {
40
+ contents: gemini_messages
41
+ }.merge(extra_params)
42
+
43
+ begin
44
+ # Always use streaming
45
+ content = ""
46
+ final_response_data = nil
47
+
48
+ @client.stream_generate_content(request_params) do |chunk|
49
+ # Handle case where chunk might be a string (from SSE VCR)
50
+ if chunk.is_a?(String)
51
+ begin
52
+ chunk = JSON.parse(chunk)
53
+ rescue JSON::ParserError => e
54
+ raise AdapterError, "Failed to parse Gemini streaming response: #{e.message}"
55
+ end
56
+ end
57
+
58
+ # Extract content from chunks
59
+ if chunk.dig('candidates', 0, 'content', 'parts')
60
+ chunk_text = extract_text_from_parts(chunk.dig('candidates', 0, 'content', 'parts'))
61
+ content += chunk_text
62
+
63
+ # Call block only if provided (for real streaming)
64
+ block.call(chunk) if block_given?
65
+ end
66
+
67
+ # Store final response data (usage, metadata) from last chunk
68
+ if chunk['usageMetadata'] || chunk.dig('candidates', 0, 'finishReason')
69
+ final_response_data = chunk
70
+ end
71
+ end
72
+
73
+ # Extract usage information from final chunk
74
+ usage_data = final_response_data&.dig('usageMetadata')
75
+ usage_struct = usage_data ? UsageFactory.create('gemini', usage_data) : nil
76
+
77
+ # Create metadata from final chunk
78
+ metadata = {
79
+ provider: 'gemini',
80
+ model: model,
81
+ finish_reason: final_response_data&.dig('candidates', 0, 'finishReason'),
82
+ safety_ratings: final_response_data&.dig('candidates', 0, 'safetyRatings'),
83
+ streaming: block_given?
84
+ }
85
+
86
+ # Create typed metadata
87
+ typed_metadata = ResponseMetadataFactory.create('gemini', metadata)
88
+
89
+ Response.new(
90
+ content: content,
91
+ usage: usage_struct,
92
+ metadata: typed_metadata
93
+ )
94
+ rescue => e
95
+ handle_gemini_error(e)
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ # Convert DSPy message format to Gemini format
102
+ def convert_messages_to_gemini_format(messages)
103
+ # Gemini expects contents array with role and parts
104
+ messages.map do |msg|
105
+ role = case msg[:role]
106
+ when 'system'
107
+ 'user' # Gemini doesn't have explicit system role, merge with user
108
+ when 'assistant'
109
+ 'model'
110
+ else
111
+ msg[:role]
112
+ end
113
+
114
+ if msg[:content].is_a?(Array)
115
+ # Multimodal content
116
+ parts = msg[:content].map do |item|
117
+ case item[:type]
118
+ when 'text'
119
+ { text: item[:text] }
120
+ when 'image'
121
+ item[:image].to_gemini_format
122
+ else
123
+ item
124
+ end
125
+ end
126
+
127
+ { role: role, parts: parts }
128
+ else
129
+ # Text-only content
130
+ { role: role, parts: [{ text: msg[:content] }] }
131
+ end
132
+ end
133
+ end
134
+
135
+ # Extract text content from Gemini parts array
136
+ def extract_text_from_parts(parts)
137
+ return "" unless parts.is_a?(Array)
138
+
139
+ parts.map { |part| part['text'] }.compact.join
140
+ end
141
+
142
+ # Format multimodal messages for Gemini
143
+ def format_multimodal_messages(messages)
144
+ messages.map do |msg|
145
+ if msg[:content].is_a?(Array)
146
+ # Convert multimodal content to Gemini format
147
+ formatted_content = msg[:content].map do |item|
148
+ case item[:type]
149
+ when 'text'
150
+ { type: 'text', text: item[:text] }
151
+ when 'image'
152
+ # Validate image compatibility before formatting
153
+ item[:image].validate_for_provider!('gemini')
154
+ item[:image].to_gemini_format
155
+ else
156
+ item
157
+ end
158
+ end
159
+
160
+ {
161
+ role: msg[:role],
162
+ content: formatted_content
163
+ }
164
+ else
165
+ msg
166
+ end
167
+ end
168
+ end
169
+
170
+ # Handle Gemini-specific errors
171
+ def handle_gemini_error(error)
172
+ error_msg = error.message.to_s
173
+
174
+ if error_msg.include?('API_KEY') || error_msg.include?('status 400') || error_msg.include?('status 401') || error_msg.include?('status 403')
175
+ raise AdapterError, "Gemini authentication failed: #{error_msg}. Check your API key."
176
+ elsif error_msg.include?('RATE_LIMIT') || error_msg.downcase.include?('quota') || error_msg.include?('status 429')
177
+ raise AdapterError, "Gemini rate limit exceeded: #{error_msg}. Please wait and try again."
178
+ elsif error_msg.include?('SAFETY') || error_msg.include?('blocked')
179
+ raise AdapterError, "Gemini content was blocked by safety filters: #{error_msg}"
180
+ elsif error_msg.include?('image') || error_msg.include?('media')
181
+ raise AdapterError, "Gemini image processing failed: #{error_msg}. Ensure your image is a valid format and under size limits."
182
+ else
183
+ # Generic error handling
184
+ raise AdapterError, "Gemini adapter error: #{error_msg}"
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -84,13 +84,42 @@ module DSPy
84
84
  end
85
85
  end
86
86
 
87
+ # Gemini-specific metadata with additional fields
88
+ class GeminiResponseMetadata < T::Struct
89
+ extend T::Sig
90
+
91
+ const :provider, String
92
+ const :model, String
93
+ const :response_id, T.nilable(String), default: nil
94
+ const :created, T.nilable(Integer), default: nil
95
+ const :structured_output, T.nilable(T::Boolean), default: nil
96
+ const :finish_reason, T.nilable(String), default: nil
97
+ const :safety_ratings, T.nilable(T::Array[T::Hash[String, T.untyped]]), default: nil
98
+ const :streaming, T.nilable(T::Boolean), default: nil
99
+
100
+ sig { returns(T::Hash[Symbol, T.untyped]) }
101
+ def to_h
102
+ hash = {
103
+ provider: provider,
104
+ model: model
105
+ }
106
+ hash[:response_id] = response_id if response_id
107
+ hash[:created] = created if created
108
+ hash[:structured_output] = structured_output unless structured_output.nil?
109
+ hash[:finish_reason] = finish_reason if finish_reason
110
+ hash[:safety_ratings] = safety_ratings if safety_ratings
111
+ hash[:streaming] = streaming unless streaming.nil?
112
+ hash
113
+ end
114
+ end
115
+
87
116
  # Normalized response format for all LM providers
88
117
  class Response < T::Struct
89
118
  extend T::Sig
90
119
 
91
120
  const :content, String
92
121
  const :usage, T.nilable(T.any(Usage, OpenAIUsage)), default: nil
93
- const :metadata, T.any(ResponseMetadata, OpenAIResponseMetadata, AnthropicResponseMetadata, T::Hash[Symbol, T.untyped])
122
+ const :metadata, T.any(ResponseMetadata, OpenAIResponseMetadata, AnthropicResponseMetadata, GeminiResponseMetadata, T::Hash[Symbol, T.untyped])
94
123
 
95
124
  sig { returns(String) }
96
125
  def to_s
@@ -112,7 +141,7 @@ module DSPy
112
141
  module ResponseMetadataFactory
113
142
  extend T::Sig
114
143
 
115
- sig { params(provider: String, metadata: T.nilable(T::Hash[Symbol, T.untyped])).returns(T.any(ResponseMetadata, OpenAIResponseMetadata, AnthropicResponseMetadata)) }
144
+ sig { params(provider: String, metadata: T.nilable(T::Hash[Symbol, T.untyped])).returns(T.any(ResponseMetadata, OpenAIResponseMetadata, AnthropicResponseMetadata, GeminiResponseMetadata)) }
116
145
  def self.create(provider, metadata)
117
146
  # Handle nil metadata
118
147
  metadata ||= {}
@@ -123,7 +152,7 @@ module DSPy
123
152
  # Extract common fields
124
153
  common_fields = {
125
154
  provider: provider,
126
- model: metadata[:model] || 'unknown',
155
+ model: metadata[:model],
127
156
  response_id: metadata[:response_id] || metadata[:id],
128
157
  created: metadata[:created],
129
158
  structured_output: metadata[:structured_output]
@@ -143,6 +172,13 @@ module DSPy
143
172
  stop_sequence: metadata[:stop_sequence]&.to_s,
144
173
  tool_calls: metadata[:tool_calls]
145
174
  )
175
+ when 'gemini'
176
+ GeminiResponseMetadata.new(
177
+ **common_fields,
178
+ finish_reason: metadata[:finish_reason]&.to_s,
179
+ safety_ratings: metadata[:safety_ratings],
180
+ streaming: metadata[:streaming]
181
+ )
146
182
  else
147
183
  ResponseMetadata.new(**common_fields)
148
184
  end
@@ -151,7 +187,7 @@ module DSPy
151
187
  # Fallback to basic metadata
152
188
  ResponseMetadata.new(
153
189
  provider: provider,
154
- model: metadata[:model] || 'unknown'
190
+ model: metadata[:model]
155
191
  )
156
192
  end
157
193
  end
data/lib/dspy/lm/usage.rb CHANGED
@@ -72,6 +72,8 @@ module DSPy
72
72
  create_openai_usage(normalized)
73
73
  when 'anthropic'
74
74
  create_anthropic_usage(normalized)
75
+ when 'gemini'
76
+ create_gemini_usage(normalized)
75
77
  else
76
78
  create_generic_usage(normalized)
77
79
  end
@@ -136,6 +138,23 @@ module DSPy
136
138
  nil
137
139
  end
138
140
 
141
+ sig { params(data: T::Hash[Symbol, T.untyped]).returns(T.nilable(Usage)) }
142
+ def self.create_gemini_usage(data)
143
+ # Gemini uses promptTokenCount/candidatesTokenCount/totalTokenCount
144
+ input_tokens = data[:promptTokenCount] || data[:input_tokens] || 0
145
+ output_tokens = data[:candidatesTokenCount] || data[:output_tokens] || 0
146
+ total_tokens = data[:totalTokenCount] || data[:total_tokens] || (input_tokens + output_tokens)
147
+
148
+ Usage.new(
149
+ input_tokens: input_tokens,
150
+ output_tokens: output_tokens,
151
+ total_tokens: total_tokens
152
+ )
153
+ rescue => e
154
+ DSPy.logger.debug("Failed to create Gemini usage: #{e.message}")
155
+ nil
156
+ end
157
+
139
158
  sig { params(data: T::Hash[Symbol, T.untyped]).returns(T.nilable(Usage)) }
140
159
  def self.create_generic_usage(data)
141
160
  # Generic fallback
@@ -26,12 +26,35 @@ module DSPy
26
26
  'claude-3-5-haiku-20241022'
27
27
  ].freeze
28
28
 
29
+ # Gemini vision-capable models (all Gemini models support vision)
30
+ GEMINI_VISION_MODELS = [
31
+ # Gemini 2.5 series (2025)
32
+ 'gemini-2.5-pro',
33
+ 'gemini-2.5-flash',
34
+ 'gemini-2.5-flash-lite',
35
+ # Gemini 2.0 series (2024-2025)
36
+ 'gemini-2.0-flash',
37
+ 'gemini-2.0-flash-experimental',
38
+ 'gemini-2.0-flash-lite',
39
+ 'gemini-2.0-pro-experimental',
40
+ # Gemini 1.5 series
41
+ 'gemini-1.5-pro',
42
+ 'gemini-1.5-flash',
43
+ 'gemini-1.5-pro-latest',
44
+ 'gemini-1.5-flash-latest',
45
+ # Legacy models
46
+ 'gemini-pro-vision',
47
+ 'gemini-1.0-pro-vision'
48
+ ].freeze
49
+
29
50
  def self.supports_vision?(provider, model)
30
51
  case provider.to_s.downcase
31
52
  when 'openai'
32
53
  OPENAI_VISION_MODELS.any? { |m| model.include?(m) }
33
54
  when 'anthropic'
34
55
  ANTHROPIC_VISION_MODELS.any? { |m| model.include?(m) }
56
+ when 'gemini'
57
+ GEMINI_VISION_MODELS.any? { |m| model.include?(m) }
35
58
  else
36
59
  false
37
60
  end
@@ -49,6 +72,8 @@ module DSPy
49
72
  OPENAI_VISION_MODELS
50
73
  when 'anthropic'
51
74
  ANTHROPIC_VISION_MODELS
75
+ when 'gemini'
76
+ GEMINI_VISION_MODELS
52
77
  else
53
78
  []
54
79
  end
data/lib/dspy/lm.rb CHANGED
@@ -14,6 +14,7 @@ require_relative 'lm/adapter_factory'
14
14
  require_relative 'lm/adapters/openai_adapter'
15
15
  require_relative 'lm/adapters/anthropic_adapter'
16
16
  require_relative 'lm/adapters/ollama_adapter'
17
+ require_relative 'lm/adapters/gemini_adapter'
17
18
 
18
19
  # Load strategy system
19
20
  require_relative 'lm/strategy_selector'
@@ -232,10 +233,10 @@ module DSPy
232
233
  usage = result.usage
233
234
  DSPy.log('span.attributes',
234
235
  span_id: DSPy::Context.current[:span_stack].last,
235
- 'gen_ai.response.model' => result.respond_to?(:model) ? result.model : nil,
236
- 'gen_ai.usage.prompt_tokens' => usage.respond_to?(:input_tokens) ? usage.input_tokens : nil,
237
- 'gen_ai.usage.completion_tokens' => usage.respond_to?(:output_tokens) ? usage.output_tokens : nil,
238
- 'gen_ai.usage.total_tokens' => usage.respond_to?(:total_tokens) ? usage.total_tokens : nil
236
+ 'gen_ai.response.model' => result.metadata.model,
237
+ 'gen_ai.usage.prompt_tokens' => usage.input_tokens,
238
+ 'gen_ai.usage.completion_tokens' => usage.output_tokens,
239
+ 'gen_ai.usage.total_tokens' => usage.total_tokens
239
240
  )
240
241
  end
241
242
 
data/lib/dspy/module.rb CHANGED
@@ -49,10 +49,10 @@ module DSPy
49
49
  forward_untyped(**input_values)
50
50
  end
51
51
 
52
- # Get the configured LM for this instance, falling back to global
52
+ # Get the configured LM for this instance, checking fiber-local context first
53
53
  sig { returns(T.untyped) }
54
54
  def lm
55
- config.lm || DSPy.config.lm
55
+ config.lm || DSPy.current_lm
56
56
  end
57
57
  end
58
58
  end
data/lib/dspy/predict.rb CHANGED
@@ -59,6 +59,42 @@ module DSPy
59
59
  @prompt = Prompt.from_signature(signature_class)
60
60
  end
61
61
 
62
+ # Reconstruct program from serialized hash
63
+ sig { params(data: T::Hash[Symbol, T.untyped]).returns(T.attached_class) }
64
+ def self.from_h(data)
65
+ state = data[:state]
66
+ raise ArgumentError, "Missing state in serialized data" unless state
67
+
68
+ signature_class_name = state[:signature_class]
69
+ signature_class = Object.const_get(signature_class_name)
70
+ program = new(signature_class)
71
+
72
+ # Restore instruction if available
73
+ if state[:instruction]
74
+ program = program.with_instruction(state[:instruction])
75
+ end
76
+
77
+ # Restore examples if available
78
+ few_shot_examples = state[:few_shot_examples]
79
+ if few_shot_examples && !few_shot_examples.empty?
80
+ # Convert hash examples back to FewShotExample objects
81
+ examples = few_shot_examples.map do |ex|
82
+ if ex.is_a?(Hash)
83
+ DSPy::FewShotExample.new(
84
+ input: ex[:input],
85
+ output: ex[:output],
86
+ reasoning: ex[:reasoning]
87
+ )
88
+ else
89
+ ex
90
+ end
91
+ end
92
+ program = program.with_examples(examples)
93
+ end
94
+
95
+ program
96
+ end
97
+
62
98
  # Backward compatibility methods - delegate to prompt object
63
99
  sig { returns(String) }
64
100
  def system_signature
@@ -172,15 +172,35 @@ module DSPy
172
172
  sig { params(struct_class: T.class_of(T::Struct)).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
173
173
  def extract_field_info(struct_class)
174
174
  struct_class.props.map do |name, prop_info|
175
- {
175
+ field_info = {
176
176
  name: name,
177
177
  type: prop_info[:type].to_s,
178
178
  description: prop_info[:description] || "",
179
179
  required: !prop_info[:rules]&.any? { |rule| rule.is_a?(T::Props::NilableRules) }
180
180
  }
181
+
182
+ # Extract enum values if this is an enum type
183
+ if enum_values = extract_enum_values(prop_info[:type])
184
+ field_info[:enum_values] = enum_values
185
+ field_info[:is_enum] = true
186
+ end
187
+
188
+ field_info
189
+ end
190
+ end
191
+
192
+ # Extract enum values from a type if it's an enum
193
+ sig { params(type: T.untyped).returns(T.nilable(T::Array[String])) }
194
+ def extract_enum_values(type)
195
+ # Handle T::Enum types
196
+ if type.is_a?(Class) && type < T::Enum
197
+ type.values.map(&:serialize)
198
+ else
199
+ nil
181
200
  end
182
201
  end
183
202
 
203
+
184
204
  # Analyze patterns in training examples
185
205
  sig { params(examples: T::Array[T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
186
206
  def analyze_example_patterns(examples)
@@ -364,8 +384,12 @@ module DSPy
364
384
  context_parts << "Task: #{signature_class.description}" if @config.use_task_description
365
385
 
366
386
  if @config.use_input_output_analysis
367
- context_parts << "Input fields: #{analysis[:input_fields].map { |f| "#{f[:name]} (#{f[:type]})" }.join(', ')}"
368
- context_parts << "Output fields: #{analysis[:output_fields].map { |f| "#{f[:name]} (#{f[:type]})" }.join(', ')}"
387
+ # Build detailed field descriptions including enum values
388
+ input_descriptions = analysis[:input_fields].map { |f| format_field_description(f) }
389
+ output_descriptions = analysis[:output_fields].map { |f| format_field_description(f) }
390
+
391
+ context_parts << "Input fields: #{input_descriptions.join(', ')}"
392
+ context_parts << "Output fields: #{output_descriptions.join(', ')}"
369
393
  end
370
394
 
371
395
  if analysis[:common_themes] && analysis[:common_themes].any?
@@ -379,6 +403,17 @@ module DSPy
379
403
  context_parts.join("\n")
380
404
  end
381
405
 
406
+ # Format field description with enum values if applicable
407
+ sig { params(field: T::Hash[Symbol, T.untyped]).returns(String) }
408
+ def format_field_description(field)
409
+ base = "#{field[:name]} (#{field[:type]})"
410
+ if field[:is_enum] && field[:enum_values] && !field[:enum_values].empty?
411
+ "#{base} [values: #{field[:enum_values].join(', ')}]"
412
+ else
413
+ base
414
+ end
415
+ end
416
+
382
417
  # Build requirements text for instruction generation
383
418
  sig { params(analysis: T::Hash[Symbol, T.untyped]).returns(String) }
384
419
  def build_requirements_text(analysis)
data/lib/dspy/re_act.rb CHANGED
@@ -112,8 +112,16 @@ module DSPy
112
112
  # Use the enhanced output struct with ReAct fields
113
113
  @output_struct_class = enhanced_output_struct
114
114
 
115
+ # Store original signature name
116
+ @original_signature_name = signature_class.name
117
+
115
118
  class << self
116
- attr_reader :input_struct_class, :output_struct_class
119
+ attr_reader :input_struct_class, :output_struct_class, :original_signature_name
120
+
121
+ # Override name to return the original signature name
122
+ def name
123
+ @original_signature_name || super
124
+ end
117
125
  end
118
126
  end
119
127
 
@@ -123,9 +131,6 @@ module DSPy
123
131
 
124
132
  sig { params(kwargs: T.untyped).returns(T.untyped).override }
125
133
  def forward(**kwargs)
126
- lm = config.lm || DSPy.config.lm
127
- available_tools = @tools.keys
128
-
129
134
  # Validate input
130
135
  input_struct = @original_signature_class.input_struct_class.new(**kwargs)
131
136
 
@@ -90,18 +90,39 @@ module DSPy
90
90
 
91
91
  sig { params(program: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
92
92
  def serialize_program(program)
93
- # Basic serialization - can be enhanced for specific program types
94
- {
95
- class_name: program.class.name,
96
- state: extract_program_state(program)
97
- }
93
+ # Basic serialization
94
+ if program.is_a?(Hash)
95
+ # Already serialized - return as-is to preserve state
96
+ program
97
+ else
98
+ # Real program object - serialize it
99
+ {
100
+ class_name: program.class.name,
101
+ state: extract_program_state(program)
102
+ }
103
+ end
98
104
  end
99
105
 
100
- sig { params(data: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
106
+ sig { params(data: T.untyped).returns(T.untyped) }
101
107
  def self.deserialize_program(data)
102
- # Basic deserialization - would need enhancement for complex programs
103
- # For now, return the serialized state
104
- data
108
+ # Ensure data is a Hash
109
+ unless data.is_a?(Hash)
110
+ raise ArgumentError, "Expected Hash for program data, got #{data.class.name}"
111
+ end
112
+
113
+ # Get class name from the serialized data
114
+ class_name = data[:class_name]
115
+ raise ArgumentError, "Missing class_name in serialized data" unless class_name
116
+
117
+ # Get the class constant
118
+ program_class = Object.const_get(class_name)
119
+
120
+ # Use the class's from_h method
121
+ unless program_class.respond_to?(:from_h)
122
+ raise ArgumentError, "Class #{class_name} does not support deserialization (missing from_h method)"
123
+ end
124
+
125
+ program_class.from_h(data)
105
126
  end
106
127
 
107
128
  sig { params(program: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
@@ -310,6 +331,20 @@ module DSPy
310
331
  { programs: [], summary: { total_programs: 0, avg_score: 0.0 } }
311
332
  end
312
333
 
334
+ # Extract signature class name from program object
335
+ unless saved_program.program.respond_to?(:signature_class)
336
+ raise ArgumentError, "Program #{saved_program.program.class.name} does not respond to signature_class method"
337
+ end
338
+
339
+ signature_class_name = saved_program.program.signature_class.name
340
+
341
+ if signature_class_name.nil? || signature_class_name.empty?
342
+ raise(
343
+ "Program #{saved_program.program.class.name} has a signature class that does not provide a name.\n" \
344
+ "Ensure the signature class responds to #name or that signature_class_name is stored in program state."
345
+ )
346
+ end
347
+
313
348
  # Add or update program entry
314
349
  program_entry = {
315
350
  program_id: saved_program.program_id,
@@ -317,7 +352,7 @@ module DSPy
317
352
  best_score: saved_program.optimization_result[:best_score_value],
318
353
  score_name: saved_program.optimization_result[:best_score_name],
319
354
  optimizer: saved_program.optimization_result[:metadata]&.dig(:optimizer),
320
- signature_class: saved_program.metadata[:signature_class],
355
+ signature_class: signature_class_name,
321
356
  metadata: saved_program.metadata
322
357
  }
323
358
 
@@ -18,8 +18,13 @@ module DSPy
18
18
  module AutoMode
19
19
  extend T::Sig
20
20
 
21
- sig { returns(MIPROv2) }
22
- def self.light
21
+ sig do
22
+ params(
23
+ metric: T.nilable(T.proc.params(arg0: T.untyped, arg1: T.untyped).returns(T.untyped)),
24
+ kwargs: T.untyped
25
+ ).returns(MIPROv2)
26
+ end
27
+ def self.light(metric: nil, **kwargs)
23
28
  config = MIPROv2Config.new
24
29
  config.num_trials = 6
25
30
  config.num_instruction_candidates = 3
@@ -28,11 +33,16 @@ module DSPy
28
33
  config.bootstrap_sets = 3
29
34
  config.optimization_strategy = "greedy"
30
35
  config.early_stopping_patience = 2
31
- MIPROv2.new(config: config)
36
+ MIPROv2.new(metric: metric, config: config, **kwargs)
32
37
  end
33
38
 
34
- sig { returns(MIPROv2) }
35
- def self.medium
39
+ sig do
40
+ params(
41
+ metric: T.nilable(T.proc.params(arg0: T.untyped, arg1: T.untyped).returns(T.untyped)),
42
+ kwargs: T.untyped
43
+ ).returns(MIPROv2)
44
+ end
45
+ def self.medium(metric: nil, **kwargs)
36
46
  config = MIPROv2Config.new
37
47
  config.num_trials = 12
38
48
  config.num_instruction_candidates = 5
@@ -41,11 +51,16 @@ module DSPy
41
51
  config.bootstrap_sets = 5
42
52
  config.optimization_strategy = "adaptive"
43
53
  config.early_stopping_patience = 3
44
- MIPROv2.new(config: config)
54
+ MIPROv2.new(metric: metric, config: config, **kwargs)
45
55
  end
46
56
 
47
- sig { returns(MIPROv2) }
48
- def self.heavy
57
+ sig do
58
+ params(
59
+ metric: T.nilable(T.proc.params(arg0: T.untyped, arg1: T.untyped).returns(T.untyped)),
60
+ kwargs: T.untyped
61
+ ).returns(MIPROv2)
62
+ end
63
+ def self.heavy(metric: nil, **kwargs)
49
64
  config = MIPROv2Config.new
50
65
  config.num_trials = 18
51
66
  config.num_instruction_candidates = 8
@@ -54,7 +69,7 @@ module DSPy
54
69
  config.bootstrap_sets = 8
55
70
  config.optimization_strategy = "bayesian"
56
71
  config.early_stopping_patience = 5
57
- MIPROv2.new(config: config)
72
+ MIPROv2.new(metric: metric, config: config, **kwargs)
58
73
  end
59
74
  end
60
75
 
@@ -736,12 +751,27 @@ module DSPy
736
751
  best_score_value: best_score,
737
752
  metadata: metadata,
738
753
  evaluated_candidates: @evaluated_candidates,
739
- optimization_trace: optimization_result[:optimization_state] || {},
754
+ optimization_trace: serialize_optimization_trace(optimization_result[:optimization_state]),
740
755
  bootstrap_statistics: bootstrap_result.statistics,
741
756
  proposal_statistics: proposal_result.analysis
742
757
  )
743
758
  end
744
759
 
760
+ # Serialize optimization trace for better JSON output
761
+ sig { params(optimization_state: T.nilable(T::Hash[Symbol, T.untyped])).returns(T::Hash[Symbol, T.untyped]) }
762
+ def serialize_optimization_trace(optimization_state)
763
+ return {} unless optimization_state
764
+
765
+ serialized_trace = optimization_state.dup
766
+
767
+ # Convert candidate objects to their hash representations
768
+ if serialized_trace[:candidates]
769
+ serialized_trace[:candidates] = serialized_trace[:candidates].map(&:to_h)
770
+ end
771
+
772
+ serialized_trace
773
+ end
774
+
745
775
  # Helper methods
746
776
  sig { params(program: T.untyped).returns(T.nilable(String)) }
747
777
  def extract_current_instruction(program)
@@ -247,15 +247,17 @@ module DSPy
247
247
  prediction = program.call(**example.input_values)
248
248
 
249
249
  # Check if prediction matches expected output
250
+ prediction_hash = extract_output_fields_from_prediction(prediction, example.signature_class)
251
+
250
252
  if metric
251
- success = metric.call(example, prediction.to_h)
253
+ success = metric.call(example, prediction_hash)
252
254
  else
253
- success = example.matches_prediction?(prediction.to_h)
255
+ success = example.matches_prediction?(prediction_hash)
254
256
  end
255
257
 
256
258
  if success
257
259
  # Create a new example with the successful prediction as reasoning/context
258
- successful_example = create_successful_bootstrap_example(example, prediction)
260
+ successful_example = create_successful_bootstrap_example(example, prediction_hash)
259
261
  successful << successful_example
260
262
 
261
263
  emit_bootstrap_example_event(index, true, nil)
@@ -311,7 +313,7 @@ module DSPy
311
313
  sig do
312
314
  params(
313
315
  original_example: DSPy::Example,
314
- prediction: T.untyped
316
+ prediction: T::Hash[Symbol, T.untyped]
315
317
  ).returns(DSPy::Example)
316
318
  end
317
319
  def self.create_successful_bootstrap_example(original_example, prediction)
@@ -319,7 +321,7 @@ module DSPy
319
321
  DSPy::Example.new(
320
322
  signature_class: original_example.signature_class,
321
323
  input: original_example.input_values,
322
- expected: prediction.to_h,
324
+ expected: prediction,
323
325
  id: "bootstrap_#{original_example.id || SecureRandom.uuid}",
324
326
  metadata: {
325
327
  source: "bootstrap",
@@ -329,6 +331,29 @@ module DSPy
329
331
  )
330
332
  end
331
333
 
334
+ # Extract only output fields from prediction (exclude input fields)
335
+ sig do
336
+ params(
337
+ prediction: T.untyped,
338
+ signature_class: T.class_of(DSPy::Signature)
339
+ ).returns(T::Hash[Symbol, T.untyped])
340
+ end
341
+ def self.extract_output_fields_from_prediction(prediction, signature_class)
342
+ prediction_hash = prediction.to_h
343
+
344
+ # Get output field names from signature
345
+ output_fields = signature_class.output_field_descriptors.keys
346
+
347
+ # Filter prediction to only include output fields
348
+ filtered_expected = {}
349
+ output_fields.each do |field_name|
350
+ if prediction_hash.key?(field_name)
351
+ filtered_expected[field_name] = prediction_hash[field_name]
352
+ end
353
+ end
354
+
355
+ filtered_expected
356
+ end
332
357
 
333
358
  # Create default metric for examples
334
359
  sig { params(examples: T::Array[T.untyped]).returns(T.nilable(T.proc.params(arg0: T.untyped, arg1: T.untyped).returns(T::Boolean))) }
data/lib/dspy/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DSPy
4
- VERSION = "0.19.1"
4
+ VERSION = "0.20.0"
5
5
  end
data/lib/dspy.rb CHANGED
@@ -74,6 +74,20 @@ module DSPy
74
74
  end
75
75
  end
76
76
 
77
+ # Fiber-local LM context for temporary model overrides
78
+ FIBER_LM_KEY = :dspy_fiber_lm
79
+
80
+ def self.current_lm
81
+ Fiber[FIBER_LM_KEY] || config.lm
82
+ end
83
+
84
+ def self.with_lm(lm)
85
+ previous_lm = Fiber[FIBER_LM_KEY]
86
+ Fiber[FIBER_LM_KEY] = lm
87
+ yield
88
+ ensure
89
+ Fiber[FIBER_LM_KEY] = previous_lm
90
+ end
77
91
  end
78
92
 
79
93
  require_relative 'dspy/module'
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dspy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.1
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-08-11 00:00:00.000000000 Z
10
+ date: 2025-08-26 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dry-configurable
@@ -79,6 +79,20 @@ dependencies:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
81
  version: 1.5.0
82
+ - !ruby/object:Gem::Dependency
83
+ name: gemini-ai
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '4.3'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '4.3'
82
96
  - !ruby/object:Gem::Dependency
83
97
  name: sorbet-runtime
84
98
  requirement: !ruby/object:Gem::Requirement
@@ -186,6 +200,7 @@ files:
186
200
  - lib/dspy/lm/adapter.rb
187
201
  - lib/dspy/lm/adapter_factory.rb
188
202
  - lib/dspy/lm/adapters/anthropic_adapter.rb
203
+ - lib/dspy/lm/adapters/gemini_adapter.rb
189
204
  - lib/dspy/lm/adapters/ollama_adapter.rb
190
205
  - lib/dspy/lm/adapters/openai/schema_converter.rb
191
206
  - lib/dspy/lm/adapters/openai_adapter.rb