langfuse-ruby 0.1.4 → 0.1.6

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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Langfuse
2
4
  class Evaluation
3
5
  attr_reader :id, :name, :value, :data_type, :comment, :trace_id, :observation_id, :created_at
@@ -84,10 +86,10 @@ module Langfuse
84
86
 
85
87
  class ExactMatchEvaluator < BaseEvaluator
86
88
  def initialize(name: 'exact_match', description: 'Exact match evaluator')
87
- super(name: name, description: description)
89
+ super
88
90
  end
89
91
 
90
- def evaluate(input, output, expected: nil, context: nil)
92
+ def evaluate(_input, output, expected: nil, context: nil)
91
93
  return create_score(value: 0, comment: 'No expected value provided') unless expected
92
94
 
93
95
  score = output.to_s.strip == expected.to_s.strip ? 1 : 0
@@ -104,7 +106,7 @@ module Langfuse
104
106
  @case_sensitive = case_sensitive
105
107
  end
106
108
 
107
- def evaluate(input, output, expected: nil, context: nil)
109
+ def evaluate(_input, output, expected: nil, context: nil)
108
110
  return create_score(value: 0, comment: 'No expected value provided') unless expected
109
111
 
110
112
  output_str = @case_sensitive ? output.to_s : output.to_s.downcase
@@ -125,11 +127,11 @@ module Langfuse
125
127
  @max_length = max_length
126
128
  end
127
129
 
128
- def evaluate(input, output, expected: nil, context: nil)
130
+ def evaluate(_input, output, expected: nil, context: nil)
129
131
  length = output.to_s.length
130
132
 
131
133
  if @min_length && @max_length
132
- score = length >= @min_length && length <= @max_length ? 1 : 0
134
+ score = length.between?(@min_length, @max_length) ? 1 : 0
133
135
  comment = score == 1 ? "Length #{length} within range" : "Length #{length} outside range #{@min_length}-#{@max_length}"
134
136
  elsif @min_length
135
137
  score = length >= @min_length ? 1 : 0
@@ -156,7 +158,7 @@ module Langfuse
156
158
  @pattern = pattern.is_a?(Regexp) ? pattern : Regexp.new(pattern)
157
159
  end
158
160
 
159
- def evaluate(input, output, expected: nil, context: nil)
161
+ def evaluate(_input, output, expected: nil, context: nil)
160
162
  match = @pattern.match(output.to_s)
161
163
  score = match ? 1 : 0
162
164
 
@@ -169,10 +171,10 @@ module Langfuse
169
171
 
170
172
  class SimilarityEvaluator < BaseEvaluator
171
173
  def initialize(name: 'similarity', description: 'Similarity evaluator')
172
- super(name: name, description: description)
174
+ super
173
175
  end
174
176
 
175
- def evaluate(input, output, expected: nil, context: nil)
177
+ def evaluate(_input, output, expected: nil, context: nil)
176
178
  return create_score(value: 0, comment: 'No expected value provided') unless expected
177
179
 
178
180
  # Simple character-based similarity (Levenshtein distance)
@@ -230,10 +232,10 @@ module Langfuse
230
232
  def evaluate(input, output, expected: nil, context: nil)
231
233
  # This is a placeholder for LLM-based evaluation
232
234
  # In a real implementation, you would call an LLM API here
233
- prompt = @prompt_template.gsub('{input}', input.to_s)
234
- .gsub('{output}', output.to_s)
235
- .gsub('{expected}', expected.to_s)
236
- .gsub('{context}', context.to_s)
235
+ @prompt_template.gsub('{input}', input.to_s)
236
+ .gsub('{output}', output.to_s)
237
+ .gsub('{expected}', expected.to_s)
238
+ .gsub('{context}', context.to_s)
237
239
 
238
240
  # Simulate LLM response (in real implementation, call actual LLM)
239
241
  score = rand(0.0..1.0).round(2)
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Langfuse
2
4
  class Event
3
5
  attr_reader :id, :trace_id, :name, :start_time, :input, :output, :metadata,
4
- :level, :status_message, :parent_observation_id, :version, :client
6
+ :level, :status_message, :parent_observation_id, :version, :as_type, :client
5
7
 
6
8
  def initialize(client:, trace_id:, name:, id: nil, start_time: nil, input: nil,
7
9
  output: nil, metadata: nil, level: nil, status_message: nil,
8
- parent_observation_id: nil, version: nil, **kwargs)
10
+ parent_observation_id: nil, version: nil, as_type: nil, **kwargs)
9
11
  @client = client
10
12
  @id = id || Utils.generate_id
11
13
  @trace_id = trace_id
@@ -18,6 +20,7 @@ module Langfuse
18
20
  @status_message = status_message
19
21
  @parent_observation_id = parent_observation_id
20
22
  @version = version
23
+ @as_type = validate_as_type(as_type)
21
24
  @kwargs = kwargs
22
25
 
23
26
  # Create the event
@@ -25,7 +28,7 @@ module Langfuse
25
28
  end
26
29
 
27
30
  def to_dict
28
- {
31
+ data = {
29
32
  id: @id,
30
33
  trace_id: @trace_id,
31
34
  name: @name,
@@ -37,11 +40,22 @@ module Langfuse
37
40
  status_message: @status_message,
38
41
  parent_observation_id: @parent_observation_id,
39
42
  version: @version
40
- }.merge(@kwargs).compact
43
+ }
44
+ data[:type] = @as_type if @as_type
45
+ data.merge(@kwargs).compact
41
46
  end
42
47
 
43
48
  private
44
49
 
50
+ def validate_as_type(type)
51
+ return nil if type.nil?
52
+
53
+ type_str = type.to_s
54
+ raise ValidationError, "Invalid observation type: #{type}. Valid types are: #{ObservationType::ALL.join(', ')}" unless ObservationType.valid?(type_str)
55
+
56
+ type_str
57
+ end
58
+
45
59
  def create_event
46
60
  data = {
47
61
  id: @id,
@@ -55,7 +69,9 @@ module Langfuse
55
69
  status_message: @status_message,
56
70
  parent_observation_id: @parent_observation_id,
57
71
  version: @version
58
- }.merge(@kwargs).compact
72
+ }
73
+ data[:type] = @as_type if @as_type
74
+ data = data.merge(@kwargs).compact
59
75
 
60
76
  @client.enqueue_event('event-create', data)
61
77
  end
@@ -1,13 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Langfuse
2
4
  class Generation
3
5
  attr_reader :id, :trace_id, :name, :start_time, :end_time, :completion_start_time,
4
6
  :model, :model_parameters, :input, :output, :usage, :metadata, :level,
5
- :status_message, :parent_observation_id, :version, :client
7
+ :status_message, :parent_observation_id, :version, :as_type, :client
6
8
 
7
9
  def initialize(client:, trace_id:, id: nil, name: nil, start_time: nil, end_time: nil,
8
10
  completion_start_time: nil, model: nil, model_parameters: nil, input: nil,
9
11
  output: nil, usage: nil, metadata: nil, level: nil, status_message: nil,
10
- parent_observation_id: nil, version: nil, **kwargs)
12
+ parent_observation_id: nil, version: nil, as_type: nil, **kwargs)
11
13
  @client = client
12
14
  @id = id || Utils.generate_id
13
15
  @trace_id = trace_id
@@ -25,6 +27,7 @@ module Langfuse
25
27
  @status_message = status_message
26
28
  @parent_observation_id = parent_observation_id
27
29
  @version = version
30
+ @as_type = validate_as_type(as_type)
28
31
  @kwargs = kwargs
29
32
 
30
33
  # Create the generation
@@ -62,8 +65,9 @@ module Langfuse
62
65
  self
63
66
  end
64
67
 
68
+ # Create a child span
65
69
  def span(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
66
- metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
70
+ metadata: nil, level: nil, status_message: nil, version: nil, as_type: nil, **kwargs)
67
71
  @client.span(
68
72
  trace_id: @trace_id,
69
73
  name: name,
@@ -76,10 +80,12 @@ module Langfuse
76
80
  status_message: status_message,
77
81
  parent_observation_id: @id,
78
82
  version: version,
83
+ as_type: as_type,
79
84
  **kwargs
80
85
  )
81
86
  end
82
87
 
88
+ # Create a child generation
83
89
  def generation(name: nil, start_time: nil, end_time: nil, completion_start_time: nil,
84
90
  model: nil, model_parameters: nil, input: nil, output: nil, usage: nil,
85
91
  metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
@@ -103,6 +109,7 @@ module Langfuse
103
109
  )
104
110
  end
105
111
 
112
+ # Create a child event
106
113
  def event(name:, start_time: nil, input: nil, output: nil, metadata: nil,
107
114
  level: nil, status_message: nil, version: nil, **kwargs)
108
115
  @client.event(
@@ -120,6 +127,138 @@ module Langfuse
120
127
  )
121
128
  end
122
129
 
130
+ # Convenience methods for enhanced observation types
131
+
132
+ # Create a child agent observation
133
+ def agent(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
134
+ metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
135
+ span(
136
+ name: name,
137
+ start_time: start_time,
138
+ end_time: end_time,
139
+ input: input,
140
+ output: output,
141
+ metadata: metadata,
142
+ level: level,
143
+ status_message: status_message,
144
+ version: version,
145
+ as_type: ObservationType::AGENT,
146
+ **kwargs
147
+ )
148
+ end
149
+
150
+ # Create a child tool observation
151
+ def tool(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
152
+ metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
153
+ span(
154
+ name: name,
155
+ start_time: start_time,
156
+ end_time: end_time,
157
+ input: input,
158
+ output: output,
159
+ metadata: metadata,
160
+ level: level,
161
+ status_message: status_message,
162
+ version: version,
163
+ as_type: ObservationType::TOOL,
164
+ **kwargs
165
+ )
166
+ end
167
+
168
+ # Create a child chain observation
169
+ def chain(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
170
+ metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
171
+ span(
172
+ name: name,
173
+ start_time: start_time,
174
+ end_time: end_time,
175
+ input: input,
176
+ output: output,
177
+ metadata: metadata,
178
+ level: level,
179
+ status_message: status_message,
180
+ version: version,
181
+ as_type: ObservationType::CHAIN,
182
+ **kwargs
183
+ )
184
+ end
185
+
186
+ # Create a child retriever observation
187
+ def retriever(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
188
+ metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
189
+ span(
190
+ name: name,
191
+ start_time: start_time,
192
+ end_time: end_time,
193
+ input: input,
194
+ output: output,
195
+ metadata: metadata,
196
+ level: level,
197
+ status_message: status_message,
198
+ version: version,
199
+ as_type: ObservationType::RETRIEVER,
200
+ **kwargs
201
+ )
202
+ end
203
+
204
+ # Create a child embedding observation
205
+ def embedding(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
206
+ model: nil, usage: nil, metadata: nil, level: nil, status_message: nil,
207
+ version: nil, **kwargs)
208
+ merged_metadata = (metadata || {}).merge(
209
+ { model: model, usage: usage }.compact
210
+ )
211
+ span(
212
+ name: name,
213
+ start_time: start_time,
214
+ end_time: end_time,
215
+ input: input,
216
+ output: output,
217
+ metadata: merged_metadata.empty? ? nil : merged_metadata,
218
+ level: level,
219
+ status_message: status_message,
220
+ version: version,
221
+ as_type: ObservationType::EMBEDDING,
222
+ **kwargs
223
+ )
224
+ end
225
+
226
+ # Create a child evaluator observation
227
+ def evaluator(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
228
+ metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
229
+ span(
230
+ name: name,
231
+ start_time: start_time,
232
+ end_time: end_time,
233
+ input: input,
234
+ output: output,
235
+ metadata: metadata,
236
+ level: level,
237
+ status_message: status_message,
238
+ version: version,
239
+ as_type: ObservationType::EVALUATOR,
240
+ **kwargs
241
+ )
242
+ end
243
+
244
+ # Create a child guardrail observation
245
+ def guardrail(name: nil, start_time: nil, end_time: nil, input: nil, output: nil,
246
+ metadata: nil, level: nil, status_message: nil, version: nil, **kwargs)
247
+ span(
248
+ name: name,
249
+ start_time: start_time,
250
+ end_time: end_time,
251
+ input: input,
252
+ output: output,
253
+ metadata: metadata,
254
+ level: level,
255
+ status_message: status_message,
256
+ version: version,
257
+ as_type: ObservationType::GUARDRAIL,
258
+ **kwargs
259
+ )
260
+ end
261
+
123
262
  def score(name:, value:, data_type: nil, comment: nil, **kwargs)
124
263
  @client.score(
125
264
  observation_id: @id,
@@ -136,7 +275,7 @@ module Langfuse
136
275
  end
137
276
 
138
277
  def to_dict
139
- {
278
+ data = {
140
279
  id: @id,
141
280
  trace_id: @trace_id,
142
281
  name: @name,
@@ -153,11 +292,22 @@ module Langfuse
153
292
  status_message: @status_message,
154
293
  parent_observation_id: @parent_observation_id,
155
294
  version: @version
156
- }.merge(@kwargs).compact
295
+ }
296
+ data[:type] = @as_type if @as_type
297
+ data.merge(@kwargs).compact
157
298
  end
158
299
 
159
300
  private
160
301
 
302
+ def validate_as_type(type)
303
+ return nil if type.nil?
304
+
305
+ type_str = type.to_s
306
+ raise ValidationError, "Invalid observation type: #{type}. Valid types are: #{ObservationType::ALL.join(', ')}" unless ObservationType.valid?(type_str)
307
+
308
+ type_str
309
+ end
310
+
161
311
  def create_generation
162
312
  @client.enqueue_event('generation-create', to_dict)
163
313
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langfuse
4
+ # NullGeneration provides a no-op generation object for graceful degradation.
5
+ # Used when Langfuse is unavailable or trace creation fails.
6
+ class NullGeneration
7
+ def update(**) = self
8
+ def end(**) = self
9
+ def span(**) = NullSpan.new
10
+ def generation(**) = NullGeneration.new
11
+ def event(**) = NullEvent.new
12
+ def agent(**) = NullSpan.new
13
+ def tool(**) = NullSpan.new
14
+ def chain(**) = NullSpan.new
15
+ def retriever(**) = NullSpan.new
16
+ def embedding(**) = NullSpan.new
17
+ def evaluator(**) = NullSpan.new
18
+ def guardrail(**) = NullSpan.new
19
+ def score(**) = nil
20
+ def get_url = nil
21
+ def to_dict = {}
22
+ def id = nil
23
+ def trace_id = nil
24
+ end
25
+
26
+ # NullSpan provides a no-op span object for graceful degradation.
27
+ class NullSpan
28
+ def update(**) = self
29
+ def end(**) = self
30
+ def span(**) = NullSpan.new
31
+ def generation(**) = NullGeneration.new
32
+ def event(**) = NullEvent.new
33
+ def agent(**) = NullSpan.new
34
+ def tool(**) = NullSpan.new
35
+ def chain(**) = NullSpan.new
36
+ def retriever(**) = NullSpan.new
37
+ def embedding(**) = NullSpan.new
38
+ def evaluator(**) = NullSpan.new
39
+ def guardrail(**) = NullSpan.new
40
+ def score(**) = nil
41
+ def get_url = nil
42
+ def to_dict = {}
43
+ def id = nil
44
+ def trace_id = nil
45
+ end
46
+
47
+ # NullEvent provides a no-op event object for graceful degradation.
48
+ class NullEvent
49
+ def to_dict = {}
50
+ def id = nil
51
+ def trace_id = nil
52
+ end
53
+
54
+ # NullTrace provides a no-op trace object for graceful degradation.
55
+ # Used when Langfuse is unavailable or trace creation fails.
56
+ # Ensures calling code doesn't break when Langfuse has issues.
57
+ class NullTrace
58
+ def update(**) = self
59
+ def span(**) = NullSpan.new
60
+ def generation(**) = NullGeneration.new
61
+ def event(**) = NullEvent.new
62
+ def agent(**) = NullSpan.new
63
+ def tool(**) = NullSpan.new
64
+ def chain(**) = NullSpan.new
65
+ def retriever(**) = NullSpan.new
66
+ def embedding(**) = NullSpan.new
67
+ def evaluator(**) = NullSpan.new
68
+ def guardrail(**) = NullSpan.new
69
+ def score(**) = nil
70
+ def get_url = nil
71
+ def to_dict = {}
72
+ def id = nil
73
+ end
74
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langfuse
4
+ # Valid observation types for Langfuse
5
+ # These types provide semantic meaning to observations in traces
6
+ module ObservationType
7
+ # Core observation types (existing)
8
+ SPAN = 'span'
9
+ GENERATION = 'generation'
10
+ EVENT = 'event'
11
+
12
+ # Enhanced observation types (new in 2025)
13
+ AGENT = 'agent' # Agent workflows and reasoning
14
+ TOOL = 'tool' # Tool/function calls
15
+ CHAIN = 'chain' # Chain operations (e.g., retrieval chains)
16
+ RETRIEVER = 'retriever' # Data retrieval (vector stores, databases)
17
+ EMBEDDING = 'embedding' # Embedding generation
18
+ EVALUATOR = 'evaluator' # Evaluation/scoring functions
19
+ GUARDRAIL = 'guardrail' # Safety filters and content moderation
20
+
21
+ # All valid observation types
22
+ ALL = [
23
+ SPAN,
24
+ GENERATION,
25
+ EVENT,
26
+ AGENT,
27
+ TOOL,
28
+ CHAIN,
29
+ RETRIEVER,
30
+ EMBEDDING,
31
+ EVALUATOR,
32
+ GUARDRAIL
33
+ ].freeze
34
+
35
+ # Types that are aliases for span (use span-create/span-update events)
36
+ SPAN_BASED = [
37
+ SPAN,
38
+ AGENT,
39
+ TOOL,
40
+ CHAIN,
41
+ RETRIEVER,
42
+ EMBEDDING,
43
+ EVALUATOR,
44
+ GUARDRAIL
45
+ ].freeze
46
+
47
+ # Validate if a type is valid
48
+ def self.valid?(type)
49
+ return true if type.nil? # nil is valid (defaults to base type)
50
+
51
+ ALL.include?(type.to_s)
52
+ end
53
+
54
+ # Check if type uses span events
55
+ def self.span_based?(type)
56
+ return true if type.nil?
57
+
58
+ SPAN_BASED.include?(type.to_s)
59
+ end
60
+ end
61
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Langfuse
2
4
  class Prompt
3
5
  attr_reader :id, :name, :version, :prompt, :config, :labels, :tags, :type, :created_at, :updated_at
@@ -148,8 +150,6 @@ module Langfuse
148
150
  new(template: template, input_variables: variables)
149
151
  end
150
152
 
151
- private
152
-
153
153
  def self.extract_variables(text)
154
154
  variables = []
155
155