Class: DSPy::Context

Inherits:
Object
  • Object
show all
Defined in:
lib/dspy/context.rb

Class Method Summary collapse

Class Method Details

.clear!Object



210
211
212
213
214
215
216
# File 'lib/dspy/context.rb', line 210

def clear!
  # Clear both the thread-specific key and the legacy key
  thread_key = :"dspy_context_#{Thread.current.object_id}"
  Thread.current[thread_key] = nil
  Thread.current[:dspy_context] = nil
  Fiber[:dspy_context] = nil
end

.currentObject



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/dspy/context.rb', line 9

def current
  # Prefer fiber-local context for async safety; fall back to thread root context.
  fiber_context = Fiber[:dspy_context]
  if fiber_context && fiber_context[:thread_id] == Thread.current.object_id
    return fiber_context if fiber_context[:fiber_id] == Fiber.current.object_id

    Fiber[:dspy_context] = fork_context(fiber_context)
    return Fiber[:dspy_context]
  end

  thread_key = :"dspy_context_#{Thread.current.object_id}"
  thread_context = Thread.current[thread_key]

  if thread_context
    Fiber[:dspy_context] = fork_context(thread_context)
    return Fiber[:dspy_context]
  end

  context = build_context
  Thread.current[thread_key] = context
  Thread.current[:dspy_context] = context  # Backward compatibility (thread root)
  Fiber[:dspy_context] = context
  context
end

.fork_context(parent_context) ⇒ Object



46
47
48
# File 'lib/dspy/context.rb', line 46

def fork_context(parent_context)
  clone_context(parent_context)
end

.module_context_attributesObject



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/dspy/context.rb', line 185

def module_context_attributes
  stack = module_stack
  return {} if stack.empty?

  path = stack.map do |entry|
    {
      id: entry[:id],
      class: entry[:class],
      label: entry[:label]
    }
  end

  ancestry_token = path.map { |node| node[:id] }.join('>')

  {
    module_path: path,
    module_root: path.first,
    module_leaf: path.last,
    module_scope: {
      ancestry_token: ancestry_token,
      depth: path.length
    }
  }
end

.module_stackObject



181
182
183
# File 'lib/dspy/context.rb', line 181

def module_stack
  current[:module_stack] ||= []
end

.with_module(module_instance, label: nil) ⇒ Object



168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/dspy/context.rb', line 168

def with_module(module_instance, label: nil)
  stack = module_stack
  entry = build_module_entry(module_instance, label)
  stack.push(entry)
  yield
ensure
  if stack.last.equal?(entry)
    stack.pop
  else
    stack.delete(entry)
  end
end

.with_request(request_id, start_time) ⇒ Object



34
35
36
37
38
39
40
41
42
43
44
# File 'lib/dspy/context.rb', line 34

def with_request(request_id, start_time)
  previous_request_id = current[:request_id]
  previous_start_time = current[:request_start_time]

  current[:request_id] = request_id
  current[:request_start_time] = start_time
  yield
ensure
  current[:request_id] = previous_request_id
  current[:request_start_time] = previous_start_time
end

.with_span(operation:, **attributes) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/dspy/context.rb', line 50

def with_span(operation:, **attributes)
  span_id = SecureRandom.uuid
  parent_span_id = current[:span_stack].last
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  sanitized_attributes = sanitize_span_attributes(attributes)

  # Prepare attributes with context information
  span_attributes = {
    trace_id: current[:trace_id],
    span_id: span_id,
    parent_span_id: parent_span_id,
    operation: operation,
    **sanitized_attributes
  }
  
  # Log span start with proper hierarchy (internal logging only)
  DSPy.log('span.start', **span_attributes) if DSPy::Observability.enabled?
  
  # Push to stack for child spans tracking
  current[:span_stack].push(span_id)
  
  begin
    # Use OpenTelemetry's proper context management for nesting
    if DSPy::Observability.enabled? && DSPy::Observability.tracer
      # Prepare attributes and add trace name for root spans
      span_attributes = sanitized_attributes.transform_keys(&:to_s).reject { |k, v| v.nil? }
      
      # Set trace name if this is likely a root span (no parent in our stack),
      # unless callers already specified one explicitly.
      if current[:span_stack].length == 1 && !span_attributes.key?('langfuse.trace.name')
        span_attributes['langfuse.trace.name'] = operation
      end
      
      # Record start time for explicit duration tracking
      otel_start_time = Time.now
      
      # Get parent OpenTelemetry span for proper context propagation
      parent_otel_span = current[:otel_span_stack].last
      if !parent_otel_span && defined?(OpenTelemetry::Trace)
        current_span = OpenTelemetry::Trace.current_span
        if current_span && current_span != OpenTelemetry::Trace::Span::INVALID
          parent_otel_span = current_span
        end
      end
      
      # Create span with proper parent context
      if parent_otel_span
        # Use the parent span's context to ensure proper nesting
        OpenTelemetry::Trace.with_span(parent_otel_span) do
          DSPy::Observability.tracer.in_span(
            operation,
            attributes: span_attributes,
            kind: :internal
          ) do |span|
            # Add to our OpenTelemetry span stack
            current[:otel_span_stack].push(span)
            succeeded = false
            
            begin
              result = yield(span)
              succeeded = true
              result
            rescue StandardError => e
              set_span_error_attributes(span, e)
              raise
            ensure
              set_span_status_attribute(span, succeeded)
              set_span_timing_attributes(span, otel_start_time)
              # Remove from our OpenTelemetry span stack
              current[:otel_span_stack].pop
            end
          end
        end
      else
        # Root span - no parent context needed
        DSPy::Observability.tracer.in_span(
          operation,
          attributes: span_attributes,
          kind: :internal
        ) do |span|
          # Add to our OpenTelemetry span stack
          current[:otel_span_stack].push(span)
          succeeded = false
          
          begin
            result = yield(span)
            succeeded = true
            result
          rescue StandardError => e
            set_span_error_attributes(span, e)
            raise
          ensure
            set_span_status_attribute(span, succeeded)
            set_span_timing_attributes(span, otel_start_time)
            # Remove from our OpenTelemetry span stack
            current[:otel_span_stack].pop
          end
        end
      end
    else
      yield(nil)
    end
  ensure
    # Pop from stack
    current[:span_stack].pop
    
    # Log span end with duration (internal logging only)
    if DSPy::Observability.enabled?
      duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
      DSPy.log('span.end',
        trace_id: current[:trace_id],
        span_id: span_id,
        duration_ms: duration_ms
      )
    end
  end
end