dspy 0.21.0 → 0.22.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.
- checksums.yaml +4 -4
- data/lib/dspy/events/subscribers.rb +43 -0
- data/lib/dspy/events/types.rb +218 -0
- data/lib/dspy/events.rb +83 -0
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +91 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e7897711c81ee7a4b72dd86a8fbeee9faf4cdf3e398b9878435e0da5750b0fc1
|
4
|
+
data.tar.gz: '079cfdea700a852a94d08415bbdbbe2ff4c4e7d61e4dfae5e739fb88dec29c79'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b81f7fefb5727bbdbc5f94b6618ba7de9cbea26de78324d2ed5f29f6aa1722f5a4e0191d99e3a1393962f5fe224198e25fe3b73fe34b59e99bd486f08bab1189
|
7
|
+
data.tar.gz: 89515f64f64e681ae9cee83ffdad853b7e29409381351528324047b9a60627ed4721b79dd1943cee1ab3300996521aeb595e7a3ce57c90ad09cda47489acca77
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DSPy
|
4
|
+
module Events
|
5
|
+
# Base subscriber class for event-driven patterns
|
6
|
+
# This provides the foundation for creating custom event subscribers
|
7
|
+
#
|
8
|
+
# Example usage:
|
9
|
+
# class MySubscriber < DSPy::Events::BaseSubscriber
|
10
|
+
# def subscribe
|
11
|
+
# add_subscription('llm.*') do |event_name, attributes|
|
12
|
+
# # Handle LLM events
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# subscriber = MySubscriber.new
|
18
|
+
# # subscriber will start receiving events
|
19
|
+
# subscriber.unsubscribe # Clean up when done
|
20
|
+
class BaseSubscriber
|
21
|
+
def initialize
|
22
|
+
@subscriptions = []
|
23
|
+
end
|
24
|
+
|
25
|
+
def subscribe
|
26
|
+
raise NotImplementedError, "Subclasses must implement #subscribe"
|
27
|
+
end
|
28
|
+
|
29
|
+
def unsubscribe
|
30
|
+
@subscriptions.each { |id| DSPy.events.unsubscribe(id) }
|
31
|
+
@subscriptions.clear
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def add_subscription(pattern, &block)
|
37
|
+
subscription_id = DSPy.events.subscribe(pattern, &block)
|
38
|
+
@subscriptions << subscription_id
|
39
|
+
subscription_id
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
|
5
|
+
module DSPy
|
6
|
+
module Events
|
7
|
+
# Base event structure using Sorbet T::Struct
|
8
|
+
class Event < T::Struct
|
9
|
+
const :name, String
|
10
|
+
const :timestamp, Time
|
11
|
+
const :attributes, T::Hash[T.any(String, Symbol), T.untyped], default: {}
|
12
|
+
|
13
|
+
def initialize(name:, timestamp: Time.now, attributes: {})
|
14
|
+
super(name: name, timestamp: timestamp, attributes: attributes)
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_attributes
|
18
|
+
result = attributes.dup
|
19
|
+
result[:timestamp] = timestamp
|
20
|
+
result
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Token usage structure for LLM events
|
25
|
+
class TokenUsage < T::Struct
|
26
|
+
const :prompt_tokens, Integer
|
27
|
+
const :completion_tokens, Integer
|
28
|
+
|
29
|
+
def total_tokens
|
30
|
+
prompt_tokens + completion_tokens
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# LLM operation events with semantic conventions
|
35
|
+
class LLMEvent < T::Struct
|
36
|
+
VALID_PROVIDERS = T.let(
|
37
|
+
['openai', 'anthropic', 'google', 'azure', 'ollama', 'together', 'groq', 'cohere'].freeze,
|
38
|
+
T::Array[String]
|
39
|
+
)
|
40
|
+
|
41
|
+
# Common event fields
|
42
|
+
const :name, String
|
43
|
+
const :timestamp, Time
|
44
|
+
|
45
|
+
# LLM-specific fields
|
46
|
+
const :provider, String
|
47
|
+
const :model, String
|
48
|
+
const :usage, T.nilable(TokenUsage), default: nil
|
49
|
+
const :duration_ms, T.nilable(Numeric), default: nil
|
50
|
+
const :temperature, T.nilable(Float), default: nil
|
51
|
+
const :max_tokens, T.nilable(Integer), default: nil
|
52
|
+
const :stream, T.nilable(T::Boolean), default: nil
|
53
|
+
|
54
|
+
def initialize(name:, provider:, model:, timestamp: Time.now, usage: nil, duration_ms: nil, temperature: nil, max_tokens: nil, stream: nil)
|
55
|
+
unless VALID_PROVIDERS.include?(provider.downcase)
|
56
|
+
raise ArgumentError, "Invalid provider '#{provider}'. Must be one of: #{VALID_PROVIDERS.join(', ')}"
|
57
|
+
end
|
58
|
+
super(
|
59
|
+
name: name,
|
60
|
+
timestamp: timestamp,
|
61
|
+
provider: provider.downcase,
|
62
|
+
model: model,
|
63
|
+
usage: usage,
|
64
|
+
duration_ms: duration_ms,
|
65
|
+
temperature: temperature,
|
66
|
+
max_tokens: max_tokens,
|
67
|
+
stream: stream
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_otel_attributes
|
72
|
+
attrs = {
|
73
|
+
'gen_ai.system' => provider,
|
74
|
+
'gen_ai.request.model' => model
|
75
|
+
}
|
76
|
+
|
77
|
+
if usage
|
78
|
+
attrs['gen_ai.usage.prompt_tokens'] = usage.prompt_tokens
|
79
|
+
attrs['gen_ai.usage.completion_tokens'] = usage.completion_tokens
|
80
|
+
attrs['gen_ai.usage.total_tokens'] = usage.total_tokens
|
81
|
+
end
|
82
|
+
|
83
|
+
attrs['gen_ai.request.temperature'] = temperature if temperature
|
84
|
+
attrs['gen_ai.request.max_tokens'] = max_tokens if max_tokens
|
85
|
+
attrs['gen_ai.request.stream'] = stream if stream
|
86
|
+
attrs['duration_ms'] = duration_ms if duration_ms
|
87
|
+
|
88
|
+
attrs
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_attributes
|
92
|
+
result = to_otel_attributes.dup
|
93
|
+
result[:timestamp] = timestamp
|
94
|
+
result[:provider] = provider
|
95
|
+
result[:model] = model
|
96
|
+
result[:duration_ms] = duration_ms if duration_ms
|
97
|
+
result
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# DSPy module execution events
|
102
|
+
class ModuleEvent < T::Struct
|
103
|
+
# Common event fields
|
104
|
+
const :name, String
|
105
|
+
const :timestamp, Time
|
106
|
+
|
107
|
+
# Module-specific fields
|
108
|
+
const :module_name, String
|
109
|
+
const :signature_name, T.nilable(String), default: nil
|
110
|
+
const :input_fields, T.nilable(T::Array[String]), default: nil
|
111
|
+
const :output_fields, T.nilable(T::Array[String]), default: nil
|
112
|
+
const :duration_ms, T.nilable(Numeric), default: nil
|
113
|
+
const :success, T.nilable(T::Boolean), default: nil
|
114
|
+
|
115
|
+
def initialize(name:, module_name:, timestamp: Time.now, signature_name: nil, input_fields: nil, output_fields: nil, duration_ms: nil, success: nil)
|
116
|
+
super(
|
117
|
+
name: name,
|
118
|
+
timestamp: timestamp,
|
119
|
+
module_name: module_name,
|
120
|
+
signature_name: signature_name,
|
121
|
+
input_fields: input_fields,
|
122
|
+
output_fields: output_fields,
|
123
|
+
duration_ms: duration_ms,
|
124
|
+
success: success
|
125
|
+
)
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_attributes
|
129
|
+
result = { timestamp: timestamp }
|
130
|
+
result[:module_name] = module_name
|
131
|
+
result[:signature_name] = signature_name if signature_name
|
132
|
+
result[:input_fields] = input_fields if input_fields
|
133
|
+
result[:output_fields] = output_fields if output_fields
|
134
|
+
result[:duration_ms] = duration_ms if duration_ms
|
135
|
+
result[:success] = success if success
|
136
|
+
result
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Optimization and training events
|
141
|
+
class OptimizationEvent < T::Struct
|
142
|
+
# Common event fields
|
143
|
+
const :name, String
|
144
|
+
const :timestamp, Time
|
145
|
+
|
146
|
+
# Optimization-specific fields
|
147
|
+
const :optimizer_name, String
|
148
|
+
const :trial_number, T.nilable(Integer), default: nil
|
149
|
+
const :score, T.nilable(Float), default: nil
|
150
|
+
const :best_score, T.nilable(Float), default: nil
|
151
|
+
const :parameters, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
|
152
|
+
const :duration_ms, T.nilable(Numeric), default: nil
|
153
|
+
|
154
|
+
def initialize(name:, optimizer_name:, timestamp: Time.now, trial_number: nil, score: nil, best_score: nil, parameters: nil, duration_ms: nil)
|
155
|
+
super(
|
156
|
+
name: name,
|
157
|
+
timestamp: timestamp,
|
158
|
+
optimizer_name: optimizer_name,
|
159
|
+
trial_number: trial_number,
|
160
|
+
score: score,
|
161
|
+
best_score: best_score,
|
162
|
+
parameters: parameters,
|
163
|
+
duration_ms: duration_ms
|
164
|
+
)
|
165
|
+
end
|
166
|
+
|
167
|
+
def to_attributes
|
168
|
+
result = { timestamp: timestamp }
|
169
|
+
result[:optimizer_name] = optimizer_name
|
170
|
+
result[:trial_number] = trial_number if trial_number
|
171
|
+
result[:score] = score if score
|
172
|
+
result[:best_score] = best_score if best_score
|
173
|
+
result[:parameters] = parameters if parameters
|
174
|
+
result[:duration_ms] = duration_ms if duration_ms
|
175
|
+
result
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Evaluation events
|
180
|
+
class EvaluationEvent < T::Struct
|
181
|
+
# Common event fields
|
182
|
+
const :name, String
|
183
|
+
const :timestamp, Time
|
184
|
+
|
185
|
+
# Evaluation-specific fields
|
186
|
+
const :evaluator_name, String
|
187
|
+
const :metric_name, T.nilable(String), default: nil
|
188
|
+
const :score, T.nilable(Float), default: nil
|
189
|
+
const :total_examples, T.nilable(Integer), default: nil
|
190
|
+
const :passed_examples, T.nilable(Integer), default: nil
|
191
|
+
const :duration_ms, T.nilable(Numeric), default: nil
|
192
|
+
|
193
|
+
def initialize(name:, evaluator_name:, timestamp: Time.now, metric_name: nil, score: nil, total_examples: nil, passed_examples: nil, duration_ms: nil)
|
194
|
+
super(
|
195
|
+
name: name,
|
196
|
+
timestamp: timestamp,
|
197
|
+
evaluator_name: evaluator_name,
|
198
|
+
metric_name: metric_name,
|
199
|
+
score: score,
|
200
|
+
total_examples: total_examples,
|
201
|
+
passed_examples: passed_examples,
|
202
|
+
duration_ms: duration_ms
|
203
|
+
)
|
204
|
+
end
|
205
|
+
|
206
|
+
def to_attributes
|
207
|
+
result = { timestamp: timestamp }
|
208
|
+
result[:evaluator_name] = evaluator_name
|
209
|
+
result[:metric_name] = metric_name if metric_name
|
210
|
+
result[:score] = score if score
|
211
|
+
result[:total_examples] = total_examples if total_examples
|
212
|
+
result[:passed_examples] = passed_examples if passed_examples
|
213
|
+
result[:duration_ms] = duration_ms if duration_ms
|
214
|
+
result
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
data/lib/dspy/events.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module DSPy
|
6
|
+
# Events module to hold typed event structures
|
7
|
+
module Events
|
8
|
+
# Will be defined in events/types.rb
|
9
|
+
end
|
10
|
+
|
11
|
+
class EventRegistry
|
12
|
+
def initialize
|
13
|
+
@listeners = {}
|
14
|
+
@subscription_counter = 0
|
15
|
+
@mutex = Mutex.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def subscribe(pattern, &block)
|
19
|
+
return unless block_given?
|
20
|
+
|
21
|
+
subscription_id = SecureRandom.uuid
|
22
|
+
@mutex.synchronize do
|
23
|
+
@listeners[subscription_id] = {
|
24
|
+
pattern: pattern,
|
25
|
+
block: block
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
subscription_id
|
30
|
+
end
|
31
|
+
|
32
|
+
def unsubscribe(subscription_id)
|
33
|
+
@mutex.synchronize do
|
34
|
+
@listeners.delete(subscription_id)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def clear_listeners
|
39
|
+
@mutex.synchronize do
|
40
|
+
@listeners.clear
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def notify(event_name, attributes)
|
45
|
+
# Take a snapshot of current listeners to avoid holding the mutex during execution
|
46
|
+
# This allows listeners to be modified while others are executing
|
47
|
+
matching_listeners = @mutex.synchronize do
|
48
|
+
@listeners.select do |id, listener|
|
49
|
+
pattern_matches?(listener[:pattern], event_name)
|
50
|
+
end.dup # Create a copy to avoid shared state
|
51
|
+
end
|
52
|
+
|
53
|
+
matching_listeners.each do |id, listener|
|
54
|
+
begin
|
55
|
+
listener[:block].call(event_name, attributes)
|
56
|
+
rescue => e
|
57
|
+
# Log the error but continue processing other listeners
|
58
|
+
# Use emit_log directly to avoid infinite recursion
|
59
|
+
DSPy.send(:emit_log, 'event.listener.error', {
|
60
|
+
subscription_id: id,
|
61
|
+
error_class: e.class.name,
|
62
|
+
error_message: e.message,
|
63
|
+
event_name: event_name
|
64
|
+
})
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def pattern_matches?(pattern, event_name)
|
72
|
+
if pattern.include?('*')
|
73
|
+
# Convert wildcard pattern to regex
|
74
|
+
# llm.* becomes ^llm\..*$
|
75
|
+
regex_pattern = "^#{Regexp.escape(pattern).gsub('\\*', '.*')}$"
|
76
|
+
Regexp.new(regex_pattern).match?(event_name)
|
77
|
+
else
|
78
|
+
# Exact match
|
79
|
+
pattern == event_name
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/dspy/version.rb
CHANGED
data/lib/dspy.rb
CHANGED
@@ -9,6 +9,8 @@ require_relative 'dspy/errors'
|
|
9
9
|
require_relative 'dspy/type_serializer'
|
10
10
|
require_relative 'dspy/observability'
|
11
11
|
require_relative 'dspy/context'
|
12
|
+
require_relative 'dspy/events'
|
13
|
+
require_relative 'dspy/events/types'
|
12
14
|
|
13
15
|
module DSPy
|
14
16
|
extend Dry::Configurable
|
@@ -34,18 +36,105 @@ module DSPy
|
|
34
36
|
end
|
35
37
|
|
36
38
|
def self.log(event, **attributes)
|
39
|
+
# Return nil early if logger is not configured (backward compatibility)
|
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
|
+
|
46
|
+
# Return nil to maintain backward compatibility
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.event(event_name_or_object, attributes = {})
|
51
|
+
# Handle typed event objects
|
52
|
+
if event_name_or_object.respond_to?(:name) && event_name_or_object.respond_to?(:to_attributes)
|
53
|
+
event_obj = event_name_or_object
|
54
|
+
event_name = event_obj.name
|
55
|
+
attributes = event_obj.to_attributes
|
56
|
+
|
57
|
+
# For LLM events, use OpenTelemetry semantic conventions for spans
|
58
|
+
if event_obj.is_a?(DSPy::Events::LLMEvent)
|
59
|
+
otel_attributes = event_obj.to_otel_attributes
|
60
|
+
create_event_span(event_name, otel_attributes)
|
61
|
+
else
|
62
|
+
create_event_span(event_name, attributes)
|
63
|
+
end
|
64
|
+
else
|
65
|
+
# Handle string event names (backward compatibility)
|
66
|
+
event_name = event_name_or_object
|
67
|
+
raise ArgumentError, "Event name cannot be nil" if event_name.nil?
|
68
|
+
|
69
|
+
# Handle nil attributes
|
70
|
+
attributes = {} if attributes.nil?
|
71
|
+
|
72
|
+
# Create OpenTelemetry span for the event if observability is enabled
|
73
|
+
create_event_span(event_name, attributes)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Perform the actual logging (original DSPy.log behavior)
|
77
|
+
emit_log(event_name, attributes)
|
78
|
+
|
79
|
+
# Notify event listeners
|
80
|
+
events.notify(event_name, attributes)
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.events
|
84
|
+
@event_registry ||= DSPy::EventRegistry.new
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def self.emit_log(event_name, attributes)
|
37
90
|
return unless logger
|
38
91
|
|
39
92
|
# Merge context automatically (but don't include span_stack)
|
40
93
|
context = Context.current.dup
|
41
94
|
context.delete(:span_stack)
|
42
95
|
attributes = context.merge(attributes)
|
43
|
-
attributes[:event] =
|
96
|
+
attributes[:event] = event_name
|
44
97
|
|
45
98
|
# Use Dry::Logger's structured logging
|
46
99
|
logger.info(attributes)
|
47
100
|
end
|
48
101
|
|
102
|
+
def self.create_event_span(event_name, attributes)
|
103
|
+
return unless DSPy::Observability.enabled?
|
104
|
+
|
105
|
+
begin
|
106
|
+
# Flatten nested hashes for OpenTelemetry span attributes
|
107
|
+
flattened_attributes = flatten_attributes(attributes)
|
108
|
+
|
109
|
+
# Create and immediately finish a span for this event
|
110
|
+
# Events are instant moments in time, not ongoing operations
|
111
|
+
span = DSPy::Observability.start_span(event_name, flattened_attributes)
|
112
|
+
DSPy::Observability.finish_span(span) if span
|
113
|
+
rescue => e
|
114
|
+
# Log error but don't let it break the event system
|
115
|
+
# Use emit_log directly to avoid infinite recursion
|
116
|
+
emit_log('event.span_creation_error', {
|
117
|
+
error_class: e.class.name,
|
118
|
+
error_message: e.message,
|
119
|
+
event_name: event_name
|
120
|
+
})
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.flatten_attributes(attributes, parent_key = '', result = {})
|
125
|
+
attributes.each do |key, value|
|
126
|
+
new_key = parent_key.empty? ? key.to_s : "#{parent_key}.#{key}"
|
127
|
+
|
128
|
+
if value.is_a?(Hash)
|
129
|
+
flatten_attributes(value, new_key, result)
|
130
|
+
else
|
131
|
+
result[new_key] = value
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
result
|
136
|
+
end
|
137
|
+
|
49
138
|
def self.create_logger
|
50
139
|
env = ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
|
51
140
|
log_output = ENV['DSPY_LOG'] # Allow override
|
@@ -101,6 +190,7 @@ require_relative 'dspy/image'
|
|
101
190
|
require_relative 'dspy/strategy'
|
102
191
|
require_relative 'dspy/prediction'
|
103
192
|
require_relative 'dspy/predict'
|
193
|
+
require_relative 'dspy/events/subscribers'
|
104
194
|
require_relative 'dspy/chain_of_thought'
|
105
195
|
require_relative 'dspy/re_act'
|
106
196
|
require_relative 'dspy/code_act'
|
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.
|
4
|
+
version: 0.22.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-
|
10
|
+
date: 2025-09-03 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: dry-configurable
|
@@ -192,6 +192,9 @@ files:
|
|
192
192
|
- lib/dspy/error_formatter.rb
|
193
193
|
- lib/dspy/errors.rb
|
194
194
|
- lib/dspy/evaluate.rb
|
195
|
+
- lib/dspy/events.rb
|
196
|
+
- lib/dspy/events/subscribers.rb
|
197
|
+
- lib/dspy/events/types.rb
|
195
198
|
- lib/dspy/example.rb
|
196
199
|
- lib/dspy/few_shot_example.rb
|
197
200
|
- lib/dspy/field.rb
|