@every-env/compound-plugin 0.2.0 → 0.5.0

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 (100) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.github/workflows/ci.yml +1 -1
  3. package/.github/workflows/deploy-docs.yml +3 -3
  4. package/.github/workflows/publish.yml +37 -0
  5. package/README.md +12 -3
  6. package/docs/index.html +13 -13
  7. package/docs/pages/changelog.html +39 -0
  8. package/docs/plans/2026-02-08-feat-convert-local-md-settings-for-opencode-codex-plan.md +143 -0
  9. package/docs/plans/2026-02-08-feat-simplify-plugin-settings-plan.md +195 -0
  10. package/docs/plans/2026-02-08-refactor-reduce-plugin-context-token-usage-plan.md +212 -0
  11. package/docs/plans/2026-02-09-refactor-dspy-ruby-skill-update-plan.md +104 -0
  12. package/docs/plans/2026-02-12-feat-add-cursor-cli-target-provider-plan.md +306 -0
  13. package/docs/specs/cursor.md +85 -0
  14. package/package.json +1 -1
  15. package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
  16. package/plugins/compound-engineering/CHANGELOG.md +64 -0
  17. package/plugins/compound-engineering/README.md +5 -3
  18. package/plugins/compound-engineering/agents/design/design-implementation-reviewer.md +16 -1
  19. package/plugins/compound-engineering/agents/design/design-iterator.md +28 -1
  20. package/plugins/compound-engineering/agents/design/figma-design-sync.md +19 -1
  21. package/plugins/compound-engineering/agents/docs/ankane-readme-writer.md +16 -1
  22. package/plugins/compound-engineering/agents/research/best-practices-researcher.md +16 -1
  23. package/plugins/compound-engineering/agents/research/framework-docs-researcher.md +16 -1
  24. package/plugins/compound-engineering/agents/research/git-history-analyzer.md +16 -1
  25. package/plugins/compound-engineering/agents/research/learnings-researcher.md +22 -1
  26. package/plugins/compound-engineering/agents/research/repo-research-analyst.md +22 -1
  27. package/plugins/compound-engineering/agents/review/agent-native-reviewer.md +16 -1
  28. package/plugins/compound-engineering/agents/review/architecture-strategist.md +16 -1
  29. package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +16 -1
  30. package/plugins/compound-engineering/agents/review/data-integrity-guardian.md +16 -1
  31. package/plugins/compound-engineering/agents/review/data-migration-expert.md +16 -1
  32. package/plugins/compound-engineering/agents/review/deployment-verification-agent.md +16 -1
  33. package/plugins/compound-engineering/agents/review/dhh-rails-reviewer.md +22 -1
  34. package/plugins/compound-engineering/agents/review/julik-frontend-races-reviewer.md +20 -21
  35. package/plugins/compound-engineering/agents/review/kieran-python-reviewer.md +30 -1
  36. package/plugins/compound-engineering/agents/review/kieran-rails-reviewer.md +30 -1
  37. package/plugins/compound-engineering/agents/review/kieran-typescript-reviewer.md +30 -1
  38. package/plugins/compound-engineering/agents/review/pattern-recognition-specialist.md +16 -1
  39. package/plugins/compound-engineering/agents/review/performance-oracle.md +28 -1
  40. package/plugins/compound-engineering/agents/review/schema-drift-detector.md +16 -1
  41. package/plugins/compound-engineering/agents/review/security-sentinel.md +22 -1
  42. package/plugins/compound-engineering/agents/workflow/bug-reproduction-validator.md +16 -1
  43. package/plugins/compound-engineering/agents/workflow/every-style-editor.md +1 -1
  44. package/plugins/compound-engineering/agents/workflow/pr-comment-resolver.md +16 -1
  45. package/plugins/compound-engineering/agents/workflow/spec-flow-analyzer.md +22 -1
  46. package/plugins/compound-engineering/commands/agent-native-audit.md +1 -0
  47. package/plugins/compound-engineering/commands/changelog.md +1 -0
  48. package/plugins/compound-engineering/commands/create-agent-skill.md +1 -0
  49. package/plugins/compound-engineering/commands/deploy-docs.md +1 -0
  50. package/plugins/compound-engineering/commands/generate_command.md +1 -0
  51. package/plugins/compound-engineering/commands/heal-skill.md +1 -0
  52. package/plugins/compound-engineering/commands/lfg.md +1 -0
  53. package/plugins/compound-engineering/commands/report-bug.md +1 -0
  54. package/plugins/compound-engineering/commands/reproduce-bug.md +1 -0
  55. package/plugins/compound-engineering/commands/resolve_parallel.md +1 -0
  56. package/plugins/compound-engineering/commands/slfg.md +1 -0
  57. package/plugins/compound-engineering/commands/{xcode-test.md → test-xcode.md} +2 -1
  58. package/plugins/compound-engineering/commands/triage.md +1 -0
  59. package/plugins/compound-engineering/commands/workflows/brainstorm.md +6 -1
  60. package/plugins/compound-engineering/commands/workflows/compound.md +1 -0
  61. package/plugins/compound-engineering/commands/workflows/review.md +23 -21
  62. package/plugins/compound-engineering/commands/workflows/work.md +29 -15
  63. package/plugins/compound-engineering/skills/compound-docs/SKILL.md +1 -0
  64. package/plugins/compound-engineering/skills/dspy-ruby/SKILL.md +539 -396
  65. package/plugins/compound-engineering/skills/dspy-ruby/assets/config-template.rb +159 -331
  66. package/plugins/compound-engineering/skills/dspy-ruby/assets/module-template.rb +210 -236
  67. package/plugins/compound-engineering/skills/dspy-ruby/assets/signature-template.rb +173 -95
  68. package/plugins/compound-engineering/skills/dspy-ruby/references/core-concepts.md +552 -143
  69. package/plugins/compound-engineering/skills/dspy-ruby/references/observability.md +366 -0
  70. package/plugins/compound-engineering/skills/dspy-ruby/references/optimization.md +440 -460
  71. package/plugins/compound-engineering/skills/dspy-ruby/references/providers.md +305 -225
  72. package/plugins/compound-engineering/skills/dspy-ruby/references/toolsets.md +502 -0
  73. package/plugins/compound-engineering/skills/file-todos/SKILL.md +1 -0
  74. package/plugins/compound-engineering/skills/orchestrating-swarms/SKILL.md +1 -0
  75. package/plugins/compound-engineering/skills/setup/SKILL.md +168 -0
  76. package/plugins/compound-engineering/skills/skill-creator/SKILL.md +1 -0
  77. package/src/commands/convert.ts +10 -5
  78. package/src/commands/install.ts +10 -5
  79. package/src/converters/claude-to-codex.ts +9 -3
  80. package/src/converters/claude-to-cursor.ts +166 -0
  81. package/src/converters/claude-to-droid.ts +174 -0
  82. package/src/converters/claude-to-opencode.ts +9 -2
  83. package/src/parsers/claude.ts +4 -0
  84. package/src/targets/cursor.ts +48 -0
  85. package/src/targets/droid.ts +50 -0
  86. package/src/targets/index.ts +18 -0
  87. package/src/types/claude.ts +2 -0
  88. package/src/types/cursor.ts +29 -0
  89. package/src/types/droid.ts +20 -0
  90. package/tests/claude-parser.test.ts +24 -2
  91. package/tests/codex-converter.test.ts +100 -0
  92. package/tests/converter.test.ts +76 -0
  93. package/tests/cursor-converter.test.ts +347 -0
  94. package/tests/cursor-writer.test.ts +137 -0
  95. package/tests/droid-converter.test.ts +277 -0
  96. package/tests/droid-writer.test.ts +100 -0
  97. package/tests/fixtures/sample-plugin/commands/disabled-command.md +7 -0
  98. package/tests/fixtures/sample-plugin/skills/disabled-skill/SKILL.md +7 -0
  99. package/plugins/compound-engineering/commands/technical_review.md +0 -7
  100. /package/{plugins/compound-engineering → .claude}/commands/release-docs.md +0 -0
@@ -1,594 +1,737 @@
1
1
  ---
2
2
  name: dspy-ruby
3
- description: This skill should be used when working with DSPy.rb, a Ruby framework for building type-safe, composable LLM applications. Use this when implementing predictable AI features, creating LLM signatures and modules, configuring language model providers (OpenAI, Anthropic, Gemini, Ollama), building agent systems with tools, optimizing prompts, or testing LLM-powered functionality in Ruby applications.
3
+ description: Build type-safe LLM applications with DSPy.rb Ruby's programmatic prompt framework with signatures, modules, agents, and optimization. Use when implementing predictable AI features, creating LLM signatures and modules, configuring language model providers, building agent systems with tools, optimizing prompts, or testing LLM-powered functionality in Ruby applications.
4
4
  ---
5
5
 
6
- # DSPy.rb Expert
6
+ # DSPy.rb
7
7
 
8
- ## Overview
8
+ > Build LLM apps like you build software. Type-safe, modular, testable.
9
+
10
+ DSPy.rb brings software engineering best practices to LLM development. Instead of tweaking prompts, define what you want with Ruby types and let DSPy handle the rest.
9
11
 
10
- DSPy.rb is a Ruby framework that enables developers to **program LLMs, not prompt them**. Instead of manually crafting prompts, define application requirements through type-safe, composable modules that can be tested, optimized, and version-controlled like regular code.
12
+ ## Overview
11
13
 
12
- This skill provides comprehensive guidance on:
13
- - Creating type-safe signatures for LLM operations
14
- - Building composable modules and workflows
15
- - Configuring multiple LLM providers
16
- - Implementing agents with tools
17
- - Testing and optimizing LLM applications
18
- - Production deployment patterns
14
+ DSPy.rb is a Ruby framework for building language model applications with programmatic prompts. It provides:
19
15
 
20
- ## Core Capabilities
16
+ - **Type-safe signatures** — Define inputs/outputs with Sorbet types
17
+ - **Modular components** — Compose and reuse LLM logic
18
+ - **Automatic optimization** — Use data to improve prompts, not guesswork
19
+ - **Production-ready** — Built-in observability, testing, and error handling
21
20
 
22
- ### 1. Type-Safe Signatures
21
+ ## Core Concepts
23
22
 
24
- Create input/output contracts for LLM operations with runtime type checking.
23
+ ### 1. Signatures
25
24
 
26
- **When to use**: Defining any LLM task, from simple classification to complex analysis.
25
+ Define interfaces between your app and LLMs using Ruby types:
27
26
 
28
- **Quick reference**:
29
27
  ```ruby
30
- class EmailClassificationSignature < DSPy::Signature
31
- description "Classify customer support emails"
28
+ class EmailClassifier < DSPy::Signature
29
+ description "Classify customer support emails by category and priority"
30
+
31
+ class Priority < T::Enum
32
+ enums do
33
+ Low = new('low')
34
+ Medium = new('medium')
35
+ High = new('high')
36
+ Urgent = new('urgent')
37
+ end
38
+ end
32
39
 
33
40
  input do
34
- const :email_subject, String
35
- const :email_body, String
41
+ const :email_content, String
42
+ const :sender, String
36
43
  end
37
44
 
38
45
  output do
39
- const :category, T.enum(["Technical", "Billing", "General"])
40
- const :priority, T.enum(["Low", "Medium", "High"])
46
+ const :category, String
47
+ const :priority, Priority # Type-safe enum with defined values
48
+ const :confidence, Float
41
49
  end
42
50
  end
43
51
  ```
44
52
 
45
- **Templates**: See `assets/signature-template.rb` for comprehensive examples including:
46
- - Basic signatures with multiple field types
47
- - Vision signatures for multimodal tasks
48
- - Sentiment analysis signatures
49
- - Code generation signatures
50
-
51
- **Best practices**:
52
- - Always provide clear, specific descriptions
53
- - Use enums for constrained outputs
54
- - Include field descriptions with `desc:` parameter
55
- - Prefer specific types over generic String when possible
53
+ ### 2. Modules
56
54
 
57
- **Full documentation**: See `references/core-concepts.md` sections on Signatures and Type Safety.
55
+ Build complex workflows from simple building blocks:
58
56
 
59
- ### 2. Composable Modules
57
+ - **Predict** Basic LLM calls with signatures
58
+ - **ChainOfThought** — Step-by-step reasoning
59
+ - **ReAct** — Tool-using agents
60
+ - **CodeAct** — Dynamic code generation agents (install the `dspy-code_act` gem)
60
61
 
61
- Build reusable, chainable modules that encapsulate LLM operations.
62
+ ### 3. Tools & Toolsets
62
63
 
63
- **When to use**: Implementing any LLM-powered feature, especially complex multi-step workflows.
64
+ Create type-safe tools for agents with comprehensive Sorbet support:
64
65
 
65
- **Quick reference**:
66
66
  ```ruby
67
- class EmailProcessor < DSPy::Module
68
- def initialize
69
- super
70
- @classifier = DSPy::Predict.new(EmailClassificationSignature)
67
+ # Enum-based tool with automatic type conversion
68
+ class CalculatorTool < DSPy::Tools::Base
69
+ tool_name 'calculator'
70
+ tool_description 'Performs arithmetic operations with type-safe enum inputs'
71
+
72
+ class Operation < T::Enum
73
+ enums do
74
+ Add = new('add')
75
+ Subtract = new('subtract')
76
+ Multiply = new('multiply')
77
+ Divide = new('divide')
78
+ end
71
79
  end
72
80
 
73
- def forward(email_subject:, email_body:)
74
- @classifier.forward(
75
- email_subject: email_subject,
76
- email_body: email_body
77
- )
81
+ sig { params(operation: Operation, num1: Float, num2: Float).returns(T.any(Float, String)) }
82
+ def call(operation:, num1:, num2:)
83
+ case operation
84
+ when Operation::Add then num1 + num2
85
+ when Operation::Subtract then num1 - num2
86
+ when Operation::Multiply then num1 * num2
87
+ when Operation::Divide
88
+ return "Error: Division by zero" if num2 == 0
89
+ num1 / num2
90
+ end
78
91
  end
79
92
  end
80
- ```
81
93
 
82
- **Templates**: See `assets/module-template.rb` for comprehensive examples including:
83
- - Basic modules with single predictors
84
- - Multi-step pipelines that chain modules
85
- - Modules with conditional logic
86
- - Error handling and retry patterns
87
- - Stateful modules with history
88
- - Caching implementations
94
+ # Multi-tool toolset with rich types
95
+ class DataToolset < DSPy::Tools::Toolset
96
+ toolset_name "data_processing"
89
97
 
90
- **Module composition**: Chain modules together to create complex workflows:
91
- ```ruby
92
- class Pipeline < DSPy::Module
93
- def initialize
94
- super
95
- @step1 = Classifier.new
96
- @step2 = Analyzer.new
97
- @step3 = Responder.new
98
+ class Format < T::Enum
99
+ enums do
100
+ JSON = new('json')
101
+ CSV = new('csv')
102
+ XML = new('xml')
103
+ end
98
104
  end
99
105
 
100
- def forward(input)
101
- result1 = @step1.forward(input)
102
- result2 = @step2.forward(result1)
103
- @step3.forward(result2)
106
+ tool :convert, description: "Convert data between formats"
107
+ tool :validate, description: "Validate data structure"
108
+
109
+ sig { params(data: String, from: Format, to: Format).returns(String) }
110
+ def convert(data:, from:, to:)
111
+ "Converted from #{from.serialize} to #{to.serialize}"
112
+ end
113
+
114
+ sig { params(data: String, format: Format).returns(T::Hash[String, T.any(String, Integer, T::Boolean)]) }
115
+ def validate(data:, format:)
116
+ { valid: true, format: format.serialize, row_count: 42, message: "Data validation passed" }
104
117
  end
105
118
  end
106
119
  ```
107
120
 
108
- **Full documentation**: See `references/core-concepts.md` sections on Modules and Module Composition.
121
+ ### 4. Type System & Discriminators
122
+
123
+ DSPy.rb uses sophisticated type discrimination for complex data structures:
124
+
125
+ - **Automatic `_type` field injection** — DSPy adds discriminator fields to structs for type safety
126
+ - **Union type support** — `T.any()` types automatically disambiguated by `_type`
127
+ - **Reserved field name** — Avoid defining your own `_type` fields in structs
128
+ - **Recursive filtering** — `_type` fields filtered during deserialization at all nesting levels
129
+
130
+ ### 5. Optimization
109
131
 
110
- ### 3. Multiple Predictor Types
132
+ Improve accuracy with real data:
111
133
 
112
- Choose the right predictor for your task:
134
+ - **MIPROv2** Advanced multi-prompt optimization with bootstrap sampling and Bayesian optimization
135
+ - **GEPA** — Genetic-Pareto Reflective Prompt Evolution with feedback maps, experiment tracking, and telemetry
136
+ - **Evaluation** — Comprehensive framework with built-in and custom metrics, error handling, and batch processing
137
+
138
+ ## Quick Start
113
139
 
114
- **Predict**: Basic LLM inference with type-safe inputs/outputs
115
140
  ```ruby
116
- predictor = DSPy::Predict.new(TaskSignature)
117
- result = predictor.forward(input: "data")
141
+ # Install
142
+ gem 'dspy'
143
+
144
+ # Configure
145
+ DSPy.configure do |c|
146
+ c.lm = DSPy::LM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY'])
147
+ end
148
+
149
+ # Define a task
150
+ class SentimentAnalysis < DSPy::Signature
151
+ description "Analyze sentiment of text"
152
+
153
+ input do
154
+ const :text, String
155
+ end
156
+
157
+ output do
158
+ const :sentiment, String # positive, negative, neutral
159
+ const :score, Float # 0.0 to 1.0
160
+ end
161
+ end
162
+
163
+ # Use it
164
+ analyzer = DSPy::Predict.new(SentimentAnalysis)
165
+ result = analyzer.call(text: "This product is amazing!")
166
+ puts result.sentiment # => "positive"
167
+ puts result.score # => 0.92
118
168
  ```
119
169
 
120
- **ChainOfThought**: Adds automatic reasoning for improved accuracy
170
+ ## Provider Adapter Gems
171
+
172
+ Two strategies for connecting to LLM providers:
173
+
174
+ ### Per-provider adapters (direct SDK access)
175
+
121
176
  ```ruby
122
- predictor = DSPy::ChainOfThought.new(TaskSignature)
123
- result = predictor.forward(input: "data")
124
- # Returns: { reasoning: "...", output: "..." }
177
+ # Gemfile
178
+ gem 'dspy'
179
+ gem 'dspy-openai' # OpenAI, OpenRouter, Ollama
180
+ gem 'dspy-anthropic' # Claude
181
+ gem 'dspy-gemini' # Gemini
125
182
  ```
126
183
 
127
- **ReAct**: Tool-using agents with iterative reasoning
184
+ Each adapter gem pulls in the official SDK (`openai`, `anthropic`, `gemini-ai`).
185
+
186
+ ### Unified adapter via RubyLLM (recommended for multi-provider)
187
+
128
188
  ```ruby
129
- predictor = DSPy::ReAct.new(
130
- TaskSignature,
131
- tools: [SearchTool.new, CalculatorTool.new],
132
- max_iterations: 5
133
- )
189
+ # Gemfile
190
+ gem 'dspy'
191
+ gem 'dspy-ruby_llm' # Routes to any provider via ruby_llm
192
+ gem 'ruby_llm'
134
193
  ```
135
194
 
136
- **CodeAct**: Dynamic code generation (requires `dspy-code_act` gem)
195
+ RubyLLM handles provider routing based on the model name. Use the `ruby_llm/` prefix:
196
+
137
197
  ```ruby
138
- predictor = DSPy::CodeAct.new(TaskSignature)
139
- result = predictor.forward(task: "Calculate factorial of 5")
198
+ DSPy.configure do |c|
199
+ c.lm = DSPy::LM.new('ruby_llm/gemini-2.5-flash', structured_outputs: true)
200
+ # c.lm = DSPy::LM.new('ruby_llm/claude-sonnet-4-20250514', structured_outputs: true)
201
+ # c.lm = DSPy::LM.new('ruby_llm/gpt-4o-mini', structured_outputs: true)
202
+ end
140
203
  ```
141
204
 
142
- **When to use each**:
143
- - **Predict**: Simple tasks, classification, extraction
144
- - **ChainOfThought**: Complex reasoning, analysis, multi-step thinking
145
- - **ReAct**: Tasks requiring external tools (search, calculation, API calls)
146
- - **CodeAct**: Tasks best solved with generated code
147
-
148
- **Full documentation**: See `references/core-concepts.md` section on Predictors.
205
+ ## Events System
149
206
 
150
- ### 4. LLM Provider Configuration
207
+ DSPy.rb ships with a structured event bus for observing runtime behavior.
151
208
 
152
- Support for OpenAI, Anthropic Claude, Google Gemini, Ollama, and OpenRouter.
209
+ ### Module-Scoped Subscriptions (preferred for agents)
153
210
 
154
- **Quick configuration examples**:
155
211
  ```ruby
156
- # OpenAI
157
- DSPy.configure do |c|
158
- c.lm = DSPy::LM.new('openai/gpt-4o-mini',
159
- api_key: ENV['OPENAI_API_KEY'])
160
- end
212
+ class MyAgent < DSPy::Module
213
+ subscribe 'lm.tokens', :track_tokens, scope: :descendants
161
214
 
162
- # Anthropic Claude
163
- DSPy.configure do |c|
164
- c.lm = DSPy::LM.new('anthropic/claude-3-5-sonnet-20241022',
165
- api_key: ENV['ANTHROPIC_API_KEY'])
215
+ def track_tokens(_event, attrs)
216
+ @total_tokens += attrs.fetch(:total_tokens, 0)
217
+ end
166
218
  end
219
+ ```
167
220
 
168
- # Google Gemini
169
- DSPy.configure do |c|
170
- c.lm = DSPy::LM.new('gemini/gemini-1.5-pro',
171
- api_key: ENV['GOOGLE_API_KEY'])
172
- end
221
+ ### Global Subscriptions (for observability/integrations)
173
222
 
174
- # Local Ollama (free, private)
175
- DSPy.configure do |c|
176
- c.lm = DSPy::LM.new('ollama/llama3.1')
223
+ ```ruby
224
+ subscription_id = DSPy.events.subscribe('score.create') do |event, attrs|
225
+ Langfuse.export_score(attrs)
177
226
  end
227
+
228
+ # Wildcards supported
229
+ DSPy.events.subscribe('llm.*') { |name, attrs| puts "[#{name}] tokens=#{attrs[:total_tokens]}" }
178
230
  ```
179
231
 
180
- **Templates**: See `assets/config-template.rb` for comprehensive examples including:
181
- - Environment-based configuration
182
- - Multi-model setups for different tasks
183
- - Configuration with observability (OpenTelemetry, Langfuse)
184
- - Retry logic and fallback strategies
185
- - Budget tracking
186
- - Rails initializer patterns
232
+ Event names use dot-separated namespaces (`llm.generate`, `react.iteration_complete`). Every event includes module metadata (`module_path`, `module_leaf`, `module_scope.ancestry_token`) for filtering.
187
233
 
188
- **Provider compatibility matrix**:
234
+ ## Lifecycle Callbacks
189
235
 
190
- | Feature | OpenAI | Anthropic | Gemini | Ollama |
191
- |---------|--------|-----------|--------|--------|
192
- | Structured Output | ✅ | ✅ | ✅ | ✅ |
193
- | Vision (Images) | ✅ | ✅ | ✅ | ⚠️ Limited |
194
- | Image URLs | ✅ | ❌ | ❌ | ❌ |
195
- | Tool Calling | ✅ | ✅ | ✅ | Varies |
236
+ Rails-style lifecycle hooks ship with every `DSPy::Module`:
196
237
 
197
- **Cost optimization strategy**:
198
- - Development: Ollama (free) or gpt-4o-mini (cheap)
199
- - Testing: gpt-4o-mini with temperature=0.0
200
- - Production simple tasks: gpt-4o-mini, claude-3-haiku, gemini-1.5-flash
201
- - Production complex tasks: gpt-4o, claude-3-5-sonnet, gemini-1.5-pro
238
+ - **`before`** — Runs ahead of `forward` for setup (metrics, context loading)
239
+ - **`around`** Wraps `forward`, calls `yield`, and lets you pair setup/teardown logic
240
+ - **`after`** Fires after `forward` returns for cleanup or persistence
202
241
 
203
- **Full documentation**: See `references/providers.md` for all configuration options, provider-specific features, and troubleshooting.
242
+ ```ruby
243
+ class InstrumentedModule < DSPy::Module
244
+ before :setup_metrics
245
+ around :manage_context
246
+ after :log_metrics
204
247
 
205
- ### 5. Multimodal & Vision Support
248
+ def forward(question:)
249
+ @predictor.call(question: question)
250
+ end
206
251
 
207
- Process images alongside text using the unified `DSPy::Image` interface.
252
+ private
208
253
 
209
- **Quick reference**:
210
- ```ruby
211
- class VisionSignature < DSPy::Signature
212
- description "Analyze image and answer questions"
254
+ def setup_metrics
255
+ @start_time = Time.now
256
+ end
213
257
 
214
- input do
215
- const :image, DSPy::Image
216
- const :question, String
258
+ def manage_context
259
+ load_context
260
+ result = yield
261
+ save_context
262
+ result
217
263
  end
218
264
 
219
- output do
220
- const :answer, String
265
+ def log_metrics
266
+ duration = Time.now - @start_time
267
+ Rails.logger.info "Prediction completed in #{duration}s"
221
268
  end
222
269
  end
223
-
224
- predictor = DSPy::Predict.new(VisionSignature)
225
- result = predictor.forward(
226
- image: DSPy::Image.from_file("path/to/image.jpg"),
227
- question: "What objects are visible?"
228
- )
229
270
  ```
230
271
 
231
- **Image loading methods**:
232
- ```ruby
233
- # From file
234
- DSPy::Image.from_file("path/to/image.jpg")
272
+ Execution order: before → around (before yield) → forward → around (after yield) → after. Callbacks are inherited from parent classes and execute in registration order.
235
273
 
236
- # From URL (OpenAI only)
237
- DSPy::Image.from_url("https://example.com/image.jpg")
274
+ ## Fiber-Local LM Context
238
275
 
239
- # From base64
240
- DSPy::Image.from_base64(base64_data, mime_type: "image/jpeg")
241
- ```
276
+ Override the language model temporarily using fiber-local storage:
242
277
 
243
- **Provider support**:
244
- - OpenAI: Full support including URLs
245
- - Anthropic, Gemini: Base64 or file loading only
246
- - Ollama: Limited multimodal depending on model
278
+ ```ruby
279
+ fast_model = DSPy::LM.new("openai/gpt-4o-mini", api_key: ENV['OPENAI_API_KEY'])
247
280
 
248
- **Full documentation**: See `references/core-concepts.md` section on Multimodal Support.
281
+ DSPy.with_lm(fast_model) do
282
+ result = classifier.call(text: "test") # Uses fast_model inside this block
283
+ end
284
+ # Back to global LM outside the block
285
+ ```
249
286
 
250
- ### 6. Testing LLM Applications
287
+ **LM resolution hierarchy**: Instance-level LM → Fiber-local LM (`DSPy.with_lm`) Global LM (`DSPy.configure`).
251
288
 
252
- Write standard RSpec tests for LLM logic.
289
+ Use `configure_predictor` for fine-grained control over agent internals:
253
290
 
254
- **Quick reference**:
255
291
  ```ruby
256
- RSpec.describe EmailClassifier do
257
- before do
258
- DSPy.configure do |c|
259
- c.lm = DSPy::LM.new('openai/gpt-4o-mini',
260
- api_key: ENV['OPENAI_API_KEY'])
261
- end
262
- end
292
+ agent = DSPy::ReAct.new(MySignature, tools: tools)
293
+ agent.configure { |c| c.lm = default_model }
294
+ agent.configure_predictor('thought_generator') { |c| c.lm = powerful_model }
295
+ ```
263
296
 
264
- it 'classifies technical emails correctly' do
265
- classifier = EmailClassifier.new
266
- result = classifier.forward(
267
- email_subject: "Can't log in",
268
- email_body: "Unable to access account"
269
- )
297
+ ## Evaluation Framework
270
298
 
271
- expect(result[:category]).to eq('Technical')
272
- expect(result[:priority]).to be_in(['High', 'Medium', 'Low'])
273
- end
274
- end
299
+ Systematically test LLM application performance with `DSPy::Evals`:
300
+
301
+ ```ruby
302
+ metric = DSPy::Metrics.exact_match(field: :answer, case_sensitive: false)
303
+ evaluator = DSPy::Evals.new(predictor, metric: metric)
304
+ result = evaluator.evaluate(test_examples, display_table: true)
305
+ puts "Pass Rate: #{(result.pass_rate * 100).round(1)}%"
275
306
  ```
276
307
 
277
- **Testing patterns**:
278
- - Mock LLM responses for unit tests
279
- - Use VCR for deterministic API testing
280
- - Test type safety and validation
281
- - Test edge cases (empty inputs, special characters, long texts)
282
- - Integration test complete workflows
308
+ Built-in metrics: `exact_match`, `contains`, `numeric_difference`, `composite_and`. Custom metrics return `true`/`false` or a `DSPy::Prediction` with `score:` and `feedback:` fields.
283
309
 
284
- **Full documentation**: See `references/optimization.md` section on Testing.
310
+ Use `DSPy::Example` for typed test data and `export_scores: true` to push results to Langfuse.
285
311
 
286
- ### 7. Optimization & Improvement
312
+ ## GEPA Optimization
287
313
 
288
- Automatically improve prompts and modules using optimization techniques.
314
+ GEPA (Genetic-Pareto Reflective Prompt Evolution) uses reflection-driven instruction rewrites:
289
315
 
290
- **MIPROv2 optimization**:
291
316
  ```ruby
292
- require 'dspy/mipro'
293
-
294
- # Define evaluation metric
295
- def accuracy_metric(example, prediction)
296
- example[:expected_output][:category] == prediction[:category] ? 1.0 : 0.0
297
- end
317
+ gem 'dspy-gepa'
298
318
 
299
- # Prepare training data
300
- training_examples = [
301
- {
302
- input: { email_subject: "...", email_body: "..." },
303
- expected_output: { category: 'Technical' }
304
- },
305
- # More examples...
306
- ]
307
-
308
- # Run optimization
309
- optimizer = DSPy::MIPROv2.new(
310
- metric: method(:accuracy_metric),
311
- num_candidates: 10
319
+ teleprompter = DSPy::Teleprompt::GEPA.new(
320
+ metric: metric,
321
+ reflection_lm: DSPy::ReflectionLM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY']),
322
+ feedback_map: feedback_map,
323
+ config: { max_metric_calls: 600, minibatch_size: 6 }
312
324
  )
313
325
 
314
- optimized_module = optimizer.compile(
315
- EmailClassifier.new,
316
- trainset: training_examples
317
- )
326
+ result = teleprompter.compile(program, trainset: train, valset: val)
327
+ optimized_program = result.optimized_program
318
328
  ```
319
329
 
320
- **A/B testing different approaches**:
330
+ The metric must return `DSPy::Prediction.new(score:, feedback:)` so the reflection model can reason about failures. Use `feedback_map` to target individual predictors in composite modules.
331
+
332
+ ## Typed Context Pattern
333
+
334
+ Replace opaque string context blobs with `T::Struct` inputs. Each field gets its own `description:` annotation in the JSON schema the LLM sees:
335
+
321
336
  ```ruby
322
- # Test ChainOfThought vs ReAct
323
- approach_a_score = evaluate_approach(ChainOfThoughtModule, test_set)
324
- approach_b_score = evaluate_approach(ReActModule, test_set)
337
+ class NavigationContext < T::Struct
338
+ const :workflow_hint, T.nilable(String),
339
+ description: "Current workflow phase guidance for the agent"
340
+ const :action_log, T::Array[String], default: [],
341
+ description: "Compact one-line-per-action history of research steps taken"
342
+ const :iterations_remaining, Integer,
343
+ description: "Budget remaining. Each tool call costs 1 iteration."
344
+ end
345
+
346
+ class ToolSelectionSignature < DSPy::Signature
347
+ input do
348
+ const :query, String
349
+ const :context, NavigationContext # Structured, not an opaque string
350
+ end
351
+
352
+ output do
353
+ const :tool_name, String
354
+ const :tool_args, String, description: "JSON-encoded arguments"
355
+ end
356
+ end
325
357
  ```
326
358
 
327
- **Full documentation**: See `references/optimization.md` section on Optimization.
359
+ Benefits: type safety at compile time, per-field descriptions in the LLM schema, easy to test as value objects, extensible by adding `const` declarations.
328
360
 
329
- ### 8. Observability & Monitoring
361
+ ## Schema Formats (BAML / TOON)
330
362
 
331
- Track performance, token usage, and behavior in production.
363
+ Control how DSPy describes signature structure to the LLM:
332
364
 
333
- **OpenTelemetry integration**:
334
- ```ruby
335
- require 'opentelemetry/sdk'
365
+ - **JSON Schema** (default) — Standard format, works with `structured_outputs: true`
366
+ - **BAML** (`schema_format: :baml`) — 84% token reduction for Enhanced Prompting mode. Requires `sorbet-baml` gem.
367
+ - **TOON** (`schema_format: :toon, data_format: :toon`) — Table-oriented format for both schemas and data. Enhanced Prompting mode only.
336
368
 
337
- OpenTelemetry::SDK.configure do |c|
338
- c.service_name = 'my-dspy-app'
339
- c.use_all
340
- end
369
+ BAML and TOON apply only when `structured_outputs: false`. With `structured_outputs: true`, the provider receives JSON Schema directly.
341
370
 
342
- # DSPy automatically creates traces
343
- ```
371
+ ## Storage System
344
372
 
345
- **Langfuse tracing**:
346
- ```ruby
347
- DSPy.configure do |c|
348
- c.lm = DSPy::LM.new('openai/gpt-4o-mini',
349
- api_key: ENV['OPENAI_API_KEY'])
373
+ Persist and reload optimized programs with `DSPy::Storage::ProgramStorage`:
350
374
 
351
- c.langfuse = {
352
- public_key: ENV['LANGFUSE_PUBLIC_KEY'],
353
- secret_key: ENV['LANGFUSE_SECRET_KEY']
354
- }
355
- end
375
+ ```ruby
376
+ storage = DSPy::Storage::ProgramStorage.new(storage_path: "./dspy_storage")
377
+ storage.save_program(result.optimized_program, result, metadata: { optimizer: 'MIPROv2' })
356
378
  ```
357
379
 
358
- **Custom monitoring**:
359
- - Token tracking
360
- - Performance monitoring
361
- - Error rate tracking
362
- - Custom logging
380
+ Supports checkpoint management, optimization history tracking, and import/export between environments.
363
381
 
364
- **Full documentation**: See `references/optimization.md` section on Observability.
382
+ ## Rails Integration
365
383
 
366
- ## Quick Start Workflow
384
+ ### Directory Structure
367
385
 
368
- ### For New Projects
386
+ Organize DSPy components using Rails conventions:
369
387
 
370
- 1. **Install DSPy.rb and provider gems**:
371
- ```bash
372
- gem install dspy dspy-openai # or dspy-anthropic, dspy-gemini
373
388
  ```
389
+ app/
390
+ entities/ # T::Struct types shared across signatures
391
+ signatures/ # DSPy::Signature definitions
392
+ tools/ # DSPy::Tools::Base implementations
393
+ concerns/ # Shared tool behaviors (error handling, etc.)
394
+ modules/ # DSPy::Module orchestrators
395
+ services/ # Plain Ruby services that compose DSPy modules
396
+ config/
397
+ initializers/
398
+ dspy.rb # DSPy + provider configuration
399
+ feature_flags.rb # Model selection per role
400
+ spec/
401
+ signatures/ # Schema validation tests
402
+ tools/ # Tool unit tests
403
+ modules/ # Integration tests with VCR
404
+ vcr_cassettes/ # Recorded HTTP interactions
405
+ ```
406
+
407
+ ### Initializer
374
408
 
375
- 2. **Configure LLM provider** (see `assets/config-template.rb`):
376
409
  ```ruby
377
- require 'dspy'
410
+ # config/initializers/dspy.rb
411
+ Rails.application.config.after_initialize do
412
+ next if Rails.env.test? && ENV["DSPY_ENABLE_IN_TEST"].blank?
413
+
414
+ RubyLLM.configure do |config|
415
+ config.gemini_api_key = ENV["GEMINI_API_KEY"] if ENV["GEMINI_API_KEY"].present?
416
+ config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"] if ENV["ANTHROPIC_API_KEY"].present?
417
+ config.openai_api_key = ENV["OPENAI_API_KEY"] if ENV["OPENAI_API_KEY"].present?
418
+ end
378
419
 
379
- DSPy.configure do |c|
380
- c.lm = DSPy::LM.new('openai/gpt-4o-mini',
381
- api_key: ENV['OPENAI_API_KEY'])
420
+ model = ENV.fetch("DSPY_MODEL", "ruby_llm/gemini-2.5-flash")
421
+ DSPy.configure do |config|
422
+ config.lm = DSPy::LM.new(model, structured_outputs: true)
423
+ config.logger = Rails.logger
424
+ end
425
+
426
+ # Langfuse observability (optional)
427
+ if ENV["LANGFUSE_PUBLIC_KEY"].present? && ENV["LANGFUSE_SECRET_KEY"].present?
428
+ DSPy::Observability.configure!
429
+ end
382
430
  end
383
431
  ```
384
432
 
385
- 3. **Create a signature** (see `assets/signature-template.rb`):
433
+ ### Feature-Flagged Model Selection
434
+
435
+ Use different models for different roles (fast/cheap for classification, powerful for synthesis):
436
+
386
437
  ```ruby
387
- class MySignature < DSPy::Signature
388
- description "Clear description of task"
438
+ # config/initializers/feature_flags.rb
439
+ module FeatureFlags
440
+ SELECTOR_MODEL = ENV.fetch("DSPY_SELECTOR_MODEL", "ruby_llm/gemini-2.5-flash-lite")
441
+ SYNTHESIZER_MODEL = ENV.fetch("DSPY_SYNTHESIZER_MODEL", "ruby_llm/gemini-2.5-flash")
442
+ end
443
+ ```
389
444
 
390
- input do
391
- const :input_field, String, desc: "Description"
392
- end
445
+ Then override per-tool or per-predictor:
393
446
 
394
- output do
395
- const :output_field, String, desc: "Description"
447
+ ```ruby
448
+ class ClassifyTool < DSPy::Tools::Base
449
+ def call(query:)
450
+ predictor = DSPy::Predict.new(ClassifyQuery)
451
+ predictor.configure { |c| c.lm = DSPy::LM.new(FeatureFlags::SELECTOR_MODEL, structured_outputs: true) }
452
+ predictor.call(query: query)
396
453
  end
397
454
  end
398
455
  ```
399
456
 
400
- 4. **Create a module** (see `assets/module-template.rb`):
457
+ ## Schema-Driven Signatures
458
+
459
+ **Prefer typed schemas over string descriptions.** Let the type system communicate structure to the LLM rather than prose in the signature description.
460
+
461
+ ### Entities as Shared Types
462
+
463
+ Define reusable `T::Struct` and `T::Enum` types in `app/entities/` and reference them across signatures:
464
+
401
465
  ```ruby
402
- class MyModule < DSPy::Module
403
- def initialize
404
- super
405
- @predictor = DSPy::Predict.new(MySignature)
466
+ # app/entities/search_strategy.rb
467
+ class SearchStrategy < T::Enum
468
+ enums do
469
+ SingleSearch = new("single_search")
470
+ DateDecomposition = new("date_decomposition")
406
471
  end
472
+ end
407
473
 
408
- def forward(input_field:)
409
- @predictor.forward(input_field: input_field)
410
- end
474
+ # app/entities/scored_item.rb
475
+ class ScoredItem < T::Struct
476
+ const :id, String
477
+ const :score, Float, description: "Relevance score 0.0-1.0"
478
+ const :verdict, String, description: "relevant, maybe, or irrelevant"
479
+ const :reason, String, default: ""
411
480
  end
412
481
  ```
413
482
 
414
- 5. **Use the module**:
483
+ ### Schema vs Description: When to Use Each
484
+
485
+ **Use schemas (T::Struct/T::Enum)** for:
486
+ - Multi-field outputs with specific types
487
+ - Enums with defined values the LLM must pick from
488
+ - Nested structures, arrays of typed objects
489
+ - Outputs consumed by code (not displayed to users)
490
+
491
+ **Use string descriptions** for:
492
+ - Simple single-field outputs where the type is `String`
493
+ - Natural language generation (summaries, answers)
494
+ - Fields where constraint guidance helps (e.g., `description: "YYYY-MM-DD format"`)
495
+
496
+ **Rule of thumb**: If you'd write a `case` statement on the output, it should be a `T::Enum`. If you'd call `.each` on it, it should be `T::Array[SomeStruct]`.
497
+
498
+ ## Tool Patterns
499
+
500
+ ### Tools That Wrap Predictions
501
+
502
+ A common pattern: tools encapsulate a DSPy prediction, adding error handling, model selection, and serialization:
503
+
415
504
  ```ruby
416
- module_instance = MyModule.new
417
- result = module_instance.forward(input_field: "test")
418
- puts result[:output_field]
505
+ class RerankTool < DSPy::Tools::Base
506
+ tool_name "rerank"
507
+ tool_description "Score and rank search results by relevance"
508
+
509
+ MAX_ITEMS = 200
510
+ MIN_ITEMS_FOR_LLM = 5
511
+
512
+ sig { params(query: String, items: T::Array[T::Hash[Symbol, T.untyped]]).returns(T::Hash[Symbol, T.untyped]) }
513
+ def call(query:, items: [])
514
+ return { scored_items: items, reranked: false } if items.size < MIN_ITEMS_FOR_LLM
515
+
516
+ capped_items = items.first(MAX_ITEMS)
517
+ predictor = DSPy::Predict.new(RerankSignature)
518
+ predictor.configure { |c| c.lm = DSPy::LM.new(FeatureFlags::SYNTHESIZER_MODEL, structured_outputs: true) }
519
+
520
+ result = predictor.call(query: query, items: capped_items)
521
+ { scored_items: result.scored_items, reranked: true }
522
+ rescue => e
523
+ Rails.logger.warn "[RerankTool] LLM rerank failed: #{e.message}"
524
+ { error: "Rerank failed: #{e.message}", scored_items: items, reranked: false }
525
+ end
526
+ end
419
527
  ```
420
528
 
421
- 6. **Add tests** (see `references/optimization.md`):
529
+ **Key patterns:**
530
+ - Short-circuit LLM calls when unnecessary (small data, trivial cases)
531
+ - Cap input size to prevent token overflow
532
+ - Per-tool model selection via `configure`
533
+ - Graceful error handling with fallback data
534
+
535
+ ### Error Handling Concern
536
+
422
537
  ```ruby
423
- RSpec.describe MyModule do
424
- it 'produces expected output' do
425
- result = MyModule.new.forward(input_field: "test")
426
- expect(result[:output_field]).to be_a(String)
538
+ module ErrorHandling
539
+ extend ActiveSupport::Concern
540
+
541
+ private
542
+
543
+ def safe_predict(signature_class, **inputs)
544
+ predictor = DSPy::Predict.new(signature_class)
545
+ yield predictor if block_given?
546
+ predictor.call(**inputs)
547
+ rescue Faraday::Error, Net::HTTPError => e
548
+ Rails.logger.error "[#{self.class.name}] API error: #{e.message}"
549
+ nil
550
+ rescue JSON::ParserError => e
551
+ Rails.logger.error "[#{self.class.name}] Invalid LLM output: #{e.message}"
552
+ nil
427
553
  end
428
554
  end
429
555
  ```
430
556
 
431
- ### For Rails Applications
557
+ ## Observability
558
+
559
+ ### Tracing with DSPy::Context
560
+
561
+ Wrap operations in spans for Langfuse/OpenTelemetry visibility:
432
562
 
433
- 1. **Add to Gemfile**:
434
563
  ```ruby
435
- gem 'dspy'
436
- gem 'dspy-openai' # or other provider
564
+ result = DSPy::Context.with_span(
565
+ operation: "tool_selector.select",
566
+ "dspy.module" => "ToolSelector",
567
+ "tool_selector.tools" => tool_names.join(",")
568
+ ) do
569
+ @predictor.call(query: query, context: context, available_tools: schemas)
570
+ end
437
571
  ```
438
572
 
439
- 2. **Create initializer** at `config/initializers/dspy.rb` (see `assets/config-template.rb` for full example):
440
- ```ruby
441
- require 'dspy'
573
+ ### Setup for Langfuse
442
574
 
443
- DSPy.configure do |c|
444
- c.lm = DSPy::LM.new('openai/gpt-4o-mini',
445
- api_key: ENV['OPENAI_API_KEY'])
446
- end
575
+ ```ruby
576
+ # Gemfile
577
+ gem 'dspy-o11y'
578
+ gem 'dspy-o11y-langfuse'
579
+
580
+ # .env
581
+ LANGFUSE_PUBLIC_KEY=pk-...
582
+ LANGFUSE_SECRET_KEY=sk-...
583
+ DSPY_TELEMETRY_BATCH_SIZE=5
447
584
  ```
448
585
 
449
- 3. **Create modules in** `app/llm/` directory:
586
+ Every `DSPy::Predict`, `DSPy::ReAct`, and tool call is automatically traced when observability is configured.
587
+
588
+ ### Score Reporting
589
+
590
+ Report evaluation scores to Langfuse:
591
+
450
592
  ```ruby
451
- # app/llm/email_classifier.rb
452
- class EmailClassifier < DSPy::Module
453
- # Implementation here
454
- end
593
+ DSPy.score(name: "relevance", value: 0.85, trace_id: current_trace_id)
455
594
  ```
456
595
 
457
- 4. **Use in controllers/services**:
596
+ ## Testing
597
+
598
+ ### VCR Setup for Rails
599
+
458
600
  ```ruby
459
- class EmailsController < ApplicationController
460
- def classify
461
- classifier = EmailClassifier.new
462
- result = classifier.forward(
463
- email_subject: params[:subject],
464
- email_body: params[:body]
465
- )
466
- render json: result
467
- end
601
+ VCR.configure do |config|
602
+ config.cassette_library_dir = "spec/vcr_cassettes"
603
+ config.hook_into :webmock
604
+ config.configure_rspec_metadata!
605
+ config.filter_sensitive_data('<GEMINI_API_KEY>') { ENV['GEMINI_API_KEY'] }
606
+ config.filter_sensitive_data('<OPENAI_API_KEY>') { ENV['OPENAI_API_KEY'] }
468
607
  end
469
608
  ```
470
609
 
471
- ## Common Patterns
610
+ ### Signature Schema Tests
472
611
 
473
- ### Pattern: Multi-Step Analysis Pipeline
612
+ Test that signatures produce valid schemas without calling any LLM:
474
613
 
475
614
  ```ruby
476
- class AnalysisPipeline < DSPy::Module
477
- def initialize
478
- super
479
- @extract = DSPy::Predict.new(ExtractSignature)
480
- @analyze = DSPy::ChainOfThought.new(AnalyzeSignature)
481
- @summarize = DSPy::Predict.new(SummarizeSignature)
615
+ RSpec.describe ClassifyResearchQuery do
616
+ it "has required input fields" do
617
+ schema = described_class.input_json_schema
618
+ expect(schema[:required]).to include("query")
482
619
  end
483
620
 
484
- def forward(text:)
485
- extracted = @extract.forward(text: text)
486
- analyzed = @analyze.forward(data: extracted[:data])
487
- @summarize.forward(analysis: analyzed[:result])
621
+ it "has typed output fields" do
622
+ schema = described_class.output_json_schema
623
+ expect(schema[:properties]).to have_key(:search_strategy)
488
624
  end
489
625
  end
490
626
  ```
491
627
 
492
- ### Pattern: Agent with Tools
628
+ ### Tool Tests with Mocked Predictions
493
629
 
494
630
  ```ruby
495
- class ResearchAgent < DSPy::Module
496
- def initialize
497
- super
498
- @agent = DSPy::ReAct.new(
499
- ResearchSignature,
500
- tools: [
501
- WebSearchTool.new,
502
- DatabaseQueryTool.new,
503
- SummarizerTool.new
504
- ],
505
- max_iterations: 10
506
- )
507
- end
631
+ RSpec.describe RerankTool do
632
+ let(:tool) { described_class.new }
508
633
 
509
- def forward(question:)
510
- @agent.forward(question: question)
634
+ it "skips LLM for small result sets" do
635
+ expect(DSPy::Predict).not_to receive(:new)
636
+ result = tool.call(query: "test", items: [{ id: "1" }])
637
+ expect(result[:reranked]).to be false
511
638
  end
512
- end
513
639
 
514
- class WebSearchTool < DSPy::Tool
515
- def call(query:)
516
- results = perform_search(query)
517
- { results: results }
640
+ it "calls LLM for large result sets", :vcr do
641
+ items = 10.times.map { |i| { id: i.to_s, title: "Item #{i}" } }
642
+ result = tool.call(query: "relevant items", items: items)
643
+ expect(result[:reranked]).to be true
518
644
  end
519
645
  end
520
646
  ```
521
647
 
522
- ### Pattern: Conditional Routing
648
+ ## Resources
649
+
650
+ - [core-concepts.md](./references/core-concepts.md) — Signatures, modules, predictors, type system deep-dive
651
+ - [toolsets.md](./references/toolsets.md) — Tools::Base, Tools::Toolset DSL, type safety, testing
652
+ - [providers.md](./references/providers.md) — Provider adapters, RubyLLM, fiber-local LM context, compatibility matrix
653
+ - [optimization.md](./references/optimization.md) — MIPROv2, GEPA, evaluation framework, storage system
654
+ - [observability.md](./references/observability.md) — Event system, dspy-o11y gems, Langfuse, score reporting
655
+ - [signature-template.rb](./assets/signature-template.rb) — Signature scaffold with T::Enum, Date/Time, defaults, union types
656
+ - [module-template.rb](./assets/module-template.rb) — Module scaffold with .call(), lifecycle callbacks, fiber-local LM
657
+ - [config-template.rb](./assets/config-template.rb) — Rails initializer with RubyLLM, observability, feature flags
658
+
659
+ ## Key URLs
660
+
661
+ - Homepage: https://oss.vicente.services/dspy.rb/
662
+ - GitHub: https://github.com/vicentereig/dspy.rb
663
+ - Documentation: https://oss.vicente.services/dspy.rb/getting-started/
664
+
665
+ ## Guidelines for Claude
666
+
667
+ When helping users with DSPy.rb:
668
+
669
+ 1. **Schema over prose** — Define output structure with `T::Struct` and `T::Enum` types, not string descriptions
670
+ 2. **Entities in `app/entities/`** — Extract shared types so signatures stay thin
671
+ 3. **Per-tool model selection** — Use `predictor.configure { |c| c.lm = ... }` to pick the right model per task
672
+ 4. **Short-circuit LLM calls** — Skip the LLM for trivial cases (small data, cached results)
673
+ 5. **Cap input sizes** — Prevent token overflow by limiting array sizes before sending to LLM
674
+ 6. **Test schemas without LLM** — Validate `input_json_schema` and `output_json_schema` in unit tests
675
+ 7. **VCR for integration tests** — Record real HTTP interactions, never mock LLM responses by hand
676
+ 8. **Trace with spans** — Wrap tool calls in `DSPy::Context.with_span` for observability
677
+ 9. **Graceful degradation** — Always rescue LLM errors and return fallback data
678
+
679
+ ### Signature Best Practices
680
+
681
+ **Keep description concise** — The signature `description` should state the goal, not the field details:
523
682
 
524
683
  ```ruby
525
- class SmartRouter < DSPy::Module
526
- def initialize
527
- super
528
- @classifier = DSPy::Predict.new(ClassifySignature)
529
- @simple_handler = SimpleModule.new
530
- @complex_handler = ComplexModule.new
531
- end
684
+ # Good concise goal
685
+ class ParseOutline < DSPy::Signature
686
+ description 'Extract block-level structure from HTML as a flat list of skeleton sections.'
532
687
 
533
- def forward(input:)
534
- classification = @classifier.forward(text: input)
688
+ input do
689
+ const :html, String, description: 'Raw HTML to parse'
690
+ end
535
691
 
536
- if classification[:complexity] == 'Simple'
537
- @simple_handler.forward(input: input)
538
- else
539
- @complex_handler.forward(input: input)
540
- end
692
+ output do
693
+ const :sections, T::Array[Section], description: 'Block elements: headings, paragraphs, code blocks, lists'
541
694
  end
542
695
  end
543
696
  ```
544
697
 
545
- ### Pattern: Retry with Fallback
698
+ **Use defaults over nilable arrays** — For OpenAI structured outputs compatibility:
546
699
 
547
700
  ```ruby
548
- class RobustModule < DSPy::Module
549
- MAX_RETRIES = 3
550
-
551
- def forward(input, retry_count: 0)
552
- begin
553
- @predictor.forward(input)
554
- rescue DSPy::ValidationError => e
555
- if retry_count < MAX_RETRIES
556
- sleep(2 ** retry_count)
557
- forward(input, retry_count: retry_count + 1)
558
- else
559
- # Fallback to default or raise
560
- raise
561
- end
562
- end
563
- end
701
+ # Good works with OpenAI structured outputs
702
+ class ASTNode < T::Struct
703
+ const :children, T::Array[ASTNode], default: []
564
704
  end
565
705
  ```
566
706
 
567
- ## Resources
707
+ ### Recursive Types with `$defs`
708
+
709
+ DSPy.rb supports recursive types in structured outputs using JSON Schema `$defs`:
710
+
711
+ ```ruby
712
+ class TreeNode < T::Struct
713
+ const :value, String
714
+ const :children, T::Array[TreeNode], default: [] # Self-reference
715
+ end
716
+ ```
568
717
 
569
- This skill includes comprehensive reference materials and templates:
718
+ The schema generator automatically creates `#/$defs/TreeNode` references for recursive types, compatible with OpenAI and Gemini structured outputs.
570
719
 
571
- ### References (load as needed for detailed information)
720
+ ### Field Descriptions for T::Struct
572
721
 
573
- - [core-concepts.md](./references/core-concepts.md): Complete guide to signatures, modules, predictors, multimodal support, and best practices
574
- - [providers.md](./references/providers.md): All LLM provider configurations, compatibility matrix, cost optimization, and troubleshooting
575
- - [optimization.md](./references/optimization.md): Testing patterns, optimization techniques, observability setup, and monitoring
722
+ DSPy.rb extends T::Struct to support field-level `description:` kwargs that flow to JSON Schema:
576
723
 
577
- ### Assets (templates for quick starts)
724
+ ```ruby
725
+ class ASTNode < T::Struct
726
+ const :node_type, NodeType, description: 'The type of node (heading, paragraph, etc.)'
727
+ const :text, String, default: "", description: 'Text content of the node'
728
+ const :level, Integer, default: 0 # No description — field is self-explanatory
729
+ const :children, T::Array[ASTNode], default: []
730
+ end
731
+ ```
578
732
 
579
- - [signature-template.rb](./assets/signature-template.rb): Examples of signatures including basic, vision, sentiment analysis, and code generation
580
- - [module-template.rb](./assets/module-template.rb): Module patterns including pipelines, agents, error handling, caching, and state management
581
- - [config-template.rb](./assets/config-template.rb): Configuration examples for all providers, environments, observability, and production patterns
733
+ **When to use field descriptions**: complex field semantics, enum-like strings, constrained values, nested structs with ambiguous names. **When to skip**: self-explanatory fields like `name`, `id`, `url`, or boolean flags.
582
734
 
583
- ## When to Use This Skill
735
+ ## Version
584
736
 
585
- Trigger this skill when:
586
- - Implementing LLM-powered features in Ruby applications
587
- - Creating type-safe interfaces for AI operations
588
- - Building agent systems with tool usage
589
- - Setting up or troubleshooting LLM providers
590
- - Optimizing prompts and improving accuracy
591
- - Testing LLM functionality
592
- - Adding observability to AI applications
593
- - Converting from manual prompt engineering to programmatic approach
594
- - Debugging DSPy.rb code or configuration issues
737
+ Current: 0.34.3