tracebook 0.1.1 → 1.0.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 +4 -4
- data/CHANGELOG.md +49 -24
- data/README.md +197 -713
- data/app/assets/javascripts/tracebook/application.js +92 -35
- data/app/assets/stylesheets/tracebook/application.css +1882 -55
- data/app/controllers/tracebook/application_controller.rb +25 -0
- data/app/controllers/tracebook/chats_controller.rb +229 -0
- data/app/controllers/tracebook/comments_controller.rb +25 -0
- data/app/helpers/tracebook/chats_helper.rb +29 -0
- data/app/models/tracebook/chat_review.rb +19 -0
- data/app/models/tracebook/comment.rb +14 -0
- data/app/models/tracebook/message_cost.rb +12 -0
- data/app/models/tracebook/pricing_rule.rb +6 -8
- data/app/views/tracebook/chats/index.html.erb +77 -0
- data/app/views/tracebook/chats/show.html.erb +94 -0
- data/config/routes.rb +6 -6
- data/db/migrate/20260325000100_create_tracebook_message_costs.rb +19 -0
- data/db/migrate/20260325000200_create_tracebook_chat_reviews.rb +19 -0
- data/db/migrate/{20241112000300_create_tracebook_pricing_rules.rb → 20260325000300_create_tracebook_pricing_rules.rb} +3 -3
- data/db/migrate/20260325000500_create_tracebook_comments.rb +15 -0
- data/lib/generators/tracebook/install/install_generator.rb +6 -9
- data/lib/generators/tracebook/install/templates/initializer.rb.tt +11 -5
- data/lib/tasks/tracebook_tasks.rake +14 -4
- data/lib/tracebook/adapters/ruby_llm.rb +19 -81
- data/lib/tracebook/adapters.rb +5 -4
- data/lib/tracebook/config.rb +83 -104
- data/lib/tracebook/engine.rb +6 -0
- data/lib/tracebook/errors.rb +0 -2
- data/lib/tracebook/pricing/calculator.rb +11 -6
- data/lib/tracebook/pricing.rb +0 -2
- data/lib/tracebook/redaction/pattern.rb +124 -0
- data/lib/tracebook/redaction/pipeline.rb +32 -0
- data/lib/tracebook/seeds/pricing_rules.rb +62 -0
- data/lib/tracebook/version.rb +1 -1
- data/lib/tracebook.rb +46 -152
- metadata +23 -51
- data/app/controllers/tracebook/exports_controller.rb +0 -25
- data/app/controllers/tracebook/interactions_controller.rb +0 -71
- data/app/helpers/tracebook/interactions_helper.rb +0 -35
- data/app/jobs/tracebook/daily_rollups_job.rb +0 -100
- data/app/jobs/tracebook/export_job.rb +0 -162
- data/app/jobs/tracebook/persist_interaction_job.rb +0 -160
- data/app/mailers/tracebook/application_mailer.rb +0 -6
- data/app/models/tracebook/interaction.rb +0 -103
- data/app/models/tracebook/redaction_rule.rb +0 -81
- data/app/models/tracebook/rollup_daily.rb +0 -73
- data/app/views/tracebook/interactions/index.html.erb +0 -108
- data/app/views/tracebook/interactions/show.html.erb +0 -44
- data/db/migrate/20241112000100_create_tracebook_interactions.rb +0 -55
- data/db/migrate/20241112000200_create_tracebook_rollups_dailies.rb +0 -24
- data/db/migrate/20241112000400_create_tracebook_redaction_rules.rb +0 -19
- data/lib/tracebook/adapters/active_agent.rb +0 -82
- data/lib/tracebook/mappers/anthropic.rb +0 -59
- data/lib/tracebook/mappers/base.rb +0 -38
- data/lib/tracebook/mappers/ollama.rb +0 -49
- data/lib/tracebook/mappers/openai.rb +0 -75
- data/lib/tracebook/mappers.rb +0 -283
- data/lib/tracebook/normalized_interaction.rb +0 -86
- data/lib/tracebook/redaction_pipeline.rb +0 -88
- data/lib/tracebook/redactors/base.rb +0 -29
- data/lib/tracebook/redactors/card_pan.rb +0 -15
- data/lib/tracebook/redactors/email.rb +0 -15
- data/lib/tracebook/redactors/phone.rb +0 -15
- data/lib/tracebook/redactors.rb +0 -8
- data/lib/tracebook/result.rb +0 -53
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tracebook
|
|
4
|
+
module Seeds
|
|
5
|
+
module PricingRules
|
|
6
|
+
# All prices are in cents per 1,000,000 tokens.
|
|
7
|
+
# effective_from is set to provider's approximate release date.
|
|
8
|
+
DEFAULTS = [
|
|
9
|
+
# Gemini (Google)
|
|
10
|
+
{ provider: "gemini", model_glob: "gemini-2.0-flash*", input_cents_per_unit: 10, output_cents_per_unit: 40, effective_from: Date.new(2024, 12, 11) },
|
|
11
|
+
{ provider: "gemini", model_glob: "gemini-1.5-pro*", input_cents_per_unit: 125, output_cents_per_unit: 500, effective_from: Date.new(2024, 5, 14) },
|
|
12
|
+
{ provider: "gemini", model_glob: "gemini-1.5-flash*", input_cents_per_unit: 8, output_cents_per_unit: 30, effective_from: Date.new(2024, 5, 14) },
|
|
13
|
+
|
|
14
|
+
# OpenAI
|
|
15
|
+
{ provider: "openai", model_glob: "gpt-4o", input_cents_per_unit: 250, output_cents_per_unit: 1000, effective_from: Date.new(2024, 5, 13) },
|
|
16
|
+
{ provider: "openai", model_glob: "gpt-4o-mini*", input_cents_per_unit: 15, output_cents_per_unit: 60, effective_from: Date.new(2024, 7, 18) },
|
|
17
|
+
{ provider: "openai", model_glob: "gpt-4-turbo*", input_cents_per_unit: 1000, output_cents_per_unit: 3000, effective_from: Date.new(2024, 4, 9) },
|
|
18
|
+
|
|
19
|
+
# Anthropic
|
|
20
|
+
{ provider: "anthropic", model_glob: "claude-3-5-sonnet*", input_cents_per_unit: 300, output_cents_per_unit: 1500, effective_from: Date.new(2024, 6, 20) },
|
|
21
|
+
{ provider: "anthropic", model_glob: "claude-3-5-haiku*", input_cents_per_unit: 80, output_cents_per_unit: 400, effective_from: Date.new(2024, 10, 22) },
|
|
22
|
+
{ provider: "anthropic", model_glob: "claude-3-opus*", input_cents_per_unit: 1500, output_cents_per_unit: 7500, effective_from: Date.new(2024, 3, 4) },
|
|
23
|
+
|
|
24
|
+
# Ollama (local/free)
|
|
25
|
+
{ provider: "ollama", model_glob: "*", input_cents_per_unit: 0, output_cents_per_unit: 0, effective_from: Date.new(2023, 1, 1) }
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
# Seeds default pricing rules idempotently.
|
|
30
|
+
# Uses find_or_create_by on provider + model_glob to avoid duplicates.
|
|
31
|
+
#
|
|
32
|
+
# @return [Hash] Summary with :created and :skipped counts
|
|
33
|
+
def seed!
|
|
34
|
+
created = 0
|
|
35
|
+
skipped = 0
|
|
36
|
+
|
|
37
|
+
DEFAULTS.each do |attrs|
|
|
38
|
+
rule = Tracebook::PricingRule.find_or_initialize_by(
|
|
39
|
+
provider: attrs[:provider],
|
|
40
|
+
model_glob: attrs[:model_glob]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if rule.new_record?
|
|
44
|
+
rule.assign_attributes(
|
|
45
|
+
input_cents_per_unit: attrs[:input_cents_per_unit],
|
|
46
|
+
output_cents_per_unit: attrs[:output_cents_per_unit],
|
|
47
|
+
effective_from: attrs[:effective_from],
|
|
48
|
+
currency: "USD"
|
|
49
|
+
)
|
|
50
|
+
rule.save!
|
|
51
|
+
created += 1
|
|
52
|
+
else
|
|
53
|
+
skipped += 1
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
{ created:, skipped: }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/tracebook/version.rb
CHANGED
data/lib/tracebook.rb
CHANGED
|
@@ -2,163 +2,80 @@ require "pagy"
|
|
|
2
2
|
require "tracebook/version"
|
|
3
3
|
require "tracebook/engine"
|
|
4
4
|
require "tracebook/errors"
|
|
5
|
-
require "tracebook/
|
|
6
|
-
require "tracebook/
|
|
5
|
+
require "tracebook/redaction/pattern"
|
|
6
|
+
require "tracebook/redaction/pipeline"
|
|
7
7
|
require "tracebook/config"
|
|
8
|
-
require "tracebook/result"
|
|
9
|
-
require "tracebook/normalized_interaction"
|
|
10
|
-
require "tracebook/redaction_pipeline"
|
|
11
8
|
require "tracebook/pricing"
|
|
12
9
|
require "tracebook/adapters"
|
|
10
|
+
require "tracebook/seeds/pricing_rules"
|
|
13
11
|
|
|
14
|
-
# TraceBook is a Rails engine for
|
|
12
|
+
# TraceBook is a Rails engine for cost tracking and review of LLM conversations.
|
|
15
13
|
#
|
|
16
|
-
# It
|
|
17
|
-
# -
|
|
18
|
-
# -
|
|
19
|
-
# -
|
|
20
|
-
# - Review workflow (approve/flag/reject) with audit trail
|
|
21
|
-
# - Hotwire-powered dashboard UI with filtering and export
|
|
22
|
-
# - Built-in adapters for OpenAI, Anthropic, Ollama
|
|
23
|
-
# - Support for hierarchical agent sessions (parent-child relationships)
|
|
14
|
+
# It works as a layer on top of RubyLLM, adding:
|
|
15
|
+
# - Cost calculation per message based on pricing rules
|
|
16
|
+
# - Review workflow (approve/flag) per chat
|
|
17
|
+
# - Dashboard UI for monitoring LLM usage
|
|
24
18
|
#
|
|
25
|
-
# @example
|
|
26
|
-
#
|
|
27
|
-
# config.
|
|
28
|
-
# config.
|
|
29
|
-
# config.
|
|
19
|
+
# @example Configuration
|
|
20
|
+
# Tracebook.configure do |config|
|
|
21
|
+
# config.chat_class = "Chat"
|
|
22
|
+
# config.message_class = "Message"
|
|
23
|
+
# config.default_currency = "USD"
|
|
24
|
+
# config.actor_display = ->(actor) { actor.try(:name) }
|
|
30
25
|
# end
|
|
31
26
|
#
|
|
32
|
-
# @example
|
|
33
|
-
#
|
|
34
|
-
# provider: "openai",
|
|
35
|
-
# model: "gpt-4o",
|
|
36
|
-
# request_payload: { messages: messages },
|
|
37
|
-
# response_payload: response,
|
|
38
|
-
# input_tokens: 100,
|
|
39
|
-
# output_tokens: 50,
|
|
40
|
-
# user: current_user,
|
|
41
|
-
# tags: ["production", "support"]
|
|
42
|
-
# )
|
|
43
|
-
#
|
|
44
|
-
# @see https://github.com/dpaluy/tracebook README for full documentation
|
|
27
|
+
# @example Cost calculation
|
|
28
|
+
# Tracebook.calculate_cost!(message)
|
|
45
29
|
module Tracebook
|
|
46
30
|
class << self
|
|
47
|
-
# Returns the current configuration instance.
|
|
48
|
-
#
|
|
49
|
-
# @return [Tracebook::Config] the configuration object
|
|
50
31
|
def config
|
|
51
32
|
@config ||= Config.new
|
|
52
33
|
end
|
|
53
34
|
|
|
54
|
-
# Configures TraceBook with a block.
|
|
55
|
-
#
|
|
56
|
-
# Configuration is frozen after the block executes. Call {#reset_configuration!}
|
|
57
|
-
# in tests to reset.
|
|
58
|
-
#
|
|
59
|
-
# @yield [config] Yields the config object for modification
|
|
60
|
-
# @yieldparam config [Tracebook::Config] the configuration object
|
|
61
|
-
# @return [Tracebook::Config] the finalized configuration
|
|
62
|
-
# @raise [ConfigurationError] if configuration is already finalized
|
|
63
|
-
#
|
|
64
|
-
# @example
|
|
65
|
-
# TraceBook.configure do |config|
|
|
66
|
-
# config.authorize = ->(user, action, resource) { user&.admin? }
|
|
67
|
-
# config.persist_async = true
|
|
68
|
-
# config.project_name = "Support App"
|
|
69
|
-
# end
|
|
70
35
|
def configure
|
|
71
36
|
ensure_configurable!
|
|
72
|
-
|
|
73
37
|
yield(config)
|
|
74
38
|
finalize_configuration!
|
|
75
39
|
config
|
|
76
40
|
end
|
|
77
41
|
|
|
78
|
-
# Resets configuration to a clean state.
|
|
79
|
-
#
|
|
80
|
-
# Used in tests to start with fresh configuration between test cases.
|
|
81
|
-
#
|
|
82
|
-
# @return [void]
|
|
83
|
-
#
|
|
84
|
-
# @example In test setup
|
|
85
|
-
# setup do
|
|
86
|
-
# TraceBook.reset_configuration!
|
|
87
|
-
# TraceBook.configure do |config|
|
|
88
|
-
# config.authorize = ->(*) { true }
|
|
89
|
-
# end
|
|
90
|
-
# end
|
|
91
42
|
def reset_configuration!
|
|
92
43
|
@config = Config.new
|
|
93
44
|
@configuration_finalized = false
|
|
94
45
|
end
|
|
95
46
|
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
# When `config.persist_async` is true, the interaction is enqueued via
|
|
99
|
-
# {PersistInteractionJob}. Otherwise, it's persisted inline.
|
|
47
|
+
# Redact PII from text using configured patterns and custom redactors.
|
|
100
48
|
#
|
|
101
|
-
# @param
|
|
102
|
-
# @
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
#
|
|
108
|
-
# @option attributes [String, nil] :response_text Human-readable response summary
|
|
109
|
-
# @option attributes [Integer, nil] :input_tokens Prompt token count
|
|
110
|
-
# @option attributes [Integer, nil] :output_tokens Completion token count
|
|
111
|
-
# @option attributes [Integer, nil] :latency_ms Request duration in milliseconds
|
|
112
|
-
# @option attributes [Symbol, String] :status :success, :error, or :canceled (default: :success)
|
|
113
|
-
# @option attributes [String, nil] :error_class Exception class name on failure
|
|
114
|
-
# @option attributes [String, nil] :error_message Exception message on failure
|
|
115
|
-
# @option attributes [Array<String>] :tags Labels for filtering (e.g., ["prod", "urgent"])
|
|
116
|
-
# @option attributes [Hash] :metadata Custom metadata (e.g., { ticket_id: 123 })
|
|
117
|
-
# @option attributes [ActiveRecord::Base, nil] :user Associated user (polymorphic)
|
|
118
|
-
# @option attributes [String, nil] :session_id Session identifier for grouping related calls
|
|
119
|
-
# @option attributes [Integer, nil] :parent_id Parent interaction ID for hierarchical chains
|
|
120
|
-
# @option attributes [String, nil] :idempotency_key Key for deduplication
|
|
121
|
-
#
|
|
122
|
-
# @return [Tracebook::Result] Result object with success/error information
|
|
123
|
-
#
|
|
124
|
-
# @example Recording a successful completion
|
|
125
|
-
# result = TraceBook.record!(
|
|
126
|
-
# provider: "openai",
|
|
127
|
-
# model: "gpt-4o-mini",
|
|
128
|
-
# request_payload: { messages: [{ role: "user", content: "Hello" }] },
|
|
129
|
-
# response_payload: { choices: [{ message: { content: "Hi!" } }] },
|
|
130
|
-
# input_tokens: 10,
|
|
131
|
-
# output_tokens: 5,
|
|
132
|
-
# latency_ms: 150,
|
|
133
|
-
# status: :success,
|
|
134
|
-
# user: current_user,
|
|
135
|
-
# tags: ["greeting"]
|
|
136
|
-
# )
|
|
49
|
+
# @param text [String] the text to redact
|
|
50
|
+
# @return [String] redacted text
|
|
51
|
+
def redact(text)
|
|
52
|
+
config.redaction_pipeline.call(text)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Calculate and store cost for a message.
|
|
137
56
|
#
|
|
138
|
-
# @
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
result = Result.new(idempotency_key: attributes[:idempotency_key])
|
|
57
|
+
# @param message [ActiveRecord::Base] a message record with input_tokens, output_tokens
|
|
58
|
+
# @param provider [String] provider name (e.g., "openai", "anthropic")
|
|
59
|
+
# @param model [String] model identifier (e.g., "gpt-4o")
|
|
60
|
+
# @param latency_ms [Integer, nil] request duration in milliseconds
|
|
61
|
+
# @return [Tracebook::MessageCost] the created cost record
|
|
62
|
+
def calculate_cost!(message, provider:, model:, latency_ms: nil)
|
|
63
|
+
cost = Pricing::Calculator.call(
|
|
64
|
+
provider: provider,
|
|
65
|
+
model: model,
|
|
66
|
+
input_tokens: message.input_tokens,
|
|
67
|
+
output_tokens: message.output_tokens,
|
|
68
|
+
occurred_at: message.created_at
|
|
69
|
+
)
|
|
152
70
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
Result.new(error: error, idempotency_key: attributes[:idempotency_key])
|
|
71
|
+
MessageCost.create!(
|
|
72
|
+
message: message,
|
|
73
|
+
cost_input_cents: cost.input_cents,
|
|
74
|
+
cost_output_cents: cost.output_cents,
|
|
75
|
+
cost_total_cents: cost.total_cents,
|
|
76
|
+
currency: cost.currency || config.default_currency,
|
|
77
|
+
latency_ms: latency_ms
|
|
78
|
+
)
|
|
162
79
|
end
|
|
163
80
|
|
|
164
81
|
private
|
|
@@ -171,30 +88,7 @@ module Tracebook
|
|
|
171
88
|
def ensure_configurable!
|
|
172
89
|
return unless @configuration_finalized || config.finalized?
|
|
173
90
|
|
|
174
|
-
raise ConfigurationError, "
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def build_normalized_interaction(attributes)
|
|
178
|
-
NormalizedInteraction.new(
|
|
179
|
-
provider: attributes.fetch(:provider),
|
|
180
|
-
model: attributes.fetch(:model),
|
|
181
|
-
project: attributes[:project],
|
|
182
|
-
request_payload: attributes[:request_payload],
|
|
183
|
-
response_payload: attributes[:response_payload],
|
|
184
|
-
request_text: attributes[:request_text],
|
|
185
|
-
response_text: attributes[:response_text],
|
|
186
|
-
input_tokens: attributes[:input_tokens],
|
|
187
|
-
output_tokens: attributes[:output_tokens],
|
|
188
|
-
latency_ms: attributes[:latency_ms],
|
|
189
|
-
status: attributes.fetch(:status, "success"),
|
|
190
|
-
error_class: attributes[:error_class],
|
|
191
|
-
error_message: attributes[:error_message],
|
|
192
|
-
tags: Array(attributes[:tags]).compact,
|
|
193
|
-
metadata: attributes[:metadata] || {},
|
|
194
|
-
user: attributes[:user],
|
|
195
|
-
parent_id: attributes[:parent_id],
|
|
196
|
-
session_id: attributes[:session_id]
|
|
197
|
-
)
|
|
91
|
+
raise ConfigurationError, "Tracebook configuration is already finalized"
|
|
198
92
|
end
|
|
199
93
|
end
|
|
200
94
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tracebook
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dpaluy
|
|
@@ -51,20 +51,6 @@ dependencies:
|
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '1.3'
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: csv
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - "~>"
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '3.3'
|
|
61
|
-
type: :runtime
|
|
62
|
-
prerelease: false
|
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - "~>"
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '3.3'
|
|
68
54
|
- !ruby/object:Gem::Dependency
|
|
69
55
|
name: pagy
|
|
70
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -79,9 +65,10 @@ dependencies:
|
|
|
79
65
|
- - ">="
|
|
80
66
|
- !ruby/object:Gem::Version
|
|
81
67
|
version: '43.0'
|
|
82
|
-
description:
|
|
83
|
-
|
|
84
|
-
|
|
68
|
+
description: Tracebook is a Rails engine that adds cost tracking, review workflows,
|
|
69
|
+
and a dashboard UI on top of RubyLLM's Chat and Message models. Features include
|
|
70
|
+
per-message cost calculation with configurable pricing rules, chat-level approval
|
|
71
|
+
workflows, and a Hotwire-powered dashboard.
|
|
85
72
|
email:
|
|
86
73
|
- dpaluy@users.noreply.github.com
|
|
87
74
|
executables: []
|
|
@@ -100,29 +87,25 @@ files:
|
|
|
100
87
|
- app/assets/stylesheets/tracebook/application.css
|
|
101
88
|
- app/controllers/concerns/.keep
|
|
102
89
|
- app/controllers/tracebook/application_controller.rb
|
|
103
|
-
- app/controllers/tracebook/
|
|
104
|
-
- app/controllers/tracebook/
|
|
90
|
+
- app/controllers/tracebook/chats_controller.rb
|
|
91
|
+
- app/controllers/tracebook/comments_controller.rb
|
|
105
92
|
- app/helpers/tracebook/application_helper.rb
|
|
106
|
-
- app/helpers/tracebook/
|
|
93
|
+
- app/helpers/tracebook/chats_helper.rb
|
|
107
94
|
- app/jobs/tracebook/application_job.rb
|
|
108
|
-
- app/jobs/tracebook/daily_rollups_job.rb
|
|
109
|
-
- app/jobs/tracebook/export_job.rb
|
|
110
|
-
- app/jobs/tracebook/persist_interaction_job.rb
|
|
111
|
-
- app/mailers/tracebook/application_mailer.rb
|
|
112
95
|
- app/models/concerns/.keep
|
|
113
96
|
- app/models/tracebook/application_record.rb
|
|
114
|
-
- app/models/tracebook/
|
|
97
|
+
- app/models/tracebook/chat_review.rb
|
|
98
|
+
- app/models/tracebook/comment.rb
|
|
99
|
+
- app/models/tracebook/message_cost.rb
|
|
115
100
|
- app/models/tracebook/pricing_rule.rb
|
|
116
|
-
- app/models/tracebook/redaction_rule.rb
|
|
117
|
-
- app/models/tracebook/rollup_daily.rb
|
|
118
101
|
- app/views/layouts/tracebook/application.html.erb
|
|
119
|
-
- app/views/tracebook/
|
|
120
|
-
- app/views/tracebook/
|
|
102
|
+
- app/views/tracebook/chats/index.html.erb
|
|
103
|
+
- app/views/tracebook/chats/show.html.erb
|
|
121
104
|
- config/routes.rb
|
|
122
|
-
- db/migrate/
|
|
123
|
-
- db/migrate/
|
|
124
|
-
- db/migrate/
|
|
125
|
-
- db/migrate/
|
|
105
|
+
- db/migrate/20260325000100_create_tracebook_message_costs.rb
|
|
106
|
+
- db/migrate/20260325000200_create_tracebook_chat_reviews.rb
|
|
107
|
+
- db/migrate/20260325000300_create_tracebook_pricing_rules.rb
|
|
108
|
+
- db/migrate/20260325000500_create_tracebook_comments.rb
|
|
126
109
|
- lib/generators/tracebook/install/USAGE
|
|
127
110
|
- lib/generators/tracebook/install/install_generator.rb
|
|
128
111
|
- lib/generators/tracebook/install/templates/initializer.rb.tt
|
|
@@ -130,26 +113,15 @@ files:
|
|
|
130
113
|
- lib/tasks/yard.rake
|
|
131
114
|
- lib/tracebook.rb
|
|
132
115
|
- lib/tracebook/adapters.rb
|
|
133
|
-
- lib/tracebook/adapters/active_agent.rb
|
|
134
116
|
- lib/tracebook/adapters/ruby_llm.rb
|
|
135
117
|
- lib/tracebook/config.rb
|
|
136
118
|
- lib/tracebook/engine.rb
|
|
137
119
|
- lib/tracebook/errors.rb
|
|
138
|
-
- lib/tracebook/mappers.rb
|
|
139
|
-
- lib/tracebook/mappers/anthropic.rb
|
|
140
|
-
- lib/tracebook/mappers/base.rb
|
|
141
|
-
- lib/tracebook/mappers/ollama.rb
|
|
142
|
-
- lib/tracebook/mappers/openai.rb
|
|
143
|
-
- lib/tracebook/normalized_interaction.rb
|
|
144
120
|
- lib/tracebook/pricing.rb
|
|
145
121
|
- lib/tracebook/pricing/calculator.rb
|
|
146
|
-
- lib/tracebook/
|
|
147
|
-
- lib/tracebook/
|
|
148
|
-
- lib/tracebook/
|
|
149
|
-
- lib/tracebook/redactors/card_pan.rb
|
|
150
|
-
- lib/tracebook/redactors/email.rb
|
|
151
|
-
- lib/tracebook/redactors/phone.rb
|
|
152
|
-
- lib/tracebook/result.rb
|
|
122
|
+
- lib/tracebook/redaction/pattern.rb
|
|
123
|
+
- lib/tracebook/redaction/pipeline.rb
|
|
124
|
+
- lib/tracebook/seeds/pricing_rules.rb
|
|
153
125
|
- lib/tracebook/version.rb
|
|
154
126
|
homepage: https://github.com/dpaluy/tracebook
|
|
155
127
|
licenses:
|
|
@@ -159,7 +131,7 @@ metadata:
|
|
|
159
131
|
homepage_uri: https://github.com/dpaluy/tracebook
|
|
160
132
|
documentation_uri: https://rubydoc.info/gems/tracebook
|
|
161
133
|
source_code_uri: https://github.com/dpaluy/tracebook
|
|
162
|
-
changelog_uri: https://github.com/dpaluy/tracebook/blob/
|
|
134
|
+
changelog_uri: https://github.com/dpaluy/tracebook/blob/master/CHANGELOG.md
|
|
163
135
|
bug_tracker_uri: https://github.com/dpaluy/tracebook/issues
|
|
164
136
|
rdoc_options: []
|
|
165
137
|
require_paths:
|
|
@@ -168,7 +140,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
168
140
|
requirements:
|
|
169
141
|
- - ">="
|
|
170
142
|
- !ruby/object:Gem::Version
|
|
171
|
-
version: 3.
|
|
143
|
+
version: 3.4.0
|
|
172
144
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
173
145
|
requirements:
|
|
174
146
|
- - ">="
|
|
@@ -177,5 +149,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
177
149
|
requirements: []
|
|
178
150
|
rubygems_version: 3.6.9
|
|
179
151
|
specification_version: 4
|
|
180
|
-
summary:
|
|
152
|
+
summary: Cost tracking and review dashboard for RubyLLM conversations.
|
|
181
153
|
test_files: []
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Tracebook
|
|
4
|
-
class ExportsController < ApplicationController
|
|
5
|
-
def create
|
|
6
|
-
blob = ExportJob.perform_now(format: params.fetch(:format, :csv), filters: export_filters)
|
|
7
|
-
redirect_to export_path(blob.signed_id), notice: "Export ready"
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def show
|
|
11
|
-
blob = ActiveStorage::Blob.find_signed(params[:id])
|
|
12
|
-
send_data blob.download, filename: blob.filename.to_s, type: blob.content_type
|
|
13
|
-
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
|
14
|
-
head :not_found
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
private
|
|
18
|
-
|
|
19
|
-
def export_filters
|
|
20
|
-
params.fetch(:filters, {}).permit(:provider, :model, :project, :status, :review_state, :from, :to)
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
TraceBook = Tracebook unless defined?(TraceBook)
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Tracebook
|
|
4
|
-
class InteractionsController < ApplicationController
|
|
5
|
-
include Pagy::Method
|
|
6
|
-
|
|
7
|
-
before_action :set_interaction, only: [ :show, :review ]
|
|
8
|
-
helper InteractionsHelper
|
|
9
|
-
|
|
10
|
-
def index
|
|
11
|
-
@filters = filter_params
|
|
12
|
-
scope = Interaction.filtered(@filters)
|
|
13
|
-
@kpis = kpis_for(scope)
|
|
14
|
-
@pagy, @interactions = pagy(scope.order(created_at: :desc), limit: Tracebook.config.per_page)
|
|
15
|
-
@providers = Interaction.distinct.order(:provider).pluck(:provider)
|
|
16
|
-
@models = Interaction.distinct.order(:model).pluck(:model)
|
|
17
|
-
@projects = Interaction.distinct.order(:project).pluck(:project).compact
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def show
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def review
|
|
24
|
-
state = params.require(:review_state).to_s
|
|
25
|
-
unless Interaction.review_states.key?(state)
|
|
26
|
-
redirect_to interaction_path(@interaction), alert: "Invalid review state: #{state}"
|
|
27
|
-
return
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
if @interaction.update(review_state: state)
|
|
31
|
-
redirect_to interaction_path(@interaction), notice: "Review updated"
|
|
32
|
-
else
|
|
33
|
-
render :show, status: :unprocessable_entity
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def bulk_review
|
|
38
|
-
ids = Array(params[:interaction_ids])
|
|
39
|
-
state = params.require(:review_state).to_s
|
|
40
|
-
unless Interaction.review_states.key?(state)
|
|
41
|
-
redirect_to interactions_path, alert: "Invalid review state: #{state}"
|
|
42
|
-
return
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
Interaction.where(id: ids).update_all(review_state: Interaction.review_states.fetch(state))
|
|
46
|
-
redirect_to interactions_path, notice: "Updated #{ids.size} interactions"
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
def set_interaction
|
|
52
|
-
@interaction = Interaction.find(params[:id])
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def filter_params
|
|
56
|
-
params.fetch(:filters, {}).permit(:provider, :model, :project, :status, :review_state, :tag, :from, :to)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def kpis_for(scope)
|
|
60
|
-
{
|
|
61
|
-
total: scope.count,
|
|
62
|
-
success: scope.status_success.count,
|
|
63
|
-
cost_cents: scope.sum(:cost_total_cents),
|
|
64
|
-
input_tokens: scope.sum(:input_tokens),
|
|
65
|
-
output_tokens: scope.sum(:output_tokens)
|
|
66
|
-
}
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
TraceBook = Tracebook unless defined?(TraceBook)
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
|
-
module Tracebook
|
|
6
|
-
module InteractionsHelper
|
|
7
|
-
def payload_for(interaction, type)
|
|
8
|
-
inline = interaction.public_send("#{type}_payload")
|
|
9
|
-
return inline unless inline.nil? || (inline.respond_to?(:empty?) && inline.empty?)
|
|
10
|
-
|
|
11
|
-
blob = interaction.public_send("#{type}_payload_blob")
|
|
12
|
-
return nil unless blob
|
|
13
|
-
|
|
14
|
-
raw = blob.download
|
|
15
|
-
JSON.parse(raw)
|
|
16
|
-
rescue JSON::ParserError
|
|
17
|
-
raw
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def formatted_payload(payload, fallback_text = nil)
|
|
21
|
-
case payload
|
|
22
|
-
when Hash, Array
|
|
23
|
-
JSON.pretty_generate(payload)
|
|
24
|
-
when String
|
|
25
|
-
payload
|
|
26
|
-
when nil
|
|
27
|
-
fallback_text.to_s
|
|
28
|
-
else
|
|
29
|
-
JSON.pretty_generate(payload.as_json)
|
|
30
|
-
end
|
|
31
|
-
rescue JSON::GeneratorError, TypeError
|
|
32
|
-
fallback_text ? fallback_text.to_s : payload.to_s
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|