Compare commits

..

9 Commits

Author SHA1 Message Date
github-actions[bot] 5bb4531245 Release 0.8.3 (#1427)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-02 19:05:28 -07:00
Alex Yang 2ff0a89891 fix: expose @llamaindex/node-parser (#1426) 2024-11-02 18:57:05 -07:00
Alex Yang d57917d782 feat: add code splitter and html node parser (#1425) 2024-11-02 15:14:03 -07:00
github-actions[bot] f231e0739f Release (#1424)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: himself65 <himself65@users.noreply.github.com>
2024-11-01 16:50:36 -07:00
Alex Yang 0765742ef3 feat: revamped workflow (#1422) 2024-11-01 15:59:19 -07:00
github-actions[bot] ec7fd6be5c Release 0.8.2 (#1421)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-31 23:54:40 -07:00
Thuc Pham c7a918c3f5 fix: export postprocessors in core (#1419) 2024-10-31 23:40:37 -07:00
Marcus Schiesser 00e681d43b ci: run type-check for reader examples 2024-11-01 12:52:06 +07:00
Alex Yang c01502fb84 docs: update document (#1418) 2024-10-31 14:02:31 -07:00
97 changed files with 6759 additions and 3297 deletions
+3 -1
View File
@@ -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
+19
View File
@@ -1,5 +1,24 @@
# docs
## 0.0.107
### Patch Changes
- llamaindex@0.8.3
## 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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "docs",
"version": "0.0.104",
"version": "0.0.107",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
+22
View File
@@ -1,5 +1,27 @@
# @llamaindex/doc
## 0.0.5
### Patch Changes
- Updated dependencies [2ff0a89]
- @llamaindex/node-parser@0.0.2
- llamaindex@0.8.3
## 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
+24 -1
View File
@@ -1,10 +1,33 @@
import { createMDX } from "fumadocs-mdx/next";
import MonacoWebpackPlugin from "monaco-editor-webpack-plugin";
const withMDX = createMDX();
/** @type {import('next').NextConfig} */
const config = {
reactStrictMode: true,
transpilePackages: ["monaco-editor"],
webpack: (config, { isServer }) => {
if (Array.isArray(config.target) && config.target.includes("web")) {
config.target = ["web", "es2020"];
}
config.resolve.alias = {
...config.resolve.alias,
sharp$: false,
"onnxruntime-node$": false,
};
config.resolve.fallback ??= {};
config.resolve.fallback.fs = false;
if (!isServer) {
config.plugins.push(
new MonacoWebpackPlugin({
languages: ["typescript"],
filename: "static/[name].worker.js",
}),
);
}
return config;
},
};
export default withMDX(config);
+30 -17
View File
@@ -1,6 +1,6 @@
{
"name": "@llamaindex/doc",
"version": "0.0.2",
"version": "0.0.5",
"private": true,
"scripts": {
"build": "pnpm run build:docs && next build",
@@ -14,60 +14,73 @@
"@icons-pack/react-simple-icons": "^10.1.0",
"@llamaindex/cloud": "workspace:*",
"@llamaindex/core": "workspace:*",
"@llamaindex/node-parser": "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-label": "^2.1.0",
"@radix-ui/react-slider": "^1.2.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",
"react-icons": "^5.3.0",
"react-monaco-editor": "^0.56.2",
"react-text-transition": "^3.1.0",
"react-use-measure": "^2.1.1",
"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",
"tree-sitter": "^0.22.0",
"tree-sitter-typescript": "^0.23.0",
"use-stick-to-bottom": "^1.0.41",
"web-tree-sitter": "^0.24.3",
"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",
"fast-glob": "^3.3.2",
"gray-matter": "^4.0.3",
"monaco-editor-webpack-plugin": "^7.1.0",
"postcss": "^8.4.47",
"remark": "^15.0.1",
"remark-gfm": "^4.0.0",
"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"
}
}
Binary file not shown.
BIN
View File
Binary file not shown.
+1 -2
View File
@@ -11,7 +11,6 @@ import { NpmInstall } from "@/components/npm-install";
import { TextEffect } from "@/components/text-effect";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { DOCUMENT_URL } from "@/lib/const";
import { SiStackblitz } from "@icons-pack/react-simple-icons";
import {
CodeBlock as FumaCodeBlock,
@@ -38,7 +37,7 @@ export default function HomePage() {
</div>
<div className="flex flex-wrap justify-center gap-4">
<Link href={DOCUMENT_URL}>
<Link href="/docs/llamaindex">
<Button variant="outline">Get Started</Button>
</Link>
<NpmInstall />
+2 -2
View File
@@ -1,4 +1,4 @@
import { DOCUMENT_URL } from "@/lib/const";
import { LEGACY_DOCUMENT_URL } from "@/lib/const";
import { redirect } from "next/navigation";
export default async function Page(props: {
@@ -7,5 +7,5 @@ export default async function Page(props: {
}>;
}) {
const path = await props.params.then(({ any }) => any.join("/"));
return redirect(new URL(path, DOCUMENT_URL).toString());
return redirect(new URL(path, LEGACY_DOCUMENT_URL).toString());
}
+14 -1
View File
@@ -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>
+40 -24
View File
@@ -1,38 +1,54 @@
import { baseOptions } from "@/app/layout.config";
import { AITrigger } from "@/components/ai-chat";
import { buttonVariants } from "@/components/ui/button";
import { LEGACY_DOCUMENT_URL } from "@/lib/const";
import { source } from "@/lib/source";
import { cn } from "@/lib/utils";
import "fumadocs-twoslash/twoslash.css";
import { Banner } from "fumadocs-ui/components/banner";
import { DocsLayout } from "fumadocs-ui/layouts/docs";
import { MessageCircle } from "lucide-react";
import type { ReactNode } from "react";
export default function Layout({ children }: { children: ReactNode }) {
return (
<DocsLayout
tree={source.pageTree}
{...baseOptions}
nav={{
...baseOptions.nav,
children: (
<AITrigger
className={cn(
buttonVariants({
variant: "secondary",
size: "xs",
className:
"md:flex-1 px-2 ms-2 gap-1.5 text-fd-muted-foreground rounded-full",
}),
)}
>
<MessageCircle className="size-3" />
Ask LlamaCloud
</AITrigger>
),
}}
>
{children}
</DocsLayout>
<>
<Banner variant="rainbow" id="welcome">
Welcome to the new LlamaIndex.TS documentation! 🎉 If you are looking
for the old documentation
<a
className="underline text-blue-500 ml-1"
target="_blank"
href={LEGACY_DOCUMENT_URL}
>
check it here
</a>
.
</Banner>
<DocsLayout
tree={source.pageTree}
{...baseOptions}
nav={{
...baseOptions.nav,
children: (
<AITrigger
className={cn(
buttonVariants({
variant: "secondary",
size: "xs",
className:
"md:flex-1 px-2 ms-2 gap-1.5 text-fd-muted-foreground rounded-full",
}),
)}
>
<MessageCircle className="size-3" />
Ask LlamaCloud
</AITrigger>
),
}}
>
{children}
</DocsLayout>
</>
);
}
+13 -3
View File
@@ -1,4 +1,4 @@
import { DOCUMENT_URL } from "@/lib/const";
import { Badge } from "@/components/ui/badge";
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
import Image from "next/image";
@@ -27,8 +27,18 @@ export const baseOptions: BaseLayoutProps = {
githubUrl: "https://github.com/run-llama/LlamaIndexTS",
links: [
{
text: "Documentation",
url: DOCUMENT_URL,
text: (
<div className="relative">
Docs
<Badge
variant="outline"
className="text-blue-500 absolute -top-5 -left-5 bg-fd-background hover:scale-125 transition-transform -rotate-3 hover:-rotate-12"
>
new
</Badge>
</div>
),
url: "/docs/llamaindex",
active: "nested-url",
},
],
+1 -9
View File
@@ -1,6 +1,5 @@
import { AIProvider } from "@/actions";
import { TooltipProvider } from "@/components/ui/tooltip";
import { Banner } from "fumadocs-ui/components/banner";
import { RootProvider } from "fumadocs-ui/provider";
import { Inter } from "next/font/google";
import type { ReactNode } from "react";
@@ -36,14 +35,7 @@ export default function Layout({ children }: { children: ReactNode }) {
<body className="flex flex-col min-h-screen">
<TooltipProvider>
<AIProvider>
<RootProvider>
<Banner variant="rainbow" id="experimental">
Welcome to the experimental LlamaIndex.TS documentation site.
Some content are still in progress, you are welcome to help
contribute to the documentation
</Banner>
{children}
</RootProvider>
<RootProvider>{children}</RootProvider>
</AIProvider>
</TooltipProvider>
</body>
@@ -0,0 +1,182 @@
"use client";
import { createContextState } from "foxact/context-state";
import { useIsClient } from "foxact/use-is-client";
import { useShiki } from "fumadocs-core/utils/use-shiki";
import { CodeBlock, Pre } from "fumadocs-ui/components/codeblock";
import { lazy, Suspense, use, useMemo } from "react";
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
import Parser from "web-tree-sitter";
import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import { Slider } from "@/components/ui/slider";
import { CodeSplitter } from "@llamaindex/node-parser/code";
let promise: Promise<CodeSplitter>;
if (typeof window !== "undefined") {
promise = Parser.init({
locateFile(scriptName: string) {
return "/" + scriptName;
},
}).then(async () => {
const parser = new Parser();
const Lang = await Parser.Language.load("/tree-sitter-typescript.wasm");
parser.setLanguage(Lang);
return new CodeSplitter({
getParser: () => parser,
maxChars: 100,
});
});
}
const [SliderProvider, useSlider, useSetSlider] = createContextState(100);
const [CodeProvider, useCode, useSetCode] =
createContextState(`interface Person {
name: string;
age: number;
}
function greet(person: Person): string {
return \`Hello, \${person.name}! You are \${person.age} years old.\`;
}
const john: Person = {
name: "John Doe",
age: 30
};
console.log(greet(john));`);
const Editor = lazy(() => import("react-monaco-editor"));
export const IDE = () => {
const codeSplitter = use(promise);
const code = useCode();
const setCode = useSetCode();
const maxChars = useSlider();
const useSetMaxChars = useSetSlider();
return (
<div className="flex flex-col p-4 border-r max-h-96 overflow-scroll">
<div>
<Label>Max Chars {maxChars}</Label>
<Slider
className="my-4"
min={10}
max={300}
step={10}
value={[maxChars]}
onValueChange={(value) => {
useSetMaxChars(value[0]);
codeSplitter.maxChars = value[0];
}}
/>
</div>
<Editor
editorWillMount={() => {}}
editorDidMount={() => {
window.MonacoEnvironment!.getWorkerUrl = (
_moduleId: string,
label: string,
) => {
if (label === "json") return "/_next/static/json.worker.js";
if (label === "css") return "/_next/static/css.worker.js";
if (label === "html") return "/_next/static/html.worker.js";
if (label === "typescript" || label === "javascript")
return "/_next/static/ts.worker.js";
return "/_next/static/editor.worker.js";
};
}}
editorWillUnmount={() => {}}
options={{
minimap: {
enabled: false,
},
}}
theme="vs-dark"
height="100%"
width="100%"
language="typescript"
onChange={setCode}
value={code}
/>
</div>
);
};
const Preview = ({ text }: { text: string }) => {
const rendered = useShiki(text, {
lang: "ts",
components: {
pre: (props) => {
return <Pre {...props}>{props.children}</Pre>;
},
},
});
return <CodeBlock className="py-0 m-2">{rendered}</CodeBlock>;
};
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 const NodePreview = () => {
const parser = use(promise);
const code = useCode();
const maxChars = useSlider();
const textChunks = useMemo(() => parser.splitText(code), [code, maxChars]);
return (
<StickToBottom
className="block relative max-h-96 overflow-scroll"
resize="smooth"
initial="smooth"
>
<StickToBottom.Content>
{textChunks.map((chunk, i) => (
<Preview key={i} text={chunk} />
))}
</StickToBottom.Content>
<ScrollToBottom />
</StickToBottom>
);
};
export const CodeNodeParserDemo = () => {
const isClient = useIsClient();
if (!isClient) {
return (
<div className="my-2 grid grid-cols-1 md:grid-cols-2 gap-2 border rounded-xl w-full max-h-96">
<Skeleton className="h-96" />
<Skeleton className="h-96" />
</div>
);
}
return (
<SliderProvider>
<CodeProvider>
<Suspense
fallback={
<div className="my-2 grid grid-cols-1 md:grid-cols-2 gap-2 border rounded-xl w-full max-h-96">
<Skeleton className="h-96" />
<Skeleton className="h-96" />
</div>
}
>
<div className="my-2 grid grid-cols-1 md:grid-cols-2 gap-2 border rounded-xl w-full max-h-96">
<IDE />
<NodePreview />
</div>
</Suspense>
</CodeProvider>
</SliderProvider>
);
};
@@ -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 dark:bg-gray-800">
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 dark:bg-yellow-800">
Computing task id: {event.data}
</div>,
]);
} else if (event instanceof ComputeResultEvent) {
setUI((ui) => [
...ui,
<div key={i++} className="bg-green-100 dark:bg-green-800">
Computed task id: {event.data}
</div>,
]);
} else if (event instanceof StartEvent) {
setUI((ui) => [
...ui,
<div key={i++} className="bg-blue-100 dark:bg-blue-800">
Started workflow with total {event.data}
</div>,
]);
} else if (event instanceof StopEvent) {
setUI((ui) => [
...ui,
<div key={i++} className="bg-red-100 dark:bg-red-800">
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>
);
}
+98
View File
@@ -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>
);
}
+36
View File
@@ -0,0 +1,36 @@
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };
+26
View File
@@ -0,0 +1,26 @@
"use client";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };
+28
View File
@@ -0,0 +1,28 @@
"use client";
import * as SliderPrimitive from "@radix-ui/react-slider";
import * as React from "react";
import { cn } from "@/lib/utils";
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className,
)}
{...props}
>
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };
@@ -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 />
+20 -31
View File
@@ -5,42 +5,31 @@ 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 want to try LlamaIndex.TS"
description="Learn how to use LlamaIndex.TS with different JS runtime and frameworks."
href="/docs/llamaindex/setup/getting-started"
/>
<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,158 @@
---
title: Node Parsers / Text Splitters
description: Learn how to use Node Parsers and Text Splitters to extract data from documents.
---
import { CodeNodeParserDemo } from '../../../../components/demo/code-node-parser';
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.
By default, we will use `Settings.nodeParser` to split the document into nodes. You can also assign a custom `NodeParser` to the `Settings` object.
```ts twoslash
import { TextFileReader } from '@llamaindex/readers/text'
import { SentenceSplitter } from '@llamaindex/core/node-parser';
import { Settings } from 'llamaindex';
const nodeParser = new SentenceSplitter();
Settings.nodeParser = nodeParser;
// ^?
```
## 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>
## CodeSplitter
The `CodeSplitter` is a more advanced `NodeParser` that can handle code documents.
It will split the code by AST nodes and then parse the nodes into a `Document` object.
<Tabs items={["with reader", "with node:fs"]}>
```ts twoslash tab="with reader"
import { TextFileReader } from '@llamaindex/readers/text'
import { CodeSplitter } from '@llamaindex/node-parser/code'
import Parser from "tree-sitter";
import TS from "tree-sitter-typescript";
const parser = new Parser();
parser.setLanguage(TS.typescript);
const codeSplitter = new CodeSplitter({
getParser: () => parser,
});
const reader = new TextFileReader();
const documents = await reader.loadData('path/to/file.ts');
const parsedDocuments = codeSplitter(documents);
// ^?
```
```ts twoslash tab="with node:fs"
import fs from 'node:fs/promises';
import { CodeSplitter } from '@llamaindex/node-parser/code'
import Parser from "tree-sitter";
import TS from "tree-sitter-typescript";
const parser = new Parser();
parser.setLanguage(TS.typescript);
const codeSplitter = new CodeSplitter({
getParser: () => parser,
});
const parsedDocuments = codeSplitter.splitText(await fs.readFile('path/to/file.ts', 'utf-8'));
// ^?
```
</Tabs>
Try it out ⬇️
<CodeNodeParserDemo/>
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
<Accordions>
<Accordion title="Use it in browser">
You might setup WASM files for `web-tree-sitter` and use it in the browser.
```ts
import Parser from 'web-tree-sitter';
Parser.init({
locateFile(scriptName: string) {
return '/' + scriptName
},
}).then(async () => {
const parser = new Parser();
const Lang = await Parser.Language.load('/tree-sitter-typescript.wasm');
parser.setLanguage(Lang);
return new CodeSplitter({
getParser: () => parser,
maxChars: 100
});
});
```
In this example, you should put `tree-sitter-typescript.wasm` to the `public` folder for Next.js.
And also update the `next.config.js` to make `@llamaindex/env` work properly.
```js
const config = {
webpack: (config) => {
if (Array.isArray(config.target) && config.target.includes('web')) {
config.target = ["web", "es2020"];
}
return config;
}
}
export default config;
```
</Accordion>
</Accordions>
@@ -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"
+5
View File
@@ -0,0 +1,5 @@
export type {
HandlerContext,
Workflow,
WorkflowContext,
} from "@llamaindex/workflow";
+1 -1
View File
@@ -1,2 +1,2 @@
// when we are ready, change to /docs/llamaindex
export const DOCUMENT_URL = 'https://legacy.ts.llamaindex.ai/'
export const LEGACY_DOCUMENT_URL = 'https://legacy.ts.llamaindex.ai/'
+135
View File
@@ -0,0 +1,135 @@
// Type definitions for Next.js cacheLife configs
declare module "next/cache" {
export { cacheTag as unstable_cacheTag } from "next/dist/server/use-cache/cache-tag";
export {
revalidatePath,
revalidateTag,
} from "next/dist/server/web/spec-extension/revalidate";
export { unstable_cache } from "next/dist/server/web/spec-extension/unstable-cache";
export { unstable_noStore } from "next/dist/server/web/spec-extension/unstable-no-store";
/**
* Cache this `"use cache"` for a timespan defined by the `"default"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 900 seconds (15 minutes)
* expire: never
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 15 minutes, start revalidating new values in the background.
* It lives for the maximum age of the server cache. If this entry has no traffic for a while, it may serve an old value the next request.
*/
export function unstable_cacheLife(profile: "default"): void;
/**
* Cache this `"use cache"` for a timespan defined by the `"seconds"` profile.
* ```
* stale: 0 seconds
* revalidate: 1 seconds
* expire: 1 seconds
* ```
*
* This cache may be stale on clients for 0 seconds before checking with the server.
* This cache will expire after 1 seconds. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "seconds"): void;
/**
* Cache this `"use cache"` for a timespan defined by the `"minutes"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 60 seconds (1 minute)
* expire: 3600 seconds (1 hour)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 minute, start revalidating new values in the background.
* If this entry has no traffic for 1 hour it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "minutes"): void;
/**
* Cache this `"use cache"` for a timespan defined by the `"hours"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 3600 seconds (1 hour)
* expire: 86400 seconds (1 day)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 hour, start revalidating new values in the background.
* If this entry has no traffic for 1 day it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "hours"): void;
/**
* Cache this `"use cache"` for a timespan defined by the `"days"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 86400 seconds (1 day)
* expire: 604800 seconds (1 week)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 day, start revalidating new values in the background.
* If this entry has no traffic for 1 week it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "days"): void;
/**
* Cache this `"use cache"` for a timespan defined by the `"weeks"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 604800 seconds (1 week)
* expire: 2592000 seconds (30 days)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 week, start revalidating new values in the background.
* If this entry has no traffic for 30 days it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "weeks"): void;
/**
* Cache this `"use cache"` for a timespan defined by the `"max"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 2592000 seconds (30 days)
* expire: never
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 30 days, start revalidating new values in the background.
* It lives for the maximum age of the server cache. If this entry has no traffic for a while, it may serve an old value the next request.
*/
export function unstable_cacheLife(profile: "max"): void;
/**
* Cache this `"use cache"` using a custom timespan.
* ```
* stale: ... // seconds
* revalidate: ... // seconds
* expire: ... // seconds
* ```
*
* This is similar to Cache-Control: max-age=`stale`,s-max-age=`revalidate`,stale-while-revalidate=`expire-revalidate`
*
* If a value is left out, the lowest of other cacheLife() calls or the default, is used instead.
*/
export function unstable_cacheLife(profile: {
/**
* This cache may be stale on clients for ... seconds before checking with the server.
*/
stale?: number;
/**
* If the server receives a new request after ... seconds, start revalidating new values in the background.
*/
revalidate?: number;
/**
* If this entry has no traffic for ... seconds it will expire. The next request will recompute it.
*/
expire?: number;
}): void;
}
+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
+7
View File
@@ -1,5 +1,12 @@
# examples
## 0.0.12
### Patch Changes
- Updated dependencies [0765742]
- @llamaindex/workflow@0.0.2
## 0.0.11
### Patch Changes
+33
View File
@@ -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
+2 -1
View File
@@ -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",
+13
View File
@@ -1,5 +1,18 @@
# @llamaindex/autotool
## 5.0.3
### Patch Changes
- llamaindex@0.8.3
## 5.0.2
### Patch Changes
- Updated dependencies [c7a918c]
- llamaindex@0.8.2
## 5.0.1
### Patch Changes
@@ -1,5 +1,20 @@
# @llamaindex/autotool-01-node-example
## 0.0.46
### Patch Changes
- llamaindex@0.8.3
- @llamaindex/autotool@5.0.3
## 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.46"
}
@@ -1,5 +1,20 @@
# @llamaindex/autotool-02-next-example
## 0.1.90
### Patch Changes
- llamaindex@0.8.3
- @llamaindex/autotool@5.0.3
## 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.90",
"scripts": {
"dev": "next dev",
"build": "next build",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@llamaindex/autotool",
"type": "module",
"version": "5.0.1",
"version": "5.0.3",
"description": "auto transpile your JS function to LLM Agent compatible",
"files": [
"dist",
+8
View File
@@ -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
}
+3 -1
View File
@@ -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;
+2 -2
View File
@@ -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[]);
});
}
+1 -1
View File
@@ -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(),
+16 -7
View File
@@ -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);
};
+13
View File
@@ -1,5 +1,18 @@
# @llamaindex/experimental
## 0.0.115
### Patch Changes
- llamaindex@0.8.3
## 0.0.114
### Patch Changes
- Updated dependencies [c7a918c]
- llamaindex@0.8.2
## 0.0.113
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@llamaindex/experimental",
"description": "Experimental package for LlamaIndexTS",
"version": "0.0.113",
"version": "0.0.115",
"type": "module",
"types": "dist/type/index.d.ts",
"main": "dist/cjs/index.js",
+13
View File
@@ -1,5 +1,18 @@
# llamaindex
## 0.8.3
### Patch Changes
- Updated dependencies [2ff0a89]
- @llamaindex/node-parser@0.0.2
## 0.8.2
### Patch Changes
- c7a918c: fix: export postprocessors in core
## 0.8.1
### Patch Changes
@@ -1,5 +1,18 @@
# @llamaindex/cloudflare-worker-agent-test
## 0.0.99
### Patch Changes
- llamaindex@0.8.3
## 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.99",
"type": "module",
"private": true,
"scripts": {
@@ -1,5 +1,18 @@
# @llamaindex/next-agent-test
## 0.1.99
### Patch Changes
- llamaindex@0.8.3
## 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.99",
"private": true,
"scripts": {
"dev": "next dev",
@@ -1,5 +1,18 @@
# test-edge-runtime
## 0.1.98
### Patch Changes
- llamaindex@0.8.3
## 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.98",
"private": true,
"scripts": {
"dev": "next dev",
@@ -1,5 +1,18 @@
# @llamaindex/next-node-runtime
## 0.0.80
### Patch Changes
- llamaindex@0.8.3
## 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.80",
"private": true,
"scripts": {
"dev": "next dev",
@@ -1,5 +1,18 @@
# @llamaindex/waku-query-engine-test
## 0.0.99
### Patch Changes
- llamaindex@0.8.3
## 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.99",
"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;
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "llamaindex",
"version": "0.8.1",
"version": "0.8.3",
"license": "MIT",
"type": "module",
"keywords": [
@@ -37,6 +37,7 @@
"@llamaindex/env": "workspace:*",
"@llamaindex/groq": "workspace:*",
"@llamaindex/huggingface": "workspace:*",
"@llamaindex/node-parser": "workspace:*",
"@llamaindex/ollama": "workspace:*",
"@llamaindex/openai": "workspace:*",
"@llamaindex/portkey-ai": "workspace:*",
+2 -1
View File
@@ -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";
@@ -67,7 +68,7 @@ export * from "./indices/index.js";
export * from "./ingestion/index.js";
export { imageToDataUrl } from "./internal/utils.js";
export * from "./llm/index.js";
export * from "./nodeParsers/index.js";
export * from "./node-parser.js";
export * from "./objects/index.js";
export * from "./OutputParser.js";
export * from "./postprocessors/index.js";
+3
View File
@@ -0,0 +1,3 @@
export * from "@llamaindex/core/node-parser";
export * from "@llamaindex/node-parser/code";
export * from "@llamaindex/node-parser/html";
@@ -1 +0,0 @@
export * from "@llamaindex/core/node-parser";
@@ -10,7 +10,7 @@ import {
TitleExtractor,
} from "llamaindex/extractors/index";
import { OpenAI } from "llamaindex/llm/openai";
import { SentenceSplitter } from "llamaindex/nodeParsers/index";
import { SentenceSplitter } from "llamaindex/node-parser";
import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
import {
DEFAULT_LLM_TEXT_OUTPUT,
@@ -5,7 +5,7 @@ import {
IngestionCache,
getTransformationHash,
} from "llamaindex/ingestion/IngestionCache";
import { SentenceSplitter } from "llamaindex/nodeParsers/index";
import { SentenceSplitter } from "llamaindex/node-parser";
import { beforeAll, describe, expect, test } from "vitest";
describe("IngestionCache", () => {
+7
View File
@@ -0,0 +1,7 @@
# @llamaindex/node-parser
## 0.0.2
### Patch Changes
- 2ff0a89: feat: add code splitter and html node parser
+8
View File
@@ -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
}
+8
View File
@@ -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
}
+59
View File
@@ -0,0 +1,59 @@
{
"name": "@llamaindex/node-parser",
"version": "0.0.2",
"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"
}
}
+79
View File
@@ -0,0 +1,79 @@
import { Settings } from "@llamaindex/core/global";
import { TextSplitter } from "@llamaindex/core/node-parser";
import type NodeParser from "tree-sitter";
import type { SyntaxNode as NodeSyntaxNode } from "tree-sitter";
import type WebParser from "web-tree-sitter";
import type { SyntaxNode as WebSyntaxNode } from "web-tree-sitter";
type SyntaxNode = NodeSyntaxNode | WebSyntaxNode;
type Parser = NodeParser | WebParser;
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;
}
}
}
+34
View File
@@ -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;
}
}
+16
View File
@@ -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"]
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": ["//"],
"tasks": {
"build": {
"outputs": ["**/dist/**"]
}
}
}
+7
View File
@@ -0,0 +1,7 @@
# @llamaindex/workflow
## 0.0.2
### Patch Changes
- 0765742: feat: revamped workflow
+60
View File
@@ -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"
}
}
+7
View File
@@ -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";
+596
View File
@@ -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;
}
}
+63
View File
@@ -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);
}
}
+191
View File
@@ -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,
});
}
}
+16
View File
@@ -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"]
}
+2544 -3165
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -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"
}
+22
View File
@@ -1,5 +1,27 @@
# @llamaindex/unit-test
## 0.0.12
### Patch Changes
- Updated dependencies [2ff0a89]
- @llamaindex/node-parser@0.0.2
- llamaindex@0.8.3
## 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
+42
View File
@@ -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;",
]);
});
});
+24
View File
@@ -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
View File
@@ -1,21 +1,30 @@
{
"name": "@llamaindex/unit-test",
"private": true,
"version": "0.0.9",
"version": "0.0.12",
"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
View File
@@ -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"
+103
View File
@@ -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",
]);
});
});
+871
View File
@@ -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);
});
});