dspy 0.3.0 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8053438ba1e55a093c35b50b9dc3b106b0c158ce426c6f286ba7f62aeee8161d
4
- data.tar.gz: 456ca182c45f1924caa6eeea6c86debba28a77ba127c6326014b1a468e07c445
3
+ metadata.gz: 64e8b7011ea06273772d2ef8a985d61aa1ee30d5d6fb3c559dc22ed81e345b16
4
+ data.tar.gz: a16fab394ee1db1bcaddc0baaa3636590a2ca00c1ce60eb7dc1a00355750009f
5
5
  SHA512:
6
- metadata.gz: 67c31136acd1ef0a01c49938b3f142abf3690264ac048969e342a750dce80b59b16b258efc4ee6385fb55fb7580e18ac9f87aeb7fb09631cfb0fb139debb4f52
7
- data.tar.gz: 94214ff9fdd61ea1d478abf9d8da37325d884e8238b01e7bd50cdb3bd21fcfcfaf2c0c6b3509241f0bff3e730587e770588c9f262206f1ca1bc809724e553b31
6
+ metadata.gz: ce4ab780cce89c2c3680e6c5703e853bbd901ac206993af1cd0660cc25e9796ef1dd5adb488d575bbc97ea75a71b563e82eb4cc521b5c84291ff9b1e106216e1
7
+ data.tar.gz: be313a08f282eb7a08638879298742baf5ec26bcb48b946ac068892c2ad542003d99df575ecc85bb5220c637368f02adf042164831cf5d48fd7139bb3f5424a7
data/README.md CHANGED
@@ -380,16 +380,6 @@ DSPy.rb includes built-in instrumentation that captures detailed events and
380
380
  performance metrics from your LLM operations. Perfect for monitoring your
381
381
  applications and integrating with observability tools.
382
382
 
383
- ### Quick Setup
384
-
385
- Enable instrumentation to start capturing events:
386
-
387
- ```ruby
388
- DSPy::Instrumentation.configure do |config|
389
- config.enabled = true
390
- end
391
- ```
392
-
393
383
  ### Available Events
394
384
 
395
385
  Subscribe to these events to monitor different aspects of your LLM operations:
@@ -449,26 +439,6 @@ giving you precise cost tracking:
449
439
  }
450
440
  ```
451
441
 
452
- ### Configuration Options
453
-
454
- ```ruby
455
- DSPy::Instrumentation.configure do |config|
456
- config.enabled = true
457
- config.log_to_stdout = false
458
- config.log_file = 'log/dspy.log'
459
- config.log_level = :info
460
-
461
- # Custom payload enrichment
462
- config.custom_options = lambda do |event|
463
- {
464
- timestamp: Time.current.iso8601,
465
- hostname: Socket.gethostname,
466
- request_id: Thread.current[:request_id]
467
- }
468
- end
469
- end
470
- ```
471
-
472
442
  ### Integration with Monitoring Tools
473
443
 
474
444
  Subscribe to events for custom processing:
@@ -7,6 +7,13 @@ module DSPy
7
7
  # Core instrumentation module using dry-monitor for event emission
8
8
  # Provides extension points for logging, Langfuse, New Relic, and custom monitoring
9
9
  module Instrumentation
10
+ # Get the current logger subscriber instance (lazy initialization)
11
+ def self.logger_subscriber
12
+ @logger_subscriber ||= begin
13
+ require_relative 'subscribers/logger_subscriber'
14
+ DSPy::Subscribers::LoggerSubscriber.new
15
+ end
16
+ end
10
17
 
11
18
  def self.notifications
12
19
  @notifications ||= Dry::Monitor::Notifications.new(:dspy).tap do |n|
@@ -94,7 +101,13 @@ module DSPy
94
101
  end
95
102
 
96
103
  def self.emit_event(event_name, payload)
104
+ # Ensure logger subscriber is initialized
105
+ logger_subscriber
97
106
  notifications.instrument(event_name, payload)
98
107
  end
108
+
109
+ def self.setup_subscribers
110
+ # Lazy initialization - will be created when first accessed
111
+ end
99
112
  end
100
113
  end
@@ -7,14 +7,20 @@ module DSPy
7
7
  class LoggerSubscriber
8
8
  extend T::Sig
9
9
 
10
- sig { params(logger: T.nilable(Logger)).void }
10
+ sig { params(logger: T.nilable(T.any(Logger, Dry::Logger::Dispatcher))).void }
11
11
  def initialize(logger: nil)
12
- @logger = T.let(logger || DSPy.config.logger, Logger)
12
+ @explicit_logger = T.let(logger, T.nilable(T.any(Logger, Dry::Logger::Dispatcher)))
13
13
  setup_event_subscriptions
14
14
  end
15
15
 
16
16
  private
17
17
 
18
+ # Always use the current configured logger or the explicit one
19
+ sig { returns(T.any(Logger, Dry::Logger::Dispatcher)) }
20
+ def logger
21
+ @explicit_logger || DSPy.config.logger
22
+ end
23
+
18
24
  sig { void }
19
25
  def setup_event_subscriptions
20
26
  # Subscribe to DSPy instrumentation events
@@ -82,18 +88,19 @@ module DSPy
82
88
  model = payload[:gen_ai_request_model] || payload[:model]
83
89
  duration = payload[:duration_ms]&.round(2)
84
90
  status = payload[:status]
85
- tokens = if payload[:tokens_total]
86
- " (#{payload[:tokens_total]} tokens)"
87
- else
88
- ""
89
- end
90
-
91
- status_emoji = status == 'success' ? '✅' : '❌'
92
- @logger.info("#{status_emoji} LM Request [#{provider}/#{model}] - #{status} (#{duration}ms)#{tokens}")
93
-
94
- if status == 'error' && payload[:error_message]
95
- @logger.error(" Error: #{payload[:error_message]}")
96
- end
91
+ tokens = payload[:tokens_total]
92
+
93
+ log_parts = [
94
+ "event=lm_request",
95
+ "provider=#{provider}",
96
+ "model=#{model}",
97
+ "status=#{status}",
98
+ "duration_ms=#{duration}"
99
+ ]
100
+ log_parts << "tokens=#{tokens}" if tokens
101
+ log_parts << "error=\"#{payload[:error_message]}\"" if status == 'error' && payload[:error_message]
102
+
103
+ logger.info(log_parts.join(' '))
97
104
  end
98
105
 
99
106
  sig { params(event: T.untyped).void }
@@ -104,13 +111,16 @@ module DSPy
104
111
  status = payload[:status]
105
112
  input_size = payload[:input_size]
106
113
 
107
- status_emoji = status == 'success' ? '🔮' : '❌'
108
- @logger.info("#{status_emoji} Prediction [#{signature}] - #{status} (#{duration}ms)")
109
- @logger.info(" Input size: #{input_size} chars") if input_size
110
-
111
- if status == 'error' && payload[:error_message]
112
- @logger.error(" Error: #{payload[:error_message]}")
113
- end
114
+ log_parts = [
115
+ "event=prediction",
116
+ "signature=#{signature}",
117
+ "status=#{status}",
118
+ "duration_ms=#{duration}"
119
+ ]
120
+ log_parts << "input_size=#{input_size}" if input_size
121
+ log_parts << "error=\"#{payload[:error_message]}\"" if status == 'error' && payload[:error_message]
122
+
123
+ logger.info(log_parts.join(' '))
114
124
  end
115
125
 
116
126
  sig { params(event: T.untyped).void }
@@ -122,14 +132,17 @@ module DSPy
122
132
  reasoning_steps = payload[:reasoning_steps]
123
133
  reasoning_length = payload[:reasoning_length]
124
134
 
125
- status_emoji = status == 'success' ? '🧠' : '❌'
126
- @logger.info("#{status_emoji} Chain of Thought [#{signature}] - #{status} (#{duration}ms)")
127
- @logger.info(" Reasoning steps: #{reasoning_steps}") if reasoning_steps
128
- @logger.info(" Reasoning length: #{reasoning_length} chars") if reasoning_length
129
-
130
- if status == 'error' && payload[:error_message]
131
- @logger.error(" Error: #{payload[:error_message]}")
132
- end
135
+ log_parts = [
136
+ "event=chain_of_thought",
137
+ "signature=#{signature}",
138
+ "status=#{status}",
139
+ "duration_ms=#{duration}"
140
+ ]
141
+ log_parts << "reasoning_steps=#{reasoning_steps}" if reasoning_steps
142
+ log_parts << "reasoning_length=#{reasoning_length}" if reasoning_length
143
+ log_parts << "error=\"#{payload[:error_message]}\"" if status == 'error' && payload[:error_message]
144
+
145
+ logger.info(log_parts.join(' '))
133
146
  end
134
147
 
135
148
  sig { params(event: T.untyped).void }
@@ -142,20 +155,18 @@ module DSPy
142
155
  tools_used = payload[:tools_used]
143
156
  final_answer = payload[:final_answer]
144
157
 
145
- status_emoji = case status
146
- when 'success' then '🤖'
147
- when 'max_iterations' then '⏰'
148
- else '❌'
149
- end
150
-
151
- @logger.info("#{status_emoji} ReAct Agent [#{signature}] - #{status} (#{duration}ms)")
152
- @logger.info(" Iterations: #{iteration_count}") if iteration_count
153
- @logger.info(" Tools used: #{tools_used.join(', ')}") if tools_used&.any?
154
- @logger.info(" Final answer: #{final_answer}") if final_answer
155
-
156
- if status == 'error' && payload[:error_message]
157
- @logger.error(" Error: #{payload[:error_message]}")
158
- end
158
+ log_parts = [
159
+ "event=react",
160
+ "signature=#{signature}",
161
+ "status=#{status}",
162
+ "duration_ms=#{duration}"
163
+ ]
164
+ log_parts << "iterations=#{iteration_count}" if iteration_count
165
+ log_parts << "tools_used=\"#{tools_used.join(',')}\"" if tools_used&.any?
166
+ log_parts << "final_answer=\"#{final_answer&.truncate(100)}\"" if final_answer
167
+ log_parts << "error=\"#{payload[:error_message]}\"" if status == 'error' && payload[:error_message]
168
+
169
+ logger.info(log_parts.join(' '))
159
170
  end
160
171
 
161
172
  sig { params(event: T.untyped).void }
@@ -167,14 +178,17 @@ module DSPy
167
178
  duration = payload[:duration_ms]&.round(2)
168
179
  status = payload[:status]
169
180
 
170
- status_emoji = status == 'success' ? '🔄' : '❌'
171
- @logger.info("#{status_emoji} ReAct Iteration #{iteration} - #{status} (#{duration}ms)")
172
- @logger.info(" Thought: #{thought.truncate(100)}") if thought
173
- @logger.info(" Action: #{action}") if action
174
-
175
- if status == 'error' && payload[:error_message]
176
- @logger.error(" Error: #{payload[:error_message]}")
177
- end
181
+ log_parts = [
182
+ "event=react_iteration",
183
+ "iteration=#{iteration}",
184
+ "status=#{status}",
185
+ "duration_ms=#{duration}"
186
+ ]
187
+ log_parts << "thought=\"#{thought&.truncate(100)}\"" if thought
188
+ log_parts << "action=\"#{action}\"" if action
189
+ log_parts << "error=\"#{payload[:error_message]}\"" if status == 'error' && payload[:error_message]
190
+
191
+ logger.info(log_parts.join(' '))
178
192
  end
179
193
 
180
194
  sig { params(event: T.untyped).void }
@@ -185,12 +199,16 @@ module DSPy
185
199
  duration = payload[:duration_ms]&.round(2)
186
200
  status = payload[:status]
187
201
 
188
- status_emoji = status == 'success' ? '🔧' : '❌'
189
- @logger.info("#{status_emoji} Tool Call [#{tool_name}] (Iteration #{iteration}) - #{status} (#{duration}ms)")
190
-
191
- if status == 'error' && payload[:error_message]
192
- @logger.error(" Error: #{payload[:error_message]}")
193
- end
202
+ log_parts = [
203
+ "event=tool_call",
204
+ "tool=#{tool_name}",
205
+ "iteration=#{iteration}",
206
+ "status=#{status}",
207
+ "duration_ms=#{duration}"
208
+ ]
209
+ log_parts << "error=\"#{payload[:error_message]}\"" if status == 'error' && payload[:error_message]
210
+
211
+ logger.info(log_parts.join(' '))
194
212
  end
195
213
  end
196
214
  end
data/lib/dspy.rb CHANGED
@@ -11,23 +11,6 @@ module DSPy
11
11
  def self.logger
12
12
  config.logger
13
13
  end
14
-
15
- # Convenient instrumentation configuration
16
- def self.configure_instrumentation(&block)
17
- require_relative 'dspy/instrumentation'
18
- require_relative 'dspy/instrumentation/dry_monitor_bridge'
19
-
20
- Instrumentation.configure(&block)
21
-
22
- # Setup dry-monitor bridge for HTTP instrumentation
23
- if Instrumentation.config.enabled
24
- Instrumentation::DryMonitorBridge.setup!
25
-
26
- # Auto-enable logger subscriber for event-based logging
27
- require_relative 'dspy/subscribers/logger_subscriber'
28
- @logger_subscriber ||= Subscribers::LoggerSubscriber.new
29
- end
30
- end
31
14
  end
32
15
 
33
16
  require_relative 'dspy/module'
@@ -39,3 +22,6 @@ require_relative 'dspy/chain_of_thought'
39
22
  require_relative 'dspy/re_act'
40
23
  require_relative 'dspy/subscribers/logger_subscriber'
41
24
  require_relative 'dspy/tools'
25
+ require_relative 'dspy/instrumentation'
26
+
27
+ # LoggerSubscriber will be lazy-initialized when first accessed
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dspy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincón de Arellano