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 { ajax } from "discourse/lib/ajax";
|
||||||
import RestModel from "discourse/models/rest";
|
import RestModel from "discourse/models/rest";
|
||||||
|
|
||||||
|
@ -63,40 +62,7 @@ const SYSTEM_ATTRIBUTES = [
|
||||||
"allow_chat_direct_messages",
|
"allow_chat_direct_messages",
|
||||||
];
|
];
|
||||||
|
|
||||||
class ToolOption {
|
|
||||||
@tracked value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class AiPersona extends RestModel {
|
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() {
|
async createUser() {
|
||||||
const result = await ajax(
|
const result = await ajax(
|
||||||
`/admin/plugins/discourse-ai/ai-personas/${this.id}/create-user.json`,
|
`/admin/plugins/discourse-ai/ai-personas/${this.id}/create-user.json`,
|
||||||
|
@ -109,63 +75,79 @@ export default class AiPersona extends RestModel {
|
||||||
return this.user;
|
return this.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
getToolOption(toolId, optionId) {
|
flattenedToolStructure(data) {
|
||||||
this.toolOptions ||= {};
|
return data.tools.map((tName) => {
|
||||||
this.toolOptions[toolId] ||= {};
|
return [tName, data.toolOptions[tName], data.forcedTools.includes(tName)];
|
||||||
return (this.toolOptions[toolId][optionId] ||= new ToolOption());
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
populateToolOptions(attrs) {
|
// this code is here to convert the wire schema to easier to work with object
|
||||||
if (!attrs.tools) {
|
// on the wire we pass in/out tools as an Array.
|
||||||
return;
|
// [[ToolName, {option1: value, option2: value}, force], ToolName2, ToolName3]
|
||||||
}
|
// We split it into tools, options and a list of forced ones.
|
||||||
let toolsWithOptions = [];
|
populateTools(attrs) {
|
||||||
attrs.tools.forEach((toolId) => {
|
const forcedTools = [];
|
||||||
if (typeof toolId !== "string") {
|
const toolOptions = {};
|
||||||
toolId = toolId[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
let force = this.forcedTools.includes(toolId);
|
const flatTools = attrs.tools?.map((tool) => {
|
||||||
if (this.toolOptions && this.toolOptions[toolId]) {
|
if (typeof tool === "string") {
|
||||||
let options = this.toolOptions[toolId];
|
return tool;
|
||||||
let optionsWithValues = {};
|
} else {
|
||||||
for (let optionId in options) {
|
let [toolId, options, force] = tool;
|
||||||
|
const mappedOptions = {};
|
||||||
|
|
||||||
|
for (const optionId in options) {
|
||||||
if (!options.hasOwnProperty(optionId)) {
|
if (!options.hasOwnProperty(optionId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let option = options[optionId];
|
|
||||||
optionsWithValues[optionId] = option.value;
|
mappedOptions[optionId] = options[optionId];
|
||||||
}
|
}
|
||||||
toolsWithOptions.push([toolId, optionsWithValues, force]);
|
|
||||||
} else {
|
if (Object.keys(mappedOptions).length > 0) {
|
||||||
toolsWithOptions.push([toolId, {}, force]);
|
toolOptions[toolId] = mappedOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
forcedTools.push(toolId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toolId;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
attrs.tools = toolsWithOptions;
|
|
||||||
|
attrs.tools = flatTools;
|
||||||
|
attrs.forcedTools = forcedTools;
|
||||||
|
attrs.toolOptions = toolOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProperties() {
|
updateProperties() {
|
||||||
let attrs = this.system
|
const attrs = this.system
|
||||||
? this.getProperties(SYSTEM_ATTRIBUTES)
|
? this.getProperties(SYSTEM_ATTRIBUTES)
|
||||||
: this.getProperties(CREATE_ATTRIBUTES);
|
: this.getProperties(CREATE_ATTRIBUTES);
|
||||||
attrs.id = this.id;
|
attrs.id = this.id;
|
||||||
this.populateToolOptions(attrs);
|
|
||||||
return attrs;
|
return attrs;
|
||||||
}
|
}
|
||||||
|
|
||||||
createProperties() {
|
createProperties() {
|
||||||
let attrs = this.getProperties(CREATE_ATTRIBUTES);
|
return this.getProperties(CREATE_ATTRIBUTES);
|
||||||
this.populateToolOptions(attrs);
|
|
||||||
return attrs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
workingCopy() {
|
fromPOJO(data) {
|
||||||
let attrs = this.getProperties(CREATE_ATTRIBUTES);
|
const dataClone = JSON.parse(JSON.stringify(data));
|
||||||
this.populateToolOptions(attrs);
|
|
||||||
|
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;
|
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 Component from "@glimmer/component";
|
||||||
|
import { action, get } from "@ember/object";
|
||||||
|
import { eq } from "truth-helpers";
|
||||||
import { i18n } from "discourse-i18n";
|
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 {
|
export default class AiPersonaToolOptions extends Component {
|
||||||
get showToolOptions() {
|
get showToolOptions() {
|
||||||
const allTools = this.args.allTools;
|
const allTools = this.args.allTools;
|
||||||
if (!allTools) {
|
if (!allTools || !this.args.data.tools) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
return this.args.data?.tools.any(
|
||||||
return this.toolNames.any((tool) => allTools.findBy("id", tool)?.options);
|
(tool) => allTools.findBy("id", tool)?.options
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get toolNames() {
|
get toolsMetadata() {
|
||||||
if (!this.args.tools) {
|
const metatada = {};
|
||||||
return [];
|
|
||||||
}
|
this.args.allTools.map((t) => {
|
||||||
return this.args.tools.map((tool) => {
|
metatada[t.id] = {
|
||||||
if (typeof tool === "string") {
|
name: t.name,
|
||||||
return tool;
|
...t?.options,
|
||||||
} else {
|
};
|
||||||
return tool[0];
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return metatada;
|
||||||
}
|
}
|
||||||
|
|
||||||
get toolOptions() {
|
@action
|
||||||
if (!this.args.tools) {
|
formObjectKeys(toolOptions) {
|
||||||
return [];
|
return Object.keys(toolOptions);
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
{{#if this.showToolOptions}}
|
{{#if this.showToolOptions}}
|
||||||
<div class="control-group">
|
<@form.Container
|
||||||
<label>{{i18n "discourse_ai.ai_persona.tool_options"}}</label>
|
@title={{i18n "discourse_ai.ai_persona.tool_options"}}
|
||||||
<div>
|
@direction="column"
|
||||||
{{#each this.toolOptions as |toolOption|}}
|
@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">
|
||||||
<div class="ai-persona-editor__tool-options-name">
|
{{#let (get this.toolsMetadata toolId) as |toolMeta|}}
|
||||||
{{toolOption.toolName}}
|
<div class="ai-persona-editor__tool-options-name">
|
||||||
</div>
|
{{toolMeta.name}}
|
||||||
<div class="ai-persona-editor__tool-option-options">
|
</div>
|
||||||
{{#each toolOption.options as |option|}}
|
<toolObj.Object @name={{toolId}} as |optionsObj optionData|>
|
||||||
<AiPersonaToolOptionEditor
|
{{#each (this.formObjectKeys optionData) as |optionName|}}
|
||||||
@option={{option}}
|
{{#let (get toolMeta optionName) as |optionMeta|}}
|
||||||
@llms={{@llms}}
|
<optionsObj.Field
|
||||||
/>
|
@name={{optionName}}
|
||||||
{{/each}}
|
@title={{optionMeta.name}}
|
||||||
</div>
|
@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>
|
</div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</@form.Object>
|
||||||
</div>
|
</@form.Container>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</template>
|
</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.uppyUpload.cancelAllUploads();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ragUploads = this.target?.rag_uploads || [];
|
this.ragUploads = this.target?.rag_uploads?.slice() || [];
|
||||||
this.filteredUploads = this.ragUploads;
|
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) {
|
if (this.ragUploads?.length && this.target?.id) {
|
||||||
ajax(
|
ajax(
|
||||||
`/admin/plugins/discourse-ai/rag-document-fragments/files/status.json?target_type=${targetName}&target_id=${this.target.id}`
|
`/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>
|
<template>
|
||||||
<div class="rag-uploader">
|
<div class="rag-uploader">
|
||||||
<h3>{{i18n "discourse_ai.rag.uploads.title"}}</h3>
|
|
||||||
{{#if @allowImages}}
|
{{#if @allowImages}}
|
||||||
<p>{{i18n "discourse_ai.rag.uploads.description_with_images"}}</p>
|
<p>{{i18n "discourse_ai.rag.uploads.description_with_images"}}</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|
|
@ -42,17 +42,8 @@
|
||||||
.ai-persona-editor {
|
.ai-persona-editor {
|
||||||
padding-left: 0.5em;
|
padding-left: 0.5em;
|
||||||
|
|
||||||
.fk-d-tooltip__icon {
|
|
||||||
padding-left: 0.25em;
|
|
||||||
color: var(--primary-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__tool-options {
|
&__tool-options {
|
||||||
padding: 5px 10px 5px;
|
padding: 1em;
|
||||||
border: 1px solid var(--primary-low-mid);
|
border: 1px solid var(--primary-low-mid);
|
||||||
width: 480px;
|
width: 480px;
|
||||||
}
|
}
|
||||||
|
@ -61,27 +52,6 @@
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-size: var(--font-down-1);
|
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 {
|
.rag-options {
|
||||||
|
|
|
@ -241,6 +241,7 @@ en:
|
||||||
custom: "Custom..."
|
custom: "Custom..."
|
||||||
|
|
||||||
ai_persona:
|
ai_persona:
|
||||||
|
ai_tools: "Tools"
|
||||||
tool_strategies:
|
tool_strategies:
|
||||||
all: "Apply to all replies"
|
all: "Apply to all replies"
|
||||||
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."
|
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."
|
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:
|
rag:
|
||||||
|
title: "RAG"
|
||||||
options:
|
options:
|
||||||
rag_chunk_tokens: "Upload chunk tokens"
|
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)"
|
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
|
RSpec.describe "Admin AI persona configuration", type: :system, js: true do
|
||||||
fab!(:admin)
|
fab!(:admin)
|
||||||
let(:page_header) { PageObjects::Components::DPageHeader.new }
|
let(:page_header) { PageObjects::Components::DPageHeader.new }
|
||||||
|
let(:form) { PageObjects::Components::FormKit.new("form") }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
SiteSetting.ai_bot_enabled = true
|
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
|
expect(page_header).to be_hidden
|
||||||
|
|
||||||
find(".ai-persona-editor__name").set("Test Persona")
|
form.field("name").fill_in("Test Persona")
|
||||||
find(".ai-persona-editor__description").fill_in(with: "I am a test persona")
|
form.field("description").fill_in("I am a test persona")
|
||||||
find(".ai-persona-editor__system_prompt").fill_in(with: "You are a helpful bot")
|
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.expand
|
||||||
tool_selector.select_row_by_value("Read")
|
tool_selector.select_row_by_value("Read")
|
||||||
tool_selector.collapse
|
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.expand
|
||||||
tool_selector.select_row_by_value("Read")
|
tool_selector.select_row_by_value("Read")
|
||||||
tool_selector.collapse
|
tool_selector.collapse
|
||||||
|
|
||||||
strategy_selector =
|
form.field("forced_tool_count").select(1)
|
||||||
PageObjects::Components::SelectKit.new(".ai-persona-editor__forced_tool_strategy")
|
|
||||||
strategy_selector.expand
|
|
||||||
strategy_selector.select_row_by_value(1)
|
|
||||||
|
|
||||||
find(".ai-persona-editor__save").click()
|
form.submit
|
||||||
|
|
||||||
expect(page).not_to have_current_path("/admin/plugins/discourse-ai/ai-personas/new")
|
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
|
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"
|
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(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
|
end
|
||||||
|
|
||||||
it "will enable persona right away when you click on enable but does not save side effects" do
|
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"
|
visit "/admin/plugins/discourse-ai/ai-personas/#{persona.id}/edit"
|
||||||
|
|
||||||
find(".ai-persona-editor__name").set("Test Persona 1")
|
form.field("name").fill_in("Test Persona 1")
|
||||||
PageObjects::Components::DToggleSwitch.new(".ai-persona-editor__enabled").toggle
|
form.field("enabled").toggle
|
||||||
|
|
||||||
try_until_success { expect(persona.reload.enabled).to eq(true) }
|
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"
|
visit "/admin/plugins/discourse-ai/ai-personas/new"
|
||||||
|
|
||||||
tool_id = AiTool.order("id desc").limit(1).pluck(:id).first
|
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.expand
|
||||||
|
|
||||||
tool_selector.select_row_by_value("custom-#{tool_id}")
|
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";
|
import AiPersona from "discourse/plugins/discourse-ai/discourse/admin/models/ai-persona";
|
||||||
|
|
||||||
module("Discourse AI | Unit | Model | ai-persona", function () {
|
module("Discourse AI | Unit | Model | ai-persona", function () {
|
||||||
test("init properties", function (assert) {
|
test("toPOJO", function (assert) {
|
||||||
const properties = {
|
const properties = {
|
||||||
tools: [
|
tools: [
|
||||||
["ToolName", { option1: "value1", option2: "value2" }],
|
["ToolName", { option1: "value1", option2: "value2" }, false],
|
||||||
"ToolName2",
|
"ToolName2",
|
||||||
"ToolName3",
|
"ToolName3",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const aiPersona = AiPersona.create(properties);
|
const aiPersonaPOJO = AiPersona.create(properties).toPOJO();
|
||||||
|
|
||||||
assert.deepEqual(aiPersona.tools, ["ToolName", "ToolName2", "ToolName3"]);
|
assert.deepEqual(aiPersonaPOJO.tools, [
|
||||||
assert.equal(
|
"ToolName",
|
||||||
aiPersona.getToolOption("ToolName", "option1").value,
|
"ToolName2",
|
||||||
"value1"
|
"ToolName3",
|
||||||
);
|
]);
|
||||||
assert.equal(
|
assert.equal(aiPersonaPOJO.toolOptions["ToolName"].option1, "value1");
|
||||||
aiPersona.getToolOption("ToolName", "option2").value,
|
assert.equal(aiPersonaPOJO.toolOptions["ToolName"].option2, "value2");
|
||||||
"value2"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("update properties", function (assert) {
|
test("fromPOJO", function (assert) {
|
||||||
const properties = {
|
const properties = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "Test",
|
name: "Test",
|
||||||
tools: ["ToolName"],
|
tools: [["ToolName", { option1: "value1" }, false]],
|
||||||
allowed_group_ids: [12],
|
allowed_group_ids: [12],
|
||||||
system: false,
|
system: false,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
@ -58,80 +56,19 @@ module("Discourse AI | Unit | Model | ai-persona", function () {
|
||||||
allow_chat_channel_mentions: true,
|
allow_chat_channel_mentions: true,
|
||||||
allow_chat_direct_messages: true,
|
allow_chat_direct_messages: true,
|
||||||
};
|
};
|
||||||
|
const updatedValue = "updated";
|
||||||
|
|
||||||
const aiPersona = AiPersona.create({ ...properties });
|
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
|
const updatedPersona = aiPersona.fromPOJO(personaPOJO);
|
||||||
properties.tools = [["ToolName", { option1: "value1" }, false]];
|
|
||||||
|
|
||||||
assert.deepEqual(updatedProperties, properties);
|
assert.deepEqual(updatedPersona.tools, [
|
||||||
});
|
["ToolName", { option1: updatedValue }, true],
|
||||||
|
]);
|
||||||
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"]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue