import { ApiConfig, HttpMethod, Integration, Pagination, ServiceMetadata } from "@superglue/shared";
import { LLMMessage, LLMToolWithContext } from "../llm/llm-base-model.js";
import { LanguageModel } from "../llm/llm-base-model.js";
import {
getWebSearchTool,
inspectSourceDataToolDefinition,
searchDocumentationToolDefinition,
} from "../llm/llm-tools.js";
import { transformData } from "../utils/helpers.js";
import { z } from "zod";
type GenerateStepConfigInput = {
retryCount: number;
messages: LLMMessage[];
sourceData: any;
integration: Integration;
metadata: ServiceMetadata;
};
export interface GenerateStepConfigResult {
success: boolean;
error?: string;
config?: Partial<ApiConfig>;
dataSelector?: string;
messages?: LLMMessage[];
}
export interface BuildSourceDataInput {
stepInput: Record<string, any>;
credentials: Record<string, any>;
dataSelector?: string;
currentItem?: any;
integrationUrlHost: string;
paginationPageSize?: string | number;
}
const stepConfigSchema = z.object({
dataSelector: z
.string()
.describe(
"JavaScript function that returns OBJECT for direct execution or ARRAY for loop execution. If returns OBJECT (including {}), step executes once with object as currentItem. If returns ARRAY, step executes once per array item. Examples: (sourceData) => ({ userId: sourceData.userId }) OR (sourceData) => sourceData.getContacts.data.filter(c => c.active)",
),
apiConfig: z
.object({
urlHost: z
.string()
.describe("The base URL host (e.g., https://api.example.com). Must not be empty."),
urlPath: z.string().describe("The API endpoint path (e.g., /v1/users)."),
method: z
.enum(["GET", "POST", "PUT", "DELETE", "PATCH"] as [string, ...string[]])
.describe("HTTP method: GET, POST, PUT, DELETE, or PATCH"),
queryParams: z
.array(
z.object({
key: z.string(),
value: z.string(),
}),
)
.optional()
.describe(
"Query parameters as key-value pairs. If pagination is configured, ensure you have included the right pagination parameters here or in the body.",
),
headers: z
.array(
z.object({
key: z.string(),
value: z.string(),
}),
)
.optional()
.describe(
"HTTP headers as key-value pairs. Use <<variable>> syntax for dynamic values or JavaScript expressions",
),
body: z
.string()
.optional()
.describe(
"Request body. Use <<variable>> syntax for dynamic values. If pagination is configured, ensure you have included the right pagination parameters here or in the queryParams.",
),
pagination: z
.object({
type: z.enum(["OFFSET_BASED", "PAGE_BASED", "CURSOR_BASED"]),
pageSize: z
.string()
.describe(
"Number of items per page (e.g., '50', '100'). Once set, this becomes available as <<limit>> (same as pageSize).",
),
cursorPath: z
.string()
.describe(
'If cursor_based: The JSONPath to the cursor in the response. If not, set this to ""',
),
stopCondition: z
.string()
.describe(
"REQUIRED: JavaScript function that determines when to stop pagination. This is the primary control for pagination. Format: (response, pageInfo) => boolean. The pageInfo object contains: page (number), offset (number), cursor (any), totalFetched (number). response is the axios response object, access response data via response.data. Return true to STOP. E.g. (response, pageInfo) => !response.data.pagination.has_more",
),
})
.optional()
.describe(
"OPTIONAL: Only configure if you are using pagination variables in the URL, headers, or body. For OFFSET_BASED, ALWAYS use <<offset>>. If PAGE_BASED, ALWAYS use <<page>>. If CURSOR_BASED, ALWAYS use <<cursor>>.",
),
})
.describe("Complete API configuration to execute"),
});
export async function generateStepConfig({
retryCount,
messages,
sourceData,
integration,
metadata,
}: GenerateStepConfigInput): Promise<GenerateStepConfigResult> {
const temperature = Math.min(retryCount * 0.1, 1);
const webSearchTool = getWebSearchTool();
const tools: LLMToolWithContext[] = [
{
toolDefinition: searchDocumentationToolDefinition,
toolContext: { orgId: metadata.orgId, traceId: metadata.traceId, integration },
maxUses: 1,
},
{ toolDefinition: inspectSourceDataToolDefinition, toolContext: { sourceData }, maxUses: 3 },
];
if (webSearchTool) {
tools.push({
toolDefinition: { web_search: webSearchTool },
toolContext: {},
});
}
const generateStepConfigResult = await LanguageModel.generateObject<
z.infer<typeof stepConfigSchema>
>({
messages,
schema: z.toJSONSchema(stepConfigSchema),
temperature,
tools,
metadata,
});
if (!generateStepConfigResult.success) {
return {
success: false,
error: generateStepConfigResult.response as string,
messages: generateStepConfigResult.messages,
};
}
const generatedConfig = generateStepConfigResult.response.apiConfig;
const generatedDataSelector = generateStepConfigResult.response.dataSelector;
if (!generatedConfig) {
return {
success: false,
error: "LLM returned invalid response: stepConfig is missing",
messages: generateStepConfigResult.messages,
};
}
const config: Partial<ApiConfig> = {
urlHost: generatedConfig.urlHost,
urlPath: generatedConfig.urlPath,
method: generatedConfig.method as HttpMethod,
queryParams: generatedConfig.queryParams
? Object.fromEntries(generatedConfig.queryParams.map((p: any) => [p.key, p.value]))
: undefined,
headers: generatedConfig.headers
? Object.fromEntries(generatedConfig.headers.map((p: any) => [p.key, p.value]))
: undefined,
body: generatedConfig.body,
pagination: generatedConfig.pagination as Pagination,
};
return {
success: true,
config: config,
dataSelector: generatedDataSelector,
messages: generateStepConfigResult.messages,
};
}
export async function buildSourceData(input: BuildSourceDataInput): Promise<Record<string, any>> {
const {
stepInput,
credentials,
dataSelector,
currentItem: providedCurrentItem,
integrationUrlHost,
paginationPageSize,
} = input;
let currentItem: any = providedCurrentItem ?? null;
if (currentItem === null && dataSelector && stepInput) {
const result = await transformData(stepInput, dataSelector);
if (result.success && result.data !== undefined) {
currentItem = Array.isArray(result.data) ? result.data[0] : result.data;
}
}
const sourceData: Record<string, any> = {
...stepInput,
...credentials,
currentItem,
};
const isHttp = integrationUrlHost?.startsWith("http");
if (isHttp) {
const pageSize = String(paginationPageSize ?? 50);
sourceData.page = 1;
sourceData.offset = 0;
sourceData.cursor = null;
sourceData.limit = pageSize;
sourceData.pageSize = pageSize;
}
return sourceData;
}