feat: support human in the loop for TS (#686)

* feat: support human in the loop for TS

* add example for custom workflow

* fix: need to request humanResponseEvent to save missing step to snapshot

* refactor: human response data should be any

* refactor runWorkflow function to support resume stream

* refactor: hitl

* fix: workflow

* add summary event

* send tool event

* use requestId from Vercel

* update chat route.ts

* fix copy utils/*

* refactor: workflow and stream

* Create eight-moons-perform.md

* update typo

* make schema simple

* fix typo

* use messages in startAgentEvent

* save to snapshots folder

* fix lint

* feat: workflowBaseEvent

* include response event in input event

* simplify type

* update readme

* update document

* fix typecheck

* bump: "@llamaindex/workflow": "~1.1.8"

* remove any

* use fixed tsx version to fix e2e

* fix wrong copy

* add cli hitl examples as a use case for both Python and TS

* update changeset to release create-llama also

* fix e2e

* fix e2e

* hitl frontend chat

* try disable hitl test
This commit is contained in:
Thuc Pham
2025-06-12 18:00:10 +07:00
committed by GitHub
parent 66b81e5323
commit e2486eb080
41 changed files with 1656 additions and 94 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@llamaindex/server": patch
"create-llama": patch
---
feat: support human in the loop for TS
@@ -20,6 +20,7 @@ const useCases: TemplateUseCase[] = [
"financial_report",
"code_generator",
"document_generator",
"hitl",
];
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
@@ -27,6 +27,7 @@ const templateUseCases = [
"financial_report",
"deep_research",
"code_generator",
// "hitl",
];
const ejectDir = "next";
@@ -3,6 +3,7 @@ import { exec } from "child_process";
import fs from "fs";
import path from "path";
import util from "util";
import { NO_DATA_USE_CASES } from "../../helpers/constant";
import {
TemplateFramework,
TemplateType,
@@ -25,6 +26,7 @@ const useCases: TemplateUseCase[] = [
"financial_report",
"code_generator",
"document_generator",
"hitl",
];
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
@@ -83,7 +85,7 @@ test.describe("Test resolve TS dependencies", () => {
});
});
// Skipping llamacloud for the use case doesn't use index.
if (useCase !== "code_generator" && useCase !== "document_generator") {
if (!useCase || !NO_DATA_USE_CASES.includes(useCase)) {
test(`llamaParse - ${optionDescription}`, async () => {
await runTest({
templateType: templateType,
@@ -1,6 +1,15 @@
import { TemplateUseCase } from "./types";
export const COMMUNITY_OWNER = "run-llama";
export const COMMUNITY_REPO = "create_llama_projects";
export const LLAMA_PACK_OWNER = "run-llama";
export const LLAMA_PACK_REPO = "llama_index";
export const LLAMA_PACK_FOLDER = "llama-index-packs";
export const LLAMA_PACK_FOLDER_PATH = `${LLAMA_PACK_OWNER}/${LLAMA_PACK_REPO}/main/${LLAMA_PACK_FOLDER}`;
// these use cases don't have data folder, so no need to run generate and no need to getIndex
export const NO_DATA_USE_CASES: TemplateUseCase[] = [
"code_generator",
"document_generator",
"hitl",
];
+3 -1
View File
@@ -4,6 +4,7 @@ import path from "path";
import picocolors, { cyan } from "picocolors";
import fsExtra from "fs-extra";
import { NO_DATA_USE_CASES } from "./constant";
import { writeLoadersConfig } from "./datasources";
import { createBackendEnvFile, createFrontendEnvFile } from "./env-variables";
import { PackageManager } from "./get-pkg-manager";
@@ -98,8 +99,9 @@ async function generateContextData(
}
} else {
console.log(`Running ${runGenerate} to generate the context data.`);
const shouldRunGenerate =
useCase !== "code_generator" && useCase !== "document_generator"; // Artifact use case doesn't use index.
!useCase || !NO_DATA_USE_CASES.includes(useCase);
if (shouldRunGenerate) {
await callPackageManager(packageManager, true, ["run", "generate"]);
+2 -1
View File
@@ -59,7 +59,8 @@ export type TemplateUseCase =
| "contract_review"
| "agentic_rag"
| "code_generator"
| "document_generator";
| "document_generator"
| "hitl";
// Config for both file and folder
export type FileSourceConfig =
| {
+2 -1
View File
@@ -4,6 +4,7 @@ import path from "path";
import { bold, cyan, red, yellow } from "picocolors";
import { assetRelocator, copy } from "../helpers/copy";
import { callPackageManager } from "../helpers/install";
import { NO_DATA_USE_CASES } from "./constant";
import { templatesDir } from "./dir";
import { PackageManager } from "./get-pkg-manager";
import { InstallTemplateArgs, ModelProvider, TemplateVectorDB } from "./types";
@@ -83,7 +84,7 @@ const installLlamaIndexServerTemplate = async ({
}
// Simplify use case code
if (useCase === "code_generator" || useCase === "document_generator") {
if (useCase && NO_DATA_USE_CASES.includes(useCase)) {
// Artifact use case doesn't use index.
// We don't need data.ts, generate.ts
await fs.rm(path.join(root, "src", "app", "data.ts"));
+17 -2
View File
@@ -1,4 +1,5 @@
import prompts from "prompts";
import { NO_DATA_USE_CASES } from "../helpers/constant";
import { EXAMPLE_10K_SEC_FILES, EXAMPLE_FILE } from "../helpers/datasources";
import { askModelConfig } from "../helpers/providers";
import { getTools } from "../helpers/tools";
@@ -11,7 +12,8 @@ type AppType =
| "financial_report"
| "deep_research"
| "code_generator"
| "document_generator";
| "document_generator"
| "hitl";
type SimpleAnswers = {
appType: AppType;
@@ -57,6 +59,12 @@ export const askSimpleQuestions = async (
value: "document_generator",
description: "Build a OpenAI canvas-styled document generator.",
},
{
title: "Human in the Loop",
value: "hitl",
description:
"Build a CLI command workflow that is reviewed by a human before execution",
},
],
},
questionHandlers,
@@ -81,7 +89,8 @@ export const askSimpleQuestions = async (
);
language = newLanguage;
if (appType !== "code_generator" && appType !== "document_generator") {
const shouldAskLlamaCloud = !NO_DATA_USE_CASES.includes(appType);
if (shouldAskLlamaCloud) {
const { useLlamaCloud: newUseLlamaCloud } = await prompts(
{
type: "toggle",
@@ -170,6 +179,12 @@ const convertAnswers = async (
tools: [],
modelConfig: MODEL_GPT41,
},
hitl: {
template: "llamaindexserver",
dataSources: [],
tools: [],
modelConfig: MODEL_GPT41,
},
};
const results = lookup[answers.appType];
@@ -0,0 +1,95 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardFooter } from "@/components/ui/card";
import { JSONValue, useChatUI } from "@llamaindex/chat-ui";
import React, { FC, useState } from "react";
import { z } from "zod";
// This schema is equivalent to the CLICommand model defined in events.py
const CLIInputEventSchema = z.object({
command: z.string(),
});
type CLIInputEvent = z.infer<typeof CLIInputEventSchema>;
const CLIHumanInput: FC<{
events: JSONValue[];
}> = ({ events }) => {
const inputEvent = (events || [])
.map((ev) => {
const parseResult = CLIInputEventSchema.safeParse(ev);
return parseResult.success ? parseResult.data : null;
})
.filter((ev): ev is CLIInputEvent => ev !== null)
.at(-1);
const { append } = useChatUI();
const [confirmedValue, setConfirmedValue] = useState<boolean | null>(null);
const [editableCommand, setEditableCommand] = useState<string | undefined>(
inputEvent?.command,
);
// Update editableCommand if inputEvent changes (e.g. new event comes in)
React.useEffect(() => {
setEditableCommand(inputEvent?.command);
}, [inputEvent?.command]);
const handleConfirm = () => {
append({
content: "Yes",
role: "user",
annotations: [
{
type: "human_response",
data: {
execute: true,
command: editableCommand, // Use editable command
},
},
],
});
setConfirmedValue(true);
};
const handleCancel = () => {
append({
content: "No",
role: "user",
annotations: [
{
type: "human_response",
data: {
execute: false,
command: inputEvent?.command,
},
},
],
});
setConfirmedValue(false);
};
return (
<Card className="my-4">
<CardContent className="pt-6">
<p className="text-sm text-gray-700">
Do you want to execute the following command?
</p>
<input
disabled
type="text"
value={editableCommand || ""}
onChange={(e) => setEditableCommand(e.target.value)}
className="my-2 w-full overflow-x-auto rounded border border-gray-300 bg-gray-100 p-3 font-mono text-xs text-gray-800"
/>
</CardContent>
{confirmedValue === null ? (
<CardFooter className="flex justify-end gap-2">
<>
<Button onClick={handleConfirm}>Yes</Button>
<Button onClick={handleCancel}>No</Button>
</>
</CardFooter>
) : null}
</Card>
);
};
export default CLIHumanInput;
@@ -0,0 +1,109 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Workflows](https://docs.llamaindex.ai/en/stable/understanding/workflows/).
## Getting Started
First, setup the environment with uv:
> **_Note:_** This step is not needed if you are using the dev-container.
```shell
uv sync
```
Then check the parameters that have been pre-configured in the `.env` file in this directory.
Make sure you have set the `OPENAI_API_KEY` for the LLM.
Then, run the development server:
```shell
uv run fastapi dev
```
Then open [http://localhost:8000](http://localhost:8000) with your browser to start the chat UI.
To start the app optimized for **production**, run:
```
uv run fastapi run
```
## Configure LLM and Embedding Model
You can configure [LLM model](https://docs.llamaindex.ai/en/stable/module_guides/models/llms) and [embedding model](https://docs.llamaindex.ai/en/stable/module_guides/models/embeddings) in [settings.py](app/settings.py).
## Use Case
This example shows how to use the LlamaIndexServer with a human in the loop. It allows you to start CLI commands that are reviewed by a human before execution.
To update the workflow, you can modify the code in [`workflow.py`](app/workflow.py).
You can start by sending an request on the [chat UI](http://localhost:8000) or you can test the `/api/chat` endpoint with the following curl request:
```
curl --location 'localhost:8000/api/chat' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "Show me the files in the current directory" }] }'
```
## How does HITL work?
### Events
The human-in-the-loop approach used here is based on a simple idea: the workflow pauses and waits for a human response before proceeding to the next step.
To do this, you will need to implement two custom events:
- [HumanInputEvent](https://github.com/run-llama/create-llama/blob/main/packages/server/src/utils/hitl/events.ts): This event is used to request input from the user.
- [HumanResponseEvent](https://github.com/run-llama/create-llama/blob/main/packages/server/src/utils/hitl/events.ts): This event is sent to the workflow to resume execution with input from the user.
In this example, we have implemented these two custom events in [`events.ts`](src/app/events.ts):
- `cliHumanInputEvent` to request input from the user for CLI command execution.
- `cliHumanResponseEvent` to resume the workflow with the response from the user.
```typescript
export const cliHumanInputEvent = humanInputEvent<{
type: "cli_human_input";
data: { command: string };
response: typeof cliHumanResponseEvent;
}>();
export const cliHumanResponseEvent = humanResponseEvent<{
type: "human_response";
data: { execute: boolean; command: string };
}>();
```
### UI Component
HITL also needs a custom UI component, that is shown when the LlamaIndexServer receives the `cliHumanInputEvent`. The name of the component is defined in the `type` field of the `cliHumanInputEvent` - in our case, it is `cli_human_input`, which corresponds to the [cli_human_input.tsx](./components/cli_human_input.tsx) component.
The custom component must use `append` to send a message with a `human_response` annotation. The data of the annotation must be in the format of the response event `cliHumanResponseEvent`, in our case, for sending to execute the command `ls -l`, we would send:
```tsx
append({
content: "Yes",
role: "user",
annotations: [
{
type: "human_response",
data: {
execute: true,
command: "ls -l", // The command to execute
},
},
],
});
```
This component displays the command to execute and the user can choose to execute or cancel the command execution.
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
- [Workflows Introduction](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - learn about LlamaIndex workflows.
- [LlamaIndex Server](https://pypi.org/project/llama-index-server/)
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
@@ -0,0 +1,34 @@
from typing import Type
from pydantic import BaseModel, Field
from llama_index.server.models import HumanInputEvent, HumanResponseEvent
class CLIHumanResponseEvent(HumanResponseEvent):
execute: bool = Field(
description="True if the human wants to execute the command, False otherwise."
)
command: str = Field(description="The command to execute.")
class CLICommand(BaseModel):
command: str = Field(description="The command to execute.")
# We need an event that extends from HumanInputEvent for HITL feature
class CLIHumanInputEvent(HumanInputEvent):
"""
CLIInputRequiredEvent is sent when the agent needs permission from the user to execute the CLI command or not.
Render this event by showing the command and a boolean button to execute the command or not.
"""
event_type: str = (
"cli_human_input" # used by UI to render with appropriate component
)
response_event_type: Type = (
CLIHumanResponseEvent # used by workflow to resume with the correct event
)
data: CLICommand = Field( # the data that sent to the UI for rendering
description="The command to execute.",
)
@@ -0,0 +1,87 @@
import platform
import subprocess
from typing import Any
from app.events import CLICommand, CLIHumanInputEvent, CLIHumanResponseEvent
from llama_index.core.prompts import PromptTemplate
from llama_index.core.settings import Settings
from llama_index.core.workflow import (
Context,
StartEvent,
StopEvent,
Workflow,
step,
)
def create_workflow() -> Workflow:
return CLIWorkflow()
class CLIWorkflow(Workflow):
"""
A workflow has ability to execute command line tool with human in the loop for confirmation.
"""
default_prompt = PromptTemplate(
template="""
You are a helpful assistant who can write CLI commands to execute using {cli_language}.
Your task is to analyze the user's request and write a CLI command to execute.
## User Request
{user_request}
Don't be verbose, only respond with the CLI command without any other text.
"""
)
def __init__(self, **kwargs: Any) -> None:
# HITL Workflow should disable timeout otherwise, we will get a timeout error from callback
kwargs["timeout"] = None
super().__init__(**kwargs)
@step
async def start(self, ctx: Context, ev: StartEvent) -> CLIHumanInputEvent:
user_msg = ev.user_msg
if user_msg is None:
raise ValueError("Missing user_msg in StartEvent")
await ctx.set("user_msg", user_msg)
# Request LLM to generate a CLI command
os_name = platform.system()
if os_name == "Linux" or os_name == "Darwin":
cli_language = "bash"
else:
cli_language = "cmd"
prompt = self.default_prompt.format(
user_request=user_msg, cli_language=cli_language
)
llm = Settings.llm
if llm is None:
raise ValueError("Missing LLM in Settings")
response = await llm.acomplete(prompt, formatted=True)
command = response.text.strip()
if command == "":
raise ValueError("Couldn't generate a command")
# Send the command to the user for confirmation
await ctx.set("command", command)
return CLIHumanInputEvent( # type: ignore
data=CLICommand(command=command),
response_event_type=CLIHumanResponseEvent,
)
@step
async def handle_human_response(
self,
ctx: Context,
ev: CLIHumanResponseEvent, # This event is sent by LlamaIndexServer when user response
) -> StopEvent:
# If we have human response, check the confirmation and execute the command
if ev.execute:
command = ev.command or ""
if command == "":
raise ValueError("Missing command in CLIExecutionEvent")
res = subprocess.run(command, shell=True, capture_output=True, text=True)
return StopEvent(result=res.stdout or res.stderr)
else:
return StopEvent(result=None)
@@ -0,0 +1,106 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) project bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
## Getting Started
First, install the dependencies:
```
npm install
```
Second, run the development server:
```
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the chat UI.
## Configure LLM and Embedding Model
You can configure [LLM model](https://ts.llamaindex.ai/docs/llamaindex/modules/llms) in the [settings file](src/app/settings.ts).
## Use Case
This example shows how to use the LlamaIndexServer with a human in the loop. It allows you to start CLI commands that are reviewed by a human before execution.
To update the workflow, you can modify the code in [`workflow.py`](app/workflow.py).
You can start by sending an request on the [chat UI](http://localhost:8000) or you can test the `/api/chat` endpoint with the following curl request:
```
curl --location 'localhost:8000/api/chat' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "Show me the files in the current directory" }] }'
```
## How does HITL work?
### Events
The human-in-the-loop approach used here is based on a simple idea: the workflow pauses and waits for a human response before proceeding to the next step.
To do this, you will need to implement two custom events:
- [HumanInputEvent](https://github.com/run-llama/create-llama/blob/main/packages/server/src/utils/hitl/events.ts): This event is used to request input from the user.
- [HumanResponseEvent](https://github.com/run-llama/create-llama/blob/main/packages/server/src/utils/hitl/events.ts): This event is sent to the workflow to resume execution with input from the user.
In this example, we have implemented these two custom events in [`events.ts`](src/app/events.ts):
- `cliHumanInputEvent` to request input from the user for CLI command execution.
- `cliHumanResponseEvent` to resume the workflow with the response from the user.
```typescript
export const cliHumanInputEvent = humanInputEvent<{
type: "cli_human_input";
data: { command: string };
response: typeof cliHumanResponseEvent;
}>();
export const cliHumanResponseEvent = humanResponseEvent<{
type: "human_response";
data: { execute: boolean; command: string };
}>();
```
### UI Component
HITL also needs a custom UI component, that is shown when the LlamaIndexServer receives the `cliHumanInputEvent`. The name of the component is defined in the `type` field of the `cliHumanInputEvent` - in our case, it is `cli_human_input`, which corresponds to the [cli_human_input.tsx](./components/cli_human_input.tsx) component.
The custom component must use `append` to send a message with a `human_response` annotation. The data of the annotation must be in the format of the response event `cliHumanResponseEvent`, in our case, for sending to execute the command `ls -l`, we would send:
```tsx
append({
content: "Yes",
role: "user",
annotations: [
{
type: "human_response",
data: {
execute: true,
command: "ls -l", // The command to execute
},
},
],
});
```
This component displays the command to execute and the user can choose to execute or cancel the command execution.
## Eject Mode
If you want to fully customize the server UI and routes, you can use `npm eject`. It will create a normal Next.js project with the same functionality as @llamaindex/server.
```bash
npm run eject
```
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai/docs/llamaindex) - learn about LlamaIndex (Typescript features).
- [Workflows Introduction](https://ts.llamaindex.ai/docs/llamaindex/modules/workflows) - learn about LlamaIndexTS workflows.
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
@@ -0,0 +1,12 @@
import { humanInputEvent, humanResponseEvent } from "@llamaindex/server";
export const cliHumanInputEvent = humanInputEvent<{
type: "cli_human_input";
data: { command: string };
response: typeof cliHumanResponseEvent;
}>();
export const cliHumanResponseEvent = humanResponseEvent<{
type: "human_response";
data: { execute: boolean; command: string };
}>();
@@ -0,0 +1,20 @@
import { execSync } from "child_process";
import { tool } from "llamaindex";
import { z } from "zod";
export const cliExecutor = tool({
name: "cli_executor",
description: "This tool executes a command and returns the output.",
parameters: z.object({ command: z.string() }),
execute: async ({ command }) => {
try {
const output = execSync(command, {
encoding: "utf-8",
});
return output;
} catch (error) {
console.error(error);
return "Command failed";
}
},
});
@@ -0,0 +1,101 @@
import { toAgentRunEvent, writeResponseToStream } from "@llamaindex/server";
import { chatWithTools } from "@llamaindex/tools";
import {
createWorkflow,
getContext,
startAgentEvent,
stopAgentEvent,
withSnapshot,
workflowEvent,
} from "@llamaindex/workflow";
import { ChatMessage, Settings, ToolCallLLM } from "llamaindex";
import { cliHumanInputEvent, cliHumanResponseEvent } from "./events";
import { cliExecutor } from "./tools";
const summaryEvent = workflowEvent<string>(); // simple event to summarize the result
export const workflowFactory = (body: unknown) => {
const llm = Settings.llm as ToolCallLLM;
if (!llm.supportToolCall) {
throw new Error("LLM is not a ToolCallLLM");
}
const { messages } = body as { messages: ChatMessage[] };
const workflow = withSnapshot(createWorkflow());
workflow.handle([startAgentEvent], async ({ data }) => {
const { userInput, chatHistory = [] } = data;
if (!userInput) {
throw new Error("User input is required");
}
// in this example, we use chatWithTools to decide should perform a tool call or not
// if cli executor is called, emit HumanInputEvent to ask user for permission
const toolCallResponse = await chatWithTools(
llm,
[cliExecutor],
chatHistory.concat({ role: "user", content: userInput }),
);
const cliExecutorToolCall = toolCallResponse.toolCalls.find(
(toolCall) => toolCall.name === cliExecutor.metadata.name,
);
const command = cliExecutorToolCall?.input?.command as string;
if (command) {
return cliHumanInputEvent.with({
type: "cli_human_input",
data: { command },
response: cliHumanResponseEvent,
});
}
// if no tool call, just response as normal
return summaryEvent.with("");
});
// do actions after getting response from human
workflow.handle([cliHumanResponseEvent], async ({ data }) => {
const { sendEvent } = getContext();
const { command, execute } = data.data;
if (!execute) {
// stop the workflow if user reject to execute the command
return summaryEvent.with(`User reject to execute the command ${command}`);
}
sendEvent(
toAgentRunEvent({
agent: "CLI Executor",
text: `Execute the command "${command}" and return the result`,
type: "text",
}),
);
const result = (await cliExecutor.call({ command })) as string;
return summaryEvent.with(
`Executed the command ${command} and got the result: ${result}`,
);
});
workflow.handle([summaryEvent], async ({ data: summaryResult }) => {
const { sendEvent } = getContext();
const chatHistory = messages;
if (summaryResult) {
chatHistory.push({ role: "user", content: summaryResult });
}
const stream = await llm.chat({
messages: chatHistory,
stream: true,
});
const result = await writeResponseToStream(stream, sendEvent);
return stopAgentEvent.with({ result });
});
return workflow;
};
@@ -12,7 +12,7 @@
"dependencies": {
"@llamaindex/openai": "~0.4.0",
"@llamaindex/server": "~0.2.1",
"@llamaindex/workflow": "~1.1.3",
"@llamaindex/workflow": "~1.1.8",
"@llamaindex/tools": "~0.0.11",
"llamaindex": "~0.11.0",
"dotenv": "^16.4.7",
@@ -20,7 +20,7 @@
},
"devDependencies": {
"@types/node": "^20.10.3",
"tsx": "^4.7.2",
"tsx": "4.7.2",
"typescript": "^5.3.2",
"nodemon": "^3.1.10"
}
+1
View File
@@ -8,6 +8,7 @@ LlamaIndexServer is a Next.js-based application that allows you to quickly launc
- Edit code and document artifacts in an OpenAI Canvas-style UI
- Extendable UI components for events and headers
- Built on Next.js for high performance and easy API development
- Human-in-the-loop (HITL) support, check out the [Human-in-the-loop](https://github.com/run-llama/create-llama/blob/main/packages/server/examples/hitl/README.md) documentation for more details.
## Installation
+172
View File
@@ -0,0 +1,172 @@
# Human in the Loop
This example shows how to use the LlamaIndexServer with a human in the loop. It allows you to start CLI commands that are reviewed by a human before execution.
## Getting Started
### Environment Setup
Export your OpenAI API key:
```bash
export OPENAI_API_KEY=<your-openai-api-key>
```
### Starting the Server
Run the server in development mode:
```bash
npx nodemon --exec tsx index.ts --ignore output/*
```
### Access the Application
Open your browser and go to:
```
http://localhost:3000
```
You will see the LlamaIndexServer UI, where you can interact with the HITL agent. Try "List all files in the current directory" and see how the agent pauses and waits for a human response before executing the command.
## How does HITL work?
### Events
The human-in-the-loop approach used here is based on a simple idea: the workflow pauses and waits for a human response before proceeding to the next step.
To do this, you will need to implement two custom events:
- [HumanInputEvent](https://github.com/run-llama/create-llama/blob/main/packages/server/src/utils/hitl/events.ts): This event is used to request input from the user.
- [HumanResponseEvent](https://github.com/run-llama/create-llama/blob/main/packages/server/src/utils/hitl/events.ts): This event is sent to the workflow to resume execution with input from the user.
In this example, we have implemented these two custom events in [`events.ts`](src/app/events.ts):
- `cliHumanInputEvent` to request input from the user for CLI command execution.
- `cliHumanResponseEvent` to resume the workflow with the response from the user.
```typescript
export const cliHumanInputEvent = humanInputEvent<{
type: "cli_human_input";
data: { command: string };
response: typeof cliHumanResponseEvent;
}>();
export const cliHumanResponseEvent = humanResponseEvent<{
type: "human_response";
data: { execute: boolean; command: string };
}>();
```
### UI Component
HITL also needs a custom UI component, that is shown when the LlamaIndexServer receives the `cliHumanInputEvent`. The name of the component is defined in the `type` field of the `cliHumanInputEvent` - in our case, it is `cli_human_input`, which corresponds to the [cli_human_input.tsx](./components/cli_human_input.tsx) component.
The custom component must use `append` to send a message with a `human_response` annotation. The data of the annotation must be in the format of the response event `cliHumanResponseEvent`, in our case, for sending to execute the command `ls -l`, we would send:
```tsx
append({
content: "Yes",
role: "user",
annotations: [
{
type: "human_response",
data: {
execute: true,
command: "ls -l", // The command to execute
},
},
],
});
```
This component displays the command to execute and the user can choose to execute or cancel the command execution.
### Workflow Implementation
The workflow is implemented in [`workflow.ts`](src/app/workflow.ts) using LlamaIndex workflows. The workflow handles three main steps:
1. **Initial Request Handling**: When a user input is received, the workflow uses `chatWithTools` to determine if a CLI command should be executed. If so, it emits a `cliHumanInputEvent` to request user permission.
```typescript
workflow.handle([startAgentEvent], async ({ data }) => {
const { userInput, chatHistory = [] } = data;
const toolCallResponse = await chatWithTools(
llm,
[cliExecutor],
chatHistory.concat({ role: "user", content: userInput }),
);
const cliExecutorToolCall = toolCallResponse.toolCalls.find(
(toolCall) => toolCall.name === cliExecutor.metadata.name,
);
const command = cliExecutorToolCall?.input?.command as string;
if (command) {
return cliHumanInputEvent.with({
type: "cli_human_input",
data: { command },
response: cliHumanResponseEvent,
});
}
return summaryEvent.with("");
});
```
2. **Human Response Handling**: After receiving human input, the workflow either executes the command or cancels based on the user's choice.
```typescript
workflow.handle([cliHumanResponseEvent], async ({ data }) => {
const { command, execute } = data.data;
if (!execute) {
return summaryEvent.with(`User reject to execute the command ${command}`);
}
const result = (await cliExecutor.call({ command })) as string;
return summaryEvent.with(
`Executed the command ${command} and got the result: ${result}`,
);
});
```
3. **Final Response**: The workflow generates a final response based on the execution result and streams it back to the user.
### Tools
The CLI executor tool is defined in [`tools.ts`](src/app/tools.ts):
```typescript
export const cliExecutor = tool({
name: "cli_executor",
description: "This tool executes a command and returns the output.",
parameters: z.object({ command: z.string() }),
execute: async ({ command }) => {
try {
const output = execSync(command, {
encoding: "utf-8",
});
return output;
} catch (error) {
console.error(error);
return "Command failed";
}
},
});
```
## Architecture
The HITL implementation consists of:
1. **Workflow Factory** (`workflow.ts`): Creates and configures the workflow with event handlers
2. **Events** (`events.ts`): Defines typed events for human input and response
3. **Tools** (`tools.ts`): Implements the CLI executor tool
4. **UI Component** (`components/cli_human_input.tsx`): Provides the user interface for human approval
5. **Server Entry** (`index.ts`): Configures and starts the LlamaIndexServer
This architecture ensures that dangerous operations like CLI command execution require explicit human approval before proceeding.
@@ -0,0 +1,95 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardFooter } from "@/components/ui/card";
import { JSONValue, useChatUI } from "@llamaindex/chat-ui";
import React, { FC, useState } from "react";
import { z } from "zod";
// This schema is equivalent to the CLICommand model defined in events.py
const CLIInputEventSchema = z.object({
command: z.string(),
});
type CLIInputEvent = z.infer<typeof CLIInputEventSchema>;
const CLIHumanInput: FC<{
events: JSONValue[];
}> = ({ events }) => {
const inputEvent = (events || [])
.map((ev) => {
const parseResult = CLIInputEventSchema.safeParse(ev);
return parseResult.success ? parseResult.data : null;
})
.filter((ev): ev is CLIInputEvent => ev !== null)
.at(-1);
const { append } = useChatUI();
const [confirmedValue, setConfirmedValue] = useState<boolean | null>(null);
const [editableCommand, setEditableCommand] = useState<string | undefined>(
inputEvent?.command,
);
// Update editableCommand if inputEvent changes (e.g. new event comes in)
React.useEffect(() => {
setEditableCommand(inputEvent?.command);
}, [inputEvent?.command]);
const handleConfirm = () => {
append({
content: "Yes",
role: "user",
annotations: [
{
type: "human_response",
data: {
execute: true,
command: editableCommand, // Use editable command
},
},
],
});
setConfirmedValue(true);
};
const handleCancel = () => {
append({
content: "No",
role: "user",
annotations: [
{
type: "human_response",
data: {
execute: false,
command: inputEvent?.command,
},
},
],
});
setConfirmedValue(false);
};
return (
<Card className="my-4">
<CardContent className="pt-6">
<p className="text-sm text-gray-700">
Do you want to execute the following command?
</p>
<input
disabled
type="text"
value={editableCommand || ""}
onChange={(e) => setEditableCommand(e.target.value)}
className="my-2 w-full overflow-x-auto rounded border border-gray-300 bg-gray-100 p-3 font-mono text-xs text-gray-800"
/>
</CardContent>
{confirmedValue === null ? (
<CardFooter className="flex justify-end gap-2">
<>
<Button onClick={handleConfirm}>Yes</Button>
<Button onClick={handleCancel}>No</Button>
</>
</CardFooter>
) : null}
</Card>
);
};
export default CLIHumanInput;
+20
View File
@@ -0,0 +1,20 @@
import { OpenAI } from "@llamaindex/openai";
import { LlamaIndexServer } from "@llamaindex/server";
import { Settings } from "llamaindex";
import { workflowFactory } from "./src/app/workflow";
Settings.llm = new OpenAI({
model: "gpt-4o-mini",
});
new LlamaIndexServer({
workflow: workflowFactory,
uiConfig: {
starterQuestions: [
"Check status of git in the current directory",
"List all files in the current directory",
],
componentsDir: "components",
},
port: 3000,
}).start();
@@ -0,0 +1,12 @@
import { humanInputEvent, humanResponseEvent } from "@llamaindex/server";
export const cliHumanInputEvent = humanInputEvent<{
type: "cli_human_input";
data: { command: string };
response: typeof cliHumanResponseEvent;
}>();
export const cliHumanResponseEvent = humanResponseEvent<{
type: "human_response";
data: { execute: boolean; command: string };
}>();
@@ -0,0 +1,20 @@
import { execSync } from "child_process";
import { tool } from "llamaindex";
import { z } from "zod";
export const cliExecutor = tool({
name: "cli_executor",
description: "This tool executes a command and returns the output.",
parameters: z.object({ command: z.string() }),
execute: async ({ command }) => {
try {
const output = execSync(command, {
encoding: "utf-8",
});
return output;
} catch (error) {
console.error(error);
return "Command failed";
}
},
});
@@ -0,0 +1,106 @@
import { OpenAI } from "@llamaindex/openai";
import { toAgentRunEvent, writeResponseToStream } from "@llamaindex/server";
import { chatWithTools } from "@llamaindex/tools";
import {
createWorkflow,
getContext,
startAgentEvent,
stopAgentEvent,
withSnapshot,
workflowEvent,
} from "@llamaindex/workflow";
import { ChatMessage, Settings, ToolCallLLM } from "llamaindex";
import { cliHumanInputEvent, cliHumanResponseEvent } from "./events";
import { cliExecutor } from "./tools";
Settings.llm = new OpenAI({
model: "gpt-4o-mini",
});
const summaryEvent = workflowEvent<string>(); // simple event to summarize the result
export const workflowFactory = (body: unknown) => {
const llm = Settings.llm as ToolCallLLM;
if (!llm.supportToolCall) {
throw new Error("LLM is not a ToolCallLLM");
}
const { messages } = body as { messages: ChatMessage[] };
const workflow = withSnapshot(createWorkflow());
workflow.handle([startAgentEvent], async ({ data }) => {
const { userInput, chatHistory = [] } = data;
if (!userInput) {
throw new Error("User input is required");
}
// in this example, we use chatWithTools to decide should perform a tool call or not
// if cli executor is called, emit HumanInputEvent to ask user for permission
const toolCallResponse = await chatWithTools(
llm,
[cliExecutor],
chatHistory.concat({ role: "user", content: userInput }),
);
const cliExecutorToolCall = toolCallResponse.toolCalls.find(
(toolCall) => toolCall.name === cliExecutor.metadata.name,
);
const command = cliExecutorToolCall?.input?.command as string;
if (command) {
return cliHumanInputEvent.with({
type: "cli_human_input",
data: { command },
response: cliHumanResponseEvent,
});
}
// if no tool call, just response as normal
return summaryEvent.with("");
});
// do actions after getting response from human
workflow.handle([cliHumanResponseEvent], async ({ data }) => {
const { sendEvent } = getContext();
const { command, execute } = data.data;
if (!execute) {
// stop the workflow if user reject to execute the command
return summaryEvent.with(`User reject to execute the command ${command}`);
}
sendEvent(
toAgentRunEvent({
agent: "CLI Executor",
text: `Execute the command "${command}" and return the result`,
type: "text",
}),
);
const result = (await cliExecutor.call({ command })) as string;
return summaryEvent.with(
`Executed the command ${command} and got the result: ${result}`,
);
});
workflow.handle([summaryEvent], async ({ data: summaryResult }) => {
const { sendEvent } = getContext();
const chatHistory = messages;
if (summaryResult) {
chatHistory.push({ role: "user", content: summaryResult });
}
const stream = await llm.chat({
messages: chatHistory,
stream: true,
});
const result = await writeResponseToStream(stream, sendEvent);
return stopAgentEvent.with({ result });
});
return workflow;
};
+1 -1
View File
@@ -18,7 +18,7 @@
"devDependencies": {
"@types/node": "^20.10.3",
"nodemon": "^3.1.10",
"tsx": "^4.7.2",
"tsx": "4.7.2",
"typescript": "^5.3.2"
}
}
+1 -1
View File
@@ -10,5 +10,5 @@
"outDir": "dist"
},
"include": ["**/*"],
"exclude": ["node_modules", "dist", "custom-layout/layout"]
"exclude": ["node_modules", "dist", "custom-layout/layout", "hitl/components"]
}
+24 -13
View File
@@ -1,16 +1,19 @@
import { type AgentInputData } from "@llamaindex/workflow";
import { type Message } from "ai";
import { type MessageType } from "llamaindex";
import { NextRequest, NextResponse } from "next/server";
// import chat utils
import {
getHumanResponsesFromMessage,
pauseForHumanInput,
processWorkflowStream,
runWorkflow,
sendSuggestedQuestionsEvent,
toDataStream,
} from "./utils";
// import workflow factory and settings from local file
import { stopAgentEvent } from "@llamaindex/workflow";
import { initSettings } from "./app/settings";
import { workflowFactory } from "./app/workflow";
@@ -21,7 +24,10 @@ export async function POST(req: NextRequest) {
const reqBody = await req.json();
const suggestNextQuestions = process.env.SUGGEST_NEXT_QUESTIONS === "true";
const { messages } = reqBody as { messages: Message[] };
const { messages, id: requestId } = reqBody as {
messages: Message[];
id?: string;
};
const chatHistory = messages.map((message) => ({
role: message.role as MessageType,
content: message.content,
@@ -36,25 +42,31 @@ export async function POST(req: NextRequest) {
{ status: 400 },
);
}
const workflowInput: AgentInputData = {
userInput: lastMessage.content,
chatHistory,
};
const abortController = new AbortController();
req.signal.addEventListener("abort", () =>
abortController.abort("Connection closed"),
);
const workflow = await workflowFactory(reqBody);
const workflowEventStream = await runWorkflow(
workflow,
workflowInput,
abortController.signal,
const context = await runWorkflow({
workflow: await workflowFactory(reqBody),
input: { userInput: lastMessage.content, chatHistory },
human: {
snapshotId: requestId, // use requestId to restore snapshot
responses: getHumanResponsesFromMessage(lastMessage),
},
});
const stream = processWorkflowStream(context.stream).until(
(event) =>
abortController.signal.aborted || stopAgentEvent.include(event),
);
const dataStream = toDataStream(workflowEventStream, {
const dataStream = toDataStream(stream, {
callbacks: {
onPauseForHumanInput: async (responseEvent) => {
await pauseForHumanInput(context, responseEvent, requestId); // use requestId to save snapshot
},
onFinal: async (completion, dataStreamWriter) => {
chatHistory.push({
role: "assistant" as MessageType,
@@ -66,7 +78,6 @@ export async function POST(req: NextRequest) {
},
},
});
return new Response(dataStream, {
status: 200,
headers: {
+1 -1
View File
@@ -114,7 +114,7 @@
},
"peerDependencies": {
"@llamaindex/env": "~0.1.30",
"@llamaindex/workflow": "~1.1.3",
"@llamaindex/workflow": "~1.1.8",
"llamaindex": "~0.11.0",
"zod": "^3.24.2",
"zod-to-json-schema": "^3.23.3"
+2 -2
View File
@@ -31,7 +31,7 @@
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^4",
"tsx": "^4.19.3",
"tsx": "4.7.2",
"tw-animate-css": "1.2.5",
"typescript": "^5"
},
@@ -46,7 +46,7 @@
"@llamaindex/openai": "~0.4.0",
"@llamaindex/readers": "~3.1.4",
"@llamaindex/tools": "~0.0.11",
"@llamaindex/workflow": "~1.1.3",
"@llamaindex/workflow": "~1.1.8",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.7",
"@radix-ui/react-aspect-ratio": "^1.1.3",
+37 -23
View File
@@ -1,16 +1,20 @@
import type { AgentInputData } from "@llamaindex/workflow";
import { stopAgentEvent } from "@llamaindex/workflow";
import { type Message } from "ai";
import { IncomingMessage, ServerResponse } from "http";
import type { MessageType } from "llamaindex";
import { type WorkflowFactory } from "../types";
import { sendSuggestedQuestionsEvent } from "../utils";
import {
getHumanResponsesFromMessage,
pauseForHumanInput,
} from "../utils/hitl";
import {
parseRequestBody,
pipeStreamToResponse,
sendJSONResponse,
} from "../utils/request";
import { toDataStream } from "../utils/stream";
import { sendSuggestedQuestionsEvent } from "../utils/suggestion";
import { runWorkflow } from "../utils/workflow";
import { processWorkflowStream, runWorkflow } from "../utils/workflow";
export const handleChat = async (
req: IncomingMessage,
@@ -18,37 +22,47 @@ export const handleChat = async (
workflowFactory: WorkflowFactory,
suggestNextQuestions: boolean,
) => {
const abortController = new AbortController();
res.on("close", () => abortController.abort("Connection closed"));
try {
const body = await parseRequestBody(req);
const { messages } = body as { messages: Message[] };
const { messages, id: requestId } = body as {
messages: Message[];
id?: string;
};
const lastMessage = messages[messages.length - 1];
if (lastMessage?.role !== "user" || !lastMessage.content) {
return sendJSONResponse(res, 400, {
error: "Messages cannot be empty and last message must be from user",
});
}
const chatHistory = messages.map((message) => ({
role: message.role as MessageType,
content: message.content,
}));
const lastMessage = messages[messages.length - 1];
if (lastMessage?.role !== "user") {
return sendJSONResponse(res, 400, {
error: "Messages cannot be empty and last message must be from user",
});
}
const workflowInput: AgentInputData = {
userInput: lastMessage.content,
chatHistory,
};
const context = await runWorkflow({
workflow: await workflowFactory(body),
input: { userInput: lastMessage.content, chatHistory },
human: {
snapshotId: requestId, // use requestId to restore snapshot
responses: getHumanResponsesFromMessage(lastMessage),
},
});
const abortController = new AbortController();
res.on("close", () => abortController.abort("Connection closed"));
const workflow = await workflowFactory(body);
const workflowEventStream = await runWorkflow(
workflow,
workflowInput,
abortController.signal,
const stream = processWorkflowStream(context.stream).until(
(event) =>
abortController.signal.aborted || stopAgentEvent.include(event),
);
const dataStream = toDataStream(workflowEventStream, {
const dataStream = toDataStream(stream, {
callbacks: {
onPauseForHumanInput: async (responseEvent) => {
await pauseForHumanInput(context, responseEvent, requestId); // use requestId to save snapshot
},
onFinal: async (completion, dataStreamWriter) => {
chatHistory.push({
role: "assistant" as MessageType,
+2
View File
@@ -3,5 +3,7 @@ export * from "./types";
export * from "./utils/events";
export { getStoredFilePath } from "./utils/file";
export { generateEventComponent } from "./utils/gen-ui";
export * from "./utils/hitl";
export * from "./utils/inline";
export * from "./utils/prompts";
export * from "./utils/stream";
+64
View File
@@ -0,0 +1,64 @@
import {
type WorkflowEvent,
type WorkflowEventData,
workflowEvent,
} from "@llamaindex/workflow";
import type { Message } from "ai";
import type { JSONValue } from "llamaindex";
import z from "zod";
export type HumanInputEventData = {
type: string;
data?: JSONValue;
response: WorkflowEvent<HumanResponseEventData>;
};
export const humanInputEvent = workflowBaseEvent<HumanInputEventData>();
export type HumanResponseEventData = {
type: "human_response";
data?: JSONValue;
};
export const humanResponseEvent = workflowBaseEvent<HumanResponseEventData>();
// helper function to extract human responses from message annotations
export const getHumanResponsesFromMessage = (message: Message) => {
const schema = z.object({ type: z.literal("human_response"), data: z.any() });
return (
message.annotations?.filter(
(annotation): annotation is z.infer<typeof schema> =>
schema.safeParse(annotation).success,
) ?? []
);
};
// TODO: move to llama-flow package
export type BaseEvent<K> = (<T extends K>() => WorkflowEvent<T>) &
WorkflowEvent<K>;
export function workflowBaseEvent<K = unknown>(): BaseEvent<K> {
const baseEvent = workflowEvent<K>();
const derivedEvents = new Set<WorkflowEvent<unknown>>();
function eventFn<T>(): WorkflowEvent<T> {
const event = workflowEvent<T>();
derivedEvents.add(event);
return event;
}
const originalInclude = baseEvent.include;
const enhancedBaseEvent = Object.assign(baseEvent, {
include: (
instance: WorkflowEventData<unknown>,
): instance is WorkflowEventData<void> => {
// Base event accepts its own instances OR instances from any derived events
return (
originalInclude(instance) ||
Array.from(derivedEvents).some((e) => e.include(instance))
);
},
});
return Object.assign(eventFn, enhancedBaseEvent) as typeof eventFn &
typeof baseEvent;
}
+4
View File
@@ -0,0 +1,4 @@
export * from "./events";
export * from "./pause";
export * from "./resume";
export * from "./snapshot";
+25
View File
@@ -0,0 +1,25 @@
import {
request,
type WorkflowContext,
type WorkflowEvent,
} from "@llamaindex/workflow";
import { randomUUID } from "node:crypto";
import type { HumanResponseEventData } from "./events";
import { ensureSnapshotWorkflowContext, saveSnapshot } from "./snapshot";
// pause the workflow and save the snapshot
export const pauseForHumanInput = async (
context: WorkflowContext,
responseEvent: WorkflowEvent<HumanResponseEventData>,
snapshotId: string = randomUUID(), // automatically generate a request id if not provided
) => {
const snapshotWorkflowContext = ensureSnapshotWorkflowContext(context);
const { snapshot, sendEvent } = snapshotWorkflowContext;
// send a request event to save the missing step (`humanResponseEvent`) to the snapshot
sendEvent(request(responseEvent));
// get and save snapshot
const [_, snapshotData] = await snapshot();
await saveSnapshot(snapshotId, snapshotData);
};
+28
View File
@@ -0,0 +1,28 @@
import { type Workflow } from "@llamaindex/workflow";
import type { HumanResponseEventData } from "./events";
import {
ensureSnapshotWorkflow,
loadSnapshot,
type SnapshotWorkflowContext,
} from "./snapshot";
// create workflow context from snapshot and start running it from the last missing step
export const resumeWorkflowFromHumanResponses = async (
workflow: Workflow, // the workflow to resume
humanResponses: Array<HumanResponseEventData>, // human can send multiple responses
snapshotId: string,
): Promise<SnapshotWorkflowContext> => {
// check workflow is snapshotable
const snapshotWorkflow = ensureSnapshotWorkflow(workflow);
const snapshot = await loadSnapshot(snapshotId);
if (!snapshot) {
// if there is no snapshot, we can't resume the workflow
throw new Error("No snapshot found for request id: " + snapshotId);
}
// resume the workflow from the snapshot with human response
const context = snapshotWorkflow.resume(humanResponses, snapshot);
return context;
};
@@ -0,0 +1,78 @@
import {
withSnapshot,
type Workflow,
type WorkflowContext,
} from "@llamaindex/workflow";
import { promises as fs } from "fs";
import path from "path";
// @llama-flow doesn't export snapshot types, we need to infer them from the functions
export type SnapshotWorkflow = ReturnType<typeof withSnapshot<Workflow>>;
export type SnapshotWorkflowContext = ReturnType<
SnapshotWorkflow["createContext"]
>;
export type SnapshotData = Awaited<
ReturnType<SnapshotWorkflowContext["snapshot"]>
>[1];
const SNAPSHOTS_DIR = path.join("output", "snapshots");
// Ensure the checkpoints directory exists
const ensureCheckpointsDir = async () => {
try {
await fs.mkdir(SNAPSHOTS_DIR, { recursive: true });
} catch (error) {
console.error("Failed to create checkpoints directory:", error);
}
};
export const saveSnapshot = async (
requestId: string,
snapshot: SnapshotData,
) => {
try {
await ensureCheckpointsDir();
const filePath = path.join(SNAPSHOTS_DIR, `${requestId}.json`);
await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
console.log(`Snapshot saved to: ${filePath}`);
} catch (error) {
console.error("Failed to save snapshot:", error);
throw error;
}
};
export const loadSnapshot = async (
requestId: string,
): Promise<SnapshotData | undefined> => {
try {
const filePath = path.join(SNAPSHOTS_DIR, `${requestId}.json`);
const data = await fs.readFile(filePath, "utf8");
return JSON.parse(data);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return undefined; // File doesn't exist
}
console.error("Failed to load snapshot:", error);
throw error;
}
};
export function ensureSnapshotWorkflow(workflow: Workflow): SnapshotWorkflow {
if (!("resume" in workflow)) {
throw new Error(
"Workflow is not a snapshot workflow. Please use withSnapshot() to make it snapshotable.",
);
}
return workflow as SnapshotWorkflow;
}
export function ensureSnapshotWorkflowContext(
context: WorkflowContext,
): SnapshotWorkflowContext {
if (!("snapshot" in context)) {
throw new Error(
"Cannot get snapshot of the workflow. Please use withSnapshot() to make workflow snapshotable.",
);
}
return context as SnapshotWorkflowContext;
}
+1
View File
@@ -1,6 +1,7 @@
export * from "./events";
export * from "./file";
export * from "./gen-ui";
export * from "./hitl";
export * from "./inline";
export * from "./prompts";
export * from "./request";
+41 -1
View File
@@ -1,10 +1,16 @@
import { agentStreamEvent, type WorkflowEventData } from "@llamaindex/workflow";
import {
agentStreamEvent,
type WorkflowEvent,
type WorkflowEventData,
} from "@llamaindex/workflow";
import {
createDataStream,
formatDataStreamPart,
type DataStreamWriter,
type JSONValue,
} from "ai";
import type { ChatResponseChunk } from "llamaindex";
import { humanInputEvent, type HumanResponseEventData } from "./hitl";
/**
* Configuration options and helper callback methods for stream lifecycle events.
@@ -24,6 +30,11 @@ export interface StreamCallbacks {
text: string,
dataStreamWriter: DataStreamWriter,
) => Promise<void> | void;
/** `onPauseForHumanInput`: Called when human input event is emitted. */
onPauseForHumanInput?:
| ((event: WorkflowEvent<HumanResponseEventData>) => Promise<void> | void)
| undefined;
}
/**
@@ -61,6 +72,14 @@ export function toDataStream(
await callbacks.onText(content, dataStreamWriter);
}
}
} else if (humanInputEvent.include(event)) {
const { response, ...rest } = event.data;
dataStreamWriter.writeMessageAnnotation(rest); // show human input in UI
if (callbacks?.onPauseForHumanInput) {
await callbacks.onPauseForHumanInput(response);
return; // stop the stream
}
} else {
dataStreamWriter.writeMessageAnnotation(event.data as JSONValue);
}
@@ -78,3 +97,24 @@ export function toDataStream(
},
});
}
export async function writeResponseToStream(
generator: AsyncIterable<ChatResponseChunk<object>>,
sendEvent: (event: WorkflowEventData<unknown>) => void,
) {
let response = "";
if (generator) {
for await (const chunk of generator) {
response += chunk.delta;
sendEvent(
agentStreamEvent.with({
delta: chunk.delta,
response,
currentAgentName: "LLM",
raw: chunk.raw,
}),
);
}
}
return response;
}
+38 -21
View File
@@ -1,12 +1,11 @@
import {
agentToolCallEvent,
agentToolCallResultEvent,
run,
startAgentEvent,
stopAgentEvent,
WorkflowStream,
type AgentInputData,
type Workflow,
type WorkflowContext,
type WorkflowEventData,
} from "@llamaindex/workflow";
import {
@@ -22,30 +21,48 @@ import {
type SourceEventNode,
} from "./events";
import { downloadFile } from "./file";
import {
resumeWorkflowFromHumanResponses,
type HumanResponseEventData,
} from "./hitl/index";
import { toInlineAnnotationEvent } from "./inline";
export async function runWorkflow(
workflow: Workflow,
input: AgentInputData,
abortSignal?: AbortSignal,
): Promise<WorkflowStream<WorkflowEventData<unknown>>> {
if (!input.userInput) {
throw new Error("Missing user input to start the workflow");
}
const workflowStream = run(workflow, [
startAgentEvent.with({
userInput: input.userInput,
chatHistory: input.chatHistory,
}),
]);
export async function runWorkflow({
workflow,
input,
human,
}: {
workflow: Workflow;
input: AgentInputData;
human?: {
snapshotId?: string | undefined; // the snapshot id to restore workflow
responses?: HumanResponseEventData[]; // the data from human to trigger events after restoring
};
}): Promise<WorkflowContext> {
let context: WorkflowContext;
// Transform the stream to handle annotations
return processWorkflowStream(workflowStream).until(
(event) => abortSignal?.aborted || stopAgentEvent.include(event),
);
if (human?.responses?.length && human?.snapshotId) {
// resume the workflow if there is human response
context = await resumeWorkflowFromHumanResponses(
workflow,
human.responses,
human.snapshotId,
);
} else {
// otherwise, create a new empty context and run the workflow with startAgentEvent
context = workflow.createContext();
context.sendEvent(
startAgentEvent.with({
userInput: input.userInput,
chatHistory: input.chatHistory,
}),
);
}
return context;
}
function processWorkflowStream(
export function processWorkflowStream(
stream: WorkflowStream<WorkflowEventData<unknown>>,
) {
return stream.pipeThrough(
+273 -23
View File
@@ -187,8 +187,8 @@ importers:
specifier: ~0.1.30
version: 0.1.30
'@llamaindex/workflow':
specifier: ~1.1.3
version: 1.1.3(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)
specifier: ~1.1.8
version: 1.1.8(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)
'@radix-ui/react-accordion':
specifier: ^1.2.3
version: 1.2.8(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -342,7 +342,7 @@ importers:
version: 7.20.7
llamaindex:
specifier: ~0.11.0
version: 0.11.1(@llama-flow/core@0.4.1(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3))(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(zod@3.24.3)
version: 0.11.1(@llama-flow/core@0.4.4(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3))(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(zod@3.24.3)
postcss:
specifier: ^8.5.3
version: 8.5.3
@@ -381,7 +381,7 @@ importers:
version: 16.5.0
llamaindex:
specifier: ~0.11.0
version: 0.11.1(@llama-flow/core@0.4.1(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.25.13))(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(zod@3.25.13)
version: 0.11.1(@llama-flow/core@0.4.4(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.25.13))(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(zod@3.25.13)
zod:
specifier: ^3.24.2
version: 3.25.13
@@ -393,8 +393,8 @@ importers:
specifier: ^3.1.10
version: 3.1.10
tsx:
specifier: ^4.7.2
version: 4.19.3
specifier: 4.7.2
version: 4.7.2
typescript:
specifier: ^5.3.2
version: 5.8.3
@@ -699,6 +699,12 @@ packages:
'@emnapi/wasi-threads@1.0.2':
resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==}
'@esbuild/aix-ppc64@0.19.12':
resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [aix]
'@esbuild/aix-ppc64@0.21.5':
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
engines: {node: '>=12'}
@@ -711,6 +717,12 @@ packages:
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.19.12':
resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm64@0.21.5':
resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
engines: {node: '>=12'}
@@ -723,6 +735,12 @@ packages:
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.19.12':
resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==}
engines: {node: '>=12'}
cpu: [arm]
os: [android]
'@esbuild/android-arm@0.21.5':
resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
engines: {node: '>=12'}
@@ -735,6 +753,12 @@ packages:
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.19.12':
resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==}
engines: {node: '>=12'}
cpu: [x64]
os: [android]
'@esbuild/android-x64@0.21.5':
resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
engines: {node: '>=12'}
@@ -747,6 +771,12 @@ packages:
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.19.12':
resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-arm64@0.21.5':
resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
engines: {node: '>=12'}
@@ -759,6 +789,12 @@ packages:
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.19.12':
resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
'@esbuild/darwin-x64@0.21.5':
resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
engines: {node: '>=12'}
@@ -771,6 +807,12 @@ packages:
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.19.12':
resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-arm64@0.21.5':
resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
engines: {node: '>=12'}
@@ -783,6 +825,12 @@ packages:
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.19.12':
resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==}
engines: {node: '>=12'}
cpu: [x64]
os: [freebsd]
'@esbuild/freebsd-x64@0.21.5':
resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
engines: {node: '>=12'}
@@ -795,6 +843,12 @@ packages:
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.19.12':
resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm64@0.21.5':
resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
engines: {node: '>=12'}
@@ -807,6 +861,12 @@ packages:
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.19.12':
resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==}
engines: {node: '>=12'}
cpu: [arm]
os: [linux]
'@esbuild/linux-arm@0.21.5':
resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
engines: {node: '>=12'}
@@ -819,6 +879,12 @@ packages:
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.19.12':
resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==}
engines: {node: '>=12'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-ia32@0.21.5':
resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
engines: {node: '>=12'}
@@ -831,6 +897,12 @@ packages:
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.19.12':
resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-loong64@0.21.5':
resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
engines: {node: '>=12'}
@@ -843,6 +915,12 @@ packages:
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.19.12':
resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==}
engines: {node: '>=12'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-mips64el@0.21.5':
resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
engines: {node: '>=12'}
@@ -855,6 +933,12 @@ packages:
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.19.12':
resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-ppc64@0.21.5':
resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
engines: {node: '>=12'}
@@ -867,6 +951,12 @@ packages:
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.19.12':
resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==}
engines: {node: '>=12'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-riscv64@0.21.5':
resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
engines: {node: '>=12'}
@@ -879,6 +969,12 @@ packages:
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.19.12':
resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==}
engines: {node: '>=12'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-s390x@0.21.5':
resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
engines: {node: '>=12'}
@@ -891,6 +987,12 @@ packages:
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.19.12':
resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
'@esbuild/linux-x64@0.21.5':
resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
engines: {node: '>=12'}
@@ -909,6 +1011,12 @@ packages:
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.19.12':
resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==}
engines: {node: '>=12'}
cpu: [x64]
os: [netbsd]
'@esbuild/netbsd-x64@0.21.5':
resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
engines: {node: '>=12'}
@@ -927,6 +1035,12 @@ packages:
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.19.12':
resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==}
engines: {node: '>=12'}
cpu: [x64]
os: [openbsd]
'@esbuild/openbsd-x64@0.21.5':
resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
engines: {node: '>=12'}
@@ -939,6 +1053,12 @@ packages:
cpu: [x64]
os: [openbsd]
'@esbuild/sunos-x64@0.19.12':
resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
'@esbuild/sunos-x64@0.21.5':
resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
engines: {node: '>=12'}
@@ -951,6 +1071,12 @@ packages:
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.19.12':
resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-arm64@0.21.5':
resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
engines: {node: '>=12'}
@@ -963,6 +1089,12 @@ packages:
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.19.12':
resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==}
engines: {node: '>=12'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-ia32@0.21.5':
resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
engines: {node: '>=12'}
@@ -975,6 +1107,12 @@ packages:
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.19.12':
resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
'@esbuild/win32-x64@0.21.5':
resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
engines: {node: '>=12'}
@@ -1333,8 +1471,8 @@ packages:
'@lezer/yaml@1.0.3':
resolution: {integrity: sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==}
'@llama-flow/core@0.4.1':
resolution: {integrity: sha512-xHhJMRmY16C1pYPWIonmLWPkkjTGuj1iVQCTXOM6sXajQ3r0+mEVERQCjPqf48tvX0K+szbdgxjg6wx+KwVqcg==}
'@llama-flow/core@0.4.4':
resolution: {integrity: sha512-hwK1EQ+atUG/E7XcDV3KsTaA8op29pb8gbpVurpsqbLnGFkdTT4F/6V7Hy1cC2o/yOY+DKc/rxoIsH1uJS0cZg==}
peerDependencies:
'@modelcontextprotocol/sdk': ^1.7.0
hono: ^4.7.4
@@ -1425,10 +1563,10 @@ packages:
'@llamaindex/env': 0.1.29
zod: ^3.23.8
'@llamaindex/workflow@1.1.3':
resolution: {integrity: sha512-B9wlN4xMtfbnIQW84wTC0GdyIaLGXYnZ4I0LENV2UM/0wqlXKI7YoBTO3cJvi6pO1sPA+K29Tqw822VsGF+GkA==}
'@llamaindex/workflow@1.1.8':
resolution: {integrity: sha512-40uy5dWnqqlojq/fjlrIw8ZQOcFmZ2kSrvTyH65vQtypoTP8JWgBSTt3SFTWBUndwijNsfl9VvLh5S9INb0XBQ==}
peerDependencies:
'@llamaindex/core': 0.6.6
'@llamaindex/core': 0.6.9
'@llamaindex/env': 0.1.30
zod: ^3.23.8
@@ -3861,6 +3999,11 @@ packages:
resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==}
engines: {node: '>=0.12'}
esbuild@0.19.12:
resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==}
engines: {node: '>=12'}
hasBin: true
esbuild@0.21.5:
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
engines: {node: '>=12'}
@@ -6715,6 +6858,11 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
tsx@4.7.2:
resolution: {integrity: sha512-BCNd4kz6fz12fyrgCTEdZHGJ9fWTGeUzXmQysh0RVocDY3h4frk05ZNCXSy4kIenF7y/QnrdiVpTsyNRn6vlAw==}
engines: {node: '>=18.0.0'}
hasBin: true
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
@@ -7730,102 +7878,153 @@ snapshots:
tslib: 2.8.1
optional: true
'@esbuild/aix-ppc64@0.19.12':
optional: true
'@esbuild/aix-ppc64@0.21.5':
optional: true
'@esbuild/aix-ppc64@0.25.3':
optional: true
'@esbuild/android-arm64@0.19.12':
optional: true
'@esbuild/android-arm64@0.21.5':
optional: true
'@esbuild/android-arm64@0.25.3':
optional: true
'@esbuild/android-arm@0.19.12':
optional: true
'@esbuild/android-arm@0.21.5':
optional: true
'@esbuild/android-arm@0.25.3':
optional: true
'@esbuild/android-x64@0.19.12':
optional: true
'@esbuild/android-x64@0.21.5':
optional: true
'@esbuild/android-x64@0.25.3':
optional: true
'@esbuild/darwin-arm64@0.19.12':
optional: true
'@esbuild/darwin-arm64@0.21.5':
optional: true
'@esbuild/darwin-arm64@0.25.3':
optional: true
'@esbuild/darwin-x64@0.19.12':
optional: true
'@esbuild/darwin-x64@0.21.5':
optional: true
'@esbuild/darwin-x64@0.25.3':
optional: true
'@esbuild/freebsd-arm64@0.19.12':
optional: true
'@esbuild/freebsd-arm64@0.21.5':
optional: true
'@esbuild/freebsd-arm64@0.25.3':
optional: true
'@esbuild/freebsd-x64@0.19.12':
optional: true
'@esbuild/freebsd-x64@0.21.5':
optional: true
'@esbuild/freebsd-x64@0.25.3':
optional: true
'@esbuild/linux-arm64@0.19.12':
optional: true
'@esbuild/linux-arm64@0.21.5':
optional: true
'@esbuild/linux-arm64@0.25.3':
optional: true
'@esbuild/linux-arm@0.19.12':
optional: true
'@esbuild/linux-arm@0.21.5':
optional: true
'@esbuild/linux-arm@0.25.3':
optional: true
'@esbuild/linux-ia32@0.19.12':
optional: true
'@esbuild/linux-ia32@0.21.5':
optional: true
'@esbuild/linux-ia32@0.25.3':
optional: true
'@esbuild/linux-loong64@0.19.12':
optional: true
'@esbuild/linux-loong64@0.21.5':
optional: true
'@esbuild/linux-loong64@0.25.3':
optional: true
'@esbuild/linux-mips64el@0.19.12':
optional: true
'@esbuild/linux-mips64el@0.21.5':
optional: true
'@esbuild/linux-mips64el@0.25.3':
optional: true
'@esbuild/linux-ppc64@0.19.12':
optional: true
'@esbuild/linux-ppc64@0.21.5':
optional: true
'@esbuild/linux-ppc64@0.25.3':
optional: true
'@esbuild/linux-riscv64@0.19.12':
optional: true
'@esbuild/linux-riscv64@0.21.5':
optional: true
'@esbuild/linux-riscv64@0.25.3':
optional: true
'@esbuild/linux-s390x@0.19.12':
optional: true
'@esbuild/linux-s390x@0.21.5':
optional: true
'@esbuild/linux-s390x@0.25.3':
optional: true
'@esbuild/linux-x64@0.19.12':
optional: true
'@esbuild/linux-x64@0.21.5':
optional: true
@@ -7835,6 +8034,9 @@ snapshots:
'@esbuild/netbsd-arm64@0.25.3':
optional: true
'@esbuild/netbsd-x64@0.19.12':
optional: true
'@esbuild/netbsd-x64@0.21.5':
optional: true
@@ -7844,30 +8046,45 @@ snapshots:
'@esbuild/openbsd-arm64@0.25.3':
optional: true
'@esbuild/openbsd-x64@0.19.12':
optional: true
'@esbuild/openbsd-x64@0.21.5':
optional: true
'@esbuild/openbsd-x64@0.25.3':
optional: true
'@esbuild/sunos-x64@0.19.12':
optional: true
'@esbuild/sunos-x64@0.21.5':
optional: true
'@esbuild/sunos-x64@0.25.3':
optional: true
'@esbuild/win32-arm64@0.19.12':
optional: true
'@esbuild/win32-arm64@0.21.5':
optional: true
'@esbuild/win32-arm64@0.25.3':
optional: true
'@esbuild/win32-ia32@0.19.12':
optional: true
'@esbuild/win32-ia32@0.21.5':
optional: true
'@esbuild/win32-ia32@0.25.3':
optional: true
'@esbuild/win32-x64@0.19.12':
optional: true
'@esbuild/win32-x64@0.21.5':
optional: true
@@ -8307,14 +8524,14 @@ snapshots:
'@lezer/highlight': 1.2.1
'@lezer/lr': 1.4.2
'@llama-flow/core@0.4.1(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)':
'@llama-flow/core@0.4.4(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)':
optionalDependencies:
'@modelcontextprotocol/sdk': 1.12.1
next: 15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
p-retry: 6.2.1
zod: 3.24.3
'@llama-flow/core@0.4.1(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.25.13)':
'@llama-flow/core@0.4.4(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.25.13)':
optionalDependencies:
'@modelcontextprotocol/sdk': 1.12.1
next: 15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -8373,17 +8590,17 @@ snapshots:
- supports-color
- yjs
'@llamaindex/cloud@4.0.9(@llama-flow/core@0.4.1(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3))(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)':
'@llamaindex/cloud@4.0.9(@llama-flow/core@0.4.4(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3))(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)':
dependencies:
'@llama-flow/core': 0.4.1(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)
'@llama-flow/core': 0.4.4(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)
'@llamaindex/core': 0.6.6
'@llamaindex/env': 0.1.30
p-retry: 6.2.1
zod: 3.25.13
'@llamaindex/cloud@4.0.9(@llama-flow/core@0.4.1(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.25.13))(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)':
'@llamaindex/cloud@4.0.9(@llama-flow/core@0.4.4(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.25.13))(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)':
dependencies:
'@llama-flow/core': 0.4.1(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.25.13)
'@llama-flow/core': 0.4.4(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.25.13)
'@llamaindex/core': 0.6.6
'@llamaindex/env': 0.1.30
p-retry: 6.2.1
@@ -8483,9 +8700,9 @@ snapshots:
'@llamaindex/env': 0.1.30
zod: 3.25.13
'@llamaindex/workflow@1.1.3(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)':
'@llamaindex/workflow@1.1.8(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)':
dependencies:
'@llama-flow/core': 0.4.1(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)
'@llama-flow/core': 0.4.4(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)
'@llamaindex/core': 0.6.6
'@llamaindex/env': 0.1.30
zod: 3.24.3
@@ -11070,6 +11287,32 @@ snapshots:
d: 1.0.2
ext: 1.7.0
esbuild@0.19.12:
optionalDependencies:
'@esbuild/aix-ppc64': 0.19.12
'@esbuild/android-arm': 0.19.12
'@esbuild/android-arm64': 0.19.12
'@esbuild/android-x64': 0.19.12
'@esbuild/darwin-arm64': 0.19.12
'@esbuild/darwin-x64': 0.19.12
'@esbuild/freebsd-arm64': 0.19.12
'@esbuild/freebsd-x64': 0.19.12
'@esbuild/linux-arm': 0.19.12
'@esbuild/linux-arm64': 0.19.12
'@esbuild/linux-ia32': 0.19.12
'@esbuild/linux-loong64': 0.19.12
'@esbuild/linux-mips64el': 0.19.12
'@esbuild/linux-ppc64': 0.19.12
'@esbuild/linux-riscv64': 0.19.12
'@esbuild/linux-s390x': 0.19.12
'@esbuild/linux-x64': 0.19.12
'@esbuild/netbsd-x64': 0.19.12
'@esbuild/openbsd-x64': 0.19.12
'@esbuild/sunos-x64': 0.19.12
'@esbuild/win32-arm64': 0.19.12
'@esbuild/win32-ia32': 0.19.12
'@esbuild/win32-x64': 0.19.12
esbuild@0.21.5:
optionalDependencies:
'@esbuild/aix-ppc64': 0.21.5
@@ -12310,9 +12553,9 @@ snapshots:
rfdc: 1.4.1
wrap-ansi: 9.0.0
llamaindex@0.11.1(@llama-flow/core@0.4.1(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3))(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(zod@3.24.3):
llamaindex@0.11.1(@llama-flow/core@0.4.4(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3))(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(zod@3.24.3):
dependencies:
'@llamaindex/cloud': 4.0.9(@llama-flow/core@0.4.1(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3))(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)
'@llamaindex/cloud': 4.0.9(@llama-flow/core@0.4.4(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3))(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)
'@llamaindex/core': 0.6.6
'@llamaindex/env': 0.1.30
'@llamaindex/node-parser': 2.0.6(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)
@@ -12330,9 +12573,9 @@ snapshots:
- web-tree-sitter
- zod
llamaindex@0.11.1(@llama-flow/core@0.4.1(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.25.13))(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(zod@3.25.13):
llamaindex@0.11.1(@llama-flow/core@0.4.4(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.25.13))(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(zod@3.25.13):
dependencies:
'@llamaindex/cloud': 4.0.9(@llama-flow/core@0.4.1(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.25.13))(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)
'@llamaindex/cloud': 4.0.9(@llama-flow/core@0.4.4(@modelcontextprotocol/sdk@1.12.1)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.25.13))(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)
'@llamaindex/core': 0.6.6
'@llamaindex/env': 0.1.30
'@llamaindex/node-parser': 2.0.6(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)
@@ -14647,6 +14890,13 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
tsx@4.7.2:
dependencies:
esbuild: 0.19.12
get-tsconfig: 4.10.0
optionalDependencies:
fsevents: 2.3.3
tunnel-agent@0.6.0:
dependencies:
safe-buffer: 5.2.1