@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/deploy-docs.yml +3 -3
- package/.github/workflows/publish.yml +37 -0
- package/README.md +12 -3
- package/docs/index.html +13 -13
- package/docs/pages/changelog.html +39 -0
- package/docs/plans/2026-02-08-feat-convert-local-md-settings-for-opencode-codex-plan.md +143 -0
- package/docs/plans/2026-02-08-feat-simplify-plugin-settings-plan.md +195 -0
- package/docs/plans/2026-02-08-refactor-reduce-plugin-context-token-usage-plan.md +212 -0
- package/docs/plans/2026-02-09-refactor-dspy-ruby-skill-update-plan.md +104 -0
- package/docs/plans/2026-02-12-feat-add-cursor-cli-target-provider-plan.md +306 -0
- package/docs/specs/cursor.md +85 -0
- package/package.json +1 -1
- package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
- package/plugins/compound-engineering/CHANGELOG.md +64 -0
- package/plugins/compound-engineering/README.md +5 -3
- package/plugins/compound-engineering/agents/design/design-implementation-reviewer.md +16 -1
- package/plugins/compound-engineering/agents/design/design-iterator.md +28 -1
- package/plugins/compound-engineering/agents/design/figma-design-sync.md +19 -1
- package/plugins/compound-engineering/agents/docs/ankane-readme-writer.md +16 -1
- package/plugins/compound-engineering/agents/research/best-practices-researcher.md +16 -1
- package/plugins/compound-engineering/agents/research/framework-docs-researcher.md +16 -1
- package/plugins/compound-engineering/agents/research/git-history-analyzer.md +16 -1
- package/plugins/compound-engineering/agents/research/learnings-researcher.md +22 -1
- package/plugins/compound-engineering/agents/research/repo-research-analyst.md +22 -1
- package/plugins/compound-engineering/agents/review/agent-native-reviewer.md +16 -1
- package/plugins/compound-engineering/agents/review/architecture-strategist.md +16 -1
- package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +16 -1
- package/plugins/compound-engineering/agents/review/data-integrity-guardian.md +16 -1
- package/plugins/compound-engineering/agents/review/data-migration-expert.md +16 -1
- package/plugins/compound-engineering/agents/review/deployment-verification-agent.md +16 -1
- package/plugins/compound-engineering/agents/review/dhh-rails-reviewer.md +22 -1
- package/plugins/compound-engineering/agents/review/julik-frontend-races-reviewer.md +20 -21
- package/plugins/compound-engineering/agents/review/kieran-python-reviewer.md +30 -1
- package/plugins/compound-engineering/agents/review/kieran-rails-reviewer.md +30 -1
- package/plugins/compound-engineering/agents/review/kieran-typescript-reviewer.md +30 -1
- package/plugins/compound-engineering/agents/review/pattern-recognition-specialist.md +16 -1
- package/plugins/compound-engineering/agents/review/performance-oracle.md +28 -1
- package/plugins/compound-engineering/agents/review/schema-drift-detector.md +16 -1
- package/plugins/compound-engineering/agents/review/security-sentinel.md +22 -1
- package/plugins/compound-engineering/agents/workflow/bug-reproduction-validator.md +16 -1
- package/plugins/compound-engineering/agents/workflow/every-style-editor.md +1 -1
- package/plugins/compound-engineering/agents/workflow/pr-comment-resolver.md +16 -1
- package/plugins/compound-engineering/agents/workflow/spec-flow-analyzer.md +22 -1
- package/plugins/compound-engineering/commands/agent-native-audit.md +1 -0
- package/plugins/compound-engineering/commands/changelog.md +1 -0
- package/plugins/compound-engineering/commands/create-agent-skill.md +1 -0
- package/plugins/compound-engineering/commands/deploy-docs.md +1 -0
- package/plugins/compound-engineering/commands/generate_command.md +1 -0
- package/plugins/compound-engineering/commands/heal-skill.md +1 -0
- package/plugins/compound-engineering/commands/lfg.md +1 -0
- package/plugins/compound-engineering/commands/report-bug.md +1 -0
- package/plugins/compound-engineering/commands/reproduce-bug.md +1 -0
- package/plugins/compound-engineering/commands/resolve_parallel.md +1 -0
- package/plugins/compound-engineering/commands/slfg.md +1 -0
- package/plugins/compound-engineering/commands/{xcode-test.md → test-xcode.md} +2 -1
- package/plugins/compound-engineering/commands/triage.md +1 -0
- package/plugins/compound-engineering/commands/workflows/brainstorm.md +6 -1
- package/plugins/compound-engineering/commands/workflows/compound.md +1 -0
- package/plugins/compound-engineering/commands/workflows/review.md +23 -21
- package/plugins/compound-engineering/commands/workflows/work.md +29 -15
- package/plugins/compound-engineering/skills/compound-docs/SKILL.md +1 -0
- package/plugins/compound-engineering/skills/dspy-ruby/SKILL.md +539 -396
- package/plugins/compound-engineering/skills/dspy-ruby/assets/config-template.rb +159 -331
- package/plugins/compound-engineering/skills/dspy-ruby/assets/module-template.rb +210 -236
- package/plugins/compound-engineering/skills/dspy-ruby/assets/signature-template.rb +173 -95
- package/plugins/compound-engineering/skills/dspy-ruby/references/core-concepts.md +552 -143
- package/plugins/compound-engineering/skills/dspy-ruby/references/observability.md +366 -0
- package/plugins/compound-engineering/skills/dspy-ruby/references/optimization.md +440 -460
- package/plugins/compound-engineering/skills/dspy-ruby/references/providers.md +305 -225
- package/plugins/compound-engineering/skills/dspy-ruby/references/toolsets.md +502 -0
- package/plugins/compound-engineering/skills/file-todos/SKILL.md +1 -0
- package/plugins/compound-engineering/skills/orchestrating-swarms/SKILL.md +1 -0
- package/plugins/compound-engineering/skills/setup/SKILL.md +168 -0
- package/plugins/compound-engineering/skills/skill-creator/SKILL.md +1 -0
- package/src/commands/convert.ts +10 -5
- package/src/commands/install.ts +10 -5
- package/src/converters/claude-to-codex.ts +9 -3
- package/src/converters/claude-to-cursor.ts +166 -0
- package/src/converters/claude-to-droid.ts +174 -0
- package/src/converters/claude-to-opencode.ts +9 -2
- package/src/parsers/claude.ts +4 -0
- package/src/targets/cursor.ts +48 -0
- package/src/targets/droid.ts +50 -0
- package/src/targets/index.ts +18 -0
- package/src/types/claude.ts +2 -0
- package/src/types/cursor.ts +29 -0
- package/src/types/droid.ts +20 -0
- package/tests/claude-parser.test.ts +24 -2
- package/tests/codex-converter.test.ts +100 -0
- package/tests/converter.test.ts +76 -0
- package/tests/cursor-converter.test.ts +347 -0
- package/tests/cursor-writer.test.ts +137 -0
- package/tests/droid-converter.test.ts +277 -0
- package/tests/droid-writer.test.ts +100 -0
- package/tests/fixtures/sample-plugin/commands/disabled-command.md +7 -0
- package/tests/fixtures/sample-plugin/skills/disabled-skill/SKILL.md +7 -0
- package/plugins/compound-engineering/commands/technical_review.md +0 -7
- /package/{plugins/compound-engineering → .claude}/commands/release-docs.md +0 -0
|
@@ -1,594 +1,737 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dspy-ruby
|
|
3
|
-
description:
|
|
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
|
|
6
|
+
# DSPy.rb
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
|
|
12
|
+
## Overview
|
|
11
13
|
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
+
## Core Concepts
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
### 1. Signatures
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
Define interfaces between your app and LLMs using Ruby types:
|
|
27
26
|
|
|
28
|
-
**Quick reference**:
|
|
29
27
|
```ruby
|
|
30
|
-
class
|
|
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 :
|
|
35
|
-
const :
|
|
41
|
+
const :email_content, String
|
|
42
|
+
const :sender, String
|
|
36
43
|
end
|
|
37
44
|
|
|
38
45
|
output do
|
|
39
|
-
const :category,
|
|
40
|
-
const :priority,
|
|
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
|
-
|
|
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
|
-
|
|
55
|
+
Build complex workflows from simple building blocks:
|
|
58
56
|
|
|
59
|
-
|
|
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
|
-
|
|
62
|
+
### 3. Tools & Toolsets
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
Create type-safe tools for agents with comprehensive Sorbet support:
|
|
64
65
|
|
|
65
|
-
**Quick reference**:
|
|
66
66
|
```ruby
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
+
Improve accuracy with real data:
|
|
111
133
|
|
|
112
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
195
|
+
RubyLLM handles provider routing based on the model name. Use the `ruby_llm/` prefix:
|
|
196
|
+
|
|
137
197
|
```ruby
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
+
DSPy.rb ships with a structured event bus for observing runtime behavior.
|
|
151
208
|
|
|
152
|
-
|
|
209
|
+
### Module-Scoped Subscriptions (preferred for agents)
|
|
153
210
|
|
|
154
|
-
**Quick configuration examples**:
|
|
155
211
|
```ruby
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
DSPy.
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
234
|
+
## Lifecycle Callbacks
|
|
189
235
|
|
|
190
|
-
|
|
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
|
-
|
|
198
|
-
-
|
|
199
|
-
-
|
|
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
|
-
|
|
242
|
+
```ruby
|
|
243
|
+
class InstrumentedModule < DSPy::Module
|
|
244
|
+
before :setup_metrics
|
|
245
|
+
around :manage_context
|
|
246
|
+
after :log_metrics
|
|
204
247
|
|
|
205
|
-
|
|
248
|
+
def forward(question:)
|
|
249
|
+
@predictor.call(question: question)
|
|
250
|
+
end
|
|
206
251
|
|
|
207
|
-
|
|
252
|
+
private
|
|
208
253
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
description "Analyze image and answer questions"
|
|
254
|
+
def setup_metrics
|
|
255
|
+
@start_time = Time.now
|
|
256
|
+
end
|
|
213
257
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
258
|
+
def manage_context
|
|
259
|
+
load_context
|
|
260
|
+
result = yield
|
|
261
|
+
save_context
|
|
262
|
+
result
|
|
217
263
|
end
|
|
218
264
|
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
DSPy::Image.from_url("https://example.com/image.jpg")
|
|
274
|
+
## Fiber-Local LM Context
|
|
238
275
|
|
|
239
|
-
|
|
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
|
-
|
|
244
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
287
|
+
**LM resolution hierarchy**: Instance-level LM → Fiber-local LM (`DSPy.with_lm`) → Global LM (`DSPy.configure`).
|
|
251
288
|
|
|
252
|
-
|
|
289
|
+
Use `configure_predictor` for fine-grained control over agent internals:
|
|
253
290
|
|
|
254
|
-
**Quick reference**:
|
|
255
291
|
```ruby
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
310
|
+
Use `DSPy::Example` for typed test data and `export_scores: true` to push results to Langfuse.
|
|
285
311
|
|
|
286
|
-
|
|
312
|
+
## GEPA Optimization
|
|
287
313
|
|
|
288
|
-
|
|
314
|
+
GEPA (Genetic-Pareto Reflective Prompt Evolution) uses reflection-driven instruction rewrites:
|
|
289
315
|
|
|
290
|
-
**MIPROv2 optimization**:
|
|
291
316
|
```ruby
|
|
292
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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
|
-
|
|
361
|
+
## Schema Formats (BAML / TOON)
|
|
330
362
|
|
|
331
|
-
|
|
363
|
+
Control how DSPy describes signature structure to the LLM:
|
|
332
364
|
|
|
333
|
-
**
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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
|
-
|
|
343
|
-
```
|
|
371
|
+
## Storage System
|
|
344
372
|
|
|
345
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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
|
-
|
|
382
|
+
## Rails Integration
|
|
365
383
|
|
|
366
|
-
|
|
384
|
+
### Directory Structure
|
|
367
385
|
|
|
368
|
-
|
|
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
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
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
|
-
|
|
388
|
-
|
|
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
|
-
|
|
391
|
-
const :input_field, String, desc: "Description"
|
|
392
|
-
end
|
|
445
|
+
Then override per-tool or per-predictor:
|
|
393
446
|
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
436
|
-
|
|
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
|
-
|
|
440
|
-
```ruby
|
|
441
|
-
require 'dspy'
|
|
573
|
+
### Setup for Langfuse
|
|
442
574
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
596
|
+
## Testing
|
|
597
|
+
|
|
598
|
+
### VCR Setup for Rails
|
|
599
|
+
|
|
458
600
|
```ruby
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
610
|
+
### Signature Schema Tests
|
|
472
611
|
|
|
473
|
-
|
|
612
|
+
Test that signatures produce valid schemas without calling any LLM:
|
|
474
613
|
|
|
475
614
|
```ruby
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
###
|
|
628
|
+
### Tool Tests with Mocked Predictions
|
|
493
629
|
|
|
494
630
|
```ruby
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
510
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
534
|
-
|
|
688
|
+
input do
|
|
689
|
+
const :html, String, description: 'Raw HTML to parse'
|
|
690
|
+
end
|
|
535
691
|
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
698
|
+
**Use defaults over nilable arrays** — For OpenAI structured outputs compatibility:
|
|
546
699
|
|
|
547
700
|
```ruby
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
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
|
-
|
|
718
|
+
The schema generator automatically creates `#/$defs/TreeNode` references for recursive types, compatible with OpenAI and Gemini structured outputs.
|
|
570
719
|
|
|
571
|
-
###
|
|
720
|
+
### Field Descriptions for T::Struct
|
|
572
721
|
|
|
573
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
735
|
+
## Version
|
|
584
736
|
|
|
585
|
-
|
|
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
|