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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 564abfdf924b5e3aa3c002a9210a3e1ef73f2d418e80bf58a4038eecee65219a
4
- data.tar.gz: c7c8c1b2338d09636a5cca0b9bab7b12affe91d22d5edfb25031b9b4740ba68c
3
+ metadata.gz: 306fa376ea024edd2fc75d7bc1af6a42e60770e9566b4dd114414a40c685f2a7
4
+ data.tar.gz: 74f321fdd09a5761d17481af846dcd85dc3a372f573f79659c4fcfc761e92a18
5
5
  SHA512:
6
- metadata.gz: d81c9af7ac29706395e20db912af207cfc02f24d5777e247615ad94ebaa07a5f352df7ce87cac0947acb09a9f9d1e1094950c835a47fda5d2441114bdedabbd2
7
- data.tar.gz: 9366f67dffe4baa183f40705e8fc5f8f781b5f87e7aca3f0f77f954bb1009edefd773ce68d704a5e3663a0518f940e2ba51cc9b8a6c5c591e4309a7b325d4d51
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** - Automatic strategy selection for OpenAI structured outputs, Anthropic patterns, and fallback modes
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** - Basic optimization result persistence
32
+ - **File-based Storage** - Optimization result persistence with versioning
32
33
  - **Multi-Platform Observability** - OpenTelemetry, New Relic, and Langfuse integration
33
- - **Basic Instrumentation** - Event tracking and logging
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
- ## Fair Warning
44
+ ## Development Status
44
45
 
45
- This is fresh off the oven and evolving fast. I'm actively building this as a Ruby port of the [DSPy library](https://dspy.ai/). If you hit bugs or want to contribute, just email me directly!
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
- Skip the gem for now - install straight from this repo while I prep the first release:
54
+ ```ruby
55
+ gem 'dspy', '~> 0.9'
56
+ ```
57
+
58
+ Or add to your Gemfile:
52
59
 
53
60
  ```ruby
54
- gem 'dspy', github: 'vicentereig/dspy.rb'
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
- ## What's Next
128
-
129
- These are my goals to release v1.0.
130
-
131
- - ✅ Prompt objects foundation - *Done*
132
- - ✅ Evaluation framework - *Done*
133
- - ✅ Teleprompter base classes - *Done*
134
- - ✅ MIPROv2 optimization algorithm - *Done*
135
- - ✅ Storage & persistence system - *Done*
136
- - ✅ Registry & version management - *Done*
137
- - ✅ OpenTelemetry integration - *Done*
138
- - New Relic integration - *Done*
139
- - ✅ Langfuse integration - *Done*
140
- - 🚧 Ollama support
141
- - Context Engineering (see recent research: [How Contexts Fail](https://www.dbreunig.com/2025/06/22/how-contexts-fail-and-how-to-fix-them.html), [How to Fix Your Context](https://www.dbreunig.com/2025/06/26/how-to-fix-your-context.html), [Context Engineering](https://simonwillison.net/2025/Jun/27/context-engineering/))
142
- - Agentic Memory support
143
- - MCP Support
144
- - Documentation website
145
- - Performance benchmarks
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
- # Defines the signature for ReAct reasoning using Sorbet signatures
34
- class Thought < DSPy::Signature
35
- description "Generate a thought about what to do next to answer the question."
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 answer to the original question. This answer MUST be directly taken from the relevant Observation in the history if available. For example, if an observation showed \"Observation: 100.0\", and you are finishing, this field MUST be \"100.0\". Do not leave empty if finishing with an observed answer."
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
- # Defines the signature for processing observations and deciding next steps
64
- class ReActObservation < DSPy::Signature
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(Thought), DSPy::Predict)
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(ReActObservation), DSPy::Predict)
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 and extract question
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(question)
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(question: String).returns(T::Hash[Symbol, T.untyped]) }
169
- def execute_react_reasoning_loop(question)
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
- question, history, available_tools_desc, iterations_count, tools_used, last_observation
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(question: String, 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]) }
206
- def execute_single_iteration(question, history, available_tools_desc, iteration, tools_used, last_observation)
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
- question: question,
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
- question, history, observation, available_tools_desc, iteration
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(question: String, history: T::Array[HistoryEntry], observation: String, available_tools_desc: T::Array[T::Hash[String, T.untyped]], iteration: Integer).returns(T::Hash[Symbol, T.untyped]) }
341
- def process_observation_and_decide_next_step(question, history, observation, available_tools_desc, iteration)
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
- question: question,
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
- question, history, available_tools_desc, observation_result, iteration
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(question: String, history: T::Array[HistoryEntry], available_tools_desc: T::Array[T::Hash[String, T.untyped]], observation_result: T.untyped, iteration: Integer).returns(String) }
360
- def generate_forced_final_answer(question, history, available_tools_desc, observation_result, iteration)
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
- question: question,
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DSPy
4
- VERSION = "0.9.0"
4
+ VERSION = "0.9.1"
5
5
  end
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.0
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-11 00:00:00.000000000 Z
10
+ date: 2025-07-16 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dry-configurable