Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
870cd8d3c0 | ||
|
|
292f6f1b18 | ||
|
|
39d6fa06ea | ||
|
|
05de508b7c | ||
|
|
bb9f07d662 |
@@ -34,3 +34,8 @@
|
||||
title: 'Handoff to Human'
|
||||
description: 'Hand off the conversation to a human agent'
|
||||
icon: 'user-switch'
|
||||
|
||||
- id: resolve
|
||||
title: 'Resolve Conversation'
|
||||
description: 'Mark the conversation as resolved'
|
||||
icon: 'checkmark-circle'
|
||||
|
||||
@@ -214,6 +214,7 @@ en:
|
||||
activity:
|
||||
captain:
|
||||
resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
|
||||
tool_resolved: 'Conversation was resolved by %{user_name}'
|
||||
open: 'Conversation was marked open by %{user_name}'
|
||||
agent_bot:
|
||||
error_moved_to_open: 'Conversation was marked open by system due to an error with the agent bot.'
|
||||
|
||||
@@ -94,7 +94,8 @@ class Captain::Assistant < ApplicationRecord
|
||||
def agent_tools
|
||||
[
|
||||
self.class.resolve_tool_class('faq_lookup').new(self),
|
||||
self.class.resolve_tool_class('handoff').new(self)
|
||||
self.class.resolve_tool_class('handoff').new(self),
|
||||
self.class.resolve_tool_class('resolve').new(self)
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
module Enterprise::ActivityMessageHandler
|
||||
def automation_status_change_activity_content
|
||||
if Current.executed_by.instance_of?(Captain::Assistant)
|
||||
locale = Current.executed_by.account.locale
|
||||
if resolved?
|
||||
I18n.t(
|
||||
'conversations.activity.captain.resolved',
|
||||
user_name: Current.executed_by.name,
|
||||
locale: locale
|
||||
)
|
||||
elsif open?
|
||||
I18n.t(
|
||||
'conversations.activity.captain.open',
|
||||
user_name: Current.executed_by.name,
|
||||
locale: locale
|
||||
)
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
return super unless Current.executed_by.instance_of?(Captain::Assistant)
|
||||
return unless resolved? || open?
|
||||
|
||||
locale = Current.executed_by.account.locale
|
||||
user_name = Current.executed_by.name
|
||||
key = captain_activity_key
|
||||
|
||||
I18n.t(key, user_name: user_name, locale: locale)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def captain_activity_key
|
||||
return 'conversations.activity.captain.open' if open?
|
||||
return 'conversations.activity.captain.tool_resolved' if Current.tool_name == 'resolve'
|
||||
|
||||
'conversations.activity.captain.resolved'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
class Captain::Tools::ResolveTool < Captain::Tools::BasePublicTool
|
||||
description 'Mark the conversation as resolved when the issue has been addressed'
|
||||
param :reason, type: 'string', desc: 'The reason why the conversation is being resolved (optional)', required: false
|
||||
|
||||
def perform(tool_context, reason: nil)
|
||||
conversation = find_conversation(tool_context.state)
|
||||
return 'Conversation not found' unless conversation
|
||||
|
||||
return 'Conversation is already resolved' if conversation.resolved?
|
||||
|
||||
log_tool_usage('tool_resolve', {
|
||||
conversation_id: conversation.id,
|
||||
reason: reason || 'Agent resolved conversation'
|
||||
})
|
||||
|
||||
resolve_conversation(conversation, reason)
|
||||
|
||||
"Conversation marked as resolved#{" (Reason: #{reason})" if reason.present?}"
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
'Failed to resolve conversation'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resolve_conversation(conversation, reason)
|
||||
# Add a private note with the reason if provided
|
||||
if reason.present?
|
||||
conversation.messages.create!(
|
||||
message_type: :outgoing,
|
||||
private: true,
|
||||
sender: @assistant,
|
||||
account: conversation.account,
|
||||
inbox: conversation.inbox,
|
||||
content: "Resolved: #{reason}"
|
||||
)
|
||||
end
|
||||
|
||||
# Mark conversation as resolved
|
||||
Current.tool_name = 'resolve'
|
||||
conversation.resolved!
|
||||
ensure
|
||||
Current.tool_name = nil
|
||||
end
|
||||
|
||||
def permissions
|
||||
%w[conversation_manage conversation_unassigned_manage conversation_participating_manage]
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,7 @@ module Current
|
||||
thread_mattr_accessor :account_user
|
||||
thread_mattr_accessor :executed_by
|
||||
thread_mattr_accessor :contact
|
||||
thread_mattr_accessor :tool_name
|
||||
|
||||
def self.reset
|
||||
Current.user = nil
|
||||
@@ -11,5 +12,6 @@ module Current
|
||||
Current.account_user = nil
|
||||
Current.executed_by = nil
|
||||
Current.contact = nil
|
||||
Current.tool_name = nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Captain::Tools::ResolveTool, type: :model do
|
||||
let(:account) { create(:account) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
let(:tool) { described_class.new(assistant) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:contact) { create(:contact, account: account) }
|
||||
let(:conversation) { create(:conversation, account: account, inbox: inbox, contact: contact, status: :open) }
|
||||
let(:tool_context) { Struct.new(:state).new({ conversation: { id: conversation.id } }) }
|
||||
|
||||
describe '#description' do
|
||||
it 'returns the correct description' do
|
||||
expect(tool.description).to eq('Mark the conversation as resolved when the issue has been addressed')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parameters' do
|
||||
it 'returns the correct parameters' do
|
||||
expect(tool.parameters).to have_key(:reason)
|
||||
expect(tool.parameters[:reason].name).to eq(:reason)
|
||||
expect(tool.parameters[:reason].type).to eq('string')
|
||||
expect(tool.parameters[:reason].description).to eq('The reason why the conversation is being resolved (optional)')
|
||||
expect(tool.parameters[:reason].required).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when conversation exists and is not resolved' do
|
||||
context 'with reason provided' do
|
||||
it 'creates a private note with reason and resolves conversation' do
|
||||
reason = 'Customer issue has been fixed'
|
||||
|
||||
expect do
|
||||
result = tool.perform(tool_context, reason: reason)
|
||||
expect(result).to eq("Conversation marked as resolved (Reason: #{reason})")
|
||||
end.to change(Message, :count).by(1)
|
||||
end
|
||||
|
||||
it 'creates message with correct attributes' do
|
||||
reason = 'Customer issue has been fixed'
|
||||
tool.perform(tool_context, reason: reason)
|
||||
|
||||
created_message = Message.last
|
||||
expect(created_message.content).to eq("Resolved: #{reason}")
|
||||
expect(created_message.message_type).to eq('outgoing')
|
||||
expect(created_message.private).to be true
|
||||
expect(created_message.sender).to eq(assistant)
|
||||
expect(created_message.account).to eq(account)
|
||||
expect(created_message.inbox).to eq(inbox)
|
||||
expect(created_message.conversation).to eq(conversation)
|
||||
end
|
||||
|
||||
it 'marks conversation as resolved' do
|
||||
expect(conversation.status).to eq('open')
|
||||
|
||||
tool.perform(tool_context, reason: 'Issue fixed')
|
||||
|
||||
expect(conversation.reload.status).to eq('resolved')
|
||||
end
|
||||
|
||||
it 'logs tool usage with reason' do
|
||||
reason = 'Customer satisfied'
|
||||
expect(tool).to receive(:log_tool_usage).with(
|
||||
'tool_resolve',
|
||||
{ conversation_id: conversation.id, reason: reason }
|
||||
)
|
||||
|
||||
tool.perform(tool_context, reason: reason)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without reason provided' do
|
||||
it 'resolves conversation without creating a private note' do
|
||||
expect do
|
||||
result = tool.perform(tool_context)
|
||||
expect(result).to eq('Conversation marked as resolved')
|
||||
end.not_to change(Message, :count)
|
||||
end
|
||||
|
||||
it 'marks conversation as resolved' do
|
||||
expect(conversation.status).to eq('open')
|
||||
|
||||
tool.perform(tool_context)
|
||||
|
||||
expect(conversation.reload.status).to eq('resolved')
|
||||
end
|
||||
|
||||
it 'logs tool usage with default reason' do
|
||||
expect(tool).to receive(:log_tool_usage).with(
|
||||
'tool_resolve',
|
||||
{ conversation_id: conversation.id, reason: 'Agent resolved conversation' }
|
||||
)
|
||||
|
||||
tool.perform(tool_context)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when resolve fails' do
|
||||
before do
|
||||
found_conversation = Conversation.find(conversation.id)
|
||||
scoped_conversations = Conversation.where(account_id: assistant.account_id)
|
||||
allow(Conversation).to receive(:where).with(account_id: assistant.account_id).and_return(scoped_conversations)
|
||||
allow(scoped_conversations).to receive(:find_by).with(id: conversation.id).and_return(found_conversation)
|
||||
allow(found_conversation).to receive(:resolved!).and_raise(StandardError, 'Resolve error')
|
||||
|
||||
exception_tracker = instance_double(ChatwootExceptionTracker)
|
||||
allow(ChatwootExceptionTracker).to receive(:new).and_return(exception_tracker)
|
||||
allow(exception_tracker).to receive(:capture_exception)
|
||||
end
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, reason: 'Test')
|
||||
expect(result).to eq('Failed to resolve conversation')
|
||||
end
|
||||
|
||||
it 'captures exception' do
|
||||
exception_tracker = instance_double(ChatwootExceptionTracker)
|
||||
expect(ChatwootExceptionTracker).to receive(:new).with(instance_of(StandardError)).and_return(exception_tracker)
|
||||
expect(exception_tracker).to receive(:capture_exception)
|
||||
|
||||
tool.perform(tool_context, reason: 'Test')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation is already resolved' do
|
||||
let(:conversation) { create(:conversation, account: account, inbox: inbox, contact: contact, status: :resolved) }
|
||||
|
||||
it 'returns message that conversation is already resolved' do
|
||||
result = tool.perform(tool_context, reason: 'Test')
|
||||
expect(result).to eq('Conversation is already resolved')
|
||||
end
|
||||
|
||||
it 'does not create a message' do
|
||||
expect do
|
||||
tool.perform(tool_context, reason: 'Test')
|
||||
end.not_to change(Message, :count)
|
||||
end
|
||||
|
||||
it 'does not change conversation status' do
|
||||
expect do
|
||||
tool.perform(tool_context, reason: 'Test')
|
||||
end.not_to(change { conversation.reload.status })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation does not exist' do
|
||||
let(:tool_context) { Struct.new(:state).new({ conversation: { id: 999_999 } }) }
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, reason: 'Test')
|
||||
expect(result).to eq('Conversation not found')
|
||||
end
|
||||
|
||||
it 'does not create a message' do
|
||||
expect do
|
||||
tool.perform(tool_context, reason: 'Test')
|
||||
end.not_to change(Message, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation state is missing' do
|
||||
let(:tool_context) { Struct.new(:state).new({}) }
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, reason: 'Test')
|
||||
expect(result).to eq('Conversation not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation id is nil' do
|
||||
let(:tool_context) { Struct.new(:state).new({ conversation: { id: nil } }) }
|
||||
|
||||
it 'returns error message' do
|
||||
result = tool.perform(tool_context, reason: 'Test')
|
||||
expect(result).to eq('Conversation not found')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#active?' do
|
||||
it 'returns true for public tools' do
|
||||
expect(tool.active?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user