FEATURE: allow personas to provide command options (#331)
Personas now support providing options for commands. This PR introduces a single option "base_query" for the SearchCommand. When supplied all searches the persona will perform will also include the pre-supplied filter. This can allow personas to search a subset of the forum (such as documentation) This system is extensible we can add options to any command trivially.
This commit is contained in:
parent
381b0d74ca
commit
6380ebd829
|
@ -14,7 +14,7 @@ module DiscourseAi
|
|||
end
|
||||
commands =
|
||||
DiscourseAi::AiBot::Personas::Persona.all_available_commands.map do |command|
|
||||
{ id: command.to_s.split("::").last, name: command.name.humanize.titleize }
|
||||
AiCommandSerializer.new(command, root: false)
|
||||
end
|
||||
render json: { ai_personas: ai_personas, meta: { commands: commands } }
|
||||
end
|
||||
|
@ -55,16 +55,36 @@ module DiscourseAi
|
|||
end
|
||||
|
||||
def ai_persona_params
|
||||
permitted =
|
||||
params.require(:ai_persona).permit(
|
||||
:name,
|
||||
:description,
|
||||
:enabled,
|
||||
:system_prompt,
|
||||
:enabled,
|
||||
:priority,
|
||||
allowed_group_ids: [],
|
||||
commands: [],
|
||||
)
|
||||
|
||||
if commands = params.dig(:ai_persona, :commands)
|
||||
permitted[:commands] = permit_commands(commands)
|
||||
end
|
||||
|
||||
permitted
|
||||
end
|
||||
|
||||
def permit_commands(commands)
|
||||
return [] if !commands.is_a?(Array)
|
||||
|
||||
commands.filter_map do |command, options|
|
||||
break nil if !command.is_a?(String)
|
||||
options&.permit! if options && options.is_a?(ActionController::Parameters)
|
||||
|
||||
if options
|
||||
[command, options]
|
||||
else
|
||||
command
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -87,10 +87,23 @@ class AiPersona < ActiveRecord::Base
|
|||
name = self.name
|
||||
description = self.description
|
||||
ai_persona_id = self.id
|
||||
|
||||
options = {}
|
||||
|
||||
commands =
|
||||
self.commands.filter_map do |inner_name|
|
||||
self.commands.filter_map do |element|
|
||||
inner_name = element
|
||||
current_options = nil
|
||||
|
||||
if element.is_a?(Array)
|
||||
inner_name = element[0]
|
||||
current_options = element[1]
|
||||
end
|
||||
|
||||
begin
|
||||
("DiscourseAi::AiBot::Commands::#{inner_name}").constantize
|
||||
klass = ("DiscourseAi::AiBot::Commands::#{inner_name}").constantize
|
||||
options[klass] = current_options if current_options
|
||||
klass
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
|
@ -134,6 +147,10 @@ class AiPersona < ActiveRecord::Base
|
|||
commands
|
||||
end
|
||||
|
||||
define_method :options do
|
||||
options
|
||||
end
|
||||
|
||||
define_method :system_prompt do
|
||||
@ai_persona&.system_prompt || "You are a helpful bot."
|
||||
end
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AiCommandSerializer < ApplicationSerializer
|
||||
attributes :options, :id, :name, :help
|
||||
|
||||
def include_options?
|
||||
object.options.present?
|
||||
end
|
||||
|
||||
def id
|
||||
object.to_s.split("::").last
|
||||
end
|
||||
|
||||
def name
|
||||
object.name.humanize.titleize
|
||||
end
|
||||
|
||||
def help
|
||||
object.help
|
||||
end
|
||||
|
||||
def options
|
||||
options = {}
|
||||
object.options.each do |option|
|
||||
options[option.name] = {
|
||||
name: option.localized_name,
|
||||
description: option.localized_description,
|
||||
type: option.type,
|
||||
}
|
||||
end
|
||||
options
|
||||
end
|
||||
end
|
|
@ -1,3 +1,4 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import RestModel from "discourse/models/rest";
|
||||
|
||||
const ATTRIBUTES = [
|
||||
|
@ -11,15 +12,81 @@ const ATTRIBUTES = [
|
|||
"priority",
|
||||
];
|
||||
|
||||
class CommandOption {
|
||||
@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 commands as an Array.
|
||||
// [[CommandName, {option1: value, option2: value}], CommandName2, CommandName3]
|
||||
// So we rework this into a "commands" property and nested commandOptions
|
||||
init(properties) {
|
||||
if (properties.commands) {
|
||||
properties.commands = properties.commands.map((command) => {
|
||||
if (typeof command === "string") {
|
||||
return command;
|
||||
} else {
|
||||
let [commandId, options] = command;
|
||||
for (let optionId in options) {
|
||||
if (!options.hasOwnProperty(optionId)) {
|
||||
continue;
|
||||
}
|
||||
this.getCommandOption(commandId, optionId).value =
|
||||
options[optionId];
|
||||
}
|
||||
return commandId;
|
||||
}
|
||||
});
|
||||
}
|
||||
super.init(properties);
|
||||
this.commands = properties.commands;
|
||||
}
|
||||
|
||||
getCommandOption(commandId, optionId) {
|
||||
this.commandOptions ||= {};
|
||||
this.commandOptions[commandId] ||= {};
|
||||
return (this.commandOptions[commandId][optionId] ||= new CommandOption());
|
||||
}
|
||||
|
||||
populateCommandOptions(attrs) {
|
||||
if (!attrs.commands) {
|
||||
return;
|
||||
}
|
||||
let commandsWithOptions = [];
|
||||
attrs.commands.forEach((commandId) => {
|
||||
if (typeof commandId !== "string") {
|
||||
commandId = commandId[0];
|
||||
}
|
||||
if (this.commandOptions && this.commandOptions[commandId]) {
|
||||
let options = this.commandOptions[commandId];
|
||||
let optionsWithValues = {};
|
||||
for (let optionId in options) {
|
||||
if (!options.hasOwnProperty(optionId)) {
|
||||
continue;
|
||||
}
|
||||
let option = options[optionId];
|
||||
optionsWithValues[optionId] = option.value;
|
||||
}
|
||||
commandsWithOptions.push([commandId, optionsWithValues]);
|
||||
} else {
|
||||
commandsWithOptions.push(commandId);
|
||||
}
|
||||
});
|
||||
attrs.commands = commandsWithOptions;
|
||||
}
|
||||
|
||||
updateProperties() {
|
||||
let attrs = this.getProperties(ATTRIBUTES);
|
||||
attrs.id = this.id;
|
||||
this.populateCommandOptions(attrs);
|
||||
return attrs;
|
||||
}
|
||||
|
||||
createProperties() {
|
||||
return this.getProperties(ATTRIBUTES);
|
||||
let attrs = this.getProperties(ATTRIBUTES);
|
||||
this.populateCommandOptions(attrs);
|
||||
return attrs;
|
||||
}
|
||||
|
||||
workingCopy() {
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import { Input } from "@ember/component";
|
||||
|
||||
<template>
|
||||
<div class="control-group ai-persona-command-option-editor">
|
||||
<label>
|
||||
{{@option.name}}
|
||||
</label>
|
||||
<div class="">
|
||||
<Input @value={{@option.value.value}} />
|
||||
</div>
|
||||
<div class="ai-persona-command-option-editor__instructions">
|
||||
{{@option.description}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,81 @@
|
|||
import Component from "@glimmer/component";
|
||||
import I18n from "discourse-i18n";
|
||||
import AiPersonaCommandOptionEditor from "./ai-persona-command-option-editor";
|
||||
|
||||
export default class AiPersonaCommandOptions extends Component {
|
||||
get showCommandOptions() {
|
||||
const allCommands = this.args.allCommands;
|
||||
if (!allCommands) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.commandNames.any(
|
||||
(command) => allCommands.find((c) => c.id === command)?.options
|
||||
);
|
||||
}
|
||||
|
||||
get commandNames() {
|
||||
if (!this.args.commands) {
|
||||
return [];
|
||||
}
|
||||
return this.args.commands.map((command) => {
|
||||
if (typeof command === "string") {
|
||||
return command;
|
||||
} else {
|
||||
return command[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get commandOptions() {
|
||||
if (!this.args.commands) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allCommands = this.args.allCommands;
|
||||
if (!allCommands) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const options = [];
|
||||
this.commandNames.forEach((commandId) => {
|
||||
const command = allCommands.find((c) => c.id === commandId);
|
||||
|
||||
const commandName = command?.name;
|
||||
const commandOptions = command?.options;
|
||||
|
||||
if (commandOptions) {
|
||||
const mappedOptions = Object.keys(commandOptions).map((key) => {
|
||||
const value = this.args.persona.getCommandOption(commandId, key);
|
||||
return Object.assign({}, commandOptions[key], { id: key, value });
|
||||
});
|
||||
|
||||
options.push({ commandName, options: mappedOptions });
|
||||
}
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.showCommandOptions}}
|
||||
<div class="control-group">
|
||||
<label>{{I18n.t "discourse_ai.ai_persona.command_options"}}</label>
|
||||
<div>
|
||||
{{#each this.commandOptions as |commandOption|}}
|
||||
<div class="ai-persona-editor__command-options">
|
||||
<div class="ai-persona-editor__command-options-name">
|
||||
{{commandOption.commandName}}
|
||||
</div>
|
||||
<div class="ai-persona-editor__command-option-options">
|
||||
{{#each commandOption.options as |option|}}
|
||||
<AiPersonaCommandOptionEditor @option={{option}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
|
@ -12,11 +12,11 @@ import Textarea from "discourse/components/d-textarea";
|
|||
import DToggleSwitch from "discourse/components/d-toggle-switch";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import Group from "discourse/models/group";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import I18n from "discourse-i18n";
|
||||
import GroupChooser from "select-kit/components/group-chooser";
|
||||
import DTooltip from "float-kit/components/d-tooltip";
|
||||
import AiCommandSelector from "./ai-command-selector";
|
||||
import AiPersonaCommandOptions from "./ai-persona-command-options";
|
||||
|
||||
export default class PersonaEditor extends Component {
|
||||
@service router;
|
||||
|
@ -159,7 +159,7 @@ export default class PersonaEditor extends Component {
|
|||
/>
|
||||
<DTooltip
|
||||
@icon="question-circle"
|
||||
@content={{i18n "discourse_ai.ai_persona.priority_help"}}
|
||||
@content={{I18n.t "discourse_ai.ai_persona.priority_help"}}
|
||||
/>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
|
@ -188,6 +188,13 @@ export default class PersonaEditor extends Component {
|
|||
@commands={{@personas.resultSetMeta.commands}}
|
||||
/>
|
||||
</div>
|
||||
{{#unless this.editingModel.system}}
|
||||
<AiPersonaCommandOptions
|
||||
@persona={{this.editingModel}}
|
||||
@commands={{this.editingModel.commands}}
|
||||
@allCommands={{@personas.resultSetMeta.commands}}
|
||||
/>
|
||||
{{/unless}}
|
||||
<div class="control-group">
|
||||
<label>{{I18n.t "discourse_ai.ai_persona.allowed_groups"}}</label>
|
||||
<GroupChooser
|
||||
|
|
|
@ -14,6 +14,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.ai-persona-command-option-editor {
|
||||
&__instructions {
|
||||
color: var(--primary-medium);
|
||||
font-size: var(--font-down-1);
|
||||
line-height: var(--line-height-large);
|
||||
}
|
||||
}
|
||||
|
||||
.ai-personas__container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -26,6 +34,15 @@
|
|||
label {
|
||||
display: block;
|
||||
}
|
||||
&__command-options {
|
||||
padding: 5px 10px 5px;
|
||||
border: 1px solid var(--primary-low-mid);
|
||||
width: 480px;
|
||||
}
|
||||
&__command-options-name {
|
||||
margin-bottom: 10px;
|
||||
font-size: var(--font-down-1);
|
||||
}
|
||||
&__description {
|
||||
width: 500px;
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ en:
|
|||
delete: Delete
|
||||
priority: Priority
|
||||
priority_help: Priority personas are displayed to users at the top of the persona list. If multiple personas have priority, they will be sorted alphabetically.
|
||||
command_options: "Command Options"
|
||||
no_persona_selected: |
|
||||
## What are AI Personas?
|
||||
|
||||
|
|
|
@ -152,6 +152,11 @@ en:
|
|||
description: "AI Bot specialized in generating images using DALL-E 3"
|
||||
topic_not_found: "Summary unavailable, topic not found!"
|
||||
searching: "Searching for: '%{query}'"
|
||||
command_options:
|
||||
search:
|
||||
base_query:
|
||||
name: "Base Search Query"
|
||||
description: "Base query to use when searching. Example: '#urgent' will prepend '#urgent' to the search query and only include topics with the urgent category or tag."
|
||||
command_summary:
|
||||
categories: "List categories"
|
||||
search: "Search"
|
||||
|
@ -165,6 +170,19 @@ en:
|
|||
schema: "Look up database schema"
|
||||
search_settings: "Searching site settings"
|
||||
dall_e: "Generate image"
|
||||
command_help:
|
||||
categories: "List all publicly visible categories on the forum"
|
||||
search: "Search all public topics on the forum"
|
||||
tags: "List all tags on the forum"
|
||||
time: "Find time in various time zones"
|
||||
summarize: "Summarize a topic"
|
||||
image: "Generate image using Stable Diffusion"
|
||||
google: "Search Google for a query"
|
||||
read: "Read public topic on the forum"
|
||||
setting_context: "Look up site setting context"
|
||||
schema: "Look up database schema"
|
||||
search_settings: "Search site settings"
|
||||
dall_e: "Generate image using DALL-E 3"
|
||||
command_description:
|
||||
read: "Reading: <a href='%{url}'>%{title}</a>"
|
||||
time: "Time in %{timezone} is %{time}"
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
class ConvertAiPersonasCommandsToJson < ActiveRecord::Migration[7.0]
|
||||
def up
|
||||
# this all may be a bit surprising, but interestingly this makes all our backend code
|
||||
# cross compatible
|
||||
# upgrading ["a", "b", "c"] to json simply works cause in both cases
|
||||
# rails will cast to a string array and all code simply expectes a string array
|
||||
#
|
||||
# this change was made so we can also start storing parameters with the commands
|
||||
execute <<~SQL
|
||||
ALTER TABLE ai_personas
|
||||
ALTER COLUMN commands DROP DEFAULT
|
||||
SQL
|
||||
|
||||
execute <<~SQL
|
||||
ALTER TABLE ai_personas
|
||||
ALTER COLUMN commands
|
||||
TYPE json USING array_to_json(commands)
|
||||
SQL
|
||||
|
||||
execute <<~SQL
|
||||
ALTER TABLE ai_personas
|
||||
ALTER COLUMN commands
|
||||
SET DEFAULT '[]'::json
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
|
@ -3,18 +3,6 @@
|
|||
module DiscourseAi
|
||||
module AiBot
|
||||
module Commands
|
||||
class Parameter
|
||||
attr_reader :item_type, :name, :description, :type, :enum, :required
|
||||
def initialize(name:, description:, type:, enum: nil, required: false, item_type: nil)
|
||||
@name = name
|
||||
@description = description
|
||||
@type = type
|
||||
@enum = enum
|
||||
@required = required
|
||||
@item_type = item_type
|
||||
end
|
||||
end
|
||||
|
||||
class Command
|
||||
CARET = "<!-- caret -->"
|
||||
PROGRESS_CARET = "<!-- progress -->"
|
||||
|
@ -38,6 +26,18 @@ module DiscourseAi
|
|||
def parameters
|
||||
raise NotImplemented
|
||||
end
|
||||
|
||||
def options
|
||||
[]
|
||||
end
|
||||
|
||||
def help
|
||||
I18n.t("discourse_ai.ai_bot.command_help.#{name}")
|
||||
end
|
||||
|
||||
def option(name, type:)
|
||||
Option.new(command: self, name: name, type: type)
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :bot_user, :bot
|
||||
|
@ -63,6 +63,22 @@ module DiscourseAi
|
|||
@invoked = false
|
||||
end
|
||||
|
||||
def persona_options
|
||||
return @persona_options if @persona_options
|
||||
|
||||
@persona_options = HashWithIndifferentAccess.new
|
||||
|
||||
# during tests we may operate without a bot
|
||||
return @persona_options if !self.bot
|
||||
|
||||
self.class.options.each do |option|
|
||||
val = self.bot.persona.options.dig(self.class, option.name)
|
||||
@persona_options[option.name] = val if val
|
||||
end
|
||||
|
||||
@persona_options
|
||||
end
|
||||
|
||||
def tokenizer
|
||||
bot.tokenizer
|
||||
end
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
module DiscourseAi
|
||||
module AiBot
|
||||
module Commands
|
||||
class Option
|
||||
attr_reader :command, :name, :type
|
||||
def initialize(command:, name:, type:)
|
||||
@command = command
|
||||
@name = name.to_s
|
||||
@type = type
|
||||
end
|
||||
|
||||
def localized_name
|
||||
I18n.t("discourse_ai.ai_bot.command_options.#{command.name}.#{name}.name")
|
||||
end
|
||||
|
||||
def localized_description
|
||||
I18n.t("discourse_ai.ai_bot.command_options.#{command.name}.#{name}.description")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -11,6 +11,10 @@ module DiscourseAi::AiBot::Commands
|
|||
"Will search topics in the current discourse instance, when rendering always prefer to link to the topics you find"
|
||||
end
|
||||
|
||||
def options
|
||||
[option(:base_query, type: :string)]
|
||||
end
|
||||
|
||||
def parameters
|
||||
[
|
||||
Parameter.new(
|
||||
|
@ -119,6 +123,10 @@ module DiscourseAi::AiBot::Commands
|
|||
|
||||
show_progress(I18n.t("discourse_ai.ai_bot.searching", query: search_string))
|
||||
|
||||
if persona_options[:base_query].present?
|
||||
search_string = "#{search_string} #{persona_options[:base_query]}"
|
||||
end
|
||||
|
||||
results =
|
||||
Search.execute(
|
||||
search_string.to_s + " status:public",
|
||||
|
|
|
@ -20,6 +20,10 @@ module DiscourseAi
|
|||
[]
|
||||
end
|
||||
|
||||
def options
|
||||
{}
|
||||
end
|
||||
|
||||
def render_commands(render_function_instructions:)
|
||||
return +"" if available_commands.empty?
|
||||
|
||||
|
|
|
@ -4,9 +4,10 @@ RSpec.describe DiscourseAi::AiBot::Commands::SearchCommand do
|
|||
before { SearchIndexer.enable }
|
||||
after { SearchIndexer.disable }
|
||||
|
||||
let(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID) }
|
||||
fab!(:admin)
|
||||
fab!(:parent_category) { Fabricate(:category, name: "animals") }
|
||||
fab!(:category) { Fabricate(:category, parent_category: parent_category, name: "amazing-cat") }
|
||||
|
||||
fab!(:tag_funny) { Fabricate(:tag, name: "funny") }
|
||||
fab!(:tag_sad) { Fabricate(:tag, name: "sad") }
|
||||
fab!(:tag_hidden) { Fabricate(:tag, name: "hidden") }
|
||||
|
@ -25,7 +26,42 @@ RSpec.describe DiscourseAi::AiBot::Commands::SearchCommand do
|
|||
|
||||
before { SiteSetting.ai_bot_enabled = true }
|
||||
|
||||
it "can properly list options" do
|
||||
options = described_class.options
|
||||
expect(options.length).to eq(1)
|
||||
expect(options.first.name.to_s).to eq("base_query")
|
||||
expect(options.first.localized_name).not_to include("Translation missing:")
|
||||
expect(options.first.localized_description).not_to include("Translation missing:")
|
||||
end
|
||||
|
||||
describe "#process" do
|
||||
it "can retreive options from persona correctly" do
|
||||
persona =
|
||||
Fabricate(
|
||||
:ai_persona,
|
||||
allowed_group_ids: [Group::AUTO_GROUPS[:admins]],
|
||||
commands: [["SearchCommand", { "base_query" => "#funny" }]],
|
||||
)
|
||||
Group.refresh_automatic_groups!
|
||||
|
||||
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona_id: persona.id, user: admin)
|
||||
search_post = Fabricate(:post, topic: topic_with_tags)
|
||||
|
||||
bot_post = Fabricate(:post)
|
||||
|
||||
search = described_class.new(bot: bot, post: bot_post, args: nil)
|
||||
|
||||
results = search.process(order: "latest")
|
||||
expect(results[:rows].length).to eq(1)
|
||||
|
||||
search_post.topic.tags = []
|
||||
search_post.topic.save!
|
||||
|
||||
# no longer has the tag funny
|
||||
results = search.process(order: "latest")
|
||||
expect(results[:rows].length).to eq(0)
|
||||
end
|
||||
|
||||
it "can handle no results" do
|
||||
post1 = Fabricate(:post, topic: topic_with_tags)
|
||||
search = described_class.new(bot: nil, post: post1, args: nil)
|
||||
|
|
|
@ -18,6 +18,41 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
|
|||
)
|
||||
end
|
||||
|
||||
it "returns commands options with each command" do
|
||||
persona1 = Fabricate(:ai_persona, name: "search1", commands: ["SearchCommand"])
|
||||
persona2 =
|
||||
Fabricate(
|
||||
:ai_persona,
|
||||
name: "search2",
|
||||
commands: [["SearchCommand", { base_query: "test" }]],
|
||||
)
|
||||
|
||||
get "/admin/plugins/discourse-ai/ai_personas.json"
|
||||
expect(response).to be_successful
|
||||
|
||||
serializer_persona1 = response.parsed_body["ai_personas"].find { |p| p["id"] == persona1.id }
|
||||
serializer_persona2 = response.parsed_body["ai_personas"].find { |p| p["id"] == persona2.id }
|
||||
|
||||
commands = response.parsed_body["meta"]["commands"]
|
||||
search_command = commands.find { |c| c["id"] == "SearchCommand" }
|
||||
|
||||
expect(search_command["help"]).to eq(I18n.t("discourse_ai.ai_bot.command_help.search"))
|
||||
|
||||
expect(search_command["options"]).to eq(
|
||||
{
|
||||
"base_query" => {
|
||||
"type" => "string",
|
||||
"name" => I18n.t("discourse_ai.ai_bot.command_options.search.base_query.name"),
|
||||
"description" =>
|
||||
I18n.t("discourse_ai.ai_bot.command_options.search.base_query.description"),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
expect(serializer_persona1["commands"]).to eq(["SearchCommand"])
|
||||
expect(serializer_persona2["commands"]).to eq([["SearchCommand", { "base_query" => "test" }]])
|
||||
end
|
||||
|
||||
it "returns localized persona names and descriptions" do
|
||||
SiteSetting.default_locale = "fr"
|
||||
|
||||
|
@ -55,16 +90,20 @@ RSpec.describe DiscourseAi::Admin::AiPersonasController do
|
|||
name: "superbot",
|
||||
description: "Assists with tasks",
|
||||
system_prompt: "you are a helpful bot",
|
||||
commands: [["search", { "base_query" => "test" }]],
|
||||
}
|
||||
end
|
||||
|
||||
it "creates a new AiPersona" do
|
||||
expect {
|
||||
post "/admin/plugins/discourse-ai/ai_personas.json",
|
||||
params: {
|
||||
ai_persona: valid_attributes,
|
||||
params: { ai_persona: valid_attributes }.to_json,
|
||||
headers: {
|
||||
"CONTENT_TYPE" => "application/json",
|
||||
}
|
||||
expect(response).to be_successful
|
||||
persona = AiPersona.find(response.parsed_body["ai_persona"]["id"])
|
||||
expect(persona.commands).to eq([["search", { "base_query" => "test" }]])
|
||||
}.to change(AiPersona, :count).by(1)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
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) {
|
||||
const properties = {
|
||||
commands: [
|
||||
["CommandName", { option1: "value1", option2: "value2" }],
|
||||
"CommandName2",
|
||||
"CommandName3",
|
||||
],
|
||||
};
|
||||
|
||||
const aiPersona = AiPersona.create(properties);
|
||||
|
||||
assert.deepEqual(aiPersona.commands, [
|
||||
"CommandName",
|
||||
"CommandName2",
|
||||
"CommandName3",
|
||||
]);
|
||||
assert.equal(
|
||||
aiPersona.getCommandOption("CommandName", "option1").value,
|
||||
"value1"
|
||||
);
|
||||
assert.equal(
|
||||
aiPersona.getCommandOption("CommandName", "option2").value,
|
||||
"value2"
|
||||
);
|
||||
});
|
||||
|
||||
test("update properties", function (assert) {
|
||||
const properties = {
|
||||
id: 1,
|
||||
name: "Test",
|
||||
commands: ["CommandName"],
|
||||
allowed_group_ids: [12],
|
||||
system: false,
|
||||
enabled: true,
|
||||
system_prompt: "System Prompt",
|
||||
priority: false,
|
||||
description: "Description",
|
||||
};
|
||||
|
||||
const aiPersona = AiPersona.create({ ...properties });
|
||||
|
||||
aiPersona.getCommandOption("CommandName", "option1").value = "value1";
|
||||
|
||||
const updatedProperties = aiPersona.updateProperties();
|
||||
|
||||
// perform remapping for save
|
||||
properties.commands = [["CommandName", { option1: "value1" }]];
|
||||
|
||||
assert.deepEqual(updatedProperties, properties);
|
||||
});
|
||||
|
||||
test("create properties", function (assert) {
|
||||
const properties = {
|
||||
name: "Test",
|
||||
commands: ["CommandName"],
|
||||
allowed_group_ids: [12],
|
||||
system: false,
|
||||
enabled: true,
|
||||
system_prompt: "System Prompt",
|
||||
priority: false,
|
||||
description: "Description",
|
||||
};
|
||||
|
||||
const aiPersona = AiPersona.create({ ...properties });
|
||||
|
||||
aiPersona.getCommandOption("CommandName", "option1").value = "value1";
|
||||
|
||||
const createdProperties = aiPersona.createProperties();
|
||||
|
||||
properties.commands = [["CommandName", { option1: "value1" }]];
|
||||
|
||||
assert.deepEqual(createdProperties, properties);
|
||||
});
|
||||
|
||||
test("working copy", function (assert) {
|
||||
const aiPersona = AiPersona.create({
|
||||
name: "Test",
|
||||
commands: ["CommandName"],
|
||||
});
|
||||
|
||||
aiPersona.getCommandOption("CommandName", "option1").value = "value1";
|
||||
|
||||
const workingCopy = aiPersona.workingCopy();
|
||||
|
||||
assert.equal(workingCopy.name, "Test");
|
||||
assert.equal(
|
||||
workingCopy.getCommandOption("CommandName", "option1").value,
|
||||
"value1"
|
||||
);
|
||||
assert.deepEqual(workingCopy.commands, ["CommandName"]);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue