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.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "mustache"
3
+ require_relative "prompt_renderer"
4
4
 
5
5
  module Langfuse
6
6
  # Chat prompt client for compiling chat prompts with variable substitution
@@ -20,6 +20,8 @@ module Langfuse
20
20
  # chat_prompt.labels # => ["production"]
21
21
  #
22
22
  class ChatPromptClient
23
+ PLACEHOLDER_TYPE = "placeholder"
24
+
23
25
  # @return [String] Prompt name
24
26
  attr_reader :name
25
27
 
@@ -35,14 +37,24 @@ module Langfuse
35
37
  # @return [Hash] Prompt configuration
36
38
  attr_reader :config
37
39
 
38
- # @return [Array<Hash>] Array of message hashes with role and content
40
+ # @return [Array<Hash>] Array of message hashes and placeholder entries
39
41
  attr_reader :prompt
40
42
 
43
+ # @return [String, nil] Optional commit message for this prompt version
44
+ attr_reader :commit_message
45
+
46
+ # @return [Hash, nil] Optional dependency resolution graph for composed prompts
47
+ attr_reader :resolution_graph
48
+
49
+ # @return [Boolean] Whether this client uses caller-provided fallback content
50
+ attr_reader :is_fallback
51
+
41
52
  # Initialize a new chat prompt client
42
53
  #
43
54
  # @param prompt_data [Hash] The prompt data from the API
55
+ # @param is_fallback [Boolean] Whether this client wraps caller-provided fallback content
44
56
  # @raise [ArgumentError] if prompt data is invalid
45
- def initialize(prompt_data)
57
+ def initialize(prompt_data, is_fallback: false)
46
58
  validate_prompt_data!(prompt_data)
47
59
 
48
60
  @name = prompt_data["name"]
@@ -51,16 +63,27 @@ module Langfuse
51
63
  @labels = prompt_data["labels"] || []
52
64
  @tags = prompt_data["tags"] || []
53
65
  @config = prompt_data["config"] || {}
66
+ @commit_message = prompt_data["commitMessage"]
67
+ @resolution_graph = prompt_data["resolutionGraph"]
68
+ @is_fallback = is_fallback
54
69
  end
55
70
 
56
- # Compile the chat prompt with variable substitution
71
+ # @return [String] Prompt type ("chat")
72
+ def type
73
+ "chat"
74
+ end
75
+
76
+ # Compile the chat prompt with variable substitution and message placeholders
57
77
  #
58
78
  # Returns an array of message hashes with roles and compiled content.
59
- # Each message in the prompt will have its content compiled with the
60
- # provided variables using Mustache templating.
79
+ # Placeholder entries are resolved from keyword arguments: arrays are
80
+ # expanded, empty arrays are skipped, unresolved placeholders stay in the
81
+ # output, and malformed values raise before invalid messages are sent to
82
+ # an LLM provider.
61
83
  #
62
- # @param kwargs [Hash] Variables to substitute in message templates (as keyword arguments)
63
- # @return [Array<Hash>] Array of compiled messages with :role and :content keys
84
+ # @param kwargs [Hash] Variables and placeholder values to compile
85
+ # @return [Array<Hash>] Array of compiled messages and unresolved placeholders
86
+ # @raise [ArgumentError] if a placeholder value is malformed
64
87
  #
65
88
  # @example
66
89
  # chat_prompt.compile(name: "Alice", topic: "Ruby")
@@ -69,9 +92,18 @@ module Langfuse
69
92
  # # { role: :user, content: "Hello Alice, let's discuss Ruby!" }
70
93
  # # ]
71
94
  def compile(**kwargs)
72
- prompt.map do |message|
73
- compile_message(message, kwargs)
95
+ unresolved = []
96
+ compiled = []
97
+ prompt.each do |message|
98
+ normalized = symbolize_keys(message)
99
+ if normalized[:type].to_s == PLACEHOLDER_TYPE
100
+ append_placeholder(normalized, kwargs, compiled, unresolved)
101
+ else
102
+ compiled << compile_message(normalized, kwargs)
103
+ end
74
104
  end
105
+ warn_unresolved(unresolved)
106
+ compiled
75
107
  end
76
108
 
77
109
  private
@@ -88,19 +120,102 @@ module Langfuse
88
120
  raise ArgumentError, "prompt must be an Array" unless prompt_data["prompt"].is_a?(Array)
89
121
  end
90
122
 
91
- # Compile a single message with variable substitution
123
+ # Compile a single role/content message with variable substitution
92
124
  #
93
- # @param message [Hash] The message with role and content
125
+ # @param normalized [Hash] Symbolized message hash
94
126
  # @param variables [Hash] Variables to substitute
95
127
  # @return [Hash] Compiled message with :role and :content as symbols
96
- def compile_message(message, variables)
97
- content = message["content"] || ""
98
- compiled_content = variables.empty? ? content : Mustache.render(content, variables)
99
-
100
- {
101
- role: normalize_role(message["role"]),
102
- content: compiled_content
103
- }
128
+ def compile_message(normalized, variables)
129
+ normalized.except(:type).merge(
130
+ role: normalize_role(normalized[:role]),
131
+ content: render(normalized[:content] || "", variables)
132
+ )
133
+ end
134
+
135
+ # @api private
136
+ def append_placeholder(message, variables, compiled, unresolved)
137
+ name = message[:name].to_s
138
+ found, value = lookup_placeholder(variables, name)
139
+ return append_unresolved(name, compiled, unresolved) unless found
140
+
141
+ expand_placeholder(name, value, variables, compiled)
142
+ end
143
+
144
+ # @api private
145
+ def append_unresolved(name, compiled, unresolved)
146
+ unresolved << name
147
+ compiled << { type: PLACEHOLDER_TYPE, name: name }
148
+ end
149
+
150
+ # @api private
151
+ def expand_placeholder(name, value, variables, compiled)
152
+ return if value.is_a?(Array) && value.empty?
153
+
154
+ unless value.is_a?(Array)
155
+ raise ArgumentError, "Placeholder '#{name}' must contain an array of chat message hashes, got #{value.class}."
156
+ end
157
+
158
+ value.each { |entry| compiled << placeholder_message(entry, variables, name) }
159
+ end
160
+
161
+ # @api private
162
+ def lookup_placeholder(variables, name)
163
+ return [true, variables[name.to_sym]] if variables.key?(name.to_sym)
164
+ return [true, variables[name]] if variables.key?(name)
165
+
166
+ [false, nil]
167
+ end
168
+
169
+ # @api private
170
+ def placeholder_message(message, variables, name)
171
+ unless message.is_a?(Hash)
172
+ raise ArgumentError,
173
+ "Placeholder '#{name}' must contain an array of chat message hashes with role and content fields."
174
+ end
175
+
176
+ normalized = symbolize_keys(message)
177
+ unless valid_placeholder_message?(normalized)
178
+ raise ArgumentError,
179
+ "Placeholder '#{name}' must contain an array of chat message hashes with role and content fields."
180
+ end
181
+
182
+ normalized.merge(
183
+ role: normalize_role(normalized[:role]),
184
+ content: render(normalized[:content] || "", variables)
185
+ )
186
+ end
187
+
188
+ # @api private
189
+ def render(content, variables)
190
+ variables.empty? ? content : PromptRenderer.render(content, variables)
191
+ end
192
+
193
+ # @api private
194
+ def valid_placeholder_message?(message)
195
+ message.is_a?(Hash) &&
196
+ message.key?(:role) &&
197
+ !message[:role].to_s.empty? &&
198
+ message.key?(:content)
199
+ end
200
+
201
+ # @api private
202
+ def warn_unresolved(names)
203
+ return if names.empty?
204
+
205
+ unresolved_names = names.uniq.sort
206
+ message = "Placeholders #{unresolved_names.inspect} have not been resolved. " \
207
+ "Pass them as keyword arguments to compile()."
208
+ warn_msg(message)
209
+ end
210
+
211
+ # @api private
212
+ def warn_msg(message)
213
+ Langfuse.configuration.logger.warn("Langfuse: #{message}")
214
+ end
215
+
216
+ # @api private
217
+ def symbolize_keys(hash)
218
+ hash.transform_keys(&:to_sym)
104
219
  end
105
220
 
106
221
  # Normalize role to symbol
@@ -46,7 +46,8 @@ module Langfuse
46
46
  base_url: config.base_url,
47
47
  timeout: config.timeout,
48
48
  logger: config.logger,
49
- cache: cache
49
+ cache: cache,
50
+ cache_observer: config.prompt_cache_observer
50
51
  )
51
52
 
52
53
  @project_id = nil
@@ -68,6 +69,7 @@ module Langfuse
68
69
  # @param label [String, nil] Optional label (e.g., "production", "latest")
69
70
  # @param fallback [String, Array, nil] Optional fallback prompt to use on error
70
71
  # @param type [Symbol, nil] Required when fallback is provided (:text or :chat)
72
+ # @param cache_ttl [Integer, nil] Optional TTL override for this fetch
71
73
  # @return [TextPromptClient, ChatPromptClient] The prompt client
72
74
  # @raise [ArgumentError] if both version and label are provided
73
75
  # @raise [ArgumentError] if fallback is provided without type
@@ -77,24 +79,114 @@ module Langfuse
77
79
  #
78
80
  # @example With fallback for graceful degradation
79
81
  # prompt = client.get_prompt("greeting", fallback: "Hello {{name}}!", type: :text)
80
- def get_prompt(name, version: nil, label: nil, fallback: nil, type: nil)
81
- # Validate fallback usage
82
- if fallback && !type
83
- raise ArgumentError, "type parameter is required when fallback is provided (use :text or :chat)"
84
- end
82
+ def get_prompt(name, version: nil, label: nil, fallback: nil, type: nil, cache_ttl: nil)
83
+ get_prompt_result(
84
+ name,
85
+ version: version,
86
+ label: label,
87
+ fallback: fallback,
88
+ type: type,
89
+ cache_ttl: cache_ttl
90
+ ).prompt
91
+ end
85
92
 
86
- # Try to fetch from API
87
- prompt_data = api_client.get_prompt(name, version: version, label: label)
88
- build_prompt_client(prompt_data)
93
+ # Fetch a prompt and return cache metadata.
94
+ #
95
+ # @param name [String] The name of the prompt
96
+ # @param version [Integer, nil] Optional specific version number
97
+ # @param label [String, nil] Optional label (e.g., "production", "latest")
98
+ # @param fallback [String, Array, nil] Optional fallback prompt to use on error
99
+ # @param type [Symbol, nil] Required when fallback is provided (:text or :chat)
100
+ # @param cache_ttl [Integer, nil] Optional TTL override for this fetch
101
+ # @return [PromptFetchResult] Prompt client plus cache metadata
102
+ # @raise [ArgumentError] if fallback is provided without type
103
+ # @raise [NotFoundError] if the prompt is not found and no fallback provided
104
+ # @raise [UnauthorizedError] if authentication fails and no fallback provided
105
+ # @raise [ApiError] for other API errors and no fallback provided
106
+ def get_prompt_result(name, version: nil, label: nil, fallback: nil, type: nil, cache_ttl: nil)
107
+ validate_fallback_usage!(fallback, type)
108
+
109
+ api_result = api_client.get_prompt_result(name, version: version, label: label, cache_ttl: cache_ttl)
110
+ build_client_fetch_result(api_result, build_prompt_client(api_result.prompt))
89
111
  rescue ApiError, NotFoundError, UnauthorizedError => e
90
112
  # If no fallback, re-raise the error
91
113
  raise e unless fallback
92
114
 
93
115
  # Log warning and return fallback
94
116
  config.logger.warn("Langfuse API error for prompt '#{name}': #{e.message}. Using fallback.")
95
- build_fallback_prompt_client(name, fallback, type)
117
+ key = api_client.prompt_cache_key(name, version: version, label: label)
118
+ build_fallback_prompt_result(key, fallback: fallback, type: type, cache_ttl: cache_ttl, error: e)
119
+ end
120
+
121
+ # Refresh a prompt from the API, optionally writing through to cache.
122
+ #
123
+ # @param name [String] The name of the prompt
124
+ # @param version [Integer, nil] Optional specific version number
125
+ # @param label [String, nil] Optional label (e.g., "production", "latest")
126
+ # @param cache_ttl [Integer, nil] Optional TTL override for this refresh
127
+ # @return [PromptFetchResult] Prompt client plus cache metadata
128
+ # @raise [ArgumentError] if both version and label are provided
129
+ # @raise [NotFoundError] if the prompt is not found
130
+ # @raise [UnauthorizedError] if authentication fails
131
+ # @raise [ApiError] for other API errors
132
+ def refresh_prompt(name, version: nil, label: nil, cache_ttl: nil)
133
+ api_result = api_client.refresh_prompt(name, version: version, label: label, cache_ttl: cache_ttl)
134
+ build_client_fetch_result(api_result, build_prompt_client(api_result.prompt))
96
135
  end
97
136
 
137
+ # Invalidate one exact logical prompt cache key.
138
+ #
139
+ # @param name [String] The prompt name
140
+ # @param version [Integer, nil] Optional specific version number
141
+ # @param label [String, nil] Optional label
142
+ # @return [PromptCacheKey] The invalidated key
143
+ def invalidate_prompt_cache(name, version: nil, label: nil)
144
+ api_client.invalidate_prompt_cache(name, version: version, label: label)
145
+ end
146
+
147
+ # Invalidate all cached variants for one prompt name.
148
+ #
149
+ # @param name [String] The prompt name
150
+ # @return [Integer, nil] New generation, or nil when cache is disabled
151
+ def invalidate_prompt_cache_by_name(name)
152
+ api_client.invalidate_prompt_cache_by_name(name)
153
+ end
154
+
155
+ # Logically clear the whole Langfuse prompt cache namespace.
156
+ #
157
+ # @return [Integer, nil] New global generation, or nil when cache is disabled
158
+ def clear_prompt_cache
159
+ api_client.clear_prompt_cache
160
+ end
161
+
162
+ # Return prompt cache statistics.
163
+ #
164
+ # @return [Hash] Cache statistics
165
+ def prompt_cache_stats
166
+ api_client.prompt_cache_stats
167
+ end
168
+
169
+ # Inspect the logical and generated cache keys for a prompt.
170
+ #
171
+ # @param name [String] The prompt name
172
+ # @param version [Integer, nil] Optional specific version number
173
+ # @param label [String, nil] Optional label
174
+ # @return [PromptCacheKey] Logical and generated cache keys
175
+ def prompt_cache_key(name, version: nil, label: nil)
176
+ api_client.prompt_cache_key(name, version: version, label: label)
177
+ end
178
+
179
+ # Validate the configured prompt cache backend before first prompt fetch.
180
+ #
181
+ # @return [Boolean] true when the configured backend is usable
182
+ # @raise [ConfigurationError] if the backend is invalid
183
+ # rubocop:disable Naming/PredicateMethod
184
+ def validate_prompt_cache_backend!
185
+ api_client.cache&.validate! if api_client.cache.respond_to?(:validate!)
186
+ true
187
+ end
188
+ # rubocop:enable Naming/PredicateMethod
189
+
98
190
  # List all prompts in the Langfuse project
99
191
  #
100
192
  # Fetches a list of all prompt names available in your project.
@@ -126,6 +218,7 @@ module Langfuse
126
218
  # @param label [String, nil] Optional label (e.g., "production", "latest")
127
219
  # @param fallback [String, Array, nil] Optional fallback prompt to use on error
128
220
  # @param type [Symbol, nil] Required when fallback is provided (:text or :chat)
221
+ # @param cache_ttl [Integer, nil] Optional TTL override for this fetch
129
222
  # @return [String, Array<Hash>] Compiled prompt (String for text, Array for chat)
130
223
  # @raise [ArgumentError] if both version and label are provided
131
224
  # @raise [ArgumentError] if fallback is provided without type
@@ -148,10 +241,19 @@ module Langfuse
148
241
  # fallback: "Hello {{name}}!",
149
242
  # type: :text
150
243
  # )
151
- def compile_prompt(name, variables: {}, version: nil, label: nil, fallback: nil, type: nil)
152
- prompt = get_prompt(name, version: version, label: label, fallback: fallback, type: type)
244
+ # rubocop:disable Metrics/ParameterLists
245
+ def compile_prompt(name, variables: {}, version: nil, label: nil, fallback: nil, type: nil, cache_ttl: nil)
246
+ prompt = get_prompt(
247
+ name,
248
+ version: version,
249
+ label: label,
250
+ fallback: fallback,
251
+ type: type,
252
+ cache_ttl: cache_ttl
253
+ )
153
254
  prompt.compile(**variables)
154
255
  end
256
+ # rubocop:enable Metrics/ParameterLists
155
257
 
156
258
  # Create a new prompt (or new version if name already exists)
157
259
  #
@@ -738,6 +840,48 @@ module Langfuse
738
840
  list_dataset_items(dataset_name: dataset_name)
739
841
  end
740
842
 
843
+ def validate_fallback_usage!(fallback, type)
844
+ return unless fallback && !type
845
+
846
+ raise ArgumentError, "type parameter is required when fallback is provided (use :text or :chat)"
847
+ end
848
+
849
+ def build_client_fetch_result(api_result, prompt_client)
850
+ PromptFetchResult.new(
851
+ prompt: prompt_client,
852
+ logical_key: api_result.logical_key,
853
+ storage_key: api_result.storage_key,
854
+ cache_status: api_result.cache_status,
855
+ source: api_result.source,
856
+ name: prompt_client.name,
857
+ version: prompt_client.version,
858
+ label: api_result.label
859
+ )
860
+ end
861
+
862
+ def build_fallback_prompt_result(key, fallback:, type:, cache_ttl:, error:)
863
+ prompt_client = build_fallback_prompt_client(key.name, fallback, type)
864
+ cache_status = fallback_cache_status(cache_ttl)
865
+ api_client.emit_prompt_fallback_event(key, cache_status: cache_status, error: error)
866
+ PromptFetchResult.new(
867
+ prompt: prompt_client,
868
+ logical_key: key.logical_key,
869
+ storage_key: key.storage_key,
870
+ cache_status: cache_status,
871
+ source: CacheSource::FALLBACK,
872
+ name: key.name,
873
+ version: key.version || prompt_client.version,
874
+ label: key.resolved_label
875
+ )
876
+ end
877
+
878
+ def fallback_cache_status(cache_ttl)
879
+ return CacheStatus::BYPASS if cache_ttl&.zero?
880
+ return CacheStatus::DISABLED unless api_client.cache
881
+
882
+ CacheStatus::MISS
883
+ end
884
+
741
885
  # Check if caching is enabled in configuration
742
886
  #
743
887
  # @return [Boolean]
@@ -754,11 +898,17 @@ module Langfuse
754
898
  create_memory_cache
755
899
  when :rails
756
900
  create_rails_cache_adapter
901
+ when :auto
902
+ rails_cache_available? ? create_rails_cache_adapter : create_memory_cache
757
903
  else
758
904
  raise ConfigurationError, "Unknown cache backend: #{config.cache_backend}"
759
905
  end
760
906
  end
761
907
 
908
+ def rails_cache_available?
909
+ defined?(Rails) && Rails.respond_to?(:cache) && Rails.cache
910
+ end
911
+
762
912
  # Create in-memory cache with SWR support if enabled
763
913
  #
764
914
  # @return [PromptCache]
@@ -823,9 +973,9 @@ module Langfuse
823
973
 
824
974
  case type
825
975
  when :text
826
- TextPromptClient.new(prompt_data)
976
+ TextPromptClient.new(prompt_data, is_fallback: true)
827
977
  when :chat
828
- ChatPromptClient.new(prompt_data)
978
+ ChatPromptClient.new(prompt_data, is_fallback: true)
829
979
  end
830
980
  end
831
981
 
@@ -856,7 +1006,8 @@ module Langfuse
856
1006
 
857
1007
  # Normalize prompt content for API request
858
1008
  #
859
- # Converts Ruby symbol keys to string keys for chat messages
1009
+ # Converts Ruby symbol keys to string keys for chat messages and preserves
1010
+ # Langfuse message placeholder entries.
860
1011
  #
861
1012
  # @param prompt [String, Array] The prompt content
862
1013
  # @param type [Symbol] The prompt type
@@ -866,18 +1017,28 @@ module Langfuse
866
1017
 
867
1018
  # Normalize chat messages to use string keys
868
1019
  prompt.map do |message|
869
- # Convert all keys to symbols first, then extract
870
- normalized = message.transform_keys do |k|
871
- k.to_sym
872
- rescue StandardError
873
- k
874
- end
875
- {
876
- "role" => normalized[:role]&.to_s,
877
- "content" => normalized[:content]
878
- }
1020
+ normalized = message.transform_keys(&:to_s)
1021
+ next placeholder_prompt_content(normalized) if normalized["type"] == ChatPromptClient::PLACEHOLDER_TYPE
1022
+
1023
+ normalize_chat_message_content(normalized)
879
1024
  end
880
1025
  end
1026
+
1027
+ # @api private
1028
+ def placeholder_prompt_content(message)
1029
+ {
1030
+ "type" => ChatPromptClient::PLACEHOLDER_TYPE,
1031
+ "name" => message["name"].to_s
1032
+ }
1033
+ end
1034
+
1035
+ # @api private
1036
+ def normalize_chat_message_content(message)
1037
+ message.merge(
1038
+ "role" => message["role"]&.to_s,
1039
+ "content" => message["content"]
1040
+ )
1041
+ end
881
1042
  end
882
1043
  # rubocop:enable Metrics/ClassLength
883
1044
  end
@@ -41,7 +41,7 @@ module Langfuse
41
41
  # @return [Integer] Maximum number of cached items
42
42
  attr_accessor :cache_max_size
43
43
 
44
- # @return [Symbol] Cache backend (:memory or :rails)
44
+ # @return [Symbol] Cache backend (:memory, :rails, or :auto)
45
45
  attr_accessor :cache_backend
46
46
 
47
47
  # @return [Integer] Lock timeout in seconds for distributed cache stampede protection
@@ -57,6 +57,9 @@ module Langfuse
57
57
  # @return [Integer] Number of background threads for cache refresh
58
58
  attr_accessor :cache_refresh_threads
59
59
 
60
+ # @return [#call, nil] Observer called for prompt cache events
61
+ attr_accessor :prompt_cache_observer
62
+
60
63
  # @return [Boolean] Use async processing for traces (requires ActiveJob)
61
64
  attr_accessor :tracing_async
62
65
 
@@ -158,6 +161,7 @@ module Langfuse
158
161
  @cache_stale_while_revalidate = DEFAULT_CACHE_STALE_WHILE_REVALIDATE
159
162
  @cache_stale_ttl = 0 # Default to 0 (SWR disabled, entries expire immediately after TTL)
160
163
  @cache_refresh_threads = DEFAULT_CACHE_REFRESH_THREADS
164
+ @prompt_cache_observer = nil
161
165
  @tracing_async = DEFAULT_TRACING_ASYNC
162
166
  @batch_size = DEFAULT_BATCH_SIZE
163
167
  @flush_interval = DEFAULT_FLUSH_INTERVAL
@@ -189,6 +193,7 @@ module Langfuse
189
193
  validate_swr_config!
190
194
 
191
195
  validate_cache_backend!
196
+ validate_prompt_cache_observer!
192
197
  validate_sample_rate!
193
198
  validate_should_export_span!
194
199
  validate_mask!
@@ -240,13 +245,19 @@ module Langfuse
240
245
  end
241
246
 
242
247
  def validate_cache_backend!
243
- valid_backends = %i[memory rails]
248
+ valid_backends = %i[memory rails auto]
244
249
  return if valid_backends.include?(cache_backend)
245
250
 
246
251
  raise ConfigurationError,
247
252
  "cache_backend must be one of #{valid_backends.inspect}, got #{cache_backend.inspect}"
248
253
  end
249
254
 
255
+ def validate_prompt_cache_observer!
256
+ return if prompt_cache_observer.nil? || prompt_cache_observer.respond_to?(:call)
257
+
258
+ raise ConfigurationError, "prompt_cache_observer must respond to #call"
259
+ end
260
+
250
261
  def validate_swr_config!
251
262
  validate_swr_stale_ttl!
252
263
  validate_refresh_threads!
@@ -302,11 +302,9 @@ module Langfuse
302
302
  # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
303
303
  def self.add_prompt_attributes(otel_attributes, prompt)
304
304
  return unless prompt
305
+ return if fallback_prompt?(prompt)
305
306
 
306
- # Handle hash-like prompts
307
307
  if prompt.is_a?(Hash) || prompt.respond_to?(:[])
308
- return if prompt[:is_fallback] || prompt["is_fallback"]
309
-
310
308
  otel_attributes[OBSERVATION_PROMPT_NAME] = prompt[:name] || prompt["name"]
311
309
  otel_attributes[OBSERVATION_PROMPT_VERSION] = prompt[:version] || prompt["version"]
312
310
  # Handle objects with name/version methods (already converted in Trace#generation)
@@ -315,6 +313,16 @@ module Langfuse
315
313
  otel_attributes[OBSERVATION_PROMPT_VERSION] = prompt.version
316
314
  end
317
315
  end
316
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
317
+
318
+ # @api private
319
+ def self.fallback_prompt?(prompt)
320
+ return true if prompt.respond_to?(:is_fallback) && prompt.is_fallback
321
+ return false unless prompt.is_a?(Hash)
322
+
323
+ !!get_hash_value(prompt, :is_fallback)
324
+ end
325
+ private_class_method :fallback_prompt?
318
326
  end
319
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/ModuleLength
327
+ # rubocop:enable Metrics/ModuleLength
320
328
  end