FEATURE: improve site setting search (#780)
This improves the site setting search so it performs a somewhat fuzzy match. Previously it did not handle seperators such as "space" and a term such as "min_post_length" would not find "min_first_post_length" A more liberal search algorithm makes it easier to the AI to navigate settings. * Minor fix, {{and parameter.enum parameter.enum.length}} is non obviously broken. If parameter.enum is a tracked array it will return the object cause embers and helper implementation. This corrects an issue where enum keeps on selecting itself by mistake.
This commit is contained in:
parent
943504049c
commit
41054c4fb8
|
@ -20,26 +20,25 @@ export default class AiTool extends RestModel {
|
||||||
return this.getProperties(CREATE_ATTRIBUTES);
|
return this.getProperties(CREATE_ATTRIBUTES);
|
||||||
}
|
}
|
||||||
|
|
||||||
workingCopy() {
|
trackParameters(parameters) {
|
||||||
const attrs = this.getProperties(CREATE_ATTRIBUTES);
|
return new TrackedArray(
|
||||||
|
parameters?.map((p) => {
|
||||||
attrs.parameters = new TrackedArray(
|
|
||||||
attrs.parameters?.map((p) => {
|
|
||||||
const parameter = new TrackedObject(p);
|
const parameter = new TrackedObject(p);
|
||||||
|
|
||||||
//Backwards-compatibility code.
|
if (parameter.enum && parameter.enum.length) {
|
||||||
// TODO(roman): Remove aug 2024. Leave only else clause.
|
|
||||||
if (parameter.enum_values) {
|
|
||||||
parameter.enum = new TrackedArray(parameter.enum_values);
|
|
||||||
delete parameter.enum_values;
|
|
||||||
} else {
|
|
||||||
parameter.enum = new TrackedArray(parameter.enum);
|
parameter.enum = new TrackedArray(parameter.enum);
|
||||||
|
} else {
|
||||||
|
parameter.enum = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return parameter;
|
return parameter;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
workingCopy() {
|
||||||
|
const attrs = this.getProperties(CREATE_ATTRIBUTES);
|
||||||
|
attrs.parameters = this.trackParameters(attrs.parameters);
|
||||||
return this.store.createRecord("ai-tool", attrs);
|
return this.store.createRecord("ai-tool", attrs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ export default class AiToolEditor extends Component {
|
||||||
@service dialog;
|
@service dialog;
|
||||||
@service modal;
|
@service modal;
|
||||||
@service toasts;
|
@service toasts;
|
||||||
|
@service store;
|
||||||
|
|
||||||
@tracked isSaving = false;
|
@tracked isSaving = false;
|
||||||
@tracked editingModel = null;
|
@tracked editingModel = null;
|
||||||
|
@ -53,8 +54,9 @@ export default class AiToolEditor extends Component {
|
||||||
@action
|
@action
|
||||||
configurePreset() {
|
configurePreset() {
|
||||||
this.selectedPreset = this.args.presets.findBy("preset_id", this.presetId);
|
this.selectedPreset = this.args.presets.findBy("preset_id", this.presetId);
|
||||||
this.editingModel = this.args.model.workingCopy();
|
this.editingModel = this.store
|
||||||
this.editingModel.setProperties(this.selectedPreset);
|
.createRecord("ai-tool", this.selectedPreset)
|
||||||
|
.workingCopy();
|
||||||
this.showDelete = false;
|
this.showDelete = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import DButton from "discourse/components/d-button";
|
||||||
import withEventValue from "discourse/helpers/with-event-value";
|
import withEventValue from "discourse/helpers/with-event-value";
|
||||||
import I18n from "discourse-i18n";
|
import I18n from "discourse-i18n";
|
||||||
import ComboBox from "select-kit/components/combo-box";
|
import ComboBox from "select-kit/components/combo-box";
|
||||||
import and from "truth-helpers/helpers/and";
|
|
||||||
|
|
||||||
const PARAMETER_TYPES = [
|
const PARAMETER_TYPES = [
|
||||||
{ name: "string", id: "string" },
|
{ name: "string", id: "string" },
|
||||||
|
@ -59,6 +58,9 @@ export default class AiToolParameterEditor extends Component {
|
||||||
@action
|
@action
|
||||||
removeEnumValue(parameter, index) {
|
removeEnumValue(parameter, index) {
|
||||||
parameter.enum.splice(index, 1);
|
parameter.enum.splice(index, 1);
|
||||||
|
if (parameter.enum.length === 0) {
|
||||||
|
parameter.enum = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -94,6 +96,7 @@ export default class AiToolParameterEditor extends Component {
|
||||||
{{on "input" (fn this.toggleRequired parameter)}}
|
{{on "input" (fn this.toggleRequired parameter)}}
|
||||||
checked={{parameter.required}}
|
checked={{parameter.required}}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
class="parameter-row__required-toggle"
|
||||||
/>
|
/>
|
||||||
{{I18n.t "discourse_ai.tools.parameter_required"}}
|
{{I18n.t "discourse_ai.tools.parameter_required"}}
|
||||||
</label>
|
</label>
|
||||||
|
@ -101,8 +104,9 @@ export default class AiToolParameterEditor extends Component {
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
{{on "input" (fn this.toggleEnum parameter)}}
|
{{on "input" (fn this.toggleEnum parameter)}}
|
||||||
checked={{and parameter.enum parameter.enum.length}}
|
checked={{parameter.enum}}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
class="parameter-row__enum-toggle"
|
||||||
/>
|
/>
|
||||||
{{I18n.t "discourse_ai.tools.parameter_enum"}}
|
{{I18n.t "discourse_ai.tools.parameter_enum"}}
|
||||||
</label>
|
</label>
|
||||||
|
@ -114,7 +118,7 @@ export default class AiToolParameterEditor extends Component {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if (and parameter.enum parameter.enum.length)}}
|
{{#if parameter.enum}}
|
||||||
<div class="parameter-enum-values">
|
<div class="parameter-enum-values">
|
||||||
{{#each parameter.enum as |enumValue enumIndex|}}
|
{{#each parameter.enum as |enumValue enumIndex|}}
|
||||||
<div class="enum-value-row">
|
<div class="enum-value-row">
|
||||||
|
|
|
@ -31,20 +31,35 @@ module DiscourseAi
|
||||||
parameters[:query].to_s
|
parameters[:query].to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def all_settings
|
||||||
|
@all_settings ||= SiteSetting.all_settings
|
||||||
|
end
|
||||||
|
|
||||||
|
def all_settings=(settings)
|
||||||
|
# this is only used for testing
|
||||||
|
@all_settings = settings
|
||||||
|
end
|
||||||
|
|
||||||
def invoke
|
def invoke
|
||||||
@last_num_results = 0
|
@last_num_results = 0
|
||||||
|
|
||||||
terms = query.split(",").map(&:strip).map(&:downcase).reject(&:blank?)
|
terms = query.split(",").map(&:strip).map(&:downcase).reject(&:blank?)
|
||||||
|
|
||||||
|
terms_regexes =
|
||||||
|
terms.map do |term|
|
||||||
|
regex_string = term.split(/[ _\.\|]/).map { |t| Regexp.escape(t) }.join(".*")
|
||||||
|
Regexp.new(regex_string, Regexp::IGNORECASE)
|
||||||
|
end
|
||||||
|
|
||||||
found =
|
found =
|
||||||
SiteSetting.all_settings.filter do |setting|
|
all_settings.filter do |setting|
|
||||||
name = setting[:setting].to_s.downcase
|
name = setting[:setting].to_s.downcase
|
||||||
description = setting[:description].to_s.downcase
|
description = setting[:description].to_s.downcase
|
||||||
plugin = setting[:plugin].to_s.downcase
|
plugin = setting[:plugin].to_s.downcase
|
||||||
|
|
||||||
search_string = "#{name} #{description} #{plugin}"
|
search_string = "#{name} #{description} #{plugin}"
|
||||||
|
|
||||||
terms.any? { |term| search_string.include?(term) }
|
terms_regexes.any? { |regex| search_string.match?(regex) }
|
||||||
end
|
end
|
||||||
|
|
||||||
if found.blank?
|
if found.blank?
|
||||||
|
|
|
@ -5,13 +5,28 @@ RSpec.describe DiscourseAi::AiBot::Tools::SearchSettings do
|
||||||
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
|
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
|
||||||
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
|
let(:llm) { DiscourseAi::Completions::Llm.proxy("custom:#{llm_model.id}") }
|
||||||
|
|
||||||
|
let(:fake_settings) do
|
||||||
|
[
|
||||||
|
{ setting: "default_locale", description: "The default locale for the site", plugin: "core" },
|
||||||
|
{ setting: "min_post_length", description: "The minimum length of a post", plugin: "core" },
|
||||||
|
{
|
||||||
|
setting: "ai_bot_enabled",
|
||||||
|
description: "Enable or disable the AI bot",
|
||||||
|
plugin: "discourse-ai",
|
||||||
|
},
|
||||||
|
{ setting: "min_first_post_length", description: "First post length", plugin: "core" },
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
SiteSetting.ai_bot_enabled = true
|
SiteSetting.ai_bot_enabled = true
|
||||||
toggle_enabled_bots(bots: [llm_model])
|
toggle_enabled_bots(bots: [llm_model])
|
||||||
end
|
end
|
||||||
|
|
||||||
def search_settings(query)
|
def search_settings(query, mock: true)
|
||||||
described_class.new({ query: query }, bot_user: bot_user, llm: llm)
|
search = described_class.new({ query: query }, bot_user: bot_user, llm: llm)
|
||||||
|
search.all_settings = fake_settings if mock
|
||||||
|
search
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#process" do
|
describe "#process" do
|
||||||
|
@ -21,8 +36,20 @@ RSpec.describe DiscourseAi::AiBot::Tools::SearchSettings do
|
||||||
expect(results[:rows]).to eq([])
|
expect(results[:rows]).to eq([])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "can find a setting based on fuzzy match" do
|
||||||
|
results = search_settings("default locale").invoke
|
||||||
|
expect(results[:rows].length).to eq(1)
|
||||||
|
expect(results[:rows][0][0]).to eq("default_locale")
|
||||||
|
|
||||||
|
results = search_settings("min_post_length").invoke
|
||||||
|
|
||||||
|
expect(results[:rows].length).to eq(2)
|
||||||
|
expect(results[:rows][0][0]).to eq("min_post_length")
|
||||||
|
expect(results[:rows][1][0]).to eq("min_first_post_length")
|
||||||
|
end
|
||||||
|
|
||||||
it "can return more many settings with no descriptions if there are lots of hits" do
|
it "can return more many settings with no descriptions if there are lots of hits" do
|
||||||
results = search_settings("a").invoke
|
results = search_settings("a", mock: false).invoke
|
||||||
|
|
||||||
expect(results[:rows].length).to be > 30
|
expect(results[:rows].length).to be > 30
|
||||||
expect(results[:rows][0].length).to eq(1)
|
expect(results[:rows][0].length).to eq(1)
|
||||||
|
|
|
@ -22,6 +22,10 @@ describe "AI Tool Management", type: :system do
|
||||||
select_kit.select_row_by_value("exchange_rate")
|
select_kit.select_row_by_value("exchange_rate")
|
||||||
|
|
||||||
find(".ai-tool-editor__next").click
|
find(".ai-tool-editor__next").click
|
||||||
|
|
||||||
|
expect(page.first(".parameter-row__required-toggle").checked?).to eq(true)
|
||||||
|
expect(page.first(".parameter-row__enum-toggle").checked?).to eq(false)
|
||||||
|
|
||||||
find(".ai-tool-editor__test-button").click
|
find(".ai-tool-editor__test-button").click
|
||||||
|
|
||||||
expect(page).not_to have_button(".ai-tool-editor__delete")
|
expect(page).not_to have_button(".ai-tool-editor__delete")
|
||||||
|
@ -49,6 +53,12 @@ describe "AI Tool Management", type: :system do
|
||||||
|
|
||||||
expect(page).to have_content("Tool saved")
|
expect(page).to have_content("Tool saved")
|
||||||
|
|
||||||
|
last_tool = AiTool.order("id desc").limit(1).first
|
||||||
|
visit "/admin/plugins/discourse-ai/ai-tools/#{last_tool.id}"
|
||||||
|
|
||||||
|
expect(page.first(".parameter-row__required-toggle").checked?).to eq(true)
|
||||||
|
expect(page.first(".parameter-row__enum-toggle").checked?).to eq(false)
|
||||||
|
|
||||||
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
|
||||||
|
|
Loading…
Reference in New Issue