Compare commits

..

26 Commits

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

* feat: Add simple chat app example with FastAPI integration

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

* update doc

* change to file_path

* include changes from #614

* fix mypy

* support devmode for backend ts server

* Revert "support devmode for backend ts server"

This reverts commit bd943fd8c1.

* fix: polling should work when server not yet started

* bump chat-ui to fix syntax highlight issue

* fix: missing language for code editor

* enhance UI with shadow overlay

* enhance doc

* fix minor UI bugs

* enhance doc

* remove unessesary debug log

* fix wrong check

* increase delay time before trigger polling

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

* feat: support dev mode for backend ts server

* update message

* validate typescript file

* fix: format

* use temp file to avoid server restart

* fix format

* use npx tsc

* remove typescript deps

---------

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

* add changeset

* fix package version

* fix typing

* bump openai

* fix package

* fix typing

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

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

* docs: word smith

* better handler typing

* refactor: remove redundant UI event handling in workflows

---------

Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2025-05-15 14:22:21 +07:00
Thuc Pham 24515393a6 fix: remove dead generated ai code (#618) 2025-05-14 12:42:43 +07:00
Huu Le b3eb0ba7d4 fix typing issue and add typing test for llamaindexserver templates (#613)
* try testing for llamaindexserver

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

* refactor template structure

* fix package conflict

* add tests for python

* fix python mypy

* use matrix for templateType

* add changeset

* add removing data.ts for artifacts template

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

* Enhance tests for LlamaIndexServer by adding conditional skips based on data source and refining use case tests for example data source
2025-05-13 16:20:24 +07:00
Huu Le 556f33c0ab pin onnxruntime version to fix issue on Windows (#609) 2025-05-12 16:25:47 +07:00
Marcus Schiesser 7a70390b00 chore: deprecate pro mode (#607) 2025-05-12 12:07:55 +07:00
github-actions[bot] ad5912b41f Release 0.5.13 (#605)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-09 17:31:53 +07:00
Marcus Schiesser 76502d28e7 chore: remove changeset 2025-05-09 17:29:50 +07:00
Huu Le f4ca602da5 feat: Add artifact use case and use new the workflow for Typescript (#595)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2025-05-09 17:20:30 +07:00
Thuc Pham d304554f33 feat: add examples package for easily testing workflow (#599) 2025-05-08 17:15:00 +07:00
github-actions[bot] 8dce9f913d Release 0.2.0 (#591)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-08 17:14:11 +07:00
Marcus Schiesser c62096c516 fix: server packages (#598) 2025-05-08 16:34:51 +07:00
Huu Le 0384268543 Support the new workflow for @llamaindex/server (#592)
* use the new llama-flow workflow

* update create-llama

* update deep research workflow to use llama-flow

* update ts config to avoid overhead checking python

* Refactor workflows to use startAgentEvent and remove workflowInputEvent. Clean up unused imports and improve error handling for missing user input.

* Refactor runWorkflow to utilize AgentInputData and improve error handling for missing user input. Replace workflowInputEvent with startAgentEvent and enhance chat history management. Add callbacks for suggested questions event.

* Implement code artifact workflow and update TypeScript helpers. Introduce new `code_workflow.ts` for managing code generation and updates, and create a factory function in `workflow.ts`. Modify TypeScript helper to copy all `.ts` files instead of just `workflow.ts`. Update chat handler to utilize `AgentInputData` for improved data handling.

* Refactor runWorkflow to utilize the run function for workflow execution, replacing the previous context creation method. This change simplifies the workflow stream initialization and enhances code clarity.

* Refactor workflows to replace stopEvent with stopAgentEvent and enhance event handling in code_workflow.ts and workflow.ts. Update grammar in enhancedPrompt for clarity and improve response handling in agentStreamEvent.

* Refactor financial report workflow to streamline event handling and improve memory management. Replace custom event classes with workflowEvent for better clarity and maintainability. Update workflow definition to utilize getWorkflow function, enhancing code organization and readability.

* Add document and code artifact workflows with event handling improvements

- Introduced `doc-workflow.ts` for managing document generation and updates.
- Created `code-workflow.ts` for code artifact management.
- Enhanced event handling with `workflowEvent` for better clarity and maintainability.
- Updated `README-template.md` to include setup instructions and use cases for new workflows.
- Modified `workflow.ts` to allow switching between code and document workflows.
- Improved grammar and clarity in prompts and comments throughout the code.

* Refactor workflow.ts to replace ReadableStream with TransformStream for improved event handling. Introduce workflowToEngineResponseStream function to streamline the processing of workflow events and enhance error handling. Update return statement in runWorkflow to utilize the new stream implementation.

* add changesets

* Remove redundant totalQuestions update in getWorkflow function to streamline event processing.

* Migrate workflow types to @llamaindex/workflow package and update imports

* Replace @llama-flow/core with @llamaindex/workflow and update stream handling

* update workflows

* update import for agentic rag

* fix wrong import

* init new stream method

* Refactor stream handling in workflow.ts and stream.ts to utilize WorkflowStream type. Update processWorkflowStream function for improved event processing and clarity. Enhance imports from @llamaindex/workflow.

* Refactor stream handling in request.ts and stream.ts to improve type usage and error handling. Update toDataStreamResponse function to toDataStream and enhance callback functionality for better stream management in workflow.ts.

* Refactor server.ts, types.ts, and chat.ts to streamline workflow type usage and improve error handling. Update toDataStream function in stream.ts for better data streaming and processing. Enhance imports from @llamaindex/workflow for consistency.

* Enhance chat handler to include suggested questions functionality. Refactor toDataStream in stream.ts to support callback options for onStart, onText, and onFinal events. Export generateNextQuestions function in suggestion.ts for improved accessibility.

* Refactor workflow imports in deep_research and financial_report templates to enhance consistency and organization. Update package.json to include @llamaindex/workflow version 1.1.0. Remove commented-out code in gen-ui.ts for cleaner implementation.

* remove log

* fix incorrect toolcall llm check

* relock

* revert changes on create-llama
2025-05-07 17:15:07 +07:00
Thuc Pham d9f9e3c1c3 chore: bump chat-ui to support code editor & document editor (#594) 2025-05-06 16:56:26 +07:00
Thuc Pham 1357c423a3 chore: move lint & prettier configs to root (#590)
* chore: move lint & prettier configs to root

* update prettier config

* fix: format

* use bunchee in root

* move typescript packages to root

* apply recommened typescript rules for create-llama and fix lints

* apply prettier-plugin-tailwindcss to auto sort tailwind classnames

* Create ninety-goats-draw.md
2025-04-29 16:45:57 +07:00
github-actions[bot] 8105aa70b6 Release 0.5.12 (#589)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-29 15:48:08 +07:00
Marcus Schiesser 23a90625d1 chore: add ruff check 2025-04-29 15:47:13 +07:00
Marcus Schiesser ac789bcb8d chore: check python format 2025-04-29 15:42:10 +07:00
Huu Le 241d82a87d feat: add create-llama artifacts template (python) (#586)
* add artifact template for python

* Add artifact workflows for code and document generation

- Introduced `CodeArtifactWorkflow` and `DocumentArtifactWorkflow` classes to handle code and document artifacts respectively.
- Updated README to include instructions for modifying the factory method to select the appropriate workflow.
- Enhanced clarity in class documentation and improved naming conventions for better understanding.

* bump packages

* fix wrong name

* add ts workflows

* revert change for TS

* docs: fix docs

* add metadata fields

---------

Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2025-04-29 14:22:16 +07:00
github-actions[bot] b16cfd873b chore(release): bump llama-index-server version to 0.1.15 (#576)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-04-28 15:55:05 +07:00
Huu Le 3130cdf18d Add support for artifact in llama-index-server (#580)
* support artifact

* migrate poetry to uv

* fix ci

* update ci

* Refactor artifact generation tools by introducing separate CodeGenerator and DocumentGenerator classes. Update app_writer to utilize FunctionAgent for code and document generation workflows. Remove deprecated ArtifactGenerator class. Enhance artifact transformation logic in callbacks. Improve system prompts for clarity and instruction adherence.

* enhance code

* remove previous content from tool input

* fix test

* bump chat ui

* revert changes

* remove dead code

* Add artifact workflows for code and document generation

- Introduced `code_workflow.py` for generating and updating code artifacts based on user requests.
- Introduced `document_workflow.py` for generating and updating document artifacts (Markdown/HTML).
- Created `main.py` to set up FastAPI server with artifact workflows.
- Added a README for setup instructions and usage.
- Implemented UI components for displaying artifact status and progress.
- Updated chat router to remove unused event callbacks.

* remove app_writer workflow

* Refactor artifact workflow classes and UI event handling

- Renamed `ArtifactUIEvents` to `UIEventData` for clarity.
- Introduced `last_artifact` attribute in `ArtifactWorkflow` to streamline artifact retrieval.
- Updated chat history handling to utilize the new `last_artifact` attribute.
- Modified event streaming to use `UIEventData` for consistent event structure.
- Added a new UI component for displaying artifact workflow status and progress.

* Use uv to release package

* Refactor artifact workflows and UI components

- Updated `code_workflow.py` and `document_workflow.py` to improve chat history handling and user message storage.
- Enhanced `ArtifactWorkflow` to utilize optional fields in the `Requirement` model.
- Revised prompt instructions for clarity and conciseness in generating requirements.
- Modified UI event components to reflect changes in workflow stages and improve user feedback.
- Improved error handling for JSON parsing in artifact annotations.

* move code

* Merge remote-tracking branch 'origin/main' into lee/add-artifact

* sort artifact

* fix mypy

* fix adding custom route does not work

* fix mypy

* revert create-llama change

* disable e2e test for python package change

* fix missing set memory

* remove include last artifact in the code

* Add ArtifactEvent model and update workflows to use it
2025-04-28 15:49:20 +07:00
179 changed files with 12828 additions and 8388 deletions
+8 -2
View File
@@ -4,10 +4,12 @@ on:
branches: [main]
paths-ignore:
- "python/llama-index-server/**"
- ".github/workflows/*llama_index_server.yml"
pull_request:
branches: [main]
paths-ignore:
- "python/llama-index-server/**"
- ".github/workflows/*llama_index_server.yml"
jobs:
e2e-python:
@@ -21,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
@@ -68,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
@@ -75,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
@@ -91,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
@@ -138,12 +143,13 @@ jobs:
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
FRAMEWORK: ${{ matrix.frameworks }}
DATASOURCE: ${{ matrix.datasources }}
TEMPLATE_TYPE: ${{ matrix.template-types }}
working-directory: packages/create-llama
- uses: actions/upload-artifact@v4
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
@@ -31,6 +31,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:
@@ -22,7 +22,8 @@ jobs:
working-directory: ./python/llama-index-server
if: |
github.event_name == 'push' &&
!startsWith(github.ref, 'refs/heads/release/llama-index-server-v')
!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
@@ -30,17 +31,19 @@ jobs:
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 Poetry
run: |
curl -sSL https://install.python-poetry.org | python3 -
- name: Install dependencies
run: poetry install
shell: bash
run: uv sync --all-extras --dev
- name: Setup Git
run: |
@@ -48,15 +51,17 @@ jobs:
git config --global user.name "github-actions[bot]"
- name: Bump patch version
shell: bash
run: |
poetry version patch
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 version to $(poetry version -s)"
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=$(poetry version -s)
version=$(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version)
echo "current_version=${version}" >> "$GITHUB_OUTPUT"
- name: Create Release PR
@@ -91,31 +96,34 @@ jobs:
- 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 Poetry
run: |
curl -sSL https://install.python-poetry.org | python3 -
- name: Install dependencies
run: poetry install
shell: bash
run: uv sync --all-extras
- name: Get current version
id: get_version
shell: bash
run: |
version=$(poetry version -s)
version=$(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version)
echo "current_version=${version}" >> "$GITHUB_OUTPUT"
- name: Build and publish to PyPI
uses: JRubics/poetry-publish@v2.1
with:
python_version: "3.11"
pypi_token: ${{ secrets.PYPI_TOKEN }}
package_directory: "python/llama-index-server"
poetry_install_options: "--without dev"
- 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
+25 -37
View File
@@ -4,7 +4,6 @@ on:
pull_request:
env:
POETRY_VERSION: "1.8.3"
PYTHON_VERSION: "3.9"
jobs:
@@ -21,29 +20,23 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install Poetry
run: pipx install poetry==${{ env.POETRY_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Set up python ${{ matrix.python-version }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Configure Poetry
run: |
poetry config virtualenvs.create true
poetry config virtualenvs.in-project true
poetry env use python
- name: Install dependencies
shell: bash
run: poetry install --with dev
run: uv sync --all-extras --dev
- name: Run unit tests
shell: bash
run: |
poetry run pytest tests
run: uv run pytest tests
type-check:
name: Type Check
@@ -54,28 +47,23 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install Poetry
run: pipx install poetry==${{ env.POETRY_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 }}
cache: "poetry"
- name: Configure Poetry
run: |
poetry config virtualenvs.create true
poetry config virtualenvs.in-project true
poetry env use python
- name: Install dependencies
shell: bash
run: poetry install --with dev
run: uv sync --all-extras --dev
- name: Run mypy
shell: bash
run: poetry run mypy llama_index
run: uv run mypy llama_index
build:
needs: [unit-test, type-check]
@@ -85,25 +73,25 @@ jobs:
working-directory: python/llama-index-server
steps:
- uses: actions/checkout@v4
- name: Install Poetry
run: pipx install poetry==${{ env.POETRY_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: Clear python cache
- name: Install build package
shell: bash
run: poetry cache clear --all pypi
- name: Build package
shell: bash
run: poetry build
- name: Test installing built package
shell: bash
run: python -m pip install .
run: uv sync --all-extras
- name: Test import
shell: bash
working-directory: ${{ vars.RUNNER_TEMP }}
run: python -c "from llama_index.server import LlamaIndexServer"
run: uv run python -c "from llama_index.server import LlamaIndexServer"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
+2 -1
View File
@@ -1,3 +1,4 @@
pnpm format
pnpm lint
uvx ruff format --check packages/create-llama/templates/
uvx ruff check .
uvx ruff format . --check
+17
View File
@@ -0,0 +1,17 @@
node_modules/
pnpm-lock.yaml
lib/
dist/
cache/
build/
.next/
out/
packages/server/server/
**/playwright-report/
**/test-results/
# Python
python/
**/*.mypy_cache/**
**/*.venv/**
**/*.ruff_cache/**
-19
View File
@@ -106,25 +106,6 @@ Ok to proceed? (y) y
You can also pass command line arguments to set up a new project
non-interactively. For a list of the latest options, call `create-llama --help`.
### Running in pro mode
If you prefer more advanced customization options, you can run `create-llama` in pro mode using the `--pro` flag.
In pro mode, instead of selecting a predefined use case, you'll be prompted to select each technical component of your project. This allows for greater flexibility in customizing your project, including:
- **Vector Store**: Choose from a variety of vector stores for keeping your documents, including MongoDB, Pinecone, Weaviate, Qdrant and Chroma.
- **Tools**: Choose from a variety of agent tools (functions called by the LLM), such as:
- Code Interpreter: Executes Python code in a secure Jupyter notebook environment
- Artifact Code Generator: Generates code artifacts that can be run in a sandbox
- OpenAPI Action: Facilitates requests to a provided OpenAPI schema
- Image Generator: Creates images based on text descriptions
- Web Search: Performs web searches to retrieve up-to-date information
- **Data Sources**: Integrate various data sources into your chat application, including local files, websites, or database-retrieved data.
- **Backend Options**: Besides using Next.js or FastAPI, you can also select to use Express for a more traditional Node.js application.
- **Observability**: Choose from a variety of LLM observability tools, including LlamaTrace and Traceloop.
Pro mode is ideal for developers who want fine-grained control over their project's configuration and are comfortable with more technical setup options.
## LlamaIndex Documentation
- [TS/JS docs](https://ts.llamaindex.ai/)
@@ -18,6 +18,21 @@ export default tseslint.config(
},
},
{
files: ["packages/create-llama/**"],
rules: {
"max-params": ["error", 4],
"prefer-const": "error",
"no-empty": "off",
"no-extra-boolean-cast": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/no-wrapper-object-types": "off",
"@typescript-eslint/ban-ts-comment": "off",
},
},
{
files: ["packages/server/**"],
rules: {
"no-irregular-whitespace": "off",
"@typescript-eslint/no-unused-vars": "off",
@@ -31,7 +46,12 @@ export default tseslint.config(
},
{
ignores: [
"python/**",
"**/*.mypy_cache/**",
"**/*.venv/**",
"**/*.ruff_cache/**",
"**/dist/**",
"**/e2e/cache/**",
"**/lib/*",
"**/.next/**",
"**/out/**",
+53 -37
View File
@@ -1,39 +1,55 @@
{
"name": "create-llama-monorepo",
"version": "1.0.0",
"private": true,
"description": "Monorepo for create-llama",
"keywords": [
"rag",
"llamaindex"
],
"repository": {
"type": "git",
"url": "https://github.com/run-llama/create-llama"
},
"license": "MIT",
"workspaces": [
"packages/*"
],
"scripts": {
"prepare": "husky",
"new-snapshot": "pnpm -r build && changeset version --snapshot",
"new-version": "pnpm -r build && changeset version",
"release": "pnpm -r build && changeset publish",
"release-snapshot": "pnpm -r build && changeset publish --tag snapshot",
"build": "pnpm -r build",
"e2e": "pnpm -r e2e",
"dev": "pnpm -r dev",
"format": "pnpm -r format",
"format:write": "pnpm -r format:write",
"lint": "pnpm -r lint"
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
"husky": "^9.0.10"
},
"packageManager": "pnpm@9.0.5",
"engines": {
"node": ">=16.14.0"
}
"name": "create-llama-monorepo",
"version": "1.0.0",
"private": true,
"description": "Monorepo for create-llama",
"keywords": [
"rag",
"llamaindex"
],
"repository": {
"type": "git",
"url": "https://github.com/run-llama/create-llama"
},
"license": "MIT",
"workspaces": [
"packages/*"
],
"scripts": {
"dev": "pnpm -r dev",
"build": "pnpm -r build",
"e2e": "pnpm -r e2e",
"lint": "eslint .",
"format": "prettier --ignore-unknown --cache --check .",
"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",
"release-snapshot": "pnpm -r build && changeset publish --tag snapshot"
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
"bunchee": "6.4.0",
"husky": "^9.0.10",
"lint-staged": "^15.2.11",
"typescript-eslint": "^8.18.0",
"globals": "^15.12.0",
"eslint": "9.22.0",
"@eslint/js": "^9.25.0",
"eslint-config-next": "^15.1.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react": "7.37.2",
"prettier": "^3.4.2",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-tailwindcss": "^0.6.11",
"typescript": "^5.7.3",
"@types/node": "^22.9.0",
"@types/react": "^19",
"@types/react-dom": "^19"
},
"packageManager": "pnpm@9.0.5",
"engines": {
"node": ">=16.14.0"
}
}
-12
View File
@@ -1,12 +0,0 @@
{
"extends": [
"prettier"
],
"rules": {
"max-params": [
"error",
4
],
"prefer-const": "error",
},
}
-6
View File
@@ -1,6 +0,0 @@
apps/docs/i18n
apps/docs/docs/api
pnpm-lock.yaml
lib/
dist/
.docusaurus/
+24
View File
@@ -1,5 +1,29 @@
# create-llama
## 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
- 241d82a: Add artifacts use case (python)
## 0.5.11
### Patch Changes
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-extraneous-dependencies */
import path from "path";
import { green, yellow } from "picocolors";
import { tryGitInit } from "./helpers/git";
@@ -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 };
}
});
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { expect, test } from "@playwright/test";
import { ChildProcess } from "child_process";
import fs from "fs";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { expect, test } from "@playwright/test";
import { ChildProcess } from "child_process";
import fs from "fs";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { expect, test } from "@playwright/test";
import { ChildProcess } from "child_process";
import fs from "fs";
@@ -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;
}
}
+2 -2
View File
@@ -67,8 +67,8 @@ export async function runCreateLlama({
].join("-");
// Handle different data source types
let dataSourceArgs = [];
if (dataSource.includes("--web-source" || "--db-source")) {
const dataSourceArgs = [];
if (dataSource.includes("--web-source")) {
const webSource = dataSource.split(" ")[1];
dataSourceArgs.push("--web-source", webSource);
} else if (dataSource.includes("--db-source")) {
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-extraneous-dependencies */
import { async as glob } from "fast-glob";
import fs from "fs";
import path from "path";
@@ -181,7 +181,7 @@ const getVectorDBEnvs = (
]
: []),
];
case "chroma":
case "chroma": {
const envs = [
{
name: "CHROMA_COLLECTION",
@@ -206,6 +206,7 @@ Otherwise, use CHROMA_HOST and CHROMA_PORT config above`,
});
}
return envs;
}
case "weaviate":
return [
{
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-extraneous-dependencies */
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-extraneous-dependencies */
import spawn from "cross-spawn";
import { yellow } from "picocolors";
import type { PackageManager } from "./get-pkg-manager";
@@ -1,4 +1,3 @@
/* eslint-disable import/no-extraneous-dependencies */
import fs from "fs";
import path from "path";
import { blue, green } from "picocolors";
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-extraneous-dependencies */
import { execSync } from "child_process";
import fs from "fs";
@@ -28,7 +28,7 @@ export async function askModelConfig({
}: ModelConfigQuestionsParams): Promise<ModelConfig> {
let modelProvider: ModelProvider = DEFAULT_MODEL_PROVIDER;
if (askModels) {
let choices = [
const choices = [
{ title: "OpenAI", value: "openai" },
{ title: "Groq", value: "groq" },
{ title: "Ollama", value: "ollama" },
+9 -4
View File
@@ -31,6 +31,7 @@ const getAdditionalDependencies = (
tools?: Tool[],
templateType?: TemplateType,
observability?: TemplateObservability,
// eslint-disable-next-line max-params
) => {
const dependencies: Dependency[] = [];
@@ -93,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": {
@@ -562,15 +567,15 @@ const installLlamaIndexServerTemplate = async ({
process.exit(1);
}
await copy("workflow.py", path.join(root, "app"), {
await copy("*.py", path.join(root, "app"), {
parents: true,
cwd: path.join(templatesDir, "components", "workflows", "python", useCase),
cwd: path.join(templatesDir, "components", "use-cases", "python", useCase),
});
// Copy custom UI component code
await copy(`*`, path.join(root, "components"), {
parents: true,
cwd: path.join(templatesDir, "components", "ui", "workflows", useCase),
cwd: path.join(templatesDir, "components", "ui", "use-cases", useCase),
});
if (useLlamaParse) {
@@ -601,7 +606,7 @@ const installLlamaIndexServerTemplate = async ({
// Copy README.md
await copy("README-template.md", path.join(root), {
parents: true,
cwd: path.join(templatesDir, "components", "workflows", "python", useCase),
cwd: path.join(templatesDir, "components", "use-cases", "python", useCase),
rename: assetRelocator,
});
};
+3 -1
View File
@@ -57,7 +57,9 @@ export type TemplateUseCase =
| "form_filling"
| "extractor"
| "contract_review"
| "agentic_rag";
| "agentic_rag"
| "code_generator"
| "document_generator";
// Config for both file and folder
export type FileSourceConfig =
| {
+15 -18
View File
@@ -31,23 +31,24 @@ 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
await copy("*", path.join(root, "components"), {
parents: true,
cwd: path.join(templatesDir, "components", "ui", "workflows", useCase),
cwd: path.join(templatesDir, "components", "ui", "use-cases", useCase),
});
// Override generate.ts if workflow use case doesn't use custom UI
if (vectorDb === "llamacloud") {
await copy("generate.ts", path.join(root, "src"), {
parents: true,
@@ -74,18 +75,14 @@ const installLlamaIndexServerTemplate = async ({
rename: () => "data.ts",
});
}
// Copy README.md
await copy("README-template.md", path.join(root), {
parents: true,
cwd: path.join(
templatesDir,
"components",
"workflows",
"typescript",
useCase,
),
rename: assetRelocator,
});
// Simplify use case code
if (useCase === "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 +387,7 @@ const providerDependencies: {
[key in ModelProvider]?: Record<string, string>;
} = {
openai: {
"@llamaindex/openai": "^0.2.0",
"@llamaindex/openai": "^0.3.7",
},
gemini: {
"@llamaindex/google": "^0.2.0",
@@ -516,7 +513,7 @@ async function updatePackageJson({
if (backend) {
packageJson.dependencies = {
...packageJson.dependencies,
"@llamaindex/readers": "^2.0.0",
"@llamaindex/readers": "^3.1.3",
};
if (vectorDb && vectorDb in vectorDbDependencies) {
@@ -1,4 +1,3 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import validateProjectName from "validate-npm-package-name";
export function validateNpmName(name: string): {
+1 -2
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-extraneous-dependencies */
import { execSync } from "child_process";
import { Command } from "commander";
import fs from "fs";
@@ -197,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 -8
View File
@@ -1,6 +1,6 @@
{
"name": "create-llama",
"version": "0.5.11",
"version": "0.5.14",
"description": "Create LlamaIndex-powered apps with one command",
"keywords": [
"rag",
@@ -31,9 +31,6 @@
"e2e": "playwright test",
"e2e:python": "playwright test e2e/shared e2e/python",
"e2e:typescript": "playwright test e2e/shared e2e/typescript",
"format": "prettier --ignore-unknown --cache --check .",
"format:write": "prettier --ignore-unknown --write .",
"lint": "eslint . --ignore-pattern dist --ignore-pattern e2e/cache",
"pack-install": "bash ./scripts/pack.sh"
},
"dependencies": {
@@ -66,10 +63,6 @@
"yaml": "2.4.1"
},
"devDependencies": {
"eslint": "^8.56.0",
"eslint-config-prettier": "^8.10.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"@playwright/test": "^1.41.1",
"@vercel/ncc": "0.38.1",
"rimraf": "^5.0.5",
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
plugins: ["prettier-plugin-organize-imports"],
};
+7
View File
@@ -1,4 +1,5 @@
import ciInfo from "ci-info";
import { bold, yellow } from "picocolors";
import { getCIQuestionResults } from "./ci";
import { askProQuestions } from "./questions";
import { askSimpleQuestions } from "./simple";
@@ -13,6 +14,12 @@ export const askQuestions = async (
return await getCIQuestionResults(args);
} else if (args.pro) {
// TODO: refactor pro questions to return a result object
console.log(
yellow(
`Pro mode is deprecated. Please use the new templates using the ${bold("LlamaIndexServer")} by not specifying pro mode.`,
),
);
await askProQuestions(args);
return args as unknown as QuestionResults;
}
+57 -30
View File
@@ -6,7 +6,12 @@ import { ModelConfig, TemplateFramework } from "../helpers/types";
import { PureQuestionArgs, QuestionResults } from "./types";
import { askPostInstallAction, questionHandlers } from "./utils";
type AppType = "agentic_rag" | "financial_report" | "deep_research";
type AppType =
| "agentic_rag"
| "financial_report"
| "deep_research"
| "code_generator"
| "document_generator";
type SimpleAnswers = {
appType: AppType;
@@ -42,6 +47,16 @@ export const askSimpleQuestions = async (
description:
"Researches and analyzes provided documents from multiple perspectives, generating a comprehensive report with citations to support key findings and insights.",
},
{
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.",
},
],
},
questionHandlers,
@@ -52,35 +67,35 @@ export const askSimpleQuestions = async (
let useLlamaCloud = false;
if (appType !== "extractor" && appType !== "contract_review") {
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
@@ -111,10 +126,10 @@ const convertAnswers = async (
args: PureQuestionArgs,
answers: SimpleAnswers,
): Promise<QuestionResults> => {
const MODEL_GPT4o: ModelConfig = {
const MODEL_GPT41: ModelConfig = {
provider: "openai",
apiKey: args.openAiKey,
model: "gpt-4o",
model: "gpt-4.1",
embeddingModel: "text-embedding-3-large",
dimensions: 1536,
isConfigured(): boolean {
@@ -135,13 +150,25 @@ const convertAnswers = async (
template: "llamaindexserver",
dataSources: EXAMPLE_10K_SEC_FILES,
tools: getTools(["interpreter", "document_generator"]),
modelConfig: MODEL_GPT4o,
modelConfig: MODEL_GPT41,
},
deep_research: {
template: "llamaindexserver",
dataSources: EXAMPLE_10K_SEC_FILES,
tools: [],
modelConfig: MODEL_GPT4o,
modelConfig: MODEL_GPT41,
},
code_generator: {
template: "llamaindexserver",
dataSources: [],
tools: [],
modelConfig: MODEL_GPT41,
},
document_generator: {
template: "llamaindexserver",
dataSources: [],
tools: [],
modelConfig: MODEL_GPT41,
},
};
@@ -191,7 +191,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
case "png":
case "jpeg":
case "svg":
case "pdf":
case "pdf": {
const { filename } = this.saveToDisk(data, ext);
output.push({
type: ext as InterpreterExtraType,
@@ -199,6 +199,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
url: this.getFileUrl(filename),
});
break;
}
default:
output.push({
type: ext as InterpreterExtraType,
@@ -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,6 +10,7 @@ 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",
]
@@ -11,6 +11,7 @@ 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",
]
@@ -6,7 +6,7 @@ import { Message } from "./chat-messages";
export default function ChatAvatar(message: Message) {
if (message.role === "user") {
return (
<div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-background">
<div className="bg-background flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
@@ -20,7 +20,7 @@ export default function ChatAvatar(message: Message) {
}
return (
<div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-black text-white">
<div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-black text-white">
<Image
className="rounded-md"
src="/llama.png"
@@ -23,20 +23,20 @@ export default function ChatInput(props: ChatInputProps) {
<>
<form
onSubmit={props.handleSubmit}
className="flex items-start justify-between w-full max-w-5xl p-4 bg-white rounded-xl shadow-xl gap-4"
className="flex w-full max-w-5xl items-start justify-between gap-4 rounded-xl bg-white p-4 shadow-xl"
>
<input
autoFocus
name="message"
placeholder="Type a message"
className="w-full p-4 rounded-xl shadow-inner flex-1"
className="w-full flex-1 rounded-xl p-4 shadow-inner"
value={props.input}
onChange={props.handleInputChange}
/>
<button
disabled={props.isLoading}
type="submit"
className="p-4 text-white rounded-xl shadow-xl bg-gradient-to-r from-cyan-500 to-sky-500 disabled:opacity-50 disabled:cursor-not-allowed"
className="rounded-xl bg-gradient-to-r from-cyan-500 to-sky-500 p-4 text-white shadow-xl disabled:cursor-not-allowed disabled:opacity-50"
>
Send message
</button>
@@ -7,7 +7,7 @@ export default function ChatItem(message: Message) {
return (
<div className="flex items-start gap-4 pt-5">
<ChatAvatar {...message} />
<p className="break-words whitespace-pre-wrap">{message.content}</p>
<p className="whitespace-pre-wrap break-words">{message.content}</p>
</div>
);
}
@@ -39,7 +39,7 @@ export default function ChatMessages({
return (
<div
className="flex-1 w-full max-w-5xl p-4 bg-white rounded-xl shadow-xl overflow-auto"
className="w-full max-w-5xl flex-1 overflow-auto rounded-xl bg-white p-4 shadow-xl"
ref={scrollableChatContainerRef}
>
<div className="flex flex-col gap-5 divide-y">
@@ -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} />;
}
@@ -97,7 +97,7 @@ export default function Component({ events }) {
case "pending":
return <Clock className="h-4 w-4 text-gray-400" />;
case "inprogress":
return <Loader2 className="h-4 w-4 text-blue-500 animate-spin" />;
return <Loader2 className="h-4 w-4 animate-spin text-blue-500" />;
case "done":
return <CheckCircle className="h-4 w-4 text-green-500" />;
case "error":
@@ -140,9 +140,9 @@ export default function Component({ events }) {
};
return (
<div className="w-full max-w-4xl mx-auto space-y-6 p-4">
<div className="mx-auto w-full max-w-4xl space-y-6 p-4">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="mb-6 flex items-center justify-between">
<h1 className="text-2xl font-bold">DeepResearch Workflow</h1>
<div className="flex items-center space-x-2">
<Badge
@@ -188,7 +188,7 @@ export default function Component({ events }) {
className={cn(
"border-2 transition-all duration-300",
retrieve?.state === "inprogress"
? "border-blue-500 shadow-blue-100 shadow-lg"
? "border-blue-500 shadow-lg shadow-blue-100"
: retrieve?.state === "done"
? "border-green-500"
: retrieve?.state === "error"
@@ -231,7 +231,7 @@ export default function Component({ events }) {
className={cn(
"border-2 transition-all duration-300",
analyze?.state === "inprogress"
? "border-blue-500 shadow-blue-100 shadow-lg"
? "border-blue-500 shadow-lg shadow-blue-100"
: analyze?.state === "done"
? "border-green-500"
: analyze?.state === "error"
@@ -288,9 +288,9 @@ export default function Component({ events }) {
key={answer.id}
value={answer.id}
className={cn(
"mb-4 border rounded-lg overflow-hidden",
"mb-4 overflow-hidden rounded-lg border",
answer.state === "inprogress"
? "border-blue-500 shadow-blue-100 shadow-sm"
? "border-blue-500 shadow-sm shadow-blue-100"
: answer.state === "done"
? "border-green-100"
: answer.state === "error"
@@ -309,7 +309,7 @@ export default function Component({ events }) {
<Badge
variant="outline"
className={cn(
"ml-auto flex items-center space-x-1 shrink-0",
"ml-auto flex shrink-0 items-center space-x-1",
answer.state === "inprogress"
? "text-blue-500"
: answer.state === "done"
@@ -327,7 +327,7 @@ export default function Component({ events }) {
<AccordionContent className="px-4 pb-4 pt-1">
<div
className={cn(
"p-3 rounded-md",
"rounded-md p-3",
answer.state === "done"
? "bg-green-50"
: answer.state === "inprogress"
@@ -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} />;
}
@@ -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!
@@ -0,0 +1,375 @@
import re
import time
from typing import Any, Literal, Optional, Union
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,
StartEvent,
StopEvent,
Workflow,
step,
)
from llama_index.server.api.models import (
Artifact,
ArtifactEvent,
ArtifactType,
ChatRequest,
CodeArtifactData,
UIEvent,
)
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
file_name: Optional[str] = None
requirement: str
class PlanEvent(Event):
user_msg: str
context: Optional[str] = None
class GenerateArtifactEvent(Event):
requirement: Requirement
class SynthesizeAnswerEvent(Event):
pass
class UIEventData(BaseModel):
"""
Event data for updating workflow status to the UI.
"""
state: Literal["plan", "generate", "completed"] = Field(
description="The current state of the workflow. "
"plan: analyze and create a plan for the next step. "
"generate: generate the artifact based on the requirement from the previous step. "
"completed: the workflow is completed. "
)
requirement: Optional[str] = Field(
description="The requirement for generating the artifact. ",
default=None,
)
class CodeArtifactWorkflow(Workflow):
"""
A simple workflow that help generate/update the chat artifact (code, document)
e.g: Help create a NextJS app.
Update the generated code with the user's feedback.
Generate a guideline for the app,...
"""
def __init__(
self,
llm: LLM,
chat_request: ChatRequest,
**kwargs: Any,
):
"""
Args:
llm: The LLM to use.
chat_request: The chat request from the chat app to use.
"""
super().__init__(**kwargs)
self.llm = llm
self.chat_request = chat_request
self.last_artifact = get_last_artifact(chat_request)
@step
async def prepare_chat_history(self, ctx: Context, ev: StartEvent) -> PlanEvent:
user_msg = ev.user_msg
if user_msg is None:
raise ValueError("user_msg is required to run the workflow")
await ctx.set("user_msg", user_msg)
chat_history = ev.chat_history or []
chat_history.append(
ChatMessage(
role="user",
content=user_msg,
)
)
memory = ChatMemoryBuffer.from_defaults(
chat_history=chat_history,
llm=self.llm,
)
await ctx.set("memory", memory)
return PlanEvent(
user_msg=user_msg,
context=str(self.last_artifact.model_dump_json())
if self.last_artifact
else "",
)
@step
async def planning(
self, ctx: Context, event: PlanEvent
) -> Union[GenerateArtifactEvent, SynthesizeAnswerEvent]:
"""
Based on the conversation history and the user's request
this step will help to provide a good next step for the code or document generation.
"""
ctx.write_event_to_stream(
UIEvent(
type="ui_event",
data=UIEventData(
state="plan",
requirement=None,
),
)
)
prompt = PromptTemplate("""
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}
""").format(
context=""
if event.context is None
else f"## The context is: \n{event.context}\n",
user_msg=event.user_msg,
)
response = await self.llm.acomplete(
prompt=prompt,
formatted=True,
)
# parse the response to Requirement
# 1. use regex to find the json block
json_block = re.search(
r"```(?:json)?\s*([\s\S]*?)\s*```", response.text, re.IGNORECASE
)
if json_block is None:
raise ValueError("No JSON block found in the response.")
# 2. parse the json block to Requirement
requirement = Requirement.model_validate_json(json_block.group(1).strip())
ctx.write_event_to_stream(
UIEvent(
type="ui_event",
data=UIEventData(
state="generate",
requirement=requirement.requirement,
),
)
)
# Put the planning result to the memory
# useful for answering step
memory: ChatMemoryBuffer = await ctx.get("memory")
memory.put(
ChatMessage(
role="assistant",
content=f"The plan for next step: \n{response.text}",
)
)
await ctx.set("memory", memory)
if requirement.next_step == "coding":
return GenerateArtifactEvent(
requirement=requirement,
)
else:
return SynthesizeAnswerEvent()
@step
async def generate_artifact(
self, ctx: Context, event: GenerateArtifactEvent
) -> SynthesizeAnswerEvent:
"""
Generate the code based on the user's request.
"""
ctx.write_event_to_stream(
UIEvent(
type="ui_event",
data=UIEventData(
state="generate",
requirement=event.requirement.requirement,
),
)
)
prompt = PromptTemplate("""
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:
```
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:
{previous_artifact}
Now, i have to generate the code for the following requirement:
{requirement}
```
""").format(
previous_artifact=self.last_artifact.model_dump_json()
if self.last_artifact
else "",
requirement=event.requirement,
)
response = await self.llm.acomplete(
prompt=prompt,
formatted=True,
)
# Extract the code from the response
language_pattern = r"```(\w+)([\s\S]*)```"
code_match = re.search(language_pattern, response.text)
if code_match is None:
return SynthesizeAnswerEvent()
else:
code = code_match.group(2).strip()
# Put the generated code to the memory
memory: ChatMemoryBuffer = await ctx.get("memory")
memory.put(
ChatMessage(
role="assistant",
content=f"Updated the code: \n{response.text}",
)
)
# To show the Canvas panel for the artifact
ctx.write_event_to_stream(
ArtifactEvent(
data=Artifact(
type=ArtifactType.CODE,
created_at=int(time.time()),
data=CodeArtifactData(
language=event.requirement.language or "",
file_name=event.requirement.file_name or "",
code=code,
),
),
)
)
return SynthesizeAnswerEvent()
@step
async def synthesize_answer(
self, ctx: Context, event: SynthesizeAnswerEvent
) -> StopEvent:
"""
Synthesize the answer.
"""
memory: ChatMemoryBuffer = await ctx.get("memory")
chat_history = memory.get()
chat_history.append(
ChatMessage(
role="system",
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.
""",
)
)
response_stream = await self.llm.astream_chat(
messages=chat_history,
)
ctx.write_event_to_stream(
UIEvent(
type="ui_event",
data=UIEventData(
state="completed",
),
)
)
return StopEvent(result=response_stream)
@@ -0,0 +1,66 @@
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 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.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!
@@ -0,0 +1,347 @@
import re
import time
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 (
Context,
Event,
StartEvent,
StopEvent,
Workflow,
step,
)
from llama_index.server.api.models import (
Artifact,
ArtifactEvent,
ArtifactType,
ChatRequest,
DocumentArtifactData,
UIEvent,
)
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
requirement: str
class PlanEvent(Event):
user_msg: str
context: Optional[str] = None
class GenerateArtifactEvent(Event):
requirement: DocumentRequirement
class SynthesizeAnswerEvent(Event):
requirement: DocumentRequirement
generated_artifact: str
class UIEventData(BaseModel):
"""
Event data for updating workflow status to the UI.
"""
state: Literal["plan", "generate", "completed"] = Field(
description="The current state of the workflow. "
"plan: analyze and create a plan for the next step. "
"generate: generate the artifact based on the requirement from the previous step. "
"completed: the workflow is completed. "
)
requirement: Optional[str] = Field(
description="The requirement for generating the artifact. ",
default=None,
)
class DocumentArtifactWorkflow(Workflow):
"""
A workflow to help generate or update document artifacts (e.g., Markdown or HTML documents).
Example use cases: Generate a project guideline, update documentation with user feedback, etc.
"""
def __init__(
self,
llm: LLM,
chat_request: ChatRequest,
**kwargs: Any,
):
"""
Args:
llm: The LLM to use.
chat_request: The chat request from the chat app to use.
"""
super().__init__(**kwargs)
self.llm = llm
self.chat_request = chat_request
self.last_artifact = get_last_artifact(chat_request)
@step
async def prepare_chat_history(self, ctx: Context, ev: StartEvent) -> PlanEvent:
user_msg = ev.user_msg
if user_msg is None:
raise ValueError("user_msg is required to run the workflow")
await ctx.set("user_msg", user_msg)
chat_history = ev.chat_history or []
chat_history.append(
ChatMessage(
role="user",
content=user_msg,
)
)
memory = ChatMemoryBuffer.from_defaults(
chat_history=chat_history,
llm=self.llm,
)
await ctx.set("memory", memory)
return PlanEvent(
user_msg=user_msg,
context=str(self.last_artifact.model_dump_json())
if self.last_artifact
else "",
)
@step
async def planning(self, ctx: Context, event: PlanEvent) -> GenerateArtifactEvent:
"""
Based on the conversation history and the user's request,
this step will provide a clear requirement for the next document generation or update.
"""
ctx.write_event_to_stream(
UIEvent(
type="ui_event",
data=UIEventData(
state="plan",
requirement=None,
),
)
)
prompt = PromptTemplate("""
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}
""").format(
context=""
if event.context is None
else f"## The context is: \n{event.context}\n",
user_msg=event.user_msg,
)
response = await self.llm.acomplete(
prompt=prompt,
formatted=True,
)
# parse the response to DocumentRequirement
json_block = re.search(r"```json([\s\S]*)```", response.text)
if json_block is None:
raise ValueError("No json block found in the response")
requirement = DocumentRequirement.model_validate_json(
json_block.group(1).strip()
)
# Put the planning result to the memory
memory: ChatMemoryBuffer = await ctx.get("memory")
memory.put(
ChatMessage(
role="assistant",
content=f"Planning for the document generation: \n{response.text}",
)
)
await ctx.set("memory", memory)
ctx.write_event_to_stream(
UIEvent(
type="ui_event",
data=UIEventData(
state="generate",
requirement=requirement.requirement,
),
)
)
return GenerateArtifactEvent(
requirement=requirement,
)
@step
async def generate_artifact(
self, ctx: Context, event: GenerateArtifactEvent
) -> SynthesizeAnswerEvent:
"""
Generate or update the document based on the user's request.
"""
ctx.write_event_to_stream(
UIEvent(
type="ui_event",
data=UIEventData(
state="generate",
requirement=event.requirement.requirement,
),
)
)
prompt = PromptTemplate("""
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:
{previous_artifact}
Now, please generate the document for the following requirement:
{requirement}
""").format(
previous_artifact=self.last_artifact.model_dump_json()
if self.last_artifact
else "",
requirement=event.requirement,
)
response = await self.llm.acomplete(
prompt=prompt,
formatted=True,
)
# Extract the document from the response
language_pattern = r"```(markdown|html)([\s\S]*)```"
doc_match = re.search(language_pattern, response.text)
if doc_match is None:
return SynthesizeAnswerEvent(
requirement=event.requirement,
generated_artifact="There is no change to the document. "
+ response.text.strip(),
)
content = doc_match.group(2).strip()
doc_type = doc_match.group(1)
# Put the generated document to the memory
memory: ChatMemoryBuffer = await ctx.get("memory")
memory.put(
ChatMessage(
role="assistant",
content=f"Generated document: \n{response.text}",
)
)
# To show the Canvas panel for the artifact
ctx.write_event_to_stream(
ArtifactEvent(
data=Artifact(
type=ArtifactType.DOCUMENT,
created_at=int(time.time()),
data=DocumentArtifactData(
title=event.requirement.title,
content=content,
type=doc_type, # type: ignore
),
),
)
)
return SynthesizeAnswerEvent(
requirement=event.requirement,
generated_artifact=response.text,
)
@step
async def synthesize_answer(
self, ctx: Context, event: SynthesizeAnswerEvent
) -> StopEvent:
"""
Synthesize the answer for the user.
"""
memory: ChatMemoryBuffer = await ctx.get("memory")
chat_history = memory.get()
chat_history.append(
ChatMessage(
role="system",
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.
""",
)
)
response_stream = await self.llm.astream_chat(
messages=chat_history,
)
ctx.write_event_to_stream(
UIEvent(
type="ui_event",
data=UIEventData(
state="completed",
requirement=event.requirement.requirement,
),
)
)
return StopEvent(result=response_stream)
@@ -1,4 +1,4 @@
import { agent } from "llamaindex";
import { agent } from "@llamaindex/workflow";
import { getIndex } from "./data";
export const workflowFactory = async (reqBody: any) => {
@@ -0,0 +1,39 @@
import { SimpleDirectoryReader } from "@llamaindex/readers/directory";
import "dotenv/config";
import { storageContextFromDefaults, VectorStoreIndex } from "llamaindex";
import { initSettings } from "./app/settings";
async function generateDatasource() {
console.log(`Generating storage context...`);
// Split documents, create embeddings and store them in the storage context
const storageContext = await storageContextFromDefaults({
persistDir: "storage",
});
// load documents from current directory into an index
const reader = new SimpleDirectoryReader();
const documents = await reader.loadData("data");
await VectorStoreIndex.fromDocuments(documents, {
storageContext,
});
console.log("Storage context successfully generated.");
}
(async () => {
const args = process.argv.slice(2);
const command = args[0];
initSettings();
if (command === "ui") {
console.error("This project doesn't use any custom UI.");
return;
} else {
if (command !== "datasource") {
console.error(
`Unrecognized command: ${command}. Generating datasource by default.`,
);
}
await generateDatasource();
}
})();
@@ -0,0 +1,53 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) project bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
## Getting Started
First, install the dependencies:
```
npm install
```
Third, run the development server:
```
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the chat UI.
## Configure LLM and Embedding Model
You can configure [LLM model](https://ts.llamaindex.ai/docs/llamaindex/modules/llms) in the [settings file](src/app/settings.ts).
## Custom UI Components
We have a custom component located in `components/ui_event.jsx`. This is used to display the state of artifact workflows in UI. You can regenerate a new UI component from the workflow event schema by running the following command:
```
npm run generate:ui
```
## Use Case
AI-powered 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 an request on the [chat UI](http://localhost:3000) or you can test the `/api/chat` endpoint with the following curl request:
```shell
curl --location 'localhost:3000/api/chat' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "Compare the financial performance of Apple and Tesla" }] }'
```
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai/docs/llamaindex) - learn about LlamaIndex (Typescript features).
- [Workflows Introduction](https://ts.llamaindex.ai/docs/llamaindex/modules/workflows) - learn about LlamaIndexTS workflows.
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
@@ -0,0 +1,347 @@
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,
});
});
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;
}
@@ -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
@@ -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;
};
@@ -0,0 +1,53 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) project bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
## Getting Started
First, install the dependencies:
```
npm install
```
Third, run the development server:
```
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the chat UI.
## Configure LLM and Embedding Model
You can configure [LLM model](https://ts.llamaindex.ai/docs/llamaindex/modules/llms) in the [settings file](src/app/settings.ts).
## Custom UI Components
We have a custom component located in `components/ui_event.jsx`. This is used to display the state of artifact workflows in UI. You can regenerate a new UI component from the workflow event schema by running the following command:
```
npm run generate:ui
```
## Use Case
AI-powered document generator that can help you generate documents with a chat interface and simple markdown editor.
To update the workflow, you can modify the code in [`workflow.ts`](app/workflow.ts).
You can start by sending an request on the [chat UI](http://localhost:3000) or you can test the `/api/chat` endpoint with the following curl request:
```shell
curl --location 'localhost:3000/api/chat' \
--header 'Content-Type: application/json' \
--data '{ "messages": [{ "role": "user", "content": "Compare the financial performance of Apple and Tesla" }] }'
```
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai/docs/llamaindex) - learn about LlamaIndex (Typescript features).
- [Workflows Introduction](https://ts.llamaindex.ai/docs/llamaindex/modules/workflows) - learn about LlamaIndexTS workflows.
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
@@ -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;
}
@@ -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;
}
@@ -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();
}
})();
@@ -1,4 +1,4 @@
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
import { LlamaCloudIndex } from "llamaindex";
type LlamaCloudDataSourceParams = {
llamaCloudPipeline?: {
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { AstraDBVectorStore } from "@llamaindex/astra";
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { AstraDBVectorStore } from "@llamaindex/astra";
import { VectorStoreIndex } from "llamaindex";
import { checkRequiredEnvVars } from "./shared";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { ChromaVectorStore } from "@llamaindex/chroma";
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { ChromaVectorStore } from "@llamaindex/chroma";
import { VectorStoreIndex } from "llamaindex";
import { checkRequiredEnvVars } from "./shared";
@@ -1,4 +1,4 @@
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
import { LlamaCloudIndex } from "llamaindex";
type LlamaCloudDataSourceParams = {
llamaCloudPipeline?: {
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { MilvusVectorStore } from "@llamaindex/milvus";
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { MongoDBAtlasVectorSearch } from "@llamaindex/mongodb";
import * as dotenv from "dotenv";
import { storageContextFromDefaults, VectorStoreIndex } from "llamaindex";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { MongoDBAtlasVectorSearch } from "@llamaindex/mongodb";
import { VectorStoreIndex } from "llamaindex";
import { MongoClient } from "mongodb";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { PineconeVectorStore } from "@llamaindex/pinecone";
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { PineconeVectorStore } from "@llamaindex/pinecone";
import { VectorStoreIndex } from "llamaindex";
import { checkRequiredEnvVars } from "./shared";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { QdrantVectorStore } from "@llamaindex/qdrant";
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
@@ -1,4 +1,3 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { WeaviateVectorStore } from "@llamaindex/weaviate";
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
@@ -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;
}
}
@@ -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 });
};
}
@@ -51,7 +51,7 @@ def generate_ui_for_workflow():
# and run the generate_ui_for_workflow function with the imported model.
# Make sure the output filename of the generated UI component matches the event type (here `ui_event`)
try:
from app.workflow import UIEventData
from app.workflow import UIEventData # type: ignore
except ImportError:
raise ImportError("Couldn't generate UI component for the current workflow.")
from llama_index.server.gen_ui import generate_event_component
@@ -12,7 +12,7 @@ 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.14,<0.2.0",
"llama-index-server>=0.1.15,<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.3.7",
"@llamaindex/server": "^0.2.0",
"@llamaindex/workflow": "^1.1.2",
"@llamaindex/tools": "^0.0.10",
"llamaindex": "^0.10.6",
"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",
@@ -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",
@@ -96,8 +96,9 @@ export function Artifact({
useEffect(() => {
// auto trigger code execution
!result && fetchArtifactResult();
// eslint-disable-next-line react-hooks/exhaustive-deps
if (!result) {
fetchArtifactResult();
}
}, []);
if (!artifact || version === undefined) return null;
@@ -284,7 +285,6 @@ function InterpreterOutput({ outputUrls }: { outputUrls: OutputUrl[] }) {
<li key={url.url}>
<div className="mt-4">
{isImageFile(url.filename) ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={url.url} alt={url.filename} className="my-4 w-1/2" />
) : (
<a
@@ -51,18 +51,21 @@ function ChatTools({
}
switch (toolCall.name) {
case "get_weather_information":
case "get_weather_information": {
const weatherData = toolOutput.output as unknown as WeatherData;
return <WeatherCard data={weatherData} />;
case "artifact":
}
case "artifact": {
return (
<Artifact
artifact={toolOutput.output as CodeArtifact}
version={artifactVersion}
/>
);
default:
}
default: {
return null;
}
}
}
-7
View File
@@ -1,7 +0,0 @@
lib/
dist/
server/
next/.next/
next/out/
node_modules/
build/
+16
View File
@@ -1,5 +1,21 @@
# @llamaindex/server
## 0.2.1
### Patch Changes
- f072308: feat: add dev mode UI
## 0.2.0
### Minor Changes
- 0384268: Use the new workflow engine and deprecate the old one.
### Patch Changes
- d9f9e3c: chore: bump chat-ui to support code editor & document editor
## 0.1.7
### Patch Changes
+128 -5
View File
@@ -4,10 +4,10 @@ LlamaIndexServer is a Next.js-based application that allows you to quickly launc
## Features
- Serving a workflow as a chatbot
- Add a sophisticated chatbot UI to your LlamaIndex workflow
- Edit code and document artifacts in an OpenAI Canvas-style UI
- Extendable UI components for events and headers
- Built on Next.js for high performance and easy API development
- Optional built-in chat UI with extendable UI components
- Prebuilt development code
## Installation
@@ -21,9 +21,11 @@ Create an `index.ts` file and add the following code:
```ts
import { LlamaIndexServer } from "@llamaindex/server";
import { openai } from "@llamaindex/openai";
import { agent } from "@llamaindex/workflow";
import { wiki } from "@llamaindex/tools"; // or any other tool
const createWorkflow = () => agent({ tools: [wiki()] });
const createWorkflow = () => agent({ tools: [wiki()], llm: openai("gpt-4o") });
new LlamaIndexServer({
workflow: createWorkflow,
@@ -34,6 +36,8 @@ new LlamaIndexServer({
}).start();
```
The `createWorkflow` function is a factory function that creates an [Agent Workflow](https://ts.llamaindex.ai/docs/llamaindex/modules/agents/agent_workflow) with a tool that retrieves information from Wikipedia in this case. For more details, read about the [Workflow factory contract](#workflow-factory-contract).
## Running the Server
In the same directory as `index.ts`, run the following command to start the server:
@@ -54,16 +58,75 @@ curl -X POST "http://localhost:3000/api/chat" -H "Content-Type: application/json
The `LlamaIndexServer` accepts the following configuration options:
- `workflow`: A callable function that creates a workflow instance for each request
- `workflow`: A callable function that creates a workflow instance for each request. See [Workflow factory contract](#workflow-factory-contract) for more details.
- `uiConfig`: An object to configure the chat UI containing the following properties:
- `appTitle`: The title of the application (default: `"LlamaIndex App"`)
- `starterQuestions`: List of starter questions for the chat UI (default: `[]`)
- `componentsDir`: The directory for custom UI components rendering events emitted by the workflow. The default is undefined, which does not render custom UI components.
- `llamaCloudIndexSelector`: Whether to show the LlamaCloud index selector in the chat UI (requires `LLAMA_CLOUD_API_KEY` to be set in the environment variables) (default: `false`)
- `dev_mode`: When enabled, you can update workflow code in the UI and see the changes immediately. It's currently in beta and only supports updating workflow code at `app/src/workflow.ts`. Please start server in dev mode (`npm run dev`) to use see this reload feature enabled.
LlamaIndexServer accepts all the configuration options from Nextjs Custom Server such as `port`, `hostname`, `dev`, etc.
See all Nextjs Custom Server options [here](https://nextjs.org/docs/app/building-your-application/configuring/custom-server).
## Workflow factory contract
The `workflow` provided will be called for each chat request to initialize a new workflow instance. The contract of the generated workflow must be the same as for the [Agent Workflow](https://ts.llamaindex.ai/docs/llamaindex/modules/agents/agent_workflow).
This means that the workflow must handle a `startAgentEvent` event, which is the entry point of the workflow and contains the following information in it's `data` property:
```typescript
{
userInput: MessageContent;
chatHistory?: ChatMessage[] | undefined;
};
```
The `userInput` is the latest user message and the `chatHistory` is the list of messages exchanged between the user and the workflow so far.
Furthermore, the workflow must stop with a `stopAgentEvent` event to mark the end of the workflow. In between, the workflow can emit [UI events](##AI-generated-UI-Components) to render custom UI components and [Artifact events](##Sending-Artifacts-to-the-UI) to send structured data like generated documents or code snippets to the UI.
```ts
import {
createStatefulMiddleware,
createWorkflow,
startAgentEvent,
} from "@llamaindex/workflow";
import { ChatMemoryBuffer, type ChatMessage, Settings } from "llamaindex";
import { openai } from "@llamaindex/openai";
import { wiki } from "@llamaindex/tools";
Settings.llm = openai("gpt-4o");
export const workflowFactory = async () => {
const workflow = createWorkflow();
workflow.handle([startAgentEvent], async ({ data }) => {
const { state, sendEvent } = getContext();
const messages = data.chatHistory;
const toolCallResponse = await chatWithTools(
Settings.llm,
[wiki()],
messages,
);
// using result from tool call and use `sendEvent` to emit the next event...
});
// define more workflow handling logic here...
// Finally stop with a `stopAgentEvent` event to mark the end of the workflow.
// return stopAgentEvent.with({
// result: "This is the end!",
// });
return workflow;
};
```
To generate sophisticated examples of workflows, you best use the [create-llama](https://github.com/run-llama/create-llama) project.
## AI-generated UI Components
The LlamaIndex server provides support for rendering workflow events using custom UI components, allowing you to extend and customize the chat interface.
@@ -137,6 +200,66 @@ new LlamaIndexServer({
}).start();
```
## Sending Artifacts to the UI
In addition to UI events for custom components, LlamaIndex Server supports a special `ArtifactEvent` to send structured data like generated documents or code snippets to the UI. These artifacts are displayed in a dedicated "Canvas" panel in the chat interface.
### Artifact Event Structure
To send an artifact, your workflow needs to emit an event with `type: "artifact"`. The `data` payload of this event should include:
- `type`: A string indicating the type of artifact (e.g., `"document"`, `"code"`).
- `created_at`: A timestamp (e.g., `Date.now()`) indicating when the artifact was created.
- `data`: An object containing the specific details of the artifact. The structure of this object depends on the artifact `type`.
### Defining and Sending an ArtifactEvent
First, define your artifact event using `workflowEvent` from `@llamaindex/workflow`:
```typescript
import { workflowEvent } from "@llamaindex/workflow";
// Example for a document artifact
const artifactEvent = workflowEvent<{
type: "artifact"; // Must be "artifact"
data: {
type: "document"; // Custom type for your artifact (e.g., "document", "code")
created_at: number;
data: {
// Specific data for the document artifact type
title: string;
content: string;
type: "markdown" | "html"; // document format
};
};
}>();
```
Then, within your workflow logic, use `sendEvent` (obtained from `getContext()`) to emit the event:
```typescript
// Assuming 'sendEvent' is available in your workflow handler
// and 'documentDetails' contains the content for the artifact.
sendEvent(
artifactEvent.with({
type: "artifact", // This top-level type must be "artifact"
data: {
type: "document", // This is your specific artifact type
created_at: Date.now(),
data: {
title: "My Generated Document",
content: "# Hello World
This is a markdown document.",
type: "markdown",
},
},
}),
);
```
This will send the artifact to the LlamaIndex Server UI, where it will be rendered in the [ChatCanvasPanel](/packages/server/next/app/components/ui/chat/canvas/panel.tsx) by a renderer depending on the artifact type. For type `document` this is using the [DocumentArtifactViewer](https://github.com/run-llama/chat-ui/blob/bacb75fc6edceacf742fba18632404a2483b5a81/packages/chat-ui/src/chat/canvas/artifacts/document.tsx#L17).
## Default Endpoints and Features
### Chat Endpoint
+12
View File
@@ -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,43 @@
import { LlamaIndexServer } from "@llamaindex/server";
import { agent } from "@llamaindex/workflow";
import {
Document,
OpenAI,
OpenAIEmbedding,
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,
uiConfig: {
appTitle: "LlamaIndex App",
starterQuestions: ["What is the color of the dog?"],
},
port: 3000,
}).start();
@@ -0,0 +1,21 @@
This example shows how to use the dev mode of the server.
First, we need to set `devMode` to `true` in the `uiConfig` of the server.
```ts
new LlamaIndexServer({
workflow: workflowFactory,
uiConfig: {
appTitle: "Calculator",
devMode: true,
},
port: 3000,
}).start();
```
Export OpenAI API key and start the server in dev mode.
```bash
export OPENAI_API_KEY=<your-openai-api-key>
npx nodemon --exec tsx index.ts
```
+15
View File
@@ -0,0 +1,15 @@
import { LlamaIndexServer } from "@llamaindex/server";
import { workflowFactory } from "./src/app/workflow";
new LlamaIndexServer({
workflow: workflowFactory,
uiConfig: {
appTitle: "Calculator",
devMode: true,
starterQuestions: [
"What is the weather in Tokyo?",
"What is the weather in New York?",
],
},
port: 3000,
}).start();
@@ -0,0 +1,16 @@
import { agent } from "@llamaindex/workflow";
import { tool } from "llamaindex";
import { z } from "zod";
export const workflowFactory = async () => {
return agent({
tools: [
tool({
name: "weather",
description: "Get the weather in a specific city",
parameters: z.object({ city: z.string() }),
execute: ({ city }) => `The weather in ${city} is sunny`,
}),
],
});
};

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