dspy 0.13.0 → 0.14.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 +9 -3
- data/lib/dspy/lm/adapters/anthropic_adapter.rb +38 -10
- data/lib/dspy/lm/strategies/anthropic_tool_use_strategy.rb +192 -0
- data/lib/dspy/lm/strategy_selector.rb +7 -1
- data/lib/dspy/lm/structured_output_strategy.rb +1 -0
- data/lib/dspy/lm.rb +1 -1
- data/lib/dspy/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 92f074914faabe00390399080ef0efecca6ca6dbfeeca38201c127cfbef11528
|
4
|
+
data.tar.gz: 91c07b99351fd1095f5e61e1b8c9560c1c67ee67c97c945437f4a2964172eaca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 303990f2586f73beaf638ed85b1e5266a318f760aae0342af4738652078b0087e7d6826588d80fb4d26b3b9ca026dfc1b3f3e6645e3b24213d0cf94b1fbb450f
|
7
|
+
data.tar.gz: db445cef172a2009d565b659fd4170895045dacbbbd9073b0d20ab8c054e2085ed62cfb8d1dbf6b98e203422cc999a495fb2e84ba3363d31360472b10f1487e9
|
data/README.md
CHANGED
@@ -46,7 +46,7 @@ The result? LLM applications that actually scale and don't break when you sneeze
|
|
46
46
|
|
47
47
|
## Development Status
|
48
48
|
|
49
|
-
DSPy.rb is actively developed and approaching stability at **v0.
|
49
|
+
DSPy.rb is actively developed and approaching stability at **v0.13.0**. The core framework is production-ready with comprehensive documentation, but I'm battle-testing features through the 0.x series before committing to a stable v1.0 API.
|
50
50
|
|
51
51
|
Real-world usage feedback is invaluable - if you encounter issues or have suggestions, please open a GitHub issue!
|
52
52
|
|
@@ -55,7 +55,7 @@ Real-world usage feedback is invaluable - if you encounter issues or have sugges
|
|
55
55
|
### Installation
|
56
56
|
|
57
57
|
```ruby
|
58
|
-
gem 'dspy', '~> 0.
|
58
|
+
gem 'dspy', '~> 0.13'
|
59
59
|
```
|
60
60
|
|
61
61
|
Or add to your Gemfile:
|
@@ -138,6 +138,12 @@ puts result.confidence # => 0.85
|
|
138
138
|
|
139
139
|
📖 **[Complete Documentation Website](https://vicentereig.github.io/dspy.rb/)**
|
140
140
|
|
141
|
+
### LLM-Friendly Documentation
|
142
|
+
|
143
|
+
For LLMs and AI assistants working with DSPy.rb:
|
144
|
+
- **[llms.txt](https://vicentereig.github.io/dspy.rb/llms.txt)** - Concise reference optimized for LLMs
|
145
|
+
- **[llms-full.txt](https://vicentereig.github.io/dspy.rb/llms-full.txt)** - Comprehensive API documentation
|
146
|
+
|
141
147
|
### Getting Started
|
142
148
|
- **[Installation & Setup](docs/src/getting-started/installation.md)** - Detailed installation and configuration
|
143
149
|
- **[Quick Start Guide](docs/src/getting-started/quick-start.md)** - Your first DSPy programs
|
@@ -177,7 +183,7 @@ DSPy.rb has rapidly evolved from experimental to production-ready:
|
|
177
183
|
|
178
184
|
## Roadmap - Battle-Testing Toward v1.0
|
179
185
|
|
180
|
-
DSPy.rb is currently at **v0.
|
186
|
+
DSPy.rb is currently at **v0.13.0** and approaching stability. I'm focusing on real-world usage and refinement through the 0.14, 0.15+ series before committing to a stable v1.0 API.
|
181
187
|
|
182
188
|
**Current Focus Areas:**
|
183
189
|
- 🚧 **Ollama Support** - Local model integration
|
@@ -15,15 +15,20 @@ module DSPy
|
|
15
15
|
# Anthropic requires system message to be separate from messages
|
16
16
|
system_message, user_messages = extract_system_message(normalize_messages(messages))
|
17
17
|
|
18
|
-
#
|
19
|
-
|
18
|
+
# Check if this is a tool use request
|
19
|
+
has_tools = extra_params.key?(:tools) && !extra_params[:tools].empty?
|
20
|
+
|
21
|
+
# Apply JSON prefilling if needed for better Claude JSON compliance (but not for tool use)
|
22
|
+
unless has_tools
|
23
|
+
user_messages = prepare_messages_for_json(user_messages, system_message)
|
24
|
+
end
|
20
25
|
|
21
26
|
request_params = {
|
22
27
|
model: model,
|
23
28
|
messages: user_messages,
|
24
29
|
max_tokens: 4096, # Required for Anthropic
|
25
30
|
temperature: 0.0 # DSPy default for deterministic responses
|
26
|
-
}
|
31
|
+
}.merge(extra_params)
|
27
32
|
|
28
33
|
# Add system message if present
|
29
34
|
request_params[:system] = system_message if system_message
|
@@ -60,21 +65,44 @@ module DSPy
|
|
60
65
|
raise AdapterError, "Anthropic API error: #{response.error}"
|
61
66
|
end
|
62
67
|
|
63
|
-
|
68
|
+
# Handle both text content and tool use
|
69
|
+
content = ""
|
70
|
+
tool_calls = []
|
71
|
+
|
72
|
+
if response.content.is_a?(Array)
|
73
|
+
response.content.each do |content_block|
|
74
|
+
case content_block.type.to_s
|
75
|
+
when "text"
|
76
|
+
content += content_block.text
|
77
|
+
when "tool_use"
|
78
|
+
tool_calls << {
|
79
|
+
id: content_block.id,
|
80
|
+
name: content_block.name,
|
81
|
+
input: content_block.input
|
82
|
+
}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
64
87
|
usage = response.usage
|
65
88
|
|
66
89
|
# Convert usage data to typed struct
|
67
90
|
usage_struct = UsageFactory.create('anthropic', usage)
|
68
91
|
|
92
|
+
metadata = {
|
93
|
+
provider: 'anthropic',
|
94
|
+
model: model,
|
95
|
+
response_id: response.id,
|
96
|
+
role: response.role
|
97
|
+
}
|
98
|
+
|
99
|
+
# Add tool calls to metadata if present
|
100
|
+
metadata[:tool_calls] = tool_calls unless tool_calls.empty?
|
101
|
+
|
69
102
|
Response.new(
|
70
103
|
content: content,
|
71
104
|
usage: usage_struct,
|
72
|
-
metadata:
|
73
|
-
provider: 'anthropic',
|
74
|
-
model: model,
|
75
|
-
response_id: response.id,
|
76
|
-
role: response.role
|
77
|
-
}
|
105
|
+
metadata: metadata
|
78
106
|
)
|
79
107
|
end
|
80
108
|
rescue => e
|
@@ -0,0 +1,192 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sorbet-runtime"
|
4
|
+
|
5
|
+
module DSPy
|
6
|
+
class LM
|
7
|
+
module Strategies
|
8
|
+
# Strategy for using Anthropic's tool use feature for guaranteed JSON output
|
9
|
+
class AnthropicToolUseStrategy < BaseStrategy
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
sig { override.returns(T::Boolean) }
|
13
|
+
def available?
|
14
|
+
# Only available for Anthropic adapters with models that support tool use
|
15
|
+
adapter.is_a?(DSPy::LM::AnthropicAdapter) && supports_tool_use?
|
16
|
+
end
|
17
|
+
|
18
|
+
sig { override.returns(Integer) }
|
19
|
+
def priority
|
20
|
+
95 # Higher priority than extraction strategy - tool use is more reliable
|
21
|
+
end
|
22
|
+
|
23
|
+
sig { override.returns(String) }
|
24
|
+
def name
|
25
|
+
"anthropic_tool_use"
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { override.params(messages: T::Array[T::Hash[Symbol, String]], request_params: T::Hash[Symbol, T.untyped]).void }
|
29
|
+
def prepare_request(messages, request_params)
|
30
|
+
# Convert signature output schema to Anthropic tool format
|
31
|
+
tool_schema = convert_to_tool_schema
|
32
|
+
|
33
|
+
# Add the tool definition to request params
|
34
|
+
request_params[:tools] = [tool_schema]
|
35
|
+
|
36
|
+
# Force the model to use our tool
|
37
|
+
request_params[:tool_choice] = {
|
38
|
+
type: "tool",
|
39
|
+
name: "json_output"
|
40
|
+
}
|
41
|
+
|
42
|
+
# Update the last user message to request tool use
|
43
|
+
if messages.any? && messages.last[:role] == "user"
|
44
|
+
messages.last[:content] += "\n\nPlease use the json_output tool to provide your response."
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { override.params(response: DSPy::LM::Response).returns(T.nilable(String)) }
|
49
|
+
def extract_json(response)
|
50
|
+
# Extract JSON from tool use response
|
51
|
+
begin
|
52
|
+
# Check for tool calls in metadata first (this is the primary method)
|
53
|
+
if response.metadata && response.metadata[:tool_calls]
|
54
|
+
tool_calls = response.metadata[:tool_calls]
|
55
|
+
if tool_calls.is_a?(Array) && !tool_calls.empty?
|
56
|
+
first_call = tool_calls.first
|
57
|
+
if first_call[:name] == "json_output" && first_call[:input]
|
58
|
+
json_result = JSON.generate(first_call[:input])
|
59
|
+
return json_result
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Fallback: try to extract from content if it contains tool use blocks
|
65
|
+
content = response.content
|
66
|
+
if content && !content.empty? && content.include?("<tool_use>")
|
67
|
+
tool_content = content[/<tool_use>.*?<\/tool_use>/m]
|
68
|
+
if tool_content
|
69
|
+
json_match = tool_content[/<input>(.*?)<\/input>/m, 1]
|
70
|
+
return json_match.strip if json_match
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
nil
|
75
|
+
rescue => e
|
76
|
+
DSPy.logger.debug("Failed to extract tool use JSON: #{e.message}")
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
sig { override.params(error: StandardError).returns(T::Boolean) }
|
82
|
+
def handle_error(error)
|
83
|
+
# Tool use errors should trigger fallback to extraction strategy
|
84
|
+
if error.message.include?("tool") || error.message.include?("invalid_request_error")
|
85
|
+
DSPy.logger.warn("Anthropic tool use failed: #{error.message}")
|
86
|
+
true # We handled it, try next strategy
|
87
|
+
else
|
88
|
+
false # Let retry handler deal with it
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
sig { returns(T::Boolean) }
|
95
|
+
def supports_tool_use?
|
96
|
+
# Check if model supports tool use
|
97
|
+
# Claude 3 models (Opus, Sonnet, Haiku) support tool use
|
98
|
+
model = adapter.model.downcase
|
99
|
+
model.include?("claude-3") || model.include?("claude-3.5")
|
100
|
+
end
|
101
|
+
|
102
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
103
|
+
def convert_to_tool_schema
|
104
|
+
# Get output fields from signature
|
105
|
+
output_fields = signature_class.output_field_descriptors
|
106
|
+
|
107
|
+
# Convert to Anthropic tool format
|
108
|
+
{
|
109
|
+
name: "json_output",
|
110
|
+
description: "Output the result in the required JSON format",
|
111
|
+
input_schema: {
|
112
|
+
type: "object",
|
113
|
+
properties: build_properties_from_fields(output_fields),
|
114
|
+
required: output_fields.keys.map(&:to_s)
|
115
|
+
}
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
sig { params(fields: T::Hash[Symbol, T.untyped]).returns(T::Hash[String, T.untyped]) }
|
120
|
+
def build_properties_from_fields(fields)
|
121
|
+
properties = {}
|
122
|
+
|
123
|
+
fields.each do |field_name, descriptor|
|
124
|
+
properties[field_name.to_s] = convert_type_to_json_schema(descriptor.type)
|
125
|
+
end
|
126
|
+
|
127
|
+
properties
|
128
|
+
end
|
129
|
+
|
130
|
+
sig { params(type: T.untyped).returns(T::Hash[String, T.untyped]) }
|
131
|
+
def convert_type_to_json_schema(type)
|
132
|
+
# Handle raw Ruby class types - use === for class comparison
|
133
|
+
if type == String
|
134
|
+
return { type: "string" }
|
135
|
+
elsif type == Integer
|
136
|
+
return { type: "integer" }
|
137
|
+
elsif type == Float
|
138
|
+
return { type: "number" }
|
139
|
+
elsif type == TrueClass || type == FalseClass
|
140
|
+
return { type: "boolean" }
|
141
|
+
end
|
142
|
+
|
143
|
+
# Handle Sorbet types
|
144
|
+
case type
|
145
|
+
when T::Types::Simple
|
146
|
+
case type.raw_type.to_s
|
147
|
+
when "String"
|
148
|
+
{ type: "string" }
|
149
|
+
when "Integer"
|
150
|
+
{ type: "integer" }
|
151
|
+
when "Float", "Numeric"
|
152
|
+
{ type: "number" }
|
153
|
+
when "TrueClass", "FalseClass"
|
154
|
+
{ type: "boolean" }
|
155
|
+
else
|
156
|
+
{ type: "string" } # Default fallback
|
157
|
+
end
|
158
|
+
when T::Types::TypedArray
|
159
|
+
{
|
160
|
+
type: "array",
|
161
|
+
items: convert_type_to_json_schema(type.type)
|
162
|
+
}
|
163
|
+
when T::Types::TypedHash
|
164
|
+
{
|
165
|
+
type: "object",
|
166
|
+
additionalProperties: convert_type_to_json_schema(type.values)
|
167
|
+
}
|
168
|
+
else
|
169
|
+
# For complex types, try to introspect
|
170
|
+
if type.respond_to?(:props)
|
171
|
+
{
|
172
|
+
type: "object",
|
173
|
+
properties: build_properties_from_props(type.props)
|
174
|
+
}
|
175
|
+
else
|
176
|
+
{ type: "object" } # Generic object fallback
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
sig { params(props: T.untyped).returns(T::Hash[String, T.untyped]) }
|
182
|
+
def build_properties_from_props(props)
|
183
|
+
result = {}
|
184
|
+
props.each do |prop_name, prop_info|
|
185
|
+
result[prop_name.to_s] = convert_type_to_json_schema(prop_info[:type])
|
186
|
+
end
|
187
|
+
result
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require "sorbet-runtime"
|
4
4
|
require_relative "strategies/base_strategy"
|
5
5
|
require_relative "strategies/openai_structured_output_strategy"
|
6
|
+
require_relative "strategies/anthropic_tool_use_strategy"
|
6
7
|
require_relative "strategies/anthropic_extraction_strategy"
|
7
8
|
require_relative "strategies/enhanced_prompting_strategy"
|
8
9
|
|
@@ -15,6 +16,7 @@ module DSPy
|
|
15
16
|
# Available strategies in order of registration
|
16
17
|
STRATEGIES = [
|
17
18
|
Strategies::OpenAIStructuredOutputStrategy,
|
19
|
+
Strategies::AnthropicToolUseStrategy,
|
18
20
|
Strategies::AnthropicExtractionStrategy,
|
19
21
|
Strategies::EnhancedPromptingStrategy
|
20
22
|
].freeze
|
@@ -99,7 +101,11 @@ module DSPy
|
|
99
101
|
openai_strategy = find_strategy_by_name("openai_structured_output")
|
100
102
|
return openai_strategy if openai_strategy&.available?
|
101
103
|
|
102
|
-
# Try Anthropic
|
104
|
+
# Try Anthropic tool use first
|
105
|
+
anthropic_tool_strategy = find_strategy_by_name("anthropic_tool_use")
|
106
|
+
return anthropic_tool_strategy if anthropic_tool_strategy&.available?
|
107
|
+
|
108
|
+
# Fall back to Anthropic extraction
|
103
109
|
anthropic_strategy = find_strategy_by_name("anthropic_extraction")
|
104
110
|
return anthropic_strategy if anthropic_strategy&.available?
|
105
111
|
|
@@ -8,6 +8,7 @@ module DSPy
|
|
8
8
|
class StructuredOutputStrategy < T::Enum
|
9
9
|
enums do
|
10
10
|
OpenAIStructuredOutput = new("openai_structured_output")
|
11
|
+
AnthropicToolUse = new("anthropic_tool_use")
|
11
12
|
AnthropicExtraction = new("anthropic_extraction")
|
12
13
|
EnhancedPrompting = new("enhanced_prompting")
|
13
14
|
end
|
data/lib/dspy/lm.rb
CHANGED
@@ -118,7 +118,7 @@ module DSPy
|
|
118
118
|
end
|
119
119
|
|
120
120
|
# Let strategy handle JSON extraction if needed
|
121
|
-
if signature_class
|
121
|
+
if signature_class
|
122
122
|
extracted_json = strategy.extract_json(response)
|
123
123
|
if extracted_json && extracted_json != response.content
|
124
124
|
# Create a new response with extracted JSON
|
data/lib/dspy/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dspy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.14.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vicente Reig Rincón de Arellano
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-07-
|
11
|
+
date: 2025-07-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-configurable
|
@@ -150,8 +150,7 @@ dependencies:
|
|
150
150
|
- - "~>"
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: '1.2'
|
153
|
-
description:
|
154
|
-
language models
|
153
|
+
description: The Ruby framework for programming with large language models.
|
155
154
|
email:
|
156
155
|
- hey@vicente.services
|
157
156
|
executables: []
|
@@ -181,6 +180,7 @@ files:
|
|
181
180
|
- lib/dspy/lm/response.rb
|
182
181
|
- lib/dspy/lm/retry_handler.rb
|
183
182
|
- lib/dspy/lm/strategies/anthropic_extraction_strategy.rb
|
183
|
+
- lib/dspy/lm/strategies/anthropic_tool_use_strategy.rb
|
184
184
|
- lib/dspy/lm/strategies/base_strategy.rb
|
185
185
|
- lib/dspy/lm/strategies/enhanced_prompting_strategy.rb
|
186
186
|
- lib/dspy/lm/strategies/openai_structured_output_strategy.rb
|
@@ -249,5 +249,5 @@ requirements: []
|
|
249
249
|
rubygems_version: 3.5.22
|
250
250
|
signing_key:
|
251
251
|
specification_version: 4
|
252
|
-
summary: Ruby
|
252
|
+
summary: The Ruby framework for programming—rather than prompting—language models.
|
253
253
|
test_files: []
|