- {{ $t('SURVEY.DESCRIPTION', { inboxName }) }}
+ {{ messageContent }}
+
diff --git a/app/models/article.rb b/app/models/article.rb
index 48e0529a2..14450b574 100644
--- a/app/models/article.rb
+++ b/app/models/article.rb
@@ -33,6 +33,7 @@
#
class Article < ApplicationRecord
include PgSearch::Model
+ include LlmFormattable
has_many :associated_articles,
class_name: :Article,
diff --git a/app/models/channel/twilio_sms.rb b/app/models/channel/twilio_sms.rb
index 8dbe47703..e4575f58a 100644
--- a/app/models/channel/twilio_sms.rb
+++ b/app/models/channel/twilio_sms.rb
@@ -30,6 +30,11 @@ class Channel::TwilioSms < ApplicationRecord
# The same parameter is used to store api_key_secret if api_key authentication is opted
validates :auth_token, presence: true
+ EDITABLE_ATTRS = [
+ :account_sid,
+ :auth_token
+ ].freeze
+
# Must have _one_ of messaging_service_sid _or_ phone_number, and messaging_service_sid is preferred
validates :messaging_service_sid, uniqueness: true, presence: true, unless: :phone_number?
validates :phone_number, absence: true, if: :messaging_service_sid?
diff --git a/app/models/concerns/activity_message_handler.rb b/app/models/concerns/activity_message_handler.rb
index ffd3f1be6..54e58b4d9 100644
--- a/app/models/concerns/activity_message_handler.rb
+++ b/app/models/concerns/activity_message_handler.rb
@@ -126,3 +126,5 @@ module ActivityMessageHandler
user_name
end
end
+
+ActivityMessageHandler.prepend_mod_with('ActivityMessageHandler')
diff --git a/app/models/concerns/llm_formattable.rb b/app/models/concerns/llm_formattable.rb
index 086ccc46a..0cdc76718 100644
--- a/app/models/concerns/llm_formattable.rb
+++ b/app/models/concerns/llm_formattable.rb
@@ -1,7 +1,7 @@
module LlmFormattable
extend ActiveSupport::Concern
- def to_llm_text
- LlmFormatter::LlmTextFormatterService.new(self).format
+ def to_llm_text(config = {})
+ LlmFormatter::LlmTextFormatterService.new(self).format(config)
end
end
diff --git a/app/models/conversation.rb b/app/models/conversation.rb
index d7efff139..bcb79c05b 100644
--- a/app/models/conversation.rb
+++ b/app/models/conversation.rb
@@ -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
diff --git a/app/models/inbox.rb b/app/models/inbox.rb
index 20c8dc610..f1343a352 100644
--- a/app/models/inbox.rb
+++ b/app/models/inbox.rb
@@ -9,6 +9,7 @@
# auto_assignment_config :jsonb
# business_name :string
# channel_type :string
+# csat_config :jsonb not null
# csat_survey_enabled :boolean default(FALSE)
# email_address :string
# enable_auto_assignment :boolean default(TRUE)
diff --git a/app/models/message.rb b/app/models/message.rb
index 20dad7403..f5d7712d2 100644
--- a/app/models/message.rb
+++ b/app/models/message.rb
@@ -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
diff --git a/app/services/instagram/message_text.rb b/app/services/instagram/message_text.rb
index 66445f5a5..e4b88c64d 100644
--- a/app/services/instagram/message_text.rb
+++ b/app/services/instagram/message_text.rb
@@ -41,10 +41,20 @@ class Instagram::MessageText < Instagram::BaseMessageText
# Access token has expired or become invalid.
channel.authorization_error! if error_code == 190
+ # TODO: Remove this once we have a better way to handle this error.
+ # https://developers.facebook.com/docs/messenger-platform/instagram/features/user-profile/#user-consent
+ # The error typically occurs when the connected Instagram account attempts to send a message to a user
+ # who has never messaged this Instagram account before.
+ # We can only get consent to access a user's profile if they have previously sent a message to the connected Instagram account.
+ # In such cases, we receive the error "User consent is required to access user profile".
+ # We can safely ignore this error.
+ return if error_code == 230
+
Rails.logger.warn("[InstagramUserFetchError]: account_id #{@inbox.account_id} inbox_id #{@inbox.id}")
Rails.logger.warn("[InstagramUserFetchError]: #{error_message} #{error_code}")
- ChatwootExceptionTracker.new(parsed_response, account: @inbox.account).capture_exception
+ exception = StandardError.new("#{error_message} (Code: #{error_code})")
+ ChatwootExceptionTracker.new(exception, account: @inbox.account).capture_exception
end
def base_uri
diff --git a/app/services/llm_formatter/article_llm_formatter.rb b/app/services/llm_formatter/article_llm_formatter.rb
new file mode 100644
index 000000000..5df7976b7
--- /dev/null
+++ b/app/services/llm_formatter/article_llm_formatter.rb
@@ -0,0 +1,22 @@
+class LlmFormatter::ArticleLlmFormatter
+ attr_reader :article
+
+ def initialize(article)
+ @article = article
+ end
+
+ def format(*)
+ <<~TEXT
+ Title: #{article.title}
+ ID: #{article.id}
+ Status: #{article.status}
+ Category: #{article.category&.name || 'Uncategorized'}
+ Author: #{article.author&.name || 'Unknown'}
+ Views: #{article.views}
+ Created At: #{article.created_at}
+ Updated At: #{article.updated_at}
+ Content:
+ #{article.content}
+ TEXT
+ end
+end
diff --git a/app/services/llm_formatter/contact_llm_formatter.rb b/app/services/llm_formatter/contact_llm_formatter.rb
index 9dcfcd299..586f8743b 100644
--- a/app/services/llm_formatter/contact_llm_formatter.rb
+++ b/app/services/llm_formatter/contact_llm_formatter.rb
@@ -1,5 +1,5 @@
class LlmFormatter::ContactLlmFormatter < LlmFormatter::DefaultLlmFormatter
- def format
+ def format(*)
sections = []
sections << "Contact ID: ##{@record.id}"
sections << 'Contact Attributes:'
diff --git a/app/services/llm_formatter/conversation_llm_formatter.rb b/app/services/llm_formatter/conversation_llm_formatter.rb
index 07afc2cac..1444d75c1 100644
--- a/app/services/llm_formatter/conversation_llm_formatter.rb
+++ b/app/services/llm_formatter/conversation_llm_formatter.rb
@@ -1,5 +1,5 @@
class LlmFormatter::ConversationLlmFormatter < LlmFormatter::DefaultLlmFormatter
- def format
+ def format(config = {})
sections = []
sections << "Conversation ID: ##{@record.display_id}"
sections << "Channel: #{@record.inbox.channel.name}"
@@ -10,6 +10,7 @@ class LlmFormatter::ConversationLlmFormatter < LlmFormatter::DefaultLlmFormatter
'No messages in this conversation'
end
+ sections << "Contact Details: #{@record.contact.to_llm_text}" if config[:include_contact_details]
sections.join("\n")
end
diff --git a/app/services/llm_formatter/default_llm_formatter.rb b/app/services/llm_formatter/default_llm_formatter.rb
index b82a039cf..3ac1fa7f4 100644
--- a/app/services/llm_formatter/default_llm_formatter.rb
+++ b/app/services/llm_formatter/default_llm_formatter.rb
@@ -3,7 +3,7 @@ class LlmFormatter::DefaultLlmFormatter
@record = record
end
- def format
+ def format(*)
# override this
end
end
diff --git a/app/services/llm_formatter/llm_text_formatter_service.rb b/app/services/llm_formatter/llm_text_formatter_service.rb
index 0f1f37cf8..198f09d58 100644
--- a/app/services/llm_formatter/llm_text_formatter_service.rb
+++ b/app/services/llm_formatter/llm_text_formatter_service.rb
@@ -3,9 +3,9 @@ class LlmFormatter::LlmTextFormatterService
@record = record
end
- def format
+ def format(config = {})
formatter_class = find_formatter
- formatter_class.new(@record).format
+ formatter_class.new(@record).format(config)
end
private
diff --git a/app/services/message_templates/template/csat_survey.rb b/app/services/message_templates/template/csat_survey.rb
index 4171367c7..3a7ca2605 100644
--- a/app/services/message_templates/template/csat_survey.rb
+++ b/app/services/message_templates/template/csat_survey.rb
@@ -2,6 +2,8 @@ class MessageTemplates::Template::CsatSurvey
pattr_initialize [:conversation!]
def perform
+ return unless should_send_csat_survey?
+
ActiveRecord::Base.transaction do
conversation.messages.create!(csat_survey_message_params)
end
@@ -9,8 +11,47 @@ class MessageTemplates::Template::CsatSurvey
private
- delegate :contact, :account, to: :conversation
- delegate :inbox, to: :message
+ delegate :contact, :account, :inbox, to: :conversation
+ delegate :csat_config, to: :inbox
+
+ def should_send_csat_survey?
+ return true unless survey_rules_configured?
+
+ labels = conversation.label_list
+
+ return true if rule_values.empty?
+
+ case rule_operator
+ when 'contains'
+ rule_values.any? { |label| labels.include?(label) }
+ when 'does_not_contain'
+ rule_values.none? { |label| labels.include?(label) }
+ else
+ true
+ end
+ end
+
+ def survey_rules_configured?
+ return false if csat_config.blank?
+ return false if csat_config['survey_rules'].blank?
+ return false if rule_values.empty?
+
+ true
+ end
+
+ def rule_operator
+ csat_config.dig('survey_rules', 'operator') || 'contains'
+ end
+
+ def rule_values
+ csat_config.dig('survey_rules', 'values') || []
+ end
+
+ def message_content
+ return I18n.t('conversations.templates.csat_input_message_body') if csat_config.blank? || csat_config['message'].blank?
+
+ csat_config['message']
+ end
def csat_survey_message_params
{
@@ -18,7 +59,18 @@ class MessageTemplates::Template::CsatSurvey
inbox_id: @conversation.inbox_id,
message_type: :template,
content_type: :input_csat,
- content: I18n.t('conversations.templates.csat_input_message_body')
+ content: message_content,
+ content_attributes: content_attributes
+ }
+ end
+
+ def csat_config
+ inbox.csat_config || {}
+ end
+
+ def content_attributes
+ {
+ display_type: csat_config['display_type'] || 'emoji'
}
end
end
diff --git a/app/services/twilio/incoming_message_service.rb b/app/services/twilio/incoming_message_service.rb
index 5c5854ee6..7a335c87d 100644
--- a/app/services/twilio/incoming_message_service.rb
+++ b/app/services/twilio/incoming_message_service.rb
@@ -106,15 +106,22 @@ class Twilio::IncomingMessageService
end
def attach_files
- return if params[:MediaUrl0].blank?
+ num_media = params[:NumMedia].to_i
+ return if num_media.zero?
- attachment_file = download_attachment_file
+ num_media.times do |i|
+ media_url = params[:"MediaUrl#{i}"]
+ attach_single_file(media_url) if media_url.present?
+ end
+ end
+ def attach_single_file(media_url)
+ attachment_file = download_attachment_file(media_url)
return if attachment_file.blank?
@message.attachments.new(
account_id: @message.account_id,
- file_type: file_type(params[:MediaContentType0]),
+ file_type: file_type(attachment_file.content_type),
file: {
io: attachment_file,
filename: attachment_file.original_filename,
@@ -123,24 +130,22 @@ class Twilio::IncomingMessageService
)
end
- def download_attachment_file
- download_with_auth
+ def download_attachment_file(media_url)
+ download_with_auth(media_url)
rescue Down::Error, Down::ClientError => e
- handle_download_attachment_error(e)
+ handle_download_attachment_error(e, media_url)
end
- def download_with_auth
+ def download_with_auth(media_url)
Down.download(
- params[:MediaUrl0],
- # https://support.twilio.com/hc/en-us/articles/223183748-Protect-Media-Access-with-HTTP-Basic-Authentication-for-Programmable-Messaging
+ media_url,
http_basic_authentication: [twilio_channel.account_sid, twilio_channel.auth_token || twilio_channel.api_key_sid]
)
end
- # This is just a temporary workaround since some users have not yet enabled media protection. We will remove this in the future.
- def handle_download_attachment_error(error)
+ def handle_download_attachment_error(error, media_url)
Rails.logger.info "Error downloading attachment from Twilio: #{error.message}: Retrying"
- Down.download(params[:MediaUrl0])
+ Down.download(media_url)
rescue StandardError => e
Rails.logger.info "Error downloading attachment from Twilio: #{e.message}: Skipping"
nil
diff --git a/app/views/api/v1/models/_inbox.json.jbuilder b/app/views/api/v1/models/_inbox.json.jbuilder
index 42a1a7370..c66a6eb78 100644
--- a/app/views/api/v1/models/_inbox.json.jbuilder
+++ b/app/views/api/v1/models/_inbox.json.jbuilder
@@ -8,6 +8,7 @@ json.greeting_message resource.greeting_message
json.working_hours_enabled resource.working_hours_enabled
json.enable_email_collect resource.enable_email_collect
json.csat_survey_enabled resource.csat_survey_enabled
+json.csat_config resource.csat_config
json.enable_auto_assignment resource.enable_auto_assignment
json.auto_assignment_config resource.auto_assignment_config
json.out_of_office_message resource.out_of_office_message
@@ -62,6 +63,10 @@ json.instagram_id resource.channel.try(:instagram_id) if resource.instagram?
json.messaging_service_sid resource.channel.try(:messaging_service_sid)
json.phone_number resource.channel.try(:phone_number)
json.medium resource.channel.try(:medium) if resource.twilio?
+if resource.twilio? && Current.account_user&.administrator?
+ json.auth_token resource.channel.try(:auth_token)
+ json.account_sid resource.channel.try(:account_sid)
+end
if resource.email?
## Email Channel Attributes
diff --git a/app/views/public/api/v1/models/_csat_survey.json.jbuilder b/app/views/public/api/v1/models/_csat_survey.json.jbuilder
index 134cb1af1..c5206e91b 100644
--- a/app/views/public/api/v1/models/_csat_survey.json.jbuilder
+++ b/app/views/public/api/v1/models/_csat_survey.json.jbuilder
@@ -1,5 +1,7 @@
json.id resource.id
json.csat_survey_response resource.csat_survey_response
+json.display_type resource.inbox.csat_config.try(:[], 'display_type') || 'emoji'
+json.content resource.inbox.csat_config.try(:[], 'message')
json.inbox_avatar_url resource.inbox.avatar_url
json.inbox_name resource.inbox.name
json.locale resource.account.locale
diff --git a/config/locales/am.yml b/config/locales/am.yml
index 1e06aebde..4cc42a9dc 100644
--- a/config/locales/am.yml
+++ b/config/locales/am.yml
@@ -132,6 +132,8 @@ am:
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.
@@ -139,6 +141,9 @@ am:
delivery_status:
error_code: 'Error code: %{error_code}'
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}'
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index 1394d927c..5006a675a 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -132,6 +132,8 @@ ar:
attachment: 'المرفقات'
no_content: 'لا يوجد محتوى'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: 'أشار %{story_sender} إليك في القصة: '
instagram_deleted_story_content: هذه القصة لم تعد متاحة.
@@ -139,6 +141,9 @@ ar:
delivery_status:
error_code: 'رمز الخطأ: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'تم تحديث حالة المحادثة لـ"مغلقة" بواسطة %{user_name}'
contact_resolved: 'تم حل المحادثة بواسطة %{contact_name}'
diff --git a/config/locales/az.yml b/config/locales/az.yml
index 2375edafb..6880539e4 100644
--- a/config/locales/az.yml
+++ b/config/locales/az.yml
@@ -132,6 +132,8 @@ az:
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.
@@ -139,6 +141,9 @@ az:
delivery_status:
error_code: 'Error code: %{error_code}'
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}'
diff --git a/config/locales/bg.yml b/config/locales/bg.yml
index 18fca0c3b..f51cee634 100644
--- a/config/locales/bg.yml
+++ b/config/locales/bg.yml
@@ -132,6 +132,8 @@ bg:
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.
@@ -139,6 +141,9 @@ bg:
delivery_status:
error_code: 'Error code: %{error_code}'
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}'
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 25fdecfe1..040671ea3 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -132,6 +132,8 @@ ca:
attachment: 'Adjunt'
no_content: 'Sense contingut'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} t''ha mencionat a la història: '
instagram_deleted_story_content: Aquesta història ja no està disponible.
@@ -139,6 +141,9 @@ ca:
delivery_status:
error_code: 'Codi d''error: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'La conversa va ser marcada com resolta per %{user_name}'
contact_resolved: 'La conversa va ser resolta per %{contact_name}'
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index 890aefe25..679fb4094 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -132,6 +132,8 @@ cs:
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.
@@ -139,6 +141,9 @@ cs:
delivery_status:
error_code: 'Error code: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'Konverzace byla vyřešena uživatelem %{user_name}'
contact_resolved: 'Conversation was resolved by %{contact_name}'
diff --git a/config/locales/da.yml b/config/locales/da.yml
index 0b03be9f6..1e694f673 100644
--- a/config/locales/da.yml
+++ b/config/locales/da.yml
@@ -132,6 +132,8 @@ da:
attachment: 'Attachment'
no_content: 'No content'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} nævnte dig i historien: '
instagram_deleted_story_content: Denne historie er ikke længere tilgængelig.
@@ -139,6 +141,9 @@ da:
delivery_status:
error_code: 'Error code: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'Samtalen blev markeret som løst af %{user_name}'
contact_resolved: 'Samtalen blev løst af %{contact_name}'
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 584092e32..5d7126947 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -132,6 +132,8 @@ de:
attachment: 'Anhang'
no_content: 'Kein Inhalt'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} erwähnte sie in der Geschichte: '
instagram_deleted_story_content: Diese Geschichte ist nicht mehr verfügbar.
@@ -139,6 +141,9 @@ de:
delivery_status:
error_code: 'Fehlercode: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'Das Gespräch wurde von %{user_name} gelöst'
contact_resolved: 'Konversation wurde von %{contact_name} gelöst'
diff --git a/config/locales/el.yml b/config/locales/el.yml
index 53e2828f5..c02723175 100644
--- a/config/locales/el.yml
+++ b/config/locales/el.yml
@@ -132,6 +132,8 @@ el:
attachment: 'Attachment'
no_content: 'No content'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: 'Ο %{story_sender} σας ανέφερε στην ιστορία: '
instagram_deleted_story_content: Η ιστορία δεν είναι πλέον διαθέσιμη.
@@ -139,6 +141,9 @@ el:
delivery_status:
error_code: 'Error code: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'Η συνομιλία έχει επιλυθεί από τον %{user_name}'
contact_resolved: 'Η συνομιλία επιλύθηκε από τον %{contact_name}'
diff --git a/config/locales/en.yml b/config/locales/en.yml
index c5138381c..2a2e73d91 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -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.
@@ -153,6 +155,9 @@ en:
delivery_status:
error_code: 'Error code: %{error_code}'
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}'
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 74dbc5c6a..22c216497 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -132,6 +132,8 @@ es:
attachment: 'Adjunto'
no_content: 'Sin contenido'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} te mencionó en la historia: '
instagram_deleted_story_content: Esta historia ya no está disponible.
@@ -139,6 +141,9 @@ es:
delivery_status:
error_code: 'Código de error: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'La conversación fue marcada por %{user_name}'
contact_resolved: 'Conversación fue resuelta por %{contact_name}'
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index 1f20e4e13..7bdab157e 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -132,6 +132,8 @@ fa:
attachment: 'پیوست'
no_content: 'فاقد محتوا'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} در داستان به شما اشاره کرده: '
instagram_deleted_story_content: این داستان دیگر در دسترس نیست.
@@ -139,6 +141,9 @@ fa:
delivery_status:
error_code: 'کد خطا " %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'مکالمه توسط ایجنت %{user_name} حل شده، اعلام شده بود'
contact_resolved: 'گفتگو توسط %{contact_name} حل شد'
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index 34afde79b..2500e52f1 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -132,6 +132,8 @@ fi:
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.
@@ -139,6 +141,9 @@ fi:
delivery_status:
error_code: 'Error code: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: '%{user_name} merkitsi keskustelun ratkaistuksi'
contact_resolved: 'Conversation was resolved by %{contact_name}'
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 7994df163..6ed711159 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -132,6 +132,8 @@ fr:
attachment: 'Attachment'
no_content: 'No content'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} vous a mentionné dans la story: '
instagram_deleted_story_content: Cette Story n'est plus disponible.
@@ -139,6 +141,9 @@ fr:
delivery_status:
error_code: 'Code d''erreur : %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'La conversation a été marquée résolue par %{user_name}'
contact_resolved: 'La conversation a été résolue par %{contact_name}'
diff --git a/config/locales/he.yml b/config/locales/he.yml
index fb0eba4e8..0c262657e 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -132,6 +132,8 @@ he:
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.
@@ -139,6 +141,9 @@ he:
delivery_status:
error_code: 'Error code: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'השיחה סומנה כפתורה על ידי %{user_name}'
contact_resolved: 'השיחה נפתרה על ידי %{contact_name}'
diff --git a/config/locales/hi.yml b/config/locales/hi.yml
index 5cd91f77f..c5188bcdd 100644
--- a/config/locales/hi.yml
+++ b/config/locales/hi.yml
@@ -132,6 +132,8 @@ hi:
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.
@@ -139,6 +141,9 @@ hi:
delivery_status:
error_code: 'Error code: %{error_code}'
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}'
diff --git a/config/locales/hr.yml b/config/locales/hr.yml
index 45bda80be..de6d9ce19 100644
--- a/config/locales/hr.yml
+++ b/config/locales/hr.yml
@@ -132,6 +132,8 @@ hr:
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.
@@ -139,6 +141,9 @@ hr:
delivery_status:
error_code: 'Error code: %{error_code}'
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}'
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index 7b9ce93a2..11836274c 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -132,6 +132,8 @@ hu:
attachment: 'Attachment'
no_content: 'No content'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} megemlített egy storyban: '
instagram_deleted_story_content: Ez a story már nem érhető el.
@@ -139,6 +141,9 @@ hu:
delivery_status:
error_code: 'Hibakód: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'A beszélgetést lezárta %{user_name}'
contact_resolved: 'A beszélgetést megoldottra állította: %{contact_name}'
diff --git a/config/locales/hy.yml b/config/locales/hy.yml
index cd4dcb783..366767c00 100644
--- a/config/locales/hy.yml
+++ b/config/locales/hy.yml
@@ -132,6 +132,8 @@ hy:
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.
@@ -139,6 +141,9 @@ hy:
delivery_status:
error_code: 'Error code: %{error_code}'
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}'
diff --git a/config/locales/id.yml b/config/locales/id.yml
index 89dce8175..3415eb77d 100644
--- a/config/locales/id.yml
+++ b/config/locales/id.yml
@@ -132,6 +132,8 @@ id:
attachment: 'Attachment'
no_content: 'No content'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} menyebutmu dalam story: '
instagram_deleted_story_content: Story ini tidak lagi tersedia.
@@ -139,6 +141,9 @@ id:
delivery_status:
error_code: 'Error code: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'Percakapan ditandai selesai oleh %{user_name}'
contact_resolved: 'Percakapan diselesaikan oleh %{contact_name}'
diff --git a/config/locales/is.yml b/config/locales/is.yml
index a234cf915..cecf1847c 100644
--- a/config/locales/is.yml
+++ b/config/locales/is.yml
@@ -132,6 +132,8 @@ is:
attachment: 'Attachment'
no_content: 'No content'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} minntist á þig í sögunni: '
instagram_deleted_story_content: This story is no longer available.
@@ -139,6 +141,9 @@ is:
delivery_status:
error_code: 'Error code: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'Samtal var merkt sem leyst af %{user_name}'
contact_resolved: 'Samtal var leyst af %{contact_name}'
diff --git a/config/locales/it.yml b/config/locales/it.yml
index c1cf9aa08..9900d2dba 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -132,6 +132,8 @@ it:
attachment: 'Attachment'
no_content: 'No content'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} ti ha menzionato nella storia: '
instagram_deleted_story_content: Questa storia non è più disponibile.
@@ -139,6 +141,9 @@ it:
delivery_status:
error_code: 'Error code: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'La conversazione è stata contrassegnata come risolta da %{user_name}'
contact_resolved: 'La conversazione è stata risolta da %{contact_name}'
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 82ff3adf1..9eac026d3 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -132,6 +132,8 @@ ja:
attachment: '添付ファイル'
no_content: 'コンテンツなし'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} さんがストーリーであなたについて言及しました: '
instagram_deleted_story_content: このストーリーはもう利用できません。
@@ -139,6 +141,9 @@ ja:
delivery_status:
error_code: 'エラーコード: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: '%{user_name} によって会話は解決済みになりました'
contact_resolved: '%{contact_name} によって会話が解決されました'
diff --git a/config/locales/ka.yml b/config/locales/ka.yml
index ad0cd5159..9bda03fc5 100644
--- a/config/locales/ka.yml
+++ b/config/locales/ka.yml
@@ -132,6 +132,8 @@ ka:
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.
@@ -139,6 +141,9 @@ ka:
delivery_status:
error_code: 'Error code: %{error_code}'
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}'
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index 42417b179..547c5b7ed 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -132,6 +132,8 @@ ko:
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.
@@ -139,6 +141,9 @@ ko:
delivery_status:
error_code: 'Error code: %{error_code}'
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}'
diff --git a/config/locales/lt.yml b/config/locales/lt.yml
index 00de10987..0c9f0b051 100644
--- a/config/locales/lt.yml
+++ b/config/locales/lt.yml
@@ -132,6 +132,8 @@ lt:
attachment: 'Attachment'
no_content: 'No content'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} paminėjo jus pasakojime: '
instagram_deleted_story_content: Šis pasakojimas nebepasiekiamas.
@@ -139,6 +141,9 @@ lt:
delivery_status:
error_code: 'Klaidos kodas: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'Pokalbį pažymėjo %{user_name} kaip baigtą'
contact_resolved: 'Pokalbį užbaigė %{contact_name}'
diff --git a/config/locales/lv.yml b/config/locales/lv.yml
index 3588f491c..a41e2f593 100644
--- a/config/locales/lv.yml
+++ b/config/locales/lv.yml
@@ -132,6 +132,8 @@ lv:
attachment: 'Pielikums'
no_content: 'Nav satura'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} pieminēja jūs stāstā: '
instagram_deleted_story_content: Šis stāsts vairs nav pieejams.
@@ -139,6 +141,9 @@ lv:
delivery_status:
error_code: 'Kļūdas kods: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: '%{user_name} sarunu atzīmēja kā atrisinātu'
contact_resolved: '%{contact_name} atrisināja sarunu'
diff --git a/config/locales/ml.yml b/config/locales/ml.yml
index 676cf8624..d7621c8ba 100644
--- a/config/locales/ml.yml
+++ b/config/locales/ml.yml
@@ -132,6 +132,8 @@ ml:
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.
@@ -139,6 +141,9 @@ ml:
delivery_status:
error_code: 'Error code: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'സംഭാഷണം %{user_name} പരിഹരിച്ചതായി അടയാളപ്പെടുത്തി'
contact_resolved: 'Conversation was resolved by %{contact_name}'
diff --git a/config/locales/ms.yml b/config/locales/ms.yml
index d471144b5..dbb936e52 100644
--- a/config/locales/ms.yml
+++ b/config/locales/ms.yml
@@ -132,6 +132,8 @@ ms:
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.
@@ -139,6 +141,9 @@ ms:
delivery_status:
error_code: 'Error code: %{error_code}'
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}'
diff --git a/config/locales/ne.yml b/config/locales/ne.yml
index 664cb7cb1..93b4466cc 100644
--- a/config/locales/ne.yml
+++ b/config/locales/ne.yml
@@ -132,6 +132,8 @@ ne:
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.
@@ -139,6 +141,9 @@ ne:
delivery_status:
error_code: 'Error code: %{error_code}'
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}'
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index c5c937fc3..de586e765 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -132,6 +132,8 @@ nl:
attachment: 'Attachment'
no_content: 'No content'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} heeft je genoemd in het verhaal: '
instagram_deleted_story_content: Dit verhaal is niet meer beschikbaar.
@@ -139,6 +141,9 @@ nl:
delivery_status:
error_code: 'Error code: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'Gesprek werd gemarkeerd door %{user_name}'
contact_resolved: 'Gesprek werd opgelost door %{contact_name}'
diff --git a/config/locales/no.yml b/config/locales/no.yml
index ef2327236..e2820c751 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -132,6 +132,8 @@
attachment: 'Vedlegg'
no_content: 'Ingen innhold'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} nevnte deg i historien: '
instagram_deleted_story_content: Denne historien er ikke lenger tilgjengelig.
@@ -139,6 +141,9 @@
delivery_status:
error_code: 'Feilkode: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'Samtale ble løst av %{user_name}'
contact_resolved: 'Conversation was resolved by %{contact_name}'
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 4d3878d45..1fbdb0fa5 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -132,6 +132,8 @@ pl:
attachment: 'Attachment'
no_content: 'No content'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} wspomniał o Tobie w historii: '
instagram_deleted_story_content: Ta historia już nie jest dostępna.
@@ -139,6 +141,9 @@ pl:
delivery_status:
error_code: 'Error code: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'Rozmowa została oznaczona przez %{user_name}'
contact_resolved: 'Rozmowa została rozwiązana przez %{contact_name}'
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index fe2286502..a6b040590 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -132,6 +132,8 @@ pt:
attachment: 'Anexo'
no_content: 'Sem conteúdo'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} mencionou você na história: '
instagram_deleted_story_content: Esta história já não está disponível.
@@ -139,6 +141,9 @@ pt:
delivery_status:
error_code: 'Código de erro: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'Conversa foi marcada como resolvida por %{user_name}'
contact_resolved: 'Conversa foi resolvida por %{contact_name}'
diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml
index 601f689ac..22215511c 100644
--- a/config/locales/pt_BR.yml
+++ b/config/locales/pt_BR.yml
@@ -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} por 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.'
diff --git a/config/locales/ro.yml b/config/locales/ro.yml
index 48e03ea29..4b58d0e1d 100644
--- a/config/locales/ro.yml
+++ b/config/locales/ro.yml
@@ -132,6 +132,8 @@ ro:
attachment: 'Attachment'
no_content: 'No content'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} menționat în poveste: '
instagram_deleted_story_content: Această poveste nu mai este disponibilă.
@@ -139,6 +141,9 @@ ro:
delivery_status:
error_code: 'Error code: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'Conversația a fost marcată de %{user_name}'
contact_resolved: 'Conversația a fost rezolvată de %{contact_name}'
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index f219e75c6..b91b036e8 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -132,6 +132,8 @@ ru:
attachment: 'Вложение'
no_content: 'Нет содержимого'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} упомянул Вас в истории: '
instagram_deleted_story_content: Эта история больше недоступна.
@@ -139,6 +141,9 @@ ru:
delivery_status:
error_code: 'Код ошибки: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: '%{user_name} завершил диалог'
contact_resolved: 'Разговор был закрыт %{contact_name}'
diff --git a/config/locales/sh.yml b/config/locales/sh.yml
index cdfb561c5..967b5840d 100644
--- a/config/locales/sh.yml
+++ b/config/locales/sh.yml
@@ -132,6 +132,8 @@ sh:
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.
@@ -139,6 +141,9 @@ sh:
delivery_status:
error_code: 'Error code: %{error_code}'
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}'
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index ff7de2de9..71a54c135 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -132,6 +132,8 @@ sk:
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.
@@ -139,6 +141,9 @@ sk:
delivery_status:
error_code: 'Error code: %{error_code}'
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}'
diff --git a/config/locales/sl.yml b/config/locales/sl.yml
index 3d9d9746c..f040afbc1 100644
--- a/config/locales/sl.yml
+++ b/config/locales/sl.yml
@@ -132,6 +132,8 @@ sl:
attachment: 'Priponka'
no_content: 'Ni vsebine'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} vas je omenil v zgodbi: '
instagram_deleted_story_content: Ta zgodba ni več na voljo.
@@ -139,6 +141,9 @@ sl:
delivery_status:
error_code: 'Koda napake: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: '%{user_name} je pogovor označil za rešenega'
contact_resolved: 'Pogovor je razrešil %{contact_name}'
diff --git a/config/locales/sq.yml b/config/locales/sq.yml
index d8e0cc689..7f5466eee 100644
--- a/config/locales/sq.yml
+++ b/config/locales/sq.yml
@@ -132,6 +132,8 @@ sq:
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.
@@ -139,6 +141,9 @@ sq:
delivery_status:
error_code: 'Error code: %{error_code}'
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}'
diff --git a/config/locales/sr.yml b/config/locales/sr.yml
index 30c0a8f63..32c33a279 100644
--- a/config/locales/sr.yml
+++ b/config/locales/sr.yml
@@ -132,6 +132,8 @@ sr-Latn:
attachment: 'Attachment'
no_content: 'No content'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} vas je pomenuo u priči: '
instagram_deleted_story_content: Ova priča više nije dostupna.
@@ -139,6 +141,9 @@ sr-Latn:
delivery_status:
error_code: 'Error code: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'Razgovor je označen kao rešen od strane %{user_name}'
contact_resolved: 'Razgovor je rešen od strane %{contact_name}'
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index 3cb09f24e..944d9fa99 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -132,6 +132,8 @@ sv:
attachment: 'Bilaga'
no_content: 'Inget innehåll'
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.
@@ -139,6 +141,9 @@ sv:
delivery_status:
error_code: 'Felkod: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'Konversationen har markerats som löst av %{user_name}'
contact_resolved: 'Conversation was resolved by %{contact_name}'
diff --git a/config/locales/ta.yml b/config/locales/ta.yml
index 0f2fb6b40..dcd0b947f 100644
--- a/config/locales/ta.yml
+++ b/config/locales/ta.yml
@@ -132,6 +132,8 @@ ta:
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.
@@ -139,6 +141,9 @@ ta:
delivery_status:
error_code: 'Error code: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'உரையாடலுக்கு %{user_name} தீர்வு வழங்கியுள்ளார்'
contact_resolved: 'Conversation was resolved by %{contact_name}'
diff --git a/config/locales/th.yml b/config/locales/th.yml
index d5b9d9ab3..1a8def1e7 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -132,6 +132,8 @@ th:
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.
@@ -139,6 +141,9 @@ th:
delivery_status:
error_code: 'Error code: %{error_code}'
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}'
diff --git a/config/locales/tl.yml b/config/locales/tl.yml
index 11a71c04f..102cb8461 100644
--- a/config/locales/tl.yml
+++ b/config/locales/tl.yml
@@ -132,6 +132,8 @@ tl:
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.
@@ -139,6 +141,9 @@ tl:
delivery_status:
error_code: 'Error code: %{error_code}'
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}'
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index 27fc93e35..d27ab3123 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -132,6 +132,8 @@ tr:
attachment: 'Attachment'
no_content: 'No content'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} hikayesinde senden bahsetti: '
instagram_deleted_story_content: Bu hikaye artık mevcut değil.
@@ -139,6 +141,9 @@ tr:
delivery_status:
error_code: 'Hata kodu: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'Konuşma %{user_name} tarafından çözümlendi olarak işaretlendi'
contact_resolved: 'Konuşma %{contact_name} tarafından çözümlendi olarak işaretlendi'
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 999508e12..211d11f2c 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -132,6 +132,8 @@ uk:
attachment: 'Вкладення'
no_content: 'Немає вмісту'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} згадав вас у сторіс: '
instagram_deleted_story_content: Ця історія більше не доступна.
@@ -139,6 +141,9 @@ uk:
delivery_status:
error_code: 'Код помилки: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'Розмова була відмічена як вирішена %{user_name}'
contact_resolved: 'Діалог був закритий %{contact_name}'
diff --git a/config/locales/ur.yml b/config/locales/ur.yml
index 6da50915c..3b1c2dceb 100644
--- a/config/locales/ur.yml
+++ b/config/locales/ur.yml
@@ -132,6 +132,8 @@ ur:
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.
@@ -139,6 +141,9 @@ ur:
delivery_status:
error_code: 'Error code: %{error_code}'
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}'
diff --git a/config/locales/ur_IN.yml b/config/locales/ur_IN.yml
index 75316ea8b..c139fe327 100644
--- a/config/locales/ur_IN.yml
+++ b/config/locales/ur_IN.yml
@@ -132,6 +132,8 @@ ur:
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.
@@ -139,6 +141,9 @@ ur:
delivery_status:
error_code: 'Error code: %{error_code}'
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}'
diff --git a/config/locales/vi.yml b/config/locales/vi.yml
index 56c36eb37..15dd53c0d 100644
--- a/config/locales/vi.yml
+++ b/config/locales/vi.yml
@@ -132,6 +132,8 @@ vi:
attachment: 'Attachment'
no_content: 'No content'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} đã đề cập đến bạn trong hội thoại: '
instagram_deleted_story_content: Hội thoại này không còn nữa.
@@ -139,6 +141,9 @@ vi:
delivery_status:
error_code: 'Error code: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: 'Cuộc trò chuyện được đánh dấu là đã giải quyết bởi %{user_name}'
contact_resolved: 'Hội thoại đã được giải quyết bởi %{contact_name}'
diff --git a/config/locales/zh_CN.yml b/config/locales/zh_CN.yml
index 7173af8c3..4a6be15c2 100644
--- a/config/locales/zh_CN.yml
+++ b/config/locales/zh_CN.yml
@@ -132,6 +132,8 @@ zh_CN:
attachment: '附件'
no_content: '无内容'
conversations:
+ captain:
+ handoff: 'Transferring to another agent for further assistance.'
messages:
instagram_story_content: '%{story_sender} 会话中提到了你: '
instagram_deleted_story_content: 本信息不存在
@@ -139,6 +141,9 @@ zh_CN:
delivery_status:
error_code: '错误代码: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: '对话被标记由 %{user_name} 解决'
contact_resolved: '对话被 %{contact_name} 重新打开'
diff --git a/config/locales/zh_TW.yml b/config/locales/zh_TW.yml
index b76f8926a..9a248dc4b 100644
--- a/config/locales/zh_TW.yml
+++ b/config/locales/zh_TW.yml
@@ -132,6 +132,8 @@ zh_TW:
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.
@@ -139,6 +141,9 @@ zh_TW:
delivery_status:
error_code: 'Error code: %{error_code}'
activity:
+ captain:
+ resolved: 'Conversation was marked resolved by %{user_name} due to inactivity'
+ open: 'Conversation was marked open by %{user_name}'
status:
resolved: '被%{user_name}標記的對話已解決。'
contact_resolved: 'Conversation was resolved by %{contact_name}'
diff --git a/config/routes.rb b/config/routes.rb
index c623ff053..ee6ec5e8a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -58,9 +58,12 @@ Rails.application.routes.draw do
end
resources :inboxes, only: [:index, :create, :destroy], param: :inbox_id
end
- resources :documents, only: [:index, :show, :create, :destroy]
resources :assistant_responses
resources :bulk_actions, only: [:create]
+ resources :copilot_threads, only: [:index] do
+ resources :copilot_messages, only: [:index]
+ end
+ resources :documents, only: [:index, :show, :create, :destroy]
end
resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do
delete :avatar, on: :member
diff --git a/db/migrate/20250512231036_create_copilot_threads.rb b/db/migrate/20250512231036_create_copilot_threads.rb
new file mode 100644
index 000000000..68686e066
--- /dev/null
+++ b/db/migrate/20250512231036_create_copilot_threads.rb
@@ -0,0 +1,14 @@
+class CreateCopilotThreads < ActiveRecord::Migration[7.0]
+ def change
+ create_table :copilot_threads do |t|
+ t.string :title, null: false
+ t.references :user, null: false, index: true
+ t.references :account, null: false, index: true
+ t.uuid :uuid, null: false, default: 'gen_random_uuid()'
+
+ t.timestamps
+ end
+
+ add_index :copilot_threads, :uuid, unique: true
+ end
+end
diff --git a/db/migrate/20250512231037_create_copilot_messages.rb b/db/migrate/20250512231037_create_copilot_messages.rb
new file mode 100644
index 000000000..fd03cc9f8
--- /dev/null
+++ b/db/migrate/20250512231037_create_copilot_messages.rb
@@ -0,0 +1,13 @@
+class CreateCopilotMessages < ActiveRecord::Migration[7.0]
+ def change
+ create_table :copilot_messages do |t|
+ t.references :copilot_thread, null: false, index: true
+ t.references :user, null: false, index: true
+ t.references :account, null: false, index: true
+ t.string :message_type, null: false
+ t.jsonb :message, null: false, default: {}
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20250514045638_add_csat_config_to_inboxes.rb b/db/migrate/20250514045638_add_csat_config_to_inboxes.rb
new file mode 100644
index 000000000..bb4fca964
--- /dev/null
+++ b/db/migrate/20250514045638_add_csat_config_to_inboxes.rb
@@ -0,0 +1,5 @@
+class AddCsatConfigToInboxes < ActiveRecord::Migration[7.0]
+ def change
+ add_column :inboxes, :csat_config, :jsonb, default: {}, null: false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index d858899ab..9c78acd33 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2025_04_21_085134) do
+ActiveRecord::Schema[7.0].define(version: 2025_05_14_045638) do
# These extensions should be enabled to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -575,6 +575,31 @@ ActiveRecord::Schema[7.0].define(version: 2025_04_21_085134) do
t.index ["waiting_since"], name: "index_conversations_on_waiting_since"
end
+ create_table "copilot_messages", force: :cascade do |t|
+ t.bigint "copilot_thread_id", null: false
+ t.bigint "user_id", null: false
+ t.bigint "account_id", null: false
+ t.string "message_type", null: false
+ t.jsonb "message", default: {}, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["account_id"], name: "index_copilot_messages_on_account_id"
+ t.index ["copilot_thread_id"], name: "index_copilot_messages_on_copilot_thread_id"
+ t.index ["user_id"], name: "index_copilot_messages_on_user_id"
+ end
+
+ create_table "copilot_threads", force: :cascade do |t|
+ t.string "title", null: false
+ t.bigint "user_id", null: false
+ t.bigint "account_id", null: false
+ t.uuid "uuid", default: -> { "gen_random_uuid()" }, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["account_id"], name: "index_copilot_threads_on_account_id"
+ t.index ["user_id"], name: "index_copilot_threads_on_user_id"
+ t.index ["uuid"], name: "index_copilot_threads_on_uuid", unique: true
+ end
+
create_table "csat_survey_responses", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "conversation_id", null: false
@@ -704,6 +729,7 @@ ActiveRecord::Schema[7.0].define(version: 2025_04_21_085134) do
t.bigint "portal_id"
t.integer "sender_name_type", default: 0, null: false
t.string "business_name"
+ t.jsonb "csat_config", default: {}, null: false
t.index ["account_id"], name: "index_inboxes_on_account_id"
t.index ["channel_id", "channel_type"], name: "index_inboxes_on_channel_id_and_channel_type"
t.index ["portal_id"], name: "index_inboxes_on_portal_id"
diff --git a/enterprise/app/controllers/api/v1/accounts/captain/copilot_messages_controller.rb b/enterprise/app/controllers/api/v1/accounts/captain/copilot_messages_controller.rb
new file mode 100644
index 000000000..2a30fba48
--- /dev/null
+++ b/enterprise/app/controllers/api/v1/accounts/captain/copilot_messages_controller.rb
@@ -0,0 +1,25 @@
+class Api::V1::Accounts::Captain::CopilotMessagesController < Api::V1::Accounts::BaseController
+ before_action :current_account
+ before_action -> { check_authorization(Captain::Assistant) }
+ before_action :set_copilot_thread
+
+ def index
+ @copilot_messages = @copilot_thread
+ .copilot_messages
+ .order(created_at: :asc)
+ .page(permitted_params[:page] || 1)
+ .per(1000)
+ end
+
+ private
+
+ def set_copilot_thread
+ @copilot_thread = Current.account.copilot_threads.find_by!(
+ uuid: params[:copilot_thread_id], user_id: Current.user.id
+ )
+ end
+
+ def permitted_params
+ params.permit(:page)
+ end
+end
diff --git a/enterprise/app/controllers/api/v1/accounts/captain/copilot_threads_controller.rb b/enterprise/app/controllers/api/v1/accounts/captain/copilot_threads_controller.rb
new file mode 100644
index 000000000..e313f448c
--- /dev/null
+++ b/enterprise/app/controllers/api/v1/accounts/captain/copilot_threads_controller.rb
@@ -0,0 +1,19 @@
+class Api::V1::Accounts::Captain::CopilotThreadsController < Api::V1::Accounts::BaseController
+ before_action :current_account
+ before_action -> { check_authorization(Captain::Assistant) }
+
+ def index
+ @copilot_threads = Current.account.copilot_threads
+ .where(user_id: Current.user.id)
+ .includes(:user)
+ .order(created_at: :desc)
+ .page(permitted_params[:page] || 1)
+ .per(5)
+ end
+
+ private
+
+ def permitted_params
+ params.permit(:page)
+ end
+end
diff --git a/enterprise/app/jobs/captain/conversation/response_builder_job.rb b/enterprise/app/jobs/captain/conversation/response_builder_job.rb
index 5bc9defa4..39a12b662 100644
--- a/enterprise/app/jobs/captain/conversation/response_builder_job.rb
+++ b/enterprise/app/jobs/captain/conversation/response_builder_job.rb
@@ -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
diff --git a/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb b/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb
index 37a2729ec..d3f1f5d96 100644
--- a/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb
+++ b/enterprise/app/jobs/captain/inbox_pending_conversations_resolution_job.rb
@@ -2,19 +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!
end
end
end
diff --git a/enterprise/app/models/captain/assistant.rb b/enterprise/app/models/captain/assistant.rb
index 4592ead1a..ad441ec28 100644
--- a/enterprise/app/models/captain/assistant.rb
+++ b/enterprise/app/models/captain/assistant.rb
@@ -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,36 @@ class Captain::Assistant < ApplicationRecord
scope :ordered, -> { order(created_at: :desc) }
scope :for_account, ->(account_id) { where(account_id: account_id) }
+
+ def available_name
+ name
+ end
+
+ 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
diff --git a/enterprise/app/models/copilot_message.rb b/enterprise/app/models/copilot_message.rb
new file mode 100644
index 000000000..16ae2c3c9
--- /dev/null
+++ b/enterprise/app/models/copilot_message.rb
@@ -0,0 +1,27 @@
+# == Schema Information
+#
+# Table name: copilot_messages
+#
+# id :bigint not null, primary key
+# message :jsonb not null
+# message_type :string not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# account_id :bigint not null
+# copilot_thread_id :bigint not null
+# user_id :bigint not null
+#
+# Indexes
+#
+# index_copilot_messages_on_account_id (account_id)
+# index_copilot_messages_on_copilot_thread_id (copilot_thread_id)
+# index_copilot_messages_on_user_id (user_id)
+#
+class CopilotMessage < ApplicationRecord
+ belongs_to :copilot_thread
+ belongs_to :user
+ belongs_to :account
+
+ validates :message_type, presence: true, inclusion: { in: %w[user assistant assistant_thinking] }
+ validates :message, presence: true
+end
diff --git a/enterprise/app/models/copilot_thread.rb b/enterprise/app/models/copilot_thread.rb
new file mode 100644
index 000000000..865418ad7
--- /dev/null
+++ b/enterprise/app/models/copilot_thread.rb
@@ -0,0 +1,26 @@
+# == Schema Information
+#
+# Table name: copilot_threads
+#
+# id :bigint not null, primary key
+# title :string not null
+# uuid :uuid not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# account_id :bigint not null
+# user_id :bigint not null
+#
+# Indexes
+#
+# index_copilot_threads_on_account_id (account_id)
+# index_copilot_threads_on_user_id (user_id)
+# index_copilot_threads_on_uuid (uuid) UNIQUE
+#
+class CopilotThread < ApplicationRecord
+ belongs_to :user
+ belongs_to :account
+ has_many :copilot_messages, dependent: :destroy
+
+ validates :title, presence: true
+ validates :uuid, presence: true, uniqueness: true
+end
diff --git a/enterprise/app/models/enterprise/activity_message_handler.rb b/enterprise/app/models/enterprise/activity_message_handler.rb
new file mode 100644
index 000000000..e6a93718f
--- /dev/null
+++ b/enterprise/app/models/enterprise/activity_message_handler.rb
@@ -0,0 +1,22 @@
+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
+ end
+end
diff --git a/enterprise/app/models/enterprise/concerns/account.rb b/enterprise/app/models/enterprise/concerns/account.rb
index 4fcb9b34b..4a573a4c4 100644
--- a/enterprise/app/models/enterprise/concerns/account.rb
+++ b/enterprise/app/models/enterprise/concerns/account.rb
@@ -9,5 +9,7 @@ module Enterprise::Concerns::Account
has_many :captain_assistants, dependent: :destroy_async, class_name: 'Captain::Assistant'
has_many :captain_assistant_responses, dependent: :destroy_async, class_name: 'Captain::AssistantResponse'
has_many :captain_documents, dependent: :destroy_async, class_name: 'Captain::Document'
+
+ has_many :copilot_threads, dependent: :destroy_async
end
end
diff --git a/enterprise/app/models/enterprise/concerns/user.rb b/enterprise/app/models/enterprise/concerns/user.rb
index 5d2687fbf..0e597b8d8 100644
--- a/enterprise/app/models/enterprise/concerns/user.rb
+++ b/enterprise/app/models/enterprise/concerns/user.rb
@@ -5,6 +5,8 @@ module Enterprise::Concerns::User
before_validation :ensure_installation_pricing_plan_quantity, on: :create
has_many :captain_responses, class_name: 'Captain::AssistantResponse', dependent: :nullify, as: :documentable
+ has_many :copilot_threads, dependent: :destroy_async
+ has_many :copilot_messages, dependent: :destroy_async
end
def ensure_installation_pricing_plan_quantity
diff --git a/enterprise/app/services/captain/llm/assistant_chat_service.rb b/enterprise/app/services/captain/llm/assistant_chat_service.rb
index 1e45cb1d8..50688f18b 100644
--- a/enterprise/app/services/captain/llm/assistant_chat_service.rb
+++ b/enterprise/app/services/captain/llm/assistant_chat_service.rb
@@ -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
diff --git a/enterprise/app/services/captain/llm/system_prompts_service.rb b/enterprise/app/services/captain/llm/system_prompts_service.rb
index b1e627275..2d2940a8e 100644
--- a/enterprise/app/services/captain/llm/system_prompts_service.rb
+++ b/enterprise/app/services/captain/llm/system_prompts_service.rb
@@ -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.
diff --git a/enterprise/app/views/api/v1/accounts/captain/copilot_messages/index.json.jbuilder b/enterprise/app/views/api/v1/accounts/captain/copilot_messages/index.json.jbuilder
new file mode 100644
index 000000000..ce0d5b175
--- /dev/null
+++ b/enterprise/app/views/api/v1/accounts/captain/copilot_messages/index.json.jbuilder
@@ -0,0 +1,8 @@
+json.payload do
+ json.array! @copilot_messages do |message|
+ json.id message.id
+ json.message message.message
+ json.message_type message.message_type
+ json.created_at message.created_at.to_i
+ end
+end
diff --git a/enterprise/app/views/api/v1/accounts/captain/copilot_threads/index.json.jbuilder b/enterprise/app/views/api/v1/accounts/captain/copilot_threads/index.json.jbuilder
new file mode 100644
index 000000000..c06182ffd
--- /dev/null
+++ b/enterprise/app/views/api/v1/accounts/captain/copilot_threads/index.json.jbuilder
@@ -0,0 +1,12 @@
+json.payload do
+ json.array! @copilot_threads do |thread|
+ json.id thread.id
+ json.title thread.title
+ json.uuid thread.uuid
+ json.created_at thread.created_at.to_i
+ json.user do
+ json.id thread.user.id
+ json.name thread.user.name
+ end
+ end
+end
diff --git a/lib/integrations/slack/send_on_slack_service.rb b/lib/integrations/slack/send_on_slack_service.rb
index e68dd81af..c37a152e8 100644
--- a/lib/integrations/slack/send_on_slack_service.rb
+++ b/lib/integrations/slack/send_on_slack_service.rb
@@ -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
diff --git a/public/assets/images/dashboard/captain/logo.svg b/public/assets/images/dashboard/captain/logo.svg
new file mode 100644
index 000000000..7eb14df2b
--- /dev/null
+++ b/public/assets/images/dashboard/captain/logo.svg
@@ -0,0 +1,6 @@
+
diff --git a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb
index 7b3dafccb..96272f9ac 100644
--- a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb
@@ -147,6 +147,32 @@ RSpec.describe 'Inboxes API', type: :request do
expect(data[:imap_login]).to eq('test@test.com')
end
+ context 'when it is a Twilio inbox' do
+ let(:twilio_channel) { create(:channel_twilio_sms, account: account, account_sid: 'AC123', auth_token: 'secrettoken') }
+ let(:twilio_inbox) { create(:inbox, channel: twilio_channel, account: account) }
+
+ it 'returns auth_token and account_sid for admin' do
+ get "/api/v1/accounts/#{account.id}/inboxes/#{twilio_inbox.id}",
+ headers: admin.create_new_auth_token,
+ as: :json
+ expect(response).to have_http_status(:success)
+ data = JSON.parse(response.body, symbolize_names: true)
+ expect(data[:auth_token]).to eq('secrettoken')
+ expect(data[:account_sid]).to eq('AC123')
+ end
+
+ it "doesn't return auth_token and account_sid for agent" do
+ create(:inbox_member, user: agent, inbox: twilio_inbox)
+ get "/api/v1/accounts/#{account.id}/inboxes/#{twilio_inbox.id}",
+ headers: agent.create_new_auth_token,
+ as: :json
+ expect(response).to have_http_status(:success)
+ data = JSON.parse(response.body, symbolize_names: true)
+ expect(data[:auth_token]).to be_nil
+ expect(data[:account_sid]).to be_nil
+ end
+ end
+
it 'fetch API inbox without hmac token when agent' do
api_channel = create(:channel_api, account: account)
api_inbox = create(:inbox, channel: api_channel, account: account)
@@ -518,6 +544,22 @@ RSpec.describe 'Inboxes API', type: :request do
expect(email_channel.reload.email).to eq('emailtest@email.test')
end
+ it 'updates twilio sms inbox when administrator' do
+ twilio_sms_channel = create(:channel_twilio_sms, account: account)
+ twilio_sms_inbox = create(:inbox, channel: twilio_sms_channel, account: account)
+ expect(twilio_sms_inbox.reload.channel.account_sid).not_to eq('account_sid')
+ expect(twilio_sms_inbox.reload.channel.auth_token).not_to eq('new_auth_token')
+
+ patch "/api/v1/accounts/#{account.id}/inboxes/#{twilio_sms_inbox.id}",
+ headers: admin.create_new_auth_token,
+ params: { channel: { account_sid: 'account_sid', auth_token: 'new_auth_token' } },
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(twilio_sms_inbox.reload.channel.account_sid).to eq('account_sid')
+ expect(twilio_sms_inbox.reload.channel.auth_token).to eq('new_auth_token')
+ end
+
it 'updates email inbox with imap when administrator' do
email_channel = create(:channel_email, account: account)
email_inbox = create(:inbox, channel: email_channel, account: account)
@@ -675,6 +717,94 @@ RSpec.describe 'Inboxes API', type: :request do
expect(email_channel.reload.smtp_authentication).to eq('plain')
end
end
+
+ context 'when handling CSAT configuration' do
+ let(:admin) { create(:user, account: account, role: :administrator) }
+ let(:inbox) { create(:inbox, account: account) }
+ let(:csat_config) do
+ {
+ 'display_type' => 'emoji',
+ 'message' => 'How would you rate your experience?',
+ 'survey_rules' => {
+ 'operator' => 'contains',
+ 'values' => %w[support help]
+ }
+ }
+ end
+
+ it 'successfully updates the inbox with CSAT configuration' do
+ patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
+ params: {
+ csat_survey_enabled: true,
+ csat_config: csat_config
+ },
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ end
+
+ context 'when CSAT is configured' do
+ before do
+ patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
+ params: {
+ csat_survey_enabled: true,
+ csat_config: csat_config
+ },
+ headers: admin.create_new_auth_token,
+ as: :json
+ end
+
+ it 'returns configured CSAT settings in inbox details' do
+ get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ json_response = response.parsed_body
+ expect(json_response['csat_survey_enabled']).to be true
+
+ saved_config = json_response['csat_config']
+ expect(saved_config).to be_present
+ expect(saved_config['display_type']).to eq('emoji')
+ end
+
+ it 'returns configured CSAT message' do
+ get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ json_response = response.parsed_body
+ saved_config = json_response['csat_config']
+ expect(saved_config['message']).to eq('How would you rate your experience?')
+ end
+
+ it 'returns configured CSAT survey rules' do
+ get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ json_response = response.parsed_body
+ saved_config = json_response['csat_config']
+ expect(saved_config['survey_rules']['operator']).to eq('contains')
+ expect(saved_config['survey_rules']['values']).to match_array(%w[support help])
+ end
+
+ it 'includes CSAT configuration in inbox list' do
+ get "/api/v1/accounts/#{account.id}/inboxes",
+ headers: admin.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ inbox_list = response.parsed_body
+ found_inbox = inbox_list['payload'].find { |i| i['id'] == inbox.id }
+
+ expect(found_inbox['csat_survey_enabled']).to be true
+ expect(found_inbox['csat_config']).to be_present
+ expect(found_inbox['csat_config']['display_type']).to eq('emoji')
+ end
+ end
+ end
end
describe 'GET /api/v1/accounts/{account.id}/inboxes/{inbox.id}/agent_bot' do
diff --git a/spec/enterprise/controllers/api/v1/accounts/captain/copilot_messages_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/captain/copilot_messages_controller_spec.rb
new file mode 100644
index 000000000..0ccca90c5
--- /dev/null
+++ b/spec/enterprise/controllers/api/v1/accounts/captain/copilot_messages_controller_spec.rb
@@ -0,0 +1,33 @@
+require 'rails_helper'
+
+RSpec.describe 'Api::V1::Accounts::Captain::CopilotMessagesController', type: :request do
+ let(:account) { create(:account) }
+ let(:user) { create(:user, account: account, role: :administrator) }
+ let(:copilot_thread) { create(:captain_copilot_thread, account: account, user: user) }
+ let!(:copilot_message) { create(:captain_copilot_message, copilot_thread: copilot_thread, user: user, account: account) }
+
+ describe 'GET /api/v1/accounts/{account.id}/captain/copilot_threads/{thread.uuid}/copilot_messages' do
+ context 'when it is an authenticated user' do
+ it 'returns all messages' do
+ get "/api/v1/accounts/#{account.id}/captain/copilot_threads/#{copilot_thread.uuid}/copilot_messages",
+ headers: user.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ json_response = response.parsed_body
+ expect(json_response['payload'].length).to eq(1)
+ expect(json_response['payload'][0]['id']).to eq(copilot_message.id)
+ end
+ end
+
+ context 'when thread uuid is invalid' do
+ it 'returns not found error' do
+ get "/api/v1/accounts/#{account.id}/captain/copilot_threads/invalid-uuid/copilot_messages",
+ headers: user.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/enterprise/controllers/api/v1/accounts/captain/copilot_threads_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/captain/copilot_threads_controller_spec.rb
new file mode 100644
index 000000000..8533a2d1d
--- /dev/null
+++ b/spec/enterprise/controllers/api/v1/accounts/captain/copilot_threads_controller_spec.rb
@@ -0,0 +1,50 @@
+require 'rails_helper'
+
+RSpec.describe 'Api::V1::Accounts::Captain::CopilotThreads', type: :request do
+ let(:account) { create(:account) }
+ let(:admin) { create(:user, account: account, role: :administrator) }
+ let(:agent) { create(:user, account: account, role: :agent) }
+
+ def json_response
+ JSON.parse(response.body, symbolize_names: true)
+ end
+
+ describe 'GET /api/v1/accounts/{account.id}/captain/copilot_threads' do
+ context 'when it is an un-authenticated user' do
+ it 'does not fetch copilot threads' do
+ get "/api/v1/accounts/#{account.id}/captain/copilot_threads",
+ as: :json
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an agent' do
+ it 'fetches copilot threads for the current user' do
+ # Create threads for the current agent
+ create_list(:captain_copilot_thread, 3, account: account, user: agent)
+ # Create threads for another user (should not be included)
+ create_list(:captain_copilot_thread, 2, account: account, user: admin)
+
+ get "/api/v1/accounts/#{account.id}/captain/copilot_threads",
+ headers: agent.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(json_response[:payload].length).to eq(3)
+
+ expect(json_response[:payload].map { |thread| thread[:user][:id] }.uniq).to eq([agent.id])
+ end
+
+ it 'returns threads in descending order of creation' do
+ threads = create_list(:captain_copilot_thread, 3, account: account, user: agent)
+
+ get "/api/v1/accounts/#{account.id}/captain/copilot_threads",
+ headers: agent.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(json_response[:payload].pluck(:id)).to eq(threads.reverse.pluck(:id))
+ end
+ end
+ end
+end
diff --git a/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb b/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb
index 09f62272d..40f1ea294 100644
--- a/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb
+++ b/spec/enterprise/jobs/captain/inbox_pending_conversations_resolution_job_spec.rb
@@ -9,8 +9,10 @@ RSpec.describe Captain::InboxPendingConversationsResolutionJob, type: :job do
let!(:recent_pending_conversation) { create(:conversation, inbox: inbox, last_activity_at: 10.minutes.ago, status: :pending) }
let!(:open_conversation) { create(:conversation, inbox: inbox, last_activity_at: 1.hour.ago, status: :open) }
+ let!(:captain_assistant) { create(:captain_assistant, account: inbox.account) }
+
before do
- create(:captain_inbox, inbox: inbox, captain_assistant: create(:captain_assistant, account: inbox.account))
+ create(:captain_inbox, inbox: inbox, captain_assistant: captain_assistant)
stub_const('Limits::BULK_ACTIONS_LIMIT', 2)
end
@@ -27,14 +29,34 @@ RSpec.describe Captain::InboxPendingConversationsResolutionJob, type: :job do
expect(open_conversation.reload.status).to eq('open')
end
- it 'creates an outgoing message for each resolved conversation' do
- # resolution message + system message
- expect { perform_enqueued_jobs { described_class.perform_later(inbox) } }
- .to change { resolvable_pending_conversation.messages.reload.count }.by(2)
+ it 'creates exactly one outgoing message with configured content' do
+ custom_message = 'This is a custom resolution message.'
+ captain_assistant.update!(config: { 'resolution_message' => custom_message })
- resolved_conversation_messages = resolvable_pending_conversation.messages.map(&:content)
- expect(resolved_conversation_messages).to include(
- 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
+ expect do
+ perform_enqueued_jobs { described_class.perform_later(inbox) }
+ end.to change { resolvable_pending_conversation.messages.outgoing.reload.count }.by(1)
+
+ outgoing_message = resolvable_pending_conversation.messages.outgoing.last
+ expect(outgoing_message.content).to eq(custom_message)
+ end
+
+ it 'creates an outgoing message with default auto resolution message if not configured' do
+ captain_assistant.update!(config: {})
+
+ perform_enqueued_jobs { described_class.perform_later(inbox) }
+ outgoing_message = resolvable_pending_conversation.messages.outgoing.last
+ expect(outgoing_message.content).to eq(
+ I18n.t('conversations.activity.auto_resolution_message')
+ )
+ end
+
+ it 'adds the correct activity message after resolution by Captain' do
+ perform_enqueued_jobs { described_class.perform_later(inbox) }
+ activity_message = resolvable_pending_conversation.messages.activity.last
+ expect(activity_message).not_to be_nil
+ expect(activity_message.content).to eq(
+ I18n.t('conversations.activity.captain.resolved', user_name: captain_assistant.name)
)
end
end
diff --git a/spec/enterprise/models/message_spec.rb b/spec/enterprise/models/message_spec.rb
new file mode 100644
index 000000000..5a9dc4e27
--- /dev/null
+++ b/spec/enterprise/models/message_spec.rb
@@ -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
diff --git a/spec/factories/captain/copilot_message.rb b/spec/factories/captain/copilot_message.rb
new file mode 100644
index 000000000..78f9f202e
--- /dev/null
+++ b/spec/factories/captain/copilot_message.rb
@@ -0,0 +1,9 @@
+FactoryBot.define do
+ factory :captain_copilot_message, class: 'CopilotMessage' do
+ account
+ user
+ copilot_thread { association :captain_copilot_thread }
+ message { { content: 'This is a test message' } }
+ message_type { 'user' }
+ end
+end
diff --git a/spec/factories/captain/copilot_thread.rb b/spec/factories/captain/copilot_thread.rb
new file mode 100644
index 000000000..fee78a7e7
--- /dev/null
+++ b/spec/factories/captain/copilot_thread.rb
@@ -0,0 +1,8 @@
+FactoryBot.define do
+ factory :captain_copilot_thread, class: 'CopilotThread' do
+ account
+ user
+ title { Faker::Lorem.sentence }
+ uuid { SecureRandom.uuid }
+ end
+end
diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb
index 0dc17d472..161f3541d 100644
--- a/spec/models/article_spec.rb
+++ b/spec/models/article_spec.rb
@@ -167,4 +167,26 @@ RSpec.describe Article do
end
end
end
+
+ describe '#to_llm_text' do
+ it 'returns formatted article text' do
+ category = create(:category, name: 'Test Category', slug: 'test_category', portal_id: portal_1.id)
+ article = create(:article, title: 'Test Article', category_id: category.id, content: 'This is the content', portal_id: portal_1.id,
+ author_id: user.id)
+ expected_output = <<~TEXT
+ Title: #{article.title}
+ ID: #{article.id}
+ Status: #{article.status}
+ Category: #{category.name}
+ Author: #{user.name}
+ Views: #{article.views}
+ Created At: #{article.created_at}
+ Updated At: #{article.updated_at}
+ Content:
+ #{article.content}
+ TEXT
+
+ expect(article.to_llm_text).to eq(expected_output)
+ end
+ end
end
diff --git a/spec/services/llm_formatter/article_llm_formatter_spec.rb b/spec/services/llm_formatter/article_llm_formatter_spec.rb
new file mode 100644
index 000000000..0f47beddb
--- /dev/null
+++ b/spec/services/llm_formatter/article_llm_formatter_spec.rb
@@ -0,0 +1,44 @@
+require 'rails_helper'
+
+RSpec.describe LlmFormatter::ArticleLlmFormatter do
+ let(:account) { create(:account) }
+ let(:portal) { create(:portal, account: account) }
+ let(:category) { create(:category, slug: 'test_category', portal: portal, account: account) }
+ let(:author) { create(:user, account: account) }
+ let(:formatter) { described_class.new(article) }
+
+ describe '#format' do
+ context 'when article has all details' do
+ let(:article) do
+ create(:article,
+ slug: 'test_article',
+ portal: portal, category: category, author: author, views: 100, account: account)
+ end
+
+ it 'formats article details correctly' do
+ expected_output = <<~TEXT
+ Title: #{article.title}
+ ID: #{article.id}
+ Status: #{article.status}
+ Category: #{category.name}
+ Author: #{author.name}
+ Views: #{article.views}
+ Created At: #{article.created_at}
+ Updated At: #{article.updated_at}
+ Content:
+ #{article.content}
+ TEXT
+
+ expect(formatter.format).to eq(expected_output)
+ end
+ end
+
+ context 'when article has no category' do
+ let(:article) { create(:article, portal: portal, category: nil, author: author, account: account) }
+
+ it 'shows Uncategorized for category' do
+ expect(formatter.format).to include('Category: Uncategorized')
+ end
+ end
+ end
+end
diff --git a/spec/services/llm_formatter/contact_llm_formatter_spec.rb b/spec/services/llm_formatter/contact_llm_formatter_spec.rb
new file mode 100644
index 000000000..bf3345b98
--- /dev/null
+++ b/spec/services/llm_formatter/contact_llm_formatter_spec.rb
@@ -0,0 +1,78 @@
+require 'rails_helper'
+
+RSpec.describe LlmFormatter::ContactLlmFormatter do
+ let(:account) { create(:account) }
+ let(:contact) { create(:contact, account: account, name: 'John Doe', email: 'john@example.com', phone_number: '+1234567890') }
+ let(:formatter) { described_class.new(contact) }
+
+ describe '#format' do
+ context 'when contact has no notes' do
+ it 'formats contact details correctly' do
+ expected_output = [
+ "Contact ID: ##{contact.id}",
+ 'Contact Attributes:',
+ 'Name: John Doe',
+ 'Email: john@example.com',
+ 'Phone: +1234567890',
+ 'Location: ',
+ 'Country Code: ',
+ 'Contact Notes:',
+ 'No notes for this contact'
+ ].join("\n")
+
+ expect(formatter.format).to eq(expected_output)
+ end
+ end
+
+ context 'when contact has notes' do
+ before do
+ create(:note, account: account, contact: contact, content: 'First interaction')
+ create(:note, account: account, contact: contact, content: 'Follow up needed')
+ end
+
+ it 'includes notes in the output' do
+ expected_output = [
+ "Contact ID: ##{contact.id}",
+ 'Contact Attributes:',
+ 'Name: John Doe',
+ 'Email: john@example.com',
+ 'Phone: +1234567890',
+ 'Location: ',
+ 'Country Code: ',
+ 'Contact Notes:',
+ ' - First interaction',
+ ' - Follow up needed'
+ ].join("\n")
+
+ expect(formatter.format).to eq(expected_output)
+ end
+ end
+
+ context 'when contact has custom attributes' do
+ let!(:custom_attribute) do
+ create(:custom_attribute_definition, account: account, attribute_model: 'contact_attribute', attribute_display_name: 'Company')
+ end
+
+ before do
+ contact.update(custom_attributes: { custom_attribute.attribute_key => 'Acme Inc' })
+ end
+
+ it 'includes custom attributes in the output' do
+ expected_output = [
+ "Contact ID: ##{contact.id}",
+ 'Contact Attributes:',
+ 'Name: John Doe',
+ 'Email: john@example.com',
+ 'Phone: +1234567890',
+ 'Location: ',
+ 'Country Code: ',
+ 'Company: Acme Inc',
+ 'Contact Notes:',
+ 'No notes for this contact'
+ ].join("\n")
+
+ expect(formatter.format).to eq(expected_output)
+ end
+ end
+ end
+end
diff --git a/spec/services/llm_formatter/conversation_llm_formatter_spec.rb b/spec/services/llm_formatter/conversation_llm_formatter_spec.rb
index 3838b8126..93fec14f7 100644
--- a/spec/services/llm_formatter/conversation_llm_formatter_spec.rb
+++ b/spec/services/llm_formatter/conversation_llm_formatter_spec.rb
@@ -47,5 +47,19 @@ RSpec.describe LlmFormatter::ConversationLlmFormatter do
expect(formatter.format).to eq(expected_output)
end
end
+
+ context 'when include_contact_details is true' do
+ it 'includes contact details' do
+ expected_output = [
+ "Conversation ID: ##{conversation.display_id}",
+ "Channel: #{conversation.inbox.channel.name}",
+ 'Message History:',
+ 'No messages in this conversation',
+ "Contact Details: #{conversation.contact.to_llm_text}"
+ ].join("\n")
+
+ expect(formatter.format(include_contact_details: true)).to eq(expected_output)
+ end
+ end
end
end
diff --git a/spec/services/message_templates/template/csat_survey_spec.rb b/spec/services/message_templates/template/csat_survey_spec.rb
index dae44ca3e..a2cae684b 100644
--- a/spec/services/message_templates/template/csat_survey_spec.rb
+++ b/spec/services/message_templates/template/csat_survey_spec.rb
@@ -1,13 +1,100 @@
require 'rails_helper'
describe MessageTemplates::Template::CsatSurvey do
- context 'when this hook is called' do
- let(:conversation) { create(:conversation) }
+ let(:account) { create(:account) }
+ let(:inbox) { create(:inbox, account: account) }
+ let(:conversation) { create(:conversation, account: account, inbox: inbox) }
+ let(:service) { described_class.new(conversation: conversation) }
- it 'creates the out of office messages' do
- described_class.new(conversation: conversation).perform
- expect(conversation.messages.template.count).to eq(1)
- expect(conversation.messages.template.first.content_type).to eq('input_csat')
+ describe '#perform' do
+ context 'when no survey rules are configured' do
+ it 'creates a CSAT survey message' do
+ inbox.update(csat_config: {})
+
+ service.perform
+
+ expect(conversation.messages.template.count).to eq(1)
+ expect(conversation.messages.template.first.content_type).to eq('input_csat')
+ end
+ end
+ end
+
+ describe '#perform with contains operator' do
+ let(:csat_config) do
+ {
+ 'display_type' => 'emoji',
+ 'message' => 'Please rate your experience',
+ 'survey_rules' => {
+ 'operator' => 'contains',
+ 'values' => %w[support help]
+ }
+ }
+ end
+
+ before do
+ inbox.update(csat_config: csat_config)
+ end
+
+ context 'when conversation has matching labels' do
+ it 'creates a CSAT survey message' do
+ conversation.update(label_list: %w[support urgent])
+
+ service.perform
+
+ expect(conversation.messages.template.count).to eq(1)
+ message = conversation.messages.template.first
+ expect(message.content_type).to eq('input_csat')
+ expect(message.content).to eq('Please rate your experience')
+ expect(message.content_attributes['display_type']).to eq('emoji')
+ end
+ end
+
+ context 'when conversation has no matching labels' do
+ it 'does not create a CSAT survey message' do
+ conversation.update(label_list: %w[billing-support payment])
+
+ service.perform
+
+ expect(conversation.messages.template.count).to eq(0)
+ end
+ end
+ end
+
+ describe '#perform with does_not_contain operator' do
+ let(:csat_config) do
+ {
+ 'display_type' => 'emoji',
+ 'message' => 'Please rate your experience',
+ 'survey_rules' => {
+ 'operator' => 'does_not_contain',
+ 'values' => %w[support help]
+ }
+ }
+ end
+
+ before do
+ inbox.update(csat_config: csat_config)
+ end
+
+ context 'when conversation does not have matching labels' do
+ it 'creates a CSAT survey message' do
+ conversation.update(label_list: %w[billing payment])
+
+ service.perform
+
+ expect(conversation.messages.template.count).to eq(1)
+ expect(conversation.messages.template.first.content_type).to eq('input_csat')
+ end
+ end
+
+ context 'when conversation has matching labels' do
+ it 'does not create a CSAT survey message' do
+ conversation.update(label_list: %w[support urgent])
+
+ service.perform
+
+ expect(conversation.messages.template.count).to eq(0)
+ end
end
end
end
diff --git a/spec/services/twilio/incoming_message_service_spec.rb b/spec/services/twilio/incoming_message_service_spec.rb
index 5f4dcec69..c8812e45d 100644
--- a/spec/services/twilio/incoming_message_service_spec.rb
+++ b/spec/services/twilio/incoming_message_service_spec.rb
@@ -173,7 +173,7 @@ describe Twilio::IncomingMessageService do
context 'when a message with an attachment is received' do
before do
stub_request(:get, 'https://chatwoot-assets.local/sample.png')
- .to_return(status: 200, body: 'image data', headers: {})
+ .to_return(status: 200, body: 'image data', headers: { 'Content-Type' => 'image/png' })
end
let(:params_with_attachment) do
@@ -203,7 +203,7 @@ describe Twilio::IncomingMessageService do
.to_raise(Down::Error.new('Download error'))
stub_request(:get, 'https://chatwoot-assets.local/sample.png')
- .to_return(status: 200, body: 'image data', headers: {})
+ .to_return(status: 200, body: 'image data', headers: { 'Content-Type' => 'image/png' })
end
let(:params_with_attachment_error) do
@@ -229,5 +229,36 @@ describe Twilio::IncomingMessageService do
expect(conversation.reload.messages.last.attachments.first.file_type).to eq('image')
end
end
+
+ context 'when a message with multiple attachments is received' do
+ before do
+ stub_request(:get, 'https://chatwoot-assets.local/sample.png')
+ .to_return(status: 200, body: 'image data 1', headers: { 'Content-Type' => 'image/png' })
+ stub_request(:get, 'https://chatwoot-assets.local/sample.jpg')
+ .to_return(status: 200, body: 'image data 2', headers: { 'Content-Type' => 'image/jpeg' })
+ end
+
+ let(:params_with_multiple_attachments) do
+ {
+ SmsSid: 'SMxx',
+ From: '+12345',
+ AccountSid: 'ACxxx',
+ MessagingServiceSid: twilio_channel.messaging_service_sid,
+ Body: 'testing multiple media',
+ NumMedia: '2',
+ MediaContentType0: 'image/png',
+ MediaUrl0: 'https://chatwoot-assets.local/sample.png',
+ MediaContentType1: 'image/jpeg',
+ MediaUrl1: 'https://chatwoot-assets.local/sample.jpg'
+ }
+ end
+
+ it 'creates a new message with multiple media attachments in existing conversation' do
+ described_class.new(params: params_with_multiple_attachments).perform
+ expect(conversation.reload.messages.last.content).to eq('testing multiple media')
+ expect(conversation.reload.messages.last.attachments.count).to eq(2)
+ expect(conversation.reload.messages.last.attachments.map(&:file_type)).to contain_exactly('image', 'image')
+ end
+ end
end
end