langfuse-rb 0.10.0 → 0.11.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/CHANGELOG.md +44 -1
- data/lib/langfuse/api_client.rb +175 -509
- data/lib/langfuse/app_root_tracking.rb +164 -0
- data/lib/langfuse/cache_warmer.rb +21 -13
- data/lib/langfuse/chat_prompt_client.rb +21 -4
- data/lib/langfuse/client.rb +174 -229
- data/lib/langfuse/config.rb +308 -65
- data/lib/langfuse/evaluation.rb +8 -4
- data/lib/langfuse/exit_hook.rb +77 -0
- data/lib/langfuse/fork_safety.rb +71 -0
- data/lib/langfuse/masking_exporter.rb +98 -0
- data/lib/langfuse/observations.rb +2 -1
- data/lib/langfuse/otel_attributes.rb +1 -0
- data/lib/langfuse/otel_setup.rb +32 -44
- data/lib/langfuse/otel_span_batch.rb +113 -0
- data/lib/langfuse/otel_span_masking.rb +89 -0
- data/lib/langfuse/otel_span_patch_applier.rb +97 -0
- data/lib/langfuse/pending_score_queue.rb +62 -0
- data/lib/langfuse/prompt_cache.rb +11 -0
- data/lib/langfuse/prompt_cache_coordinator.rb +288 -0
- data/lib/langfuse/prompt_cache_events.rb +31 -10
- data/lib/langfuse/prompt_variables.rb +54 -0
- data/lib/langfuse/propagation.rb +101 -34
- data/lib/langfuse/rails_cache_adapter.rb +27 -2
- data/lib/langfuse/read_api.rb +242 -0
- data/lib/langfuse/resilient_metrics_reporter.rb +60 -0
- data/lib/langfuse/score_client.rb +209 -89
- data/lib/langfuse/score_value.rb +58 -0
- data/lib/langfuse/span_processor.rb +53 -4
- data/lib/langfuse/stale_while_revalidate.rb +3 -4
- data/lib/langfuse/text_prompt_client.rb +15 -6
- data/lib/langfuse/trace_export_guard.rb +47 -0
- data/lib/langfuse/traced_execution.rb +18 -12
- data/lib/langfuse/types.rb +15 -1
- data/lib/langfuse/version.rb +1 -1
- data/lib/langfuse.rb +157 -44
- metadata +16 -2
data/lib/langfuse/api_client.rb
CHANGED
|
@@ -6,6 +6,8 @@ require "base64"
|
|
|
6
6
|
require "json"
|
|
7
7
|
require "uri"
|
|
8
8
|
require_relative "prompt_fetch_result"
|
|
9
|
+
require_relative "prompt_cache_coordinator"
|
|
10
|
+
require_relative "read_api"
|
|
9
11
|
|
|
10
12
|
module Langfuse
|
|
11
13
|
# HTTP client for Langfuse API
|
|
@@ -24,14 +26,7 @@ module Langfuse
|
|
|
24
26
|
#
|
|
25
27
|
class ApiClient # rubocop:disable Metrics/ClassLength
|
|
26
28
|
include PromptCacheEvents
|
|
27
|
-
|
|
28
|
-
# Bundles the resolved cache key with the per-call TTL override so private
|
|
29
|
-
# prompt-fetch helpers take one arg instead of four.
|
|
30
|
-
PromptFetchOptions = Struct.new(:key, :cache_ttl, keyword_init: true) do
|
|
31
|
-
def name = key.name
|
|
32
|
-
def version = key.version
|
|
33
|
-
def label = key.label
|
|
34
|
-
end
|
|
29
|
+
include ReadApi
|
|
35
30
|
|
|
36
31
|
# @return [String] Langfuse public API key
|
|
37
32
|
attr_reader :public_key
|
|
@@ -69,8 +64,12 @@ module Langfuse
|
|
|
69
64
|
@timeout = timeout
|
|
70
65
|
@logger = logger || Logger.new($stdout, level: Logger::WARN)
|
|
71
66
|
@cache = cache
|
|
72
|
-
@cache_backend_name = compute_cache_backend_name
|
|
73
67
|
setup_prompt_cache_events(cache_observer: cache_observer)
|
|
68
|
+
@prompt_cache_coordinator = PromptCacheCoordinator.new(
|
|
69
|
+
cache: cache,
|
|
70
|
+
event_emitter: self,
|
|
71
|
+
fetch_prompt: ->(name, version:, label:) { fetch_prompt_from_api(name, version: version, label: label) }
|
|
72
|
+
)
|
|
74
73
|
end
|
|
75
74
|
# rubocop:enable Metrics/ParameterLists
|
|
76
75
|
|
|
@@ -105,15 +104,7 @@ module Langfuse
|
|
|
105
104
|
# puts "#{prompt['name']} (v#{prompt['version']})"
|
|
106
105
|
# end
|
|
107
106
|
def list_prompts(page: nil, limit: nil)
|
|
108
|
-
|
|
109
|
-
params = { page: page, limit: limit }.compact
|
|
110
|
-
|
|
111
|
-
response = connection.get("/api/public/v2/prompts", params)
|
|
112
|
-
result = handle_response(response)
|
|
113
|
-
|
|
114
|
-
# API returns { data: [...], meta: {...} }
|
|
115
|
-
result["data"] || []
|
|
116
|
-
end
|
|
107
|
+
request(:get, "/api/public/v2/prompts", params: { page: page, limit: limit }.compact)["data"] || []
|
|
117
108
|
end
|
|
118
109
|
|
|
119
110
|
# Fetch a prompt from the Langfuse API
|
|
@@ -148,16 +139,7 @@ module Langfuse
|
|
|
148
139
|
# @raise [UnauthorizedError] if authentication fails
|
|
149
140
|
# @raise [ApiError] for other API errors
|
|
150
141
|
def get_prompt_result(name, version: nil, label: nil, cache_ttl: nil)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
options = PromptFetchOptions.new(
|
|
154
|
-
key: prompt_cache_key(name, version: version, label: label),
|
|
155
|
-
cache_ttl: cache_ttl
|
|
156
|
-
)
|
|
157
|
-
return fetch_uncached_prompt_result(options, CacheStatus::DISABLED) if cache.nil?
|
|
158
|
-
return fetch_uncached_prompt_result(options, CacheStatus::BYPASS) if cache_ttl&.zero?
|
|
159
|
-
|
|
160
|
-
fetch_cached_prompt_result(options)
|
|
142
|
+
@prompt_cache_coordinator.get_prompt_result(name, version: version, label: label, cache_ttl: cache_ttl)
|
|
161
143
|
end
|
|
162
144
|
|
|
163
145
|
# Refresh a prompt from the API, optionally writing through to cache.
|
|
@@ -173,14 +155,7 @@ module Langfuse
|
|
|
173
155
|
# @raise [UnauthorizedError] if authentication fails
|
|
174
156
|
# @raise [ApiError] for other API errors
|
|
175
157
|
def refresh_prompt(name, version: nil, label: nil, cache_ttl: nil)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
refresh_prompt_result(
|
|
179
|
-
PromptFetchOptions.new(
|
|
180
|
-
key: prompt_cache_key(name, version: version, label: label),
|
|
181
|
-
cache_ttl: cache_ttl
|
|
182
|
-
)
|
|
183
|
-
)
|
|
158
|
+
@prompt_cache_coordinator.refresh_prompt(name, version: version, label: label, cache_ttl: cache_ttl)
|
|
184
159
|
end
|
|
185
160
|
|
|
186
161
|
# Inspect the logical and generated cache keys for a prompt.
|
|
@@ -191,15 +166,7 @@ module Langfuse
|
|
|
191
166
|
# @return [PromptCacheKey] Logical and generated cache keys
|
|
192
167
|
# @raise [ArgumentError] if both version and label are provided
|
|
193
168
|
def prompt_cache_key(name, version: nil, label: nil)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
logical_key = PromptCache.build_key(name, version: version, label: label)
|
|
197
|
-
storage_key = if generated_storage_key_cache?
|
|
198
|
-
cache.storage_key(logical_key, name: name)
|
|
199
|
-
else
|
|
200
|
-
logical_key
|
|
201
|
-
end
|
|
202
|
-
PromptCacheKey.new(name: name, version: version, label: label, logical_key: logical_key, storage_key: storage_key)
|
|
169
|
+
@prompt_cache_coordinator.prompt_cache_key(name, version: version, label: label)
|
|
203
170
|
end
|
|
204
171
|
|
|
205
172
|
# Invalidate one exact logical prompt cache key.
|
|
@@ -210,13 +177,7 @@ module Langfuse
|
|
|
210
177
|
# @return [PromptCacheKey] The invalidated key
|
|
211
178
|
# @raise [ArgumentError] if both version and label are provided
|
|
212
179
|
def invalidate_prompt_cache(name, version: nil, label: nil)
|
|
213
|
-
|
|
214
|
-
deleted = cache&.delete(key.storage_key) || false
|
|
215
|
-
emit_prompt_cache_event(:delete) { event_payload(key, CacheStatus::MISS, CacheSource::CACHE, deleted: deleted) }
|
|
216
|
-
emit_prompt_cache_event(:invalidate) do
|
|
217
|
-
event_payload(key, CacheStatus::MISS, CacheSource::CACHE, scope: :exact)
|
|
218
|
-
end
|
|
219
|
-
key
|
|
180
|
+
@prompt_cache_coordinator.invalidate_prompt_cache(name, version: version, label: label)
|
|
220
181
|
end
|
|
221
182
|
|
|
222
183
|
# Invalidate all cached variants for one prompt name.
|
|
@@ -224,29 +185,33 @@ module Langfuse
|
|
|
224
185
|
# @param name [String] The prompt name
|
|
225
186
|
# @return [Integer, nil] New generation, or nil when cache is disabled
|
|
226
187
|
def invalidate_prompt_cache_by_name(name)
|
|
227
|
-
|
|
228
|
-
payload = { name: name, backend: cache_backend_name, generation: generation, scope: :name }
|
|
229
|
-
emit_prompt_cache_event(:invalidate, payload)
|
|
230
|
-
generation
|
|
188
|
+
@prompt_cache_coordinator.invalidate_prompt_cache_by_name(name)
|
|
231
189
|
end
|
|
232
190
|
|
|
233
191
|
# Logically clear the whole Langfuse prompt cache namespace.
|
|
234
192
|
#
|
|
235
193
|
# @return [Integer, nil] New global generation, or nil when cache is disabled
|
|
236
194
|
def clear_prompt_cache
|
|
237
|
-
|
|
238
|
-
emit_prompt_cache_event(:clear, backend: cache_backend_name, generation: generation)
|
|
239
|
-
generation
|
|
195
|
+
@prompt_cache_coordinator.clear_prompt_cache
|
|
240
196
|
end
|
|
241
197
|
|
|
242
198
|
# Return prompt cache statistics.
|
|
243
199
|
#
|
|
244
200
|
# @return [Hash] Cache statistics
|
|
245
201
|
def prompt_cache_stats
|
|
246
|
-
|
|
202
|
+
@prompt_cache_coordinator.prompt_cache_stats
|
|
203
|
+
end
|
|
247
204
|
|
|
248
|
-
|
|
205
|
+
# Validate the configured prompt cache backend.
|
|
206
|
+
#
|
|
207
|
+
# @return [Boolean] true when the configured backend is usable
|
|
208
|
+
# @raise [ConfigurationError] if the backend is invalid
|
|
209
|
+
# rubocop:disable Naming/PredicateMethod
|
|
210
|
+
def validate_prompt_cache_backend!
|
|
211
|
+
@cache&.validate!
|
|
212
|
+
true
|
|
249
213
|
end
|
|
214
|
+
# rubocop:enable Naming/PredicateMethod
|
|
250
215
|
|
|
251
216
|
# Create a new prompt (or new version if prompt with same name exists)
|
|
252
217
|
#
|
|
@@ -271,21 +236,12 @@ module Langfuse
|
|
|
271
236
|
#
|
|
272
237
|
# rubocop:disable Metrics/ParameterLists
|
|
273
238
|
def create_prompt(name:, prompt:, type:, config: {}, labels: [], tags: [], commit_message: nil)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
config: config,
|
|
281
|
-
labels: labels,
|
|
282
|
-
tags: tags
|
|
283
|
-
}
|
|
284
|
-
payload[:commitMessage] = commit_message if commit_message
|
|
285
|
-
|
|
286
|
-
response = connection.post(path, payload)
|
|
287
|
-
handle_response(response).tap { invalidate_prompt_cache_after_mutation(name) }
|
|
288
|
-
end
|
|
239
|
+
payload = {
|
|
240
|
+
name: name, prompt: prompt, type: type, config: config,
|
|
241
|
+
labels: labels, tags: tags, commitMessage: commit_message
|
|
242
|
+
}.compact
|
|
243
|
+
request(:post, "/api/public/v2/prompts", body: payload)
|
|
244
|
+
.tap { @prompt_cache_coordinator.invalidate_after_mutation(name) }
|
|
289
245
|
end
|
|
290
246
|
# rubocop:enable Metrics/ParameterLists
|
|
291
247
|
|
|
@@ -309,13 +265,9 @@ module Langfuse
|
|
|
309
265
|
def update_prompt(name:, version:, labels:)
|
|
310
266
|
raise ArgumentError, "labels must be an array" unless labels.is_a?(Array)
|
|
311
267
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
response = connection.patch(path, payload)
|
|
317
|
-
handle_response(response).tap { invalidate_prompt_cache_after_mutation(name) }
|
|
318
|
-
end
|
|
268
|
+
path = "/api/public/v2/prompts/#{URI.encode_uri_component(name)}/versions/#{version}"
|
|
269
|
+
request(:patch, path, body: { newLabels: labels })
|
|
270
|
+
.tap { @prompt_cache_coordinator.invalidate_after_mutation(name) }
|
|
319
271
|
end
|
|
320
272
|
|
|
321
273
|
# Send a batch of events to the Langfuse ingestion API
|
|
@@ -344,18 +296,28 @@ module Langfuse
|
|
|
344
296
|
raise ArgumentError, "events must be an array" unless events.is_a?(Array)
|
|
345
297
|
raise ArgumentError, "events array cannot be empty" if events.empty?
|
|
346
298
|
|
|
347
|
-
|
|
348
|
-
payload = { batch: events }
|
|
349
|
-
|
|
350
|
-
response = connection.post(path, payload)
|
|
299
|
+
response = connection.post("/api/public/ingestion", { batch: events })
|
|
351
300
|
handle_batch_response(response)
|
|
352
301
|
rescue Faraday::RetriableResponse => e
|
|
353
|
-
# Retry middleware exhausted all retries - handle the final response
|
|
354
302
|
logger.error("Langfuse batch send failed: Retries exhausted - #{e.response.status}")
|
|
355
303
|
handle_batch_response(e.response)
|
|
356
304
|
rescue Faraday::Error => e
|
|
357
305
|
logger.error("Langfuse batch send failed: #{e.message}")
|
|
358
|
-
raise
|
|
306
|
+
raise BatchDeliveryError.new("Batch send failed: #{e.message}", retryable: true)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Create a score through the synchronous Scores API.
|
|
310
|
+
#
|
|
311
|
+
# @param payload [Hash] Validated score attributes in API format
|
|
312
|
+
# @return [String] ID of the created score
|
|
313
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
314
|
+
# @raise [ApiError] if the API request fails or omits the created score ID
|
|
315
|
+
def create_score(payload:)
|
|
316
|
+
response = request(:post, "/api/public/scores", body: payload)
|
|
317
|
+
score_id = response["id"]
|
|
318
|
+
return score_id if score_id.is_a?(String) && !score_id.empty?
|
|
319
|
+
|
|
320
|
+
raise ApiError, "Score creation response did not include an id"
|
|
359
321
|
end
|
|
360
322
|
|
|
361
323
|
# Create a dataset run item (link a trace to a dataset item within a run)
|
|
@@ -374,16 +336,12 @@ module Langfuse
|
|
|
374
336
|
# api_client.create_dataset_run_item(dataset_item_id: "item-123", run_name: "eval-v1", trace_id: "trace-abc")
|
|
375
337
|
def create_dataset_run_item(dataset_item_id:, run_name:, trace_id: nil,
|
|
376
338
|
observation_id: nil, metadata: nil, run_description: nil)
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
response = connection.post("/api/public/dataset-run-items", payload)
|
|
385
|
-
handle_response(response)
|
|
386
|
-
end
|
|
339
|
+
payload = {
|
|
340
|
+
datasetItemId: dataset_item_id, runName: run_name,
|
|
341
|
+
traceId: trace_id, observationId: observation_id,
|
|
342
|
+
metadata: metadata, runDescription: run_description
|
|
343
|
+
}.compact
|
|
344
|
+
request(:post, "/api/public/dataset-run-items", body: payload)
|
|
387
345
|
end
|
|
388
346
|
|
|
389
347
|
# Fetch a dataset run by dataset and run name
|
|
@@ -395,10 +353,7 @@ module Langfuse
|
|
|
395
353
|
# @raise [UnauthorizedError] if authentication fails
|
|
396
354
|
# @raise [ApiError] for other API errors
|
|
397
355
|
def get_dataset_run(dataset_name:, run_name:)
|
|
398
|
-
|
|
399
|
-
response = connection.get(dataset_run_path(dataset_name: dataset_name, run_name: run_name))
|
|
400
|
-
handle_response(response)
|
|
401
|
-
end
|
|
356
|
+
request(:get, dataset_run_path(dataset_name: dataset_name, run_name: run_name))
|
|
402
357
|
end
|
|
403
358
|
|
|
404
359
|
# List dataset runs in a dataset
|
|
@@ -410,8 +365,7 @@ module Langfuse
|
|
|
410
365
|
# @raise [UnauthorizedError] if authentication fails
|
|
411
366
|
# @raise [ApiError] for other API errors
|
|
412
367
|
def list_dataset_runs(dataset_name:, page: nil, limit: nil)
|
|
413
|
-
|
|
414
|
-
result["data"] || []
|
|
368
|
+
list_dataset_runs_paginated(dataset_name: dataset_name, page: page, limit: limit)["data"] || []
|
|
415
369
|
end
|
|
416
370
|
|
|
417
371
|
# Full paginated response including "meta" for internal pagination use
|
|
@@ -419,10 +373,7 @@ module Langfuse
|
|
|
419
373
|
# @api private
|
|
420
374
|
# @return [Hash] Full response hash with "data" array and "meta" pagination info
|
|
421
375
|
def list_dataset_runs_paginated(dataset_name:, page: nil, limit: nil)
|
|
422
|
-
|
|
423
|
-
response = connection.get(dataset_runs_path(dataset_name), build_dataset_runs_params(page: page, limit: limit))
|
|
424
|
-
handle_response(response)
|
|
425
|
-
end
|
|
376
|
+
request(:get, dataset_runs_path(dataset_name), params: { page: page, limit: limit }.compact)
|
|
426
377
|
end
|
|
427
378
|
|
|
428
379
|
# Delete a dataset run by name
|
|
@@ -451,19 +402,16 @@ module Langfuse
|
|
|
451
402
|
# data = api_client.get_projects
|
|
452
403
|
# project_id = data["data"][0]["id"]
|
|
453
404
|
def get_projects # rubocop:disable Naming/AccessorMethodName
|
|
454
|
-
|
|
455
|
-
response = connection.get("/api/public/projects")
|
|
456
|
-
handle_response(response)
|
|
457
|
-
end
|
|
405
|
+
request(:get, "/api/public/projects")
|
|
458
406
|
end
|
|
459
407
|
|
|
460
408
|
# Shut down the API client and release resources
|
|
461
409
|
#
|
|
462
|
-
# Shuts down the cache
|
|
410
|
+
# Shuts down the cache backend's SWR thread pool when present.
|
|
463
411
|
#
|
|
464
412
|
# @return [void]
|
|
465
413
|
def shutdown
|
|
466
|
-
cache
|
|
414
|
+
@cache&.shutdown
|
|
467
415
|
end
|
|
468
416
|
|
|
469
417
|
# List traces in the project
|
|
@@ -493,14 +441,13 @@ module Langfuse
|
|
|
493
441
|
from_timestamp: nil, to_timestamp: nil, order_by: nil,
|
|
494
442
|
tags: nil, version: nil, release: nil, environment: nil,
|
|
495
443
|
fields: nil, filter: nil)
|
|
496
|
-
|
|
444
|
+
list_traces_paginated(
|
|
497
445
|
page: page, limit: limit, user_id: user_id, name: name,
|
|
498
446
|
session_id: session_id, from_timestamp: from_timestamp,
|
|
499
447
|
to_timestamp: to_timestamp, order_by: order_by, tags: tags,
|
|
500
448
|
version: version, release: release, environment: environment,
|
|
501
449
|
fields: fields, filter: filter
|
|
502
|
-
)
|
|
503
|
-
result["data"] || []
|
|
450
|
+
)["data"] || []
|
|
504
451
|
end
|
|
505
452
|
# rubocop:enable Metrics/ParameterLists
|
|
506
453
|
|
|
@@ -513,17 +460,14 @@ module Langfuse
|
|
|
513
460
|
from_timestamp: nil, to_timestamp: nil, order_by: nil,
|
|
514
461
|
tags: nil, version: nil, release: nil, environment: nil,
|
|
515
462
|
fields: nil, filter: nil)
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
response = connection.get("/api/public/traces", params)
|
|
525
|
-
handle_response(response)
|
|
526
|
-
end
|
|
463
|
+
params = build_traces_params(
|
|
464
|
+
page: page, limit: limit, user_id: user_id, name: name,
|
|
465
|
+
session_id: session_id, from_timestamp: from_timestamp,
|
|
466
|
+
to_timestamp: to_timestamp, order_by: order_by, tags: tags,
|
|
467
|
+
version: version, release: release, environment: environment,
|
|
468
|
+
fields: fields, filter: filter
|
|
469
|
+
)
|
|
470
|
+
request(:get, "/api/public/traces", params: params)
|
|
527
471
|
end
|
|
528
472
|
# rubocop:enable Metrics/ParameterLists
|
|
529
473
|
|
|
@@ -538,11 +482,7 @@ module Langfuse
|
|
|
538
482
|
# @example
|
|
539
483
|
# trace = api_client.get_trace("trace-uuid-123")
|
|
540
484
|
def get_trace(id)
|
|
541
|
-
|
|
542
|
-
encoded_id = URI.encode_uri_component(id)
|
|
543
|
-
response = connection.get("/api/public/traces/#{encoded_id}")
|
|
544
|
-
handle_response(response)
|
|
545
|
-
end
|
|
485
|
+
request(:get, "/api/public/traces/#{URI.encode_uri_component(id)}")
|
|
546
486
|
end
|
|
547
487
|
|
|
548
488
|
# List all datasets in the project
|
|
@@ -556,13 +496,7 @@ module Langfuse
|
|
|
556
496
|
# @example
|
|
557
497
|
# datasets = api_client.list_datasets(page: 1, limit: 10)
|
|
558
498
|
def list_datasets(page: nil, limit: nil)
|
|
559
|
-
|
|
560
|
-
params = { page: page, limit: limit }.compact
|
|
561
|
-
|
|
562
|
-
response = connection.get("/api/public/v2/datasets", params)
|
|
563
|
-
result = handle_response(response)
|
|
564
|
-
result["data"] || []
|
|
565
|
-
end
|
|
499
|
+
request(:get, "/api/public/v2/datasets", params: { page: page, limit: limit }.compact)["data"] || []
|
|
566
500
|
end
|
|
567
501
|
|
|
568
502
|
# Fetch a dataset by name
|
|
@@ -576,11 +510,7 @@ module Langfuse
|
|
|
576
510
|
# @example
|
|
577
511
|
# data = api_client.get_dataset("my-dataset")
|
|
578
512
|
def get_dataset(name)
|
|
579
|
-
|
|
580
|
-
encoded_name = URI.encode_uri_component(name)
|
|
581
|
-
response = connection.get("/api/public/v2/datasets/#{encoded_name}")
|
|
582
|
-
handle_response(response)
|
|
583
|
-
end
|
|
513
|
+
request(:get, "/api/public/v2/datasets/#{URI.encode_uri_component(name)}")
|
|
584
514
|
end
|
|
585
515
|
|
|
586
516
|
# Create a new dataset
|
|
@@ -595,12 +525,8 @@ module Langfuse
|
|
|
595
525
|
# @example
|
|
596
526
|
# data = api_client.create_dataset(name: "my-dataset", description: "QA evaluation set")
|
|
597
527
|
def create_dataset(name:, description: nil, metadata: nil)
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
response = connection.post("/api/public/v2/datasets", payload)
|
|
602
|
-
handle_response(response)
|
|
603
|
-
end
|
|
528
|
+
request(:post, "/api/public/v2/datasets",
|
|
529
|
+
body: { name: name, description: description, metadata: metadata }.compact)
|
|
604
530
|
end
|
|
605
531
|
|
|
606
532
|
# Create a new dataset item (or upsert if id is provided)
|
|
@@ -627,16 +553,13 @@ module Langfuse
|
|
|
627
553
|
def create_dataset_item(dataset_name:, input: nil, expected_output: nil,
|
|
628
554
|
metadata: nil, id: nil, source_trace_id: nil,
|
|
629
555
|
source_observation_id: nil, status: nil)
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
response = connection.post("/api/public/dataset-items", payload)
|
|
638
|
-
handle_response(response)
|
|
639
|
-
end
|
|
556
|
+
payload = {
|
|
557
|
+
datasetName: dataset_name, id: id, input: input,
|
|
558
|
+
expectedOutput: expected_output, metadata: metadata,
|
|
559
|
+
sourceTraceId: source_trace_id, sourceObservationId: source_observation_id,
|
|
560
|
+
status: status&.to_s&.upcase
|
|
561
|
+
}.compact
|
|
562
|
+
request(:post, "/api/public/dataset-items", body: payload)
|
|
640
563
|
end
|
|
641
564
|
# rubocop:enable Metrics/ParameterLists
|
|
642
565
|
|
|
@@ -651,11 +574,7 @@ module Langfuse
|
|
|
651
574
|
# @example
|
|
652
575
|
# data = api_client.get_dataset_item("item-uuid-123")
|
|
653
576
|
def get_dataset_item(id)
|
|
654
|
-
|
|
655
|
-
encoded_id = URI.encode_uri_component(id)
|
|
656
|
-
response = connection.get("/api/public/dataset-items/#{encoded_id}")
|
|
657
|
-
handle_response(response)
|
|
658
|
-
end
|
|
577
|
+
request(:get, "/api/public/dataset-items/#{URI.encode_uri_component(id)}")
|
|
659
578
|
end
|
|
660
579
|
|
|
661
580
|
# List items in a dataset with optional filters
|
|
@@ -671,13 +590,8 @@ module Langfuse
|
|
|
671
590
|
#
|
|
672
591
|
# @example
|
|
673
592
|
# items = api_client.list_dataset_items(dataset_name: "my-dataset", limit: 50)
|
|
674
|
-
def list_dataset_items(
|
|
675
|
-
|
|
676
|
-
result = list_dataset_items_paginated(
|
|
677
|
-
dataset_name: dataset_name, page: page, limit: limit,
|
|
678
|
-
source_trace_id: source_trace_id, source_observation_id: source_observation_id
|
|
679
|
-
)
|
|
680
|
-
result["data"] || []
|
|
593
|
+
def list_dataset_items(**)
|
|
594
|
+
list_dataset_items_paginated(**)["data"] || []
|
|
681
595
|
end
|
|
682
596
|
|
|
683
597
|
# Full paginated response including "meta" for internal pagination use
|
|
@@ -686,15 +600,11 @@ module Langfuse
|
|
|
686
600
|
# @return [Hash] Full response hash with "data" array and "meta" pagination info
|
|
687
601
|
def list_dataset_items_paginated(dataset_name:, page: nil, limit: nil,
|
|
688
602
|
source_trace_id: nil, source_observation_id: nil)
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
response = connection.get("/api/public/dataset-items", params)
|
|
696
|
-
handle_response(response)
|
|
697
|
-
end
|
|
603
|
+
params = {
|
|
604
|
+
datasetName: dataset_name, page: page, limit: limit,
|
|
605
|
+
sourceTraceId: source_trace_id, sourceObservationId: source_observation_id
|
|
606
|
+
}.compact
|
|
607
|
+
request(:get, "/api/public/dataset-items", params: params)
|
|
698
608
|
end
|
|
699
609
|
|
|
700
610
|
# Delete a dataset item by ID
|
|
@@ -708,8 +618,7 @@ module Langfuse
|
|
|
708
618
|
# @example
|
|
709
619
|
# api_client.delete_dataset_item("item-uuid-123")
|
|
710
620
|
def delete_dataset_item(id)
|
|
711
|
-
|
|
712
|
-
response = connection.delete("/api/public/dataset-items/#{encoded_id}")
|
|
621
|
+
response = connection.delete("/api/public/dataset-items/#{URI.encode_uri_component(id)}")
|
|
713
622
|
handle_delete_dataset_item_response(response, id)
|
|
714
623
|
rescue Faraday::RetriableResponse => e
|
|
715
624
|
logger.error("Faraday error: Retries exhausted - #{e.response.status}")
|
|
@@ -721,314 +630,46 @@ module Langfuse
|
|
|
721
630
|
|
|
722
631
|
private
|
|
723
632
|
|
|
724
|
-
def
|
|
725
|
-
|
|
726
|
-
return if cache_ttl.nil?
|
|
727
|
-
raise ArgumentError, "cache_ttl must be a non-negative Integer" unless cache_ttl.is_a?(Integer)
|
|
728
|
-
raise ArgumentError, "cache_ttl must be non-negative" if cache_ttl.negative?
|
|
729
|
-
end
|
|
730
|
-
|
|
731
|
-
def fetch_uncached_prompt_result(options, cache_status)
|
|
732
|
-
prompt_data = fetch_prompt_for_options(options)
|
|
733
|
-
build_prompt_result(options.key, prompt_data, cache_status, CacheSource::API)
|
|
734
|
-
end
|
|
735
|
-
|
|
736
|
-
def fetch_cached_prompt_result(options)
|
|
737
|
-
return fetch_swr_prompt_result(options) if swr_cache_available?
|
|
738
|
-
|
|
739
|
-
fetch_non_swr_prompt_result(options)
|
|
740
|
-
end
|
|
741
|
-
|
|
742
|
-
def fetch_swr_prompt_result(options)
|
|
743
|
-
unless generated_storage_key_cache?
|
|
744
|
-
prompt_data = fetch_with_swr_cache(options.key.storage_key, options.name, options.version, options.label)
|
|
745
|
-
return cache_hit_prompt_result(options.key, prompt_data)
|
|
746
|
-
end
|
|
747
|
-
|
|
748
|
-
result = fetch_swr_cached_prompt_result(options)
|
|
749
|
-
return result if result
|
|
750
|
-
|
|
751
|
-
fetch_cache_miss_prompt_result(options, swr_enabled: true, distributed_enabled: false)
|
|
752
|
-
end
|
|
753
|
-
|
|
754
|
-
def fetch_non_swr_prompt_result(options)
|
|
755
|
-
distributed_enabled = distributed_cache_available?
|
|
756
|
-
|
|
757
|
-
if !generated_storage_key_cache? && distributed_enabled
|
|
758
|
-
prompt_data = fetch_with_distributed_cache(options.key.storage_key, options.name, options.version,
|
|
759
|
-
options.label)
|
|
760
|
-
return cache_hit_prompt_result(options.key, prompt_data)
|
|
761
|
-
end
|
|
762
|
-
|
|
763
|
-
cached_data = cache.get(options.key.storage_key)
|
|
764
|
-
return cache_hit_prompt_result(options.key, cached_data) if cached_data
|
|
765
|
-
|
|
766
|
-
fetch_cache_miss_prompt_result(options, swr_enabled: false, distributed_enabled: distributed_enabled)
|
|
767
|
-
end
|
|
768
|
-
|
|
769
|
-
def fetch_swr_cached_prompt_result(options)
|
|
770
|
-
key = options.key
|
|
771
|
-
entry = cache.entry(key.storage_key) if cache.respond_to?(:entry)
|
|
772
|
-
return nil unless entry.respond_to?(:fresh?)
|
|
773
|
-
return cache_hit_prompt_result(key, entry.data) if entry.fresh?
|
|
774
|
-
return nil unless entry.stale?
|
|
775
|
-
|
|
776
|
-
emit_prompt_cache_event(:stale_serve) { event_payload(key, CacheStatus::STALE, CacheSource::CACHE) }
|
|
777
|
-
schedule_prompt_cache_refresh(options)
|
|
778
|
-
build_prompt_result(key, entry.data, CacheStatus::STALE, CacheSource::CACHE)
|
|
779
|
-
end
|
|
780
|
-
|
|
781
|
-
def cache_hit_prompt_result(key, prompt_data)
|
|
782
|
-
emit_prompt_cache_event(:hit) { event_payload(key, CacheStatus::HIT, CacheSource::CACHE) }
|
|
783
|
-
build_prompt_result(key, prompt_data, CacheStatus::HIT, CacheSource::CACHE)
|
|
784
|
-
end
|
|
785
|
-
|
|
786
|
-
def fetch_cache_miss_prompt_result(options, swr_enabled: false, distributed_enabled: nil)
|
|
787
|
-
emit_prompt_cache_event(:miss) { event_payload(options.key, CacheStatus::MISS, CacheSource::API) }
|
|
788
|
-
distributed_enabled = distributed_cache_available? if distributed_enabled.nil?
|
|
789
|
-
|
|
790
|
-
if !swr_enabled && distributed_enabled
|
|
791
|
-
fetch_cache_miss_with_lock(options)
|
|
792
|
-
else
|
|
793
|
-
fetch_cache_miss_directly(options, swr_enabled: swr_enabled)
|
|
794
|
-
end
|
|
795
|
-
end
|
|
796
|
-
|
|
797
|
-
def fetch_cache_miss_with_lock(options)
|
|
798
|
-
key = options.key
|
|
799
|
-
fetched = false
|
|
800
|
-
prompt_data = cache_fetch_with_lock(key.storage_key, options.cache_ttl) do
|
|
801
|
-
fetched = true
|
|
802
|
-
fetch_prompt_for_options(options)
|
|
803
|
-
end
|
|
804
|
-
emit_prompt_cache_event(:write) { event_payload(key, CacheStatus::MISS, CacheSource::API) } if fetched
|
|
805
|
-
status = fetched ? CacheStatus::MISS : CacheStatus::HIT
|
|
806
|
-
source = fetched ? CacheSource::API : CacheSource::CACHE
|
|
807
|
-
build_prompt_result(key, prompt_data, status, source)
|
|
808
|
-
end
|
|
809
|
-
|
|
810
|
-
def fetch_cache_miss_directly(options, swr_enabled: false)
|
|
811
|
-
prompt_data = fetch_prompt_for_options(options)
|
|
812
|
-
write_prompt_cache(options.key, prompt_data, options.cache_ttl, swr_enabled: swr_enabled)
|
|
813
|
-
build_prompt_result(options.key, prompt_data, CacheStatus::MISS, CacheSource::API)
|
|
814
|
-
end
|
|
815
|
-
|
|
816
|
-
def refresh_prompt_result(options)
|
|
817
|
-
key = options.key
|
|
818
|
-
emit_prompt_cache_event(:refresh_start) { event_payload(key, CacheStatus::REFRESH, CacheSource::API) }
|
|
819
|
-
prompt_data = fetch_prompt_for_options(options)
|
|
820
|
-
write_refresh_prompt_cache(key, prompt_data, options.cache_ttl)
|
|
821
|
-
status = refresh_cache_status(options.cache_ttl)
|
|
822
|
-
emit_prompt_cache_event(:refresh_success) { event_payload(key, status, CacheSource::API) }
|
|
823
|
-
build_prompt_result(key, prompt_data, status, CacheSource::API)
|
|
824
|
-
rescue StandardError => e
|
|
825
|
-
emit_prompt_cache_event(:refresh_failure) do
|
|
826
|
-
event_payload(key, CacheStatus::REFRESH, CacheSource::API,
|
|
827
|
-
error_class: e.class.name, error_message: e.message)
|
|
828
|
-
end
|
|
829
|
-
raise
|
|
830
|
-
end
|
|
831
|
-
|
|
832
|
-
def schedule_prompt_cache_refresh(options)
|
|
833
|
-
return unless cache.respond_to?(:refresh_async)
|
|
834
|
-
|
|
835
|
-
key = options.key
|
|
836
|
-
scheduled = cache.refresh_async(
|
|
837
|
-
key.storage_key,
|
|
838
|
-
ttl: options.cache_ttl,
|
|
839
|
-
on_success: ->(_value) { emit_refresh_success_events(key) },
|
|
840
|
-
on_failure: ->(error) { emit_refresh_failure_event(key, error) }
|
|
841
|
-
) { fetch_prompt_for_options(options) }
|
|
842
|
-
return unless scheduled
|
|
843
|
-
|
|
844
|
-
emit_prompt_cache_event(:refresh_start) { event_payload(key, CacheStatus::STALE, CacheSource::CACHE) }
|
|
845
|
-
end
|
|
846
|
-
|
|
847
|
-
def fetch_prompt_for_options(options)
|
|
848
|
-
fetch_prompt_from_api(options.name, version: options.version, label: options.label)
|
|
849
|
-
end
|
|
850
|
-
|
|
851
|
-
def emit_refresh_success_events(key)
|
|
852
|
-
emit_prompt_cache_event(:refresh_success) { event_payload(key, CacheStatus::REFRESH, CacheSource::API) }
|
|
853
|
-
emit_prompt_cache_event(:write) { event_payload(key, CacheStatus::REFRESH, CacheSource::API) }
|
|
854
|
-
end
|
|
855
|
-
|
|
856
|
-
def emit_refresh_failure_event(key, error)
|
|
857
|
-
emit_prompt_cache_event(:refresh_failure) do
|
|
858
|
-
event_payload(key, CacheStatus::STALE, CacheSource::CACHE,
|
|
859
|
-
error_class: error.class.name, error_message: error.message)
|
|
860
|
-
end
|
|
861
|
-
end
|
|
862
|
-
|
|
863
|
-
def write_refresh_prompt_cache(key, prompt_data, cache_ttl)
|
|
864
|
-
return unless cache
|
|
865
|
-
return if cache_ttl&.zero?
|
|
866
|
-
|
|
867
|
-
write_prompt_cache(key, prompt_data, cache_ttl,
|
|
868
|
-
cache_status: CacheStatus::REFRESH, swr_enabled: swr_cache_available?)
|
|
869
|
-
end
|
|
870
|
-
|
|
871
|
-
def write_prompt_cache(key, prompt_data, cache_ttl, cache_status: CacheStatus::MISS, swr_enabled: false)
|
|
872
|
-
if swr_enabled && cache.respond_to?(:write_with_stale_while_revalidate)
|
|
873
|
-
cache.write_with_stale_while_revalidate(key.storage_key, prompt_data, ttl: cache_ttl)
|
|
874
|
-
elsif cache_ttl.nil?
|
|
875
|
-
cache.set(key.storage_key, prompt_data)
|
|
876
|
-
else
|
|
877
|
-
cache.set(key.storage_key, prompt_data, ttl: cache_ttl)
|
|
878
|
-
end
|
|
879
|
-
emit_prompt_cache_event(:write) { event_payload(key, cache_status, CacheSource::API) }
|
|
880
|
-
end
|
|
881
|
-
|
|
882
|
-
def cache_fetch_with_lock(storage_key, cache_ttl, &)
|
|
883
|
-
return cache.fetch_with_lock(storage_key, &) if cache_ttl.nil?
|
|
884
|
-
|
|
885
|
-
cache.fetch_with_lock(storage_key, ttl: cache_ttl, &)
|
|
886
|
-
end
|
|
887
|
-
|
|
888
|
-
def refresh_cache_status(cache_ttl)
|
|
889
|
-
return CacheStatus::DISABLED unless cache
|
|
890
|
-
return CacheStatus::BYPASS if cache_ttl&.zero?
|
|
891
|
-
|
|
892
|
-
CacheStatus::REFRESH
|
|
893
|
-
end
|
|
894
|
-
|
|
895
|
-
def build_prompt_result(key, prompt_data, cache_status, source)
|
|
896
|
-
PromptFetchResult.new(
|
|
897
|
-
prompt: prompt_data,
|
|
898
|
-
logical_key: key.logical_key,
|
|
899
|
-
storage_key: key.storage_key,
|
|
900
|
-
cache_status: cache_status,
|
|
901
|
-
source: source,
|
|
902
|
-
name: prompt_data["name"] || key.name,
|
|
903
|
-
version: prompt_data["version"] || key.version,
|
|
904
|
-
label: key.resolved_label
|
|
905
|
-
)
|
|
906
|
-
end
|
|
907
|
-
|
|
908
|
-
attr_reader :cache_backend_name
|
|
909
|
-
|
|
910
|
-
def compute_cache_backend_name
|
|
911
|
-
return CacheBackend::DISABLED unless cache
|
|
912
|
-
return CacheBackend::RAILS if cache.is_a?(RailsCacheAdapter)
|
|
913
|
-
return CacheBackend::MEMORY if cache.is_a?(PromptCache)
|
|
914
|
-
|
|
915
|
-
cache.class.name
|
|
916
|
-
end
|
|
917
|
-
|
|
918
|
-
def disabled_prompt_cache_stats
|
|
919
|
-
{
|
|
920
|
-
backend: CacheBackend::DISABLED,
|
|
921
|
-
enabled: false,
|
|
922
|
-
current_generation_entries: nil,
|
|
923
|
-
orphaned_entries: nil,
|
|
924
|
-
total_entries: nil,
|
|
925
|
-
unsupported_counts: CacheBackend::UNSUPPORTED_COUNT_KEYS
|
|
926
|
-
}
|
|
927
|
-
end
|
|
928
|
-
|
|
929
|
-
def generated_storage_key_cache?
|
|
930
|
-
cache.is_a?(PromptCache) || cache.is_a?(RailsCacheAdapter)
|
|
931
|
-
end
|
|
932
|
-
|
|
933
|
-
def invalidate_prompt_cache_after_mutation(name)
|
|
934
|
-
generation = cache&.invalidate_name(name)
|
|
935
|
-
payload = { name: name, backend: cache_backend_name, generation: generation, scope: :name, mutation: true }
|
|
936
|
-
emit_prompt_cache_event(:invalidate, payload)
|
|
937
|
-
end
|
|
938
|
-
|
|
939
|
-
# Check if SWR cache is available
|
|
940
|
-
def swr_cache_available?
|
|
941
|
-
cache.respond_to?(:swr_enabled?) && cache.swr_enabled?
|
|
942
|
-
end
|
|
943
|
-
|
|
944
|
-
# Check if distributed cache is available
|
|
945
|
-
def distributed_cache_available?
|
|
946
|
-
cache.respond_to?(:fetch_with_lock)
|
|
633
|
+
def cache_backend_name
|
|
634
|
+
@prompt_cache_coordinator.backend_name
|
|
947
635
|
end
|
|
948
636
|
|
|
949
|
-
#
|
|
950
|
-
#
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
637
|
+
# Issue an HTTP request, raise on Faraday errors, parse the response.
|
|
638
|
+
#
|
|
639
|
+
# @api private
|
|
640
|
+
# @param verb [Symbol] HTTP verb (:get, :post, :patch, :delete)
|
|
641
|
+
# @param path [String] Request path
|
|
642
|
+
# @param params [Hash, nil] Query string params (GET/DELETE)
|
|
643
|
+
# @param body [Hash, nil] JSON body (POST/PATCH)
|
|
644
|
+
# @param params_encoder [Object, nil] Faraday query encoder for this request
|
|
645
|
+
# @return [Hash] Parsed response body
|
|
646
|
+
def request(verb, path, params: nil, body: nil, params_encoder: nil)
|
|
647
|
+
with_faraday_error_handling do
|
|
648
|
+
response = connection.public_send(verb, path, body || params) do |faraday_request|
|
|
649
|
+
faraday_request.options.params_encoder = params_encoder if params_encoder
|
|
650
|
+
end
|
|
651
|
+
handle_response(response)
|
|
957
652
|
end
|
|
958
653
|
end
|
|
959
|
-
# rubocop:enable Metrics/ParameterLists
|
|
960
|
-
|
|
961
|
-
def add_optional_dataset_item_fields(payload, input, expected_output, metadata, id)
|
|
962
|
-
payload[:id] = id if id
|
|
963
|
-
payload[:input] = input if input
|
|
964
|
-
payload[:expectedOutput] = expected_output if expected_output
|
|
965
|
-
payload[:metadata] = metadata if metadata
|
|
966
|
-
end
|
|
967
|
-
|
|
968
|
-
def add_optional_source_fields(payload, source_trace_id, source_observation_id, status)
|
|
969
|
-
payload[:sourceTraceId] = source_trace_id if source_trace_id
|
|
970
|
-
payload[:sourceObservationId] = source_observation_id if source_observation_id
|
|
971
|
-
payload[:status] = status.to_s.upcase if status
|
|
972
|
-
end
|
|
973
654
|
|
|
974
|
-
|
|
975
|
-
def build_dataset_items_params(dataset_name:, page:, limit:,
|
|
976
|
-
source_trace_id:, source_observation_id:)
|
|
655
|
+
def build_traces_params(**options)
|
|
977
656
|
{
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
657
|
+
page: options[:page], limit: options[:limit], userId: options[:user_id], name: options[:name],
|
|
658
|
+
sessionId: options[:session_id],
|
|
659
|
+
fromTimestamp: options[:from_timestamp]&.iso8601,
|
|
660
|
+
toTimestamp: options[:to_timestamp]&.iso8601,
|
|
661
|
+
orderBy: options[:order_by], tags: options[:tags], version: options[:version],
|
|
662
|
+
release: options[:release], environment: options[:environment], fields: options[:fields],
|
|
663
|
+
filter: options[:filter]
|
|
983
664
|
}.compact
|
|
984
665
|
end
|
|
985
666
|
|
|
986
|
-
# Build params for list_dataset_runs
|
|
987
|
-
def build_dataset_runs_params(page:, limit:)
|
|
988
|
-
{ page: page, limit: limit }.compact
|
|
989
|
-
end
|
|
990
|
-
|
|
991
|
-
# Build endpoint path for dataset runs
|
|
992
667
|
def dataset_runs_path(dataset_name)
|
|
993
|
-
|
|
994
|
-
"/api/public/datasets/#{encoded_name}/runs"
|
|
668
|
+
"/api/public/datasets/#{URI.encode_uri_component(dataset_name)}/runs"
|
|
995
669
|
end
|
|
996
670
|
|
|
997
|
-
# Build endpoint path for a specific dataset run
|
|
998
671
|
def dataset_run_path(dataset_name:, run_name:)
|
|
999
|
-
|
|
1000
|
-
"#{dataset_runs_path(dataset_name)}/#{encoded_run_name}"
|
|
1001
|
-
end
|
|
1002
|
-
|
|
1003
|
-
# Build query params for list_traces, mapping snake_case to camelCase
|
|
1004
|
-
# rubocop:disable Metrics/ParameterLists
|
|
1005
|
-
def build_traces_params(page:, limit:, user_id:, name:, session_id:,
|
|
1006
|
-
from_timestamp:, to_timestamp:, order_by:,
|
|
1007
|
-
tags:, version:, release:, environment:, fields:, filter:)
|
|
1008
|
-
{
|
|
1009
|
-
page: page, limit: limit, userId: user_id, name: name,
|
|
1010
|
-
sessionId: session_id,
|
|
1011
|
-
fromTimestamp: from_timestamp&.iso8601,
|
|
1012
|
-
toTimestamp: to_timestamp&.iso8601,
|
|
1013
|
-
orderBy: order_by, tags: tags, version: version,
|
|
1014
|
-
release: release, environment: environment, fields: fields,
|
|
1015
|
-
filter: filter
|
|
1016
|
-
}.compact
|
|
1017
|
-
end
|
|
1018
|
-
# rubocop:enable Metrics/ParameterLists
|
|
1019
|
-
|
|
1020
|
-
# Fetch with SWR cache
|
|
1021
|
-
def fetch_with_swr_cache(cache_key, name, version, label)
|
|
1022
|
-
cache.fetch_with_stale_while_revalidate(cache_key) do
|
|
1023
|
-
fetch_prompt_from_api(name, version: version, label: label)
|
|
1024
|
-
end
|
|
1025
|
-
end
|
|
1026
|
-
|
|
1027
|
-
# Fetch with distributed cache (Rails.cache with stampede protection)
|
|
1028
|
-
def fetch_with_distributed_cache(cache_key, name, version, label)
|
|
1029
|
-
cache.fetch_with_lock(cache_key) do
|
|
1030
|
-
fetch_prompt_from_api(name, version: version, label: label)
|
|
1031
|
-
end
|
|
672
|
+
"#{dataset_runs_path(dataset_name)}/#{URI.encode_uri_component(run_name)}"
|
|
1032
673
|
end
|
|
1033
674
|
|
|
1034
675
|
# Fetch a prompt from the API (without caching)
|
|
@@ -1041,13 +682,8 @@ module Langfuse
|
|
|
1041
682
|
# @raise [UnauthorizedError] if authentication fails
|
|
1042
683
|
# @raise [ApiError] for other API errors
|
|
1043
684
|
def fetch_prompt_from_api(name, version: nil, label: nil)
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
path = "/api/public/v2/prompts/#{URI.encode_uri_component(name)}"
|
|
1047
|
-
|
|
1048
|
-
response = connection.get(path, params)
|
|
1049
|
-
handle_response(response)
|
|
1050
|
-
end
|
|
685
|
+
path = "/api/public/v2/prompts/#{URI.encode_uri_component(name)}"
|
|
686
|
+
request(:get, path, params: { version: version, label: label }.compact)
|
|
1051
687
|
end
|
|
1052
688
|
|
|
1053
689
|
# Build a new Faraday connection
|
|
@@ -1073,7 +709,7 @@ module Langfuse
|
|
|
1073
709
|
# - Max 2 retries (3 total attempts)
|
|
1074
710
|
# - Exponential backoff (0.05s * 2^retry_count)
|
|
1075
711
|
# - Retries GET, PATCH, and DELETE requests (idempotent operations)
|
|
1076
|
-
# - Retries
|
|
712
|
+
# - Retries ingestion batches and score creation (both include stable IDs)
|
|
1077
713
|
# - Note: POST to create_prompt is NOT idempotent; retries may create duplicate versions
|
|
1078
714
|
# - Retries on: 429 (rate limit), 503 (service unavailable), 504 (gateway timeout)
|
|
1079
715
|
# - Does NOT retry on: 4xx errors (except 429), 5xx errors (except 503, 504)
|
|
@@ -1116,15 +752,6 @@ module Langfuse
|
|
|
1116
752
|
"langfuse-rb/#{Langfuse::VERSION}"
|
|
1117
753
|
end
|
|
1118
754
|
|
|
1119
|
-
# Build query parameters for prompt request
|
|
1120
|
-
#
|
|
1121
|
-
# @param version [Integer, nil] Optional version number
|
|
1122
|
-
# @param label [String, nil] Optional label
|
|
1123
|
-
# @return [Hash] Query parameters
|
|
1124
|
-
def build_prompt_params(version: nil, label: nil)
|
|
1125
|
-
{ version: version, label: label }.compact
|
|
1126
|
-
end
|
|
1127
|
-
|
|
1128
755
|
# Wrap a block with standard Faraday error handling.
|
|
1129
756
|
#
|
|
1130
757
|
# Catches RetriableResponse (retries exhausted) and generic Faraday errors,
|
|
@@ -1173,38 +800,77 @@ module Langfuse
|
|
|
1173
800
|
|
|
1174
801
|
# Handle HTTP response for batch requests
|
|
1175
802
|
#
|
|
803
|
+
# Per-event input errors can arrive with HTTP 207 instead of a 4xx response.
|
|
804
|
+
# The `errors` array reports rejected events, so HTTP status alone cannot
|
|
805
|
+
# identify a batch with rejected events. An empty array confirms only that
|
|
806
|
+
# the response reported no rejection; it does not prove downstream
|
|
807
|
+
# processing or immediate read visibility.
|
|
808
|
+
#
|
|
1176
809
|
# @param response [Faraday::Response] The HTTP response
|
|
1177
810
|
# @return [void]
|
|
1178
811
|
# @raise [UnauthorizedError] if status is 401
|
|
1179
|
-
# @raise [ApiError] for other error statuses
|
|
812
|
+
# @raise [ApiError] for other error statuses, or if the body lists rejected events
|
|
1180
813
|
def handle_batch_response(response)
|
|
1181
814
|
case response.status
|
|
1182
815
|
when 200, 201, 204, 207
|
|
1183
|
-
|
|
816
|
+
raise_on_batch_errors(response)
|
|
1184
817
|
when 401
|
|
1185
818
|
raise UnauthorizedError, "Authentication failed. Check your API keys."
|
|
1186
819
|
else
|
|
1187
820
|
error_message = extract_error_message(response)
|
|
1188
|
-
raise
|
|
821
|
+
raise BatchDeliveryError.new(
|
|
822
|
+
"Batch send failed (#{response.status}): #{error_message}",
|
|
823
|
+
retryable: retryable_batch_status?(response.status)
|
|
824
|
+
)
|
|
1189
825
|
end
|
|
1190
826
|
end
|
|
1191
827
|
|
|
828
|
+
# @param response [Faraday::Response] The HTTP response
|
|
829
|
+
# @return [void]
|
|
830
|
+
# @raise [ApiError] if the response body's `errors` array is non-empty
|
|
831
|
+
def raise_on_batch_errors(response)
|
|
832
|
+
errors = Array(parse_response_body(response)["errors"])
|
|
833
|
+
return if errors.empty?
|
|
834
|
+
|
|
835
|
+
messages = errors.filter_map { |e| e["message"] || e["error"] }
|
|
836
|
+
summary = messages.empty? ? "#{errors.size} event(s) rejected" : messages.join("; ")
|
|
837
|
+
raise BatchDeliveryError.new(
|
|
838
|
+
"Batch send failed: #{summary}",
|
|
839
|
+
retryable: retryable_batch_errors?(errors)
|
|
840
|
+
)
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
def retryable_batch_errors?(errors)
|
|
844
|
+
statuses = errors.filter_map { |error| Integer(error["status"], exception: false) }
|
|
845
|
+
statuses.length == errors.length && statuses.all? { |status| retryable_batch_status?(status) }
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
def retryable_batch_status?(status)
|
|
849
|
+
status == 429 || status >= 500
|
|
850
|
+
end
|
|
851
|
+
|
|
1192
852
|
# Extract error message from response body
|
|
1193
853
|
#
|
|
1194
854
|
# @param response [Faraday::Response] The HTTP response
|
|
1195
855
|
# @return [String] The error message
|
|
1196
856
|
def extract_error_message(response)
|
|
1197
|
-
body_hash =
|
|
1198
|
-
in Hash => h then h
|
|
1199
|
-
in String => s then begin
|
|
1200
|
-
JSON.parse(s)
|
|
1201
|
-
rescue StandardError
|
|
1202
|
-
{}
|
|
1203
|
-
end
|
|
1204
|
-
else {}
|
|
1205
|
-
end
|
|
857
|
+
body_hash = parse_response_body(response)
|
|
1206
858
|
|
|
1207
859
|
%w[message error].filter_map { |key| body_hash[key] }.first || "Unknown error"
|
|
1208
860
|
end
|
|
861
|
+
|
|
862
|
+
# @param response [Faraday::Response] The HTTP response
|
|
863
|
+
# @return [Hash] The parsed JSON body, or {} if absent/unparsable
|
|
864
|
+
def parse_response_body(response)
|
|
865
|
+
case response.body
|
|
866
|
+
in Hash => h then h
|
|
867
|
+
in String => s then begin
|
|
868
|
+
JSON.parse(s)
|
|
869
|
+
rescue StandardError
|
|
870
|
+
{}
|
|
871
|
+
end
|
|
872
|
+
else {}
|
|
873
|
+
end
|
|
874
|
+
end
|
|
1209
875
|
end
|
|
1210
876
|
end
|