mirror of
https://github.com/run-llama/workflows-ts.git
synced 2026-07-21 14:15:24 -04:00
docs: add new docs (#43)
Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@llama-flow/docs": patch
|
||||
---
|
||||
|
||||
feat: update docs
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
@@ -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 (≤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!
|
||||
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user