mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-23 11:23:27 +08:00
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>
204 lines
6 KiB
Ruby
Vendored
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
|