dspy 0.9.0 → 0.9.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 +44 -26
- data/lib/dspy/re_act.rb +86 -43
- data/lib/dspy/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 306fa376ea024edd2fc75d7bc1af6a42e60770e9566b4dd114414a40c685f2a7
|
4
|
+
data.tar.gz: 74f321fdd09a5761d17481af846dcd85dc3a372f573f79659c4fcfc761e92a18
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5efb12df9c365114696857067bb4f869ce2329c0977666331846a5f070aca050312992c176fddb60ab701b5d31b017d23182d7a4e813e80cba152d155fc2091c
|
7
|
+
data.tar.gz: 2fd7f831b6fab1624d2de7702d54d6377a4a96fff4cb0230eb810285828b764033288bc3180e2aa2cf92b1a1026fa6552cdbf851174216a3fc1a651419accc59
|
data/README.md
CHANGED
@@ -25,12 +25,13 @@ The result? LLM applications that actually scale and don't break when you sneeze
|
|
25
25
|
- **Basic Optimization** - Simple prompt optimization techniques
|
26
26
|
|
27
27
|
**Production Features:**
|
28
|
-
- **Reliable JSON Extraction** -
|
28
|
+
- **Reliable JSON Extraction** - Native OpenAI structured outputs, Anthropic extraction patterns, and automatic strategy selection with fallback
|
29
|
+
- **Type-Safe Configuration** - Strategy enums with automatic provider optimization (Strict/Compatible modes)
|
29
30
|
- **Smart Retry Logic** - Progressive fallback with exponential backoff for handling transient failures
|
30
31
|
- **Performance Caching** - Schema and capability caching for faster repeated operations
|
31
|
-
- **File-based Storage** -
|
32
|
+
- **File-based Storage** - Optimization result persistence with versioning
|
32
33
|
- **Multi-Platform Observability** - OpenTelemetry, New Relic, and Langfuse integration
|
33
|
-
- **
|
34
|
+
- **Comprehensive Instrumentation** - Event tracking, performance monitoring, and detailed logging
|
34
35
|
|
35
36
|
**Developer Experience:**
|
36
37
|
- LLM provider support using official Ruby clients:
|
@@ -40,18 +41,30 @@ The result? LLM applications that actually scale and don't break when you sneeze
|
|
40
41
|
- Type-safe tool definitions for ReAct agents
|
41
42
|
- Comprehensive instrumentation and observability
|
42
43
|
|
43
|
-
##
|
44
|
+
## Development Status
|
44
45
|
|
45
|
-
|
46
|
+
DSPy.rb is actively developed and approaching stability at **v0.9.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.
|
47
|
+
|
48
|
+
Real-world usage feedback is invaluable - if you encounter issues or have suggestions, please open a GitHub issue!
|
46
49
|
|
47
50
|
## Quick Start
|
48
51
|
|
49
52
|
### Installation
|
50
53
|
|
51
|
-
|
54
|
+
```ruby
|
55
|
+
gem 'dspy', '~> 0.9'
|
56
|
+
```
|
57
|
+
|
58
|
+
Or add to your Gemfile:
|
52
59
|
|
53
60
|
```ruby
|
54
|
-
gem 'dspy'
|
61
|
+
gem 'dspy'
|
62
|
+
```
|
63
|
+
|
64
|
+
Then run:
|
65
|
+
|
66
|
+
```bash
|
67
|
+
bundle install
|
55
68
|
```
|
56
69
|
|
57
70
|
### Your First DSPy Program
|
@@ -124,25 +137,30 @@ puts result.confidence # => 0.85
|
|
124
137
|
- **[RAG Patterns](docs/src/advanced/rag.md)** - Manual RAG implementation with external services
|
125
138
|
- **[Custom Metrics](docs/src/advanced/custom-metrics.md)** - Proc-based evaluation logic
|
126
139
|
|
127
|
-
##
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
- ✅
|
132
|
-
- ✅
|
133
|
-
- ✅
|
134
|
-
- ✅
|
135
|
-
- ✅
|
136
|
-
- ✅
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
-
|
141
|
-
|
142
|
-
|
143
|
-
-
|
144
|
-
-
|
145
|
-
-
|
140
|
+
## Recent Achievements
|
141
|
+
|
142
|
+
DSPy.rb has rapidly evolved from experimental to production-ready:
|
143
|
+
|
144
|
+
- ✅ **JSON Parsing Reliability** (v0.8.0) - Native OpenAI structured outputs, strategy selection, retry logic
|
145
|
+
- ✅ **Type-Safe Strategy Configuration** (v0.9.0) - Provider-optimized automatic strategy selection
|
146
|
+
- ✅ **Documentation Website** (v0.6.4) - Comprehensive docs at [vicentereig.github.io/dspy.rb](https://vicentereig.github.io/dspy.rb)
|
147
|
+
- ✅ **Production Observability** - OpenTelemetry, New Relic, and Langfuse integration
|
148
|
+
- ✅ **Optimization Framework** - MIPROv2 algorithm with storage & persistence
|
149
|
+
- ✅ **Core Module System** - Predict, ChainOfThought, ReAct, CodeAct with type safety
|
150
|
+
|
151
|
+
## Roadmap - Battle-Testing Toward v1.0
|
152
|
+
|
153
|
+
DSPy.rb is currently at **v0.9.0** and approaching stability. I'm focusing on real-world usage and refinement through the 0.10, 0.11, 0.12+ series before committing to a stable v1.0 API.
|
154
|
+
|
155
|
+
**Current Focus Areas:**
|
156
|
+
- 🚧 **Ollama Support** - Local model integration
|
157
|
+
- 🚧 **Context Engineering** - Advanced prompt optimization techniques
|
158
|
+
- 🚧 **MCP Support** - Model Context Protocol integration
|
159
|
+
- 🚧 **Agentic Memory** - Persistent agent state management
|
160
|
+
- 🚧 **Performance Optimization** - Based on production usage patterns
|
161
|
+
|
162
|
+
**v1.0 Philosophy:**
|
163
|
+
v1.0 will be released after extensive production battle-testing, not after checking off features. This ensures a stable, reliable API backed by real-world validation.
|
146
164
|
|
147
165
|
## License
|
148
166
|
|
data/lib/dspy/re_act.rb
CHANGED
@@ -30,18 +30,9 @@ module DSPy
|
|
30
30
|
}.compact
|
31
31
|
end
|
32
32
|
end
|
33
|
-
#
|
34
|
-
class
|
35
|
-
description "Generate a thought about what to do next to
|
36
|
-
|
37
|
-
input do
|
38
|
-
const :question, String,
|
39
|
-
description: "The question to answer"
|
40
|
-
const :history, T::Array[HistoryEntry],
|
41
|
-
description: "Previous thoughts and actions, including observations from tools. The agent MUST use information from the history to inform its actions and final answer. Each entry is a hash representing a step in the reasoning process."
|
42
|
-
const :available_tools, T::Array[T::Hash[String, T.untyped]],
|
43
|
-
description: "Array of available tools with their JSON schemas. The agent MUST choose an action from the tool names in this list or use \"finish\". For each tool, use the name exactly as specified and provide action_input as a JSON object matching the tool's schema."
|
44
|
-
end
|
33
|
+
# Base class for ReAct thought generation - will be customized per input type
|
34
|
+
class ThoughtBase < DSPy::Signature
|
35
|
+
description "Generate a thought about what to do next to process the given inputs."
|
45
36
|
|
46
37
|
output do
|
47
38
|
const :thought, String,
|
@@ -49,7 +40,7 @@ module DSPy
|
|
49
40
|
const :action, String,
|
50
41
|
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."
|
51
42
|
const :action_input, T.any(String, T::Hash[T.untyped, T.untyped]),
|
52
|
-
description: "Input for the chosen action. If action is a tool name, this MUST be a JSON object matching the tool's schema. If action is \"finish\", this field MUST contain the final
|
43
|
+
description: "Input for the chosen action. If action is a tool name, this MUST be a JSON object matching the tool's schema. If action is \"finish\", this field MUST contain the final result based on processing the input data. This result MUST be directly taken from the relevant Observation in the history if available."
|
53
44
|
end
|
54
45
|
end
|
55
46
|
|
@@ -60,19 +51,10 @@ module DSPy
|
|
60
51
|
end
|
61
52
|
end
|
62
53
|
|
63
|
-
#
|
64
|
-
class
|
54
|
+
# Base class for observation processing - will be customized per input type
|
55
|
+
class ReActObservationBase < DSPy::Signature
|
65
56
|
description "Process the observation from a tool and decide what to do next."
|
66
57
|
|
67
|
-
input do
|
68
|
-
const :question, String,
|
69
|
-
description: "The original question"
|
70
|
-
const :history, T::Array[HistoryEntry],
|
71
|
-
description: "Previous thoughts, actions, and observations. Each entry is a hash representing a step in the reasoning process."
|
72
|
-
const :observation, String,
|
73
|
-
description: "The result from the last action"
|
74
|
-
end
|
75
|
-
|
76
58
|
output do
|
77
59
|
const :interpretation, String,
|
78
60
|
description: "Interpretation of the observation"
|
@@ -108,11 +90,15 @@ module DSPy
|
|
108
90
|
tools.each { |tool| @tools[tool.name.downcase] = tool }
|
109
91
|
@max_iterations = max_iterations
|
110
92
|
|
93
|
+
# Create dynamic signature classes that include the original input fields
|
94
|
+
thought_signature = create_thought_signature(signature_class)
|
95
|
+
observation_signature = create_observation_signature(signature_class)
|
96
|
+
|
111
97
|
# Create thought generator using Predict to preserve field descriptions
|
112
|
-
@thought_generator = T.let(DSPy::Predict.new(
|
98
|
+
@thought_generator = T.let(DSPy::Predict.new(thought_signature), DSPy::Predict)
|
113
99
|
|
114
100
|
# Create observation processor using Predict to preserve field descriptions
|
115
|
-
@observation_processor = T.let(DSPy::Predict.new(
|
101
|
+
@observation_processor = T.let(DSPy::Predict.new(observation_signature), DSPy::Predict)
|
116
102
|
|
117
103
|
# Create enhanced output struct with ReAct fields
|
118
104
|
@enhanced_output_struct = create_enhanced_output_struct(signature_class)
|
@@ -148,12 +134,11 @@ module DSPy
|
|
148
134
|
max_iterations: @max_iterations,
|
149
135
|
available_tools: available_tools
|
150
136
|
}) do
|
151
|
-
# Validate input
|
137
|
+
# Validate input
|
152
138
|
input_struct = @original_signature_class.input_struct_class.new(**kwargs)
|
153
|
-
question = T.cast(input_struct.serialize.values.first, String)
|
154
139
|
|
155
140
|
# Execute ReAct reasoning loop
|
156
|
-
reasoning_result = execute_react_reasoning_loop(
|
141
|
+
reasoning_result = execute_react_reasoning_loop(input_struct)
|
157
142
|
|
158
143
|
# Create enhanced output with all ReAct data
|
159
144
|
create_enhanced_result(kwargs, reasoning_result)
|
@@ -164,9 +149,67 @@ module DSPy
|
|
164
149
|
|
165
150
|
private
|
166
151
|
|
152
|
+
# Creates a dynamic Thought signature that includes the original input fields
|
153
|
+
sig { params(signature_class: T.class_of(DSPy::Signature)).returns(T.class_of(DSPy::Signature)) }
|
154
|
+
def create_thought_signature(signature_class)
|
155
|
+
# Create new class that inherits from DSPy::Signature
|
156
|
+
Class.new(DSPy::Signature) do
|
157
|
+
# Set description
|
158
|
+
description "Generate a thought about what to do next to process the given inputs."
|
159
|
+
|
160
|
+
# Define input fields
|
161
|
+
input do
|
162
|
+
const :input_context, String,
|
163
|
+
desc: "Serialized representation of all input fields"
|
164
|
+
const :history, T::Array[HistoryEntry],
|
165
|
+
desc: "Previous thoughts and actions, including observations from tools."
|
166
|
+
const :available_tools, T::Array[T::Hash[String, T.untyped]],
|
167
|
+
desc: "Array of available tools with their JSON schemas."
|
168
|
+
end
|
169
|
+
|
170
|
+
# Define output fields (same as ThoughtBase)
|
171
|
+
output do
|
172
|
+
const :thought, String,
|
173
|
+
desc: "Reasoning about what to do next, considering the history and observations."
|
174
|
+
const :action, String,
|
175
|
+
desc: "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."
|
176
|
+
const :action_input, T.any(String, T::Hash[T.untyped, T.untyped]),
|
177
|
+
desc: "Input for the chosen action. If action is a tool name, this MUST be a JSON object matching the tool's schema. If action is \"finish\", this field MUST contain the final result based on processing the input data."
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Creates a dynamic observation signature that includes the original input fields
|
183
|
+
sig { params(signature_class: T.class_of(DSPy::Signature)).returns(T.class_of(DSPy::Signature)) }
|
184
|
+
def create_observation_signature(signature_class)
|
185
|
+
# Create new class that inherits from DSPy::Signature
|
186
|
+
Class.new(DSPy::Signature) do
|
187
|
+
# Set description
|
188
|
+
description "Process the observation from a tool and decide what to do next."
|
189
|
+
|
190
|
+
# Define input fields
|
191
|
+
input do
|
192
|
+
const :input_context, String,
|
193
|
+
desc: "Serialized representation of all input fields"
|
194
|
+
const :history, T::Array[HistoryEntry],
|
195
|
+
desc: "Previous thoughts, actions, and observations."
|
196
|
+
const :observation, String,
|
197
|
+
desc: "The result from the last action"
|
198
|
+
end
|
199
|
+
|
200
|
+
# Define output fields (same as ReActObservationBase)
|
201
|
+
output do
|
202
|
+
const :interpretation, String,
|
203
|
+
desc: "Interpretation of the observation"
|
204
|
+
const :next_step, NextStep,
|
205
|
+
desc: "What to do next: '#{NextStep::Continue}' or '#{NextStep::Finish}'"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
167
210
|
# Executes the main ReAct reasoning loop
|
168
|
-
sig { params(
|
169
|
-
def execute_react_reasoning_loop(
|
211
|
+
sig { params(input_struct: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
|
212
|
+
def execute_react_reasoning_loop(input_struct)
|
170
213
|
history = T.let([], T::Array[HistoryEntry])
|
171
214
|
available_tools_desc = @tools.map { |name, tool| JSON.parse(tool.schema) }
|
172
215
|
final_answer = T.let(nil, T.nilable(String))
|
@@ -178,7 +221,7 @@ module DSPy
|
|
178
221
|
iterations_count += 1
|
179
222
|
|
180
223
|
iteration_result = execute_single_iteration(
|
181
|
-
|
224
|
+
input_struct, history, available_tools_desc, iterations_count, tools_used, last_observation
|
182
225
|
)
|
183
226
|
|
184
227
|
if iteration_result[:should_finish]
|
@@ -202,8 +245,8 @@ module DSPy
|
|
202
245
|
end
|
203
246
|
|
204
247
|
# Executes a single iteration of the ReAct loop
|
205
|
-
sig { params(
|
206
|
-
def execute_single_iteration(
|
248
|
+
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], available_tools_desc: T::Array[T::Hash[String, T.untyped]], iteration: Integer, tools_used: T::Array[String], last_observation: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }
|
249
|
+
def execute_single_iteration(input_struct, history, available_tools_desc, iteration, tools_used, last_observation)
|
207
250
|
# Instrument each iteration
|
208
251
|
Instrumentation.instrument('dspy.react.iteration', {
|
209
252
|
iteration: iteration,
|
@@ -213,7 +256,7 @@ module DSPy
|
|
213
256
|
}) do
|
214
257
|
# Generate thought and action
|
215
258
|
thought_obj = @thought_generator.forward(
|
216
|
-
|
259
|
+
input_context: input_struct.serialize.to_json,
|
217
260
|
history: history,
|
218
261
|
available_tools: available_tools_desc
|
219
262
|
)
|
@@ -243,7 +286,7 @@ module DSPy
|
|
243
286
|
|
244
287
|
# Process observation and decide next step
|
245
288
|
observation_decision = process_observation_and_decide_next_step(
|
246
|
-
|
289
|
+
input_struct, history, observation, available_tools_desc, iteration
|
247
290
|
)
|
248
291
|
|
249
292
|
if observation_decision[:should_finish]
|
@@ -337,12 +380,12 @@ module DSPy
|
|
337
380
|
)
|
338
381
|
end
|
339
382
|
|
340
|
-
sig { params(
|
341
|
-
def process_observation_and_decide_next_step(
|
383
|
+
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], observation: String, available_tools_desc: T::Array[T::Hash[String, T.untyped]], iteration: Integer).returns(T::Hash[Symbol, T.untyped]) }
|
384
|
+
def process_observation_and_decide_next_step(input_struct, history, observation, available_tools_desc, iteration)
|
342
385
|
return { should_finish: false } if observation.include?("Unknown action")
|
343
386
|
|
344
387
|
observation_result = @observation_processor.forward(
|
345
|
-
|
388
|
+
input_context: input_struct.serialize.to_json,
|
346
389
|
history: history,
|
347
390
|
observation: observation
|
348
391
|
)
|
@@ -350,16 +393,16 @@ module DSPy
|
|
350
393
|
return { should_finish: false } unless observation_result.next_step == NextStep::Finish
|
351
394
|
|
352
395
|
final_answer = generate_forced_final_answer(
|
353
|
-
|
396
|
+
input_struct, history, available_tools_desc, observation_result, iteration
|
354
397
|
)
|
355
398
|
|
356
399
|
{ should_finish: true, final_answer: final_answer }
|
357
400
|
end
|
358
401
|
|
359
|
-
sig { params(
|
360
|
-
def generate_forced_final_answer(
|
402
|
+
sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], available_tools_desc: T::Array[T::Hash[String, T.untyped]], observation_result: T.untyped, iteration: Integer).returns(String) }
|
403
|
+
def generate_forced_final_answer(input_struct, history, available_tools_desc, observation_result, iteration)
|
361
404
|
final_thought = @thought_generator.forward(
|
362
|
-
|
405
|
+
input_context: input_struct.serialize.to_json,
|
363
406
|
history: history,
|
364
407
|
available_tools: available_tools_desc
|
365
408
|
)
|
data/lib/dspy/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dspy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vicente Reig Rincón de Arellano
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-07-
|
10
|
+
date: 2025-07-16 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: dry-configurable
|