diff --git a/app/controllers/discourse_ai/admin/ai_features_controller.rb b/app/controllers/discourse_ai/admin/ai_features_controller.rb index 7f087cf1..3390310e 100644 --- a/app/controllers/discourse_ai/admin/ai_features_controller.rb +++ b/app/controllers/discourse_ai/admin/ai_features_controller.rb @@ -6,27 +6,40 @@ module DiscourseAi requires_plugin ::DiscourseAi::PLUGIN_NAME def index - render json: serialize_features(DiscourseAi::Features.features) + render json: serialize_modules(DiscourseAi::Configuration::Module.all) end def edit raise Discourse::InvalidParameters.new(:id) if params[:id].blank? - render json: serialize_module(DiscourseAi::Features.find_module_by_id(params[:id].to_i)) + + a_module = DiscourseAi::Configuration::Module.find_by(id: params[:id].to_i) + + render json: serialize_module(a_module) end private - def serialize_features(modules) + def serialize_modules(modules) modules.map { |a_module| serialize_module(a_module) } end def serialize_module(a_module) return nil if a_module.blank? - a_module.merge( - features: - a_module[:features].map { |f| f.merge(persona: serialize_persona(f[:persona])) }, - ) + { + id: a_module.id, + module_name: a_module.name, + module_enabled: a_module.enabled?, + features: a_module.features.map { |f| serialize_feature(f) }, + } + end + + def serialize_feature(feature) + { + name: feature.name, + persona: serialize_persona(persona_id_obj_hash[feature.persona_id]), + enabled: feature.enabled?, + } end def serialize_persona(persona) @@ -34,6 +47,18 @@ module DiscourseAi serialize_data(persona, AiFeaturesPersonaSerializer, root: false) end + + private + + def persona_id_obj_hash + @persona_id_obj_hash ||= + begin + setting_names = DiscourseAi::Configuration::Feature.all_persona_setting_names + ids = setting_names.map { |sn| SiteSetting.public_send(sn) } + + AiPersona.where(id: ids).index_by(&:id) + end + end end end end diff --git a/app/models/ai_persona.rb b/app/models/ai_persona.rb index eba075bf..4c40b5c8 100644 --- a/app/models/ai_persona.rb +++ b/app/models/ai_persona.rb @@ -316,6 +316,10 @@ class AiPersona < ActiveRecord::Base end end + def features + DiscourseAi::Configuration::Feature.find_features_using(persona_id: id) + end + private def chat_preconditions diff --git a/app/serializers/ai_features_persona_serializer.rb b/app/serializers/ai_features_persona_serializer.rb index 782b5604..34cd4dd8 100644 --- a/app/serializers/ai_features_persona_serializer.rb +++ b/app/serializers/ai_features_persona_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class AiFeaturesPersonaSerializer < ApplicationSerializer - attributes :id, :name, :system_prompt, :allowed_groups, :enabled + attributes :id, :name, :allowed_groups def allowed_groups Group diff --git a/app/serializers/basic_llm_model_serializer.rb b/app/serializers/basic_llm_model_serializer.rb new file mode 100644 index 00000000..e5f13f1f --- /dev/null +++ b/app/serializers/basic_llm_model_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class BasicLlmModelSerializer < ApplicationSerializer + attributes :id, :display_name +end diff --git a/app/serializers/localized_ai_persona_serializer.rb b/app/serializers/localized_ai_persona_serializer.rb index 11b9d815..52ff044b 100644 --- a/app/serializers/localized_ai_persona_serializer.rb +++ b/app/serializers/localized_ai_persona_serializer.rb @@ -36,6 +36,7 @@ class LocalizedAiPersonaSerializer < ApplicationSerializer has_one :user, serializer: BasicUserSerializer, embed: :object has_many :rag_uploads, serializer: UploadSerializer, embed: :object + has_one :default_llm, serializer: BasicLlmModelSerializer, embed: :object def rag_uploads object.uploads @@ -48,4 +49,8 @@ class LocalizedAiPersonaSerializer < ApplicationSerializer def description object.class_instance.description end + + def default_llm + LlmModel.find_by(id: object.default_llm_id) + end end diff --git a/assets/javascripts/initializers/admin-plugin-configuration-nav.js b/assets/javascripts/initializers/admin-plugin-configuration-nav.js index d9ca17a0..d0669dc2 100644 --- a/assets/javascripts/initializers/admin-plugin-configuration-nav.js +++ b/assets/javascripts/initializers/admin-plugin-configuration-nav.js @@ -11,6 +11,11 @@ export default { withPluginApi("1.1.0", (api) => { api.addAdminPluginConfigurationNav("discourse-ai", [ + { + label: "discourse_ai.features.short_title", + route: "adminPlugins.show.discourse-ai-features", + description: "discourse_ai.features.description", + }, { label: "discourse_ai.usage.short_title", route: "adminPlugins.show.discourse-ai-usage", @@ -41,11 +46,6 @@ export default { route: "adminPlugins.show.discourse-ai-spam", description: "discourse_ai.spam.spam_description", }, - { - label: "discourse_ai.features.short_title", - route: "adminPlugins.show.discourse-ai-features", - description: "discourse_ai.features.description", - }, ]); }); }, diff --git a/lib/configuration/feature.rb b/lib/configuration/feature.rb new file mode 100644 index 00000000..a1b4eb2b --- /dev/null +++ b/lib/configuration/feature.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +module DiscourseAi + module Configuration + class Feature + class << self + def feature_cache + @feature_cache ||= ::DiscourseAi::MultisiteHash.new("feature_cache") + end + + def summarization_features + feature_cache[:summarization] ||= [ + new( + "topic_summaries", + "ai_summarization_persona", + DiscourseAi::Configuration::Module::SUMMARIZATION_ID, + DiscourseAi::Configuration::Module::SUMMARIZATION, + ), + new( + "gists", + "ai_summary_gists_persona", + DiscourseAi::Configuration::Module::SUMMARIZATION_ID, + DiscourseAi::Configuration::Module::SUMMARIZATION, + enabled_by_setting: "ai_summary_gists_enabled", + ), + ] + end + + def search_features + feature_cache[:search] ||= [ + new( + "discoveries", + "ai_bot_discover_persona", + DiscourseAi::Configuration::Module::SEARCH_ID, + DiscourseAi::Configuration::Module::SEARCH, + ), + ] + end + + def discord_features + feature_cache[:discord] ||= [ + new( + "search", + "ai_discord_search_persona", + DiscourseAi::Configuration::Module::DISCORD_ID, + DiscourseAi::Configuration::Module::DISCORD, + ), + ] + end + + def inference_features + feature_cache[:inference] ||= [ + new( + "generate_concepts", + "inferred_concepts_generate_persona", + DiscourseAi::Configuration::Module::INFERENCE_ID, + DiscourseAi::Configuration::Module::INFERENCE, + ), + new( + "match_concepts", + "inferred_concepts_match_persona", + DiscourseAi::Configuration::Module::INFERENCE_ID, + DiscourseAi::Configuration::Module::INFERENCE, + ), + new( + "deduplicate_concepts", + "inferred_concepts_deduplicate_persona", + DiscourseAi::Configuration::Module::INFERENCE_ID, + DiscourseAi::Configuration::Module::INFERENCE, + ), + ] + end + + def ai_helper_features + feature_cache[:ai_helper] ||= [ + new( + "proofread", + "ai_helper_proofreader_persona", + DiscourseAi::Configuration::Module::AI_HELPER_ID, + DiscourseAi::Configuration::Module::AI_HELPER, + ), + new( + "title_suggestions", + "ai_helper_title_suggestions_persona", + DiscourseAi::Configuration::Module::AI_HELPER_ID, + DiscourseAi::Configuration::Module::AI_HELPER, + ), + new( + "explain", + "ai_helper_explain_persona", + DiscourseAi::Configuration::Module::AI_HELPER_ID, + DiscourseAi::Configuration::Module::AI_HELPER, + ), + new( + "smart_dates", + "ai_helper_smart_dates_persona", + DiscourseAi::Configuration::Module::AI_HELPER_ID, + DiscourseAi::Configuration::Module::AI_HELPER, + ), + new( + "markdown_tables", + "ai_helper_markdown_tables_persona", + DiscourseAi::Configuration::Module::AI_HELPER_ID, + DiscourseAi::Configuration::Module::AI_HELPER, + ), + new( + "custom_prompt", + "ai_helper_custom_prompt_persona", + DiscourseAi::Configuration::Module::AI_HELPER_ID, + DiscourseAi::Configuration::Module::AI_HELPER, + ), + new( + "image_caption", + "ai_helper_image_caption_persona", + DiscourseAi::Configuration::Module::AI_HELPER_ID, + DiscourseAi::Configuration::Module::AI_HELPER, + ), + ] + end + + def all + [ + summarization_features, + search_features, + discord_features, + inference_features, + ai_helper_features, + ].flatten + end + + def all_persona_setting_names + all.map(&:persona_setting) + end + + def find_features_using(persona_id:) + all.select { |feature| feature.persona_id == persona_id } + end + end + + def initialize(name, persona_setting, module_id, module_name, enabled_by_setting: "") + @name = name + @persona_setting = persona_setting + @module_id = module_id + @module_name = module_name + @enabled_by_setting = enabled_by_setting + end + + attr_reader :name, :persona_setting, :module_id, :module_name + + def enabled? + @enabled_by_setting.blank? || SiteSetting.get(@enabled_by_setting) + end + + def persona_id + SiteSetting.get(persona_setting).to_i + end + end + end +end diff --git a/lib/configuration/module.rb b/lib/configuration/module.rb new file mode 100644 index 00000000..be2b8237 --- /dev/null +++ b/lib/configuration/module.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module DiscourseAi + module Configuration + class Module + SUMMARIZATION = "summarization" + SEARCH = "search" + DISCORD = "discord" + INFERENCE = "inference" + AI_HELPER = "ai_helper" + + NAMES = [SUMMARIZATION, SEARCH, DISCORD, INFERENCE, AI_HELPER] + + SUMMARIZATION_ID = 1 + SEARCH_ID = 2 + DISCORD_ID = 3 + INFERENCE_ID = 4 + AI_HELPER_ID = 5 + + class << self + def all + [ + new( + SUMMARIZATION_ID, + SUMMARIZATION, + "ai_summarization_enabled", + features: DiscourseAi::Configuration::Feature.summarization_features, + ), + new( + SEARCH_ID, + SEARCH, + "ai_bot_enabled", + features: DiscourseAi::Configuration::Feature.search_features, + ), + new( + DISCORD_ID, + DISCORD, + "ai_discord_search_enabled", + features: DiscourseAi::Configuration::Feature.discord_features, + ), + new( + INFERENCE_ID, + INFERENCE, + "inferred_concepts_enabled", + features: DiscourseAi::Configuration::Feature.inference_features, + ), + new( + AI_HELPER_ID, + AI_HELPER, + "ai_helper_enabled", + features: DiscourseAi::Configuration::Feature.ai_helper_features, + ), + ] + end + + def find_by(id:) + all.find { |m| m.id == id } + end + end + + def initialize(id, name, enabled_by_setting, features: []) + @id = id + @name = name + @enabled_by_setting = enabled_by_setting + @features = features + end + + attr_reader :id, :name, :enabled_by_setting, :features + + def enabled? + SiteSetting.get(enabled_by_setting) + end + end + end +end diff --git a/lib/features.rb b/lib/features.rb deleted file mode 100644 index a1070d2d..00000000 --- a/lib/features.rb +++ /dev/null @@ -1,108 +0,0 @@ -# frozen_string_literal: true - -module DiscourseAi - module Features - def self.features_config - [ - { - id: 1, - module_name: "summarization", - module_enabled: "ai_summarization_enabled", - features: [ - { name: "topic_summaries", persona_setting_name: "ai_summarization_persona" }, - { - name: "gists", - persona_setting_name: "ai_summary_gists_persona", - enabled: "ai_summary_gists_enabled", - }, - ], - }, - { - id: 2, - module_name: "search", - module_enabled: "ai_bot_enabled", - features: [{ name: "discoveries", persona_setting_name: "ai_bot_discover_persona" }], - }, - { - id: 3, - module_name: "discord", - module_enabled: "ai_discord_search_enabled", - features: [{ name: "search", persona_setting_name: "ai_discord_search_persona" }], - }, - { - id: 4, - module_name: "inference", - module_enabled: "inferred_concepts_enabled", - features: [ - { - name: "generate_concepts", - persona_setting_name: "inferred_concepts_generate_persona", - }, - { name: "match_concepts", persona_setting_name: "inferred_concepts_match_persona" }, - { - name: "deduplicate_concepts", - persona_setting_name: "inferred_concepts_deduplicate_persona", - }, - ], - }, - { - id: 5, - module_name: "ai_helper", - module_enabled: "ai_helper_enabled", - features: [ - { name: "proofread", persona_setting_name: "ai_helper_proofreader_persona" }, - { - name: "title_suggestions", - persona_setting_name: "ai_helper_title_suggestions_persona", - }, - { name: "explain", persona_setting_name: "ai_helper_explain_persona" }, - { name: "illustrate_post", persona_setting_name: "ai_helper_post_illustrator_persona" }, - { name: "smart_dates", persona_setting_name: "ai_helper_smart_dates_persona" }, - { name: "translate", persona_setting_name: "ai_helper_translator_persona" }, - { name: "markdown_tables", persona_setting_name: "ai_helper_markdown_tables_persona" }, - { name: "custom_prompt", persona_setting_name: "ai_helper_custom_prompt_persona" }, - { name: "image_caption", persona_setting_name: "ai_helper_image_caption_persona" }, - ], - }, - ] - end - - def self.features - features_config.map do |a_module| - { - id: a_module[:id], - module_name: a_module[:module_name], - module_enabled: SiteSetting.get(a_module[:module_enabled]), - features: - a_module[:features].map do |feature| - { - name: feature[:name], - persona: AiPersona.find_by(id: SiteSetting.get(feature[:persona_setting_name])), - enabled: feature[:enabled].present? ? SiteSetting.get(feature[:enabled]) : true, - } - end, - } - end - end - - def self.find_module_by_id(id) - lookup = features.index_by { |f| f[:id] } - lookup[id] - end - - def self.find_module_by_name(module_name) - lookup = features.index_by { |f| f[:module] } - lookup[module_name] - end - - def self.find_module_id_by_name(module_name) - find_module_by_name(module_name)&.dig(:id) - end - - def self.feature_area(module_name) - name_s = module_name.to_s - find_module_by_name(name_s) || raise(ArgumentError, "Feature not found: #{name_s}") - "ai-features/#{name_s}" - end - end -end diff --git a/plugin.rb b/plugin.rb index bda721fa..8ee1e3b9 100644 --- a/plugin.rb +++ b/plugin.rb @@ -72,10 +72,10 @@ end Rails.autoloaders.main.push_dir(File.join(__dir__, "lib"), namespace: ::DiscourseAi) require_relative "lib/engine" -require_relative "lib/features" +require_relative "lib/configuration/module" -DiscourseAi::Features.features_config.each do |feature| - register_site_setting_area("ai-features/#{feature[:module_name]}") +::DiscourseAi::Configuration::Module::NAMES.each do |module_name| + register_site_setting_area("ai-features/#{module_name}") end after_initialize do diff --git a/spec/serializers/ai_features_persona_serializer_spec.rb b/spec/serializers/ai_features_persona_serializer_spec.rb index 677e2743..07330895 100644 --- a/spec/serializers/ai_features_persona_serializer_spec.rb +++ b/spec/serializers/ai_features_persona_serializer_spec.rb @@ -24,9 +24,7 @@ RSpec.describe AiFeaturesPersonaSerializer do serialized = described_class.new(ai_persona, scope: Guardian.new(admin), root: nil) expect(serialized.id).to eq(ai_persona.id) expect(serialized.name).to eq(ai_persona.name) - expect(serialized.system_prompt).to eq(ai_persona.system_prompt) expect(serialized.allowed_groups).to eq(allowed_groups) - expect(serialized.enabled).to eq(ai_persona.enabled) end end end