diff --git a/assets/javascripts/discourse/connectors/user-summary-stat/sentiment.hbs b/assets/javascripts/discourse/connectors/user-summary-stat/sentiment.hbs
new file mode 100644
index 00000000..b5fff1fb
--- /dev/null
+++ b/assets/javascripts/discourse/connectors/user-summary-stat/sentiment.hbs
@@ -0,0 +1,8 @@
+
+
+ {{d-icon this.icon}}
+
+
+ {{html-safe (i18n "discourse_ai.sentiments.summary.label")}}
+
+
\ No newline at end of file
diff --git a/assets/javascripts/discourse/connectors/user-summary-stat/sentiment.js b/assets/javascripts/discourse/connectors/user-summary-stat/sentiment.js
new file mode 100644
index 00000000..0a3c6fa3
--- /dev/null
+++ b/assets/javascripts/discourse/connectors/user-summary-stat/sentiment.js
@@ -0,0 +1,26 @@
+import Component from "@glimmer/component";
+
+export default class Sentiment extends Component {
+ static shouldRender(outletArgs, helper) {
+ return (
+ helper.siteSettings.ai_sentiment_enabled &&
+ helper.siteSettings.ai_sentiment_show_sentiment_public_profile &&
+ outletArgs.model.sentiment &&
+ helper.currentUser &&
+ helper.currentUser.staff
+ );
+ }
+
+ get icon() {
+ switch (this.args.outletArgs.model.sentiment) {
+ case "positive":
+ return "smile";
+ case "negative":
+ return "frown";
+ case "neutral":
+ return "meh";
+ default:
+ return "meh";
+ }
+ }
+}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index b199e608..1a4be8fe 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -129,6 +129,11 @@ en:
sentiments:
dashboard:
title: "Sentiment"
+ summary:
+ label: "sentiment"
+ title: "Experimental AI-powered sentiment analysis of this person's most recent posts."
+
+
review:
types:
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 8216e1e5..cfd20662 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -25,6 +25,7 @@ en:
ai_sentiment_inference_service_api_endpoint: "URL where the API is running for the sentiment module"
ai_sentiment_inference_service_api_key: "API key for the sentiment API"
ai_sentiment_models: "Models to use for inference. Sentiment classifies post on the positive/neutral/negative space. Emotion classifies on the anger/disgust/fear/joy/neutral/sadness/surprise space."
+ ai_sentiment_show_sentiment_public_profile: "Make a user dominant sentiment visible on their public profile."
ai_nsfw_detection_enabled: "Enable the NSFW module."
ai_nsfw_inference_service_api_endpoint: "URL where the API is running for the NSFW module"
diff --git a/config/settings.yml b/config/settings.yml
index 00e33c2e..0257eb6a 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -66,6 +66,9 @@ discourse_ai:
choices:
- sentiment
- emotion
+ ai_sentiment_show_sentiment_public_profile:
+ default: true
+ client: true
ai_nsfw_detection_enabled: false
ai_nsfw_inference_service_api_endpoint:
diff --git a/lib/sentiment/sentiment_classification.rb b/lib/sentiment/sentiment_classification.rb
index 00993d01..6c4c8ef3 100644
--- a/lib/sentiment/sentiment_classification.rb
+++ b/lib/sentiment/sentiment_classification.rb
@@ -28,7 +28,8 @@ module DiscourseAi
end
def request(target_to_classify)
- target_content = content_of(target_to_classify)
+ target_content =
+ DiscourseAi::Tokenizer::BertTokenizer.truncate(content_of(target_to_classify), 500)
available_models.reduce({}) do |memo, model|
memo[model] = request_with(model, target_content)
diff --git a/lib/tasks/modules/sentiment/backfill.rake b/lib/tasks/modules/sentiment/backfill.rake
index 0e14bf52..5942728a 100644
--- a/lib/tasks/modules/sentiment/backfill.rake
+++ b/lib/tasks/modules/sentiment/backfill.rake
@@ -6,9 +6,12 @@ task "ai:sentiment:backfill", [:start_post] => [:environment] do |_, args|
Post
.joins("INNER JOIN topics ON topics.id = posts.topic_id")
- .joins(
- "LEFT JOIN classification_results ON classification_results.target_id = posts.id AND classification_results.target_type = 'Post'",
- )
+ .joins(<<~SQL)
+ LEFT JOIN classification_results ON
+ classification_results.target_id = posts.id AND
+ classification_results.model_used = 'sentiment' AND
+ classification_results.target_type = 'Post'
+ SQL
.where("classification_results.target_id IS NULL")
.where("posts.id >= ?", args[:start_post].to_i || 0)
.where("category_id IN (?)", public_categories)
@@ -16,8 +19,12 @@ task "ai:sentiment:backfill", [:start_post] => [:environment] do |_, args|
.order("posts.id ASC")
.find_each do |post|
print "."
- DiscourseAi::PostClassificator.new(
- DiscourseAi::Sentiment::SentimentClassification.new,
- ).classify!(post)
+ begin
+ DiscourseAi::PostClassificator.new(
+ DiscourseAi::Sentiment::SentimentClassification.new,
+ ).classify!(post)
+ rescue => e
+ puts "Error: #{e.message}"
+ end
end
end
diff --git a/plugin.rb b/plugin.rb
index 2dd299ee..79996d0e 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -33,6 +33,10 @@ Rails.autoloaders.main.push_dir(File.join(__dir__, "lib"), namespace: ::Discours
require_relative "lib/engine"
+register_svg_icon "smile"
+register_svg_icon "frown"
+register_svg_icon "meh"
+
after_initialize do
# do not autoload this cause we may have no namespace
require_relative "discourse_automation/llm_triage"
@@ -56,6 +60,67 @@ after_initialize do
ModelAccuracy.adjust_model_accuracy(new_status, reviewable)
end
+ require_dependency "user_summary"
+ class ::UserSummary
+ def sentiment
+ neutral, positive, negative = DB.query_single(<<~SQL, user_id: @user.id)
+ WITH last_interactions_classified AS (
+ SELECT
+ 1 AS total,
+ CASE WHEN (classification::jsonb->'positive')::integer >= 60 THEN 1 ELSE 0 END AS positive,
+ CASE WHEN (classification::jsonb->'negative')::integer >= 60 THEN 1 ELSE 0 END AS negative
+ FROM
+ classification_results AS cr
+ INNER JOIN
+ posts AS p ON
+ p.id = cr.target_id AND
+ cr.target_type = 'Post'
+ INNER JOIN topics AS t ON
+ t.id = p.topic_id
+ INNER JOIN categories AS c ON
+ c.id = t.category_id
+ WHERE
+ model_used = 'sentiment' AND
+ p.user_id = :user_id
+ ORDER BY
+ p.created_at DESC
+ LIMIT
+ 100
+ )
+ SELECT
+ SUM(total) - SUM(positive) - SUM(negative) AS neutral,
+ SUM(positive) AS positive,
+ SUM(negative) AS negative
+ FROM
+ last_interactions_classified
+ SQL
+
+ neutral = neutral || 0
+ positive = positive || 0
+ negative = negative || 0
+
+ return nil if neutral + positive + negative < 5
+
+ case [neutral / 5, positive, negative].max
+ when positive
+ :positive
+ when negative
+ :negative
+ else
+ :neutral
+ end
+ end
+ end
+
+ require_dependency "user_summary_serializer"
+ class ::UserSummarySerializer
+ attributes :sentiment
+
+ def sentiment
+ object.sentiment.to_s
+ end
+ end
+
if Rails.env.test?
require_relative "spec/support/openai_completions_inference_stubs"
require_relative "spec/support/anthropic_completion_stubs"