Compare commits

..
Author SHA1 Message Date
Roman Rizzi 822594f3e3 Update db/migrate/20250716005855_copy_ai_image_caption_model_to_persona_default.rb 2025-07-21 18:00:01 -03:00
Keegan George 7dee59c97f DEV: Remove no longer needed seeded model check 2025-07-21 08:08:20 -07:00
Keegan George fbbe01e576 DEV: Hide seeded LLMs in the enumerator for now 2025-07-21 07:45:27 -07:00
Keegan George f420261196 DEV: Apply feedback from review 2025-07-18 12:17:08 -07:00
Keegan George eb93e1736b Final spec fix 🤞 2025-07-17 14:46:15 -07:00
Keegan George cc34882074 FIX: rest of specs 2025-07-17 14:34:29 -07:00
Keegan George 4b03cdcc96 FIX: LLM controller spec 2025-07-17 13:31:26 -07:00
Keegan George d9b53b4a3d FIX: semantic search 2025-07-17 12:23:38 -07:00
Keegan George a927fd2270 fix 2025-07-17 12:00:05 -07:00
Keegan George f077d58abf FIX: automation spec 2025-07-17 11:48:28 -07:00
Keegan George 08d2f3ddf9 FIX: spam 2025-07-17 11:36:19 -07:00
Keegan George 273a1fa618 FIX: occurrences of old model setting 2025-07-17 09:43:45 -07:00
Keegan George 7af3ce820d DEV: Remove references to translation model 2025-07-17 09:27:08 -07:00
Keegan George 5841543bd9 FIX: validator 2025-07-16 14:51:32 -07:00
Keegan George 5ae80645bb Merge branch 'main' into default-llm-model 2025-07-16 12:07:10 -07:00
Keegan George b675c4c39b DEV: Remove custom prefix in specs 2025-07-16 11:39:55 -07:00
Keegan George 0fadf1da1a DEV: update enumerator 2025-07-16 10:56:18 -07:00
Keegan George c26d604072 FIX: dependency validator should depend on default LLM setting 2025-07-16 07:08:29 -07:00
Natalie Tay ad6a8cb812 DEV: Switch translations out of structured output as it returns only a single value (#1503)
Since translations only require a single key back, there is little point in using structured output. This PR also includes some prompt updates dealing with quotes, details, and code.

Related: #1502

This does mean reverting discourse/discourse-translator#257, but we can see how it goes.
2025-07-16 13:38:00 +08:00
Keegan George 7ae61ce877 DEV: update automation to remove custom: prefix 2025-07-15 18:08:41 -07:00
Keegan George f593ab6e1e DEV: remove custom: prefix
and update migrations for translations
2025-07-15 18:07:05 -07:00
Keegan George b58f198ee8 FIX: don't run_test in testing env 2025-07-15 11:08:39 -07:00
Roman Rizzi 06743d1939 FIX: Fix embeddings to use the old OpenAI tokenizer (#1506) 2025-07-15 14:44:11 -03:00
Keegan George 88811f97bf fixes 2025-07-15 10:23:58 -07:00
Keegan George 2377b286dd DEV: rely on default llm model in spec 2025-07-15 09:56:24 -07:00
Kris 67664029e5 FIX: prevent crash in "all" filter on features page (#1505)
* FIX: prevent crash in "all" filter on features page

* simplify
2025-07-15 12:51:46 -04:00
Sam 1b16fc876c FIX: avoid using structured outputs for report runs (#1502)
Structured outputs are prone to formatting issue, especially around newlines
and custom pieces of text that need escaping.

This avoids using it for the automation reporting.

Particularly previous to this fix o4-mini based reports were broken
2025-07-15 13:14:52 +10:00
Discourse Translator Bot 8c069490f0 Update translations (#1498) 2025-07-15 07:28:15 +10:00
Keegan George 8da4318021 DEV: assign_fake_provider 2025-07-11 12:32:36 -07:00
Keegan George b6a88ff03e spec 2025-07-11 12:02:50 -07:00
Keegan George f366ded03b DEV: Force default to be set if it was found not to be set! 2025-07-11 08:19:30 -07:00
Keegan George 87c42145d4 DEV: hide hyde model setting 2025-07-10 15:07:12 -07:00
Keegan George c49f1df2ba DEV: migrate hyde model to content creator persona 2025-07-10 15:00:34 -07:00
Keegan George b46940fc66 DEV: hide reset button
It resets it to default before the migration
2025-07-10 14:46:02 -07:00
Keegan George 249aab100c DEV: Use a simple validator
- no need for seeding checks or module dependant checks
2025-07-10 14:44:02 -07:00
Keegan George 5a29074799 DEV: Use default LLM model 2025-07-10 11:54:35 -07:00
Keegan George 29d415755a fix 2025-07-09 15:42:44 -07:00
Keegan George 5a590fddc1 DEV: updates... 2025-07-09 15:36:34 -07:00
Keegan George cad39c1aa4 DEV: client side 2025-07-09 15:29:50 -07:00
Keegan George 2592be65ff FEATURE: configure a default LLM model for all features
This update adds a setting to the configure a default LLM model to be used for all features (unless overridden).
2025-07-09 15:29:24 -07:00
135 changed files with 879 additions and 581 deletions
-3
View File
@@ -1,8 +1,5 @@
# **Discourse AI** Plugin
> [!IMPORTANT]
> This plugin has now been bundled into Discourse core. See: https://meta.discourse.org/t/bundling-more-popular-plugins-with-discourse-core/373574
**Plugin Summary**
For more information, please see: https://meta.discourse.org/t/discourse-ai/259214?u=falco
-12
View File
@@ -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?
+3 -3
View File
@@ -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: {
+1 -1
View File
@@ -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;
}
}
+3
View File
@@ -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:
+5 -1
View File
@@ -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"
+3 -1
View File
@@ -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}."
+2 -1
View File
@@ -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
View File
@@ -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
+2 -11
View File
@@ -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)
+5 -1
View File
@@ -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>"],
+7 -1
View File
@@ -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
View File
@@ -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
+1 -3
View File
@@ -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
+2
View File
@@ -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
+39 -15
View File
@@ -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

Some files were not shown because too many files have changed in this diff Show More