mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-05 00:46:20 -04:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c5fc33c42 | |||
| 590637e862 | |||
| 38e3b9a88b | |||
| 82ac925224 | |||
| 111a869a99 | |||
| f24ee8e6f9 | |||
| 078b31de1c | |||
| 3acec88fbc | |||
| eee3230e99 | |||
| d8425e5290 | |||
| 0bc5a0d882 | |||
| bbae802bed | |||
| 25fba4381b | |||
| d0618fa2fa | |||
| f3fe3ffc9b | |||
| 6f75d4ab6e | |||
| 3242738fe4 | |||
| 17538eb0dd | |||
| d3772cb4a2 | |||
| 527075c086 | |||
| fb7d4da149 | |||
| 5c35b194bb | |||
| 85e5e7e662 | |||
| 58362542c0 | |||
| 6f44185f68 | |||
| afe9e9fc16 | |||
| 1b5a519f13 | |||
| f072308d03 | |||
| 1df8cfbdc2 | |||
| 24515393a6 | |||
| b3eb0ba7d4 | |||
| 556f33c0ab | |||
| 7a70390b00 | |||
| ad5912b41f | |||
| 76502d28e7 | |||
| f4ca602da5 | |||
| d304554f33 |
@@ -23,6 +23,7 @@ jobs:
|
||||
os: [macos-latest, windows-latest, ubuntu-22.04]
|
||||
frameworks: ["fastapi"]
|
||||
datasources: ["--no-files", "--example-file", "--llamacloud"]
|
||||
template-types: ["streaming", "llamaindexserver"]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -70,6 +71,7 @@ jobs:
|
||||
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
|
||||
FRAMEWORK: ${{ matrix.frameworks }}
|
||||
DATASOURCE: ${{ matrix.datasources }}
|
||||
TEMPLATE_TYPE: ${{ matrix.template-types }}
|
||||
PYTHONIOENCODING: utf-8
|
||||
PYTHONLEGACYWINDOWSSTDIO: utf-8
|
||||
working-directory: packages/create-llama
|
||||
@@ -77,7 +79,7 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-python-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}
|
||||
name: playwright-report-python-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}-${{ matrix.template-types }}
|
||||
path: packages/create-llama/playwright-report/
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
@@ -93,6 +95,7 @@ jobs:
|
||||
os: [macos-latest, windows-latest, ubuntu-22.04]
|
||||
frameworks: ["nextjs"]
|
||||
datasources: ["--no-files", "--example-file", "--llamacloud"]
|
||||
template-types: ["streaming", "llamaindexserver"]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -133,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:
|
||||
@@ -140,12 +158,14 @@ jobs:
|
||||
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
|
||||
FRAMEWORK: ${{ matrix.frameworks }}
|
||||
DATASOURCE: ${{ matrix.datasources }}
|
||||
TEMPLATE_TYPE: ${{ matrix.template-types }}
|
||||
SERVER_PACKAGE_PATH: ${{ runner.temp }}/llamaindex-server.tgz
|
||||
working-directory: packages/create-llama
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-typescript-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}-node${{ matrix.node-version }}
|
||||
name: playwright-report-typescript-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}-node${{ matrix.node-version }}-${{ matrix.template-types }}
|
||||
path: packages/create-llama/playwright-report/
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
@@ -16,6 +16,16 @@ jobs:
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -31,6 +41,13 @@ jobs:
|
||||
- name: Run Prettier
|
||||
run: pnpm run format
|
||||
|
||||
- name: Run build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Run Typecheck for examples
|
||||
run: pnpm run typecheck
|
||||
working-directory: packages/server/examples
|
||||
|
||||
- name: Run Python format check
|
||||
uses: chartboost/ruff-action@v1
|
||||
with:
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -7,6 +7,8 @@ build/
|
||||
.next/
|
||||
out/
|
||||
packages/server/server/
|
||||
**/playwright-report/
|
||||
**/test-results/
|
||||
|
||||
# Python
|
||||
python/
|
||||
|
||||
@@ -106,25 +106,6 @@ Ok to proceed? (y) y
|
||||
You can also pass command line arguments to set up a new project
|
||||
non-interactively. For a list of the latest options, call `create-llama --help`.
|
||||
|
||||
### Running in pro mode
|
||||
|
||||
If you prefer more advanced customization options, you can run `create-llama` in pro mode using the `--pro` flag.
|
||||
|
||||
In pro mode, instead of selecting a predefined use case, you'll be prompted to select each technical component of your project. This allows for greater flexibility in customizing your project, including:
|
||||
|
||||
- **Vector Store**: Choose from a variety of vector stores for keeping your documents, including MongoDB, Pinecone, Weaviate, Qdrant and Chroma.
|
||||
- **Tools**: Choose from a variety of agent tools (functions called by the LLM), such as:
|
||||
- Code Interpreter: Executes Python code in a secure Jupyter notebook environment
|
||||
- Artifact Code Generator: Generates code artifacts that can be run in a sandbox
|
||||
- OpenAPI Action: Facilitates requests to a provided OpenAPI schema
|
||||
- Image Generator: Creates images based on text descriptions
|
||||
- Web Search: Performs web searches to retrieve up-to-date information
|
||||
- **Data Sources**: Integrate various data sources into your chat application, including local files, websites, or database-retrieved data.
|
||||
- **Backend Options**: Besides using Next.js or FastAPI, you can also select to use Express for a more traditional Node.js application.
|
||||
- **Observability**: Choose from a variety of LLM observability tools, including LlamaTrace and Traceloop.
|
||||
|
||||
Pro mode is ideal for developers who want fine-grained control over their project's configuration and are comfortable with more technical setup options.
|
||||
|
||||
## LlamaIndex Documentation
|
||||
|
||||
- [TS/JS docs](https://ts.llamaindex.ai/)
|
||||
|
||||
+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,42 @@
|
||||
# create-llama
|
||||
|
||||
## 0.5.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- eee3230: feat: support custom layout
|
||||
|
||||
## 0.5.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6f75d4a: fix: unsupported language in code gen workflow
|
||||
- d0618fa: Fix LlamaCloud generate script issue
|
||||
|
||||
## 0.5.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 527075c: Enable dev mode that allows updating code directly in the UI
|
||||
|
||||
## 0.5.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1df8cfb: Split artifacts use case to document generator and code generator
|
||||
- 1b5a519: chore: improve dev experience with nodemon
|
||||
- b3eb0ba: Fix typing check issue
|
||||
- 556f33c: fix chromadb dependency issue
|
||||
- 2451539: fix: remove dead generated ai code
|
||||
- 7a70390: Deprecate pro mode
|
||||
|
||||
## 0.5.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f4ca602: Add artifact use case for Typescript template
|
||||
- f4ca602: Update typescript use cases to use the new workflow engine
|
||||
|
||||
## 0.5.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -3,7 +3,7 @@ import { exec } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import util from "util";
|
||||
import { TemplateFramework, TemplateVectorDB } from "../../helpers/types";
|
||||
import { TemplateFramework, TemplateType, TemplateUseCase, TemplateVectorDB } from "../../helpers/types";
|
||||
import { RunCreateLlamaOptions, createTestDir, runCreateLlama } from "../utils";
|
||||
|
||||
const execAsync = util.promisify(exec);
|
||||
@@ -11,123 +11,193 @@ const execAsync = util.promisify(exec);
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
: "fastapi";
|
||||
const templateType: TemplateType = process.env.TEMPLATE_TYPE
|
||||
? (process.env.TEMPLATE_TYPE as TemplateType)
|
||||
: "llamaindexserver";
|
||||
const useCases: TemplateUseCase[] = [
|
||||
"agentic_rag",
|
||||
"deep_research",
|
||||
"financial_report",
|
||||
"code_generator",
|
||||
"document_generator",
|
||||
];
|
||||
const dataSource: string = process.env.DATASOURCE
|
||||
? process.env.DATASOURCE
|
||||
: "--example-file";
|
||||
|
||||
// TODO: add support for other templates
|
||||
test.describe("Mypy check", () => {
|
||||
test.describe.configure({ retries: 0 });
|
||||
|
||||
if (
|
||||
dataSource === "--example-file" // XXX: this test provides its own data source - only trigger it on one data source (usually the CI matrix will trigger multiple data sources)
|
||||
) {
|
||||
// vectorDBs, tools, and data source combinations to test
|
||||
const vectorDbs: TemplateVectorDB[] = [
|
||||
"mongo",
|
||||
"pg",
|
||||
"pinecone",
|
||||
"milvus",
|
||||
"astra",
|
||||
"qdrant",
|
||||
"chroma",
|
||||
"weaviate",
|
||||
];
|
||||
// Test for streaming template
|
||||
test.describe("StreamingTemplate", () => {
|
||||
test.skip(templateType !== "streaming", `skipping streaming test for ${templateType}`);
|
||||
if (
|
||||
dataSource === "--example-file" // XXX: this test provides its own data source - only trigger it on one data source (usually the CI matrix will trigger multiple data sources)
|
||||
) {
|
||||
// vectorDBs, tools, and data source combinations to test
|
||||
const vectorDbs: TemplateVectorDB[] = [
|
||||
"mongo",
|
||||
"pg",
|
||||
"pinecone",
|
||||
"milvus",
|
||||
"astra",
|
||||
"qdrant",
|
||||
"chroma",
|
||||
"weaviate",
|
||||
];
|
||||
const toolOptions = [
|
||||
"wikipedia.WikipediaToolSpec",
|
||||
"google.GoogleSearchToolSpec",
|
||||
"document_generator",
|
||||
"artifact",
|
||||
];
|
||||
|
||||
const toolOptions = [
|
||||
"wikipedia.WikipediaToolSpec",
|
||||
"google.GoogleSearchToolSpec",
|
||||
"document_generator",
|
||||
"artifact",
|
||||
];
|
||||
const dataSources = [
|
||||
"--example-file",
|
||||
"--web-source https://www.example.com",
|
||||
"--db-source mysql+pymysql://user:pass@localhost:3306/mydb",
|
||||
];
|
||||
|
||||
const dataSources = [
|
||||
"--example-file",
|
||||
"--web-source https://www.example.com",
|
||||
"--db-source mysql+pymysql://user:pass@localhost:3306/mydb",
|
||||
];
|
||||
const observabilityOptions = ["llamatrace", "traceloop"];
|
||||
|
||||
const observabilityOptions = ["llamatrace", "traceloop"];
|
||||
// Test vector databases
|
||||
for (const vectorDb of vectorDbs) {
|
||||
test(`vectorDB: ${vectorDb} ${templateType}`, async () => {
|
||||
const cwd = await createTestDir();
|
||||
const { pyprojectPath } = await createAndCheckLlamaProject({
|
||||
options: {
|
||||
cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework,
|
||||
dataSource: "--example-file",
|
||||
vectorDb,
|
||||
tools: "none",
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
observability: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
test.describe("Mypy check", () => {
|
||||
test.describe.configure({ retries: 0 });
|
||||
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
|
||||
if (vectorDb !== "none") {
|
||||
if (vectorDb === "pg") {
|
||||
expect(pyprojectContent).toContain(
|
||||
"llama-index-vector-stores-postgres",
|
||||
);
|
||||
} else {
|
||||
expect(pyprojectContent).toContain(
|
||||
`llama-index-vector-stores-${vectorDb}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Test vector databases
|
||||
for (const vectorDb of vectorDbs) {
|
||||
test(`Mypy check for vectorDB: ${vectorDb}`, async () => {
|
||||
// // Test tools
|
||||
for (const tool of toolOptions) {
|
||||
test(`tool: ${tool} ${templateType}`, async () => {
|
||||
const cwd = await createTestDir();
|
||||
const { pyprojectPath } = await createAndCheckLlamaProject({
|
||||
options: {
|
||||
cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework,
|
||||
dataSource: "--example-file",
|
||||
vectorDb: "none",
|
||||
tools: tool,
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
observability: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
|
||||
if (tool === "wikipedia.WikipediaToolSpec") {
|
||||
expect(pyprojectContent).toContain("wikipedia");
|
||||
}
|
||||
if (tool === "google.GoogleSearchToolSpec") {
|
||||
expect(pyprojectContent).toContain("google");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// // Test data sources
|
||||
for (const dataSource of dataSources) {
|
||||
test(`data source: ${dataSource} ${templateType}`, async () => {
|
||||
const dataSourceType = dataSource.split(" ")[0];
|
||||
const cwd = await createTestDir();
|
||||
const { pyprojectPath } = await createAndCheckLlamaProject({
|
||||
options: {
|
||||
cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework,
|
||||
dataSource,
|
||||
vectorDb: "none",
|
||||
tools: "none",
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
observability: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
|
||||
if (dataSource.includes("--web-source")) {
|
||||
expect(pyprojectContent).toContain("llama-index-readers-web");
|
||||
}
|
||||
if (dataSource.includes("--db-source")) {
|
||||
expect(pyprojectContent).toContain("llama-index-readers-database");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Test observability options
|
||||
for (const observability of observabilityOptions) {
|
||||
test.describe(`observability: ${observability} ${templateType}`, async () => {
|
||||
const cwd = await createTestDir();
|
||||
|
||||
const { pyprojectPath } = await createAndCheckLlamaProject({
|
||||
options: {
|
||||
cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework,
|
||||
dataSource: "--example-file",
|
||||
vectorDb: "none",
|
||||
tools: "none",
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
observability,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test.describe("LlamaIndexServer", async () => {
|
||||
test.skip(templateType !== "llamaindexserver", `skipping llamaindexserver test for ${templateType}`);
|
||||
test.skip(dataSource !== "--example-file", `skipping llamaindexserver test for ${dataSource}`);
|
||||
for (const useCase of useCases) {
|
||||
const cwd = await createTestDir();
|
||||
const { pyprojectPath } = await createAndCheckLlamaProject({
|
||||
await createAndCheckLlamaProject({
|
||||
options: {
|
||||
cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework,
|
||||
dataSource: "--example-file",
|
||||
vectorDb,
|
||||
tools: "none",
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
observability: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
|
||||
if (vectorDb !== "none") {
|
||||
if (vectorDb === "pg") {
|
||||
expect(pyprojectContent).toContain(
|
||||
"llama-index-vector-stores-postgres",
|
||||
);
|
||||
} else {
|
||||
expect(pyprojectContent).toContain(
|
||||
`llama-index-vector-stores-${vectorDb}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Test tools
|
||||
for (const tool of toolOptions) {
|
||||
test(`Mypy check for tool: ${tool}`, async () => {
|
||||
const cwd = await createTestDir();
|
||||
const { pyprojectPath } = await createAndCheckLlamaProject({
|
||||
options: {
|
||||
cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework,
|
||||
dataSource: "--example-file",
|
||||
vectorDb: "none",
|
||||
tools: tool,
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
observability: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
|
||||
if (tool === "wikipedia.WikipediaToolSpec") {
|
||||
expect(pyprojectContent).toContain("wikipedia");
|
||||
}
|
||||
if (tool === "google.GoogleSearchToolSpec") {
|
||||
expect(pyprojectContent).toContain("google");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Test data sources
|
||||
for (const dataSource of dataSources) {
|
||||
const dataSourceType = dataSource.split(" ")[0];
|
||||
test(`Mypy check for data source: ${dataSourceType}`, async () => {
|
||||
const cwd = await createTestDir();
|
||||
const { pyprojectPath } = await createAndCheckLlamaProject({
|
||||
options: {
|
||||
cwd,
|
||||
templateType: "streaming",
|
||||
templateType: "llamaindexserver",
|
||||
templateFramework,
|
||||
dataSource,
|
||||
vectorDb: "none",
|
||||
@@ -139,110 +209,77 @@ if (
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
observability: undefined,
|
||||
useCase,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
|
||||
if (dataSource.includes("--web-source")) {
|
||||
expect(pyprojectContent).toContain("llama-index-readers-web");
|
||||
}
|
||||
if (dataSource.includes("--db-source")) {
|
||||
expect(pyprojectContent).toContain("llama-index-readers-database");
|
||||
}
|
||||
});
|
||||
}
|
||||
async function createAndCheckLlamaProject({
|
||||
options,
|
||||
}: {
|
||||
options: RunCreateLlamaOptions;
|
||||
}): Promise<{ pyprojectPath: string; projectPath: string }> {
|
||||
const result = await runCreateLlama(options);
|
||||
const name = result.projectName;
|
||||
const projectPath = path.join(options.cwd, name);
|
||||
|
||||
// Test observability options
|
||||
for (const observability of observabilityOptions) {
|
||||
test(`Mypy check for observability: ${observability}`, async () => {
|
||||
const cwd = await createTestDir();
|
||||
// Check if the app folder exists
|
||||
expect(fs.existsSync(projectPath)).toBeTruthy();
|
||||
|
||||
const { pyprojectPath } = await createAndCheckLlamaProject({
|
||||
options: {
|
||||
cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework,
|
||||
dataSource: "--example-file",
|
||||
vectorDb: "none",
|
||||
tools: "none",
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
observability,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// Check if pyproject.toml exists
|
||||
const pyprojectPath = path.join(projectPath, "pyproject.toml");
|
||||
expect(fs.existsSync(pyprojectPath)).toBeTruthy();
|
||||
|
||||
async function createAndCheckLlamaProject({
|
||||
options,
|
||||
}: {
|
||||
options: RunCreateLlamaOptions;
|
||||
}): Promise<{ pyprojectPath: string; projectPath: string }> {
|
||||
const result = await runCreateLlama(options);
|
||||
const name = result.projectName;
|
||||
const projectPath = path.join(options.cwd, name);
|
||||
// Modify environment for the command
|
||||
const commandEnv = {
|
||||
...process.env,
|
||||
};
|
||||
|
||||
// Check if the app folder exists
|
||||
expect(fs.existsSync(projectPath)).toBeTruthy();
|
||||
console.log("Running uv venv...");
|
||||
try {
|
||||
const { stdout: venvStdout, stderr: venvStderr } = await execAsync(
|
||||
"uv venv",
|
||||
{ cwd: projectPath, env: commandEnv },
|
||||
);
|
||||
console.log("uv venv stdout:", venvStdout);
|
||||
console.error("uv venv stderr:", venvStderr);
|
||||
} catch (error) {
|
||||
console.error("Error running uv venv:", error);
|
||||
throw error; // Re-throw error to fail the test
|
||||
}
|
||||
|
||||
// Check if pyproject.toml exists
|
||||
const pyprojectPath = path.join(projectPath, "pyproject.toml");
|
||||
expect(fs.existsSync(pyprojectPath)).toBeTruthy();
|
||||
console.log("Running uv sync...");
|
||||
try {
|
||||
const { stdout: syncStdout, stderr: syncStderr } = await execAsync(
|
||||
"uv sync --all-extras",
|
||||
{ cwd: projectPath, env: commandEnv },
|
||||
);
|
||||
console.log("uv sync stdout:", syncStdout);
|
||||
console.error("uv sync stderr:", syncStderr);
|
||||
} catch (error) {
|
||||
console.error("Error running uv sync:", error);
|
||||
throw error; // Re-throw error to fail the test
|
||||
}
|
||||
|
||||
// Modify environment for the command
|
||||
const commandEnv = {
|
||||
...process.env,
|
||||
};
|
||||
console.log("Running uv run mypy ....");
|
||||
try {
|
||||
const { stdout: mypyStdout, stderr: mypyStderr } = await execAsync(
|
||||
"uv run mypy .",
|
||||
{ cwd: projectPath, env: commandEnv },
|
||||
);
|
||||
console.log("uv run mypy stdout:", mypyStdout);
|
||||
console.error("uv run mypy stderr:", mypyStderr);
|
||||
// Assuming mypy success means no output or specific success message
|
||||
// Adjust checks based on actual expected mypy output
|
||||
} catch (error) {
|
||||
console.error("Error running mypy:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log("Running uv venv...");
|
||||
try {
|
||||
const { stdout: venvStdout, stderr: venvStderr } = await execAsync(
|
||||
"uv venv",
|
||||
{ cwd: projectPath, env: commandEnv },
|
||||
);
|
||||
console.log("uv venv stdout:", venvStdout);
|
||||
console.error("uv venv stderr:", venvStderr);
|
||||
} catch (error) {
|
||||
console.error("Error running uv venv:", error);
|
||||
throw error; // Re-throw error to fail the test
|
||||
// If we reach this point without throwing an error, the test passes
|
||||
expect(true).toBeTruthy();
|
||||
|
||||
return { pyprojectPath, projectPath };
|
||||
}
|
||||
|
||||
console.log("Running uv sync...");
|
||||
try {
|
||||
const { stdout: syncStdout, stderr: syncStderr } = await execAsync(
|
||||
"uv sync --all-extras",
|
||||
{ cwd: projectPath, env: commandEnv },
|
||||
);
|
||||
console.log("uv sync stdout:", syncStdout);
|
||||
console.error("uv sync stderr:", syncStderr);
|
||||
} catch (error) {
|
||||
console.error("Error running uv sync:", error);
|
||||
throw error; // Re-throw error to fail the test
|
||||
}
|
||||
|
||||
console.log("Running uv run mypy ....");
|
||||
try {
|
||||
const { stdout: mypyStdout, stderr: mypyStderr } = await execAsync(
|
||||
"uv run mypy .",
|
||||
{ cwd: projectPath, env: commandEnv },
|
||||
);
|
||||
console.log("uv run mypy stdout:", mypyStdout);
|
||||
console.error("uv run mypy stderr:", mypyStderr);
|
||||
// Assuming mypy success means no output or specific success message
|
||||
// Adjust checks based on actual expected mypy output
|
||||
} catch (error) {
|
||||
console.error("Error running mypy:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// If we reach this point without throwing an error, the test passes
|
||||
expect(true).toBeTruthy();
|
||||
|
||||
return { pyprojectPath, projectPath };
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -3,7 +3,12 @@ import { exec } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import util from "util";
|
||||
import { TemplateFramework, TemplateVectorDB } from "../../helpers/types";
|
||||
import {
|
||||
TemplateFramework,
|
||||
TemplateType,
|
||||
TemplateUseCase,
|
||||
TemplateVectorDB,
|
||||
} from "../../helpers/types";
|
||||
import { createTestDir, runCreateLlama } from "../utils";
|
||||
|
||||
const execAsync = util.promisify(exec);
|
||||
@@ -11,6 +16,16 @@ const execAsync = util.promisify(exec);
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
: "nextjs";
|
||||
const templateType: TemplateType = process.env.TEMPLATE_TYPE
|
||||
? (process.env.TEMPLATE_TYPE as TemplateType)
|
||||
: "llamaindexserver";
|
||||
const useCases: TemplateUseCase[] = [
|
||||
"agentic_rag",
|
||||
"deep_research",
|
||||
"financial_report",
|
||||
"code_generator",
|
||||
"document_generator",
|
||||
];
|
||||
const dataSource: string = process.env.DATASOURCE
|
||||
? process.env.DATASOURCE
|
||||
: "--example-file";
|
||||
@@ -29,77 +44,118 @@ const vectorDbs: TemplateVectorDB[] = [
|
||||
];
|
||||
|
||||
test.describe("Test resolve TS dependencies", () => {
|
||||
test.describe.configure({ retries: 0 });
|
||||
|
||||
// Test vector DBs without LlamaParse
|
||||
for (const vectorDb of vectorDbs) {
|
||||
const optionDescription = `vectorDb: ${vectorDb}, dataSource: ${dataSource}`;
|
||||
const optionDescription = `templateType: ${templateType}, vectorDb: ${vectorDb}, dataSource: ${dataSource}`;
|
||||
|
||||
test(`Vector DB test - ${optionDescription}`, async () => {
|
||||
await runTest(vectorDb, false);
|
||||
// skip vectordb test for llamaindexserver
|
||||
test.skip(
|
||||
templateType === "llamaindexserver",
|
||||
"skipping vectorDB test for llamaindexserver",
|
||||
);
|
||||
|
||||
await runTest({
|
||||
templateType: templateType,
|
||||
useLlamaParse: false, // Disable LlamaParse for vectorDB test
|
||||
vectorDb: vectorDb,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Test LlamaParse with vectorDB 'none'
|
||||
test(`LlamaParse test - vectorDb: none, dataSource: ${dataSource}, llamaParse: true`, async () => {
|
||||
await runTest("none", true);
|
||||
});
|
||||
|
||||
async function runTest(
|
||||
vectorDb: TemplateVectorDB | "none",
|
||||
useLlamaParse: boolean,
|
||||
) {
|
||||
const cwd = await createTestDir();
|
||||
|
||||
const result = await runCreateLlama({
|
||||
cwd: cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework: templateFramework,
|
||||
dataSource: dataSource,
|
||||
vectorDb: vectorDb,
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: templateFramework === "nextjs" ? "" : "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
tools: undefined,
|
||||
useLlamaParse: useLlamaParse,
|
||||
});
|
||||
const name = result.projectName;
|
||||
|
||||
// Check if the app folder exists
|
||||
const appDir = path.join(cwd, name);
|
||||
const dirExists = fs.existsSync(appDir);
|
||||
expect(dirExists).toBeTruthy();
|
||||
|
||||
// Install dependencies using pnpm
|
||||
try {
|
||||
const { stderr: installStderr } = await execAsync(
|
||||
"pnpm install --prefer-offline --ignore-workspace",
|
||||
{
|
||||
cwd: appDir,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error installing dependencies:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Run tsc type check and capture the output
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(
|
||||
"pnpm exec tsc -b --diagnostics",
|
||||
{
|
||||
cwd: appDir,
|
||||
},
|
||||
);
|
||||
// Check if there's any error output
|
||||
expect(stderr).toBeFalsy();
|
||||
|
||||
// Log the stdout for debugging purposes
|
||||
console.log("TypeScript type-check output:", stdout);
|
||||
} catch (error) {
|
||||
console.error("Error running tsc:", error);
|
||||
throw error;
|
||||
// No vectorDB, with LlamaParse and useCase
|
||||
// Only need to test use case with example data source
|
||||
if (dataSource === "--example-file") {
|
||||
for (const useCase of useCases) {
|
||||
const optionDescription = `templateType: ${templateType}, useCase: ${useCase}`;
|
||||
test.describe(`useCase test - ${optionDescription}`, () => {
|
||||
test.skip(
|
||||
templateType === "streaming",
|
||||
"Skipping use case test for streaming template.",
|
||||
);
|
||||
test(`no llamaParse - ${optionDescription}`, async () => {
|
||||
await runTest({
|
||||
templateType: templateType,
|
||||
useLlamaParse: false,
|
||||
useCase: useCase,
|
||||
});
|
||||
});
|
||||
// Skipping llamacloud for the use case doesn't use index.
|
||||
if (useCase !== "code_generator" && useCase !== "document_generator") {
|
||||
test(`llamaParse - ${optionDescription}`, async () => {
|
||||
await runTest({
|
||||
templateType: templateType,
|
||||
useLlamaParse: true,
|
||||
useCase: useCase,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function runTest(options: {
|
||||
templateType: TemplateType;
|
||||
useLlamaParse: boolean;
|
||||
useCase?: TemplateUseCase;
|
||||
vectorDb?: TemplateVectorDB;
|
||||
}) {
|
||||
const cwd = await createTestDir();
|
||||
|
||||
const result = await runCreateLlama({
|
||||
cwd: cwd,
|
||||
templateType: options.templateType,
|
||||
templateFramework: templateFramework,
|
||||
dataSource: dataSource,
|
||||
vectorDb: options.vectorDb ?? "none",
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: templateFramework === "nextjs" ? "" : "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
tools: undefined,
|
||||
useLlamaParse: options.useLlamaParse,
|
||||
useCase: options.useCase,
|
||||
});
|
||||
const name = result.projectName;
|
||||
|
||||
// Check if the app folder exists
|
||||
const appDir = path.join(cwd, name);
|
||||
const dirExists = fs.existsSync(appDir);
|
||||
expect(dirExists).toBeTruthy();
|
||||
|
||||
// Install dependencies using pnpm
|
||||
try {
|
||||
const { stderr: installStderr } = await execAsync(
|
||||
"pnpm install --prefer-offline --ignore-workspace",
|
||||
{
|
||||
cwd: appDir,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error installing dependencies:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Run tsc type check and capture the output
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(
|
||||
"pnpm exec tsc -b --diagnostics",
|
||||
{
|
||||
cwd: appDir,
|
||||
},
|
||||
);
|
||||
// Check if there's any error output
|
||||
expect(stderr).toBeFalsy();
|
||||
|
||||
// Log the stdout for debugging purposes
|
||||
console.log("TypeScript type-check output:", stdout);
|
||||
} catch (error) {
|
||||
console.error("Error running tsc:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,10 @@ const getAdditionalDependencies = (
|
||||
name: "llama-index-vector-stores-chroma",
|
||||
version: ">=0.4.0,<0.5.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "onnxruntime",
|
||||
version: "<1.22.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "weaviate": {
|
||||
@@ -263,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") {
|
||||
@@ -565,13 +569,19 @@ const installLlamaIndexServerTemplate = async ({
|
||||
|
||||
await copy("*.py", path.join(root, "app"), {
|
||||
parents: true,
|
||||
cwd: path.join(templatesDir, "components", "workflows", "python", useCase),
|
||||
cwd: path.join(templatesDir, "components", "use-cases", "python", useCase),
|
||||
});
|
||||
|
||||
// Copy custom UI component code
|
||||
await copy(`*`, path.join(root, "components"), {
|
||||
parents: true,
|
||||
cwd: path.join(templatesDir, "components", "ui", "workflows", useCase),
|
||||
cwd: path.join(templatesDir, "components", "ui", "use-cases", useCase),
|
||||
});
|
||||
|
||||
// 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) {
|
||||
@@ -602,7 +612,7 @@ const installLlamaIndexServerTemplate = async ({
|
||||
// Copy README.md
|
||||
await copy("README-template.md", path.join(root), {
|
||||
parents: true,
|
||||
cwd: path.join(templatesDir, "components", "workflows", "python", useCase),
|
||||
cwd: path.join(templatesDir, "components", "use-cases", "python", useCase),
|
||||
rename: assetRelocator,
|
||||
});
|
||||
};
|
||||
@@ -673,6 +683,7 @@ export const installPythonTemplate = async ({
|
||||
dataSources,
|
||||
tools,
|
||||
template,
|
||||
observability,
|
||||
);
|
||||
|
||||
await addDependencies(root, addOnDependencies);
|
||||
|
||||
@@ -58,7 +58,8 @@ export type TemplateUseCase =
|
||||
| "extractor"
|
||||
| "contract_review"
|
||||
| "agentic_rag"
|
||||
| "artifacts";
|
||||
| "code_generator"
|
||||
| "document_generator";
|
||||
// Config for both file and folder
|
||||
export type FileSourceConfig =
|
||||
| {
|
||||
|
||||
@@ -31,23 +31,30 @@ const installLlamaIndexServerTemplate = async ({
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await copy("workflow.ts", path.join(root, "src", "app"), {
|
||||
parents: true,
|
||||
await copy("**", path.join(root), {
|
||||
cwd: path.join(
|
||||
templatesDir,
|
||||
"components",
|
||||
"workflows",
|
||||
"use-cases",
|
||||
"typescript",
|
||||
useCase,
|
||||
),
|
||||
rename: assetRelocator,
|
||||
});
|
||||
|
||||
// copy workflow UI components to output/components folder
|
||||
// copy workflow UI components to components folder in root
|
||||
await copy("*", path.join(root, "components"), {
|
||||
parents: true,
|
||||
cwd: path.join(templatesDir, "components", "ui", "workflows", useCase),
|
||||
cwd: path.join(templatesDir, "components", "ui", "use-cases", useCase),
|
||||
});
|
||||
|
||||
// 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"), {
|
||||
parents: true,
|
||||
@@ -74,18 +81,14 @@ const installLlamaIndexServerTemplate = async ({
|
||||
rename: () => "data.ts",
|
||||
});
|
||||
}
|
||||
// Copy README.md
|
||||
await copy("README-template.md", path.join(root), {
|
||||
parents: true,
|
||||
cwd: path.join(
|
||||
templatesDir,
|
||||
"components",
|
||||
"workflows",
|
||||
"typescript",
|
||||
useCase,
|
||||
),
|
||||
rename: assetRelocator,
|
||||
});
|
||||
|
||||
// Simplify use case code
|
||||
if (useCase === "code_generator" || useCase === "document_generator") {
|
||||
// Artifact use case doesn't use index.
|
||||
// We don't need data.ts, generate.ts
|
||||
await fs.rm(path.join(root, "src", "app", "data.ts"));
|
||||
// TODO: Remove generate index in generate.ts and package.json if possible
|
||||
}
|
||||
};
|
||||
|
||||
const installLegacyTSTemplate = async ({
|
||||
@@ -390,7 +393,7 @@ const providerDependencies: {
|
||||
[key in ModelProvider]?: Record<string, string>;
|
||||
} = {
|
||||
openai: {
|
||||
"@llamaindex/openai": "^0.2.0",
|
||||
"@llamaindex/openai": "~0.4.0",
|
||||
},
|
||||
gemini: {
|
||||
"@llamaindex/google": "^0.2.0",
|
||||
@@ -516,7 +519,7 @@ async function updatePackageJson({
|
||||
if (backend) {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"@llamaindex/readers": "^2.0.0",
|
||||
"@llamaindex/readers": "~3.1.4",
|
||||
};
|
||||
|
||||
if (vectorDb && vectorDb in vectorDbDependencies) {
|
||||
@@ -546,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,
|
||||
|
||||
@@ -196,7 +196,7 @@ const program = new Command(packageJson.name)
|
||||
"--pro",
|
||||
`
|
||||
|
||||
Allow interactive selection of all features.
|
||||
Deprecated: Allow interactive selection of all features.
|
||||
`,
|
||||
false,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.5.12",
|
||||
"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: "",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ciInfo from "ci-info";
|
||||
import { bold, yellow } from "picocolors";
|
||||
import { getCIQuestionResults } from "./ci";
|
||||
import { askProQuestions } from "./questions";
|
||||
import { askSimpleQuestions } from "./simple";
|
||||
@@ -13,6 +14,12 @@ export const askQuestions = async (
|
||||
return await getCIQuestionResults(args);
|
||||
} else if (args.pro) {
|
||||
// TODO: refactor pro questions to return a result object
|
||||
console.log(
|
||||
yellow(
|
||||
`Pro mode is deprecated. Please use the new templates using the ${bold("LlamaIndexServer")} by not specifying pro mode.`,
|
||||
),
|
||||
);
|
||||
|
||||
await askProQuestions(args);
|
||||
return args as unknown as QuestionResults;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ type AppType =
|
||||
| "agentic_rag"
|
||||
| "financial_report"
|
||||
| "deep_research"
|
||||
| "artifacts";
|
||||
| "code_generator"
|
||||
| "document_generator";
|
||||
|
||||
type SimpleAnswers = {
|
||||
appType: AppType;
|
||||
@@ -47,10 +48,14 @@ export const askSimpleQuestions = async (
|
||||
"Researches and analyzes provided documents from multiple perspectives, generating a comprehensive report with citations to support key findings and insights.",
|
||||
},
|
||||
{
|
||||
title: "Artifacts",
|
||||
value: "artifacts",
|
||||
description:
|
||||
"Build your own Vercel's v0 or OpenAI's canvas-styled UI.",
|
||||
title: "Code Generator",
|
||||
value: "code_generator",
|
||||
description: "Build a Vercel v0 styled code generator.",
|
||||
},
|
||||
{
|
||||
title: "Document Generator",
|
||||
value: "document_generator",
|
||||
description: "Build a OpenAI canvas-styled document generator.",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -62,35 +67,35 @@ export const askSimpleQuestions = async (
|
||||
|
||||
let useLlamaCloud = false;
|
||||
|
||||
if (appType !== "artifacts") {
|
||||
const { language: newLanguage } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "language",
|
||||
message: "What language do you want to use?",
|
||||
choices: [
|
||||
{ title: "Python (FastAPI)", value: "fastapi" },
|
||||
{ title: "Typescript (NextJS)", value: "nextjs" },
|
||||
],
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
language = newLanguage;
|
||||
}
|
||||
|
||||
const { useLlamaCloud: newUseLlamaCloud } = await prompts(
|
||||
const { language: newLanguage } = await prompts(
|
||||
{
|
||||
type: "toggle",
|
||||
name: "useLlamaCloud",
|
||||
message: "Do you want to use LlamaCloud services?",
|
||||
initial: false,
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
hint: "see https://www.llamaindex.ai/enterprise for more info",
|
||||
type: "select",
|
||||
name: "language",
|
||||
message: "What language do you want to use?",
|
||||
choices: [
|
||||
{ title: "Python (FastAPI)", value: "fastapi" },
|
||||
{ title: "Typescript (NextJS)", value: "nextjs" },
|
||||
],
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
useLlamaCloud = newUseLlamaCloud;
|
||||
language = newLanguage;
|
||||
|
||||
if (appType !== "code_generator" && appType !== "document_generator") {
|
||||
const { useLlamaCloud: newUseLlamaCloud } = await prompts(
|
||||
{
|
||||
type: "toggle",
|
||||
name: "useLlamaCloud",
|
||||
message: "Do you want to use LlamaCloud services?",
|
||||
initial: false,
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
hint: "see https://www.llamaindex.ai/enterprise for more info",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
useLlamaCloud = newUseLlamaCloud;
|
||||
}
|
||||
|
||||
if (useLlamaCloud && !llamaCloudKey) {
|
||||
// Ask for LlamaCloud API key, if not set
|
||||
@@ -153,7 +158,13 @@ const convertAnswers = async (
|
||||
tools: [],
|
||||
modelConfig: MODEL_GPT41,
|
||||
},
|
||||
artifacts: {
|
||||
code_generator: {
|
||||
template: "llamaindexserver",
|
||||
dataSources: [],
|
||||
tools: [],
|
||||
modelConfig: MODEL_GPT41,
|
||||
},
|
||||
document_generator: {
|
||||
template: "llamaindexserver",
|
||||
dataSources: [],
|
||||
tools: [],
|
||||
|
||||
+6
-2
@@ -1,5 +1,9 @@
|
||||
import { Document, LLamaCloudFileService, VectorStoreIndex } from "llamaindex";
|
||||
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
|
||||
import {
|
||||
Document,
|
||||
LLamaCloudFileService,
|
||||
LlamaCloudIndex,
|
||||
VectorStoreIndex,
|
||||
} from "llamaindex";
|
||||
import { DocumentFile } from "../streaming/annotations";
|
||||
import { parseFile, storeFile } from "./helper";
|
||||
import { runPipeline } from "./pipeline";
|
||||
|
||||
@@ -10,8 +10,9 @@ dependencies = [
|
||||
"python-dotenv>=1.0.0",
|
||||
"pydantic<2.10",
|
||||
"llama-index>=0.12.1",
|
||||
"llama-parse>=0.6.21,<0.7.0",
|
||||
"cachetools>=5.3.3",
|
||||
"reflex>=0.6.2.post1",
|
||||
"reflex==0.7.10",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -11,8 +11,9 @@ dependencies = [
|
||||
"python-dotenv>=1.0.0",
|
||||
"pydantic<2.10",
|
||||
"llama-index>=0.12.1",
|
||||
"llama-parse>=0.6.21,<0.7.0",
|
||||
"cachetools>=5.3.3",
|
||||
"reflex>=0.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>
|
||||
);
|
||||
}
|
||||
-5
@@ -113,11 +113,6 @@ function ArtifactWorkflowCard({ event }) {
|
||||
state === "plan" && "bg-blue-200",
|
||||
state === "generate" && "bg-violet-200",
|
||||
)}
|
||||
indicatorClassName={cn(
|
||||
"transition-all duration-500",
|
||||
state === "plan" && "bg-blue-500",
|
||||
state === "generate" && "bg-violet-500",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Markdown } from "@llamaindex/chat-ui/widgets";
|
||||
import { ListChecks, Loader2, Wand2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const STAGE_META = {
|
||||
plan: {
|
||||
icon: ListChecks,
|
||||
badgeText: "Step 1/2: Planning",
|
||||
gradient: "from-blue-100 via-blue-50 to-white",
|
||||
progress: 33,
|
||||
iconBg: "bg-blue-100 text-blue-600",
|
||||
badge: "bg-blue-100 text-blue-700",
|
||||
},
|
||||
generate: {
|
||||
icon: Wand2,
|
||||
badgeText: "Step 2/2: Generating",
|
||||
gradient: "from-violet-100 via-violet-50 to-white",
|
||||
progress: 66,
|
||||
iconBg: "bg-violet-100 text-violet-600",
|
||||
badge: "bg-violet-100 text-violet-700",
|
||||
},
|
||||
};
|
||||
|
||||
function ArtifactWorkflowCard({ event }) {
|
||||
const [visible, setVisible] = useState(event?.state !== "completed");
|
||||
const [fade, setFade] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (event?.state === "completed") {
|
||||
setVisible(false);
|
||||
} else {
|
||||
setVisible(true);
|
||||
setFade(false);
|
||||
}
|
||||
}, [event?.state]);
|
||||
|
||||
if (!event || !visible) return null;
|
||||
|
||||
const { state, requirement } = event;
|
||||
const meta = STAGE_META[state];
|
||||
|
||||
if (!meta) return null;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[180px] w-full items-center justify-center py-2">
|
||||
<Card
|
||||
className={cn(
|
||||
"w-full rounded-xl shadow-md transition-all duration-500",
|
||||
"border-0",
|
||||
fade && "pointer-events-none opacity-0",
|
||||
`bg-gradient-to-br ${meta.gradient}`,
|
||||
)}
|
||||
style={{
|
||||
boxShadow:
|
||||
"0 2px 12px 0 rgba(80, 80, 120, 0.08), 0 1px 3px 0 rgba(80, 80, 120, 0.04)",
|
||||
}}
|
||||
>
|
||||
<CardHeader className="flex flex-row items-center gap-2 px-3 pb-1 pt-2">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-full p-1",
|
||||
meta.iconBg,
|
||||
)}
|
||||
>
|
||||
<meta.icon className="h-5 w-5" />
|
||||
</div>
|
||||
<CardTitle className="flex items-center gap-2 text-base font-semibold">
|
||||
<Badge className={cn("ml-1", meta.badge, "px-2 py-0.5 text-xs")}>
|
||||
{meta.badgeText}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-3 py-1">
|
||||
{state === "plan" && (
|
||||
<div className="flex flex-col items-center gap-2 py-2">
|
||||
<Loader2 className="mb-1 h-6 w-6 animate-spin text-blue-400" />
|
||||
<div className="text-center text-sm font-medium text-blue-900">
|
||||
Analyzing your request...
|
||||
</div>
|
||||
<Skeleton className="mt-1 h-3 w-1/2 rounded-full" />
|
||||
</div>
|
||||
)}
|
||||
{state === "generate" && (
|
||||
<div className="flex flex-col gap-2 py-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<Loader2 className="h-4 w-4 animate-spin text-violet-400" />
|
||||
<span className="text-sm font-medium text-violet-900">
|
||||
Working on the requirement:
|
||||
</span>
|
||||
</div>
|
||||
<div className="max-h-24 overflow-auto rounded-lg border border-violet-200 bg-violet-50 px-2 py-1 text-xs">
|
||||
{requirement ? (
|
||||
<Markdown content={requirement} />
|
||||
) : (
|
||||
<span className="italic text-violet-400">
|
||||
No requirements available yet.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<div className="px-3 pb-2 pt-1">
|
||||
<Progress
|
||||
value={meta.progress}
|
||||
className={cn(
|
||||
"h-1 rounded-full bg-gray-200",
|
||||
state === "plan" && "bg-blue-200",
|
||||
state === "generate" && "bg-violet-200",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Component({ events }) {
|
||||
const aggregateEvents = () => {
|
||||
if (!events || events.length === 0) return null;
|
||||
return events[events.length - 1];
|
||||
};
|
||||
|
||||
const event = aggregateEvents();
|
||||
|
||||
return <ArtifactWorkflowCard event={event} />;
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Workflows](https://docs.llamaindex.ai/en/stable/understanding/workflows/).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, setup the environment with uv:
|
||||
|
||||
> **_Note:_** This step is not needed if you are using the dev-container.
|
||||
|
||||
```shell
|
||||
uv sync
|
||||
```
|
||||
|
||||
Then check the parameters that have been pre-configured in the `.env` file in this directory.
|
||||
Make sure you have set the `OPENAI_API_KEY` for the LLM.
|
||||
|
||||
Then, run the development server:
|
||||
|
||||
```shell
|
||||
uv run fastapi dev
|
||||
```
|
||||
|
||||
Then open [http://localhost:8000](http://localhost:8000) with your browser to start the chat UI.
|
||||
|
||||
To start the app optimized for **production**, run:
|
||||
|
||||
```
|
||||
uv run fastapi run
|
||||
```
|
||||
|
||||
## Configure LLM and Embedding Model
|
||||
|
||||
You can configure [LLM model](https://docs.llamaindex.ai/en/stable/module_guides/models/llms) and [embedding model](https://docs.llamaindex.ai/en/stable/module_guides/models/embeddings) in [settings.py](app/settings.py).
|
||||
|
||||
## Use Case
|
||||
AI-powered code generator that can help you generate app with a chat interface, code editor and app preview.
|
||||
|
||||
To update the workflow, you can modify the code in [`workflow.py`](app/workflow.py).
|
||||
|
||||
You can start by sending an request on the [chat UI](http://localhost:8000) or you can test the `/api/chat` endpoint with the following curl request:
|
||||
|
||||
```
|
||||
curl --location 'localhost:8000/api/chat' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{ "messages": [{ "role": "user", "content": "Create a report comparing the finances of Apple and Tesla" }] }'
|
||||
```
|
||||
|
||||
## Customize the UI
|
||||
|
||||
To customize the UI, you can start by modifying the [./components/ui_event.jsx](./components/ui_event.jsx) file.
|
||||
|
||||
You can also generate a new code for the workflow using LLM by running the following command:
|
||||
|
||||
```
|
||||
uv run generate_ui
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
|
||||
- [Workflows Introduction](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - learn about LlamaIndex workflows.
|
||||
- [LlamaIndex Server](https://pypi.org/project/llama-index-server/)
|
||||
|
||||
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
|
||||
+10
@@ -6,6 +6,7 @@ from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.llms import LLM
|
||||
from llama_index.core.memory import ChatMemoryBuffer
|
||||
from llama_index.core.prompts import PromptTemplate
|
||||
from llama_index.llms.openai import OpenAI
|
||||
from llama_index.core.workflow import (
|
||||
Context,
|
||||
Event,
|
||||
@@ -26,6 +27,15 @@ from llama_index.server.api.utils import get_last_artifact
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
def create_workflow(chat_request: ChatRequest) -> Workflow:
|
||||
workflow = CodeArtifactWorkflow(
|
||||
llm=OpenAI(model="gpt-4.1"),
|
||||
chat_request=chat_request,
|
||||
timeout=120.0,
|
||||
)
|
||||
return workflow
|
||||
|
||||
|
||||
class Requirement(BaseModel):
|
||||
next_step: Literal["answering", "coding"]
|
||||
language: Optional[str] = None
|
||||
+2
-5
@@ -33,12 +33,9 @@ You can configure [LLM model](https://docs.llamaindex.ai/en/stable/module_guides
|
||||
|
||||
## Use Case
|
||||
|
||||
We have prepared two artifact workflows:
|
||||
AI-powered document generator that can help you generate documents with a chat interface and simple markdown editor.
|
||||
|
||||
- [Code Workflow](app/code_workflow.py): To generate code and display it in the UI like Vercel's v0.
|
||||
- [Document Workflow](app/document_workflow.py): Generate and update a document like OpenAI's canvas.
|
||||
|
||||
Modify the factory method in [`workflow.py`](app/workflow.py) to decide which artifact workflow to use. Without any changes the Code Workflow is used.
|
||||
To update the workflow, you can modify the code in [`workflow.py`](app/workflow.py).
|
||||
|
||||
You can start by sending an request on the [chat UI](http://localhost:8000) or you can test the `/api/chat` endpoint with the following curl request:
|
||||
|
||||
+10
@@ -4,6 +4,7 @@ from typing import Any, Literal, Optional
|
||||
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.llms import LLM
|
||||
from llama_index.llms.openai import OpenAI
|
||||
from llama_index.core.memory import ChatMemoryBuffer
|
||||
from llama_index.core.prompts import PromptTemplate
|
||||
from llama_index.core.workflow import (
|
||||
@@ -26,6 +27,15 @@ from llama_index.server.api.utils import get_last_artifact
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
def create_workflow(chat_request: ChatRequest) -> Workflow:
|
||||
workflow = DocumentArtifactWorkflow(
|
||||
llm=OpenAI(model="gpt-4.1"),
|
||||
chat_request=chat_request,
|
||||
timeout=120.0,
|
||||
)
|
||||
return workflow
|
||||
|
||||
|
||||
class DocumentRequirement(BaseModel):
|
||||
type: Literal["markdown", "html"]
|
||||
title: str
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { agent } from "llamaindex";
|
||||
import { agent } from "@llamaindex/workflow";
|
||||
import { getIndex } from "./data";
|
||||
|
||||
export const workflowFactory = async (reqBody: any) => {
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { SimpleDirectoryReader } from "@llamaindex/readers/directory";
|
||||
import "dotenv/config";
|
||||
import { storageContextFromDefaults, VectorStoreIndex } from "llamaindex";
|
||||
import { initSettings } from "./app/settings";
|
||||
|
||||
async function generateDatasource() {
|
||||
console.log(`Generating storage context...`);
|
||||
// Split documents, create embeddings and store them in the storage context
|
||||
const storageContext = await storageContextFromDefaults({
|
||||
persistDir: "storage",
|
||||
});
|
||||
// load documents from current directory into an index
|
||||
const reader = new SimpleDirectoryReader();
|
||||
const documents = await reader.loadData("data");
|
||||
|
||||
await VectorStoreIndex.fromDocuments(documents, {
|
||||
storageContext,
|
||||
});
|
||||
console.log("Storage context successfully generated.");
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
|
||||
initSettings();
|
||||
|
||||
if (command === "ui") {
|
||||
console.error("This project doesn't use any custom UI.");
|
||||
return;
|
||||
} else {
|
||||
if (command !== "datasource") {
|
||||
console.error(
|
||||
`Unrecognized command: ${command}. Generating datasource by default.`,
|
||||
);
|
||||
}
|
||||
await generateDatasource();
|
||||
}
|
||||
})();
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) project bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, install the dependencies:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Second, run the development server:
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the chat UI.
|
||||
|
||||
## Configure LLM and Embedding Model
|
||||
|
||||
You can configure [LLM model](https://ts.llamaindex.ai/docs/llamaindex/modules/llms) in the [settings file](src/app/settings.ts).
|
||||
|
||||
## Custom UI Components
|
||||
|
||||
We have a custom component located in `components/ui_event.jsx`. This is used to display the state of artifact workflows in UI. You can regenerate a new UI component from the workflow event schema by running the following command:
|
||||
|
||||
```
|
||||
npm run generate:ui
|
||||
```
|
||||
|
||||
## Use Case
|
||||
|
||||
AI-powered code generator that can help you generate app with a chat interface, code editor and app preview.
|
||||
|
||||
To update the workflow, you can modify the code in [`workflow.ts`](app/workflow.ts).
|
||||
|
||||
You can start by sending 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' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{ "messages": [{ "role": "user", "content": "Compare the financial performance of Apple and Tesla" }] }'
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
|
||||
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai/docs/llamaindex) - learn about LlamaIndex (Typescript features).
|
||||
- [Workflows Introduction](https://ts.llamaindex.ai/docs/llamaindex/modules/workflows) - learn about LlamaIndexTS workflows.
|
||||
|
||||
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
|
||||
+350
@@ -0,0 +1,350 @@
|
||||
import { extractLastArtifact } from "@llamaindex/server";
|
||||
import { ChatMemoryBuffer, MessageContent, Settings } from "llamaindex";
|
||||
|
||||
import {
|
||||
agentStreamEvent,
|
||||
createStatefulMiddleware,
|
||||
createWorkflow,
|
||||
startAgentEvent,
|
||||
stopAgentEvent,
|
||||
workflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
export const RequirementSchema = z.object({
|
||||
next_step: z.enum(["answering", "coding"]),
|
||||
language: z.string().nullable().optional(),
|
||||
file_name: z.string().nullable().optional(),
|
||||
requirement: z.string(),
|
||||
});
|
||||
|
||||
export type Requirement = z.infer<typeof RequirementSchema>;
|
||||
|
||||
export const UIEventSchema = z.object({
|
||||
type: z.literal("ui_event"),
|
||||
data: z.object({
|
||||
state: z
|
||||
.enum(["plan", "generate", "completed"])
|
||||
.describe(
|
||||
"The current state of the workflow: 'plan', 'generate', or 'completed'.",
|
||||
),
|
||||
requirement: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"An optional requirement creating or updating a code, if applicable.",
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
export type UIEvent = z.infer<typeof UIEventSchema>;
|
||||
const planEvent = workflowEvent<{
|
||||
userInput: MessageContent;
|
||||
context?: string | undefined;
|
||||
}>();
|
||||
|
||||
const generateArtifactEvent = workflowEvent<{
|
||||
requirement: Requirement;
|
||||
}>();
|
||||
|
||||
const synthesizeAnswerEvent = workflowEvent<object>();
|
||||
|
||||
const uiEvent = workflowEvent<UIEvent>();
|
||||
|
||||
const artifactEvent = workflowEvent<{
|
||||
type: "artifact";
|
||||
data: {
|
||||
type: "code";
|
||||
created_at: number;
|
||||
data: {
|
||||
language: string;
|
||||
file_name: string;
|
||||
code: string;
|
||||
};
|
||||
};
|
||||
}>();
|
||||
|
||||
export function workflowFactory(reqBody: any) {
|
||||
const llm = Settings.llm;
|
||||
|
||||
const { withState, getContext } = createStatefulMiddleware(() => {
|
||||
return {
|
||||
memory: new ChatMemoryBuffer({ llm }),
|
||||
lastArtifact: extractLastArtifact(reqBody),
|
||||
};
|
||||
});
|
||||
const workflow = withState(createWorkflow());
|
||||
|
||||
workflow.handle([startAgentEvent], async ({ data }) => {
|
||||
const { userInput, chatHistory = [] } = data;
|
||||
// Prepare chat history
|
||||
const { state } = getContext();
|
||||
// Put user input to the memory
|
||||
if (!userInput) {
|
||||
throw new Error("Missing user input to start the workflow");
|
||||
}
|
||||
state.memory.set(chatHistory);
|
||||
state.memory.put({ role: "user", content: userInput });
|
||||
|
||||
return planEvent.with({
|
||||
userInput: userInput,
|
||||
context: state.lastArtifact
|
||||
? JSON.stringify(state.lastArtifact)
|
||||
: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
workflow.handle([planEvent], async ({ data: planData }) => {
|
||||
const { sendEvent } = getContext();
|
||||
const { state } = getContext();
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
state: "plan",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const user_msg = planData.userInput;
|
||||
const context = planData.context
|
||||
? `## The context is: \n${planData.context}\n`
|
||||
: "";
|
||||
const prompt = `
|
||||
You are a product analyst responsible for analyzing the user's request and providing the next step for code or document generation.
|
||||
You are helping user with their code artifact. To update the code, you need to plan a coding step.
|
||||
|
||||
Follow these instructions:
|
||||
1. Carefully analyze the conversation history and the user's request to determine what has been done and what the next step should be.
|
||||
2. The next step must be one of the following two options:
|
||||
- "coding": To make the changes to the current code.
|
||||
- "answering": If you don't need to update the current code or need clarification from the user.
|
||||
Important: Avoid telling the user to update the code themselves, you are the one who will update the code (by planning a coding step).
|
||||
3. If the next step is "coding", you may specify the language ("typescript" or "python") and file_name if known, otherwise set them to null.
|
||||
4. The requirement must be provided clearly what is the user request and what need to be done for the next step in details
|
||||
as precise and specific as possible, don't be stingy with in the requirement.
|
||||
5. If the next step is "answering", set language and file_name to null, and the requirement should describe what to answer or explain to the user.
|
||||
6. Be concise; only return the requirements for the next step.
|
||||
7. The requirements must be in the following format:
|
||||
\`\`\`json
|
||||
{
|
||||
"next_step": "answering" | "coding",
|
||||
"language": "typescript" | "python" | null,
|
||||
"file_name": string | null,
|
||||
"requirement": string
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Example 1:
|
||||
User request: Create a calculator app.
|
||||
You should return:
|
||||
\`\`\`json
|
||||
{
|
||||
"next_step": "coding",
|
||||
"language": "typescript",
|
||||
"file_name": "calculator.tsx",
|
||||
"requirement": "Generate code for a calculator app that has a simple UI with a display and button layout. The display should show the current input and the result. The buttons should include basic operators, numbers, clear, and equals. The calculation should work correctly."
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Example 2:
|
||||
User request: Explain how the game loop works.
|
||||
Context: You have already generated the code for a snake game.
|
||||
You should return:
|
||||
\`\`\`json
|
||||
{
|
||||
"next_step": "answering",
|
||||
"language": null,
|
||||
"file_name": null,
|
||||
"requirement": "The user is asking about the game loop. Explain how the game loop works."
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
${context}
|
||||
|
||||
Now, plan the user's next step for this request:
|
||||
${user_msg}
|
||||
`;
|
||||
|
||||
const response = await llm.complete({
|
||||
prompt,
|
||||
});
|
||||
// parse the response to Requirement
|
||||
// 1. use regex to find the json block
|
||||
const jsonBlock = response.text.match(/```json\s*([\s\S]*?)\s*```/);
|
||||
if (!jsonBlock) {
|
||||
throw new Error("No JSON block found in the response.");
|
||||
}
|
||||
const requirement = RequirementSchema.parse(JSON.parse(jsonBlock[1]));
|
||||
state.memory.put({
|
||||
role: "assistant",
|
||||
content: `The plan for next step: \n${response.text}`,
|
||||
});
|
||||
|
||||
if (requirement.next_step === "coding") {
|
||||
return generateArtifactEvent.with({
|
||||
requirement,
|
||||
});
|
||||
} else {
|
||||
return synthesizeAnswerEvent.with({});
|
||||
}
|
||||
});
|
||||
|
||||
workflow.handle([generateArtifactEvent], async ({ data: planData }) => {
|
||||
const { sendEvent } = getContext();
|
||||
const { state } = getContext();
|
||||
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
state: "generate",
|
||||
requirement: planData.requirement.requirement,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const previousArtifact = state.lastArtifact
|
||||
? JSON.stringify(state.lastArtifact)
|
||||
: "There is no previous artifact";
|
||||
const requirementText = planData.requirement.requirement;
|
||||
|
||||
const prompt = `
|
||||
You are a skilled developer who can help user with coding.
|
||||
You are given a task to generate or update a code for a given requirement.
|
||||
|
||||
## Follow these instructions:
|
||||
**1. Carefully read the user's requirements.**
|
||||
If any details are ambiguous or missing, make reasonable assumptions and clearly reflect those in your output.
|
||||
If the previous code is provided:
|
||||
+ Carefully analyze the code with the request to make the right changes.
|
||||
+ Avoid making a lot of changes from the previous code if the request is not to write the code from scratch again.
|
||||
**2. For code requests:**
|
||||
- If the user does not specify a framework or language, default to a React component using the Next.js framework.
|
||||
- For Next.js, use Shadcn UI components, Typescript, @types/node, @types/react, @types/react-dom, PostCSS, and TailwindCSS.
|
||||
The import pattern should be:
|
||||
\`\`\`typescript
|
||||
import { ComponentName } from "@/components/ui/component-name"
|
||||
import { Markdown } from "@llamaindex/chat-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
\`\`\`
|
||||
- Ensure the code is idiomatic, production-ready, and includes necessary imports.
|
||||
- Only generate code relevant to the user's request—do not add extra boilerplate.
|
||||
**3. Don't be verbose on response**
|
||||
- No other text or comments only return the code which wrapped by \`\`\`language\`\`\` block.
|
||||
- If the user's request is to update the code, only return the updated code.
|
||||
**4. Only the following languages are allowed: "typescript", "python".**
|
||||
**5. If there is no code to update, return the reason without any code block.**
|
||||
|
||||
## Example:
|
||||
\`\`\`typescript
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function MyComponent() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-screen">
|
||||
<Button>Click me</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
The previous code is:
|
||||
{previousArtifact}
|
||||
|
||||
Now, i have to generate the code for the following requirement:
|
||||
{requirement}
|
||||
`
|
||||
.replace("{previousArtifact}", previousArtifact)
|
||||
.replace("{requirement}", requirementText);
|
||||
|
||||
const response = await llm.complete({
|
||||
prompt,
|
||||
});
|
||||
|
||||
// Extract the code from the response
|
||||
const codeMatch = response.text.match(/```(\w+)([\s\S]*)```/);
|
||||
if (!codeMatch) {
|
||||
return synthesizeAnswerEvent.with({});
|
||||
}
|
||||
|
||||
const code = codeMatch[2].trim();
|
||||
|
||||
// Put the generated code to the memory
|
||||
state.memory.put({
|
||||
role: "assistant",
|
||||
content: `Updated the code: \n${response.text}`,
|
||||
});
|
||||
|
||||
// To show the Canvas panel for the artifact
|
||||
sendEvent(
|
||||
artifactEvent.with({
|
||||
type: "artifact",
|
||||
data: {
|
||||
type: "code",
|
||||
created_at: Date.now(),
|
||||
data: {
|
||||
language: planData.requirement.language || "",
|
||||
file_name: planData.requirement.file_name || "",
|
||||
code,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return synthesizeAnswerEvent.with({});
|
||||
});
|
||||
|
||||
workflow.handle([synthesizeAnswerEvent], async () => {
|
||||
const { sendEvent } = getContext();
|
||||
const { state } = getContext();
|
||||
|
||||
const chatHistory = await state.memory.getMessages();
|
||||
const messages = [
|
||||
...chatHistory,
|
||||
{
|
||||
role: "system" as const,
|
||||
content: `
|
||||
You are a helpful assistant who is responsible for explaining the work to the user.
|
||||
Based on the conversation history, provide an answer to the user's question.
|
||||
The user has access to the code so avoid mentioning the whole code again in your response.
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const responseStream = await llm.chat({
|
||||
messages,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
state: "completed",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
let response = "";
|
||||
for await (const chunk of responseStream) {
|
||||
response += chunk.delta;
|
||||
sendEvent(
|
||||
agentStreamEvent.with({
|
||||
delta: chunk.delta,
|
||||
response: "",
|
||||
currentAgentName: "assistant",
|
||||
raw: chunk,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return stopAgentEvent.with({
|
||||
result: response,
|
||||
});
|
||||
});
|
||||
|
||||
return workflow;
|
||||
}
|
||||
+1
-1
@@ -31,7 +31,7 @@ You can configure [LLM model](https://ts.llamaindex.ai/docs/llamaindex/modules/l
|
||||
|
||||
## Custom UI Components
|
||||
|
||||
For Deep Research, we have a custom component located in `components/deep_research_event.jsx`. This is used to display the results of the deep research workflow in a more user-friendly way
|
||||
For Deep Research, we have a custom component located in `components/ui_event.jsx`. This is used to display the results of the deep research workflow in a more user-friendly way
|
||||
|
||||
### Generate a new UI Component from workflow event
|
||||
|
||||
+416
@@ -0,0 +1,416 @@
|
||||
import { toSourceEvent } from "@llamaindex/server";
|
||||
import {
|
||||
agentStreamEvent,
|
||||
createStatefulMiddleware,
|
||||
createWorkflow,
|
||||
startAgentEvent,
|
||||
stopAgentEvent,
|
||||
workflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
import {
|
||||
ChatMemoryBuffer,
|
||||
LlamaCloudIndex,
|
||||
MessageContent,
|
||||
Metadata,
|
||||
MetadataMode,
|
||||
NodeWithScore,
|
||||
PromptTemplate,
|
||||
Settings,
|
||||
VectorStoreIndex,
|
||||
extractText,
|
||||
} from "llamaindex";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { z } from "zod";
|
||||
import { getIndex } from "./data";
|
||||
|
||||
// workflow factory
|
||||
export const workflowFactory = async (reqBody: any) => {
|
||||
const index = await getIndex(reqBody?.data);
|
||||
return getWorkflow(index);
|
||||
};
|
||||
|
||||
// workflow configs
|
||||
const MAX_QUESTIONS = 6; // max number of questions to research, research will stop when this number is reached
|
||||
const TOP_K = 10; // number of nodes to retrieve from the vector store
|
||||
|
||||
const createPlanResearchPrompt = new PromptTemplate({
|
||||
template: `
|
||||
You are a professor who is guiding a researcher to research a specific request/problem.
|
||||
Your task is to decide on a research plan for the researcher.
|
||||
|
||||
The possible actions are:
|
||||
+ Provide a list of questions for the researcher to investigate, with the purpose of clarifying the request.
|
||||
+ Write a report if the researcher has already gathered enough research on the topic and can resolve the initial request.
|
||||
+ Cancel the research if most of the answers from researchers indicate there is insufficient information to research the request. Do not attempt more than 3 research iterations or too many questions.
|
||||
|
||||
The workflow should be:
|
||||
+ Always begin by providing some initial questions for the researcher to investigate.
|
||||
+ Analyze the provided answers against the initial topic/request. If the answers are insufficient to resolve the initial request, provide additional questions for the researcher to investigate.
|
||||
+ If the answers are sufficient to resolve the initial request, instruct the researcher to write a report.
|
||||
|
||||
Here are the context:
|
||||
<Collected information>
|
||||
{context_str}
|
||||
</Collected information>
|
||||
|
||||
<Conversation context>
|
||||
{conversation_context}
|
||||
</Conversation context>
|
||||
|
||||
{enhanced_prompt}
|
||||
|
||||
Now, provide your decision in the required format for this user request:
|
||||
<User request>
|
||||
{user_request}
|
||||
</User request>
|
||||
`,
|
||||
templateVars: [
|
||||
"context_str",
|
||||
"conversation_context",
|
||||
"enhanced_prompt",
|
||||
"user_request",
|
||||
],
|
||||
});
|
||||
|
||||
const researchPrompt = new PromptTemplate({
|
||||
template: `
|
||||
You are a researcher who is in the process of answering the question.
|
||||
The purpose is to answer the question based on the collected information, without using prior knowledge or making up any new information.
|
||||
Always add citations to the sentence/point/paragraph using the id of the provided content.
|
||||
The citation should follow this format: [citation:id] where id is the id of the content.
|
||||
|
||||
E.g:
|
||||
If we have a context like this:
|
||||
<Citation id='abc-xyz'>
|
||||
Baby llama is called cria
|
||||
</Citation id='abc-xyz'>
|
||||
|
||||
And your answer uses the content, then the citation should be:
|
||||
- Baby llama is called cria [citation:abc-xyz]
|
||||
|
||||
Here is the provided context for the question:
|
||||
<Collected information>
|
||||
{context_str}
|
||||
</Collected information>
|
||||
|
||||
No prior knowledge, just use the provided context to answer the question: {question}
|
||||
`,
|
||||
templateVars: ["context_str", "question"],
|
||||
});
|
||||
|
||||
const WRITE_REPORT_PROMPT = `
|
||||
You are a researcher writing a report based on a user request and the research context.
|
||||
You have researched various perspectives related to the user request.
|
||||
The report should provide a comprehensive outline covering all important points from the researched perspectives.
|
||||
Create a well-structured outline for the research report that covers all the answers.
|
||||
|
||||
# IMPORTANT when writing in markdown format:
|
||||
+ Use tables or figures where appropriate to enhance presentation.
|
||||
+ Preserve all citation syntax (the \`[citation:id]()\` parts in the provided context). Keep these citations in the final report - no separate reference section is needed.
|
||||
+ Do not add links, a table of contents, or a references section to the report.
|
||||
`;
|
||||
|
||||
// workflow events
|
||||
type ResearchQuestion = { questionId: string; question: string };
|
||||
type ResearchResult = ResearchQuestion & { answer: string };
|
||||
|
||||
// class PlanResearchEvent extends WorkflowEvent<{}> {}
|
||||
const planResearchEvent = workflowEvent<{}>();
|
||||
const researchEvent = workflowEvent<ResearchQuestion>();
|
||||
const reportEvent = workflowEvent<{}>();
|
||||
|
||||
export const UIEventSchema = z
|
||||
.object({
|
||||
event: z
|
||||
.enum(["retrieve", "analyze", "answer"])
|
||||
.describe(
|
||||
"The type of event. DeepResearch has 3 main stages:\n1. retrieve: Retrieve the context from the vector store\n2. analyze: Analyze the context and generate a research questions to answer\n3. answer: Answer the provided questions. Each question has a unique id, when the state is done, the event will have the answer for the question.",
|
||||
),
|
||||
state: z
|
||||
.enum(["pending", "inprogress", "done", "error"])
|
||||
.describe("The state for each event"),
|
||||
id: z.string().optional().describe("The id of the question"),
|
||||
question: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The question generated by the LLM"),
|
||||
answer: z.string().optional().describe("The answer generated by the LLM"),
|
||||
})
|
||||
.describe("DeepResearchEvent");
|
||||
|
||||
type UIEventData = z.infer<typeof UIEventSchema>;
|
||||
|
||||
const uiEvent = workflowEvent<{
|
||||
type: "ui_event";
|
||||
data: UIEventData;
|
||||
}>();
|
||||
|
||||
// workflow definition
|
||||
export function getWorkflow(index: VectorStoreIndex | LlamaCloudIndex) {
|
||||
const retriever = index.asRetriever({ similarityTopK: TOP_K });
|
||||
const { withState, getContext } = createStatefulMiddleware(() => {
|
||||
return {
|
||||
memory: new ChatMemoryBuffer({
|
||||
llm: Settings.llm,
|
||||
chatHistory: [],
|
||||
}),
|
||||
contextNodes: [] as NodeWithScore<Metadata>[],
|
||||
userRequest: "" as MessageContent,
|
||||
totalQuestions: 0,
|
||||
researchResults: [] as ResearchResult[],
|
||||
};
|
||||
});
|
||||
const workflow = withState(createWorkflow());
|
||||
|
||||
workflow.handle([startAgentEvent], async ({ data }) => {
|
||||
const { userInput, chatHistory = [] } = data;
|
||||
const { sendEvent, state } = getContext();
|
||||
if (!userInput) throw new Error("Invalid input");
|
||||
|
||||
state.memory.set(chatHistory);
|
||||
state.memory.put({ role: "user", content: userInput });
|
||||
state.userRequest = userInput;
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
event: "retrieve",
|
||||
state: "inprogress",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const retrievedNodes = await retriever.retrieve({ query: userInput });
|
||||
|
||||
sendEvent(toSourceEvent(retrievedNodes));
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: { event: "retrieve", state: "done" },
|
||||
}),
|
||||
);
|
||||
|
||||
state.contextNodes.push(...retrievedNodes);
|
||||
|
||||
return planResearchEvent.with({});
|
||||
});
|
||||
|
||||
workflow.handle([planResearchEvent], async ({ data }) => {
|
||||
const { sendEvent, state, stream } = getContext();
|
||||
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: { event: "analyze", state: "inprogress" },
|
||||
}),
|
||||
);
|
||||
|
||||
const { decision, researchQuestions, cancelReason } =
|
||||
await createResearchPlan(
|
||||
state.memory,
|
||||
state.contextNodes
|
||||
.map((node) => node.node.getContent(MetadataMode.NONE))
|
||||
.join("\n"),
|
||||
enhancedPrompt(state.totalQuestions),
|
||||
state.userRequest,
|
||||
);
|
||||
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: { event: "analyze", state: "done" },
|
||||
}),
|
||||
);
|
||||
if (decision === "cancel") {
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: { event: "analyze", state: "done" },
|
||||
}),
|
||||
);
|
||||
return agentStreamEvent.with({
|
||||
delta: cancelReason ?? "Research cancelled without any reason.",
|
||||
response: cancelReason ?? "Research cancelled without any reason.",
|
||||
currentAgentName: "",
|
||||
raw: null,
|
||||
});
|
||||
}
|
||||
if (decision === "research" && researchQuestions.length > 0) {
|
||||
state.totalQuestions += researchQuestions.length;
|
||||
state.memory.put({
|
||||
role: "assistant",
|
||||
content:
|
||||
"We need to find answers to the following questions:\n" +
|
||||
researchQuestions.join("\n"),
|
||||
});
|
||||
researchQuestions.forEach(({ questionId: id, question }) => {
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: { event: "answer", state: "pending", id, question },
|
||||
}),
|
||||
);
|
||||
sendEvent(researchEvent.with({ questionId: id, question }));
|
||||
});
|
||||
const events = await stream
|
||||
.until(() => state.researchResults.length === researchQuestions.length)
|
||||
.toArray();
|
||||
return planResearchEvent.with({});
|
||||
}
|
||||
state.memory.put({
|
||||
role: "assistant",
|
||||
content: "No more idea to analyze. We should report the answers.",
|
||||
});
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: { event: "analyze", state: "done" },
|
||||
}),
|
||||
);
|
||||
return reportEvent.with({});
|
||||
});
|
||||
|
||||
workflow.handle([researchEvent], async ({ data }) => {
|
||||
const { sendEvent, state } = getContext();
|
||||
const { questionId, question } = data;
|
||||
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
event: "answer",
|
||||
state: "inprogress",
|
||||
id: questionId,
|
||||
question,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const answer = await answerQuestion(
|
||||
contextStr(state.contextNodes),
|
||||
question,
|
||||
);
|
||||
state.researchResults.push({ questionId, question, answer });
|
||||
|
||||
state.memory.put({
|
||||
role: "assistant",
|
||||
content: `<Question>${question}</Question>\n<Answer>${answer}</Answer>`,
|
||||
});
|
||||
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
event: "answer",
|
||||
state: "done",
|
||||
id: questionId,
|
||||
question,
|
||||
answer,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
workflow.handle([reportEvent], async ({ data }) => {
|
||||
const { sendEvent, state } = getContext();
|
||||
const chatHistory = await state.memory.getAllMessages();
|
||||
const messages = chatHistory.concat([
|
||||
{
|
||||
role: "system",
|
||||
content: WRITE_REPORT_PROMPT,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
"Write a report addressing the user request based on the research provided the context",
|
||||
},
|
||||
]);
|
||||
|
||||
const stream = await Settings.llm.chat({ messages, stream: true });
|
||||
let response = "";
|
||||
for await (const chunk of stream) {
|
||||
response += chunk.delta;
|
||||
sendEvent(
|
||||
agentStreamEvent.with({
|
||||
delta: chunk.delta,
|
||||
response,
|
||||
currentAgentName: "",
|
||||
raw: stream,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return stopAgentEvent.with({
|
||||
result: response,
|
||||
});
|
||||
});
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
const createResearchPlan = async (
|
||||
memory: ChatMemoryBuffer,
|
||||
contextStr: string,
|
||||
enhancedPrompt: string,
|
||||
userRequest: MessageContent,
|
||||
) => {
|
||||
const chatHistory = await memory.getMessages();
|
||||
|
||||
const conversationContext = chatHistory
|
||||
.map((message) => `${message.role}: ${message.content}`)
|
||||
.join("\n");
|
||||
|
||||
const prompt = createPlanResearchPrompt.format({
|
||||
context_str: contextStr,
|
||||
conversation_context: conversationContext,
|
||||
enhanced_prompt: enhancedPrompt,
|
||||
user_request: extractText(userRequest),
|
||||
});
|
||||
|
||||
const responseFormat = z.object({
|
||||
decision: z.enum(["research", "write", "cancel"]),
|
||||
researchQuestions: z.array(z.string()),
|
||||
cancelReason: z.string().optional(),
|
||||
});
|
||||
|
||||
const result = await Settings.llm.complete({ prompt, responseFormat });
|
||||
const plan = JSON.parse(result.text) as z.infer<typeof responseFormat>;
|
||||
|
||||
return {
|
||||
...plan,
|
||||
researchQuestions: plan.researchQuestions.map((question) => ({
|
||||
questionId: randomUUID(),
|
||||
question,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const contextStr = (contextNodes: NodeWithScore<Metadata>[]) => {
|
||||
return contextNodes
|
||||
.map((node) => {
|
||||
const nodeId = node.node.id_;
|
||||
const nodeContent = node.node.getContent(MetadataMode.NONE);
|
||||
return `<Citation id='${nodeId}'>\n${nodeContent}</Citation id='${nodeId}'>`;
|
||||
})
|
||||
.join("\n");
|
||||
};
|
||||
|
||||
const enhancedPrompt = (totalQuestions: number) => {
|
||||
if (totalQuestions === 0) {
|
||||
return "The student has no questions to research. Let start by providing some questions for the student to research.";
|
||||
}
|
||||
|
||||
if (totalQuestions >= MAX_QUESTIONS) {
|
||||
return `The student has researched ${totalQuestions} questions. Should proceeding writing report or cancel the research if the answers are not enough to write a report.`;
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
const answerQuestion = async (contextStr: string, question: string) => {
|
||||
const prompt = researchPrompt.format({
|
||||
context_str: contextStr,
|
||||
question,
|
||||
});
|
||||
const result = await Settings.llm.complete({ prompt });
|
||||
return result.text;
|
||||
};
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) project bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, install the dependencies:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Second, run the development server:
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the chat UI.
|
||||
|
||||
## Configure LLM and Embedding Model
|
||||
|
||||
You can configure [LLM model](https://ts.llamaindex.ai/docs/llamaindex/modules/llms) in the [settings file](src/app/settings.ts).
|
||||
|
||||
## Custom UI Components
|
||||
|
||||
We have a custom component located in `components/ui_event.jsx`. This is used to display the state of artifact workflows in UI. You can regenerate a new UI component from the workflow event schema by running the following command:
|
||||
|
||||
```
|
||||
npm run generate:ui
|
||||
```
|
||||
|
||||
## Use Case
|
||||
|
||||
AI-powered document generator that can help you generate documents with a chat interface and simple markdown editor.
|
||||
|
||||
To update the workflow, you can modify the code in [`workflow.ts`](app/workflow.ts).
|
||||
|
||||
You can start by sending 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' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{ "messages": [{ "role": "user", "content": "Compare the financial performance of Apple and Tesla" }] }'
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
|
||||
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai/docs/llamaindex) - learn about LlamaIndex (Typescript features).
|
||||
- [Workflows Introduction](https://ts.llamaindex.ai/docs/llamaindex/modules/workflows) - learn about LlamaIndexTS workflows.
|
||||
|
||||
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
import { extractLastArtifact } from "@llamaindex/server";
|
||||
import { ChatMemoryBuffer, MessageContent, Settings } from "llamaindex";
|
||||
|
||||
import {
|
||||
agentStreamEvent,
|
||||
createStatefulMiddleware,
|
||||
createWorkflow,
|
||||
startAgentEvent,
|
||||
stopAgentEvent,
|
||||
workflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
export const DocumentRequirementSchema = z.object({
|
||||
type: z.enum(["markdown", "html"]),
|
||||
title: z.string(),
|
||||
requirement: z.string(),
|
||||
});
|
||||
|
||||
export type DocumentRequirement = z.infer<typeof DocumentRequirementSchema>;
|
||||
|
||||
export const UIEventSchema = z.object({
|
||||
type: z.literal("ui_event"),
|
||||
data: z.object({
|
||||
state: z
|
||||
.enum(["plan", "generate", "completed"])
|
||||
.describe(
|
||||
"The current state of the workflow: 'plan', 'generate', or 'completed'.",
|
||||
),
|
||||
requirement: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"An optional requirement creating or updating a document, if applicable.",
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
export type UIEvent = z.infer<typeof UIEventSchema>;
|
||||
|
||||
const planEvent = workflowEvent<{
|
||||
userInput: MessageContent;
|
||||
context?: string | undefined;
|
||||
}>();
|
||||
|
||||
const generateArtifactEvent = workflowEvent<{
|
||||
requirement: DocumentRequirement;
|
||||
}>();
|
||||
|
||||
const synthesizeAnswerEvent = workflowEvent<{
|
||||
requirement: DocumentRequirement;
|
||||
generatedArtifact: string;
|
||||
}>();
|
||||
|
||||
const uiEvent = workflowEvent<UIEvent>();
|
||||
|
||||
const artifactEvent = workflowEvent<{
|
||||
type: "artifact";
|
||||
data: {
|
||||
type: "document";
|
||||
created_at: number;
|
||||
data: {
|
||||
title: string;
|
||||
content: string;
|
||||
type: "markdown" | "html";
|
||||
};
|
||||
};
|
||||
}>();
|
||||
|
||||
export function workflowFactory(reqBody: any) {
|
||||
const llm = Settings.llm;
|
||||
|
||||
const { withState, getContext } = createStatefulMiddleware(() => {
|
||||
return {
|
||||
memory: new ChatMemoryBuffer({ llm }),
|
||||
lastArtifact: extractLastArtifact(reqBody),
|
||||
};
|
||||
});
|
||||
const workflow = withState(createWorkflow());
|
||||
|
||||
workflow.handle([startAgentEvent], async ({ data }) => {
|
||||
const { userInput, chatHistory = [] } = data;
|
||||
// Prepare chat history
|
||||
const { state } = getContext();
|
||||
// Put user input to the memory
|
||||
if (!userInput) {
|
||||
throw new Error("Missing user input to start the workflow");
|
||||
}
|
||||
state.memory.set(chatHistory);
|
||||
state.memory.put({ role: "user", content: userInput });
|
||||
|
||||
return planEvent.with({
|
||||
userInput,
|
||||
context: state.lastArtifact
|
||||
? JSON.stringify(state.lastArtifact)
|
||||
: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
workflow.handle([planEvent], async ({ data: planData }) => {
|
||||
const { sendEvent } = getContext();
|
||||
const { state } = getContext();
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
state: "plan",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const user_msg = planData.userInput;
|
||||
const context = planData.context
|
||||
? `## The context is: \n${planData.context}\n`
|
||||
: "";
|
||||
const prompt = `
|
||||
You are a documentation analyst responsible for analyzing the user's request and providing requirements for document generation or update.
|
||||
Follow these instructions:
|
||||
1. Carefully analyze the conversation history and the user's request to determine what has been done and what the next step should be.
|
||||
2. From the user's request, provide requirements for the next step of the document generation or update.
|
||||
3. Do not be verbose; only return the requirements for the next step of the document generation or update.
|
||||
4. Only the following document types are allowed: "markdown", "html".
|
||||
5. The requirement should be in the following format:
|
||||
\`\`\`json
|
||||
{
|
||||
"type": "markdown" | "html",
|
||||
"title": string,
|
||||
"requirement": string
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Example:
|
||||
User request: Create a project guideline document.
|
||||
You should return:
|
||||
\`\`\`json
|
||||
{
|
||||
"type": "markdown",
|
||||
"title": "Project Guideline",
|
||||
"requirement": "Generate a Markdown document that outlines the project goals, deliverables, and timeline. Include sections for introduction, objectives, deliverables, and timeline."
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
User request: Add a troubleshooting section to the guideline.
|
||||
You should return:
|
||||
\`\`\`json
|
||||
{
|
||||
"type": "markdown",
|
||||
"title": "Project Guideline",
|
||||
"requirement": "Add a 'Troubleshooting' section at the end of the document with common issues and solutions."
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
${context}
|
||||
|
||||
Now, please plan for the user's request:
|
||||
${user_msg}
|
||||
`;
|
||||
|
||||
const response = await llm.complete({
|
||||
prompt,
|
||||
});
|
||||
// Parse the response to DocumentRequirement
|
||||
const jsonBlock = response.text.match(/```json\s*([\s\S]*?)\s*```/);
|
||||
if (!jsonBlock) {
|
||||
throw new Error("No JSON block found in the response.");
|
||||
}
|
||||
const requirement = DocumentRequirementSchema.parse(
|
||||
JSON.parse(jsonBlock[1]),
|
||||
);
|
||||
state.memory.put({
|
||||
role: "assistant",
|
||||
content: `Planning for the document generation: \n${response.text}`,
|
||||
});
|
||||
return generateArtifactEvent.with({
|
||||
requirement,
|
||||
});
|
||||
});
|
||||
|
||||
workflow.handle(
|
||||
[generateArtifactEvent],
|
||||
async ({ data: { requirement } }) => {
|
||||
const { sendEvent } = getContext();
|
||||
const { state } = getContext();
|
||||
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
state: "generate",
|
||||
requirement: requirement.requirement,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const previousArtifact = state.lastArtifact
|
||||
? JSON.stringify(state.lastArtifact)
|
||||
: "";
|
||||
const requirementStr = JSON.stringify(requirement);
|
||||
|
||||
const prompt = `
|
||||
You are a skilled technical writer who can help users with documentation.
|
||||
You are given a task to generate or update a document for a given requirement.
|
||||
|
||||
## Follow these instructions:
|
||||
**1. Carefully read the user's requirements.**
|
||||
If any details are ambiguous or missing, make reasonable assumptions and clearly reflect those in your output.
|
||||
If the previous document is provided:
|
||||
+ Carefully analyze the document with the request to make the right changes.
|
||||
+ Avoid making unnecessary changes from the previous document if the request is not to rewrite it from scratch.
|
||||
**2. For document requests:**
|
||||
- If the user does not specify a type, default to Markdown.
|
||||
- Ensure the document is clear, well-structured, and grammatically correct.
|
||||
- Only generate content relevant to the user's request—do not add extra boilerplate.
|
||||
**3. Do not be verbose in your response.**
|
||||
- No other text or comments; only return the document content wrapped by the appropriate code block (\`\`\`markdown or \`\`\`html).
|
||||
- If the user's request is to update the document, only return the updated document.
|
||||
**4. Only the following types are allowed: "markdown", "html".**
|
||||
**5. If there is no change to the document, return the reason without any code block.**
|
||||
|
||||
## Example:
|
||||
\`\`\`markdown
|
||||
# Project Guideline
|
||||
|
||||
## Introduction
|
||||
...
|
||||
\`\`\`
|
||||
|
||||
The previous content is:
|
||||
${previousArtifact}
|
||||
|
||||
Now, please generate the document for the following requirement:
|
||||
${requirementStr}
|
||||
`;
|
||||
|
||||
const response = await llm.complete({
|
||||
prompt,
|
||||
});
|
||||
|
||||
// Extract the document from the response
|
||||
const docMatch = response.text.match(/```(markdown|html)([\s\S]*)```/);
|
||||
const generatedContent = response.text;
|
||||
|
||||
if (docMatch) {
|
||||
const content = docMatch[2].trim();
|
||||
const docType = docMatch[1] as "markdown" | "html";
|
||||
|
||||
// Put the generated document to the memory
|
||||
state.memory.put({
|
||||
role: "assistant",
|
||||
content: `Generated document: \n${response.text}`,
|
||||
});
|
||||
|
||||
// To show the Canvas panel for the artifact
|
||||
sendEvent(
|
||||
artifactEvent.with({
|
||||
type: "artifact",
|
||||
data: {
|
||||
type: "document",
|
||||
created_at: Date.now(),
|
||||
data: {
|
||||
title: requirement.title,
|
||||
content: content,
|
||||
type: docType,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return synthesizeAnswerEvent.with({
|
||||
requirement,
|
||||
generatedArtifact: generatedContent,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
workflow.handle([synthesizeAnswerEvent], async ({ data }) => {
|
||||
const { sendEvent } = getContext();
|
||||
const { state } = getContext();
|
||||
|
||||
const chatHistory = await state.memory.getMessages();
|
||||
const messages = [
|
||||
...chatHistory,
|
||||
{
|
||||
role: "system" as const,
|
||||
content: `
|
||||
Your responsibility is to explain the work to the user.
|
||||
If there is no document to update, explain the reason.
|
||||
If the document is updated, just summarize what changed. Don't need to include the whole document again in the response.
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const responseStream = await llm.chat({
|
||||
messages,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
sendEvent(
|
||||
uiEvent.with({
|
||||
type: "ui_event",
|
||||
data: {
|
||||
state: "completed",
|
||||
requirement: data.requirement.requirement,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
let response = "";
|
||||
for await (const chunk of responseStream) {
|
||||
response += chunk.delta;
|
||||
sendEvent(
|
||||
agentStreamEvent.with({
|
||||
delta: chunk.delta,
|
||||
response: "",
|
||||
currentAgentName: "assistant",
|
||||
raw: chunk,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return stopAgentEvent.with({
|
||||
result: response,
|
||||
});
|
||||
});
|
||||
|
||||
return workflow;
|
||||
}
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
import { toAgentRunEvent, toSourceEvent } from "@llamaindex/server";
|
||||
import {
|
||||
callTools,
|
||||
chatWithTools,
|
||||
documentGenerator,
|
||||
interpreter,
|
||||
} from "@llamaindex/tools";
|
||||
import {
|
||||
agentStreamEvent,
|
||||
createStatefulMiddleware,
|
||||
createWorkflow,
|
||||
startAgentEvent,
|
||||
stopAgentEvent,
|
||||
workflowEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
import {
|
||||
BaseToolWithCall,
|
||||
ChatMemoryBuffer,
|
||||
ChatMessage,
|
||||
Metadata,
|
||||
NodeWithScore,
|
||||
Settings,
|
||||
ToolCall,
|
||||
ToolCallLLM,
|
||||
} from "llamaindex";
|
||||
import { getIndex } from "./data";
|
||||
|
||||
export async function workflowFactory(reqBody: any) {
|
||||
const index = await getIndex(reqBody?.data);
|
||||
|
||||
const queryEngineTool = index.queryTool({
|
||||
metadata: {
|
||||
name: "query_document",
|
||||
description: `This tool can retrieve information about Apple and Tesla financial data`,
|
||||
},
|
||||
includeSourceNodes: true,
|
||||
});
|
||||
|
||||
if (!process.env.E2B_API_KEY) {
|
||||
throw new Error("E2B_API_KEY is required to use the code interpreter tool");
|
||||
}
|
||||
|
||||
const codeInterpreterTool = interpreter({
|
||||
apiKey: process.env.E2B_API_KEY!,
|
||||
});
|
||||
const documentGeneratorTool = documentGenerator();
|
||||
|
||||
return getWorkflow(
|
||||
queryEngineTool,
|
||||
codeInterpreterTool,
|
||||
documentGeneratorTool,
|
||||
);
|
||||
}
|
||||
|
||||
// workflow events
|
||||
const inputEvent = workflowEvent<{ input: ChatMessage[] }>();
|
||||
const researchEvent = workflowEvent<{ toolCalls: ToolCall[] }>();
|
||||
const analyzeEvent = workflowEvent<{ input: ChatMessage | ToolCall[] }>();
|
||||
const reportGenerationEvent = workflowEvent<{ toolCalls: ToolCall[] }>();
|
||||
|
||||
const DEFAULT_SYSTEM_PROMPT = `
|
||||
You are a financial analyst who are given a set of tools to help you.
|
||||
It's good to using appropriate tools for the user request and always use the information from the tools, don't make up anything yourself.
|
||||
For the query engine tool, you should break down the user request into a list of queries and call the tool with the queries.
|
||||
`;
|
||||
|
||||
// workflow definition
|
||||
export function getWorkflow(
|
||||
queryEngineTool: BaseToolWithCall,
|
||||
codeInterpreterTool: BaseToolWithCall,
|
||||
documentGeneratorTool: BaseToolWithCall,
|
||||
) {
|
||||
const llm = Settings.llm as ToolCallLLM;
|
||||
if (!llm.supportToolCall) {
|
||||
throw new Error("LLM is not a ToolCallLLM");
|
||||
}
|
||||
const { withState, getContext } = createStatefulMiddleware(() => ({
|
||||
memory: new ChatMemoryBuffer({ llm, chatHistory: [] }),
|
||||
}));
|
||||
|
||||
const workflow = withState(createWorkflow());
|
||||
|
||||
// Add steps
|
||||
workflow.handle([startAgentEvent], async ({ data }) => {
|
||||
const { state } = getContext();
|
||||
const { userInput, chatHistory = [] } = data;
|
||||
if (!userInput) throw new Error("Invalid input");
|
||||
|
||||
state.memory.set(chatHistory);
|
||||
|
||||
state.memory.put({ role: "system", content: DEFAULT_SYSTEM_PROMPT });
|
||||
|
||||
state.memory.put({ role: "user", content: userInput });
|
||||
|
||||
const messages = await state.memory.getMessages();
|
||||
return inputEvent.with({ input: messages });
|
||||
});
|
||||
|
||||
workflow.handle([inputEvent], async ({ data }) => {
|
||||
const { sendEvent, state } = getContext();
|
||||
const chatHistory = data.input;
|
||||
|
||||
const tools = [codeInterpreterTool, documentGeneratorTool, queryEngineTool];
|
||||
|
||||
const toolCallResponse = await chatWithTools(llm, tools, chatHistory);
|
||||
|
||||
if (!toolCallResponse.hasToolCall()) {
|
||||
const generator = toolCallResponse.responseGenerator;
|
||||
let response = "";
|
||||
if (generator) {
|
||||
for await (const chunk of generator) {
|
||||
response += chunk.delta;
|
||||
sendEvent(
|
||||
agentStreamEvent.with({
|
||||
delta: chunk.delta,
|
||||
response,
|
||||
currentAgentName: "LLM", // Or derive from context if needed
|
||||
raw: chunk.raw,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
return stopAgentEvent.with({ result: response });
|
||||
}
|
||||
|
||||
if (toolCallResponse.hasMultipleTools()) {
|
||||
state.memory.put({
|
||||
role: "assistant",
|
||||
content:
|
||||
"Calling different tools is not allowed. Please only use multiple calls of the same tool.",
|
||||
});
|
||||
const newChatHistory = await state.memory.getMessages();
|
||||
return inputEvent.with({ input: newChatHistory });
|
||||
}
|
||||
|
||||
// Put the LLM tool call message into the memory
|
||||
// And trigger the next step according to the tool call
|
||||
if (toolCallResponse.toolCallMessage) {
|
||||
state.memory.put(toolCallResponse.toolCallMessage);
|
||||
}
|
||||
const toolName = toolCallResponse.getToolNames()[0];
|
||||
switch (toolName) {
|
||||
case codeInterpreterTool.metadata.name:
|
||||
return analyzeEvent.with({
|
||||
input: toolCallResponse.toolCalls,
|
||||
});
|
||||
case documentGeneratorTool.metadata.name:
|
||||
return reportGenerationEvent.with({
|
||||
toolCalls: toolCallResponse.toolCalls,
|
||||
});
|
||||
default:
|
||||
if (queryEngineTool.metadata.name === toolName) {
|
||||
return researchEvent.with({
|
||||
toolCalls: toolCallResponse.toolCalls,
|
||||
});
|
||||
}
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
});
|
||||
|
||||
workflow.handle([researchEvent], async ({ data }) => {
|
||||
const { sendEvent, state } = getContext();
|
||||
sendEvent(
|
||||
toAgentRunEvent({
|
||||
agent: "Researcher",
|
||||
text: "Researching data",
|
||||
type: "text",
|
||||
}),
|
||||
);
|
||||
|
||||
const { toolCalls } = data;
|
||||
|
||||
const toolMsgs = await callTools({
|
||||
tools: [queryEngineTool],
|
||||
toolCalls,
|
||||
writeEvent: (text, step) => {
|
||||
sendEvent(
|
||||
toAgentRunEvent({
|
||||
agent: "Researcher",
|
||||
text,
|
||||
type: toolCalls.length > 1 ? "progress" : "text",
|
||||
current: step,
|
||||
total: toolCalls.length,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
for (const toolMsg of toolMsgs) {
|
||||
state.memory.put(toolMsg);
|
||||
}
|
||||
|
||||
const sourcesNodes: NodeWithScore<Metadata>[] = toolMsgs
|
||||
.map((msg) => (msg.options as any)?.toolResult?.result?.sourceNodes)
|
||||
.flat()
|
||||
.filter(Boolean);
|
||||
|
||||
if (sourcesNodes.length > 0) {
|
||||
sendEvent(toSourceEvent(sourcesNodes));
|
||||
}
|
||||
|
||||
// Send a message indicating research is done, triggering analysis
|
||||
return analyzeEvent.with({
|
||||
input: {
|
||||
role: "assistant",
|
||||
content:
|
||||
"I have finished researching the data, please analyze the data.",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Analyze a research result or a tool call for code interpreter from the LLM
|
||||
*/
|
||||
workflow.handle([analyzeEvent], async ({ data }) => {
|
||||
const { sendEvent, state } = getContext();
|
||||
sendEvent(
|
||||
toAgentRunEvent({
|
||||
agent: "Analyst",
|
||||
text: "Analyzing data",
|
||||
type: "text",
|
||||
}),
|
||||
);
|
||||
// Request by workflow LLM, input is a list of tool calls
|
||||
let toolCalls: ToolCall[] = [];
|
||||
if (Array.isArray(data.input)) {
|
||||
toolCalls = data.input;
|
||||
} else {
|
||||
// Requested by Researcher, input is a ChatMessage
|
||||
// We start new LLM chat specifically for analyzing the data
|
||||
const analysisPrompt = `
|
||||
You are an expert in analyzing financial data.
|
||||
You are given a set of financial data to analyze. Your task is to analyze the financial data and return a report.
|
||||
Your response should include a detailed analysis of the financial data, including any trends, patterns, or insights that you find.
|
||||
Construct the analysis in textual format; including tables would be great!
|
||||
Don't need to synthesize the data, just analyze and provide your findings.
|
||||
`;
|
||||
|
||||
// Clone the current chat history
|
||||
// Add the analysis system prompt and the message from the researcher
|
||||
const currentChatHistory = await state.memory.getMessages();
|
||||
const newChatHistory = [
|
||||
...currentChatHistory,
|
||||
{ role: "system", content: analysisPrompt },
|
||||
data.input, // This is the ChatMessage from the research step
|
||||
];
|
||||
const toolCallResponse = await chatWithTools(
|
||||
llm,
|
||||
[codeInterpreterTool],
|
||||
newChatHistory as ChatMessage[],
|
||||
);
|
||||
|
||||
if (!toolCallResponse.hasToolCall()) {
|
||||
// If no tool call needed for analysis, put the response directly
|
||||
state.memory.put(await toolCallResponse.asFullResponse());
|
||||
const finalChatHistory = await state.memory.getMessages();
|
||||
return inputEvent.with({ input: finalChatHistory });
|
||||
} else {
|
||||
state.memory.put(toolCallResponse.toolCallMessage!);
|
||||
toolCalls = toolCallResponse.toolCalls;
|
||||
}
|
||||
}
|
||||
|
||||
// Call the code interpreter tools if needed
|
||||
if (toolCalls.length > 0) {
|
||||
const toolMsgs = await callTools({
|
||||
tools: [codeInterpreterTool],
|
||||
toolCalls,
|
||||
writeEvent: (text, step) => {
|
||||
sendEvent(
|
||||
toAgentRunEvent({
|
||||
agent: "Analyst",
|
||||
text,
|
||||
type: toolCalls.length > 1 ? "progress" : "text",
|
||||
current: step,
|
||||
total: toolCalls.length,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
for (const toolMsg of toolMsgs) {
|
||||
state.memory.put(toolMsg);
|
||||
}
|
||||
}
|
||||
|
||||
const finalChatHistory = await state.memory.getMessages();
|
||||
// After analysis (or tool calls for analysis), trigger the next LLM input cycle
|
||||
return inputEvent.with({ input: finalChatHistory });
|
||||
});
|
||||
|
||||
workflow.handle([reportGenerationEvent], async ({ data }) => {
|
||||
const { sendEvent, state } = getContext();
|
||||
const { toolCalls } = data;
|
||||
|
||||
const toolMsgs = await callTools({
|
||||
tools: [documentGeneratorTool],
|
||||
toolCalls,
|
||||
writeEvent: (text, step) => {
|
||||
sendEvent(
|
||||
toAgentRunEvent({
|
||||
agent: "Reporter",
|
||||
text,
|
||||
type: toolCalls.length > 1 ? "progress" : "text",
|
||||
current: step,
|
||||
total: toolCalls.length,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
for (const toolMsg of toolMsgs) {
|
||||
state.memory.put(toolMsg);
|
||||
}
|
||||
const chatHistory = await state.memory.getMessages();
|
||||
// After report generation, trigger the next LLM input cycle
|
||||
return inputEvent.with({ input: chatHistory });
|
||||
});
|
||||
|
||||
return workflow;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { SimpleDirectoryReader } from "@llamaindex/readers/directory";
|
||||
import "dotenv/config";
|
||||
import { storageContextFromDefaults, VectorStoreIndex } from "llamaindex";
|
||||
import { initSettings } from "./app/settings";
|
||||
|
||||
async function generateDatasource() {
|
||||
console.log(`Generating storage context...`);
|
||||
// Split documents, create embeddings and store them in the storage context
|
||||
const storageContext = await storageContextFromDefaults({
|
||||
persistDir: "storage",
|
||||
});
|
||||
// load documents from current directory into an index
|
||||
const reader = new SimpleDirectoryReader();
|
||||
const documents = await reader.loadData("data");
|
||||
|
||||
await VectorStoreIndex.fromDocuments(documents, {
|
||||
storageContext,
|
||||
});
|
||||
console.log("Storage context successfully generated.");
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
|
||||
initSettings();
|
||||
|
||||
if (command === "ui") {
|
||||
console.error("This project doesn't use any custom UI.");
|
||||
return;
|
||||
} else {
|
||||
if (command !== "datasource") {
|
||||
console.error(
|
||||
`Unrecognized command: ${command}. Generating datasource by default.`,
|
||||
);
|
||||
}
|
||||
await generateDatasource();
|
||||
}
|
||||
})();
|
||||
+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
|
||||
}
|
||||
})();
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
|
||||
import { LlamaCloudIndex } from "llamaindex";
|
||||
|
||||
type LlamaCloudDataSourceParams = {
|
||||
llamaCloudPipeline?: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
|
||||
import { LlamaCloudIndex } from "llamaindex";
|
||||
|
||||
type LlamaCloudDataSourceParams = {
|
||||
llamaCloudPipeline?: {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
from app.code_workflow import CodeArtifactWorkflow
|
||||
|
||||
# from app.document_workflow import DocumentArtifactWorkflow to generate documents
|
||||
from llama_index.core.workflow import Workflow
|
||||
from llama_index.llms.openai import OpenAI
|
||||
from llama_index.server.api.models import ChatRequest
|
||||
|
||||
|
||||
def create_workflow(chat_request: ChatRequest) -> Workflow:
|
||||
workflow = CodeArtifactWorkflow(
|
||||
llm=OpenAI(model="gpt-4.1"),
|
||||
chat_request=chat_request,
|
||||
timeout=120.0,
|
||||
)
|
||||
return workflow
|
||||
-447
@@ -1,447 +0,0 @@
|
||||
import { toSourceEvent, toStreamGenerator } from "@llamaindex/server";
|
||||
import {
|
||||
AgentInputData,
|
||||
AgentWorkflowContext,
|
||||
ChatMemoryBuffer,
|
||||
ChatResponseChunk,
|
||||
HandlerContext,
|
||||
LlamaCloudIndex,
|
||||
Metadata,
|
||||
MetadataMode,
|
||||
NodeWithScore,
|
||||
PromptTemplate,
|
||||
Settings,
|
||||
StartEvent,
|
||||
StopEvent as StopEventBase,
|
||||
ToolCallLLM,
|
||||
VectorStoreIndex,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "llamaindex";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { z } from "zod";
|
||||
import { getIndex } from "./data";
|
||||
|
||||
// workflow factory
|
||||
export const workflowFactory = async (reqBody: any) => {
|
||||
const index = await getIndex(reqBody?.data);
|
||||
return new DeepResearchWorkflow(index);
|
||||
};
|
||||
|
||||
// workflow configs
|
||||
const MAX_QUESTIONS = 6; // max number of questions to research, research will stop when this number is reached
|
||||
const TIMEOUT = 360; // timeout in seconds
|
||||
const TOP_K = 10; // number of nodes to retrieve from the vector store
|
||||
|
||||
const createPlanResearchPrompt = new PromptTemplate({
|
||||
template: `
|
||||
You are a professor who is guiding a researcher to research a specific request/problem.
|
||||
Your task is to decide on a research plan for the researcher.
|
||||
|
||||
The possible actions are:
|
||||
+ Provide a list of questions for the researcher to investigate, with the purpose of clarifying the request.
|
||||
+ Write a report if the researcher has already gathered enough research on the topic and can resolve the initial request.
|
||||
+ Cancel the research if most of the answers from researchers indicate there is insufficient information to research the request. Do not attempt more than 3 research iterations or too many questions.
|
||||
|
||||
The workflow should be:
|
||||
+ Always begin by providing some initial questions for the researcher to investigate.
|
||||
+ Analyze the provided answers against the initial topic/request. If the answers are insufficient to resolve the initial request, provide additional questions for the researcher to investigate.
|
||||
+ If the answers are sufficient to resolve the initial request, instruct the researcher to write a report.
|
||||
|
||||
Here are the context:
|
||||
<Collected information>
|
||||
{context_str}
|
||||
</Collected information>
|
||||
|
||||
<Conversation context>
|
||||
{conversation_context}
|
||||
</Conversation context>
|
||||
|
||||
{enhanced_prompt}
|
||||
|
||||
Now, provide your decision in the required format for this user request:
|
||||
<User request>
|
||||
{user_request}
|
||||
</User request>
|
||||
`,
|
||||
templateVars: [
|
||||
"context_str",
|
||||
"conversation_context",
|
||||
"enhanced_prompt",
|
||||
"user_request",
|
||||
],
|
||||
});
|
||||
|
||||
const researchPrompt = new PromptTemplate({
|
||||
template: `
|
||||
You are a researcher who is in the process of answering the question.
|
||||
The purpose is to answer the question based on the collected information, without using prior knowledge or making up any new information.
|
||||
Always add citations to the sentence/point/paragraph using the id of the provided content.
|
||||
The citation should follow this format: [citation:id] where id is the id of the content.
|
||||
|
||||
E.g:
|
||||
If we have a context like this:
|
||||
<Citation id='abc-xyz'>
|
||||
Baby llama is called cria
|
||||
</Citation id='abc-xyz'>
|
||||
|
||||
And your answer uses the content, then the citation should be:
|
||||
- Baby llama is called cria [citation:abc-xyz]
|
||||
|
||||
Here is the provided context for the question:
|
||||
<Collected information>
|
||||
{context_str}
|
||||
</Collected information>
|
||||
|
||||
No prior knowledge, just use the provided context to answer the question: {question}
|
||||
`,
|
||||
templateVars: ["context_str", "question"],
|
||||
});
|
||||
|
||||
const WRITE_REPORT_PROMPT = `
|
||||
You are a researcher writing a report based on a user request and the research context.
|
||||
You have researched various perspectives related to the user request.
|
||||
The report should provide a comprehensive outline covering all important points from the researched perspectives.
|
||||
Create a well-structured outline for the research report that covers all the answers.
|
||||
|
||||
# IMPORTANT when writing in markdown format:
|
||||
+ Use tables or figures where appropriate to enhance presentation.
|
||||
+ Preserve all citation syntax (the \`[citation:id]()\` parts in the provided context). Keep these citations in the final report - no separate reference section is needed.
|
||||
+ Do not add links, a table of contents, or a references section to the report.
|
||||
`;
|
||||
|
||||
// workflow events
|
||||
type ResearchQuestion = { questionId: string; question: string };
|
||||
type ResearchResult = ResearchQuestion & { answer: string };
|
||||
|
||||
class PlanResearchEvent extends WorkflowEvent<{}> {}
|
||||
class ResearchEvent extends WorkflowEvent<ResearchQuestion[]> {}
|
||||
class ReportEvent extends WorkflowEvent<{}> {}
|
||||
class StopEvent extends StopEventBase<AsyncGenerator<ChatResponseChunk>> {}
|
||||
|
||||
export const UIEventSchema = z
|
||||
.object({
|
||||
event: z
|
||||
.enum(["retrieve", "analyze", "answer"])
|
||||
.describe(
|
||||
"The type of event. DeepResearch has 3 main stages:\n1. retrieve: Retrieve the context from the vector store\n2. analyze: Analyze the context and generate a research questions to answer\n3. answer: Answer the provided questions. Each question has a unique id, when the state is done, the event will have the answer for the question.",
|
||||
),
|
||||
state: z
|
||||
.enum(["pending", "inprogress", "done", "error"])
|
||||
.describe("The state for each event"),
|
||||
id: z.string().optional().describe("The id of the question"),
|
||||
question: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The question generated by the LLM"),
|
||||
answer: z.string().optional().describe("The answer generated by the LLM"),
|
||||
})
|
||||
.describe("DeepResearchEvent");
|
||||
|
||||
type UIEventData = z.infer<typeof UIEventSchema>;
|
||||
|
||||
class UIEvent extends WorkflowEvent<{
|
||||
type: "ui_event";
|
||||
data: UIEventData;
|
||||
}> {}
|
||||
|
||||
// workflow definition
|
||||
class DeepResearchWorkflow extends Workflow<
|
||||
AgentWorkflowContext,
|
||||
AgentInputData,
|
||||
string
|
||||
> {
|
||||
#llm = Settings.llm as ToolCallLLM;
|
||||
#index?: VectorStoreIndex | LlamaCloudIndex;
|
||||
|
||||
userRequest: string = "";
|
||||
totalQuestions: number = 0;
|
||||
contextNodes: NodeWithScore<Metadata>[] = [];
|
||||
memory: ChatMemoryBuffer = new ChatMemoryBuffer({ llm: Settings.llm });
|
||||
|
||||
constructor(index: VectorStoreIndex | LlamaCloudIndex) {
|
||||
super({ timeout: TIMEOUT });
|
||||
this.#index = index;
|
||||
this.addWorkflowSteps();
|
||||
}
|
||||
|
||||
addWorkflowSteps() {
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [StartEvent<AgentInputData>],
|
||||
outputs: [PlanResearchEvent],
|
||||
},
|
||||
this.handleStartWorkflow,
|
||||
);
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [PlanResearchEvent],
|
||||
outputs: [ResearchEvent, ReportEvent, StopEvent],
|
||||
},
|
||||
this.handlePlanResearch,
|
||||
);
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [ResearchEvent],
|
||||
outputs: [PlanResearchEvent],
|
||||
},
|
||||
this.handleResearch,
|
||||
);
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [ReportEvent],
|
||||
outputs: [StopEvent],
|
||||
},
|
||||
this.handleReport,
|
||||
);
|
||||
}
|
||||
|
||||
async initWorkflow(data: AgentInputData) {
|
||||
const { userInput, chatHistory = [] } = data;
|
||||
if (!userInput) throw new Error("Invalid input");
|
||||
|
||||
this.userRequest = userInput;
|
||||
|
||||
await this.memory.set(chatHistory);
|
||||
await this.memory.put({ role: "user", content: userInput });
|
||||
}
|
||||
|
||||
handleStartWorkflow = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ev: StartEvent<AgentInputData>,
|
||||
): Promise<PlanResearchEvent> => {
|
||||
await this.initWorkflow(ev.data);
|
||||
|
||||
ctx.sendEvent(
|
||||
new UIEvent({
|
||||
type: "ui_event",
|
||||
data: { event: "retrieve", state: "inprogress" },
|
||||
}),
|
||||
);
|
||||
|
||||
const retrievedNodes = await this.retriever.retrieve(this.userRequest);
|
||||
|
||||
ctx.sendEvent(toSourceEvent(retrievedNodes));
|
||||
|
||||
ctx.sendEvent(
|
||||
new UIEvent({
|
||||
type: "ui_event",
|
||||
data: { event: "retrieve", state: "done" },
|
||||
}),
|
||||
);
|
||||
|
||||
this.contextNodes = retrievedNodes;
|
||||
|
||||
return new PlanResearchEvent({});
|
||||
};
|
||||
|
||||
handlePlanResearch = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ev: PlanResearchEvent,
|
||||
): Promise<ResearchEvent | ReportEvent | StopEvent> => {
|
||||
ctx.sendEvent(
|
||||
new UIEvent({
|
||||
type: "ui_event",
|
||||
data: { event: "analyze", state: "inprogress" },
|
||||
}),
|
||||
);
|
||||
|
||||
const { decision, researchQuestions, cancelReason } =
|
||||
await this.createResearchPlan();
|
||||
|
||||
// Stop workflow due to decision from LLM
|
||||
if (decision === "cancel") {
|
||||
ctx.sendEvent(
|
||||
new UIEvent({
|
||||
type: "ui_event",
|
||||
data: { event: "analyze", state: "done" },
|
||||
}),
|
||||
);
|
||||
return new StopEvent(
|
||||
toStreamGenerator(
|
||||
cancelReason ?? "Research cancelled without any reason.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Trigger research from generated questions
|
||||
if (decision === "research") {
|
||||
this.memory.put({
|
||||
role: "assistant",
|
||||
content:
|
||||
"We need to find answers to the following questions:\n" +
|
||||
researchQuestions.join("\n"),
|
||||
});
|
||||
|
||||
researchQuestions.forEach(({ questionId: id, question }) => {
|
||||
ctx.sendEvent(
|
||||
new UIEvent({
|
||||
type: "ui_event",
|
||||
data: { event: "answer", state: "pending", id, question },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return new ResearchEvent(researchQuestions);
|
||||
}
|
||||
|
||||
// Resarch done, start writing report
|
||||
this.memory.put({
|
||||
role: "assistant",
|
||||
content: "No more idea to analyze. We should report the answers.",
|
||||
});
|
||||
|
||||
ctx.sendEvent(
|
||||
new UIEvent({
|
||||
type: "ui_event",
|
||||
data: { event: "analyze", state: "done" },
|
||||
}),
|
||||
);
|
||||
|
||||
return new ReportEvent({});
|
||||
};
|
||||
|
||||
handleResearch = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ev: ResearchEvent,
|
||||
): Promise<PlanResearchEvent> => {
|
||||
const researchQuestions = ev.data;
|
||||
|
||||
// Answer questions in parallel
|
||||
const researchResults: ResearchResult[] = await Promise.all(
|
||||
researchQuestions.map(async ({ questionId: id, question }) => {
|
||||
ctx.sendEvent(
|
||||
new UIEvent({
|
||||
type: "ui_event",
|
||||
data: { event: "answer", state: "inprogress", id, question },
|
||||
}),
|
||||
);
|
||||
|
||||
const answer = await this.answerQuestion(question);
|
||||
|
||||
ctx.sendEvent(
|
||||
new UIEvent({
|
||||
type: "ui_event",
|
||||
data: { event: "answer", state: "done", id, question, answer },
|
||||
}),
|
||||
);
|
||||
|
||||
return { questionId: id, question, answer };
|
||||
}),
|
||||
);
|
||||
|
||||
// Save answers to memory
|
||||
researchResults.forEach(({ question, answer }) => {
|
||||
this.memory.put({
|
||||
role: "assistant",
|
||||
content: `<Question>${question}</Question>\n<Answer>${answer}</Answer>`,
|
||||
});
|
||||
});
|
||||
|
||||
this.memory.put({
|
||||
role: "assistant",
|
||||
content:
|
||||
"Researched all the questions. Now, I need to analyze if it's ready to write a report or need to research more.",
|
||||
});
|
||||
|
||||
this.totalQuestions += researchResults.length;
|
||||
|
||||
return new PlanResearchEvent({});
|
||||
};
|
||||
|
||||
handleReport = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ev: ReportEvent,
|
||||
): Promise<StopEvent> => {
|
||||
const chatHistory = await this.memory.getAllMessages();
|
||||
|
||||
const messages = chatHistory.concat([
|
||||
{
|
||||
role: "system",
|
||||
content: WRITE_REPORT_PROMPT,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
"Write a report addressing the user request based on the research provided the context",
|
||||
},
|
||||
]);
|
||||
|
||||
const stream = await this.llm.chat({ messages, stream: true });
|
||||
|
||||
return new StopEvent(toStreamGenerator(stream));
|
||||
};
|
||||
|
||||
get llm() {
|
||||
if (!this.#llm.supportToolCall) throw new Error("LLM is not a ToolCallLLM");
|
||||
return this.#llm;
|
||||
}
|
||||
|
||||
get retriever() {
|
||||
if (!this.#index) throw new Error("Index is not initialized");
|
||||
return this.#index.asRetriever({ similarityTopK: TOP_K });
|
||||
}
|
||||
|
||||
get contextStr() {
|
||||
return this.contextNodes
|
||||
.map((node) => {
|
||||
const nodeId = node.node.id_;
|
||||
const nodeContent = node.node.getContent(MetadataMode.NONE);
|
||||
return `<Citation id='${nodeId}'>\n${nodeContent}</Citation id='${nodeId}'>`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
get enhancedPrompt() {
|
||||
if (this.totalQuestions === 0) {
|
||||
return "The student has no questions to research. Let start by asking some questions.";
|
||||
}
|
||||
|
||||
if (this.totalQuestions > MAX_QUESTIONS) {
|
||||
return `The student has researched ${this.totalQuestions} questions. Should cancel the research if the context is not enough to write a report.`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
async createResearchPlan() {
|
||||
const chatHistory = await this.memory.getMessages();
|
||||
|
||||
const conversationContext = chatHistory
|
||||
.map((message) => `${message.role}: ${message.content}`)
|
||||
.join("\n");
|
||||
|
||||
const prompt = createPlanResearchPrompt.format({
|
||||
context_str: this.contextStr,
|
||||
conversation_context: conversationContext,
|
||||
enhanced_prompt: this.enhancedPrompt,
|
||||
user_request: this.userRequest,
|
||||
});
|
||||
|
||||
const responseFormat = z.object({
|
||||
decision: z.enum(["research", "write", "cancel"]),
|
||||
researchQuestions: z.array(z.string()),
|
||||
cancelReason: z.string().optional(),
|
||||
});
|
||||
|
||||
const result = await this.llm.complete({ prompt, responseFormat });
|
||||
const plan = JSON.parse(result.text) as z.infer<typeof responseFormat>;
|
||||
|
||||
return {
|
||||
...plan,
|
||||
researchQuestions: plan.researchQuestions.map((question) => ({
|
||||
questionId: randomUUID(),
|
||||
question,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async answerQuestion(question: string) {
|
||||
const prompt = researchPrompt.format({
|
||||
context_str: this.contextStr,
|
||||
question,
|
||||
});
|
||||
const result = await this.llm.complete({ prompt });
|
||||
return result.text;
|
||||
}
|
||||
}
|
||||
-396
@@ -1,396 +0,0 @@
|
||||
import { toAgentRunEvent, toSourceEvent } from "@llamaindex/server";
|
||||
import {
|
||||
callTools,
|
||||
chatWithTools,
|
||||
documentGenerator,
|
||||
interpreter,
|
||||
} from "@llamaindex/tools";
|
||||
import {
|
||||
AgentInputData,
|
||||
AgentWorkflowContext,
|
||||
BaseToolWithCall,
|
||||
ChatMemoryBuffer,
|
||||
ChatMessage,
|
||||
ChatResponseChunk,
|
||||
HandlerContext,
|
||||
Metadata,
|
||||
NodeWithScore,
|
||||
Settings,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
ToolCall,
|
||||
ToolCallLLM,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "llamaindex";
|
||||
import { getIndex } from "./data";
|
||||
|
||||
const TIMEOUT = 360 * 1000;
|
||||
|
||||
export async function workflowFactory(reqBody: any) {
|
||||
const index = await getIndex(reqBody?.data);
|
||||
|
||||
const queryEngineTool = index.queryTool({
|
||||
metadata: {
|
||||
name: "query_document",
|
||||
description: `This tool can retrieve information about Apple and Tesla financial data`,
|
||||
},
|
||||
includeSourceNodes: true,
|
||||
});
|
||||
|
||||
if (!process.env.E2B_API_KEY) {
|
||||
throw new Error("E2B_API_KEY is required to use the code interpreter tool");
|
||||
}
|
||||
|
||||
const codeInterpreterTool = interpreter({
|
||||
apiKey: process.env.E2B_API_KEY!,
|
||||
});
|
||||
const documentGeneratorTool = documentGenerator();
|
||||
|
||||
return new FinancialReportWorkflow({
|
||||
queryEngineTool,
|
||||
codeInterpreterTool,
|
||||
documentGeneratorTool,
|
||||
timeout: TIMEOUT,
|
||||
});
|
||||
}
|
||||
|
||||
// Create a custom event type
|
||||
class InputEvent extends WorkflowEvent<{ input: ChatMessage[] }> {}
|
||||
|
||||
class ResearchEvent extends WorkflowEvent<{
|
||||
toolCalls: ToolCall[];
|
||||
}> {}
|
||||
|
||||
class AnalyzeEvent extends WorkflowEvent<{
|
||||
input: ChatMessage | ToolCall[];
|
||||
}> {}
|
||||
|
||||
class ReportGenerationEvent extends WorkflowEvent<{
|
||||
toolCalls: ToolCall[];
|
||||
}> {}
|
||||
|
||||
const DEFAULT_SYSTEM_PROMPT = `
|
||||
You are a financial analyst who are given a set of tools to help you.
|
||||
It's good to using appropriate tools for the user request and always use the information from the tools, don't make up anything yourself.
|
||||
For the query engine tool, you should break down the user request into a list of queries and call the tool with the queries.
|
||||
`;
|
||||
|
||||
class FinancialReportWorkflow extends Workflow<
|
||||
AgentWorkflowContext,
|
||||
AgentInputData,
|
||||
string
|
||||
> {
|
||||
llm: ToolCallLLM;
|
||||
memory: ChatMemoryBuffer;
|
||||
queryEngineTool: BaseToolWithCall;
|
||||
codeInterpreterTool: BaseToolWithCall;
|
||||
documentGeneratorTool: BaseToolWithCall;
|
||||
systemPrompt?: string;
|
||||
|
||||
constructor(options: {
|
||||
queryEngineTool: BaseToolWithCall;
|
||||
codeInterpreterTool: BaseToolWithCall;
|
||||
documentGeneratorTool: BaseToolWithCall;
|
||||
systemPrompt?: string;
|
||||
verbose?: boolean;
|
||||
timeout?: number;
|
||||
}) {
|
||||
super({
|
||||
verbose: options?.verbose ?? false,
|
||||
timeout: options?.timeout ?? 360,
|
||||
});
|
||||
|
||||
this.llm = Settings.llm as ToolCallLLM;
|
||||
if (!this.llm.supportToolCall) {
|
||||
throw new Error("LLM is not a ToolCallLLM");
|
||||
}
|
||||
this.systemPrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
||||
this.queryEngineTool = options.queryEngineTool;
|
||||
this.codeInterpreterTool = options.codeInterpreterTool;
|
||||
|
||||
this.documentGeneratorTool = options.documentGeneratorTool;
|
||||
this.memory = new ChatMemoryBuffer({ llm: this.llm, chatHistory: [] });
|
||||
|
||||
// Add steps
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [StartEvent<AgentInputData>],
|
||||
outputs: [InputEvent],
|
||||
},
|
||||
this.prepareChatHistory,
|
||||
);
|
||||
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [InputEvent],
|
||||
outputs: [
|
||||
InputEvent,
|
||||
ResearchEvent,
|
||||
AnalyzeEvent,
|
||||
ReportGenerationEvent,
|
||||
StopEvent,
|
||||
],
|
||||
},
|
||||
this.handleLLMInput,
|
||||
);
|
||||
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [ResearchEvent],
|
||||
outputs: [AnalyzeEvent],
|
||||
},
|
||||
this.handleResearch,
|
||||
);
|
||||
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [AnalyzeEvent],
|
||||
outputs: [InputEvent],
|
||||
},
|
||||
this.handleAnalyze,
|
||||
);
|
||||
|
||||
this.addStep(
|
||||
{
|
||||
inputs: [ReportGenerationEvent],
|
||||
outputs: [InputEvent],
|
||||
},
|
||||
this.handleReportGeneration,
|
||||
);
|
||||
}
|
||||
|
||||
prepareChatHistory = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ev: StartEvent<AgentInputData>,
|
||||
): Promise<InputEvent> => {
|
||||
const { userInput, chatHistory = [] } = ev.data;
|
||||
if (!userInput) throw new Error("Invalid input");
|
||||
|
||||
this.memory.set(chatHistory);
|
||||
|
||||
if (this.systemPrompt) {
|
||||
this.memory.put({ role: "system", content: this.systemPrompt });
|
||||
}
|
||||
|
||||
this.memory.put({ role: "user", content: userInput });
|
||||
|
||||
const messages = await this.memory.getMessages();
|
||||
return new InputEvent({ input: messages });
|
||||
};
|
||||
|
||||
handleLLMInput = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ev: InputEvent,
|
||||
): Promise<
|
||||
| InputEvent
|
||||
| ResearchEvent
|
||||
| AnalyzeEvent
|
||||
| ReportGenerationEvent
|
||||
| StopEvent<AsyncGenerator<ChatResponseChunk, any, any> | undefined>
|
||||
> => {
|
||||
const chatHistory = ev.data.input;
|
||||
|
||||
const tools = [
|
||||
this.codeInterpreterTool,
|
||||
this.documentGeneratorTool,
|
||||
this.queryEngineTool,
|
||||
];
|
||||
|
||||
const toolCallResponse = await chatWithTools(this.llm, tools, chatHistory);
|
||||
|
||||
if (!toolCallResponse.hasToolCall()) {
|
||||
return new StopEvent(toolCallResponse.responseGenerator);
|
||||
}
|
||||
|
||||
if (toolCallResponse.hasMultipleTools()) {
|
||||
this.memory.put({
|
||||
role: "assistant",
|
||||
content:
|
||||
"Calling different tools is not allowed. Please only use multiple calls of the same tool.",
|
||||
});
|
||||
const chatHistory = await this.memory.getMessages();
|
||||
return new InputEvent({ input: chatHistory });
|
||||
}
|
||||
|
||||
// Put the LLM tool call message into the memory
|
||||
// And trigger the next step according to the tool call
|
||||
if (toolCallResponse.toolCallMessage) {
|
||||
this.memory.put(toolCallResponse.toolCallMessage);
|
||||
}
|
||||
const toolName = toolCallResponse.getToolNames()[0];
|
||||
switch (toolName) {
|
||||
case this.codeInterpreterTool.metadata.name:
|
||||
return new AnalyzeEvent({
|
||||
input: toolCallResponse.toolCalls,
|
||||
});
|
||||
case this.documentGeneratorTool.metadata.name:
|
||||
return new ReportGenerationEvent({
|
||||
toolCalls: toolCallResponse.toolCalls,
|
||||
});
|
||||
default:
|
||||
if (this.queryEngineTool.metadata.name === toolName) {
|
||||
return new ResearchEvent({
|
||||
toolCalls: toolCallResponse.toolCalls,
|
||||
});
|
||||
}
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
};
|
||||
|
||||
handleResearch = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ev: ResearchEvent,
|
||||
): Promise<AnalyzeEvent> => {
|
||||
ctx.sendEvent(
|
||||
toAgentRunEvent({
|
||||
agent: "Researcher",
|
||||
text: "Researching data",
|
||||
type: "text",
|
||||
}),
|
||||
);
|
||||
|
||||
const { toolCalls } = ev.data;
|
||||
|
||||
const toolMsgs = await callTools({
|
||||
tools: [this.queryEngineTool],
|
||||
toolCalls,
|
||||
writeEvent: (text, step) => {
|
||||
ctx.sendEvent(
|
||||
toAgentRunEvent({
|
||||
agent: "Researcher",
|
||||
text,
|
||||
type: toolCalls.length > 1 ? "progress" : "text",
|
||||
current: step,
|
||||
total: toolCalls.length,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
for (const toolMsg of toolMsgs) {
|
||||
this.memory.put(toolMsg);
|
||||
}
|
||||
|
||||
const sourcesNodes: NodeWithScore<Metadata>[] = toolMsgs
|
||||
.map((msg) => (msg.options as any)?.toolResult?.result?.sourceNodes)
|
||||
.flat()
|
||||
.filter(Boolean);
|
||||
|
||||
if (sourcesNodes.length > 0) {
|
||||
ctx.sendEvent(toSourceEvent(sourcesNodes));
|
||||
}
|
||||
|
||||
return new AnalyzeEvent({
|
||||
input: {
|
||||
role: "assistant",
|
||||
content:
|
||||
"I have finished researching the data, please analyze the data.",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Analyze a research result or a tool call for code interpreter from the LLM
|
||||
*/
|
||||
handleAnalyze = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ev: AnalyzeEvent,
|
||||
): Promise<InputEvent> => {
|
||||
ctx.sendEvent(
|
||||
toAgentRunEvent({
|
||||
agent: "Analyst",
|
||||
text: "Analyzing data",
|
||||
type: "text",
|
||||
}),
|
||||
);
|
||||
// Request by workflow LLM, input is a list of tool calls
|
||||
let toolCalls: ToolCall[] = [];
|
||||
if (Array.isArray(ev.data.input)) {
|
||||
toolCalls = ev.data.input;
|
||||
} else {
|
||||
// Requested by Researcher, input is a ChatMessage
|
||||
// We start new LLM chat specifically for analyzing the data
|
||||
const analysisPrompt = `
|
||||
You are an expert in analyzing financial data.
|
||||
You are given a set of financial data to analyze. Your task is to analyze the financial data and return a report.
|
||||
Your response should include a detailed analysis of the financial data, including any trends, patterns, or insights that you find.
|
||||
Construct the analysis in textual format; including tables would be great!
|
||||
Don't need to synthesize the data, just analyze and provide your findings.
|
||||
`;
|
||||
|
||||
// Clone the current chat history
|
||||
// Add the analysis system prompt and the message from the researcher
|
||||
const chatHistory = await this.memory.getMessages();
|
||||
const newChatHistory = [
|
||||
...chatHistory,
|
||||
{ role: "system", content: analysisPrompt },
|
||||
ev.data.input,
|
||||
];
|
||||
const toolCallResponse = await chatWithTools(
|
||||
this.llm,
|
||||
[this.codeInterpreterTool],
|
||||
newChatHistory as ChatMessage[],
|
||||
);
|
||||
|
||||
if (!toolCallResponse.hasToolCall()) {
|
||||
this.memory.put(await toolCallResponse.asFullResponse());
|
||||
const chatHistory = await this.memory.getMessages();
|
||||
return new InputEvent({ input: chatHistory });
|
||||
} else {
|
||||
this.memory.put(toolCallResponse.toolCallMessage!);
|
||||
toolCalls = toolCallResponse.toolCalls;
|
||||
}
|
||||
}
|
||||
|
||||
// Call the tools
|
||||
const toolMsgs = await callTools({
|
||||
tools: [this.codeInterpreterTool],
|
||||
toolCalls,
|
||||
writeEvent: (text, step) => {
|
||||
ctx.sendEvent(
|
||||
toAgentRunEvent({
|
||||
agent: "Analyst",
|
||||
text,
|
||||
type: toolCalls.length > 1 ? "progress" : "text",
|
||||
current: step,
|
||||
total: toolCalls.length,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
for (const toolMsg of toolMsgs) {
|
||||
this.memory.put(toolMsg);
|
||||
}
|
||||
|
||||
const chatHistory = await this.memory.getMessages();
|
||||
return new InputEvent({ input: chatHistory });
|
||||
};
|
||||
|
||||
handleReportGeneration = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
ev: ReportGenerationEvent,
|
||||
): Promise<InputEvent> => {
|
||||
const { toolCalls } = ev.data;
|
||||
|
||||
const toolMsgs = await callTools({
|
||||
tools: [this.documentGeneratorTool],
|
||||
toolCalls,
|
||||
writeEvent: (text, step) => {
|
||||
ctx.sendEvent(
|
||||
toAgentRunEvent({
|
||||
agent: "Reporter",
|
||||
text,
|
||||
type: toolCalls.length > 1 ? "progress" : "text",
|
||||
current: step,
|
||||
total: toolCalls.length,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
for (const toolMsg of toolMsgs) {
|
||||
this.memory.put(toolMsg);
|
||||
}
|
||||
const chatHistory = await this.memory.getMessages();
|
||||
return new InputEvent({ input: chatHistory });
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
import os
|
||||
|
||||
from llama_index.core import Settings
|
||||
from llama_index.embeddings.openai import OpenAIEmbedding
|
||||
from llama_index.llms.openai import OpenAI
|
||||
|
||||
|
||||
def init_settings():
|
||||
if os.getenv("OPENAI_API_KEY") is None:
|
||||
raise RuntimeError("OPENAI_API_KEY is missing in environment variables")
|
||||
Settings.llm = OpenAI(model="gpt-4o-mini")
|
||||
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
|
||||
|
||||
@@ -51,7 +51,7 @@ def generate_ui_for_workflow():
|
||||
# and run the generate_ui_for_workflow function with the imported model.
|
||||
# Make sure the output filename of the generated UI component matches the event type (here `ui_event`)
|
||||
try:
|
||||
from app.workflow import UIEventData
|
||||
from app.workflow import UIEventData # type: ignore
|
||||
except ImportError:
|
||||
raise ImportError("Couldn't generate UI component for the current workflow.")
|
||||
from llama_index.server.gen_ui import generate_event_component
|
||||
|
||||
@@ -16,9 +16,10 @@ def create_app():
|
||||
workflow_factory=create_workflow, # A factory function that creates a new workflow for each request
|
||||
ui_config=UIConfig(
|
||||
component_dir=COMPONENT_DIR,
|
||||
app_title="Chat App",
|
||||
dev_mode=True, # Please disable this in production
|
||||
),
|
||||
logger=logger,
|
||||
env="dev",
|
||||
)
|
||||
# You can also add custom FastAPI routes to app
|
||||
app.add_api_route("/api/health", lambda: {"message": "OK"}, status_code=200)
|
||||
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"pydantic<2.10",
|
||||
"aiostream>=0.5.2,<0.6.0",
|
||||
"llama-index-core>=0.12.28,<0.13.0",
|
||||
"llama-index-server>=0.1.15,<0.2.0",
|
||||
"llama-index-server>=0.1.16,<0.2.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"watch": ["src/**/*.ts"],
|
||||
"exec": "nodemon --exec tsx src/index.ts",
|
||||
"ext": "js ts",
|
||||
"ignore": ["src/app/workflow_*.ts"]
|
||||
}
|
||||
@@ -5,21 +5,22 @@
|
||||
"generate": "tsx src/generate.ts datasource",
|
||||
"generate:datasource": "tsx src/generate.ts datasource",
|
||||
"generate:ui": "tsx src/generate.ts ui",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"dev": "nodemon",
|
||||
"start": "tsx src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@llamaindex/openai": "0.2.0",
|
||||
"@llamaindex/readers": "^2.0.0",
|
||||
"@llamaindex/server": "0.1.5",
|
||||
"@llamaindex/tools": "0.0.4",
|
||||
"@llamaindex/openai": "~0.4.0",
|
||||
"@llamaindex/server": "~0.2.1",
|
||||
"@llamaindex/workflow": "~1.1.3",
|
||||
"@llamaindex/tools": "~0.0.11",
|
||||
"llamaindex": "~0.11.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"zod": "^3.23.8",
|
||||
"llamaindex": "0.10.2"
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.3",
|
||||
"tsx": "^4.7.2",
|
||||
"typescript": "^5.3.2"
|
||||
"typescript": "^5.3.2",
|
||||
"nodemon": "^3.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Settings } from "llamaindex";
|
||||
|
||||
export function initSettings() {
|
||||
Settings.llm = new OpenAI({
|
||||
model: "gpt-4o-mini",
|
||||
model: "gpt-4.1",
|
||||
});
|
||||
Settings.embedModel = new OpenAIEmbedding({
|
||||
model: "text-embedding-3-small",
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import "dotenv/config";
|
||||
import { SimpleDirectoryReader } from "@llamaindex/readers/directory";
|
||||
import {
|
||||
OpenAI,
|
||||
storageContextFromDefaults,
|
||||
VectorStoreIndex,
|
||||
} from "llamaindex";
|
||||
import { storageContextFromDefaults, VectorStoreIndex } from "llamaindex";
|
||||
import { initSettings } from "./app/settings";
|
||||
import fs from "fs";
|
||||
import { generateEventComponent } from "@llamaindex/server";
|
||||
import { UIEventSchema } from "./app/workflow";
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
|
||||
async function generateDatasource() {
|
||||
console.log(`Generating storage context...`);
|
||||
@@ -30,6 +26,14 @@ async function generateUi() {
|
||||
// Also works well with Claude 3.5 Sonnet and Google Gemini 2.5 Pro
|
||||
const llm = new OpenAI({ model: "gpt-4.1" });
|
||||
|
||||
const workflowModule = await import("./app/workflow");
|
||||
const UIEventSchema = (workflowModule as any).UIEventSchema;
|
||||
if (!UIEventSchema) {
|
||||
throw new Error(
|
||||
"To generate the UI, you must define a UIEventSchema in your workflow.",
|
||||
);
|
||||
}
|
||||
|
||||
// You can also generate for other workflow events
|
||||
const generatedCode = await generateEventComponent(UIEventSchema, llm);
|
||||
// Write the generated code to components/ui_event.ts
|
||||
|
||||
@@ -8,7 +8,7 @@ initSettings();
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
uiConfig: {
|
||||
appTitle: "LlamaIndex App",
|
||||
componentsDir: "components",
|
||||
devMode: true,
|
||||
},
|
||||
}).start();
|
||||
|
||||
@@ -9,6 +9,7 @@ readme = "README.md"
|
||||
requires-python = ">=3.11,<3.14"
|
||||
dependencies = [
|
||||
"llama-index>=0.12.1",
|
||||
"llama-parse>=0.6.21,<0.7.0",
|
||||
"fastapi[standard]>=0.109.1",
|
||||
"uvicorn>=0.23.2",
|
||||
"python-dotenv>=1.0.0",
|
||||
|
||||
@@ -1 +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,26 @@
|
||||
# @llamaindex/server
|
||||
|
||||
## 0.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- eee3230: feat: support custom layout
|
||||
- 0bc5a0d: Add suggestNextQuestions config
|
||||
- 3acec88: chore: bump chat-ui
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 25fba43: refactor: migrate to Nextjs Route Handler
|
||||
- 6f75d4a: fix: unsupported language in code gen workflow
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f072308: feat: add dev mode UI
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
+152
-8
@@ -4,10 +4,10 @@ LlamaIndexServer is a Next.js-based application that allows you to quickly launc
|
||||
|
||||
## Features
|
||||
|
||||
- Serving a workflow as a chatbot
|
||||
- Add a sophisticated chatbot UI to your LlamaIndex workflow
|
||||
- Edit code and document artifacts in an OpenAI Canvas-style UI
|
||||
- Extendable UI components for events and headers
|
||||
- Built on Next.js for high performance and easy API development
|
||||
- Optional built-in chat UI with extendable UI components
|
||||
- Prebuilt development code
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -21,19 +21,22 @@ Create an `index.ts` file and add the following code:
|
||||
|
||||
```ts
|
||||
import { LlamaIndexServer } from "@llamaindex/server";
|
||||
import { openai } from "@llamaindex/openai";
|
||||
import { agent } from "@llamaindex/workflow";
|
||||
import { wiki } from "@llamaindex/tools"; // or any other tool
|
||||
|
||||
const createWorkflow = () => agent({ tools: [wiki()] });
|
||||
const createWorkflow = () => agent({ tools: [wiki()], llm: openai("gpt-4o") });
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: createWorkflow,
|
||||
uiConfig: {
|
||||
appTitle: "LlamaIndex App",
|
||||
starterQuestions: ["Who is the first president of the United States?"],
|
||||
},
|
||||
}).start();
|
||||
```
|
||||
|
||||
The `createWorkflow` function is a factory function that creates an [Agent Workflow](https://ts.llamaindex.ai/docs/llamaindex/modules/agents/agent_workflow) with a tool that retrieves information from Wikipedia in this case. For more details, read about the [Workflow factory contract](#workflow-factory-contract).
|
||||
|
||||
## Running the Server
|
||||
|
||||
In the same directory as `index.ts`, run the following command to start the server:
|
||||
@@ -54,16 +57,76 @@ curl -X POST "http://localhost:3000/api/chat" -H "Content-Type: application/json
|
||||
|
||||
The `LlamaIndexServer` accepts the following configuration options:
|
||||
|
||||
- `workflow`: A callable function that creates a workflow instance for each request
|
||||
- `workflow`: A callable function that creates a workflow instance for each request. See [Workflow factory contract](#workflow-factory-contract) for more details.
|
||||
- `uiConfig`: An object to configure the chat UI containing the following properties:
|
||||
- `appTitle`: The title of the application (default: `"LlamaIndex App"`)
|
||||
- `starterQuestions`: List of starter questions for the chat UI (default: `[]`)
|
||||
- `componentsDir`: The directory for custom UI components rendering events emitted by the workflow. The default is undefined, which does not render custom UI components.
|
||||
- `layoutDir`: The directory for custom layout sections. The default value is `layout`. See [Custom Layout](#custom-layout) for more details.
|
||||
- `llamaCloudIndexSelector`: Whether to show the LlamaCloud index selector in the chat UI (requires `LLAMA_CLOUD_API_KEY` to be set in the environment variables) (default: `false`)
|
||||
- `dev_mode`: When enabled, you can update workflow code in the UI and see the changes immediately. It's currently in beta and only supports updating workflow code at `app/src/workflow.ts`. Please start server in dev mode (`npm run dev`) to use see this reload feature enabled.
|
||||
- `suggestNextQuestions`: Whether to suggest next questions after the assistant's response (default: `true`). You can change the prompt for the next questions by setting the `NEXT_QUESTION_PROMPT` environment variable.
|
||||
|
||||
LlamaIndexServer accepts all the configuration options from Nextjs Custom Server such as `port`, `hostname`, `dev`, etc.
|
||||
See all Nextjs Custom Server options [here](https://nextjs.org/docs/app/building-your-application/configuring/custom-server).
|
||||
|
||||
## Workflow factory contract
|
||||
|
||||
The `workflow` provided will be called for each chat request to initialize a new workflow instance. The contract of the generated workflow must be the same as for the [Agent Workflow](https://ts.llamaindex.ai/docs/llamaindex/modules/agents/agent_workflow).
|
||||
|
||||
This means that the workflow must handle a `startAgentEvent` event, which is the entry point of the workflow and contains the following information in it's `data` property:
|
||||
|
||||
```typescript
|
||||
{
|
||||
userInput: MessageContent;
|
||||
chatHistory?: ChatMessage[] | undefined;
|
||||
};
|
||||
```
|
||||
|
||||
The `userInput` is the latest user message and the `chatHistory` is the list of messages exchanged between the user and the workflow so far.
|
||||
|
||||
Furthermore, the workflow must stop with a `stopAgentEvent` event to mark the end of the workflow. In between, the workflow can emit [UI events](##AI-generated-UI-Components) to render custom UI components and [Artifact events](##Sending-Artifacts-to-the-UI) to send structured data like generated documents or code snippets to the UI.
|
||||
|
||||
```ts
|
||||
import {
|
||||
createStatefulMiddleware,
|
||||
createWorkflow,
|
||||
startAgentEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
import { ChatMemoryBuffer, type ChatMessage, Settings } from "llamaindex";
|
||||
import { openai } from "@llamaindex/openai";
|
||||
import { wiki } from "@llamaindex/tools";
|
||||
|
||||
Settings.llm = openai("gpt-4o");
|
||||
|
||||
export const workflowFactory = async () => {
|
||||
const workflow = createWorkflow();
|
||||
|
||||
workflow.handle([startAgentEvent], async ({ data }) => {
|
||||
const { state, sendEvent } = getContext();
|
||||
const messages = data.chatHistory;
|
||||
|
||||
const toolCallResponse = await chatWithTools(
|
||||
Settings.llm,
|
||||
[wiki()],
|
||||
messages,
|
||||
);
|
||||
|
||||
// using result from tool call and use `sendEvent` to emit the next event...
|
||||
});
|
||||
|
||||
// define more workflow handling logic here...
|
||||
|
||||
// Finally stop with a `stopAgentEvent` event to mark the end of the workflow.
|
||||
// return stopAgentEvent.with({
|
||||
// result: "This is the end!",
|
||||
// });
|
||||
|
||||
return workflow;
|
||||
};
|
||||
```
|
||||
|
||||
To generate sophisticated examples of workflows, you best use the [create-llama](https://github.com/run-llama/create-llama) project.
|
||||
|
||||
## AI-generated UI Components
|
||||
|
||||
The LlamaIndex server provides support for rendering workflow events using custom UI components, allowing you to extend and customize the chat interface.
|
||||
@@ -123,6 +186,28 @@ Feel free to modify the generated code to match your needs. If you're not satisf
|
||||
|
||||
> Note that `generateEventComponent` is generating JSX code, but you can also provide a TSX file.
|
||||
|
||||
## Custom Layout
|
||||
|
||||
LlamaIndex Server supports custom layout for header and footer. To use custom layout, you need to initialize the LlamaIndex server with the `layoutDir` that contains your custom layout files.
|
||||
|
||||
```ts
|
||||
new LlamaIndexServer({
|
||||
workflow: createWorkflow,
|
||||
uiConfig: {
|
||||
layoutDir: "layout",
|
||||
},
|
||||
}).start();
|
||||
```
|
||||
|
||||
```
|
||||
layout/
|
||||
header.tsx
|
||||
footer.tsx
|
||||
```
|
||||
|
||||
We currently support custom header and footer for the chat interface. The syntax for these files is the same as events components in components directory.
|
||||
Note that by default, we are still rendering the default LlamaIndex Header. It's also the fallback when having errors rendering the custom header. Example layout files will be generated in the `layout` directory of your project when creating a new project with `create-llama`.
|
||||
|
||||
### Server Setup
|
||||
|
||||
To use the generated UI components, you need to initialize the LlamaIndex server with the `componentsDir` that contains your custom UI components:
|
||||
@@ -131,12 +216,71 @@ To use the generated UI components, you need to initialize the LlamaIndex server
|
||||
new LlamaIndexServer({
|
||||
workflow: createWorkflow,
|
||||
uiConfig: {
|
||||
appTitle: "LlamaIndex App",
|
||||
componentsDir: "components",
|
||||
},
|
||||
}).start();
|
||||
```
|
||||
|
||||
## Sending Artifacts to the UI
|
||||
|
||||
In addition to UI events for custom components, LlamaIndex Server supports a special `ArtifactEvent` to send structured data like generated documents or code snippets to the UI. These artifacts are displayed in a dedicated "Canvas" panel in the chat interface.
|
||||
|
||||
### Artifact Event Structure
|
||||
|
||||
To send an artifact, your workflow needs to emit an event with `type: "artifact"`. The `data` payload of this event should include:
|
||||
|
||||
- `type`: A string indicating the type of artifact (e.g., `"document"`, `"code"`).
|
||||
- `created_at`: A timestamp (e.g., `Date.now()`) indicating when the artifact was created.
|
||||
- `data`: An object containing the specific details of the artifact. The structure of this object depends on the artifact `type`.
|
||||
|
||||
### Defining and Sending an ArtifactEvent
|
||||
|
||||
First, define your artifact event using `workflowEvent` from `@llamaindex/workflow`:
|
||||
|
||||
```typescript
|
||||
import { workflowEvent } from "@llamaindex/workflow";
|
||||
|
||||
// Example for a document artifact
|
||||
const artifactEvent = workflowEvent<{
|
||||
type: "artifact"; // Must be "artifact"
|
||||
data: {
|
||||
type: "document"; // Custom type for your artifact (e.g., "document", "code")
|
||||
created_at: number;
|
||||
data: {
|
||||
// Specific data for the document artifact type
|
||||
title: string;
|
||||
content: string;
|
||||
type: "markdown" | "html"; // document format
|
||||
};
|
||||
};
|
||||
}>();
|
||||
```
|
||||
|
||||
Then, within your workflow logic, use `sendEvent` (obtained from `getContext()`) to emit the event:
|
||||
|
||||
```typescript
|
||||
// Assuming 'sendEvent' is available in your workflow handler
|
||||
// and 'documentDetails' contains the content for the artifact.
|
||||
|
||||
sendEvent(
|
||||
artifactEvent.with({
|
||||
type: "artifact", // This top-level type must be "artifact"
|
||||
data: {
|
||||
type: "document", // This is your specific artifact type
|
||||
created_at: Date.now(),
|
||||
data: {
|
||||
title: "My Generated Document",
|
||||
content: "# Hello World
|
||||
This is a markdown document.",
|
||||
type: "markdown",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
This will send the artifact to the LlamaIndex Server UI, where it will be rendered in the [ChatCanvasPanel](/packages/server/next/app/components/ui/chat/canvas/panel.tsx) by a renderer depending on the artifact type. For type `document` this is using the [DocumentArtifactViewer](https://github.com/run-llama/chat-ui/blob/bacb75fc6edceacf742fba18632404a2483b5a81/packages/chat-ui/src/chat/canvas/artifacts/document.tsx#L17).
|
||||
|
||||
## Default Endpoints and Features
|
||||
|
||||
### Chat Endpoint
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# LlamaIndex Server Examples
|
||||
|
||||
This directory contains examples of how to use the LlamaIndex Server.
|
||||
|
||||
## Running the examples
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=your_openai_api_key
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
## Open browser at http://localhost:3000
|
||||
@@ -0,0 +1,38 @@
|
||||
import { OpenAI, OpenAIEmbedding } from "@llamaindex/openai";
|
||||
import { LlamaIndexServer } from "@llamaindex/server";
|
||||
import { agent } from "@llamaindex/workflow";
|
||||
import { Document, Settings, VectorStoreIndex } from "llamaindex";
|
||||
|
||||
Settings.llm = new OpenAI({
|
||||
model: "gpt-4o-mini",
|
||||
});
|
||||
|
||||
Settings.embedModel = new OpenAIEmbedding({
|
||||
model: "text-embedding-3-small",
|
||||
});
|
||||
|
||||
export const workflowFactory = async () => {
|
||||
const index = await VectorStoreIndex.fromDocuments([
|
||||
new Document({ text: "The dog is brown" }),
|
||||
new Document({ text: "The dog is yellow" }),
|
||||
]);
|
||||
|
||||
const queryEngineTool = index.queryTool({
|
||||
metadata: {
|
||||
name: "query_document",
|
||||
description: `This tool can retrieve information in documents`,
|
||||
},
|
||||
includeSourceNodes: true,
|
||||
});
|
||||
|
||||
return agent({ tools: [queryEngineTool] });
|
||||
};
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
suggestNextQuestions: true,
|
||||
uiConfig: {
|
||||
starterQuestions: ["What is the color of the dog?"],
|
||||
},
|
||||
port: 3000,
|
||||
}).start();
|
||||
@@ -0,0 +1,30 @@
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import { LlamaIndexServer } from "@llamaindex/server";
|
||||
import { agent } from "@llamaindex/workflow";
|
||||
import { Settings, tool } from "llamaindex";
|
||||
import { z } from "zod";
|
||||
|
||||
Settings.llm = new OpenAI({ model: "gpt-4o-mini" });
|
||||
|
||||
const weatherAgent = agent({
|
||||
tools: [
|
||||
tool({
|
||||
name: "weather",
|
||||
description: "Get the weather in a given city",
|
||||
parameters: z.object({ city: z.string() }),
|
||||
execute: ({ city }) => `The weather in ${city} is sunny`,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: () => weatherAgent,
|
||||
uiConfig: {
|
||||
starterQuestions: [
|
||||
"What is the weather in Tokyo?",
|
||||
"What is the weather in Ho Chi Minh City?",
|
||||
],
|
||||
layoutDir: "layout",
|
||||
},
|
||||
port: 3000,
|
||||
}).start();
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { Sparkles, Star } from "lucide-react";
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<div className="flex items-center justify-between px-4 pt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="size-4" />
|
||||
<h1 className="font-semibold">LlamaIndex App</h1>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href="https://www.llamaindex.ai/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
Built by LlamaIndex
|
||||
</a>
|
||||
<img
|
||||
className="h-[24px] w-[24px] rounded-sm"
|
||||
src="/llama.png"
|
||||
alt="Llama Logo"
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
href="https://github.com/run-llama/LlamaIndexTS"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:bg-accent flex items-center gap-2 rounded-md border border-gray-300 px-2 py-1 text-sm"
|
||||
>
|
||||
<Star className="size-4" />
|
||||
Star on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
This example shows how to use the dev mode of the server.
|
||||
|
||||
First, we need to set `devMode` to `true` in the `uiConfig` of the server.
|
||||
|
||||
```ts
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
uiConfig: {
|
||||
devMode: true,
|
||||
},
|
||||
port: 3000,
|
||||
}).start();
|
||||
```
|
||||
|
||||
Export OpenAI API key and start the server in dev mode.
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=<your-openai-api-key>
|
||||
npx nodemon --exec tsx index.ts --ignore src/app/workflow_*.ts
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import { LlamaIndexServer } from "@llamaindex/server";
|
||||
import { Settings } from "llamaindex";
|
||||
import { workflowFactory } from "./src/app/workflow";
|
||||
|
||||
Settings.llm = new OpenAI({ model: "gpt-4o-mini" });
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: workflowFactory,
|
||||
uiConfig: {
|
||||
devMode: true,
|
||||
starterQuestions: [
|
||||
"What is the weather in Tokyo?",
|
||||
"What is the weather in New York?",
|
||||
],
|
||||
},
|
||||
port: 3000,
|
||||
}).start();
|
||||
@@ -0,0 +1,16 @@
|
||||
import { agent } from "@llamaindex/workflow";
|
||||
import { tool } from "llamaindex";
|
||||
import { z } from "zod";
|
||||
|
||||
export const workflowFactory = async () => {
|
||||
return agent({
|
||||
tools: [
|
||||
tool({
|
||||
name: "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();
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "llamaindex-server-examples",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "nodemon --exec tsx simple-workflow/calculator.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@llamaindex/openai": "~0.4.0",
|
||||
"@llamaindex/readers": "~3.1.4",
|
||||
"@llamaindex/server": "workspace:*",
|
||||
"@llamaindex/tools": "~0.0.11",
|
||||
"@llamaindex/workflow": "~1.1.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"llamaindex": "~0.11.3",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.3",
|
||||
"nodemon": "^3.1.10",
|
||||
"tsx": "^4.7.2",
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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 calculatorAgent = agent({
|
||||
tools: [
|
||||
tool({
|
||||
name: "add",
|
||||
description: "Adds two numbers",
|
||||
parameters: z.object({ x: z.number(), y: z.number() }),
|
||||
execute: ({ x, y }) => x + y,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
new LlamaIndexServer({
|
||||
workflow: () => calculatorAgent,
|
||||
uiConfig: {
|
||||
starterQuestions: ["1 + 1", "2 + 2"],
|
||||
},
|
||||
port: 3000,
|
||||
}).start();
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["**/*"],
|
||||
"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,13 +6,14 @@ 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";
|
||||
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({
|
||||
@@ -31,18 +32,18 @@ export default function ChatSection() {
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-screen w-screen flex-col overflow-hidden">
|
||||
<ChatHeader />
|
||||
<ChatLayout>
|
||||
<ChatUI
|
||||
handler={handler}
|
||||
className="flex min-h-0 flex-1 flex-row justify-center gap-4 px-4 py-0"
|
||||
className="relative flex min-h-0 flex-1 flex-row justify-center gap-4 px-4 py-0"
|
||||
>
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<ChatSectionPanel />
|
||||
<ChatCanvasPanel />
|
||||
</ResizablePanelGroup>
|
||||
<DevModePanel />
|
||||
</ChatUI>
|
||||
</div>
|
||||
</ChatLayout>
|
||||
<ChatInjection />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
CodeEditor,
|
||||
fileExtensionToEditorLang,
|
||||
} from "@llamaindex/chat-ui/widgets";
|
||||
import { AlertCircle, Loader2 } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Button } from "../button";
|
||||
import { getConfig } from "../lib/utils";
|
||||
|
||||
const API_PATH = "/api/dev/files/workflow";
|
||||
const POLLING_TIMEOUT = 30_000; // 30 seconds
|
||||
|
||||
type WorkflowFile = {
|
||||
last_modified: number;
|
||||
file_path: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export function DevModePanel() {
|
||||
const devModeEnabled = getConfig("DEV_MODE");
|
||||
if (!devModeEnabled) return null;
|
||||
return <DevModePanelComp />;
|
||||
}
|
||||
|
||||
function DevModePanelComp() {
|
||||
const [devModeOpen, setDevModeOpen] = useState(false);
|
||||
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
const [fetchingError, setFetchingError] = useState<string | null>();
|
||||
const [workflowFile, setWorkflowFile] = useState<WorkflowFile | null>(null);
|
||||
|
||||
const [updatedCode, setUpdatedCode] = useState<string | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
|
||||
const [isPolling, setIsPolling] = useState(false);
|
||||
const [pollingError, setPollingError] = useState<string | null>(null);
|
||||
|
||||
async function fetchWorkflowCode() {
|
||||
try {
|
||||
setIsFetching(true);
|
||||
const response = await fetch(API_PATH);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.detail ?? "Unknown error");
|
||||
}
|
||||
|
||||
setWorkflowFile(data);
|
||||
setFetchingError(null);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
setFetchingError(errorMessage);
|
||||
console.warn("Error fetching workflow code:", error);
|
||||
} finally {
|
||||
setIsFetching(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function restartingWorkflow() {
|
||||
if (!workflowFile) return;
|
||||
|
||||
const initialLastModified = workflowFile.last_modified;
|
||||
setIsPolling(true);
|
||||
setPollingError(null);
|
||||
|
||||
const pollStartTime = Date.now();
|
||||
|
||||
// interval refetching the updated workflow code
|
||||
const poll = async () => {
|
||||
if (Date.now() - pollStartTime > POLLING_TIMEOUT) {
|
||||
setPollingError(
|
||||
`Server not responding after ${POLLING_TIMEOUT / 1000} seconds.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pollResponse = await fetch(API_PATH);
|
||||
const pollData = (await pollResponse.json()) as WorkflowFile;
|
||||
if (pollData.last_modified !== initialLastModified) {
|
||||
setWorkflowFile(pollData);
|
||||
setUpdatedCode(pollData.content);
|
||||
setIsPolling(false);
|
||||
setPollingError(null);
|
||||
setDevModeOpen(false);
|
||||
} else {
|
||||
setTimeout(poll, 2000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.info("Polling error", error);
|
||||
setTimeout(poll, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(poll, 2000);
|
||||
}
|
||||
|
||||
const handleResetCode = () => {
|
||||
setUpdatedCode(workflowFile?.content ?? null);
|
||||
setSaveError(null);
|
||||
};
|
||||
|
||||
const handleSaveCode = async () => {
|
||||
if (!workflowFile) return;
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
const response = await fetch(API_PATH, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: updatedCode,
|
||||
file_path: workflowFile.file_path,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.detail ?? "Unknown error");
|
||||
}
|
||||
setSaveError(null);
|
||||
await restartingWorkflow();
|
||||
} catch (error) {
|
||||
console.warn("Error saving workflow code:", error);
|
||||
setSaveError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Unknown error happened when saving workflow code",
|
||||
);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (devModeOpen) {
|
||||
fetchWorkflowCode();
|
||||
}
|
||||
}, [devModeOpen]);
|
||||
|
||||
const codeEditorLanguage = useMemo(() => {
|
||||
if (!workflowFile?.file_path) return undefined;
|
||||
return fileExtensionToEditorLang(
|
||||
workflowFile.file_path.split(".").pop() ?? "",
|
||||
);
|
||||
}, [workflowFile]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => setDevModeOpen(!devModeOpen)}
|
||||
className="fixed right-2 top-1/2 origin-right -translate-y-1/2 rotate-90 transform rounded-l-md shadow-md transition-transform hover:-translate-x-1"
|
||||
>
|
||||
Dev Mode
|
||||
</Button>
|
||||
|
||||
{isPolling && (
|
||||
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
{!pollingError && (
|
||||
<>
|
||||
<Loader2 className="mb-4 h-16 w-16 animate-spin text-white" />
|
||||
<p className="text-lg font-semibold text-white">
|
||||
Applying changes and restarting server...
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-slate-300">
|
||||
Please wait for a while then you can start chatting with the
|
||||
updated workflow.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{pollingError && (
|
||||
<div className="bg-destructive/20 text-destructive-foreground mt-4 max-w-md rounded-md p-4 text-center">
|
||||
<div className="mb-2 flex items-center justify-center gap-2">
|
||||
<AlertCircle className="shrink-0" size={16} />
|
||||
<h6 className="text-sm font-medium">Server Starting Error</h6>
|
||||
</div>
|
||||
<p className="text-sm">{pollingError}</p>
|
||||
|
||||
<p className="text-sm">
|
||||
Please reload the page and check server logs.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`border-border fixed right-0 top-0 z-10 h-full w-full border-l shadow-xl transition-all duration-300 ease-in-out ${
|
||||
devModeOpen ? "translate-x-0 bg-black/50" : "translate-x-full"
|
||||
}`}
|
||||
onClick={() => setDevModeOpen(false)}
|
||||
>
|
||||
<div
|
||||
className={`bg-background ml-auto flex h-full w-[800px] flex-col p-4`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold">Workflow Editor</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{isFetching ? (
|
||||
"Loading..."
|
||||
) : workflowFile ? (
|
||||
<>
|
||||
Edit the code of <b>{workflowFile.file_path}</b> and save to
|
||||
apply changes to your workflow.
|
||||
</>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setDevModeOpen(false)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
{fetchingError ? (
|
||||
<div className="bg-destructive/10 text-destructive/70 mb-4 flex items-center gap-2 rounded-md p-4">
|
||||
<AlertCircle className="shrink-0" size={16} />
|
||||
<p className="text-sm font-medium">{fetchingError}</p>
|
||||
</div>
|
||||
) : (
|
||||
<CodeEditor
|
||||
code={updatedCode ?? workflowFile?.content ?? ""}
|
||||
onChange={setUpdatedCode}
|
||||
language={codeEditorLanguage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col">
|
||||
{saveError && (
|
||||
<div className="bg-destructive/10 text-destructive/70 mb-4 rounded-md p-4">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<AlertCircle className="shrink-0" size={16} />
|
||||
<h6 className="text-sm font-medium">Error Saving Code</h6>
|
||||
</div>
|
||||
<p className="whitespace-pre-wrap text-sm">{saveError}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mr-2"
|
||||
onClick={handleResetCode}
|
||||
>
|
||||
Reset Code
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveCode}
|
||||
disabled={isSaving || !updatedCode || !workflowFile}
|
||||
>
|
||||
Save & Restart Server
|
||||
{isSaving && <Loader2 className="ml-2 h-4 w-4 animate-spin" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user