mirror of
https://github.com/run-llama/LlamaIndexTS.git
synced 2026-07-02 20:13:52 -04:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d57917d782 | |||
| f231e0739f | |||
| 0765742ef3 | |||
| ec7fd6be5c | |||
| c7a918c3f5 | |||
| 00e681d43b | |||
| c01502fb84 |
@@ -84,7 +84,9 @@ jobs:
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
- name: Use Build For Examples
|
||||
run: pnpm link ../packages/llamaindex/
|
||||
run: |
|
||||
pnpm link ../packages/llamaindex/
|
||||
cd readers && pnpm link ../../packages/llamaindex/
|
||||
working-directory: ./examples
|
||||
- name: Run Type Check
|
||||
run: pnpm run type-check
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# docs
|
||||
|
||||
## 0.0.106
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @llamaindex/examples@0.0.12
|
||||
|
||||
## 0.0.105
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [c7a918c]
|
||||
- llamaindex@0.8.2
|
||||
|
||||
## 0.0.104
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "0.0.104",
|
||||
"version": "0.0.106",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @llamaindex/doc
|
||||
|
||||
## 0.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [0765742]
|
||||
- @llamaindex/workflow@0.0.2
|
||||
|
||||
## 0.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [c7a918c]
|
||||
- llamaindex@0.8.2
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -5,6 +5,14 @@ const withMDX = createMDX();
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
reactStrictMode: true,
|
||||
webpack: (config) => {
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
sharp$: false,
|
||||
"onnxruntime-node$": false,
|
||||
};
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default withMDX(config);
|
||||
|
||||
+22
-17
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@llamaindex/doc",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "pnpm run build:docs && next build",
|
||||
@@ -16,26 +16,30 @@
|
||||
"@llamaindex/core": "workspace:*",
|
||||
"@llamaindex/openai": "workspace:*",
|
||||
"@llamaindex/readers": "workspace:*",
|
||||
"@llamaindex/workflow": "workspace:*",
|
||||
"@mdx-js/mdx": "^3.1.0",
|
||||
"@number-flow/react": "^0.3.0",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-icons": "^1.3.1",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.3",
|
||||
"ai": "^3.3.21",
|
||||
"@vercel/functions": "^1.5.0",
|
||||
"ai": "^3.4.31",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "2.1.1",
|
||||
"foxact": "^0.2.39",
|
||||
"framer-motion": "^11.11.10",
|
||||
"fumadocs-core": "14.0.2",
|
||||
"fumadocs-docgen": "^1.3.0",
|
||||
"fumadocs-mdx": "11.0.0",
|
||||
"fumadocs-openapi": "^5.5.3",
|
||||
"fumadocs-twoslash": "^2.0.0",
|
||||
"fumadocs-ui": "14.0.2",
|
||||
"foxact": "^0.2.40",
|
||||
"framer-motion": "^11.11.11",
|
||||
"fumadocs-core": "14.2.0",
|
||||
"fumadocs-docgen": "^1.3.1",
|
||||
"fumadocs-mdx": "^11.1.1",
|
||||
"fumadocs-openapi": "^5.5.6",
|
||||
"fumadocs-twoslash": "^2.0.1",
|
||||
"fumadocs-typescript": "^3.0.1",
|
||||
"fumadocs-ui": "14.2.0",
|
||||
"hast-util-to-jsx-runtime": "^2.3.2",
|
||||
"llamaindex": "workspace:*",
|
||||
"lucide-react": "^0.436.0",
|
||||
"next": "15.0.1",
|
||||
"lucide-react": "^0.454.0",
|
||||
"next": "15.0.2",
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -45,17 +49,18 @@
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"shiki": "^1.22.0",
|
||||
"shiki": "^1.22.2",
|
||||
"shiki-magic-move": "^0.5.0",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"use-stick-to-bottom": "^1.0.41",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/env": "^15.0.1",
|
||||
"@next/env": "^15.0.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "22.8.4",
|
||||
"@types/node": "22.8.6",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
@@ -67,7 +72,7 @@
|
||||
"remark-mdx": "^3.1.0",
|
||||
"remark-stringify": "^11.0.0",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"tsx": "^4.19.0",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createMetadata, metadataImage } from "@/lib/metadata";
|
||||
import { openapi, source } from "@/lib/source";
|
||||
import { Popup, PopupContent, PopupTrigger } from "fumadocs-twoslash/ui";
|
||||
import { createTypeTable } from "fumadocs-typescript/ui";
|
||||
import defaultMdxComponents from "fumadocs-ui/mdx";
|
||||
import {
|
||||
DocsBody,
|
||||
@@ -10,6 +11,8 @@ import {
|
||||
} from "fumadocs-ui/page";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
const { AutoTypeTable } = createTypeTable();
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ slug?: string[] }>;
|
||||
}) {
|
||||
@@ -20,7 +23,16 @@ export default async function Page(props: {
|
||||
const MDX = page.data.body;
|
||||
|
||||
return (
|
||||
<DocsPage toc={page.data.toc} full={page.data.full}>
|
||||
<DocsPage
|
||||
toc={page.data.toc}
|
||||
full={page.data.full}
|
||||
editOnGithub={{
|
||||
owner: "run-llama",
|
||||
repo: "LlamaIndexTS",
|
||||
sha: "main",
|
||||
path: `apps/next/src/content/docs/${page.file.path}`,
|
||||
}}
|
||||
>
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
<DocsDescription>{page.data.description}</DocsDescription>
|
||||
<DocsBody>
|
||||
@@ -31,6 +43,7 @@ export default async function Page(props: {
|
||||
Popup,
|
||||
PopupContent,
|
||||
PopupTrigger,
|
||||
AutoTypeTable,
|
||||
}}
|
||||
/>
|
||||
</DocsBody>
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
"use client";
|
||||
import FlowInput from "@/components/flow-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
import { ReactNode, startTransition, useState } from "react";
|
||||
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
||||
|
||||
class ComputeEvent extends WorkflowEvent<number> {
|
||||
constructor(data: number) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
class ComputeResultEvent extends WorkflowEvent<number> {
|
||||
constructor(data: number) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
type ContextData = {
|
||||
sum: number;
|
||||
};
|
||||
|
||||
const workflow = new Workflow<ContextData, number, number>();
|
||||
|
||||
const max = 1000;
|
||||
const min = 100;
|
||||
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<number>],
|
||||
outputs: [StopEvent<number>],
|
||||
},
|
||||
async (context, event) => {
|
||||
const total = event.data;
|
||||
for (let i = 0; i < total; i++) {
|
||||
context.sendEvent(new ComputeEvent(i));
|
||||
}
|
||||
console.log("waiting");
|
||||
const computeResults = await Promise.all(
|
||||
Array.from({ length: total }).map(() =>
|
||||
context.requireEvent(ComputeResultEvent),
|
||||
),
|
||||
);
|
||||
context.data.sum = computeResults.reduce(
|
||||
(acc, result) => acc + result.data,
|
||||
0,
|
||||
);
|
||||
console.log("stop");
|
||||
return new StopEvent(context.data.sum);
|
||||
},
|
||||
);
|
||||
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [ComputeEvent],
|
||||
outputs: [ComputeResultEvent],
|
||||
},
|
||||
async (context, event) => {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, Math.floor(Math.random() * (max - min + 1) + min)),
|
||||
);
|
||||
return new ComputeResultEvent(event.data);
|
||||
},
|
||||
);
|
||||
|
||||
function ScrollToBottom() {
|
||||
const { isAtBottom, scrollToBottom } = useStickToBottomContext();
|
||||
|
||||
return (
|
||||
!isAtBottom && (
|
||||
<button
|
||||
className="absolute i-ph-arrow-circle-down-fill text-4xl rounded-lg left-[50%] translate-x-[-50%] bottom-0"
|
||||
onClick={() => scrollToBottom()}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function WorkflowStreamingDemo() {
|
||||
const [ui, setUI] = useState<ReactNode[]>([
|
||||
<div key={0} className="bg-gray-100">
|
||||
Waiting for workflow to start
|
||||
</div>,
|
||||
]);
|
||||
const [total, setTotal] = useState<number>(10);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start w-full gap-2">
|
||||
<div className="flex flex-row justify-center items-center">
|
||||
<div className="text-lg mr-2">Compute total</div>{" "}
|
||||
<FlowInput value={total} onChange={(value) => setTotal(value)} />
|
||||
</div>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
startTransition(() => {
|
||||
setUI([]);
|
||||
});
|
||||
const context = workflow.run(total, {
|
||||
sum: 0,
|
||||
});
|
||||
let i = 0;
|
||||
for await (const event of context) {
|
||||
console.log(event);
|
||||
if (event instanceof ComputeEvent) {
|
||||
setUI((ui) => [
|
||||
...ui,
|
||||
<div key={i++} className="bg-yellow-100">
|
||||
Computing task id: {event.data}
|
||||
</div>,
|
||||
]);
|
||||
} else if (event instanceof ComputeResultEvent) {
|
||||
setUI((ui) => [
|
||||
...ui,
|
||||
<div key={i++} className="bg-green-100">
|
||||
Computed task id: {event.data}
|
||||
</div>,
|
||||
]);
|
||||
} else if (event instanceof StartEvent) {
|
||||
setUI((ui) => [
|
||||
...ui,
|
||||
<div key={i++} className="bg-blue-100">
|
||||
Started workflow with total {event.data}
|
||||
</div>,
|
||||
]);
|
||||
} else if (event instanceof StopEvent) {
|
||||
setUI((ui) => [
|
||||
...ui,
|
||||
<div key={i++} className="bg-red-100">
|
||||
Workflow stopped
|
||||
</div>,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
Start Workflow
|
||||
</Button>
|
||||
<StickToBottom className="w-full flex flex-col gap-2 p-2 border border-gray-200 rounded-lg max-h-96 overflow-y-auto">
|
||||
<StickToBottom.Content className="flex flex-col gap-2">
|
||||
{ui}
|
||||
</StickToBottom.Content>
|
||||
<ScrollToBottom />
|
||||
</StickToBottom>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import NumberFlow from "@number-flow/react";
|
||||
import clsx from "clsx/lite";
|
||||
import { Minus, Plus } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
value?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
onChange?: (value: number) => void;
|
||||
};
|
||||
export default function FlowInput({
|
||||
value = 0,
|
||||
min = -Infinity,
|
||||
max = Infinity,
|
||||
onChange,
|
||||
}: Props) {
|
||||
const defaultValue = React.useRef(value);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const [animated, setAnimated] = React.useState(true);
|
||||
const [showCaret, setShowCaret] = React.useState(true);
|
||||
const handleInput: React.ChangeEventHandler<HTMLInputElement> = ({
|
||||
currentTarget: el,
|
||||
}) => {
|
||||
setAnimated(false);
|
||||
let next = value;
|
||||
if (el.value === "") {
|
||||
next = defaultValue.current;
|
||||
} else {
|
||||
const num = parseInt(el.value);
|
||||
if (!isNaN(num) && min <= num && num <= max) next = num;
|
||||
}
|
||||
el.value = String(next);
|
||||
onChange?.(next);
|
||||
};
|
||||
const handlePointerDown =
|
||||
(diff: number) => (event: React.PointerEvent<HTMLButtonElement>) => {
|
||||
setAnimated(true);
|
||||
if (event.pointerType === "mouse") {
|
||||
event?.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
const newVal = Math.min(Math.max(value + diff, min), max);
|
||||
onChange?.(newVal);
|
||||
};
|
||||
return (
|
||||
<div className="group flex items-stretch rounded-md text-lg font-semibold ring ring-zinc-200 transition-[box-shadow] focus-within:ring-2 focus-within:ring-blue-500 dark:ring-zinc-800">
|
||||
<button
|
||||
aria-hidden
|
||||
tabIndex={-1}
|
||||
className="flex items-center pl-[.5em] pr-[.325em]"
|
||||
disabled={min != null && value <= min}
|
||||
onPointerDown={handlePointerDown(-1)}
|
||||
>
|
||||
<Minus className="size-4" absoluteStrokeWidth strokeWidth={3.5} />
|
||||
</button>
|
||||
<div className="relative grid items-center justify-items-center text-center [grid-template-areas:'overlap'] *:[grid-area:overlap]">
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={clsx(
|
||||
showCaret ? "caret-primary" : "caret-transparent",
|
||||
"spin-hide w-[1.5em] bg-transparent py-2 text-center font-[inherit] text-transparent outline-none",
|
||||
"[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none",
|
||||
)}
|
||||
// Make sure to disable kerning, to match NumberFlow:
|
||||
style={{ fontKerning: "none" }}
|
||||
type="number"
|
||||
min={min}
|
||||
step={1}
|
||||
autoComplete="off"
|
||||
inputMode="numeric"
|
||||
max={max}
|
||||
value={value}
|
||||
onInput={handleInput}
|
||||
/>
|
||||
<NumberFlow
|
||||
value={value}
|
||||
format={{ useGrouping: false }}
|
||||
aria-hidden
|
||||
animated={animated}
|
||||
onAnimationsStart={() => setShowCaret(false)}
|
||||
onAnimationsFinish={() => setShowCaret(true)}
|
||||
className="pointer-events-none"
|
||||
willChange
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
aria-hidden
|
||||
tabIndex={-1}
|
||||
className="flex items-center pl-[.325em] pr-[.5em]"
|
||||
disabled={max != null && value >= max}
|
||||
onPointerDown={handlePointerDown(1)}
|
||||
>
|
||||
<Plus className="size-4" absoluteStrokeWidth strokeWidth={3.5} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Guide",
|
||||
"description": "See our guide",
|
||||
"pages": ["workflow"]
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
---
|
||||
title: Inputs / Outputs
|
||||
description: Learn how to use different inputs and outputs in your workflows.
|
||||
---
|
||||
|
||||
Inputs and outputs are the way to communicate between steps in a workflow. In the previous example,
|
||||
we used `StartEvent` and `StopEvent` to communicate between steps. However, you can use any type of event to communicate between steps.
|
||||
|
||||
## Multiple inputs
|
||||
|
||||
You can define multiple inputs for a step.
|
||||
|
||||
In the following example, we define a complex workflow with multiple inputs and outputs.
|
||||
|
||||
```ts twoslash
|
||||
import { Workflow, StartEvent, StopEvent, WorkflowEvent } from '@llamaindex/workflow';
|
||||
|
||||
class AEvent extends WorkflowEvent<string> {
|
||||
constructor(data: string) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
class BEvent extends WorkflowEvent<number> {
|
||||
constructor(data: number) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
class ResultEvent extends WorkflowEvent<string> {
|
||||
constructor(data: string) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
First, let's define the events that we will use in the workflow.
|
||||
|
||||
```ts twoslash
|
||||
import { Workflow, StartEvent, StopEvent, WorkflowEvent } from '@llamaindex/workflow';
|
||||
|
||||
class AEvent extends WorkflowEvent<string> {
|
||||
constructor(data: string) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
class BEvent extends WorkflowEvent<number> {
|
||||
constructor(data: number) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
class ResultEvent extends WorkflowEvent<string> {
|
||||
constructor(data: string) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
const workflow = new Workflow<never, string, string>();
|
||||
|
||||
workflow.addStep({
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>]
|
||||
}, async (
|
||||
context,
|
||||
startEvent
|
||||
) => {
|
||||
const input = startEvent.data;
|
||||
const aEvent = await context.requireEvent(AEvent);
|
||||
const bEvent = await context.requireEvent(BEvent);
|
||||
const a = aEvent.data;
|
||||
const b = bEvent.data;
|
||||
return new StopEvent(`Hello, ${input}! A: ${a}, B: ${b}`);
|
||||
});
|
||||
|
||||
// ---cut---
|
||||
workflow.addStep({
|
||||
inputs: [AEvent, BEvent],
|
||||
outputs: [ResultEvent]
|
||||
}, async (
|
||||
context,
|
||||
aEvent,
|
||||
bEvent
|
||||
) => {
|
||||
const a = aEvent.data;
|
||||
const b = bEvent.data;
|
||||
return new ResultEvent(`A: ${a}, B: ${b}`);
|
||||
});
|
||||
```
|
||||
|
||||
This step means that it requires two events: `AEvent` and `BEvent`. It will return a `ResultEvent` with the data `A: ${a}, B: ${b}`.
|
||||
|
||||
## A or B input
|
||||
|
||||
If we want to have a step that can accept either `AEvent` or `BEvent`, we can define the step like this:
|
||||
|
||||
```ts twoslash
|
||||
import { Workflow, StartEvent, StopEvent, WorkflowEvent } from '@llamaindex/workflow';
|
||||
|
||||
class AEvent extends WorkflowEvent<string> {
|
||||
constructor(data: string) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
class BEvent extends WorkflowEvent<number> {
|
||||
constructor(data: number) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
class ResultEvent extends WorkflowEvent<string> {
|
||||
constructor(data: string) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
const workflow = new Workflow<never, string, string>();
|
||||
|
||||
workflow.addStep({
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>]
|
||||
}, async (
|
||||
context,
|
||||
startEvent
|
||||
) => {
|
||||
const input = startEvent.data;
|
||||
const aEvent = await context.requireEvent(AEvent);
|
||||
const bEvent = await context.requireEvent(BEvent);
|
||||
const a = aEvent.data;
|
||||
const b = bEvent.data;
|
||||
return new StopEvent(`Hello, ${input}! A: ${a}, B: ${b}`);
|
||||
});
|
||||
|
||||
// ---cut---
|
||||
workflow.addStep({
|
||||
inputs: [WorkflowEvent.or(AEvent, BEvent)],
|
||||
outputs: [ResultEvent]
|
||||
}, async (
|
||||
context,
|
||||
aOrBEvent
|
||||
) => {
|
||||
if (aOrBEvent instanceof AEvent) {
|
||||
// ^?
|
||||
|
||||
|
||||
const a = aOrBEvent.data;
|
||||
// ^?
|
||||
|
||||
|
||||
return new ResultEvent(`A: ${a}`);
|
||||
} else {
|
||||
const b = aOrBEvent.data;
|
||||
// ^?
|
||||
|
||||
|
||||
return new ResultEvent(`B: ${b}`);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
This step means that it requires either `AEvent` or `BEvent`. It will return a `ResultEvent` with the data `A: ${a}` or `B: ${b}`.
|
||||
|
||||
You can still combine the logic with `context.requireEvent` to get the data from the event.
|
||||
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
|
||||
<Accordions>
|
||||
<Accordion title="Under the hood">
|
||||
We use JavaScript Inheritance and the prototype chain to implement the `or` logic.
|
||||
The `or` method creates a new class that extends the two classes that you pass to it.
|
||||
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain"
|
||||
>
|
||||
MDN - Inheritance and the prototype chain
|
||||
</a>
|
||||
</Accordion>
|
||||
</Accordions>
|
||||
|
||||
## Multiple outputs
|
||||
|
||||
You can define multiple outputs for a step.
|
||||
|
||||
```ts twoslash
|
||||
import { Workflow, StartEvent, StopEvent, WorkflowEvent } from '@llamaindex/workflow';
|
||||
|
||||
class AEvent extends WorkflowEvent<string> {
|
||||
constructor(data: string) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
class BEvent extends WorkflowEvent<number> {
|
||||
constructor(data: number) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
class ResultEvent extends WorkflowEvent<string> {
|
||||
constructor(data: string) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
const workflow = new Workflow<never, string, string>();
|
||||
// ---cut---
|
||||
workflow.addStep({
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [AEvent, BEvent]
|
||||
}, async (
|
||||
context,
|
||||
startEvent
|
||||
) => {
|
||||
const input = startEvent.data;
|
||||
if (Math.random() > 0.5) {
|
||||
return new AEvent(`Hello, ${input}!`);
|
||||
} else {
|
||||
return new BEvent(42);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
This step will return either an `AEvent` or a `BEvent` based on a random number.
|
||||
@@ -0,0 +1,208 @@
|
||||
---
|
||||
title: Basic Usage
|
||||
description: Learn how to use the LlamaIndex workflow.
|
||||
---
|
||||
|
||||
A `Workflow` in LlamaIndex.TS is an event-driven abstraction used to chain together several events.
|
||||
Workflows are made up of steps, with each step responsible for handling certain event types and emitting new events.
|
||||
|
||||
Workflows are designed for any cases that benefit from event-driven programming, not only for LLM and AI tasks.
|
||||
|
||||
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
|
||||
|
||||
<Tabs groupId="install" items={["npm", "yarn", "pnpm"]} persist>
|
||||
```shell tab="npm"
|
||||
npm install @llamaindex/workflow
|
||||
```
|
||||
|
||||
```shell tab="yarn"
|
||||
yarn add @llamaindex/workflow
|
||||
```
|
||||
|
||||
```shell tab="pnpm"
|
||||
pnpm add @llamaindex/workflow
|
||||
```
|
||||
</Tabs>
|
||||
|
||||
## Start from scratch
|
||||
|
||||
Let's start from a Hello World workflow.
|
||||
|
||||
```ts twoslash
|
||||
import { Workflow } from '@llamaindex/workflow';
|
||||
|
||||
type ContextData = {
|
||||
counter: number;
|
||||
}
|
||||
// ---cut---
|
||||
const contextData: ContextData = { counter: 0 };
|
||||
|
||||
const workflow = new Workflow<ContextData, string, string>();
|
||||
// ^?
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
||||
First, we define a workflow with 3 generic types: `ContextData`, `Input`, and `Output`.
|
||||
|
||||
In general, `ContextData` is used to store the shared data between steps, `Input` is the type of the input event, and `Output` is the type of the output event.
|
||||
|
||||
In you code logic, you should **share state between steps via `ContextData`**.
|
||||
|
||||
```ts twoslash
|
||||
import { Workflow, StartEvent, StopEvent } from '@llamaindex/workflow';
|
||||
|
||||
type ContextData = {
|
||||
counter: number;
|
||||
}
|
||||
|
||||
const contextData: ContextData = { counter: 0 };
|
||||
|
||||
const workflow = new Workflow<ContextData, string, string>();
|
||||
// ---cut---
|
||||
workflow.addStep({
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>]
|
||||
}, async (context, startEvent) => {
|
||||
const input = startEvent.data;
|
||||
context.data.counter++;
|
||||
return new StopEvent(`Hello, ${input}!`);
|
||||
});
|
||||
```
|
||||
|
||||
In the workflow, we add a step that listens to `StartEvent<string>` and emits `StopEvent<string>`.
|
||||
|
||||
The step is an async function that takes two arguments: `context` and `event`.
|
||||
|
||||
### `context` type
|
||||
|
||||
<AutoTypeTable path="./src/deps/type.ts" name="HandlerContext" />
|
||||
|
||||
There are two more properties in `HandlerContext`:
|
||||
|
||||
- `sendEvent`: invoke another event in the workflow, other than `StartEvent`, `StopEvent`, or the current event. (Or there will have circular reference)
|
||||
- `requireEvent`: wait for a specific event to be emitted.
|
||||
|
||||
You can use `sendEvent` and `requireEvent` to build complex workflows.
|
||||
|
||||
```ts twoslash
|
||||
import { Workflow, StartEvent, StopEvent, WorkflowEvent } from '@llamaindex/workflow';
|
||||
|
||||
type ContextData = {
|
||||
counter: number;
|
||||
}
|
||||
|
||||
const contextData: ContextData = { counter: 0 };
|
||||
|
||||
const workflow = new Workflow<ContextData, string, string>();
|
||||
|
||||
// ---cut---
|
||||
class AnalysisStartEvent extends WorkflowEvent<string> {}
|
||||
class AnalysisStopEvent extends WorkflowEvent<boolean> {}
|
||||
workflow.addStep({
|
||||
inputs: [AnalysisStartEvent],
|
||||
outputs: [AnalysisStopEvent]
|
||||
}, async (...args) => {
|
||||
// do some analysis
|
||||
return new AnalysisStopEvent(true);
|
||||
})
|
||||
workflow.addStep({
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>]
|
||||
}, async (context, startEvent) => {
|
||||
const input = startEvent.data;
|
||||
context.sendEvent(new AnalysisStartEvent('start'));
|
||||
context.data.counter++;
|
||||
const { data } = await context.requireEvent(AnalysisStopEvent);
|
||||
return new StopEvent(`Hello, ${input}! Analysis result: ${data ? 'success' : 'fail'}`);
|
||||
});
|
||||
```
|
||||
|
||||
For example, you can compile `requireEvent` with `waitUntil` in [Vercel Functions](https://vercel.com/docs/functions/functions-api-reference#waituntil) or [Cloudflare Worker](https://developers.cloudflare.com/workers/runtime-apis/context/#waituntil)
|
||||
|
||||
```ts twoslash
|
||||
import { waitUntil } from '@vercel/functions';
|
||||
import { Workflow, StartEvent, StopEvent, WorkflowEvent } from '@llamaindex/workflow';
|
||||
|
||||
type ContextData = {
|
||||
counter: number;
|
||||
}
|
||||
|
||||
const contextData: ContextData = { counter: 0 };
|
||||
|
||||
const workflow = new Workflow<ContextData, string, string>();
|
||||
|
||||
class AnalysisStartEvent extends WorkflowEvent<string> {}
|
||||
class AnalysisStopEvent extends WorkflowEvent<boolean> {}
|
||||
|
||||
// ---cut---
|
||||
workflow.addStep({
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>]
|
||||
}, async (context, startEvent) => {
|
||||
const input = startEvent.data;
|
||||
context.sendEvent(new AnalysisStartEvent('start'));
|
||||
context.data.counter++;
|
||||
waitUntil(context.requireEvent(AnalysisStopEvent));
|
||||
// note that `waitUntil` is not a promise, it will extend the lifetime of the workflow
|
||||
// you can wait for some background tasks to finish
|
||||
return new StopEvent(`Hello, ${input}!`);
|
||||
});
|
||||
```
|
||||
|
||||
## Multiple runs
|
||||
|
||||
You can run the same workflow multiple times with different inputs.
|
||||
|
||||
```ts twoslash
|
||||
import { Workflow, StartEvent, StopEvent } from '@llamaindex/workflow';
|
||||
|
||||
type ContextData = {
|
||||
counter: number;
|
||||
}
|
||||
|
||||
const contextData: ContextData = { counter: 0 };
|
||||
|
||||
const workflow = new Workflow<ContextData, string, string>();
|
||||
|
||||
workflow.addStep({
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>]
|
||||
}, async (context, startEvent) => {
|
||||
const input = startEvent.data;
|
||||
context.data.counter++;
|
||||
return new StopEvent(`Hello, ${input}!`);
|
||||
});
|
||||
|
||||
// ---cut---
|
||||
{
|
||||
const ret = await workflow.run('Alex', contextData);
|
||||
console.log(ret.data); // Hello, Alex!
|
||||
}
|
||||
|
||||
{
|
||||
const ret = await workflow.run('World', contextData);
|
||||
console.log(ret.data); // Hello, World!
|
||||
}
|
||||
```
|
||||
|
||||
Context is shared between runs, so the counter will be increased.
|
||||
|
||||
Ideally, it should be serializable to make sure it can be recovered from HTTP requests or other storage.
|
||||
|
||||
### Full example
|
||||
|
||||
<iframe
|
||||
className="w-full h-[440px]"
|
||||
aria-label="Workflow example"
|
||||
src="https://stackblitz.com/github/run-llama/LlamaIndexTS/tree/main/examples?file=node/workflow/basic.ts"
|
||||
/>
|
||||
|
||||
## `Workflow` type
|
||||
|
||||
<AutoTypeTable path="./src/deps/type.ts" name="Workflow" />
|
||||
|
||||
## `WorkflowContext` type
|
||||
|
||||
<AutoTypeTable path="./src/deps/type.ts" name="WorkflowContext" />
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"title": "Workflow",
|
||||
"description": "See how to use @llamaindex/workflow",
|
||||
"defaultOpen": false,
|
||||
"pages": ["index", "different-inputs-outputs", "streaming"]
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
---
|
||||
title: Streaming
|
||||
description: Learn how to use the LlamaIndex workflow with streaming.
|
||||
---
|
||||
import { WorkflowStreamingDemo } from '../../../../../components/demo/workflow-streaming-ui';
|
||||
|
||||
`Workflow` API by default is designed for streaming data. In this guide, we will show you how to use the `Workflow` API with streaming data.
|
||||
|
||||
Each `workflow.run` call returns `WorkflowContext`, which implements `AsyncIterable` interface. You can use it to stream data.
|
||||
|
||||
```ts twoslash
|
||||
import { Workflow, WorkflowEvent, StartEvent, StopEvent } from '@llamaindex/workflow';
|
||||
class ComputeEvent extends WorkflowEvent<number> {
|
||||
constructor(data: number) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
class ComputeResultEvent extends WorkflowEvent<number> {
|
||||
constructor(data: number) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
type ContextData = {
|
||||
sum: number;
|
||||
}
|
||||
|
||||
const workflow = new Workflow<ContextData, number, number>();
|
||||
workflow.addStep({
|
||||
inputs: [StartEvent<number>],
|
||||
outputs: [StopEvent<number>]
|
||||
}, async (context, startEvent) => {
|
||||
const total = startEvent.data;
|
||||
for (let i = 0; i < total; i++) {
|
||||
context.sendEvent(new ComputeEvent(i));
|
||||
}
|
||||
const computeResults = await Promise.all(Array.from({ length: total }).map(() => context.requireEvent(ComputeResultEvent)));
|
||||
// Workflow API allows you to start events in parallel and wait for all of them to finish
|
||||
context.data.sum = computeResults.reduce((acc, curr) => acc + curr.data, 0);
|
||||
return new StopEvent(context.data.sum);
|
||||
});
|
||||
```
|
||||
|
||||
We define a parallel computation workflow that computes the sum of numbers from 0 to `total`.
|
||||
|
||||
The workflow sends `ComputeEvent` events for each number and waits for `ComputeResultEvent` events. After receiving all `ComputeResultEvent` events, the workflow returns the sum as a `StopEvent`.
|
||||
|
||||
What if we want cutoff if the sum exceeds a certain value?
|
||||
|
||||
## Streaming
|
||||
|
||||
```ts twoslash
|
||||
import { Workflow, WorkflowEvent, StartEvent, StopEvent } from '@llamaindex/workflow';
|
||||
import { StopCircle } from 'lucide-react';
|
||||
class ComputeEvent extends WorkflowEvent<number> {
|
||||
constructor(data: number) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
class ComputeResultEvent extends WorkflowEvent<number> {
|
||||
constructor(data: number) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type ContextData = {
|
||||
sum: number;
|
||||
}
|
||||
|
||||
const workflow = new Workflow<ContextData, number, number>();
|
||||
// ---cut---
|
||||
const context = workflow.run(1000, {
|
||||
sum: 0
|
||||
});
|
||||
|
||||
for await (const event of context) {
|
||||
if (event instanceof ComputeEvent) {
|
||||
if (context.data.sum > 100) {
|
||||
throw new Error('Sum exceeds 100');
|
||||
}
|
||||
}
|
||||
if (event instanceof StopEvent) {
|
||||
console.log('result', event.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can define more custom logic using `AsyncIterable` interface.
|
||||
|
||||
For example. I just want to stop the workflow if I get a `ComputeResultEvent`
|
||||
|
||||
|
||||
```ts twoslash
|
||||
import { Workflow, WorkflowEvent, StartEvent, StopEvent } from '@llamaindex/workflow';
|
||||
import { StopCircle } from 'lucide-react';
|
||||
class ComputeEvent extends WorkflowEvent<number> {
|
||||
constructor(data: number) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
class ComputeResultEvent extends WorkflowEvent<number> {
|
||||
constructor(data: number) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type ContextData = {
|
||||
sum: number;
|
||||
}
|
||||
|
||||
const workflow = new Workflow<ContextData, number, number>();
|
||||
// ---cut---
|
||||
async function compute() {
|
||||
const context = workflow.run(1000, {
|
||||
sum: 0
|
||||
});
|
||||
for await (const event of context) {
|
||||
if (event instanceof ComputeResultEvent) {
|
||||
return event.data;
|
||||
}
|
||||
}
|
||||
throw new Error('UNREACHABLE');
|
||||
}
|
||||
|
||||
const result = await compute();
|
||||
```
|
||||
|
||||
### Streaming with UI
|
||||
|
||||
You can use the `Workflow` API with UI libraries like React.
|
||||
|
||||
```tsx twoslash
|
||||
// @filename: utils.ts
|
||||
export async function runWithoutBlocking(fn: () => Promise<void>) {
|
||||
fn();
|
||||
}
|
||||
// @filename: action.ts
|
||||
// ---cut---
|
||||
'use server';
|
||||
// "use server" is required to enable server side feature in React
|
||||
import { createStreamableUI } from 'ai/rsc';
|
||||
import { runWithoutBlocking } from './utils';
|
||||
// ---cut-start---
|
||||
import { Workflow, WorkflowEvent, StartEvent, StopEvent } from '@llamaindex/workflow';
|
||||
class ComputeEvent extends WorkflowEvent<number> {
|
||||
constructor(data: number) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
class ComputeResultEvent extends WorkflowEvent<number> {
|
||||
constructor(data: number) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type ContextData = {
|
||||
sum: number;
|
||||
}
|
||||
|
||||
const workflow = new Workflow<ContextData, number, number>();
|
||||
const min = 100;
|
||||
const max = 1000;
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [ComputeEvent],
|
||||
outputs: [ComputeResultEvent]
|
||||
},
|
||||
async (context, event) => {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, Math.floor(Math.random() * (max - min + 1) + min))
|
||||
);
|
||||
return new ComputeResultEvent(event.data);
|
||||
}
|
||||
);
|
||||
// ---cut-end---
|
||||
export async function compute() {
|
||||
'use server';
|
||||
const ui = createStreamableUI();
|
||||
const context = workflow.run(100, {
|
||||
sum: 0
|
||||
});
|
||||
runWithoutBlocking(async () => {
|
||||
for await (const event of context) {
|
||||
if (event instanceof ComputeResultEvent) {
|
||||
// Update UI
|
||||
} else if (event instanceof StopEvent) {
|
||||
// Update UI
|
||||
}
|
||||
// ...
|
||||
}
|
||||
});
|
||||
return ui.value;
|
||||
}
|
||||
```
|
||||
|
||||
<WorkflowStreamingDemo />
|
||||
@@ -5,42 +5,41 @@ description: Install llamaindex by running a single command.
|
||||
|
||||
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
|
||||
|
||||
<Tabs groupId="install-llamaindex" items={["npm", "yarn", "pnpm"]} persist>
|
||||
```shell tab="npm"
|
||||
npm install llamaindex
|
||||
```
|
||||
<Tabs groupId="install" items={["npm", "yarn", "pnpm"]} persist>
|
||||
```shell tab="npm"
|
||||
npm install llamaindex
|
||||
```
|
||||
|
||||
```shell tab="yarn"
|
||||
yarn add llamaindex
|
||||
```
|
||||
|
||||
```shell tab="pnpm"
|
||||
pnpm add llamaindex
|
||||
```
|
||||
```shell tab="yarn"
|
||||
yarn add llamaindex
|
||||
```
|
||||
|
||||
```shell tab="pnpm"
|
||||
pnpm add llamaindex
|
||||
```
|
||||
</Tabs>
|
||||
|
||||
## What's next?
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
title="I'm new to RAG"
|
||||
description="Learn more about RAG (Retrieval-augmented generation) using LlamaIndex.TS."
|
||||
href="/docs/llamaindex/setup/what-is-rag"
|
||||
/>
|
||||
<Card
|
||||
title="I want to try LlamaIndex.TS"
|
||||
description="Learn how to use LlamaIndex.TS with different JS runtime and frameworks."
|
||||
href="/docs/llamaindex/setup"
|
||||
/>
|
||||
<Card
|
||||
title="I want to improve performance when using LlamaIndex.TS"
|
||||
description="Learn how to improve performance, reduce bundle size, improve accuracy."
|
||||
href="/docs/llamaindex/performance"
|
||||
/>
|
||||
<Card
|
||||
title="Show me code examples"
|
||||
description="Explore code examples using LlamaIndex.TS."
|
||||
href="https://stackblitz.com/github/run-llama/LlamaIndexTS/tree/main/examples?file=README.md"
|
||||
/>
|
||||
<Card
|
||||
title="I'm new to RAG"
|
||||
description="Learn more about RAG (Retrieval-augmented generation) using LlamaIndex.TS."
|
||||
href="/docs/llamaindex/setup/what-is-rag"
|
||||
/>
|
||||
<Card
|
||||
title="I want to try LlamaIndex.TS"
|
||||
description="Learn how to use LlamaIndex.TS with different JS runtime and frameworks."
|
||||
href="/docs/llamaindex/setup"
|
||||
/>
|
||||
<Card
|
||||
title="I want to improve performance when using LlamaIndex.TS"
|
||||
description="Learn how to improve performance, reduce bundle size, improve accuracy."
|
||||
href="/docs/llamaindex/performance"
|
||||
/>
|
||||
<Card
|
||||
title="Show me code examples"
|
||||
description="Explore code examples using LlamaIndex.TS."
|
||||
href="https://stackblitz.com/github/run-llama/LlamaIndexTS/tree/main/examples?file=README.md"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: Langtrace
|
||||
description: Learn how to integrate LlamaIndex.TS with Langtrace.
|
||||
---
|
||||
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
|
||||
|
||||
Enhance your observability with Langtrace, a robust open-source tool supports OpenTelemetry and is designed to trace, evaluate, and manage LLM applications seamlessly. Langtrace integrates directly with LlamaIndex, offering detailed, real-time insights into performance metrics such as accuracy, evaluations, and latency.
|
||||
|
||||
## Install
|
||||
|
||||
- Self-host or sign-up and generate an API key using [Langtrace](https://www.langtrace.ai) Cloud
|
||||
|
||||
<Tabs groupId="install-langtrase" items={["npm", "yarn", "pnpm"]} persist>
|
||||
```shell tab="npm"
|
||||
npm install @langtrase/typescript-sdk
|
||||
```
|
||||
|
||||
```shell tab="yarn"
|
||||
yarn add @langtrase/typescript-sdk
|
||||
```
|
||||
|
||||
```shell tab="pnpm"
|
||||
pnpm add @langtrase/typescript-sdk
|
||||
```
|
||||
</Tabs>
|
||||
|
||||
## Initialize
|
||||
|
||||
```js
|
||||
import * as Langtrace from "@langtrase/typescript-sdk";
|
||||
Langtrace.init({ api_key: "<YOUR_API_KEY>" });
|
||||
```
|
||||
|
||||
Features:
|
||||
|
||||
- OpenTelemetry compliant, ensuring broad compatibility with observability platforms.
|
||||
- Provides comprehensive logs and detailed traces of all components.
|
||||
- Real-time monitoring of accuracy, evaluations, usage, costs, and latency.
|
||||
- For more configuration options and details, visit [Langtrace Docs](https://docs.langtrace.ai/introduction).
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Integration",
|
||||
"description": "See our integrations",
|
||||
"pages": ["open-llm-metry", "lang-trace"]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: OpenLLMetry
|
||||
description: Learn how to integrate LlamaIndex.TS with OpenLLMetry.
|
||||
---
|
||||
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
|
||||
|
||||
[OpenLLMetry](https://github.com/traceloop/openllmetry-js) is an open-source project based on OpenTelemetry for tracing and monitoring
|
||||
LLM applications. It connects to [all major observability platforms](https://www.traceloop.com/docs/openllmetry/integrations/introduction) and installs in minutes.
|
||||
|
||||
### Usage Pattern
|
||||
|
||||
|
||||
<Tabs groupId="install-traceloop" items={["npm", "yarn", "pnpm"]} persist>
|
||||
```shell tab="npm"
|
||||
npm install @traceloop/node-server-sdk
|
||||
```
|
||||
|
||||
```shell tab="yarn"
|
||||
yarn add @traceloop/node-server-sdk
|
||||
```
|
||||
|
||||
```shell tab="pnpm"
|
||||
pnpm add @traceloop/node-server-sdk
|
||||
```
|
||||
</Tabs>
|
||||
|
||||
```js
|
||||
import * as traceloop from "@traceloop/node-server-sdk";
|
||||
|
||||
traceloop.initialize({
|
||||
apiKey: process.env.TRACELOOP_API_KEY,
|
||||
disableBatch: true
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Loading Data",
|
||||
"description": "Loading Data using LlamaIndex.TS",
|
||||
"pages": ["index", "node-parser"]
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: Node Parsers / Text Splitters
|
||||
description: Learn how to use Node Parsers and Text Splitters to extract data from documents.
|
||||
---
|
||||
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
|
||||
|
||||
Node parsers are a simple abstraction that take a list of documents, and chunk them into `Node` objects, such that each node is a specific chunk of the parent document. When a document is broken into nodes, all of it's attributes are inherited to the children nodes (i.e. `metadata`, text and metadata templates, etc.). You can read more about `Node` and `Document` properties [here](./).
|
||||
|
||||
## NodeParser
|
||||
|
||||
The `NodeParser` in LlamaIndex is responsible for splitting `Document` objects into more manageable `Node` objects.
|
||||
|
||||
<Tabs items={["with reader", "with node:fs"]}>
|
||||
```ts twoslash tab="with reader"
|
||||
import { TextFileReader } from '@llamaindex/readers/text'
|
||||
import {SentenceSplitter} from '@llamaindex/core/node-parser';
|
||||
|
||||
const nodeParser = new SentenceSplitter();
|
||||
const reader = new TextFileReader();
|
||||
const documents = await reader.loadData('path/to/file.txt');
|
||||
|
||||
const parsedDocuments = nodeParser(documents);
|
||||
// ^?
|
||||
|
||||
```
|
||||
|
||||
```ts twoslash tab="with node:fs"
|
||||
import fs from 'node:fs/promises';
|
||||
import { SentenceSplitter } from '@llamaindex/core/node-parser';
|
||||
|
||||
const nodeParser = new SentenceSplitter();
|
||||
|
||||
const texts = nodeParser.splitText(await fs.readFile('path/to/file.txt', 'utf-8'));
|
||||
// ^?
|
||||
```
|
||||
</Tabs>
|
||||
|
||||
## TextSplitter
|
||||
|
||||
The underlying text splitter will split text by sentences. It can also be used as a standalone module for splitting raw text.
|
||||
|
||||
```ts twoslash
|
||||
import { SentenceSplitter } from "@llamaindex/core/node-parser";
|
||||
|
||||
const splitter = new SentenceSplitter({ chunkSize: 1 });
|
||||
|
||||
const texts = splitter.splitText("Hello World");
|
||||
// ^?
|
||||
```
|
||||
|
||||
## MarkdownNodeParser
|
||||
|
||||
The `MarkdownNodeParser` is a more advanced `NodeParser` that can handle markdown documents. It will split the markdown into nodes and then parse the nodes into a `Document` object.
|
||||
|
||||
<Tabs items={["with reader", "with node:fs"]}>
|
||||
```ts twoslash tab="with reader"
|
||||
import { MarkdownNodeParser } from "@llamaindex/core/node-parser";
|
||||
import { MarkdownReader } from '@llamaindex/readers/markdown'
|
||||
|
||||
const reader = new MarkdownReader();
|
||||
const markdownNodeParser = new MarkdownNodeParser();
|
||||
|
||||
const documents = await reader.loadData('path/to/file.md');
|
||||
const parsedDocuments = markdownNodeParser(documents);
|
||||
// ^?
|
||||
|
||||
```
|
||||
|
||||
```ts twoslash tab="with node:fs"
|
||||
import fs from 'node:fs/promises';
|
||||
import { MarkdownNodeParser } from "@llamaindex/core/node-parser";
|
||||
import { Document } from '@llamaindex/core/schema';
|
||||
|
||||
const markdownNodeParser = new MarkdownNodeParser();
|
||||
const text = await fs.readFile('path/to/file.md', 'utf-8');
|
||||
const document = new Document({ text });
|
||||
|
||||
const parsedDocuments = markdownNodeParser([document]);
|
||||
// ^?
|
||||
|
||||
```
|
||||
</Tabs>
|
||||
@@ -8,6 +8,8 @@
|
||||
"index",
|
||||
"setup",
|
||||
"starter",
|
||||
"readers"
|
||||
"loading",
|
||||
"guide",
|
||||
"integration"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"title": "Loading",
|
||||
"description": "File Readers Collection",
|
||||
"pages": ["index"]
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
<>
|
||||
<SiTypescript className="inline" color="#3178C6" /> TypeScript
|
||||
</>
|
||||
} href="/docs/llamaindex/setup/typescript.mdx" />
|
||||
} href="/docs/llamaindex/setup/typescript" />
|
||||
<Card title={
|
||||
<>
|
||||
<SiVite className='inline' color='#646CFF' /> Vite
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
<Card
|
||||
title={
|
||||
<>
|
||||
<SiNextdotjs className='inline' color='#000000' /> Next.js (React Server Component)
|
||||
<SiNextdotjs className='inline' /> Next.js (React Server Component)
|
||||
</>
|
||||
}
|
||||
href="/docs/llamaindex/setup/next"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export type {
|
||||
HandlerContext,
|
||||
Workflow,
|
||||
WorkflowContext,
|
||||
} from "@llamaindex/workflow";
|
||||
@@ -1,5 +1,12 @@
|
||||
# examples
|
||||
|
||||
## 0.0.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [0765742]
|
||||
- @llamaindex/workflow@0.0.2
|
||||
|
||||
## 0.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { StartEvent, StopEvent, Workflow } from "@llamaindex/workflow";
|
||||
|
||||
type ContextData = {
|
||||
counter: number;
|
||||
};
|
||||
|
||||
const contextData: ContextData = { counter: 0 };
|
||||
|
||||
const workflow = new Workflow<ContextData, string, string>();
|
||||
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (context, startEvent) => {
|
||||
const input = startEvent.data;
|
||||
context.data.counter++;
|
||||
return new StopEvent(`Hello, ${input}!`);
|
||||
},
|
||||
);
|
||||
|
||||
{
|
||||
const ret = await workflow.run("Alex", contextData);
|
||||
console.log(ret.data); // Hello, Alex!
|
||||
}
|
||||
|
||||
{
|
||||
const ret = await workflow.run("World", contextData);
|
||||
console.log(ret.data); // Hello, World!
|
||||
}
|
||||
|
||||
console.log(contextData.counter); // 2
|
||||
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "@llamaindex/examples",
|
||||
"private": true,
|
||||
"version": "0.0.11",
|
||||
"version": "0.0.12",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-js": "^5.2.0",
|
||||
"@azure/identity": "^4.4.1",
|
||||
"@datastax/astra-db-ts": "^1.4.1",
|
||||
"@llamaindex/core": "^0.4.0",
|
||||
"@llamaindex/readers": "^1.0.0",
|
||||
"@llamaindex/workflow": "^0.0.2",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@pinecone-database/pinecone": "^3.0.2",
|
||||
"@vercel/postgres": "^0.10.0",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @llamaindex/autotool
|
||||
|
||||
## 5.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [c7a918c]
|
||||
- llamaindex@0.8.2
|
||||
|
||||
## 5.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @llamaindex/autotool-01-node-example
|
||||
|
||||
## 0.0.45
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [c7a918c]
|
||||
- llamaindex@0.8.2
|
||||
- @llamaindex/autotool@5.0.2
|
||||
|
||||
## 0.0.44
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"scripts": {
|
||||
"start": "node --import tsx --import @llamaindex/autotool/node ./src/index.ts"
|
||||
},
|
||||
"version": "0.0.44"
|
||||
"version": "0.0.45"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @llamaindex/autotool-02-next-example
|
||||
|
||||
## 0.1.89
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [c7a918c]
|
||||
- llamaindex@0.8.2
|
||||
- @llamaindex/autotool@5.0.2
|
||||
|
||||
## 0.1.88
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@llamaindex/autotool-02-next-example",
|
||||
"private": true,
|
||||
"version": "0.1.88",
|
||||
"version": "0.1.89",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@llamaindex/autotool",
|
||||
"type": "module",
|
||||
"version": "5.0.1",
|
||||
"version": "5.0.2",
|
||||
"description": "auto transpile your JS function to LLM Agent compatible",
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": "./dist/index.js",
|
||||
"private": true
|
||||
}
|
||||
@@ -19,7 +19,9 @@ export type BaseEmbeddingOptions = {
|
||||
logProgress?: boolean;
|
||||
};
|
||||
|
||||
export abstract class BaseEmbedding extends TransformComponent {
|
||||
export abstract class BaseEmbedding extends TransformComponent<
|
||||
Promise<BaseNode[]>
|
||||
> {
|
||||
embedBatchSize = DEFAULT_EMBED_BATCH_SIZE;
|
||||
embedInfo?: EmbeddingInfo;
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ import {
|
||||
TransformComponent,
|
||||
} from "../schema";
|
||||
|
||||
export abstract class NodeParser extends TransformComponent {
|
||||
export abstract class NodeParser extends TransformComponent<BaseNode[]> {
|
||||
includeMetadata: boolean = true;
|
||||
includePrevNextRel: boolean = true;
|
||||
|
||||
constructor() {
|
||||
super(async (nodes: BaseNode[]): Promise<BaseNode[]> => {
|
||||
super((nodes: BaseNode[]): BaseNode[] => {
|
||||
return this.getNodesFromDocuments(nodes as TextNode[]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -470,7 +470,7 @@ export function buildNodeFromSplits(
|
||||
doc: BaseNode,
|
||||
refDoc: BaseNode = doc,
|
||||
idGenerator: (idx: number, refDoc: BaseNode) => string = () => randomUUID(),
|
||||
) {
|
||||
): TextNode[] {
|
||||
const nodes: TextNode[] = [];
|
||||
const relationships = {
|
||||
[NodeRelationship.SOURCE]: refDoc.asRelatedNodeInfo(),
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
import { fs, path, randomUUID } from "@llamaindex/env";
|
||||
import type { BaseNode, Document } from "./node";
|
||||
|
||||
interface TransformComponentSignature {
|
||||
interface TransformComponentSignature<
|
||||
Result extends BaseNode[] | Promise<BaseNode[]>,
|
||||
> {
|
||||
<Options extends Record<string, unknown>>(
|
||||
nodes: BaseNode[],
|
||||
options?: Options,
|
||||
): Promise<BaseNode[]>;
|
||||
): Result;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
||||
export interface TransformComponent extends TransformComponentSignature {
|
||||
export interface TransformComponent<
|
||||
Result extends BaseNode[] | Promise<BaseNode[]> =
|
||||
| BaseNode[]
|
||||
| Promise<BaseNode[]>,
|
||||
> extends TransformComponentSignature<Result> {
|
||||
id: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
||||
export class TransformComponent {
|
||||
constructor(transformFn: TransformComponentSignature) {
|
||||
export class TransformComponent<
|
||||
Result extends BaseNode[] | Promise<BaseNode[]> =
|
||||
| BaseNode[]
|
||||
| Promise<BaseNode[]>,
|
||||
> {
|
||||
constructor(transformFn: TransformComponentSignature<Result>) {
|
||||
Object.defineProperties(
|
||||
transformFn,
|
||||
Object.getOwnPropertyDescriptors(this.constructor.prototype),
|
||||
);
|
||||
const transform = function transform(
|
||||
...args: Parameters<TransformComponentSignature>
|
||||
...args: Parameters<TransformComponentSignature<Result>>
|
||||
) {
|
||||
return transformFn(...args);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @llamaindex/experimental
|
||||
|
||||
## 0.0.114
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [c7a918c]
|
||||
- llamaindex@0.8.2
|
||||
|
||||
## 0.0.113
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@llamaindex/experimental",
|
||||
"description": "Experimental package for LlamaIndexTS",
|
||||
"version": "0.0.113",
|
||||
"version": "0.0.114",
|
||||
"type": "module",
|
||||
"types": "dist/type/index.d.ts",
|
||||
"main": "dist/cjs/index.js",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# llamaindex
|
||||
|
||||
## 0.8.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c7a918c: fix: export postprocessors in core
|
||||
|
||||
## 0.8.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @llamaindex/cloudflare-worker-agent-test
|
||||
|
||||
## 0.0.98
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [c7a918c]
|
||||
- llamaindex@0.8.2
|
||||
|
||||
## 0.0.97
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@llamaindex/cloudflare-worker-agent-test",
|
||||
"version": "0.0.97",
|
||||
"version": "0.0.98",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @llamaindex/next-agent-test
|
||||
|
||||
## 0.1.98
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [c7a918c]
|
||||
- llamaindex@0.8.2
|
||||
|
||||
## 0.1.97
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@llamaindex/next-agent-test",
|
||||
"version": "0.1.97",
|
||||
"version": "0.1.98",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# test-edge-runtime
|
||||
|
||||
## 0.1.97
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [c7a918c]
|
||||
- llamaindex@0.8.2
|
||||
|
||||
## 0.1.96
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@llamaindex/nextjs-edge-runtime-test",
|
||||
"version": "0.1.96",
|
||||
"version": "0.1.97",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @llamaindex/next-node-runtime
|
||||
|
||||
## 0.0.79
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [c7a918c]
|
||||
- llamaindex@0.8.2
|
||||
|
||||
## 0.0.78
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@llamaindex/next-node-runtime-test",
|
||||
"version": "0.0.78",
|
||||
"version": "0.0.79",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @llamaindex/waku-query-engine-test
|
||||
|
||||
## 0.0.98
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [c7a918c]
|
||||
- llamaindex@0.8.2
|
||||
|
||||
## 0.0.97
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@llamaindex/waku-query-engine-test",
|
||||
"version": "0.0.97",
|
||||
"version": "0.0.98",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -119,7 +119,7 @@ export class OpenAI implements LLM {
|
||||
}
|
||||
|
||||
export class OpenAIEmbedding
|
||||
extends TransformComponent
|
||||
extends TransformComponent<Promise<BaseNode[]>>
|
||||
implements BaseEmbedding
|
||||
{
|
||||
embedInfo?: EmbeddingInfo;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "llamaindex",
|
||||
"version": "0.8.1",
|
||||
"version": "0.8.2",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
|
||||
@@ -51,6 +51,7 @@ export type {
|
||||
export * from "@llamaindex/core/indices";
|
||||
export * from "@llamaindex/core/llms";
|
||||
export * from "@llamaindex/core/memory";
|
||||
export * from "@llamaindex/core/postprocessor";
|
||||
export * from "@llamaindex/core/prompts";
|
||||
export * from "@llamaindex/core/query-engine";
|
||||
export * from "@llamaindex/core/response-synthesizers";
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": "./dist/index.js",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": "./dist/index.js",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "@llamaindex/node-parser",
|
||||
"version": "0.0.1",
|
||||
"description": "Node parser for LlamaIndex",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./html": {
|
||||
"require": {
|
||||
"types": "./html/dist/index.d.cts",
|
||||
"default": "./html/dist/index.cjs"
|
||||
},
|
||||
"import": {
|
||||
"types": "./html/dist/index.d.ts",
|
||||
"default": "./html/dist/index.js"
|
||||
}
|
||||
},
|
||||
"./code": {
|
||||
"require": {
|
||||
"types": "./code/dist/index.d.cts",
|
||||
"default": "./code/dist/index.cjs"
|
||||
},
|
||||
"import": {
|
||||
"types": "./code/dist/index.d.ts",
|
||||
"default": "./code/dist/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"html",
|
||||
"code"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/run-llama/LlamaIndexTS.git",
|
||||
"directory": "packages/node-parser"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bunchee",
|
||||
"dev": "bunchee --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@llamaindex/core": "workspace:*",
|
||||
"@llamaindex/env": "workspace:*",
|
||||
"@types/html-to-text": "^9.0.4",
|
||||
"@types/node": "^22.8.4",
|
||||
"bunchee": "5.5.1",
|
||||
"tree-sitter": "^0.22.0",
|
||||
"web-tree-sitter": "^0.24.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@llamaindex/core": "workspace:*",
|
||||
"@llamaindex/env": "workspace:*",
|
||||
"tree-sitter": "^0.22.0",
|
||||
"web-tree-sitter": "^0.24.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"html-to-text": "^9.0.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Settings } from "@llamaindex/core/global";
|
||||
import { TextSplitter } from "@llamaindex/core/node-parser";
|
||||
import type Parser from "tree-sitter";
|
||||
import type { SyntaxNode } from "tree-sitter";
|
||||
|
||||
export type CodeSplitterParam = {
|
||||
getParser: () => Parser;
|
||||
maxChars?: number;
|
||||
};
|
||||
|
||||
export const DEFAULT_MAX_CHARS = 1500;
|
||||
|
||||
export class CodeSplitter extends TextSplitter {
|
||||
maxChars: number = DEFAULT_MAX_CHARS;
|
||||
|
||||
#parser: Parser;
|
||||
|
||||
constructor(params: CodeSplitterParam) {
|
||||
super();
|
||||
this.#parser = params.getParser();
|
||||
if (params.maxChars) {
|
||||
this.maxChars = params.maxChars;
|
||||
}
|
||||
}
|
||||
|
||||
#chunkNode(node: SyntaxNode, text: string, lastEnd: number = 0): string[] {
|
||||
let newChunks: string[] = [];
|
||||
let currentChunk: string = "";
|
||||
|
||||
for (const child of node.children) {
|
||||
if (child.endIndex - child.startIndex > this.maxChars) {
|
||||
// Child is too big, recursively chunk the child
|
||||
if (currentChunk.length > 0) {
|
||||
newChunks.push(currentChunk.trim());
|
||||
currentChunk = "";
|
||||
}
|
||||
newChunks = newChunks.concat(this.#chunkNode(child, text, lastEnd));
|
||||
} else if (
|
||||
currentChunk.length + (child.endIndex - child.startIndex) >
|
||||
this.maxChars
|
||||
) {
|
||||
// Child would make the current chunk too big, so start a new chunk
|
||||
newChunks.push(currentChunk.trim());
|
||||
currentChunk = text.slice(lastEnd, child.endIndex);
|
||||
} else {
|
||||
currentChunk += text.slice(lastEnd, child.endIndex);
|
||||
}
|
||||
lastEnd = child.endIndex;
|
||||
}
|
||||
|
||||
if (currentChunk.length > 0) {
|
||||
newChunks.push(currentChunk.trim());
|
||||
}
|
||||
|
||||
return newChunks;
|
||||
}
|
||||
|
||||
splitText(text: string): string[] {
|
||||
const callbackManager = Settings.callbackManager;
|
||||
callbackManager.dispatchEvent("chunking-start", { text: [text] });
|
||||
const tree = this.#parser.parse(text);
|
||||
const rootNode = tree.rootNode;
|
||||
if (
|
||||
rootNode.children.length === 0 ||
|
||||
rootNode.children[0]?.type === "ERROR"
|
||||
) {
|
||||
throw new Error("Could not parse code with language");
|
||||
} else {
|
||||
const chunks = this.#chunkNode(rootNode, text);
|
||||
callbackManager.dispatchEvent("chunking-end", { chunks });
|
||||
return chunks;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { NodeParser } from "@llamaindex/core/node-parser";
|
||||
import {
|
||||
buildNodeFromSplits,
|
||||
MetadataMode,
|
||||
TextNode,
|
||||
} from "@llamaindex/core/schema";
|
||||
import { htmlToText, type HtmlToTextOptions } from "html-to-text";
|
||||
|
||||
export type HTMLNodeParserParam = {
|
||||
htmlToTextOptions?: HtmlToTextOptions;
|
||||
};
|
||||
|
||||
export class HTMLNodeParser extends NodeParser {
|
||||
public readonly htmlToTextOptions: HtmlToTextOptions | undefined = undefined;
|
||||
|
||||
constructor(params?: HTMLNodeParserParam) {
|
||||
super();
|
||||
if (params?.htmlToTextOptions) {
|
||||
this.htmlToTextOptions = params.htmlToTextOptions;
|
||||
}
|
||||
}
|
||||
|
||||
protected parseNodes(documents: TextNode[]): TextNode[] {
|
||||
const nodes: TextNode[] = [];
|
||||
for (const document of documents) {
|
||||
const text = htmlToText(
|
||||
document.getContent(MetadataMode.NONE),
|
||||
this.htmlToTextOptions,
|
||||
);
|
||||
nodes.push(...buildNodeFromSplits([text], document));
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist/type",
|
||||
"tsBuildInfoFile": "./dist/.tsbuildinfo",
|
||||
"emitDeclarationOnly": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["./src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"outputs": ["**/dist/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# @llamaindex/workflow
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0765742: feat: revamped workflow
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "@llamaindex/workflow",
|
||||
"description": "Workflow API",
|
||||
"version": "0.0.2",
|
||||
"type": "module",
|
||||
"types": "dist/index.d.ts",
|
||||
"module": "dist/index.js",
|
||||
"main": "dist/index.cjs",
|
||||
"keywords": [
|
||||
"workflow"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"node": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs",
|
||||
"default": "./dist/index.cjs"
|
||||
},
|
||||
"workerd": {
|
||||
"types": "./dist/index.workerd.d.ts",
|
||||
"default": "./dist/index.workerd.js"
|
||||
},
|
||||
"edge-light": {
|
||||
"types": "./dist/index.edge-light.d.ts",
|
||||
"default": "./dist/index.edge-light.js"
|
||||
},
|
||||
"browser": {
|
||||
"types": "./dist/index.browser.d.ts",
|
||||
"default": "./dist/index.browser.js"
|
||||
},
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"CHANGELOG.md",
|
||||
"!**/*.tsbuildinfo"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/run-llama/LlamaIndexTS.git",
|
||||
"directory": "packages/workflow"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "bunchee --watch",
|
||||
"build": "bunchee"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.8.4",
|
||||
"bunchee": "5.5.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export {
|
||||
WorkflowContext,
|
||||
type HandlerContext,
|
||||
type StepHandler,
|
||||
} from "./workflow-context.js";
|
||||
export { StartEvent, StopEvent, WorkflowEvent } from "./workflow-event.js";
|
||||
export { Workflow, type StepParameters } from "./workflow.js";
|
||||
@@ -0,0 +1,596 @@
|
||||
import {
|
||||
type AnyWorkflowEventConstructor,
|
||||
StartEvent,
|
||||
type StartEventConstructor,
|
||||
StopEvent,
|
||||
type StopEventConstructor,
|
||||
WorkflowEvent,
|
||||
} from "./workflow-event";
|
||||
|
||||
export type StepHandler<
|
||||
Data = unknown,
|
||||
Inputs extends [
|
||||
AnyWorkflowEventConstructor | StartEventConstructor,
|
||||
...(AnyWorkflowEventConstructor | StopEventConstructor)[],
|
||||
] = [AnyWorkflowEventConstructor | StartEventConstructor],
|
||||
Out extends [
|
||||
AnyWorkflowEventConstructor | StartEventConstructor,
|
||||
...(AnyWorkflowEventConstructor | StopEventConstructor)[],
|
||||
] = [AnyWorkflowEventConstructor | StopEventConstructor],
|
||||
> = (
|
||||
context: HandlerContext<Data>,
|
||||
...events: {
|
||||
[K in keyof Inputs]: InstanceType<Inputs[K]>;
|
||||
}
|
||||
) => Promise<
|
||||
{
|
||||
[K in keyof Out]: InstanceType<Out[K]>;
|
||||
}[number]
|
||||
>;
|
||||
|
||||
export type ReadonlyStepMap<Data> = ReadonlyMap<
|
||||
StepHandler<Data, never, never>,
|
||||
{
|
||||
inputs: AnyWorkflowEventConstructor[];
|
||||
outputs: AnyWorkflowEventConstructor[];
|
||||
}
|
||||
>;
|
||||
|
||||
type GlobalEvent = typeof globalThis.Event;
|
||||
|
||||
export type Wait = () => Promise<void>;
|
||||
|
||||
export type ContextParams<Start, Stop, Data> = {
|
||||
startEvent: StartEvent<Start>;
|
||||
contextData: Data;
|
||||
steps: ReadonlyStepMap<Data>;
|
||||
timeout: number | null;
|
||||
verbose: boolean;
|
||||
wait: Wait;
|
||||
|
||||
queue: QueueProtocol[] | undefined;
|
||||
pendingInputQueue: WorkflowEvent<unknown>[] | undefined;
|
||||
resolved: StopEvent<Stop> | null | undefined;
|
||||
rejected: Error | null | undefined;
|
||||
};
|
||||
|
||||
function flattenEvents(
|
||||
acceptEventTypes: AnyWorkflowEventConstructor[],
|
||||
inputEvents: WorkflowEvent<unknown>[],
|
||||
): WorkflowEvent<unknown>[] {
|
||||
const eventMap = new Map<
|
||||
AnyWorkflowEventConstructor,
|
||||
WorkflowEvent<unknown>
|
||||
>();
|
||||
|
||||
for (const event of inputEvents) {
|
||||
for (const acceptType of acceptEventTypes) {
|
||||
if (event instanceof acceptType && !eventMap.has(acceptType)) {
|
||||
eventMap.set(acceptType, event);
|
||||
break; // Once matched, no need to check other accept types
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(eventMap.values());
|
||||
}
|
||||
|
||||
export type HandlerContext<Data = unknown> = {
|
||||
get data(): Data;
|
||||
sendEvent(event: WorkflowEvent<unknown>): void;
|
||||
requireEvent<T extends AnyWorkflowEventConstructor>(
|
||||
event: T,
|
||||
): Promise<InstanceType<T>>;
|
||||
};
|
||||
|
||||
export type QueueProtocol =
|
||||
| {
|
||||
type: "event";
|
||||
event: WorkflowEvent<unknown>;
|
||||
}
|
||||
| {
|
||||
type: "requestEvent";
|
||||
id: string;
|
||||
requestEvent: AnyWorkflowEventConstructor;
|
||||
};
|
||||
|
||||
export class WorkflowContext<Start = string, Stop = string, Data = unknown>
|
||||
implements
|
||||
AsyncIterable<WorkflowEvent<unknown>, unknown, void>,
|
||||
Promise<StopEvent<Stop>>
|
||||
{
|
||||
readonly #steps: ReadonlyStepMap<Data>;
|
||||
|
||||
readonly #startEvent: StartEvent<Start>;
|
||||
readonly #queue: QueueProtocol[] = [];
|
||||
readonly #queueEventTarget = new EventTarget();
|
||||
readonly #wait: Wait;
|
||||
|
||||
#timeout: number | null = null;
|
||||
#verbose: boolean = false;
|
||||
#data: Data;
|
||||
|
||||
#stepCache: WeakMap<
|
||||
WorkflowEvent<unknown>,
|
||||
[
|
||||
step: Set<StepHandler<Data, never, never>>,
|
||||
stepInputs: WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>,
|
||||
stepOutputs: WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>,
|
||||
]
|
||||
> = new Map();
|
||||
|
||||
#getStepFunction(
|
||||
event: WorkflowEvent<unknown>,
|
||||
): [
|
||||
step: Set<StepHandler<Data, never, never>>,
|
||||
stepInputs: WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>,
|
||||
stepOutputs: WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>,
|
||||
] {
|
||||
if (this.#stepCache.has(event)) {
|
||||
return this.#stepCache.get(event)!;
|
||||
}
|
||||
const set = new Set<StepHandler<Data, never, never>>();
|
||||
const stepInputs = new WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>();
|
||||
const stepOutputs = new WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>();
|
||||
const res: [
|
||||
step: Set<StepHandler<Data, never, never>>,
|
||||
stepInputs: WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>,
|
||||
stepOutputs: WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>,
|
||||
] = [set, stepInputs, stepOutputs];
|
||||
this.#stepCache.set(event, res);
|
||||
for (const [step, { inputs, outputs }] of this.#steps) {
|
||||
if (inputs.some((input) => event instanceof input)) {
|
||||
set.add(step);
|
||||
stepInputs.set(step, inputs);
|
||||
stepOutputs.set(step, outputs);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
constructor(params: ContextParams<Start, Stop, Data>) {
|
||||
this.#steps = params.steps;
|
||||
this.#startEvent = params.startEvent;
|
||||
if (typeof params.timeout === "number") {
|
||||
this.#timeout = params.timeout;
|
||||
}
|
||||
this.#data = params.contextData;
|
||||
this.#verbose = params.verbose ?? false;
|
||||
this.#wait = params.wait;
|
||||
|
||||
// push start event to the queue
|
||||
const [step] = this.#getStepFunction(this.#startEvent);
|
||||
if (step.size === 0) {
|
||||
throw new TypeError("No step found for start event");
|
||||
}
|
||||
|
||||
// restore from snapshot
|
||||
if (params.queue) {
|
||||
params.queue.forEach((protocol) => {
|
||||
this.#queue.push(protocol);
|
||||
});
|
||||
} else {
|
||||
this.#sendEvent(this.#startEvent);
|
||||
}
|
||||
if (params.pendingInputQueue) {
|
||||
this.#pendingInputQueue = params.pendingInputQueue;
|
||||
}
|
||||
if (params.resolved) {
|
||||
this.#resolved = params.resolved;
|
||||
}
|
||||
if (params.rejected) {
|
||||
this.#rejected = params.rejected;
|
||||
}
|
||||
}
|
||||
|
||||
// make sure it will only be called once
|
||||
#iterator: AsyncIterableIterator<WorkflowEvent<unknown>> | null = null;
|
||||
#signal: AbortSignal | null = null;
|
||||
|
||||
get #iteratorSingleton(): AsyncIterableIterator<WorkflowEvent<unknown>> {
|
||||
if (this.#iterator === null) {
|
||||
this.#iterator = this.#createStreamEvents();
|
||||
}
|
||||
return this.#iterator;
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator](): AsyncIterableIterator<WorkflowEvent<unknown>> {
|
||||
return this.#iteratorSingleton;
|
||||
}
|
||||
|
||||
#sendEvent = (event: WorkflowEvent<unknown>): void => {
|
||||
this.#queue.push({
|
||||
type: "event",
|
||||
event,
|
||||
});
|
||||
};
|
||||
|
||||
#requireEvent = async <T extends AnyWorkflowEventConstructor>(
|
||||
event: T,
|
||||
): Promise<InstanceType<T>> => {
|
||||
const requestId = crypto.randomUUID();
|
||||
this.#queue.push({
|
||||
type: "requestEvent",
|
||||
id: requestId,
|
||||
requestEvent: event,
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
const handler = (event: InstanceType<GlobalEvent>) => {
|
||||
if (event instanceof CustomEvent) {
|
||||
const { id } = event.detail;
|
||||
if (requestId === id) {
|
||||
this.#queueEventTarget.removeEventListener("update", handler);
|
||||
resolve(event.detail.event);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.#queueEventTarget.addEventListener("update", handler);
|
||||
});
|
||||
};
|
||||
|
||||
#pendingInputQueue: WorkflowEvent<unknown>[] = [];
|
||||
|
||||
// if strict mode is enabled, it will throw an error if there's input or output events are not expected
|
||||
#strict = false;
|
||||
|
||||
strict() {
|
||||
this.#strict = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
get data(): Data {
|
||||
return this.#data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream events from the start event
|
||||
*
|
||||
* Note that this function will stop once there's no more future events,
|
||||
* if you want stop immediately once reach a StopEvent, you should handle it in the other side.
|
||||
* @private
|
||||
*/
|
||||
#createStreamEvents(): AsyncIterableIterator<WorkflowEvent<unknown>> {
|
||||
const isPendingEvents = new WeakSet<WorkflowEvent<unknown>>();
|
||||
const pendingTasks = new Set<Promise<WorkflowEvent<unknown>>>();
|
||||
const enqueuedEvents = new Set<WorkflowEvent<unknown>>();
|
||||
const stream = new ReadableStream<WorkflowEvent<unknown>>({
|
||||
start: async (controller) => {
|
||||
while (true) {
|
||||
const eventProtocol = this.#queue.shift();
|
||||
if (eventProtocol) {
|
||||
switch (eventProtocol.type) {
|
||||
case "requestEvent": {
|
||||
const { id, requestEvent } = eventProtocol;
|
||||
const acceptableInput = this.#pendingInputQueue.find(
|
||||
(event) => event instanceof requestEvent,
|
||||
);
|
||||
if (acceptableInput) {
|
||||
this.#pendingInputQueue.splice(
|
||||
this.#pendingInputQueue.indexOf(acceptableInput),
|
||||
1,
|
||||
);
|
||||
this.#queueEventTarget.dispatchEvent(
|
||||
new CustomEvent("update", {
|
||||
detail: { id, event: acceptableInput },
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// push back to the queue as there are not enough events
|
||||
this.#queue.push(eventProtocol);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "event": {
|
||||
const { event } = eventProtocol;
|
||||
if (isPendingEvents.has(event)) {
|
||||
// this event is still processing
|
||||
this.#sendEvent(event);
|
||||
} else {
|
||||
if (!enqueuedEvents.has(event)) {
|
||||
controller.enqueue(event);
|
||||
enqueuedEvents.add(event);
|
||||
}
|
||||
const [steps, inputsMap, outputsMap] =
|
||||
this.#getStepFunction(event);
|
||||
const nextEventPromises: Promise<WorkflowEvent<unknown>>[] = [
|
||||
...steps,
|
||||
]
|
||||
.map((step) => {
|
||||
const inputs = [...(inputsMap.get(step) ?? [])];
|
||||
const acceptableInputs: WorkflowEvent<unknown>[] =
|
||||
this.#pendingInputQueue.filter((event) =>
|
||||
inputs.some((input) => event instanceof input),
|
||||
);
|
||||
const events: WorkflowEvent<unknown>[] = flattenEvents(
|
||||
inputs,
|
||||
[event, ...acceptableInputs],
|
||||
);
|
||||
if (events.length !== inputs.length) {
|
||||
if (this.#verbose) {
|
||||
console.log(
|
||||
`Not enough inputs for step ${step.name}, waiting for more events`,
|
||||
);
|
||||
}
|
||||
// not enough to run the step, push back to the queue
|
||||
this.#sendEvent(event);
|
||||
isPendingEvents.add(event);
|
||||
return null;
|
||||
}
|
||||
if (isPendingEvents.has(event)) {
|
||||
isPendingEvents.delete(event);
|
||||
}
|
||||
if (this.#verbose) {
|
||||
console.log(
|
||||
`Running step ${step.name} with inputs ${events}`,
|
||||
);
|
||||
}
|
||||
const data = this.data;
|
||||
return (step as StepHandler<Data>)
|
||||
.call(
|
||||
null,
|
||||
{
|
||||
get data() {
|
||||
return data;
|
||||
},
|
||||
sendEvent: this.#sendEvent,
|
||||
requireEvent: this.#requireEvent,
|
||||
},
|
||||
// @ts-expect-error IDK why
|
||||
...events.sort((a, b) => {
|
||||
const aIndex = inputs.indexOf(
|
||||
a.constructor as AnyWorkflowEventConstructor,
|
||||
);
|
||||
const bIndex = inputs.indexOf(
|
||||
b.constructor as AnyWorkflowEventConstructor,
|
||||
);
|
||||
return aIndex - bIndex;
|
||||
}),
|
||||
)
|
||||
.then((nextEvent) => {
|
||||
if (this.#verbose) {
|
||||
console.log(
|
||||
`Step ${step.name} completed, next event is ${nextEvent}`,
|
||||
);
|
||||
}
|
||||
const outputs = outputsMap.get(step) ?? [];
|
||||
const outputEvents = flattenEvents(outputs, [
|
||||
nextEvent,
|
||||
]);
|
||||
if (outputEvents.length !== outputs.length) {
|
||||
if (this.#strict) {
|
||||
const error = Error(
|
||||
`Step ${step.name} returned an unexpected output event ${nextEvent}`,
|
||||
);
|
||||
controller.error(error);
|
||||
} else {
|
||||
console.warn(
|
||||
`Step ${step.name} returned an unexpected output event ${nextEvent}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!(nextEvent instanceof StopEvent)) {
|
||||
this.#pendingInputQueue.unshift(nextEvent);
|
||||
this.#sendEvent(nextEvent);
|
||||
}
|
||||
return nextEvent;
|
||||
});
|
||||
})
|
||||
.filter((promise) => promise !== null);
|
||||
nextEventPromises.forEach((promise) => {
|
||||
pendingTasks.add(promise);
|
||||
promise
|
||||
.catch((err) => {
|
||||
console.error("Error in step", err);
|
||||
})
|
||||
.finally(() => {
|
||||
pendingTasks.delete(promise);
|
||||
});
|
||||
});
|
||||
Promise.race(nextEventPromises)
|
||||
.then((fastestNextEvent) => {
|
||||
if (!enqueuedEvents.has(fastestNextEvent)) {
|
||||
controller.enqueue(fastestNextEvent);
|
||||
enqueuedEvents.add(fastestNextEvent);
|
||||
}
|
||||
return fastestNextEvent;
|
||||
})
|
||||
.then(async (fastestNextEvent) =>
|
||||
Promise.all(nextEventPromises).then((nextEvents) => {
|
||||
for (const nextEvent of nextEvents) {
|
||||
// do not enqueue the same event twice
|
||||
if (fastestNextEvent !== nextEvent) {
|
||||
if (!enqueuedEvents.has(nextEvent)) {
|
||||
controller.enqueue(nextEvent);
|
||||
enqueuedEvents.add(nextEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.catch((err) => {
|
||||
controller.error(err);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.#queue.length === 0 && pendingTasks.size === 0) {
|
||||
if (this.#verbose) {
|
||||
console.log("No more events in the queue");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
await this.#wait();
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
return stream[Symbol.asyncIterator]();
|
||||
}
|
||||
|
||||
with<Initial extends Data>(
|
||||
data: Initial,
|
||||
): WorkflowContext<Start, Stop, Initial> {
|
||||
return new WorkflowContext({
|
||||
startEvent: this.#startEvent,
|
||||
wait: this.#wait,
|
||||
contextData: data,
|
||||
steps: this.#steps,
|
||||
timeout: this.#timeout,
|
||||
verbose: this.#verbose,
|
||||
queue: this.#queue,
|
||||
pendingInputQueue: this.#pendingInputQueue,
|
||||
resolved: this.#resolved,
|
||||
rejected: this.#rejected,
|
||||
});
|
||||
}
|
||||
|
||||
// PromiseLike implementation, this is following the Promise/A+ spec
|
||||
// It will consume the iterator and resolve the promise once it reaches the StopEvent
|
||||
// If you want to customize the behavior, you can use the async iterator directly
|
||||
#resolved: StopEvent<Stop> | null = null;
|
||||
#rejected: Error | null = null;
|
||||
|
||||
async then<TResult1, TResult2 = never>(
|
||||
onfulfilled?:
|
||||
| ((value: StopEvent<Stop>) => TResult1 | PromiseLike<TResult1>)
|
||||
| null
|
||||
| undefined,
|
||||
onrejected?:
|
||||
| ((reason: unknown) => TResult2 | PromiseLike<TResult2>)
|
||||
| null
|
||||
| undefined,
|
||||
) {
|
||||
onfulfilled ??= (value) => value as TResult1;
|
||||
onrejected ??= (reason) => {
|
||||
throw reason;
|
||||
};
|
||||
if (this.#resolved !== null) {
|
||||
return Promise.resolve(this.#resolved).then(onfulfilled, onrejected);
|
||||
} else if (this.#rejected !== null) {
|
||||
return Promise.reject(this.#rejected).then(onfulfilled, onrejected);
|
||||
}
|
||||
|
||||
if (this.#timeout !== null) {
|
||||
const timeout = this.#timeout;
|
||||
this.#signal = AbortSignal.timeout(timeout * 1000);
|
||||
}
|
||||
|
||||
this.#signal?.addEventListener("abort", () => {
|
||||
this.#rejected = new Error(
|
||||
`Operation timed out after ${this.#timeout} seconds`,
|
||||
);
|
||||
onrejected?.(this.#rejected);
|
||||
});
|
||||
try {
|
||||
for await (const event of this.#iteratorSingleton) {
|
||||
if (this.#rejected !== null) {
|
||||
return onrejected?.(this.#rejected);
|
||||
}
|
||||
if (event instanceof StartEvent) {
|
||||
if (this.#verbose) {
|
||||
console.log(`Starting workflow with event ${event}`);
|
||||
}
|
||||
}
|
||||
if (event instanceof StopEvent) {
|
||||
if (this.#verbose && this.#pendingInputQueue.length > 0) {
|
||||
// fixme: #pendingInputQueue might should be cleanup correctly?
|
||||
}
|
||||
this.#resolved = event;
|
||||
return onfulfilled?.(event);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
this.#rejected = err;
|
||||
}
|
||||
return onrejected?.(err);
|
||||
}
|
||||
const nextValue = await this.#iteratorSingleton.next();
|
||||
if (nextValue.done === false) {
|
||||
this.#rejected = new Error("Workflow did not complete");
|
||||
return onrejected?.(this.#rejected);
|
||||
}
|
||||
return onrejected?.(new Error("UNREACHABLE"));
|
||||
}
|
||||
|
||||
catch<TResult = never>(
|
||||
onrejected?:
|
||||
| ((reason: unknown) => TResult | PromiseLike<TResult>)
|
||||
| null
|
||||
| undefined,
|
||||
) {
|
||||
return this.then((v) => v, onrejected);
|
||||
}
|
||||
|
||||
finally(onfinally?: (() => void) | undefined | null) {
|
||||
return this.then(
|
||||
() => {
|
||||
onfinally?.();
|
||||
},
|
||||
() => {
|
||||
onfinally?.();
|
||||
},
|
||||
) as Promise<never>;
|
||||
}
|
||||
|
||||
[Symbol.toStringTag]: string = "Context";
|
||||
|
||||
// for worker thread
|
||||
snapshot(): ArrayBuffer {
|
||||
const state = {
|
||||
startEvent: this.#startEvent,
|
||||
queue: this.#queue,
|
||||
pendingInputQueue: this.#pendingInputQueue,
|
||||
data: this.#data,
|
||||
timeout: this.#timeout,
|
||||
verbose: this.#verbose,
|
||||
resolved: this.#resolved,
|
||||
rejected: this.#rejected,
|
||||
};
|
||||
|
||||
const jsonString = JSON.stringify(state, (_, value) => {
|
||||
// If value is an instance of a class, serialize only its properties
|
||||
if (value instanceof WorkflowEvent) {
|
||||
return { data: value.data, constructor: value.constructor.name };
|
||||
}
|
||||
// value is Subtype of WorkflowEvent
|
||||
if (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
value?.prototype instanceof WorkflowEvent
|
||||
) {
|
||||
return { constructor: value.prototype.constructor.name };
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
return new TextEncoder().encode(jsonString).buffer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
export class WorkflowEvent<Data> {
|
||||
displayName: string;
|
||||
data: Data;
|
||||
|
||||
constructor(data: Data) {
|
||||
this.data = data;
|
||||
this.displayName = this.constructor.name;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.displayName;
|
||||
}
|
||||
|
||||
static or<
|
||||
A extends AnyWorkflowEventConstructor,
|
||||
B extends AnyWorkflowEventConstructor,
|
||||
>(AEvent: A, BEvent: B): A | B {
|
||||
function OrEvent() {
|
||||
throw new Error("Cannot instantiate OrEvent");
|
||||
}
|
||||
|
||||
OrEvent.prototype = Object.create(AEvent.prototype);
|
||||
|
||||
Object.getOwnPropertyNames(BEvent.prototype).forEach((property) => {
|
||||
if (!(property in OrEvent.prototype)) {
|
||||
Object.defineProperty(
|
||||
OrEvent.prototype,
|
||||
property,
|
||||
Object.getOwnPropertyDescriptor(BEvent.prototype, property)!,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
OrEvent.prototype.constructor = OrEvent;
|
||||
|
||||
Object.defineProperty(OrEvent, Symbol.hasInstance, {
|
||||
value: function (instance: unknown) {
|
||||
return instance instanceof AEvent || instance instanceof BEvent;
|
||||
},
|
||||
});
|
||||
|
||||
return OrEvent as unknown as A | B;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type AnyWorkflowEventConstructor = new (data: any) => WorkflowEvent<any>;
|
||||
|
||||
export type StartEventConstructor<T = string> = new (data: T) => StartEvent<T>;
|
||||
export type StopEventConstructor<T = string> = new (data: T) => StopEvent<T>;
|
||||
|
||||
// These are special events that are used to control the workflow
|
||||
export class StartEvent<T = string> extends WorkflowEvent<T> {
|
||||
constructor(data: T) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
export class StopEvent<T = string> extends WorkflowEvent<T> {
|
||||
constructor(data: T) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import {
|
||||
WorkflowContext,
|
||||
type HandlerContext,
|
||||
type QueueProtocol,
|
||||
type StepHandler,
|
||||
type Wait,
|
||||
} from "./workflow-context.js";
|
||||
import {
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
type AnyWorkflowEventConstructor,
|
||||
type StartEventConstructor,
|
||||
type StopEventConstructor,
|
||||
} from "./workflow-event.js";
|
||||
|
||||
export type StepParameters<
|
||||
In extends AnyWorkflowEventConstructor[],
|
||||
Out extends AnyWorkflowEventConstructor[],
|
||||
> = {
|
||||
inputs: In;
|
||||
outputs: Out;
|
||||
};
|
||||
|
||||
export class Workflow<ContextData, Start, Stop> {
|
||||
#steps: Map<
|
||||
StepHandler<ContextData, never, never>,
|
||||
{
|
||||
inputs: AnyWorkflowEventConstructor[];
|
||||
outputs: AnyWorkflowEventConstructor[];
|
||||
}
|
||||
> = new Map();
|
||||
#verbose: boolean = false;
|
||||
#timeout: number | null = null;
|
||||
// fixme: allow microtask
|
||||
#nextTick: Wait = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
constructor(
|
||||
params: {
|
||||
verbose?: boolean;
|
||||
timeout?: number | null;
|
||||
wait?: Wait;
|
||||
} = {},
|
||||
) {
|
||||
if (params.verbose) {
|
||||
this.#verbose = params.verbose;
|
||||
}
|
||||
if (params.timeout) {
|
||||
this.#timeout = params.timeout;
|
||||
}
|
||||
if (params.wait) {
|
||||
this.#nextTick = params.wait;
|
||||
}
|
||||
}
|
||||
|
||||
addStep<
|
||||
const In extends [
|
||||
AnyWorkflowEventConstructor | StartEventConstructor,
|
||||
...(AnyWorkflowEventConstructor | StopEventConstructor)[],
|
||||
],
|
||||
const Out extends [
|
||||
AnyWorkflowEventConstructor | StopEventConstructor,
|
||||
...(AnyWorkflowEventConstructor | StopEventConstructor)[],
|
||||
],
|
||||
>(
|
||||
parameters: StepParameters<In, Out>,
|
||||
stepFn: (
|
||||
context: HandlerContext<ContextData>,
|
||||
...events: {
|
||||
[K in keyof In]: InstanceType<In[K]>;
|
||||
}
|
||||
) => Promise<
|
||||
{
|
||||
[K in keyof Out]: InstanceType<Out[K]>;
|
||||
}[number]
|
||||
>,
|
||||
): this {
|
||||
const { inputs, outputs } = parameters;
|
||||
this.#steps.set(stepFn as never, { inputs, outputs });
|
||||
return this;
|
||||
}
|
||||
|
||||
removeStep(stepFn: StepHandler): this {
|
||||
this.#steps.delete(stepFn);
|
||||
return this;
|
||||
}
|
||||
|
||||
run(
|
||||
event: StartEvent<Start> | Start,
|
||||
): unknown extends ContextData
|
||||
? WorkflowContext<Start, Stop, ContextData>
|
||||
: WorkflowContext<Start, Stop, ContextData | undefined>;
|
||||
run<Data extends ContextData>(
|
||||
event: StartEvent<Start> | Start,
|
||||
data: Data,
|
||||
): WorkflowContext<Start, Stop, Data>;
|
||||
run<Data extends ContextData>(
|
||||
event: StartEvent<Start> | Start,
|
||||
data?: Data,
|
||||
): WorkflowContext<Start, Stop, Data> {
|
||||
const startEvent: StartEvent<Start> =
|
||||
event instanceof StartEvent ? event : new StartEvent(event);
|
||||
|
||||
return new WorkflowContext<Start, Stop, Data>({
|
||||
startEvent,
|
||||
wait: this.#nextTick,
|
||||
contextData: data!,
|
||||
steps: new Map(this.#steps),
|
||||
timeout: this.#timeout,
|
||||
verbose: this.#verbose,
|
||||
queue: undefined,
|
||||
pendingInputQueue: undefined,
|
||||
resolved: null,
|
||||
rejected: null,
|
||||
});
|
||||
}
|
||||
|
||||
recover(data: ArrayBuffer): WorkflowContext<Start, Stop, ContextData> {
|
||||
const jsonString = new TextDecoder().decode(data);
|
||||
|
||||
const state = JSON.parse(jsonString);
|
||||
|
||||
const reconstructedStartEvent = new StartEvent<Start>(state.startEvent);
|
||||
const AllEvents = [...this.#steps]
|
||||
.map(([, { inputs, outputs }]) => [...inputs, ...(outputs ?? [])])
|
||||
.flat();
|
||||
const reconstructedQueue: QueueProtocol[] = state.queue.map(
|
||||
(protocol: QueueProtocol): QueueProtocol => {
|
||||
switch (protocol.type) {
|
||||
case "requestEvent": {
|
||||
const { requestEvent, id } = protocol;
|
||||
const EventType = AllEvents.find(
|
||||
(type) =>
|
||||
type.prototype.constructor.name ===
|
||||
(requestEvent.constructor as unknown as string),
|
||||
);
|
||||
if (!EventType) {
|
||||
throw new TypeError(
|
||||
`Event type not found: ${requestEvent.constructor}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
type: "requestEvent",
|
||||
id,
|
||||
requestEvent: EventType,
|
||||
};
|
||||
}
|
||||
case "event": {
|
||||
const { event } = protocol;
|
||||
const EventType = AllEvents.find(
|
||||
(type) =>
|
||||
type.prototype.constructor.name ===
|
||||
(event.constructor as unknown as string),
|
||||
);
|
||||
if (!EventType) {
|
||||
throw new TypeError(`Event type not found: ${event.constructor}`);
|
||||
}
|
||||
return {
|
||||
type: "event",
|
||||
event: new EventType(event.data),
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const reconstructedPendingInputQueue = state.pendingInputQueue.map(
|
||||
(event: Record<string, unknown>) => {
|
||||
const EventType = AllEvents.find(
|
||||
(type) => type.prototype.constructor.name === event.constructor,
|
||||
);
|
||||
if (!EventType) {
|
||||
throw new TypeError(`Event type not found: ${event.constructor}`);
|
||||
}
|
||||
return new EventType(event.data);
|
||||
},
|
||||
);
|
||||
|
||||
return new WorkflowContext<Start, Stop, ContextData>({
|
||||
startEvent: reconstructedStartEvent,
|
||||
contextData: state.data,
|
||||
wait: this.#nextTick,
|
||||
steps: this.#steps, // Assuming steps do not change and are part of the class prototype or similar
|
||||
timeout: state.timeout,
|
||||
verbose: state.verbose,
|
||||
queue: reconstructedQueue,
|
||||
pendingInputQueue: reconstructedPendingInputQueue,
|
||||
resolved: state.resolved ? new StopEvent<Stop>(state.resolved) : null,
|
||||
rejected: state.rejected ? new Error(state.rejected) : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist/type",
|
||||
"tsBuildInfoFile": "./dist/.tsbuildinfo",
|
||||
"emitDeclarationOnly": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
|
||||
"types": ["node"],
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["./src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Generated
+2443
-3176
File diff suppressed because it is too large
Load Diff
@@ -95,6 +95,9 @@
|
||||
{
|
||||
"path": "./packages/env/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "./packages/workflow/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "./packages/env/tests/tsconfig.json"
|
||||
},
|
||||
@@ -107,6 +110,12 @@
|
||||
{
|
||||
"path": "./examples/vector-store/pg/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "./packages/readers/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "./packages/node-parser/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "./packages/experimental/tsconfig.json"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @llamaindex/unit-test
|
||||
|
||||
## 0.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [0765742]
|
||||
- @llamaindex/workflow@0.0.2
|
||||
|
||||
## 0.0.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [c7a918c]
|
||||
- llamaindex@0.8.2
|
||||
|
||||
## 0.0.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { CodeSplitter } from "@llamaindex/node-parser/code";
|
||||
import Parser from "tree-sitter";
|
||||
import JS from "tree-sitter-javascript";
|
||||
import TS from "tree-sitter-typescript";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
describe("CodeSplitter", () => {
|
||||
test("basic split js", async () => {
|
||||
const parser = new Parser();
|
||||
parser.setLanguage(JS);
|
||||
const codeSplitter = new CodeSplitter({
|
||||
maxChars: "const a = 1;".length,
|
||||
getParser: () => parser,
|
||||
});
|
||||
const result = codeSplitter.splitText(
|
||||
"const a = 1; const b = 2; const c = 3; const d = 4;",
|
||||
);
|
||||
expect(result).toEqual([
|
||||
"const a = 1;",
|
||||
"const b = 2;",
|
||||
"const c = 3;",
|
||||
"const d = 4;",
|
||||
]);
|
||||
});
|
||||
test("basic split ts", async () => {
|
||||
const parser = new Parser();
|
||||
parser.setLanguage(TS.typescript);
|
||||
const codeSplitter = new CodeSplitter({
|
||||
maxChars: "const a: number = 1;".length,
|
||||
getParser: () => parser,
|
||||
});
|
||||
const result = codeSplitter.splitText(
|
||||
"const a: number = 1; const b = 2; const c: number = 3; const d = 4;",
|
||||
);
|
||||
expect(result).toEqual([
|
||||
"const a: number = 1;",
|
||||
"const b = 2;",
|
||||
"const c: number = 3;",
|
||||
"const d = 4;",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Document } from "@llamaindex/core/schema";
|
||||
import { HTMLNodeParser } from "@llamaindex/node-parser/html";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
describe("HTMLNodeParser", () => {
|
||||
test("basic split", async () => {
|
||||
const parser = new HTMLNodeParser();
|
||||
const result = parser.getNodesFromDocuments([
|
||||
new Document({
|
||||
text: `<DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hello World</p>
|
||||
</body>
|
||||
</html>`,
|
||||
}),
|
||||
]);
|
||||
expect(result.length).toEqual(1);
|
||||
expect(result[0]!.getContent()).toEqual("Hello World");
|
||||
});
|
||||
});
|
||||
+11
-2
@@ -1,21 +1,30 @@
|
||||
{
|
||||
"name": "@llamaindex/unit-test",
|
||||
"private": true,
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.11",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^9.0.1",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"msw": "^2.6.0",
|
||||
"vitest": "^2.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@llamaindex/cloud": "workspace:*",
|
||||
"@llamaindex/core": "workspace:*",
|
||||
"@llamaindex/node-parser": "workspace:*",
|
||||
"@llamaindex/openai": "workspace:*",
|
||||
"@llamaindex/readers": "workspace:*",
|
||||
"llamaindex": "workspace:*"
|
||||
"@llamaindex/workflow": "workspace:*",
|
||||
"llamaindex": "workspace:*",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tree-sitter": "^0.22.0",
|
||||
"tree-sitter-javascript": "^0.23.0",
|
||||
"tree-sitter-typescript": "^0.23.0"
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -4,9 +4,10 @@
|
||||
"outDir": "./lib",
|
||||
"module": "node16",
|
||||
"moduleResolution": "node16",
|
||||
"target": "ESNext"
|
||||
"target": "ESNext",
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["./**/*.ts"],
|
||||
"include": ["./**/*.ts", "./**/*.tsx"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../packages/core/tsconfig.json"
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
import type { ReactNode } from "react";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
describe("workflow integration", () => {
|
||||
type Context = {
|
||||
pending: string[];
|
||||
};
|
||||
type Start = string;
|
||||
type Stop = ReactNode;
|
||||
|
||||
test("nodejs", async () => {
|
||||
const workflow = new Workflow<never, Start, Stop>({
|
||||
wait: async () => await new Promise((resolve) => setTimeout(resolve, 0)),
|
||||
});
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async (_, __) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
return new StopEvent("hello");
|
||||
},
|
||||
);
|
||||
|
||||
console.log("start");
|
||||
|
||||
const run = workflow.run("start");
|
||||
await run.then((stop) => {
|
||||
expect(stop.data).toBe("hello");
|
||||
});
|
||||
});
|
||||
|
||||
test("with jsx", async () => {
|
||||
const workflow = new Workflow<never, Start, Stop>();
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async (_, __) => {
|
||||
return new StopEvent(<div>Hey there!</div>);
|
||||
},
|
||||
);
|
||||
|
||||
const run = workflow.run("start");
|
||||
const stop = await run;
|
||||
expect(stop.data).toEqual(<div>Hey there!</div>);
|
||||
});
|
||||
|
||||
test("with message channel", async () => {
|
||||
const workflow = new Workflow<Context, Start, Stop>();
|
||||
|
||||
class AnalysisStartEvent extends WorkflowEvent<string> {}
|
||||
|
||||
class AnalysisStopEvent extends WorkflowEvent<string> {}
|
||||
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async ({ data, sendEvent, requireEvent }) => {
|
||||
data.pending.push("analyzing");
|
||||
sendEvent(new AnalysisStartEvent("analysis my document"));
|
||||
const event = await requireEvent(AnalysisStopEvent);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
data.pending.push("analysis complete");
|
||||
return new StopEvent(event.data);
|
||||
},
|
||||
);
|
||||
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [AnalysisStartEvent],
|
||||
outputs: [AnalysisStopEvent],
|
||||
},
|
||||
async ({ data }) => {
|
||||
data.pending.push("loading document");
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
data.pending.push("document loaded");
|
||||
return new AnalysisStopEvent("analysis complete");
|
||||
},
|
||||
);
|
||||
|
||||
const run = workflow.run("start").with({
|
||||
pending: [],
|
||||
});
|
||||
await run;
|
||||
expect(run.data.pending).toEqual([
|
||||
"analyzing",
|
||||
"loading document",
|
||||
"document loaded",
|
||||
"analysis complete",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,871 @@
|
||||
import type {
|
||||
HandlerContext,
|
||||
StepHandler,
|
||||
StepParameters,
|
||||
} from "@llamaindex/workflow";
|
||||
import {
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
expectTypeOf,
|
||||
test,
|
||||
vi,
|
||||
type Mocked,
|
||||
} from "vitest";
|
||||
|
||||
class JokeEvent extends WorkflowEvent<{ joke: string }> {}
|
||||
|
||||
class AnalysisEvent extends WorkflowEvent<{ analysis: string }> {}
|
||||
|
||||
describe("type system", () => {
|
||||
test("handler", () => {
|
||||
type Parameters = StepParameters<
|
||||
[typeof StartEvent<string>],
|
||||
[typeof StopEvent<string>]
|
||||
>;
|
||||
type Handler = (
|
||||
context: HandlerContext,
|
||||
ev: StartEvent<string>,
|
||||
) => Promise<StopEvent<string>>;
|
||||
type Handler2 = (
|
||||
context: HandlerContext,
|
||||
ev: StartEvent<string>,
|
||||
) => Promise<StopEvent<number>>;
|
||||
type Handler3 = (
|
||||
context: HandlerContext,
|
||||
ev: StartEvent<string>,
|
||||
) => Promise<AnalysisEvent>;
|
||||
expectTypeOf<Parameters>().toEqualTypeOf<{
|
||||
inputs: [typeof StartEvent<string>];
|
||||
outputs: [typeof StopEvent<string>];
|
||||
}>();
|
||||
expectTypeOf<
|
||||
StepHandler<
|
||||
unknown,
|
||||
[typeof StartEvent<string>],
|
||||
[typeof StopEvent<string>]
|
||||
>
|
||||
>().toEqualTypeOf<Handler>();
|
||||
expectTypeOf<
|
||||
StepHandler<
|
||||
unknown,
|
||||
[typeof StartEvent<string>],
|
||||
[typeof StopEvent<string>]
|
||||
>
|
||||
>().not.toEqualTypeOf<Handler2>();
|
||||
expectTypeOf<
|
||||
StepHandler<
|
||||
unknown,
|
||||
[typeof StartEvent<string>],
|
||||
[typeof StopEvent<string>]
|
||||
>
|
||||
>().not.toEqualTypeOf<Handler3>();
|
||||
});
|
||||
});
|
||||
|
||||
describe("workflow basic", () => {
|
||||
let generateJoke: Mocked<
|
||||
(context: HandlerContext, ev: StartEvent) => Promise<JokeEvent>
|
||||
>;
|
||||
let critiqueJoke: Mocked<
|
||||
(context: HandlerContext, ev: JokeEvent) => Promise<StopEvent<string>>
|
||||
>;
|
||||
let analyzeJoke: Mocked<
|
||||
(context: HandlerContext, ev: JokeEvent) => Promise<AnalysisEvent>
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
generateJoke = vi.fn(async (_context, _: StartEvent) => {
|
||||
return new JokeEvent({ joke: "a joke" });
|
||||
});
|
||||
|
||||
critiqueJoke = vi.fn(async (_context, _: JokeEvent) => {
|
||||
return new StopEvent("stop");
|
||||
});
|
||||
|
||||
analyzeJoke = vi.fn(async (_context: HandlerContext, _: JokeEvent) => {
|
||||
return new AnalysisEvent({ analysis: "an analysis" });
|
||||
});
|
||||
});
|
||||
|
||||
test("workflow basic", async () => {
|
||||
const workflow = new Workflow<
|
||||
{
|
||||
foo: string;
|
||||
bar: number;
|
||||
},
|
||||
string,
|
||||
string
|
||||
>();
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async ({ data }, start) => {
|
||||
expect(start).toBeInstanceOf(StartEvent);
|
||||
expect(start.data).toBe("start");
|
||||
expect(data.bar).toBe(42);
|
||||
expect(data.foo).toBe("foo");
|
||||
return new StopEvent("stopped");
|
||||
},
|
||||
);
|
||||
|
||||
const result = workflow.run("start", {
|
||||
foo: "foo",
|
||||
bar: 42,
|
||||
});
|
||||
await result;
|
||||
});
|
||||
|
||||
test("run workflow", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
jokeFlow.addStep(
|
||||
{ inputs: [StartEvent<string>], outputs: [JokeEvent] },
|
||||
generateJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{ inputs: [JokeEvent], outputs: [StopEvent] },
|
||||
critiqueJoke,
|
||||
);
|
||||
|
||||
const result = await jokeFlow.run("pirates");
|
||||
|
||||
expect(generateJoke).toHaveBeenCalledTimes(1);
|
||||
expect(critiqueJoke).toHaveBeenCalledTimes(1);
|
||||
expect(result.data).toBe("stop");
|
||||
});
|
||||
|
||||
test("stream events", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [JokeEvent],
|
||||
},
|
||||
generateJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
critiqueJoke,
|
||||
);
|
||||
|
||||
const run = jokeFlow.run("pirates");
|
||||
const event = await run[Symbol.asyncIterator]().next(); // get one event to avoid testing timeout
|
||||
const result = await run;
|
||||
|
||||
expect(generateJoke).toHaveBeenCalledTimes(1);
|
||||
expect(critiqueJoke).toHaveBeenCalledTimes(1);
|
||||
expect(result.data).toBe("stop");
|
||||
expect(event).not.toBeNull();
|
||||
});
|
||||
|
||||
test("workflow timeout", async () => {
|
||||
const TIMEOUT = 1;
|
||||
const jokeFlow = new Workflow<unknown, string, string>({
|
||||
verbose: true,
|
||||
timeout: TIMEOUT,
|
||||
});
|
||||
|
||||
const longRunning = async (_context: HandlerContext, ev: StartEvent) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for 2 seconds
|
||||
return new StopEvent("We waited 2 seconds");
|
||||
};
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
longRunning,
|
||||
);
|
||||
const run = jokeFlow.run("Let's start");
|
||||
await expect(run).rejects.toThrow(
|
||||
`Operation timed out after ${TIMEOUT} seconds`,
|
||||
);
|
||||
});
|
||||
|
||||
test("workflow validation", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
jokeFlow.addStep(
|
||||
{ inputs: [StartEvent<string>], outputs: [StopEvent] },
|
||||
generateJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{ inputs: [JokeEvent], outputs: [StopEvent] },
|
||||
critiqueJoke,
|
||||
);
|
||||
expect(async () => {
|
||||
await jokeFlow.run("pirates").strict();
|
||||
}).rejects.toThrow(
|
||||
"Step spy returned an unexpected output event JokeEvent",
|
||||
);
|
||||
});
|
||||
|
||||
test("requireEvents - 1", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async (ctx, start) => {
|
||||
ctx.sendEvent(new AnalysisEvent({ analysis: "an analysis" }));
|
||||
await ctx.requireEvent(JokeEvent);
|
||||
return new StopEvent("Report generated");
|
||||
},
|
||||
);
|
||||
|
||||
const fn = vi.fn(async () => {
|
||||
return new JokeEvent({ joke: "a joke" });
|
||||
});
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [AnalysisEvent],
|
||||
outputs: [JokeEvent],
|
||||
},
|
||||
fn,
|
||||
);
|
||||
|
||||
const result = await jokeFlow.run("pirates");
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
expect(result.data).toBe("Report generated");
|
||||
});
|
||||
|
||||
test("run workflow with multiple in-degree", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [JokeEvent],
|
||||
},
|
||||
async (context, _) => {
|
||||
context.sendEvent(
|
||||
new AnalysisEvent({
|
||||
analysis: "an analysis",
|
||||
}),
|
||||
);
|
||||
return new JokeEvent({
|
||||
joke: "a joke",
|
||||
});
|
||||
},
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent, AnalysisEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async () => {
|
||||
return new StopEvent("The analysis is insightful and helpful.");
|
||||
},
|
||||
);
|
||||
|
||||
const result = await jokeFlow.run("pirates");
|
||||
expect(result.data).toBe("The analysis is insightful and helpful.");
|
||||
});
|
||||
|
||||
test("run invalid workflow", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [JokeEvent],
|
||||
},
|
||||
generateJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
// @ts-expect-error it actually returns AnalysisEvent
|
||||
analyzeJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async () => {
|
||||
return new StopEvent("The analysis is insightful and helpful.");
|
||||
},
|
||||
);
|
||||
const consoleSpy = vi.spyOn(console, "warn");
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(0);
|
||||
const result = await jokeFlow.run("pirates");
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(1);
|
||||
consoleSpy.mockRestore();
|
||||
expect(result.data).toBe("The analysis is insightful and helpful.");
|
||||
});
|
||||
|
||||
test("run workflow with object-based StartEvent and StopEvent", async () => {
|
||||
const objectFlow = new Workflow<
|
||||
unknown,
|
||||
Person,
|
||||
{
|
||||
result: {
|
||||
greeting: string;
|
||||
};
|
||||
}
|
||||
>({ verbose: true });
|
||||
|
||||
type Person = { name: string; age: number };
|
||||
|
||||
const processObject = vi.fn(async (_context, ev: StartEvent<Person>) => {
|
||||
const { name, age } = ev.data;
|
||||
return new StopEvent({
|
||||
result: { greeting: `Hello ${name}, you are ${age} years old!` },
|
||||
});
|
||||
});
|
||||
|
||||
objectFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<Person>],
|
||||
outputs: [
|
||||
StopEvent<{
|
||||
result: {
|
||||
greeting: string;
|
||||
};
|
||||
}>,
|
||||
],
|
||||
},
|
||||
processObject,
|
||||
);
|
||||
|
||||
const result = await objectFlow.run(
|
||||
new StartEvent<Person>({ name: "Alice", age: 30 }),
|
||||
);
|
||||
|
||||
expect(processObject).toHaveBeenCalledTimes(1);
|
||||
expect(result.data.result).toEqual({
|
||||
greeting: "Hello Alice, you are 30 years old!",
|
||||
});
|
||||
});
|
||||
|
||||
test("workflow with two concurrent steps", async () => {
|
||||
const concurrentFlow = new Workflow<unknown, string, string>({
|
||||
verbose: true,
|
||||
});
|
||||
|
||||
const step1 = vi.fn(async (_context, _ev: StartEvent) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
return new StopEvent("Step 1 completed");
|
||||
});
|
||||
|
||||
const step2 = vi.fn(async (_context, _ev: StartEvent) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
return new StopEvent("Step 2 completed");
|
||||
});
|
||||
|
||||
concurrentFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
step1,
|
||||
);
|
||||
concurrentFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
step2,
|
||||
);
|
||||
|
||||
const startTime = new Date();
|
||||
const result = await concurrentFlow.run("start");
|
||||
const endTime = new Date();
|
||||
const duration = endTime.getTime() - startTime.getTime();
|
||||
|
||||
expect(step1).toHaveBeenCalledTimes(1);
|
||||
expect(step2).toHaveBeenCalledTimes(1);
|
||||
expect(duration).toBeLessThan(200);
|
||||
expect(result.data).toBe("Step 2 completed");
|
||||
});
|
||||
|
||||
test("workflow with two concurrent cyclic steps", async () => {
|
||||
const concurrentCyclicFlow = new Workflow<unknown, string, string>({
|
||||
verbose: true,
|
||||
});
|
||||
|
||||
class Step1Event extends WorkflowEvent<{
|
||||
result: string;
|
||||
}> {}
|
||||
|
||||
class Step2Event extends WorkflowEvent<{
|
||||
result: string;
|
||||
}> {}
|
||||
|
||||
let step2Count = 0;
|
||||
|
||||
const step1 = vi.fn(async (_context, ev: StartEvent | Step1Event) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return new Step1Event({ result: "Step 1 completed" });
|
||||
});
|
||||
|
||||
const step2 = vi.fn(async (_context, ev: StartEvent | Step2Event) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
step2Count++;
|
||||
if (step2Count >= 5) {
|
||||
return new StopEvent("Step 2 completed 5 times");
|
||||
}
|
||||
return new Step2Event({ result: "Step 2 completed" });
|
||||
});
|
||||
|
||||
concurrentCyclicFlow.addStep(
|
||||
{
|
||||
inputs: [WorkflowEvent.or(StartEvent<string>, Step1Event)],
|
||||
outputs: [Step1Event],
|
||||
},
|
||||
step1,
|
||||
);
|
||||
concurrentCyclicFlow.addStep(
|
||||
{
|
||||
inputs: [WorkflowEvent.or(StartEvent<string>, Step2Event)],
|
||||
outputs: [Step2Event, StopEvent],
|
||||
},
|
||||
step2,
|
||||
);
|
||||
|
||||
const startTime = new Date();
|
||||
const result = await concurrentCyclicFlow.run("start");
|
||||
const endTime = new Date();
|
||||
const duration = endTime.getTime() - startTime.getTime();
|
||||
|
||||
expect(step1).toHaveBeenCalledTimes(1);
|
||||
expect(step2).toHaveBeenCalledTimes(5);
|
||||
expect(duration).toBeGreaterThanOrEqual(500); // At least 5 * 100ms for step2
|
||||
expect(duration).toBeLessThan(1000); // Less than 1 second
|
||||
expect(result.data).toBe("Step 2 completed 5 times");
|
||||
});
|
||||
|
||||
test("sendEvent", async () => {
|
||||
const myWorkflow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
class QueryEvent extends WorkflowEvent<{ query: string }> {}
|
||||
|
||||
class QueryResultEvent extends WorkflowEvent<{ result: string }> {}
|
||||
|
||||
class PendingEvent extends WorkflowEvent<void> {}
|
||||
|
||||
myWorkflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [PendingEvent],
|
||||
},
|
||||
async (context: HandlerContext, events) => {
|
||||
context.sendEvent(new QueryEvent({ query: "something" }));
|
||||
return new PendingEvent();
|
||||
},
|
||||
);
|
||||
|
||||
myWorkflow.addStep(
|
||||
{
|
||||
inputs: [QueryEvent],
|
||||
outputs: [QueryResultEvent],
|
||||
},
|
||||
async (context, event) => {
|
||||
return new QueryResultEvent({ result: "query result" });
|
||||
},
|
||||
);
|
||||
|
||||
myWorkflow.addStep(
|
||||
{
|
||||
inputs: [PendingEvent, QueryResultEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (context, ev0, ev1) => {
|
||||
return new StopEvent(ev1.data.result);
|
||||
},
|
||||
);
|
||||
|
||||
const result = await myWorkflow.run("start");
|
||||
expect(result.data).toBe("query result");
|
||||
});
|
||||
|
||||
test("requireEvents - 2", async () => {
|
||||
const myWorkflow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
class QueryEvent extends WorkflowEvent<{ query: string }> {}
|
||||
|
||||
class QueryResultEvent extends WorkflowEvent<{ result: string }> {}
|
||||
|
||||
myWorkflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (context: HandlerContext) => {
|
||||
context.sendEvent(new QueryEvent({ query: "something" }));
|
||||
const queryResultEvent = await context.requireEvent(QueryResultEvent);
|
||||
return new StopEvent(queryResultEvent.data.result);
|
||||
},
|
||||
);
|
||||
|
||||
myWorkflow.addStep(
|
||||
{
|
||||
inputs: [QueryEvent],
|
||||
outputs: [QueryResultEvent],
|
||||
},
|
||||
async (context, event) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
return new QueryResultEvent({ result: "query result" });
|
||||
},
|
||||
);
|
||||
|
||||
const result = await myWorkflow.run("start");
|
||||
expect(result.data).toBe("query result");
|
||||
});
|
||||
});
|
||||
|
||||
describe("workflow event loop", () => {
|
||||
test("basic", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (_context, ev: StartEvent) => {
|
||||
return new StopEvent(`Hello ${ev.data}!`);
|
||||
},
|
||||
);
|
||||
|
||||
const result = await jokeFlow.run("world");
|
||||
expect(result.data).toBe("Hello world!");
|
||||
});
|
||||
|
||||
test("branch", async () => {
|
||||
const myFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
class BranchA1Event extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
class BranchA2Event extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
class BranchB1Event extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
class BranchB2Event extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
let control = false;
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [BranchA1Event, BranchB1Event],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
if (control) {
|
||||
return new BranchA1Event({ payload: ev.data });
|
||||
} else {
|
||||
return new BranchB1Event({ payload: ev.data });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [BranchA1Event],
|
||||
outputs: [BranchA2Event],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
return new BranchA2Event({ payload: ev.data.payload });
|
||||
},
|
||||
);
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [BranchB1Event],
|
||||
outputs: [BranchB2Event],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
return new BranchB2Event({ payload: ev.data.payload });
|
||||
},
|
||||
);
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [BranchA2Event],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
return new StopEvent(`Branch A2: ${ev.data.payload}`);
|
||||
},
|
||||
);
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [BranchB2Event],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
return new StopEvent(`Branch B2: ${ev.data.payload}`);
|
||||
},
|
||||
);
|
||||
|
||||
{
|
||||
const result = await myFlow.run("world");
|
||||
expect(result.data).toMatch(/Branch B2: world/);
|
||||
}
|
||||
|
||||
control = true;
|
||||
|
||||
{
|
||||
const result = await myFlow.run("world");
|
||||
expect(result.data).toMatch(/Branch A2: world/);
|
||||
}
|
||||
|
||||
{
|
||||
const context = myFlow.run("world");
|
||||
for await (const event of context) {
|
||||
if (event instanceof BranchA2Event) {
|
||||
expect(event.data.payload).toBe("world");
|
||||
}
|
||||
if (event instanceof StopEvent) {
|
||||
expect(event.data).toMatch(/Branch A2: world/);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("one event have multiple outputs", async () => {
|
||||
const myFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
class AEvent extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
class BEvent extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
class CEvent extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
class DEvent extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
return new StopEvent("STOP");
|
||||
},
|
||||
);
|
||||
|
||||
const fn = vi.fn(async (_context, ev: StartEvent) => {
|
||||
return new AEvent({ payload: ev.data });
|
||||
});
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [AEvent],
|
||||
},
|
||||
fn,
|
||||
);
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [AEvent],
|
||||
outputs: [BEvent, CEvent],
|
||||
},
|
||||
async (_context, ev: AEvent) => {
|
||||
return new BEvent({ payload: ev.data.payload });
|
||||
},
|
||||
);
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [AEvent],
|
||||
outputs: [CEvent],
|
||||
},
|
||||
async (_context, ev: AEvent) => {
|
||||
return new CEvent({ payload: ev.data.payload });
|
||||
},
|
||||
);
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [BEvent],
|
||||
outputs: [DEvent],
|
||||
},
|
||||
async (_context, ev: BEvent) => {
|
||||
return new DEvent({ payload: ev.data.payload });
|
||||
},
|
||||
);
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [CEvent],
|
||||
outputs: [DEvent],
|
||||
},
|
||||
async (_context, ev: CEvent) => {
|
||||
return new DEvent({ payload: ev.data.payload });
|
||||
},
|
||||
);
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [DEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (_context, ev: DEvent) => {
|
||||
return new StopEvent(`Hello ${ev.data.payload}!`);
|
||||
},
|
||||
);
|
||||
|
||||
const result = await myFlow.run("world");
|
||||
expect(result.data).toBe("STOP");
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
// streaming events will allow to consume event even stop event is reached
|
||||
const stream = myFlow.run("world");
|
||||
for await (const _ of stream) {
|
||||
/* empty */
|
||||
}
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("run with custom context", async () => {
|
||||
type MyContext = { name: string };
|
||||
const myFlow = new Workflow<MyContext, string, string>({ verbose: true });
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async ({ data }, _: StartEvent) => {
|
||||
return new StopEvent(`Hello ${data.name}!`);
|
||||
},
|
||||
);
|
||||
|
||||
const result = await myFlow.run("world", { name: "Alice" });
|
||||
expect(result.data).toBe("Hello Alice!");
|
||||
});
|
||||
|
||||
test("run with custom context with two streaming", async () => {
|
||||
type MyContext = { name: string };
|
||||
const myFlow = new Workflow<MyContext, string, string>({ verbose: true });
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async ({ data }, _) => {
|
||||
if (data == null) {
|
||||
return new StopEvent({ result: "EMPTY" });
|
||||
}
|
||||
return new StopEvent({ result: `Hello ${data.name}!` });
|
||||
},
|
||||
);
|
||||
|
||||
const context1 = myFlow.run("world");
|
||||
const context2 = context1.with({ name: "Alice" });
|
||||
const context3 = context1.with({ name: "Bob" });
|
||||
expect(await context1).toMatchInlineSnapshot(`
|
||||
StopEvent {
|
||||
"data": {
|
||||
"result": "EMPTY",
|
||||
},
|
||||
"displayName": "StopEvent",
|
||||
}
|
||||
`);
|
||||
expect(await context2).toMatchInlineSnapshot(`
|
||||
StopEvent {
|
||||
"data": {
|
||||
"result": "Hello Alice!",
|
||||
},
|
||||
"displayName": "StopEvent",
|
||||
}
|
||||
`);
|
||||
expect(await context3).toMatchInlineSnapshot(`
|
||||
StopEvent {
|
||||
"data": {
|
||||
"result": "Hello Bob!",
|
||||
},
|
||||
"displayName": "StopEvent",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("snapshot", async () => {
|
||||
test("snapshot and recover", async () => {
|
||||
const myFlow = new Workflow({ verbose: true });
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (_, ev: StartEvent) => {
|
||||
return new StopEvent(`Hello ${ev.data}!`);
|
||||
},
|
||||
);
|
||||
const context = myFlow.run("world");
|
||||
const arrayBuffer = context.snapshot();
|
||||
expect(arrayBuffer).toBeInstanceOf(ArrayBuffer);
|
||||
const context2 = await myFlow.recover(arrayBuffer);
|
||||
expect(context2.data).toBe("Hello world!");
|
||||
});
|
||||
|
||||
test("snapshot in middle of workflow run ", async () => {
|
||||
const myFlow = new Workflow<
|
||||
{
|
||||
value: number;
|
||||
},
|
||||
string,
|
||||
string
|
||||
>({ verbose: true });
|
||||
|
||||
class AEvent extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
const fn = vi.fn(async (_, ev: StartEvent) => {
|
||||
return new AEvent({ payload: ev.data });
|
||||
});
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [AEvent],
|
||||
},
|
||||
fn,
|
||||
);
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [AEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async ({ data }, _: AEvent) => {
|
||||
return new StopEvent(`Hello ${data.value}!`);
|
||||
},
|
||||
);
|
||||
|
||||
const context = myFlow.run("world", {
|
||||
value: 1,
|
||||
});
|
||||
for await (const event of context) {
|
||||
if (event instanceof AEvent) {
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
const arrayBuffer = context.snapshot();
|
||||
expect(arrayBuffer).toBeInstanceOf(ArrayBuffer);
|
||||
const context2 = await myFlow.recover(arrayBuffer).with({
|
||||
value: 2,
|
||||
});
|
||||
expect(context2.data).toBe("Hello 2!");
|
||||
break;
|
||||
}
|
||||
if (event instanceof StopEvent) {
|
||||
expect(event.data).toBe("Hello 1!");
|
||||
}
|
||||
}
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user