dspy 0.16.0 → 0.18.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/README.md +3 -3
- data/lib/dspy/chain_of_thought.rb +13 -16
- data/lib/dspy/code_act.rb +31 -37
- data/lib/dspy/context.rb +67 -0
- data/lib/dspy/evaluate.rb +20 -17
- data/lib/dspy/lm.rb +72 -37
- data/lib/dspy/memory/memory_compactor.rb +5 -6
- data/lib/dspy/memory/memory_manager.rb +5 -4
- data/lib/dspy/observability.rb +109 -0
- data/lib/dspy/predict.rb +18 -6
- data/lib/dspy/propose/grounded_proposer.rb +13 -12
- data/lib/dspy/re_act.rb +34 -41
- data/lib/dspy/registry/registry_manager.rb +8 -10
- data/lib/dspy/registry/signature_registry.rb +40 -52
- data/lib/dspy/storage/program_storage.rb +28 -37
- data/lib/dspy/storage/storage_manager.rb +3 -4
- data/lib/dspy/teleprompt/teleprompter.rb +11 -12
- data/lib/dspy/teleprompt/utils.rb +24 -22
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +42 -82
- metadata +33 -26
- data/lib/dspy/instrumentation/event_payload_factory.rb +0 -282
- data/lib/dspy/instrumentation/event_payloads.rb +0 -476
- data/lib/dspy/instrumentation/token_tracker.rb +0 -70
- data/lib/dspy/instrumentation.rb +0 -341
- data/lib/dspy/mixins/instrumentation_helpers.rb +0 -120
- data/lib/dspy/subscribers/langfuse_subscriber.rb +0 -669
- data/lib/dspy/subscribers/logger_subscriber.rb +0 -480
- data/lib/dspy/subscribers/newrelic_subscriber.rb +0 -686
- data/lib/dspy/subscribers/otel_subscriber.rb +0 -537
data/lib/dspy/instrumentation.rb
DELETED
@@ -1,341 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'dry-monitor'
|
4
|
-
require 'dry-configurable'
|
5
|
-
require 'time'
|
6
|
-
require_relative 'instrumentation/event_payload_factory'
|
7
|
-
|
8
|
-
module DSPy
|
9
|
-
# Core instrumentation module using dry-monitor for event emission
|
10
|
-
# Provides extension points for logging, OpenTelemetry, New Relic, Langfuse, and custom monitoring
|
11
|
-
module Instrumentation
|
12
|
-
# Get a logger subscriber instance (creates new instance each time)
|
13
|
-
def self.logger_subscriber(**options)
|
14
|
-
require_relative 'subscribers/logger_subscriber'
|
15
|
-
DSPy::Subscribers::LoggerSubscriber.new(**options)
|
16
|
-
end
|
17
|
-
|
18
|
-
# Get an OpenTelemetry subscriber instance (creates new instance each time)
|
19
|
-
def self.otel_subscriber(**options)
|
20
|
-
require_relative 'subscribers/otel_subscriber'
|
21
|
-
DSPy::Subscribers::OtelSubscriber.new(**options)
|
22
|
-
end
|
23
|
-
|
24
|
-
# Get a New Relic subscriber instance (creates new instance each time)
|
25
|
-
def self.newrelic_subscriber(**options)
|
26
|
-
require_relative 'subscribers/newrelic_subscriber'
|
27
|
-
DSPy::Subscribers::NewrelicSubscriber.new(**options)
|
28
|
-
end
|
29
|
-
|
30
|
-
# Get a Langfuse subscriber instance (creates new instance each time)
|
31
|
-
def self.langfuse_subscriber(**options)
|
32
|
-
require_relative 'subscribers/langfuse_subscriber'
|
33
|
-
DSPy::Subscribers::LangfuseSubscriber.new(**options)
|
34
|
-
end
|
35
|
-
|
36
|
-
def self.notifications
|
37
|
-
@notifications ||= Dry::Monitor::Notifications.new(:dspy).tap do |n|
|
38
|
-
# Register all DSPy events
|
39
|
-
n.register_event('dspy.lm.request')
|
40
|
-
n.register_event('dspy.lm.tokens')
|
41
|
-
n.register_event('dspy.lm.response.parsed')
|
42
|
-
n.register_event('dspy.predict')
|
43
|
-
n.register_event('dspy.predict.validation_error')
|
44
|
-
n.register_event('dspy.chain_of_thought')
|
45
|
-
n.register_event('dspy.chain_of_thought.reasoning_step')
|
46
|
-
n.register_event('dspy.react')
|
47
|
-
n.register_event('dspy.react.tool_call')
|
48
|
-
n.register_event('dspy.react.iteration_complete')
|
49
|
-
n.register_event('dspy.react.max_iterations')
|
50
|
-
|
51
|
-
# CodeAct events
|
52
|
-
n.register_event('dspy.codeact')
|
53
|
-
n.register_event('dspy.codeact.iteration')
|
54
|
-
n.register_event('dspy.codeact.code_execution')
|
55
|
-
n.register_event('dspy.codeact.iteration_complete')
|
56
|
-
n.register_event('dspy.codeact.max_iterations')
|
57
|
-
|
58
|
-
# Evaluation events
|
59
|
-
n.register_event('dspy.evaluation.start')
|
60
|
-
n.register_event('dspy.evaluation.example')
|
61
|
-
n.register_event('dspy.evaluation.batch')
|
62
|
-
n.register_event('dspy.evaluation.batch_complete')
|
63
|
-
|
64
|
-
# Optimization events
|
65
|
-
n.register_event('dspy.optimization.start')
|
66
|
-
n.register_event('dspy.optimization.complete')
|
67
|
-
n.register_event('dspy.optimization.trial_start')
|
68
|
-
n.register_event('dspy.optimization.trial_complete')
|
69
|
-
n.register_event('dspy.optimization.bootstrap_start')
|
70
|
-
n.register_event('dspy.optimization.bootstrap_complete')
|
71
|
-
n.register_event('dspy.optimization.bootstrap_example')
|
72
|
-
n.register_event('dspy.optimization.minibatch_evaluation')
|
73
|
-
n.register_event('dspy.optimization.instruction_proposal_start')
|
74
|
-
n.register_event('dspy.optimization.instruction_proposal_complete')
|
75
|
-
n.register_event('dspy.optimization.error')
|
76
|
-
n.register_event('dspy.optimization.save')
|
77
|
-
n.register_event('dspy.optimization.load')
|
78
|
-
|
79
|
-
# Storage events
|
80
|
-
n.register_event('dspy.storage.save_start')
|
81
|
-
n.register_event('dspy.storage.save_complete')
|
82
|
-
n.register_event('dspy.storage.save_error')
|
83
|
-
n.register_event('dspy.storage.load_start')
|
84
|
-
n.register_event('dspy.storage.load_complete')
|
85
|
-
n.register_event('dspy.storage.load_error')
|
86
|
-
n.register_event('dspy.storage.delete')
|
87
|
-
n.register_event('dspy.storage.export')
|
88
|
-
n.register_event('dspy.storage.import')
|
89
|
-
n.register_event('dspy.storage.cleanup')
|
90
|
-
|
91
|
-
# Memory compaction events
|
92
|
-
n.register_event('dspy.memory.compaction_check')
|
93
|
-
n.register_event('dspy.memory.size_compaction')
|
94
|
-
n.register_event('dspy.memory.age_compaction')
|
95
|
-
n.register_event('dspy.memory.deduplication')
|
96
|
-
n.register_event('dspy.memory.relevance_pruning')
|
97
|
-
n.register_event('dspy.memory.compaction_complete')
|
98
|
-
|
99
|
-
# Registry events
|
100
|
-
n.register_event('dspy.registry.register_start')
|
101
|
-
n.register_event('dspy.registry.register_complete')
|
102
|
-
n.register_event('dspy.registry.register_error')
|
103
|
-
n.register_event('dspy.registry.deploy_start')
|
104
|
-
n.register_event('dspy.registry.deploy_complete')
|
105
|
-
n.register_event('dspy.registry.deploy_error')
|
106
|
-
n.register_event('dspy.registry.rollback_start')
|
107
|
-
n.register_event('dspy.registry.rollback_complete')
|
108
|
-
n.register_event('dspy.registry.rollback_error')
|
109
|
-
n.register_event('dspy.registry.performance_update')
|
110
|
-
n.register_event('dspy.registry.export')
|
111
|
-
n.register_event('dspy.registry.import')
|
112
|
-
n.register_event('dspy.registry.auto_deployment')
|
113
|
-
n.register_event('dspy.registry.automatic_rollback')
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
# High-precision timing for performance tracking
|
118
|
-
def self.instrument(event_name, payload = {}, &block)
|
119
|
-
# If no block is given, return early
|
120
|
-
return unless block_given?
|
121
|
-
|
122
|
-
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
123
|
-
start_cpu = Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID)
|
124
|
-
|
125
|
-
begin
|
126
|
-
result = yield
|
127
|
-
|
128
|
-
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
129
|
-
end_cpu = Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID)
|
130
|
-
|
131
|
-
enhanced_payload = payload.merge(
|
132
|
-
duration_ms: ((end_time - start_time) * 1000).round(2),
|
133
|
-
cpu_time_ms: ((end_cpu - start_cpu) * 1000).round(2),
|
134
|
-
status: 'success'
|
135
|
-
).merge(generate_timestamp)
|
136
|
-
|
137
|
-
# Create typed event struct
|
138
|
-
event_struct = EventPayloadFactory.create_event(event_name, enhanced_payload)
|
139
|
-
self.emit_event(event_name, event_struct)
|
140
|
-
result
|
141
|
-
rescue => error
|
142
|
-
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
143
|
-
end_cpu = Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID)
|
144
|
-
|
145
|
-
error_payload = payload.merge(
|
146
|
-
duration_ms: ((end_time - start_time) * 1000).round(2),
|
147
|
-
cpu_time_ms: ((end_cpu - start_cpu) * 1000).round(2),
|
148
|
-
status: 'error',
|
149
|
-
error_type: error.class.name,
|
150
|
-
error_message: error.message
|
151
|
-
).merge(generate_timestamp)
|
152
|
-
|
153
|
-
# Create typed event struct
|
154
|
-
event_struct = EventPayloadFactory.create_event(event_name, error_payload)
|
155
|
-
self.emit_event(event_name, event_struct)
|
156
|
-
raise
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
# Emit event without timing (for discrete events)
|
161
|
-
def self.emit(event_name, payload = {})
|
162
|
-
# Handle nil payload
|
163
|
-
payload ||= {}
|
164
|
-
|
165
|
-
enhanced_payload = payload.merge(
|
166
|
-
status: payload[:status] || 'success'
|
167
|
-
).merge(generate_timestamp)
|
168
|
-
|
169
|
-
# Create typed event struct
|
170
|
-
event_struct = EventPayloadFactory.create_event(event_name, enhanced_payload)
|
171
|
-
self.emit_event(event_name, event_struct)
|
172
|
-
end
|
173
|
-
|
174
|
-
# Register additional events dynamically (useful for testing)
|
175
|
-
def self.register_event(event_name)
|
176
|
-
notifications.register_event(event_name)
|
177
|
-
end
|
178
|
-
|
179
|
-
# Subscribe to DSPy instrumentation events
|
180
|
-
def self.subscribe(event_pattern = nil, &block)
|
181
|
-
if event_pattern
|
182
|
-
notifications.subscribe(event_pattern, &block)
|
183
|
-
else
|
184
|
-
# Subscribe to all DSPy events
|
185
|
-
%w[dspy.lm.request dspy.lm.tokens dspy.lm.response.parsed dspy.predict dspy.predict.validation_error dspy.chain_of_thought dspy.chain_of_thought.reasoning_step dspy.react dspy.react.tool_call dspy.react.iteration_complete dspy.react.max_iterations].each do |event_name|
|
186
|
-
notifications.subscribe(event_name, &block)
|
187
|
-
end
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
def self.emit_event(event_name, payload)
|
192
|
-
# Only emit events - subscribers self-register when explicitly created
|
193
|
-
# Convert struct to hash if needed (dry-monitor expects hash)
|
194
|
-
if payload.respond_to?(:to_h)
|
195
|
-
payload_hash = payload.to_h
|
196
|
-
# Restore original timestamp format if needed
|
197
|
-
restore_timestamp_format(payload_hash)
|
198
|
-
else
|
199
|
-
payload_hash = payload
|
200
|
-
end
|
201
|
-
notifications.instrument(event_name, payload_hash)
|
202
|
-
end
|
203
|
-
|
204
|
-
# Restore timestamp to original format based on configuration
|
205
|
-
def self.restore_timestamp_format(payload_hash)
|
206
|
-
return unless payload_hash[:timestamp]
|
207
|
-
|
208
|
-
case DSPy.config.instrumentation.timestamp_format
|
209
|
-
when DSPy::TimestampFormat::UNIX_NANO
|
210
|
-
# Convert ISO8601 back to nanoseconds
|
211
|
-
timestamp = Time.parse(payload_hash[:timestamp])
|
212
|
-
payload_hash.delete(:timestamp)
|
213
|
-
payload_hash[:timestamp_ns] = (timestamp.to_f * 1_000_000_000).to_i
|
214
|
-
when DSPy::TimestampFormat::RFC3339_NANO
|
215
|
-
# Convert to RFC3339 with nanoseconds
|
216
|
-
timestamp = Time.parse(payload_hash[:timestamp])
|
217
|
-
payload_hash[:timestamp] = timestamp.strftime('%Y-%m-%dT%H:%M:%S.%9N%z')
|
218
|
-
end
|
219
|
-
end
|
220
|
-
|
221
|
-
def self.setup_subscribers
|
222
|
-
config = DSPy.config.instrumentation
|
223
|
-
|
224
|
-
# Return early if instrumentation is disabled
|
225
|
-
return unless config.enabled
|
226
|
-
|
227
|
-
# Validate configuration first
|
228
|
-
DSPy.validate_instrumentation!
|
229
|
-
|
230
|
-
# Setup each configured subscriber
|
231
|
-
config.subscribers.each do |subscriber_type|
|
232
|
-
setup_subscriber(subscriber_type)
|
233
|
-
end
|
234
|
-
end
|
235
|
-
|
236
|
-
def self.setup_subscriber(subscriber_type)
|
237
|
-
case subscriber_type
|
238
|
-
when :logger
|
239
|
-
setup_logger_subscriber
|
240
|
-
when :otel
|
241
|
-
setup_otel_subscriber if otel_available?
|
242
|
-
when :newrelic
|
243
|
-
setup_newrelic_subscriber if newrelic_available?
|
244
|
-
when :langfuse
|
245
|
-
setup_langfuse_subscriber if langfuse_available?
|
246
|
-
else
|
247
|
-
raise ArgumentError, "Unknown subscriber type: #{subscriber_type}"
|
248
|
-
end
|
249
|
-
rescue LoadError => e
|
250
|
-
DSPy.logger.warn "Failed to setup #{subscriber_type} subscriber: #{e.message}"
|
251
|
-
end
|
252
|
-
|
253
|
-
def self.setup_logger_subscriber
|
254
|
-
# Create subscriber - it will read configuration when handling events
|
255
|
-
logger_subscriber
|
256
|
-
end
|
257
|
-
|
258
|
-
def self.setup_otel_subscriber
|
259
|
-
# Create subscriber - it will read configuration when handling events
|
260
|
-
otel_subscriber
|
261
|
-
end
|
262
|
-
|
263
|
-
def self.setup_newrelic_subscriber
|
264
|
-
# Create subscriber - it will read configuration when handling events
|
265
|
-
newrelic_subscriber
|
266
|
-
end
|
267
|
-
|
268
|
-
def self.setup_langfuse_subscriber
|
269
|
-
# Create subscriber - it will read configuration when handling events
|
270
|
-
langfuse_subscriber
|
271
|
-
end
|
272
|
-
|
273
|
-
# Dependency checking methods
|
274
|
-
def self.otel_available?
|
275
|
-
begin
|
276
|
-
require 'opentelemetry/sdk'
|
277
|
-
true
|
278
|
-
rescue LoadError
|
279
|
-
false
|
280
|
-
end
|
281
|
-
end
|
282
|
-
|
283
|
-
def self.newrelic_available?
|
284
|
-
begin
|
285
|
-
require 'newrelic_rpm'
|
286
|
-
true
|
287
|
-
rescue LoadError
|
288
|
-
false
|
289
|
-
end
|
290
|
-
end
|
291
|
-
|
292
|
-
def self.langfuse_available?
|
293
|
-
begin
|
294
|
-
require 'langfuse'
|
295
|
-
true
|
296
|
-
rescue LoadError
|
297
|
-
false
|
298
|
-
end
|
299
|
-
end
|
300
|
-
|
301
|
-
# Generate timestamp in the configured format
|
302
|
-
def self.generate_timestamp
|
303
|
-
case DSPy.config.instrumentation.timestamp_format
|
304
|
-
when DSPy::TimestampFormat::ISO8601
|
305
|
-
{ timestamp: Time.now.iso8601 }
|
306
|
-
when DSPy::TimestampFormat::RFC3339_NANO
|
307
|
-
{ timestamp: Time.now.strftime('%Y-%m-%dT%H:%M:%S.%9N%z') }
|
308
|
-
when DSPy::TimestampFormat::UNIX_NANO
|
309
|
-
{ timestamp_ns: (Time.now.to_f * 1_000_000_000).to_i }
|
310
|
-
else
|
311
|
-
{ timestamp: Time.now.iso8601 } # Fallback to iso8601
|
312
|
-
end
|
313
|
-
end
|
314
|
-
|
315
|
-
# Legacy setup method for backward compatibility
|
316
|
-
def self.setup_subscribers_legacy
|
317
|
-
# Legacy initialization - will be created when first accessed
|
318
|
-
# Force initialization of enabled subscribers
|
319
|
-
logger_subscriber
|
320
|
-
|
321
|
-
# Only initialize if dependencies are available
|
322
|
-
begin
|
323
|
-
otel_subscriber if ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] || defined?(OpenTelemetry)
|
324
|
-
rescue LoadError
|
325
|
-
# OpenTelemetry not available, skip
|
326
|
-
end
|
327
|
-
|
328
|
-
begin
|
329
|
-
newrelic_subscriber if defined?(NewRelic)
|
330
|
-
rescue LoadError
|
331
|
-
# New Relic not available, skip
|
332
|
-
end
|
333
|
-
|
334
|
-
begin
|
335
|
-
langfuse_subscriber if ENV['LANGFUSE_SECRET_KEY'] || defined?(Langfuse)
|
336
|
-
rescue LoadError
|
337
|
-
# Langfuse not available, skip
|
338
|
-
end
|
339
|
-
end
|
340
|
-
end
|
341
|
-
end
|
@@ -1,120 +0,0 @@
|
|
1
|
-
# typed: strict
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
require 'sorbet-runtime'
|
5
|
-
require_relative '../instrumentation'
|
6
|
-
|
7
|
-
module DSPy
|
8
|
-
module Mixins
|
9
|
-
# Shared instrumentation helper methods for DSPy modules
|
10
|
-
module InstrumentationHelpers
|
11
|
-
extend T::Sig
|
12
|
-
|
13
|
-
private
|
14
|
-
|
15
|
-
# Prepares base instrumentation payload for prediction-based modules
|
16
|
-
sig { params(signature_class: T.class_of(DSPy::Signature), input_values: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
17
|
-
def prepare_base_instrumentation_payload(signature_class, input_values)
|
18
|
-
# Validate LM is configured before accessing its properties
|
19
|
-
raise DSPy::ConfigurationError.missing_lm(self.class.name) if lm.nil?
|
20
|
-
|
21
|
-
{
|
22
|
-
signature_class: signature_class.name,
|
23
|
-
model: lm.model,
|
24
|
-
provider: lm.provider,
|
25
|
-
input_fields: input_values.keys.map(&:to_s)
|
26
|
-
}
|
27
|
-
end
|
28
|
-
|
29
|
-
# Instruments a prediction operation with base payload
|
30
|
-
sig { params(event_name: String, signature_class: T.class_of(DSPy::Signature), input_values: T::Hash[Symbol, T.untyped], additional_payload: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
|
31
|
-
def instrument_prediction(event_name, signature_class, input_values, additional_payload = {})
|
32
|
-
base_payload = prepare_base_instrumentation_payload(signature_class, input_values)
|
33
|
-
full_payload = base_payload.merge(additional_payload)
|
34
|
-
|
35
|
-
# Use smart consolidation: skip nested events when higher-level events are being emitted
|
36
|
-
if should_emit_event?(event_name)
|
37
|
-
Instrumentation.instrument(event_name, full_payload) do
|
38
|
-
yield
|
39
|
-
end
|
40
|
-
else
|
41
|
-
# Skip instrumentation, just execute the block
|
42
|
-
yield
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
# Emits a validation error event
|
47
|
-
sig { params(signature_class: T.class_of(DSPy::Signature), validation_type: String, error_message: String).void }
|
48
|
-
def emit_validation_error(signature_class, validation_type, error_message)
|
49
|
-
Instrumentation.emit('dspy.prediction.validation_error', {
|
50
|
-
signature_class: signature_class.name,
|
51
|
-
validation_type: validation_type,
|
52
|
-
validation_errors: { validation_type.to_sym => error_message }
|
53
|
-
})
|
54
|
-
end
|
55
|
-
|
56
|
-
# Emits a prediction completion event
|
57
|
-
sig { params(signature_class: T.class_of(DSPy::Signature), success: T::Boolean, additional_data: T::Hash[Symbol, T.untyped]).void }
|
58
|
-
def emit_prediction_complete(signature_class, success, additional_data = {})
|
59
|
-
Instrumentation.emit('dspy.prediction.complete', {
|
60
|
-
signature_class: signature_class.name,
|
61
|
-
success: success
|
62
|
-
}.merge(additional_data))
|
63
|
-
end
|
64
|
-
|
65
|
-
# Determines if an event should be emitted using smart consolidation
|
66
|
-
sig { params(event_name: String).returns(T::Boolean) }
|
67
|
-
def should_emit_event?(event_name)
|
68
|
-
# Smart consolidation: skip nested events when higher-level events are being emitted
|
69
|
-
if is_nested_context?
|
70
|
-
# If we're in a nested context, only emit higher-level events
|
71
|
-
event_name.match?(/^dspy\.(chain_of_thought|react|codeact)$/)
|
72
|
-
else
|
73
|
-
# If we're not in a nested context, emit all events normally
|
74
|
-
true
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
# Determines if this is a top-level event (not nested)
|
79
|
-
sig { params(event_name: String).returns(T::Boolean) }
|
80
|
-
def is_top_level_event?(event_name)
|
81
|
-
# Check if we're in a nested call by looking at the call stack
|
82
|
-
caller_locations = caller_locations(1, 20)
|
83
|
-
return false if caller_locations.nil?
|
84
|
-
|
85
|
-
# Look for other instrumentation calls in the stack
|
86
|
-
instrumentation_calls = caller_locations.select do |loc|
|
87
|
-
loc.label.include?('instrument_prediction') ||
|
88
|
-
loc.label.include?('instrument') ||
|
89
|
-
loc.path.include?('instrumentation')
|
90
|
-
end
|
91
|
-
|
92
|
-
# If we have more than one instrumentation call, this is nested
|
93
|
-
instrumentation_calls.size <= 1
|
94
|
-
end
|
95
|
-
|
96
|
-
# Determines if we're in a nested call context
|
97
|
-
sig { returns(T::Boolean) }
|
98
|
-
def is_nested_call?
|
99
|
-
!is_top_level_event?('')
|
100
|
-
end
|
101
|
-
|
102
|
-
# Determines if we're in a nested context where higher-level events are being emitted
|
103
|
-
sig { returns(T::Boolean) }
|
104
|
-
def is_nested_context?
|
105
|
-
caller_locations = caller_locations(1, 30)
|
106
|
-
return false if caller_locations.nil?
|
107
|
-
|
108
|
-
# Look for higher-level DSPy modules in the call stack
|
109
|
-
# We consider ChainOfThought, ReAct, and CodeAct as higher-level modules
|
110
|
-
higher_level_modules = caller_locations.select do |loc|
|
111
|
-
loc.path.match?(/(?:chain_of_thought|re_act|react|code_act)/)
|
112
|
-
end
|
113
|
-
|
114
|
-
# If we have higher-level modules in the call stack, we're in a nested context
|
115
|
-
higher_level_modules.any?
|
116
|
-
end
|
117
|
-
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|