FIX: Make sure custom tool enums follow json-schema. (#718)

Enums didn't work as expected because we the dialect couldn't translate
them correctly. It doesn't understand what "enum_values" is.
This commit is contained in:
Roman Rizzi 2024-07-16 14:23:17 -03:00 committed by GitHub
parent 0a8195242b
commit f328b81c78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 76 additions and 24 deletions

View File

@ -81,7 +81,7 @@ module DiscourseAi
:description, :description,
:script, :script,
:summary, :summary,
parameters: [:name, :type, :description, :required, :enum, enum_values: []], parameters: [:name, :type, :description, :required, enum: []],
) )
end end
end end

View File

@ -27,9 +27,13 @@ export default class AiTool extends RestModel {
attrs.parameters?.map((p) => { attrs.parameters?.map((p) => {
const parameter = new TrackedObject(p); const parameter = new TrackedObject(p);
//Backwards-compatibility code.
// TODO(roman): Remove aug 2024. Leave only else clause.
if (parameter.enum_values) { if (parameter.enum_values) {
parameter.enumValues = new TrackedArray(parameter.enum_values); parameter.enum = new TrackedArray(parameter.enum_values);
delete parameter.enum_values; delete parameter.enum_values;
} else {
parameter.enum = new TrackedArray(parameter.enum);
} }
return parameter; return parameter;

View File

@ -71,13 +71,6 @@ export default class AiToolEditor extends Component {
"summary" "summary"
); );
for (const p of data.parameters) {
if (p.enumValues) {
p.enum_values = p.enumValues;
delete p.enumValues;
}
}
await this.args.model.save(data); await this.args.model.save(data);
this.toasts.success({ this.toasts.success({
@ -124,7 +117,7 @@ export default class AiToolEditor extends Component {
<template> <template>
<BackButton <BackButton
@route="adminPlugins.show.discourse-ai-tools" @route="adminPlugins.show.discourse-ai-tools"
@label="discourse_ai.ai_tool.back" @label="discourse_ai.tools.back"
/> />
<form <form

View File

@ -7,6 +7,7 @@ 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" },
@ -24,8 +25,7 @@ export default class AiToolParameterEditor extends Component {
description: "", description: "",
type: "string", type: "string",
required: false, required: false,
enum: false, enum: null,
enumValues: null,
}) })
); );
} }
@ -43,26 +43,27 @@ export default class AiToolParameterEditor extends Component {
@action @action
toggleEnum(parameter) { toggleEnum(parameter) {
parameter.enum = !parameter.enum; if (parameter.enum) {
if (!parameter.enum) { parameter.enum = null;
parameter.enumValues = null; } else {
this.addEnumValue(parameter);
} }
} }
@action @action
addEnumValue(parameter) { addEnumValue(parameter) {
parameter.enumValues ||= new TrackedArray(); parameter.enum ||= new TrackedArray();
parameter.enumValues.push(""); parameter.enum.push("");
} }
@action @action
removeEnumValue(parameter, index) { removeEnumValue(parameter, index) {
parameter.enumValues.splice(index, 1); parameter.enum.splice(index, 1);
} }
@action @action
updateEnumValue(parameter, index, event) { updateEnumValue(parameter, index, event) {
parameter.enumValues[index] = event.target.value; parameter.enum[index] = event.target.value;
} }
<template> <template>
@ -100,7 +101,7 @@ export default class AiToolParameterEditor extends Component {
<label> <label>
<input <input
{{on "input" (fn this.toggleEnum parameter)}} {{on "input" (fn this.toggleEnum parameter)}}
checked={{parameter.enum}} checked={{and parameter.enum parameter.enum.length}}
type="checkbox" type="checkbox"
/> />
{{I18n.t "discourse_ai.tools.parameter_enum"}} {{I18n.t "discourse_ai.tools.parameter_enum"}}
@ -113,9 +114,9 @@ export default class AiToolParameterEditor extends Component {
/> />
</div> </div>
{{#if parameter.enum}} {{#if (and parameter.enum parameter.enum.length)}}
<div class="parameter-enum-values"> <div class="parameter-enum-values">
{{#each parameter.enumValues as |enumValue enumIndex|}} {{#each parameter.enum as |enumValue enumIndex|}}
<div class="enum-value-row"> <div class="enum-value-row">
<input <input
{{on "change" (fn this.updateEnumValue parameter enumIndex)}} {{on "change" (fn this.updateEnumValue parameter enumIndex)}}

View File

@ -184,6 +184,7 @@ en:
remove: "Remove upload" remove: "Remove upload"
tools: tools:
back: "Back"
short_title: "Tools" short_title: "Tools"
new: "New Tool" new: "New Tool"
name: "Name" name: "Name"

View File

@ -18,7 +18,10 @@ module DiscourseAi
name = p[:name] name = p[:name]
memo[:required] << name if p[:required] memo[:required] << name if p[:required]
memo[:properties][name] = p.except(:name, :required, :item_type) except = %i[name required item_type]
except << :enum if p[:enum].blank?
memo[:properties][name] = p.except(*except)
memo[:properties][name][:items] = { type: p[:item_type] } if p[:item_type] memo[:properties][name][:items] = { type: p[:item_type] } if p[:item_type]
memo memo

View File

@ -7,7 +7,15 @@ RSpec.describe DiscourseAi::Admin::AiToolsController do
name: "Test Tool", name: "Test Tool",
description: "A test tool", description: "A test tool",
script: "function invoke(params) { return params; }", script: "function invoke(params) { return params; }",
parameters: [{ name: "query", type: "string", description: "perform a search" }], parameters: [
{
name: "unit",
type: "string",
description: "the unit of measurement celcius c or fahrenheit f",
enum: %w[c f],
required: true,
},
],
summary: "Test tool summary", summary: "Test tool summary",
created_by_id: -1, created_by_id: -1,
) )
@ -58,6 +66,27 @@ RSpec.describe DiscourseAi::Admin::AiToolsController do
expect(response).to have_http_status(:created) expect(response).to have_http_status(:created)
expect(response.parsed_body["ai_tool"]["name"]).to eq("Test Tool") expect(response.parsed_body["ai_tool"]["name"]).to eq("Test Tool")
end end
context "when the parameter is a enum" do
it "creates the tool with the correct parameters" do
attrs = valid_attributes
attrs[:parameters] = [attrs[:parameters].first.merge(enum: %w[c f])]
expect {
post "/admin/plugins/discourse-ai/ai-tools.json",
params: { ai_tool: valid_attributes }.to_json,
headers: {
"CONTENT_TYPE" => "application/json",
}
}.to change(AiTool, :count).by(1)
expect(response).to have_http_status(:created)
expect(response.parsed_body.dig("ai_tool", "parameters", 0, "enum")).to contain_exactly(
"c",
"f",
)
end
end
end end
describe "PUT #update" do describe "PUT #update" do
@ -72,6 +101,27 @@ RSpec.describe DiscourseAi::Admin::AiToolsController do
expect(response).to be_successful expect(response).to be_successful
expect(ai_tool.reload.name).to eq("Updated Tool") expect(ai_tool.reload.name).to eq("Updated Tool")
end end
context "when updating an enum parameters" do
it "updates the enum fixed values" do
put "/admin/plugins/discourse-ai/ai-tools/#{ai_tool.id}.json",
params: {
ai_tool: {
parameters: [
{
name: "unit",
type: "string",
description: "the unit of measurement celcius c or fahrenheit f",
enum: %w[g d],
},
],
},
}
expect(response).to be_successful
expect(ai_tool.reload.parameters.dig(0, "enum")).to contain_exactly("g", "d")
end
end
end end
describe "DELETE #destroy" do describe "DELETE #destroy" do