langfuse-ruby 0.1.5 → 0.1.7

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.
@@ -4,7 +4,7 @@
4
4
 
5
5
  ### 1. 代码质量检查
6
6
  - [ ] 所有测试通过 (`bundle exec rspec`)
7
- - [ ] 离线测试通过 (`ruby test_offline.rb`)
7
+ - [ ] 离线测试通过 (`ruby scripts/test_offline.rb`)
8
8
  - [ ] 代码风格检查 (`bundle exec rubocop`)
9
9
  - [ ] 文档更新完成
10
10
 
@@ -26,34 +26,65 @@
26
26
 
27
27
  ## 🚀 发布步骤
28
28
 
29
- ### 方法 1: 使用发布脚本 (推荐)
29
+ ### 方法 1: 使用 GitHub Actions (推荐)
30
+
31
+ > ⚠️ **重要**: 必须先运行 `bundle install` 更新 `Gemfile.lock`,否则 CI 会失败!
32
+
33
+ #### 前提条件
34
+ 在 GitHub 仓库的 **Settings → Secrets and variables → Actions** 中配置:
35
+ - `RUBYGEMS_API_KEY` - RubyGems API 密钥(在 [rubygems.org](https://rubygems.org) → Settings → API keys 中获取)
36
+
37
+ > 注意:`GITHUB_TOKEN` 无需手动配置,GitHub Actions 会自动提供。
38
+
39
+ #### 发布流程
40
+ ```bash
41
+ # 1. 更新版本号
42
+ # 编辑 lib/langfuse/version.rb
43
+
44
+ # 2. 更新 Gemfile.lock(重要!)
45
+ bundle install
46
+
47
+ # 3. 提交更改
48
+ git add .
49
+ git commit -m "Bump version to x.x.x"
50
+
51
+ # 4. 推送代码
52
+ git push origin <branch>
53
+
54
+ # 5. 创建并推送标签(触发自动发布)
55
+ git tag vx.x.x
56
+ git push origin vx.x.x
57
+ ```
58
+
59
+ GitHub Actions 会自动:
60
+ - 运行测试 (rspec + offline tests)
61
+ - 构建 gem
62
+ - 发布到 RubyGems
63
+ - 创建 GitHub Release
64
+
65
+ ### 方法 2: 使用发布脚本
30
66
  ```bash
31
67
  ./scripts/release.sh
32
68
  ```
33
69
 
34
- ### 方法 2: 手动发布
70
+ ### 方法 3: 手动发布
35
71
  ```bash
36
72
  # 1. 运行测试
37
73
  bundle exec rspec
38
- ruby test_offline.rb
74
+ ruby scripts/test_offline.rb
39
75
 
40
76
  # 2. 构建 gem
41
- gem build langfuse.gemspec
77
+ gem build langfuse-ruby.gemspec
42
78
 
43
79
  # 3. 发布到 RubyGems
44
- gem push langfuse-0.1.0.gem
80
+ gem push langfuse-ruby-x.x.x.gem
45
81
 
46
82
  # 4. 创建 Git 标签
47
- git tag v0.1.0
83
+ git tag vx.x.x
48
84
  git push origin main
49
- git push origin v0.1.0
85
+ git push origin vx.x.x
50
86
  ```
51
87
 
52
- ### 方法 3: 使用 GitHub Actions
53
- 1. 推送代码到 GitHub
54
- 2. 创建版本标签 (`git tag v0.1.0 && git push origin v0.1.0`)
55
- 3. GitHub Actions 自动发布
56
-
57
88
  ## 📊 发布后验证
58
89
 
59
90
  ### 1. 检查 RubyGems
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require_relative '../lib/langfuse'
4
5
 
@@ -26,7 +27,7 @@ puts " 自动刷新: #{client_auto.auto_flush}"
26
27
  puts " 刷新间隔: #{client_auto.flush_interval}秒"
27
28
 
28
29
  # 创建一些事件
29
- trace_auto = client_auto.trace(
30
+ client_auto.trace(
30
31
  name: 'auto-flush-demo',
31
32
  input: { message: '这是自动刷新演示' },
32
33
  metadata: { demo: true }
@@ -60,7 +61,7 @@ trace_manual = client_manual.trace(
60
61
  metadata: { demo: true }
61
62
  )
62
63
 
63
- generation_manual = trace_manual.generation(
64
+ trace_manual.generation(
64
65
  name: 'manual-generation',
65
66
  model: 'gpt-3.5-turbo',
66
67
  input: [{ role: 'user', content: 'Hello!' }],
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'langfuse'
4
5
 
5
6
  # Initialize the Langfuse client
6
7
  client = Langfuse.new(
7
- public_key: ENV['LANGFUSE_PUBLIC_KEY'],
8
- secret_key: ENV['LANGFUSE_SECRET_KEY'],
8
+ public_key: ENV.fetch('LANGFUSE_PUBLIC_KEY', nil),
9
+ secret_key: ENV.fetch('LANGFUSE_SECRET_KEY', nil),
9
10
  host: ENV['LANGFUSE_HOST'] || 'https://cloud.langfuse.com'
10
11
  )
11
12
 
@@ -65,7 +66,7 @@ retrieval_span = workflow_trace.span(
65
66
  )
66
67
 
67
68
  # Embedding generation within retrieval
68
- embedding_gen = retrieval_span.generation(
69
+ retrieval_span.generation(
69
70
  name: 'embedding-generation',
70
71
  model: 'text-embedding-ada-002',
71
72
  input: 'What is machine learning?',
@@ -158,7 +159,7 @@ puts "\n🚨 Example 4: Error handling"
158
159
  begin
159
160
  error_trace = client.trace(name: 'error-example')
160
161
 
161
- error_gen = error_trace.generation(
162
+ error_trace.generation(
162
163
  name: 'failed-generation',
163
164
  model: 'gpt-3.5-turbo',
164
165
  input: [{ role: 'user', content: 'This will fail' }],
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require_relative '../lib/langfuse'
4
5
 
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'langfuse'
4
5
 
5
6
  # Initialize the Langfuse client
6
7
  client = Langfuse.new(
7
- public_key: ENV['LANGFUSE_PUBLIC_KEY'],
8
- secret_key: ENV['LANGFUSE_SECRET_KEY'],
8
+ public_key: ENV.fetch('LANGFUSE_PUBLIC_KEY', nil),
9
+ secret_key: ENV.fetch('LANGFUSE_SECRET_KEY', nil),
9
10
  host: ENV['LANGFUSE_HOST'] || 'https://cloud.langfuse.com'
10
11
  )
11
12
 
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'langfuse'
4
5
 
5
6
  # Initialize the Langfuse client
6
7
  client = Langfuse.new(
7
- public_key: ENV['LANGFUSE_PUBLIC_KEY'],
8
- secret_key: ENV['LANGFUSE_SECRET_KEY'],
8
+ public_key: ENV.fetch('LANGFUSE_PUBLIC_KEY', nil),
9
+ secret_key: ENV.fetch('LANGFUSE_SECRET_KEY', nil),
9
10
  host: ENV['LANGFUSE_HOST'] || 'https://cloud.langfuse.com'
10
11
  )
11
12
 
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'langfuse'
5
+
6
+ # Configure Langfuse globally
7
+ Langfuse.configure do |config|
8
+ config.public_key = ENV.fetch('LANGFUSE_PUBLIC_KEY', nil)
9
+ config.secret_key = ENV.fetch('LANGFUSE_SECRET_KEY', nil)
10
+ config.host = ENV['LANGFUSE_HOST'] || 'https://cloud.langfuse.com'
11
+ end
12
+
13
+ puts '🚀 Simplified usage example...'
14
+
15
+ # Example 1: Block-based tracing with automatic flush
16
+ puts "\n📝 Example 1: Block-based tracing (recommended)"
17
+
18
+ result = Langfuse.trace('simplified-chat', user_id: 'user-123', input: { message: 'Hello!' }) do |trace|
19
+ # Create a generation for the LLM call
20
+ generation = trace.generation(
21
+ name: 'openai-chat',
22
+ model: 'gpt-4',
23
+ input: [{ role: 'user', content: 'Hello!' }],
24
+ model_parameters: { temperature: 0.7 }
25
+ )
26
+
27
+ # Simulate LLM response
28
+ response_content = "Hi there! How can I help you today?"
29
+ usage = { prompt_tokens: 10, completion_tokens: 15, total_tokens: 25 }
30
+
31
+ # End the generation with output and usage
32
+ generation.end(output: response_content, usage: usage)
33
+
34
+ # Update trace with final output
35
+ trace.update(output: response_content)
36
+
37
+ # Return value from block
38
+ response_content
39
+ end
40
+ # Flush happens automatically here!
41
+
42
+ puts "Response: #{result}"
43
+
44
+ # Example 2: Nested spans with block-based tracing
45
+ puts "\n🔗 Example 2: Complex workflow with spans"
46
+
47
+ Langfuse.trace('document-qa', user_id: 'user-456') do |trace|
48
+ # Retrieval span
49
+ retrieval = trace.span(name: 'document-retrieval', input: { query: 'What is Ruby?' })
50
+
51
+ # Simulate embedding generation
52
+ retrieval.generation(
53
+ name: 'embedding',
54
+ model: 'text-embedding-ada-002',
55
+ input: 'What is Ruby?',
56
+ output: [0.1, 0.2, 0.3],
57
+ usage: { prompt_tokens: 5, total_tokens: 5 }
58
+ )
59
+
60
+ retrieval.end(output: { documents: ['Ruby is a programming language...'] })
61
+
62
+ # Answer generation span
63
+ answer_span = trace.span(name: 'answer-generation')
64
+
65
+ gen = answer_span.generation(
66
+ name: 'openai-completion',
67
+ model: 'gpt-4',
68
+ input: [{ role: 'user', content: 'What is Ruby?' }]
69
+ )
70
+
71
+ gen.end(
72
+ output: 'Ruby is a dynamic, object-oriented programming language.',
73
+ usage: { prompt_tokens: 50, completion_tokens: 20, total_tokens: 70 }
74
+ )
75
+
76
+ answer_span.end(output: { answer: 'Ruby is a dynamic programming language.' })
77
+
78
+ # Score the trace
79
+ trace.score(name: 'relevance', value: 0.95, comment: 'Highly relevant answer')
80
+ end
81
+
82
+ # Example 3: Direct trace without block (manual flush required)
83
+ puts "\n📌 Example 3: Direct trace usage"
84
+
85
+ trace = Langfuse.trace('manual-trace', user_id: 'user-789')
86
+ puts "Created trace: #{trace.id}"
87
+
88
+ generation = trace.generation(
89
+ name: 'quick-generation',
90
+ model: 'gpt-3.5-turbo',
91
+ input: 'Quick test'
92
+ )
93
+ generation.end(output: 'Done!')
94
+
95
+ # Manual flush required when not using block
96
+ Langfuse.flush
97
+
98
+ # Example 4: Get and compile prompts
99
+ puts "\n📋 Example 4: Prompt management"
100
+
101
+ # Get a prompt (would fail gracefully with nil if not configured)
102
+ # prompt = Langfuse.get_prompt('my-prompt', variables: { name: 'World' })
103
+ # puts "Compiled prompt: #{prompt}"
104
+
105
+ # Graceful degradation example
106
+ puts "Testing graceful degradation with invalid config..."
107
+ Langfuse.reset!
108
+ Langfuse.configure do |config|
109
+ config.public_key = nil
110
+ config.secret_key = nil
111
+ end
112
+
113
+ # This will use NullTrace when client creation fails
114
+ # The block still executes, just without actual tracing
115
+ begin
116
+ Langfuse.trace('will-fail') do |trace|
117
+ puts "Trace type: #{trace.class}"
118
+ gen = trace.generation(name: 'test', model: 'gpt-4', input: 'hello')
119
+ puts "Generation type: #{gen.class}"
120
+ gen.end(output: 'world')
121
+ end
122
+ rescue => e
123
+ puts "Handled error: #{e.message}"
124
+ end
125
+
126
+ puts "\n✅ Simplified usage example completed!"
@@ -51,7 +51,7 @@ rescue Langfuse::ValidationError => e
51
51
  puts "✗ Error: #{e.message}"
52
52
  end
53
53
 
54
- puts "\n" + '=' * 60
54
+ puts "\n#{'=' * 60}"
55
55
  puts 'Note: The client now automatically URL-encodes prompt names.'
56
56
  puts 'You no longer need to manually encode them!'
57
57
  puts '=' * 60
@@ -20,6 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.metadata['changelog_uri'] = 'https://github.com/ai-firstly/langfuse-ruby/blob/main/CHANGELOG.md'
21
21
  spec.metadata['documentation_uri'] = 'https://rubydoc.info/gems/langfuse-ruby'
22
22
  spec.metadata['bug_tracker_uri'] = 'https://github.com/ai-firstly/langfuse-ruby/issues'
23
+ spec.metadata['rubygems_mfa_required'] = 'true'
23
24
 
24
25
  # Specify which files should be added to the gem when it is released.
25
26
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
@@ -44,8 +45,8 @@ Gem::Specification.new do |spec|
44
45
  # Dependencies
45
46
  spec.add_dependency 'concurrent-ruby', '~> 1.0'
46
47
  spec.add_dependency 'faraday', '>= 1.8', '< 3.0'
47
- spec.add_dependency 'faraday-net_http', '>= 1.0', '< 4.0'
48
48
  spec.add_dependency 'faraday-multipart', '~> 1.0'
49
+ spec.add_dependency 'faraday-net_http', '>= 1.0', '< 4.0'
49
50
  spec.add_dependency 'json', '~> 2.0'
50
51
 
51
52
  # Development dependencies
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'faraday'
2
4
  require 'faraday/net_http'
3
5
  require 'faraday/multipart'
@@ -7,10 +9,11 @@ require 'concurrent'
7
9
 
8
10
  module Langfuse
9
11
  class Client
10
- attr_reader :public_key, :secret_key, :host, :debug, :timeout, :retries, :flush_interval, :auto_flush
12
+ attr_reader :public_key, :secret_key, :host, :debug, :timeout, :retries, :flush_interval, :auto_flush,
13
+ :ingestion_mode
11
14
 
12
15
  def initialize(public_key: nil, secret_key: nil, host: nil, debug: false, timeout: 30, retries: 3,
13
- flush_interval: nil, auto_flush: nil)
16
+ flush_interval: nil, auto_flush: nil, ingestion_mode: nil)
14
17
  @public_key = public_key || ENV['LANGFUSE_PUBLIC_KEY'] || Langfuse.configuration.public_key
15
18
  @secret_key = secret_key || ENV['LANGFUSE_SECRET_KEY'] || Langfuse.configuration.secret_key
16
19
  @host = host || ENV['LANGFUSE_HOST'] || Langfuse.configuration.host
@@ -23,11 +26,14 @@ module Langfuse
23
26
  else
24
27
  auto_flush
25
28
  end
29
+ @ingestion_mode = resolve_ingestion_mode(ingestion_mode)
26
30
 
27
31
  raise AuthenticationError, 'Public key is required' unless @public_key
28
32
  raise AuthenticationError, 'Secret key is required' unless @secret_key
29
33
 
30
34
  @connection = build_connection
35
+ @otel_connection = build_otel_connection if @ingestion_mode == :otel
36
+ @otel_exporter = OtelExporter.new(connection: @otel_connection, debug: @debug) if @ingestion_mode == :otel
31
37
  @event_queue = Concurrent::Array.new
32
38
  @flush_thread = start_flush_thread if @auto_flush
33
39
  end
@@ -450,16 +456,38 @@ module Langfuse
450
456
  return
451
457
  end
452
458
 
459
+ if @ingestion_mode == :otel
460
+ send_batch_otel(valid_events)
461
+ else
462
+ send_batch_legacy(valid_events)
463
+ end
464
+ end
465
+
466
+ def send_batch_legacy(valid_events)
453
467
  batch_data = build_batch_data(valid_events)
454
468
  puts "Sending batch data: #{batch_data}" if @debug
455
469
 
456
470
  begin
457
471
  response = post('/api/public/ingestion', batch_data)
458
- puts "Flushed #{valid_events.length} events" if @debug
472
+ puts "Flushed #{valid_events.length} events (legacy)" if @debug
459
473
  response
460
474
  rescue StandardError => e
461
475
  puts "Failed to flush events: #{e.message}" if @debug
462
- # Re-queue events on failure
476
+ valid_events.each { |event| @event_queue << event }
477
+ raise
478
+ end
479
+ end
480
+
481
+ def send_batch_otel(valid_events)
482
+ puts "Sending #{valid_events.length} events via OTEL" if @debug
483
+
484
+ begin
485
+ response = @otel_exporter.export(valid_events)
486
+ handle_response(response)
487
+ puts "Flushed #{valid_events.length} events (otel)" if @debug
488
+ response
489
+ rescue StandardError => e
490
+ puts "Failed to flush OTEL events: #{e.message}" if @debug
463
491
  valid_events.each { |event| @event_queue << event }
464
492
  raise
465
493
  end
@@ -491,6 +519,15 @@ module Langfuse
491
519
  end
492
520
  end
493
521
 
522
+ def resolve_ingestion_mode(explicit_mode)
523
+ return explicit_mode.to_sym if explicit_mode
524
+
525
+ env_mode = ENV['LANGFUSE_INGESTION_MODE']
526
+ return env_mode.to_sym if env_mode && !env_mode.empty?
527
+
528
+ Langfuse.configuration.ingestion_mode || :legacy
529
+ end
530
+
494
531
  def build_connection
495
532
  Faraday.new(url: @host) do |conn|
496
533
  # 配置请求和响应处理
@@ -514,6 +551,22 @@ module Langfuse
514
551
  end
515
552
  end
516
553
 
554
+ # Build a separate Faraday connection for OTEL with the v4 ingestion header.
555
+ def build_otel_connection
556
+ Faraday.new(url: @host) do |conn|
557
+ conn.response :json, content_type: /\bjson$/
558
+
559
+ conn.headers['User-Agent'] = "langfuse-ruby/#{Langfuse::VERSION}"
560
+ conn.headers['Authorization'] = "Basic #{Base64.strict_encode64("#{@public_key}:#{@secret_key}")}"
561
+ conn.headers['x-langfuse-ingestion-version'] = '4'
562
+ conn.headers['Content-Type'] = 'application/json'
563
+
564
+ conn.options.timeout = @timeout
565
+ conn.response :logger if @debug
566
+ conn.adapter Faraday.default_adapter
567
+ end
568
+ end
569
+
517
570
  # HTTP methods
518
571
  def get(path, params = {})
519
572
  request(:get, path, params: params)
@@ -549,7 +602,7 @@ module Langfuse
549
602
  rescue Faraday::TimeoutError => e
550
603
  raise TimeoutError, "Request timed out: #{e.message}"
551
604
  rescue Faraday::ConnectionFailed => e
552
- if retries_left > 0
605
+ if retries_left.positive?
553
606
  retries_left -= 1
554
607
  sleep(2**(@retries - retries_left))
555
608
  retry
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Langfuse
2
4
  class Error < StandardError; end
3
5
  class AuthenticationError < Error; end
@@ -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,3 +1,5 @@
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,
@@ -49,9 +51,7 @@ module Langfuse
49
51
  return nil if type.nil?
50
52
 
51
53
  type_str = type.to_s
52
- unless ObservationType.valid?(type_str)
53
- raise ValidationError, "Invalid observation type: #{type}. Valid types are: #{ObservationType::ALL.join(', ')}"
54
- end
54
+ raise ValidationError, "Invalid observation type: #{type}. Valid types are: #{ObservationType::ALL.join(', ')}" unless ObservationType.valid?(type_str)
55
55
 
56
56
  type_str
57
57
  end
@@ -1,3 +1,5 @@
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,
@@ -301,9 +303,7 @@ module Langfuse
301
303
  return nil if type.nil?
302
304
 
303
305
  type_str = type.to_s
304
- unless ObservationType.valid?(type_str)
305
- raise ValidationError, "Invalid observation type: #{type}. Valid types are: #{ObservationType::ALL.join(', ')}"
306
- end
306
+ raise ValidationError, "Invalid observation type: #{type}. Valid types are: #{ObservationType::ALL.join(', ')}" unless ObservationType.valid?(type_str)
307
307
 
308
308
  type_str
309
309
  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