Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
EthanHealy01 marked this conversation as resolved.
Outdated
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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",
"<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
void zipResponseIsUnpackedIntoIndividualFiles() throws IOException {
when(toolMetadataService.isMultiInput(SPLIT)).thenReturn(false);
Expand Down
9 changes: 9 additions & 0 deletions frontend/editor/src/core/services/buildApiUrl.ts
Original file line number Diff line number Diff line change
@@ -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(/^\/+/, "")}`;
}
Comment thread
EthanHealy01 marked this conversation as resolved.
Outdated
Comment thread
EthanHealy01 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
Loading