discourse/plugins/discourse-ai/plugin.rb
Kris e470f3df9f
FEATURE: create AI tagging automation (#34587)
Creates a "Tag posts with AI" script for the Automation plugin. 

This allows new/edited posts to automatically get tagged by an LLM. I've
included two modes, one that uses the tag tool to find existing tags and
another that takes a manual list of tags to choose from.

I've tested this using Open AI with the following prompt: 

```markdown
 You are an expert content classifier and tagging assistant for this forum.

  Your task is to analyze posts and suggest appropriate tags based on content, images, and the
  available tags list provided.

  Guidelines:
  - Only suggest tags from the provided available tags list
  - Be conservative - only tag what you're confident about
  - Consider both content topic and post intent

  You must always respond with valid JSON in this exact format:
  {"tags": ["tag1", "tag2"], "confidence": 85}

  - tags: array of tag names from the available list
  - confidence: integer from 0 to 100 representing your confidence level

  If no tags are appropriate, use: {"tags": [], "confidence": 0}
 ```
 
 JSON response format: 
 
 ```json
 {
  "tags": "[string]",
  "confidence": "integer"
}
```

In action 

<img width="2726" height="640" alt="image"
src="https://github.com/user-attachments/assets/ce0fd21f-5df5-411c-82f0-d4ac05305fed"
/>

Included some settings to include restricted tags, max tags to apply,
max posts for context, and max tokens per post
2025-08-28 13:13:27 -04:00

156 lines
5.2 KiB
Ruby

# frozen_string_literal: true
# name: discourse-ai
# about: Enables integration between AI modules and features in Discourse
# meta_topic_id: 259214
# version: 0.0.1
# authors: Discourse
# url: https://github.com/discourse/discourse/tree/main/plugins/discourse-ai
require "tokenizers"
require "tiktoken_ruby"
require "discourse_ai/tokenizers"
require "ed25519"
enabled_site_setting :discourse_ai_enabled
register_asset "stylesheets/common/streaming.scss"
register_asset "stylesheets/common/ai-blinking-animation.scss"
register_asset "stylesheets/common/ai-user-settings.scss"
register_asset "stylesheets/common/ai-features.scss"
register_asset "stylesheets/modules/translation/common/admin-translations.scss"
register_asset "stylesheets/modules/ai-helper/common/ai-helper.scss"
register_asset "stylesheets/modules/ai-helper/desktop/ai-helper-fk-modals.scss", :desktop
register_asset "stylesheets/modules/ai-helper/mobile/ai-helper.scss", :mobile
register_asset "stylesheets/modules/summarization/common/ai-summary.scss"
register_asset "stylesheets/modules/summarization/desktop/ai-summary.scss", :desktop
register_asset "stylesheets/modules/summarization/common/ai-gists.scss"
register_asset "stylesheets/modules/ai-bot/common/bot-replies.scss"
register_asset "stylesheets/modules/ai-bot/common/ai-persona.scss"
register_asset "stylesheets/modules/ai-bot/common/ai-discobot-discoveries.scss"
register_asset "stylesheets/modules/ai-bot/mobile/ai-persona.scss", :mobile
register_asset "stylesheets/modules/ai-bot-conversations/common.scss"
register_asset "stylesheets/modules/embeddings/common/semantic-related-topics.scss"
register_asset "stylesheets/modules/embeddings/common/semantic-search.scss"
register_asset "stylesheets/modules/sentiment/common/dashboard.scss"
register_asset "stylesheets/modules/llms/common/ai-llms-editor.scss"
register_asset "stylesheets/modules/embeddings/common/ai-embedding-editor.scss"
register_asset "stylesheets/modules/llms/common/usage.scss"
register_asset "stylesheets/modules/llms/common/spam.scss"
register_asset "stylesheets/modules/llms/common/ai-llm-quotas.scss"
register_asset "stylesheets/modules/ai-bot/common/ai-tools.scss"
register_asset "stylesheets/modules/ai-bot/common/ai-artifact.scss"
module ::DiscourseAi
PLUGIN_NAME = "discourse-ai"
def self.public_asset_path(name)
File.expand_path(File.join(__dir__, "public", name))
end
end
Rails.autoloaders.main.push_dir(File.join(__dir__, "lib"), namespace: ::DiscourseAi)
require_relative "lib/engine"
require_relative "lib/configuration/module"
::DiscourseAi::Configuration::Module::NAMES.each do |module_name|
register_site_setting_area("ai-features/#{module_name}")
end
after_initialize do
if defined?(Rack::MiniProfiler)
Rack::MiniProfiler.config.skip_paths << "/discourse-ai/ai-bot/artifacts"
end
# do not autoload this cause we may have no namespace
require_relative "discourse_automation/llm_triage"
require_relative "discourse_automation/llm_report"
require_relative "discourse_automation/llm_tool_triage"
require_relative "discourse_automation/llm_persona_triage"
require_relative "discourse_automation/llm_tagger"
add_admin_route("discourse_ai.title", "discourse-ai", { use_new_show_route: true })
register_seedfu_fixtures(Rails.root.join("plugins", "discourse-ai", "db", "fixtures", "personas"))
[
DiscourseAi::Embeddings::EntryPoint.new,
DiscourseAi::Sentiment::EntryPoint.new,
DiscourseAi::AiHelper::EntryPoint.new,
DiscourseAi::Summarization::EntryPoint.new,
DiscourseAi::AiBot::EntryPoint.new,
DiscourseAi::AiModeration::EntryPoint.new,
DiscourseAi::Translation::EntryPoint.new,
].each { |a_module| a_module.inject_into(self) }
register_problem_check ProblemCheck::AiLlmStatus
register_reviewable_type ReviewableAiChatMessage
register_reviewable_type ReviewableAiPost
on(:reviewable_transitioned_to) do |new_status, reviewable|
ModelAccuracy.adjust_model_accuracy(new_status, reviewable)
if DiscourseAi::AiModeration::SpamScanner.enabled?
DiscourseAi::AiModeration::SpamMetric.update(new_status, reviewable)
end
end
if Rails.env.test?
require_relative "spec/support/embeddings_generation_stubs"
require_relative "spec/support/stable_diffusion_stubs"
end
reloadable_patch do |plugin|
Guardian.prepend DiscourseAi::GuardianExtensions
Topic.prepend DiscourseAi::TopicExtensions
Post.prepend DiscourseAi::PostExtensions
end
register_modifier(:post_should_secure_uploads?) do |_, _, topic|
if topic.private_message? && SharedAiConversation.exists?(target: topic)
false
else
# revert to default behavior
# even though this can be shortened this is the clearest way to express it
nil
end
end
add_api_key_scope(
:discourse_ai,
{ update_personas: { actions: %w[discourse_ai/admin/ai_personas#update] } },
)
plugin_icons = %w[
chart-column
spell-check
language
images
far-copy
robot
info
bars-staggered
far-circle-question
face-smile
face-meh
face-angry
]
plugin_icons.each { |icon| register_svg_icon(icon) }
add_model_callback(DiscourseAutomation::Automation, :after_save) do
DiscourseAi::Configuration::Feature.feature_cache.flush!
end
end