diff --git a/assets/javascripts/discourse/admin/models/ai-persona.js b/assets/javascripts/discourse/admin/models/ai-persona.js
index 18c97a7d4..98ef84ec0 100644
--- a/assets/javascripts/discourse/admin/models/ai-persona.js
+++ b/assets/javascripts/discourse/admin/models/ai-persona.js
@@ -1,4 +1,3 @@
-import { tracked } from "@glimmer/tracking";
import { ajax } from "discourse/lib/ajax";
import RestModel from "discourse/models/rest";
@@ -63,40 +62,7 @@ const SYSTEM_ATTRIBUTES = [
"allow_chat_direct_messages",
];
-class ToolOption {
- @tracked value = null;
-}
-
export default class AiPersona extends RestModel {
- // this code is here to convert the wire schema to easier to work with object
- // on the wire we pass in/out tools as an Array.
- // [[ToolName, {option1: value, option2: value}, force], ToolName2, ToolName3]
- // So we rework this into a "tools" property and nested toolOptions
- init(properties) {
- this.forcedTools = [];
- if (properties.tools) {
- properties.tools = properties.tools.map((tool) => {
- if (typeof tool === "string") {
- return tool;
- } else {
- let [toolId, options, force] = tool;
- for (let optionId in options) {
- if (!options.hasOwnProperty(optionId)) {
- continue;
- }
- this.getToolOption(toolId, optionId).value = options[optionId];
- }
- if (force) {
- this.forcedTools.push(toolId);
- }
- return toolId;
- }
- });
- }
- super.init(properties);
- this.tools = properties.tools;
- }
-
async createUser() {
const result = await ajax(
`/admin/plugins/discourse-ai/ai-personas/${this.id}/create-user.json`,
@@ -109,63 +75,79 @@ export default class AiPersona extends RestModel {
return this.user;
}
- getToolOption(toolId, optionId) {
- this.toolOptions ||= {};
- this.toolOptions[toolId] ||= {};
- return (this.toolOptions[toolId][optionId] ||= new ToolOption());
+ flattenedToolStructure(data) {
+ return data.tools.map((tName) => {
+ return [tName, data.toolOptions[tName], data.forcedTools.includes(tName)];
+ });
}
- populateToolOptions(attrs) {
- if (!attrs.tools) {
- return;
- }
- let toolsWithOptions = [];
- attrs.tools.forEach((toolId) => {
- if (typeof toolId !== "string") {
- toolId = toolId[0];
- }
+ // this code is here to convert the wire schema to easier to work with object
+ // on the wire we pass in/out tools as an Array.
+ // [[ToolName, {option1: value, option2: value}, force], ToolName2, ToolName3]
+ // We split it into tools, options and a list of forced ones.
+ populateTools(attrs) {
+ const forcedTools = [];
+ const toolOptions = {};
+
+ const flatTools = attrs.tools?.map((tool) => {
+ if (typeof tool === "string") {
+ return tool;
+ } else {
+ let [toolId, options, force] = tool;
+ const mappedOptions = {};
- let force = this.forcedTools.includes(toolId);
- if (this.toolOptions && this.toolOptions[toolId]) {
- let options = this.toolOptions[toolId];
- let optionsWithValues = {};
- for (let optionId in options) {
+ for (const optionId in options) {
if (!options.hasOwnProperty(optionId)) {
continue;
}
- let option = options[optionId];
- optionsWithValues[optionId] = option.value;
+
+ mappedOptions[optionId] = options[optionId];
}
- toolsWithOptions.push([toolId, optionsWithValues, force]);
- } else {
- toolsWithOptions.push([toolId, {}, force]);
+
+ if (Object.keys(mappedOptions).length > 0) {
+ toolOptions[toolId] = mappedOptions;
+ }
+
+ if (force) {
+ forcedTools.push(toolId);
+ }
+
+ return toolId;
}
});
- attrs.tools = toolsWithOptions;
+
+ attrs.tools = flatTools;
+ attrs.forcedTools = forcedTools;
+ attrs.toolOptions = toolOptions;
}
updateProperties() {
- let attrs = this.system
+ const attrs = this.system
? this.getProperties(SYSTEM_ATTRIBUTES)
: this.getProperties(CREATE_ATTRIBUTES);
attrs.id = this.id;
- this.populateToolOptions(attrs);
+
return attrs;
}
createProperties() {
- let attrs = this.getProperties(CREATE_ATTRIBUTES);
- this.populateToolOptions(attrs);
- return attrs;
+ return this.getProperties(CREATE_ATTRIBUTES);
}
- workingCopy() {
- let attrs = this.getProperties(CREATE_ATTRIBUTES);
- this.populateToolOptions(attrs);
+ fromPOJO(data) {
+ const dataClone = JSON.parse(JSON.stringify(data));
+
+ const persona = AiPersona.create(dataClone);
+ persona.tools = this.flattenedToolStructure(dataClone);
- const persona = AiPersona.create(attrs);
- persona.forcedTools = (this.forcedTools || []).slice();
- persona.forced_tool_count = this.forced_tool_count || -1;
return persona;
}
+
+ toPOJO() {
+ const attrs = this.getProperties(CREATE_ATTRIBUTES);
+ this.populateTools(attrs);
+ attrs.forced_tool_count = this.forced_tool_count || -1;
+
+ return attrs;
+ }
}
diff --git a/assets/javascripts/discourse/components/ai-forced-tool-strategy-selector.gjs b/assets/javascripts/discourse/components/ai-forced-tool-strategy-selector.gjs
deleted file mode 100644
index 3c60c76f2..000000000
--- a/assets/javascripts/discourse/components/ai-forced-tool-strategy-selector.gjs
+++ /dev/null
@@ -1,29 +0,0 @@
-import { computed } from "@ember/object";
-import { i18n } from "discourse-i18n";
-import ComboBox from "select-kit/components/combo-box";
-
-export default ComboBox.extend({
- content: computed(function () {
- const content = [
- {
- id: -1,
- name: i18n("discourse_ai.ai_persona.tool_strategies.all"),
- },
- ];
-
- [1, 2, 5].forEach((i) => {
- content.push({
- id: i,
- name: i18n("discourse_ai.ai_persona.tool_strategies.replies", {
- count: i,
- }),
- });
- });
-
- return content;
- }),
-
- selectKitOptions: {
- filterable: false,
- },
-});
diff --git a/assets/javascripts/discourse/components/ai-llm-selector.gjs b/assets/javascripts/discourse/components/ai-llm-selector.gjs
new file mode 100644
index 000000000..f5d9d95e4
--- /dev/null
+++ b/assets/javascripts/discourse/components/ai-llm-selector.gjs
@@ -0,0 +1,17 @@
+import { hash } from "@ember/helper";
+import ComboBox from "select-kit/components/combo-box";
+
+const AiLlmSelector =
+
+;
+
+export default AiLlmSelector;
diff --git a/assets/javascripts/discourse/components/ai-llm-selector.js b/assets/javascripts/discourse/components/ai-llm-selector.js
deleted file mode 100644
index bfd6f9c07..000000000
--- a/assets/javascripts/discourse/components/ai-llm-selector.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { computed } from "@ember/object";
-import { observes } from "@ember-decorators/object";
-import { i18n } from "discourse-i18n";
-import ComboBox from "select-kit/components/combo-box";
-import { selectKitOptions } from "select-kit/components/select-kit";
-
-@selectKitOptions({
- filterable: true,
-})
-export default class AiLlmSelector extends ComboBox {
- @observes("attrs.disabled")
- _modelDisabledChanged() {
- this.selectKit.options.set("disabled", this.get("attrs.disabled.value"));
- }
-
- @computed
- get content() {
- const blankName =
- this.attrs.blankName || i18n("discourse_ai.ai_persona.no_llm_selected");
- return [
- {
- id: "blank",
- name: blankName,
- },
- ].concat(this.llms);
- }
-}
diff --git a/assets/javascripts/discourse/components/ai-persona-editor.gjs b/assets/javascripts/discourse/components/ai-persona-editor.gjs
index eb69f77d6..f8c303d66 100644
--- a/assets/javascripts/discourse/components/ai-persona-editor.gjs
+++ b/assets/javascripts/discourse/components/ai-persona-editor.gjs
@@ -1,30 +1,24 @@
import Component from "@glimmer/component";
import { cached, tracked } from "@glimmer/tracking";
-import { Input } from "@ember/component";
-import { on } from "@ember/modifier";
+import { fn } from "@ember/helper";
import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
-import didUpdate from "@ember/render-modifiers/modifiers/did-update";
import { LinkTo } from "@ember/routing";
import { later } from "@ember/runloop";
import { service } from "@ember/service";
+import { gt, or } from "truth-helpers";
import BackButton from "discourse/components/back-button";
-import DButton from "discourse/components/d-button";
-import Textarea from "discourse/components/d-textarea";
-import DToggleSwitch from "discourse/components/d-toggle-switch";
+import Form from "discourse/components/form";
import Avatar from "discourse/helpers/bound-avatar-template";
import { popupAjaxError } from "discourse/lib/ajax-error";
import Group from "discourse/models/group";
import { i18n } from "discourse-i18n";
import AdminUser from "admin/models/admin-user";
-import ComboBox from "select-kit/components/combo-box";
import GroupChooser from "select-kit/components/group-chooser";
-import DTooltip from "float-kit/components/d-tooltip";
-import AiForcedToolStrategySelector from "./ai-forced-tool-strategy-selector";
import AiLlmSelector from "./ai-llm-selector";
import AiPersonaToolOptions from "./ai-persona-tool-options";
import AiToolSelector from "./ai-tool-selector";
-import RagOptions from "./rag-options";
+import RagOptionsFk from "./rag-options-fk";
import RagUploader from "./rag-uploader";
export default class PersonaEditor extends Component {
@@ -36,84 +30,55 @@ export default class PersonaEditor extends Component {
@tracked allGroups = [];
@tracked isSaving = false;
- @tracked editingModel = null;
- @tracked showDelete = false;
- @tracked maxPixelsValue = null;
- @tracked ragIndexingStatuses = null;
+ @tracked availableForcedTools = [];
- @tracked selectedTools = [];
- @tracked selectedToolNames = [];
- @tracked forcedToolNames = [];
- @tracked hasDefaultLlm = false;
-
- get chatPluginEnabled() {
- return this.siteSettings.chat_enabled;
- }
-
- get allowForceTools() {
- return !this.editingModel?.system && this.selectedToolNames.length > 0;
- }
-
- get hasForcedTools() {
- return this.forcedToolNames.length > 0;
- }
-
- @action
- forcedToolsChanged(tools) {
- this.forcedToolNames = tools;
- this.editingModel.forcedTools = this.forcedToolNames;
- }
-
- @action
- toolsChanged(tools) {
- this.selectedTools = this.args.personas.resultSetMeta.tools.filter((tool) =>
- tools.includes(tool.id)
- );
- this.selectedToolNames = tools.slice();
+ @cached
+ get formData() {
+ const data = this.args.model.toPOJO();
- this.forcedToolNames = this.forcedToolNames.filter(
- (tool) => this.editingModel.tools.indexOf(tool) !== -1
- );
+ if (data.tools) {
+ data.toolOptions = this.mapToolOptions(data.toolOptions, data.tools);
+ }
- this.editingModel.tools = this.selectedToolNames;
- this.editingModel.forcedTools = this.forcedToolNames;
+ return data;
}
- @action
- updateModel() {
- this.editingModel = this.args.model.workingCopy();
- this.hasDefaultLlm = !!this.editingModel.default_llm;
- this.showDelete = !this.args.model.isNew && !this.args.model.system;
- this.maxPixelsValue = this.findClosestPixelValue(
- this.editingModel.vision_max_pixels
- );
-
- this.selectedToolNames = this.editingModel.tools || [];
- this.selectedTools = this.args.personas.resultSetMeta.tools.filter((tool) =>
- this.selectedToolNames.includes(tool.id)
- );
- this.forcedToolNames = this.editingModel.forcedTools || [];
+ get chatPluginEnabled() {
+ return this.siteSettings.chat_enabled;
}
- findClosestPixelValue(pixels) {
- let value = "high";
- this.maxPixelValues.forEach((info) => {
- if (pixels === info.pixels) {
- value = info.id;
- }
- });
- return value;
+ get allTools() {
+ return this.args.personas.resultSetMeta.tools;
}
- @cached
get maxPixelValues() {
const l = (key) =>
i18n(`discourse_ai.ai_persona.vision_max_pixel_sizes.${key}`);
return [
- { id: "low", name: l("low"), pixels: 65536 },
- { id: "medium", name: l("medium"), pixels: 262144 },
- { id: "high", name: l("high"), pixels: 1048576 },
+ { name: l("low"), id: 65536 },
+ { name: l("medium"), id: 262144 },
+ { name: l("high"), id: 1048576 },
+ ];
+ }
+
+ get forcedToolStrategies() {
+ const content = [
+ {
+ id: -1,
+ name: i18n("discourse_ai.ai_persona.tool_strategies.all"),
+ },
];
+
+ [1, 2, 5].forEach((i) => {
+ content.push({
+ id: i,
+ name: i18n("discourse_ai.ai_persona.tool_strategies.replies", {
+ count: i,
+ }),
+ });
+ });
+
+ return content;
}
@action
@@ -122,21 +87,24 @@ export default class PersonaEditor extends Component {
}
@action
- async save() {
+ async save(data) {
const isNew = this.args.model.isNew;
this.isSaving = true;
- const backupModel = this.args.model.workingCopy();
-
- this.args.model.setProperties(this.editingModel);
try {
- await this.args.model.save();
+ const personaToSave = Object.assign(
+ this.args.model,
+ this.args.model.fromPOJO(data)
+ );
+
+ await personaToSave.save();
this.#sortPersonas();
+
if (isNew && this.args.model.rag_uploads.length === 0) {
- this.args.personas.addObject(this.args.model);
+ this.args.personas.addObject(personaToSave);
this.router.transitionTo(
"adminPlugins.show.discourse-ai-personas.edit",
- this.args.model
+ personaToSave
);
} else {
this.toasts.success({
@@ -145,7 +113,6 @@ export default class PersonaEditor extends Component {
});
}
} catch (e) {
- this.args.model.setProperties(backupModel);
popupAjaxError(e);
} finally {
later(() => {
@@ -154,52 +121,11 @@ export default class PersonaEditor extends Component {
}
}
- get showTemperature() {
- return this.editingModel?.temperature || !this.editingModel?.system;
- }
-
- get showTopP() {
- return this.editingModel?.top_p || !this.editingModel?.system;
- }
-
get adminUser() {
- return AdminUser.create(this.editingModel?.user);
- }
+ // Work around user not being extensible.
+ const userClone = Object.assign({}, this.args.model?.user);
- get mappedQuestionConsolidatorLlm() {
- return this.editingModel?.question_consolidator_llm_id ?? "blank";
- }
-
- set mappedQuestionConsolidatorLlm(value) {
- if (value === "blank") {
- this.editingModel.question_consolidator_llm_id = null;
- } else {
- this.editingModel.question_consolidator_llm_id = value;
- }
- }
-
- get mappedDefaultLlm() {
- return this.editingModel?.default_llm_id ?? "blank";
- }
-
- set mappedDefaultLlm(value) {
- if (value === "blank") {
- this.editingModel.default_llm_id = null;
- this.hasDefaultLlm = false;
- } else {
- this.editingModel.default_llm_id = value;
- this.hasDefaultLlm = true;
- }
- }
-
- @action
- onChangeMaxPixels(value) {
- const entry = this.maxPixelValues.findBy("id", value);
- if (!entry) {
- return;
- }
- this.maxPixelsValue = value;
- this.editingModel.vision_max_pixels = entry.pixels;
+ return AdminUser.create(userClone);
}
@action
@@ -218,51 +144,101 @@ export default class PersonaEditor extends Component {
}
@action
- updateAllowedGroups(ids) {
- this.editingModel.set("allowed_group_ids", ids);
- }
-
- @action
- async toggleEnabled() {
- await this.toggleField("enabled");
+ async toggleEnabled(value, { set }) {
+ set("enabled", value);
+ await this.persistField("enabled", value);
}
@action
- async togglePriority() {
- await this.toggleField("priority", true);
+ async togglePriority(value, { set }) {
+ set("priority", value);
+ await this.persistField("priority", value, true);
}
@action
- async createUser() {
+ async createUser(form) {
try {
let user = await this.args.model.createUser();
- this.editingModel.set("user", user);
- this.editingModel.set("user_id", user.id);
+ form.set("user", user);
+ form.set("user_id", user.id);
} catch (e) {
popupAjaxError(e);
}
}
@action
- updateUploads(uploads) {
- this.editingModel.rag_uploads = uploads;
+ updateUploads(form, newUploads) {
+ form.set("rag_uploads", newUploads);
}
@action
- removeUpload(upload) {
- this.editingModel.rag_uploads.removeObject(upload);
+ async removeUpload(form, currentUploads, upload) {
+ const updatedUploads = currentUploads.filter(
+ (file) => file.id !== upload.id
+ );
+
+ form.set("rag_uploads", updatedUploads);
+
if (!this.args.model.isNew) {
- this.save();
+ await this.persistField("rag_uploads", updatedUploads);
}
}
- async toggleField(field, sortPersonas) {
- this.args.model.set(field, !this.args.model[field]);
- this.editingModel.set(field, this.args.model[field]);
+ @action
+ updateToolNames(form, currentData, updatedTools) {
+ const removedTools =
+ currentData?.tools?.filter((ct) => !updatedTools.includes(ct)) || [];
+ const updatedOptions = this.mapToolOptions(
+ currentData.toolOptions,
+ updatedTools
+ );
+
+ form.setProperties({
+ tools: updatedTools,
+ toolOptions: updatedOptions,
+ });
+
+ this.availableForcedTools = this.allTools.filter((tool) =>
+ updatedTools.includes(tool.id)
+ );
+
+ if (currentData.forcedTools?.length > 0) {
+ const updatedForcedTools = currentData.forcedTools.filter(
+ (fct) => !removedTools.includes(fct)
+ );
+ form.set("forcedTools", updatedForcedTools);
+ }
+ }
+
+ mapToolOptions(currentOptions, toolNames) {
+ const updatedOptions = Object.assign({}, currentOptions);
+
+ toolNames.forEach((toolId) => {
+ const tool = this.allTools.findBy("id", toolId);
+ const toolOptions = tool?.options;
+
+ if (!toolOptions || updatedOptions[toolId]) {
+ return;
+ }
+
+ const mappedOptions = {};
+ Object.keys(toolOptions).forEach((key) => {
+ mappedOptions[key] = null;
+ });
+
+ updatedOptions[toolId] = mappedOptions;
+ });
+
+ return updatedOptions;
+ }
+
+ async persistField(field, newValue, sortPersonas) {
+ this.args.model.set(field, newValue);
+
if (!this.args.model.isNew) {
try {
const args = {};
- args[field] = this.args.model[field];
+ args[field] = newValue;
await this.args.model.update(args);
if (sortPersonas) {
@@ -293,369 +269,401 @@ export default class PersonaEditor extends Component {
@route="adminPlugins.show.discourse-ai-personas"
@label="discourse_ai.ai_persona.back"
/>
-
+
+
+
+ {{#if data.vision_enabled}}
+
+
+ {{#each this.maxPixelValues as |pixelValue|}}
+ {{pixelValue.name}}
+ {{/each}}
+
+
{{/if}}
- {{/if}}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{#if this.editingModel.user}}
-
-
-
-
- {{#if this.chatPluginEnabled}}
-
+
+
+
+ {{#unless data.system}}
+
-
-
-
-
+
+
+
+
+
+ {{/unless}}
+
+
+
-
-
-
- {{/if}}
- {{/if}}
-
-
-
-
-
-
-
-
- {{#if this.editingModel.vision_enabled}}
-
-
-
-
- {{/if}}
-
-
-
-
-
- {{#if this.showTemperature}}
-
-
-
-
-
- {{/if}}
- {{#if this.showTopP}}
-
-
-
-
-
- {{/if}}
- {{#if this.siteSettings.ai_embeddings_enabled}}
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ {{#if (gt data.tools.length 0)}}
+
+
+
+
+
+ {{/if}}
+
+ {{#if (gt data.forcedTools.length 0)}}
+
+
+ {{#each this.forcedToolStrategies as |fts|}}
+ {{fts.name}}
+ {{/each}}
+
+
+ {{/if}}
+
+ {{#if (gt data.tools.length 0)}}
+
+
+
+
+
+ {{/if}}
+
+
+ {{#if this.siteSettings.ai_embeddings_enabled}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{/if}}
-
+ {{#if @model.isNew}}
+ {{i18n "discourse_ai.ai_persona.ai_bot.save_first"}}
+ {{else}}
+ {{#if data.default_llm_id}}
+
+
+
+ {{/if}}
+
+
+ {{#if data.user}}
+
+ {{Avatar data.user.avatar_template "small"}}
+
+
+ {{data.user.username}}
+
+ {{else}}
+
+ {{/if}}
+
+
+ {{#if data.user}}
+
+
+
+
+
+
+
+
+ {{#if this.chatPluginEnabled}}
+
+
+
+
+
+
+
+ {{/if}}
+ {{/if}}
+ {{/if}}
+
+
+
+
+
+ {{#unless (or @model.isNew @model.system)}}
+
-
-
- {{/if}}
-
- {{i18n "discourse_ai.ai_persona.save"}}
- {{#if this.showDelete}}
-
- {{i18n "discourse_ai.ai_persona.delete"}}
-
- {{/if}}
-
-
+ {{/unless}}
+
+
+
}
diff --git a/assets/javascripts/discourse/components/ai-persona-tool-option-editor.gjs b/assets/javascripts/discourse/components/ai-persona-tool-option-editor.gjs
deleted file mode 100644
index b03fe013f..000000000
--- a/assets/javascripts/discourse/components/ai-persona-tool-option-editor.gjs
+++ /dev/null
@@ -1,91 +0,0 @@
-import Component from "@glimmer/component";
-import { Input } from "@ember/component";
-import { on } from "@ember/modifier";
-import { action } from "@ember/object";
-import { eq } from "truth-helpers";
-import { i18n } from "discourse-i18n";
-import AiLlmSelector from "./ai-llm-selector";
-
-export default class AiPersonaToolOptionEditor extends Component {
- get isBoolean() {
- return this.args.option.type === "boolean";
- }
-
- get isEnum() {
- return this.args.option.type === "enum";
- }
-
- get isLlm() {
- return this.args.option.type === "llm";
- }
-
- get selectedValue() {
- return this.args.option.value.value === "true";
- }
-
- get selectedLlm() {
- if (this.args.option.value.value) {
- return `custom:${this.args.option.value.value}`;
- } else {
- return "blank";
- }
- }
-
- set selectedLlm(value) {
- if (value === "blank") {
- this.args.option.value.value = null;
- } else {
- this.args.option.value.value = value.replace("custom:", "");
- }
- }
-
- @action
- onCheckboxChange(event) {
- this.args.option.value.value = event.target.checked ? "true" : "false";
- }
-
- @action
- onSelectOption(event) {
- this.args.option.value.value = event.target.value;
- }
-
-
-
-
-}
diff --git a/assets/javascripts/discourse/components/ai-persona-tool-options.gjs b/assets/javascripts/discourse/components/ai-persona-tool-options.gjs
index 1a1df8179..1c1a49176 100644
--- a/assets/javascripts/discourse/components/ai-persona-tool-options.gjs
+++ b/assets/javascripts/discourse/components/ai-persona-tool-options.gjs
@@ -1,82 +1,95 @@
import Component from "@glimmer/component";
+import { action, get } from "@ember/object";
+import { eq } from "truth-helpers";
import { i18n } from "discourse-i18n";
-import AiPersonaToolOptionEditor from "./ai-persona-tool-option-editor";
+import AiLlmSelector from "./ai-llm-selector";
export default class AiPersonaToolOptions extends Component {
get showToolOptions() {
const allTools = this.args.allTools;
- if (!allTools) {
+ if (!allTools || !this.args.data.tools) {
return false;
}
-
- return this.toolNames.any((tool) => allTools.findBy("id", tool)?.options);
+ return this.args.data?.tools.any(
+ (tool) => allTools.findBy("id", tool)?.options
+ );
}
- get toolNames() {
- if (!this.args.tools) {
- return [];
- }
- return this.args.tools.map((tool) => {
- if (typeof tool === "string") {
- return tool;
- } else {
- return tool[0];
- }
- });
- }
-
- get toolOptions() {
- if (!this.args.tools) {
- return [];
- }
-
- const allTools = this.args.allTools;
- if (!allTools) {
- return [];
- }
-
- const options = [];
- this.toolNames.forEach((toolId) => {
- const tool = allTools.findBy("id", toolId);
-
- const toolName = tool?.name;
- const toolOptions = tool?.options;
+ get toolsMetadata() {
+ const metatada = {};
- if (toolOptions) {
- const mappedOptions = Object.keys(toolOptions).map((key) => {
- const value = this.args.persona.getToolOption(toolId, key);
- return Object.assign({}, toolOptions[key], { id: key, value });
- });
-
- options.push({ toolName, options: mappedOptions });
- }
+ this.args.allTools.map((t) => {
+ metatada[t.id] = {
+ name: t.name,
+ ...t?.options,
+ };
});
- return options;
+ return metatada;
+ }
+
+ @action
+ formObjectKeys(toolOptions) {
+ return Object.keys(toolOptions);
}
{{#if this.showToolOptions}}
-
-
-
- {{#each this.toolOptions as |toolOption|}}
+ <@form.Container
+ @title={{i18n "discourse_ai.ai_persona.tool_options"}}
+ @direction="column"
+ @format="full"
+ >
+ <@form.Object
+ @name="toolOptions"
+ @title={{i18n "discourse_ai.ai_persona.tool_options"}}
+ as |toolObj optsPerTool|
+ >
+ {{#each (this.formObjectKeys optsPerTool) as |toolId|}}
{{/each}}
-
-
+ @form.Object>
+ @form.Container>
{{/if}}
}
diff --git a/assets/javascripts/discourse/components/ai-tool-selector.gjs b/assets/javascripts/discourse/components/ai-tool-selector.gjs
new file mode 100644
index 000000000..c4509d1b4
--- /dev/null
+++ b/assets/javascripts/discourse/components/ai-tool-selector.gjs
@@ -0,0 +1,13 @@
+import { hash } from "@ember/helper";
+import MultiSelect from "select-kit/components/multi-select";
+
+const AiToolSelector =
+
+;
+
+export default AiToolSelector;
diff --git a/assets/javascripts/discourse/components/ai-tool-selector.js b/assets/javascripts/discourse/components/ai-tool-selector.js
deleted file mode 100644
index db6ab77bf..000000000
--- a/assets/javascripts/discourse/components/ai-tool-selector.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import { readOnly } from "@ember/object/computed";
-import { observes } from "@ember-decorators/object";
-import MultiSelectComponent from "select-kit/components/multi-select";
-import { selectKitOptions } from "select-kit/components/select-kit";
-
-@selectKitOptions({
- filterable: true,
-})
-export default class AiToolSelector extends MultiSelectComponent {
- @readOnly("tools") content;
-
- value = "";
-
- @observes("attrs.disabled")
- _modelDisabledChanged() {
- this.selectKit.options.set("disabled", this.get("attrs.disabled.value"));
- }
-}
diff --git a/assets/javascripts/discourse/components/rag-options-fk.gjs b/assets/javascripts/discourse/components/rag-options-fk.gjs
new file mode 100644
index 000000000..e540f7441
--- /dev/null
+++ b/assets/javascripts/discourse/components/rag-options-fk.gjs
@@ -0,0 +1,81 @@
+import Component from "@glimmer/component";
+import { tracked } from "@glimmer/tracking";
+import { on } from "@ember/modifier";
+import { action } from "@ember/object";
+import { i18n } from "discourse-i18n";
+import AiLlmSelector from "./ai-llm-selector";
+
+export default class RagOptionsFk extends Component {
+ @tracked showIndexingOptions = false;
+
+ @action
+ toggleIndexingOptions(event) {
+ this.showIndexingOptions = !this.showIndexingOptions;
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ get indexingOptionsText() {
+ return this.showIndexingOptions
+ ? i18n("discourse_ai.rag.options.hide_indexing_options")
+ : i18n("discourse_ai.rag.options.show_indexing_options");
+ }
+
+ get visionLlms() {
+ return this.args.llms.filter((llm) => llm.vision_enabled);
+ }
+
+
+ {{#if @data.rag_uploads}}
+ {{this.indexingOptionsText}}
+ {{/if}}
+
+ {{#if this.showIndexingOptions}}
+ <@form.Field
+ @name="rag_chunk_tokens"
+ @title={{i18n "discourse_ai.rag.options.rag_chunk_tokens"}}
+ @tooltip={{i18n "discourse_ai.rag.options.rag_chunk_tokens_help"}}
+ @format="large"
+ as |field|
+ >
+
+ @form.Field>
+
+ <@form.Field
+ @name="rag_chunk_overlap_tokens"
+ @title={{i18n "discourse_ai.rag.options.rag_chunk_tokens"}}
+ @tooltip={{i18n
+ "discourse_ai.rag.options.rag_chunk_overlap_tokens_help"
+ }}
+ @format="large"
+ as |field|
+ >
+
+ @form.Field>
+
+ {{#if @allowImages}}
+ <@form.Field
+ @name="rag_llm_model_id"
+ @title={{i18n "discourse_ai.rag.options.rag_llm_model"}}
+ @tooltip={{i18n "discourse_ai.rag.options.rag_llm_model_help"}}
+ @format="large"
+ as |field|
+ >
+
+
+
+ @form.Field>
+ {{/if}}
+ {{yield}}
+ {{/if}}
+
+}
diff --git a/assets/javascripts/discourse/components/rag-uploader.gjs b/assets/javascripts/discourse/components/rag-uploader.gjs
index 612266faf..7d1fbb480 100644
--- a/assets/javascripts/discourse/components/rag-uploader.gjs
+++ b/assets/javascripts/discourse/components/rag-uploader.gjs
@@ -54,11 +54,10 @@ export default class RagUploader extends Component {
this.uppyUpload.cancelAllUploads();
}
- this.ragUploads = this.target?.rag_uploads || [];
+ this.ragUploads = this.target?.rag_uploads?.slice() || [];
this.filteredUploads = this.ragUploads;
- const targetName = this.target?.constructor?.name;
-
+ const targetName = this.targetName || this.target?.constructor?.name;
if (this.ragUploads?.length && this.target?.id) {
ajax(
`/admin/plugins/discourse-ai/rag-document-fragments/files/status.json?target_type=${targetName}&target_id=${this.target.id}`
@@ -127,7 +126,6 @@ export default class RagUploader extends Component {
-
{{i18n "discourse_ai.rag.uploads.title"}}
{{#if @allowImages}}
{{i18n "discourse_ai.rag.uploads.description_with_images"}}
{{else}}
diff --git a/assets/stylesheets/modules/ai-bot/common/ai-persona.scss b/assets/stylesheets/modules/ai-bot/common/ai-persona.scss
index ae663cc8a..585ac0dde 100644
--- a/assets/stylesheets/modules/ai-bot/common/ai-persona.scss
+++ b/assets/stylesheets/modules/ai-bot/common/ai-persona.scss
@@ -42,17 +42,8 @@
.ai-persona-editor {
padding-left: 0.5em;
- .fk-d-tooltip__icon {
- padding-left: 0.25em;
- color: var(--primary-medium);
- }
-
- label {
- display: block;
- }
-
&__tool-options {
- padding: 5px 10px 5px;
+ padding: 1em;
border: 1px solid var(--primary-low-mid);
width: 480px;
}
@@ -61,27 +52,6 @@
margin-bottom: 10px;
font-size: var(--font-down-1);
}
-
- &__description {
- width: 500px;
- }
-
- &__system_prompt {
- width: 500px;
- height: 400px;
- }
-
- &__tool-details,
- &__vision_enabled,
- &__allow_chat_direct_messages,
- &__allow_chat_channel_mentions,
- &__allow_topic_mentions,
- &__allow_personal_messages,
- &__force_default_llm,
- &__priority {
- display: flex;
- align-items: center;
- }
}
.rag-options {
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 0eb81da4e..5f4c7295c 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -241,6 +241,7 @@ en:
custom: "Custom..."
ai_persona:
+ ai_tools: "Tools"
tool_strategies:
all: "Apply to all replies"
replies:
@@ -306,7 +307,12 @@ en:
rag_conversation_chunks_help: "The number of chunks to use for the RAG model searches. Increase to increase the amount of context the AI can use."
persona_description: "Personas are a powerful feature that allows you to customize the behavior of the AI engine in your Discourse forum. They act as a 'system message' that guides the AI's responses and interactions, helping to create a more personalized and engaging user experience."
+ ai_bot:
+ title: "AI bot options"
+ save_first: "AI bot options will become available once you save the persona."
+
rag:
+ title: "RAG"
options:
rag_chunk_tokens: "Upload chunk tokens"
rag_chunk_tokens_help: "The number of tokens to use for each chunk in the RAG model. Increase to increase the amount of context the AI can use. (changing will re-index all uploads)"
diff --git a/spec/system/admin_ai_persona_spec.rb b/spec/system/admin_ai_persona_spec.rb
index 19c1e100c..482349260 100644
--- a/spec/system/admin_ai_persona_spec.rb
+++ b/spec/system/admin_ai_persona_spec.rb
@@ -3,6 +3,7 @@
RSpec.describe "Admin AI persona configuration", type: :system, js: true do
fab!(:admin)
let(:page_header) { PageObjects::Components::DPageHeader.new }
+ let(:form) { PageObjects::Components::FormKit.new("form") }
before do
SiteSetting.ai_bot_enabled = true
@@ -19,26 +20,23 @@
expect(page_header).to be_hidden
- find(".ai-persona-editor__name").set("Test Persona")
- find(".ai-persona-editor__description").fill_in(with: "I am a test persona")
- find(".ai-persona-editor__system_prompt").fill_in(with: "You are a helpful bot")
+ form.field("name").fill_in("Test Persona")
+ form.field("description").fill_in("I am a test persona")
+ form.field("system_prompt").fill_in("You are a helpful bot")
- tool_selector = PageObjects::Components::SelectKit.new(".ai-persona-editor__tools")
+ tool_selector = PageObjects::Components::SelectKit.new("#control-tools .select-kit")
tool_selector.expand
tool_selector.select_row_by_value("Read")
tool_selector.collapse
- tool_selector = PageObjects::Components::SelectKit.new(".ai-persona-editor__forced_tools")
+ tool_selector = PageObjects::Components::SelectKit.new("#control-forcedTools .select-kit")
tool_selector.expand
tool_selector.select_row_by_value("Read")
tool_selector.collapse
- strategy_selector =
- PageObjects::Components::SelectKit.new(".ai-persona-editor__forced_tool_strategy")
- strategy_selector.expand
- strategy_selector.select_row_by_value(1)
+ form.field("forced_tool_count").select(1)
- find(".ai-persona-editor__save").click()
+ form.submit
expect(page).not_to have_current_path("/admin/plugins/discourse-ai/ai-personas/new")
@@ -55,7 +53,7 @@
it "will not allow deletion or editing of system personas" do
visit "/admin/plugins/discourse-ai/ai-personas/#{DiscourseAi::AiBot::Personas::Persona.system_personas.values.first}/edit"
expect(page).not_to have_selector(".ai-persona-editor__delete")
- expect(find(".ai-persona-editor__system_prompt")).to be_disabled
+ expect(form.field("system_prompt")).to be_disabled
end
it "will enable persona right away when you click on enable but does not save side effects" do
@@ -63,8 +61,8 @@
visit "/admin/plugins/discourse-ai/ai-personas/#{persona.id}/edit"
- find(".ai-persona-editor__name").set("Test Persona 1")
- PageObjects::Components::DToggleSwitch.new(".ai-persona-editor__enabled").toggle
+ form.field("name").fill_in("Test Persona 1")
+ form.field("enabled").toggle
try_until_success { expect(persona.reload.enabled).to eq(true) }
diff --git a/spec/system/ai_bot/tool_spec.rb b/spec/system/ai_bot/tool_spec.rb
index 2defa2938..052361da1 100644
--- a/spec/system/ai_bot/tool_spec.rb
+++ b/spec/system/ai_bot/tool_spec.rb
@@ -71,7 +71,7 @@ def ensure_can_run_test
visit "/admin/plugins/discourse-ai/ai-personas/new"
tool_id = AiTool.order("id desc").limit(1).pluck(:id).first
- tool_selector = PageObjects::Components::SelectKit.new(".ai-persona-editor__tools")
+ tool_selector = PageObjects::Components::SelectKit.new("#control-tools .select-kit")
tool_selector.expand
tool_selector.select_row_by_value("custom-#{tool_id}")
diff --git a/test/javascripts/unit/models/ai-persona-test.js b/test/javascripts/unit/models/ai-persona-test.js
index 0ed6fe66a..a21d51a22 100644
--- a/test/javascripts/unit/models/ai-persona-test.js
+++ b/test/javascripts/unit/models/ai-persona-test.js
@@ -2,33 +2,31 @@ import { module, test } from "qunit";
import AiPersona from "discourse/plugins/discourse-ai/discourse/admin/models/ai-persona";
module("Discourse AI | Unit | Model | ai-persona", function () {
- test("init properties", function (assert) {
+ test("toPOJO", function (assert) {
const properties = {
tools: [
- ["ToolName", { option1: "value1", option2: "value2" }],
+ ["ToolName", { option1: "value1", option2: "value2" }, false],
"ToolName2",
"ToolName3",
],
};
- const aiPersona = AiPersona.create(properties);
+ const aiPersonaPOJO = AiPersona.create(properties).toPOJO();
- assert.deepEqual(aiPersona.tools, ["ToolName", "ToolName2", "ToolName3"]);
- assert.equal(
- aiPersona.getToolOption("ToolName", "option1").value,
- "value1"
- );
- assert.equal(
- aiPersona.getToolOption("ToolName", "option2").value,
- "value2"
- );
+ assert.deepEqual(aiPersonaPOJO.tools, [
+ "ToolName",
+ "ToolName2",
+ "ToolName3",
+ ]);
+ assert.equal(aiPersonaPOJO.toolOptions["ToolName"].option1, "value1");
+ assert.equal(aiPersonaPOJO.toolOptions["ToolName"].option2, "value2");
});
- test("update properties", function (assert) {
+ test("fromPOJO", function (assert) {
const properties = {
id: 1,
name: "Test",
- tools: ["ToolName"],
+ tools: [["ToolName", { option1: "value1" }, false]],
allowed_group_ids: [12],
system: false,
enabled: true,
@@ -58,80 +56,19 @@ module("Discourse AI | Unit | Model | ai-persona", function () {
allow_chat_channel_mentions: true,
allow_chat_direct_messages: true,
};
+ const updatedValue = "updated";
const aiPersona = AiPersona.create({ ...properties });
- aiPersona.getToolOption("ToolName", "option1").value = "value1";
+ const personaPOJO = aiPersona.toPOJO();
- const updatedProperties = aiPersona.updateProperties();
+ personaPOJO.toolOptions["ToolName"].option1 = updatedValue;
+ personaPOJO.forcedTools = "ToolName";
- // perform remapping for save
- properties.tools = [["ToolName", { option1: "value1" }, false]];
+ const updatedPersona = aiPersona.fromPOJO(personaPOJO);
- assert.deepEqual(updatedProperties, properties);
- });
-
- test("create properties", function (assert) {
- const properties = {
- id: 1,
- name: "Test",
- tools: ["ToolName"],
- allowed_group_ids: [12],
- system: false,
- enabled: true,
- system_prompt: "System Prompt",
- priority: false,
- description: "Description",
- top_p: 0.8,
- temperature: 0.7,
- user: null,
- user_id: null,
- default_llm_id: 1,
- max_context_posts: 5,
- vision_enabled: true,
- vision_max_pixels: 100,
- rag_uploads: [],
- rag_chunk_tokens: 374,
- rag_chunk_overlap_tokens: 10,
- rag_conversation_chunks: 10,
- question_consolidator_llm_id: 2,
- allow_chat: false,
- tool_details: true,
- forced_tool_count: -1,
- allow_personal_messages: true,
- allow_topic_mentions: true,
- allow_chat_channel_mentions: true,
- allow_chat_direct_messages: true,
- force_default_llm: false,
- rag_llm_model_id: 1,
- };
-
- const aiPersona = AiPersona.create({ ...properties });
-
- aiPersona.getToolOption("ToolName", "option1").value = "value1";
-
- const createdProperties = aiPersona.createProperties();
-
- properties.tools = [["ToolName", { option1: "value1" }, false]];
-
- assert.deepEqual(createdProperties, properties);
- });
-
- test("working copy", function (assert) {
- const aiPersona = AiPersona.create({
- name: "Test",
- tools: ["ToolName"],
- });
-
- aiPersona.getToolOption("ToolName", "option1").value = "value1";
-
- const workingCopy = aiPersona.workingCopy();
-
- assert.equal(workingCopy.name, "Test");
- assert.equal(
- workingCopy.getToolOption("ToolName", "option1").value,
- "value1"
- );
- assert.deepEqual(workingCopy.tools, ["ToolName"]);
+ assert.deepEqual(updatedPersona.tools, [
+ ["ToolName", { option1: updatedValue }, true],
+ ]);
});
});