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

Commit a1f859a

Browse files
authored
FEATURE: improve visibility of AI usage in LLM page (#845)
This changeset: 1. Corrects some issues with "force_default_llm" not applying 2. Expands the LLM list page to show LLM usage 3. Clarifies better what "enabling a bot" on an llm means (you get it in the selector)
1 parent 712a07c commit a1f859a

File tree

12 files changed

+197
-78
lines changed

12 files changed

+197
-78
lines changed

admin/assets/javascripts/discourse/routes/admin-plugins-show-discourse-ai-personas-new.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export default DiscourseRoute.extend({
1010
record.set("rag_chunk_tokens", 374);
1111
record.set("rag_chunk_overlap_tokens", 10);
1212
record.set("rag_conversation_chunks", 10);
13+
record.set("allow_personal_messages", true);
14+
record.set("tool_details", false);
1315
return record;
1416
},
1517

app/controllers/discourse_ai/admin/ai_llms_controller.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ def index
1414
llms,
1515
each_serializer: LlmModelSerializer,
1616
root: false,
17+
scope: {
18+
llm_usage: DiscourseAi::Configuration::LlmEnumerator.global_usage,
19+
},
1720
).as_json,
1821
meta: {
1922
provider_params: LlmModel.provider_params,

app/serializers/llm_model_serializer.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,16 @@ class LlmModelSerializer < ApplicationSerializer
2222
has_one :user, serializer: BasicUserSerializer, embed: :object
2323

2424
def used_by
25-
DiscourseAi::Configuration::LlmValidator.new.modules_using(object)
25+
llm_usage =
26+
(
27+
if (scope && scope[:llm_usage])
28+
scope[:llm_usage]
29+
else
30+
DiscourseAi::Configuration::LlmEnumerator.global_usage
31+
end
32+
)
33+
34+
llm_usage[object.id]
2635
end
2736

2837
def api_key

assets/javascripts/discourse/components/ai-llm-editor-form.gjs

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { later } from "@ember/runloop";
99
import { inject as service } from "@ember/service";
1010
import { eq } from "truth-helpers";
1111
import DButton from "discourse/components/d-button";
12-
import DToggleSwitch from "discourse/components/d-toggle-switch";
1312
import Avatar from "discourse/helpers/bound-avatar-template";
1413
import { popupAjaxError } from "discourse/lib/ajax-error";
1514
import icon from "discourse-common/helpers/d-icon";
@@ -59,7 +58,20 @@ export default class AiLlmEditorForm extends Component {
5958
}
6059

6160
get modulesUsingModel() {
62-
return this.args.model.used_by?.join(", ");
61+
const usedBy = this.args.model.used_by?.filter((m) => m.type !== "ai_bot");
62+
63+
if (!usedBy || usedBy.length === 0) {
64+
return null;
65+
}
66+
67+
const localized = usedBy.map((m) => {
68+
return I18n.t(`discourse_ai.llms.usage.${m.type}`, {
69+
persona: m.name,
70+
});
71+
});
72+
73+
// TODO: this is not perfectly localized
74+
return localized.join(", ");
6375
}
6476

6577
get seeded() {
@@ -157,20 +169,6 @@ export default class AiLlmEditorForm extends Component {
157169
});
158170
}
159171

160-
@action
161-
async toggleEnabledChatBot() {
162-
this.args.model.set("enabled_chat_bot", !this.args.model.enabled_chat_bot);
163-
if (!this.args.model.isNew) {
164-
try {
165-
await this.args.model.update({
166-
enabled_chat_bot: this.args.model.enabled_chat_bot,
167-
});
168-
} catch (e) {
169-
popupAjaxError(e);
170-
}
171-
}
172-
}
173-
174172
<template>
175173
{{#if this.seeded}}
176174
<div class="alert alert-info">
@@ -291,12 +289,12 @@ export default class AiLlmEditorForm extends Component {
291289
@content={{I18n.t "discourse_ai.llms.hints.vision_enabled"}}
292290
/>
293291
</div>
294-
<div class="control-group">
295-
<DToggleSwitch
296-
class="ai-llm-editor__enabled-chat-bot"
297-
@state={{@model.enabled_chat_bot}}
298-
@label="discourse_ai.llms.enabled_chat_bot"
299-
{{on "click" this.toggleEnabledChatBot}}
292+
<div class="control-group ai-llm-editor__enabled-chat-bot">
293+
<Input @type="checkbox" @checked={{@model.enabled_chat_bot}} />
294+
<label>{{I18n.t "discourse_ai.llms.enabled_chat_bot"}}</label>
295+
<DTooltip
296+
@icon="question-circle"
297+
@content={{I18n.t "discourse_ai.llms.hints.enabled_chat_bot"}}
300298
/>
301299
</div>
302300
{{#if @model.user}}

assets/javascripts/discourse/components/ai-llms-list-editor.gjs

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import Component from "@glimmer/component";
22
import { concat, fn } from "@ember/helper";
3-
import { on } from "@ember/modifier";
43
import { action } from "@ember/object";
54
import { LinkTo } from "@ember/routing";
65
import { inject as service } from "@ember/service";
76
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
8-
import DToggleSwitch from "discourse/components/d-toggle-switch";
9-
import { popupAjaxError } from "discourse/lib/ajax-error";
107
import icon from "discourse-common/helpers/d-icon";
118
import i18n from "discourse-common/helpers/i18n";
129
import I18n from "discourse-i18n";
@@ -100,18 +97,13 @@ export default class AiLlmsListEditor extends Component {
10097
});
10198
}
10299

103-
@action
104-
async toggleEnabledChatBot(llm) {
105-
const oldValue = llm.enabled_chat_bot;
106-
const newValue = !oldValue;
107-
try {
108-
llm.set("enabled_chat_bot", newValue);
109-
await llm.update({
110-
enabled_chat_bot: newValue,
100+
localizeUsage(usage) {
101+
if (usage.type === "ai_persona") {
102+
return I18n.t("discourse_ai.llms.usage.ai_persona", {
103+
persona: usage.name,
111104
});
112-
} catch (err) {
113-
llm.set("enabled_chat_bot", oldValue);
114-
popupAjaxError(err);
105+
} else {
106+
return I18n.t("discourse_ai.llms.usage." + usage.type);
115107
}
116108
}
117109

@@ -138,7 +130,6 @@ export default class AiLlmsListEditor extends Component {
138130
<tr>
139131
<th>{{i18n "discourse_ai.llms.display_name"}}</th>
140132
<th>{{i18n "discourse_ai.llms.provider"}}</th>
141-
<th>{{i18n "discourse_ai.llms.enabled_chat_bot"}}</th>
142133
<th></th>
143134
</tr>
144135
</thead>
@@ -150,18 +141,19 @@ export default class AiLlmsListEditor extends Component {
150141
<p>
151142
{{this.modelDescription llm}}
152143
</p>
144+
{{#if llm.used_by}}
145+
<ul class="ai-llm-list-editor__usages">
146+
{{#each llm.used_by as |usage|}}
147+
<li>{{this.localizeUsage usage}}</li>
148+
{{/each}}
149+
</ul>
150+
{{/if}}
153151
</td>
154152
<td>
155153
{{i18n
156154
(concat "discourse_ai.llms.providers." llm.provider)
157155
}}
158156
</td>
159-
<td>
160-
<DToggleSwitch
161-
@state={{llm.enabled_chat_bot}}
162-
{{on "click" (fn this.toggleEnabledChatBot llm)}}
163-
/>
164-
</td>
165157
<td class="column-edit">
166158
<LinkTo
167159
@route="adminPlugins.show.discourse-ai-llms.show"

assets/javascripts/discourse/components/ai-persona-editor.gjs

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -336,27 +336,29 @@ export default class PersonaEditor extends Component {
336336
disabled={{this.editingModel.system}}
337337
/>
338338
</div>
339-
<div class="control-group">
340-
<label>{{I18n.t "discourse_ai.ai_persona.default_llm"}}</label>
341-
<AiLlmSelector
342-
class="ai-persona-editor__llms"
343-
@value={{this.mappedDefaultLlm}}
344-
@llms={{@personas.resultSetMeta.llms}}
345-
/>
346-
<DTooltip
347-
@icon="question-circle"
348-
@content={{I18n.t "discourse_ai.ai_persona.default_llm_help"}}
349-
/>
350-
</div>
351-
{{#if this.hasDefaultLlm}}
339+
{{#if this.editingModel.user}}
352340
<div class="control-group">
353-
<label>
354-
<Input
355-
@type="checkbox"
356-
@checked={{this.editingModel.force_default_llm}}
357-
/>
358-
{{I18n.t "discourse_ai.ai_persona.force_default_llm"}}</label>
341+
<label>{{I18n.t "discourse_ai.ai_persona.default_llm"}}</label>
342+
<AiLlmSelector
343+
class="ai-persona-editor__llms"
344+
@value={{this.mappedDefaultLlm}}
345+
@llms={{@personas.resultSetMeta.llms}}
346+
/>
347+
<DTooltip
348+
@icon="question-circle"
349+
@content={{I18n.t "discourse_ai.ai_persona.default_llm_help"}}
350+
/>
359351
</div>
352+
{{#if this.hasDefaultLlm}}
353+
<div class="control-group">
354+
<label>
355+
<Input
356+
@type="checkbox"
357+
@checked={{this.editingModel.force_default_llm}}
358+
/>
359+
{{I18n.t "discourse_ai.ai_persona.force_default_llm"}}</label>
360+
</div>
361+
{{/if}}
360362
{{/if}}
361363
{{#unless @model.isNew}}
362364
<div class="control-group">

assets/javascripts/discourse/connectors/composer-fields/persona-llm-selector.gjs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,13 @@ export default class BotSelector extends Component {
5858
this.setAllowLLMSelector();
5959

6060
let llm = this.preferredLlmStore.getObject("id");
61-
llm = llm || this.llmOptions[0].id;
61+
62+
const llmOption =
63+
this.llmOptions.find((innerLlmOption) => innerLlmOption.id === llm) ||
64+
this.llmOptions[0];
65+
66+
llm = llmOption.id;
67+
6268
if (llm) {
6369
next(() => {
6470
this.currentLlm = llm;
@@ -96,6 +102,7 @@ export default class BotSelector extends Component {
96102
this.preferredPersonaStore.setObject({ key: "id", value: newValue });
97103
this.composer.metaData = { ai_persona_id: newValue };
98104
this.setAllowLLMSelector();
105+
this.resetTargetRecipients();
99106
}
100107

101108
setAllowLLMSelector() {
@@ -112,11 +119,16 @@ export default class BotSelector extends Component {
112119

113120
set currentLlm(newValue) {
114121
this.llm = newValue;
115-
const botUsername = this.currentUser.ai_enabled_chat_bots.find(
116-
(bot) => bot.model_name === this.llm
117-
).username;
118122
this.preferredLlmStore.setObject({ key: "id", value: newValue });
123+
124+
this.resetTargetRecipients();
125+
}
126+
127+
resetTargetRecipients() {
119128
if (this.allowLLMSelector) {
129+
const botUsername = this.currentUser.ai_enabled_chat_bots.find(
130+
(bot) => bot.model_name === this.llm
131+
).username;
120132
this.composer.set("targetRecipients", botUsername);
121133
} else {
122134
const persona = this.currentUser.ai_enabled_personas.find(

assets/stylesheets/modules/llms/common/ai-llms-editor.scss

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@
5151
align-items: center;
5252
}
5353

54-
&__vision-enabled {
54+
&__vision-enabled,
55+
&__enabled-chat-bot {
5556
display: flex;
5657
align-items: flex-start;
5758
}
@@ -150,3 +151,17 @@
150151
letter-spacing: 0.1px;
151152
}
152153
}
154+
155+
.ai-llm-list-editor__usages {
156+
list-style: none;
157+
margin: 0.5em 0 0 0;
158+
display: flex;
159+
li {
160+
font-size: var(--font-down-2);
161+
border-radius: 0.25em;
162+
background: var(--primary-very-low);
163+
border: 1px solid var(--primary-low);
164+
padding: 1px 3px;
165+
margin-right: 0.5em;
166+
}
167+
}

config/locales/client.en.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ en:
252252
max_prompt_tokens: "Number of tokens for the prompt"
253253
url: "URL of the service hosting the model"
254254
api_key: "API Key of the service hosting the model"
255-
enabled_chat_bot: "Allow AI Bot"
255+
enabled_chat_bot: "Allow AI Bot selector"
256256
vision_enabled: "Vision enabled"
257257
ai_bot_user: "AI Bot User"
258258
save: "Save"
@@ -262,9 +262,14 @@ en:
262262
confirm_delete: Are you sure you want to delete this model?
263263
delete: Delete
264264
seeded_warning: "This model is pre-configured on your site and cannot be edited."
265+
usage:
266+
ai_bot: "AI Bot"
267+
ai_helper: "AI Helper"
268+
ai_persona: "Persona (%{persona})"
269+
ai_summarization: "Summarization"
265270
in_use_warning:
266-
one: "This model is currently used by the %{settings} setting. If misconfigured, the feature won't work as expected."
267-
other: "This model is currently used by the following settings: %{settings}. If misconfigured, features won't work as expected. "
271+
one: "This model is currently used by %{settings}. If misconfigured, the feature won't work as expected."
272+
other: "This model is currently used by the following: %{settings}. If misconfigured, features won't work as expected. "
268273

269274
model_description:
270275
none: "General settings that work for most language models"
@@ -299,7 +304,7 @@ en:
299304
max_prompt_tokens: "Max numbers of tokens for the prompt. As a rule of thumb, this should be 50% of the model's context window."
300305
name: "We include this in the API call to specify which model we'll use."
301306
vision_enabled: "If enabled, the AI will attempt to understand images. It depends on the model being used supporting vision. Supported by latest models from Anthropic, Google, and OpenAI."
302-
307+
enabled_chat_bot: "If enabled, users can select this model when creating PMs with the AI bot."
303308
providers:
304309
aws_bedrock: "AWS Bedrock"
305310
anthropic: "Anthropic"

lib/configuration/llm_enumerator.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,42 @@
55
module DiscourseAi
66
module Configuration
77
class LlmEnumerator < ::EnumSiteSetting
8+
def self.global_usage
9+
rval = Hash.new { |h, k| h[k] = [] }
10+
11+
if SiteSetting.ai_bot_enabled
12+
LlmModel
13+
.where("enabled_chat_bot = ?", true)
14+
.pluck(:id)
15+
.each { |llm_id| rval[llm_id] << { type: :ai_bot } }
16+
17+
AiPersona
18+
.where("force_default_llm = ?", true)
19+
.pluck(:default_llm, :name, :id)
20+
.each do |llm_name, name, id|
21+
llm_id = llm_name.split(":").last.to_i
22+
rval[llm_id] << { type: :ai_persona, name: name, id: id }
23+
end
24+
end
25+
26+
if SiteSetting.ai_helper_enabled
27+
model_id = SiteSetting.ai_helper_model.split(":").last.to_i
28+
rval[model_id] << { type: :ai_helper }
29+
end
30+
31+
if SiteSetting.ai_summarization_enabled
32+
model_id = SiteSetting.ai_summarization_model.split(":").last.to_i
33+
rval[model_id] << { type: :ai_summarization }
34+
end
35+
36+
if SiteSetting.ai_embeddings_semantic_search_enabled
37+
model_id = SiteSetting.ai_embeddings_semantic_search_hyde_model.split(":").last.to_i
38+
rval[model_id] << { type: :ai_embeddings_semantic_search }
39+
end
40+
41+
rval
42+
end
43+
844
def self.valid_value?(val)
945
true
1046
end

0 commit comments

Comments
 (0)