dspy 0.34.2 → 0.34.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +8 -16
- data/lib/dspy/chain_of_thought.rb +3 -2
- data/lib/dspy/context.rb +70 -21
- data/lib/dspy/evals/version.rb +1 -1
- data/lib/dspy/evals.rb +42 -31
- data/lib/dspy/events.rb +2 -3
- data/lib/dspy/example.rb +1 -1
- data/lib/dspy/lm/adapter.rb +39 -0
- data/lib/dspy/lm/json_strategy.rb +28 -67
- data/lib/dspy/lm/message.rb +1 -1
- data/lib/dspy/lm/response.rb +2 -2
- data/lib/dspy/lm/usage.rb +35 -10
- data/lib/dspy/lm.rb +22 -51
- data/lib/dspy/mixins/type_coercion.rb +256 -35
- data/lib/dspy/module.rb +203 -31
- data/lib/dspy/predict.rb +33 -6
- data/lib/dspy/prediction.rb +25 -58
- data/lib/dspy/prompt.rb +52 -76
- data/lib/dspy/propose/dataset_summary_generator.rb +1 -1
- data/lib/dspy/propose/grounded_proposer.rb +3 -3
- data/lib/dspy/re_act.rb +159 -196
- data/lib/dspy/registry/signature_registry.rb +3 -3
- data/lib/dspy/ruby_llm/lm/adapters/ruby_llm_adapter.rb +1 -27
- data/lib/dspy/schema/sorbet_json_schema.rb +7 -6
- data/lib/dspy/schema/version.rb +1 -1
- data/lib/dspy/schema_adapters.rb +1 -1
- data/lib/dspy/signature.rb +4 -5
- data/lib/dspy/storage/program_storage.rb +2 -2
- data/lib/dspy/structured_outputs_prompt.rb +4 -4
- data/lib/dspy/teleprompt/utils.rb +2 -2
- data/lib/dspy/tools/github_cli_toolset.rb +7 -7
- data/lib/dspy/tools/text_processing_toolset.rb +2 -2
- data/lib/dspy/tools/toolset.rb +1 -1
- data/lib/dspy/utils/serialization.rb +2 -6
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +50 -5
- metadata +7 -26
- data/lib/dspy/events/subscriber_mixin.rb +0 -79
- data/lib/dspy/events/subscribers.rb +0 -43
- data/lib/dspy/memory/embedding_engine.rb +0 -68
- data/lib/dspy/memory/in_memory_store.rb +0 -216
- data/lib/dspy/memory/local_embedding_engine.rb +0 -244
- data/lib/dspy/memory/memory_compactor.rb +0 -298
- data/lib/dspy/memory/memory_manager.rb +0 -266
- data/lib/dspy/memory/memory_record.rb +0 -163
- data/lib/dspy/memory/memory_store.rb +0 -90
- data/lib/dspy/memory.rb +0 -30
- data/lib/dspy/tools/memory_toolset.rb +0 -117
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5d5296e0130d0550156345659e6703451bd6ca6fb9ffbac33d8584ee203f2a84
|
|
4
|
+
data.tar.gz: 339772eb768a2babbb8b700b868fc772183473b7cf4f8a6867e55945f75a3655
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 827360cba1ad8d03373d40d9b1b9ce3acf966e896261b918537150568cf4635904e591c238cdea7d8e8bf45980851ce3c581f709639215c8d7e4c7e1ea78dc08
|
|
7
|
+
data.tar.gz: e8d91df3e7204ac0d0db830c5839a4cbdd9683a00be2a77dec38de156052f0a0b3956e25c8b7026fcce23880e9806fc9af023fc2a4fe830926f288f82474a85b
|
data/README.md
CHANGED
|
@@ -137,26 +137,18 @@ result.answer # => "60 km/h"
|
|
|
137
137
|
Build agents that use tools to accomplish tasks:
|
|
138
138
|
|
|
139
139
|
```ruby
|
|
140
|
-
class SearchTool < DSPy::Tools::
|
|
140
|
+
class SearchTool < DSPy::Tools::Base
|
|
141
141
|
tool_name "search"
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
input do
|
|
145
|
-
const :query, String
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
output do
|
|
149
|
-
const :results, T::Array[String]
|
|
150
|
-
end
|
|
142
|
+
tool_description "Search for information"
|
|
151
143
|
|
|
144
|
+
sig { params(query: String).returns(String) }
|
|
152
145
|
def call(query:)
|
|
153
146
|
# Your search implementation
|
|
154
|
-
|
|
147
|
+
"Result 1, Result 2"
|
|
155
148
|
end
|
|
156
149
|
end
|
|
157
150
|
|
|
158
|
-
|
|
159
|
-
agent = DSPy::ReAct.new(signature: ResearchTask, tools: toolset, max_iterations: 5)
|
|
151
|
+
agent = DSPy::ReAct.new(ResearchTask, tools: [SearchTool.new], max_iterations: 5)
|
|
160
152
|
result = agent.call(question: "What's the latest on Ruby 3.4?")
|
|
161
153
|
```
|
|
162
154
|
|
|
@@ -185,8 +177,8 @@ result = agent.call(question: "What's the latest on Ruby 3.4?")
|
|
|
185
177
|
A [Claude Skill](https://github.com/vicentereig/dspy-rb-skill) is available to help you build DSPy.rb applications:
|
|
186
178
|
|
|
187
179
|
```bash
|
|
188
|
-
# Claude Code
|
|
189
|
-
|
|
180
|
+
# Claude Code — install from the vicentereig/engineering marketplace
|
|
181
|
+
claude install-skill vicentereig/engineering --skill dspy-rb
|
|
190
182
|
```
|
|
191
183
|
|
|
192
184
|
For Claude.ai Pro/Max, download the [skill ZIP](https://github.com/vicentereig/dspy-rb-skill/archive/refs/heads/main.zip) and upload via Settings > Skills.
|
|
@@ -201,7 +193,7 @@ The [examples/](examples/) directory has runnable code for common patterns:
|
|
|
201
193
|
- Prompt optimization
|
|
202
194
|
|
|
203
195
|
```bash
|
|
204
|
-
bundle exec ruby examples/
|
|
196
|
+
bundle exec ruby examples/basic_search_agent.rb
|
|
205
197
|
```
|
|
206
198
|
|
|
207
199
|
## Optional Gems
|
|
@@ -47,7 +47,8 @@ module DSPy
|
|
|
47
47
|
output_schema: @signature_class.output_json_schema,
|
|
48
48
|
few_shot_examples: new_prompt.few_shot_examples,
|
|
49
49
|
signature_class_name: @signature_class.name,
|
|
50
|
-
schema_format: new_prompt.schema_format
|
|
50
|
+
schema_format: new_prompt.schema_format,
|
|
51
|
+
data_format: new_prompt.data_format
|
|
51
52
|
)
|
|
52
53
|
|
|
53
54
|
instance.instance_variable_set(:@prompt, enhanced_prompt)
|
|
@@ -93,7 +94,7 @@ module DSPy
|
|
|
93
94
|
|
|
94
95
|
# Create a temporary Predict instance with our enhanced signature to get the prediction
|
|
95
96
|
predict_instance = DSPy::Predict.new(@signature_class)
|
|
96
|
-
predict_instance.
|
|
97
|
+
predict_instance.configure { |c| c.lm = self.lm } # Use the same LM configuration
|
|
97
98
|
|
|
98
99
|
# Call predict's forward method, which will create the Predict span
|
|
99
100
|
prediction_result = predict_instance.forward(**input_values)
|
data/lib/dspy/context.rb
CHANGED
|
@@ -31,6 +31,18 @@ module DSPy
|
|
|
31
31
|
context
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
def with_request(request_id, start_time)
|
|
35
|
+
previous_request_id = current[:request_id]
|
|
36
|
+
previous_start_time = current[:request_start_time]
|
|
37
|
+
|
|
38
|
+
current[:request_id] = request_id
|
|
39
|
+
current[:request_start_time] = start_time
|
|
40
|
+
yield
|
|
41
|
+
ensure
|
|
42
|
+
current[:request_id] = previous_request_id
|
|
43
|
+
current[:request_start_time] = previous_start_time
|
|
44
|
+
end
|
|
45
|
+
|
|
34
46
|
def fork_context(parent_context)
|
|
35
47
|
clone_context(parent_context)
|
|
36
48
|
end
|
|
@@ -62,8 +74,9 @@ module DSPy
|
|
|
62
74
|
# Prepare attributes and add trace name for root spans
|
|
63
75
|
span_attributes = sanitized_attributes.transform_keys(&:to_s).reject { |k, v| v.nil? }
|
|
64
76
|
|
|
65
|
-
# Set trace name if this is likely a root span (no parent in our stack)
|
|
66
|
-
|
|
77
|
+
# Set trace name if this is likely a root span (no parent in our stack),
|
|
78
|
+
# unless callers already specified one explicitly.
|
|
79
|
+
if current[:span_stack].length == 1 && !span_attributes.key?('langfuse.trace.name')
|
|
67
80
|
span_attributes['langfuse.trace.name'] = operation
|
|
68
81
|
end
|
|
69
82
|
|
|
@@ -72,6 +85,12 @@ module DSPy
|
|
|
72
85
|
|
|
73
86
|
# Get parent OpenTelemetry span for proper context propagation
|
|
74
87
|
parent_otel_span = current[:otel_span_stack].last
|
|
88
|
+
if !parent_otel_span && defined?(OpenTelemetry::Trace)
|
|
89
|
+
current_span = OpenTelemetry::Trace.current_span
|
|
90
|
+
if current_span && current_span != OpenTelemetry::Trace::Span::INVALID
|
|
91
|
+
parent_otel_span = current_span
|
|
92
|
+
end
|
|
93
|
+
end
|
|
75
94
|
|
|
76
95
|
# Create span with proper parent context
|
|
77
96
|
if parent_otel_span
|
|
@@ -84,20 +103,18 @@ module DSPy
|
|
|
84
103
|
) do |span|
|
|
85
104
|
# Add to our OpenTelemetry span stack
|
|
86
105
|
current[:otel_span_stack].push(span)
|
|
106
|
+
succeeded = false
|
|
87
107
|
|
|
88
108
|
begin
|
|
89
109
|
result = yield(span)
|
|
90
|
-
|
|
91
|
-
# Add explicit timing information to help Langfuse
|
|
92
|
-
if span
|
|
93
|
-
duration_ms = ((Time.now - otel_start_time) * 1000).round(3)
|
|
94
|
-
span.set_attribute('duration.ms', duration_ms)
|
|
95
|
-
span.set_attribute('langfuse.observation.startTime', otel_start_time.iso8601(3))
|
|
96
|
-
span.set_attribute('langfuse.observation.endTime', Time.now.iso8601(3))
|
|
97
|
-
end
|
|
98
|
-
|
|
110
|
+
succeeded = true
|
|
99
111
|
result
|
|
112
|
+
rescue StandardError => e
|
|
113
|
+
set_span_error_attributes(span, e)
|
|
114
|
+
raise
|
|
100
115
|
ensure
|
|
116
|
+
set_span_status_attribute(span, succeeded)
|
|
117
|
+
set_span_timing_attributes(span, otel_start_time)
|
|
101
118
|
# Remove from our OpenTelemetry span stack
|
|
102
119
|
current[:otel_span_stack].pop
|
|
103
120
|
end
|
|
@@ -112,20 +129,18 @@ module DSPy
|
|
|
112
129
|
) do |span|
|
|
113
130
|
# Add to our OpenTelemetry span stack
|
|
114
131
|
current[:otel_span_stack].push(span)
|
|
132
|
+
succeeded = false
|
|
115
133
|
|
|
116
134
|
begin
|
|
117
135
|
result = yield(span)
|
|
118
|
-
|
|
119
|
-
# Add explicit timing information to help Langfuse
|
|
120
|
-
if span
|
|
121
|
-
duration_ms = ((Time.now - otel_start_time) * 1000).round(3)
|
|
122
|
-
span.set_attribute('duration.ms', duration_ms)
|
|
123
|
-
span.set_attribute('langfuse.observation.startTime', otel_start_time.iso8601(3))
|
|
124
|
-
span.set_attribute('langfuse.observation.endTime', Time.now.iso8601(3))
|
|
125
|
-
end
|
|
126
|
-
|
|
136
|
+
succeeded = true
|
|
127
137
|
result
|
|
138
|
+
rescue StandardError => e
|
|
139
|
+
set_span_error_attributes(span, e)
|
|
140
|
+
raise
|
|
128
141
|
ensure
|
|
142
|
+
set_span_status_attribute(span, succeeded)
|
|
143
|
+
set_span_timing_attributes(span, otel_start_time)
|
|
129
144
|
# Remove from our OpenTelemetry span stack
|
|
130
145
|
current[:otel_span_stack].pop
|
|
131
146
|
end
|
|
@@ -216,7 +231,9 @@ module DSPy
|
|
|
216
231
|
fiber_id: Fiber.current.object_id,
|
|
217
232
|
span_stack: [],
|
|
218
233
|
otel_span_stack: [],
|
|
219
|
-
module_stack: []
|
|
234
|
+
module_stack: [],
|
|
235
|
+
request_id: nil,
|
|
236
|
+
request_start_time: nil
|
|
220
237
|
}
|
|
221
238
|
end
|
|
222
239
|
|
|
@@ -227,6 +244,8 @@ module DSPy
|
|
|
227
244
|
cloned[:module_stack] = Array(context[:module_stack]).map { |entry| entry.dup }
|
|
228
245
|
cloned[:thread_id] = Thread.current.object_id
|
|
229
246
|
cloned[:fiber_id] = Fiber.current.object_id
|
|
247
|
+
cloned[:request_id] = context[:request_id]
|
|
248
|
+
cloned[:request_start_time] = context[:request_start_time]
|
|
230
249
|
cloned
|
|
231
250
|
end
|
|
232
251
|
|
|
@@ -280,6 +299,36 @@ module DSPy
|
|
|
280
299
|
label: explicit_label || (module_instance.respond_to?(:module_scope_label) ? module_instance.module_scope_label : nil)
|
|
281
300
|
}
|
|
282
301
|
end
|
|
302
|
+
|
|
303
|
+
def set_span_timing_attributes(span, otel_start_time)
|
|
304
|
+
return unless span
|
|
305
|
+
|
|
306
|
+
now = Time.now
|
|
307
|
+
duration_ms = ((now - otel_start_time) * 1000).round(3)
|
|
308
|
+
span.set_attribute('duration.ms', duration_ms)
|
|
309
|
+
span.set_attribute('langfuse.observation.startTime', otel_start_time.iso8601(3))
|
|
310
|
+
span.set_attribute('langfuse.observation.endTime', now.iso8601(3))
|
|
311
|
+
rescue StandardError
|
|
312
|
+
nil
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def set_span_error_attributes(span, error)
|
|
316
|
+
return unless span
|
|
317
|
+
|
|
318
|
+
span.set_attribute('error', true)
|
|
319
|
+
span.set_attribute('error.type', error.class.name)
|
|
320
|
+
span.set_attribute('error.message', error.message.to_s[0, 2000]) if error.message
|
|
321
|
+
rescue StandardError
|
|
322
|
+
nil
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def set_span_status_attribute(span, succeeded)
|
|
326
|
+
return unless span
|
|
327
|
+
|
|
328
|
+
span.set_attribute('dspy.status', succeeded ? 'completed' : 'error')
|
|
329
|
+
rescue StandardError
|
|
330
|
+
nil
|
|
331
|
+
end
|
|
283
332
|
end
|
|
284
333
|
end
|
|
285
334
|
end
|
data/lib/dspy/evals/version.rb
CHANGED
data/lib/dspy/evals.rb
CHANGED
|
@@ -254,25 +254,7 @@ module DSPy
|
|
|
254
254
|
# Evaluate program on a single example
|
|
255
255
|
sig { params(example: T.untyped, trace: T.nilable(T.untyped)).returns(EvaluationResult) }
|
|
256
256
|
def call(example, trace: nil)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
DSPy::Context.with_span(
|
|
260
|
-
operation: 'evaluation.example',
|
|
261
|
-
'dspy.module' => 'Evaluator',
|
|
262
|
-
'evaluation.program' => @program.class.name,
|
|
263
|
-
'evaluation.has_metric' => !@metric.nil?
|
|
264
|
-
) do
|
|
265
|
-
begin
|
|
266
|
-
perform_call(example, trace: trace)
|
|
267
|
-
rescue => e
|
|
268
|
-
build_error_result(example, e, trace: trace)
|
|
269
|
-
end
|
|
270
|
-
end.then do |result|
|
|
271
|
-
@last_example_result = result
|
|
272
|
-
emit_example_observation(example, result)
|
|
273
|
-
run_callbacks(:after, :call, example: example, result: result)
|
|
274
|
-
result
|
|
275
|
-
end
|
|
257
|
+
call_with_program(@program, example, trace: trace, track_state: true)
|
|
276
258
|
end
|
|
277
259
|
|
|
278
260
|
# Evaluate program on multiple examples
|
|
@@ -403,8 +385,9 @@ module DSPy
|
|
|
403
385
|
|
|
404
386
|
futures = batch.map do |item|
|
|
405
387
|
Concurrent::Promises.future_on(executor) do
|
|
406
|
-
|
|
407
|
-
|
|
388
|
+
program_for_thread = fork_program_for_thread
|
|
389
|
+
[:ok, item[:index], safe_call(item[:example], program: program_for_thread, track_state: false)]
|
|
390
|
+
rescue StandardError => e
|
|
408
391
|
[:error, item[:index], e]
|
|
409
392
|
end
|
|
410
393
|
end
|
|
@@ -441,18 +424,18 @@ module DSPy
|
|
|
441
424
|
results.compact
|
|
442
425
|
end
|
|
443
426
|
|
|
444
|
-
def safe_call(example)
|
|
445
|
-
|
|
446
|
-
rescue => e
|
|
427
|
+
def safe_call(example, program: @program, track_state: true)
|
|
428
|
+
call_with_program(program, example, track_state: track_state)
|
|
429
|
+
rescue StandardError => e
|
|
447
430
|
build_error_result(example, e)
|
|
448
431
|
end
|
|
449
432
|
|
|
450
|
-
def perform_call(example, trace:)
|
|
433
|
+
def perform_call(example, trace:, program:)
|
|
451
434
|
# Extract input from example - support both hash and object formats
|
|
452
435
|
input_values = extract_input_values(example)
|
|
453
436
|
|
|
454
437
|
# Run prediction
|
|
455
|
-
prediction =
|
|
438
|
+
prediction = program.call(**input_values)
|
|
456
439
|
|
|
457
440
|
# Calculate metrics if provided
|
|
458
441
|
metrics = {}
|
|
@@ -469,7 +452,7 @@ module DSPy
|
|
|
469
452
|
passed = !!metric_result
|
|
470
453
|
metrics[:passed] = passed
|
|
471
454
|
end
|
|
472
|
-
rescue => e
|
|
455
|
+
rescue StandardError => e
|
|
473
456
|
passed = false
|
|
474
457
|
metrics[:error] = e.message
|
|
475
458
|
metrics[:passed] = false
|
|
@@ -490,6 +473,34 @@ module DSPy
|
|
|
490
473
|
)
|
|
491
474
|
end
|
|
492
475
|
|
|
476
|
+
def call_with_program(program, example, trace: nil, track_state: true)
|
|
477
|
+
run_callbacks(:before, :call, example: example)
|
|
478
|
+
|
|
479
|
+
DSPy::Context.with_span(
|
|
480
|
+
operation: 'evaluation.example',
|
|
481
|
+
'dspy.module' => 'Evaluator',
|
|
482
|
+
'evaluation.program' => program.class.name,
|
|
483
|
+
'evaluation.has_metric' => !@metric.nil?
|
|
484
|
+
) do
|
|
485
|
+
begin
|
|
486
|
+
perform_call(example, trace: trace, program: program)
|
|
487
|
+
rescue StandardError => e
|
|
488
|
+
build_error_result(example, e, trace: trace)
|
|
489
|
+
end
|
|
490
|
+
end.then do |result|
|
|
491
|
+
@last_example_result = result if track_state
|
|
492
|
+
emit_example_observation(example, result)
|
|
493
|
+
run_callbacks(:after, :call, example: example, result: result)
|
|
494
|
+
result
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def fork_program_for_thread
|
|
499
|
+
return @program if @program.nil?
|
|
500
|
+
return @program.dup_for_thread if @program.respond_to?(:dup_for_thread)
|
|
501
|
+
@program.dup
|
|
502
|
+
end
|
|
503
|
+
|
|
493
504
|
def build_error_result(example, error, trace: nil)
|
|
494
505
|
metrics = {
|
|
495
506
|
error: error.message,
|
|
@@ -680,7 +691,7 @@ module DSPy
|
|
|
680
691
|
if @export_scores
|
|
681
692
|
export_example_score(example, result)
|
|
682
693
|
end
|
|
683
|
-
rescue => e
|
|
694
|
+
rescue StandardError => e
|
|
684
695
|
DSPy.log('evals.example.observation_error', error: e.message)
|
|
685
696
|
end
|
|
686
697
|
|
|
@@ -698,7 +709,7 @@ module DSPy
|
|
|
698
709
|
if @export_scores
|
|
699
710
|
export_batch_score(batch_result)
|
|
700
711
|
end
|
|
701
|
-
rescue => e
|
|
712
|
+
rescue StandardError => e
|
|
702
713
|
DSPy.log('evals.batch.observation_error', error: e.message)
|
|
703
714
|
end
|
|
704
715
|
|
|
@@ -711,7 +722,7 @@ module DSPy
|
|
|
711
722
|
score_value,
|
|
712
723
|
comment: "Example: #{example_id || 'unknown'}, passed: #{result.passed}"
|
|
713
724
|
)
|
|
714
|
-
rescue => e
|
|
725
|
+
rescue StandardError => e
|
|
715
726
|
DSPy.log('evals.score_export_error', error: e.message)
|
|
716
727
|
end
|
|
717
728
|
|
|
@@ -721,7 +732,7 @@ module DSPy
|
|
|
721
732
|
batch_result.pass_rate,
|
|
722
733
|
comment: "Batch: #{batch_result.passed_examples}/#{batch_result.total_examples} passed"
|
|
723
734
|
)
|
|
724
|
-
rescue => e
|
|
735
|
+
rescue StandardError => e
|
|
725
736
|
DSPy.log('evals.batch_score_export_error', error: e.message)
|
|
726
737
|
end
|
|
727
738
|
|
data/lib/dspy/events.rb
CHANGED
|
@@ -11,7 +11,6 @@ module DSPy
|
|
|
11
11
|
class EventRegistry
|
|
12
12
|
def initialize
|
|
13
13
|
@listeners = {}
|
|
14
|
-
@subscription_counter = 0
|
|
15
14
|
@mutex = Mutex.new
|
|
16
15
|
end
|
|
17
16
|
|
|
@@ -53,7 +52,7 @@ module DSPy
|
|
|
53
52
|
matching_listeners.each do |id, listener|
|
|
54
53
|
begin
|
|
55
54
|
listener[:block].call(event_name, attributes)
|
|
56
|
-
rescue => e
|
|
55
|
+
rescue StandardError => e
|
|
57
56
|
# Log the error but continue processing other listeners
|
|
58
57
|
# Use emit_log directly to avoid infinite recursion
|
|
59
58
|
DSPy.send(:emit_log, 'event.listener.error', {
|
|
@@ -80,4 +79,4 @@ module DSPy
|
|
|
80
79
|
end
|
|
81
80
|
end
|
|
82
81
|
end
|
|
83
|
-
end
|
|
82
|
+
end
|
data/lib/dspy/example.rb
CHANGED
data/lib/dspy/lm/adapter.rb
CHANGED
|
@@ -57,6 +57,45 @@ module DSPy
|
|
|
57
57
|
content.is_a?(Array) && content.any? { |item| item[:type] == 'image' }
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
|
+
|
|
61
|
+
# Format multimodal messages for a specific provider
|
|
62
|
+
# @param messages [Array<Hash>] Array of message hashes
|
|
63
|
+
# @param provider_name [String] Provider name for image validation and formatting
|
|
64
|
+
# @return [Array<Hash>] Messages with images formatted for the provider
|
|
65
|
+
def format_multimodal_messages(messages, provider_name)
|
|
66
|
+
messages.map do |msg|
|
|
67
|
+
if msg[:content].is_a?(Array)
|
|
68
|
+
formatted_content = msg[:content].map do |item|
|
|
69
|
+
case item[:type]
|
|
70
|
+
when 'text'
|
|
71
|
+
{ type: 'text', text: item[:text] }
|
|
72
|
+
when 'image'
|
|
73
|
+
format_image_for_provider(item[:image], provider_name)
|
|
74
|
+
else
|
|
75
|
+
item
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
{ role: msg[:role], content: formatted_content }
|
|
79
|
+
else
|
|
80
|
+
msg
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Format an image for a specific provider
|
|
86
|
+
# @param image [DSPy::Image] The image to format
|
|
87
|
+
# @param provider_name [String] Provider name (openai, anthropic, gemini, etc.)
|
|
88
|
+
# @return [Hash] Provider-specific image format
|
|
89
|
+
def format_image_for_provider(image, provider_name)
|
|
90
|
+
image.validate_for_provider!(provider_name)
|
|
91
|
+
format_method = "to_#{provider_name}_format"
|
|
92
|
+
if image.respond_to?(format_method)
|
|
93
|
+
image.send(format_method)
|
|
94
|
+
else
|
|
95
|
+
# For providers without specific format methods, return the item as-is
|
|
96
|
+
{ type: 'image', image: image }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
60
99
|
end
|
|
61
100
|
end
|
|
62
101
|
end
|
|
@@ -38,17 +38,8 @@ module DSPy
|
|
|
38
38
|
# OpenAI/Ollama: try to extract JSON from various formats
|
|
39
39
|
extract_json_from_content(response.content)
|
|
40
40
|
elsif adapter_class_name.include?('AnthropicAdapter')
|
|
41
|
-
# Anthropic:
|
|
42
|
-
|
|
43
|
-
structured_outputs_enabled = true if structured_outputs_enabled.nil? # Default to true
|
|
44
|
-
|
|
45
|
-
if structured_outputs_enabled
|
|
46
|
-
extracted = extract_anthropic_tool_json(response)
|
|
47
|
-
extracted || extract_json_from_content(response.content)
|
|
48
|
-
else
|
|
49
|
-
# Skip tool extraction, use enhanced prompting extraction
|
|
50
|
-
extract_json_from_content(response.content)
|
|
51
|
-
end
|
|
41
|
+
# Anthropic: Beta API returns JSON in content, same as OpenAI/Gemini
|
|
42
|
+
extract_json_from_content(response.content)
|
|
52
43
|
elsif adapter_class_name.include?('GeminiAdapter')
|
|
53
44
|
# Gemini: try to extract JSON from various formats
|
|
54
45
|
extract_json_from_content(response.content)
|
|
@@ -90,25 +81,30 @@ module DSPy
|
|
|
90
81
|
# Anthropic preparation
|
|
91
82
|
sig { params(messages: T::Array[T::Hash[Symbol, T.untyped]], request_params: T::Hash[Symbol, T.untyped]).void }
|
|
92
83
|
def prepare_anthropic_request(messages, request_params)
|
|
93
|
-
|
|
94
|
-
|
|
84
|
+
begin
|
|
85
|
+
require "dspy/anthropic/lm/schema_converter"
|
|
86
|
+
rescue LoadError
|
|
87
|
+
msg = <<~MSG
|
|
88
|
+
Anthropic adapter is optional; structured output helpers will be unavailable until the gem is installed.
|
|
89
|
+
Add `gem 'dspy-anthropic'` to your Gemfile and run `bundle install`.
|
|
90
|
+
MSG
|
|
91
|
+
raise DSPy::LM::MissingAdapterError, msg
|
|
92
|
+
end
|
|
95
93
|
|
|
96
|
-
#
|
|
94
|
+
# Only use Beta API structured outputs if enabled (default: true)
|
|
95
|
+
structured_outputs_enabled = adapter.instance_variable_get(:@structured_outputs_enabled)
|
|
97
96
|
structured_outputs_enabled = true if structured_outputs_enabled.nil?
|
|
98
97
|
|
|
99
98
|
return unless structured_outputs_enabled
|
|
100
99
|
|
|
101
|
-
#
|
|
102
|
-
|
|
100
|
+
# Use Anthropic Beta API structured outputs
|
|
101
|
+
schema = DSPy::Anthropic::LM::SchemaConverter.to_beta_format(signature_class)
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
request_params[:
|
|
109
|
-
type: "tool",
|
|
110
|
-
name: "json_output"
|
|
111
|
-
}
|
|
103
|
+
request_params[:output_format] = ::Anthropic::Models::Beta::BetaJSONOutputFormat.new(
|
|
104
|
+
type: :json_schema,
|
|
105
|
+
schema: schema
|
|
106
|
+
)
|
|
107
|
+
request_params[:betas] = ["structured-outputs-2025-11-13"]
|
|
112
108
|
end
|
|
113
109
|
|
|
114
110
|
# Gemini preparation
|
|
@@ -135,54 +131,19 @@ module DSPy
|
|
|
135
131
|
end
|
|
136
132
|
end
|
|
137
133
|
|
|
138
|
-
# Convert signature to Anthropic tool schema
|
|
139
|
-
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
140
|
-
def convert_to_anthropic_tool_schema
|
|
141
|
-
output_fields = signature_class.output_field_descriptors
|
|
142
|
-
|
|
143
|
-
{
|
|
144
|
-
name: "json_output",
|
|
145
|
-
description: "Output the result in the required JSON format",
|
|
146
|
-
input_schema: {
|
|
147
|
-
type: "object",
|
|
148
|
-
properties: build_properties_from_fields(output_fields),
|
|
149
|
-
required: output_fields.keys.map(&:to_s)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
# Build JSON schema properties from output fields
|
|
155
|
-
sig { params(fields: T::Hash[Symbol, T.untyped]).returns(T::Hash[String, T.untyped]) }
|
|
156
|
-
def build_properties_from_fields(fields)
|
|
157
|
-
properties = {}
|
|
158
|
-
fields.each do |field_name, descriptor|
|
|
159
|
-
properties[field_name.to_s] = DSPy::TypeSystem::SorbetJsonSchema.type_to_json_schema(descriptor.type)
|
|
160
|
-
end
|
|
161
|
-
properties
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# Extract JSON from Anthropic tool use response
|
|
165
|
-
sig { params(response: DSPy::LM::Response).returns(T.nilable(String)) }
|
|
166
|
-
def extract_anthropic_tool_json(response)
|
|
167
|
-
# Check for tool calls in metadata
|
|
168
|
-
if response.metadata.respond_to?(:tool_calls) && response.metadata.tool_calls
|
|
169
|
-
tool_calls = response.metadata.tool_calls
|
|
170
|
-
if tool_calls.is_a?(Array) && !tool_calls.empty?
|
|
171
|
-
first_call = tool_calls.first
|
|
172
|
-
if first_call[:name] == "json_output" && first_call[:input]
|
|
173
|
-
return JSON.generate(first_call[:input])
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
nil
|
|
179
|
-
end
|
|
180
|
-
|
|
181
134
|
# Extract JSON from content that may contain markdown or plain JSON
|
|
182
135
|
sig { params(content: String).returns(String) }
|
|
183
136
|
def extract_json_from_content(content)
|
|
184
137
|
return content if content.nil? || content.empty?
|
|
185
138
|
|
|
139
|
+
# Fix Anthropic Beta API bug with optional fields producing invalid JSON
|
|
140
|
+
# When some output fields are optional and not returned, Anthropic's structured outputs
|
|
141
|
+
# can produce trailing comma+brace: {"field1": {...},} instead of {"field1": {...}}
|
|
142
|
+
# This workaround removes the invalid trailing syntax before JSON parsing
|
|
143
|
+
if content =~ /,\s*\}\s*$/
|
|
144
|
+
content = content.sub(/,(\s*\}\s*)$/, '\1')
|
|
145
|
+
end
|
|
146
|
+
|
|
186
147
|
# Try 1: Check for ```json code block (with or without preceding text)
|
|
187
148
|
if content.include?('```json')
|
|
188
149
|
json_match = content.match(/```json\s*\n(.*?)\n```/m)
|
data/lib/dspy/lm/message.rb
CHANGED
data/lib/dspy/lm/response.rb
CHANGED
|
@@ -118,7 +118,7 @@ module DSPy
|
|
|
118
118
|
extend T::Sig
|
|
119
119
|
|
|
120
120
|
const :content, String
|
|
121
|
-
const :usage, T.nilable(T.any(Usage, OpenAIUsage)), default: nil
|
|
121
|
+
const :usage, T.nilable(T.any(Usage, OpenAIUsage, AnthropicUsage)), default: nil
|
|
122
122
|
const :metadata, T.any(ResponseMetadata, OpenAIResponseMetadata, AnthropicResponseMetadata, GeminiResponseMetadata, T::Hash[Symbol, T.untyped])
|
|
123
123
|
|
|
124
124
|
sig { returns(String) }
|
|
@@ -182,7 +182,7 @@ module DSPy
|
|
|
182
182
|
else
|
|
183
183
|
ResponseMetadata.new(**common_fields)
|
|
184
184
|
end
|
|
185
|
-
rescue => e
|
|
185
|
+
rescue StandardError => e
|
|
186
186
|
DSPy.logger.debug("Failed to create response metadata: #{e.message}")
|
|
187
187
|
# Fallback to basic metadata
|
|
188
188
|
ResponseMetadata.new(
|