Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 471f96f

Browse files
authored
FEATURE: allow seeing configured LLM on feature page (#1460)
This is an interim fix so we can at least tell what feature is being used for what LLM. It also adds some test coverage to the feature page.
1 parent 1f851bb commit 471f96f

File tree

9 files changed

+206
-12
lines changed

9 files changed

+206
-12
lines changed

app/controllers/discourse_ai/admin/ai_features_controller.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ def serialize_feature(feature)
3838
{
3939
name: feature.name,
4040
persona: serialize_persona(persona_id_obj_hash[feature.persona_id]),
41+
llm_model: {
42+
id: feature.llm_model&.id,
43+
name: feature.llm_model&.name,
44+
},
4145
enabled: feature.enabled?,
4246
}
4347
end

assets/javascripts/discourse/components/ai-features-list.gjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ const AiFeaturesList = <template>
5959
{{i18n "discourse_ai.features.no_persona"}}
6060
{{/if}}
6161
</div>
62+
<div class="ai-feature-card__llm">
63+
<span>{{i18n "discourse_ai.features.llm"}}</span>
64+
{{#if feature.llm_model.name}}
65+
<DButton
66+
class="btn-flat btn-small ai-feature-card__llm-button"
67+
@translatedLabel={{feature.llm_model.name}}
68+
@route="adminPlugins.show.discourse-ai-llms.edit"
69+
@routeModels={{feature.llm_model.id}}
70+
/>
71+
{{else}}
72+
{{i18n "discourse_ai.features.no_llm"}}
73+
{{/if}}
74+
</div>
6275
{{#if feature.persona}}
6376
<div class="ai-feature-card__groups">
6477
<span>{{i18n "discourse_ai.features.groups"}}</span>

assets/stylesheets/common/ai-features.scss

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
padding: 0.5rem;
2525
display: block;
2626

27+
&__llm,
2728
&__persona,
2829
&__groups {
2930
font-size: var(--font-down-1-rem);
@@ -37,12 +38,18 @@
3738
padding-left: 0;
3839
}
3940

41+
&__groups {
42+
display: flex;
43+
flex-flow: row wrap;
44+
gap: 0.25em;
45+
}
46+
4047
&__item-groups {
4148
list-style: none;
4249
display: flex;
4350
flex-flow: row wrap;
4451
gap: 0.25em;
45-
margin: 0.5em 0;
52+
margin: 0;
4653

4754
li {
4855
font-size: var(--font-down-1);

config/locales/client.en.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ en:
188188
disabled: "(disabled)"
189189
persona: "Persona:"
190190
groups: "Groups:"
191+
llm: "LLM:"
192+
no_llm: "No LLM selected"
191193
no_persona: "Not set"
192194
no_groups: "None"
193195
edit: "Edit"

lib/ai_helper/assistant.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,11 +286,15 @@ def build_bot(helper_mode, user)
286286
DiscourseAi::Personas::Bot.as(user, persona: persona_klass.new, model: llm_model)
287287
end
288288

289+
def find_ai_helper_model(helper_mode, persona_klass)
290+
self.class.find_ai_helper_model(helper_mode, persona_klass)
291+
end
292+
289293
# Priorities are:
290294
# 1. Persona's default LLM
291295
# 2. Hidden `ai_helper_model` setting, or `ai_helper_image_caption_model` for image_caption.
292296
# 3. Newest LLM config
293-
def find_ai_helper_model(helper_mode, persona_klass)
297+
def self.find_ai_helper_model(helper_mode, persona_klass)
294298
model_id = persona_klass.default_llm_id
295299

296300
if !model_id

lib/configuration/feature.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,28 @@ def initialize(name, persona_setting, module_id, module_name, enabled_by_setting
175175
@enabled_by_setting = enabled_by_setting
176176
end
177177

178+
def llm_model
179+
persona = AiPersona.find_by(id: persona_id)
180+
return if persona.blank?
181+
182+
persona_klass = persona.class_instance
183+
184+
llm_model =
185+
case module_name
186+
when DiscourseAi::Configuration::Module::SUMMARIZATION
187+
DiscourseAi::Summarization.find_summarization_model(persona_klass)
188+
when DiscourseAi::Configuration::Module::AI_HELPER
189+
DiscourseAi::AiHelper::Assistant.find_ai_helper_model(name, persona_klass)
190+
when DiscourseAi::Configuration::Module::TRANSLATION
191+
DiscourseAi::Translation::BaseTranslator.preferred_llm_model(persona_klass)
192+
end
193+
194+
if llm_model.blank? && persona.default_llm_id
195+
llm_model = LlmModel.find_by(id: persona.default_llm_id)
196+
end
197+
llm_model
198+
end
199+
178200
attr_reader :name, :persona_setting, :module_id, :module_name
179201

180202
def enabled?

lib/translation/base_translator.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def translate
1919
persona_klass = ai_persona.class_instance
2020
persona = persona_klass.new
2121

22-
model = LlmModel.find_by(id: preferred_llm_model(persona_klass))
22+
model = self.class.preferred_llm_model(persona_klass)
2323
return nil if model.blank?
2424

2525
bot = DiscourseAi::Personas::Bot.as(translation_user, persona:, model:)
@@ -59,8 +59,10 @@ def persona_setting
5959
raise NotImplementedError
6060
end
6161

62-
def preferred_llm_model(persona_klass)
63-
persona_klass.default_llm_id || SiteSetting.ai_translation_model&.split(":")&.last
62+
def self.preferred_llm_model(persona_klass)
63+
id = persona_klass.default_llm_id || SiteSetting.ai_translation_model&.split(":")&.last
64+
return nil if id.blank?
65+
LlmModel.find_by(id:)
6466
end
6567
end
6668
end

lib/translation/language_detector.rb

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def detect
2020
persona_klass = ai_persona.class_instance
2121
persona = persona_klass.new
2222

23-
llm_model = LlmModel.find_by(id: preferred_llm_model(persona_klass))
23+
llm_model = DiscourseAi::Translation::BaseTranslator.preferred_llm_model(persona_klass)
2424
return nil if llm_model.blank?
2525

2626
bot =
@@ -44,12 +44,6 @@ def detect
4444
end
4545
structured_output&.read_buffered_property(:locale) || []
4646
end
47-
48-
private
49-
50-
def preferred_llm_model(persona_klass)
51-
persona_klass.default_llm_id || SiteSetting.ai_translation_model&.split(":")&.last
52-
end
5347
end
5448
end
5549
end

spec/configuration/feature_spec.rb

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# frozen_string_literal: true
2+
3+
require "rails_helper"
4+
5+
RSpec.describe DiscourseAi::Configuration::Feature do
6+
fab!(:llm_model)
7+
fab!(:ai_persona) { Fabricate(:ai_persona, default_llm_id: llm_model.id) }
8+
9+
def allow_configuring_setting(&block)
10+
DiscourseAi::Completions::Llm.with_prepared_responses(["OK"]) { block.call }
11+
end
12+
13+
describe "#llm_model" do
14+
context "when persona is not found" do
15+
it "returns nil when persona_id is invalid" do
16+
ai_feature =
17+
described_class.new(
18+
"topic_summaries",
19+
"ai_summarization_persona",
20+
DiscourseAi::Configuration::Module::SUMMARIZATION_ID,
21+
DiscourseAi::Configuration::Module::SUMMARIZATION,
22+
)
23+
24+
SiteSetting.ai_summarization_persona = 999_999
25+
expect(ai_feature.llm_model).to be_nil
26+
end
27+
end
28+
29+
context "with summarization module" do
30+
let(:ai_feature) do
31+
described_class.new(
32+
"topic_summaries",
33+
"ai_summarization_persona",
34+
DiscourseAi::Configuration::Module::SUMMARIZATION_ID,
35+
DiscourseAi::Configuration::Module::SUMMARIZATION,
36+
)
37+
end
38+
39+
it "returns the configured llm model" do
40+
SiteSetting.ai_summarization_persona = ai_persona.id
41+
allow_configuring_setting { SiteSetting.ai_summarization_model = "custom:#{llm_model.id}" }
42+
expect(ai_feature.llm_model).to eq(llm_model)
43+
end
44+
end
45+
46+
context "with AI helper module" do
47+
let(:ai_feature) do
48+
described_class.new(
49+
"proofread",
50+
"ai_helper_proofreader_persona",
51+
DiscourseAi::Configuration::Module::AI_HELPER_ID,
52+
DiscourseAi::Configuration::Module::AI_HELPER,
53+
)
54+
end
55+
56+
it "returns the persona's default llm when no specific helper model is set" do
57+
SiteSetting.ai_helper_proofreader_persona = ai_persona.id
58+
SiteSetting.ai_helper_model = ""
59+
60+
expect(ai_feature.llm_model).to eq(llm_model)
61+
end
62+
end
63+
64+
context "with translation module" do
65+
fab!(:translation_model) { Fabricate(:llm_model) }
66+
67+
let(:ai_feature) do
68+
described_class.new(
69+
"locale_detector",
70+
"ai_translation_locale_detector_persona",
71+
DiscourseAi::Configuration::Module::TRANSLATION_ID,
72+
DiscourseAi::Configuration::Module::TRANSLATION,
73+
)
74+
end
75+
76+
it "uses translation model when configured" do
77+
SiteSetting.ai_translation_locale_detector_persona = ai_persona.id
78+
ai_persona.update!(default_llm_id: nil)
79+
allow_configuring_setting do
80+
SiteSetting.ai_translation_model = "custom:#{translation_model.id}"
81+
end
82+
83+
expect(ai_feature.llm_model).to eq(translation_model)
84+
end
85+
end
86+
end
87+
88+
describe "#enabled?" do
89+
it "returns true when no enabled_by_setting is specified" do
90+
ai_feature =
91+
described_class.new(
92+
"topic_summaries",
93+
"ai_summarization_persona",
94+
DiscourseAi::Configuration::Module::SUMMARIZATION_ID,
95+
DiscourseAi::Configuration::Module::SUMMARIZATION,
96+
)
97+
98+
expect(ai_feature.enabled?).to be true
99+
end
100+
101+
it "respects the enabled_by_setting when specified" do
102+
ai_feature =
103+
described_class.new(
104+
"gists",
105+
"ai_summary_gists_persona",
106+
DiscourseAi::Configuration::Module::SUMMARIZATION_ID,
107+
DiscourseAi::Configuration::Module::SUMMARIZATION,
108+
enabled_by_setting: "ai_summary_gists_enabled",
109+
)
110+
111+
SiteSetting.ai_summary_gists_enabled = false
112+
expect(ai_feature.enabled?).to be false
113+
114+
SiteSetting.ai_summary_gists_enabled = true
115+
expect(ai_feature.enabled?).to be true
116+
end
117+
end
118+
119+
describe "#persona_id" do
120+
it "returns the persona id from site settings" do
121+
ai_feature =
122+
described_class.new(
123+
"topic_summaries",
124+
"ai_summarization_persona",
125+
DiscourseAi::Configuration::Module::SUMMARIZATION_ID,
126+
DiscourseAi::Configuration::Module::SUMMARIZATION,
127+
)
128+
129+
SiteSetting.ai_summarization_persona = ai_persona.id
130+
expect(ai_feature.persona_id).to eq(ai_persona.id)
131+
end
132+
end
133+
134+
describe ".find_features_using" do
135+
it "returns all features using a specific persona" do
136+
SiteSetting.ai_summarization_persona = ai_persona.id
137+
SiteSetting.ai_helper_proofreader_persona = ai_persona.id
138+
SiteSetting.ai_translation_locale_detector_persona = 999
139+
140+
features = described_class.find_features_using(persona_id: ai_persona.id)
141+
142+
expect(features.map(&:name)).to include("topic_summaries", "proofread")
143+
expect(features.map(&:name)).not_to include("locale_detector")
144+
end
145+
end
146+
end

0 commit comments

Comments
 (0)