Compare commits

...

18 Commits

Author SHA1 Message Date
github-actions[bot] bbae802bed Release 0.2.2 (#638)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-22 17:17:34 +07:00
Thuc Pham 25fba4381b refactor: migrate to Nextjs Route Handler (#625) 2025-05-22 11:47:24 +07:00
Huu Le d0618fa2fa add changeset (#639) 2025-05-21 14:31:41 +07:00
Huu Le f3fe3ffc9b fix: llamacloud generate not working and re-add tests (#636) 2025-05-21 12:49:44 +07:00
Thuc Pham 6f75d4ab6e fix: unsupported language in code gen workflow (#633) 2025-05-21 12:31:11 +07:00
Huu Le 3242738fe4 chore: Fix Python e2e tests (#632) 2025-05-21 11:30:02 +07:00
Sourabh Kondapaka 17538eb0dd Fixed bug when traceloop observability is chosen but does not install the latest version (#603)
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2025-05-20 11:48:32 +07:00
github-actions[bot] d3772cb4a2 Release 0.5.15 (#629)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-16 16:33:35 +07:00
Huu Le 527075c086 enable dev mode that allows updating code directly in the UI (#624)
* Enable dev mode that allows updating code directly in the UI

* bump server packages
2025-05-16 16:05:56 +07:00
github-actions[bot] fb7d4da149 chore(release): bump llama-index-server version to 0.1.16 (#587)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-05-16 15:16:57 +07:00
leehuwuj 5c35b194bb bump chat ui version 2025-05-16 14:53:57 +07:00
github-actions[bot] 85e5e7e662 Release 0.5.14 (#608)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-16 14:41:46 +07:00
Huu Le 58362542c0 chore: add workflow contract for server (#623) 2025-05-16 14:26:24 +07:00
Thuc Pham 6f44185f68 fix: init messages memory in start event handler (#627) 2025-05-16 12:45:35 +07:00
Thuc Pham afe9e9fc16 fix: nodemon should ignore temp file (#622) 2025-05-15 15:33:24 +07:00
Thuc Pham 1b5a519f13 chore: improve dev experience with nodemon (#621) 2025-05-15 15:18:12 +07:00
Huu Le f072308d03 feat: Add dev mode (#610)
* Add UI components and static assets for chat interface

* feat: Add simple chat app example with FastAPI integration

* fix: update default workflow file path and improve error handling

* update doc

* change to file_path

* include changes from #614

* fix mypy

* support devmode for backend ts server

* Revert "support devmode for backend ts server"

This reverts commit bd943fd8c1.

* fix: polling should work when server not yet started

* bump chat-ui to fix syntax highlight issue

* fix: missing language for code editor

* enhance UI with shadow overlay

* enhance doc

* fix minor UI bugs

* enhance doc

* remove unessesary debug log

* fix wrong check

* increase delay time before trigger polling

* feat: support dev mode for backend ts server (#616)

* feat: support dev mode for backend ts server

* update message

* validate typescript file

* fix: format

* use temp file to avoid server restart

* fix format

* use npx tsc

* remove typescript deps

---------

Co-authored-by: thucpn <thucsh2@gmail.com>
Co-authored-by: Thuc Pham <51660321+thucpn@users.noreply.github.com>
2025-05-15 14:56:08 +07:00
Huu Le 1df8cfbdc2 refactor: split artifacts use case into document generator and code generator (#617)
* split artifacts use case to code generator and document generator

* add changeset

* fix package version

* fix typing

* bump openai

* fix package

* fix typing

* fix: improve type handling and clean up UI event component

- Removed unnecessary string conversion for userInput in code_generator and deep_research workflows.
- Updated userRequest type to MessageContent for better type safety.
- Cleaned up the UI event component by removing redundant indicatorClassName logic.

* docs: word smith

* better handler typing

* refactor: remove redundant UI event handling in workflows

---------

Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2025-05-15 14:22:21 +07:00
82 changed files with 4580 additions and 2997 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"create-llama": patch
---
Fix typing check issue
-5
View File
@@ -1,5 +0,0 @@
---
"create-llama": patch
---
fix chromadb dependency issue
-5
View File
@@ -1,5 +0,0 @@
---
"create-llama": patch
---
fix: remove dead generated ai code
-5
View File
@@ -1,5 +0,0 @@
---
"create-llama": patch
---
Deprecate pro mode
+18 -2
View File
@@ -79,7 +79,7 @@ jobs:
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-python-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}
name: playwright-report-python-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}-${{ matrix.template-types }}
path: packages/create-llama/playwright-report/
overwrite: true
retention-days: 30
@@ -136,6 +136,21 @@ 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:
@@ -144,12 +159,13 @@ jobs:
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 }}
name: playwright-report-typescript-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}-node${{ matrix.node-version }}-${{ matrix.template-types }}
path: packages/create-llama/playwright-report/
overwrite: true
retention-days: 30
+24
View File
@@ -1,5 +1,29 @@
# 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
@@ -18,7 +18,8 @@ const useCases: TemplateUseCase[] = [
"agentic_rag",
"deep_research",
"financial_report",
"artifacts",
"code_generator",
"document_generator",
];
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
@@ -12,21 +12,30 @@ import { createTestDir, runCreateLlama, type AppType } from "../utils";
const templateFramework: TemplateFramework = process.env.FRAMEWORK
? (process.env.FRAMEWORK as TemplateFramework)
: "fastapi";
const dataSource: string = "--example-file";
const dataSource: string = process.env.DATASOURCE
? (process.env.DATASOURCE as string)
: "--example-file";
const llamaCloudProjectName = "create-llama";
const llamaCloudIndexName = "e2e-test";
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 = ["financial_report", "agentic_rag", "deep_research"];
const templateUseCases = [
"agentic_rag",
"financial_report",
"deep_research",
"code_generator",
];
for (const useCase of templateUseCases) {
test.describe(`Test use case ${useCase} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
test.skip(
process.platform !== "linux" ||
process.env.DATASOURCE === "--no-files" ||
templateFramework === "express",
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;
@@ -48,6 +57,9 @@ for (const useCase of templateUseCases) {
templateUI,
appType,
useCase,
llamaCloudProjectName,
llamaCloudIndexName,
useLlamaParse,
});
name = result.projectName;
appProcess = result.appProcess;
@@ -23,7 +23,8 @@ const useCases: TemplateUseCase[] = [
"agentic_rag",
"deep_research",
"financial_report",
"artifacts",
"code_generator",
"document_generator",
];
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
@@ -82,7 +83,7 @@ test.describe("Test resolve TS dependencies", () => {
});
});
// Skipping llamacloud for the use case doesn't use index.
if (useCase !== "artifacts") {
if (useCase !== "code_generator" && useCase !== "document_generator") {
test(`llamaParse - ${optionDescription}`, async () => {
await runTest({
templateType: templateType,
+9 -1
View File
@@ -18,6 +18,7 @@ import {
ModelConfig,
TemplateDataSource,
TemplateFramework,
TemplateUseCase,
TemplateVectorDB,
} from "./types";
import { installTSTemplate } from "./typescript";
@@ -60,6 +61,7 @@ async function generateContextData(
vectorDb?: TemplateVectorDB,
llamaCloudKey?: string,
useLlamaParse?: boolean,
useCase?: TemplateUseCase,
) {
if (packageManager) {
const runGenerate = `${cyan(
@@ -96,7 +98,12 @@ async function generateContextData(
}
} else {
console.log(`Running ${runGenerate} to generate the context data.`);
await callPackageManager(packageManager, true, ["run", "generate"]);
const shouldRunGenerate =
useCase !== "code_generator" && useCase !== "document_generator"; // Artifact use case doesn't use index.
if (shouldRunGenerate) {
await callPackageManager(packageManager, true, ["run", "generate"]);
}
return;
}
}
@@ -224,6 +231,7 @@ export const installTemplate = async (
props.vectorDb,
props.llamaCloudKey,
props.useLlamaParse,
props.useCase,
);
}
+2 -1
View File
@@ -267,7 +267,7 @@ const getAdditionalDependencies = (
if (observability === "traceloop") {
dependencies.push({
name: "traceloop-sdk",
version: ">=0.15.11,<0.16.0",
version: ">=0.15.11",
});
}
if (observability === "llamatrace") {
@@ -677,6 +677,7 @@ export const installPythonTemplate = async ({
dataSources,
tools,
template,
observability,
);
await addDependencies(root, addOnDependencies);
+2 -1
View File
@@ -58,7 +58,8 @@ export type TemplateUseCase =
| "extractor"
| "contract_review"
| "agentic_rag"
| "artifacts";
| "code_generator"
| "document_generator";
// Config for both file and folder
export type FileSourceConfig =
| {
+13 -3
View File
@@ -77,7 +77,7 @@ const installLlamaIndexServerTemplate = async ({
}
// Simplify use case code
if (useCase === "artifacts") {
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"));
@@ -387,7 +387,7 @@ const providerDependencies: {
[key in ModelProvider]?: Record<string, string>;
} = {
openai: {
"@llamaindex/openai": "^0.2.0",
"@llamaindex/openai": "~0.4.0",
},
gemini: {
"@llamaindex/google": "^0.2.0",
@@ -513,7 +513,7 @@ async function updatePackageJson({
if (backend) {
packageJson.dependencies = {
...packageJson.dependencies,
"@llamaindex/readers": "^3.0.0",
"@llamaindex/readers": "~3.1.4",
};
if (vectorDb && vectorDb in vectorDbDependencies) {
@@ -543,6 +543,16 @@ 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
@@ -1,6 +1,6 @@
{
"name": "create-llama",
"version": "0.5.13",
"version": "0.5.16",
"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: "",
llamaCloudKey: undefined,
useLlamaParse: false,
communityProjectConfig: undefined,
llamapack: "",
+18 -7
View File
@@ -10,7 +10,8 @@ type AppType =
| "agentic_rag"
| "financial_report"
| "deep_research"
| "artifacts";
| "code_generator"
| "document_generator";
type SimpleAnswers = {
appType: AppType;
@@ -47,10 +48,14 @@ 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: "Artifacts",
value: "artifacts",
description:
"Build your own Vercel's v0 or OpenAI's canvas-styled UI.",
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.",
},
],
},
@@ -76,7 +81,7 @@ export const askSimpleQuestions = async (
);
language = newLanguage;
if (appType !== "artifacts") {
if (appType !== "code_generator" && appType !== "document_generator") {
const { useLlamaCloud: newUseLlamaCloud } = await prompts(
{
type: "toggle",
@@ -153,7 +158,13 @@ const convertAnswers = async (
tools: [],
modelConfig: MODEL_GPT41,
},
artifacts: {
code_generator: {
template: "llamaindexserver",
dataSources: [],
tools: [],
modelConfig: MODEL_GPT41,
},
document_generator: {
template: "llamaindexserver",
dataSources: [],
tools: [],
@@ -12,7 +12,7 @@ dependencies = [
"llama-index>=0.12.1",
"llama-parse>=0.6.21,<0.7.0",
"cachetools>=5.3.3",
"reflex>=0.6.2.post1",
"reflex==0.7.10",
]
[project.scripts]
@@ -13,7 +13,7 @@ dependencies = [
"llama-index>=0.12.1",
"llama-parse>=0.6.21,<0.7.0",
"cachetools>=5.3.3",
"reflex>=0.6.2.post1",
"reflex==0.7.10",
]
[project.scripts]
@@ -0,0 +1,132 @@
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} />;
}
@@ -1,15 +0,0 @@
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
@@ -0,0 +1,65 @@
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!
@@ -6,6 +6,7 @@ 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,
@@ -26,6 +27,15 @@ 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
@@ -33,12 +33,9 @@ You can configure [LLM model](https://docs.llamaindex.ai/en/stable/module_guides
## Use Case
We have prepared two artifact workflows:
AI-powered document generator that can help you generate documents with a chat interface and simple markdown editor.
- [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.
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:
@@ -4,6 +4,7 @@ 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 (
@@ -26,6 +27,15 @@ 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
@@ -1,12 +0,0 @@
import { createCodeArtifactWorkflow, UIEventSchema } from "./code-workflow";
// import { createDocumentArtifactWorkflow, UIEventSchema } from "./doc-workflow";
export const workflowFactory = async (reqBody: any) => {
// Uncomment the import and change to createDocumentArtifactWorkflow to use the document workflow
const workflow = createCodeArtifactWorkflow(reqBody);
return workflow;
};
// Re-export the UIEventSchema for generating the UI by `pnpm generate:ui` command
export { UIEventSchema };
@@ -30,12 +30,9 @@ npm run generate:ui
## Use Case
We have prepared two artifact workflows:
AI-powered code generator that can help you generate app with a chat interface, code editor and app preview.
- [Code Workflow](app/code_workflow.ts): To generate code and display it in the UI like Vercel's v0.
- [Document Workflow](app/document_workflow.ts): Generate and update a document like OpenAI's canvas.
Modify the factory method in [`workflow.ts`](app/workflow.ts) to decide which artifact workflow to use. Without any changes the Code Workflow is used.
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:
@@ -1,5 +1,5 @@
import { extractLastArtifact } from "@llamaindex/server";
import { ChatMemoryBuffer, LLM, Settings } from "llamaindex";
import { ChatMemoryBuffer, MessageContent, Settings } from "llamaindex";
import {
agentStreamEvent,
@@ -40,7 +40,7 @@ export const UIEventSchema = z.object({
export type UIEvent = z.infer<typeof UIEventSchema>;
const planEvent = workflowEvent<{
userInput: string;
userInput: MessageContent;
context?: string | undefined;
}>();
@@ -65,34 +65,33 @@ const artifactEvent = workflowEvent<{
};
}>();
export function createCodeArtifactWorkflow(reqBody: any, llm?: LLM) {
if (!llm) {
llm = Settings.llm;
}
export function workflowFactory(reqBody: any) {
const llm = Settings.llm;
const { withState, getContext } = createStatefulMiddleware(() => {
return {
memory: new ChatMemoryBuffer({
llm,
chatHistory: reqBody.chatHistory,
}),
memory: new ChatMemoryBuffer({ llm }),
lastArtifact: extractLastArtifact(reqBody),
};
});
const workflow = withState(createWorkflow());
workflow.handle([startAgentEvent], async ({ data: { userInput } }) => {
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.put({
role: "user",
content: userInput,
});
state.memory.set(chatHistory);
state.memory.put({ role: "user", content: userInput });
return planEvent.with({
userInput,
userInput: userInput,
context: state.lastArtifact
? JSON.stringify(state.lastArtifact)
: undefined,
});
});
@@ -177,15 +176,6 @@ ${user_msg}
throw new Error("No JSON block found in the response.");
}
const requirement = RequirementSchema.parse(JSON.parse(jsonBlock[1]));
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
state: "generate",
requirement: requirement.requirement,
},
}),
);
state.memory.put({
role: "assistant",
content: `The plan for next step: \n${response.text}`,
@@ -10,12 +10,14 @@ import {
import {
ChatMemoryBuffer,
LlamaCloudIndex,
MessageContent,
Metadata,
MetadataMode,
NodeWithScore,
PromptTemplate,
Settings,
VectorStoreIndex,
extractText,
} from "llamaindex";
import { randomUUID } from "node:crypto";
import { z } from "zod";
@@ -153,7 +155,7 @@ export function getWorkflow(index: VectorStoreIndex | LlamaCloudIndex) {
chatHistory: [],
}),
contextNodes: [] as NodeWithScore<Metadata>[],
userRequest: "",
userRequest: "" as MessageContent,
totalQuestions: 0,
researchResults: [] as ResearchResult[],
};
@@ -178,7 +180,7 @@ export function getWorkflow(index: VectorStoreIndex | LlamaCloudIndex) {
}),
);
const retrievedNodes = await retriever.retrieve(userInput);
const retrievedNodes = await retriever.retrieve({ query: userInput });
sendEvent(toSourceEvent(retrievedNodes));
sendEvent(
@@ -349,7 +351,7 @@ const createResearchPlan = async (
memory: ChatMemoryBuffer,
contextStr: string,
enhancedPrompt: string,
userRequest: string,
userRequest: MessageContent,
) => {
const chatHistory = await memory.getMessages();
@@ -361,7 +363,7 @@ const createResearchPlan = async (
context_str: contextStr,
conversation_context: conversationContext,
enhanced_prompt: enhancedPrompt,
user_request: userRequest,
user_request: extractText(userRequest),
});
const responseFormat = z.object({
@@ -0,0 +1,53 @@
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,5 +1,5 @@
import { extractLastArtifact } from "@llamaindex/server";
import { ChatMemoryBuffer, LLM, Settings } from "llamaindex";
import { ChatMemoryBuffer, MessageContent, Settings } from "llamaindex";
import {
agentStreamEvent,
@@ -40,7 +40,7 @@ export const UIEventSchema = z.object({
export type UIEvent = z.infer<typeof UIEventSchema>;
const planEvent = workflowEvent<{
userInput: string;
userInput: MessageContent;
context?: string | undefined;
}>();
@@ -68,32 +68,28 @@ const artifactEvent = workflowEvent<{
};
}>();
export function createDocumentArtifactWorkflow(reqBody: any, llm?: LLM) {
if (!llm) {
llm = Settings.llm;
}
export function workflowFactory(reqBody: any) {
const llm = Settings.llm;
const { withState, getContext } = createStatefulMiddleware(() => {
return {
memory: new ChatMemoryBuffer({
llm,
chatHistory: reqBody.chatHistory,
}),
memory: new ChatMemoryBuffer({ llm }),
lastArtifact: extractLastArtifact(reqBody),
};
});
const workflow = withState(createWorkflow());
workflow.handle([startAgentEvent], async ({ data: { userInput } }) => {
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.put({
role: "user",
content: userInput,
});
state.memory.set(chatHistory);
state.memory.put({ role: "user", content: userInput });
return planEvent.with({
userInput,
context: state.lastArtifact
@@ -175,15 +171,6 @@ export function createDocumentArtifactWorkflow(reqBody: any, llm?: LLM) {
role: "assistant",
content: `Planning for the document generation: \n${response.text}`,
});
sendEvent(
uiEvent.with({
type: "ui_event",
data: {
state: "generate",
requirement: requirement.requirement,
},
}),
);
return generateArtifactEvent.with({
requirement,
});
@@ -12,11 +12,12 @@ from llama_index.server.services.llamacloud.generate import (
load_to_llamacloud,
)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
def generate_datasource():
def generate_index():
init_settings()
logger.info("Generate index for the provided data")
@@ -27,5 +28,26 @@ def generate_datasource():
load_to_llamacloud(index, logger=logger)
if __name__ == "__main__":
generate_datasource()
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)
@@ -1,7 +1,8 @@
import { generateEventComponent } from "@llamaindex/server";
import * as dotenv from "dotenv";
import "dotenv/config";
import * as fs from "fs/promises";
import { LLamaCloudFileService } from "llamaindex";
import { LLamaCloudFileService, OpenAI } from "llamaindex";
import * as path from "path";
import { getIndex } from "./app/data";
import { initSettings } from "./app/settings";
@@ -88,7 +89,7 @@ async function loadAndIndex() {
console.log(`Successfully uploaded documents to LlamaCloud!`);
}
(async () => {
async function generateDatasource() {
try {
checkRequiredEnvVars();
initSettings();
@@ -97,4 +98,39 @@ async function loadAndIndex() {
} 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,8 +1,12 @@
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")
@@ -17,8 +17,10 @@ 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.15,<0.2.0",
"llama-index-server>=0.1.16,<0.2.0",
]
[project.optional-dependencies]
@@ -0,0 +1,6 @@
{
"watch": ["src/**/*.ts"],
"exec": "nodemon --exec tsx src/index.ts",
"ext": "js ts",
"ignore": ["src/app/workflow_*.ts"]
}
@@ -5,21 +5,22 @@
"generate": "tsx src/generate.ts datasource",
"generate:datasource": "tsx src/generate.ts datasource",
"generate:ui": "tsx src/generate.ts ui",
"dev": "tsx watch src/index.ts",
"dev": "nodemon",
"start": "tsx src/index.ts"
},
"dependencies": {
"@llamaindex/openai": "0.2.0",
"@llamaindex/server": "0.2.0",
"@llamaindex/workflow": "1.1.1",
"@llamaindex/tools": "0.0.9",
"@llamaindex/openai": "~0.4.0",
"@llamaindex/server": "~0.2.1",
"@llamaindex/workflow": "~1.1.3",
"@llamaindex/tools": "~0.0.11",
"llamaindex": "~0.11.0",
"dotenv": "^16.4.7",
"zod": "^3.23.8",
"llamaindex": "0.10.5"
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20.10.3",
"tsx": "^4.7.2",
"typescript": "^5.3.2"
"typescript": "^5.3.2",
"nodemon": "^3.1.10"
}
}
@@ -3,7 +3,7 @@ import { Settings } from "llamaindex";
export function initSettings() {
Settings.llm = new OpenAI({
model: "gpt-4o-mini",
model: "gpt-4.1",
});
Settings.embedModel = new OpenAIEmbedding({
model: "text-embedding-3-small",
@@ -1,14 +1,10 @@
import "dotenv/config";
import { SimpleDirectoryReader } from "@llamaindex/readers/directory";
import {
OpenAI,
storageContextFromDefaults,
VectorStoreIndex,
} from "llamaindex";
import { storageContextFromDefaults, VectorStoreIndex } from "llamaindex";
import { initSettings } from "./app/settings";
import fs from "fs";
import { generateEventComponent } from "@llamaindex/server";
import { UIEventSchema } from "./app/workflow";
import { OpenAI } from "@llamaindex/openai";
async function generateDatasource() {
console.log(`Generating storage context...`);
@@ -30,6 +26,14 @@ 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,5 +10,6 @@ new LlamaIndexServer({
uiConfig: {
appTitle: "LlamaIndex App",
componentsDir: "components",
devMode: true,
},
}).start();
+5 -1
View File
@@ -1 +1,5 @@
server/
# 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/
+13
View File
@@ -1,5 +1,18 @@
# @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
+128 -5
View File
@@ -4,10 +4,10 @@ LlamaIndexServer is a Next.js-based application that allows you to quickly launc
## Features
- Serving a workflow as a chatbot
- 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
- 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,9 +21,11 @@ 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()] });
const createWorkflow = () => agent({ tools: [wiki()], llm: openai("gpt-4o") });
new LlamaIndexServer({
workflow: createWorkflow,
@@ -34,6 +36,8 @@ 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:
@@ -54,16 +58,75 @@ 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
- `workflow`: A callable function that creates a workflow instance for each request. See [Workflow factory contract](#workflow-factory-contract) for more details.
- `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.
@@ -137,6 +200,66 @@ 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
+1 -1
View File
@@ -6,7 +6,7 @@ This directory contains examples of how to use the LlamaIndex Server.
```bash
export OPENAI_API_KEY=your_openai_api_key
npx tsx simple-workflow/calculator.ts
pnpm run dev
```
## Open browser at http://localhost:3000
@@ -39,5 +39,5 @@ new LlamaIndexServer({
appTitle: "LlamaIndex App",
starterQuestions: ["What is the color of the dog?"],
},
port: 4100,
port: 3000,
}).start();
@@ -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: 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
@@ -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: 3000,
}).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`,
}),
],
});
};
+2 -1
View File
@@ -4,7 +4,7 @@
"private": true,
"scripts": {
"typecheck": "tsc --noEmit",
"dev": "tsx simple-workflow/calculator.ts"
"dev": "nodemon --exec tsx simple-workflow/calculator.ts"
},
"dependencies": {
"@llamaindex/openai": "^0.2.0",
@@ -18,6 +18,7 @@
},
"devDependencies": {
"@types/node": "^20.10.3",
"nodemon": "^3.1.10",
"tsx": "^4.7.2",
"typescript": "^5.3.2"
}
@@ -20,5 +20,5 @@ new LlamaIndexServer({
appTitle: "Calculator",
starterQuestions: ["1 + 1", "2 + 2"],
},
port: 4000,
port: 3000,
}).start();
@@ -0,0 +1,32 @@
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 },
);
}
}
@@ -0,0 +1,77 @@
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,20 +1,19 @@
import fs from "fs";
import type { IncomingMessage, ServerResponse } from "http";
import { NextRequest, NextResponse } from "next/server";
import path from "path";
import { promisify } from "util";
import { sendJSONResponse } from "../utils/request";
export const getComponents = async (
req: IncomingMessage,
res: ServerResponse,
componentsDir: string,
) => {
export async function GET(request: NextRequest) {
const params = request.nextUrl.searchParams;
const componentsDir = params.get("componentsDir") || "components";
try {
const exists = await promisify(fs.exists)(componentsDir);
if (!exists) {
return sendJSONResponse(res, 404, {
error: "Components directory not found",
});
return NextResponse.json(
{ error: "Components directory not found" },
{ status: 404 },
);
}
const files = await promisify(fs.readdir)(componentsDir);
@@ -40,12 +39,15 @@ export const getComponents = async (
}),
);
sendJSONResponse(res, 200, components);
return NextResponse.json(components, { status: 200 });
} catch (error) {
console.error("Error reading components:", error);
sendJSONResponse(res, 500, { error: "Failed to read components" });
return NextResponse.json(
{ error: "Failed to read components" },
{ status: 500 },
);
}
};
}
function filterDuplicateComponents(files: string[]) {
const compMap = new Map<string, string>();
@@ -0,0 +1,96 @@
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,
};
}
@@ -0,0 +1,24 @@
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,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,273 @@
"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": "ES2017"
"target": "ES2022"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
+17 -13
View File
@@ -1,7 +1,7 @@
{
"name": "@llamaindex/server",
"description": "LlamaIndex Server",
"version": "0.2.0",
"version": "0.2.2",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
@@ -27,21 +27,25 @@
"directory": "packages/server"
},
"scripts": {
"clean": "rm -rf ./dist ./server next/.next next/out",
"dev": "bunchee --watch",
"clean": "rm -rf ./dist ./server next/.next next/out ./temp",
"prebuild": "pnpm clean",
"build": "bunchee",
"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"
"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"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/babel__standalone": "^7.1.9",
"@types/babel__traverse": "^7.20.7",
"llamaindex": "0.10.2",
"llamaindex": "~0.11.0",
"postcss": "^8.5.3",
"postcss-cli": "^11.0.1",
"tailwindcss": "^4",
@@ -55,7 +59,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",
@@ -103,9 +107,9 @@
"vaul": "^1.1.2"
},
"peerDependencies": {
"@llamaindex/env": "^0.1.29",
"@llamaindex/workflow": "^1.1.0",
"llamaindex": "^0.10.2",
"@llamaindex/env": "~0.1.30",
"@llamaindex/workflow": "~1.1.3",
"llamaindex": "~0.11.0",
"zod": "^3.24.2",
"zod-to-json-schema": "^3.23.3"
},
+4 -2
View File
@@ -155,10 +155,12 @@ export function extractAllArtifacts(messages: Message[]): Artifact[] {
const artifacts =
message.annotations
?.filter(
(annotation) =>
(
annotation,
): annotation is z.infer<typeof artifactAnnotationSchema> =>
artifactAnnotationSchema.safeParse(annotation).success,
)
.map((artifact) => artifact as Artifact) ?? [];
.map((annotation) => annotation.data as Artifact) ?? [];
allArtifacts.push(...artifacts);
}
-30
View File
@@ -1,30 +0,0 @@
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",
});
}
};
-23
View File
@@ -1,23 +0,0 @@
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" });
}
};
+10 -18
View File
@@ -7,10 +7,8 @@ import path from "path";
import { parse } from "url";
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 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 +42,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 +51,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);
@@ -71,33 +71,25 @@ 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"
) {
return getComponents(req, res, this.componentsDir);
}
if (
getEnv("LLAMA_CLOUD_API_KEY") &&
pathname === "/api/chat/config/llamacloud" &&
req.method === "GET"
) {
return getLlamaCloudConfig(req, res);
query.componentsDir = this.componentsDir;
}
const handle = this.app.getRequestHandler();
handle(req, res, parsedUrl);
handle(req, res, { ...parsedUrl, query });
});
server.listen(this.port, () => {
+1
View File
@@ -17,6 +17,7 @@ export type UIConfig = {
starterQuestions?: string[];
componentsDir?: string;
llamaCloudIndexSelector?: boolean;
devMode?: boolean;
};
export type LlamaIndexServerOptions = NextAppOptions & {
+10
View File
@@ -0,0 +1,10 @@
{
"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"]
}
+243 -32
View File
@@ -181,14 +181,14 @@ 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
specifier: ~0.1.30
version: 0.1.30
'@llamaindex/workflow':
specifier: ^1.1.0
version: 1.1.0(@llamaindex/core@0.6.2)(@llamaindex/env@0.1.29)(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)
specifier: ~1.1.3
version: 1.1.3(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)
'@radix-ui/react-accordion':
specifier: ^1.2.3
version: 1.2.8(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -341,8 +341,8 @@ importers:
specifier: ^7.20.7
version: 7.20.7
llamaindex:
specifier: 0.10.2
version: 0.10.2(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(zod@3.24.3)
specifier: ~0.11.0
version: 0.11.1(@llama-flow/core@0.4.1(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3))(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(zod@3.24.3)
postcss:
specifier: ^8.5.3
version: 8.5.3
@@ -378,7 +378,7 @@ importers:
version: 0.0.4(openapi-types@12.1.3)
'@llamaindex/workflow':
specifier: 1.1.0
version: 1.1.0(@llamaindex/core@0.6.2)(@llamaindex/env@0.1.29)(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)
version: 1.1.0(@llamaindex/core@0.6.2)(@llamaindex/env@0.1.29)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)
dotenv:
specifier: ^16.4.7
version: 16.5.0
@@ -392,6 +392,9 @@ importers:
'@types/node':
specifier: ^20.10.3
version: 20.17.30
nodemon:
specifier: ^3.1.10
version: 3.1.10
tsx:
specifier: ^4.7.2
version: 4.19.3
@@ -1177,8 +1180,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
@@ -1188,12 +1191,22 @@ packages:
'@llamaindex/core': 0.6.2
'@llamaindex/env': 0.1.29
'@llamaindex/cloud@4.0.9':
resolution: {integrity: sha512-+tIPghKAzXFvKEPmbJZpcFJgzmxLq/HfSKlJAIinRdrSDBC489oAJsqkt6StxmlFUhWS3ATOtTWpqWZIQ9HtzA==}
peerDependencies:
'@llama-flow/core': ^0.4.1
'@llamaindex/core': 0.6.6
'@llamaindex/env': 0.1.30
'@llamaindex/core@0.6.1':
resolution: {integrity: sha512-OsY8JV2nbfWYjp24HEKDbtM6vOa0TNUreMjDMxUkw5Wet1TxKxjVq0YHY01RoqMbbTJ+Jsxa42FhzTaDV2gMkg==}
'@llamaindex/core@0.6.2':
resolution: {integrity: sha512-e0NH7X1C8yq9sCqj1Ys9jL9rITCXBKfZwmKnTHYxpXorPeuNmlFmuoogahJTwLNsILBDTOVAUexO88SqmAFUrA==}
'@llamaindex/core@0.6.6':
resolution: {integrity: sha512-UplAAxsv3LnU1EP2kPuyqDtRQmQqYtsI00rVDeexOwgqwNTfGddI0yPRrFTBSNe0q2ttBuPsB7D3M7j4UY3yXQ==}
'@llamaindex/env@0.1.29':
resolution: {integrity: sha512-GoITt+QLDNIhu2i1sGsPH8tHH13Gxp4i4ofMFlefrHagytvF761MG7nvTAQVIqP9kxBYk4dnSQ3lQnVPFAfMZQ==}
peerDependencies:
@@ -1205,6 +1218,17 @@ packages:
gpt-tokenizer:
optional: true
'@llamaindex/env@0.1.30':
resolution: {integrity: sha512-y6kutMcCevzbmexUgz+HXf7KiZemzAoFEYSjAILfR+cG6FmYSF8XvLbGOB34Kx8mlRi7EI8rZXpezJ5qCqOyZg==}
peerDependencies:
'@huggingface/transformers': ^3.5.0
gpt-tokenizer: ^2.5.0
peerDependenciesMeta:
'@huggingface/transformers':
optional: true
gpt-tokenizer:
optional: true
'@llamaindex/node-parser@2.0.2':
resolution: {integrity: sha512-pWqEfFRMW5TFzbDyEDWqhipR9P/HBF2C+pEeTroyRACZtKJLbeg3R3e2g4gmfnPxmxQIXkGn2UFK8Io4nwVNOg==}
peerDependencies:
@@ -1213,6 +1237,14 @@ packages:
tree-sitter: ^0.22.0
web-tree-sitter: ^0.24.3
'@llamaindex/node-parser@2.0.6':
resolution: {integrity: sha512-8vJ7hsh1FKfAfO90nNe/G+/E2hM31dT/+1+0cPpZp02Ez98Jhwj+vTcCRlvyZznCn6DzHNBuw+uP1wKCqylSnQ==}
peerDependencies:
'@llamaindex/core': 0.6.6
'@llamaindex/env': 0.1.30
tree-sitter: ^0.22.0
web-tree-sitter: ^0.24.3
'@llamaindex/openai@0.2.1':
resolution: {integrity: sha512-gSBYL1vMaEihXm5K5gIHpUmvDS78ACPgQPlwf/ZnE82UD/vFNOWOqQbwESJKcygC0lHrwhrKJAuBAzXSNcpznA==}
@@ -1252,6 +1284,13 @@ packages:
'@llamaindex/env': 0.1.29
zod: ^3.23.8
'@llamaindex/workflow@1.1.3':
resolution: {integrity: sha512-B9wlN4xMtfbnIQW84wTC0GdyIaLGXYnZ4I0LENV2UM/0wqlXKI7YoBTO3cJvi6pO1sPA+K29Tqw822VsGF+GkA==}
peerDependencies:
'@llamaindex/core': 0.6.6
'@llamaindex/env': 0.1.30
zod: ^3.23.8
'@manypkg/find-root@1.1.0':
resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
@@ -2481,6 +2520,9 @@ packages:
'@types/responselike@1.0.3':
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
'@types/retry@0.12.2':
resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==}
'@types/retry@0.12.5':
resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==}
@@ -3882,6 +3924,10 @@ packages:
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
engines: {node: '>= 0.4'}
has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
@@ -3987,6 +4033,9 @@ packages:
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
ignore-by-default@1.0.1:
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -4117,6 +4166,10 @@ packages:
is-module@1.0.0:
resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
is-network-error@1.1.0:
resolution: {integrity: sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==}
engines: {node: '>=16'}
is-number-object@1.1.1:
resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==}
engines: {node: '>= 0.4'}
@@ -4387,6 +4440,10 @@ packages:
resolution: {integrity: sha512-tUbb+HYaQ+WP1f4NrYnEOIgPPRUePh3sPnOf/wbxTYAyZTnREpSiwCfLUGgYOiIYKK5PS7Oo/eTZCP7jpi7gSA==}
engines: {node: '>=18.0.0'}
llamaindex@0.11.1:
resolution: {integrity: sha512-d+sMHc6b/Lw8S1HhTVtR5hgEkg4uCpADpgZDcpHa/QlSQXvCmSkEdI9DOhoTDMhNWWatKnQZADPDuVzkyysQ3w==}
engines: {node: '>=18.0.0'}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
@@ -4805,6 +4862,11 @@ packages:
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
hasBin: true
nodemon@3.1.10:
resolution: {integrity: sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==}
engines: {node: '>=10'}
hasBin: true
nopt@5.0.0:
resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
engines: {node: '>=6'}
@@ -4962,6 +5024,10 @@ packages:
resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==}
engines: {node: '>=6'}
p-retry@6.2.1:
resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==}
engines: {node: '>=16.17'}
p-timeout@3.2.0:
resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==}
engines: {node: '>=8'}
@@ -5258,6 +5324,9 @@ packages:
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
pstree.remy@1.1.8:
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
pump@3.0.2:
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
@@ -5503,6 +5572,10 @@ packages:
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
engines: {node: '>= 4'}
retry@0.13.1:
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
engines: {node: '>= 4'}
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -5674,6 +5747,10 @@ packages:
simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
simple-update-notifier@2.0.0:
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
engines: {node: '>=10'}
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
@@ -5832,6 +5909,10 @@ packages:
babel-plugin-macros:
optional: true
supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -5928,6 +6009,10 @@ packages:
to-vfile@6.1.0:
resolution: {integrity: sha512-BxX8EkCxOAZe+D/ToHdDsJcVI4HqQfmw0tCkp31zf3dNP/XWIAjU4CmeuSwsSoOzOTqHPOL0KUzyZqJplkD0Qw==}
touch@3.1.1:
resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
hasBin: true
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
@@ -6018,6 +6103,9 @@ packages:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'}
undefsafe@2.0.5:
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
underscore@1.13.7:
resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==}
@@ -6348,6 +6436,9 @@ packages:
zod@3.24.3:
resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==}
zod@3.25.13:
resolution: {integrity: sha512-Q8mvk2iWi7rTDfpQBsu4ziE7A6AxgzJ5hzRyRYQkoV3A3niYsXVwDaP1Kbz3nWav6S+VZ6k2OznFn8ZyDHvIrg==}
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@@ -6462,7 +6553,7 @@ snapshots:
'@babel/parser': 7.27.0
'@babel/template': 7.27.0
'@babel/types': 7.27.0
debug: 4.4.0
debug: 4.4.0(supports-color@5.5.0)
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@@ -6891,7 +6982,7 @@ snapshots:
'@eslint/config-array@0.19.2':
dependencies:
'@eslint/object-schema': 2.1.6
debug: 4.4.0
debug: 4.4.0(supports-color@5.5.0)
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@@ -6909,7 +7000,7 @@ snapshots:
'@eslint/eslintrc@3.3.1':
dependencies:
ajv: 6.12.6
debug: 4.4.0
debug: 4.4.0(supports-color@5.5.0)
espree: 10.3.0
globals: 14.0.0
ignore: 5.3.2
@@ -7108,12 +7199,13 @@ snapshots:
'@lezer/highlight': 1.2.1
'@lezer/lr': 1.4.2
'@llama-flow/core@0.4.1(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(zod@3.24.3)':
'@llama-flow/core@0.4.1(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)':
optionalDependencies:
next: 15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
p-retry: 6.2.1
zod: 3.24.3
'@llamaindex/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
@@ -7173,6 +7265,14 @@ snapshots:
'@llamaindex/core': 0.6.2
'@llamaindex/env': 0.1.29
'@llamaindex/cloud@4.0.9(@llama-flow/core@0.4.1(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3))(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)':
dependencies:
'@llama-flow/core': 0.4.1(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)
'@llamaindex/core': 0.6.6
'@llamaindex/env': 0.1.30
p-retry: 6.2.1
zod: 3.25.13
'@llamaindex/core@0.6.1':
dependencies:
'@llamaindex/env': 0.1.29
@@ -7195,12 +7295,29 @@ snapshots:
- '@huggingface/transformers'
- gpt-tokenizer
'@llamaindex/core@0.6.6':
dependencies:
'@llamaindex/env': 0.1.30
'@types/node': 22.15.2
magic-bytes.js: 1.12.1
zod: 3.24.3
zod-to-json-schema: 3.24.5(zod@3.24.3)
transitivePeerDependencies:
- '@huggingface/transformers'
- gpt-tokenizer
'@llamaindex/env@0.1.29':
dependencies:
'@aws-crypto/sha256-js': 5.2.0
js-tiktoken: 1.0.20
pathe: 1.1.2
'@llamaindex/env@0.1.30':
dependencies:
'@aws-crypto/sha256-js': 5.2.0
js-tiktoken: 1.0.20
pathe: 1.1.2
'@llamaindex/node-parser@2.0.2(@llamaindex/core@0.6.2)(@llamaindex/env@0.1.29)(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)':
dependencies:
'@llamaindex/core': 0.6.2
@@ -7209,6 +7326,14 @@ snapshots:
tree-sitter: 0.22.4
web-tree-sitter: 0.24.7
'@llamaindex/node-parser@2.0.6(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)':
dependencies:
'@llamaindex/core': 0.6.6
'@llamaindex/env': 0.1.30
html-to-text: 9.0.5
tree-sitter: 0.22.4
web-tree-sitter: 0.24.7
'@llamaindex/openai@0.2.1':
dependencies:
'@llamaindex/core': 0.6.1
@@ -7286,9 +7411,15 @@ snapshots:
'@llamaindex/env': 0.1.29
zod: 3.24.3
'@llamaindex/workflow@1.1.0(@llamaindex/core@0.6.2)(@llamaindex/env@0.1.29)(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/workflow@1.0.3(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)(zod@3.24.3)':
dependencies:
'@llama-flow/core': 0.4.1(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(zod@3.24.3)
'@llamaindex/core': 0.6.6
'@llamaindex/env': 0.1.30
zod: 3.24.3
'@llamaindex/workflow@1.1.0(@llamaindex/core@0.6.2)(@llamaindex/env@0.1.29)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)':
dependencies:
'@llama-flow/core': 0.4.1(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)
'@llamaindex/core': 0.6.2
'@llamaindex/env': 0.1.29
zod: 3.24.3
@@ -7299,6 +7430,19 @@ snapshots:
- p-retry
- rxjs
'@llamaindex/workflow@1.1.3(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)':
dependencies:
'@llama-flow/core': 0.4.1(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3)
'@llamaindex/core': 0.6.6
'@llamaindex/env': 0.1.30
zod: 3.24.3
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- hono
- next
- p-retry
- rxjs
'@manypkg/find-root@1.1.0':
dependencies:
'@babel/runtime': 7.27.0
@@ -8495,6 +8639,8 @@ snapshots:
dependencies:
'@types/node': 20.17.30
'@types/retry@0.12.2': {}
'@types/retry@0.12.5': {}
'@types/tar@6.1.5':
@@ -8531,7 +8677,7 @@ snapshots:
'@typescript-eslint/types': 8.31.0
'@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.31.0
debug: 4.4.0
debug: 4.4.0(supports-color@5.5.0)
eslint: 9.22.0(jiti@2.4.2)
typescript: 5.8.3
transitivePeerDependencies:
@@ -8546,7 +8692,7 @@ snapshots:
dependencies:
'@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3)
'@typescript-eslint/utils': 8.31.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3)
debug: 4.4.0
debug: 4.4.0(supports-color@5.5.0)
eslint: 9.22.0(jiti@2.4.2)
ts-api-utils: 2.1.0(typescript@5.8.3)
typescript: 5.8.3
@@ -8559,7 +8705,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 8.31.0
'@typescript-eslint/visitor-keys': 8.31.0
debug: 4.4.0
debug: 4.4.0(supports-color@5.5.0)
fast-glob: 3.3.3
is-glob: 4.0.3
minimatch: 9.0.5
@@ -8743,7 +8889,7 @@ snapshots:
agent-base@6.0.2:
dependencies:
debug: 4.4.0
debug: 4.4.0(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
optional: true
@@ -9101,7 +9247,7 @@ snapshots:
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.2
fsevents: 2.3.3
chownr@1.1.4:
optional: true
@@ -9297,9 +9443,11 @@ snapshots:
dependencies:
ms: 2.1.3
debug@4.4.0:
debug@4.4.0(supports-color@5.5.0):
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 5.5.0
decimal.js-light@2.5.1: {}
@@ -9665,7 +9813,7 @@ snapshots:
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.0
debug: 4.4.0(supports-color@5.5.0)
eslint: 9.22.0(jiti@2.4.2)
get-tsconfig: 4.10.0
is-bun-module: 2.0.0
@@ -9789,7 +9937,7 @@ snapshots:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.0
debug: 4.4.0(supports-color@5.5.0)
escape-string-regexp: 4.0.0
eslint-scope: 8.3.0
eslint-visitor-keys: 4.2.0
@@ -10168,6 +10316,8 @@ snapshots:
has-bigints@1.1.0: {}
has-flag@3.0.0: {}
has-flag@4.0.0: {}
has-property-descriptors@1.0.2:
@@ -10278,7 +10428,7 @@ snapshots:
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.4.0
debug: 4.4.0(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
optional: true
@@ -10304,6 +10454,8 @@ snapshots:
ieee754@1.2.1:
optional: true
ignore-by-default@1.0.1: {}
ignore@5.3.2: {}
immediate@3.0.6: {}
@@ -10428,6 +10580,8 @@ snapshots:
is-module@1.0.0: {}
is-network-error@1.1.0: {}
is-number-object@1.1.1:
dependencies:
call-bound: 1.0.4
@@ -10664,7 +10818,7 @@ snapshots:
dependencies:
chalk: 5.4.1
commander: 13.1.0
debug: 4.4.0
debug: 4.4.0(supports-color@5.5.0)
execa: 8.0.1
lilconfig: 3.1.3
listr2: 8.3.2
@@ -10706,6 +10860,26 @@ snapshots:
- ws
- zod
llamaindex@0.11.1(@llama-flow/core@0.4.1(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3))(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(zod@3.24.3):
dependencies:
'@llamaindex/cloud': 4.0.9(@llama-flow/core@0.4.1(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.3))(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)
'@llamaindex/core': 0.6.6
'@llamaindex/env': 0.1.30
'@llamaindex/node-parser': 2.0.6(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)
'@llamaindex/workflow': 1.0.3(@llamaindex/core@0.6.6)(@llamaindex/env@0.1.30)(zod@3.24.3)
'@types/lodash': 4.17.16
'@types/node': 22.15.2
ajv: 8.17.1
lodash: 4.17.21
magic-bytes.js: 1.12.1
transitivePeerDependencies:
- '@huggingface/transformers'
- '@llama-flow/core'
- gpt-tokenizer
- tree-sitter
- web-tree-sitter
- zod
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
@@ -11115,7 +11289,7 @@ snapshots:
micromark@3.2.0:
dependencies:
'@types/debug': 4.1.12
debug: 4.4.0
debug: 4.4.0(supports-color@5.5.0)
decode-named-character-reference: 1.1.0
micromark-core-commonmark: 1.1.0
micromark-factory-space: 1.1.0
@@ -11260,6 +11434,19 @@ snapshots:
node-gyp-build@4.8.4: {}
nodemon@3.1.10:
dependencies:
chokidar: 3.6.0
debug: 4.4.0(supports-color@5.5.0)
ignore-by-default: 1.0.1
minimatch: 3.1.2
pstree.remy: 1.1.8
semver: 7.7.1
simple-update-notifier: 2.0.0
supports-color: 5.5.0
touch: 3.1.1
undefsafe: 2.0.5
nopt@5.0.0:
dependencies:
abbrev: 1.1.1
@@ -11430,6 +11617,12 @@ snapshots:
p-map@2.1.0: {}
p-retry@6.2.1:
dependencies:
'@types/retry': 0.12.2
is-network-error: 1.1.0
retry: 0.13.1
p-timeout@3.2.0:
dependencies:
p-finally: 1.0.0
@@ -11672,6 +11865,8 @@ snapshots:
proxy-from-env@1.1.0: {}
pstree.remy@1.1.8: {}
pump@3.0.2:
dependencies:
end-of-stream: 1.4.4
@@ -11992,6 +12187,8 @@ snapshots:
retry@0.12.0: {}
retry@0.13.1: {}
reusify@1.1.0: {}
rfdc@1.4.1: {}
@@ -12235,6 +12432,10 @@ snapshots:
is-arrayish: 0.3.2
optional: true
simple-update-notifier@2.0.0:
dependencies:
semver: 7.7.1
sisteransi@1.0.5: {}
slash@3.0.0: {}
@@ -12393,6 +12594,10 @@ snapshots:
client-only: 0.0.1
react: 19.1.0
supports-color@5.5.0:
dependencies:
has-flag: 3.0.0
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@@ -12489,6 +12694,8 @@ snapshots:
is-buffer: 2.0.5
vfile: 4.2.1
touch@3.1.1: {}
tr46@0.0.3: {}
tree-sitter@0.22.4:
@@ -12593,6 +12800,8 @@ snapshots:
has-symbols: 1.1.0
which-boxed-primitive: 1.1.1
undefsafe@2.0.5: {}
underscore@1.13.7: {}
undici-types@5.26.5: {}
@@ -12820,7 +13029,7 @@ snapshots:
vite-node@2.1.9(@types/node@20.17.30)(lightningcss@1.29.2):
dependencies:
cac: 6.7.14
debug: 4.4.0
debug: 4.4.0(supports-color@5.5.0)
es-module-lexer: 1.7.0
pathe: 1.1.2
vite: 5.4.18(@types/node@20.17.30)(lightningcss@1.29.2)
@@ -12855,7 +13064,7 @@ snapshots:
'@vitest/spy': 2.1.9
'@vitest/utils': 2.1.9
chai: 5.2.0
debug: 4.4.0
debug: 4.4.0(supports-color@5.5.0)
expect-type: 1.2.1
magic-string: 0.30.17
pathe: 1.1.2
@@ -12886,7 +13095,7 @@ snapshots:
dependencies:
chalk: 4.1.2
commander: 9.5.0
debug: 4.4.0
debug: 4.4.0(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@@ -13023,4 +13232,6 @@ snapshots:
zod@3.24.3: {}
zod@3.25.13: {}
zwitch@2.0.4: {}
+45 -11
View File
@@ -72,7 +72,7 @@ app = LlamaIndexServer(
The LlamaIndexServer accepts the following configuration parameters:
- `workflow_factory`: A callable that creates a workflow instance for each request
- `workflow_factory`: A callable that creates a workflow instance for each request. See [Workflow factory contract](#workflow-factory-contract) for more details.
- `logger`: Optional logger instance (defaults to uvicorn logger)
- `use_default_routers`: Whether to include default routers (chat, static file serving)
- `env`: Environment setting ('dev' enables CORS and UI by default)
@@ -83,10 +83,36 @@ 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)
## Workflow factory contract
The `workflow_factory` provided will be called for each chat request to initialize a new workflow instance. Additionally, we provide the [ChatRequest](https://github.com/run-llama/create-llama/blob/afe9e9fc16427d20e1dfb635a45e7ed4b46285cb/python/llama-index-server/llama_index/server/api/models.py#L32) object, which includes the request information that is helpful for initializing the workflow. For example:
```python
def create_workflow(chat_request: ChatRequest) -> Workflow:
# using messages from the chat request to initialize the workflow
return MyCustomWorkflow(chat_request.messages)
```
Your workflow will be executed once for each chat request with the following input parameters are included in workflow's `StartEvent`:
- `user_msg` [str]: The current user message
- `chat_history` [list[[ChatMessage](https://docs.llamaindex.ai/en/stable/api_reference/prompts/#llama_index.core.prompts.ChatMessage)]]: All the previous messages of the conversation
Example:
```python
@step
def handle_start_event(ev: StartEvent) -> MyNextEvent:
user_msg = ev.user_msg
chat_history = ev.chat_history
...
```
Your workflows can emit `UIEvent` events to render [Custom UI Components](https://github.com/run-llama/create-llama/blob/main/python/llama-index-server/docs/custom_ui_component.md) in the chat UI to improve the user experience.
Furthermore, you can send `ArtifactEvent` events to render code or document [Artifacts](https://github.com/run-llama/create-llama/blob/main/python/llama-index-server/docs/custom_artifact_event.md) in a dedicated Canvas panel in the chat UI.
## Default Routers and Features
### Chat Router
@@ -107,11 +133,6 @@ When enabled, the server provides a chat interface at the root path (`/`) with:
- Real-time chat interface
- API endpoint integration
### Custom UI Components
You can add custom UI components for your workflow by providing `component_dir` config and adding custom .jsx or .tsx files to the directory.
See [Custom UI Components](https://github.com/run-llama/create-llama/blob/main/llama-index-server/docs/custom_ui_component.md) for more details.
## Development Mode
In development mode (`env="dev"`), the server:
@@ -120,6 +141,20 @@ 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:
@@ -130,11 +165,10 @@ The server provides the following default endpoints:
## Best Practices
1. Always provide a workflow factory that creates fresh workflow instances
2. Use environment variables for sensitive configuration
3. Enable verbose logging during development
4. Configure CORS appropriately for your deployment environment
5. Use starter questions to guide users in the chat UI
1. Use environment variables for sensitive configuration
2. Enable verbose logging during development
3. Configure CORS appropriately for your deployment environment
4. Use starter questions to guide users in the chat UI
## Getting Started with a New Project
@@ -0,0 +1,59 @@
# 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`: An `ArtifactType` enum indicating the type of artifact (e.g., `ArtifactType.DOCUMENT`, `ArtifactType.CODE`).
- `created_at`: A timestamp (e.g., `int(time.time())`) 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`. For example, `DocumentArtifactData` or `CodeArtifactData`.
## Defining and Sending an ArtifactEvent
First, import the necessary classes:
```python
import time
from llama_index.server.api.models import (
Artifact,
ArtifactEvent,
ArtifactType,
DocumentArtifactData,
# CodeArtifactData, # Import if sending code artifacts
)
```
Then, within your workflow logic, use `ctx.write_event_to_stream` to emit the event. Here's an example of sending a document artifact, taken from [document_workflow.py](/python/llama-index-server/examples/artifact/document_workflow.py):
```python
# Assuming 'ctx' is the workflow Context and 'content' is a markdown string
ctx.write_event_to_stream(
ArtifactEvent(
data=Artifact(
type=ArtifactType.DOCUMENT,
created_at=int(time.time()),
data=DocumentArtifactData(
title="My cooking recipes",
content=content,
type="markdown",
),
),
)
)
```
This will send the artifact to the LlamaIndex Server UI, where it will be rendered in the Canvas panel by a renderer depending on the artifact type. For `ArtifactType.DOCUMENT`, this uses a `DocumentArtifactViewer`.
## Available Artifact Types
LlamaIndex Server currently supports the following artifact types:
- `ArtifactType.DOCUMENT`: For text-based documents like Markdown or HTML.
- `data` should be an instance of `DocumentArtifactData` which includes `title: str`, `content: str`, and `type: Literal["markdown", "html"]`.
- `ArtifactType.CODE`: For code snippets.
- `data` should be an instance of `CodeArtifactData` which includes `title: str`, `code: str`, and `language: str`.
Ensure you provide the correct data model corresponding to the `ArtifactType` you are sending. You can find these data models in `llama_index.server.api.models`.
@@ -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
@@ -5,7 +5,7 @@ from typing import Optional
import requests
CHAT_UI_VERSION = "0.1.6"
CHAT_UI_VERSION = "0.2.1"
def download_chat_ui(
@@ -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")
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "llama-index-server"
version = "0.1.15"
version = "0.1.16"
description = "llama-index fastapi server"
readme = "README.md"
license = "MIT"
+2678 -2677
View File
File diff suppressed because it is too large Load Diff