mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +00:00
feat: Integration with Captain (alpha) (#9834)
- Integration with captain (alpha) Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
@@ -257,3 +257,5 @@ AZURE_APP_SECRET=
|
||||
# Set to true if you want to remove stale contact inboxes
|
||||
# contact_inboxes with no conversation older than 90 days will be removed
|
||||
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
|
||||
|
||||
# CAPTAIN_API_URL=http://localhost:3001/api
|
||||
|
||||
@@ -10,7 +10,7 @@ class Api::V1::Accounts::Integrations::AppsController < Api::V1::Accounts::BaseC
|
||||
private
|
||||
|
||||
def fetch_apps
|
||||
@apps = Integrations::App.all.select(&:active?)
|
||||
@apps = Integrations::App.all.select { |app| app.active?(Current.account) }
|
||||
end
|
||||
|
||||
def fetch_app
|
||||
|
||||
@@ -9,6 +9,8 @@ class HookJob < ApplicationJob
|
||||
process_slack_integration(hook, event_name, event_data)
|
||||
when 'dialogflow'
|
||||
process_dialogflow_integration(hook, event_name, event_data)
|
||||
when 'captain'
|
||||
process_captain_integration(hook, event_name, event_data)
|
||||
when 'google_translate'
|
||||
google_translate_integration(hook, event_name, event_data)
|
||||
end
|
||||
@@ -35,6 +37,12 @@ class HookJob < ApplicationJob
|
||||
Integrations::Dialogflow::ProcessorService.new(event_name: event_name, hook: hook, event_data: event_data).perform
|
||||
end
|
||||
|
||||
def process_captain_integration(hook, event_name, event_data)
|
||||
return unless ['message.created'].include?(event_name)
|
||||
|
||||
Integrations::Captain::ProcessorService.new(event_name: event_data, hook: hook, event_data: event_data).perform
|
||||
end
|
||||
|
||||
def google_translate_integration(hook, event_name, event_data)
|
||||
return unless ['message.created'].include?(event_name)
|
||||
|
||||
|
||||
@@ -129,7 +129,16 @@ class Inbox < ApplicationRecord
|
||||
end
|
||||
|
||||
def active_bot?
|
||||
agent_bot_inbox&.active? || hooks.where(app_id: 'dialogflow', status: 'enabled').count.positive?
|
||||
agent_bot_inbox&.active? || hooks.where(app_id: %w[dialogflow],
|
||||
status: 'enabled').count.positive? || captain_enabled?
|
||||
end
|
||||
|
||||
def captain_enabled?
|
||||
captain_hook = account.hooks.where(
|
||||
app_id: %w[captain], status: 'enabled'
|
||||
).first
|
||||
|
||||
captain_hook.present? && captain_hook.settings['inbox_ids'].split(',').include?(id.to_s)
|
||||
end
|
||||
|
||||
def inbox_type
|
||||
|
||||
@@ -34,12 +34,14 @@ class Integrations::App
|
||||
end
|
||||
end
|
||||
|
||||
def active?
|
||||
def active?(account)
|
||||
case params[:id]
|
||||
when 'slack'
|
||||
ENV['SLACK_CLIENT_SECRET'].present?
|
||||
when 'linear'
|
||||
Current.account.feature_enabled?('linear_integration')
|
||||
account.feature_enabled?('linear_integration')
|
||||
when 'captain'
|
||||
account.feature_enabled?('captain_integration') && ENV['CAPTAIN_API_URL'].present?
|
||||
else
|
||||
true
|
||||
end
|
||||
|
||||
@@ -87,3 +87,5 @@
|
||||
premium: true
|
||||
- name: linear_integration
|
||||
enabled: false
|
||||
- name: captain_integration
|
||||
enabled: false
|
||||
|
||||
@@ -8,7 +8,58 @@
|
||||
# settings_json_schema: the json schema used to validate the settings hash (https://json-schema.org/)
|
||||
# settings_form_schema: the formulate schema used in frontend to render settings form (https://vueformulate.com/)
|
||||
########################################################
|
||||
|
||||
captain:
|
||||
id: captain
|
||||
logo: captain.png
|
||||
i18n_key: captain
|
||||
action: /captain
|
||||
hook_type: account
|
||||
allow_multiple_hooks: false
|
||||
settings_json_schema: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_token": { "type": "string" },
|
||||
"account_id": { "type": "string" },
|
||||
"account_email": { "type": "string" },
|
||||
"assistant_id": { "type": "string" },
|
||||
"inbox_ids": { "type": "strings" },
|
||||
},
|
||||
"required": ["access_token", "account_id", "account_email", "assistant_id"],
|
||||
"additionalProperties": false,
|
||||
}
|
||||
settings_form_schema: [
|
||||
{
|
||||
"label": "Access Token",
|
||||
"type": "text",
|
||||
"name": "access_token",
|
||||
"validation": "required",
|
||||
},
|
||||
{
|
||||
"label": "Account ID",
|
||||
"type": "text",
|
||||
"name": "account_id",
|
||||
"validation": "required",
|
||||
},
|
||||
{
|
||||
"label": "Account Email",
|
||||
"type": "text",
|
||||
"name": "account_email",
|
||||
"validation": "required",
|
||||
},
|
||||
{
|
||||
"label": "Assistant Id",
|
||||
"type": "text",
|
||||
"name": "assistant_id",
|
||||
"validation": "required",
|
||||
},
|
||||
{
|
||||
"label": "Inbox Ids",
|
||||
"type": "text",
|
||||
"name": "inbox_ids",
|
||||
"validation": "",
|
||||
},
|
||||
]
|
||||
visible_properties: []
|
||||
webhooks:
|
||||
id: webhook
|
||||
logo: webhooks.png
|
||||
|
||||
@@ -227,6 +227,9 @@ en:
|
||||
linear:
|
||||
name: "Linear"
|
||||
description: "Create issues in Linear directly from your conversation window. Alternatively, link existing Linear issues for a more streamlined and efficient issue tracking process."
|
||||
captain:
|
||||
name: "Captain"
|
||||
description: "Captain is a native AI assistant built for your product and trained on your company's knowledge base. It responds like a human and resolves customer queries effectively. Configure it to your inboxes easily."
|
||||
public_portal:
|
||||
search:
|
||||
search_placeholder: Search for article by title or body...
|
||||
|
||||
65
lib/integrations/captain/processor_service.rb
Normal file
65
lib/integrations/captain/processor_service.rb
Normal file
@@ -0,0 +1,65 @@
|
||||
class Integrations::Captain::ProcessorService < Integrations::BotProcessorService
|
||||
pattr_initialize [:event_name!, :hook!, :event_data!]
|
||||
|
||||
private
|
||||
|
||||
def get_response(_session_id, message_content)
|
||||
call_captain(message_content)
|
||||
end
|
||||
|
||||
def process_response(message, response)
|
||||
if response == 'conversation_handoff'
|
||||
message.conversation.bot_handoff!
|
||||
else
|
||||
create_conversation(message, { content: response })
|
||||
end
|
||||
end
|
||||
|
||||
def create_conversation(message, content_params)
|
||||
return if content_params.blank?
|
||||
|
||||
conversation = message.conversation
|
||||
conversation.messages.create!(
|
||||
content_params.merge(
|
||||
{
|
||||
message_type: :outgoing,
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id
|
||||
}
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def call_captain(message_content)
|
||||
url = "#{ENV.fetch('CAPTAIN_API_URL', nil)}/accounts/#{hook.settings['account_id']}/assistants/#{hook.settings['assistant_id']}/chat"
|
||||
|
||||
headers = {
|
||||
'X-USER-EMAIL' => hook.settings['account_email'],
|
||||
'X-USER-TOKEN' => hook.settings['access_token'],
|
||||
'Content-Type' => 'application/json'
|
||||
}
|
||||
|
||||
body = {
|
||||
message: message_content,
|
||||
previous_messages: previous_messages
|
||||
}
|
||||
|
||||
response = HTTParty.post(url, headers: headers, body: body.to_json)
|
||||
response.parsed_response['message']
|
||||
end
|
||||
|
||||
def previous_messages
|
||||
previous_messages = []
|
||||
conversation.messages.where(message_type: [:outgoing, :incoming]).where(private: false).offset(1).find_each do |message|
|
||||
next if message.content_type != 'text'
|
||||
|
||||
role = determine_role(message)
|
||||
previous_messages << { message: message.content, type: role }
|
||||
end
|
||||
previous_messages
|
||||
end
|
||||
|
||||
def determine_role(message)
|
||||
message.message_type == 'incoming' ? 'User' : 'Bot'
|
||||
end
|
||||
end
|
||||
@@ -14,13 +14,13 @@ class Integrations::Dialogflow::ProcessorService < Integrations::BotProcessorSer
|
||||
message.content
|
||||
end
|
||||
|
||||
def get_response(session_id, message)
|
||||
def get_response(session_id, message_content)
|
||||
if hook.settings['credentials'].blank?
|
||||
Rails.logger.warn "Account: #{hook.try(:account_id)} Hook: #{hook.id} credentials are not present." && return
|
||||
end
|
||||
|
||||
configure_dialogflow_client_defaults
|
||||
detect_intent(session_id, message)
|
||||
detect_intent(session_id, message_content)
|
||||
rescue Google::Cloud::PermissionDeniedError => e
|
||||
Rails.logger.warn "DialogFlow Error: (account-#{hook.try(:account_id)}, hook-#{hook.id}) #{e.message}"
|
||||
hook.prompt_reauthorization!
|
||||
|
||||
BIN
public/dashboard/images/integrations/captain-dark.png
Normal file
BIN
public/dashboard/images/integrations/captain-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/dashboard/images/integrations/captain.png
Normal file
BIN
public/dashboard/images/integrations/captain.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
@@ -16,7 +16,7 @@ RSpec.describe 'Integration Apps API', type: :request do
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
it 'returns all active apps without sensitive information if the user is an agent' do
|
||||
first_app = Integrations::App.all.find(&:active?)
|
||||
first_app = Integrations::App.all.find { |app| app.active?(account) }
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
@@ -41,7 +41,7 @@ RSpec.describe 'Integration Apps API', type: :request do
|
||||
end
|
||||
|
||||
it 'returns all active apps with sensitive information if user is an admin' do
|
||||
first_app = Integrations::App.all.find(&:active?)
|
||||
first_app = Integrations::App.all.find { |app| app.active?(account) }
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
@@ -5,10 +5,6 @@ RSpec.describe Integrations::App do
|
||||
let(:app) { apps.find(id: app_name) }
|
||||
let(:account) { create(:account) }
|
||||
|
||||
before do
|
||||
allow(Current).to receive(:account).and_return(account)
|
||||
end
|
||||
|
||||
describe '#name' do
|
||||
let(:app_name) { 'slack' }
|
||||
|
||||
@@ -28,6 +24,10 @@ RSpec.describe Integrations::App do
|
||||
describe '#action' do
|
||||
let(:app_name) { 'slack' }
|
||||
|
||||
before do
|
||||
allow(Current).to receive(:account).and_return(account)
|
||||
end
|
||||
|
||||
context 'when the app is slack' do
|
||||
it 'returns the action URL with client_id and redirect_uri' do
|
||||
with_modified_env SLACK_CLIENT_ID: 'dummy_client_id' do
|
||||
@@ -46,7 +46,7 @@ RSpec.describe Integrations::App do
|
||||
context 'when the app is slack' do
|
||||
it 'returns true if SLACK_CLIENT_SECRET is present' do
|
||||
with_modified_env SLACK_CLIENT_SECRET: 'random_secret' do
|
||||
expect(app.active?).to be true
|
||||
expect(app.active?(account)).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -55,14 +55,14 @@ RSpec.describe Integrations::App do
|
||||
let(:app_name) { 'linear' }
|
||||
|
||||
it 'returns true if the linear integration feature is disabled' do
|
||||
expect(app.active?).to be false
|
||||
expect(app.active?(account)).to be false
|
||||
end
|
||||
|
||||
it 'returns false if the linear integration feature is enabled' do
|
||||
account.enable_features('linear_integration')
|
||||
account.save!
|
||||
|
||||
expect(app.active?).to be true
|
||||
expect(app.active?(account)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -70,7 +70,7 @@ RSpec.describe Integrations::App do
|
||||
let(:app_name) { 'webhook' }
|
||||
|
||||
it 'returns true' do
|
||||
expect(app.active?).to be true
|
||||
expect(app.active?(account)).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user