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

Commit 0decfdd

Browse files
committed
FEATURE: allow seeing configured LLM on feature page
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 0decfdd

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
@@ -73,6 +73,19 @@ const AiFeaturesList = <template>
7373
{{/if}}
7474
</div>
7575
{{/if}}
76+
<div class="ai-feature-card__llm">
77+
<span>{{i18n "discourse_ai.features.llm"}}</span>
78+
{{#if feature.llm_model.name}}
79+
<DButton
80+
class="btn-flat btn-small ai-feature-card__llm-button"
81+
@translatedLabel={{feature.llm_model.name}}
82+
@route="adminPlugins.show.discourse-ai-llms.edit"
83+
@routeModels={{feature.llm_model.id}}
84+
/>
85+
{{else}}
86+
{{i18n "discourse_ai.features.no_llm"}}
87+
{{/if}}
88+
</div>
7689
</div>
7790
</div>
7891
{{/each}}

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 = 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)