langfuse-rb 0.1.0 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5905472e2a3dc7fc674f1fbdc86b76105f8513a3079c00c8f9c5c346c19c2f16
4
- data.tar.gz: 683d86aca0810243d76fd4a0e5418e635ccaf50fd0cbe966290541eff91ccf4e
3
+ metadata.gz: ae64454a24bf090bcaedf13432b8c2c7ecc0c1d986bc5ffdec8ad3f5ada1b2d5
4
+ data.tar.gz: 05dcbaaa1aa3aa90fcb75d42086a526c60914b3f88ce2fa0049b77d7cafc33cb
5
5
  SHA512:
6
- metadata.gz: a5a5b0352f26ce66c2997b6a9bdebde37f1629550bca0c7ef221ad1954f763f1d5a1343915c54d6354ccaafb02b9e7183ac66ef2bf18a9142a69a13852e41762
7
- data.tar.gz: a2c05dd96df86951eccefd43719805b07595139f6dc56bc4fe1c35a54fe9ee3936ac4f06b4b1ac78fa419cdf717fd988f4555e349ab3b9a67738405e4ee34220
6
+ metadata.gz: 1b8b91ba6180d4450fef21f59bcca7c06b21c2a5d336e70bf233ca3d2b4d325387f78950ac3ee2c41c4655a0199a4fbd03fdf07164cf224dba7bc2734af1f95c
7
+ data.tar.gz: 00d0a265e3f41cf6690f740b63c7d61853733b9701e1d1ea0630fe19a6d8b8022d8a1cd26b9eceb62133fe169fde093544d738ba3f258f765cd9ced0e2aecea9
data/CHANGELOG.md CHANGED
@@ -7,54 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [1.0.0] - 2025-10-16 🚀
11
-
12
- ### Initial Release
13
-
14
- Complete Ruby SDK for Langfuse with prompt management, distributed caching, LLM tracing, and Rails integration.
15
-
16
- #### Prompt Management
17
- - Fetch and compile text and chat prompts with Mustache templating
18
- - Support for prompt versioning and label-based fetching (production, staging, etc.)
19
- - Automatic variable substitution with nested objects and arrays
20
- - Global configuration pattern with `Langfuse.configure` block
21
- - Fallback prompt support for graceful error recovery
22
-
23
- #### Caching
24
- - Dual backend support: in-memory (default) and Rails.cache (distributed)
25
- - Thread-safe in-memory cache with TTL and LRU eviction
26
- - Distributed caching with Redis/Memcached via Rails.cache
27
- - Automatic stampede protection with distributed locks (Rails.cache only)
28
- - Cache warming utilities for deployment automation
29
- - Auto-discovery of all prompts with configurable labels
30
-
31
- #### LLM Tracing & Observability
32
- - Built on OpenTelemetry for industry-standard distributed tracing
33
- - Block-based Ruby API for traces, spans, and generations
34
- - Automatic prompt-to-trace linking
35
- - Token usage and cost tracking
36
- - W3C Trace Context support for distributed tracing across services
37
- - Integration with APM tools (Datadog, New Relic, Honeycomb, etc.)
38
- - Async processing with batch span export
39
-
40
- #### Rails Integration
41
- - Rails-friendly configuration with initializer support
42
- - Background job integration (Sidekiq, GoodJob, Delayed Job, etc.)
43
- - Rake tasks for cache management
44
- - Environment-specific configuration patterns
45
- - Credentials support for secure key management
46
-
47
- #### Developer Experience
48
- - Comprehensive error handling with specific error classes
49
- - HTTP client with automatic retry logic and exponential backoff
50
- - Circuit breaker pattern for resilience (via Stoplight)
51
- - 99.7% test coverage with 339 comprehensive test cases
52
- - Extensive documentation with guides for Rails, tracing, and migration
53
-
54
- #### Dependencies
55
- - Ruby >= 3.2.0
56
- - No Rails dependency (works with any Ruby project)
57
- - Minimal runtime dependencies (Faraday, Mustache, OpenTelemetry)
58
-
59
- [Unreleased]: https://github.com/langfuse/langfuse-ruby/compare/v1.0.0...HEAD
60
- [1.0.0]: https://github.com/langfuse/langfuse-ruby/releases/tag/v1.0.0
10
+ ### Added
11
+ - Create and update methods for prompts (#36)
12
+
13
+
14
+ ## [0.2.0] - 2025-12-19
15
+
16
+ ### Added
17
+ - Prompt creation and update methods (`create_prompt`, `update_prompt`)
18
+ - Extended prompt management documentation with create/update examples
19
+
20
+ ## [0.1.0] - 2025-12-01
21
+
22
+ ### Added
23
+ - Observe API with context propagation and scoring (#31)
24
+ - W3C TraceContext propagator for distributed tracing (#1)
25
+ - Ruby 3.4 support (#3)
26
+ - OpenTelemetry-based tracing with OTLP export
27
+ - Distributed caching with Rails.cache backend and stampede protection
28
+ - Prompt management (text and chat) with Mustache templating
29
+ - In-memory caching with TTL and LRU eviction
30
+ - Fallback prompt support
31
+ - Global configuration pattern with `Langfuse.configure`
32
+
33
+ ### Changed
34
+ - Migrated from legacy ingestion API to OTLP endpoint
35
+ - Removed `tracing_enabled` configuration flag (#2)
36
+
37
+ [Unreleased]: https://github.com/simplepractice/langfuse-rb/compare/v0.1.0...HEAD
38
+ [0.1.0]: https://github.com/simplepractice/langfuse-rb/releases/tag/v0.1.0
data/README.md CHANGED
@@ -2,13 +2,15 @@
2
2
 
3
3
  # Langfuse Ruby SDK
4
4
 
5
- [![Gem Version](https://badge.fury.io/rb/langfuse.svg)](https://badge.fury.io/rb/langfuse)
5
+ [![Gem Version](https://badge.fury.io/rb/langfuse-rb.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/langfuse-rb)
6
6
  [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2.0-ruby.svg)](https://www.ruby-lang.org/en/)
7
7
  [![Test Coverage](https://img.shields.io/badge/coverage-99.6%25-brightgreen.svg)](coverage)
8
8
 
9
9
  > Ruby SDK for [Langfuse](https://langfuse.com) - Open-source LLM observability and prompt management.
10
10
 
11
- ## Features
11
+ <br>
12
+
13
+ ### Features
12
14
 
13
15
  - 🎯 **Prompt Management** - Centralized prompt versioning with Mustache templating
14
16
  - 📊 **LLM Tracing** - Zero-boilerplate observability built on OpenTelemetry
@@ -18,24 +20,24 @@
18
20
  - 🛡️ **Fallback Support** - Graceful degradation when API unavailable
19
21
  - 🚀 **Rails-Friendly** - Global configuration pattern, works with any Ruby project
20
22
 
21
- ## Installation
23
+ <br>
24
+
25
+ ### Installation
22
26
 
23
27
  ```ruby
24
- # Gemfile
28
+ # Add to Gemfile & bundle install
25
29
  gem 'langfuse-rb'
26
30
  ```
27
31
 
28
- ```bash
29
- bundle install
30
- ```
32
+ <br>
31
33
 
32
- ## Quick Start
34
+ ### Quick Start
33
35
 
34
- **Configure once at startup:**
36
+ > Configure once at startup
35
37
 
36
38
  ```ruby
37
39
  # config/initializers/langfuse.rb (Rails)
38
- # or at the top of your script
40
+ # Or at the top of your script
39
41
  Langfuse.configure do |config|
40
42
  config.public_key = ENV['LANGFUSE_PUBLIC_KEY']
41
43
  config.secret_key = ENV['LANGFUSE_SECRET_KEY']
@@ -44,7 +46,7 @@ Langfuse.configure do |config|
44
46
  end
45
47
  ```
46
48
 
47
- **Fetch and use a prompt:**
49
+ > Fetch and use a prompt
48
50
 
49
51
  ```ruby
50
52
  prompt = Langfuse.client.get_prompt("greeting")
@@ -52,7 +54,7 @@ message = prompt.compile(name: "Alice")
52
54
  # => "Hello Alice!"
53
55
  ```
54
56
 
55
- **Trace an LLM call:**
57
+ > Trace an LLM call
56
58
 
57
59
  ```ruby
58
60
  Langfuse.observe("chat-completion", as_type: :generation) do |gen|
@@ -74,33 +76,37 @@ Langfuse.observe("chat-completion", as_type: :generation) do |gen|
74
76
  end
75
77
  ```
76
78
 
77
- > [!IMPORTANT]
79
+ > [!IMPORTANT]
78
80
  > For complete reference see [docs](./docs/) section.
79
81
 
80
- ## Requirements
82
+ <br>
83
+
84
+ ### Requirements
81
85
 
82
86
  - Ruby >= 3.2.0
83
87
  - No Rails dependency (works with any Ruby project)
84
88
 
85
- ## Contributing
89
+ <br>
90
+
91
+ ### Contributing
86
92
 
87
93
  We welcome contributions! Please:
88
94
 
89
- 1. Check existing [issues](https://github.com/simplepractice/langfuse-rb/issues) and roadmap
95
+ 1. Check existing [issues](https://github.com/simplepractice/langfuse-rb/issues)
90
96
  2. Open an issue to discuss your idea
91
97
  3. Fork the repo and create a feature branch
92
98
  4. Write tests (maintain >95% coverage)
93
99
  5. Ensure `bundle exec rspec` and `bundle exec rubocop` pass
94
100
  6. Submit a pull request
95
101
 
96
- See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
102
+ > [!TIP]
103
+ > See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
104
+
105
+ <br>
97
106
 
98
- ## Support
107
+ ### Support
99
108
 
100
109
  - **[GitHub Issues](https://github.com/simplepractice/langfuse-rb/issues)** - Bug reports and feature requests
101
110
  - **[Langfuse Documentation](https://langfuse.com/docs)** - Platform documentation
102
111
  - **[API Reference](https://api.reference.langfuse.com)** - REST API reference
103
112
 
104
- ## License
105
-
106
- [MIT](LICENSE)
@@ -4,6 +4,7 @@ require "faraday"
4
4
  require "faraday/retry"
5
5
  require "base64"
6
6
  require "json"
7
+ require "uri"
7
8
 
8
9
  module Langfuse
9
10
  # HTTP client for Langfuse API
@@ -128,6 +129,84 @@ module Langfuse
128
129
  end
129
130
  end
130
131
 
132
+ # Create a new prompt (or new version if prompt with same name exists)
133
+ #
134
+ # @param name [String] The prompt name
135
+ # @param prompt [String, Array<Hash>] The prompt content
136
+ # @param type [String] Prompt type ("text" or "chat")
137
+ # @param config [Hash] Optional configuration (model params, etc.)
138
+ # @param labels [Array<String>] Optional labels (e.g., ["production"])
139
+ # @param tags [Array<String>] Optional tags
140
+ # @param commit_message [String, nil] Optional commit message
141
+ # @return [Hash] The created prompt data
142
+ # @raise [UnauthorizedError] if authentication fails
143
+ # @raise [ApiError] for other API errors
144
+ #
145
+ # @example Create a text prompt
146
+ # api_client.create_prompt(
147
+ # name: "greeting",
148
+ # prompt: "Hello {{name}}!",
149
+ # type: "text",
150
+ # labels: ["production"]
151
+ # )
152
+ #
153
+ # rubocop:disable Metrics/ParameterLists
154
+ def create_prompt(name:, prompt:, type:, config: {}, labels: [], tags: [], commit_message: nil)
155
+ path = "/api/public/v2/prompts"
156
+ payload = {
157
+ name: name,
158
+ prompt: prompt,
159
+ type: type,
160
+ config: config,
161
+ labels: labels,
162
+ tags: tags
163
+ }
164
+ payload[:commitMessage] = commit_message if commit_message
165
+
166
+ response = connection.post(path, payload)
167
+ handle_response(response)
168
+ rescue Faraday::RetriableResponse => e
169
+ logger.error("Faraday error: Retries exhausted - #{e.response.status}")
170
+ handle_response(e.response)
171
+ rescue Faraday::Error => e
172
+ logger.error("Faraday error: #{e.message}")
173
+ raise ApiError, "HTTP request failed: #{e.message}"
174
+ end
175
+ # rubocop:enable Metrics/ParameterLists
176
+
177
+ # Update labels for an existing prompt version
178
+ #
179
+ # @param name [String] The prompt name
180
+ # @param version [Integer] The version number to update
181
+ # @param labels [Array<String>] New labels (replaces existing). Required.
182
+ # @return [Hash] The updated prompt data
183
+ # @raise [ArgumentError] if labels is not an array
184
+ # @raise [NotFoundError] if the prompt is not found
185
+ # @raise [UnauthorizedError] if authentication fails
186
+ # @raise [ApiError] for other API errors
187
+ #
188
+ # @example Promote a prompt to production
189
+ # api_client.update_prompt(
190
+ # name: "greeting",
191
+ # version: 2,
192
+ # labels: ["production"]
193
+ # )
194
+ def update_prompt(name:, version:, labels:)
195
+ raise ArgumentError, "labels must be an array" unless labels.is_a?(Array)
196
+
197
+ path = "/api/public/v2/prompts/#{URI.encode_uri_component(name)}/versions/#{version}"
198
+ payload = { newLabels: labels }
199
+
200
+ response = connection.patch(path, payload)
201
+ handle_response(response)
202
+ rescue Faraday::RetriableResponse => e
203
+ logger.error("Faraday error: Retries exhausted - #{e.response.status}")
204
+ handle_response(e.response)
205
+ rescue Faraday::Error => e
206
+ logger.error("Faraday error: #{e.message}")
207
+ raise ApiError, "HTTP request failed: #{e.message}"
208
+ end
209
+
131
210
  # Send a batch of events to the Langfuse ingestion API
132
211
  #
133
212
  # Sends events (scores, traces, observations) to the ingestion endpoint.
@@ -180,7 +259,7 @@ module Langfuse
180
259
  # @raise [ApiError] for other API errors
181
260
  def fetch_prompt_from_api(name, version: nil, label: nil)
182
261
  params = build_prompt_params(version: version, label: label)
183
- path = "/api/public/v2/prompts/#{name}"
262
+ path = "/api/public/v2/prompts/#{URI.encode_uri_component(name)}"
184
263
 
185
264
  response = connection.get(path, params)
186
265
  handle_response(response)
@@ -215,7 +294,9 @@ module Langfuse
215
294
  # Retries transient errors with exponential backoff:
216
295
  # - Max 2 retries (3 total attempts)
217
296
  # - Exponential backoff (0.05s * 2^retry_count)
218
- # - Retries GET requests and POST requests to batch endpoint (idempotent operations)
297
+ # - Retries GET and PATCH requests (idempotent operations)
298
+ # - Retries POST requests to batch endpoint (idempotent due to event UUIDs)
299
+ # - Note: POST to create_prompt is NOT idempotent; retries may create duplicate versions
219
300
  # - Retries on: 429 (rate limit), 503 (service unavailable), 504 (gateway timeout)
220
301
  # - Does NOT retry on: 4xx errors (except 429), 5xx errors (except 503, 504)
221
302
  #
@@ -225,7 +306,7 @@ module Langfuse
225
306
  max: 2,
226
307
  interval: 0.05,
227
308
  backoff_factor: 2,
228
- methods: %i[get post],
309
+ methods: %i[get post patch],
229
310
  retry_statuses: [429, 503, 504],
230
311
  exceptions: [Faraday::TimeoutError, Faraday::ConnectionFailed]
231
312
  }
@@ -278,7 +359,7 @@ module Langfuse
278
359
  # @raise [ApiError] for other error statuses
279
360
  def handle_response(response)
280
361
  case response.status
281
- when 200
362
+ when 200, 201
282
363
  response.body
283
364
  when 401
284
365
  raise UnauthorizedError, "Authentication failed. Check your API keys."
@@ -17,6 +17,7 @@ module Langfuse
17
17
  # prompt = client.get_prompt("greeting")
18
18
  # compiled = prompt.compile(name: "Alice")
19
19
  #
20
+ # rubocop:disable Metrics/ClassLength
20
21
  class Client
21
22
  attr_reader :config, :api_client
22
23
 
@@ -139,6 +140,93 @@ module Langfuse
139
140
  prompt.compile(**variables)
140
141
  end
141
142
 
143
+ # Create a new prompt (or new version if name already exists)
144
+ #
145
+ # Creates a new prompt in Langfuse. If a prompt with the same name already
146
+ # exists, this creates a new version of that prompt.
147
+ #
148
+ # @param name [String] The prompt name (required)
149
+ # @param prompt [String, Array<Hash>] The prompt content (required)
150
+ # - For text prompts: a string with {{variable}} placeholders
151
+ # - For chat prompts: array of message hashes with role and content
152
+ # @param type [Symbol] Prompt type (:text or :chat) (required)
153
+ # @param config [Hash] Optional configuration (model parameters, tools, etc.)
154
+ # @param labels [Array<String>] Optional labels (e.g., ["production"])
155
+ # @param tags [Array<String>] Optional tags for categorization
156
+ # @param commit_message [String, nil] Optional commit message
157
+ # @return [TextPromptClient, ChatPromptClient] The created prompt client
158
+ # @raise [ArgumentError] if required parameters are missing or invalid
159
+ # @raise [UnauthorizedError] if authentication fails
160
+ # @raise [ApiError] for other API errors
161
+ #
162
+ # @example Create a text prompt
163
+ # prompt = client.create_prompt(
164
+ # name: "greeting",
165
+ # prompt: "Hello {{name}}!",
166
+ # type: :text,
167
+ # labels: ["production"],
168
+ # config: { model: "gpt-4o", temperature: 0.7 }
169
+ # )
170
+ #
171
+ # @example Create a chat prompt
172
+ # prompt = client.create_prompt(
173
+ # name: "support-bot",
174
+ # prompt: [
175
+ # { role: "system", content: "You are a {{role}} assistant" },
176
+ # { role: "user", content: "{{question}}" }
177
+ # ],
178
+ # type: :chat,
179
+ # labels: ["staging"]
180
+ # )
181
+ # rubocop:disable Metrics/ParameterLists
182
+ def create_prompt(name:, prompt:, type:, config: {}, labels: [], tags: [], commit_message: nil)
183
+ validate_prompt_type!(type)
184
+ validate_prompt_content!(prompt, type)
185
+
186
+ prompt_data = api_client.create_prompt(
187
+ name: name,
188
+ prompt: normalize_prompt_content(prompt, type),
189
+ type: type.to_s,
190
+ config: config,
191
+ labels: labels,
192
+ tags: tags,
193
+ commit_message: commit_message
194
+ )
195
+
196
+ build_prompt_client(prompt_data)
197
+ end
198
+ # rubocop:enable Metrics/ParameterLists
199
+
200
+ # Update an existing prompt version's metadata
201
+ #
202
+ # Updates the labels of an existing prompt version.
203
+ # Note: The prompt content itself cannot be changed after creation.
204
+ #
205
+ # @param name [String] The prompt name (required)
206
+ # @param version [Integer] The version number to update (required)
207
+ # @param labels [Array<String>] New labels (replaces existing). Required.
208
+ # @return [TextPromptClient, ChatPromptClient] The updated prompt client
209
+ # @raise [ArgumentError] if labels is not an array
210
+ # @raise [NotFoundError] if the prompt is not found
211
+ # @raise [UnauthorizedError] if authentication fails
212
+ # @raise [ApiError] for other API errors
213
+ #
214
+ # @example Update labels to promote to production
215
+ # prompt = client.update_prompt(
216
+ # name: "greeting",
217
+ # version: 2,
218
+ # labels: ["production"]
219
+ # )
220
+ def update_prompt(name:, version:, labels:)
221
+ prompt_data = api_client.update_prompt(
222
+ name: name,
223
+ version: version,
224
+ labels: labels
225
+ )
226
+
227
+ build_prompt_client(prompt_data)
228
+ end
229
+
142
230
  # Generate URL for viewing a trace in Langfuse UI
143
231
  #
144
232
  # @param trace_id [String] The trace ID (hex-encoded, 32 characters)
@@ -314,6 +402,8 @@ module Langfuse
314
402
  # @return [TextPromptClient, ChatPromptClient]
315
403
  # @raise [ArgumentError] if type is invalid
316
404
  def build_fallback_prompt_client(name, fallback, type)
405
+ validate_prompt_type!(type)
406
+
317
407
  # Create minimal prompt data structure
318
408
  prompt_data = {
319
409
  "name" => name,
@@ -330,9 +420,58 @@ module Langfuse
330
420
  TextPromptClient.new(prompt_data)
331
421
  when :chat
332
422
  ChatPromptClient.new(prompt_data)
333
- else
334
- raise ArgumentError, "Invalid type: #{type}. Must be :text or :chat"
423
+ end
424
+ end
425
+
426
+ # Validate prompt type parameter
427
+ #
428
+ # @param type [Symbol] The type to validate
429
+ # @raise [ArgumentError] if type is invalid
430
+ def validate_prompt_type!(type)
431
+ valid_types = %i[text chat]
432
+ return if valid_types.include?(type)
433
+
434
+ raise ArgumentError, "Invalid type: #{type}. Must be :text or :chat"
435
+ end
436
+
437
+ # Validate prompt content matches the declared type
438
+ #
439
+ # @param prompt [String, Array] The prompt content
440
+ # @param type [Symbol] The declared type
441
+ # @raise [ArgumentError] if content doesn't match type
442
+ def validate_prompt_content!(prompt, type)
443
+ case type
444
+ when :text
445
+ raise ArgumentError, "Text prompt must be a String" unless prompt.is_a?(String)
446
+ when :chat
447
+ raise ArgumentError, "Chat prompt must be an Array" unless prompt.is_a?(Array)
448
+ end
449
+ end
450
+
451
+ # Normalize prompt content for API request
452
+ #
453
+ # Converts Ruby symbol keys to string keys for chat messages
454
+ #
455
+ # @param prompt [String, Array] The prompt content
456
+ # @param type [Symbol] The prompt type
457
+ # @return [String, Array] Normalized content
458
+ def normalize_prompt_content(prompt, type)
459
+ return prompt if type == :text
460
+
461
+ # Normalize chat messages to use string keys
462
+ prompt.map do |message|
463
+ # Convert all keys to symbols first, then extract
464
+ normalized = message.transform_keys do |k|
465
+ k.to_sym
466
+ rescue StandardError
467
+ k
468
+ end
469
+ {
470
+ "role" => normalized[:role]&.to_s,
471
+ "content" => normalized[:content]
472
+ }
335
473
  end
336
474
  end
337
475
  end
476
+ # rubocop:enable Metrics/ClassLength
338
477
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Langfuse
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: langfuse-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SimplePractice
@@ -171,7 +171,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
171
171
  - !ruby/object:Gem::Version
172
172
  version: '0'
173
173
  requirements: []
174
- rubygems_version: 3.7.2
174
+ rubygems_version: 4.0.2
175
175
  specification_version: 4
176
176
  summary: Ruby SDK for Langfuse - LLM observability and prompt management
177
177
  test_files: []