FEATURE: User sentiment on profile summary page (#329)
* FEATURE: User sentiment on profile summary page This introduces a new user stat in a user profile summary page. It will show either neutral/positive/negative according to the dominant sentiment in the user last interactions. The user-stat widget is only rendered for staff. Co-authored-by: Keegan George <kgeorge13@gmail.com>
This commit is contained in:
parent
c8cd38cdda
commit
71c5077228
|
@ -0,0 +1,8 @@
|
||||||
|
<li class="user-summary-stat-outlet sentiment">
|
||||||
|
<span class="value" title={{i18n "discourse_ai.sentiments.summary.title"}}>
|
||||||
|
{{d-icon this.icon}}
|
||||||
|
</span>
|
||||||
|
<span class="label">
|
||||||
|
{{html-safe (i18n "discourse_ai.sentiments.summary.label")}}
|
||||||
|
</span>
|
||||||
|
</li>
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -129,6 +129,11 @@ en:
|
||||||
sentiments:
|
sentiments:
|
||||||
dashboard:
|
dashboard:
|
||||||
title: "Sentiment"
|
title: "Sentiment"
|
||||||
|
summary:
|
||||||
|
label: "sentiment"
|
||||||
|
title: "Experimental AI-powered sentiment analysis of this person's most recent posts."
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
review:
|
review:
|
||||||
types:
|
types:
|
||||||
|
|
|
@ -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_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_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_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_detection_enabled: "Enable the NSFW module."
|
||||||
ai_nsfw_inference_service_api_endpoint: "URL where the API is running for the NSFW module"
|
ai_nsfw_inference_service_api_endpoint: "URL where the API is running for the NSFW module"
|
||||||
|
|
|
@ -66,6 +66,9 @@ discourse_ai:
|
||||||
choices:
|
choices:
|
||||||
- sentiment
|
- sentiment
|
||||||
- emotion
|
- emotion
|
||||||
|
ai_sentiment_show_sentiment_public_profile:
|
||||||
|
default: true
|
||||||
|
client: true
|
||||||
|
|
||||||
ai_nsfw_detection_enabled: false
|
ai_nsfw_detection_enabled: false
|
||||||
ai_nsfw_inference_service_api_endpoint:
|
ai_nsfw_inference_service_api_endpoint:
|
||||||
|
|
|
@ -28,7 +28,8 @@ module DiscourseAi
|
||||||
end
|
end
|
||||||
|
|
||||||
def request(target_to_classify)
|
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|
|
available_models.reduce({}) do |memo, model|
|
||||||
memo[model] = request_with(model, target_content)
|
memo[model] = request_with(model, target_content)
|
||||||
|
|
|
@ -6,9 +6,12 @@ task "ai:sentiment:backfill", [:start_post] => [:environment] do |_, args|
|
||||||
|
|
||||||
Post
|
Post
|
||||||
.joins("INNER JOIN topics ON topics.id = posts.topic_id")
|
.joins("INNER JOIN topics ON topics.id = posts.topic_id")
|
||||||
.joins(
|
.joins(<<~SQL)
|
||||||
"LEFT JOIN classification_results ON classification_results.target_id = posts.id AND classification_results.target_type = 'Post'",
|
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("classification_results.target_id IS NULL")
|
||||||
.where("posts.id >= ?", args[:start_post].to_i || 0)
|
.where("posts.id >= ?", args[:start_post].to_i || 0)
|
||||||
.where("category_id IN (?)", public_categories)
|
.where("category_id IN (?)", public_categories)
|
||||||
|
@ -16,8 +19,12 @@ task "ai:sentiment:backfill", [:start_post] => [:environment] do |_, args|
|
||||||
.order("posts.id ASC")
|
.order("posts.id ASC")
|
||||||
.find_each do |post|
|
.find_each do |post|
|
||||||
print "."
|
print "."
|
||||||
DiscourseAi::PostClassificator.new(
|
begin
|
||||||
DiscourseAi::Sentiment::SentimentClassification.new,
|
DiscourseAi::PostClassificator.new(
|
||||||
).classify!(post)
|
DiscourseAi::Sentiment::SentimentClassification.new,
|
||||||
|
).classify!(post)
|
||||||
|
rescue => e
|
||||||
|
puts "Error: #{e.message}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
65
plugin.rb
65
plugin.rb
|
@ -33,6 +33,10 @@ Rails.autoloaders.main.push_dir(File.join(__dir__, "lib"), namespace: ::Discours
|
||||||
|
|
||||||
require_relative "lib/engine"
|
require_relative "lib/engine"
|
||||||
|
|
||||||
|
register_svg_icon "smile"
|
||||||
|
register_svg_icon "frown"
|
||||||
|
register_svg_icon "meh"
|
||||||
|
|
||||||
after_initialize do
|
after_initialize do
|
||||||
# do not autoload this cause we may have no namespace
|
# do not autoload this cause we may have no namespace
|
||||||
require_relative "discourse_automation/llm_triage"
|
require_relative "discourse_automation/llm_triage"
|
||||||
|
@ -56,6 +60,67 @@ after_initialize do
|
||||||
ModelAccuracy.adjust_model_accuracy(new_status, reviewable)
|
ModelAccuracy.adjust_model_accuracy(new_status, reviewable)
|
||||||
end
|
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?
|
if Rails.env.test?
|
||||||
require_relative "spec/support/openai_completions_inference_stubs"
|
require_relative "spec/support/openai_completions_inference_stubs"
|
||||||
require_relative "spec/support/anthropic_completion_stubs"
|
require_relative "spec/support/anthropic_completion_stubs"
|
||||||
|
|
Loading…
Reference in New Issue