Compare commits
40
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
822594f3e3 | ||
|
|
7dee59c97f | ||
|
|
fbbe01e576 | ||
|
|
f420261196 | ||
|
|
eb93e1736b | ||
|
|
cc34882074 | ||
|
|
4b03cdcc96 | ||
|
|
d9b53b4a3d | ||
|
|
a927fd2270 | ||
|
|
f077d58abf | ||
|
|
08d2f3ddf9 | ||
|
|
273a1fa618 | ||
|
|
7af3ce820d | ||
|
|
5841543bd9 | ||
|
|
5ae80645bb | ||
|
|
b675c4c39b | ||
|
|
0fadf1da1a | ||
|
|
c26d604072 | ||
|
|
ad6a8cb812 | ||
|
|
7ae61ce877 | ||
|
|
f593ab6e1e | ||
|
|
b58f198ee8 | ||
|
|
06743d1939 | ||
|
|
88811f97bf | ||
|
|
2377b286dd | ||
|
|
67664029e5 | ||
|
|
1b16fc876c | ||
|
|
8c069490f0 | ||
|
|
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?
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ class EmbeddingDefinition < ActiveRecord::Base
|
||||
dimensions: 2000,
|
||||
max_sequence_length: 8191,
|
||||
pg_function: "<=>",
|
||||
tokenizer_class: "DiscourseAi::Tokenizer::OpenAiTokenizer",
|
||||
tokenizer_class: "DiscourseAi::Tokenizer::OpenAiCl100kTokenizer",
|
||||
url: "https://api.openai.com/v1/embeddings",
|
||||
provider: OPEN_AI,
|
||||
matryoshka_dimensions: true,
|
||||
@@ -98,7 +98,7 @@ class EmbeddingDefinition < ActiveRecord::Base
|
||||
dimensions: 1536,
|
||||
max_sequence_length: 8191,
|
||||
pg_function: "<=>",
|
||||
tokenizer_class: "DiscourseAi::Tokenizer::OpenAiTokenizer",
|
||||
tokenizer_class: "DiscourseAi::Tokenizer::OpenAiCl100kTokenizer",
|
||||
url: "https://api.openai.com/v1/embeddings",
|
||||
provider: OPEN_AI,
|
||||
matryoshka_dimensions: true,
|
||||
@@ -112,7 +112,7 @@ class EmbeddingDefinition < ActiveRecord::Base
|
||||
dimensions: 1536,
|
||||
max_sequence_length: 8191,
|
||||
pg_function: "<=>",
|
||||
tokenizer_class: "DiscourseAi::Tokenizer::OpenAiTokenizer",
|
||||
tokenizer_class: "DiscourseAi::Tokenizer::OpenAiCl100kTokenizer",
|
||||
url: "https://api.openai.com/v1/embeddings",
|
||||
provider: OPEN_AI,
|
||||
provider_params: {
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -66,12 +66,9 @@ class ExpandableList extends Component {
|
||||
|
||||
export default class AiFeaturesList extends Component {
|
||||
get sortedModules() {
|
||||
if (!this.args.modules || !this.args.modules.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.args.modules.toSorted((a, b) =>
|
||||
a.module_name.localeCompare(b.module_name)
|
||||
const modules = this.args.modules?.toArray() || this.args.modules;
|
||||
return [...(modules || [])].toSorted((a, b) =>
|
||||
(a.module_name || "").localeCompare(b.module_name || "")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +233,9 @@ de:
|
||||
inference:
|
||||
name: "Abgeleitete Konzepte"
|
||||
description: "Ordnet Themen und Beiträge in Interessensgebiete / Labels."
|
||||
generate_concepts: "Konzepterkennung"
|
||||
match_concepts: "Passende Konzepte"
|
||||
deduplicate_concepts: "Deduplizierung von Konzepten"
|
||||
ai_helper:
|
||||
name: "Helfer"
|
||||
description: "Unterstützt Benutzer bei der Interaktion in der Community, z. B. beim Erstellen von Themen, Verfassen von Beiträgen und Lesen von Inhalten"
|
||||
@@ -773,6 +775,7 @@ de:
|
||||
shortcut_title: "Starte eine PN mit einem KI-Bot"
|
||||
share: "KI-Unterhaltung kopieren"
|
||||
conversation_shared: "Unterhaltung kopiert"
|
||||
embed_copied: "Einbettung in die Zwischenablage kopiert"
|
||||
debug_ai: "Rohdaten der KI-Anfrage und -Antwort anzeigen"
|
||||
sidebar_empty: "Der Verlauf der Bot-Konversation wird hier angezeigt."
|
||||
debug_ai_modal:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -116,7 +116,7 @@ de:
|
||||
ai_translation_model: "Das Modell, das für die Übersetzung verwendet werden soll. Dieses Modell muss die Übersetzung unterstützen. Personas können diese Einstellung überschreiben."
|
||||
ai_translation_backfill_limit_to_public_content: "Wenn diese Option aktiviert ist, werden nur Inhalte in öffentlichen Kategorien übersetzt. Wenn sie deaktiviert ist, werden auch Inhalte in Gruppen-PMs und privaten Kategorien zur Übersetzung gesendet."
|
||||
ai_translation_max_post_length: "Die maximale Länge eines zu übersetzenden Beitrags. Längere Beiträge werden nicht übersetzt."
|
||||
ai_translation_backfill_max_age_days: "Das maximale Alter eines zu übersetzenden Beitrags und Themas. Ältere Beiträge und Themen werden nicht übersetzt."
|
||||
ai_translation_backfill_max_age_days: "Das maximale Alter eines Beitrags oder Themas, das übersetzt werden soll. Beiträge und Themen, die älter sind, werden nicht übersetzt. 0 deaktiviert die rückwirkende Übersetzung, aber nicht die Übersetzung von neuen Beiträgen."
|
||||
reviewables:
|
||||
reasons:
|
||||
flagged_by_toxicity: Das KI-Plug-in meldete dies nach der Klassifizierung als toxisch.
|
||||
@@ -221,6 +221,7 @@ de:
|
||||
one: "Du kannst nur %{count} Schlüssel im Artefakt haben."
|
||||
other: "Du kannst nur %{count} Schlüssel im Artefakt haben."
|
||||
link: "In neuem Tab anzeigen"
|
||||
copy_embed: "Einbettung kopieren"
|
||||
view_source: "Quelle anzeigen"
|
||||
view_changes: "Änderungen anzeigen"
|
||||
change_description: "Beschreibung ändern"
|
||||
@@ -590,6 +591,7 @@ de:
|
||||
bedrock_invalid_url: "Bitte fülle alle Felder aus, um dieses Modell zu verwenden."
|
||||
ai_staff_action_logger:
|
||||
updated: "aktualisiert"
|
||||
set: "gesetzt"
|
||||
removed: "entfernt"
|
||||
errors:
|
||||
quota_exceeded: "Du hast das Kontingent für dieses Modell überschritten. Bitte versuche es erneut in %{relative_time}."
|
||||
|
||||
@@ -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,14 @@
|
||||
# frozen_string_literal: true
|
||||
class UpdateOpenAiEmbeddingsTokenizer < ActiveRecord::Migration[7.2]
|
||||
def up
|
||||
execute <<~SQL
|
||||
UPDATE embedding_definitions
|
||||
SET tokenizer_class = 'DiscourseAi::Tokenizer::OpenAiCl100kTokenizer'
|
||||
WHERE url LIKE '%https://api.openai.com/%' AND tokenizer_class <> 'DiscourseAi::Tokenizer::OpenAiCl100kTokenizer'
|
||||
SQL
|
||||
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
|
||||
@@ -154,12 +154,10 @@ module DiscourseAi
|
||||
result = +""
|
||||
bot = DiscourseAi::Personas::Bot.as(Discourse.system_user, persona: @persona, model: @model)
|
||||
json_summary_schema_key = @persona.response_format&.first.to_h
|
||||
output = nil
|
||||
|
||||
buffer_blk =
|
||||
Proc.new do |partial, _, type|
|
||||
if type == :structured_output
|
||||
output = partial.dup
|
||||
read_chunk = partial.read_buffered_property(json_summary_schema_key["key"]&.to_sym)
|
||||
|
||||
print read_chunk if Rails.env.development? && @debug_mode
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user