mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 03:57:52 +00:00
chore(cleanup): Delete sentiment feature (#9304)
- The feature is unused, removing it for now, will bring it back with better models later.
This commit is contained in:
@@ -249,9 +249,6 @@ AZURE_APP_SECRET=
|
|||||||
## OpenAI key
|
## OpenAI key
|
||||||
# OPENAI_API_KEY=
|
# OPENAI_API_KEY=
|
||||||
|
|
||||||
# Sentiment analysis model file path
|
|
||||||
SENTIMENT_FILE_PATH=
|
|
||||||
|
|
||||||
# Housekeeping/Performance related configurations
|
# Housekeeping/Performance related configurations
|
||||||
# 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
|
||||||
|
|||||||
3
Gemfile
3
Gemfile
@@ -175,9 +175,6 @@ gem 'pgvector'
|
|||||||
# Convert Website HTML to Markdown
|
# Convert Website HTML to Markdown
|
||||||
gem 'reverse_markdown'
|
gem 'reverse_markdown'
|
||||||
|
|
||||||
# Sentiment analysis
|
|
||||||
gem 'informers'
|
|
||||||
|
|
||||||
### Gems required only in specific deployment environments ###
|
### Gems required only in specific deployment environments ###
|
||||||
##############################################################
|
##############################################################
|
||||||
|
|
||||||
|
|||||||
40
Gemfile.lock
40
Gemfile.lock
@@ -151,7 +151,6 @@ GEM
|
|||||||
base64 (0.1.1)
|
base64 (0.1.1)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
blingfire (0.1.8)
|
|
||||||
bootsnap (1.16.0)
|
bootsnap (1.16.0)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
brakeman (5.4.1)
|
brakeman (5.4.1)
|
||||||
@@ -316,15 +315,15 @@ GEM
|
|||||||
google-cloud-translate-v3 (0.6.0)
|
google-cloud-translate-v3 (0.6.0)
|
||||||
gapic-common (>= 0.17.1, < 2.a)
|
gapic-common (>= 0.17.1, < 2.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-protobuf (3.25.2)
|
google-protobuf (3.25.3)
|
||||||
google-protobuf (3.25.2-arm64-darwin)
|
google-protobuf (3.25.3-arm64-darwin)
|
||||||
google-protobuf (3.25.2-x86_64-darwin)
|
google-protobuf (3.25.3-x86_64-darwin)
|
||||||
google-protobuf (3.25.2-x86_64-linux)
|
google-protobuf (3.25.3-x86_64-linux)
|
||||||
googleapis-common-protos (1.4.0)
|
googleapis-common-protos (1.4.0)
|
||||||
google-protobuf (~> 3.14)
|
google-protobuf (~> 3.14)
|
||||||
googleapis-common-protos-types (~> 1.2)
|
googleapis-common-protos-types (~> 1.2)
|
||||||
grpc (~> 1.27)
|
grpc (~> 1.27)
|
||||||
googleapis-common-protos-types (1.11.0)
|
googleapis-common-protos-types (1.14.0)
|
||||||
google-protobuf (~> 3.18)
|
google-protobuf (~> 3.18)
|
||||||
googleauth (1.5.2)
|
googleauth (1.5.2)
|
||||||
faraday (>= 0.17.3, < 3.a)
|
faraday (>= 0.17.3, < 3.a)
|
||||||
@@ -335,14 +334,17 @@ GEM
|
|||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
groupdate (6.2.1)
|
groupdate (6.2.1)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
grpc (1.54.3)
|
grpc (1.62.0)
|
||||||
google-protobuf (~> 3.21)
|
google-protobuf (~> 3.25)
|
||||||
googleapis-common-protos-types (~> 1.0)
|
googleapis-common-protos-types (~> 1.0)
|
||||||
grpc (1.54.3-x86_64-darwin)
|
grpc (1.62.0-arm64-darwin)
|
||||||
google-protobuf (~> 3.21)
|
google-protobuf (~> 3.25)
|
||||||
googleapis-common-protos-types (~> 1.0)
|
googleapis-common-protos-types (~> 1.0)
|
||||||
grpc (1.54.3-x86_64-linux)
|
grpc (1.62.0-x86_64-darwin)
|
||||||
google-protobuf (~> 3.21)
|
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)
|
googleapis-common-protos-types (~> 1.0)
|
||||||
haikunator (1.1.1)
|
haikunator (1.1.1)
|
||||||
hairtrigger (1.0.0)
|
hairtrigger (1.0.0)
|
||||||
@@ -371,10 +373,6 @@ GEM
|
|||||||
image_processing (1.12.2)
|
image_processing (1.12.2)
|
||||||
mini_magick (>= 4.9.5, < 5)
|
mini_magick (>= 4.9.5, < 5)
|
||||||
ruby-vips (>= 2.0.17, < 3)
|
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)
|
io-console (0.6.0)
|
||||||
irb (1.7.2)
|
irb (1.7.2)
|
||||||
reline (>= 0.3.6)
|
reline (>= 0.3.6)
|
||||||
@@ -500,7 +498,6 @@ GEM
|
|||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.16.4-x86_64-linux)
|
nokogiri (1.16.4-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
numo-narray (0.9.2.1)
|
|
||||||
oauth (1.1.0)
|
oauth (1.1.0)
|
||||||
oauth-tty (~> 1.0, >= 1.0.1)
|
oauth-tty (~> 1.0, >= 1.0.1)
|
||||||
snaky_hash (~> 2.0)
|
snaky_hash (~> 2.0)
|
||||||
@@ -529,14 +526,6 @@ GEM
|
|||||||
omniauth-rails_csrf_protection (1.0.1)
|
omniauth-rails_csrf_protection (1.0.1)
|
||||||
actionpack (>= 4.2)
|
actionpack (>= 4.2)
|
||||||
omniauth (~> 2.0)
|
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)
|
openssl (3.1.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
@@ -892,7 +881,6 @@ DEPENDENCIES
|
|||||||
hashie
|
hashie
|
||||||
html2text!
|
html2text!
|
||||||
image_processing
|
image_processing
|
||||||
informers
|
|
||||||
jbuilder
|
jbuilder
|
||||||
json_refs
|
json_refs
|
||||||
json_schemer
|
json_schemer
|
||||||
|
|||||||
@@ -310,5 +310,4 @@ class Conversation < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
Conversation.include_mod_with('Concerns::Conversation')
|
Conversation.include_mod_with('Concerns::Conversation')
|
||||||
Conversation.include_mod_with('SentimentAnalysisHelper')
|
|
||||||
Conversation.prepend_mod_with('Conversation')
|
Conversation.prepend_mod_with('Conversation')
|
||||||
|
|||||||
@@ -269,7 +269,6 @@ class Message < ApplicationRecord
|
|||||||
reopen_conversation
|
reopen_conversation
|
||||||
notify_via_mail
|
notify_via_mail
|
||||||
set_conversation_activity
|
set_conversation_activity
|
||||||
update_message_sentiments
|
|
||||||
dispatch_create_events
|
dispatch_create_events
|
||||||
send_reply
|
send_reply
|
||||||
execute_message_template_hooks
|
execute_message_template_hooks
|
||||||
@@ -406,10 +405,6 @@ class Message < ApplicationRecord
|
|||||||
conversation.update_columns(last_activity_at: created_at)
|
conversation.update_columns(last_activity_at: created_at)
|
||||||
# rubocop:enable Rails/SkipsModelValidations
|
# rubocop:enable Rails/SkipsModelValidations
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_message_sentiments
|
|
||||||
# override in the enterprise ::Enterprise::SentimentAnalysisJob.perform_later(self)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Message.prepend_mod_with('Message')
|
Message.prepend_mod_with('Message')
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -36,36 +36,6 @@ RSpec.describe Conversation, type: :model do
|
|||||||
# end
|
# end
|
||||||
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
|
describe 'sla_policy' do
|
||||||
let(:account) { create(:account) }
|
let(:account) { create(:account) }
|
||||||
let(:conversation) { create(:conversation, account: account) }
|
let(:conversation) { create(:conversation, account: account) }
|
||||||
|
|||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user