mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 03:57:52 +00:00
feat: Improve Captain interactions, activity messages (#11493)
Show captain messages under the name of the assistant which generated the message. - Add support for `Captain::Assistant` sender type - Add push_event_data for captain_assistants - Add activity message handler for captain_assistants - Update UI to show captain messages under the name of the assistant - Fix the issue where openAI errors when image is sent - Add support for custom name of the assistant --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
@@ -186,12 +186,20 @@ const isBotOrAgentMessage = computed(() => {
|
||||
return true;
|
||||
}
|
||||
const senderId = props.senderId ?? props.sender?.id;
|
||||
const senderType = props.senderType ?? props.sender?.type;
|
||||
const senderType = props.sender?.type ?? props.senderType;
|
||||
|
||||
if (!senderType || !senderId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
[SENDER_TYPES.AGENT_BOT, SENDER_TYPES.CAPTAIN_ASSISTANT].includes(
|
||||
senderType
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return senderType.toLowerCase() === SENDER_TYPES.USER.toLowerCase();
|
||||
});
|
||||
|
||||
@@ -406,7 +414,7 @@ const avatarInfo = computed(() => {
|
||||
const { name, type, avatarUrl, thumbnail } = sender || {};
|
||||
|
||||
// If sender type is agent bot, use avatarUrl
|
||||
if (type === SENDER_TYPES.AGENT_BOT) {
|
||||
if ([SENDER_TYPES.AGENT_BOT, SENDER_TYPES.CAPTAIN_ASSISTANT].includes(type)) {
|
||||
return {
|
||||
name: name ?? '',
|
||||
src: avatarUrl ?? '',
|
||||
|
||||
@@ -21,6 +21,7 @@ export const SENDER_TYPES = {
|
||||
CONTACT: 'Contact',
|
||||
USER: 'User',
|
||||
AGENT_BOT: 'agent_bot',
|
||||
CAPTAIN_ASSISTANT: 'captain_assistant',
|
||||
};
|
||||
|
||||
export const ORIENTATION = {
|
||||
|
||||
@@ -108,6 +108,7 @@ class Conversation < ApplicationRecord
|
||||
has_many :conversation_participants, dependent: :destroy_async
|
||||
has_many :notifications, as: :primary_actor, dependent: :destroy_async
|
||||
has_many :attachments, through: :messages
|
||||
has_many :reporting_events, dependent: :destroy_async
|
||||
|
||||
before_save :ensure_snooze_until_reset
|
||||
before_create :determine_conversation_status
|
||||
|
||||
@@ -197,10 +197,10 @@ class Message < ApplicationRecord
|
||||
end
|
||||
|
||||
def valid_first_reply?
|
||||
return false unless outgoing? && human_response? && !private?
|
||||
return false unless human_response? && !private?
|
||||
return false if conversation.first_reply_created_at.present?
|
||||
return false if conversation.messages.outgoing
|
||||
.where.not(sender_type: 'AgentBot')
|
||||
.where.not(sender_type: ['AgentBot', 'Captain::Assistant'])
|
||||
.where.not(private: true)
|
||||
.where("(additional_attributes->'campaign_id') is null").count > 1
|
||||
|
||||
|
||||
@@ -146,6 +146,8 @@ en:
|
||||
attachment: 'Attachment'
|
||||
no_content: 'No content'
|
||||
conversations:
|
||||
captain:
|
||||
handoff: 'Transferring to another agent for further assistance.'
|
||||
messages:
|
||||
instagram_story_content: '%{story_sender} mentioned you in the story: '
|
||||
instagram_deleted_story_content: This story is no longer available.
|
||||
@@ -155,6 +157,7 @@ en:
|
||||
activity:
|
||||
captain:
|
||||
resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
|
||||
open: 'Conversation was marked open by %{user_name}'
|
||||
status:
|
||||
resolved: 'Conversation was marked resolved by %{user_name}'
|
||||
contact_resolved: 'Conversation was resolved by %{contact_name}'
|
||||
|
||||
@@ -132,6 +132,8 @@ pt_BR:
|
||||
attachment: 'Anexo'
|
||||
no_content: 'Sem conteúdo'
|
||||
conversations:
|
||||
captain:
|
||||
handoff: 'Transferindo para outro agente para mais assistência.'
|
||||
messages:
|
||||
instagram_story_content: '%{story_sender} mencionou você na conversa: '
|
||||
instagram_deleted_story_content: Este Story não está mais disponível.
|
||||
@@ -139,6 +141,9 @@ pt_BR:
|
||||
delivery_status:
|
||||
error_code: 'Código de erro: %{error_code}'
|
||||
activity:
|
||||
captain:
|
||||
resolved: 'A conversa foi marcada como resolvida por %{user_name} devido à inatividade'
|
||||
open: 'A conversa foi marcada como aberta por %{user_name}'
|
||||
status:
|
||||
resolved: 'Conversa foi marcada como resolvida por %{user_name}'
|
||||
contact_resolved: 'A conversa foi resolvida por %{contact_name}'
|
||||
@@ -203,7 +208,7 @@ pt_BR:
|
||||
meeting_name: '%{agent_name} começou a reunião'
|
||||
slack:
|
||||
name: 'Slack'
|
||||
description: "Integre Chatwoot com Slack para manter seu time em sincronia. Essa integração permite que você receba notificações de novas conversas e as responda diretamente na interface do Slack."
|
||||
description: 'Integre Chatwoot com Slack para manter seu time em sincronia. Essa integração permite que você receba notificações de novas conversas e as responda diretamente na interface do Slack.'
|
||||
webhooks:
|
||||
name: 'Webhooks'
|
||||
description: 'Eventos webhook fornecem atualizações sobre atividades em tempo real na sua conta Chatwoot. Você pode se inscrever em seus eventos preferidos, e o Chatwoot enviará as chamadas HTTP com as atualizações.'
|
||||
@@ -212,7 +217,7 @@ pt_BR:
|
||||
description: 'Construa chatbots com o Dialogflow e integre-os facilmente na sua caixa de entrada. Esses bots podem lidar com as consultas iniciais antes de transferi-las para um agente de atendimento ao cliente.'
|
||||
google_translate:
|
||||
name: 'Tradutor do Google'
|
||||
description: "Integre o Google Tradutor para ajudar os agentes a traduzir facilmente as mensagens dos clientes. Esta integração detecta automaticamente o idioma e o converte para o idioma preferido do agente ou do administrador."
|
||||
description: 'Integre o Google Tradutor para ajudar os agentes a traduzir facilmente as mensagens dos clientes. Esta integração detecta automaticamente o idioma e o converte para o idioma preferido do agente ou do administrador.'
|
||||
openai:
|
||||
name: 'OpenAI'
|
||||
description: 'Aproveite o poder dos grandes modelos de linguagem do OpenAI com recursos como sugestões de resposta, resumo, reformulação de mensagens, verificação ortográfica e classificação de rótulos.'
|
||||
|
||||
@@ -6,11 +6,15 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
|
||||
@inbox = conversation.inbox
|
||||
@assistant = assistant
|
||||
|
||||
Current.executed_by = @assistant
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
generate_and_process_response
|
||||
end
|
||||
rescue StandardError => e
|
||||
handle_error(e)
|
||||
ensure
|
||||
Current.executed_by = nil
|
||||
end
|
||||
|
||||
private
|
||||
@@ -37,13 +41,23 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
|
||||
.where(private: false)
|
||||
.map do |message|
|
||||
{
|
||||
content: message.content,
|
||||
content: message_content(message),
|
||||
role: determine_role(message)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def message_content(message)
|
||||
return message.content if message.content.present?
|
||||
|
||||
'User has shared an attachment' if message.attachments.any?
|
||||
|
||||
'User has shared a message without content'
|
||||
end
|
||||
|
||||
def determine_role(message)
|
||||
return 'system' if message.content.blank?
|
||||
|
||||
message.message_type == 'incoming' ? 'user' : 'system'
|
||||
end
|
||||
|
||||
@@ -54,13 +68,15 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
|
||||
def process_action(action)
|
||||
case action
|
||||
when 'handoff'
|
||||
I18n.with_locale(@assistant.account.locale) do
|
||||
create_handoff_message
|
||||
@conversation.bot_handoff!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create_handoff_message
|
||||
create_outgoing_message(@assistant.config['handoff_message'] || 'Transferring to another agent for further assistance.')
|
||||
create_outgoing_message(@assistant.config['handoff_message'].presence || I18n.t('conversations.captain.handoff'))
|
||||
end
|
||||
|
||||
def create_messages
|
||||
@@ -77,6 +93,7 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
|
||||
message_type: :outgoing,
|
||||
account_id: account.id,
|
||||
inbox_id: inbox.id,
|
||||
sender: @assistant,
|
||||
content: message_content
|
||||
)
|
||||
end
|
||||
|
||||
@@ -2,22 +2,31 @@ class Captain::InboxPendingConversationsResolutionJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
def perform(inbox)
|
||||
# limiting the number of conversations to be resolved to avoid any performance issues
|
||||
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)
|
||||
resolvable_conversations.each do |conversation|
|
||||
resolution_message = conversation.inbox.captain_assistant.config['resolution_message']
|
||||
create_outgoing_message(conversation, inbox)
|
||||
conversation.resolved!
|
||||
end
|
||||
ensure
|
||||
Current.reset
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_outgoing_message(conversation, inbox)
|
||||
I18n.with_locale(inbox.account.locale) do
|
||||
resolution_message = inbox.captain_assistant.config['resolution_message']
|
||||
conversation.messages.create!(
|
||||
{
|
||||
message_type: :outgoing,
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
content: resolution_message || I18n.t('conversations.activity.auto_resolution_message')
|
||||
content: resolution_message.presence || I18n.t('conversations.activity.auto_resolution_message'),
|
||||
sender: inbox.captain_assistant
|
||||
}
|
||||
)
|
||||
conversation.resolved!
|
||||
ensure
|
||||
Current.reset
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
# index_captain_assistants_on_account_id (account_id)
|
||||
#
|
||||
class Captain::Assistant < ApplicationRecord
|
||||
include Avatarable
|
||||
|
||||
self.table_name = 'captain_assistants'
|
||||
|
||||
belongs_to :account
|
||||
@@ -26,6 +28,7 @@ class Captain::Assistant < ApplicationRecord
|
||||
dependent: :destroy_async
|
||||
has_many :inboxes,
|
||||
through: :captain_inboxes
|
||||
has_many :messages, as: :sender, dependent: :nullify
|
||||
|
||||
validates :name, presence: true
|
||||
validates :description, presence: true
|
||||
@@ -34,4 +37,32 @@ class Captain::Assistant < ApplicationRecord
|
||||
scope :ordered, -> { order(created_at: :desc) }
|
||||
|
||||
scope :for_account, ->(account_id) { where(account_id: account_id) }
|
||||
|
||||
def push_event_data
|
||||
{
|
||||
id: id,
|
||||
name: name,
|
||||
avatar_url: avatar_url.presence || default_avatar_url,
|
||||
description: description,
|
||||
created_at: created_at,
|
||||
type: 'captain_assistant'
|
||||
}
|
||||
end
|
||||
|
||||
def webhook_data
|
||||
{
|
||||
id: id,
|
||||
name: name,
|
||||
avatar_url: avatar_url.presence || default_avatar_url,
|
||||
description: description,
|
||||
created_at: created_at,
|
||||
type: 'captain_assistant'
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_avatar_url
|
||||
"#{ENV.fetch('FRONTEND_URL', nil)}/assets/images/dashboard/captain/logo.svg"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
module Enterprise::ActivityMessageHandler
|
||||
def automation_status_change_activity_content
|
||||
if Current.executed_by.instance_of?(Captain::Assistant) && resolved?
|
||||
I18n.t('conversations.activity.captain.resolved', user_name: Current.executed_by.name)
|
||||
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
|
||||
|
||||
@@ -22,7 +22,7 @@ class Captain::Llm::AssistantChatService < Llm::BaseOpenAiService
|
||||
def system_message
|
||||
{
|
||||
role: 'system',
|
||||
content: Captain::Llm::SystemPromptsService.assistant_response_generator(@assistant.config['product_name'], @assistant.config)
|
||||
content: Captain::Llm::SystemPromptsService.assistant_response_generator(@assistant.name, @assistant.config['product_name'], @assistant.config)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -103,10 +103,10 @@ class Captain::Llm::SystemPromptsService
|
||||
SYSTEM_PROMPT_MESSAGE
|
||||
end
|
||||
|
||||
def assistant_response_generator(product_name, config = {})
|
||||
def assistant_response_generator(assistant_name, product_name, config = {})
|
||||
<<~SYSTEM_PROMPT_MESSAGE
|
||||
[Identity]
|
||||
You are Captain, a helpful, friendly, and knowledgeable assistant for the product #{product_name}. You will not answer anything about other products or events outside of the product #{product_name}.
|
||||
Your name is #{assistant_name || 'Captain'}, a helpful, friendly, and knowledgeable assistant for the product #{product_name}. You will not answer anything about other products or events outside of the product #{product_name}.
|
||||
|
||||
[Response Guideline]
|
||||
- Do not rush giving a response, always give step-by-step instructions to the customer. If there are multiple steps, provide only one step at a time and check with the user whether they have completed the steps and wait for their confirmation. If the user has said okay or yes, continue with the steps.
|
||||
|
||||
@@ -153,12 +153,12 @@ class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService
|
||||
def sender_type(sender)
|
||||
if sender.instance_of?(Contact)
|
||||
'Contact'
|
||||
elsif message.message_type == 'template' && sender.nil?
|
||||
'Bot'
|
||||
elsif sender.instance_of?(User)
|
||||
'Agent'
|
||||
elsif message.message_type == 'activity' && sender.nil?
|
||||
'System'
|
||||
else
|
||||
'Agent'
|
||||
'Bot'
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
6
public/assets/images/dashboard/captain/logo.svg
Normal file
6
public/assets/images/dashboard/captain/logo.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="28" height="28" fill="#E1D9FF"/>
|
||||
<path d="M10.2766 11.9346C10.2766 11.5593 10.5808 11.255 10.9562 11.255V11.255C11.3315 11.255 11.6358 11.5593 11.6358 11.9346V13.5053C11.6358 13.8806 11.3315 14.1849 10.9562 14.1849V14.1849C10.5808 14.1849 10.2766 13.8806 10.2766 13.5053V11.9346Z" fill="#272962"/>
|
||||
<path d="M12.4514 11.9346C12.4514 11.5593 12.7556 11.255 13.131 11.255V11.255C13.5063 11.255 13.8106 11.5593 13.8106 11.9346V13.5053C13.8106 13.8806 13.5063 14.1849 13.131 14.1849V14.1849C12.7556 14.1849 12.4514 13.8806 12.4514 13.5053V11.9346Z" fill="#272962"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.7282 9.39092C15.1974 9.03385 13.1343 9.01976 10.5466 9.38935C9.89253 9.48277 9.45974 9.54591 9.12976 9.64235C8.82378 9.73177 8.64977 9.8384 8.50584 9.99925C8.20779 10.3323 8.16851 10.7091 8.12789 12.023C8.08796 13.3143 8.16704 14.4715 8.31682 15.7737C8.39664 16.4675 8.45156 16.9358 8.53925 17.2918C8.62244 17.6295 8.72304 17.8111 8.86287 17.9501C9.00381 18.0902 9.18443 18.189 9.51593 18.2692C9.86659 18.354 10.3267 18.4053 11.0105 18.4799C13.2219 18.7212 14.8773 18.72 17.097 18.4812C17.7891 18.4067 18.2567 18.3554 18.6126 18.2711C18.9507 18.1911 19.1325 18.093 19.2715 17.9569C19.4076 17.8236 19.5116 17.6398 19.6017 17.2853C19.6958 16.9154 19.7598 16.426 19.8525 15.707C20.0178 14.4242 20.1368 13.2976 20.1414 12.0634C20.1463 10.7439 20.1194 10.3684 19.8179 10.0202C19.6724 9.85197 19.4954 9.74137 19.1824 9.64927C18.8445 9.54981 18.4 9.4857 17.7282 9.39092ZM10.3886 8.28297C13.0843 7.89796 15.2531 7.91304 17.8843 8.28428L17.9228 8.28971C18.5458 8.37758 19.0721 8.45182 19.4979 8.57713C19.9575 8.71238 20.3418 8.91768 20.663 9.28876C21.2688 9.98876 21.2649 10.8193 21.2596 11.9227C21.2594 11.9705 21.2592 12.0188 21.259 12.0676C21.2541 13.3725 21.1278 14.5541 20.9609 15.8498L20.9565 15.8841C20.8694 16.5602 20.7976 17.1174 20.6848 17.5608C20.5662 18.0271 20.389 18.4268 20.0535 18.7554C19.7209 19.0811 19.3278 19.2503 18.87 19.3587C18.4377 19.461 17.8998 19.5189 17.2514 19.5886L17.2166 19.5924C14.918 19.8396 13.1818 19.841 10.8893 19.5909L10.8539 19.587C10.2146 19.5173 9.68223 19.4592 9.25317 19.3555C8.79707 19.2451 8.40665 19.0724 8.07492 18.7427C7.74207 18.4118 7.56725 18.0185 7.45409 17.5591C7.34735 17.1258 7.28531 16.5863 7.21059 15.9365L7.20654 15.9014C7.05293 14.566 6.96868 13.3513 7.01082 11.9884C7.01235 11.939 7.01381 11.8901 7.01526 11.8417C7.04801 10.7465 7.07258 9.92502 7.67297 9.25403C7.99151 8.89802 8.36802 8.70062 8.81625 8.56962C9.23195 8.44813 9.74451 8.37494 10.3512 8.28832C10.3636 8.28654 10.3761 8.28476 10.3886 8.28297Z" fill="#272962"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
25
spec/enterprise/models/message_spec.rb
Normal file
25
spec/enterprise/models/message_spec.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Message do
|
||||
let!(:conversation) { create(:conversation) }
|
||||
|
||||
it 'updates first reply if the message is human and even if there are messages from captain' do
|
||||
captain_assistant = create(:captain_assistant, account: conversation.account)
|
||||
expect(conversation.first_reply_created_at).to be_nil
|
||||
|
||||
## There is a difference on how the time is stored in the database and how it is retrieved
|
||||
# This is because of the precision of the time stored in the database
|
||||
# In the test, we will check whether the time is within the range
|
||||
expect(conversation.waiting_since).to be_within(0.000001.seconds).of(conversation.created_at)
|
||||
|
||||
create(:message, message_type: :outgoing, conversation: conversation, sender: captain_assistant)
|
||||
|
||||
expect(conversation.first_reply_created_at).to be_nil
|
||||
expect(conversation.waiting_since).to be_within(0.000001.seconds).of(conversation.created_at)
|
||||
|
||||
create(:message, message_type: :outgoing, conversation: conversation)
|
||||
|
||||
expect(conversation.first_reply_created_at).not_to be_nil
|
||||
expect(conversation.waiting_since).to be_nil
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user