dspy 0.34.2 → 0.34.3
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/lib/dspy/chain_of_thought.rb +3 -2
- data/lib/dspy/context.rb +17 -1
- 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 +37 -2
- data/lib/dspy/lm/message.rb +1 -1
- data/lib/dspy/lm/response.rb +1 -1
- data/lib/dspy/lm/usage.rb +4 -4
- data/lib/dspy/lm.rb +9 -49
- data/lib/dspy/mixins/type_coercion.rb +189 -30
- data/lib/dspy/module.rb +70 -25
- data/lib/dspy/predict.rb +32 -5
- data/lib/dspy/prediction.rb +15 -57
- data/lib/dspy/prompt.rb +50 -30
- 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 +0 -162
- 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/storage/program_storage.rb +2 -2
- data/lib/dspy/structured_outputs_prompt.rb +3 -3
- 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/version.rb +1 -1
- data/lib/dspy.rb +1 -4
- metadata +1 -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: 01f38786c88d525a1031cf41931f578c3d2dcbfa29ee6a8dac1a381cafe47edf
|
|
4
|
+
data.tar.gz: 6334bfb483b3011fa91e163f688127be763a126ea7cd0edc44f07b0557dc2a30
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 744087dd87e936b247d194539407f2a74b29d5e6a28b4ba872c4aa0ef77103c4a6957c97b6bed3ee7e8ef899824f3e6e0f40c2b429c47312aa10924bb1fbca3c
|
|
7
|
+
data.tar.gz: 4e343687e84570d199ce9c7695d19d0a0a551cac66693fda131fe03268d3907e2d20f4648530d1e6a5de0a73092b03f3ec7bcec877d9c23662332193aaee0e31
|
|
@@ -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
|
|
@@ -216,7 +228,9 @@ module DSPy
|
|
|
216
228
|
fiber_id: Fiber.current.object_id,
|
|
217
229
|
span_stack: [],
|
|
218
230
|
otel_span_stack: [],
|
|
219
|
-
module_stack: []
|
|
231
|
+
module_stack: [],
|
|
232
|
+
request_id: nil,
|
|
233
|
+
request_start_time: nil
|
|
220
234
|
}
|
|
221
235
|
end
|
|
222
236
|
|
|
@@ -227,6 +241,8 @@ module DSPy
|
|
|
227
241
|
cloned[:module_stack] = Array(context[:module_stack]).map { |entry| entry.dup }
|
|
228
242
|
cloned[:thread_id] = Thread.current.object_id
|
|
229
243
|
cloned[:fiber_id] = Fiber.current.object_id
|
|
244
|
+
cloned[:request_id] = context[:request_id]
|
|
245
|
+
cloned[:request_start_time] = context[:request_start_time]
|
|
230
246
|
cloned
|
|
231
247
|
end
|
|
232
248
|
|
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
|
|
@@ -136,19 +136,54 @@ module DSPy
|
|
|
136
136
|
end
|
|
137
137
|
|
|
138
138
|
# Convert signature to Anthropic tool schema
|
|
139
|
+
# Uses strict: true for constrained decoding (Anthropic structured outputs)
|
|
140
|
+
# Anthropic strict mode requires ALL properties in required at every level.
|
|
139
141
|
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
140
142
|
def convert_to_anthropic_tool_schema
|
|
141
143
|
output_fields = signature_class.output_field_descriptors
|
|
142
144
|
|
|
143
|
-
{
|
|
145
|
+
schema = {
|
|
144
146
|
name: "json_output",
|
|
145
147
|
description: "Output the result in the required JSON format",
|
|
148
|
+
strict: true,
|
|
146
149
|
input_schema: {
|
|
147
150
|
type: "object",
|
|
148
151
|
properties: build_properties_from_fields(output_fields),
|
|
149
|
-
required: output_fields
|
|
152
|
+
required: build_required_from_fields(output_fields),
|
|
153
|
+
additionalProperties: false
|
|
150
154
|
}
|
|
151
155
|
}
|
|
156
|
+
|
|
157
|
+
# Anthropic strict mode: ALL properties must be in required at every level.
|
|
158
|
+
# Non-required properties get auto-wrapped in null unions by the grammar compiler,
|
|
159
|
+
# which counts against the 16-union-parameter limit.
|
|
160
|
+
enforce_all_required(schema[:input_schema])
|
|
161
|
+
|
|
162
|
+
schema
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Build required field list, excluding fields that have defaults
|
|
166
|
+
sig { params(fields: T::Hash[Symbol, T.untyped]).returns(T::Array[String]) }
|
|
167
|
+
def build_required_from_fields(fields)
|
|
168
|
+
fields.reject { |_name, descriptor| descriptor.has_default }.keys.map(&:to_s)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Recursively enforce that all properties are in required and
|
|
172
|
+
# additionalProperties is false, as required by Anthropic strict mode.
|
|
173
|
+
sig { params(schema: T::Hash[Symbol, T.untyped]).void }
|
|
174
|
+
def enforce_all_required(schema)
|
|
175
|
+
return unless schema.is_a?(Hash)
|
|
176
|
+
|
|
177
|
+
if schema[:type] == "object" && schema[:properties]
|
|
178
|
+
schema[:required] = schema[:properties].keys.map(&:to_s)
|
|
179
|
+
schema[:additionalProperties] = false
|
|
180
|
+
schema[:properties].each_value { |v| enforce_all_required(v) }
|
|
181
|
+
elsif schema[:type] == "array" && schema[:items]
|
|
182
|
+
enforce_all_required(schema[:items])
|
|
183
|
+
elsif schema[:type].is_a?(Array)
|
|
184
|
+
# type: ["array", "null"] — check items if present
|
|
185
|
+
enforce_all_required(schema[:items]) if schema[:items]
|
|
186
|
+
end
|
|
152
187
|
end
|
|
153
188
|
|
|
154
189
|
# Build JSON schema properties from output fields
|
data/lib/dspy/lm/message.rb
CHANGED
data/lib/dspy/lm/response.rb
CHANGED
data/lib/dspy/lm/usage.rb
CHANGED
|
@@ -99,7 +99,7 @@ module DSPy
|
|
|
99
99
|
prompt_tokens_details: prompt_details,
|
|
100
100
|
completion_tokens_details: completion_details
|
|
101
101
|
)
|
|
102
|
-
rescue => e
|
|
102
|
+
rescue StandardError => e
|
|
103
103
|
DSPy.logger.debug("Failed to create OpenAI usage: #{e.message}")
|
|
104
104
|
nil
|
|
105
105
|
end
|
|
@@ -133,7 +133,7 @@ module DSPy
|
|
|
133
133
|
output_tokens: output_tokens,
|
|
134
134
|
total_tokens: total_tokens
|
|
135
135
|
)
|
|
136
|
-
rescue => e
|
|
136
|
+
rescue StandardError => e
|
|
137
137
|
DSPy.logger.debug("Failed to create Anthropic usage: #{e.message}")
|
|
138
138
|
nil
|
|
139
139
|
end
|
|
@@ -150,7 +150,7 @@ module DSPy
|
|
|
150
150
|
output_tokens: output_tokens,
|
|
151
151
|
total_tokens: total_tokens
|
|
152
152
|
)
|
|
153
|
-
rescue => e
|
|
153
|
+
rescue StandardError => e
|
|
154
154
|
DSPy.logger.debug("Failed to create Gemini usage: #{e.message}")
|
|
155
155
|
nil
|
|
156
156
|
end
|
|
@@ -167,7 +167,7 @@ module DSPy
|
|
|
167
167
|
output_tokens: output_tokens,
|
|
168
168
|
total_tokens: total_tokens
|
|
169
169
|
)
|
|
170
|
-
rescue => e
|
|
170
|
+
rescue StandardError => e
|
|
171
171
|
DSPy.logger.debug("Failed to create generic usage: #{e.message}")
|
|
172
172
|
nil
|
|
173
173
|
end
|
data/lib/dspy/lm.rb
CHANGED
|
@@ -146,7 +146,7 @@ module DSPy
|
|
|
146
146
|
|
|
147
147
|
# Determine if structured outputs will be used and wrap prompt if so
|
|
148
148
|
base_prompt = inference_module.prompt
|
|
149
|
-
prompt = if will_use_structured_outputs?(inference_module.signature_class)
|
|
149
|
+
prompt = if will_use_structured_outputs?(inference_module.signature_class, data_format: base_prompt.data_format)
|
|
150
150
|
StructuredOutputsPrompt.new(**base_prompt.to_h)
|
|
151
151
|
else
|
|
152
152
|
base_prompt
|
|
@@ -171,8 +171,9 @@ module DSPy
|
|
|
171
171
|
messages
|
|
172
172
|
end
|
|
173
173
|
|
|
174
|
-
def will_use_structured_outputs?(signature_class)
|
|
174
|
+
def will_use_structured_outputs?(signature_class, data_format: nil)
|
|
175
175
|
return false unless signature_class
|
|
176
|
+
return false if data_format == :toon
|
|
176
177
|
|
|
177
178
|
adapter_class_name = adapter.class.name
|
|
178
179
|
|
|
@@ -327,8 +328,9 @@ module DSPy
|
|
|
327
328
|
})
|
|
328
329
|
|
|
329
330
|
# Add timing and request correlation if available
|
|
330
|
-
|
|
331
|
-
|
|
331
|
+
context = DSPy::Context.current
|
|
332
|
+
request_id = context[:request_id]
|
|
333
|
+
start_time = context[:request_start_time]
|
|
332
334
|
|
|
333
335
|
if request_id
|
|
334
336
|
event_attributes['request_id'] = request_id
|
|
@@ -384,63 +386,21 @@ module DSPy
|
|
|
384
386
|
end
|
|
385
387
|
end
|
|
386
388
|
|
|
387
|
-
public
|
|
388
|
-
|
|
389
|
-
def validate_messages!(messages)
|
|
390
|
-
unless messages.is_a?(Array)
|
|
391
|
-
raise ArgumentError, "messages must be an array"
|
|
392
|
-
end
|
|
393
|
-
|
|
394
|
-
messages.each_with_index do |message, index|
|
|
395
|
-
# Accept both Message objects and hash format for backward compatibility
|
|
396
|
-
if message.is_a?(Message)
|
|
397
|
-
# Already validated by type system
|
|
398
|
-
next
|
|
399
|
-
elsif message.is_a?(Hash) || message.respond_to?(:to_h)
|
|
400
|
-
data = message.is_a?(Hash) ? message : message.to_h
|
|
401
|
-
unless data.is_a?(Hash)
|
|
402
|
-
raise ArgumentError, "Message at index #{index} must be a Message object or hash with :role and :content"
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
normalized = data.transform_keys(&:to_sym)
|
|
406
|
-
unless normalized.key?(:role) && normalized.key?(:content)
|
|
407
|
-
raise ArgumentError, "Message at index #{index} must have :role and :content"
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
role = normalized[:role].to_s
|
|
411
|
-
valid_roles = %w[system user assistant]
|
|
412
|
-
unless valid_roles.include?(role)
|
|
413
|
-
raise ArgumentError, "Invalid role at index #{index}: #{normalized[:role]}. Must be one of: #{valid_roles.join(', ')}"
|
|
414
|
-
end
|
|
415
|
-
else
|
|
416
|
-
raise ArgumentError, "Message at index #{index} must be a Message object or hash with :role and :content"
|
|
417
|
-
end
|
|
418
|
-
end
|
|
419
|
-
end
|
|
420
|
-
|
|
421
389
|
def execute_raw_chat(messages, &streaming_block)
|
|
422
390
|
# Generate unique request ID for tracking
|
|
423
391
|
request_id = SecureRandom.hex(8)
|
|
424
392
|
start_time = Time.now
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
Thread.current[:dspy_request_id] = request_id
|
|
428
|
-
Thread.current[:dspy_request_start_time] = start_time
|
|
429
|
-
|
|
430
|
-
begin
|
|
393
|
+
|
|
394
|
+
DSPy::Context.with_request(request_id, start_time) do
|
|
431
395
|
response = instrument_lm_request(messages, 'RawPrompt') do
|
|
432
396
|
# Convert messages to hash format for adapter
|
|
433
397
|
hash_messages = messages_to_hash_array(messages)
|
|
434
398
|
# Direct adapter call, no strategies or JSON parsing
|
|
435
399
|
adapter.chat(messages: hash_messages, signature: nil, &streaming_block)
|
|
436
400
|
end
|
|
437
|
-
|
|
401
|
+
|
|
438
402
|
# Return raw response content, not parsed JSON
|
|
439
403
|
response.content
|
|
440
|
-
ensure
|
|
441
|
-
# Clean up thread-local storage
|
|
442
|
-
Thread.current[:dspy_request_id] = nil
|
|
443
|
-
Thread.current[:dspy_request_start_time] = nil
|
|
444
404
|
end
|
|
445
405
|
end
|
|
446
406
|
|