This adds a `captain_sessions` table to log every Captain run, starting with Assistant Responses and Copilot Responses. Each session records the assistant, model, credits consumed, the FAQs/documents/scenario that contributed to the response, and the full run context — giving customers visibility into how a response was generated and giving us durable stats on credit, FAQ, and document usage (which today only exist as ephemeral trace metadata and an aggregate account counter). ## What changed - New `Captain::Session` model with a `session_type` enum (`assistant`, `copilot`). The subject (`Conversation` / `CopilotThread`) and result (`Message` / `CopilotMessage`) classes are inferred from the session type, so the table stores plain `subject_id` / `result_id` ids. `result_id` is nullable so failed runs that still consumed credits can be logged. - Composite indexes on `[session_type, subject_id]`, `[session_type, result_id]`, and `[account_id, session_type, created_at]` for lookup and usage-stats queries. - Factory and model specs. This PR is schema + model only; the writer/instrumentation that records sessions from the assistant and copilot flows will follow. --------- Co-authored-by: Sony Mathew <sony@chatwoot.com>
87 lines
3.0 KiB
Ruby
87 lines
3.0 KiB
Ruby
# == Schema Information
|
|
#
|
|
# Table name: agent_sessions
|
|
#
|
|
# id :bigint not null, primary key
|
|
# credits_consumed :float
|
|
# document_ids :jsonb
|
|
# faq_ids :jsonb
|
|
# llm_model :string
|
|
# result_type :string
|
|
# run_context :jsonb
|
|
# scenario_ids :jsonb
|
|
# session_type :integer not null
|
|
# subject_type :string not null
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# account_id :bigint not null
|
|
# assistant_id :bigint not null
|
|
# result_id :bigint
|
|
# subject_id :bigint not null
|
|
# user_id :bigint
|
|
#
|
|
# Indexes
|
|
#
|
|
# idx_on_account_id_result_type_result_id_ca66c00cd7 (account_id,result_type,result_id)
|
|
# idx_on_account_id_session_type_created_at_c20a14bd4e (account_id,session_type,created_at)
|
|
# idx_on_account_id_subject_type_subject_id_6d60963b3d (account_id,subject_type,subject_id)
|
|
# index_agent_sessions_on_account_id (account_id)
|
|
# index_agent_sessions_on_assistant_id (assistant_id)
|
|
# index_agent_sessions_on_user_id (user_id)
|
|
#
|
|
class Captain::AgentSession < ApplicationRecord
|
|
self.table_name = 'agent_sessions'
|
|
|
|
SUBJECT_TYPES = { 'assistant' => 'Conversation', 'copilot' => 'CopilotThread' }.freeze
|
|
RESULT_TYPES = { 'assistant' => 'Message', 'copilot' => 'CopilotMessage' }.freeze
|
|
|
|
belongs_to :account
|
|
belongs_to :assistant, class_name: 'Captain::Assistant'
|
|
belongs_to :user, optional: true
|
|
belongs_to :subject, ->(session) { where(account_id: session.account_id) }, polymorphic: true
|
|
belongs_to :result, ->(session) { where(account_id: session.account_id) }, polymorphic: true, optional: true
|
|
|
|
enum :session_type, { assistant: 0, copilot: 1 }, prefix: :session
|
|
|
|
before_validation :ensure_account
|
|
|
|
validate :subject_type_matches_session_type
|
|
validate :result_type_matches_session_type, if: -> { result_type.present? }
|
|
validate :subject_belongs_to_account
|
|
validate :result_belongs_to_account, if: -> { result_id.present? }
|
|
|
|
private
|
|
|
|
def ensure_account
|
|
self.account = assistant&.account
|
|
end
|
|
|
|
def subject_type_matches_session_type
|
|
expected_type = SUBJECT_TYPES[session_type]
|
|
return if subject_type == expected_type
|
|
|
|
errors.add(:subject_type, "must be #{expected_type} for #{session_type} sessions")
|
|
end
|
|
|
|
def result_type_matches_session_type
|
|
expected_type = RESULT_TYPES[session_type]
|
|
return if result_type == expected_type
|
|
|
|
errors.add(:result_type, "must be #{expected_type} for #{session_type} sessions")
|
|
end
|
|
|
|
def subject_belongs_to_account
|
|
return if subject.nil? || subject.account_id == account_id
|
|
|
|
errors.add(:subject, 'must belong to the session account')
|
|
end
|
|
|
|
def result_belongs_to_account
|
|
target_class = result_type.safe_constantize
|
|
actual_account_id = target_class && target_class.unscoped.where(id: result_id).pick(:account_id)
|
|
return if actual_account_id == account_id
|
|
|
|
errors.add(:result, 'must belong to the session account')
|
|
end
|
|
end
|