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:
Pranav
2025-05-16 19:27:57 -07:00
committed by GitHub
parent a295d5b61d
commit cbdac45824
15 changed files with 143 additions and 24 deletions

View File

@@ -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'
create_handoff_message
@conversation.bot_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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.