dspy 0.27.6 → 0.28.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 +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/chat_strategy.rb +38 -0
- data/lib/dspy/lm/json_strategy.rb +222 -0
- data/lib/dspy/lm.rb +13 -16
- data/lib/dspy/re_act.rb +253 -68
- data/lib/dspy/tools/base.rb +5 -7
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +0 -8
- metadata +4 -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: df0e7cf901df85567e1553a3d7df8659a71f254fa5671803ea98f0573de0c39a
|
4
|
+
data.tar.gz: b2d5d37cb05678f97143a1463410d92848ba10178bd50d60ee384827e9efe9b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0d870f2d338fcdce0540143decd3674e94a9c24e5fad796536772e6c8064af75dcafc36533bf39a0a1eeefac30777de1d6541a3ab57eb2568a007260d7a5d7a3
|
7
|
+
data.tar.gz: 13f92278a81ca870f90b662fa40972e41aa6203d27a8792d3d03c13b97d971d7684a0f661dfb86bf03ef5c58e4a8f2f57c2ac48414ed9b8f8e5f0bce7628f231
|
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)
|
@@ -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,222 @@
|
|
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
|
+
|
106
|
+
# Update last user message
|
107
|
+
if messages.any? && messages.last[:role] == "user"
|
108
|
+
messages.last[:content] += "\n\nPlease use the json_output tool to provide your response."
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Gemini preparation
|
113
|
+
sig { params(request_params: T::Hash[Symbol, T.untyped]).void }
|
114
|
+
def prepare_gemini_request(request_params)
|
115
|
+
# Check if structured outputs are supported
|
116
|
+
if adapter.instance_variable_get(:@structured_outputs_enabled) &&
|
117
|
+
DSPy::LM::Adapters::Gemini::SchemaConverter.supports_structured_outputs?(adapter.model)
|
118
|
+
schema = DSPy::LM::Adapters::Gemini::SchemaConverter.to_gemini_format(signature_class)
|
119
|
+
|
120
|
+
request_params[:generation_config] = {
|
121
|
+
response_mime_type: "application/json",
|
122
|
+
response_json_schema: schema
|
123
|
+
}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Convert signature to Anthropic tool schema
|
128
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
129
|
+
def convert_to_anthropic_tool_schema
|
130
|
+
output_fields = signature_class.output_field_descriptors
|
131
|
+
|
132
|
+
{
|
133
|
+
name: "json_output",
|
134
|
+
description: "Output the result in the required JSON format",
|
135
|
+
input_schema: {
|
136
|
+
type: "object",
|
137
|
+
properties: build_properties_from_fields(output_fields),
|
138
|
+
required: output_fields.keys.map(&:to_s)
|
139
|
+
}
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
143
|
+
# Build JSON schema properties from output fields
|
144
|
+
sig { params(fields: T::Hash[Symbol, T.untyped]).returns(T::Hash[String, T.untyped]) }
|
145
|
+
def build_properties_from_fields(fields)
|
146
|
+
properties = {}
|
147
|
+
fields.each do |field_name, descriptor|
|
148
|
+
properties[field_name.to_s] = DSPy::TypeSystem::SorbetJsonSchema.type_to_json_schema(descriptor.type)
|
149
|
+
end
|
150
|
+
properties
|
151
|
+
end
|
152
|
+
|
153
|
+
# Extract JSON from Anthropic tool use response
|
154
|
+
sig { params(response: DSPy::LM::Response).returns(T.nilable(String)) }
|
155
|
+
def extract_anthropic_tool_json(response)
|
156
|
+
# Check for tool calls in metadata
|
157
|
+
if response.metadata.respond_to?(:tool_calls) && response.metadata.tool_calls
|
158
|
+
tool_calls = response.metadata.tool_calls
|
159
|
+
if tool_calls.is_a?(Array) && !tool_calls.empty?
|
160
|
+
first_call = tool_calls.first
|
161
|
+
if first_call[:name] == "json_output" && first_call[:input]
|
162
|
+
return JSON.generate(first_call[:input])
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
nil
|
168
|
+
end
|
169
|
+
|
170
|
+
# Extract JSON from content that may contain markdown or plain JSON
|
171
|
+
sig { params(content: String).returns(String) }
|
172
|
+
def extract_json_from_content(content)
|
173
|
+
return content if content.nil? || content.empty?
|
174
|
+
|
175
|
+
# Try 1: Check for ```json code block (with or without preceding text)
|
176
|
+
if content.include?('```json')
|
177
|
+
json_match = content.match(/```json\s*\n(.*?)\n```/m)
|
178
|
+
return json_match[1].strip if json_match
|
179
|
+
end
|
180
|
+
|
181
|
+
# Try 2: Check for generic ``` code block
|
182
|
+
if content.include?('```')
|
183
|
+
code_match = content.match(/```\s*\n(.*?)\n```/m)
|
184
|
+
if code_match
|
185
|
+
potential_json = code_match[1].strip
|
186
|
+
# Verify it's JSON
|
187
|
+
begin
|
188
|
+
JSON.parse(potential_json)
|
189
|
+
return potential_json
|
190
|
+
rescue JSON::ParserError
|
191
|
+
# Not valid JSON, continue
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Try 3: Try parsing entire content as JSON
|
197
|
+
begin
|
198
|
+
JSON.parse(content)
|
199
|
+
return content
|
200
|
+
rescue JSON::ParserError
|
201
|
+
# Not pure JSON, try extracting
|
202
|
+
end
|
203
|
+
|
204
|
+
# Try 4: Look for JSON object pattern in text (greedy match for nested objects)
|
205
|
+
json_pattern = /\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\}/m
|
206
|
+
json_match = content.match(json_pattern)
|
207
|
+
if json_match
|
208
|
+
potential_json = json_match[0]
|
209
|
+
begin
|
210
|
+
JSON.parse(potential_json)
|
211
|
+
return potential_json
|
212
|
+
rescue JSON::ParserError
|
213
|
+
# Not valid JSON
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Return content as-is if no JSON found
|
218
|
+
content
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
data/lib/dspy/lm.rb
CHANGED
@@ -20,8 +20,8 @@ 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'
|
@@ -64,7 +64,10 @@ module DSPy
|
|
64
64
|
response = instrument_lm_request(messages, signature_class.name) do
|
65
65
|
chat_with_strategy(messages, signature_class, &block)
|
66
66
|
end
|
67
|
-
|
67
|
+
|
68
|
+
# Emit the standard lm.tokens event (consistent with raw_chat)
|
69
|
+
emit_token_usage(response, signature_class.name)
|
70
|
+
|
68
71
|
# Parse response (no longer needs separate instrumentation)
|
69
72
|
parsed_result = parse_response(response, input_values, signature_class)
|
70
73
|
|
@@ -96,21 +99,15 @@ module DSPy
|
|
96
99
|
private
|
97
100
|
|
98
101
|
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
|
102
|
+
# Choose strategy based on whether we need JSON extraction
|
103
|
+
strategy = if signature_class
|
104
|
+
JSONStrategy.new(adapter, signature_class)
|
110
105
|
else
|
111
|
-
|
112
|
-
execute_chat_with_strategy(messages, signature_class, initial_strategy, &block)
|
106
|
+
ChatStrategy.new(adapter)
|
113
107
|
end
|
108
|
+
|
109
|
+
# Execute with the selected strategy (no retry, no fallback)
|
110
|
+
execute_chat_with_strategy(messages, signature_class, strategy, &block)
|
114
111
|
end
|
115
112
|
|
116
113
|
def execute_chat_with_strategy(messages, signature_class, strategy, &block)
|