Compare commits

...

25 Commits

Author SHA1 Message Date
thucpn 1c5fc33c42 Merge branch 'main' into tp/support-fully-custom-next-folder 2025-05-26 09:56:40 +07:00
thucpn 590637e862 fix import 2025-05-23 17:57:33 +07:00
thucpn 38e3b9a88b bump packages for examples 2025-05-23 17:57:26 +07:00
github-actions[bot] 82ac925224 Release 0.1.17 (#644)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-23 17:10:05 +07:00
thucpn 111a869a99 merge with main 2025-05-23 17:01:35 +07:00
thucpn f24ee8e6f9 fix: missing comma in config 2025-05-23 16:39:27 +07:00
thucpn 078b31de1c feat: support fully custom next folder 2025-05-23 16:06:47 +07:00
Thuc Pham 3acec88fbc chore: bump chat-ui (#645) 2025-05-23 15:18:17 +07:00
Thuc Pham eee3230e99 feat: support custom layout (#641) 2025-05-23 14:18:22 +07:00
Marcus Schiesser d8425e5290 docs: fix type 2025-05-23 13:22:11 +07:00
Huu Le 0bc5a0d882 feat: Add config for suggest next question (#640)
* Enhance LlamaIndexServer with next question suggestion feature

- Added `suggest_next_questions` parameter to the LlamaIndexServer for suggesting follow-up questions after the assistant's response.
- Updated README.md to document the new configuration option.
- Introduced `SUGGEST_NEXT_QUESTION_PROMPT` in prompts.py for customizable question suggestions.
- Bumped version to 0.1.16 in uv.lock to reflect the new feature.

* Implement next question suggestion feature in LlamaIndexServer

- Added `suggestNextQuestions` option to LlamaIndexServer for suggesting follow-up questions after the assistant's response.
- Updated README.md to include the new configuration option.
- Modified example workflow to utilize the new feature.
- Enhanced chat handler to conditionally send suggested questions based on the new option.

* add changeset

* remove log

* bundle ui instead of download

* check test

* check test

check test

check test

check test

check test

check test

check test

check test

check test

check test

* fix tests

* Update artifact path in workflow and clarify README.md text

- Changed the artifact path in the GitHub Actions workflow from `python/llama-index-server/dist/` to `dist/`.
- Revised README.md to clarify the default prompt used for the `suggest_next_questions` configuration option.

* support changeset for python

* refactor: update llama-index-server structure and workflows

* fix workflows

* fix workflows

* fix workflows

* add changeset

* fix cannot release python

* Update packages/server/README.md

Co-authored-by: Thuc Pham <51660321+thucpn@users.noreply.github.com>

* Update starter questions in LlamaIndex App and add TODO for suggestion feature in chat API

---------

Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
Co-authored-by: Thuc Pham <51660321+thucpn@users.noreply.github.com>
2025-05-23 12:48:45 +07:00
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
106 changed files with 2516 additions and 1101 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"create-llama": patch
---
Split artifacts use case to document generator and code generator
-5
View File
@@ -1,5 +0,0 @@
---
"create-llama": patch
---
chore: improve dev experience with nodemon
-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 @@
---
"@llamaindex/server": patch
---
feat: add dev mode UI
-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
@@ -16,6 +16,16 @@ jobs:
- uses: pnpm/action-setup@v3
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
+7
View File
@@ -17,6 +17,11 @@ jobs:
- uses: pnpm/action-setup@v3
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v3
@@ -56,3 +61,5 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
@@ -1,138 +0,0 @@
name: Release llama-index-server
on:
push:
branches:
- main
paths:
- "python/llama-index-server/**"
- ".github/workflows/release_llama_index_server.yml"
pull_request:
types:
- closed
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Create Release PR
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./python/llama-index-server
if: |
github.event_name == 'push' &&
!startsWith(github.ref, 'refs/heads/release/llama-index-server-v') &&
!contains(github.event.head_commit.message, 'Release: llama-index-server v')
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
shell: bash
run: uv sync --all-extras --dev
- name: Setup Git
run: |
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
- name: Bump patch version
shell: bash
run: |
uvx --from=toml-cli toml set --toml-path=pyproject.toml project.version $(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version | awk -F. '{$NF = $NF + 1;}1' OFS=.)
git add pyproject.toml
git commit -m "chore(release): bump llama-index-server version to $(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version)"
- name: Get current version
id: get_version
shell: bash
run: |
version=$(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version)
echo "current_version=${version}" >> "$GITHUB_OUTPUT"
- name: Create Release PR
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Release: llama-index-server v${{ steps.get_version.outputs.current_version }}"
title: "Release: llama-index-server v${{ steps.get_version.outputs.current_version }}"
body: |
This PR was automatically created to release a new version of the llama-index-server package.
Version: ${{ steps.get_version.outputs.current_version }}
Please review the changes and merge to trigger the release.
branch: release/llama-index-server-v${{ steps.get_version.outputs.current_version }}
base: main
labels: release, llama-index-server
publish:
name: Publish to PyPI
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./python/llama-index-server
if: |
github.event_name == 'pull_request' &&
github.event.pull_request.merged == true &&
startsWith(github.event.pull_request.title, 'Release: llama-index-server') &&
startsWith(github.event.pull_request.head.ref, 'release/llama-index-server-v')
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
shell: bash
run: uv sync --all-extras
- name: Get current version
id: get_version
shell: bash
run: |
version=$(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version)
echo "current_version=${version}" >> "$GITHUB_OUTPUT"
- name: Build package
shell: bash
run: uv build --no-sources
- name: Publish to PyPI
shell: bash
run: uv publish --token ${{ secrets.PYPI_TOKEN }}
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: llama-index-server-v${{ steps.get_version.outputs.current_version }}
name: "llama-index-server v${{ steps.get_version.outputs.current_version }}"
body: |
Release of llama-index-server v${{ steps.get_version.outputs.current_version }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+57 -20
View File
@@ -5,6 +5,7 @@ on:
env:
PYTHON_VERSION: "3.9"
UI_TEST: "true"
jobs:
unit-test:
@@ -19,20 +20,27 @@ jobs:
python-version: ["3.9"]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
- name: Setup Node.js
uses: actions/setup-node@v4
with:
python-version: ${{ matrix.python-version }}
node-version-file: ".nvmrc"
cache: "pnpm"
- name: Install dependencies
shell: bash
run: uv sync --all-extras --dev
run: pnpm install && pnpm build
- name: Run unit tests
shell: bash
@@ -46,20 +54,20 @@ jobs:
working-directory: python/llama-index-server
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
shell: bash
run: uv sync --all-extras --dev
run: pnpm install
- name: Run mypy
shell: bash
@@ -73,27 +81,56 @@ jobs:
working-directory: python/llama-index-server
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- uses: pnpm/action-setup@v3
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install build package
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"
- name: Install dependencies
run: pnpm install && pnpm build
- name: Build package
shell: bash
run: uv sync --all-extras
run: uv build
- name: Get the absolute wheel file path and save it to the output
shell: bash
id: get_whl_path
run: |
WHL_FILE=$(readlink -f dist/*.whl)
echo "whl_file=$WHL_FILE" >> $GITHUB_OUTPUT
- name: Test import
shell: bash
run: uv run python -c "from llama_index.server import LlamaIndexServer"
working-directory: ${{ github.workspace }}
env:
WHL_FILE: ${{ steps.get_whl_path.outputs.whl_file }}
run: |
uv run --with $WHL_FILE python -c "from llama_index.server import LlamaIndexServer"
- name: Check frontend resources is present
shell: bash
working-directory: ${{ github.workspace }}
env:
WHL_FILE: ${{ steps.get_whl_path.outputs.whl_file }}
run: |
uv run --with $WHL_FILE python -c "from llama_index.server.chat_ui import check_ui_resources; check_ui_resources()"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: llama-index-server
path: python/llama-index-server/dist/
path: dist/
+6 -3
View File
@@ -13,7 +13,8 @@
},
"license": "MIT",
"workspaces": [
"packages/*"
"packages/*",
"python/*"
],
"scripts": {
"dev": "pnpm -r dev",
@@ -24,8 +25,10 @@
"format:write": "prettier --ignore-unknown --write .",
"prepare": "husky",
"new-snapshot": "pnpm -r build && changeset version --snapshot",
"new-version": "pnpm -r build && changeset version",
"release": "pnpm -r build && changeset publish",
"new-version-python": "pnpm --filter @create-llama/llama-index-server new-version",
"new-version": "pnpm -r build && changeset version && pnpm new-version-python",
"release-python": "pnpm --filter @create-llama/llama-index-server release",
"release": "pnpm -r build && changeset publish && pnpm release-python",
"release-snapshot": "pnpm -r build && changeset publish --tag snapshot"
},
"devDependencies": {
+30
View File
@@ -1,5 +1,35 @@
# create-llama
## 0.5.17
### Patch Changes
- eee3230: feat: support custom layout
## 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
@@ -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;
+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,
);
}
+8 -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") {
@@ -578,6 +578,12 @@ const installLlamaIndexServerTemplate = async ({
cwd: path.join(templatesDir, "components", "ui", "use-cases", useCase),
});
// Copy layout components to layout folder in root
await copy("*", path.join(root, "layout"), {
parents: true,
cwd: path.join(templatesDir, "components", "ui", "layout"),
});
if (useLlamaParse) {
await copy("index.py", path.join(root, "app"), {
parents: true,
@@ -677,6 +683,7 @@ export const installPythonTemplate = async ({
dataSources,
tools,
template,
observability,
);
await addDependencies(root, addOnDependencies);
+19 -3
View File
@@ -42,12 +42,18 @@ const installLlamaIndexServerTemplate = async ({
rename: assetRelocator,
});
// copy workflow UI components to output/components folder
// copy workflow UI components to components folder in root
await copy("*", path.join(root, "components"), {
parents: true,
cwd: path.join(templatesDir, "components", "ui", "use-cases", useCase),
});
// copy layout components to layout folder in root
await copy("*", path.join(root, "layout"), {
parents: true,
cwd: path.join(templatesDir, "components", "ui", "layout"),
});
// Override generate.ts if workflow use case doesn't use custom UI
if (vectorDb === "llamacloud") {
await copy("generate.ts", path.join(root, "src"), {
@@ -387,7 +393,7 @@ const providerDependencies: {
[key in ModelProvider]?: Record<string, string>;
} = {
openai: {
"@llamaindex/openai": "^0.3.7",
"@llamaindex/openai": "~0.4.0",
},
gemini: {
"@llamaindex/google": "^0.2.0",
@@ -513,7 +519,7 @@ async function updatePackageJson({
if (backend) {
packageJson.dependencies = {
...packageJson.dependencies,
"@llamaindex/readers": "^3.1.3",
"@llamaindex/readers": "~3.1.4",
};
if (vectorDb && vectorDb in vectorDbDependencies) {
@@ -543,6 +549,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.17",
"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: "",
@@ -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,40 @@
"use client";
import { Sparkles, Star } from "lucide-react";
export default function Header() {
return (
<div className="flex items-center justify-between px-4 pt-2">
<div className="flex items-center gap-2">
<Sparkles className="size-4" />
<h1 className="font-semibold">LlamaIndex App</h1>
</div>
<div className="flex items-center justify-end gap-4">
<div className="flex items-center gap-2">
<a
href="https://www.llamaindex.ai/"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
>
Built by LlamaIndex
</a>
<img
className="h-[24px] w-[24px] rounded-sm"
src="/llama.png"
alt="Llama Logo"
/>
</div>
<a
href="https://github.com/run-llama/LlamaIndexTS"
target="_blank"
rel="noopener noreferrer"
className="hover:bg-accent flex items-center gap-2 rounded-md border border-gray-300 px-2 py-1 text-sm"
>
<Star className="size-4" />
Star on GitHub
</a>
</div>
</div>
);
}
@@ -8,7 +8,7 @@ First, install the dependencies:
npm install
```
Third, run the development server:
Second, run the development server:
```
npm run dev
@@ -34,7 +34,7 @@ AI-powered code generator that can help you generate app with a chat interface,
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:
You can start by sending a 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' \
@@ -1,5 +1,5 @@
import { extractLastArtifact } from "@llamaindex/server";
import { ChatMemoryBuffer, LLM, MessageContent, Settings } from "llamaindex";
import { ChatMemoryBuffer, MessageContent, Settings } from "llamaindex";
import {
agentStreamEvent,
@@ -12,12 +12,6 @@ import {
import { z } from "zod";
export const workflowFactory = async (reqBody: any) => {
const workflow = createCodeArtifactWorkflow(reqBody);
return workflow;
};
export const RequirementSchema = z.object({
next_step: z.enum(["answering", "coding"]),
language: z.string().nullable().optional(),
@@ -71,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,
context: state.lastArtifact
? JSON.stringify(state.lastArtifact)
: undefined,
});
});
@@ -8,7 +8,7 @@ First, install the dependencies:
npm install
```
Third, run the development server:
Second, run the development server:
```
npm run dev
@@ -34,7 +34,7 @@ AI-powered document generator that can help you generate documents with a chat i
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:
You can start by sending a 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' \
@@ -1,5 +1,5 @@
import { extractLastArtifact } from "@llamaindex/server";
import { ChatMemoryBuffer, LLM, MessageContent, Settings } from "llamaindex";
import { ChatMemoryBuffer, MessageContent, Settings } from "llamaindex";
import {
agentStreamEvent,
@@ -12,12 +12,6 @@ import {
import { z } from "zod";
export const workflowFactory = async (reqBody: any) => {
const workflow = createDocumentArtifactWorkflow(reqBody);
return workflow;
};
export const DocumentRequirementSchema = z.object({
type: z.enum(["markdown", "html"]),
title: z.string(),
@@ -74,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
@@ -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")
@@ -16,9 +16,10 @@ def create_app():
workflow_factory=create_workflow, # A factory function that creates a new workflow for each request
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]
@@ -9,11 +9,11 @@
"start": "tsx src/index.ts"
},
"dependencies": {
"@llamaindex/openai": "^0.3.7",
"@llamaindex/server": "^0.2.0",
"@llamaindex/workflow": "^1.1.2",
"@llamaindex/tools": "^0.0.10",
"llamaindex": "^0.10.6",
"@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"
},
@@ -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
@@ -8,7 +8,7 @@ initSettings();
new LlamaIndexServer({
workflow: workflowFactory,
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/
+21
View File
@@ -1,5 +1,26 @@
# @llamaindex/server
## 0.2.3
### Patch Changes
- eee3230: feat: support custom layout
- 0bc5a0d: Add suggestNextQuestions config
- 3acec88: chore: bump chat-ui
## 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
+152 -8
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,19 +21,22 @@ 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,
uiConfig: {
appTitle: "LlamaIndex App",
starterQuestions: ["Who is the first president of the United States?"],
},
}).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 +57,76 @@ 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.
- `layoutDir`: The directory for custom layout sections. The default value is `layout`. See [Custom Layout](#custom-layout) for more details.
- `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.
- `suggestNextQuestions`: Whether to suggest next questions after the assistant's response (default: `true`). You can change the prompt for the next questions by setting the `NEXT_QUESTION_PROMPT` environment variable.
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.
@@ -123,6 +186,28 @@ Feel free to modify the generated code to match your needs. If you're not satisf
> Note that `generateEventComponent` is generating JSX code, but you can also provide a TSX file.
## Custom Layout
LlamaIndex Server supports custom layout for header and footer. To use custom layout, you need to initialize the LlamaIndex server with the `layoutDir` that contains your custom layout files.
```ts
new LlamaIndexServer({
workflow: createWorkflow,
uiConfig: {
layoutDir: "layout",
},
}).start();
```
```
layout/
header.tsx
footer.tsx
```
We currently support custom header and footer for the chat interface. The syntax for these files is the same as events components in components directory.
Note that by default, we are still rendering the default LlamaIndex Header. It's also the fallback when having errors rendering the custom header. Example layout files will be generated in the `layout` directory of your project when creating a new project with `create-llama`.
### Server Setup
To use the generated UI components, you need to initialize the LlamaIndex server with the `componentsDir` that contains your custom UI components:
@@ -131,12 +216,71 @@ To use the generated UI components, you need to initialize the LlamaIndex server
new LlamaIndexServer({
workflow: createWorkflow,
uiConfig: {
appTitle: "LlamaIndex App",
componentsDir: "components",
},
}).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
@@ -1,12 +1,7 @@
import { OpenAI, OpenAIEmbedding } from "@llamaindex/openai";
import { LlamaIndexServer } from "@llamaindex/server";
import { agent } from "@llamaindex/workflow";
import {
Document,
OpenAI,
OpenAIEmbedding,
Settings,
VectorStoreIndex,
} from "llamaindex";
import { Document, Settings, VectorStoreIndex } from "llamaindex";
Settings.llm = new OpenAI({
model: "gpt-4o-mini",
@@ -35,9 +30,9 @@ export const workflowFactory = async () => {
new LlamaIndexServer({
workflow: workflowFactory,
suggestNextQuestions: true,
uiConfig: {
appTitle: "LlamaIndex App",
starterQuestions: ["What is the color of the dog?"],
},
port: 4100,
port: 3000,
}).start();
@@ -0,0 +1,30 @@
import { OpenAI } from "@llamaindex/openai";
import { LlamaIndexServer } from "@llamaindex/server";
import { agent } from "@llamaindex/workflow";
import { Settings, tool } from "llamaindex";
import { z } from "zod";
Settings.llm = new OpenAI({ model: "gpt-4o-mini" });
const weatherAgent = agent({
tools: [
tool({
name: "weather",
description: "Get the weather in a given city",
parameters: z.object({ city: z.string() }),
execute: ({ city }) => `The weather in ${city} is sunny`,
}),
],
});
new LlamaIndexServer({
workflow: () => weatherAgent,
uiConfig: {
starterQuestions: [
"What is the weather in Tokyo?",
"What is the weather in Ho Chi Minh City?",
],
layoutDir: "layout",
},
port: 3000,
}).start();
@@ -0,0 +1,40 @@
"use client";
import { Sparkles, Star } from "lucide-react";
export default function Header() {
return (
<div className="flex items-center justify-between px-4 pt-2">
<div className="flex items-center gap-2">
<Sparkles className="size-4" />
<h1 className="font-semibold">LlamaIndex App</h1>
</div>
<div className="flex items-center justify-end gap-4">
<div className="flex items-center gap-2">
<a
href="https://www.llamaindex.ai/"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
>
Built by LlamaIndex
</a>
<img
className="h-[24px] w-[24px] rounded-sm"
src="/llama.png"
alt="Llama Logo"
/>
</div>
<a
href="https://github.com/run-llama/LlamaIndexTS"
target="_blank"
rel="noopener noreferrer"
className="hover:bg-accent flex items-center gap-2 rounded-md border border-gray-300 px-2 py-1 text-sm"
>
<Star className="size-4" />
Star on GitHub
</a>
</div>
</div>
);
}
+2 -3
View File
@@ -6,10 +6,9 @@ First, we need to set `devMode` to `true` in the `uiConfig` of the server.
new LlamaIndexServer({
workflow: workflowFactory,
uiConfig: {
appTitle: "Calculator",
devMode: true,
},
port: 6000,
port: 3000,
}).start();
```
@@ -17,5 +16,5 @@ Export OpenAI API key and start the server in dev mode.
```bash
export OPENAI_API_KEY=<your-openai-api-key>
npx tsx watch index.ts
npx nodemon --exec tsx index.ts --ignore src/app/workflow_*.ts
```
+5 -2
View File
@@ -1,15 +1,18 @@
import { OpenAI } from "@llamaindex/openai";
import { LlamaIndexServer } from "@llamaindex/server";
import { Settings } from "llamaindex";
import { workflowFactory } from "./src/app/workflow";
Settings.llm = new OpenAI({ model: "gpt-4o-mini" });
new LlamaIndexServer({
workflow: workflowFactory,
uiConfig: {
appTitle: "Calculator",
devMode: true,
starterQuestions: [
"What is the weather in Tokyo?",
"What is the weather in New York?",
],
},
port: 6005,
port: 3000,
}).start();
@@ -6,10 +6,10 @@ 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`,
name: "add",
description: "Adds two numbers",
parameters: z.object({ x: z.number(), y: z.number() }),
execute: ({ x, y }) => x + y,
}),
],
});
@@ -0,0 +1,2 @@
next/
next-env.d.ts
@@ -0,0 +1,15 @@
# Fully Custom Next Folder
This example shows how to fully customize the Next.js frontend and Route Handlers.
## Running ejection
```bash
pnpm run eject
```
## Running the example
```bash
npx nodemon --exec tsx src/index.ts --ignore next/
```
@@ -0,0 +1,8 @@
import { OpenAI } from "@llamaindex/openai";
import { Settings } from "llamaindex";
export function initSettings() {
Settings.llm = new OpenAI({
model: "gpt-4.1",
});
}
@@ -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 given city",
parameters: z.object({ city: z.string() }),
execute: ({ city }) => `The weather in ${city} is sunny`,
}),
],
});
};
@@ -0,0 +1,14 @@
import { LlamaIndexServer } from "@llamaindex/server";
import "dotenv/config";
import { initSettings } from "./app/settings";
import { workflowFactory } from "./app/workflow";
initSettings();
new LlamaIndexServer({
workflow: workflowFactory,
// dir: "./src/next",
uiConfig: {
// maybe having an option here to use ejected next folder
},
}).start();
+5 -5
View File
@@ -7,13 +7,13 @@
"dev": "nodemon --exec tsx simple-workflow/calculator.ts"
},
"dependencies": {
"@llamaindex/openai": "^0.2.0",
"@llamaindex/readers": "^3.0.0",
"@llamaindex/openai": "~0.4.0",
"@llamaindex/readers": "~3.1.4",
"@llamaindex/server": "workspace:*",
"@llamaindex/tools": "0.0.4",
"@llamaindex/workflow": "1.1.0",
"@llamaindex/tools": "~0.0.11",
"@llamaindex/workflow": "~1.1.3",
"dotenv": "^16.4.7",
"llamaindex": "0.10.2",
"llamaindex": "~0.11.3",
"zod": "^3.23.8"
},
"devDependencies": {
@@ -1,8 +1,11 @@
import { OpenAI } from "@llamaindex/openai";
import { LlamaIndexServer } from "@llamaindex/server";
import { agent } from "@llamaindex/workflow";
import { tool } from "llamaindex";
import { Settings, tool } from "llamaindex";
import { z } from "zod";
Settings.llm = new OpenAI({ model: "gpt-4o-mini" });
const calculatorAgent = agent({
tools: [
tool({
@@ -17,8 +20,7 @@ const calculatorAgent = agent({
new LlamaIndexServer({
workflow: () => calculatorAgent,
uiConfig: {
appTitle: "Calculator",
starterQuestions: ["1 + 1", "2 + 2"],
},
port: 4000,
port: 3000,
}).start();
+1 -1
View File
@@ -10,5 +10,5 @@
"outDir": "dist"
},
"include": ["**/*"],
"exclude": ["node_modules", "dist"]
"exclude": ["node_modules", "dist", "custom-layout/layout"]
}
@@ -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,78 @@
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, {
// TODO: Support enable/disable suggestion
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 },
);
}
}
@@ -0,0 +1,8 @@
import { NextRequest } from "next/server";
import { handleComponentRoute } from "../shared/component-handler";
export async function GET(request: NextRequest) {
const params = request.nextUrl.searchParams;
const directory = params.get("componentsDir") || "components";
return handleComponentRoute(directory);
}
@@ -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 });
}
}
@@ -0,0 +1,10 @@
import { NextRequest } from "next/server";
import { handleComponentRoute } from "../shared/component-handler";
const LAYOUT_TYPES = ["header", "footer"] as const;
export async function GET(request: NextRequest) {
const params = request.nextUrl.searchParams;
const directory = params.get("layoutDir") || "layout";
return handleComponentRoute(directory, LAYOUT_TYPES);
}
@@ -0,0 +1,80 @@
import fs from "fs";
import { NextResponse } from "next/server";
import path from "path";
import { promisify } from "util";
const VALID_EXTENSIONS = [".tsx", ".jsx"];
export type Item = {
type: string;
filename: string;
code: string;
};
function filterDuplicateFiles(files: string[]): string[] {
const fileMap = new Map<string, string>();
for (const file of files) {
const type = path.basename(file, path.extname(file));
if (fileMap.has(type)) {
const existingFile = fileMap.get(type)!;
// Prefer .tsx files
if (file.endsWith(".tsx") && !existingFile.endsWith(".tsx")) {
console.warn(`Preferring ${file} over ${existingFile}`);
fileMap.set(type, file);
}
} else {
fileMap.set(type, file);
}
}
return Array.from(fileMap.values());
}
export async function handleComponentRoute(
directory: string,
itemTypes?: readonly string[],
): Promise<NextResponse> {
try {
const exists = await promisify(fs.exists)(directory);
if (!exists) {
return NextResponse.json(
{ error: `Directory not found at ${directory}` },
{ status: 404 },
);
}
const filesInDir = await promisify(fs.readdir)(directory);
const validFiles = filesInDir.filter((file) =>
VALID_EXTENSIONS.includes(path.extname(file)),
);
let filesToProcess = filterDuplicateFiles(validFiles);
if (itemTypes?.length) {
// Specific item types provided (e.g., for layouts "header", "footer")
filesToProcess = filesToProcess.filter((file) =>
itemTypes.includes(path.basename(file, path.extname(file))),
);
}
const items: Item[] = await Promise.all(
filesToProcess.map(async (file) => {
const filePath = path.join(directory, file);
const content = await promisify(fs.readFile)(filePath, "utf-8");
return {
type: path.basename(file, path.extname(file)),
code: content,
filename: file,
};
}),
);
return NextResponse.json(items, { status: 200 });
} catch (error) {
console.error(`Error reading directory ${directory}:`, error);
return NextResponse.json(
{ error: `Failed to read directory ${directory}` },
{ status: 500 },
);
}
}
@@ -1,55 +0,0 @@
"use client";
import { Sparkles, Star } from "lucide-react";
import { Button } from "../button";
import { getConfig } from "../lib/utils";
export function ChatHeader() {
return (
<div className="flex items-center justify-between px-4 pt-2">
<ChatAppTitle />
<LlamaIndexLinks />
</div>
);
}
function ChatAppTitle() {
return (
<div className="flex items-center gap-2">
<Sparkles className="size-4" />
<h1 className="font-semibold">{getConfig("APP_TITLE")}</h1>
</div>
);
}
function LlamaIndexLinks() {
return (
<div className="flex items-center justify-end gap-4">
<div className="flex items-center gap-2">
<a
href="https://www.llamaindex.ai/"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
>
Built by LlamaIndex
</a>
<img
className="h-[24px] w-[24px] rounded-sm"
src="/llama.png"
alt="Llama Logo"
/>
</div>
<a
href="https://github.com/run-llama/LlamaIndexTS"
target="_blank"
rel="noopener noreferrer"
>
<Button variant="outline" size="sm">
<Star className="mr-2 size-4" />
Star on GitHub
</Button>
</a>
</div>
);
}
@@ -6,7 +6,6 @@ import { useEffect, useMemo, useState } from "react";
import { getConfig } from "../lib/utils";
import { ResizablePanel, ResizablePanelGroup } from "../resizable";
import { ChatCanvasPanel } from "./canvas/panel";
import { ChatHeader } from "./chat-header";
import { ChatInjection } from "./chat-injection";
import CustomChatInput from "./chat-input";
import CustomChatMessages from "./chat-messages";
@@ -14,6 +13,7 @@ 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";
import { ChatLayout } from "./layout";
export default function ChatSection() {
const handler = useChat({
@@ -32,8 +32,7 @@ export default function ChatSection() {
});
return (
<>
<div className="flex h-screen w-screen flex-col overflow-hidden">
<ChatHeader />
<ChatLayout>
<ChatUI
handler={handler}
className="relative flex min-h-0 flex-1 flex-row justify-center gap-4 px-4 py-0"
@@ -44,7 +43,7 @@ export default function ChatSection() {
</ResizablePanelGroup>
<DevModePanel />
</ChatUI>
</div>
</ChatLayout>
<ChatInjection />
</>
);
@@ -5,7 +5,7 @@ import {
fileExtensionToEditorLang,
} from "@llamaindex/chat-ui/widgets";
import { AlertCircle, Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { Button } from "../button";
import { getConfig } from "../lib/utils";
@@ -144,9 +144,12 @@ function DevModePanelComp() {
}
}, [devModeOpen]);
const codeEditorLanguage = fileExtensionToEditorLang(
workflowFile?.file_path.split(".").pop() ?? "",
);
const codeEditorLanguage = useMemo(() => {
if (!workflowFile?.file_path) return undefined;
return fileExtensionToEditorLang(
workflowFile.file_path.split(".").pop() ?? "",
);
}, [workflowFile]);
return (
<>
@@ -0,0 +1,40 @@
"use client";
import { Sparkles, Star } from "lucide-react";
export function DefaultHeader() {
return (
<div className="flex items-center justify-between px-4 pt-2">
<div className="flex items-center gap-2">
<Sparkles className="size-4" />
<h1 className="font-semibold">LlamaIndex App</h1>
</div>
<div className="flex items-center justify-end gap-4">
<div className="flex items-center gap-2">
<a
href="https://www.llamaindex.ai/"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
>
Built by LlamaIndex
</a>
<img
className="h-[24px] w-[24px] rounded-sm"
src="/llama.png"
alt="Llama Logo"
/>
</div>
<a
href="https://github.com/run-llama/LlamaIndexTS"
target="_blank"
rel="noopener noreferrer"
className="hover:bg-accent flex items-center gap-2 rounded-md border border-gray-300 px-2 py-1 text-sm"
>
<Star className="size-4" />
Star on GitHub
</a>
</div>
</div>
);
}
@@ -0,0 +1,133 @@
"use client";
import { Loader2 } from "lucide-react";
import React, { FunctionComponent, useEffect, useState } from "react";
import { getConfig } from "../../lib/utils";
import { DynamicComponentErrorBoundary } from "../custom/events/error-boundary";
import { parseComponent } from "../custom/events/loader";
import { DefaultHeader } from "./header";
type LayoutFile = {
type: "header" | "footer";
code: string;
filename: string;
};
type LayoutComponent = LayoutFile & {
component?: FunctionComponent | null;
error?: string;
};
export function ChatLayout({ children }: { children: React.ReactNode }) {
const [layoutComponents, setLayoutComponents] = useState<LayoutComponent[]>(
[],
);
const [isRendering, setIsRendering] = useState(false);
const [errors, setErrors] = useState<string[]>([]);
useEffect(() => {
const loadLayout = async () => {
setIsRendering(true);
const layoutFiles = await fetchLayoutFiles();
if (layoutFiles.length) {
const layoutComponents = await parseLayoutComponents(layoutFiles);
setLayoutComponents(layoutComponents);
setErrors((errors) => [
...errors,
...(layoutComponents.map((c) => c.error).filter(Boolean) as string[]),
]);
}
setIsRendering(false);
};
loadLayout();
}, []);
const handleError = (error: string) => {
setErrors((prev) => [...prev, error]);
};
const getLayoutCode = (type: "header" | "footer") => {
return layoutComponents.find((c) => c.type === type)?.component;
};
if (isRendering) {
return (
<div className="flex h-screen w-screen flex-col items-center justify-center overflow-hidden">
<Loader2 className="text-muted-foreground animate-spin" />
</div>
);
}
const uniqueErrors = [...new Set(errors)];
return (
<div className="flex h-screen w-screen flex-col overflow-hidden">
{uniqueErrors.length > 0 && (
<div className="w-full bg-yellow-100 px-4 py-2 text-black/70">
<h2 className="mb-2 font-semibold">
Errors happened while rendering the layout:
</h2>
{uniqueErrors.map((error) => (
<div key={error} className="text-sm">
{error}
</div>
))}
</div>
)}
<LayoutRenderer
component={getLayoutCode("header")}
onError={handleError}
fallback={<DefaultHeader />}
/>
{children}
<LayoutRenderer
component={getLayoutCode("footer")}
onError={handleError}
/>
</div>
);
}
function LayoutRenderer({
component,
onError,
fallback,
}: {
component?: FunctionComponent | null;
onError: (error: string) => void;
fallback?: React.ReactNode;
}) {
if (!component) return fallback;
return (
<DynamicComponentErrorBoundary onError={onError} fallback={fallback}>
{React.createElement(component)}
</DynamicComponentErrorBoundary>
);
}
async function parseLayoutComponents(layoutFiles: LayoutFile[]) {
const layoutComponents: LayoutComponent[] = await Promise.all(
layoutFiles.map(async (layoutFile) => {
const result = await parseComponent(layoutFile.code, layoutFile.filename);
return { ...layoutFile, ...result };
}),
);
return layoutComponents;
}
async function fetchLayoutFiles(): Promise<LayoutFile[]> {
try {
const response = await fetch(getConfig("LAYOUT_API"));
const layoutFiles: LayoutFile[] = await response.json();
return layoutFiles;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
console.warn("Error fetching layout files: ", errorMessage);
return [];
}
}
-1
View File
@@ -1,6 +1,5 @@
window.LLAMAINDEX = {
CHAT_API: "/api/chat",
APP_TITLE: "Deep Research App",
LLAMA_CLOUD_API: undefined,
STARTER_QUESTIONS: [
"Research about Apple and Tesla revenue",
+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.3",
"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.4",
"@llamaindex/chat-ui": "0.4.5",
"@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);
}
+4 -1
View File
@@ -16,6 +16,7 @@ export const handleChat = async (
req: IncomingMessage,
res: ServerResponse,
workflowFactory: WorkflowFactory,
suggestNextQuestions: boolean,
) => {
try {
const body = await parseRequestBody(req);
@@ -53,7 +54,9 @@ export const handleChat = async (
role: "assistant" as MessageType,
content: completion,
});
await sendSuggestedQuestionsEvent(dataStreamWriter, chatHistory);
if (suggestNextQuestions) {
await sendSuggestedQuestionsEvent(dataStreamWriter, chatHistory);
}
},
},
});
-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",
});
}
};
@@ -1,69 +0,0 @@
import fs from "fs";
import type { IncomingMessage, ServerResponse } from "http";
import path from "path";
import { promisify } from "util";
import { sendJSONResponse } from "../utils/request";
export const getComponents = async (
req: IncomingMessage,
res: ServerResponse,
componentsDir: string,
) => {
try {
const exists = await promisify(fs.exists)(componentsDir);
if (!exists) {
return sendJSONResponse(res, 404, {
error: "Components directory not found",
});
}
const files = await promisify(fs.readdir)(componentsDir);
// filter files with valid extensions
const validExtensions = [".tsx", ".jsx"];
const filteredFiles = files.filter((file) =>
validExtensions.includes(path.extname(file)),
);
// filter duplicate components
const uniqueFiles = filterDuplicateComponents(filteredFiles);
const components = await Promise.all(
uniqueFiles.map(async (file) => {
const filePath = path.join(componentsDir, file);
const content = await promisify(fs.readFile)(filePath, "utf-8");
return {
type: path.basename(file, path.extname(file)),
code: content,
filename: file,
};
}),
);
sendJSONResponse(res, 200, components);
} catch (error) {
console.error("Error reading components:", error);
sendJSONResponse(res, 500, { error: "Failed to read components" });
}
};
function filterDuplicateComponents(files: string[]) {
const compMap = new Map<string, string>();
for (const file of files) {
const type = path.basename(file, path.extname(file));
if (compMap.has(type)) {
const existingComp = compMap.get(type)!;
if (file.endsWith(".tsx") && !existingComp.endsWith(".tsx")) {
// prefer .tsx files over others
console.warn(`Preferring ${file} over ${existingComp}`);
compMap.set(type, file);
}
} else {
compMap.set(type, file);
}
}
return Array.from(compMap.values());
}
-106
View File
@@ -1,106 +0,0 @@
import { exec } from "child_process";
import fs from "fs";
import type { IncomingMessage, ServerResponse } from "http";
import path from "path";
import { promisify } from "util";
import { parseRequestBody, 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" });
}
};
const DEFAULT_WORKFLOW_FILE_PATH = "src/app/workflow.ts"; // TODO: we can make it as a parameter in server later
export const getWorkflowFile = async (
req: IncomingMessage,
res: ServerResponse,
filePath: string = DEFAULT_WORKFLOW_FILE_PATH,
) => {
const fileExists = await promisify(fs.exists)(filePath);
if (!fileExists) {
return sendJSONResponse(res, 404, {
detail: `Dev mode is currently in beta. It only supports updating workflow file at ${DEFAULT_WORKFLOW_FILE_PATH}`,
});
}
const content = await promisify(fs.readFile)(filePath, "utf-8");
const last_modified = fs.statSync(filePath).mtime.getTime();
sendJSONResponse(res, 200, { content, file_path: filePath, last_modified });
};
export const updateWorkflowFile = async (
req: IncomingMessage,
res: ServerResponse,
filePath: string = DEFAULT_WORKFLOW_FILE_PATH,
) => {
const body = await parseRequestBody(req);
const { content } = body as { content: string };
const fileExists = await promisify(fs.exists)(filePath);
if (!fileExists) {
return sendJSONResponse(res, 404, {
detail: `Dev mode is currently in beta. It only supports updating workflow file at ${DEFAULT_WORKFLOW_FILE_PATH}`,
});
}
try {
const resolvedFilePath = path.resolve(DEFAULT_WORKFLOW_FILE_PATH);
const result = await validateTypeScriptFile(resolvedFilePath, content);
if (!result.isValid) {
return sendJSONResponse(res, 400, {
detail: result.errors.join("\n"),
});
}
await promisify(fs.writeFile)(filePath, content);
sendJSONResponse(res, 200, { content });
} catch (error) {
console.error("Error updating workflow file:", error);
sendJSONResponse(res, 500, { error: "Failed to update workflow file" });
}
};
// use typescript package to validate the file syntax and imports
async function validateTypeScriptFile(filePath: string, content: string) {
// Update workflow file directly will cause the server restart immediately.
// So we create a temporary file with the same content in the same directory as the workflow file
// This file will be used to validate the file syntax and imports. It will be deleted after validation.
const tempFilePath = path.join(
path.dirname(filePath),
`workflow_${Date.now()}.ts`,
);
fs.writeFileSync(tempFilePath, content);
const errors = [];
try {
const tscCommand = `npx tsc ${tempFilePath} --noEmit --skipLibCheck true`;
await promisify(exec)(tscCommand);
} catch (error) {
const errorMessage = (error as { stdout: string })?.stdout;
errors.push(errorMessage);
} finally {
// Clean up temporary file
if (fs.existsSync(tempFilePath)) fs.unlinkSync(tempFilePath);
}
return {
isValid: errors.length === 0,
errors: errors,
};
}
+1
View File
@@ -1,4 +1,5 @@
export * from "./events";
export * from "./prompts";
export * from "./server";
export * from "./types";
export { generateEventComponent } from "./utils/gen-ui";
+14
View File
@@ -0,0 +1,14 @@
export const NEXT_QUESTION_PROMPT = `You're a helpful assistant!
Your task is to suggest the next question that user might ask.
Here is the conversation history
---------------------
{conversation}
---------------------
Given the conversation history, please give me 3 questions that user might ask next!
Your answer should be wrapped in three sticks which follows the following format:
\`\`\`
<question 1>
<question 2>
<question 3>
\`\`\`
`;
+23 -32
View File
@@ -7,13 +7,6 @@ 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 {
getWorkflowFile,
handleServeFiles,
updateWorkflowFile,
} from "./handlers/files";
import type { LlamaIndexServerOptions } from "./types";
const nextDir = path.join(__dirname, "..", "server");
@@ -25,13 +18,17 @@ export class LlamaIndexServer {
app: ReturnType<typeof next>;
workflowFactory: () => Promise<Workflow> | Workflow;
componentsDir?: string | undefined;
layoutDir: string;
suggestNextQuestions: boolean;
constructor(options: LlamaIndexServerOptions) {
const { workflow, ...nextAppOptions } = options;
const { workflow, suggestNextQuestions, ...nextAppOptions } = options;
this.app = next({ dev, dir: nextDir, ...nextAppOptions });
this.port = nextAppOptions.port ?? parseInt(process.env.PORT || "3000", 10);
this.workflowFactory = workflow;
this.componentsDir = options.uiConfig?.componentsDir;
this.layoutDir = options.uiConfig?.layoutDir ?? "layout";
this.suggestNextQuestions = suggestNextQuestions ?? true;
if (this.componentsDir) {
this.createComponentsDir(this.componentsDir);
@@ -42,24 +39,25 @@ export class LlamaIndexServer {
private modifyConfig(options: LlamaIndexServerOptions) {
const { uiConfig } = options;
const appTitle = uiConfig?.appTitle ?? "LlamaIndex App";
const starterQuestions = uiConfig?.starterQuestions ?? [];
const llamaCloudApi =
uiConfig?.llamaCloudIndexSelector && getEnv("LLAMA_CLOUD_API_KEY")
? "/api/chat/config/llamacloud"
: undefined;
const componentsApi = this.componentsDir ? "/api/components" : undefined;
const layoutApi = this.layoutDir ? "/api/layout" : undefined;
const devMode = uiConfig?.devMode ?? false;
// content in javascript format
const content = `
window.LLAMAINDEX = {
CHAT_API: '/api/chat',
APP_TITLE: ${JSON.stringify(appTitle)},
LLAMA_CLOUD_API: ${JSON.stringify(llamaCloudApi)},
STARTER_QUESTIONS: ${JSON.stringify(starterQuestions)},
COMPONENTS_API: ${JSON.stringify(componentsApi)},
DEV_MODE: ${JSON.stringify(devMode)}
LAYOUT_API: ${JSON.stringify(layoutApi)},
DEV_MODE: ${JSON.stringify(devMode)},
SUGGEST_NEXT_QUESTIONS: ${JSON.stringify(this.suggestNextQuestions)}
}
`;
fs.writeFileSync(configFile, content);
@@ -78,13 +76,18 @@ 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") {
return handleChat(req, res, this.workflowFactory);
}
if (pathname?.startsWith("/api/files") && req.method === "GET") {
return handleServeFiles(req, res, pathname);
// 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,
this.suggestNextQuestions,
);
}
if (
@@ -92,27 +95,15 @@ export class LlamaIndexServer {
pathname === "/api/components" &&
req.method === "GET"
) {
return getComponents(req, res, this.componentsDir);
query.componentsDir = this.componentsDir;
}
if (
getEnv("LLAMA_CLOUD_API_KEY") &&
pathname === "/api/chat/config/llamacloud" &&
req.method === "GET"
) {
return getLlamaCloudConfig(req, res);
}
if (pathname === "/api/dev/files/workflow" && req.method === "GET") {
return getWorkflowFile(req, res);
}
if (pathname === "/api/dev/files/workflow" && req.method === "PUT") {
return updateWorkflowFile(req, res);
if (pathname === "/api/layout" && req.method === "GET") {
query.layoutDir = this.layoutDir;
}
const handle = this.app.getRequestHandler();
handle(req, res, parsedUrl);
handle(req, res, { ...parsedUrl, query });
});
server.listen(this.port, () => {
+2 -1
View File
@@ -13,9 +13,9 @@ export type WorkflowFactory = (
export type NextAppOptions = Parameters<typeof next>[0];
export type UIConfig = {
appTitle?: string;
starterQuestions?: string[];
componentsDir?: string;
layoutDir?: string;
llamaCloudIndexSelector?: boolean;
devMode?: boolean;
};
@@ -23,4 +23,5 @@ export type UIConfig = {
export type LlamaIndexServerOptions = NextAppOptions & {
workflow: WorkflowFactory;
uiConfig?: UIConfig;
suggestNextQuestions?: boolean;
};
+4 -18
View File
@@ -1,19 +1,7 @@
import { getEnv } from "@llamaindex/env";
import type { DataStreamWriter } from "ai";
import { type ChatMessage, Settings } from "llamaindex";
const NEXT_QUESTION_PROMPT = `You're a helpful assistant! Your task is to suggest the next question that user might ask.
Here is the conversation history
---------------------
{conversation}
---------------------
Given the conversation history, please give me 3 questions that user might ask next!
Your answer should be wrapped in three sticks which follows the following format:
\`\`\`
<question 1>
<question 2>
<question 3>
\`\`\`
`;
import { NEXT_QUESTION_PROMPT } from "../prompts";
export const sendSuggestedQuestionsEvent = async (
streamWriter: DataStreamWriter,
@@ -32,10 +20,8 @@ export async function generateNextQuestions(conversation: ChatMessage[]) {
const conversationText = conversation
.map((message) => `${message.role}: ${message.content}`)
.join("\n");
const message = NEXT_QUESTION_PROMPT.replace(
"{conversation}",
conversationText,
);
const promptTemplate = getEnv("NEXT_QUESTION_PROMPT") || NEXT_QUESTION_PROMPT;
const message = promptTemplate.replace("{conversation}", conversationText);
try {
const response = await Settings.llm.complete({ prompt: message });
+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"]
}
+629 -116
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -1,3 +1,4 @@
packages:
- "packages/*"
- "packages/server/examples"
- "python/*"
+1
View File
@@ -5,6 +5,7 @@
**/venv
**/env
**/llama-index-server.egg-info
llama_index/server/resources/ui
# Jupyter files
**/*.ipynb
+11
View File
@@ -0,0 +1,11 @@
# @create-llama/llama-index-server
## 0.1.17
### Patch Changes
- 0bc5a0d: Add suggestNextQuestions config
- Updated dependencies [eee3230]
- Updated dependencies [0bc5a0d]
- Updated dependencies [3acec88]
- @llamaindex/server@0.2.3
+32 -14
View File
@@ -44,7 +44,6 @@ app = LlamaIndexServer(
workflow_factory=create_workflow, # Supports Workflow or AgentWorkflow
env="dev", # Enable development mode
ui_config={ # Configure the chat UI, optional
"app_title": "Weather Bot",
"starter_questions": ["What is the weather in LA?", "Will it rain in SF?"],
},
verbose=True
@@ -72,22 +71,48 @@ 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)
- `ui_config`: UI configuration as a dictionary or UIConfig object with options:
- `enabled`: Whether to enable the chat UI (default: True)
- `app_title`: The title of the chat application (default: "LlamaIndex Server")
- `starter_questions`: List of starter questions for the chat UI (default: None)
- `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.
- `layout_dir`: The directory for custom layout sections. The default value is `layout`. See [Custom Layout](https://github.com/run-llama/create-llama/blob/main/python/llama-index-server/docs/custom_layout.md) for more details.
- `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.
- `suggest_next_questions`: Whether to suggest next questions after the assistant's response (default: True). You can change the prompt for the next questions by setting the `NEXT_QUESTION_PROMPT` environment variable. The default prompt used is defined in `llama_index.server.prompts.SUGGEST_NEXT_QUESTION_PROMPT`.
- `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
@@ -108,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:
@@ -135,7 +155,6 @@ app = LlamaIndexServer(
**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:
@@ -146,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,22 @@
# Custom Layout
LlamaIndex Server supports custom layout for header and footer. To use custom layout, you need to initialize the LlamaIndex server with the `layout_dir` that contains your custom layout files.
```python
server = LlamaIndexServer(
workflow_factory=your_workflow,
ui_config={
"layout_dir": "path/to/layout",
},
include_ui=True
)
```
```
layout/
header.tsx
footer.tsx
```
We currently support custom header and footer for the chat interface. The syntax for these files is the same as events components in components directory (see [Custom UI Component](./custom_ui_component.md) for more details).
Note that by default, we are still rendering the default LlamaIndex Header. It's also the fallback when having errors rendering the custom header. Example layout files will be generated in the `layout` directory of your project when creating a new project with `create-llama`.
@@ -0,0 +1,40 @@
"use client";
import { Sparkles, Star } from "lucide-react";
export default function Header() {
return (
<div className="flex items-center justify-between px-4 pt-2">
<div className="flex items-center gap-2">
<Sparkles className="size-4" />
<h1 className="font-semibold">Artifact Workflow</h1>
</div>
<div className="flex items-center justify-end gap-4">
<div className="flex items-center gap-2">
<a
href="https://www.llamaindex.ai/"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
>
Built by LlamaIndex
</a>
<img
className="h-[24px] w-[24px] rounded-sm"
src="/llama.png"
alt="Llama Logo"
/>
</div>
<a
href="https://github.com/run-llama/LlamaIndexTS"
target="_blank"
rel="noopener noreferrer"
className="hover:bg-accent flex items-center gap-2 rounded-md border border-gray-300 px-2 py-1 text-sm"
>
<Star className="size-4" />
Star on GitHub
</a>
</div>
</div>
);
}
@@ -23,12 +23,12 @@ def create_app() -> FastAPI:
app = LlamaIndexServer(
workflow_factory=create_workflow,
ui_config=UIConfig(
app_title="Artifact",
starter_questions=[
"Write a simple calculator app",
"Write a guideline on how to use LLM effectively",
],
component_dir="components",
layout_dir="layout",
),
)
return app
@@ -7,11 +7,12 @@ from llama_index.server import LlamaIndexServer, UIConfig
def create_app() -> FastAPI:
app = LlamaIndexServer(
workflow_factory=create_workflow,
suggest_next_questions=True,
env="dev",
ui_config=UIConfig(
app_title="Artifact",
starter_questions=[
"Tell me a funny joke.",
"Tell me some jokes about AI.",
"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
@@ -3,7 +3,7 @@ import os
from enum import Enum
from typing import Any, Dict, List, Literal, Optional, Union
from pydantic import BaseModel, Field, field_validator
from pydantic import BaseModel, field_validator
from llama_index.core.schema import NodeWithScore
from llama_index.core.types import ChatMessage, MessageRole
@@ -13,13 +13,6 @@ from llama_index.server.settings import server_settings
logger = logging.getLogger("uvicorn")
class ChatConfig(BaseModel):
next_question_suggestions: bool = Field(
default=True,
description="Whether to suggest next questions",
)
class ChatAPIMessage(BaseModel):
role: MessageRole
content: str
@@ -32,7 +25,6 @@ class ChatAPIMessage(BaseModel):
class ChatRequest(BaseModel):
messages: List[ChatAPIMessage]
data: Optional[Any] = None
config: Optional[ChatConfig] = ChatConfig()
@field_validator("messages")
def validate_messages(cls, v: List[ChatAPIMessage]) -> List[ChatAPIMessage]:
@@ -1,9 +1,13 @@
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.ui import (
custom_components_router,
custom_layout_router,
)
from llama_index.server.api.routers.dev import dev_router
__all__ = [
"chat_router",
"custom_components_router",
"custom_layout_router",
"dev_router",
]
@@ -28,6 +28,7 @@ from llama_index.server.services.llamacloud import LlamaCloudFileService
def chat_router(
workflow_factory: Callable[..., Workflow],
logger: logging.Logger,
suggest_next_questions: bool = True,
) -> APIRouter:
router = APIRouter(prefix="/chat")
@@ -56,7 +57,7 @@ def chat_router(
SourceNodesFromToolCall(),
LlamaCloudFileDownload(background_tasks),
]
if request.config and request.config.next_question_suggestions:
if suggest_next_questions:
callbacks.append(SuggestNextQuestions(request))
stream_handler = StreamHandler(
workflow_handler=workflow_handler,
@@ -14,7 +14,23 @@ def custom_components_router(
@router.get("")
async def components() -> List[ComponentDefinition]:
custom_ui = CustomUI(component_dir=component_dir, logger=logger)
return custom_ui.get_components()
custom_ui = CustomUI(logger=logger)
return custom_ui.get_components(directory=component_dir)
return router
def custom_layout_router(
layout_dir: str,
logger: logging.Logger,
) -> APIRouter:
router = APIRouter(prefix="/layout")
@router.get("")
async def layout() -> List[ComponentDefinition]:
custom_ui = CustomUI(logger=logger)
return custom_ui.get_components(
directory=layout_dir, filter_types=["header", "footer"]
)
return router
@@ -1,55 +1,87 @@
import importlib.resources
import logging
import shutil
from pathlib import Path
from typing import Optional
import requests
CHAT_UI_VERSION = "0.1.6"
PACKAGE_NAME = "llama_index.server.resources"
RESOURCE_DIR_NAME = "ui"
def download_chat_ui(
def check_ui_resources() -> None:
"""
Checks if the UI resources directory exists in the specified package and lists its contents.
Raises a FileNotFoundError with a clear message if the directory is missing.
"""
try:
_ = importlib.resources.files(PACKAGE_NAME).joinpath(RESOURCE_DIR_NAME)
except Exception as e:
raise Exception("UI resources not found in bundled package") from e
def copy_bundled_chat_ui(
logger: Optional[logging.Logger] = None, target_path: str = ".ui"
) -> None:
# Check if the UI resources directory exists
check_ui_resources()
if logger is None:
logger = logging.getLogger("uvicorn")
path = Path(target_path)
temp_dir = _download_package(_get_download_link(CHAT_UI_VERSION))
_copy_ui_files(temp_dir, path)
logger.info("Chat UI downloaded and copied to static folder")
destination_path = Path(target_path)
destination_path.mkdir(parents=True, exist_ok=True)
def _get_download_link(version: str) -> str:
"""Get the download link for the chat UI from the npm registry."""
return f"https://registry.npmjs.org/@llamaindex/server/-/server-{version}.tgz"
def _download_package(url: str) -> Path:
"""Download tar.gz file and extract all files into a temporary directory."""
import io
import tarfile
import tempfile
response = requests.get(url, headers={"User-Agent": "Mozilla/5.0"})
content = response.content
temp_dir = Path(tempfile.mkdtemp())
with tarfile.open(fileobj=io.BytesIO(content), mode="r:gz") as tar:
tar.extractall(path=temp_dir)
return temp_dir
def _copy_ui_files(temp_dir: Path, target_path: Path) -> None:
"""Copy files from the .next directory to the static directory."""
target_path.mkdir(parents=True, exist_ok=True)
next_dir = temp_dir / "package/dist/static"
if next_dir.exists():
for item in next_dir.iterdir():
dest = target_path / item.name
try:
# Clear the destination directory first to avoid stale files
for item in destination_path.iterdir():
if item.is_dir():
shutil.copytree(item, dest, dirs_exist_ok=True)
shutil.rmtree(item)
else:
shutil.copy2(item, dest)
item.unlink()
# Get a reference to the source directory using importlib.resources.files (Python 3.9+)
source_dir_ref = importlib.resources.files(PACKAGE_NAME).joinpath(
RESOURCE_DIR_NAME
)
if not source_dir_ref.is_dir():
logger.error(
f"Static UI resource directory '{RESOURCE_DIR_NAME}' not found in package '{PACKAGE_NAME}'. Path: {source_dir_ref}"
)
logger.error(
"Ensure the static files are correctly bundled with the package and the path is correct."
)
return
for source_item_path_ref in source_dir_ref.iterdir():
# Skip __init__.py or other non-static files if present (though less likely needed with direct iteration)
if source_item_path_ref.name.startswith(
"__"
) or source_item_path_ref.name.endswith(".py"):
continue
dest_item_path = destination_path / source_item_path_ref.name
# importlib.resources.as_file is needed to get a concrete path for shutil operations
with importlib.resources.as_file(
source_item_path_ref
) as concrete_source_item_path:
if concrete_source_item_path.is_dir():
shutil.copytree(
concrete_source_item_path, dest_item_path, dirs_exist_ok=True
)
elif concrete_source_item_path.is_file():
shutil.copy2(concrete_source_item_path, dest_item_path)
else:
logger.warning(
f"Skipping resource '{source_item_path_ref.name}' as it's not a file or directory."
)
logger.info(f"Chat UI files copied from package to '{destination_path}'")
except FileNotFoundError:
logger.error(
"Oops! The chat UI files are not found. Please report this issue to the LlamaIndex team."
)
except Exception as e:
logger.error(f"Failed to copy bundled chat UI files: {e}.")
@@ -0,0 +1,15 @@
# Used by SuggestNextQuestionsService
# Override this prompt by setting the `NEXT_QUESTION_PROMPT` environment variable
SUGGEST_NEXT_QUESTION_PROMPT = """You're a helpful assistant! Your task is to suggest the next questions that user might interested in to keep the conversation going.
Here is the conversation history
---------------------
{conversation}
---------------------
Given the conversation history, please give me 3 questions that user might ask next!
Your answer should be wrapped in three sticks without any index numbers and follows the following format:
\`\`\`
<question 1>
<question 2>
<question 3>
\`\`\`
"""
@@ -13,17 +13,15 @@ from llama_index.core.workflow import Workflow
from llama_index.server.api.routers import (
chat_router,
custom_components_router,
custom_layout_router,
dev_router,
)
from llama_index.server.chat_ui import download_chat_ui
from llama_index.server.chat_ui import copy_bundled_chat_ui
from llama_index.server.settings import server_settings
class UIConfig(BaseModel):
enabled: bool = Field(default=True, description="Whether to enable the chat UI")
app_title: str = Field(
default="LlamaIndex Server", description="The title of the chat UI"
)
starter_questions: Optional[list[str]] = Field(
default=None, description="The starter questions for the chat UI"
)
@@ -37,6 +35,10 @@ class UIConfig(BaseModel):
component_dir: Optional[str] = Field(
default=None, description="The directory to custom UI components code"
)
layout_dir: str = Field(
default="layout",
description="The directory to custom UI layout such as header and footer",
)
dev_mode: bool = Field(
default=False, description="Whether to enable the UI dev mode"
)
@@ -46,13 +48,20 @@ class UIConfig(BaseModel):
{
"CHAT_API": f"{server_settings.api_url}/chat",
"STARTER_QUESTIONS": self.starter_questions or [],
"LLAMA_CLOUD_API": f"{server_settings.api_url}/chat/config/llamacloud"
if self.llamacloud_index_selector and os.getenv("LLAMA_CLOUD_API_KEY")
else None,
"APP_TITLE": self.app_title,
"COMPONENTS_API": f"{server_settings.api_url}/components"
if self.component_dir
else None,
"LLAMA_CLOUD_API": (
f"{server_settings.api_url}/chat/config/llamacloud"
if self.llamacloud_index_selector
and os.getenv("LLAMA_CLOUD_API_KEY")
else None
),
"COMPONENTS_API": (
f"{server_settings.api_url}/components"
if self.component_dir
else None
),
"LAYOUT_API": (
f"{server_settings.api_url}/layout" if self.layout_dir else None
),
"DEV_MODE": self.dev_mode,
},
indent=2,
@@ -68,11 +77,12 @@ class LlamaIndexServer(FastAPI):
self,
workflow_factory: Callable[..., Workflow],
logger: Optional[logging.Logger] = None,
use_default_routers: Optional[bool] = True,
use_default_routers: Optional[bool] = None,
env: Optional[str] = None,
ui_config: Optional[Union[UIConfig, dict]] = None,
server_url: Optional[str] = None,
api_prefix: Optional[str] = None,
suggest_next_questions: Optional[bool] = None,
verbose: bool = False,
*args: Any,
**kwargs: Any,
@@ -88,6 +98,7 @@ class LlamaIndexServer(FastAPI):
ui_config: The configuration for the chat UI.
server_url: The URL of the server.
api_prefix: The prefix for the API endpoints.
suggest_next_questions: Whether to suggest next questions after the assistant's response.
verbose: Whether to show verbose logs.
"""
super().__init__(*args, **kwargs)
@@ -95,7 +106,12 @@ class LlamaIndexServer(FastAPI):
self.workflow_factory = workflow_factory
self.logger = logger or logging.getLogger("uvicorn")
self.verbose = verbose
self.use_default_routers = use_default_routers or True
self.use_default_routers = (
True if use_default_routers is None else use_default_routers
)
self.suggest_next_questions = (
True if suggest_next_questions is None else suggest_next_questions
)
if ui_config is None:
self.ui_config = UIConfig()
elif isinstance(ui_config, dict):
@@ -146,6 +162,7 @@ class LlamaIndexServer(FastAPI):
chat_router(
self.workflow_factory,
self.logger,
self.suggest_next_questions,
),
prefix=server_settings.api_prefix,
)
@@ -162,6 +179,15 @@ class LlamaIndexServer(FastAPI):
prefix=server_settings.api_prefix,
)
def add_layout_router(self) -> None:
"""
Add the layout router.
"""
self.include_router(
custom_layout_router(self.ui_config.layout_dir, self.logger),
prefix=server_settings.api_prefix,
)
def mount_ui(self) -> None:
"""
Mount the UI.
@@ -173,13 +199,20 @@ class LlamaIndexServer(FastAPI):
if not os.path.exists(self.ui_config.component_dir):
os.makedirs(self.ui_config.component_dir)
self.add_components_router()
# Layout dir
if self.ui_config.layout_dir:
if not os.path.exists(self.ui_config.layout_dir):
os.makedirs(self.ui_config.layout_dir)
self.add_layout_router()
# UI static files
if not os.path.exists(self.ui_config.ui_path):
os.makedirs(self.ui_config.ui_path)
self.logger.warning(
f"UI files not found, downloading UI to {self.ui_config.ui_path}"
f"UI files not found at {self.ui_config.ui_path}. Copying bundled UI files."
)
copy_bundled_chat_ui(
logger=self.logger, target_path=self.ui_config.ui_path
)
download_chat_ui(logger=self.logger, target_path=self.ui_config.ui_path)
self._mount_static_files(
directory=self.ui_config.ui_path,
path="/",
@@ -6,31 +6,28 @@ from llama_index.server.api.models import ComponentDefinition
class CustomUI:
def __init__(
self, component_dir: str, logger: Optional[logging.Logger] = None
) -> None:
self.component_dir = component_dir
def __init__(self, logger: Optional[logging.Logger] = None) -> None:
self.logger = logger or logging.getLogger(__name__)
def get_components(self) -> List[ComponentDefinition]:
def get_components(
self, directory: str, filter_types: Optional[List[str]] = None
) -> List[ComponentDefinition]:
"""
List all js files in the component directory and return a list of ComponentDefinition objects.
Ignores files that fail to load and logs the error.
TSX files take precedence over JSX files when duplicate component names are found.
"""
components_dict: dict[str, ComponentDefinition] = {}
if not os.path.exists(self.component_dir):
self.logger.warning(
f"Component directory {self.component_dir} does not exist"
)
if not os.path.exists(directory):
self.logger.warning(f"Component directory {directory} does not exist")
return []
try:
for file in os.listdir(self.component_dir):
for file in os.listdir(directory):
if not file.endswith((".jsx", ".tsx")):
continue
component_name = file.split(".")[0]
file_path = os.path.join(self.component_dir, file)
file_path = os.path.join(directory, file)
file_ext = os.path.splitext(file)[1]
try:
@@ -78,4 +75,11 @@ class CustomUI:
except Exception as e:
self.logger.error(f"Error reading component directory: {str(e)}")
return list(components_dict.values())
result = list(components_dict.values())
if filter_types:
result = [
component for component in result if component.type in filter_types
]
return result
@@ -6,6 +6,7 @@ from typing import List, Optional, Union
from llama_index.core.prompts import PromptTemplate
from llama_index.core.settings import Settings
from llama_index.server.api.models import ChatAPIMessage
from llama_index.server.prompts import SUGGEST_NEXT_QUESTION_PROMPT
logger = logging.getLogger("uvicorn")
@@ -15,28 +16,11 @@ class SuggestNextQuestionsService:
Suggest the next questions that user might ask based on the conversation history.
"""
prompt = PromptTemplate(
r"""
You're a helpful assistant! Your task is to suggest the next questions that user might interested in to keep the conversation going.
Here is the conversation history
---------------------
{conversation}
---------------------
Given the conversation history, please give me 3 questions that user might ask next!
Your answer should be wrapped in three sticks without any index numbers and follows the following format:
\`\`\`
<question 1>
<question 2>
<question 3>
\`\`\`
"""
)
@classmethod
def get_configured_prompt(cls) -> PromptTemplate:
prompt = os.getenv("NEXT_QUESTION_PROMPT", None)
if not prompt:
return cls.prompt
return PromptTemplate(SUGGEST_NEXT_QUESTION_PROMPT)
return PromptTemplate(prompt)
@classmethod

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