FEATURE: support upload.getUrl in custom tools (#1384)

* FEATURE: support upload.getUrl in custom tools

Some tools need to share images with an API. A common pattern
is for APIs to expect a URL.

This allows converting upload://123123 to a proper CDN friendly
URL from within a custom tool

* no support for secure uploads, so be explicit about it.
This commit is contained in:
Sam 2025-05-30 15:47:07 +10:00 committed by GitHub
parent 9f43df0302
commit 77ae426d95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 80 additions and 0 deletions

View File

@ -142,6 +142,7 @@ class AiTool < ActiveRecord::Base
* base_64_content (string): Base64 encoded content of the file.
* Returns: { id: number, url: string, short_url: string } - Details of the created upload record.
*
* upload.getUrl(shortUrl): Given a short URL, eg upload://12345, returns the full CDN friendly URL of the upload.
* 5. chain
* Controls the execution flow.
*

View File

@ -72,6 +72,7 @@ module DiscourseAi
const upload = {
create: _upload_create,
getUrl: _upload_get_url,
}
const chain = {
@ -570,6 +571,24 @@ module DiscourseAi
end
def attach_upload(mini_racer_context)
mini_racer_context.attach(
"_upload_get_url",
->(short_url) do
in_attached_function do
return nil if short_url.blank?
sha1 = Upload.sha1_from_short_url(short_url)
return nil if sha1.blank?
upload = Upload.find_by(sha1: sha1)
return nil if upload.nil?
# TODO we may need to introduce an API to unsecure, secure uploads
return nil if upload.secure?
GlobalPath.full_cdn_url(upload.url)
end
end,
)
mini_racer_context.attach(
"_upload_create",
->(filename, base_64_content) do

View File

@ -675,4 +675,64 @@ RSpec.describe AiTool do
expect(ai_persona.temperature).to eq(0.5)
end
end
describe "upload URL resolution" do
it "can resolve upload short URLs to public URLs" do
upload =
Fabricate(
:upload,
sha1: "abcdef1234567890abcdef1234567890abcdef12",
url: "/uploads/default/original/1X/test.jpg",
original_filename: "test.jpg",
)
script = <<~JS
function invoke(params) {
return upload.getUrl(params.short_url);
}
JS
tool = create_tool(script: script)
runner = tool.runner({ "short_url" => upload.short_url }, llm: nil, bot_user: nil)
result = runner.invoke
expect(result).to eq(GlobalPath.full_cdn_url(upload.url))
end
it "returns null for invalid upload short URLs" do
script = <<~JS
function invoke(params) {
return upload.getUrl(params.short_url);
}
JS
tool = create_tool(script: script)
runner = tool.runner({ "short_url" => "upload://invalid" }, llm: nil, bot_user: nil)
result = runner.invoke
expect(result).to be_nil
end
it "returns null for non-existent uploads" do
script = <<~JS
function invoke(params) {
return upload.getUrl(params.short_url);
}
JS
tool = create_tool(script: script)
runner =
tool.runner(
{ "short_url" => "upload://hwmUkTAL9mwhQuRMLsXw6tvDi5C.jpeg" },
llm: nil,
bot_user: nil,
)
result = runner.invoke
expect(result).to be_nil
end
end
end