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 <jbrunton96@gmail.com>
This commit is contained in:
EthanHealy01
2026-06-19 17:06:28 +00:00
committed by GitHub
co-authored by James Brunton
parent 3870ac3d7d
commit 1a770af47c
2 changed files with 39 additions and 0 deletions
@@ -116,6 +116,10 @@ public class PolicyExecutor {
ToolResult r = callEndpoint(step, inputFiles, supportingFiles); ToolResult r = callEndpoint(step, inputFiles, supportingFiles);
files.addAll(r.files()); files.addAll(r.files());
report = r.report(); report = r.report();
} else if (inputFiles.isEmpty()) {
ToolResult r = callEndpoint(step, List.of(), supportingFiles);
files.addAll(r.files());
report = r.report();
} else { } else {
for (Resource file : inputFiles) { for (Resource file : inputFiles) {
ToolResult r = callEndpoint(step, List.of(file), supportingFiles); ToolResult r = callEndpoint(step, List.of(file), supportingFiles);
@@ -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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; 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.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@@ -149,6 +150,40 @@ class PolicyExecutorTest {
verify(internalApiClient, times(2)).post(eq(ROTATE), any()); 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",
"<p>hi</p>",
"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<MultiValueMap<String, Object>> 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 @Test
void zipResponseIsUnpackedIntoIndividualFiles() throws IOException { void zipResponseIsUnpackedIntoIndividualFiles() throws IOException {
when(toolMetadataService.isMultiInput(SPLIT)).thenReturn(false); when(toolMetadataService.isMultiInput(SPLIT)).thenReturn(false);