langfuse-ruby 0.1.2 → 0.1.3

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.
@@ -10,6 +10,8 @@ puts "\n📋 默认配置信息:"
10
10
  puts " 默认主机: #{Langfuse.configuration.host}"
11
11
  puts " 默认超时: #{Langfuse.configuration.timeout}秒"
12
12
  puts " 默认重试: #{Langfuse.configuration.retries}次"
13
+ puts " 默认刷新间隔: #{Langfuse.configuration.flush_interval}秒"
14
+ puts " 默认自动刷新: #{Langfuse.configuration.auto_flush}"
13
15
 
14
16
  # 方法 1: 使用测试密钥创建客户端(仅用于演示)
15
17
  puts "\n📝 方法 1: 直接参数配置"
@@ -17,7 +19,9 @@ puts '代码示例:'
17
19
  puts 'client = Langfuse.new('
18
20
  puts " public_key: 'pk-lf-your-public-key',"
19
21
  puts " secret_key: 'sk-lf-your-secret-key',"
20
- puts " host: 'https://us.cloud.langfuse.com'"
22
+ puts " host: 'https://us.cloud.langfuse.com',"
23
+ puts ' flush_interval: 10, # 每10秒刷新一次'
24
+ puts ' auto_flush: true # 启用自动刷新(默认)'
21
25
  puts ')'
22
26
 
23
27
  # 使用测试密钥创建客户端
@@ -31,6 +35,8 @@ puts '✅ 客户端配置成功'
31
35
  puts " 主机: #{test_client.host}"
32
36
  puts " 超时: #{test_client.timeout}秒"
33
37
  puts " 重试: #{test_client.retries}次"
38
+ puts " 刷新间隔: #{test_client.flush_interval}秒"
39
+ puts " 自动刷新: #{test_client.auto_flush}"
34
40
 
35
41
  # 方法 2: 全局配置
36
42
  puts "\n📝 方法 2: 全局配置"
@@ -40,6 +46,8 @@ puts " config.public_key = 'pk-lf-your-public-key'"
40
46
  puts " config.secret_key = 'sk-lf-your-secret-key'"
41
47
  puts " config.host = 'https://us.cloud.langfuse.com'"
42
48
  puts ' config.debug = true'
49
+ puts ' config.flush_interval = 10 # 每10秒刷新一次'
50
+ puts ' config.auto_flush = true # 启用自动刷新'
43
51
  puts 'end'
44
52
 
45
53
  Langfuse.configure do |config|
@@ -49,6 +57,8 @@ Langfuse.configure do |config|
49
57
  config.debug = true
50
58
  config.timeout = 60
51
59
  config.retries = 5
60
+ config.flush_interval = 10
61
+ config.auto_flush = true
52
62
  end
53
63
 
54
64
  global_client = Langfuse.new
@@ -57,6 +67,8 @@ puts " 主机: #{global_client.host}"
57
67
  puts " 调试模式: #{global_client.debug}"
58
68
  puts " 超时: #{global_client.timeout}秒"
59
69
  puts " 重试: #{global_client.retries}次"
70
+ puts " 刷新间隔: #{global_client.flush_interval}秒"
71
+ puts " 自动刷新: #{global_client.auto_flush}"
60
72
 
61
73
  # 方法 3: 环境变量配置
62
74
  puts "\n📝 方法 3: 环境变量配置"
@@ -64,6 +76,8 @@ puts '设置环境变量:'
64
76
  puts "export LANGFUSE_PUBLIC_KEY='pk-lf-your-public-key'"
65
77
  puts "export LANGFUSE_SECRET_KEY='sk-lf-your-secret-key'"
66
78
  puts "export LANGFUSE_HOST='https://us.cloud.langfuse.com'"
79
+ puts 'export LANGFUSE_FLUSH_INTERVAL=10'
80
+ puts 'export LANGFUSE_AUTO_FLUSH=true'
67
81
  puts ''
68
82
  puts '然后使用:'
69
83
  puts 'client = Langfuse.new'
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'langfuse'
4
+
5
+ # Initialize the Langfuse client
6
+ client = Langfuse.new(
7
+ public_key: ENV['LANGFUSE_PUBLIC_KEY'],
8
+ secret_key: ENV['LANGFUSE_SECRET_KEY'],
9
+ host: ENV['LANGFUSE_HOST'] || 'https://cloud.langfuse.com'
10
+ )
11
+
12
+ puts '🎯 Starting event usage example...'
13
+
14
+ # Example 1: Create a trace and add events
15
+ puts "\n📝 Example 1: Creating events within a trace"
16
+
17
+ trace = client.trace(
18
+ name: 'user-workflow',
19
+ user_id: 'user-123',
20
+ session_id: 'session-456',
21
+ input: { action: 'start_workflow' }
22
+ )
23
+
24
+ puts "Created trace: #{trace.id}"
25
+
26
+ # Create a generic event
27
+ event1 = trace.event(
28
+ name: 'user_login',
29
+ input: { username: 'john_doe', login_method: 'oauth' },
30
+ output: { success: true, user_id: 'user-123' },
31
+ metadata: {
32
+ ip_address: '192.168.1.1',
33
+ user_agent: 'Mozilla/5.0...'
34
+ }
35
+ )
36
+
37
+ puts "Created event: #{event1.id}"
38
+
39
+ # Create another event with custom level
40
+ event2 = trace.event(
41
+ name: 'data_processing',
42
+ input: { data_size: 1024, format: 'json' },
43
+ output: { processed_records: 100, errors: 0 },
44
+ level: 'INFO',
45
+ metadata: {
46
+ processing_time_ms: 250,
47
+ memory_usage: '45MB'
48
+ }
49
+ )
50
+
51
+ puts "Created event: #{event2.id}"
52
+
53
+ # Example 2: Creating events from spans
54
+ puts "\n🔗 Example 2: Creating events from spans"
55
+
56
+ span = trace.span(
57
+ name: 'data-validation',
58
+ input: { data: 'raw_data' }
59
+ )
60
+
61
+ # Create an event within the span
62
+ validation_event = span.event(
63
+ name: 'validation_check',
64
+ input: { rules: %w[required format length] },
65
+ output: { valid: true, warnings: [] },
66
+ metadata: { validation_time_ms: 15 }
67
+ )
68
+
69
+ puts "Created span event: #{validation_event.id}"
70
+
71
+ # End the span
72
+ span.end(output: { validation_result: 'passed' })
73
+
74
+ # Example 3: Creating events from generations
75
+ puts "\n🤖 Example 3: Creating events from generations"
76
+
77
+ generation = trace.generation(
78
+ name: 'text-generation',
79
+ model: 'gpt-3.5-turbo',
80
+ input: [{ role: 'user', content: 'Hello, how are you?' }],
81
+ output: { content: 'I am doing well, thank you!' },
82
+ usage: { prompt_tokens: 12, completion_tokens: 8, total_tokens: 20 }
83
+ )
84
+
85
+ # Create an event for content filtering
86
+ filter_event = generation.event(
87
+ name: 'content_filter',
88
+ input: { text: 'I am doing well, thank you!' },
89
+ output: {
90
+ filtered: false,
91
+ flags: [],
92
+ confidence: 0.95
93
+ },
94
+ metadata: {
95
+ filter_model: 'content-filter-v1',
96
+ processing_time_ms: 5
97
+ }
98
+ )
99
+
100
+ puts "Created generation event: #{filter_event.id}"
101
+
102
+ # Example 4: Error event
103
+ puts "\n❌ Example 4: Error event"
104
+
105
+ error_event = trace.event(
106
+ name: 'error_occurred',
107
+ input: { operation: 'database_query' },
108
+ output: {
109
+ error: true,
110
+ message: 'Connection timeout',
111
+ code: 'DB_TIMEOUT'
112
+ },
113
+ level: 'ERROR',
114
+ status_message: 'Database connection failed',
115
+ metadata: {
116
+ retry_count: 3,
117
+ last_attempt: Time.now.iso8601
118
+ }
119
+ )
120
+
121
+ puts "Created error event: #{error_event.id}"
122
+
123
+ puts "Trace URL: #{trace.get_url}"
124
+
125
+ # Example 5: Direct event creation via client
126
+ puts "\n🎯 Example 5: Direct event creation via client"
127
+
128
+ direct_event = client.event(
129
+ trace_id: trace.id,
130
+ name: 'audit_log',
131
+ input: { action: 'workflow_completed', user_id: 'user-123' },
132
+ output: { logged: true, log_id: 'audit-789' },
133
+ metadata: {
134
+ timestamp: Time.now.iso8601,
135
+ source: 'audit_system'
136
+ }
137
+ )
138
+
139
+ puts "Created direct event: #{direct_event.id}"
140
+
141
+ puts "\n✅ Event usage example completed!"
142
+ puts 'Check the Langfuse dashboard to see all events in the trace.'
143
+
144
+ # Flush events to ensure they're sent
145
+ client.flush
@@ -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
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,
@@ -198,32 +217,99 @@ module Langfuse
198
217
  events = @event_queue.shift(@event_queue.length)
199
218
  return if events.empty?
200
219
 
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
- }
220
+ send_batch(events)
221
+ end
222
+
223
+ def shutdown
224
+ @flush_thread&.kill if @auto_flush
225
+ flush unless @event_queue.empty?
226
+ end
227
+
228
+ private
229
+
230
+ def debug_event_data(events)
231
+ return unless @debug
232
+
233
+ puts "\n=== Event Data Debug Information ==="
234
+ events.each_with_index do |event, index|
235
+ puts "Event #{index + 1}:"
236
+ puts " ID: #{event[:id]}"
237
+ puts " Type: #{event[:type]}"
238
+ puts " Timestamp: #{event[:timestamp]}"
239
+ puts " Body keys: #{event[:body]&.keys || 'nil'}"
240
+
241
+ # 检查常见的问题
242
+ puts ' ⚠️ WARNING: Empty or nil type!' if event[:type].nil? || event[:type].to_s.empty?
243
+
244
+ puts ' ⚠️ WARNING: Empty body!' if event[:body].nil?
245
+
246
+ puts ' ---'
247
+ end
248
+ puts "=== End Debug Information ===\n"
249
+ end
250
+
251
+ def send_batch(events)
252
+ # 调试事件数据
253
+ debug_event_data(events)
254
+
255
+ # 验证事件数据
256
+ valid_events = events.select do |event|
257
+ if event[:type].nil? || event[:type].to_s.empty?
258
+ puts "Warning: Event with empty type detected, skipping: #{event[:id]}" if @debug
259
+ false
260
+ elsif event[:body].nil?
261
+ puts "Warning: Event with empty body detected, skipping: #{event[:id]}" if @debug
262
+ false
263
+ else
264
+ true
265
+ end
266
+ end
267
+
268
+ if valid_events.empty?
269
+ puts 'No valid events to send' if @debug
270
+ return
271
+ end
272
+
273
+ batch_data = build_batch_data(valid_events)
274
+ puts "Sending batch data: #{batch_data}" if @debug
209
275
 
210
276
  begin
211
277
  response = post('/api/public/ingestion', batch_data)
212
- puts "Flushed #{events.length} events" if @debug
278
+ puts "Flushed #{valid_events.length} events" if @debug
279
+ response
213
280
  rescue StandardError => e
214
281
  puts "Failed to flush events: #{e.message}" if @debug
215
282
  # Re-queue events on failure
216
- events.each { |event| @event_queue << event }
283
+ valid_events.each { |event| @event_queue << event }
217
284
  raise
218
285
  end
219
286
  end
220
287
 
221
- def shutdown
222
- @flush_thread&.kill
223
- flush unless @event_queue.empty?
288
+ def build_batch_data(events)
289
+ {
290
+ batch: events,
291
+ metadata: Utils.deep_camelize_keys({
292
+ batch_size: events.length,
293
+ sdk_name: 'langfuse-ruby',
294
+ sdk_version: Langfuse::VERSION
295
+ })
296
+ }
224
297
  end
225
298
 
226
- private
299
+ def start_flush_thread
300
+ return unless @auto_flush
301
+
302
+ Thread.new do
303
+ loop do
304
+ sleep(@flush_interval) # Configurable flush interval
305
+ begin
306
+ flush unless @event_queue.empty?
307
+ rescue StandardError => e
308
+ puts "Error in flush thread: #{e.message}" if @debug
309
+ end
310
+ end
311
+ end
312
+ end
227
313
 
228
314
  def build_connection
229
315
  Faraday.new(url: @host) do |conn|
@@ -248,6 +334,27 @@ module Langfuse
248
334
  end
249
335
  end
250
336
 
337
+ # HTTP methods
338
+ def get(path, params = {})
339
+ request(:get, path, params: params)
340
+ end
341
+
342
+ def post(path, data = {})
343
+ request(:post, path, json: data)
344
+ end
345
+
346
+ def put(path, data = {})
347
+ request(:put, path, json: data)
348
+ end
349
+
350
+ def delete(path, params = {})
351
+ request(:delete, path, params: params)
352
+ end
353
+
354
+ def patch(path, data = {})
355
+ request(:patch, path, json: data)
356
+ end
357
+
251
358
  def request(method, path, params: {}, json: nil)
252
359
  retries_left = @retries
253
360
 
@@ -293,25 +400,27 @@ module Langfuse
293
400
  when 429
294
401
  raise RateLimitError, "Rate limit exceeded: #{response.body}"
295
402
  when 400..499
296
- raise ValidationError, "Client error (#{response.status}): #{response.body}"
403
+ # 400 错误提供更详细的错误信息
404
+ error_details = ''
405
+ if response.body.is_a?(Hash) && response.body['error']
406
+ error_details = "\nError details: #{response.body['error']}"
407
+ elsif response.body.is_a?(String)
408
+ error_details = "\nError details: #{response.body}"
409
+ end
410
+
411
+ # 特别处理类型验证错误
412
+ unless response.body.to_s.include?('invalid_union') || response.body.to_s.include?('discriminator')
413
+ raise ValidationError, "Client error (#{response.status}): #{response.body}#{error_details}"
414
+ end
415
+
416
+ raise ValidationError,
417
+ "Event type validation failed (#{response.status}): The event type or structure is invalid. Please check the event format.#{error_details}"
418
+
297
419
  when 500..599
298
420
  raise APIError, "Server error (#{response.status}): #{response.body}"
299
421
  else
300
422
  raise APIError, "Unexpected response (#{response.status}): #{response.body}"
301
423
  end
302
424
  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
425
  end
317
426
  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,