fix: captain talking over support agent (#13673)
This commit is contained in:
@@ -310,6 +310,7 @@ class Message < ApplicationRecord
|
|||||||
def execute_after_create_commit_callbacks
|
def execute_after_create_commit_callbacks
|
||||||
# rails issue with order of active record callbacks being executed https://github.com/rails/rails/issues/20911
|
# rails issue with order of active record callbacks being executed https://github.com/rails/rails/issues/20911
|
||||||
reopen_conversation
|
reopen_conversation
|
||||||
|
mark_pending_conversation_as_open_for_human_response
|
||||||
set_conversation_activity
|
set_conversation_activity
|
||||||
dispatch_create_events
|
dispatch_create_events
|
||||||
send_reply
|
send_reply
|
||||||
@@ -390,6 +391,18 @@ class Message < ApplicationRecord
|
|||||||
reopen_resolved_conversation if conversation.resolved?
|
reopen_resolved_conversation if conversation.resolved?
|
||||||
end
|
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
|
def reopen_resolved_conversation
|
||||||
# mark resolved bot conversation as pending to be reopened by bot processor service
|
# mark resolved bot conversation as pending to be reopened by bot processor service
|
||||||
if conversation.inbox.active_bot?
|
if conversation.inbox.active_bot?
|
||||||
|
|||||||
@@ -236,6 +236,7 @@ en:
|
|||||||
resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
|
resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
|
||||||
resolved_by_tool: 'Conversation was marked resolved by %{user_name}: %{reason}'
|
resolved_by_tool: 'Conversation was marked resolved by %{user_name}: %{reason}'
|
||||||
open: 'Conversation was marked open by %{user_name}'
|
open: 'Conversation was marked open by %{user_name}'
|
||||||
|
auto_opened_after_agent_reply: 'Conversation was marked open automatically after an agent reply'
|
||||||
agent_bot:
|
agent_bot:
|
||||||
error_moved_to_open: 'Conversation was marked open by system due to an error with the agent bot.'
|
error_moved_to_open: 'Conversation was marked open by system due to an error with the agent bot.'
|
||||||
status:
|
status:
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
|
|||||||
@inbox = conversation.inbox
|
@inbox = conversation.inbox
|
||||||
@assistant = assistant
|
@assistant = assistant
|
||||||
|
|
||||||
|
return unless conversation_pending?
|
||||||
|
|
||||||
Current.executed_by = @assistant
|
Current.executed_by = @assistant
|
||||||
|
|
||||||
if captain_v2_enabled?
|
if captain_v2_enabled?
|
||||||
@@ -15,9 +17,10 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
|
|||||||
else
|
else
|
||||||
generate_and_process_response
|
generate_and_process_response
|
||||||
end
|
end
|
||||||
|
rescue ActiveStorage::FileNotFoundError, Faraday::BadRequestError => e
|
||||||
|
handle_error(e)
|
||||||
|
raise e
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
raise e if e.is_a?(ActiveStorage::FileNotFoundError) || e.is_a?(Faraday::BadRequestError)
|
|
||||||
|
|
||||||
handle_error(e)
|
handle_error(e)
|
||||||
ensure
|
ensure
|
||||||
Current.executed_by = nil
|
Current.executed_by = nil
|
||||||
@@ -42,6 +45,8 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
|
|||||||
end
|
end
|
||||||
|
|
||||||
def process_response
|
def process_response
|
||||||
|
return unless conversation_pending?
|
||||||
|
|
||||||
if handoff_requested?
|
if handoff_requested?
|
||||||
process_action('handoff')
|
process_action('handoff')
|
||||||
else
|
else
|
||||||
@@ -144,4 +149,9 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
|
|||||||
def captain_v2_enabled?
|
def captain_v2_enabled?
|
||||||
account.feature_enabled?('captain_integration_v2')
|
account.feature_enabled?('captain_integration_v2')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def conversation_pending?
|
||||||
|
status = Conversation.where(id: @conversation.id).pick(:status)
|
||||||
|
status == 'pending' || status == Conversation.statuses[:pending]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
40
enterprise/app/models/enterprise/message.rb
Normal file
40
enterprise/app/models/enterprise/message.rb
Normal 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
|
||||||
@@ -7,7 +7,7 @@ RSpec.describe Captain::Conversation::ResponseBuilderJob, type: :job do
|
|||||||
let(:captain_inbox_association) { create(:captain_inbox, captain_assistant: assistant, inbox: inbox) }
|
let(:captain_inbox_association) { create(:captain_inbox, captain_assistant: assistant, inbox: inbox) }
|
||||||
|
|
||||||
describe '#perform' do
|
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_llm_chat_service) { instance_double(Captain::Llm::AssistantChatService) }
|
||||||
let(:mock_agent_runner_service) { instance_double(Captain::Assistant::AgentRunnerService) }
|
let(:mock_agent_runner_service) { instance_double(Captain::Assistant::AgentRunnerService) }
|
||||||
|
|
||||||
@@ -47,6 +47,15 @@ RSpec.describe Captain::Conversation::ResponseBuilderJob, type: :job do
|
|||||||
account.reload
|
account.reload
|
||||||
expect(account.usage_limits[:captain][:responses][:consumed]).to eq(1)
|
expect(account.usage_limits[:captain][:responses][:consumed]).to eq(1)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
context 'when captain_v2 is enabled' do
|
context 'when captain_v2 is enabled' do
|
||||||
@@ -157,7 +166,7 @@ RSpec.describe Captain::Conversation::ResponseBuilderJob, type: :job do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe 'retry mechanisms for image processing' do
|
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_llm_chat_service) { instance_double(Captain::Llm::AssistantChatService) }
|
||||||
let(:mock_message_builder) { instance_double(Captain::OpenAiMessageBuilderService) }
|
let(:mock_message_builder) { instance_double(Captain::OpenAiMessageBuilderService) }
|
||||||
|
|
||||||
|
|||||||
@@ -23,4 +23,69 @@ RSpec.describe Message do
|
|||||||
expect(conversation.first_reply_created_at).not_to be_nil
|
expect(conversation.first_reply_created_at).not_to be_nil
|
||||||
expect(conversation.waiting_since).to be_nil
|
expect(conversation.waiting_since).to be_nil
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -271,6 +271,15 @@ RSpec.describe Message do
|
|||||||
end
|
end
|
||||||
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
|
describe '#waiting since' do
|
||||||
let(:conversation) { create(:conversation) }
|
let(:conversation) { create(:conversation) }
|
||||||
let(:agent) { create(:user, account: conversation.account) }
|
let(:agent) { create(:user, account: conversation.account) }
|
||||||
|
|||||||
Reference in New Issue
Block a user