Captain V2 assistants can now use every enabled custom tool from their account through the main assistant. The change keeps existing custom tool access when an assistant has no migrated scenarios, so switching from V1 does not remove the capability without warning. ## How to reproduce 1. Create and enable an account custom tool. 2. Use an assistant with no custom instructions and no generated scenarios. 3. Enable Captain V2 for the account. 4. Before this change, the main assistant receives only FAQ lookup and handoff. After this change, it also receives the enabled account custom tool. ## What changed The main V2 assistant now loads enabled custom tools through its account association. Scenario agents still load only the tools named in their scenario instructions. The account custom tool limit keeps the added tool count bounded. Focused model coverage verifies enabled tools, disabled tools, account isolation, FAQ lookup, and handoff. Existing V1 assistant, V2 scenario, and V2 runner coverage passes. RuboCop passes.
127 lines
3.5 KiB
Ruby
127 lines
3.5 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),
|
|
*account.captain_custom_tools.enabled.map { |custom_tool| custom_tool.tool(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
|