dspy 0.29.1 → 0.30.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +45 -0
  3. data/README.md +159 -95
  4. data/lib/dspy/callbacks.rb +93 -19
  5. data/lib/dspy/context.rb +101 -5
  6. data/lib/dspy/errors.rb +19 -1
  7. data/lib/dspy/{datasets.rb → evals/version.rb} +2 -3
  8. data/lib/dspy/{evaluate.rb → evals.rb} +373 -110
  9. data/lib/dspy/mixins/instruction_updatable.rb +22 -0
  10. data/lib/dspy/module.rb +213 -17
  11. data/lib/dspy/observability.rb +40 -182
  12. data/lib/dspy/predict.rb +10 -2
  13. data/lib/dspy/propose/dataset_summary_generator.rb +28 -18
  14. data/lib/dspy/re_act.rb +21 -0
  15. data/lib/dspy/schema/sorbet_json_schema.rb +302 -0
  16. data/lib/dspy/schema/version.rb +7 -0
  17. data/lib/dspy/schema.rb +4 -0
  18. data/lib/dspy/structured_outputs_prompt.rb +48 -0
  19. data/lib/dspy/support/warning_filters.rb +27 -0
  20. data/lib/dspy/teleprompt/gepa.rb +9 -588
  21. data/lib/dspy/teleprompt/instruction_updates.rb +94 -0
  22. data/lib/dspy/teleprompt/teleprompter.rb +6 -6
  23. data/lib/dspy/teleprompt/utils.rb +5 -65
  24. data/lib/dspy/type_system/sorbet_json_schema.rb +2 -299
  25. data/lib/dspy/version.rb +1 -1
  26. data/lib/dspy.rb +39 -7
  27. metadata +18 -61
  28. data/lib/dspy/code_act.rb +0 -477
  29. data/lib/dspy/datasets/ade.rb +0 -90
  30. data/lib/dspy/observability/async_span_processor.rb +0 -250
  31. data/lib/dspy/observability/observation_type.rb +0 -65
  32. data/lib/dspy/optimizers/gaussian_process.rb +0 -141
  33. data/lib/dspy/teleprompt/mipro_v2.rb +0 -1672
  34. data/lib/gepa/api.rb +0 -61
  35. data/lib/gepa/core/engine.rb +0 -226
  36. data/lib/gepa/core/evaluation_batch.rb +0 -26
  37. data/lib/gepa/core/result.rb +0 -92
  38. data/lib/gepa/core/state.rb +0 -231
  39. data/lib/gepa/logging/experiment_tracker.rb +0 -54
  40. data/lib/gepa/logging/logger.rb +0 -57
  41. data/lib/gepa/logging.rb +0 -9
  42. data/lib/gepa/proposer/base.rb +0 -27
  43. data/lib/gepa/proposer/merge_proposer.rb +0 -424
  44. data/lib/gepa/proposer/reflective_mutation/base.rb +0 -48
  45. data/lib/gepa/proposer/reflective_mutation/reflective_mutation.rb +0 -188
  46. data/lib/gepa/strategies/batch_sampler.rb +0 -91
  47. data/lib/gepa/strategies/candidate_selector.rb +0 -97
  48. data/lib/gepa/strategies/component_selector.rb +0 -57
  49. data/lib/gepa/strategies/instruction_proposal.rb +0 -120
  50. data/lib/gepa/telemetry.rb +0 -122
  51. data/lib/gepa/utils/pareto.rb +0 -119
  52. data/lib/gepa.rb +0 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42a6bfa4a1e0fe7b7dd38e3a2fe017959365f566248319e1deec6cadcd06efc3
4
- data.tar.gz: ffceb724c96e7803ec7531bf52c12f502266e21d4ab9bcf534a8005e59835883
3
+ metadata.gz: 0bdf656ce5715a455d3fb36427878ae507d8d4bd9a7f092a6b9752196f9ebc4b
4
+ data.tar.gz: cd21540e0eea82c1567c085b9ef2fd5c2d9742cd1ff08f855a16f4109893ebb2
5
5
  SHA512:
6
- metadata.gz: 2d81f0ab86954523f8b3511f3105f8005b9709f0a46fb08f0403dcac841c70efcfbff90dcdedccaf322554280f62bdbab6a36be34d0e787f01734d8ee2219bf6
7
- data.tar.gz: 31d048ad3d7493be0c52a24729d033ce58a891039c180945aa80d54afb7a529b6e420ccfc49fdab32ef00803483ecbb63dd2c2bd9d15f569c44debfdfd6dea0c
6
+ metadata.gz: 992c191cbc6bbdd1bc770a83d4a1151869c4f57a20d7236a19b0add9c50329ec2dea901fe440488b2194fe96951d729c4509d33a9df8fb6c2dc7154c570e9c00
7
+ data.tar.gz: 82e67a674db952801adf40407d34daf0e2808f4c1b4d99da60f63c33fd2033b7f5a2deaefe63ea91bf4bcc7129e7580772877df3ca3b69bf9894d2454a5bdf38
data/LICENSE ADDED
@@ -0,0 +1,45 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Vicente Services SL
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ This project is a Ruby port of the original Python [DSPy library](https://github.com/stanfordnlp/dspy), which is licensed under the MIT License:
24
+
25
+ MIT License
26
+
27
+ Copyright (c) 2023 Stanford Future Data Systems
28
+
29
+ Permission is hereby granted, free of charge, to any person obtaining a copy
30
+ of this software and associated documentation files (the "Software"), to deal
31
+ in the Software without restriction, including without limitation the rights
32
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
33
+ copies of the Software, and to permit persons to whom the Software is
34
+ furnished to do so, subject to the following conditions:
35
+
36
+ The above copyright notice and this permission notice shall be included in all
37
+ copies or substantial portions of the Software.
38
+
39
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
41
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
42
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
43
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
44
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
45
+ SOFTWARE.
data/README.md CHANGED
@@ -5,26 +5,62 @@
5
5
  [![Build Status](https://img.shields.io/github/actions/workflow/status/vicentereig/dspy.rb/ruby.yml?branch=main&label=build)](https://github.com/vicentereig/dspy.rb/actions/workflows/ruby.yml)
6
6
  [![Documentation](https://img.shields.io/badge/docs-vicentereig.github.io%2Fdspy.rb-blue)](https://vicentereig.github.io/dspy.rb/)
7
7
 
8
+ > [!NOTE]
9
+ > The core Prompt Engineering Framework is production-ready with
10
+ > comprehensive documentation. I am focusing now on educational content on systematic Prompt Optimization and Context Engineering.
11
+ > Your feedback is invaluable. if you encounter issues, please open an [issue](https://github.com/vicentereig/dspy.rb/issues). If you have suggestions, open a [new thread](https://github.com/vicentereig/dspy.rb/discussions).
12
+ >
13
+ > If you want to contribute, feel free to reach out to me to coordinate efforts: hey at vicente.services
14
+ >
15
+ > And, yes, this is 100% a legit project. :)
16
+
17
+
8
18
  **Build reliable LLM applications in idiomatic Ruby using composable, type-safe modules.**
9
19
 
10
- The Ruby framework for programming with large language models. DSPy.rb brings structured LLM programming to Ruby developers. Instead of wrestling with prompt strings and parsing responses, you define typed signatures using idiomatic Ruby to compose and decompose AI Worklows and AI Agents.
20
+ The Ruby framework for programming with large language models. DSPy.rb brings structured LLM programming to Ruby developers, programmatic Prompt Engineering and Context Engineering.
21
+ Instead of wrestling with prompt strings and parsing responses, you define typed signatures using idiomatic Ruby to compose and decompose AI Worklows and AI Agents.
11
22
 
12
23
  **Prompts are the just Functions.** Traditional prompting is like writing code with string concatenation: it works until it doesn't. DSPy.rb brings you
13
24
  the programming approach pioneered by [dspy.ai](https://dspy.ai/): instead of crafting fragile prompts, you define modular
14
25
  signatures and let the framework handle the messy details.
15
26
 
16
27
  DSPy.rb is an idiomatic Ruby surgical port of Stanford's [DSPy framework](https://github.com/stanfordnlp/dspy). While implementing
17
- the core concepts of signatures, predictors, and optimization from the original Python library, DSPy.rb embraces Ruby
18
- conventions and adds Ruby-specific innovations like CodeAct agents and enhanced production instrumentation.
28
+ the core concepts of signatures, predictors, and the main optimization algorithms from the original Python library, DSPy.rb embraces Ruby
29
+ conventions and adds Ruby-specific innovations like Sorbet-base Typed system, ReAct loops, and production-ready integrations like non-blocking Open Telemetry Instrumentation.
30
+
31
+ **What you get?** Ruby LLM applications that actually scale and don't break when you sneeze.
19
32
 
20
- The result? LLM applications that actually scale and don't break when you sneeze.
33
+ Check the [examples](examples/) and take them for a spin!
21
34
 
22
35
  ## Your First DSPy Program
36
+ ### Installation
37
+
38
+ Add to your Gemfile:
23
39
 
24
40
  ```ruby
25
- # Define a signature for sentiment classification
41
+ gem 'dspy'
42
+ ```
43
+
44
+ and
45
+
46
+ ```bash
47
+ bundle install
48
+ ```
49
+
50
+ ### Your First Reliable Predictor
51
+
52
+ ```ruby
53
+
54
+ # Configure DSPy globablly to use your fave LLM - you can override this on an instance levle.
55
+ DSPy.configure do |c|
56
+ c.lm = DSPy::LM.new('openai/gpt-4o-mini',
57
+ api_key: ENV['OPENAI_API_KEY'],
58
+ structured_outputs: true) # Enable OpenAI's native JSON mode
59
+ end
60
+
61
+ # Define a signature for sentiment classification - instead of writing a full prompt!
26
62
  class Classify < DSPy::Signature
27
- description "Classify sentiment of a given sentence."
63
+ description "Classify sentiment of a given sentence." # sets the goal of the underlying prompt
28
64
 
29
65
  class Sentiment < T::Enum
30
66
  enums do
@@ -33,26 +69,83 @@ class Classify < DSPy::Signature
33
69
  Neutral = new('neutral')
34
70
  end
35
71
  end
36
-
72
+
73
+ # Structured Inputs: makes sure you are sending only valid prompt inputs to your model
37
74
  input do
38
- const :sentence, String
75
+ const :sentence, String, description: 'The sentence to analyze'
39
76
  end
40
77
 
78
+ # Structured Outputs: your predictor will validate the output of the model too.
41
79
  output do
42
- const :sentiment, Sentiment
43
- const :confidence, Float
80
+ const :sentiment, Sentiment, description: 'The sentiment of the sentence'
81
+ const :confidence, Float, description: 'A number between 0.0 and 1.0'
44
82
  end
45
83
  end
46
84
 
47
- # Configure DSPy with your LLM
85
+ # Wire it to the simplest prompting technique - a Predictn.
86
+ classify = DSPy::Predict.new(Classify)
87
+ # it may raise an error if you mess the inputs or your LLM messes the outputs.
88
+ result = classify.call(sentence: "This book was super fun to read!")
89
+
90
+ puts result.sentiment # => #<Sentiment::Positive>
91
+ puts result.confidence # => 0.85
92
+ ```
93
+
94
+ ### Sibling Gems
95
+
96
+ DSPy.rb ships multiple gems from this monorepo so you can opt into features with heavier dependency trees (e.g., datasets pull in Polars/Arrow, MIPROv2 requires `numo-*` BLAS bindings) only when you need them. Add these alongside `dspy`:
97
+
98
+ | Gem | Description | Status |
99
+ | --- | --- | --- |
100
+ | `dspy-schema` | Exposes `DSPy::TypeSystem::SorbetJsonSchema` for downstream reuse. (Still required by the core `dspy` gem; extraction lets other projects depend on it directly.) | **Stable** (v1.0.0) |
101
+ | `dspy-code_act` | Think-Code-Observe agents that synthesize and execute Ruby safely. (Add the gem or set `DSPY_WITH_CODE_ACT=1` before requiring `dspy/code_act`.) | **Stable** (v1.0.0) |
102
+ | `dspy-datasets` | Dataset helpers plus Parquet/Polars tooling for richer evaluation corpora. (Toggle via `DSPY_WITH_DATASETS`.) | **Stable** (v1.0.0) |
103
+ | `dspy-evals` | High-throughput evaluation harness with metrics, callbacks, and regression fixtures. (Toggle via `DSPY_WITH_EVALS`.) | **Stable** (v1.0.0) |
104
+ | `dspy-miprov2` | Bayesian optimization + Gaussian Process backend for the MIPROv2 teleprompter. (Install or export `DSPY_WITH_MIPROV2=1` before requiring the teleprompter.) | **Stable** (v1.0.0) |
105
+ | `dspy-gepa` | `DSPy::Teleprompt::GEPA`, reflection loops, experiment tracking, telemetry adapters. (Install or set `DSPY_WITH_GEPA=1`.) | **Stable** (v1.0.0) |
106
+ | `gepa` | GEPA optimizer core (Pareto engine, telemetry, reflective proposer). | **Stable** (v1.0.0) |
107
+ | `dspy-o11y` | Core observability APIs: `DSPy::Observability`, async span processor, observation types. (Install or set `DSPY_WITH_O11Y=1`.) | **Stable** (v1.0.0) |
108
+ | `dspy-o11y-langfuse` | Auto-configures DSPy observability to stream spans to Langfuse via OTLP. (Install or set `DSPY_WITH_O11Y_LANGFUSE=1`.) | **Stable** (v1.0.0) |
109
+
110
+ Set the matching `DSPY_WITH_*` environment variables (see `Gemfile`) to include or exclude each sibling gem when running Bundler locally (for example `DSPY_WITH_GEPA=1` or `DSPY_WITH_O11Y_LANGFUSE=1`). Refer to `adr/013-dependency-tree.md` for the full dependency map and roadmap.
111
+ ### Your First Reliable Predictor
112
+
113
+ ```ruby
114
+
115
+ # Configure DSPy globablly to use your fave LLM - you can override this on an instance levle.
48
116
  DSPy.configure do |c|
49
- c.lm = DSPy::LM.new('openai/gpt-4o-mini',
117
+ c.lm = DSPy::LM.new('openai/gpt-4o-mini',
50
118
  api_key: ENV['OPENAI_API_KEY'],
51
119
  structured_outputs: true) # Enable OpenAI's native JSON mode
52
120
  end
53
121
 
54
- # Create the predictor and run inference
122
+ # Define a signature for sentiment classification - instead of writing a full prompt!
123
+ class Classify < DSPy::Signature
124
+ description "Classify sentiment of a given sentence." # sets the goal of the underlying prompt
125
+
126
+ class Sentiment < T::Enum
127
+ enums do
128
+ Positive = new('positive')
129
+ Negative = new('negative')
130
+ Neutral = new('neutral')
131
+ end
132
+ end
133
+
134
+ # Structured Inputs: makes sure you are sending only valid prompt inputs to your model
135
+ input do
136
+ const :sentence, String, description: 'The sentence to analyze'
137
+ end
138
+
139
+ # Structured Outputs: your predictor will validate the output of the model too.
140
+ output do
141
+ const :sentiment, Sentiment, description: 'The sentiment of the sentence'
142
+ const :confidence, Float, description: 'A number between 0.0 and 1.0'
143
+ end
144
+ end
145
+
146
+ # Wire it to the simplest prompting technique - a Predictn.
55
147
  classify = DSPy::Predict.new(Classify)
148
+ # it may raise an error if you mess the inputs or your LLM messes the outputs.
56
149
  result = classify.call(sentence: "This book was super fun to read!")
57
150
 
58
151
  puts result.sentiment # => #<Sentiment::Positive>
@@ -99,12 +192,22 @@ end
99
192
 
100
193
  ## What You Get
101
194
 
195
+ **Developer Experience:**
196
+ - LLM provider support using official Ruby clients:
197
+ - [OpenAI Ruby](https://github.com/openai/openai-ruby) with vision model support
198
+ - [Anthropic Ruby SDK](https://github.com/anthropics/anthropic-sdk-ruby) with multimodal capabilities
199
+ - [Google Gemini API](https://ai.google.dev/) with native structured outputs
200
+ - [Ollama](https://ollama.com/) via OpenAI compatibility layer for local models
201
+ - **Multimodal Support** - Complete image analysis with DSPy::Image, type-safe bounding boxes, vision-capable models
202
+ - Runtime type checking with [Sorbet](https://sorbet.org/) including T::Enum and union types
203
+ - Type-safe tool definitions for ReAct agents
204
+ - Comprehensive instrumentation and observability
205
+
102
206
  **Core Building Blocks:**
103
207
  - **Signatures** - Define input/output schemas using Sorbet types with T::Enum and union type support
104
208
  - **Predict** - LLM completion with structured data extraction and multimodal support
105
209
  - **Chain of Thought** - Step-by-step reasoning for complex problems with automatic prompt optimization
106
210
  - **ReAct** - Tool-using agents with type-safe tool definitions and error recovery
107
- - **CodeAct** - Dynamic code execution agents for programming tasks
108
211
  - **Module Composition** - Combine multiple LLM calls into production-ready workflows
109
212
 
110
213
  **Optimization & Evaluation:**
@@ -122,24 +225,40 @@ end
122
225
  - **File-based Storage** - Optimization result persistence with versioning
123
226
  - **Structured Logging** - JSON and key=value formats with span tracking
124
227
 
125
- **Developer Experience:**
126
- - LLM provider support using official Ruby clients:
127
- - [OpenAI Ruby](https://github.com/openai/openai-ruby) with vision model support
128
- - [Anthropic Ruby SDK](https://github.com/anthropics/anthropic-sdk-ruby) with multimodal capabilities
129
- - [Google Gemini API](https://ai.google.dev/) with native structured outputs
130
- - [Ollama](https://ollama.com/) via OpenAI compatibility layer for local models
131
- - **Multimodal Support** - Complete image analysis with DSPy::Image, type-safe bounding boxes, vision-capable models
132
- - Runtime type checking with [Sorbet](https://sorbet.org/) including T::Enum and union types
133
- - Type-safe tool definitions for ReAct agents
134
- - Comprehensive instrumentation and observability
228
+ ## Recent Achievements
229
+
230
+ DSPy.rb has rapidly evolved from experimental to production-ready:
135
231
 
136
- ## Development Status
232
+ ### Foundation
233
+ - ✅ **JSON Parsing Reliability** - Native OpenAI structured outputs with adaptive retry logic and schema-aware fallbacks
234
+ - ✅ **Type-Safe Strategy Configuration** - Provider-optimized strategy selection and enum-backed optimizer presets
235
+ - ✅ **Core Module System** - Predict, ChainOfThought, ReAct with type safety (add `dspy-code_act` for Think-Code-Observe agents)
236
+ - ✅ **Production Observability** - OpenTelemetry, New Relic, and Langfuse integration
237
+ - ✅ **Advanced Optimization** - MIPROv2 with Bayesian optimization, Gaussian Processes, and multi-mode search
137
238
 
138
- DSPy.rb is actively developed and approaching stability. The core framework is production-ready with
139
- comprehensive documentation, but I'm battle-testing features through the 0.x series before committing
140
- to a stable v1.0 API.
239
+ ### Recent Advances
240
+ - **MIPROv2 ADE Integrity (v0.29.1)** - Stratified train/val/test splits, honest precision accounting, and enum-driven `--auto` presets with integration coverage
241
+ - **Instruction Deduplication (v0.29.1)** - Candidate generation now filters repeated programs so optimization logs highlight unique strategies
242
+ - ✅ **GEPA Teleprompter (v0.29.0)** - Genetic-Pareto reflective prompt evolution with merge proposer scheduling, reflective mutation, and ADE demo parity
243
+ - ✅ **Optimizer Utilities Parity (v0.29.0)** - Bootstrap strategies, dataset summaries, and Layer 3 utilities unlock multi-predictor programs on Ruby
244
+ - ✅ **Observability Hardening (v0.29.0)** - OTLP exporter runs on a single-thread executor preventing frozen SSL contexts without blocking spans
245
+ - ✅ **Documentation Refresh (v0.29.x)** - New GEPA guide plus ADE optimization docs covering presets, stratified splits, and error-handling defaults
246
+
247
+ **Current Focus Areas:**
248
+
249
+ ### Production Readiness
250
+ - 🚧 **Production Patterns** - Real-world usage validation and performance optimization
251
+ - 🚧 **Ruby Ecosystem Integration** - Rails integration, Sidekiq compatibility, deployment patterns
252
+
253
+ ### Community & Adoption
254
+ - 🚧 **Community Examples** - Real-world applications and case studies
255
+ - 🚧 **Contributor Experience** - Making it easier to contribute and extend
256
+ - 🚧 **Performance Benchmarks** - Comparative analysis vs other frameworks
257
+
258
+ **v1.0 Philosophy:**
259
+ v1.0 will be released after extensive production battle-testing, not after checking off features.
260
+ The API is already stable - v1.0 represents confidence in production reliability backed by real-world validation.
141
261
 
142
- Real-world usage feedback is invaluable - if you encounter issues or have suggestions, please open a GitHub issue!
143
262
 
144
263
  ## Documentation
145
264
 
@@ -156,92 +275,37 @@ For LLMs and AI assistants working with DSPy.rb:
156
275
  - **[Quick Start Guide](docs/src/getting-started/quick-start.md)** - Your first DSPy programs
157
276
  - **[Core Concepts](docs/src/getting-started/core-concepts.md)** - Understanding signatures, predictors, and modules
158
277
 
159
- ### Core Features
278
+ ### Prompt Engineering
160
279
  - **[Signatures & Types](docs/src/core-concepts/signatures.md)** - Define typed interfaces for LLM operations
161
280
  - **[Predictors](docs/src/core-concepts/predictors.md)** - Predict, ChainOfThought, ReAct, and more
162
281
  - **[Modules & Pipelines](docs/src/core-concepts/modules.md)** - Compose complex multi-stage workflows
163
282
  - **[Multimodal Support](docs/src/core-concepts/multimodal.md)** - Image analysis with vision-capable models
164
283
  - **[Examples & Validation](docs/src/core-concepts/examples.md)** - Type-safe training data
284
+ - **[Rich Types](docs/src/advanced/complex-types.md)** - Sorbet type integration with automatic coercion for structs, enums, and arrays
285
+ - **[Composable Pipelines](docs/src/advanced/pipelines.md)** - Manual module composition patterns
165
286
 
166
- ### Optimization
287
+ ### Prompt Optimization
167
288
  - **[Evaluation Framework](docs/src/optimization/evaluation.md)** - Advanced metrics beyond simple accuracy
168
289
  - **[Prompt Optimization](docs/src/optimization/prompt-optimization.md)** - Manipulate prompts as objects
169
290
  - **[MIPROv2 Optimizer](docs/src/optimization/miprov2.md)** - Advanced Bayesian optimization with Gaussian Processes
170
291
  - **[GEPA Optimizer](docs/src/optimization/gepa.md)** *(beta)* - Reflective mutation with optional reflection LMs
171
292
 
172
- ### Production Features
173
- - **[Storage System](docs/src/production/storage.md)** - Persistence and optimization result storage
174
- - **[Observability](docs/src/production/observability.md)** - Zero-config Langfuse integration with a dedicated export worker that never blocks your LLMs
175
-
176
- ### Advanced Usage
177
- - **[Complex Types](docs/src/advanced/complex-types.md)** - Sorbet type integration with automatic coercion for structs, enums, and arrays
178
- - **[Manual Pipelines](docs/src/advanced/pipelines.md)** - Manual module composition patterns
293
+ ### Context Engineering
294
+ - **[Tools](docs/src/core-concepts/toolsets.md)** - Tool wieldint agents.
295
+ - **[Agentic Memory](docs/src/core-concepts/memory.md)** - Memory Tools & Agentic Loops
179
296
  - **[RAG Patterns](docs/src/advanced/rag.md)** - Manual RAG implementation with external services
180
- - **[Custom Metrics](docs/src/advanced/custom-metrics.md)** - Proc-based evaluation logic
181
297
 
182
- ## Quick Start
183
-
184
- ### Installation
185
-
186
- Add to your Gemfile:
187
-
188
- ```ruby
189
- gem 'dspy'
190
- ```
191
-
192
- Then run:
193
-
194
- ```bash
195
- bundle install
196
- ```
197
-
198
- ## Recent Achievements
199
-
200
- DSPy.rb has rapidly evolved from experimental to production-ready:
201
-
202
- ### Foundation
203
- - ✅ **JSON Parsing Reliability** - Native OpenAI structured outputs, strategy selection, retry logic
204
- - ✅ **Type-Safe Strategy Configuration** - Provider-optimized automatic strategy selection
205
- - ✅ **Core Module System** - Predict, ChainOfThought, ReAct, CodeAct with type safety
206
- - ✅ **Production Observability** - OpenTelemetry, New Relic, and Langfuse integration
207
- - ✅ **Advanced Optimization** - MIPROv2 with Bayesian optimization, Gaussian Processes, and multiple strategies
208
-
209
- ### Recent Advances
210
- - ✅ **Enhanced Langfuse Integration (v0.25.0)** - Comprehensive OpenTelemetry span reporting with proper input/output, hierarchical nesting, accurate timing, and observation types
211
- - ✅ **Comprehensive Multimodal Framework** - Complete image analysis with `DSPy::Image`, type-safe bounding boxes, vision model integration
212
- - ✅ **Advanced Type System** - `T::Enum` integration, union types for agentic workflows, complex type coercion
213
- - ✅ **Production-Ready Evaluation** - Multi-factor metrics beyond accuracy, error-resilient evaluation pipelines
214
- - ✅ **Documentation Ecosystem** - `llms.txt` for AI assistants, ADRs, blog articles, comprehensive examples
215
- - ✅ **API Maturation** - Simplified idiomatic patterns, better error handling, production-proven designs
298
+ ### Production Features
299
+ - **[Observability](docs/src/production/observability.md)** - Zero-config Langfuse integration with a dedicated export worker that never blocks your LLMs
300
+ - **[Storage System](docs/src/production/storage.md)** - Persistence and optimization result storage
301
+ - **[Custom Metrics](docs/src/advanced/custom-metrics.md)** - Proc-based evaluation logic
216
302
 
217
- ## Roadmap - Production Battle-Testing Toward v1.0
218
303
 
219
- DSPy.rb has transitioned from **feature building** to **production validation**. The core framework is
220
- feature-complete and stable - now I'm focusing on real-world usage patterns, performance optimization,
221
- and ecosystem integration.
222
304
 
223
- **Current Focus Areas:**
224
305
 
225
- ### Production Readiness
226
- - 🚧 **Production Patterns** - Real-world usage validation and performance optimization
227
- - 🚧 **Ruby Ecosystem Integration** - Rails integration, Sidekiq compatibility, deployment patterns
228
- - 🚧 **Scale Testing** - High-volume usage, memory management, connection pooling
229
- - 🚧 **Error Recovery** - Robust failure handling patterns for production environments
230
306
 
231
- ### Ecosystem Expansion
232
- - 🚧 **Model Context Protocol (MCP)** - Integration with MCP ecosystem
233
- - 🚧 **Additional Provider Support** - Azure OpenAI, local models beyond Ollama
234
- - 🚧 **Tool Ecosystem** - Expanded tool integrations for ReAct agents
235
307
 
236
- ### Community & Adoption
237
- - 🚧 **Community Examples** - Real-world applications and case studies
238
- - 🚧 **Contributor Experience** - Making it easier to contribute and extend
239
- - 🚧 **Performance Benchmarks** - Comparative analysis vs other frameworks
240
308
 
241
- **v1.0 Philosophy:**
242
- v1.0 will be released after extensive production battle-testing, not after checking off features.
243
- The API is already stable - v1.0 represents confidence in production reliability backed by real-world validation.
244
309
 
245
310
  ## License
246
-
247
311
  This project is licensed under the MIT License.
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
3
4
  require 'sorbet-runtime'
4
5
 
5
6
  module DSPy
@@ -42,48 +43,60 @@ module DSPy
42
43
  end
43
44
 
44
45
  module ClassMethods
46
+ # Configures a method to expose before callbacks.
47
+ #
48
+ # @param method_name [Symbol] the target method.
49
+ # @param wrap [Boolean] When true (default), the method is transparently wrapped so callbacks
50
+ # run automatically on every invocation. Pass false when you plan to trigger callbacks
51
+ # manually (e.g., to interleave custom spans or thread management). Manual targets are
52
+ # never re-wrapped, so execution order stays entirely in your control.
45
53
  # Creates a before callback hook for the specified method
46
54
  #
47
55
  # @param method_name [Symbol] the method to add callback support to
48
- def create_before_callback(method_name)
49
- mark_method_has_callbacks(method_name)
56
+ def create_before_callback(method_name, wrap: true)
57
+ mark_method_has_callbacks(method_name, wrap: wrap)
50
58
  ensure_callback_method_defined(:before, method_name)
51
- wrap_method_with_callbacks(method_name)
59
+ wrap_method_with_callbacks(method_name) if wrap
52
60
  end
53
61
 
54
62
  # Creates an after callback hook for the specified method
55
63
  #
56
64
  # @param method_name [Symbol] the method to add callback support to
57
- def create_after_callback(method_name)
58
- mark_method_has_callbacks(method_name)
65
+ def create_after_callback(method_name, wrap: true)
66
+ mark_method_has_callbacks(method_name, wrap: wrap)
59
67
  ensure_callback_method_defined(:after, method_name)
60
- wrap_method_with_callbacks(method_name)
68
+ wrap_method_with_callbacks(method_name) if wrap
61
69
  end
62
70
 
63
71
  # Creates an around callback hook for the specified method
64
72
  #
65
73
  # @param method_name [Symbol] the method to add callback support to
66
- def create_around_callback(method_name)
67
- mark_method_has_callbacks(method_name)
74
+ def create_around_callback(method_name, wrap: true)
75
+ mark_method_has_callbacks(method_name, wrap: wrap)
68
76
  ensure_callback_method_defined(:around, method_name)
69
- wrap_method_with_callbacks(method_name)
77
+ wrap_method_with_callbacks(method_name) if wrap
70
78
  end
71
79
 
72
80
  private
73
81
 
74
82
  # Ensures the callback registration method exists
75
83
  def ensure_callback_method_defined(type, target_method_name)
84
+ set_default_callback_target(type, target_method_name)
85
+
76
86
  return if singleton_class.method_defined?(type)
77
87
 
78
- define_singleton_method(type) do |callback_method|
79
- register_callback(type, target_method_name, callback_method)
88
+ define_singleton_method(type) do |callback_method = nil, target: default_callback_target(type), &block|
89
+ callback = callback_method || block
90
+ raise ArgumentError, "No callback provided for #{type}" unless callback
91
+
92
+ register_callback(type, target || default_callback_target(type), callback)
80
93
  end
81
94
  end
82
95
 
83
96
  # Registers a callback for execution
84
- def register_callback(type, method_name, callback_method)
97
+ def register_callback(type, method_name, callback)
85
98
  own_callbacks_for(method_name)[type] ||= []
86
- own_callbacks_for(method_name)[type] << callback_method
99
+ own_callbacks_for(method_name)[type] << callback
87
100
  end
88
101
 
89
102
  # Returns own callbacks (not including parent)
@@ -93,8 +106,9 @@ module DSPy
93
106
  end
94
107
 
95
108
  # Marks that a method has callback support (even if no callbacks registered yet)
96
- def mark_method_has_callbacks(method_name)
109
+ def mark_method_has_callbacks(method_name, wrap: true)
97
110
  own_callbacks_for(method_name)
111
+ manual_callback_targets << method_name unless wrap
98
112
  end
99
113
 
100
114
  # Returns the callback registry for a method
@@ -164,6 +178,7 @@ module DSPy
164
178
 
165
179
  return unless has_callback_support
166
180
  return if method_wrapped?(method_name)
181
+ return if manual_callback_targets.include?(method_name)
167
182
 
168
183
  wrap_method_with_callbacks(method_name)
169
184
  end
@@ -191,29 +206,88 @@ module DSPy
191
206
  def method_wrapped?(method_name)
192
207
  @wrapped_methods&.include?(method_name)
193
208
  end
209
+
210
+ def manual_callback_targets
211
+ @manual_callback_targets ||= Set.new
212
+ end
213
+
214
+ def callback_defaults
215
+ @callback_defaults ||= {}
216
+ end
217
+
218
+ def set_default_callback_target(type, method_name)
219
+ callback_defaults[type] ||= method_name
220
+ end
221
+
222
+ def default_callback_target(type)
223
+ callback_defaults[type] || begin
224
+ if superclass.respond_to?(:default_callback_target, true)
225
+ superclass.send(:default_callback_target, type)
226
+ end
227
+ end
228
+ end
194
229
  end
195
230
 
196
231
  private
197
232
 
198
233
  # Executes callbacks of a specific type
199
- def run_callbacks(type, method_name)
234
+ def run_callbacks(type, method_name, payload = nil)
200
235
  callbacks = self.class.send(:callbacks_for, method_name)[type]
201
236
  return unless callbacks
202
237
 
203
- callbacks.each do |callback_method|
204
- send(callback_method)
238
+ callbacks.each do |callback|
239
+ case callback
240
+ when Symbol
241
+ method_obj = method(callback)
242
+ if method_obj.arity.zero?
243
+ send(callback)
244
+ else
245
+ send(callback, payload)
246
+ end
247
+ when Proc
248
+ if callback.arity.zero?
249
+ instance_exec(&callback)
250
+ else
251
+ instance_exec(payload, &callback)
252
+ end
253
+ else
254
+ callback.call(payload)
255
+ end
205
256
  end
206
257
  end
207
258
 
208
259
  # Executes method with around callbacks
209
260
  def execute_with_around_callbacks(method_name, original_method, *args, **kwargs, &block)
210
261
  callbacks = self.class.send(:callbacks_for, method_name)[:around]
262
+ args_copy = args.dup
263
+ kwargs_copy = kwargs.dup
211
264
 
212
265
  # Build callback chain from innermost (original method) to outermost
213
266
  chain = callbacks.reverse.inject(
214
267
  -> { original_method.bind(self).call(*args, **kwargs, &block) }
215
- ) do |inner, callback_method|
216
- -> { send(callback_method) { inner.call } }
268
+ ) do |inner, callback|
269
+ if callback.is_a?(Proc)
270
+ -> do
271
+ next_proc = -> { inner.call }
272
+ proc_arity = callback.arity
273
+ expects_extra = proc_arity.abs > 1
274
+
275
+ if expects_extra
276
+ instance_exec(next_proc, args_copy, kwargs_copy, &callback)
277
+ else
278
+ instance_exec(next_proc, &callback)
279
+ end
280
+ end
281
+ else
282
+ -> do
283
+ method_obj = method(callback)
284
+ if method_obj.arity.zero?
285
+ send(callback) { inner.call }
286
+ else
287
+ send(callback, args_copy, kwargs_copy) { inner.call }
288
+ end
289
+ end
290
+ end
217
291
  end
218
292
 
219
293
  chain.call