Files
chatwoot/enterprise/app/models/captain/message_generation.rb
PranavandClaude Opus 4.8 eb203bb144 feat(captain): capture and surface how a reply was generated
Records reasoning, citations (the FAQs the assistant found), the tool
path, and the model for each V1 Captain reply in a new
captain_message_generations table. The assistant reports the source IDs
it actually used so cited FAQs can be flagged, and handoff messages
capture the transfer reason.

Agents can expand any Captain message in the conversation (a sparkle
toggle on the timestamp line) to see this breakdown, fetched on demand
from a dedicated endpoint. Model is shown only in development.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 10:10:55 -07:00

67 lines
2.2 KiB
Ruby

# == Schema Information
#
# Table name: captain_message_generations
#
# id :bigint not null, primary key
# citations :jsonb not null
# generation_path :jsonb not null
# model :string
# reasoning :text
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# assistant_id :bigint not null
# conversation_id :bigint not null
# message_id :bigint not null
#
# Indexes
#
# index_captain_message_generations_on_account_id (account_id)
# index_captain_message_generations_on_assistant_id (assistant_id)
# index_captain_message_generations_on_conversation_id (conversation_id)
# index_captain_message_generations_on_message_id (message_id) UNIQUE
#
class Captain::MessageGeneration < ApplicationRecord
self.table_name = 'captain_message_generations'
belongs_to :account
# `Captain::Conversation` exists as a job namespace, so the association would
# resolve to that module instead of the top-level model without this override.
belongs_to :conversation, class_name: '::Conversation'
belongs_to :message
belongs_to :assistant, class_name: 'Captain::Assistant'
before_validation :ensure_account_and_conversation
def self.record!(message:, assistant:, reasoning:, used_sources:, metadata: nil)
metadata ||= {}
create!(
message: message,
assistant: assistant,
reasoning: reasoning,
model: metadata[:model],
citations: flag_used_citations(metadata[:citations], used_sources),
generation_path: metadata[:generation_path] || []
)
end
# A found FAQ counts as "used" when the assistant lists its Source ID (the
# AssistantResponse id) in the `used_sources` field of its response.
def self.flag_used_citations(citations, used_sources)
used = Array(used_sources).map(&:to_s)
Array(citations).map do |citation|
citation.merge('used' => used.include?(citation['response_id'].to_s))
end
end
private
def ensure_account_and_conversation
return if message.blank?
self.account ||= message.account
self.conversation ||= message.conversation
end
end