Resolved conversations need a separate suggestion layer so repeated FAQ signals can be grouped without creating untrusted knowledge entries. This PR adds the persistence foundation only; it introduces no user-facing behavior by itself. ## Closes - [CW-7495](https://linear.app/chatwoot/issue/CW-7495/backend-llm-changes-to-make-conversation-faqs-as-signalssuggestions) (stacked PR 1/3; the issue is complete after the full stack lands) ## What changed - Added `captain_faq_suggestions` with question, answer, embedding, source count, and review status. - Added `captain_faq_observations` to retain conversation-level signals. - Added Captain assistant, account, and conversation associations. - Added vector and lookup indexes for semantic grouping. ## How to test This layer has no standalone UI behavior. Apply the migration and confirm Captain assistants can persist open FAQ suggestions with attached conversation observations.
126 lines
3.4 KiB
Ruby
126 lines
3.4 KiB
Ruby
# == Schema Information
|
|
#
|
|
# Table name: captain_assistants
|
|
#
|
|
# id :bigint not null, primary key
|
|
# config :jsonb not null
|
|
# description :text
|
|
# guardrails :jsonb
|
|
# name :string not null
|
|
# response_guidelines :jsonb
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# account_id :bigint not null
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_captain_assistants_on_account_id (account_id)
|
|
#
|
|
class Captain::Assistant < ApplicationRecord
|
|
DESCRIPTION_LENGTH_LIMIT = 500
|
|
|
|
include Avatarable
|
|
include Concerns::CaptainToolsHelpers
|
|
include Concerns::Agentable
|
|
|
|
self.table_name = 'captain_assistants'
|
|
|
|
belongs_to :account
|
|
has_many :documents, class_name: 'Captain::Document', dependent: :destroy_async
|
|
has_many :responses, class_name: 'Captain::AssistantResponse', dependent: :destroy_async
|
|
has_many :faq_suggestions, class_name: 'Captain::FaqSuggestion', dependent: :destroy_async
|
|
has_many :captain_inboxes,
|
|
class_name: 'CaptainInbox',
|
|
foreign_key: :captain_assistant_id,
|
|
dependent: :destroy_async
|
|
has_many :inboxes,
|
|
through: :captain_inboxes
|
|
has_many :messages, as: :sender, dependent: :nullify
|
|
has_many :copilot_threads, dependent: :destroy_async
|
|
has_many :scenarios, class_name: 'Captain::Scenario', dependent: :destroy_async
|
|
has_many :agent_sessions, class_name: 'Captain::AgentSession', dependent: :destroy_async
|
|
|
|
store_accessor :config, :temperature, :feature_faq, :feature_memory, :feature_contact_attributes, :product_name
|
|
|
|
validates :name, presence: true
|
|
validates :description, presence: true, length: { maximum: DESCRIPTION_LENGTH_LIMIT }
|
|
validates :account_id, presence: true
|
|
|
|
scope :ordered, -> { order(created_at: :desc) }
|
|
|
|
scope :for_account, ->(account_id) { where(account_id: account_id) }
|
|
|
|
def available_name
|
|
name
|
|
end
|
|
|
|
def available_agent_tools
|
|
tools = self.class.built_in_agent_tools.dup
|
|
|
|
custom_tools = account.captain_custom_tools.enabled.map(&:to_tool_metadata)
|
|
tools.concat(custom_tools)
|
|
|
|
tools
|
|
end
|
|
|
|
def available_tool_ids
|
|
available_agent_tools.pluck(:id)
|
|
end
|
|
|
|
def push_event_data
|
|
{
|
|
id: id,
|
|
name: name,
|
|
avatar_url: avatar_url.presence || default_avatar_url,
|
|
description: description,
|
|
created_at: created_at,
|
|
type: 'captain_assistant'
|
|
}
|
|
end
|
|
|
|
def webhook_data
|
|
{
|
|
id: id,
|
|
name: name,
|
|
avatar_url: avatar_url.presence || default_avatar_url,
|
|
description: description,
|
|
created_at: created_at,
|
|
type: 'captain_assistant'
|
|
}
|
|
end
|
|
|
|
private
|
|
|
|
def agent_name
|
|
name.parameterize(separator: '_')
|
|
end
|
|
|
|
def agent_tools
|
|
[
|
|
self.class.resolve_tool_class('faq_lookup').new(self),
|
|
self.class.resolve_tool_class('handoff').new(self)
|
|
]
|
|
end
|
|
|
|
def prompt_context
|
|
{
|
|
name: name,
|
|
description: description,
|
|
product_name: config['product_name'] || 'this product',
|
|
scenarios: scenarios.enabled.map do |scenario|
|
|
{
|
|
title: scenario.title,
|
|
key: scenario.handoff_key,
|
|
description: scenario.description
|
|
}
|
|
end,
|
|
response_guidelines: response_guidelines || [],
|
|
guardrails: guardrails || []
|
|
}
|
|
end
|
|
|
|
def default_avatar_url
|
|
"#{ENV.fetch('FRONTEND_URL', nil)}/assets/images/dashboard/captain/logo.svg"
|
|
end
|
|
end
|