dspy 1.0.1 → 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/predict.rb +5 -0
- data/lib/dspy/re_act.rb +17 -1
- 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
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
|
@@ -149,6 +149,15 @@ module DSPy
|
|
|
149
149
|
# Create enhanced output struct with ReAct fields
|
|
150
150
|
@enhanced_output_struct = create_enhanced_output_struct(signature_class)
|
|
151
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
|
+
)
|
|
152
161
|
|
|
153
162
|
# Create enhanced signature class
|
|
154
163
|
enhanced_signature = Class.new(DSPy::Signature) do
|
|
@@ -157,9 +166,11 @@ module DSPy
|
|
|
157
166
|
|
|
158
167
|
# Use the same input struct
|
|
159
168
|
@input_struct_class = signature_class.input_struct_class
|
|
169
|
+
@input_field_descriptors = input_descriptors
|
|
160
170
|
|
|
161
171
|
# Use the enhanced output struct with ReAct fields
|
|
162
172
|
@output_struct_class = enhanced_output_struct
|
|
173
|
+
@output_field_descriptors = output_descriptors
|
|
163
174
|
|
|
164
175
|
# Store original signature name
|
|
165
176
|
@original_signature_name = signature_class.name
|
|
@@ -346,7 +357,7 @@ module DSPy
|
|
|
346
357
|
description: "Reasoning about what to do next, considering the history and observations."
|
|
347
358
|
const :action, action_enum_class,
|
|
348
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."
|
|
349
|
-
const :tool_input, ToolInput,
|
|
360
|
+
const :tool_input, ToolInput, default: nil,
|
|
350
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\"."
|
|
351
362
|
const :final_answer, T.nilable(output_field_type),
|
|
352
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."
|
|
@@ -541,6 +552,11 @@ module DSPy
|
|
|
541
552
|
|
|
542
553
|
output_data[output_field_name] = deserialized_value
|
|
543
554
|
|
|
555
|
+
@signature_class.validate_required_fields!(
|
|
556
|
+
output_data,
|
|
557
|
+
@signature_class.output_field_descriptors
|
|
558
|
+
)
|
|
559
|
+
|
|
544
560
|
@enhanced_output_struct.new(**output_data)
|
|
545
561
|
end
|
|
546
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
|