tracebook 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/.yardopts +10 -0
- data/CHANGELOG.md +43 -0
- data/MIT-LICENSE +20 -0
- data/README.md +881 -0
- data/Rakefile +21 -0
- data/app/assets/images/tracebook/.keep +0 -0
- data/app/assets/javascripts/tracebook/application.js +88 -0
- data/app/assets/stylesheets/tracebook/application.css +173 -0
- data/app/controllers/concerns/.keep +0 -0
- data/app/controllers/tracebook/application_controller.rb +4 -0
- data/app/controllers/tracebook/exports_controller.rb +25 -0
- data/app/controllers/tracebook/interactions_controller.rb +71 -0
- data/app/helpers/tracebook/application_helper.rb +4 -0
- data/app/helpers/tracebook/interactions_helper.rb +35 -0
- data/app/jobs/tracebook/application_job.rb +5 -0
- data/app/jobs/tracebook/daily_rollups_job.rb +100 -0
- data/app/jobs/tracebook/export_job.rb +162 -0
- data/app/jobs/tracebook/persist_interaction_job.rb +160 -0
- data/app/mailers/tracebook/application_mailer.rb +6 -0
- data/app/models/concerns/.keep +0 -0
- data/app/models/tracebook/application_record.rb +5 -0
- data/app/models/tracebook/interaction.rb +100 -0
- data/app/models/tracebook/pricing_rule.rb +84 -0
- data/app/models/tracebook/redaction_rule.rb +81 -0
- data/app/models/tracebook/rollup_daily.rb +73 -0
- data/app/views/layouts/tracebook/application.html.erb +18 -0
- data/app/views/tracebook/interactions/index.html.erb +105 -0
- data/app/views/tracebook/interactions/show.html.erb +44 -0
- data/config/routes.rb +8 -0
- data/db/migrate/20241112000100_create_tracebook_interactions.rb +55 -0
- data/db/migrate/20241112000200_create_tracebook_rollups_dailies.rb +24 -0
- data/db/migrate/20241112000300_create_tracebook_pricing_rules.rb +21 -0
- data/db/migrate/20241112000400_create_tracebook_redaction_rules.rb +19 -0
- data/lib/tasks/tracebook_tasks.rake +4 -0
- data/lib/tasks/yard.rake +29 -0
- data/lib/tracebook/adapters/active_agent.rb +82 -0
- data/lib/tracebook/adapters/ruby_llm.rb +97 -0
- data/lib/tracebook/adapters.rb +6 -0
- data/lib/tracebook/config.rb +130 -0
- data/lib/tracebook/engine.rb +5 -0
- data/lib/tracebook/errors.rb +9 -0
- data/lib/tracebook/mappers/anthropic.rb +59 -0
- data/lib/tracebook/mappers/base.rb +38 -0
- data/lib/tracebook/mappers/ollama.rb +49 -0
- data/lib/tracebook/mappers/openai.rb +75 -0
- data/lib/tracebook/mappers.rb +283 -0
- data/lib/tracebook/normalized_interaction.rb +86 -0
- data/lib/tracebook/pricing/calculator.rb +39 -0
- data/lib/tracebook/pricing.rb +5 -0
- data/lib/tracebook/redaction_pipeline.rb +88 -0
- data/lib/tracebook/redactors/base.rb +29 -0
- data/lib/tracebook/redactors/card_pan.rb +15 -0
- data/lib/tracebook/redactors/email.rb +15 -0
- data/lib/tracebook/redactors/phone.rb +15 -0
- data/lib/tracebook/redactors.rb +8 -0
- data/lib/tracebook/result.rb +53 -0
- data/lib/tracebook/version.rb +3 -0
- data/lib/tracebook.rb +201 -0
- metadata +164 -0
data/config/routes.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateTracebookInteractions < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :tracebook_interactions do |t|
|
|
6
|
+
t.string :project
|
|
7
|
+
t.string :provider, null: false
|
|
8
|
+
t.string :model, null: false
|
|
9
|
+
t.string :session_id
|
|
10
|
+
|
|
11
|
+
t.text :request_payload
|
|
12
|
+
t.text :response_payload
|
|
13
|
+
t.text :request_text
|
|
14
|
+
t.text :response_text
|
|
15
|
+
|
|
16
|
+
t.integer :input_tokens
|
|
17
|
+
t.integer :output_tokens
|
|
18
|
+
t.integer :total_tokens
|
|
19
|
+
t.integer :latency_ms
|
|
20
|
+
|
|
21
|
+
t.integer :status, null: false, default: 0
|
|
22
|
+
t.integer :review_state, null: false, default: 0
|
|
23
|
+
t.string :error_class
|
|
24
|
+
t.text :error_message
|
|
25
|
+
|
|
26
|
+
t.string :user_type
|
|
27
|
+
t.bigint :user_id
|
|
28
|
+
t.bigint :parent_id
|
|
29
|
+
|
|
30
|
+
t.text :tags
|
|
31
|
+
t.text :metadata
|
|
32
|
+
|
|
33
|
+
t.string :request_payload_store, null: false, default: "inline"
|
|
34
|
+
t.string :response_payload_store, null: false, default: "inline"
|
|
35
|
+
t.bigint :request_payload_blob_id
|
|
36
|
+
t.bigint :response_payload_blob_id
|
|
37
|
+
|
|
38
|
+
t.integer :cost_input_cents, default: 0, null: false
|
|
39
|
+
t.integer :cost_output_cents, default: 0, null: false
|
|
40
|
+
t.integer :cost_total_cents, default: 0, null: false
|
|
41
|
+
t.string :currency, null: false, default: "USD"
|
|
42
|
+
|
|
43
|
+
t.timestamps
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
add_index :tracebook_interactions, :created_at
|
|
47
|
+
add_index :tracebook_interactions, [ :provider, :model, :created_at ]
|
|
48
|
+
add_index :tracebook_interactions, [ :project, :created_at ]
|
|
49
|
+
add_index :tracebook_interactions, :session_id
|
|
50
|
+
add_index :tracebook_interactions, :status
|
|
51
|
+
add_index :tracebook_interactions, :review_state
|
|
52
|
+
add_index :tracebook_interactions, :parent_id
|
|
53
|
+
add_index :tracebook_interactions, [ :user_type, :user_id ]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateTracebookRollupsDailies < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :tracebook_rollups_dailies do |t|
|
|
6
|
+
t.date :date, null: false
|
|
7
|
+
t.string :project
|
|
8
|
+
t.string :provider
|
|
9
|
+
t.string :model
|
|
10
|
+
|
|
11
|
+
t.integer :interactions_count, null: false, default: 0
|
|
12
|
+
t.integer :success_count, null: false, default: 0
|
|
13
|
+
t.integer :error_count, null: false, default: 0
|
|
14
|
+
t.integer :input_tokens_sum, null: false, default: 0
|
|
15
|
+
t.integer :output_tokens_sum, null: false, default: 0
|
|
16
|
+
t.integer :cost_cents_sum, null: false, default: 0
|
|
17
|
+
t.string :currency, null: false, default: "USD"
|
|
18
|
+
|
|
19
|
+
t.timestamps
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
add_index :tracebook_rollups_dailies, [ :date, :project, :provider, :model ], unique: true, name: "index_tracebook_rollups_on_dimensions"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateTracebookPricingRules < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :tracebook_pricing_rules do |t|
|
|
6
|
+
t.string :provider, null: false
|
|
7
|
+
t.string :model_glob, null: false
|
|
8
|
+
t.string :unit, null: false, default: "per_1k_tokens"
|
|
9
|
+
t.integer :input_cents_per_unit, null: false, default: 0
|
|
10
|
+
t.integer :output_cents_per_unit, null: false, default: 0
|
|
11
|
+
t.date :effective_from, null: false
|
|
12
|
+
t.date :effective_to
|
|
13
|
+
t.string :currency, null: false, default: "USD"
|
|
14
|
+
|
|
15
|
+
t.timestamps
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
add_index :tracebook_pricing_rules, :provider
|
|
19
|
+
add_index :tracebook_pricing_rules, [ :provider, :effective_from ], name: "index_tracebook_pricing_on_provider_effective_from"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateTracebookRedactionRules < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :tracebook_redaction_rules do |t|
|
|
6
|
+
t.string :name, null: false
|
|
7
|
+
t.text :pattern, null: false
|
|
8
|
+
t.string :replacement, null: false, default: "[REDACTED]"
|
|
9
|
+
t.integer :applies_to, null: false, default: 2
|
|
10
|
+
t.boolean :enabled, null: false, default: true
|
|
11
|
+
t.integer :priority, null: false, default: 100
|
|
12
|
+
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
add_index :tracebook_redaction_rules, :enabled
|
|
17
|
+
add_index :tracebook_redaction_rules, :priority
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/tasks/yard.rake
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "yard"
|
|
5
|
+
|
|
6
|
+
YARD::Rake::YardocTask.new(:yard) do |t|
|
|
7
|
+
t.files = [ "lib/**/*.rb", "app/**/*.rb" ]
|
|
8
|
+
t.options = [
|
|
9
|
+
"--markup", "markdown",
|
|
10
|
+
"--markup-provider", "kramdown",
|
|
11
|
+
"--readme", "README.md",
|
|
12
|
+
"--output-dir", "doc",
|
|
13
|
+
"--protected",
|
|
14
|
+
"--private"
|
|
15
|
+
]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc "Generate YARD documentation and open in browser"
|
|
19
|
+
task "yard:open" => :yard do
|
|
20
|
+
system "open doc/index.html"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc "Generate YARD stats"
|
|
24
|
+
task "yard:stats" do
|
|
25
|
+
sh "yard stats --list-undoc"
|
|
26
|
+
end
|
|
27
|
+
rescue LoadError
|
|
28
|
+
# YARD not available
|
|
29
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
4
|
+
|
|
5
|
+
module Tracebook
|
|
6
|
+
module Adapters
|
|
7
|
+
module ActiveAgent
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
def enable!(bus: nil)
|
|
11
|
+
bus ||= discover_bus
|
|
12
|
+
return unless bus.respond_to?(:subscribe)
|
|
13
|
+
|
|
14
|
+
subscribers << bus.subscribe { |event| handle_event(event) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def disable!
|
|
18
|
+
subscribers.clear
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def handle_event(event)
|
|
24
|
+
payload = event.with_indifferent_access
|
|
25
|
+
provider = payload[:provider]&.to_s.presence || "active_agent"
|
|
26
|
+
mapper = mapper_for(provider)
|
|
27
|
+
meta_hash = payload[:meta] || {}
|
|
28
|
+
meta = meta_hash.merge(
|
|
29
|
+
session_id: payload[:session_id],
|
|
30
|
+
parent_id: payload[:parent_id]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
normalized = if mapper
|
|
34
|
+
mapper.normalize(raw_request: payload[:request], raw_response: payload[:response], meta: meta)
|
|
35
|
+
else
|
|
36
|
+
fallback(provider, payload, meta)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
TraceBook.record!(**normalized.to_h)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def mapper_for(provider)
|
|
43
|
+
case provider
|
|
44
|
+
when "openai"
|
|
45
|
+
Tracebook::Mappers::OpenAI.new
|
|
46
|
+
when "anthropic"
|
|
47
|
+
Tracebook::Mappers::Anthropic.new
|
|
48
|
+
when "ollama"
|
|
49
|
+
Tracebook::Mappers::Ollama.new
|
|
50
|
+
else
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def fallback(provider, payload, meta)
|
|
56
|
+
NormalizedInteraction.new(
|
|
57
|
+
provider: provider,
|
|
58
|
+
model: payload.dig(:request, :model),
|
|
59
|
+
project: meta[:project],
|
|
60
|
+
request_payload: payload[:request],
|
|
61
|
+
response_payload: payload[:response],
|
|
62
|
+
status: meta[:status] || :success,
|
|
63
|
+
tags: Array(meta[:tags]),
|
|
64
|
+
session_id: meta[:session_id],
|
|
65
|
+
parent_id: meta[:parent_id]
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def subscribers
|
|
70
|
+
@subscribers ||= []
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def discover_bus
|
|
74
|
+
ActiveAgent::Bus if defined?(ActiveAgent::Bus)
|
|
75
|
+
rescue NameError
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
TraceBook = Tracebook unless defined?(TraceBook)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/notifications"
|
|
4
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
5
|
+
|
|
6
|
+
module Tracebook
|
|
7
|
+
module Adapters
|
|
8
|
+
# Adapter for capturing LLM interactions via ActiveSupport::Notifications.
|
|
9
|
+
#
|
|
10
|
+
# This adapter subscribes to instrumentation events (default: "ruby_llm.request")
|
|
11
|
+
# and automatically records interactions in TraceBook.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic setup
|
|
14
|
+
# # config/initializers/tracebook_adapters.rb
|
|
15
|
+
# TraceBook::Adapters::RubyLLM.enable!
|
|
16
|
+
#
|
|
17
|
+
# @example Custom event name
|
|
18
|
+
# TraceBook::Adapters::RubyLLM.enable!(instrumentation: "my_llm.complete")
|
|
19
|
+
#
|
|
20
|
+
# @example Emitting events from your LLM client
|
|
21
|
+
# ActiveSupport::Notifications.instrument("ruby_llm.request", {
|
|
22
|
+
# provider: "openai",
|
|
23
|
+
# request: { model: "gpt-4o", messages: messages },
|
|
24
|
+
# response: response,
|
|
25
|
+
# meta: {
|
|
26
|
+
# project: "support",
|
|
27
|
+
# user: current_user,
|
|
28
|
+
# session_id: session.id,
|
|
29
|
+
# latency_ms: 150,
|
|
30
|
+
# status: :success,
|
|
31
|
+
# tags: ["production", "triage"]
|
|
32
|
+
# }
|
|
33
|
+
# })
|
|
34
|
+
#
|
|
35
|
+
# @see Mappers
|
|
36
|
+
module RubyLLM
|
|
37
|
+
extend self
|
|
38
|
+
|
|
39
|
+
# Default ActiveSupport::Notifications event name
|
|
40
|
+
DEFAULT_EVENT = "ruby_llm.request".freeze
|
|
41
|
+
|
|
42
|
+
# Enables the adapter to start capturing events.
|
|
43
|
+
#
|
|
44
|
+
# Subscribes to the specified instrumentation event and routes payloads
|
|
45
|
+
# through {Mappers} to {TraceBook.record!}.
|
|
46
|
+
#
|
|
47
|
+
# @param instrumentation [String] Event name to subscribe to
|
|
48
|
+
# @return [void]
|
|
49
|
+
#
|
|
50
|
+
# @example
|
|
51
|
+
# TraceBook::Adapters::RubyLLM.enable!
|
|
52
|
+
# TraceBook::Adapters::RubyLLM.enable!(instrumentation: "custom.llm")
|
|
53
|
+
def enable!(instrumentation: DEFAULT_EVENT)
|
|
54
|
+
return if subscribers.key?(instrumentation)
|
|
55
|
+
|
|
56
|
+
subscribers[instrumentation] = ActiveSupport::Notifications.subscribe(instrumentation) do |*args|
|
|
57
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
58
|
+
handle_payload(event.payload.with_indifferent_access)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Disables the adapter and unsubscribes from events.
|
|
63
|
+
#
|
|
64
|
+
# @param instrumentation [String] Event name to unsubscribe from
|
|
65
|
+
# @return [void]
|
|
66
|
+
#
|
|
67
|
+
# @example
|
|
68
|
+
# TraceBook::Adapters::RubyLLM.disable!
|
|
69
|
+
def disable!(instrumentation: DEFAULT_EVENT)
|
|
70
|
+
token = subscribers.delete(instrumentation)
|
|
71
|
+
ActiveSupport::Notifications.unsubscribe(token) if token
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def handle_payload(payload)
|
|
77
|
+
provider = payload[:provider].to_s.presence || "ruby_llm"
|
|
78
|
+
normalized = Mappers.normalize(
|
|
79
|
+
provider,
|
|
80
|
+
raw_request: payload[:request],
|
|
81
|
+
raw_response: payload[:response],
|
|
82
|
+
meta: payload[:meta] || {}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
TraceBook.record!(**normalized.to_h)
|
|
86
|
+
rescue KeyError => error
|
|
87
|
+
Rails.logger.error("TraceBook RubyLLM adapter error: #{error.message}") if defined?(Rails)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def subscribers
|
|
91
|
+
@subscribers ||= {}
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
TraceBook = Tracebook unless defined?(TraceBook)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tracebook
|
|
4
|
+
# Configuration object for TraceBook.
|
|
5
|
+
#
|
|
6
|
+
# Contains all configurable options for the TraceBook engine. Configuration
|
|
7
|
+
# is frozen after the {Tracebook.configure} block executes to prevent
|
|
8
|
+
# runtime modifications.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic configuration
|
|
11
|
+
# TraceBook.configure do |config|
|
|
12
|
+
# config.project_name = "Support Console"
|
|
13
|
+
# config.persist_async = Rails.env.production?
|
|
14
|
+
# config.default_currency = "USD"
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# @example With custom redactors
|
|
18
|
+
# TraceBook.configure do |config|
|
|
19
|
+
# config.custom_redactors += [
|
|
20
|
+
# ->(payload) { payload.gsub(/api_key=\w+/, "api_key=[REDACTED]") }
|
|
21
|
+
# ]
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# @see Tracebook.configure
|
|
25
|
+
class Config
|
|
26
|
+
# @!attribute [rw] project_name
|
|
27
|
+
# @return [String, nil] Project identifier for this application (optional)
|
|
28
|
+
# Used to filter interactions by project in the dashboard
|
|
29
|
+
attr_accessor :project_name
|
|
30
|
+
|
|
31
|
+
# @!attribute [rw] persist_async
|
|
32
|
+
# @return [Boolean] Whether to persist interactions asynchronously (default: true)
|
|
33
|
+
# When true, {Tracebook.record!} enqueues {PersistInteractionJob}.
|
|
34
|
+
# When false, interactions are persisted inline.
|
|
35
|
+
attr_accessor :persist_async
|
|
36
|
+
|
|
37
|
+
# @!attribute [rw] inline_payload_bytes
|
|
38
|
+
# @return [Integer] Maximum payload size before spilling to ActiveStorage (default: 64KB)
|
|
39
|
+
# Payloads larger than this threshold are stored as ActiveStorage blobs
|
|
40
|
+
# instead of inline JSONB columns.
|
|
41
|
+
attr_accessor :inline_payload_bytes
|
|
42
|
+
|
|
43
|
+
# @!attribute [rw] default_currency
|
|
44
|
+
# @return [String] Currency code for cost calculations (default: "USD")
|
|
45
|
+
# Used in {PricingRule} cost tracking
|
|
46
|
+
attr_accessor :default_currency
|
|
47
|
+
|
|
48
|
+
# @!attribute [rw] export_formats
|
|
49
|
+
# @return [Array<Symbol>] Available export formats (default: [:csv, :ndjson])
|
|
50
|
+
# Supported formats for {ExportJob}
|
|
51
|
+
attr_accessor :export_formats
|
|
52
|
+
|
|
53
|
+
# @!attribute [rw] redactors
|
|
54
|
+
# @return [Array<Redactor>] Built-in PII redactors
|
|
55
|
+
# Default redactors for email, phone, credit card numbers.
|
|
56
|
+
# See {Redactors::Email}, {Redactors::Phone}, {Redactors::CardPAN}
|
|
57
|
+
attr_accessor :redactors
|
|
58
|
+
|
|
59
|
+
# @!attribute [rw] custom_redactors
|
|
60
|
+
# @return [Array<Proc>] Custom redaction lambdas
|
|
61
|
+
# Additional user-defined redactors that receive the payload string
|
|
62
|
+
# and return a redacted version.
|
|
63
|
+
# @example
|
|
64
|
+
# config.custom_redactors += [
|
|
65
|
+
# ->(payload) { payload.gsub(/secret=\w+/, "secret=[REDACTED]") }
|
|
66
|
+
# ]
|
|
67
|
+
attr_accessor :custom_redactors
|
|
68
|
+
|
|
69
|
+
# @!attribute [rw] auto_subscribe_ruby_llm
|
|
70
|
+
# @return [Boolean] Auto-enable RubyLLM adapter on boot (default: false)
|
|
71
|
+
attr_accessor :auto_subscribe_ruby_llm
|
|
72
|
+
|
|
73
|
+
# @!attribute [rw] auto_subscribe_active_agent
|
|
74
|
+
# @return [Boolean] Auto-enable ActiveAgent adapter on boot (default: false)
|
|
75
|
+
attr_accessor :auto_subscribe_active_agent
|
|
76
|
+
|
|
77
|
+
# Creates a new configuration with default values.
|
|
78
|
+
#
|
|
79
|
+
# @return [Config]
|
|
80
|
+
def initialize
|
|
81
|
+
@project_name = nil
|
|
82
|
+
@persist_async = true
|
|
83
|
+
@inline_payload_bytes = 64 * 1024
|
|
84
|
+
@default_currency = "USD"
|
|
85
|
+
@export_formats = [ :csv, :ndjson ]
|
|
86
|
+
@redactors = default_redactors
|
|
87
|
+
@custom_redactors = []
|
|
88
|
+
@auto_subscribe_ruby_llm = false
|
|
89
|
+
@auto_subscribe_active_agent = false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns true if configuration has been finalized.
|
|
93
|
+
#
|
|
94
|
+
# @return [Boolean]
|
|
95
|
+
def finalized?
|
|
96
|
+
@finalized == true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Freezes the configuration to prevent further changes.
|
|
100
|
+
#
|
|
101
|
+
# Called automatically by {Tracebook.configure} after the block executes.
|
|
102
|
+
#
|
|
103
|
+
# @return [void]
|
|
104
|
+
def finalize!
|
|
105
|
+
return if finalized?
|
|
106
|
+
|
|
107
|
+
@finalized = true
|
|
108
|
+
freeze_collections!
|
|
109
|
+
freeze
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def default_redactors
|
|
115
|
+
[
|
|
116
|
+
Tracebook::Redactors::Email.new,
|
|
117
|
+
Tracebook::Redactors::Phone.new,
|
|
118
|
+
Tracebook::Redactors::CardPAN.new
|
|
119
|
+
]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def freeze_collections!
|
|
123
|
+
@redactors = @redactors.map { |redactor| redactor }.freeze
|
|
124
|
+
@custom_redactors = @custom_redactors.map { |callable| callable }.freeze
|
|
125
|
+
@export_formats = @export_formats.map(&:to_sym).freeze
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
TraceBook = Tracebook unless defined?(TraceBook)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Tracebook
|
|
6
|
+
module Mappers
|
|
7
|
+
class Anthropic < Base
|
|
8
|
+
def normalize(raw_request:, raw_response:, meta: {})
|
|
9
|
+
request = symbolize(raw_request || {})
|
|
10
|
+
response = symbolize(raw_response || {})
|
|
11
|
+
meta_info = indifferent_meta(meta)
|
|
12
|
+
|
|
13
|
+
build_interaction(
|
|
14
|
+
provider: "anthropic",
|
|
15
|
+
model: request[:model] || response[:model],
|
|
16
|
+
project: meta_info[:project],
|
|
17
|
+
request_payload: raw_request,
|
|
18
|
+
response_payload: raw_response,
|
|
19
|
+
request_text: extract_blocks(request[:messages]),
|
|
20
|
+
response_text: extract_blocks(response[:content]),
|
|
21
|
+
input_tokens: anthropic_usage(response, :input_tokens),
|
|
22
|
+
output_tokens: anthropic_usage(response, :output_tokens),
|
|
23
|
+
latency_ms: meta_info[:latency_ms],
|
|
24
|
+
status: meta_info[:status]&.to_sym || :success,
|
|
25
|
+
error_class: nil,
|
|
26
|
+
error_message: nil,
|
|
27
|
+
tags: Array(meta_info[:tags]).compact,
|
|
28
|
+
metadata: {},
|
|
29
|
+
user: meta_info[:user],
|
|
30
|
+
parent_id: meta_info[:parent_id],
|
|
31
|
+
session_id: meta_info[:session_id]
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def extract_blocks(blocks)
|
|
38
|
+
Array(blocks).flat_map do |block|
|
|
39
|
+
block = block.with_indifferent_access
|
|
40
|
+
case block[:type]
|
|
41
|
+
when "text"
|
|
42
|
+
block[:text]
|
|
43
|
+
when "input_text"
|
|
44
|
+
block[:text]
|
|
45
|
+
else
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
end.compact.join("\n\n")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def anthropic_usage(response, key)
|
|
52
|
+
usage = response[:usage] || {}
|
|
53
|
+
usage.with_indifferent_access[key]&.to_i
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
TraceBook = Tracebook unless defined?(TraceBook)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
4
|
+
require "active_support/core_ext/object/deep_dup"
|
|
5
|
+
|
|
6
|
+
module Tracebook
|
|
7
|
+
module Mappers
|
|
8
|
+
class Base
|
|
9
|
+
def normalize(raw_request:, raw_response:, meta: {})
|
|
10
|
+
raise NotImplementedError
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def build_interaction(**attributes)
|
|
16
|
+
NormalizedInteraction.new(**attributes)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def indifferent_meta(meta)
|
|
20
|
+
(meta || {}).with_indifferent_access
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def symbolize(hash)
|
|
24
|
+
hash.deep_dup.transform_keys { |key| key.respond_to?(:to_sym) ? key.to_sym : key }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def compact_hash(hash)
|
|
28
|
+
hash.each_with_object({}) do |(key, value), memo|
|
|
29
|
+
next if value.nil?
|
|
30
|
+
|
|
31
|
+
memo[key] = value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
TraceBook = Tracebook unless defined?(TraceBook)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Tracebook
|
|
6
|
+
module Mappers
|
|
7
|
+
class Ollama < Base
|
|
8
|
+
def normalize(raw_request:, raw_response:, meta: {})
|
|
9
|
+
request = symbolize(raw_request || {})
|
|
10
|
+
response = symbolize(raw_response || {})
|
|
11
|
+
meta_info = indifferent_meta(meta)
|
|
12
|
+
|
|
13
|
+
metadata = {}
|
|
14
|
+
metadata["eval_count"] = response[:eval_count] if response.key?(:eval_count)
|
|
15
|
+
|
|
16
|
+
build_interaction(
|
|
17
|
+
provider: "ollama",
|
|
18
|
+
model: request[:model] || response[:model],
|
|
19
|
+
project: meta_info[:project],
|
|
20
|
+
request_payload: raw_request,
|
|
21
|
+
response_payload: raw_response,
|
|
22
|
+
request_text: request[:prompt] || request[:input],
|
|
23
|
+
response_text: response[:response],
|
|
24
|
+
input_tokens: response[:prompt_eval_count],
|
|
25
|
+
output_tokens: response[:eval_count],
|
|
26
|
+
latency_ms: meta_info[:latency_ms] || to_milliseconds(response[:total_duration]),
|
|
27
|
+
status: meta_info[:status]&.to_sym || :success,
|
|
28
|
+
error_class: nil,
|
|
29
|
+
error_message: nil,
|
|
30
|
+
tags: Array(meta_info[:tags]).compact,
|
|
31
|
+
metadata: metadata,
|
|
32
|
+
user: meta_info[:user],
|
|
33
|
+
parent_id: meta_info[:parent_id],
|
|
34
|
+
session_id: meta_info[:session_id]
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def to_milliseconds(value)
|
|
41
|
+
return unless value
|
|
42
|
+
|
|
43
|
+
(value.to_f * 1000).to_i
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
TraceBook = Tracebook unless defined?(TraceBook)
|