langfuse-ruby 0.1.7 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +21 -10
- data/.github/workflows/release.yml +2 -2
- data/.mise.toml +8 -0
- data/.rubocop.yml +2 -2
- data/CHANGELOG.md +78 -10
- data/CLAUDE.md +54 -16
- data/Gemfile +1 -0
- data/Gemfile.lock +15 -5
- data/Makefile +6 -5
- data/README.md +338 -89
- data/Rakefile +0 -6
- data/docs/FINAL_SUMMARY.md +12 -185
- data/docs/PUBLISH_GUIDE.md +36 -272
- data/docs/README.md +12 -22
- data/docs/RELEASE_CHECKLIST.md +20 -145
- data/docs/V4.md +159 -0
- data/examples/basic_tracing.rb +11 -10
- data/examples/simplified_usage.rb +7 -6
- data/examples/v4_otel_tracing.rb +70 -0
- data/langfuse-ruby.gemspec +2 -0
- data/lib/langfuse/client.rb +757 -276
- data/lib/langfuse/event.rb +1 -17
- data/lib/langfuse/generation.rb +71 -124
- data/lib/langfuse/null_objects.rb +4 -0
- data/lib/langfuse/otel_exporter.rb +145 -63
- data/lib/langfuse/partial_updates.rb +30 -0
- data/lib/langfuse/prompt.rb +9 -83
- data/lib/langfuse/prompt_cache.rb +65 -0
- data/lib/langfuse/span.rb +29 -153
- data/lib/langfuse/span_wrappers.rb +32 -0
- data/lib/langfuse/template_compiler.rb +56 -0
- data/lib/langfuse/trace.rb +38 -164
- data/lib/langfuse/utils.rb +54 -20
- data/lib/langfuse/version.rb +1 -1
- data/lib/langfuse.rb +61 -56
- data/scripts/release.sh +12 -12
- metadata +37 -2
data/lib/langfuse/prompt.rb
CHANGED
|
@@ -64,7 +64,7 @@ module Langfuse
|
|
|
64
64
|
# Convert text prompt to LangChain PromptTemplate format
|
|
65
65
|
{
|
|
66
66
|
_type: 'prompt',
|
|
67
|
-
input_variables: extract_variables(@prompt),
|
|
67
|
+
input_variables: TemplateCompiler.extract_variables(@prompt),
|
|
68
68
|
template: @prompt
|
|
69
69
|
}
|
|
70
70
|
end
|
|
@@ -75,7 +75,7 @@ module Langfuse
|
|
|
75
75
|
{
|
|
76
76
|
_type: "#{message[:role]}_message",
|
|
77
77
|
content: message[:content],
|
|
78
|
-
input_variables: extract_variables(message[:content])
|
|
78
|
+
input_variables: TemplateCompiler.extract_variables(message[:content])
|
|
79
79
|
}
|
|
80
80
|
end
|
|
81
81
|
|
|
@@ -87,44 +87,11 @@ module Langfuse
|
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
def compile_text_prompt(variables)
|
|
90
|
-
|
|
91
|
-
variables.each do |key, value|
|
|
92
|
-
compiled.gsub!("{{#{key}}}", value.to_s)
|
|
93
|
-
compiled.gsub!("{#{key}}", value.to_s)
|
|
94
|
-
end
|
|
95
|
-
compiled
|
|
90
|
+
TemplateCompiler.compile(@prompt, variables)
|
|
96
91
|
end
|
|
97
92
|
|
|
98
93
|
def compile_chat_prompt(variables)
|
|
99
|
-
@prompt
|
|
100
|
-
compiled_content = message[:content].dup
|
|
101
|
-
variables.each do |key, value|
|
|
102
|
-
compiled_content.gsub!("{{#{key}}}", value.to_s)
|
|
103
|
-
compiled_content.gsub!("{#{key}}", value.to_s)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
{
|
|
107
|
-
role: message[:role],
|
|
108
|
-
content: compiled_content
|
|
109
|
-
}
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def extract_variables(text)
|
|
114
|
-
# Extract variables from template text (supports {{var}} and {var} formats)
|
|
115
|
-
variables = []
|
|
116
|
-
|
|
117
|
-
# Match {{variable}} format
|
|
118
|
-
text.scan(/\{\{(\w+)\}\}/) do |match|
|
|
119
|
-
variables << match[0]
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Match {variable} format
|
|
123
|
-
text.scan(/\{(\w+)\}/) do |match|
|
|
124
|
-
variables << match[0] unless variables.include?(match[0])
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
variables
|
|
94
|
+
TemplateCompiler.compile_messages(@prompt, variables)
|
|
128
95
|
end
|
|
129
96
|
end
|
|
130
97
|
|
|
@@ -137,33 +104,15 @@ module Langfuse
|
|
|
137
104
|
end
|
|
138
105
|
|
|
139
106
|
def format(variables = {})
|
|
140
|
-
|
|
141
|
-
variables.each do |key, value|
|
|
142
|
-
compiled.gsub!("{{#{key}}}", value.to_s)
|
|
143
|
-
compiled.gsub!("{#{key}}", value.to_s)
|
|
144
|
-
end
|
|
145
|
-
compiled
|
|
107
|
+
TemplateCompiler.compile(@template, variables)
|
|
146
108
|
end
|
|
147
109
|
|
|
148
110
|
def self.from_template(template)
|
|
149
|
-
|
|
150
|
-
new(template: template, input_variables: variables)
|
|
111
|
+
new(template: template, input_variables: extract_variables(template))
|
|
151
112
|
end
|
|
152
113
|
|
|
153
114
|
def self.extract_variables(text)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
# Match {{variable}} format
|
|
157
|
-
text.scan(/\{\{(\w+)\}\}/) do |match|
|
|
158
|
-
variables << match[0]
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
# Match {variable} format
|
|
162
|
-
text.scan(/\{(\w+)\}/) do |match|
|
|
163
|
-
variables << match[0] unless variables.include?(match[0])
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
variables
|
|
115
|
+
TemplateCompiler.extract_variables(text)
|
|
167
116
|
end
|
|
168
117
|
end
|
|
169
118
|
|
|
@@ -176,34 +125,11 @@ module Langfuse
|
|
|
176
125
|
end
|
|
177
126
|
|
|
178
127
|
def format(variables = {})
|
|
179
|
-
@messages
|
|
180
|
-
compiled_content = message[:content].dup
|
|
181
|
-
variables.each do |key, value|
|
|
182
|
-
compiled_content.gsub!("{{#{key}}}", value.to_s)
|
|
183
|
-
compiled_content.gsub!("{#{key}}", value.to_s)
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
{
|
|
187
|
-
role: message[:role],
|
|
188
|
-
content: compiled_content
|
|
189
|
-
}
|
|
190
|
-
end
|
|
128
|
+
TemplateCompiler.compile_messages(@messages, variables)
|
|
191
129
|
end
|
|
192
130
|
|
|
193
131
|
def self.from_messages(messages)
|
|
194
|
-
input_variables
|
|
195
|
-
|
|
196
|
-
messages.each do |message|
|
|
197
|
-
message[:content].scan(/\{\{(\w+)\}\}/) do |match|
|
|
198
|
-
input_variables << match[0] unless input_variables.include?(match[0])
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
message[:content].scan(/\{(\w+)\}/) do |match|
|
|
202
|
-
input_variables << match[0] unless input_variables.include?(match[0])
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
new(messages: messages, input_variables: input_variables)
|
|
132
|
+
new(messages: messages, input_variables: TemplateCompiler.extract_message_variables(messages))
|
|
207
133
|
end
|
|
208
134
|
end
|
|
209
135
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'concurrent'
|
|
4
|
+
|
|
5
|
+
module Langfuse
|
|
6
|
+
# Bounded, thread-safe cache of Prompt objects, keyed by name/version/label.
|
|
7
|
+
#
|
|
8
|
+
# TTLs are measured on the monotonic clock so wall-clock jumps (NTP, DST) cannot
|
|
9
|
+
# extend or shorten an entry's lifetime. Entries keep insertion order and the
|
|
10
|
+
# oldest ones are evicted once the cache is full, so a long-running process with
|
|
11
|
+
# many prompt names cannot grow it without bound.
|
|
12
|
+
class PromptCache
|
|
13
|
+
DEFAULT_MAX_ENTRIES = 200
|
|
14
|
+
|
|
15
|
+
Entry = Struct.new(:prompt, :cached_at) do
|
|
16
|
+
def fresh?(ttl_seconds, now)
|
|
17
|
+
return false if ttl_seconds.nil?
|
|
18
|
+
|
|
19
|
+
now - cached_at < ttl_seconds
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(max_entries: DEFAULT_MAX_ENTRIES)
|
|
24
|
+
@max_entries = [max_entries.to_i, 0].max
|
|
25
|
+
@entries = Concurrent::Hash.new
|
|
26
|
+
@mutex = Mutex.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The cached prompt while it is still fresh, otherwise nil.
|
|
30
|
+
def read(key, ttl_seconds)
|
|
31
|
+
entry = @entries[key]
|
|
32
|
+
return nil unless entry&.fresh?(ttl_seconds, monotonic_time)
|
|
33
|
+
|
|
34
|
+
entry.prompt
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The cached prompt regardless of age. Used to keep serving prompts while the
|
|
38
|
+
# Langfuse API is unreachable.
|
|
39
|
+
def read_stale(key)
|
|
40
|
+
@entries[key]&.prompt
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Writes are serialized so concurrent fetches cannot corrupt the hash while it
|
|
44
|
+
# is being trimmed. Returns the prompt for convenient chaining.
|
|
45
|
+
def write(key, prompt)
|
|
46
|
+
@mutex.synchronize do
|
|
47
|
+
@entries.delete(key)
|
|
48
|
+
@entries[key] = Entry.new(prompt, monotonic_time)
|
|
49
|
+
@entries.shift while @entries.length > @max_entries && !@entries.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
prompt
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def length
|
|
56
|
+
@entries.length
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def monotonic_time
|
|
62
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
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
|
|
35
|
-
@end_time = end_time
|
|
36
|
-
@input = input
|
|
37
|
-
@output = 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
|
|
40
|
-
@status_message = status_message
|
|
41
|
-
@version = 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
|
|
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
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
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,44 +157,9 @@ 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(
|
|
162
|
+
trace_id: @trace_id,
|
|
253
163
|
observation_id: @id,
|
|
254
164
|
name: name,
|
|
255
165
|
value: value,
|
|
@@ -294,45 +204,11 @@ module Langfuse
|
|
|
294
204
|
end
|
|
295
205
|
|
|
296
206
|
def create_span
|
|
297
|
-
|
|
298
|
-
id: @id,
|
|
299
|
-
trace_id: @trace_id,
|
|
300
|
-
name: @name,
|
|
301
|
-
start_time: @start_time,
|
|
302
|
-
end_time: @end_time,
|
|
303
|
-
input: @input,
|
|
304
|
-
output: @output,
|
|
305
|
-
metadata: @metadata,
|
|
306
|
-
level: @level,
|
|
307
|
-
status_message: @status_message,
|
|
308
|
-
parent_observation_id: @parent_observation_id,
|
|
309
|
-
version: @version
|
|
310
|
-
}
|
|
311
|
-
data[:type] = @as_type if @as_type
|
|
312
|
-
data = data.merge(@kwargs).compact
|
|
313
|
-
|
|
314
|
-
@client.enqueue_event('span-create', data)
|
|
207
|
+
@client.enqueue_event('span-create', to_dict)
|
|
315
208
|
end
|
|
316
209
|
|
|
317
210
|
def update_span
|
|
318
|
-
|
|
319
|
-
id: @id,
|
|
320
|
-
trace_id: @trace_id,
|
|
321
|
-
name: @name,
|
|
322
|
-
start_time: @start_time,
|
|
323
|
-
end_time: @end_time,
|
|
324
|
-
input: @input,
|
|
325
|
-
output: @output,
|
|
326
|
-
metadata: @metadata,
|
|
327
|
-
level: @level,
|
|
328
|
-
status_message: @status_message,
|
|
329
|
-
parent_observation_id: @parent_observation_id,
|
|
330
|
-
version: @version
|
|
331
|
-
}
|
|
332
|
-
data[:type] = @as_type if @as_type
|
|
333
|
-
data = data.merge(@kwargs).compact
|
|
334
|
-
|
|
335
|
-
@client.enqueue_event('span-update', data)
|
|
211
|
+
@client.enqueue_event('span-update', update_body)
|
|
336
212
|
end
|
|
337
213
|
end
|
|
338
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
|