langfuse-rb 0.9.0 → 0.10.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 +13 -1
- data/README.md +8 -65
- data/lib/langfuse/api_client.rb +344 -30
- data/lib/langfuse/cache_constants.rb +32 -0
- data/lib/langfuse/client.rb +162 -12
- data/lib/langfuse/config.rb +13 -2
- data/lib/langfuse/prompt_cache.rb +142 -7
- data/lib/langfuse/prompt_cache_events.rb +110 -0
- data/lib/langfuse/prompt_fetch_result.rb +122 -0
- data/lib/langfuse/rails_cache_adapter.rb +161 -9
- data/lib/langfuse/stale_while_revalidate.rb +62 -19
- data/lib/langfuse/version.rb +1 -1
- data/lib/langfuse.rb +3 -0
- metadata +9 -3
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Langfuse
|
|
4
|
+
# Public metadata returned by prompt fetch operations.
|
|
5
|
+
class PromptFetchResult
|
|
6
|
+
# @return [Object] Prompt data or prompt client returned by the fetch
|
|
7
|
+
attr_reader :prompt
|
|
8
|
+
|
|
9
|
+
# @return [String] Stable logical cache identity
|
|
10
|
+
attr_reader :logical_key
|
|
11
|
+
|
|
12
|
+
# @return [String] Generated backend key for the current cache generation
|
|
13
|
+
attr_reader :storage_key
|
|
14
|
+
|
|
15
|
+
# @return [Symbol] Cache status (:hit, :miss, :stale, :refresh, :bypass, :disabled)
|
|
16
|
+
attr_reader :cache_status
|
|
17
|
+
|
|
18
|
+
# @return [Symbol] Source of the returned prompt (:cache, :api, :fallback)
|
|
19
|
+
attr_reader :source
|
|
20
|
+
|
|
21
|
+
# @return [String] Prompt name
|
|
22
|
+
attr_reader :name
|
|
23
|
+
|
|
24
|
+
# @return [Integer, nil] Prompt version
|
|
25
|
+
attr_reader :version
|
|
26
|
+
|
|
27
|
+
# @return [String, nil] Prompt label
|
|
28
|
+
attr_reader :label
|
|
29
|
+
|
|
30
|
+
# @param prompt [Object] Prompt data or prompt client
|
|
31
|
+
# @param logical_key [String] Stable logical cache identity
|
|
32
|
+
# @param storage_key [String] Generated backend key
|
|
33
|
+
# @param cache_status [Symbol] Cache status
|
|
34
|
+
# @param source [Symbol] Prompt source
|
|
35
|
+
# @param name [String] Prompt name
|
|
36
|
+
# @param version [Integer, nil] Prompt version
|
|
37
|
+
# @param label [String, nil] Prompt label
|
|
38
|
+
# @return [PromptFetchResult]
|
|
39
|
+
# rubocop:disable Metrics/ParameterLists
|
|
40
|
+
def initialize(prompt:, logical_key:, storage_key:, cache_status:, source:, name:, version: nil, label: nil)
|
|
41
|
+
@prompt = prompt
|
|
42
|
+
@logical_key = logical_key
|
|
43
|
+
@storage_key = storage_key
|
|
44
|
+
@cache_status = cache_status
|
|
45
|
+
@source = source
|
|
46
|
+
@name = name
|
|
47
|
+
@version = version
|
|
48
|
+
@label = label
|
|
49
|
+
end
|
|
50
|
+
# rubocop:enable Metrics/ParameterLists
|
|
51
|
+
|
|
52
|
+
# @return [Boolean] Whether this result used caller-provided fallback content
|
|
53
|
+
def fallback?
|
|
54
|
+
source == CacheSource::FALLBACK
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Hash] Result metadata as a hash
|
|
58
|
+
def to_h
|
|
59
|
+
{
|
|
60
|
+
logical_key: logical_key,
|
|
61
|
+
storage_key: storage_key,
|
|
62
|
+
cache_status: cache_status,
|
|
63
|
+
source: source,
|
|
64
|
+
name: name,
|
|
65
|
+
version: version,
|
|
66
|
+
label: label,
|
|
67
|
+
fallback: fallback?
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Public key inspection result for prompt cache operations.
|
|
73
|
+
class PromptCacheKey
|
|
74
|
+
# @return [String] Prompt name
|
|
75
|
+
attr_reader :name
|
|
76
|
+
|
|
77
|
+
# @return [Integer, nil] Prompt version
|
|
78
|
+
attr_reader :version
|
|
79
|
+
|
|
80
|
+
# @return [String, nil] Prompt label
|
|
81
|
+
attr_reader :label
|
|
82
|
+
|
|
83
|
+
# @return [String] Stable logical cache identity
|
|
84
|
+
attr_reader :logical_key
|
|
85
|
+
|
|
86
|
+
# @return [String] Generated backend key for the current cache generation
|
|
87
|
+
attr_reader :storage_key
|
|
88
|
+
|
|
89
|
+
# @param name [String] Prompt name
|
|
90
|
+
# @param logical_key [String] Stable logical cache identity
|
|
91
|
+
# @param storage_key [String] Generated backend key
|
|
92
|
+
# @param version [Integer, nil] Prompt version
|
|
93
|
+
# @param label [String, nil] Prompt label
|
|
94
|
+
# @return [PromptCacheKey]
|
|
95
|
+
def initialize(name:, logical_key:, storage_key:, version: nil, label: nil)
|
|
96
|
+
@name = name
|
|
97
|
+
@version = version
|
|
98
|
+
@label = label
|
|
99
|
+
@logical_key = logical_key
|
|
100
|
+
@storage_key = storage_key
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Resolve the effective label, defaulting to "production" when neither
|
|
104
|
+
# an explicit label nor a version was specified.
|
|
105
|
+
#
|
|
106
|
+
# @return [String, nil] Effective label
|
|
107
|
+
def resolved_label
|
|
108
|
+
label || (version ? nil : "production")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @return [Hash] Cache key data as a hash
|
|
112
|
+
def to_h
|
|
113
|
+
{
|
|
114
|
+
name: name,
|
|
115
|
+
version: version,
|
|
116
|
+
label: label,
|
|
117
|
+
logical_key: logical_key,
|
|
118
|
+
storage_key: storage_key
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -14,9 +14,12 @@ module Langfuse
|
|
|
14
14
|
# adapter.set("greeting:1", prompt_data)
|
|
15
15
|
# adapter.get("greeting:1") # => prompt_data
|
|
16
16
|
#
|
|
17
|
+
# rubocop:disable Metrics/ClassLength
|
|
17
18
|
class RailsCacheAdapter
|
|
18
19
|
include StaleWhileRevalidate
|
|
19
20
|
|
|
21
|
+
GENERATION_MEMO_TTL_SECONDS = 1.0
|
|
22
|
+
|
|
20
23
|
# @return [Integer] Time-to-live in seconds
|
|
21
24
|
attr_reader :ttl
|
|
22
25
|
|
|
@@ -51,9 +54,12 @@ module Langfuse
|
|
|
51
54
|
|
|
52
55
|
@ttl = ttl
|
|
53
56
|
@namespace = namespace
|
|
57
|
+
@namespace_prefix = "#{namespace}:"
|
|
54
58
|
@lock_timeout = lock_timeout
|
|
55
59
|
@stale_ttl = stale_ttl
|
|
56
60
|
@logger = logger
|
|
61
|
+
@generation_memo = {}
|
|
62
|
+
@generation_memo_mutex = Mutex.new
|
|
57
63
|
initialize_swr(refresh_threads: refresh_threads) if swr_enabled?
|
|
58
64
|
end
|
|
59
65
|
|
|
@@ -65,18 +71,37 @@ module Langfuse
|
|
|
65
71
|
Rails.cache.read(namespaced_key(key))
|
|
66
72
|
end
|
|
67
73
|
|
|
74
|
+
# Read a raw cache entry, including stale entries.
|
|
75
|
+
#
|
|
76
|
+
# @param key [String] Cache key
|
|
77
|
+
# @return [Object, nil] Raw cache entry
|
|
78
|
+
def entry(key)
|
|
79
|
+
Rails.cache.read(namespaced_key(key))
|
|
80
|
+
end
|
|
81
|
+
|
|
68
82
|
# Set a value in the cache
|
|
69
83
|
#
|
|
70
84
|
# @param key [String] Cache key
|
|
71
85
|
# @param value [Object] Value to cache
|
|
72
86
|
# @return [Object] The cached value
|
|
73
|
-
def set(key, value)
|
|
74
|
-
#
|
|
75
|
-
|
|
87
|
+
def set(key, value, ttl: nil, stale_ttl: nil)
|
|
88
|
+
# Total ttl when SWR is enabled, otherwise just ttl. Inlined (not pushed
|
|
89
|
+
# to a shared helper) to keep this hot write path allocation-free.
|
|
90
|
+
effective_ttl = ttl.nil? ? self.ttl : ttl
|
|
91
|
+
effective_stale_ttl = stale_ttl.nil? ? self.stale_ttl : stale_ttl
|
|
92
|
+
expires_in = swr_enabled? ? effective_ttl + effective_stale_ttl : effective_ttl
|
|
76
93
|
Rails.cache.write(namespaced_key(key), value, expires_in:)
|
|
77
94
|
value
|
|
78
95
|
end
|
|
79
96
|
|
|
97
|
+
# Delete one generated storage key.
|
|
98
|
+
#
|
|
99
|
+
# @param key [String] Cache key
|
|
100
|
+
# @return [Boolean] true if an entry was removed
|
|
101
|
+
def delete(key)
|
|
102
|
+
Rails.cache.delete(namespaced_key(key))
|
|
103
|
+
end
|
|
104
|
+
|
|
80
105
|
# Clear the entire Langfuse cache namespace
|
|
81
106
|
#
|
|
82
107
|
# Note: This uses delete_matched which may not be available on all cache stores.
|
|
@@ -86,6 +111,37 @@ module Langfuse
|
|
|
86
111
|
def clear
|
|
87
112
|
# Delete all keys matching the namespace pattern
|
|
88
113
|
Rails.cache.delete_matched("#{namespace}:*")
|
|
114
|
+
clear_generation_memo
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Logically invalidate every generated storage key.
|
|
118
|
+
#
|
|
119
|
+
# @return [Integer] New global generation
|
|
120
|
+
def clear_logically
|
|
121
|
+
bump_generation(global_generation_key)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Logically invalidate every cache variant for one prompt name.
|
|
125
|
+
#
|
|
126
|
+
# @param name [String] Prompt name
|
|
127
|
+
# @return [Integer] New name generation
|
|
128
|
+
def invalidate_name(name)
|
|
129
|
+
bump_generation(name_generation_key(name))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Build a generated storage key for the current cache generation.
|
|
133
|
+
#
|
|
134
|
+
# @param logical_key [String] Stable logical cache identity
|
|
135
|
+
# @param name [String] Prompt name
|
|
136
|
+
# @return [String] Generated storage key
|
|
137
|
+
def storage_key(logical_key, name:)
|
|
138
|
+
generated = PromptCache.storage_key(
|
|
139
|
+
logical_key,
|
|
140
|
+
name: name,
|
|
141
|
+
global_generation: generation_value(global_generation_key),
|
|
142
|
+
name_generation: generation_value(name_generation_key(name))
|
|
143
|
+
)
|
|
144
|
+
namespaced_key(generated)
|
|
89
145
|
end
|
|
90
146
|
|
|
91
147
|
# Get current cache size
|
|
@@ -98,6 +154,19 @@ module Langfuse
|
|
|
98
154
|
nil
|
|
99
155
|
end
|
|
100
156
|
|
|
157
|
+
# @return [Hash] Prompt cache statistics
|
|
158
|
+
def stats
|
|
159
|
+
{
|
|
160
|
+
backend: CacheBackend::RAILS,
|
|
161
|
+
enabled: true,
|
|
162
|
+
current_generation_entries: nil,
|
|
163
|
+
orphaned_entries: nil,
|
|
164
|
+
total_entries: nil,
|
|
165
|
+
global_generation: generation_value(global_generation_key),
|
|
166
|
+
unsupported_counts: CacheBackend::UNSUPPORTED_COUNT_KEYS
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
|
|
101
170
|
# Check if cache is empty
|
|
102
171
|
#
|
|
103
172
|
# Note: Rails.cache doesn't provide an efficient way to check if empty,
|
|
@@ -108,6 +177,17 @@ module Langfuse
|
|
|
108
177
|
false
|
|
109
178
|
end
|
|
110
179
|
|
|
180
|
+
# Validate that Rails.cache is available for prompt caching.
|
|
181
|
+
#
|
|
182
|
+
# @return [Boolean]
|
|
183
|
+
# @raise [ConfigurationError] if Rails.cache is not available
|
|
184
|
+
# rubocop:disable Naming/PredicateMethod
|
|
185
|
+
def validate!
|
|
186
|
+
validate_rails_cache!
|
|
187
|
+
true
|
|
188
|
+
end
|
|
189
|
+
# rubocop:enable Naming/PredicateMethod
|
|
190
|
+
|
|
111
191
|
# Build a cache key from prompt name and options
|
|
112
192
|
#
|
|
113
193
|
# @param name [String] Prompt name
|
|
@@ -135,7 +215,7 @@ module Langfuse
|
|
|
135
215
|
# cache.fetch_with_lock("greeting:v1") do
|
|
136
216
|
# api_client.get_prompt("greeting")
|
|
137
217
|
# end
|
|
138
|
-
def fetch_with_lock(key)
|
|
218
|
+
def fetch_with_lock(key, ttl: nil)
|
|
139
219
|
# 1. Check cache first (fast path - no lock needed)
|
|
140
220
|
cached = get(key)
|
|
141
221
|
return cached if cached
|
|
@@ -147,7 +227,7 @@ module Langfuse
|
|
|
147
227
|
begin
|
|
148
228
|
# We got the lock - fetch from source and populate cache
|
|
149
229
|
value = yield
|
|
150
|
-
set(key, value)
|
|
230
|
+
set(key, value, ttl: ttl)
|
|
151
231
|
value
|
|
152
232
|
ensure
|
|
153
233
|
# Always release lock, even if block raises
|
|
@@ -173,7 +253,7 @@ module Langfuse
|
|
|
173
253
|
# @param key [String] Cache key
|
|
174
254
|
# @return [Object, nil] Cached value
|
|
175
255
|
def cache_get(key)
|
|
176
|
-
|
|
256
|
+
entry(key)
|
|
177
257
|
end
|
|
178
258
|
|
|
179
259
|
# Set value in cache (SWR interface)
|
|
@@ -181,8 +261,9 @@ module Langfuse
|
|
|
181
261
|
# @param key [String] Cache key
|
|
182
262
|
# @param value [Object] Value to cache (expects CacheEntry)
|
|
183
263
|
# @return [Object] The cached value
|
|
184
|
-
def cache_set(key, value)
|
|
185
|
-
|
|
264
|
+
def cache_set(key, value, ttl: nil)
|
|
265
|
+
Rails.cache.write(namespaced_key(key), value, expires_in: ttl || total_ttl)
|
|
266
|
+
value
|
|
186
267
|
end
|
|
187
268
|
|
|
188
269
|
# Build lock key with namespace
|
|
@@ -248,7 +329,77 @@ module Langfuse
|
|
|
248
329
|
# @param key [String] Original cache key
|
|
249
330
|
# @return [String] Namespaced cache key
|
|
250
331
|
def namespaced_key(key)
|
|
251
|
-
"#{
|
|
332
|
+
key.start_with?(@namespace_prefix) ? key : "#{@namespace_prefix}#{key}"
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def global_generation_key
|
|
336
|
+
namespaced_key("__prompt_cache_generation__:global")
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def name_generation_key(name)
|
|
340
|
+
encoded_name = Base64.urlsafe_encode64(name.to_s, padding: false)
|
|
341
|
+
namespaced_key("__prompt_cache_generation__:name:#{encoded_name}")
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def generation_value(key)
|
|
345
|
+
now = monotonic_time
|
|
346
|
+
memoized = memoized_generation_value(key, now)
|
|
347
|
+
return memoized unless memoized.nil?
|
|
348
|
+
|
|
349
|
+
Rails.cache.read(key).to_i.tap do |value|
|
|
350
|
+
memoize_generation_value(key, value, now)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def bump_generation(key)
|
|
355
|
+
incremented = increment_generation(key)
|
|
356
|
+
if incremented
|
|
357
|
+
memoize_generation_value(key, incremented.to_i)
|
|
358
|
+
return incremented
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
new_value = generation_value(key) + 1
|
|
362
|
+
Rails.cache.write(key, new_value)
|
|
363
|
+
memoize_generation_value(key, new_value)
|
|
364
|
+
new_value
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def increment_generation(key)
|
|
368
|
+
return unless Rails.cache.respond_to?(:increment)
|
|
369
|
+
|
|
370
|
+
Rails.cache.write(key, 0, unless_exist: true)
|
|
371
|
+
Rails.cache.increment(key, 1)
|
|
372
|
+
rescue StandardError => e
|
|
373
|
+
logger.warn("Langfuse prompt cache generation increment failed for key '#{key}': #{e.class} - #{e.message}")
|
|
374
|
+
nil
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def memoized_generation_value(key, now)
|
|
378
|
+
@generation_memo_mutex.synchronize do
|
|
379
|
+
entry = @generation_memo[key]
|
|
380
|
+
return nil unless entry
|
|
381
|
+
|
|
382
|
+
return entry.fetch(:value) if now < entry.fetch(:expires_at)
|
|
383
|
+
|
|
384
|
+
@generation_memo.delete(key)
|
|
385
|
+
nil
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def memoize_generation_value(key, value, now = monotonic_time)
|
|
390
|
+
@generation_memo_mutex.synchronize do
|
|
391
|
+
@generation_memo[key] = { value: value, expires_at: now + GENERATION_MEMO_TTL_SECONDS }
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def clear_generation_memo
|
|
396
|
+
@generation_memo_mutex.synchronize do
|
|
397
|
+
@generation_memo.clear
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def monotonic_time
|
|
402
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
252
403
|
end
|
|
253
404
|
|
|
254
405
|
# Validate that Rails.cache is available
|
|
@@ -273,4 +424,5 @@ module Langfuse
|
|
|
273
424
|
end
|
|
274
425
|
end
|
|
275
426
|
end
|
|
427
|
+
# rubocop:enable Metrics/ClassLength
|
|
276
428
|
end
|
|
@@ -41,6 +41,7 @@ module Langfuse
|
|
|
41
41
|
# # Implementation-specific lock release
|
|
42
42
|
# end
|
|
43
43
|
# end
|
|
44
|
+
# rubocop:disable Metrics/ModuleLength
|
|
44
45
|
module StaleWhileRevalidate
|
|
45
46
|
# Initialize SWR infrastructure
|
|
46
47
|
#
|
|
@@ -72,7 +73,7 @@ module Langfuse
|
|
|
72
73
|
# cache.fetch_with_stale_while_revalidate("greeting:v1") do
|
|
73
74
|
# api_client.get_prompt("greeting")
|
|
74
75
|
# end
|
|
75
|
-
def fetch_with_stale_while_revalidate(key, &)
|
|
76
|
+
def fetch_with_stale_while_revalidate(key, ttl: nil, stale_ttl: nil, &)
|
|
76
77
|
raise ConfigurationError, "fetch_with_stale_while_revalidate requires a positive stale_ttl" unless swr_enabled?
|
|
77
78
|
|
|
78
79
|
entry = cache_get(key)
|
|
@@ -84,15 +85,50 @@ module Langfuse
|
|
|
84
85
|
elsif entry&.stale?
|
|
85
86
|
# REVALIDATE - return stale + refresh in background
|
|
86
87
|
logger.debug("CACHE STALE!")
|
|
87
|
-
schedule_refresh(key, &)
|
|
88
|
+
schedule_refresh(key, ttl: ttl, stale_ttl: stale_ttl, &)
|
|
88
89
|
entry.data # Instant response!
|
|
89
90
|
else
|
|
90
91
|
# MISS - must fetch synchronously
|
|
91
92
|
logger.debug("CACHE MISS!")
|
|
92
|
-
fetch_and_cache(key, &)
|
|
93
|
+
fetch_and_cache(key, ttl: ttl, stale_ttl: stale_ttl, &)
|
|
93
94
|
end
|
|
94
95
|
end
|
|
95
96
|
|
|
97
|
+
# Schedule a cache refresh without performing a read.
|
|
98
|
+
#
|
|
99
|
+
# @param key [String] Cache key
|
|
100
|
+
# @param ttl [Integer, nil] Optional fresh TTL override
|
|
101
|
+
# @param stale_ttl [Integer, nil] Optional stale TTL override
|
|
102
|
+
# @param on_success [#call, nil] Callback invoked after a successful write
|
|
103
|
+
# @param on_failure [#call, nil] Callback invoked when refresh raises
|
|
104
|
+
# @yield Block to execute to fetch fresh data
|
|
105
|
+
# @return [Boolean] true if a refresh was scheduled
|
|
106
|
+
def refresh_async(key, ttl: nil, stale_ttl: nil, on_success: nil, on_failure: nil, &)
|
|
107
|
+
raise ConfigurationError, "refresh_async requires a positive stale_ttl" unless swr_enabled?
|
|
108
|
+
|
|
109
|
+
schedule_refresh(
|
|
110
|
+
key,
|
|
111
|
+
ttl: ttl,
|
|
112
|
+
stale_ttl: stale_ttl,
|
|
113
|
+
on_success: on_success,
|
|
114
|
+
on_failure: on_failure,
|
|
115
|
+
&
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Write a value with stale-while-revalidate metadata.
|
|
120
|
+
#
|
|
121
|
+
# @param key [String] Cache key
|
|
122
|
+
# @param value [Object] Value to cache
|
|
123
|
+
# @param ttl [Integer, nil] Optional fresh TTL override
|
|
124
|
+
# @param stale_ttl [Integer, nil] Optional stale TTL override
|
|
125
|
+
# @return [Object] The cached value
|
|
126
|
+
def write_with_stale_while_revalidate(key, value, ttl: nil, stale_ttl: nil)
|
|
127
|
+
raise ConfigurationError, "write_with_stale_while_revalidate requires a positive stale_ttl" unless swr_enabled?
|
|
128
|
+
|
|
129
|
+
set_cache_entry(key, value, ttl: ttl, stale_ttl: stale_ttl)
|
|
130
|
+
end
|
|
131
|
+
|
|
96
132
|
# Check if SWR is enabled
|
|
97
133
|
#
|
|
98
134
|
# SWR is enabled when stale_ttl is positive, meaning there's a grace period
|
|
@@ -138,29 +174,35 @@ module Langfuse
|
|
|
138
174
|
# @param key [String] Cache key
|
|
139
175
|
# @yield Block to execute to fetch fresh data
|
|
140
176
|
# @return [void]
|
|
141
|
-
|
|
177
|
+
# rubocop:disable Naming/PredicateMethod
|
|
178
|
+
def schedule_refresh(key, ttl: nil, stale_ttl: nil, on_success: nil, on_failure: nil, &block)
|
|
142
179
|
# Prevent duplicate refreshes
|
|
143
180
|
lock_key = build_lock_key(key)
|
|
144
|
-
return unless acquire_lock(lock_key)
|
|
181
|
+
return false unless acquire_lock(lock_key)
|
|
145
182
|
|
|
146
183
|
@thread_pool.post do
|
|
147
|
-
value =
|
|
148
|
-
set_cache_entry(key, value)
|
|
184
|
+
value = block.call
|
|
185
|
+
set_cache_entry(key, value, ttl: ttl, stale_ttl: stale_ttl)
|
|
186
|
+
on_success&.call(value)
|
|
149
187
|
rescue StandardError => e
|
|
188
|
+
on_failure&.call(e)
|
|
150
189
|
logger.error("Langfuse cache refresh failed for key '#{key}': #{e.class} - #{e.message}")
|
|
151
190
|
ensure
|
|
152
191
|
release_lock(lock_key)
|
|
153
192
|
end
|
|
193
|
+
|
|
194
|
+
true
|
|
154
195
|
end
|
|
196
|
+
# rubocop:enable Naming/PredicateMethod
|
|
155
197
|
|
|
156
198
|
# Fetch data and cache it with SWR metadata
|
|
157
199
|
#
|
|
158
200
|
# @param key [String] Cache key
|
|
159
201
|
# @yield Block to execute to fetch fresh data
|
|
160
202
|
# @return [Object] Freshly fetched value
|
|
161
|
-
def fetch_and_cache(key, &block)
|
|
162
|
-
value =
|
|
163
|
-
set_cache_entry(key, value)
|
|
203
|
+
def fetch_and_cache(key, ttl: nil, stale_ttl: nil, &block)
|
|
204
|
+
value = block.call
|
|
205
|
+
set_cache_entry(key, value, ttl: ttl, stale_ttl: stale_ttl)
|
|
164
206
|
end
|
|
165
207
|
|
|
166
208
|
# Set value in cache with SWR metadata (CacheEntry)
|
|
@@ -168,14 +210,14 @@ module Langfuse
|
|
|
168
210
|
# @param key [String] Cache key
|
|
169
211
|
# @param value [Object] Value to cache
|
|
170
212
|
# @return [Object] The cached value
|
|
171
|
-
def set_cache_entry(key, value)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
213
|
+
def set_cache_entry(key, value, ttl: nil, stale_ttl: nil)
|
|
214
|
+
# TTL math is inlined (not extracted to a helper) to keep this hot write
|
|
215
|
+
# path allocation-free apart from the CacheEntry below.
|
|
216
|
+
effective_ttl = ttl.nil? ? self.ttl : ttl
|
|
217
|
+
effective_stale_ttl = stale_ttl.nil? ? self.stale_ttl : stale_ttl
|
|
218
|
+
fresh_until = Time.now + effective_ttl
|
|
219
|
+
entry = PromptCache::CacheEntry.new(value, fresh_until, fresh_until + effective_stale_ttl)
|
|
220
|
+
cache_set(key, entry, ttl: effective_ttl + effective_stale_ttl)
|
|
179
221
|
value
|
|
180
222
|
end
|
|
181
223
|
|
|
@@ -213,7 +255,7 @@ module Langfuse
|
|
|
213
255
|
# @param value [Object] Value to cache
|
|
214
256
|
# @return [Object] The cached value
|
|
215
257
|
# @raise [NotImplementedError] if not implemented by including class
|
|
216
|
-
def cache_set(_key, _value)
|
|
258
|
+
def cache_set(_key, _value, ttl: nil)
|
|
217
259
|
raise NotImplementedError, "#{self.class} must implement #cache_set"
|
|
218
260
|
end
|
|
219
261
|
|
|
@@ -259,4 +301,5 @@ module Langfuse
|
|
|
259
301
|
@logger || raise(NotImplementedError, "#{self.class} must provide @logger")
|
|
260
302
|
end
|
|
261
303
|
end
|
|
304
|
+
# rubocop:enable Metrics/ModuleLength
|
|
262
305
|
end
|
data/lib/langfuse/version.rb
CHANGED
data/lib/langfuse.rb
CHANGED
|
@@ -40,9 +40,12 @@ module Langfuse
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
require_relative "langfuse/config"
|
|
43
|
+
require_relative "langfuse/cache_constants"
|
|
43
44
|
require_relative "langfuse/prompt_cache"
|
|
45
|
+
require_relative "langfuse/prompt_fetch_result"
|
|
44
46
|
require_relative "langfuse/rails_cache_adapter"
|
|
45
47
|
require_relative "langfuse/cache_warmer"
|
|
48
|
+
require_relative "langfuse/prompt_cache_events"
|
|
46
49
|
require_relative "langfuse/api_client"
|
|
47
50
|
require_relative "langfuse/span_filter"
|
|
48
51
|
require_relative "langfuse/sampling"
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: langfuse-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- SimplePractice
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-05-05 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: faraday
|
|
@@ -160,6 +161,7 @@ files:
|
|
|
160
161
|
- README.md
|
|
161
162
|
- lib/langfuse.rb
|
|
162
163
|
- lib/langfuse/api_client.rb
|
|
164
|
+
- lib/langfuse/cache_constants.rb
|
|
163
165
|
- lib/langfuse/cache_warmer.rb
|
|
164
166
|
- lib/langfuse/chat_prompt_client.rb
|
|
165
167
|
- lib/langfuse/client.rb
|
|
@@ -176,6 +178,8 @@ files:
|
|
|
176
178
|
- lib/langfuse/otel_attributes.rb
|
|
177
179
|
- lib/langfuse/otel_setup.rb
|
|
178
180
|
- lib/langfuse/prompt_cache.rb
|
|
181
|
+
- lib/langfuse/prompt_cache_events.rb
|
|
182
|
+
- lib/langfuse/prompt_fetch_result.rb
|
|
179
183
|
- lib/langfuse/prompt_renderer.rb
|
|
180
184
|
- lib/langfuse/propagation.rb
|
|
181
185
|
- lib/langfuse/rails_cache_adapter.rb
|
|
@@ -198,6 +202,7 @@ metadata:
|
|
|
198
202
|
source_code_uri: https://github.com/simplepractice/langfuse-rb
|
|
199
203
|
changelog_uri: https://github.com/simplepractice/langfuse-rb/blob/main/CHANGELOG.md
|
|
200
204
|
rubygems_mfa_required: 'true'
|
|
205
|
+
post_install_message:
|
|
201
206
|
rdoc_options: []
|
|
202
207
|
require_paths:
|
|
203
208
|
- lib
|
|
@@ -212,7 +217,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
212
217
|
- !ruby/object:Gem::Version
|
|
213
218
|
version: '0'
|
|
214
219
|
requirements: []
|
|
215
|
-
rubygems_version: 4.
|
|
220
|
+
rubygems_version: 3.4.1
|
|
221
|
+
signing_key:
|
|
216
222
|
specification_version: 4
|
|
217
223
|
summary: Ruby SDK for Langfuse - LLM observability and prompt management
|
|
218
224
|
test_files: []
|