Skip to content

Commit a431021

Browse files
Merge pull request #3 from test-IO/tracer
added tracer class
2 parents f8def0d + f5e08e8 commit a431021

16 files changed

+532
-6
lines changed

.rubocop.yml

+7-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,10 @@ Style/Documentation:
1111
Enabled: false
1212

1313
Metrics/MethodLength:
14-
Max: 20
14+
Max: 25
15+
16+
Metrics/PerceivedComplexity:
17+
Max: 10
18+
19+
Metrics/CyclomaticComplexity:
20+
Max: 10

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@
77
## [0.2.0] - 2024-11-27
88

99
- Added text and chat prompts
10+
11+
## [0.2.2] - 2024-12-03
12+
13+
- Introduced Tracer

Gemfile.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
llm_eval_ruby (0.2.1)
4+
llm_eval_ruby (0.2.2)
55
httparty (~> 0.22.0)
66
liquid (~> 5.5.0)
77

lib/llm_eval_ruby.rb

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
require_relative "llm_eval_ruby/version"
44
require_relative "llm_eval_ruby/prompt_repository"
55
require_relative "llm_eval_ruby/configuration"
6+
require_relative "llm_eval_ruby/tracer"
7+
require_relative "llm_eval_ruby/observable"
68

79
module LlmEvalRuby
810
class Error < StandardError; end

lib/llm_eval_ruby/api_clients/langfuse.rb

+84-2
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,96 @@ class Langfuse
1414
raise_on [400, 401, 406, 422, 500]
1515

1616
def initialize(host:, username:, password:)
17-
self.class.base_uri "#{host}/api/public/v2"
17+
self.class.base_uri "#{host}/api/public/"
1818
self.class.basic_auth username, password
1919
end
2020

2121
def fetch_prompt(name:, version:)
22-
response = self.class.get("/prompts/#{name}", { query: { version: } })
22+
response = self.class.get("/v2/prompts/#{name}", { query: { version: } })
2323
response["prompt"]
2424
end
25+
26+
def create_trace(params = {})
27+
body = {
28+
id: params[:id],
29+
name: params[:name],
30+
input: params[:input],
31+
sessionId: params[:session_id],
32+
userId: params[:user_id]
33+
}
34+
create_event(type: "trace-create", body:)
35+
end
36+
37+
def create_span(params = {})
38+
body = {
39+
id: params[:id],
40+
name: params[:name],
41+
input: params[:input],
42+
traceId: params[:trace_id]
43+
}
44+
create_event(type: "span-create", body:)
45+
end
46+
47+
def update_span(params = {})
48+
body = {
49+
id: params[:id],
50+
output: params[:output],
51+
endTime: params[:end_time]
52+
}
53+
create_event(type: "span-update", body:)
54+
end
55+
56+
def create_generation(params = {})
57+
body = {
58+
id: params[:id],
59+
timestamp: params[:timestamp],
60+
name: params[:name],
61+
input: params[:input],
62+
output: params[:output] || "UNKNOWN",
63+
traceId: params[:trace_id],
64+
release: params[:release] || "UNKNOWN",
65+
version: params[:version] || "UNKNOWN",
66+
metadata: params[:metadata] || {},
67+
promptName: params[:prompt_name],
68+
promptVersion: params[:prompt_version]
69+
}
70+
create_event(type: "generation-create", body:)
71+
end
72+
73+
def update_generation(params = {})
74+
body = {
75+
id: params[:id],
76+
output: params[:output],
77+
endTime: params[:end_time],
78+
usage: convert_keys_to_camel_case(params[:usage])
79+
}
80+
create_event(type: "generation-update", body:)
81+
end
82+
83+
def create_event(type:, body:)
84+
payload = {
85+
batch: [
86+
{
87+
id: SecureRandom.uuid,
88+
type:,
89+
body:,
90+
timestamp: Time.now.utc.iso8601,
91+
metadata: {}
92+
}
93+
]
94+
}
95+
96+
self.class.post("/ingestion", body: payload.to_json)
97+
end
98+
99+
private
100+
101+
def convert_keys_to_camel_case(hash)
102+
hash.each_with_object({}) do |(key, value), new_hash|
103+
camel_case_key = key.gsub(/_([a-z])/) { ::Regexp.last_match(1).upcase }
104+
new_hash[camel_case_key] = value
105+
end
106+
end
25107
end
26108
end
27109
end

lib/llm_eval_ruby/observable.rb

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# frozen_string_literal: true
2+
3+
module LlmEvalRuby
4+
module Observable
5+
def self.included(base)
6+
base.extend(ClassMethods)
7+
end
8+
9+
module ClassMethods
10+
def observed_methods
11+
@observed_methods ||= {}
12+
end
13+
14+
def observe(method_name, options = {})
15+
observed_methods[method_name] = options
16+
end
17+
18+
def method_added(method_name)
19+
super
20+
return unless observed_methods.key?(method_name)
21+
22+
wrap_observed_method(method_name)
23+
end
24+
25+
private
26+
27+
def wrap_observed_method(method_name)
28+
options = observed_methods[method_name]
29+
original_method = instance_method(method_name)
30+
observed_methods.delete(method_name)
31+
wrap_method(method_name, original_method, options)
32+
end
33+
34+
def wrap_method(method_name, original_method, options)
35+
define_method(method_name) do |*args, **kwargs, &block|
36+
result = nil
37+
input = prepare_input(args, kwargs)
38+
case options[:type]
39+
when :span
40+
LlmEvalRuby::Tracer.span(name: method_name, input: input, trace_id: @trace_id) do
41+
result = original_method.bind(self).call(*args, **kwargs, &block)
42+
end
43+
when :generation
44+
LlmEvalRuby::Tracer.generation(name: method_name, input: input, trace_id: @trace_id) do
45+
result = original_method.bind(self).call(*args, **kwargs, &block)
46+
end
47+
else
48+
LlmEvalRuby::Tracer.trace(name: method_name, input: input, trace_id: @trace_id) do
49+
result = original_method.bind(self).call(*args, **kwargs, &block)
50+
end
51+
end
52+
53+
result
54+
end
55+
end
56+
end
57+
58+
def prepare_input(*args, **kwargs)
59+
return nil if args.empty? && kwargs.empty?
60+
61+
inputs = deep_copy(Array[*args, **kwargs].flatten)
62+
inputs.each do |item|
63+
trim_base64_images(item) if item.is_a?(Hash)
64+
end
65+
66+
inputs
67+
end
68+
69+
def trim_base64_images(hash, max_length = 30)
70+
# Iterate through each key-value pair in the hash
71+
hash.each do |key, value|
72+
if value.is_a?(Hash)
73+
# Recursively process nested hashes
74+
trim_base64_images(value, max_length)
75+
elsif value.is_a?(String) && value.start_with?("data:image/jpeg;base64,")
76+
# Trim the byte string while keeping the prefix; set max length limit
77+
prefix = "data:image/jpeg;base64,"
78+
byte_string = value[prefix.length..]
79+
trimmed_byte_string = byte_string[0, max_length] # Trim to max_length characters
80+
hash[key] = "#{prefix}#{trimmed_byte_string}... (truncated)"
81+
elsif value.is_a?(Array)
82+
# Recursively process arrays
83+
value.each do |element|
84+
trim_base64_images(element, max_length) if element.is_a?(Hash)
85+
end
86+
end
87+
end
88+
hash
89+
end
90+
91+
def deep_copy(obj)
92+
case obj
93+
when Numeric, Symbol, NilClass, TrueClass, FalseClass
94+
obj
95+
when String
96+
obj.dup
97+
when Array
98+
obj.map { |e| deep_copy(e) }
99+
when Hash
100+
obj.each_with_object({}) do |(key, value), result|
101+
result[deep_copy(key)] = deep_copy(value)
102+
end
103+
else
104+
begin
105+
Marshal.load(Marshal.dump(obj))
106+
rescue TypeError
107+
nil # or handle as needed, perhaps log or raise a specific error
108+
end
109+
end
110+
end
111+
end
112+
end

lib/llm_eval_ruby/prompt_types/base.rb

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class Base
77

88
def initialize(adapter:, content:, role:)
99
@adapter = adapter
10+
@adapter = adapter.safe_constantize if adapter.is_a?(String)
1011
@role = role
1112
@content = content
1213
end
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# frozen_string_literal: true
2+
3+
module LlmEvalRuby
4+
module TraceAdapters
5+
class Base
6+
end
7+
end
8+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "base"
4+
require_relative "../api_clients/langfuse"
5+
require_relative "../trace_types"
6+
7+
module LlmEvalRuby
8+
module TraceAdapters
9+
class Langfuse < Base
10+
class << self
11+
def trace(**kwargs)
12+
trace = TraceTypes::Trace.new(id: SecureRandom.uuid, **kwargs)
13+
response = client.create_trace(trace.to_h)
14+
15+
logger.warn "Failed to create generation" if response["successes"].blank?
16+
17+
trace
18+
end
19+
20+
def span(**kwargs)
21+
span = TraceTypes::Span.new(id: SecureRandom.uuid, **kwargs)
22+
response = client.create_span(span.to_h)
23+
24+
logger.warn "Failed to create span" if response["successes"].blank?
25+
26+
return span unless block_given?
27+
28+
result = yield
29+
30+
end_span(span, result)
31+
32+
result
33+
end
34+
35+
def update_generation(**kwargs)
36+
generation = TraceTypes::Generation.new(**kwargs)
37+
response = client.update_generation(generation.to_h)
38+
39+
logger.warn "Failed to create generation" if response["successes"].blank?
40+
41+
generation
42+
end
43+
44+
def generation(**kwargs)
45+
generation = TraceTypes::Generation.new(id: SecureRandom.uuid, tracer: self, **kwargs)
46+
response = client.create_generation(generation.to_h)
47+
logger.warn "Failed to create generation" if response["successes"].blank?
48+
49+
return generation unless block_given?
50+
51+
result = yield generation
52+
53+
finish_generation(generation, result)
54+
55+
result
56+
end
57+
58+
private
59+
60+
def logger
61+
@logger ||= Logger.new($stdout)
62+
end
63+
64+
def client
65+
@client ||= ApiClients::Langfuse.new(**LlmEvalRuby.config.langfuse_options)
66+
end
67+
68+
def end_span(span, result)
69+
span.end_time = Time.now.utc.iso8601
70+
span.output = result
71+
72+
client.update_span(span.to_h)
73+
end
74+
75+
def end_generation(generation, result)
76+
generation.output = result.dig("choices", 0, "message", "content")
77+
generation.usage = result["usage"]
78+
generation.end_time = Time.now.utc.iso8601
79+
80+
client.update_generation(generation.to_h)
81+
end
82+
end
83+
end
84+
end
85+
end

0 commit comments

Comments
 (0)