dspy 0.24.1 → 0.25.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.
@@ -0,0 +1,35 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module DSPy
5
+ module Utils
6
+ class Serialization
7
+ extend T::Sig
8
+
9
+ # Deep serializes any T::Struct objects to hashes for observability
10
+ sig { params(obj: T.untyped).returns(T.untyped) }
11
+ def self.deep_serialize(obj)
12
+ case obj
13
+ when T::Struct
14
+ # Use the serialize method to convert to a plain hash
15
+ deep_serialize(obj.serialize)
16
+ when Hash
17
+ # Recursively serialize hash values
18
+ obj.transform_values { |v| deep_serialize(v) }
19
+ when Array
20
+ # Recursively serialize array elements
21
+ obj.map { |v| deep_serialize(v) }
22
+ else
23
+ # Return primitive values as-is
24
+ obj
25
+ end
26
+ end
27
+
28
+ # Serializes an object to JSON with proper T::Struct handling
29
+ sig { params(obj: T.untyped).returns(String) }
30
+ def self.to_json(obj)
31
+ deep_serialize(obj).to_json
32
+ end
33
+ end
34
+ end
35
+ end
data/lib/dspy/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DSPy
4
- VERSION = "0.24.1"
4
+ VERSION = "0.25.0"
5
5
  end
data/lib/dspy.rb CHANGED
@@ -14,10 +14,10 @@ require_relative 'dspy/events/types'
14
14
 
15
15
  module DSPy
16
16
  extend Dry::Configurable
17
-
17
+
18
18
  setting :lm
19
19
  setting :logger, default: Dry.Logger(:dspy, formatter: :string)
20
-
20
+
21
21
  # Structured output configuration for LLM providers
22
22
  setting :structured_outputs do
23
23
  setting :openai, default: false
@@ -27,7 +27,7 @@ module DSPy
27
27
  setting :max_retries, default: 3
28
28
  setting :fallback_enabled, default: true
29
29
  end
30
-
30
+
31
31
  # Test mode disables sleeps in retry logic
32
32
  setting :test_mode, default: false
33
33
 
@@ -35,14 +35,13 @@ module DSPy
35
35
  @logger ||= create_logger
36
36
  end
37
37
 
38
- def self.log(event, **attributes)
38
+ def self.log(event_name, **attributes)
39
39
  # Return nil early if logger is not configured (backward compatibility)
40
40
  return nil unless logger
41
-
42
- # Forward to event system - this maintains backward compatibility
43
- # while providing all new event system benefits
44
- event(event, attributes)
45
-
41
+
42
+ # Direct logging - simple and straightforward
43
+ emit_log(event_name, attributes)
44
+
46
45
  # Return nil to maintain backward compatibility
47
46
  nil
48
47
  end
@@ -53,7 +52,7 @@ module DSPy
53
52
  event_obj = event_name_or_object
54
53
  event_name = event_obj.name
55
54
  attributes = event_obj.to_attributes
56
-
55
+
57
56
  # For LLM events, use OpenTelemetry semantic conventions for spans
58
57
  if event_obj.is_a?(DSPy::Events::LLMEvent)
59
58
  otel_attributes = event_obj.to_otel_attributes
@@ -65,44 +64,50 @@ module DSPy
65
64
  # Handle string event names (backward compatibility)
66
65
  event_name = event_name_or_object
67
66
  raise ArgumentError, "Event name cannot be nil" if event_name.nil?
68
-
67
+
69
68
  # Handle nil attributes
70
69
  attributes = {} if attributes.nil?
71
-
70
+
72
71
  # Create OpenTelemetry span for the event if observability is enabled
73
72
  create_event_span(event_name, attributes)
74
73
  end
75
-
74
+
76
75
  # Perform the actual logging (original DSPy.log behavior)
77
- emit_log(event_name, attributes)
78
-
76
+ # emit_log(event_name, attributes)
77
+
79
78
  # Notify event listeners
80
79
  events.notify(event_name, attributes)
81
80
  end
82
81
 
83
82
  def self.events
84
- @event_registry ||= DSPy::EventRegistry.new
83
+ @event_registry ||= DSPy::EventRegistry.new.tap do |registry|
84
+ # Subscribe logger to all events - use a proc that calls logger each time
85
+ # to support mocking in tests
86
+ registry.subscribe('*') { |event_name, attributes|
87
+ emit_log(event_name, attributes) if logger
88
+ }
89
+ end
85
90
  end
86
91
 
87
92
  private
88
93
 
89
94
  def self.emit_log(event_name, attributes)
90
95
  return unless logger
91
-
96
+
92
97
  # Merge context automatically (but don't include span_stack)
93
98
  context = Context.current.dup
94
99
  context.delete(:span_stack)
95
100
  attributes = context.merge(attributes)
96
101
  attributes[:event] = event_name
97
-
102
+
98
103
  # Use Dry::Logger's structured logging
99
104
  logger.info(attributes)
100
105
  end
101
106
 
102
107
  # Internal events that should not create OpenTelemetry spans
103
108
  INTERNAL_EVENTS = [
104
- 'span.start',
105
- 'span.end',
109
+ 'span.start',
110
+ 'span.end',
106
111
  'span.attributes',
107
112
  'observability.disabled',
108
113
  'observability.error',
@@ -114,11 +119,11 @@ module DSPy
114
119
  def self.create_event_span(event_name, attributes)
115
120
  return unless DSPy::Observability.enabled?
116
121
  return if INTERNAL_EVENTS.include?(event_name)
117
-
122
+
118
123
  begin
119
124
  # Flatten nested hashes for OpenTelemetry span attributes
120
125
  flattened_attributes = flatten_attributes(attributes)
121
-
126
+
122
127
  # Create and immediately finish a span for this event
123
128
  # Events are instant moments in time, not ongoing operations
124
129
  span = DSPy::Observability.start_span(event_name, flattened_attributes)
@@ -137,21 +142,21 @@ module DSPy
137
142
  def self.flatten_attributes(attributes, parent_key = '', result = {})
138
143
  attributes.each do |key, value|
139
144
  new_key = parent_key.empty? ? key.to_s : "#{parent_key}.#{key}"
140
-
145
+
141
146
  if value.is_a?(Hash)
142
147
  flatten_attributes(value, new_key, result)
143
148
  else
144
149
  result[new_key] = value
145
150
  end
146
151
  end
147
-
152
+
148
153
  result
149
154
  end
150
155
 
151
156
  def self.create_logger
152
157
  env = ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
153
158
  log_output = ENV['DSPY_LOG'] # Allow override
154
-
159
+
155
160
  case env
156
161
  when 'test'
157
162
  # Test: key=value format to log/test.log (or override)
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dspy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.24.1
4
+ version: 0.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-09-05 00:00:00.000000000 Z
10
+ date: 2025-09-07 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dry-configurable
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '2.23'
46
+ version: '2.29'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '2.23'
53
+ version: '2.29'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: openai
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -238,6 +238,7 @@ files:
238
238
  - lib/dspy/mixins/type_coercion.rb
239
239
  - lib/dspy/module.rb
240
240
  - lib/dspy/observability.rb
241
+ - lib/dspy/observability/async_span_processor.rb
241
242
  - lib/dspy/predict.rb
242
243
  - lib/dspy/prediction.rb
243
244
  - lib/dspy/prompt.rb
@@ -262,6 +263,7 @@ files:
262
263
  - lib/dspy/tools/text_processing_toolset.rb
263
264
  - lib/dspy/tools/toolset.rb
264
265
  - lib/dspy/type_serializer.rb
266
+ - lib/dspy/utils/serialization.rb
265
267
  - lib/dspy/version.rb
266
268
  homepage: https://github.com/vicentereig/dspy.rb
267
269
  licenses: