dspy 0.19.0 → 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 +4 -4
- data/README.md +93 -69
- data/lib/dspy/chain_of_thought.rb +1 -0
- data/lib/dspy/code_act.rb +9 -3
- data/lib/dspy/image.rb +33 -0
- data/lib/dspy/lm/adapter_factory.rb +2 -1
- data/lib/dspy/lm/adapters/gemini_adapter.rb +189 -0
- data/lib/dspy/lm/response.rb +40 -4
- data/lib/dspy/lm/usage.rb +19 -0
- data/lib/dspy/lm/vision_models.rb +25 -0
- data/lib/dspy/lm.rb +5 -4
- data/lib/dspy/module.rb +2 -2
- data/lib/dspy/predict.rb +36 -0
- data/lib/dspy/propose/grounded_proposer.rb +38 -3
- data/lib/dspy/re_act.rb +9 -4
- data/lib/dspy/storage/program_storage.rb +45 -10
- data/lib/dspy/teleprompt/mipro_v2.rb +40 -10
- data/lib/dspy/teleprompt/utils.rb +30 -5
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +14 -0
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 44cf35be07e90187237ccdc79533d0ca76dbe2cb1040f0e841bff69afd7e71fc
|
4
|
+
data.tar.gz: 443a4d5dafe1fc7c90335e2b6294a4b1e1dd77b77cb24c19c7f8237f1824b2d0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6e2e2e6098773c599190e0ab5e43ecd883e04359268557f28b0350219b98995c8fbe6633db2a5d1a34be1d250ae10ce05cd4aaaebefd6c13f5e855fcf587b5ef
|
7
|
+
data.tar.gz: b14760bb9cf7075991fdfc274bab60e1d55a4547a8b0da2b78c37bb76c69c4963a899f8ab26f26612210957f6184999b769adc590f90bd60714f992ab1c20230
|
data/README.md
CHANGED
@@ -2,12 +2,17 @@
|
|
2
2
|
|
3
3
|
[](https://rubygems.org/gems/dspy)
|
4
4
|
[](https://rubygems.org/gems/dspy)
|
5
|
+
[](https://github.com/vicentereig/dspy.rb/actions/workflows/ruby.yml)
|
6
|
+
[](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
|
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
|
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** -
|
59
|
-
- **Chain of Thought** - Step-by-step reasoning for complex problems
|
60
|
-
- **ReAct** - Tool-using agents with
|
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
|
-
- **
|
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** -
|
68
|
-
- **
|
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
|
-
-
|
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
|
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
|
-
|
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
|
-
|
179
|
-
- ✅ **
|
180
|
-
- ✅ **
|
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
|
-
|
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
|
-
|
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
|
-
|
191
|
-
|
192
|
-
- 🚧 **
|
193
|
-
- 🚧 **
|
194
|
-
- 🚧 **
|
195
|
-
- 🚧 **
|
196
|
-
|
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.
|
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
|
|
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
|
@@ -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
|
data/lib/dspy/lm/response.rb
CHANGED
@@ -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]
|
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]
|
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.
|
236
|
-
'gen_ai.usage.prompt_tokens' => usage.
|
237
|
-
'gen_ai.usage.completion_tokens' => usage.
|
238
|
-
'gen_ai.usage.total_tokens' => usage.
|
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,
|
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.
|
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
|
-
|
368
|
-
|
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
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
106
|
+
sig { params(data: T.untyped).returns(T.untyped) }
|
101
107
|
def self.deserialize_program(data)
|
102
|
-
#
|
103
|
-
|
104
|
-
|
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:
|
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
|
22
|
-
|
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
|
35
|
-
|
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
|
48
|
-
|
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,
|
253
|
+
success = metric.call(example, prediction_hash)
|
252
254
|
else
|
253
|
-
success = example.matches_prediction?(
|
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,
|
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
|
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
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.
|
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-
|
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
|