langfuse-ruby 0.1.2 → 0.1.4

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.
@@ -6,22 +6,29 @@ require 'concurrent'
6
6
 
7
7
  module Langfuse
8
8
  class Client
9
- attr_reader :public_key, :secret_key, :host, :debug, :timeout, :retries
9
+ attr_reader :public_key, :secret_key, :host, :debug, :timeout, :retries, :flush_interval, :auto_flush
10
10
 
11
- def initialize(public_key: nil, secret_key: nil, host: nil, debug: false, timeout: 30, retries: 3)
11
+ def initialize(public_key: nil, secret_key: nil, host: nil, debug: false, timeout: 30, retries: 3,
12
+ flush_interval: nil, auto_flush: nil)
12
13
  @public_key = public_key || ENV['LANGFUSE_PUBLIC_KEY'] || Langfuse.configuration.public_key
13
14
  @secret_key = secret_key || ENV['LANGFUSE_SECRET_KEY'] || Langfuse.configuration.secret_key
14
15
  @host = host || ENV['LANGFUSE_HOST'] || Langfuse.configuration.host
15
16
  @debug = debug || Langfuse.configuration.debug
16
17
  @timeout = timeout || Langfuse.configuration.timeout
17
18
  @retries = retries || Langfuse.configuration.retries
19
+ @flush_interval = flush_interval || ENV['LANGFUSE_FLUSH_INTERVAL']&.to_i || Langfuse.configuration.flush_interval
20
+ @auto_flush = if auto_flush.nil?
21
+ ENV['LANGFUSE_AUTO_FLUSH'] == 'false' ? false : Langfuse.configuration.auto_flush
22
+ else
23
+ auto_flush
24
+ end
18
25
 
19
26
  raise AuthenticationError, 'Public key is required' unless @public_key
20
27
  raise AuthenticationError, 'Secret key is required' unless @secret_key
21
28
 
22
29
  @connection = build_connection
23
30
  @event_queue = Concurrent::Array.new
24
- @flush_thread = start_flush_thread
31
+ @flush_thread = start_flush_thread if @auto_flush
25
32
  end
26
33
 
27
34
  # Trace operations
@@ -91,6 +98,25 @@ module Langfuse
91
98
  )
92
99
  end
93
100
 
101
+ # Event operations
102
+ def event(trace_id:, name:, start_time: nil, input: nil, output: nil, metadata: nil,
103
+ level: nil, status_message: nil, parent_observation_id: nil, version: nil, **kwargs)
104
+ Event.new(
105
+ client: self,
106
+ trace_id: trace_id,
107
+ name: name,
108
+ start_time: start_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
+ **kwargs
117
+ )
118
+ end
119
+
94
120
  # Prompt operations
95
121
  def get_prompt(name, version: nil, label: nil, cache_ttl_seconds: 60)
96
122
  cache_key = "prompt:#{name}:#{version}:#{label}"
@@ -158,29 +184,22 @@ module Langfuse
158
184
  enqueue_event('score-create', data)
159
185
  end
160
186
 
161
- # HTTP methods
162
- def get(path, params = {})
163
- request(:get, path, params: params)
164
- end
165
-
166
- def post(path, data = {})
167
- request(:post, path, json: data)
168
- end
169
-
170
- def put(path, data = {})
171
- request(:put, path, json: data)
172
- end
173
-
174
- def delete(path, params = {})
175
- request(:delete, path, params: params)
176
- end
177
-
178
- def patch(path, data = {})
179
- request(:patch, path, json: data)
180
- end
181
-
182
187
  # Event queue management
183
188
  def enqueue_event(type, body)
189
+ # 验证事件类型是否有效
190
+ valid_types = %w[
191
+ trace-create trace-update
192
+ generation-create generation-update
193
+ span-create span-update
194
+ event-create
195
+ score-create
196
+ ]
197
+
198
+ unless valid_types.include?(type)
199
+ puts "Warning: Invalid event type '#{type}'. Skipping event." if @debug
200
+ return
201
+ end
202
+
184
203
  event = {
185
204
  id: Utils.generate_id,
186
205
  type: type,
@@ -188,7 +207,32 @@ module Langfuse
188
207
  body: Utils.deep_stringify_keys(body)
189
208
  }
190
209
 
191
- @event_queue << event
210
+ if type == 'trace-update'
211
+ # 查找对应的 trace-create 事件并更新
212
+ trace_id = body['id'] || body[:id]
213
+ if trace_id
214
+ existing_event_index = @event_queue.find_index do |existing_event|
215
+ existing_event[:type] == 'trace-create' &&
216
+ (existing_event[:body]['id'] == trace_id || existing_event[:body][:id] == trace_id)
217
+ end
218
+
219
+ if existing_event_index
220
+ # 更新现有的 trace-create 事件
221
+ @event_queue[existing_event_index][:body].merge!(event[:body])
222
+ @event_queue[existing_event_index][:timestamp] = event[:timestamp]
223
+ puts "Updated existing trace-create event for trace_id: #{trace_id}" if @debug
224
+ else
225
+ # 如果没找到对应的 trace-create 事件,将 trace-update 转换为 trace-create
226
+ event[:type] = 'trace-create'
227
+ @event_queue << event
228
+ puts "Converted trace-update to trace-create for trace_id: #{trace_id}" if @debug
229
+ end
230
+ elsif @debug
231
+ puts 'Warning: trace-update event missing trace_id, skipping'
232
+ end
233
+ else
234
+ @event_queue << event
235
+ end
192
236
  puts "Enqueued event: #{type}" if @debug
193
237
  end
194
238
 
@@ -198,32 +242,99 @@ module Langfuse
198
242
  events = @event_queue.shift(@event_queue.length)
199
243
  return if events.empty?
200
244
 
201
- batch_data = {
202
- batch: events,
203
- metadata: {
204
- batch_size: events.length,
205
- sdk_name: 'langfuse-ruby',
206
- sdk_version: Langfuse::VERSION
207
- }
208
- }
245
+ send_batch(events)
246
+ end
247
+
248
+ def shutdown
249
+ @flush_thread&.kill if @auto_flush
250
+ flush unless @event_queue.empty?
251
+ end
252
+
253
+ private
254
+
255
+ def debug_event_data(events)
256
+ return unless @debug
257
+
258
+ puts "\n=== Event Data Debug Information ==="
259
+ events.each_with_index do |event, index|
260
+ puts "Event #{index + 1}:"
261
+ puts " ID: #{event[:id]}"
262
+ puts " Type: #{event[:type]}"
263
+ puts " Timestamp: #{event[:timestamp]}"
264
+ puts " Body keys: #{event[:body]&.keys || 'nil'}"
265
+
266
+ # 检查常见的问题
267
+ puts ' ⚠️ WARNING: Empty or nil type!' if event[:type].nil? || event[:type].to_s.empty?
268
+
269
+ puts ' ⚠️ WARNING: Empty body!' if event[:body].nil?
270
+
271
+ puts ' ---'
272
+ end
273
+ puts "=== End Debug Information ===\n"
274
+ end
275
+
276
+ def send_batch(events)
277
+ # 调试事件数据
278
+ debug_event_data(events)
279
+
280
+ # 验证事件数据
281
+ valid_events = events.select do |event|
282
+ if event[:type].nil? || event[:type].to_s.empty?
283
+ puts "Warning: Event with empty type detected, skipping: #{event[:id]}" if @debug
284
+ false
285
+ elsif event[:body].nil?
286
+ puts "Warning: Event with empty body detected, skipping: #{event[:id]}" if @debug
287
+ false
288
+ else
289
+ true
290
+ end
291
+ end
292
+
293
+ if valid_events.empty?
294
+ puts 'No valid events to send' if @debug
295
+ return
296
+ end
297
+
298
+ batch_data = build_batch_data(valid_events)
299
+ puts "Sending batch data: #{batch_data}" if @debug
209
300
 
210
301
  begin
211
302
  response = post('/api/public/ingestion', batch_data)
212
- puts "Flushed #{events.length} events" if @debug
303
+ puts "Flushed #{valid_events.length} events" if @debug
304
+ response
213
305
  rescue StandardError => e
214
306
  puts "Failed to flush events: #{e.message}" if @debug
215
307
  # Re-queue events on failure
216
- events.each { |event| @event_queue << event }
308
+ valid_events.each { |event| @event_queue << event }
217
309
  raise
218
310
  end
219
311
  end
220
312
 
221
- def shutdown
222
- @flush_thread&.kill
223
- flush unless @event_queue.empty?
313
+ def build_batch_data(events)
314
+ {
315
+ batch: events,
316
+ metadata: Utils.deep_camelize_keys({
317
+ batch_size: events.length,
318
+ sdk_name: 'langfuse-ruby',
319
+ sdk_version: Langfuse::VERSION
320
+ })
321
+ }
224
322
  end
225
323
 
226
- private
324
+ def start_flush_thread
325
+ return unless @auto_flush
326
+
327
+ Thread.new do
328
+ loop do
329
+ sleep(@flush_interval) # Configurable flush interval
330
+ begin
331
+ flush unless @event_queue.empty?
332
+ rescue StandardError => e
333
+ puts "Error in flush thread: #{e.message}" if @debug
334
+ end
335
+ end
336
+ end
337
+ end
227
338
 
228
339
  def build_connection
229
340
  Faraday.new(url: @host) do |conn|
@@ -248,6 +359,27 @@ module Langfuse
248
359
  end
249
360
  end
250
361
 
362
+ # HTTP methods
363
+ def get(path, params = {})
364
+ request(:get, path, params: params)
365
+ end
366
+
367
+ def post(path, data = {})
368
+ request(:post, path, json: data)
369
+ end
370
+
371
+ def put(path, data = {})
372
+ request(:put, path, json: data)
373
+ end
374
+
375
+ def delete(path, params = {})
376
+ request(:delete, path, params: params)
377
+ end
378
+
379
+ def patch(path, data = {})
380
+ request(:patch, path, json: data)
381
+ end
382
+
251
383
  def request(method, path, params: {}, json: nil)
252
384
  retries_left = @retries
253
385
 
@@ -293,25 +425,27 @@ module Langfuse
293
425
  when 429
294
426
  raise RateLimitError, "Rate limit exceeded: #{response.body}"
295
427
  when 400..499
296
- raise ValidationError, "Client error (#{response.status}): #{response.body}"
428
+ # 400 错误提供更详细的错误信息
429
+ error_details = ''
430
+ if response.body.is_a?(Hash) && response.body['error']
431
+ error_details = "\nError details: #{response.body['error']}"
432
+ elsif response.body.is_a?(String)
433
+ error_details = "\nError details: #{response.body}"
434
+ end
435
+
436
+ # 特别处理类型验证错误
437
+ unless response.body.to_s.include?('invalid_union') || response.body.to_s.include?('discriminator')
438
+ raise ValidationError, "Client error (#{response.status}): #{response.body}#{error_details}"
439
+ end
440
+
441
+ raise ValidationError,
442
+ "Event type validation failed (#{response.status}): The event type or structure is invalid. Please check the event format.#{error_details}"
443
+
297
444
  when 500..599
298
445
  raise APIError, "Server error (#{response.status}): #{response.body}"
299
446
  else
300
447
  raise APIError, "Unexpected response (#{response.status}): #{response.body}"
301
448
  end
302
449
  end
303
-
304
- def start_flush_thread
305
- Thread.new do
306
- loop do
307
- sleep(5) # Flush every 5 seconds
308
- begin
309
- flush unless @event_queue.empty?
310
- rescue StandardError => e
311
- puts "Error in flush thread: #{e.message}" if @debug
312
- end
313
- end
314
- end
315
- end
316
450
  end
317
451
  end
@@ -0,0 +1,63 @@
1
+ module Langfuse
2
+ class Event
3
+ attr_reader :id, :trace_id, :name, :start_time, :input, :output, :metadata,
4
+ :level, :status_message, :parent_observation_id, :version, :client
5
+
6
+ def initialize(client:, trace_id:, name:, id: nil, start_time: nil, input: nil,
7
+ output: nil, metadata: nil, level: nil, status_message: nil,
8
+ parent_observation_id: nil, version: nil, **kwargs)
9
+ @client = client
10
+ @id = id || Utils.generate_id
11
+ @trace_id = trace_id
12
+ @name = name
13
+ @start_time = start_time || Utils.current_timestamp
14
+ @input = input
15
+ @output = output
16
+ @metadata = metadata || {}
17
+ @level = level
18
+ @status_message = status_message
19
+ @parent_observation_id = parent_observation_id
20
+ @version = version
21
+ @kwargs = kwargs
22
+
23
+ # Create the event
24
+ create_event
25
+ end
26
+
27
+ def to_dict
28
+ {
29
+ id: @id,
30
+ trace_id: @trace_id,
31
+ name: @name,
32
+ start_time: @start_time,
33
+ input: @input,
34
+ output: @output,
35
+ metadata: @metadata,
36
+ level: @level,
37
+ status_message: @status_message,
38
+ parent_observation_id: @parent_observation_id,
39
+ version: @version
40
+ }.merge(@kwargs).compact
41
+ end
42
+
43
+ private
44
+
45
+ def create_event
46
+ data = {
47
+ id: @id,
48
+ trace_id: @trace_id,
49
+ name: @name,
50
+ start_time: @start_time,
51
+ input: @input,
52
+ output: @output,
53
+ metadata: @metadata,
54
+ level: @level,
55
+ status_message: @status_message,
56
+ parent_observation_id: @parent_observation_id,
57
+ version: @version
58
+ }.merge(@kwargs).compact
59
+
60
+ @client.enqueue_event('event-create', data)
61
+ end
62
+ end
63
+ end
@@ -103,6 +103,23 @@ module Langfuse
103
103
  )
104
104
  end
105
105
 
106
+ def event(name:, start_time: nil, input: nil, output: nil, metadata: nil,
107
+ level: nil, status_message: nil, version: nil, **kwargs)
108
+ @client.event(
109
+ trace_id: @trace_id,
110
+ name: name,
111
+ start_time: start_time,
112
+ input: input,
113
+ output: output,
114
+ metadata: metadata,
115
+ level: level,
116
+ status_message: status_message,
117
+ parent_observation_id: @id,
118
+ version: version,
119
+ **kwargs
120
+ )
121
+ end
122
+
106
123
  def score(name:, value:, data_type: nil, comment: nil, **kwargs)
107
124
  @client.score(
108
125
  observation_id: @id,
@@ -142,49 +159,11 @@ module Langfuse
142
159
  private
143
160
 
144
161
  def create_generation
145
- data = {
146
- id: @id,
147
- trace_id: @trace_id,
148
- name: @name,
149
- start_time: @start_time,
150
- end_time: @end_time,
151
- completion_start_time: @completion_start_time,
152
- model: @model,
153
- model_parameters: @model_parameters,
154
- input: @input,
155
- output: @output,
156
- usage: @usage,
157
- metadata: @metadata,
158
- level: @level,
159
- status_message: @status_message,
160
- parent_observation_id: @parent_observation_id,
161
- version: @version
162
- }.merge(@kwargs).compact
163
-
164
- @client.enqueue_event('generation-create', data)
162
+ @client.enqueue_event('generation-create', to_dict)
165
163
  end
166
164
 
167
165
  def update_generation
168
- data = {
169
- id: @id,
170
- trace_id: @trace_id,
171
- name: @name,
172
- start_time: @start_time,
173
- end_time: @end_time,
174
- completion_start_time: @completion_start_time,
175
- model: @model,
176
- model_parameters: @model_parameters,
177
- input: @input,
178
- output: @output,
179
- usage: @usage,
180
- metadata: @metadata,
181
- level: @level,
182
- status_message: @status_message,
183
- parent_observation_id: @parent_observation_id,
184
- version: @version
185
- }.merge(@kwargs).compact
186
-
187
- @client.enqueue_event('generation-update', data)
166
+ @client.enqueue_event('generation-update', to_dict)
188
167
  end
189
168
  end
190
169
  end
data/lib/langfuse/span.rb CHANGED
@@ -91,6 +91,23 @@ module Langfuse
91
91
  )
92
92
  end
93
93
 
94
+ def event(name:, start_time: nil, input: nil, output: nil, metadata: nil,
95
+ level: nil, status_message: nil, version: nil, **kwargs)
96
+ @client.event(
97
+ trace_id: @trace_id,
98
+ name: name,
99
+ start_time: start_time,
100
+ input: input,
101
+ output: output,
102
+ metadata: metadata,
103
+ level: level,
104
+ status_message: status_message,
105
+ parent_observation_id: @id,
106
+ version: version,
107
+ **kwargs
108
+ )
109
+ end
110
+
94
111
  def score(name:, value:, data_type: nil, comment: nil, **kwargs)
95
112
  @client.score(
96
113
  observation_id: @id,
@@ -24,23 +24,6 @@ module Langfuse
24
24
  create_trace
25
25
  end
26
26
 
27
- def update(name: nil, user_id: nil, session_id: nil, version: nil, release: nil,
28
- input: nil, output: nil, metadata: nil, tags: nil, **kwargs)
29
- @name = name if name
30
- @user_id = user_id if user_id
31
- @session_id = session_id if session_id
32
- @version = version if version
33
- @release = release if release
34
- @input = input if input
35
- @output = output if output
36
- @metadata.merge!(metadata) if metadata
37
- @tags.concat(tags) if tags
38
- @kwargs.merge!(kwargs)
39
-
40
- update_trace
41
- self
42
- end
43
-
44
27
  def span(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
45
28
  metadata: nil, level: nil, status_message: nil, parent_observation_id: nil,
46
29
  version: nil, **kwargs)
@@ -84,6 +67,23 @@ module Langfuse
84
67
  )
85
68
  end
86
69
 
70
+ def event(name:, start_time: nil, input: nil, output: nil, metadata: nil,
71
+ level: nil, status_message: nil, parent_observation_id: nil, version: nil, **kwargs)
72
+ @client.event(
73
+ trace_id: @id,
74
+ name: name,
75
+ start_time: start_time,
76
+ input: input,
77
+ output: output,
78
+ metadata: metadata,
79
+ level: level,
80
+ status_message: status_message,
81
+ parent_observation_id: parent_observation_id,
82
+ version: version,
83
+ **kwargs
84
+ )
85
+ end
86
+
87
87
  def score(name:, value:, data_type: nil, comment: nil, **kwargs)
88
88
  @client.score(
89
89
  trace_id: @id,
@@ -95,6 +95,23 @@ module Langfuse
95
95
  )
96
96
  end
97
97
 
98
+ def update(name: nil, user_id: nil, session_id: nil, version: nil,
99
+ release: nil, input: nil, output: nil, metadata: nil, tags: nil, **kwargs)
100
+ # 更新实例变量
101
+ @name = name if name
102
+ @user_id = user_id if user_id
103
+ @session_id = session_id if session_id
104
+ @version = version if version
105
+ @release = release if release
106
+ @input = input if input
107
+ @output = output if output
108
+ @metadata = metadata if metadata
109
+ @tags = tags if tags
110
+ @kwargs.merge!(kwargs) if kwargs.any?
111
+ # 触发 trace-update 事件
112
+ update_trace
113
+ end
114
+
98
115
  def get_url
99
116
  "#{@client.host}/trace/#{@id}"
100
117
  end
@@ -146,7 +163,8 @@ module Langfuse
146
163
  input: @input,
147
164
  output: @output,
148
165
  metadata: @metadata,
149
- tags: @tags
166
+ tags: @tags,
167
+ timestamp: @timestamp
150
168
  }.merge(@kwargs).compact
151
169
 
152
170
  @client.enqueue_event('trace-update', data)
@@ -26,12 +26,34 @@ module Langfuse
26
26
  return hash unless hash.is_a?(Hash)
27
27
 
28
28
  hash.each_with_object({}) do |(key, value), result|
29
- new_key = key.to_s
29
+ new_key = camelize_key(key.to_s)
30
30
  new_value = value.is_a?(Hash) ? deep_stringify_keys(value) : value
31
31
  result[new_key] = new_value
32
32
  end
33
33
  end
34
34
 
35
+ # 将哈希的键名转换为小驼峰格式
36
+ def deep_camelize_keys(hash)
37
+ return hash unless hash.is_a?(Hash)
38
+
39
+ hash.each_with_object({}) do |(key, value), result|
40
+ new_key = camelize_key(key.to_s)
41
+ new_value = value.is_a?(Hash) ? deep_camelize_keys(value) : value
42
+ result[new_key] = new_value
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ # 将蛇形命名转换为小驼峰命名
49
+ def camelize_key(key)
50
+ return key if key.empty? || !key.include?('_')
51
+
52
+ key.split('_').map.with_index do |part, index|
53
+ index.zero? ? part.downcase : part.capitalize
54
+ end.join
55
+ end
56
+
35
57
  def validate_required_fields(data, required_fields)
36
58
  missing_fields = required_fields.select { |field| data[field].nil? || data[field].to_s.empty? }
37
59
  raise ValidationError, "Missing required fields: #{missing_fields.join(', ')}" unless missing_fields.empty?
@@ -1,3 +1,3 @@
1
1
  module Langfuse
2
- VERSION = '0.1.2'
2
+ VERSION = '0.1.4'
3
3
  end
data/lib/langfuse.rb CHANGED
@@ -5,6 +5,7 @@ require_relative 'langfuse/client'
5
5
  require_relative 'langfuse/trace'
6
6
  require_relative 'langfuse/span'
7
7
  require_relative 'langfuse/generation'
8
+ require_relative 'langfuse/event'
8
9
  require_relative 'langfuse/prompt'
9
10
  require_relative 'langfuse/evaluation'
10
11
  require_relative 'langfuse/errors'
@@ -30,7 +31,7 @@ module Langfuse
30
31
 
31
32
  # Configuration class for Langfuse client settings
32
33
  class Configuration
33
- attr_accessor :public_key, :secret_key, :host, :debug, :timeout, :retries
34
+ attr_accessor :public_key, :secret_key, :host, :debug, :timeout, :retries, :flush_interval, :auto_flush
34
35
 
35
36
  def initialize
36
37
  @public_key = nil
@@ -39,6 +40,8 @@ module Langfuse
39
40
  @debug = false
40
41
  @timeout = 30
41
42
  @retries = 3
43
+ @flush_interval = 5
44
+ @auto_flush = true
42
45
  end
43
46
  end
44
47
  end