feat: move Captain auto-resolve policy to assistants (1/5) (#15299)
Captain auto-resolve policy is now owned by each assistant. Existing assistants first read an assistant-level setting, fall back to the current account setting during rollout, and are backfilled asynchronously so behavior is preserved. ## Closes [AI-163](https://linear.app/chatwoot/issue/AI-163) ## How to test 1. Configure different auto-resolve modes on two assistants in the same account through the API. 2. Confirm each assistant follows its own mode when the inactivity job runs. 3. Confirm an assistant without the new setting follows the existing account mode. 4. Run the migration job and confirm the account mode is copied without overwriting an existing assistant mode. 5. Confirm evaluated mode falls back to time-based resolution when the `captain_tasks` capability is unavailable. ## What changed - Added assistant-level `disabled`, `legacy`, and `evaluated` policy storage and validation. - Updated scheduling and resolution runtime reads to use the assistant policy. - Added a compatibility fallback and asynchronous backfill from the account setting. - Preserved the account-level `captain_tasks` capability gate for evaluation. - Kept this foundation independent of the Captain V2 guard; the timer and advanced settings in stacks 2–5 are Captain V2-only. Stack 1 of 5. This is the base for #15303.
This commit is contained in:
@@ -14,7 +14,12 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base
|
||||
end
|
||||
|
||||
def update
|
||||
@assistant.update!(assistant_params)
|
||||
@assistant.with_lock do
|
||||
permitted_params = assistant_params
|
||||
permitted_params[:config] = @assistant.config.merge(permitted_params[:config].to_h) if permitted_params[:config]
|
||||
|
||||
@assistant.update!(permitted_params)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -119,13 +124,14 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base
|
||||
end
|
||||
|
||||
def assistant_params
|
||||
assistant_config_attributes = [
|
||||
:product_name, :feature_faq, :feature_memory, :feature_citation,
|
||||
:feature_contact_attributes, :welcome_message, :handoff_message,
|
||||
:resolution_message, :instructions, :temperature, :auto_resolve_mode
|
||||
]
|
||||
|
||||
permitted = params.require(:assistant).permit(:name, :description,
|
||||
config: [
|
||||
:product_name, :feature_faq, :feature_memory, :feature_citation,
|
||||
:feature_contact_attributes,
|
||||
:welcome_message, :handoff_message, :resolution_message,
|
||||
:instructions, :temperature
|
||||
])
|
||||
config: assistant_config_attributes)
|
||||
|
||||
# Handle array parameters separately to allow partial updates
|
||||
permitted[:response_guidelines] = params[:assistant][:response_guidelines] if params[:assistant].key?(:response_guidelines)
|
||||
|
||||
@@ -5,9 +5,10 @@ class Captain::InboxPendingConversationsResolutionJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
def perform(inbox)
|
||||
return if inbox.account.captain_auto_resolve_disabled?
|
||||
captain_assistant = inbox.captain_assistant
|
||||
return if captain_assistant.blank? || captain_assistant.inactive_conversation_resolution_disabled?
|
||||
|
||||
if evaluate_conversation_completion?(inbox.account)
|
||||
if evaluate_conversation_completion?(captain_assistant, inbox.account)
|
||||
perform_with_evaluation(inbox)
|
||||
else
|
||||
perform_time_based(inbox)
|
||||
@@ -18,8 +19,8 @@ class Captain::InboxPendingConversationsResolutionJob < ApplicationJob
|
||||
|
||||
private
|
||||
|
||||
def evaluate_conversation_completion?(account)
|
||||
account.feature_enabled?('captain_tasks') && account.captain_auto_resolve_evaluated?
|
||||
def evaluate_conversation_completion?(assistant, account)
|
||||
account.feature_enabled?('captain_tasks') && assistant.evaluate_inactive_conversations_before_resolving?
|
||||
end
|
||||
|
||||
def perform_time_based(inbox)
|
||||
|
||||
@@ -10,9 +10,10 @@ module Enterprise::Account::ConversationsResolutionSchedulerJob
|
||||
def resolve_captain_conversations
|
||||
CaptainInbox.all.find_each(batch_size: 100) do |captain_inbox|
|
||||
inbox = captain_inbox.inbox
|
||||
assistant = captain_inbox.captain_assistant
|
||||
|
||||
next if inbox.email?
|
||||
next if inbox.account.captain_auto_resolve_disabled?
|
||||
next if assistant.blank? || assistant.inactive_conversation_resolution_disabled?
|
||||
|
||||
Captain::InboxPendingConversationsResolutionJob.perform_later(
|
||||
inbox
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
class Migration::CopyCaptainAutoResolveModeToAssistantsJob < ApplicationJob
|
||||
queue_as :async_database_migration
|
||||
|
||||
def perform
|
||||
Captain::Assistant.includes(:account).find_each do |assistant|
|
||||
assistant.with_lock do
|
||||
next if assistant.config.key?('auto_resolve_mode')
|
||||
|
||||
config = assistant.config.merge('auto_resolve_mode' => assistant.account.captain_auto_resolve_mode)
|
||||
assistant.update!(config: config)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -18,6 +18,7 @@
|
||||
#
|
||||
class Captain::Assistant < ApplicationRecord
|
||||
DESCRIPTION_LENGTH_LIMIT = 500
|
||||
AUTO_RESOLVE_MODES = %w[disabled legacy evaluated].freeze
|
||||
|
||||
include Avatarable
|
||||
include Concerns::CaptainToolsHelpers
|
||||
@@ -41,11 +42,15 @@ class Captain::Assistant < ApplicationRecord
|
||||
has_many :agent_sessions, class_name: 'Captain::AgentSession', dependent: :destroy_async
|
||||
has_many :conversation_outcomes, dependent: :destroy_async
|
||||
|
||||
store_accessor :config, :temperature, :feature_faq, :feature_memory, :feature_contact_attributes, :product_name
|
||||
store_accessor :config, :temperature, :feature_faq, :feature_memory, :feature_contact_attributes, :product_name,
|
||||
:auto_resolve_mode
|
||||
|
||||
before_validation :set_default_auto_resolve_mode, on: :create
|
||||
|
||||
validates :name, presence: true
|
||||
validates :description, presence: true, length: { maximum: DESCRIPTION_LENGTH_LIMIT }
|
||||
validates :account_id, presence: true
|
||||
validates :auto_resolve_mode, inclusion: { in: AUTO_RESOLVE_MODES }
|
||||
|
||||
scope :ordered, -> { order(created_at: :desc) }
|
||||
|
||||
@@ -55,6 +60,18 @@ class Captain::Assistant < ApplicationRecord
|
||||
name
|
||||
end
|
||||
|
||||
def auto_resolve_mode
|
||||
config.fetch('auto_resolve_mode') { account&.captain_auto_resolve_mode || 'evaluated' }
|
||||
end
|
||||
|
||||
def inactive_conversation_resolution_disabled?
|
||||
auto_resolve_mode == 'disabled'
|
||||
end
|
||||
|
||||
def evaluate_inactive_conversations_before_resolving?
|
||||
auto_resolve_mode == 'evaluated'
|
||||
end
|
||||
|
||||
def available_agent_tools
|
||||
tools = self.class.built_in_agent_tools.dup
|
||||
|
||||
@@ -92,6 +109,12 @@ class Captain::Assistant < ApplicationRecord
|
||||
|
||||
private
|
||||
|
||||
def set_default_auto_resolve_mode
|
||||
return if config.key?('auto_resolve_mode')
|
||||
|
||||
self.auto_resolve_mode = account&.captain_auto_resolve_mode || 'evaluated'
|
||||
end
|
||||
|
||||
def agent_name
|
||||
name.parameterize(separator: '_')
|
||||
end
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
json.account_id resource.account_id
|
||||
json.config resource.config
|
||||
json.config resource.config.merge('auto_resolve_mode' => resource.auto_resolve_mode)
|
||||
json.created_at resource.created_at.to_i
|
||||
json.description resource.description
|
||||
json.guardrails resource.guardrails
|
||||
|
||||
@@ -6,7 +6,7 @@ class Captain::Tools::ResolveConversationTool < Captain::Tools::BasePublicTool
|
||||
conversation = find_conversation(tool_context.state)
|
||||
return 'Conversation not found' unless conversation
|
||||
return "Conversation ##{conversation.display_id} is already resolved" if conversation.resolved?
|
||||
return 'Auto-resolve is disabled for this account' if conversation.account.captain_auto_resolve_disabled?
|
||||
return 'Auto-resolve is disabled for this assistant' if @assistant.inactive_conversation_resolution_disabled?
|
||||
|
||||
log_tool_usage('resolve_conversation', { conversation_id: conversation.id, reason: reason })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user