mirror of
https://github.com/run-llama/LlamaIndexTS.git
synced 2026-07-09 11:25:43 -04:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 502e48bbc5 | |||
| 8994904b06 | |||
| 8d49887131 | |||
| b3309a10dd | |||
| 48888569dc | |||
| c816c16a97 | |||
| 94dbe381b0 | |||
| bf33bf04c3 | |||
| 8707a3e688 | |||
| be66ccc6c8 | |||
| 109ec63779 | |||
| 82d4b46fe4 | |||
| f8c2d0b8ad | |||
| 6d7bc4ccbb | |||
| 294f502441 |
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"@llamaindex/workflow": minor
|
||||
---
|
||||
|
||||
refactor!: migrate to llamaflow
|
||||
|
||||
- remove `outputs` in workflow. You shuld use TypeScript and define returns type to validate the workflow correctly.
|
||||
- remove `timeout` and `verbose` in workflow. Workflow now is a very lightly engine, so you should do this by youself. For example, `abortSignal.timeout`, `console.log`...
|
||||
- `workflow.run` now retunrs `ReadableStream | Promise<WorkflowEvent<Result>>`, you shouldn't use steram and promise in both time.
|
||||
@@ -7,3 +7,4 @@ dist/
|
||||
.source/
|
||||
# prttier doesn't support mdx3 we are using
|
||||
*.mdx
|
||||
packages/server/server/
|
||||
@@ -57,6 +57,41 @@ const researchAgent = agent({
|
||||
});
|
||||
```
|
||||
|
||||
## MCP tools
|
||||
|
||||
If you have a MCP server running, you can fetch tools from the server and use them in your agents.
|
||||
|
||||
```ts
|
||||
// 1. Import MCP tools adapter
|
||||
import { mcp } from "@llamaindex/tools";
|
||||
import { agent } from "llamaindex";
|
||||
|
||||
// 2. Initialize a MCP client
|
||||
// by npx
|
||||
const server = mcp({
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-filesystem", "."],
|
||||
verbose: true,
|
||||
});
|
||||
// or by SSE
|
||||
const server = mcp({
|
||||
url: "http://localhost:8000/mcp",
|
||||
verbose: true,
|
||||
});
|
||||
|
||||
// 3. Get tools from MCP server
|
||||
const tools = await server.tools();
|
||||
|
||||
// Now you can create an agent with the tools
|
||||
const agent = agent({
|
||||
name: "My Agent",
|
||||
systemPrompt: "You are a helpful assistant that can use the provided tools to answer questions.",
|
||||
llm: openai({ model: "gpt-4o" }),
|
||||
tools: tools,
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
## Function tool
|
||||
|
||||
You can still use the `FunctionTool` class to define a tool.
|
||||
|
||||
@@ -2,149 +2,260 @@
|
||||
title: Workflows
|
||||
---
|
||||
|
||||
A `Workflow` in LlamaIndexTS is an event-driven abstraction used to chain together several events. Workflows are made up of `steps`, with each step responsible for handling certain event types and emitting new events.
|
||||
|
||||
Workflows in LlamaIndexTS work by defining step functions that handle specific event types and emit new events.
|
||||
|
||||
When a step function is added to a workflow, you need to specify the input and optionally the output event types (used for validation). The specification of the input events ensures each step only runs when an accepted event is ready.
|
||||
|
||||
You can create a `Workflow` to do anything! Build an agent, a RAG flow, an extraction flow, or anything else you want.
|
||||
A `Workflow` in LlamaIndex is a lightweight, event-driven abstraction used to chain together several events. Workflows are made up of `handlers`, with each one responsible for processing specific event types and emitting new events.
|
||||
|
||||
Workflows are designed to be flexible and can be used to build agents, RAG flows, extraction flows, or anything else you want to implement.
|
||||
|
||||
```package-install
|
||||
npm i @llamaindex/workflow
|
||||
npm i @llama-flow/core @llamaindex/openai
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
As an illustrative example, let's consider a naive workflow where a joke is generated and then critiqued.
|
||||
Let's explore a simple workflow example where a joke is generated and then critiqued and iterated on:
|
||||
|
||||
<include cwd>../../examples/workflow/joke.ts</include>
|
||||
```typescript
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import { createWorkflow, workflowEvent } from "@llama-flow/core";
|
||||
import { withStore } from "@llama-flow/core/middleware/store";
|
||||
|
||||
There's a few moving pieces here, so let's go through this piece by piece.
|
||||
// Create LLM instance
|
||||
const llm = new OpenAI({ model: "gpt-4.1-mini", apiKey: "..."});
|
||||
|
||||
// Define our workflow events
|
||||
const startEvent = workflowEvent<string>(); // Input topic for joke
|
||||
const jokeEvent = workflowEvent<{ joke: string }>(); // Intermediate joke
|
||||
const critiqueEvent = workflowEvent<{ joke: string, critique: string }>(); // Intermediate critique
|
||||
const resultEvent = workflowEvent<{ joke: string, critique: string }>(); // Final joke + critique
|
||||
|
||||
// Create our workflow
|
||||
const jokeFlow = withStore(
|
||||
() => ({
|
||||
numIterations: 0,
|
||||
maxIterations: 3,
|
||||
}),
|
||||
createWorkflow()
|
||||
);
|
||||
|
||||
// Define handlers for each step
|
||||
jokeFlow.handle([startEvent], async (event) => {
|
||||
// Prompt the LLM to write a joke
|
||||
const prompt = `Write your best joke about ${event.data}. Write the joke between <joke> and </joke> tags.`;
|
||||
const response = await llm.complete({ prompt });
|
||||
|
||||
// Parse the joke from the response
|
||||
const joke = response.text.match(/<joke>([\s\S]*?)<\/joke>/)?.[1]?.trim() ?? response.text;
|
||||
return jokeEvent.with({ joke: joke });
|
||||
});
|
||||
|
||||
jokeFlow.handle([jokeEvent], async (event) => {
|
||||
// Prompt the LLM to critique the joke
|
||||
const prompt = `Give a thorough critique of the following joke. If the joke needs improvement, put "IMPROVE" somewhere in the critique: ${event.data.joke}`;
|
||||
const response = await llm.complete({ prompt });
|
||||
|
||||
// If the critique includes "IMPROVE", keep iterating, else, return the result
|
||||
if (response.text.includes("IMPROVE")) {
|
||||
return critiqueEvent.with({ joke: event.data.joke, critique: response.text });
|
||||
}
|
||||
|
||||
return resultEvent.with({ joke: event.data.joke, critique: response.text });
|
||||
});
|
||||
|
||||
jokeFlow.handle([critiqueEvent], async (event) => {
|
||||
// Keep track of the number of iterations
|
||||
const store = jokeFlow.getStore();
|
||||
store.numIterations++;
|
||||
|
||||
// Write a new joke based on the previous joke and critique
|
||||
const prompt = `Write a new joke based on the following critique and the original joke. Write the joke between <joke> and </joke> tags.\n\nJoke: ${event.data.joke}\n\nCritique: ${event.data.critique}`;
|
||||
const response = await llm.complete({ prompt });
|
||||
|
||||
// Parse the joke from the response
|
||||
const joke = response.text.match(/<joke>([\s\S]*?)<\/joke>/)?.[1]?.trim() ?? response.text;
|
||||
|
||||
// If we've done less than the max number of iterations, keep iterating
|
||||
// else, return the result
|
||||
if (store.numIterations < store.maxIterations) {
|
||||
return jokeEvent.with({ joke: joke });
|
||||
}
|
||||
|
||||
return resultEvent.with({ joke: joke, critique: event.data.critique });
|
||||
});
|
||||
|
||||
// Usage
|
||||
async function main() {
|
||||
const { stream, sendEvent } = jokeFlow.createContext();
|
||||
sendEvent(startEvent.with("pirates"));
|
||||
|
||||
let result: { joke: string, critique: string } | undefined;
|
||||
|
||||
for await (const event of stream) {
|
||||
// console.log(event.data); optionally log the event data
|
||||
if (resultEvent.include(event)) {
|
||||
result = event.data;
|
||||
break; // Stop when we get the final result
|
||||
}
|
||||
}
|
||||
|
||||
console.log(result);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
```
|
||||
|
||||
There are a few moving pieces here, so let's go through this step by step.
|
||||
|
||||
### Defining Workflow Events
|
||||
|
||||
```typescript
|
||||
export class JokeEvent extends WorkflowEvent<{ joke: string }> {}
|
||||
const startEvent = workflowEvent<string>(); // Input topic for joke
|
||||
const jokeEvent = workflowEvent<{ joke: string }>(); // Intermediate joke
|
||||
const critiqueEvent = workflowEvent<{ joke: string, critique: string }>(); // Intermediate critique
|
||||
const resultEvent = workflowEvent<{ joke: string, critique: string }>(); // Final joke + critique
|
||||
```
|
||||
|
||||
Events are user-defined classes that extend `WorkflowEvent` and contain arbitrary data provided as template argument. In this case, our workflow relies on a single user-defined event, the `JokeEvent` with a `joke` attribute of type `string`.
|
||||
Events are defined using the `workflowEvent` function and contain arbitrary data provided as a generic type. In this example, we have four events:
|
||||
- `startEvent`: Takes a string input (the joke topic)
|
||||
- `jokeEvent`: Contains an object with a joke property
|
||||
- `critiqueEvent`: Contains both the joke and its critique, used for the feedback loop
|
||||
- `resultEvent`: Contains the final joke and critique after any iterations
|
||||
|
||||
### Setting up the Workflow Class
|
||||
### Setting up the Workflow with Store Middleware
|
||||
|
||||
```typescript
|
||||
const llm = new OpenAI();
|
||||
...
|
||||
const jokeFlow = new Workflow<unknown, string, string>();
|
||||
```
|
||||
|
||||
Our workflow is implemented by initiating the `Workflow` class with three generic types: the context type (unknown), input type (string), and output type (string). The context type is `unknown`, as we're not using a shared context in this example.
|
||||
|
||||
For simplicity, we created an `OpenAI` llm instance that we're using for inference in our workflow.
|
||||
|
||||
### Workflow Entry Points
|
||||
|
||||
```typescript
|
||||
const generateJoke = async (_: unknown, ev: StartEvent<string>) => {
|
||||
const prompt = `Write your best joke about ${ev.data}.`;
|
||||
const response = await llm.complete({ prompt });
|
||||
return new JokeEvent({ joke: response.text });
|
||||
};
|
||||
```
|
||||
|
||||
Here, we come to the entry-point of our workflow. While events are user-defined, there are two special-case events, the `StartEvent` and the `StopEvent`. These events are predefined, but we can specify the payload type using generic types. We're using `StartEvent<string>` to indicate that we're going to send an input of type string.
|
||||
|
||||
To add this step to the workflow, we use the `addStep` method with an object specifying the input and output event types:
|
||||
|
||||
```typescript
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [JokeEvent],
|
||||
},
|
||||
generateJoke
|
||||
const jokeFlow = withStore(
|
||||
() => ({
|
||||
numIterations: 0,
|
||||
maxIterations: 3,
|
||||
}),
|
||||
createWorkflow()
|
||||
);
|
||||
```
|
||||
|
||||
### Workflow Exit Points
|
||||
Our workflow is implemented using the `createWorkflow()` function, enhanced with the `withStore` middleware. The store provides shared state across all handlers, which in this case tracks:
|
||||
- `numIterations`: Counts how many iterations of joke improvement we've done
|
||||
- `maxIterations`: Sets a limit to prevent infinite loops
|
||||
|
||||
This store will be accesible within workflows by using the `jokeFlow.getStore()` function.
|
||||
|
||||
### Adding Handlers with Loops
|
||||
|
||||
We have three key handlers in our workflow:
|
||||
|
||||
1. The first handler processes the `startEvent`, generates an initial joke, and emits a `jokeEvent`:
|
||||
|
||||
```typescript
|
||||
const critiqueJoke = async (_: unknown, ev: JokeEvent) => {
|
||||
const prompt = `Give a thorough critique of the following joke: ${ev.data.joke}`;
|
||||
jokeFlow.handle([startEvent], async (event) => {
|
||||
// Prompt the LLM to write a joke
|
||||
const prompt = `Write your best joke about ${event.data}. Write the joke between <joke> and </joke> tags.`;
|
||||
const response = await llm.complete({ prompt });
|
||||
return new StopEvent(response.text);
|
||||
};
|
||||
|
||||
// Parse the joke from the response
|
||||
const joke = response.text.match(/<joke>([\s\S]*?)<\/joke>/)?.[1]?.trim() ?? response.text;
|
||||
return jokeEvent.with({ joke: joke });
|
||||
});
|
||||
```
|
||||
|
||||
Here, we have our second and last step in the workflow. We know it's the last step because the special `StopEvent` is returned. When the workflow encounters a returned `StopEvent`, it immediately stops the workflow and returns the result. Note that we're using the generic type `StopEvent<string>` to indicate that we're returning a string.
|
||||
|
||||
Add this step to the workflow:
|
||||
2. The second handler handles the `jokeEvent`, critiques the joke, and either:
|
||||
- Emits a `critiqueEvent` if the joke needs improvement
|
||||
- Emits a `resultEvent` if the joke is good enough
|
||||
|
||||
```typescript
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
critiqueJoke
|
||||
);
|
||||
jokeFlow.handle([jokeEvent], async (event) => {
|
||||
// Prompt the LLM to critique the joke
|
||||
const prompt = `Give a thorough critique of the following joke. If the joke needs improvement, put "IMPROVE" somewhere in the critique: ${event.data.joke}`;
|
||||
const response = await llm.complete({ prompt });
|
||||
|
||||
// If the critique includes "IMPROVE", keep iterating, else, return the result
|
||||
if (response.text.includes("IMPROVE")) {
|
||||
return critiqueEvent.with({ joke: event.data.joke, critique: response.text });
|
||||
}
|
||||
|
||||
return resultEvent.with({ joke: event.data.joke, critique: response.text });
|
||||
});
|
||||
```
|
||||
|
||||
3. The third handler processes the `critiqueEvent`, generates an improved joke based on the critique, and either:
|
||||
- Loops back to the joke evaluation (if under the iteration limit)
|
||||
- Emits the final `resultEvent` (if iteration limit reached)
|
||||
|
||||
```typescript
|
||||
jokeFlow.handle([critiqueEvent], async (event) => {
|
||||
// Keep track of the number of iterations
|
||||
const store = jokeFlow.getStore();
|
||||
store.numIterations++;
|
||||
|
||||
// Write a new joke based on the previous joke and critique
|
||||
const prompt = `Write a new joke based on the following critique and the original joke. Write the joke between <joke> and </joke> tags.\n\nJoke: ${event.data.joke}\n\nCritique: ${event.data.critique}`;
|
||||
const response = await llm.complete({ prompt });
|
||||
|
||||
// Parse the joke from the response
|
||||
const joke = response.text.match(/<joke>([\s\S]*?)<\/joke>/)?.[1]?.trim() ?? response.text;
|
||||
|
||||
// If we've done less than the max number of iterations, keep iterating
|
||||
// else, return the result
|
||||
if (store.numIterations < store.maxIterations) {
|
||||
return jokeEvent.with({ joke: joke });
|
||||
}
|
||||
|
||||
return resultEvent.with({ joke: joke, critique: event.data.critique });
|
||||
});
|
||||
```
|
||||
|
||||
### Running the Workflow
|
||||
|
||||
```typescript
|
||||
const result = await jokeFlow.run("pirates");
|
||||
console.log(result.data.result);
|
||||
```
|
||||
async function main() {
|
||||
const { stream, sendEvent } = jokeFlow.createContext();
|
||||
sendEvent(startEvent.with("pirates"));
|
||||
|
||||
Lastly, we run the workflow. The `.run()` method is async, so we use await here to wait for the result.
|
||||
let result: { joke: string, critique: string } | undefined;
|
||||
|
||||
## Working with Shared Context/State
|
||||
|
||||
Optionally, you can choose to use a shared context between steps by specifying a context type when creating the workflow. Here's an example where multiple steps access a shared state:
|
||||
|
||||
```typescript
|
||||
import { HandlerContext } from "llamaindex";
|
||||
|
||||
type MyContextData = {
|
||||
query: string;
|
||||
intermediateResults: any[];
|
||||
for await (const event of stream) {
|
||||
// console.log(event.data); optionally log the event data
|
||||
if (resultEvent.include(event)) {
|
||||
result = event.data;
|
||||
break; // Stop when we get the final result
|
||||
}
|
||||
}
|
||||
|
||||
console.log(result);
|
||||
}
|
||||
|
||||
const query = async (context: HandlerContext<MyContextData>, ev: MyEvent) => {
|
||||
// get the query from the context
|
||||
const query = context.data.query;
|
||||
// do something with context and event
|
||||
const val = ...
|
||||
// store in context
|
||||
context.data.intermediateResults.push(val);
|
||||
|
||||
return new StopEvent({ result });
|
||||
};
|
||||
```
|
||||
|
||||
## Waiting for Multiple Events
|
||||
To run the workflow, we:
|
||||
1. Create a workflow context with `createContext()`
|
||||
2. Trigger the initial event with `sendEvent()`
|
||||
3. Listen to the event stream and process events as they arrive
|
||||
4. Use `include()` to check if an event is of a specific type
|
||||
5. Break the loop when we receive our final result
|
||||
|
||||
The context does more than just hold data, it also provides utilities to buffer and wait for multiple events.
|
||||
### Using Stream Utilities
|
||||
|
||||
For example, you might have a step that waits for a query and retrieved nodes before synthesizing a response:
|
||||
Workflows provide utility functions to make working with event streams easier:
|
||||
|
||||
```typescript
|
||||
const synthesize = async (context: Context, ev1: QueryEvent, ev2: RetrieveEvent) => {
|
||||
const subPrompts = [`Answer this query using the context provided: ${ev1.data.query}`, `Context: ${ev2.data.context}`];
|
||||
const prompt = subPrompts.join("\n");
|
||||
const response = await llm.complete({ prompt });
|
||||
return new StopEvent({ result: response.text });
|
||||
};
|
||||
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 } = jokeFlow.createContext();
|
||||
sendEvent(startEvent.with("pirates"));
|
||||
|
||||
// Collect all events until we get a resultEvent
|
||||
const allEvents = await collect(until(stream, resultEvent));
|
||||
|
||||
// The last event will be the resultEvent
|
||||
const finalEvent = allEvents[allEvents.length - 1];
|
||||
console.log(finalEvent.data); // Output the joke and critique
|
||||
```
|
||||
|
||||
Passing multiple events, we can buffer and wait for ALL expected events to arrive. The receiving step function will only be called once all events have arrived.
|
||||
The stream utilities make it easier to work with the asynchronous event flow. In this example, we use:
|
||||
- `collect`: Aggregates all events into an array
|
||||
- `until`: Creates a stream that emits events until a condition is met (in this case, until a resultEvent is received)
|
||||
|
||||
## Manually Triggering Events
|
||||
You can combine these utilities with other stream operators like `filter` and `map` to create powerful processing pipelines.
|
||||
|
||||
Normally, events are triggered by returning another event during a step. However, events can also be manually dispatched using the `ctx.sendEvent(event)` method within a workflow.
|
||||
## Next Steps
|
||||
|
||||
## Examples
|
||||
|
||||
You can find many useful examples of using workflows in the [examples folder](https://github.com/run-llama/LlamaIndexTS/blob/main/examples/workflow).
|
||||
To learn more about workflows, check out [the documentation in the tutorial section](../../../llamaflow).
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^5.4.16",
|
||||
"vite-plugin-wasm": "^3.3.0"
|
||||
"vite": "^6.3.3",
|
||||
"vite-plugin-wasm": "^3.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@llamaindex/cloud": "workspace:*"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"@size-limit/preset-big-lib": "^11.1.6",
|
||||
"size-limit": "^11.1.6",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^5.4.16"
|
||||
"vite": "^6.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"llamaindex": "workspace:*"
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# examples
|
||||
|
||||
## 0.3.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [82d4b46]
|
||||
- @llamaindex/server@0.1.6
|
||||
- @llamaindex/tools@0.0.7
|
||||
|
||||
## 0.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [294f502]
|
||||
- @llamaindex/tools@0.0.6
|
||||
|
||||
## 0.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -9,6 +9,12 @@ async function main() {
|
||||
args: ["-y", "@modelcontextprotocol/server-filesystem", "."],
|
||||
verbose: true,
|
||||
});
|
||||
// You can also connect to the MCP server using SSE
|
||||
// See: https://modelcontextprotocol.io/docs/concepts/transports#server-sent-events-sse
|
||||
// const server = mcp({
|
||||
// url: "http://localhost:8000/mcp",
|
||||
// verbose: true,
|
||||
// });
|
||||
|
||||
try {
|
||||
// Create an agent that uses the MCP tools
|
||||
@@ -21,9 +27,7 @@ async function main() {
|
||||
});
|
||||
|
||||
// Run a task
|
||||
const response = await myAgent.run(
|
||||
"what are the files in the current directory?",
|
||||
);
|
||||
const response = await myAgent.run("What are the available tools?");
|
||||
|
||||
console.log("Agent response:", response.data);
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Run LlamaIndex Server with simple steps
|
||||
|
||||
1. Setup environment variables
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=<your-openai-api-key>
|
||||
```
|
||||
|
||||
2. Run the server
|
||||
|
||||
```bash
|
||||
npx tsx llamaindex-server/simple-workflow/index.ts
|
||||
```
|
||||
|
||||
3. Open the app at `http://localhost:4000` and start chatting with the agent
|
||||
|
||||

|
||||
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
@@ -0,0 +1,19 @@
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import { LlamaIndexServer } from "@llamaindex/server";
|
||||
import { weather } from "@llamaindex/tools";
|
||||
import "dotenv/config";
|
||||
import { agent } from "llamaindex";
|
||||
|
||||
const weatherAgent = agent({
|
||||
tools: [weather()],
|
||||
llm: new OpenAI({ model: "gpt-4o-mini" }),
|
||||
});
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: () => weatherAgent,
|
||||
uiConfig: {
|
||||
appTitle: "Weather Agent",
|
||||
starterQuestions: ["Ho Chi Minh city weather", "New York weather"],
|
||||
},
|
||||
port: 4000,
|
||||
}).start();
|
||||
@@ -11,7 +11,6 @@ const workflow = new Workflow<ContextData, string, string>();
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (context, startEvent) => {
|
||||
const input = startEvent.data;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@llamaindex/examples",
|
||||
"version": "0.3.10",
|
||||
"version": "0.3.12",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
@@ -50,8 +50,9 @@
|
||||
"@llamaindex/together": "^0.0.12",
|
||||
"@llamaindex/jinaai": "^0.0.12",
|
||||
"@llamaindex/perplexity": "^0.0.9",
|
||||
"@llamaindex/server": "^0.1.6",
|
||||
"@llamaindex/supabase": "^0.1.1",
|
||||
"@llamaindex/tools": "^0.0.5",
|
||||
"@llamaindex/tools": "^0.0.7",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@pinecone-database/pinecone": "^4.0.0",
|
||||
"@llamaindex/assemblyai": "^0.1.1",
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# Workflow Examples
|
||||
|
||||
These examples demonstrate LlamaIndexTS's workflow system. Check out [its documentation](https://ts.llamaindex.ai/modules/workflows) for more information.
|
||||
|
||||
## Running the Examples
|
||||
|
||||
To run the examples, make sure to run them from the parent folder called `examples`). For example, to run the joke workflow, run `npx tsx workflow/joke.ts`.
|
||||
@@ -1,157 +0,0 @@
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import {
|
||||
HandlerContext,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "llamaindex";
|
||||
|
||||
const MAX_REVIEWS = 3;
|
||||
|
||||
type Context = {
|
||||
specification: string;
|
||||
numberReviews: number;
|
||||
};
|
||||
|
||||
// Using the o1-preview model (see https://platform.openai.com/docs/guides/reasoning?reasoning-prompt-examples=coding-planning)
|
||||
const llm = new OpenAI({ model: "o1-preview", temperature: 1 });
|
||||
|
||||
// example specification from https://platform.openai.com/docs/guides/reasoning?reasoning-prompt-examples=coding-planning
|
||||
const specification = `Python app that takes user questions and looks them up in a
|
||||
database where they are mapped to answers. If there is a close match, it retrieves
|
||||
the matched answer. If there isn't, it asks the user to provide an answer and
|
||||
stores the question/answer pair in the database.`;
|
||||
|
||||
// Create custom event types
|
||||
export class MessageEvent extends WorkflowEvent<{ msg: string }> {}
|
||||
|
||||
export class CodeEvent extends WorkflowEvent<{ code: string }> {}
|
||||
|
||||
export class ReviewEvent extends WorkflowEvent<{
|
||||
review: string;
|
||||
code: string;
|
||||
}> {}
|
||||
|
||||
// Helper function to truncate long strings
|
||||
const truncate = (str: string) => {
|
||||
const MAX_LENGTH = 60;
|
||||
if (str.length <= MAX_LENGTH) return str;
|
||||
return str.slice(0, MAX_LENGTH) + "...";
|
||||
};
|
||||
|
||||
// the architect is responsible for writing the structure and the initial code based on the specification
|
||||
const architect = async (
|
||||
context: HandlerContext<Context>,
|
||||
_: StartEvent<string>,
|
||||
) => {
|
||||
const spec = context.data.specification;
|
||||
// write a message to send an update to the user
|
||||
context.sendEvent(
|
||||
new MessageEvent({
|
||||
msg: `Writing app using this specification: ${truncate(spec)}`,
|
||||
}),
|
||||
);
|
||||
const prompt = `Build an app for this specification: <spec>${spec}</spec>. Make a plan for the directory structure you'll need, then return each file in full. Don't supply any reasoning, just code.`;
|
||||
const code = await llm.complete({ prompt });
|
||||
return new CodeEvent({ code: code.text });
|
||||
};
|
||||
|
||||
// the coder is responsible for updating the code based on the review
|
||||
const coder = async (context: HandlerContext<Context>, ev: ReviewEvent) => {
|
||||
// get the specification from the context
|
||||
const spec = context.data.specification;
|
||||
// get the latest review and code
|
||||
const { review, code } = ev.data;
|
||||
// write a message to send an update to the user
|
||||
context.sendEvent(
|
||||
new MessageEvent({
|
||||
msg: `Update code based on review: ${truncate(review)}`,
|
||||
}),
|
||||
);
|
||||
const prompt = `We need to improve code that should implement this specification: <spec>${spec}</spec>. Here is the current code: <code>${code}</code>. And here is a review of the code: <review>${review}</review>. Improve the code based on the review, keep the specification in mind, and return the full updated code. Don't supply any reasoning, just code.`;
|
||||
const updatedCode = await llm.complete({ prompt });
|
||||
return new CodeEvent({ code: updatedCode.text });
|
||||
};
|
||||
|
||||
// the reviewer is responsible for reviewing the code and providing feedback
|
||||
const reviewer = async (context: HandlerContext<Context>, ev: CodeEvent) => {
|
||||
// get the specification from the context
|
||||
const spec = context.data.specification;
|
||||
// get latest code from the event
|
||||
const { code } = ev.data;
|
||||
// update and check the number of reviews
|
||||
context.data.numberReviews++;
|
||||
if (context.data.numberReviews > MAX_REVIEWS) {
|
||||
// the we've done this too many times - return the code
|
||||
context.sendEvent(
|
||||
new MessageEvent({
|
||||
msg: `Already reviewed ${
|
||||
context.data.numberReviews - 1
|
||||
} times, stopping!`,
|
||||
}),
|
||||
);
|
||||
return new StopEvent({ result: code });
|
||||
}
|
||||
// write a message to send an update to the user
|
||||
context.sendEvent(
|
||||
new MessageEvent({
|
||||
msg: `Review #${context.data.numberReviews}: ${truncate(code)}`,
|
||||
}),
|
||||
);
|
||||
const prompt = `Review this code: <code>${code}</code>. Check if the code quality and whether it correctly implements this specification: <spec>${spec}</spec>. If you're satisfied, just return 'Looks great', nothing else. If not, return a review with a list of changes you'd like to see.`;
|
||||
const review = (await llm.complete({ prompt })).text;
|
||||
if (review.includes("Looks great")) {
|
||||
// the reviewer is satisfied with the code, let's return the review
|
||||
context.sendEvent(
|
||||
new MessageEvent({
|
||||
msg: `Reviewer says: ${review}`,
|
||||
}),
|
||||
);
|
||||
return new StopEvent({ result: code });
|
||||
}
|
||||
|
||||
return new ReviewEvent({ review, code });
|
||||
};
|
||||
|
||||
const codeAgent = new Workflow<Context, string, string>();
|
||||
codeAgent.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [CodeEvent],
|
||||
},
|
||||
architect,
|
||||
);
|
||||
codeAgent.addStep(
|
||||
{
|
||||
inputs: [ReviewEvent],
|
||||
outputs: [CodeEvent],
|
||||
},
|
||||
coder,
|
||||
);
|
||||
codeAgent.addStep(
|
||||
{
|
||||
inputs: [CodeEvent],
|
||||
outputs: [ReviewEvent, StopEvent],
|
||||
},
|
||||
reviewer,
|
||||
);
|
||||
|
||||
// Usage
|
||||
async function main() {
|
||||
const run = codeAgent.run(specification).with({
|
||||
specification,
|
||||
numberReviews: 0,
|
||||
});
|
||||
for await (const event of run) {
|
||||
if (event instanceof MessageEvent) {
|
||||
const msg = (event as MessageEvent).data.msg;
|
||||
console.log(`${msg}\n`);
|
||||
} else if (event instanceof StopEvent) {
|
||||
const result = (event as StopEvent<string>).data;
|
||||
console.log("Final code:\n", result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,88 +0,0 @@
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import {
|
||||
HandlerContext,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "llamaindex";
|
||||
|
||||
// Create LLM instance
|
||||
const llm = new OpenAI();
|
||||
|
||||
// Create custom event types
|
||||
export class JokeEvent extends WorkflowEvent<{ joke: string }> {}
|
||||
|
||||
export class CritiqueEvent extends WorkflowEvent<{ critique: string }> {}
|
||||
|
||||
export class AnalysisEvent extends WorkflowEvent<{ analysis: string }> {}
|
||||
|
||||
const generateJoke = async (_: unknown, ev: StartEvent<string>) => {
|
||||
const prompt = `Write your best joke about ${ev.data}.`;
|
||||
const response = await llm.complete({ prompt });
|
||||
return new JokeEvent({ joke: response.text });
|
||||
};
|
||||
|
||||
const critiqueJoke = async (_: unknown, ev: JokeEvent) => {
|
||||
const prompt = `Give a thorough critique of the following joke: ${ev.data.joke}`;
|
||||
const response = await llm.complete({ prompt });
|
||||
return new CritiqueEvent({ critique: response.text });
|
||||
};
|
||||
|
||||
const analyzeJoke = async (_: unknown, ev: JokeEvent) => {
|
||||
const prompt = `Give a thorough analysis of the following joke: ${ev.data.joke}`;
|
||||
const response = await llm.complete({ prompt });
|
||||
return new AnalysisEvent({ analysis: response.text });
|
||||
};
|
||||
|
||||
const reportJoke = async (
|
||||
context: HandlerContext,
|
||||
ev1: AnalysisEvent,
|
||||
ev2: CritiqueEvent,
|
||||
) => {
|
||||
const subPrompts = [ev1.data.analysis, ev2.data.critique];
|
||||
|
||||
const prompt = `Based on the following information about a joke:\n${subPrompts.join(
|
||||
"\n",
|
||||
)}\nProvide a comprehensive report on the joke's quality and impact.`;
|
||||
const response = await llm.complete({ prompt });
|
||||
return new StopEvent(response.text);
|
||||
};
|
||||
|
||||
const jokeFlow = new Workflow<unknown, string, string>();
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [JokeEvent],
|
||||
},
|
||||
generateJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent],
|
||||
outputs: [CritiqueEvent],
|
||||
},
|
||||
critiqueJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent],
|
||||
outputs: [AnalysisEvent],
|
||||
},
|
||||
analyzeJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [AnalysisEvent, CritiqueEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
reportJoke,
|
||||
);
|
||||
|
||||
// Usage
|
||||
async function main() {
|
||||
const result = await jokeFlow.run("pirates");
|
||||
console.log(result.data);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,44 +0,0 @@
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import { StartEvent, StopEvent, Workflow, WorkflowEvent } from "llamaindex";
|
||||
|
||||
// Create LLM instance
|
||||
const llm = new OpenAI();
|
||||
|
||||
// Create a custom event type
|
||||
export class JokeEvent extends WorkflowEvent<{ joke: string }> {}
|
||||
|
||||
const generateJoke = async (_: unknown, ev: StartEvent<string>) => {
|
||||
const prompt = `Write your best joke about ${ev.data}.`;
|
||||
const response = await llm.complete({ prompt });
|
||||
return new JokeEvent({ joke: response.text });
|
||||
};
|
||||
|
||||
const critiqueJoke = async (_: unknown, ev: JokeEvent) => {
|
||||
const prompt = `Give a thorough critique of the following joke: ${ev.data.joke}`;
|
||||
const response = await llm.complete({ prompt });
|
||||
return new StopEvent(response.text);
|
||||
};
|
||||
|
||||
const jokeFlow = new Workflow<unknown, string, string>();
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [JokeEvent],
|
||||
},
|
||||
generateJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
critiqueJoke,
|
||||
);
|
||||
|
||||
// Usage
|
||||
async function main() {
|
||||
const result = await jokeFlow.run("pirates");
|
||||
console.log(result.data);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,66 +0,0 @@
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import {
|
||||
HandlerContext,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "llamaindex";
|
||||
|
||||
// Create LLM instance
|
||||
const llm = new OpenAI();
|
||||
|
||||
// Create custom event types
|
||||
export class JokeEvent extends WorkflowEvent<{ joke: string }> {}
|
||||
|
||||
export class MessageEvent extends WorkflowEvent<{ msg: string }> {}
|
||||
|
||||
const generateJoke = async (context: HandlerContext, ev: StartEvent) => {
|
||||
context.sendEvent(
|
||||
new MessageEvent({ msg: `Generating a joke about: ${ev.data}` }),
|
||||
);
|
||||
const prompt = `Write your best joke about ${ev.data}.`;
|
||||
const response = await llm.complete({ prompt });
|
||||
return new JokeEvent({ joke: response.text });
|
||||
};
|
||||
|
||||
const critiqueJoke = async (context: HandlerContext, ev: JokeEvent) => {
|
||||
context.sendEvent(
|
||||
new MessageEvent({ msg: `Write a critique of this joke: ${ev.data.joke}` }),
|
||||
);
|
||||
const prompt = `Give a thorough critique of the following joke: ${ev.data.joke}`;
|
||||
const response = await llm.complete({ prompt });
|
||||
return new StopEvent(response.text);
|
||||
};
|
||||
|
||||
const jokeFlow = new Workflow();
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [JokeEvent],
|
||||
},
|
||||
generateJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
critiqueJoke,
|
||||
);
|
||||
|
||||
// Usage
|
||||
async function main() {
|
||||
const run = jokeFlow.run("pirates");
|
||||
for await (const event of run) {
|
||||
if (event instanceof MessageEvent) {
|
||||
console.log("Message:");
|
||||
console.log((event as MessageEvent).data.msg);
|
||||
} else if (event instanceof StopEvent) {
|
||||
console.log("Result:");
|
||||
console.log((event as StopEvent<string>).data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,48 +0,0 @@
|
||||
import { StartEvent, StopEvent, Workflow } from "llamaindex";
|
||||
|
||||
const longRunning = async (_: unknown, ev: StartEvent<string>) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for 2 seconds
|
||||
return new StopEvent("We waited 2 seconds");
|
||||
};
|
||||
|
||||
async function timeout() {
|
||||
const workflow = new Workflow<unknown, string, string>({
|
||||
timeout: 1,
|
||||
});
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
longRunning,
|
||||
);
|
||||
try {
|
||||
await workflow.run("Let's start");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function notimeout() {
|
||||
// Increase timeout to 3 seconds - no timeout
|
||||
const workflow = new Workflow<unknown, string, string>({
|
||||
timeout: 3,
|
||||
});
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
longRunning,
|
||||
);
|
||||
const result = await workflow.run("Let's start");
|
||||
console.log(result.data);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await timeout();
|
||||
console.log("---");
|
||||
await notimeout();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,73 +0,0 @@
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import { StartEvent, StopEvent, Workflow, WorkflowEvent } from "llamaindex";
|
||||
|
||||
// Create LLM instance
|
||||
const llm = new OpenAI();
|
||||
|
||||
// Create a custom event type
|
||||
export class JokeEvent extends WorkflowEvent<{ joke: string }> {}
|
||||
|
||||
const generateJoke = async (_: unknown, ev: StartEvent<string>) => {
|
||||
const prompt = `Write your best joke about ${ev.data}.`;
|
||||
const response = await llm.complete({ prompt });
|
||||
return new JokeEvent({ joke: response.text });
|
||||
};
|
||||
|
||||
const critiqueJoke = async (_: unknown, ev: JokeEvent) => {
|
||||
const prompt = `Give a thorough critique of the following joke: ${ev.data.joke}`;
|
||||
const response = await llm.complete({ prompt });
|
||||
return new StopEvent(response.text);
|
||||
};
|
||||
|
||||
async function validateFails() {
|
||||
try {
|
||||
const jokeFlow = new Workflow();
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
// @ts-expect-error outputs should be JokeEvent
|
||||
generateJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
critiqueJoke,
|
||||
);
|
||||
await jokeFlow.run("pirates").strict();
|
||||
} catch (e) {
|
||||
console.error("Validation failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
async function validate() {
|
||||
const jokeFlow = new Workflow();
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [JokeEvent],
|
||||
},
|
||||
generateJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
critiqueJoke,
|
||||
);
|
||||
const result = await jokeFlow.run("pirates").strict();
|
||||
console.log(result.data);
|
||||
}
|
||||
|
||||
// Usage
|
||||
async function main() {
|
||||
await validateFails();
|
||||
console.log("---");
|
||||
await validate();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,5 +1,11 @@
|
||||
# @llamaindex/server
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 82d4b46: feat: re-add supports for artifacts
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ChatSection as ChatSectionUI } from "@llamaindex/chat-ui";
|
||||
import "@llamaindex/chat-ui/styles/markdown.css";
|
||||
import "@llamaindex/chat-ui/styles/pdf.css";
|
||||
import { useChat } from "ai/react";
|
||||
import { Sparkles, Star } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { RenderingErrors } from "./rendering-errors";
|
||||
import { Button } from "./ui/button";
|
||||
import CustomChatInput from "./ui/chat/chat-input";
|
||||
import CustomChatMessages from "./ui/chat/chat-messages";
|
||||
import { fetchComponentDefinitions } from "./ui/chat/custom/events/loader";
|
||||
import { ComponentDef } from "./ui/chat/custom/events/types";
|
||||
import { getConfig } from "./ui/lib/utils";
|
||||
|
||||
export default function ChatSection() {
|
||||
const [componentDefs, setComponentDefs] = useState<ComponentDef[]>([]);
|
||||
const [errors, setErrors] = useState<string[]>([]); // contain all errors when compiling with Babel and runtime
|
||||
|
||||
const appendError = (error: string) => {
|
||||
setErrors((prev) => [...prev, error]);
|
||||
};
|
||||
|
||||
const uniqueErrors = useMemo(() => {
|
||||
return Array.from(new Set(errors));
|
||||
}, [errors]);
|
||||
|
||||
// fetch component definitions and use Babel to tranform JSX code to JS code
|
||||
// this is triggered only once when the page is initialised
|
||||
useEffect(() => {
|
||||
fetchComponentDefinitions().then(({ components, errors }) => {
|
||||
setComponentDefs(components);
|
||||
if (errors.length > 0) {
|
||||
setErrors((prev) => [...prev, ...errors]);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handler = useChat({
|
||||
api: getConfig("CHAT_API"),
|
||||
onError: (error: unknown) => {
|
||||
if (!(error instanceof Error)) throw error;
|
||||
let errorMessage: string;
|
||||
try {
|
||||
errorMessage = JSON.parse(error.message).detail;
|
||||
} catch (e) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
alert(errorMessage);
|
||||
},
|
||||
experimental_throttle: 100,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<div className="grid h-screen w-screen grid-cols-4 gap-4 overflow-hidden">
|
||||
<div className="col-span-1">
|
||||
<div className="flex flex-col gap-8 p-2 pl-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="size-4" />
|
||||
<h1 className="font-semibold">{getConfig("APP_TITLE")}</h1>
|
||||
</div>
|
||||
<RenderingErrors
|
||||
uniqueErrors={uniqueErrors}
|
||||
clearErrors={() => setErrors([])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 h-full min-h-0">
|
||||
<ChatSectionUI handler={handler} className="p-0">
|
||||
<CustomChatMessages
|
||||
componentDefs={componentDefs}
|
||||
appendError={appendError}
|
||||
/>
|
||||
<CustomChatInput />
|
||||
</ChatSectionUI>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<LlamaIndexLinks />
|
||||
</div>
|
||||
</div>
|
||||
<TailwindCDNInjection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function LlamaIndexLinks() {
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-4 p-2 pr-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href="https://www.llamaindex.ai/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
Built by LlamaIndex
|
||||
</a>
|
||||
<img
|
||||
className="h-[24px] w-[24px] rounded-sm"
|
||||
src="/llama.png"
|
||||
alt="Llama Logo"
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
href="https://github.com/run-llama/LlamaIndexTS"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button variant="outline" size="sm">
|
||||
<Star className="mr-2 size-4" />
|
||||
Star on GitHub
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||
* so adding these compatibility styles to make sure everything still
|
||||
* looks the same as it did with Tailwind CSS v3.
|
||||
*/
|
||||
const tailwindConfig = `
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function TailwindCDNInjection() {
|
||||
if (!getConfig("COMPONENTS_API")) return null;
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
async
|
||||
src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"
|
||||
></script>
|
||||
<style type="text/tailwindcss">{tailwindConfig}</style>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { ChatCanvas, useChatCanvas } from "@llamaindex/chat-ui";
|
||||
import { ResizableHandle, ResizablePanel } from "../../resizable";
|
||||
import { CodeArtifactRenderer } from "./preview";
|
||||
|
||||
export function ChatCanvasPanel() {
|
||||
const { displayedArtifact, isCanvasOpen } = useChatCanvas();
|
||||
if (!displayedArtifact || !isCanvasOpen) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={60} minSize={50}>
|
||||
<ChatCanvas className="w-full">
|
||||
<ChatCanvas.CodeArtifact
|
||||
tabs={{ preview: <CodeArtifactRenderer /> }}
|
||||
/>
|
||||
<ChatCanvas.DocumentArtifact />
|
||||
</ChatCanvas>
|
||||
</ResizablePanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
"use client";
|
||||
|
||||
import { CodeArtifact, useChatCanvas } from "@llamaindex/chat-ui";
|
||||
import { Loader2, WandSparkles } from "lucide-react";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "../../accordion";
|
||||
import { buttonVariants } from "../../button";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { DynamicComponentErrorBoundary } from "../custom/events/error-boundary";
|
||||
import { parseComponent } from "../custom/events/loader";
|
||||
|
||||
const SUPPORTED_FRONTEND_PREVIEW = [
|
||||
"js",
|
||||
"ts",
|
||||
"jsx",
|
||||
"tsx",
|
||||
"javascript",
|
||||
"typescript",
|
||||
];
|
||||
|
||||
export function CodeArtifactRenderer() {
|
||||
const { displayedArtifact } = useChatCanvas();
|
||||
|
||||
if (displayedArtifact?.type !== "code") return null;
|
||||
const codeArtifact = displayedArtifact as CodeArtifact;
|
||||
|
||||
if (!SUPPORTED_FRONTEND_PREVIEW.includes(codeArtifact.data.language)) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center gap-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
Preview is not supported for this language
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <CodeArtifactRendererComp artifact={codeArtifact} />;
|
||||
}
|
||||
|
||||
function CodeArtifactRendererComp({ artifact }: { artifact: CodeArtifact }) {
|
||||
const { appendErrors } = useChatCanvas();
|
||||
const [isRendering, setIsRendering] = useState(true);
|
||||
const [component, setComponent] = useState<FunctionComponent | null>(null);
|
||||
|
||||
const {
|
||||
data: { code, file_name },
|
||||
} = artifact;
|
||||
|
||||
useEffect(() => {
|
||||
const renderComponent = async () => {
|
||||
setIsRendering(true);
|
||||
const { component: parsedComponent, error } = await parseComponent(
|
||||
code,
|
||||
file_name,
|
||||
);
|
||||
|
||||
if (error) {
|
||||
setComponent(null);
|
||||
appendErrors(artifact, [error]);
|
||||
} else {
|
||||
setComponent(() => parsedComponent);
|
||||
}
|
||||
|
||||
setIsRendering(false);
|
||||
};
|
||||
|
||||
renderComponent();
|
||||
}, [artifact]);
|
||||
|
||||
if (isRendering) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center gap-2">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
<p className="text-sm text-gray-500">Rendering Artifact...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!component) {
|
||||
return <CodeErrors artifact={artifact} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DynamicComponentErrorBoundary
|
||||
onError={(error) => appendErrors(artifact, [error])}
|
||||
>
|
||||
{React.createElement(component)}
|
||||
</DynamicComponentErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function CodeErrors({ artifact }: { artifact: CodeArtifact }) {
|
||||
const { getCodeErrors, fixCodeErrors } = useChatCanvas();
|
||||
const uniqueErrors = getCodeErrors(artifact);
|
||||
|
||||
if (uniqueErrors.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-10 px-10 pt-10">
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
Error when rendering code, please check the details and try fixing them.
|
||||
</p>
|
||||
<Accordion
|
||||
type="single"
|
||||
defaultValue="errors"
|
||||
collapsible
|
||||
className="w-full rounded-xl border border-gray-100 bg-white shadow-md"
|
||||
>
|
||||
<AccordionItem value="errors" className="border-none px-4">
|
||||
<AccordionTrigger className="py-2 hover:no-underline">
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground font-bold">
|
||||
Rendering errors
|
||||
</span>
|
||||
<span className="inline-flex h-5 w-5 items-center justify-center rounded-full bg-yellow-500 text-xs text-white">
|
||||
{uniqueErrors.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default", size: "sm" }),
|
||||
"mr-2 h-8 cursor-pointer bg-gradient-to-r from-blue-500 to-purple-500 text-white hover:from-blue-600 hover:to-purple-600",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
fixCodeErrors(artifact);
|
||||
}}
|
||||
>
|
||||
<WandSparkles className="mr-2 h-4 w-4" />
|
||||
<span>Fix errors</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-4">
|
||||
<div className="space-y-2">
|
||||
{uniqueErrors.map((error, index) => (
|
||||
<p key={index} className="text-muted-foreground text-sm">
|
||||
{error}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { Sparkles, Star } from "lucide-react";
|
||||
import { Button } from "../button";
|
||||
import { getConfig } from "../lib/utils";
|
||||
|
||||
export function ChatHeader() {
|
||||
return (
|
||||
<div className="flex items-center justify-between px-4 pt-2">
|
||||
<ChatAppTitle />
|
||||
<LlamaIndexLinks />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatAppTitle() {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="size-4" />
|
||||
<h1 className="font-semibold">{getConfig("APP_TITLE")}</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LlamaIndexLinks() {
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href="https://www.llamaindex.ai/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
Built by LlamaIndex
|
||||
</a>
|
||||
<img
|
||||
className="h-[24px] w-[24px] rounded-sm"
|
||||
src="/llama.png"
|
||||
alt="Llama Logo"
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
href="https://github.com/run-llama/LlamaIndexTS"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button variant="outline" size="sm">
|
||||
<Star className="mr-2 size-4" />
|
||||
Star on GitHub
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||
* so adding these compatibility styles to make sure everything still
|
||||
* looks the same as it did with Tailwind CSS v3.
|
||||
*/
|
||||
const tailwindConfig = `
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function ChatInjection() {
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
async
|
||||
src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"
|
||||
></script>
|
||||
<style type="text/tailwindcss">{tailwindConfig}</style>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ export function ChatMessageContent({
|
||||
<ToolAnnotations />
|
||||
<ChatMessage.Content.Image />
|
||||
<DynamicEvents componentDefs={componentDefs} appendError={appendError} />
|
||||
<ChatMessage.Content.Artifact />
|
||||
<ChatMessage.Content.Markdown />
|
||||
<ChatMessage.Content.DocumentFile />
|
||||
<ChatMessage.Content.Source />
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import { ChatSection as ChatUI } from "@llamaindex/chat-ui";
|
||||
import { useChat } from "ai/react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { getConfig } from "../lib/utils";
|
||||
import { ResizablePanel, ResizablePanelGroup } from "../resizable";
|
||||
import { ChatCanvasPanel } from "./canvas/panel";
|
||||
import { ChatHeader } from "./chat-header";
|
||||
import { ChatInjection } from "./chat-injection";
|
||||
import CustomChatInput from "./chat-input";
|
||||
import CustomChatMessages from "./chat-messages";
|
||||
import { DynamicEventsErrors } from "./custom/events/dynamic-events-errors";
|
||||
import { fetchComponentDefinitions } from "./custom/events/loader";
|
||||
import { ComponentDef } from "./custom/events/types";
|
||||
|
||||
export default function ChatSection() {
|
||||
const handler = useChat({
|
||||
api: getConfig("CHAT_API"),
|
||||
onError: (error: unknown) => {
|
||||
if (!(error instanceof Error)) throw error;
|
||||
let errorMessage: string;
|
||||
try {
|
||||
errorMessage = JSON.parse(error.message).detail;
|
||||
} catch (e) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
alert(errorMessage);
|
||||
},
|
||||
experimental_throttle: 100,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-screen w-screen flex-col overflow-hidden">
|
||||
<ChatHeader />
|
||||
<ChatUI
|
||||
handler={handler}
|
||||
className="flex min-h-0 flex-1 flex-row justify-center gap-4 px-4 py-0"
|
||||
>
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<ChatSectionPanel />
|
||||
<ChatCanvasPanel />
|
||||
</ResizablePanelGroup>
|
||||
</ChatUI>
|
||||
</div>
|
||||
<ChatInjection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatSectionPanel() {
|
||||
const [componentDefs, setComponentDefs] = useState<ComponentDef[]>([]);
|
||||
const [dynamicEventsErrors, setDynamicEventsErrors] = useState<string[]>([]); // contain all errors when rendering dynamic events from componentDir
|
||||
|
||||
const appendError = (error: string) => {
|
||||
setDynamicEventsErrors((prev) => [...prev, error]);
|
||||
};
|
||||
|
||||
const uniqueErrors = useMemo(() => {
|
||||
return Array.from(new Set(dynamicEventsErrors));
|
||||
}, [dynamicEventsErrors]);
|
||||
|
||||
// fetch component definitions and use Babel to tranform JSX code to JS code
|
||||
// this is triggered only once when the page is initialised
|
||||
useEffect(() => {
|
||||
fetchComponentDefinitions().then(({ components, errors }) => {
|
||||
setComponentDefs(components);
|
||||
if (errors.length > 0) {
|
||||
setDynamicEventsErrors((prev) => [...prev, ...errors]);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ResizablePanel defaultSize={40} minSize={30} className="max-w-1/2 mx-auto">
|
||||
<div className="flex h-full min-w-0 flex-1 flex-col gap-4">
|
||||
<DynamicEventsErrors
|
||||
errors={uniqueErrors}
|
||||
clearErrors={() => setDynamicEventsErrors([])}
|
||||
/>
|
||||
<CustomChatMessages
|
||||
componentDefs={componentDefs}
|
||||
appendError={appendError}
|
||||
/>
|
||||
<CustomChatInput />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
);
|
||||
}
|
||||
+10
-10
@@ -8,18 +8,18 @@ import {
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "./ui/accordion";
|
||||
import { buttonVariants } from "./ui/button";
|
||||
import { cn } from "./ui/lib/utils";
|
||||
} from "../../../accordion";
|
||||
import { buttonVariants } from "../../../button";
|
||||
import { cn } from "../../../lib/utils";
|
||||
|
||||
export function RenderingErrors({
|
||||
uniqueErrors,
|
||||
export function DynamicEventsErrors({
|
||||
errors,
|
||||
clearErrors,
|
||||
}: {
|
||||
uniqueErrors: string[];
|
||||
errors: string[];
|
||||
clearErrors: () => void;
|
||||
}) {
|
||||
if (uniqueErrors.length === 0) return null;
|
||||
if (errors.length === 0) return null;
|
||||
return (
|
||||
<Accordion
|
||||
type="single"
|
||||
@@ -32,10 +32,10 @@ export function RenderingErrors({
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground font-bold">
|
||||
Rendering errors
|
||||
Errors when rendering dynamic events from components directory
|
||||
</span>
|
||||
<span className="inline-flex h-5 w-5 items-center justify-center rounded-full bg-yellow-500 text-xs text-white">
|
||||
{uniqueErrors.length}
|
||||
{errors.length}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -52,7 +52,7 @@ export function RenderingErrors({
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-4">
|
||||
<div className="space-y-2">
|
||||
{uniqueErrors.map((error, index) => (
|
||||
{errors.map((error, index) => (
|
||||
<p key={index} className="text-muted-foreground text-sm">
|
||||
{error}
|
||||
</p>
|
||||
@@ -122,7 +122,7 @@ export async function parseImports(code: string) {
|
||||
const importPromises = imports.map(async ({ name, source }) => {
|
||||
if (!(source in SOURCE_MAP)) {
|
||||
throw new Error(
|
||||
`Fail to import ${name} from ${source}. Reason: Module not found. \nCurrently we only support importing UI components from Shadcn components, widgets from "llamaindex/chat-ui/widgets" and icons from "lucide-react". See https://ts.llamaindex.ai/docs/llamaindex/modules/ui/llamaindex-server for more information.`,
|
||||
`Fail to import ${name} from ${source}. Reason: Module not found. \nCurrently we only support importing UI components from Shadcn components, widgets from "llamaindex/chat-ui/widgets" and icons from "lucide-react"`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
"use client";
|
||||
|
||||
import * as Babel from "@babel/standalone";
|
||||
import { FunctionComponent } from "react";
|
||||
import { getConfig } from "../../../lib/utils";
|
||||
import { parseImports } from "./import";
|
||||
import { ComponentDef, EventRenderComponent } from "./types";
|
||||
import { ComponentDef } from "./types";
|
||||
|
||||
export type SourceComponentDef = {
|
||||
type: string;
|
||||
@@ -17,11 +18,10 @@ export async function fetchComponentDefinitions(): Promise<{
|
||||
errors: string[];
|
||||
}> {
|
||||
const endpoint = getConfig("COMPONENTS_API");
|
||||
if (!endpoint)
|
||||
return {
|
||||
components: [],
|
||||
errors: ["/api/components endpoint is not defined in config"],
|
||||
};
|
||||
if (!endpoint) {
|
||||
console.warn("/api/components endpoint is not defined in config");
|
||||
return { components: [], errors: [] };
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint);
|
||||
const components = (await response.json()) as SourceComponentDef[];
|
||||
@@ -59,10 +59,10 @@ export async function fetchComponentDefinitions(): Promise<{
|
||||
}
|
||||
|
||||
// create React component from code
|
||||
async function parseComponent(
|
||||
export async function parseComponent(
|
||||
code: string,
|
||||
filename: string,
|
||||
): Promise<{ component: EventRenderComponent | null; error?: string }> {
|
||||
): Promise<{ component: FunctionComponent<any> | null; error?: string }> {
|
||||
try {
|
||||
const [transpiledCode, resolvedImports] = await Promise.all([
|
||||
transpileCode(code, filename),
|
||||
@@ -119,7 +119,7 @@ async function createComponentFromCode(
|
||||
transpiledCode: string,
|
||||
importMap: Record<string, any>,
|
||||
componentName: string | null = "Component",
|
||||
): Promise<EventRenderComponent | null> {
|
||||
): Promise<FunctionComponent<any> | null> {
|
||||
const argNames = Object.keys(importMap); // e.g., ["React", "Button", "Badge"]
|
||||
const argValues = Object.values(importMap); // list of corresponding modules
|
||||
|
||||
|
||||
@@ -5,5 +5,3 @@ export type ComponentDef = {
|
||||
type: string; // eg. deep_research_event
|
||||
comp: FunctionComponent<{ events: JSONValue[] }>;
|
||||
};
|
||||
|
||||
export type EventRenderComponent = FunctionComponent<{ events: JSONValue[] }>;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
|
||||
import "@llamaindex/chat-ui/styles/markdown.css";
|
||||
import "@llamaindex/chat-ui/styles/pdf.css";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Loader2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const ChatSection = dynamic(() => import("./components/chat-section"), {
|
||||
const ChatSection = dynamic(() => import("./components/ui/chat/chat-section"), {
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@llamaindex/server",
|
||||
"description": "LlamaIndex Server",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
@@ -62,7 +62,7 @@
|
||||
"@babel/types": "^7.27.0",
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@llama-flow/core": "^0.3.4",
|
||||
"@llamaindex/chat-ui": "0.4.0",
|
||||
"@llamaindex/chat-ui": "0.4.1",
|
||||
"@llamaindex/env": "workspace:*",
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.7",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { randomUUID } from "@llamaindex/env";
|
||||
import type { Message } from "ai";
|
||||
import {
|
||||
MetadataMode,
|
||||
WorkflowEvent,
|
||||
type Metadata,
|
||||
type NodeWithScore,
|
||||
} from "llamaindex";
|
||||
import { z } from "zod";
|
||||
|
||||
// Events that appended to stream as annotations
|
||||
export type SourceEventNode = {
|
||||
@@ -88,3 +90,128 @@ export function toAgentRunEvent(input: {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export type ArtifactType = "code" | "document";
|
||||
|
||||
export type Artifact<T = unknown> = {
|
||||
created_at: number;
|
||||
type: ArtifactType;
|
||||
data: T;
|
||||
};
|
||||
|
||||
export type CodeArtifactData = {
|
||||
file_name: string;
|
||||
code: string;
|
||||
language: string;
|
||||
};
|
||||
|
||||
export type DocumentArtifactData = {
|
||||
title: string;
|
||||
content: string;
|
||||
type: string; // markdown, html,...
|
||||
};
|
||||
|
||||
export type CodeArtifact = Artifact<CodeArtifactData> & {
|
||||
type: "code";
|
||||
};
|
||||
|
||||
export type DocumentArtifact = Artifact<DocumentArtifactData> & {
|
||||
type: "document";
|
||||
};
|
||||
|
||||
export class ArtifactEvent extends WorkflowEvent<{
|
||||
type: "artifact";
|
||||
data: Artifact;
|
||||
}> {}
|
||||
|
||||
export const codeArtifactSchema = z.object({
|
||||
type: z.literal("code"),
|
||||
data: z.object({
|
||||
file_name: z.string(),
|
||||
code: z.string(),
|
||||
language: z.string(),
|
||||
}),
|
||||
created_at: z.number(),
|
||||
});
|
||||
|
||||
export const documentArtifactSchema = z.object({
|
||||
type: z.literal("document"),
|
||||
data: z.object({
|
||||
title: z.string(),
|
||||
content: z.string(),
|
||||
type: z.string(),
|
||||
}),
|
||||
created_at: z.number(),
|
||||
});
|
||||
|
||||
export const artifactSchema = z.union([
|
||||
codeArtifactSchema,
|
||||
documentArtifactSchema,
|
||||
]);
|
||||
|
||||
export const artifactAnnotationSchema = z.object({
|
||||
type: z.literal("artifact"),
|
||||
data: artifactSchema,
|
||||
});
|
||||
|
||||
export function extractAllArtifacts(messages: Message[]): Artifact[] {
|
||||
const allArtifacts: Artifact[] = [];
|
||||
|
||||
for (const message of messages) {
|
||||
const artifacts =
|
||||
message.annotations
|
||||
?.filter(
|
||||
(annotation) =>
|
||||
artifactAnnotationSchema.safeParse(annotation).success,
|
||||
)
|
||||
.map((artifact) => artifact as Artifact) ?? [];
|
||||
|
||||
allArtifacts.push(...artifacts);
|
||||
}
|
||||
|
||||
return allArtifacts;
|
||||
}
|
||||
|
||||
export function extractLastArtifact(
|
||||
requestBody: unknown,
|
||||
type: "code",
|
||||
): CodeArtifact | undefined;
|
||||
|
||||
export function extractLastArtifact(
|
||||
requestBody: unknown,
|
||||
type: "document",
|
||||
): DocumentArtifact | undefined;
|
||||
|
||||
export function extractLastArtifact(
|
||||
requestBody: unknown,
|
||||
type?: ArtifactType,
|
||||
): Artifact | undefined;
|
||||
|
||||
export function extractLastArtifact(
|
||||
requestBody: unknown,
|
||||
type?: ArtifactType,
|
||||
): CodeArtifact | DocumentArtifact | Artifact | undefined {
|
||||
const { messages } = (requestBody as { messages?: Message[] }) ?? {};
|
||||
if (!messages) return undefined;
|
||||
|
||||
const artifacts = extractAllArtifacts(messages);
|
||||
if (!artifacts.length) return undefined;
|
||||
|
||||
if (type) {
|
||||
const lastArtifact = artifacts
|
||||
.reverse()
|
||||
.find((artifact) => artifact.type === type);
|
||||
|
||||
if (!lastArtifact) return undefined;
|
||||
|
||||
if (type === "code") {
|
||||
return lastArtifact as CodeArtifact;
|
||||
}
|
||||
|
||||
if (type === "document") {
|
||||
return lastArtifact as DocumentArtifact;
|
||||
}
|
||||
}
|
||||
|
||||
return artifacts[artifacts.length - 1];
|
||||
}
|
||||
|
||||
@@ -29,7 +29,10 @@ export const handleChat = async (
|
||||
|
||||
const stream = await runWorkflow(workflow, {
|
||||
userInput: lastMessage.content,
|
||||
chatHistory: messages.slice(0, -1) as ChatMessage[],
|
||||
chatHistory: messages.slice(0, -1).map((message) => ({
|
||||
content: message.content,
|
||||
role: message.role as ChatMessage["role"],
|
||||
})),
|
||||
});
|
||||
|
||||
pipeStreamToResponse(res, stream);
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @llamaindex/tools
|
||||
|
||||
## 0.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 82d4b46: feat: re-add supports for artifacts
|
||||
|
||||
## 0.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 294f502: Support SSE for MCP tools adapter
|
||||
|
||||
## 0.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@llamaindex/tools",
|
||||
"description": "LlamaIndex Tools",
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.7",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export * from "./tools/code-artifact-generator";
|
||||
export * from "./tools/code-generator";
|
||||
export * from "./tools/document-artifact-generator";
|
||||
export * from "./tools/document-generator";
|
||||
export * from "./tools/duckduckgo";
|
||||
export * from "./tools/form-filling";
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Settings, type JSONValue } from "@llamaindex/core/global";
|
||||
import type { ChatMessage, LLM } from "@llamaindex/core/llms";
|
||||
import { tool } from "@llamaindex/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
const CODE_ARTIFACT_GENERATION_PROMPT = `You are a highly skilled software engineer. Your task is to generate a code artifact based on the user's request.
|
||||
Follow these instructions exactly:
|
||||
1. Carefully read the user's requirements. If any details are ambiguous or missing, make reasonable assumptions and clearly reflect those in your output.
|
||||
2. For code requests:
|
||||
- If the user does not specify a framework or language, default to a React component using the Next.js framework.
|
||||
- For Next.js, use Shadcn UI components, Typescript, @types/node, @types/react, @types/react-dom, PostCSS, and TailwindCSS.
|
||||
- Please don't use inline styles or any external libraries.
|
||||
- For Shadcn ui components, you can import them like this: import { Button } from "@/components/ui/button"
|
||||
- Ensure the code is idiomatic, production-ready, and includes necessary imports.
|
||||
- Only generate code relevant to the user's request—do not add extra boilerplate.
|
||||
3. Return ONLY valid, parseable JSON in the following format. Do not include any explanations, markdown formatting, or code blocks around the JSON.
|
||||
|
||||
{
|
||||
"type": "code",
|
||||
"data": {
|
||||
"file_name": "filename.ext",
|
||||
"code": "your code here",
|
||||
"language": "programming language"
|
||||
}
|
||||
}
|
||||
|
||||
4. Your entire response must be valid JSON matching this format exactly. Do not include any explanations, markdown formatting, or code blocks around the JSON.
|
||||
---
|
||||
EXAMPLE
|
||||
{
|
||||
"type": "code",
|
||||
"data": {
|
||||
"file_name": "MyComponent.tsx",
|
||||
"code": "import React from 'react';\nexport default function MyComponent() { return <div>Hello World</div>; }",
|
||||
"language": "typescript"
|
||||
}
|
||||
}`;
|
||||
|
||||
type CodeArtifact = {
|
||||
created_at: number;
|
||||
type: "code";
|
||||
data: {
|
||||
file_name: string;
|
||||
code: string;
|
||||
language: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type CodeArtifactGeneratorToolOutput = CodeArtifact;
|
||||
|
||||
export const codeArtifactGenerator = ({
|
||||
llm,
|
||||
lastArtifact,
|
||||
}: {
|
||||
llm?: LLM;
|
||||
lastArtifact?: CodeArtifact;
|
||||
}) => {
|
||||
return tool({
|
||||
name: "code_artifact_generator",
|
||||
description: "Generate a code artifact based on the input.",
|
||||
parameters: z.object({
|
||||
requirement: z
|
||||
.string()
|
||||
.describe("The description of the code artifact you want to build."),
|
||||
}),
|
||||
execute: async ({ requirement }) => {
|
||||
const artifact = await generateCodeArtifact(
|
||||
requirement,
|
||||
lastArtifact,
|
||||
llm,
|
||||
);
|
||||
return artifact as JSONValue;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
async function generateCodeArtifact(
|
||||
requirement: string,
|
||||
lastArtifact?: CodeArtifact,
|
||||
llm?: LLM,
|
||||
): Promise<CodeArtifact | null> {
|
||||
const userMessage = `
|
||||
Generate a code artifact: ${requirement}
|
||||
${lastArtifact ? `The existing content is: \n\`\`\`${lastArtifact.data.code}\`\`\`` : ""}`;
|
||||
|
||||
const messages: ChatMessage[] = [
|
||||
{ role: "system", content: CODE_ARTIFACT_GENERATION_PROMPT },
|
||||
{ role: "user", content: userMessage },
|
||||
];
|
||||
|
||||
try {
|
||||
const response = await (llm ?? Settings.llm).chat({ messages });
|
||||
const content = response.message.content.toString();
|
||||
const jsonContent = content
|
||||
.replace(/^```json\s*|\s*```$/g, "")
|
||||
.replace(/^`+|`+$/g, "")
|
||||
.trim();
|
||||
|
||||
const parsedResponse = JSON.parse(jsonContent);
|
||||
|
||||
const artifact: CodeArtifact = {
|
||||
type: "code",
|
||||
data: parsedResponse.data,
|
||||
created_at: Date.now(),
|
||||
};
|
||||
|
||||
return artifact;
|
||||
} catch (error) {
|
||||
console.error("Failed to generate code artifact", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Settings, type JSONValue } from "@llamaindex/core/global";
|
||||
import type { ChatMessage, LLM } from "@llamaindex/core/llms";
|
||||
import { tool } from "@llamaindex/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
const DOCUMENT_ARTIFACT_GENERATION_PROMPT = `You are a highly skilled content creator. Your task is to generate a document artifact based on the user's request.
|
||||
Follow these instructions exactly:
|
||||
1. Carefully read the user's requirements. If any details are ambiguous or missing, make reasonable assumptions and clearly reflect those in your output.
|
||||
2. For document requests:
|
||||
- Always generate Markdown (.md) documents.
|
||||
- Use clear structure: headings, subheadings, lists, and tables for comparisons.
|
||||
- Ensure content is concise, well-organized, and directly addresses the user's needs.
|
||||
3. Return ONLY valid, parseable JSON in the following format. Do not include any explanations, markdown formatting, or code blocks around the JSON.
|
||||
|
||||
{
|
||||
"type": "document",
|
||||
"data": {
|
||||
"title": "Document Title",
|
||||
"content": "Markdown content here",
|
||||
"type": "markdown"
|
||||
}
|
||||
}
|
||||
|
||||
4. Your entire response must be valid JSON matching this format exactly. Do not include any explanations, markdown formatting, or code blocks around the JSON.
|
||||
---
|
||||
EXAMPLE
|
||||
{
|
||||
"type": "document",
|
||||
"data": {
|
||||
"title": "Quick Start Guide",
|
||||
"content": "# Quick Start\n\nFollow these steps to begin...",
|
||||
"type": "markdown"
|
||||
}
|
||||
}`;
|
||||
|
||||
type DocumentArtifact = {
|
||||
created_at: number;
|
||||
type: "document";
|
||||
data: {
|
||||
title: string;
|
||||
content: string;
|
||||
type: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type DocumentArtifactGeneratorToolOutput = DocumentArtifact;
|
||||
|
||||
export const documentArtifactGenerator = ({
|
||||
llm,
|
||||
lastArtifact,
|
||||
}: {
|
||||
llm?: LLM;
|
||||
lastArtifact?: DocumentArtifact;
|
||||
}) => {
|
||||
return tool({
|
||||
name: "document_artifact_generator",
|
||||
description: "Generate a document artifact based on the input.",
|
||||
parameters: z.object({
|
||||
requirement: z
|
||||
.string()
|
||||
.describe(
|
||||
"The description of the document artifact you want to build.",
|
||||
),
|
||||
}),
|
||||
execute: async ({ requirement }) => {
|
||||
const artifact = await generateDocumentArtifact(
|
||||
requirement,
|
||||
lastArtifact,
|
||||
llm,
|
||||
);
|
||||
return artifact as JSONValue;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
async function generateDocumentArtifact(
|
||||
requirement: string,
|
||||
lastArtifact?: DocumentArtifact,
|
||||
llm?: LLM,
|
||||
): Promise<DocumentArtifact | null> {
|
||||
const userMessage = `
|
||||
Generate a document artifact: ${requirement}
|
||||
${lastArtifact ? `The existing content is: \n\`\`\`${lastArtifact.data.content}\`\`\`` : ""}
|
||||
`;
|
||||
|
||||
const messages: ChatMessage[] = [
|
||||
{ role: "system", content: DOCUMENT_ARTIFACT_GENERATION_PROMPT },
|
||||
{ role: "user", content: userMessage },
|
||||
];
|
||||
|
||||
try {
|
||||
const response = await (llm ?? Settings.llm).chat({ messages });
|
||||
const content = response.message.content.toString();
|
||||
const jsonContent = content
|
||||
.replace(/^```json\s*|\s*```$/g, "")
|
||||
.replace(/^`+|`+$/g, "")
|
||||
.trim();
|
||||
|
||||
const parsedResponse = JSON.parse(jsonContent);
|
||||
|
||||
const artifact: DocumentArtifact = {
|
||||
type: "document",
|
||||
data: parsedResponse.data,
|
||||
created_at: Date.now(),
|
||||
};
|
||||
|
||||
return artifact;
|
||||
} catch (error) {
|
||||
console.error("Failed to generate document artifact", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,10 @@ import type { JSONValue } from "@llamaindex/core/global";
|
||||
import type { BaseToolWithCall } from "@llamaindex/core/llms";
|
||||
import { FunctionTool } from "@llamaindex/core/tools";
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import {
|
||||
SSEClientTransport,
|
||||
type SSEClientTransportOptions,
|
||||
} from "@modelcontextprotocol/sdk/client/sse.js";
|
||||
import {
|
||||
StdioClientTransport,
|
||||
type StdioServerParameters,
|
||||
@@ -13,7 +17,7 @@ interface ToolInput {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
type MCPClientOptions = StdioServerParameters & {
|
||||
type MCPCommonOptions = {
|
||||
/**
|
||||
* The prefix to add to the tool name
|
||||
*/
|
||||
@@ -32,11 +36,20 @@ type MCPClientOptions = StdioServerParameters & {
|
||||
verbose?: boolean;
|
||||
};
|
||||
|
||||
type StdioMCPClientOptions = StdioServerParameters & MCPCommonOptions;
|
||||
type SSEMCPClientOptions = SSEClientTransportOptions &
|
||||
MCPCommonOptions & {
|
||||
url: string;
|
||||
};
|
||||
|
||||
type MCPClientOptions = StdioMCPClientOptions | SSEMCPClientOptions;
|
||||
|
||||
class MCPClient {
|
||||
private mcp: Client;
|
||||
private transport: StdioClientTransport | null = null;
|
||||
private transport: StdioClientTransport | SSEClientTransport | null = null;
|
||||
private verbose: boolean;
|
||||
private toolNamePrefix?: string | undefined;
|
||||
private connected: boolean = false;
|
||||
|
||||
constructor(options: MCPClientOptions) {
|
||||
this.mcp = new Client({
|
||||
@@ -46,18 +59,34 @@ class MCPClient {
|
||||
|
||||
this.verbose = options.verbose ?? false;
|
||||
this.toolNamePrefix = options.toolNamePrefix;
|
||||
this.connectToSever(options);
|
||||
if ("url" in options) {
|
||||
this.transport = new SSEClientTransport(
|
||||
new URL(options.url),
|
||||
options as SSEClientTransportOptions,
|
||||
);
|
||||
} else {
|
||||
this.transport = new StdioClientTransport(
|
||||
options as StdioServerParameters,
|
||||
);
|
||||
}
|
||||
this.connected = false;
|
||||
}
|
||||
|
||||
async connectToSever(options: StdioServerParameters) {
|
||||
async connectToSever() {
|
||||
if (this.verbose) {
|
||||
console.log("Connecting to MCP server...");
|
||||
}
|
||||
this.transport = new StdioClientTransport(options);
|
||||
if (!this.transport) {
|
||||
throw new Error("Initialized with invalid options");
|
||||
}
|
||||
await this.mcp.connect(this.transport);
|
||||
this.connected = true;
|
||||
}
|
||||
|
||||
async listTools(): Promise<Tool[]> {
|
||||
private async listTools(): Promise<Tool[]> {
|
||||
if (!this.connected) {
|
||||
await this.connectToSever();
|
||||
}
|
||||
const tools = await this.mcp.listTools();
|
||||
return tools.tools;
|
||||
}
|
||||
|
||||
@@ -11,24 +11,6 @@
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"node": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs",
|
||||
"default": "./dist/index.cjs"
|
||||
},
|
||||
"workerd": {
|
||||
"types": "./dist/index.workerd.d.ts",
|
||||
"default": "./dist/index.workerd.js"
|
||||
},
|
||||
"edge-light": {
|
||||
"types": "./dist/index.edge-light.d.ts",
|
||||
"default": "./dist/index.edge-light.js"
|
||||
},
|
||||
"browser": {
|
||||
"types": "./dist/index.browser.d.ts",
|
||||
"default": "./dist/index.browser.js"
|
||||
},
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
@@ -55,14 +37,17 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@llamaindex/env": "workspace:*",
|
||||
"@llamaindex/core": "workspace:*",
|
||||
"@llamaindex/env": "workspace:*",
|
||||
"@types/node": "^22.9.0",
|
||||
"vitest": "^2.1.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@llamaindex/env": "workspace:*",
|
||||
"@llamaindex/core": "workspace:*",
|
||||
"@llamaindex/env": "workspace:*",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@llama-flow/llamaindex": "^0.0.12"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import {
|
||||
StartEvent,
|
||||
type StepContext,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "@llama-flow/llamaindex";
|
||||
import type { ChatMessage } from "@llamaindex/core/llms";
|
||||
import { ChatMemoryBuffer } from "@llamaindex/core/memory";
|
||||
import { PromptTemplate } from "@llamaindex/core/prompts";
|
||||
import { FunctionTool } from "@llamaindex/core/tools";
|
||||
import { stringifyJSONToMessageContent } from "@llamaindex/core/utils";
|
||||
import { z } from "zod";
|
||||
import { Workflow } from "../workflow";
|
||||
import type { HandlerContext, WorkflowContext } from "../workflow-context";
|
||||
import { StartEvent, StopEvent, WorkflowEvent } from "../workflow-event";
|
||||
import type { AgentWorkflowContext, BaseWorkflowAgent } from "./base";
|
||||
import {
|
||||
AgentInput,
|
||||
@@ -115,10 +119,7 @@ export class AgentWorkflow {
|
||||
private rootAgentName: string;
|
||||
|
||||
constructor({ agents, rootAgent, verbose, timeout }: AgentWorkflowParams) {
|
||||
this.workflow = new Workflow({
|
||||
verbose: verbose ?? false,
|
||||
timeout: timeout ?? 60,
|
||||
});
|
||||
this.workflow = new Workflow();
|
||||
this.verbose = verbose ?? false;
|
||||
|
||||
// Handle AgentWorkflow cases for agents
|
||||
@@ -254,7 +255,7 @@ export class AgentWorkflow {
|
||||
}
|
||||
|
||||
private handleInputStep = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ctx: StepContext<AgentWorkflowContext>,
|
||||
event: StartEvent<AgentInputData>,
|
||||
): Promise<AgentInput> => {
|
||||
const { userInput, chatHistory } = event.data;
|
||||
@@ -290,7 +291,7 @@ export class AgentWorkflow {
|
||||
};
|
||||
|
||||
private setupAgent = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ctx: StepContext<AgentWorkflowContext>,
|
||||
event: AgentInput,
|
||||
): Promise<AgentSetup> => {
|
||||
const currentAgentName = event.data.currentAgentName;
|
||||
@@ -314,9 +315,9 @@ export class AgentWorkflow {
|
||||
};
|
||||
|
||||
private runAgentStep = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ctx: StepContext<AgentWorkflowContext>,
|
||||
event: AgentSetup,
|
||||
): Promise<AgentStepEvent> => {
|
||||
) => {
|
||||
const agent = this.agents.get(event.data.currentAgentName);
|
||||
if (!agent) {
|
||||
throw new Error("No valid agent found");
|
||||
@@ -330,17 +331,19 @@ export class AgentWorkflow {
|
||||
|
||||
const output = await agent.takeStep(ctx, event.data.input, agent.tools);
|
||||
|
||||
ctx.sendEvent(output);
|
||||
ctx.sendEvent(
|
||||
new AgentStepEvent({
|
||||
agentName: agent.name,
|
||||
response: output.data.response,
|
||||
toolCalls: output.data.toolCalls,
|
||||
}),
|
||||
);
|
||||
|
||||
return new AgentStepEvent({
|
||||
agentName: agent.name,
|
||||
response: output.data.response,
|
||||
toolCalls: output.data.toolCalls,
|
||||
});
|
||||
ctx.sendEvent(output);
|
||||
};
|
||||
|
||||
private parseAgentOutput = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ctx: StepContext<AgentWorkflowContext>,
|
||||
event: AgentStepEvent,
|
||||
): Promise<ToolCallsEvent | StopEvent<{ result: string }>> => {
|
||||
const { agentName, response, toolCalls } = event.data;
|
||||
@@ -374,7 +377,7 @@ export class AgentWorkflow {
|
||||
};
|
||||
|
||||
private executeToolCalls = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ctx: StepContext<AgentWorkflowContext>,
|
||||
event: ToolCallsEvent,
|
||||
): Promise<ToolResultsEvent | StopEvent<{ result: string }>> => {
|
||||
const { agentName, toolCalls } = event.data;
|
||||
@@ -423,7 +426,7 @@ export class AgentWorkflow {
|
||||
};
|
||||
|
||||
private processToolResults = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ctx: StepContext<AgentWorkflowContext>,
|
||||
event: ToolResultsEvent,
|
||||
): Promise<AgentInput | StopEvent<{ result: string }>> => {
|
||||
const { agentName, results } = event.data;
|
||||
@@ -491,7 +494,6 @@ export class AgentWorkflow {
|
||||
this.workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<AgentInputData>],
|
||||
outputs: [AgentInput],
|
||||
},
|
||||
this.handleInputStep,
|
||||
);
|
||||
@@ -499,7 +501,6 @@ export class AgentWorkflow {
|
||||
this.workflow.addStep(
|
||||
{
|
||||
inputs: [AgentInput],
|
||||
outputs: [AgentSetup],
|
||||
},
|
||||
this.setupAgent,
|
||||
);
|
||||
@@ -507,7 +508,6 @@ export class AgentWorkflow {
|
||||
this.workflow.addStep(
|
||||
{
|
||||
inputs: [AgentSetup],
|
||||
outputs: [AgentStepEvent],
|
||||
},
|
||||
this.runAgentStep,
|
||||
);
|
||||
@@ -515,7 +515,6 @@ export class AgentWorkflow {
|
||||
this.workflow.addStep(
|
||||
{
|
||||
inputs: [AgentStepEvent],
|
||||
outputs: [ToolCallsEvent, StopEvent],
|
||||
},
|
||||
this.parseAgentOutput,
|
||||
);
|
||||
@@ -523,7 +522,6 @@ export class AgentWorkflow {
|
||||
this.workflow.addStep(
|
||||
{
|
||||
inputs: [ToolCallsEvent],
|
||||
outputs: [ToolResultsEvent, StopEvent],
|
||||
},
|
||||
this.executeToolCalls,
|
||||
);
|
||||
@@ -531,7 +529,6 @@ export class AgentWorkflow {
|
||||
this.workflow.addStep(
|
||||
{
|
||||
inputs: [ToolResultsEvent],
|
||||
outputs: [AgentInput, StopEvent],
|
||||
},
|
||||
this.processToolResults,
|
||||
);
|
||||
@@ -541,7 +538,7 @@ export class AgentWorkflow {
|
||||
|
||||
private callTool(
|
||||
toolCall: AgentToolCall,
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ctx: StepContext<AgentWorkflowContext>,
|
||||
) {
|
||||
const tool = this.agents
|
||||
.get(toolCall.data.agentName)
|
||||
@@ -563,7 +560,7 @@ export class AgentWorkflow {
|
||||
chatHistory?: ChatMessage[];
|
||||
context?: AgentWorkflowContext;
|
||||
},
|
||||
): WorkflowContext<AgentInputData, string, AgentWorkflowContext> {
|
||||
) {
|
||||
if (this.agents.size === 0) {
|
||||
throw new Error("No agents added to workflow");
|
||||
}
|
||||
@@ -577,15 +574,13 @@ export class AgentWorkflow {
|
||||
nextAgentName: null,
|
||||
};
|
||||
|
||||
const result = this.workflow.run(
|
||||
return this.workflow.run(
|
||||
{
|
||||
userInput: userInput,
|
||||
chatHistory: params?.chatHistory,
|
||||
},
|
||||
contextData,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { StepContext } from "@llama-flow/llamaindex";
|
||||
import type { BaseToolWithCall, ChatMessage, LLM } from "@llamaindex/core/llms";
|
||||
import { BaseMemory } from "@llamaindex/core/memory";
|
||||
import type { HandlerContext } from "../workflow-context";
|
||||
import type { AgentOutput, AgentToolCallResult } from "./events";
|
||||
|
||||
export type AgentWorkflowContext = {
|
||||
@@ -28,7 +28,7 @@ export interface BaseWorkflowAgent {
|
||||
* Using memory directly to get messages instead of requiring them to be passed in
|
||||
*/
|
||||
takeStep(
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ctx: StepContext<AgentWorkflowContext>,
|
||||
llmInput: ChatMessage[],
|
||||
tools: BaseToolWithCall[],
|
||||
): Promise<AgentOutput>;
|
||||
@@ -37,7 +37,7 @@ export interface BaseWorkflowAgent {
|
||||
* Handle results from tool calls
|
||||
*/
|
||||
handleToolCallResults(
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ctx: StepContext<AgentWorkflowContext>,
|
||||
results: AgentToolCallResult[],
|
||||
): Promise<void>;
|
||||
|
||||
@@ -45,7 +45,7 @@ export interface BaseWorkflowAgent {
|
||||
* Finalize the agent's output
|
||||
*/
|
||||
finalize(
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ctx: StepContext<AgentWorkflowContext>,
|
||||
output: AgentOutput,
|
||||
memory: BaseMemory,
|
||||
): Promise<AgentOutput>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { WorkflowEvent } from "@llama-flow/llamaindex";
|
||||
import type { JSONValue } from "@llamaindex/core/global";
|
||||
import type { ChatMessage, ToolResult } from "@llamaindex/core/llms";
|
||||
import { WorkflowEvent } from "../workflow-event";
|
||||
|
||||
export class AgentToolCall extends WorkflowEvent<{
|
||||
agentName: string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { StepContext } from "@llama-flow/llamaindex";
|
||||
import type { JSONObject } from "@llamaindex/core/global";
|
||||
import { Settings } from "@llamaindex/core/global";
|
||||
import {
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
type ChatResponseChunk,
|
||||
} from "@llamaindex/core/llms";
|
||||
import { BaseMemory } from "@llamaindex/core/memory";
|
||||
import type { HandlerContext } from "../workflow-context";
|
||||
import { AgentWorkflow } from "./agent-workflow";
|
||||
import { type AgentWorkflowContext, type BaseWorkflowAgent } from "./base";
|
||||
import {
|
||||
@@ -110,7 +110,7 @@ export class FunctionAgent implements BaseWorkflowAgent {
|
||||
}
|
||||
|
||||
async takeStep(
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ctx: StepContext<AgentWorkflowContext>,
|
||||
llmInput: ChatMessage[],
|
||||
tools: BaseToolWithCall[],
|
||||
): Promise<AgentOutput> {
|
||||
@@ -170,7 +170,7 @@ export class FunctionAgent implements BaseWorkflowAgent {
|
||||
}
|
||||
|
||||
async handleToolCallResults(
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ctx: StepContext<AgentWorkflowContext>,
|
||||
results: AgentToolCallResult[],
|
||||
): Promise<void> {
|
||||
const scratchpad: ChatMessage[] = ctx.data.scratchpad;
|
||||
@@ -196,7 +196,7 @@ export class FunctionAgent implements BaseWorkflowAgent {
|
||||
}
|
||||
|
||||
async finalize(
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ctx: StepContext<AgentWorkflowContext>,
|
||||
output: AgentOutput,
|
||||
memory: BaseMemory,
|
||||
): Promise<AgentOutput> {
|
||||
|
||||
@@ -1,8 +1,2 @@
|
||||
export * from "@llama-flow/llamaindex";
|
||||
export * from "./agent/index.js";
|
||||
export {
|
||||
WorkflowContext,
|
||||
type HandlerContext,
|
||||
type StepHandler,
|
||||
} from "./workflow-context.js";
|
||||
export { StartEvent, StopEvent, WorkflowEvent } from "./workflow-event.js";
|
||||
export { Workflow, type StepParameters } from "./workflow.js";
|
||||
|
||||
@@ -1,628 +0,0 @@
|
||||
import { CustomEvent, randomUUID } from "@llamaindex/env";
|
||||
import {
|
||||
type AnyWorkflowEventConstructor,
|
||||
StartEvent,
|
||||
type StartEventConstructor,
|
||||
StopEvent,
|
||||
type StopEventConstructor,
|
||||
WorkflowEvent,
|
||||
} from "./workflow-event";
|
||||
|
||||
export type StepHandler<
|
||||
Data = unknown,
|
||||
Inputs extends [
|
||||
AnyWorkflowEventConstructor | StartEventConstructor,
|
||||
...(AnyWorkflowEventConstructor | StopEventConstructor)[],
|
||||
] = [AnyWorkflowEventConstructor | StartEventConstructor],
|
||||
Out extends (AnyWorkflowEventConstructor | StopEventConstructor)[] = [],
|
||||
> = (
|
||||
context: HandlerContext<Data>,
|
||||
...events: {
|
||||
[K in keyof Inputs]: InstanceType<Inputs[K]>;
|
||||
}
|
||||
) => Promise<
|
||||
Out extends []
|
||||
? void
|
||||
: {
|
||||
[K in keyof Out]: InstanceType<Out[K]>;
|
||||
}[number]
|
||||
>;
|
||||
|
||||
export type ReadonlyStepMap<Data> = ReadonlyMap<
|
||||
StepHandler<Data, never, never>,
|
||||
{
|
||||
inputs: AnyWorkflowEventConstructor[];
|
||||
outputs: AnyWorkflowEventConstructor[];
|
||||
}
|
||||
>;
|
||||
|
||||
type GlobalEvent = typeof globalThis.Event;
|
||||
|
||||
export type Wait = () => Promise<void>;
|
||||
|
||||
export type ContextParams<Start, Stop, Data> = {
|
||||
startEvent: StartEvent<Start>;
|
||||
contextData: Data;
|
||||
steps: ReadonlyStepMap<Data>;
|
||||
timeout: number | null;
|
||||
verbose: boolean;
|
||||
wait: Wait;
|
||||
|
||||
queue: QueueProtocol[] | undefined;
|
||||
pendingInputQueue: WorkflowEvent<unknown>[] | undefined;
|
||||
resolved: StopEvent<Stop> | null | undefined;
|
||||
rejected: Error | null | undefined;
|
||||
};
|
||||
|
||||
function flattenEvents(
|
||||
acceptEventTypes: AnyWorkflowEventConstructor[],
|
||||
inputEvents: WorkflowEvent<unknown>[],
|
||||
): WorkflowEvent<unknown>[] {
|
||||
const eventMap = new Map<
|
||||
AnyWorkflowEventConstructor,
|
||||
WorkflowEvent<unknown>
|
||||
>();
|
||||
|
||||
for (const event of inputEvents) {
|
||||
for (const acceptType of acceptEventTypes) {
|
||||
if (event instanceof acceptType && !eventMap.has(acceptType)) {
|
||||
eventMap.set(acceptType, event);
|
||||
break; // Once matched, no need to check other accept types
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(eventMap.values());
|
||||
}
|
||||
|
||||
export type HandlerContext<Data = unknown> = {
|
||||
get data(): Data;
|
||||
sendEvent(event: WorkflowEvent<unknown>): void;
|
||||
requireEvent<T extends AnyWorkflowEventConstructor>(
|
||||
event: T,
|
||||
): Promise<InstanceType<T>>;
|
||||
};
|
||||
|
||||
export type QueueProtocol =
|
||||
| {
|
||||
type: "event";
|
||||
event: WorkflowEvent<unknown>;
|
||||
}
|
||||
| {
|
||||
type: "requestEvent";
|
||||
id: string;
|
||||
requestEvent: AnyWorkflowEventConstructor;
|
||||
};
|
||||
|
||||
export class WorkflowContext<Start = string, Stop = string, Data = unknown>
|
||||
implements
|
||||
AsyncIterable<WorkflowEvent<unknown>, unknown, void>,
|
||||
Promise<StopEvent<Stop>>
|
||||
{
|
||||
readonly #steps: ReadonlyStepMap<Data>;
|
||||
|
||||
readonly #startEvent: StartEvent<Start>;
|
||||
readonly #queue: QueueProtocol[] = [];
|
||||
readonly #queueEventTarget = new EventTarget();
|
||||
readonly #wait: Wait;
|
||||
|
||||
#timeout: number | null = null;
|
||||
#verbose: boolean = false;
|
||||
#data: Data;
|
||||
|
||||
#stepCache: WeakMap<
|
||||
WorkflowEvent<unknown>,
|
||||
[
|
||||
step: Set<StepHandler<Data, never, never>>,
|
||||
stepInputs: WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>,
|
||||
stepOutputs: WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>,
|
||||
]
|
||||
> = new Map();
|
||||
|
||||
#getStepFunction(
|
||||
event: WorkflowEvent<unknown>,
|
||||
): [
|
||||
step: Set<StepHandler<Data, never, never>>,
|
||||
stepInputs: WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>,
|
||||
stepOutputs: WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>,
|
||||
] {
|
||||
if (this.#stepCache.has(event)) {
|
||||
return this.#stepCache.get(event)!;
|
||||
}
|
||||
const set = new Set<StepHandler<Data, never, never>>();
|
||||
const stepInputs = new WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>();
|
||||
const stepOutputs = new WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>();
|
||||
const res: [
|
||||
step: Set<StepHandler<Data, never, never>>,
|
||||
stepInputs: WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>,
|
||||
stepOutputs: WeakMap<
|
||||
StepHandler<Data, never, never>,
|
||||
AnyWorkflowEventConstructor[]
|
||||
>,
|
||||
] = [set, stepInputs, stepOutputs];
|
||||
this.#stepCache.set(event, res);
|
||||
for (const [step, { inputs, outputs }] of this.#steps) {
|
||||
if (inputs.some((input) => event instanceof input)) {
|
||||
set.add(step);
|
||||
stepInputs.set(step, inputs);
|
||||
stepOutputs.set(step, outputs);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
constructor(params: ContextParams<Start, Stop, Data>) {
|
||||
this.#steps = params.steps;
|
||||
this.#startEvent = params.startEvent;
|
||||
if (typeof params.timeout === "number") {
|
||||
this.#timeout = params.timeout;
|
||||
}
|
||||
this.#data = params.contextData;
|
||||
this.#verbose = params.verbose ?? false;
|
||||
this.#wait = params.wait;
|
||||
|
||||
// push start event to the queue
|
||||
const [step] = this.#getStepFunction(this.#startEvent);
|
||||
if (step.size === 0) {
|
||||
throw new TypeError("No step found for start event");
|
||||
}
|
||||
|
||||
// restore from snapshot
|
||||
if (params.queue) {
|
||||
params.queue.forEach((protocol) => {
|
||||
this.#queue.push(protocol);
|
||||
});
|
||||
} else {
|
||||
this.#sendEvent(this.#startEvent);
|
||||
}
|
||||
if (params.pendingInputQueue) {
|
||||
this.#pendingInputQueue = params.pendingInputQueue;
|
||||
}
|
||||
if (params.resolved) {
|
||||
this.#resolved = params.resolved;
|
||||
}
|
||||
if (params.rejected) {
|
||||
this.#rejected = params.rejected;
|
||||
}
|
||||
}
|
||||
|
||||
// make sure it will only be called once
|
||||
#iterator: AsyncIterableIterator<WorkflowEvent<unknown>> | null = null;
|
||||
#signal: AbortSignal | null = null;
|
||||
|
||||
get #iteratorSingleton(): AsyncIterableIterator<WorkflowEvent<unknown>> {
|
||||
if (this.#iterator === null) {
|
||||
this.#iterator = this.#createStreamEvents();
|
||||
}
|
||||
return this.#iterator;
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator](): AsyncIterableIterator<WorkflowEvent<unknown>> {
|
||||
return this.#iteratorSingleton;
|
||||
}
|
||||
|
||||
#sendEvent = (event: WorkflowEvent<unknown>): void => {
|
||||
this.#queue.push({
|
||||
type: "event",
|
||||
event,
|
||||
});
|
||||
};
|
||||
|
||||
#requireEvent = async <T extends AnyWorkflowEventConstructor>(
|
||||
event: T,
|
||||
): Promise<InstanceType<T>> => {
|
||||
const requestId = randomUUID();
|
||||
this.#queue.push({
|
||||
type: "requestEvent",
|
||||
id: requestId,
|
||||
requestEvent: event,
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
const handler = (event: InstanceType<GlobalEvent>) => {
|
||||
if (event instanceof CustomEvent) {
|
||||
const { id } = event.detail;
|
||||
if (requestId === id) {
|
||||
this.#queueEventTarget.removeEventListener("update", handler);
|
||||
resolve(event.detail.event);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.#queueEventTarget.addEventListener("update", handler);
|
||||
});
|
||||
};
|
||||
|
||||
#pendingInputQueue: WorkflowEvent<unknown>[] = [];
|
||||
|
||||
// if strict mode is enabled, it will throw an error if there's input or output events are not expected
|
||||
#strict = false;
|
||||
|
||||
strict() {
|
||||
this.#strict = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
get data(): Data {
|
||||
return this.#data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream events from the start event
|
||||
*
|
||||
* Note that this function will stop once there's no more future events,
|
||||
* if you want stop immediately once reach a StopEvent, you should handle it in the other side.
|
||||
* @private
|
||||
*/
|
||||
#createStreamEvents(): AsyncIterableIterator<WorkflowEvent<unknown>> {
|
||||
const isPendingEvents = new WeakSet<WorkflowEvent<unknown>>();
|
||||
const pendingTasks = new Set<Promise<WorkflowEvent<unknown> | void>>();
|
||||
const enqueuedEvents = new Set<WorkflowEvent<unknown>>();
|
||||
const stream = new ReadableStream<WorkflowEvent<unknown>>({
|
||||
start: async (controller) => {
|
||||
while (true) {
|
||||
const eventProtocol = this.#queue.shift();
|
||||
if (eventProtocol) {
|
||||
switch (eventProtocol.type) {
|
||||
case "requestEvent": {
|
||||
const { id, requestEvent } = eventProtocol;
|
||||
const acceptableInput = this.#pendingInputQueue.find(
|
||||
(event) => event instanceof requestEvent,
|
||||
);
|
||||
if (acceptableInput) {
|
||||
// remove the event from the queue, in case of infinite loop
|
||||
const protocolIdx = this.#queue.findIndex(
|
||||
(protocol) =>
|
||||
protocol.type === "event" &&
|
||||
protocol.event === acceptableInput,
|
||||
);
|
||||
if (protocolIdx !== -1) {
|
||||
this.#queue.splice(protocolIdx, 1);
|
||||
}
|
||||
this.#pendingInputQueue.splice(
|
||||
this.#pendingInputQueue.indexOf(acceptableInput),
|
||||
1,
|
||||
);
|
||||
this.#queueEventTarget.dispatchEvent(
|
||||
new CustomEvent("update", {
|
||||
detail: { id, event: acceptableInput },
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// push back to the queue as there are not enough events
|
||||
this.#queue.push(eventProtocol);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "event": {
|
||||
const { event } = eventProtocol;
|
||||
if (isPendingEvents.has(event)) {
|
||||
// this event is still processing
|
||||
this.#sendEvent(event);
|
||||
} else {
|
||||
if (!enqueuedEvents.has(event)) {
|
||||
controller.enqueue(event);
|
||||
enqueuedEvents.add(event);
|
||||
}
|
||||
const [steps, inputsMap, outputsMap] =
|
||||
this.#getStepFunction(event);
|
||||
const nextEventPromises: Promise<WorkflowEvent<unknown> | void>[] =
|
||||
[...steps]
|
||||
.map((step) => {
|
||||
const inputs = [...(inputsMap.get(step) ?? [])];
|
||||
const acceptableInputs: WorkflowEvent<unknown>[] =
|
||||
this.#pendingInputQueue.filter((event) =>
|
||||
inputs.some((input) => event instanceof input),
|
||||
);
|
||||
const events: WorkflowEvent<unknown>[] = flattenEvents(
|
||||
inputs,
|
||||
[event, ...acceptableInputs],
|
||||
);
|
||||
// remove the event from the queue, in case of infinite loop
|
||||
events.forEach((event) => {
|
||||
const protocolIdx = this.#queue.findIndex(
|
||||
(protocol) =>
|
||||
protocol.type === "event" &&
|
||||
protocol.event === event,
|
||||
);
|
||||
if (protocolIdx !== -1) {
|
||||
this.#queue.splice(protocolIdx, 1);
|
||||
}
|
||||
});
|
||||
if (events.length !== inputs.length) {
|
||||
if (this.#verbose) {
|
||||
console.log(
|
||||
`Not enough inputs for step ${step.name}, waiting for more events`,
|
||||
);
|
||||
}
|
||||
// not enough to run the step, push back to the queue
|
||||
this.#sendEvent(event);
|
||||
isPendingEvents.add(event);
|
||||
return null;
|
||||
}
|
||||
if (isPendingEvents.has(event)) {
|
||||
isPendingEvents.delete(event);
|
||||
}
|
||||
if (this.#verbose) {
|
||||
console.log(
|
||||
`Running step ${step.name} with inputs ${events}`,
|
||||
);
|
||||
}
|
||||
const data = this.data;
|
||||
return (step as StepHandler<Data>)
|
||||
.call(
|
||||
null,
|
||||
{
|
||||
get data() {
|
||||
return data;
|
||||
},
|
||||
sendEvent: this.#sendEvent,
|
||||
requireEvent: this.#requireEvent,
|
||||
},
|
||||
// @ts-expect-error IDK why
|
||||
...events.sort((a, b) => {
|
||||
const aIndex = inputs.indexOf(
|
||||
a.constructor as AnyWorkflowEventConstructor,
|
||||
);
|
||||
const bIndex = inputs.indexOf(
|
||||
b.constructor as AnyWorkflowEventConstructor,
|
||||
);
|
||||
return aIndex - bIndex;
|
||||
}),
|
||||
)
|
||||
.then((nextEvent: void | WorkflowEvent<unknown>) => {
|
||||
if (nextEvent === undefined) {
|
||||
return;
|
||||
}
|
||||
if (this.#verbose) {
|
||||
console.log(
|
||||
`Step ${step.name} completed, next event is ${nextEvent}`,
|
||||
);
|
||||
}
|
||||
const outputs = outputsMap.get(step) ?? [];
|
||||
if (
|
||||
!outputs.some(
|
||||
(output) => nextEvent.constructor === output,
|
||||
)
|
||||
) {
|
||||
if (this.#strict) {
|
||||
const error = Error(
|
||||
`Step ${step.name} returned an unexpected output event ${nextEvent}`,
|
||||
);
|
||||
controller.error(error);
|
||||
} else {
|
||||
console.warn(
|
||||
`Step ${step.name} returned an unexpected output event ${nextEvent}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!(nextEvent instanceof StopEvent)) {
|
||||
this.#pendingInputQueue.unshift(nextEvent);
|
||||
this.#sendEvent(nextEvent);
|
||||
}
|
||||
return nextEvent;
|
||||
});
|
||||
})
|
||||
.filter((promise) => promise !== null);
|
||||
nextEventPromises.forEach((promise) => {
|
||||
pendingTasks.add(promise);
|
||||
promise
|
||||
.catch((err) => {
|
||||
console.error("Error in step", err);
|
||||
})
|
||||
.finally(() => {
|
||||
pendingTasks.delete(promise);
|
||||
});
|
||||
});
|
||||
Promise.race(nextEventPromises)
|
||||
.then((fastestNextEvent) => {
|
||||
if (fastestNextEvent === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!enqueuedEvents.has(fastestNextEvent)) {
|
||||
controller.enqueue(fastestNextEvent);
|
||||
enqueuedEvents.add(fastestNextEvent);
|
||||
}
|
||||
return fastestNextEvent;
|
||||
})
|
||||
.then(async (fastestNextEvent) =>
|
||||
Promise.all(nextEventPromises).then((nextEvents) => {
|
||||
const events = nextEvents.filter(
|
||||
(event) => event !== undefined,
|
||||
);
|
||||
for (const nextEvent of events) {
|
||||
// do not enqueue the same event twice
|
||||
if (fastestNextEvent !== nextEvent) {
|
||||
if (!enqueuedEvents.has(nextEvent)) {
|
||||
controller.enqueue(nextEvent);
|
||||
enqueuedEvents.add(nextEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.catch((err) => {
|
||||
// when the step raise an error, should go back to the previous step
|
||||
this.#sendEvent(event);
|
||||
isPendingEvents.add(event);
|
||||
controller.error(err);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.#queue.length === 0 && pendingTasks.size === 0) {
|
||||
if (this.#verbose) {
|
||||
console.log("No more events in the queue");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
await this.#wait();
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
return stream[Symbol.asyncIterator]();
|
||||
}
|
||||
|
||||
with<Initial extends Data>(
|
||||
data: Initial,
|
||||
): WorkflowContext<Start, Stop, Initial> {
|
||||
return new WorkflowContext({
|
||||
startEvent: this.#startEvent,
|
||||
wait: this.#wait,
|
||||
contextData: data,
|
||||
steps: this.#steps,
|
||||
timeout: this.#timeout,
|
||||
verbose: this.#verbose,
|
||||
queue: this.#queue,
|
||||
pendingInputQueue: this.#pendingInputQueue,
|
||||
resolved: this.#resolved,
|
||||
rejected: this.#rejected,
|
||||
});
|
||||
}
|
||||
|
||||
// PromiseLike implementation, this is following the Promise/A+ spec
|
||||
// It will consume the iterator and resolve the promise once it reaches the StopEvent
|
||||
// If you want to customize the behavior, you can use the async iterator directly
|
||||
#resolved: StopEvent<Stop> | null = null;
|
||||
#rejected: Error | null = null;
|
||||
|
||||
async then<TResult1, TResult2 = never>(
|
||||
onfulfilled?:
|
||||
| ((value: StopEvent<Stop>) => TResult1 | PromiseLike<TResult1>)
|
||||
| null
|
||||
| undefined,
|
||||
onrejected?:
|
||||
| ((reason: unknown) => TResult2 | PromiseLike<TResult2>)
|
||||
| null
|
||||
| undefined,
|
||||
) {
|
||||
onfulfilled ??= (value) => value as TResult1;
|
||||
onrejected ??= (reason) => {
|
||||
throw reason;
|
||||
};
|
||||
if (this.#resolved !== null) {
|
||||
return Promise.resolve(this.#resolved).then(onfulfilled, onrejected);
|
||||
} else if (this.#rejected !== null) {
|
||||
return Promise.reject(this.#rejected).then(onfulfilled, onrejected);
|
||||
}
|
||||
|
||||
if (this.#timeout !== null) {
|
||||
const timeout = this.#timeout;
|
||||
this.#signal = AbortSignal.timeout(timeout * 1000);
|
||||
}
|
||||
|
||||
this.#signal?.addEventListener("abort", () => {
|
||||
this.#rejected = new Error(
|
||||
`Operation timed out after ${this.#timeout} seconds`,
|
||||
);
|
||||
onrejected?.(this.#rejected);
|
||||
});
|
||||
try {
|
||||
for await (const event of this.#iteratorSingleton) {
|
||||
if (this.#rejected !== null) {
|
||||
return onrejected?.(this.#rejected);
|
||||
}
|
||||
if (event instanceof StartEvent) {
|
||||
if (this.#verbose) {
|
||||
console.log(`Starting workflow with event ${event}`);
|
||||
}
|
||||
}
|
||||
if (event instanceof StopEvent) {
|
||||
if (this.#verbose && this.#pendingInputQueue.length > 0) {
|
||||
// fixme: #pendingInputQueue might should be cleanup correctly?
|
||||
}
|
||||
this.#resolved = event;
|
||||
return onfulfilled?.(event);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
this.#rejected = err;
|
||||
}
|
||||
return onrejected?.(err);
|
||||
}
|
||||
const nextValue = await this.#iteratorSingleton.next();
|
||||
if (nextValue.done === false) {
|
||||
this.#rejected = new Error("Workflow did not complete");
|
||||
return onrejected?.(this.#rejected);
|
||||
}
|
||||
return onrejected?.(new Error("UNREACHABLE"));
|
||||
}
|
||||
|
||||
catch<TResult = never>(
|
||||
onrejected?:
|
||||
| ((reason: unknown) => TResult | PromiseLike<TResult>)
|
||||
| null
|
||||
| undefined,
|
||||
) {
|
||||
return this.then((v) => v, onrejected);
|
||||
}
|
||||
|
||||
finally(onfinally?: (() => void) | undefined | null) {
|
||||
return this.then(
|
||||
() => {
|
||||
onfinally?.();
|
||||
},
|
||||
() => {
|
||||
onfinally?.();
|
||||
},
|
||||
) as Promise<never>;
|
||||
}
|
||||
|
||||
[Symbol.toStringTag]: string = "Context";
|
||||
|
||||
// for worker thread
|
||||
snapshot(): ArrayBuffer {
|
||||
const state = {
|
||||
startEvent: this.#startEvent,
|
||||
queue: this.#queue,
|
||||
pendingInputQueue: this.#pendingInputQueue,
|
||||
data: this.#data,
|
||||
timeout: this.#timeout,
|
||||
verbose: this.#verbose,
|
||||
resolved: this.#resolved,
|
||||
rejected: this.#rejected,
|
||||
};
|
||||
|
||||
const jsonString = JSON.stringify(state, (_, value) => {
|
||||
// If value is an instance of a class, serialize only its properties
|
||||
if (value instanceof WorkflowEvent) {
|
||||
return { data: value.data, constructor: value.constructor.name };
|
||||
}
|
||||
// value is Subtype of WorkflowEvent
|
||||
if (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
value?.prototype instanceof WorkflowEvent
|
||||
) {
|
||||
return { constructor: value.prototype.constructor.name };
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
return new TextEncoder().encode(jsonString).buffer;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
export class WorkflowEvent<Data> {
|
||||
displayName: string;
|
||||
data: Data;
|
||||
|
||||
constructor(data: Data) {
|
||||
this.data = data;
|
||||
this.displayName = this.constructor.name;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.displayName;
|
||||
}
|
||||
|
||||
static or<
|
||||
A extends AnyWorkflowEventConstructor,
|
||||
B extends AnyWorkflowEventConstructor,
|
||||
>(AEvent: A, BEvent: B): A | B {
|
||||
function OrEvent() {
|
||||
throw new Error("Cannot instantiate OrEvent");
|
||||
}
|
||||
|
||||
OrEvent.prototype = Object.create(AEvent.prototype);
|
||||
|
||||
Object.getOwnPropertyNames(BEvent.prototype).forEach((property) => {
|
||||
if (!(property in OrEvent.prototype)) {
|
||||
Object.defineProperty(
|
||||
OrEvent.prototype,
|
||||
property,
|
||||
Object.getOwnPropertyDescriptor(BEvent.prototype, property)!,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
OrEvent.prototype.constructor = OrEvent;
|
||||
|
||||
Object.defineProperty(OrEvent, Symbol.hasInstance, {
|
||||
value: function (instance: unknown) {
|
||||
return instance instanceof AEvent || instance instanceof BEvent;
|
||||
},
|
||||
});
|
||||
|
||||
return OrEvent as unknown as A | B;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type AnyWorkflowEventConstructor = new (data: any) => WorkflowEvent<any>;
|
||||
|
||||
export type StartEventConstructor<T = string> = new (data: T) => StartEvent<T>;
|
||||
export type StopEventConstructor<T = string> = new (data: T) => StopEvent<T>;
|
||||
|
||||
// These are special events that are used to control the workflow
|
||||
export class StartEvent<T = string> extends WorkflowEvent<T> {
|
||||
constructor(data: T) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
export class StopEvent<T = string> extends WorkflowEvent<T> {
|
||||
constructor(data: T) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
import {
|
||||
WorkflowContext,
|
||||
type HandlerContext,
|
||||
type QueueProtocol,
|
||||
type StepHandler,
|
||||
type Wait,
|
||||
} from "./workflow-context.js";
|
||||
import {
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
type AnyWorkflowEventConstructor,
|
||||
type StartEventConstructor,
|
||||
type StopEventConstructor,
|
||||
} from "./workflow-event.js";
|
||||
|
||||
export type StepParameters<
|
||||
In extends AnyWorkflowEventConstructor[],
|
||||
Out extends AnyWorkflowEventConstructor[],
|
||||
> = {
|
||||
inputs: In;
|
||||
outputs: Out;
|
||||
};
|
||||
|
||||
export class Workflow<ContextData, Start, Stop> {
|
||||
#steps: Map<
|
||||
StepHandler<ContextData, never, never>,
|
||||
{
|
||||
inputs: AnyWorkflowEventConstructor[];
|
||||
outputs: AnyWorkflowEventConstructor[];
|
||||
}
|
||||
> = new Map();
|
||||
#verbose: boolean = false;
|
||||
#timeout: number | null = null;
|
||||
// fixme: allow microtask
|
||||
#nextTick: Wait = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
constructor(
|
||||
params: {
|
||||
verbose?: boolean;
|
||||
timeout?: number | null;
|
||||
wait?: Wait;
|
||||
} = {},
|
||||
) {
|
||||
if (params.verbose) {
|
||||
this.#verbose = params.verbose;
|
||||
}
|
||||
if (params.timeout) {
|
||||
this.#timeout = params.timeout;
|
||||
}
|
||||
if (params.wait) {
|
||||
this.#nextTick = params.wait;
|
||||
}
|
||||
}
|
||||
|
||||
addStep<
|
||||
const In extends [
|
||||
AnyWorkflowEventConstructor | StartEventConstructor,
|
||||
...(AnyWorkflowEventConstructor | StopEventConstructor)[],
|
||||
],
|
||||
const Out extends (AnyWorkflowEventConstructor | StopEventConstructor)[],
|
||||
>(
|
||||
parameters: StepParameters<In, Out>,
|
||||
stepFn: (
|
||||
context: HandlerContext<ContextData>,
|
||||
...events: {
|
||||
[K in keyof In]: InstanceType<In[K]>;
|
||||
}
|
||||
) => Promise<
|
||||
Out extends []
|
||||
? void
|
||||
: {
|
||||
[K in keyof Out]: InstanceType<Out[K]>;
|
||||
}[number]
|
||||
>,
|
||||
): this {
|
||||
const { inputs, outputs } = parameters;
|
||||
this.#steps.set(stepFn as never, { inputs, outputs });
|
||||
return this;
|
||||
}
|
||||
|
||||
hasStep(stepFn: StepHandler): boolean {
|
||||
return this.#steps.has(stepFn);
|
||||
}
|
||||
|
||||
removeStep(stepFn: StepHandler): this {
|
||||
this.#steps.delete(stepFn);
|
||||
return this;
|
||||
}
|
||||
|
||||
run(
|
||||
event: StartEvent<Start> | Start,
|
||||
): unknown extends ContextData
|
||||
? WorkflowContext<Start, Stop, ContextData>
|
||||
: WorkflowContext<Start, Stop, ContextData | undefined>;
|
||||
run<Data extends ContextData>(
|
||||
event: StartEvent<Start> | Start,
|
||||
data: Data,
|
||||
): WorkflowContext<Start, Stop, Data>;
|
||||
run<Data extends ContextData>(
|
||||
event: StartEvent<Start> | Start,
|
||||
data?: Data,
|
||||
): WorkflowContext<Start, Stop, Data> {
|
||||
const startEvent: StartEvent<Start> =
|
||||
event instanceof StartEvent ? event : new StartEvent(event);
|
||||
|
||||
return new WorkflowContext<Start, Stop, Data>({
|
||||
startEvent,
|
||||
wait: this.#nextTick,
|
||||
contextData: data!,
|
||||
steps: new Map(this.#steps),
|
||||
timeout: this.#timeout,
|
||||
verbose: this.#verbose,
|
||||
queue: undefined,
|
||||
pendingInputQueue: undefined,
|
||||
resolved: null,
|
||||
rejected: null,
|
||||
});
|
||||
}
|
||||
|
||||
recover(data: ArrayBuffer): WorkflowContext<Start, Stop, ContextData> {
|
||||
const jsonString = new TextDecoder().decode(data);
|
||||
|
||||
const state = JSON.parse(jsonString);
|
||||
|
||||
const reconstructedStartEvent = new StartEvent<Start>(state.startEvent);
|
||||
const AllEvents = [...this.#steps]
|
||||
.map(([, { inputs, outputs }]) => [...inputs, ...(outputs ?? [])])
|
||||
.flat();
|
||||
const reconstructedQueue: QueueProtocol[] = state.queue.map(
|
||||
(protocol: QueueProtocol): QueueProtocol => {
|
||||
switch (protocol.type) {
|
||||
case "requestEvent": {
|
||||
const { requestEvent, id } = protocol;
|
||||
const EventType = AllEvents.find(
|
||||
(type) =>
|
||||
type.prototype.constructor.name ===
|
||||
(requestEvent.constructor as unknown as string),
|
||||
);
|
||||
if (!EventType) {
|
||||
throw new TypeError(
|
||||
`Event type not found: ${requestEvent.constructor}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
type: "requestEvent",
|
||||
id,
|
||||
requestEvent: EventType,
|
||||
};
|
||||
}
|
||||
case "event": {
|
||||
const { event } = protocol;
|
||||
const EventType = AllEvents.find(
|
||||
(type) =>
|
||||
type.prototype.constructor.name ===
|
||||
(event.constructor as unknown as string),
|
||||
);
|
||||
if (!EventType) {
|
||||
throw new TypeError(`Event type not found: ${event.constructor}`);
|
||||
}
|
||||
return {
|
||||
type: "event",
|
||||
event: new EventType(event.data),
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const reconstructedPendingInputQueue = state.pendingInputQueue.map(
|
||||
(event: Record<string, unknown>) => {
|
||||
const EventType = AllEvents.find(
|
||||
(type) => type.prototype.constructor.name === event.constructor,
|
||||
);
|
||||
if (!EventType) {
|
||||
throw new TypeError(`Event type not found: ${event.constructor}`);
|
||||
}
|
||||
return new EventType(event.data);
|
||||
},
|
||||
);
|
||||
|
||||
return new WorkflowContext<Start, Stop, ContextData>({
|
||||
startEvent: reconstructedStartEvent,
|
||||
contextData: state.data,
|
||||
wait: this.#nextTick,
|
||||
steps: this.#steps, // Assuming steps do not change and are part of the class prototype or similar
|
||||
timeout: state.timeout,
|
||||
verbose: state.verbose,
|
||||
queue: reconstructedQueue,
|
||||
pendingInputQueue: reconstructedPendingInputQueue,
|
||||
resolved: state.resolved ? new StopEvent<Stop>(state.resolved) : null,
|
||||
rejected: state.rejected ? new Error(state.rejected) : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -128,17 +128,17 @@ describe("AgentWorkflow", () => {
|
||||
"StartEvent",
|
||||
"AgentInput",
|
||||
"AgentSetup",
|
||||
"AgentStepEvent",
|
||||
"AgentStream",
|
||||
"AgentStepEvent",
|
||||
"AgentOutput",
|
||||
"ToolCallsEvent",
|
||||
"ToolResultsEvent",
|
||||
"AgentToolCall",
|
||||
"AgentToolCallResult",
|
||||
"ToolResultsEvent",
|
||||
"AgentInput",
|
||||
"AgentSetup",
|
||||
"AgentStepEvent",
|
||||
"AgentStream",
|
||||
"AgentStepEvent",
|
||||
"AgentOutput",
|
||||
"StopEvent",
|
||||
];
|
||||
|
||||
@@ -39,7 +39,7 @@ describe("FunctionAgent", () => {
|
||||
returnDirect: false,
|
||||
},
|
||||
displayName: "test",
|
||||
} as AgentToolCallResult;
|
||||
} as unknown as AgentToolCallResult;
|
||||
|
||||
const dummyContext = {
|
||||
data: {
|
||||
|
||||
@@ -26,6 +26,7 @@ export function setupToolCallingMockLLM(
|
||||
topP: 1,
|
||||
contextWindow: 4096,
|
||||
tokenizer: undefined,
|
||||
structuredOutput: false,
|
||||
},
|
||||
});
|
||||
mockLLM.supportToolCall = true;
|
||||
|
||||
@@ -1,972 +0,0 @@
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
expectTypeOf,
|
||||
test,
|
||||
vi,
|
||||
type Mocked,
|
||||
} from "vitest";
|
||||
import type { HandlerContext, StepHandler, StepParameters } from "../src";
|
||||
import { StartEvent, StopEvent, Workflow, WorkflowEvent } from "../src";
|
||||
|
||||
class JokeEvent extends WorkflowEvent<{ joke: string }> {}
|
||||
|
||||
class AnalysisEvent extends WorkflowEvent<{ analysis: string }> {}
|
||||
|
||||
describe("type system", () => {
|
||||
test("handler", () => {
|
||||
type Parameters = StepParameters<
|
||||
[typeof StartEvent<string>],
|
||||
[typeof StopEvent<string>]
|
||||
>;
|
||||
type Handler = (
|
||||
context: HandlerContext,
|
||||
ev: StartEvent<string>,
|
||||
) => Promise<StopEvent<string>>;
|
||||
type Handler2 = (
|
||||
context: HandlerContext,
|
||||
ev: StartEvent<string>,
|
||||
) => Promise<StopEvent<number>>;
|
||||
type Handler3 = (
|
||||
context: HandlerContext,
|
||||
ev: StartEvent<string>,
|
||||
) => Promise<AnalysisEvent>;
|
||||
expectTypeOf<Parameters>().toEqualTypeOf<{
|
||||
inputs: [typeof StartEvent<string>];
|
||||
outputs: [typeof StopEvent<string>];
|
||||
}>();
|
||||
expectTypeOf<
|
||||
StepHandler<
|
||||
unknown,
|
||||
[typeof StartEvent<string>],
|
||||
[typeof StopEvent<string>]
|
||||
>
|
||||
>().toEqualTypeOf<Handler>();
|
||||
expectTypeOf<
|
||||
StepHandler<
|
||||
unknown,
|
||||
[typeof StartEvent<string>],
|
||||
[typeof StopEvent<string>]
|
||||
>
|
||||
>().not.toEqualTypeOf<Handler2>();
|
||||
expectTypeOf<
|
||||
StepHandler<
|
||||
unknown,
|
||||
[typeof StartEvent<string>],
|
||||
[typeof StopEvent<string>]
|
||||
>
|
||||
>().not.toEqualTypeOf<Handler3>();
|
||||
});
|
||||
});
|
||||
|
||||
describe("workflow basic", () => {
|
||||
let generateJoke: Mocked<
|
||||
(context: HandlerContext, ev: StartEvent) => Promise<JokeEvent>
|
||||
>;
|
||||
let critiqueJoke: Mocked<
|
||||
(context: HandlerContext, ev: JokeEvent) => Promise<StopEvent<string>>
|
||||
>;
|
||||
let analyzeJoke: Mocked<
|
||||
(context: HandlerContext, ev: JokeEvent) => Promise<AnalysisEvent>
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
generateJoke = vi.fn(async (_context, _: StartEvent) => {
|
||||
return new JokeEvent({ joke: "a joke" });
|
||||
});
|
||||
|
||||
critiqueJoke = vi.fn(async (_context, _: JokeEvent) => {
|
||||
return new StopEvent("stop");
|
||||
});
|
||||
|
||||
analyzeJoke = vi.fn(async (_context: HandlerContext, _: JokeEvent) => {
|
||||
return new AnalysisEvent({ analysis: "an analysis" });
|
||||
});
|
||||
});
|
||||
|
||||
test("workflow basic", async () => {
|
||||
const workflow = new Workflow<
|
||||
{
|
||||
foo: string;
|
||||
bar: number;
|
||||
},
|
||||
string,
|
||||
string
|
||||
>();
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async ({ data }, start) => {
|
||||
expect(start).toBeInstanceOf(StartEvent);
|
||||
expect(start.data).toBe("start");
|
||||
expect(data.bar).toBe(42);
|
||||
expect(data.foo).toBe("foo");
|
||||
return new StopEvent("stopped");
|
||||
},
|
||||
);
|
||||
|
||||
const result = workflow.run("start", {
|
||||
foo: "foo",
|
||||
bar: 42,
|
||||
});
|
||||
await result;
|
||||
});
|
||||
|
||||
test("run workflow", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
jokeFlow.addStep(
|
||||
{ inputs: [StartEvent<string>], outputs: [JokeEvent] },
|
||||
generateJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{ inputs: [JokeEvent], outputs: [StopEvent] },
|
||||
critiqueJoke,
|
||||
);
|
||||
|
||||
const result = await jokeFlow.run("pirates");
|
||||
|
||||
expect(generateJoke).toHaveBeenCalledTimes(1);
|
||||
expect(critiqueJoke).toHaveBeenCalledTimes(1);
|
||||
expect(result.data).toBe("stop");
|
||||
});
|
||||
|
||||
test("stream events", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [JokeEvent],
|
||||
},
|
||||
generateJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
critiqueJoke,
|
||||
);
|
||||
|
||||
const run = jokeFlow.run("pirates");
|
||||
const event = await run[Symbol.asyncIterator]().next(); // get one event to avoid testing timeout
|
||||
const result = await run;
|
||||
|
||||
expect(generateJoke).toHaveBeenCalledTimes(1);
|
||||
expect(critiqueJoke).toHaveBeenCalledTimes(1);
|
||||
expect(result.data).toBe("stop");
|
||||
expect(event).not.toBeNull();
|
||||
});
|
||||
|
||||
test("workflow timeout", async () => {
|
||||
const TIMEOUT = 1;
|
||||
const jokeFlow = new Workflow<unknown, string, string>({
|
||||
verbose: true,
|
||||
timeout: TIMEOUT,
|
||||
});
|
||||
|
||||
const longRunning = async (_context: HandlerContext, ev: StartEvent) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for 2 seconds
|
||||
return new StopEvent("We waited 2 seconds");
|
||||
};
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
longRunning,
|
||||
);
|
||||
const run = jokeFlow.run("Let's start");
|
||||
await expect(run).rejects.toThrow(
|
||||
`Operation timed out after ${TIMEOUT} seconds`,
|
||||
);
|
||||
});
|
||||
|
||||
test("workflow validation", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
jokeFlow.addStep(
|
||||
{ inputs: [StartEvent<string>], outputs: [StopEvent] },
|
||||
generateJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{ inputs: [JokeEvent], outputs: [StopEvent] },
|
||||
critiqueJoke,
|
||||
);
|
||||
expect(async () => {
|
||||
await jokeFlow.run("pirates").strict();
|
||||
}).rejects.toThrow(
|
||||
"Step spy returned an unexpected output event JokeEvent",
|
||||
);
|
||||
});
|
||||
|
||||
test("requireEvents - 1", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async (ctx, start) => {
|
||||
ctx.sendEvent(new AnalysisEvent({ analysis: "an analysis" }));
|
||||
await ctx.requireEvent(JokeEvent);
|
||||
return new StopEvent("Report generated");
|
||||
},
|
||||
);
|
||||
|
||||
const fn = vi.fn(async () => {
|
||||
return new JokeEvent({ joke: "a joke" });
|
||||
});
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [AnalysisEvent],
|
||||
outputs: [JokeEvent],
|
||||
},
|
||||
fn,
|
||||
);
|
||||
|
||||
const result = await jokeFlow.run("pirates");
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
expect(result.data).toBe("Report generated");
|
||||
});
|
||||
|
||||
test("run workflow with multiple in-degree", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [JokeEvent],
|
||||
},
|
||||
async (context, _) => {
|
||||
context.sendEvent(
|
||||
new AnalysisEvent({
|
||||
analysis: "an analysis",
|
||||
}),
|
||||
);
|
||||
return new JokeEvent({
|
||||
joke: "a joke",
|
||||
});
|
||||
},
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent, AnalysisEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async () => {
|
||||
return new StopEvent("The analysis is insightful and helpful.");
|
||||
},
|
||||
);
|
||||
|
||||
const result = await jokeFlow.run("pirates");
|
||||
expect(result.data).toBe("The analysis is insightful and helpful.");
|
||||
});
|
||||
|
||||
test("run invalid workflow", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [JokeEvent],
|
||||
},
|
||||
generateJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
// @ts-expect-error it actually returns AnalysisEvent
|
||||
analyzeJoke,
|
||||
);
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [JokeEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async () => {
|
||||
return new StopEvent("The analysis is insightful and helpful.");
|
||||
},
|
||||
);
|
||||
const consoleSpy = vi.spyOn(console, "warn");
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(0);
|
||||
const result = await jokeFlow.run("pirates");
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(1);
|
||||
consoleSpy.mockRestore();
|
||||
expect(result.data).toBe("The analysis is insightful and helpful.");
|
||||
});
|
||||
|
||||
test("run workflow with object-based StartEvent and StopEvent", async () => {
|
||||
const objectFlow = new Workflow<
|
||||
unknown,
|
||||
Person,
|
||||
{
|
||||
result: {
|
||||
greeting: string;
|
||||
};
|
||||
}
|
||||
>({ verbose: true });
|
||||
|
||||
type Person = { name: string; age: number };
|
||||
|
||||
const processObject = vi.fn(async (_context, ev: StartEvent<Person>) => {
|
||||
const { name, age } = ev.data;
|
||||
return new StopEvent({
|
||||
result: { greeting: `Hello ${name}, you are ${age} years old!` },
|
||||
});
|
||||
});
|
||||
|
||||
objectFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<Person>],
|
||||
outputs: [
|
||||
StopEvent<{
|
||||
result: {
|
||||
greeting: string;
|
||||
};
|
||||
}>,
|
||||
],
|
||||
},
|
||||
processObject,
|
||||
);
|
||||
|
||||
const result = await objectFlow.run(
|
||||
new StartEvent<Person>({ name: "Alice", age: 30 }),
|
||||
);
|
||||
|
||||
expect(processObject).toHaveBeenCalledTimes(1);
|
||||
expect(result.data.result).toEqual({
|
||||
greeting: "Hello Alice, you are 30 years old!",
|
||||
});
|
||||
});
|
||||
|
||||
test("workflow with two concurrent steps", async () => {
|
||||
const concurrentFlow = new Workflow<unknown, string, string>({
|
||||
verbose: true,
|
||||
});
|
||||
|
||||
const step1 = vi.fn(async (_context, _ev: StartEvent) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
return new StopEvent("Step 1 completed");
|
||||
});
|
||||
|
||||
const step2 = vi.fn(async (_context, _ev: StartEvent) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
return new StopEvent("Step 2 completed");
|
||||
});
|
||||
|
||||
concurrentFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
step1,
|
||||
);
|
||||
concurrentFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
step2,
|
||||
);
|
||||
|
||||
const startTime = new Date();
|
||||
const result = await concurrentFlow.run("start");
|
||||
const endTime = new Date();
|
||||
const duration = endTime.getTime() - startTime.getTime();
|
||||
|
||||
expect(step1).toHaveBeenCalledTimes(1);
|
||||
expect(step2).toHaveBeenCalledTimes(1);
|
||||
expect(duration).toBeLessThan(200);
|
||||
expect(result.data).toBe("Step 2 completed");
|
||||
});
|
||||
|
||||
test("workflow with two concurrent cyclic steps", async () => {
|
||||
const concurrentCyclicFlow = new Workflow<unknown, string, string>({
|
||||
verbose: true,
|
||||
});
|
||||
|
||||
class Step1Event extends WorkflowEvent<{
|
||||
result: string;
|
||||
}> {}
|
||||
|
||||
class Step2Event extends WorkflowEvent<{
|
||||
result: string;
|
||||
}> {}
|
||||
|
||||
let step2Count = 0;
|
||||
|
||||
const step1 = vi.fn(async (_context, ev: StartEvent | Step1Event) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return new Step1Event({ result: "Step 1 completed" });
|
||||
});
|
||||
|
||||
const step2 = vi.fn(async (_context, ev: StartEvent | Step2Event) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
step2Count++;
|
||||
if (step2Count >= 5) {
|
||||
return new StopEvent("Step 2 completed 5 times");
|
||||
}
|
||||
return new Step2Event({ result: "Step 2 completed" });
|
||||
});
|
||||
|
||||
concurrentCyclicFlow.addStep(
|
||||
{
|
||||
inputs: [WorkflowEvent.or(StartEvent<string>, Step1Event)],
|
||||
outputs: [Step1Event],
|
||||
},
|
||||
step1,
|
||||
);
|
||||
concurrentCyclicFlow.addStep(
|
||||
{
|
||||
inputs: [WorkflowEvent.or(StartEvent<string>, Step2Event)],
|
||||
outputs: [Step2Event, StopEvent],
|
||||
},
|
||||
step2,
|
||||
);
|
||||
|
||||
const startTime = new Date();
|
||||
const result = await concurrentCyclicFlow.run("start");
|
||||
const endTime = new Date();
|
||||
const duration = endTime.getTime() - startTime.getTime();
|
||||
|
||||
expect(step1).toHaveBeenCalledTimes(1);
|
||||
expect(step2).toHaveBeenCalledTimes(5);
|
||||
expect(duration).toBeGreaterThanOrEqual(500); // At least 5 * 100ms for step2
|
||||
expect(duration).toBeLessThan(1000); // Less than 1 second
|
||||
expect(result.data).toBe("Step 2 completed 5 times");
|
||||
});
|
||||
|
||||
test("sendEvent", async () => {
|
||||
const myWorkflow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
class QueryEvent extends WorkflowEvent<{ query: string }> {}
|
||||
|
||||
class QueryResultEvent extends WorkflowEvent<{ result: string }> {}
|
||||
|
||||
class PendingEvent extends WorkflowEvent<void> {}
|
||||
|
||||
myWorkflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [PendingEvent],
|
||||
},
|
||||
async (context: HandlerContext, events) => {
|
||||
context.sendEvent(new QueryEvent({ query: "something" }));
|
||||
return new PendingEvent();
|
||||
},
|
||||
);
|
||||
|
||||
myWorkflow.addStep(
|
||||
{
|
||||
inputs: [QueryEvent],
|
||||
outputs: [QueryResultEvent],
|
||||
},
|
||||
async (context, event) => {
|
||||
return new QueryResultEvent({ result: "query result" });
|
||||
},
|
||||
);
|
||||
|
||||
myWorkflow.addStep(
|
||||
{
|
||||
inputs: [PendingEvent, QueryResultEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (context, ev0, ev1) => {
|
||||
return new StopEvent(ev1.data.result);
|
||||
},
|
||||
);
|
||||
|
||||
const result = await myWorkflow.run("start");
|
||||
expect(result.data).toBe("query result");
|
||||
});
|
||||
|
||||
test("requireEvents - 2", async () => {
|
||||
const myWorkflow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
class QueryEvent extends WorkflowEvent<{ query: string }> {}
|
||||
|
||||
class QueryResultEvent extends WorkflowEvent<{ result: string }> {}
|
||||
|
||||
myWorkflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (context: HandlerContext) => {
|
||||
context.sendEvent(new QueryEvent({ query: "something" }));
|
||||
const queryResultEvent = await context.requireEvent(QueryResultEvent);
|
||||
return new StopEvent(queryResultEvent.data.result);
|
||||
},
|
||||
);
|
||||
|
||||
myWorkflow.addStep(
|
||||
{
|
||||
inputs: [QueryEvent],
|
||||
outputs: [QueryResultEvent],
|
||||
},
|
||||
async (context, event) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
return new QueryResultEvent({ result: "query result" });
|
||||
},
|
||||
);
|
||||
|
||||
const result = await myWorkflow.run("start");
|
||||
expect(result.data).toBe("query result");
|
||||
});
|
||||
|
||||
test("allow output with send event", async () => {
|
||||
const myFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [],
|
||||
},
|
||||
async (context, ev) => {
|
||||
context.sendEvent(new StopEvent(`Hello ${ev.data}!`));
|
||||
},
|
||||
);
|
||||
const result = myFlow.run("world");
|
||||
expect((await result).data).toBe("Hello world!");
|
||||
});
|
||||
});
|
||||
|
||||
describe("workflow event loop", () => {
|
||||
test("basic", async () => {
|
||||
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
jokeFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (_context, ev: StartEvent) => {
|
||||
return new StopEvent(`Hello ${ev.data}!`);
|
||||
},
|
||||
);
|
||||
|
||||
const result = await jokeFlow.run("world");
|
||||
expect(result.data).toBe("Hello world!");
|
||||
});
|
||||
|
||||
test("branch", async () => {
|
||||
const myFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
class BranchA1Event extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
class BranchA2Event extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
class BranchB1Event extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
class BranchB2Event extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
let control = false;
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [BranchA1Event, BranchB1Event],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
if (control) {
|
||||
return new BranchA1Event({ payload: ev.data });
|
||||
} else {
|
||||
return new BranchB1Event({ payload: ev.data });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [BranchA1Event],
|
||||
outputs: [BranchA2Event],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
return new BranchA2Event({ payload: ev.data.payload });
|
||||
},
|
||||
);
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [BranchB1Event],
|
||||
outputs: [BranchB2Event],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
return new BranchB2Event({ payload: ev.data.payload });
|
||||
},
|
||||
);
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [BranchA2Event],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
return new StopEvent(`Branch A2: ${ev.data.payload}`);
|
||||
},
|
||||
);
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [BranchB2Event],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
return new StopEvent(`Branch B2: ${ev.data.payload}`);
|
||||
},
|
||||
);
|
||||
|
||||
{
|
||||
const result = await myFlow.run("world");
|
||||
expect(result.data).toMatch(/Branch B2: world/);
|
||||
}
|
||||
|
||||
control = true;
|
||||
|
||||
{
|
||||
const result = await myFlow.run("world");
|
||||
expect(result.data).toMatch(/Branch A2: world/);
|
||||
}
|
||||
|
||||
{
|
||||
const context = myFlow.run("world");
|
||||
for await (const event of context) {
|
||||
if (event instanceof BranchA2Event) {
|
||||
expect(event.data.payload).toBe("world");
|
||||
}
|
||||
if (event instanceof StopEvent) {
|
||||
expect(event.data).toMatch(/Branch A2: world/);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("one event have multiple outputs", async () => {
|
||||
const myFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
|
||||
class AEvent extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
class BEvent extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
class CEvent extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
class DEvent extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
return new StopEvent("STOP");
|
||||
},
|
||||
);
|
||||
|
||||
const fn = vi.fn(async (_context, ev: StartEvent) => {
|
||||
return new AEvent({ payload: ev.data });
|
||||
});
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [AEvent],
|
||||
},
|
||||
fn,
|
||||
);
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [AEvent],
|
||||
outputs: [BEvent, CEvent],
|
||||
},
|
||||
async (_context, ev: AEvent) => {
|
||||
return new BEvent({ payload: ev.data.payload });
|
||||
},
|
||||
);
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [AEvent],
|
||||
outputs: [CEvent],
|
||||
},
|
||||
async (_context, ev: AEvent) => {
|
||||
return new CEvent({ payload: ev.data.payload });
|
||||
},
|
||||
);
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [BEvent],
|
||||
outputs: [DEvent],
|
||||
},
|
||||
async (_context, ev: BEvent) => {
|
||||
return new DEvent({ payload: ev.data.payload });
|
||||
},
|
||||
);
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [CEvent],
|
||||
outputs: [DEvent],
|
||||
},
|
||||
async (_context, ev: CEvent) => {
|
||||
return new DEvent({ payload: ev.data.payload });
|
||||
},
|
||||
);
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [DEvent],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (_context, ev: DEvent) => {
|
||||
return new StopEvent(`Hello ${ev.data.payload}!`);
|
||||
},
|
||||
);
|
||||
|
||||
const result = await myFlow.run("world");
|
||||
expect(result.data).toBe("STOP");
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
// streaming events will allow to consume event even stop event is reached
|
||||
const stream = myFlow.run("world");
|
||||
for await (const _ of stream) {
|
||||
/* empty */
|
||||
}
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("run with custom context", async () => {
|
||||
type MyContext = { name: string };
|
||||
const myFlow = new Workflow<MyContext, string, string>({ verbose: true });
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async ({ data }, _: StartEvent) => {
|
||||
return new StopEvent(`Hello ${data.name}!`);
|
||||
},
|
||||
);
|
||||
|
||||
const result = await myFlow.run("world", { name: "Alice" });
|
||||
expect(result.data).toBe("Hello Alice!");
|
||||
});
|
||||
|
||||
test("run with custom context with two streaming", async () => {
|
||||
type MyContext = { name: string };
|
||||
const myFlow = new Workflow<MyContext, string, string>({ verbose: true });
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async ({ data }, _) => {
|
||||
if (data == null) {
|
||||
return new StopEvent({ result: "EMPTY" });
|
||||
}
|
||||
return new StopEvent({ result: `Hello ${data.name}!` });
|
||||
},
|
||||
);
|
||||
|
||||
const context1 = myFlow.run("world");
|
||||
const context2 = context1.with({ name: "Alice" });
|
||||
const context3 = context1.with({ name: "Bob" });
|
||||
expect(await context1).toMatchInlineSnapshot(`
|
||||
StopEvent {
|
||||
"data": {
|
||||
"result": "EMPTY",
|
||||
},
|
||||
"displayName": "StopEvent",
|
||||
}
|
||||
`);
|
||||
expect(await context2).toMatchInlineSnapshot(`
|
||||
StopEvent {
|
||||
"data": {
|
||||
"result": "Hello Alice!",
|
||||
},
|
||||
"displayName": "StopEvent",
|
||||
}
|
||||
`);
|
||||
expect(await context3).toMatchInlineSnapshot(`
|
||||
StopEvent {
|
||||
"data": {
|
||||
"result": "Hello Bob!",
|
||||
},
|
||||
"displayName": "StopEvent",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test("workflow multiple output", async () => {
|
||||
const myFlow = new Workflow<unknown, string, string>({ verbose: true });
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>, StopEvent<string>],
|
||||
},
|
||||
async (_context, ev) => {
|
||||
return new StopEvent(`Hello ${ev.data}!`);
|
||||
},
|
||||
);
|
||||
const result = await myFlow.run("world").strict();
|
||||
expect(result.data).toBe("Hello world!");
|
||||
});
|
||||
});
|
||||
|
||||
describe("snapshot", async () => {
|
||||
test("snapshot and recover", async () => {
|
||||
const myFlow = new Workflow({ verbose: true });
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async (_, ev: StartEvent) => {
|
||||
return new StopEvent(`Hello ${ev.data}!`);
|
||||
},
|
||||
);
|
||||
const context = myFlow.run("world");
|
||||
const arrayBuffer = context.snapshot();
|
||||
expect(arrayBuffer).toBeInstanceOf(ArrayBuffer);
|
||||
const context2 = await myFlow.recover(arrayBuffer);
|
||||
expect(context2.data).toBe("Hello world!");
|
||||
});
|
||||
|
||||
test("snapshot in middle of workflow run ", async () => {
|
||||
const myFlow = new Workflow<
|
||||
{
|
||||
value: number;
|
||||
},
|
||||
string,
|
||||
string
|
||||
>({ verbose: true });
|
||||
|
||||
class AEvent extends WorkflowEvent<{ payload: string }> {}
|
||||
|
||||
const fn = vi.fn(async (_, ev: StartEvent) => {
|
||||
return new AEvent({ payload: ev.data });
|
||||
});
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [AEvent],
|
||||
},
|
||||
fn,
|
||||
);
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [AEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async ({ data }, _: AEvent) => {
|
||||
return new StopEvent(`Hello ${data.value}!`);
|
||||
},
|
||||
);
|
||||
|
||||
const context = myFlow.run("world", {
|
||||
value: 1,
|
||||
});
|
||||
for await (const event of context) {
|
||||
if (event instanceof AEvent) {
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
const arrayBuffer = context.snapshot();
|
||||
expect(arrayBuffer).toBeInstanceOf(ArrayBuffer);
|
||||
const context2 = await myFlow.recover(arrayBuffer).with({
|
||||
value: 2,
|
||||
});
|
||||
expect(context2.data).toBe("Hello 2!");
|
||||
break;
|
||||
}
|
||||
if (event instanceof StopEvent) {
|
||||
expect(event.data).toBe("Hello 1!");
|
||||
}
|
||||
}
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("error", () => {
|
||||
test("error in handler", async () => {
|
||||
const myFlow = new Workflow<boolean, string, string>({ verbose: true });
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [StopEvent<string>],
|
||||
},
|
||||
async ({ data }) => {
|
||||
if (!data) {
|
||||
throw new Error("Something went wrong");
|
||||
} else {
|
||||
return new StopEvent(`Hello ${data}!`);
|
||||
}
|
||||
},
|
||||
);
|
||||
await expect(myFlow.run("world")).rejects.toThrow("Something went wrong");
|
||||
{
|
||||
const context = myFlow.run("world");
|
||||
try {
|
||||
for await (const _ of context) {
|
||||
// do nothing
|
||||
}
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect((error as Error).message).toBe("Something went wrong");
|
||||
const snapshot = context.snapshot();
|
||||
const newContext = myFlow.recover(snapshot).with(true);
|
||||
expect((await newContext).data).toBe("Hello true!");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("recover in the middle of workflow", async () => {
|
||||
const myFlow = new Workflow<string | undefined, string, string>({
|
||||
verbose: true,
|
||||
});
|
||||
|
||||
class AEvent extends WorkflowEvent<string> {}
|
||||
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<string>],
|
||||
outputs: [AEvent],
|
||||
},
|
||||
async ({ data }) => {
|
||||
if (data !== undefined) {
|
||||
throw new Error("Something went wrong");
|
||||
}
|
||||
return new AEvent("world");
|
||||
},
|
||||
);
|
||||
myFlow.addStep(
|
||||
{
|
||||
inputs: [AEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async ({ data }, ev) => {
|
||||
if (data === undefined) {
|
||||
throw new Error("Something went wrong");
|
||||
}
|
||||
return new StopEvent(`Hello, ${data}!`);
|
||||
},
|
||||
);
|
||||
// no context, so will throw error
|
||||
const context = myFlow.run("world");
|
||||
try {
|
||||
for await (const _ of context) {
|
||||
// do nothing
|
||||
}
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect((error as Error).message).toBe("Something went wrong");
|
||||
const snapshot = context.snapshot();
|
||||
const newContext = myFlow.recover(snapshot).with("Recovered Data");
|
||||
expect((await newContext).data).toBe("Hello, Recovered Data!");
|
||||
}
|
||||
});
|
||||
});
|
||||
Generated
+176
-51
@@ -67,7 +67,7 @@ importers:
|
||||
version: 8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3)
|
||||
vitest:
|
||||
specifier: ^3.1.1
|
||||
version: 3.1.1(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.14.1)(happy-dom@17.4.4)(lightningcss@1.29.3)(msw@2.7.4(@types/node@22.14.1)(typescript@5.7.3))(terser@5.39.0)
|
||||
version: 3.1.1(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.14.1)(happy-dom@17.4.4)(jiti@2.4.2)(lightningcss@1.29.3)(msw@2.7.4(@types/node@22.14.1)(typescript@5.7.3))(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
|
||||
|
||||
apps/next:
|
||||
dependencies:
|
||||
@@ -424,11 +424,11 @@ importers:
|
||||
specifier: ^5.7.3
|
||||
version: 5.7.3
|
||||
vite:
|
||||
specifier: ^5.4.16
|
||||
version: 5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
|
||||
specifier: ^6.3.3
|
||||
version: 6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
|
||||
vite-plugin-wasm:
|
||||
specifier: ^3.3.0
|
||||
version: 3.4.1(vite@5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0))
|
||||
specifier: ^3.4.1
|
||||
version: 3.4.1(vite@6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))
|
||||
|
||||
e2e/examples/nextjs-agent:
|
||||
dependencies:
|
||||
@@ -551,8 +551,8 @@ importers:
|
||||
specifier: ^5.7.3
|
||||
version: 5.7.3
|
||||
vite:
|
||||
specifier: ^5.4.16
|
||||
version: 5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
|
||||
specifier: ^6.3.3
|
||||
version: 6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
|
||||
|
||||
e2e/examples/waku-query-engine:
|
||||
dependencies:
|
||||
@@ -713,6 +713,9 @@ importers:
|
||||
'@llamaindex/replicate':
|
||||
specifier: ^0.0.44
|
||||
version: link:../packages/providers/replicate
|
||||
'@llamaindex/server':
|
||||
specifier: ^0.1.6
|
||||
version: link:../packages/server
|
||||
'@llamaindex/supabase':
|
||||
specifier: ^0.1.1
|
||||
version: link:../packages/providers/storage/supabase
|
||||
@@ -720,7 +723,7 @@ importers:
|
||||
specifier: ^0.0.12
|
||||
version: link:../packages/providers/together
|
||||
'@llamaindex/tools':
|
||||
specifier: ^0.0.5
|
||||
specifier: ^0.0.7
|
||||
version: link:../packages/tools
|
||||
'@llamaindex/upstash':
|
||||
specifier: ^0.0.16
|
||||
@@ -1449,7 +1452,7 @@ importers:
|
||||
version: link:../../openai
|
||||
vitest:
|
||||
specifier: ^3.0.9
|
||||
version: 3.1.1(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.14.1)(happy-dom@17.4.4)(lightningcss@1.29.3)(msw@2.7.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.39.0)
|
||||
version: 3.1.1(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.14.1)(happy-dom@17.4.4)(jiti@2.4.2)(lightningcss@1.29.3)(msw@2.7.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
|
||||
|
||||
packages/providers/storage/firestore:
|
||||
dependencies:
|
||||
@@ -1701,8 +1704,8 @@ importers:
|
||||
specifier: ^0.3.4
|
||||
version: 0.3.4(@modelcontextprotocol/sdk@1.9.0)(hono@4.7.7)(next@15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.2)
|
||||
'@llamaindex/chat-ui':
|
||||
specifier: 0.4.0
|
||||
version: 0.4.0(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
specifier: 0.4.1
|
||||
version: 0.4.1(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@llamaindex/env':
|
||||
specifier: workspace:*
|
||||
version: link:../env
|
||||
@@ -1976,6 +1979,9 @@ importers:
|
||||
|
||||
packages/workflow:
|
||||
dependencies:
|
||||
'@llama-flow/llamaindex':
|
||||
specifier: ^0.0.12
|
||||
version: 0.0.12(@modelcontextprotocol/sdk@1.9.0)(hono@4.7.7)(next@15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.2)
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.24.2
|
||||
@@ -3858,16 +3864,42 @@ packages:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@llama-flow/core@0.3.9':
|
||||
resolution: {integrity: sha512-/s0L16qo/hbF8Ahe5KEjJp6YfomIizp5lyhwgwQ0bomOijP3wX6L2xHnXucPRg5RGxgzpIIlDFW1sTQkjIamqg==}
|
||||
peerDependencies:
|
||||
'@modelcontextprotocol/sdk': ^1.7.0
|
||||
hono: ^4.7.4
|
||||
next: ^15.2.2
|
||||
p-retry: ^6.2.1
|
||||
rxjs: ^7.8.2
|
||||
zod: ^3.24.2
|
||||
peerDependenciesMeta:
|
||||
'@modelcontextprotocol/sdk':
|
||||
optional: true
|
||||
hono:
|
||||
optional: true
|
||||
next:
|
||||
optional: true
|
||||
p-retry:
|
||||
optional: true
|
||||
rxjs:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@llama-flow/docs@0.0.5':
|
||||
resolution: {integrity: sha512-iAEqqWgPnJNxm4syNSxudDE1aHYNi1eTrxHg+FjcPeZhxyksLYRzpmzUikcZv0uhxgLwRinF7UPTnH9ioKlaEw==}
|
||||
|
||||
'@llama-flow/llamaindex@0.0.12':
|
||||
resolution: {integrity: sha512-NoUCyVaZTHkdwzllcEY0mzqOkGFTemgpOBHg96fQx5ewZczsy+QpW09pJkMNsLaGQ6JJeBZ4mrBjMkgNUI57Mw==}
|
||||
|
||||
'@llamaindex/chat-ui@0.2.0':
|
||||
resolution: {integrity: sha512-9U5+9l2UVBaOG8fSuMjnere5R2QSNxCEcixMwBgt4L4b0evo8jU4ZzlSxLPunWfpn1PWFVMUwKLlSSwa1qTTyA==}
|
||||
peerDependencies:
|
||||
react: ^18.2.0 || ^19.0.0 || ^19.0.0-rc
|
||||
|
||||
'@llamaindex/chat-ui@0.4.0':
|
||||
resolution: {integrity: sha512-u9jOUuyKPDFnJsorfH8oIE0UVO+zlabbD8lgTFbN37XUdYFFG4rteEYwUWc/n4/h/GjnFcTfxpiyWmiwTKzicw==}
|
||||
'@llamaindex/chat-ui@0.4.1':
|
||||
resolution: {integrity: sha512-lRPsnpPe2mBJJVlKEpIKSmr5ofCGVRP7U21CfSCFaFVA/V81BaZZ/gyt2G8+vzXmTWGqRYJGBNHcsGVPx0G7Mw==}
|
||||
peerDependencies:
|
||||
react: ^18.2.0 || ^19.0.0 || ^19.0.0-rc
|
||||
|
||||
@@ -3904,6 +3936,7 @@ packages:
|
||||
|
||||
'@mixedbread-ai/sdk@2.2.11':
|
||||
resolution: {integrity: sha512-NJiY6BVPR+s/DTzUPQS1Pv418trOmII/8hftmIqxXlYaKbIrgJimQfwCW9M6Y21YPcMA8zTQGYZHm4IWlMjIQw==}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
|
||||
'@modelcontextprotocol/sdk@0.5.0':
|
||||
resolution: {integrity: sha512-RXgulUX6ewvxjAG0kOpLMEdXXWkzWgaoCGaA2CwNW7cQCIphjpJhjpHSiaPdVCnisjRF/0Cm9KWHUuIoeiAblQ==}
|
||||
@@ -8176,6 +8209,14 @@ packages:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fdir@6.4.4:
|
||||
resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==}
|
||||
peerDependencies:
|
||||
picomatch: ^3 || ^4
|
||||
peerDependenciesMeta:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fecha@4.2.3:
|
||||
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
|
||||
|
||||
@@ -12462,6 +12503,10 @@ packages:
|
||||
resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tinyglobby@0.2.13:
|
||||
resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tinypool@1.0.2:
|
||||
resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
@@ -13061,6 +13106,46 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vite@6.3.3:
|
||||
resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
|
||||
jiti: '>=1.21.0'
|
||||
less: '*'
|
||||
lightningcss: ^1.21.0
|
||||
sass: '*'
|
||||
sass-embedded: '*'
|
||||
stylus: '*'
|
||||
sugarss: '*'
|
||||
terser: ^5.16.0
|
||||
tsx: ^4.8.1
|
||||
yaml: ^2.4.2
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
jiti:
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
sass-embedded:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
tsx:
|
||||
optional: true
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vitest@2.1.0:
|
||||
resolution: {integrity: sha512-XuuEeyNkqbfr0FtAvd9vFbInSSNY1ykCQTYQ0sj9wPy4hx+1gR7gqVNdW0AX2wrrM1wWlN5fnJDjF9xG6mYRSQ==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
@@ -15957,8 +16042,27 @@ snapshots:
|
||||
p-retry: 6.2.1
|
||||
zod: 3.24.2
|
||||
|
||||
'@llama-flow/core@0.3.9(@modelcontextprotocol/sdk@1.9.0)(hono@4.7.7)(next@15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.2)':
|
||||
optionalDependencies:
|
||||
'@modelcontextprotocol/sdk': 1.9.0
|
||||
hono: 4.7.7
|
||||
next: 15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
p-retry: 6.2.1
|
||||
zod: 3.24.2
|
||||
|
||||
'@llama-flow/docs@0.0.5': {}
|
||||
|
||||
'@llama-flow/llamaindex@0.0.12(@modelcontextprotocol/sdk@1.9.0)(hono@4.7.7)(next@15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.2)':
|
||||
dependencies:
|
||||
'@llama-flow/core': 0.3.9(@modelcontextprotocol/sdk@1.9.0)(hono@4.7.7)(next@15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.2)
|
||||
transitivePeerDependencies:
|
||||
- '@modelcontextprotocol/sdk'
|
||||
- hono
|
||||
- next
|
||||
- p-retry
|
||||
- rxjs
|
||||
- zod
|
||||
|
||||
'@llamaindex/chat-ui@0.2.0(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@llamaindex/pdf-viewer': 1.3.0(@types/react@19.0.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
@@ -15989,12 +16093,14 @@ snapshots:
|
||||
- react-dom
|
||||
- supports-color
|
||||
|
||||
'@llamaindex/chat-ui@0.4.0(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
'@llamaindex/chat-ui@0.4.1(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@llamaindex/pdf-viewer': 1.3.0(@types/react@19.0.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-accordion': 1.2.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-collapsible': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-hover-card': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-icons': 1.3.2(react@19.1.0)
|
||||
'@radix-ui/react-popover': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-progress': 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-select': 2.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-slot': 1.2.0(@types/react@19.0.10)(react@19.1.0)
|
||||
@@ -18822,23 +18928,23 @@ snapshots:
|
||||
msw: 2.7.4(@types/node@22.9.0)(typescript@5.8.3)
|
||||
vite: 5.4.16(@types/node@22.9.0)(lightningcss@1.29.3)(terser@5.39.0)
|
||||
|
||||
'@vitest/mocker@3.1.1(msw@2.7.4(@types/node@22.14.1)(typescript@5.7.3))(vite@5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0))':
|
||||
'@vitest/mocker@3.1.1(msw@2.7.4(@types/node@22.14.1)(typescript@5.7.3))(vite@6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.1.1
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.17
|
||||
optionalDependencies:
|
||||
msw: 2.7.4(@types/node@22.14.1)(typescript@5.7.3)
|
||||
vite: 5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
|
||||
vite: 6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
|
||||
|
||||
'@vitest/mocker@3.1.1(msw@2.7.4(@types/node@22.14.1)(typescript@5.8.3))(vite@5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0))':
|
||||
'@vitest/mocker@3.1.1(msw@2.7.4(@types/node@22.14.1)(typescript@5.8.3))(vite@6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.1.1
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.17
|
||||
optionalDependencies:
|
||||
msw: 2.7.4(@types/node@22.14.1)(typescript@5.8.3)
|
||||
vite: 5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
|
||||
vite: 6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
|
||||
|
||||
'@vitest/pretty-format@2.1.0':
|
||||
dependencies:
|
||||
@@ -20804,7 +20910,7 @@ snapshots:
|
||||
'@typescript-eslint/parser': 8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3)
|
||||
eslint: 9.22.0(jiti@2.4.2)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2))
|
||||
eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2))
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.7.0)(eslint@9.22.0(jiti@2.4.2))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.22.0(jiti@2.4.2))
|
||||
eslint-plugin-react: 7.37.2(eslint@9.22.0(jiti@2.4.2))
|
||||
@@ -20834,22 +20940,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.0
|
||||
enhanced-resolve: 5.18.1
|
||||
eslint: 9.22.0(jiti@2.4.2)
|
||||
fast-glob: 3.3.3
|
||||
get-tsconfig: 4.10.0
|
||||
is-bun-module: 1.3.0
|
||||
is-glob: 4.0.3
|
||||
stable-hash: 0.0.4
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.7.0)(eslint@9.22.0(jiti@2.4.2))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@9.16.0(jiti@2.4.2)):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
@@ -20878,7 +20968,7 @@ snapshots:
|
||||
is-glob: 4.0.3
|
||||
stable-hash: 0.0.4
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@9.22.0(jiti@2.4.2))
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.7.0)(eslint@9.22.0(jiti@2.4.2))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -20904,14 +20994,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)):
|
||||
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@9.22.0(jiti@2.4.2)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3)
|
||||
eslint: 9.22.0(jiti@2.4.2)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2))
|
||||
eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -20984,7 +21074,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 9.22.0(jiti@2.4.2)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2))
|
||||
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@9.22.0(jiti@2.4.2))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
@@ -21446,6 +21536,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.2
|
||||
|
||||
fdir@6.4.4(picomatch@4.0.2):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.2
|
||||
|
||||
fecha@4.2.3: {}
|
||||
|
||||
fetch-h2@3.0.2:
|
||||
@@ -26988,6 +27082,11 @@ snapshots:
|
||||
fdir: 6.4.3(picomatch@4.0.2)
|
||||
picomatch: 4.0.2
|
||||
|
||||
tinyglobby@0.2.13:
|
||||
dependencies:
|
||||
fdir: 6.4.4(picomatch@4.0.2)
|
||||
picomatch: 4.0.2
|
||||
|
||||
tinypool@1.0.2: {}
|
||||
|
||||
tinyrainbow@1.2.0: {}
|
||||
@@ -27617,15 +27716,16 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vite-node@3.1.1(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0):
|
||||
vite-node@3.1.1(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.0
|
||||
es-module-lexer: 1.6.0
|
||||
pathe: 2.0.3
|
||||
vite: 5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
|
||||
vite: 6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
@@ -27634,10 +27734,12 @@ snapshots:
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite-plugin-wasm@3.4.1(vite@5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)):
|
||||
vite-plugin-wasm@3.4.1(vite@6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)):
|
||||
dependencies:
|
||||
vite: 5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
|
||||
vite: 6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
|
||||
|
||||
vite@5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0):
|
||||
dependencies:
|
||||
@@ -27675,6 +27777,23 @@ snapshots:
|
||||
tsx: 4.19.3
|
||||
yaml: 2.7.1
|
||||
|
||||
vite@6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1):
|
||||
dependencies:
|
||||
esbuild: 0.25.2
|
||||
fdir: 6.4.4(picomatch@4.0.2)
|
||||
picomatch: 4.0.2
|
||||
postcss: 8.5.3
|
||||
rollup: 4.38.0
|
||||
tinyglobby: 0.2.13
|
||||
optionalDependencies:
|
||||
'@types/node': 22.14.1
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.4.2
|
||||
lightningcss: 1.29.3
|
||||
terser: 5.39.0
|
||||
tsx: 4.19.3
|
||||
yaml: 2.7.1
|
||||
|
||||
vitest@2.1.0(@edge-runtime/vm@5.0.0)(@types/node@22.14.1)(happy-dom@17.4.4)(lightningcss@1.29.3)(msw@2.7.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.39.0):
|
||||
dependencies:
|
||||
'@vitest/expect': 2.1.0
|
||||
@@ -27859,10 +27978,10 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vitest@3.1.1(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.14.1)(happy-dom@17.4.4)(lightningcss@1.29.3)(msw@2.7.4(@types/node@22.14.1)(typescript@5.7.3))(terser@5.39.0):
|
||||
vitest@3.1.1(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.14.1)(happy-dom@17.4.4)(jiti@2.4.2)(lightningcss@1.29.3)(msw@2.7.4(@types/node@22.14.1)(typescript@5.7.3))(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1):
|
||||
dependencies:
|
||||
'@vitest/expect': 3.1.1
|
||||
'@vitest/mocker': 3.1.1(msw@2.7.4(@types/node@22.14.1)(typescript@5.7.3))(vite@5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0))
|
||||
'@vitest/mocker': 3.1.1(msw@2.7.4(@types/node@22.14.1)(typescript@5.7.3))(vite@6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))
|
||||
'@vitest/pretty-format': 3.1.1
|
||||
'@vitest/runner': 3.1.1
|
||||
'@vitest/snapshot': 3.1.1
|
||||
@@ -27878,8 +27997,8 @@ snapshots:
|
||||
tinyexec: 0.3.2
|
||||
tinypool: 1.0.2
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
|
||||
vite-node: 3.1.1(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
|
||||
vite: 6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
|
||||
vite-node: 3.1.1(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@edge-runtime/vm': 5.0.0
|
||||
@@ -27887,6 +28006,7 @@ snapshots:
|
||||
'@types/node': 22.14.1
|
||||
happy-dom: 17.4.4
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
@@ -27896,11 +28016,13 @@ snapshots:
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vitest@3.1.1(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.14.1)(happy-dom@17.4.4)(lightningcss@1.29.3)(msw@2.7.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.39.0):
|
||||
vitest@3.1.1(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.14.1)(happy-dom@17.4.4)(jiti@2.4.2)(lightningcss@1.29.3)(msw@2.7.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1):
|
||||
dependencies:
|
||||
'@vitest/expect': 3.1.1
|
||||
'@vitest/mocker': 3.1.1(msw@2.7.4(@types/node@22.14.1)(typescript@5.8.3))(vite@5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0))
|
||||
'@vitest/mocker': 3.1.1(msw@2.7.4(@types/node@22.14.1)(typescript@5.8.3))(vite@6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))
|
||||
'@vitest/pretty-format': 3.1.1
|
||||
'@vitest/runner': 3.1.1
|
||||
'@vitest/snapshot': 3.1.1
|
||||
@@ -27916,8 +28038,8 @@ snapshots:
|
||||
tinyexec: 0.3.2
|
||||
tinypool: 1.0.2
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
|
||||
vite-node: 3.1.1(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
|
||||
vite: 6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
|
||||
vite-node: 3.1.1(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@edge-runtime/vm': 5.0.0
|
||||
@@ -27925,6 +28047,7 @@ snapshots:
|
||||
'@types/node': 22.14.1
|
||||
happy-dom: 17.4.4
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
@@ -27934,6 +28057,8 @@ snapshots:
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
voyageai@0.0.3-1:
|
||||
dependencies:
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { StartEvent, StopEvent, Workflow, WorkflowEvent } from "llamaindex";
|
||||
import type { ReactNode } from "react";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
describe("workflow integration", () => {
|
||||
type Context = {
|
||||
pending: string[];
|
||||
};
|
||||
type Start = string;
|
||||
type Stop = ReactNode;
|
||||
|
||||
test("nodejs", async () => {
|
||||
const workflow = new Workflow<never, Start, Stop>({
|
||||
wait: async () => await new Promise((resolve) => setTimeout(resolve, 0)),
|
||||
});
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async (_, __) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
return new StopEvent("hello");
|
||||
},
|
||||
);
|
||||
|
||||
console.log("start");
|
||||
|
||||
const run = workflow.run("start");
|
||||
await run.then((stop) => {
|
||||
expect(stop.data).toBe("hello");
|
||||
});
|
||||
});
|
||||
|
||||
test("with jsx", async () => {
|
||||
const workflow = new Workflow<never, Start, Stop>();
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async (_, __) => {
|
||||
return new StopEvent(<div>Hey there!</div>);
|
||||
},
|
||||
);
|
||||
|
||||
const run = workflow.run("start");
|
||||
const stop = await run;
|
||||
expect(stop.data).toEqual(<div>Hey there!</div>);
|
||||
});
|
||||
|
||||
test("with message channel", async () => {
|
||||
const workflow = new Workflow<Context, Start, Stop>();
|
||||
|
||||
class AnalysisStartEvent extends WorkflowEvent<string> {}
|
||||
|
||||
class AnalysisStopEvent extends WorkflowEvent<string> {}
|
||||
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
async ({ data, sendEvent, requireEvent }) => {
|
||||
data.pending.push("analyzing");
|
||||
sendEvent(new AnalysisStartEvent("analysis my document"));
|
||||
const event = await requireEvent(AnalysisStopEvent);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
data.pending.push("analysis complete");
|
||||
return new StopEvent(event.data);
|
||||
},
|
||||
);
|
||||
|
||||
workflow.addStep(
|
||||
{
|
||||
inputs: [AnalysisStartEvent],
|
||||
outputs: [AnalysisStopEvent],
|
||||
},
|
||||
async ({ data }) => {
|
||||
data.pending.push("loading document");
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
data.pending.push("document loaded");
|
||||
return new AnalysisStopEvent("analysis complete");
|
||||
},
|
||||
);
|
||||
|
||||
const run = workflow.run("start").with({
|
||||
pending: [],
|
||||
});
|
||||
await run;
|
||||
expect(run.data.pending).toEqual([
|
||||
"analyzing",
|
||||
"loading document",
|
||||
"document loaded",
|
||||
"analysis complete",
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user