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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +5 -5
- data/.github/workflows/release.yml +10 -9
- data/.rubocop.yml +66 -0
- data/CHANGELOG.md +13 -1
- data/CLAUDE.md +100 -0
- data/Gemfile +2 -1
- data/Gemfile.lock +3 -3
- data/Makefile +73 -0
- data/README.md +116 -8
- data/Rakefile +4 -2
- data/docs/FINAL_SUMMARY.md +11 -10
- data/docs/PUBLISH_GUIDE.md +2 -2
- data/docs/README.md +3 -3
- data/docs/RELEASE_CHECKLIST.md +44 -13
- data/examples/auto_flush_control.rb +3 -2
- data/examples/basic_tracing.rb +5 -4
- data/examples/connection_config_demo.rb +1 -0
- data/examples/event_usage.rb +3 -2
- data/examples/prompt_management.rb +3 -2
- data/examples/simplified_usage.rb +126 -0
- data/examples/url_encoding_demo.rb +1 -1
- data/langfuse-ruby.gemspec +2 -1
- data/lib/langfuse/client.rb +58 -5
- data/lib/langfuse/errors.rb +2 -0
- data/lib/langfuse/evaluation.rb +14 -12
- data/lib/langfuse/event.rb +3 -3
- data/lib/langfuse/generation.rb +3 -3
- data/lib/langfuse/null_objects.rb +74 -0
- data/lib/langfuse/otel_exporter.rb +333 -0
- data/lib/langfuse/prompt.rb +2 -2
- data/lib/langfuse/span.rb +3 -3
- data/lib/langfuse/trace.rb +2 -0
- data/lib/langfuse/utils.rb +2 -0
- data/lib/langfuse/version.rb +3 -1
- data/lib/langfuse.rb +131 -3
- data/scripts/release.sh +1 -1
- data/{test_offline.rb → scripts/test_offline.rb} +4 -3
- data/scripts/verify_release.rb +5 -4
- metadata +23 -21
- data/docs/TYPE_VALIDATION_TROUBLESHOOTING.md +0 -202
- data/docs/URL_ENCODING_FIX.md +0 -164
data/docs/RELEASE_CHECKLIST.md
CHANGED
|
@@ -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
|
-
### 方法
|
|
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-
|
|
80
|
+
gem push langfuse-ruby-x.x.x.gem
|
|
45
81
|
|
|
46
82
|
# 4. 创建 Git 标签
|
|
47
|
-
git tag
|
|
83
|
+
git tag vx.x.x
|
|
48
84
|
git push origin main
|
|
49
|
-
git push origin
|
|
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
|
-
|
|
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
|
-
|
|
64
|
+
trace_manual.generation(
|
|
64
65
|
name: 'manual-generation',
|
|
65
66
|
model: 'gpt-3.5-turbo',
|
|
66
67
|
input: [{ role: 'user', content: 'Hello!' }],
|
data/examples/basic_tracing.rb
CHANGED
|
@@ -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
|
|
8
|
-
secret_key: ENV
|
|
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
|
-
|
|
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
|
-
|
|
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' }],
|
data/examples/event_usage.rb
CHANGED
|
@@ -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
|
|
8
|
-
secret_key: ENV
|
|
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
|
|
8
|
-
secret_key: ENV
|
|
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
|
|
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
|
data/langfuse-ruby.gemspec
CHANGED
|
@@ -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
|
data/lib/langfuse/client.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
605
|
+
if retries_left.positive?
|
|
553
606
|
retries_left -= 1
|
|
554
607
|
sleep(2**(@retries - retries_left))
|
|
555
608
|
retry
|
data/lib/langfuse/errors.rb
CHANGED
data/lib/langfuse/evaluation.rb
CHANGED
|
@@ -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
|
|
89
|
+
super
|
|
88
90
|
end
|
|
89
91
|
|
|
90
|
-
def evaluate(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
174
|
+
super
|
|
173
175
|
end
|
|
174
176
|
|
|
175
|
-
def evaluate(
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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)
|
data/lib/langfuse/event.rb
CHANGED
|
@@ -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
|
data/lib/langfuse/generation.rb
CHANGED
|
@@ -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
|