dspy 0.27.6 → 0.28.1
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 +28 -9
- data/lib/dspy/lm/adapter_factory.rb +1 -1
- data/lib/dspy/lm/adapters/anthropic_adapter.rb +3 -2
- data/lib/dspy/lm/adapters/gemini/schema_converter.rb +25 -16
- data/lib/dspy/lm/chat_strategy.rb +38 -0
- data/lib/dspy/lm/json_strategy.rb +217 -0
- data/lib/dspy/lm.rb +46 -21
- data/lib/dspy/mixins/type_coercion.rb +7 -7
- data/lib/dspy/re_act.rb +253 -68
- data/lib/dspy/structured_outputs_prompt.rb +53 -0
- data/lib/dspy/tools/base.rb +5 -7
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +0 -8
- metadata +5 -12
- data/lib/dspy/lm/retry_handler.rb +0 -132
- data/lib/dspy/lm/strategies/anthropic_extraction_strategy.rb +0 -78
- data/lib/dspy/lm/strategies/anthropic_tool_use_strategy.rb +0 -132
- data/lib/dspy/lm/strategies/base_strategy.rb +0 -53
- data/lib/dspy/lm/strategies/enhanced_prompting_strategy.rb +0 -178
- data/lib/dspy/lm/strategies/gemini_structured_output_strategy.rb +0 -80
- data/lib/dspy/lm/strategies/openai_structured_output_strategy.rb +0 -65
- data/lib/dspy/lm/strategy_selector.rb +0 -144
- data/lib/dspy/lm/structured_output_strategy.rb +0 -17
- data/lib/dspy/strategy.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8b377060443eeb9c3c5d975e76750d6c519d1b93cf6f20dc6ad20bcda08d1ca4
|
4
|
+
data.tar.gz: 4580845b3fd9991b531c8c2bd809595cbd3328da57a23e58307cdbe52e3822bc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 444a5e08364b2e996bf49d230cb9a94a1930e26d2ea796cd0104ea4888b45ade4099cec502b92c08752631adff47b1d9d1897da9209e9faabbb28fb6761935b0
|
7
|
+
data.tar.gz: c1b3a83482c861923304c463d6f0b1c9042fbc31da920526ec18ed0f5148b4408a9023d312eded9263ca2d706e4779b78526e28dc50a17458cbbac9afa56b024
|
data/README.md
CHANGED
@@ -59,22 +59,41 @@ puts result.sentiment # => #<Sentiment::Positive>
|
|
59
59
|
puts result.confidence # => 0.85
|
60
60
|
```
|
61
61
|
|
62
|
-
###
|
62
|
+
### Access to 200+ Models Across 5 Providers
|
63
63
|
|
64
|
-
DSPy.rb
|
64
|
+
DSPy.rb provides unified access to major LLM providers with provider-specific optimizations:
|
65
65
|
|
66
66
|
```ruby
|
67
|
-
#
|
67
|
+
# OpenAI (GPT-4, GPT-4o, GPT-4o-mini, GPT-5, etc.)
|
68
68
|
DSPy.configure do |c|
|
69
|
-
c.lm = DSPy::LM.new('
|
70
|
-
api_key: ENV['
|
71
|
-
structured_outputs: true) #
|
69
|
+
c.lm = DSPy::LM.new('openai/gpt-4o-mini',
|
70
|
+
api_key: ENV['OPENAI_API_KEY'],
|
71
|
+
structured_outputs: true) # Native JSON mode
|
72
|
+
end
|
73
|
+
|
74
|
+
# Google Gemini (Gemini 1.5 Pro, Flash, Gemini 2.0, etc.)
|
75
|
+
DSPy.configure do |c|
|
76
|
+
c.lm = DSPy::LM.new('gemini/gemini-2.5-flash',
|
77
|
+
api_key: ENV['GEMINI_API_KEY'],
|
78
|
+
structured_outputs: true) # Native structured outputs
|
79
|
+
end
|
80
|
+
|
81
|
+
# Anthropic Claude (Claude 3.5, Claude 4, etc.)
|
82
|
+
DSPy.configure do |c|
|
83
|
+
c.lm = DSPy::LM.new('anthropic/claude-sonnet-4-5-20250929',
|
84
|
+
api_key: ENV['ANTHROPIC_API_KEY'],
|
85
|
+
structured_outputs: true) # Tool-based extraction (default)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Ollama - Run any local model (Llama, Mistral, Gemma, etc.)
|
89
|
+
DSPy.configure do |c|
|
90
|
+
c.lm = DSPy::LM.new('ollama/llama3.2') # Free, runs locally, no API key needed
|
72
91
|
end
|
73
92
|
|
74
|
-
#
|
93
|
+
# OpenRouter - Access to 200+ models from multiple providers
|
75
94
|
DSPy.configure do |c|
|
76
|
-
c.lm = DSPy::LM.new('
|
77
|
-
api_key: ENV['
|
95
|
+
c.lm = DSPy::LM.new('openrouter/deepseek/deepseek-chat-v3.1:free',
|
96
|
+
api_key: ENV['OPENROUTER_API_KEY'])
|
78
97
|
end
|
79
98
|
```
|
80
99
|
|
@@ -13,7 +13,7 @@ module DSPy
|
|
13
13
|
'openrouter' => 'OpenrouterAdapter'
|
14
14
|
}.freeze
|
15
15
|
|
16
|
-
PROVIDERS_WITH_EXTRA_OPTIONS = %w[openai ollama gemini openrouter].freeze
|
16
|
+
PROVIDERS_WITH_EXTRA_OPTIONS = %w[openai anthropic ollama gemini openrouter].freeze
|
17
17
|
|
18
18
|
class << self
|
19
19
|
# Creates an adapter instance based on model_id
|
@@ -6,10 +6,11 @@ require_relative '../vision_models'
|
|
6
6
|
module DSPy
|
7
7
|
class LM
|
8
8
|
class AnthropicAdapter < Adapter
|
9
|
-
def initialize(model:, api_key:)
|
10
|
-
super
|
9
|
+
def initialize(model:, api_key:, structured_outputs: true)
|
10
|
+
super(model: model, api_key: api_key)
|
11
11
|
validate_api_key!(api_key, 'anthropic')
|
12
12
|
@client = Anthropic::Client.new(api_key: api_key)
|
13
|
+
@structured_outputs_enabled = structured_outputs
|
13
14
|
end
|
14
15
|
|
15
16
|
def chat(messages:, signature: nil, **extra_params, &block)
|
@@ -11,29 +11,32 @@ module DSPy
|
|
11
11
|
extend T::Sig
|
12
12
|
|
13
13
|
# Models that support structured outputs (JSON + Schema)
|
14
|
-
# Based on official Google documentation
|
14
|
+
# Based on official Google documentation: https://ai.google.dev/gemini-api/docs/models/gemini
|
15
|
+
# Last updated: Oct 2025
|
16
|
+
# Note: Gemini 1.5 series deprecated Oct 2025
|
15
17
|
STRUCTURED_OUTPUT_MODELS = T.let([
|
16
|
-
# Gemini 1.5 series
|
17
|
-
"gemini-1.5-pro",
|
18
|
-
"gemini-1.5-pro-preview-0514",
|
19
|
-
"gemini-1.5-pro-preview-0409",
|
20
|
-
"gemini-1.5-flash", # ✅ Now supports structured outputs
|
21
|
-
"gemini-1.5-flash-8b",
|
22
18
|
# Gemini 2.0 series
|
23
19
|
"gemini-2.0-flash",
|
24
|
-
"gemini-2.0-flash-
|
25
|
-
# Gemini 2.5 series
|
20
|
+
"gemini-2.0-flash-lite",
|
21
|
+
# Gemini 2.5 series (current)
|
26
22
|
"gemini-2.5-pro",
|
27
|
-
"gemini-2.5-flash",
|
28
|
-
"gemini-2.5-flash-lite"
|
23
|
+
"gemini-2.5-flash",
|
24
|
+
"gemini-2.5-flash-lite",
|
25
|
+
"gemini-2.5-flash-image"
|
29
26
|
].freeze, T::Array[String])
|
30
27
|
|
31
|
-
# Models that do not support structured outputs
|
28
|
+
# Models that do not support structured outputs or are deprecated
|
32
29
|
UNSUPPORTED_MODELS = T.let([
|
33
|
-
# Legacy Gemini 1.0 series
|
34
|
-
"gemini-pro",
|
30
|
+
# Legacy Gemini 1.0 series
|
31
|
+
"gemini-pro",
|
35
32
|
"gemini-1.0-pro-002",
|
36
|
-
"gemini-1.0-pro"
|
33
|
+
"gemini-1.0-pro",
|
34
|
+
# Deprecated Gemini 1.5 series (removed Oct 2025)
|
35
|
+
"gemini-1.5-pro",
|
36
|
+
"gemini-1.5-pro-preview-0514",
|
37
|
+
"gemini-1.5-pro-preview-0409",
|
38
|
+
"gemini-1.5-flash",
|
39
|
+
"gemini-1.5-flash-8b"
|
37
40
|
].freeze, T::Array[String])
|
38
41
|
|
39
42
|
sig { params(signature_class: T.class_of(DSPy::Signature)).returns(T::Hash[Symbol, T.untyped]) }
|
@@ -111,7 +114,13 @@ module DSPy
|
|
111
114
|
case property_schema[:type]
|
112
115
|
when "string"
|
113
116
|
result = { type: "string" }
|
114
|
-
|
117
|
+
# Gemini responseJsonSchema doesn't support const, so convert to single-value enum
|
118
|
+
# See: https://ai.google.dev/api/generate-content#FIELDS.response_json_schema
|
119
|
+
if property_schema[:const]
|
120
|
+
result[:enum] = [property_schema[:const]]
|
121
|
+
elsif property_schema[:enum]
|
122
|
+
result[:enum] = property_schema[:enum]
|
123
|
+
end
|
115
124
|
result
|
116
125
|
when "integer"
|
117
126
|
{ type: "integer" }
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sorbet-runtime"
|
4
|
+
|
5
|
+
module DSPy
|
6
|
+
class LM
|
7
|
+
# Simple chat strategy that passes messages through without JSON extraction
|
8
|
+
class ChatStrategy
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { params(adapter: T.untyped).void }
|
12
|
+
def initialize(adapter)
|
13
|
+
@adapter = adapter
|
14
|
+
end
|
15
|
+
|
16
|
+
# No modifications to messages for simple chat
|
17
|
+
sig { params(messages: T::Array[T::Hash[Symbol, T.untyped]], request_params: T::Hash[Symbol, T.untyped]).void }
|
18
|
+
def prepare_request(messages, request_params)
|
19
|
+
# Pass through unchanged
|
20
|
+
end
|
21
|
+
|
22
|
+
# No JSON extraction for chat
|
23
|
+
sig { params(response: DSPy::LM::Response).returns(NilClass) }
|
24
|
+
def extract_json(response)
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { returns(String) }
|
29
|
+
def name
|
30
|
+
'chat'
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
attr_reader :adapter
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sorbet-runtime"
|
4
|
+
require_relative "adapters/openai/schema_converter"
|
5
|
+
require_relative "adapters/gemini/schema_converter"
|
6
|
+
|
7
|
+
module DSPy
|
8
|
+
class LM
|
9
|
+
# JSON extraction strategy with provider-specific handling
|
10
|
+
class JSONStrategy
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
sig { params(adapter: T.untyped, signature_class: T.class_of(DSPy::Signature)).void }
|
14
|
+
def initialize(adapter, signature_class)
|
15
|
+
@adapter = adapter
|
16
|
+
@signature_class = signature_class
|
17
|
+
end
|
18
|
+
|
19
|
+
# Prepare request with provider-specific JSON extraction parameters
|
20
|
+
sig { params(messages: T::Array[T::Hash[Symbol, T.untyped]], request_params: T::Hash[Symbol, T.untyped]).void }
|
21
|
+
def prepare_request(messages, request_params)
|
22
|
+
adapter_class_name = adapter.class.name
|
23
|
+
|
24
|
+
if adapter_class_name.include?('OpenAIAdapter') || adapter_class_name.include?('OllamaAdapter')
|
25
|
+
prepare_openai_request(request_params)
|
26
|
+
elsif adapter_class_name.include?('AnthropicAdapter')
|
27
|
+
prepare_anthropic_request(messages, request_params)
|
28
|
+
elsif adapter_class_name.include?('GeminiAdapter')
|
29
|
+
prepare_gemini_request(request_params)
|
30
|
+
end
|
31
|
+
# Unknown provider - no special handling
|
32
|
+
end
|
33
|
+
|
34
|
+
# Extract JSON from response based on provider
|
35
|
+
sig { params(response: DSPy::LM::Response).returns(T.nilable(String)) }
|
36
|
+
def extract_json(response)
|
37
|
+
adapter_class_name = adapter.class.name
|
38
|
+
|
39
|
+
if adapter_class_name.include?('OpenAIAdapter') || adapter_class_name.include?('OllamaAdapter')
|
40
|
+
# OpenAI/Ollama: try to extract JSON from various formats
|
41
|
+
extract_json_from_content(response.content)
|
42
|
+
elsif adapter_class_name.include?('AnthropicAdapter')
|
43
|
+
# Anthropic: try tool use first if structured_outputs enabled, else use content extraction
|
44
|
+
structured_outputs_enabled = adapter.instance_variable_get(:@structured_outputs_enabled)
|
45
|
+
structured_outputs_enabled = true if structured_outputs_enabled.nil? # Default to true
|
46
|
+
|
47
|
+
if structured_outputs_enabled
|
48
|
+
extracted = extract_anthropic_tool_json(response)
|
49
|
+
extracted || extract_json_from_content(response.content)
|
50
|
+
else
|
51
|
+
# Skip tool extraction, use enhanced prompting extraction
|
52
|
+
extract_json_from_content(response.content)
|
53
|
+
end
|
54
|
+
elsif adapter_class_name.include?('GeminiAdapter')
|
55
|
+
# Gemini: try to extract JSON from various formats
|
56
|
+
extract_json_from_content(response.content)
|
57
|
+
else
|
58
|
+
# Unknown provider: try to extract JSON
|
59
|
+
extract_json_from_content(response.content)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
sig { returns(String) }
|
64
|
+
def name
|
65
|
+
'json'
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
attr_reader :adapter, :signature_class
|
71
|
+
|
72
|
+
# OpenAI/Ollama preparation
|
73
|
+
sig { params(request_params: T::Hash[Symbol, T.untyped]).void }
|
74
|
+
def prepare_openai_request(request_params)
|
75
|
+
# Check if structured outputs are supported
|
76
|
+
if adapter.instance_variable_get(:@structured_outputs_enabled) &&
|
77
|
+
DSPy::LM::Adapters::OpenAI::SchemaConverter.supports_structured_outputs?(adapter.model)
|
78
|
+
response_format = DSPy::LM::Adapters::OpenAI::SchemaConverter.to_openai_format(signature_class)
|
79
|
+
request_params[:response_format] = response_format
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Anthropic preparation
|
84
|
+
sig { params(messages: T::Array[T::Hash[Symbol, T.untyped]], request_params: T::Hash[Symbol, T.untyped]).void }
|
85
|
+
def prepare_anthropic_request(messages, request_params)
|
86
|
+
# Only use tool-based extraction if structured_outputs is enabled (default: true)
|
87
|
+
structured_outputs_enabled = adapter.instance_variable_get(:@structured_outputs_enabled)
|
88
|
+
|
89
|
+
# Default to true if not set (backward compatibility)
|
90
|
+
structured_outputs_enabled = true if structured_outputs_enabled.nil?
|
91
|
+
|
92
|
+
return unless structured_outputs_enabled
|
93
|
+
|
94
|
+
# Convert signature to tool schema
|
95
|
+
tool_schema = convert_to_anthropic_tool_schema
|
96
|
+
|
97
|
+
# Add tool definition
|
98
|
+
request_params[:tools] = [tool_schema]
|
99
|
+
|
100
|
+
# Force tool use
|
101
|
+
request_params[:tool_choice] = {
|
102
|
+
type: "tool",
|
103
|
+
name: "json_output"
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
# Gemini preparation
|
108
|
+
sig { params(request_params: T::Hash[Symbol, T.untyped]).void }
|
109
|
+
def prepare_gemini_request(request_params)
|
110
|
+
# Check if structured outputs are supported
|
111
|
+
if adapter.instance_variable_get(:@structured_outputs_enabled) &&
|
112
|
+
DSPy::LM::Adapters::Gemini::SchemaConverter.supports_structured_outputs?(adapter.model)
|
113
|
+
schema = DSPy::LM::Adapters::Gemini::SchemaConverter.to_gemini_format(signature_class)
|
114
|
+
|
115
|
+
request_params[:generation_config] = {
|
116
|
+
response_mime_type: "application/json",
|
117
|
+
response_json_schema: schema
|
118
|
+
}
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Convert signature to Anthropic tool schema
|
123
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
124
|
+
def convert_to_anthropic_tool_schema
|
125
|
+
output_fields = signature_class.output_field_descriptors
|
126
|
+
|
127
|
+
{
|
128
|
+
name: "json_output",
|
129
|
+
description: "Output the result in the required JSON format",
|
130
|
+
input_schema: {
|
131
|
+
type: "object",
|
132
|
+
properties: build_properties_from_fields(output_fields),
|
133
|
+
required: output_fields.keys.map(&:to_s)
|
134
|
+
}
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
138
|
+
# Build JSON schema properties from output fields
|
139
|
+
sig { params(fields: T::Hash[Symbol, T.untyped]).returns(T::Hash[String, T.untyped]) }
|
140
|
+
def build_properties_from_fields(fields)
|
141
|
+
properties = {}
|
142
|
+
fields.each do |field_name, descriptor|
|
143
|
+
properties[field_name.to_s] = DSPy::TypeSystem::SorbetJsonSchema.type_to_json_schema(descriptor.type)
|
144
|
+
end
|
145
|
+
properties
|
146
|
+
end
|
147
|
+
|
148
|
+
# Extract JSON from Anthropic tool use response
|
149
|
+
sig { params(response: DSPy::LM::Response).returns(T.nilable(String)) }
|
150
|
+
def extract_anthropic_tool_json(response)
|
151
|
+
# Check for tool calls in metadata
|
152
|
+
if response.metadata.respond_to?(:tool_calls) && response.metadata.tool_calls
|
153
|
+
tool_calls = response.metadata.tool_calls
|
154
|
+
if tool_calls.is_a?(Array) && !tool_calls.empty?
|
155
|
+
first_call = tool_calls.first
|
156
|
+
if first_call[:name] == "json_output" && first_call[:input]
|
157
|
+
return JSON.generate(first_call[:input])
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
nil
|
163
|
+
end
|
164
|
+
|
165
|
+
# Extract JSON from content that may contain markdown or plain JSON
|
166
|
+
sig { params(content: String).returns(String) }
|
167
|
+
def extract_json_from_content(content)
|
168
|
+
return content if content.nil? || content.empty?
|
169
|
+
|
170
|
+
# Try 1: Check for ```json code block (with or without preceding text)
|
171
|
+
if content.include?('```json')
|
172
|
+
json_match = content.match(/```json\s*\n(.*?)\n```/m)
|
173
|
+
return json_match[1].strip if json_match
|
174
|
+
end
|
175
|
+
|
176
|
+
# Try 2: Check for generic ``` code block
|
177
|
+
if content.include?('```')
|
178
|
+
code_match = content.match(/```\s*\n(.*?)\n```/m)
|
179
|
+
if code_match
|
180
|
+
potential_json = code_match[1].strip
|
181
|
+
# Verify it's JSON
|
182
|
+
begin
|
183
|
+
JSON.parse(potential_json)
|
184
|
+
return potential_json
|
185
|
+
rescue JSON::ParserError
|
186
|
+
# Not valid JSON, continue
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Try 3: Try parsing entire content as JSON
|
192
|
+
begin
|
193
|
+
JSON.parse(content)
|
194
|
+
return content
|
195
|
+
rescue JSON::ParserError
|
196
|
+
# Not pure JSON, try extracting
|
197
|
+
end
|
198
|
+
|
199
|
+
# Try 4: Look for JSON object pattern in text (greedy match for nested objects)
|
200
|
+
json_pattern = /\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\}/m
|
201
|
+
json_match = content.match(json_pattern)
|
202
|
+
if json_match
|
203
|
+
potential_json = json_match[0]
|
204
|
+
begin
|
205
|
+
JSON.parse(potential_json)
|
206
|
+
return potential_json
|
207
|
+
rescue JSON::ParserError
|
208
|
+
# Not valid JSON
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Return content as-is if no JSON found
|
213
|
+
content
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
data/lib/dspy/lm.rb
CHANGED
@@ -20,12 +20,13 @@ require_relative 'lm/adapters/gemini_adapter'
|
|
20
20
|
require_relative 'lm/adapters/openrouter_adapter'
|
21
21
|
|
22
22
|
# Load strategy system
|
23
|
-
require_relative 'lm/
|
24
|
-
require_relative 'lm/
|
23
|
+
require_relative 'lm/chat_strategy'
|
24
|
+
require_relative 'lm/json_strategy'
|
25
25
|
|
26
26
|
# Load message builder and message types
|
27
27
|
require_relative 'lm/message'
|
28
28
|
require_relative 'lm/message_builder'
|
29
|
+
require_relative 'structured_outputs_prompt'
|
29
30
|
|
30
31
|
module DSPy
|
31
32
|
class LM
|
@@ -64,7 +65,10 @@ module DSPy
|
|
64
65
|
response = instrument_lm_request(messages, signature_class.name) do
|
65
66
|
chat_with_strategy(messages, signature_class, &block)
|
66
67
|
end
|
67
|
-
|
68
|
+
|
69
|
+
# Emit the standard lm.tokens event (consistent with raw_chat)
|
70
|
+
emit_token_usage(response, signature_class.name)
|
71
|
+
|
68
72
|
# Parse response (no longer needs separate instrumentation)
|
69
73
|
parsed_result = parse_response(response, input_values, signature_class)
|
70
74
|
|
@@ -96,21 +100,15 @@ module DSPy
|
|
96
100
|
private
|
97
101
|
|
98
102
|
def chat_with_strategy(messages, signature_class, &block)
|
99
|
-
#
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
if DSPy.config.structured_outputs.retry_enabled && signature_class
|
104
|
-
# Use retry handler for JSON responses
|
105
|
-
retry_handler = RetryHandler.new(adapter, signature_class)
|
106
|
-
|
107
|
-
retry_handler.with_retry(initial_strategy) do |strategy|
|
108
|
-
execute_chat_with_strategy(messages, signature_class, strategy, &block)
|
109
|
-
end
|
103
|
+
# Choose strategy based on whether we need JSON extraction
|
104
|
+
strategy = if signature_class
|
105
|
+
JSONStrategy.new(adapter, signature_class)
|
110
106
|
else
|
111
|
-
|
112
|
-
execute_chat_with_strategy(messages, signature_class, initial_strategy, &block)
|
107
|
+
ChatStrategy.new(adapter)
|
113
108
|
end
|
109
|
+
|
110
|
+
# Execute with the selected strategy (no retry, no fallback)
|
111
|
+
execute_chat_with_strategy(messages, signature_class, strategy, &block)
|
114
112
|
end
|
115
113
|
|
116
114
|
def execute_chat_with_strategy(messages, signature_class, strategy, &block)
|
@@ -179,26 +177,53 @@ module DSPy
|
|
179
177
|
|
180
178
|
def build_messages(inference_module, input_values)
|
181
179
|
messages = []
|
182
|
-
|
180
|
+
|
181
|
+
# Determine if structured outputs will be used and wrap prompt if so
|
182
|
+
base_prompt = inference_module.prompt
|
183
|
+
prompt = if will_use_structured_outputs?(inference_module.signature_class)
|
184
|
+
StructuredOutputsPrompt.new(**base_prompt.to_h)
|
185
|
+
else
|
186
|
+
base_prompt
|
187
|
+
end
|
188
|
+
|
183
189
|
# Add system message
|
184
|
-
system_prompt =
|
190
|
+
system_prompt = prompt.render_system_prompt
|
185
191
|
if system_prompt
|
186
192
|
messages << Message.new(
|
187
193
|
role: Message::Role::System,
|
188
194
|
content: system_prompt
|
189
195
|
)
|
190
196
|
end
|
191
|
-
|
197
|
+
|
192
198
|
# Add user message
|
193
|
-
user_prompt =
|
199
|
+
user_prompt = prompt.render_user_prompt(input_values)
|
194
200
|
messages << Message.new(
|
195
201
|
role: Message::Role::User,
|
196
202
|
content: user_prompt
|
197
203
|
)
|
198
|
-
|
204
|
+
|
199
205
|
messages
|
200
206
|
end
|
201
207
|
|
208
|
+
def will_use_structured_outputs?(signature_class)
|
209
|
+
return false unless signature_class
|
210
|
+
|
211
|
+
adapter_class_name = adapter.class.name
|
212
|
+
|
213
|
+
if adapter_class_name.include?('OpenAIAdapter') || adapter_class_name.include?('OllamaAdapter')
|
214
|
+
adapter.instance_variable_get(:@structured_outputs_enabled) &&
|
215
|
+
DSPy::LM::Adapters::OpenAI::SchemaConverter.supports_structured_outputs?(adapter.model)
|
216
|
+
elsif adapter_class_name.include?('GeminiAdapter')
|
217
|
+
adapter.instance_variable_get(:@structured_outputs_enabled) &&
|
218
|
+
DSPy::LM::Adapters::Gemini::SchemaConverter.supports_structured_outputs?(adapter.model)
|
219
|
+
elsif adapter_class_name.include?('AnthropicAdapter')
|
220
|
+
structured_outputs_enabled = adapter.instance_variable_get(:@structured_outputs_enabled)
|
221
|
+
structured_outputs_enabled.nil? ? true : structured_outputs_enabled
|
222
|
+
else
|
223
|
+
false
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
202
227
|
def parse_response(response, input_values, signature_class)
|
203
228
|
# Try to parse the response as JSON
|
204
229
|
content = response.content
|
@@ -208,26 +208,26 @@ module DSPy
|
|
208
208
|
sig { params(value: T.untyped, union_type: T.untyped).returns(T.untyped) }
|
209
209
|
def coerce_union_value(value, union_type)
|
210
210
|
return value unless value.is_a?(Hash)
|
211
|
-
|
211
|
+
|
212
212
|
# Check for _type discriminator field
|
213
213
|
type_name = value[:_type] || value["_type"]
|
214
214
|
return value unless type_name
|
215
|
-
|
215
|
+
|
216
216
|
# Find matching struct type in the union
|
217
217
|
union_type.types.each do |type|
|
218
218
|
next if type == T::Utils.coerce(NilClass)
|
219
|
-
|
219
|
+
|
220
220
|
if type.is_a?(T::Types::Simple) && type.raw_type < T::Struct
|
221
221
|
struct_name = type.raw_type.name.split('::').last
|
222
222
|
if struct_name == type_name
|
223
223
|
# Convert string keys to symbols and remove _type
|
224
224
|
symbolized_hash = value.transform_keys(&:to_sym)
|
225
225
|
symbolized_hash.delete(:_type)
|
226
|
-
|
226
|
+
|
227
227
|
# Coerce struct field values based on their types
|
228
228
|
struct_class = type.raw_type
|
229
229
|
struct_props = struct_class.props
|
230
|
-
|
230
|
+
|
231
231
|
# ONLY include fields that exist in the struct
|
232
232
|
coerced_hash = {}
|
233
233
|
struct_props.each_key do |key|
|
@@ -236,13 +236,13 @@ module DSPy
|
|
236
236
|
coerced_hash[key] = coerce_value_to_type(symbolized_hash[key], prop_type)
|
237
237
|
end
|
238
238
|
end
|
239
|
-
|
239
|
+
|
240
240
|
# Create the struct instance with coerced values
|
241
241
|
return struct_class.new(**coerced_hash)
|
242
242
|
end
|
243
243
|
end
|
244
244
|
end
|
245
|
-
|
245
|
+
|
246
246
|
# If no matching type found, return original value
|
247
247
|
value
|
248
248
|
rescue ArgumentError => e
|