Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 4a762cd

Browse files
committed
Work in progress - import tool / persona
1 parent e9b1d73 commit 4a762cd

File tree

6 files changed

+154
-3
lines changed

6 files changed

+154
-3
lines changed

app/controllers/discourse_ai/admin/ai_tools_controller.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,38 @@ def export
3939
render_serialized(@ai_tool, AiCustomToolSerializer)
4040
end
4141

42+
def import
43+
existing_tool = AiTool.find_by(tool_name: ai_tool_params[:tool_name])
44+
force_update = params[:force].present? && params[:force] == "true"
45+
46+
if existing_tool && !force_update
47+
return(
48+
render_json_error "Tool with tool_name '#{ai_tool_params[:tool_name]}' already exists. Use force=true to overwrite.",
49+
status: :conflict
50+
)
51+
end
52+
53+
if existing_tool && force_update
54+
initial_attributes = existing_tool.attributes.dup
55+
if existing_tool.update(ai_tool_params)
56+
log_ai_tool_update(existing_tool, initial_attributes)
57+
render_serialized(existing_tool, AiCustomToolSerializer)
58+
else
59+
render_json_error existing_tool
60+
end
61+
else
62+
ai_tool = AiTool.new(ai_tool_params)
63+
ai_tool.created_by_id = current_user.id
64+
65+
if ai_tool.save
66+
log_ai_tool_creation(ai_tool)
67+
render_serialized(ai_tool, AiCustomToolSerializer, status: :created)
68+
else
69+
render_json_error ai_tool
70+
end
71+
end
72+
end
73+
4274
def update
4375
initial_attributes = @ai_tool.attributes.dup
4476

assets/javascripts/discourse/components/ai-tool-editor-form.gjs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { service } from "@ember/service";
66
import { and, gt } from "truth-helpers";
77
import Form from "discourse/components/form";
88
import { popupAjaxError } from "discourse/lib/ajax-error";
9+
import getURL from "discourse/lib/get-url";
910
import { i18n } from "discourse-i18n";
1011
import AiToolTestModal from "./modal/ai-tool-test-modal";
1112
import RagOptionsFk from "./rag-options-fk";
@@ -151,6 +152,12 @@ export default class AiToolEditorForm extends Component {
151152
: i18n("discourse_ai.rag.uploads.description");
152153
}
153154

155+
@action
156+
exportTool() {
157+
const exportUrl = `/admin/plugins/discourse-ai/ai-tools/${this.args.model.id}/export.json`;
158+
window.location.href = getURL(exportUrl);
159+
}
160+
154161
<template>
155162
<Form
156163
@onSubmit={{this.save}}
@@ -386,7 +393,11 @@ export default class AiToolEditorForm extends Component {
386393
@action={{this.openTestModal}}
387394
class="ai-tool-editor__test-button"
388395
/>
389-
396+
<form.Button
397+
@label="discourse_ai.tools.export"
398+
@action={{this.exportTool}}
399+
class="ai-tool-editor__export"
400+
/>
390401
<form.Button
391402
@label="discourse_ai.tools.delete"
392403
@icon="trash-can"

assets/javascripts/discourse/components/ai-tool-list-editor.gjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export default class AiToolListEditor extends Component {
4444
@descriptionLabel={{i18n "discourse_ai.tools.subheader_description"}}
4545
>
4646
<:actions>
47+
<DButton
48+
@translatedLabel={{i18n "discourse_ai.tools.import"}}
49+
@icon="upload"
50+
class="btn btn-small ai-tool-list-editor__import-button"
51+
/>
4752
<DMenu
4853
@triggerClass="btn-primary btn-small ai-tool-list-editor__new-button"
4954
@label={{i18n "discourse_ai.tools.new"}}

config/locales/client.en.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,8 @@ en:
445445
tools:
446446
back: "Back"
447447
short_title: "Tools"
448+
export: "Export"
449+
import: "Import"
448450
no_tools: "You have not created any tools yet"
449451
name: "Name"
450452
name_help: "Name will show up in the Discourse UI and is the short identifier you will use to find the tool in various settings, it should be distinct (it is required)"

config/routes.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,11 @@
8585
)
8686

8787
post "/ai-tools/:id/test", to: "discourse_ai/admin/ai_tools#test"
88-
get "/ai-tools/:id/export", to: "discourse_ai/admin/ai_tools#export"
88+
get "/ai-tools/:id/export", to: "discourse_ai/admin/ai_tools#export", format: :json
8989
post "/ai-tools/import", to: "discourse_ai/admin/ai_tools#import"
9090

9191
post "/ai-personas/:id/create-user", to: "discourse_ai/admin/ai_personas#create_user"
92-
get "/ai-personas/:id/export", to: "discourse_ai/admin/ai_personas#export"
92+
get "/ai-personas/:id/export", to: "discourse_ai/admin/ai_personas#export", format: :json
9393
post "/ai-personas/import", to: "discourse_ai/admin/ai_personas#import"
9494

9595
put "/ai-personas/:id/files/remove", to: "discourse_ai/admin/ai_personas#remove_file"

spec/requests/admin/ai_tools_controller_spec.rb

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,107 @@
4646
end
4747
end
4848

49+
describe "GET #export" do
50+
it "returns the ai_tool as JSON attachment" do
51+
get "/admin/plugins/discourse-ai/ai-tools/#{ai_tool.id}/export.json"
52+
53+
expect(response).to be_successful
54+
expect(response.headers["Content-Disposition"]).to eq(
55+
"attachment; filename=\"#{ai_tool.tool_name}.json\"",
56+
)
57+
expect(response.parsed_body["ai_tool"]["name"]).to eq(ai_tool.name)
58+
expect(response.parsed_body["ai_tool"]["tool_name"]).to eq(ai_tool.tool_name)
59+
expect(response.parsed_body["ai_tool"]["description"]).to eq(ai_tool.description)
60+
expect(response.parsed_body["ai_tool"]["parameters"]).to eq(ai_tool.parameters)
61+
end
62+
63+
it "returns 404 for non-existent ai_tool" do
64+
get "/admin/plugins/discourse-ai/ai-tools/99999/export.json"
65+
66+
expect(response).to have_http_status(:not_found)
67+
end
68+
end
69+
70+
describe "POST #import" do
71+
let(:import_attributes) do
72+
{
73+
name: "Imported Tool",
74+
tool_name: "imported_tool",
75+
description: "An imported test tool",
76+
parameters: [{ name: "query", type: "string", description: "perform a search" }],
77+
script: "function invoke(params) { return params; }",
78+
summary: "Imported tool summary",
79+
}
80+
end
81+
82+
it "imports a new AI tool successfully" do
83+
expect {
84+
post "/admin/plugins/discourse-ai/ai-tools/import.json",
85+
params: { ai_tool: import_attributes }.to_json,
86+
headers: {
87+
"CONTENT_TYPE" => "application/json",
88+
}
89+
}.to change(AiTool, :count).by(1)
90+
91+
expect(response).to have_http_status(:created)
92+
expect(response.parsed_body["ai_tool"]["name"]).to eq("Imported Tool")
93+
expect(response.parsed_body["ai_tool"]["tool_name"]).to eq("imported_tool")
94+
end
95+
96+
it "returns conflict error when tool with same tool_name exists without force" do
97+
_existing_tool =
98+
AiTool.create!(
99+
name: "Existing Tool",
100+
tool_name: "imported_tool",
101+
description: "Existing tool",
102+
script: "function invoke(params) { return 'existing'; }",
103+
summary: "Existing summary",
104+
created_by_id: admin.id,
105+
)
106+
107+
expect {
108+
post "/admin/plugins/discourse-ai/ai-tools/import.json",
109+
params: { ai_tool: import_attributes }.to_json,
110+
headers: {
111+
"CONTENT_TYPE" => "application/json",
112+
}
113+
}.not_to change(AiTool, :count)
114+
115+
expect(response).to have_http_status(:conflict)
116+
expect(response.parsed_body["errors"]).to include(
117+
"Tool with tool_name 'imported_tool' already exists. Use force=true to overwrite.",
118+
)
119+
end
120+
121+
it "force updates existing tool when force=true" do
122+
existing_tool =
123+
AiTool.create!(
124+
name: "Existing Tool",
125+
tool_name: "imported_tool",
126+
description: "Existing tool",
127+
script: "function invoke(params) { return 'existing'; }",
128+
summary: "Existing summary",
129+
created_by_id: admin.id,
130+
)
131+
132+
expect {
133+
post "/admin/plugins/discourse-ai/ai-tools/import.json?force=true",
134+
params: { ai_tool: import_attributes }.to_json,
135+
headers: {
136+
"CONTENT_TYPE" => "application/json",
137+
}
138+
}.not_to change(AiTool, :count)
139+
140+
expect(response).to have_http_status(:ok)
141+
expect(response.parsed_body["ai_tool"]["name"]).to eq("Imported Tool")
142+
expect(response.parsed_body["ai_tool"]["description"]).to eq("An imported test tool")
143+
144+
existing_tool.reload
145+
expect(existing_tool.name).to eq("Imported Tool")
146+
expect(existing_tool.description).to eq("An imported test tool")
147+
end
148+
end
149+
49150
describe "POST #create" do
50151
let(:valid_attributes) do
51152
{

0 commit comments

Comments
 (0)