langfuse 0.1.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.
@@ -0,0 +1,27 @@
1
+ require 'securerandom'
2
+
3
+ module Langfuse
4
+ module Models
5
+ class IngestionEvent
6
+ attr_accessor :id, :type, :timestamp, :body, :metadata
7
+
8
+ def initialize(type:, body:, metadata: nil)
9
+ @id = SecureRandom.uuid
10
+ @type = type
11
+ @timestamp = Time.now.utc.iso8601(3) # Millisecond precision
12
+ @body = body
13
+ @metadata = metadata
14
+ end
15
+
16
+ def to_h
17
+ {
18
+ id: @id,
19
+ type: @type,
20
+ timestamp: @timestamp,
21
+ body: @body.respond_to?(:to_h) ? @body.to_h : @body,
22
+ metadata: @metadata
23
+ }.compact
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,31 @@
1
+ require 'securerandom'
2
+
3
+ module Langfuse
4
+ module Models
5
+ class Score
6
+ attr_accessor :id, :trace_id, :name, :value, :observation_id,
7
+ :comment, :data_type, :config_id, :environment
8
+
9
+ def initialize(attributes = {})
10
+ attributes.each do |key, value|
11
+ send("#{key}=", value) if respond_to?("#{key}=")
12
+ end
13
+ @id ||= SecureRandom.uuid
14
+ end
15
+
16
+ def to_h
17
+ {
18
+ id: @id,
19
+ traceId: @trace_id,
20
+ name: @name,
21
+ value: @value,
22
+ observationId: @observation_id,
23
+ comment: @comment,
24
+ dataType: @data_type,
25
+ configId: @config_id,
26
+ environment: @environment
27
+ }.compact
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ require 'securerandom'
2
+
3
+ module Langfuse
4
+ module Models
5
+ class Span
6
+ attr_accessor :id, :trace_id, :name, :start_time, :end_time,
7
+ :metadata, :input, :output, :level, :status_message,
8
+ :parent_observation_id, :version, :environment
9
+
10
+ def initialize(attributes = {})
11
+ attributes.each do |key, value|
12
+ send("#{key}=", value) if respond_to?("#{key}=")
13
+ end
14
+ @id ||= SecureRandom.uuid
15
+ @start_time ||= Time.now.utc
16
+ end
17
+
18
+ def to_h
19
+ {
20
+ id: @id,
21
+ traceId: @trace_id,
22
+ name: @name,
23
+ startTime: @start_time&.iso8601(3),
24
+ endTime: @end_time&.iso8601(3),
25
+ metadata: @metadata,
26
+ input: @input,
27
+ output: @output,
28
+ level: @level,
29
+ statusMessage: @status_message,
30
+ parentObservationId: @parent_observation_id,
31
+ version: @version,
32
+ environment: @environment
33
+ }.compact
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,37 @@
1
+ require 'securerandom'
2
+
3
+ module Langfuse
4
+ module Models
5
+ class Trace
6
+ attr_accessor :id, :name, :user_id, :input, :output,
7
+ :session_id, :metadata, :tags, :public,
8
+ :release, :version, :timestamp, :environment
9
+
10
+ def initialize(attributes = {})
11
+ attributes.each do |key, value|
12
+ send("#{key}=", value) if respond_to?("#{key}=")
13
+ end
14
+ @id ||= SecureRandom.uuid
15
+ @timestamp ||= Time.now.utc
16
+ end
17
+
18
+ def to_h
19
+ {
20
+ id: @id,
21
+ name: @name,
22
+ userId: @user_id,
23
+ input: @input,
24
+ output: @output,
25
+ sessionId: @session_id,
26
+ metadata: @metadata,
27
+ tags: @tags,
28
+ public: @public,
29
+ release: @release,
30
+ version: @version,
31
+ timestamp: @timestamp&.iso8601(3),
32
+ environment: @environment
33
+ }.compact
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,30 @@
1
+ module Langfuse
2
+ module Models
3
+ class Usage
4
+ attr_accessor :input, :output, :total, :unit,
5
+ :input_cost, :output_cost, :total_cost,
6
+ :prompt_tokens, :completion_tokens, :total_tokens
7
+
8
+ def initialize(attributes = {})
9
+ attributes.each do |key, value|
10
+ send("#{key}=", value) if respond_to?("#{key}=")
11
+ end
12
+ end
13
+
14
+ def to_h
15
+ {
16
+ input: @input,
17
+ output: @output,
18
+ total: @total,
19
+ unit: @unit,
20
+ inputCost: @input_cost,
21
+ outputCost: @output_cost,
22
+ totalCost: @total_cost,
23
+ promptTokens: @prompt_tokens,
24
+ completionTokens: @completion_tokens,
25
+ totalTokens: @total_tokens
26
+ }.compact
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,31 @@
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)
@@ -0,0 +1,3 @@
1
+ module Langfuse
2
+ VERSION = '0.1.0'
3
+ end
data/lib/langfuse.rb ADDED
@@ -0,0 +1,71 @@
1
+ require 'langfuse/version'
2
+ require 'langfuse/configuration'
3
+
4
+ # Load models
5
+ require 'langfuse/models/ingestion_event'
6
+ require 'langfuse/models/trace'
7
+ require 'langfuse/models/span'
8
+ require 'langfuse/models/generation'
9
+ require 'langfuse/models/event'
10
+ require 'langfuse/models/score'
11
+ require 'langfuse/models/usage'
12
+
13
+ # Load API client
14
+ require 'langfuse/api_client'
15
+
16
+ # Load batch worker (works with or without Sidekiq)
17
+ require 'langfuse/batch_worker'
18
+
19
+ # Load main client
20
+ require 'langfuse/client'
21
+
22
+ module Langfuse
23
+ class << self
24
+ attr_writer :configuration
25
+
26
+ def configuration
27
+ @configuration ||= Configuration.new
28
+ end
29
+
30
+ def configure
31
+ yield(configuration)
32
+ end
33
+
34
+ # Convenience delegators to the client instance
35
+ def trace(attributes = {})
36
+ Client.instance.trace(attributes)
37
+ end
38
+
39
+ def span(attributes = {})
40
+ Client.instance.span(attributes)
41
+ end
42
+
43
+ def update_span(span)
44
+ Client.instance.update_span(span)
45
+ end
46
+
47
+ def generation(attributes = {})
48
+ Client.instance.generation(attributes)
49
+ end
50
+
51
+ def update_generation(generation)
52
+ Client.instance.update_generation(generation)
53
+ end
54
+
55
+ def event(attributes = {})
56
+ Client.instance.event(attributes)
57
+ end
58
+
59
+ def score(attributes = {})
60
+ Client.instance.score(attributes)
61
+ end
62
+
63
+ def flush
64
+ Client.instance.flush
65
+ end
66
+
67
+ def shutdown
68
+ Client.instance.shutdown
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,33 @@
1
+ class LangfuseContext
2
+ def self.current
3
+ Thread.current[:langfuse_context] ||= {}
4
+ end
5
+
6
+ def self.current_trace_id
7
+ current[:trace_id]
8
+ end
9
+
10
+ def self.current_span_id
11
+ current[:span_id]
12
+ end
13
+
14
+ def self.with_trace(trace)
15
+ old_context = current.dup
16
+ begin
17
+ Thread.current[:langfuse_context] = { trace_id: trace.id }
18
+ yield
19
+ ensure
20
+ Thread.current[:langfuse_context] = old_context
21
+ end
22
+ end
23
+
24
+ def self.with_span(span)
25
+ old_context = current.dup
26
+ begin
27
+ Thread.current[:langfuse_context] = current.merge({ span_id: span.id })
28
+ yield
29
+ ensure
30
+ Thread.current[:langfuse_context] = old_context
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,176 @@
1
+ require_relative 'langfuse_context'
2
+
3
+ module LangfuseHelper
4
+ # Execute a block within the context of a span
5
+ def with_span(name:, trace_id:, parent_id: nil, input: nil, **attributes)
6
+ # Create the span
7
+ span = Langfuse.span(
8
+ name: name,
9
+ trace_id: trace_id,
10
+ parent_observation_id: parent_id,
11
+ input: input,
12
+ **attributes
13
+ )
14
+
15
+ with_span_implementation(span) { yield(span) }
16
+ end
17
+
18
+ # 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)
20
+ # Create the generation
21
+ generation = Langfuse.generation(
22
+ name: name,
23
+ trace_id: trace_id,
24
+ parent_observation_id: parent_id,
25
+ model: model,
26
+ input: input,
27
+ model_parameters: model_parameters,
28
+ **attributes
29
+ )
30
+
31
+ start_time = Time.now
32
+ result = nil
33
+ error = nil
34
+
35
+ begin
36
+ # Execute the block with the generation passed as argument
37
+ result = yield(generation)
38
+ result
39
+ rescue StandardError => e
40
+ # Capture any error
41
+ error = e
42
+ raise
43
+ ensure
44
+ # Always update the generation with results
45
+ generation.end_time = Time.now.utc
46
+ generation.start_time = start_time.utc
47
+
48
+ # Add output if there was a result and it wasn't already set
49
+ generation.output = result if result && !generation.output
50
+
51
+ # Add error information if there was an error
52
+ if error
53
+ generation.level = 'ERROR'
54
+ generation.status_message = error.message
55
+ generation.metadata ||= {}
56
+ generation.metadata[:error_backtrace] = error.backtrace.first(10) if error.backtrace
57
+ end
58
+
59
+ # Update the generation
60
+ Langfuse.update_generation(generation)
61
+ end
62
+ end
63
+
64
+ # Execute a block within the context of a trace
65
+ def with_trace(name:, user_id: nil, **attributes)
66
+ # Create the trace
67
+ trace = Langfuse.trace(
68
+ name: name,
69
+ user_id: user_id,
70
+ **attributes
71
+ )
72
+
73
+ result = nil
74
+ error = nil
75
+
76
+ begin
77
+ # Execute the block with the trace passed as argument
78
+ result = yield(trace)
79
+ result
80
+ rescue StandardError => e
81
+ # Capture any error
82
+ error = e
83
+ raise
84
+ ensure
85
+ # 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
+ )
94
+ end
95
+
96
+ # Ensure all events are sent (only in case of error, otherwise let the automatic flushing handle it)
97
+ Langfuse.flush if error
98
+ end
99
+ end
100
+
101
+ # Create a trace and set it as the current context
102
+ def with_context_trace(name:, user_id: nil, **attributes)
103
+ trace = Langfuse.trace(
104
+ name: name,
105
+ user_id: user_id,
106
+ **attributes
107
+ )
108
+
109
+ LangfuseContext.with_trace(trace) do
110
+ yield(trace)
111
+ end
112
+ end
113
+
114
+ # Create a span using the current trace context
115
+ def with_context_span(name:, input: nil, **attributes)
116
+ # Get trace_id from context
117
+ trace_id = LangfuseContext.current_trace_id
118
+ parent_id = LangfuseContext.current_span_id
119
+
120
+ raise 'No trace context found. Make sure to call within with_context_trace' if trace_id.nil?
121
+
122
+ span = Langfuse.span(
123
+ name: name,
124
+ trace_id: trace_id,
125
+ parent_observation_id: parent_id,
126
+ input: input,
127
+ **attributes
128
+ )
129
+
130
+ LangfuseContext.with_span(span) do
131
+ # Execute the block with the span
132
+ with_span_implementation(span) { yield(span) }
133
+ end
134
+ end
135
+
136
+ # Add a score to a trace
137
+ def score_trace(trace_id:, name:, value:, comment: nil)
138
+ Langfuse.score(
139
+ trace_id: trace_id,
140
+ name: name,
141
+ value: value,
142
+ comment: comment
143
+ )
144
+ end
145
+
146
+ private
147
+
148
+ def with_span_implementation(span)
149
+ Time.now
150
+ result = nil
151
+ error = nil
152
+
153
+ begin
154
+ # Execute the block with the span passed as argument
155
+ result = yield
156
+ result
157
+ rescue StandardError => e
158
+ # Capture any error
159
+ error = e
160
+ raise
161
+ ensure
162
+ # Update span
163
+ span.end_time = Time.now.utc
164
+ span.output = result if result && !span.output
165
+
166
+ if error
167
+ span.level = 'ERROR'
168
+ span.status_message = error.message
169
+ span.metadata ||= {}
170
+ span.metadata[:error_backtrace] = error.backtrace.first(10) if error.backtrace
171
+ end
172
+
173
+ Langfuse.update_span(span)
174
+ end
175
+ end
176
+ end