Compare commits

..

8 Commits

Author SHA1 Message Date
Thuc Pham c7b7990990 Create ninety-goats-draw.md 2025-04-29 16:15:59 +07:00
thucpn e9e3cb8a69 apply prettier-plugin-tailwindcss to auto sort tailwind classnames 2025-04-29 16:10:58 +07:00
thucpn 6668fa21de apply recommened typescript rules for create-llama and fix lints 2025-04-29 16:06:53 +07:00
thucpn eec050e7a2 move typescript packages to root 2025-04-29 15:45:04 +07:00
thucpn ba23b7ab46 use bunchee in root 2025-04-29 15:06:51 +07:00
thucpn 9b9558dbd7 fix: format 2025-04-29 14:51:38 +07:00
thucpn 10e39cb31c update prettier config 2025-04-29 14:50:47 +07:00
thucpn ad824abe95 chore: move lint & prettier configs to root 2025-04-29 14:48:07 +07:00
116 changed files with 4363 additions and 8275 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Add artifacts use case (python)
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
chore: move lint & prettier configs to root
+2 -22
View File
@@ -23,7 +23,6 @@ jobs:
os: [macos-latest, windows-latest, ubuntu-22.04]
frameworks: ["fastapi"]
datasources: ["--no-files", "--example-file", "--llamacloud"]
template-types: ["streaming", "llamaindexserver"]
defaults:
run:
shell: bash
@@ -71,7 +70,6 @@ jobs:
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
FRAMEWORK: ${{ matrix.frameworks }}
DATASOURCE: ${{ matrix.datasources }}
TEMPLATE_TYPE: ${{ matrix.template-types }}
PYTHONIOENCODING: utf-8
PYTHONLEGACYWINDOWSSTDIO: utf-8
working-directory: packages/create-llama
@@ -79,7 +77,7 @@ jobs:
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-python-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}-${{ matrix.template-types }}
name: playwright-report-python-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}
path: packages/create-llama/playwright-report/
overwrite: true
retention-days: 30
@@ -95,7 +93,6 @@ jobs:
os: [macos-latest, windows-latest, ubuntu-22.04]
frameworks: ["nextjs"]
datasources: ["--no-files", "--example-file", "--llamacloud"]
template-types: ["streaming", "llamaindexserver"]
defaults:
run:
shell: bash
@@ -136,21 +133,6 @@ jobs:
run: pnpm run pack-install
working-directory: packages/create-llama
- name: Build server
run: pnpm run build
working-directory: packages/server
- name: Pack @llamaindex/server package
run: |
pnpm pack --pack-destination "${{ runner.temp }}"
if [ "${{ runner.os }}" == "Windows" ]; then
file=$(find "${{ runner.temp }}" -name "llamaindex-server-*.tgz" | head -n 1)
mv "$file" "${{ runner.temp }}/llamaindex-server.tgz"
else
mv ${{ runner.temp }}/llamaindex-server-*.tgz ${{ runner.temp }}/llamaindex-server.tgz
fi
working-directory: packages/server
- name: Run Playwright tests for TypeScript
run: pnpm run e2e:typescript
env:
@@ -158,14 +140,12 @@ jobs:
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
FRAMEWORK: ${{ matrix.frameworks }}
DATASOURCE: ${{ matrix.datasources }}
TEMPLATE_TYPE: ${{ matrix.template-types }}
SERVER_PACKAGE_PATH: ${{ runner.temp }}/llamaindex-server.tgz
working-directory: packages/create-llama
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-typescript-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}-node${{ matrix.node-version }}-${{ matrix.template-types }}
name: playwright-report-typescript-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}-node${{ matrix.node-version }}
path: packages/create-llama/playwright-report/
overwrite: true
retention-days: 30
@@ -31,13 +31,6 @@ jobs:
- name: Run Prettier
run: pnpm run format
- name: Run build
run: pnpm run build
- name: Run Typecheck for examples
run: pnpm run typecheck
working-directory: packages/server/examples
- name: Run Python format check
uses: chartboost/ruff-action@v1
with:
+1 -2
View File
@@ -1,4 +1,3 @@
pnpm format
pnpm lint
uvx ruff check .
uvx ruff format . --check
uvx ruff format --check packages/create-llama/templates/
-8
View File
@@ -7,11 +7,3 @@ build/
.next/
out/
packages/server/server/
**/playwright-report/
**/test-results/
# Python
python/
**/*.mypy_cache/**
**/*.venv/**
**/*.ruff_cache/**
+19
View File
@@ -106,6 +106,25 @@ Ok to proceed? (y) y
You can also pass command line arguments to set up a new project
non-interactively. For a list of the latest options, call `create-llama --help`.
### Running in pro mode
If you prefer more advanced customization options, you can run `create-llama` in pro mode using the `--pro` flag.
In pro mode, instead of selecting a predefined use case, you'll be prompted to select each technical component of your project. This allows for greater flexibility in customizing your project, including:
- **Vector Store**: Choose from a variety of vector stores for keeping your documents, including MongoDB, Pinecone, Weaviate, Qdrant and Chroma.
- **Tools**: Choose from a variety of agent tools (functions called by the LLM), such as:
- Code Interpreter: Executes Python code in a secure Jupyter notebook environment
- Artifact Code Generator: Generates code artifacts that can be run in a sandbox
- OpenAPI Action: Facilitates requests to a provided OpenAPI schema
- Image Generator: Creates images based on text descriptions
- Web Search: Performs web searches to retrieve up-to-date information
- **Data Sources**: Integrate various data sources into your chat application, including local files, websites, or database-retrieved data.
- **Backend Options**: Besides using Next.js or FastAPI, you can also select to use Express for a more traditional Node.js application.
- **Observability**: Choose from a variety of LLM observability tools, including LlamaTrace and Traceloop.
Pro mode is ideal for developers who want fine-grained control over their project's configuration and are comfortable with more technical setup options.
## LlamaIndex Documentation
- [TS/JS docs](https://ts.llamaindex.ai/)
-3
View File
@@ -47,9 +47,6 @@ export default tseslint.config(
{
ignores: [
"python/**",
"**/*.mypy_cache/**",
"**/*.venv/**",
"**/*.ruff_cache/**",
"**/dist/**",
"**/e2e/cache/**",
"**/lib/*",
-37
View File
@@ -1,42 +1,5 @@
# create-llama
## 0.5.16
### Patch Changes
- 6f75d4a: fix: unsupported language in code gen workflow
- d0618fa: Fix LlamaCloud generate script issue
## 0.5.15
### Patch Changes
- 527075c: Enable dev mode that allows updating code directly in the UI
## 0.5.14
### Patch Changes
- 1df8cfb: Split artifacts use case to document generator and code generator
- 1b5a519: chore: improve dev experience with nodemon
- b3eb0ba: Fix typing check issue
- 556f33c: fix chromadb dependency issue
- 2451539: fix: remove dead generated ai code
- 7a70390: Deprecate pro mode
## 0.5.13
### Patch Changes
- f4ca602: Add artifact use case for Typescript template
- f4ca602: Update typescript use cases to use the new workflow engine
## 0.5.12
### Patch Changes
- 241d82a: Add artifacts use case (python)
## 0.5.11
### Patch Changes
@@ -3,7 +3,7 @@ import { exec } from "child_process";
import fs from "fs";
import path from "path";
import util from "util";
import { TemplateFramework, TemplateType, TemplateUseCase, TemplateVectorDB } from "../../helpers/types";
import { TemplateFramework, TemplateVectorDB } from "../../helpers/types";
import { RunCreateLlamaOptions, createTestDir, runCreateLlama } from "../utils";
const execAsync = util.promisify(exec);
@@ -11,193 +11,123 @@ const execAsync = util.promisify(exec);
const templateFramework: TemplateFramework = process.env.FRAMEWORK
? (process.env.FRAMEWORK as TemplateFramework)
: "fastapi";
const templateType: TemplateType = process.env.TEMPLATE_TYPE
? (process.env.TEMPLATE_TYPE as TemplateType)
: "llamaindexserver";
const useCases: TemplateUseCase[] = [
"agentic_rag",
"deep_research",
"financial_report",
"code_generator",
"document_generator",
];
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
: "--example-file";
test.describe("Mypy check", () => {
test.describe.configure({ retries: 0 });
// TODO: add support for other templates
// Test for streaming template
test.describe("StreamingTemplate", () => {
test.skip(templateType !== "streaming", `skipping streaming test for ${templateType}`);
if (
dataSource === "--example-file" // XXX: this test provides its own data source - only trigger it on one data source (usually the CI matrix will trigger multiple data sources)
) {
// vectorDBs, tools, and data source combinations to test
const vectorDbs: TemplateVectorDB[] = [
"mongo",
"pg",
"pinecone",
"milvus",
"astra",
"qdrant",
"chroma",
"weaviate",
];
const toolOptions = [
"wikipedia.WikipediaToolSpec",
"google.GoogleSearchToolSpec",
"document_generator",
"artifact",
];
if (
dataSource === "--example-file" // XXX: this test provides its own data source - only trigger it on one data source (usually the CI matrix will trigger multiple data sources)
) {
// vectorDBs, tools, and data source combinations to test
const vectorDbs: TemplateVectorDB[] = [
"mongo",
"pg",
"pinecone",
"milvus",
"astra",
"qdrant",
"chroma",
"weaviate",
];
const dataSources = [
"--example-file",
"--web-source https://www.example.com",
"--db-source mysql+pymysql://user:pass@localhost:3306/mydb",
];
const toolOptions = [
"wikipedia.WikipediaToolSpec",
"google.GoogleSearchToolSpec",
"document_generator",
"artifact",
];
const observabilityOptions = ["llamatrace", "traceloop"];
const dataSources = [
"--example-file",
"--web-source https://www.example.com",
"--db-source mysql+pymysql://user:pass@localhost:3306/mydb",
];
// Test vector databases
for (const vectorDb of vectorDbs) {
test(`vectorDB: ${vectorDb} ${templateType}`, async () => {
const cwd = await createTestDir();
const { pyprojectPath } = await createAndCheckLlamaProject({
options: {
cwd,
templateType: "streaming",
templateFramework,
dataSource: "--example-file",
vectorDb,
tools: "none",
port: 3000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
observability: undefined,
},
});
const observabilityOptions = ["llamatrace", "traceloop"];
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
if (vectorDb !== "none") {
if (vectorDb === "pg") {
expect(pyprojectContent).toContain(
"llama-index-vector-stores-postgres",
);
} else {
expect(pyprojectContent).toContain(
`llama-index-vector-stores-${vectorDb}`,
);
}
}
});
}
test.describe("Mypy check", () => {
test.describe.configure({ retries: 0 });
// // Test tools
for (const tool of toolOptions) {
test(`tool: ${tool} ${templateType}`, async () => {
const cwd = await createTestDir();
const { pyprojectPath } = await createAndCheckLlamaProject({
options: {
cwd,
templateType: "streaming",
templateFramework,
dataSource: "--example-file",
vectorDb: "none",
tools: tool,
port: 3000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
observability: undefined,
},
});
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
if (tool === "wikipedia.WikipediaToolSpec") {
expect(pyprojectContent).toContain("wikipedia");
}
if (tool === "google.GoogleSearchToolSpec") {
expect(pyprojectContent).toContain("google");
}
});
}
// // Test data sources
for (const dataSource of dataSources) {
test(`data source: ${dataSource} ${templateType}`, async () => {
const dataSourceType = dataSource.split(" ")[0];
const cwd = await createTestDir();
const { pyprojectPath } = await createAndCheckLlamaProject({
options: {
cwd,
templateType: "streaming",
templateFramework,
dataSource,
vectorDb: "none",
tools: "none",
port: 3000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
observability: undefined,
},
});
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
if (dataSource.includes("--web-source")) {
expect(pyprojectContent).toContain("llama-index-readers-web");
}
if (dataSource.includes("--db-source")) {
expect(pyprojectContent).toContain("llama-index-readers-database");
}
});
}
// Test observability options
for (const observability of observabilityOptions) {
test.describe(`observability: ${observability} ${templateType}`, async () => {
const cwd = await createTestDir();
const { pyprojectPath } = await createAndCheckLlamaProject({
options: {
cwd,
templateType: "streaming",
templateFramework,
dataSource: "--example-file",
vectorDb: "none",
tools: "none",
port: 3000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
observability,
},
});
});
}
}
});
test.describe("LlamaIndexServer", async () => {
test.skip(templateType !== "llamaindexserver", `skipping llamaindexserver test for ${templateType}`);
test.skip(dataSource !== "--example-file", `skipping llamaindexserver test for ${dataSource}`);
for (const useCase of useCases) {
// Test vector databases
for (const vectorDb of vectorDbs) {
test(`Mypy check for vectorDB: ${vectorDb}`, async () => {
const cwd = await createTestDir();
await createAndCheckLlamaProject({
const { pyprojectPath } = await createAndCheckLlamaProject({
options: {
cwd,
templateType: "llamaindexserver",
templateType: "streaming",
templateFramework,
dataSource: "--example-file",
vectorDb,
tools: "none",
port: 3000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
observability: undefined,
},
});
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
if (vectorDb !== "none") {
if (vectorDb === "pg") {
expect(pyprojectContent).toContain(
"llama-index-vector-stores-postgres",
);
} else {
expect(pyprojectContent).toContain(
`llama-index-vector-stores-${vectorDb}`,
);
}
}
});
}
// Test tools
for (const tool of toolOptions) {
test(`Mypy check for tool: ${tool}`, async () => {
const cwd = await createTestDir();
const { pyprojectPath } = await createAndCheckLlamaProject({
options: {
cwd,
templateType: "streaming",
templateFramework,
dataSource: "--example-file",
vectorDb: "none",
tools: tool,
port: 3000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
observability: undefined,
},
});
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
if (tool === "wikipedia.WikipediaToolSpec") {
expect(pyprojectContent).toContain("wikipedia");
}
if (tool === "google.GoogleSearchToolSpec") {
expect(pyprojectContent).toContain("google");
}
});
}
// Test data sources
for (const dataSource of dataSources) {
const dataSourceType = dataSource.split(" ")[0];
test(`Mypy check for data source: ${dataSourceType}`, async () => {
const cwd = await createTestDir();
const { pyprojectPath } = await createAndCheckLlamaProject({
options: {
cwd,
templateType: "streaming",
templateFramework,
dataSource,
vectorDb: "none",
@@ -209,77 +139,110 @@ test.describe("Mypy check", () => {
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
observability: undefined,
useCase,
},
});
}
});
async function createAndCheckLlamaProject({
options,
}: {
options: RunCreateLlamaOptions;
}): Promise<{ pyprojectPath: string; projectPath: string }> {
const result = await runCreateLlama(options);
const name = result.projectName;
const projectPath = path.join(options.cwd, name);
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
if (dataSource.includes("--web-source")) {
expect(pyprojectContent).toContain("llama-index-readers-web");
}
if (dataSource.includes("--db-source")) {
expect(pyprojectContent).toContain("llama-index-readers-database");
}
});
}
// Check if the app folder exists
expect(fs.existsSync(projectPath)).toBeTruthy();
// Test observability options
for (const observability of observabilityOptions) {
test(`Mypy check for observability: ${observability}`, async () => {
const cwd = await createTestDir();
// Check if pyproject.toml exists
const pyprojectPath = path.join(projectPath, "pyproject.toml");
expect(fs.existsSync(pyprojectPath)).toBeTruthy();
const { pyprojectPath } = await createAndCheckLlamaProject({
options: {
cwd,
templateType: "streaming",
templateFramework,
dataSource: "--example-file",
vectorDb: "none",
tools: "none",
port: 3000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
observability,
},
});
});
}
});
}
// Modify environment for the command
const commandEnv = {
...process.env,
};
async function createAndCheckLlamaProject({
options,
}: {
options: RunCreateLlamaOptions;
}): Promise<{ pyprojectPath: string; projectPath: string }> {
const result = await runCreateLlama(options);
const name = result.projectName;
const projectPath = path.join(options.cwd, name);
console.log("Running uv venv...");
try {
const { stdout: venvStdout, stderr: venvStderr } = await execAsync(
"uv venv",
{ cwd: projectPath, env: commandEnv },
);
console.log("uv venv stdout:", venvStdout);
console.error("uv venv stderr:", venvStderr);
} catch (error) {
console.error("Error running uv venv:", error);
throw error; // Re-throw error to fail the test
}
// Check if the app folder exists
expect(fs.existsSync(projectPath)).toBeTruthy();
console.log("Running uv sync...");
try {
const { stdout: syncStdout, stderr: syncStderr } = await execAsync(
"uv sync --all-extras",
{ cwd: projectPath, env: commandEnv },
);
console.log("uv sync stdout:", syncStdout);
console.error("uv sync stderr:", syncStderr);
} catch (error) {
console.error("Error running uv sync:", error);
throw error; // Re-throw error to fail the test
}
// Check if pyproject.toml exists
const pyprojectPath = path.join(projectPath, "pyproject.toml");
expect(fs.existsSync(pyprojectPath)).toBeTruthy();
console.log("Running uv run mypy ....");
try {
const { stdout: mypyStdout, stderr: mypyStderr } = await execAsync(
"uv run mypy .",
{ cwd: projectPath, env: commandEnv },
);
console.log("uv run mypy stdout:", mypyStdout);
console.error("uv run mypy stderr:", mypyStderr);
// Assuming mypy success means no output or specific success message
// Adjust checks based on actual expected mypy output
} catch (error) {
console.error("Error running mypy:", error);
throw error;
}
// Modify environment for the command
const commandEnv = {
...process.env,
};
// If we reach this point without throwing an error, the test passes
expect(true).toBeTruthy();
return { pyprojectPath, projectPath };
console.log("Running uv venv...");
try {
const { stdout: venvStdout, stderr: venvStderr } = await execAsync(
"uv venv",
{ cwd: projectPath, env: commandEnv },
);
console.log("uv venv stdout:", venvStdout);
console.error("uv venv stderr:", venvStderr);
} catch (error) {
console.error("Error running uv venv:", error);
throw error; // Re-throw error to fail the test
}
});
console.log("Running uv sync...");
try {
const { stdout: syncStdout, stderr: syncStderr } = await execAsync(
"uv sync --all-extras",
{ cwd: projectPath, env: commandEnv },
);
console.log("uv sync stdout:", syncStdout);
console.error("uv sync stderr:", syncStderr);
} catch (error) {
console.error("Error running uv sync:", error);
throw error; // Re-throw error to fail the test
}
console.log("Running uv run mypy ....");
try {
const { stdout: mypyStdout, stderr: mypyStderr } = await execAsync(
"uv run mypy .",
{ cwd: projectPath, env: commandEnv },
);
console.log("uv run mypy stdout:", mypyStdout);
console.error("uv run mypy stderr:", mypyStderr);
// Assuming mypy success means no output or specific success message
// Adjust checks based on actual expected mypy output
} catch (error) {
console.error("Error running mypy:", error);
throw error;
}
// If we reach this point without throwing an error, the test passes
expect(true).toBeTruthy();
return { pyprojectPath, projectPath };
}
@@ -12,30 +12,21 @@ import { createTestDir, runCreateLlama, type AppType } from "../utils";
const templateFramework: TemplateFramework = process.env.FRAMEWORK
? (process.env.FRAMEWORK as TemplateFramework)
: "fastapi";
const dataSource: string = process.env.DATASOURCE
? (process.env.DATASOURCE as string)
: "--example-file";
const llamaCloudProjectName = "create-llama";
const llamaCloudIndexName = "e2e-test";
const dataSource: string = "--example-file";
const templateUI: TemplateUI = "shadcn";
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
const appType: AppType = "--frontend";
const userMessage = "Write a blog post about physical standards for letters";
const templateUseCases = [
"agentic_rag",
"financial_report",
"deep_research",
"code_generator",
];
const templateUseCases = ["financial_report", "agentic_rag", "deep_research"];
for (const useCase of templateUseCases) {
test.describe(`Test use case ${useCase} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
test.skip(
dataSource === "--no-files" || templateFramework === "express",
process.platform !== "linux" ||
process.env.DATASOURCE === "--no-files" ||
templateFramework === "express",
"The llamaindexserver template currently only works with nextjs, fastapi. We also only run on Linux to speed up tests.",
);
const useLlamaParse = dataSource === "--llamacloud";
let port: number;
let cwd: string;
let name: string;
@@ -57,9 +48,6 @@ for (const useCase of templateUseCases) {
templateUI,
appType,
useCase,
llamaCloudProjectName,
llamaCloudIndexName,
useLlamaParse,
});
name = result.projectName;
appProcess = result.appProcess;
@@ -3,12 +3,7 @@ import { exec } from "child_process";
import fs from "fs";
import path from "path";
import util from "util";
import {
TemplateFramework,
TemplateType,
TemplateUseCase,
TemplateVectorDB,
} from "../../helpers/types";
import { TemplateFramework, TemplateVectorDB } from "../../helpers/types";
import { createTestDir, runCreateLlama } from "../utils";
const execAsync = util.promisify(exec);
@@ -16,16 +11,6 @@ const execAsync = util.promisify(exec);
const templateFramework: TemplateFramework = process.env.FRAMEWORK
? (process.env.FRAMEWORK as TemplateFramework)
: "nextjs";
const templateType: TemplateType = process.env.TEMPLATE_TYPE
? (process.env.TEMPLATE_TYPE as TemplateType)
: "llamaindexserver";
const useCases: TemplateUseCase[] = [
"agentic_rag",
"deep_research",
"financial_report",
"code_generator",
"document_generator",
];
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
: "--example-file";
@@ -44,118 +29,77 @@ const vectorDbs: TemplateVectorDB[] = [
];
test.describe("Test resolve TS dependencies", () => {
test.describe.configure({ retries: 0 });
// Test vector DBs without LlamaParse
for (const vectorDb of vectorDbs) {
const optionDescription = `templateType: ${templateType}, vectorDb: ${vectorDb}, dataSource: ${dataSource}`;
const optionDescription = `vectorDb: ${vectorDb}, dataSource: ${dataSource}`;
test(`Vector DB test - ${optionDescription}`, async () => {
// skip vectordb test for llamaindexserver
test.skip(
templateType === "llamaindexserver",
"skipping vectorDB test for llamaindexserver",
);
await runTest({
templateType: templateType,
useLlamaParse: false, // Disable LlamaParse for vectorDB test
vectorDb: vectorDb,
});
await runTest(vectorDb, false);
});
}
// No vectorDB, with LlamaParse and useCase
// Only need to test use case with example data source
if (dataSource === "--example-file") {
for (const useCase of useCases) {
const optionDescription = `templateType: ${templateType}, useCase: ${useCase}`;
test.describe(`useCase test - ${optionDescription}`, () => {
test.skip(
templateType === "streaming",
"Skipping use case test for streaming template.",
);
test(`no llamaParse - ${optionDescription}`, async () => {
await runTest({
templateType: templateType,
useLlamaParse: false,
useCase: useCase,
});
});
// Skipping llamacloud for the use case doesn't use index.
if (useCase !== "code_generator" && useCase !== "document_generator") {
test(`llamaParse - ${optionDescription}`, async () => {
await runTest({
templateType: templateType,
useLlamaParse: true,
useCase: useCase,
});
});
}
});
// Test LlamaParse with vectorDB 'none'
test(`LlamaParse test - vectorDb: none, dataSource: ${dataSource}, llamaParse: true`, async () => {
await runTest("none", true);
});
async function runTest(
vectorDb: TemplateVectorDB | "none",
useLlamaParse: boolean,
) {
const cwd = await createTestDir();
const result = await runCreateLlama({
cwd: cwd,
templateType: "streaming",
templateFramework: templateFramework,
dataSource: dataSource,
vectorDb: vectorDb,
port: 3000,
postInstallAction: "none",
templateUI: undefined,
appType: templateFramework === "nextjs" ? "" : "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
tools: undefined,
useLlamaParse: useLlamaParse,
});
const name = result.projectName;
// Check if the app folder exists
const appDir = path.join(cwd, name);
const dirExists = fs.existsSync(appDir);
expect(dirExists).toBeTruthy();
// Install dependencies using pnpm
try {
const { stderr: installStderr } = await execAsync(
"pnpm install --prefer-offline --ignore-workspace",
{
cwd: appDir,
},
);
} catch (error) {
console.error("Error installing dependencies:", error);
throw error;
}
// Run tsc type check and capture the output
try {
const { stdout, stderr } = await execAsync(
"pnpm exec tsc -b --diagnostics",
{
cwd: appDir,
},
);
// Check if there's any error output
expect(stderr).toBeFalsy();
// Log the stdout for debugging purposes
console.log("TypeScript type-check output:", stdout);
} catch (error) {
console.error("Error running tsc:", error);
throw error;
}
}
});
async function runTest(options: {
templateType: TemplateType;
useLlamaParse: boolean;
useCase?: TemplateUseCase;
vectorDb?: TemplateVectorDB;
}) {
const cwd = await createTestDir();
const result = await runCreateLlama({
cwd: cwd,
templateType: options.templateType,
templateFramework: templateFramework,
dataSource: dataSource,
vectorDb: options.vectorDb ?? "none",
port: 3000,
postInstallAction: "none",
templateUI: undefined,
appType: templateFramework === "nextjs" ? "" : "--no-frontend",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
tools: undefined,
useLlamaParse: options.useLlamaParse,
useCase: options.useCase,
});
const name = result.projectName;
// Check if the app folder exists
const appDir = path.join(cwd, name);
const dirExists = fs.existsSync(appDir);
expect(dirExists).toBeTruthy();
// Install dependencies using pnpm
try {
const { stderr: installStderr } = await execAsync(
"pnpm install --prefer-offline --ignore-workspace",
{
cwd: appDir,
},
);
} catch (error) {
console.error("Error installing dependencies:", error);
throw error;
}
// Run tsc type check and capture the output
try {
const { stdout, stderr } = await execAsync(
"pnpm exec tsc -b --diagnostics",
{
cwd: appDir,
},
);
// Check if there's any error output
expect(stderr).toBeFalsy();
// Log the stdout for debugging purposes
console.log("TypeScript type-check output:", stdout);
} catch (error) {
console.error("Error running tsc:", error);
throw error;
}
}
+1 -9
View File
@@ -18,7 +18,6 @@ import {
ModelConfig,
TemplateDataSource,
TemplateFramework,
TemplateUseCase,
TemplateVectorDB,
} from "./types";
import { installTSTemplate } from "./typescript";
@@ -61,7 +60,6 @@ async function generateContextData(
vectorDb?: TemplateVectorDB,
llamaCloudKey?: string,
useLlamaParse?: boolean,
useCase?: TemplateUseCase,
) {
if (packageManager) {
const runGenerate = `${cyan(
@@ -98,12 +96,7 @@ 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.
if (shouldRunGenerate) {
await callPackageManager(packageManager, true, ["run", "generate"]);
}
await callPackageManager(packageManager, true, ["run", "generate"]);
return;
}
}
@@ -231,7 +224,6 @@ export const installTemplate = async (
props.vectorDb,
props.llamaCloudKey,
props.useLlamaParse,
props.useCase,
);
}
+4 -9
View File
@@ -94,10 +94,6 @@ const getAdditionalDependencies = (
name: "llama-index-vector-stores-chroma",
version: ">=0.4.0,<0.5.0",
});
dependencies.push({
name: "onnxruntime",
version: "<1.22.0",
});
break;
}
case "weaviate": {
@@ -267,7 +263,7 @@ const getAdditionalDependencies = (
if (observability === "traceloop") {
dependencies.push({
name: "traceloop-sdk",
version: ">=0.15.11",
version: ">=0.15.11,<0.16.0",
});
}
if (observability === "llamatrace") {
@@ -569,13 +565,13 @@ const installLlamaIndexServerTemplate = async ({
await copy("*.py", path.join(root, "app"), {
parents: true,
cwd: path.join(templatesDir, "components", "use-cases", "python", useCase),
cwd: path.join(templatesDir, "components", "workflows", "python", useCase),
});
// Copy custom UI component code
await copy(`*`, path.join(root, "components"), {
parents: true,
cwd: path.join(templatesDir, "components", "ui", "use-cases", useCase),
cwd: path.join(templatesDir, "components", "ui", "workflows", useCase),
});
if (useLlamaParse) {
@@ -606,7 +602,7 @@ const installLlamaIndexServerTemplate = async ({
// Copy README.md
await copy("README-template.md", path.join(root), {
parents: true,
cwd: path.join(templatesDir, "components", "use-cases", "python", useCase),
cwd: path.join(templatesDir, "components", "workflows", "python", useCase),
rename: assetRelocator,
});
};
@@ -677,7 +673,6 @@ export const installPythonTemplate = async ({
dataSources,
tools,
template,
observability,
);
await addDependencies(root, addOnDependencies);
+1 -2
View File
@@ -58,8 +58,7 @@ export type TemplateUseCase =
| "extractor"
| "contract_review"
| "agentic_rag"
| "code_generator"
| "document_generator";
| "artifacts";
// Config for both file and folder
export type FileSourceConfig =
| {
+18 -25
View File
@@ -31,24 +31,23 @@ const installLlamaIndexServerTemplate = async ({
process.exit(1);
}
await copy("**", path.join(root), {
await copy("workflow.ts", path.join(root, "src", "app"), {
parents: true,
cwd: path.join(
templatesDir,
"components",
"use-cases",
"workflows",
"typescript",
useCase,
),
rename: assetRelocator,
});
// copy workflow UI components to output/components folder
await copy("*", path.join(root, "components"), {
parents: true,
cwd: path.join(templatesDir, "components", "ui", "use-cases", useCase),
cwd: path.join(templatesDir, "components", "ui", "workflows", useCase),
});
// Override generate.ts if workflow use case doesn't use custom UI
if (vectorDb === "llamacloud") {
await copy("generate.ts", path.join(root, "src"), {
parents: true,
@@ -75,14 +74,18 @@ const installLlamaIndexServerTemplate = async ({
rename: () => "data.ts",
});
}
// Simplify use case code
if (useCase === "code_generator" || useCase === "document_generator") {
// 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"));
// TODO: Remove generate index in generate.ts and package.json if possible
}
// Copy README.md
await copy("README-template.md", path.join(root), {
parents: true,
cwd: path.join(
templatesDir,
"components",
"workflows",
"typescript",
useCase,
),
rename: assetRelocator,
});
};
const installLegacyTSTemplate = async ({
@@ -387,7 +390,7 @@ const providerDependencies: {
[key in ModelProvider]?: Record<string, string>;
} = {
openai: {
"@llamaindex/openai": "~0.4.0",
"@llamaindex/openai": "^0.2.0",
},
gemini: {
"@llamaindex/google": "^0.2.0",
@@ -513,7 +516,7 @@ async function updatePackageJson({
if (backend) {
packageJson.dependencies = {
...packageJson.dependencies,
"@llamaindex/readers": "~3.1.4",
"@llamaindex/readers": "^2.0.0",
};
if (vectorDb && vectorDb in vectorDbDependencies) {
@@ -543,16 +546,6 @@ async function updatePackageJson({
};
}
// if having custom server package tgz file, use it for testing @llamaindex/server
const serverPackagePath = process.env.SERVER_PACKAGE_PATH;
if (serverPackagePath && template === "llamaindexserver") {
const relativePath = path.relative(process.cwd(), serverPackagePath);
packageJson.dependencies = {
...packageJson.dependencies,
"@llamaindex/server": `file:${relativePath}`,
};
}
await fs.writeFile(
packageJsonFile,
JSON.stringify(packageJson, null, 2) + os.EOL,
+1 -1
View File
@@ -196,7 +196,7 @@ const program = new Command(packageJson.name)
"--pro",
`
Deprecated: Allow interactive selection of all features.
Allow interactive selection of all features.
`,
false,
)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-llama",
"version": "0.5.16",
"version": "0.5.11",
"description": "Create LlamaIndex-powered apps with one command",
"keywords": [
"rag",
+1 -1
View File
@@ -6,7 +6,7 @@ const defaults: Omit<QuestionArgs, "modelConfig"> = {
framework: "nextjs",
ui: "shadcn",
frontend: false,
llamaCloudKey: undefined,
llamaCloudKey: "",
useLlamaParse: false,
communityProjectConfig: undefined,
llamapack: "",
-7
View File
@@ -1,5 +1,4 @@
import ciInfo from "ci-info";
import { bold, yellow } from "picocolors";
import { getCIQuestionResults } from "./ci";
import { askProQuestions } from "./questions";
import { askSimpleQuestions } from "./simple";
@@ -14,12 +13,6 @@ export const askQuestions = async (
return await getCIQuestionResults(args);
} else if (args.pro) {
// TODO: refactor pro questions to return a result object
console.log(
yellow(
`Pro mode is deprecated. Please use the new templates using the ${bold("LlamaIndexServer")} by not specifying pro mode.`,
),
);
await askProQuestions(args);
return args as unknown as QuestionResults;
}
+30 -41
View File
@@ -10,8 +10,7 @@ type AppType =
| "agentic_rag"
| "financial_report"
| "deep_research"
| "code_generator"
| "document_generator";
| "artifacts";
type SimpleAnswers = {
appType: AppType;
@@ -48,14 +47,10 @@ export const askSimpleQuestions = async (
"Researches and analyzes provided documents from multiple perspectives, generating a comprehensive report with citations to support key findings and insights.",
},
{
title: "Code Generator",
value: "code_generator",
description: "Build a Vercel v0 styled code generator.",
},
{
title: "Document Generator",
value: "document_generator",
description: "Build a OpenAI canvas-styled document generator.",
title: "Artifacts",
value: "artifacts",
description:
"Build your own Vercel's v0 or OpenAI's canvas-styled UI.",
},
],
},
@@ -67,36 +62,36 @@ export const askSimpleQuestions = async (
let useLlamaCloud = false;
const { language: newLanguage } = await prompts(
{
type: "select",
name: "language",
message: "What language do you want to use?",
choices: [
{ title: "Python (FastAPI)", value: "fastapi" },
{ title: "Typescript (NextJS)", value: "nextjs" },
],
},
questionHandlers,
);
language = newLanguage;
if (appType !== "code_generator" && appType !== "document_generator") {
const { useLlamaCloud: newUseLlamaCloud } = await prompts(
if (appType !== "artifacts") {
const { language: newLanguage } = await prompts(
{
type: "toggle",
name: "useLlamaCloud",
message: "Do you want to use LlamaCloud services?",
initial: false,
active: "Yes",
inactive: "No",
hint: "see https://www.llamaindex.ai/enterprise for more info",
type: "select",
name: "language",
message: "What language do you want to use?",
choices: [
{ title: "Python (FastAPI)", value: "fastapi" },
{ title: "Typescript (NextJS)", value: "nextjs" },
],
},
questionHandlers,
);
useLlamaCloud = newUseLlamaCloud;
language = newLanguage;
}
const { useLlamaCloud: newUseLlamaCloud } = await prompts(
{
type: "toggle",
name: "useLlamaCloud",
message: "Do you want to use LlamaCloud services?",
initial: false,
active: "Yes",
inactive: "No",
hint: "see https://www.llamaindex.ai/enterprise for more info",
},
questionHandlers,
);
useLlamaCloud = newUseLlamaCloud;
if (useLlamaCloud && !llamaCloudKey) {
// Ask for LlamaCloud API key, if not set
const { llamaCloudKey: newLlamaCloudKey } = await prompts(
@@ -158,13 +153,7 @@ const convertAnswers = async (
tools: [],
modelConfig: MODEL_GPT41,
},
code_generator: {
template: "llamaindexserver",
dataSources: [],
tools: [],
modelConfig: MODEL_GPT41,
},
document_generator: {
artifacts: {
template: "llamaindexserver",
dataSources: [],
tools: [],
@@ -1,9 +1,5 @@
import {
Document,
LLamaCloudFileService,
LlamaCloudIndex,
VectorStoreIndex,
} from "llamaindex";
import { Document, LLamaCloudFileService, VectorStoreIndex } from "llamaindex";
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
import { DocumentFile } from "../streaming/annotations";
import { parseFile, storeFile } from "./helper";
import { runPipeline } from "./pipeline";
@@ -10,9 +10,8 @@ dependencies = [
"python-dotenv>=1.0.0",
"pydantic<2.10",
"llama-index>=0.12.1",
"llama-parse>=0.6.21,<0.7.0",
"cachetools>=5.3.3",
"reflex==0.7.10",
"reflex>=0.6.2.post1",
]
[project.scripts]
@@ -11,9 +11,8 @@ dependencies = [
"python-dotenv>=1.0.0",
"pydantic<2.10",
"llama-index>=0.12.1",
"llama-parse>=0.6.21,<0.7.0",
"cachetools>=5.3.3",
"reflex==0.7.10",
"reflex>=0.6.2.post1",
]
[project.scripts]
@@ -1,132 +0,0 @@
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { Markdown } from "@llamaindex/chat-ui/widgets";
import { ListChecks, Loader2, Wand2 } from "lucide-react";
import { useEffect, useState } from "react";
const STAGE_META = {
plan: {
icon: ListChecks,
badgeText: "Step 1/2: Planning",
gradient: "from-blue-100 via-blue-50 to-white",
progress: 33,
iconBg: "bg-blue-100 text-blue-600",
badge: "bg-blue-100 text-blue-700",
},
generate: {
icon: Wand2,
badgeText: "Step 2/2: Generating",
gradient: "from-violet-100 via-violet-50 to-white",
progress: 66,
iconBg: "bg-violet-100 text-violet-600",
badge: "bg-violet-100 text-violet-700",
},
};
function ArtifactWorkflowCard({ event }) {
const [visible, setVisible] = useState(event?.state !== "completed");
const [fade, setFade] = useState(false);
useEffect(() => {
if (event?.state === "completed") {
setVisible(false);
} else {
setVisible(true);
setFade(false);
}
}, [event?.state]);
if (!event || !visible) return null;
const { state, requirement } = event;
const meta = STAGE_META[state];
if (!meta) return null;
return (
<div className="flex min-h-[180px] w-full items-center justify-center py-2">
<Card
className={cn(
"w-full rounded-xl shadow-md transition-all duration-500",
"border-0",
fade && "pointer-events-none opacity-0",
`bg-gradient-to-br ${meta.gradient}`,
)}
style={{
boxShadow:
"0 2px 12px 0 rgba(80, 80, 120, 0.08), 0 1px 3px 0 rgba(80, 80, 120, 0.04)",
}}
>
<CardHeader className="flex flex-row items-center gap-2 px-3 pb-1 pt-2">
<div
className={cn(
"flex items-center justify-center rounded-full p-1",
meta.iconBg,
)}
>
<meta.icon className="h-5 w-5" />
</div>
<CardTitle className="flex items-center gap-2 text-base font-semibold">
<Badge className={cn("ml-1", meta.badge, "px-2 py-0.5 text-xs")}>
{meta.badgeText}
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="px-3 py-1">
{state === "plan" && (
<div className="flex flex-col items-center gap-2 py-2">
<Loader2 className="mb-1 h-6 w-6 animate-spin text-blue-400" />
<div className="text-center text-sm font-medium text-blue-900">
Analyzing your request...
</div>
<Skeleton className="mt-1 h-3 w-1/2 rounded-full" />
</div>
)}
{state === "generate" && (
<div className="flex flex-col gap-2 py-2">
<div className="flex items-center gap-1">
<Loader2 className="h-4 w-4 animate-spin text-violet-400" />
<span className="text-sm font-medium text-violet-900">
Working on the requirement:
</span>
</div>
<div className="max-h-24 overflow-auto rounded-lg border border-violet-200 bg-violet-50 px-2 py-1 text-xs">
{requirement ? (
<Markdown content={requirement} />
) : (
<span className="italic text-violet-400">
No requirements available yet.
</span>
)}
</div>
</div>
)}
</CardContent>
<div className="px-3 pb-2 pt-1">
<Progress
value={meta.progress}
className={cn(
"h-1 rounded-full bg-gray-200",
state === "plan" && "bg-blue-200",
state === "generate" && "bg-violet-200",
)}
/>
</div>
</Card>
</div>
);
}
export default function Component({ events }) {
const aggregateEvents = () => {
if (!events || events.length === 0) return null;
return events[events.length - 1];
};
const event = aggregateEvents();
return <ArtifactWorkflowCard event={event} />;
}
@@ -113,6 +113,11 @@ function ArtifactWorkflowCard({ event }) {
state === "plan" && "bg-blue-200",
state === "generate" && "bg-violet-200",
)}
indicatorClassName={cn(
"transition-all duration-500",
state === "plan" && "bg-blue-500",
state === "generate" && "bg-violet-500",
)}
/>
</div>
</Card>
@@ -1,65 +0,0 @@
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
AI-powered code generator that can help you generate app with a chat interface, code editor and app preview.
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": "Create a report comparing the finances of Apple and Tesla" }] }'
```
## Customize the UI
To customize the UI, you can start by modifying the [./components/ui_event.jsx](./components/ui_event.jsx) file.
You can also generate a new code for the workflow using LLM by running the following command:
```
uv run generate_ui
```
## 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!
@@ -1,39 +0,0 @@
import { SimpleDirectoryReader } from "@llamaindex/readers/directory";
import "dotenv/config";
import { storageContextFromDefaults, VectorStoreIndex } from "llamaindex";
import { initSettings } from "./app/settings";
async function generateDatasource() {
console.log(`Generating storage context...`);
// Split documents, create embeddings and store them in the storage context
const storageContext = await storageContextFromDefaults({
persistDir: "storage",
});
// load documents from current directory into an index
const reader = new SimpleDirectoryReader();
const documents = await reader.loadData("data");
await VectorStoreIndex.fromDocuments(documents, {
storageContext,
});
console.log("Storage context successfully generated.");
}
(async () => {
const args = process.argv.slice(2);
const command = args[0];
initSettings();
if (command === "ui") {
console.error("This project doesn't use any custom UI.");
return;
} else {
if (command !== "datasource") {
console.error(
`Unrecognized command: ${command}. Generating datasource by default.`,
);
}
await generateDatasource();
}
})();
@@ -1,53 +0,0 @@
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
```
Third, 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).
## Custom UI Components
We have a custom component located in `components/ui_event.jsx`. This is used to display the state of artifact workflows in UI. You can regenerate a new UI component from the workflow event schema by running the following command:
```
npm run generate:ui
```
## Use Case
AI-powered code generator that can help you generate app with a chat interface, code editor and app preview.
To update the workflow, you can modify the code in [`workflow.ts`](app/workflow.ts).
You can start by sending an request on the [chat UI](http://localhost:3000) or you can test the `/api/chat` endpoint with the following curl request:
```shell
curl --location 'localhost:3000/api/chat' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "Compare the financial performance of Apple and Tesla" }] }'
```
## 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!
@@ -1,350 +0,0 @@
import { extractLastArtifact } from "@llamaindex/server";
import { ChatMemoryBuffer, MessageContent, Settings } from "llamaindex";
import {
agentStreamEvent,
createStatefulMiddleware,
createWorkflow,
startAgentEvent,
stopAgentEvent,
workflowEvent,
} from "@llamaindex/workflow";
import { z } from "zod";
export const RequirementSchema = z.object({
next_step: z.enum(["answering", "coding"]),
language: z.string().nullable().optional(),
file_name: z.string().nullable().optional(),
requirement: z.string(),
});
export type Requirement = z.infer<typeof RequirementSchema>;
export const UIEventSchema = z.object({
type: z.literal("ui_event"),
data: z.object({
state: z
.enum(["plan", "generate", "completed"])
.describe(
"The current state of the workflow: 'plan', 'generate', or 'completed'.",
),
requirement: z
.string()
.optional()
.describe(
"An optional requirement creating or updating a code, if applicable.",
),
}),
});
export type UIEvent = z.infer<typeof UIEventSchema>;
const planEvent = workflowEvent<{
userInput: MessageContent;
context?: string | undefined;
}>();
const generateArtifactEvent = workflowEvent<{
requirement: Requirement;
}>();
const synthesizeAnswerEvent = workflowEvent<object>();
const uiEvent = workflowEvent<UIEvent>();
const artifactEvent = workflowEvent<{
type: "artifact";
data: {
type: "code";
created_at: number;
data: {
language: string;
file_name: string;
code: string;
};
};
}>();
export function workflowFactory(reqBody: any) {
const llm = Settings.llm;
const { withState, getContext } = createStatefulMiddleware(() => {
return {
memory: new ChatMemoryBuffer({ llm }),
lastArtifact: extractLastArtifact(reqBody),
};
});
const workflow = withState(createWorkflow());
workflow.handle([startAgentEvent], async ({ data }) => {
const { userInput, chatHistory = [] } = data;
// Prepare chat history
const { state } = getContext();
// Put user input to the memory
if (!userInput) {
throw new Error("Missing user input to start the workflow");
}
state.memory.set(chatHistory);
state.memory.put({ role: "user", content: userInput });
return planEvent.with({
userInput: userInput,
context: state.lastArtifact
? JSON.stringify(state.lastArtifact)
: undefined,
});
});
workflow.handle([planEvent], async ({ data: planData }) => {
const { sendEvent } = getContext();
const { state } = getContext();
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
state: "plan",
},
}),
);
const user_msg = planData.userInput;
const context = planData.context
? `## The context is: \n${planData.context}\n`
: "";
const prompt = `
You are a product analyst responsible for analyzing the user's request and providing the next step for code or document generation.
You are helping user with their code artifact. To update the code, you need to plan a coding step.
Follow these instructions:
1. Carefully analyze the conversation history and the user's request to determine what has been done and what the next step should be.
2. The next step must be one of the following two options:
- "coding": To make the changes to the current code.
- "answering": If you don't need to update the current code or need clarification from the user.
Important: Avoid telling the user to update the code themselves, you are the one who will update the code (by planning a coding step).
3. If the next step is "coding", you may specify the language ("typescript" or "python") and file_name if known, otherwise set them to null.
4. The requirement must be provided clearly what is the user request and what need to be done for the next step in details
as precise and specific as possible, don't be stingy with in the requirement.
5. If the next step is "answering", set language and file_name to null, and the requirement should describe what to answer or explain to the user.
6. Be concise; only return the requirements for the next step.
7. The requirements must be in the following format:
\`\`\`json
{
"next_step": "answering" | "coding",
"language": "typescript" | "python" | null,
"file_name": string | null,
"requirement": string
}
\`\`\`
## Example 1:
User request: Create a calculator app.
You should return:
\`\`\`json
{
"next_step": "coding",
"language": "typescript",
"file_name": "calculator.tsx",
"requirement": "Generate code for a calculator app that has a simple UI with a display and button layout. The display should show the current input and the result. The buttons should include basic operators, numbers, clear, and equals. The calculation should work correctly."
}
\`\`\`
## Example 2:
User request: Explain how the game loop works.
Context: You have already generated the code for a snake game.
You should return:
\`\`\`json
{
"next_step": "answering",
"language": null,
"file_name": null,
"requirement": "The user is asking about the game loop. Explain how the game loop works."
}
\`\`\`
${context}
Now, plan the user's next step for this request:
${user_msg}
`;
const response = await llm.complete({
prompt,
});
// parse the response to Requirement
// 1. use regex to find the json block
const jsonBlock = response.text.match(/```json\s*([\s\S]*?)\s*```/);
if (!jsonBlock) {
throw new Error("No JSON block found in the response.");
}
const requirement = RequirementSchema.parse(JSON.parse(jsonBlock[1]));
state.memory.put({
role: "assistant",
content: `The plan for next step: \n${response.text}`,
});
if (requirement.next_step === "coding") {
return generateArtifactEvent.with({
requirement,
});
} else {
return synthesizeAnswerEvent.with({});
}
});
workflow.handle([generateArtifactEvent], async ({ data: planData }) => {
const { sendEvent } = getContext();
const { state } = getContext();
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
state: "generate",
requirement: planData.requirement.requirement,
},
}),
);
const previousArtifact = state.lastArtifact
? JSON.stringify(state.lastArtifact)
: "There is no previous artifact";
const requirementText = planData.requirement.requirement;
const prompt = `
You are a skilled developer who can help user with coding.
You are given a task to generate or update a code for a given requirement.
## Follow these instructions:
**1. Carefully read the user's requirements.**
If any details are ambiguous or missing, make reasonable assumptions and clearly reflect those in your output.
If the previous code is provided:
+ Carefully analyze the code with the request to make the right changes.
+ Avoid making a lot of changes from the previous code if the request is not to write the code from scratch again.
**2. For code requests:**
- If the user does not specify a framework or language, default to a React component using the Next.js framework.
- For Next.js, use Shadcn UI components, Typescript, @types/node, @types/react, @types/react-dom, PostCSS, and TailwindCSS.
The import pattern should be:
\`\`\`typescript
import { ComponentName } from "@/components/ui/component-name"
import { Markdown } from "@llamaindex/chat-ui"
import { cn } from "@/lib/utils"
\`\`\`
- Ensure the code is idiomatic, production-ready, and includes necessary imports.
- Only generate code relevant to the user's request—do not add extra boilerplate.
**3. Don't be verbose on response**
- No other text or comments only return the code which wrapped by \`\`\`language\`\`\` block.
- If the user's request is to update the code, only return the updated code.
**4. Only the following languages are allowed: "typescript", "python".**
**5. If there is no code to update, return the reason without any code block.**
## Example:
\`\`\`typescript
import React from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export default function MyComponent() {
return (
<div className="flex flex-col items-center justify-center h-screen">
<Button>Click me</Button>
</div>
);
}
\`\`\`
The previous code is:
{previousArtifact}
Now, i have to generate the code for the following requirement:
{requirement}
`
.replace("{previousArtifact}", previousArtifact)
.replace("{requirement}", requirementText);
const response = await llm.complete({
prompt,
});
// Extract the code from the response
const codeMatch = response.text.match(/```(\w+)([\s\S]*)```/);
if (!codeMatch) {
return synthesizeAnswerEvent.with({});
}
const code = codeMatch[2].trim();
// Put the generated code to the memory
state.memory.put({
role: "assistant",
content: `Updated the code: \n${response.text}`,
});
// To show the Canvas panel for the artifact
sendEvent(
artifactEvent.with({
type: "artifact",
data: {
type: "code",
created_at: Date.now(),
data: {
language: planData.requirement.language || "",
file_name: planData.requirement.file_name || "",
code,
},
},
}),
);
return synthesizeAnswerEvent.with({});
});
workflow.handle([synthesizeAnswerEvent], async () => {
const { sendEvent } = getContext();
const { state } = getContext();
const chatHistory = await state.memory.getMessages();
const messages = [
...chatHistory,
{
role: "system" as const,
content: `
You are a helpful assistant who is responsible for explaining the work to the user.
Based on the conversation history, provide an answer to the user's question.
The user has access to the code so avoid mentioning the whole code again in your response.
`,
},
];
const responseStream = await llm.chat({
messages,
stream: true,
});
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
state: "completed",
},
}),
);
let response = "";
for await (const chunk of responseStream) {
response += chunk.delta;
sendEvent(
agentStreamEvent.with({
delta: chunk.delta,
response: "",
currentAgentName: "assistant",
raw: chunk,
}),
);
}
return stopAgentEvent.with({
result: response,
});
});
return workflow;
}
@@ -1,416 +0,0 @@
import { toSourceEvent } from "@llamaindex/server";
import {
agentStreamEvent,
createStatefulMiddleware,
createWorkflow,
startAgentEvent,
stopAgentEvent,
workflowEvent,
} from "@llamaindex/workflow";
import {
ChatMemoryBuffer,
LlamaCloudIndex,
MessageContent,
Metadata,
MetadataMode,
NodeWithScore,
PromptTemplate,
Settings,
VectorStoreIndex,
extractText,
} from "llamaindex";
import { randomUUID } from "node:crypto";
import { z } from "zod";
import { getIndex } from "./data";
// workflow factory
export const workflowFactory = async (reqBody: any) => {
const index = await getIndex(reqBody?.data);
return getWorkflow(index);
};
// workflow configs
const MAX_QUESTIONS = 6; // max number of questions to research, research will stop when this number is reached
const TOP_K = 10; // number of nodes to retrieve from the vector store
const createPlanResearchPrompt = new PromptTemplate({
template: `
You are a professor who is guiding a researcher to research a specific request/problem.
Your task is to decide on a research plan for the researcher.
The possible actions are:
+ Provide a list of questions for the researcher to investigate, with the purpose of clarifying the request.
+ Write a report if the researcher has already gathered enough research on the topic and can resolve the initial request.
+ Cancel the research if most of the answers from researchers indicate there is insufficient information to research the request. Do not attempt more than 3 research iterations or too many questions.
The workflow should be:
+ Always begin by providing some initial questions for the researcher to investigate.
+ Analyze the provided answers against the initial topic/request. If the answers are insufficient to resolve the initial request, provide additional questions for the researcher to investigate.
+ If the answers are sufficient to resolve the initial request, instruct the researcher to write a report.
Here are the context:
<Collected information>
{context_str}
</Collected information>
<Conversation context>
{conversation_context}
</Conversation context>
{enhanced_prompt}
Now, provide your decision in the required format for this user request:
<User request>
{user_request}
</User request>
`,
templateVars: [
"context_str",
"conversation_context",
"enhanced_prompt",
"user_request",
],
});
const researchPrompt = new PromptTemplate({
template: `
You are a researcher who is in the process of answering the question.
The purpose is to answer the question based on the collected information, without using prior knowledge or making up any new information.
Always add citations to the sentence/point/paragraph using the id of the provided content.
The citation should follow this format: [citation:id] where id is the id of the content.
E.g:
If we have a context like this:
<Citation id='abc-xyz'>
Baby llama is called cria
</Citation id='abc-xyz'>
And your answer uses the content, then the citation should be:
- Baby llama is called cria [citation:abc-xyz]
Here is the provided context for the question:
<Collected information>
{context_str}
</Collected information>
No prior knowledge, just use the provided context to answer the question: {question}
`,
templateVars: ["context_str", "question"],
});
const WRITE_REPORT_PROMPT = `
You are a researcher writing a report based on a user request and the research context.
You have researched various perspectives related to the user request.
The report should provide a comprehensive outline covering all important points from the researched perspectives.
Create a well-structured outline for the research report that covers all the answers.
# IMPORTANT when writing in markdown format:
+ Use tables or figures where appropriate to enhance presentation.
+ Preserve all citation syntax (the \`[citation:id]()\` parts in the provided context). Keep these citations in the final report - no separate reference section is needed.
+ Do not add links, a table of contents, or a references section to the report.
`;
// workflow events
type ResearchQuestion = { questionId: string; question: string };
type ResearchResult = ResearchQuestion & { answer: string };
// class PlanResearchEvent extends WorkflowEvent<{}> {}
const planResearchEvent = workflowEvent<{}>();
const researchEvent = workflowEvent<ResearchQuestion>();
const reportEvent = workflowEvent<{}>();
export const UIEventSchema = z
.object({
event: z
.enum(["retrieve", "analyze", "answer"])
.describe(
"The type of event. DeepResearch has 3 main stages:\n1. retrieve: Retrieve the context from the vector store\n2. analyze: Analyze the context and generate a research questions to answer\n3. answer: Answer the provided questions. Each question has a unique id, when the state is done, the event will have the answer for the question.",
),
state: z
.enum(["pending", "inprogress", "done", "error"])
.describe("The state for each event"),
id: z.string().optional().describe("The id of the question"),
question: z
.string()
.optional()
.describe("The question generated by the LLM"),
answer: z.string().optional().describe("The answer generated by the LLM"),
})
.describe("DeepResearchEvent");
type UIEventData = z.infer<typeof UIEventSchema>;
const uiEvent = workflowEvent<{
type: "ui_event";
data: UIEventData;
}>();
// workflow definition
export function getWorkflow(index: VectorStoreIndex | LlamaCloudIndex) {
const retriever = index.asRetriever({ similarityTopK: TOP_K });
const { withState, getContext } = createStatefulMiddleware(() => {
return {
memory: new ChatMemoryBuffer({
llm: Settings.llm,
chatHistory: [],
}),
contextNodes: [] as NodeWithScore<Metadata>[],
userRequest: "" as MessageContent,
totalQuestions: 0,
researchResults: [] as ResearchResult[],
};
});
const workflow = withState(createWorkflow());
workflow.handle([startAgentEvent], async ({ data }) => {
const { userInput, chatHistory = [] } = data;
const { sendEvent, state } = getContext();
if (!userInput) throw new Error("Invalid input");
state.memory.set(chatHistory);
state.memory.put({ role: "user", content: userInput });
state.userRequest = userInput;
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
event: "retrieve",
state: "inprogress",
},
}),
);
const retrievedNodes = await retriever.retrieve({ query: userInput });
sendEvent(toSourceEvent(retrievedNodes));
sendEvent(
uiEvent.with({
type: "ui_event",
data: { event: "retrieve", state: "done" },
}),
);
state.contextNodes.push(...retrievedNodes);
return planResearchEvent.with({});
});
workflow.handle([planResearchEvent], async ({ data }) => {
const { sendEvent, state, stream } = getContext();
sendEvent(
uiEvent.with({
type: "ui_event",
data: { event: "analyze", state: "inprogress" },
}),
);
const { decision, researchQuestions, cancelReason } =
await createResearchPlan(
state.memory,
state.contextNodes
.map((node) => node.node.getContent(MetadataMode.NONE))
.join("\n"),
enhancedPrompt(state.totalQuestions),
state.userRequest,
);
sendEvent(
uiEvent.with({
type: "ui_event",
data: { event: "analyze", state: "done" },
}),
);
if (decision === "cancel") {
sendEvent(
uiEvent.with({
type: "ui_event",
data: { event: "analyze", state: "done" },
}),
);
return agentStreamEvent.with({
delta: cancelReason ?? "Research cancelled without any reason.",
response: cancelReason ?? "Research cancelled without any reason.",
currentAgentName: "",
raw: null,
});
}
if (decision === "research" && researchQuestions.length > 0) {
state.totalQuestions += researchQuestions.length;
state.memory.put({
role: "assistant",
content:
"We need to find answers to the following questions:\n" +
researchQuestions.join("\n"),
});
researchQuestions.forEach(({ questionId: id, question }) => {
sendEvent(
uiEvent.with({
type: "ui_event",
data: { event: "answer", state: "pending", id, question },
}),
);
sendEvent(researchEvent.with({ questionId: id, question }));
});
const events = await stream
.until(() => state.researchResults.length === researchQuestions.length)
.toArray();
return planResearchEvent.with({});
}
state.memory.put({
role: "assistant",
content: "No more idea to analyze. We should report the answers.",
});
sendEvent(
uiEvent.with({
type: "ui_event",
data: { event: "analyze", state: "done" },
}),
);
return reportEvent.with({});
});
workflow.handle([researchEvent], async ({ data }) => {
const { sendEvent, state } = getContext();
const { questionId, question } = data;
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
event: "answer",
state: "inprogress",
id: questionId,
question,
},
}),
);
const answer = await answerQuestion(
contextStr(state.contextNodes),
question,
);
state.researchResults.push({ questionId, question, answer });
state.memory.put({
role: "assistant",
content: `<Question>${question}</Question>\n<Answer>${answer}</Answer>`,
});
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
event: "answer",
state: "done",
id: questionId,
question,
answer,
},
}),
);
});
workflow.handle([reportEvent], async ({ data }) => {
const { sendEvent, state } = getContext();
const chatHistory = await state.memory.getAllMessages();
const messages = chatHistory.concat([
{
role: "system",
content: WRITE_REPORT_PROMPT,
},
{
role: "user",
content:
"Write a report addressing the user request based on the research provided the context",
},
]);
const stream = await Settings.llm.chat({ messages, stream: true });
let response = "";
for await (const chunk of stream) {
response += chunk.delta;
sendEvent(
agentStreamEvent.with({
delta: chunk.delta,
response,
currentAgentName: "",
raw: stream,
}),
);
}
return stopAgentEvent.with({
result: response,
});
});
return workflow;
}
const createResearchPlan = async (
memory: ChatMemoryBuffer,
contextStr: string,
enhancedPrompt: string,
userRequest: MessageContent,
) => {
const chatHistory = await memory.getMessages();
const conversationContext = chatHistory
.map((message) => `${message.role}: ${message.content}`)
.join("\n");
const prompt = createPlanResearchPrompt.format({
context_str: contextStr,
conversation_context: conversationContext,
enhanced_prompt: enhancedPrompt,
user_request: extractText(userRequest),
});
const responseFormat = z.object({
decision: z.enum(["research", "write", "cancel"]),
researchQuestions: z.array(z.string()),
cancelReason: z.string().optional(),
});
const result = await Settings.llm.complete({ prompt, responseFormat });
const plan = JSON.parse(result.text) as z.infer<typeof responseFormat>;
return {
...plan,
researchQuestions: plan.researchQuestions.map((question) => ({
questionId: randomUUID(),
question,
})),
};
};
const contextStr = (contextNodes: NodeWithScore<Metadata>[]) => {
return contextNodes
.map((node) => {
const nodeId = node.node.id_;
const nodeContent = node.node.getContent(MetadataMode.NONE);
return `<Citation id='${nodeId}'>\n${nodeContent}</Citation id='${nodeId}'>`;
})
.join("\n");
};
const enhancedPrompt = (totalQuestions: number) => {
if (totalQuestions === 0) {
return "The student has no questions to research. Let start by providing some questions for the student to research.";
}
if (totalQuestions >= MAX_QUESTIONS) {
return `The student has researched ${totalQuestions} questions. Should proceeding writing report or cancel the research if the answers are not enough to write a report.`;
}
return "";
};
const answerQuestion = async (contextStr: string, question: string) => {
const prompt = researchPrompt.format({
context_str: contextStr,
question,
});
const result = await Settings.llm.complete({ prompt });
return result.text;
};
@@ -1,53 +0,0 @@
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
```
Third, 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).
## Custom UI Components
We have a custom component located in `components/ui_event.jsx`. This is used to display the state of artifact workflows in UI. You can regenerate a new UI component from the workflow event schema by running the following command:
```
npm run generate:ui
```
## Use Case
AI-powered document generator that can help you generate documents with a chat interface and simple markdown editor.
To update the workflow, you can modify the code in [`workflow.ts`](app/workflow.ts).
You can start by sending an request on the [chat UI](http://localhost:3000) or you can test the `/api/chat` endpoint with the following curl request:
```shell
curl --location 'localhost:3000/api/chat' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "Compare the financial performance of Apple and Tesla" }] }'
```
## 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!
@@ -1,328 +0,0 @@
import { extractLastArtifact } from "@llamaindex/server";
import { ChatMemoryBuffer, MessageContent, Settings } from "llamaindex";
import {
agentStreamEvent,
createStatefulMiddleware,
createWorkflow,
startAgentEvent,
stopAgentEvent,
workflowEvent,
} from "@llamaindex/workflow";
import { z } from "zod";
export const DocumentRequirementSchema = z.object({
type: z.enum(["markdown", "html"]),
title: z.string(),
requirement: z.string(),
});
export type DocumentRequirement = z.infer<typeof DocumentRequirementSchema>;
export const UIEventSchema = z.object({
type: z.literal("ui_event"),
data: z.object({
state: z
.enum(["plan", "generate", "completed"])
.describe(
"The current state of the workflow: 'plan', 'generate', or 'completed'.",
),
requirement: z
.string()
.optional()
.describe(
"An optional requirement creating or updating a document, if applicable.",
),
}),
});
export type UIEvent = z.infer<typeof UIEventSchema>;
const planEvent = workflowEvent<{
userInput: MessageContent;
context?: string | undefined;
}>();
const generateArtifactEvent = workflowEvent<{
requirement: DocumentRequirement;
}>();
const synthesizeAnswerEvent = workflowEvent<{
requirement: DocumentRequirement;
generatedArtifact: string;
}>();
const uiEvent = workflowEvent<UIEvent>();
const artifactEvent = workflowEvent<{
type: "artifact";
data: {
type: "document";
created_at: number;
data: {
title: string;
content: string;
type: "markdown" | "html";
};
};
}>();
export function workflowFactory(reqBody: any) {
const llm = Settings.llm;
const { withState, getContext } = createStatefulMiddleware(() => {
return {
memory: new ChatMemoryBuffer({ llm }),
lastArtifact: extractLastArtifact(reqBody),
};
});
const workflow = withState(createWorkflow());
workflow.handle([startAgentEvent], async ({ data }) => {
const { userInput, chatHistory = [] } = data;
// Prepare chat history
const { state } = getContext();
// Put user input to the memory
if (!userInput) {
throw new Error("Missing user input to start the workflow");
}
state.memory.set(chatHistory);
state.memory.put({ role: "user", content: userInput });
return planEvent.with({
userInput,
context: state.lastArtifact
? JSON.stringify(state.lastArtifact)
: undefined,
});
});
workflow.handle([planEvent], async ({ data: planData }) => {
const { sendEvent } = getContext();
const { state } = getContext();
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
state: "plan",
},
}),
);
const user_msg = planData.userInput;
const context = planData.context
? `## The context is: \n${planData.context}\n`
: "";
const prompt = `
You are a documentation analyst responsible for analyzing the user's request and providing requirements for document generation or update.
Follow these instructions:
1. Carefully analyze the conversation history and the user's request to determine what has been done and what the next step should be.
2. From the user's request, provide requirements for the next step of the document generation or update.
3. Do not be verbose; only return the requirements for the next step of the document generation or update.
4. Only the following document types are allowed: "markdown", "html".
5. The requirement should be in the following format:
\`\`\`json
{
"type": "markdown" | "html",
"title": string,
"requirement": string
}
\`\`\`
## Example:
User request: Create a project guideline document.
You should return:
\`\`\`json
{
"type": "markdown",
"title": "Project Guideline",
"requirement": "Generate a Markdown document that outlines the project goals, deliverables, and timeline. Include sections for introduction, objectives, deliverables, and timeline."
}
\`\`\`
User request: Add a troubleshooting section to the guideline.
You should return:
\`\`\`json
{
"type": "markdown",
"title": "Project Guideline",
"requirement": "Add a 'Troubleshooting' section at the end of the document with common issues and solutions."
}
\`\`\`
${context}
Now, please plan for the user's request:
${user_msg}
`;
const response = await llm.complete({
prompt,
});
// Parse the response to DocumentRequirement
const jsonBlock = response.text.match(/```json\s*([\s\S]*?)\s*```/);
if (!jsonBlock) {
throw new Error("No JSON block found in the response.");
}
const requirement = DocumentRequirementSchema.parse(
JSON.parse(jsonBlock[1]),
);
state.memory.put({
role: "assistant",
content: `Planning for the document generation: \n${response.text}`,
});
return generateArtifactEvent.with({
requirement,
});
});
workflow.handle(
[generateArtifactEvent],
async ({ data: { requirement } }) => {
const { sendEvent } = getContext();
const { state } = getContext();
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
state: "generate",
requirement: requirement.requirement,
},
}),
);
const previousArtifact = state.lastArtifact
? JSON.stringify(state.lastArtifact)
: "";
const requirementStr = JSON.stringify(requirement);
const prompt = `
You are a skilled technical writer who can help users with documentation.
You are given a task to generate or update a document for a given requirement.
## Follow these instructions:
**1. Carefully read the user's requirements.**
If any details are ambiguous or missing, make reasonable assumptions and clearly reflect those in your output.
If the previous document is provided:
+ Carefully analyze the document with the request to make the right changes.
+ Avoid making unnecessary changes from the previous document if the request is not to rewrite it from scratch.
**2. For document requests:**
- If the user does not specify a type, default to Markdown.
- Ensure the document is clear, well-structured, and grammatically correct.
- Only generate content relevant to the user's request—do not add extra boilerplate.
**3. Do not be verbose in your response.**
- No other text or comments; only return the document content wrapped by the appropriate code block (\`\`\`markdown or \`\`\`html).
- If the user's request is to update the document, only return the updated document.
**4. Only the following types are allowed: "markdown", "html".**
**5. If there is no change to the document, return the reason without any code block.**
## Example:
\`\`\`markdown
# Project Guideline
## Introduction
...
\`\`\`
The previous content is:
${previousArtifact}
Now, please generate the document for the following requirement:
${requirementStr}
`;
const response = await llm.complete({
prompt,
});
// Extract the document from the response
const docMatch = response.text.match(/```(markdown|html)([\s\S]*)```/);
const generatedContent = response.text;
if (docMatch) {
const content = docMatch[2].trim();
const docType = docMatch[1] as "markdown" | "html";
// Put the generated document to the memory
state.memory.put({
role: "assistant",
content: `Generated document: \n${response.text}`,
});
// To show the Canvas panel for the artifact
sendEvent(
artifactEvent.with({
type: "artifact",
data: {
type: "document",
created_at: Date.now(),
data: {
title: requirement.title,
content: content,
type: docType,
},
},
}),
);
}
return synthesizeAnswerEvent.with({
requirement,
generatedArtifact: generatedContent,
});
},
);
workflow.handle([synthesizeAnswerEvent], async ({ data }) => {
const { sendEvent } = getContext();
const { state } = getContext();
const chatHistory = await state.memory.getMessages();
const messages = [
...chatHistory,
{
role: "system" as const,
content: `
Your responsibility is to explain the work to the user.
If there is no document to update, explain the reason.
If the document is updated, just summarize what changed. Don't need to include the whole document again in the response.
`,
},
];
const responseStream = await llm.chat({
messages,
stream: true,
});
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
state: "completed",
requirement: data.requirement.requirement,
},
}),
);
let response = "";
for await (const chunk of responseStream) {
response += chunk.delta;
sendEvent(
agentStreamEvent.with({
delta: chunk.delta,
response: "",
currentAgentName: "assistant",
raw: chunk,
}),
);
}
return stopAgentEvent.with({
result: response,
});
});
return workflow;
}
@@ -1,318 +0,0 @@
import { toAgentRunEvent, toSourceEvent } from "@llamaindex/server";
import {
callTools,
chatWithTools,
documentGenerator,
interpreter,
} from "@llamaindex/tools";
import {
agentStreamEvent,
createStatefulMiddleware,
createWorkflow,
startAgentEvent,
stopAgentEvent,
workflowEvent,
} from "@llamaindex/workflow";
import {
BaseToolWithCall,
ChatMemoryBuffer,
ChatMessage,
Metadata,
NodeWithScore,
Settings,
ToolCall,
ToolCallLLM,
} from "llamaindex";
import { getIndex } from "./data";
export async function workflowFactory(reqBody: any) {
const index = await getIndex(reqBody?.data);
const queryEngineTool = index.queryTool({
metadata: {
name: "query_document",
description: `This tool can retrieve information about Apple and Tesla financial data`,
},
includeSourceNodes: true,
});
if (!process.env.E2B_API_KEY) {
throw new Error("E2B_API_KEY is required to use the code interpreter tool");
}
const codeInterpreterTool = interpreter({
apiKey: process.env.E2B_API_KEY!,
});
const documentGeneratorTool = documentGenerator();
return getWorkflow(
queryEngineTool,
codeInterpreterTool,
documentGeneratorTool,
);
}
// workflow events
const inputEvent = workflowEvent<{ input: ChatMessage[] }>();
const researchEvent = workflowEvent<{ toolCalls: ToolCall[] }>();
const analyzeEvent = workflowEvent<{ input: ChatMessage | ToolCall[] }>();
const reportGenerationEvent = workflowEvent<{ toolCalls: ToolCall[] }>();
const DEFAULT_SYSTEM_PROMPT = `
You are a financial analyst who are given a set of tools to help you.
It's good to using appropriate tools for the user request and always use the information from the tools, don't make up anything yourself.
For the query engine tool, you should break down the user request into a list of queries and call the tool with the queries.
`;
// workflow definition
export function getWorkflow(
queryEngineTool: BaseToolWithCall,
codeInterpreterTool: BaseToolWithCall,
documentGeneratorTool: BaseToolWithCall,
) {
const llm = Settings.llm as ToolCallLLM;
if (!llm.supportToolCall) {
throw new Error("LLM is not a ToolCallLLM");
}
const { withState, getContext } = createStatefulMiddleware(() => ({
memory: new ChatMemoryBuffer({ llm, chatHistory: [] }),
}));
const workflow = withState(createWorkflow());
// Add steps
workflow.handle([startAgentEvent], async ({ data }) => {
const { state } = getContext();
const { userInput, chatHistory = [] } = data;
if (!userInput) throw new Error("Invalid input");
state.memory.set(chatHistory);
state.memory.put({ role: "system", content: DEFAULT_SYSTEM_PROMPT });
state.memory.put({ role: "user", content: userInput });
const messages = await state.memory.getMessages();
return inputEvent.with({ input: messages });
});
workflow.handle([inputEvent], async ({ data }) => {
const { sendEvent, state } = getContext();
const chatHistory = data.input;
const tools = [codeInterpreterTool, documentGeneratorTool, queryEngineTool];
const toolCallResponse = await chatWithTools(llm, tools, chatHistory);
if (!toolCallResponse.hasToolCall()) {
const generator = toolCallResponse.responseGenerator;
let response = "";
if (generator) {
for await (const chunk of generator) {
response += chunk.delta;
sendEvent(
agentStreamEvent.with({
delta: chunk.delta,
response,
currentAgentName: "LLM", // Or derive from context if needed
raw: chunk.raw,
}),
);
}
}
return stopAgentEvent.with({ result: response });
}
if (toolCallResponse.hasMultipleTools()) {
state.memory.put({
role: "assistant",
content:
"Calling different tools is not allowed. Please only use multiple calls of the same tool.",
});
const newChatHistory = await state.memory.getMessages();
return inputEvent.with({ input: newChatHistory });
}
// Put the LLM tool call message into the memory
// And trigger the next step according to the tool call
if (toolCallResponse.toolCallMessage) {
state.memory.put(toolCallResponse.toolCallMessage);
}
const toolName = toolCallResponse.getToolNames()[0];
switch (toolName) {
case codeInterpreterTool.metadata.name:
return analyzeEvent.with({
input: toolCallResponse.toolCalls,
});
case documentGeneratorTool.metadata.name:
return reportGenerationEvent.with({
toolCalls: toolCallResponse.toolCalls,
});
default:
if (queryEngineTool.metadata.name === toolName) {
return researchEvent.with({
toolCalls: toolCallResponse.toolCalls,
});
}
throw new Error(`Unknown tool: ${toolName}`);
}
});
workflow.handle([researchEvent], async ({ data }) => {
const { sendEvent, state } = getContext();
sendEvent(
toAgentRunEvent({
agent: "Researcher",
text: "Researching data",
type: "text",
}),
);
const { toolCalls } = data;
const toolMsgs = await callTools({
tools: [queryEngineTool],
toolCalls,
writeEvent: (text, step) => {
sendEvent(
toAgentRunEvent({
agent: "Researcher",
text,
type: toolCalls.length > 1 ? "progress" : "text",
current: step,
total: toolCalls.length,
}),
);
},
});
for (const toolMsg of toolMsgs) {
state.memory.put(toolMsg);
}
const sourcesNodes: NodeWithScore<Metadata>[] = toolMsgs
.map((msg) => (msg.options as any)?.toolResult?.result?.sourceNodes)
.flat()
.filter(Boolean);
if (sourcesNodes.length > 0) {
sendEvent(toSourceEvent(sourcesNodes));
}
// Send a message indicating research is done, triggering analysis
return analyzeEvent.with({
input: {
role: "assistant",
content:
"I have finished researching the data, please analyze the data.",
},
});
});
/**
* Analyze a research result or a tool call for code interpreter from the LLM
*/
workflow.handle([analyzeEvent], async ({ data }) => {
const { sendEvent, state } = getContext();
sendEvent(
toAgentRunEvent({
agent: "Analyst",
text: "Analyzing data",
type: "text",
}),
);
// Request by workflow LLM, input is a list of tool calls
let toolCalls: ToolCall[] = [];
if (Array.isArray(data.input)) {
toolCalls = data.input;
} else {
// Requested by Researcher, input is a ChatMessage
// We start new LLM chat specifically for analyzing the data
const analysisPrompt = `
You are an expert in analyzing financial data.
You are given a set of financial data to analyze. Your task is to analyze the financial data and return a report.
Your response should include a detailed analysis of the financial data, including any trends, patterns, or insights that you find.
Construct the analysis in textual format; including tables would be great!
Don't need to synthesize the data, just analyze and provide your findings.
`;
// Clone the current chat history
// Add the analysis system prompt and the message from the researcher
const currentChatHistory = await state.memory.getMessages();
const newChatHistory = [
...currentChatHistory,
{ role: "system", content: analysisPrompt },
data.input, // This is the ChatMessage from the research step
];
const toolCallResponse = await chatWithTools(
llm,
[codeInterpreterTool],
newChatHistory as ChatMessage[],
);
if (!toolCallResponse.hasToolCall()) {
// If no tool call needed for analysis, put the response directly
state.memory.put(await toolCallResponse.asFullResponse());
const finalChatHistory = await state.memory.getMessages();
return inputEvent.with({ input: finalChatHistory });
} else {
state.memory.put(toolCallResponse.toolCallMessage!);
toolCalls = toolCallResponse.toolCalls;
}
}
// Call the code interpreter tools if needed
if (toolCalls.length > 0) {
const toolMsgs = await callTools({
tools: [codeInterpreterTool],
toolCalls,
writeEvent: (text, step) => {
sendEvent(
toAgentRunEvent({
agent: "Analyst",
text,
type: toolCalls.length > 1 ? "progress" : "text",
current: step,
total: toolCalls.length,
}),
);
},
});
for (const toolMsg of toolMsgs) {
state.memory.put(toolMsg);
}
}
const finalChatHistory = await state.memory.getMessages();
// After analysis (or tool calls for analysis), trigger the next LLM input cycle
return inputEvent.with({ input: finalChatHistory });
});
workflow.handle([reportGenerationEvent], async ({ data }) => {
const { sendEvent, state } = getContext();
const { toolCalls } = data;
const toolMsgs = await callTools({
tools: [documentGeneratorTool],
toolCalls,
writeEvent: (text, step) => {
sendEvent(
toAgentRunEvent({
agent: "Reporter",
text,
type: toolCalls.length > 1 ? "progress" : "text",
current: step,
total: toolCalls.length,
}),
);
},
});
for (const toolMsg of toolMsgs) {
state.memory.put(toolMsg);
}
const chatHistory = await state.memory.getMessages();
// After report generation, trigger the next LLM input cycle
return inputEvent.with({ input: chatHistory });
});
return workflow;
}
@@ -1,39 +0,0 @@
import { SimpleDirectoryReader } from "@llamaindex/readers/directory";
import "dotenv/config";
import { storageContextFromDefaults, VectorStoreIndex } from "llamaindex";
import { initSettings } from "./app/settings";
async function generateDatasource() {
console.log(`Generating storage context...`);
// Split documents, create embeddings and store them in the storage context
const storageContext = await storageContextFromDefaults({
persistDir: "storage",
});
// load documents from current directory into an index
const reader = new SimpleDirectoryReader();
const documents = await reader.loadData("data");
await VectorStoreIndex.fromDocuments(documents, {
storageContext,
});
console.log("Storage context successfully generated.");
}
(async () => {
const args = process.argv.slice(2);
const command = args[0];
initSettings();
if (command === "ui") {
console.error("This project doesn't use any custom UI.");
return;
} else {
if (command !== "datasource") {
console.error(
`Unrecognized command: ${command}. Generating datasource by default.`,
);
}
await generateDatasource();
}
})();
@@ -12,12 +12,11 @@ from llama_index.server.services.llamacloud.generate import (
load_to_llamacloud,
)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
def generate_index():
def generate_datasource():
init_settings()
logger.info("Generate index for the provided data")
@@ -28,26 +27,5 @@ def generate_index():
load_to_llamacloud(index, logger=logger)
def generate_ui_for_workflow():
"""
Generate UI for UIEventData event in app/workflow.py
"""
import asyncio
from llama_index.llms.openai import OpenAI
from main import COMPONENT_DIR
# To generate UI components for additional event types,
# import the corresponding data model (e.g., MyCustomEventData)
# and run the generate_ui_for_workflow function with the imported model.
# Make sure the output filename of the generated UI component matches the event type (here `ui_event`)
try:
from app.workflow import UIEventData # type: ignore
except ImportError:
raise ImportError("Couldn't generate UI component for the current workflow.")
from llama_index.server.gen_ui import generate_event_component
# works also well with Claude 3.7 Sonnet or Gemini Pro 2.5
llm = OpenAI(model="gpt-4.1")
code = asyncio.run(generate_event_component(event_cls=UIEventData, llm=llm))
with open(f"{COMPONENT_DIR}/ui_event.jsx", "w") as f:
f.write(code)
if __name__ == "__main__":
generate_datasource()
@@ -1,8 +1,7 @@
import { generateEventComponent } from "@llamaindex/server";
import * as dotenv from "dotenv";
import "dotenv/config";
import * as fs from "fs/promises";
import { LLamaCloudFileService, OpenAI } from "llamaindex";
import { LLamaCloudFileService } from "llamaindex";
import * as path from "path";
import { getIndex } from "./app/data";
import { initSettings } from "./app/settings";
@@ -89,7 +88,7 @@ async function loadAndIndex() {
console.log(`Successfully uploaded documents to LlamaCloud!`);
}
async function generateDatasource() {
(async () => {
try {
checkRequiredEnvVars();
initSettings();
@@ -98,39 +97,4 @@ async function generateDatasource() {
} catch (error) {
console.error("Error generating storage.", error);
}
}
async function generateUi() {
// Also works well with Claude 3.5 Sonnet and Google Gemini 2.5 Pro
const llm = new OpenAI({ model: "gpt-4.1" });
const workflowModule = await import("./app/workflow");
const UIEventSchema = (workflowModule as any).UIEventSchema;
if (!UIEventSchema) {
throw new Error(
"To generate the UI, you must define a UIEventSchema in your workflow.",
);
}
const generatedCode = await generateEventComponent(UIEventSchema, llm);
// Write the generated code to components/ui_event.ts
await fs.writeFile("components/ui_event.jsx", generatedCode);
}
(async () => {
const args = process.argv.slice(2);
const command = args[0];
initSettings();
if (command === "datasource") {
await generateDatasource();
} else if (command === "ui") {
await generateUi();
} else {
console.error(
'Invalid command. Please use "datasource" or "ui". Running "datasource" by default.',
);
await generateDatasource(); // Default behavior or could throw an error
}
})();
@@ -1,4 +1,4 @@
import { LlamaCloudIndex } from "llamaindex";
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
type LlamaCloudDataSourceParams = {
llamaCloudPipeline?: {
@@ -1,4 +1,4 @@
import { LlamaCloudIndex } from "llamaindex";
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
type LlamaCloudDataSourceParams = {
llamaCloudPipeline?: {
@@ -33,9 +33,12 @@ You can configure [LLM model](https://docs.llamaindex.ai/en/stable/module_guides
## Use Case
AI-powered document generator that can help you generate documents with a chat interface and simple markdown editor.
We have prepared two artifact workflows:
To update the workflow, you can modify the code in [`workflow.py`](app/workflow.py).
- [Code Workflow](app/code_workflow.py): To generate code and display it in the UI like Vercel's v0.
- [Document Workflow](app/document_workflow.py): Generate and update a document like OpenAI's canvas.
Modify the factory method in [`workflow.py`](app/workflow.py) to decide which artifact workflow to use. Without any changes the Code Workflow is used.
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:
@@ -6,7 +6,6 @@ from llama_index.core.chat_engine.types import ChatMessage
from llama_index.core.llms import LLM
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.prompts import PromptTemplate
from llama_index.llms.openai import OpenAI
from llama_index.core.workflow import (
Context,
Event,
@@ -27,15 +26,6 @@ from llama_index.server.api.utils import get_last_artifact
from pydantic import BaseModel, Field
def create_workflow(chat_request: ChatRequest) -> Workflow:
workflow = CodeArtifactWorkflow(
llm=OpenAI(model="gpt-4.1"),
chat_request=chat_request,
timeout=120.0,
)
return workflow
class Requirement(BaseModel):
next_step: Literal["answering", "coding"]
language: Optional[str] = None
@@ -4,7 +4,6 @@ from typing import Any, Literal, Optional
from llama_index.core.chat_engine.types import ChatMessage
from llama_index.core.llms import LLM
from llama_index.llms.openai import OpenAI
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.prompts import PromptTemplate
from llama_index.core.workflow import (
@@ -27,15 +26,6 @@ from llama_index.server.api.utils import get_last_artifact
from pydantic import BaseModel, Field
def create_workflow(chat_request: ChatRequest) -> Workflow:
workflow = DocumentArtifactWorkflow(
llm=OpenAI(model="gpt-4.1"),
chat_request=chat_request,
timeout=120.0,
)
return workflow
class DocumentRequirement(BaseModel):
type: Literal["markdown", "html"]
title: str
@@ -0,0 +1,15 @@
from app.code_workflow import CodeArtifactWorkflow
# from app.document_workflow import DocumentArtifactWorkflow to generate documents
from llama_index.core.workflow import Workflow
from llama_index.llms.openai import OpenAI
from llama_index.server.api.models import ChatRequest
def create_workflow(chat_request: ChatRequest) -> Workflow:
workflow = CodeArtifactWorkflow(
llm=OpenAI(model="gpt-4.1"),
chat_request=chat_request,
timeout=120.0,
)
return workflow
@@ -1,4 +1,4 @@
import { agent } from "@llamaindex/workflow";
import { agent } from "llamaindex";
import { getIndex } from "./data";
export const workflowFactory = async (reqBody: any) => {
@@ -31,7 +31,7 @@ You can configure [LLM model](https://ts.llamaindex.ai/docs/llamaindex/modules/l
## Custom UI Components
For Deep Research, we have a custom component located in `components/ui_event.jsx`. This is used to display the results of the deep research workflow in a more user-friendly way
For Deep Research, we have a custom component located in `components/deep_research_event.jsx`. This is used to display the results of the deep research workflow in a more user-friendly way
### Generate a new UI Component from workflow event
@@ -0,0 +1,447 @@
import { toSourceEvent, toStreamGenerator } from "@llamaindex/server";
import {
AgentInputData,
AgentWorkflowContext,
ChatMemoryBuffer,
ChatResponseChunk,
HandlerContext,
LlamaCloudIndex,
Metadata,
MetadataMode,
NodeWithScore,
PromptTemplate,
Settings,
StartEvent,
StopEvent as StopEventBase,
ToolCallLLM,
VectorStoreIndex,
Workflow,
WorkflowEvent,
} from "llamaindex";
import { randomUUID } from "node:crypto";
import { z } from "zod";
import { getIndex } from "./data";
// workflow factory
export const workflowFactory = async (reqBody: any) => {
const index = await getIndex(reqBody?.data);
return new DeepResearchWorkflow(index);
};
// workflow configs
const MAX_QUESTIONS = 6; // max number of questions to research, research will stop when this number is reached
const TIMEOUT = 360; // timeout in seconds
const TOP_K = 10; // number of nodes to retrieve from the vector store
const createPlanResearchPrompt = new PromptTemplate({
template: `
You are a professor who is guiding a researcher to research a specific request/problem.
Your task is to decide on a research plan for the researcher.
The possible actions are:
+ Provide a list of questions for the researcher to investigate, with the purpose of clarifying the request.
+ Write a report if the researcher has already gathered enough research on the topic and can resolve the initial request.
+ Cancel the research if most of the answers from researchers indicate there is insufficient information to research the request. Do not attempt more than 3 research iterations or too many questions.
The workflow should be:
+ Always begin by providing some initial questions for the researcher to investigate.
+ Analyze the provided answers against the initial topic/request. If the answers are insufficient to resolve the initial request, provide additional questions for the researcher to investigate.
+ If the answers are sufficient to resolve the initial request, instruct the researcher to write a report.
Here are the context:
<Collected information>
{context_str}
</Collected information>
<Conversation context>
{conversation_context}
</Conversation context>
{enhanced_prompt}
Now, provide your decision in the required format for this user request:
<User request>
{user_request}
</User request>
`,
templateVars: [
"context_str",
"conversation_context",
"enhanced_prompt",
"user_request",
],
});
const researchPrompt = new PromptTemplate({
template: `
You are a researcher who is in the process of answering the question.
The purpose is to answer the question based on the collected information, without using prior knowledge or making up any new information.
Always add citations to the sentence/point/paragraph using the id of the provided content.
The citation should follow this format: [citation:id] where id is the id of the content.
E.g:
If we have a context like this:
<Citation id='abc-xyz'>
Baby llama is called cria
</Citation id='abc-xyz'>
And your answer uses the content, then the citation should be:
- Baby llama is called cria [citation:abc-xyz]
Here is the provided context for the question:
<Collected information>
{context_str}
</Collected information>
No prior knowledge, just use the provided context to answer the question: {question}
`,
templateVars: ["context_str", "question"],
});
const WRITE_REPORT_PROMPT = `
You are a researcher writing a report based on a user request and the research context.
You have researched various perspectives related to the user request.
The report should provide a comprehensive outline covering all important points from the researched perspectives.
Create a well-structured outline for the research report that covers all the answers.
# IMPORTANT when writing in markdown format:
+ Use tables or figures where appropriate to enhance presentation.
+ Preserve all citation syntax (the \`[citation:id]()\` parts in the provided context). Keep these citations in the final report - no separate reference section is needed.
+ Do not add links, a table of contents, or a references section to the report.
`;
// workflow events
type ResearchQuestion = { questionId: string; question: string };
type ResearchResult = ResearchQuestion & { answer: string };
class PlanResearchEvent extends WorkflowEvent<{}> {}
class ResearchEvent extends WorkflowEvent<ResearchQuestion[]> {}
class ReportEvent extends WorkflowEvent<{}> {}
class StopEvent extends StopEventBase<AsyncGenerator<ChatResponseChunk>> {}
export const UIEventSchema = z
.object({
event: z
.enum(["retrieve", "analyze", "answer"])
.describe(
"The type of event. DeepResearch has 3 main stages:\n1. retrieve: Retrieve the context from the vector store\n2. analyze: Analyze the context and generate a research questions to answer\n3. answer: Answer the provided questions. Each question has a unique id, when the state is done, the event will have the answer for the question.",
),
state: z
.enum(["pending", "inprogress", "done", "error"])
.describe("The state for each event"),
id: z.string().optional().describe("The id of the question"),
question: z
.string()
.optional()
.describe("The question generated by the LLM"),
answer: z.string().optional().describe("The answer generated by the LLM"),
})
.describe("DeepResearchEvent");
type UIEventData = z.infer<typeof UIEventSchema>;
class UIEvent extends WorkflowEvent<{
type: "ui_event";
data: UIEventData;
}> {}
// workflow definition
class DeepResearchWorkflow extends Workflow<
AgentWorkflowContext,
AgentInputData,
string
> {
#llm = Settings.llm as ToolCallLLM;
#index?: VectorStoreIndex | LlamaCloudIndex;
userRequest: string = "";
totalQuestions: number = 0;
contextNodes: NodeWithScore<Metadata>[] = [];
memory: ChatMemoryBuffer = new ChatMemoryBuffer({ llm: Settings.llm });
constructor(index: VectorStoreIndex | LlamaCloudIndex) {
super({ timeout: TIMEOUT });
this.#index = index;
this.addWorkflowSteps();
}
addWorkflowSteps() {
this.addStep(
{
inputs: [StartEvent<AgentInputData>],
outputs: [PlanResearchEvent],
},
this.handleStartWorkflow,
);
this.addStep(
{
inputs: [PlanResearchEvent],
outputs: [ResearchEvent, ReportEvent, StopEvent],
},
this.handlePlanResearch,
);
this.addStep(
{
inputs: [ResearchEvent],
outputs: [PlanResearchEvent],
},
this.handleResearch,
);
this.addStep(
{
inputs: [ReportEvent],
outputs: [StopEvent],
},
this.handleReport,
);
}
async initWorkflow(data: AgentInputData) {
const { userInput, chatHistory = [] } = data;
if (!userInput) throw new Error("Invalid input");
this.userRequest = userInput;
await this.memory.set(chatHistory);
await this.memory.put({ role: "user", content: userInput });
}
handleStartWorkflow = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: StartEvent<AgentInputData>,
): Promise<PlanResearchEvent> => {
await this.initWorkflow(ev.data);
ctx.sendEvent(
new UIEvent({
type: "ui_event",
data: { event: "retrieve", state: "inprogress" },
}),
);
const retrievedNodes = await this.retriever.retrieve(this.userRequest);
ctx.sendEvent(toSourceEvent(retrievedNodes));
ctx.sendEvent(
new UIEvent({
type: "ui_event",
data: { event: "retrieve", state: "done" },
}),
);
this.contextNodes = retrievedNodes;
return new PlanResearchEvent({});
};
handlePlanResearch = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: PlanResearchEvent,
): Promise<ResearchEvent | ReportEvent | StopEvent> => {
ctx.sendEvent(
new UIEvent({
type: "ui_event",
data: { event: "analyze", state: "inprogress" },
}),
);
const { decision, researchQuestions, cancelReason } =
await this.createResearchPlan();
// Stop workflow due to decision from LLM
if (decision === "cancel") {
ctx.sendEvent(
new UIEvent({
type: "ui_event",
data: { event: "analyze", state: "done" },
}),
);
return new StopEvent(
toStreamGenerator(
cancelReason ?? "Research cancelled without any reason.",
),
);
}
// Trigger research from generated questions
if (decision === "research") {
this.memory.put({
role: "assistant",
content:
"We need to find answers to the following questions:\n" +
researchQuestions.join("\n"),
});
researchQuestions.forEach(({ questionId: id, question }) => {
ctx.sendEvent(
new UIEvent({
type: "ui_event",
data: { event: "answer", state: "pending", id, question },
}),
);
});
return new ResearchEvent(researchQuestions);
}
// Resarch done, start writing report
this.memory.put({
role: "assistant",
content: "No more idea to analyze. We should report the answers.",
});
ctx.sendEvent(
new UIEvent({
type: "ui_event",
data: { event: "analyze", state: "done" },
}),
);
return new ReportEvent({});
};
handleResearch = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: ResearchEvent,
): Promise<PlanResearchEvent> => {
const researchQuestions = ev.data;
// Answer questions in parallel
const researchResults: ResearchResult[] = await Promise.all(
researchQuestions.map(async ({ questionId: id, question }) => {
ctx.sendEvent(
new UIEvent({
type: "ui_event",
data: { event: "answer", state: "inprogress", id, question },
}),
);
const answer = await this.answerQuestion(question);
ctx.sendEvent(
new UIEvent({
type: "ui_event",
data: { event: "answer", state: "done", id, question, answer },
}),
);
return { questionId: id, question, answer };
}),
);
// Save answers to memory
researchResults.forEach(({ question, answer }) => {
this.memory.put({
role: "assistant",
content: `<Question>${question}</Question>\n<Answer>${answer}</Answer>`,
});
});
this.memory.put({
role: "assistant",
content:
"Researched all the questions. Now, I need to analyze if it's ready to write a report or need to research more.",
});
this.totalQuestions += researchResults.length;
return new PlanResearchEvent({});
};
handleReport = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: ReportEvent,
): Promise<StopEvent> => {
const chatHistory = await this.memory.getAllMessages();
const messages = chatHistory.concat([
{
role: "system",
content: WRITE_REPORT_PROMPT,
},
{
role: "user",
content:
"Write a report addressing the user request based on the research provided the context",
},
]);
const stream = await this.llm.chat({ messages, stream: true });
return new StopEvent(toStreamGenerator(stream));
};
get llm() {
if (!this.#llm.supportToolCall) throw new Error("LLM is not a ToolCallLLM");
return this.#llm;
}
get retriever() {
if (!this.#index) throw new Error("Index is not initialized");
return this.#index.asRetriever({ similarityTopK: TOP_K });
}
get contextStr() {
return this.contextNodes
.map((node) => {
const nodeId = node.node.id_;
const nodeContent = node.node.getContent(MetadataMode.NONE);
return `<Citation id='${nodeId}'>\n${nodeContent}</Citation id='${nodeId}'>`;
})
.join("\n");
}
get enhancedPrompt() {
if (this.totalQuestions === 0) {
return "The student has no questions to research. Let start by asking some questions.";
}
if (this.totalQuestions > MAX_QUESTIONS) {
return `The student has researched ${this.totalQuestions} questions. Should cancel the research if the context is not enough to write a report.`;
}
return "";
}
async createResearchPlan() {
const chatHistory = await this.memory.getMessages();
const conversationContext = chatHistory
.map((message) => `${message.role}: ${message.content}`)
.join("\n");
const prompt = createPlanResearchPrompt.format({
context_str: this.contextStr,
conversation_context: conversationContext,
enhanced_prompt: this.enhancedPrompt,
user_request: this.userRequest,
});
const responseFormat = z.object({
decision: z.enum(["research", "write", "cancel"]),
researchQuestions: z.array(z.string()),
cancelReason: z.string().optional(),
});
const result = await this.llm.complete({ prompt, responseFormat });
const plan = JSON.parse(result.text) as z.infer<typeof responseFormat>;
return {
...plan,
researchQuestions: plan.researchQuestions.map((question) => ({
questionId: randomUUID(),
question,
})),
};
}
async answerQuestion(question: string) {
const prompt = researchPrompt.format({
context_str: this.contextStr,
question,
});
const result = await this.llm.complete({ prompt });
return result.text;
}
}
@@ -0,0 +1,396 @@
import { toAgentRunEvent, toSourceEvent } from "@llamaindex/server";
import {
callTools,
chatWithTools,
documentGenerator,
interpreter,
} from "@llamaindex/tools";
import {
AgentInputData,
AgentWorkflowContext,
BaseToolWithCall,
ChatMemoryBuffer,
ChatMessage,
ChatResponseChunk,
HandlerContext,
Metadata,
NodeWithScore,
Settings,
StartEvent,
StopEvent,
ToolCall,
ToolCallLLM,
Workflow,
WorkflowEvent,
} from "llamaindex";
import { getIndex } from "./data";
const TIMEOUT = 360 * 1000;
export async function workflowFactory(reqBody: any) {
const index = await getIndex(reqBody?.data);
const queryEngineTool = index.queryTool({
metadata: {
name: "query_document",
description: `This tool can retrieve information about Apple and Tesla financial data`,
},
includeSourceNodes: true,
});
if (!process.env.E2B_API_KEY) {
throw new Error("E2B_API_KEY is required to use the code interpreter tool");
}
const codeInterpreterTool = interpreter({
apiKey: process.env.E2B_API_KEY!,
});
const documentGeneratorTool = documentGenerator();
return new FinancialReportWorkflow({
queryEngineTool,
codeInterpreterTool,
documentGeneratorTool,
timeout: TIMEOUT,
});
}
// Create a custom event type
class InputEvent extends WorkflowEvent<{ input: ChatMessage[] }> {}
class ResearchEvent extends WorkflowEvent<{
toolCalls: ToolCall[];
}> {}
class AnalyzeEvent extends WorkflowEvent<{
input: ChatMessage | ToolCall[];
}> {}
class ReportGenerationEvent extends WorkflowEvent<{
toolCalls: ToolCall[];
}> {}
const DEFAULT_SYSTEM_PROMPT = `
You are a financial analyst who are given a set of tools to help you.
It's good to using appropriate tools for the user request and always use the information from the tools, don't make up anything yourself.
For the query engine tool, you should break down the user request into a list of queries and call the tool with the queries.
`;
class FinancialReportWorkflow extends Workflow<
AgentWorkflowContext,
AgentInputData,
string
> {
llm: ToolCallLLM;
memory: ChatMemoryBuffer;
queryEngineTool: BaseToolWithCall;
codeInterpreterTool: BaseToolWithCall;
documentGeneratorTool: BaseToolWithCall;
systemPrompt?: string;
constructor(options: {
queryEngineTool: BaseToolWithCall;
codeInterpreterTool: BaseToolWithCall;
documentGeneratorTool: BaseToolWithCall;
systemPrompt?: string;
verbose?: boolean;
timeout?: number;
}) {
super({
verbose: options?.verbose ?? false,
timeout: options?.timeout ?? 360,
});
this.llm = Settings.llm as ToolCallLLM;
if (!this.llm.supportToolCall) {
throw new Error("LLM is not a ToolCallLLM");
}
this.systemPrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
this.queryEngineTool = options.queryEngineTool;
this.codeInterpreterTool = options.codeInterpreterTool;
this.documentGeneratorTool = options.documentGeneratorTool;
this.memory = new ChatMemoryBuffer({ llm: this.llm, chatHistory: [] });
// Add steps
this.addStep(
{
inputs: [StartEvent<AgentInputData>],
outputs: [InputEvent],
},
this.prepareChatHistory,
);
this.addStep(
{
inputs: [InputEvent],
outputs: [
InputEvent,
ResearchEvent,
AnalyzeEvent,
ReportGenerationEvent,
StopEvent,
],
},
this.handleLLMInput,
);
this.addStep(
{
inputs: [ResearchEvent],
outputs: [AnalyzeEvent],
},
this.handleResearch,
);
this.addStep(
{
inputs: [AnalyzeEvent],
outputs: [InputEvent],
},
this.handleAnalyze,
);
this.addStep(
{
inputs: [ReportGenerationEvent],
outputs: [InputEvent],
},
this.handleReportGeneration,
);
}
prepareChatHistory = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: StartEvent<AgentInputData>,
): Promise<InputEvent> => {
const { userInput, chatHistory = [] } = ev.data;
if (!userInput) throw new Error("Invalid input");
this.memory.set(chatHistory);
if (this.systemPrompt) {
this.memory.put({ role: "system", content: this.systemPrompt });
}
this.memory.put({ role: "user", content: userInput });
const messages = await this.memory.getMessages();
return new InputEvent({ input: messages });
};
handleLLMInput = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: InputEvent,
): Promise<
| InputEvent
| ResearchEvent
| AnalyzeEvent
| ReportGenerationEvent
| StopEvent<AsyncGenerator<ChatResponseChunk, any, any> | undefined>
> => {
const chatHistory = ev.data.input;
const tools = [
this.codeInterpreterTool,
this.documentGeneratorTool,
this.queryEngineTool,
];
const toolCallResponse = await chatWithTools(this.llm, tools, chatHistory);
if (!toolCallResponse.hasToolCall()) {
return new StopEvent(toolCallResponse.responseGenerator);
}
if (toolCallResponse.hasMultipleTools()) {
this.memory.put({
role: "assistant",
content:
"Calling different tools is not allowed. Please only use multiple calls of the same tool.",
});
const chatHistory = await this.memory.getMessages();
return new InputEvent({ input: chatHistory });
}
// Put the LLM tool call message into the memory
// And trigger the next step according to the tool call
if (toolCallResponse.toolCallMessage) {
this.memory.put(toolCallResponse.toolCallMessage);
}
const toolName = toolCallResponse.getToolNames()[0];
switch (toolName) {
case this.codeInterpreterTool.metadata.name:
return new AnalyzeEvent({
input: toolCallResponse.toolCalls,
});
case this.documentGeneratorTool.metadata.name:
return new ReportGenerationEvent({
toolCalls: toolCallResponse.toolCalls,
});
default:
if (this.queryEngineTool.metadata.name === toolName) {
return new ResearchEvent({
toolCalls: toolCallResponse.toolCalls,
});
}
throw new Error(`Unknown tool: ${toolName}`);
}
};
handleResearch = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: ResearchEvent,
): Promise<AnalyzeEvent> => {
ctx.sendEvent(
toAgentRunEvent({
agent: "Researcher",
text: "Researching data",
type: "text",
}),
);
const { toolCalls } = ev.data;
const toolMsgs = await callTools({
tools: [this.queryEngineTool],
toolCalls,
writeEvent: (text, step) => {
ctx.sendEvent(
toAgentRunEvent({
agent: "Researcher",
text,
type: toolCalls.length > 1 ? "progress" : "text",
current: step,
total: toolCalls.length,
}),
);
},
});
for (const toolMsg of toolMsgs) {
this.memory.put(toolMsg);
}
const sourcesNodes: NodeWithScore<Metadata>[] = toolMsgs
.map((msg) => (msg.options as any)?.toolResult?.result?.sourceNodes)
.flat()
.filter(Boolean);
if (sourcesNodes.length > 0) {
ctx.sendEvent(toSourceEvent(sourcesNodes));
}
return new AnalyzeEvent({
input: {
role: "assistant",
content:
"I have finished researching the data, please analyze the data.",
},
});
};
/**
* Analyze a research result or a tool call for code interpreter from the LLM
*/
handleAnalyze = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: AnalyzeEvent,
): Promise<InputEvent> => {
ctx.sendEvent(
toAgentRunEvent({
agent: "Analyst",
text: "Analyzing data",
type: "text",
}),
);
// Request by workflow LLM, input is a list of tool calls
let toolCalls: ToolCall[] = [];
if (Array.isArray(ev.data.input)) {
toolCalls = ev.data.input;
} else {
// Requested by Researcher, input is a ChatMessage
// We start new LLM chat specifically for analyzing the data
const analysisPrompt = `
You are an expert in analyzing financial data.
You are given a set of financial data to analyze. Your task is to analyze the financial data and return a report.
Your response should include a detailed analysis of the financial data, including any trends, patterns, or insights that you find.
Construct the analysis in textual format; including tables would be great!
Don't need to synthesize the data, just analyze and provide your findings.
`;
// Clone the current chat history
// Add the analysis system prompt and the message from the researcher
const chatHistory = await this.memory.getMessages();
const newChatHistory = [
...chatHistory,
{ role: "system", content: analysisPrompt },
ev.data.input,
];
const toolCallResponse = await chatWithTools(
this.llm,
[this.codeInterpreterTool],
newChatHistory as ChatMessage[],
);
if (!toolCallResponse.hasToolCall()) {
this.memory.put(await toolCallResponse.asFullResponse());
const chatHistory = await this.memory.getMessages();
return new InputEvent({ input: chatHistory });
} else {
this.memory.put(toolCallResponse.toolCallMessage!);
toolCalls = toolCallResponse.toolCalls;
}
}
// Call the tools
const toolMsgs = await callTools({
tools: [this.codeInterpreterTool],
toolCalls,
writeEvent: (text, step) => {
ctx.sendEvent(
toAgentRunEvent({
agent: "Analyst",
text,
type: toolCalls.length > 1 ? "progress" : "text",
current: step,
total: toolCalls.length,
}),
);
},
});
for (const toolMsg of toolMsgs) {
this.memory.put(toolMsg);
}
const chatHistory = await this.memory.getMessages();
return new InputEvent({ input: chatHistory });
};
handleReportGeneration = async (
ctx: HandlerContext<AgentWorkflowContext>,
ev: ReportGenerationEvent,
): Promise<InputEvent> => {
const { toolCalls } = ev.data;
const toolMsgs = await callTools({
tools: [this.documentGeneratorTool],
toolCalls,
writeEvent: (text, step) => {
ctx.sendEvent(
toAgentRunEvent({
agent: "Reporter",
text,
type: toolCalls.length > 1 ? "progress" : "text",
current: step,
total: toolCalls.length,
}),
);
},
});
for (const toolMsg of toolMsgs) {
this.memory.put(toolMsg);
}
const chatHistory = await this.memory.getMessages();
return new InputEvent({ input: chatHistory });
};
}
@@ -1,12 +1,8 @@
import os
from llama_index.core import Settings
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
def init_settings():
if os.getenv("OPENAI_API_KEY") is None:
raise RuntimeError("OPENAI_API_KEY is missing in environment variables")
Settings.llm = OpenAI(model="gpt-4o-mini")
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
@@ -51,7 +51,7 @@ def generate_ui_for_workflow():
# and run the generate_ui_for_workflow function with the imported model.
# Make sure the output filename of the generated UI component matches the event type (here `ui_event`)
try:
from app.workflow import UIEventData # type: ignore
from app.workflow import UIEventData
except ImportError:
raise ImportError("Couldn't generate UI component for the current workflow.")
from llama_index.server.gen_ui import generate_event_component
@@ -17,10 +17,8 @@ def create_app():
ui_config=UIConfig(
component_dir=COMPONENT_DIR,
app_title="Chat App",
dev_mode=True, # Please disable this in production
),
logger=logger,
env="dev",
)
# You can also add custom FastAPI routes to app
app.add_api_route("/api/health", lambda: {"message": "OK"}, status_code=200)
@@ -12,7 +12,7 @@ dependencies = [
"pydantic<2.10",
"aiostream>=0.5.2,<0.6.0",
"llama-index-core>=0.12.28,<0.13.0",
"llama-index-server>=0.1.16,<0.2.0",
"llama-index-server>=0.1.15,<0.2.0",
]
[project.optional-dependencies]
@@ -1,6 +0,0 @@
{
"watch": ["src/**/*.ts"],
"exec": "nodemon --exec tsx src/index.ts",
"ext": "js ts",
"ignore": ["src/app/workflow_*.ts"]
}
@@ -5,22 +5,21 @@
"generate": "tsx src/generate.ts datasource",
"generate:datasource": "tsx src/generate.ts datasource",
"generate:ui": "tsx src/generate.ts ui",
"dev": "nodemon",
"dev": "tsx watch src/index.ts",
"start": "tsx src/index.ts"
},
"dependencies": {
"@llamaindex/openai": "~0.4.0",
"@llamaindex/server": "~0.2.1",
"@llamaindex/workflow": "~1.1.3",
"@llamaindex/tools": "~0.0.11",
"llamaindex": "~0.11.0",
"@llamaindex/openai": "0.2.0",
"@llamaindex/readers": "^2.0.0",
"@llamaindex/server": "0.1.5",
"@llamaindex/tools": "0.0.4",
"dotenv": "^16.4.7",
"zod": "^3.23.8"
"zod": "^3.23.8",
"llamaindex": "0.10.2"
},
"devDependencies": {
"@types/node": "^20.10.3",
"tsx": "^4.7.2",
"typescript": "^5.3.2",
"nodemon": "^3.1.10"
"typescript": "^5.3.2"
}
}
@@ -3,7 +3,7 @@ import { Settings } from "llamaindex";
export function initSettings() {
Settings.llm = new OpenAI({
model: "gpt-4.1",
model: "gpt-4o-mini",
});
Settings.embedModel = new OpenAIEmbedding({
model: "text-embedding-3-small",
@@ -1,10 +1,14 @@
import "dotenv/config";
import { SimpleDirectoryReader } from "@llamaindex/readers/directory";
import { storageContextFromDefaults, VectorStoreIndex } from "llamaindex";
import {
OpenAI,
storageContextFromDefaults,
VectorStoreIndex,
} from "llamaindex";
import { initSettings } from "./app/settings";
import fs from "fs";
import { generateEventComponent } from "@llamaindex/server";
import { OpenAI } from "@llamaindex/openai";
import { UIEventSchema } from "./app/workflow";
async function generateDatasource() {
console.log(`Generating storage context...`);
@@ -26,14 +30,6 @@ async function generateUi() {
// Also works well with Claude 3.5 Sonnet and Google Gemini 2.5 Pro
const llm = new OpenAI({ model: "gpt-4.1" });
const workflowModule = await import("./app/workflow");
const UIEventSchema = (workflowModule as any).UIEventSchema;
if (!UIEventSchema) {
throw new Error(
"To generate the UI, you must define a UIEventSchema in your workflow.",
);
}
// You can also generate for other workflow events
const generatedCode = await generateEventComponent(UIEventSchema, llm);
// Write the generated code to components/ui_event.ts
@@ -10,6 +10,5 @@ new LlamaIndexServer({
uiConfig: {
appTitle: "LlamaIndex App",
componentsDir: "components",
devMode: true,
},
}).start();
@@ -9,7 +9,6 @@ readme = "README.md"
requires-python = ">=3.11,<3.14"
dependencies = [
"llama-index>=0.12.1",
"llama-parse>=0.6.21,<0.7.0",
"fastapi[standard]>=0.109.1",
"uvicorn>=0.23.2",
"python-dotenv>=1.0.0",
+1 -5
View File
@@ -1,5 +1 @@
# server contains Nextjs frontend code (not compiled)
server/
# temp is the copy of next folder but without API folder, used to build frontend static files
temp/
server/
-23
View File
@@ -1,28 +1,5 @@
# @llamaindex/server
## 0.2.2
### Patch Changes
- 25fba43: refactor: migrate to Nextjs Route Handler
- 6f75d4a: fix: unsupported language in code gen workflow
## 0.2.1
### Patch Changes
- f072308: feat: add dev mode UI
## 0.2.0
### Minor Changes
- 0384268: Use the new workflow engine and deprecate the old one.
### Patch Changes
- d9f9e3c: chore: bump chat-ui to support code editor & document editor
## 0.1.7
### Patch Changes
+5 -128
View File
@@ -4,10 +4,10 @@ LlamaIndexServer is a Next.js-based application that allows you to quickly launc
## Features
- Add a sophisticated chatbot UI to your LlamaIndex workflow
- Edit code and document artifacts in an OpenAI Canvas-style UI
- Extendable UI components for events and headers
- Serving a workflow as a chatbot
- Built on Next.js for high performance and easy API development
- Optional built-in chat UI with extendable UI components
- Prebuilt development code
## Installation
@@ -21,11 +21,9 @@ Create an `index.ts` file and add the following code:
```ts
import { LlamaIndexServer } from "@llamaindex/server";
import { openai } from "@llamaindex/openai";
import { agent } from "@llamaindex/workflow";
import { wiki } from "@llamaindex/tools"; // or any other tool
const createWorkflow = () => agent({ tools: [wiki()], llm: openai("gpt-4o") });
const createWorkflow = () => agent({ tools: [wiki()] });
new LlamaIndexServer({
workflow: createWorkflow,
@@ -36,8 +34,6 @@ new LlamaIndexServer({
}).start();
```
The `createWorkflow` function is a factory function that creates an [Agent Workflow](https://ts.llamaindex.ai/docs/llamaindex/modules/agents/agent_workflow) with a tool that retrieves information from Wikipedia in this case. For more details, read about the [Workflow factory contract](#workflow-factory-contract).
## Running the Server
In the same directory as `index.ts`, run the following command to start the server:
@@ -58,75 +54,16 @@ curl -X POST "http://localhost:3000/api/chat" -H "Content-Type: application/json
The `LlamaIndexServer` accepts the following configuration options:
- `workflow`: A callable function that creates a workflow instance for each request. See [Workflow factory contract](#workflow-factory-contract) for more details.
- `workflow`: A callable function that creates a workflow instance for each request
- `uiConfig`: An object to configure the chat UI containing the following properties:
- `appTitle`: The title of the application (default: `"LlamaIndex App"`)
- `starterQuestions`: List of starter questions for the chat UI (default: `[]`)
- `componentsDir`: The directory for custom UI components rendering events emitted by the workflow. The default is undefined, which does not render custom UI components.
- `llamaCloudIndexSelector`: Whether to show the LlamaCloud index selector in the chat UI (requires `LLAMA_CLOUD_API_KEY` to be set in the environment variables) (default: `false`)
- `dev_mode`: When enabled, you can update workflow code in the UI and see the changes immediately. It's currently in beta and only supports updating workflow code at `app/src/workflow.ts`. Please start server in dev mode (`npm run dev`) to use see this reload feature enabled.
LlamaIndexServer accepts all the configuration options from Nextjs Custom Server such as `port`, `hostname`, `dev`, etc.
See all Nextjs Custom Server options [here](https://nextjs.org/docs/app/building-your-application/configuring/custom-server).
## Workflow factory contract
The `workflow` provided will be called for each chat request to initialize a new workflow instance. The contract of the generated workflow must be the same as for the [Agent Workflow](https://ts.llamaindex.ai/docs/llamaindex/modules/agents/agent_workflow).
This means that the workflow must handle a `startAgentEvent` event, which is the entry point of the workflow and contains the following information in it's `data` property:
```typescript
{
userInput: MessageContent;
chatHistory?: ChatMessage[] | undefined;
};
```
The `userInput` is the latest user message and the `chatHistory` is the list of messages exchanged between the user and the workflow so far.
Furthermore, the workflow must stop with a `stopAgentEvent` event to mark the end of the workflow. In between, the workflow can emit [UI events](##AI-generated-UI-Components) to render custom UI components and [Artifact events](##Sending-Artifacts-to-the-UI) to send structured data like generated documents or code snippets to the UI.
```ts
import {
createStatefulMiddleware,
createWorkflow,
startAgentEvent,
} from "@llamaindex/workflow";
import { ChatMemoryBuffer, type ChatMessage, Settings } from "llamaindex";
import { openai } from "@llamaindex/openai";
import { wiki } from "@llamaindex/tools";
Settings.llm = openai("gpt-4o");
export const workflowFactory = async () => {
const workflow = createWorkflow();
workflow.handle([startAgentEvent], async ({ data }) => {
const { state, sendEvent } = getContext();
const messages = data.chatHistory;
const toolCallResponse = await chatWithTools(
Settings.llm,
[wiki()],
messages,
);
// using result from tool call and use `sendEvent` to emit the next event...
});
// define more workflow handling logic here...
// Finally stop with a `stopAgentEvent` event to mark the end of the workflow.
// return stopAgentEvent.with({
// result: "This is the end!",
// });
return workflow;
};
```
To generate sophisticated examples of workflows, you best use the [create-llama](https://github.com/run-llama/create-llama) project.
## AI-generated UI Components
The LlamaIndex server provides support for rendering workflow events using custom UI components, allowing you to extend and customize the chat interface.
@@ -200,66 +137,6 @@ new LlamaIndexServer({
}).start();
```
## Sending Artifacts to the UI
In addition to UI events for custom components, LlamaIndex Server supports a special `ArtifactEvent` to send structured data like generated documents or code snippets to the UI. These artifacts are displayed in a dedicated "Canvas" panel in the chat interface.
### Artifact Event Structure
To send an artifact, your workflow needs to emit an event with `type: "artifact"`. The `data` payload of this event should include:
- `type`: A string indicating the type of artifact (e.g., `"document"`, `"code"`).
- `created_at`: A timestamp (e.g., `Date.now()`) indicating when the artifact was created.
- `data`: An object containing the specific details of the artifact. The structure of this object depends on the artifact `type`.
### Defining and Sending an ArtifactEvent
First, define your artifact event using `workflowEvent` from `@llamaindex/workflow`:
```typescript
import { workflowEvent } from "@llamaindex/workflow";
// Example for a document artifact
const artifactEvent = workflowEvent<{
type: "artifact"; // Must be "artifact"
data: {
type: "document"; // Custom type for your artifact (e.g., "document", "code")
created_at: number;
data: {
// Specific data for the document artifact type
title: string;
content: string;
type: "markdown" | "html"; // document format
};
};
}>();
```
Then, within your workflow logic, use `sendEvent` (obtained from `getContext()`) to emit the event:
```typescript
// Assuming 'sendEvent' is available in your workflow handler
// and 'documentDetails' contains the content for the artifact.
sendEvent(
artifactEvent.with({
type: "artifact", // This top-level type must be "artifact"
data: {
type: "document", // This is your specific artifact type
created_at: Date.now(),
data: {
title: "My Generated Document",
content: "# Hello World
This is a markdown document.",
type: "markdown",
},
},
}),
);
```
This will send the artifact to the LlamaIndex Server UI, where it will be rendered in the [ChatCanvasPanel](/packages/server/next/app/components/ui/chat/canvas/panel.tsx) by a renderer depending on the artifact type. For type `document` this is using the [DocumentArtifactViewer](https://github.com/run-llama/chat-ui/blob/bacb75fc6edceacf742fba18632404a2483b5a81/packages/chat-ui/src/chat/canvas/artifacts/document.tsx#L17).
## Default Endpoints and Features
### Chat Endpoint
-12
View File
@@ -1,12 +0,0 @@
# LlamaIndex Server Examples
This directory contains examples of how to use the LlamaIndex Server.
## Running the examples
```bash
export OPENAI_API_KEY=your_openai_api_key
pnpm run dev
```
## Open browser at http://localhost:3000
@@ -1,43 +0,0 @@
import { LlamaIndexServer } from "@llamaindex/server";
import { agent } from "@llamaindex/workflow";
import {
Document,
OpenAI,
OpenAIEmbedding,
Settings,
VectorStoreIndex,
} from "llamaindex";
Settings.llm = new OpenAI({
model: "gpt-4o-mini",
});
Settings.embedModel = new OpenAIEmbedding({
model: "text-embedding-3-small",
});
export const workflowFactory = async () => {
const index = await VectorStoreIndex.fromDocuments([
new Document({ text: "The dog is brown" }),
new Document({ text: "The dog is yellow" }),
]);
const queryEngineTool = index.queryTool({
metadata: {
name: "query_document",
description: `This tool can retrieve information in documents`,
},
includeSourceNodes: true,
});
return agent({ tools: [queryEngineTool] });
};
new LlamaIndexServer({
workflow: workflowFactory,
uiConfig: {
appTitle: "LlamaIndex App",
starterQuestions: ["What is the color of the dog?"],
},
port: 3000,
}).start();
@@ -1,21 +0,0 @@
This example shows how to use the dev mode of the server.
First, we need to set `devMode` to `true` in the `uiConfig` of the server.
```ts
new LlamaIndexServer({
workflow: workflowFactory,
uiConfig: {
appTitle: "Calculator",
devMode: true,
},
port: 3000,
}).start();
```
Export OpenAI API key and start the server in dev mode.
```bash
export OPENAI_API_KEY=<your-openai-api-key>
npx nodemon --exec tsx index.ts --ignore src/app/workflow_*.ts
```
-15
View File
@@ -1,15 +0,0 @@
import { LlamaIndexServer } from "@llamaindex/server";
import { workflowFactory } from "./src/app/workflow";
new LlamaIndexServer({
workflow: workflowFactory,
uiConfig: {
appTitle: "Calculator",
devMode: true,
starterQuestions: [
"What is the weather in Tokyo?",
"What is the weather in New York?",
],
},
port: 3000,
}).start();
@@ -1,16 +0,0 @@
import { agent } from "@llamaindex/workflow";
import { tool } from "llamaindex";
import { z } from "zod";
export const workflowFactory = async () => {
return agent({
tools: [
tool({
name: "weather",
description: "Get the weather in a specific city",
parameters: z.object({ city: z.string() }),
execute: ({ city }) => `The weather in ${city} is sunny`,
}),
],
});
};
-25
View File
@@ -1,25 +0,0 @@
{
"name": "llamaindex-server-examples",
"version": "0.0.1",
"private": true,
"scripts": {
"typecheck": "tsc --noEmit",
"dev": "nodemon --exec tsx simple-workflow/calculator.ts"
},
"dependencies": {
"@llamaindex/openai": "^0.2.0",
"@llamaindex/readers": "^3.0.0",
"@llamaindex/server": "workspace:*",
"@llamaindex/tools": "0.0.4",
"@llamaindex/workflow": "1.1.0",
"dotenv": "^16.4.7",
"llamaindex": "0.10.2",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20.10.3",
"nodemon": "^3.1.10",
"tsx": "^4.7.2",
"typescript": "^5.3.2"
}
}
@@ -1,24 +0,0 @@
import { LlamaIndexServer } from "@llamaindex/server";
import { agent } from "@llamaindex/workflow";
import { tool } from "llamaindex";
import { z } from "zod";
const calculatorAgent = agent({
tools: [
tool({
name: "add",
description: "Adds two numbers",
parameters: z.object({ x: z.number(), y: z.number() }),
execute: ({ x, y }) => x + y,
}),
],
});
new LlamaIndexServer({
workflow: () => calculatorAgent,
uiConfig: {
appTitle: "Calculator",
starterQuestions: ["1 + 1", "2 + 2"],
},
port: 3000,
}).start();
-14
View File
@@ -1,14 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["**/*"],
"exclude": ["node_modules", "dist"]
}
@@ -1,32 +0,0 @@
import { getEnv } from "@llamaindex/env";
import { LLamaCloudFileService } from "llamaindex";
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest): Promise<NextResponse> {
if (!getEnv("LLAMA_CLOUD_API_KEY")) {
return NextResponse.json(
{
error: "env variable LLAMA_CLOUD_API_KEY is required to use LlamaCloud",
},
{ status: 500 },
);
}
try {
const config = {
projects: await LLamaCloudFileService.getAllProjectsWithPipelines(),
pipeline: {
pipeline: getEnv("LLAMA_CLOUD_INDEX_NAME"),
project: getEnv("LLAMA_CLOUD_PROJECT_NAME"),
},
};
return NextResponse.json(config, { status: 200 });
} catch (error) {
return NextResponse.json(
{
error: "Failed to fetch LlamaCloud configuration",
},
{ status: 500 },
);
}
}
@@ -1,77 +0,0 @@
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 { toDataStream } from "./utils/stream";
import { sendSuggestedQuestionsEvent } from "./utils/suggestion";
import { runWorkflow } from "./utils/workflow";
// import workflow factory from local file
import { workflowFactory } from "../../../../app/workflow";
export async function POST(req: NextRequest) {
try {
const reqBody = await req.json();
const { messages } = reqBody as { messages: Message[] };
const chatHistory = messages.map((message) => ({
role: message.role as MessageType,
content: message.content,
}));
const lastMessage = messages[messages.length - 1];
if (lastMessage?.role !== "user") {
return NextResponse.json(
{
detail: "Messages cannot be empty and last message must be from user",
},
{ 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 dataStream = toDataStream(workflowEventStream, {
callbacks: {
onFinal: async (completion, dataStreamWriter) => {
chatHistory.push({
role: "assistant" as MessageType,
content: completion,
});
await sendSuggestedQuestionsEvent(dataStreamWriter, chatHistory);
},
},
});
return new Response(dataStream, {
status: 200,
headers: {
"Content-Type": "text/plain; charset=utf-8",
"X-Vercel-AI-Data-Stream": "v1",
},
});
} catch (error) {
console.error("Chat handler error:", error);
return NextResponse.json(
{
detail: (error as Error).message || "Internal server error",
},
{ status: 500 },
);
}
}
@@ -1,96 +0,0 @@
import { exec } from "child_process";
import fs from "fs";
import { NextRequest, NextResponse } from "next/server";
import path from "path";
import { promisify } from "util";
const DEFAULT_WORKFLOW_FILE_PATH = "src/app/workflow.ts"; // TODO: we can make it as a parameter in server later
export async function GET(request: NextRequest) {
const filePath = DEFAULT_WORKFLOW_FILE_PATH;
const fileExists = await promisify(fs.exists)(DEFAULT_WORKFLOW_FILE_PATH);
if (!fileExists) {
return NextResponse.json(
{
detail: `Dev mode is currently in beta. It only supports updating workflow file at ${filePath}`,
},
{ status: 404 },
);
}
const content = await promisify(fs.readFile)(filePath, "utf-8");
const last_modified = fs.statSync(filePath).mtime.getTime();
return NextResponse.json(
{ content, file_path: filePath, last_modified },
{ status: 200 },
);
}
export async function PUT(request: NextRequest) {
const filePath = DEFAULT_WORKFLOW_FILE_PATH;
const { content } = await request.json();
const fileExists = await promisify(fs.exists)(filePath);
if (!fileExists) {
return NextResponse.json(
{
detail: `Dev mode is currently in beta. It only supports updating workflow file at ${DEFAULT_WORKFLOW_FILE_PATH}`,
},
{ status: 404 },
);
}
try {
const resolvedFilePath = path.resolve(DEFAULT_WORKFLOW_FILE_PATH);
const result = await validateTypeScriptFile(resolvedFilePath, content);
if (!result.isValid) {
return NextResponse.json(
{
detail: result.errors.join("\n"),
},
{ status: 400 },
);
}
await promisify(fs.writeFile)(filePath, content);
return NextResponse.json({ content }, { status: 200 });
} catch (error) {
console.error("Error updating workflow file:", error);
return NextResponse.json(
{ error: "Failed to update workflow file" },
{ status: 500 },
);
}
}
// use typescript package to validate the file syntax and imports
async function validateTypeScriptFile(filePath: string, content: string) {
// Update workflow file directly will cause the server restart immediately.
// So we create a temporary file with the same content in the same directory as the workflow file
// This file will be used to validate the file syntax and imports. It will be deleted after validation.
const tempFilePath = path.join(
path.dirname(filePath),
`workflow_${Date.now()}.ts`,
);
fs.writeFileSync(tempFilePath, content);
const errors = [];
try {
const tscCommand = `npx tsc ${tempFilePath} --noEmit --skipLibCheck true`;
await promisify(exec)(tscCommand);
} catch (error) {
const errorMessage = (error as { stdout: string })?.stdout;
errors.push(errorMessage);
} finally {
// Clean up temporary file
if (fs.existsSync(tempFilePath)) fs.unlinkSync(tempFilePath);
}
return {
isValid: errors.length === 0,
errors: errors,
};
}
@@ -1,24 +0,0 @@
import fs from "fs";
import { NextRequest, NextResponse } from "next/server";
import { promisify } from "util";
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ slug: string[] }> },
) {
const filePath = (await params).slug.join("/");
if (!filePath.startsWith("output") && !filePath.startsWith("data")) {
return NextResponse.json({ error: "No permission" }, { status: 400 });
}
const decodedFilePath = decodeURIComponent(filePath);
const fileExists = await promisify(fs.exists)(decodedFilePath);
if (fileExists) {
const fileBuffer = await promisify(fs.readFile)(decodedFilePath);
return new NextResponse(fileBuffer);
} else {
return NextResponse.json({ error: "File not found" }, { status: 404 });
}
}
@@ -13,7 +13,6 @@ import CustomChatMessages from "./chat-messages";
import { DynamicEventsErrors } from "./custom/events/dynamic-events-errors";
import { fetchComponentDefinitions } from "./custom/events/loader";
import { ComponentDef } from "./custom/events/types";
import { DevModePanel } from "./dev-mode-panel";
export default function ChatSection() {
const handler = useChat({
@@ -36,13 +35,12 @@ export default function ChatSection() {
<ChatHeader />
<ChatUI
handler={handler}
className="relative flex min-h-0 flex-1 flex-row justify-center gap-4 px-4 py-0"
className="flex min-h-0 flex-1 flex-row justify-center gap-4 px-4 py-0"
>
<ResizablePanelGroup direction="horizontal">
<ChatSectionPanel />
<ChatCanvasPanel />
</ResizablePanelGroup>
<DevModePanel />
</ChatUI>
</div>
<ChatInjection />
@@ -1,273 +0,0 @@
"use client";
import {
CodeEditor,
fileExtensionToEditorLang,
} from "@llamaindex/chat-ui/widgets";
import { AlertCircle, Loader2 } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { Button } from "../button";
import { getConfig } from "../lib/utils";
const API_PATH = "/api/dev/files/workflow";
const POLLING_TIMEOUT = 30_000; // 30 seconds
type WorkflowFile = {
last_modified: number;
file_path: string;
content: string;
};
export function DevModePanel() {
const devModeEnabled = getConfig("DEV_MODE");
if (!devModeEnabled) return null;
return <DevModePanelComp />;
}
function DevModePanelComp() {
const [devModeOpen, setDevModeOpen] = useState(false);
const [isFetching, setIsFetching] = useState(false);
const [fetchingError, setFetchingError] = useState<string | null>();
const [workflowFile, setWorkflowFile] = useState<WorkflowFile | null>(null);
const [updatedCode, setUpdatedCode] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
const [isPolling, setIsPolling] = useState(false);
const [pollingError, setPollingError] = useState<string | null>(null);
async function fetchWorkflowCode() {
try {
setIsFetching(true);
const response = await fetch(API_PATH);
const data = await response.json();
if (!response.ok) {
throw new Error(data?.detail ?? "Unknown error");
}
setWorkflowFile(data);
setFetchingError(null);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
setFetchingError(errorMessage);
console.warn("Error fetching workflow code:", error);
} finally {
setIsFetching(false);
}
}
async function restartingWorkflow() {
if (!workflowFile) return;
const initialLastModified = workflowFile.last_modified;
setIsPolling(true);
setPollingError(null);
const pollStartTime = Date.now();
// interval refetching the updated workflow code
const poll = async () => {
if (Date.now() - pollStartTime > POLLING_TIMEOUT) {
setPollingError(
`Server not responding after ${POLLING_TIMEOUT / 1000} seconds.`,
);
return;
}
try {
const pollResponse = await fetch(API_PATH);
const pollData = (await pollResponse.json()) as WorkflowFile;
if (pollData.last_modified !== initialLastModified) {
setWorkflowFile(pollData);
setUpdatedCode(pollData.content);
setIsPolling(false);
setPollingError(null);
setDevModeOpen(false);
} else {
setTimeout(poll, 2000);
}
} catch (error) {
console.info("Polling error", error);
setTimeout(poll, 2000);
}
};
setTimeout(poll, 2000);
}
const handleResetCode = () => {
setUpdatedCode(workflowFile?.content ?? null);
setSaveError(null);
};
const handleSaveCode = async () => {
if (!workflowFile) return;
try {
setIsSaving(true);
const response = await fetch(API_PATH, {
method: "PUT",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
content: updatedCode,
file_path: workflowFile.file_path,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data?.detail ?? "Unknown error");
}
setSaveError(null);
await restartingWorkflow();
} catch (error) {
console.warn("Error saving workflow code:", error);
setSaveError(
error instanceof Error
? error.message
: "Unknown error happened when saving workflow code",
);
} finally {
setIsSaving(false);
}
};
useEffect(() => {
if (devModeOpen) {
fetchWorkflowCode();
}
}, [devModeOpen]);
const codeEditorLanguage = useMemo(() => {
if (!workflowFile?.file_path) return undefined;
return fileExtensionToEditorLang(
workflowFile.file_path.split(".").pop() ?? "",
);
}, [workflowFile]);
return (
<>
<Button
onClick={() => setDevModeOpen(!devModeOpen)}
className="fixed right-2 top-1/2 origin-right -translate-y-1/2 rotate-90 transform rounded-l-md shadow-md transition-transform hover:-translate-x-1"
>
Dev Mode
</Button>
{isPolling && (
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-black/50 backdrop-blur-sm">
{!pollingError && (
<>
<Loader2 className="mb-4 h-16 w-16 animate-spin text-white" />
<p className="text-lg font-semibold text-white">
Applying changes and restarting server...
</p>
<p className="mt-2 text-sm text-slate-300">
Please wait for a while then you can start chatting with the
updated workflow.
</p>
</>
)}
{pollingError && (
<div className="bg-destructive/20 text-destructive-foreground mt-4 max-w-md rounded-md p-4 text-center">
<div className="mb-2 flex items-center justify-center gap-2">
<AlertCircle className="shrink-0" size={16} />
<h6 className="text-sm font-medium">Server Starting Error</h6>
</div>
<p className="text-sm">{pollingError}</p>
<p className="text-sm">
Please reload the page and check server logs.
</p>
</div>
)}
</div>
)}
<div
className={`border-border fixed right-0 top-0 z-10 h-full w-full border-l shadow-xl transition-all duration-300 ease-in-out ${
devModeOpen ? "translate-x-0 bg-black/50" : "translate-x-full"
}`}
onClick={() => setDevModeOpen(false)}
>
<div
className={`bg-background ml-auto flex h-full w-[800px] flex-col p-4`}
onClick={(e) => e.stopPropagation()}
>
<div className="mb-4 flex items-center justify-between">
<div>
<h2 className="text-xl font-bold">Workflow Editor</h2>
<p className="text-muted-foreground text-sm">
{isFetching ? (
"Loading..."
) : workflowFile ? (
<>
Edit the code of <b>{workflowFile.file_path}</b> and save to
apply changes to your workflow.
</>
) : (
""
)}
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => setDevModeOpen(false)}
>
Close
</Button>
</div>
<div className="flex-1 overflow-auto">
{fetchingError ? (
<div className="bg-destructive/10 text-destructive/70 mb-4 flex items-center gap-2 rounded-md p-4">
<AlertCircle className="shrink-0" size={16} />
<p className="text-sm font-medium">{fetchingError}</p>
</div>
) : (
<CodeEditor
code={updatedCode ?? workflowFile?.content ?? ""}
onChange={setUpdatedCode}
language={codeEditorLanguage}
/>
)}
</div>
<div className="mt-4 flex flex-col">
{saveError && (
<div className="bg-destructive/10 text-destructive/70 mb-4 rounded-md p-4">
<div className="mb-2 flex items-center gap-2">
<AlertCircle className="shrink-0" size={16} />
<h6 className="text-sm font-medium">Error Saving Code</h6>
</div>
<p className="whitespace-pre-wrap text-sm">{saveError}</p>
</div>
)}
<div className="flex justify-end gap-2">
<Button
variant="outline"
className="mr-2"
onClick={handleResetCode}
>
Reset Code
</Button>
<Button
onClick={handleSaveCode}
disabled={isSaving || !updatedCode || !workflowFile}
>
Save & Restart Server
{isSaving && <Loader2 className="ml-2 h-4 w-4 animate-spin" />}
</Button>
</div>
</div>
</div>
</div>
</>
);
}
+1 -1
View File
@@ -20,7 +20,7 @@
"paths": {
"@/*": ["./*"]
},
"target": "ES2022"
"target": "ES2017"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
+13 -17
View File
@@ -1,7 +1,7 @@
{
"name": "@llamaindex/server",
"description": "LlamaIndex Server",
"version": "0.2.2",
"version": "0.1.7",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
@@ -27,25 +27,21 @@
"directory": "packages/server"
},
"scripts": {
"dev": "bunchee --watch",
"clean": "rm -rf ./dist ./server next/.next next/out ./temp",
"clean": "rm -rf ./dist ./server next/.next next/out",
"prebuild": "pnpm clean",
"build": "bunchee",
"postbuild": "pnpm prepare:ts-server && pnpm prepare:py-static",
"prepare:ts-server": "pnpm copy:next-src && pnpm build:css && pnpm build:api",
"prepare:py-static": "pnpm prepare:static && pnpm build:static && pnpm copy:static",
"copy:next-src": "cp -r ./next ./server",
"build:css": "postcss server/app/globals.css -o server/app/globals.css && rm -rf ./server/postcss.config.js",
"build:api": "rm -rf ./server/app/api && tsc --skipLibCheck --project tsconfig.api.json",
"prepare:static": "cp -r ./next ./temp && rm -rf ./temp/app/api && mv ./temp/next-build.config.ts ./temp/next.config.ts",
"build:static": "cd ./temp && next build",
"copy:static": "cp -r ./temp/out ./dist/static && rm -rf ./temp"
"postbuild": "pnpm copy:next-src && pnpm build:static && pnpm copy:static",
"copy:next-src": "cp -r ./next ./server && pnpm build:css && rm -rf ./server/postcss.config.js",
"build:css": "postcss server/app/globals.css -o server/app/globals.css",
"build:static": "cd ./next && next build",
"copy:static": "cp -r ./next/out ./dist/static",
"dev": "bunchee --watch"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/babel__standalone": "^7.1.9",
"@types/babel__traverse": "^7.20.7",
"llamaindex": "~0.11.0",
"llamaindex": "0.10.2",
"postcss": "^8.5.3",
"postcss-cli": "^11.0.1",
"tailwindcss": "^4",
@@ -59,7 +55,9 @@
"@babel/traverse": "^7.27.0",
"@babel/types": "^7.27.0",
"@hookform/resolvers": "^5.0.1",
"@llamaindex/chat-ui": "0.4.4",
"@llama-flow/core": "^0.3.4",
"@llamaindex/chat-ui": "0.4.1",
"@llamaindex/env": "0.1.29",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.7",
"@radix-ui/react-aspect-ratio": "^1.1.3",
@@ -107,9 +105,7 @@
"vaul": "^1.1.2"
},
"peerDependencies": {
"@llamaindex/env": "~0.1.30",
"@llamaindex/workflow": "~1.1.3",
"llamaindex": "~0.11.0",
"llamaindex": "0.10.2",
"zod": "^3.24.2",
"zod-to-json-schema": "^3.23.3"
},
+29 -25
View File
@@ -1,7 +1,11 @@
import { randomUUID } from "@llamaindex/env";
import { workflowEvent } from "@llamaindex/workflow";
import type { Message } from "ai";
import { MetadataMode, type Metadata, type NodeWithScore } from "llamaindex";
import {
MetadataMode,
WorkflowEvent,
type Metadata,
type NodeWithScore,
} from "llamaindex";
import { z } from "zod";
// Events that appended to stream as annotations
@@ -16,23 +20,25 @@ export type SourceEventNode = {
};
export type SourceEventData = {
type: "sources";
data: {
nodes: SourceEventNode[];
};
nodes: SourceEventNode[];
};
export const sourceEvent = workflowEvent<SourceEventData>();
export class SourceEvent extends WorkflowEvent<{
type: "sources";
data: SourceEventData;
}> {}
export type AgentRunEventData = {
type: "agent";
data: {
agent: string;
text: string;
type: "text" | "progress";
data?: { id: string; total: number; current: number } | undefined;
};
agent: string;
text: string;
type: "text" | "progress";
data?: { id: string; total: number; current: number } | undefined;
};
export const agentRunEvent = workflowEvent<AgentRunEventData>();
export class AgentRunEvent extends WorkflowEvent<{
type: "agent";
data: AgentRunEventData;
}> {}
export function toSourceEventNode(node: NodeWithScore<Metadata>) {
const { file_name, pipeline_id } = node.node.metadata;
@@ -56,9 +62,9 @@ export function toSourceEvent(sourceNodes: NodeWithScore<Metadata>[] = []) {
const nodes: SourceEventNode[] = sourceNodes.map((node) =>
toSourceEventNode(node),
);
return sourceEvent.with({
data: { nodes },
return new SourceEvent({
type: "sources",
data: { nodes },
});
}
@@ -69,7 +75,8 @@ export function toAgentRunEvent(input: {
current?: number;
total?: number;
}) {
return agentRunEvent.with({
return new AgentRunEvent({
type: "agent",
data: {
...input,
data:
@@ -81,7 +88,6 @@ export function toAgentRunEvent(input: {
}
: undefined,
},
type: "agent",
});
}
@@ -113,10 +119,10 @@ export type DocumentArtifact = Artifact<DocumentArtifactData> & {
type: "document";
};
export const artifactEvent = workflowEvent<{
export class ArtifactEvent extends WorkflowEvent<{
type: "artifact";
data: Artifact;
}>();
}> {}
export const codeArtifactSchema = z.object({
type: z.literal("code"),
@@ -155,12 +161,10 @@ export function extractAllArtifacts(messages: Message[]): Artifact[] {
const artifacts =
message.annotations
?.filter(
(
annotation,
): annotation is z.infer<typeof artifactAnnotationSchema> =>
(annotation) =>
artifactAnnotationSchema.safeParse(annotation).success,
)
.map((annotation) => annotation.data as Artifact) ?? [];
.map((artifact) => artifact as Artifact) ?? [];
allArtifacts.push(...artifacts);
}
+11 -33
View File
@@ -1,15 +1,12 @@
import type { AgentInputData } from "@llamaindex/workflow";
import { type Message } from "ai";
import { IncomingMessage, ServerResponse } from "http";
import type { MessageType } from "llamaindex";
import { type WorkflowFactory } from "../types";
import type { ChatMessage } from "llamaindex";
import type { WorkflowFactory } from "../types";
import {
parseRequestBody,
pipeStreamToResponse,
sendJSONResponse,
} from "../utils/request";
import { toDataStream } from "../utils/stream";
import { sendSuggestedQuestionsEvent } from "../utils/suggestion";
import { runWorkflow } from "../utils/workflow";
export const handleChat = async (
@@ -20,10 +17,6 @@ export const handleChat = async (
try {
const body = await parseRequestBody(req);
const { messages } = body as { messages: Message[] };
const chatHistory = messages.map((message) => ({
role: message.role as MessageType,
content: message.content,
}));
const lastMessage = messages[messages.length - 1];
if (lastMessage?.role !== "user") {
@@ -31,35 +24,20 @@ export const handleChat = async (
error: "Messages cannot be empty and last message must be from user",
});
}
const workflowInput: AgentInputData = {
userInput: lastMessage.content,
chatHistory,
};
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 dataStream = toDataStream(workflowEventStream, {
callbacks: {
onFinal: async (completion, dataStreamWriter) => {
chatHistory.push({
role: "assistant" as MessageType,
content: completion,
});
await sendSuggestedQuestionsEvent(dataStreamWriter, chatHistory);
},
},
const stream = await runWorkflow(workflow, {
userInput: lastMessage.content,
chatHistory: messages.slice(0, -1).map((message) => ({
content: message.content,
role: message.role as ChatMessage["role"],
})),
});
pipeStreamToResponse(res, dataStream);
pipeStreamToResponse(res, stream);
} catch (error) {
console.error("Chat handler error:", error);
console.error("Chat error:", error);
return sendJSONResponse(res, 500, {
detail: (error as Error).message || "Internal server error",
});
+30
View File
@@ -0,0 +1,30 @@
import { getEnv } from "@llamaindex/env";
import type { IncomingMessage, ServerResponse } from "http";
import { LLamaCloudFileService } from "llamaindex";
import { sendJSONResponse } from "../utils/request";
export const getLlamaCloudConfig = async (
req: IncomingMessage,
res: ServerResponse,
) => {
if (!getEnv("LLAMA_CLOUD_API_KEY")) {
return sendJSONResponse(res, 500, {
error: "env variable LLAMA_CLOUD_API_KEY is required to use LlamaCloud",
});
}
try {
const config = {
projects: await LLamaCloudFileService.getAllProjectsWithPipelines(),
pipeline: {
pipeline: getEnv("LLAMA_CLOUD_INDEX_NAME"),
project: getEnv("LLAMA_CLOUD_PROJECT_NAME"),
},
};
return sendJSONResponse(res, 200, config);
} catch (error) {
return sendJSONResponse(res, 500, {
error: "Failed to fetch LlamaCloud configuration",
});
}
};
@@ -1,19 +1,20 @@
import fs from "fs";
import { NextRequest, NextResponse } from "next/server";
import type { IncomingMessage, ServerResponse } from "http";
import path from "path";
import { promisify } from "util";
import { sendJSONResponse } from "../utils/request";
export async function GET(request: NextRequest) {
const params = request.nextUrl.searchParams;
const componentsDir = params.get("componentsDir") || "components";
export const getComponents = async (
req: IncomingMessage,
res: ServerResponse,
componentsDir: string,
) => {
try {
const exists = await promisify(fs.exists)(componentsDir);
if (!exists) {
return NextResponse.json(
{ error: "Components directory not found" },
{ status: 404 },
);
return sendJSONResponse(res, 404, {
error: "Components directory not found",
});
}
const files = await promisify(fs.readdir)(componentsDir);
@@ -39,15 +40,12 @@ export async function GET(request: NextRequest) {
}),
);
return NextResponse.json(components, { status: 200 });
sendJSONResponse(res, 200, components);
} catch (error) {
console.error("Error reading components:", error);
return NextResponse.json(
{ error: "Failed to read components" },
{ status: 500 },
);
sendJSONResponse(res, 500, { error: "Failed to read components" });
}
}
};
function filterDuplicateComponents(files: string[]) {
const compMap = new Map<string, string>();
+23
View File
@@ -0,0 +1,23 @@
import fs from "fs";
import type { IncomingMessage, ServerResponse } from "http";
import { promisify } from "util";
import { sendJSONResponse } from "../utils/request";
export const handleServeFiles = async (
req: IncomingMessage,
res: ServerResponse,
pathname: string,
) => {
const filePath = pathname.substring("/api/files/".length);
if (!filePath.startsWith("output") && !filePath.startsWith("data")) {
return sendJSONResponse(res, 400, { error: "No permission" });
}
const decodedFilePath = decodeURIComponent(filePath);
const fileExists = await promisify(fs.exists)(decodedFilePath);
if (fileExists) {
const fileStream = fs.createReadStream(decodedFilePath);
fileStream.pipe(res);
} else {
return sendJSONResponse(res, 404, { error: "File not found" });
}
};
+1
View File
@@ -2,3 +2,4 @@ export * from "./events";
export * from "./server";
export * from "./types";
export { generateEventComponent } from "./utils/gen-ui";
export { toStreamGenerator } from "./utils/workflow";
+20 -12
View File
@@ -1,5 +1,4 @@
import { getEnv } from "@llamaindex/env";
import type { Workflow } from "@llamaindex/workflow";
import fs from "fs";
import { createServer } from "http";
import next from "next";
@@ -7,7 +6,10 @@ import path from "path";
import { parse } from "url";
import { promisify } from "util";
import { handleChat } from "./handlers/chat";
import type { LlamaIndexServerOptions } from "./types";
import { getLlamaCloudConfig } from "./handlers/cloud";
import { getComponents } from "./handlers/components";
import { handleServeFiles } from "./handlers/files";
import type { LlamaIndexServerOptions, ServerWorkflow } from "./types";
const nextDir = path.join(__dirname, "..", "server");
const configFile = path.join(__dirname, "..", "server", "public", "config.js");
@@ -16,7 +18,7 @@ const dev = process.env.NODE_ENV !== "production";
export class LlamaIndexServer {
port: number;
app: ReturnType<typeof next>;
workflowFactory: () => Promise<Workflow> | Workflow;
workflowFactory: () => Promise<ServerWorkflow> | ServerWorkflow;
componentsDir?: string | undefined;
constructor(options: LlamaIndexServerOptions) {
@@ -42,7 +44,6 @@ export class LlamaIndexServer {
? "/api/chat/config/llamacloud"
: undefined;
const componentsApi = this.componentsDir ? "/api/components" : undefined;
const devMode = uiConfig?.devMode ?? false;
// content in javascript format
const content = `
@@ -51,8 +52,7 @@ export class LlamaIndexServer {
APP_TITLE: ${JSON.stringify(appTitle)},
LLAMA_CLOUD_API: ${JSON.stringify(llamaCloudApi)},
STARTER_QUESTIONS: ${JSON.stringify(starterQuestions)},
COMPONENTS_API: ${JSON.stringify(componentsApi)},
DEV_MODE: ${JSON.stringify(devMode)}
COMPONENTS_API: ${JSON.stringify(componentsApi)}
}
`;
fs.writeFileSync(configFile, content);
@@ -71,25 +71,33 @@ export class LlamaIndexServer {
const server = createServer((req, res) => {
const parsedUrl = parse(req.url!, true);
const pathname = parsedUrl.pathname;
const query = parsedUrl.query;
if (pathname === "/api/chat" && req.method === "POST") {
// because of https://github.com/vercel/next.js/discussions/79402 we can't use route.ts here, so we need to call this custom route
// when calling `pnpm eject`, the user will get an equivalent route at [path to chat route.ts]
// make sure to keep its semantic in sync with handleChat
return handleChat(req, res, this.workflowFactory);
}
if (pathname?.startsWith("/api/files") && req.method === "GET") {
return handleServeFiles(req, res, pathname);
}
if (
this.componentsDir &&
pathname === "/api/components" &&
req.method === "GET"
) {
query.componentsDir = this.componentsDir;
return getComponents(req, res, this.componentsDir);
}
if (
getEnv("LLAMA_CLOUD_API_KEY") &&
pathname === "/api/chat/config/llamacloud" &&
req.method === "GET"
) {
return getLlamaCloudConfig(req, res);
}
const handle = this.app.getRequestHandler();
handle(req, res, { ...parsedUrl, query });
handle(req, res, parsedUrl);
});
server.listen(this.port, () => {
+15 -4
View File
@@ -1,14 +1,26 @@
import type { Workflow } from "@llamaindex/workflow";
import {
type AgentInputData,
type AgentWorkflow,
type AgentWorkflowContext,
type Workflow,
} from "llamaindex";
import type next from "next";
/**
* A factory function that creates a Workflow instance, possibly asynchronously.
* ServerWorkflow can be either a custom Workflow or an AgentWorkflow
*/
export type ServerWorkflow =
| Workflow<AgentWorkflowContext, AgentInputData, string>
| AgentWorkflow;
/**
* A factory function that creates a ServerWorkflow instance, possibly asynchronously.
* The requestBody parameter is the body from the request, which can be used to customize the workflow per request.
*/
export type WorkflowFactory = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
requestBody?: any,
) => Promise<Workflow> | Workflow;
) => Promise<ServerWorkflow> | ServerWorkflow;
export type NextAppOptions = Parameters<typeof next>[0];
@@ -17,7 +29,6 @@ export type UIConfig = {
starterQuestions?: string[];
componentsDir?: string;
llamaCloudIndexSelector?: boolean;
devMode?: boolean;
};
export type LlamaIndexServerOptions = NextAppOptions & {
+4 -6
View File
@@ -8,11 +8,9 @@ import type {
ImportNamespaceSpecifier,
ImportSpecifier,
} from "@babel/types";
import {
createWorkflow,
getContext,
workflowEvent,
} from "@llamaindex/workflow";
import { createWorkflow, getContext, workflowEvent } from "@llama-flow/core";
import { collect } from "@llama-flow/core/stream/consumer";
import { until } from "@llama-flow/core/stream/until";
import type { LLM } from "llamaindex";
import type { ZodType } from "zod";
@@ -546,7 +544,7 @@ export async function generateEventComponent(
sendEvent(startEvent.with({ eventSchema }));
// Collect all events until the stop event and get the last one
const allEvents = await stream.toArray();
const allEvents = await collect(until(stream, stopEvent));
const result = allEvents[allEvents.length - 1];
if (result?.data === null) {
throw new Error("Workflow failed.");
+3 -3
View File
@@ -29,10 +29,10 @@ export function sendJSONResponse(
export async function pipeStreamToResponse(
response: ServerResponse,
stream: ReadableStream,
stream: Response,
) {
if (!stream) return;
const reader = stream.getReader();
if (!stream.body) return;
const reader = stream.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) return response.end();
-80
View File
@@ -1,80 +0,0 @@
import { agentStreamEvent, type WorkflowEventData } from "@llamaindex/workflow";
import {
createDataStream,
formatDataStreamPart,
type DataStreamWriter,
type JSONValue,
} from "ai";
/**
* Configuration options and helper callback methods for stream lifecycle events.
*/
export interface StreamCallbacks {
/** `onStart`: Called once when the stream is initialized. */
onStart?: (dataStreamWriter: DataStreamWriter) => Promise<void> | void;
/** `onFinal`: Called once when the stream is closed with the final completion message. */
onFinal?: (
completion: string,
dataStreamWriter: DataStreamWriter,
) => Promise<void> | void;
/** `onText`: Called for each text chunk. */
onText?: (
text: string,
dataStreamWriter: DataStreamWriter,
) => Promise<void> | void;
}
/**
* Convert a stream of WorkflowEventData to a Response object.
* @param stream - The input stream of WorkflowEventData.
* @param options - Optional options for stream lifecycle events.
* @returns A readable stream of data.
*/
export function toDataStream(
stream: AsyncIterable<WorkflowEventData<unknown>>,
options: {
callbacks?: StreamCallbacks;
} = {},
) {
const { callbacks } = options;
let completionText = "";
let hasStarted = false;
return createDataStream({
execute: async (dataStreamWriter: DataStreamWriter) => {
if (!hasStarted && callbacks?.onStart) {
await callbacks.onStart(dataStreamWriter);
hasStarted = true;
}
for await (const event of stream) {
if (agentStreamEvent.include(event) && event.data.delta) {
const content = event.data.delta;
if (content) {
completionText += content;
dataStreamWriter.write(formatDataStreamPart("text", content));
if (callbacks?.onText) {
await callbacks.onText(content, dataStreamWriter);
}
}
} else {
dataStreamWriter.writeMessageAnnotation(event.data as JSONValue);
}
}
// Call onFinal with the complete text when stream ends
if (callbacks?.onFinal) {
await callbacks.onFinal(completionText, dataStreamWriter);
}
},
onError: (error: unknown) => {
return error instanceof Error
? error.message
: "An unknown error occurred during stream finalization";
},
});
}
+4 -4
View File
@@ -1,4 +1,4 @@
import type { DataStreamWriter } from "ai";
import type { StreamData } from "ai";
import { type ChatMessage, Settings } from "llamaindex";
const NEXT_QUESTION_PROMPT = `You're a helpful assistant! Your task is to suggest the next question that user might ask.
@@ -16,19 +16,19 @@ Your answer should be wrapped in three sticks which follows the following format
`;
export const sendSuggestedQuestionsEvent = async (
streamWriter: DataStreamWriter,
dataStream: StreamData,
chatHistory: ChatMessage[] = [],
) => {
const questions = await generateNextQuestions(chatHistory);
if (questions.length > 0) {
streamWriter.writeMessageAnnotation({
dataStream.appendMessageAnnotation({
type: "suggested_questions",
data: questions,
});
}
};
export async function generateNextQuestions(conversation: ChatMessage[]) {
async function generateNextQuestions(conversation: ChatMessage[]) {
const conversationText = conversation
.map((message) => `${message.role}: ${message.content}`)
.join("\n");
+177 -69
View File
@@ -1,90 +1,198 @@
import {
agentToolCallEvent,
agentToolCallResultEvent,
run,
startAgentEvent,
stopAgentEvent,
WorkflowStream,
type AgentInputData,
type Workflow,
type WorkflowEventData,
} from "@llamaindex/workflow";
import {
LLamaCloudFileService,
type Metadata,
type NodeWithScore,
import { LlamaIndexAdapter, StreamData, type JSONValue } from "ai";
import type {
AgentInputData,
ChatResponseChunk,
EngineResponse,
Metadata,
NodeWithScore,
WorkflowEvent,
} from "llamaindex";
import {
sourceEvent,
AgentStream,
AgentToolCall,
AgentToolCallResult,
AgentWorkflow,
LLamaCloudFileService,
StopEvent,
Workflow,
type AgentWorkflowContext,
} from "llamaindex";
import { ReadableStream } from "stream/web";
import {
SourceEvent,
toAgentRunEvent,
toSourceEvent,
type SourceEventNode,
} from "../events";
import type { ServerWorkflow } from "../types";
import { downloadFile } from "./file";
import { sendSuggestedQuestionsEvent } from "./suggestion";
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");
workflow: ServerWorkflow,
agentInput: AgentInputData,
) {
if (workflow instanceof AgentWorkflow) {
return runAgentWorkflow(workflow, agentInput);
}
const workflowStream = run(workflow, [
startAgentEvent.with({
userInput: input.userInput,
chatHistory: input.chatHistory,
}),
]);
// Transform the stream to handle annotations
return processWorkflowStream(workflowStream).until(
(event) => abortSignal?.aborted || stopAgentEvent.include(event),
);
return runCustomWorkflow(workflow, agentInput);
}
function processWorkflowStream(
stream: WorkflowStream<WorkflowEventData<unknown>>,
async function runAgentWorkflow(
workflow: AgentWorkflow,
agentInput: AgentInputData,
) {
return stream.pipeThrough(
new TransformStream<WorkflowEventData<unknown>, WorkflowEventData<unknown>>(
{
async transform(event, controller) {
let transformedEvent = event;
const { userInput = "", chatHistory = [] } = agentInput;
const context = workflow.run(userInput, { chatHistory });
// Handle agent events from AgentToolCall
if (agentToolCallEvent.include(event)) {
const inputString = JSON.stringify(event.data.toolKwargs);
transformedEvent = toAgentRunEvent({
agent: event.data.agentName,
text: `Using tool: '${event.data.toolName}' with inputs: '${inputString}'`,
type: "text",
});
}
// Handle source nodes from AgentToolCallResult
else if (agentToolCallResultEvent.include(event)) {
const rawOutput = event.data.raw;
if (
rawOutput &&
typeof rawOutput === "object" &&
"sourceNodes" in rawOutput // TODO: better use Zod to validate and extract sourceNodes from toolCallResult
) {
const sourceNodes =
rawOutput.sourceNodes as unknown as NodeWithScore<Metadata>[];
transformedEvent = toSourceEvent(sourceNodes);
const dataStream = new StreamData();
const stream = new ReadableStream<EngineResponse>({
async pull(controller) {
try {
for await (const event of context) {
if (event instanceof AgentStream) {
// for agent workflow, get the delta from AgentStream event and enqueue it
const delta = event.data.delta;
if (delta) {
controller.enqueue({ delta } as EngineResponse);
}
} else {
appendEventDataToAnnotations(dataStream, event);
}
// Post-process for llama-cloud files
if (sourceEvent.include(transformedEvent)) {
const sourceNodesForDownload = transformedEvent.data.data.nodes; // These are SourceEventNode[]
downloadLlamaCloudFilesFromNodes(sourceNodesForDownload); // download files in background
}
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "An unknown error occurred";
controller.enqueue({ delta: errorMessage } as EngineResponse);
dataStream.close();
} finally {
controller.close();
}
},
});
controller.enqueue(transformedEvent);
},
return LlamaIndexAdapter.toDataStreamResponse(stream, {
data: dataStream,
callbacks: {
onFinal: async (content: string) => {
const history = chatHistory.concat({ role: "assistant", content });
await sendSuggestedQuestionsEvent(dataStream, history);
dataStream.close();
},
),
);
},
});
}
async function runCustomWorkflow(
workflow: Workflow<AgentWorkflowContext, AgentInputData, string>,
agentInput: AgentInputData,
) {
const context = workflow.run(agentInput);
const dataStream = new StreamData();
const stream = new ReadableStream<EngineResponse>({
async pull(controller) {
try {
for await (const event of context) {
if (event instanceof StopEvent) {
// for normal workflow, the event data from StopEvent is a generator of ChatResponseChunk
// iterate over the generator and enqueue the delta of each chunk
const generator = event.data as AsyncGenerator<ChatResponseChunk>;
for await (const chunk of generator) {
controller.enqueue({ delta: chunk.delta } as EngineResponse);
}
} else {
appendEventDataToAnnotations(dataStream, event);
}
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "An unknown error occurred";
controller.enqueue({ delta: errorMessage } as EngineResponse);
dataStream.close();
} finally {
controller.close();
}
},
});
return LlamaIndexAdapter.toDataStreamResponse(stream, {
data: dataStream,
callbacks: {
onFinal: async (content: string) => {
const history = agentInput.chatHistory?.concat({
role: "assistant",
content,
});
await sendSuggestedQuestionsEvent(dataStream, history);
dataStream.close();
},
},
});
}
export async function* toStreamGenerator(
input: AsyncIterable<ChatResponseChunk> | string,
): AsyncGenerator<ChatResponseChunk> {
if (typeof input === "string") {
yield { delta: input } as ChatResponseChunk;
return;
}
for await (const chunk of input) {
yield chunk;
}
}
// append data of other events to the data stream as message annotations
function appendEventDataToAnnotations(
dataStream: StreamData,
event: WorkflowEvent<unknown>,
) {
const transformedEvent = transformWorkflowEvent(event);
// for SourceEvent, we need to trigger download files from LlamaCloud (if having)
if (transformedEvent instanceof SourceEvent) {
const sourceNodes = transformedEvent.data.data.nodes;
downloadLlamaCloudFilesFromNodes(sourceNodes); // download files in background
}
dataStream.appendMessageAnnotation(transformedEvent.data as JSONValue);
}
// transform WorkflowEvent to another WorkflowEvent for annotations display purpose
// this useful for handling AgentWorkflow events, because we cannot easily append custom events like custom workflows
function transformWorkflowEvent(
event: WorkflowEvent<unknown>,
): WorkflowEvent<unknown> {
// convert AgentToolCall event to AgentRunEvent
if (event instanceof AgentToolCall) {
const inputString = JSON.stringify(event.data.toolKwargs);
return toAgentRunEvent({
agent: event.data.agentName,
text: `Using tool: '${event.data.toolName}' with inputs: '${inputString}'`,
type: "text",
});
}
// modify AgentToolCallResult event
if (event instanceof AgentToolCallResult) {
const rawOutput = event.data.raw;
// if AgentToolCallResult contains sourceNodes, convert it to SourceEvent
if (
rawOutput &&
typeof rawOutput === "object" &&
"sourceNodes" in rawOutput // TODO: better use Zod to validate and extract sourceNodes from toolCallResult
) {
return toSourceEvent(
rawOutput.sourceNodes as unknown as NodeWithScore<Metadata>[],
);
}
}
return event;
}
async function downloadLlamaCloudFilesFromNodes(nodes: SourceEventNode[]) {
-10
View File
@@ -1,10 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./next/app/api",
"outDir": "./server/app/api",
"emitDeclarationOnly": false
},
"include": ["./next/app/api"],
"exclude": ["./next/app/api/chat/route.ts"]
}

Some files were not shown because too many files have changed in this diff Show More