From ffd47081bde73bc33ea90abcfd6bb5a4ea27d144 Mon Sep 17 00:00:00 2001 From: Pranav Date: Thu, 25 Apr 2024 22:49:10 -0700 Subject: [PATCH] chore(cleanup): Delete sentiment feature (#9304) - The feature is unused, removing it for now, will bring it back with better models later. --- .env.example | 3 - Gemfile | 3 - Gemfile.lock | 40 +++--- app/models/conversation.rb | 1 - app/models/message.rb | 5 - .../jobs/enterprise/sentiment_analysis_job.rb | 57 --------- enterprise/app/models/enterprise/message.rb | 5 - .../enterprise/sentiment_analysis_helper.rb | 45 ------- .../enterprise/sentiment_analysis_job_spec.rb | 117 ------------------ spec/enterprise/models/conversation_spec.rb | 30 ----- spec/enterprise/models/message_spec.rb | 20 --- 11 files changed, 14 insertions(+), 312 deletions(-) delete mode 100644 enterprise/app/jobs/enterprise/sentiment_analysis_job.rb delete mode 100644 enterprise/app/models/enterprise/message.rb delete mode 100644 enterprise/app/models/enterprise/sentiment_analysis_helper.rb delete mode 100644 spec/enterprise/jobs/enterprise/sentiment_analysis_job_spec.rb delete mode 100644 spec/enterprise/models/message_spec.rb diff --git a/.env.example b/.env.example index 2b4ba9795..55b998f19 100644 --- a/.env.example +++ b/.env.example @@ -249,9 +249,6 @@ AZURE_APP_SECRET= ## OpenAI key # OPENAI_API_KEY= -# Sentiment analysis model file path -SENTIMENT_FILE_PATH= - # Housekeeping/Performance related configurations # Set to true if you want to remove stale contact inboxes # contact_inboxes with no conversation older than 90 days will be removed diff --git a/Gemfile b/Gemfile index 0dac6e95f..65cb4de6b 100644 --- a/Gemfile +++ b/Gemfile @@ -175,9 +175,6 @@ gem 'pgvector' # Convert Website HTML to Markdown gem 'reverse_markdown' -# Sentiment analysis -gem 'informers' - ### Gems required only in specific deployment environments ### ############################################################## diff --git a/Gemfile.lock b/Gemfile.lock index 55021a04b..542910f5d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -151,7 +151,6 @@ GEM base64 (0.1.1) bcrypt (3.1.20) bindex (0.8.1) - blingfire (0.1.8) bootsnap (1.16.0) msgpack (~> 1.2) brakeman (5.4.1) @@ -316,15 +315,15 @@ GEM google-cloud-translate-v3 (0.6.0) gapic-common (>= 0.17.1, < 2.a) google-cloud-errors (~> 1.0) - google-protobuf (3.25.2) - google-protobuf (3.25.2-arm64-darwin) - google-protobuf (3.25.2-x86_64-darwin) - google-protobuf (3.25.2-x86_64-linux) + google-protobuf (3.25.3) + google-protobuf (3.25.3-arm64-darwin) + google-protobuf (3.25.3-x86_64-darwin) + google-protobuf (3.25.3-x86_64-linux) googleapis-common-protos (1.4.0) google-protobuf (~> 3.14) googleapis-common-protos-types (~> 1.2) grpc (~> 1.27) - googleapis-common-protos-types (1.11.0) + googleapis-common-protos-types (1.14.0) google-protobuf (~> 3.18) googleauth (1.5.2) faraday (>= 0.17.3, < 3.a) @@ -335,14 +334,17 @@ GEM signet (>= 0.16, < 2.a) groupdate (6.2.1) activesupport (>= 5.2) - grpc (1.54.3) - google-protobuf (~> 3.21) + grpc (1.62.0) + google-protobuf (~> 3.25) googleapis-common-protos-types (~> 1.0) - grpc (1.54.3-x86_64-darwin) - google-protobuf (~> 3.21) + grpc (1.62.0-arm64-darwin) + google-protobuf (~> 3.25) googleapis-common-protos-types (~> 1.0) - grpc (1.54.3-x86_64-linux) - google-protobuf (~> 3.21) + grpc (1.62.0-x86_64-darwin) + google-protobuf (~> 3.25) + googleapis-common-protos-types (~> 1.0) + grpc (1.62.0-x86_64-linux) + google-protobuf (~> 3.25) googleapis-common-protos-types (~> 1.0) haikunator (1.1.1) hairtrigger (1.0.0) @@ -371,10 +373,6 @@ GEM image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - informers (0.2.0) - blingfire (>= 0.1.7) - numo-narray - onnxruntime (>= 0.5.1) io-console (0.6.0) irb (1.7.2) reline (>= 0.3.6) @@ -500,7 +498,6 @@ GEM racc (~> 1.4) nokogiri (1.16.4-x86_64-linux) racc (~> 1.4) - numo-narray (0.9.2.1) oauth (1.1.0) oauth-tty (~> 1.0, >= 1.0.1) snaky_hash (~> 2.0) @@ -529,14 +526,6 @@ GEM omniauth-rails_csrf_protection (1.0.1) actionpack (>= 4.2) omniauth (~> 2.0) - onnxruntime (0.7.6) - ffi - onnxruntime (0.7.6-arm64-darwin) - ffi - onnxruntime (0.7.6-x86_64-darwin) - ffi - onnxruntime (0.7.6-x86_64-linux) - ffi openssl (3.1.0) orm_adapter (0.5.0) os (1.1.4) @@ -892,7 +881,6 @@ DEPENDENCIES hashie html2text! image_processing - informers jbuilder json_refs json_schemer diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 4d2ce55ba..a99d7f227 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -310,5 +310,4 @@ class Conversation < ApplicationRecord end Conversation.include_mod_with('Concerns::Conversation') -Conversation.include_mod_with('SentimentAnalysisHelper') Conversation.prepend_mod_with('Conversation') diff --git a/app/models/message.rb b/app/models/message.rb index 465c7e988..eb43c20ea 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -269,7 +269,6 @@ class Message < ApplicationRecord reopen_conversation notify_via_mail set_conversation_activity - update_message_sentiments dispatch_create_events send_reply execute_message_template_hooks @@ -406,10 +405,6 @@ class Message < ApplicationRecord conversation.update_columns(last_activity_at: created_at) # rubocop:enable Rails/SkipsModelValidations end - - def update_message_sentiments - # override in the enterprise ::Enterprise::SentimentAnalysisJob.perform_later(self) - end end Message.prepend_mod_with('Message') diff --git a/enterprise/app/jobs/enterprise/sentiment_analysis_job.rb b/enterprise/app/jobs/enterprise/sentiment_analysis_job.rb deleted file mode 100644 index e85a89ed8..000000000 --- a/enterprise/app/jobs/enterprise/sentiment_analysis_job.rb +++ /dev/null @@ -1,57 +0,0 @@ -class Enterprise::SentimentAnalysisJob < ApplicationJob - queue_as :low - - def perform(message) - return if message.account.locale != 'en' || !valid_incoming_message?(message) - - save_message_sentiment(message) - rescue StandardError => e - Rails.logger.error("Sentiment Analysis Error for message #{message.id}: #{e}") - ChatwootExceptionTracker.new(e, account: message.account).capture_exception - end - - def save_message_sentiment(message) - # We are truncating the data here to avoind the OnnxRuntime::Error - # Indices element out of data bounds, idx=512 must be within the inclusive range [-512,511] - # While gathering the maningfull node the Array/tensor index is going out of bound - - text = message.content&.truncate(2900) - return if model.blank? - - sentiment = model.predict(text) - message.sentiment = sentiment.merge(value: label_val(sentiment)) - - message.save! - end - - # Model initializes OnnxRuntime::Model, with given file for inference session and to create the tensor - def model - model_file = save_and_open_sentiment_file - - return if File.empty?(model_file) - - Informers::SentimentAnalysis.new(model_file) - end - - def label_val(sentiment) - sentiment[:label] == 'positive' ? 1 : -1 - end - - def valid_incoming_message?(message) - message.incoming? && message.content.present? && !message.private? - end - - # returns the sentiment file from vendor folder else download it to the path from AWS-S3 - def save_and_open_sentiment_file - model_path = ENV.fetch('SENTIMENT_FILE_PATH', nil) - - sentiment_file = Rails.root.join('vendor/db/sentiment-analysis.onnx') - - return sentiment_file if File.exist?(sentiment_file) - - source_file = Down.download(model_path) # Download file from AWS-S3 - File.rename(source_file, sentiment_file) # Change the file path - - sentiment_file - end -end diff --git a/enterprise/app/models/enterprise/message.rb b/enterprise/app/models/enterprise/message.rb deleted file mode 100644 index 11c7044e5..000000000 --- a/enterprise/app/models/enterprise/message.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Enterprise::Message - def update_message_sentiments - ::Enterprise::SentimentAnalysisJob.perform_later(self) if ENV.fetch('SENTIMENT_FILE_PATH', nil).present? - end -end diff --git a/enterprise/app/models/enterprise/sentiment_analysis_helper.rb b/enterprise/app/models/enterprise/sentiment_analysis_helper.rb deleted file mode 100644 index 6c26d5c07..000000000 --- a/enterprise/app/models/enterprise/sentiment_analysis_helper.rb +++ /dev/null @@ -1,45 +0,0 @@ -module Enterprise::SentimentAnalysisHelper - extend ActiveSupport::Concern - - included do - def opening_sentiments - records = incoming_messages.first(average_message_count) - average_sentiment(records) - end - - def closing_sentiments - return unless resolved? - - records = incoming_messages.last(average_message_count) - average_sentiment(records) - end - - def average_sentiment(records) - { - label: average_sentiment_label(records), - score: average_sentiment_score(records) - } - end - - private - - def average_sentiment_label(records) - value = records.pluck(:sentiment).sum { |a| a['value'].to_i } - value.negative? ? 'negative' : 'positive' - end - - def average_sentiment_score(records) - total = records.pluck(:sentiment).sum { |a| a['score'].to_f } - total / average_message_count - end - - def average_message_count - # incoming_messages.count >= 10 ? 5 : ((incoming_messages.count / 2) - 1) - 5 - end - - def incoming_messages - messages.incoming.where(private: false) - end - end -end diff --git a/spec/enterprise/jobs/enterprise/sentiment_analysis_job_spec.rb b/spec/enterprise/jobs/enterprise/sentiment_analysis_job_spec.rb deleted file mode 100644 index bb2a89395..000000000 --- a/spec/enterprise/jobs/enterprise/sentiment_analysis_job_spec.rb +++ /dev/null @@ -1,117 +0,0 @@ -require 'rails_helper' - -RSpec.describe Enterprise::SentimentAnalysisJob do - context 'when account locale set to english language' do - let(:account) { create(:account, locale: 'en') } - let(:message) { build(:message, content_type: nil, account: account) } - - context 'when update the message sentiments' do - let(:model_path) { Rails.root.join('vendor/db/sentiment-analysis.onnx') } - let(:model) { double } - - before do - allow(Informers::SentimentAnalysis).to receive(:new).with(model_path).and_return(model) - allow(model).to receive(:predict).and_return({ label: 'positive', score: '0.6' }) - end - - it 'with incoming message' do - with_modified_env SENTIMENT_FILE_PATH: 'sentiment-analysis.onnx' do - message.update(message_type: :incoming) - - described_class.perform_now(message) - - expect(message.sentiment).not_to be_empty - end - end - - it 'update sentiment label for positive message' do - with_modified_env SENTIMENT_FILE_PATH: 'sentiment-analysis.onnx' do - message.update(message_type: :incoming, content: 'I like your product') - - described_class.perform_now(message) - - expect(message.sentiment).not_to be_empty - expect(message.sentiment['label']).to eq('positive') - expect(message.sentiment['value']).to eq(1) - end - end - - it 'update sentiment label for negative message' do - with_modified_env SENTIMENT_FILE_PATH: 'sentiment-analysis.onnx' do - message.update(message_type: :incoming, content: 'I did not like your product') - allow(model).to receive(:predict).and_return({ label: 'negative', score: '0.6' }) - - described_class.perform_now(message) - - expect(message.sentiment).not_to be_empty - expect(message.sentiment['label']).to eq('negative') - expect(message.sentiment['value']).to eq(-1) - end - end - end - - context 'with download sentiment files' do - let(:model_path) { nil } - let(:model) { double } - - before do - allow(Informers::SentimentAnalysis).to receive(:new).with(model_path).and_return(model) - allow(model).to receive(:predict).and_return({ label: 'positive', score: '0.6' }) - end - - it 'fetch saved file in the server' do - with_modified_env SENTIMENT_FILE_PATH: 'sentiment-analysis.onnx' do - message.update(message_type: :incoming, content: 'I did not like your product') - - described_class.new(message).save_and_open_sentiment_file - - sentiment_file = Rails.root.join('vendor/db/sentiment-analysis.onnx') - expect(File).to exist(sentiment_file) - end - end - - it 'fetch file from the storage' do - with_modified_env SENTIMENT_FILE_PATH: 'sentiment-analysis.onnx' do - message.update(message_type: :incoming, content: 'I did not like your product') - allow(File).to receive(:exist?).and_return(false) - allow(Down).to receive(:download).and_return('./sentiment-analysis.onnx') - allow(File).to receive(:rename).and_return(100) - - described_class.new(message).save_and_open_sentiment_file - - sentiment_file = Rails.root.join('vendor/db/sentiment-analysis.onnx') - expect(sentiment_file).to be_present - end - end - end - - context 'when does not update the message sentiments' do - it 'with outgoing message' do - message.update(message_type: :outgoing) - - described_class.perform_now(message) - - expect(message.sentiment).to be_empty - end - - it 'with private message' do - message.update(private: true) - - described_class.perform_now(message) - - expect(message.sentiment).to be_empty - end - end - end - - context 'when account locale is not set to english language' do - let(:account) { create(:account, locale: 'es') } - let(:message) { build(:message, content_type: nil, account: account) } - - it 'does not update the message sentiments' do - described_class.perform_now(message) - - expect(message.sentiment).to be_empty - end - end -end diff --git a/spec/enterprise/models/conversation_spec.rb b/spec/enterprise/models/conversation_spec.rb index e9fe811ca..9585274dd 100644 --- a/spec/enterprise/models/conversation_spec.rb +++ b/spec/enterprise/models/conversation_spec.rb @@ -36,36 +36,6 @@ RSpec.describe Conversation, type: :model do # end end - describe 'conversation sentiments' do - include ActiveJob::TestHelper - - let(:conversation) { create(:conversation, additional_attributes: { referer: 'https://www.chatwoot.com/' }) } - - before do - 10.times do - message = create(:message, conversation_id: conversation.id, account_id: conversation.account_id, message_type: 'incoming') - message.update(sentiment: { 'label': 'positive', score: '0.4' }) - end - end - - it 'returns opening sentiments' do - sentiments = conversation.opening_sentiments - expect(sentiments[:label]).to eq('positive') - end - - it 'returns closing sentiments if conversation is not resolved' do - sentiments = conversation.closing_sentiments - expect(sentiments).to be_nil - end - - it 'returns closing sentiments if it is resolved' do - conversation.resolved! - - sentiments = conversation.closing_sentiments - expect(sentiments[:label]).to eq('positive') - end - end - describe 'sla_policy' do let(:account) { create(:account) } let(:conversation) { create(:conversation, account: account) } diff --git a/spec/enterprise/models/message_spec.rb b/spec/enterprise/models/message_spec.rb deleted file mode 100644 index 5fd423163..000000000 --- a/spec/enterprise/models/message_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require Rails.root.join 'spec/models/concerns/liquidable_shared.rb' - -RSpec.describe Message do - context 'with sentiment analysis' do - let(:message) { build(:message, message_type: :incoming, content_type: nil, account: create(:account)) } - - it 'calls SentimentAnalysisJob' do - with_modified_env SENTIMENT_FILE_PATH: 'sentiment-analysis.onnx' do - allow(Enterprise::SentimentAnalysisJob).to receive(:perform_later).and_return(:perform_later).with(message) - - message.save! - - expect(Enterprise::SentimentAnalysisJob).to have_received(:perform_later) - end - end - end -end