Merge branch 'develop' into chore/improve-conversation-snooze

This commit is contained in:
Sivin Varghese
2026-02-28 12:22:05 +05:30
committed by GitHub
12 changed files with 131 additions and 4 deletions
+2
View File
@@ -41,6 +41,7 @@ class Account < ApplicationRecord
'audio_transcriptions': { 'type': %w[boolean null] },
'auto_resolve_label': { 'type': %w[string null] },
'keep_pending_on_bot_failure': { 'type': %w[boolean null] },
'captain_disable_auto_resolve': { 'type': %w[boolean null] },
'conversation_required_attributes': {
'type': %w[array null],
'items': { 'type': 'string' }
@@ -90,6 +91,7 @@ class Account < ApplicationRecord
store_accessor :settings, :audio_transcriptions, :auto_resolve_label
store_accessor :settings, :captain_models, :captain_features
store_accessor :settings, :keep_pending_on_bot_failure
store_accessor :settings, :captain_disable_auto_resolve
has_many :account_users, dependent: :destroy_async
has_many :agent_bot_inboxes, dependent: :destroy_async
+1
View File
@@ -159,6 +159,7 @@ class Conversation < ApplicationRecord
end
def bot_handoff!
update(waiting_since: Time.current) if waiting_since.blank?
open!
dispatcher_dispatch(CONVERSATION_BOT_HANDOFF)
end
@@ -42,10 +42,10 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
end
def process_response
ActiveRecord::Base.transaction do
if handoff_requested?
process_action('handoff')
else
if handoff_requested?
process_action('handoff')
else
ActiveRecord::Base.transaction do
create_messages
Rails.logger.info("[CAPTAIN][ResponseBuilderJob] Incrementing response usage for #{account.id}")
account.increment_response_usage
@@ -2,6 +2,8 @@ class Captain::InboxPendingConversationsResolutionJob < ApplicationJob
queue_as :low
def perform(inbox)
return if inbox.account.captain_disable_auto_resolve
Current.executed_by = inbox.captain_assistant
resolvable_conversations = inbox.conversations.pending.where('last_activity_at < ? ', Time.now.utc - 1.hour).limit(Limits::BULK_ACTIONS_LIMIT)
@@ -12,6 +12,7 @@ module Enterprise::Account::ConversationsResolutionSchedulerJob
inbox = captain_inbox.inbox
next if inbox.email?
next if inbox.account.captain_disable_auto_resolve
Captain::InboxPendingConversationsResolutionJob.perform_later(
inbox
@@ -19,6 +19,9 @@ class Messages::AudioTranscriptionService< Llm::LegacyBaseOpenAiService
transcriptions = transcribe_audio
Rails.logger.info "Audio transcription successful: #{transcriptions}"
{ success: true, transcriptions: transcriptions }
rescue Faraday::UnauthorizedError
Rails.logger.warn('Skipping audio transcription: OpenAI configuration is invalid or disabled (401 Unauthorized).')
{ error: 'OpenAI configuration is invalid or disabled (401)' }
end
private
@@ -6,6 +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_disable_auto_resolve
log_tool_usage('resolve_conversation', { conversation_id: conversation.id, reason: reason })
@@ -92,6 +92,44 @@ RSpec.describe Captain::Conversation::ResponseBuilderJob, type: :job do
end
end
# Regression (PR #13417): wrapping create_handoff_message and bot_handoff! in the
# same transaction defers the message's after_create_commit until commit, at which
# point it clears waiting_since (bot_response). The handoff path must stay outside
# the transaction so the callback fires before bot_handoff! sets waiting_since.
context 'when handoff is requested' do
let(:conversation) { create(:conversation, inbox: inbox, account: account, status: :pending) }
let(:agent) { create(:user, account: account, role: :agent) }
before do
allow(account).to receive(:feature_enabled?).and_return(false)
allow(account).to receive(:feature_enabled?).with('captain_integration_v2').and_return(false)
allow(mock_llm_chat_service).to receive(:generate_response).and_return({ 'response' => 'conversation_handoff' })
end
it 'sets waiting_since to approximately the handoff time' do
freeze_time do
described_class.perform_now(conversation, assistant)
conversation.reload
expect(conversation.status).to eq('open')
expect(conversation.waiting_since).to be_within(1.second).of(Time.current)
end
end
it 'preserves waiting_since so a human reply consumes it for reply_time tracking' do
described_class.perform_now(conversation, assistant)
conversation.reload
expect(conversation.waiting_since).to be_present
# A human reply clears waiting_since (consumed by dispatch_create_events
# to emit FIRST_REPLY_CREATED or REPLY_CREATED for reply_time tracking).
create(:message, conversation: conversation, message_type: :outgoing,
sender: agent, account: account, inbox: inbox)
expect(conversation.reload.waiting_since).to be_nil
end
end
context 'when message contains an image' do
let(:message_with_image) { create(:message, conversation: conversation, message_type: :incoming, content: 'Can you help with this error?') }
let(:image_attachment) { message_with_image.attachments.create!(account: account, file_type: :image, external_url: 'https://example.com/error.jpg') }
@@ -64,4 +64,15 @@ RSpec.describe Captain::InboxPendingConversationsResolutionJob, type: :job do
}
)
end
it 'does not resolve conversations when auto-resolve is disabled at execution time' do
inbox.account.update!(captain_disable_auto_resolve: true)
expect do
described_class.perform_now(inbox)
end.not_to(change { resolvable_pending_conversation.reload.status })
expect(resolvable_pending_conversation.reload.status).to eq('pending')
expect(resolvable_pending_conversation.messages.outgoing).to be_empty
end
end
@@ -30,6 +30,22 @@ RSpec.describe Account::ConversationsResolutionSchedulerJob, type: :job do
end
end
context 'when account has captain_disable_auto_resolve enabled' do
let!(:regular_inbox) { create(:inbox, account: account) }
before do
create(:captain_inbox, captain_assistant: assistant, inbox: regular_inbox)
account.update!(captain_disable_auto_resolve: true)
end
it 'does not enqueue resolution jobs' do
expect do
described_class.perform_now
end.not_to have_enqueued_job(Captain::InboxPendingConversationsResolutionJob)
.with(regular_inbox)
end
end
context 'when inbox has no captain enabled' do
let!(:inbox_without_captain) { create(:inbox, account: create(:account)) }
@@ -36,6 +36,17 @@ RSpec.describe Captain::Tools::ResolveConversationTool do
end
end
describe 'when auto-resolve is disabled for the account' do
before { account.update!(captain_disable_auto_resolve: true) }
it 'does not resolve and returns a disabled message' do
result = tool.perform(tool_context, reason: 'Possible spam')
expect(result).to eq('Auto-resolve is disabled for this account')
expect(conversation.reload).not_to be_resolved
end
end
describe 'resolving an already resolved conversation' do
let(:conversation) { create(:conversation, account: account, inbox: inbox, status: :resolved) }
+41
View File
@@ -313,6 +313,47 @@ RSpec.describe Conversation do
end
end
describe '#bot_handoff!' do
let(:conversation) { create(:conversation, status: :pending) }
before do
allow(Rails.configuration.dispatcher).to receive(:dispatch)
end
context 'when waiting_since is blank' do
before { conversation.update(waiting_since: nil) }
it 'sets waiting_since to current time' do
freeze_time do
conversation.bot_handoff!
expect(conversation.reload.waiting_since).to eq(Time.current)
end
end
end
context 'when waiting_since is already set' do
let(:original_time) { 1.hour.ago }
before { conversation.update(waiting_since: original_time) }
it 'preserves existing waiting_since' do
conversation.bot_handoff!
expect(conversation.reload.waiting_since).to be_within(1.second).of(original_time)
end
end
it 'changes status to open' do
conversation.bot_handoff!
expect(conversation.reload.status).to eq('open')
end
it 'dispatches CONVERSATION_BOT_HANDOFF event' do
expect(Rails.configuration.dispatcher).to receive(:dispatch)
.with(described_class::CONVERSATION_BOT_HANDOFF, anything, hash_including(conversation: conversation))
conversation.bot_handoff!
end
end
describe '#toggle_priority' do
it 'defaults priority to nil when created' do
conversation = create(:conversation, status: 'open')