docs: add new docs (#43)

Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
Logan
2025-04-21 03:54:56 -06:00
committed by GitHub
parent 7ce94ffa89
commit 91a994dc58
10 changed files with 1717 additions and 24 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@llama-flow/docs": patch
---
feat: update docs
+127
View File
@@ -1,5 +1,14 @@
name: Publish Preview
on: [pull_request]
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.LLAMAINDEX_VERCEL_PROJECT_ID }}
TURBO_TOKEN: ${{ secrets.VERCEL_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
pre_release:
@@ -26,3 +35,121 @@ jobs:
- name: Pre Release
run: pnpx pkg-pr-new publish --pnpm ./packages/*
pre_release_doc:
name: Pre Release Doc
runs-on: ubuntu-latest
outputs:
deployment_url: ${{ steps.deploy.outputs.deployment_url }}
steps:
- name: Checkout Repo
uses: actions/checkout@v4
with:
path: llamaflow
repository: "${{ github.repository }}"
fetch-depth: "1"
- name: Checkout LITS
uses: actions/checkout@v4
with:
path: llamaindex
repository: "run-llama/LlamaIndexTS"
fetch-depth: "1"
- uses: pnpm/action-setup@v4
with:
package_json_file: "llamaflow/package.json"
- uses: pnpm/action-setup@v4
with:
package_json_file: "llamaindex/package.json"
- name: Setup Node.js for Llamaflow
uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm"
cache-dependency-path: "llamaflow/pnpm-lock.yaml"
- name: Setup Node.js for LlamaIndex
uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm"
cache-dependency-path: "llamaindex/pnpm-lock.yaml"
- name: Install dependencies for llamaflow
run: pnpm install
working-directory: llamaflow
- name: Install dependencies for llamaindex
run: pnpm install
working-directory: llamaindex
- name: Link Llamaflow docs to LlamaIndex
run: |
pnpm link ${{github.workspace}}/llamaflow/docs
working-directory: llamaindex/apps/next
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel Environment Information
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
working-directory: llamaindex
- name: Build Project Artifacts
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
working-directory: llamaindex
- name: Deploy Project Artifacts to Vercel
id: deploy
run: |
vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} > deploy.log
URL=$(cat deploy.log | grep -o 'https://[^ ]*.llamaindex.ai' | head -n1)
echo "deployment_url=$URL" >> $GITHUB_OUTPUT
working-directory: llamaindex
add-comment:
name: Add Comment
runs-on: ubuntu-latest
needs: pre_release_doc
permissions:
issues: write
pull-requests: write
steps:
- name: Comment URL to PR
uses: actions/github-script@v6
id: comment-deployment-url-script
env:
DEPLOYMENT_URL: ${{ needs.pre_release_doc.outputs.deployment_url }}
with:
script: |
// Get pull requests that are open for current ref.
const pullRequests = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${context.repo.owner}:${context.ref.replace('refs/heads/', '')}`
})
// Set issue number for following calls from context (if on pull request event) or from above variable.
const issueNumber = context.issue.number || pullRequests.data[0].number
// Retrieve existing bot comments for the PR
const {data: comments} = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
})
const botComment = comments.find(comment => {
return comment.user.type === 'Bot' && comment.body.includes('Deployed at')
})
const output = "Deployed at " + process.env.DEPLOYMENT_URL
// If we have a comment, update it, otherwise create a new one
if (botComment) {
github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: output
})
} else {
github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
}
+4
View File
@@ -39,6 +39,10 @@ jobs:
jq --arg version "$(jq -r '.version' ./packages/core/package.json)" '.version = $version' ./packages/core/jsr.json > tmp.json && mv tmp.json ./packages/core/jsr.json
pnpx prettier --write ./packages/core/jsr.json
- name: Remove unsued changelog
run: |
rm -rf ./docs/CHANGELOG.md
- name: Commit lock file
continue-on-error: true
uses: stefanzweifel/git-auto-commit-action@v5
-13
View File
@@ -1,13 +0,0 @@
# @llama-flow/docs
## 0.0.3
### Patch Changes
- 2226f33: chore: remove changelog
## 0.0.2
### Patch Changes
- 2026212: fix: grammar issue
+533
View File
@@ -0,0 +1,533 @@
---
title: Advanced Event Handling
description: Master complex event patterns and middleware with Workflows
---
This guide explores advanced event handling techniques and patterns you can use with Workflows to build more sophisticated patterns.
## Event Composition
Workflows allow you to work with different event types and compose them in powerful ways:
### Multiple Event Types
You can define multiple event types for different kinds of data flowing through your workflow:
```ts
import { createWorkflow, workflowEvent } from "@llama-flow/core";
// Define different event types
const textEvent = workflowEvent<string>();
const numberEvent = workflowEvent<number>();
const booleanEvent = workflowEvent<boolean>();
const complexEvent = workflowEvent<{ id: string; value: number }>();
// Create a workflow that can process different event types
const workflow = createWorkflow();
// Handle text events
workflow.handle([textEvent], (event) => {
console.log(`Processing text: ${event.data}`);
return numberEvent.with(event.data.length);
});
// Handle number events
workflow.handle([numberEvent], (event) => {
const isEven = event.data % 2 === 0;
console.log(`Number ${event.data} is ${isEven ? "even" : "odd"}`);
return booleanEvent.with(isEven);
});
// Handle boolean events
workflow.handle([booleanEvent], (event) => {
return complexEvent.with({
id: crypto.randomUUID(),
value: event.data ? 100 : 0,
});
});
// Run the workflow
const { stream, sendEvent } = workflow.createContext();
sendEvent(textEvent.with("Hello, world!"));
for await (const event of stream) {
if (complexEvent.include(event)) {
console.log(event.data);
break;
}
}
```
### Event Branching and Merging
You can create complex event flows with branching and merging patterns:
```ts
import { createWorkflow, workflowEvent, getContext } from "@llama-flow/core";
import { collect } from "@llama-flow/core/stream/consumer";
import { until } from "@llama-flow/core/stream/until";
import { filter } from "@llama-flow/core/stream/filter";
import { pipeline } from "node:stream/promises";
// Define events for a data processing pipeline with branching and merging
const startEvent = workflowEvent<string[], "start">();
const itemEvent = workflowEvent<string, "item">();
const validEvent = workflowEvent<string, "valid">();
const invalidEvent = workflowEvent<string, "invalid">();
const processedValidEvent = workflowEvent<string, "processedValid">();
const processedInvalidEvent = workflowEvent<string, "processedInvalid">();
const resultEvent = workflowEvent<string[], "result">();
// Create workflow
const workflow = createWorkflow();
// Initial handler: branch out to process multiple items
workflow.handle([startEvent], (event) => {
const { sendEvent } = getContext();
// Fan out: emit an itemEvent for each input item
console.log(`Received ${event.data.length} items to process`);
for (const item of event.data) {
sendEvent(itemEvent.with(item));
}
// No return here - this handler just fans out events
});
// Branch based on item validation
workflow.handle([itemEvent], (event) => {
const item = event.data;
console.log(`Validating item: ${item}`);
// Branch: send to different paths based on condition
if (item && item.length >= 3) {
return validEvent.with(item);
} else {
return invalidEvent.with(item || "[empty]");
}
});
// Process valid items
workflow.handle([validEvent], (event) => {
const item = event.data;
console.log(`Processing valid item: ${item}`);
return processedValidEvent.with(`✓ ${item.toUpperCase()}`);
});
// Process invalid items
workflow.handle([invalidEvent], (event) => {
const item = event.data;
console.log(`Processing invalid item: ${item}`);
return processedInvalidEvent.with(`✗ ${item}`);
});
// Merge handler: collect results from both processing paths
workflow.handle([startEvent], async (event) => {
const { stream } = getContext();
const totalItems = event.data.length;
console.log(`Setting up merger to collect ${totalItems} processed results`);
// Merge: collect results from both processing paths
const results = await collect(
until(
filter(
stream,
(ev) =>
processedValidEvent.include(ev) || processedInvalidEvent.include(ev),
),
() => {
// Stop when we've collected the expected number of results
return results.length >= totalItems;
},
),
);
// Extract and sort the data from collected events
const processedResults = results.map((event) => event.data);
// The valid items (with ✓) first, then invalid items (with ✗)
processedResults.sort();
return resultEvent.with(processedResults);
});
// Example usage
async function runBranchingMergingWorkflow() {
const { stream, sendEvent } = workflow.createContext();
const items = ["apple", "b", "cherry", "", "elderberry", "fig"];
console.log("Starting workflow with items:", items);
sendEvent(startEvent.with(items));
// Process the stream using pipeline to get the final result
const result = await pipeline(stream, async function* (source) {
for await (const event of source) {
if (validEvent.include(event) || invalidEvent.include(event)) {
console.log(`Branched event with data: ${event.data}`);
} else if (
processedValidEvent.include(event) ||
processedInvalidEvent.include(event)
) {
console.log(`Processed event with data: ${event.data}`);
} else if (resultEvent.include(event)) {
console.log("Merged results:", event.data);
yield event.data;
return; // Stop processing
}
}
});
return result;
}
runBranchingMergingWorkflow();
```
In this branching and merging example:
1. We start with a list of items and fan out to process each one individually
2. Each item branches to either the valid or invalid path based on its length
3. Different processing is applied to each branch
4. Finally, we merge the results from both branches back into a single resulting array
This pattern is useful for parallel processing with different business logic for different categories of inputs.
## Event Filtering and Transformation
You can filter and transform events to build sophisticated data processing pipelines:
```ts
import { createWorkflow, workflowEvent, getContext } from "@llama-flow/core";
import { collect } from "@llama-flow/core/stream/consumer";
import { until } from "@llama-flow/core/stream/until";
import { filter } from "@llama-flow/core/stream/filter";
// Define events
const dataEvent = workflowEvent<number>();
const initEvent = workflowEvent<number>();
const evenEvent = workflowEvent<number>();
const oddEvent = workflowEvent<number>();
const transformedEvent = workflowEvent<string>();
const resultEvent = workflowEvent<string[]>();
// Create workflow
const workflow = createWorkflow();
// Filter even numbers
workflow.handle([dataEvent], (event) => {
if (event.data % 2 === 0) {
return evenEvent.with(event.data);
} else {
return oddEvent.with(event.data);
}
});
// Transform even numbers
workflow.handle([evenEvent], (event) => {
return transformedEvent.with(`Even: ${event.data}`);
});
// Transform odd numbers
workflow.handle([oddEvent], (event) => {
return transformedEvent.with(`Odd: ${event.data}`);
});
// Collect and organize results
workflow.handle([initEvent], async (event) => {
const { stream, sendEvent } = getContext();
// Generate a sequence of numbers
for (let i = 1; i <= event.data; i++) {
sendEvent(dataEvent.with(i));
}
// Collect transformed events
let numResults = 0;
const results = await collect(
until(
filter(stream, (ev) => transformedEvent.include(ev)),
() => {
numResults++;
return numResults >= event.data;
},
),
);
return resultEvent.with(results.map((r) => r.data));
});
// Example usage
async function runFilterTransformWorkflow() {
const { stream, sendEvent } = workflow.createContext();
sendEvent(initEvent.with(10));
// Process the stream to get the final result
for await (const event of stream) {
if (resultEvent.include(event)) {
console.log("Results:", event.data);
return event.data;
}
}
}
runFilterTransformWorkflow();
```
### Debugging with Substreams
You can use the `substream` feature to debug specific event flows:
```ts
import { createWorkflow, workflowEvent, getContext } from "@llama-flow/core";
import { withTraceEvents } from "@llama-flow/core/middleware/trace-events";
// Define events
const queryEvent = workflowEvent<string>({
debugLabel: "query",
});
const fetchEvent = workflowEvent<string>({
debugLabel: "fetch",
});
const resultEvent = workflowEvent<string>({
debugLabel: "result",
});
// Create workflow with tracing
const workflow = withTraceEvents(createWorkflow());
// Query handler
workflow.handle([queryEvent], (event) => {
const { sendEvent, stream } = getContext();
// Create a specific fetch event for this query
const fetchInstance = fetchEvent.with(event.data);
sendEvent(fetchInstance);
// Create a substream to only track events related to this fetch
const substream = workflow.substream(fetchInstance, stream);
// Listen for results in the substream
(async () => {
for await (const event of substream) {
console.log(`Event in substream: ${event}`);
}
})();
return resultEvent.with(`Querying: ${event.data}`);
});
// Fetch handler
workflow.handle([fetchEvent], (event) => {
console.log(`Fetching data for: ${event.data}`);
// Actual fetch logic would go here
return resultEvent.with(`Results for: ${event.data}`);
});
async function run() {
const { stream, sendEvent } = workflow.createContext();
sendEvent(queryEvent.with("Hello"));
for await (const event of stream) {
if (resultEvent.include(event)) {
console.log(`Result: ${event.data}`);
break;
}
}
}
run();
```
## Advanced Validation and Type Safety
The `withValidation` middleware ensures your workflow connections are both type-safe and runtime-safe:
```ts
import { createWorkflow, workflowEvent } from "@llama-flow/core";
import { withValidation } from "@llama-flow/core/middleware/validation";
// Define events with explicit types
const inputEvent = workflowEvent<string>();
const validateEvent = workflowEvent<string>();
const processEvent = workflowEvent<string>();
const resultEvent = workflowEvent<string>();
const errorEvent = workflowEvent<Error>({
debugLabel: "errorEvent", // Add debug labels for better error messages
});
// Define the allowed event flow paths
const workflow = withValidation(createWorkflow(), [
[[inputEvent], [validateEvent, errorEvent]], // inputEvent can lead to validateEvent or errorEvent
[[validateEvent], [processEvent, errorEvent]], // validateEvent can lead to processEvent or errorEvent
[[processEvent], [resultEvent, errorEvent]], // processEvent can lead to resultEvent or errorEvent
[[errorEvent], [resultEvent]], // errorEvent can lead to resultEvent
]);
// Now use strictHandle to get compile-time validation
workflow.strictHandle([inputEvent], (sendEvent, event) => {
try {
if (!event.data || event.data.trim().length === 0) {
throw new Error("Empty input");
}
// This is allowed by our validation rules
sendEvent(validateEvent.with(event.data.trim()));
// This would cause a compile-time error:
// sendEvent(resultEvent.with("Result")); // ❌ Not allowed by validation rules
} catch (err) {
// This is allowed by our validation rules
sendEvent(
errorEvent.with(err instanceof Error ? err : new Error(String(err))),
);
}
});
// The rest of the workflow with strict validation
workflow.strictHandle([validateEvent], (sendEvent, event) => {
// Validation logic here
sendEvent(processEvent.with(event.data));
});
workflow.strictHandle([processEvent], (sendEvent, event) => {
// Processing logic here
sendEvent(resultEvent.with(`Processed: ${event.data}`));
});
workflow.strictHandle([errorEvent], (sendEvent, event) => {
// Error handling logic here
sendEvent(resultEvent.with(`Error handled: ${event.data.message}`));
});
```
## Integrating with External Systems
You can extend your workflows to integrate with external systems:
```ts
import { createWorkflow, workflowEvent, getContext } from "@llama-flow/core";
// Define events
const fetchEvent = workflowEvent<string>();
const successEvent = workflowEvent<any>();
const failureEvent = workflowEvent<Error>();
// Create workflow
const workflow = createWorkflow();
// Handle external API calls with proper error handling
workflow.handle([fetchEvent], async (event) => {
const { signal } = getContext();
try {
// Use AbortSignal for cancellation support
const response = await fetch(event.data, { signal });
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
return successEvent.with(data);
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
return failureEvent.with(new Error("Request was aborted"));
}
return failureEvent.with(
error instanceof Error ? error : new Error(String(error)),
);
}
});
// Database integration example
const dbQueryEvent = workflowEvent<{ collection: string; query: any }>();
const dbResultEvent = workflowEvent<any[]>();
workflow.handle([dbQueryEvent], async (event) => {
// Connect to database (pseudo-code)
const db = await connectToDatabase();
try {
const results = await db
.collection(event.data.collection)
.find(event.data.query)
.toArray();
return dbResultEvent.with(results);
} catch (error) {
return failureEvent.with(
error instanceof Error ? error : new Error(String(error)),
);
} finally {
await db.close();
}
});
```
## Handling Complex Asynchronous Patterns
LlamaIndex workflows excel at managing complex asynchronous patterns:
```ts
import { createWorkflow, workflowEvent, getContext } from "@llama-flow/core";
import { until } from "@llama-flow/core/stream/until";
// Events for an orchestration workflow
const orchestrateEvent = workflowEvent<string[]>();
const taskEvent = workflowEvent<string>();
const progressEvent = workflowEvent<{ task: string; progress: number }>();
const taskCompleteEvent = workflowEvent<string>();
const aggregateEvent = workflowEvent<any>();
// Create workflow
const workflow = createWorkflow();
// Orchestrator: distribute tasks and collect results
workflow.handle([orchestrateEvent], async (event) => {
const { sendEvent, stream } = getContext();
const tasks = event.data;
// Start all tasks
tasks.forEach((task) => sendEvent(taskEvent.with(task)));
// Track progress
let completed = 0;
const results: Record<string, string> = {};
// Process task completion and progress events
for await (const event of until(stream, () => completed === tasks.length)) {
if (progressEvent.include(event)) {
console.log(`Task ${event.data.task}: ${event.data.progress}%`);
} else if (taskCompleteEvent.include(event)) {
completed++;
results[event.data] = `Completed ${event.data}`;
console.log(`Completed ${completed}/${tasks.length} tasks`);
}
}
return aggregateEvent.with(results);
});
// Task processor
workflow.handle([taskEvent], async (event) => {
const { sendEvent } = getContext();
const task = event.data;
// Simulate task processing with progress updates
for (let progress = 0; progress <= 100; progress += 20) {
sendEvent(progressEvent.with({ task, progress }));
await new Promise((resolve) => setTimeout(resolve, 200));
}
return taskCompleteEvent.with(task);
});
```
## Next Steps
Now that you've explored advanced event handling with workflows, you're ready to build sophisticated applications:
- [Integrating Workflows with other LlamaIndex Features](./llamaindex-integration.mdx)
+343
View File
@@ -0,0 +1,343 @@
---
title: Basic Workflow Patterns
description: Learn common patterns and techniques for building effective workflows
---
This guide explores common patterns you can use to build more complex workflows with workflows.
## Fan-out (Parallelism)
One of the most powerful features of workflows is the ability to run tasks in parallel:
```ts
import { createWorkflow, workflowEvent, getContext } from "@llama-flow/core";
import { collect } from "@llama-flow/core/stream/consumer";
import { until } from "@llama-flow/core/stream/until";
import { filter } from "@llama-flow/core/stream/filter";
// Define events
const startEvent = workflowEvent<string>();
const processItemEvent = workflowEvent<number>();
const resultEvent = workflowEvent<string>();
const completeEvent = workflowEvent<string[]>();
// Create workflow
const workflow = createWorkflow();
// Define a variable accessible within the handler scope to signal completion
let itemsToProcess = 10; // Total number of items
let itemsProcessed = 0;
// Process start event: fan out to multiple processItemEvent events
workflow.handle([startEvent], (start) => {
const { sendEvent, stream } = getContext();
itemsProcessed = 0; // Reset counter for this execution context
// Emit multiple events to be processed in parallel
for (let i = 0; i < itemsToProcess; i++) {
sendEvent(processItemEvent.with(i));
}
// Use an async IIFE to collect results and emit completeEvent
(async () => {
const results = await collect(
// Filter for resultEvent and stop when all items are processed
until(
filter(stream, (event) => resultEvent.include(event)), // Only consider resultEvent
() => itemsProcessed === itemsToProcess, // Stop condition check
),
);
// Send the final aggregated result
sendEvent(completeEvent.with(results.map((event) => event.data)));
})().catch(console.error); // Handle potential errors during collection
// Note: This handler finishes *before* the collection completes.
// Returning nothing or a specific "processing started" event might be appropriate.
});
// Process each item
workflow.handle([processItemEvent], async (event) => {
// Simulate async work
await new Promise((resolve) => setTimeout(resolve, Math.random() * 100));
const processedValue = `Processed: ${event.data}`;
// Crucially, update the shared counter *after* processing
itemsProcessed++;
return resultEvent.with(processedValue);
});
// Example E2E Run Usage
async function runFanOut() {
console.log("Running fan-out");
const { stream, sendEvent } = workflow.createContext();
sendEvent(startEvent.with("Start fan-out"));
for await (const event of stream) {
if (processItemEvent.include(event)) {
console.log(`Processing item: ${event.data}`);
} else if (resultEvent.include(event)) {
console.log(`Result received: ${event.data}`);
} else if (completeEvent.include(event)) {
console.log("Final aggregated results:", event.data);
break; // Stop processing the stream
}
}
}
runFanOut();
```
This pattern allows you to:
1. Emit multiple events to be processed in parallel
2. Collect results as they come in
3. Complete once all parallel tasks are finished
## Conditional Branching
You can implement conditional logic in your workflows:
```ts
import { createWorkflow, workflowEvent } from "@llama-flow/core";
import { collect } from "@llama-flow/core/stream/consumer";
import { until } from "@llama-flow/core/stream/until";
const inputEvent = workflowEvent<number>();
const evenNumberEvent = workflowEvent<string>();
const oddNumberEvent = workflowEvent<string>();
const resultEvent = workflowEvent<string>();
const workflow = createWorkflow();
// Branch based on whether the number is even or odd
workflow.handle([inputEvent], (event) => {
if (event.data % 2 === 0) {
return evenNumberEvent.with(`${event.data} is even`);
} else {
return oddNumberEvent.with(`${event.data} is odd`);
}
});
// Handle even numbers
workflow.handle([evenNumberEvent], (event) => {
return resultEvent.with(`Even result: ${event.data}`);
});
// Handle odd numbers
workflow.handle([oddNumberEvent], (event) => {
return resultEvent.with(`Odd result: ${event.data}`);
});
// Example E2E Run Usage
async function run(input_number: number) {
// Create a workflow context and send the initial event
const { stream, sendEvent } = workflow.createContext();
sendEvent(inputEvent.with(input_number));
// Collect all events until we get a stopEvent
const allEvents = await collect(until(stream, resultEvent));
// The last event will be the stopEvent that was requested
const finalEvent = allEvents[allEvents.length - 1];
if (resultEvent.include(finalEvent)) {
console.log(`Result: ${finalEvent.data}`);
}
}
run(42);
run(43);
```
## Using Middleware
LlamaIndex workflows provide middleware that can enhance your workflows:
### `withStore` Middleware
The `withStore` middleware adds a persistent store to your workflow context:
```ts
import { createWorkflow, workflowEvent } from "@llama-flow/core";
import { withStore } from "@llama-flow/core/middleware/store";
const startEvent = workflowEvent<void>();
const incrementEvent = workflowEvent<number>();
const resultEvent = workflowEvent<number>();
// Create a workflow with store middleware
const workflow = withStore(
() => ({
// Initializer function
count: 0,
history: [] as number[],
}),
createWorkflow(),
);
// Increment the counter
workflow.handle([startEvent], () => {
const store = workflow.getStore(); // Use the provided getStore method
store.count += 1;
store.history.push(store.count);
return incrementEvent.with(store.count);
});
// Return the current count
workflow.handle([incrementEvent], (event) => {
const store = workflow.getStore();
console.log("Current count:", store.count);
console.log("History:", store.history);
return resultEvent.with(store.count);
});
// Example E2E Run Usage
async function runWithStore() {
const { stream, sendEvent } = workflow.createContext();
// Send start event multiple times to see store update
sendEvent(startEvent.with());
sendEvent(startEvent.with());
for await (const event of stream) {
if (resultEvent.include(event)) {
console.log("Final count received:", event.data);
// Note: In a real app, you might need logic to stop listening
// if you only expect one result per startEvent.
}
}
}
runWithStore();
```
### `withValidation` Middleware
The `withValidation` middleware adds compile-time and runtime validation to your workflows:
```ts
import { createWorkflow, workflowEvent } from "@llama-flow/core";
import { withValidation } from "@llama-flow/core/middleware/validation";
const startEvent = workflowEvent<string, "start">();
const processEvent = workflowEvent<number, "process">();
const resultEvent = workflowEvent<string, "result">();
const disallowedEvent = workflowEvent<void, "disallowed">();
// Create a workflow with validation middleware
// Define allowed event paths
const workflow = withValidation(createWorkflow(), [
[[startEvent], [processEvent]], // startEvent can only lead to processEvent
[[processEvent], [resultEvent]], // processEvent can only lead to resultEvent
]);
// This will pass validation
workflow.strictHandle([startEvent], (sendEvent, start) => {
sendEvent(processEvent.with(123)); // ✅ This is allowed
});
// This would fail at compile time and runtime
workflow.strictHandle([startEvent], (sendEvent, start) => {
// sendEvent(disallowedEvent.with("disallowed")); // ❌ This would cause an error
// sendEvent(resultEvent.with("result")); // ❌ This would also cause an error
});
```
## Error Handling
LlamaIndex workflows provide built-in mechanisms for handling errors:
```ts
import { createWorkflow, workflowEvent, getContext } from "@llama-flow/core";
const startEvent = workflowEvent<string>();
const processEvent = workflowEvent<number>();
const errorEvent = workflowEvent<Error>();
const resultEvent = workflowEvent<string>();
const workflow = createWorkflow();
workflow.handle([startEvent], (start) => {
try {
const num = Number.parseInt(start.data, 10);
if (isNaN(num)) {
throw new Error("Invalid number");
}
return processEvent.with(num);
} catch (err) {
return errorEvent.with(err instanceof Error ? err : new Error(String(err)));
}
});
workflow.handle([processEvent], (event) => {
return resultEvent.with(`Result: ${event.data * 2}`);
});
workflow.handle([errorEvent], (event) => {
return resultEvent.with(`Error: ${event.data.message}`);
});
```
You can also use the signal in `getContext()` to handle errors:
```ts
workflow.handle([processEvent], () => {
const { signal } = getContext();
signal.onabort = () => {
console.error("Process aborted:", signal.reason);
// Clean up resources
};
// Your processing logic here
});
```
## Connecting with Server Endpoints
Workflow can be used as middleware in server frameworks like Express, Hono, or Fastify:
```ts
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { createHonoHandler } from "@llama-flow/core/interrupter/hono";
import { createWorkflow, workflowEvent } from "@llama-flow/core";
// Define events
const queryEvent = workflowEvent<string>();
const responseEvent = workflowEvent<string>();
// Create workflow
const workflow = createWorkflow();
workflow.handle([queryEvent], (event) => {
const response = `Processed: ${event.data}`;
return responseEvent.with(response);
});
// Create Hono app
const app = new Hono();
// Set up workflow endpoint
app.post(
"/workflow",
createHonoHandler(
workflow,
async (ctx) => queryEvent.with(await ctx.req.text()),
responseEvent,
),
);
// Start server
serve(app, ({ port }) => {
console.log(`Server started at http://localhost:${port}`);
});
```
## Next Steps
Now that you've learned about basic workflow patterns, explore more advanced topics:
- [Streaming with Workflows](./streaming.mdx)
- [Advanced Event Handling](./advanced-events.mdx)
+117 -8
View File
@@ -1,19 +1,128 @@
---
title: What is LlamaFlow
description: 🌊 is a simple, lightweight workflow engine, in TypeScript.
title: Getting Started with Workflows
description: Learn how to use LlamaIndex's lightweight workflow engine for TypeScript
---
LlamaFlow is a simple, lightweight workflow engine, in TypeScript.
It is designed to be easy to use and integrate with your existing applications.
Workflows are a simple and lightweight engine for TypeScript. Built with ❤️ by LlamaIndex.
## Features
- Minimal core API (&le;2kb)
- Minimal core API (\<\=2kb)
- 100% Type safe
- Event-driven, stream oriented programming
- Support for multiple JS runtimes/frameworks
## Installation
```package-install
Install the package directly:
```shell
npm i @llama-flow/core
# or with yarn
yarn add @llama-flow/core
# or with pnpm
pnpm add @llama-flow/core
```
## Key Concepts
- **Events**: Data carriers that flow through the workflow
- **Handlers**: Functions that process events and emit new events
- **Workflow**: Connects events and handlers together
- **Context**: Runtime environment for a workflow execution
## Basic Usage
Let's build a simple workflow that processes a text input:
### 1. Define events
First, we need to define the events that will flow through our workflow:
```ts
import { workflowEvent } from "@llama-flow/core";
// Define input and output events
const startEvent = workflowEvent<string>(); // Takes a string input
const convertEvent = workflowEvent<number>(); // Intermediate event
const stopEvent = workflowEvent<1 | -1>(); // Final output event, returns 1 or -1
```
### 2. Create a workflow and connect events
Next, we'll create our workflow and define how events are processed:
```ts
import { createWorkflow } from "@llama-flow/core";
const workflow = createWorkflow();
// Handle the start event: convert the string to a number
workflow.handle([startEvent], (start) => {
return convertEvent.with(Number.parseInt(start.data, 10));
});
// Handle the convert event: determine if number is positive or negative
workflow.handle([convertEvent], (convert) => {
return stopEvent.with(convert.data > 0 ? 1 : -1);
});
```
### 3. Run the workflow
Finally, we can execute our workflow. You can process the event stream directly or use utilities.
**Using `node:stream/promises`:**
:::note
This may require setting `"lib": ["ES2022", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],` and `"module": "NodeNext"` or similar in your `tsconfig.json` to use top-level `await` and `pipeline`.
:::
```ts
import { pipeline } from "node:stream/promises";
// Create a workflow context and send the initial event
const { stream, sendEvent } = workflow.createContext();
sendEvent(startEvent.with("42"));
// Process the stream using pipeline to find the stopEvent
const result = await pipeline(stream, async function (source) {
for await (const event of source) {
if (stopEvent.include(event)) {
return `Result: ${event.data === 1 ? "positive" : "negative"}`;
}
}
});
console.log(result); // "Result: positive"
// Alternative: Process the stream manually
for await (const event of stream) {
if (stopEvent.include(event)) {
console.log(`Result: ${event.data === 1 ? "positive" : "negative"}`);
break; // Exit the loop
}
}
```
**Using Stream Utilities:**
```ts
import { collect } from "@llama-flow/core/stream/consumer";
import { until } from "@llama-flow/core/stream/until";
// Create a workflow context and send the initial event
const { stream, sendEvent } = workflow.createContext();
sendEvent(startEvent.with("42"));
// Collect all events until we get a stopEvent
const allEvents = await collect(until(stream, stopEvent));
// The last event will be the stopEvent that was requested
const finalEvent = allEvents[allEvents.length - 1];
if (stopEvent.include(finalEvent)) {
console.log(`Result: ${finalEvent.data === 1 ? "positive" : "negative"}`);
}
```
Ready to learn more? Check out our [detailed examples](./basic-workflow.mdx) to see workflows in action!
+287
View File
@@ -0,0 +1,287 @@
---
title: Integrating with LlamaIndex
description: Build AI applications by combining Workflows with other LlamaIndex features
---
This guide demonstrates how to combine the power of the workflow engine with LlamaIndex's retrieval and reasoning capabilities to build sophisticated AI applications.
## Basic RAG Workflow
Let's build a simple Retrieval-Augmented Generation (RAG) workflow:
:::note
This example requires installing the `openai` provider: `npm i @llamaindex/openai` and setting `OPENAI_API_KEY` in your env vars.
:::
```ts
import { createWorkflow, workflowEvent } from "@llama-flow/core";
import {
Document,
VectorStoreIndex,
Settings,
BaseNode,
MetadataMode,
} from "llamaindex";
import { OpenAI, OpenAIEmbedding } from "@llamaindex/openai";
// Define events
const queryEvent = workflowEvent<string>();
const retrieveEvent = workflowEvent<{ query: string; nodes: BaseNode[] }>();
const generateEvent = workflowEvent<{ query: string; context: string }>();
const responseEvent = workflowEvent<string>();
// Create workflow
const workflow = createWorkflow();
// Set default global llm
Settings.llm = new OpenAI({
model: "gpt-4.1-mini",
temperature: 0.2,
});
// Set the default global embedModel
Settings.embedModel = new OpenAIEmbedding({
model: "text-embedding-3-small",
});
// Sample documents
const documents = [
new Document({
text: "LlamaIndex is a data framework for LLM applications to ingest, structure, and access private or domain-specific data.",
}),
new Document({
text: "LlamaIndex workflows are a lightweight workflow engine for TypeScript, designed to create event-driven processes.",
}),
];
// Create vector store index
const index = await VectorStoreIndex.fromDocuments(documents);
// Handle query: Retrieve relevant documents
workflow.handle([queryEvent], async (event) => {
const query = event.data;
console.log(`Processing query: ${query}`);
// Retrieve relevant documents
const retriever = index.asRetriever();
const nodes = await retriever.retrieve(query);
return retrieveEvent.with({
query,
nodes: nodes.map((node) => node.node),
});
});
// Handle retrieval results: Generate response
workflow.handle([retrieveEvent], async (event) => {
const { query, nodes } = event.data;
// Combine document content as context
const context = nodes
.map((node) => node.getContent(MetadataMode.NONE))
.join("\n\n");
return generateEvent.with({ query, context });
});
// Handle generation: Produce final response
workflow.handle([generateEvent], async (event) => {
const { query, context } = event.data;
// Create a prompt with the context and query
const prompt = `
Context information:
${context}
Based on the context information and no other knowledge, answer the following query:
${query}
`;
// Generate response with LLM
const response = await Settings.llm.complete({ prompt });
return responseEvent.with(response.text);
});
// Execute the workflow
const { stream, sendEvent } = workflow.createContext();
sendEvent(queryEvent.with("What is LlamaIndex?"));
// Process the stream
for await (const event of stream) {
if (responseEvent.include(event)) {
console.log("Final response:", event.data);
break;
}
}
```
## Building an Tool Calling Agent
Workflows can orchestrate calls to LlamaIndex Agents, which can use tools (like functions or other query engines). This example adapts the agent from `examples/11_rag.ts`.
```ts
import { createWorkflow, workflowEvent } from "@llama-flow/core";
import {
Settings,
Document,
VectorStoreIndex,
agent, // Import the agent factory
tool, // Import the tool factory
QueryEngineTool, // Specific tool type for query engines
} from "llamaindex";
import { OpenAI, OpenAIEmbedding } from "@llamaindex/openai";
// import { SimpleDirectoryReader } from "@llamaindex/readers/directory"; // If loading from files
import { z } from "zod"; // For defining tool parameters
// --- LlamaIndex Settings ---
// Set default global llm
Settings.llm = new OpenAI({
model: "gpt-4.1-mini",
temperature: 0.2,
});
// Set the default global embedModel
Settings.embedModel = new OpenAIEmbedding({
model: "text-embedding-3-small",
});
// --- Agent Setup ---
// 1. Define Tools
// Example Tool 1: Simple Sum Function
const sumNumbers = tool(
({ a, b }: { a: number; b: number }) => {
// Function logic
return Promise.resolve(`${a} + ${b} = ${a + b}`); // Agents often expect promises
},
{
// Tool metadata
name: "sumNumbers",
description: "Use this function to calculate the sum of two numbers.",
parameters: z.object({
a: z.number().describe("The first number"),
b: z.number().describe("The second number"),
}),
},
);
// Example Tool 2: RAG Query Tool (Needs an index)
async function createRagTool(): Promise<QueryEngineTool> {
console.log("[AgentSetup] Creating RAG Tool Index...");
// Load or define documents for the RAG tool
const ragDocs = [
new Document({
text: "The LlamaFlow workflow engine orchestrates complex tasks.",
}),
new Document({
text: "LlamaIndex agents can use tools to interact with external systems.",
}),
new Document({
text: "San Francisco's budget for 2023-2024 was approximately $14.6 billion.",
}),
];
const index = await VectorStoreIndex.fromDocuments(ragDocs);
const retriever = index.asRetriever({ similarityTopK: 1 });
const queryEngine = index.asQueryEngine({ retriever }); // Create a query engine
console.log("[AgentSetup] RAG Tool Index ready.");
// Create the QueryEngineTool
return new QueryEngineTool({
queryEngine: queryEngine, // Pass the query engine instance
metadata: {
name: "llama_info_tool",
description:
"Provides information about LlamaFlow, LlamaIndex Agents, and sometimes specific facts like SF budget figures.",
},
});
}
// 2. Create the Agent
let llamaAgent: ReturnType<typeof agent>; // Declare agent variable
async function initializeAgent() {
console.log("[AgentSetup] Initializing agent...");
const ragTool = await createRagTool();
const tools = [sumNumbers, ragTool];
llamaAgent = agent({
// Use the agent factory
tools,
llm: new OpenAI({ model: "gpt-4o-mini" }), // Explicitly pass LLM if not relying solely on global Settings
// verbose: true, // Enable verbose logging for debugging agent steps
});
console.log(
"[AgentSetup] Agent initialized with tools:",
tools.map((t) => t.metadata.name).join(", "),
);
}
// --- Workflow Definition ---
const agentQueryEvent = workflowEvent<string>({ debugLabel: "agentQuery" });
const agentResponseEvent = workflowEvent<string>({
debugLabel: "agentResponse",
});
const agentErrorEvent = workflowEvent<Error>({ debugLabel: "agentError" });
const agentWorkflow = createWorkflow();
// Handle incoming query, run the agent, and return the result or error
agentWorkflow.handle([agentQueryEvent], async (event) => {
if (!llamaAgent) {
return agentErrorEvent.with(
new Error("Agent not initialized. Call initializeAgent() first."),
);
}
const query = event.data;
console.log(`[AgentWorkflow] Received query for agent: ${query}`);
try {
const response = await llamaAgent.run(query);
console.log(`[AgentWorkflow] Agent response received.`);
const resultText = JSON.stringify(response.data);
return agentResponseEvent.with(resultText);
} catch (error) {
console.error(`[AgentWorkflow] Error running agent:`, error);
return agentErrorEvent.with(
error instanceof Error ? error : new Error(String(error)),
);
}
});
// --- Workflow Execution ---
async function runAgentWorkflow() {
await initializeAgent(); // Ensure agent is ready
const { stream, sendEvent } = agentWorkflow.createContext();
const query = "What is LlamaFlow and what is 15 + 27?";
console.log(`Sending query to agent workflow: ${query}`);
sendEvent(agentQueryEvent.with(query));
// Process the stream
for await (const event of stream) {
console.log(`[Stream] Event: ${event}`);
if (agentResponseEvent.include(event)) {
console.log("Agent Response:", event.data);
}
if (agentErrorEvent.include(event)) {
console.error("Agent Error:", event.data.message);
}
}
}
runAgentWorkflow();
```
## Conclusion
By combining the lightweight, event-driven workflow engine with LlamaIndex's powerful document indexing and querying capabilities, you can build sophisticated AI applications with clean, maintainable code.
The event-driven architecture allows you to:
1. Break complex processes into manageable steps
2. Create reusable components for common AI workflows
3. Easily debug and monitor each phase of execution
4. Scale your applications by isolating resource-intensive steps
5. Build more resilient systems with better error handling
As you build your own applications, consider how the patterns shown here can be adapted to your specific use cases.
+10 -3
View File
@@ -1,6 +1,13 @@
{
"title": "LlamaFlow",
"description": "Simple, lightweight workflow engine",
"title": "Workflows",
"description": "Event-driven workflow engine",
"root": true,
"pages": ["---Guide---", "index", "..."]
"defaultOpen": false,
"pages": [
"index",
"basic-workflow",
"streaming",
"advanced-events",
"llamaindex-integration"
]
}
+291
View File
@@ -0,0 +1,291 @@
---
title: Streaming with Workflows
description: Learn how to build streaming workflows
---
LlamaIndex workflows are designed from the ground up to work with streaming data. The streaming capabilities make it perfect for:
- Building real-time applications
- Handling large datasets incrementally
- Creating responsive UIs that update as data becomes available
- Implementing long-running tasks with partial results
## Basic Streaming
Every workflow context provides a stream of events:
```ts
import { createWorkflow, workflowEvent, getContext } from "@llama-flow/core";
// Define events
const startEvent = workflowEvent<string>();
const intermediateEvent = workflowEvent<string>();
const resultEvent = workflowEvent<string>();
// Create workflow
const workflow = createWorkflow();
workflow.handle([startEvent], (event) => {
const { sendEvent } = getContext();
// Emit multiple intermediate events
for (let i = 0; i < 5; i++) {
sendEvent(intermediateEvent.with(`Progress: ${i * 20}%`));
}
return resultEvent.with("Completed");
});
// Run the workflow
const { stream, sendEvent } = workflow.createContext();
sendEvent(startEvent.with("Start processing"));
// Process events as they arrive
for await (const event of stream) {
if (intermediateEvent.include(event)) {
console.log(event.data); // Show progress updates
} else if (resultEvent.include(event)) {
console.log("Final result:", event.data);
break; // Exit the loop when done
}
}
```
## Using the Stream Utilities
Workflows provide utility functions to make working with streams easier:
```ts
import { createWorkflow, workflowEvent, getContext } from "@llama-flow/core";
import { collect } from "@llama-flow/core/stream/consumer";
import { until } from "@llama-flow/core/stream/until";
const startEvent = workflowEvent<void>();
const progressEvent = workflowEvent<number>();
const resultEvent = workflowEvent<string>();
const workflow = createWorkflow();
workflow.handle([startEvent], () => {
const { sendEvent } = getContext();
// Emit progress events
for (let i = 0; i < 100; i += 10) {
sendEvent(progressEvent.with(i));
}
return resultEvent.with("Complete");
});
// Run the workflow and collect events until a condition is met
const { stream, sendEvent } = workflow.createContext();
sendEvent(startEvent.with());
// Collect all events until resultEvent is encountered
const events = await collect(until(stream, resultEvent));
// Filter only progress events
const progressEvents = events.filter((event) => progressEvent.include(event));
console.log(`Received ${progressEvents.length} progress updates`);
```
## Conditional Stream Processing
You can conditionally process events and even stop the stream early:
```ts
import { createWorkflow, workflowEvent, getContext } from "@llama-flow/core";
const startEvent = workflowEvent<number>();
const dataEvent = workflowEvent<number>();
const thresholdEvent = workflowEvent<void>();
const resultEvent = workflowEvent<number[]>();
const workflow = createWorkflow();
workflow.handle([startEvent], (event) => {
const { sendEvent } = getContext();
const max = event.data;
for (let i = 0; i < max; i++) {
sendEvent(dataEvent.with(i));
if (i >= 10) {
// Signal that we've hit a threshold
sendEvent(thresholdEvent.with());
}
}
return resultEvent.with(Array.from({ length: max }, (_, i) => i));
});
// Run the workflow
const { stream, sendEvent } = workflow.createContext();
sendEvent(startEvent.with(100)); // Generate 100 numbers
const results = [];
let hitThreshold = false;
// Process the stream
for await (const event of stream) {
if (dataEvent.include(event)) {
results.push(event.data);
} else if (thresholdEvent.include(event)) {
hitThreshold = true;
break; // Stop processing early
}
}
console.log(
`Collected ${results.length} items before ${hitThreshold ? "hitting threshold" : "completion"}`,
);
```
## Integration with UI Frameworks
Workflow streams can be easily integrated with UI frameworks like React to create responsive interfaces:
```tsx
// In a React component
import { useEffect, useState } from "react";
import { createWorkflow, workflowEvent, getContext } from "@llama-flow/core";
function StreamingComponent() {
const [updates, setUpdates] = useState([]);
const [isComplete, setIsComplete] = useState(false);
useEffect(() => {
// Set up workflow
const startEvent = workflowEvent<void>();
const updateEvent = workflowEvent<string>();
const completeEvent = workflowEvent<void>();
const workflow = createWorkflow();
workflow.handle([startEvent], () => {
const { sendEvent, signal } = getContext();
// Simulate async updates
const intervals = [
setTimeout(() => sendEvent(updateEvent.with("First update")), 500),
setTimeout(() => sendEvent(updateEvent.with("Second update")), 1000),
setTimeout(() => sendEvent(updateEvent.with("Final update")), 1500),
setTimeout(() => sendEvent(completeEvent.with()), 2000),
];
// Cleanup function using the signal
signal.onabort = () => {
console.log("Workflow context aborted, clearing timeouts.");
intervals.forEach(clearTimeout);
};
});
// Run the workflow
const { stream, sendEvent, abort } = workflow.createContext();
sendEvent(startEvent.with());
// Process events
const processEvents = async () => {
for await (const event of stream) {
if (updateEvent.include(event)) {
setUpdates((prev) => [...prev, event.data]);
} else if (completeEvent.include(event)) {
setIsComplete(true);
break;
}
}
};
processEvents();
// Cleanup
return () => {
console.log("Component unmounting, aborting workflow context.");
abort(); // Call abort on cleanup
};
}, []);
return (
<div>
<h2>Streaming Updates</h2>
<ul>
{updates.map((update, i) => (
<li key={i}>{update}</li>
))}
</ul>
{isComplete && <div>Process complete!</div>}
</div>
);
}
```
## Server-Sent Events (SSE)
Workflows are also suitable for implementing Server-Sent Events:
```ts
import { createWorkflow, workflowEvent, getContext } from "@llama-flow/core";
import express from "express";
// Define events
const startEvent = workflowEvent<void>();
const dataEvent = workflowEvent<string>();
// Create workflow
const workflow = createWorkflow();
workflow.handle([startEvent], () => {
const { sendEvent, signal } = getContext();
// Send periodic updates
const intervalId = setInterval(() => {
// Store single interval ID
sendEvent(dataEvent.with(`Update: ${new Date().toISOString()}`));
}, 1000);
// Cleanup using the signal
signal.onabort = () => {
console.log("Workflow context aborted, clearing interval.");
clearInterval(intervalId);
};
});
// Set up Express server
const app = express();
app.get("/events", (req, res) => {
// Set headers for SSE
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
// Run workflow
const { stream, sendEvent, abort } = workflow.createContext();
sendEvent(startEvent.with());
// Handle client disconnect
req.on("close", () => {
console.log("Client disconnected, aborting workflow context.");
abort(); // Abort the context when client closes connection
});
// Process and send events
(async () => {
for await (const event of stream) {
if (dataEvent.include(event)) {
res.write(`data: ${JSON.stringify(event.data)}\n\n`);
}
}
})();
});
app.listen(3000, () => {
console.log("SSE server running on port 3000");
});
```
## Next Steps
Now that you've learned about streaming with workflows, explore more advanced topics:
- [Advanced Event Handling](./advanced-events.mdx)
- [Integration Workflows with other LlamaIndex features](./llamaindex-integration.mdx)