langfuse 0.1.0 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e165c60bb44cb4d93fa3bfcda49f4948078714bc80e50a37fb90501093915b3b
4
- data.tar.gz: 5e930ae7b06502dce3c805b4b820cb1de7826b5bfc4d690f82fb1ba1b53e2053
3
+ metadata.gz: 13d36d5500f51cce6bf73e497b4d173d62da2e8be2c9142a3eba5b563b786f3a
4
+ data.tar.gz: ffa0ca7036c84a49e771e43945db532d7382cf08b8894e8b58f71ad994e9179a
5
5
  SHA512:
6
- metadata.gz: e0dd66aa77a3389cff7eff1ac9ce9a7be987821179f61cf0f2635136ead05c486100675905971be8f8f4b0d33bf999718a09ee0b0ccb67a90e94426cdb44f0b6
7
- data.tar.gz: 02b1a9ae14210d8216fa180b9dda79477502fdb117ef6a023b05e0b1d183b9823d38ff56e107b19edc1b6db3f0a5cb5fe77f96ee78523275cc818488a76fd7fc
6
+ metadata.gz: d3e236f29058f9d03879d9413e5be6ed8c8780c95ab5ceace863c1fdddb0564ec9422a194a474f4b9cbfed6656dc30f15d37917de78a73195dc3f5293f9fa2d9
7
+ data.tar.gz: 6c08d42e2feb0c900b92f1c5f7cc991afaf0d6e3eb12432d64243258ad31ab6d12e2fada6c481990563f53242ca0ebb7caed082807cf56f96e4e5fc62e88e8f2
data/bin/tapioca ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'tapioca' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("tapioca", "tapioca")
@@ -1,16 +1,24 @@
1
+ # typed: strict
2
+
1
3
  require 'net/http'
2
4
  require 'uri'
3
5
  require 'json'
4
6
  require 'base64'
7
+ require 'sorbet-runtime'
5
8
 
6
9
  module Langfuse
7
10
  class ApiClient
11
+ extend T::Sig
12
+
13
+ sig { returns(T.untyped) }
8
14
  attr_reader :config
9
15
 
16
+ sig { params(config: T.untyped).void }
10
17
  def initialize(config)
11
18
  @config = config
12
19
  end
13
20
 
21
+ sig { params(events: T::Array[T::Hash[T.untyped, T.untyped]]).returns(T::Hash[String, T.untyped]) }
14
22
  def ingest(events)
15
23
  uri = URI.parse("#{@config.host}/api/public/ingestion")
16
24
 
@@ -44,14 +52,18 @@ module Langfuse
44
52
  log("Request url: #{uri}")
45
53
  end
46
54
 
55
+ log('---') # Moved log statement before response handling to avoid affecting return value
56
+
47
57
  response = http.request(request)
48
58
 
59
+ result = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
60
+
49
61
  if response.code.to_i == 207 # Partial success
50
62
  log('Received 207 partial success response') if @config.debug
51
- JSON.parse(response.body)
63
+ result = JSON.parse(response.body)
52
64
  elsif response.code.to_i >= 200 && response.code.to_i < 300
53
65
  log("Received successful response: #{response.code}") if @config.debug
54
- JSON.parse(response.body)
66
+ result = JSON.parse(response.body)
55
67
  else
56
68
  error_msg = "API error: #{response.code} #{response.message}"
57
69
  if @config.debug
@@ -62,7 +74,7 @@ module Langfuse
62
74
  raise error_msg
63
75
  end
64
76
 
65
- log('---')
77
+ result
66
78
  rescue StandardError => e
67
79
  log("Error during API request: #{e.message}", :error)
68
80
  raise
@@ -70,11 +82,11 @@ module Langfuse
70
82
 
71
83
  private
72
84
 
85
+ sig { params(message: String, level: Symbol).returns(T.untyped) }
73
86
  def log(message, level = :debug)
74
87
  return unless @config.debug
75
88
 
76
- logger = defined?(Rails) ? Rails.logger : Logger.new($stdout)
77
- logger.send(level, "[Langfuse] #{message}")
89
+ T.unsafe(@config.logger).send(level, "[Langfuse] #{message}")
78
90
  end
79
91
  end
80
92
  end
@@ -1,57 +1,83 @@
1
+ # typed: strict
2
+
3
+ require 'sorbet-runtime'
4
+
1
5
  module Langfuse
2
6
  class BatchWorker
7
+ extend T::Sig
3
8
  # This is a placeholder class that will be defined with Sidekiq::Worker
4
9
  # when Sidekiq is available.
5
10
  #
6
11
  # If Sidekiq is available, this will be replaced with a real worker class
7
12
  # that includes Sidekiq::Worker
8
13
 
14
+ # Ensure return type matches the synchronous perform call
15
+ sig { params(events: T::Array[T::Hash[T.untyped, T.untyped]]).void }
9
16
  def self.perform_async(events)
10
- # When Sidekiq is not available, process synchronously
17
+ # When Sidekiq is not available, process synchronously and return result
11
18
  new.perform(events)
12
19
  end
13
20
 
21
+ sig { params(events: T::Array[T::Hash[T.untyped, T.untyped]]).returns(T::Hash[String, T.untyped]) }
14
22
  def perform(events)
15
- Langfuse::ApiClient.new(Langfuse.configuration).ingest(events)
23
+ # Assuming Langfuse.configuration returns a valid config object for ApiClient
24
+ T.unsafe(Langfuse::ApiClient).new(T.unsafe(Langfuse).configuration).ingest(events)
16
25
  end
17
26
  end
18
27
 
19
28
  # Define the real Sidekiq worker if Sidekiq is available
20
29
  if defined?(Sidekiq)
21
30
  class BatchWorker
31
+ # Re-extend T::Sig within the conditional definition
32
+ extend T::Sig
33
+ # Include Sidekiq::Worker directly - rely on T.unsafe for its methods
22
34
  include Sidekiq::Worker
23
35
 
24
- sidekiq_options queue: 'langfuse', retry: 5, backtrace: true
36
+ # Using T.unsafe for sidekiq_options DSL
37
+ T.unsafe(self).sidekiq_options queue: 'langfuse', retry: 5, backtrace: true
25
38
 
26
39
  # Custom retry delay logic (exponential backoff)
27
- sidekiq_retry_in do |count|
40
+ # Using T.unsafe for sidekiq_retry_in DSL
41
+ T.unsafe(self).sidekiq_retry_in do |count|
28
42
  10 * (count + 1) # 10s, 20s, 30s, 40s, 50s
29
43
  end
30
44
 
45
+ sig { params(event_hashes: T::Array[T::Hash[T.untyped, T.untyped]]).void }
31
46
  def perform(event_hashes)
32
- api_client = ApiClient.new(Langfuse.configuration)
47
+ # Assuming Langfuse.configuration returns a valid config object
48
+ api_client = T.unsafe(ApiClient).new(T.unsafe(Langfuse).configuration)
33
49
 
34
50
  begin
35
51
  response = api_client.ingest(event_hashes)
36
52
 
37
- # Check for partial failures
38
- if response && response['errors']&.any?
39
- response['errors'].each do |error|
40
- logger.error("Langfuse API error for event #{error['id']}: #{error['message']}")
53
+ # Check for partial failures using standard hash access
54
+ errors = T.let(response['errors'], T.nilable(T::Array[T::Hash[String, T.untyped]]))
55
+ if errors && errors.any?
56
+ errors.each do |error|
57
+ # Use T.unsafe(self).logger provided by Sidekiq::Worker
58
+ T.unsafe(self).logger.error("Langfuse API error for event #{error['id']}: #{error['message']}")
41
59
 
42
60
  # Store permanently failed events if needed
43
- if non_retryable_error?(error['status'])
44
- store_failed_event(event_hashes.find { |e| e[:id] == error['id'] }, error['message'])
61
+ # Assuming error['status'] exists and can be converted to integer
62
+ status = T.let(error['status'], T.untyped)
63
+ next unless non_retryable_error?(status)
64
+
65
+ # Assuming event_hashes elements have :id key
66
+ failed_event = event_hashes.find { |e| T.unsafe(e)[:id] == error['id'] }
67
+ if failed_event
68
+ # Remove redundant T.cast
69
+ store_failed_event(failed_event, T.cast(error['message'], String))
45
70
  end
46
71
  end
47
72
  end
48
73
  rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED => e
49
74
  # Network errors - Sidekiq will retry
50
- logger.error("Langfuse network error: #{e.message}")
75
+ T.unsafe(self).logger.error("Langfuse network error: #{e.full_message}")
51
76
  raise
52
77
  rescue StandardError => e
53
78
  # Other errors
54
- logger.error("Langfuse API error: #{e.message}")
79
+ # Use T.unsafe(self).logger
80
+ T.unsafe(self).logger.error("Langfuse API error: #{e.message}")
55
81
 
56
82
  # Let Sidekiq retry
57
83
  raise
@@ -60,17 +86,21 @@ module Langfuse
60
86
 
61
87
  private
62
88
 
89
+ sig { params(status: T.untyped).returns(T::Boolean) }
63
90
  def non_retryable_error?(status)
64
91
  # 4xx errors except 429 (rate limit) are not retryable
65
- status.to_i >= 400 && status.to_i < 500 && status.to_i != 429
92
+ status_int = T.let(status.to_i, Integer)
93
+ status_int >= 400 && status_int < 500 && status_int != 429
66
94
  end
67
95
 
68
- def store_failed_event(event, error)
96
+ sig { params(event: T::Hash[T.untyped, T.untyped], error_msg: String).returns(T.untyped) }
97
+ def store_failed_event(event, error_msg)
69
98
  # Store in Redis for later inspection/retry
70
- Sidekiq.redis do |redis|
71
- redis.rpush('langfuse:failed_events', {
99
+ # Using T.unsafe for Sidekiq.redis block and redis operations
100
+ T.unsafe(Sidekiq).redis do |redis|
101
+ T.unsafe(redis).rpush('langfuse:failed_events', {
72
102
  event: event,
73
- error: error,
103
+ error: error_msg,
74
104
  timestamp: Time.now.utc.iso8601
75
105
  }.to_json)
76
106
  end
@@ -1,29 +1,50 @@
1
+ # typed: strict
2
+
1
3
  require 'singleton'
2
4
  require 'concurrent'
3
5
  require 'logger'
6
+ require 'sorbet-runtime'
7
+
8
+ # Model requires are implicitly handled by the main langfuse.rb require
9
+ # No need for placeholder type aliases here
4
10
 
5
11
  module Langfuse
6
12
  class Client
13
+ extend T::Sig
7
14
  include Singleton
8
15
 
16
+ sig { returns(::Langfuse::Configuration) } # Use actual Configuration type
17
+ attr_reader :config
18
+
19
+ # Use the class directly, Sorbet should handle Concurrent::Array generics
20
+ sig { returns(Concurrent::Array) }
21
+ attr_reader :events
22
+
23
+ sig { returns(T.nilable(Thread)) }
24
+ attr_reader :flush_thread
25
+
26
+ sig { void }
9
27
  def initialize
10
- @config = Langfuse.configuration
11
- @events = Concurrent::Array.new # Thread-safe array
12
- @mutex = Mutex.new # For operations that need additional thread safety
28
+ @config = T.let(Langfuse.configuration, ::Langfuse::Configuration)
29
+ # Let Sorbet infer the type for Concurrent::Array here
30
+ @events = T.let(Concurrent::Array.new, Concurrent::Array)
31
+ @mutex = T.let(Mutex.new, Mutex)
32
+ @flush_thread = T.let(nil, T.nilable(Thread))
13
33
 
14
- # Start periodic flusher only in server context
15
- schedule_periodic_flush if defined?(Rails) && Rails.server?
34
+ schedule_periodic_flush
16
35
 
17
36
  # Register shutdown hook
18
37
  return if @config.disable_at_exit_hook
19
38
 
20
- at_exit { shutdown }
39
+ Kernel.at_exit { shutdown }
21
40
  end
22
41
 
23
42
  # Creates a new trace
43
+ sig { params(attributes: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
24
44
  def trace(attributes = {})
25
- trace = Models::Trace.new(attributes)
26
- event = Models::IngestionEvent.new(
45
+ # Ideally Models::Trace.new would have its own signature
46
+ trace = T.unsafe(Models::Trace).new(attributes)
47
+ event = T.unsafe(Models::IngestionEvent).new(
27
48
  type: 'trace-create',
28
49
  body: trace
29
50
  )
@@ -32,11 +53,12 @@ module Langfuse
32
53
  end
33
54
 
34
55
  # Creates a new span within a trace
56
+ sig { params(attributes: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
35
57
  def span(attributes = {})
36
58
  raise ArgumentError, 'trace_id is required for creating a span' unless attributes[:trace_id]
37
59
 
38
- span = Models::Span.new(attributes)
39
- event = Models::IngestionEvent.new(
60
+ span = T.unsafe(Models::Span).new(attributes)
61
+ event = T.unsafe(Models::IngestionEvent).new(
40
62
  type: 'span-create',
41
63
  body: span
42
64
  )
@@ -45,10 +67,15 @@ module Langfuse
45
67
  end
46
68
 
47
69
  # Updates an existing span
70
+ sig { params(span: T.untyped).returns(T.untyped) }
48
71
  def update_span(span)
49
- raise ArgumentError, 'span.id and span.trace_id are required for updating a span' unless span.id && span.trace_id
72
+ # Assuming span object has :id and :trace_id methods/attributes
73
+ unless T.unsafe(span).id && T.unsafe(span).trace_id
74
+ raise ArgumentError,
75
+ 'span.id and span.trace_id are required for updating a span'
76
+ end
50
77
 
51
- event = Models::IngestionEvent.new(
78
+ event = T.unsafe(Models::IngestionEvent).new(
52
79
  type: 'span-update',
53
80
  body: span
54
81
  )
@@ -57,11 +84,12 @@ module Langfuse
57
84
  end
58
85
 
59
86
  # Creates a new generation within a trace
87
+ sig { params(attributes: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
60
88
  def generation(attributes = {})
61
89
  raise ArgumentError, 'trace_id is required for creating a generation' unless attributes[:trace_id]
62
90
 
63
- generation = Models::Generation.new(attributes)
64
- event = Models::IngestionEvent.new(
91
+ generation = T.unsafe(Models::Generation).new(attributes)
92
+ event = T.unsafe(Models::IngestionEvent).new(
65
93
  type: 'generation-create',
66
94
  body: generation
67
95
  )
@@ -70,12 +98,13 @@ module Langfuse
70
98
  end
71
99
 
72
100
  # Updates an existing generation
101
+ sig { params(generation: T.untyped).returns(T.untyped) }
73
102
  def update_generation(generation)
74
- unless generation.id && generation.trace_id
103
+ unless T.unsafe(generation).id && T.unsafe(generation).trace_id
75
104
  raise ArgumentError, 'generation.id and generation.trace_id are required for updating a generation'
76
105
  end
77
106
 
78
- event = Models::IngestionEvent.new(
107
+ event = T.unsafe(Models::IngestionEvent).new(
79
108
  type: 'generation-update',
80
109
  body: generation
81
110
  )
@@ -84,11 +113,12 @@ module Langfuse
84
113
  end
85
114
 
86
115
  # Creates a new event within a trace
116
+ sig { params(attributes: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
87
117
  def event(attributes = {})
88
118
  raise ArgumentError, 'trace_id is required for creating an event' unless attributes[:trace_id]
89
119
 
90
- event_obj = Models::Event.new(attributes)
91
- event = Models::IngestionEvent.new(
120
+ event_obj = T.unsafe(Models::Event).new(attributes)
121
+ event = T.unsafe(Models::IngestionEvent).new(
92
122
  type: 'event-create',
93
123
  body: event_obj
94
124
  )
@@ -97,11 +127,12 @@ module Langfuse
97
127
  end
98
128
 
99
129
  # Creates a new score
130
+ sig { params(attributes: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
100
131
  def score(attributes = {})
101
132
  raise ArgumentError, 'trace_id is required for creating a score' unless attributes[:trace_id]
102
133
 
103
- score = Models::Score.new(attributes)
104
- event = Models::IngestionEvent.new(
134
+ score = T.unsafe(Models::Score).new(attributes)
135
+ event = T.unsafe(Models::IngestionEvent).new(
105
136
  type: 'score-create',
106
137
  body: score
107
138
  )
@@ -110,8 +141,9 @@ module Langfuse
110
141
  end
111
142
 
112
143
  # Flushes all pending events to the API
144
+ sig { void }
113
145
  def flush
114
- events_to_process = nil
146
+ events_to_process = T.let([], T::Array[T.untyped])
115
147
 
116
148
  # Atomically swap the events array to avoid race conditions
117
149
  @mutex.synchronize do
@@ -122,15 +154,17 @@ module Langfuse
122
154
  return if events_to_process.empty?
123
155
 
124
156
  # Convert objects to hashes for serialization
157
+ # Assuming `to_h` exists on Models::IngestionEvent and returns T::Hash[T.untyped, T.untyped]
125
158
  event_hashes = events_to_process.map(&:to_h)
126
159
 
127
160
  log("Flushing #{event_hashes.size} events")
128
161
 
129
162
  # Send to background worker
130
- BatchWorker.perform_async(event_hashes)
163
+ T.unsafe(BatchWorker).perform_async(event_hashes)
131
164
  end
132
165
 
133
166
  # Gracefully shuts down the client, ensuring all events are flushed
167
+ sig { void }
134
168
  def shutdown
135
169
  log('Shutting down Langfuse client...')
136
170
 
@@ -145,18 +179,22 @@ module Langfuse
145
179
 
146
180
  private
147
181
 
182
+ sig { params(event: T.untyped).void }
148
183
  def enqueue_event(event)
149
184
  @events << event
150
185
 
151
186
  # Trigger immediate flush if batch size reached
187
+ # Assuming @config.batch_size is an Integer
152
188
  flush if @events.size >= @config.batch_size
153
189
  end
154
190
 
191
+ sig { returns(Thread) }
155
192
  def schedule_periodic_flush
156
193
  log("Starting periodic flush thread (interval: #{@config.flush_interval}s)")
157
194
 
158
195
  @flush_thread = Thread.new do
159
196
  loop do
197
+ # Assuming @config.flush_interval is Numeric
160
198
  sleep @config.flush_interval
161
199
  flush
162
200
  rescue StandardError => e
@@ -166,11 +204,12 @@ module Langfuse
166
204
  end
167
205
  end
168
206
 
207
+ sig { params(message: String, level: Symbol).returns(T.untyped) }
169
208
  def log(message, level = :debug)
209
+ # Assuming @config.debug is Boolean
170
210
  return unless @config.debug
171
211
 
172
- logger = defined?(Rails) ? Rails.logger : Logger.new($stdout)
173
- logger.send(level, "[Langfuse] #{message}")
212
+ T.unsafe(@config.logger).send(level, "[Langfuse] #{message}")
174
213
  end
175
214
  end
176
215
  end
@@ -1,20 +1,39 @@
1
+ # typed: strict
2
+
3
+ require 'logger'
4
+ require 'sorbet-runtime'
5
+
1
6
  module Langfuse
2
7
  class Configuration
3
- attr_accessor :public_key, :secret_key, :host,
4
- :batch_size, :flush_interval, :debug,
5
- :disable_at_exit_hook, :shutdown_timeout, :logger
8
+ extend T::Sig
9
+
10
+ sig { returns(T.nilable(String)) }
11
+ attr_accessor :public_key, :secret_key
12
+
13
+ sig { returns(String) }
14
+ attr_accessor :host
15
+
16
+ sig { returns(Integer) }
17
+ attr_accessor :batch_size, :flush_interval, :shutdown_timeout
18
+
19
+ sig { returns(T::Boolean) }
20
+ attr_accessor :debug, :disable_at_exit_hook
21
+
22
+ sig { returns(T.untyped) }
23
+ attr_accessor :logger
6
24
 
25
+ sig { void }
7
26
  def initialize
8
27
  # Default configuration with environment variable fallbacks
9
- @public_key = ENV['LANGFUSE_PUBLIC_KEY']
10
- @secret_key = ENV['LANGFUSE_SECRET_KEY']
11
- @host = ENV.fetch('LANGFUSE_HOST', 'https://us.cloud.langfuse.com')
12
- @batch_size = ENV.fetch('LANGFUSE_BATCH_SIZE', '10').to_i
13
- @flush_interval = ENV.fetch('LANGFUSE_FLUSH_INTERVAL', '60').to_i
14
- @debug = ENV.fetch('LANGFUSE_DEBUG', 'false') == 'true'
15
- @disable_at_exit_hook = false
16
- @shutdown_timeout = ENV.fetch('LANGFUSE_SHUTDOWN_TIMEOUT', '5').to_i
17
- @logger = Logger.new(STDOUT)
28
+ @public_key = T.let(ENV['LANGFUSE_PUBLIC_KEY'], T.nilable(String))
29
+ @secret_key = T.let(ENV['LANGFUSE_SECRET_KEY'], T.nilable(String))
30
+ @host = T.let(ENV.fetch('LANGFUSE_HOST', 'https://us.cloud.langfuse.com'), String)
31
+ @batch_size = T.let(ENV.fetch('LANGFUSE_BATCH_SIZE', '10').to_i, Integer)
32
+ @flush_interval = T.let(ENV.fetch('LANGFUSE_FLUSH_INTERVAL', '60').to_i, Integer)
33
+ @debug = T.let(ENV.fetch('LANGFUSE_DEBUG', 'false') == 'true', T::Boolean)
34
+ @disable_at_exit_hook = T.let(false, T::Boolean)
35
+ @shutdown_timeout = T.let(ENV.fetch('LANGFUSE_SHUTDOWN_TIMEOUT', '5').to_i, Integer)
36
+ @logger = T.let(Logger.new($stdout), Logger)
18
37
  end
19
38
  end
20
39
  end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  require 'securerandom'
2
3
 
3
4
  module Langfuse
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  require 'securerandom'
2
3
 
3
4
  module Langfuse
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  require 'securerandom'
2
3
 
3
4
  module Langfuse
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  require 'securerandom'
2
3
 
3
4
  module Langfuse
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  require 'securerandom'
2
3
 
3
4
  module Langfuse
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  require 'securerandom'
2
3
 
3
4
  module Langfuse
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  module Langfuse
2
3
  module Models
3
4
  class Usage
@@ -1,3 +1,4 @@
1
+ # typed: strict
1
2
  module Langfuse
2
- VERSION = '0.1.0'
3
+ VERSION = '0.1.1'
3
4
  end
data/lib/langfuse.rb CHANGED
@@ -1,7 +1,11 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
1
5
  require 'langfuse/version'
2
6
  require 'langfuse/configuration'
3
7
 
4
- # Load models
8
+ # Load models - Use fully qualified names in sigs below
5
9
  require 'langfuse/models/ingestion_event'
6
10
  require 'langfuse/models/trace'
7
11
  require 'langfuse/models/span'
@@ -13,57 +17,92 @@ require 'langfuse/models/usage'
13
17
  # Load API client
14
18
  require 'langfuse/api_client'
15
19
 
16
- # Load batch worker (works with or without Sidekiq)
20
+ # Load batch worker
17
21
  require 'langfuse/batch_worker'
18
22
 
19
23
  # Load main client
20
24
  require 'langfuse/client'
21
25
 
22
26
  module Langfuse
27
+ extend T::Sig
28
+
23
29
  class << self
30
+ extend T::Sig
31
+
32
+ # Sig for the writer method created by attr_writer
33
+ sig { params(configuration: ::Langfuse::Configuration).void }
24
34
  attr_writer :configuration
25
35
 
36
+ # Sig for the reader method
37
+ sig { returns(::Langfuse::Configuration) }
26
38
  def configuration
27
- @configuration ||= Configuration.new
39
+ # Use T.let for clarity on initialization
40
+ @configuration = T.let(@configuration, T.nilable(::Langfuse::Configuration))
41
+ @configuration ||= ::Langfuse::Configuration.new
28
42
  end
29
43
 
30
- def configure
44
+ # Configuration block
45
+ sig { params(_block: T.proc.params(config: ::Langfuse::Configuration).void).void }
46
+ def configure(&_block)
47
+ # Pass the block to yield
31
48
  yield(configuration)
32
49
  end
33
50
 
34
- # Convenience delegators to the client instance
51
+ # --- Convenience delegators to the client instance ---
52
+
53
+ # Create Trace
54
+ sig { params(attributes: T::Hash[Symbol, T.untyped]).returns(::Langfuse::Models::Trace) }
35
55
  def trace(attributes = {})
36
- Client.instance.trace(attributes)
56
+ # Use T.unsafe as Client returns T.untyped for Models for now
57
+ T.unsafe(Client.instance).trace(attributes)
37
58
  end
38
59
 
60
+ # Create Span
61
+ sig { params(attributes: T::Hash[Symbol, T.untyped]).returns(::Langfuse::Models::Span) }
39
62
  def span(attributes = {})
40
- Client.instance.span(attributes)
63
+ T.unsafe(Client.instance).span(attributes)
41
64
  end
42
65
 
66
+ # Update Span
67
+ sig { params(span: ::Langfuse::Models::Span).void }
43
68
  def update_span(span)
44
- Client.instance.update_span(span)
69
+ T.unsafe(Client.instance).update_span(span)
70
+ # Return void implicitly
45
71
  end
46
72
 
73
+ # Create Generation
74
+ sig { params(attributes: T::Hash[Symbol, T.untyped]).returns(::Langfuse::Models::Generation) }
47
75
  def generation(attributes = {})
48
- Client.instance.generation(attributes)
76
+ T.unsafe(Client.instance).generation(attributes)
49
77
  end
50
78
 
79
+ # Update Generation
80
+ sig { params(generation: ::Langfuse::Models::Generation).void }
51
81
  def update_generation(generation)
52
- Client.instance.update_generation(generation)
82
+ T.unsafe(Client.instance).update_generation(generation)
83
+ # Return void implicitly
53
84
  end
54
85
 
86
+ # Create Event
87
+ sig { params(attributes: T::Hash[Symbol, T.untyped]).returns(::Langfuse::Models::Event) }
55
88
  def event(attributes = {})
56
- Client.instance.event(attributes)
89
+ T.unsafe(Client.instance).event(attributes)
57
90
  end
58
91
 
92
+ # Create Score
93
+ sig { params(attributes: T::Hash[Symbol, T.untyped]).returns(::Langfuse::Models::Score) }
59
94
  def score(attributes = {})
60
- Client.instance.score(attributes)
95
+ T.unsafe(Client.instance).score(attributes)
61
96
  end
62
97
 
98
+ # Flush events
99
+ sig { void }
63
100
  def flush
64
101
  Client.instance.flush
65
102
  end
66
103
 
104
+ # Shutdown client
105
+ sig { void }
67
106
  def shutdown
68
107
  Client.instance.shutdown
69
108
  end
@@ -1,30 +1,70 @@
1
+ # typed: true
2
+
3
+ require 'sorbet-runtime' # Ensure Sorbet is required
4
+
1
5
  class LangfuseContext
6
+ extend T::Sig # Add this to enable sig blocks on class methods
7
+
8
+ # Define the type for the context hash
9
+ ContextHash = T.type_alias { T::Hash[Symbol, T.nilable(String)] }
10
+
11
+ # Gets the current context hash for the thread
12
+ sig { returns(ContextHash) }
2
13
  def self.current
3
- Thread.current[:langfuse_context] ||= {}
14
+ # T.let is used for type assertion
15
+ context = T.let(Thread.current[:langfuse_context], T.nilable(ContextHash))
16
+ # Initialize if nil
17
+ context ||= T.let({}, ContextHash)
18
+ Thread.current[:langfuse_context] = context
19
+ context
4
20
  end
5
21
 
22
+ # Gets the current trace ID from the context
23
+ sig { returns(T.nilable(String)) }
6
24
  def self.current_trace_id
7
25
  current[:trace_id]
8
26
  end
9
27
 
28
+ # Gets the current span ID from the context
29
+ sig { returns(T.nilable(String)) }
10
30
  def self.current_span_id
11
31
  current[:span_id]
12
32
  end
13
33
 
14
- def self.with_trace(trace)
34
+ # Executes a block with a specific trace context
35
+ sig do
36
+ params(
37
+ trace: T.untyped, # Use T.untyped until Models::Trace is fully typed
38
+ _block: T.proc.void
39
+ ).void
40
+ end
41
+ def self.with_trace(trace, &_block)
15
42
  old_context = current.dup
16
43
  begin
17
- Thread.current[:langfuse_context] = { trace_id: trace.id }
44
+ # Assuming trace.id returns a String
45
+ trace_id = T.let(T.unsafe(trace).id, T.nilable(String))
46
+ Thread.current[:langfuse_context] = { trace_id: trace_id } if trace_id
18
47
  yield
19
48
  ensure
20
49
  Thread.current[:langfuse_context] = old_context
21
50
  end
22
51
  end
23
52
 
24
- def self.with_span(span)
53
+ # Executes a block with a specific span context (merging with existing context)
54
+ sig do
55
+ params(
56
+ span: T.untyped, # Use T.untyped until Models::Span is fully typed
57
+ _block: T.proc.void
58
+ ).void
59
+ end
60
+ def self.with_span(span, &_block)
25
61
  old_context = current.dup
26
62
  begin
27
- Thread.current[:langfuse_context] = current.merge({ span_id: span.id })
63
+ # Assuming span.id returns a String
64
+ span_id = T.let(T.unsafe(span).id, T.nilable(String))
65
+ # Merge span_id into the current context
66
+ new_context = current.merge({ span_id: span_id })
67
+ Thread.current[:langfuse_context] = new_context if span_id
28
68
  yield
29
69
  ensure
30
70
  Thread.current[:langfuse_context] = old_context
@@ -1,10 +1,25 @@
1
+ # typed: true
2
+
1
3
  require_relative 'langfuse_context'
4
+ require 'sorbet-runtime' # Ensure Sorbet is required
2
5
 
3
6
  module LangfuseHelper
7
+ extend T::Sig # Add this to enable sig blocks in the module
8
+
4
9
  # Execute a block within the context of a span
5
- def with_span(name:, trace_id:, parent_id: nil, input: nil, **attributes)
10
+ sig do
11
+ params(
12
+ name: String,
13
+ trace_id: String,
14
+ parent_id: T.nilable(String),
15
+ input: T.untyped,
16
+ attributes: T::Hash[Symbol, T.untyped],
17
+ block: T.proc.params(span: ::Langfuse::Models::Span).returns(T.untyped)
18
+ ).returns(T.untyped)
19
+ end
20
+ def with_span(name:, trace_id:, parent_id: nil, input: nil, **attributes, &block)
6
21
  # Create the span
7
- span = Langfuse.span(
22
+ span = T.unsafe(Langfuse).span(
8
23
  name: name,
9
24
  trace_id: trace_id,
10
25
  parent_observation_id: parent_id,
@@ -12,13 +27,26 @@ module LangfuseHelper
12
27
  **attributes
13
28
  )
14
29
 
15
- with_span_implementation(span) { yield(span) }
30
+ # Pass the block to the implementation
31
+ with_span_implementation(span, &block)
16
32
  end
17
33
 
18
34
  # Execute a block within the context of an LLM generation
19
- def with_generation(name:, trace_id:, model:, input:, parent_id: nil, model_parameters: {}, **attributes)
35
+ sig do
36
+ params(
37
+ name: String,
38
+ trace_id: String,
39
+ model: String,
40
+ input: T.untyped,
41
+ parent_id: T.nilable(String),
42
+ model_parameters: T::Hash[T.untyped, T.untyped],
43
+ attributes: T::Hash[Symbol, T.untyped],
44
+ block: T.proc.params(generation: ::Langfuse::Models::Generation).returns(T.untyped)
45
+ ).returns(T.untyped)
46
+ end
47
+ def with_generation(name:, trace_id:, model:, input:, parent_id: nil, model_parameters: {}, **attributes, &block)
20
48
  # Create the generation
21
- generation = Langfuse.generation(
49
+ generation = T.unsafe(Langfuse).generation(
22
50
  name: name,
23
51
  trace_id: trace_id,
24
52
  parent_observation_id: parent_id,
@@ -28,50 +56,59 @@ module LangfuseHelper
28
56
  **attributes
29
57
  )
30
58
 
31
- start_time = Time.now
32
- result = nil
33
- error = nil
59
+ T.let(Time.now, Time)
60
+ result = T.let(nil, T.untyped)
61
+ error = T.let(nil, T.nilable(StandardError))
34
62
 
35
63
  begin
36
64
  # Execute the block with the generation passed as argument
37
- result = yield(generation)
65
+ result = block.call(generation)
38
66
  result
39
67
  rescue StandardError => e
40
68
  # Capture any error
41
69
  error = e
42
- raise
70
+ Kernel.raise # Use Kernel.raise
43
71
  ensure
44
72
  # Always update the generation with results
45
73
  generation.end_time = Time.now.utc
46
- generation.start_time = start_time.utc
74
+ # generation.start_time = start_time.utc # start_time is already UTC if using Time.now.utc
47
75
 
48
76
  # Add output if there was a result and it wasn't already set
49
- generation.output = result if result && !generation.output
77
+ generation.output = result if result && generation.output.nil?
50
78
 
51
79
  # Add error information if there was an error
52
80
  if error
53
81
  generation.level = 'ERROR'
54
82
  generation.status_message = error.message
55
83
  generation.metadata ||= {}
56
- generation.metadata[:error_backtrace] = error.backtrace.first(10) if error.backtrace
84
+ backtrace = error.backtrace
85
+ generation.metadata[:error_backtrace] = backtrace.first(10) if backtrace # Check if backtrace is nil
57
86
  end
58
87
 
59
88
  # Update the generation
60
- Langfuse.update_generation(generation)
89
+ T.unsafe(Langfuse).update_generation(generation)
61
90
  end
62
91
  end
63
92
 
64
93
  # Execute a block within the context of a trace
65
- def with_trace(name:, user_id: nil, **attributes)
94
+ sig do
95
+ params(
96
+ name: String,
97
+ user_id: T.nilable(String),
98
+ attributes: T::Hash[Symbol, T.untyped],
99
+ _block: T.proc.params(trace: ::Langfuse::Models::Trace).returns(T.untyped)
100
+ ).returns(T.untyped)
101
+ end
102
+ def with_trace(name:, user_id: nil, **attributes, &_block)
66
103
  # Create the trace
67
- trace = Langfuse.trace(
104
+ trace = T.unsafe(Langfuse).trace(
68
105
  name: name,
69
106
  user_id: user_id,
70
107
  **attributes
71
108
  )
72
109
 
73
- result = nil
74
- error = nil
110
+ result = T.let(nil, T.untyped)
111
+ error = T.let(nil, T.nilable(StandardError))
75
112
 
76
113
  begin
77
114
  # Execute the block with the trace passed as argument
@@ -80,62 +117,91 @@ module LangfuseHelper
80
117
  rescue StandardError => e
81
118
  # Capture any error
82
119
  error = e
83
- raise
120
+ Kernel.raise # Use Kernel.raise
84
121
  ensure
85
122
  # Update trace output if available
86
- if result && !trace.output
87
- trace.output = result.is_a?(String) ? result : result
88
-
89
- # Create a new trace event to update the trace
90
- Langfuse.trace(
91
- id: trace.id,
92
- output: trace.output
93
- )
123
+ if result && trace.output.nil?
124
+ # Assuming trace.output is writable and can be inferred or is T.untyped
125
+ T.unsafe(trace).output = result # Use T.unsafe if Trace model type isn't fully defined
126
+
127
+ # Create a new trace event to update the trace - Reuse trace object
128
+ # This seems incorrect, updating should likely use an update method or modify the object
129
+ # directly if it's mutable and the original trace object is used later.
130
+ # Re-creating a trace event just to update seems wrong. Commenting out for now.
131
+ # Langfuse.trace(
132
+ # id: trace.id,
133
+ # output: trace.output
134
+ # )
94
135
  end
95
136
 
96
137
  # Ensure all events are sent (only in case of error, otherwise let the automatic flushing handle it)
97
- Langfuse.flush if error
138
+ T.unsafe(Langfuse).flush if error
98
139
  end
99
140
  end
100
141
 
101
142
  # Create a trace and set it as the current context
102
- def with_context_trace(name:, user_id: nil, **attributes)
103
- trace = Langfuse.trace(
143
+ sig do
144
+ params(
145
+ name: String,
146
+ user_id: T.nilable(String),
147
+ attributes: T::Hash[Symbol, T.untyped],
148
+ block: T.proc.params(trace: ::Langfuse::Models::Trace).void
149
+ ).void
150
+ end
151
+ def with_context_trace(name:, user_id: nil, **attributes, &block)
152
+ trace = T.unsafe(Langfuse).trace(
104
153
  name: name,
105
154
  user_id: user_id,
106
155
  **attributes
107
156
  )
108
157
 
109
158
  LangfuseContext.with_trace(trace) do
110
- yield(trace)
159
+ block.call(trace)
111
160
  end
112
161
  end
113
162
 
114
163
  # Create a span using the current trace context
115
- def with_context_span(name:, input: nil, **attributes)
164
+ sig do
165
+ params(
166
+ name: String,
167
+ input: T.untyped,
168
+ attributes: T::Hash[Symbol, T.untyped],
169
+ block: T.proc.params(span: ::Langfuse::Models::Span).returns(T.untyped)
170
+ ).returns(T.untyped)
171
+ end
172
+ def with_context_span(name:, input: nil, **attributes, &block)
116
173
  # Get trace_id from context
117
174
  trace_id = LangfuseContext.current_trace_id
118
175
  parent_id = LangfuseContext.current_span_id
119
176
 
120
- raise 'No trace context found. Make sure to call within with_context_trace' if trace_id.nil?
177
+ # Use Kernel.raise
178
+ Kernel.raise 'No trace context found. Make sure to call within with_context_trace' if trace_id.nil?
121
179
 
122
- span = Langfuse.span(
180
+ span = T.unsafe(Langfuse).span(
123
181
  name: name,
124
- trace_id: trace_id,
182
+ trace_id: T.must(trace_id), # Must be present due to check above
125
183
  parent_observation_id: parent_id,
126
184
  input: input,
127
185
  **attributes
128
186
  )
129
187
 
130
188
  LangfuseContext.with_span(span) do
131
- # Execute the block with the span
132
- with_span_implementation(span) { yield(span) }
189
+ # Pass the block to the implementation
190
+ with_span_implementation(span, &block)
133
191
  end
134
192
  end
135
193
 
136
194
  # Add a score to a trace
195
+ sig do
196
+ params(
197
+ trace_id: String,
198
+ name: String,
199
+ value: T.any(Integer, Float), # Assuming score value is numeric
200
+ comment: T.nilable(String)
201
+ ).void
202
+ end
137
203
  def score_trace(trace_id:, name:, value:, comment: nil)
138
- Langfuse.score(
204
+ T.unsafe(Langfuse).score(
139
205
  trace_id: trace_id,
140
206
  name: name,
141
207
  value: value,
@@ -145,32 +211,42 @@ module LangfuseHelper
145
211
 
146
212
  private
147
213
 
148
- def with_span_implementation(span)
149
- Time.now
150
- result = nil
151
- error = nil
214
+ # Type the private helper method
215
+ sig do
216
+ params(
217
+ span: ::Langfuse::Models::Span,
218
+ block: T.proc.params(span: ::Langfuse::Models::Span).returns(T.untyped)
219
+ ).returns(T.untyped)
220
+ end
221
+ def with_span_implementation(span, &block)
222
+ T.let(Time.now, Time) # Use start_time
223
+ result = T.let(nil, T.untyped)
224
+ error = T.let(nil, T.nilable(StandardError))
152
225
 
153
226
  begin
154
227
  # Execute the block with the span passed as argument
155
- result = yield
228
+ result = block.call(span) # Pass span to block
156
229
  result
157
230
  rescue StandardError => e
158
231
  # Capture any error
159
232
  error = e
160
- raise
233
+ Kernel.raise # Use Kernel.raise
161
234
  ensure
162
235
  # Update span
163
236
  span.end_time = Time.now.utc
164
- span.output = result if result && !span.output
237
+ # span.start_time = start_time.utc # Add start time if needed by update_span
238
+
239
+ span.output = result if result && span.output.nil?
165
240
 
166
241
  if error
167
242
  span.level = 'ERROR'
168
243
  span.status_message = error.message
169
244
  span.metadata ||= {}
170
- span.metadata[:error_backtrace] = error.backtrace.first(10) if error.backtrace
245
+ backtrace = error.backtrace
246
+ span.metadata[:error_backtrace] = backtrace.first(10) if backtrace # Check if backtrace is nil
171
247
  end
172
248
 
173
- Langfuse.update_span(span)
249
+ T.unsafe(Langfuse).update_span(span)
174
250
  end
175
251
  end
176
252
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: langfuse
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Langfuse
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-27 00:00:00.000000000 Z
10
+ date: 2025-04-17 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: concurrent-ruby
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '1.2'
26
+ - !ruby/object:Gem::Dependency
27
+ name: sorbet-runtime
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.5'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.5'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: bundler
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -149,6 +163,34 @@ dependencies:
149
163
  - - "~>"
150
164
  - !ruby/object:Gem::Version
151
165
  version: '0.22'
166
+ - !ruby/object:Gem::Dependency
167
+ name: sorbet
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '0.5'
173
+ type: :development
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: '0.5'
180
+ - !ruby/object:Gem::Dependency
181
+ name: tapioca
182
+ requirement: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - "~>"
185
+ - !ruby/object:Gem::Version
186
+ version: '0.11'
187
+ type: :development
188
+ prerelease: false
189
+ version_requirements: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - "~>"
192
+ - !ruby/object:Gem::Version
193
+ version: '0.11'
152
194
  - !ruby/object:Gem::Dependency
153
195
  name: timecop
154
196
  requirement: !ruby/object:Gem::Requirement
@@ -192,9 +234,9 @@ dependencies:
192
234
  - !ruby/object:Gem::Version
193
235
  version: '3.18'
194
236
  description: Langfuse is an open source observability platform for LLM applications.
195
- This is the Ruby client for Langfuse's API.
237
+ This is the Ruby client for Langfuse's API. Rough first alpha
196
238
  email:
197
- - hello@langfuse.com
239
+ - manuel@mento.co
198
240
  executables: []
199
241
  extensions: []
200
242
  extra_rdoc_files: []
@@ -203,6 +245,7 @@ files:
203
245
  - README.md
204
246
  - bin/console
205
247
  - bin/setup
248
+ - bin/tapioca
206
249
  - lib/langfuse.rb
207
250
  - lib/langfuse/api_client.rb
208
251
  - lib/langfuse/batch_worker.rb
@@ -215,7 +258,6 @@ files:
215
258
  - lib/langfuse/models/span.rb
216
259
  - lib/langfuse/models/trace.rb
217
260
  - lib/langfuse/models/usage.rb
218
- - lib/langfuse/rails.rb
219
261
  - lib/langfuse/version.rb
220
262
  - lib/langfuse_context.rb
221
263
  - lib/langfuse_helper.rb
@@ -1,31 +0,0 @@
1
- require 'active_support/notifications'
2
-
3
- module Langfuse
4
- module Rails
5
- class << self
6
- def setup_notifications
7
- ActiveSupport::Notifications.subscribe(/langfuse/) do |name, start, finish, _id, payload|
8
- case name
9
- when 'langfuse.trace'
10
- Langfuse.trace(payload)
11
- when 'langfuse.span'
12
- # Set end_time based on notification timing if not provided
13
- payload[:end_time] ||= finish
14
- payload[:start_time] ||= start
15
- Langfuse.span(payload)
16
- when 'langfuse.generation'
17
- # Set end_time based on notification timing if not provided
18
- payload[:end_time] ||= finish
19
- payload[:start_time] ||= start
20
- Langfuse.generation(payload)
21
- when 'langfuse.score'
22
- Langfuse.score(payload)
23
- end
24
- end
25
- end
26
- end
27
- end
28
- end
29
-
30
- # Set up notifications if we're in a Rails environment
31
- Langfuse::Rails.setup_notifications if defined?(::Rails)