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:
Sam 2024-08-29 16:05:38 +10:00 committed by GitHub
parent 943504049c
commit 41054c4fb8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 78 additions and 21 deletions

View File

@ -20,26 +20,25 @@ export default class AiTool extends RestModel {
return this.getProperties(CREATE_ATTRIBUTES);
}
workingCopy() {
const attrs = this.getProperties(CREATE_ATTRIBUTES);
attrs.parameters = new TrackedArray(
attrs.parameters?.map((p) => {
trackParameters(parameters) {
return new TrackedArray(
parameters?.map((p) => {
const parameter = new TrackedObject(p);
//Backwards-compatibility code.
// 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 {
if (parameter.enum && parameter.enum.length) {
parameter.enum = new TrackedArray(parameter.enum);
} else {
parameter.enum = null;
}
return parameter;
})
);
}
workingCopy() {
const attrs = this.getProperties(CREATE_ATTRIBUTES);
attrs.parameters = this.trackParameters(attrs.parameters);
return this.store.createRecord("ai-tool", attrs);
}
}

View File

@ -25,6 +25,7 @@ export default class AiToolEditor extends Component {
@service dialog;
@service modal;
@service toasts;
@service store;
@tracked isSaving = false;
@tracked editingModel = null;
@ -53,8 +54,9 @@ export default class AiToolEditor extends Component {
@action
configurePreset() {
this.selectedPreset = this.args.presets.findBy("preset_id", this.presetId);
this.editingModel = this.args.model.workingCopy();
this.editingModel.setProperties(this.selectedPreset);
this.editingModel = this.store
.createRecord("ai-tool", this.selectedPreset)
.workingCopy();
this.showDelete = false;
}

View File

@ -7,7 +7,6 @@ import DButton from "discourse/components/d-button";
import withEventValue from "discourse/helpers/with-event-value";
import I18n from "discourse-i18n";
import ComboBox from "select-kit/components/combo-box";
import and from "truth-helpers/helpers/and";
const PARAMETER_TYPES = [
{ name: "string", id: "string" },
@ -59,6 +58,9 @@ export default class AiToolParameterEditor extends Component {
@action
removeEnumValue(parameter, index) {
parameter.enum.splice(index, 1);
if (parameter.enum.length === 0) {
parameter.enum = null;
}
}
@action
@ -94,6 +96,7 @@ export default class AiToolParameterEditor extends Component {
{{on "input" (fn this.toggleRequired parameter)}}
checked={{parameter.required}}
type="checkbox"
class="parameter-row__required-toggle"
/>
{{I18n.t "discourse_ai.tools.parameter_required"}}
</label>
@ -101,8 +104,9 @@ export default class AiToolParameterEditor extends Component {
<label>
<input
{{on "input" (fn this.toggleEnum parameter)}}
checked={{and parameter.enum parameter.enum.length}}
checked={{parameter.enum}}
type="checkbox"
class="parameter-row__enum-toggle"
/>
{{I18n.t "discourse_ai.tools.parameter_enum"}}
</label>
@ -114,7 +118,7 @@ export default class AiToolParameterEditor extends Component {
/>
</div>
{{#if (and parameter.enum parameter.enum.length)}}
{{#if parameter.enum}}
<div class="parameter-enum-values">
{{#each parameter.enum as |enumValue enumIndex|}}
<div class="enum-value-row">

View File

@ -31,20 +31,35 @@ module DiscourseAi
parameters[:query].to_s
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
@last_num_results = 0
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 =
SiteSetting.all_settings.filter do |setting|
all_settings.filter do |setting|
name = setting[:setting].to_s.downcase
description = setting[:description].to_s.downcase
plugin = setting[:plugin].to_s.downcase
search_string = "#{name} #{description} #{plugin}"
terms.any? { |term| search_string.include?(term) }
terms_regexes.any? { |regex| search_string.match?(regex) }
end
if found.blank?

View File

@ -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(: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
SiteSetting.ai_bot_enabled = true
toggle_enabled_bots(bots: [llm_model])
end
def search_settings(query)
described_class.new({ query: query }, bot_user: bot_user, llm: llm)
def search_settings(query, mock: true)
search = described_class.new({ query: query }, bot_user: bot_user, llm: llm)
search.all_settings = fake_settings if mock
search
end
describe "#process" do
@ -21,8 +36,20 @@ RSpec.describe DiscourseAi::AiBot::Tools::SearchSettings do
expect(results[:rows]).to eq([])
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
results = search_settings("a").invoke
results = search_settings("a", mock: false).invoke
expect(results[:rows].length).to be > 30
expect(results[:rows][0].length).to eq(1)

View File

@ -22,6 +22,10 @@ describe "AI Tool Management", type: :system do
select_kit.select_row_by_value("exchange_rate")
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
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")
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"
tool_id = AiTool.order("id desc").limit(1).pluck(:id).first