dspy 0.34.1 → 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/README.md +139 -216
- data/lib/dspy/chain_of_thought.rb +3 -2
- data/lib/dspy/context.rb +57 -30
- 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 +27 -79
- 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
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
|
@@ -42,15 +42,11 @@ module DSPy
|
|
|
42
42
|
|
|
43
43
|
def chat(inference_module, input_values, &block)
|
|
44
44
|
# Capture the current DSPy context before entering Sync block
|
|
45
|
-
parent_context = DSPy::Context.current
|
|
45
|
+
parent_context = DSPy::Context.current
|
|
46
46
|
|
|
47
47
|
Sync do
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
thread_key = :"dspy_context_#{Thread.current.object_id}"
|
|
51
|
-
Thread.current[thread_key] = parent_context
|
|
52
|
-
Thread.current[:dspy_context] = parent_context # Keep for backward compatibility
|
|
53
|
-
Fiber[:dspy_context] = parent_context
|
|
48
|
+
# Isolate fiber context while preserving trace/module ancestry
|
|
49
|
+
Fiber[:dspy_context] = DSPy::Context.fork_context(parent_context)
|
|
54
50
|
|
|
55
51
|
signature_class = inference_module.signature_class
|
|
56
52
|
|
|
@@ -136,29 +132,6 @@ module DSPy
|
|
|
136
132
|
response
|
|
137
133
|
end
|
|
138
134
|
|
|
139
|
-
# Determines if LM-level events should be emitted using smart consolidation
|
|
140
|
-
def should_emit_lm_events?
|
|
141
|
-
# Emit LM events only if we're not in a nested context (smart consolidation)
|
|
142
|
-
!is_nested_context?
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Determines if we're in a nested context where higher-level events are being emitted
|
|
146
|
-
def is_nested_context?
|
|
147
|
-
caller_locations = caller_locations(1, 30)
|
|
148
|
-
return false if caller_locations.nil?
|
|
149
|
-
|
|
150
|
-
# Look for higher-level DSPy modules in the call stack
|
|
151
|
-
# We consider ChainOfThought and ReAct as higher-level modules
|
|
152
|
-
higher_level_modules = caller_locations.select do |loc|
|
|
153
|
-
loc.path.include?('chain_of_thought') ||
|
|
154
|
-
loc.path.include?('re_act') ||
|
|
155
|
-
loc.path.include?('react')
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
# If we have higher-level modules in the call stack, we're in a nested context
|
|
159
|
-
higher_level_modules.any?
|
|
160
|
-
end
|
|
161
|
-
|
|
162
135
|
def parse_model_id(model_id)
|
|
163
136
|
unless model_id.include?('/')
|
|
164
137
|
raise ArgumentError, "model_id must include provider (e.g., 'openai/gpt-4', 'anthropic/claude-3'). Legacy format without provider is no longer supported."
|
|
@@ -173,7 +146,7 @@ module DSPy
|
|
|
173
146
|
|
|
174
147
|
# Determine if structured outputs will be used and wrap prompt if so
|
|
175
148
|
base_prompt = inference_module.prompt
|
|
176
|
-
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)
|
|
177
150
|
StructuredOutputsPrompt.new(**base_prompt.to_h)
|
|
178
151
|
else
|
|
179
152
|
base_prompt
|
|
@@ -198,8 +171,9 @@ module DSPy
|
|
|
198
171
|
messages
|
|
199
172
|
end
|
|
200
173
|
|
|
201
|
-
def will_use_structured_outputs?(signature_class)
|
|
174
|
+
def will_use_structured_outputs?(signature_class, data_format: nil)
|
|
202
175
|
return false unless signature_class
|
|
176
|
+
return false if data_format == :toon
|
|
203
177
|
|
|
204
178
|
adapter_class_name = adapter.class.name
|
|
205
179
|
|
|
@@ -354,8 +328,9 @@ module DSPy
|
|
|
354
328
|
})
|
|
355
329
|
|
|
356
330
|
# Add timing and request correlation if available
|
|
357
|
-
|
|
358
|
-
|
|
331
|
+
context = DSPy::Context.current
|
|
332
|
+
request_id = context[:request_id]
|
|
333
|
+
start_time = context[:request_start_time]
|
|
359
334
|
|
|
360
335
|
if request_id
|
|
361
336
|
event_attributes['request_id'] = request_id
|
|
@@ -411,53 +386,21 @@ module DSPy
|
|
|
411
386
|
end
|
|
412
387
|
end
|
|
413
388
|
|
|
414
|
-
public
|
|
415
|
-
|
|
416
|
-
def validate_messages!(messages)
|
|
417
|
-
unless messages.is_a?(Array)
|
|
418
|
-
raise ArgumentError, "messages must be an array"
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
messages.each_with_index do |message, index|
|
|
422
|
-
# Accept both Message objects and hash format for backward compatibility
|
|
423
|
-
if message.is_a?(Message)
|
|
424
|
-
# Already validated by type system
|
|
425
|
-
next
|
|
426
|
-
elsif message.is_a?(Hash) && message.key?(:role) && message.key?(:content)
|
|
427
|
-
# Legacy hash format - validate role
|
|
428
|
-
valid_roles = %w[system user assistant]
|
|
429
|
-
unless valid_roles.include?(message[:role])
|
|
430
|
-
raise ArgumentError, "Invalid role at index #{index}: #{message[:role]}. Must be one of: #{valid_roles.join(', ')}"
|
|
431
|
-
end
|
|
432
|
-
else
|
|
433
|
-
raise ArgumentError, "Message at index #{index} must be a Message object or hash with :role and :content"
|
|
434
|
-
end
|
|
435
|
-
end
|
|
436
|
-
end
|
|
437
|
-
|
|
438
389
|
def execute_raw_chat(messages, &streaming_block)
|
|
439
390
|
# Generate unique request ID for tracking
|
|
440
391
|
request_id = SecureRandom.hex(8)
|
|
441
392
|
start_time = Time.now
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
Thread.current[:dspy_request_id] = request_id
|
|
445
|
-
Thread.current[:dspy_request_start_time] = start_time
|
|
446
|
-
|
|
447
|
-
begin
|
|
393
|
+
|
|
394
|
+
DSPy::Context.with_request(request_id, start_time) do
|
|
448
395
|
response = instrument_lm_request(messages, 'RawPrompt') do
|
|
449
396
|
# Convert messages to hash format for adapter
|
|
450
397
|
hash_messages = messages_to_hash_array(messages)
|
|
451
398
|
# Direct adapter call, no strategies or JSON parsing
|
|
452
399
|
adapter.chat(messages: hash_messages, signature: nil, &streaming_block)
|
|
453
400
|
end
|
|
454
|
-
|
|
401
|
+
|
|
455
402
|
# Return raw response content, not parsed JSON
|
|
456
403
|
response.content
|
|
457
|
-
ensure
|
|
458
|
-
# Clean up thread-local storage
|
|
459
|
-
Thread.current[:dspy_request_id] = nil
|
|
460
|
-
Thread.current[:dspy_request_start_time] = nil
|
|
461
404
|
end
|
|
462
405
|
end
|
|
463
406
|
|
|
@@ -475,23 +418,28 @@ module DSPy
|
|
|
475
418
|
messages.each_with_index do |msg, index|
|
|
476
419
|
if msg.is_a?(Message)
|
|
477
420
|
normalized << msg
|
|
478
|
-
elsif msg.is_a?(Hash)
|
|
479
|
-
|
|
480
|
-
unless
|
|
421
|
+
elsif msg.is_a?(Hash) || msg.respond_to?(:to_h)
|
|
422
|
+
data = msg.is_a?(Hash) ? msg : msg.to_h
|
|
423
|
+
unless data.is_a?(Hash)
|
|
424
|
+
raise ArgumentError, "Message at index #{index} must be a Message object or hash with :role and :content"
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
normalized_hash = data.transform_keys(&:to_sym)
|
|
428
|
+
unless normalized_hash.key?(:role) && normalized_hash.key?(:content)
|
|
481
429
|
raise ArgumentError, "Message at index #{index} must have :role and :content"
|
|
482
430
|
end
|
|
483
|
-
|
|
484
|
-
|
|
431
|
+
|
|
432
|
+
role = normalized_hash[:role].to_s
|
|
485
433
|
valid_roles = %w[system user assistant]
|
|
486
|
-
unless valid_roles.include?(
|
|
487
|
-
raise ArgumentError, "Invalid role at index #{index}: #{
|
|
434
|
+
unless valid_roles.include?(role)
|
|
435
|
+
raise ArgumentError, "Invalid role at index #{index}: #{normalized_hash[:role]}. Must be one of: #{valid_roles.join(', ')}"
|
|
488
436
|
end
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
message = MessageFactory.create(msg)
|
|
437
|
+
|
|
438
|
+
message = MessageFactory.create(normalized_hash)
|
|
492
439
|
if message.nil?
|
|
493
440
|
raise ArgumentError, "Failed to create Message from hash at index #{index}"
|
|
494
441
|
end
|
|
442
|
+
|
|
495
443
|
normalized << message
|
|
496
444
|
else
|
|
497
445
|
raise ArgumentError, "Message at index #{index} must be a Message object or hash with :role and :content"
|