FEATURE: UI to update ai personas on admin page (#290)
Introduces a UI to manage customizable personas (admin only feature)
Part of the change was some extensive internal refactoring:
- AIBot now has a persona set in the constructor, once set it never changes
- Command now takes in bot as a constructor param, so it has the correct persona and is not generating AIBot objects on the fly
- Added a .prettierignore file, due to the way ALE is configured in nvim it is a pre-req for prettier to work
- Adds a bunch of validations on the AIPersona model, system personas (artist/creative etc...) are all seeded. We now ensure
- name uniqueness, and only allow certain properties to be touched for system personas.
- (JS note) the client side design takes advantage of nested routes, the parent route for personas gets all the personas via this.store.findAll("ai-persona") then child routes simply reach into this model to find a particular persona.
- (JS note) data is sideloaded into the ai-persona model the meta property supplied from the controller, resultSetMeta
- This removes ai_bot_enabled_personas and ai_bot_enabled_chat_commands, both should be controlled from the UI on a per persona basis
- Fixes a long standing bug in token accounting ... we were doing to_json.length instead of to_json.to_s.length
- Amended it so {commands} are always inserted at the end unconditionally, no need to add it to the template of the system message as it just confuses things
- Adds a concept of required_commands to stock personas, these are commands that must be configured for this stock persona to show up.
- Refactored tests so we stop requiring inference_stubs, it was very confusing to need it, added to plugin.rb for now which at least is clearer
- Migrates the persona selector to gjs
---------
Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Co-authored-by: Martin Brennan <martin@discourse.org>
2023-11-21 00:56:43 -05:00
|
|
|
# frozen_string_literal: true
|
|
|
|
require "rails_helper"
|
|
|
|
|
|
|
|
RSpec.describe DiscourseAi::Admin::AiPersonasController do
|
|
|
|
fab!(:admin)
|
|
|
|
fab!(:ai_persona)
|
|
|
|
|
|
|
|
before { sign_in(admin) }
|
|
|
|
|
|
|
|
describe "GET #index" do
|
|
|
|
it "returns a success response" do
|
|
|
|
get "/admin/plugins/discourse-ai/ai_personas.json"
|
|
|
|
expect(response).to be_successful
|
|
|
|
|
|
|
|
expect(response.parsed_body["ai_personas"].length).to eq(AiPersona.count)
|
|
|
|
expect(response.parsed_body["meta"]["commands"].length).to eq(
|
|
|
|
DiscourseAi::AiBot::Personas::Persona.all_available_commands.length,
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2023-12-07 16:42:56 -05:00
|
|
|
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"),
|
|
|
|
},
|
2023-12-11 00:54:16 -05:00
|
|
|
"max_results" => {
|
|
|
|
"type" => "integer",
|
|
|
|
"name" => I18n.t("discourse_ai.ai_bot.command_options.search.max_results.name"),
|
|
|
|
"description" =>
|
|
|
|
I18n.t("discourse_ai.ai_bot.command_options.search.max_results.description"),
|
|
|
|
},
|
2023-12-07 16:42:56 -05:00
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
expect(serializer_persona1["commands"]).to eq(["SearchCommand"])
|
|
|
|
expect(serializer_persona2["commands"]).to eq([["SearchCommand", { "base_query" => "test" }]])
|
|
|
|
end
|
|
|
|
|
FEATURE: UI to update ai personas on admin page (#290)
Introduces a UI to manage customizable personas (admin only feature)
Part of the change was some extensive internal refactoring:
- AIBot now has a persona set in the constructor, once set it never changes
- Command now takes in bot as a constructor param, so it has the correct persona and is not generating AIBot objects on the fly
- Added a .prettierignore file, due to the way ALE is configured in nvim it is a pre-req for prettier to work
- Adds a bunch of validations on the AIPersona model, system personas (artist/creative etc...) are all seeded. We now ensure
- name uniqueness, and only allow certain properties to be touched for system personas.
- (JS note) the client side design takes advantage of nested routes, the parent route for personas gets all the personas via this.store.findAll("ai-persona") then child routes simply reach into this model to find a particular persona.
- (JS note) data is sideloaded into the ai-persona model the meta property supplied from the controller, resultSetMeta
- This removes ai_bot_enabled_personas and ai_bot_enabled_chat_commands, both should be controlled from the UI on a per persona basis
- Fixes a long standing bug in token accounting ... we were doing to_json.length instead of to_json.to_s.length
- Amended it so {commands} are always inserted at the end unconditionally, no need to add it to the template of the system message as it just confuses things
- Adds a concept of required_commands to stock personas, these are commands that must be configured for this stock persona to show up.
- Refactored tests so we stop requiring inference_stubs, it was very confusing to need it, added to plugin.rb for now which at least is clearer
- Migrates the persona selector to gjs
---------
Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Co-authored-by: Martin Brennan <martin@discourse.org>
2023-11-21 00:56:43 -05:00
|
|
|
it "returns localized persona names and descriptions" do
|
|
|
|
SiteSetting.default_locale = "fr"
|
|
|
|
|
|
|
|
get "/admin/plugins/discourse-ai/ai_personas.json"
|
|
|
|
|
|
|
|
TranslationOverride.upsert!(:fr, "discourse_ai.ai_bot.personas.general.name", "Général")
|
|
|
|
TranslationOverride.upsert!(
|
|
|
|
:fr,
|
|
|
|
"discourse_ai.ai_bot.personas.general.description",
|
|
|
|
"Général Description",
|
|
|
|
)
|
|
|
|
|
|
|
|
id = DiscourseAi::AiBot::Personas.system_personas[DiscourseAi::AiBot::Personas::General]
|
|
|
|
name = I18n.t("discourse_ai.ai_bot.personas.general.name")
|
|
|
|
description = I18n.t("discourse_ai.ai_bot.personas.general.description")
|
|
|
|
persona = response.parsed_body["ai_personas"].find { |p| p["id"] == id }
|
|
|
|
|
|
|
|
expect(persona["name"]).to eq(name)
|
|
|
|
expect(persona["description"]).to eq(description)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe "GET #show" do
|
|
|
|
it "returns a success response" do
|
|
|
|
get "/admin/plugins/discourse-ai/ai_personas/#{ai_persona.id}.json"
|
|
|
|
expect(response).to be_successful
|
|
|
|
expect(response.parsed_body["ai_persona"]["name"]).to eq(ai_persona.name)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe "POST #create" do
|
|
|
|
context "with valid params" do
|
|
|
|
let(:valid_attributes) do
|
|
|
|
{
|
|
|
|
name: "superbot",
|
|
|
|
description: "Assists with tasks",
|
|
|
|
system_prompt: "you are a helpful bot",
|
2023-12-07 16:42:56 -05:00
|
|
|
commands: [["search", { "base_query" => "test" }]],
|
FEATURE: UI to update ai personas on admin page (#290)
Introduces a UI to manage customizable personas (admin only feature)
Part of the change was some extensive internal refactoring:
- AIBot now has a persona set in the constructor, once set it never changes
- Command now takes in bot as a constructor param, so it has the correct persona and is not generating AIBot objects on the fly
- Added a .prettierignore file, due to the way ALE is configured in nvim it is a pre-req for prettier to work
- Adds a bunch of validations on the AIPersona model, system personas (artist/creative etc...) are all seeded. We now ensure
- name uniqueness, and only allow certain properties to be touched for system personas.
- (JS note) the client side design takes advantage of nested routes, the parent route for personas gets all the personas via this.store.findAll("ai-persona") then child routes simply reach into this model to find a particular persona.
- (JS note) data is sideloaded into the ai-persona model the meta property supplied from the controller, resultSetMeta
- This removes ai_bot_enabled_personas and ai_bot_enabled_chat_commands, both should be controlled from the UI on a per persona basis
- Fixes a long standing bug in token accounting ... we were doing to_json.length instead of to_json.to_s.length
- Amended it so {commands} are always inserted at the end unconditionally, no need to add it to the template of the system message as it just confuses things
- Adds a concept of required_commands to stock personas, these are commands that must be configured for this stock persona to show up.
- Refactored tests so we stop requiring inference_stubs, it was very confusing to need it, added to plugin.rb for now which at least is clearer
- Migrates the persona selector to gjs
---------
Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Co-authored-by: Martin Brennan <martin@discourse.org>
2023-11-21 00:56:43 -05:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
it "creates a new AiPersona" do
|
|
|
|
expect {
|
|
|
|
post "/admin/plugins/discourse-ai/ai_personas.json",
|
2023-12-07 16:42:56 -05:00
|
|
|
params: { ai_persona: valid_attributes }.to_json,
|
|
|
|
headers: {
|
|
|
|
"CONTENT_TYPE" => "application/json",
|
FEATURE: UI to update ai personas on admin page (#290)
Introduces a UI to manage customizable personas (admin only feature)
Part of the change was some extensive internal refactoring:
- AIBot now has a persona set in the constructor, once set it never changes
- Command now takes in bot as a constructor param, so it has the correct persona and is not generating AIBot objects on the fly
- Added a .prettierignore file, due to the way ALE is configured in nvim it is a pre-req for prettier to work
- Adds a bunch of validations on the AIPersona model, system personas (artist/creative etc...) are all seeded. We now ensure
- name uniqueness, and only allow certain properties to be touched for system personas.
- (JS note) the client side design takes advantage of nested routes, the parent route for personas gets all the personas via this.store.findAll("ai-persona") then child routes simply reach into this model to find a particular persona.
- (JS note) data is sideloaded into the ai-persona model the meta property supplied from the controller, resultSetMeta
- This removes ai_bot_enabled_personas and ai_bot_enabled_chat_commands, both should be controlled from the UI on a per persona basis
- Fixes a long standing bug in token accounting ... we were doing to_json.length instead of to_json.to_s.length
- Amended it so {commands} are always inserted at the end unconditionally, no need to add it to the template of the system message as it just confuses things
- Adds a concept of required_commands to stock personas, these are commands that must be configured for this stock persona to show up.
- Refactored tests so we stop requiring inference_stubs, it was very confusing to need it, added to plugin.rb for now which at least is clearer
- Migrates the persona selector to gjs
---------
Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Co-authored-by: Martin Brennan <martin@discourse.org>
2023-11-21 00:56:43 -05:00
|
|
|
}
|
|
|
|
expect(response).to be_successful
|
2023-12-07 16:42:56 -05:00
|
|
|
persona = AiPersona.find(response.parsed_body["ai_persona"]["id"])
|
|
|
|
expect(persona.commands).to eq([["search", { "base_query" => "test" }]])
|
FEATURE: UI to update ai personas on admin page (#290)
Introduces a UI to manage customizable personas (admin only feature)
Part of the change was some extensive internal refactoring:
- AIBot now has a persona set in the constructor, once set it never changes
- Command now takes in bot as a constructor param, so it has the correct persona and is not generating AIBot objects on the fly
- Added a .prettierignore file, due to the way ALE is configured in nvim it is a pre-req for prettier to work
- Adds a bunch of validations on the AIPersona model, system personas (artist/creative etc...) are all seeded. We now ensure
- name uniqueness, and only allow certain properties to be touched for system personas.
- (JS note) the client side design takes advantage of nested routes, the parent route for personas gets all the personas via this.store.findAll("ai-persona") then child routes simply reach into this model to find a particular persona.
- (JS note) data is sideloaded into the ai-persona model the meta property supplied from the controller, resultSetMeta
- This removes ai_bot_enabled_personas and ai_bot_enabled_chat_commands, both should be controlled from the UI on a per persona basis
- Fixes a long standing bug in token accounting ... we were doing to_json.length instead of to_json.to_s.length
- Amended it so {commands} are always inserted at the end unconditionally, no need to add it to the template of the system message as it just confuses things
- Adds a concept of required_commands to stock personas, these are commands that must be configured for this stock persona to show up.
- Refactored tests so we stop requiring inference_stubs, it was very confusing to need it, added to plugin.rb for now which at least is clearer
- Migrates the persona selector to gjs
---------
Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Co-authored-by: Martin Brennan <martin@discourse.org>
2023-11-21 00:56:43 -05:00
|
|
|
}.to change(AiPersona, :count).by(1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context "with invalid params" do
|
|
|
|
it "renders a JSON response with errors for the new ai_persona" do
|
|
|
|
post "/admin/plugins/discourse-ai/ai_personas.json", params: { ai_persona: { foo: "" } } # invalid attribute
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
|
|
expect(response.content_type).to include("application/json")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe "PUT #update" do
|
|
|
|
context "with valid params" do
|
|
|
|
it "updates the requested ai_persona" do
|
|
|
|
put "/admin/plugins/discourse-ai/ai_personas/#{ai_persona.id}.json",
|
|
|
|
params: {
|
|
|
|
ai_persona: {
|
|
|
|
name: "SuperBot",
|
|
|
|
enabled: false,
|
|
|
|
commands: ["search"],
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
expect(response.content_type).to include("application/json")
|
|
|
|
|
|
|
|
ai_persona.reload
|
|
|
|
expect(ai_persona.name).to eq("SuperBot")
|
|
|
|
expect(ai_persona.enabled).to eq(false)
|
|
|
|
expect(ai_persona.commands).to eq(["search"])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context "with system personas" do
|
|
|
|
it "does not allow editing of system prompts" do
|
|
|
|
put "/admin/plugins/discourse-ai/ai_personas/#{DiscourseAi::AiBot::Personas.system_personas.values.first}.json",
|
|
|
|
params: {
|
|
|
|
ai_persona: {
|
|
|
|
system_prompt: "you are not a helpful bot",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
|
|
expect(response.parsed_body["errors"].join).not_to be_blank
|
|
|
|
expect(response.parsed_body["errors"].join).not_to include("en.discourse")
|
|
|
|
end
|
|
|
|
|
|
|
|
it "does not allow editing of commands" do
|
|
|
|
put "/admin/plugins/discourse-ai/ai_personas/#{DiscourseAi::AiBot::Personas.system_personas.values.first}.json",
|
|
|
|
params: {
|
|
|
|
ai_persona: {
|
|
|
|
commands: %w[SearchCommand ImageCommand],
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
|
|
expect(response.parsed_body["errors"].join).not_to be_blank
|
|
|
|
expect(response.parsed_body["errors"].join).not_to include("en.discourse")
|
|
|
|
end
|
|
|
|
|
|
|
|
it "does not allow editing of name and description cause it is localized" do
|
|
|
|
put "/admin/plugins/discourse-ai/ai_personas/#{DiscourseAi::AiBot::Personas.system_personas.values.first}.json",
|
|
|
|
params: {
|
|
|
|
ai_persona: {
|
|
|
|
name: "bob",
|
|
|
|
dscription: "the bob",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
|
|
expect(response.parsed_body["errors"].join).not_to be_blank
|
|
|
|
expect(response.parsed_body["errors"].join).not_to include("en.discourse")
|
|
|
|
end
|
|
|
|
|
|
|
|
it "does allow some actions" do
|
|
|
|
put "/admin/plugins/discourse-ai/ai_personas/#{DiscourseAi::AiBot::Personas.system_personas.values.first}.json",
|
|
|
|
params: {
|
|
|
|
ai_persona: {
|
|
|
|
allowed_group_ids: [Group::AUTO_GROUPS[:trust_level_1]],
|
|
|
|
enabled: false,
|
|
|
|
priority: 989,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(response).to be_successful
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context "with invalid params" do
|
|
|
|
it "renders a JSON response with errors for the ai_persona" do
|
|
|
|
put "/admin/plugins/discourse-ai/ai_personas/#{ai_persona.id}.json",
|
|
|
|
params: {
|
|
|
|
ai_persona: {
|
|
|
|
name: "",
|
|
|
|
},
|
|
|
|
} # invalid attribute
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
|
|
expect(response.content_type).to include("application/json")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe "DELETE #destroy" do
|
|
|
|
it "destroys the requested ai_persona" do
|
|
|
|
expect {
|
|
|
|
delete "/admin/plugins/discourse-ai/ai_personas/#{ai_persona.id}.json"
|
|
|
|
|
|
|
|
expect(response).to have_http_status(:no_content)
|
|
|
|
}.to change(AiPersona, :count).by(-1)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "is not allowed to delete system personas" do
|
|
|
|
expect {
|
|
|
|
delete "/admin/plugins/discourse-ai/ai_personas/#{DiscourseAi::AiBot::Personas.system_personas.values.first}.json"
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
|
|
expect(response.parsed_body["errors"].join).not_to be_blank
|
|
|
|
# let's make sure this is translated
|
|
|
|
expect(response.parsed_body["errors"].join).not_to include("en.discourse")
|
|
|
|
}.not_to change(AiPersona, :count)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|