mirror of
https://github.com/discourse/discourse.git
synced 2026-08-08 17:53:55 +08:00
Follow up to https://github.com/discourse/discourse/pull/41586 1. **Bulk of the PR diff** -- there's a mix of usages for "description" and "caption", we'll stick to V1 terminology and keep "AI caption" instead of bringing in "description". 2. Move image caption out of AI helper, into its own dedicated "AI Feature" for consolidated settings 3. Update default values for the existing image caption agent - vision_enabled was default off, now on - update description 4. Validations and problem checks - via Site Setting validation - agent / llm - when enabling the feature - or when selecting invalid agent - inform admin when they've enabled the setting but the agent is misconfigured via ProblemCheck
139 lines
5 KiB
Ruby
Vendored
139 lines
5 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
require "enum_site_setting"
|
|
|
|
module DiscourseAi
|
|
module Configuration
|
|
class LlmEnumerator < ::EnumSiteSetting
|
|
def self.global_usage
|
|
rval = Hash.new { |h, k| h[k] = [] }
|
|
|
|
if SiteSetting.ai_bot_enabled
|
|
LlmModel.enabled_chat_bot_ids.each { |llm_id| rval[llm_id] << { type: :ai_bot } }
|
|
end
|
|
|
|
# this is unconditional, so it is clear that we always signal configuration
|
|
AiAgent
|
|
.where.not(default_llm_id: nil)
|
|
.pluck(:default_llm_id, :name, :id)
|
|
.each { |llm_id, name, id| rval[llm_id] << { type: :ai_agent, name: name, id: id } }
|
|
|
|
if SiteSetting.ai_helper_enabled
|
|
{
|
|
"#{I18n.t("js.discourse_ai.features.ai_helper.proofread")}" =>
|
|
SiteSetting.ai_helper_proofreader_agent,
|
|
"#{I18n.t("js.discourse_ai.features.ai_helper.title_suggestions")}" =>
|
|
SiteSetting.ai_helper_title_suggestions_agent,
|
|
"#{I18n.t("js.discourse_ai.features.ai_helper.explain")}" =>
|
|
SiteSetting.ai_helper_explain_agent,
|
|
"#{I18n.t("js.discourse_ai.features.ai_helper.illustrate_post")}" =>
|
|
SiteSetting.ai_helper_post_illustrator_agent,
|
|
"#{I18n.t("js.discourse_ai.features.ai_helper.smart_dates")}" =>
|
|
SiteSetting.ai_helper_smart_dates_agent,
|
|
"#{I18n.t("js.discourse_ai.features.ai_helper.translator")}" =>
|
|
SiteSetting.ai_helper_translator_agent,
|
|
"#{I18n.t("js.discourse_ai.features.ai_helper.markdown_tables")}" =>
|
|
SiteSetting.ai_helper_markdown_tables_agent,
|
|
"#{I18n.t("js.discourse_ai.features.ai_helper.custom_prompt")}" =>
|
|
SiteSetting.ai_helper_custom_prompt_agent,
|
|
}.each do |helper_type, agent_id|
|
|
next if agent_id.blank?
|
|
|
|
agent = AiAgent.find_by(id: agent_id)
|
|
next if agent.blank? || agent.default_llm_id.blank?
|
|
|
|
model_id = agent.default_llm_id || SiteSetting.ai_default_llm_model.to_i
|
|
rval[model_id] << { type: :ai_helper, name: helper_type }
|
|
end
|
|
end
|
|
|
|
if SiteSetting.ai_post_image_captions_enabled
|
|
image_caption_agent = AiAgent.find_by(id: SiteSetting.ai_image_caption_agent)
|
|
|
|
if image_caption_agent.present?
|
|
model_id = image_caption_agent.default_llm_id || SiteSetting.ai_default_llm_model.to_i
|
|
|
|
rval[model_id] << { type: :ai_image_caption }
|
|
end
|
|
end
|
|
|
|
if SiteSetting.ai_summarization_enabled
|
|
summarization_agent = AiAgent.find_by(id: SiteSetting.ai_summarization_agent)
|
|
model_id = summarization_agent.default_llm_id || SiteSetting.ai_default_llm_model.to_i
|
|
|
|
rval[model_id] << { type: :ai_summarization }
|
|
end
|
|
|
|
if SiteSetting.ai_embeddings_semantic_search_enabled
|
|
search_agent = AiAgent.find_by(id: SiteSetting.ai_embeddings_semantic_search_hyde_agent)
|
|
model_id = search_agent.default_llm_id || SiteSetting.ai_default_llm_model.to_i
|
|
|
|
rval[model_id] << { type: :ai_embeddings_semantic_search }
|
|
end
|
|
|
|
if SiteSetting.ai_spam_detection_enabled && AiModerationSetting.spam.present?
|
|
model_id = AiModerationSetting.spam[:llm_model_id]
|
|
rval[model_id] << { type: :ai_spam }
|
|
end
|
|
|
|
if defined?(DiscourseAutomation::Automation)
|
|
DiscourseAutomation::Automation
|
|
.joins(:fields)
|
|
.where(script: %w[llm_report llm_triage])
|
|
.where("discourse_automation_fields.name = ?", "model")
|
|
.pluck(
|
|
"metadata ->> 'value', discourse_automation_automations.name, discourse_automation_automations.id",
|
|
)
|
|
.each do |model_text, name, id|
|
|
next if model_text.blank?
|
|
model_id = model_text.to_i
|
|
rval[model_id] << { type: :automation, name: name, id: id } if model_id.present?
|
|
end
|
|
end
|
|
|
|
rval
|
|
end
|
|
|
|
def self.valid_value?(val)
|
|
true
|
|
end
|
|
|
|
# returns an array of hashes (id: , name:, vision_enabled:, supported_native_tools:)
|
|
def self.values_for_serialization
|
|
return [] unless table_exists?
|
|
|
|
llm_models = LlmModel.all.index_by(&:id)
|
|
|
|
DB
|
|
.query_hash(<<~SQL)
|
|
SELECT id, display_name AS name, vision_enabled
|
|
FROM llm_models
|
|
SQL
|
|
.map do |row|
|
|
row = row.symbolize_keys
|
|
llm_model = llm_models[row[:id]]
|
|
row[:supported_native_tools] = DiscourseAi::Completions::NativeTools.supported_ids_for(
|
|
llm_model,
|
|
)
|
|
row
|
|
end
|
|
end
|
|
|
|
def self.values
|
|
return [] unless table_exists?
|
|
|
|
DB.query_hash(<<~SQL).map(&:symbolize_keys)
|
|
SELECT display_name AS name, id AS value
|
|
FROM llm_models
|
|
SQL
|
|
end
|
|
|
|
def self.table_exists?
|
|
DB.exec("SELECT 1 FROM llm_models LIMIT 0")
|
|
true
|
|
rescue StandardError
|
|
false
|
|
end
|
|
end
|
|
end
|
|
end
|