dspy 0.26.1 → 0.27.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: e243b7278275462baea2f493270166a1ae4b5419d4f072a769e4ba4b0f65e3e0
4
- data.tar.gz: 13bcbcf4ee67c08f19ad619bc8118e39ca9a02045c5a18b3a664fb112724fa87
3
+ metadata.gz: 5bb3b493e5411fd1f18028a3177c99149c2507f4d05a100746b2da734daa6a63
4
+ data.tar.gz: 78d2325d7b28a1b393284ec765c0f9fa3048c60afd864e3fcec0a88aac96cdc7
5
5
  SHA512:
6
- metadata.gz: 687385021bf9391b22ae51a3f7c05880bec9691347a4e8ecac9175b7e81190c9f63cb0670a94e7324a045d748346cc91b6f7e174808607eaf0d02b8a0a117992
7
- data.tar.gz: 3212712d53aca34cbc475503396d4fbeb7b8c11632b673782e7cd2dc2e6fdb22a3ad67688d92b60bd72e845f7b598b446e94fd3f5c6efd5e87f212bb1be14b9e
6
+ metadata.gz: c11ef22db12b776b0dacb648cc60312aedb398b13020991827b85eca169b319960e8c4f31cc8216c93a766f67ed779998ee22f932bc02cb4ba802ce58c0a4ff4
7
+ data.tar.gz: 68847a5ef35187be690b82e0bd1b30d4da7988c2bf1da9fd3820e4ae2315c96900f798b4580b1dcd236fa4c4e45a4289c57301b36b1c24564fda020ee174107b
data/README.md CHANGED
@@ -59,6 +59,25 @@ puts result.sentiment # => #<Sentiment::Positive>
59
59
  puts result.confidence # => 0.85
60
60
  ```
61
61
 
62
+ ### Alternative Providers
63
+
64
+ DSPy.rb supports multiple providers with native structured outputs:
65
+
66
+ ```ruby
67
+ # Google Gemini with native structured outputs
68
+ DSPy.configure do |c|
69
+ c.lm = DSPy::LM.new('gemini/gemini-1.5-flash',
70
+ api_key: ENV['GEMINI_API_KEY'],
71
+ structured_outputs: true) # Supports gemini-1.5-pro, gemini-1.5-flash, gemini-2.0-flash-exp
72
+ end
73
+
74
+ # Anthropic Claude with tool-based extraction
75
+ DSPy.configure do |c|
76
+ c.lm = DSPy::LM.new('anthropic/claude-3-sonnet-20241022',
77
+ api_key: ENV['ANTHROPIC_API_KEY']) # Automatic strategy selection
78
+ end
79
+ ```
80
+
62
81
  ## What You Get
63
82
 
64
83
  **Core Building Blocks:**
@@ -77,7 +96,7 @@ puts result.confidence # => 0.85
77
96
  - **GEPA Optimization** - Genetic-Pareto optimization for multi-objective prompt improvement
78
97
 
79
98
  **Production Features:**
80
- - **Reliable JSON Extraction** - Native OpenAI structured outputs, Anthropic extraction patterns, and automatic strategy selection with fallback
99
+ - **Reliable JSON Extraction** - Native structured outputs for OpenAI and Gemini, Anthropic tool-based extraction, and automatic strategy selection with fallback
81
100
  - **Type-Safe Configuration** - Strategy enums with automatic provider optimization (Strict/Compatible modes)
82
101
  - **Smart Retry Logic** - Progressive fallback with exponential backoff for handling transient failures
83
102
  - **Zero-Config Langfuse Integration** - Set env vars and get automatic OpenTelemetry traces in Langfuse
@@ -89,6 +108,7 @@ puts result.confidence # => 0.85
89
108
  - LLM provider support using official Ruby clients:
90
109
  - [OpenAI Ruby](https://github.com/openai/openai-ruby) with vision model support
91
110
  - [Anthropic Ruby SDK](https://github.com/anthropics/anthropic-sdk-ruby) with multimodal capabilities
111
+ - [Google Gemini API](https://ai.google.dev/) with native structured outputs
92
112
  - [Ollama](https://ollama.com/) via OpenAI compatibility layer for local models
93
113
  - **Multimodal Support** - Complete image analysis with DSPy::Image, type-safe bounding boxes, vision-capable models
94
114
  - Runtime type checking with [Sorbet](https://sorbet.org/) including T::Enum and union types
@@ -24,8 +24,8 @@ module DSPy
24
24
 
25
25
  # Pass provider-specific options
26
26
  adapter_options = { model: model, api_key: api_key }
27
- # Both OpenAI and Ollama accept additional options
28
- adapter_options.merge!(options) if %w[openai ollama].include?(provider)
27
+ # OpenAI, Ollama, and Gemini accept additional options
28
+ adapter_options.merge!(options) if %w[openai ollama gemini].include?(provider)
29
29
 
30
30
  adapter_class.new(**adapter_options)
31
31
  end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sorbet-runtime"
4
+ require_relative "../../cache_manager"
5
+
6
+ module DSPy
7
+ class LM
8
+ module Adapters
9
+ module Gemini
10
+ # Converts DSPy signatures to Gemini structured output format
11
+ class SchemaConverter
12
+ extend T::Sig
13
+
14
+ # Models that support structured outputs
15
+ STRUCTURED_OUTPUT_MODELS = T.let([
16
+ "gemini-1.5-pro",
17
+ "gemini-1.5-flash",
18
+ "gemini-2.0-flash-exp"
19
+ ].freeze, T::Array[String])
20
+
21
+ sig { params(signature_class: T.class_of(DSPy::Signature)).returns(T::Hash[Symbol, T.untyped]) }
22
+ def self.to_gemini_format(signature_class)
23
+ # Check cache first
24
+ cache_manager = DSPy::LM.cache_manager
25
+ cached_schema = cache_manager.get_schema(signature_class, "gemini", {})
26
+
27
+ if cached_schema
28
+ DSPy.logger.debug("Using cached schema for #{signature_class.name}")
29
+ return cached_schema
30
+ end
31
+
32
+ # Get the output JSON schema from the signature class
33
+ output_schema = signature_class.output_json_schema
34
+
35
+ # Convert to Gemini format (OpenAPI 3.0 Schema subset - not related to OpenAI)
36
+ gemini_schema = convert_dspy_schema_to_gemini(output_schema)
37
+
38
+ # Cache the result
39
+ cache_manager.cache_schema(signature_class, "gemini", gemini_schema, {})
40
+
41
+ gemini_schema
42
+ end
43
+
44
+ sig { params(model: String).returns(T::Boolean) }
45
+ def self.supports_structured_outputs?(model)
46
+ # Check cache first
47
+ cache_manager = DSPy::LM.cache_manager
48
+ cached_result = cache_manager.get_capability(model, "structured_outputs")
49
+
50
+ if !cached_result.nil?
51
+ DSPy.logger.debug("Using cached capability check for #{model}")
52
+ return cached_result
53
+ end
54
+
55
+ # Extract base model name without provider prefix
56
+ base_model = model.sub(/^gemini\//, "")
57
+
58
+ # Check if it's a supported model or a newer version
59
+ result = STRUCTURED_OUTPUT_MODELS.any? { |supported| base_model.start_with?(supported) }
60
+
61
+ # Cache the result
62
+ cache_manager.cache_capability(model, "structured_outputs", result)
63
+
64
+ result
65
+ end
66
+
67
+ sig { params(schema: T::Hash[Symbol, T.untyped]).returns(T::Array[String]) }
68
+ def self.validate_compatibility(schema)
69
+ issues = []
70
+
71
+ # Check for deeply nested objects (Gemini has depth limits)
72
+ depth = calculate_depth(schema)
73
+ if depth > 5
74
+ issues << "Schema depth (#{depth}) exceeds recommended limit of 5 levels"
75
+ end
76
+
77
+ issues
78
+ end
79
+
80
+ private
81
+
82
+ sig { params(dspy_schema: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
83
+ def self.convert_dspy_schema_to_gemini(dspy_schema)
84
+ result = {
85
+ type: "object",
86
+ properties: {},
87
+ required: []
88
+ }
89
+
90
+ # Convert properties
91
+ properties = dspy_schema[:properties] || {}
92
+ properties.each do |prop_name, prop_schema|
93
+ result[:properties][prop_name] = convert_property_to_gemini(prop_schema)
94
+ end
95
+
96
+ # Set required fields
97
+ result[:required] = (dspy_schema[:required] || []).map(&:to_s)
98
+
99
+ result
100
+ end
101
+
102
+ sig { params(property_schema: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
103
+ def self.convert_property_to_gemini(property_schema)
104
+ case property_schema[:type]
105
+ when "string"
106
+ result = { type: "string" }
107
+ result[:enum] = property_schema[:enum] if property_schema[:enum]
108
+ result
109
+ when "integer"
110
+ { type: "integer" }
111
+ when "number"
112
+ { type: "number" }
113
+ when "boolean"
114
+ { type: "boolean" }
115
+ when "array"
116
+ {
117
+ type: "array",
118
+ items: convert_property_to_gemini(property_schema[:items] || { type: "string" })
119
+ }
120
+ when "object"
121
+ result = { type: "object" }
122
+
123
+ if property_schema[:properties]
124
+ result[:properties] = {}
125
+ property_schema[:properties].each do |nested_prop, nested_schema|
126
+ result[:properties][nested_prop] = convert_property_to_gemini(nested_schema)
127
+ end
128
+
129
+ # Set required fields for nested objects
130
+ if property_schema[:required]
131
+ result[:required] = property_schema[:required].map(&:to_s)
132
+ end
133
+ end
134
+
135
+ result
136
+ else
137
+ # Default to string for unknown types
138
+ { type: "string" }
139
+ end
140
+ end
141
+
142
+ sig { params(schema: T::Hash[Symbol, T.untyped], current_depth: Integer).returns(Integer) }
143
+ def self.calculate_depth(schema, current_depth = 0)
144
+ return current_depth unless schema.is_a?(Hash)
145
+
146
+ max_depth = current_depth
147
+
148
+ # Check properties
149
+ if schema[:properties].is_a?(Hash)
150
+ schema[:properties].each_value do |prop|
151
+ if prop.is_a?(Hash)
152
+ prop_depth = calculate_depth(prop, current_depth + 1)
153
+ max_depth = [max_depth, prop_depth].max
154
+ end
155
+ end
156
+ end
157
+
158
+ # Check array items
159
+ if schema[:items].is_a?(Hash)
160
+ items_depth = calculate_depth(schema[:items], current_depth + 1)
161
+ max_depth = [max_depth, items_depth].max
162
+ end
163
+
164
+ max_depth
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -7,10 +7,12 @@ require_relative '../vision_models'
7
7
  module DSPy
8
8
  class LM
9
9
  class GeminiAdapter < Adapter
10
- def initialize(model:, api_key:)
11
- super
10
+ def initialize(model:, api_key:, structured_outputs: false)
11
+ super(model: model, api_key: api_key)
12
12
  validate_api_key!(api_key, 'gemini')
13
13
 
14
+ @structured_outputs_enabled = structured_outputs
15
+
14
16
  @client = Gemini.new(
15
17
  credentials: {
16
18
  service: 'generative-language-api',
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_strategy"
4
+ require_relative "../adapters/gemini/schema_converter"
5
+
6
+ module DSPy
7
+ class LM
8
+ module Strategies
9
+ # Strategy for using Gemini's native structured output feature
10
+ class GeminiStructuredOutputStrategy < BaseStrategy
11
+ extend T::Sig
12
+
13
+ sig { override.returns(T::Boolean) }
14
+ def available?
15
+ # Check if adapter is Gemini and supports structured outputs
16
+ return false unless adapter.is_a?(DSPy::LM::GeminiAdapter)
17
+ return false unless adapter.instance_variable_get(:@structured_outputs_enabled)
18
+
19
+ DSPy::LM::Adapters::Gemini::SchemaConverter.supports_structured_outputs?(adapter.model)
20
+ end
21
+
22
+ sig { override.returns(Integer) }
23
+ def priority
24
+ 100 # Highest priority - native structured outputs are most reliable
25
+ end
26
+
27
+ sig { override.returns(String) }
28
+ def name
29
+ "gemini_structured_output"
30
+ end
31
+
32
+ sig { override.params(messages: T::Array[T::Hash[Symbol, String]], request_params: T::Hash[Symbol, T.untyped]).void }
33
+ def prepare_request(messages, request_params)
34
+ # Convert signature to Gemini schema format
35
+ schema = DSPy::LM::Adapters::Gemini::SchemaConverter.to_gemini_format(signature_class)
36
+
37
+ # Add generation_config for structured output
38
+ request_params[:generation_config] = {
39
+ response_mime_type: "application/json",
40
+ response_schema: schema
41
+ }
42
+ end
43
+
44
+ sig { override.params(response: DSPy::LM::Response).returns(T.nilable(String)) }
45
+ def extract_json(response)
46
+ # With Gemini structured outputs, the response should already be valid JSON
47
+ # Just return the content as-is
48
+ response.content
49
+ end
50
+
51
+ sig { override.params(error: StandardError).returns(T::Boolean) }
52
+ def handle_error(error)
53
+ # Handle Gemini-specific structured output errors
54
+ error_msg = error.message.to_s.downcase
55
+ if error_msg.include?("schema") || error_msg.include?("generation_config") || error_msg.include?("response_schema")
56
+ # Log the error and return true to indicate we handled it
57
+ # This allows fallback to another strategy
58
+ DSPy.logger.warn("Gemini structured output failed: #{error.message}")
59
+ true
60
+ else
61
+ false
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -5,6 +5,7 @@ require_relative "strategies/base_strategy"
5
5
  require_relative "strategies/openai_structured_output_strategy"
6
6
  require_relative "strategies/anthropic_tool_use_strategy"
7
7
  require_relative "strategies/anthropic_extraction_strategy"
8
+ require_relative "strategies/gemini_structured_output_strategy"
8
9
  require_relative "strategies/enhanced_prompting_strategy"
9
10
 
10
11
  module DSPy
@@ -13,11 +14,23 @@ module DSPy
13
14
  class StrategySelector
14
15
  extend T::Sig
15
16
 
17
+ # Strategy names enum for type safety
18
+ class StrategyName < T::Enum
19
+ enums do
20
+ OpenAIStructuredOutput = new('openai_structured_output')
21
+ AnthropicToolUse = new('anthropic_tool_use')
22
+ AnthropicExtraction = new('anthropic_extraction')
23
+ GeminiStructuredOutput = new('gemini_structured_output')
24
+ EnhancedPrompting = new('enhanced_prompting')
25
+ end
26
+ end
27
+
16
28
  # Available strategies in order of registration
17
29
  STRATEGIES = [
18
30
  Strategies::OpenAIStructuredOutputStrategy,
19
31
  Strategies::AnthropicToolUseStrategy,
20
32
  Strategies::AnthropicExtractionStrategy,
33
+ Strategies::GeminiStructuredOutputStrategy,
21
34
  Strategies::EnhancedPromptingStrategy
22
35
  ].freeze
23
36
 
@@ -38,7 +51,7 @@ module DSPy
38
51
 
39
52
  # If strict strategy not available, fall back to compatible for Strict preference
40
53
  if is_strict_preference?(DSPy.config.structured_outputs.strategy)
41
- compatible_strategy = find_strategy_by_name("enhanced_prompting")
54
+ compatible_strategy = find_strategy_by_name(StrategyName::EnhancedPrompting)
42
55
  return compatible_strategy if compatible_strategy&.available?
43
56
  end
44
57
 
@@ -65,7 +78,7 @@ module DSPy
65
78
  end
66
79
 
67
80
  # Check if a specific strategy is available
68
- sig { params(strategy_name: String).returns(T::Boolean) }
81
+ sig { params(strategy_name: StrategyName).returns(T::Boolean) }
69
82
  def strategy_available?(strategy_name)
70
83
  strategy = find_strategy_by_name(strategy_name)
71
84
  strategy&.available? || false
@@ -82,7 +95,7 @@ module DSPy
82
95
  select_provider_optimized_strategy
83
96
  when DSPy::Strategy::Compatible
84
97
  # Use enhanced prompting
85
- find_strategy_by_name("enhanced_prompting")
98
+ find_strategy_by_name(StrategyName::EnhancedPrompting)
86
99
  else
87
100
  nil
88
101
  end
@@ -98,15 +111,19 @@ module DSPy
98
111
  sig { returns(T.nilable(Strategies::BaseStrategy)) }
99
112
  def select_provider_optimized_strategy
100
113
  # Try OpenAI structured output first
101
- openai_strategy = find_strategy_by_name("openai_structured_output")
114
+ openai_strategy = find_strategy_by_name(StrategyName::OpenAIStructuredOutput)
102
115
  return openai_strategy if openai_strategy&.available?
103
116
 
117
+ # Try Gemini structured output
118
+ gemini_strategy = find_strategy_by_name(StrategyName::GeminiStructuredOutput)
119
+ return gemini_strategy if gemini_strategy&.available?
120
+
104
121
  # Try Anthropic tool use first
105
- anthropic_tool_strategy = find_strategy_by_name("anthropic_tool_use")
122
+ anthropic_tool_strategy = find_strategy_by_name(StrategyName::AnthropicToolUse)
106
123
  return anthropic_tool_strategy if anthropic_tool_strategy&.available?
107
124
 
108
125
  # Fall back to Anthropic extraction
109
- anthropic_strategy = find_strategy_by_name("anthropic_extraction")
126
+ anthropic_strategy = find_strategy_by_name(StrategyName::AnthropicExtraction)
110
127
  return anthropic_strategy if anthropic_strategy&.available?
111
128
 
112
129
  # No provider-specific strategy available
@@ -118,9 +135,9 @@ module DSPy
118
135
  STRATEGIES.map { |klass| klass.new(@adapter, @signature_class) }
119
136
  end
120
137
 
121
- sig { params(name: String).returns(T.nilable(Strategies::BaseStrategy)) }
138
+ sig { params(name: StrategyName).returns(T.nilable(Strategies::BaseStrategy)) }
122
139
  def find_strategy_by_name(name)
123
- @strategies.find { |s| s.name == name }
140
+ @strategies.find { |s| s.name == name.serialize }
124
141
  end
125
142
  end
126
143
  end
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.26.1"
4
+ VERSION = "0.27.0"
5
5
  end
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.26.1
4
+ version: 0.27.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-09-10 00:00:00.000000000 Z
10
+ date: 2025-09-13 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dry-configurable
@@ -207,6 +207,7 @@ files:
207
207
  - lib/dspy/lm/adapter.rb
208
208
  - lib/dspy/lm/adapter_factory.rb
209
209
  - lib/dspy/lm/adapters/anthropic_adapter.rb
210
+ - lib/dspy/lm/adapters/gemini/schema_converter.rb
210
211
  - lib/dspy/lm/adapters/gemini_adapter.rb
211
212
  - lib/dspy/lm/adapters/ollama_adapter.rb
212
213
  - lib/dspy/lm/adapters/openai/schema_converter.rb
@@ -221,6 +222,7 @@ files:
221
222
  - lib/dspy/lm/strategies/anthropic_tool_use_strategy.rb
222
223
  - lib/dspy/lm/strategies/base_strategy.rb
223
224
  - lib/dspy/lm/strategies/enhanced_prompting_strategy.rb
225
+ - lib/dspy/lm/strategies/gemini_structured_output_strategy.rb
224
226
  - lib/dspy/lm/strategies/openai_structured_output_strategy.rb
225
227
  - lib/dspy/lm/strategy_selector.rb
226
228
  - lib/dspy/lm/structured_output_strategy.rb