Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01c84be757 | ||
|
|
5ac32803dc | ||
|
|
18e0cfb327 | ||
|
|
a70d331813 | ||
|
|
3b4c5bd916 | ||
|
|
491e7d0c6c | ||
|
|
e81b812ac2 | ||
|
|
baec4523d5 | ||
|
|
65136c90d8 | ||
|
|
24125e2d9a | ||
|
|
52e3642127 | ||
|
|
6e13eb965b | ||
|
|
7d78f67d3b | ||
|
|
a7420f8fae | ||
|
|
847ce98006 | ||
|
|
8b9da2e409 | ||
|
|
d540d885f2 |
@@ -1,11 +0,0 @@
|
||||
class Api::V1::Accounts::Captain::ConfigController < Api::V1::Accounts::BaseController
|
||||
before_action :current_account
|
||||
|
||||
def show
|
||||
render json: {
|
||||
providers: Llm::ConfigService.providers,
|
||||
models: Llm::ConfigService.models,
|
||||
features: Llm::ConfigService.all_features_config
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -92,10 +92,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
end
|
||||
|
||||
def settings_params
|
||||
permitted = params.permit(:auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting, :audio_transcriptions, :auto_resolve_label)
|
||||
permitted[:captain_models] = params[:captain_models].to_unsafe_h if params[:captain_models].present?
|
||||
permitted[:captain_features] = params[:captain_features].to_unsafe_h if params[:captain_features].present?
|
||||
permitted
|
||||
params.permit(:auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting, :audio_transcriptions, :auto_resolve_label)
|
||||
end
|
||||
|
||||
def check_signup_enabled
|
||||
|
||||
@@ -102,8 +102,8 @@ const createNonDraftMessageAIAssistActions = (t, replyMode) => {
|
||||
const createDraftMessageAIAssistActions = t => {
|
||||
return [
|
||||
{
|
||||
label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.REPHRASE'),
|
||||
key: 'rephrase',
|
||||
label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.CONFIDENT'),
|
||||
key: 'confident',
|
||||
icon: ICON_AI_ASSIST,
|
||||
},
|
||||
{
|
||||
@@ -112,13 +112,13 @@ const createDraftMessageAIAssistActions = t => {
|
||||
icon: ICON_AI_GRAMMAR,
|
||||
},
|
||||
{
|
||||
label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.EXPAND'),
|
||||
key: 'expand',
|
||||
label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.PROFESSIONAL'),
|
||||
key: 'professional',
|
||||
icon: ICON_AI_EXPAND,
|
||||
},
|
||||
{
|
||||
label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.SHORTEN'),
|
||||
key: 'shorten',
|
||||
label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.CASUAL'),
|
||||
key: 'casual',
|
||||
icon: ICON_AI_SHORTEN,
|
||||
},
|
||||
{
|
||||
@@ -132,8 +132,8 @@ const createDraftMessageAIAssistActions = t => {
|
||||
icon: ICON_AI_ASSIST,
|
||||
},
|
||||
{
|
||||
label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.SIMPLIFY'),
|
||||
key: 'simplify',
|
||||
label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.STRAIGHTFORWARD'),
|
||||
key: 'straightforward',
|
||||
icon: ICON_AI_ASSIST,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -145,7 +145,11 @@
|
||||
"EXPAND": "Expand",
|
||||
"MAKE_FRIENDLY": "Change message tone to friendly",
|
||||
"MAKE_FORMAL": "Use formal tone",
|
||||
"SIMPLIFY": "Simplify"
|
||||
"SIMPLIFY": "Simplify",
|
||||
"CONFIDENT": "Use confident tone",
|
||||
"PROFESSIONAL": "Use professional tone",
|
||||
"CASUAL": "Use casual tone",
|
||||
"STRAIGHTFORWARD": "Use straightforward tone"
|
||||
},
|
||||
"ASSISTANCE_MODAL": {
|
||||
"DRAFT_TITLE": "Draft content",
|
||||
|
||||
@@ -8,4 +8,8 @@ export const OPEN_AI_OPTIONS = {
|
||||
SIMPLIFY: 'simplify',
|
||||
REPLY_SUGGESTION: 'reply_suggestion',
|
||||
SUMMARIZE: 'summarize',
|
||||
CASUAL: 'casual',
|
||||
PROFESSIONAL: 'professional',
|
||||
STRAIGHTFORWARD: 'straightforward',
|
||||
CONFIDENT: 'confident',
|
||||
};
|
||||
|
||||
@@ -53,11 +53,9 @@ class Account < ApplicationRecord
|
||||
validates_with JsonSchemaValidator,
|
||||
schema: SETTINGS_PARAMS_SCHEMA,
|
||||
attribute_resolver: ->(record) { record.settings }
|
||||
validate :validate_captain_models
|
||||
|
||||
store_accessor :settings, :auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting
|
||||
store_accessor :settings, :audio_transcriptions, :auto_resolve_label
|
||||
store_accessor :settings, :captain_models, :captain_features
|
||||
|
||||
has_many :account_users, dependent: :destroy_async
|
||||
has_many :agent_bot_inboxes, dependent: :destroy_async
|
||||
@@ -161,46 +159,8 @@ class Account < ApplicationRecord
|
||||
ISO_639.find(account_locale)&.english_name&.downcase || 'english'
|
||||
end
|
||||
|
||||
def captain_preferences
|
||||
{
|
||||
models: captain_models_with_defaults,
|
||||
features: captain_features_with_defaults
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def captain_models_with_defaults
|
||||
stored_models = captain_models || {}
|
||||
Llm::ConfigService.feature_keys.each_with_object({}) do |feature_key, result|
|
||||
stored_value = stored_models[feature_key]
|
||||
result[feature_key] = if stored_value.present? && Llm::ConfigService.valid_model_for_feature?(feature_key, stored_value)
|
||||
stored_value
|
||||
else
|
||||
Llm::ConfigService.default_model_for_feature(feature_key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def captain_features_with_defaults
|
||||
stored_features = captain_features || {}
|
||||
Llm::ConfigService.feature_keys.index_with do |feature_key|
|
||||
stored_features[feature_key] == true
|
||||
end
|
||||
end
|
||||
|
||||
def validate_captain_models
|
||||
return if captain_models.blank?
|
||||
|
||||
captain_models.each do |feature_key, model_name|
|
||||
next if model_name.blank?
|
||||
next if Llm::ConfigService.valid_model_for_feature?(feature_key, model_name)
|
||||
|
||||
allowed_models = Llm::ConfigService.models_for_feature(feature_key)
|
||||
errors.add(:captain_models, "'#{model_name}' is not a valid model for #{feature_key}. Allowed: #{allowed_models.join(', ')}")
|
||||
end
|
||||
end
|
||||
|
||||
def notify_creation
|
||||
Rails.configuration.dispatcher.dispatch(ACCOUNT_CREATED, Time.zone.now, account: self)
|
||||
end
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
class MessageTemplates::Template::OutOfOffice
|
||||
pattr_initialize [:conversation!]
|
||||
|
||||
def self.perform_if_applicable(conversation)
|
||||
inbox = conversation.inbox
|
||||
return unless inbox.out_of_office?
|
||||
return if inbox.out_of_office_message.blank?
|
||||
|
||||
new(conversation: conversation).perform
|
||||
end
|
||||
|
||||
def perform
|
||||
ActiveRecord::Base.transaction do
|
||||
conversation.messages.create!(out_of_office_message_params)
|
||||
|
||||
@@ -25,4 +25,3 @@ json.name @account.name
|
||||
json.support_email @account.support_email
|
||||
json.status @account.status
|
||||
json.cache_keys @account.cache_keys
|
||||
json.captain_preferences @account.captain_preferences
|
||||
|
||||
+177
-177
@@ -15,58 +15,58 @@
|
||||
|
||||
# ------- Branding Related Config ------- #
|
||||
- name: INSTALLATION_NAME
|
||||
value: "Chatwoot"
|
||||
display_title: "Installation Name"
|
||||
description: "The installation wide name that would be used in the dashboard, title etc."
|
||||
value: 'Chatwoot'
|
||||
display_title: 'Installation Name'
|
||||
description: 'The installation wide name that would be used in the dashboard, title etc.'
|
||||
- name: LOGO_THUMBNAIL
|
||||
value: "/brand-assets/logo_thumbnail.svg"
|
||||
display_title: "Logo Thumbnail"
|
||||
description: "The thumbnail that would be used for favicon (512px X 512px)"
|
||||
value: '/brand-assets/logo_thumbnail.svg'
|
||||
display_title: 'Logo Thumbnail'
|
||||
description: 'The thumbnail that would be used for favicon (512px X 512px)'
|
||||
- name: LOGO
|
||||
value: "/brand-assets/logo.svg"
|
||||
display_title: "Logo"
|
||||
description: "The logo that would be used on the dashboard, login page etc."
|
||||
value: '/brand-assets/logo.svg'
|
||||
display_title: 'Logo'
|
||||
description: 'The logo that would be used on the dashboard, login page etc.'
|
||||
- name: LOGO_DARK
|
||||
value: "/brand-assets/logo_dark.svg"
|
||||
display_title: "Logo Dark Mode"
|
||||
description: "The logo that would be used on the dashboard, login page etc. for dark mode"
|
||||
value: '/brand-assets/logo_dark.svg'
|
||||
display_title: 'Logo Dark Mode'
|
||||
description: 'The logo that would be used on the dashboard, login page etc. for dark mode'
|
||||
- name: BRAND_URL
|
||||
value: "https://www.chatwoot.com"
|
||||
display_title: "Brand URL"
|
||||
description: "The URL that would be used in emails under the section “Powered By”"
|
||||
value: 'https://www.chatwoot.com'
|
||||
display_title: 'Brand URL'
|
||||
description: 'The URL that would be used in emails under the section “Powered By”'
|
||||
- name: WIDGET_BRAND_URL
|
||||
value: "https://www.chatwoot.com"
|
||||
display_title: "Widget Brand URL"
|
||||
description: "The URL that would be used in the widget under the section “Powered By”"
|
||||
value: 'https://www.chatwoot.com'
|
||||
display_title: 'Widget Brand URL'
|
||||
description: 'The URL that would be used in the widget under the section “Powered By”'
|
||||
- name: BRAND_NAME
|
||||
value: "Chatwoot"
|
||||
display_title: "Brand Name"
|
||||
description: "The name that would be used in emails and the widget"
|
||||
value: 'Chatwoot'
|
||||
display_title: 'Brand Name'
|
||||
description: 'The name that would be used in emails and the widget'
|
||||
- name: TERMS_URL
|
||||
value: "https://www.chatwoot.com/terms-of-service"
|
||||
display_title: "Terms URL"
|
||||
description: "The terms of service URL displayed in Signup Page"
|
||||
value: 'https://www.chatwoot.com/terms-of-service'
|
||||
display_title: 'Terms URL'
|
||||
description: 'The terms of service URL displayed in Signup Page'
|
||||
- name: PRIVACY_URL
|
||||
value: "https://www.chatwoot.com/privacy-policy"
|
||||
display_title: "Privacy URL"
|
||||
description: "The privacy policy URL displayed in the app"
|
||||
value: 'https://www.chatwoot.com/privacy-policy'
|
||||
display_title: 'Privacy URL'
|
||||
description: 'The privacy policy URL displayed in the app'
|
||||
- name: DISPLAY_MANIFEST
|
||||
value: true
|
||||
display_title: "Chatwoot Metadata"
|
||||
description: "Display default Chatwoot metadata like favicons and upgrade warnings"
|
||||
display_title: 'Chatwoot Metadata'
|
||||
description: 'Display default Chatwoot metadata like favicons and upgrade warnings'
|
||||
type: boolean
|
||||
# ------- End of Branding Related Config ------- #
|
||||
|
||||
# ------- Signup & Account Related Config ------- #
|
||||
- name: ENABLE_ACCOUNT_SIGNUP
|
||||
display_title: "Enable Account Signup"
|
||||
display_title: 'Enable Account Signup'
|
||||
value: false
|
||||
description: "Allow users to signup for new accounts"
|
||||
description: 'Allow users to signup for new accounts'
|
||||
locked: false
|
||||
type: boolean
|
||||
- name: CREATE_NEW_ACCOUNT_FROM_DASHBOARD
|
||||
value: false
|
||||
description: "Allow users to create new accounts from the dashboard"
|
||||
description: 'Allow users to create new accounts from the dashboard'
|
||||
locked: false
|
||||
- name: HCAPTCHA_SITE_KEY
|
||||
value:
|
||||
@@ -76,127 +76,127 @@
|
||||
locked: false
|
||||
- name: INSTALLATION_EVENTS_WEBHOOK_URL
|
||||
value:
|
||||
display_title: "System events Webhook URL"
|
||||
description: "The URL to which the system events like new accounts created will be sent"
|
||||
display_title: 'System events Webhook URL'
|
||||
description: 'The URL to which the system events like new accounts created will be sent'
|
||||
locked: false
|
||||
- name: WEBHOOK_TIMEOUT
|
||||
value: 5
|
||||
display_title: "Webhook request timeout (seconds)"
|
||||
description: "Maximum time Chatwoot waits for a webhook response before failing the request"
|
||||
display_title: 'Webhook request timeout (seconds)'
|
||||
description: 'Maximum time Chatwoot waits for a webhook response before failing the request'
|
||||
locked: false
|
||||
- name: DIRECT_UPLOADS_ENABLED
|
||||
type: boolean
|
||||
value: false
|
||||
description: "Enable direct uploads to cloud storage"
|
||||
description: 'Enable direct uploads to cloud storage'
|
||||
locked: false
|
||||
- name: MAXIMUM_FILE_UPLOAD_SIZE
|
||||
value: 40
|
||||
display_title: "Attachment size limit (MB)"
|
||||
description: "Maximum attachment size in MB allowed for uploads"
|
||||
display_title: 'Attachment size limit (MB)'
|
||||
description: 'Maximum attachment size in MB allowed for uploads'
|
||||
locked: false
|
||||
# ------- End of Account Related Config ------- #
|
||||
|
||||
# ------- Email Related Config ------- #
|
||||
- name: MAILER_INBOUND_EMAIL_DOMAIN
|
||||
display_title: "Inbound Email Domain"
|
||||
display_title: 'Inbound Email Domain'
|
||||
value:
|
||||
description: "The domain name to be used for generating conversation continuity emails (reply+id@domain.com)"
|
||||
description: 'The domain name to be used for generating conversation continuity emails (reply+id@domain.com)'
|
||||
locked: false
|
||||
- name: MAILER_SUPPORT_EMAIL
|
||||
display_title: "Support Email"
|
||||
display_title: 'Support Email'
|
||||
value:
|
||||
description: "The support email address for your installation"
|
||||
description: 'The support email address for your installation'
|
||||
locked: false
|
||||
# ------- End of Email Related Config ------- #
|
||||
|
||||
# ------- Facebook Channel Related Config ------- #
|
||||
- name: FB_APP_ID
|
||||
display_title: "Facebook App ID"
|
||||
display_title: 'Facebook App ID'
|
||||
locked: false
|
||||
- name: FB_VERIFY_TOKEN
|
||||
display_title: "Facebook Verify Token"
|
||||
description: "The verify token used for Facebook Messenger Webhook"
|
||||
display_title: 'Facebook Verify Token'
|
||||
description: 'The verify token used for Facebook Messenger Webhook'
|
||||
locked: false
|
||||
type: secret
|
||||
- name: FB_APP_SECRET
|
||||
display_title: "Facebook App Secret"
|
||||
display_title: 'Facebook App Secret'
|
||||
locked: false
|
||||
type: secret
|
||||
- name: IG_VERIFY_TOKEN
|
||||
display_title: "Instagram Verify Token"
|
||||
description: "The verify token used for Instagram Webhook"
|
||||
display_title: 'Instagram Verify Token'
|
||||
description: 'The verify token used for Instagram Webhook'
|
||||
locked: false
|
||||
type: secret
|
||||
- name: FACEBOOK_API_VERSION
|
||||
display_title: "Facebook API Version"
|
||||
description: "Configure this if you want to use a different Facebook API version. Make sure its prefixed with `v`"
|
||||
value: "v18.0"
|
||||
display_title: 'Facebook API Version'
|
||||
description: 'Configure this if you want to use a different Facebook API version. Make sure its prefixed with `v`'
|
||||
value: 'v18.0'
|
||||
locked: false
|
||||
- name: ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT
|
||||
display_title: "Enable human agent"
|
||||
display_title: 'Enable human agent'
|
||||
value: false
|
||||
locked: false
|
||||
description: "Enable human agent for messenger channel for longer message back period. Needs additional app approval: https://developers.facebook.com/docs/features-reference/human-agent/"
|
||||
description: 'Enable human agent for messenger channel for longer message back period. Needs additional app approval: https://developers.facebook.com/docs/features-reference/human-agent/'
|
||||
type: boolean
|
||||
# ------- End of Facebook Channel Related Config ------- #
|
||||
|
||||
# ------- WhatsApp Channel Related Config ------- #
|
||||
- name: WHATSAPP_APP_ID
|
||||
display_title: "WhatsApp App ID"
|
||||
description: "The Facebook App ID for WhatsApp Business API integration"
|
||||
display_title: 'WhatsApp App ID'
|
||||
description: 'The Facebook App ID for WhatsApp Business API integration'
|
||||
locked: false
|
||||
- name: WHATSAPP_CONFIGURATION_ID
|
||||
display_title: "WhatsApp Configuration ID"
|
||||
description: "The Configuration ID for WhatsApp Embedded Signup flow (required for embedded signup)"
|
||||
display_title: 'WhatsApp Configuration ID'
|
||||
description: 'The Configuration ID for WhatsApp Embedded Signup flow (required for embedded signup)'
|
||||
locked: false
|
||||
- name: WHATSAPP_APP_SECRET
|
||||
display_title: "WhatsApp App Secret"
|
||||
description: "The App Secret for WhatsApp Embedded Signup flow (required for embedded signup)"
|
||||
display_title: 'WhatsApp App Secret'
|
||||
description: 'The App Secret for WhatsApp Embedded Signup flow (required for embedded signup)'
|
||||
locked: false
|
||||
- name: WHATSAPP_API_VERSION
|
||||
display_title: "WhatsApp API Version"
|
||||
description: "Configure this if you want to use a different WhatsApp API version. Make sure its prefixed with `v`"
|
||||
value: "v22.0"
|
||||
display_title: 'WhatsApp API Version'
|
||||
description: 'Configure this if you want to use a different WhatsApp API version. Make sure its prefixed with `v`'
|
||||
value: 'v22.0'
|
||||
locked: false
|
||||
# ------- End of WhatsApp Channel Related Config ------- #
|
||||
|
||||
# MARK: Microsoft Email Channel Config
|
||||
- name: AZURE_APP_ID
|
||||
display_title: "Azure App ID"
|
||||
description: "The App ID that will be used to authenticate with customer Microsoft accounts. Find more details on setting up Azure here: https://chwt.app/dev/ms"
|
||||
display_title: 'Azure App ID'
|
||||
description: 'The App ID that will be used to authenticate with customer Microsoft accounts. Find more details on setting up Azure here: https://chwt.app/dev/ms'
|
||||
locked: false
|
||||
- name: AZURE_APP_SECRET
|
||||
display_title: "Azure App Secret"
|
||||
display_title: 'Azure App Secret'
|
||||
locked: false
|
||||
type: secret
|
||||
# End of Microsoft Email Channel Config
|
||||
|
||||
# MARK: Captain Config
|
||||
- name: CAPTAIN_OPEN_AI_API_KEY
|
||||
display_title: "OpenAI API Key"
|
||||
description: "The API key used to authenticate requests to OpenAI services for Captain AI."
|
||||
display_title: 'OpenAI API Key'
|
||||
description: 'The API key used to authenticate requests to OpenAI services for Captain AI.'
|
||||
locked: false
|
||||
type: secret
|
||||
- name: CAPTAIN_OPEN_AI_MODEL
|
||||
display_title: "OpenAI Model"
|
||||
description: "The OpenAI model configured for use in Captain AI. Default: gpt-4.1-mini"
|
||||
display_title: 'OpenAI Model'
|
||||
description: 'The OpenAI model configured for use in Captain AI. Default: gpt-4o-mini'
|
||||
locked: false
|
||||
- name: CAPTAIN_OPEN_AI_ENDPOINT
|
||||
display_title: "OpenAI API Endpoint (optional)"
|
||||
description: "The OpenAI endpoint configured for use in Captain AI. Default: https://api.openai.com/"
|
||||
display_title: 'OpenAI API Endpoint (optional)'
|
||||
description: 'The OpenAI endpoint configured for use in Captain AI. Default: https://api.openai.com/'
|
||||
locked: false
|
||||
- name: CAPTAIN_EMBEDDING_MODEL
|
||||
display_title: "Embedding Model (optional)"
|
||||
description: "The embedding model configured for use in Captain AI. Default: text-embedding-3-small"
|
||||
display_title: 'Embedding Model (optional)'
|
||||
description: 'The embedding model configured for use in Captain AI. Default: text-embedding-3-small'
|
||||
locked: false
|
||||
- name: CAPTAIN_FIRECRAWL_API_KEY
|
||||
display_title: "FireCrawl API Key (optional)"
|
||||
description: "The FireCrawl API key for the Captain AI service"
|
||||
display_title: 'FireCrawl API Key (optional)'
|
||||
description: 'The FireCrawl API key for the Captain AI service'
|
||||
locked: false
|
||||
type: secret
|
||||
- name: CAPTAIN_CLOUD_PLAN_LIMITS
|
||||
display_title: "Captain Cloud Plan Limits"
|
||||
description: "The limits for the Captain AI service for different plans"
|
||||
display_title: 'Captain Cloud Plan Limits'
|
||||
description: 'The limits for the Captain AI service for different plans'
|
||||
value:
|
||||
type: code
|
||||
# End of Captain Config
|
||||
@@ -204,65 +204,65 @@
|
||||
# ------- Chatwoot Internal Config for Cloud ----#
|
||||
- name: CHATWOOT_INBOX_TOKEN
|
||||
value:
|
||||
display_title: "Inbox Token"
|
||||
description: "The Chatwoot Inbox Token for Contact Support in Cloud"
|
||||
display_title: 'Inbox Token'
|
||||
description: 'The Chatwoot Inbox Token for Contact Support in Cloud'
|
||||
locked: false
|
||||
type: secret
|
||||
- name: CHATWOOT_INBOX_HMAC_KEY
|
||||
value:
|
||||
display_title: "Inbox HMAC Key"
|
||||
description: "The Chatwoot Inbox HMAC Key for Contact Support in Cloud"
|
||||
display_title: 'Inbox HMAC Key'
|
||||
description: 'The Chatwoot Inbox HMAC Key for Contact Support in Cloud'
|
||||
locked: false
|
||||
type: secret
|
||||
- name: CHATWOOT_CLOUD_PLANS
|
||||
display_title: "Cloud Plans"
|
||||
display_title: 'Cloud Plans'
|
||||
value:
|
||||
description: "Config to store stripe plans for cloud"
|
||||
description: 'Config to store stripe plans for cloud'
|
||||
- name: CHATWOOT_CLOUD_PLAN_FEATURES
|
||||
display_title: "Planwise Features List"
|
||||
display_title: 'Planwise Features List'
|
||||
value:
|
||||
description: "Config to features and their associated plans"
|
||||
description: 'Config to features and their associated plans'
|
||||
- name: DEPLOYMENT_ENV
|
||||
value: self-hosted
|
||||
description: "The deployment environment of the installation, to differentiate between Chatwoot cloud and self-hosted"
|
||||
description: 'The deployment environment of the installation, to differentiate between Chatwoot cloud and self-hosted'
|
||||
- name: ANALYTICS_TOKEN
|
||||
value:
|
||||
display_title: "Analytics Token"
|
||||
description: "The PostHog analytics token for Chatwoot cloud"
|
||||
display_title: 'Analytics Token'
|
||||
description: 'The PostHog analytics token for Chatwoot cloud'
|
||||
type: secret
|
||||
- name: CLEARBIT_API_KEY
|
||||
value:
|
||||
display_title: "Clearbit API Key"
|
||||
description: "This API key is used for onboarding the users, to pre-fill account data."
|
||||
display_title: 'Clearbit API Key'
|
||||
description: 'This API key is used for onboarding the users, to pre-fill account data.'
|
||||
type: secret
|
||||
- name: DASHBOARD_SCRIPTS
|
||||
value:
|
||||
display_title: "Dashboard Scripts"
|
||||
description: "Scripts are loaded as the last item in the <body> tag"
|
||||
display_title: 'Dashboard Scripts'
|
||||
description: 'Scripts are loaded as the last item in the <body> tag'
|
||||
type: code
|
||||
- name: BLOCKED_EMAIL_DOMAINS
|
||||
value:
|
||||
display_title: "Blocked Email Domains"
|
||||
description: "Add a domain per line to block them from signing up, accepts Regex"
|
||||
display_title: 'Blocked Email Domains'
|
||||
description: 'Add a domain per line to block them from signing up, accepts Regex'
|
||||
type: code
|
||||
- name: SKIP_INCOMING_BCC_PROCESSING
|
||||
value:
|
||||
display_title: "Skip BCC Processing For"
|
||||
description: "Comma-separated list of account IDs that should be skipped from incoming BCC processing"
|
||||
display_title: 'Skip BCC Processing For'
|
||||
description: 'Comma-separated list of account IDs that should be skipped from incoming BCC processing'
|
||||
- name: INACTIVE_WHATSAPP_NUMBERS
|
||||
value: ""
|
||||
display_title: "Inactive WhatsApp Numbers"
|
||||
description: "Comma-separated list of WhatsApp numbers that should be rejected with a 422 error"
|
||||
value: ''
|
||||
display_title: 'Inactive WhatsApp Numbers'
|
||||
description: 'Comma-separated list of WhatsApp numbers that should be rejected with a 422 error'
|
||||
type: code
|
||||
# ------- End of Chatwoot Internal Config for Cloud ----#
|
||||
|
||||
# ------- Chatwoot Internal Config for Self Hosted ----#
|
||||
- name: INSTALLATION_PRICING_PLAN
|
||||
value: "community"
|
||||
description: "The pricing plan for the installation, retrieved from the billing API"
|
||||
value: 'community'
|
||||
description: 'The pricing plan for the installation, retrieved from the billing API'
|
||||
- name: INSTALLATION_PRICING_PLAN_QUANTITY
|
||||
value: 0
|
||||
description: "The number of licenses purchased for the installation, retrieved from the billing API"
|
||||
description: 'The number of licenses purchased for the installation, retrieved from the billing API'
|
||||
- name: CHATWOOT_SUPPORT_WEBSITE_TOKEN
|
||||
value:
|
||||
description: 'The Chatwoot website token, used to identify the Chatwoot inbox and display the "Contact Support" option on the billing page'
|
||||
@@ -272,7 +272,7 @@
|
||||
description: 'The Chatwoot script base URL, to display the "Contact Support" option on the billing page'
|
||||
- name: CHATWOOT_SUPPORT_IDENTIFIER_HASH
|
||||
value:
|
||||
description: "The Chatwoot identifier hash, to validate the contact in the live chat window."
|
||||
description: 'The Chatwoot identifier hash, to validate the contact in the live chat window.'
|
||||
type: secret
|
||||
- name: ACCOUNT_SECURITY_NOTIFICATION_WEBHOOK_URL
|
||||
display_title: Webhook URL to post security analysis
|
||||
@@ -282,221 +282,221 @@
|
||||
|
||||
# ------- Compliance Related Config ----#
|
||||
- name: CHATWOOT_INSTANCE_ADMIN_EMAIL
|
||||
display_title: "Instance Admin Email"
|
||||
display_title: 'Instance Admin Email'
|
||||
value:
|
||||
description: "The email of the instance administrator to receive compliance-related notifications"
|
||||
description: 'The email of the instance administrator to receive compliance-related notifications'
|
||||
locked: false
|
||||
# ------- End of Compliance Related Config ----#
|
||||
|
||||
## ------ Configs added for enterprise clients ------ ##
|
||||
- name: API_CHANNEL_NAME
|
||||
value:
|
||||
description: "Custom name for the API channel"
|
||||
description: 'Custom name for the API channel'
|
||||
- name: API_CHANNEL_THUMBNAIL
|
||||
value:
|
||||
description: "Custom thumbnail for the API channel"
|
||||
description: 'Custom thumbnail for the API channel'
|
||||
- name: LOGOUT_REDIRECT_LINK
|
||||
value: /app/login
|
||||
locked: false
|
||||
description: "Redirect to a different link after logout"
|
||||
description: 'Redirect to a different link after logout'
|
||||
- name: DISABLE_USER_PROFILE_UPDATE
|
||||
value: false
|
||||
locked: false
|
||||
description: "Disable rendering profile update page for users"
|
||||
description: 'Disable rendering profile update page for users'
|
||||
## ------ End of Configs added for enterprise clients ------ ##
|
||||
|
||||
## ------ Configs added for FCM v1 notifications ------ ##
|
||||
- name: FIREBASE_PROJECT_ID
|
||||
display_title: "Firebase Project ID"
|
||||
display_title: 'Firebase Project ID'
|
||||
value:
|
||||
locked: false
|
||||
description: "Firebase project ID"
|
||||
description: 'Firebase project ID'
|
||||
- name: FIREBASE_CREDENTIALS
|
||||
display_title: "Firebase Credentials"
|
||||
display_title: 'Firebase Credentials'
|
||||
value:
|
||||
locked: false
|
||||
type: secret
|
||||
description: "Contents on your firebase credentials json file"
|
||||
description: 'Contents on your firebase credentials json file'
|
||||
## ------ End of Configs added for FCM v1 notifications ------ ##
|
||||
|
||||
## ------ Configs added for Linear ------ ##
|
||||
- name: LINEAR_CLIENT_ID
|
||||
display_title: "Linear Client ID"
|
||||
display_title: 'Linear Client ID'
|
||||
value:
|
||||
locked: false
|
||||
description: "Linear client ID"
|
||||
description: 'Linear client ID'
|
||||
- name: LINEAR_CLIENT_SECRET
|
||||
display_title: "Linear Client Secret"
|
||||
display_title: 'Linear Client Secret'
|
||||
value:
|
||||
locked: false
|
||||
description: "Linear client secret"
|
||||
description: 'Linear client secret'
|
||||
type: secret
|
||||
## ------ End of Configs added for Linear ------ ##
|
||||
|
||||
## ------ Configs added for Notion ------ ##
|
||||
- name: NOTION_CLIENT_ID
|
||||
display_title: "Notion Client ID"
|
||||
display_title: 'Notion Client ID'
|
||||
value:
|
||||
locked: false
|
||||
description: "Notion client ID"
|
||||
description: 'Notion client ID'
|
||||
- name: NOTION_CLIENT_SECRET
|
||||
display_title: "Notion Client Secret"
|
||||
display_title: 'Notion Client Secret'
|
||||
value:
|
||||
locked: false
|
||||
description: "Notion client secret"
|
||||
description: 'Notion client secret'
|
||||
type: secret
|
||||
- name: NOTION_VERSION
|
||||
display_title: "Notion Version"
|
||||
value: "2022-06-28"
|
||||
display_title: 'Notion Version'
|
||||
value: '2022-06-28'
|
||||
locked: false
|
||||
description: "Notion version"
|
||||
description: 'Notion version'
|
||||
## ------ End of Configs added for Notion ------ ##
|
||||
|
||||
## ------ Configs added for Slack ------ ##
|
||||
- name: SLACK_CLIENT_ID
|
||||
display_title: "Slack Client ID"
|
||||
display_title: 'Slack Client ID'
|
||||
value:
|
||||
locked: false
|
||||
description: "Slack client ID"
|
||||
description: 'Slack client ID'
|
||||
- name: SLACK_CLIENT_SECRET
|
||||
display_title: "Slack Client Secret"
|
||||
display_title: 'Slack Client Secret'
|
||||
value:
|
||||
locked: false
|
||||
description: "Slack client secret"
|
||||
description: 'Slack client secret'
|
||||
type: secret
|
||||
## ------ End of Configs added for Slack ------ ##
|
||||
|
||||
# ------- Shopify Integration Config ------- #
|
||||
- name: SHOPIFY_CLIENT_ID
|
||||
display_title: "Shopify Client ID"
|
||||
description: "The Client ID (API Key) from your Shopify Partner account"
|
||||
display_title: 'Shopify Client ID'
|
||||
description: 'The Client ID (API Key) from your Shopify Partner account'
|
||||
locked: false
|
||||
type: secret
|
||||
- name: SHOPIFY_CLIENT_SECRET
|
||||
display_title: "Shopify Client Secret"
|
||||
description: "The Client Secret (API Secret Key) from your Shopify Partner account"
|
||||
display_title: 'Shopify Client Secret'
|
||||
description: 'The Client Secret (API Secret Key) from your Shopify Partner account'
|
||||
locked: false
|
||||
type: secret
|
||||
# ------- End of Shopify Related Config ------- #
|
||||
|
||||
# ------- Instagram Channel Related Config ------- #
|
||||
- name: INSTAGRAM_APP_ID
|
||||
display_title: "Instagram App ID"
|
||||
display_title: 'Instagram App ID'
|
||||
locked: false
|
||||
- name: INSTAGRAM_APP_SECRET
|
||||
display_title: "Instagram App Secret"
|
||||
description: "The App Secret used for Instagram authentication"
|
||||
display_title: 'Instagram App Secret'
|
||||
description: 'The App Secret used for Instagram authentication'
|
||||
locked: false
|
||||
type: secret
|
||||
- name: INSTAGRAM_VERIFY_TOKEN
|
||||
display_title: "Instagram Verify Token"
|
||||
description: "The verify token used for Instagram Webhook"
|
||||
display_title: 'Instagram Verify Token'
|
||||
description: 'The verify token used for Instagram Webhook'
|
||||
locked: false
|
||||
type: secret
|
||||
- name: ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT
|
||||
display_title: "Enable human agent for instagram channel"
|
||||
display_title: 'Enable human agent for instagram channel'
|
||||
value: false
|
||||
locked: false
|
||||
description: "Enable human agent for instagram channel for longer message back period. Needs additional app approval: https://developers.facebook.com/docs/features-reference/human-agent/"
|
||||
description: 'Enable human agent for instagram channel for longer message back period. Needs additional app approval: https://developers.facebook.com/docs/features-reference/human-agent/'
|
||||
type: boolean
|
||||
- name: INSTAGRAM_API_VERSION
|
||||
display_title: "Instagram API Version"
|
||||
description: "Configure this if you want to use a different Instagram API version. Make sure its prefixed with `v`"
|
||||
value: "v22.0"
|
||||
display_title: 'Instagram API Version'
|
||||
description: 'Configure this if you want to use a different Instagram API version. Make sure its prefixed with `v`'
|
||||
value: 'v22.0'
|
||||
locked: true
|
||||
# ------- End of Instagram Channel Related Config ------- #
|
||||
|
||||
# ------- OG Image Related Config ------- #
|
||||
- name: OG_IMAGE_CDN_URL
|
||||
display_title: "OG Image CDN URL"
|
||||
description: "The CDN URL for serving OG images"
|
||||
value: ""
|
||||
display_title: 'OG Image CDN URL'
|
||||
description: 'The CDN URL for serving OG images'
|
||||
value: ''
|
||||
locked: false
|
||||
- name: OG_IMAGE_CLIENT_REF
|
||||
display_title: "OG Image Client Reference"
|
||||
description: "Token used to block unauthorized access to OG images"
|
||||
value: ""
|
||||
display_title: 'OG Image Client Reference'
|
||||
description: 'Token used to block unauthorized access to OG images'
|
||||
value: ''
|
||||
locked: false
|
||||
type: secret
|
||||
# ------- End of OG Image Related Config ------- #
|
||||
|
||||
## ------ Configs added for Google OAuth ------ ##
|
||||
- name: GOOGLE_OAUTH_CLIENT_ID
|
||||
display_title: "Google OAuth Client ID"
|
||||
display_title: 'Google OAuth Client ID'
|
||||
value:
|
||||
locked: false
|
||||
description: "Google OAuth Client ID for email authentication"
|
||||
description: 'Google OAuth Client ID for email authentication'
|
||||
- name: GOOGLE_OAUTH_CLIENT_SECRET
|
||||
display_title: "Google OAuth Client Secret"
|
||||
display_title: 'Google OAuth Client Secret'
|
||||
value:
|
||||
locked: false
|
||||
description: "Google OAuth Client Secret for email authentication"
|
||||
description: 'Google OAuth Client Secret for email authentication'
|
||||
type: secret
|
||||
- name: GOOGLE_OAUTH_REDIRECT_URI
|
||||
display_title: "Google OAuth Redirect URI"
|
||||
display_title: 'Google OAuth Redirect URI'
|
||||
value:
|
||||
locked: false
|
||||
description: "The redirect URI configured in your Google OAuth app"
|
||||
description: 'The redirect URI configured in your Google OAuth app'
|
||||
- name: ENABLE_GOOGLE_OAUTH_LOGIN
|
||||
display_title: "Enable Google OAuth login"
|
||||
display_title: 'Enable Google OAuth login'
|
||||
value: true
|
||||
locked: false
|
||||
description: "Show Google OAuth as a login option when credentials are configured"
|
||||
description: 'Show Google OAuth as a login option when credentials are configured'
|
||||
type: boolean
|
||||
## ------ End of Configs added for Google OAuth ------ ##
|
||||
|
||||
## ------ Configs added for SAML SSO ------ ##
|
||||
- name: ENABLE_SAML_SSO_LOGIN
|
||||
display_title: "Enable SAML SSO login"
|
||||
display_title: 'Enable SAML SSO login'
|
||||
value: true
|
||||
locked: false
|
||||
description: "Show SAML SSO as a login option. Cannot be disabled if any users are using SAML authentication."
|
||||
description: 'Show SAML SSO as a login option. Cannot be disabled if any users are using SAML authentication.'
|
||||
type: boolean
|
||||
## ------ End of Configs added for SAML SSO ------ ##
|
||||
|
||||
## ------ Configs added for Cloudflare ------ ##
|
||||
- name: CLOUDFLARE_API_KEY
|
||||
display_title: "Cloudflare API Key"
|
||||
display_title: 'Cloudflare API Key'
|
||||
value:
|
||||
locked: false
|
||||
description: "API key for Cloudflare account authentication"
|
||||
description: 'API key for Cloudflare account authentication'
|
||||
type: secret
|
||||
- name: CLOUDFLARE_ZONE_ID
|
||||
display_title: "Cloudflare Zone ID"
|
||||
display_title: 'Cloudflare Zone ID'
|
||||
value:
|
||||
locked: false
|
||||
description: "Zone ID for the Cloudflare domain"
|
||||
description: 'Zone ID for the Cloudflare domain'
|
||||
## ------ End of Configs added for Cloudflare ------ ##
|
||||
|
||||
## ------ Customizations for Customers ------ ##
|
||||
- name: WIDGET_TOKEN_EXPIRY
|
||||
display_title: "Widget Token Expiry"
|
||||
display_title: 'Widget Token Expiry'
|
||||
value: 180
|
||||
locked: false
|
||||
description: "Token expiry in days"
|
||||
description: 'Token expiry in days'
|
||||
## ------ End of Customizations for Customers ------ ##
|
||||
|
||||
## ----- LLM Observability ---- ##
|
||||
- name: OTEL_PROVIDER
|
||||
display_title: "OpenTelemetry Provider"
|
||||
description: "LLM observability provider (langfuse, langsmith, etc.)"
|
||||
value: ""
|
||||
display_title: 'OpenTelemetry Provider'
|
||||
description: 'LLM observability provider (langfuse, langsmith, etc.)'
|
||||
value: ''
|
||||
locked: false
|
||||
- name: LANGFUSE_PUBLIC_KEY
|
||||
display_title: "Langfuse Public Key"
|
||||
description: "Public key for Langfuse authentication"
|
||||
value: ""
|
||||
display_title: 'Langfuse Public Key'
|
||||
description: 'Public key for Langfuse authentication'
|
||||
value: ''
|
||||
locked: false
|
||||
type: secret
|
||||
- name: LANGFUSE_SECRET_KEY
|
||||
display_title: "Langfuse Secret Key"
|
||||
description: "Secret key for Langfuse authentication"
|
||||
value: ""
|
||||
display_title: 'Langfuse Secret Key'
|
||||
description: 'Secret key for Langfuse authentication'
|
||||
value: ''
|
||||
locked: false
|
||||
type: secret
|
||||
- name: LANGFUSE_BASE_URL
|
||||
display_title: "Langfuse Base URL"
|
||||
description: "Langfuse endpoint (US: https://us.cloud.langfuse.com, EU: https://cloud.langfuse.com)"
|
||||
value: "https://us.cloud.langfuse.com"
|
||||
display_title: 'Langfuse Base URL'
|
||||
description: 'Langfuse endpoint (US: https://us.cloud.langfuse.com, EU: https://cloud.langfuse.com)'
|
||||
value: 'https://us.cloud.langfuse.com'
|
||||
locked: false
|
||||
## ---- End of LLM Observability ---- ##
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
providers:
|
||||
openai:
|
||||
display_name: "OpenAI"
|
||||
logo: openai.png
|
||||
|
||||
models:
|
||||
gpt-4.1:
|
||||
provider: openai
|
||||
display_name: "GPT-4.1"
|
||||
credit_multiplier: 0.5
|
||||
gpt-4.1-mini:
|
||||
provider: openai
|
||||
display_name: "GPT-4.1 Mini"
|
||||
credit_multiplier: 0.3
|
||||
gpt-4.1-nano:
|
||||
provider: openai
|
||||
display_name: "GPT-4.1 Nano"
|
||||
credit_multiplier: 0.1
|
||||
gpt-5:
|
||||
provider: openai
|
||||
display_name: "GPT-5"
|
||||
credit_multiplier: 1
|
||||
gpt-5.1:
|
||||
provider: openai
|
||||
display_name: "GPT-5.1"
|
||||
credit_multiplier: 1
|
||||
gpt-5-mini:
|
||||
provider: openai
|
||||
display_name: "GPT-5 Mini"
|
||||
credit_multiplier: 0.8
|
||||
gpt-5-nano:
|
||||
provider: openai
|
||||
display_name: "GPT-5 Nano"
|
||||
credit_multiplier: 0.3
|
||||
gpt-5.2:
|
||||
provider: openai
|
||||
display_name: "GPT-5.2"
|
||||
credit_multiplier: 1
|
||||
|
||||
features:
|
||||
editor:
|
||||
models:
|
||||
[gpt-4.1-mini, gpt-4.1-nano, gpt-5-mini, gpt-4.1, gpt-5, gpt-5.1, gpt-5.2]
|
||||
default: gpt-5-mini
|
||||
assistant:
|
||||
models: [gpt-5-mini, gpt-4.1, gpt-5, gpt-5.1, gpt-5.2]
|
||||
default: gpt-5-mini
|
||||
copilot:
|
||||
models: [gpt-5-mini, gpt-4.1, gpt-5, gpt-5.1, gpt-5.2]
|
||||
default: gpt-5-mini
|
||||
label_suggestion:
|
||||
models: [gpt-4.1-nano, gpt-4.1-mini, gpt-5-mini]
|
||||
default: gpt-4.1-nano
|
||||
# add deprecation_map later
|
||||
@@ -52,7 +52,6 @@ Rails.application.routes.draw do
|
||||
post :bulk_create, on: :collection
|
||||
end
|
||||
namespace :captain do
|
||||
resource :config, only: [:show], controller: 'config'
|
||||
resources :assistants do
|
||||
member do
|
||||
post :playground
|
||||
|
||||
@@ -87,15 +87,10 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
|
||||
I18n.with_locale(@assistant.account.locale) do
|
||||
create_handoff_message
|
||||
@conversation.bot_handoff!
|
||||
send_out_of_office_message_if_applicable
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def send_out_of_office_message_if_applicable
|
||||
::MessageTemplates::Template::OutOfOffice.perform_if_applicable(@conversation)
|
||||
end
|
||||
|
||||
def create_handoff_message
|
||||
create_outgoing_message(
|
||||
@assistant.config['handoff_message'].presence || I18n.t('conversations.captain.handoff')
|
||||
|
||||
@@ -9,24 +9,6 @@ module Enterprise::MessageTemplates::HookExecutionService
|
||||
schedule_captain_response
|
||||
end
|
||||
|
||||
def should_send_greeting?
|
||||
return false if captain_handling_conversation?
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def should_send_out_of_office_message?
|
||||
return false if captain_handling_conversation?
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def should_send_email_collect?
|
||||
return false if captain_handling_conversation?
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def schedule_captain_response
|
||||
@@ -64,14 +46,5 @@ module Enterprise::MessageTemplates::HookExecutionService
|
||||
content: 'Transferring to another agent for further assistance.'
|
||||
)
|
||||
conversation.bot_handoff!
|
||||
send_out_of_office_message_after_handoff
|
||||
end
|
||||
|
||||
def send_out_of_office_message_after_handoff
|
||||
::MessageTemplates::Template::OutOfOffice.perform_if_applicable(conversation)
|
||||
end
|
||||
|
||||
def captain_handling_conversation?
|
||||
conversation.pending? && inbox.respond_to?(:captain_assistant) && inbox.captain_assistant.present?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
# For all other LLM operations, use Llm::BaseAiService with RubyLLM instead.
|
||||
class Llm::LegacyBaseOpenAiService
|
||||
DEFAULT_MODEL = 'gpt-4.1-mini'
|
||||
DEFAULT_MODEL = 'gpt-4o-mini'
|
||||
|
||||
attr_reader :client, :model
|
||||
|
||||
|
||||
@@ -36,13 +36,6 @@ class Captain::Tools::HandoffTool < Captain::Tools::BasePublicTool
|
||||
|
||||
# Trigger the bot handoff (sets status to open + dispatches events)
|
||||
conversation.bot_handoff!
|
||||
|
||||
# Send out of office message if applicable (since template messages were suppressed while Captain was handling)
|
||||
send_out_of_office_message_if_applicable(conversation)
|
||||
end
|
||||
|
||||
def send_out_of_office_message_if_applicable(conversation)
|
||||
::MessageTemplates::Template::OutOfOffice.perform_if_applicable(conversation)
|
||||
end
|
||||
|
||||
# TODO: Future enhancement - Add team assignment capability
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module Enterprise::Integrations::OpenaiProcessorService
|
||||
ALLOWED_EVENT_NAMES = %w[rephrase summarize reply_suggestion label_suggestion fix_spelling_grammar shorten expand
|
||||
make_friendly make_formal simplify].freeze
|
||||
ALLOWED_EVENT_NAMES = %w[summarize reply_suggestion label_suggestion fix_spelling_grammar improve
|
||||
make_friendly make_formal casual professional confident straightforward].freeze
|
||||
CACHEABLE_EVENTS = %w[label_suggestion].freeze
|
||||
|
||||
def label_suggestion_message
|
||||
|
||||
@@ -7,7 +7,8 @@ class Integrations::LlmBaseService
|
||||
# 120000 * 4 = 480,000 characters (rounding off downwards to 400,000 to be safe)
|
||||
TOKEN_LIMIT = 400_000
|
||||
GPT_MODEL = Llm::Config::DEFAULT_MODEL
|
||||
ALLOWED_EVENT_NAMES = %w[rephrase summarize reply_suggestion fix_spelling_grammar shorten expand make_friendly make_formal simplify].freeze
|
||||
ALLOWED_EVENT_NAMES = %w[summarize reply_suggestion fix_spelling_grammar casual professional make_friendly make_formal confident
|
||||
straightforward improve].freeze
|
||||
CACHEABLE_EVENTS = %w[].freeze
|
||||
|
||||
pattr_initialize [:hook!, :event!]
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
You are an AI writing assistant integrated into Chatwoot, an omnichannel customer support platform.
|
||||
|
||||
{% if selection -%}
|
||||
# Task
|
||||
Improve ONLY the selected portion of the message. Return the COMPLETE message with the improved selection seamlessly integrated. Keep all other parts exactly as they are.
|
||||
|
||||
{% endif -%}
|
||||
{% if tone_instruction -%}
|
||||
# Tone
|
||||
Apply the following tone to {% if selection %}the selected portion{% else %}the message{% endif %}:
|
||||
<tone_instruction>
|
||||
{{ tone_instruction }}
|
||||
</tone_instruction>
|
||||
|
||||
{% endif -%}
|
||||
{% unless tone_instruction or selection -%}
|
||||
# Task
|
||||
Improve the message for clarity, grammar, spelling, and natural flow while maintaining the original tone and intent.
|
||||
|
||||
{% endunless -%}
|
||||
# Guidelines
|
||||
- Preserve the core meaning and all important information
|
||||
- Keep the message concise and appropriate for customer support
|
||||
- Do not add information that wasn't in the original
|
||||
- Do not remove critical details or instructions
|
||||
- Reply in the same language as the original message
|
||||
|
||||
Output only the final improved message without any preamble, tags or explanation.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
You are an AI writing assistant integrated into Chatwoot, an omnichannel customer support platform. Your task is to rewrite customer support message to match a specific tone while preserving the original meaning and intent.
|
||||
|
||||
Here is the tone to apply to the message you will receive:
|
||||
<tone_instruction>
|
||||
%s
|
||||
</tone_instruction>
|
||||
|
||||
Your task is to rewrite the message according to the specified tone instructions.
|
||||
|
||||
Important guidelines:
|
||||
- Preserve the core meaning and all important information from the original message
|
||||
- Keep the rewritten message concise and appropriate for customer support
|
||||
- Maintain helpfulness and respect regardless of tone
|
||||
- Do not add information that wasn't in the original message
|
||||
- Do not remove critical details or instructions
|
||||
- Ensure that the reply should be in user language.
|
||||
|
||||
Output only the rewritten message without any preamble, tags or explanation.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
class Integrations::Openai::ProcessorService < Integrations::LlmBaseService
|
||||
AGENT_INSTRUCTION = 'You are a helpful support agent.'.freeze
|
||||
LANGUAGE_INSTRUCTION = 'Ensure that the reply should be in user language.'.freeze
|
||||
def reply_suggestion_message
|
||||
make_api_call(reply_suggestion_body)
|
||||
@@ -9,39 +8,50 @@ class Integrations::Openai::ProcessorService < Integrations::LlmBaseService
|
||||
make_api_call(summarize_body)
|
||||
end
|
||||
|
||||
def rephrase_message
|
||||
make_api_call(build_api_call_body("#{AGENT_INSTRUCTION} Please rephrase the following response. " \
|
||||
"#{LANGUAGE_INSTRUCTION}"))
|
||||
def confident_message
|
||||
tone_instruction = determine_tone_instruction('confident')
|
||||
make_api_call(build_api_call_body(tone_rewrite_prompt(tone_instruction)))
|
||||
end
|
||||
|
||||
def fix_spelling_grammar_message
|
||||
make_api_call(build_api_call_body("#{AGENT_INSTRUCTION} Please fix the spelling and grammar of the following response. " \
|
||||
make_api_call(build_api_call_body('Please fix the spelling and grammar of the following response. ' \
|
||||
"#{LANGUAGE_INSTRUCTION}"))
|
||||
end
|
||||
|
||||
def shorten_message
|
||||
make_api_call(build_api_call_body("#{AGENT_INSTRUCTION} Please shorten the following response. " \
|
||||
"#{LANGUAGE_INSTRUCTION}"))
|
||||
def straightforward_message
|
||||
tone_instruction = determine_tone_instruction('straightforward')
|
||||
make_api_call(build_api_call_body(tone_rewrite_prompt(tone_instruction)))
|
||||
end
|
||||
|
||||
def expand_message
|
||||
make_api_call(build_api_call_body("#{AGENT_INSTRUCTION} Please expand the following response. " \
|
||||
"#{LANGUAGE_INSTRUCTION}"))
|
||||
def casual_message
|
||||
tone_instruction = determine_tone_instruction('casual')
|
||||
make_api_call(build_api_call_body(tone_rewrite_prompt(tone_instruction)))
|
||||
end
|
||||
|
||||
def make_friendly_message
|
||||
make_api_call(build_api_call_body("#{AGENT_INSTRUCTION} Please make the following response more friendly. " \
|
||||
"#{LANGUAGE_INSTRUCTION}"))
|
||||
tone_instruction = determine_tone_instruction('friendly')
|
||||
make_api_call(build_api_call_body(tone_rewrite_prompt(tone_instruction)))
|
||||
end
|
||||
|
||||
def make_formal_message
|
||||
make_api_call(build_api_call_body("#{AGENT_INSTRUCTION} Please make the following response more formal. " \
|
||||
"#{LANGUAGE_INSTRUCTION}"))
|
||||
tone_instruction = determine_tone_instruction('formal')
|
||||
make_api_call(build_api_call_body(tone_rewrite_prompt(tone_instruction)))
|
||||
end
|
||||
|
||||
def simplify_message
|
||||
make_api_call(build_api_call_body("#{AGENT_INSTRUCTION} Please simplify the following response. " \
|
||||
"#{LANGUAGE_INSTRUCTION}"))
|
||||
def professional_message
|
||||
tone_instruction = determine_tone_instruction('professional')
|
||||
make_api_call(build_api_call_body(tone_rewrite_prompt(tone_instruction)))
|
||||
end
|
||||
|
||||
def improve_message
|
||||
content = event['data']['content']
|
||||
selection = event['data']['selection'].presence
|
||||
tone = event['data']['tone'].presence
|
||||
|
||||
system_prompt = improve_prompt(selection, tone)
|
||||
user_content = selection ? improve_user_content(content, selection) : content
|
||||
|
||||
make_api_call(build_api_call_body(system_prompt, user_content))
|
||||
end
|
||||
|
||||
private
|
||||
@@ -51,6 +61,25 @@ class Integrations::Openai::ProcessorService < Integrations::LlmBaseService
|
||||
Rails.root.join(path, "#{file_name}.txt").read
|
||||
end
|
||||
|
||||
def tone_rewrite_prompt(tone_instruction)
|
||||
format(prompt_from_file('tone_rewrite'), tone_instruction)
|
||||
end
|
||||
|
||||
def improve_prompt(selection, tone)
|
||||
tone_instruction = determine_tone_instruction(tone) if tone.present?
|
||||
render_liquid_prompt('improve', { selection: selection, tone_instruction: tone_instruction })
|
||||
end
|
||||
|
||||
def improve_user_content(content, selection)
|
||||
"Full message:\n#{content}\n\nSelected portion to improve:\n#{selection}"
|
||||
end
|
||||
|
||||
def render_liquid_prompt(template_name, context = {})
|
||||
template_path = Rails.root.join('lib/integrations/openai/openai_prompts', "#{template_name}.liquid")
|
||||
template = Liquid::Template.parse(template_path.read)
|
||||
template.render(context.deep_stringify_keys)
|
||||
end
|
||||
|
||||
def build_api_call_body(system_content, user_content = event['data']['content'])
|
||||
{
|
||||
model: GPT_MODEL,
|
||||
@@ -133,6 +162,26 @@ class Integrations::Openai::ProcessorService < Integrations::LlmBaseService
|
||||
].concat(conversation_messages(in_array_format: true))
|
||||
}.to_json
|
||||
end
|
||||
|
||||
def determine_tone_instruction(tone)
|
||||
case tone
|
||||
when 'friendly'
|
||||
'Warm, approachable, and personable. Use conversational language, positive words, and show empathy. ' \
|
||||
'May include phrases like \"Happy to help!\" or \"I\'d be glad to...\"'
|
||||
when 'confident'
|
||||
'Assertive and assured. Use definitive language, avoid hedging words like \"maybe\" or \"I think\". ' \
|
||||
'Be direct and authoritative while remaining helpful.'
|
||||
when 'straightforward'
|
||||
'Clear, direct, and to-the-point. Remove unnecessary words, get straight to the information or solution. No fluff or extra pleasantries.'
|
||||
when 'casual'
|
||||
'Relaxed and informal. Use contractions, simpler words, and a conversational style. Friendly but less formal than professional tone.'
|
||||
when 'professional'
|
||||
'Formal, polished, and business-appropriate. Use complete sentences, proper grammar, ' \
|
||||
'and maintain respectful distance. Avoid slang or overly casual language.'
|
||||
else
|
||||
determine_tone_instruction('friendly')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Integrations::Openai::ProcessorService.prepend_mod_with('Integrations::OpenaiProcessorService')
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
require 'ruby_llm'
|
||||
|
||||
module Llm::Config
|
||||
DEFAULT_MODEL = 'gpt-4.1-mini'.freeze
|
||||
DEFAULT_MODEL = 'gpt-4o-mini'.freeze
|
||||
class << self
|
||||
def initialized?
|
||||
@initialized ||= false
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Llm::ConfigService
|
||||
CONFIG_PATH = Rails.root.join('config/llm.yml').freeze
|
||||
|
||||
class << self
|
||||
def config
|
||||
@config ||= load_config
|
||||
end
|
||||
|
||||
def reload!
|
||||
@config = load_config
|
||||
end
|
||||
|
||||
%w[providers models features].each do |key|
|
||||
define_method(key) do
|
||||
config[key] || {}
|
||||
end
|
||||
end
|
||||
|
||||
def models_for_feature(feature_key)
|
||||
features.dig(feature_key.to_s, 'models') || []
|
||||
end
|
||||
|
||||
def default_model_for_feature(feature_key)
|
||||
features.dig(feature_key.to_s, 'default')
|
||||
end
|
||||
|
||||
def model_info(model_name)
|
||||
models[model_name.to_s]
|
||||
end
|
||||
|
||||
def provider_info(provider_name)
|
||||
providers[provider_name.to_s]
|
||||
end
|
||||
|
||||
def feature_keys
|
||||
features.keys
|
||||
end
|
||||
|
||||
def valid_model_for_feature?(feature_key, model_name)
|
||||
allowed_models = models_for_feature(feature_key)
|
||||
allowed_models.include?(model_name.to_s)
|
||||
end
|
||||
|
||||
def feature_config(feature_key)
|
||||
feature = features[feature_key.to_s]
|
||||
return nil unless feature
|
||||
|
||||
{
|
||||
models: feature['models'].filter_map do |model_name|
|
||||
model = models[model_name]
|
||||
next nil unless model
|
||||
|
||||
{
|
||||
id: model_name,
|
||||
display_name: model['display_name'],
|
||||
provider: model['provider'],
|
||||
credit_multiplier: model['credit_multiplier']
|
||||
}
|
||||
end,
|
||||
default: feature['default']
|
||||
}
|
||||
end
|
||||
|
||||
def all_features_config
|
||||
features.keys.index_with do |feature_key|
|
||||
feature_config(feature_key)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_config
|
||||
return {} unless File.exist?(CONFIG_PATH)
|
||||
|
||||
YAML.load_file(CONFIG_PATH) || {}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,50 +0,0 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::Accounts::Captain::Config', 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/config' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v1/accounts/#{account.id}/captain/config",
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an agent' do
|
||||
it 'returns available AI model options for each feature' do
|
||||
get "/api/v1/accounts/#{account.id}/captain/config",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
%w[editor assistant copilot label_suggestion].each do |feature|
|
||||
expect(json_response.dig(:features, feature.to_sym, :models)).to be_present
|
||||
expect(json_response.dig(:features, feature.to_sym, :default)).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an admin' do
|
||||
it 'returns providers, models, and features configuration' do
|
||||
get "/api/v1/accounts/#{account.id}/captain/config",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(json_response).to have_key(:providers)
|
||||
expect(json_response).to have_key(:models)
|
||||
expect(json_response).to have_key(:features)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -229,106 +229,4 @@ RSpec.describe Captain::Conversation::ResponseBuilderJob, type: :job do
|
||||
expect(described_class::MAX_MESSAGE_LENGTH).to eq(10_000)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'out of office message after handoff' do
|
||||
let(:conversation) { create(:conversation, inbox: inbox, account: account, status: :pending) }
|
||||
let(:mock_llm_chat_service) { instance_double(Captain::Llm::AssistantChatService) }
|
||||
|
||||
before do
|
||||
create(:message, conversation: conversation, content: 'Hello', message_type: :incoming)
|
||||
allow(Captain::Llm::AssistantChatService).to receive(:new).and_return(mock_llm_chat_service)
|
||||
allow(account).to receive(:feature_enabled?).and_return(false)
|
||||
allow(account).to receive(:feature_enabled?).with('captain_integration_v2').and_return(false)
|
||||
end
|
||||
|
||||
context 'when handoff occurs outside business hours' do
|
||||
before do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed. Please leave your email.'
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
allow(mock_llm_chat_service).to receive(:generate_response).and_return({ 'response' => 'conversation_handoff' })
|
||||
end
|
||||
|
||||
it 'sends out of office message after handoff' do
|
||||
expect do
|
||||
described_class.perform_now(conversation, assistant)
|
||||
end.to change { conversation.messages.template.count }.by(1)
|
||||
|
||||
expect(conversation.reload.status).to eq('open')
|
||||
ooo_message = conversation.messages.template.last
|
||||
expect(ooo_message.content).to eq('We are currently closed. Please leave your email.')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when handoff occurs within business hours' do
|
||||
before do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed.'
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
open_all_day: true,
|
||||
closed_all_day: false
|
||||
)
|
||||
allow(mock_llm_chat_service).to receive(:generate_response).and_return({ 'response' => 'conversation_handoff' })
|
||||
end
|
||||
|
||||
it 'does not send out of office message after handoff' do
|
||||
expect do
|
||||
described_class.perform_now(conversation, assistant)
|
||||
end.not_to(change { conversation.messages.template.count })
|
||||
|
||||
expect(conversation.reload.status).to eq('open')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when handoff occurs due to error outside business hours' do
|
||||
before do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed.'
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
allow(mock_llm_chat_service).to receive(:generate_response).and_raise(StandardError, 'API error')
|
||||
end
|
||||
|
||||
it 'sends out of office message after error-triggered handoff' do
|
||||
expect do
|
||||
described_class.perform_now(conversation, assistant)
|
||||
end.to change { conversation.messages.template.count }.by(1)
|
||||
|
||||
expect(conversation.reload.status).to eq('open')
|
||||
ooo_message = conversation.messages.template.last
|
||||
expect(ooo_message.content).to eq('We are currently closed.')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no out of office message is configured' do
|
||||
before do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: nil
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
allow(mock_llm_chat_service).to receive(:generate_response).and_return({ 'response' => 'conversation_handoff' })
|
||||
end
|
||||
|
||||
it 'does not send out of office message' do
|
||||
expect do
|
||||
described_class.perform_now(conversation, assistant)
|
||||
end.not_to(change { conversation.messages.template.count })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -163,66 +163,4 @@ RSpec.describe Captain::Tools::HandoffTool, type: :model do
|
||||
expect(tool.active?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'out of office message after handoff' do
|
||||
context 'when outside business hours' do
|
||||
before do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed. Please leave your email.'
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
end
|
||||
|
||||
it 'sends out of office message after handoff' do
|
||||
expect do
|
||||
tool.perform(tool_context, reason: 'Customer needs help')
|
||||
end.to change { conversation.messages.template.count }.by(1)
|
||||
|
||||
ooo_message = conversation.messages.template.last
|
||||
expect(ooo_message.content).to eq('We are currently closed. Please leave your email.')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when within business hours' do
|
||||
before do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed.'
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
open_all_day: true,
|
||||
closed_all_day: false
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not send out of office message after handoff' do
|
||||
expect do
|
||||
tool.perform(tool_context, reason: 'Customer needs help')
|
||||
end.not_to(change { conversation.messages.template.count })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no out of office message is configured' do
|
||||
before do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: nil
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not send out of office message' do
|
||||
expect do
|
||||
tool.perform(tool_context, reason: 'Customer needs help')
|
||||
end.not_to(change { conversation.messages.template.count })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe MessageTemplates::HookExecutionService do
|
||||
let(:account) { create(:account, custom_attributes: { plan_name: 'startups' }) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:contact) { create(:contact, account: account) }
|
||||
let(:conversation) { create(:conversation, inbox: inbox, account: account, contact: contact, status: :pending) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
|
||||
before do
|
||||
create(:captain_inbox, captain_assistant: assistant, inbox: inbox)
|
||||
end
|
||||
|
||||
context 'when captain assistant is configured' do
|
||||
context 'when within business hours' do
|
||||
before do
|
||||
inbox.update!(working_hours_enabled: true)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
open_all_day: true,
|
||||
closed_all_day: false
|
||||
)
|
||||
end
|
||||
|
||||
it 'schedules captain response job for incoming messages on pending conversations' do
|
||||
expect(Captain::Conversation::ResponseBuilderJob).to receive(:perform_later).with(conversation, assistant)
|
||||
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when outside business hours' do
|
||||
before do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed'
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
end
|
||||
|
||||
it 'schedules captain response job outside business hours (Captain always responds when configured)' do
|
||||
expect(Captain::Conversation::ResponseBuilderJob).to receive(:perform_later).with(conversation, assistant)
|
||||
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end
|
||||
|
||||
it 'performs captain handoff when quota is exceeded (OOO template will kick in after handoff)' do
|
||||
account.update!(
|
||||
limits: { 'captain_responses' => 100 },
|
||||
custom_attributes: account.custom_attributes.merge('captain_responses_usage' => 100)
|
||||
)
|
||||
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
|
||||
expect(conversation.reload.status).to eq('open')
|
||||
end
|
||||
|
||||
it 'does not send out of office message when Captain is handling' do
|
||||
out_of_office_service = instance_double(MessageTemplates::Template::OutOfOffice)
|
||||
allow(MessageTemplates::Template::OutOfOffice).to receive(:new).and_return(out_of_office_service)
|
||||
allow(out_of_office_service).to receive(:perform).and_return(true)
|
||||
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
|
||||
expect(MessageTemplates::Template::OutOfOffice).not_to have_received(:new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when business hours are not enabled' do
|
||||
before do
|
||||
inbox.update!(working_hours_enabled: false)
|
||||
end
|
||||
|
||||
it 'schedules captain response job regardless of time' do
|
||||
expect(Captain::Conversation::ResponseBuilderJob).to receive(:perform_later).with(conversation, assistant)
|
||||
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when captain quota is exceeded within business hours' do
|
||||
before do
|
||||
inbox.update!(working_hours_enabled: true)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
open_all_day: true,
|
||||
closed_all_day: false
|
||||
)
|
||||
|
||||
account.update!(
|
||||
limits: { 'captain_responses' => 100 },
|
||||
custom_attributes: account.custom_attributes.merge('captain_responses_usage' => 100)
|
||||
)
|
||||
end
|
||||
|
||||
it 'performs handoff within business hours when quota exceeded' do
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
|
||||
expect(conversation.reload.status).to eq('open')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no captain assistant is configured' do
|
||||
before do
|
||||
CaptainInbox.where(inbox: inbox).destroy_all
|
||||
end
|
||||
|
||||
it 'does not schedule captain response job' do
|
||||
expect(Captain::Conversation::ResponseBuilderJob).not_to receive(:perform_later)
|
||||
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation is not pending' do
|
||||
before do
|
||||
conversation.update!(status: :open)
|
||||
end
|
||||
|
||||
it 'does not schedule captain response job' do
|
||||
expect(Captain::Conversation::ResponseBuilderJob).not_to receive(:perform_later)
|
||||
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when message is outgoing' do
|
||||
it 'does not schedule captain response job' do
|
||||
expect(Captain::Conversation::ResponseBuilderJob).not_to receive(:perform_later)
|
||||
|
||||
create(:message, conversation: conversation, message_type: :outgoing)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when greeting and out of office messages with Captain enabled' do
|
||||
context 'when conversation is pending (Captain is handling)' do
|
||||
before do
|
||||
conversation.update!(status: :pending)
|
||||
end
|
||||
|
||||
it 'does not create greeting message in conversation' do
|
||||
inbox.update!(greeting_enabled: true, greeting_message: 'Hello! How can we help you?', enable_email_collect: false)
|
||||
|
||||
expect do
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end.not_to(change { conversation.reload.messages.template.count })
|
||||
end
|
||||
|
||||
it 'does not create out of office message in conversation' do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed',
|
||||
enable_email_collect: false
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
|
||||
expect do
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end.not_to(change { conversation.reload.messages.template.count })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation is open (transferred to agent)' do
|
||||
before do
|
||||
conversation.update!(status: :open)
|
||||
end
|
||||
|
||||
it 'creates greeting message in conversation' do
|
||||
inbox.update!(greeting_enabled: true, greeting_message: 'Hello! How can we help you?', enable_email_collect: false)
|
||||
|
||||
expect do
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end.to change { conversation.reload.messages.template.count }.by(1)
|
||||
|
||||
greeting_message = conversation.reload.messages.template.last
|
||||
expect(greeting_message.content).to eq('Hello! How can we help you?')
|
||||
end
|
||||
|
||||
it 'creates out of office message when outside business hours' do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed',
|
||||
enable_email_collect: false
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
|
||||
expect do
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end.to change { conversation.reload.messages.template.count }.by(1)
|
||||
|
||||
out_of_office_message = conversation.reload.messages.template.last
|
||||
expect(out_of_office_message.content).to eq('We are currently closed')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Captain is not configured' do
|
||||
before do
|
||||
CaptainInbox.where(inbox: inbox).destroy_all
|
||||
end
|
||||
|
||||
it 'creates greeting message in conversation' do
|
||||
inbox.update!(greeting_enabled: true, greeting_message: 'Hello! How can we help you?', enable_email_collect: false)
|
||||
|
||||
expect do
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end.to change { conversation.reload.messages.template.count }.by(1)
|
||||
|
||||
greeting_message = conversation.reload.messages.template.last
|
||||
expect(greeting_message.content).to eq('Hello! How can we help you?')
|
||||
end
|
||||
|
||||
it 'creates out of office message when outside business hours' do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed',
|
||||
enable_email_collect: false
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
|
||||
expect do
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end.to change { conversation.reload.messages.template.count }.by(1)
|
||||
|
||||
out_of_office_message = conversation.reload.messages.template.last
|
||||
expect(out_of_office_message.content).to eq('We are currently closed')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Captain quota is exceeded and handoff happens' do
|
||||
before do
|
||||
account.update!(
|
||||
limits: { 'captain_responses' => 100 },
|
||||
custom_attributes: account.custom_attributes.merge('captain_responses_usage' => 100)
|
||||
)
|
||||
end
|
||||
|
||||
context 'when outside business hours' do
|
||||
before do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed. Please leave your email.',
|
||||
enable_email_collect: false
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
end
|
||||
|
||||
it 'sends out of office message after handoff due to quota exceeded' do
|
||||
expect do
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end.to change { conversation.messages.template.count }.by(1)
|
||||
|
||||
expect(conversation.reload.status).to eq('open')
|
||||
ooo_message = conversation.messages.template.last
|
||||
expect(ooo_message.content).to eq('We are currently closed. Please leave your email.')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when within business hours' do
|
||||
before do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed.',
|
||||
enable_email_collect: false
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
open_all_day: true,
|
||||
closed_all_day: false
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not send out of office message after handoff' do
|
||||
expect do
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end.not_to(change { conversation.messages.template.count })
|
||||
|
||||
expect(conversation.reload.status).to eq('open')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -9,7 +9,7 @@ RSpec.describe Messages::AudioTranscriptionService, type: :service do
|
||||
before do
|
||||
# Create required installation configs
|
||||
create(:installation_config, name: 'CAPTAIN_OPEN_AI_API_KEY', value: 'test-api-key')
|
||||
create(:installation_config, name: 'CAPTAIN_OPEN_AI_MODEL', value: 'gpt-4.1-mini')
|
||||
create(:installation_config, name: 'CAPTAIN_OPEN_AI_MODEL', value: 'gpt-4o-mini')
|
||||
|
||||
# Mock usage limits for transcription to be available
|
||||
allow(account).to receive(:usage_limits).and_return({ captain: { responses: { current_available: 100 } } })
|
||||
|
||||
@@ -20,7 +20,7 @@ RSpec.describe Integrations::LlmInstrumentation do
|
||||
account_id: 123,
|
||||
conversation_id: 456,
|
||||
feature_name: 'reply_suggestion',
|
||||
model: 'gpt-4.1-mini',
|
||||
model: 'gpt-4o-mini',
|
||||
messages: [{ 'role' => 'user', 'content' => 'Hello' }],
|
||||
temperature: 0.7
|
||||
}
|
||||
@@ -105,7 +105,7 @@ RSpec.describe Integrations::LlmInstrumentation do
|
||||
instance.instrument_llm_call(params) { 'result' }
|
||||
|
||||
expect(mock_span).to have_received(:set_attribute).with('gen_ai.provider.name', 'openai')
|
||||
expect(mock_span).to have_received(:set_attribute).with('gen_ai.request.model', 'gpt-4.1-mini')
|
||||
expect(mock_span).to have_received(:set_attribute).with('gen_ai.request.model', 'gpt-4o-mini')
|
||||
expect(mock_span).to have_received(:set_attribute).with('gen_ai.request.temperature', 0.7)
|
||||
end
|
||||
|
||||
|
||||
@@ -53,17 +53,23 @@ RSpec.describe Integrations::Openai::ProcessorService do
|
||||
|
||||
it 'sets system instructions' do
|
||||
service.perform
|
||||
expect(mock_chat).to have_received(:with_instructions).with(a_string_including('You are a helpful support agent'))
|
||||
if event_name == 'fix_spelling_grammar'
|
||||
expect(mock_chat).to have_received(:with_instructions)
|
||||
.with(a_string_including('Please fix the spelling and grammar'))
|
||||
else
|
||||
expect(mock_chat).to have_received(:with_instructions)
|
||||
.with(a_string_including('You are an AI writing assistant integrated into Chatwoot'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'text transformation operation', 'rephrase'
|
||||
it_behaves_like 'text transformation operation', 'confident'
|
||||
it_behaves_like 'text transformation operation', 'fix_spelling_grammar'
|
||||
it_behaves_like 'text transformation operation', 'shorten'
|
||||
it_behaves_like 'text transformation operation', 'expand'
|
||||
it_behaves_like 'text transformation operation', 'casual'
|
||||
it_behaves_like 'text transformation operation', 'professional'
|
||||
it_behaves_like 'text transformation operation', 'make_friendly'
|
||||
it_behaves_like 'text transformation operation', 'make_formal'
|
||||
it_behaves_like 'text transformation operation', 'simplify'
|
||||
it_behaves_like 'text transformation operation', 'straightforward'
|
||||
end
|
||||
|
||||
describe 'conversation-based operations' do
|
||||
@@ -125,7 +131,7 @@ RSpec.describe Integrations::Openai::ProcessorService do
|
||||
end
|
||||
|
||||
describe 'response structure' do
|
||||
let(:event) { { 'name' => 'rephrase', 'data' => { 'content' => 'test message' } } }
|
||||
let(:event) { { 'name' => 'confident', 'data' => { 'content' => 'test message' } } }
|
||||
|
||||
context 'when response includes usage data' do
|
||||
before do
|
||||
@@ -166,10 +172,13 @@ RSpec.describe Integrations::Openai::ProcessorService do
|
||||
end
|
||||
|
||||
describe 'endpoint configuration' do
|
||||
let(:event) { { 'name' => 'rephrase', 'data' => { 'content' => 'test message' } } }
|
||||
let(:event) { { 'name' => 'confident', 'data' => { 'content' => 'test message' } } }
|
||||
|
||||
context 'without CAPTAIN_OPEN_AI_ENDPOINT configured' do
|
||||
before { InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.destroy }
|
||||
before do
|
||||
InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.destroy
|
||||
allow(Llm::Config).to receive(:with_api_key).and_call_original
|
||||
end
|
||||
|
||||
it 'uses default OpenAI endpoint' do
|
||||
expect(Llm::Config).to receive(:with_api_key).with(
|
||||
@@ -185,6 +194,7 @@ RSpec.describe Integrations::Openai::ProcessorService do
|
||||
before do
|
||||
InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.destroy
|
||||
create(:installation_config, name: 'CAPTAIN_OPEN_AI_ENDPOINT', value: 'https://custom.azure.com/')
|
||||
allow(Llm::Config).to receive(:with_api_key).and_call_original
|
||||
end
|
||||
|
||||
it 'uses custom endpoint' do
|
||||
|
||||
@@ -218,59 +218,4 @@ RSpec.describe Account do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'captain_preferences' do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe 'with no saved preferences' do
|
||||
it 'returns defaults from llm.yml' do
|
||||
prefs = account.captain_preferences
|
||||
|
||||
expect(prefs[:features].values).to all(be false)
|
||||
|
||||
Llm::ConfigService.feature_keys.each do |feature|
|
||||
expect(prefs[:models][feature]).to eq(Llm::ConfigService.default_model_for_feature(feature))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with saved model preferences' do
|
||||
it 'returns saved preferences merged with defaults' do
|
||||
account.update!(captain_models: { 'editor' => 'gpt-4o', 'assistant' => 'gpt-5' })
|
||||
|
||||
prefs = account.captain_preferences
|
||||
|
||||
expect(prefs[:models]['editor']).to eq('gpt-4o')
|
||||
expect(prefs[:models]['assistant']).to eq('gpt-5')
|
||||
expect(prefs[:models]['copilot']).to eq(Llm::ConfigService.default_model_for_feature('copilot'))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with saved feature preferences' do
|
||||
it 'returns saved feature states' do
|
||||
account.update!(captain_features: { 'editor' => true, 'assistant' => true })
|
||||
|
||||
prefs = account.captain_preferences
|
||||
|
||||
expect(prefs[:features]['editor']).to be true
|
||||
expect(prefs[:features]['assistant']).to be true
|
||||
expect(prefs[:features]['copilot']).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validation' do
|
||||
it 'rejects invalid model for a feature' do
|
||||
account.captain_models = { 'label_suggestion' => 'gpt-5' }
|
||||
|
||||
expect(account).not_to be_valid
|
||||
expect(account.errors[:captain_models].first).to include('not a valid model for label_suggestion')
|
||||
end
|
||||
|
||||
it 'accepts valid model for a feature' do
|
||||
account.captain_models = { 'editor' => 'gpt-4o', 'label_suggestion' => 'gpt-4.1-nano' }
|
||||
|
||||
expect(account).to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user