langfuse-rb 0.8.0 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba92036fbe7b63b8355a113e44ff3f62cfcb11817ae3b3c1c444756af55ebf84
4
- data.tar.gz: 44097d4d441ad6d2d8780a95cb836d0368ed0b772f16036e301ece7e018df0a1
3
+ metadata.gz: 1eedf02269727792fdb13a343429d2108556cc4c7df52dea9e2c638a011409a0
4
+ data.tar.gz: 0130f17065fa3e4233090e858a2699105d9be48dce85d6765b0c3ee394c88bff
5
5
  SHA512:
6
- metadata.gz: 6e8880c58683dc7ff719f0c966515841e9a3e3df024f88175297090949a6befaddf23528b24a65d3825fa40e33118348087c0dbe5312543c16bfef65856eaf12
7
- data.tar.gz: a82f8be469125e99355c5b6a1bd747a1f2e395dd6e0eb098f8319a0976419dfd60087c6252e26c2570db15994ed084d3156069019541c3b32acc7f1827c92792
6
+ metadata.gz: 036b34ce7908114bdad51bf895c5f50159f005bbf53f3256a347dc7a6c813d9c49d18c9f7491dc42c8771d66fb6b509ae1441af6812d575ff45b80fe4e73ce6e
7
+ data.tar.gz: c4cf129fd0260c35f91e75797903d29e16d74b0bf543a89d9f566201a69e08eaf6245fc9d117e5f2f0e3bad5673cfee5cd54e0802d46a53809ab9c5ea825f172
data/CHANGELOG.md CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.0] - 2026-04-28
11
+
12
+ ### Added
13
+ - Expose `type`, `commit_message`, and `resolution_graph` metadata on text and chat prompt clients (#87)
14
+
15
+ ### Fixed
16
+ - Preserve and compile chat prompt message placeholders in parity with Langfuse Python and JS SDKs (#86)
17
+ - Preserve raw prompt compile variables instead of HTML-escaping JSON, XML, and HTML-like values (#85)
18
+ - Suppress prompt name/version attribution on fallback prompt clients so fallback output is not reported as prompt version 0 (#84)
19
+
20
+ ### Documentation
21
+ - Link to upstream Langfuse agent skills and refresh README header image (#81, #83)
22
+
10
23
  ## [0.8.0] - 2026-04-24
11
24
 
12
25
  ### Added
@@ -100,7 +113,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
100
113
  - Migrated from legacy ingestion API to OTLP endpoint
101
114
  - Removed `tracing_enabled` configuration flag (#2)
102
115
 
103
- [Unreleased]: https://github.com/simplepractice/langfuse-rb/compare/v0.8.0...HEAD
116
+ [Unreleased]: https://github.com/simplepractice/langfuse-rb/compare/v0.9.0...HEAD
117
+ [0.9.0]: https://github.com/simplepractice/langfuse-rb/compare/v0.8.0...v0.9.0
104
118
  [0.8.0]: https://github.com/simplepractice/langfuse-rb/compare/v0.7.0...v0.8.0
105
119
  [0.7.0]: https://github.com/simplepractice/langfuse-rb/compare/v0.6.0...v0.7.0
106
120
  [0.6.0]: https://github.com/simplepractice/langfuse-rb/compare/v0.5.0...v0.6.0
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
- ![header](https://camo.githubusercontent.com/26d19b945bc752101b4aca468e07b118a44af07340db79af29f7df95505f2cea/68747470733a2f2f6c616e67667573652e636f6d2f6c616e67667573655f6c6f676f5f77686974652e706e67)
1
+ <img width="2255" height="527" alt="langfuse-wordart" src="https://github.com/user-attachments/assets/59422d0a-6ecb-4e5f-a21c-cae955b5ce75" />
2
+
2
3
 
3
4
  # Langfuse Ruby SDK
4
5
 
@@ -68,6 +69,8 @@ end
68
69
  - [Tracing](docs/TRACING.md)
69
70
  - [Scoring](docs/SCORING.md)
70
71
  - [Rails Patterns](docs/RAILS.md)
72
+ - [Agent Skills](https://github.com/langfuse/skills)
73
+ - [Agent Skill Docs](https://langfuse.com/docs/api-and-data-platform/features/agent-skill)
71
74
 
72
75
  ## License
73
76
 
@@ -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
@@ -823,9 +823,9 @@ module Langfuse
823
823
 
824
824
  case type
825
825
  when :text
826
- TextPromptClient.new(prompt_data)
826
+ TextPromptClient.new(prompt_data, is_fallback: true)
827
827
  when :chat
828
- ChatPromptClient.new(prompt_data)
828
+ ChatPromptClient.new(prompt_data, is_fallback: true)
829
829
  end
830
830
  end
831
831
 
@@ -856,7 +856,8 @@ module Langfuse
856
856
 
857
857
  # Normalize prompt content for API request
858
858
  #
859
- # Converts Ruby symbol keys to string keys for chat messages
859
+ # Converts Ruby symbol keys to string keys for chat messages and preserves
860
+ # Langfuse message placeholder entries.
860
861
  #
861
862
  # @param prompt [String, Array] The prompt content
862
863
  # @param type [Symbol] The prompt type
@@ -866,18 +867,28 @@ module Langfuse
866
867
 
867
868
  # Normalize chat messages to use string keys
868
869
  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
- }
870
+ normalized = message.transform_keys(&:to_s)
871
+ next placeholder_prompt_content(normalized) if normalized["type"] == ChatPromptClient::PLACEHOLDER_TYPE
872
+
873
+ normalize_chat_message_content(normalized)
879
874
  end
880
875
  end
876
+
877
+ # @api private
878
+ def placeholder_prompt_content(message)
879
+ {
880
+ "type" => ChatPromptClient::PLACEHOLDER_TYPE,
881
+ "name" => message["name"].to_s
882
+ }
883
+ end
884
+
885
+ # @api private
886
+ def normalize_chat_message_content(message)
887
+ message.merge(
888
+ "role" => message["role"]&.to_s,
889
+ "content" => message["content"]
890
+ )
891
+ end
881
892
  end
882
893
  # rubocop:enable Metrics/ClassLength
883
894
  end
@@ -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
@@ -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
@@ -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
  # Text prompt client for compiling text prompts with variable substitution
@@ -38,11 +38,21 @@ module Langfuse
38
38
  # @return [String] Raw prompt template string
39
39
  attr_reader :prompt
40
40
 
41
+ # @return [String, nil] Optional commit message for this prompt version
42
+ attr_reader :commit_message
43
+
44
+ # @return [Hash, nil] Optional dependency resolution graph for composed prompts
45
+ attr_reader :resolution_graph
46
+
47
+ # @return [Boolean] Whether this client uses caller-provided fallback content
48
+ attr_reader :is_fallback
49
+
41
50
  # Initialize a new text prompt client
42
51
  #
43
52
  # @param prompt_data [Hash] The prompt data from the API
53
+ # @param is_fallback [Boolean] Whether this client wraps caller-provided fallback content
44
54
  # @raise [ArgumentError] if prompt data is invalid
45
- def initialize(prompt_data)
55
+ def initialize(prompt_data, is_fallback: false)
46
56
  validate_prompt_data!(prompt_data)
47
57
 
48
58
  @name = prompt_data["name"]
@@ -51,6 +61,14 @@ module Langfuse
51
61
  @labels = prompt_data["labels"] || []
52
62
  @tags = prompt_data["tags"] || []
53
63
  @config = prompt_data["config"] || {}
64
+ @commit_message = prompt_data["commitMessage"]
65
+ @resolution_graph = prompt_data["resolutionGraph"]
66
+ @is_fallback = is_fallback
67
+ end
68
+
69
+ # @return [String] Prompt type ("text")
70
+ def type
71
+ "text"
54
72
  end
55
73
 
56
74
  # Compile the prompt with variable substitution
@@ -64,7 +82,7 @@ module Langfuse
64
82
  def compile(**kwargs)
65
83
  return prompt if kwargs.empty?
66
84
 
67
- Mustache.render(prompt, kwargs)
85
+ PromptRenderer.render(prompt, kwargs)
68
86
  end
69
87
 
70
88
  private
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Langfuse
4
- VERSION = "0.8.0"
4
+ VERSION = "0.9.0"
5
5
  end
data/lib/langfuse.rb CHANGED
@@ -54,6 +54,7 @@ require_relative "langfuse/span_processor"
54
54
  require_relative "langfuse/observations"
55
55
  require_relative "langfuse/trace_id"
56
56
  require_relative "langfuse/score_client"
57
+ require_relative "langfuse/prompt_renderer"
57
58
  require_relative "langfuse/text_prompt_client"
58
59
  require_relative "langfuse/chat_prompt_client"
59
60
  require_relative "langfuse/timestamp_parser"
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: langfuse-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SimplePractice
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-04-24 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: faraday
@@ -177,6 +176,7 @@ files:
177
176
  - lib/langfuse/otel_attributes.rb
178
177
  - lib/langfuse/otel_setup.rb
179
178
  - lib/langfuse/prompt_cache.rb
179
+ - lib/langfuse/prompt_renderer.rb
180
180
  - lib/langfuse/propagation.rb
181
181
  - lib/langfuse/rails_cache_adapter.rb
182
182
  - lib/langfuse/sampling.rb
@@ -198,7 +198,6 @@ metadata:
198
198
  source_code_uri: https://github.com/simplepractice/langfuse-rb
199
199
  changelog_uri: https://github.com/simplepractice/langfuse-rb/blob/main/CHANGELOG.md
200
200
  rubygems_mfa_required: 'true'
201
- post_install_message:
202
201
  rdoc_options: []
203
202
  require_paths:
204
203
  - lib
@@ -213,8 +212,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
213
212
  - !ruby/object:Gem::Version
214
213
  version: '0'
215
214
  requirements: []
216
- rubygems_version: 3.4.1
217
- signing_key:
215
+ rubygems_version: 4.0.8
218
216
  specification_version: 4
219
217
  summary: Ruby SDK for Langfuse - LLM observability and prompt management
220
218
  test_files: []