Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
822594f3e3 | ||
|
|
7dee59c97f | ||
|
|
fbbe01e576 | ||
|
|
f420261196 | ||
|
|
eb93e1736b | ||
|
|
cc34882074 | ||
|
|
4b03cdcc96 | ||
|
|
d9b53b4a3d | ||
|
|
a927fd2270 | ||
|
|
f077d58abf | ||
|
|
08d2f3ddf9 | ||
|
|
273a1fa618 | ||
|
|
7af3ce820d | ||
|
|
5841543bd9 | ||
|
|
5ae80645bb | ||
|
|
b675c4c39b | ||
|
|
0fadf1da1a | ||
|
|
c26d604072 | ||
|
|
7ae61ce877 | ||
|
|
f593ab6e1e | ||
|
|
b58f198ee8 | ||
|
|
88811f97bf | ||
|
|
2377b286dd | ||
|
|
8da4318021 | ||
|
|
b6a88ff03e | ||
|
|
f366ded03b | ||
|
|
87c42145d4 | ||
|
|
c49f1df2ba | ||
|
|
b46940fc66 | ||
|
|
249aab100c | ||
|
|
5a29074799 | ||
|
|
29d415755a | ||
|
|
5a590fddc1 | ||
|
|
cad39c1aa4 | ||
|
|
2592be65ff |
@@ -12,7 +12,6 @@ class AiPersona < ActiveRecord::Base
|
||||
validates :system_prompt, presence: true, length: { maximum: 10_000_000 }
|
||||
validate :system_persona_unchangeable, on: :update, if: :system
|
||||
validate :chat_preconditions
|
||||
validate :allowed_seeded_model, if: :default_llm_id
|
||||
validate :well_formated_examples
|
||||
validates :max_context_posts, numericality: { greater_than: 0 }, allow_nil: true
|
||||
# leaves some room for growth but sets a maximum to avoid memory issues
|
||||
@@ -365,17 +364,6 @@ class AiPersona < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
def allowed_seeded_model
|
||||
return if default_llm_id.blank?
|
||||
|
||||
return if default_llm.nil?
|
||||
return if !default_llm.seeded?
|
||||
|
||||
return if SiteSetting.ai_bot_allowed_seeded_models_map.include?(default_llm.id.to_s)
|
||||
|
||||
errors.add(:default_llm, I18n.t("discourse_ai.llm.configuration.invalid_seeded_model"))
|
||||
end
|
||||
|
||||
def well_formated_examples
|
||||
return if examples.blank?
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class LlmModel < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def identifier
|
||||
"custom:#{id}"
|
||||
"#{id}"
|
||||
end
|
||||
|
||||
def toggle_companion_user
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import SiteSettingComponent from "admin/components/site-setting";
|
||||
import SiteSetting from "admin/models/site-setting";
|
||||
|
||||
export default class AiDefaultLlmSelector extends Component {
|
||||
@tracked defaultLlmSetting = null;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.#loadDefaultLlmSetting();
|
||||
}
|
||||
|
||||
async #loadDefaultLlmSetting() {
|
||||
const { site_settings } = await ajax("/admin/config/site_settings.json", {
|
||||
data: {
|
||||
plugin: "discourse-ai",
|
||||
category: "discourse_ai",
|
||||
},
|
||||
});
|
||||
|
||||
const defaultLlmSetting = site_settings.find(
|
||||
(setting) => setting.setting === "ai_default_llm_model"
|
||||
);
|
||||
|
||||
this.defaultLlmSetting = SiteSetting.create(defaultLlmSetting);
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="ai-configure-default-llm">
|
||||
<div class="ai-configure-default-llm__header">
|
||||
<h3>{{i18n "discourse_ai.default_llm.title"}}</h3>
|
||||
<p>{{i18n "discourse_ai.default_llm.description"}}</p>
|
||||
</div>
|
||||
|
||||
{{#if this.defaultLlmSetting}}
|
||||
<SiteSettingComponent
|
||||
@setting={{this.defaultLlmSetting}}
|
||||
class="ai-configure-default-llm__setting"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import DPageSubheader from "discourse/components/d-page-subheader";
|
||||
import DSelect from "discourse/components/d-select";
|
||||
import FilterInput from "discourse/components/filter-input";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import AiDefaultLlmSelector from "./ai-default-llm-selector";
|
||||
import AiFeaturesList from "./ai-features-list";
|
||||
|
||||
const ALL = "all";
|
||||
@@ -202,6 +203,8 @@ export default class AiFeatures extends Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AiDefaultLlmSelector />
|
||||
|
||||
{{#if this.filteredFeatures.length}}
|
||||
<AiFeaturesList @modules={{this.filteredFeatures}} />
|
||||
{{else}}
|
||||
|
||||
@@ -9,6 +9,7 @@ import I18n, { i18n } from "discourse-i18n";
|
||||
import AdminSectionLandingItem from "admin/components/admin-section-landing-item";
|
||||
import AdminSectionLandingWrapper from "admin/components/admin-section-landing-wrapper";
|
||||
import DTooltip from "float-kit/components/d-tooltip";
|
||||
import AiDefaultLlmSelector from "./ai-default-llm-selector";
|
||||
import AiLlmEditor from "./ai-llm-editor";
|
||||
|
||||
function isPreseeded(llm) {
|
||||
@@ -137,6 +138,9 @@ export default class AiLlmsListEditor extends Component {
|
||||
}}
|
||||
@learnMoreUrl="https://meta.discourse.org/t/discourse-ai-large-language-model-llm-settings-page/319903"
|
||||
/>
|
||||
|
||||
<AiDefaultLlmSelector />
|
||||
|
||||
{{#if this.hasLlmElements}}
|
||||
<section class="ai-llms-list-editor__configured">
|
||||
<DPageSubheader
|
||||
|
||||
@@ -90,7 +90,7 @@ export default class AiSpam extends Component {
|
||||
this.isEnabled = model.is_enabled;
|
||||
|
||||
if (model.llm_id) {
|
||||
this.selectedLLM = "custom:" + model.llm_id;
|
||||
this.selectedLLM = model.llm_id;
|
||||
} else {
|
||||
if (this.availableLLMs.length) {
|
||||
this.selectedLLM = this.availableLLMs[0].id;
|
||||
@@ -127,7 +127,7 @@ export default class AiSpam extends Component {
|
||||
}
|
||||
|
||||
get llmId() {
|
||||
return this.selectedLLM.toString().split(":")[1];
|
||||
return this.selectedLLM;
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
@@ -158,3 +158,51 @@
|
||||
.ai-expanded-list__toggle-button {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ai-configure-default-llm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
background: var(--primary-very-low);
|
||||
padding: 1rem;
|
||||
margin-block: 1rem;
|
||||
border-radius: var(--d-border-radius);
|
||||
|
||||
&__header {
|
||||
flex: 3;
|
||||
}
|
||||
|
||||
&__setting {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.setting-controls__undo {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: var(--primary);
|
||||
font-size: var(--font-0);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
font-size: var(--font-down-1);
|
||||
color: var(--primary-high-or-secondary-low);
|
||||
}
|
||||
|
||||
.select-kit,
|
||||
.btn {
|
||||
font-size: var(--font-down-1);
|
||||
}
|
||||
|
||||
.setting-label,
|
||||
.desc {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +184,10 @@ en:
|
||||
discourse_ai:
|
||||
title: "AI"
|
||||
|
||||
default_llm:
|
||||
title: "Default LLM model"
|
||||
description: "The default LLM model to use for all AI features. This will be used if no LLM is specified in the feature configuration or persona. If no default LLM is specified, the last created LLM will be used."
|
||||
|
||||
features:
|
||||
short_title: "Features"
|
||||
description: "These are the AI features available to visitors on your site. These can be configured to use specific personas and LLMs, and can be access controlled by groups."
|
||||
@@ -566,7 +570,7 @@ en:
|
||||
max_usages_required: "Must be set if max tokens is not set"
|
||||
usage:
|
||||
ai_bot: "AI bot"
|
||||
ai_helper: "Helper"
|
||||
ai_helper: "Helper (%{persona})"
|
||||
ai_helper_image_caption: "Image caption"
|
||||
ai_persona: "Persona (%{persona})"
|
||||
ai_summarization: "Summarize"
|
||||
|
||||
@@ -25,6 +25,7 @@ en:
|
||||
description: "Periodic report based on a large language model"
|
||||
site_settings:
|
||||
discourse_ai_enabled: "Enable the discourse AI plugin."
|
||||
ai_default_llm_model: "The default LLM model to use for all AI features"
|
||||
ai_artifact_security: "The AI artifact system generates IFRAMEs with runnable code. Strict mode forces an extra click to run code. Lax mode runs code immediately. Hybrid mode allows user to supply data-ai-artifact-autorun to show right away. Disabled mode disables the artifact system."
|
||||
ai_toxicity_enabled: "Enable the toxicity module."
|
||||
ai_toxicity_inference_service_api_endpoint: "URL where the API is running for the toxicity module"
|
||||
@@ -582,7 +583,7 @@ en:
|
||||
llm:
|
||||
configuration:
|
||||
create_llm: "You need to setup an LLM before enabling this feature"
|
||||
disable_module_first: "You have to disable %{setting} first."
|
||||
disable_modules_first: "You must disable these modules first: %{settings}"
|
||||
set_llm_first: "Set %{setting} first"
|
||||
model_unreachable: "We couldn't get a response from this model. Check your settings first."
|
||||
invalid_seeded_model: "You can't use this model with this feature"
|
||||
|
||||
+12
-8
@@ -11,6 +11,12 @@ discourse_ai:
|
||||
- "lax"
|
||||
- "hybrid"
|
||||
- "strict"
|
||||
ai_default_llm_model:
|
||||
default: ""
|
||||
type: enum
|
||||
allow_any: false
|
||||
enum: "DiscourseAi::Configuration::LlmEnumerator"
|
||||
validator: "DiscourseAi::Configuration::LlmValidator"
|
||||
|
||||
ai_sentiment_enabled:
|
||||
default: false
|
||||
@@ -103,7 +109,7 @@ discourse_ai:
|
||||
default: false
|
||||
client: true
|
||||
area: "ai-features/ai_helper"
|
||||
ai_helper_model:
|
||||
ai_helper_model: # Deprecated. TODO(keegan): Remove 2025-09-01
|
||||
default: ""
|
||||
allow_any: false
|
||||
type: enum
|
||||
@@ -149,7 +155,7 @@ discourse_ai:
|
||||
- "context_menu"
|
||||
- "image_caption"
|
||||
area: "ai-features/ai_helper"
|
||||
ai_helper_image_caption_model:
|
||||
ai_helper_image_caption_model: # Deprecated. TODO(keegan): Remove 2025-09-01
|
||||
default: ""
|
||||
type: enum
|
||||
enum: "DiscourseAi::Configuration::LlmVisionEnumerator"
|
||||
@@ -260,12 +266,12 @@ discourse_ai:
|
||||
client: true
|
||||
validator: "DiscourseAi::Configuration::LlmDependencyValidator"
|
||||
area: "ai-features/embeddings"
|
||||
ai_embeddings_semantic_search_hyde_model:
|
||||
ai_embeddings_semantic_search_hyde_model: # Deprecated. TODO(keegan): Remove 2025-09-01
|
||||
hidden: true
|
||||
default: ""
|
||||
type: enum
|
||||
allow_any: false
|
||||
enum: "DiscourseAi::Configuration::LlmEnumerator"
|
||||
validator: "DiscourseAi::Configuration::LlmValidator"
|
||||
area: "ai-features/embeddings"
|
||||
ai_embeddings_semantic_search_hyde_model_allowed_seeded_models:
|
||||
default: ""
|
||||
@@ -316,12 +322,11 @@ discourse_ai:
|
||||
client: true
|
||||
validator: "DiscourseAi::Configuration::LlmDependencyValidator"
|
||||
area: "ai-features/summarization"
|
||||
ai_summarization_model:
|
||||
ai_summarization_model: # Deprecated. TODO(keegan): Remove 2025-09-01
|
||||
default: ""
|
||||
allow_any: false
|
||||
type: enum
|
||||
enum: "DiscourseAi::Configuration::LlmEnumerator"
|
||||
validator: "DiscourseAi::Configuration::LlmValidator"
|
||||
hidden: true
|
||||
ai_summarization_persona:
|
||||
default: "-11"
|
||||
@@ -483,12 +488,11 @@ discourse_ai:
|
||||
client: true
|
||||
validator: "DiscourseAi::Configuration::LlmDependencyValidator"
|
||||
area: "ai-features/translation"
|
||||
ai_translation_model:
|
||||
ai_translation_model: # Deprecated. TODO(keegan): Remove 2025-09-01
|
||||
default: ""
|
||||
type: enum
|
||||
allow_any: false
|
||||
enum: "DiscourseAi::Configuration::LlmEnumerator"
|
||||
validator: "DiscourseAi::Configuration::LlmValidator"
|
||||
area: "ai-features/translation"
|
||||
ai_translation_locale_detector_persona:
|
||||
default: "-27"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
class SeedAiDefaultLlmModel < ActiveRecord::Migration[7.2]
|
||||
def up
|
||||
return if DB.query_single("SELECT 1 FROM llm_models LIMIT 1").empty?
|
||||
|
||||
last_model_id = DB.query_single("SELECT id FROM llm_models ORDER BY id DESC LIMIT 1").first
|
||||
|
||||
if last_model_id.present?
|
||||
execute "UPDATE site_settings SET value = '#{last_model_id}' WHERE name = 'ai_default_llm_model' AND (value IS NULL OR value = '');"
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
class CopyAiSummarizationModelToPersonaDefault < ActiveRecord::Migration[7.2]
|
||||
def up
|
||||
ai_summarization_model =
|
||||
DB.query_single("SELECT value FROM site_settings WHERE name = 'ai_summarization_model'").first
|
||||
|
||||
if ai_summarization_model.present? && ai_summarization_model.start_with?("custom:")
|
||||
# Extract the model ID from the setting value (e.g., "custom:-5" -> "-5")
|
||||
model_id = ai_summarization_model.split(":").last
|
||||
|
||||
persona_settings = %w[ai_summarization_persona ai_summary_gists_persona]
|
||||
default_persona_ids = [-11, -12]
|
||||
|
||||
persona_ids_query =
|
||||
persona_settings
|
||||
.map { |setting| "SELECT value FROM site_settings WHERE name = '#{setting}'" }
|
||||
.join(" UNION ")
|
||||
persona_ids = DB.query_single(persona_ids_query).compact
|
||||
all_persona_ids = (default_persona_ids + persona_ids).map(&:to_i).uniq.join(",")
|
||||
|
||||
# Update the summarization personas with the extracted model ID
|
||||
execute(<<~SQL)
|
||||
UPDATE ai_personas
|
||||
SET default_llm_id = #{model_id}
|
||||
WHERE id IN (#{all_persona_ids}) AND default_llm_id IS NULL
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
class CopyAiHelperModelToPersonaDefault < ActiveRecord::Migration[7.2]
|
||||
def up
|
||||
ai_helper_model =
|
||||
DB.query_single("SELECT value FROM site_settings WHERE name = 'ai_helper_model'").first
|
||||
|
||||
if ai_helper_model.present? && ai_helper_model.start_with?("custom:")
|
||||
# Extract the model ID from the setting value (e.g., "custom:1" -> "1")
|
||||
model_id = ai_helper_model.split(":").last
|
||||
|
||||
# Get persona IDs from site settings
|
||||
persona_settings = %w[
|
||||
ai_helper_proofreader_persona
|
||||
ai_helper_title_suggestions_persona
|
||||
ai_helper_explain_persona
|
||||
ai_helper_smart_dates_persona
|
||||
ai_helper_translator_persona
|
||||
ai_helper_markdown_tables_persona
|
||||
ai_helper_custom_prompt_persona
|
||||
]
|
||||
|
||||
default_persona_ids = [-18, -19, -20, -22, -23, -24, -25]
|
||||
persona_ids_query =
|
||||
persona_settings
|
||||
.map { |setting| "SELECT value FROM site_settings WHERE name = '#{setting}'" }
|
||||
.join(" UNION ")
|
||||
persona_ids = DB.query_single(persona_ids_query).compact
|
||||
|
||||
all_persona_ids = (default_persona_ids + persona_ids).map(&:to_i).uniq.join(",")
|
||||
|
||||
# Update the helper personas with the extracted model ID
|
||||
execute(<<~SQL)
|
||||
UPDATE ai_personas
|
||||
SET default_llm_id = #{model_id}
|
||||
WHERE id IN (#{all_persona_ids}) AND default_llm_id IS NULL
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
class CopyHydeModelToPersona < ActiveRecord::Migration[7.2]
|
||||
def up
|
||||
hyde_model =
|
||||
DB.query_single(
|
||||
"SELECT value FROM site_settings WHERE name = 'ai_embeddings_semantic_search_hyde_model'",
|
||||
).first
|
||||
|
||||
if hyde_model.present? && hyde_model.start_with?("custom:")
|
||||
# Extract the model ID from the setting value (e.g., "custom:1" -> "1")
|
||||
model_id = hyde_model.split(":").last
|
||||
|
||||
persona_settings = %w[ai_embeddings_semantic_search_hyde_persona]
|
||||
default_persona_ids = [-32]
|
||||
persona_ids_query =
|
||||
persona_settings
|
||||
.map { |setting| "SELECT value FROM site_settings WHERE name = '#{setting}'" }
|
||||
.join(" UNION ")
|
||||
persona_ids = DB.query_single(persona_ids_query).compact
|
||||
|
||||
all_persona_ids = (default_persona_ids + persona_ids).map(&:to_i).uniq.join(",")
|
||||
|
||||
# Update the hyde persona with the extracted model ID
|
||||
execute(<<~SQL)
|
||||
UPDATE ai_personas
|
||||
SET default_llm_id = #{model_id}
|
||||
WHERE id IN (#{all_persona_ids}) AND default_llm_id IS NULL
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
class CopyTranslationModelToPersona < ActiveRecord::Migration[7.2]
|
||||
def up
|
||||
ai_translation_model =
|
||||
DB.query_single("SELECT value FROM site_settings WHERE name = 'ai_translation_model'").first
|
||||
|
||||
if ai_translation_model.present? && ai_translation_model.start_with?("custom:")
|
||||
# Extract the model ID from the setting value (e.g., "custom:-5" -> "-5")
|
||||
model_id = ai_translation_model.split(":").last
|
||||
|
||||
persona_settings = %w[
|
||||
ai_translation_locale_detector_persona
|
||||
ai_translation_post_raw_translator_persona
|
||||
ai_translation_topic_title_translator_persona
|
||||
ai_translation_short_text_translator_persona
|
||||
]
|
||||
default_persona_ids = [-27, -28, -29, -30]
|
||||
|
||||
persona_ids_query =
|
||||
persona_settings
|
||||
.map { |setting| "SELECT value FROM site_settings WHERE name = '#{setting}'" }
|
||||
.join(" UNION ")
|
||||
persona_ids = DB.query_single(persona_ids_query).compact
|
||||
|
||||
all_persona_ids = (default_persona_ids + persona_ids).map(&:to_i).uniq.join(",")
|
||||
|
||||
# Update the translation personas (IDs -27, -28, -29, -30) with the extracted model ID
|
||||
execute(<<~SQL)
|
||||
UPDATE ai_personas
|
||||
SET default_llm_id = #{model_id}
|
||||
WHERE id IN (#{all_persona_ids}) AND default_llm_id IS NULL
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
class CopyAiImageCaptionModelToPersonaDefault < ActiveRecord::Migration[7.2]
|
||||
def up
|
||||
ai_helper_image_caption_model =
|
||||
DB.query_single(
|
||||
"SELECT value FROM site_settings WHERE name = 'ai_helper_image_caption_model'",
|
||||
).first
|
||||
|
||||
if ai_helper_image_caption_model.present? &&
|
||||
ai_helper_image_caption_model.start_with?("custom:")
|
||||
# Extract the model ID from the setting value (e.g., "custom:1" -> "1")
|
||||
model_id = ai_helper_image_caption_model.split(":").last
|
||||
|
||||
persona_settings = %w[ai_helper_post_illustrator_persona ai_helper_image_caption_persona]
|
||||
default_persona_ids = [-21, -26]
|
||||
|
||||
persona_ids_query =
|
||||
persona_settings
|
||||
.map { |setting| "SELECT value FROM site_settings WHERE name = '#{setting}'" }
|
||||
.join(" UNION ")
|
||||
persona_ids = DB.query_single(persona_ids_query).compact
|
||||
|
||||
all_persona_ids = (default_persona_ids + persona_ids).map(&:to_i).uniq.join(",")
|
||||
|
||||
# Update the helper personas with the extracted model ID
|
||||
execute(<<~SQL)
|
||||
UPDATE ai_personas
|
||||
SET default_llm_id = #{model_id}
|
||||
WHERE id IN (#{all_persona_ids}) AND default_llm_id IS NULL
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
||||
@@ -312,18 +312,9 @@ module DiscourseAi
|
||||
|
||||
# Priorities are:
|
||||
# 1. Persona's default LLM
|
||||
# 2. Hidden `ai_helper_model` setting, or `ai_helper_image_caption_model` for image_caption.
|
||||
# 3. Newest LLM config
|
||||
# 2. SiteSetting.ai_default_llm_id (or newest LLM if not set)
|
||||
def self.find_ai_helper_model(helper_mode, persona_klass)
|
||||
model_id = persona_klass.default_llm_id
|
||||
|
||||
if !model_id
|
||||
if helper_mode == IMAGE_CAPTION
|
||||
model_id = SiteSetting.ai_helper_image_caption_model&.split(":")&.last
|
||||
else
|
||||
model_id = SiteSetting.ai_helper_model&.split(":")&.last
|
||||
end
|
||||
end
|
||||
model_id = persona_klass.default_llm_id || SiteSetting.ai_default_llm_model
|
||||
|
||||
if model_id.present?
|
||||
LlmModel.find_by(id: model_id)
|
||||
|
||||
@@ -12,10 +12,14 @@ module DiscourseAi
|
||||
return nil if content.blank?
|
||||
|
||||
suggested_title = call_llm(content)
|
||||
return nil if suggested_title.blank?
|
||||
|
||||
cleanup(suggested_title)
|
||||
end
|
||||
|
||||
def call_llm(thread_content)
|
||||
return nil if SiteSetting.ai_default_llm_model.blank?
|
||||
|
||||
chat = "<input>\n#{thread_content}\n</input>"
|
||||
|
||||
prompt =
|
||||
@@ -30,7 +34,7 @@ module DiscourseAi
|
||||
messages: [{ type: :user, content: chat, id: "User" }],
|
||||
)
|
||||
|
||||
DiscourseAi::Completions::Llm.proxy(SiteSetting.ai_helper_model).generate(
|
||||
DiscourseAi::Completions::Llm.proxy(SiteSetting.ai_default_llm_model).generate(
|
||||
prompt,
|
||||
user: Discourse.system_user,
|
||||
stop_sequences: ["</input>"],
|
||||
|
||||
@@ -57,6 +57,12 @@ module DiscourseAi
|
||||
end
|
||||
|
||||
def diffusion_prompt(text, user)
|
||||
llm_model =
|
||||
AiPersona.find_by(id: SiteSetting.ai_helper_post_illustrator_persona)&.default_llm_id ||
|
||||
SiteSetting.ai_default_llm_model
|
||||
|
||||
return nil if llm_model.blank?
|
||||
|
||||
prompt =
|
||||
DiscourseAi::Completions::Prompt.new(
|
||||
<<~TEXT.strip,
|
||||
@@ -66,7 +72,7 @@ module DiscourseAi
|
||||
messages: [{ type: :user, content: text, id: user.username }],
|
||||
)
|
||||
|
||||
DiscourseAi::Completions::Llm.proxy(SiteSetting.ai_helper_model).generate(
|
||||
DiscourseAi::Completions::Llm.proxy(llm_model).generate(
|
||||
prompt,
|
||||
user: user,
|
||||
feature_name: "illustrate_post",
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ module DiscourseAi
|
||||
value_h["id"] > 0 ||
|
||||
SiteSetting.ai_automation_allowed_seeded_models_map.include?(value_h["id"].to_s)
|
||||
end
|
||||
.each { |value_h| value_h["id"] = "custom:#{value_h["id"]}" }
|
||||
.each { |value_h| value_h["id"] = "#{value_h["id"]}" }
|
||||
|
||||
values
|
||||
end
|
||||
|
||||
@@ -74,7 +74,7 @@ module DiscourseAi
|
||||
else
|
||||
I18n.t("discourse_automation.scriptables.llm_report.title")
|
||||
end
|
||||
@model = LlmModel.find_by(id: model.split(":")&.last)
|
||||
@model = LlmModel.find_by(id: model)
|
||||
@persona = AiPersona.find(persona_id).class_instance.new
|
||||
@category_ids = category_ids
|
||||
@tags = tags
|
||||
|
||||
@@ -292,6 +292,8 @@ module DiscourseAi
|
||||
llm_model =
|
||||
if model.is_a?(LlmModel)
|
||||
model
|
||||
elsif model.is_a?(Numeric)
|
||||
LlmModel.find_by(id: model)
|
||||
else
|
||||
model_name_without_prov = model.split(":").last.to_i
|
||||
|
||||
|
||||
@@ -10,16 +10,9 @@ module DiscourseAi
|
||||
def valid_value?(val)
|
||||
return true if val == "f"
|
||||
|
||||
if @opts[:name] == :ai_summarization_enabled || @opts[:name] == :ai_helper_enabled
|
||||
has_llms = LlmModel.count > 0
|
||||
@no_llms_configured = !has_llms
|
||||
has_llms
|
||||
else
|
||||
@llm_dependency_setting_name =
|
||||
DiscourseAi::Configuration::LlmValidator.new.choose_llm_setting_for(@opts[:name])
|
||||
@llm_dependency_setting_name = :ai_default_llm_model
|
||||
|
||||
SiteSetting.public_send(@llm_dependency_setting_name).present?
|
||||
end
|
||||
SiteSetting.public_send(@llm_dependency_setting_name).present?
|
||||
end
|
||||
|
||||
def error_message
|
||||
|
||||
@@ -22,24 +22,53 @@ module DiscourseAi
|
||||
.each { |llm_id, name, id| rval[llm_id] << { type: :ai_persona, name: name, id: id } }
|
||||
|
||||
if SiteSetting.ai_helper_enabled
|
||||
model_id = SiteSetting.ai_helper_model.split(":").last.to_i
|
||||
rval[model_id] << { type: :ai_helper } if model_id != 0
|
||||
{
|
||||
"#{I18n.t("js.discourse_ai.features.ai_helper.proofread")}" =>
|
||||
SiteSetting.ai_helper_proofreader_persona,
|
||||
"#{I18n.t("js.discourse_ai.features.ai_helper.title_suggestions")}" =>
|
||||
SiteSetting.ai_helper_title_suggestions_persona,
|
||||
"#{I18n.t("js.discourse_ai.features.ai_helper.explain")}" =>
|
||||
SiteSetting.ai_helper_explain_persona,
|
||||
"#{I18n.t("js.discourse_ai.features.ai_helper.illustrate_post")}" =>
|
||||
SiteSetting.ai_helper_post_illustrator_persona,
|
||||
"#{I18n.t("js.discourse_ai.features.ai_helper.smart_dates")}" =>
|
||||
SiteSetting.ai_helper_smart_dates_persona,
|
||||
"#{I18n.t("js.discourse_ai.features.ai_helper.translator")}" =>
|
||||
SiteSetting.ai_helper_translator_persona,
|
||||
"#{I18n.t("js.discourse_ai.features.ai_helper.markdown_tables")}" =>
|
||||
SiteSetting.ai_helper_markdown_tables_persona,
|
||||
"#{I18n.t("js.discourse_ai.features.ai_helper.custom_prompt")}" =>
|
||||
SiteSetting.ai_helper_custom_prompt_persona,
|
||||
}.each do |helper_type, persona_id|
|
||||
next if persona_id.blank?
|
||||
|
||||
persona = AiPersona.find_by(id: persona_id)
|
||||
next if persona.blank? || persona.default_llm_id.blank?
|
||||
|
||||
model_id = persona.default_llm_id || SiteSetting.ai_default_llm_model.to_i
|
||||
rval[model_id] << { type: :ai_helper, name: helper_type }
|
||||
end
|
||||
end
|
||||
|
||||
if SiteSetting.ai_helper_image_caption_model
|
||||
model_id = SiteSetting.ai_helper_image_caption_model.split(":").last.to_i
|
||||
rval[model_id] << { type: :ai_helper_image_caption } if model_id != 0
|
||||
if SiteSetting.ai_helper_enabled_features.split("|").include?("image_caption")
|
||||
image_caption_persona = AiPersona.find_by(id: SiteSetting.ai_helper_image_caption_persona)
|
||||
model_id = image_caption_persona.default_llm_id || SiteSetting.ai_default_llm_model.to_i
|
||||
|
||||
rval[model_id] << { type: :ai_helper_image_caption }
|
||||
end
|
||||
|
||||
if SiteSetting.ai_summarization_enabled
|
||||
summarization_persona = AiPersona.find_by(id: SiteSetting.ai_summarization_persona)
|
||||
model_id = summarization_persona.default_llm_id || LlmModel.last&.id
|
||||
model_id = summarization_persona.default_llm_id || SiteSetting.ai_default_llm_model.to_i
|
||||
|
||||
rval[model_id] << { type: :ai_summarization }
|
||||
end
|
||||
|
||||
if SiteSetting.ai_embeddings_semantic_search_enabled
|
||||
model_id = SiteSetting.ai_embeddings_semantic_search_hyde_model.split(":").last.to_i
|
||||
search_persona =
|
||||
AiPersona.find_by(id: SiteSetting.ai_embeddings_semantic_search_hyde_persona)
|
||||
model_id = search_persona.default_llm_id || SiteSetting.ai_default_llm_model.to_i
|
||||
|
||||
rval[model_id] << { type: :ai_embeddings_semantic_search }
|
||||
end
|
||||
|
||||
@@ -58,12 +87,8 @@ module DiscourseAi
|
||||
)
|
||||
.each do |model_text, name, id|
|
||||
next if model_text.blank?
|
||||
model_id = model_text.split("custom:").last.to_i
|
||||
if model_id.present?
|
||||
if model_text =~ /custom:(\d+)/
|
||||
rval[model_id] << { type: :automation, name: name, id: id }
|
||||
end
|
||||
end
|
||||
model_id = model_text.to_i
|
||||
rval[model_id] << { type: :automation, name: name, id: id } if model_id.present?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -94,7 +119,7 @@ module DiscourseAi
|
||||
builder.query_hash.map(&:symbolize_keys)
|
||||
end
|
||||
|
||||
def self.values(allowed_seeded_llms: nil)
|
||||
def self.values(allowed_seeded_llms: [])
|
||||
values = DB.query_hash(<<~SQL).map(&:symbolize_keys)
|
||||
SELECT display_name AS name, id AS value
|
||||
FROM llm_models
|
||||
@@ -107,7 +132,6 @@ module DiscourseAi
|
||||
end
|
||||
end
|
||||
|
||||
values.each { |value_h| value_h[:value] = "custom:#{value_h[:value]}" }
|
||||
values
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
|
||||
module DiscourseAi
|
||||
module Configuration
|
||||
class InvalidSeededModelError < StandardError
|
||||
end
|
||||
|
||||
class LlmValidator
|
||||
def initialize(opts = {})
|
||||
@opts = opts
|
||||
@@ -12,18 +9,21 @@ module DiscourseAi
|
||||
|
||||
def valid_value?(val)
|
||||
if val == ""
|
||||
@parent_module_name = modules_and_choose_llm_settings.invert[@opts[:name]]
|
||||
if @opts[:name] == :ai_default_llm_model
|
||||
@parent_module_names = []
|
||||
|
||||
@parent_enabled = SiteSetting.public_send(@parent_module_name)
|
||||
return !@parent_enabled
|
||||
enabled_settings.each do |setting_name|
|
||||
if SiteSetting.public_send(setting_name) == true
|
||||
@parent_module_names << setting_name
|
||||
@parent_enabled = true
|
||||
end
|
||||
end
|
||||
|
||||
return !@parent_enabled
|
||||
end
|
||||
end
|
||||
|
||||
allowed_seeded_model?(val)
|
||||
|
||||
run_test(val).tap { |result| @unreachable = result }
|
||||
rescue DiscourseAi::Configuration::InvalidSeededModelError => e
|
||||
@unreachable = true
|
||||
false
|
||||
rescue StandardError => e
|
||||
raise e if Rails.env.test?
|
||||
@unreachable = true
|
||||
@@ -38,54 +38,35 @@ module DiscourseAi
|
||||
end
|
||||
|
||||
def modules_using(llm_model)
|
||||
choose_llm_settings = modules_and_choose_llm_settings.values
|
||||
in_use_llms = AiPersona.where.not(default_llm_id: nil).pluck(:default_llm_id)
|
||||
default_llm = SiteSetting.ai_default_llm_model.presence&.to_i
|
||||
|
||||
choose_llm_settings.select { |s| SiteSetting.public_send(s) == "custom:#{llm_model.id}" }
|
||||
combined_llms = (in_use_llms + [default_llm]).compact.uniq
|
||||
combined_llms
|
||||
end
|
||||
|
||||
def error_message
|
||||
if @parent_enabled
|
||||
if @parent_enabled && @parent_module_names.present?
|
||||
return(
|
||||
I18n.t(
|
||||
"discourse_ai.llm.configuration.disable_module_first",
|
||||
setting: @parent_module_name,
|
||||
"discourse_ai.llm.configuration.disable_modules_first",
|
||||
settings: @parent_module_names.join(", "),
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
if @invalid_seeded_model
|
||||
return I18n.t("discourse_ai.llm.configuration.invalid_seeded_model")
|
||||
end
|
||||
|
||||
return unless @unreachable
|
||||
|
||||
I18n.t("discourse_ai.llm.configuration.model_unreachable")
|
||||
end
|
||||
|
||||
def choose_llm_setting_for(module_enabler_setting)
|
||||
modules_and_choose_llm_settings[module_enabler_setting]
|
||||
end
|
||||
|
||||
def modules_and_choose_llm_settings
|
||||
{
|
||||
ai_embeddings_semantic_search_enabled: :ai_embeddings_semantic_search_hyde_model,
|
||||
ai_helper_enabled: :ai_helper_model,
|
||||
ai_summarization_enabled: :ai_summarization_model,
|
||||
ai_translation_enabled: :ai_translation_model,
|
||||
}
|
||||
end
|
||||
|
||||
def allowed_seeded_model?(val)
|
||||
id = val.split(":").last
|
||||
return true if id.to_i > 0
|
||||
|
||||
setting = @opts[:name]
|
||||
allowed_list = SiteSetting.public_send("#{setting}_allowed_seeded_models")
|
||||
|
||||
if allowed_list.split("|").exclude?(id)
|
||||
@invalid_seeded_model = true
|
||||
raise DiscourseAi::Configuration::InvalidSeededModelError.new
|
||||
end
|
||||
def enabled_settings
|
||||
%i[
|
||||
ai_embeddings_semantic_search_enabled
|
||||
ai_helper_enabled
|
||||
ai_summarization_enabled
|
||||
ai_translation_enabled
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,8 +16,6 @@ module DiscourseAi
|
||||
WHERE vision_enabled
|
||||
SQL
|
||||
|
||||
values.each { |value_h| value_h[:value] = "custom:#{value_h[:value]}" }
|
||||
|
||||
values
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,8 +6,8 @@ module DiscourseAi
|
||||
def self.clear_cache_for(query)
|
||||
digest = OpenSSL::Digest::SHA1.hexdigest(query)
|
||||
|
||||
hyde_key =
|
||||
"semantic-search-#{digest}-#{SiteSetting.ai_embeddings_semantic_search_hyde_model}"
|
||||
hyde_model_id = find_ai_hyde_model_id
|
||||
hyde_key = "semantic-search-#{digest}-#{hyde_model_id}"
|
||||
|
||||
Discourse.cache.delete(hyde_key)
|
||||
Discourse.cache.delete("#{hyde_key}-#{SiteSetting.ai_embeddings_selected_model}")
|
||||
@@ -20,12 +20,9 @@ module DiscourseAi
|
||||
|
||||
def cached_query?(query)
|
||||
digest = OpenSSL::Digest::SHA1.hexdigest(query)
|
||||
hyde_model_id = self.class.find_ai_hyde_model_id
|
||||
embedding_key =
|
||||
build_embedding_key(
|
||||
digest,
|
||||
SiteSetting.ai_embeddings_semantic_search_hyde_model,
|
||||
SiteSetting.ai_embeddings_selected_model,
|
||||
)
|
||||
build_embedding_key(digest, hyde_model_id, SiteSetting.ai_embeddings_selected_model)
|
||||
|
||||
Discourse.cache.read(embedding_key).present?
|
||||
end
|
||||
@@ -36,14 +33,11 @@ module DiscourseAi
|
||||
|
||||
def hyde_embedding(search_term)
|
||||
digest = OpenSSL::Digest::SHA1.hexdigest(search_term)
|
||||
hyde_key = build_hyde_key(digest, SiteSetting.ai_embeddings_semantic_search_hyde_model)
|
||||
hyde_model_id = self.class.find_ai_hyde_model_id
|
||||
hyde_key = build_hyde_key(digest, hyde_model_id)
|
||||
|
||||
embedding_key =
|
||||
build_embedding_key(
|
||||
digest,
|
||||
SiteSetting.ai_embeddings_semantic_search_hyde_model,
|
||||
SiteSetting.ai_embeddings_selected_model,
|
||||
)
|
||||
build_embedding_key(digest, hyde_model_id, SiteSetting.ai_embeddings_selected_model)
|
||||
|
||||
hypothetical_post =
|
||||
Discourse
|
||||
@@ -111,6 +105,7 @@ module DiscourseAi
|
||||
max_semantic_results_per_page = 100
|
||||
search = Search.new(query, { guardian: guardian })
|
||||
search_term = search.term
|
||||
hyde_model_id = self.class.find_ai_hyde_model_id
|
||||
|
||||
return [] if search_term.nil? || search_term.length < SiteSetting.min_search_term_length
|
||||
|
||||
@@ -119,11 +114,7 @@ module DiscourseAi
|
||||
digest = OpenSSL::Digest::SHA1.hexdigest(search_term)
|
||||
|
||||
embedding_key =
|
||||
build_embedding_key(
|
||||
digest,
|
||||
SiteSetting.ai_embeddings_semantic_search_hyde_model,
|
||||
SiteSetting.ai_embeddings_selected_model,
|
||||
)
|
||||
build_embedding_key(digest, hyde_model_id, SiteSetting.ai_embeddings_selected_model)
|
||||
|
||||
search_term_embedding =
|
||||
Discourse
|
||||
@@ -210,15 +201,28 @@ module DiscourseAi
|
||||
|
||||
# Priorities are:
|
||||
# 1. Persona's default LLM
|
||||
# 2. `ai_embeddings_semantic_search_hyde_model` setting.
|
||||
# 2. SiteSetting.ai_default_llm_id (or newest LLM if not set)
|
||||
def find_ai_hyde_model(persona_klass)
|
||||
model_id =
|
||||
persona_klass.default_llm_id ||
|
||||
SiteSetting.ai_embeddings_semantic_search_hyde_model&.split(":")&.last
|
||||
model_id = persona_klass.default_llm_id || SiteSetting.ai_default_llm_model
|
||||
|
||||
return if model_id.blank?
|
||||
if model_id.present?
|
||||
LlmModel.find_by(id: model_id)
|
||||
else
|
||||
LlmModel.last
|
||||
end
|
||||
end
|
||||
|
||||
LlmModel.find_by(id: model_id)
|
||||
def self.find_ai_hyde_model_id
|
||||
persona_llm_id =
|
||||
AiPersona.find_by(
|
||||
id: SiteSetting.ai_embeddings_semantic_search_hyde_persona,
|
||||
)&.default_llm_id
|
||||
|
||||
if persona_llm_id.present?
|
||||
persona_llm_id
|
||||
else
|
||||
SiteSetting.ai_default_llm_model.to_i || LlmModel.last&.id
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -54,11 +54,9 @@ module DiscourseAi
|
||||
|
||||
# Priorities are:
|
||||
# 1. Persona's default LLM
|
||||
# 2. Hidden `ai_summarization_model` setting
|
||||
# 3. Newest LLM config
|
||||
# 2. SiteSetting.ai_default_llm_id (or newest LLM if not set)
|
||||
def find_summarization_model(persona_klass)
|
||||
model_id =
|
||||
persona_klass.default_llm_id || SiteSetting.ai_summarization_model&.split(":")&.last # Remove legacy custom provider.
|
||||
model_id = persona_klass.default_llm_id || SiteSetting.ai_default_llm_model
|
||||
|
||||
if model_id.present?
|
||||
LlmModel.find_by(id: model_id)
|
||||
@@ -73,7 +71,7 @@ module DiscourseAi
|
||||
persona = persona_klass.new
|
||||
user = User.find_by(id: persona_klass.user_id) || Discourse.system_user
|
||||
|
||||
bot = DiscourseAi::Personas::Bot.as(user, persona: persona, model: llm_model)
|
||||
DiscourseAi::Personas::Bot.as(user, persona: persona, model: llm_model)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace :ai do
|
||||
messages: [{ type: :user, content: prompt, id: "user" }],
|
||||
)
|
||||
|
||||
DiscourseAi::Completions::Llm.proxy(SiteSetting.ai_helper_model).generate(
|
||||
DiscourseAi::Completions::Llm.proxy(SiteSetting.ai_defaulit_llm_model).generate(
|
||||
prompt,
|
||||
user: Discourse.system_user,
|
||||
feature_name: "topic-generator",
|
||||
|
||||
+19
-2
@@ -3,11 +3,28 @@
|
||||
module DiscourseAi
|
||||
module Translation
|
||||
def self.enabled?
|
||||
SiteSetting.discourse_ai_enabled && SiteSetting.ai_translation_enabled &&
|
||||
SiteSetting.ai_translation_model.present? &&
|
||||
SiteSetting.discourse_ai_enabled && SiteSetting.ai_translation_enabled && has_llm_model? &&
|
||||
SiteSetting.content_localization_supported_locales.present?
|
||||
end
|
||||
|
||||
def self.has_llm_model?
|
||||
persona_ids = [
|
||||
SiteSetting.ai_translation_locale_detector_persona,
|
||||
SiteSetting.ai_translation_post_raw_translator_persona,
|
||||
SiteSetting.ai_translation_topic_title_translator_persona,
|
||||
SiteSetting.ai_translation_short_text_translator_persona,
|
||||
]
|
||||
|
||||
persona_default_llms = AiPersona.where(id: persona_ids).pluck(:default_llm_id)
|
||||
default_llm_model = SiteSetting.ai_default_llm_model
|
||||
|
||||
if persona_default_llms.any?(&:blank?) && default_llm_model.blank?
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def self.backfill_enabled?
|
||||
enabled? && SiteSetting.ai_translation_backfill_hourly_rate > 0 &&
|
||||
SiteSetting.ai_translation_backfill_max_age_days > 0
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user