Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e4c07f9
feat: support human in the loop for TS
thucpn Jun 9, 2025
eba44a3
add example for custom workflow
thucpn Jun 10, 2025
7875178
fix: need to request humanResponseEvent to save missing step to snapshot
thucpn Jun 10, 2025
ac5a1ef
refactor: human response data should be any
thucpn Jun 10, 2025
832f5b6
refactor runWorkflow function to support resume stream
thucpn Jun 10, 2025
23fdd51
refactor: hitl
thucpn Jun 10, 2025
7b21682
fix: workflow
thucpn Jun 10, 2025
ddccbcf
add summary event
thucpn Jun 10, 2025
45af254
send tool event
thucpn Jun 10, 2025
be894df
use requestId from Vercel
thucpn Jun 11, 2025
99ff5b4
Merge branch 'main' into tp/hitl-for-ts
thucpn Jun 11, 2025
2d31294
update chat route.ts
thucpn Jun 11, 2025
baf16fc
fix copy utils/*
thucpn Jun 11, 2025
98913ed
refactor: workflow and stream
thucpn Jun 11, 2025
d93ee94
Create eight-moons-perform.md
thucpn Jun 11, 2025
38cd475
update typo
thucpn Jun 11, 2025
0e67d8a
make schema simple
thucpn Jun 11, 2025
6851960
fix typo
thucpn Jun 11, 2025
537489a
use messages in startAgentEvent
thucpn Jun 11, 2025
a440a34
save to snapshots folder
thucpn Jun 11, 2025
0896824
fix lint
thucpn Jun 11, 2025
7e4c68b
feat: workflowBaseEvent
thucpn Jun 11, 2025
6a5db05
include response event in input event
thucpn Jun 12, 2025
8f107f5
simplify type
thucpn Jun 12, 2025
2c062c9
update readme
thucpn Jun 12, 2025
af47dbb
update document
thucpn Jun 12, 2025
c5c72f5
fix typecheck
thucpn Jun 12, 2025
9b3c1ad
bump: "@llamaindex/workflow": "~1.1.8"
thucpn Jun 12, 2025
66b8db6
remove any
thucpn Jun 12, 2025
22cd865
use fixed tsx version to fix e2e
thucpn Jun 12, 2025
7ee59c5
fix wrong copy
thucpn Jun 12, 2025
5a16f10
add cli hitl examples as a use case for both Python and TS
thucpn Jun 12, 2025
159d15d
update changeset to release create-llama also
thucpn Jun 12, 2025
10fcf50
fix e2e
thucpn Jun 12, 2025
9175ad9
fix e2e
thucpn Jun 12, 2025
d4c822d
hitl frontend chat
thucpn Jun 12, 2025
5b06106
try disable hitl test
thucpn Jun 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/eight-moons-perform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@llamaindex/server": patch
"create-llama": patch
---

feat: support human in the loop for TS
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const useCases: TemplateUseCase[] = [
"financial_report",
"code_generator",
"document_generator",
"hitl",
];
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const templateUseCases = [
"financial_report",
"deep_research",
"code_generator",
// "hitl",
];
const ejectDir = "next";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -25,6 +26,7 @@ const useCases: TemplateUseCase[] = [
"financial_report",
"code_generator",
"document_generator",
"hitl",
];
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions packages/create-llama/helpers/constant.ts
Original file line number Diff line number Diff line change
@@ -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",
];
4 changes: 3 additions & 1 deletion packages/create-llama/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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"]);
Expand Down
3 changes: 2 additions & 1 deletion packages/create-llama/helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
| {
Expand Down
3 changes: 2 additions & 1 deletion packages/create-llama/helpers/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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"));
Expand Down
19 changes: 17 additions & 2 deletions packages/create-llama/questions/simple.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -11,7 +12,8 @@ type AppType =
| "financial_report"
| "deep_research"
| "code_generator"
| "document_generator";
| "document_generator"
| "hitl";

type SimpleAnswers = {
appType: AppType;
Expand Down Expand Up @@ -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,
Expand All @@ -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",
Expand Down Expand Up @@ -170,6 +179,12 @@ const convertAnswers = async (
tools: [],
modelConfig: MODEL_GPT41,
},
hitl: {
template: "llamaindexserver",
dataSources: [],
tools: [],
modelConfig: MODEL_GPT41,
},
};

const results = lookup[answers.appType];
Expand Down
Original file line number Diff line number Diff line change
@@ -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);

Comment thread
thucpn marked this conversation as resolved.
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;
Original file line number Diff line number Diff line change
@@ -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!
Loading
Loading