mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-06-24 16:42:15 +00:00
DEV: Use a PORO to represent modules/features. (#1421)
Additional changes: Adds a "#features" method in AiPersona to find which features are using that persona. Serializes a basic version of a LlmModel in the persona's "#default_llm" serializer attribute.
This commit is contained in:
parent
b54db133cd
commit
f7e0ea888d
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
5
app/serializers/basic_llm_model_serializer.rb
Normal file
5
app/serializers/basic_llm_model_serializer.rb
Normal file
@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class BasicLlmModelSerializer < ApplicationSerializer
|
||||
attributes :id, :display_name
|
||||
end
|
@ -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
|
||||
|
@ -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",
|
||||
},
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
159
lib/configuration/feature.rb
Normal file
159
lib/configuration/feature.rb
Normal file
@ -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
|
75
lib/configuration/module.rb
Normal file
75
lib/configuration/module.rb
Normal file
@ -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
|
108
lib/features.rb
108
lib/features.rb
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user