langfuse-ruby 0.1.7 → 0.2.0
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/.github/workflows/ci.yml +21 -10
- data/.github/workflows/release.yml +2 -2
- data/.mise.toml +8 -0
- data/.rubocop.yml +2 -2
- data/CHANGELOG.md +34 -10
- data/CLAUDE.md +21 -9
- data/Gemfile +1 -0
- data/Gemfile.lock +15 -5
- data/Makefile +5 -1
- data/README.md +141 -9
- data/langfuse-ruby.gemspec +2 -0
- data/lib/langfuse/client.rb +345 -74
- data/lib/langfuse/generation.rb +36 -4
- data/lib/langfuse/otel_exporter.rb +49 -57
- data/lib/langfuse/span.rb +1 -0
- data/lib/langfuse/trace.rb +16 -6
- data/lib/langfuse/utils.rb +32 -0
- data/lib/langfuse/version.rb +1 -1
- data/lib/langfuse.rb +14 -6
- metadata +31 -2
data/lib/langfuse/generation.rb
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
module Langfuse
|
|
4
4
|
class Generation
|
|
5
5
|
attr_reader :id, :trace_id, :name, :start_time, :end_time, :completion_start_time,
|
|
6
|
-
:model, :model_parameters, :input, :output, :usage, :
|
|
6
|
+
:model, :model_parameters, :input, :output, :usage, :usage_details, :cost_details,
|
|
7
|
+
:prompt_name, :prompt_version, :metadata, :level,
|
|
7
8
|
:status_message, :parent_observation_id, :version, :as_type, :client
|
|
8
9
|
|
|
9
10
|
def initialize(client:, trace_id:, id: nil, name: nil, start_time: nil, end_time: nil,
|
|
10
11
|
completion_start_time: nil, model: nil, model_parameters: nil, input: nil,
|
|
11
|
-
output: nil, usage: nil,
|
|
12
|
+
output: nil, usage: nil, usage_details: nil, cost_details: nil, prompt: nil,
|
|
13
|
+
metadata: nil, level: nil, status_message: nil,
|
|
12
14
|
parent_observation_id: nil, version: nil, as_type: nil, **kwargs)
|
|
13
15
|
@client = client
|
|
14
16
|
@id = id || Utils.generate_id
|
|
@@ -22,6 +24,9 @@ module Langfuse
|
|
|
22
24
|
@input = input
|
|
23
25
|
@output = output
|
|
24
26
|
@usage = usage || {}
|
|
27
|
+
@usage_details = usage_details || {}
|
|
28
|
+
@cost_details = cost_details || {}
|
|
29
|
+
@prompt_name, @prompt_version = extract_prompt_info(prompt)
|
|
25
30
|
@metadata = metadata || {}
|
|
26
31
|
@level = level
|
|
27
32
|
@status_message = status_message
|
|
@@ -35,7 +40,8 @@ module Langfuse
|
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
def update(name: nil, end_time: nil, completion_start_time: nil, model: nil,
|
|
38
|
-
model_parameters: nil, input: nil, output: nil, usage: nil,
|
|
43
|
+
model_parameters: nil, input: nil, output: nil, usage: nil,
|
|
44
|
+
usage_details: nil, cost_details: nil, prompt: nil, metadata: nil,
|
|
39
45
|
level: nil, status_message: nil, version: nil, **kwargs)
|
|
40
46
|
@name = name if name
|
|
41
47
|
@end_time = end_time if end_time
|
|
@@ -45,6 +51,9 @@ module Langfuse
|
|
|
45
51
|
@input = input if input
|
|
46
52
|
@output = output if output
|
|
47
53
|
@usage.merge!(usage) if usage
|
|
54
|
+
@usage_details.merge!(usage_details) if usage_details
|
|
55
|
+
@cost_details.merge!(cost_details) if cost_details
|
|
56
|
+
@prompt_name, @prompt_version = extract_prompt_info(prompt) if prompt
|
|
48
57
|
@metadata.merge!(metadata) if metadata
|
|
49
58
|
@level = level if level
|
|
50
59
|
@status_message = status_message if status_message
|
|
@@ -55,10 +64,12 @@ module Langfuse
|
|
|
55
64
|
self
|
|
56
65
|
end
|
|
57
66
|
|
|
58
|
-
def end(output: nil, end_time: nil, usage: nil, **kwargs)
|
|
67
|
+
def end(output: nil, end_time: nil, usage: nil, usage_details: nil, cost_details: nil, **kwargs)
|
|
59
68
|
@end_time = end_time || Utils.current_timestamp
|
|
60
69
|
@output = output if output
|
|
61
70
|
@usage.merge!(usage) if usage
|
|
71
|
+
@usage_details.merge!(usage_details) if usage_details
|
|
72
|
+
@cost_details.merge!(cost_details) if cost_details
|
|
62
73
|
@kwargs.merge!(kwargs)
|
|
63
74
|
|
|
64
75
|
update_generation
|
|
@@ -261,6 +272,7 @@ module Langfuse
|
|
|
261
272
|
|
|
262
273
|
def score(name:, value:, data_type: nil, comment: nil, **kwargs)
|
|
263
274
|
@client.score(
|
|
275
|
+
trace_id: @trace_id,
|
|
264
276
|
observation_id: @id,
|
|
265
277
|
name: name,
|
|
266
278
|
value: value,
|
|
@@ -293,12 +305,32 @@ module Langfuse
|
|
|
293
305
|
parent_observation_id: @parent_observation_id,
|
|
294
306
|
version: @version
|
|
295
307
|
}
|
|
308
|
+
data[:usage_details] = @usage_details unless @usage_details.nil? || @usage_details.empty?
|
|
309
|
+
data[:cost_details] = @cost_details unless @cost_details.nil? || @cost_details.empty?
|
|
310
|
+
data[:prompt_name] = @prompt_name if @prompt_name
|
|
311
|
+
data[:prompt_version] = @prompt_version if @prompt_version
|
|
296
312
|
data[:type] = @as_type if @as_type
|
|
297
313
|
data.merge(@kwargs).compact
|
|
298
314
|
end
|
|
299
315
|
|
|
300
316
|
private
|
|
301
317
|
|
|
318
|
+
# Accepts a Langfuse::Prompt, a Hash with :name/:version, or nil.
|
|
319
|
+
# Returns [prompt_name, prompt_version] used to link the generation to a prompt version.
|
|
320
|
+
def extract_prompt_info(prompt)
|
|
321
|
+
return [nil, nil] if prompt.nil?
|
|
322
|
+
|
|
323
|
+
if prompt.respond_to?(:name) && prompt.respond_to?(:version)
|
|
324
|
+
[prompt.name, prompt.version]
|
|
325
|
+
elsif prompt.is_a?(Hash)
|
|
326
|
+
name = prompt[:name] || prompt['name']
|
|
327
|
+
version = prompt[:version] || prompt['version']
|
|
328
|
+
[name, version]
|
|
329
|
+
else
|
|
330
|
+
[nil, nil]
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
302
334
|
def validate_as_type(type)
|
|
303
335
|
return nil if type.nil?
|
|
304
336
|
|
|
@@ -9,21 +9,45 @@ module Langfuse
|
|
|
9
9
|
class OtelExporter
|
|
10
10
|
OTEL_ENDPOINT = '/api/public/otel/v1/traces'
|
|
11
11
|
|
|
12
|
+
class << self
|
|
13
|
+
# Convert an ID (UUID or hex string) to an OTEL 32-char hex trace ID.
|
|
14
|
+
# OTEL trace IDs are 16 bytes (32 hex chars). Native hex IDs pass through unchanged.
|
|
15
|
+
def to_otel_trace_id(id_str)
|
|
16
|
+
return '0' * 32 unless id_str
|
|
17
|
+
|
|
18
|
+
hex = id_str.to_s.delete('-')
|
|
19
|
+
hex.ljust(32, '0')[0, 32]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Convert an ID (UUID or hex string) to an OTEL 16-char hex span ID.
|
|
23
|
+
# OTEL span IDs are 8 bytes (16 hex chars). Native hex IDs pass through unchanged.
|
|
24
|
+
def to_otel_span_id(id_str)
|
|
25
|
+
return '0' * 16 unless id_str
|
|
26
|
+
|
|
27
|
+
hex = id_str.to_s.delete('-')
|
|
28
|
+
hex[0, 16]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
12
32
|
# @param connection [Faraday::Connection] HTTP connection to Langfuse host
|
|
13
33
|
# @param debug [Boolean] whether to print debug output
|
|
14
|
-
|
|
34
|
+
# @param logger [Logger, nil] logger for debug output
|
|
35
|
+
def initialize(connection:, debug: false, logger: nil)
|
|
15
36
|
@connection = connection
|
|
16
37
|
@debug = debug
|
|
38
|
+
@logger = logger
|
|
17
39
|
end
|
|
18
40
|
|
|
19
41
|
# Export a batch of Langfuse events as OTLP spans.
|
|
42
|
+
# Note: score-create events are not part of the OTLP mapping and are
|
|
43
|
+
# handled separately by the client via the ingestion API.
|
|
20
44
|
# @param events [Array<Hash>] array of event hashes from the event queue
|
|
21
45
|
# @return [Faraday::Response]
|
|
22
46
|
def export(events)
|
|
23
47
|
resource_spans = build_resource_spans(events)
|
|
24
48
|
payload = { resourceSpans: resource_spans }
|
|
25
49
|
|
|
26
|
-
|
|
50
|
+
log_debug { "OTEL export payload: #{JSON.pretty_generate(payload)}" }
|
|
27
51
|
|
|
28
52
|
@connection.post(OTEL_ENDPOINT) do |req|
|
|
29
53
|
req.headers['Content-Type'] = 'application/json'
|
|
@@ -33,6 +57,16 @@ module Langfuse
|
|
|
33
57
|
|
|
34
58
|
private
|
|
35
59
|
|
|
60
|
+
def log_debug(&block)
|
|
61
|
+
return unless @debug
|
|
62
|
+
|
|
63
|
+
if @logger
|
|
64
|
+
@logger.debug(block.call)
|
|
65
|
+
else
|
|
66
|
+
puts block.call
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
36
70
|
# Build the top-level resourceSpans array from events.
|
|
37
71
|
# Groups events by trace_id, producing one scopeSpan per trace.
|
|
38
72
|
def build_resource_spans(events)
|
|
@@ -86,8 +120,6 @@ module Langfuse
|
|
|
86
120
|
build_observation_span(body, 'generation')
|
|
87
121
|
when 'event-create'
|
|
88
122
|
build_event_span(body)
|
|
89
|
-
when 'score-create'
|
|
90
|
-
build_score_span(body)
|
|
91
123
|
end
|
|
92
124
|
end
|
|
93
125
|
|
|
@@ -102,6 +134,9 @@ module Langfuse
|
|
|
102
134
|
add_attr(attributes, 'langfuse.session.id', body['sessionId'])
|
|
103
135
|
add_attr(attributes, 'langfuse.release', body['release'])
|
|
104
136
|
add_attr(attributes, 'langfuse.version', body['version'])
|
|
137
|
+
add_attr(attributes, 'langfuse.environment', body['environment'])
|
|
138
|
+
add_attr(attributes, 'langfuse.trace.public', body['public']) unless body['public'].nil?
|
|
139
|
+
add_attr(attributes, 'langfuse.internal.as_root', true)
|
|
105
140
|
add_json_attr(attributes, 'langfuse.trace.input', body['input'])
|
|
106
141
|
add_json_attr(attributes, 'langfuse.trace.output', body['output'])
|
|
107
142
|
add_json_attr(attributes, 'langfuse.trace.metadata', body['metadata'])
|
|
@@ -186,44 +221,6 @@ module Langfuse
|
|
|
186
221
|
span
|
|
187
222
|
end
|
|
188
223
|
|
|
189
|
-
# Build a minimal OTEL span for a score event.
|
|
190
|
-
def build_score_span(body)
|
|
191
|
-
trace_id_raw = body['traceId']
|
|
192
|
-
return nil unless trace_id_raw
|
|
193
|
-
|
|
194
|
-
trace_id = to_otel_trace_id(trace_id_raw)
|
|
195
|
-
span_id = to_otel_span_id(body['id'] || SecureRandom.uuid)
|
|
196
|
-
timestamp = to_unix_nano(body['timestamp'] || Time.now.utc.iso8601(3))
|
|
197
|
-
|
|
198
|
-
attributes = []
|
|
199
|
-
add_attr(attributes, 'langfuse.score.name', body['name'])
|
|
200
|
-
add_attr(attributes, 'langfuse.score.value', body['value'])
|
|
201
|
-
add_attr(attributes, 'langfuse.score.data_type', body['dataType'])
|
|
202
|
-
add_attr(attributes, 'langfuse.score.comment', body['comment'])
|
|
203
|
-
add_attr(attributes, 'langfuse.observation.type', 'score')
|
|
204
|
-
|
|
205
|
-
if body['observationId']
|
|
206
|
-
add_attr(attributes, 'langfuse.score.observation_id', body['observationId'])
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
span = {
|
|
210
|
-
traceId: trace_id,
|
|
211
|
-
spanId: span_id,
|
|
212
|
-
name: "score-#{body['name']}",
|
|
213
|
-
kind: 1,
|
|
214
|
-
startTimeUnixNano: timestamp,
|
|
215
|
-
endTimeUnixNano: timestamp,
|
|
216
|
-
attributes: attributes,
|
|
217
|
-
status: { code: 1 }
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
# Parent is either the observation or the trace
|
|
221
|
-
parent_raw = body['observationId'] || trace_id_raw
|
|
222
|
-
span[:parentSpanId] = to_otel_span_id(parent_raw) if parent_raw
|
|
223
|
-
|
|
224
|
-
span
|
|
225
|
-
end
|
|
226
|
-
|
|
227
224
|
# Build OTEL attributes for a span/generation observation.
|
|
228
225
|
def build_observation_attributes(body, obs_type)
|
|
229
226
|
attributes = []
|
|
@@ -234,6 +231,7 @@ module Langfuse
|
|
|
234
231
|
add_json_attr(attributes, 'langfuse.observation.metadata', body['metadata'])
|
|
235
232
|
add_attr(attributes, 'langfuse.observation.level', body['level'])
|
|
236
233
|
add_attr(attributes, 'langfuse.observation.status_message', body['statusMessage'])
|
|
234
|
+
add_attr(attributes, 'langfuse.environment', body['environment'])
|
|
237
235
|
|
|
238
236
|
if obs_type == 'generation'
|
|
239
237
|
add_generation_attributes(attributes, body)
|
|
@@ -261,25 +259,19 @@ module Langfuse
|
|
|
261
259
|
add_attr(attributes, 'gen_ai.usage.total_tokens', total) if total
|
|
262
260
|
end
|
|
263
261
|
|
|
262
|
+
add_json_attr(attributes, 'langfuse.observation.usage_details', body['usageDetails'])
|
|
263
|
+
add_json_attr(attributes, 'langfuse.observation.cost_details', body['costDetails'])
|
|
264
|
+
add_attr(attributes, 'langfuse.observation.prompt.name', body['promptName'])
|
|
265
|
+
add_attr(attributes, 'langfuse.observation.prompt.version', body['promptVersion'])
|
|
264
266
|
add_attr(attributes, 'langfuse.observation.completion_start_time', body['completionStartTime'])
|
|
265
267
|
end
|
|
266
268
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
def to_otel_trace_id(uuid_str)
|
|
270
|
-
return '0' * 32 unless uuid_str
|
|
271
|
-
|
|
272
|
-
hex = uuid_str.to_s.delete('-')
|
|
273
|
-
hex.ljust(32, '0')[0, 32]
|
|
269
|
+
def to_otel_trace_id(id_str)
|
|
270
|
+
self.class.to_otel_trace_id(id_str)
|
|
274
271
|
end
|
|
275
272
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def to_otel_span_id(uuid_str)
|
|
279
|
-
return '0' * 16 unless uuid_str
|
|
280
|
-
|
|
281
|
-
hex = uuid_str.to_s.delete('-')
|
|
282
|
-
hex[0, 16]
|
|
273
|
+
def to_otel_span_id(id_str)
|
|
274
|
+
self.class.to_otel_span_id(id_str)
|
|
283
275
|
end
|
|
284
276
|
|
|
285
277
|
# Convert an ISO8601 timestamp string to nanoseconds since epoch.
|
|
@@ -287,7 +279,7 @@ module Langfuse
|
|
|
287
279
|
return '0' unless timestamp_str
|
|
288
280
|
|
|
289
281
|
time = Time.parse(timestamp_str.to_s)
|
|
290
|
-
(
|
|
282
|
+
(time.to_f * 1_000_000_000).to_i.to_s
|
|
291
283
|
rescue ArgumentError
|
|
292
284
|
'0'
|
|
293
285
|
end
|
data/lib/langfuse/span.rb
CHANGED
data/lib/langfuse/trace.rb
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
module Langfuse
|
|
4
4
|
class Trace
|
|
5
5
|
attr_reader :id, :name, :user_id, :session_id, :version, :release, :input, :output,
|
|
6
|
-
:metadata, :tags, :timestamp, :client
|
|
6
|
+
:metadata, :tags, :timestamp, :public, :client
|
|
7
7
|
|
|
8
8
|
def initialize(client:, id:, name: nil, user_id: nil, session_id: nil, version: nil,
|
|
9
9
|
release: nil, input: nil, output: nil, metadata: nil, tags: nil,
|
|
10
|
-
timestamp: nil, **kwargs)
|
|
10
|
+
timestamp: nil, public: nil, **kwargs)
|
|
11
11
|
@client = client
|
|
12
12
|
@id = id
|
|
13
13
|
@name = name
|
|
@@ -20,6 +20,7 @@ module Langfuse
|
|
|
20
20
|
@metadata = metadata || {}
|
|
21
21
|
@tags = tags || []
|
|
22
22
|
@timestamp = timestamp
|
|
23
|
+
@public = public
|
|
23
24
|
@kwargs = kwargs
|
|
24
25
|
|
|
25
26
|
# Create the trace
|
|
@@ -50,6 +51,7 @@ module Langfuse
|
|
|
50
51
|
# Create a child generation
|
|
51
52
|
def generation(name: nil, start_time: nil, end_time: nil, completion_start_time: nil,
|
|
52
53
|
model: nil, model_parameters: nil, input: nil, output: nil, usage: nil,
|
|
54
|
+
usage_details: nil, cost_details: nil, prompt: nil,
|
|
53
55
|
metadata: nil, level: nil, status_message: nil, parent_observation_id: nil,
|
|
54
56
|
version: nil, **kwargs)
|
|
55
57
|
@client.generation(
|
|
@@ -63,6 +65,9 @@ module Langfuse
|
|
|
63
65
|
input: input,
|
|
64
66
|
output: output,
|
|
65
67
|
usage: usage,
|
|
68
|
+
usage_details: usage_details,
|
|
69
|
+
cost_details: cost_details,
|
|
70
|
+
prompt: prompt,
|
|
66
71
|
metadata: metadata,
|
|
67
72
|
level: level,
|
|
68
73
|
status_message: status_message,
|
|
@@ -247,7 +252,8 @@ module Langfuse
|
|
|
247
252
|
end
|
|
248
253
|
|
|
249
254
|
def update(name: nil, user_id: nil, session_id: nil, version: nil,
|
|
250
|
-
release: nil, input: nil, output: nil, metadata: nil, tags: nil,
|
|
255
|
+
release: nil, input: nil, output: nil, metadata: nil, tags: nil,
|
|
256
|
+
public: nil, **kwargs)
|
|
251
257
|
# 更新实例变量
|
|
252
258
|
@name = name if name
|
|
253
259
|
@user_id = user_id if user_id
|
|
@@ -258,6 +264,7 @@ module Langfuse
|
|
|
258
264
|
@output = output if output
|
|
259
265
|
@metadata = metadata if metadata
|
|
260
266
|
@tags = tags if tags
|
|
267
|
+
@public = public unless public.nil?
|
|
261
268
|
@kwargs.merge!(kwargs) if kwargs.any?
|
|
262
269
|
# 触发 trace-update 事件
|
|
263
270
|
update_trace
|
|
@@ -279,7 +286,8 @@ module Langfuse
|
|
|
279
286
|
output: @output,
|
|
280
287
|
metadata: @metadata,
|
|
281
288
|
tags: @tags,
|
|
282
|
-
timestamp: @timestamp
|
|
289
|
+
timestamp: @timestamp,
|
|
290
|
+
public: @public
|
|
283
291
|
}.merge(@kwargs).compact
|
|
284
292
|
end
|
|
285
293
|
|
|
@@ -297,7 +305,8 @@ module Langfuse
|
|
|
297
305
|
output: @output,
|
|
298
306
|
metadata: @metadata,
|
|
299
307
|
tags: @tags,
|
|
300
|
-
timestamp: @timestamp
|
|
308
|
+
timestamp: @timestamp,
|
|
309
|
+
public: @public
|
|
301
310
|
}.merge(@kwargs).compact
|
|
302
311
|
|
|
303
312
|
@client.enqueue_event('trace-create', data)
|
|
@@ -315,7 +324,8 @@ module Langfuse
|
|
|
315
324
|
output: @output,
|
|
316
325
|
metadata: @metadata,
|
|
317
326
|
tags: @tags,
|
|
318
|
-
timestamp: @timestamp
|
|
327
|
+
timestamp: @timestamp,
|
|
328
|
+
public: @public
|
|
319
329
|
}.merge(@kwargs).compact
|
|
320
330
|
|
|
321
331
|
@client.enqueue_event('trace-update', data)
|
data/lib/langfuse/utils.rb
CHANGED
|
@@ -6,11 +6,25 @@ require 'erb'
|
|
|
6
6
|
|
|
7
7
|
module Langfuse
|
|
8
8
|
module Utils
|
|
9
|
+
# Body keys whose values must be passed through verbatim (user data),
|
|
10
|
+
# everything else nested under them keeps its original key format.
|
|
11
|
+
VERBATIM_BODY_KEYS = %w[input output metadata usageDetails costDetails modelParameters].freeze
|
|
12
|
+
|
|
9
13
|
class << self
|
|
10
14
|
def generate_id
|
|
11
15
|
SecureRandom.uuid
|
|
12
16
|
end
|
|
13
17
|
|
|
18
|
+
# W3C-compatible 32-char hex trace ID (16 bytes), used for OTel ingestion
|
|
19
|
+
def generate_hex_trace_id
|
|
20
|
+
SecureRandom.hex(16)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# W3C-compatible 16-char hex span/observation ID (8 bytes), used for OTel ingestion
|
|
24
|
+
def generate_hex_span_id
|
|
25
|
+
SecureRandom.hex(8)
|
|
26
|
+
end
|
|
27
|
+
|
|
14
28
|
def current_timestamp
|
|
15
29
|
Time.now.utc.iso8601(3)
|
|
16
30
|
end
|
|
@@ -39,6 +53,24 @@ module Langfuse
|
|
|
39
53
|
end
|
|
40
54
|
end
|
|
41
55
|
|
|
56
|
+
# Prepare an event body for the ingestion API:
|
|
57
|
+
# - top-level keys are camelized (snake_case -> camelCase)
|
|
58
|
+
# - user data values (input/output/metadata/usageDetails/costDetails/modelParameters)
|
|
59
|
+
# are passed through verbatim so user-provided keys are not mangled
|
|
60
|
+
# - the legacy `usage` object keys are camelized (prompt_tokens -> promptTokens)
|
|
61
|
+
def prepare_event_body(body)
|
|
62
|
+
return body unless body.is_a?(Hash)
|
|
63
|
+
|
|
64
|
+
body.each_with_object({}) do |(key, value), result|
|
|
65
|
+
new_key = camelize_key(key.to_s)
|
|
66
|
+
result[new_key] = if !VERBATIM_BODY_KEYS.include?(new_key) && value.is_a?(Hash)
|
|
67
|
+
deep_stringify_keys(value)
|
|
68
|
+
else
|
|
69
|
+
value
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
42
74
|
# 将哈希的键名转换为小驼峰格式
|
|
43
75
|
def deep_camelize_keys(hash)
|
|
44
76
|
return hash unless hash.is_a?(Hash)
|
data/lib/langfuse/version.rb
CHANGED
data/lib/langfuse.rb
CHANGED
|
@@ -16,6 +16,8 @@ require_relative 'langfuse/otel_exporter'
|
|
|
16
16
|
|
|
17
17
|
# Ruby SDK for Langfuse - Open source LLM engineering platform
|
|
18
18
|
module Langfuse
|
|
19
|
+
CLIENT_MUTEX = Mutex.new
|
|
20
|
+
|
|
19
21
|
class << self
|
|
20
22
|
# Configure the Langfuse client with default settings
|
|
21
23
|
def configure
|
|
@@ -31,10 +33,10 @@ module Langfuse
|
|
|
31
33
|
Client.new(**kwargs)
|
|
32
34
|
end
|
|
33
35
|
|
|
34
|
-
# Get a thread-safe singleton client instance
|
|
36
|
+
# Get a process-wide, thread-safe singleton client instance
|
|
35
37
|
# @return [Client] Langfuse client
|
|
36
38
|
def client
|
|
37
|
-
|
|
39
|
+
@client || CLIENT_MUTEX.synchronize { @client ||= Client.new }
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
# Get a prompt and optionally compile it with variables
|
|
@@ -59,7 +61,7 @@ module Langfuse
|
|
|
59
61
|
end
|
|
60
62
|
rescue StandardError => e
|
|
61
63
|
if attempts <= retries
|
|
62
|
-
sleep_time = 2**(attempts - 1) * 0.1 # Exponential backoff: 0.1s, 0.2s, 0.4s...
|
|
64
|
+
sleep_time = (2**(attempts - 1)) * 0.1 # Exponential backoff: 0.1s, 0.2s, 0.4s...
|
|
63
65
|
warn "Langfuse prompt fetch failed (#{prompt_name}), retrying in #{sleep_time}s... (attempt #{attempts}/#{retries + 1})" if configuration.debug
|
|
64
66
|
sleep(sleep_time)
|
|
65
67
|
retry
|
|
@@ -100,7 +102,7 @@ module Langfuse
|
|
|
100
102
|
# Langfuse.flush
|
|
101
103
|
#
|
|
102
104
|
def trace(name = nil, user_id: nil, session_id: nil, input: nil, output: nil,
|
|
103
|
-
metadata: nil, tags: nil, version: nil, release: nil, **kwargs
|
|
105
|
+
metadata: nil, tags: nil, version: nil, release: nil, **kwargs)
|
|
104
106
|
trace = client.trace(
|
|
105
107
|
name: name,
|
|
106
108
|
user_id: user_id,
|
|
@@ -152,14 +154,14 @@ module Langfuse
|
|
|
152
154
|
|
|
153
155
|
# Reset the singleton client (mainly for testing)
|
|
154
156
|
def reset!
|
|
155
|
-
|
|
157
|
+
CLIENT_MUTEX.synchronize { @client = nil }
|
|
156
158
|
end
|
|
157
159
|
end
|
|
158
160
|
|
|
159
161
|
# Configuration class for Langfuse client settings
|
|
160
162
|
class Configuration
|
|
161
163
|
attr_accessor :public_key, :secret_key, :host, :debug, :timeout, :retries, :flush_interval, :auto_flush,
|
|
162
|
-
:ingestion_mode
|
|
164
|
+
:ingestion_mode, :environment, :sample_rate, :mask, :flush_at, :logger, :shutdown_on_exit
|
|
163
165
|
|
|
164
166
|
def initialize
|
|
165
167
|
@public_key = nil
|
|
@@ -171,6 +173,12 @@ module Langfuse
|
|
|
171
173
|
@flush_interval = 5
|
|
172
174
|
@auto_flush = true
|
|
173
175
|
@ingestion_mode = :legacy # :legacy or :otel
|
|
176
|
+
@environment = nil # default tracing environment for all events
|
|
177
|
+
@sample_rate = nil # 0.0..1.0, nil disables sampling
|
|
178
|
+
@mask = nil # callable applied to input/output/metadata before sending
|
|
179
|
+
@flush_at = 15 # flush as soon as this many events are queued
|
|
180
|
+
@logger = nil # custom Logger instance
|
|
181
|
+
@shutdown_on_exit = true # register an at_exit hook that flushes pending events
|
|
174
182
|
end
|
|
175
183
|
end
|
|
176
184
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: langfuse-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Richard Sun
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: base64
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.1.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.1.0
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: concurrent-ruby
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -147,6 +161,20 @@ dependencies:
|
|
|
147
161
|
- - "~>"
|
|
148
162
|
- !ruby/object:Gem::Version
|
|
149
163
|
version: '1.0'
|
|
164
|
+
- !ruby/object:Gem::Dependency
|
|
165
|
+
name: tsort
|
|
166
|
+
requirement: !ruby/object:Gem::Requirement
|
|
167
|
+
requirements:
|
|
168
|
+
- - ">="
|
|
169
|
+
- !ruby/object:Gem::Version
|
|
170
|
+
version: 0.1.0
|
|
171
|
+
type: :development
|
|
172
|
+
prerelease: false
|
|
173
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
174
|
+
requirements:
|
|
175
|
+
- - ">="
|
|
176
|
+
- !ruby/object:Gem::Version
|
|
177
|
+
version: 0.1.0
|
|
150
178
|
- !ruby/object:Gem::Dependency
|
|
151
179
|
name: vcr
|
|
152
180
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -200,6 +228,7 @@ files:
|
|
|
200
228
|
- ".github/workflows/ci.yml"
|
|
201
229
|
- ".github/workflows/release.yml"
|
|
202
230
|
- ".gitignore"
|
|
231
|
+
- ".mise.toml"
|
|
203
232
|
- ".rubocop.yml"
|
|
204
233
|
- CHANGELOG.md
|
|
205
234
|
- CLAUDE.md
|
|
@@ -263,7 +292,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
263
292
|
- !ruby/object:Gem::Version
|
|
264
293
|
version: '0'
|
|
265
294
|
requirements: []
|
|
266
|
-
rubygems_version:
|
|
295
|
+
rubygems_version: 4.0.16
|
|
267
296
|
specification_version: 4
|
|
268
297
|
summary: Ruby SDK for Langfuse - Open source LLM engineering platform
|
|
269
298
|
test_files: []
|