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
|
# Set to true if you want to remove stale contact inboxes
|
||||||
# contact_inboxes with no conversation older than 90 days will be removed
|
# contact_inboxes with no conversation older than 90 days will be removed
|
||||||
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
|
# 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
|
private
|
||||||
|
|
||||||
def fetch_apps
|
def fetch_apps
|
||||||
@apps = Integrations::App.all.select(&:active?)
|
@apps = Integrations::App.all.select { |app| app.active?(Current.account) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_app
|
def fetch_app
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ class HookJob < ApplicationJob
|
|||||||
process_slack_integration(hook, event_name, event_data)
|
process_slack_integration(hook, event_name, event_data)
|
||||||
when 'dialogflow'
|
when 'dialogflow'
|
||||||
process_dialogflow_integration(hook, event_name, event_data)
|
process_dialogflow_integration(hook, event_name, event_data)
|
||||||
|
when 'captain'
|
||||||
|
process_captain_integration(hook, event_name, event_data)
|
||||||
when 'google_translate'
|
when 'google_translate'
|
||||||
google_translate_integration(hook, event_name, event_data)
|
google_translate_integration(hook, event_name, event_data)
|
||||||
end
|
end
|
||||||
@@ -35,6 +37,12 @@ class HookJob < ApplicationJob
|
|||||||
Integrations::Dialogflow::ProcessorService.new(event_name: event_name, hook: hook, event_data: event_data).perform
|
Integrations::Dialogflow::ProcessorService.new(event_name: event_name, hook: hook, event_data: event_data).perform
|
||||||
end
|
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)
|
def google_translate_integration(hook, event_name, event_data)
|
||||||
return unless ['message.created'].include?(event_name)
|
return unless ['message.created'].include?(event_name)
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,16 @@ class Inbox < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def active_bot?
|
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
|
end
|
||||||
|
|
||||||
def inbox_type
|
def inbox_type
|
||||||
|
|||||||
@@ -34,12 +34,14 @@ class Integrations::App
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def active?
|
def active?(account)
|
||||||
case params[:id]
|
case params[:id]
|
||||||
when 'slack'
|
when 'slack'
|
||||||
ENV['SLACK_CLIENT_SECRET'].present?
|
ENV['SLACK_CLIENT_SECRET'].present?
|
||||||
when 'linear'
|
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
|
else
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -87,3 +87,5 @@
|
|||||||
premium: true
|
premium: true
|
||||||
- name: linear_integration
|
- name: linear_integration
|
||||||
enabled: false
|
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_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/)
|
# 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:
|
webhooks:
|
||||||
id: webhook
|
id: webhook
|
||||||
logo: webhooks.png
|
logo: webhooks.png
|
||||||
|
|||||||
@@ -227,6 +227,9 @@ en:
|
|||||||
linear:
|
linear:
|
||||||
name: "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."
|
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:
|
public_portal:
|
||||||
search:
|
search:
|
||||||
search_placeholder: Search for article by title or body...
|
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
|
message.content
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_response(session_id, message)
|
def get_response(session_id, message_content)
|
||||||
if hook.settings['credentials'].blank?
|
if hook.settings['credentials'].blank?
|
||||||
Rails.logger.warn "Account: #{hook.try(:account_id)} Hook: #{hook.id} credentials are not present." && return
|
Rails.logger.warn "Account: #{hook.try(:account_id)} Hook: #{hook.id} credentials are not present." && return
|
||||||
end
|
end
|
||||||
|
|
||||||
configure_dialogflow_client_defaults
|
configure_dialogflow_client_defaults
|
||||||
detect_intent(session_id, message)
|
detect_intent(session_id, message_content)
|
||||||
rescue Google::Cloud::PermissionDeniedError => e
|
rescue Google::Cloud::PermissionDeniedError => e
|
||||||
Rails.logger.warn "DialogFlow Error: (account-#{hook.try(:account_id)}, hook-#{hook.id}) #{e.message}"
|
Rails.logger.warn "DialogFlow Error: (account-#{hook.try(:account_id)}, hook-#{hook.id}) #{e.message}"
|
||||||
hook.prompt_reauthorization!
|
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) }
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||||
|
|
||||||
it 'returns all active apps without sensitive information if the user is an agent' do
|
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),
|
get api_v1_account_integrations_apps_url(account),
|
||||||
headers: agent.create_new_auth_token,
|
headers: agent.create_new_auth_token,
|
||||||
as: :json
|
as: :json
|
||||||
@@ -41,7 +41,7 @@ RSpec.describe 'Integration Apps API', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'returns all active apps with sensitive information if user is an admin' do
|
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),
|
get api_v1_account_integrations_apps_url(account),
|
||||||
headers: admin.create_new_auth_token,
|
headers: admin.create_new_auth_token,
|
||||||
as: :json
|
as: :json
|
||||||
|
|||||||
@@ -5,10 +5,6 @@ RSpec.describe Integrations::App do
|
|||||||
let(:app) { apps.find(id: app_name) }
|
let(:app) { apps.find(id: app_name) }
|
||||||
let(:account) { create(:account) }
|
let(:account) { create(:account) }
|
||||||
|
|
||||||
before do
|
|
||||||
allow(Current).to receive(:account).and_return(account)
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#name' do
|
describe '#name' do
|
||||||
let(:app_name) { 'slack' }
|
let(:app_name) { 'slack' }
|
||||||
|
|
||||||
@@ -28,6 +24,10 @@ RSpec.describe Integrations::App do
|
|||||||
describe '#action' do
|
describe '#action' do
|
||||||
let(:app_name) { 'slack' }
|
let(:app_name) { 'slack' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Current).to receive(:account).and_return(account)
|
||||||
|
end
|
||||||
|
|
||||||
context 'when the app is slack' do
|
context 'when the app is slack' do
|
||||||
it 'returns the action URL with client_id and redirect_uri' do
|
it 'returns the action URL with client_id and redirect_uri' do
|
||||||
with_modified_env SLACK_CLIENT_ID: 'dummy_client_id' 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
|
context 'when the app is slack' do
|
||||||
it 'returns true if SLACK_CLIENT_SECRET is present' do
|
it 'returns true if SLACK_CLIENT_SECRET is present' do
|
||||||
with_modified_env SLACK_CLIENT_SECRET: 'random_secret' 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
|
end
|
||||||
end
|
end
|
||||||
@@ -55,14 +55,14 @@ RSpec.describe Integrations::App do
|
|||||||
let(:app_name) { 'linear' }
|
let(:app_name) { 'linear' }
|
||||||
|
|
||||||
it 'returns true if the linear integration feature is disabled' do
|
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
|
end
|
||||||
|
|
||||||
it 'returns false if the linear integration feature is enabled' do
|
it 'returns false if the linear integration feature is enabled' do
|
||||||
account.enable_features('linear_integration')
|
account.enable_features('linear_integration')
|
||||||
account.save!
|
account.save!
|
||||||
|
|
||||||
expect(app.active?).to be true
|
expect(app.active?(account)).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ RSpec.describe Integrations::App do
|
|||||||
let(:app_name) { 'webhook' }
|
let(:app_name) { 'webhook' }
|
||||||
|
|
||||||
it 'returns true' do
|
it 'returns true' do
|
||||||
expect(app.active?).to be true
|
expect(app.active?(account)).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user