Skip to content

Commit 22b204a

Browse files
committed
[feat] Introduce Loop tasks: Batch, ForEach, Map, Reduce, and While
- Added BatchTask for processing arrays in configurable batches, supporting parallel and sequential execution modes. - Implemented ForEachTask to iterate over arrays and execute workflows for each element, optimized for side effects. - Introduced MapTask for transforming arrays with configurable result collection and order preservation. - Added ReduceTask for sequentially processing array elements with an accumulator, allowing for complex reductions. - Implemented WhileTask for looping until a specified condition is met, with configurable maximum iterations and chaining of outputs. - Enhanced Workflow interface to support new loop tasks, improving task graph management and execution flow. - Updated tests to validate the functionality of new tasks and their integration within workflows.
1 parent 42a5433 commit 22b204a

10 files changed

Lines changed: 4444 additions & 344 deletions

File tree

packages/task-graph/src/task-graph/Workflow.ts

Lines changed: 554 additions & 343 deletions
Large diffs are not rendered by default.
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Steven Roussey <sroussey@gmail.com>
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { type DataPortSchema } from "@workglow/util";
8+
import { TaskGraph } from "../task-graph/TaskGraph";
9+
import { PROPERTY_ARRAY } from "../task-graph/TaskGraphRunner";
10+
import {
11+
CreateEndLoopWorkflow,
12+
CreateLoopWorkflow,
13+
Workflow,
14+
} from "../task-graph/Workflow";
15+
import { IteratorTask, IteratorTaskConfig } from "./IteratorTask";
16+
import type { TaskInput, TaskOutput, TaskTypeName } from "./TaskTypes";
17+
18+
/**
19+
* Configuration for BatchTask.
20+
*/
21+
export interface BatchTaskConfig extends IteratorTaskConfig {
22+
/**
23+
* Number of items per batch.
24+
* @default 10
25+
*/
26+
readonly batchSize?: number;
27+
28+
/**
29+
* Whether to flatten results from all batches into a single array.
30+
* When false, results are grouped by batch.
31+
* @default true
32+
*/
33+
readonly flattenResults?: boolean;
34+
35+
/**
36+
* Whether to execute batches in parallel or sequentially.
37+
* @default "sequential"
38+
*/
39+
readonly batchExecutionMode?: "parallel" | "sequential";
40+
}
41+
42+
/**
43+
* BatchTask processes an array in configurable chunks/batches.
44+
*
45+
* This task is useful for:
46+
* - Rate-limited API calls that accept multiple items
47+
* - Memory-constrained processing
48+
* - Progress tracking at batch granularity
49+
*
50+
* ## Features
51+
*
52+
* - Groups array into chunks of batchSize
53+
* - Runs inner workflow per batch (receives array of items)
54+
* - Configurable batch and within-batch execution
55+
* - Optional result flattening
56+
*
57+
* ## Usage
58+
*
59+
* ```typescript
60+
* // Process in batches of 10
61+
* workflow
62+
* .input({ documents: [...100 docs...] })
63+
* .batch({ batchSize: 10 })
64+
* .bulkEmbed()
65+
* .bulkStore()
66+
* .endBatch()
67+
*
68+
* // Sequential batches for rate limiting
69+
* workflow
70+
* .batch({ batchSize: 5, batchExecutionMode: "sequential" })
71+
* .apiCall()
72+
* .endBatch()
73+
* ```
74+
*
75+
* @template Input - The input type containing the array to batch
76+
* @template Output - The output type (collected batch results)
77+
* @template Config - The configuration type
78+
*/
79+
export class BatchTask<
80+
Input extends TaskInput = TaskInput,
81+
Output extends TaskOutput = TaskOutput,
82+
Config extends BatchTaskConfig = BatchTaskConfig,
83+
> extends IteratorTask<Input, Output, Config> {
84+
public static type: TaskTypeName = "BatchTask";
85+
public static category: string = "Flow Control";
86+
public static title: string = "Batch";
87+
public static description: string = "Processes an array in configurable batches";
88+
89+
/**
90+
* BatchTask always uses PROPERTY_ARRAY merge strategy.
91+
*/
92+
public static readonly compoundMerge = PROPERTY_ARRAY;
93+
94+
/**
95+
* Static input schema for BatchTask.
96+
*/
97+
public static inputSchema(): DataPortSchema {
98+
return {
99+
type: "object",
100+
properties: {},
101+
additionalProperties: true,
102+
} as const satisfies DataPortSchema;
103+
}
104+
105+
/**
106+
* Static output schema for BatchTask.
107+
*/
108+
public static outputSchema(): DataPortSchema {
109+
return {
110+
type: "object",
111+
properties: {},
112+
additionalProperties: true,
113+
} as const satisfies DataPortSchema;
114+
}
115+
116+
/**
117+
* Gets the batch size.
118+
*/
119+
public override get batchSize(): number {
120+
return this.config.batchSize ?? 10;
121+
}
122+
123+
/**
124+
* Whether to flatten results from all batches.
125+
*/
126+
public get flattenResults(): boolean {
127+
return this.config.flattenResults ?? true;
128+
}
129+
130+
/**
131+
* Batch execution mode.
132+
*/
133+
public get batchExecutionMode(): "parallel" | "sequential" {
134+
return this.config.batchExecutionMode ?? "sequential";
135+
}
136+
137+
/**
138+
* Override to group items into batches instead of individual items.
139+
*/
140+
protected override getIterableItems(input: Input): unknown[] {
141+
const items = super.getIterableItems(input);
142+
return this.groupIntoBatches(items);
143+
}
144+
145+
/**
146+
* Groups items into batches of batchSize.
147+
*/
148+
protected groupIntoBatches(items: unknown[]): unknown[][] {
149+
const batches: unknown[][] = [];
150+
const size = this.batchSize;
151+
152+
for (let i = 0; i < items.length; i += size) {
153+
batches.push(items.slice(i, i + size));
154+
}
155+
156+
return batches;
157+
}
158+
159+
/**
160+
* Creates iteration tasks for batches.
161+
* Each batch receives the array of items for that batch.
162+
*/
163+
protected override createIterationTasks(batches: unknown[]): void {
164+
const portName = this.getIteratorPortName();
165+
if (!portName) return;
166+
167+
// Get all non-iterator input values
168+
const baseInput: Record<string, unknown> = {};
169+
for (const [key, value] of Object.entries(this.runInputData)) {
170+
if (key !== portName) {
171+
baseInput[key] = value;
172+
}
173+
}
174+
175+
// Create tasks for each batch
176+
for (let i = 0; i < batches.length; i++) {
177+
const batch = batches[i];
178+
const batchInput = {
179+
...baseInput,
180+
[portName]: batch, // Batch is an array of items
181+
_batchIndex: i,
182+
_batchItems: batch,
183+
};
184+
185+
this.cloneTemplateForIteration(batchInput, i);
186+
}
187+
}
188+
189+
/**
190+
* Returns the empty result for BatchTask.
191+
*/
192+
protected override getEmptyResult(): Output {
193+
const schema = this.outputSchema();
194+
if (typeof schema === "boolean") {
195+
return {} as Output;
196+
}
197+
198+
const result: Record<string, unknown[]> = {};
199+
for (const key of Object.keys(schema.properties || {})) {
200+
result[key] = [];
201+
}
202+
203+
return result as Output;
204+
}
205+
206+
/**
207+
* Output schema for BatchTask.
208+
* Similar to MapTask - wraps inner outputs in arrays.
209+
*/
210+
public override outputSchema(): DataPortSchema {
211+
if (!this.hasChildren() && !this._templateGraph) {
212+
return (this.constructor as typeof BatchTask).outputSchema();
213+
}
214+
215+
return this.getWrappedOutputSchema();
216+
}
217+
218+
/**
219+
* Collects and optionally flattens results from all batches.
220+
*/
221+
protected override collectResults(results: TaskOutput[]): Output {
222+
const collected = super.collectResults(results);
223+
224+
if (!this.flattenResults || typeof collected !== "object" || collected === null) {
225+
return collected;
226+
}
227+
228+
// Flatten nested arrays (from batch results)
229+
const flattened: Record<string, unknown[]> = {};
230+
for (const [key, value] of Object.entries(collected)) {
231+
if (Array.isArray(value)) {
232+
// Deep flatten for batch results
233+
flattened[key] = value.flat(2);
234+
} else {
235+
flattened[key] = value as unknown[];
236+
}
237+
}
238+
239+
return flattened as Output;
240+
}
241+
242+
/**
243+
* Regenerates the graph for batch execution.
244+
*/
245+
public override regenerateGraph(): void {
246+
// Clear the existing subgraph
247+
this.subGraph = new TaskGraph();
248+
249+
if (!this._templateGraph || !this._templateGraph.getTasks().length) {
250+
super.regenerateGraph();
251+
return;
252+
}
253+
254+
const batches = this.getIterableItems(this.runInputData as Input);
255+
if (batches.length === 0) {
256+
super.regenerateGraph();
257+
return;
258+
}
259+
260+
// Create tasks for each batch
261+
this.createIterationTasks(batches);
262+
263+
// Emit regenerate event
264+
this.events.emit("regenerate");
265+
}
266+
}
267+
268+
// ============================================================================
269+
// Workflow Prototype Extensions
270+
// ============================================================================
271+
272+
declare module "../task-graph/Workflow" {
273+
interface Workflow {
274+
/**
275+
* Starts a batch loop that processes arrays in chunks.
276+
* Use .endBatch() to close the loop and return to the parent workflow.
277+
*
278+
* @param config - Configuration for the batch loop
279+
* @returns A Workflow in loop builder mode for defining the batch processing
280+
*
281+
* @example
282+
* ```typescript
283+
* workflow
284+
* .batch({ batchSize: 10 })
285+
* .bulkProcess()
286+
* .endBatch()
287+
* ```
288+
*/
289+
// batch(config?: Partial<BatchTaskConfig>): Workflow;
290+
batch: CreateLoopWorkflow<TaskInput, TaskOutput, BatchTaskConfig>;
291+
292+
/**
293+
* Ends the batch loop and returns to the parent workflow.
294+
* Only callable on workflows in loop builder mode.
295+
*
296+
* @returns The parent workflow
297+
*/
298+
endBatch(): Workflow;
299+
}
300+
}
301+
302+
Workflow.prototype.batch = CreateLoopWorkflow(BatchTask);
303+
304+
Workflow.prototype.endBatch = CreateEndLoopWorkflow("endBatch");

0 commit comments

Comments
 (0)