From 1a770af47c53e61ba9cd600b9ed8eb8edd09d421 Mon Sep 17 00:00:00 2001 From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:06:28 +0100 Subject: [PATCH] fix create tool in the AI chat (#6673) AI PDF creation ("create a PDF for me") has been broken since the Policies backend (#6527) introduced PolicyExecutor as the tool execution pipeline. PolicyExecutor runs normal single-input tools with a per-file loop, but generator tools like `create-pdf-from-html-agent` take no input file and build their output purely from parameters. With zero input files the loop ran zero times, so the endpoint was never called and the step silently produced nothing. The chat reported success ("Created Purchase Order") while no document ever appeared. This adds an `else if (inputFiles.isEmpty())` branch so a generator tool is called once with an empty file list, matching what the multi-input branch already does for an empty input. Two files changed: the one-line-ish fix in `PolicyExecutor`, and a regression test covering the no-input case. --------- Co-authored-by: James Brunton --- .../policy/engine/PolicyExecutor.java | 4 +++ .../policy/engine/PolicyExecutorTest.java | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+) 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..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 @@ -116,6 +116,10 @@ public class PolicyExecutor { ToolResult r = callEndpoint(step, inputFiles, supportingFiles); files.addAll(r.files()); report = r.report(); + } else if (inputFiles.isEmpty()) { + 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 @@ package stirling.software.proprietary.policy.engine; 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 @@ class PolicyExecutorTest { 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);