Compare commits

..

42 Commits

Author SHA1 Message Date
thucpn 3eb05facd1 Merge branch 'lee/add-workflow-editor' into tp/support-dev-mode-for-backend-ts-server 2025-05-15 11:17:47 +07:00
thucpn 89c9ce8bb5 increase delay time before trigger polling 2025-05-15 11:16:52 +07:00
thucpn e55c5cc939 Merge branch 'lee/add-workflow-editor' into tp/support-dev-mode-for-backend-ts-server 2025-05-15 10:03:24 +07:00
leehuwuj cd72078b7e fix wrong check 2025-05-15 10:02:31 +07:00
thucpn 972fe0d1c1 remove typescript deps 2025-05-15 09:56:11 +07:00
thucpn 7a6a753dda use npx tsc 2025-05-15 09:55:52 +07:00
leehuwuj df5017da6c remove unessesary debug log 2025-05-15 09:53:08 +07:00
leehuwuj 31c07d5b5c enhance doc 2025-05-15 09:34:20 +07:00
thucpn 6529cc3693 Merge branch 'lee/add-workflow-editor' into tp/support-dev-mode-for-backend-ts-server 2025-05-14 16:29:34 +07:00
thucpn 7774021eaa fix minor UI bugs 2025-05-14 16:28:56 +07:00
thucpn 2fec57c895 Merge branch 'lee/add-workflow-editor' into tp/support-dev-mode-for-backend-ts-server 2025-05-14 16:11:42 +07:00
thucpn fecec469fd fix format 2025-05-14 16:02:32 +07:00
thucpn 9b0abfe4f0 use temp file to avoid server restart 2025-05-14 16:02:27 +07:00
thucpn e699b274d3 fix: format 2025-05-14 15:36:01 +07:00
thucpn 9eb3226468 validate typescript file 2025-05-14 15:02:58 +07:00
thucpn b888e874c6 Merge branch 'lee/add-workflow-editor' into tp/support-dev-mode-for-backend-ts-server 2025-05-14 14:25:46 +07:00
leehuwuj 1d375d9e6a enhance doc 2025-05-14 14:25:31 +07:00
thucpn fc3b4b6216 enhance UI with shadow overlay 2025-05-14 14:24:56 +07:00
thucpn 3f0ee7706a Merge branch 'lee/add-workflow-editor' into tp/support-dev-mode-for-backend-ts-server 2025-05-14 14:05:28 +07:00
thucpn 44a0f80955 Merge branch 'main' into lee/add-workflow-editor 2025-05-14 14:04:53 +07:00
thucpn 36c9e09469 fix: missing language for code editor 2025-05-14 14:03:48 +07:00
thucpn c8367b8a33 update message 2025-05-14 13:52:07 +07:00
Thuc Pham 24515393a6 fix: remove dead generated ai code (#618) 2025-05-14 12:42:43 +07:00
thucpn 174b448570 Merge branch 'lee/add-workflow-editor' into tp/support-dev-mode-for-backend-ts-server 2025-05-14 11:39:19 +07:00
thucpn c3d5d206cb bump chat-ui to fix syntax highlight issue 2025-05-14 11:32:04 +07:00
thucpn 978cc93038 Merge branch 'lee/add-workflow-editor' into tp/support-dev-mode-for-backend-ts-server 2025-05-13 18:26:51 +07:00
thucpn 63b670f4a1 fix: polling should work when server not yet started 2025-05-13 18:23:37 +07:00
thucpn 135629492c feat: support dev mode for backend ts server 2025-05-13 18:12:06 +07:00
leehuwuj 3f8868f0f3 Merge remote-tracking branch 'origin/main' into lee/add-workflow-editor 2025-05-13 18:03:33 +07:00
leehuwuj 4dd395ec1d Revert "support devmode for backend ts server"
This reverts commit bd943fd8c1.
2025-05-13 18:03:20 +07:00
thucpn bd943fd8c1 support devmode for backend ts server 2025-05-13 17:51:36 +07:00
leehuwuj 126430553a fix mypy 2025-05-13 17:45:10 +07:00
leehuwuj 138a26deb5 include changes from #614 2025-05-13 17:41:46 +07:00
leehuwuj 8ce137d779 change to file_path 2025-05-13 17:37:11 +07:00
leehuwuj 731fc09358 update doc 2025-05-13 16:33:10 +07:00
leehuwuj d3226e833b fix: update default workflow file path and improve error handling 2025-05-13 16:24:21 +07:00
Huu Le b3eb0ba7d4 fix typing issue and add typing test for llamaindexserver templates (#613)
* try testing for llamaindexserver

* Enhance TypeScript tests for dependency resolution by introducing template types and use cases

* refactor template structure

* fix package conflict

* add tests for python

* fix python mypy

* use matrix for templateType

* add changeset

* add removing data.ts for artifacts template

* don't ask llamacloud for unsupported use case and skip test

* Enhance tests for LlamaIndexServer by adding conditional skips based on data source and refining use case tests for example data source
2025-05-13 16:20:24 +07:00
leehuwuj 97f6c26dd2 feat: Add simple chat app example with FastAPI integration 2025-05-13 14:52:57 +07:00
Huu Le 556f33c0ab pin onnxruntime version to fix issue on Windows (#609) 2025-05-12 16:25:47 +07:00
leehuwuj 3d641bdacd Merge remote-tracking branch 'origin/main' into lee/add-workflow-editor 2025-05-12 16:20:07 +07:00
Marcus Schiesser 7a70390b00 chore: deprecate pro mode (#607) 2025-05-12 12:07:55 +07:00
leehuwuj 9843960484 Add UI components and static assets for chat interface 2025-05-06 13:09:32 +07:00
61 changed files with 3910 additions and 3023 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Fix typing check issue
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
fix chromadb dependency issue
+5
View File
@@ -0,0 +1,5 @@
---
"@llamaindex/server": patch
---
feat: add dev mode UI
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
fix: remove dead generated ai code
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Deprecate pro mode
+4
View File
@@ -23,6 +23,7 @@ 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
@@ -70,6 +71,7 @@ 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
@@ -93,6 +95,7 @@ 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
@@ -140,6 +143,7 @@ jobs:
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
FRAMEWORK: ${{ matrix.frameworks }}
DATASOURCE: ${{ matrix.datasources }}
TEMPLATE_TYPE: ${{ matrix.template-types }}
working-directory: packages/create-llama
- uses: actions/upload-artifact@v4
+2
View File
@@ -7,6 +7,8 @@ build/
.next/
out/
packages/server/server/
**/playwright-report/
**/test-results/
# Python
python/
-19
View File
@@ -106,25 +106,6 @@ 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,7 +3,7 @@ import { exec } from "child_process";
import fs from "fs";
import path from "path";
import util from "util";
import { TemplateFramework, TemplateVectorDB } from "../../helpers/types";
import { TemplateFramework, TemplateType, TemplateUseCase, TemplateVectorDB } from "../../helpers/types";
import { RunCreateLlamaOptions, createTestDir, runCreateLlama } from "../utils";
const execAsync = util.promisify(exec);
@@ -11,123 +11,192 @@ 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",
"artifacts",
];
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
: "--example-file";
// TODO: add support for other templates
test.describe("Mypy check", () => {
test.describe.configure({ retries: 0 });
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",
];
// 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",
];
const toolOptions = [
"wikipedia.WikipediaToolSpec",
"google.GoogleSearchToolSpec",
"document_generator",
"artifact",
];
const dataSources = [
"--example-file",
"--web-source https://www.example.com",
"--db-source mysql+pymysql://user:pass@localhost:3306/mydb",
];
const dataSources = [
"--example-file",
"--web-source https://www.example.com",
"--db-source mysql+pymysql://user:pass@localhost:3306/mydb",
];
const observabilityOptions = ["llamatrace", "traceloop"];
const observabilityOptions = ["llamatrace", "traceloop"];
// 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,
},
});
test.describe("Mypy check", () => {
test.describe.configure({ retries: 0 });
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 vector databases
for (const vectorDb of vectorDbs) {
test(`Mypy check for vectorDB: ${vectorDb}`, async () => {
// // 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) {
const cwd = await createTestDir();
const { pyprojectPath } = await createAndCheckLlamaProject({
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 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",
templateType: "llamaindexserver",
templateFramework,
dataSource,
vectorDb: "none",
@@ -139,110 +208,77 @@ if (
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
observability: undefined,
useCase,
},
});
}
});
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");
}
});
}
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);
// Test observability options
for (const observability of observabilityOptions) {
test(`Mypy check for observability: ${observability}`, async () => {
const cwd = await createTestDir();
// Check if the app folder exists
expect(fs.existsSync(projectPath)).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,
},
});
});
}
});
}
// Check if pyproject.toml exists
const pyprojectPath = path.join(projectPath, "pyproject.toml");
expect(fs.existsSync(pyprojectPath)).toBeTruthy();
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);
// Modify environment for the command
const commandEnv = {
...process.env,
};
// Check if the app folder exists
expect(fs.existsSync(projectPath)).toBeTruthy();
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 pyproject.toml exists
const pyprojectPath = path.join(projectPath, "pyproject.toml");
expect(fs.existsSync(pyprojectPath)).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
}
// Modify environment for the command
const commandEnv = {
...process.env,
};
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;
}
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
// If we reach this point without throwing an error, the test passes
expect(true).toBeTruthy();
return { pyprojectPath, projectPath };
}
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 };
}
});
@@ -3,7 +3,12 @@ import { exec } from "child_process";
import fs from "fs";
import path from "path";
import util from "util";
import { TemplateFramework, TemplateVectorDB } from "../../helpers/types";
import {
TemplateFramework,
TemplateType,
TemplateUseCase,
TemplateVectorDB,
} from "../../helpers/types";
import { createTestDir, runCreateLlama } from "../utils";
const execAsync = util.promisify(exec);
@@ -11,6 +16,15 @@ 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",
"artifacts",
];
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
: "--example-file";
@@ -29,77 +43,118 @@ 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 = `vectorDb: ${vectorDb}, dataSource: ${dataSource}`;
const optionDescription = `templateType: ${templateType}, vectorDb: ${vectorDb}, dataSource: ${dataSource}`;
test(`Vector DB test - ${optionDescription}`, async () => {
await runTest(vectorDb, false);
// 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,
});
});
}
// 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;
// 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 !== "artifacts") {
test(`llamaParse - ${optionDescription}`, async () => {
await runTest({
templateType: templateType,
useLlamaParse: true,
useCase: useCase,
});
});
}
});
}
}
});
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;
}
}
+7 -3
View File
@@ -94,6 +94,10 @@ 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": {
@@ -565,13 +569,13 @@ const installLlamaIndexServerTemplate = async ({
await copy("*.py", path.join(root, "app"), {
parents: true,
cwd: path.join(templatesDir, "components", "workflows", "python", useCase),
cwd: path.join(templatesDir, "components", "use-cases", "python", useCase),
});
// Copy custom UI component code
await copy(`*`, path.join(root, "components"), {
parents: true,
cwd: path.join(templatesDir, "components", "ui", "workflows", useCase),
cwd: path.join(templatesDir, "components", "ui", "use-cases", useCase),
});
if (useLlamaParse) {
@@ -602,7 +606,7 @@ const installLlamaIndexServerTemplate = async ({
// Copy README.md
await copy("README-template.md", path.join(root), {
parents: true,
cwd: path.join(templatesDir, "components", "workflows", "python", useCase),
cwd: path.join(templatesDir, "components", "use-cases", "python", useCase),
rename: assetRelocator,
});
};
+13 -16
View File
@@ -31,23 +31,24 @@ const installLlamaIndexServerTemplate = async ({
process.exit(1);
}
await copy("*.ts", path.join(root, "src", "app"), {
parents: true,
await copy("**", path.join(root), {
cwd: path.join(
templatesDir,
"components",
"workflows",
"use-cases",
"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", "workflows", useCase),
cwd: path.join(templatesDir, "components", "ui", "use-cases", 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,
@@ -74,18 +75,14 @@ const installLlamaIndexServerTemplate = async ({
rename: () => "data.ts",
});
}
// Copy README.md
await copy("README-template.md", path.join(root), {
parents: true,
cwd: path.join(
templatesDir,
"components",
"workflows",
"typescript",
useCase,
),
rename: assetRelocator,
});
// Simplify use case code
if (useCase === "artifacts") {
// 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
}
};
const installLegacyTSTemplate = async ({
+1 -1
View File
@@ -196,7 +196,7 @@ const program = new Command(packageJson.name)
"--pro",
`
Allow interactive selection of all features.
Deprecated: Allow interactive selection of all features.
`,
false,
)
+7
View File
@@ -1,4 +1,5 @@
import ciInfo from "ci-info";
import { bold, yellow } from "picocolors";
import { getCIQuestionResults } from "./ci";
import { askProQuestions } from "./questions";
import { askSimpleQuestions } from "./simple";
@@ -13,6 +14,12 @@ 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;
}
+15 -13
View File
@@ -76,19 +76,21 @@ export const askSimpleQuestions = async (
);
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 (appType !== "artifacts") {
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
@@ -113,11 +113,6 @@ 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>
@@ -0,0 +1,39 @@
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();
}
})();
@@ -0,0 +1,39 @@
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();
}
})();
@@ -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
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
@@ -12,7 +12,7 @@
"@llamaindex/openai": "0.2.0",
"@llamaindex/server": "0.2.0",
"@llamaindex/workflow": "1.1.1",
"@llamaindex/tools": "0.0.4",
"@llamaindex/tools": "0.0.9",
"dotenv": "^16.4.7",
"zod": "^3.23.8",
"llamaindex": "0.10.5"
@@ -0,0 +1,21 @@
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: 6000,
}).start();
```
Export OpenAI API key and start the server in dev mode.
```bash
export OPENAI_API_KEY=<your-openai-api-key>
npx tsx watch index.ts
```
+15
View File
@@ -0,0 +1,15 @@
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: 6005,
}).start();
@@ -0,0 +1,16 @@
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`,
}),
],
});
};
@@ -13,6 +13,7 @@ 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({
@@ -35,12 +36,13 @@ export default function ChatSection() {
<ChatHeader />
<ChatUI
handler={handler}
className="flex min-h-0 flex-1 flex-row justify-center gap-4 px-4 py-0"
className="relative 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 />
@@ -0,0 +1,270 @@
"use client";
import {
CodeEditor,
fileExtensionToEditorLang,
} from "@llamaindex/chat-ui/widgets";
import { AlertCircle, Loader2 } from "lucide-react";
import { useEffect, 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 = fileExtensionToEditorLang(
workflowFile?.file_path.split(".").pop() ?? "",
);
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
@@ -55,7 +55,7 @@
"@babel/traverse": "^7.27.0",
"@babel/types": "^7.27.0",
"@hookform/resolvers": "^5.0.1",
"@llamaindex/chat-ui": "0.4.3",
"@llamaindex/chat-ui": "0.4.4",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.7",
"@radix-ui/react-aspect-ratio": "^1.1.3",
+84 -1
View File
@@ -1,7 +1,9 @@
import { exec } from "child_process";
import fs from "fs";
import type { IncomingMessage, ServerResponse } from "http";
import path from "path";
import { promisify } from "util";
import { sendJSONResponse } from "../utils/request";
import { parseRequestBody, sendJSONResponse } from "../utils/request";
export const handleServeFiles = async (
req: IncomingMessage,
@@ -21,3 +23,84 @@ export const handleServeFiles = async (
return sendJSONResponse(res, 404, { error: "File not found" });
}
};
const DEFAULT_WORKFLOW_FILE_PATH = "src/app/workflow.ts"; // TODO: we can make it as a parameter in server later
export const getWorkflowFile = async (
req: IncomingMessage,
res: ServerResponse,
filePath: string = DEFAULT_WORKFLOW_FILE_PATH,
) => {
const fileExists = await promisify(fs.exists)(filePath);
if (!fileExists) {
return sendJSONResponse(res, 404, {
detail: `Dev mode is currently in beta. It only supports updating workflow file at ${DEFAULT_WORKFLOW_FILE_PATH}`,
});
}
const content = await promisify(fs.readFile)(filePath, "utf-8");
const last_modified = fs.statSync(filePath).mtime.getTime();
sendJSONResponse(res, 200, { content, file_path: filePath, last_modified });
};
export const updateWorkflowFile = async (
req: IncomingMessage,
res: ServerResponse,
filePath: string = DEFAULT_WORKFLOW_FILE_PATH,
) => {
const body = await parseRequestBody(req);
const { content } = body as { content: string };
const fileExists = await promisify(fs.exists)(filePath);
if (!fileExists) {
return sendJSONResponse(res, 404, {
detail: `Dev mode is currently in beta. It only supports updating workflow file at ${DEFAULT_WORKFLOW_FILE_PATH}`,
});
}
try {
const resolvedFilePath = path.resolve(DEFAULT_WORKFLOW_FILE_PATH);
const result = await validateTypeScriptFile(resolvedFilePath, content);
if (!result.isValid) {
return sendJSONResponse(res, 400, {
detail: result.errors.join("\n"),
});
}
await promisify(fs.writeFile)(filePath, content);
sendJSONResponse(res, 200, { content });
} catch (error) {
console.error("Error updating workflow file:", error);
sendJSONResponse(res, 500, { error: "Failed to update workflow file" });
}
};
// 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,
};
}
+17 -2
View File
@@ -9,8 +9,13 @@ import { promisify } from "util";
import { handleChat } from "./handlers/chat";
import { getLlamaCloudConfig } from "./handlers/cloud";
import { getComponents } from "./handlers/components";
import { handleServeFiles } from "./handlers/files";
import {
getWorkflowFile,
handleServeFiles,
updateWorkflowFile,
} from "./handlers/files";
import type { LlamaIndexServerOptions } from "./types";
const nextDir = path.join(__dirname, "..", "server");
const configFile = path.join(__dirname, "..", "server", "public", "config.js");
const dev = process.env.NODE_ENV !== "production";
@@ -44,6 +49,7 @@ 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 = `
@@ -52,7 +58,8 @@ export class LlamaIndexServer {
APP_TITLE: ${JSON.stringify(appTitle)},
LLAMA_CLOUD_API: ${JSON.stringify(llamaCloudApi)},
STARTER_QUESTIONS: ${JSON.stringify(starterQuestions)},
COMPONENTS_API: ${JSON.stringify(componentsApi)}
COMPONENTS_API: ${JSON.stringify(componentsApi)},
DEV_MODE: ${JSON.stringify(devMode)}
}
`;
fs.writeFileSync(configFile, content);
@@ -96,6 +103,14 @@ export class LlamaIndexServer {
return getLlamaCloudConfig(req, res);
}
if (pathname === "/api/dev/files/workflow" && req.method === "GET") {
return getWorkflowFile(req, res);
}
if (pathname === "/api/dev/files/workflow" && req.method === "PUT") {
return updateWorkflowFile(req, res);
}
const handle = this.app.getRequestHandler();
handle(req, res, parsedUrl);
});
+1
View File
@@ -17,6 +17,7 @@ export type UIConfig = {
starterQuestions?: string[];
componentsDir?: string;
llamaCloudIndexSelector?: boolean;
devMode?: boolean;
};
export type LlamaIndexServerOptions = NextAppOptions & {
+5 -5
View File
@@ -181,8 +181,8 @@ importers:
specifier: ^5.0.1
version: 5.0.1(react-hook-form@7.56.1(react@19.1.0))
'@llamaindex/chat-ui':
specifier: 0.4.3
version: 0.4.3(@babel/runtime@7.27.0)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.7)(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(codemirror@6.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
specifier: 0.4.4
version: 0.4.4(@babel/runtime@7.27.0)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.7)(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(codemirror@6.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@llamaindex/env':
specifier: ^0.1.29
version: 0.1.29
@@ -1177,8 +1177,8 @@ packages:
zod:
optional: true
'@llamaindex/chat-ui@0.4.3':
resolution: {integrity: sha512-JeS5pzEXZRRzTL70lHRtbWbaPjeZuMMHlqkTNBgYqc2eMM+o9VMOzVKKsXoYfOeE+wunqJd+cBrtkECGEmhRZg==}
'@llamaindex/chat-ui@0.4.4':
resolution: {integrity: sha512-sE3mJxlmAV3eiIaqOnioUNYYBLJJL8sy2mdFP4xXDf3PfkcEn0wT2Om/ButpVgIXHlPG9lONtv8mzw7hWq2Atg==}
peerDependencies:
react: ^18.2.0 || ^19.0.0 || ^19.0.0-rc
@@ -7113,7 +7113,7 @@ snapshots:
next: 15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
zod: 3.24.3
'@llamaindex/chat-ui@0.4.3(@babel/runtime@7.27.0)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.7)(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(codemirror@6.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
'@llamaindex/chat-ui@0.4.4(@babel/runtime@7.27.0)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.7)(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(codemirror@6.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@codemirror/lang-css': 6.3.1
'@codemirror/lang-html': 6.4.9
+16
View File
@@ -83,6 +83,7 @@ The LlamaIndexServer accepts the following configuration parameters:
- `ui_path`: Path for downloaded UI static files (default: ".ui")
- `component_dir`: The directory for custom UI components rendering events emitted by the workflow. The default is None, which does not render custom UI components.
- `llamacloud_index_selector`: Whether to show the LlamaCloud index selector in the chat UI (default: False). Requires `LLAMA_CLOUD_API_KEY` to be set.
- `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/workflow.py`. You might also need to set `env="dev"` and start the server with the reload feature enabled.
- `verbose`: Enable verbose logging
- `api_prefix`: API route prefix (default: "/api")
- `server_url`: The deployment URL of the server (default is None)
@@ -120,6 +121,21 @@ In development mode (`env="dev"`), the server:
- Automatically includes the chat UI
- Provides more verbose logging
### Workflow Editor (Beta)
In development mode, you can set `dev_mode` to `True` in the UI configuration to enable the workflow editor, which allows you to edit the workflow code directly in the browser.
```python
app = LlamaIndexServer(
workflow_factory=create_workflow,
env="dev",
ui_config={"dev_mode": True},
)
```
**Note**: The workflow editor is currently in beta and only supports updating LlamaIndexServer projects created with [create-llama](https://github.com/run-llama/create-llama/). You also need to start the server via `fastapi dev` so that the server can hot reload the workflow code.
## API Endpoints
The server provides the following default endpoints:
@@ -113,11 +113,6 @@ 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>
@@ -0,0 +1,38 @@
# A simple chat app
This guide explains how to set up and use the LlamaIndex server with a simple chatbot agent.
## Prerequisites
- [uv](https://github.com/astral-sh/uv) installed (a fast Python package manager and runner)
- An OpenAI API key
## Steps
1. **Set the OpenAI API Key**
Export your OpenAI API key as an environment variable:
```sh
export OPENAI_API_KEY=your_openai_api_key_here
```
2. **Run the Server Using uv**
Start the server with the following command:
```sh
uv run workflow.py
```
This will launch the FastAPI server using the workflow defined in `main.py`.
3. **Access the Application**
Open your browser and go to:
```
http://localhost:8000
```
You will see the LlamaIndex chat app UI, where you can interact with the agent.
@@ -0,0 +1,14 @@
from typing import Optional
from llama_index.core.agent.workflow import AgentWorkflow
from llama_index.core.settings import Settings
from llama_index.llms.openai import OpenAI
from llama_index.server.api.models import ChatRequest
def create_workflow(chat_request: Optional[ChatRequest] = None) -> AgentWorkflow:
return AgentWorkflow.from_tools_or_functions(
tools_or_functions=[],
llm=Settings.llm or OpenAI(model="gpt-4o-mini"),
system_prompt="You are a helpful assistant that can tell a joke about Llama.",
)
@@ -0,0 +1,29 @@
from app.workflow import create_workflow
from fastapi import FastAPI
from llama_index.server import LlamaIndexServer, UIConfig
def create_app() -> FastAPI:
app = LlamaIndexServer(
workflow_factory=create_workflow,
ui_config=UIConfig(
app_title="Artifact",
starter_questions=[
"Tell me a funny joke.",
"Tell me some jokes about AI.",
],
component_dir="components",
dev_mode=True, # To show the dev UI, should disable this in production
),
)
return app
app = create_app()
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
@@ -1,4 +1,9 @@
from llama_index.server.api.routers.chat import chat_router
from llama_index.server.api.routers.ui import custom_components_router
from llama_index.server.api.routers.dev import dev_router
__all__ = ["chat_router", "custom_components_router"]
__all__ = [
"chat_router",
"custom_components_router",
"dev_router",
]
@@ -0,0 +1,102 @@
import os
import tempfile
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from llama_index.server.settings import server_settings
from llama_index.server.utils.workflow_validation import validate_workflow_file
class WorkflowFile(BaseModel):
last_modified: int
file_path: str = Field(
default="app/workflow.py",
description="Relative path to the workflow file",
)
content: str
class WorkflowFileUpdate(BaseModel):
content: str
file_path: str = Field(
default="app/workflow.py",
description="Relative path to the workflow file",
)
class WorkflowValidationResult(BaseModel):
valid: bool
error: str
def dev_router() -> APIRouter:
# Use a prefix here to avoid conflicts with other routers
# but we probably don't need to do this
router = APIRouter(prefix="/dev", tags=["dev"])
default_workflow_file_path = "app/workflow.py"
@router.get("/files/workflow")
async def get_workflow_file() -> WorkflowFile:
"""
Fetch the current workflow code
"""
# Check if the file exists
if not os.path.exists(default_workflow_file_path):
raise HTTPException(
status_code=400,
detail="Dev mode is currently in beta. It only supports updating workflow file at 'app/workflow.py'",
)
stat = os.stat(default_workflow_file_path)
with open(default_workflow_file_path, "r") as f:
return WorkflowFile(
last_modified=int(stat.st_mtime),
file_path=default_workflow_file_path,
content=f.read(),
)
@router.post("/files/workflow/validate")
async def validate_workflow(file: WorkflowFileUpdate) -> WorkflowValidationResult:
"""
Validate the current workflow code
"""
try:
if file.file_path != default_workflow_file_path:
raise HTTPException(
status_code=400, detail=f"Updating {file.file_path} is not allowed"
)
validate_workflow_file(
workflow_content=file.content,
factory_signature=server_settings.workflow_factory_signature,
)
return WorkflowValidationResult(valid=True, error="")
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("/files/workflow")
async def put_workflow_file(update: WorkflowFileUpdate) -> None:
"""
Update the current workflow code
"""
# Validations
if update.file_path != default_workflow_file_path:
raise HTTPException(
status_code=400, detail=f"Updating {update.file_path} is not allowed"
)
with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) as tmp:
tmp.write(update.content)
tmp_path = tmp.name
try:
# Validate workflow file using the actual callable name from the workflow_factory
factory_func_name = server_settings.workflow_factory_signature
validate_workflow_file(
workflow_path=tmp_path, factory_signature=factory_func_name
)
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
# If all checks pass, overwrite the real file
with open(default_workflow_file_path, "w") as f:
f.write(update.content)
return router
@@ -10,7 +10,11 @@ from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Field
from llama_index.core.workflow import Workflow
from llama_index.server.api.routers import chat_router, custom_components_router
from llama_index.server.api.routers import (
chat_router,
custom_components_router,
dev_router,
)
from llama_index.server.chat_ui import download_chat_ui
from llama_index.server.settings import server_settings
@@ -33,6 +37,9 @@ class UIConfig(BaseModel):
component_dir: Optional[str] = Field(
default=None, description="The directory to custom UI components code"
)
dev_mode: bool = Field(
default=False, description="Whether to enable the UI dev mode"
)
def get_config_content(self) -> str:
return json.dumps(
@@ -46,6 +53,7 @@ class UIConfig(BaseModel):
"COMPONENTS_API": f"{server_settings.api_url}/components"
if self.component_dir
else None,
"DEV_MODE": self.dev_mode,
},
indent=2,
)
@@ -100,24 +108,33 @@ class LlamaIndexServer(FastAPI):
server_settings.set_url(server_url)
if api_prefix:
server_settings.set_api_prefix(api_prefix)
if self.use_default_routers:
self.add_default_routers()
server_settings.set_workflow_factory(workflow_factory.__name__)
if str(env).lower() == "dev":
self.allow_cors("*")
if self.ui_config.enabled is None:
self.ui_config.enabled = True
else:
if self.ui_config.enabled and self.ui_config.dev_mode:
raise ValueError(
"UI dev mode requires the environment variable for LlamaIndexServer to be set to 'dev' and start the FastAPI app in dev mode."
)
if self.ui_config.enabled is None:
self.ui_config.enabled = False
# Routers
if self.use_default_routers:
self.add_default_routers()
# Should mount ui at the end
if self.ui_config.enabled:
self.mount_ui()
# Default routers
def add_default_routers(self) -> None:
self.add_chat_router()
if self.ui_config.enabled and self.ui_config.dev_mode:
self.include_router(dev_router(), prefix=server_settings.api_prefix)
self.mount_data_dir()
self.mount_output_dir()
@@ -11,6 +11,10 @@ class ServerSettings(BaseSettings):
default="/api",
description="The prefix for the API endpoints",
)
workflow_factory_signature: str = Field(
default="",
description="The signature of the workflow factory function",
)
@property
def file_server_url_prefix(self) -> str:
@@ -40,6 +44,9 @@ class ServerSettings(BaseSettings):
self.api_prefix = v
self.validate_api_prefix(v) # type: ignore
def set_workflow_factory(self, v: str) -> None:
self.workflow_factory_signature = v
class Config:
env_file_encoding = "utf-8"
@@ -0,0 +1,54 @@
"""
Utilities for validating workflow.py files (syntax and import checks only).
"""
import ast
import importlib.util
from typing import Optional
def validate_workflow_file(
workflow_path: Optional[str] = None,
workflow_content: Optional[str] = None,
factory_signature: Optional[str] = None,
) -> None:
"""
Validate that the workflow file is syntactically correct, can be imported, and defines a callable factory function with the given name.
Raises an exception if invalid.
"""
if workflow_path is None and workflow_content is None:
raise ValueError("Either workflow_path or workflow_content must be provided")
# 1. Syntax check
if workflow_path is not None:
with open(workflow_path, "r") as f:
content = f.read()
else:
if workflow_content is None:
raise ValueError(
"workflow_content must be provided if workflow_path is not specified"
)
content = workflow_content
try:
ast.parse(content)
except SyntaxError as e:
raise ValueError(f"Syntax error in workflow: {e}")
# 2. Import check (will catch missing modules, etc.)
spec = importlib.util.spec_from_file_location("workflow", workflow_path)
if spec is None or spec.loader is None:
raise ValueError(f"Could not load module specification for {workflow_path}")
mod = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(mod)
except Exception as e:
raise ValueError(f"Import error: {e}")
# 3. Contract validation: require the given factory function name
if factory_signature:
if not hasattr(mod, factory_signature):
raise ValueError(f"Missing required function: '{factory_signature}'")
obj = getattr(mod, factory_signature)
if not callable(obj):
raise ValueError(f"'{factory_signature}' is not callable")
+2678 -2677
View File
File diff suppressed because it is too large Load Diff