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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +201 -0
- data/bin/console +21 -0
- data/bin/setup +8 -0
- data/lib/langfuse/api_client.rb +80 -0
- data/lib/langfuse/batch_worker.rb +80 -0
- data/lib/langfuse/client.rb +176 -0
- data/lib/langfuse/configuration.rb +20 -0
- data/lib/langfuse/models/event.rb +36 -0
- data/lib/langfuse/models/generation.rb +45 -0
- data/lib/langfuse/models/ingestion_event.rb +27 -0
- data/lib/langfuse/models/score.rb +31 -0
- data/lib/langfuse/models/span.rb +37 -0
- data/lib/langfuse/models/trace.rb +37 -0
- data/lib/langfuse/models/usage.rb +30 -0
- data/lib/langfuse/rails.rb +31 -0
- data/lib/langfuse/version.rb +3 -0
- data/lib/langfuse.rb +71 -0
- data/lib/langfuse_context.rb +33 -0
- data/lib/langfuse_helper.rb +176 -0
- metadata +243 -0
@@ -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)
|
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
|