langfuse-rb 0.3.0 → 0.4.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 +16 -1
- data/lib/langfuse/api_client.rb +343 -63
- data/lib/langfuse/cache_warmer.rb +7 -6
- data/lib/langfuse/chat_prompt_client.rb +17 -1
- data/lib/langfuse/client.rb +293 -5
- data/lib/langfuse/config.rb +25 -2
- data/lib/langfuse/dataset_client.rb +113 -0
- data/lib/langfuse/dataset_item_client.rb +149 -0
- data/lib/langfuse/evaluation.rb +73 -0
- data/lib/langfuse/experiment_item.rb +12 -0
- data/lib/langfuse/experiment_result.rb +167 -0
- data/lib/langfuse/experiment_runner.rb +245 -0
- data/lib/langfuse/item_result.rb +47 -0
- data/lib/langfuse/observations.rb +74 -7
- data/lib/langfuse/otel_setup.rb +1 -0
- data/lib/langfuse/prompt_cache.rb +11 -1
- data/lib/langfuse/propagation.rb +2 -0
- data/lib/langfuse/rails_cache_adapter.rb +17 -1
- data/lib/langfuse/score_client.rb +18 -4
- data/lib/langfuse/text_prompt_client.rb +17 -1
- data/lib/langfuse/timestamp_parser.rb +18 -0
- data/lib/langfuse/traced_execution.rb +43 -0
- data/lib/langfuse/version.rb +1 -1
- data/lib/langfuse.rb +24 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e356e95702ea99ed80bfc258a3a1dbfa6c71985bb8a31110f26c09e6ac8c8b8d
|
|
4
|
+
data.tar.gz: 742033eb63885f9a1b33377258c423a25c0fe6af8554965de8fd76b1807d3a93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d26c930cf723cbdbd5fdd738fcbae8b2181aca3f340eb0929c3ab2d76720aa3b1f217fe8f71804607f0cac32a3b115b7a625e02cf982d30ceceb1c0c45c0d603
|
|
7
|
+
data.tar.gz: a4667dc7d5abe11b7facd78c26e406a394696f9d0ca6663a200ad0232adcd7bd45d7d4bf495b2f98c7a7ed23b5f7b62ab7c2ba7b76a606b546859072b98cedec
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-02-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Dataset and dataset item management support (#40)
|
|
14
|
+
- Experiment runner for evaluating datasets (#41)
|
|
15
|
+
- Project-scoped URL generation for traces, observations, datasets, and experiments (#43)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Extracted duplicated Faraday rescue pattern in API client (#44)
|
|
19
|
+
|
|
20
|
+
### Documentation
|
|
21
|
+
- YARD documentation for all public methods (#42)
|
|
22
|
+
- Dataset and experiment usage guides (#45)
|
|
23
|
+
|
|
10
24
|
## [0.3.0] - 2026-01-23
|
|
11
25
|
|
|
12
26
|
### Added
|
|
@@ -40,7 +54,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
40
54
|
- Migrated from legacy ingestion API to OTLP endpoint
|
|
41
55
|
- Removed `tracing_enabled` configuration flag (#2)
|
|
42
56
|
|
|
43
|
-
[Unreleased]: https://github.com/simplepractice/langfuse-rb/compare/v0.
|
|
57
|
+
[Unreleased]: https://github.com/simplepractice/langfuse-rb/compare/v0.4.0...HEAD
|
|
58
|
+
[0.4.0]: https://github.com/simplepractice/langfuse-rb/compare/v0.3.0...v0.4.0
|
|
44
59
|
[0.3.0]: https://github.com/simplepractice/langfuse-rb/compare/v0.2.0...v0.3.0
|
|
45
60
|
[0.2.0]: https://github.com/simplepractice/langfuse-rb/compare/v0.1.0...v0.2.0
|
|
46
61
|
[0.1.0]: https://github.com/simplepractice/langfuse-rb/releases/tag/v0.1.0
|
data/lib/langfuse/api_client.rb
CHANGED
|
@@ -22,7 +22,23 @@ module Langfuse
|
|
|
22
22
|
# )
|
|
23
23
|
#
|
|
24
24
|
class ApiClient # rubocop:disable Metrics/ClassLength
|
|
25
|
-
|
|
25
|
+
# @return [String] Langfuse public API key
|
|
26
|
+
attr_reader :public_key
|
|
27
|
+
|
|
28
|
+
# @return [String] Langfuse secret API key
|
|
29
|
+
attr_reader :secret_key
|
|
30
|
+
|
|
31
|
+
# @return [String] Base URL for Langfuse API
|
|
32
|
+
attr_reader :base_url
|
|
33
|
+
|
|
34
|
+
# @return [Integer] HTTP request timeout in seconds
|
|
35
|
+
attr_reader :timeout
|
|
36
|
+
|
|
37
|
+
# @return [Logger] Logger instance for debugging
|
|
38
|
+
attr_reader :logger
|
|
39
|
+
|
|
40
|
+
# @return [PromptCache, RailsCacheAdapter, nil] Optional cache for prompt responses
|
|
41
|
+
attr_reader :cache
|
|
26
42
|
|
|
27
43
|
# Initialize a new API client
|
|
28
44
|
#
|
|
@@ -31,7 +47,8 @@ module Langfuse
|
|
|
31
47
|
# @param base_url [String] Base URL for Langfuse API
|
|
32
48
|
# @param timeout [Integer] HTTP request timeout in seconds
|
|
33
49
|
# @param logger [Logger] Logger instance for debugging
|
|
34
|
-
# @param cache [PromptCache, nil] Optional cache for prompt responses
|
|
50
|
+
# @param cache [PromptCache, RailsCacheAdapter, nil] Optional cache for prompt responses
|
|
51
|
+
# @return [ApiClient]
|
|
35
52
|
def initialize(public_key:, secret_key:, base_url:, timeout: 5, logger: nil, cache: nil)
|
|
36
53
|
@public_key = public_key
|
|
37
54
|
@secret_key = secret_key
|
|
@@ -72,22 +89,15 @@ module Langfuse
|
|
|
72
89
|
# puts "#{prompt['name']} (v#{prompt['version']})"
|
|
73
90
|
# end
|
|
74
91
|
def list_prompts(page: nil, limit: nil)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
params[:limit] = limit if limit
|
|
92
|
+
with_faraday_error_handling do
|
|
93
|
+
params = { page: page, limit: limit }.compact
|
|
78
94
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
result = handle_response(response)
|
|
95
|
+
response = connection.get("/api/public/v2/prompts", params)
|
|
96
|
+
result = handle_response(response)
|
|
82
97
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
logger.error("Faraday error: Retries exhausted - #{e.response.status}")
|
|
87
|
-
handle_response(e.response)
|
|
88
|
-
rescue Faraday::Error => e
|
|
89
|
-
logger.error("Faraday error: #{e.message}")
|
|
90
|
-
raise ApiError, "HTTP request failed: #{e.message}"
|
|
98
|
+
# API returns { data: [...], meta: {...} }
|
|
99
|
+
result["data"] || []
|
|
100
|
+
end
|
|
91
101
|
end
|
|
92
102
|
|
|
93
103
|
# Fetch a prompt from the Langfuse API
|
|
@@ -135,25 +145,21 @@ module Langfuse
|
|
|
135
145
|
#
|
|
136
146
|
# rubocop:disable Metrics/ParameterLists
|
|
137
147
|
def create_prompt(name:, prompt:, type:, config: {}, labels: [], tags: [], commit_message: nil)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
handle_response(e.response)
|
|
154
|
-
rescue Faraday::Error => e
|
|
155
|
-
logger.error("Faraday error: #{e.message}")
|
|
156
|
-
raise ApiError, "HTTP request failed: #{e.message}"
|
|
148
|
+
with_faraday_error_handling do
|
|
149
|
+
path = "/api/public/v2/prompts"
|
|
150
|
+
payload = {
|
|
151
|
+
name: name,
|
|
152
|
+
prompt: prompt,
|
|
153
|
+
type: type,
|
|
154
|
+
config: config,
|
|
155
|
+
labels: labels,
|
|
156
|
+
tags: tags
|
|
157
|
+
}
|
|
158
|
+
payload[:commitMessage] = commit_message if commit_message
|
|
159
|
+
|
|
160
|
+
response = connection.post(path, payload)
|
|
161
|
+
handle_response(response)
|
|
162
|
+
end
|
|
157
163
|
end
|
|
158
164
|
# rubocop:enable Metrics/ParameterLists
|
|
159
165
|
|
|
@@ -177,17 +183,13 @@ module Langfuse
|
|
|
177
183
|
def update_prompt(name:, version:, labels:)
|
|
178
184
|
raise ArgumentError, "labels must be an array" unless labels.is_a?(Array)
|
|
179
185
|
|
|
180
|
-
|
|
181
|
-
|
|
186
|
+
with_faraday_error_handling do
|
|
187
|
+
path = "/api/public/v2/prompts/#{URI.encode_uri_component(name)}/versions/#{version}"
|
|
188
|
+
payload = { newLabels: labels }
|
|
182
189
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
logger.error("Faraday error: Retries exhausted - #{e.response.status}")
|
|
187
|
-
handle_response(e.response)
|
|
188
|
-
rescue Faraday::Error => e
|
|
189
|
-
logger.error("Faraday error: #{e.message}")
|
|
190
|
-
raise ApiError, "HTTP request failed: #{e.message}"
|
|
190
|
+
response = connection.patch(path, payload)
|
|
191
|
+
handle_response(response)
|
|
192
|
+
end
|
|
191
193
|
end
|
|
192
194
|
|
|
193
195
|
# Send a batch of events to the Langfuse ingestion API
|
|
@@ -198,6 +200,7 @@ module Langfuse
|
|
|
198
200
|
#
|
|
199
201
|
# @param events [Array<Hash>] Array of event hashes to send
|
|
200
202
|
# @return [void]
|
|
203
|
+
# @raise [ArgumentError] if events is not an Array or is empty
|
|
201
204
|
# @raise [UnauthorizedError] if authentication fails
|
|
202
205
|
# @raise [ApiError] for other API errors after retries exhausted
|
|
203
206
|
#
|
|
@@ -229,10 +232,233 @@ module Langfuse
|
|
|
229
232
|
raise ApiError, "Batch send failed: #{e.message}"
|
|
230
233
|
end
|
|
231
234
|
|
|
235
|
+
# Create a dataset run item (link a trace to a dataset item within a run)
|
|
236
|
+
#
|
|
237
|
+
# @param dataset_item_id [String] Dataset item ID (required)
|
|
238
|
+
# @param run_name [String] Run name (required)
|
|
239
|
+
# @param trace_id [String, nil] Trace ID to link
|
|
240
|
+
# @param observation_id [String, nil] Observation ID to link
|
|
241
|
+
# @param metadata [Hash, nil] Optional metadata
|
|
242
|
+
# @param run_description [String, nil] Optional run description
|
|
243
|
+
# @return [Hash] The created dataset run item data
|
|
244
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
245
|
+
# @raise [ApiError] for other API errors
|
|
246
|
+
#
|
|
247
|
+
# @example
|
|
248
|
+
# api_client.create_dataset_run_item(dataset_item_id: "item-123", run_name: "eval-v1", trace_id: "trace-abc")
|
|
249
|
+
def create_dataset_run_item(dataset_item_id:, run_name:, trace_id: nil,
|
|
250
|
+
observation_id: nil, metadata: nil, run_description: nil)
|
|
251
|
+
with_faraday_error_handling do
|
|
252
|
+
payload = { datasetItemId: dataset_item_id, runName: run_name }
|
|
253
|
+
payload[:traceId] = trace_id if trace_id
|
|
254
|
+
payload[:observationId] = observation_id if observation_id
|
|
255
|
+
payload[:metadata] = metadata if metadata
|
|
256
|
+
payload[:runDescription] = run_description if run_description
|
|
257
|
+
|
|
258
|
+
response = connection.post("/api/public/dataset-run-items", payload)
|
|
259
|
+
handle_response(response)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Fetch projects accessible with the current API keys
|
|
264
|
+
#
|
|
265
|
+
# @return [Hash] The parsed response body containing project data
|
|
266
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
267
|
+
# @raise [ApiError] for other API errors
|
|
268
|
+
#
|
|
269
|
+
# @example
|
|
270
|
+
# data = api_client.get_projects
|
|
271
|
+
# project_id = data["data"][0]["id"]
|
|
272
|
+
def get_projects # rubocop:disable Naming/AccessorMethodName
|
|
273
|
+
with_faraday_error_handling do
|
|
274
|
+
response = connection.get("/api/public/projects")
|
|
275
|
+
handle_response(response)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Shut down the API client and release resources
|
|
280
|
+
#
|
|
281
|
+
# Shuts down the cache if it supports shutdown (e.g., SWR thread pool).
|
|
282
|
+
#
|
|
283
|
+
# @return [void]
|
|
232
284
|
def shutdown
|
|
233
285
|
cache.shutdown if cache.respond_to?(:shutdown)
|
|
234
286
|
end
|
|
235
287
|
|
|
288
|
+
# List all datasets in the project
|
|
289
|
+
#
|
|
290
|
+
# @param page [Integer, nil] Optional page number for pagination
|
|
291
|
+
# @param limit [Integer, nil] Optional limit per page
|
|
292
|
+
# @return [Array<Hash>] Array of dataset metadata hashes
|
|
293
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
294
|
+
# @raise [ApiError] for other API errors
|
|
295
|
+
#
|
|
296
|
+
# @example
|
|
297
|
+
# datasets = api_client.list_datasets(page: 1, limit: 10)
|
|
298
|
+
def list_datasets(page: nil, limit: nil)
|
|
299
|
+
with_faraday_error_handling do
|
|
300
|
+
params = { page: page, limit: limit }.compact
|
|
301
|
+
|
|
302
|
+
response = connection.get("/api/public/v2/datasets", params)
|
|
303
|
+
result = handle_response(response)
|
|
304
|
+
result["data"] || []
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Fetch a dataset by name
|
|
309
|
+
#
|
|
310
|
+
# @param name [String] Dataset name (supports folder paths like "evaluation/qa-dataset")
|
|
311
|
+
# @return [Hash] The dataset data
|
|
312
|
+
# @raise [NotFoundError] if the dataset is not found
|
|
313
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
314
|
+
# @raise [ApiError] for other API errors
|
|
315
|
+
#
|
|
316
|
+
# @example
|
|
317
|
+
# data = api_client.get_dataset("my-dataset")
|
|
318
|
+
def get_dataset(name)
|
|
319
|
+
with_faraday_error_handling do
|
|
320
|
+
encoded_name = URI.encode_uri_component(name)
|
|
321
|
+
response = connection.get("/api/public/v2/datasets/#{encoded_name}")
|
|
322
|
+
handle_response(response)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Create a new dataset
|
|
327
|
+
#
|
|
328
|
+
# @param name [String] Dataset name (required)
|
|
329
|
+
# @param description [String, nil] Optional description
|
|
330
|
+
# @param metadata [Hash, nil] Optional metadata hash
|
|
331
|
+
# @return [Hash] The created dataset data
|
|
332
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
333
|
+
# @raise [ApiError] for other API errors
|
|
334
|
+
#
|
|
335
|
+
# @example
|
|
336
|
+
# data = api_client.create_dataset(name: "my-dataset", description: "QA evaluation set")
|
|
337
|
+
def create_dataset(name:, description: nil, metadata: nil)
|
|
338
|
+
with_faraday_error_handling do
|
|
339
|
+
payload = { name: name, description: description, metadata: metadata }.compact
|
|
340
|
+
|
|
341
|
+
response = connection.post("/api/public/v2/datasets", payload)
|
|
342
|
+
handle_response(response)
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Create a new dataset item (or upsert if id is provided)
|
|
347
|
+
#
|
|
348
|
+
# @param dataset_name [String] Name of the dataset (required)
|
|
349
|
+
# @param input [Object, nil] Input data for the item
|
|
350
|
+
# @param expected_output [Object, nil] Expected output for evaluation
|
|
351
|
+
# @param metadata [Hash, nil] Optional metadata
|
|
352
|
+
# @param id [String, nil] Optional ID for upsert behavior
|
|
353
|
+
# @param source_trace_id [String, nil] Link to source trace
|
|
354
|
+
# @param source_observation_id [String, nil] Link to source observation
|
|
355
|
+
# @param status [Symbol, nil] Item status (:active or :archived)
|
|
356
|
+
# @return [Hash] The created dataset item data
|
|
357
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
358
|
+
# @raise [ApiError] for other API errors
|
|
359
|
+
#
|
|
360
|
+
# @example
|
|
361
|
+
# data = api_client.create_dataset_item(
|
|
362
|
+
# dataset_name: "my-dataset",
|
|
363
|
+
# input: { query: "What is Ruby?" },
|
|
364
|
+
# expected_output: { answer: "A programming language" }
|
|
365
|
+
# )
|
|
366
|
+
# rubocop:disable Metrics/ParameterLists
|
|
367
|
+
def create_dataset_item(dataset_name:, input: nil, expected_output: nil,
|
|
368
|
+
metadata: nil, id: nil, source_trace_id: nil,
|
|
369
|
+
source_observation_id: nil, status: nil)
|
|
370
|
+
with_faraday_error_handling do
|
|
371
|
+
payload = build_dataset_item_payload(
|
|
372
|
+
dataset_name: dataset_name, input: input, expected_output: expected_output,
|
|
373
|
+
metadata: metadata, id: id, source_trace_id: source_trace_id,
|
|
374
|
+
source_observation_id: source_observation_id, status: status
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
response = connection.post("/api/public/dataset-items", payload)
|
|
378
|
+
handle_response(response)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
# rubocop:enable Metrics/ParameterLists
|
|
382
|
+
|
|
383
|
+
# Fetch a dataset item by ID
|
|
384
|
+
#
|
|
385
|
+
# @param id [String] Dataset item ID
|
|
386
|
+
# @return [Hash] The dataset item data
|
|
387
|
+
# @raise [NotFoundError] if the item is not found
|
|
388
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
389
|
+
# @raise [ApiError] for other API errors
|
|
390
|
+
#
|
|
391
|
+
# @example
|
|
392
|
+
# data = api_client.get_dataset_item("item-uuid-123")
|
|
393
|
+
def get_dataset_item(id)
|
|
394
|
+
with_faraday_error_handling do
|
|
395
|
+
encoded_id = URI.encode_uri_component(id)
|
|
396
|
+
response = connection.get("/api/public/dataset-items/#{encoded_id}")
|
|
397
|
+
handle_response(response)
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# List items in a dataset with optional filters
|
|
402
|
+
#
|
|
403
|
+
# @param dataset_name [String] Name of the dataset (required)
|
|
404
|
+
# @param page [Integer, nil] Optional page number for pagination
|
|
405
|
+
# @param limit [Integer, nil] Optional limit per page
|
|
406
|
+
# @param source_trace_id [String, nil] Filter by source trace ID
|
|
407
|
+
# @param source_observation_id [String, nil] Filter by source observation ID
|
|
408
|
+
# @return [Array<Hash>] Array of dataset item hashes
|
|
409
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
410
|
+
# @raise [ApiError] for other API errors
|
|
411
|
+
#
|
|
412
|
+
# @example
|
|
413
|
+
# items = api_client.list_dataset_items(dataset_name: "my-dataset", limit: 50)
|
|
414
|
+
def list_dataset_items(dataset_name:, page: nil, limit: nil,
|
|
415
|
+
source_trace_id: nil, source_observation_id: nil)
|
|
416
|
+
result = list_dataset_items_paginated(
|
|
417
|
+
dataset_name: dataset_name, page: page, limit: limit,
|
|
418
|
+
source_trace_id: source_trace_id, source_observation_id: source_observation_id
|
|
419
|
+
)
|
|
420
|
+
result["data"] || []
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Full paginated response including "meta" for internal pagination use
|
|
424
|
+
#
|
|
425
|
+
# @api private
|
|
426
|
+
# @return [Hash] Full response hash with "data" array and "meta" pagination info
|
|
427
|
+
def list_dataset_items_paginated(dataset_name:, page: nil, limit: nil,
|
|
428
|
+
source_trace_id: nil, source_observation_id: nil)
|
|
429
|
+
with_faraday_error_handling do
|
|
430
|
+
params = build_dataset_items_params(
|
|
431
|
+
dataset_name: dataset_name, page: page, limit: limit,
|
|
432
|
+
source_trace_id: source_trace_id, source_observation_id: source_observation_id
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
response = connection.get("/api/public/dataset-items", params)
|
|
436
|
+
handle_response(response)
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Delete a dataset item by ID
|
|
441
|
+
#
|
|
442
|
+
# @param id [String] Dataset item ID
|
|
443
|
+
# @return [Hash] The response body
|
|
444
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
445
|
+
# @raise [ApiError] for other API errors
|
|
446
|
+
# @note 404 responses are treated as success to keep DELETE idempotent across retries
|
|
447
|
+
#
|
|
448
|
+
# @example
|
|
449
|
+
# api_client.delete_dataset_item("item-uuid-123")
|
|
450
|
+
def delete_dataset_item(id)
|
|
451
|
+
encoded_id = URI.encode_uri_component(id)
|
|
452
|
+
response = connection.delete("/api/public/dataset-items/#{encoded_id}")
|
|
453
|
+
handle_delete_dataset_item_response(response, id)
|
|
454
|
+
rescue Faraday::RetriableResponse => e
|
|
455
|
+
logger.error("Faraday error: Retries exhausted - #{e.response.status}")
|
|
456
|
+
handle_delete_dataset_item_response(e.response, id)
|
|
457
|
+
rescue Faraday::Error => e
|
|
458
|
+
logger.error("Faraday error: #{e.message}")
|
|
459
|
+
raise ApiError, "HTTP request failed: #{e.message}"
|
|
460
|
+
end
|
|
461
|
+
|
|
236
462
|
private
|
|
237
463
|
|
|
238
464
|
# Fetch prompt using the most appropriate caching strategy available
|
|
@@ -262,6 +488,43 @@ module Langfuse
|
|
|
262
488
|
cache.respond_to?(:fetch_with_lock)
|
|
263
489
|
end
|
|
264
490
|
|
|
491
|
+
# Build payload for create_dataset_item
|
|
492
|
+
# rubocop:disable Metrics/ParameterLists
|
|
493
|
+
def build_dataset_item_payload(dataset_name:, input:, expected_output:,
|
|
494
|
+
metadata:, id:, source_trace_id:,
|
|
495
|
+
source_observation_id:, status:)
|
|
496
|
+
{ datasetName: dataset_name }.tap do |payload|
|
|
497
|
+
add_optional_dataset_item_fields(payload, input, expected_output, metadata, id)
|
|
498
|
+
add_optional_source_fields(payload, source_trace_id, source_observation_id, status)
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
# rubocop:enable Metrics/ParameterLists
|
|
502
|
+
|
|
503
|
+
def add_optional_dataset_item_fields(payload, input, expected_output, metadata, id)
|
|
504
|
+
payload[:id] = id if id
|
|
505
|
+
payload[:input] = input if input
|
|
506
|
+
payload[:expectedOutput] = expected_output if expected_output
|
|
507
|
+
payload[:metadata] = metadata if metadata
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def add_optional_source_fields(payload, source_trace_id, source_observation_id, status)
|
|
511
|
+
payload[:sourceTraceId] = source_trace_id if source_trace_id
|
|
512
|
+
payload[:sourceObservationId] = source_observation_id if source_observation_id
|
|
513
|
+
payload[:status] = status.to_s.upcase if status
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# Build params for list_dataset_items
|
|
517
|
+
def build_dataset_items_params(dataset_name:, page:, limit:,
|
|
518
|
+
source_trace_id:, source_observation_id:)
|
|
519
|
+
{
|
|
520
|
+
datasetName: dataset_name,
|
|
521
|
+
page: page,
|
|
522
|
+
limit: limit,
|
|
523
|
+
sourceTraceId: source_trace_id,
|
|
524
|
+
sourceObservationId: source_observation_id
|
|
525
|
+
}.compact
|
|
526
|
+
end
|
|
527
|
+
|
|
265
528
|
# Fetch with SWR cache
|
|
266
529
|
def fetch_with_swr_cache(cache_key, name, version, label)
|
|
267
530
|
cache.fetch_with_stale_while_revalidate(cache_key) do
|
|
@@ -296,18 +559,13 @@ module Langfuse
|
|
|
296
559
|
# @raise [UnauthorizedError] if authentication fails
|
|
297
560
|
# @raise [ApiError] for other API errors
|
|
298
561
|
def fetch_prompt_from_api(name, version: nil, label: nil)
|
|
299
|
-
|
|
300
|
-
|
|
562
|
+
with_faraday_error_handling do
|
|
563
|
+
params = build_prompt_params(version: version, label: label)
|
|
564
|
+
path = "/api/public/v2/prompts/#{URI.encode_uri_component(name)}"
|
|
301
565
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
# Retry middleware exhausted all retries - handle the final response
|
|
306
|
-
logger.error("Faraday error: Retries exhausted - #{e.response.status}")
|
|
307
|
-
handle_response(e.response)
|
|
308
|
-
rescue Faraday::Error => e
|
|
309
|
-
logger.error("Faraday error: #{e.message}")
|
|
310
|
-
raise ApiError, "HTTP request failed: #{e.message}"
|
|
566
|
+
response = connection.get(path, params)
|
|
567
|
+
handle_response(response)
|
|
568
|
+
end
|
|
311
569
|
end
|
|
312
570
|
|
|
313
571
|
# Build a new Faraday connection
|
|
@@ -332,7 +590,7 @@ module Langfuse
|
|
|
332
590
|
# Retries transient errors with exponential backoff:
|
|
333
591
|
# - Max 2 retries (3 total attempts)
|
|
334
592
|
# - Exponential backoff (0.05s * 2^retry_count)
|
|
335
|
-
# - Retries GET and
|
|
593
|
+
# - Retries GET, PATCH, and DELETE requests (idempotent operations)
|
|
336
594
|
# - Retries POST requests to batch endpoint (idempotent due to event UUIDs)
|
|
337
595
|
# - Note: POST to create_prompt is NOT idempotent; retries may create duplicate versions
|
|
338
596
|
# - Retries on: 429 (rate limit), 503 (service unavailable), 504 (gateway timeout)
|
|
@@ -344,7 +602,7 @@ module Langfuse
|
|
|
344
602
|
max: 2,
|
|
345
603
|
interval: 0.05,
|
|
346
604
|
backoff_factor: 2,
|
|
347
|
-
methods: %i[get post patch],
|
|
605
|
+
methods: %i[get post patch delete],
|
|
348
606
|
retry_statuses: [429, 503, 504],
|
|
349
607
|
exceptions: [Faraday::TimeoutError, Faraday::ConnectionFailed]
|
|
350
608
|
}
|
|
@@ -382,10 +640,25 @@ module Langfuse
|
|
|
382
640
|
# @param label [String, nil] Optional label
|
|
383
641
|
# @return [Hash] Query parameters
|
|
384
642
|
def build_prompt_params(version: nil, label: nil)
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
643
|
+
{ version: version, label: label }.compact
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
# Wrap a block with standard Faraday error handling.
|
|
647
|
+
#
|
|
648
|
+
# Catches RetriableResponse (retries exhausted) and generic Faraday errors,
|
|
649
|
+
# translating them into ApiError with consistent logging.
|
|
650
|
+
#
|
|
651
|
+
# @yield The block containing the Faraday request and response handling
|
|
652
|
+
# @return [Object] The return value of the block
|
|
653
|
+
# @raise [ApiError] when a Faraday error occurs
|
|
654
|
+
def with_faraday_error_handling
|
|
655
|
+
yield
|
|
656
|
+
rescue Faraday::RetriableResponse => e
|
|
657
|
+
logger.error("Faraday error: Retries exhausted - #{e.response.status}")
|
|
658
|
+
handle_response(e.response)
|
|
659
|
+
rescue Faraday::Error => e
|
|
660
|
+
logger.error("Faraday error: #{e.message}")
|
|
661
|
+
raise ApiError, "HTTP request failed: #{e.message}"
|
|
389
662
|
end
|
|
390
663
|
|
|
391
664
|
# Handle HTTP response and raise appropriate errors
|
|
@@ -402,13 +675,20 @@ module Langfuse
|
|
|
402
675
|
when 401
|
|
403
676
|
raise UnauthorizedError, "Authentication failed. Check your API keys."
|
|
404
677
|
when 404
|
|
405
|
-
raise NotFoundError,
|
|
678
|
+
raise NotFoundError, extract_error_message(response)
|
|
406
679
|
else
|
|
407
680
|
error_message = extract_error_message(response)
|
|
408
681
|
raise ApiError, "API request failed (#{response.status}): #{error_message}"
|
|
409
682
|
end
|
|
410
683
|
end
|
|
411
684
|
|
|
685
|
+
def handle_delete_dataset_item_response(response, id)
|
|
686
|
+
return { "id" => id } if response&.status == 404
|
|
687
|
+
return response.body if [200, 201].include?(response&.status)
|
|
688
|
+
|
|
689
|
+
handle_response(response)
|
|
690
|
+
end
|
|
691
|
+
|
|
412
692
|
# Handle HTTP response for batch requests
|
|
413
693
|
#
|
|
414
694
|
# @param response [Faraday::Response] The HTTP response
|
|
@@ -20,6 +20,7 @@ module Langfuse
|
|
|
20
20
|
# end
|
|
21
21
|
#
|
|
22
22
|
class CacheWarmer
|
|
23
|
+
# @return [Client] Langfuse client used for fetching prompts
|
|
23
24
|
attr_reader :client
|
|
24
25
|
|
|
25
26
|
# Initialize a new cache warmer
|
|
@@ -35,8 +36,8 @@ module Langfuse
|
|
|
35
36
|
# safe to call multiple times.
|
|
36
37
|
#
|
|
37
38
|
# @param prompt_names [Array<String>] List of prompt names to cache
|
|
38
|
-
# @param versions [Hash<String, Integer
|
|
39
|
-
# @param labels [Hash<String, String
|
|
39
|
+
# @param versions [Hash<String, Integer>] Optional version numbers per prompt
|
|
40
|
+
# @param labels [Hash<String, String>] Optional labels per prompt
|
|
40
41
|
# @return [Hash] Results with :success and :failed arrays
|
|
41
42
|
#
|
|
42
43
|
# @example Basic warming
|
|
@@ -73,8 +74,8 @@ module Langfuse
|
|
|
73
74
|
# are cached without manually specifying them.
|
|
74
75
|
#
|
|
75
76
|
# @param default_label [String, nil] Label to use for all prompts (default: "production")
|
|
76
|
-
# @param versions [Hash<String, Integer
|
|
77
|
-
# @param labels [Hash<String, String
|
|
77
|
+
# @param versions [Hash<String, Integer>] Optional version numbers per prompt
|
|
78
|
+
# @param labels [Hash<String, String>] Optional labels per specific prompts (overrides default_label)
|
|
78
79
|
# @return [Hash] Results with :success and :failed arrays
|
|
79
80
|
#
|
|
80
81
|
# @example Auto-discover and warm all prompts with "production" label
|
|
@@ -119,8 +120,8 @@ module Langfuse
|
|
|
119
120
|
# Useful when you want to abort deployment if cache warming fails.
|
|
120
121
|
#
|
|
121
122
|
# @param prompt_names [Array<String>] List of prompt names to cache
|
|
122
|
-
# @param versions [Hash<String, Integer
|
|
123
|
-
# @param labels [Hash<String, String
|
|
123
|
+
# @param versions [Hash<String, Integer>] Optional version numbers per prompt
|
|
124
|
+
# @param labels [Hash<String, String>] Optional labels per prompt
|
|
124
125
|
# @return [Hash] Results with :success array
|
|
125
126
|
# @raise [CacheWarmingError] if any prompts fail to cache
|
|
126
127
|
#
|
|
@@ -20,7 +20,23 @@ module Langfuse
|
|
|
20
20
|
# chat_prompt.labels # => ["production"]
|
|
21
21
|
#
|
|
22
22
|
class ChatPromptClient
|
|
23
|
-
|
|
23
|
+
# @return [String] Prompt name
|
|
24
|
+
attr_reader :name
|
|
25
|
+
|
|
26
|
+
# @return [Integer] Prompt version number
|
|
27
|
+
attr_reader :version
|
|
28
|
+
|
|
29
|
+
# @return [Array<String>] Labels assigned to this prompt
|
|
30
|
+
attr_reader :labels
|
|
31
|
+
|
|
32
|
+
# @return [Array<String>] Tags assigned to this prompt
|
|
33
|
+
attr_reader :tags
|
|
34
|
+
|
|
35
|
+
# @return [Hash] Prompt configuration
|
|
36
|
+
attr_reader :config
|
|
37
|
+
|
|
38
|
+
# @return [Array<Hash>] Array of message hashes with role and content
|
|
39
|
+
attr_reader :prompt
|
|
24
40
|
|
|
25
41
|
# Initialize a new chat prompt client
|
|
26
42
|
#
|