feat: Integration with Captain (alpha) (#9834)

- Integration with captain (alpha)

Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
Sojan Jose
2024-07-25 14:24:04 -07:00
committed by GitHub
parent 027a540bbd
commit 0331815cc5
14 changed files with 159 additions and 17 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -87,3 +87,5 @@
premium: true premium: true
- name: linear_integration - name: linear_integration
enabled: false enabled: false
- name: captain_integration
enabled: false

View File

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

View File

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

View 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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

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

View File

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