langfuse-ruby 0.1.5 → 0.1.7
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 +5 -5
- data/.github/workflows/release.yml +10 -9
- data/.rubocop.yml +66 -0
- data/CHANGELOG.md +13 -1
- data/CLAUDE.md +100 -0
- data/Gemfile +2 -1
- data/Gemfile.lock +3 -3
- data/Makefile +73 -0
- data/README.md +116 -8
- data/Rakefile +4 -2
- data/docs/FINAL_SUMMARY.md +11 -10
- data/docs/PUBLISH_GUIDE.md +2 -2
- data/docs/README.md +3 -3
- data/docs/RELEASE_CHECKLIST.md +44 -13
- data/examples/auto_flush_control.rb +3 -2
- data/examples/basic_tracing.rb +5 -4
- data/examples/connection_config_demo.rb +1 -0
- data/examples/event_usage.rb +3 -2
- data/examples/prompt_management.rb +3 -2
- data/examples/simplified_usage.rb +126 -0
- data/examples/url_encoding_demo.rb +1 -1
- data/langfuse-ruby.gemspec +2 -1
- data/lib/langfuse/client.rb +58 -5
- data/lib/langfuse/errors.rb +2 -0
- data/lib/langfuse/evaluation.rb +14 -12
- data/lib/langfuse/event.rb +3 -3
- data/lib/langfuse/generation.rb +3 -3
- data/lib/langfuse/null_objects.rb +74 -0
- data/lib/langfuse/otel_exporter.rb +333 -0
- data/lib/langfuse/prompt.rb +2 -2
- data/lib/langfuse/span.rb +3 -3
- data/lib/langfuse/trace.rb +2 -0
- data/lib/langfuse/utils.rb +2 -0
- data/lib/langfuse/version.rb +3 -1
- data/lib/langfuse.rb +131 -3
- data/scripts/release.sh +1 -1
- data/{test_offline.rb → scripts/test_offline.rb} +4 -3
- data/scripts/verify_release.rb +5 -4
- metadata +23 -21
- data/docs/TYPE_VALIDATION_TROUBLESHOOTING.md +0 -202
- data/docs/URL_ENCODING_FIX.md +0 -164
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
|
|
6
|
+
module Langfuse
|
|
7
|
+
# Converts batched Langfuse events into OTLP/HTTP JSON (ExportTraceServiceRequest)
|
|
8
|
+
# and sends them to the Langfuse OTEL endpoint for v4-compatible ingestion.
|
|
9
|
+
class OtelExporter
|
|
10
|
+
OTEL_ENDPOINT = '/api/public/otel/v1/traces'
|
|
11
|
+
|
|
12
|
+
# @param connection [Faraday::Connection] HTTP connection to Langfuse host
|
|
13
|
+
# @param debug [Boolean] whether to print debug output
|
|
14
|
+
def initialize(connection:, debug: false)
|
|
15
|
+
@connection = connection
|
|
16
|
+
@debug = debug
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Export a batch of Langfuse events as OTLP spans.
|
|
20
|
+
# @param events [Array<Hash>] array of event hashes from the event queue
|
|
21
|
+
# @return [Faraday::Response]
|
|
22
|
+
def export(events)
|
|
23
|
+
resource_spans = build_resource_spans(events)
|
|
24
|
+
payload = { resourceSpans: resource_spans }
|
|
25
|
+
|
|
26
|
+
puts "OTEL export payload: #{JSON.pretty_generate(payload)}" if @debug
|
|
27
|
+
|
|
28
|
+
@connection.post(OTEL_ENDPOINT) do |req|
|
|
29
|
+
req.headers['Content-Type'] = 'application/json'
|
|
30
|
+
req.body = JSON.generate(payload)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Build the top-level resourceSpans array from events.
|
|
37
|
+
# Groups events by trace_id, producing one scopeSpan per trace.
|
|
38
|
+
def build_resource_spans(events)
|
|
39
|
+
grouped = group_events_by_trace(events)
|
|
40
|
+
|
|
41
|
+
scope_spans = grouped.map do |_trace_id, trace_events|
|
|
42
|
+
spans = trace_events.filter_map { |event| convert_event_to_span(event) }
|
|
43
|
+
next if spans.empty?
|
|
44
|
+
|
|
45
|
+
{ scope: { name: 'langfuse-ruby', version: Langfuse::VERSION }, spans: spans }
|
|
46
|
+
end.compact
|
|
47
|
+
|
|
48
|
+
return [] if scope_spans.empty?
|
|
49
|
+
|
|
50
|
+
[{
|
|
51
|
+
resource: {
|
|
52
|
+
attributes: [
|
|
53
|
+
{ key: 'service.name', value: { stringValue: 'langfuse-ruby' } },
|
|
54
|
+
{ key: 'telemetry.sdk.name', value: { stringValue: 'langfuse-ruby' } },
|
|
55
|
+
{ key: 'telemetry.sdk.version', value: { stringValue: Langfuse::VERSION } }
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
scopeSpans: scope_spans
|
|
59
|
+
}]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Group events by their trace ID for proper OTEL span hierarchy.
|
|
63
|
+
def group_events_by_trace(events)
|
|
64
|
+
groups = Hash.new { |h, k| h[k] = [] }
|
|
65
|
+
|
|
66
|
+
events.each do |event|
|
|
67
|
+
body = event[:body] || {}
|
|
68
|
+
trace_id = body['traceId'] || body['trace_id'] || body['id'] || 'unknown'
|
|
69
|
+
groups[trace_id] << event
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
groups
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Convert a single Langfuse event to an OTLP span hash, or nil if not convertible.
|
|
76
|
+
def convert_event_to_span(event)
|
|
77
|
+
type = event[:type]
|
|
78
|
+
body = event[:body] || {}
|
|
79
|
+
|
|
80
|
+
case type
|
|
81
|
+
when 'trace-create'
|
|
82
|
+
build_trace_span(body)
|
|
83
|
+
when 'span-create', 'span-update'
|
|
84
|
+
build_observation_span(body, 'span')
|
|
85
|
+
when 'generation-create', 'generation-update'
|
|
86
|
+
build_observation_span(body, 'generation')
|
|
87
|
+
when 'event-create'
|
|
88
|
+
build_event_span(body)
|
|
89
|
+
when 'score-create'
|
|
90
|
+
build_score_span(body)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Build a root OTEL span for a Langfuse trace.
|
|
95
|
+
def build_trace_span(body)
|
|
96
|
+
trace_id = to_otel_trace_id(body['id'])
|
|
97
|
+
span_id = to_otel_span_id(body['id'])
|
|
98
|
+
|
|
99
|
+
attributes = []
|
|
100
|
+
add_attr(attributes, 'langfuse.trace.name', body['name'])
|
|
101
|
+
add_attr(attributes, 'langfuse.user.id', body['userId'])
|
|
102
|
+
add_attr(attributes, 'langfuse.session.id', body['sessionId'])
|
|
103
|
+
add_attr(attributes, 'langfuse.release', body['release'])
|
|
104
|
+
add_attr(attributes, 'langfuse.version', body['version'])
|
|
105
|
+
add_json_attr(attributes, 'langfuse.trace.input', body['input'])
|
|
106
|
+
add_json_attr(attributes, 'langfuse.trace.output', body['output'])
|
|
107
|
+
add_json_attr(attributes, 'langfuse.trace.metadata', body['metadata'])
|
|
108
|
+
|
|
109
|
+
tags = body['tags']
|
|
110
|
+
if tags.is_a?(Array) && !tags.empty?
|
|
111
|
+
add_array_attr(attributes, 'langfuse.trace.tags', tags)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
{
|
|
115
|
+
traceId: trace_id,
|
|
116
|
+
spanId: span_id,
|
|
117
|
+
name: body['name'] || 'trace',
|
|
118
|
+
kind: 1, # SPAN_KIND_INTERNAL
|
|
119
|
+
startTimeUnixNano: to_unix_nano(body['timestamp']),
|
|
120
|
+
endTimeUnixNano: to_unix_nano(body['timestamp']),
|
|
121
|
+
attributes: attributes,
|
|
122
|
+
status: { code: 1 } # STATUS_CODE_OK
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Build an OTEL span for a Langfuse span or generation observation.
|
|
127
|
+
def build_observation_span(body, obs_type)
|
|
128
|
+
trace_id = to_otel_trace_id(body['traceId'])
|
|
129
|
+
span_id = to_otel_span_id(body['id'])
|
|
130
|
+
|
|
131
|
+
span = {
|
|
132
|
+
traceId: trace_id,
|
|
133
|
+
spanId: span_id,
|
|
134
|
+
name: body['name'] || obs_type,
|
|
135
|
+
kind: 1, # SPAN_KIND_INTERNAL
|
|
136
|
+
startTimeUnixNano: to_unix_nano(body['startTime']),
|
|
137
|
+
endTimeUnixNano: to_unix_nano(body['endTime'] || body['startTime']),
|
|
138
|
+
attributes: build_observation_attributes(body, obs_type),
|
|
139
|
+
status: { code: 1 }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Set parent span ID
|
|
143
|
+
parent_id = body['parentObservationId']
|
|
144
|
+
if parent_id
|
|
145
|
+
span[:parentSpanId] = to_otel_span_id(parent_id)
|
|
146
|
+
else
|
|
147
|
+
# Parent is the trace root span
|
|
148
|
+
span[:parentSpanId] = to_otel_span_id(body['traceId'])
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
span
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Build an OTEL span for a Langfuse event (zero-duration span).
|
|
155
|
+
def build_event_span(body)
|
|
156
|
+
trace_id = to_otel_trace_id(body['traceId'])
|
|
157
|
+
span_id = to_otel_span_id(body['id'])
|
|
158
|
+
timestamp = to_unix_nano(body['startTime'])
|
|
159
|
+
|
|
160
|
+
attributes = []
|
|
161
|
+
add_attr(attributes, 'langfuse.observation.type', 'event')
|
|
162
|
+
add_json_attr(attributes, 'langfuse.observation.input', body['input'])
|
|
163
|
+
add_json_attr(attributes, 'langfuse.observation.output', body['output'])
|
|
164
|
+
add_json_attr(attributes, 'langfuse.observation.metadata', body['metadata'])
|
|
165
|
+
add_attr(attributes, 'langfuse.observation.level', body['level'])
|
|
166
|
+
add_attr(attributes, 'langfuse.observation.status_message', body['statusMessage'])
|
|
167
|
+
|
|
168
|
+
span = {
|
|
169
|
+
traceId: trace_id,
|
|
170
|
+
spanId: span_id,
|
|
171
|
+
name: body['name'] || 'event',
|
|
172
|
+
kind: 1,
|
|
173
|
+
startTimeUnixNano: timestamp,
|
|
174
|
+
endTimeUnixNano: timestamp,
|
|
175
|
+
attributes: attributes,
|
|
176
|
+
status: { code: 1 }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
parent_id = body['parentObservationId']
|
|
180
|
+
if parent_id
|
|
181
|
+
span[:parentSpanId] = to_otel_span_id(parent_id)
|
|
182
|
+
elsif body['traceId']
|
|
183
|
+
span[:parentSpanId] = to_otel_span_id(body['traceId'])
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
span
|
|
187
|
+
end
|
|
188
|
+
|
|
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
|
+
# Build OTEL attributes for a span/generation observation.
|
|
228
|
+
def build_observation_attributes(body, obs_type)
|
|
229
|
+
attributes = []
|
|
230
|
+
effective_type = body['type'] || obs_type
|
|
231
|
+
add_attr(attributes, 'langfuse.observation.type', effective_type)
|
|
232
|
+
add_json_attr(attributes, 'langfuse.observation.input', body['input'])
|
|
233
|
+
add_json_attr(attributes, 'langfuse.observation.output', body['output'])
|
|
234
|
+
add_json_attr(attributes, 'langfuse.observation.metadata', body['metadata'])
|
|
235
|
+
add_attr(attributes, 'langfuse.observation.level', body['level'])
|
|
236
|
+
add_attr(attributes, 'langfuse.observation.status_message', body['statusMessage'])
|
|
237
|
+
|
|
238
|
+
if obs_type == 'generation'
|
|
239
|
+
add_generation_attributes(attributes, body)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
attributes
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Add generation-specific gen_ai.* attributes.
|
|
246
|
+
def add_generation_attributes(attributes, body)
|
|
247
|
+
add_attr(attributes, 'gen_ai.request.model', body['model'])
|
|
248
|
+
|
|
249
|
+
model_params = body['modelParameters']
|
|
250
|
+
if model_params.is_a?(Hash)
|
|
251
|
+
model_params.each do |key, value|
|
|
252
|
+
add_attr(attributes, "gen_ai.request.#{key}", value) unless value.nil?
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
usage = body['usage']
|
|
257
|
+
if usage.is_a?(Hash)
|
|
258
|
+
add_attr(attributes, 'gen_ai.usage.prompt_tokens', usage['promptTokens'] || usage['prompt_tokens'])
|
|
259
|
+
add_attr(attributes, 'gen_ai.usage.completion_tokens', usage['completionTokens'] || usage['completion_tokens'])
|
|
260
|
+
total = usage['totalTokens'] || usage['total_tokens']
|
|
261
|
+
add_attr(attributes, 'gen_ai.usage.total_tokens', total) if total
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
add_attr(attributes, 'langfuse.observation.completion_start_time', body['completionStartTime'])
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Convert a UUID string to OTEL 32-char hex trace ID.
|
|
268
|
+
# OTEL trace IDs are 16 bytes (32 hex chars).
|
|
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]
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Convert a UUID string to OTEL 16-char hex span ID.
|
|
277
|
+
# OTEL span IDs are 8 bytes (16 hex chars).
|
|
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]
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Convert an ISO8601 timestamp string to nanoseconds since epoch.
|
|
286
|
+
def to_unix_nano(timestamp_str)
|
|
287
|
+
return '0' unless timestamp_str
|
|
288
|
+
|
|
289
|
+
time = Time.parse(timestamp_str.to_s)
|
|
290
|
+
((time.to_f * 1_000_000_000).to_i).to_s
|
|
291
|
+
rescue ArgumentError
|
|
292
|
+
'0'
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Add a string/numeric attribute to the attributes array.
|
|
296
|
+
def add_attr(attributes, key, value)
|
|
297
|
+
return if value.nil?
|
|
298
|
+
|
|
299
|
+
otel_value = case value
|
|
300
|
+
when String
|
|
301
|
+
{ stringValue: value }
|
|
302
|
+
when Integer
|
|
303
|
+
{ intValue: value.to_s }
|
|
304
|
+
when Float
|
|
305
|
+
{ doubleValue: value }
|
|
306
|
+
when TrueClass, FalseClass
|
|
307
|
+
{ boolValue: value }
|
|
308
|
+
else
|
|
309
|
+
{ stringValue: value.to_s }
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
attributes << { key: key, value: otel_value }
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Add a JSON-serialized attribute (for complex objects like input/output).
|
|
316
|
+
def add_json_attr(attributes, key, value)
|
|
317
|
+
return if value.nil?
|
|
318
|
+
return if value.is_a?(Hash) && value.empty?
|
|
319
|
+
return if value.is_a?(Array) && value.empty?
|
|
320
|
+
|
|
321
|
+
json_str = value.is_a?(String) ? value : JSON.generate(value)
|
|
322
|
+
attributes << { key: key, value: { stringValue: json_str } }
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Add an array attribute for tags.
|
|
326
|
+
def add_array_attr(attributes, key, values)
|
|
327
|
+
return if values.nil? || values.empty?
|
|
328
|
+
|
|
329
|
+
array_values = values.map { |v| { stringValue: v.to_s } }
|
|
330
|
+
attributes << { key: key, value: { arrayValue: { values: array_values } } }
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
data/lib/langfuse/prompt.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Langfuse
|
|
2
4
|
class Prompt
|
|
3
5
|
attr_reader :id, :name, :version, :prompt, :config, :labels, :tags, :type, :created_at, :updated_at
|
|
@@ -148,8 +150,6 @@ module Langfuse
|
|
|
148
150
|
new(template: template, input_variables: variables)
|
|
149
151
|
end
|
|
150
152
|
|
|
151
|
-
private
|
|
152
|
-
|
|
153
153
|
def self.extract_variables(text)
|
|
154
154
|
variables = []
|
|
155
155
|
|
data/lib/langfuse/span.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Langfuse
|
|
2
4
|
class Span
|
|
3
5
|
attr_reader :id, :trace_id, :name, :start_time, :end_time, :input, :output,
|
|
@@ -286,9 +288,7 @@ module Langfuse
|
|
|
286
288
|
return nil if type.nil?
|
|
287
289
|
|
|
288
290
|
type_str = type.to_s
|
|
289
|
-
unless ObservationType.valid?(type_str)
|
|
290
|
-
raise ValidationError, "Invalid observation type: #{type}. Valid types are: #{ObservationType::ALL.join(', ')}"
|
|
291
|
-
end
|
|
291
|
+
raise ValidationError, "Invalid observation type: #{type}. Valid types are: #{ObservationType::ALL.join(', ')}" unless ObservationType.valid?(type_str)
|
|
292
292
|
|
|
293
293
|
type_str
|
|
294
294
|
end
|
data/lib/langfuse/trace.rb
CHANGED
data/lib/langfuse/utils.rb
CHANGED
data/lib/langfuse/version.rb
CHANGED
data/lib/langfuse.rb
CHANGED
|
@@ -11,6 +11,8 @@ require_relative 'langfuse/prompt'
|
|
|
11
11
|
require_relative 'langfuse/evaluation'
|
|
12
12
|
require_relative 'langfuse/errors'
|
|
13
13
|
require_relative 'langfuse/utils'
|
|
14
|
+
require_relative 'langfuse/null_objects'
|
|
15
|
+
require_relative 'langfuse/otel_exporter'
|
|
14
16
|
|
|
15
17
|
# Ruby SDK for Langfuse - Open source LLM engineering platform
|
|
16
18
|
module Langfuse
|
|
@@ -25,14 +27,139 @@ module Langfuse
|
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
# Create a new Langfuse client instance
|
|
28
|
-
def new(**
|
|
29
|
-
Client.new(**
|
|
30
|
+
def new(**kwargs)
|
|
31
|
+
Client.new(**kwargs)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Get a thread-safe singleton client instance
|
|
35
|
+
# @return [Client] Langfuse client
|
|
36
|
+
def client
|
|
37
|
+
Thread.current[:langfuse_client] ||= Client.new
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Get a prompt and optionally compile it with variables
|
|
41
|
+
# @param prompt_name [String] prompt name
|
|
42
|
+
# @param variables [Hash] optional variables for compilation
|
|
43
|
+
# @param label [String] optional prompt label (defaults to 'production' or 'latest')
|
|
44
|
+
# @param version [Integer] optional prompt version
|
|
45
|
+
# @param cache_ttl_seconds [Integer] cache TTL in seconds (default: 60)
|
|
46
|
+
# @param retries [Integer] number of retries on failure (default: 2)
|
|
47
|
+
# @return [String, Prompt, nil] compiled prompt string if variables provided, Prompt object otherwise, nil on failure
|
|
48
|
+
def get_prompt(prompt_name, variables: nil, label: nil, version: nil, cache_ttl_seconds: 60, retries: 2)
|
|
49
|
+
attempts = 0
|
|
50
|
+
|
|
51
|
+
begin
|
|
52
|
+
attempts += 1
|
|
53
|
+
prompt = client.get_prompt(prompt_name, label: label, version: version, cache_ttl_seconds: cache_ttl_seconds)
|
|
54
|
+
|
|
55
|
+
if variables
|
|
56
|
+
prompt.compile(variables)
|
|
57
|
+
else
|
|
58
|
+
prompt
|
|
59
|
+
end
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
if attempts <= retries
|
|
62
|
+
sleep_time = 2**(attempts - 1) * 0.1 # Exponential backoff: 0.1s, 0.2s, 0.4s...
|
|
63
|
+
warn "Langfuse prompt fetch failed (#{prompt_name}), retrying in #{sleep_time}s... (attempt #{attempts}/#{retries + 1})" if configuration.debug
|
|
64
|
+
sleep(sleep_time)
|
|
65
|
+
retry
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
warn "Langfuse prompt fetch failed (#{prompt_name}): #{e.message}" if configuration.debug
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Create a trace and optionally execute a block with it
|
|
74
|
+
# When a block is given, the trace is yielded and flush is called automatically after the block
|
|
75
|
+
# If trace creation fails, a NullTrace is yielded to ensure the block still executes
|
|
76
|
+
#
|
|
77
|
+
# @param name [String] trace name
|
|
78
|
+
# @param user_id [String] optional user identifier
|
|
79
|
+
# @param session_id [String] optional session identifier
|
|
80
|
+
# @param input [Object] optional input data
|
|
81
|
+
# @param output [Object] optional output data
|
|
82
|
+
# @param metadata [Hash] optional metadata
|
|
83
|
+
# @param tags [Array] optional tags
|
|
84
|
+
# @param version [String] optional version
|
|
85
|
+
# @param release [String] optional release
|
|
86
|
+
# @yield [Trace, NullTrace] trace object for recording observations
|
|
87
|
+
# @return [Object] block return value if block given, trace otherwise
|
|
88
|
+
#
|
|
89
|
+
# @example Block-based usage with automatic flush
|
|
90
|
+
# Langfuse.trace("my-trace", user_id: "user-1") do |trace|
|
|
91
|
+
# generation = trace.generation(name: "openai", model: "gpt-4", input: messages)
|
|
92
|
+
# response = call_openai(...)
|
|
93
|
+
# generation.end(output: response, usage: response.usage)
|
|
94
|
+
# trace.update(output: response)
|
|
95
|
+
# end
|
|
96
|
+
#
|
|
97
|
+
# @example Direct usage without block
|
|
98
|
+
# trace = Langfuse.trace("my-trace")
|
|
99
|
+
# # ... work with trace
|
|
100
|
+
# Langfuse.flush
|
|
101
|
+
#
|
|
102
|
+
def trace(name = nil, user_id: nil, session_id: nil, input: nil, output: nil,
|
|
103
|
+
metadata: nil, tags: nil, version: nil, release: nil, **kwargs, &block)
|
|
104
|
+
trace = client.trace(
|
|
105
|
+
name: name,
|
|
106
|
+
user_id: user_id,
|
|
107
|
+
session_id: session_id,
|
|
108
|
+
input: input,
|
|
109
|
+
output: output,
|
|
110
|
+
metadata: metadata,
|
|
111
|
+
tags: tags,
|
|
112
|
+
version: version,
|
|
113
|
+
release: release,
|
|
114
|
+
**kwargs
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if block_given?
|
|
118
|
+
begin
|
|
119
|
+
result = yield(trace)
|
|
120
|
+
result
|
|
121
|
+
ensure
|
|
122
|
+
flush
|
|
123
|
+
end
|
|
124
|
+
else
|
|
125
|
+
trace
|
|
126
|
+
end
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
warn "Langfuse trace creation failed: #{e.message}" if configuration.debug
|
|
129
|
+
|
|
130
|
+
# If block given, execute with NullTrace to ensure code continues
|
|
131
|
+
if block_given?
|
|
132
|
+
null_trace = NullTrace.new
|
|
133
|
+
yield(null_trace)
|
|
134
|
+
else
|
|
135
|
+
NullTrace.new
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Flush all pending events to Langfuse
|
|
140
|
+
def flush
|
|
141
|
+
client.flush
|
|
142
|
+
rescue StandardError => e
|
|
143
|
+
warn "Langfuse flush failed: #{e.message}" if configuration.debug
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Shutdown the singleton client
|
|
147
|
+
def shutdown
|
|
148
|
+
client.shutdown
|
|
149
|
+
rescue StandardError => e
|
|
150
|
+
warn "Langfuse shutdown failed: #{e.message}" if configuration.debug
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Reset the singleton client (mainly for testing)
|
|
154
|
+
def reset!
|
|
155
|
+
Thread.current[:langfuse_client] = nil
|
|
30
156
|
end
|
|
31
157
|
end
|
|
32
158
|
|
|
33
159
|
# Configuration class for Langfuse client settings
|
|
34
160
|
class Configuration
|
|
35
|
-
attr_accessor :public_key, :secret_key, :host, :debug, :timeout, :retries, :flush_interval, :auto_flush
|
|
161
|
+
attr_accessor :public_key, :secret_key, :host, :debug, :timeout, :retries, :flush_interval, :auto_flush,
|
|
162
|
+
:ingestion_mode
|
|
36
163
|
|
|
37
164
|
def initialize
|
|
38
165
|
@public_key = nil
|
|
@@ -43,6 +170,7 @@ module Langfuse
|
|
|
43
170
|
@retries = 3
|
|
44
171
|
@flush_interval = 5
|
|
45
172
|
@auto_flush = true
|
|
173
|
+
@ingestion_mode = :legacy # :legacy or :otel
|
|
46
174
|
end
|
|
47
175
|
end
|
|
48
176
|
end
|
data/scripts/release.sh
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
|
|
3
|
-
require_relative 'lib/langfuse'
|
|
4
|
+
require_relative '../lib/langfuse'
|
|
4
5
|
|
|
5
6
|
puts '🚀 Testing Langfuse Ruby SDK (Offline Mode)...'
|
|
6
7
|
|
|
@@ -223,7 +224,7 @@ begin
|
|
|
223
224
|
)
|
|
224
225
|
|
|
225
226
|
# Embedding generation
|
|
226
|
-
|
|
227
|
+
retrieval_span.generation(
|
|
227
228
|
name: 'embedding-generation',
|
|
228
229
|
model: 'text-embedding-ada-002',
|
|
229
230
|
input: 'quantum computing basics',
|
|
@@ -250,7 +251,7 @@ begin
|
|
|
250
251
|
)
|
|
251
252
|
|
|
252
253
|
# LLM generation
|
|
253
|
-
|
|
254
|
+
answer_span.generation(
|
|
254
255
|
name: 'openai-completion',
|
|
255
256
|
model: 'gpt-4',
|
|
256
257
|
input: [
|
data/scripts/verify_release.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
# Langfuse Ruby SDK Release Verification Script
|
|
4
5
|
require 'net/http'
|
|
@@ -78,10 +79,10 @@ end
|
|
|
78
79
|
|
|
79
80
|
# Run checks
|
|
80
81
|
puts "\n🔍 Checking RubyGems availability..."
|
|
81
|
-
gem_available = check_rubygems('langfuse', current_version)
|
|
82
|
+
gem_available = check_rubygems('langfuse-ruby', current_version)
|
|
82
83
|
|
|
83
84
|
puts "\n🔍 Checking version availability..."
|
|
84
|
-
version_available = check_version_availability('langfuse', current_version)
|
|
85
|
+
version_available = check_version_availability('langfuse-ruby', current_version)
|
|
85
86
|
|
|
86
87
|
# Test local installation
|
|
87
88
|
puts "\n🔍 Testing local gem functionality..."
|
|
@@ -94,7 +95,7 @@ begin
|
|
|
94
95
|
|
|
95
96
|
# Test client creation (without real credentials)
|
|
96
97
|
begin
|
|
97
|
-
|
|
98
|
+
Langfuse.new(public_key: 'test', secret_key: 'test')
|
|
98
99
|
puts '✅ Client creation successful'
|
|
99
100
|
rescue Langfuse::AuthenticationError
|
|
100
101
|
puts '✅ Authentication error expected (no real credentials)'
|
|
@@ -134,6 +135,6 @@ else
|
|
|
134
135
|
end
|
|
135
136
|
|
|
136
137
|
puts "\n🔗 Useful links:"
|
|
137
|
-
puts ' - RubyGems page: https://rubygems.org/gems/langfuse'
|
|
138
|
+
puts ' - RubyGems page: https://rubygems.org/gems/langfuse-ruby'
|
|
138
139
|
puts ' - Documentation: https://github.com/ai-firstly/langfuse-ruby'
|
|
139
140
|
puts ' - Issues: https://github.com/ai-firstly/langfuse-ruby/issues'
|