REFACTOR: Migrate Personas' form to FormKit (#1178)

* REFACTOR: Migrate Personas' form to FormKit

We re-arranged fields into sections so we can better differentiate which options are specific to the AI bot.

* few form-kit improvements

https://github.com/discourse/discourse/pull/31934

---------

Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
This commit is contained in:
Roman Rizzi 2025-03-21 14:46:33 -03:00 committed by GitHub
parent 0e2dd7378f
commit 2a8be6e2d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 788 additions and 930 deletions

View File

@ -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 = {};
let force = this.forcedTools.includes(toolId);
if (this.toolOptions && this.toolOptions[toolId]) {
let options = this.toolOptions[toolId];
let optionsWithValues = {};
for (let optionId in options) {
const flatTools = attrs.tools?.map((tool) => {
if (typeof tool === "string") {
return tool;
} else {
let [toolId, options, force] = tool;
const mappedOptions = {};
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;
}
}

View File

@ -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,
},
});

View File

@ -0,0 +1,17 @@
import { hash } from "@ember/helper";
import ComboBox from "select-kit/components/combo-box";
const AiLlmSelector = <template>
<ComboBox
@value={{@value}}
@content={{@llms}}
@onChange={{@onChange}}
@options={{hash
filterable=true
none="discourse_ai.ai_persona.no_llm_selected"
}}
class={{@class}}
/>
</template>;
export default AiLlmSelector;

View File

@ -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);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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;
}
<template>
<div class="control-group ai-persona-tool-option-editor">
<label>
{{@option.name}}
</label>
<div class="">
{{#if this.isEnum}}
<select name="input" {{on "change" this.onSelectOption}}>
{{#each @option.values as |value|}}
<option value={{value}} selected={{eq value @option.value.value}}>
{{value}}
</option>
{{/each}}
</select>
{{else if this.isLlm}}
<AiLlmSelector
class="ai-persona-tool-option-editor__llms"
@value={{this.selectedLlm}}
@llms={{@llms}}
@blankName={{i18n "discourse_ai.ai_persona.use_parent_llm"}}
/>
{{else if this.isBoolean}}
<input
type="checkbox"
checked={{this.selectedValue}}
{{on "click" this.onCheckboxChange}}
/>
{{@option.description}}
{{else}}
<Input @value={{@option.value.value}} />
{{/if}}
</div>
{{#unless this.isBoolean}}
<div class="ai-persona-tool-option-editor__instructions">
{{@option.description}}
</div>
{{/unless}}
</div>
</template>
}

View File

@ -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 toolsMetadata() {
const metatada = {};
this.args.allTools.map((t) => {
metatada[t.id] = {
name: t.name,
...t?.options,
};
});
return metatada;
}
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;
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 });
}
});
return options;
@action
formObjectKeys(toolOptions) {
return Object.keys(toolOptions);
}
<template>
{{#if this.showToolOptions}}
<div class="control-group">
<label>{{i18n "discourse_ai.ai_persona.tool_options"}}</label>
<div>
{{#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|}}
<div class="ai-persona-editor__tool-options">
<div class="ai-persona-editor__tool-options-name">
{{toolOption.toolName}}
</div>
<div class="ai-persona-editor__tool-option-options">
{{#each toolOption.options as |option|}}
<AiPersonaToolOptionEditor
@option={{option}}
@llms={{@llms}}
/>
{{/each}}
</div>
{{#let (get this.toolsMetadata toolId) as |toolMeta|}}
<div class="ai-persona-editor__tool-options-name">
{{toolMeta.name}}
</div>
<toolObj.Object @name={{toolId}} as |optionsObj optionData|>
{{#each (this.formObjectKeys optionData) as |optionName|}}
{{#let (get toolMeta optionName) as |optionMeta|}}
<optionsObj.Field
@name={{optionName}}
@title={{optionMeta.name}}
@helpText={{optionMeta.description}}
@format="full"
as |field|
>
{{#if (eq optionMeta.type "enum")}}
<field.Select @includeNone={{false}} as |select|>
{{#each optionsObj.values as |v|}}
<select.Option @value={{v}}>{{v}}</select.Option>
{{/each}}
</field.Select>
{{else if (eq optionMeta.type "llm")}}
<field.Custom>
<AiLlmSelector
@value={{field.value}}
@llms={{@llms}}
@onChange={{field.set}}
@class="ai-persona-tool-option-editor__llms"
/>
</field.Custom>
{{else if (eq optionMeta.type "boolean")}}
<field.Checkbox />
{{else}}
<field.Input />
{{/if}}
</optionsObj.Field>
{{/let}}
{{/each}}
</toolObj.Object>
{{/let}}
</div>
{{/each}}
</div>
</div>
</@form.Object>
</@form.Container>
{{/if}}
</template>
}

View File

@ -0,0 +1,13 @@
import { hash } from "@ember/helper";
import MultiSelect from "select-kit/components/multi-select";
const AiToolSelector = <template>
<MultiSelect
@value={{@value}}
@onChange={{@onChange}}
@content={{@content}}
@options={{hash filterable=true allowAny=false disabled=@disabled}}
/>
</template>;
export default AiToolSelector;

View File

@ -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"));
}
}

View File

@ -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);
}
<template>
{{#if @data.rag_uploads}}
<a
href="#"
class="rag-options__indexing-options"
{{on "click" this.toggleIndexingOptions}}
>{{this.indexingOptionsText}}</a>
{{/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|
>
<field.Input @type="number" step="any" lang="en" />
</@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|
>
<field.Input @type="number" step="any" lang="en" />
</@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|
>
<field.Custom>
<AiLlmSelector
@value={{field.value}}
@llms={{this.visionLlms}}
@onChange={{field.set}}
@class="ai-persona-editor__llms"
/>
</field.Custom>
</@form.Field>
{{/if}}
{{yield}}
{{/if}}
</template>
}

View File

@ -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 {
<template>
<div class="rag-uploader">
<h3>{{i18n "discourse_ai.rag.uploads.title"}}</h3>
{{#if @allowImages}}
<p>{{i18n "discourse_ai.rag.uploads.description_with_images"}}</p>
{{else}}

View File

@ -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 {

View File

@ -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)"

View File

@ -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 @@ RSpec.describe "Admin AI persona configuration", type: :system, js: true do
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 @@ RSpec.describe "Admin AI persona configuration", type: :system, js: true do
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 @@ RSpec.describe "Admin AI persona configuration", type: :system, js: true do
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) }

View File

@ -71,7 +71,7 @@ describe "AI Tool Management", type: :system do
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}")

View File

@ -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],
]);
});
});