langfuse-rb 0.9.0 → 0.10.1

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.
@@ -5,6 +5,8 @@ require "faraday/retry"
5
5
  require "base64"
6
6
  require "json"
7
7
  require "uri"
8
+ require_relative "prompt_fetch_result"
9
+ require_relative "prompt_cache_coordinator"
8
10
 
9
11
  module Langfuse
10
12
  # HTTP client for Langfuse API
@@ -22,6 +24,8 @@ module Langfuse
22
24
  # )
23
25
  #
24
26
  class ApiClient # rubocop:disable Metrics/ClassLength
27
+ include PromptCacheEvents
28
+
25
29
  # @return [String] Langfuse public API key
26
30
  attr_reader :public_key
27
31
 
@@ -48,15 +52,24 @@ module Langfuse
48
52
  # @param timeout [Integer] HTTP request timeout in seconds
49
53
  # @param logger [Logger] Logger instance for debugging
50
54
  # @param cache [PromptCache, RailsCacheAdapter, nil] Optional cache for prompt responses
55
+ # @param cache_observer [#call, nil] Optional observer for prompt cache events
51
56
  # @return [ApiClient]
52
- def initialize(public_key:, secret_key:, base_url:, timeout: 5, logger: nil, cache: nil)
57
+ # rubocop:disable Metrics/ParameterLists
58
+ def initialize(public_key:, secret_key:, base_url:, timeout: 5, logger: nil, cache: nil, cache_observer: nil)
53
59
  @public_key = public_key
54
60
  @secret_key = secret_key
55
61
  @base_url = base_url
56
62
  @timeout = timeout
57
63
  @logger = logger || Logger.new($stdout, level: Logger::WARN)
58
64
  @cache = cache
65
+ setup_prompt_cache_events(cache_observer: cache_observer)
66
+ @prompt_cache_coordinator = PromptCacheCoordinator.new(
67
+ cache: cache,
68
+ event_emitter: self,
69
+ fetch_prompt: ->(name, version:, label:) { fetch_prompt_from_api(name, version: version, label: label) }
70
+ )
59
71
  end
72
+ # rubocop:enable Metrics/ParameterLists
60
73
 
61
74
  # Get a Faraday connection
62
75
  #
@@ -89,15 +102,7 @@ module Langfuse
89
102
  # puts "#{prompt['name']} (v#{prompt['version']})"
90
103
  # end
91
104
  def list_prompts(page: nil, limit: nil)
92
- with_faraday_error_handling do
93
- params = { page: page, limit: limit }.compact
94
-
95
- response = connection.get("/api/public/v2/prompts", params)
96
- result = handle_response(response)
97
-
98
- # API returns { data: [...], meta: {...} }
99
- result["data"] || []
100
- end
105
+ request(:get, "/api/public/v2/prompts", params: { page: page, limit: limit }.compact)["data"] || []
101
106
  end
102
107
 
103
108
  # Fetch a prompt from the Langfuse API
@@ -109,18 +114,102 @@ module Langfuse
109
114
  # @param name [String] The name of the prompt
110
115
  # @param version [Integer, nil] Optional specific version number
111
116
  # @param label [String, nil] Optional label (e.g., "production", "latest")
117
+ # @param cache_ttl [Integer, nil] Optional TTL override for this fetch
112
118
  # @return [Hash] The prompt data
113
119
  # @raise [ArgumentError] if both version and label are provided
114
120
  # @raise [NotFoundError] if the prompt is not found
115
121
  # @raise [UnauthorizedError] if authentication fails
116
122
  # @raise [ApiError] for other API errors
117
- def get_prompt(name, version: nil, label: nil)
118
- raise ArgumentError, "Cannot specify both version and label" if version && label
119
- return fetch_prompt_from_api(name, version: version, label: label) if cache.nil?
123
+ def get_prompt(name, version: nil, label: nil, cache_ttl: nil)
124
+ get_prompt_result(name, version: version, label: label, cache_ttl: cache_ttl).prompt
125
+ end
126
+
127
+ # Fetch a prompt and include cache metadata.
128
+ #
129
+ # @param name [String] The name of the prompt
130
+ # @param version [Integer, nil] Optional specific version number
131
+ # @param label [String, nil] Optional label (e.g., "production", "latest")
132
+ # @param cache_ttl [Integer, nil] Optional TTL override for this fetch
133
+ # @return [PromptFetchResult] Prompt data plus cache metadata
134
+ # @raise [ArgumentError] if both version and label are provided
135
+ # @raise [ArgumentError] if cache_ttl is negative
136
+ # @raise [NotFoundError] if the prompt is not found
137
+ # @raise [UnauthorizedError] if authentication fails
138
+ # @raise [ApiError] for other API errors
139
+ def get_prompt_result(name, version: nil, label: nil, cache_ttl: nil)
140
+ @prompt_cache_coordinator.get_prompt_result(name, version: version, label: label, cache_ttl: cache_ttl)
141
+ end
142
+
143
+ # Refresh a prompt from the API, optionally writing through to cache.
144
+ #
145
+ # @param name [String] The name of the prompt
146
+ # @param version [Integer, nil] Optional specific version number
147
+ # @param label [String, nil] Optional label
148
+ # @param cache_ttl [Integer, nil] Optional TTL override for this refresh
149
+ # @return [PromptFetchResult] Prompt data plus cache metadata
150
+ # @raise [ArgumentError] if both version and label are provided
151
+ # @raise [ArgumentError] if cache_ttl is negative
152
+ # @raise [NotFoundError] if the prompt is not found
153
+ # @raise [UnauthorizedError] if authentication fails
154
+ # @raise [ApiError] for other API errors
155
+ def refresh_prompt(name, version: nil, label: nil, cache_ttl: nil)
156
+ @prompt_cache_coordinator.refresh_prompt(name, version: version, label: label, cache_ttl: cache_ttl)
157
+ end
158
+
159
+ # Inspect the logical and generated cache keys for a prompt.
160
+ #
161
+ # @param name [String] The prompt name
162
+ # @param version [Integer, nil] Optional specific version number
163
+ # @param label [String, nil] Optional label
164
+ # @return [PromptCacheKey] Logical and generated cache keys
165
+ # @raise [ArgumentError] if both version and label are provided
166
+ def prompt_cache_key(name, version: nil, label: nil)
167
+ @prompt_cache_coordinator.prompt_cache_key(name, version: version, label: label)
168
+ end
120
169
 
121
- cache_key = PromptCache.build_key(name, version: version, label: label)
122
- fetch_with_appropriate_caching_strategy(cache_key, name, version, label)
170
+ # Invalidate one exact logical prompt cache key.
171
+ #
172
+ # @param name [String] The prompt name
173
+ # @param version [Integer, nil] Optional specific version number
174
+ # @param label [String, nil] Optional label
175
+ # @return [PromptCacheKey] The invalidated key
176
+ # @raise [ArgumentError] if both version and label are provided
177
+ def invalidate_prompt_cache(name, version: nil, label: nil)
178
+ @prompt_cache_coordinator.invalidate_prompt_cache(name, version: version, label: label)
179
+ end
180
+
181
+ # Invalidate all cached variants for one prompt name.
182
+ #
183
+ # @param name [String] The prompt name
184
+ # @return [Integer, nil] New generation, or nil when cache is disabled
185
+ def invalidate_prompt_cache_by_name(name)
186
+ @prompt_cache_coordinator.invalidate_prompt_cache_by_name(name)
187
+ end
188
+
189
+ # Logically clear the whole Langfuse prompt cache namespace.
190
+ #
191
+ # @return [Integer, nil] New global generation, or nil when cache is disabled
192
+ def clear_prompt_cache
193
+ @prompt_cache_coordinator.clear_prompt_cache
194
+ end
195
+
196
+ # Return prompt cache statistics.
197
+ #
198
+ # @return [Hash] Cache statistics
199
+ def prompt_cache_stats
200
+ @prompt_cache_coordinator.prompt_cache_stats
201
+ end
202
+
203
+ # Validate the configured prompt cache backend.
204
+ #
205
+ # @return [Boolean] true when the configured backend is usable
206
+ # @raise [ConfigurationError] if the backend is invalid
207
+ # rubocop:disable Naming/PredicateMethod
208
+ def validate_prompt_cache_backend!
209
+ @cache&.validate!
210
+ true
123
211
  end
212
+ # rubocop:enable Naming/PredicateMethod
124
213
 
125
214
  # Create a new prompt (or new version if prompt with same name exists)
126
215
  #
@@ -145,21 +234,12 @@ module Langfuse
145
234
  #
146
235
  # rubocop:disable Metrics/ParameterLists
147
236
  def create_prompt(name:, prompt:, type:, config: {}, labels: [], tags: [], commit_message: nil)
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
237
+ payload = {
238
+ name: name, prompt: prompt, type: type, config: config,
239
+ labels: labels, tags: tags, commitMessage: commit_message
240
+ }.compact
241
+ request(:post, "/api/public/v2/prompts", body: payload)
242
+ .tap { @prompt_cache_coordinator.invalidate_after_mutation(name) }
163
243
  end
164
244
  # rubocop:enable Metrics/ParameterLists
165
245
 
@@ -183,13 +263,9 @@ module Langfuse
183
263
  def update_prompt(name:, version:, labels:)
184
264
  raise ArgumentError, "labels must be an array" unless labels.is_a?(Array)
185
265
 
186
- with_faraday_error_handling do
187
- path = "/api/public/v2/prompts/#{URI.encode_uri_component(name)}/versions/#{version}"
188
- payload = { newLabels: labels }
189
-
190
- response = connection.patch(path, payload)
191
- handle_response(response)
192
- end
266
+ path = "/api/public/v2/prompts/#{URI.encode_uri_component(name)}/versions/#{version}"
267
+ request(:patch, path, body: { newLabels: labels })
268
+ .tap { @prompt_cache_coordinator.invalidate_after_mutation(name) }
193
269
  end
194
270
 
195
271
  # Send a batch of events to the Langfuse ingestion API
@@ -218,13 +294,9 @@ module Langfuse
218
294
  raise ArgumentError, "events must be an array" unless events.is_a?(Array)
219
295
  raise ArgumentError, "events array cannot be empty" if events.empty?
220
296
 
221
- path = "/api/public/ingestion"
222
- payload = { batch: events }
223
-
224
- response = connection.post(path, payload)
297
+ response = connection.post("/api/public/ingestion", { batch: events })
225
298
  handle_batch_response(response)
226
299
  rescue Faraday::RetriableResponse => e
227
- # Retry middleware exhausted all retries - handle the final response
228
300
  logger.error("Langfuse batch send failed: Retries exhausted - #{e.response.status}")
229
301
  handle_batch_response(e.response)
230
302
  rescue Faraday::Error => e
@@ -248,16 +320,12 @@ module Langfuse
248
320
  # api_client.create_dataset_run_item(dataset_item_id: "item-123", run_name: "eval-v1", trace_id: "trace-abc")
249
321
  def create_dataset_run_item(dataset_item_id:, run_name:, trace_id: nil,
250
322
  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
323
+ payload = {
324
+ datasetItemId: dataset_item_id, runName: run_name,
325
+ traceId: trace_id, observationId: observation_id,
326
+ metadata: metadata, runDescription: run_description
327
+ }.compact
328
+ request(:post, "/api/public/dataset-run-items", body: payload)
261
329
  end
262
330
 
263
331
  # Fetch a dataset run by dataset and run name
@@ -269,10 +337,7 @@ module Langfuse
269
337
  # @raise [UnauthorizedError] if authentication fails
270
338
  # @raise [ApiError] for other API errors
271
339
  def get_dataset_run(dataset_name:, run_name:)
272
- with_faraday_error_handling do
273
- response = connection.get(dataset_run_path(dataset_name: dataset_name, run_name: run_name))
274
- handle_response(response)
275
- end
340
+ request(:get, dataset_run_path(dataset_name: dataset_name, run_name: run_name))
276
341
  end
277
342
 
278
343
  # List dataset runs in a dataset
@@ -284,8 +349,7 @@ module Langfuse
284
349
  # @raise [UnauthorizedError] if authentication fails
285
350
  # @raise [ApiError] for other API errors
286
351
  def list_dataset_runs(dataset_name:, page: nil, limit: nil)
287
- result = list_dataset_runs_paginated(dataset_name: dataset_name, page: page, limit: limit)
288
- result["data"] || []
352
+ list_dataset_runs_paginated(dataset_name: dataset_name, page: page, limit: limit)["data"] || []
289
353
  end
290
354
 
291
355
  # Full paginated response including "meta" for internal pagination use
@@ -293,10 +357,7 @@ module Langfuse
293
357
  # @api private
294
358
  # @return [Hash] Full response hash with "data" array and "meta" pagination info
295
359
  def list_dataset_runs_paginated(dataset_name:, page: nil, limit: nil)
296
- with_faraday_error_handling do
297
- response = connection.get(dataset_runs_path(dataset_name), build_dataset_runs_params(page: page, limit: limit))
298
- handle_response(response)
299
- end
360
+ request(:get, dataset_runs_path(dataset_name), params: { page: page, limit: limit }.compact)
300
361
  end
301
362
 
302
363
  # Delete a dataset run by name
@@ -325,19 +386,16 @@ module Langfuse
325
386
  # data = api_client.get_projects
326
387
  # project_id = data["data"][0]["id"]
327
388
  def get_projects # rubocop:disable Naming/AccessorMethodName
328
- with_faraday_error_handling do
329
- response = connection.get("/api/public/projects")
330
- handle_response(response)
331
- end
389
+ request(:get, "/api/public/projects")
332
390
  end
333
391
 
334
392
  # Shut down the API client and release resources
335
393
  #
336
- # Shuts down the cache if it supports shutdown (e.g., SWR thread pool).
394
+ # Shuts down the cache backend's SWR thread pool when present.
337
395
  #
338
396
  # @return [void]
339
397
  def shutdown
340
- cache.shutdown if cache.respond_to?(:shutdown)
398
+ @cache&.shutdown
341
399
  end
342
400
 
343
401
  # List traces in the project
@@ -367,14 +425,13 @@ module Langfuse
367
425
  from_timestamp: nil, to_timestamp: nil, order_by: nil,
368
426
  tags: nil, version: nil, release: nil, environment: nil,
369
427
  fields: nil, filter: nil)
370
- result = list_traces_paginated(
428
+ list_traces_paginated(
371
429
  page: page, limit: limit, user_id: user_id, name: name,
372
430
  session_id: session_id, from_timestamp: from_timestamp,
373
431
  to_timestamp: to_timestamp, order_by: order_by, tags: tags,
374
432
  version: version, release: release, environment: environment,
375
433
  fields: fields, filter: filter
376
- )
377
- result["data"] || []
434
+ )["data"] || []
378
435
  end
379
436
  # rubocop:enable Metrics/ParameterLists
380
437
 
@@ -387,17 +444,14 @@ module Langfuse
387
444
  from_timestamp: nil, to_timestamp: nil, order_by: nil,
388
445
  tags: nil, version: nil, release: nil, environment: nil,
389
446
  fields: nil, filter: nil)
390
- with_faraday_error_handling do
391
- params = build_traces_params(
392
- page: page, limit: limit, user_id: user_id, name: name,
393
- session_id: session_id, from_timestamp: from_timestamp,
394
- to_timestamp: to_timestamp, order_by: order_by, tags: tags,
395
- version: version, release: release, environment: environment,
396
- fields: fields, filter: filter
397
- )
398
- response = connection.get("/api/public/traces", params)
399
- handle_response(response)
400
- end
447
+ params = build_traces_params(
448
+ page: page, limit: limit, user_id: user_id, name: name,
449
+ session_id: session_id, from_timestamp: from_timestamp,
450
+ to_timestamp: to_timestamp, order_by: order_by, tags: tags,
451
+ version: version, release: release, environment: environment,
452
+ fields: fields, filter: filter
453
+ )
454
+ request(:get, "/api/public/traces", params: params)
401
455
  end
402
456
  # rubocop:enable Metrics/ParameterLists
403
457
 
@@ -412,11 +466,7 @@ module Langfuse
412
466
  # @example
413
467
  # trace = api_client.get_trace("trace-uuid-123")
414
468
  def get_trace(id)
415
- with_faraday_error_handling do
416
- encoded_id = URI.encode_uri_component(id)
417
- response = connection.get("/api/public/traces/#{encoded_id}")
418
- handle_response(response)
419
- end
469
+ request(:get, "/api/public/traces/#{URI.encode_uri_component(id)}")
420
470
  end
421
471
 
422
472
  # List all datasets in the project
@@ -430,13 +480,7 @@ module Langfuse
430
480
  # @example
431
481
  # datasets = api_client.list_datasets(page: 1, limit: 10)
432
482
  def list_datasets(page: nil, limit: nil)
433
- with_faraday_error_handling do
434
- params = { page: page, limit: limit }.compact
435
-
436
- response = connection.get("/api/public/v2/datasets", params)
437
- result = handle_response(response)
438
- result["data"] || []
439
- end
483
+ request(:get, "/api/public/v2/datasets", params: { page: page, limit: limit }.compact)["data"] || []
440
484
  end
441
485
 
442
486
  # Fetch a dataset by name
@@ -450,11 +494,7 @@ module Langfuse
450
494
  # @example
451
495
  # data = api_client.get_dataset("my-dataset")
452
496
  def get_dataset(name)
453
- with_faraday_error_handling do
454
- encoded_name = URI.encode_uri_component(name)
455
- response = connection.get("/api/public/v2/datasets/#{encoded_name}")
456
- handle_response(response)
457
- end
497
+ request(:get, "/api/public/v2/datasets/#{URI.encode_uri_component(name)}")
458
498
  end
459
499
 
460
500
  # Create a new dataset
@@ -469,12 +509,8 @@ module Langfuse
469
509
  # @example
470
510
  # data = api_client.create_dataset(name: "my-dataset", description: "QA evaluation set")
471
511
  def create_dataset(name:, description: nil, metadata: nil)
472
- with_faraday_error_handling do
473
- payload = { name: name, description: description, metadata: metadata }.compact
474
-
475
- response = connection.post("/api/public/v2/datasets", payload)
476
- handle_response(response)
477
- end
512
+ request(:post, "/api/public/v2/datasets",
513
+ body: { name: name, description: description, metadata: metadata }.compact)
478
514
  end
479
515
 
480
516
  # Create a new dataset item (or upsert if id is provided)
@@ -501,16 +537,13 @@ module Langfuse
501
537
  def create_dataset_item(dataset_name:, input: nil, expected_output: nil,
502
538
  metadata: nil, id: nil, source_trace_id: nil,
503
539
  source_observation_id: nil, status: nil)
504
- with_faraday_error_handling do
505
- payload = build_dataset_item_payload(
506
- dataset_name: dataset_name, input: input, expected_output: expected_output,
507
- metadata: metadata, id: id, source_trace_id: source_trace_id,
508
- source_observation_id: source_observation_id, status: status
509
- )
510
-
511
- response = connection.post("/api/public/dataset-items", payload)
512
- handle_response(response)
513
- end
540
+ payload = {
541
+ datasetName: dataset_name, id: id, input: input,
542
+ expectedOutput: expected_output, metadata: metadata,
543
+ sourceTraceId: source_trace_id, sourceObservationId: source_observation_id,
544
+ status: status&.to_s&.upcase
545
+ }.compact
546
+ request(:post, "/api/public/dataset-items", body: payload)
514
547
  end
515
548
  # rubocop:enable Metrics/ParameterLists
516
549
 
@@ -525,11 +558,7 @@ module Langfuse
525
558
  # @example
526
559
  # data = api_client.get_dataset_item("item-uuid-123")
527
560
  def get_dataset_item(id)
528
- with_faraday_error_handling do
529
- encoded_id = URI.encode_uri_component(id)
530
- response = connection.get("/api/public/dataset-items/#{encoded_id}")
531
- handle_response(response)
532
- end
561
+ request(:get, "/api/public/dataset-items/#{URI.encode_uri_component(id)}")
533
562
  end
534
563
 
535
564
  # List items in a dataset with optional filters
@@ -545,13 +574,8 @@ module Langfuse
545
574
  #
546
575
  # @example
547
576
  # items = api_client.list_dataset_items(dataset_name: "my-dataset", limit: 50)
548
- def list_dataset_items(dataset_name:, page: nil, limit: nil,
549
- source_trace_id: nil, source_observation_id: nil)
550
- result = list_dataset_items_paginated(
551
- dataset_name: dataset_name, page: page, limit: limit,
552
- source_trace_id: source_trace_id, source_observation_id: source_observation_id
553
- )
554
- result["data"] || []
577
+ def list_dataset_items(**)
578
+ list_dataset_items_paginated(**)["data"] || []
555
579
  end
556
580
 
557
581
  # Full paginated response including "meta" for internal pagination use
@@ -560,15 +584,11 @@ module Langfuse
560
584
  # @return [Hash] Full response hash with "data" array and "meta" pagination info
561
585
  def list_dataset_items_paginated(dataset_name:, page: nil, limit: nil,
562
586
  source_trace_id: nil, source_observation_id: nil)
563
- with_faraday_error_handling do
564
- params = build_dataset_items_params(
565
- dataset_name: dataset_name, page: page, limit: limit,
566
- source_trace_id: source_trace_id, source_observation_id: source_observation_id
567
- )
568
-
569
- response = connection.get("/api/public/dataset-items", params)
570
- handle_response(response)
571
- end
587
+ params = {
588
+ datasetName: dataset_name, page: page, limit: limit,
589
+ sourceTraceId: source_trace_id, sourceObservationId: source_observation_id
590
+ }.compact
591
+ request(:get, "/api/public/dataset-items", params: params)
572
592
  end
573
593
 
574
594
  # Delete a dataset item by ID
@@ -582,8 +602,7 @@ module Langfuse
582
602
  # @example
583
603
  # api_client.delete_dataset_item("item-uuid-123")
584
604
  def delete_dataset_item(id)
585
- encoded_id = URI.encode_uri_component(id)
586
- response = connection.delete("/api/public/dataset-items/#{encoded_id}")
605
+ response = connection.delete("/api/public/dataset-items/#{URI.encode_uri_component(id)}")
587
606
  handle_delete_dataset_item_response(response, id)
588
607
  rescue Faraday::RetriableResponse => e
589
608
  logger.error("Faraday error: Retries exhausted - #{e.response.status}")
@@ -595,126 +614,42 @@ module Langfuse
595
614
 
596
615
  private
597
616
 
598
- # Fetch prompt using the most appropriate caching strategy available
599
- #
600
- # @param cache_key [String] The cache key for this prompt
601
- # @param name [String] The name of the prompt
602
- # @param version [Integer, nil] Optional specific version number
603
- # @param label [String, nil] Optional label
604
- # @return [Hash] The prompt data
605
- def fetch_with_appropriate_caching_strategy(cache_key, name, version, label)
606
- if swr_cache_available?
607
- fetch_with_swr_cache(cache_key, name, version, label)
608
- elsif distributed_cache_available?
609
- fetch_with_distributed_cache(cache_key, name, version, label)
610
- else
611
- fetch_with_simple_cache(cache_key, name, version, label)
612
- end
613
- end
614
-
615
- # Check if SWR cache is available
616
- def swr_cache_available?
617
- cache.respond_to?(:swr_enabled?) && cache.swr_enabled?
617
+ def cache_backend_name
618
+ @prompt_cache_coordinator.backend_name
618
619
  end
619
620
 
620
- # Check if distributed cache is available
621
- def distributed_cache_available?
622
- cache.respond_to?(:fetch_with_lock)
623
- end
624
-
625
- # Build payload for create_dataset_item
626
- # rubocop:disable Metrics/ParameterLists
627
- def build_dataset_item_payload(dataset_name:, input:, expected_output:,
628
- metadata:, id:, source_trace_id:,
629
- source_observation_id:, status:)
630
- { datasetName: dataset_name }.tap do |payload|
631
- add_optional_dataset_item_fields(payload, input, expected_output, metadata, id)
632
- add_optional_source_fields(payload, source_trace_id, source_observation_id, status)
621
+ # Issue an HTTP request, raise on Faraday errors, parse the response.
622
+ #
623
+ # @api private
624
+ # @param verb [Symbol] HTTP verb (:get, :post, :patch, :delete)
625
+ # @param path [String] Request path
626
+ # @param params [Hash, nil] Query string params (GET/DELETE)
627
+ # @param body [Hash, nil] JSON body (POST/PATCH)
628
+ # @return [Hash] Parsed response body
629
+ def request(verb, path, params: nil, body: nil)
630
+ with_faraday_error_handling do
631
+ handle_response(connection.public_send(verb, path, body || params))
633
632
  end
634
633
  end
635
- # rubocop:enable Metrics/ParameterLists
636
-
637
- def add_optional_dataset_item_fields(payload, input, expected_output, metadata, id)
638
- payload[:id] = id if id
639
- payload[:input] = input if input
640
- payload[:expectedOutput] = expected_output if expected_output
641
- payload[:metadata] = metadata if metadata
642
- end
643
634
 
644
- def add_optional_source_fields(payload, source_trace_id, source_observation_id, status)
645
- payload[:sourceTraceId] = source_trace_id if source_trace_id
646
- payload[:sourceObservationId] = source_observation_id if source_observation_id
647
- payload[:status] = status.to_s.upcase if status
648
- end
649
-
650
- # Build params for list_dataset_items
651
- def build_dataset_items_params(dataset_name:, page:, limit:,
652
- source_trace_id:, source_observation_id:)
635
+ def build_traces_params(**options)
653
636
  {
654
- datasetName: dataset_name,
655
- page: page,
656
- limit: limit,
657
- sourceTraceId: source_trace_id,
658
- sourceObservationId: source_observation_id
637
+ page: options[:page], limit: options[:limit], userId: options[:user_id], name: options[:name],
638
+ sessionId: options[:session_id],
639
+ fromTimestamp: options[:from_timestamp]&.iso8601,
640
+ toTimestamp: options[:to_timestamp]&.iso8601,
641
+ orderBy: options[:order_by], tags: options[:tags], version: options[:version],
642
+ release: options[:release], environment: options[:environment], fields: options[:fields],
643
+ filter: options[:filter]
659
644
  }.compact
660
645
  end
661
646
 
662
- # Build params for list_dataset_runs
663
- def build_dataset_runs_params(page:, limit:)
664
- { page: page, limit: limit }.compact
665
- end
666
-
667
- # Build endpoint path for dataset runs
668
647
  def dataset_runs_path(dataset_name)
669
- encoded_name = URI.encode_uri_component(dataset_name)
670
- "/api/public/datasets/#{encoded_name}/runs"
648
+ "/api/public/datasets/#{URI.encode_uri_component(dataset_name)}/runs"
671
649
  end
672
650
 
673
- # Build endpoint path for a specific dataset run
674
651
  def dataset_run_path(dataset_name:, run_name:)
675
- encoded_run_name = URI.encode_uri_component(run_name)
676
- "#{dataset_runs_path(dataset_name)}/#{encoded_run_name}"
677
- end
678
-
679
- # Build query params for list_traces, mapping snake_case to camelCase
680
- # rubocop:disable Metrics/ParameterLists
681
- def build_traces_params(page:, limit:, user_id:, name:, session_id:,
682
- from_timestamp:, to_timestamp:, order_by:,
683
- tags:, version:, release:, environment:, fields:, filter:)
684
- {
685
- page: page, limit: limit, userId: user_id, name: name,
686
- sessionId: session_id,
687
- fromTimestamp: from_timestamp&.iso8601,
688
- toTimestamp: to_timestamp&.iso8601,
689
- orderBy: order_by, tags: tags, version: version,
690
- release: release, environment: environment, fields: fields,
691
- filter: filter
692
- }.compact
693
- end
694
- # rubocop:enable Metrics/ParameterLists
695
-
696
- # Fetch with SWR cache
697
- def fetch_with_swr_cache(cache_key, name, version, label)
698
- cache.fetch_with_stale_while_revalidate(cache_key) do
699
- fetch_prompt_from_api(name, version: version, label: label)
700
- end
701
- end
702
-
703
- # Fetch with distributed cache (Rails.cache with stampede protection)
704
- def fetch_with_distributed_cache(cache_key, name, version, label)
705
- cache.fetch_with_lock(cache_key) do
706
- fetch_prompt_from_api(name, version: version, label: label)
707
- end
708
- end
709
-
710
- # Fetch with simple cache (in-memory cache)
711
- def fetch_with_simple_cache(cache_key, name, version, label)
712
- cached_data = cache.get(cache_key)
713
- return cached_data if cached_data
714
-
715
- prompt_data = fetch_prompt_from_api(name, version: version, label: label)
716
- cache.set(cache_key, prompt_data)
717
- prompt_data
652
+ "#{dataset_runs_path(dataset_name)}/#{URI.encode_uri_component(run_name)}"
718
653
  end
719
654
 
720
655
  # Fetch a prompt from the API (without caching)
@@ -727,13 +662,8 @@ module Langfuse
727
662
  # @raise [UnauthorizedError] if authentication fails
728
663
  # @raise [ApiError] for other API errors
729
664
  def fetch_prompt_from_api(name, version: nil, label: nil)
730
- with_faraday_error_handling do
731
- params = build_prompt_params(version: version, label: label)
732
- path = "/api/public/v2/prompts/#{URI.encode_uri_component(name)}"
733
-
734
- response = connection.get(path, params)
735
- handle_response(response)
736
- end
665
+ path = "/api/public/v2/prompts/#{URI.encode_uri_component(name)}"
666
+ request(:get, path, params: { version: version, label: label }.compact)
737
667
  end
738
668
 
739
669
  # Build a new Faraday connection
@@ -802,15 +732,6 @@ module Langfuse
802
732
  "langfuse-rb/#{Langfuse::VERSION}"
803
733
  end
804
734
 
805
- # Build query parameters for prompt request
806
- #
807
- # @param version [Integer, nil] Optional version number
808
- # @param label [String, nil] Optional label
809
- # @return [Hash] Query parameters
810
- def build_prompt_params(version: nil, label: nil)
811
- { version: version, label: label }.compact
812
- end
813
-
814
735
  # Wrap a block with standard Faraday error handling.
815
736
  #
816
737
  # Catches RetriableResponse (retries exhausted) and generic Faraday errors,