chore: clean up demo (#185)

Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
This commit is contained in:
Thuc Pham
2025-09-17 10:27:11 +07:00
committed by GitHub
parent a0d9d3a6d6
commit 26d0ea4270
85 changed files with 5644 additions and 2637 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
run: pnpm install
- name: Run build
run: pnpm run build
run: pnpm run build --force
- name: Pre Release
run: pnpx pkg-pr-new publish --pnpm ./packages/*
+4
View File
@@ -25,6 +25,10 @@ bun add @llamaindex/workflow-core
deno add npm:@llamaindex/workflow-core
```
### Demos
For examples, check out the [demo folder](./demo).
### First, define events
```ts
+60
View File
@@ -0,0 +1,60 @@
# LlamaIndex Workflows TS Demos
This directory contains various demo applications showcasing different aspects of the LlamaIndex Workflows TS engine across different runtimes and frameworks. Each demo is **standalone** and can be run independently with `npm install` and the appropriate start command.
## Demos
### 🌐 **Browser** - Frontend-only workflow execution
- **Location**: `./browser/`
- **Tech**: React + Vite + TypeScript
- **Features**: Client-side workflow execution, React integration
- **Run**: `npm install && npm run dev`
### ☁️ **Cloudflare Workers** - Edge runtime workflows
- **Location**: `./cloudflare/`
- **Tech**: Cloudflare Workers + Hono + Wrangler
- **Features**: Edge computing, serverless workflows
- **Run**: `npm install && npm run dev`
### 🦕 **Deno** - Deno runtime integration
- **Location**: `./deno/`
- **Tech**: Deno + JSR + npm compatibility
- **Features**: Native Deno support, JSR imports, testing
- **Run**: `deno task dev`
### 🚀 **Express** - Client-server architecture
- **Location**: `./express/`
- **Tech**: Express.js + TypeScript + OpenAI
- **Features**: REST API, human-in-the-loop, state snapshots
- **Run**: `npm install && npm run server`
### ⚡ **Hono** - Lightweight web framework
- **Location**: `./hono/`
- **Tech**: Hono + Node.js + OpenAI
- **Features**: Fast web framework, AI agents, HITL workflows
- **Run**: `npm install && npm run dev`
### 📦 **Node.js** - Comprehensive Node.js examples
- **Location**: `./node/`
- **Tech**: Node.js + TypeScript + various libraries
- **Features**: Multiple patterns, RxJS, MCP, file parsing
- **Run**: `npm install && npm run basic`
### ⚛️ **Next.js** - Full-stack React application
- **Location**: `./next/`
- **Tech**: Next.js + React + Tailwind CSS
- **Features**: PDF processing, AI workflows, modern UI
- **Run**: `npm install && npm run dev`
### 🔍 **Trace Events** - OpenTelemetry integration
- **Location**: `./trace-events/`
- **Tech**: Node.js + OpenTelemetry + TypeScript
- **Features**: Workflow tracing, observability, debugging
- **Run**: `npm install && npm run dev`
### 📊 **Visualization** - Workflow visualization
- **Location**: `./visualization/`
- **Tech**: React + Vite + D3.js
- **Features**: Workflow graph visualization, interactive UI
- **Run**: `npm install && npm run dev`
+1 -4
View File
@@ -2,9 +2,6 @@
"root": false,
"extends": "//",
"files": {
"includes": ["src/**", "tests/**"]
},
"linter": {
"enabled": false
"includes": ["src/**", "tests/**", "!**/.next"]
}
}
+4 -4
View File
@@ -1,15 +1,15 @@
{
"name": "browser",
"private": true,
"version": "0.0.0",
"name": "demo-browser",
"version": "0.0.1",
"type": "module",
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@llamaindex/workflow-core": "latest",
"@llamaindex/workflow-core": "^1.3.3",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
+7 -10
View File
@@ -1,12 +1,7 @@
import llamaindexLogo from "/llamaindex.svg";
import reactLogo from "./assets/react.svg";
import "./App.css";
import {
createWorkflow,
getContext,
workflowEvent,
} from "@llamaindex/workflow-core";
import { runWorkflow } from "@llamaindex/workflow-core/stream/run";
import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core";
import { Suspense } from "react";
const startEvent = workflowEvent();
@@ -14,14 +9,16 @@ const stopEvent = workflowEvent<string>();
const workflow = createWorkflow();
workflow.handle([startEvent], () => {
workflow.handle([startEvent], (context) => {
setTimeout(() => {
const context = getContext();
context.sendEvent(stopEvent.with("Hello, World!"));
}, 1000);
});
const promise = runWorkflow(workflow, startEvent.with(), stopEvent);
const context = workflow.createContext();
context.sendEvent(startEvent.with());
const events = await context.stream.until(stopEvent).toArray();
function App() {
return (
@@ -38,7 +35,7 @@ function App() {
<div className="card">
<p>
<Suspense fallback="Loading...">
{promise.then(({ data }) => data)}
{events.map(({ data }) => data)}
</Suspense>
</p>
<p>
+1 -1
View File
@@ -3,7 +3,7 @@ import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
createRoot(document.getElementById("root")!).render(
createRoot(document.getElementById("root") as HTMLElement).render(
<StrictMode>
<App />
</StrictMode>,
+113
View File
@@ -0,0 +1,113 @@
# Cloudflare Workers Workflow Demo
This demo shows how to use the LlamaIndex Workflows TS engine with Cloudflare Workers, demonstrating a simple workflow that processes user input and returns a response.
> **Note**: This demo is part of the `cloudflare-workers-openapi` package and showcases basic workflow integration with Cloudflare Workers.
## Overview
The demo creates a Cloudflare Worker that:
- Serves a simple HTML form interface
- Handles POST requests to execute workflows
- Uses Hono as the web framework
- Demonstrates basic workflow event handling
## Architecture
The demo consists of:
- **`src/index.ts`** - Main Cloudflare Worker entry point with Hono app
- **`wrangler.jsonc`** - Cloudflare Workers configuration
- **`package.json`** - Dependencies and scripts
## How it works
1. **User visits the page** - The worker serves an HTML form with an input field
2. **User submits form** - JavaScript sends a POST request to `/workflow` with the input data
3. **Workflow execution** - The worker processes the input through a simple workflow:
- `startEvent` receives the user input
- Handler processes the input and emits a `stopEvent` with a greeting
4. **Response** - The worker returns the workflow result as JSON
## Workflow Definition
```typescript
const startEvent = workflowEvent<string>();
const stopEvent = workflowEvent<string>();
const workflow = createWorkflow();
workflow.handle([startEvent], (_context, { data }) => {
return stopEvent.with(`hello, ${data}!`);
});
```
## Usage
### Prerequisites
- Node.js 18+
- npm (or pnpm)
- Cloudflare account (for deployment)
### Local Development
1. Install dependencies:
```bash
npm install
```
2. Start the development server:
```bash
npm run dev
```
This will start the Wrangler development server, typically at `http://localhost:8787`
3. Open your browser and navigate to the local URL
4. Enter a name in the form and click "Send event" to test the workflow
### Deployment
1. Configure your Cloudflare Worker (if not already done):
```bash
npm run cf-typegen
```
2. Deploy to Cloudflare Workers:
```bash
npm run deploy
```
## API Endpoints
- **`GET /`** - Serves the HTML form interface
- **`POST /workflow`** - Executes the workflow with the provided input data
## Configuration
The `wrangler.jsonc` file contains the Cloudflare Workers configuration:
- **`name`**: Worker name (currently "workspace-worker")
- **`main`**: Entry point file (`src/index.ts`)
- **`compatibility_flags`**: Enables Node.js compatibility features
- **`compatibility_date`**: Sets the compatibility date for Cloudflare Workers features
## Dependencies
- **`@llamaindex/workflow-core`** - Core workflow engine
- **`hono`** - Lightweight web framework for Cloudflare Workers
- **`@cloudflare/workers-types`** - TypeScript types for Cloudflare Workers
- **`wrangler`** - Cloudflare Workers CLI tool
## Key Features Demonstrated
- **Event-driven workflows** - Simple event handling with type safety
- **Cloudflare Workers integration** - Running workflows in the edge runtime
- **Hono framework** - Clean, minimal web framework for Workers
- **TypeScript support** - Full type safety throughout the application
- **Client-side interaction** - JavaScript form handling with fetch API
+1 -2
View File
@@ -5,11 +5,10 @@
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev",
"start": "wrangler dev",
"cf-typegen": "wrangler types"
},
"dependencies": {
"@llamaindex/workflow-core": "latest",
"@llamaindex/workflow-core": "^1.3.3",
"hono": "^4.8.3"
},
"devDependencies": {
+1
View File
@@ -1,6 +1,7 @@
/**
* For more details on how to configure Wrangler, refer to:
* https://developers.cloudflare.com/workers/wrangler/configuration/
* For testing locally, add "nodejs_compat" to the compatibility_flags.
*/
{
"$schema": "node_modules/wrangler/config-schema.json",
+41
View File
@@ -0,0 +1,41 @@
# Deno Workflow Demo
Simple workflow demo using LlamaIndex Workflows TS with Deno runtime.
## Files
- `main.ts` - Workflow definition and execution
- `main_test.ts` - Deno test suite
- `deno.json` - Deno configuration
## How it works
1. Creates workflow with start/end events
2. Uses setTimeout for async processing
3. Sends "Hello World!" message
4. Tests workflow with Deno streams
## Usage
```bash
# Run workflow
deno run main.ts
# Run with watch mode
deno task dev
# Run tests
deno test
```
## Dependencies
- `@llamaindex/workflow-core` - Workflow engine (npm)
- `@std/assert` - Deno assertions (JSR)
## Key Features
- **Deno Integration** - Native runtime support
- **JSR Imports** - JavaScript Registry
- **NPM Compatibility** - npm packages in Deno
- **Stream Testing** - Native TransformStream/WritableStream
+6 -3
View File
@@ -11,7 +11,7 @@ import type {
ChatCompletionMessageFunctionToolCall as ToolCall,
ChatCompletionToolMessageParam as ToolResponseMessage,
} from "openai/resources/chat/completions";
import * as readline from "readline/promises";
import * as readline from "node:readline/promises";
type AgentWorkflowState = {
expectedToolCount: number;
@@ -195,11 +195,14 @@ workflow.handle([toolCallEvent], async (context, event) => {
workflow.handle([humanResponseEvent], async (context, event) => {
const { sendEvent, state } = context;
if (!state.humanToolId) {
throw new Error("No human tool id");
}
sendEvent(
toolResponseEvent.with({
role: "tool",
content: "My name is " + event.data,
tool_call_id: state.humanToolId!,
content: `My name is ${event.data}`,
tool_call_id: state.humanToolId,
}),
);
});
+2 -2
View File
@@ -1,8 +1,8 @@
import * as readline from "readline/promises";
import * as readline from "node:readline/promises";
const SERVER_URL = "http://localhost:3000";
async function makeRequest(endpoint: string, data: any) {
async function makeRequest(endpoint: string, data: object) {
const response = await fetch(`${SERVER_URL}${endpoint}`, {
method: "POST",
headers: {
+5 -2
View File
@@ -185,11 +185,14 @@ workflow.handle([toolCallEvent], async (context, event) => {
workflow.handle([humanResponseEvent], async (context, event) => {
const { sendEvent, state } = context;
if (!state.humanToolId) {
throw new Error("No human tool id");
}
sendEvent(
toolResponseEvent.with({
role: "tool",
content: "My name is " + event.data,
tool_call_id: state.humanToolId!,
content: `My name is ${event.data}`,
tool_call_id: state.humanToolId,
}),
);
});
+7 -11
View File
@@ -1,27 +1,23 @@
{
"name": "@llamaindex/express-demo",
"private": true,
"version": "1.0.4",
"description": "",
"main": "index.js",
"name": "demo-express",
"version": "0.0.1",
"type": "module",
"private": true,
"scripts": {
"server": "tsx 5-server.ts",
"client": "tsx 5-client.ts",
"test": "echo \"Error: no test specified\" && exit 1"
"dev": "tsx 5-server.ts"
},
"keywords": [],
"author": "",
"license": "MIT",
"packageManager": "pnpm@10.15.0",
"dependencies": {
"@llamaindex/workflow-core": "workspace:*",
"@llamaindex/workflow-core": "^1.3.3",
"express": "^5.1.0",
"openai": "^5.7.0",
"uuid": "^11.1.0"
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/node": "^24.0.4",
"@types/uuid": "^10.0.0",
"tsx": "^4.20.4"
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noUncheckedIndexedAccess": false,
"outDir": "./dist",
"rootDir": ".",
"declaration": true,
"sourceMap": true
},
"include": ["*.ts"],
"exclude": ["node_modules", "dist"]
}
+125
View File
@@ -0,0 +1,125 @@
# Hono Workflow Demo
This demo shows how to use the LlamaIndex Workflows TS engine with Hono web framework, demonstrating AI agent workflows with tool calling and human-in-the-loop interactions.
## Overview
The demo creates a Hono server that:
- Handles AI agent workflows with tool calling
- Implements human-in-the-loop (HITL) workflows with state snapshots
- Uses OpenAI for AI completions and tool execution
- Demonstrates workflow state management and resumption
## Architecture
The demo consists of:
- **`app.ts`** - Main Hono server with workflow endpoints
- **`package.json`** - Dependencies and scripts
- **`../workflows/tool-call-agent.ts`** - AI agent workflow with tool calling
- **`../workflows/human-in-the-loop.ts`** - HITL workflow with state management
## How it works
1. **Tool Call Workflow** - AI agent processes user input and can call tools (like weather API)
2. **Human-in-the-Loop Workflow** - AI can request human input and resume from snapshots
3. **State Management** - Workflows can be paused, snapshotted, and resumed
4. **Response** - Server returns workflow results as JSON
## Workflow Definitions
### Tool Call Agent
```typescript
const startEvent = workflowEvent<string>();
const toolCallEvent = workflowEvent<ChatCompletionMessageToolCall>();
const stopEvent = workflowEvent<string>();
workflow.handle([startEvent], async (context, { data }) => {
// Process with OpenAI and handle tool calls
});
```
### Human-in-the-Loop
```typescript
const { withState } = createStatefulMiddleware();
const workflow = withState(createWorkflow());
workflow.handle([startEvent], async (context, { data }) => {
// AI can request human input via humanRequestEvent
});
```
## Usage
### Prerequisites
- Node.js 18+
- npm (or pnpm)
- OpenAI API key
### Local Development
1. Install dependencies:
```bash
npm install
```
2. Set your OpenAI API key:
```bash
export OPENAI_API_KEY=your-api-key-here
```
3. Start the development server:
```bash
npx tsx app.ts
```
This will start the server at `http://localhost:3000`
4. Test the endpoints with curl or a REST client
### API Endpoints
- **`POST /workflow`** - Execute tool call agent workflow
- **`POST /human-in-the-loop`** - Execute HITL workflow with state snapshots
### Example Usage
```bash
# Tool call workflow
curl -X POST http://localhost:3000/workflow \
-H "Content-Type: text/plain" \
-d "What's the weather like in Tokyo?"
# Human-in-the-loop workflow
curl -X POST http://localhost:3000/human-in-the-loop \
-H "Content-Type: application/json" \
-d '{"data": "Hello"}'
```
## Configuration
The server uses:
- **Hono** - Lightweight web framework
- **@hono/node-server** - Node.js server adapter
- **OpenAI** - AI completions and tool calling
- **tsx** - TypeScript execution
## Dependencies
- **`@llamaindex/workflow-core`** - Core workflow engine
- **`hono`** - Web framework
- **`@hono/node-server`** - Node.js server
- **`openai`** - OpenAI API client
- **`tsx`** - TypeScript runner
## Key Features Demonstrated
- **AI Agent Workflows** - Tool calling with OpenAI
- **Human-in-the-Loop** - Interactive workflows with human input
- **State Management** - Workflow snapshots and resumption
- **Hono Integration** - Clean web framework integration
- **TypeScript Support** - Full type safety throughout
+14 -8
View File
@@ -1,11 +1,8 @@
import { serve } from "@hono/node-server";
import { createHonoHandler } from "@llamaindex/workflow-core/hono";
import { Hono } from "hono";
import {
startEvent,
stopEvent,
toolCallWorkflow,
} from "../workflows/tool-call-agent.js";
import { startEvent, stopEvent, toolCallWorkflow } from "./tool-call-agent.js";
import type { SnapshotData } from "@llamaindex/workflow-core/middleware/state";
const app = new Hono();
@@ -18,7 +15,10 @@ app.post(
),
);
const serializableMemoryMap = new Map<string, any>();
const serializableMemoryMap = new Map<
string,
Omit<SnapshotData, "unrecoverableQueue">
>();
app.post("/human-in-the-loop", async (ctx) => {
const {
@@ -27,12 +27,15 @@ app.post("/human-in-the-loop", async (ctx) => {
startEvent,
humanRequestEvent,
humanInteractionEvent,
} = await import("../workflows/human-in-the-loop");
} = await import("./human-in-the-loop.js");
const json = await ctx.req.json();
let context: ReturnType<typeof workflow.createContext>;
if (json.requestId) {
const data = json.data;
const serializable = serializableMemoryMap.get(json.requestId);
if (!serializable) {
throw new Error("Snapshot data not found");
}
context = workflow.resume(serializable);
context.sendEvent(humanInteractionEvent.with(data));
} else {
@@ -62,7 +65,10 @@ app.post("/human-in-the-loop", async (ctx) => {
.until(stopEvent)
.toArray()
.then((events) => {
const stopEvent = events.at(-1)!;
const stopEvent = events.at(-1);
if (!stopEvent) {
throw new Error("No stop event");
}
resolve(Response.json(stopEvent.data));
});
});
@@ -67,7 +67,7 @@ For example, alex is from "Alexander the Great", who was a king of the ancient G
);
}
}
return stopEvent.with(response.choices[0].message.content!);
return stopEvent.with(response.choices[0].message.content ?? "");
});
workflow.handle([humanInteractionEvent], async (context, { data }) => {
+20
View File
@@ -0,0 +1,20 @@
{
"name": "demo-hono",
"version": "0.0.1",
"type": "module",
"private": true,
"scripts": {
"dev": "tsx app.ts"
},
"dependencies": {
"@hono/node-server": "^1.14.4",
"@llamaindex/workflow-core": "^1.3.3",
"hono": "^4.8.3",
"openai": "^4.0.0",
"zod": "^3.22.0"
},
"devDependencies": {
"@types/node": "^24.0.4",
"tsx": "^4.20.3"
}
}
@@ -70,13 +70,14 @@ toolCallWorkflow.handle([chatEvent], async (context, { data }) => {
}),
)
)
.map((list) => list.at(-1)!)
.map((list) => list.at(-1))
.filter((event) => event !== undefined)
.map(({ data }) => data)
.join("\n");
console.log("toolcall result", result);
sendEvent(chatEvent.with(result));
} else {
console.log("no choices");
return stopEvent.with(choices[0]!.message.content!);
return stopEvent.with(choices[0]?.message.content ?? "");
}
});
+2 -1
View File
@@ -26,7 +26,8 @@
"puppeteer": "^24.19.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwind-merge": "^3.3.1"
"tailwind-merge": "^3.3.1",
"zod": "^3.25.76"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
+2
View File
@@ -1,3 +1,5 @@
/* biome-ignore-all lint/suspicious/noUnknownAtRules: needed for Tailwind v4 custom rules */
@import "tailwindcss";
@import "tw-animate-css";
+1
View File
@@ -78,6 +78,7 @@ export default function HomePage() {
<div className="space-y-4">
<Search className="w-12 h-12 mx-auto text-muted-foreground" />
<div className="space-y-2">
{/* biome-ignore lint/correctness/useUniqueElementIds: used for search */}
<Input
id="search-docs"
value={query}
+1 -1
View File
@@ -1,4 +1,4 @@
import * as React from "react";
import type * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
+1 -1
View File
@@ -1,4 +1,4 @@
import * as React from "react";
import type * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
+1 -1
View File
@@ -1,4 +1,4 @@
import * as React from "react";
import type * as React from "react";
import { cn } from "@/lib/utils";
+1 -1
View File
@@ -1,4 +1,4 @@
import * as React from "react";
import type * as React from "react";
import { cn } from "@/lib/utils";
+1 -1
View File
@@ -1,6 +1,6 @@
"use client";
import * as React from "react";
import type * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cn } from "@/lib/utils";
+1 -1
View File
@@ -1,6 +1,6 @@
"use client";
import * as React from "react";
import type * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
+1 -1
View File
@@ -1,6 +1,6 @@
"use client";
import * as React from "react";
import type * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
+1
View File
@@ -52,6 +52,7 @@ function Slider({
{Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
// biome-ignore lint/suspicious/noArrayIndexKey: used for slider
key={index}
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/>
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+1 -1
View File
@@ -1,6 +1,6 @@
"use server";
import fs from "fs/promises";
import fs from "node:fs/promises";
export async function readFileBlob(path: string): Promise<Blob> {
const content = await fs.readFile(path);
+15 -11
View File
@@ -7,10 +7,6 @@ type ReportContent = {
reportTitle: string | null;
};
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY!,
});
const QueryApprove = z.object({
isNewsRelatedQuery: z.boolean(),
enhancedQuery: z.string(),
@@ -21,7 +17,17 @@ const Report = z.object({
reportContent: z.string(),
});
function getLLM() {
if (!process.env.OPENAI_API_KEY) {
throw new Error("OPENAI_API_KEY is not set in the environment variables");
}
return new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
}
export async function webSearch(textInput: string): Promise<string> {
const client = getLLM();
const response = await client.responses.create({
model: "gpt-4.1",
tools: [{ type: "web_search" }],
@@ -32,6 +38,7 @@ export async function webSearch(textInput: string): Promise<string> {
}
export async function evaluateQueryAndEnhance(text: string): Promise<string> {
const client = getLLM();
const response = await client.responses.parse({
model: "gpt-4.1",
input: [
@@ -40,7 +47,7 @@ export async function evaluateQueryAndEnhance(text: string): Promise<string> {
content:
"Please evaluate the query by the user, identifying whether or not it is related to searching the news, and, if so, produce an enhanced query. If the user's query is not related to news, leave the enhanced query simply as an empty string.",
},
{ role: "user", content: "Evaluate the following query: '" + text + "'" },
{ role: "user", content: `Evaluate the following query: '${text}'` },
],
text: {
format: zodTextFormat(QueryApprove, "query_approve"),
@@ -62,6 +69,7 @@ export async function evaluateQueryAndEnhance(text: string): Promise<string> {
export async function createReport(
webSearchText: string,
): Promise<ReportContent> {
const client = getLLM();
const response = await client.responses.parse({
model: "gpt-4.1",
input: [
@@ -72,7 +80,7 @@ export async function createReport(
},
{
role: "user",
content: "Evaluate the following query: '" + webSearchText + "'",
content: `Evaluate the following query: '${webSearchText}'`,
},
],
text: {
@@ -83,11 +91,7 @@ export async function createReport(
const generatedReport = response.output_parsed;
if (generatedReport) {
return {
reportContent:
"# " +
generatedReport.reportTitle +
"\n\n" +
generatedReport.reportContent,
reportContent: `# ${generatedReport.reportTitle}\n\n${generatedReport.reportContent}`,
reportTitle: generatedReport.reportTitle,
} as ReportContent;
} else {
+144
View File
@@ -0,0 +1,144 @@
# Node.js Workflow Demos
This directory contains various Node.js examples demonstrating different aspects of the LlamaIndex Workflows TS engine.
## Overview
The demos showcase:
- Basic workflow patterns with parallel processing
- AI agent workflows with tool calling
- File parsing workflows with state management
- Human-in-the-loop interactions
- RxJS integration for reactive programming
- MCP (Model Context Protocol) server integration
## Files
- **`basic.ts`** - Basic workflow with parallel branch processing
- **`tool-call-agent.ts`** - AI agent with tool calling capabilities
- **`llama-parse-workflow.ts`** - File parsing using LlamaIndex
- **`name-ask-readline.ts`** - Human-in-the-loop with readline interface
- **`file-parse-promise.ts`** - File parsing with promise-based approach
- **`file-parse-rxjs.ts`** - File parsing with RxJS reactive streams
- **`mcp-file-parse-tool.ts`** - MCP server with file parsing workflow
## How it works
### Basic Workflow (`basic.ts`)
1. **Parallel Processing** - Emits multiple events simultaneously
2. **Branch Handling** - Each branch processes independently
3. **Result Aggregation** - Collects results from all branches
4. **Stream Processing** - Uses Node.js streams for event handling
### AI Agent Workflow (`tool-call-agent.ts`)
1. **Tool Calling** - AI can call external tools (weather API)
2. **Workflow Execution** - Uses `runWorkflow` helper for simple execution
3. **Response Handling** - Returns AI-generated responses
### File Parsing (`file-parse-*.ts`)
1. **Directory Scanning** - Recursively processes files
2. **State Management** - Tracks parsing progress and results
3. **Multiple Approaches** - Promise-based and RxJS implementations
### Human-in-the-Loop (`name-ask-readline.ts`)
1. **Interactive Input** - Uses readline for user interaction
2. **Event Handling** - Listens for human request events
3. **Workflow Resumption** - Continues after human input
## Usage
### Prerequisites
- Node.js 18+
- npm (or pnpm)
- OpenAI API key (for AI workflows)
- LlamaIndex API key (for file parsing)
### Basic Examples
```bash
# Basic parallel workflow
npx tsx basic.ts
# AI agent with tool calling
export OPENAI_API_KEY=your-key
npx tsx tool-call-agent.ts
# File parsing workflow
export LLAMA_CLOUD_API=your-key
npx tsx llama-parse-workflow.ts path/to/file.pdf
# Human-in-the-loop interaction
npx tsx name-ask-readline.ts
```
### Advanced Examples
```bash
# File parsing with promises
npx tsx file-parse-promise.ts
# File parsing with RxJS
npx tsx file-parse-rxjs.ts
# MCP server
npx tsx mcp-file-parse-tool.ts
```
## Workflow Patterns
### Basic Parallel Processing
```typescript
const workflow = createWorkflow();
workflow.handle([startEvent], async () => {
const { sendEvent, stream } = getContext();
sendEvent(branchAEvent.with("Branch A"));
sendEvent(branchBEvent.with("Branch B"));
sendEvent(branchCEvent.with("Branch C"));
const results = await stream.filter(branchCompleteEvent).take(3).toArray();
return allCompleteEvent.with(results.map((e) => e.data).join(", "));
});
```
### AI Agent with Tools
```typescript
import { runWorkflow } from "@llamaindex/workflow-core/stream/run";
runWorkflow(
toolCallWorkflow,
startEvent.with("what is weather today"),
stopEvent,
).then(({ data }) => {
console.log("AI response", data);
});
```
### Human-in-the-Loop
```typescript
stream.on(humanRequestEvent, async (event) => {
const name = await input({
message: JSON.parse(event.data).message,
});
sendEvent(humanInteractionEvent.with(name));
});
```
## Dependencies
- **`@llamaindex/workflow-core`** - Core workflow engine
- **`rxjs`** - Reactive programming (file-parse-rxjs.ts)
- **`@inquirer/prompts`** - Interactive prompts (name-ask-readline.ts)
- **`@modelcontextprotocol/sdk`** - MCP server (mcp-file-parse-tool.ts)
- **`zod`** - Schema validation (mcp-file-parse-tool.ts)
## Key Features Demonstrated
- **Parallel Processing** - Multiple workflow branches
- **AI Integration** - OpenAI tool calling
- **State Management** - Workflow state tracking
- **Human Interaction** - Interactive workflows
- **Reactive Programming** - RxJS integration
- **Stream Processing** - Node.js stream handling
- **MCP Integration** - Model Context Protocol server
- **File Processing** - Document parsing workflows
+4 -5
View File
@@ -4,7 +4,6 @@ import {
getContext,
workflowEvent,
} from "@llamaindex/workflow-core";
import { collect } from "@llamaindex/workflow-core/stream/consumer";
//#region define workflow events
const startEvent = workflowEvent<string>();
@@ -30,19 +29,19 @@ workflow.handle([startEvent], async () => {
return allCompleteEvent.with(results.map((e) => e.data).join(", "));
});
workflow.handle([branchAEvent], (context, branchA) => {
workflow.handle([branchAEvent], (_context, branchA) => {
return branchCompleteEvent.with(branchA.data);
});
workflow.handle([branchBEvent], (context, branchB) => {
workflow.handle([branchBEvent], (_context, branchB) => {
return branchCompleteEvent.with(branchB.data);
});
workflow.handle([branchCEvent], (context, branchC) => {
workflow.handle([branchCEvent], (_context, branchC) => {
return branchCompleteEvent.with(branchC.data);
});
workflow.handle([allCompleteEvent], (context, allComplete) => {
workflow.handle([allCompleteEvent], (_context, allComplete) => {
return stopEvent.with(allComplete.data);
});
+1 -1
View File
@@ -2,7 +2,7 @@ import {
fileParseWorkflow,
startEvent,
stopEvent,
} from "../workflows/file-parse-agent.js";
} from "./workflows/file-parse-agent.js";
const directory = "..";
+1 -1
View File
@@ -5,7 +5,7 @@ import {
fileParseWorkflow,
messageEvent,
startEvent,
} from "../workflows/file-parse-agent.js";
} from "./workflows/file-parse-agent.js";
const directory = "..";
+2 -2
View File
@@ -3,13 +3,13 @@ import {
llamaParseWorkflow,
startEvent,
stopEvent,
} from "../workflows/llama-parse-workflow.js";
} from "./workflows/llama-parse-workflow.js";
runWorkflow(
llamaParseWorkflow,
startEvent.with({
inputFile: process.argv[2],
apiKey: process.env.LLAMA_CLOUD_API!,
apiKey: process.env.LLAMA_CLOUD_API,
}),
stopEvent,
).then(({ data }) => {
+2 -2
View File
@@ -3,7 +3,7 @@ import { mcpTool } from "@llamaindex/workflow-core/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { fileParseWorkflow } from "../workflows/file-parse-agent.js";
import { fileParseWorkflow } from "./workflows/file-parse-agent.js";
const server = new McpServer({
name: "Demo",
@@ -21,7 +21,7 @@ const wrappedWorkflow = createWorkflow();
wrappedWorkflow.handle(
[startEvent],
async (context, { data: { filePath } }) => {
async (_context, { data: { filePath } }) => {
const { stream, sendEvent, state } = fileParseWorkflow.createContext();
sendEvent(startEvent.with({ filePath }));
await stream.until(stopEvent).toArray();
+1 -1
View File
@@ -5,7 +5,7 @@ import {
startEvent,
stopEvent,
workflow,
} from "../workflows/human-in-the-loop";
} from "./workflows/human-in-the-loop";
const name = await input({
message: "What is your name?",
+27
View File
@@ -0,0 +1,27 @@
{
"name": "demo-node",
"version": "0.0.1",
"type": "module",
"private": true,
"scripts": {
"basic": "tsx basic.ts",
"tool-call": "tsx tool-call-agent.ts",
"llama-parse": "tsx llama-parse-workflow.ts",
"name-ask": "tsx name-ask-readline.ts",
"file-parse-promise": "tsx file-parse-promise.ts",
"file-parse-rxjs": "tsx file-parse-rxjs.ts",
"mcp-server": "tsx mcp-file-parse-tool.ts"
},
"dependencies": {
"@llamaindex/workflow-core": "^1.3.3",
"@inquirer/prompts": "^7.8.6",
"@modelcontextprotocol/sdk": "^1.18.0",
"openai": "^4.0.0",
"rxjs": "^7.8.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/node": "^24.0.4",
"tsx": "^4.20.3"
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import {
startEvent,
stopEvent,
toolCallWorkflow,
} from "../workflows/tool-call-agent.js";
} from "./workflows/tool-call-agent.js";
runWorkflow(
toolCallWorkflow,
@@ -24,7 +24,7 @@ export const stopEvent = workflowEvent({
debugLabel: "stop",
});
const { withState, getContext } = createStatefulMiddleware(() => ({
const { withState } = createStatefulMiddleware(() => ({
output: "",
apiKey: "",
}));
@@ -53,7 +53,7 @@ fileParseWorkflow.handle(
context.sendEvent(messageEvent.with(dir));
const { sendEvent } = context;
const items = await readdir(dir);
context.state.output += " ".repeat(tab) + dir + "\n";
context.state.output += `${" ".repeat(tab)}${dir}\n`;
await Promise.all(
items.map(async (item) => {
const filePath = resolve(dir, item);
@@ -93,7 +93,7 @@ fileParseWorkflow.handle(
lock.finish = true;
}
context.sendEvent(messageEvent.with(filePath));
context.state.output += " ".repeat(tab) + filePath + "\n";
context.state.output += `${" ".repeat(tab)}${filePath}\n`;
return readResultEvent.with();
},
);
+85
View File
@@ -0,0 +1,85 @@
import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core";
import { createStatefulMiddleware } from "@llamaindex/workflow-core/middleware/state";
import { OpenAI } from "openai";
const openai = new OpenAI();
const { withState } = createStatefulMiddleware();
const workflow = withState(createWorkflow());
const startEvent = workflowEvent<string>({
debugLabel: "start",
});
const humanInteractionEvent = workflowEvent<string>({
debugLabel: "humanInteraction",
});
const humanRequestEvent = workflowEvent<string>({
debugLabel: "humanRequest",
});
const stopEvent = workflowEvent<string>({
debugLabel: "stop",
});
workflow.handle([startEvent], async (context, { data }) => {
const response = await openai.chat.completions.create({
stream: false,
model: "gpt-4.1",
messages: [
{
role: "system",
content: `You are a helpful assistant.
If user doesn't provide his/her name, call ask_name tool to ask for user's name.
Otherwise, analyze user's name with a good meaning and return the analysis.
For example, alex is from "Alexander the Great", who was a king of the ancient Greek kingdom of Macedon and one of history's greatest military minds.`,
},
{
role: "user",
content: data,
},
],
tools: [
{
type: "function",
function: {
name: "ask_name",
description: "Ask for user's name",
parameters: {
type: "object",
properties: {
message: {
type: "string",
description: "The message to ask for user's name",
},
},
required: ["message"],
},
},
},
],
});
const tools = response.choices[0].message.tool_calls;
if (tools && tools.length > 0) {
const askName = tools.find((tool) => tool.function.name === "ask_name");
if (askName) {
return context.sendEvent(
humanRequestEvent.with(askName.function.arguments),
);
}
}
return stopEvent.with(response.choices[0].message.content ?? "");
});
workflow.handle([humanInteractionEvent], async (context, { data }) => {
const { sendEvent } = context;
// going back to the start event
sendEvent(startEvent.with(data));
});
export {
workflow,
startEvent,
humanInteractionEvent,
humanRequestEvent,
stopEvent,
};
@@ -95,7 +95,7 @@ const context = llamaParseWorkflow.createContext();
context.sendEvent(
startEvent.with({
inputFile: "sample.pdf",
apiKey: process.env.LLAMA_CLOUD_API_KEY!,
apiKey: process.env.LLAMA_CLOUD_API_KEY,
}),
);
+83
View File
@@ -0,0 +1,83 @@
import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core";
import { OpenAI } from "openai";
import type {
ChatCompletionMessageToolCall,
ChatCompletionTool,
} from "openai/resources/chat/completions/completions";
const llm = new OpenAI();
const tools = [
{
function: {
name: "get_weather",
description: "Get Weather Weather",
parameters: {
type: "object",
properties: {
location: {
type: "string",
description: "City and country e.g. Bogotá, Colombia",
},
},
required: ["location"],
},
},
type: "function",
},
] satisfies ChatCompletionTool[];
export const startEvent = workflowEvent<string>();
const chatEvent = workflowEvent<string>();
const toolCallEvent = workflowEvent<ChatCompletionMessageToolCall>();
const toolCallResultEvent = workflowEvent<string>();
export const stopEvent = workflowEvent<string>();
export const toolCallWorkflow = createWorkflow();
toolCallWorkflow.handle([startEvent], async (context, { data }) => {
console.log("start event");
context.sendEvent(chatEvent.with(data));
});
toolCallWorkflow.handle([toolCallEvent], async () => {
console.log("tool call event");
return toolCallResultEvent.with("Today is sunny.");
});
toolCallWorkflow.handle([chatEvent], async (context, { data }) => {
console.log("chat event");
const { choices } = await llm.chat.completions.create({
model: "gpt-4-turbo",
tools,
messages: [
{
role: "system",
content: "You are a helpful assistant.",
},
{
role: "user",
content: data,
},
],
});
const { sendEvent, stream } = context;
if (
choices[0]?.message?.tool_calls?.length &&
choices[0].message.tool_calls.length > 0
) {
console.log("sending choices", choices[0].message.tool_calls);
const result = (
await Promise.all(
choices[0].message.tool_calls.map(async (tool_call) => {
sendEvent(toolCallEvent.with(tool_call));
return stream.until(toolCallResultEvent).toArray();
}),
)
)
.map((list) => list.at(-1))
.filter((event) => event !== undefined)
.map(({ data }) => data)
.join("\n");
console.log("toolcall result", result);
sendEvent(chatEvent.with(result));
} else {
console.log("no choices");
return stopEvent.with(choices[0]?.message.content ?? "");
}
});
-20
View File
@@ -1,20 +0,0 @@
{
"name": "demo",
"type": "module",
"private": true,
"dependencies": {
"@hono/node-server": "^1.14.4",
"@inquirer/prompts": "^7.5.3",
"@llamaindex/workflow-core": "latest",
"@modelcontextprotocol/sdk": "^1.13.1",
"hono": "^4.8.3",
"openai": "^5.7.0",
"p-retry": "^6.2.1",
"rxjs": "^7.8.2",
"zod": "^3.25.67"
},
"devDependencies": {
"@types/node": "^24.0.4",
"tsx": "^4.20.3"
}
}
+7 -3
View File
@@ -1,10 +1,14 @@
{
"name": "trace-events-demo",
"name": "demo-trace-events",
"version": "0.0.1",
"type": "module",
"private": true,
"scripts": {
"dev": "tsx open-telemetry.ts"
},
"dependencies": {
"@llamaindex/workflow-core": "^1.3.1",
"@llamaindex/workflow-otel": "^1.0.1",
"@llamaindex/workflow-core": "^1.3.3",
"@llamaindex/workflow-otel": "^1.0.4",
"openai": "^5.7.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/sdk-node": "^0.203.0",
+19
View File
@@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": ".",
"declaration": true,
"sourceMap": true
},
"include": ["*.ts"],
"exclude": ["node_modules", "dist"]
}
-23
View File
@@ -1,23 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "bundler",
"outDir": "./lib",
"tsBuildInfoFile": "./lib/.tsbuildinfo",
"lib": ["DOM", "DOM.AsyncIterable", "DOM.Iterable", "esnext"],
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"include": ["hono", "node", "workflows", "express/4-adding-hitl.ts"],
"references": [
{
"path": "./browser/tsconfig.json"
},
{
"path": "./cloudflare/tsconfig.json"
}
]
}
+4 -4
View File
@@ -1,16 +1,16 @@
{
"name": "demo-viz",
"private": true,
"name": "demo-visualization",
"version": "0.0.1",
"type": "module",
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@llamaindex/workflow-core": "^1.3.1",
"@llamaindex/workflow-viz": "^1.0.1",
"@llamaindex/workflow-core": "^1.3.3",
"@llamaindex/workflow-viz": "^1.0.4",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
-8
View File
@@ -1,8 +0,0 @@
node_modules
dist
.env*
*.tsbuildinfo
.cache
.DS_Store
*.pem
src/pages.gen.ts
-18
View File
@@ -1,18 +0,0 @@
import { defaultPlugins, defineConfig } from "@hey-api/openapi-ts";
export default defineConfig({
input: "https://api.cloud.llamaindex.ai/api/openapi.json",
output: "src/lib/api",
plugins: [
...defaultPlugins,
"@hey-api/client-fetch",
"zod",
"@hey-api/schemas",
"@hey-api/sdk",
{
enums: "javascript",
identifierCase: "PascalCase",
name: "@hey-api/typescript",
},
],
});
-34
View File
@@ -1,34 +0,0 @@
{
"name": "waku-project",
"version": "0.0.0",
"type": "module",
"private": true,
"scripts": {
"postinstall": "openapi-ts",
"dev": "waku dev",
"build": "waku build",
"start": "waku start"
},
"dependencies": {
"@llamaindex/workflow-core": "latest",
"@llamaindex/workflow-http": "latest",
"@neondatabase/serverless": "^1.0.1",
"lucide-react": "^0.523.0",
"openai": "^5.7.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-server-dom-webpack": "19.1.0",
"stable-hash": "^0.0.6",
"waku": "0.23.2"
},
"devDependencies": {
"@hey-api/client-fetch": "^0.13.1",
"@hey-api/openapi-ts": "^0.77.0",
"@tailwindcss/postcss": "4.1.10",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"postcss": "8.5.6",
"tailwindcss": "4.1.10",
"typescript": "5.8.3"
}
}
-5
View File
@@ -1,5 +0,0 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

-77
View File
@@ -1,77 +0,0 @@
"use client";
import { createClient } from "@llamaindex/workflow-http/client";
import { useCallback, useState } from "react";
import * as events from "../workflow/events";
const { fetch } = createClient("/api/store", events);
export const RAG = () => {
const [list, setList] = useState<any[]>([]);
const [file, setFile] = useState<File | null>(null);
const handleFileInput = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const selectedFiles = Array.from(e.target.files);
setFile(selectedFiles[0]!);
}
},
[],
);
return (
<section className="border-blue-400 -mx-4 mt-4 rounded-sm border p-4">
<div>
<input type="file" onChange={handleFileInput} id="file-upload" />
<button
onClick={async () => {
fetch({
file,
}).then((stream) => {
stream.forEach((event) => {
console.log(event);
if (event.data) {
setList((prev) => [...prev, `${event.data}`]);
}
});
});
}}
>
Run
</button>
</div>
<form
action={(form) => {
const search = form.get("search") as string;
fetch({
search,
}).then((stream) => {
stream.forEach((event) => {
console.log(event);
if (event.data) {
setList((prev) => [...prev, `${event.data}`]);
}
});
});
}}
>
<input
type="text"
name="search"
className="border-gray-400 mt-4 w-full rounded-sm border p-2"
placeholder="Search something..."
/>
<button type="submit" />
</form>
{list.map((item, index) => (
<div key={index} className="flex items-center gap-2">
<div className="text-sm max-h-12 max-w-64 overflow-scroll">
{item}
</div>
</div>
))}
</section>
);
};
-18
View File
@@ -1,18 +0,0 @@
export const Footer = () => {
return (
<footer className="p-6 lg:fixed lg:bottom-0 lg:left-0">
<div>
visit{" "}
<a
href="https://waku.gg/"
target="_blank"
rel="noreferrer"
className="mt-4 inline-block underline"
>
waku.gg
</a>{" "}
to learn more
</div>
</footer>
);
};
-11
View File
@@ -1,11 +0,0 @@
import { Link } from "waku";
export const Header = () => {
return (
<header className="flex items-center gap-4 p-6 lg:fixed lg:left-0 lg:top-0">
<h2 className="text-lg font-bold tracking-tight">
<Link to="/">Waku starter</Link>
</h2>
</header>
);
};
-38
View File
@@ -1,38 +0,0 @@
import "../styles.css";
import type { ReactNode } from "react";
import { Footer } from "../components/footer";
import { Header } from "../components/header";
type RootLayoutProps = { children: ReactNode };
export default async function RootLayout({ children }: RootLayoutProps) {
const data = await getData();
return (
<div className="font-['Nunito']">
<meta name="description" content={data.description} />
<link rel="icon" type="image/png" href={data.icon} />
<Header />
<main className="m-6 flex items-center *:min-h-64 *:min-w-64 lg:m-0 lg:min-h-svh lg:justify-center">
{children}
</main>
<Footer />
</div>
);
}
const getData = async () => {
const data = {
description: "An internet website!",
icon: "/images/favicon.png",
};
return data;
};
export const getConfig = async () => {
return {
render: "static",
} as const;
};
-32
View File
@@ -1,32 +0,0 @@
import { Link } from "waku";
export default async function AboutPage() {
const data = await getData();
return (
<div>
<title>{data.title}</title>
<h1 className="text-4xl font-bold tracking-tight">{data.headline}</h1>
<p>{data.body}</p>
<Link to="/" className="mt-4 inline-block underline">
Return home
</Link>
</div>
);
}
const getData = async () => {
const data = {
title: "About",
headline: "About Waku",
body: "The minimal React framework",
};
return data;
};
export const getConfig = async () => {
return {
render: "static",
} as const;
};
-26
View File
@@ -1,26 +0,0 @@
import { createServer } from "@llamaindex/workflow-http/server";
import { workflow } from "../../workflow/basic";
import { searchEvent, stopEvent, storeEvent } from "../../workflow/events";
import { upload } from "../../workflow/llama-parse";
process.on("unhandledRejection", (reason) => {
console.error("Unhandled Rejection at:", reason);
});
export const POST = createServer(
workflow,
async (data, sendEvent) => {
if (data.file) {
const file = data.file;
const job = await upload({
file,
});
const text = await job.markdown();
sendEvent(storeEvent.with(text));
} else if (data.search) {
const search = data.search;
sendEvent(searchEvent.with(search));
}
},
(stream) => stream.until(stopEvent),
);
-30
View File
@@ -1,30 +0,0 @@
import { RAG } from "../components/RAG";
export default async function HomePage() {
const data = await getData();
return (
<div>
<title>{data.title}</title>
<h1 className="text-4xl font-bold tracking-tight">{data.headline}</h1>
<p>{data.body}</p>
<RAG />
</div>
);
}
const getData = async () => {
const data = {
title: "Waku",
headline: "Waku",
body: "Hello world!",
};
return data;
};
export const getConfig = async () => {
return {
render: "static",
} as const;
};
-130
View File
@@ -1,130 +0,0 @@
import { z } from "zod";
import { FailPageMode, ParserLanguages, ParsingMode } from "../lib/api";
type Language = (typeof ParserLanguages)[keyof typeof ParserLanguages];
const VALUES: [Language, ...Language[]] = [
ParserLanguages.EN,
...Object.values(ParserLanguages),
];
const languageSchema = z.enum(VALUES);
const PARSE_PRESETS = [
"fast",
"balanced",
"premium",
"structured",
"auto",
"scientific",
"invoice",
"slides",
"_carlyle",
] as const;
export const parsePresetSchema = z.enum(PARSE_PRESETS);
export const parseFormSchema = z.object({
adaptive_long_table: z.boolean().optional(),
annotate_links: z.boolean().optional(),
auto_mode: z.boolean().optional(),
auto_mode_trigger_on_image_in_page: z.boolean().optional(),
auto_mode_trigger_on_table_in_page: z.boolean().optional(),
auto_mode_trigger_on_text_in_page: z.string().optional(),
auto_mode_trigger_on_regexp_in_page: z.string().optional(),
auto_mode_configuration_json: z.string().optional(),
azure_openai_api_version: z.string().optional(),
azure_openai_deployment_name: z.string().optional(),
azure_openai_endpoint: z.string().optional(),
azure_openai_key: z.string().optional(),
bbox_bottom: z.number().min(0).max(1).optional(),
bbox_left: z.number().min(0).max(1).optional(),
bbox_right: z.number().min(0).max(1).optional(),
bbox_top: z.number().min(0).max(1).optional(),
disable_ocr: z.boolean().optional(),
disable_reconstruction: z.boolean().optional(),
disable_image_extraction: z.boolean().optional(),
do_not_cache: z.coerce.boolean().optional(),
do_not_unroll_columns: z.coerce.boolean().optional(),
extract_charts: z.boolean().optional(),
guess_xlsx_sheet_name: z.boolean().optional(),
html_make_all_elements_visible: z.boolean().optional(),
html_remove_fixed_elements: z.boolean().optional(),
html_remove_navigation_elements: z.boolean().optional(),
http_proxy: z
.string()
.url(
'Set a valid URL for the HTTP proxy, e.g., "http://proxy.example.com:8080"',
)
.refine(
(url) => {
try {
const parsedUrl = new URL(url);
return (
parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:"
);
} catch {
return false;
}
},
{
message: "Invalid HTTP proxy URL",
},
)
.optional(),
input_s3_path: z.string().optional(),
input_s3_region: z.string().optional(),
input_url: z.string().optional(),
invalidate_cache: z.boolean().optional(),
language: z.array(languageSchema).optional(),
extract_layout: z.boolean().optional(),
max_pages: z.number().nullable().optional(),
output_pdf_of_document: z.boolean().optional(),
output_s3_path_prefix: z.string().optional(),
output_s3_region: z.string().optional(),
page_prefix: z.string().optional(),
page_separator: z.string().optional(),
page_suffix: z.string().optional(),
preserve_layout_alignment_across_pages: z.boolean().optional(),
skip_diagonal_text: z.boolean().optional(),
spreadsheet_extract_sub_tables: z.boolean().optional(),
structured_output: z.boolean().optional(),
structured_output_json_schema: z.string().optional(),
structured_output_json_schema_name: z.string().optional(),
take_screenshot: z.boolean().optional(),
target_pages: z.string().optional(),
vendor_multimodal_api_key: z.string().optional(),
vendor_multimodal_model_name: z.string().optional(),
model: z.string().optional(),
webhook_url: z.string().url().optional(),
parse_mode: z.nativeEnum(ParsingMode).nullable().optional(),
system_prompt: z.string().optional(),
system_prompt_append: z.string().optional(),
user_prompt: z.string().optional(),
job_timeout_in_seconds: z.number().optional(),
job_timeout_extra_time_per_page_in_seconds: z.number().optional(),
strict_mode_image_extraction: z.boolean().optional(),
strict_mode_image_ocr: z.boolean().optional(),
strict_mode_reconstruction: z.boolean().optional(),
strict_mode_buggy_font: z.boolean().optional(),
save_images: z.boolean().optional(),
ignore_document_elements_for_layout_detection: z.boolean().optional(),
output_tables_as_HTML: z.boolean().optional(),
use_vendor_multimodal_model: z.boolean().optional(),
bounding_box: z.string().optional(),
gpt4o_mode: z.boolean().optional(),
gpt4o_api_key: z.string().optional(),
complemental_formatting_instruction: z.string().optional(),
content_guideline_instruction: z.string().optional(),
premium_mode: z.boolean().optional(),
is_formatting_instruction: z.boolean().optional(),
continuous_mode: z.boolean().optional(),
parsing_instruction: z.string().optional(),
fast_mode: z.boolean().optional(),
formatting_instruction: z.string().optional(),
preset: parsePresetSchema.optional(),
compact_markdown_table: z.boolean().optional(),
markdown_table_multiline_header_separator: z.string().optional(),
page_error_tolerance: z.number().min(0).max(1).optional(),
replace_failed_page_mode: z.nativeEnum(FailPageMode).nullable().optional(),
replace_failed_page_with_error_message_prefix: z.string().optional(),
replace_failed_page_with_error_message_suffix: z.string().optional(),
});
-3
View File
@@ -1,3 +0,0 @@
@import url("https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,400;0,700;1,400;1,700&display=swap")
layer(base);
@import "tailwindcss";
-44
View File
@@ -1,44 +0,0 @@
import { createWorkflow, getContext } from "@llamaindex/workflow-core";
import { neon } from "@neondatabase/serverless";
import { OpenAI } from "openai";
import { getEnv } from "waku";
import { searchEvent, stopEvent, storeEvent } from "./events";
const openai = new OpenAI({
apiKey: getEnv("OPENAI_API_KEY")!,
});
const sql = neon(getEnv("DATABASE_URL")!);
export const workflow = createWorkflow();
workflow.handle([storeEvent], async (context, { data }) => {
const embeddingResponse = await openai.embeddings.create({
model: "text-embedding-ada-002",
input: data,
});
const embedding = embeddingResponse.data[0]!.embedding;
await sql`
INSERT INTO text_vectors (text, embedding)
VALUES (${data}, ${JSON.stringify(embedding)})
`;
return stopEvent.with("success");
});
workflow.handle([searchEvent], async (context, { data }) => {
const { signal } = context;
signal.addEventListener("abort", () => {
console.error("error", signal.reason);
});
const embeddingResponse = await openai.embeddings.create({
model: "text-embedding-ada-002",
input: data,
});
const embedding = embeddingResponse.data[0]!.embedding;
const result = await sql`
SELECT text
FROM text_vectors
ORDER BY embedding <=> ${JSON.stringify(embedding)}
LIMIT 5
`;
return stopEvent.with(result.map((row) => row.text).join("\n"));
});
-68
View File
@@ -1,68 +0,0 @@
import { workflowEvent } from "@llamaindex/workflow-core";
import { zodEvent } from "@llamaindex/workflow-core/util/zod";
import { z } from "zod";
import { parseFormSchema } from "../schema";
export const searchEvent = workflowEvent<string>({
debugLabel: "search",
uniqueId: "search",
});
export const storeEvent = workflowEvent<string>({
debugLabel: "store",
uniqueId: "store",
});
export const stopEvent = workflowEvent<string>({
debugLabel: "stop",
uniqueId: "stop",
});
export const startEvent = zodEvent(
parseFormSchema.merge(
z.object({
file: z
.string()
.or(z.instanceof(File))
.or(z.instanceof(Blob))
.or(z.instanceof(Uint8Array))
.optional()
.describe("input"),
}),
),
{
debugLabel: "llama-parse",
uniqueId: "llama-parse",
},
);
export const checkStatusEvent = workflowEvent<string>({
debugLabel: "check-status",
uniqueId: "check-status",
});
export const checkStatusSuccessEvent = workflowEvent<string>({
debugLabel: "check-status-success",
uniqueId: "check-status-success",
});
export const requestMarkdownEvent = workflowEvent<string>({
debugLabel: "markdown-request",
uniqueId: "markdown-request",
});
export const requestTextEvent = workflowEvent<string>({
debugLabel: "text-request",
uniqueId: "text-request",
});
export const requestJsonEvent = workflowEvent<string>({
debugLabel: "json-request",
uniqueId: "json-request",
});
export const markdownResultEvent = workflowEvent<string>({
debugLabel: "markdown-result",
uniqueId: "markdown-result",
});
export const textResultEvent = workflowEvent<string>({
debugLabel: "text-result",
uniqueId: "text-result",
});
export const jsonResultEvent = workflowEvent<unknown>({
debugLabel: "json-result",
uniqueId: "json-result",
});
-258
View File
@@ -1,258 +0,0 @@
import fs from "node:fs/promises";
import path from "node:path";
import { createClient, createConfig } from "@hey-api/client-fetch";
import {
createWorkflow,
type InferWorkflowEventData,
workflowEvent,
} from "@llamaindex/workflow-core";
import { createStatefulMiddleware } from "@llamaindex/workflow-core/middleware/state";
import { withTraceEvents } from "@llamaindex/workflow-core/middleware/trace-events";
import { pRetryHandler } from "@llamaindex/workflow-core/util/p-retry";
import { zodEvent } from "@llamaindex/workflow-core/util/zod";
import hash from "stable-hash";
import { getEnv } from "waku";
import { z } from "zod";
import {
type BodyUploadFileApiV1ParsingUploadPost,
getJobApiV1ParsingJobJobIdGet,
getJobJsonResultApiV1ParsingJobJobIdResultJsonGet,
getJobResultApiV1ParsingJobJobIdResultMarkdownGet,
getJobTextResultApiV1ParsingJobJobIdResultTextGet,
type StatusEnum,
uploadFileApiV1ParsingUploadPost,
} from "../lib/api";
import { parseFormSchema } from "../schema";
import {
checkStatusEvent,
checkStatusSuccessEvent,
jsonResultEvent,
markdownResultEvent,
requestJsonEvent,
requestMarkdownEvent,
requestTextEvent,
startEvent,
textResultEvent,
} from "./events";
export type LlamaParseWorkflowParams = {
region?: "us" | "eu" | "us-staging";
apiKey?: string;
};
const URLS = {
us: "https://api.cloud.llamaindex.ai",
eu: "https://api.cloud.eu.llamaindex.ai",
"us-staging": "https://api.staging.llamaindex.ai",
} as const;
const { withState } = createStatefulMiddleware(
(params: LlamaParseWorkflowParams) => {
const apiKey = params.apiKey ?? getEnv("LLAMA_CLOUD_API_KEY");
const region = params.region ?? "us";
if (!apiKey) {
throw new Error("LLAMA_CLOUD_API_KEY is not set");
}
return {
cache: {} as Record<string, StatusEnum>,
client: createClient(
createConfig({
baseUrl: URLS[region],
headers: {
Authorization: `Bearer ${apiKey}`,
},
}),
),
};
},
);
const llamaParseWorkflow = withState(withTraceEvents(createWorkflow()));
llamaParseWorkflow.handle([startEvent], async (context, { data: form }) => {
const { state } = context;
const finalForm = { ...form };
if ("file" in form) {
// support loads from the file system
const file = form?.file;
const isFilePath = typeof file === "string";
const data = isFilePath ? await fs.readFile(file) : file;
const filename: string | undefined = isFilePath
? path.basename(file)
: undefined;
finalForm.file = data
? globalThis.File && filename
? new File([data], filename)
: new Blob([data])
: undefined;
}
const {
data: { id, status },
} = await uploadFileApiV1ParsingUploadPost({
throwOnError: true,
body: {
...finalForm,
} as BodyUploadFileApiV1ParsingUploadPost,
client: state.client,
});
state.cache[id] = status;
return checkStatusEvent.with(id);
});
llamaParseWorkflow.handle(
[checkStatusEvent],
pRetryHandler(
async (context, { data: uuid }) => {
const { state } = context;
if (state.cache[uuid] === "SUCCESS") {
return checkStatusSuccessEvent.with(uuid);
}
const {
data: { status },
} = await getJobApiV1ParsingJobJobIdGet({
throwOnError: true,
path: {
job_id: uuid,
},
client: state.client,
});
state.cache[uuid] = status;
if (status === "SUCCESS") {
return checkStatusSuccessEvent.with(uuid);
}
throw new Error(`LLamaParse status: ${status}`);
},
{
retries: 100,
},
),
);
//#region sub workflow
llamaParseWorkflow.handle(
[requestMarkdownEvent],
async (context, { data: job_id }) => {
const { state } = context;
const { data } = await getJobResultApiV1ParsingJobJobIdResultMarkdownGet({
throwOnError: true,
path: {
job_id,
},
client: state.client,
});
return markdownResultEvent.with(data.markdown);
},
);
llamaParseWorkflow.handle(
[requestTextEvent],
async (context, { data: job_id }) => {
const { state } = context;
const { data } = await getJobTextResultApiV1ParsingJobJobIdResultTextGet({
throwOnError: true,
path: {
job_id,
},
client: state.client,
});
return textResultEvent.with(data.text);
},
);
llamaParseWorkflow.handle(
[requestJsonEvent],
async (context, { data: job_id }) => {
const { state } = context;
const { data } = await getJobJsonResultApiV1ParsingJobJobIdResultJsonGet({
throwOnError: true,
path: {
job_id,
},
client: state.client,
});
return jsonResultEvent.with(data.pages);
},
);
//#endregion
const cacheMap = new Map<
string,
ReturnType<typeof llamaParseWorkflow.createContext>
>();
export type ParseJob = {
get jobId(): string;
get signal(): AbortSignal;
get context(): ReturnType<typeof llamaParseWorkflow.createContext>;
get form(): InferWorkflowEventData<typeof startEvent>;
markdown(): Promise<string>;
text(): Promise<string>;
//eslint-disable-next-line @typescript-eslint/no-explicit-any
json(): Promise<any[]>;
};
export const upload = async (
params: InferWorkflowEventData<typeof startEvent> & LlamaParseWorkflowParams,
): Promise<ParseJob> => {
//#region cache
const key = hash({ apiKey: params.apiKey, region: params.region });
if (!cacheMap.has(key)) {
const context = llamaParseWorkflow.createContext(params);
cacheMap.set(key, context);
}
//#endregion
//#region upload event
const context = cacheMap.get(key)!;
const { stream, sendEvent } = context;
const ev = startEvent.with(params);
sendEvent(ev);
const uploadThread = await llamaParseWorkflow
.substream(ev, stream)
.until((ev) => checkStatusSuccessEvent.include(ev))
.toArray();
//#region
const jobId: string = uploadThread.at(-1)!.data;
return {
get signal() {
// lazy load
return context.signal;
},
get jobId() {
return jobId;
},
get form() {
return ev.data;
},
get context() {
return context;
},
async markdown(): Promise<string> {
const requestEv = requestMarkdownEvent.with(jobId);
const { sendEvent, stream } = llamaParseWorkflow.createContext(params);
sendEvent(requestEv);
const markdownThread = await stream.until(markdownResultEvent).toArray();
return markdownThread.at(-1)!.data;
},
async text(): Promise<string> {
const requestEv = requestTextEvent.with(jobId);
const { sendEvent, stream } = llamaParseWorkflow.createContext(params);
sendEvent(requestEv);
const textThread = await stream.until(textResultEvent).toArray();
console.log("textThread", textThread);
return textThread.at(-1)!.data;
},
//eslint-disable-next-line @typescript-eslint/no-explicit-any
async json(): Promise<any[]> {
const requestEv = requestJsonEvent.with(jobId);
const { sendEvent, stream } = llamaParseWorkflow.createContext(params);
sendEvent(requestEv);
const jsonThread = await stream
.until((ev) => jsonResultEvent.include(ev))
.toArray();
return jsonThread.at(-1)!.data;
},
};
};
-16
View File
@@ -1,16 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"strict": true,
"target": "esnext",
"downlevelIteration": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"jsx": "react-jsx"
},
"include": ["./src"]
}
+7 -7
View File
@@ -6,7 +6,7 @@ const processStringEvent = workflowEvent<string>();
const processNumberEvent = workflowEvent<number>();
export const successEvent = workflowEvent<string>();
workflow.handle([inputEvent], async (context, event) => {
workflow.handle([inputEvent], async (_context, event) => {
if (typeof event.data === "string") {
return processStringEvent.with(event.data);
} else {
@@ -14,22 +14,22 @@ workflow.handle([inputEvent], async (context, event) => {
}
});
workflow.handle([processStringEvent], async (context, event) => {
workflow.handle([processStringEvent], async (_context, event) => {
return successEvent.with(`Processed string ${event.data}`);
});
workflow.handle([processNumberEvent], async (context, event) => {
workflow.handle([processNumberEvent], async (_context, event) => {
return successEvent.with(`Processed number ${event.data}`);
});
let context1 = workflow.createContext();
const context1 = workflow.createContext();
context1.sendEvent(inputEvent.with("I am some data"));
const result = await context1.stream.until(successEvent).toArray();
console.log(result.at(-1)!.data);
console.log(result.at(-1)?.data);
let context2 = workflow.createContext();
const context2 = workflow.createContext();
context2.sendEvent(inputEvent.with(1));
const result2 = await context2.stream.until(successEvent).toArray();
console.log(result2.at(-1)!.data);
console.log(result2.at(-1)?.data);
+1 -1
View File
@@ -12,7 +12,7 @@ const { withState } = createStatefulMiddleware(() => ({
processResults: [] as string[],
}));
export const workflow = withState(createWorkflow());
workflow.handle([startEvent], async (context, start) => {
workflow.handle([startEvent], async (context) => {
const { sendEvent, state } = context;
state.itemsProcessed = 0; // Reset counter for this execution
+1 -1
View File
@@ -15,7 +15,7 @@ workflow.handle([startEvent], () => {
return humanRequestEvent.with();
});
workflow.handle([humanResponseEvent], (context, event) => {
workflow.handle([humanResponseEvent], (_context, event) => {
return stopEvent.with(`Human said: ${event.data}`);
});
+2 -2
View File
@@ -15,7 +15,7 @@ export const startEvent = workflowEvent<void>();
const increaseCounterEvent = workflowEvent<void>();
export const stopEvent = workflowEvent<number>();
workflow.handle([startEvent], async (context, { data }) => {
workflow.handle([startEvent], async (context) => {
const { sendEvent, state } = context;
if (state.counter < state.max_counter) {
sendEvent(increaseCounterEvent.with());
@@ -24,7 +24,7 @@ workflow.handle([startEvent], async (context, { data }) => {
}
});
workflow.handle([increaseCounterEvent], async (context, { data }) => {
workflow.handle([increaseCounterEvent], async (context) => {
const { sendEvent, state } = context;
state.counter += 1;
sendEvent(startEvent.with());
+1 -1
View File
@@ -15,7 +15,7 @@ export const startEvent = workflowEvent<{ userInput: string }>();
export const stopEvent = workflowEvent<{ result: string }>();
workflow.handle([startEvent], async (context, { data }) => {
const { sendEvent, state } = context;
const { state } = context;
const { userInput } = data;
const previous_message = state.previous_message;
+1 -1
View File
@@ -37,5 +37,5 @@ workflow.handle([stepEvent], (_context, event) => {
});
// Run
const { sendEvent, stream } = workflow.createContext();
const { sendEvent } = workflow.createContext();
sendEvent(startEvent.with());
+1 -5
View File
@@ -1,8 +1,4 @@
import {
createWorkflow,
workflowEvent,
type Workflow,
} from "@llamaindex/workflow-core";
import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core";
import { withDrawing } from "@llamaindex/workflow-viz";
// Define events (debug labels are used for node names in the graph)
+2 -2
View File
@@ -3,14 +3,14 @@ import { workflow, inputEvent, successEvent } from "../src/branching";
describe("Branching workflow should return expected results", () => {
test("Sending event with context1", async () => {
let context1 = workflow.createContext();
const context1 = workflow.createContext();
context1.sendEvent(inputEvent.with("I am some data"));
const result = await context1.stream.until(successEvent).toArray();
expect(result.at(-1)!.data).toBe("Processed string I am some data");
});
test("Sending event with context2", async () => {
let context2 = workflow.createContext();
const context2 = workflow.createContext();
context2.sendEvent(inputEvent.with(1));
const result2 = await context2.stream.until(successEvent).toArray();
+2 -2
View File
@@ -5,8 +5,8 @@
"type": "module",
"packageManager": "pnpm@10.15.0",
"scripts": {
"build": "turbo build --filter=\"./packages/*\" --filter=\"./docs\"",
"test": "turbo test --filter=\"./packages/*\" --filter=\"./docs\"",
"build": "turbo build",
"test": "turbo test",
"typecheck": "tsc -b --diagnostics",
"format": "biome format .",
"format:write": "biome format --write .",
+4757 -1652
View File
File diff suppressed because it is too large Load Diff
+1 -7
View File
@@ -1,11 +1,5 @@
packages:
- ./demo
- ./demo/cloudflare
- ./demo/browser
- ./demo/waku
- ./demo/express
- ./demo/visualization
- ./demo/trace-events
- ./demo/*
- ./docs
- ./packages/*
- ./tests/*
+1 -1
View File
@@ -56,7 +56,7 @@ describe("Llama Flow Pure CJS Tests", () => {
jokeFlow = withState(createWorkflow());
// Define handlers for each step
jokeFlow.handle([startEvent], async (event: any) => {
jokeFlow.handle([startEvent], async (_context, event) => {
// Increment our manual state counter
numIterations++;
+10 -1
View File
@@ -20,11 +20,20 @@
"include": [],
"references": [
{
"path": "./demo/tsconfig.json"
"path": "./demo/browser/tsconfig.json"
},
{
"path": "./demo/cloudflare/tsconfig.json"
},
{
"path": "./demo/express/tsconfig.json"
},
{
"path": "./demo/next/tsconfig.json"
},
{
"path": "./demo/trace-events/tsconfig.json"
},
{
"path": "./demo/visualization/tsconfig.json"
},