langfuse-ruby 0.2.0 → 0.2.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.
data/lib/langfuse/span.rb CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Langfuse
4
4
  class Span
5
+ include PartialUpdates
6
+
5
7
  attr_reader :id, :trace_id, :name, :start_time, :end_time, :input, :output,
6
8
  :metadata, :level, :status_message, :parent_observation_id, :version,
7
9
  :as_type, :client
@@ -31,25 +33,32 @@ module Langfuse
31
33
 
32
34
  def update(name: nil, end_time: nil, input: nil, output: nil, metadata: nil,
33
35
  level: nil, status_message: nil, version: nil, **kwargs)
34
- @name = name if name
35
- @end_time = end_time if end_time
36
- @input = input if input
37
- @output = output if output
36
+ @name = name unless name.nil?
37
+ @end_time = end_time unless end_time.nil?
38
+ @input = input unless input.nil?
39
+ @output = output unless output.nil?
38
40
  @metadata.merge!(metadata) if metadata
39
- @level = level if level
40
- @status_message = status_message if status_message
41
- @version = version if version
41
+ @level = level unless level.nil?
42
+ @status_message = status_message unless status_message.nil?
43
+ @version = version unless version.nil?
42
44
  @kwargs.merge!(kwargs)
43
45
 
46
+ track_changes(
47
+ { name: name, end_time: end_time, input: input, output: output,
48
+ metadata: metadata, level: level, status_message: status_message,
49
+ version: version },
50
+ kwargs.keys
51
+ )
44
52
  update_span
45
53
  self
46
54
  end
47
55
 
48
56
  def end(output: nil, end_time: nil, **kwargs)
49
57
  @end_time = end_time || Utils.current_timestamp
50
- @output = output if output
58
+ @output = output unless output.nil?
51
59
  @kwargs.merge!(kwargs)
52
60
 
61
+ track_changes({ end_time: @end_time, output: output }, kwargs.keys)
53
62
  update_span
54
63
  self
55
64
  end
@@ -77,6 +86,7 @@ module Langfuse
77
86
  # Create a child generation
78
87
  def generation(name: nil, start_time: nil, end_time: nil, completion_start_time: nil,
79
88
  model: nil, model_parameters: nil, input: nil, output: nil, usage: nil,
89
+ usage_details: nil, cost_details: nil, prompt: nil,
80
90
  metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
81
91
  @client.generation(
82
92
  trace_id: @trace_id,
@@ -89,6 +99,9 @@ module Langfuse
89
99
  input: input,
90
100
  output: output,
91
101
  usage: usage,
102
+ usage_details: usage_details,
103
+ cost_details: cost_details,
104
+ prompt: prompt,
92
105
  metadata: metadata,
93
106
  level: level,
94
107
  status_message: status_message,
@@ -116,79 +129,11 @@ module Langfuse
116
129
  )
117
130
  end
118
131
 
119
- # Convenience methods for enhanced observation types
120
-
121
- # Create a child agent observation
122
- def agent(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
123
- metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
124
- span(
125
- name: name,
126
- start_time: start_time,
127
- end_time: end_time,
128
- input: input,
129
- output: output,
130
- metadata: metadata,
131
- level: level,
132
- status_message: status_message,
133
- version: version,
134
- as_type: ObservationType::AGENT,
135
- **kwargs
136
- )
137
- end
138
-
139
- # Create a child tool observation
140
- def tool(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
141
- metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
142
- span(
143
- name: name,
144
- start_time: start_time,
145
- end_time: end_time,
146
- input: input,
147
- output: output,
148
- metadata: metadata,
149
- level: level,
150
- status_message: status_message,
151
- version: version,
152
- as_type: ObservationType::TOOL,
153
- **kwargs
154
- )
155
- end
156
-
157
- # Create a child chain observation
158
- def chain(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
159
- metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
160
- span(
161
- name: name,
162
- start_time: start_time,
163
- end_time: end_time,
164
- input: input,
165
- output: output,
166
- metadata: metadata,
167
- level: level,
168
- status_message: status_message,
169
- version: version,
170
- as_type: ObservationType::CHAIN,
171
- **kwargs
172
- )
173
- end
174
-
175
- # Create a child retriever observation
176
- def retriever(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
177
- metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
178
- span(
179
- name: name,
180
- start_time: start_time,
181
- end_time: end_time,
182
- input: input,
183
- output: output,
184
- metadata: metadata,
185
- level: level,
186
- status_message: status_message,
187
- version: version,
188
- as_type: ObservationType::RETRIEVER,
189
- **kwargs
190
- )
191
- end
132
+ # Convenience methods for enhanced observation types: each is a child span
133
+ # with a fixed as_type. (embedding keeps its own definition because it folds
134
+ # model/usage into metadata first.)
135
+ extend SpanWrappers
136
+ define_span_wrappers
192
137
 
193
138
  # Create a child embedding observation
194
139
  def embedding(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
@@ -212,42 +157,6 @@ module Langfuse
212
157
  )
213
158
  end
214
159
 
215
- # Create a child evaluator observation
216
- def evaluator(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
217
- metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
218
- span(
219
- name: name,
220
- start_time: start_time,
221
- end_time: end_time,
222
- input: input,
223
- output: output,
224
- metadata: metadata,
225
- level: level,
226
- status_message: status_message,
227
- version: version,
228
- as_type: ObservationType::EVALUATOR,
229
- **kwargs
230
- )
231
- end
232
-
233
- # Create a child guardrail observation
234
- def guardrail(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
235
- metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
236
- span(
237
- name: name,
238
- start_time: start_time,
239
- end_time: end_time,
240
- input: input,
241
- output: output,
242
- metadata: metadata,
243
- level: level,
244
- status_message: status_message,
245
- version: version,
246
- as_type: ObservationType::GUARDRAIL,
247
- **kwargs
248
- )
249
- end
250
-
251
160
  def score(name:, value:, data_type: nil, comment: nil, **kwargs)
252
161
  @client.score(
253
162
  trace_id: @trace_id,
@@ -295,45 +204,11 @@ module Langfuse
295
204
  end
296
205
 
297
206
  def create_span
298
- data = {
299
- id: @id,
300
- trace_id: @trace_id,
301
- name: @name,
302
- start_time: @start_time,
303
- end_time: @end_time,
304
- input: @input,
305
- output: @output,
306
- metadata: @metadata,
307
- level: @level,
308
- status_message: @status_message,
309
- parent_observation_id: @parent_observation_id,
310
- version: @version
311
- }
312
- data[:type] = @as_type if @as_type
313
- data = data.merge(@kwargs).compact
314
-
315
- @client.enqueue_event('span-create', data)
207
+ @client.enqueue_event('span-create', to_dict)
316
208
  end
317
209
 
318
210
  def update_span
319
- data = {
320
- id: @id,
321
- trace_id: @trace_id,
322
- name: @name,
323
- start_time: @start_time,
324
- end_time: @end_time,
325
- input: @input,
326
- output: @output,
327
- metadata: @metadata,
328
- level: @level,
329
- status_message: @status_message,
330
- parent_observation_id: @parent_observation_id,
331
- version: @version
332
- }
333
- data[:type] = @as_type if @as_type
334
- data = data.merge(@kwargs).compact
335
-
336
- @client.enqueue_event('span-update', data)
211
+ @client.enqueue_event('span-update', update_body)
337
212
  end
338
213
  end
339
214
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langfuse
4
+ # Defines the enhanced observation helpers (agent, tool, chain, retriever,
5
+ # evaluator, guardrail) as thin wrappers around the extending class's #span,
6
+ # each pinning a fixed as_type.
7
+ #
8
+ # `embedding` is intentionally not generated here: every wrapper folds
9
+ # model/usage into metadata before delegating, so it stays hand-written.
10
+ module SpanWrappers
11
+ TYPES = {
12
+ agent: ObservationType::AGENT,
13
+ tool: ObservationType::TOOL,
14
+ chain: ObservationType::CHAIN,
15
+ retriever: ObservationType::RETRIEVER,
16
+ evaluator: ObservationType::EVALUATOR,
17
+ guardrail: ObservationType::GUARDRAIL
18
+ }.freeze
19
+
20
+ # `evaluator_name` exists because Client exposes the helper as
21
+ # `evaluator_obs`, keeping it distinct from the Evaluators API.
22
+ def define_span_wrappers(evaluator_name: :evaluator)
23
+ TYPES.each do |name, as_type|
24
+ method_name = name == :evaluator ? evaluator_name : name
25
+
26
+ define_method(method_name) do |**kwargs|
27
+ span(**kwargs, as_type: as_type)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langfuse
4
+ # Placeholder handling shared by Prompt, PromptTemplate and ChatPromptTemplate.
5
+ # Both `{{var}}` and `{var}` are supported.
6
+ module TemplateCompiler
7
+ VARIABLE_PATTERN = /\{\{([a-zA-Z0-9_.]+)\}\}|\{([a-zA-Z0-9_.]+)\}/
8
+
9
+ class << self
10
+ # Substitutes every provided variable in a single pass, so a value that
11
+ # itself contains a placeholder is never expanded again (a value like
12
+ # "{{admin_prompt}}" stays literal). Placeholders without a matching
13
+ # variable are left untouched.
14
+ def compile(template, variables = {})
15
+ text = template.to_s
16
+ return text if variables.nil? || variables.empty?
17
+
18
+ values = variables.to_h { |key, value| [key.to_s, value.to_s] }
19
+
20
+ text.gsub(placeholder_pattern(values.keys)) do
21
+ values[Regexp.last_match(1) || Regexp.last_match(2)]
22
+ end
23
+ end
24
+
25
+ def compile_messages(messages, variables = {})
26
+ messages.map do |message|
27
+ role = message[:role] || message['role']
28
+ content = compile(message[:content] || message['content'], variables)
29
+ message.merge(role: role, content: content)
30
+ end
31
+ end
32
+
33
+ def extract_variables(text)
34
+ text.to_s.scan(VARIABLE_PATTERN).map { |double, single| double || single }.uniq
35
+ end
36
+
37
+ def extract_message_variables(messages)
38
+ messages.flat_map do |message|
39
+ content = message[:content] || message['content']
40
+ extract_variables(content)
41
+ end.uniq
42
+ end
43
+
44
+ private
45
+
46
+ # Longest name first so a `user` variable cannot shadow `{{user_name}}`.
47
+ def placeholder_pattern(names)
48
+ alternatives = names.sort_by { |name| -name.length }
49
+ .map { |name| Regexp.escape(name) }
50
+ .join('|')
51
+
52
+ /\{\{(#{alternatives})\}\}|\{(#{alternatives})\}/
53
+ end
54
+ end
55
+ end
56
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Langfuse
4
4
  class Trace
5
+ include PartialUpdates
6
+
5
7
  attr_reader :id, :name, :user_id, :session_id, :version, :release, :input, :output,
6
8
  :metadata, :tags, :timestamp, :public, :client
7
9
 
@@ -95,87 +97,11 @@ module Langfuse
95
97
  )
96
98
  end
97
99
 
98
- # Convenience methods for enhanced observation types
99
-
100
- # Create a child agent observation
101
- def agent(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
102
- metadata: nil, level: nil, status_message: nil, parent_observation_id: nil,
103
- version: nil, **kwargs)
104
- span(
105
- name: name,
106
- start_time: start_time,
107
- end_time: end_time,
108
- input: input,
109
- output: output,
110
- metadata: metadata,
111
- level: level,
112
- status_message: status_message,
113
- parent_observation_id: parent_observation_id,
114
- version: version,
115
- as_type: ObservationType::AGENT,
116
- **kwargs
117
- )
118
- end
119
-
120
- # Create a child tool observation
121
- def tool(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
122
- metadata: nil, level: nil, status_message: nil, parent_observation_id: nil,
123
- version: nil, **kwargs)
124
- span(
125
- name: name,
126
- start_time: start_time,
127
- end_time: end_time,
128
- input: input,
129
- output: output,
130
- metadata: metadata,
131
- level: level,
132
- status_message: status_message,
133
- parent_observation_id: parent_observation_id,
134
- version: version,
135
- as_type: ObservationType::TOOL,
136
- **kwargs
137
- )
138
- end
139
-
140
- # Create a child chain observation
141
- def chain(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
142
- metadata: nil, level: nil, status_message: nil, parent_observation_id: nil,
143
- version: nil, **kwargs)
144
- span(
145
- name: name,
146
- start_time: start_time,
147
- end_time: end_time,
148
- input: input,
149
- output: output,
150
- metadata: metadata,
151
- level: level,
152
- status_message: status_message,
153
- parent_observation_id: parent_observation_id,
154
- version: version,
155
- as_type: ObservationType::CHAIN,
156
- **kwargs
157
- )
158
- end
159
-
160
- # Create a child retriever observation
161
- def retriever(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
162
- metadata: nil, level: nil, status_message: nil, parent_observation_id: nil,
163
- version: nil, **kwargs)
164
- span(
165
- name: name,
166
- start_time: start_time,
167
- end_time: end_time,
168
- input: input,
169
- output: output,
170
- metadata: metadata,
171
- level: level,
172
- status_message: status_message,
173
- parent_observation_id: parent_observation_id,
174
- version: version,
175
- as_type: ObservationType::RETRIEVER,
176
- **kwargs
177
- )
178
- end
100
+ # Convenience methods for enhanced observation types: each is a child span
101
+ # with a fixed as_type. (embedding keeps its own definition because it folds
102
+ # model/usage into metadata first.)
103
+ extend SpanWrappers
104
+ define_span_wrappers
179
105
 
180
106
  # Create a child embedding observation
181
107
  def embedding(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
@@ -200,46 +126,6 @@ module Langfuse
200
126
  )
201
127
  end
202
128
 
203
- # Create a child evaluator observation
204
- def evaluator(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
205
- metadata: nil, level: nil, status_message: nil, parent_observation_id: nil,
206
- version: nil, **kwargs)
207
- span(
208
- name: name,
209
- start_time: start_time,
210
- end_time: end_time,
211
- input: input,
212
- output: output,
213
- metadata: metadata,
214
- level: level,
215
- status_message: status_message,
216
- parent_observation_id: parent_observation_id,
217
- version: version,
218
- as_type: ObservationType::EVALUATOR,
219
- **kwargs
220
- )
221
- end
222
-
223
- # Create a child guardrail observation
224
- def guardrail(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
225
- metadata: nil, level: nil, status_message: nil, parent_observation_id: nil,
226
- version: nil, **kwargs)
227
- span(
228
- name: name,
229
- start_time: start_time,
230
- end_time: end_time,
231
- input: input,
232
- output: output,
233
- metadata: metadata,
234
- level: level,
235
- status_message: status_message,
236
- parent_observation_id: parent_observation_id,
237
- version: version,
238
- as_type: ObservationType::GUARDRAIL,
239
- **kwargs
240
- )
241
- end
242
-
243
129
  def score(name:, value:, data_type: nil, comment: nil, **kwargs)
244
130
  @client.score(
245
131
  trace_id: @id,
@@ -255,19 +141,27 @@ module Langfuse
255
141
  release: nil, input: nil, output: nil, metadata: nil, tags: nil,
256
142
  public: nil, **kwargs)
257
143
  # 更新实例变量
258
- @name = name if name
259
- @user_id = user_id if user_id
260
- @session_id = session_id if session_id
261
- @version = version if version
262
- @release = release if release
263
- @input = input if input
264
- @output = output if output
265
- @metadata = metadata if metadata
266
- @tags = tags if tags
144
+ @name = name unless name.nil?
145
+ @user_id = user_id unless user_id.nil?
146
+ @session_id = session_id unless session_id.nil?
147
+ @version = version unless version.nil?
148
+ @release = release unless release.nil?
149
+ @input = input unless input.nil?
150
+ @output = output unless output.nil?
151
+ @metadata.merge!(metadata) if metadata
152
+ @tags = tags unless tags.nil?
267
153
  @public = public unless public.nil?
268
154
  @kwargs.merge!(kwargs) if kwargs.any?
155
+
156
+ track_changes(
157
+ { name: name, user_id: user_id, session_id: session_id, version: version,
158
+ release: release, input: input, output: output, metadata: metadata,
159
+ tags: tags, public: public },
160
+ kwargs.keys
161
+ )
269
162
  # 触发 trace-update 事件
270
163
  update_trace
164
+ self
271
165
  end
272
166
 
273
167
  def get_url
@@ -294,41 +188,11 @@ module Langfuse
294
188
  private
295
189
 
296
190
  def create_trace
297
- data = {
298
- id: @id,
299
- name: @name,
300
- user_id: @user_id,
301
- session_id: @session_id,
302
- version: @version,
303
- release: @release,
304
- input: @input,
305
- output: @output,
306
- metadata: @metadata,
307
- tags: @tags,
308
- timestamp: @timestamp,
309
- public: @public
310
- }.merge(@kwargs).compact
311
-
312
- @client.enqueue_event('trace-create', data)
191
+ @client.enqueue_event('trace-create', to_dict)
313
192
  end
314
193
 
315
194
  def update_trace
316
- data = {
317
- id: @id,
318
- name: @name,
319
- user_id: @user_id,
320
- session_id: @session_id,
321
- version: @version,
322
- release: @release,
323
- input: @input,
324
- output: @output,
325
- metadata: @metadata,
326
- tags: @tags,
327
- timestamp: @timestamp,
328
- public: @public
329
- }.merge(@kwargs).compact
330
-
331
- @client.enqueue_event('trace-update', data)
195
+ @client.enqueue_event('trace-update', update_body, trace_ref: self)
332
196
  end
333
197
  end
334
198
  end
@@ -33,26 +33,39 @@ module Langfuse
33
33
  ERB::Util.url_encode(string.to_s)
34
34
  end
35
35
 
36
- def deep_symbolize_keys(hash)
37
- return hash unless hash.is_a?(Hash)
38
-
39
- hash.each_with_object({}) do |(key, value), result|
40
- new_key = key.is_a?(String) ? key.to_sym : key
41
- new_value = value.is_a?(Hash) ? deep_symbolize_keys(value) : value
42
- result[new_key] = new_value
36
+ def deep_symbolize_keys(object)
37
+ case object
38
+ when Hash
39
+ object.each_with_object({}) do |(key, value), result|
40
+ new_key = key.is_a?(String) ? key.to_sym : key
41
+ result[new_key] = deep_symbolize_keys(value)
42
+ end
43
+ when Array
44
+ object.map { |item| deep_symbolize_keys(item) }
45
+ else
46
+ object
43
47
  end
44
48
  end
45
49
 
46
- def deep_stringify_keys(hash)
47
- return hash unless hash.is_a?(Hash)
48
-
49
- hash.each_with_object({}) do |(key, value), result|
50
- new_key = camelize_key(key.to_s)
51
- new_value = value.is_a?(Hash) ? deep_stringify_keys(value) : value
52
- result[new_key] = new_value
50
+ # 将哈希的键名转换为小驼峰格式
51
+ def deep_camelize_keys(object)
52
+ case object
53
+ when Hash
54
+ object.each_with_object({}) do |(key, value), result|
55
+ new_key = camelize_key(key.to_s)
56
+ result[new_key] = deep_camelize_keys(value)
57
+ end
58
+ when Array
59
+ object.map { |item| deep_camelize_keys(item) }
60
+ else
61
+ object
53
62
  end
54
63
  end
55
64
 
65
+ # Kept for backwards compatibility: it has always camelized keys rather
66
+ # than only stringifying them.
67
+ alias deep_stringify_keys deep_camelize_keys
68
+
56
69
  # Prepare an event body for the ingestion API:
57
70
  # - top-level keys are camelized (snake_case -> camelCase)
58
71
  # - user data values (input/output/metadata/usageDetails/costDetails/modelParameters)
@@ -64,24 +77,13 @@ module Langfuse
64
77
  body.each_with_object({}) do |(key, value), result|
65
78
  new_key = camelize_key(key.to_s)
66
79
  result[new_key] = if !VERBATIM_BODY_KEYS.include?(new_key) && value.is_a?(Hash)
67
- deep_stringify_keys(value)
80
+ deep_camelize_keys(value)
68
81
  else
69
82
  value
70
83
  end
71
84
  end
72
85
  end
73
86
 
74
- # 将哈希的键名转换为小驼峰格式
75
- def deep_camelize_keys(hash)
76
- return hash unless hash.is_a?(Hash)
77
-
78
- hash.each_with_object({}) do |(key, value), result|
79
- new_key = camelize_key(key.to_s)
80
- new_value = value.is_a?(Hash) ? deep_camelize_keys(value) : value
81
- result[new_key] = new_value
82
- end
83
- end
84
-
85
87
  private
86
88
 
87
89
  # 将蛇形命名转换为小驼峰命名
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Langfuse
4
- VERSION = '0.2.0'
4
+ VERSION = '0.2.1'
5
5
  end