langfuse-ruby 0.1.3 → 0.1.5

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.
@@ -1,5 +1,6 @@
1
1
  require 'faraday'
2
2
  require 'faraday/net_http'
3
+ require 'faraday/multipart'
3
4
  require 'json'
4
5
  require 'base64'
5
6
  require 'concurrent'
@@ -54,7 +55,7 @@ module Langfuse
54
55
  # Span operations
55
56
  def span(trace_id:, name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
56
57
  metadata: nil, level: nil, status_message: nil, parent_observation_id: nil,
57
- version: nil, **kwargs)
58
+ version: nil, as_type: nil, **kwargs)
58
59
  Span.new(
59
60
  client: self,
60
61
  trace_id: trace_id,
@@ -68,6 +69,159 @@ module Langfuse
68
69
  status_message: status_message,
69
70
  parent_observation_id: parent_observation_id,
70
71
  version: version,
72
+ as_type: as_type,
73
+ **kwargs
74
+ )
75
+ end
76
+
77
+ # Convenience methods for enhanced observation types
78
+
79
+ # Create an agent observation (wrapper around span with as_type: 'agent')
80
+ def agent(trace_id:, name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
81
+ metadata: nil, level: nil, status_message: nil, parent_observation_id: nil,
82
+ version: nil, **kwargs)
83
+ span(
84
+ trace_id: trace_id,
85
+ name: name,
86
+ start_time: start_time,
87
+ end_time: end_time,
88
+ input: input,
89
+ output: output,
90
+ metadata: metadata,
91
+ level: level,
92
+ status_message: status_message,
93
+ parent_observation_id: parent_observation_id,
94
+ version: version,
95
+ as_type: ObservationType::AGENT,
96
+ **kwargs
97
+ )
98
+ end
99
+
100
+ # Create a tool observation (wrapper around span with as_type: 'tool')
101
+ def tool(trace_id:, name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
102
+ metadata: nil, level: nil, status_message: nil, parent_observation_id: nil,
103
+ version: nil, **kwargs)
104
+ span(
105
+ trace_id: trace_id,
106
+ name: name,
107
+ start_time: start_time,
108
+ end_time: end_time,
109
+ input: input,
110
+ output: output,
111
+ metadata: metadata,
112
+ level: level,
113
+ status_message: status_message,
114
+ parent_observation_id: parent_observation_id,
115
+ version: version,
116
+ as_type: ObservationType::TOOL,
117
+ **kwargs
118
+ )
119
+ end
120
+
121
+ # Create a chain observation (wrapper around span with as_type: 'chain')
122
+ def chain(trace_id:, name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
123
+ metadata: nil, level: nil, status_message: nil, parent_observation_id: nil,
124
+ version: nil, **kwargs)
125
+ span(
126
+ trace_id: trace_id,
127
+ name: name,
128
+ start_time: start_time,
129
+ end_time: end_time,
130
+ input: input,
131
+ output: output,
132
+ metadata: metadata,
133
+ level: level,
134
+ status_message: status_message,
135
+ parent_observation_id: parent_observation_id,
136
+ version: version,
137
+ as_type: ObservationType::CHAIN,
138
+ **kwargs
139
+ )
140
+ end
141
+
142
+ # Create a retriever observation (wrapper around span with as_type: 'retriever')
143
+ def retriever(trace_id:, name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
144
+ metadata: nil, level: nil, status_message: nil, parent_observation_id: nil,
145
+ version: nil, **kwargs)
146
+ span(
147
+ trace_id: trace_id,
148
+ name: name,
149
+ start_time: start_time,
150
+ end_time: end_time,
151
+ input: input,
152
+ output: output,
153
+ metadata: metadata,
154
+ level: level,
155
+ status_message: status_message,
156
+ parent_observation_id: parent_observation_id,
157
+ version: version,
158
+ as_type: ObservationType::RETRIEVER,
159
+ **kwargs
160
+ )
161
+ end
162
+
163
+ # Create an embedding observation (wrapper around span with as_type: 'embedding')
164
+ def embedding(trace_id:, name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
165
+ model: nil, usage: nil, metadata: nil, level: nil, status_message: nil,
166
+ parent_observation_id: nil, version: nil, **kwargs)
167
+ merged_metadata = (metadata || {}).merge(
168
+ { model: model, usage: usage }.compact
169
+ )
170
+ span(
171
+ trace_id: trace_id,
172
+ name: name,
173
+ start_time: start_time,
174
+ end_time: end_time,
175
+ input: input,
176
+ output: output,
177
+ metadata: merged_metadata.empty? ? nil : merged_metadata,
178
+ level: level,
179
+ status_message: status_message,
180
+ parent_observation_id: parent_observation_id,
181
+ version: version,
182
+ as_type: ObservationType::EMBEDDING,
183
+ **kwargs
184
+ )
185
+ end
186
+
187
+ # Create an evaluator observation (wrapper around span with as_type: 'evaluator')
188
+ def evaluator_obs(trace_id:, name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
189
+ metadata: nil, level: nil, status_message: nil, parent_observation_id: nil,
190
+ version: nil, **kwargs)
191
+ span(
192
+ trace_id: trace_id,
193
+ name: name,
194
+ start_time: start_time,
195
+ end_time: end_time,
196
+ input: input,
197
+ output: output,
198
+ metadata: metadata,
199
+ level: level,
200
+ status_message: status_message,
201
+ parent_observation_id: parent_observation_id,
202
+ version: version,
203
+ as_type: ObservationType::EVALUATOR,
204
+ **kwargs
205
+ )
206
+ end
207
+
208
+ # Create a guardrail observation (wrapper around span with as_type: 'guardrail')
209
+ def guardrail(trace_id:, name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
210
+ metadata: nil, level: nil, status_message: nil, parent_observation_id: nil,
211
+ version: nil, **kwargs)
212
+ span(
213
+ trace_id: trace_id,
214
+ name: name,
215
+ start_time: start_time,
216
+ end_time: end_time,
217
+ input: input,
218
+ output: output,
219
+ metadata: metadata,
220
+ level: level,
221
+ status_message: status_message,
222
+ parent_observation_id: parent_observation_id,
223
+ version: version,
224
+ as_type: ObservationType::GUARDRAIL,
71
225
  **kwargs
72
226
  )
73
227
  end
@@ -125,7 +279,8 @@ module Langfuse
125
279
  return cached_prompt[:prompt]
126
280
  end
127
281
 
128
- path = "/api/public/v2/prompts/#{name}"
282
+ encoded_name = Utils.url_encode(name)
283
+ path = "/api/public/v2/prompts/#{encoded_name}"
129
284
  params = {}
130
285
  params[:version] = version if version
131
286
  params[:label] = label if label
@@ -188,7 +343,7 @@ module Langfuse
188
343
  def enqueue_event(type, body)
189
344
  # 验证事件类型是否有效
190
345
  valid_types = %w[
191
- trace-create
346
+ trace-create trace-update
192
347
  generation-create generation-update
193
348
  span-create span-update
194
349
  event-create
@@ -207,7 +362,32 @@ module Langfuse
207
362
  body: Utils.deep_stringify_keys(body)
208
363
  }
209
364
 
210
- @event_queue << event
365
+ if type == 'trace-update'
366
+ # 查找对应的 trace-create 事件并更新
367
+ trace_id = body['id'] || body[:id]
368
+ if trace_id
369
+ existing_event_index = @event_queue.find_index do |existing_event|
370
+ existing_event[:type] == 'trace-create' &&
371
+ (existing_event[:body]['id'] == trace_id || existing_event[:body][:id] == trace_id)
372
+ end
373
+
374
+ if existing_event_index
375
+ # 更新现有的 trace-create 事件
376
+ @event_queue[existing_event_index][:body].merge!(event[:body])
377
+ @event_queue[existing_event_index][:timestamp] = event[:timestamp]
378
+ puts "Updated existing trace-create event for trace_id: #{trace_id}" if @debug
379
+ else
380
+ # 如果没找到对应的 trace-create 事件,将 trace-update 转换为 trace-create
381
+ event[:type] = 'trace-create'
382
+ @event_queue << event
383
+ puts "Converted trace-update to trace-create for trace_id: #{trace_id}" if @debug
384
+ end
385
+ elsif @debug
386
+ puts 'Warning: trace-update event missing trace_id, skipping'
387
+ end
388
+ else
389
+ @event_queue << event
390
+ end
211
391
  puts "Enqueued event: #{type}" if @debug
212
392
  end
213
393
 
@@ -1,11 +1,11 @@
1
1
  module Langfuse
2
2
  class Event
3
3
  attr_reader :id, :trace_id, :name, :start_time, :input, :output, :metadata,
4
- :level, :status_message, :parent_observation_id, :version, :client
4
+ :level, :status_message, :parent_observation_id, :version, :as_type, :client
5
5
 
6
6
  def initialize(client:, trace_id:, name:, id: nil, start_time: nil, input: nil,
7
7
  output: nil, metadata: nil, level: nil, status_message: nil,
8
- parent_observation_id: nil, version: nil, **kwargs)
8
+ parent_observation_id: nil, version: nil, as_type: nil, **kwargs)
9
9
  @client = client
10
10
  @id = id || Utils.generate_id
11
11
  @trace_id = trace_id
@@ -18,6 +18,7 @@ module Langfuse
18
18
  @status_message = status_message
19
19
  @parent_observation_id = parent_observation_id
20
20
  @version = version
21
+ @as_type = validate_as_type(as_type)
21
22
  @kwargs = kwargs
22
23
 
23
24
  # Create the event
@@ -25,7 +26,7 @@ module Langfuse
25
26
  end
26
27
 
27
28
  def to_dict
28
- {
29
+ data = {
29
30
  id: @id,
30
31
  trace_id: @trace_id,
31
32
  name: @name,
@@ -37,11 +38,24 @@ module Langfuse
37
38
  status_message: @status_message,
38
39
  parent_observation_id: @parent_observation_id,
39
40
  version: @version
40
- }.merge(@kwargs).compact
41
+ }
42
+ data[:type] = @as_type if @as_type
43
+ data.merge(@kwargs).compact
41
44
  end
42
45
 
43
46
  private
44
47
 
48
+ def validate_as_type(type)
49
+ return nil if type.nil?
50
+
51
+ type_str = type.to_s
52
+ unless ObservationType.valid?(type_str)
53
+ raise ValidationError, "Invalid observation type: #{type}. Valid types are: #{ObservationType::ALL.join(', ')}"
54
+ end
55
+
56
+ type_str
57
+ end
58
+
45
59
  def create_event
46
60
  data = {
47
61
  id: @id,
@@ -55,7 +69,9 @@ module Langfuse
55
69
  status_message: @status_message,
56
70
  parent_observation_id: @parent_observation_id,
57
71
  version: @version
58
- }.merge(@kwargs).compact
72
+ }
73
+ data[:type] = @as_type if @as_type
74
+ data = data.merge(@kwargs).compact
59
75
 
60
76
  @client.enqueue_event('event-create', data)
61
77
  end
@@ -2,12 +2,12 @@ module Langfuse
2
2
  class Generation
3
3
  attr_reader :id, :trace_id, :name, :start_time, :end_time, :completion_start_time,
4
4
  :model, :model_parameters, :input, :output, :usage, :metadata, :level,
5
- :status_message, :parent_observation_id, :version, :client
5
+ :status_message, :parent_observation_id, :version, :as_type, :client
6
6
 
7
7
  def initialize(client:, trace_id:, id: nil, name: nil, start_time: nil, end_time: nil,
8
8
  completion_start_time: nil, model: nil, model_parameters: nil, input: nil,
9
9
  output: nil, usage: nil, metadata: nil, level: nil, status_message: nil,
10
- parent_observation_id: nil, version: nil, **kwargs)
10
+ parent_observation_id: nil, version: nil, as_type: nil, **kwargs)
11
11
  @client = client
12
12
  @id = id || Utils.generate_id
13
13
  @trace_id = trace_id
@@ -25,6 +25,7 @@ module Langfuse
25
25
  @status_message = status_message
26
26
  @parent_observation_id = parent_observation_id
27
27
  @version = version
28
+ @as_type = validate_as_type(as_type)
28
29
  @kwargs = kwargs
29
30
 
30
31
  # Create the generation
@@ -62,8 +63,9 @@ module Langfuse
62
63
  self
63
64
  end
64
65
 
66
+ # Create a child span
65
67
  def span(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
66
- metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
68
+ metadata: nil, level: nil, status_message: nil, version: nil, as_type: nil, **kwargs)
67
69
  @client.span(
68
70
  trace_id: @trace_id,
69
71
  name: name,
@@ -76,10 +78,12 @@ module Langfuse
76
78
  status_message: status_message,
77
79
  parent_observation_id: @id,
78
80
  version: version,
81
+ as_type: as_type,
79
82
  **kwargs
80
83
  )
81
84
  end
82
85
 
86
+ # Create a child generation
83
87
  def generation(name: nil, start_time: nil, end_time: nil, completion_start_time: nil,
84
88
  model: nil, model_parameters: nil, input: nil, output: nil, usage: nil,
85
89
  metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
@@ -103,6 +107,7 @@ module Langfuse
103
107
  )
104
108
  end
105
109
 
110
+ # Create a child event
106
111
  def event(name:, start_time: nil, input: nil, output: nil, metadata: nil,
107
112
  level: nil, status_message: nil, version: nil, **kwargs)
108
113
  @client.event(
@@ -120,6 +125,138 @@ module Langfuse
120
125
  )
121
126
  end
122
127
 
128
+ # Convenience methods for enhanced observation types
129
+
130
+ # Create a child agent observation
131
+ def agent(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
132
+ metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
133
+ span(
134
+ name: name,
135
+ start_time: start_time,
136
+ end_time: end_time,
137
+ input: input,
138
+ output: output,
139
+ metadata: metadata,
140
+ level: level,
141
+ status_message: status_message,
142
+ version: version,
143
+ as_type: ObservationType::AGENT,
144
+ **kwargs
145
+ )
146
+ end
147
+
148
+ # Create a child tool observation
149
+ def tool(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
150
+ metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
151
+ span(
152
+ name: name,
153
+ start_time: start_time,
154
+ end_time: end_time,
155
+ input: input,
156
+ output: output,
157
+ metadata: metadata,
158
+ level: level,
159
+ status_message: status_message,
160
+ version: version,
161
+ as_type: ObservationType::TOOL,
162
+ **kwargs
163
+ )
164
+ end
165
+
166
+ # Create a child chain observation
167
+ def chain(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
168
+ metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
169
+ span(
170
+ name: name,
171
+ start_time: start_time,
172
+ end_time: end_time,
173
+ input: input,
174
+ output: output,
175
+ metadata: metadata,
176
+ level: level,
177
+ status_message: status_message,
178
+ version: version,
179
+ as_type: ObservationType::CHAIN,
180
+ **kwargs
181
+ )
182
+ end
183
+
184
+ # Create a child retriever observation
185
+ def retriever(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
186
+ metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
187
+ span(
188
+ name: name,
189
+ start_time: start_time,
190
+ end_time: end_time,
191
+ input: input,
192
+ output: output,
193
+ metadata: metadata,
194
+ level: level,
195
+ status_message: status_message,
196
+ version: version,
197
+ as_type: ObservationType::RETRIEVER,
198
+ **kwargs
199
+ )
200
+ end
201
+
202
+ # Create a child embedding observation
203
+ def embedding(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
204
+ model: nil, usage: nil, metadata: nil, level: nil, status_message: nil,
205
+ version: nil, **kwargs)
206
+ merged_metadata = (metadata || {}).merge(
207
+ { model: model, usage: usage }.compact
208
+ )
209
+ span(
210
+ name: name,
211
+ start_time: start_time,
212
+ end_time: end_time,
213
+ input: input,
214
+ output: output,
215
+ metadata: merged_metadata.empty? ? nil : merged_metadata,
216
+ level: level,
217
+ status_message: status_message,
218
+ version: version,
219
+ as_type: ObservationType::EMBEDDING,
220
+ **kwargs
221
+ )
222
+ end
223
+
224
+ # Create a child evaluator observation
225
+ def evaluator(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
226
+ metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
227
+ span(
228
+ name: name,
229
+ start_time: start_time,
230
+ end_time: end_time,
231
+ input: input,
232
+ output: output,
233
+ metadata: metadata,
234
+ level: level,
235
+ status_message: status_message,
236
+ version: version,
237
+ as_type: ObservationType::EVALUATOR,
238
+ **kwargs
239
+ )
240
+ end
241
+
242
+ # Create a child guardrail observation
243
+ def guardrail(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
244
+ metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
245
+ span(
246
+ name: name,
247
+ start_time: start_time,
248
+ end_time: end_time,
249
+ input: input,
250
+ output: output,
251
+ metadata: metadata,
252
+ level: level,
253
+ status_message: status_message,
254
+ version: version,
255
+ as_type: ObservationType::GUARDRAIL,
256
+ **kwargs
257
+ )
258
+ end
259
+
123
260
  def score(name:, value:, data_type: nil, comment: nil, **kwargs)
124
261
  @client.score(
125
262
  observation_id: @id,
@@ -136,7 +273,7 @@ module Langfuse
136
273
  end
137
274
 
138
275
  def to_dict
139
- {
276
+ data = {
140
277
  id: @id,
141
278
  trace_id: @trace_id,
142
279
  name: @name,
@@ -153,11 +290,24 @@ module Langfuse
153
290
  status_message: @status_message,
154
291
  parent_observation_id: @parent_observation_id,
155
292
  version: @version
156
- }.merge(@kwargs).compact
293
+ }
294
+ data[:type] = @as_type if @as_type
295
+ data.merge(@kwargs).compact
157
296
  end
158
297
 
159
298
  private
160
299
 
300
+ def validate_as_type(type)
301
+ return nil if type.nil?
302
+
303
+ type_str = type.to_s
304
+ unless ObservationType.valid?(type_str)
305
+ raise ValidationError, "Invalid observation type: #{type}. Valid types are: #{ObservationType::ALL.join(', ')}"
306
+ end
307
+
308
+ type_str
309
+ end
310
+
161
311
  def create_generation
162
312
  @client.enqueue_event('generation-create', to_dict)
163
313
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langfuse
4
+ # Valid observation types for Langfuse
5
+ # These types provide semantic meaning to observations in traces
6
+ module ObservationType
7
+ # Core observation types (existing)
8
+ SPAN = 'span'
9
+ GENERATION = 'generation'
10
+ EVENT = 'event'
11
+
12
+ # Enhanced observation types (new in 2025)
13
+ AGENT = 'agent' # Agent workflows and reasoning
14
+ TOOL = 'tool' # Tool/function calls
15
+ CHAIN = 'chain' # Chain operations (e.g., retrieval chains)
16
+ RETRIEVER = 'retriever' # Data retrieval (vector stores, databases)
17
+ EMBEDDING = 'embedding' # Embedding generation
18
+ EVALUATOR = 'evaluator' # Evaluation/scoring functions
19
+ GUARDRAIL = 'guardrail' # Safety filters and content moderation
20
+
21
+ # All valid observation types
22
+ ALL = [
23
+ SPAN,
24
+ GENERATION,
25
+ EVENT,
26
+ AGENT,
27
+ TOOL,
28
+ CHAIN,
29
+ RETRIEVER,
30
+ EMBEDDING,
31
+ EVALUATOR,
32
+ GUARDRAIL
33
+ ].freeze
34
+
35
+ # Types that are aliases for span (use span-create/span-update events)
36
+ SPAN_BASED = [
37
+ SPAN,
38
+ AGENT,
39
+ TOOL,
40
+ CHAIN,
41
+ RETRIEVER,
42
+ EMBEDDING,
43
+ EVALUATOR,
44
+ GUARDRAIL
45
+ ].freeze
46
+
47
+ # Validate if a type is valid
48
+ def self.valid?(type)
49
+ return true if type.nil? # nil is valid (defaults to base type)
50
+
51
+ ALL.include?(type.to_s)
52
+ end
53
+
54
+ # Check if type uses span events
55
+ def self.span_based?(type)
56
+ return true if type.nil?
57
+
58
+ SPAN_BASED.include?(type.to_s)
59
+ end
60
+ end
61
+ end