langfuse-ruby 0.1.1 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +59 -0
- data/Gemfile +5 -5
- data/Gemfile.lock +1 -1
- data/README.md +111 -20
- data/docs/PUBLISH_GUIDE.md +2 -2
- data/docs/README.md +0 -4
- data/docs/TYPE_VALIDATION_TROUBLESHOOTING.md +202 -0
- data/examples/auto_flush_control.rb +205 -0
- data/examples/basic_tracing.rb +0 -12
- data/examples/connection_config_demo.rb +141 -0
- data/examples/event_usage.rb +145 -0
- data/examples/prompt_management.rb +62 -71
- data/lib/langfuse/client.rb +176 -58
- data/lib/langfuse/event.rb +63 -0
- data/lib/langfuse/generation.rb +19 -40
- data/lib/langfuse/span.rb +17 -0
- data/lib/langfuse/trace.rb +17 -34
- data/lib/langfuse/utils.rb +23 -1
- data/lib/langfuse/version.rb +1 -1
- data/lib/langfuse.rb +5 -2
- data/test_offline.rb +93 -101
- metadata +8 -7
- data/.github/workflows/ci.yml +0 -47
- data/PROMPT_TROUBLESHOOTING.md +0 -206
- data/docs/CHANGELOG.md +0 -49
- data/docs/PROJECT_SUMMARY.md +0 -263
- data/test_basic.rb +0 -183
@@ -9,7 +9,7 @@ client = Langfuse.new(
|
|
9
9
|
host: ENV['LANGFUSE_HOST'] || 'https://cloud.langfuse.com'
|
10
10
|
)
|
11
11
|
|
12
|
-
puts
|
12
|
+
puts '🚀 Starting prompt management example...'
|
13
13
|
|
14
14
|
# Example 1: Create and use text prompts
|
15
15
|
puts "\n📝 Example 1: Create and use text prompts"
|
@@ -17,9 +17,9 @@ puts "\n📝 Example 1: Create and use text prompts"
|
|
17
17
|
begin
|
18
18
|
# Create a text prompt
|
19
19
|
text_prompt = client.create_prompt(
|
20
|
-
name:
|
21
|
-
prompt:
|
22
|
-
labels: [
|
20
|
+
name: 'greeting-prompt',
|
21
|
+
prompt: 'Hello {{user_name}}! Welcome to {{service_name}}. How can I help you with {{topic}} today?',
|
22
|
+
labels: %w[greeting customer-service],
|
23
23
|
config: {
|
24
24
|
temperature: 0.7,
|
25
25
|
max_tokens: 100
|
@@ -27,24 +27,22 @@ begin
|
|
27
27
|
)
|
28
28
|
|
29
29
|
puts "Created text prompt: #{text_prompt.name} (Version: #{text_prompt.version})"
|
30
|
-
|
31
30
|
rescue Langfuse::APIError => e
|
32
31
|
puts "Note: Prompt might already exist - #{e.message}"
|
33
32
|
end
|
34
33
|
|
35
34
|
# Get and use the prompt
|
36
35
|
begin
|
37
|
-
prompt = client.get_prompt(
|
36
|
+
prompt = client.get_prompt('greeting-prompt')
|
38
37
|
|
39
38
|
# Compile prompt with variables
|
40
39
|
compiled_text = prompt.compile(
|
41
|
-
user_name:
|
42
|
-
service_name:
|
43
|
-
topic:
|
40
|
+
user_name: 'Alice',
|
41
|
+
service_name: 'AI Assistant',
|
42
|
+
topic: 'machine learning'
|
44
43
|
)
|
45
44
|
|
46
45
|
puts "Compiled prompt: #{compiled_text}"
|
47
|
-
|
48
46
|
rescue Langfuse::APIError => e
|
49
47
|
puts "Could not retrieve prompt: #{e.message}"
|
50
48
|
end
|
@@ -55,18 +53,18 @@ puts "\n💬 Example 2: Create and use chat prompts"
|
|
55
53
|
begin
|
56
54
|
# Create a chat prompt
|
57
55
|
chat_prompt = client.create_prompt(
|
58
|
-
name:
|
56
|
+
name: 'ai-assistant-chat',
|
59
57
|
prompt: [
|
60
58
|
{
|
61
|
-
role:
|
62
|
-
content:
|
59
|
+
role: 'system',
|
60
|
+
content: 'You are a helpful AI assistant specialized in {{domain}}. Always be {{tone}} and provide {{detail_level}} answers.'
|
63
61
|
},
|
64
62
|
{
|
65
|
-
role:
|
66
|
-
content:
|
63
|
+
role: 'user',
|
64
|
+
content: '{{user_message}}'
|
67
65
|
}
|
68
66
|
],
|
69
|
-
labels: [
|
67
|
+
labels: %w[chat assistant ai],
|
70
68
|
config: {
|
71
69
|
temperature: 0.8,
|
72
70
|
max_tokens: 200
|
@@ -74,28 +72,26 @@ begin
|
|
74
72
|
)
|
75
73
|
|
76
74
|
puts "Created chat prompt: #{chat_prompt.name}"
|
77
|
-
|
78
75
|
rescue Langfuse::APIError => e
|
79
76
|
puts "Note: Chat prompt might already exist - #{e.message}"
|
80
77
|
end
|
81
78
|
|
82
79
|
# Get and use the chat prompt
|
83
80
|
begin
|
84
|
-
chat_prompt = client.get_prompt(
|
81
|
+
chat_prompt = client.get_prompt('ai-assistant-chat')
|
85
82
|
|
86
83
|
# Compile chat prompt with variables
|
87
84
|
compiled_messages = chat_prompt.compile(
|
88
|
-
domain:
|
89
|
-
tone:
|
90
|
-
detail_level:
|
91
|
-
user_message:
|
85
|
+
domain: 'software development',
|
86
|
+
tone: 'friendly and professional',
|
87
|
+
detail_level: 'detailed',
|
88
|
+
user_message: 'How do I implement a REST API in Ruby?'
|
92
89
|
)
|
93
90
|
|
94
|
-
puts
|
91
|
+
puts 'Compiled chat messages:'
|
95
92
|
compiled_messages.each_with_index do |message, index|
|
96
93
|
puts " #{index + 1}. #{message[:role]}: #{message[:content]}"
|
97
94
|
end
|
98
|
-
|
99
95
|
rescue Langfuse::APIError => e
|
100
96
|
puts "Could not retrieve chat prompt: #{e.message}"
|
101
97
|
end
|
@@ -112,34 +108,34 @@ puts "Template variables: #{translation_template.input_variables}"
|
|
112
108
|
|
113
109
|
# Use the template
|
114
110
|
translated_prompt = translation_template.format(
|
115
|
-
source_language:
|
116
|
-
target_language:
|
117
|
-
text:
|
111
|
+
source_language: 'English',
|
112
|
+
target_language: 'Spanish',
|
113
|
+
text: 'Hello, how are you today?'
|
118
114
|
)
|
119
115
|
|
120
116
|
puts "Translation prompt: #{translated_prompt}"
|
121
117
|
|
122
118
|
# Create a reusable chat template
|
123
119
|
coding_template = Langfuse::ChatPromptTemplate.from_messages([
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
])
|
120
|
+
{
|
121
|
+
role: 'system',
|
122
|
+
content: 'You are an expert {{language}} developer. Provide clean, well-commented code examples.'
|
123
|
+
},
|
124
|
+
{
|
125
|
+
role: 'user',
|
126
|
+
content: '{{request}}'
|
127
|
+
}
|
128
|
+
])
|
133
129
|
|
134
130
|
puts "Chat template variables: #{coding_template.input_variables}"
|
135
131
|
|
136
132
|
# Use the chat template
|
137
133
|
coding_messages = coding_template.format(
|
138
|
-
language:
|
139
|
-
request:
|
134
|
+
language: 'Ruby',
|
135
|
+
request: 'Show me how to create a simple HTTP server'
|
140
136
|
)
|
141
137
|
|
142
|
-
puts
|
138
|
+
puts 'Coding chat messages:'
|
143
139
|
coding_messages.each_with_index do |message, index|
|
144
140
|
puts " #{index + 1}. #{message[:role]}: #{message[:content]}"
|
145
141
|
end
|
@@ -149,17 +145,16 @@ puts "\n🔄 Example 4: Prompt versioning and caching"
|
|
149
145
|
|
150
146
|
# Get specific version of a prompt
|
151
147
|
begin
|
152
|
-
versioned_prompt = client.get_prompt(
|
148
|
+
versioned_prompt = client.get_prompt('greeting-prompt', version: 1)
|
153
149
|
puts "Retrieved prompt version: #{versioned_prompt.version}"
|
154
150
|
|
155
151
|
# Get latest version (cached)
|
156
|
-
latest_prompt = client.get_prompt(
|
152
|
+
latest_prompt = client.get_prompt('greeting-prompt')
|
157
153
|
puts "Latest prompt version: #{latest_prompt.version}"
|
158
154
|
|
159
155
|
# Get with label
|
160
|
-
labeled_prompt = client.get_prompt(
|
156
|
+
labeled_prompt = client.get_prompt('greeting-prompt', label: 'production')
|
161
157
|
puts "Labeled prompt: #{labeled_prompt.labels}"
|
162
|
-
|
163
158
|
rescue Langfuse::APIError => e
|
164
159
|
puts "Could not retrieve versioned prompt: #{e.message}"
|
165
160
|
end
|
@@ -169,27 +164,27 @@ puts "\n🔗 Example 5: Using prompts in tracing"
|
|
169
164
|
|
170
165
|
begin
|
171
166
|
# Get a prompt for use in generation
|
172
|
-
system_prompt = client.get_prompt(
|
167
|
+
system_prompt = client.get_prompt('ai-assistant-chat')
|
173
168
|
|
174
169
|
# Create a trace
|
175
170
|
trace = client.trace(
|
176
|
-
name:
|
177
|
-
user_id:
|
178
|
-
input: { message:
|
171
|
+
name: 'prompt-based-chat',
|
172
|
+
user_id: 'user-789',
|
173
|
+
input: { message: 'Explain Ruby blocks' }
|
179
174
|
)
|
180
175
|
|
181
176
|
# Compile the prompt
|
182
177
|
messages = system_prompt.compile(
|
183
|
-
domain:
|
184
|
-
tone:
|
185
|
-
detail_level:
|
186
|
-
user_message:
|
178
|
+
domain: 'Ruby programming',
|
179
|
+
tone: 'educational and clear',
|
180
|
+
detail_level: 'beginner-friendly',
|
181
|
+
user_message: 'Explain Ruby blocks'
|
187
182
|
)
|
188
183
|
|
189
184
|
# Create generation with prompt
|
190
185
|
generation = trace.generation(
|
191
|
-
name:
|
192
|
-
model:
|
186
|
+
name: 'openai-chat-with-prompt',
|
187
|
+
model: 'gpt-3.5-turbo',
|
193
188
|
input: messages,
|
194
189
|
output: {
|
195
190
|
content: "Ruby blocks are pieces of code that can be passed to methods. They're defined using either do...end or curly braces {}. Blocks are commonly used with iterators like .each, .map, and .select."
|
@@ -207,7 +202,6 @@ begin
|
|
207
202
|
|
208
203
|
puts "Created generation with prompt: #{generation.id}"
|
209
204
|
puts "Trace URL: #{trace.get_url}"
|
210
|
-
|
211
205
|
rescue Langfuse::APIError => e
|
212
206
|
puts "Could not use prompt in tracing: #{e.message}"
|
213
207
|
end
|
@@ -218,12 +212,12 @@ puts "\n🎯 Example 6: Advanced prompt features"
|
|
218
212
|
# Create a prompt with complex templating
|
219
213
|
begin
|
220
214
|
complex_prompt = client.create_prompt(
|
221
|
-
name:
|
215
|
+
name: 'code-review-prompt',
|
222
216
|
prompt: {
|
223
|
-
system:
|
217
|
+
system: 'You are a senior {{language}} developer reviewing code. Focus on {{review_aspects}}.',
|
224
218
|
user: "Please review this {{language}} code:\n\n```{{language}}\n{{code}}\n```\n\nProvide feedback on: {{specific_feedback}}"
|
225
219
|
},
|
226
|
-
labels: [
|
220
|
+
labels: %w[code-review development],
|
227
221
|
config: {
|
228
222
|
temperature: 0.3,
|
229
223
|
max_tokens: 500
|
@@ -231,7 +225,6 @@ begin
|
|
231
225
|
)
|
232
226
|
|
233
227
|
puts "Created complex prompt: #{complex_prompt.name}"
|
234
|
-
|
235
228
|
rescue Langfuse::APIError => e
|
236
229
|
puts "Note: Complex prompt might already exist - #{e.message}"
|
237
230
|
end
|
@@ -239,16 +232,14 @@ end
|
|
239
232
|
# Create a prompt with conditional logic (using Ruby)
|
240
233
|
class ConditionalPrompt
|
241
234
|
def self.generate(user_level:, topic:, include_examples: true)
|
242
|
-
base_prompt =
|
235
|
+
base_prompt = 'Explain {{topic}} for a {{user_level}} audience.'
|
243
236
|
|
244
|
-
if include_examples
|
245
|
-
base_prompt += " Include practical examples."
|
246
|
-
end
|
237
|
+
base_prompt += ' Include practical examples.' if include_examples
|
247
238
|
|
248
|
-
if user_level ==
|
249
|
-
base_prompt +=
|
250
|
-
elsif user_level ==
|
251
|
-
base_prompt +=
|
239
|
+
if user_level == 'beginner'
|
240
|
+
base_prompt += ' Use simple language and avoid jargon.'
|
241
|
+
elsif user_level == 'advanced'
|
242
|
+
base_prompt += ' Feel free to use technical terminology.'
|
252
243
|
end
|
253
244
|
|
254
245
|
base_prompt
|
@@ -256,8 +247,8 @@ class ConditionalPrompt
|
|
256
247
|
end
|
257
248
|
|
258
249
|
conditional_prompt_text = ConditionalPrompt.generate(
|
259
|
-
user_level:
|
260
|
-
topic:
|
250
|
+
user_level: 'beginner',
|
251
|
+
topic: 'machine learning',
|
261
252
|
include_examples: true
|
262
253
|
)
|
263
254
|
|
@@ -266,8 +257,8 @@ puts "Conditional prompt: #{conditional_prompt_text}"
|
|
266
257
|
# Use with template
|
267
258
|
conditional_template = Langfuse::PromptTemplate.from_template(conditional_prompt_text)
|
268
259
|
formatted_prompt = conditional_template.format(
|
269
|
-
topic:
|
270
|
-
user_level:
|
260
|
+
topic: 'neural networks',
|
261
|
+
user_level: 'beginner'
|
271
262
|
)
|
272
263
|
|
273
264
|
puts "Formatted conditional prompt: #{formatted_prompt}"
|
@@ -277,7 +268,7 @@ puts "\n🔄 Flushing events..."
|
|
277
268
|
client.flush
|
278
269
|
|
279
270
|
puts "\n✅ Prompt management example completed!"
|
280
|
-
puts
|
271
|
+
puts 'Check your Langfuse dashboard to see the prompts and traces.'
|
281
272
|
|
282
273
|
# Shutdown client
|
283
274
|
client.shutdown
|
data/lib/langfuse/client.rb
CHANGED
@@ -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}"
|
@@ -99,7 +125,7 @@ module Langfuse
|
|
99
125
|
return cached_prompt[:prompt]
|
100
126
|
end
|
101
127
|
|
102
|
-
path = "/api/public/prompts/#{name}"
|
128
|
+
path = "/api/public/v2/prompts/#{name}"
|
103
129
|
params = {}
|
104
130
|
params[:version] = version if version
|
105
131
|
params[:label] = label if label
|
@@ -138,7 +164,7 @@ module Langfuse
|
|
138
164
|
**kwargs
|
139
165
|
}
|
140
166
|
|
141
|
-
response = post('/api/public/prompts', data)
|
167
|
+
response = post('/api/public/v2/prompts', data)
|
142
168
|
Prompt.new(response.body)
|
143
169
|
end
|
144
170
|
|
@@ -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,47 +217,144 @@ module Langfuse
|
|
198
217
|
events = @event_queue.shift(@event_queue.length)
|
199
218
|
return if events.empty?
|
200
219
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
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 #{
|
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
|
-
|
283
|
+
valid_events.each { |event| @event_queue << event }
|
217
284
|
raise
|
218
285
|
end
|
219
286
|
end
|
220
287
|
|
221
|
-
def
|
222
|
-
|
223
|
-
|
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
|
-
|
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|
|
230
|
-
|
316
|
+
# 配置请求和响应处理
|
231
317
|
conn.request :json
|
232
|
-
# 更宽松的 JSON 响应处理,支持更多 content-type
|
233
318
|
conn.response :json, content_type: /\bjson$/
|
234
|
-
|
319
|
+
|
320
|
+
# 设置 User-Agent 头部
|
321
|
+
conn.headers['User-Agent'] = "langfuse-ruby/#{Langfuse::VERSION}"
|
322
|
+
# 根据 Langfuse 文档配置 Basic Auth
|
323
|
+
# username: Langfuse Public Key, password: Langfuse Secret Key
|
324
|
+
conn.headers['Authorization'] = "Basic #{Base64.strict_encode64("#{@public_key}:#{@secret_key}")}"
|
325
|
+
|
326
|
+
# 设置超时
|
235
327
|
conn.options.timeout = @timeout
|
236
328
|
|
237
|
-
#
|
329
|
+
# 添加调试日志
|
238
330
|
conn.response :logger if @debug
|
331
|
+
|
332
|
+
# 使用默认适配器
|
333
|
+
conn.adapter Faraday.default_adapter
|
239
334
|
end
|
240
335
|
end
|
241
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
|
+
|
242
358
|
def request(method, path, params: {}, json: nil)
|
243
359
|
retries_left = @retries
|
244
360
|
|
@@ -284,25 +400,27 @@ module Langfuse
|
|
284
400
|
when 429
|
285
401
|
raise RateLimitError, "Rate limit exceeded: #{response.body}"
|
286
402
|
when 400..499
|
287
|
-
|
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
|
+
|
288
419
|
when 500..599
|
289
420
|
raise APIError, "Server error (#{response.status}): #{response.body}"
|
290
421
|
else
|
291
422
|
raise APIError, "Unexpected response (#{response.status}): #{response.body}"
|
292
423
|
end
|
293
424
|
end
|
294
|
-
|
295
|
-
def start_flush_thread
|
296
|
-
Thread.new do
|
297
|
-
loop do
|
298
|
-
sleep(5) # Flush every 5 seconds
|
299
|
-
begin
|
300
|
-
flush unless @event_queue.empty?
|
301
|
-
rescue StandardError => e
|
302
|
-
puts "Error in flush thread: #{e.message}" if @debug
|
303
|
-
end
|
304
|
-
end
|
305
|
-
end
|
306
|
-
end
|
307
425
|
end
|
308
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
|