Discourse AI - Sentiment

:bookmark: This guide explains how to configure sentiment and emotion classification in Discourse AI and use the reports to explore your community’s posts.

:person_raising_hand: Required user level: Administrator to configure; administrator or moderator to view reports.

Sentiment and emotion classification help you explore patterns in your community’s discussions:

  • Sentiment classifies posts as positive, neutral or negative.
  • Emotion assigns scores across 28 labels, including gratitude, curiosity, anger and sadness.

These are separate capabilities. You can enable either one or both, and choose a different classification backend for each. Reports count classified posts, not unique people or personality types. Use them to find discussions worth reading, not as a substitute for understanding their context.

Before you start

You need Discourse AI enabled through discourse_ai_enabled and a configured classification backend.

Hosted by Discourse? Your site may already have dedicated classification endpoints provisioned. Check its existing configuration before changing it. Contact team@discourse.org if you need help with availability, endpoint configuration or backfill limits.

Self-hosted? Configure a compatible dedicated endpoint, an AI agent with a language model, or a decision model. Enabling the feature alone does not provide an inference service.

Review your provider’s privacy policy, retention rules and costs before enabling automatic processing. Classification sends post text to the selected backend. The screenshots below use invented posts on a test site and a decision model; they do not recommend that provider for private community content.

Configure sentiment and emotion

  1. Open Admin → Plugins → AI → Features.
  2. Open “Sentiment and emotion”.
  3. Under “Sentiment”, select “Enabled” if you want positive, neutral and negative classification. This controls ai_sentiment_enabled.
  4. Choose its “Backend” and configure the corresponding endpoint, agent or model as described below.
  5. Under “Emotion”, select “Enabled” if you want emotion classification. This controls ai_sentiment_emotion_enabled.
  6. Choose its backend and configure it independently.
  7. Set the background-processing limits described in the next section.
  8. Click “Submit” and check that your selections were saved.

This example uses a decision model for both capabilities. “Posts per hour” is deliberately set to 0 to pause automatic scanning while testing. Use a positive value to process new and historical posts automatically.

Use dedicated classification endpoints

Choose “Dedicated classification endpoint” for each capability you want to run with a dedicated model. Follow “Configure dedicated classification endpoints” to the ai_sentiment_model_configs setting.

The reference dedicated models are:

Capability Model
Sentiment cardiffnlp/twitter-roberta-base-sentiment-latest
Emotion SamLowe/roberta-base-go_emotions

Each configuration entry identifies its classification_type (sentiment or emotion), model_name, endpoint and api_key. Supply a separate entry for each dedicated classifier you use. Keep credentials private; do not include them in support posts or screenshots.

See Self-hosting sentiment and emotion for Discourse AI for inference-server deployment. Enable and select each capability separately using the controls above.

The dedicated backend expects a compatible classification API. Copying an Anthropic, OpenAI chat or Azure Language endpoint into this setting does not make it compatible. A successful connection test for a language model elsewhere in Discourse does not test a dedicated sentiment endpoint.

Use an AI agent or decision model

For “LLM agent”, select an appropriate classifier agent with a configured language model. Discourse includes sentiment and emotion classifier agents. A general conversational agent is not a substitute for an agent configured to return the expected label scores.

For “Decision model”, first configure and test a compatible decision model, then select it for each enabled capability. A decision model does not require a separate conversational language model for this classification.

Agent and decision-model requests can incur provider charges or consume credits. Review the selected model’s data-handling rules before assigning it to an automatically processed feature.

Control processing and historical coverage

A scheduled job checks for posts with missing classifications every five minutes. New and historical posts share this scan; results are not necessarily available immediately after posting. There is no minimum number of posts or days before data can appear: one successfully classified post can contribute.

Control Site setting Default Effect
Posts per hour ai_sentiment_backfill_maximum_posts_per_hour 240 Approximate number of posts processed per hour. 0 stops the scheduled scan, including new posts.
Post age limit, in days ai_sentiment_backfill_post_max_age_days 60 Scan posts created within this age window. 0 means today, not unlimited history.

Increase the age limit to include older posts. The scheduled job processes missing results over time; selecting an older report date range does not itself classify that history.

The rate is approximate, not an exact request or spending cap. Enabling both classifiers can make two requests per post. Failed requests or refusals can be retried. Edited posts are checked separately and are not controlled by the scheduled scan’s rate or age limits.

Private messages, deleted posts, deleted topics and non-regular post types are excluded from the scheduled backfill scan. The PM exclusion in the scanner and reports is not a guarantee about all classification paths: the edited-post job does not apply the same PM filter. Take this into account before configuring an external provider.

Explore sentiment by category or tag

  1. Open Admin → Reports → Sentiment analysis, or visit /admin/reports/sentiment_analysis on your site.
  2. Select the start and end dates.
  3. Choose “Category” or “Tag” to group the results.
  4. Use the corresponding category or tag filter to narrow the results. Category filtering also provides an option to include subcategories.
  5. Read each group’s total and its positive, negative and neutral breakdown.
  6. Click a group’s row to inspect its chart and underlying posts.

The example contains nine classified posts: three positive, three negative and three neutral. Neutral posts are included in this report.

In the detail view:

  • Select “All”, “Positive”, “Neutral” or “Negative” to filter the loaded posts.
  • Open a post’s topic to read the surrounding discussion.
  • Use the link icon beside the chart to copy the URL for that report view. Recipients still need staff access.
  • Click “Back” to return to the grouped results.

Report dates refer to when posts were created, not when classification ran. A post can contribute to multiple tag groups if its topic has multiple tags, so adding tag totals can count the same post more than once.

Read the overall sentiment trend

Open the “Overall sentiment” report from Admin → Reports, or visit /admin/reports/overall_sentiment.

The score is:

Number of positive posts − number of negative posts

A positive or negative score must be greater than 0.6 for the post to count in that group. If neither exceeds the threshold, the grouped report treats the post as neutral. The threshold is fixed; there is no site setting to change it.

Overall sentiment is a net post count, not an average probability or happiness percentage. Three positive posts and three negative posts produce an overall score of 0, regardless of how many neutral posts there are. Inspect the grouped breakdown and underlying discussions before interpreting a flat trend as an absence of strong opinions.

Explore emotions

Each emotion has a report under Admin → Reports. For example, visit /admin/reports/emotion_anger for “Anger” or /admin/reports/emotion_curiosity for “Curiosity”. Choose a date range and daily, weekly or monthly grouping to explore the trend.

An emotion report counts posts whose score for that label is greater than 0.1. This threshold is also fixed. More than one emotion can qualify for the same post, so emotion totals are not mutually exclusive.

You can also filter topics by an emotion. Visit /filter?q=order:emotion_anger to find matching topics, ranked by their number of qualifying anger-classified posts rather than by an average emotion score. Use order:emotion_anger-asc to reverse the order. Without an activity-after: filter, this ordering counts qualifying posts from the past year. Normal topic-list access permissions still apply.

Available emotion labels

Admiration, amusement, anger, annoyance, approval, caring, confusion, curiosity, desire, disappointment, disapproval, disgust, embarrassment, excitement, fear, gratitude, grief, joy, love, nervousness, neutral, optimism, pride, realization, relief, remorse, sadness and surprise.

Understand the limitations

  • Classification is model-dependent. Sarcasm, quoted material, specialist language and multilingual discussions can be misinterpreted. Language coverage and accuracy depend on your chosen model. Scores from different backends are not necessarily calibrated alike.
  • Reports only cover stored results. An enabled feature with no working backend, paused processing or insufficient historical coverage can produce empty or incomplete reports.
  • Existing classifications are reused. Editing a classified post does not automatically replace its results. Changing the selected agent or decision model within the same backend does not reclassify existing results either.
  • Changing backend changes the report’s result source. Old results are not converted to the selected backend. Reports can appear empty while missing classifications are processed.
  • Reports exclude private messages. This reporting exclusion is separate from the data-processing limitation described above.

Troubleshoot missing or unexpected results

Problem What to check
No results after enabling Confirm the relevant capability is enabled, its backend is configured, “Posts per hour” is positive, and background jobs are running. Allow time for the five-minute scan and processing queue.
Sentiment works but emotion is empty Enable and configure “Emotion” separately. A sentiment-only dedicated endpoint does not provide emotion classifications.
Older dates have no results Increase the post age limit, allow processing time, and check that the report range includes the posts’ creation dates.
Dedicated endpoint returns an error Check its URL, credentials, model and response format. The adapter expects a flat array of labels and scores, with sentiment labels such as positive, neutral and negative, not arbitrary chat responses or LABEL_0-style labels.
Private-network endpoint cannot be reached Check connectivity and, where required, add only the trusted inference endpoint hostname or IP to allowed_internal_hosts. Do not broadly disable server-side request forgery protections.
Results do not change after an edit or model selection Existing results are reused. Do not assume that editing content or saving another model schedules a full reclassification.
A filter shows fewer posts than its count The count describes the whole group, while sentiment tabs filter loaded posts. Check for more results to load and open the original topics when investigating discrepancies.

Administrators can review agent and decision-model request records in Admin → Plugins → AI → Logs. Check /logs for background classification errors, including dedicated endpoint failures. Treat log contents as sensitive: they can contain post text and model responses. Remove credentials and private content before sharing diagnostics.

Additional resources

Last edited by @keegan 2025-10-23T22:28:36Z

Last checked by @hugh 2024-08-06T05:27:46Z

Check documentPerform check on document:
10 Likes

A post was merged into an existing topic: Problems with Sentiment Backfill

A post was merged into an existing topic: Problems with Sentiment Backfill

The OP has been updated with a new video showcasing the updated features of Sentiment including a ton more emotions and understanding which topics/posts are associated with each emotion

I configured the sentiment model config with a model_name, endpoint, and api_key copied from the LLM settings, where it passes the test, but I get the error below in /logs.

(But maybe I don’t understand correctly, because why doesn’t sentiment use one of the configured LLMs?)

Using claude-3-5-sonnet.


{"type":"error","error":{"type":"invalid_request_error","message":"anthropic-version: header is required"}} (Net::HTTPBadResponse)
/var/www/discourse/plugins/discourse-ai/lib/inference/hugging_face_text_embeddings.rb:71:in `classify'
/var/www/discourse/plugins/discourse-ai/lib/sentiment/post_classification.rb:142:in `request_with'
/var/www/discourse/plugins/discourse-ai/lib/sentiment/post_classification.rb:78:in `block (4 levels) in bulk_classify!'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/concurrent-ruby-1.3.5/lib/concurrent-ruby/concurrent/promises.rb:1593:in `evaluate_to'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/concurrent-ruby-1.3.5/lib/concurrent-ruby/concurrent/promises.rb:1776:in `block in on_resolvable'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/concurrent-ruby-1.3.5/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:359:in `run_task'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/concurrent-ruby-1.3.5/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:350:in `block (3 levels) in create_worker'
<internal:kernel>:187:in `loop'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/concurrent-ruby-1.3.5/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:341:in `block (2 levels) in create_worker'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/concurrent-ruby-1.3.5/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:340:in `catch'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/concurrent-ruby-1.3.5/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:340:in `block in create_worker'
Status: 400

{"type":"error","error":{"type":"invalid_request_error","message":"anthropic-version: header is required"}} (Net::HTTPBadResponse)
/var/www/discourse/plugins/discourse-ai/lib/inference/hugging_face_text_embeddings.rb:71:in `classify'
/var/www/discourse/plugins/discourse-ai/lib/sentiment/post_classification.rb:142:in `request_wit...

The sentiment module doesn’t use general LLMs, but models specifically fine tuned to sentiment classification. If you want to run those models on your own that is documented at Self-Hosting Sentiment and Emotion for DiscourseAI

3 Likes

@Falco I just noticed the sentiment has stopped running as of Jan 2025. My guess that there’s this new setting ai_sentiment_model which as the above link explains if for running your own dedicated sentiment model/image. I noticed that after updating discourse now ai_sentiment_model_configs is all blank (should it be blank?).

When I try to run a backfill rake it gives me an error:

rake ai:sentiment:backfill
rake aborted!
ActiveRecord::StatementInvalid: PG::SyntaxError: ERROR:  syntax error at or near ")" (ActiveRecord::StatementInvalid)
LINE 1: ...e_upload_id", "posts"."outbound_message_id" FROM () as posts...
                                                             ^
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/rack-mini-profiler-3.3.1/lib/patches/db/pg.rb:69:in `exec_params'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/rack-mini-profiler-3.3.1/lib/patches/db/pg.rb:69:in `exec_params'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/postgresql_adapter.rb:894:in `block (2 levels) in exec_no_cache'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/abstract_adapter.rb:1004:in `block in with_raw_connection'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activesupport-7.2.2.1/lib/active_support/concurrency/null_lock.rb:9:in `synchronize'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/abstract_adapter.rb:976:in `with_raw_connection'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/postgresql_adapter.rb:893:in `block in exec_no_cache'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activesupport-7.2.2.1/lib/active_support/notifications/instrumenter.rb:58:in `instrument'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/abstract_adapter.rb:1119:in `log'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/postgresql_adapter.rb:892:in `exec_no_cache'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/postgresql_adapter.rb:872:in `execute_and_clear'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/postgresql/database_statements.rb:66:in `internal_exec_query'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/abstract/database_statements.rb:647:in `select'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/abstract/database_statements.rb:73:in `select_all'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/abstract/query_cache.rb:251:in `select_all'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/querying.rb:70:in `_query_by_sql'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1431:in `block (2 levels) in exec_main_query'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/abstract/connection_pool.rb:415:in `with_connection'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_handling.rb:296:in `with_connection'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1430:in `block in exec_main_query'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/abstract/query_cache.rb:143:in `disable_query_cache'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/query_cache.rb:30:in `uncached'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation/delegation.rb:78:in `block in uncached'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1355:in `_scoping'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:541:in `scoping'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation/delegation.rb:78:in `uncached'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1450:in `skip_query_cache_if_necessary'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1414:in `exec_main_query'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1392:in `block in exec_queries'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/abstract/query_cache.rb:143:in `disable_query_cache'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/query_cache.rb:30:in `uncached'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation/delegation.rb:120:in `public_send'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation/delegation.rb:120:in `block in method_missing'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1355:in `_scoping'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:541:in `scoping'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation/delegation.rb:120:in `method_missing'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1450:in `skip_query_cache_if_necessary'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1386:in `exec_queries'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1167:in `load'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:336:in `records'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation/batches.rb:380:in `block in batch_on_unloaded_relation'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation/batches.rb:378:in `batch_on_unloaded_relation'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation/batches.rb:269:in `in_batches'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation/batches.rb:157:in `find_in_batches'
/var/www/discourse/plugins/discourse-ai/lib/tasks/modules/sentiment/backfill.rake:7:in `block in <main>'
/usr/local/bin/bundle:25:in `load'
/usr/local/bin/bundle:25:in `<main>'

Caused by:
PG::SyntaxError: ERROR:  syntax error at or near ")" (PG::SyntaxError)
LINE 1: ...e_upload_id", "posts"."outbound_message_id" FROM () as posts...
                                                             ^
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/rack-mini-profiler-3.3.1/lib/patches/db/pg.rb:69:in `exec_params'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/rack-mini-profiler-3.3.1/lib/patches/db/pg.rb:69:in `exec_params'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/postgresql_adapter.rb:894:in `block (2 levels) in exec_no_cache'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/abstract_adapter.rb:1004:in `block in with_raw_connection'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activesupport-7.2.2.1/lib/active_support/concurrency/null_lock.rb:9:in `synchronize'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/abstract_adapter.rb:976:in `with_raw_connection'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/postgresql_adapter.rb:893:in `block in exec_no_cache'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activesupport-7.2.2.1/lib/active_support/notifications/instrumenter.rb:58:in `instrument'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/abstract_adapter.rb:1119:in `log'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/postgresql_adapter.rb:892:in `exec_no_cache'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/postgresql_adapter.rb:872:in `execute_and_clear'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/postgresql/database_statements.rb:66:in `internal_exec_query'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/abstract/database_statements.rb:647:in `select'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/abstract/database_statements.rb:73:in `select_all'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/abstract/query_cache.rb:251:in `select_all'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/querying.rb:70:in `_query_by_sql'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1431:in `block (2 levels) in exec_main_query'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/abstract/connection_pool.rb:415:in `with_connection'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_handling.rb:296:in `with_connection'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1430:in `block in exec_main_query'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/abstract/query_cache.rb:143:in `disable_query_cache'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/query_cache.rb:30:in `uncached'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation/delegation.rb:78:in `block in uncached'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1355:in `_scoping'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:541:in `scoping'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation/delegation.rb:78:in `uncached'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1450:in `skip_query_cache_if_necessary'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1414:in `exec_main_query'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1392:in `block in exec_queries'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/abstract/query_cache.rb:143:in `disable_query_cache'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/query_cache.rb:30:in `uncached'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation/delegation.rb:120:in `public_send'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation/delegation.rb:120:in `block in method_missing'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1355:in `_scoping'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:541:in `scoping'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation/delegation.rb:120:in `method_missing'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1450:in `skip_query_cache_if_necessary'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1386:in `exec_queries'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:1167:in `load'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation.rb:336:in `records'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation/batches.rb:380:in `block in batch_on_unloaded_relation'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation/batches.rb:378:in `batch_on_unloaded_relation'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation/batches.rb:269:in `in_batches'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/relation/batches.rb:157:in `find_in_batches'
/var/www/discourse/plugins/discourse-ai/lib/tasks/modules/sentiment/backfill.rake:7:in `block in <main>'
/usr/local/bin/bundle:25:in `load'
/usr/local/bin/bundle:25:in `<main>'
Tasks: TOP => ai:sentiment:backfill
(See full trace by running task with --trace)

My question is, this was working fine until Nov/Dec 2024. How was it working earlier without a dedicated sentiment model/server? Is there a way to get it working again using a generic public service or built in service without having to run a dedicated server for it? I really like discourse is a self contained setup which makes it easy for simple deployment. Having to do custom deployments increases the complexity and maintenance costs which is troublesome for small deployments. Is there way to get back to the pre 2025 setup where sentiment worked out of the box?

The plugin defaulted to a server located in DigitalOcean which I put together to make testing easier.

I have since changed the plugin defaults to a clean slate and people who want AI classified need to run servers following the documentation here on Meta.

Indeed, but we were paying that cost for testing purposes. It’s not sustainable to offer that for every self hoster.

Worth mentioning, that we do offer this classification service on GPU accelerated servers as part of our hosting service.

2 Likes

Discourse AI Sentiment Analysis Issues: Hugging Face Model Format & Azure Endpoint Failure


Hi Discourse Community and Developers,

I’m encountering significant issues when attempting to configure and use the Sentiment Analysis feature within the Discourse AI plugin on my forum. It appears there are two distinct problems preventing it from working correctly.


Issue 1: Hugging Face Model Response Format Mismatch

I’ve configured the cardiffnlp/twitter-roberta-base-sentiment model from Hugging Face for sentiment analysis. While my API key is valid and I can successfully call the API from my Discourse instance using curl, the Discourse AI plugin seems to be parsing the response incorrectly due to a change in the Hugging Face model’s output format.

My curl command (confirming valid API key and new format):

Bashcurl -X POST https://api-inference.huggingface.co/models/cardiffnlp/twitter-roberta-base-sentiment \ -H "Authorization: Bearer hf_xxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d "{\"inputs\": \"I love Discourse!\"}"

Output from curl (showing the new nested array format):

[[{"label":"LABEL_2","score":0.9891520738601685},{"label":"LABEL_1","score":0.009014752693474293},{"label":"LABEL_0","score":0.0018332178005948663}]]

Problem: The twitter-roberta-base-sentiment model used to return a single array of label-score hashes: [{"label": "LABEL_2", "score": 0.98}, ...]. However, it now returns a nested array: [[{"label": "LABEL_2", "score": 0.98}, ...]].

The Discourse AI plugin’s hardcoded parsing logic (specifically, classification["label"][/\d+/].to_i as indicated by the backtrace) does not account for this outer array layer. This leads to a TypeError when it tries to access a Symbol as an Integer.

Error Message (from Job Exception):

no implicit conversion of Symbol into Integer (TypeError)
/var/www/discourse/plugins/discourse-ai/lib/sentiment/post_classification.rb:163:in block in transform_result’
/var/www/discourse/plugins/discourse-ai/lib/sentiment/post_classification.rb:163:in each’
/var/www/discourse/plugin…`

Full Backtrace for Hugging Face Issue:

/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/concurrent-ruby-1.3.5/lib/concurrent-ruby/concurrent/promises.rb:1268:in `raise'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/concurrent-ruby-1.3.5/lib/concurrent-ruby/concurrent/promises.rb:1268:in `wait_until_resolved!'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/concurrent-ruby-1.3.5/lib/concurrent-ruby/concurrent/promises.rb:998:in `value!'
/var/www/discourse/plugins/discourse-ai/lib/sentiment/post_classification.rb:93:in `bulk_classify!'
/var/www/discourse/plugins/discourse-ai/app/jobs/scheduled/sentiment_backfill.rb:27:in `execute'
/var/www/discourse/app/jobs/base.rb:316:in `block (2 levels) in perform'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/rails_multisite-6.1.0/lib/rails_multisite/connection_management/null_instance.rb:49:in `with_connection'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/rails_multisite-6.1.0/lib/rails_multisite/connection_management.rb:21:in `with_connection'
/var/www/discourse/app/jobs/base.rb:303:in `block in perform'
/var/www/discourse/app/jobs/base.rb:299:in `each'
/var/www/discourse/app/jobs/base.rb:299:in `perform'
/var/www/discourse/app/jobs/base.rb:379:in `perform'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mini_scheduler-0.18.0/lib/mini_scheduler/manager.rb:137:in `process_queue'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mini_scheduler-0.18.0/lib/mini_scheduler/manager.rb:77:in `worker_loop'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mini_scheduler-0.18.0/lib/mini_scheduler/manager.rb:63:in `block (2 levels) in ensure_worker_threads'
<internal:kernel>:187:in `loop'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/concurrent-ruby-1.3.5/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:341:in `block (2 levels) in create_worker'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/concurrent-ruby-1.3.5/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:340:in `catch'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/concurrent-ruby-1.3.5/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:340:in `block in create_worker'
no implicit conversion of Symbol into Integer (TypeError)
/var/www/discourse/plugins/discourse-ai/lib/sentiment/post_classification.rb:163:in `block in transform_result'
/var/www/discourse/plugins/discourse-ai/lib/sentiment/post_classification.rb:163:in `each'
/var/www/discourse/plugin...

Issue 2: Microsoft Azure Model Configuration Leads to Hugging Face Error

When I attempted to switch to the Microsoft Text Analytics model within the Discourse AI settings, I encountered a 404 Resource not found error, and surprisingly, the backtrace still points to hugging_face_text_embeddings.rb.

Error Message (from Job Exception):

Job exception: 416 errors
{"error":{"code":"404","message": "Resource not found"}} (Net::HTTPBadResponse)

Relevant Backtrace Snippet (pointing to Hugging Face despite Microsoft model selected):

/var/www/discourse/plugins/discourse-ai/lib/inference/hugging_face_text_embeddings.rb:76:in `do_request!'
/var/www/discourse/plugins/discourse-ai/lib/inference/hugging_face_text_embeddings.rb:51:in `classify_by_sentiment!'
/var/www/discourse/plugins/discourse-ai/lib/sentiment/post_classification.rb:156:in `request_with'

Observation: This indicates that even when I select and configure the Microsoft model’s endpoint and API key, the Discourse AI plugin appears to be hardcoded or incorrectly routing the sentiment analysis requests through Hugging Face-specific logic or endpoints. This prevents the Microsoft model from being used at all.


Configuration Screenshots:

I’ve attached screenshot of my Discourse AI settings to show the configuration:

  • Detailed configuration for AI sentiment models (showing both Hugging Face and Microsoft models) - I tested with just Hugging Face or just Microsoft model configs with the same result

These issues make the sentiment analysis feature effectively unusable. It seems the plugin requires an update to handle the new Hugging Face response format and to correctly route requests when different sentiment providers are configured.

Any assistance or guidance on these issues would be greatly appreciated.

Thank you!

2 Likes

I am curious if sentiment reporting is functioning for others, or if I have misconfigured something. I would like to know what else I need to check or configure to enable sentiment reporting, as I am still experiencing the same issue.

1 Like

We are trying to use this feature with Azure AI Language (from our self-hosted Discourse instance) - as we are already using our Azure subscription to integrate GPT-4.5 with Discourse (for summarization and chat-bot functionality):

…but we are getting no data in the the sentiment dashboard, and can see these errors in the logs:

Discourse AI: Errors during bulk classification: Failed to classify 208 posts (example ids: 2256, 909, 2270, 2260, 2797) : JSON::ParserError : An empty string is not a valid JSON string.

The backtrace shows that Discourse might be trying to use HuggingFace - are these the only models supported at the moment?

Thanks,

N

1 Like

So, is the only way to use this feature to setup your own instances of the models (which need resource heavy GPU instances that would be costly)? This feature looks very useful, but it seems like it would cost me more to set up than my actual Discourse hosting.

Yes, the supported models are the ones listed in the OP.

We will eventually add support for classifying using LLMs for people whose cost isn’t an issue.

Well, the whole feature is build around classifying posts using ML models, so yes, you need somewhere to run those.

And since Discourse can run in the very cheapest VPS out there, running ML models is indeed more expensive. If you wanna have the feature on the cheapest way possible, it is doable to run it on a server with just a handful of CPU cores, as long as you have enough RAM to load the models.

1 Like

Sorry if this question has already been asked before, but I was unable to find a reference to where exactly the threshold score can be configured :sweat_smile:

1 Like

Unfortunately, the threshold score is not something that is configurable by users or admins. It’s a specific value set in the code base.

1 Like