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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -24
  3. data/README.md +197 -713
  4. data/app/assets/javascripts/tracebook/application.js +92 -35
  5. data/app/assets/stylesheets/tracebook/application.css +1882 -55
  6. data/app/controllers/tracebook/application_controller.rb +25 -0
  7. data/app/controllers/tracebook/chats_controller.rb +229 -0
  8. data/app/controllers/tracebook/comments_controller.rb +25 -0
  9. data/app/helpers/tracebook/chats_helper.rb +29 -0
  10. data/app/models/tracebook/chat_review.rb +19 -0
  11. data/app/models/tracebook/comment.rb +14 -0
  12. data/app/models/tracebook/message_cost.rb +12 -0
  13. data/app/models/tracebook/pricing_rule.rb +6 -8
  14. data/app/views/tracebook/chats/index.html.erb +77 -0
  15. data/app/views/tracebook/chats/show.html.erb +94 -0
  16. data/config/routes.rb +6 -6
  17. data/db/migrate/20260325000100_create_tracebook_message_costs.rb +19 -0
  18. data/db/migrate/20260325000200_create_tracebook_chat_reviews.rb +19 -0
  19. data/db/migrate/{20241112000300_create_tracebook_pricing_rules.rb → 20260325000300_create_tracebook_pricing_rules.rb} +3 -3
  20. data/db/migrate/20260325000500_create_tracebook_comments.rb +15 -0
  21. data/lib/generators/tracebook/install/install_generator.rb +6 -9
  22. data/lib/generators/tracebook/install/templates/initializer.rb.tt +11 -5
  23. data/lib/tasks/tracebook_tasks.rake +14 -4
  24. data/lib/tracebook/adapters/ruby_llm.rb +19 -81
  25. data/lib/tracebook/adapters.rb +5 -4
  26. data/lib/tracebook/config.rb +83 -104
  27. data/lib/tracebook/engine.rb +6 -0
  28. data/lib/tracebook/errors.rb +0 -2
  29. data/lib/tracebook/pricing/calculator.rb +11 -6
  30. data/lib/tracebook/pricing.rb +0 -2
  31. data/lib/tracebook/redaction/pattern.rb +124 -0
  32. data/lib/tracebook/redaction/pipeline.rb +32 -0
  33. data/lib/tracebook/seeds/pricing_rules.rb +62 -0
  34. data/lib/tracebook/version.rb +1 -1
  35. data/lib/tracebook.rb +46 -152
  36. metadata +23 -51
  37. data/app/controllers/tracebook/exports_controller.rb +0 -25
  38. data/app/controllers/tracebook/interactions_controller.rb +0 -71
  39. data/app/helpers/tracebook/interactions_helper.rb +0 -35
  40. data/app/jobs/tracebook/daily_rollups_job.rb +0 -100
  41. data/app/jobs/tracebook/export_job.rb +0 -162
  42. data/app/jobs/tracebook/persist_interaction_job.rb +0 -160
  43. data/app/mailers/tracebook/application_mailer.rb +0 -6
  44. data/app/models/tracebook/interaction.rb +0 -103
  45. data/app/models/tracebook/redaction_rule.rb +0 -81
  46. data/app/models/tracebook/rollup_daily.rb +0 -73
  47. data/app/views/tracebook/interactions/index.html.erb +0 -108
  48. data/app/views/tracebook/interactions/show.html.erb +0 -44
  49. data/db/migrate/20241112000100_create_tracebook_interactions.rb +0 -55
  50. data/db/migrate/20241112000200_create_tracebook_rollups_dailies.rb +0 -24
  51. data/db/migrate/20241112000400_create_tracebook_redaction_rules.rb +0 -19
  52. data/lib/tracebook/adapters/active_agent.rb +0 -82
  53. data/lib/tracebook/mappers/anthropic.rb +0 -59
  54. data/lib/tracebook/mappers/base.rb +0 -38
  55. data/lib/tracebook/mappers/ollama.rb +0 -49
  56. data/lib/tracebook/mappers/openai.rb +0 -75
  57. data/lib/tracebook/mappers.rb +0 -283
  58. data/lib/tracebook/normalized_interaction.rb +0 -86
  59. data/lib/tracebook/redaction_pipeline.rb +0 -88
  60. data/lib/tracebook/redactors/base.rb +0 -29
  61. data/lib/tracebook/redactors/card_pan.rb +0 -15
  62. data/lib/tracebook/redactors/email.rb +0 -15
  63. data/lib/tracebook/redactors/phone.rb +0 -15
  64. data/lib/tracebook/redactors.rb +0 -8
  65. data/lib/tracebook/result.rb +0 -53
data/README.md CHANGED
@@ -1,45 +1,26 @@
1
- # TraceBook
1
+ # Tracebook
2
2
 
3
3
  [![Gem Version](https://img.shields.io/gem/v/tracebook.svg)](https://rubygems.org/gems/tracebook)
4
4
  [![CI](https://github.com/dpaluy/tracebook/actions/workflows/ci.yml/badge.svg)](https://github.com/dpaluy/tracebook/actions/workflows/ci.yml)
5
5
 
6
- > **Note:** This gem is in active development. APIs may change before 1.0 release.
6
+ Cost tracking and review dashboard for [RubyLLM](https://github.com/crmne/ruby_llm) conversations.
7
7
 
8
- TraceBook is a Rails engine that ingests, redacts, and reviews LLM interactions with optional encryption. It ships with a Hotwire UI, cost tracking, rollup analytics, and adapters for popular Ruby LLM libraries.
8
+ Tracebook is a Rails engine that sits on top of RubyLLM's `acts_as_chat` and `acts_as_message` models. It adds per-message cost calculation, chat-level review workflows, and a Hotwire-powered dashboard without duplicating any conversation data.
9
9
 
10
10
  ## Features
11
11
 
12
- - **Privacy-first**: Request/response payloads are redacted (PII removal) with optional encryption at rest
13
- - **Cost tracking**: Automatic token usage and cost calculation per provider/model
14
- - **Review workflow**: Approve, flag, or reject interactions with audit trail
15
- - **Hierarchical sessions**: Track parent-child relationships for agent chains
16
- - **Analytics**: Daily rollups for reporting and cost analysis
17
- - **Flexible adapters**: Built-in support for multiple providers; easy to extend
18
- - **Production-ready**: Async job processing, export to CSV/NDJSON, filterable dashboards
12
+ - **Cost tracking**: Per-message cost calculation based on configurable pricing rules
13
+ - **Review workflow**: Approve or flag entire chat conversations with comments
14
+ - **Dashboard**: Browse chats, view conversation threads, see cost/token summaries
15
+ - **RubyLLM native**: Reads directly from your Chat and Message models — no data duplication
19
16
 
20
17
  ## Requirements
21
18
 
22
- - Ruby 3.2+
19
+ - Ruby 3.4+
23
20
  - Rails 8.1+
24
- - ActiveJob backend (`:async` for development; Sidekiq/SolidQueue for production)
25
- - Database with JSONB support (PostgreSQL recommended)
26
-
27
- ## Table of Contents
28
-
29
- - [Installation](#installation--setup)
30
- - [Configuration](#configuration)
31
- - [Capturing Interactions](#capturing-interactions)
32
- - [Manual API](#manual-api)
33
- - [Built-in Adapters](#built-in-adapters)
34
- - [Creating Custom Adapters](#creating-custom-adapters)
35
- - [Creating Custom Mappers](#creating-custom-mappers)
36
- - [Cost Tracking](#cost-tracking)
37
- - [Reviewing Data](#reviewing-data)
38
- - [Production Setup](#production-setup)
39
- - [Securing the Dashboard](#securing-the-dashboard)
40
- - [Development & Testing](#development--testing)
41
-
42
- ## Installation & Setup
21
+ - [RubyLLM](https://github.com/crmne/ruby_llm) with `acts_as_chat` / `acts_as_message` models
22
+
23
+ ## Installation
43
24
 
44
25
  ```bash
45
26
  bundle add tracebook
@@ -47,798 +28,301 @@ bin/rails generate tracebook:install
47
28
  bin/rails db:migrate
48
29
  ```
49
30
 
50
- The install generator copies migrations and creates `config/initializers/tracebook.rb`.
51
-
52
- ### Mount the engine
53
-
54
- Add to `config/routes.rb`:
31
+ Mount the engine in `config/routes.rb`:
55
32
 
56
33
  ```ruby
57
- mount TraceBook::Engine => "/tracebook"
58
- ```
59
-
60
- See [Securing the Dashboard](#securing-the-dashboard) for authentication options.
61
-
62
- ### Optional: Configure encryption
63
-
64
- TraceBook supports ActiveRecord::Encryption for encrypting sensitive payload data at rest. This is **optional** but recommended for production environments handling sensitive data.
65
-
66
- **Step 1: Generate encryption keys**
67
-
68
- ```bash
69
- bin/rails db:encryption:init
70
- ```
71
-
72
- This outputs:
73
-
74
- ```yaml
75
- active_record_encryption:
76
- primary_key: [generated_key]
77
- deterministic_key: [generated_key]
78
- key_derivation_salt: [generated_salt]
34
+ mount Tracebook::Engine => "/tracebook"
79
35
  ```
80
36
 
81
- **Step 2: Add keys to credentials**
37
+ Seed pricing rules for common providers:
82
38
 
83
39
  ```bash
84
- EDITOR=vim bin/rails credentials:edit
85
- ```
86
-
87
- ```yaml
88
- # config/credentials.yml.enc
89
- active_record_encryption:
90
- primary_key: <generated_key>
91
- deterministic_key: <generated_key>
92
- key_derivation_salt: <generated_salt>
93
- ```
94
-
95
- **Step 3: Enable encryption in your app**
96
-
97
- Create an initializer to add encryption to the Interaction model:
98
-
99
- ```ruby
100
- # config/initializers/tracebook_encryption.rb
101
- Rails.application.config.after_initialize do
102
- Tracebook::Interaction.class_eval do
103
- encrypts :request_payload, :response_payload
104
- end
105
- end
40
+ bin/rails tracebook:seed_pricing
106
41
  ```
107
42
 
108
- > **Note**: Enabling encryption on an existing database requires migrating existing unencrypted data. See the [Rails encryption guide](https://guides.rubyonrails.org/active_record_encryption.html) for migration strategies.
109
-
110
43
  ## Configuration
111
44
 
112
- The install generator creates `config/initializers/tracebook.rb` with sensible defaults.
113
-
114
- Available options:
115
-
116
45
  ```ruby
117
- TraceBook.configure do |config|
118
- # Project identifier for filtering in the dashboard
119
- config.project_name = "My App"
120
-
121
- # Use async jobs for persistence (default: true)
122
- # Set to false for tests or simple setups
123
- config.persist_async = Rails.env.production?
46
+ # config/initializers/tracebook.rb
47
+ Tracebook.configure do |config|
48
+ # Class names for your RubyLLM models
49
+ config.chat_class = "Chat" # default
50
+ config.message_class = "Message" # default
124
51
 
125
- # Payload size threshold for ActiveStorage spillover (default: 64KB)
126
- config.inline_payload_bytes = 64 * 1024
52
+ # Currency for cost calculations
53
+ config.default_currency = "USD" # default
127
54
 
128
- # Auto-enable adapters on boot
129
- config.auto_subscribe_ruby_llm = true
130
- config.auto_subscribe_active_agent = true
55
+ # How to display the user in the dashboard
56
+ config.actor_display = ->(actor) { actor.try(:name) }
131
57
 
132
- # Custom PII redactors (in addition to built-in email/phone/card)
133
- config.custom_redactors += [
134
- ->(payload) { payload.gsub(/api_key=\w+/, "api_key=[REDACTED]") }
135
- ]
58
+ # Items per page
59
+ config.per_page = 25 # default
136
60
  end
137
61
  ```
138
62
 
139
- Configuration is frozen after the block runs. Call `TraceBook.reset_configuration!` in tests when you need a clean slate.
140
-
141
- ## Capturing Interactions
63
+ ## PII Redaction
142
64
 
143
- ### Manual API
65
+ Tracebook includes an opt-in PII redaction pipeline for unstructured natural language in LLM conversations. Nothing is redacted unless explicitly configured.
144
66
 
145
- Call `TraceBook.record!` anywhere you have access to an LLM request/response:
67
+ ### Enabling Patterns
146
68
 
147
69
  ```ruby
148
- TraceBook.record!(
149
- provider: "openai",
150
- model: "gpt-4o-mini",
151
- project: "support",
152
- request_payload: { messages: messages, temperature: 0.2 },
153
- response_payload: response_body,
154
- input_tokens: usage[:prompt_tokens],
155
- output_tokens: usage[:completion_tokens],
156
- latency_ms: 187,
157
- status: :success,
158
- tags: %w[triage priority],
159
- metadata: { ticket_id: ticket.id },
160
- user: current_user,
161
- session_id: session_id,
162
- parent_id: parent_interaction_id
163
- )
164
- ```
165
-
166
- **Parameters:**
167
-
168
- - **Required:**
169
- - `provider` (String) — LLM provider name (e.g., "openai", "anthropic", "ollama")
170
- - `model` (String) — Model identifier (e.g., "gpt-4o", "claude-3-5-sonnet-20241022")
171
-
172
- - **Optional:**
173
- - `project` (String) — Project/app name for filtering
174
- - `request_payload` (Hash) — Full request sent to provider
175
- - `response_payload` (Hash) — Full response from provider
176
- - `request_text` (String) — Human-readable request summary
177
- - `response_text` (String) — Human-readable response summary
178
- - `input_tokens` (Integer) — Prompt token count
179
- - `output_tokens` (Integer) — Completion token count
180
- - `latency_ms` (Integer) — Request duration in milliseconds
181
- - `status` (Symbol) — `:success`, `:error`, `:canceled`
182
- - `error_class` (String) — Exception class name on failure
183
- - `error_message` (String) — Exception message on failure
184
- - `tags` (Array<String>) — Labels for filtering (e.g., ["prod", "high-priority"])
185
- - `metadata` (Hash) — Custom metadata (e.g., `{ ticket_id: 123 }`)
186
- - `user` (ActiveRecord object) — Associated user (polymorphic)
187
- - `session_id` (String) — Session identifier for grouping related calls
188
- - `parent_id` (Integer) — Parent `Interaction` ID for hierarchical chains
189
-
190
- **Return value:**
191
-
192
- ```ruby
193
- result = TraceBook.record!(...)
194
- result.success? # => true/false
195
- result.error # => exception when persistence failed
196
- result.interaction # => AR record when persisted inline (persist_async = false)
197
- ```
198
-
199
- When `config.persist_async = true`, the interaction is enqueued via `Tracebook::PersistInteractionJob`.
200
-
201
- ### Background Jobs & Rollups
202
-
203
- **PersistInteractionJob** handles redaction, cost calculation, and writes the `Interaction` record.
204
-
205
- **DailyRollupsJob** summarizes counts, token totals, and cost into `RollupDaily` rows. Schedule it nightly per provider/model/project:
206
-
207
- ```ruby
208
- # Example: Schedule with Sidekiq Cron or whenever
209
- Tracebook::DailyRollupsJob.perform_later(
210
- date: Date.yesterday,
211
- provider: "openai",
212
- model: "gpt-4o",
213
- project: nil
214
- )
215
- ```
216
-
217
- Wrap this in your scheduler to cover all active provider/model/project combinations.
218
-
219
- **ExportJob** streams large CSV/NDJSON exports respecting your filters.
220
-
221
- ## Built-in Adapters
222
-
223
- TraceBook ships with adapters that automatically capture LLM interactions from popular libraries. Adapters normalize provider-specific responses and call `TraceBook.record!`, so you get instrumentation without modifying application code.
70
+ Tracebook.configure do |config|
71
+ # Enable individual patterns
72
+ config.redact :email, :phone, :ssn, :credit_card
224
73
 
225
- ### RubyLLM Adapter
226
-
227
- The RubyLLM adapter subscribes to `ActiveSupport::Notifications` events (default: `ruby_llm.request`).
228
-
229
- **Setup:**
230
-
231
- ```ruby
232
- # config/initializers/tracebook_adapters.rb
233
- TraceBook::Adapters::RubyLLM.enable!
74
+ # Or enable a whole group
75
+ config.redact :pii, :api_keys
76
+ end
234
77
  ```
235
78
 
236
- **Emit events from your LLM client:**
79
+ ### Available Patterns
80
+
81
+ | Pattern | Detects | Validation |
82
+ |---------|---------|------------|
83
+ | `email` | Email addresses | -- |
84
+ | `phone` | Phone numbers (US format) | -- |
85
+ | `ssn` | Social Security Numbers | SSA area-number range check |
86
+ | `credit_card` | Credit card numbers | Luhn algorithm |
87
+ | `openai_key` | OpenAI API keys (`sk-...`) | -- |
88
+ | `anthropic_key` | Anthropic API keys (`sk-ant-...`) | -- |
89
+ | `aws_key` | AWS access key IDs (`AKIA...`) | -- |
90
+ | `stripe_key` | Stripe API keys | -- |
91
+ | `github_token` | GitHub tokens (`ghp_`, `gho_`, etc.) | -- |
92
+ | `ipv4` | IPv4 addresses | Octet range 0-255 |
93
+ | `bearer_token` | Authorization bearer tokens | -- |
94
+ | `jwt` | JSON Web Tokens | -- |
95
+ | `private_key` | PEM-format private key blocks | -- |
96
+
97
+ ### Pattern Groups
98
+
99
+ | Group | Patterns included |
100
+ |-------|-------------------|
101
+ | `pii` | `email`, `phone`, `ssn` |
102
+ | `financial` | `credit_card` |
103
+ | `api_keys` | `openai_key`, `anthropic_key`, `aws_key`, `stripe_key`, `github_token` |
104
+ | `auth` | `bearer_token`, `jwt` |
105
+ | `network` | `ipv4` |
106
+ | `crypto` | `private_key` |
107
+
108
+ ### Custom Patterns
237
109
 
238
110
  ```ruby
239
- # Example: Wrapping an OpenAI client call
240
- class OpenAIService
241
- def chat_completion(messages:, model: "gpt-4o", **options)
242
- started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
243
-
244
- request = {
245
- model: model,
246
- messages: messages,
247
- **options
248
- }
249
-
250
- begin
251
- response = openai_client.chat(parameters: request)
252
- elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).to_i
253
-
254
- ActiveSupport::Notifications.instrument("ruby_llm.request", {
255
- provider: "openai",
256
- request: request,
257
- response: response,
258
- meta: {
259
- project: "support-chatbot",
260
- tags: ["customer-support", "triage"],
261
- user: current_user,
262
- session_id: session.id,
263
- latency_ms: elapsed_ms,
264
- status: :success
265
- }
266
- })
267
-
268
- response
269
- rescue => e
270
- elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).to_i
271
-
272
- ActiveSupport::Notifications.instrument("ruby_llm.request", {
273
- provider: "openai",
274
- request: request,
275
- response: nil,
276
- meta: {
277
- project: "support-chatbot",
278
- user: current_user,
279
- session_id: session.id,
280
- latency_ms: elapsed_ms,
281
- status: :error,
282
- error_class: e.class.name,
283
- error_message: e.message
284
- }
285
- })
286
-
287
- raise
288
- end
289
- end
290
-
291
- private
292
-
293
- def openai_client
294
- @openai_client ||= OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])
295
- end
111
+ Tracebook.configure do |config|
112
+ config.redact_pattern(
113
+ /policy[:\s]*\d{10}/i,
114
+ "[POLICY_NUMBER]",
115
+ name: "policy_number"
116
+ )
296
117
  end
297
118
  ```
298
119
 
299
- **Supported providers:** OpenAI, Anthropic, Ollama (built-in mappers). Other providers use the fallback mapper.
120
+ ### Custom Redactors
300
121
 
301
- **Custom event name:**
122
+ Provide any callable (proc, lambda, or object responding to `call`):
302
123
 
303
124
  ```ruby
304
- # If your library uses a different event name
305
- TraceBook::Adapters::RubyLLM.enable!(instrumentation: "my_llm.complete")
125
+ Tracebook.configure do |config|
126
+ config.custom_redactors << ->(text) {
127
+ text.gsub(/MRN-\d{8}/, "[MEDICAL_RECORD]")
128
+ }
129
+ end
306
130
  ```
307
131
 
308
- **Disabling:**
132
+ ### Using Redaction
309
133
 
310
134
  ```ruby
311
- # In test environment or when switching instrumentation
312
- TraceBook::Adapters::RubyLLM.disable!
313
- ```
314
-
315
- ### ActiveAgent Adapter
316
-
317
- For applications using ActiveAgent (agentic frameworks), enable the bus adapter:
135
+ # Redact text directly
136
+ Tracebook.redact("Email user@test.com or call 555-123-4567")
137
+ # => "Email [EMAIL] or call [PHONE]"
318
138
 
319
- ```ruby
320
- # config/initializers/tracebook_adapters.rb
321
- TraceBook::Adapters::ActiveAgent.enable!(bus: ActiveAgent::Bus)
139
+ # Use in your application before saving messages
140
+ content = Tracebook.redact(user_input)
141
+ chat.ask(content)
322
142
  ```
323
143
 
324
- The adapter automatically captures agent interactions including parent-child relationships for hierarchical agent chains.
144
+ ### Planned: LLM-Based Redaction
325
145
 
326
- **Note:** If you omit `bus:`, the adapter attempts to locate `ActiveAgent::Bus` automatically when loaded.
146
+ For context-sensitive PII that regex can't catch (e.g. "my social is seven eight two three three three two"), a future version will support LLM-based redaction using a local model (e.g., Ollama) to detect PII in natural language before persistence.
327
147
 
328
- ## Creating Custom Adapters
148
+ ## Tracebook Tables
329
149
 
330
- Adapters follow a simple pattern:
150
+ Tracebook adds four tables — all prefixed with `tracebook_` to avoid collisions:
331
151
 
332
- 1. Listen to whatever instrumentation your LLM client exposes (Notifications, middleware, observers, etc.)
333
- 2. Normalize the payload using `Tracebook::Mappers.normalize` or build a `NormalizedInteraction` manually
334
- 3. Call `TraceBook.record!(**normalized.to_h)`
152
+ | Table | Purpose |
153
+ |-------|---------|
154
+ | `tracebook_message_costs` | Cost + latency per message (polymorphic join to your Message) |
155
+ | `tracebook_chat_reviews` | Review state per chat (polymorphic join to your Chat) |
156
+ | `tracebook_comments` | Comments on chat reviews |
157
+ | `tracebook_pricing_rules` | Cost per token by provider/model |
335
158
 
336
- **Example: Custom adapter for Langchain.rb**
159
+ Your Chat and Message tables are untouched.
337
160
 
338
- ```ruby
339
- # lib/tracebook/adapters/langchain_rb.rb
340
- module Tracebook
341
- module Adapters
342
- module LangchainRb
343
- extend self
344
-
345
- def enable!
346
- return if @enabled
347
-
348
- # Hook into Langchain's middleware or callback system
349
- ::Langchain::LLM::Base.after_completion do |llm, request, response, duration|
350
- handle_completion(
351
- provider: llm.class.provider_name,
352
- request: request,
353
- response: response,
354
- duration_ms: (duration * 1000).to_i,
355
- meta: {
356
- project: "langchain-app",
357
- user: Current.user,
358
- session_id: Current.session_id
359
- }
360
- )
361
- end
362
-
363
- @enabled = true
364
- end
365
-
366
- def disable!
367
- # Unhook callback
368
- @enabled = false
369
- end
370
-
371
- private
372
-
373
- def handle_completion(provider:, request:, response:, duration_ms:, meta:)
374
- normalized = Tracebook::Mappers.normalize(
375
- provider,
376
- raw_request: request,
377
- raw_response: response,
378
- meta: meta.merge(latency_ms: duration_ms)
379
- )
380
-
381
- TraceBook.record!(**normalized.to_h)
382
- rescue => error
383
- Rails.logger.error("TraceBook LangchainRb adapter error: #{error.message}")
384
- end
385
- end
386
- end
387
- end
161
+ ## Cost Calculation
388
162
 
389
- TraceBook = Tracebook unless defined?(TraceBook)
390
- ```
391
-
392
- **Enable your adapter:**
163
+ After an LLM response, call `Tracebook.calculate_cost!` to record the cost:
393
164
 
394
165
  ```ruby
395
- # config/initializers/tracebook_adapters.rb
396
- require "tracebook/adapters/langchain_rb"
397
- Tracebook::Adapters::LangchainRb.enable!
166
+ Tracebook.calculate_cost!(
167
+ message,
168
+ provider: "openai",
169
+ model: "gpt-4o",
170
+ latency_ms: elapsed_ms
171
+ )
398
172
  ```
399
173
 
400
- ## Creating Custom Mappers
174
+ This looks up the matching pricing rule, calculates input/output costs, and creates a `tracebook_message_costs` record joined to the message.
401
175
 
402
- Mappers normalize provider-specific request/response formats into TraceBook's standard schema. Create a custom mapper when the built-in ones (OpenAI, Anthropic, Ollama) don't match your provider's format.
176
+ ### Integration Example
403
177
 
404
- **Example: Custom mapper for Cohere**
178
+ In a typical RubyLLM app, hook into the chat response flow:
405
179
 
406
180
  ```ruby
407
- # lib/tracebook/mappers/cohere.rb
408
- module Tracebook
409
- module Mappers
410
- class Cohere < Base
411
- def self.normalize(raw_request:, raw_response:, meta: {})
412
- new.normalize(
413
- raw_request: raw_request,
414
- raw_response: raw_response,
415
- meta: meta
416
- )
417
- end
418
-
419
- def normalize(raw_request:, raw_response:, meta: {})
420
- request = symbolize(raw_request || {})
421
- response = symbolize(raw_response || {})
422
- meta_info = indifferent_meta(meta)
423
-
424
- build_interaction(
425
- provider: "cohere",
426
- model: request[:model] || response[:model],
427
- project: meta_info[:project],
428
- request_payload: raw_request,
429
- response_payload: raw_response,
430
- request_text: request[:message] || request[:prompt],
431
- response_text: extract_response_text(response),
432
- input_tokens: extract_token_count(response, :prompt_tokens),
433
- output_tokens: extract_token_count(response, :completion_tokens),
434
- latency_ms: meta_info[:latency_ms],
435
- status: meta_info[:status]&.to_sym || :success,
436
- error_class: nil,
437
- error_message: nil,
438
- tags: Array(meta_info[:tags]).compact,
439
- metadata: extract_metadata(response),
440
- user: meta_info[:user],
441
- parent_id: meta_info[:parent_id],
442
- session_id: meta_info[:session_id]
443
- )
444
- end
445
-
446
- private
447
-
448
- def extract_response_text(response)
449
- response[:text] || response.dig(:generations, 0, :text)
450
- end
451
-
452
- def extract_token_count(response, key)
453
- response.dig(:meta, :billed_units, key)&.to_i
454
- end
455
-
456
- def extract_metadata(response)
457
- metadata = {}
458
- metadata["generation_id"] = response[:generation_id] if response[:generation_id]
459
- metadata["finish_reason"] = response[:finish_reason] if response[:finish_reason]
460
- compact_hash(metadata)
461
- end
462
- end
463
- end
464
- end
181
+ class ChatResponseJob < ApplicationJob
182
+ def perform(chat_id, content)
183
+ chat = Chat.find(chat_id)
465
184
 
466
- TraceBook = Tracebook unless defined?(TraceBook)
467
- ```
185
+ chat.ask(content) do |chunk|
186
+ # stream chunks...
187
+ end
468
188
 
469
- **Register your mapper:**
189
+ # After response, calculate cost for the last assistant message
190
+ message = chat.messages.where(role: "assistant").last
191
+ model = chat.model
470
192
 
471
- ```ruby
472
- # lib/tracebook/mappers.rb
473
- require_relative "mappers/cohere"
474
-
475
- module Tracebook
476
- module Mappers
477
- def normalize(provider, raw_request:, raw_response:, meta: {})
478
- case provider.to_s
479
- when "openai"
480
- normalize_openai(raw_request, raw_response, meta)
481
- when "anthropic"
482
- normalize_anthropic(raw_request, raw_response, meta)
483
- when "ollama"
484
- normalize_ollama(raw_request, raw_response, meta)
485
- when "cohere"
486
- Mappers::Cohere.normalize(
487
- raw_request: raw_request,
488
- raw_response: raw_response,
489
- meta: meta
490
- )
491
- else
492
- fallback_normalized(provider, raw_request, raw_response, meta)
493
- end
494
- end
193
+ Tracebook.calculate_cost!(
194
+ message,
195
+ provider: model.provider,
196
+ model: model.model_id
197
+ )
495
198
  end
496
199
  end
497
200
  ```
498
201
 
499
- **Mapper requirements:**
500
-
501
- - Inherit from `Tracebook::Mappers::Base`
502
- - Implement `.normalize(raw_request:, raw_response:, meta:)`
503
- - Return a `Tracebook::NormalizedInteraction` instance
504
- - Handle missing fields gracefully (return `nil` for unavailable data)
505
- - Extract token counts if available, otherwise leave as `nil`
202
+ ## Pricing Rules
506
203
 
507
- ## Cost Tracking
204
+ Tracebook calculates costs using `PricingRule` records. Seed defaults for common providers:
508
205
 
509
- TraceBook automatically calculates costs based on `PricingRule` records. Create pricing rules for your providers/models:
510
-
511
- ```ruby
512
- # db/seeds.rb or a migration
513
-
514
- # OpenAI pricing (as of 2024)
515
- TraceBook::PricingRule.create!(
516
- provider: "openai",
517
- model_pattern: "gpt-4o",
518
- input_per_1k: 2.50,
519
- output_per_1k: 10.00,
520
- currency: "USD",
521
- effective_from: Date.new(2024, 8, 6)
522
- )
523
-
524
- TraceBook::PricingRule.create!(
525
- provider: "openai",
526
- model_pattern: "gpt-4o-mini",
527
- input_per_1k: 0.150,
528
- output_per_1k: 0.600,
529
- currency: "USD",
530
- effective_from: Date.new(2024, 7, 18)
531
- )
532
-
533
- TraceBook::PricingRule.create!(
534
- provider: "openai",
535
- model_pattern: "o1",
536
- input_per_1k: 15.00,
537
- output_per_1k: 60.00,
538
- currency: "USD",
539
- effective_from: Date.new(2024, 12, 17)
540
- )
541
-
542
- # Anthropic pricing
543
- TraceBook::PricingRule.create!(
544
- provider: "anthropic",
545
- model_pattern: "claude-3-5-sonnet-*",
546
- input_per_1k: 3.00,
547
- output_per_1k: 15.00,
548
- currency: "USD",
549
- effective_from: Date.new(2024, 10, 22)
550
- )
551
-
552
- TraceBook::PricingRule.create!(
553
- provider: "anthropic",
554
- model_pattern: "claude-3-5-haiku-*",
555
- input_per_1k: 1.00,
556
- output_per_1k: 5.00,
557
- currency: "USD",
558
- effective_from: Date.new(2024, 11, 1)
559
- )
560
-
561
- # Ollama (free/local)
562
- TraceBook::PricingRule.create!(
563
- provider: "ollama",
564
- model_pattern: "*",
565
- input_per_1k: 0.0,
566
- output_per_1k: 0.0,
567
- currency: "USD",
568
- effective_from: Date.new(2024, 1, 1)
569
- )
206
+ ```bash
207
+ bin/rails tracebook:seed_pricing
570
208
  ```
571
209
 
572
- **Glob patterns:**
573
-
574
- - `gpt-4o` — Exact match
575
- - `gpt-4o*` — Matches `gpt-4o`, `gpt-4o-mini`, `gpt-4o-2024-08-06`
576
- - `claude-3-5-*` — Matches all Claude 3.5 models
577
- - `*` — Matches everything (fallback rule)
578
-
579
- TraceBook uses the most specific matching rule. If multiple rules match, it prefers the most recently effective one.
580
-
581
- ## Reviewing Data
582
-
583
- ### Dashboard UI
584
-
585
- Visit the mount path (`/tracebook` by default) to access the dashboard.
586
-
587
- **Index screen:**
588
-
589
- - **Filters**: Provider, model, project, status, review state, tags, date range
590
- - **KPI tiles**: Total calls, tokens used, total cost, error rate, avg latency
591
- - **Interaction table**: Columns include:
592
- - Timestamp
593
- - Label (first 100 chars of request)
594
- - User
595
- - Provider/Model
596
- - Tokens (input/output)
597
- - Cost
598
- - Duration (ms)
599
- - Review state
600
- - Actions (Approve/Flag/Reject, detail link)
601
-
602
- **Detail screen:**
603
-
604
- - **Header**: ID, label, user, timestamp, review state dropdown + comment form
605
- - **Metrics panel**: Model, duration, token breakdown, cost, HTTP status
606
- - **Collapsible sections**:
607
- - Input (messages)
608
- - Output (text + tool calls)
609
- - Full JSON (request/response payloads)
610
- - Error (if failed)
611
- - **Sidebar**: Parent/child links, tags, session breadcrumb
210
+ This creates rules for OpenAI, Anthropic, Gemini, and Ollama models.
612
211
 
613
- **Keyboard shortcuts:**
614
-
615
- - `j/k` — Navigate rows
616
- - `a` — Approve selected
617
- - `f` — Flag selected
618
- - `r` — Reject selected
619
- - `?` — Show help
620
-
621
- **Bulk review:**
622
-
623
- Select multiple interactions using checkboxes, then apply a review state to all at once.
624
-
625
- ### Review Workflow
626
-
627
- Interactions start in `unreviewed` state. Reviewers can transition to:
628
-
629
- - **`approved`** — Interaction is acceptable; no issues found
630
- - **`flagged`** — Interaction requires attention (e.g., sensitive data, unexpected behavior)
631
- - **`rejected`** — Interaction is problematic and should not have occurred
632
-
633
- Only `admin` users (as defined in your `authorize` proc) can change review states.
634
-
635
- ## Production Setup
636
-
637
- ### Securing the Dashboard
638
-
639
- The dashboard should only be accessible to trusted reviewers. Here are common approaches:
640
-
641
- **Devise with admin check:**
212
+ ### Adding Custom Rules
642
213
 
643
214
  ```ruby
644
- # config/routes.rb
645
- authenticate :user, ->(u) { u.admin? } do
646
- mount TraceBook::Engine => "/tracebook"
647
- end
215
+ Tracebook::PricingRule.create!(
216
+ provider: "xai",
217
+ model_glob: "grok-4-1-fast*",
218
+ input_cents_per_unit: 20, # per 1k tokens
219
+ output_cents_per_unit: 50,
220
+ effective_from: Date.new(2025, 7, 1),
221
+ currency: "USD"
222
+ )
648
223
  ```
649
224
 
650
- **Session-based constraint:**
225
+ ### Glob Patterns
651
226
 
652
- ```ruby
653
- # config/routes.rb
654
- constraints ->(req) { req.session[:admin] } do
655
- mount TraceBook::Engine => "/tracebook"
656
- end
657
- ```
227
+ - `gpt-4o` — exact match
228
+ - `gpt-4o*` — matches `gpt-4o`, `gpt-4o-mini`, `gpt-4o-2024-08-06`
229
+ - `claude-3-5-*` matches all Claude 3.5 models
230
+ - `*` fallback for any model
658
231
 
659
- **HTTP Basic Auth (simple setups):**
232
+ When multiple rules match, Tracebook prefers the most specific glob (most literal characters), then the most recent `effective_from` date.
660
233
 
661
- ```ruby
662
- # config/routes.rb
663
- TraceBook::Engine.middleware.use Rack::Auth::Basic do |username, password|
664
- ActiveSupport::SecurityUtils.secure_compare(username, ENV["TRACEBOOK_USER"]) &
665
- ActiveSupport::SecurityUtils.secure_compare(password, ENV["TRACEBOOK_PASSWORD"])
666
- end
234
+ ## Review Workflow
667
235
 
668
- mount TraceBook::Engine => "/tracebook"
669
- ```
236
+ Reviews happen at the chat level, not per-message. In the dashboard:
670
237
 
671
- ### Queue Adapter
238
+ 1. Open a chat to see the full conversation thread
239
+ 2. Click **Approve** or **Flag**
240
+ 3. Add comments for context
672
241
 
673
- Configure ActiveJob to use a production queue backend:
242
+ Programmatic access:
674
243
 
675
244
  ```ruby
676
- # config/environments/production.rb
677
- config.active_job.queue_adapter = :sidekiq # or :solid_queue, etc.
678
- ```
245
+ chat = Chat.find(id)
246
+ review = Tracebook::ChatReview.for_chat(chat)
679
247
 
680
- ### Encryption Keys (if enabled)
681
-
682
- If you've enabled payload encryption (see [Configure encryption](#optional-configure-encryption)), store keys securely:
683
-
684
- - Use Rails encrypted credentials (`bin/rails credentials:edit`)
685
- - Or environment variables with a secrets manager (AWS Secrets Manager, HashiCorp Vault)
686
-
687
- ### Scheduling Rollup Jobs
688
-
689
- Use a scheduler to run `DailyRollupsJob` nightly:
690
-
691
- **Sidekiq Cron:**
692
-
693
- ```ruby
694
- # config/initializers/sidekiq_cron.rb
695
- Sidekiq::Cron::Job.create(
696
- name: "TraceBook daily rollups - OpenAI",
697
- cron: "0 2 * * *", # 2am daily
698
- class: "Tracebook::DailyRollupsJob",
699
- kwargs: { date: Date.yesterday, provider: "openai", model: nil, project: nil }
248
+ review.update!(
249
+ review_state: :approved,
250
+ reviewed_by: "admin@example.com"
700
251
  )
252
+
253
+ review.comments.create!(author: "admin", body: "Looks good")
701
254
  ```
702
255
 
703
- **Whenever:**
256
+ ### Review States
704
257
 
705
- ```ruby
706
- # config/schedule.rb
707
- every 1.day, at: "2:00 am" do
708
- runner "Tracebook::DailyRollupsJob.perform_later(date: Date.yesterday, provider: 'openai', model: nil, project: nil)"
709
- end
710
- ```
258
+ | State | Meaning |
259
+ |-------|---------|
260
+ | `pending` | Not yet reviewed (default) |
261
+ | `approved` | Reviewed and accepted |
262
+ | `flagged` | Needs attention |
711
263
 
712
- ### Monitoring
264
+ ## Dashboard
713
265
 
714
- Add error tracking to catch adapter failures:
266
+ The dashboard is available at `/tracebook/chats` (or wherever you mount the engine).
715
267
 
716
- ```ruby
717
- # config/initializers/tracebook.rb
718
- TraceBook.configure do |config|
719
- # Existing config...
268
+ ### Chat List (`/tracebook/chats`)
269
+ - All chats with actor, model, message count, token usage, cost, review state
270
+ - KPIs: total chats, messages, cost
720
271
 
721
- # Hook into error logging
722
- config.on_error = ->(error, context) do
723
- Sentry.capture_exception(error, extra: context) if defined?(Sentry)
724
- Rails.logger.error("TraceBook error: #{error.message} - #{context.inspect}")
725
- end
726
- end
727
- ```
272
+ ### Chat Detail (`/tracebook/chats/:id`)
273
+ - Full conversation thread (user and assistant messages)
274
+ - Per-message token counts and costs
275
+ - Review controls (approve/flag/reset)
276
+ - Comment thread
728
277
 
729
- ### Database Indexes
278
+ ### Actor Display
730
279
 
731
- TraceBook migrations include indexes for common queries. If you add custom filters, consider additional indexes:
280
+ By default, actors are shown as `Name` or `ClassName#id`. Customize with:
732
281
 
733
282
  ```ruby
734
- # db/migrate/xxx_add_custom_tracebook_indexes.rb
735
- class AddCustomTracebookIndexes < ActiveRecord::Migration[7.1]
736
- def change
737
- add_index :tracebook_interactions, [:project, :occurred_at], name: "idx_tracebook_project_time"
738
- add_index :tracebook_interactions, :tags, using: :gin, name: "idx_tracebook_tags"
283
+ config.actor_display = ->(actor) {
284
+ case actor
285
+ when User then actor.email
286
+ else "#{actor.class}##{actor.id}"
739
287
  end
740
- end
288
+ }
741
289
  ```
742
290
 
743
- ### Data Retention
291
+ ## Securing the Dashboard
744
292
 
745
- Consider archiving or deleting old interactions to manage database size:
293
+ The engine inherits from `ActionController::Base`. Restrict access with route constraints:
746
294
 
747
295
  ```ruby
748
- # app/jobs/archive_old_interactions_job.rb
749
- class ArchiveOldInteractionsJob < ApplicationJob
750
- def perform
751
- cutoff = 90.days.ago
752
-
753
- # Option 1: Delete
754
- TraceBook::Interaction.where("occurred_at < ?", cutoff).delete_all
755
-
756
- # Option 2: Export to S3 before deleting
757
- interactions = TraceBook::Interaction.where("occurred_at < ?", cutoff)
758
- S3Archiver.archive(interactions)
759
- interactions.delete_all
760
- end
296
+ # HTTP Basic Auth
297
+ mount Tracebook::Engine => "/tracebook",
298
+ constraints: ->(req) {
299
+ Rack::Auth::Basic::Request.new(req.env).provided? &&
300
+ Rack::Auth::Basic::Request.new(req.env).credentials == ["admin", ENV["TRACEBOOK_PASSWORD"]]
301
+ }
302
+
303
+ # Devise
304
+ authenticate :user, ->(u) { u.admin? } do
305
+ mount Tracebook::Engine => "/tracebook"
761
306
  end
762
307
  ```
763
308
 
764
309
  ## Development & Testing
765
310
 
766
- ### Inside the engine repository
767
-
768
311
  ```bash
769
- cd tracebook/
770
- bundle install
771
- bundle exec rails db:migrate # Run dummy app migrations
772
- bundle exec rake test # Run full test suite
773
- bundle exec rubocop --fix-unsafe # Fix style issues
774
- ```
312
+ # Run tests
313
+ bin/rails test
775
314
 
776
- ### Inside a host application
777
-
778
- After updating the gem, install any new migrations:
779
-
780
- ```bash
781
- bin/rails tracebook:install:migrations
782
- bin/rails db:migrate
315
+ # Seed pricing in development
316
+ bin/rails tracebook:seed_pricing
783
317
  ```
784
318
 
785
- ### Testing with adapters disabled
319
+ ### Reset configuration in tests
786
320
 
787
321
  ```ruby
788
- # test/test_helper.rb
789
- class ActiveSupport::TestCase
790
- setup do
791
- TraceBook::Adapters::RubyLLM.disable!
792
- TraceBook.reset_configuration!
793
-
794
- TraceBook.configure do |config|
795
- config.authorize = ->(*) { true }
796
- config.persist_async = false # Inline for tests
797
- end
798
- end
799
- end
322
+ setup { Tracebook.reset_configuration! }
323
+ teardown { Tracebook.reset_configuration! }
800
324
  ```
801
325
 
802
- ## API Documentation
803
-
804
- TraceBook uses [YARD](https://yardoc.org/) for API documentation. The full API docs are available at [rubydoc.info/gems/tracebook](https://rubydoc.info/gems/tracebook).
805
-
806
- ### Generating Documentation Locally
807
-
808
- ```bash
809
- # Install YARD
810
- bundle install
811
-
812
- # Generate documentation
813
- bundle exec rake yard
814
-
815
- # Generate and open in browser
816
- bundle exec rake yard:open
817
-
818
- # View documentation coverage stats
819
- bundle exec rake yard:stats
820
- ```
821
-
822
- Documentation is generated in the `doc/` directory. Open `doc/index.html` in your browser to view.
823
-
824
- ### Key Documentation Areas
825
-
826
- - **{Tracebook}** - Main module and `record!` method
827
- - **{Tracebook::Mappers}** - Provider normalization
828
- - **{Tracebook::Adapters::RubyLLM}** - ActiveSupport::Notifications adapter
829
- - **{Tracebook::Interaction}** - ActiveRecord model
830
- - **{Tracebook::NormalizedInteraction}** - Standard data structure
831
- - **{Tracebook::Result}** - Return value from `record!`
832
-
833
- ## Contributing
834
-
835
- 1. Fork the repo and create a topic branch
836
- 2. Ensure `bundle exec rake test` passes
837
- 3. Update documentation and add regression tests for new behavior
838
- 4. Run `bundle exec rubocop -A` to fix style issues
839
- 5. Add YARD documentation for new public methods
840
- 6. Open a PR describing the motivation and changes
841
-
842
326
  ## License
843
327
 
844
- TraceBook is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
328
+ MIT License. See [MIT-LICENSE](MIT-LICENSE).