tracebook 0.1.1 → 1.0.1

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 +56 -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 +8 -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
@@ -1,108 +0,0 @@
1
- <div class="tb-container">
2
- <header class="tb-header">
3
- <h1>TraceBook Interactions</h1>
4
- <div class="tb-kpis">
5
- <div class="tb-kpi">
6
- <span>Total</span>
7
- <strong><%= @kpis[:total] %></strong>
8
- </div>
9
- <div class="tb-kpi">
10
- <span>Success</span>
11
- <strong><%= @kpis[:success] %></strong>
12
- </div>
13
- <div class="tb-kpi">
14
- <span>Total Tokens</span>
15
- <strong><%= @kpis[:input_tokens] + @kpis[:output_tokens] %></strong>
16
- </div>
17
- <div class="tb-kpi">
18
- <span>Cost (¢)</span>
19
- <strong><%= @kpis[:cost_cents] %></strong>
20
- </div>
21
- </div>
22
- </header>
23
-
24
- <section class="tb-filters">
25
- <%= form_with url: interactions_path, method: :get, scope: :filters, data: { turbo_frame: "interactions_table" }, class: "tb-filter-form" do |form| %>
26
- <div class="tb-filter-grid">
27
- <div>
28
- <%= form.label :provider %>
29
- <%= form.select :provider, options_for_select(@providers, @filters[:provider]), include_blank: "All" %>
30
- </div>
31
- <div>
32
- <%= form.label :model %>
33
- <%= form.select :model, options_for_select(@models, @filters[:model]), include_blank: "All" %>
34
- </div>
35
- <div>
36
- <%= form.label :project %>
37
- <%= form.select :project, options_for_select(@projects, @filters[:project]), include_blank: "All" %>
38
- </div>
39
- <div>
40
- <%= form.label :status %>
41
- <%= form.select :status, options_for_select(TraceBook::Interaction.statuses.keys, @filters[:status]), include_blank: "All" %>
42
- </div>
43
- <div>
44
- <%= form.label :review_state %>
45
- <%= form.select :review_state, options_for_select(TraceBook::Interaction.review_states.keys, @filters[:review_state]), include_blank: "All" %>
46
- </div>
47
- <div>
48
- <%= form.label :tag %>
49
- <%= form.text_field :tag, value: @filters[:tag] %>
50
- </div>
51
- <div>
52
- <%= form.label :from, "From (date)" %>
53
- <%= form.date_field :from, value: @filters[:from] %>
54
- </div>
55
- <div>
56
- <%= form.label :to, "To (date)" %>
57
- <%= form.date_field :to, value: @filters[:to] %>
58
- </div>
59
- </div>
60
- <div class="tb-filter-actions">
61
- <%= form.submit "Apply" %>
62
- <%= link_to "Reset", interactions_path, class: "tb-link" %>
63
- </div>
64
- <% end %>
65
- </section>
66
-
67
- <section class="tb-table-wrapper">
68
- <%= form_with url: bulk_review_interactions_path, method: :post, data: { controller: "keyboard" }, html: { tabindex: 0 } do %>
69
- <%= hidden_field_tag :review_state, "approved", data: { keyboard_target: "reviewState" } %>
70
- <div class="tb-table-actions">
71
- <button type="submit">Bulk Approve</button>
72
- <button type="submit" formaction="<%= exports_path(format: :csv, filters: @filters.to_h) %>" formmethod="post">Export CSV</button>
73
- </div>
74
-
75
- <turbo-frame id="interactions_table">
76
- <table class="tb-table" data-keyboard-target="table">
77
- <thead>
78
- <tr>
79
- <th><input type="checkbox" data-keyboard-target="toggleAll"></th>
80
- <th>Timestamp</th>
81
- <th>Provider</th>
82
- <th>Model</th>
83
- <th>Status</th>
84
- <th>Tokens</th>
85
- <th>Cost (¢)</th>
86
- </tr>
87
- </thead>
88
- <tbody>
89
- <% @interactions.each do |interaction| %>
90
- <tr data-controller="row" data-keyboard-target="row">
91
- <td><%= check_box_tag "interaction_ids[]", interaction.id, false, data: { keyboard_target: "checkbox" } %></td>
92
- <td><%= link_to interaction.created_at.strftime("%Y-%m-%d %H:%M"), interaction_path(interaction) %></td>
93
- <td><%= interaction.provider %></td>
94
- <td><%= interaction.model %></td>
95
- <td><%= interaction.status %></td>
96
- <td><%= interaction.total_tokens %></td>
97
- <td><%= interaction.cost_total_cents %></td>
98
- </tr>
99
- <% end %>
100
- </tbody>
101
- </table>
102
- <nav class="tb-pagination" aria-label="Interactions pagination">
103
- <%== @pagy.series_nav(anchor_string: 'data-turbo-frame="interactions_table"') %>
104
- </nav>
105
- </turbo-frame>
106
- <% end %>
107
- </section>
108
- </div>
@@ -1,44 +0,0 @@
1
- <div class="tb-container">
2
- <nav class="tb-breadcrumbs">
3
- <%= link_to "← Back", interactions_path %>
4
- </nav>
5
-
6
- <header class="tb-header">
7
- <h1>Interaction <%= @interaction.id %></h1>
8
- <div class="tb-meta-grid">
9
- <div><span>Provider</span><strong><%= @interaction.provider %></strong></div>
10
- <div><span>Model</span><strong><%= @interaction.model %></strong></div>
11
- <div><span>Status</span><strong><%= @interaction.status %></strong></div>
12
- <div><span>Review</span><strong><%= @interaction.review_state %></strong></div>
13
- <div><span>Tokens</span><strong><%= @interaction.total_tokens %></strong></div>
14
- <div><span>Cost (¢)</span><strong><%= @interaction.cost_total_cents %></strong></div>
15
- </div>
16
- </header>
17
-
18
- <% request_payload = payload_for(@interaction, :request) %>
19
- <section class="tb-section" data-controller="json-viewer" data-json-viewer-collapsed-value="true">
20
- <h2>Request</h2>
21
- <pre data-json-viewer-target="content"><%= formatted_payload(request_payload, @interaction.request_text) %></pre>
22
- </section>
23
-
24
- <% response_payload = payload_for(@interaction, :response) %>
25
- <section class="tb-section" data-controller="json-viewer">
26
- <h2>Response</h2>
27
- <pre data-json-viewer-target="content"><%= formatted_payload(response_payload, @interaction.response_text) %></pre>
28
- </section>
29
-
30
- <section class="tb-section">
31
- <h2>Metadata</h2>
32
- <pre><%= formatted_payload(@interaction.metadata, "{}") %></pre>
33
- </section>
34
-
35
- <section class="tb-section">
36
- <%= form_with url: review_interaction_path(@interaction), method: :post, class: "tb-review-form" do %>
37
- <div>
38
- <%= label_tag :review_state, "Review State" %>
39
- <%= select_tag :review_state, options_for_select(TraceBook::Interaction.review_states.keys, @interaction.review_state) %>
40
- </div>
41
- <%= submit_tag "Update Review" %>
42
- <% end %>
43
- </section>
44
- </div>
@@ -1,55 +0,0 @@
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
@@ -1,24 +0,0 @@
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
@@ -1,19 +0,0 @@
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
@@ -1,82 +0,0 @@
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)
@@ -1,59 +0,0 @@
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)
@@ -1,38 +0,0 @@
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)
@@ -1,49 +0,0 @@
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)
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "base"
4
-
5
- module Tracebook
6
- module Mappers
7
- class OpenAI < Base
8
- def normalize(raw_request:, raw_response:, meta: {})
9
- request = symbolize(raw_request || {})
10
- response = symbolize(raw_response || {})
11
- metadata = build_metadata(response)
12
- meta_info = indifferent_meta(meta)
13
-
14
- build_interaction(
15
- provider: "openai",
16
- model: request[:model] || response[:model],
17
- project: meta_info[:project],
18
- request_payload: raw_request,
19
- response_payload: raw_response,
20
- request_text: join_messages(request[:messages]),
21
- response_text: first_choice_text(response),
22
- input_tokens: usage_tokens(response, :prompt_tokens),
23
- output_tokens: usage_tokens(response, :completion_tokens),
24
- latency_ms: meta_info[:latency_ms],
25
- status: meta_info[:status]&.to_sym || default_status(response),
26
- error_class: nil,
27
- error_message: nil,
28
- tags: Array(meta_info[:tags]).compact,
29
- metadata: metadata,
30
- user: meta_info[:user],
31
- parent_id: meta_info[:parent_id],
32
- session_id: meta_info[:session_id]
33
- )
34
- end
35
-
36
- private
37
-
38
- def join_messages(messages)
39
- Array(messages).map { |message| message.with_indifferent_access[:content].to_s }.reject(&:empty?).join("\n\n")
40
- end
41
-
42
- def first_choice(response)
43
- Array(response[:choices]).first || {}
44
- end
45
-
46
- def first_choice_text(response)
47
- choice = first_choice(response)
48
- message = choice[:message] || {}
49
- message.with_indifferent_access[:content].to_s
50
- end
51
-
52
- def usage_tokens(response, key)
53
- usage = response[:usage] || {}
54
- usage.with_indifferent_access[key]&.to_i
55
- end
56
-
57
- def build_metadata(response)
58
- choice = first_choice(response)
59
- metadata = {}
60
- metadata["finish_reason"] = choice[:finish_reason] if choice[:finish_reason]
61
- metadata
62
- end
63
-
64
- def default_status(response)
65
- finish_reason = first_choice(response)[:finish_reason]
66
- return :canceled if finish_reason == "length"
67
- return :error if finish_reason == "error"
68
-
69
- :success
70
- end
71
- end
72
- end
73
- end
74
-
75
- TraceBook = Tracebook unless defined?(TraceBook)