dspy 1.0.0 → 1.0.2
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 +23 -171
- data/lib/dspy/lm/adapter_factory.rb +32 -2
- data/lib/dspy/lm/json_strategy.rb +7 -4
- data/lib/dspy/lm/reasoning.rb +98 -0
- data/lib/dspy/lm.rb +1 -0
- data/lib/dspy/mixins/type_coercion.rb +15 -1
- data/lib/dspy/predict.rb +5 -0
- data/lib/dspy/re_act.rb +40 -3
- data/lib/dspy/ruby_llm/version.rb +1 -1
- data/lib/dspy/scores/data_type.rb +1 -1
- data/lib/dspy/signature.rb +37 -1
- data/lib/dspy/tools/toolset.rb +2 -1
- data/lib/dspy/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: abf60ee3b93bfb4b1a66af4bf59c2b6d4422bbe4f6b130f84fcafe03e9817cf3
|
|
4
|
+
data.tar.gz: 4e5678be47e6b76b974a7cc2dec2adf6a1f5609ac5ac6d07cdfdc3c67f804703
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91036445e7e1c45c56275722a1c96beb8b716adb2d4036fe6df051a4d554eb47c39dbbc2e6c0994135df3d0000dfca25572b21975d1ac02fa2ff49ce09c183c4
|
|
7
|
+
data.tar.gz: 302be904ad43e6a64405521d6bf9f02c2539debd252933cf9d9bcebb0fa7c81abb7fa0b26b5abdb1a3502a55abed8f8c3dabb76278ff27813f196e9ae7938cd3
|
data/README.md
CHANGED
|
@@ -6,96 +6,16 @@
|
|
|
6
6
|
[](https://oss.vicente.services/dspy.rb/)
|
|
7
7
|
[](https://discord.gg/zWBhrMqn)
|
|
8
8
|
|
|
9
|
-
**Build
|
|
9
|
+
**Build typed agents and model-backed programs in Ruby.**
|
|
10
10
|
|
|
11
|
-
DSPy.rb
|
|
12
|
-
The `1.x` line is the stable release track for production Ruby LLM applications.
|
|
11
|
+
DSPy.rb brings [DSPy](https://dspy.ai)'s signature, module, agent, and optimizer model to Ruby, with Sorbet types and Ruby-native integrations. A signature declares a task as typed inputs and outputs. A module chooses how to execute it. Ruby composes modules into programs, while `ReAct` adds a bounded loop when the model should choose the next action. DSPy.rb builds the provider request and validates the returned shape.
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
require 'dspy'
|
|
16
|
-
|
|
17
|
-
DSPy.configure do |c|
|
|
18
|
-
c.lm = DSPy::LM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY'])
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
class Summarize < DSPy::Signature
|
|
22
|
-
description "Summarize the given text in one sentence."
|
|
23
|
-
|
|
24
|
-
input do
|
|
25
|
-
const :text, String
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
output do
|
|
29
|
-
const :summary, String
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
summarizer = DSPy::Predict.new(Summarize)
|
|
34
|
-
result = summarizer.call(text: "DSPy.rb brings structured LLM programming to Ruby...")
|
|
35
|
-
puts result.summary
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
That's it. No prompt templates. No JSON parsing. No prayer-based error handling.
|
|
39
|
-
|
|
40
|
-
## Installation
|
|
41
|
-
|
|
42
|
-
```ruby
|
|
43
|
-
# Gemfile
|
|
44
|
-
gem 'dspy'
|
|
45
|
-
gem 'dspy-openai' # For OpenAI, OpenRouter, or Ollama
|
|
46
|
-
# gem 'dspy-anthropic' # For Claude
|
|
47
|
-
# gem 'dspy-gemini' # For Gemini
|
|
48
|
-
# gem 'dspy-ruby_llm' # For 12+ providers via RubyLLM
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
bundle install
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## Quick Start
|
|
56
|
-
|
|
57
|
-
### Configure Your LLM
|
|
58
|
-
|
|
59
|
-
```ruby
|
|
60
|
-
# OpenAI
|
|
61
|
-
DSPy.configure do |c|
|
|
62
|
-
c.lm = DSPy::LM.new('openai/gpt-4o-mini',
|
|
63
|
-
api_key: ENV['OPENAI_API_KEY'],
|
|
64
|
-
structured_outputs: true)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Anthropic Claude
|
|
68
|
-
DSPy.configure do |c|
|
|
69
|
-
c.lm = DSPy::LM.new('anthropic/claude-sonnet-4-20250514',
|
|
70
|
-
api_key: ENV['ANTHROPIC_API_KEY'])
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Google Gemini
|
|
74
|
-
DSPy.configure do |c|
|
|
75
|
-
c.lm = DSPy::LM.new('gemini/gemini-2.5-flash',
|
|
76
|
-
api_key: ENV['GEMINI_API_KEY'])
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Ollama (local, free)
|
|
80
|
-
DSPy.configure do |c|
|
|
81
|
-
c.lm = DSPy::LM.new('ollama/llama3.2')
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# OpenRouter (200+ models)
|
|
85
|
-
DSPy.configure do |c|
|
|
86
|
-
c.lm = DSPy::LM.new('openrouter/deepseek/deepseek-chat-v3.1:free',
|
|
87
|
-
api_key: ENV['OPENROUTER_API_KEY'])
|
|
88
|
-
end
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### Define a Signature
|
|
13
|
+
The `1.x` series is the current stable release line.
|
|
92
14
|
|
|
93
|
-
|
|
15
|
+
In a configured application, the task contract and call look like this:
|
|
94
16
|
|
|
95
17
|
```ruby
|
|
96
18
|
class Classify < DSPy::Signature
|
|
97
|
-
description "Classify sentiment of a given sentence."
|
|
98
|
-
|
|
99
19
|
class Sentiment < T::Enum
|
|
100
20
|
enums do
|
|
101
21
|
Positive = new('positive')
|
|
@@ -105,7 +25,7 @@ class Classify < DSPy::Signature
|
|
|
105
25
|
end
|
|
106
26
|
|
|
107
27
|
input do
|
|
108
|
-
const :sentence, String
|
|
28
|
+
const :sentence, String
|
|
109
29
|
end
|
|
110
30
|
|
|
111
31
|
output do
|
|
@@ -115,107 +35,39 @@ class Classify < DSPy::Signature
|
|
|
115
35
|
end
|
|
116
36
|
|
|
117
37
|
classifier = DSPy::Predict.new(Classify)
|
|
118
|
-
result = classifier.call(sentence: "This book was
|
|
119
|
-
|
|
120
|
-
result.sentiment # => #<Sentiment::Positive>
|
|
121
|
-
result.confidence # => 0.92
|
|
38
|
+
result = classifier.call(sentence: "This book was fun to read!")
|
|
122
39
|
```
|
|
123
40
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
For complex reasoning, use `ChainOfThought` to get step-by-step explanations:
|
|
127
|
-
|
|
128
|
-
```ruby
|
|
129
|
-
solver = DSPy::ChainOfThought.new(MathProblem)
|
|
130
|
-
result = solver.call(problem: "If a train travels 120km in 2 hours, what's its speed?")
|
|
131
|
-
|
|
132
|
-
result.reasoning # => "Speed = Distance / Time = 120km / 2h = 60km/h"
|
|
133
|
-
result.answer # => "60 km/h"
|
|
134
|
-
```
|
|
41
|
+
Define a typed task contract instead of maintaining an output template. Receive validated Ruby values instead of parsing provider JSON. Handle configuration, transport, and validation errors explicitly.
|
|
135
42
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
Build agents that use tools to accomplish tasks:
|
|
139
|
-
|
|
140
|
-
```ruby
|
|
141
|
-
class SearchTool < DSPy::Tools::Base
|
|
142
|
-
tool_name "search"
|
|
143
|
-
tool_description "Search for information"
|
|
144
|
-
|
|
145
|
-
sig { params(query: String).returns(String) }
|
|
146
|
-
def call(query:)
|
|
147
|
-
# Your search implementation
|
|
148
|
-
"Result 1, Result 2"
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
agent = DSPy::ReAct.new(ResearchTask, tools: [SearchTool.new], max_iterations: 5)
|
|
153
|
-
result = agent.call(question: "What's the latest on Ruby 3.4?")
|
|
154
|
-
```
|
|
43
|
+
## Start Here
|
|
155
44
|
|
|
156
|
-
|
|
45
|
+
The [Quick Start](https://oss.vicente.services/dspy.rb/getting-started/quick-start/) is the complete supported path: install the core and provider gems, configure a key, save a program, and run it.
|
|
157
46
|
|
|
158
|
-
|
|
47
|
+
Use [Installation](https://oss.vicente.services/dspy.rb/getting-started/installation/) to choose a provider. The [package and capability matrix](https://oss.vicente.services/dspy.rb/getting-started/packages/) records exact gem names, require behavior, support labels, and model or SDK boundaries.
|
|
159
48
|
|
|
160
|
-
|
|
49
|
+
## Mental Model
|
|
161
50
|
|
|
162
|
-
|
|
51
|
+
- A `DSPy::Signature` defines the task contract.
|
|
52
|
+
- `DSPy::Predict` and other modules choose an execution strategy.
|
|
53
|
+
- Ordinary Ruby owns fixed sequencing, branching, persistence, permissions, and failure policy.
|
|
54
|
+
- Results cross a runtime validation boundary; validation establishes shape, not factual correctness.
|
|
163
55
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
**Optimization**: MIPROv2 (Bayesian optimization) and GEPA (genetic evolution) for prompt tuning.
|
|
167
|
-
|
|
168
|
-
**Provider Support**: OpenAI, Anthropic, Gemini, Ollama, and OpenRouter via official SDKs.
|
|
169
|
-
|
|
170
|
-
## Documentation
|
|
171
|
-
|
|
172
|
-
**[Full Documentation](https://oss.vicente.services/dspy.rb/)** — Getting started, core concepts, advanced patterns.
|
|
173
|
-
|
|
174
|
-
**[llms.txt](https://oss.vicente.services/dspy.rb/llms.txt)** — LLM-friendly reference for AI assistants.
|
|
175
|
-
|
|
176
|
-
### Claude Skill
|
|
177
|
-
|
|
178
|
-
A [Claude Skill](https://github.com/vicentereig/dspy-rb-skill) is available to help you build DSPy.rb applications:
|
|
179
|
-
|
|
180
|
-
```bash
|
|
181
|
-
# Claude Code — install from the vicentereig/engineering marketplace
|
|
182
|
-
claude install-skill vicentereig/engineering --skill dspy-rb
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
For Claude.ai Pro/Max, download the [skill ZIP](https://github.com/vicentereig/dspy-rb-skill/archive/refs/heads/main.zip) and upload via Settings > Skills.
|
|
186
|
-
|
|
187
|
-
## Examples
|
|
188
|
-
|
|
189
|
-
The [examples/](examples/) directory has runnable code for common patterns:
|
|
190
|
-
|
|
191
|
-
- Sentiment classification
|
|
192
|
-
- ReAct agents with tools
|
|
193
|
-
- Image analysis
|
|
194
|
-
- Prompt optimization
|
|
195
|
-
|
|
196
|
-
```bash
|
|
197
|
-
bundle exec ruby examples/basic_search_agent.rb
|
|
198
|
-
```
|
|
56
|
+
### ReAct Agents
|
|
199
57
|
|
|
200
|
-
|
|
58
|
+
Use `ReAct` when the model needs to choose among typed Ruby tools inside an iteration bound. The application still owns tool authorization, side effects, budgets, and errors. See [Predictors](https://oss.vicente.services/dspy.rb/core-concepts/predictors/) and [Toolsets](https://oss.vicente.services/dspy.rb/core-concepts/toolsets/).
|
|
201
59
|
|
|
202
|
-
|
|
60
|
+
## Explore
|
|
203
61
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
| `dspy-evals` | Evaluation harness with metrics and callbacks |
|
|
208
|
-
| `dspy-miprov2` | Bayesian optimization for prompt tuning |
|
|
209
|
-
| `dspy-gepa` | Genetic-Pareto prompt evolution |
|
|
210
|
-
| `dspy-o11y-langfuse` | Auto-configure Langfuse tracing |
|
|
211
|
-
| `dspy-code_act` | Think-Code-Observe agents |
|
|
212
|
-
| `dspy-deep_search` | Production DeepSearch with Exa |
|
|
62
|
+
- [Documentation](https://oss.vicente.services/dspy.rb/) — task-oriented guides and reference
|
|
63
|
+
- [Examples](examples/) — repository demos indexed by capability and prerequisites
|
|
64
|
+
- [llms.txt](https://oss.vicente.services/dspy.rb/llms.txt) — generated reference for AI assistants
|
|
213
65
|
|
|
214
|
-
|
|
66
|
+
Provider adapters, optimizers, observability exporters, datasets, and code-executing agents are separate packages when they add dependencies. Check the package matrix before selecting one; package availability does not guarantee uniform provider or model behavior.
|
|
215
67
|
|
|
216
68
|
## Contributing
|
|
217
69
|
|
|
218
|
-
|
|
70
|
+
For bugs, [open an issue](https://github.com/vicentereig/dspy.rb/issues). For suggestions, [start a discussion](https://github.com/vicentereig/dspy.rb/discussions).
|
|
219
71
|
|
|
220
72
|
Want to contribute code? Reach out: hey at vicente.services
|
|
221
73
|
|
|
@@ -16,6 +16,13 @@ module DSPy
|
|
|
16
16
|
|
|
17
17
|
PROVIDERS_WITH_EXTRA_OPTIONS = %w[openai anthropic ollama gemini openrouter ruby_llm].freeze
|
|
18
18
|
|
|
19
|
+
# `reasoning:` is currently implemented only for the Anthropic adapter
|
|
20
|
+
# (see adr/019-anthropic-reasoning-temperature-config.md). Checked here,
|
|
21
|
+
# once, so every provider fails the same way instead of each adapter
|
|
22
|
+
# either raising a raw ArgumentError (no `reasoning:` keyword declared)
|
|
23
|
+
# or silently ignoring it (RubyLLMAdapter's `**options`).
|
|
24
|
+
REASONING_SUPPORTED_PROVIDERS = %w[anthropic].freeze
|
|
25
|
+
|
|
19
26
|
class AdapterData < Data.define(:class_name, :gem_name)
|
|
20
27
|
def self.from_prefix(provider_prefix)
|
|
21
28
|
if ADAPTER_MAP.key?(provider_prefix)
|
|
@@ -37,7 +44,9 @@ module DSPy
|
|
|
37
44
|
def create(model_id, api_key:, **options)
|
|
38
45
|
provider, model = parse_model_id(model_id)
|
|
39
46
|
adapter_class = get_adapter_class(provider)
|
|
40
|
-
|
|
47
|
+
ensure_reasoning_supported!(provider, options)
|
|
48
|
+
options = normalize_reasoning_option(provider, options)
|
|
49
|
+
|
|
41
50
|
# Pass provider-specific options
|
|
42
51
|
adapter_options = { model: model, api_key: api_key }
|
|
43
52
|
# Some providers accept additional options
|
|
@@ -48,6 +57,27 @@ module DSPy
|
|
|
48
57
|
|
|
49
58
|
private
|
|
50
59
|
|
|
60
|
+
def ensure_reasoning_supported!(provider, options)
|
|
61
|
+
return unless options.key?(:reasoning) && !options[:reasoning].nil?
|
|
62
|
+
return if REASONING_SUPPORTED_PROVIDERS.include?(provider)
|
|
63
|
+
|
|
64
|
+
raise DSPy::LM::ConfigurationError,
|
|
65
|
+
"reasoning: is currently supported only by the Anthropic adapter " \
|
|
66
|
+
"(got provider: #{provider.inspect}). See adr/019-anthropic-reasoning-temperature-config.md."
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# `reasoning: nil` on an unsupported provider should behave exactly like
|
|
70
|
+
# not passing `reasoning:` at all — ensure_reasoning_supported! already
|
|
71
|
+
# let it through (only a *truthy* reasoning: raises above), but the key
|
|
72
|
+
# would otherwise still get forwarded into `**options`, and adapters
|
|
73
|
+
# that don't declare a `reasoning:` keyword (OpenAI, Gemini, Ollama,
|
|
74
|
+
# OpenRouter) would raise a raw ArgumentError over a value nobody set.
|
|
75
|
+
def normalize_reasoning_option(provider, options)
|
|
76
|
+
return options if REASONING_SUPPORTED_PROVIDERS.include?(provider)
|
|
77
|
+
|
|
78
|
+
options.reject { |key, _| key == :reasoning }
|
|
79
|
+
end
|
|
80
|
+
|
|
51
81
|
# Parse model_id to determine provider and model
|
|
52
82
|
def parse_model_id(model_id)
|
|
53
83
|
unless model_id.include?('/')
|
|
@@ -78,11 +108,11 @@ module DSPy
|
|
|
78
108
|
|
|
79
109
|
def ensure_adapter_loaded!(provider)
|
|
80
110
|
adapter_data = adapter_data(provider)
|
|
81
|
-
require adapter_data.require_path
|
|
82
111
|
msg = <<~ERROR
|
|
83
112
|
Adapter not found: #{adapter_data.class_name}.
|
|
84
113
|
Install the #{adapter_data.gem_name} gem and try again.
|
|
85
114
|
ERROR
|
|
115
|
+
require adapter_data.require_path
|
|
86
116
|
rescue LoadError
|
|
87
117
|
raise MissingAdapterError, msg
|
|
88
118
|
end
|
|
@@ -38,7 +38,7 @@ module DSPy
|
|
|
38
38
|
# OpenAI/Ollama: try to extract JSON from various formats
|
|
39
39
|
extract_json_from_content(response.content)
|
|
40
40
|
elsif adapter_class_name.include?('AnthropicAdapter')
|
|
41
|
-
# Anthropic:
|
|
41
|
+
# Anthropic: structured output returns JSON in content, same as OpenAI/Gemini
|
|
42
42
|
extract_json_from_content(response.content)
|
|
43
43
|
elsif adapter_class_name.include?('GeminiAdapter')
|
|
44
44
|
# Gemini: try to extract JSON from various formats
|
|
@@ -97,14 +97,17 @@ module DSPy
|
|
|
97
97
|
|
|
98
98
|
return unless structured_outputs_enabled
|
|
99
99
|
|
|
100
|
-
#
|
|
100
|
+
# Structured outputs live under the stable (non-beta) `output_config.format`
|
|
101
|
+
# shape. `output_format`/`betas` are deprecated by Anthropic in favor of
|
|
102
|
+
# this (see adr/019-anthropic-reasoning-temperature-config.md); the adapter
|
|
103
|
+
# merges this with any `reasoning:`-derived effort into a single
|
|
104
|
+
# `output_config:` hash before calling the Anthropic client.
|
|
101
105
|
schema = DSPy::Anthropic::LM::SchemaConverter.to_beta_format(signature_class)
|
|
102
106
|
|
|
103
|
-
request_params[:output_format] = ::Anthropic::Models::
|
|
107
|
+
request_params[:output_format] = ::Anthropic::Models::JSONOutputFormat.new(
|
|
104
108
|
type: :json_schema,
|
|
105
109
|
schema: schema
|
|
106
110
|
)
|
|
107
|
-
request_params[:betas] = ["structured-outputs-2025-11-13"]
|
|
108
111
|
end
|
|
109
112
|
|
|
110
113
|
# Gemini preparation
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sorbet-runtime'
|
|
4
|
+
|
|
5
|
+
module DSPy
|
|
6
|
+
# Shared, provider-agnostic reasoning/thinking configuration.
|
|
7
|
+
#
|
|
8
|
+
# Passed to +DSPy::LM.new(..., reasoning: ...)+. Each adapter is responsible
|
|
9
|
+
# for mapping the resulting struct onto its own provider's request shape and
|
|
10
|
+
# raising +DSPy::LM::ConfigurationError+ for combinations it can't support
|
|
11
|
+
# (see the Anthropic adapter's +ModelCapabilities+ registry).
|
|
12
|
+
#
|
|
13
|
+
# Exactly one reasoning mode is set per instance (effort tier, manual token
|
|
14
|
+
# budget, adaptive thinking, or explicitly disabled) — this is a
|
|
15
|
+
# discriminated union, not a combinable set of options.
|
|
16
|
+
class Reasoning < T::Struct
|
|
17
|
+
extend T::Sig
|
|
18
|
+
|
|
19
|
+
# Named effort tiers, as documented by Anthropic's `output_config.effort`
|
|
20
|
+
# (https://platform.claude.com/docs/en/build-with-claude/effort).
|
|
21
|
+
class Effort < T::Enum
|
|
22
|
+
enums do
|
|
23
|
+
Low = new('low')
|
|
24
|
+
Medium = new('medium')
|
|
25
|
+
High = new('high')
|
|
26
|
+
XHigh = new('xhigh')
|
|
27
|
+
Max = new('max')
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
const :effort, T.nilable(Effort), default: nil
|
|
32
|
+
const :budget_tokens, T.nilable(Integer), default: nil
|
|
33
|
+
const :adaptive, T::Boolean, default: false
|
|
34
|
+
const :disabled, T::Boolean, default: false
|
|
35
|
+
|
|
36
|
+
sig { params(args: T.untyped).void }
|
|
37
|
+
def initialize(**args)
|
|
38
|
+
super
|
|
39
|
+
|
|
40
|
+
active = {
|
|
41
|
+
effort: !effort.nil?,
|
|
42
|
+
budget_tokens: !budget_tokens.nil?,
|
|
43
|
+
adaptive: adaptive,
|
|
44
|
+
disabled: disabled
|
|
45
|
+
}.select { |_, set| set }.keys
|
|
46
|
+
|
|
47
|
+
if active.size > 1
|
|
48
|
+
raise DSPy::LM::ConfigurationError,
|
|
49
|
+
"DSPy::Reasoning represents exactly one reasoning mode at a time " \
|
|
50
|
+
"(effort, budget_tokens, adaptive, or disabled), but #{active.size} were set: #{active.join(', ')}."
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class << self
|
|
55
|
+
extend T::Sig
|
|
56
|
+
|
|
57
|
+
sig { returns(Reasoning) }
|
|
58
|
+
def low
|
|
59
|
+
new(effort: Effort::Low)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sig { returns(Reasoning) }
|
|
63
|
+
def medium
|
|
64
|
+
new(effort: Effort::Medium)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sig { returns(Reasoning) }
|
|
68
|
+
def high
|
|
69
|
+
new(effort: Effort::High)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
sig { returns(Reasoning) }
|
|
73
|
+
def xhigh
|
|
74
|
+
new(effort: Effort::XHigh)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
sig { returns(Reasoning) }
|
|
78
|
+
def max
|
|
79
|
+
new(effort: Effort::Max)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
sig { params(tokens: Integer).returns(Reasoning) }
|
|
83
|
+
def budget(tokens)
|
|
84
|
+
new(budget_tokens: tokens)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
sig { returns(Reasoning) }
|
|
88
|
+
def adaptive
|
|
89
|
+
new(adaptive: true)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
sig { returns(Reasoning) }
|
|
93
|
+
def disabled
|
|
94
|
+
new(disabled: true)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/dspy/lm.rb
CHANGED
|
@@ -299,9 +299,11 @@ module DSPy
|
|
|
299
299
|
# Coerces an array value, converting each element as needed
|
|
300
300
|
sig { params(value: T.untyped, prop_type: T.untyped).returns(T.untyped) }
|
|
301
301
|
def coerce_array_value(value, prop_type)
|
|
302
|
-
return value unless value.is_a?(Array)
|
|
303
302
|
return value unless prop_type.is_a?(T::Types::TypedArray)
|
|
304
303
|
|
|
304
|
+
value = try_parse_string_to_array(value)
|
|
305
|
+
return value unless value.is_a?(Array)
|
|
306
|
+
|
|
305
307
|
element_type = prop_type.type
|
|
306
308
|
value.map { |element| coerce_value_to_type(element, element_type) }
|
|
307
309
|
end
|
|
@@ -347,6 +349,18 @@ module DSPy
|
|
|
347
349
|
value
|
|
348
350
|
end
|
|
349
351
|
|
|
352
|
+
# Attempts to parse a JSON string into an Array.
|
|
353
|
+
# Returns the parsed Array on success, or the original value otherwise.
|
|
354
|
+
sig { params(value: T.untyped).returns(T.untyped) }
|
|
355
|
+
def try_parse_string_to_array(value)
|
|
356
|
+
return value unless value.is_a?(String)
|
|
357
|
+
|
|
358
|
+
parsed = JSON.parse(value)
|
|
359
|
+
parsed.is_a?(Array) ? parsed : value
|
|
360
|
+
rescue JSON::ParserError
|
|
361
|
+
value
|
|
362
|
+
end
|
|
363
|
+
|
|
350
364
|
# Attempts to parse a JSON string into a Hash.
|
|
351
365
|
# Returns the parsed Hash on success, or the original value otherwise.
|
|
352
366
|
sig { params(value: T.untyped).returns(T.untyped) }
|
data/lib/dspy/predict.rb
CHANGED
|
@@ -246,6 +246,11 @@ module DSPy
|
|
|
246
246
|
sig { params(input_values: T::Hash[Symbol, T.untyped], output_attributes: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
|
|
247
247
|
def create_prediction_result(input_values, output_attributes)
|
|
248
248
|
begin
|
|
249
|
+
@signature_class.validate_required_fields!(
|
|
250
|
+
output_attributes,
|
|
251
|
+
@signature_class.output_field_descriptors
|
|
252
|
+
)
|
|
253
|
+
|
|
249
254
|
combined_struct = create_combined_struct_class
|
|
250
255
|
all_attributes = input_values.merge(output_attributes)
|
|
251
256
|
|
data/lib/dspy/re_act.rb
CHANGED
|
@@ -110,6 +110,8 @@ module DSPy
|
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
FINISH_ACTION = "finish"
|
|
113
|
+
THOUGHT_LOOP_INSTRUCTION = "Generate a thought about what to do next to process the given inputs."
|
|
114
|
+
OBSERVATION_LOOP_INSTRUCTION = "Process the observation from a tool and decide what to do next."
|
|
113
115
|
sig { returns(T.class_of(DSPy::Signature)) }
|
|
114
116
|
attr_reader :original_signature_class
|
|
115
117
|
|
|
@@ -147,6 +149,15 @@ module DSPy
|
|
|
147
149
|
# Create enhanced output struct with ReAct fields
|
|
148
150
|
@enhanced_output_struct = create_enhanced_output_struct(signature_class)
|
|
149
151
|
enhanced_output_struct = @enhanced_output_struct
|
|
152
|
+
input_descriptors = signature_class.input_field_descriptors
|
|
153
|
+
output_descriptors = input_descriptors.merge(signature_class.output_field_descriptors).merge(
|
|
154
|
+
history: DSPy::Signature::FieldDescriptor.new(
|
|
155
|
+
T::Array[T::Hash[Symbol, T.untyped]],
|
|
156
|
+
"ReAct execution history"
|
|
157
|
+
),
|
|
158
|
+
iterations: DSPy::Signature::FieldDescriptor.new(Integer, "Number of iterations executed"),
|
|
159
|
+
tools_used: DSPy::Signature::FieldDescriptor.new(T::Array[String], "List of tools used during execution")
|
|
160
|
+
)
|
|
150
161
|
|
|
151
162
|
# Create enhanced signature class
|
|
152
163
|
enhanced_signature = Class.new(DSPy::Signature) do
|
|
@@ -155,9 +166,11 @@ module DSPy
|
|
|
155
166
|
|
|
156
167
|
# Use the same input struct
|
|
157
168
|
@input_struct_class = signature_class.input_struct_class
|
|
169
|
+
@input_field_descriptors = input_descriptors
|
|
158
170
|
|
|
159
171
|
# Use the enhanced output struct with ReAct fields
|
|
160
172
|
@output_struct_class = enhanced_output_struct
|
|
173
|
+
@output_field_descriptors = output_descriptors
|
|
161
174
|
|
|
162
175
|
# Store original signature name
|
|
163
176
|
@original_signature_name = signature_class.name
|
|
@@ -198,7 +211,9 @@ module DSPy
|
|
|
198
211
|
def with_instruction(instruction)
|
|
199
212
|
clone = self.class.new(@original_signature_class, tools: @tools.values, max_iterations: @max_iterations)
|
|
200
213
|
thought_generator = clone.instance_variable_get(:@thought_generator)
|
|
214
|
+
observation_processor = clone.instance_variable_get(:@observation_processor)
|
|
201
215
|
clone.instance_variable_set(:@thought_generator, thought_generator.with_instruction(instruction))
|
|
216
|
+
clone.instance_variable_set(:@observation_processor, observation_processor.with_instruction(instruction))
|
|
202
217
|
clone
|
|
203
218
|
end
|
|
204
219
|
|
|
@@ -224,6 +239,14 @@ module DSPy
|
|
|
224
239
|
|
|
225
240
|
private
|
|
226
241
|
|
|
242
|
+
sig { params(signature_class: T.class_of(DSPy::Signature), loop_instruction: String).returns(String) }
|
|
243
|
+
def loop_description(signature_class, loop_instruction)
|
|
244
|
+
[
|
|
245
|
+
signature_class.description,
|
|
246
|
+
loop_instruction
|
|
247
|
+
].compact.reject(&:empty?).join("\n\n")
|
|
248
|
+
end
|
|
249
|
+
|
|
227
250
|
# Serialize value for LLM display
|
|
228
251
|
sig { params(value: T.untyped).returns(T.untyped) }
|
|
229
252
|
def serialize_for_llm(value)
|
|
@@ -302,6 +325,13 @@ module DSPy
|
|
|
302
325
|
action_enum_class = @action_enum_class
|
|
303
326
|
input_context_type = signature_class.input_struct_class || String
|
|
304
327
|
|
|
328
|
+
# Carry the user signature's instructions into the loop prompt, then append the
|
|
329
|
+
# loop mechanics — mirroring Python DSPy's ReAct, which prepends
|
|
330
|
+
# signature.instructions to the react predictor's instructions. Without this the
|
|
331
|
+
# user's task description never reaches the LM: it is only set on the enhanced
|
|
332
|
+
# signature used for typing, which issues no LM calls.
|
|
333
|
+
thought_description = loop_description(signature_class, THOUGHT_LOOP_INSTRUCTION)
|
|
334
|
+
|
|
305
335
|
# Get the output field type for the final_answer field
|
|
306
336
|
output_field_name = signature_class.output_struct_class.props.keys.first
|
|
307
337
|
output_field_type = signature_class.output_struct_class.props[output_field_name][:type_object]
|
|
@@ -309,7 +339,7 @@ module DSPy
|
|
|
309
339
|
# Create new class that inherits from DSPy::Signature
|
|
310
340
|
Class.new(DSPy::Signature) do
|
|
311
341
|
# Set description
|
|
312
|
-
description
|
|
342
|
+
description thought_description
|
|
313
343
|
|
|
314
344
|
# Define input fields
|
|
315
345
|
input do
|
|
@@ -327,7 +357,7 @@ module DSPy
|
|
|
327
357
|
description: "Reasoning about what to do next, considering the history and observations."
|
|
328
358
|
const :action, action_enum_class,
|
|
329
359
|
description: "The action to take. MUST be one of the tool names listed in `available_tools` input, or the literal string \"finish\" to provide the final answer."
|
|
330
|
-
const :tool_input, ToolInput,
|
|
360
|
+
const :tool_input, ToolInput, default: nil,
|
|
331
361
|
description: "Input for the chosen tool action. Required when action is a tool name. MUST be a JSON object matching the tool's parameter schema. Set to null when action is \"finish\"."
|
|
332
362
|
const :final_answer, T.nilable(output_field_type),
|
|
333
363
|
description: "The final answer to return. Required when action is \"finish\". Must match the expected output type. Set to null when action is a tool name."
|
|
@@ -339,10 +369,12 @@ module DSPy
|
|
|
339
369
|
sig { params(signature_class: T.class_of(DSPy::Signature), data_format: Symbol).returns(T.class_of(DSPy::Signature)) }
|
|
340
370
|
def create_observation_signature(signature_class, data_format)
|
|
341
371
|
input_context_type = signature_class.input_struct_class || String
|
|
372
|
+
observation_description = loop_description(signature_class, OBSERVATION_LOOP_INSTRUCTION)
|
|
373
|
+
|
|
342
374
|
# Create new class that inherits from DSPy::Signature
|
|
343
375
|
Class.new(DSPy::Signature) do
|
|
344
376
|
# Set description
|
|
345
|
-
description
|
|
377
|
+
description observation_description
|
|
346
378
|
|
|
347
379
|
# Define input fields
|
|
348
380
|
input do
|
|
@@ -520,6 +552,11 @@ module DSPy
|
|
|
520
552
|
|
|
521
553
|
output_data[output_field_name] = deserialized_value
|
|
522
554
|
|
|
555
|
+
@signature_class.validate_required_fields!(
|
|
556
|
+
output_data,
|
|
557
|
+
@signature_class.output_field_descriptors
|
|
558
|
+
)
|
|
559
|
+
|
|
523
560
|
@enhanced_output_struct.new(**output_data)
|
|
524
561
|
end
|
|
525
562
|
|
data/lib/dspy/signature.rb
CHANGED
|
@@ -55,7 +55,7 @@ module DSPy
|
|
|
55
55
|
sig { returns(T.class_of(T::Struct)) }
|
|
56
56
|
def build_struct_class
|
|
57
57
|
descriptors = @field_descriptors
|
|
58
|
-
Class.new(T::Struct) do
|
|
58
|
+
struct_class = Class.new(T::Struct) do
|
|
59
59
|
extend T::Sig
|
|
60
60
|
descriptors.each do |name, descriptor|
|
|
61
61
|
opts = {}
|
|
@@ -64,6 +64,22 @@ module DSPy
|
|
|
64
64
|
const name, descriptor.type, **opts
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
|
+
|
|
68
|
+
struct_class.instance_variable_set(:@dspy_field_descriptors, descriptors)
|
|
69
|
+
|
|
70
|
+
# Use a virtual source path so source-comment introspection does not mistake
|
|
71
|
+
# this generated method for the anonymous struct's class definition.
|
|
72
|
+
struct_class.singleton_class.class_eval(<<~RUBY, "(dspy-signature-struct)", 1)
|
|
73
|
+
def new(*args, **kwargs)
|
|
74
|
+
properties = DSPy::Signature.constructor_properties(args, kwargs)
|
|
75
|
+
descriptors = instance_variable_get(:@dspy_field_descriptors)
|
|
76
|
+
DSPy::Signature.validate_required_fields!(properties, descriptors)
|
|
77
|
+
|
|
78
|
+
args.empty? ? super(**kwargs) : super(args.first)
|
|
79
|
+
end
|
|
80
|
+
RUBY
|
|
81
|
+
|
|
82
|
+
struct_class
|
|
67
83
|
end
|
|
68
84
|
end
|
|
69
85
|
|
|
@@ -85,6 +101,26 @@ module DSPy
|
|
|
85
101
|
sig { returns(T::Hash[Symbol, FieldDescriptor]) }
|
|
86
102
|
attr_reader :output_field_descriptors
|
|
87
103
|
|
|
104
|
+
sig { params(args: T.untyped, kwargs: T.untyped).returns(T::Hash[T.untyped, T.untyped]) }
|
|
105
|
+
def constructor_properties(args, kwargs)
|
|
106
|
+
return kwargs if args.empty?
|
|
107
|
+
return args.first if args.length == 1 && args.first.is_a?(Hash) && kwargs.empty?
|
|
108
|
+
|
|
109
|
+
raise ArgumentError, "Expected properties as keywords or one positional hash, not both"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
sig { params(properties: T::Hash[T.untyped, T.untyped], descriptors: T::Hash[Symbol, FieldDescriptor]).void }
|
|
113
|
+
def validate_required_fields!(properties, descriptors)
|
|
114
|
+
required_fields = descriptors.reject { |_name, descriptor| descriptor.has_default }.keys
|
|
115
|
+
missing_fields = required_fields.reject do |name|
|
|
116
|
+
properties.key?(name) || properties.key?(name.to_s)
|
|
117
|
+
end
|
|
118
|
+
return if missing_fields.empty?
|
|
119
|
+
|
|
120
|
+
names = missing_fields.map(&:inspect).join(', ')
|
|
121
|
+
raise ArgumentError, "Missing required properties: #{names}"
|
|
122
|
+
end
|
|
123
|
+
|
|
88
124
|
sig { params(desc: T.nilable(String)).returns(T.nilable(String)) }
|
|
89
125
|
def description(desc = nil)
|
|
90
126
|
if desc.nil?
|
data/lib/dspy/tools/toolset.rb
CHANGED
data/lib/dspy/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dspy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vicente Reig Rincón de Arellano
|
|
@@ -177,6 +177,7 @@ files:
|
|
|
177
177
|
- lib/dspy/lm/json_strategy.rb
|
|
178
178
|
- lib/dspy/lm/message.rb
|
|
179
179
|
- lib/dspy/lm/message_builder.rb
|
|
180
|
+
- lib/dspy/lm/reasoning.rb
|
|
180
181
|
- lib/dspy/lm/response.rb
|
|
181
182
|
- lib/dspy/lm/usage.rb
|
|
182
183
|
- lib/dspy/lm/vision_models.rb
|