UNPKG

@llamaindex/core

Version:
302 lines (297 loc) 11.4 kB
Object.defineProperty(exports, '__esModule', { value: true }); class Context { #workflow; #queues; #eventBuffer; // eslint-disable-next-line @typescript-eslint/no-explicit-any #globals; #streamingQueue; #verbose; constructor(params){ this.#queues = new Map(); this.#eventBuffer = new Map(); this.#globals = new Map(); this.#streamingQueue = []; this.running = true; this.#verbose = false; this.#workflow = params.workflow; this.#verbose = params.verbose ?? false; } // eslint-disable-next-line @typescript-eslint/no-explicit-any set(key, value) { this.#globals.set(key, value); } // eslint-disable-next-line @typescript-eslint/no-explicit-any get(key, defaultValue) { if (this.#globals.has(key)) { return this.#globals.get(key); } else if (defaultValue !== undefined) { return defaultValue; } throw new Error(`Key '${key}' not found in Context`); } collectEvents(event, expected) { const eventType = event.constructor; if (!this.#eventBuffer.has(eventType)) { this.#eventBuffer.set(eventType, []); } this.#eventBuffer.get(eventType).push(event); const retval = []; for (const expectedType of expected){ const events = this.#eventBuffer.get(expectedType); if (events && events.length > 0) { retval.push(events.shift()); } } if (retval.length === expected.length) { return retval; } // Put back the events if unable to collect all for (const ev of retval){ const eventType = ev.constructor; if (!this.#eventBuffer.has(eventType)) { this.#eventBuffer.set(eventType, []); } this.#eventBuffer.get(eventType).unshift(ev); } return null; } sendEvent(message, step) { const stepName = step?.name ? `step ${step.name}` : "all steps"; if (this.#verbose) { console.log(`Sending event ${message} to ${stepName}`); } if (step === undefined) { for (const queue of this.#queues.values()){ queue.push(message); } } else { if (!this.#workflow.hasStep(step)) { throw new Error(`Step ${step} does not exist`); } if (!this.#queues.has(step)) { this.#queues.set(step, []); } this.#queues.get(step).push(message); } } getNextEvent(step) { const queue = this.#queues.get(step); if (queue && queue.length > 0) { return queue.shift(); } return undefined; } writeEventToStream(event) { this.#streamingQueue.push(event); } async *streamEvents() { while(true){ const event = this.#streamingQueue.shift(); if (event) { yield event; } else { if (!this.running) { break; } await new Promise((resolve)=>setTimeout(resolve, 0)); } } } } // eslint-disable-next-line @typescript-eslint/no-explicit-any class WorkflowEvent { constructor(data){ this.data = data; } toString() { return `${this.constructor.name}(${JSON.stringify(this.data)})`; } } class StartEvent extends WorkflowEvent { } class StopEvent extends WorkflowEvent { } let once = false; class Workflow { #steps; #contexts; #verbose; #timeout; #validate; constructor(params = {}){ this.#steps = new Map(); this.#contexts = new Set(); this.#verbose = false; this.#timeout = null; this.#validate = false; if (!once && !params.ignoreDeprecatedWarning) { console.warn("@llamaindex/core/workflow is going to use the new workflow API in the next major version.", "Please update your imports to @llamaindex/workflow"); console.warn("See https://ts.llamaindex.ai/docs/llamaindex/guide/workflow for more information"); once = true; } this.#verbose = params.verbose ?? false; this.#timeout = params.timeout ?? null; this.#validate = params.validate ?? false; } addStep(eventType, method, params = {}) { const inputs = Array.isArray(eventType) ? eventType : [ eventType ]; const outputs = params.outputs ? Array.isArray(params.outputs) ? params.outputs : [ params.outputs ] : undefined; this.#steps.set(method, { inputs, outputs }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any hasStep(step) { return this.#steps.has(step); } // eslint-disable-next-line @typescript-eslint/no-explicit-any #acceptsEvent(step, event) { const eventType = event.constructor; const stepInfo = this.#steps.get(step); if (!stepInfo) { throw new Error(`No method found for step: ${step.name}`); } return stepInfo.inputs.includes(eventType); } async *streamEvents() { if (this.#contexts.size > 1) { throw new Error("This workflow has multiple concurrent runs in progress and cannot stream events. " + "To be able to stream events, make sure you call `run()` on this workflow only once."); } const context = this.#contexts.values().next().value; if (!context) { throw new Error("No active context found for streaming events."); } yield* context.streamEvents(); } validate() { if (this.#verbose) { console.log("Validating workflow..."); } // Check if all steps have outputs defined // precondition for the validation to work const allStepsHaveOutputs = Array.from(this.#steps.values()).every((stepInfo)=>stepInfo.outputs !== undefined); if (!allStepsHaveOutputs) { throw new Error("Not all steps have outputs defined. Can't validate. Add the 'outputs' parameter to each 'addStep' method call to do validation"); } // input events that are consumed by any step of the workflow const consumedEvents = new Set(); // output events that are produced by any step of the workflow const producedEvents = new Set([ StartEvent ]); for (const [, stepInfo] of this.#steps){ stepInfo.inputs.forEach((eventType)=>consumedEvents.add(eventType)); stepInfo.outputs?.forEach((eventType)=>producedEvents.add(eventType)); } // Check if all consumed events are produced const unconsumedEvents = Array.from(consumedEvents).filter((event)=>!producedEvents.has(event)); if (unconsumedEvents.length > 0) { const names = unconsumedEvents.map((event)=>event.name).join(", "); throw new Error(`The following events are consumed but never produced: ${names}`); } // Check if there are any unused produced events (except StopEvent) const unusedEvents = Array.from(producedEvents).filter((event)=>!consumedEvents.has(event) && event !== StopEvent); if (unusedEvents.length > 0) { const names = unusedEvents.map((event)=>event.name).join(", "); throw new Error(`The following events are produced but never consumed: ${names}`); } if (this.#verbose) { console.log("Workflow validation passed"); } } async run(event) { // Validate the workflow before running if #validate is true if (this.#validate) { this.validate(); } const context = new Context({ workflow: this, verbose: this.#verbose }); this.#contexts.add(context); const stopWorkflow = ()=>{ if (context.running) { context.running = false; this.#contexts.delete(context); } }; const startEvent = typeof event === "string" ? new StartEvent({ input: event }) : event; if (this.#verbose) { console.log(`Starting workflow with event ${startEvent}`); } const workflowPromise = new Promise((resolve, reject)=>{ for (const [step] of this.#steps){ // send initial event to step context.sendEvent(startEvent, step); if (this.#verbose) { console.log(`Starting tasks for step ${step.name}`); } queueMicrotask(async ()=>{ try { while(context.running){ const currentEvent = context.getNextEvent(step); if (!currentEvent) { // if there's no event, wait and try again await new Promise((resolve)=>setTimeout(resolve, 0)); continue; } if (!this.#acceptsEvent(step, currentEvent)) { continue; } if (this.#verbose) { console.log(`Step ${step.name} received event ${currentEvent}`); } const result = await step.call(this, context, currentEvent); if (!context.running) { // workflow was stopped during the execution (e.g. there was a timeout) return; } if (result instanceof StopEvent) { if (this.#verbose) { console.log(`Stopping workflow with event ${result}`); } resolve(result); return; } if (result instanceof WorkflowEvent) { context.sendEvent(result); } } } catch (error) { if (this.#verbose) { console.error(`Error in calling step ${step.name}:`, error); } reject(error); } finally{ stopWorkflow(); } }); } }); if (this.#timeout !== null) { const timeout = this.#timeout; const timeoutPromise = new Promise((_, reject)=>setTimeout(()=>{ stopWorkflow(); reject(new Error(`Operation timed out after ${timeout} seconds`)); }, timeout * 1000)); return Promise.race([ workflowPromise, timeoutPromise ]); } return workflowPromise; } } exports.Context = Context; exports.StartEvent = StartEvent; exports.StopEvent = StopEvent; exports.Workflow = Workflow; exports.WorkflowEvent = WorkflowEvent;