From 80e5c36b5babe28a4b8fbc875048b79fbab5d622 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Mon, 15 Jun 2026 22:12:58 +0100 Subject: [PATCH 1/6] fix create tool in the AI chat --- .../policy/engine/PolicyExecutor.java | 8 +++++ .../policy/engine/PolicyExecutorTest.java | 35 +++++++++++++++++++ .../editor/src/core/services/buildApiUrl.ts | 9 +++++ .../components/chat/ChatContext.tsx | 4 +-- .../components/chat/ChatContext.tsx | 4 +-- 5 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 frontend/editor/src/core/services/buildApiUrl.ts diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyExecutor.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyExecutor.java index af9f8c7283..bf5bf36407 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyExecutor.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyExecutor.java @@ -116,6 +116,14 @@ private ToolResult executeStep( ToolResult r = callEndpoint(step, inputFiles, supportingFiles); files.addAll(r.files()); report = r.report(); + } else if (inputFiles.isEmpty()) { + // Generator tools (e.g. create-pdf-from-html-agent) take no input file and produce + // their output purely from parameters. Per-file dispatch would skip them entirely + // (zero files = zero iterations = zero calls), so call once with no file — matching + // the single call the multi-input branch already makes for an empty input list. + ToolResult r = callEndpoint(step, List.of(), supportingFiles); + files.addAll(r.files()); + report = r.report(); } else { for (Resource file : inputFiles) { ToolResult r = callEndpoint(step, List.of(file), supportingFiles); diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyExecutorTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyExecutorTest.java index d682c35cd7..9b10d9090e 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyExecutorTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyExecutorTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -149,6 +150,40 @@ void singleInputEndpointIsCalledOncePerFile() throws IOException { verify(internalApiClient, times(2)).post(eq(ROTATE), any()); } + @Test + void noInputGeneratorEndpointIsCalledOnceWithNoFile() throws IOException { + // A "create" workflow has no source documents: a generator tool (e.g. + // create-pdf-from-html-agent) produces its output purely from parameters. Per-file + // dispatch would skip it entirely (zero files = zero calls), so it must still run once. + String createPdf = "/api/v1/ai/tools/create-pdf-from-html-agent"; + when(toolMetadataService.isMultiInput(createPdf)).thenReturn(false); + when(toolMetadataService.shouldUnpackZipResponse(createPdf)).thenReturn(false); + stubEndpoint(createPdf, pdf("generated", "purchase-order.pdf")); + + PolicyExecutionResult result = + executor.execute( + definition( + new PipelineStep( + createPdf, + Map.of( + "htmlContent", + "

hi

", + "filename", + "purchase-order.pdf"))), + PolicyInputs.of(List.of()), + PolicyProgressListener.NOOP); + + assertEquals(1, result.files().size()); + assertEquals("purchase-order.pdf", result.files().get(0).getFilename()); + + @SuppressWarnings("unchecked") + ArgumentCaptor> bodyCaptor = + ArgumentCaptor.forClass(MultiValueMap.class); + verify(internalApiClient, times(1)).post(eq(createPdf), bodyCaptor.capture()); + // No document stream: the body carries only the generator's parameters, no fileInput. + assertNull(bodyCaptor.getValue().get("fileInput")); + } + @Test void zipResponseIsUnpackedIntoIndividualFiles() throws IOException { when(toolMetadataService.isMultiInput(SPLIT)).thenReturn(false); diff --git a/frontend/editor/src/core/services/buildApiUrl.ts b/frontend/editor/src/core/services/buildApiUrl.ts new file mode 100644 index 0000000000..c3b5bba17d --- /dev/null +++ b/frontend/editor/src/core/services/buildApiUrl.ts @@ -0,0 +1,9 @@ +import { getApiBaseUrl } from "@app/services/apiClientConfig"; + +// Join the API base URL with a path for `fetch`, collapsing slashes so a "/" +// base doesn't produce "//api/..." (a protocol-relative URL pointing at host "api"). +// getApiBaseUrl resolves per build via @app, so this works in web and desktop. +export function buildApiUrl(path: string): string { + const base = (getApiBaseUrl() || "").replace(/\/+$/, ""); + return `${base}/${path.replace(/^\/+/, "")}`; +} diff --git a/frontend/editor/src/proprietary/components/chat/ChatContext.tsx b/frontend/editor/src/proprietary/components/chat/ChatContext.tsx index 05577885b7..42a95f583e 100644 --- a/frontend/editor/src/proprietary/components/chat/ChatContext.tsx +++ b/frontend/editor/src/proprietary/components/chat/ChatContext.tsx @@ -10,7 +10,7 @@ import { useTranslation } from "react-i18next"; import { generateId } from "@app/utils/generateId"; import { useAllFiles, useFileActions } from "@app/contexts/FileContext"; import apiClient from "@app/services/apiClient"; -import { getApiBaseUrl } from "@app/services/apiClientConfig"; +import { buildApiUrl } from "@app/services/buildApiUrl"; import { getAuthHeaders } from "@app/services/apiClientSetup"; import { dispatchPaygLimitReached } from "@app/services/usageLimitBridge"; import { createChildStub } from "@app/contexts/file/fileActions"; @@ -520,7 +520,7 @@ export function ChatProvider({ children }: { children: ReactNode }) { formData.append(`conversationHistory[${i}].content`, message.content); }); const response = await fetch( - `${getApiBaseUrl()}/api/v1/ai/orchestrate/stream`, + buildApiUrl("/api/v1/ai/orchestrate/stream"), { method: "POST", body: formData, diff --git a/frontend/editor/src/prototypes/components/chat/ChatContext.tsx b/frontend/editor/src/prototypes/components/chat/ChatContext.tsx index dd7bd353d6..fb6ffc5397 100644 --- a/frontend/editor/src/prototypes/components/chat/ChatContext.tsx +++ b/frontend/editor/src/prototypes/components/chat/ChatContext.tsx @@ -9,7 +9,7 @@ import { import { useAllFiles, useFileActions } from "@app/contexts/FileContext"; import { generateId } from "@app/utils/generateId"; import apiClient from "@app/services/apiClient"; -import { getApiBaseUrl } from "@app/services/apiClientConfig"; +import { buildApiUrl } from "@app/services/buildApiUrl"; import { getAuthHeaders } from "@app/services/apiClientSetup"; import { createChildStub } from "@app/contexts/file/fileActions"; import { @@ -442,7 +442,7 @@ export function ChatProvider({ children }: { children: ReactNode }) { }); const response = await fetch( - `${getApiBaseUrl()}/api/v1/ai/orchestrate/stream`, + buildApiUrl("/api/v1/ai/orchestrate/stream"), { method: "POST", body: formData, From 99255836ee3df68111cb6d65f88c960540129eb5 Mon Sep 17 00:00:00 2001 From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:37:20 +0100 Subject: [PATCH 2/6] Update app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyExecutor.java remove verbose comments Co-authored-by: James Brunton --- .../software/proprietary/policy/engine/PolicyExecutor.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyExecutor.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyExecutor.java index bf5bf36407..fe88b40391 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyExecutor.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyExecutor.java @@ -117,10 +117,6 @@ private ToolResult executeStep( files.addAll(r.files()); report = r.report(); } else if (inputFiles.isEmpty()) { - // Generator tools (e.g. create-pdf-from-html-agent) take no input file and produce - // their output purely from parameters. Per-file dispatch would skip them entirely - // (zero files = zero iterations = zero calls), so call once with no file — matching - // the single call the multi-input branch already makes for an empty input list. ToolResult r = callEndpoint(step, List.of(), supportingFiles); files.addAll(r.files()); report = r.report(); From bd46289ed3d29b54d06dd872d422fbdd8a37f9da Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Thu, 18 Jun 2026 17:34:59 +0100 Subject: [PATCH 3/6] drop redundant buildApiUrl.ts --- frontend/editor/src/core/services/buildApiUrl.ts | 9 --------- .../src/proprietary/components/chat/ChatContext.tsx | 3 +-- .../src/prototypes/components/chat/ChatContext.tsx | 3 +-- 3 files changed, 2 insertions(+), 13 deletions(-) delete mode 100644 frontend/editor/src/core/services/buildApiUrl.ts diff --git a/frontend/editor/src/core/services/buildApiUrl.ts b/frontend/editor/src/core/services/buildApiUrl.ts deleted file mode 100644 index c3b5bba17d..0000000000 --- a/frontend/editor/src/core/services/buildApiUrl.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { getApiBaseUrl } from "@app/services/apiClientConfig"; - -// Join the API base URL with a path for `fetch`, collapsing slashes so a "/" -// base doesn't produce "//api/..." (a protocol-relative URL pointing at host "api"). -// getApiBaseUrl resolves per build via @app, so this works in web and desktop. -export function buildApiUrl(path: string): string { - const base = (getApiBaseUrl() || "").replace(/\/+$/, ""); - return `${base}/${path.replace(/^\/+/, "")}`; -} diff --git a/frontend/editor/src/proprietary/components/chat/ChatContext.tsx b/frontend/editor/src/proprietary/components/chat/ChatContext.tsx index 42a95f583e..38cb04d4a9 100644 --- a/frontend/editor/src/proprietary/components/chat/ChatContext.tsx +++ b/frontend/editor/src/proprietary/components/chat/ChatContext.tsx @@ -10,7 +10,6 @@ import { useTranslation } from "react-i18next"; import { generateId } from "@app/utils/generateId"; import { useAllFiles, useFileActions } from "@app/contexts/FileContext"; import apiClient from "@app/services/apiClient"; -import { buildApiUrl } from "@app/services/buildApiUrl"; import { getAuthHeaders } from "@app/services/apiClientSetup"; import { dispatchPaygLimitReached } from "@app/services/usageLimitBridge"; import { createChildStub } from "@app/contexts/file/fileActions"; @@ -520,7 +519,7 @@ export function ChatProvider({ children }: { children: ReactNode }) { formData.append(`conversationHistory[${i}].content`, message.content); }); const response = await fetch( - buildApiUrl("/api/v1/ai/orchestrate/stream"), + apiClient.getUri({ url: "/api/v1/ai/orchestrate/stream" }), { method: "POST", body: formData, diff --git a/frontend/editor/src/prototypes/components/chat/ChatContext.tsx b/frontend/editor/src/prototypes/components/chat/ChatContext.tsx index fb6ffc5397..0fbd2a2f9d 100644 --- a/frontend/editor/src/prototypes/components/chat/ChatContext.tsx +++ b/frontend/editor/src/prototypes/components/chat/ChatContext.tsx @@ -9,7 +9,6 @@ import { import { useAllFiles, useFileActions } from "@app/contexts/FileContext"; import { generateId } from "@app/utils/generateId"; import apiClient from "@app/services/apiClient"; -import { buildApiUrl } from "@app/services/buildApiUrl"; import { getAuthHeaders } from "@app/services/apiClientSetup"; import { createChildStub } from "@app/contexts/file/fileActions"; import { @@ -442,7 +441,7 @@ export function ChatProvider({ children }: { children: ReactNode }) { }); const response = await fetch( - buildApiUrl("/api/v1/ai/orchestrate/stream"), + apiClient.getUri({ url: "/api/v1/ai/orchestrate/stream" }), { method: "POST", body: formData, From 1a8b7563d43ed46e51705d075cfa3b1572a74525 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Thu, 18 Jun 2026 17:54:16 +0100 Subject: [PATCH 4/6] add an in chat appended message for create responses --- .../editor/public/locales/en-US/translation.toml | 1 + .../src/proprietary/components/chat/ChatContext.tsx | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 01a9eceac1..60afc7a56d 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -2709,6 +2709,7 @@ splitOne = "Split this document" [chat.responses] cannot_continue = "Something went wrong and I can't continue." cannot_do = "I'm unable to do that." +create_verify = "AI-generated documents can include mistakes, so double-check the details before sharing." done = "Done." need_clarification = "Could you clarify your request?" not_found = "I couldn't find the requested information." diff --git a/frontend/editor/src/proprietary/components/chat/ChatContext.tsx b/frontend/editor/src/proprietary/components/chat/ChatContext.tsx index 38cb04d4a9..32e09ac6c3 100644 --- a/frontend/editor/src/proprietary/components/chat/ChatContext.tsx +++ b/frontend/editor/src/proprietary/components/chat/ChatContext.tsx @@ -200,6 +200,8 @@ function isPaygLimitCode(code: string | null | undefined): boolean { return code != null && PAYG_LIMIT_CODES.has(code); } +const CREATE_PDF_AGENT_TOOL = "/api/v1/ai/tools/create-pdf-from-html-agent"; + interface ChatState { messages: ChatMessage[]; isLoading: boolean; @@ -608,12 +610,20 @@ export function ChatProvider({ children }: { children: ReactNode }) { if (isLimit) { dispatchPaygLimitReached(data.errorSubscribed ?? null); } + // A create turn creates a whole document, so nudge the user to check its details + const createdDocument = + !isLimit && toolsUsed.includes(CREATE_PDF_AGENT_TOOL); const replyContent = isLimit ? t( "chat.responses.usage_limit_reached", "You've reached your usage limit. Check your plan options to keep going.", ) - : formatWorkflowResponse(data, t); + : createdDocument + ? `${formatWorkflowResponse(data, t)}\n\n${t( + "chat.responses.create_verify", + "AI-generated documents can include mistakes, so double-check the details before sharing.", + )}` + : formatWorkflowResponse(data, t); dispatch({ type: "ADD_MESSAGE", message: { From fe6bd91ab5c63bae1b9bf7af1caca52e14ca1e14 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Thu, 18 Jun 2026 18:14:00 +0100 Subject: [PATCH 5/6] remove redundant warning --- .../src/proprietary/components/chat/ChatContext.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/frontend/editor/src/proprietary/components/chat/ChatContext.tsx b/frontend/editor/src/proprietary/components/chat/ChatContext.tsx index 8ce05a0931..5900000879 100644 --- a/frontend/editor/src/proprietary/components/chat/ChatContext.tsx +++ b/frontend/editor/src/proprietary/components/chat/ChatContext.tsx @@ -201,8 +201,6 @@ function isPaygLimitCode(code: string | null | undefined): boolean { return code != null && PAYG_LIMIT_CODES.has(code); } -const CREATE_PDF_AGENT_TOOL = "/api/v1/ai/tools/create-pdf-from-html-agent"; - interface ChatState { messages: ChatMessage[]; isLoading: boolean; @@ -613,20 +611,12 @@ export function ChatProvider({ children }: { children: ReactNode }) { if (isLimit) { dispatchPaygLimitReached(data.errorSubscribed ?? null); } - // A create turn creates a whole document, so nudge the user to check its details - const createdDocument = - !isLimit && toolsUsed.includes(CREATE_PDF_AGENT_TOOL); const replyContent = isLimit ? t( "chat.responses.usage_limit_reached", "You've reached your usage limit. Check your plan options to keep going.", ) - : createdDocument - ? `${formatWorkflowResponse(data, t)}\n\n${t( - "chat.responses.create_verify", - "AI-generated documents can include mistakes, so double-check the details before sharing.", - )}` - : formatWorkflowResponse(data, t); + : formatWorkflowResponse(data, t); dispatch({ type: "ADD_MESSAGE", message: { From 5ca2f5d5e800d80f43058fcd20d6b9a7a7dfe9d5 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Thu, 18 Jun 2026 18:14:18 +0100 Subject: [PATCH 6/6] remove redundant warning --- frontend/editor/public/locales/en-US/translation.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index a04daabcae..46dd123b3f 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -2709,7 +2709,6 @@ splitOne = "Split this document" [chat.responses] cannot_continue = "Something went wrong and I can't continue." cannot_do = "I'm unable to do that." -create_verify = "AI-generated documents can include mistakes, so double-check the details before sharing." done = "Done." need_clarification = "Could you clarify your request?" not_found = "I couldn't find the requested information."