langfuse-rb 0.8.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 +27 -1
- data/README.md +9 -63
- data/lib/langfuse/api_client.rb +344 -30
- data/lib/langfuse/cache_constants.rb +32 -0
- data/lib/langfuse/chat_prompt_client.rb +135 -20
- data/lib/langfuse/client.rb +186 -25
- data/lib/langfuse/config.rb +13 -2
- data/lib/langfuse/otel_attributes.rb +12 -4
- 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/prompt_renderer.rb +18 -0
- data/lib/langfuse/rails_cache_adapter.rb +161 -9
- data/lib/langfuse/stale_while_revalidate.rb +62 -19
- data/lib/langfuse/text_prompt_client.rb +21 -3
- data/lib/langfuse/version.rb +1 -1
- data/lib/langfuse.rb +4 -0
- metadata +6 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "monitor"
|
|
4
|
+
require "base64"
|
|
4
5
|
require_relative "stale_while_revalidate"
|
|
5
6
|
|
|
6
7
|
module Langfuse
|
|
@@ -14,9 +15,15 @@ module Langfuse
|
|
|
14
15
|
# cache.set("greeting:1", prompt_data)
|
|
15
16
|
# cache.get("greeting:1") # => prompt_data
|
|
16
17
|
#
|
|
18
|
+
# rubocop:disable Metrics/ClassLength
|
|
17
19
|
class PromptCache
|
|
18
20
|
include StaleWhileRevalidate
|
|
19
21
|
|
|
22
|
+
# Caps the per-name generation map. Without a cap, long-lived processes
|
|
23
|
+
# that invalidate across many distinct prompts grow it unboundedly; LRU
|
|
24
|
+
# eviction keeps the working set live and lets cold names go.
|
|
25
|
+
MAX_NAME_GENERATIONS = 1024
|
|
26
|
+
|
|
20
27
|
# Cache entry with data and expiration time
|
|
21
28
|
#
|
|
22
29
|
# Supports stale-while-revalidate pattern:
|
|
@@ -79,6 +86,9 @@ module Langfuse
|
|
|
79
86
|
@stale_ttl = stale_ttl
|
|
80
87
|
@logger = logger
|
|
81
88
|
@cache = {}
|
|
89
|
+
@global_generation = 0
|
|
90
|
+
@name_generations = {}
|
|
91
|
+
@name_generation_counter = 0
|
|
82
92
|
@monitor = Monitor.new
|
|
83
93
|
@locks = {} # Track locks for in-memory locking
|
|
84
94
|
initialize_swr(refresh_threads: refresh_threads) if swr_enabled?
|
|
@@ -98,24 +108,45 @@ module Langfuse
|
|
|
98
108
|
end
|
|
99
109
|
end
|
|
100
110
|
|
|
111
|
+
# Read a raw cache entry, including stale entries.
|
|
112
|
+
#
|
|
113
|
+
# @param key [String] Cache key
|
|
114
|
+
# @return [CacheEntry, nil] Raw cache entry
|
|
115
|
+
def entry(key)
|
|
116
|
+
@monitor.synchronize do
|
|
117
|
+
@cache[key]
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
101
121
|
# Set a value in the cache
|
|
102
122
|
#
|
|
103
123
|
# @param key [String] Cache key
|
|
104
124
|
# @param value [Object] Value to cache
|
|
105
125
|
# @return [Object] The cached value
|
|
106
|
-
def set(key, value)
|
|
126
|
+
def set(key, value, ttl: nil, stale_ttl: nil)
|
|
107
127
|
@monitor.synchronize do
|
|
108
128
|
# Evict oldest entry if at max size
|
|
109
129
|
evict_oldest if @cache.size >= max_size
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
130
|
+
# TTL math is inlined (not extracted to a helper) to keep this hot path
|
|
131
|
+
# allocation-free apart from the CacheEntry below.
|
|
132
|
+
effective_ttl = ttl.nil? ? self.ttl : ttl
|
|
133
|
+
effective_stale_ttl = stale_ttl.nil? ? self.stale_ttl : stale_ttl
|
|
134
|
+
fresh_until = Time.now + effective_ttl
|
|
135
|
+
@cache[key] = CacheEntry.new(value, fresh_until, fresh_until + effective_stale_ttl)
|
|
115
136
|
value
|
|
116
137
|
end
|
|
117
138
|
end
|
|
118
139
|
|
|
140
|
+
# Delete one generated storage key.
|
|
141
|
+
#
|
|
142
|
+
# @param key [String] Generated storage key
|
|
143
|
+
# @return [Boolean] true if an entry was removed
|
|
144
|
+
def delete(key)
|
|
145
|
+
@monitor.synchronize do
|
|
146
|
+
!@cache.delete(key).nil?
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
119
150
|
# Clear the entire cache
|
|
120
151
|
#
|
|
121
152
|
# @return [void]
|
|
@@ -125,6 +156,65 @@ module Langfuse
|
|
|
125
156
|
end
|
|
126
157
|
end
|
|
127
158
|
|
|
159
|
+
# Logically invalidate every generated storage key.
|
|
160
|
+
#
|
|
161
|
+
# @return [Integer] New global generation
|
|
162
|
+
def clear_logically
|
|
163
|
+
@monitor.synchronize do
|
|
164
|
+
@global_generation += 1
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Logically invalidate every cache variant for one prompt name.
|
|
169
|
+
#
|
|
170
|
+
# Generations come from a monotonic global counter, not a per-name counter,
|
|
171
|
+
# so an evicted name re-entering the map can't reuse a generation value
|
|
172
|
+
# that's still embedded in a stale @cache entry.
|
|
173
|
+
#
|
|
174
|
+
# @param name [String] Prompt name
|
|
175
|
+
# @return [Integer] New name generation
|
|
176
|
+
def invalidate_name(name)
|
|
177
|
+
@monitor.synchronize do
|
|
178
|
+
name_str = name.to_s
|
|
179
|
+
@name_generations.delete(name_str)
|
|
180
|
+
@name_generations.shift if @name_generations.size >= MAX_NAME_GENERATIONS
|
|
181
|
+
@name_generation_counter += 1
|
|
182
|
+
@name_generations[name_str] = @name_generation_counter
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Build a generated storage key for the current cache generation.
|
|
187
|
+
#
|
|
188
|
+
# @param logical_key [String] Stable logical cache identity
|
|
189
|
+
# @param name [String] Prompt name
|
|
190
|
+
# @return [String] Generated storage key
|
|
191
|
+
def storage_key(logical_key, name:)
|
|
192
|
+
@monitor.synchronize do
|
|
193
|
+
self.class.storage_key(
|
|
194
|
+
logical_key,
|
|
195
|
+
name: name,
|
|
196
|
+
global_generation: @global_generation,
|
|
197
|
+
name_generation: @name_generations.fetch(name.to_s, 0)
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# @return [Hash] Prompt cache statistics
|
|
203
|
+
def stats
|
|
204
|
+
@monitor.synchronize do
|
|
205
|
+
counts = count_entries_by_generation
|
|
206
|
+
{
|
|
207
|
+
backend: CacheBackend::MEMORY,
|
|
208
|
+
enabled: true,
|
|
209
|
+
current_generation_entries: counts.fetch(:current),
|
|
210
|
+
orphaned_entries: counts.fetch(:orphaned),
|
|
211
|
+
total_entries: @cache.size,
|
|
212
|
+
global_generation: @global_generation,
|
|
213
|
+
unsupported_counts: []
|
|
214
|
+
}
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
128
218
|
# Remove expired entries from cache
|
|
129
219
|
#
|
|
130
220
|
# @return [Integer] Number of entries removed
|
|
@@ -154,6 +244,15 @@ module Langfuse
|
|
|
154
244
|
end
|
|
155
245
|
end
|
|
156
246
|
|
|
247
|
+
# Validate that the memory cache backend is usable.
|
|
248
|
+
#
|
|
249
|
+
# @return [Boolean]
|
|
250
|
+
# rubocop:disable Naming/PredicateMethod
|
|
251
|
+
def validate!
|
|
252
|
+
true
|
|
253
|
+
end
|
|
254
|
+
# rubocop:enable Naming/PredicateMethod
|
|
255
|
+
|
|
157
256
|
# Build a cache key from prompt name and options
|
|
158
257
|
#
|
|
159
258
|
# @param name [String] Prompt name
|
|
@@ -168,6 +267,18 @@ module Langfuse
|
|
|
168
267
|
key
|
|
169
268
|
end
|
|
170
269
|
|
|
270
|
+
# Build a generated storage key from generation metadata.
|
|
271
|
+
#
|
|
272
|
+
# @param logical_key [String] Stable logical cache identity
|
|
273
|
+
# @param name [String] Prompt name
|
|
274
|
+
# @param global_generation [Integer] Global cache generation
|
|
275
|
+
# @param name_generation [Integer] Prompt-name cache generation
|
|
276
|
+
# @return [String] Generated storage key
|
|
277
|
+
def self.storage_key(logical_key, name:, global_generation:, name_generation:)
|
|
278
|
+
encoded_name = Base64.urlsafe_encode64(name.to_s, padding: false)
|
|
279
|
+
"g#{global_generation}:n#{encoded_name}:#{name_generation}:#{logical_key}"
|
|
280
|
+
end
|
|
281
|
+
|
|
171
282
|
private
|
|
172
283
|
|
|
173
284
|
# Implementation of StaleWhileRevalidate abstract methods
|
|
@@ -187,7 +298,7 @@ module Langfuse
|
|
|
187
298
|
# @param key [String] Cache key
|
|
188
299
|
# @param value [PromptCache::CacheEntry] Value to cache
|
|
189
300
|
# @return [PromptCache::CacheEntry] The cached value
|
|
190
|
-
def cache_set(key, value)
|
|
301
|
+
def cache_set(key, value, **_options)
|
|
191
302
|
@monitor.synchronize do
|
|
192
303
|
# Evict oldest entry if at max size
|
|
193
304
|
evict_oldest if @cache.size >= max_size
|
|
@@ -230,6 +341,29 @@ module Langfuse
|
|
|
230
341
|
end
|
|
231
342
|
end
|
|
232
343
|
|
|
344
|
+
def count_entries_by_generation
|
|
345
|
+
@cache.each_key.with_object({ current: 0, orphaned: 0 }) do |key, counts|
|
|
346
|
+
if current_generation_key?(key)
|
|
347
|
+
counts[:current] += 1
|
|
348
|
+
else
|
|
349
|
+
counts[:orphaned] += 1
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def current_generation_key?(key)
|
|
355
|
+
parts = key.split(":", 4)
|
|
356
|
+
return false unless parts.size == 4
|
|
357
|
+
return false unless parts[0].start_with?("g") && parts[1].start_with?("n")
|
|
358
|
+
|
|
359
|
+
global = Integer(parts[0][1..])
|
|
360
|
+
name = Base64.urlsafe_decode64(parts[1][1..])
|
|
361
|
+
name_generation = Integer(parts[2])
|
|
362
|
+
global == @global_generation && name_generation == @name_generations.fetch(name, 0)
|
|
363
|
+
rescue ArgumentError
|
|
364
|
+
false
|
|
365
|
+
end
|
|
366
|
+
|
|
233
367
|
# In-memory cache helper methods
|
|
234
368
|
|
|
235
369
|
# Evict the oldest entry from cache
|
|
@@ -250,4 +384,5 @@ module Langfuse
|
|
|
250
384
|
Logger.new($stdout, level: Logger::WARN)
|
|
251
385
|
end
|
|
252
386
|
end
|
|
387
|
+
# rubocop:enable Metrics/ClassLength
|
|
253
388
|
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Langfuse
|
|
4
|
+
# Prompt cache event emission for ApiClient.
|
|
5
|
+
#
|
|
6
|
+
# Includers must expose:
|
|
7
|
+
# - `cache_backend_name` — used in {#event_payload} to tag the cache backend
|
|
8
|
+
# - `logger` — used to warn on observer/notifier failures
|
|
9
|
+
module PromptCacheEvents
|
|
10
|
+
# ActiveSupport::Notifications event name used for prompt cache events.
|
|
11
|
+
PROMPT_CACHE_NOTIFICATION = "prompt_cache.langfuse"
|
|
12
|
+
|
|
13
|
+
# Configure prompt cache event dispatch. Wraps the observer once into a
|
|
14
|
+
# 1-arg callable so the per-event hot path never re-checks arity.
|
|
15
|
+
#
|
|
16
|
+
# @param cache_observer [#call, nil] Optional observer
|
|
17
|
+
# @return [void]
|
|
18
|
+
def setup_prompt_cache_events(cache_observer:)
|
|
19
|
+
@cache_observer_callable = wrap_cache_observer(cache_observer)
|
|
20
|
+
@active_support_notifications = defined?(ActiveSupport::Notifications) ? ActiveSupport::Notifications : nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Emit a prompt cache event to configured hooks. Accepts an eager payload
|
|
24
|
+
# hash or a block that builds one. The block is only evaluated when at
|
|
25
|
+
# least one listener is active, avoiding hash allocations on the hot path.
|
|
26
|
+
#
|
|
27
|
+
# @param event [Symbol] Event name
|
|
28
|
+
# @param payload [Hash, nil] Event payload (omit when passing a block)
|
|
29
|
+
# @yieldreturn [Hash] Lazily constructed payload
|
|
30
|
+
# @return [void]
|
|
31
|
+
def emit_prompt_cache_event(event, payload = nil)
|
|
32
|
+
observer_callable = @cache_observer_callable
|
|
33
|
+
as_listening = active_support_listening?
|
|
34
|
+
return if observer_callable.nil? && !as_listening
|
|
35
|
+
|
|
36
|
+
payload ||= block_given? ? yield : {}
|
|
37
|
+
normalized_payload = payload.merge(event: event.to_sym)
|
|
38
|
+
notify_cache_observer(normalized_payload) if observer_callable
|
|
39
|
+
notify_active_support(normalized_payload) if as_listening
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Emit a fallback event for a prompt fetch that fell back to caller-provided content.
|
|
43
|
+
#
|
|
44
|
+
# @param key [PromptCacheKey] Logical and storage cache key
|
|
45
|
+
# @param cache_status [Symbol] Cache status to report
|
|
46
|
+
# @param error [StandardError] The error that triggered the fallback
|
|
47
|
+
# @return [void]
|
|
48
|
+
def emit_prompt_fallback_event(key, cache_status:, error:)
|
|
49
|
+
emit_prompt_cache_event(:fallback) do
|
|
50
|
+
event_payload(key, cache_status, CacheSource::FALLBACK,
|
|
51
|
+
error_class: error.class.name, error_message: error.message)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# @api private
|
|
58
|
+
def event_payload(key, cache_status, source, extra = {})
|
|
59
|
+
{
|
|
60
|
+
name: key.name,
|
|
61
|
+
version: key.version,
|
|
62
|
+
label: key.resolved_label,
|
|
63
|
+
logical_key: key.logical_key,
|
|
64
|
+
storage_key: key.storage_key,
|
|
65
|
+
backend: cache_backend_name,
|
|
66
|
+
cache_status: cache_status,
|
|
67
|
+
source: source
|
|
68
|
+
}.merge(extra)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @api private
|
|
72
|
+
def notify_cache_observer(payload)
|
|
73
|
+
@cache_observer_callable.call(payload)
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
logger.warn("Langfuse prompt cache observer failed: #{e.class} - #{e.message}")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @api private
|
|
79
|
+
def active_support_listening?
|
|
80
|
+
return false unless @active_support_notifications
|
|
81
|
+
|
|
82
|
+
notifier = @active_support_notifications.notifier
|
|
83
|
+
# Defensive: notifier stand-ins (test fakes, AS::Notifications forks,
|
|
84
|
+
# very old AS versions) may not implement listening?. Assume they're
|
|
85
|
+
# listening so we still attempt to instrument; notify_active_support
|
|
86
|
+
# rescues failures.
|
|
87
|
+
return true unless notifier.respond_to?(:listening?)
|
|
88
|
+
|
|
89
|
+
notifier.listening?(PROMPT_CACHE_NOTIFICATION)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @api private
|
|
93
|
+
def notify_active_support(payload)
|
|
94
|
+
@active_support_notifications.instrument(PROMPT_CACHE_NOTIFICATION, payload)
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
logger.warn("Langfuse ActiveSupport cache notification failed: #{e.class} - #{e.message}")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @api private
|
|
100
|
+
def wrap_cache_observer(observer)
|
|
101
|
+
return nil if observer.nil?
|
|
102
|
+
|
|
103
|
+
if observer.method(:call).arity == 1
|
|
104
|
+
->(payload) { observer.call(payload) }
|
|
105
|
+
else
|
|
106
|
+
->(payload) { observer.call(payload[:event], payload) }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -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
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mustache"
|
|
4
|
+
|
|
5
|
+
module Langfuse
|
|
6
|
+
# Renders prompt templates with Langfuse SDK-compatible variable semantics.
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
class PromptRenderer < Mustache
|
|
10
|
+
# Langfuse variables are model input, not browser output; JS/Python SDKs substitute raw values.
|
|
11
|
+
#
|
|
12
|
+
# @param value [Object] Value to insert into the prompt
|
|
13
|
+
# @return [String] Raw string representation
|
|
14
|
+
def escape(value)
|
|
15
|
+
value.to_s
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|