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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Tracebook
2
- VERSION = "0.1.1"
2
+ VERSION = "1.0.0"
3
3
  end
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/redactors"
6
- require "tracebook/mappers"
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 capturing, storing, and reviewing LLM interactions.
12
+ # TraceBook is a Rails engine for cost tracking and review of LLM conversations.
15
13
  #
16
- # It provides:
17
- # - Automatic redaction of PII from request/response payloads
18
- # - Encrypted storage of sensitive data using ActiveRecord::Encryption
19
- # - Cost tracking based on token usage and configurable pricing rules
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 Basic configuration
26
- # TraceBook.configure do |config|
27
- # config.authorize = ->(user, action, resource) { user&.admin? }
28
- # config.project_name = "My App"
29
- # config.persist_async = Rails.env.production?
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 Recording an interaction
33
- # TraceBook.record!(
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
- # Records an LLM interaction.
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 attributes [Hash] Interaction attributes
102
- # @option attributes [String] :provider Provider name (e.g., "openai", "anthropic") **required**
103
- # @option attributes [String] :model Model identifier (e.g., "gpt-4o", "claude-3-5-sonnet") **required**
104
- # @option attributes [String, nil] :project Project name for filtering
105
- # @option attributes [Hash, nil] :request_payload Full request sent to provider (will be encrypted)
106
- # @option attributes [Hash, nil] :response_payload Full response from provider (will be encrypted)
107
- # @option attributes [String, nil] :request_text Human-readable request summary
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
- # @example Recording a failed request
139
- # TraceBook.record!(
140
- # provider: "anthropic",
141
- # model: "claude-3-5-sonnet",
142
- # request_payload: request,
143
- # response_payload: nil,
144
- # status: :error,
145
- # error_class: "Faraday::TimeoutError",
146
- # error_message: "Request timed out after 30s",
147
- # latency_ms: 30000
148
- # )
149
- def record!(**attributes)
150
- payload = build_normalized_interaction(attributes)
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
- if config.persist_async
154
- PersistInteractionJob.perform_later(payload.to_h)
155
- result
156
- else
157
- interaction = PersistInteractionJob.perform_now(payload.to_h)
158
- Result.new(interaction: interaction, idempotency_key: attributes[:idempotency_key])
159
- end
160
- rescue StandardError => error
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, "TraceBook configuration is already finalized"
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.1.1
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: TraceBook provides a Rails engine for capturing, storing, and reviewing
83
- LLM API interactions with built-in support for OpenAI, Anthropic, and Ollama. Features
84
- include PII redaction, cost tracking, review workflows, and export capabilities.
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/exports_controller.rb
104
- - app/controllers/tracebook/interactions_controller.rb
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/interactions_helper.rb
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/interaction.rb
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/interactions/index.html.erb
120
- - app/views/tracebook/interactions/show.html.erb
102
+ - app/views/tracebook/chats/index.html.erb
103
+ - app/views/tracebook/chats/show.html.erb
121
104
  - config/routes.rb
122
- - db/migrate/20241112000100_create_tracebook_interactions.rb
123
- - db/migrate/20241112000200_create_tracebook_rollups_dailies.rb
124
- - db/migrate/20241112000300_create_tracebook_pricing_rules.rb
125
- - db/migrate/20241112000400_create_tracebook_redaction_rules.rb
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/redaction_pipeline.rb
147
- - lib/tracebook/redactors.rb
148
- - lib/tracebook/redactors/base.rb
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/main/CHANGELOG.md
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.2.0
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: Rails engine for LLM interaction telemetry and review.
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