mirror of
https://github.com/run-llama/LlamaIndexTS.git
synced 2026-07-02 20:13:52 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5bb4531245 | |||
| 2ff0a89891 | |||
| d57917d782 | |||
| f231e0739f | |||
| 0765742ef3 |
@@ -1,5 +1,17 @@
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "0.0.105",
|
||||
"version": "0.0.107",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# @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
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@llamaindex/doc",
|
||||
"version": "0.0.3",
|
||||
"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.
Executable
BIN
Binary file not shown.
@@ -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 />
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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[] }>;
|
||||
}) {
|
||||
@@ -40,6 +43,7 @@ export default async function Page(props: {
|
||||
Popup,
|
||||
PopupContent,
|
||||
PopupTrigger,
|
||||
AutoTypeTable,
|
||||
}}
|
||||
/>
|
||||
</DocsBody>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import NumberFlow from "@number-flow/react";
|
||||
import clsx from "clsx/lite";
|
||||
import { Minus, Plus } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
value?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
onChange?: (value: number) => void;
|
||||
};
|
||||
export default function FlowInput({
|
||||
value = 0,
|
||||
min = -Infinity,
|
||||
max = Infinity,
|
||||
onChange,
|
||||
}: Props) {
|
||||
const defaultValue = React.useRef(value);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const [animated, setAnimated] = React.useState(true);
|
||||
const [showCaret, setShowCaret] = React.useState(true);
|
||||
const handleInput: React.ChangeEventHandler<HTMLInputElement> = ({
|
||||
currentTarget: el,
|
||||
}) => {
|
||||
setAnimated(false);
|
||||
let next = value;
|
||||
if (el.value === "") {
|
||||
next = defaultValue.current;
|
||||
} else {
|
||||
const num = parseInt(el.value);
|
||||
if (!isNaN(num) && min <= num && num <= max) next = num;
|
||||
}
|
||||
el.value = String(next);
|
||||
onChange?.(next);
|
||||
};
|
||||
const handlePointerDown =
|
||||
(diff: number) => (event: React.PointerEvent<HTMLButtonElement>) => {
|
||||
setAnimated(true);
|
||||
if (event.pointerType === "mouse") {
|
||||
event?.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
const newVal = Math.min(Math.max(value + diff, min), max);
|
||||
onChange?.(newVal);
|
||||
};
|
||||
return (
|
||||
<div className="group flex items-stretch rounded-md text-lg font-semibold ring ring-zinc-200 transition-[box-shadow] focus-within:ring-2 focus-within:ring-blue-500 dark:ring-zinc-800">
|
||||
<button
|
||||
aria-hidden
|
||||
tabIndex={-1}
|
||||
className="flex items-center pl-[.5em] pr-[.325em]"
|
||||
disabled={min != null && value <= min}
|
||||
onPointerDown={handlePointerDown(-1)}
|
||||
>
|
||||
<Minus className="size-4" absoluteStrokeWidth strokeWidth={3.5} />
|
||||
</button>
|
||||
<div className="relative grid items-center justify-items-center text-center [grid-template-areas:'overlap'] *:[grid-area:overlap]">
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={clsx(
|
||||
showCaret ? "caret-primary" : "caret-transparent",
|
||||
"spin-hide w-[1.5em] bg-transparent py-2 text-center font-[inherit] text-transparent outline-none",
|
||||
"[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none",
|
||||
)}
|
||||
// Make sure to disable kerning, to match NumberFlow:
|
||||
style={{ fontKerning: "none" }}
|
||||
type="number"
|
||||
min={min}
|
||||
step={1}
|
||||
autoComplete="off"
|
||||
inputMode="numeric"
|
||||
max={max}
|
||||
value={value}
|
||||
onInput={handleInput}
|
||||
/>
|
||||
<NumberFlow
|
||||
value={value}
|
||||
format={{ useGrouping: false }}
|
||||
aria-hidden
|
||||
animated={animated}
|
||||
onAnimationsStart={() => setShowCaret(false)}
|
||||
onAnimationsFinish={() => setShowCaret(true)}
|
||||
className="pointer-events-none"
|
||||
willChange
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
aria-hidden
|
||||
tabIndex={-1}
|
||||
className="flex items-center pl-[.325em] pr-[.5em]"
|
||||
disabled={max != null && value >= max}
|
||||
onPointerDown={handlePointerDown(1)}
|
||||
>
|
||||
<Plus className="size-4" absoluteStrokeWidth strokeWidth={3.5} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,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 };
|
||||
@@ -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 };
|
||||
@@ -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 />
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"title": "Loading Data",
|
||||
"description": "Loading Data using LlamaIndex.TS",
|
||||
"pages": ["index"]
|
||||
"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>
|
||||
@@ -9,6 +9,7 @@
|
||||
"setup",
|
||||
"starter",
|
||||
"loading",
|
||||
"Integration"
|
||||
"guide",
|
||||
"integration"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export type {
|
||||
HandlerContext,
|
||||
Workflow,
|
||||
WorkflowContext,
|
||||
} from "@llamaindex/workflow";
|
||||
@@ -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/'
|
||||
|
||||
Vendored
+135
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
# examples
|
||||
|
||||
## 0.0.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [0765742]
|
||||
- @llamaindex/workflow@0.0.2
|
||||
|
||||
## 0.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { StartEvent, StopEvent, Workflow } from "@llamaindex/workflow";
|
||||
|
||||
type ContextData = {
|
||||
counter: number;
|
||||
};
|
||||
|
||||
const contextData: ContextData = { counter: 0 };
|
||||
|
||||
const workflow = new Workflow<ContextData, string, string>();
|
||||
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (context, startEvent) => {
|
||||
const input = startEvent.data;
|
||||
context.data.counter++;
|
||||
return new StopEvent(`Hello, ${input}!`);
|
||||
},
|
||||
);
|
||||
|
||||
{
|
||||
const ret = await workflow.run("Alex", contextData);
|
||||
console.log(ret.data); // Hello, Alex!
|
||||
}
|
||||
|
||||
{
|
||||
const ret = await workflow.run("World", contextData);
|
||||
console.log(ret.data); // Hello, World!
|
||||
}
|
||||
|
||||
console.log(contextData.counter); // 2
|
||||
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "@llamaindex/examples",
|
||||
"private": true,
|
||||
"version": "0.0.11",
|
||||
"version": "0.0.12",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-js": "^5.2.0",
|
||||
"@azure/identity": "^4.4.1",
|
||||
"@datastax/astra-db-ts": "^1.4.1",
|
||||
"@llamaindex/core": "^0.4.0",
|
||||
"@llamaindex/readers": "^1.0.0",
|
||||
"@llamaindex/workflow": "^0.0.2",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@pinecone-database/pinecone": "^3.0.2",
|
||||
"@vercel/postgres": "^0.10.0",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @llamaindex/autotool
|
||||
|
||||
## 5.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- llamaindex@0.8.3
|
||||
|
||||
## 5.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @llamaindex/autotool-01-node-example
|
||||
|
||||
## 0.0.46
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- llamaindex@0.8.3
|
||||
- @llamaindex/autotool@5.0.3
|
||||
|
||||
## 0.0.45
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"scripts": {
|
||||
"start": "node --import tsx --import @llamaindex/autotool/node ./src/index.ts"
|
||||
},
|
||||
"version": "0.0.45"
|
||||
"version": "0.0.46"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @llamaindex/autotool-02-next-example
|
||||
|
||||
## 0.1.90
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- llamaindex@0.8.3
|
||||
- @llamaindex/autotool@5.0.3
|
||||
|
||||
## 0.1.89
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@llamaindex/autotool-02-next-example",
|
||||
"private": true,
|
||||
"version": "0.1.89",
|
||||
"version": "0.1.90",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@llamaindex/autotool",
|
||||
"type": "module",
|
||||
"version": "5.0.2",
|
||||
"version": "5.0.3",
|
||||
"description": "auto transpile your JS function to LLM Agent compatible",
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": "./dist/index.js",
|
||||
"private": true
|
||||
}
|
||||
@@ -19,7 +19,9 @@ export type BaseEmbeddingOptions = {
|
||||
logProgress?: boolean;
|
||||
};
|
||||
|
||||
export abstract class BaseEmbedding extends TransformComponent {
|
||||
export abstract class BaseEmbedding extends TransformComponent<
|
||||
Promise<BaseNode[]>
|
||||
> {
|
||||
embedBatchSize = DEFAULT_EMBED_BATCH_SIZE;
|
||||
embedInfo?: EmbeddingInfo;
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ import {
|
||||
TransformComponent,
|
||||
} from "../schema";
|
||||
|
||||
export abstract class NodeParser extends TransformComponent {
|
||||
export abstract class NodeParser extends TransformComponent<BaseNode[]> {
|
||||
includeMetadata: boolean = true;
|
||||
includePrevNextRel: boolean = true;
|
||||
|
||||
constructor() {
|
||||
super(async (nodes: BaseNode[]): Promise<BaseNode[]> => {
|
||||
super((nodes: BaseNode[]): BaseNode[] => {
|
||||
return this.getNodesFromDocuments(nodes as TextNode[]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -470,7 +470,7 @@ export function buildNodeFromSplits(
|
||||
doc: BaseNode,
|
||||
refDoc: BaseNode = doc,
|
||||
idGenerator: (idx: number, refDoc: BaseNode) => string = () => randomUUID(),
|
||||
) {
|
||||
): TextNode[] {
|
||||
const nodes: TextNode[] = [];
|
||||
const relationships = {
|
||||
[NodeRelationship.SOURCE]: refDoc.asRelatedNodeInfo(),
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
import { fs, path, randomUUID } from "@llamaindex/env";
|
||||
import type { BaseNode, Document } from "./node";
|
||||
|
||||
interface TransformComponentSignature {
|
||||
interface TransformComponentSignature<
|
||||
Result extends BaseNode[] | Promise<BaseNode[]>,
|
||||
> {
|
||||
<Options extends Record<string, unknown>>(
|
||||
nodes: BaseNode[],
|
||||
options?: Options,
|
||||
): Promise<BaseNode[]>;
|
||||
): Result;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
||||
export interface TransformComponent extends TransformComponentSignature {
|
||||
export interface TransformComponent<
|
||||
Result extends BaseNode[] | Promise<BaseNode[]> =
|
||||
| BaseNode[]
|
||||
| Promise<BaseNode[]>,
|
||||
> extends TransformComponentSignature<Result> {
|
||||
id: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
||||
export class TransformComponent {
|
||||
constructor(transformFn: TransformComponentSignature) {
|
||||
export class TransformComponent<
|
||||
Result extends BaseNode[] | Promise<BaseNode[]> =
|
||||
| BaseNode[]
|
||||
| Promise<BaseNode[]>,
|
||||
> {
|
||||
constructor(transformFn: TransformComponentSignature<Result>) {
|
||||
Object.defineProperties(
|
||||
transformFn,
|
||||
Object.getOwnPropertyDescriptors(this.constructor.prototype),
|
||||
);
|
||||
const transform = function transform(
|
||||
...args: Parameters<TransformComponentSignature>
|
||||
...args: Parameters<TransformComponentSignature<Result>>
|
||||
) {
|
||||
return transformFn(...args);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @llamaindex/experimental
|
||||
|
||||
## 0.0.115
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- llamaindex@0.8.3
|
||||
|
||||
## 0.0.114
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@llamaindex/experimental",
|
||||
"description": "Experimental package for LlamaIndexTS",
|
||||
"version": "0.0.114",
|
||||
"version": "0.0.115",
|
||||
"type": "module",
|
||||
"types": "dist/type/index.d.ts",
|
||||
"main": "dist/cjs/index.js",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# llamaindex
|
||||
|
||||
## 0.8.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2ff0a89]
|
||||
- @llamaindex/node-parser@0.0.2
|
||||
|
||||
## 0.8.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @llamaindex/cloudflare-worker-agent-test
|
||||
|
||||
## 0.0.99
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- llamaindex@0.8.3
|
||||
|
||||
## 0.0.98
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@llamaindex/cloudflare-worker-agent-test",
|
||||
"version": "0.0.98",
|
||||
"version": "0.0.99",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @llamaindex/next-agent-test
|
||||
|
||||
## 0.1.99
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- llamaindex@0.8.3
|
||||
|
||||
## 0.1.98
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@llamaindex/next-agent-test",
|
||||
"version": "0.1.98",
|
||||
"version": "0.1.99",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# test-edge-runtime
|
||||
|
||||
## 0.1.98
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- llamaindex@0.8.3
|
||||
|
||||
## 0.1.97
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@llamaindex/nextjs-edge-runtime-test",
|
||||
"version": "0.1.97",
|
||||
"version": "0.1.98",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @llamaindex/next-node-runtime
|
||||
|
||||
## 0.0.80
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- llamaindex@0.8.3
|
||||
|
||||
## 0.0.79
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@llamaindex/next-node-runtime-test",
|
||||
"version": "0.0.79",
|
||||
"version": "0.0.80",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @llamaindex/waku-query-engine-test
|
||||
|
||||
## 0.0.99
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- llamaindex@0.8.3
|
||||
|
||||
## 0.0.98
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@llamaindex/waku-query-engine-test",
|
||||
"version": "0.0.98",
|
||||
"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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "llamaindex",
|
||||
"version": "0.8.2",
|
||||
"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:*",
|
||||
|
||||
@@ -68,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";
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# @llamaindex/node-parser
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2ff0a89: feat: add code splitter and html node parser
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": "./dist/index.js",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": "./dist/index.js",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "@llamaindex/node-parser",
|
||||
"version": "0.0.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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { NodeParser } from "@llamaindex/core/node-parser";
|
||||
import {
|
||||
buildNodeFromSplits,
|
||||
MetadataMode,
|
||||
TextNode,
|
||||
} from "@llamaindex/core/schema";
|
||||
import { htmlToText, type HtmlToTextOptions } from "html-to-text";
|
||||
|
||||
export type HTMLNodeParserParam = {
|
||||
htmlToTextOptions?: HtmlToTextOptions;
|
||||
};
|
||||
|
||||
export class HTMLNodeParser extends NodeParser {
|
||||
public readonly htmlToTextOptions: HtmlToTextOptions | undefined = undefined;
|
||||
|
||||
constructor(params?: HTMLNodeParserParam) {
|
||||
super();
|
||||
if (params?.htmlToTextOptions) {
|
||||
this.htmlToTextOptions = params.htmlToTextOptions;
|
||||
}
|
||||
}
|
||||
|
||||
protected parseNodes(documents: TextNode[]): TextNode[] {
|
||||
const nodes: TextNode[] = [];
|
||||
for (const document of documents) {
|
||||
const text = htmlToText(
|
||||
document.getContent(MetadataMode.NONE),
|
||||
this.htmlToTextOptions,
|
||||
);
|
||||
nodes.push(...buildNodeFromSplits([text], document));
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist/type",
|
||||
"tsBuildInfoFile": "./dist/.tsbuildinfo",
|
||||
"emitDeclarationOnly": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["./src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"outputs": ["**/dist/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# @llamaindex/workflow
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0765742: feat: revamped workflow
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "@llamaindex/workflow",
|
||||
"description": "Workflow API",
|
||||
"version": "0.0.2",
|
||||
"type": "module",
|
||||
"types": "dist/index.d.ts",
|
||||
"module": "dist/index.js",
|
||||
"main": "dist/index.cjs",
|
||||
"keywords": [
|
||||
"workflow"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"node": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs",
|
||||
"default": "./dist/index.cjs"
|
||||
},
|
||||
"workerd": {
|
||||
"types": "./dist/index.workerd.d.ts",
|
||||
"default": "./dist/index.workerd.js"
|
||||
},
|
||||
"edge-light": {
|
||||
"types": "./dist/index.edge-light.d.ts",
|
||||
"default": "./dist/index.edge-light.js"
|
||||
},
|
||||
"browser": {
|
||||
"types": "./dist/index.browser.d.ts",
|
||||
"default": "./dist/index.browser.js"
|
||||
},
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"CHANGELOG.md",
|
||||
"!**/*.tsbuildinfo"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/run-llama/LlamaIndexTS.git",
|
||||
"directory": "packages/workflow"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "bunchee --watch",
|
||||
"build": "bunchee"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.8.4",
|
||||
"bunchee": "5.5.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export {
|
||||
WorkflowContext,
|
||||
type HandlerContext,
|
||||
type StepHandler,
|
||||
} from "./workflow-context.js";
|
||||
export { StartEvent, StopEvent, WorkflowEvent } from "./workflow-event.js";
|
||||
export { Workflow, type StepParameters } from "./workflow.js";
|
||||
@@ -0,0 +1,596 @@
|
||||
import {
|
||||
type AnyWorkflowEventConstructor,
|
||||
StartEvent,
|
||||
type StartEventConstructor,
|
||||
StopEvent,
|
||||
type StopEventConstructor,
|
||||
WorkflowEvent,
|
||||
} from "./workflow-event";
|
||||
|
||||
export type StepHandler<
|
||||
Data = unknown,
|
||||
Inputs extends [
|
||||
AnyWorkflowEventConstructor | StartEventConstructor,
|
||||
...(AnyWorkflowEventConstructor | StopEventConstructor)[],
|
||||
] = [AnyWorkflowEventConstructor | StartEventConstructor],
|
||||
Out extends [
|
||||
AnyWorkflowEventConstructor | StartEventConstructor,
|
||||
...(AnyWorkflowEventConstructor | StopEventConstructor)[],
|
||||
] = [AnyWorkflowEventConstructor | StopEventConstructor],
|
||||
> = (
|
||||
context: HandlerContext<Data>,
|
||||
...events: {
|
||||
[K in keyof Inputs]: InstanceType<Inputs[K]>;
|
||||
}
|
||||
) => Promise<
|
||||
{
|
||||
[K in keyof Out]: InstanceType<Out[K]>;
|
||||
}[number]
|
||||
>;
|
||||
|
||||
export type ReadonlyStepMap<Data> = ReadonlyMap<
|
||||
StepHandler<Data, never, never>,
|
||||
{
|
||||
inputs: AnyWorkflowEventConstructor[];
|
||||
outputs: AnyWorkflowEventConstructor[];
|
||||
}
|
||||
>;
|
||||
|
||||
type GlobalEvent = typeof globalThis.Event;
|
||||
|
||||
export type Wait = () => Promise<void>;
|
||||
|
||||
export type ContextParams<Start, Stop, Data> = {
|
||||
startEvent: StartEvent<Start>;
|
||||
contextData: Data;
|
||||
steps: ReadonlyStepMap<Data>;
|
||||
timeout: number | null;
|
||||
verbose: boolean;
|
||||
wait: Wait;
|
||||
|
||||
queue: QueueProtocol[] | undefined;
|
||||
pendingInputQueue: WorkflowEvent<unknown>[] | undefined;
|
||||
resolved: StopEvent<Stop> | null | undefined;
|
||||
rejected: Error | null | undefined;
|
||||
};
|
||||
|
||||
function flattenEvents(
|
||||
acceptEventTypes: AnyWorkflowEventConstructor[],
|
||||
inputEvents: WorkflowEvent<unknown>[],
|
||||
): WorkflowEvent<unknown>[] {
|
||||
const eventMap = new Map<
|
||||
AnyWorkflowEventConstructor,
|
||||
WorkflowEvent<unknown>
|
||||
>();
|
||||
|
||||
for (const event of inputEvents) {
|
||||
for (const acceptType of acceptEventTypes) {
|
||||
if (event instanceof acceptType && !eventMap.has(acceptType)) {
|
||||
eventMap.set(acceptType, event);
|
||||
break; // Once matched, no need to check other accept types
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(eventMap.values());
|
||||
}
|
||||
|
||||
export type HandlerContext<Data = unknown> = {
|
||||
get data(): Data;
|
||||
sendEvent(event: WorkflowEvent<unknown>): void;
|
||||
requireEvent<T extends AnyWorkflowEventConstructor>(
|
||||
event: T,
|
||||
): Promise<InstanceType<T>>;
|
||||
};
|
||||
|
||||
export type QueueProtocol =
|
||||
| {
|
||||
type: "event";
|
||||
event: WorkflowEvent<unknown>;
|
||||
}
|
||||
| {
|
||||
type: "requestEvent";
|
||||
id: string;
|
||||
requestEvent: AnyWorkflowEventConstructor;
|
||||
};
|
||||
|
||||
export class WorkflowContext<Start = string, Stop = string, Data = unknown>
|
||||
implements
|
||||
AsyncIterable<WorkflowEvent<unknown>, unknown, void>,
|
||||
Promise<StopEvent<Stop>>
|
||||
{
|
||||
readonly #steps: ReadonlyStepMap<Data>;
|
||||
|
||||
readonly #startEvent: StartEvent<Start>;
|
||||
readonly #queue: QueueProtocol[] = [];
|
||||
readonly #queueEventTarget = new EventTarget();
|
||||
readonly #wait: Wait;
|
||||
|
||||
#timeout: number | null = null;
|
||||
#verbose: boolean = false;
|
||||
#data: Data;
|
||||
|
||||
#stepCache: WeakMap<
|
||||
WorkflowEvent<unknown>,
|
||||
[
|
||||
step: Set<StepHandler<Data, never, never>>,
|
||||
stepInputs: WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>,
|
||||
stepOutputs: WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>,
|
||||
]
|
||||
> = new Map();
|
||||
|
||||
#getStepFunction(
|
||||
event: WorkflowEvent<unknown>,
|
||||
): [
|
||||
step: Set<StepHandler<Data, never, never>>,
|
||||
stepInputs: WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>,
|
||||
stepOutputs: WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>,
|
||||
] {
|
||||
if (this.#stepCache.has(event)) {
|
||||
return this.#stepCache.get(event)!;
|
||||
}
|
||||
const set = new Set<StepHandler<Data, never, never>>();
|
||||
const stepInputs = new WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>();
|
||||
const stepOutputs = new WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>();
|
||||
const res: [
|
||||
step: Set<StepHandler<Data, never, never>>,
|
||||
stepInputs: WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>,
|
||||
stepOutputs: WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>,
|
||||
] = [set, stepInputs, stepOutputs];
|
||||
this.#stepCache.set(event, res);
|
||||
for (const [step, { inputs, outputs }] of this.#steps) {
|
||||
if (inputs.some((input) => event instanceof input)) {
|
||||
set.add(step);
|
||||
stepInputs.set(step, inputs);
|
||||
stepOutputs.set(step, outputs);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
constructor(params: ContextParams<Start, Stop, Data>) {
|
||||
this.#steps = params.steps;
|
||||
this.#startEvent = params.startEvent;
|
||||
if (typeof params.timeout === "number") {
|
||||
this.#timeout = params.timeout;
|
||||
}
|
||||
this.#data = params.contextData;
|
||||
this.#verbose = params.verbose ?? false;
|
||||
this.#wait = params.wait;
|
||||
|
||||
// push start event to the queue
|
||||
const [step] = this.#getStepFunction(this.#startEvent);
|
||||
if (step.size === 0) {
|
||||
throw new TypeError("No step found for start event");
|
||||
}
|
||||
|
||||
// restore from snapshot
|
||||
if (params.queue) {
|
||||
params.queue.forEach((protocol) => {
|
||||
this.#queue.push(protocol);
|
||||
});
|
||||
} else {
|
||||
this.#sendEvent(this.#startEvent);
|
||||
}
|
||||
if (params.pendingInputQueue) {
|
||||
this.#pendingInputQueue = params.pendingInputQueue;
|
||||
}
|
||||
if (params.resolved) {
|
||||
this.#resolved = params.resolved;
|
||||
}
|
||||
if (params.rejected) {
|
||||
this.#rejected = params.rejected;
|
||||
}
|
||||
}
|
||||
|
||||
// make sure it will only be called once
|
||||
#iterator: AsyncIterableIterator<WorkflowEvent<unknown>> | null = null;
|
||||
#signal: AbortSignal | null = null;
|
||||
|
||||
get #iteratorSingleton(): AsyncIterableIterator<WorkflowEvent<unknown>> {
|
||||
if (this.#iterator === null) {
|
||||
this.#iterator = this.#createStreamEvents();
|
||||
}
|
||||
return this.#iterator;
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator](): AsyncIterableIterator<WorkflowEvent<unknown>> {
|
||||
return this.#iteratorSingleton;
|
||||
}
|
||||
|
||||
#sendEvent = (event: WorkflowEvent<unknown>): void => {
|
||||
this.#queue.push({
|
||||
type: "event",
|
||||
event,
|
||||
});
|
||||
};
|
||||
|
||||
#requireEvent = async <T extends AnyWorkflowEventConstructor>(
|
||||
event: T,
|
||||
): Promise<InstanceType<T>> => {
|
||||
const requestId = crypto.randomUUID();
|
||||
this.#queue.push({
|
||||
type: "requestEvent",
|
||||
id: requestId,
|
||||
requestEvent: event,
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
const handler = (event: InstanceType<GlobalEvent>) => {
|
||||
if (event instanceof CustomEvent) {
|
||||
const { id } = event.detail;
|
||||
if (requestId === id) {
|
||||
this.#queueEventTarget.removeEventListener("update", handler);
|
||||
resolve(event.detail.event);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.#queueEventTarget.addEventListener("update", handler);
|
||||
});
|
||||
};
|
||||
|
||||
#pendingInputQueue: WorkflowEvent<unknown>[] = [];
|
||||
|
||||
// if strict mode is enabled, it will throw an error if there's input or output events are not expected
|
||||
#strict = false;
|
||||
|
||||
strict() {
|
||||
this.#strict = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
get data(): Data {
|
||||
return this.#data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream events from the start event
|
||||
*
|
||||
* Note that this function will stop once there's no more future events,
|
||||
* if you want stop immediately once reach a StopEvent, you should handle it in the other side.
|
||||
* @private
|
||||
*/
|
||||
#createStreamEvents(): AsyncIterableIterator<WorkflowEvent<unknown>> {
|
||||
const isPendingEvents = new WeakSet<WorkflowEvent<unknown>>();
|
||||
const pendingTasks = new Set<Promise<WorkflowEvent<unknown>>>();
|
||||
const enqueuedEvents = new Set<WorkflowEvent<unknown>>();
|
||||
const stream = new ReadableStream<WorkflowEvent<unknown>>({
|
||||
start: async (controller) => {
|
||||
while (true) {
|
||||
const eventProtocol = this.#queue.shift();
|
||||
if (eventProtocol) {
|
||||
switch (eventProtocol.type) {
|
||||
case "requestEvent": {
|
||||
const { id, requestEvent } = eventProtocol;
|
||||
const acceptableInput = this.#pendingInputQueue.find(
|
||||
(event) => event instanceof requestEvent,
|
||||
);
|
||||
if (acceptableInput) {
|
||||
this.#pendingInputQueue.splice(
|
||||
this.#pendingInputQueue.indexOf(acceptableInput),
|
||||
1,
|
||||
);
|
||||
this.#queueEventTarget.dispatchEvent(
|
||||
new CustomEvent("update", {
|
||||
detail: { id, event: acceptableInput },
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// push back to the queue as there are not enough events
|
||||
this.#queue.push(eventProtocol);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "event": {
|
||||
const { event } = eventProtocol;
|
||||
if (isPendingEvents.has(event)) {
|
||||
// this event is still processing
|
||||
this.#sendEvent(event);
|
||||
} else {
|
||||
if (!enqueuedEvents.has(event)) {
|
||||
controller.enqueue(event);
|
||||
enqueuedEvents.add(event);
|
||||
}
|
||||
const [steps, inputsMap, outputsMap] =
|
||||
this.#getStepFunction(event);
|
||||
const nextEventPromises: Promise<WorkflowEvent<unknown>>[] = [
|
||||
...steps,
|
||||
]
|
||||
.map((step) => {
|
||||
const inputs = [...(inputsMap.get(step) ?? [])];
|
||||
const acceptableInputs: WorkflowEvent<unknown>[] =
|
||||
this.#pendingInputQueue.filter((event) =>
|
||||
inputs.some((input) => event instanceof input),
|
||||
);
|
||||
const events: WorkflowEvent<unknown>[] = flattenEvents(
|
||||
inputs,
|
||||
[event, ...acceptableInputs],
|
||||
);
|
||||
if (events.length !== inputs.length) {
|
||||
if (this.#verbose) {
|
||||
console.log(
|
||||
`Not enough inputs for step ${step.name}, waiting for more events`,
|
||||
);
|
||||
}
|
||||
// not enough to run the step, push back to the queue
|
||||
this.#sendEvent(event);
|
||||
isPendingEvents.add(event);
|
||||
return null;
|
||||
}
|
||||
if (isPendingEvents.has(event)) {
|
||||
isPendingEvents.delete(event);
|
||||
}
|
||||
if (this.#verbose) {
|
||||
console.log(
|
||||
`Running step ${step.name} with inputs ${events}`,
|
||||
);
|
||||
}
|
||||
const data = this.data;
|
||||
return (step as StepHandler<Data>)
|
||||
.call(
|
||||
null,
|
||||
{
|
||||
get data() {
|
||||
return data;
|
||||
},
|
||||
sendEvent: this.#sendEvent,
|
||||
requireEvent: this.#requireEvent,
|
||||
},
|
||||
// @ts-expect-error IDK why
|
||||
...events.sort((a, b) => {
|
||||
const aIndex = inputs.indexOf(
|
||||
a.constructor as AnyWorkflowEventConstructor,
|
||||
);
|
||||
const bIndex = inputs.indexOf(
|
||||
b.constructor as AnyWorkflowEventConstructor,
|
||||
);
|
||||
return aIndex - bIndex;
|
||||
}),
|
||||
)
|
||||
.then((nextEvent) => {
|
||||
if (this.#verbose) {
|
||||
console.log(
|
||||
`Step ${step.name} completed, next event is ${nextEvent}`,
|
||||
);
|
||||
}
|
||||
const outputs = outputsMap.get(step) ?? [];
|
||||
const outputEvents = flattenEvents(outputs, [
|
||||
nextEvent,
|
||||
]);
|
||||
if (outputEvents.length !== outputs.length) {
|
||||
if (this.#strict) {
|
||||
const error = Error(
|
||||
`Step ${step.name} returned an unexpected output event ${nextEvent}`,
|
||||
);
|
||||
controller.error(error);
|
||||
} else {
|
||||
console.warn(
|
||||
`Step ${step.name} returned an unexpected output event ${nextEvent}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!(nextEvent instanceof StopEvent)) {
|
||||
this.#pendingInputQueue.unshift(nextEvent);
|
||||
this.#sendEvent(nextEvent);
|
||||
}
|
||||
return nextEvent;
|
||||
});
|
||||
})
|
||||
.filter((promise) => promise !== null);
|
||||
nextEventPromises.forEach((promise) => {
|
||||
pendingTasks.add(promise);
|
||||
promise
|
||||
.catch((err) => {
|
||||
console.error("Error in step", err);
|
||||
})
|
||||
.finally(() => {
|
||||
pendingTasks.delete(promise);
|
||||
});
|
||||
});
|
||||
Promise.race(nextEventPromises)
|
||||
.then((fastestNextEvent) => {
|
||||
if (!enqueuedEvents.has(fastestNextEvent)) {
|
||||
controller.enqueue(fastestNextEvent);
|
||||
enqueuedEvents.add(fastestNextEvent);
|
||||
}
|
||||
return fastestNextEvent;
|
||||
})
|
||||
.then(async (fastestNextEvent) =>
|
||||
Promise.all(nextEventPromises).then((nextEvents) => {
|
||||
for (const nextEvent of nextEvents) {
|
||||
// do not enqueue the same event twice
|
||||
if (fastestNextEvent !== nextEvent) {
|
||||
if (!enqueuedEvents.has(nextEvent)) {
|
||||
controller.enqueue(nextEvent);
|
||||
enqueuedEvents.add(nextEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.catch((err) => {
|
||||
controller.error(err);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.#queue.length === 0 && pendingTasks.size === 0) {
|
||||
if (this.#verbose) {
|
||||
console.log("No more events in the queue");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
await this.#wait();
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
return stream[Symbol.asyncIterator]();
|
||||
}
|
||||
|
||||
with<Initial extends Data>(
|
||||
data: Initial,
|
||||
): WorkflowContext<Start, Stop, Initial> {
|
||||
return new WorkflowContext({
|
||||
startEvent: this.#startEvent,
|
||||
wait: this.#wait,
|
||||
contextData: data,
|
||||
steps: this.#steps,
|
||||
timeout: this.#timeout,
|
||||
verbose: this.#verbose,
|
||||
queue: this.#queue,
|
||||
pendingInputQueue: this.#pendingInputQueue,
|
||||
resolved: this.#resolved,
|
||||
rejected: this.#rejected,
|
||||
});
|
||||
}
|
||||
|
||||
// PromiseLike implementation, this is following the Promise/A+ spec
|
||||
// It will consume the iterator and resolve the promise once it reaches the StopEvent
|
||||
// If you want to customize the behavior, you can use the async iterator directly
|
||||
#resolved: StopEvent<Stop> | null = null;
|
||||
#rejected: Error | null = null;
|
||||
|
||||
async then<TResult1, TResult2 = never>(
|
||||
onfulfilled?:
|
||||
| ((value: StopEvent<Stop>) => TResult1 | PromiseLike<TResult1>)
|
||||
| null
|
||||
| undefined,
|
||||
onrejected?:
|
||||
| ((reason: unknown) => TResult2 | PromiseLike<TResult2>)
|
||||
| null
|
||||
| undefined,
|
||||
) {
|
||||
onfulfilled ??= (value) => value as TResult1;
|
||||
onrejected ??= (reason) => {
|
||||
throw reason;
|
||||
};
|
||||
if (this.#resolved !== null) {
|
||||
return Promise.resolve(this.#resolved).then(onfulfilled, onrejected);
|
||||
} else if (this.#rejected !== null) {
|
||||
return Promise.reject(this.#rejected).then(onfulfilled, onrejected);
|
||||
}
|
||||
|
||||
if (this.#timeout !== null) {
|
||||
const timeout = this.#timeout;
|
||||
this.#signal = AbortSignal.timeout(timeout * 1000);
|
||||
}
|
||||
|
||||
this.#signal?.addEventListener("abort", () => {
|
||||
this.#rejected = new Error(
|
||||
`Operation timed out after ${this.#timeout} seconds`,
|
||||
);
|
||||
onrejected?.(this.#rejected);
|
||||
});
|
||||
try {
|
||||
for await (const event of this.#iteratorSingleton) {
|
||||
if (this.#rejected !== null) {
|
||||
return onrejected?.(this.#rejected);
|
||||
}
|
||||
if (event instanceof StartEvent) {
|
||||
if (this.#verbose) {
|
||||
console.log(`Starting workflow with event ${event}`);
|
||||
}
|
||||
}
|
||||
if (event instanceof StopEvent) {
|
||||
if (this.#verbose && this.#pendingInputQueue.length > 0) {
|
||||
// fixme: #pendingInputQueue might should be cleanup correctly?
|
||||
}
|
||||
this.#resolved = event;
|
||||
return onfulfilled?.(event);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
this.#rejected = err;
|
||||
}
|
||||
return onrejected?.(err);
|
||||
}
|
||||
const nextValue = await this.#iteratorSingleton.next();
|
||||
if (nextValue.done === false) {
|
||||
this.#rejected = new Error("Workflow did not complete");
|
||||
return onrejected?.(this.#rejected);
|
||||
}
|
||||
return onrejected?.(new Error("UNREACHABLE"));
|
||||
}
|
||||
|
||||
catch<TResult = never>(
|
||||
onrejected?:
|
||||
| ((reason: unknown) => TResult | PromiseLike<TResult>)
|
||||
| null
|
||||
| undefined,
|
||||
) {
|
||||
return this.then((v) => v, onrejected);
|
||||
}
|
||||
|
||||
finally(onfinally?: (() => void) | undefined | null) {
|
||||
return this.then(
|
||||
() => {
|
||||
onfinally?.();
|
||||
},
|
||||
() => {
|
||||
onfinally?.();
|
||||
},
|
||||
) as Promise<never>;
|
||||
}
|
||||
|
||||
[Symbol.toStringTag]: string = "Context";
|
||||
|
||||
// for worker thread
|
||||
snapshot(): ArrayBuffer {
|
||||
const state = {
|
||||
startEvent: this.#startEvent,
|
||||
queue: this.#queue,
|
||||
pendingInputQueue: this.#pendingInputQueue,
|
||||
data: this.#data,
|
||||
timeout: this.#timeout,
|
||||
verbose: this.#verbose,
|
||||
resolved: this.#resolved,
|
||||
rejected: this.#rejected,
|
||||
};
|
||||
|
||||
const jsonString = JSON.stringify(state, (_, value) => {
|
||||
// If value is an instance of a class, serialize only its properties
|
||||
if (value instanceof WorkflowEvent) {
|
||||
return { data: value.data, constructor: value.constructor.name };
|
||||
}
|
||||
// value is Subtype of WorkflowEvent
|
||||
if (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
value?.prototype instanceof WorkflowEvent
|
||||
) {
|
||||
return { constructor: value.prototype.constructor.name };
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
return new TextEncoder().encode(jsonString).buffer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
export class WorkflowEvent<Data> {
|
||||
displayName: string;
|
||||
data: Data;
|
||||
|
||||
constructor(data: Data) {
|
||||
this.data = data;
|
||||
this.displayName = this.constructor.name;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.displayName;
|
||||
}
|
||||
|
||||
static or<
|
||||
A extends AnyWorkflowEventConstructor,
|
||||
B extends AnyWorkflowEventConstructor,
|
||||
>(AEvent: A, BEvent: B): A | B {
|
||||
function OrEvent() {
|
||||
throw new Error("Cannot instantiate OrEvent");
|
||||
}
|
||||
|
||||
OrEvent.prototype = Object.create(AEvent.prototype);
|
||||
|
||||
Object.getOwnPropertyNames(BEvent.prototype).forEach((property) => {
|
||||
if (!(property in OrEvent.prototype)) {
|
||||
Object.defineProperty(
|
||||
OrEvent.prototype,
|
||||
property,
|
||||
Object.getOwnPropertyDescriptor(BEvent.prototype, property)!,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
OrEvent.prototype.constructor = OrEvent;
|
||||
|
||||
Object.defineProperty(OrEvent, Symbol.hasInstance, {
|
||||
value: function (instance: unknown) {
|
||||
return instance instanceof AEvent || instance instanceof BEvent;
|
||||
},
|
||||
});
|
||||
|
||||
return OrEvent as unknown as A | B;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type AnyWorkflowEventConstructor = new (data: any) => WorkflowEvent<any>;
|
||||
|
||||
export type StartEventConstructor<T = string> = new (data: T) => StartEvent<T>;
|
||||
export type StopEventConstructor<T = string> = new (data: T) => StopEvent<T>;
|
||||
|
||||
// These are special events that are used to control the workflow
|
||||
export class StartEvent<T = string> extends WorkflowEvent<T> {
|
||||
constructor(data: T) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
export class StopEvent<T = string> extends WorkflowEvent<T> {
|
||||
constructor(data: T) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import {
|
||||
WorkflowContext,
|
||||
type HandlerContext,
|
||||
type QueueProtocol,
|
||||
type StepHandler,
|
||||
type Wait,
|
||||
} from "./workflow-context.js";
|
||||
import {
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
type AnyWorkflowEventConstructor,
|
||||
type StartEventConstructor,
|
||||
type StopEventConstructor,
|
||||
} from "./workflow-event.js";
|
||||
|
||||
export type StepParameters<
|
||||
In extends AnyWorkflowEventConstructor[],
|
||||
Out extends AnyWorkflowEventConstructor[],
|
||||
> = {
|
||||
inputs: In;
|
||||
outputs: Out;
|
||||
};
|
||||
|
||||
export class Workflow<ContextData, Start, Stop> {
|
||||
#steps: Map<
|
||||
StepHandler<ContextData, never, never>,
|
||||
{
|
||||
inputs: AnyWorkflowEventConstructor[];
|
||||
outputs: AnyWorkflowEventConstructor[];
|
||||
}
|
||||
> = new Map();
|
||||
#verbose: boolean = false;
|
||||
#timeout: number | null = null;
|
||||
// fixme: allow microtask
|
||||
#nextTick: Wait = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
constructor(
|
||||
params: {
|
||||
verbose?: boolean;
|
||||
timeout?: number | null;
|
||||
wait?: Wait;
|
||||
} = {},
|
||||
) {
|
||||
if (params.verbose) {
|
||||
this.#verbose = params.verbose;
|
||||
}
|
||||
if (params.timeout) {
|
||||
this.#timeout = params.timeout;
|
||||
}
|
||||
if (params.wait) {
|
||||
this.#nextTick = params.wait;
|
||||
}
|
||||
}
|
||||
|
||||
addStep<
|
||||
const In extends [
|
||||
AnyWorkflowEventConstructor | StartEventConstructor,
|
||||
...(AnyWorkflowEventConstructor | StopEventConstructor)[],
|
||||
],
|
||||
const Out extends [
|
||||
AnyWorkflowEventConstructor | StopEventConstructor,
|
||||
...(AnyWorkflowEventConstructor | StopEventConstructor)[],
|
||||
],
|
||||
>(
|
||||
parameters: StepParameters<In, Out>,
|
||||
stepFn: (
|
||||
context: HandlerContext<ContextData>,
|
||||
...events: {
|
||||
[K in keyof In]: InstanceType<In[K]>;
|
||||
}
|
||||
) => Promise<
|
||||
{
|
||||
[K in keyof Out]: InstanceType<Out[K]>;
|
||||
}[number]
|
||||
>,
|
||||
): this {
|
||||
const { inputs, outputs } = parameters;
|
||||
this.#steps.set(stepFn as never, { inputs, outputs });
|
||||
return this;
|
||||
}
|
||||
|
||||
removeStep(stepFn: StepHandler): this {
|
||||
this.#steps.delete(stepFn);
|
||||
return this;
|
||||
}
|
||||
|
||||
run(
|
||||
event: StartEvent<Start> | Start,
|
||||
): unknown extends ContextData
|
||||
? WorkflowContext<Start, Stop, ContextData>
|
||||
: WorkflowContext<Start, Stop, ContextData | undefined>;
|
||||
run<Data extends ContextData>(
|
||||
event: StartEvent<Start> | Start,
|
||||
data: Data,
|
||||
): WorkflowContext<Start, Stop, Data>;
|
||||
run<Data extends ContextData>(
|
||||
event: StartEvent<Start> | Start,
|
||||
data?: Data,
|
||||
): WorkflowContext<Start, Stop, Data> {
|
||||
const startEvent: StartEvent<Start> =
|
||||
event instanceof StartEvent ? event : new StartEvent(event);
|
||||
|
||||
return new WorkflowContext<Start, Stop, Data>({
|
||||
startEvent,
|
||||
wait: this.#nextTick,
|
||||
contextData: data!,
|
||||
steps: new Map(this.#steps),
|
||||
timeout: this.#timeout,
|
||||
verbose: this.#verbose,
|
||||
queue: undefined,
|
||||
pendingInputQueue: undefined,
|
||||
resolved: null,
|
||||
rejected: null,
|
||||
});
|
||||
}
|
||||
|
||||
recover(data: ArrayBuffer): WorkflowContext<Start, Stop, ContextData> {
|
||||
const jsonString = new TextDecoder().decode(data);
|
||||
|
||||
const state = JSON.parse(jsonString);
|
||||
|
||||
const reconstructedStartEvent = new StartEvent<Start>(state.startEvent);
|
||||
const AllEvents = [...this.#steps]
|
||||
.map(([, { inputs, outputs }]) => [...inputs, ...(outputs ?? [])])
|
||||
.flat();
|
||||
const reconstructedQueue: QueueProtocol[] = state.queue.map(
|
||||
(protocol: QueueProtocol): QueueProtocol => {
|
||||
switch (protocol.type) {
|
||||
case "requestEvent": {
|
||||
const { requestEvent, id } = protocol;
|
||||
const EventType = AllEvents.find(
|
||||
(type) =>
|
||||
type.prototype.constructor.name ===
|
||||
(requestEvent.constructor as unknown as string),
|
||||
);
|
||||
if (!EventType) {
|
||||
throw new TypeError(
|
||||
`Event type not found: ${requestEvent.constructor}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
type: "requestEvent",
|
||||
id,
|
||||
requestEvent: EventType,
|
||||
};
|
||||
}
|
||||
case "event": {
|
||||
const { event } = protocol;
|
||||
const EventType = AllEvents.find(
|
||||
(type) =>
|
||||
type.prototype.constructor.name ===
|
||||
(event.constructor as unknown as string),
|
||||
);
|
||||
if (!EventType) {
|
||||
throw new TypeError(`Event type not found: ${event.constructor}`);
|
||||
}
|
||||
return {
|
||||
type: "event",
|
||||
event: new EventType(event.data),
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const reconstructedPendingInputQueue = state.pendingInputQueue.map(
|
||||
(event: Record<string, unknown>) => {
|
||||
const EventType = AllEvents.find(
|
||||
(type) => type.prototype.constructor.name === event.constructor,
|
||||
);
|
||||
if (!EventType) {
|
||||
throw new TypeError(`Event type not found: ${event.constructor}`);
|
||||
}
|
||||
return new EventType(event.data);
|
||||
},
|
||||
);
|
||||
|
||||
return new WorkflowContext<Start, Stop, ContextData>({
|
||||
startEvent: reconstructedStartEvent,
|
||||
contextData: state.data,
|
||||
wait: this.#nextTick,
|
||||
steps: this.#steps, // Assuming steps do not change and are part of the class prototype or similar
|
||||
timeout: state.timeout,
|
||||
verbose: state.verbose,
|
||||
queue: reconstructedQueue,
|
||||
pendingInputQueue: reconstructedPendingInputQueue,
|
||||
resolved: state.resolved ? new StopEvent<Stop>(state.resolved) : null,
|
||||
rejected: state.rejected ? new Error(state.rejected) : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist/type",
|
||||
"tsBuildInfoFile": "./dist/.tsbuildinfo",
|
||||
"emitDeclarationOnly": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
|
||||
"types": ["node"],
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["./src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Generated
+2544
-3165
File diff suppressed because it is too large
Load Diff
@@ -95,6 +95,9 @@
|
||||
{
|
||||
"path": "./packages/env/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "./packages/workflow/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "./packages/env/tests/tsconfig.json"
|
||||
},
|
||||
@@ -107,6 +110,12 @@
|
||||
{
|
||||
"path": "./examples/vector-store/pg/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "./packages/readers/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "./packages/node-parser/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "./packages/experimental/tsconfig.json"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# @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
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { CodeSplitter } from "@llamaindex/node-parser/code";
|
||||
import Parser from "tree-sitter";
|
||||
import JS from "tree-sitter-javascript";
|
||||
import TS from "tree-sitter-typescript";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
describe("CodeSplitter", () => {
|
||||
test("basic split js", async () => {
|
||||
const parser = new Parser();
|
||||
parser.setLanguage(JS);
|
||||
const codeSplitter = new CodeSplitter({
|
||||
maxChars: "const a = 1;".length,
|
||||
getParser: () => parser,
|
||||
});
|
||||
const result = codeSplitter.splitText(
|
||||
"const a = 1; const b = 2; const c = 3; const d = 4;",
|
||||
);
|
||||
expect(result).toEqual([
|
||||
"const a = 1;",
|
||||
"const b = 2;",
|
||||
"const c = 3;",
|
||||
"const d = 4;",
|
||||
]);
|
||||
});
|
||||
test("basic split ts", async () => {
|
||||
const parser = new Parser();
|
||||
parser.setLanguage(TS.typescript);
|
||||
const codeSplitter = new CodeSplitter({
|
||||
maxChars: "const a: number = 1;".length,
|
||||
getParser: () => parser,
|
||||
});
|
||||
const result = codeSplitter.splitText(
|
||||
"const a: number = 1; const b = 2; const c: number = 3; const d = 4;",
|
||||
);
|
||||
expect(result).toEqual([
|
||||
"const a: number = 1;",
|
||||
"const b = 2;",
|
||||
"const c: number = 3;",
|
||||
"const d = 4;",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Document } from "@llamaindex/core/schema";
|
||||
import { HTMLNodeParser } from "@llamaindex/node-parser/html";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
describe("HTMLNodeParser", () => {
|
||||
test("basic split", async () => {
|
||||
const parser = new HTMLNodeParser();
|
||||
const result = parser.getNodesFromDocuments([
|
||||
new Document({
|
||||
text: `<DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hello World</p>
|
||||
</body>
|
||||
</html>`,
|
||||
}),
|
||||
]);
|
||||
expect(result.length).toEqual(1);
|
||||
expect(result[0]!.getContent()).toEqual("Hello World");
|
||||
});
|
||||
});
|
||||
+11
-2
@@ -1,21 +1,30 @@
|
||||
{
|
||||
"name": "@llamaindex/unit-test",
|
||||
"private": true,
|
||||
"version": "0.0.10",
|
||||
"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
@@ -4,9 +4,10 @@
|
||||
"outDir": "./lib",
|
||||
"module": "node16",
|
||||
"moduleResolution": "node16",
|
||||
"target": "ESNext"
|
||||
"target": "ESNext",
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["./**/*.ts"],
|
||||
"include": ["./**/*.ts", "./**/*.tsx"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../packages/core/tsconfig.json"
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
import type { ReactNode } from "react";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
describe("workflow integration", () => {
|
||||
type Context = {
|
||||
pending: string[];
|
||||
};
|
||||
type Start = string;
|
||||
type Stop = ReactNode;
|
||||
|
||||
test("nodejs", async () => {
|
||||
const workflow = new Workflow<never, Start, Stop>({
|
||||
wait: async () => await new Promise((resolve) => setTimeout(resolve, 0)),
|
||||
});
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async (_, __) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
return new StopEvent("hello");
|
||||
},
|
||||
);
|
||||
|
||||
console.log("start");
|
||||
|
||||
const run = workflow.run("start");
|
||||
await run.then((stop) => {
|
||||
expect(stop.data).toBe("hello");
|
||||
});
|
||||
});
|
||||
|
||||
test("with jsx", async () => {
|
||||
const workflow = new Workflow<never, Start, Stop>();
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async (_, __) => {
|
||||
return new StopEvent(<div>Hey there!</div>);
|
||||
},
|
||||
);
|
||||
|
||||
const run = workflow.run("start");
|
||||
const stop = await run;
|
||||
expect(stop.data).toEqual(<div>Hey there!</div>);
|
||||
});
|
||||
|
||||
test("with message channel", async () => {
|
||||
const workflow = new Workflow<Context, Start, Stop>();
|
||||
|
||||
class AnalysisStartEvent extends WorkflowEvent<string> {}
|
||||
|
||||
class AnalysisStopEvent extends WorkflowEvent<string> {}
|
||||
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async ({ data, sendEvent, requireEvent }) => {
|
||||
data.pending.push("analyzing");
|
||||
sendEvent(new AnalysisStartEvent("analysis my document"));
|
||||
const event = await requireEvent(AnalysisStopEvent);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
data.pending.push("analysis complete");
|
||||
return new StopEvent(event.data);
|
||||
},
|
||||
);
|
||||
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [AnalysisStartEvent],
|
||||
outputs: [AnalysisStopEvent],
|
||||
},
|
||||
async ({ data }) => {
|
||||
data.pending.push("loading document");
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
data.pending.push("document loaded");
|
||||
return new AnalysisStopEvent("analysis complete");
|
||||
},
|
||||
);
|
||||
|
||||
const run = workflow.run("start").with({
|
||||
pending: [],
|
||||
});
|
||||
await run;
|
||||
expect(run.data.pending).toEqual([
|
||||
"analyzing",
|
||||
"loading document",
|
||||
"document loaded",
|
||||
"analysis complete",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,871 @@
|
||||
import type {
|
||||
HandlerContext,
|
||||
StepHandler,
|
||||
StepParameters,
|
||||
} from "@llamaindex/workflow";
|
||||
import {
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
expectTypeOf,
|
||||
test,
|
||||
vi,
|
||||
type Mocked,
|
||||
} from "vitest";
|
||||
|
||||
class JokeEvent extends WorkflowEvent<{ joke: string }> {}
|
||||
|
||||
class AnalysisEvent extends WorkflowEvent<{ analysis: string }> {}
|
||||
|
||||
describe("type system", () => {
|
||||
test("handler", () => {
|
||||
type Parameters = StepParameters<
|
||||
[typeof StartEvent<string>],
|
||||
[typeof StopEvent<string>]
|
||||
>;
|
||||
type Handler = (
|
||||
context: HandlerContext,
|
||||
ev: StartEvent<string>,
|
||||
) => Promise<StopEvent<string>>;
|
||||
type Handler2 = (
|
||||
context: HandlerContext,
|
||||
ev: StartEvent<string>,
|
||||
) => Promise<StopEvent<number>>;
|
||||
type Handler3 = (
|
||||
context: HandlerContext,
|
||||
ev: StartEvent<string>,
|
||||
) => Promise<AnalysisEvent>;
|
||||
expectTypeOf<Parameters>().toEqualTypeOf<{
|
||||
inputs: [typeof StartEvent<string>];
|
||||
outputs: [typeof StopEvent<string>];
|
||||
}>();
|
||||
expectTypeOf<
|
||||
StepHandler<
|
||||
unknown,
|
||||
[typeof StartEvent<string>],
|
||||
[typeof StopEvent<string>]
|
||||
>
|
||||
>().toEqualTypeOf<Handler>();
|
||||
expectTypeOf<
|
||||
StepHandler<
|
||||
unknown,
|
||||
[typeof StartEvent<string>],
|
||||
[typeof StopEvent<string>]
|
||||
>
|
||||
>().not.toEqualTypeOf<Handler2>();
|
||||
expectTypeOf<
|
||||
StepHandler<
|
||||
unknown,
|
||||
[typeof StartEvent<string>],
|
||||
[typeof StopEvent<string>]
|
||||
>
|
||||
>().not.toEqualTypeOf<Handler3>();
|
||||
});
|
||||
});
|
||||
|
||||
describe("workflow basic", () => {
|
||||
let generateJoke: Mocked<
|
||||
(context: HandlerContext, ev: StartEvent) => Promise<JokeEvent>
|
||||
>;
|
||||
let critiqueJoke: Mocked<
|
||||
(context: HandlerContext, ev: JokeEvent) => Promise<StopEvent<string>>
|
||||
>;
|
||||
let analyzeJoke: Mocked<
|
||||
(context: HandlerContext, ev: JokeEvent) => Promise<AnalysisEvent>
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
generateJoke = vi.fn(async (_context, _: StartEvent) => {
|
||||
return new JokeEvent({ joke: "a joke" });
|
||||
});
|
||||
|
||||
critiqueJoke = vi.fn(async (_context, _: JokeEvent) => {
|
||||
return new StopEvent("stop");
|
||||
});
|
||||
|
||||
analyzeJoke = vi.fn(async (_context: HandlerContext, _: JokeEvent) => {
|
||||
return new AnalysisEvent({ analysis: "an analysis" });
|
||||
});
|
||||
});
|
||||
|
||||
test("workflow basic", async () => {
|
||||
const workflow = new Workflow<
|
||||
{
|
||||
foo: string;
|
||||
bar: number;
|
||||
},
|
||||
string,
|
||||
string
|
||||
>();
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async ({ data }, start) => {
|
||||
expect(start).toBeInstanceOf(StartEvent);
|
||||
expect(start.data).toBe("start");
|
||||
expect(data.bar).toBe(42);
|
||||
expect(data.foo).toBe("foo");
|
||||
return new StopEvent("stopped");
|
||||
},
|
||||
);
|
||||
|
||||
const result = workflow.run("start", {
|
||||
foo: "foo",
|
||||
bar: 42,
|
||||
});
|
||||
await result;
|
||||
});
|
||||
|
||||
test("run workflow", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
jokeFlow.addStep(
|
||||
{ inputs: [StartEvent<string>], outputs: [JokeEvent] },
|
||||
generateJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{ inputs: [JokeEvent], outputs: [StopEvent] },
|
||||
critiqueJoke,
|
||||
);
|
||||
|
||||
const result = await jokeFlow.run("pirates");
|
||||
|
||||
expect(generateJoke).toHaveBeenCalledTimes(1);
|
||||
expect(critiqueJoke).toHaveBeenCalledTimes(1);
|
||||
expect(result.data).toBe("stop");
|
||||
});
|
||||
|
||||
test("stream events", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [JokeEvent],
|
||||
},
|
||||
generateJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
critiqueJoke,
|
||||
);
|
||||
|
||||
const run = jokeFlow.run("pirates");
|
||||
const event = await run[Symbol.asyncIterator]().next(); // get one event to avoid testing timeout
|
||||
const result = await run;
|
||||
|
||||
expect(generateJoke).toHaveBeenCalledTimes(1);
|
||||
expect(critiqueJoke).toHaveBeenCalledTimes(1);
|
||||
expect(result.data).toBe("stop");
|
||||
expect(event).not.toBeNull();
|
||||
});
|
||||
|
||||
test("workflow timeout", async () => {
|
||||
const TIMEOUT = 1;
|
||||
const jokeFlow = new Workflow<unknown, string, string>({
|
||||
verbose: true,
|
||||
timeout: TIMEOUT,
|
||||
});
|
||||
|
||||
const longRunning = async (_context: HandlerContext, ev: StartEvent) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for 2 seconds
|
||||
return new StopEvent("We waited 2 seconds");
|
||||
};
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
longRunning,
|
||||
);
|
||||
const run = jokeFlow.run("Let's start");
|
||||
await expect(run).rejects.toThrow(
|
||||
`Operation timed out after ${TIMEOUT} seconds`,
|
||||
);
|
||||
});
|
||||
|
||||
test("workflow validation", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
jokeFlow.addStep(
|
||||
{ inputs: [StartEvent<string>], outputs: [StopEvent] },
|
||||
generateJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{ inputs: [JokeEvent], outputs: [StopEvent] },
|
||||
critiqueJoke,
|
||||
);
|
||||
expect(async () => {
|
||||
await jokeFlow.run("pirates").strict();
|
||||
}).rejects.toThrow(
|
||||
"Step spy returned an unexpected output event JokeEvent",
|
||||
);
|
||||
});
|
||||
|
||||
test("requireEvents - 1", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async (ctx, start) => {
|
||||
ctx.sendEvent(new AnalysisEvent({ analysis: "an analysis" }));
|
||||
await ctx.requireEvent(JokeEvent);
|
||||
return new StopEvent("Report generated");
|
||||
},
|
||||
);
|
||||
|
||||
const fn = vi.fn(async () => {
|
||||
return new JokeEvent({ joke: "a joke" });
|
||||
});
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [AnalysisEvent],
|
||||
outputs: [JokeEvent],
|
||||
},
|
||||
fn,
|
||||
);
|
||||
|
||||
const result = await jokeFlow.run("pirates");
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
expect(result.data).toBe("Report generated");
|
||||
});
|
||||
|
||||
test("run workflow with multiple in-degree", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [JokeEvent],
|
||||
},
|
||||
async (context, _) => {
|
||||
context.sendEvent(
|
||||
new AnalysisEvent({
|
||||
analysis: "an analysis",
|
||||
}),
|
||||
);
|
||||
return new JokeEvent({
|
||||
joke: "a joke",
|
||||
});
|
||||
},
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent, AnalysisEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async () => {
|
||||
return new StopEvent("The analysis is insightful and helpful.");
|
||||
},
|
||||
);
|
||||
|
||||
const result = await jokeFlow.run("pirates");
|
||||
expect(result.data).toBe("The analysis is insightful and helpful.");
|
||||
});
|
||||
|
||||
test("run invalid workflow", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [JokeEvent],
|
||||
},
|
||||
generateJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
// @ts-expect-error it actually returns AnalysisEvent
|
||||
analyzeJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async () => {
|
||||
return new StopEvent("The analysis is insightful and helpful.");
|
||||
},
|
||||
);
|
||||
const consoleSpy = vi.spyOn(console, "warn");
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(0);
|
||||
const result = await jokeFlow.run("pirates");
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(1);
|
||||
consoleSpy.mockRestore();
|
||||
expect(result.data).toBe("The analysis is insightful and helpful.");
|
||||
});
|
||||
|
||||
test("run workflow with object-based StartEvent and StopEvent", async () => {
|
||||
const objectFlow = new Workflow<
|
||||
unknown,
|
||||
Person,
|
||||
{
|
||||
result: {
|
||||
greeting: string;
|
||||
};
|
||||
}
|
||||
>({ verbose: true });
|
||||
|
||||
type Person = { name: string; age: number };
|
||||
|
||||
const processObject = vi.fn(async (_context, ev: StartEvent<Person>) => {
|
||||
const { name, age } = ev.data;
|
||||
return new StopEvent({
|
||||
result: { greeting: `Hello ${name}, you are ${age} years old!` },
|
||||
});
|
||||
});
|
||||
|
||||
objectFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<Person>],
|
||||
outputs: [
|
||||
StopEvent<{
|
||||
result: {
|
||||
greeting: string;
|
||||
};
|
||||
}>,
|
||||
],
|
||||
},
|
||||
processObject,
|
||||
);
|
||||
|
||||
const result = await objectFlow.run(
|
||||
new StartEvent<Person>({ name: "Alice", age: 30 }),
|
||||
);
|
||||
|
||||
expect(processObject).toHaveBeenCalledTimes(1);
|
||||
expect(result.data.result).toEqual({
|
||||
greeting: "Hello Alice, you are 30 years old!",
|
||||
});
|
||||
});
|
||||
|
||||
test("workflow with two concurrent steps", async () => {
|
||||
const concurrentFlow = new Workflow<unknown, string, string>({
|
||||
verbose: true,
|
||||
});
|
||||
|
||||
const step1 = vi.fn(async (_context, _ev: StartEvent) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
return new StopEvent("Step 1 completed");
|
||||
});
|
||||
|
||||
const step2 = vi.fn(async (_context, _ev: StartEvent) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
return new StopEvent("Step 2 completed");
|
||||
});
|
||||
|
||||
concurrentFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
step1,
|
||||
);
|
||||
concurrentFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
step2,
|
||||
);
|
||||
|
||||
const startTime = new Date();
|
||||
const result = await concurrentFlow.run("start");
|
||||
const endTime = new Date();
|
||||
const duration = endTime.getTime() - startTime.getTime();
|
||||
|
||||
expect(step1).toHaveBeenCalledTimes(1);
|
||||
expect(step2).toHaveBeenCalledTimes(1);
|
||||
expect(duration).toBeLessThan(200);
|
||||
expect(result.data).toBe("Step 2 completed");
|
||||
});
|
||||
|
||||
test("workflow with two concurrent cyclic steps", async () => {
|
||||
const concurrentCyclicFlow = new Workflow<unknown, string, string>({
|
||||
verbose: true,
|
||||
});
|
||||
|
||||
class Step1Event extends WorkflowEvent<{
|
||||
result: string;
|
||||
}> {}
|
||||
|
||||
class Step2Event extends WorkflowEvent<{
|
||||
result: string;
|
||||
}> {}
|
||||
|
||||
let step2Count = 0;
|
||||
|
||||
const step1 = vi.fn(async (_context, ev: StartEvent | Step1Event) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return new Step1Event({ result: "Step 1 completed" });
|
||||
});
|
||||
|
||||
const step2 = vi.fn(async (_context, ev: StartEvent | Step2Event) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
step2Count++;
|
||||
if (step2Count >= 5) {
|
||||
return new StopEvent("Step 2 completed 5 times");
|
||||
}
|
||||
return new Step2Event({ result: "Step 2 completed" });
|
||||
});
|
||||
|
||||
concurrentCyclicFlow.addStep(
|
||||
{
|
||||
inputs: [WorkflowEvent.or(StartEvent<string>, Step1Event)],
|
||||
outputs: [Step1Event],
|
||||
},
|
||||
step1,
|
||||
);
|
||||
concurrentCyclicFlow.addStep(
|
||||
{
|
||||
inputs: [WorkflowEvent.or(StartEvent<string>, Step2Event)],
|
||||
outputs: [Step2Event, StopEvent],
|
||||
},
|
||||
step2,
|
||||
);
|
||||
|
||||
const startTime = new Date();
|
||||
const result = await concurrentCyclicFlow.run("start");
|
||||
const endTime = new Date();
|
||||
const duration = endTime.getTime() - startTime.getTime();
|
||||
|
||||
expect(step1).toHaveBeenCalledTimes(1);
|
||||
expect(step2).toHaveBeenCalledTimes(5);
|
||||
expect(duration).toBeGreaterThanOrEqual(500); // At least 5 * 100ms for step2
|
||||
expect(duration).toBeLessThan(1000); // Less than 1 second
|
||||
expect(result.data).toBe("Step 2 completed 5 times");
|
||||
});
|
||||
|
||||
test("sendEvent", async () => {
|
||||
const myWorkflow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
class QueryEvent extends WorkflowEvent<{ query: string }> {}
|
||||
|
||||
class QueryResultEvent extends WorkflowEvent<{ result: string }> {}
|
||||
|
||||
class PendingEvent extends WorkflowEvent<void> {}
|
||||
|
||||
myWorkflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [PendingEvent],
|
||||
},
|
||||
async (context: HandlerContext, events) => {
|
||||
context.sendEvent(new QueryEvent({ query: "something" }));
|
||||
return new PendingEvent();
|
||||
},
|
||||
);
|
||||
|
||||
myWorkflow.addStep(
|
||||
{
|
||||
inputs: [QueryEvent],
|
||||
outputs: [QueryResultEvent],
|
||||
},
|
||||
async (context, event) => {
|
||||
return new QueryResultEvent({ result: "query result" });
|
||||
},
|
||||
);
|
||||
|
||||
myWorkflow.addStep(
|
||||
{
|
||||
inputs: [PendingEvent, QueryResultEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (context, ev0, ev1) => {
|
||||
return new StopEvent(ev1.data.result);
|
||||
},
|
||||
);
|
||||
|
||||
const result = await myWorkflow.run("start");
|
||||
expect(result.data).toBe("query result");
|
||||
});
|
||||
|
||||
test("requireEvents - 2", async () => {
|
||||
const myWorkflow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
class QueryEvent extends WorkflowEvent<{ query: string }> {}
|
||||
|
||||
class QueryResultEvent extends WorkflowEvent<{ result: string }> {}
|
||||
|
||||
myWorkflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (context: HandlerContext) => {
|
||||
context.sendEvent(new QueryEvent({ query: "something" }));
|
||||
const queryResultEvent = await context.requireEvent(QueryResultEvent);
|
||||
return new StopEvent(queryResultEvent.data.result);
|
||||
},
|
||||
);
|
||||
|
||||
myWorkflow.addStep(
|
||||
{
|
||||
inputs: [QueryEvent],
|
||||
outputs: [QueryResultEvent],
|
||||
},
|
||||
async (context, event) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
return new QueryResultEvent({ result: "query result" });
|
||||
},
|
||||
);
|
||||
|
||||
const result = await myWorkflow.run("start");
|
||||
expect(result.data).toBe("query result");
|
||||
});
|
||||
});
|
||||
|
||||
describe("workflow event loop", () => {
|
||||
test("basic", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (_context, ev: StartEvent) => {
|
||||
return new StopEvent(`Hello ${ev.data}!`);
|
||||
},
|
||||
);
|
||||
|
||||
const result = await jokeFlow.run("world");
|
||||
expect(result.data).toBe("Hello world!");
|
||||
});
|
||||
|
||||
test("branch", async () => {
|
||||
const myFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
class BranchA1Event extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
class BranchA2Event extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
class BranchB1Event extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
class BranchB2Event extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
let control = false;
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [BranchA1Event, BranchB1Event],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
if (control) {
|
||||
return new BranchA1Event({ payload: ev.data });
|
||||
} else {
|
||||
return new BranchB1Event({ payload: ev.data });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [BranchA1Event],
|
||||
outputs: [BranchA2Event],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
return new BranchA2Event({ payload: ev.data.payload });
|
||||
},
|
||||
);
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [BranchB1Event],
|
||||
outputs: [BranchB2Event],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
return new BranchB2Event({ payload: ev.data.payload });
|
||||
},
|
||||
);
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [BranchA2Event],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
return new StopEvent(`Branch A2: ${ev.data.payload}`);
|
||||
},
|
||||
);
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [BranchB2Event],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
return new StopEvent(`Branch B2: ${ev.data.payload}`);
|
||||
},
|
||||
);
|
||||
|
||||
{
|
||||
const result = await myFlow.run("world");
|
||||
expect(result.data).toMatch(/Branch B2: world/);
|
||||
}
|
||||
|
||||
control = true;
|
||||
|
||||
{
|
||||
const result = await myFlow.run("world");
|
||||
expect(result.data).toMatch(/Branch A2: world/);
|
||||
}
|
||||
|
||||
{
|
||||
const context = myFlow.run("world");
|
||||
for await (const event of context) {
|
||||
if (event instanceof BranchA2Event) {
|
||||
expect(event.data.payload).toBe("world");
|
||||
}
|
||||
if (event instanceof StopEvent) {
|
||||
expect(event.data).toMatch(/Branch A2: world/);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("one event have multiple outputs", async () => {
|
||||
const myFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
class AEvent extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
class BEvent extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
class CEvent extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
class DEvent extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
return new StopEvent("STOP");
|
||||
},
|
||||
);
|
||||
|
||||
const fn = vi.fn(async (_context, ev: StartEvent) => {
|
||||
return new AEvent({ payload: ev.data });
|
||||
});
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [AEvent],
|
||||
},
|
||||
fn,
|
||||
);
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [AEvent],
|
||||
outputs: [BEvent, CEvent],
|
||||
},
|
||||
async (_context, ev: AEvent) => {
|
||||
return new BEvent({ payload: ev.data.payload });
|
||||
},
|
||||
);
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [AEvent],
|
||||
outputs: [CEvent],
|
||||
},
|
||||
async (_context, ev: AEvent) => {
|
||||
return new CEvent({ payload: ev.data.payload });
|
||||
},
|
||||
);
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [BEvent],
|
||||
outputs: [DEvent],
|
||||
},
|
||||
async (_context, ev: BEvent) => {
|
||||
return new DEvent({ payload: ev.data.payload });
|
||||
},
|
||||
);
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [CEvent],
|
||||
outputs: [DEvent],
|
||||
},
|
||||
async (_context, ev: CEvent) => {
|
||||
return new DEvent({ payload: ev.data.payload });
|
||||
},
|
||||
);
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [DEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (_context, ev: DEvent) => {
|
||||
return new StopEvent(`Hello ${ev.data.payload}!`);
|
||||
},
|
||||
);
|
||||
|
||||
const result = await myFlow.run("world");
|
||||
expect(result.data).toBe("STOP");
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
// streaming events will allow to consume event even stop event is reached
|
||||
const stream = myFlow.run("world");
|
||||
for await (const _ of stream) {
|
||||
/* empty */
|
||||
}
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("run with custom context", async () => {
|
||||
type MyContext = { name: string };
|
||||
const myFlow = new Workflow<MyContext, string, string>({ verbose: true });
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async ({ data }, _: StartEvent) => {
|
||||
return new StopEvent(`Hello ${data.name}!`);
|
||||
},
|
||||
);
|
||||
|
||||
const result = await myFlow.run("world", { name: "Alice" });
|
||||
expect(result.data).toBe("Hello Alice!");
|
||||
});
|
||||
|
||||
test("run with custom context with two streaming", async () => {
|
||||
type MyContext = { name: string };
|
||||
const myFlow = new Workflow<MyContext, string, string>({ verbose: true });
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async ({ data }, _) => {
|
||||
if (data == null) {
|
||||
return new StopEvent({ result: "EMPTY" });
|
||||
}
|
||||
return new StopEvent({ result: `Hello ${data.name}!` });
|
||||
},
|
||||
);
|
||||
|
||||
const context1 = myFlow.run("world");
|
||||
const context2 = context1.with({ name: "Alice" });
|
||||
const context3 = context1.with({ name: "Bob" });
|
||||
expect(await context1).toMatchInlineSnapshot(`
|
||||
StopEvent {
|
||||
"data": {
|
||||
"result": "EMPTY",
|
||||
},
|
||||
"displayName": "StopEvent",
|
||||
}
|
||||
`);
|
||||
expect(await context2).toMatchInlineSnapshot(`
|
||||
StopEvent {
|
||||
"data": {
|
||||
"result": "Hello Alice!",
|
||||
},
|
||||
"displayName": "StopEvent",
|
||||
}
|
||||
`);
|
||||
expect(await context3).toMatchInlineSnapshot(`
|
||||
StopEvent {
|
||||
"data": {
|
||||
"result": "Hello Bob!",
|
||||
},
|
||||
"displayName": "StopEvent",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("snapshot", async () => {
|
||||
test("snapshot and recover", async () => {
|
||||
const myFlow = new Workflow({ verbose: true });
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (_, ev: StartEvent) => {
|
||||
return new StopEvent(`Hello ${ev.data}!`);
|
||||
},
|
||||
);
|
||||
const context = myFlow.run("world");
|
||||
const arrayBuffer = context.snapshot();
|
||||
expect(arrayBuffer).toBeInstanceOf(ArrayBuffer);
|
||||
const context2 = await myFlow.recover(arrayBuffer);
|
||||
expect(context2.data).toBe("Hello world!");
|
||||
});
|
||||
|
||||
test("snapshot in middle of workflow run ", async () => {
|
||||
const myFlow = new Workflow<
|
||||
{
|
||||
value: number;
|
||||
},
|
||||
string,
|
||||
string
|
||||
>({ verbose: true });
|
||||
|
||||
class AEvent extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
const fn = vi.fn(async (_, ev: StartEvent) => {
|
||||
return new AEvent({ payload: ev.data });
|
||||
});
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [AEvent],
|
||||
},
|
||||
fn,
|
||||
);
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [AEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async ({ data }, _: AEvent) => {
|
||||
return new StopEvent(`Hello ${data.value}!`);
|
||||
},
|
||||
);
|
||||
|
||||
const context = myFlow.run("world", {
|
||||
value: 1,
|
||||
});
|
||||
for await (const event of context) {
|
||||
if (event instanceof AEvent) {
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
const arrayBuffer = context.snapshot();
|
||||
expect(arrayBuffer).toBeInstanceOf(ArrayBuffer);
|
||||
const context2 = await myFlow.recover(arrayBuffer).with({
|
||||
value: 2,
|
||||
});
|
||||
expect(context2.data).toBe("Hello 2!");
|
||||
break;
|
||||
}
|
||||
if (event instanceof StopEvent) {
|
||||
expect(event.data).toBe("Hello 1!");
|
||||
}
|
||||
}
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user