@llamaindex/core
Version:
LlamaIndex Core Module
302 lines (297 loc) • 11.4 kB
JavaScript
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;