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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dspy/chain_of_thought.rb +3 -2
  3. data/lib/dspy/context.rb +17 -1
  4. data/lib/dspy/evals/version.rb +1 -1
  5. data/lib/dspy/evals.rb +42 -31
  6. data/lib/dspy/events.rb +2 -3
  7. data/lib/dspy/example.rb +1 -1
  8. data/lib/dspy/lm/adapter.rb +39 -0
  9. data/lib/dspy/lm/json_strategy.rb +37 -2
  10. data/lib/dspy/lm/message.rb +1 -1
  11. data/lib/dspy/lm/response.rb +1 -1
  12. data/lib/dspy/lm/usage.rb +4 -4
  13. data/lib/dspy/lm.rb +9 -49
  14. data/lib/dspy/mixins/type_coercion.rb +189 -30
  15. data/lib/dspy/module.rb +70 -25
  16. data/lib/dspy/predict.rb +32 -5
  17. data/lib/dspy/prediction.rb +15 -57
  18. data/lib/dspy/prompt.rb +50 -30
  19. data/lib/dspy/propose/dataset_summary_generator.rb +1 -1
  20. data/lib/dspy/propose/grounded_proposer.rb +3 -3
  21. data/lib/dspy/re_act.rb +0 -162
  22. data/lib/dspy/registry/signature_registry.rb +3 -3
  23. data/lib/dspy/ruby_llm/lm/adapters/ruby_llm_adapter.rb +1 -27
  24. data/lib/dspy/schema/sorbet_json_schema.rb +7 -6
  25. data/lib/dspy/schema/version.rb +1 -1
  26. data/lib/dspy/schema_adapters.rb +1 -1
  27. data/lib/dspy/storage/program_storage.rb +2 -2
  28. data/lib/dspy/structured_outputs_prompt.rb +3 -3
  29. data/lib/dspy/teleprompt/utils.rb +2 -2
  30. data/lib/dspy/tools/github_cli_toolset.rb +7 -7
  31. data/lib/dspy/tools/text_processing_toolset.rb +2 -2
  32. data/lib/dspy/tools/toolset.rb +1 -1
  33. data/lib/dspy/version.rb +1 -1
  34. data/lib/dspy.rb +1 -4
  35. metadata +1 -26
  36. data/lib/dspy/events/subscriber_mixin.rb +0 -79
  37. data/lib/dspy/events/subscribers.rb +0 -43
  38. data/lib/dspy/memory/embedding_engine.rb +0 -68
  39. data/lib/dspy/memory/in_memory_store.rb +0 -216
  40. data/lib/dspy/memory/local_embedding_engine.rb +0 -244
  41. data/lib/dspy/memory/memory_compactor.rb +0 -298
  42. data/lib/dspy/memory/memory_manager.rb +0 -266
  43. data/lib/dspy/memory/memory_record.rb +0 -163
  44. data/lib/dspy/memory/memory_store.rb +0 -90
  45. data/lib/dspy/memory.rb +0 -30
  46. data/lib/dspy/tools/memory_toolset.rb +0 -117
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7fe4cbffffd520f31219caa7dbce4b75ef39684cdd5e422b60144aa3ae3329e9
4
- data.tar.gz: '0974465586686c0b93292b464f9b49abcb10a265ca85295c65584e989a271d7f'
3
+ metadata.gz: 01f38786c88d525a1031cf41931f578c3d2dcbfa29ee6a8dac1a381cafe47edf
4
+ data.tar.gz: 6334bfb483b3011fa91e163f688127be763a126ea7cd0edc44f07b0557dc2a30
5
5
  SHA512:
6
- metadata.gz: 5602b8a59a8454306921a2528ccb58824cfe3878a4d31f3b8b4b93d94730c6f2e804c439a9fd1f7bb44a2afe6aff6cb3843e1d7ad6f45c51e4e01aec7d44db5c
7
- data.tar.gz: 4bf2656ade52cbf55e6416a061e9107c9332a0e669b9bafe07acf045aac605eaa38410ae433268b78a7d285b47450e03e8416e9a456d978e258177fce00a90e9
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.config.lm = self.lm # Use the same LM configuration
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
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module DSPy
4
4
  class Evals
5
- VERSION = '1.0.1'
5
+ VERSION = '1.0.2'
6
6
  end
7
7
  end
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
- run_callbacks(:before, :call, example: example)
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
- [:ok, item[:index], safe_call(item[:example])]
407
- rescue => e
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
- call(example)
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 = @program.call(**input_values)
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
@@ -178,7 +178,7 @@ module DSPy
178
178
  id: example_data[:id] || "example_#{index}"
179
179
  )
180
180
  examples << example
181
- rescue => e
181
+ rescue StandardError => e
182
182
  errors << "Example #{index}: #{e.message}"
183
183
  end
184
184
  end
@@ -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.keys.map(&:to_s)
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
@@ -154,7 +154,7 @@ module DSPy
154
154
  content: formatted_content,
155
155
  name: data[:name]&.to_s
156
156
  )
157
- rescue => e
157
+ rescue StandardError => e
158
158
  DSPy.logger.debug("Failed to create Message: #{e.message}")
159
159
  nil
160
160
  end
@@ -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(
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
- request_id = Thread.current[:dspy_request_id]
331
- start_time = Thread.current[:dspy_request_start_time]
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
- # Store request context for correlation
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