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.
@@ -1,161 +1,36 @@
1
1
  # Langfuse Ruby SDK 发布检查清单
2
2
 
3
- ## 📋 发布前检查
3
+ ## 发布前
4
4
 
5
- ### 1. 代码质量检查
6
- - [ ] 所有测试通过 (`bundle exec rspec`)
7
- - [ ] 离线测试通过 (`ruby scripts/test_offline.rb`)
8
- - [ ] 代码风格检查 (`bundle exec rubocop`)
9
- - [ ] 文档更新完成
5
+ - [ ] `bundle exec rspec`
6
+ - [ ] `bundle exec ruby scripts/test_offline.rb`
7
+ - [ ] `bundle exec rubocop`
8
+ - [ ] README / [V4.md](V4.md) / 示例与代码一致
9
+ - [ ] `lib/langfuse/version.rb`
10
+ - [ ] `CHANGELOG.md`(Unreleased → 新版本)
11
+ - [ ] `bundle install` 更新 `Gemfile.lock`
10
12
 
11
- ### 2. 版本管理
12
- - [ ] 更新版本号 (`lib/langfuse/version.rb`)
13
- - [ ] 更新变更日志 (`CHANGELOG.md`)
14
- - [ ] 更新 README 如有必要
13
+ ## 发布
15
14
 
16
- ### 3. 个人信息更新
17
- - [ ] 更新 `langfuse.gemspec` 中的作者信息
18
- - [ ] 更新 GitHub 仓库链接
19
- - [ ] 更新邮箱地址
15
+ 推荐:推送 `vX.Y.Z` tag,由 `.github/workflows/release.yml` 发布。
20
16
 
21
- ### 4. Git 仓库设置
22
- - [ ] 初始化 Git 仓库 (`git init`)
23
- - [ ] 添加远程仓库
24
- - [ ] 提交所有更改
25
- - [ ] 推送到 GitHub
26
-
27
- ## 🚀 发布步骤
28
-
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
17
  ```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
18
+ git push origin HEAD
19
+ git tag vX.Y.Z
20
+ git push origin vX.Y.Z
57
21
  ```
58
22
 
59
- GitHub Actions 会自动:
60
- - 运行测试 (rspec + offline tests)
61
- - 构建 gem
62
- - 发布到 RubyGems
63
- - 创建 GitHub Release
23
+ 需要仓库 secret:`RUBYGEMS_API_KEY`。
64
24
 
65
- ### 方法 2: 使用发布脚本
66
- ```bash
67
- ./scripts/release.sh
68
- ```
25
+ 本地:
69
26
 
70
- ### 方法 3: 手动发布
71
27
  ```bash
72
- # 1. 运行测试
73
- bundle exec rspec
74
- ruby scripts/test_offline.rb
75
-
76
- # 2. 构建 gem
77
28
  gem build langfuse-ruby.gemspec
78
-
79
- # 3. 发布到 RubyGems
80
- gem push langfuse-ruby-x.x.x.gem
81
-
82
- # 4. 创建 Git 标签
83
- git tag vx.x.x
84
- git push origin main
85
- git push origin vx.x.x
86
- ```
87
-
88
- ## 📊 发布后验证
89
-
90
- ### 1. 检查 RubyGems
91
- - [ ] 访问 https://rubygems.org/gems/langfuse
92
- - [ ] 确认版本号正确
93
- - [ ] 检查下载链接
94
-
95
- ### 2. 测试安装
96
- ```bash
97
- # 从 RubyGems 安装
98
- gem install langfuse-ruby
99
-
100
- # 测试基本功能
101
- ruby -e "require 'langfuse'; puts Langfuse::VERSION"
102
- ```
103
-
104
- ### 3. 更新文档
105
- - [ ] 更新 README 中的安装说明
106
- - [ ] 更新项目网站(如有)
107
- - [ ] 发布公告
108
-
109
- ## 🔧 常见问题解决
110
-
111
- ### RubyGems 认证问题
112
- ```bash
113
- # 登录 RubyGems
114
- gem signin
115
-
116
- # 或设置 API 密钥
117
- gem signin --key your_api_key
118
- ```
119
-
120
- ### Git 标签问题
121
- ```bash
122
- # 删除本地标签
123
- git tag -d v0.1.0
124
-
125
- # 删除远程标签
126
- git push origin --delete v0.1.0
127
-
128
- # 重新创建标签
129
- git tag v0.1.0
130
- git push origin v0.1.0
131
- ```
132
-
133
- ### 版本冲突
134
- ```bash
135
- # 检查现有版本
136
- gem search langfuse
137
-
138
- # 更新版本号后重新发布
139
- # 编辑 lib/langfuse/version.rb
140
- # 重新构建和发布
29
+ gem push langfuse-ruby-X.Y.Z.gem
141
30
  ```
142
31
 
143
- ## 📞 获取帮助
144
-
145
- 如果遇到问题:
146
- 1. 查看 RubyGems 文档
147
- 2. 检查 GitHub Issues
148
- 3. 联系 Ruby 社区
149
- 4. 参考其他成功的 gem 项目
150
-
151
- ## 🎉 发布成功后
152
-
153
- 1. 🎊 庆祝!您的 gem 已成功发布
154
- 2. 📢 在社交媒体上分享
155
- 3. 📝 写博客文章介绍
156
- 4. 🔗 联系 Langfuse 团队添加到官方文档
157
- 5. 📈 监控使用情况和反馈
158
-
159
- ---
32
+ ## 发布后
160
33
 
161
- **记住:发布是一个里程碑,但维护才是长期的工作!**
34
+ - [ ] https://rubygems.org/gems/langfuse-ruby 版本正确
35
+ - [ ] `gem install langfuse-ruby && ruby -e "require 'langfuse'; puts Langfuse::VERSION"`
36
+ - [ ] GitHub Release 已创建
data/docs/V4.md ADDED
@@ -0,0 +1,159 @@
1
+ # Langfuse v4 with the Ruby SDK
2
+
3
+ This SDK talks to **Langfuse v4** through OpenTelemetry (OTLP/HTTP JSON). The
4
+ tracing API (`Langfuse.trace`, `#generation`, `#span`, `#agent`, …) is the same
5
+ as in 0.2.0; only the transport, ID format, and how create/update events are
6
+ exported change.
7
+
8
+ Official references:
9
+
10
+ - [Migrate custom ingestion to Langfuse v4](https://langfuse.com/integrations/native/opentelemetry/migration-to-v4)
11
+ - [OpenTelemetry integration](https://langfuse.com/integrations/native/opentelemetry)
12
+ - [Observation types](https://langfuse.com/docs/observability/features/observation-types)
13
+ - [SDK overview](https://langfuse.com/docs/observability/sdk/overview)
14
+
15
+ On **Langfuse Cloud**, `POST /api/public/ingestion` stops accepting everything
16
+ except scores on **16 November 2026**. New work should use `:otel` now.
17
+
18
+ ## Enable v4
19
+
20
+ `ingestion_mode` still defaults to `:legacy` so existing 0.2.0 apps do not
21
+ change transport on upgrade. Set it explicitly:
22
+
23
+ ```ruby
24
+ Langfuse.configure do |config|
25
+ config.public_key = ENV.fetch("LANGFUSE_PUBLIC_KEY")
26
+ config.secret_key = ENV.fetch("LANGFUSE_SECRET_KEY")
27
+ config.host = ENV["LANGFUSE_HOST"] || ENV["LANGFUSE_BASE_URL"] || "https://us.cloud.langfuse.com"
28
+ config.ingestion_mode = :otel
29
+ end
30
+ ```
31
+
32
+ ```bash
33
+ export LANGFUSE_INGESTION_MODE=otel
34
+ ```
35
+
36
+ Unrecognized values (for example `otlp`) used to silently behave like
37
+ `:legacy`. They now log a warning and fall back to `:legacy`. Values are
38
+ downcased (`OTEL` → `:otel`).
39
+
40
+ The SDK default host is **US Cloud** (`https://us.cloud.langfuse.com`). Use
41
+ `https://cloud.langfuse.com` (EU), `https://jp.cloud.langfuse.com`,
42
+ `https://hipaa.cloud.langfuse.com`, or your self-hosted origin.
43
+
44
+ No extra gems. The OTEL Faraday connection sends:
45
+
46
+ - `Authorization: Basic …` (public key : secret key)
47
+ - `x-langfuse-ingestion-version: 4` (real-time v4 path; without it, OTEL data
48
+ can be delayed up to ~10 minutes)
49
+ - `POST {host}/api/public/otel/v1/traces`
50
+
51
+ ## Observations-first model
52
+
53
+ In v4 a **trace** is not a separately ingested record. It is the group of
54
+ **observations** that share a trace ID. The SDK still has a `Trace` object: in
55
+ `:otel` mode that object is exported as the **root span**
56
+ (`langfuse.internal.as_root = true`).
57
+
58
+ | You call | Exported as |
59
+ | --- | --- |
60
+ | `client.trace(...)` / `Langfuse.trace(...)` | Root OTEL span; `langfuse.trace.name`, `user.id`, `session.id`, tags, metadata, `langfuse.trace.input` / `output` |
61
+ | `#generation` | Child span, `langfuse.observation.type = generation`, plus `gen_ai.*` |
62
+ | `#span` / `#agent` / `#tool` / … | Child span with the matching `langfuse.observation.type` |
63
+ | `#event` | Zero-duration child span, type `event` |
64
+ | `#score` / `client.score` | **Not** OTLP — `score-create` on `/api/public/ingestion`, IDs coerced to W3C hex |
65
+
66
+ v4 queries observations directly. For filters and observation-level evaluators:
67
+
68
+ 1. Put **operation** input/output on that observation (`generation.end(output:)`,
69
+ `retriever.end(output:)`, …).
70
+ 2. Put the **overall** request/response on the trace (`Langfuse.trace(..., input:)`
71
+ and `trace.update(output:)`). The SDK sends those as `langfuse.trace.input` /
72
+ `langfuse.trace.output` on the root span (still required by some legacy
73
+ trace-level LLM-as-a-judge configs).
74
+ 3. Prefer typed observations (`#agent`, `#tool`, `#retriever`, …) over a generic
75
+ `#span` when the UI should filter or evaluate that step.
76
+
77
+ Do not export the same span ID twice to “update” it. The SDK collapses each
78
+ observation’s `*-create` and `*-update` events into **one** OTEL span before
79
+ flush, because v4 is append-only and a second export with the same ID becomes a
80
+ duplicate observation.
81
+
82
+ ## Usage and cost
83
+
84
+ v4 cost uses `langfuse.observation.usage_details`, not the legacy `usage`
85
+ object. Prefer:
86
+
87
+ ```ruby
88
+ generation.end(
89
+ output: text,
90
+ usage_details: { input: 100, output: 50, cache_read: 30, total: 180 },
91
+ cost_details: { input: 0.001, output: 0.003, total: 0.004 }
92
+ )
93
+ ```
94
+
95
+ If you only have a legacy `usage` hash, still pass it. In `:otel` mode the
96
+ exporter:
97
+
98
+ - maps `promptTokens` / `inputTokens` / `input` (and snake_case forms) to
99
+ `gen_ai.usage.prompt_tokens` / `completion_tokens` / `total_tokens`
100
+ - copies that into `langfuse.observation.usage_details` when you did not set
101
+ `usage_details` yourself
102
+ - **skips** usage whose `unit` is not tokens, so bytes/milliseconds are not
103
+ billed as tokens
104
+
105
+ `Span#generation` and `Generation#generation` forward `usage_details`,
106
+ `cost_details`, and `prompt` (this was broken before 0.2.1).
107
+
108
+ Link a prompt version with `prompt:` (`Langfuse::Prompt` or
109
+ `{ name:, version: }`) → `langfuse.observation.prompt.name` / `.version`.
110
+
111
+ ## IDs
112
+
113
+ | Mode | Trace ID | Observation ID |
114
+ | --- | --- | --- |
115
+ | `:otel` | 32-char W3C hex | 16-char W3C hex |
116
+ | `:legacy` | UUID | UUID |
117
+
118
+ Do not mix modes for the same IDs in one project.
119
+
120
+ ## Scores
121
+
122
+ Scores are not OTLP spans. In both modes they are `score-create` batches.
123
+ In `:otel` mode `trace_id` / `observation_id` are normalized with
124
+ `OtelExporter.to_otel_trace_id` / `to_otel_span_id` so they attach to hex
125
+ spans. If OTEL export fails, not-yet-sent OTEL chunks **and** that batch’s
126
+ scores are re-queued together.
127
+
128
+ ```ruby
129
+ generation.score(name: "faithfulness", value: 0.9, data_type: "NUMERIC")
130
+ trace.score(name: "user-satisfaction", value: 0.8)
131
+ client.score(name: "csat", value: 5, session_id: "sess-1")
132
+ ```
133
+
134
+ ## Flush, errors, and limits
135
+
136
+ - Background flush on `flush_interval` (default 5s) **or** `flush_at` (default
137
+ 15 events). `Langfuse.trace { ... }` flushes when the block ends.
138
+ - OTLP and ingestion payloads are chunked at 3.5 MB. HTTP 200 with
139
+ `partialSuccess` is logged as a warning.
140
+ - Transient failures (network, timeout, 429, 5xx) re-queue; 4xx validation/auth
141
+ drops the batch with a warning.
142
+ - Queue cap: `max_queue_size` (default 10_000); oldest events dropped.
143
+ - After `fork` (Puma workers) each process gets its own flush thread; inherited
144
+ events are dropped so the parent still owns them.
145
+
146
+ ## Cutover checklist
147
+
148
+ 1. Point `host` at the correct region or self-hosted v4 origin.
149
+ 2. Set `ingestion_mode: :otel` (or `LANGFUSE_INGESTION_MODE=otel`).
150
+ 3. Send a canary trace: root + one `generation` + one child (`retriever` /
151
+ `tool` / `span`), unique tag or metadata.
152
+ 4. Confirm in the UI: all observations appear without the legacy delay,
153
+ hierarchy and timestamps look right, generation has model / usage / cost.
154
+ 5. Confirm scores on that generation attach.
155
+ 6. Stop dual-sending the same IDs through `:legacy` and `:otel`.
156
+ 7. Move evaluators to **observation** level where possible; trace-level I/O is
157
+ compatibility-only in v4.
158
+
159
+ Runnable sample: [`examples/v4_otel_tracing.rb`](../examples/v4_otel_tracing.rb).
@@ -7,7 +7,8 @@ require 'langfuse'
7
7
  client = Langfuse.new(
8
8
  public_key: ENV.fetch('LANGFUSE_PUBLIC_KEY', nil),
9
9
  secret_key: ENV.fetch('LANGFUSE_SECRET_KEY', nil),
10
- host: ENV['LANGFUSE_HOST'] || 'https://cloud.langfuse.com'
10
+ host: ENV['LANGFUSE_HOST'] || ENV['LANGFUSE_BASE_URL'] || 'https://us.cloud.langfuse.com',
11
+ ingestion_mode: :otel # Langfuse v4
11
12
  )
12
13
 
13
14
  puts '🚀 Starting basic tracing example...'
@@ -35,10 +36,10 @@ generation = trace.generation(
35
36
  { role: 'user', content: 'Hello, how are you?' }
36
37
  ],
37
38
  output: { content: "I'm doing well, thank you! How can I help you today?" },
38
- usage: {
39
- prompt_tokens: 12,
40
- completion_tokens: 18,
41
- total_tokens: 30
39
+ usage_details: {
40
+ input: 12,
41
+ output: 18,
42
+ total: 30
42
43
  },
43
44
  model_parameters: {
44
45
  temperature: 0.7,
@@ -71,7 +72,7 @@ retrieval_span.generation(
71
72
  model: 'text-embedding-ada-002',
72
73
  input: 'What is machine learning?',
73
74
  output: [0.1, 0.2, 0.3, 0.4, 0.5], # Simplified embedding
74
- usage: { prompt_tokens: 5, total_tokens: 5 }
75
+ usage_details: { input: 5, total: 5 }
75
76
  )
76
77
 
77
78
  # End retrieval span
@@ -113,10 +114,10 @@ answer_gen = answer_span.generation(
113
114
  output: {
114
115
  content: 'Machine learning is a subset of artificial intelligence that enables computers to learn and improve from experience without being explicitly programmed. ML algorithms identify patterns in data and use these patterns to make predictions or decisions.'
115
116
  },
116
- usage: {
117
- prompt_tokens: 85,
118
- completion_tokens: 45,
119
- total_tokens: 130
117
+ usage_details: {
118
+ input: 85,
119
+ output: 45,
120
+ total: 130
120
121
  }
121
122
  )
122
123
 
@@ -7,7 +7,8 @@ require 'langfuse'
7
7
  Langfuse.configure do |config|
8
8
  config.public_key = ENV.fetch('LANGFUSE_PUBLIC_KEY', nil)
9
9
  config.secret_key = ENV.fetch('LANGFUSE_SECRET_KEY', nil)
10
- config.host = ENV['LANGFUSE_HOST'] || 'https://cloud.langfuse.com'
10
+ config.host = ENV['LANGFUSE_HOST'] || ENV['LANGFUSE_BASE_URL'] || 'https://us.cloud.langfuse.com'
11
+ config.ingestion_mode = :otel # Langfuse v4
11
12
  end
12
13
 
13
14
  puts '🚀 Simplified usage example...'
@@ -26,10 +27,10 @@ result = Langfuse.trace('simplified-chat', user_id: 'user-123', input: { message
26
27
 
27
28
  # Simulate LLM response
28
29
  response_content = "Hi there! How can I help you today?"
29
- usage = { prompt_tokens: 10, completion_tokens: 15, total_tokens: 25 }
30
+ usage_details = { input: 10, output: 15, total: 25 }
30
31
 
31
- # End the generation with output and usage
32
- generation.end(output: response_content, usage: usage)
32
+ # End the generation with output and v4 usage_details (cost model)
33
+ generation.end(output: response_content, usage_details: usage_details)
33
34
 
34
35
  # Update trace with final output
35
36
  trace.update(output: response_content)
@@ -54,7 +55,7 @@ Langfuse.trace('document-qa', user_id: 'user-456') do |trace|
54
55
  model: 'text-embedding-ada-002',
55
56
  input: 'What is Ruby?',
56
57
  output: [0.1, 0.2, 0.3],
57
- usage: { prompt_tokens: 5, total_tokens: 5 }
58
+ usage_details: { input: 5, total: 5 }
58
59
  )
59
60
 
60
61
  retrieval.end(output: { documents: ['Ruby is a programming language...'] })
@@ -70,7 +71,7 @@ Langfuse.trace('document-qa', user_id: 'user-456') do |trace|
70
71
 
71
72
  gen.end(
72
73
  output: 'Ruby is a dynamic, object-oriented programming language.',
73
- usage: { prompt_tokens: 50, completion_tokens: 20, total_tokens: 70 }
74
+ usage_details: { input: 50, output: 20, total: 70 }
74
75
  )
75
76
 
76
77
  answer_span.end(output: { answer: 'Ruby is a dynamic programming language.' })
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Langfuse v4 (OTLP) tracing example.
5
+ # Tracing APIs are the same as :legacy; only ingestion_mode: :otel changes the
6
+ # transport (OTLP/HTTP + x-langfuse-ingestion-version: 4).
7
+ #
8
+ # export LANGFUSE_PUBLIC_KEY=pk-lf-...
9
+ # export LANGFUSE_SECRET_KEY=sk-lf-...
10
+ # export LANGFUSE_HOST=https://us.cloud.langfuse.com # or EU / self-hosted
11
+ # ruby examples/v4_otel_tracing.rb
12
+
13
+ require 'langfuse'
14
+
15
+ Langfuse.configure do |config|
16
+ config.public_key = ENV.fetch('LANGFUSE_PUBLIC_KEY', nil)
17
+ config.secret_key = ENV.fetch('LANGFUSE_SECRET_KEY', nil)
18
+ config.host = ENV['LANGFUSE_HOST'] || ENV['LANGFUSE_BASE_URL'] || 'https://us.cloud.langfuse.com'
19
+ config.ingestion_mode = :otel
20
+ end
21
+
22
+ puts '🚀 Langfuse v4 / OTEL tracing example'
23
+ puts " host: #{Langfuse.client.host}"
24
+ puts " ingestion_mode: #{Langfuse.client.ingestion_mode}"
25
+
26
+ query = 'What is Ruby?'
27
+
28
+ Langfuse.trace(
29
+ 'v4-document-qa',
30
+ user_id: 'user-123',
31
+ session_id: 'sess-456',
32
+ tags: %w[v4 example],
33
+ input: { query: query }
34
+ ) do |trace|
35
+ agent = trace.agent(name: 'qa-agent', input: { query: query })
36
+
37
+ retrieval = agent.retriever(
38
+ name: 'vector-search',
39
+ input: { query: query, top_k: 3 }
40
+ )
41
+ docs = ['Ruby is a dynamic, object-oriented programming language.']
42
+ retrieval.end(output: { documents: docs })
43
+
44
+ generation = agent.generation(
45
+ name: 'openai-completion',
46
+ model: 'gpt-4o',
47
+ input: [
48
+ { role: 'system', content: 'Answer using the retrieved context only.' },
49
+ { role: 'user', content: query }
50
+ ],
51
+ model_parameters: { temperature: 0.2 }
52
+ )
53
+
54
+ answer = 'Ruby is a dynamic, object-oriented programming language.'
55
+ generation.end(
56
+ output: answer,
57
+ usage_details: { input: 48, output: 16, total: 64 },
58
+ cost_details: { input: 0.0001, output: 0.0004, total: 0.0005 }
59
+ )
60
+ generation.score(name: 'faithfulness', value: 0.92, comment: 'Grounded in retrieved docs')
61
+
62
+ agent.end(output: { answer: answer })
63
+ trace.update(output: { answer: answer })
64
+ trace.score(name: 'user-satisfaction', value: 0.9)
65
+
66
+ puts "Created v4 trace: #{trace.id}"
67
+ puts "Trace URL: #{trace.get_url}"
68
+ end
69
+
70
+ puts '✅ Flushed. Check the Langfuse UI — observations should appear in real time.'
@@ -43,6 +43,7 @@ Gem::Specification.new do |spec|
43
43
  spec.require_paths = ['lib']
44
44
 
45
45
  # Dependencies
46
+ spec.add_dependency 'base64', '>= 0.1.0' # stdlib in Ruby < 4.0, bundled gem since Ruby 4.0
46
47
  spec.add_dependency 'concurrent-ruby', '~> 1.0'
47
48
  spec.add_dependency 'faraday', '>= 1.8', '< 3.0'
48
49
  spec.add_dependency 'faraday-multipart', '~> 1.0'
@@ -54,6 +55,7 @@ Gem::Specification.new do |spec|
54
55
  spec.add_development_dependency 'rake', '~> 13.0'
55
56
  spec.add_development_dependency 'rspec', '~> 3.0'
56
57
  spec.add_development_dependency 'rubocop', '~> 1.0'
58
+ spec.add_development_dependency 'tsort', '>= 0.1.0' # stdlib in Ruby < 4.0, default gem since Ruby 4.0
57
59
  spec.add_development_dependency 'vcr', '~> 6.0'
58
60
  spec.add_development_dependency 'webmock', '~> 3.0'
59
61
  spec.add_development_dependency 'yard', '~> 0.9'