discourse/plugins/discourse-ai/lib/configuration/module.rb
Natalie Tay 8cf0637583
DEV: Add the ability to register an AI module for data explorer (#38891)
Right now, AI modules and features are hardcoded in
`Configuration::Feature` and `Configuration::Module`. If another plugin
wants to register an AI-powered feature (with its own agent, LLM config,
and a spot on the AI features admin page), there's no way to do it
without modifying discourse-ai directly.

Data explorer needs a query generation agent, and other plugins will be
able to add their own agents too. So we need a proper extensible API
here.

The consumer code ends up looking like this:

```
  DiscoursePluginRegistry.register_external_ai_feature(
    {
      module_name: :data_explorer,
      feature: :query_generation,
      agent_klass: DiscourseDataExplorer::AiQueryGenerator,
      enabled_by_setting: "data_explorer_ai_queries_enabled",
    },
    self,
  )
```

<img width="658" height="258" alt="Screenshot 2026-03-26 at 2 25 19 PM"
src="https://github.com/user-attachments/assets/9239b903-78a5-4e8b-9bf0-e385a030920b"
/>

---------

Co-authored-by: Roman Rizzi <rizziromanalejandro@gmail.com>
2026-04-03 13:59:06 +08:00

204 lines
6 KiB
Ruby
Vendored

# frozen_string_literal: true
module DiscourseAi
module Configuration
class Module
SUMMARIZATION = "summarization"
SEARCH = "search"
DISCORD = "discord"
INFERENCE = "inference"
AI_HELPER = "ai_helper"
TRANSLATION = "translation"
BOT = "bot"
SPAM = "spam"
EMBEDDINGS = "embeddings"
AUTOMATION_REPORTS = "automation_reports"
AUTOMATION_TRIAGE = "automation_triage"
NAMES = [
SUMMARIZATION,
SEARCH,
DISCORD,
INFERENCE,
AI_HELPER,
TRANSLATION,
BOT,
SPAM,
EMBEDDINGS,
AUTOMATION_REPORTS,
AUTOMATION_TRIAGE,
].freeze
SUMMARIZATION_ID = 1
SEARCH_ID = 2
DISCORD_ID = 3
INFERENCE_ID = 4
AI_HELPER_ID = 5
TRANSLATION_ID = 6
BOT_ID = 7
SPAM_ID = 8
EMBEDDINGS_ID = 9
AUTOMATION_REPORTS_ID = 10
AUTOMATION_TRIAGE_ID = 11
class << self
def external_module_id(module_name)
Digest::SHA1.hexdigest(module_name.to_s).to_i(16) % 100_000 + 1000
end
def all
base_modules = [
new(
SUMMARIZATION_ID,
SUMMARIZATION,
enabled_by_setting: "ai_summarization_enabled",
features: DiscourseAi::Configuration::Feature.summarization_features,
),
new(
SEARCH_ID,
SEARCH,
enabled_by_setting: "ai_discover_enabled",
features: DiscourseAi::Configuration::Feature.search_features,
),
new(
DISCORD_ID,
DISCORD,
enabled_by_setting: "ai_discord_search_enabled",
features: DiscourseAi::Configuration::Feature.discord_features,
),
new(
INFERENCE_ID,
INFERENCE,
enabled_by_setting: "inferred_concepts_enabled",
features: DiscourseAi::Configuration::Feature.inference_features,
),
new(
AI_HELPER_ID,
AI_HELPER,
enabled_by_setting: "ai_helper_enabled",
features: DiscourseAi::Configuration::Feature.ai_helper_features,
),
new(
TRANSLATION_ID,
TRANSLATION,
enabled_by_setting: "ai_translation_enabled",
features: DiscourseAi::Configuration::Feature.translation_features,
),
new(
BOT_ID,
BOT,
enabled_by_setting: "ai_bot_enabled",
features: DiscourseAi::Configuration::Feature.bot_features,
),
new(
SPAM_ID,
SPAM,
enabled_by_setting: "ai_spam_detection_enabled",
features: DiscourseAi::Configuration::Feature.spam_features,
),
new(
EMBEDDINGS_ID,
EMBEDDINGS,
enabled_by_setting: "ai_embeddings_enabled",
features: DiscourseAi::Configuration::Feature.embeddings_features,
extra_check: -> { SiteSetting.ai_embeddings_semantic_search_enabled },
),
]
if SiteSetting.discourse_automation_enabled
base_modules << new(
AUTOMATION_REPORTS_ID,
AUTOMATION_REPORTS,
enabled_by_setting: "discourse_automation_enabled",
features: DiscourseAi::Configuration::Feature.ai_automation_report_scripts,
extra_check: -> { has_scripts?(["llm_report"]) },
)
base_modules << new(
AUTOMATION_TRIAGE_ID,
AUTOMATION_TRIAGE,
enabled_by_setting: "discourse_automation_enabled",
features: DiscourseAi::Configuration::Feature.ai_automation_triage_scripts,
extra_check: -> { has_scripts?(%w[llm_triage llm_agent_triage]) },
)
end
# external modules from plugin registry
DiscoursePluginRegistry
.external_ai_features
.group_by { |e| e[:module_name] }
.each do |mod_name, entries|
module_id = external_module_id(mod_name)
features =
entries.map do |e|
setting_name = "#{mod_name}_#{e[:feature]}_agent"
DiscourseAi::Configuration::Feature.new(
e[:feature].to_s,
setting_name,
module_id,
mod_name.to_s,
enabled_by_setting: e[:enabled_by_setting],
)
end
base_modules << new(
module_id,
mod_name,
features:,
extra_check: -> { features.any?(&:enabled?) },
visible: entries.any? { |e| e.fetch(:visible, true) },
)
end
base_modules
end
def has_scripts?(script_names)
DB
.query_single(
"SELECT COUNT(*) FROM discourse_automation_automations WHERE script IN (:names) and enabled",
names: script_names,
)
.first
.to_i > 0
end
def find_by(id:)
all.find { |m| m.id == id }
end
end
def initialize(
id,
name,
enabled_by_setting: nil,
features: [],
extra_check: nil,
visible: true
)
@id = id
@name = name
@enabled_by_setting = enabled_by_setting
@features = features
@extra_check = extra_check
@visible = visible
end
attr_reader :id, :name, :enabled_by_setting, :features
def visible?
@visible
end
def enabled?
return @extra_check.present? ? @extra_check.call : true if enabled_by_setting.blank?
enabled_setting = SiteSetting.get(enabled_by_setting)
if @extra_check
enabled_setting && @extra_check.call
else
enabled_setting
end
end
end
end
end