diff --git a/app/models/ai_tool.rb b/app/models/ai_tool.rb index 6ff5582a..ec0d3ec6 100644 --- a/app/models/ai_tool.rb +++ b/app/models/ai_tool.rb @@ -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. * diff --git a/lib/personas/tool_runner.rb b/lib/personas/tool_runner.rb index 6ce68476..309f6d25 100644 --- a/lib/personas/tool_runner.rb +++ b/lib/personas/tool_runner.rb @@ -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 diff --git a/spec/models/ai_tool_spec.rb b/spec/models/ai_tool_spec.rb index 56de5de5..4e6ba5a0 100644 --- a/spec/models/ai_tool_spec.rb +++ b/spec/models/ai_tool_spec.rb @@ -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