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:
parent
0e2dd7378f
commit
2a8be6e2d7
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
|
@ -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;
|
|
@ -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
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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}}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue