diff --git a/.circleci/config.yml b/.circleci/config.yml index b8a628677..db7f87d5c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,6 +26,12 @@ jobs: override-ci-command: pnpm i - run: node --version - run: pnpm --version + - run: + name: Add PostgreSQL repository and update + command: | + sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' + wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - + sudo apt-get update -y - run: name: Install System Dependencies @@ -34,7 +40,9 @@ jobs: DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \ libpq-dev \ redis-server \ - postgresql \ + postgresql-common \ + postgresql-16 \ + postgresql-16-pgvector \ build-essential \ git \ curl \ diff --git a/.codeclimate.yml b/.codeclimate.yml index 213f7d172..c68251f32 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -51,6 +51,7 @@ exclude_patterns: - 'app/javascript/dashboard/routes/dashboard/settings/automation/constants.js' - 'app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js' - 'app/javascript/dashboard/routes/dashboard/settings/reports/constants.js' + - 'app/javascript/dashboard/store/captain/storeFactory.js' - 'app/javascript/dashboard/i18n/index.js' - 'app/javascript/widget/i18n/index.js' - 'app/javascript/survey/i18n/index.js' diff --git a/.github/workflows/run_foss_spec.yml b/.github/workflows/run_foss_spec.yml index 6a018b314..0af172849 100644 --- a/.github/workflows/run_foss_spec.yml +++ b/.github/workflows/run_foss_spec.yml @@ -1,9 +1,3 @@ -# # -# # This action will strip the enterprise folder -# # and run the spec. -# # This is set to run against every PR. -# # - name: Run Chatwoot CE spec on: push: @@ -18,7 +12,7 @@ jobs: runs-on: ubuntu-20.04 services: postgres: - image: postgres:15.3 + image: pgvector/pgvector:pg15 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: '' diff --git a/.github/workflows/run_response_bot_spec.yml b/.github/workflows/run_response_bot_spec.yml deleted file mode 100644 index c788a0400..000000000 --- a/.github/workflows/run_response_bot_spec.yml +++ /dev/null @@ -1,89 +0,0 @@ -# # -# # This workflow will run specs related to response bot -# # This can only be activated in installations Where vector extension is available. -# # - -name: Run Response Bot spec -on: - push: - branches: - - develop - - master - pull_request: - workflow_dispatch: - -jobs: - test: - runs-on: ubuntu-20.04 - services: - postgres: - image: ankane/pgvector - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: "" - POSTGRES_DB: postgres - POSTGRES_HOST_AUTH_METHOD: trust - ports: - - 5432:5432 - # needed because the postgres container does not provide a healthcheck - # tmpfs makes DB faster by using RAM - options: >- - --mount type=tmpfs,destination=/var/lib/postgresql/data - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - redis: - image: redis - ports: - - 6379:6379 - options: --entrypoint redis-server - - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.ref }} - repository: ${{ github.event.pull_request.head.repo.full_name }} - - - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true # runs 'bundle install' and caches installed gems automatically - - - uses: pnpm/action-setup@v2 - with: - version: 9.3.0 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: pnpm - - - name: pnpm - run: pnpm install - - - name: Create database - run: bundle exec rake db:create - - - name: Seed database - run: bundle exec rake db:schema:load - - - name: Enable ResponseBotService in installation - run: RAILS_ENV=test bundle exec rails runner "Features::ResponseBotService.new.enable_in_installation" - - # Run Response Bot specs - - name: Run backend tests - run: | - bundle exec rspec \ - spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb \ - spec/enterprise/services/enterprise/message_templates/response_bot_service_spec.rb \ - spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 \ - spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb \ - --profile=10 \ - --format documentation - - - name: Upload rails log folder - uses: actions/upload-artifact@v4 - if: always() - with: - name: rails-log-folder - path: log diff --git a/Gemfile b/Gemfile index 439b0f63e..45e194e13 100644 --- a/Gemfile +++ b/Gemfile @@ -175,6 +175,8 @@ gem 'pgvector' # Convert Website HTML to Markdown gem 'reverse_markdown' +gem 'ruby-openai' + ### Gems required only in specific deployment environments ### ############################################################## diff --git a/Gemfile.lock b/Gemfile.lock index 74cdbeea8..45c54bd46 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -231,6 +231,7 @@ GEM erubi (1.13.0) et-orbi (1.2.11) tzinfo + event_stream_parser (1.0.0) execjs (2.8.1) facebook-messenger (2.0.1) httparty (~> 0.13, >= 0.13.7) @@ -684,6 +685,10 @@ GEM rubocop-rspec (2.21.0) rubocop (~> 1.33) rubocop-capybara (~> 2.17) + ruby-openai (7.3.1) + event_stream_parser (>= 0.3.0, < 2.0.0) + faraday (>= 1) + faraday-multipart (>= 1) ruby-progressbar (1.13.0) ruby-vips (2.1.4) ffi (~> 1.12) @@ -941,6 +946,7 @@ DEPENDENCIES rubocop-performance rubocop-rails rubocop-rspec + ruby-openai scout_apm scss_lint seed_dump diff --git a/app/controllers/api/v1/accounts/integrations/captain_controller.rb b/app/controllers/api/v1/accounts/integrations/captain_controller.rb deleted file mode 100644 index 6813d14b5..000000000 --- a/app/controllers/api/v1/accounts/integrations/captain_controller.rb +++ /dev/null @@ -1,78 +0,0 @@ -class Api::V1::Accounts::Integrations::CaptainController < Api::V1::Accounts::BaseController - before_action :hook - - def proxy - request_url = build_request_url(request_path) - response = HTTParty.send(request_method, request_url, body: permitted_params[:body].to_json, headers: headers) - render plain: response.body, status: response.code - end - - def copilot - request_url = build_request_url(build_request_path("/assistants/#{hook.settings['assistant_id']}/copilot")) - params = { - previous_messages: copilot_params[:previous_messages], - conversation_history: conversation_history, - message: copilot_params[:message] - } - response = HTTParty.send(:post, request_url, body: params.to_json, headers: headers) - render plain: response.body, status: response.code - end - - private - - def headers - { - 'X-User-Email' => hook.settings['account_email'], - 'X-User-Token' => hook.settings['access_token'], - 'Content-Type' => 'application/json', - 'Accept' => '*/*' - } - end - - def build_request_path(route) - "api/accounts/#{hook.settings['account_id']}#{route}" - end - - def request_path - request_route = with_leading_hash_on_route(params[:route]) - - return 'api/sessions/profile' if request_route == '/sessions/profile' - - build_request_path(request_route) - end - - def build_request_url(request_path) - base_url = InstallationConfig.find_by(name: 'CAPTAIN_API_URL').value - URI.join(base_url, request_path).to_s - end - - def hook - @hook ||= Current.account.hooks.find_by!(app_id: 'captain') - end - - def request_method - method = permitted_params[:method].downcase - raise 'Invalid or missing HTTP method' unless %w[get post put patch delete options head].include?(method) - - method - end - - def with_leading_hash_on_route(request_route) - return '' if request_route.blank? - - request_route.start_with?('/') ? request_route : "/#{request_route}" - end - - def conversation_history - conversation = Current.account.conversations.find_by!(display_id: copilot_params[:conversation_id]) - conversation.to_llm_text - end - - def copilot_params - params.permit(:previous_messages, :conversation_id, :message) - end - - def permitted_params - params.permit(:method, :route, body: {}) - end -end diff --git a/app/controllers/public/api/v1/portals/articles_controller.rb b/app/controllers/public/api/v1/portals/articles_controller.rb index 08c62bada..f0fcf403d 100644 --- a/app/controllers/public/api/v1/portals/articles_controller.rb +++ b/app/controllers/public/api/v1/portals/articles_controller.rb @@ -7,9 +7,10 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B def index @articles = @portal.articles.published + @articles_count = @articles.count search_articles order_by_sort_param - @articles.page(list_params[:page]) if list_params[:page].present? + @articles = @articles.page(list_params[:page]) if list_params[:page].present? end def show; end @@ -44,7 +45,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B end def list_params - params.permit(:query, :locale, :sort, :status) + params.permit(:query, :locale, :sort, :status, :page) end def permitted_params diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb index b582bc5dd..7416b7861 100644 --- a/app/dispatchers/async_dispatcher.rb +++ b/app/dispatchers/async_dispatcher.rb @@ -22,3 +22,5 @@ class AsyncDispatcher < BaseDispatcher ] end end + +AsyncDispatcher.prepend_mod_with('AsyncDispatcher') diff --git a/app/javascript/dashboard/api/captain/assistant.js b/app/javascript/dashboard/api/captain/assistant.js new file mode 100644 index 000000000..ce636e526 --- /dev/null +++ b/app/javascript/dashboard/api/captain/assistant.js @@ -0,0 +1,19 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class CaptainAssistant extends ApiClient { + constructor() { + super('captain/assistants', { accountScoped: true }); + } + + get({ page = 1, searchKey } = {}) { + return axios.get(this.url, { + params: { + page, + searchKey, + }, + }); + } +} + +export default new CaptainAssistant(); diff --git a/app/javascript/dashboard/api/captain/document.js b/app/javascript/dashboard/api/captain/document.js new file mode 100644 index 000000000..165fa79b6 --- /dev/null +++ b/app/javascript/dashboard/api/captain/document.js @@ -0,0 +1,19 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class CaptainDocument extends ApiClient { + constructor() { + super('captain/documents', { accountScoped: true }); + } + + get({ page = 1, searchKey } = {}) { + return axios.get(this.url, { + params: { + page, + searchKey, + }, + }); + } +} + +export default new CaptainDocument(); diff --git a/app/javascript/dashboard/api/captain/inboxes.js b/app/javascript/dashboard/api/captain/inboxes.js new file mode 100644 index 000000000..e0a1efdfe --- /dev/null +++ b/app/javascript/dashboard/api/captain/inboxes.js @@ -0,0 +1,26 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class CaptainInboxes extends ApiClient { + constructor() { + super('captain/assistants', { accountScoped: true }); + } + + get({ assistantId } = {}) { + return axios.get(`${this.url}/${assistantId}/inboxes`); + } + + create(params = {}) { + const { assistantId, inboxId } = params; + return axios.post(`${this.url}/${assistantId}/inboxes`, { + inbox: { inbox_id: inboxId }, + }); + } + + delete(params = {}) { + const { assistantId, inboxId } = params; + return axios.delete(`${this.url}/${assistantId}/inboxes/${inboxId}`); + } +} + +export default new CaptainInboxes(); diff --git a/app/javascript/dashboard/api/captain/response.js b/app/javascript/dashboard/api/captain/response.js new file mode 100644 index 000000000..0cd31fa3c --- /dev/null +++ b/app/javascript/dashboard/api/captain/response.js @@ -0,0 +1,21 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class CaptainResponses extends ApiClient { + constructor() { + super('captain/assistant_responses', { accountScoped: true }); + } + + get({ page = 1, searchKey, assistantId, documentId } = {}) { + return axios.get(this.url, { + params: { + page, + searchKey, + assistant_id: assistantId, + document_id: documentId, + }, + }); + } +} + +export default new CaptainResponses(); diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index ebc6176e7..8b9eacf3f 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -133,6 +133,10 @@ class ConversationApi extends ApiClient { getAllAttachments(conversationId) { return axios.get(`${this.url}/${conversationId}/attachments`); } + + requestCopilot(conversationId, body) { + return axios.post(`${this.url}/${conversationId}/copilot`, body); + } } export default new ConversationApi(); diff --git a/app/javascript/dashboard/api/integrations.js b/app/javascript/dashboard/api/integrations.js index b78a2ea7e..2b816e603 100644 --- a/app/javascript/dashboard/api/integrations.js +++ b/app/javascript/dashboard/api/integrations.js @@ -32,14 +32,6 @@ class IntegrationsAPI extends ApiClient { deleteHook(hookId) { return axios.delete(`${this.baseUrl()}/integrations/hooks/${hookId}`); } - - requestCaptain(body) { - return axios.post(`${this.baseUrl()}/integrations/captain/proxy`, body); - } - - requestCaptainCopilot(body) { - return axios.post(`${this.baseUrl()}/integrations/captain/copilot`, body); - } } export default new IntegrationsAPI(); diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/ContactNotes.vue b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/ContactNotes.vue index dfe3f437c..d5586cb12 100644 --- a/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/ContactNotes.vue +++ b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/ContactNotes.vue @@ -29,7 +29,7 @@ const getWrittenBy = note => { const isCurrentUser = note?.user?.id === currentUser.value.id; return isCurrentUser ? t('CONTACTS_LAYOUT.SIDEBAR.NOTES.YOU') - : note.user.name; + : note?.user?.name || 'Bot'; }; const onAdd = content => { diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue index 504be1547..17367c930 100644 --- a/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue +++ b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue @@ -33,8 +33,8 @@ const handleDelete = () => {
diff --git a/app/javascript/dashboard/components-next/captain/PageLayout.vue b/app/javascript/dashboard/components-next/captain/PageLayout.vue new file mode 100644 index 000000000..36229d428 --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/PageLayout.vue @@ -0,0 +1,84 @@ + + + diff --git a/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.story.vue b/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.story.vue new file mode 100644 index 000000000..02544439f --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.story.vue @@ -0,0 +1,85 @@ + + + diff --git a/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.vue b/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.vue new file mode 100644 index 000000000..8c2b9b749 --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/assistant/AssistantCard.vue @@ -0,0 +1,101 @@ + + + diff --git a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.story.vue b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.story.vue new file mode 100644 index 000000000..ec004e6c0 --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.story.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue new file mode 100644 index 000000000..e541ee5cf --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue @@ -0,0 +1,108 @@ + + + diff --git a/app/javascript/dashboard/components-next/captain/assistant/InboxCard.story.vue b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.story.vue new file mode 100644 index 000000000..34439a135 --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.story.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue new file mode 100644 index 000000000..4e94fffc3 --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue @@ -0,0 +1,100 @@ + + + diff --git a/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.story.vue b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.story.vue new file mode 100644 index 000000000..2522c113a --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.story.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue new file mode 100644 index 000000000..23ef50a07 --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue @@ -0,0 +1,118 @@ + + + diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue new file mode 100644 index 000000000..3301e085d --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue @@ -0,0 +1,56 @@ + + + diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue new file mode 100644 index 000000000..3c8afec07 --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue @@ -0,0 +1,174 @@ + + + diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue new file mode 100644 index 000000000..ac67063ec --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue @@ -0,0 +1,87 @@ + + +