mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-15 13:15:42 -04:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c5fc33c42 | |||
| 590637e862 | |||
| 38e3b9a88b | |||
| 82ac925224 | |||
| 111a869a99 | |||
| f24ee8e6f9 | |||
| 078b31de1c | |||
| 3acec88fbc | |||
| eee3230e99 | |||
| d8425e5290 | |||
| 0bc5a0d882 | |||
| bbae802bed | |||
| 25fba4381b | |||
| d0618fa2fa | |||
| f3fe3ffc9b | |||
| 6f75d4ab6e | |||
| 3242738fe4 | |||
| 17538eb0dd |
@@ -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,6 +159,7 @@ 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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# 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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.5.15",
|
||||
"version": "0.5.17",
|
||||
"description": "Create LlamaIndex-powered apps with one command",
|
||||
"keywords": [
|
||||
"rag",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -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' \
|
||||
|
||||
+3
@@ -89,6 +89,9 @@ export function workflowFactory(reqBody: any) {
|
||||
|
||||
return planEvent.with({
|
||||
userInput: userInput,
|
||||
context: state.lastArtifact
|
||||
? JSON.stringify(state.lastArtifact)
|
||||
: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+2
-2
@@ -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' \
|
||||
|
||||
+25
-3
@@ -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)
|
||||
|
||||
+38
-2
@@ -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
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -16,7 +16,6 @@ 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,
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
"start": "tsx src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@llamaindex/openai": "^0.3.7",
|
||||
"@llamaindex/server": "^0.2.1",
|
||||
"@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,6 @@ initSettings();
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
uiConfig: {
|
||||
appTitle: "LlamaIndex App",
|
||||
componentsDir: "components",
|
||||
devMode: true,
|
||||
},
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# @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
|
||||
|
||||
@@ -30,7 +30,6 @@ 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();
|
||||
@@ -60,11 +59,12 @@ The `LlamaIndexServer` accepts the following configuration options:
|
||||
|
||||
- `workflow`: A callable function that creates a workflow instance for each request. See [Workflow factory contract](#workflow-factory-contract) for more details.
|
||||
- `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).
|
||||
@@ -186,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:
|
||||
@@ -194,7 +216,6 @@ To use the generated UI components, you need to initialize the LlamaIndex server
|
||||
new LlamaIndexServer({
|
||||
workflow: createWorkflow,
|
||||
uiConfig: {
|
||||
appTitle: "LlamaIndex App",
|
||||
componentsDir: "components",
|
||||
},
|
||||
}).start();
|
||||
|
||||
@@ -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,8 +30,8 @@ export const workflowFactory = async () => {
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
suggestNextQuestions: true,
|
||||
uiConfig: {
|
||||
appTitle: "LlamaIndex App",
|
||||
starterQuestions: ["What is the color of the dog?"],
|
||||
},
|
||||
port: 3000,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,6 @@ First, we need to set `devMode` to `true` in the `uiConfig` of the server.
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
uiConfig: {
|
||||
appTitle: "Calculator",
|
||||
devMode: true,
|
||||
},
|
||||
port: 3000,
|
||||
@@ -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 nodemon --exec tsx index.ts
|
||||
npx nodemon --exec tsx index.ts --ignore src/app/workflow_*.ts
|
||||
```
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
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?",
|
||||
|
||||
@@ -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();
|
||||
@@ -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,7 +20,6 @@ const calculatorAgent = agent({
|
||||
new LlamaIndexServer({
|
||||
workflow: () => calculatorAgent,
|
||||
uiConfig: {
|
||||
appTitle: "Calculator",
|
||||
starterQuestions: ["1 + 1", "2 + 2"],
|
||||
},
|
||||
port: 3000,
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"target": "ES2017"
|
||||
"target": "ES2022"
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@llamaindex/server",
|
||||
"description": "LlamaIndex Server",
|
||||
"version": "0.2.1",
|
||||
"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"
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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,4 +1,5 @@
|
||||
export * from "./events";
|
||||
export * from "./prompts";
|
||||
export * from "./server";
|
||||
export * from "./types";
|
||||
export { generateEventComponent } from "./utils/gen-ui";
|
||||
|
||||
@@ -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>
|
||||
\`\`\`
|
||||
`;
|
||||
@@ -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, () => {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
Generated
+629
-116
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
packages:
|
||||
- "packages/*"
|
||||
- "packages/server/examples"
|
||||
- "python/*"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
**/venv
|
||||
**/env
|
||||
**/llama-index-server.egg-info
|
||||
llama_index/server/resources/ui
|
||||
|
||||
# Jupyter files
|
||||
**/*.ipynb
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -78,12 +77,13 @@ The LlamaIndexServer accepts the following configuration parameters:
|
||||
- `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)
|
||||
|
||||
@@ -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.2.1"
|
||||
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
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@create-llama/llama-index-server",
|
||||
"private": true,
|
||||
"version": "0.1.17",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"prebuild": "uv run -- scripts/frontend.py --mode copy",
|
||||
"build": "uv build",
|
||||
"clean": "rm -rf dist build *.egg-info",
|
||||
"new-version": "uv run python scripts/sync_version.py && git add pyproject.toml",
|
||||
"release": "uv publish"
|
||||
},
|
||||
"dependencies": {
|
||||
"@llamaindex/server": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -63,5 +63,6 @@ dev = [
|
||||
"llama-cloud>=0.1.17,<1.0.0",
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
[tool.hatch.build]
|
||||
packages = ["llama_index/"]
|
||||
artifacts = ["llama_index/server/resources"]
|
||||
@@ -0,0 +1,154 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = []
|
||||
# ///
|
||||
# This script is used to build the frontend for the llama-index-server
|
||||
# You need to have pnpm installed to run this script
|
||||
import os
|
||||
import subprocess
|
||||
import argparse
|
||||
import shutil
|
||||
|
||||
|
||||
def _get_pnpm_executable() -> str:
|
||||
"""Determines the correct pnpm executable (pnpm or pnpm.cmd) and returns it.
|
||||
Exits if pnpm is not found."""
|
||||
pnpm_exe = shutil.which("pnpm")
|
||||
if pnpm_exe:
|
||||
return pnpm_exe
|
||||
pnpm_cmd_exe = shutil.which("pnpm.cmd")
|
||||
if pnpm_cmd_exe:
|
||||
return pnpm_cmd_exe
|
||||
print("pnpm not found. Please ensure pnpm is installed and in your PATH.")
|
||||
exit(1)
|
||||
|
||||
|
||||
def check_pnpm_installation() -> None:
|
||||
pnpm_exe = _get_pnpm_executable()
|
||||
try:
|
||||
subprocess.run(
|
||||
[pnpm_exe, "--version"], check=True, capture_output=True
|
||||
) # capture_output to silence stdout on success
|
||||
except subprocess.CalledProcessError:
|
||||
# This case might be redundant if _get_pnpm_executable exits,
|
||||
# but kept for robustness in case _get_pnpm_executable is changed.
|
||||
print(
|
||||
"pnpm is installed, but '--version' command failed. Please check your pnpm installation."
|
||||
)
|
||||
exit(1)
|
||||
|
||||
|
||||
def get_workspace_path() -> str:
|
||||
pnpm_exe = _get_pnpm_executable()
|
||||
# Get the absolute path of the workspace
|
||||
# by running `pnpm root -w`
|
||||
try:
|
||||
output = (
|
||||
subprocess.check_output([pnpm_exe, "root", "-w"]).decode("utf-8").strip()
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Failed to get workspace path using 'pnpm root -w': {e}")
|
||||
print("Ensure you are in a pnpm workspace and pnpm is functioning correctly.")
|
||||
exit(1)
|
||||
# remove 'node_modules' at the end of the path if it exists
|
||||
if output.endswith("node_modules"):
|
||||
return output[:-12]
|
||||
return output
|
||||
|
||||
|
||||
def build_frontend() -> None:
|
||||
pnpm_exe = _get_pnpm_executable()
|
||||
# Build Frontend
|
||||
print("Building Frontend...")
|
||||
# TODO: This probably can be copied from node_modules to save time
|
||||
# but it could be an issue if the user haven't run `pnpm build` for server package
|
||||
try:
|
||||
subprocess.run(
|
||||
[pnpm_exe, "--filter", "@llamaindex/server", "build"], check=True
|
||||
)
|
||||
print("Frontend built successfully.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Frontend build failed: {e}")
|
||||
exit(1)
|
||||
|
||||
|
||||
def get_paths() -> tuple[str, str, str]:
|
||||
workspace_path = get_workspace_path()
|
||||
fe_assets_dir = os.path.join(workspace_path, "packages", "server", "dist", "static")
|
||||
link_path = os.path.join(
|
||||
workspace_path,
|
||||
"python",
|
||||
"llama-index-server",
|
||||
"llama_index",
|
||||
"server",
|
||||
"resources",
|
||||
"ui",
|
||||
)
|
||||
return workspace_path, fe_assets_dir, link_path
|
||||
|
||||
|
||||
def link_static_files() -> None:
|
||||
"""
|
||||
Only works for POSIX systems.
|
||||
Instead of copying the static files, we can link them.
|
||||
This is useful for development purposes.
|
||||
"""
|
||||
# Link the static files to the llama-index-server directory
|
||||
# If user is on Windows, tell them to use WSL
|
||||
if os.name == "nt":
|
||||
print("Windows is not supported. Please use WSL to run this script.")
|
||||
exit(1)
|
||||
print("Linking static files...")
|
||||
# Need to link by absolute path of the server directory
|
||||
workspace_path, fe_assets_dir, link_path = get_paths()
|
||||
# Check
|
||||
if not os.path.exists(fe_assets_dir):
|
||||
print(
|
||||
f"Frontend assets directory {fe_assets_dir} does not exist. Please build the frontend first."
|
||||
)
|
||||
exit(1)
|
||||
if os.path.exists(link_path):
|
||||
if os.path.islink(link_path):
|
||||
os.unlink(link_path)
|
||||
else:
|
||||
shutil.rmtree(link_path)
|
||||
# Link the static files to the server directory
|
||||
subprocess.run(["ln", "-s", fe_assets_dir, link_path], check=True)
|
||||
print("Static files linked successfully.")
|
||||
|
||||
|
||||
def copy_static_files() -> None:
|
||||
# Copy the static files to the output directory
|
||||
workspace_path, fe_assets_dir, link_path = get_paths()
|
||||
# Remove the ui directory if it exists
|
||||
if os.path.exists(link_path):
|
||||
if os.path.islink(link_path):
|
||||
os.unlink(link_path)
|
||||
else:
|
||||
shutil.rmtree(link_path)
|
||||
# Copy the static files to the output directory
|
||||
shutil.copytree(fe_assets_dir, link_path, dirs_exist_ok=True)
|
||||
print("Static files copied successfully.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Prepare the frontend for the llama-index-server"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=["link", "copy"],
|
||||
default="copy",
|
||||
help="Link the static files instead of copying them. Only works for POSIX systems.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-build", action="store_true", help="Skip the build step."
|
||||
)
|
||||
args = parser.parse_args()
|
||||
check_pnpm_installation()
|
||||
if not args.skip_build:
|
||||
build_frontend()
|
||||
if args.mode == "link":
|
||||
link_static_files()
|
||||
else:
|
||||
copy_static_files()
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def sync_versions():
|
||||
# Read package.json
|
||||
with open("package.json", "r") as f:
|
||||
package_data = json.load(f)
|
||||
npm_version = package_data["version"]
|
||||
|
||||
# Read pyproject.toml
|
||||
pyproject_path = Path("pyproject.toml")
|
||||
content = pyproject_path.read_text()
|
||||
|
||||
# Find the project section and update version
|
||||
sections = content.split("\n\n")
|
||||
for i, section in enumerate(sections):
|
||||
if section.startswith("[project]"):
|
||||
lines = section.split("\n")
|
||||
for j, line in enumerate(lines):
|
||||
if line.startswith("version = "):
|
||||
lines[j] = f'version = "{npm_version}"'
|
||||
sections[i] = "\n".join(lines)
|
||||
break
|
||||
|
||||
# Write back to pyproject.toml
|
||||
pyproject_path.write_text("\n\n".join(sections))
|
||||
print(f"Updated pyproject.toml version to {npm_version}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sync_versions()
|
||||
@@ -1,13 +1,18 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from llama_index.core.agent.workflow import AgentWorkflow
|
||||
from llama_index.core.llms import MockLLM
|
||||
from llama_index.server import LlamaIndexServer, UIConfig
|
||||
|
||||
UI_TEST = os.getenv("UI_TEST", "false").lower() == "true"
|
||||
|
||||
|
||||
def fetch_weather(city: str) -> str:
|
||||
"""Fetch the weather for a given city."""
|
||||
@@ -31,8 +36,7 @@ def server() -> LlamaIndexServer:
|
||||
workflow_factory=_agent_workflow,
|
||||
verbose=True,
|
||||
use_default_routers=True,
|
||||
mount_ui=False,
|
||||
env="dev",
|
||||
ui_config=UIConfig(enabled=False),
|
||||
)
|
||||
|
||||
|
||||
@@ -55,241 +59,91 @@ async def test_server_swagger_docs(server: LlamaIndexServer) -> None:
|
||||
assert "Swagger UI" in response.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_ui_is_downloaded(server: LlamaIndexServer) -> None:
|
||||
"""
|
||||
Test if the UI is downloaded and mounted correctly.
|
||||
"""
|
||||
# Clean up any existing static directory first
|
||||
if os.path.exists(".ui"):
|
||||
shutil.rmtree(".ui")
|
||||
# UI Integration Tests
|
||||
# Make sure you run the scripts/build_frontend.py script before running these tests
|
||||
if UI_TEST:
|
||||
|
||||
# Create a new server with UI enabled
|
||||
ui_config = UIConfig(
|
||||
enabled=True,
|
||||
app_title="Test UI",
|
||||
starter_questions=["What's the weather like?"],
|
||||
)
|
||||
ui_server = LlamaIndexServer(
|
||||
workflow_factory=_agent_workflow,
|
||||
verbose=True,
|
||||
use_default_routers=True,
|
||||
env="dev",
|
||||
ui_config=ui_config,
|
||||
)
|
||||
@pytest.mark.asyncio()
|
||||
async def test_ui_is_copied_and_mounted(tmp_path: Path) -> None:
|
||||
"""
|
||||
Test if the UI is copied from bundle and mounted correctly.
|
||||
"""
|
||||
tmp_ui_dir = str(tmp_path / "ui")
|
||||
print(f"tmp_ui_dir: {tmp_ui_dir}")
|
||||
tmp_component_dir = tempfile.mkdtemp()
|
||||
|
||||
# Verify that static directory was created with index.html
|
||||
assert os.path.exists("./.ui"), "Static directory was not created"
|
||||
assert os.path.isdir("./.ui"), "Static path is not a directory"
|
||||
assert os.path.exists("./.ui/index.html"), "index.html was not downloaded"
|
||||
|
||||
# Check if the config.js was created with correct content
|
||||
config_path = os.path.join(".ui", "config.js")
|
||||
assert os.path.exists(config_path), "config.js was not created"
|
||||
|
||||
with open(config_path, "r") as f:
|
||||
config_content = f.read()
|
||||
assert "window.LLAMAINDEX =" in config_content
|
||||
config_json = json.loads(
|
||||
config_content.replace("window.LLAMAINDEX = ", "").rstrip(";")
|
||||
# Create a new server with UI enabled
|
||||
ui_config = UIConfig(
|
||||
enabled=True,
|
||||
starter_questions=["What's the weather like?"],
|
||||
ui_path=tmp_ui_dir,
|
||||
component_dir=tmp_component_dir,
|
||||
)
|
||||
assert config_json["CHAT_API"] == "/api/chat"
|
||||
assert config_json["STARTER_QUESTIONS"] == ["What's the weather like?"]
|
||||
assert config_json["LLAMA_CLOUD_API"] is None
|
||||
assert config_json["APP_TITLE"] == "Test UI"
|
||||
|
||||
# Check if the UI is mounted and accessible
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=ui_server), base_url="http://test"
|
||||
) as ac:
|
||||
response = await ac.get("/")
|
||||
assert response.status_code == 200
|
||||
assert "text/html" in response.headers["content-type"]
|
||||
|
||||
# Clean up after test
|
||||
shutil.rmtree("./.ui")
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_ui_is_accessible(server: LlamaIndexServer) -> None:
|
||||
"""
|
||||
Test if the UI is accessible.
|
||||
"""
|
||||
# Manually trigger UI mounting
|
||||
server.mount_ui()
|
||||
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=server), base_url="http://test"
|
||||
) as ac:
|
||||
response = await ac.get("/")
|
||||
assert response.status_code == 200
|
||||
assert "text/html" in response.headers["content-type"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_ui_config_customization() -> None:
|
||||
"""
|
||||
Test if UI configuration can be customized.
|
||||
"""
|
||||
custom_config = UIConfig(
|
||||
enabled=True,
|
||||
app_title="Custom App",
|
||||
starter_questions=["Question 1", "Question 2"],
|
||||
ui_path=".custom_ui",
|
||||
)
|
||||
|
||||
server = LlamaIndexServer(
|
||||
workflow_factory=_agent_workflow, verbose=True, ui_config=custom_config
|
||||
)
|
||||
|
||||
assert server.ui_config.app_title == "Custom App"
|
||||
assert server.ui_config.starter_questions == ["Question 1", "Question 2"]
|
||||
assert server.ui_config.ui_path == ".custom_ui"
|
||||
|
||||
# Clean up if directory was created
|
||||
if os.path.exists(".custom_ui"):
|
||||
shutil.rmtree(".custom_ui")
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_ui_config_from_dict() -> None:
|
||||
"""
|
||||
Test if UI configuration can be initialized from a dictionary.
|
||||
"""
|
||||
ui_config_dict = {
|
||||
"enabled": True,
|
||||
"app_title": "Dict Config App",
|
||||
"starter_questions": ["Dict Q1", "Dict Q2"],
|
||||
"ui_path": ".dict_ui",
|
||||
}
|
||||
|
||||
server = LlamaIndexServer(
|
||||
workflow_factory=_agent_workflow,
|
||||
verbose=True,
|
||||
ui_config=ui_config_dict,
|
||||
)
|
||||
|
||||
# Verify the config was properly converted to UIConfig object
|
||||
assert isinstance(server.ui_config, UIConfig)
|
||||
assert server.ui_config.app_title == "Dict Config App"
|
||||
assert server.ui_config.starter_questions == ["Dict Q1", "Dict Q2"]
|
||||
assert server.ui_config.ui_path == ".dict_ui"
|
||||
|
||||
# Verify the config.js is created with correct content
|
||||
server.mount_ui()
|
||||
config_path = os.path.join(".dict_ui", "config.js")
|
||||
assert os.path.exists(config_path), "config.js was not created"
|
||||
|
||||
with open(config_path, "r") as f:
|
||||
config_content = f.read()
|
||||
assert "window.LLAMAINDEX =" in config_content
|
||||
config_json = json.loads(
|
||||
config_content.replace("window.LLAMAINDEX = ", "").rstrip(";")
|
||||
ui_server = LlamaIndexServer(
|
||||
workflow_factory=_agent_workflow,
|
||||
verbose=True,
|
||||
use_default_routers=True,
|
||||
env="dev",
|
||||
ui_config=ui_config,
|
||||
)
|
||||
assert config_json["APP_TITLE"] == "Dict Config App"
|
||||
assert config_json["STARTER_QUESTIONS"] == ["Dict Q1", "Dict Q2"]
|
||||
assert config_json["CHAT_API"] == "/api/chat"
|
||||
assert config_json["LLAMA_CLOUD_API"] is None
|
||||
|
||||
# Clean up
|
||||
if os.path.exists(".dict_ui"):
|
||||
shutil.rmtree(".dict_ui")
|
||||
# Verify that static directory was created with index.html
|
||||
# List files in tmp_ui_dir
|
||||
print("Files in tmp_ui_dir: ", os.listdir(tmp_ui_dir))
|
||||
assert os.path.exists(tmp_ui_dir), "Static directory was not created"
|
||||
assert os.path.isdir(tmp_ui_dir), "Static path is not a directory"
|
||||
assert os.path.exists(os.path.join(tmp_ui_dir, "index.html")), (
|
||||
"index.html was not copied from bundle"
|
||||
)
|
||||
|
||||
# Check if the config.js was created with correct content
|
||||
config_path = os.path.join(tmp_ui_dir, "config.js")
|
||||
assert os.path.exists(config_path), "config.js was not created"
|
||||
|
||||
async def test_component_dir_creation(server: LlamaIndexServer) -> None:
|
||||
"""
|
||||
Test if the component directory is created when specified and doesn't exist.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
with open(config_path, "r") as f:
|
||||
config_content = f.read()
|
||||
assert "window.LLAMAINDEX =" in config_content
|
||||
config_json = json.loads(
|
||||
config_content.replace("window.LLAMAINDEX = ", "").rstrip(";")
|
||||
)
|
||||
assert config_json["CHAT_API"] == "/api/chat"
|
||||
assert config_json["STARTER_QUESTIONS"] == ["What's the weather like?"]
|
||||
assert config_json["LLAMA_CLOUD_API"] is None
|
||||
|
||||
test_component_dir = "./test_components"
|
||||
# Verify directory was created
|
||||
assert os.path.exists(tmp_component_dir), "Component directory was not created"
|
||||
assert os.path.isdir(tmp_component_dir), "Component path is not a directory"
|
||||
|
||||
# Clean up any existing directory
|
||||
if os.path.exists(test_component_dir):
|
||||
shutil.rmtree(test_component_dir)
|
||||
# Verify component route exists
|
||||
component_route_exists = any(
|
||||
route.path == "/api/components" # type: ignore
|
||||
for route in ui_server.routes
|
||||
)
|
||||
assert component_route_exists, "Component API route not found in server routes"
|
||||
|
||||
# Create server with component directory
|
||||
_ = LlamaIndexServer(
|
||||
workflow_factory=_agent_workflow,
|
||||
verbose=True,
|
||||
ui_config={
|
||||
"component_dir": test_component_dir,
|
||||
"include_ui": True,
|
||||
},
|
||||
)
|
||||
# Check if the UI is mounted and accessible
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=ui_server), base_url="http://test"
|
||||
) as ac:
|
||||
response = await ac.get("/")
|
||||
assert response.status_code == 200
|
||||
assert "text/html" in response.headers["content-type"]
|
||||
|
||||
# Verify directory was created
|
||||
assert os.path.exists(test_component_dir), "Component directory was not created"
|
||||
assert os.path.isdir(test_component_dir), "Component path is not a directory"
|
||||
|
||||
# Clean up after test
|
||||
shutil.rmtree(test_component_dir)
|
||||
# Clean up after test
|
||||
shutil.rmtree(tmp_ui_dir)
|
||||
shutil.rmtree(tmp_component_dir)
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_component_router_addition(server: LlamaIndexServer, tmp_path) -> None:
|
||||
"""
|
||||
Test if the component router is added when component directory is specified.
|
||||
"""
|
||||
test_component_dir = tmp_path / "test_components"
|
||||
|
||||
# Create server with component directory
|
||||
component_server = LlamaIndexServer(
|
||||
workflow_factory=_agent_workflow,
|
||||
verbose=True,
|
||||
ui_config={
|
||||
"component_dir": str(test_component_dir),
|
||||
"include_ui": True,
|
||||
},
|
||||
)
|
||||
|
||||
# Verify component route exists
|
||||
component_route_exists = any(
|
||||
route.path == "/api/components" for route in component_server.routes
|
||||
)
|
||||
assert component_route_exists, "Component API route not found in server routes"
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_ui_config_includes_components_api(
|
||||
server: LlamaIndexServer, tmp_path
|
||||
) -> None:
|
||||
"""
|
||||
Test if the UI config includes components API when component directory is set.
|
||||
"""
|
||||
test_component_dir = tmp_path / "test_components"
|
||||
|
||||
# Create server with component directory
|
||||
component_server = LlamaIndexServer(
|
||||
workflow_factory=_agent_workflow,
|
||||
verbose=True,
|
||||
ui_config={
|
||||
"component_dir": str(test_component_dir),
|
||||
"include_ui": True,
|
||||
},
|
||||
)
|
||||
|
||||
# Check if components API is in UI config
|
||||
ui_config = component_server.ui_config
|
||||
assert "COMPONENTS_API" in ui_config.get_config_content(), (
|
||||
"Components API not found in UI config"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_component_router_requires_component_dir(
|
||||
server: LlamaIndexServer,
|
||||
) -> None:
|
||||
async def test_component_router_requires_component_dir() -> None:
|
||||
"""
|
||||
Test that adding components router without component_dir raises an error.
|
||||
"""
|
||||
tmp_ui_dir = tempfile.mkdtemp()
|
||||
server_without_component_dir = LlamaIndexServer(
|
||||
workflow_factory=_agent_workflow,
|
||||
verbose=True,
|
||||
ui_config={
|
||||
"include_ui": True,
|
||||
},
|
||||
ui_config=UIConfig(enabled=True, ui_path=tmp_ui_dir),
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
|
||||
Generated
+1
-1
@@ -1897,7 +1897,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "llama-index-server"
|
||||
version = "0.1.15"
|
||||
version = "0.1.16"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "cachetools" },
|
||||
|
||||
Reference in New Issue
Block a user