fix: captain talking over support agent (#13673)

This commit is contained in:
Aakash Bakhle
2026-03-03 16:13:34 +05:30
committed by GitHub
parent 89da4a2292
commit 374d2258c7
7 changed files with 151 additions and 4 deletions

View File

@@ -310,6 +310,7 @@ class Message < ApplicationRecord
def execute_after_create_commit_callbacks
# rails issue with order of active record callbacks being executed https://github.com/rails/rails/issues/20911
reopen_conversation
mark_pending_conversation_as_open_for_human_response
set_conversation_activity
dispatch_create_events
send_reply
@@ -390,6 +391,18 @@ class Message < ApplicationRecord
reopen_resolved_conversation if conversation.resolved?
end
def mark_pending_conversation_as_open_for_human_response
return unless captain_pending_conversation?
return unless human_response?
return if private?
conversation.open!
end
def captain_pending_conversation?
false
end
def reopen_resolved_conversation
# mark resolved bot conversation as pending to be reopened by bot processor service
if conversation.inbox.active_bot?

View File

@@ -236,6 +236,7 @@ en:
resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
resolved_by_tool: 'Conversation was marked resolved by %{user_name}: %{reason}'
open: 'Conversation was marked open by %{user_name}'
auto_opened_after_agent_reply: 'Conversation was marked open automatically after an agent reply'
agent_bot:
error_moved_to_open: 'Conversation was marked open by system due to an error with the agent bot.'
status:

View File

@@ -8,6 +8,8 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
@inbox = conversation.inbox
@assistant = assistant
return unless conversation_pending?
Current.executed_by = @assistant
if captain_v2_enabled?
@@ -15,9 +17,10 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
else
generate_and_process_response
end
rescue ActiveStorage::FileNotFoundError, Faraday::BadRequestError => e
handle_error(e)
raise e
rescue StandardError => e
raise e if e.is_a?(ActiveStorage::FileNotFoundError) || e.is_a?(Faraday::BadRequestError)
handle_error(e)
ensure
Current.executed_by = nil
@@ -42,6 +45,8 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
end
def process_response
return unless conversation_pending?
if handoff_requested?
process_action('handoff')
else
@@ -144,4 +149,9 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
def captain_v2_enabled?
account.feature_enabled?('captain_integration_v2')
end
def conversation_pending?
status = Conversation.where(id: @conversation.id).pick(:status)
status == 'pending' || status == Conversation.statuses[:pending]
end
end

View File

@@ -0,0 +1,40 @@
module Enterprise::Message
private
def mark_pending_conversation_as_open_for_human_response
return unless captain_pending_conversation?
return unless human_response?
return if private?
previous_user = Current.user
previous_executed_by = Current.executed_by
Current.user = nil
Current.executed_by = nil
begin
conversation.open!
return unless conversation.saved_change_to_status?
create_captain_auto_open_activity_message
ensure
Current.user = previous_user
Current.executed_by = previous_executed_by
end
end
def captain_pending_conversation?
return false unless conversation.pending?
::CaptainInbox.exists?(inbox_id: conversation.inbox_id)
end
def create_captain_auto_open_activity_message
::Conversations::ActivityMessageJob.perform_later(
conversation,
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: :activity,
content: I18n.t('conversations.activity.captain.auto_opened_after_agent_reply', locale: conversation.account.locale)
)
end
end

View File

@@ -7,7 +7,7 @@ RSpec.describe Captain::Conversation::ResponseBuilderJob, type: :job do
let(:captain_inbox_association) { create(:captain_inbox, captain_assistant: assistant, inbox: inbox) }
describe '#perform' do
let(:conversation) { create(:conversation, inbox: inbox, account: account) }
let(:conversation) { create(:conversation, inbox: inbox, account: account, status: :pending) }
let(:mock_llm_chat_service) { instance_double(Captain::Llm::AssistantChatService) }
let(:mock_agent_runner_service) { instance_double(Captain::Assistant::AgentRunnerService) }
@@ -47,6 +47,15 @@ RSpec.describe Captain::Conversation::ResponseBuilderJob, type: :job do
account.reload
expect(account.usage_limits[:captain][:responses][:consumed]).to eq(1)
end
it 'does not send a response when the conversation is no longer pending' do
conversation.open!
expect(mock_llm_chat_service).not_to receive(:generate_response)
expect do
described_class.perform_now(conversation, assistant)
end.not_to(change { conversation.messages.outgoing.count })
end
end
context 'when captain_v2 is enabled' do
@@ -157,7 +166,7 @@ RSpec.describe Captain::Conversation::ResponseBuilderJob, type: :job do
end
describe 'retry mechanisms for image processing' do
let(:conversation) { create(:conversation, inbox: inbox, account: account) }
let(:conversation) { create(:conversation, inbox: inbox, account: account, status: :pending) }
let(:mock_llm_chat_service) { instance_double(Captain::Llm::AssistantChatService) }
let(:mock_message_builder) { instance_double(Captain::OpenAiMessageBuilderService) }

View File

@@ -23,4 +23,69 @@ RSpec.describe Message do
expect(conversation.first_reply_created_at).not_to be_nil
expect(conversation.waiting_since).to be_nil
end
describe '#mark_pending_conversation_as_open_for_human_response' do
let(:conversation) { create(:conversation, status: :pending) }
let(:captain_assistant) { create(:captain_assistant, account: conversation.account) }
let(:auto_open_activity_content) { I18n.t('conversations.activity.captain.auto_opened_after_agent_reply', locale: conversation.account.locale) }
before do
create(:captain_inbox, inbox: conversation.inbox, captain_assistant: captain_assistant)
end
it 'marks the conversation open when a human sends a public outgoing message' do
create(:message, message_type: :outgoing, conversation: conversation)
expect(conversation.reload.open?).to be true
end
it 'creates an activity message when a human sends a public outgoing message' do
expect do
create(:message, message_type: :outgoing, conversation: conversation)
end.to have_enqueued_job(Conversations::ActivityMessageJob).with(
conversation,
{
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: :activity,
content: auto_open_activity_content
}
)
end
it 'creates an activity message for external echo replies' do
message = build(
:message,
message_type: :outgoing,
conversation: conversation,
content_attributes: { external_echo: true }
)
message.sender = nil
expect do
message.save!
end.to have_enqueued_job(Conversations::ActivityMessageJob).with(
conversation,
{
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: :activity,
content: auto_open_activity_content
}
)
end
it 'does not mark the conversation open for private outgoing messages' do
create(:message, message_type: :outgoing, conversation: conversation, private: true)
expect(conversation.reload.pending?).to be true
end
it 'does not mark the conversation open for bot outgoing messages' do
agent_bot = create(:agent_bot, account: conversation.account)
create(:message, message_type: :outgoing, conversation: conversation, sender: agent_bot)
expect(conversation.reload.pending?).to be true
end
end
end

View File

@@ -271,6 +271,15 @@ RSpec.describe Message do
end
end
describe '#mark_pending_conversation_as_open_for_human_response' do
let(:conversation) { create(:conversation, status: :pending) }
it 'does not mark the conversation open when pending is used without captain' do
create(:message, message_type: :outgoing, conversation: conversation)
expect(conversation.reload.pending?).to be true
end
end
describe '#waiting since' do
let(:conversation) { create(:conversation) }
let(:agent) { create(:user, account: conversation.account) }