Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::BaseController
before_action :fetch_hook, only: [:update, :destroy]
before_action :check_authorization
# before_action :check_authorization

def create
@hook = Current.account.hooks.create!(permitted_params)
end

def create_chatgpt
@hook = Current.account.hooks.create!(gpt_params)
end

def update
@hook.update!(permitted_params.slice(:status, :settings))
end
Expand All @@ -28,4 +32,8 @@ def check_authorization
def permitted_params
params.require(:hook).permit(:app_id, :inbox_id, :status, settings: {})
end

def gpt_params
params.require(:hook).permit(:app_id, :status, :chatgpt_api_key, :chatgpt_document_id, settings: {})
end
end
16 changes: 16 additions & 0 deletions app/javascript/dashboard/api/integrations.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class IntegrationsAPI extends ApiClient {
}

connectSlack(code) {

return axios.post(`${this.baseUrl()}/integrations/slack`, {
code: code,
});
Expand All @@ -24,6 +25,21 @@ class IntegrationsAPI extends ApiClient {
deleteHook(hookId) {
return axios.delete(`${this.baseUrl()}/integrations/hooks/${hookId}`);
}

createChatGPT(hookData) {
return axios.post(`${this.baseUrl()}/integrations/hooks/create_chatgpt`, hookData);
}

async uploadFile(params) {
const formData = new FormData();
formData.append('file', params.file);
formData.append('apiKey', params.apiKey);
return await axios.post(`${params.openAIUrl}/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
}
}

export default new IntegrationsAPI();
1 change: 1 addition & 0 deletions app/javascript/dashboard/components/ChatList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ export default {
},
computed: {
...mapGetters({

currentChat: 'getSelectedChat',
currentUser: 'getCurrentUser',
chatLists: 'getAllConversations',
Expand Down
4 changes: 3 additions & 1 deletion app/javascript/dashboard/i18n/locale/en/integrationApps.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@
},
"API": {
"SUCCESS_MESSAGE": "Integration hook added successfully",
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later",
"API_ERROR_MESSAGE": "Api Key and document are required."

}
},
"CONNECT": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
validation="required"
validation-name="Inbox"
/>
<div class="small-11 columns with-right-space">
<p class="wrap-content">Your file must be a JSONL document, where each line is a prompt-completion pair.</p>
<input type="file" id="file" name="file" @change="gptFile($event)" accept=".jsonl" class="input-file" />
<p class="formulate-file-error" v-if="errorMessage">{{ errorMessage }}</p>
</div>

<div class="modal-footer">
<woot-button :disabled="hasErrors" :loading="uiFlags.isCreatingHook">
{{ $t('INTEGRATION_APPS.ADD.FORM.SUBMIT') }}
Expand All @@ -39,8 +45,10 @@
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import hookMixin from './hookMixin';
import Input from "../../../../../widget/components/Form/Input.vue";

export default {
components: {Input},
mixins: [alertMixin, hookMixin],
props: {
integration: {
Expand All @@ -53,6 +61,10 @@ export default {
endPoint: '',
alertMessage: '',
values: {},
fileData: '',
gptDocument: '',
errorMessage: null,
aiData: '',
};
},
computed: {
Expand All @@ -76,6 +88,9 @@ export default {
if (!this.isIntegrationDialogflow) {
return [];
}
if (!this.isIntegrationChatGPT) {
return [];
}
return this.integration.hooks.map(hook => hook.inbox?.id);
},
formItems() {
Expand All @@ -84,11 +99,24 @@ export default {
isIntegrationDialogflow() {
return this.integration.id === 'dialogflow';
},
isIntegrationChatGPT() {
return this.integration.id === 'chatgpt';
},
},
methods: {
onClose() {
this.$emit('close');
},
gptFile(event) {
this.fileData = event.target.files[0];
const fileName = this.fileData.name;
// Validate file extension
if (!fileName.endsWith('.jsonl')) {
this.errorMessage = 'File must have .jsonl extension';
return;
}
this.errorMessage = null;
},
buildHookPayload() {
const hookPayload = {
app_id: this.integration.id,
Expand All @@ -102,26 +130,67 @@ export default {
return acc;
}, {});

this.formItems.forEach(item => {
if (item.validation.includes('JSON')) {
hookPayload.settings[item.name] = JSON.parse(
hookPayload.settings[item.name]
);
}
});
if (this.integration?.id !== 'chatgpt') {
this.formItems.forEach(item => {
if (item.validation.includes('JSON')) {
hookPayload.settings[item.name] = JSON.parse(
hookPayload.settings[item.name]
);
}
});
}

if (this.isHookTypeInbox && this.values.inbox) {
hookPayload.inbox_id = this.values.inbox;
}

return hookPayload;
},
async submitForm() {
try {
await this.$store.dispatch(
'integrations/createHook',
this.buildHookPayload()
);
if(this.errorMessage != null) {
return;
}

if (this.integration.id === 'chatgpt') {
const aiData = this.buildHookPayload();
if (this.fileData && aiData.settings?.openai_api_key) {
try {
this.gptDocument = await this.$store.dispatch(
'integrations/uploadFileChatGPT',
{
file: this.fileData,
apiKey: aiData.settings?.openai_api_key,
openAIUrl: window.chatwootConfig.openAIBaseUrl
}
);
} catch (error) {
this.showAlert(error.message);
throw error;
return;
}
} else {
this.alertMessage = this.$t('INTEGRATION_APPS.ADD.API.API_ERROR_MESSAGE');
this.showAlert(this.alertMessage);
return;
}
}

if (this.integration.id === 'chatgpt') {
const aiInfo = this.buildHookPayload();
const payload = {
app_id: this.integration.id,
chatgpt_api_key: aiInfo.settings?.openai_api_key,
chatgpt_document_id: this.gptDocument?.id,
settings: {
chatgpt_api_key: aiInfo.settings?.openai_api_key,
chatgpt_document_id: this.gptDocument?.id
}
}
await this.$store.dispatch('integrations/createGPTHook', payload);
}else {
await this.$store.dispatch('integrations/createHook', this.buildHookPayload());
}

this.alertMessage = this.$t('INTEGRATION_APPS.ADD.API.SUCCESS_MESSAGE');
this.onClose();
} catch (error) {
Expand All @@ -135,3 +204,15 @@ export default {
},
};
</script>
<style scoped>
.formulate-file-error {
color: #F94B4A;
color: var(--r-400);
display: block;
font-size: 1.4rem;
font-size: var(--font-size-small);
font-weight: 400;
margin-bottom: 1rem;
width: 100%;
}
</style>
21 changes: 20 additions & 1 deletion app/javascript/dashboard/store/modules/integrations.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const state = {
};

const isAValidAppIntegration = integration => {
return ['dialogflow', 'dyte', 'google_translate'].includes(integration.id);
return ['dialogflow', 'dyte', 'google_translate', 'chatgpt'].includes(integration.id);
};
export const getters = {
getIntegrations($state) {
Expand Down Expand Up @@ -95,6 +95,25 @@ export const actions = {
throw new Error(error);
}
},
uploadFileChatGPT: async ({ commit }, params) => {
try {
const response = await IntegrationsAPI.uploadFile(params);
return response.data;
} catch (error) {
throw error;
}
},
createGPTHook: async ({ commit }, params) => {
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isCreatingHook: true });
try {
const response = await IntegrationsAPI.createChatGPT(params);
commit(types.default.ADD_INTEGRATION_HOOKS, response.data);
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isCreatingHook: false });
} catch (error) {
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isCreatingHook: false });
throw new Error(error);
}
},
};

export const mutations = {
Expand Down
27 changes: 15 additions & 12 deletions app/models/integrations/hook.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@
#
# Table name: integrations_hooks
#
# id :bigint not null, primary key
# access_token :string
# hook_type :integer default("account")
# settings :jsonb
# status :integer default("disabled")
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer
# app_id :string
# inbox_id :integer
# reference_id :string
# id :bigint not null, primary key
# access_token :string
# chatgpt_api_key :string
# hook_type :integer default("account")
# settings :jsonb
# status :integer default("disabled")
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer
# app_id :string
# chatgpt_document_id :string
# inbox_id :integer
# reference_id :string
#
class Integrations::Hook < ApplicationRecord
include Reauthorizable
Expand All @@ -22,7 +24,7 @@ class Integrations::Hook < ApplicationRecord
validates :account_id, presence: true
validates :app_id, presence: true
validates :inbox_id, presence: true, if: -> { hook_type == 'inbox' }
validate :validate_settings_json_schema
# validate :validate_settings_json_schema
validates :app_id, uniqueness: { scope: [:account_id], unless: -> { app.present? && app.params[:allow_multiple_hooks].present? } }

enum status: { disabled: 0, enabled: 1 }
Expand Down Expand Up @@ -53,6 +55,7 @@ def ensure_hook_type

def validate_settings_json_schema
return if app.blank? || app.params[:settings_json_schema].blank?
return if app.params[:app_id] == 'chatgpt'

errors.add(:settings, ': Invalid settings data') unless JSONSchemer.schema(app.params[:settings_json_schema]).valid?(settings)
end
Expand Down
27 changes: 27 additions & 0 deletions app/services/message_templates/template/chat_gpt_conversation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class MessageTemplates::Template::ChatGptConversation
pattr_initialize [:conversation!]

def perform
ActiveRecord::Base.transaction do
conversation.messages.create!(chat_gpt_response)
end
rescue StandardError => e
ChatwootExceptionTracker.new(e, account: conversation.account).capture_exception
true
end

private

delegate :contact, :account, to: :conversation
delegate :inbox, to: :message

def chat_gpt_response
content = 'Hi CHATGPT Response'
{
account_id: @conversation.account_id,
inbox_id: @conversation.inbox_id,
message_type: :chat_gpt,
content: content
}
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
json.partial! 'api/v1/models/hook', formats: [:json], resource: @hook
json.chatgpt_api_key @hook.chatgpt_api_key
json.chatgpt_document_id @hook.chatgpt_document_id
1 change: 1 addition & 0 deletions app/views/layouts/vueapp.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
fbApiVersion: '<%= @global_config['FACEBOOK_API_VERSION'] %>',
signupEnabled: '<%= @global_config['ENABLE_ACCOUNT_SIGNUP'] %>',
isEnterprise: '<%= @global_config['IS_ENTERPRISE'] %>',
openAIBaseUrl: '<%= ENV.fetch('CHATWOOT_CHATGPT_URL', nil) %>',
<% if @global_config['VAPID_PUBLIC_KEY'] %>
vapidPublicKey: new Uint8Array(<%= Base64.urlsafe_decode64(@global_config['VAPID_PUBLIC_KEY']).bytes %>),
<% end %>
Expand Down
25 changes: 25 additions & 0 deletions config/integration/apps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,28 @@ google_translate:
},
]
visible_properties: ['project_id']
chatgpt:
id: chatgpt
logo: chatgpt.png
i18n_key: chatgpt
action: /chatgpt
hook_type: account
allow_multiple_hooks: false
settings_json_schema: {
"type": "object",
"properties": {
"openai_api_key": { "type": "string" },
},
"required": ["openai_api_key"],
"additionalProperties": true,
}
settings_form_schema: [
{
"label": "OpenAI Api Key",
"type": "text",
"name": "openai_api_key",
"validation": "required",
"validationName": "OpenAI Api Key",
}
]
visible_properties: ['openai_api_key']
Loading