Compare commits

..

7 Commits

Author SHA1 Message Date
leehuwuj c3215ccc7b better log 2024-05-02 15:23:06 +07:00
leehuwuj 18ca18123f split code to run_ingestion_pipeline and persist_storage 2024-05-02 15:18:40 +07:00
leehuwuj 5ecb0c9fb7 update comments and remove stores_index 2024-05-02 14:15:56 +07:00
leehuwuj 7e45f604e6 Fix dimensions typo in settings.py 2024-05-02 10:45:58 +07:00
leehuwuj bbacf0f199 refactor code and comments 2024-05-02 10:43:54 +07:00
leehuwuj c0c6df80c7 fix redundant stashed code 2024-05-02 09:25:05 +07:00
leehuwuj 3b39a12ad6 Refactor code to persist the docstore and index in the SimpleVectorStore case 2024-05-02 08:50:09 +07:00
452 changed files with 4074 additions and 37293 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Use ingestion pipeline for Python
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Display events (e.g. retrieving nodes) per chat message
-6
View File
@@ -1,6 +0,0 @@
# coderabbit.yml
reviews:
path_instructions:
- path: "templates/**"
instructions: |
For files under the `templates` folder, do not report 'Missing Dependencies Detected' errors.
+8 -88
View File
@@ -2,98 +2,22 @@ name: E2E Tests
on:
push:
branches: [main]
paths-ignore:
- "llama-index-server/**"
pull_request:
branches: [main]
paths-ignore:
- "llama-index-server/**"
env:
POETRY_VERSION: "1.6.1"
jobs:
e2e-python:
name: python
timeout-minutes: 60
strategy:
fail-fast: true
matrix:
node-version: [20]
python-version: ["3.11"]
os: [macos-latest, windows-latest, ubuntu-22.04]
frameworks: ["fastapi"]
datasources: ["--no-files", "--example-file", "--llamacloud"]
defaults:
run:
shell: bash
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Set up python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: ${{ env.POETRY_VERSION }}
- uses: pnpm/action-setup@v3
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
working-directory: .
- name: Build create-llama
run: pnpm run build
working-directory: .
- name: Install
run: pnpm run pack-install
working-directory: .
- name: Run Playwright tests for Python
run: pnpm run e2e:python
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
FRAMEWORK: ${{ matrix.frameworks }}
DATASOURCE: ${{ matrix.datasources }}
PYTHONIOENCODING: utf-8
PYTHONLEGACYWINDOWSSTDIO: utf-8
working-directory: .
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-python-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}
path: ./playwright-report/
overwrite: true
retention-days: 30
e2e-typescript:
name: typescript
e2e:
name: create-llama
timeout-minutes: 60
strategy:
fail-fast: true
matrix:
node-version: [18, 20]
python-version: ["3.11"]
os: [macos-latest, windows-latest, ubuntu-22.04]
frameworks: ["nextjs"]
datasources: ["--no-files", "--example-file", "--llamacloud"]
os: [macos-latest, windows-latest]
defaults:
run:
shell: bash
@@ -102,7 +26,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set up python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
@@ -134,19 +58,15 @@ jobs:
run: pnpm run pack-install
working-directory: .
- name: Run Playwright tests for TypeScript
run: pnpm run e2e:typescript
- name: Run Playwright tests
run: pnpm run e2e
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
FRAMEWORK: ${{ matrix.frameworks }}
DATASOURCE: ${{ matrix.datasources }}
working-directory: .
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report-typescript-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}-node${{ matrix.node-version }}
name: playwright-report
path: ./playwright-report/
overwrite: true
retention-days: 30
@@ -30,13 +30,3 @@ jobs:
- name: Run Prettier
run: pnpm run format
- name: Run Python format check
uses: chartboost/ruff-action@v1
with:
args: "format --check"
- name: Run Python lint
uses: chartboost/ruff-action@v1
with:
args: "check"
-3
View File
@@ -17,9 +17,6 @@ jobs:
- uses: pnpm/action-setup@v3
- name: Install uv
uses: astral-sh/setup-uv@v3
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -1,130 +0,0 @@
name: Release llama-index-server
on:
push:
branches:
- main
paths:
- "llama-index-server/**"
- ".github/workflows/release_llama_index_server.yml"
pull_request:
types:
- closed
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Create Release PR
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./llama-index-server
if: |
github.event_name == 'push' &&
!startsWith(github.ref, 'refs/heads/release/llama-index-server-v')
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- 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
- name: Setup Git
run: |
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
- name: Bump patch version
run: |
poetry version patch
git add pyproject.toml
git commit -m "chore(release): bump version to $(poetry version -s)"
- name: Get current version
id: get_version
run: |
version=$(poetry version -s)
echo "current_version=${version}" >> "$GITHUB_OUTPUT"
- name: Create Release PR
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Release: llama-index-server v${{ steps.get_version.outputs.current_version }}"
title: "Release: llama-index-server v${{ steps.get_version.outputs.current_version }}"
body: |
This PR was automatically created to release a new version of the llama-index-server package.
Version: ${{ steps.get_version.outputs.current_version }}
Please review the changes and merge to trigger the release.
branch: release/llama-index-server-v${{ steps.get_version.outputs.current_version }}
base: main
labels: release, llama-index-server
publish:
name: Publish to PyPI
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./llama-index-server
if: |
github.event_name == 'pull_request' &&
github.event.pull_request.merged == true &&
startsWith(github.event.pull_request.title, 'Release: llama-index-server') &&
startsWith(github.event.pull_request.head.ref, 'release/llama-index-server-v')
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: 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
- name: Get current version
id: get_version
run: |
version=$(poetry version -s)
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: "llama-index-server"
poetry_install_options: "--without dev"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: llama-index-server-v${{ steps.get_version.outputs.current_version }}
name: "llama-index-server v${{ steps.get_version.outputs.current_version }}"
body: |
Release of llama-index-server v${{ steps.get_version.outputs.current_version }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,111 +0,0 @@
name: Build Package
on:
pull_request:
env:
POETRY_VERSION: "1.8.3"
PYTHON_VERSION: "3.9"
jobs:
unit-test:
name: Unit Tests
runs-on: ${{ matrix.os }}
defaults:
run:
working-directory: llama-index-server
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.9"]
steps:
- uses: actions/checkout@v4
- name: Install Poetry
run: pipx install poetry==${{ env.POETRY_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
- name: Run unit tests
shell: bash
run: |
poetry run pytest tests
type-check:
name: Type Check
runs-on: ubuntu-latest
defaults:
run:
working-directory: llama-index-server
steps:
- uses: actions/checkout@v4
- name: Install Poetry
run: pipx install poetry==${{ env.POETRY_VERSION }}
- 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
- name: Run mypy
shell: bash
run: poetry run mypy llama_index
build:
needs: [unit-test, type-check]
runs-on: ubuntu-latest
defaults:
run:
working-directory: llama-index-server
steps:
- uses: actions/checkout@v4
- name: Install Poetry
run: pipx install poetry==${{ env.POETRY_VERSION }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Clear python cache
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 .
- name: Test import
shell: bash
working-directory: ${{ vars.RUNNER_TEMP }}
run: python -c "from llama_index.server import LlamaIndexServer"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: llama-index-server
path: llama-index-server/dist/
-15
View File
@@ -8,7 +8,6 @@ node_modules
# testing
coverage
.coverage
# next.js
.next/
@@ -47,19 +46,5 @@ e2e/cache
# intellij
**/.idea
# Python
.mypy_cache/
venv/
.venv/
dist/
.__pycache__
__pycache__
.python-version
.ui
# build artifacts
create-llama-*.tgz
# vscode
.vscode
!.vscode/settings.json
-1
View File
@@ -1,3 +1,2 @@
pnpm format
pnpm lint
uvx ruff format --check templates/
-677
View File
@@ -1,682 +1,5 @@
# create-llama
## 0.5.4
### Patch Changes
- 7c3b279: Support code generation of event components using an LLM (Python)
## 0.5.3
### Patch Changes
- 76ec360: Update templates to use new chat ui config
## 0.5.2
### Patch Changes
- c9f8f8d: Use custom component for deep research use case
## 0.5.1
### Patch Changes
- 08b3e07: Simplify the local index code.
## 0.5.0
### Minor Changes
- 54c9e2f: Simplified generated code using LlamaIndexServer
### Patch Changes
- 0e4ecfa: fix: add trycatch for generating error
- ee69ce7: bump: chat-ui and tailwind v4
## 0.4.0
### Minor Changes
- 61204a1: chore: bump LITS 0.9
### Patch Changes
- 9e723c3: Standardize the code of the workflow use case (Python)
- d5da55b: feat: add components.json to use CLI
- c1552eb: chore: move wikipedia tool to create-llama
## 0.3.28
### Patch Changes
- 4e06714: Fix the error: Unable to view file sources due to CORS.
## 0.3.27
### Patch Changes
- b4e41aa: Add deep research over own documents use case (Python)
## 0.3.26
### Patch Changes
- f73d46b: Fix missing copy of the multiagent code
## 0.3.25
### Patch Changes
- 5450096: bump: react 19 stable
## 0.3.24
### Patch Changes
- a84743c: Change --agents paramameter to --use-case
- a84743c: Add LlamaCloud support for Reflex templates
- a7a6592: Fix the npm issue on the full-stack Python template
- fc5e56e: bump: code interpreter v1
## 0.3.23
### Patch Changes
- 9077cae: Add contract review use case (Python)
## 0.3.22
### Patch Changes
- 25667d4: Make OpenAPI spec usable by custom GPTs
## 0.3.21
### Patch Changes
- 95227a7: Add query endpoint
## 0.3.20
### Patch Changes
- 27d2499: Bump the LlamaCloud library and fix breaking changes (Python).
## 0.3.19
### Patch Changes
- f9a057d: Add support multimodal indexes (e.g. from LlamaCloud)
- aedd73d: bump: chat-ui
## 0.3.18
### Patch Changes
- fe90a7e: chore: bump ai v4
- 02b2473: Show streaming errors in Python, optimize system prompts for tool usage and set the weather tool as default for the Agentic RAG use case
- 63e961e: Use auto_routed retriever mode for LlamaCloudIndex
## 0.3.17
### Patch Changes
- 28c8808: Add fly.io deployment
- 0a7dfcf: Generate NEXT_PUBLIC_CHAT_API for NextJS backend to specify alternative backend
## 0.3.16
### Patch Changes
- 8b371d8: Set pydantic version to <2.10 to avoid incompatibility with llama-index.
- 30fe269: Deactive duckduckgo tool for TS
- 30fe269: Replace DuckDuckGo by Wikipedia tool for agentic template
## 0.3.15
### Patch Changes
- fc5b266: Improve DX for Python template (use one deployment instead of two)
- f8f97d2: Add support for python 3.13
## 0.3.14
### Patch Changes
- 00f0b3a: fix: dont include user message in chat history
- 4663dec: chore: bump react19 rc
- 44b34fb: chore: update eslint 9, nextjs 15, react 19
- 6925676: feat: use latest chat UI
## 0.3.13
### Patch Changes
- 282eaa0: Ensure that the index and document store are created when uploading a file with no available index.
## 0.3.12
### Patch Changes
- 6edea6a: Optimize generated workflow code for Python
- 8431b78: Optimize Typescript multi-agent code
- 8431b78: Add form filling use case (Typescript)
## 0.3.11
### Patch Changes
- 2b8aaa8: Add support for local models via Hugging Face
- b9570b2: Fix: use generic LLMAgent instead of OpenAIAgent (adds support for Gemini and Anthropic for Agentic RAG)
- 1fe21f8: Fix the highlight.js issue with the Next.js static build
- 00009ae: feat: import pdf css
## 0.3.10
### Patch Changes
- 9172fed: feat: bump LITS 0.8.2
- 78ccde7: feat: use llamaindex chat-ui for nextjs frontend
## 0.3.9
### Patch Changes
- ed59927: Add form filling use case (Python)
## 0.3.8
### Patch Changes
- 4a83469: Add multi-agent financial report for Typescript (and update LITS to 0.7.10)
## 0.3.7
### Patch Changes
- fa80378: DocumentInfo working with relative URLs
## 0.3.6
### Patch Changes
- 0182368: Fix the streaming issue to prevent the UI from hanging.
## 0.3.5
### Patch Changes
- 2209409: Add financial report as the default use case in the multi-agent template (Python).
## 0.3.4
### Patch Changes
- 384a136: Fix import error if the artifact tool is selected
## 0.3.3
### Patch Changes
- 99b8247: Simplify and unify handling file uploads
## 0.3.2
### Patch Changes
- 6d1b6b9: Update README.md for pro mode
## 0.3.1
### Patch Changes
- f3577c5: Fix event streaming is blocked
- f3577c5: Add upload file to sandbox (artifact and code interpreter)
## 0.3.0
### Minor Changes
- 7562cb4: Simplified default questions and added pro mode
### Patch Changes
- 0a69fe0: fix: missing params when init Astra vectorstore
- 98a82b0: docs: chroma env variables
## 0.2.19
### Patch Changes
- 3d41488: feat: use selected llamacloud for multiagent
## 0.2.18
### Patch Changes
- 75e1f61: Fix cannot query public document from llamacloud
- 88220f1: fix workflow doesn't stop when user presses stop generation button
- 75e1f61: Fix typescript templates cannot upload file to llamacloud
- 88220f1: Bump llama_index@0.11.17
## 0.2.17
### Patch Changes
- cd3fcd0: bump: use LlamaIndexTS 0.6.18
- 6335de1: Fix using LlamaCloud selector does not use the configured values in the environment (Python)
## 0.2.16
### Patch Changes
- 0e78ba4: Fix: programmatically ensure index for LlamaCloud
- 0e78ba4: Fix .env not loaded on poetry run generate
- 7f4ac22: Don't need to run generate script for LlamaCloud
- 5263bde: Use selected LlamaCloud index in multi-agent template
## 0.2.15
### Patch Changes
- 16e6124: Bump package for llamatrace observability
- 3790ca0: Add multi-agent task selector for TS template
- d18f039: Add e2b code artifact tool for the FastAPI template
## 0.2.14
### Patch Changes
- 5a7216e: feat: implement artifact tool in TS
## 0.2.13
### Patch Changes
- 04ddebc: Add publisher agent to multi-agents for generating documents (PDF and HTML)
- 04ddebc: Allow tool selection for multi-agents (Python and TS)
## 0.2.12
### Patch Changes
- 70f7dca: feat: add test deps for llamaparse
- ef070c0: Add multi agents template for Typescript
## 0.2.11
### Patch Changes
- 7c2a3f6: fix: postgres import
## 0.2.10
### Patch Changes
- cb8d535: Fix only produces one agent event
## 0.2.9
### Patch Changes
- 0213fe0: Update dependencies for vector stores and add e2e test to ensure that they work as expected.
## 0.2.8
### Patch Changes
- 0031e67: Bump llama-index to 0.11.11 for the multi-agent template
## 0.2.7
### Patch Changes
- 505b8e9: bump: use latest ai package version
- cf3ec97: Dynamically select model for Groq
- 8c1087f: feat: enhance style for markdown
## 0.2.6
### Patch Changes
- adc40cf: fix: vercel ai update crash sending annotations
## 0.2.5
### Patch Changes
- 38a8be8: fix: filter in mongo vector store
## 0.2.4
### Patch Changes
- 917e862: Fix errors in building the frontend
## 0.2.3
### Patch Changes
- b6da3c2: Ensure the generation script always works
## 0.2.2
### Patch Changes
- 8105c5c: Add env config for next questions feature
## 0.2.1
### Patch Changes
- 6a409cb: Bump web and database reader packages
## 0.2.0
### Minor Changes
- 435109f: Add multi-agents template based on workflows
## 0.1.44
### Patch Changes
- bedde2b: Change metadata filters to use already existing documents in LlamaCloud Index
- 5cd12fa: Use one callback manager per request
- 5cd12fa: Bump llama_index version to 0.11.1
- fd4abb3: Fix to use filename for uploaded documents in NextJS
- 2f8feab: Simplify CLI interface
## 0.1.43
### Patch Changes
- 4fa2b76: feat: implement citation for TS
## 0.1.42
### Patch Changes
- 8f670a9: Allow relative URL in documents
## 0.1.41
### Patch Changes
- 57e7638: Use the retrieval defaults from LlamaCloud
## 0.1.40
### Patch Changes
- 8ce4a85: Add UI for extractor template
## 0.1.39
### Patch Changes
- 3fb93c7: Use LlamaCloud pipeline for data ingestion in TS (private file uploads and generate script)
## 0.1.38
### Patch Changes
- bd5e39a: Fix error that files in sub folders of 'data' are not displayed
## 0.1.37
### Patch Changes
- 9fd832c: Add in-text citation references
## 0.1.36
### Patch Changes
- 2b7a5d8: Fix: private file upload not working in Python without LlamaCloud
## 0.1.35
### Patch Changes
- 81ef7f0: Use LlamaCloud pipeline for data ingestion (private file uploads and generate script)
## 0.1.34
### Patch Changes
- c49a5e1: Add error handling for generating the next question
- c49a5e1: Fix wrong api key variable in Azure OpenAI provider
## 0.1.33
### Patch Changes
- d746c75: Add Weaviate vector store (Typescript)
## 0.1.32
### Patch Changes
- 3ec5163: Add Weaviate vector database support (Python)
## 0.1.31
### Patch Changes
- 04a9c71: Cluster nodes by document
## 0.1.30
### Patch Changes
- 09e3022: Add support for LlamaTrace (Python)
- c06ec4f: Fix imports for MongoDB
- b6dd7a9: Always send chat data when submit message
## 0.1.29
### Patch Changes
- 8890e27: Let user change indexes in LlamaCloud projects
## 0.1.28
### Patch Changes
- 9a09e8c: Fix Vercel deployment
## 0.1.27
### Patch Changes
- c5c7eee: Make components reusable for chat-llamaindex
## 0.1.26
### Patch Changes
- f43399c: Add metadatafilters to context chat engine (Typescript)
## 0.1.25
### Patch Changes
- c67daeb: fix: missing set private to false for default generate.py
## 0.1.24
### Patch Changes
- 43474a5: Configure LlamaCloud organization ID for Python
- cf11b23: Add Azure code interpreter for Python and TS
- fd9fb42: Add Azure OpenAI as model provider
- 5c13646: Fix starter questions not working in python backend
## 0.1.23
### Patch Changes
- 6bd76fb: Add template for structured extraction
## 0.1.22
### Patch Changes
- b0becaa: Add e2e testing for llamacloud datasource
- df9cca5: Upgrade pdf viewer
## 0.1.21
### Patch Changes
- bd4714c: Filter private documents for Typescript (Using MetadataFilters) and update to LlamaIndexTS 0.5.7
- 58e6c15: Add using LlamaParse for private file uploader
- 455ab68: Display files in sources using LlamaCloud indexes.
- 23b7357: Use gpt-4o-mini as default model
- 0900413: Add suggestions for next questions.
## 0.1.20
### Patch Changes
- 624c721: Update to LlamaIndex 0.10.55
## 0.1.19
### Patch Changes
- df96159: Use Qdrant FastEmbed as local embedding provider
- 32fb32a: Support upload document files: pdf, docx, txt
## 0.1.18
### Patch Changes
- d1026ea: support Mistral as llm and embedding
- a221cfc: Use LlamaParse for all the file types that it supports (if activated)
## 0.1.17
### Patch Changes
- 9ecd061: Add new template for a multi-agents app
## 0.1.16
### Patch Changes
- a0aab03: Add T-System's LLMHUB as a model provider
## 0.1.15
### Patch Changes
- 64732f0: Fix the issue of images not showing with the sandbox URL from OpenAI's models
- aeb6fef: use llamacloud for chat
## 0.1.14
### Patch Changes
- f2c3389: chore: update to llamaindex 0.4.3
- 5093b37: Remove non-working file selectors for Linux
## 0.1.13
### Patch Changes
- b3c969d: Add image generator tool
## 0.1.12
### Patch Changes
- aa69014: Fix NextJS for TS 5.2
## 0.1.11
### Patch Changes
- 48b96ff: Add DuckDuckGo search tool
- 9c9decb: Reuse function tool instances and improve e2b interpreter tool for Python
- 02ed277: Add Groq as a model provider
- 0748f2e: Remove hard-coded Gemini supported models
## 0.1.10
### Patch Changes
- 9112d08: Add OpenAPI tool for Typescript
- 8f03f8d: Add OLLAMA_REQUEST_TIMEOUT variable to config Ollama timeout (Python)
- 8f03f8d: Apply nest_asyncio for llama parse
## 0.1.9
### Patch Changes
- a42fa53: Add CSV upload
- 563b51d: Fix Vercel streaming (python) to stream data events instantly
- d60b3c5: Add E2B code interpreter tool for FastAPI
- 956538e: Add OpenAPI action tool for FastAPI
## 0.1.8
### Patch Changes
- cd50a33: Add interpreter tool for TS using e2b.dev
## 0.1.7
### Patch Changes
- 260d37a: Add system prompt env variable for TS
- bbd5b8d: Fix postgres connection leaking issue
- bb53425: Support HTTP proxies by setting the GLOBAL_AGENT_HTTP_PROXY env variable
- 69c2e16: Fix streaming for Express
- 7873bfb: Update Ollama provider to run with the base URL from the environment variable
## 0.1.6
### Patch Changes
- 56537a1: Display PDF files in source nodes
## 0.1.5
### Patch Changes
- 84db798: feat: support display latex in chat markdown
## 0.1.4
### Patch Changes
- 0bc8e75: Use ingestion pipeline for dedicated vector stores (Python only)
- cb1001d: Add ChromaDB vector store
## 0.1.3
### Patch Changes
- 416073d: Directly import vector stores to work with NextJS
## 0.1.2
### Patch Changes
- 056e376: Add support for displaying tool outputs (including weather widget as example)
## 0.1.1
### Patch Changes
- 7bd3ed5: Support Anthropic and Gemini as model providers
- 7bd3ed5: Support new agents from LITS 0.3
- cfb5257: Display events (e.g. retrieving nodes) per chat message
## 0.1.0
### Minor Changes
+43 -44
View File
@@ -1,20 +1,14 @@
# Create Llama
# Create LlamaIndex App
The easiest way to get started with [LlamaIndex](https://www.llamaindex.ai/) is by using `create-llama`. This CLI tool enables you to quickly start building a new LlamaIndex application, with everything set up for you.
## Get started
Just run
```bash
npx create-llama@latest
```
to get started, or watch this video for a demo session:
<img src="https://github.com/user-attachments/assets/c4a7fe18-8e30-498a-96f8-78127dd706b9" width="100%">
Once your app is generated, run
to get started, or see below for more options. Once your app is generated, run
```bash
npm run dev
@@ -24,25 +18,21 @@ to start the development server. You can then visit [http://localhost:3000](http
## What you'll get
- A set of pre-configured use cases to get you started, e.g. Agentic RAG, Data Analysis, Report Generation, etc.
- A Next.js-powered front-end using components from [shadcn/ui](https://ui.shadcn.com/). The app is set up as a chat interface that can answer questions about your data or interact with your agent
- Your choice of two back-ends:
- A Next.js-powered front-end using components from [shadcn/ui](https://ui.shadcn.com/). The app is set up as a chat interface that can answer questions about your data (see below)
- Your choice of 3 back-ends:
- **Next.js**: if you select this option, youll have a full-stack Next.js application that you can deploy to a host like [Vercel](https://vercel.com/) in just a few clicks. This uses [LlamaIndex.TS](https://www.npmjs.com/package/llamaindex), our TypeScript library.
- **Python FastAPI**: if you select this option, youll get a separate backend powered by the [llama-index Python package](https://pypi.org/project/llama-index/), which you can deploy to a service like [Render](https://render.com/) or [fly.io](https://fly.io/). The separate Next.js front-end will connect to this backend.
- Each back-end has two endpoints:
- One streaming chat endpoint, that allow you to send the state of your chat and receive additional responses
- One endpoint to upload private files which can be used in your chat
- **Express**: if you want a more traditional Node.js application you can generate an Express backend. This also uses LlamaIndex.TS.
- **Python FastAPI**: if you select this option, youll get a backend powered by the [llama-index python package](https://pypi.org/project/llama-index/), which you can deploy to a service like Render or fly.io.
- The back-end has two endpoints (one streaming, the other one non-streaming) that allow you to send the state of your chat and receive additional responses
- You add arbitrary data sources to your chat, like local files, websites, or data retrieved from a database.
- Turn your chat into an AI agent by adding tools (functions called by the LLM).
- The app uses OpenAI by default, so you'll need an OpenAI API key, or you can customize it to use any of the dozens of LLMs we support.
Here's how it looks like:
https://github.com/user-attachments/assets/d57af1a1-d99b-4e9c-98d9-4cbd1327eff8
## Using your data
Optionally, you can supply your own data; the app will index it and make use of it, e.g. to answer questions. Your generated app will have a folder called `data` (If you're using Express or Python and generate a frontend, it will be `./backend/data`).
You can supply your own data; the app will index it and answer questions. Your generated app will have a folder called `data` (If you're using Express or Python and generate a frontend, it will be `./backend/data`).
The app will ingest any supported files you put in this directory. Your Next.js and Express apps use LlamaIndex.TS, so they will be able to ingest any PDF, text, CSV, Markdown, Word and HTML files. The Python backend can read even more types, including video and audio files.
The app will ingest any supported files you put in this directory. Your Next.js and Express apps use LlamaIndex.TS so they will be able to ingest any PDF, text, CSV, Markdown, Word and HTML files. The Python backend can read even more types, including video and audio files.
Before you can use your data, you need to index it. If you're using the Next.js or Express apps, run:
@@ -58,9 +48,13 @@ If you're using the Python backend, you can trigger indexing of your data by cal
poetry run generate
```
## Want a front-end?
Optionally generate a frontend if you've selected the Python or Express back-ends. If you do so, `create-llama` will generate two folders: `frontend`, for your Next.js-based frontend code, and `backend` containing your API.
## Customizing the AI models
The app will default to OpenAI's `gpt-4o-mini` LLM and `text-embedding-3-large` embedding model.
The app will default to OpenAI's `gpt-4-turbo` LLM and `text-embedding-3-large` embedding model.
If you want to use different OpenAI models, add the `--ask-models` CLI parameter.
@@ -90,40 +84,45 @@ Need to install the following packages:
create-llama@latest
Ok to proceed? (y) y
✔ What is your project named? … my-app
✔ What app do you want to build? Agentic RAG
✔ What language do you want to use? Python (FastAPI)
Do you want to use LlamaCloud services? No / Yes
✔ Please provide your LlamaCloud API key (leave blank to skip): …
✔ Which template would you like to use? Chat
✔ Which framework would you like to use? NextJS
Would you like to set up observability? No
✔ Please provide your OpenAI API key (leave blank to skip): …
✔ Which data source would you like to use? Use an example PDF
✔ Would you like to add another data source? No
✔ Would you like to use LlamaParse (improved parser for RAG - requires API key)? … no / yes
✔ Would you like to use a vector database? No, just store the data in the file system
? How would you like to proceed? - Use arrow-keys. Return to submit.
Just generate code (~1 sec)
Start in VSCode (~1 sec)
Generate code and install dependencies (~2 min)
Just generate code (~1 sec)
Start in VSCode (~1 sec)
Generate code and install dependencies (~2 min)
Generate code, install dependencies, and run the app (~2 min)
```
### Running non-interactively
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`.
non-interactively. See `create-llama --help`:
### Running in pro mode
```bash
create-llama <project-directory> [options]
If you prefer more advanced customization options, you can run `create-llama` in pro mode using the `--pro` flag.
Options:
-V, --version output the version number
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:
--use-npm
- **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.
Explicitly tell the CLI to bootstrap the app using npm
Pro mode is ideal for developers who want fine-grained control over their project's configuration and are comfortable with more technical setup options.
--use-pnpm
Explicitly tell the CLI to bootstrap the app using pnpm
--use-yarn
Explicitly tell the CLI to bootstrap the app using Yarn
```
## LlamaIndex Documentation
+27 -45
View File
@@ -7,16 +7,17 @@ import { getOnline } from "./helpers/is-online";
import { isWriteable } from "./helpers/is-writeable";
import { makeDir } from "./helpers/make-dir";
import fs from "fs";
import terminalLink from "terminal-link";
import type { InstallTemplateArgs, TemplateObservability } from "./helpers";
import type { InstallTemplateArgs } from "./helpers";
import { installTemplate } from "./helpers";
import { writeDevcontainer } from "./helpers/devcontainer";
import { templatesDir } from "./helpers/dir";
import { toolsRequireConfig } from "./helpers/tools";
import { configVSCode } from "./helpers/vscode";
export type InstallAppArgs = Omit<
InstallTemplateArgs,
"appName" | "root" | "isOnline" | "port"
"appName" | "root" | "isOnline" | "customApiPath"
> & {
appPath: string;
frontend: boolean;
@@ -34,12 +35,12 @@ export async function createApp({
communityProjectConfig,
llamapack,
vectorDb,
externalPort,
postInstallAction,
dataSources,
tools,
useLlamaParse,
observability,
useCase,
}: InstallAppArgs): Promise<void> {
const root = path.resolve(appPath);
@@ -79,30 +80,39 @@ export async function createApp({
communityProjectConfig,
llamapack,
vectorDb,
externalPort,
postInstallAction,
dataSources,
tools,
useLlamaParse,
observability,
useCase,
};
// Install backend
await installTemplate({ ...args, backend: true });
if (frontend && framework === "fastapi" && template !== "llamaindexserver") {
if (frontend) {
// install backend
const backendRoot = path.join(root, "backend");
await makeDir(backendRoot);
await installTemplate({ ...args, root: backendRoot, backend: true });
// install frontend
const frontendRoot = path.join(root, ".frontend");
const frontendRoot = path.join(root, "frontend");
await makeDir(frontendRoot);
await installTemplate({
...args,
root: frontendRoot,
framework: "nextjs",
customApiPath: `http://localhost:${externalPort ?? 8000}/api/chat`,
backend: false,
});
// copy readme for fullstack
await fs.promises.copyFile(
path.join(templatesDir, "README-fullstack.md"),
path.join(root, "README.md"),
);
} else {
await installTemplate({ ...args, backend: true });
}
await configVSCode(root, templatesDir, framework);
await writeDevcontainer(root, templatesDir, framework, frontend);
process.chdir(root);
if (tryGitInit(root)) {
@@ -110,7 +120,7 @@ export async function createApp({
console.log();
}
if (toolsRequireConfig(tools) && template !== "llamaindexserver") {
if (toolsRequireConfig(tools)) {
const configFile =
framework === "fastapi" ? "config/tools.yaml" : "config/tools.json";
console.log(
@@ -132,42 +142,14 @@ export async function createApp({
)} and learn how to get started.`,
);
outputObservability(args.observability);
if (
dataSources.some((dataSource) => dataSource.type === "file") &&
process.platform === "linux"
) {
if (args.observability === "opentelemetry") {
console.log(
yellow(
`You can add your own data files to ${terminalLink(
"data",
`file://${root}/data`,
)} folder manually.`,
),
`\n${yellow("Observability")}: Visit the ${terminalLink(
"documentation",
"https://traceloop.com/docs/openllmetry/integrations",
)} to set up the environment variables and start seeing execution traces.`,
);
}
console.log();
}
function outputObservability(observability?: TemplateObservability) {
switch (observability) {
case "traceloop":
console.log(
`\n${yellow("Observability")}: Visit the ${terminalLink(
"documentation",
"https://traceloop.com/docs/openllmetry/integrations",
)} to set up the environment variables and start seeing execution traces.`,
);
break;
case "llamatrace":
console.log(
`\n${yellow("Observability")}: LlamaTrace has been configured for your project. Visit the ${terminalLink(
"LlamaTrace dashboard",
"https://llamatrace.com/login",
)} to view your traces and monitor your application.`,
);
break;
}
}
+129
View File
@@ -0,0 +1,129 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { expect, test } from "@playwright/test";
import { ChildProcess } from "child_process";
import fs from "fs";
import path from "path";
import type {
TemplateFramework,
TemplatePostInstallAction,
TemplateType,
TemplateUI,
} from "../helpers";
import { createTestDir, runCreateLlama, type AppType } from "./utils";
const templateTypes: TemplateType[] = ["streaming"];
const templateFrameworks: TemplateFramework[] = [
"nextjs",
"express",
"fastapi",
];
const dataSources: string[] = ["--no-files", "--example-file"];
const templateUIs: TemplateUI[] = ["shadcn", "html"];
const templatePostInstallActions: TemplatePostInstallAction[] = [
"none",
"runApp",
];
for (const templateType of templateTypes) {
for (const templateFramework of templateFrameworks) {
for (const dataSource of dataSources) {
for (const templateUI of templateUIs) {
for (const templatePostInstallAction of templatePostInstallActions) {
const appType: AppType =
templateFramework === "nextjs" ? "" : "--frontend";
test.describe(`try create-llama ${templateType} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
let port: number;
let externalPort: number;
let cwd: string;
let name: string;
let appProcess: ChildProcess;
// Only test without using vector db for now
const vectorDb = "none";
test.beforeAll(async () => {
port = Math.floor(Math.random() * 10000) + 10000;
externalPort = port + 1;
cwd = await createTestDir();
const result = await runCreateLlama(
cwd,
templateType,
templateFramework,
dataSource,
templateUI,
vectorDb,
appType,
port,
externalPort,
templatePostInstallAction,
);
name = result.projectName;
appProcess = result.appProcess;
});
test("App folder should exist", async () => {
const dirExists = fs.existsSync(path.join(cwd, name));
expect(dirExists).toBeTruthy();
});
test("Frontend should have a title", async ({ page }) => {
test.skip(templatePostInstallAction !== "runApp");
await page.goto(`http://localhost:${port}`);
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
});
test("Frontend should be able to submit a message and receive a response", async ({
page,
}) => {
test.skip(templatePostInstallAction !== "runApp");
await page.goto(`http://localhost:${port}`);
await page.fill("form input", "hello");
const [response] = await Promise.all([
page.waitForResponse(
(res) => {
return (
res.url().includes("/api/chat") && res.status() === 200
);
},
{
timeout: 1000 * 60,
},
),
page.click("form button[type=submit]"),
]);
const text = await response.text();
console.log("AI response when submitting message: ", text);
expect(response.ok()).toBeTruthy();
});
test("Backend frameworks should response when calling non-streaming chat API", async ({
request,
}) => {
test.skip(templatePostInstallAction !== "runApp");
test.skip(templateFramework === "nextjs");
const response = await request.post(
`http://localhost:${externalPort}/api/chat/request`,
{
data: {
messages: [
{
role: "user",
content: "Hello",
},
],
},
},
);
const text = await response.text();
console.log("AI response when calling API: ", text);
expect(response.ok()).toBeTruthy();
});
// clean processes
test.afterAll(async () => {
appProcess?.kill();
});
});
}
}
}
}
}
-233
View File
@@ -1,233 +0,0 @@
import { expect, test } from "@playwright/test";
import { exec } from "child_process";
import fs from "fs";
import path from "path";
import util from "util";
import { TemplateFramework, TemplateVectorDB } from "../../helpers/types";
import { RunCreateLlamaOptions, createTestDir, runCreateLlama } from "../utils";
const execAsync = util.promisify(exec);
const templateFramework: TemplateFramework = process.env.FRAMEWORK
? (process.env.FRAMEWORK as TemplateFramework)
: "fastapi";
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
: "--example-file";
// TODO: add support for other templates
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 dataSources = [
"--example-file",
"--web-source https://www.example.com",
"--db-source mysql+pymysql://user:pass@localhost:3306/mydb",
];
const observabilityOptions = ["llamatrace", "traceloop"];
test.describe("Mypy check", () => {
test.describe.configure({ retries: 0 });
// Test vector databases
for (const vectorDb of vectorDbs) {
test(`Mypy check for vectorDB: ${vectorDb}`, 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,
},
});
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",
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(`Mypy check for observability: ${observability}`, 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,
},
});
});
}
});
}
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);
// Check if the app folder exists
expect(fs.existsSync(projectPath)).toBeTruthy();
// Check if pyproject.toml exists
const pyprojectPath = path.join(projectPath, "pyproject.toml");
expect(fs.existsSync(pyprojectPath)).toBeTruthy();
const env = {
...process.env,
POETRY_VIRTUALENVS_IN_PROJECT: "true",
};
// Run poetry install
try {
const { stdout: installStdout, stderr: installStderr } = await execAsync(
"poetry install",
{ cwd: projectPath, env },
);
console.log("poetry install stdout:", installStdout);
console.error("poetry install stderr:", installStderr);
} catch (error) {
console.error("Error running poetry install:", error);
throw error;
}
// Run poetry run mypy
try {
const { stdout: mypyStdout, stderr: mypyStderr } = await execAsync(
"poetry run mypy .",
{ cwd: projectPath, env },
);
console.log("poetry run mypy stdout:", mypyStdout);
console.error("poetry run mypy stderr:", mypyStderr);
} 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,105 +0,0 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { expect, test } from "@playwright/test";
import { ChildProcess } from "child_process";
import fs from "fs";
import path from "path";
import type {
TemplateFramework,
TemplatePostInstallAction,
TemplateUI,
} from "../../helpers";
import { createTestDir, runCreateLlama, type AppType } from "../utils";
const templateFramework: TemplateFramework = process.env.FRAMEWORK
? (process.env.FRAMEWORK as TemplateFramework)
: "fastapi";
const dataSource: string = "--example-file";
const templateUI: TemplateUI = "shadcn";
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
const appType: AppType = "--frontend";
const userMessage = "Write a blog post about physical standards for letters";
const templateUseCases = ["financial_report", "agentic_rag", "deep_research"];
for (const useCase of templateUseCases) {
test.describe(`Test use case ${useCase} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
test.skip(
process.platform !== "linux" ||
process.env.DATASOURCE === "--no-files" ||
templateFramework === "express",
"The llamaindexserver template currently only works with nextjs, fastapi. We also only run on Linux to speed up tests.",
);
let port: number;
let cwd: string;
let name: string;
let appProcess: ChildProcess;
// Only test without using vector db for now
const vectorDb = "none";
test.beforeAll(async () => {
port = Math.floor(Math.random() * 10000) + 10000;
cwd = await createTestDir();
const result = await runCreateLlama({
cwd,
templateType: "llamaindexserver",
templateFramework,
dataSource,
vectorDb,
port,
postInstallAction: templatePostInstallAction,
templateUI,
appType,
useCase,
});
name = result.projectName;
appProcess = result.appProcess;
});
test("App folder should exist", async () => {
const dirExists = fs.existsSync(path.join(cwd, name));
expect(dirExists).toBeTruthy();
});
test("Frontend should have a title", async ({ page }) => {
test.skip(
templatePostInstallAction !== "runApp" ||
templateFramework === "express",
);
await page.goto(`http://localhost:${port}`);
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
});
test("Frontend should be able to submit a message and receive the start of a streamed response", async ({
page,
}) => {
test.skip(
templatePostInstallAction !== "runApp" ||
useCase === "financial_report" ||
useCase === "deep_research" ||
templateFramework === "express",
"Skip chat tests for financial report and deep research.",
);
await page.goto(`http://localhost:${port}`);
await page.fill("form textarea", userMessage);
const responsePromise = page.waitForResponse((res) =>
res.url().includes("/api/chat"),
);
await page.click("form button[type=submit]");
const response = await responsePromise;
console.log(`Response status: ${response.status()}`);
const responseBody = await response
.text()
.catch((e) => `Error reading body: ${e}`);
console.log(`Response body: ${responseBody}`);
expect(response.ok()).toBeTruthy();
});
// clean processes
test.afterAll(async () => {
appProcess?.kill();
});
});
}
-64
View File
@@ -1,64 +0,0 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { expect, test } from "@playwright/test";
import { ChildProcess } from "child_process";
import fs from "fs";
import path from "path";
import { TemplateFramework, TemplateUseCase } from "../../helpers";
import { createTestDir, runCreateLlama } from "../utils";
const templateFramework: TemplateFramework = process.env.FRAMEWORK
? (process.env.FRAMEWORK as TemplateFramework)
: "fastapi";
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
: "--example-file";
const templateUseCases: TemplateUseCase[] = ["extractor", "contract_review"];
// The reflex template currently only works with FastAPI and files (and not on Windows)
if (
process.platform !== "win32" &&
templateFramework === "fastapi" &&
dataSource === "--example-file"
) {
for (const useCase of templateUseCases) {
test.describe(`Test reflex template ${useCase} ${templateFramework} ${dataSource}`, async () => {
let appPort: number;
let name: string;
let appProcess: ChildProcess;
let cwd: string;
// Create reflex app
test.beforeAll(async () => {
cwd = await createTestDir();
appPort = Math.floor(Math.random() * 10000) + 10000;
const result = await runCreateLlama({
cwd,
templateType: "reflex",
templateFramework: "fastapi",
dataSource: "--example-file",
vectorDb: "none",
port: appPort,
postInstallAction: "runApp",
useCase,
});
name = result.projectName;
appProcess = result.appProcess;
});
test.afterAll(async () => {
appProcess.kill();
});
test("App folder should exist", async () => {
const dirExists = fs.existsSync(path.join(cwd, name));
expect(dirExists).toBeTruthy();
});
test("Frontend should have a title", async ({ page }) => {
await page.goto(`http://localhost:${appPort}`);
await expect(page.getByText("Built by LlamaIndex")).toBeVisible({
timeout: 2000 * 60,
});
});
});
}
}
-128
View File
@@ -1,128 +0,0 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { expect, test } from "@playwright/test";
import { ChildProcess } from "child_process";
import fs from "fs";
import path from "path";
import type {
TemplateFramework,
TemplatePostInstallAction,
TemplateUI,
} from "../../helpers";
import { createTestDir, runCreateLlama, type AppType } from "../utils";
const templateFramework: TemplateFramework = process.env.FRAMEWORK
? (process.env.FRAMEWORK as TemplateFramework)
: "fastapi";
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
: "--example-file";
const templateUI: TemplateUI = "shadcn";
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
const llamaCloudProjectName = "create-llama";
const llamaCloudIndexName = "e2e-test";
const appType: AppType = templateFramework === "fastapi" ? "--frontend" : "";
const userMessage =
dataSource !== "--no-files" ? "Physical standard for letters" : "Hello";
test.describe(`Test streaming template ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
const isNode18 = process.version.startsWith("v18");
const isLlamaCloud = dataSource === "--llamacloud";
// llamacloud is using File API which is not supported on node 18
if (isNode18 && isLlamaCloud) {
test.skip(true, "Skipping tests for Node 18 and LlamaCloud data source");
}
let port: number;
let cwd: string;
let name: string;
let appProcess: ChildProcess;
// Only test without using vector db for now
const vectorDb = "none";
test.beforeAll(async () => {
port = Math.floor(Math.random() * 10000) + 10000;
cwd = await createTestDir();
const result = await runCreateLlama({
cwd,
templateType: "streaming",
templateFramework,
dataSource,
vectorDb,
port,
postInstallAction: templatePostInstallAction,
templateUI,
appType,
llamaCloudProjectName,
llamaCloudIndexName,
});
name = result.projectName;
appProcess = result.appProcess;
});
test("App folder should exist", async () => {
const dirExists = fs.existsSync(path.join(cwd, name));
expect(dirExists).toBeTruthy();
});
test("Frontend should have a title", async ({ page }) => {
test.skip(
templatePostInstallAction !== "runApp" || templateFramework === "express",
);
await page.goto(`http://localhost:${port}`);
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
});
test("Frontend should be able to submit a message and receive a response", async ({
page,
}) => {
test.skip(
templatePostInstallAction !== "runApp" || templateFramework === "express",
);
await page.goto(`http://localhost:${port}`);
await page.fill("form textarea", userMessage);
const [response] = await Promise.all([
page.waitForResponse(
(res) => {
return res.url().includes("/api/chat") && res.status() === 200;
},
{
timeout: 1000 * 60,
},
),
page.click("form button[type=submit]"),
]);
const text = await response.text();
console.log("AI response when submitting message: ", text);
expect(response.ok()).toBeTruthy();
});
test("Backend frameworks should response when calling non-streaming chat API", async ({
request,
}) => {
test.skip(templatePostInstallAction !== "runApp");
test.skip(templateFramework === "nextjs");
const response = await request.post(
`http://localhost:${port}/api/chat/request`,
{
data: {
messages: [
{
role: "user",
content: userMessage,
},
],
},
},
);
const text = await response.text();
console.log("AI response when calling API: ", text);
expect(response.ok()).toBeTruthy();
});
// clean processes
test.afterAll(async () => {
appProcess?.kill();
});
});
-105
View File
@@ -1,105 +0,0 @@
import { expect, test } from "@playwright/test";
import { exec } from "child_process";
import fs from "fs";
import path from "path";
import util from "util";
import { TemplateFramework, TemplateVectorDB } from "../../helpers/types";
import { createTestDir, runCreateLlama } from "../utils";
const execAsync = util.promisify(exec);
const templateFramework: TemplateFramework = process.env.FRAMEWORK
? (process.env.FRAMEWORK as TemplateFramework)
: "nextjs";
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
: "--example-file";
// vectorDBs combinations to test
const vectorDbs: TemplateVectorDB[] = [
"mongo",
"pg",
"qdrant",
"pinecone",
"milvus",
"astra",
"chroma",
"llamacloud",
"weaviate",
];
test.describe("Test resolve TS dependencies", () => {
// Test vector DBs without LlamaParse
for (const vectorDb of vectorDbs) {
const optionDescription = `vectorDb: ${vectorDb}, dataSource: ${dataSource}`;
test(`Vector DB test - ${optionDescription}`, async () => {
await runTest(vectorDb, false);
});
}
// 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",
{
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;
}
}
});
+95 -128
View File
@@ -18,137 +18,141 @@ export type CreateLlamaResult = {
appProcess: ChildProcess;
};
export type RunCreateLlamaOptions = {
cwd: string;
templateType: TemplateType;
templateFramework: TemplateFramework;
dataSource: string;
vectorDb: TemplateVectorDB;
port: number;
postInstallAction: TemplatePostInstallAction;
templateUI?: TemplateUI;
appType?: AppType;
llamaCloudProjectName?: string;
llamaCloudIndexName?: string;
tools?: string;
useLlamaParse?: boolean;
observability?: string;
useCase?: string;
};
// eslint-disable-next-line max-params
export async function checkAppHasStarted(
frontend: boolean,
framework: TemplateFramework,
port: number,
externalPort: number,
timeout: number,
) {
if (frontend) {
await Promise.all([
waitPort({
host: "localhost",
port: port,
timeout,
}),
waitPort({
host: "localhost",
port: externalPort,
timeout,
}),
]).catch((err) => {
console.error(err);
throw err;
});
} else {
let wPort: number;
if (framework === "nextjs") {
wPort = port;
} else {
wPort = externalPort;
}
await waitPort({
host: "localhost",
port: wPort,
timeout,
}).catch((err) => {
console.error(err);
throw err;
});
}
}
export async function runCreateLlama({
cwd,
templateType,
templateFramework,
dataSource,
vectorDb,
port,
postInstallAction,
templateUI,
appType,
llamaCloudProjectName,
llamaCloudIndexName,
tools,
useLlamaParse,
observability,
useCase,
}: RunCreateLlamaOptions): Promise<CreateLlamaResult> {
if (!process.env.OPENAI_API_KEY || !process.env.LLAMA_CLOUD_API_KEY) {
throw new Error(
"Setting the OPENAI_API_KEY and LLAMA_CLOUD_API_KEY is mandatory to run tests",
);
// eslint-disable-next-line max-params
export async function runCreateLlama(
cwd: string,
templateType: TemplateType,
templateFramework: TemplateFramework,
dataSource: string,
templateUI: TemplateUI,
vectorDb: TemplateVectorDB,
appType: AppType,
port: number,
externalPort: number,
postInstallAction: TemplatePostInstallAction,
): Promise<CreateLlamaResult> {
if (!process.env.OPENAI_API_KEY) {
throw new Error("Setting OPENAI_API_KEY is mandatory to run tests");
}
const name = [
templateType,
templateFramework,
dataSource.split(" ")[0],
dataSource,
templateUI,
appType,
].join("-");
// Handle different data source types
let dataSourceArgs = [];
if (dataSource.includes("--web-source" || "--db-source")) {
const webSource = dataSource.split(" ")[1];
dataSourceArgs.push("--web-source", webSource);
} else if (dataSource.includes("--db-source")) {
const dbSource = dataSource.split(" ")[1];
dataSourceArgs.push("--db-source", dbSource);
} else {
dataSourceArgs.push(dataSource);
}
const commandArgs = [
const command = [
"create-llama",
name,
"--template",
templateType,
"--framework",
templateFramework,
...dataSourceArgs,
dataSource,
"--ui",
templateUI,
"--vector-db",
vectorDb,
"--use-npm",
"--open-ai-key",
process.env.OPENAI_API_KEY,
appType,
"--use-pnpm",
"--port",
port,
"--external-port",
externalPort,
"--post-install-action",
postInstallAction,
"--tools",
tools ?? "none",
"none",
"--no-llama-parse",
"--observability",
"none",
];
if (templateUI) {
commandArgs.push("--ui", templateUI);
}
if (appType) {
commandArgs.push(appType);
}
if (useLlamaParse) {
commandArgs.push("--use-llama-parse");
} else {
commandArgs.push("--no-llama-parse");
}
if (observability) {
commandArgs.push("--observability", observability);
}
if (
(templateType === "multiagent" ||
templateType === "reflex" ||
templateType === "llamaindexserver") &&
useCase
) {
commandArgs.push("--use-case", useCase);
}
const command = commandArgs.join(" ");
].join(" ");
console.log(`running command '${command}' in ${cwd}`);
const appProcess = exec(command, {
cwd,
env: {
...process.env,
LLAMA_CLOUD_PROJECT_NAME: llamaCloudProjectName,
LLAMA_CLOUD_INDEX_NAME: llamaCloudIndexName,
},
});
appProcess.stderr?.on("data", (data) => {
console.error(data.toString());
console.log(data.toString());
});
appProcess.on("exit", (code) => {
if (code !== 0 && code !== null) {
throw new Error(`create-llama command failed with exit code ${code}`);
throw new Error(`create-llama command was failed!`);
}
});
// Wait for app to start
if (postInstallAction === "runApp") {
await waitPorts([port]);
} else if (postInstallAction === "dependencies") {
await waitForProcess(appProcess, 1000 * 60); // wait 1 min for dependencies to be resolved
await checkAppHasStarted(
appType === "--frontend",
templateFramework,
port,
externalPort,
1000 * 60 * 5,
);
} else {
// wait 10 seconds for create-llama to exit
await waitForProcess(appProcess, 1000 * 10);
// wait create-llama to exit
// we don't test install dependencies for now, so just set timeout for 10 seconds
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("create-llama timeout error"));
}, 1000 * 10);
appProcess.on("exit", (code) => {
if (code !== 0 && code !== null) {
clearTimeout(timeout);
reject(new Error("create-llama command was failed!"));
} else {
clearTimeout(timeout);
resolve(undefined);
}
});
});
}
return {
@@ -162,40 +166,3 @@ export async function createTestDir() {
await mkdir(cwd, { recursive: true });
return cwd;
}
async function waitPorts(ports: number[]): Promise<void> {
const waitForPort = async (port: number): Promise<void> => {
await waitPort({
host: "localhost",
port: port,
// wait max. 5 mins for start up of app
timeout: 1000 * 60 * 5,
});
};
try {
await Promise.all(ports.map(waitForPort));
} catch (err) {
console.error(err);
throw err;
}
}
async function waitForProcess(
process: ChildProcess,
timeoutMs: number,
): Promise<void> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("Process timeout error"));
}, timeoutMs);
process.on("exit", (code) => {
clearTimeout(timeout);
if (code !== 0 && code !== null) {
reject(new Error("Process exited with non-zero code"));
} else {
resolve();
}
});
});
}
-3
View File
@@ -61,9 +61,6 @@ export const assetRelocator = (name: string) => {
case "README-template.md": {
return "README.md";
}
case "vscode_settings.json": {
return "settings.json";
}
default: {
return name;
}
+60 -93
View File
@@ -11,47 +11,6 @@ export const EXAMPLE_FILE: TemplateDataSource = {
},
};
export const EXAMPLE_10K_SEC_FILES: TemplateDataSource[] = [
{
type: "file",
config: {
url: new URL(
"https://s2.q4cdn.com/470004039/files/doc_earnings/2023/q4/filing/_10-K-Q4-2023-As-Filed.pdf",
),
filename: "apple_10k_report.pdf",
},
},
{
type: "file",
config: {
url: new URL(
"https://ir.tesla.com/_flysystem/s3/sec/000162828024002390/tsla-20231231-gen.pdf",
),
filename: "tesla_10k_report.pdf",
},
},
];
export const EXAMPLE_GDPR: TemplateDataSource = {
type: "file",
config: {
url: new URL(
"https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:32016R0679",
),
filename: "gdpr.pdf",
},
};
export const AI_REPORTS: TemplateDataSource = {
type: "file",
config: {
url: new URL(
"https://www.europarl.europa.eu/RegData/etudes/ATAG/2024/760392/EPRS_ATA(2024)760392_EN.pdf",
),
filename: "EPRS_ATA_2024_760392_EN.pdf",
},
};
export function getDataSources(
files?: string,
exampleFile?: boolean,
@@ -77,66 +36,74 @@ export async function writeLoadersConfig(
dataSources: TemplateDataSource[],
useLlamaParse?: boolean,
) {
const loaderConfig: Record<string, any> = {};
// Always set file loader config
loaderConfig.file = createFileLoaderConfig(useLlamaParse);
if (dataSources.length === 0) return; // no datasources, no config needed
const loaderConfig = new Document({});
// Web loader config
if (dataSources.some((ds) => ds.type === "web")) {
loaderConfig.web = createWebLoaderConfig(dataSources);
const webLoaderConfig = new Document({});
// Create config for browser driver arguments
const driverArgNodeValue = webLoaderConfig.createNode([
"--no-sandbox",
"--disable-dev-shm-usage",
]);
driverArgNodeValue.commentBefore =
" The arguments to pass to the webdriver. E.g.: add --headless to run in headless mode";
webLoaderConfig.set("driver_arguments", driverArgNodeValue);
// Create config for urls
const urlConfigs = dataSources
.filter((ds) => ds.type === "web")
.map((ds) => {
const dsConfig = ds.config as WebSourceConfig;
return {
base_url: dsConfig.baseUrl,
prefix: dsConfig.prefix,
depth: dsConfig.depth,
};
});
const urlConfigNode = webLoaderConfig.createNode(urlConfigs);
urlConfigNode.commentBefore = ` base_url: The URL to start crawling with
prefix: Only crawl URLs matching the specified prefix
depth: The maximum depth for BFS traversal
You can add more websites by adding more entries (don't forget the - prefix from YAML)`;
webLoaderConfig.set("urls", urlConfigNode);
// Add web config to the loaders config
loaderConfig.set("web", webLoaderConfig);
}
// File loader config
if (dataSources.some((ds) => ds.type === "file")) {
// Add documentation to web loader config
const node = loaderConfig.createNode({
use_llama_parse: useLlamaParse,
});
node.commentBefore = ` use_llama_parse: Use LlamaParse if \`true\`. Needs a \`LLAMA_CLOUD_API_KEY\` from https://cloud.llamaindex.ai set as environment variable`;
loaderConfig.set("file", node);
}
// DB loader config
const dbLoaders = dataSources.filter((ds) => ds.type === "db");
if (dbLoaders.length > 0) {
loaderConfig.db = createDbLoaderConfig(dbLoaders);
}
const dbLoaderConfig = new Document({});
const configEntries = dbLoaders.map((ds) => {
const dsConfig = ds.config as DbSourceConfig;
return {
uri: dsConfig.uri,
queries: [dsConfig.queries],
};
});
// Create a new Document with the loaderConfig
const yamlDoc = new Document(loaderConfig);
const node = dbLoaderConfig.createNode(configEntries);
node.commentBefore = ` The configuration for the database loader, only supports MySQL and PostgreSQL databases for now.
uri: The URI for the database. E.g.: mysql+pymysql://user:password@localhost:3306/db or postgresql+psycopg2://user:password@localhost:5432/db
query: The query to fetch data from the database. E.g.: SELECT * FROM table`;
loaderConfig.set("db", node);
}
// Write loaders config
const loaderConfigPath = path.join(root, "config", "loaders.yaml");
await fs.mkdir(path.join(root, "config"), { recursive: true });
await fs.writeFile(loaderConfigPath, yaml.stringify(yamlDoc));
}
function createWebLoaderConfig(dataSources: TemplateDataSource[]): any {
const webLoaderConfig: Record<string, any> = {};
// Create config for browser driver arguments
webLoaderConfig.driver_arguments = [
"--no-sandbox",
"--disable-dev-shm-usage",
];
// Create config for urls
const urlConfigs = dataSources
.filter((ds) => ds.type === "web")
.map((ds) => {
const dsConfig = ds.config as WebSourceConfig;
return {
base_url: dsConfig.baseUrl,
prefix: dsConfig.prefix,
depth: dsConfig.depth,
};
});
webLoaderConfig.urls = urlConfigs;
return webLoaderConfig;
}
function createFileLoaderConfig(useLlamaParse?: boolean): any {
return {
use_llama_parse: useLlamaParse,
};
}
function createDbLoaderConfig(dbLoaders: TemplateDataSource[]): any {
return dbLoaders.map((ds) => {
const dsConfig = ds.config as DbSourceConfig;
return {
uri: dsConfig.uri,
queries: [dsConfig.queries],
};
});
await fs.writeFile(loaderConfigPath, yaml.stringify(loaderConfig));
}
+23 -29
View File
@@ -1,26 +1,40 @@
import fs from "fs";
import path from "path";
import { assetRelocator, copy } from "./copy";
import { TemplateFramework } from "./types";
function renderDevcontainerContent(
templatesDir: string,
framework: TemplateFramework,
frontend: boolean,
) {
const devcontainerJson: any = JSON.parse(
fs.readFileSync(path.join(templatesDir, "devcontainer.json"), "utf8"),
);
// Modify postCreateCommand
devcontainerJson.postCreateCommand =
framework === "fastapi" ? "poetry install" : "npm install";
if (frontend) {
devcontainerJson.postCreateCommand =
framework === "fastapi"
? "cd backend && poetry install && cd ../frontend && npm install"
: "cd backend && npm install && cd ../frontend && npm install";
} else {
devcontainerJson.postCreateCommand =
framework === "fastapi" ? "poetry install" : "npm install";
}
// Modify containerEnv
if (framework === "fastapi") {
devcontainerJson.containerEnv = {
...devcontainerJson.containerEnv,
PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}",
};
if (frontend) {
devcontainerJson.containerEnv = {
...devcontainerJson.containerEnv,
PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}/backend",
};
} else {
devcontainerJson.containerEnv = {
...devcontainerJson.containerEnv,
PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}",
};
}
}
return JSON.stringify(devcontainerJson, null, 2);
@@ -30,6 +44,7 @@ export const writeDevcontainer = async (
root: string,
templatesDir: string,
framework: TemplateFramework,
frontend: boolean,
) => {
const devcontainerDir = path.join(root, ".devcontainer");
if (fs.existsSync(devcontainerDir)) {
@@ -39,6 +54,7 @@ export const writeDevcontainer = async (
const devcontainerContent = renderDevcontainerContent(
templatesDir,
framework,
frontend,
);
fs.mkdirSync(devcontainerDir);
await fs.promises.writeFile(
@@ -46,25 +62,3 @@ export const writeDevcontainer = async (
devcontainerContent,
);
};
export const copyVSCodeSettings = async (
root: string,
templatesDir: string,
) => {
const vscodeDir = path.join(root, ".vscode");
await copy("vscode_settings.json", vscodeDir, {
cwd: templatesDir,
rename: assetRelocator,
});
};
export const configVSCode = async (
root: string,
templatesDir: string,
framework: TemplateFramework,
) => {
await writeDevcontainer(root, templatesDir, framework);
if (framework === "fastapi") {
await copyVSCodeSettings(root, templatesDir);
}
};
+62 -424
View File
@@ -1,25 +1,13 @@
import fs from "fs/promises";
import path from "path";
import { TOOL_SYSTEM_PROMPT_ENV_VAR, Tool } from "./tools";
import {
InstallTemplateArgs,
ModelConfig,
TemplateDataSource,
TemplateFramework,
TemplateObservability,
TemplateType,
TemplateVectorDB,
} from "./types";
import { TSYSTEMS_LLMHUB_API_URL } from "./providers/llmhub";
const DEFAULT_SYSTEM_PROMPT =
"You are a helpful assistant who helps users with their questions.";
const DATA_SOURCES_PROMPT =
"You have access to a knowledge base including the facts that you should start with to find the answer for the user question. Use the query engine tool to retrieve the facts from the knowledge base.";
export type EnvVar = {
type EnvVar = {
name?: string;
description?: string;
value?: string;
@@ -41,21 +29,17 @@ const renderEnvVar = (envVars: EnvVar[]): string => {
);
};
const getVectorDBEnvs = (
vectorDb?: TemplateVectorDB,
framework?: TemplateFramework,
template?: TemplateType,
): EnvVar[] => {
if (!vectorDb || !framework) {
const getVectorDBEnvs = (vectorDb?: TemplateVectorDB): EnvVar[] => {
if (!vectorDb) {
return [];
}
switch (vectorDb) {
case "mongo":
return [
{
name: "MONGODB_URI",
name: "MONGO_URI",
description:
"For generating a connection URI, see https://www.mongodb.com/docs/manual/reference/connection-string/ \nThe MongoDB connection URI.",
"For generating a connection URI, see https://docs.timescale.com/use-timescale/latest/services/create-a-service\nThe MongoDB connection URI.",
},
{
name: "MONGODB_DATABASE",
@@ -72,7 +56,7 @@ const getVectorDBEnvs = (
{
name: "PG_CONNECTION_STRING",
description:
"For generating a connection URI, see https://supabase.com/vector\nThe PostgreSQL connection string.",
"For generating a connection URI, see https://docs.timescale.com/use-timescale/latest/services/create-a-service\nThe PostgreSQL connection string.",
},
];
@@ -145,94 +129,8 @@ const getVectorDBEnvs = (
"Optional API key for authenticating requests to Qdrant.",
},
];
case "llamacloud":
return [
{
name: "LLAMA_CLOUD_INDEX_NAME",
description:
"The name of the LlamaCloud index to use (part of the LlamaCloud project).",
value: "test",
},
{
name: "LLAMA_CLOUD_PROJECT_NAME",
description: "The name of the LlamaCloud project.",
value: "Default",
},
{
name: "LLAMA_CLOUD_BASE_URL",
description:
"The base URL for the LlamaCloud API. Only change this for non-production environments",
value: "https://api.cloud.llamaindex.ai",
},
{
name: "LLAMA_CLOUD_ORGANIZATION_ID",
description:
"The organization ID for the LlamaCloud project (uses default organization if not specified)",
},
...(framework === "nextjs" && template !== "llamaindexserver"
? // activate index selector per default (not needed for non-NextJS backends as it's handled by createFrontendEnvFile)
[
{
name: "NEXT_PUBLIC_USE_LLAMACLOUD",
description:
"Let's the user change indexes in LlamaCloud projects",
value: "true",
},
]
: []),
];
case "chroma":
const envs = [
{
name: "CHROMA_COLLECTION",
description: "The name of the collection in your Chroma database",
},
{
name: "CHROMA_HOST",
description: "The hostname for your Chroma database. Eg: localhost",
},
{
name: "CHROMA_PORT",
description: "The port for your Chroma database. Eg: 8000",
},
];
// TS Version doesn't support config local storage path
if (framework === "fastapi") {
envs.push({
name: "CHROMA_PATH",
description: `The local path to the Chroma database.
Specify this if you are using a local Chroma database.
Otherwise, use CHROMA_HOST and CHROMA_PORT config above`,
});
}
return envs;
case "weaviate":
return [
{
name: "WEAVIATE_CLUSTER_URL",
description:
"The URL of the Weaviate cloud cluster, see: https://weaviate.io/developers/wcs/connect",
},
{
name: "WEAVIATE_API_KEY",
description: "The API key for the Weaviate cloud cluster",
},
{
name: "WEAVIATE_INDEX_NAME",
description:
"(Optional) The collection name to use, default is LlamaIndex if not specified",
},
];
default:
return template !== "llamaindexserver"
? [
{
name: "STORAGE_CACHE_DIR",
description: "The directory to store the local storage cache.",
value: ".cache",
},
]
: [];
return [];
}
};
@@ -258,10 +156,6 @@ const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
description: "Dimension of the embedding model to use.",
value: modelConfig.dimensions.toString(),
},
{
name: "CONVERSATION_STARTERS",
description: "The questions to help users get started (multi-line).",
},
...(modelConfig.provider === "openai"
? [
{
@@ -279,155 +173,41 @@ const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
},
]
: []),
...(modelConfig.provider === "anthropic"
? [
{
name: "ANTHROPIC_API_KEY",
description: "The Anthropic API key to use.",
value: modelConfig.apiKey,
},
]
: []),
...(modelConfig.provider === "groq"
? [
{
name: "GROQ_API_KEY",
description: "The Groq API key to use.",
value: modelConfig.apiKey,
},
]
: []),
...(modelConfig.provider === "gemini"
? [
{
name: "GOOGLE_API_KEY",
description: "The Google API key to use.",
value: modelConfig.apiKey,
},
]
: []),
...(modelConfig.provider === "ollama"
? [
{
name: "OLLAMA_BASE_URL",
description:
"The base URL for the Ollama API. Eg: http://127.0.0.1:11434",
},
]
: []),
...(modelConfig.provider === "mistral"
? [
{
name: "MISTRAL_API_KEY",
description: "The Mistral API key to use.",
value: modelConfig.apiKey,
},
]
: []),
...(modelConfig.provider === "azure-openai"
? [
{
name: "AZURE_OPENAI_API_KEY",
description: "The Azure OpenAI key to use.",
value: modelConfig.apiKey,
},
{
name: "AZURE_OPENAI_ENDPOINT",
description: "The Azure OpenAI endpoint to use.",
},
{
name: "AZURE_OPENAI_API_VERSION",
description: "The Azure OpenAI API version to use.",
},
{
name: "AZURE_OPENAI_LLM_DEPLOYMENT",
description:
"The Azure OpenAI deployment to use for LLM deployment.",
},
{
name: "AZURE_OPENAI_EMBEDDING_DEPLOYMENT",
description:
"The Azure OpenAI deployment to use for embedding deployment.",
},
]
: []),
...(modelConfig.provider === "huggingface"
? [
{
name: "EMBEDDING_BACKEND",
description:
"The backend to use for the Sentence Transformers embedding model, either 'torch', 'onnx', or 'openvino'. Defaults to 'onnx'.",
},
{
name: "EMBEDDING_TRUST_REMOTE_CODE",
description:
"Whether to trust remote code for the embedding model, required for some models with custom code.",
},
]
: []),
...(modelConfig.provider === "t-systems"
? [
{
name: "T_SYSTEMS_LLMHUB_BASE_URL",
description:
"The base URL for the T-Systems AI Foundation Model API. Eg: http://localhost:11434",
value: TSYSTEMS_LLMHUB_API_URL,
},
{
name: "T_SYSTEMS_LLMHUB_API_KEY",
description: "API Key for T-System's AI Foundation Model.",
value: modelConfig.apiKey,
},
]
: []),
];
};
const getFrameworkEnvs = (
framework: TemplateFramework,
template: TemplateType,
framework?: TemplateFramework,
port?: number,
): EnvVar[] => {
const sPort = port?.toString() || "8000";
const result: EnvVar[] =
template !== "llamaindexserver"
? [
{
name: "FILESERVER_URL_PREFIX",
description:
"FILESERVER_URL_PREFIX is the URL prefix of the server storing the images generated by the interpreter.",
value:
framework === "nextjs"
? // FIXME: if we are using nextjs, port should be 3000
"http://localhost:3000/api/files"
: `http://localhost:${sPort}/api/files`,
},
]
: [];
if (framework === "fastapi") {
result.push(
...[
{
name: "APP_HOST",
description: "The address to start the FastAPI app.",
value: "0.0.0.0",
},
{
name: "APP_PORT",
description: "The port to start the FastAPI app.",
value: sPort,
},
],
);
if (framework !== "fastapi") {
return [];
}
if (framework === "nextjs" && template !== "llamaindexserver") {
result.push({
name: "NEXT_PUBLIC_CHAT_API",
description:
"The API for the chat endpoint. Set when using a custom backend (e.g. Express). Use full URL like http://localhost:8000/api/chat",
});
}
return result;
return [
{
name: "APP_HOST",
description: "The address to start the backend app.",
value: "0.0.0.0",
},
{
name: "APP_PORT",
description: "The port to start the backend app.",
value: port?.toString() || "8000",
},
// TODO: Once LlamaIndexTS supports string templates, move this to `getEngineEnvs`
{
name: "SYSTEM_PROMPT",
description: `Custom system prompt.
Example:
SYSTEM_PROMPT="
We have provided context information below.
---------------------
{context_str}
---------------------
Given this information, please answer the question: {query_str}
"`,
},
];
};
const getEngineEnvs = (): EnvVar[] => {
@@ -436,181 +216,37 @@ const getEngineEnvs = (): EnvVar[] => {
name: "TOP_K",
description:
"The number of similar embeddings to return when retrieving documents.",
value: "3",
},
];
};
const getToolEnvs = (tools?: Tool[]): EnvVar[] => {
if (!tools?.length) return [];
const toolEnvs: EnvVar[] = [];
tools.forEach((tool) => {
if (tool.envVars?.length) {
toolEnvs.push(
// Don't include the system prompt env var here
// It should be handled separately by merging with the default system prompt
...tool.envVars.filter(
(env) => env.name !== TOOL_SYSTEM_PROMPT_ENV_VAR,
),
);
}
});
return toolEnvs;
};
const getSystemPromptEnv = (
tools?: Tool[],
dataSources?: TemplateDataSource[],
template?: TemplateType,
): EnvVar[] => {
const systemPromptEnv: EnvVar[] = [];
// build tool system prompt by merging all tool system prompts
// multiagent template doesn't need system prompt
if (template !== "multiagent") {
let toolSystemPrompt = "";
tools?.forEach((tool) => {
const toolSystemPromptEnv = tool.envVars?.find(
(env) => env.name === TOOL_SYSTEM_PROMPT_ENV_VAR,
);
if (toolSystemPromptEnv) {
toolSystemPrompt += toolSystemPromptEnv.value + "\n";
}
});
const systemPrompt =
'"' +
DEFAULT_SYSTEM_PROMPT +
(dataSources?.length ? `\n${DATA_SOURCES_PROMPT}` : "") +
(toolSystemPrompt ? `\n${toolSystemPrompt}` : "") +
'"';
systemPromptEnv.push({
name: "SYSTEM_PROMPT",
description: "The system prompt for the AI model.",
value: systemPrompt,
});
}
if (tools?.length == 0 && (dataSources?.length ?? 0 > 0)) {
const citationPrompt = `'You have provided information from a knowledge base that has been passed to you in nodes of information.
Each node has useful metadata such as node ID, file name, page, etc.
Please add the citation to the data node for each sentence or paragraph that you reference in the provided information.
The citation format is: . [citation:<node_id>]()
Where the <node_id> is the unique identifier of the data node.
Example:
We have two nodes:
node_id: xyz
file_name: llama.pdf
node_id: abc
file_name: animal.pdf
User question: Tell me a fun fact about Llama.
Your answer:
A baby llama is called "Cria" [citation:xyz]().
It often live in desert [citation:abc]().
It\\'s cute animal.
'`;
systemPromptEnv.push({
name: "SYSTEM_CITATION_PROMPT",
description:
"An additional system prompt to add citation when responding to user questions.",
value: citationPrompt,
});
}
return systemPromptEnv;
};
const getTemplateEnvs = (template?: TemplateType): EnvVar[] => {
const nextQuestionEnvs: EnvVar[] = [
{
name: "NEXT_QUESTION_PROMPT",
description: `Customize prompt to generate the next question suggestions based on the conversation history.
Disable this prompt to disable the next question suggestions feature.`,
value: `"You're a helpful assistant! Your task is to suggest the next question that user might ask.
Here is the conversation history
---------------------
{conversation}
---------------------
Given the conversation history, please give me 3 questions that user might ask next!
Your answer should be wrapped in three sticks which follows the following format:
\`\`\`
<question 1>
<question 2>
<question 3>
\`\`\`"`,
},
];
if (template === "multiagent" || template === "streaming") {
return nextQuestionEnvs;
}
return [];
};
const getObservabilityEnvs = (
observability?: TemplateObservability,
): EnvVar[] => {
if (observability === "llamatrace") {
return [
{
name: "PHOENIX_API_KEY",
description:
"API key for LlamaTrace observability. Retrieve from https://llamatrace.com/login",
},
];
}
return [];
};
export const createBackendEnvFile = async (
root: string,
opts: Pick<
InstallTemplateArgs,
| "llamaCloudKey"
| "vectorDb"
| "modelConfig"
| "framework"
| "dataSources"
| "template"
| "port"
| "tools"
| "observability"
| "useLlamaParse"
>,
opts: {
llamaCloudKey?: string;
vectorDb?: TemplateVectorDB;
modelConfig: ModelConfig;
framework?: TemplateFramework;
dataSources?: TemplateDataSource[];
port?: number;
},
) => {
// Init env values
const envFileName = ".env";
const envVars: EnvVar[] = [
...(opts.useLlamaParse
? [
{
name: "LLAMA_CLOUD_API_KEY",
description: `The Llama Cloud API key.`,
value: opts.llamaCloudKey,
},
]
: []),
...getVectorDBEnvs(opts.vectorDb, opts.framework, opts.template),
...getToolEnvs(opts.tools),
...getFrameworkEnvs(opts.framework, opts.template, opts.port),
// Add environment variables of each component
...(opts.template === "llamaindexserver"
? [
{
name: "OPENAI_API_KEY",
description: "The OpenAI API key to use.",
value: opts.modelConfig.apiKey,
},
]
: [
// don't use this stuff for llama-indexserver
...getModelEnvs(opts.modelConfig),
...getEngineEnvs(),
...getTemplateEnvs(opts.template),
...getObservabilityEnvs(opts.observability),
...getSystemPromptEnv(opts.tools, opts.dataSources, opts.template),
]),
{
name: "LLAMA_CLOUD_API_KEY",
description: `The Llama Cloud API key.`,
value: opts.llamaCloudKey,
},
// Add model environment variables
...getModelEnvs(opts.modelConfig),
// Add engine environment variables
...getEngineEnvs(),
// Add vector database environment variables
...getVectorDBEnvs(opts.vectorDb),
...getFrameworkEnvs(opts.framework, opts.port),
];
// Render and write env file
const content = renderEnvVar(envVars);
@@ -621,14 +257,16 @@ export const createBackendEnvFile = async (
export const createFrontendEnvFile = async (
root: string,
opts: {
vectorDb?: TemplateVectorDB;
customApiPath?: string;
},
) => {
const defaultFrontendEnvs = [
{
name: "NEXT_PUBLIC_USE_LLAMACLOUD",
description: "Let's the user change indexes in LlamaCloud projects",
value: opts.vectorDb === "llamacloud" ? "true" : "false",
name: "NEXT_PUBLIC_CHAT_API",
description: "The backend API for chat endpoint.",
value: opts.customApiPath
? opts.customApiPath
: "http://localhost:8000/api/chat",
},
];
const content = renderEnvVar(defaultFrontendEnvs);
+62 -116
View File
@@ -1,15 +1,15 @@
import { callPackageManager } from "./install";
import path from "path";
import picocolors, { cyan } from "picocolors";
import { cyan } from "picocolors";
import fsExtra from "fs-extra";
import { writeLoadersConfig } from "./datasources";
import { createBackendEnvFile, createFrontendEnvFile } from "./env-variables";
import { PackageManager } from "./get-pkg-manager";
import { installLlamapackProject } from "./llama-pack";
import { makeDir } from "./make-dir";
import { isHavingPoetryLockFile, tryPoetryRun } from "./poetry";
import { isModelConfigured } from "./providers";
import { installPythonTemplate } from "./python";
import { downloadAndExtractRepo } from "./repo";
import { ConfigFileType, writeToolsConfig } from "./tools";
@@ -23,35 +23,6 @@ import {
} from "./types";
import { installTSTemplate } from "./typescript";
const checkForGenerateScript = (
modelConfig: ModelConfig,
vectorDb?: TemplateVectorDB,
llamaCloudKey?: string,
useLlamaParse?: boolean,
) => {
const missingSettings = [];
if (!modelConfig.isConfigured()) {
missingSettings.push("your model provider API key");
}
const llamaCloudApiKey = llamaCloudKey ?? process.env["LLAMA_CLOUD_API_KEY"];
const isRequiredLlamaCloudKey = useLlamaParse || vectorDb === "llamacloud";
if (isRequiredLlamaCloudKey && !llamaCloudApiKey) {
missingSettings.push("your LLAMA_CLOUD_API_KEY");
}
if (
vectorDb !== undefined &&
vectorDb !== "none" &&
vectorDb !== "llamacloud"
) {
missingSettings.push("your Vector DB environment variables");
}
return missingSettings;
};
// eslint-disable-next-line max-params
async function generateContextData(
framework: TemplateFramework,
@@ -67,15 +38,12 @@ async function generateContextData(
? "poetry run generate"
: `${packageManager} run generate`,
)}`;
const missingSettings = checkForGenerateScript(
modelConfig,
vectorDb,
llamaCloudKey,
useLlamaParse,
);
if (!missingSettings.length) {
const modelConfigured = isModelConfigured(modelConfig);
const llamaCloudKeyConfigured = useLlamaParse
? llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
: true;
const hasVectorDb = vectorDb && vectorDb !== "none";
if (modelConfigured && llamaCloudKeyConfigured && !hasVectorDb) {
// If all the required environment variables are set, run the generate script
if (framework === "fastapi") {
if (isHavingPoetryLockFile()) {
@@ -95,47 +63,30 @@ async function generateContextData(
}
}
const settingsMessage = `After setting ${missingSettings.join(" and ")}, run ${runGenerate} to generate the context data.`;
console.log(picocolors.yellow(`\n${settingsMessage}\n\n`));
// generate the message of what to do to run the generate script manually
const settings = [];
if (!modelConfigured) settings.push("your model provider API key");
if (!llamaCloudKeyConfigured) settings.push("your Llama Cloud key");
if (hasVectorDb) settings.push("your Vector DB environment variables");
const settingsMessage =
settings.length > 0 ? `After setting ${settings.join(" and ")}, ` : "";
const generateMessage = `run ${runGenerate} to generate the context data.`;
console.log(`\n${settingsMessage}${generateMessage}\n\n`);
}
}
const downloadFile = async (url: string, destPath: string) => {
const response = await fetch(url);
const fileBuffer = await response.arrayBuffer();
await fsExtra.writeFile(destPath, Buffer.from(fileBuffer));
};
const prepareContextData = async (
const copyContextData = async (
root: string,
dataSources: TemplateDataSource[],
) => {
await makeDir(path.join(root, "data"));
for (const dataSource of dataSources) {
const dataSourceConfig = dataSource?.config as FileSourceConfig;
// If the path is URLs, download the data and save it to the data directory
if ("url" in dataSourceConfig) {
console.log(
"Downloading file from URL:",
dataSourceConfig.url.toString(),
);
const destPath = path.join(
root,
"data",
dataSourceConfig.filename ??
path.basename(dataSourceConfig.url.toString()),
);
await downloadFile(dataSourceConfig.url.toString(), destPath);
} else {
// Copy local data
console.log("Copying data from path:", dataSourceConfig.path);
const destPath = path.join(
root,
"data",
path.basename(dataSourceConfig.path),
);
await fsExtra.copy(dataSourceConfig.path, destPath);
}
// Copy local data
const dataPath = dataSourceConfig.path;
const destPath = path.join(root, "data", path.basename(dataPath));
console.log("Copying data from path:", dataPath);
await fsExtra.copy(dataPath, destPath);
}
};
@@ -170,65 +121,60 @@ export const installTemplate = async (
if (props.framework === "fastapi") {
await installPythonTemplate(props);
// write loaders configuration (currently Python only)
await writeLoadersConfig(
props.root,
props.dataSources,
props.useLlamaParse,
);
} else {
await installTSTemplate(props);
}
// write configurations
if (props.template !== "llamaindexserver") {
await writeToolsConfig(
props.root,
props.tools,
props.framework === "fastapi" ? ConfigFileType.YAML : ConfigFileType.JSON,
);
if (props.vectorDb !== "llamacloud") {
// write loaders configuration (currently Python only)
// not needed for LlamaCloud as it has its own loaders
await writeLoadersConfig(
props.root,
props.dataSources,
props.useLlamaParse,
);
}
}
// write tools configuration
await writeToolsConfig(
props.root,
props.tools,
props.framework === "fastapi" ? ConfigFileType.YAML : ConfigFileType.JSON,
);
if (props.backend) {
// This is a backend, so we need to copy the test data and create the env file.
// Copy the environment file to the target directory.
if (props.template !== "community" && props.template !== "llamapack") {
await createBackendEnvFile(props.root, props);
}
await createBackendEnvFile(props.root, {
modelConfig: props.modelConfig,
llamaCloudKey: props.llamaCloudKey,
vectorDb: props.vectorDb,
framework: props.framework,
dataSources: props.dataSources,
port: props.externalPort,
});
await prepareContextData(
props.root,
props.dataSources.filter((ds) => ds.type === "file"),
);
if (
props.dataSources.length > 0 &&
(props.postInstallAction === "runApp" ||
props.postInstallAction === "dependencies")
) {
if (props.dataSources.length > 0) {
console.log("\nGenerating context data...\n");
await generateContextData(
props.framework,
props.modelConfig,
props.packageManager,
props.vectorDb,
props.llamaCloudKey,
props.useLlamaParse,
await copyContextData(
props.root,
props.dataSources.filter((ds) => ds.type === "file"),
);
if (
props.postInstallAction === "runApp" ||
props.postInstallAction === "dependencies"
) {
await generateContextData(
props.framework,
props.modelConfig,
props.packageManager,
props.vectorDb,
props.llamaCloudKey,
props.useLlamaParse,
);
}
}
// Create outputs directory
await makeDir(path.join(props.root, "output/tools"));
await makeDir(path.join(props.root, "output/uploaded"));
await makeDir(path.join(props.root, "output/llamacloud"));
} else {
// this is a frontend for a full-stack app, create .env file with model information
await createFrontendEnvFile(props.root, {
vectorDb: props.vectorDb,
customApiPath: props.customApiPath,
});
}
};
-103
View File
@@ -1,103 +0,0 @@
import prompts from "prompts";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions/utils";
const MODELS = [
"claude-3-opus",
"claude-3-sonnet",
"claude-3-haiku",
"claude-2.1",
"claude-instant-1.2",
];
const DEFAULT_MODEL = MODELS[0];
// TODO: get embedding vector dimensions from the anthropic sdk (currently not supported)
// Use huggingface embedding models for now
enum HuggingFaceEmbeddingModelType {
XENOVA_ALL_MINILM_L6_V2 = "all-MiniLM-L6-v2",
XENOVA_ALL_MPNET_BASE_V2 = "all-mpnet-base-v2",
}
type ModelData = {
dimensions: number;
};
const EMBEDDING_MODELS: Record<HuggingFaceEmbeddingModelType, ModelData> = {
[HuggingFaceEmbeddingModelType.XENOVA_ALL_MINILM_L6_V2]: {
dimensions: 384,
},
[HuggingFaceEmbeddingModelType.XENOVA_ALL_MPNET_BASE_V2]: {
dimensions: 768,
},
};
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
type AnthropicQuestionsParams = {
apiKey?: string;
askModels: boolean;
};
export async function askAnthropicQuestions({
askModels,
apiKey,
}: AnthropicQuestionsParams): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: DEFAULT_DIMENSIONS,
isConfigured(): boolean {
if (config.apiKey) {
return true;
}
if (process.env["ANTHROPIC_API_KEY"]) {
return true;
}
return false;
},
};
if (!config.apiKey) {
const { key } = await prompts(
{
type: "text",
name: "key",
message:
"Please provide your Anthropic API key (or leave blank to use ANTHROPIC_API_KEY env variable):",
},
questionHandlers,
);
config.apiKey = key || process.env.ANTHROPIC_API_KEY;
}
if (askModels) {
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: MODELS.map(toChoice),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions =
EMBEDDING_MODELS[
embeddingModel as HuggingFaceEmbeddingModelType
].dimensions;
}
return config;
}
-115
View File
@@ -1,115 +0,0 @@
import prompts from "prompts";
import { ModelConfigParams, ModelConfigQuestionsParams } from ".";
import { questionHandlers } from "../../questions/utils";
const ALL_AZURE_OPENAI_CHAT_MODELS: Record<string, { openAIModel: string }> = {
"gpt-35-turbo": { openAIModel: "gpt-3.5-turbo" },
"gpt-35-turbo-16k": {
openAIModel: "gpt-3.5-turbo-16k",
},
"gpt-4o": { openAIModel: "gpt-4o" },
"gpt-4o-mini": { openAIModel: "gpt-4o-mini" },
"gpt-4": { openAIModel: "gpt-4" },
"gpt-4-32k": { openAIModel: "gpt-4-32k" },
"gpt-4-turbo": {
openAIModel: "gpt-4-turbo",
},
"gpt-4-turbo-2024-04-09": {
openAIModel: "gpt-4-turbo",
},
"gpt-4-vision-preview": {
openAIModel: "gpt-4-vision-preview",
},
"gpt-4-1106-preview": {
openAIModel: "gpt-4-1106-preview",
},
"gpt-4o-2024-05-13": {
openAIModel: "gpt-4o-2024-05-13",
},
"gpt-4o-mini-2024-07-18": {
openAIModel: "gpt-4o-mini-2024-07-18",
},
};
const ALL_AZURE_OPENAI_EMBEDDING_MODELS: Record<
string,
{
dimensions: number;
openAIModel: string;
}
> = {
"text-embedding-3-small": {
dimensions: 1536,
openAIModel: "text-embedding-3-small",
},
"text-embedding-3-large": {
dimensions: 3072,
openAIModel: "text-embedding-3-large",
},
};
const DEFAULT_MODEL = "gpt-4o";
const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large";
export async function askAzureQuestions({
openAiKey,
askModels,
}: ModelConfigQuestionsParams): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey: openAiKey || process.env.AZURE_OPENAI_KEY,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: getDimensions(DEFAULT_EMBEDDING_MODEL),
isConfigured(): boolean {
// the Azure model provider can't be fully configured as endpoint and deployment names have to be configured with env variables
return false;
},
};
if (askModels) {
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: getAvailableModelChoices(),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: getAvailableEmbeddingModelChoices(),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions = getDimensions(embeddingModel);
}
return config;
}
function getAvailableModelChoices() {
return Object.keys(ALL_AZURE_OPENAI_CHAT_MODELS).map((key) => ({
title: key,
value: key,
}));
}
function getAvailableEmbeddingModelChoices() {
return Object.keys(ALL_AZURE_OPENAI_EMBEDDING_MODELS).map((key) => ({
title: key,
value: key,
}));
}
function getDimensions(modelName: string) {
return ALL_AZURE_OPENAI_EMBEDDING_MODELS[modelName].dimensions;
}
-84
View File
@@ -1,84 +0,0 @@
import prompts from "prompts";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions/utils";
const MODELS = ["gemini-1.5-pro-latest", "gemini-pro", "gemini-pro-vision"];
type ModelData = {
dimensions: number;
};
const EMBEDDING_MODELS: Record<string, ModelData> = {
"embedding-001": { dimensions: 768 },
"text-embedding-004": { dimensions: 768 },
};
const DEFAULT_MODEL = MODELS[0];
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
type GeminiQuestionsParams = {
apiKey?: string;
askModels: boolean;
};
export async function askGeminiQuestions({
askModels,
apiKey,
}: GeminiQuestionsParams): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: DEFAULT_DIMENSIONS,
isConfigured(): boolean {
if (config.apiKey) {
return true;
}
if (process.env["GOOGLE_API_KEY"]) {
return true;
}
return false;
},
};
if (!config.apiKey) {
const { key } = await prompts(
{
type: "text",
name: "key",
message:
"Please provide your Google API key (or leave blank to use GOOGLE_API_KEY env variable):",
},
questionHandlers,
);
config.apiKey = key || process.env.GOOGLE_API_KEY;
}
if (askModels) {
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: MODELS.map(toChoice),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
}
return config;
}
-145
View File
@@ -1,145 +0,0 @@
import prompts from "prompts";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions/utils";
import got from "got";
import ora from "ora";
import { red } from "picocolors";
const GROQ_API_URL = "https://api.groq.com/openai/v1";
async function getAvailableModelChoicesGroq(apiKey: string) {
if (!apiKey) {
throw new Error("Need Groq API key to retrieve model choices");
}
const spinner = ora("Fetching available models from Groq").start();
try {
const response = await got(`${GROQ_API_URL}/models`, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
timeout: 5000,
responseType: "json",
});
const data: any = await response.body;
spinner.stop();
// Filter out the Whisper models
return data.data
.filter((model: any) => !model.id.toLowerCase().includes("whisper"))
.map((el: any) => {
return {
title: el.id,
value: el.id,
};
});
} catch (error: unknown) {
spinner.stop();
console.log(error);
if ((error as any).response?.statusCode === 401) {
console.log(
red(
"Invalid Groq API key provided! Please provide a valid key and try again!",
),
);
} else {
console.log(red("Request failed: " + error));
}
process.exit(1);
}
}
const DEFAULT_MODEL = "llama3-70b-8192";
// Use huggingface embedding models for now as Groq doesn't support embedding models
enum HuggingFaceEmbeddingModelType {
XENOVA_ALL_MINILM_L6_V2 = "all-MiniLM-L6-v2",
XENOVA_ALL_MPNET_BASE_V2 = "all-mpnet-base-v2",
}
type ModelData = {
dimensions: number;
};
const EMBEDDING_MODELS: Record<HuggingFaceEmbeddingModelType, ModelData> = {
[HuggingFaceEmbeddingModelType.XENOVA_ALL_MINILM_L6_V2]: {
dimensions: 384,
},
[HuggingFaceEmbeddingModelType.XENOVA_ALL_MPNET_BASE_V2]: {
dimensions: 768,
},
};
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
type GroqQuestionsParams = {
apiKey?: string;
askModels: boolean;
};
export async function askGroqQuestions({
askModels,
apiKey,
}: GroqQuestionsParams): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: DEFAULT_DIMENSIONS,
isConfigured(): boolean {
if (config.apiKey) {
return true;
}
if (process.env["GROQ_API_KEY"]) {
return true;
}
return false;
},
};
if (!config.apiKey) {
const { key } = await prompts(
{
type: "text",
name: "key",
message:
"Please provide your Groq API key (or leave blank to use GROQ_API_KEY env variable):",
},
questionHandlers,
);
config.apiKey = key || process.env.GROQ_API_KEY;
}
if (askModels) {
const modelChoices = await getAvailableModelChoicesGroq(config.apiKey!);
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: modelChoices,
initial: 0,
},
questionHandlers,
);
config.model = model;
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions =
EMBEDDING_MODELS[
embeddingModel as HuggingFaceEmbeddingModelType
].dimensions;
}
return config;
}
-68
View File
@@ -1,68 +0,0 @@
import prompts from "prompts";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions/utils";
const MODELS = ["HuggingFaceH4/zephyr-7b-alpha"];
type ModelData = {
dimensions: number;
};
const EMBEDDING_MODELS: Record<string, ModelData> = {
"BAAI/bge-small-en-v1.5": { dimensions: 384 },
"BAAI/bge-base-en-v1.5": { dimensions: 768 },
"BAAI/bge-large-en-v1.5": { dimensions: 1024 },
"sentence-transformers/all-MiniLM-L6-v2": { dimensions: 384 },
"sentence-transformers/all-mpnet-base-v2": { dimensions: 768 },
"intfloat/multilingual-e5-large": { dimensions: 1024 },
"mixedbread-ai/mxbai-embed-large-v1": { dimensions: 1024 },
"nomic-ai/nomic-embed-text-v1.5": { dimensions: 768 },
};
const DEFAULT_MODEL = MODELS[0];
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
type HuggingfaceQuestionsParams = {
askModels: boolean;
};
export async function askHuggingfaceQuestions({
askModels,
}: HuggingfaceQuestionsParams): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: DEFAULT_DIMENSIONS,
isConfigured(): boolean {
return true;
},
};
if (askModels) {
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which Hugging Face model would you like to use?",
choices: MODELS.map(toChoice),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
}
return config;
}
+21 -49
View File
@@ -1,22 +1,15 @@
import ciInfo from "ci-info";
import prompts from "prompts";
import { questionHandlers } from "../../questions/utils";
import { ModelConfig, ModelProvider, TemplateFramework } from "../types";
import { askAnthropicQuestions } from "./anthropic";
import { askAzureQuestions } from "./azure";
import { askGeminiQuestions } from "./gemini";
import { askGroqQuestions } from "./groq";
import { askHuggingfaceQuestions } from "./huggingface";
import { askLLMHubQuestions } from "./llmhub";
import { askMistralQuestions } from "./mistral";
import { questionHandlers } from "../../questions";
import { ModelConfig, ModelProvider } from "../types";
import { askOllamaQuestions } from "./ollama";
import { askOpenAIQuestions } from "./openai";
import { askOpenAIQuestions, isOpenAIConfigured } from "./openai";
const DEFAULT_MODEL_PROVIDER = "openai";
export type ModelConfigQuestionsParams = {
openAiKey?: string;
askModels: boolean;
framework?: TemplateFramework;
};
export type ModelConfigParams = Omit<ModelConfig, "provider">;
@@ -24,30 +17,21 @@ export type ModelConfigParams = Omit<ModelConfig, "provider">;
export async function askModelConfig({
askModels,
openAiKey,
framework,
}: ModelConfigQuestionsParams): Promise<ModelConfig> {
let modelProvider: ModelProvider = DEFAULT_MODEL_PROVIDER;
if (askModels) {
let choices = [
{ title: "OpenAI", value: "openai" },
{ title: "Groq", value: "groq" },
{ title: "Ollama", value: "ollama" },
{ title: "Anthropic", value: "anthropic" },
{ title: "Gemini", value: "gemini" },
{ title: "Mistral", value: "mistral" },
{ title: "AzureOpenAI", value: "azure-openai" },
];
if (framework === "fastapi") {
choices.push({ title: "T-Systems", value: "t-systems" });
choices.push({ title: "Huggingface", value: "huggingface" });
}
if (askModels && !ciInfo.isCI) {
const { provider } = await prompts(
{
type: "select",
name: "provider",
message: "Which model provider would you like to use",
choices: choices,
choices: [
{
title: "OpenAI",
value: "openai",
},
{ title: "Ollama", value: "ollama" },
],
initial: 0,
},
questionHandlers,
@@ -60,27 +44,6 @@ export async function askModelConfig({
case "ollama":
modelConfig = await askOllamaQuestions({ askModels });
break;
case "groq":
modelConfig = await askGroqQuestions({ askModels });
break;
case "anthropic":
modelConfig = await askAnthropicQuestions({ askModels });
break;
case "gemini":
modelConfig = await askGeminiQuestions({ askModels });
break;
case "mistral":
modelConfig = await askMistralQuestions({ askModels });
break;
case "azure-openai":
modelConfig = await askAzureQuestions({ askModels });
break;
case "t-systems":
modelConfig = await askLLMHubQuestions({ askModels });
break;
case "huggingface":
modelConfig = await askHuggingfaceQuestions({ askModels });
break;
default:
modelConfig = await askOpenAIQuestions({
openAiKey,
@@ -92,3 +55,12 @@ export async function askModelConfig({
provider: modelProvider,
};
}
export function isModelConfigured(modelConfig: ModelConfig): boolean {
switch (modelConfig.provider) {
case "openai":
return isOpenAIConfigured(modelConfig);
default:
return true;
}
}
-166
View File
@@ -1,166 +0,0 @@
import got from "got";
import ora from "ora";
import { red } from "picocolors";
import prompts from "prompts";
import { ModelConfigParams } from ".";
import { questionHandlers } from "../../questions/utils";
export const TSYSTEMS_LLMHUB_API_URL =
"https://llm-server.llmhub.t-systems.net/v2";
const DEFAULT_MODEL = "gpt-3.5-turbo";
const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large";
const LLMHUB_MODELS = [
"gpt-35-turbo",
"gpt-4-32k-1",
"gpt-4-32k-canada",
"gpt-4-32k-france",
"gpt-4-turbo-128k-france",
"Llama2-70b-Instruct",
"Llama-3-70B-Instruct",
"Mixtral-8x7B-Instruct-v0.1",
"mistral-large-32k-france",
"CodeLlama-2",
];
const LLMHUB_EMBEDDING_MODELS = [
"text-embedding-ada-002",
"text-embedding-ada-002-france",
"jina-embeddings-v2-base-de",
"jina-embeddings-v2-base-code",
"text-embedding-bge-m3",
];
type LLMHubQuestionsParams = {
apiKey?: string;
askModels: boolean;
};
export async function askLLMHubQuestions({
askModels,
apiKey,
}: LLMHubQuestionsParams): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: getDimensions(DEFAULT_EMBEDDING_MODEL),
isConfigured(): boolean {
if (config.apiKey) {
return true;
}
if (process.env["T_SYSTEMS_LLMHUB_API_KEY"]) {
return true;
}
return false;
},
};
if (!config.apiKey) {
const { key } = await prompts(
{
type: "text",
name: "key",
message: askModels
? "Please provide your LLMHub API key (or leave blank to use T_SYSTEMS_LLMHUB_API_KEY env variable):"
: "Please provide your LLMHub API key (leave blank to skip):",
validate: (value: string) => {
if (askModels && !value) {
if (process.env.T_SYSTEMS_LLMHUB_API_KEY) {
return true;
}
return "T_SYSTEMS_LLMHUB_API_KEY env variable is not set - key is required";
}
return true;
},
},
questionHandlers,
);
config.apiKey = key || process.env.T_SYSTEMS_LLMHUB_API_KEY;
}
if (askModels) {
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: await getAvailableModelChoices(false, config.apiKey),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: await getAvailableModelChoices(true, config.apiKey),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions = getDimensions(embeddingModel);
}
return config;
}
async function getAvailableModelChoices(
selectEmbedding: boolean,
apiKey?: string,
) {
if (!apiKey) {
throw new Error("Need LLMHub key to retrieve model choices");
}
const isLLMModel = (modelId: string) => {
return LLMHUB_MODELS.includes(modelId);
};
const isEmbeddingModel = (modelId: string) => {
return LLMHUB_EMBEDDING_MODELS.includes(modelId);
};
const spinner = ora("Fetching available models").start();
try {
const response = await got(`${TSYSTEMS_LLMHUB_API_URL}/models`, {
headers: {
Authorization: "Bearer " + apiKey,
},
timeout: 5000,
responseType: "json",
});
const data: any = await response.body;
spinner.stop();
return data.data
.filter((model: any) =>
selectEmbedding ? isEmbeddingModel(model.id) : isLLMModel(model.id),
)
.map((el: any) => {
return {
title: el.id,
value: el.id,
};
});
} catch (error) {
spinner.stop();
if ((error as any).response?.statusCode === 401) {
console.log(
red(
"Invalid LLMHub API key provided! Please provide a valid key and try again!",
),
);
} else {
console.log(red("Request failed: " + error));
}
process.exit(1);
}
}
function getDimensions(modelName: string) {
// Assuming dimensions similar to OpenAI for simplicity. Update if different.
return modelName === "text-embedding-004" ? 768 : 1536;
}
-83
View File
@@ -1,83 +0,0 @@
import prompts from "prompts";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions/utils";
const MODELS = ["mistral-tiny", "mistral-small", "mistral-medium"];
type ModelData = {
dimensions: number;
};
const EMBEDDING_MODELS: Record<string, ModelData> = {
"mistral-embed": { dimensions: 1024 },
};
const DEFAULT_MODEL = MODELS[0];
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
type MistralQuestionsParams = {
apiKey?: string;
askModels: boolean;
};
export async function askMistralQuestions({
askModels,
apiKey,
}: MistralQuestionsParams): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: DEFAULT_DIMENSIONS,
isConfigured(): boolean {
if (config.apiKey) {
return true;
}
if (process.env["MISTRAL_API_KEY"]) {
return true;
}
return false;
},
};
if (!config.apiKey) {
const { key } = await prompts(
{
type: "text",
name: "key",
message:
"Please provide your Mistral API key (or leave blank to use MISTRAL_API_KEY env variable):",
},
questionHandlers,
);
config.apiKey = key || process.env.MISTRAL_API_KEY;
}
if (askModels) {
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: MODELS.map(toChoice),
initial: 0,
},
questionHandlers,
);
config.model = model;
const { embeddingModel } = await prompts(
{
type: "select",
name: "embeddingModel",
message: "Which embedding model would you like to use?",
choices: Object.keys(EMBEDDING_MODELS).map(toChoice),
initial: 0,
},
questionHandlers,
);
config.embeddingModel = embeddingModel;
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
}
return config;
}
+5 -5
View File
@@ -1,8 +1,9 @@
import ciInfo from "ci-info";
import ollama, { type ModelResponse } from "ollama";
import { red } from "picocolors";
import prompts from "prompts";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions/utils";
import { questionHandlers, toChoice } from "../../questions";
type ModelData = {
dimensions: number;
@@ -28,12 +29,11 @@ export async function askOllamaQuestions({
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: EMBEDDING_MODELS[DEFAULT_EMBEDDING_MODEL].dimensions,
isConfigured(): boolean {
return true;
},
};
if (askModels) {
// use default model values in CI or if user should not be asked
const useDefaults = ciInfo.isCI || !askModels;
if (!useDefaults) {
const { model } = await prompts(
{
type: "select",
+18 -14
View File
@@ -1,14 +1,14 @@
import ciInfo from "ci-info";
import got from "got";
import ora from "ora";
import { red } from "picocolors";
import prompts from "prompts";
import { ModelConfigParams, ModelConfigQuestionsParams } from ".";
import { isCI } from "../../questions";
import { questionHandlers } from "../../questions/utils";
import { questionHandlers } from "../../questions";
const OPENAI_API_URL = "https://api.openai.com/v1";
const DEFAULT_MODEL = "gpt-4o-mini";
const DEFAULT_MODEL = "gpt-4-turbo";
const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large";
export async function askOpenAIQuestions({
@@ -20,18 +20,9 @@ export async function askOpenAIQuestions({
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: getDimensions(DEFAULT_EMBEDDING_MODEL),
isConfigured(): boolean {
if (config.apiKey) {
return true;
}
if (process.env["OPENAI_API_KEY"]) {
return true;
}
return false;
},
};
if (!config.apiKey && !isCI) {
if (!config.apiKey) {
const { key } = await prompts(
{
type: "text",
@@ -40,6 +31,7 @@ export async function askOpenAIQuestions({
? "Please provide your OpenAI API key (or leave blank to use OPENAI_API_KEY env variable):"
: "Please provide your OpenAI API key (leave blank to skip):",
validate: (value: string) => {
console.log(value);
if (askModels && !value) {
if (process.env.OPENAI_API_KEY) {
return true;
@@ -54,7 +46,9 @@ export async function askOpenAIQuestions({
config.apiKey = key || process.env.OPENAI_API_KEY;
}
if (askModels) {
// use default model values in CI or if user should not be asked
const useDefaults = ciInfo.isCI || !askModels;
if (!useDefaults) {
const { model } = await prompts(
{
type: "select",
@@ -84,6 +78,16 @@ export async function askOpenAIQuestions({
return config;
}
export function isOpenAIConfigured(params: ModelConfigParams): boolean {
if (params.apiKey) {
return true;
}
if (process.env["OPENAI_API_KEY"]) {
return true;
}
return false;
}
async function getAvailableModelChoices(
selectEmbedding: boolean,
apiKey?: string,
-8
View File
@@ -1,8 +0,0 @@
/* Function to conditionally load the global-agent/bootstrap module */
export async function initializeGlobalAgent() {
if (process.env.GLOBAL_AGENT_HTTP_PROXY) {
/* Dynamically import global-agent/bootstrap */
await import("global-agent/bootstrap");
console.log("Proxy enabled via global-agent.");
}
}
+87 -425
View File
@@ -12,8 +12,6 @@ import {
InstallTemplateArgs,
ModelConfig,
TemplateDataSource,
TemplateObservability,
TemplateType,
TemplateVectorDB,
} from "./types";
@@ -21,16 +19,13 @@ interface Dependency {
name: string;
version?: string;
extras?: string[];
constraints?: Record<string, string>;
}
const getAdditionalDependencies = (
modelConfig: ModelConfig,
vectorDb?: TemplateVectorDB,
dataSources?: TemplateDataSource[],
dataSource?: TemplateDataSource,
tools?: Tool[],
templateType?: TemplateType,
observability?: TemplateObservability,
) => {
const dependencies: Dependency[] = [];
@@ -39,115 +34,76 @@ const getAdditionalDependencies = (
case "mongo": {
dependencies.push({
name: "llama-index-vector-stores-mongodb",
version: "^0.6.0",
version: "^0.1.3",
});
break;
}
case "pg": {
dependencies.push({
name: "llama-index-vector-stores-postgres",
version: "^0.3.2",
version: "^0.1.1",
});
break;
}
case "pinecone": {
dependencies.push({
name: "llama-index-vector-stores-pinecone",
version: "^0.4.1",
constraints: {
python: ">=3.11,<3.13",
},
version: "^0.1.3",
});
break;
}
case "milvus": {
dependencies.push({
name: "llama-index-vector-stores-milvus",
version: "^0.3.0",
version: "^0.1.6",
});
dependencies.push({
name: "pymilvus",
version: "2.4.4",
version: "2.3.7",
});
break;
}
case "astra": {
dependencies.push({
name: "llama-index-vector-stores-astra-db",
version: "^0.4.0",
version: "^0.1.5",
});
break;
}
case "qdrant": {
dependencies.push({
name: "llama-index-vector-stores-qdrant",
version: "^0.4.0",
constraints: {
python: ">=3.11,<3.13",
},
});
break;
}
case "chroma": {
dependencies.push({
name: "llama-index-vector-stores-chroma",
version: "^0.4.0",
});
break;
}
case "weaviate": {
dependencies.push({
name: "llama-index-vector-stores-weaviate",
version: "^1.2.3",
});
break;
}
case "llamacloud":
dependencies.push({
name: "llama-index-indices-managed-llama-cloud",
version: "0.6.3",
});
break;
}
// Add data source dependencies
if (dataSources) {
for (const ds of dataSources) {
const dsType = ds?.type;
switch (dsType) {
case "file":
dependencies.push({
name: "docx2txt",
version: "^0.8",
});
break;
case "web":
dependencies.push({
name: "llama-index-readers-web",
version: "^0.3.0",
});
break;
case "db":
dependencies.push({
name: "llama-index-readers-database",
version: "^0.3.0",
});
dependencies.push({
name: "pymysql",
version: "^1.1.0",
extras: ["rsa"],
});
dependencies.push({
name: "psycopg2-binary",
version: "^2.9.9",
});
break;
}
}
const dataSourceType = dataSource?.type;
switch (dataSourceType) {
case "file":
dependencies.push({
name: "docx2txt",
version: "^0.8",
});
break;
case "web":
dependencies.push({
name: "llama-index-readers-web",
version: "^0.1.6",
});
break;
case "db":
dependencies.push({
name: "llama-index-readers-database",
version: "^0.1.3",
});
dependencies.push({
name: "pymysql",
version: "^1.1.0",
extras: ["rsa"],
});
dependencies.push({
name: "psycopg2",
version: "^2.9.9",
});
break;
}
// Add tools dependencies
console.log("Adding tools dependencies");
tools?.forEach((tool) => {
tool.dependencies?.forEach((dep) => {
dependencies.push(dep);
@@ -158,139 +114,27 @@ const getAdditionalDependencies = (
case "ollama":
dependencies.push({
name: "llama-index-llms-ollama",
version: "0.3.0",
version: "0.1.2",
});
dependencies.push({
name: "llama-index-embeddings-ollama",
version: "0.3.0",
version: "0.1.2",
});
break;
case "openai":
if (templateType !== "multiagent") {
dependencies.push({
name: "llama-index-llms-openai",
version: "^0.3.2",
});
dependencies.push({
name: "llama-index-embeddings-openai",
version: "^0.3.1",
});
dependencies.push({
name: "llama-index-agent-openai",
version: "^0.4.0",
});
}
break;
case "groq":
// Fastembed==0.2.0 does not support python3.13 at the moment
// Fixed the python version less than 3.13
dependencies.push({
name: "python",
version: "^3.11,<3.13",
});
dependencies.push({
name: "llama-index-llms-groq",
version: "0.2.0",
});
dependencies.push({
name: "llama-index-embeddings-fastembed",
version: "^0.2.0",
});
break;
case "anthropic":
// Fastembed==0.2.0 does not support python3.13 at the moment
// Fixed the python version less than 3.13
dependencies.push({
name: "python",
version: "^3.11,<3.13",
});
dependencies.push({
name: "llama-index-llms-anthropic",
version: "0.3.0",
});
dependencies.push({
name: "llama-index-embeddings-fastembed",
version: "^0.2.0",
});
break;
case "gemini":
dependencies.push({
name: "llama-index-llms-gemini",
version: "0.3.4",
});
dependencies.push({
name: "llama-index-embeddings-gemini",
version: "^0.2.0",
});
break;
case "mistral":
dependencies.push({
name: "llama-index-llms-mistralai",
version: "0.2.1",
});
dependencies.push({
name: "llama-index-embeddings-mistralai",
version: "0.2.0",
});
break;
case "azure-openai":
dependencies.push({
name: "llama-index-llms-azure-openai",
version: "0.2.0",
});
dependencies.push({
name: "llama-index-embeddings-azure-openai",
version: "0.2.4",
});
break;
case "huggingface":
dependencies.push({
name: "llama-index-llms-huggingface",
version: "^0.3.5",
});
dependencies.push({
name: "llama-index-embeddings-huggingface",
version: "^0.3.1",
});
dependencies.push({
name: "optimum",
version: "^1.23.3",
extras: ["onnxruntime"],
});
break;
case "t-systems":
dependencies.push({
name: "llama-index-agent-openai",
version: "0.3.0",
});
dependencies.push({
name: "llama-index-llms-openai-like",
version: "0.2.0",
version: "0.2.2",
});
break;
}
if (observability && observability !== "none") {
if (observability === "traceloop") {
dependencies.push({
name: "traceloop-sdk",
version: "^0.15.11",
});
}
if (observability === "llamatrace") {
dependencies.push({
name: "llama-index-callbacks-arize-phoenix",
version: "^0.3.0",
});
}
}
return dependencies;
};
const mergePoetryDependencies = (
dependencies: Dependency[],
existingDependencies: Record<string, Omit<Dependency, "name"> | string>,
existingDependencies: Record<string, Omit<Dependency, "name">>,
) => {
for (const dependency of dependencies) {
let value = existingDependencies[dependency.name] ?? {};
@@ -303,35 +147,13 @@ const mergePoetryDependencies = (
value.version = dependency.version ?? value.version;
value.extras = dependency.extras ?? value.extras;
// Merge constraints if they exist
if (dependency.constraints) {
value = { ...value, ...dependency.constraints };
}
if (value.version === undefined) {
throw new Error(
`Dependency "${dependency.name}" is missing attribute "version"!`,
);
}
// Serialize as object if there are any additional properties
if (Object.keys(value).length > 1) {
existingDependencies[dependency.name] = value;
} else {
// Otherwise, serialize just the version string
existingDependencies[dependency.name] = value.version;
}
}
};
const copyRouterCode = async (root: string, tools: Tool[]) => {
// Copy sandbox router if the artifact tool is selected
if (tools?.some((t) => t.name === "artifact")) {
await copy("sandbox.py", path.join(root, "app", "api", "routers"), {
parents: true,
cwd: path.join(templatesDir, "components", "routers", "python"),
rename: assetRelocator,
});
existingDependencies[dependency.name] = value;
}
};
@@ -396,24 +218,36 @@ export const installPythonDependencies = (
}
};
const installLegacyPythonTemplate = async ({
export const installPythonTemplate = async ({
root,
template,
framework,
vectorDb,
dataSources,
tools,
useCase,
postInstallAction,
observability,
modelConfig,
}: Pick<
InstallTemplateArgs,
| "root"
| "framework"
| "template"
| "vectorDb"
| "dataSources"
| "tools"
| "useCase"
| "postInstallAction"
| "observability"
| "modelConfig"
>) => {
console.log("\nInitializing Python project with template:", template, "\n");
const templatePath = path.join(templatesDir, "types", template, framework);
await copy("**", root, {
parents: true,
cwd: templatePath,
rename: assetRelocator,
});
const compPath = path.join(templatesDir, "components");
const enginePath = path.join(root, "app", "engine");
@@ -423,229 +257,57 @@ const installLegacyPythonTemplate = async ({
cwd: path.join(compPath, "vectordbs", "python", vectorDb ?? "none"),
});
if (vectorDb !== "llamacloud") {
// Copy all loaders to enginePath
// Not needed for LlamaCloud as it has its own loaders
const loaderPath = path.join(enginePath, "loaders");
await copy("**", loaderPath, {
parents: true,
cwd: path.join(compPath, "loaders", "python"),
});
}
// Copy settings.py to app
await copy("**", path.join(root, "app"), {
cwd: path.join(compPath, "settings", "python"),
// Copy all loaders to enginePath
const loaderPath = path.join(enginePath, "loaders");
await copy("**", loaderPath, {
parents: true,
cwd: path.join(compPath, "loaders", "python"),
});
// Copy services
if (template == "streaming" || template == "multiagent") {
await copy("**", path.join(root, "app", "api", "services"), {
cwd: path.join(compPath, "services", "python"),
});
// Select and copy engine code based on data sources and tools
let engine;
tools = tools ?? [];
if (dataSources.length > 0 && tools.length === 0) {
console.log("\nNo tools selected - use optimized context chat engine\n");
engine = "chat";
} else {
engine = "agent";
}
// Copy engine code
if (template === "streaming" || template === "multiagent") {
// Select and copy engine code based on data sources and tools
let engine;
// Multiagent always uses agent engine
if (template === "multiagent") {
engine = "agent";
} else {
// For streaming, use chat engine by default
// Unless tools are selected, in which case use agent engine
if (dataSources.length > 0 && (!tools || tools.length === 0)) {
console.log(
"\nNo tools selected - use optimized context chat engine\n",
);
engine = "chat";
} else {
engine = "agent";
}
}
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "engines", "python", engine),
});
// Copy engine code
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "engines", "python", engine),
const addOnDependencies = dataSources
.map((ds) => getAdditionalDependencies(modelConfig, vectorDb, ds, tools))
.flat();
if (observability === "opentelemetry") {
addOnDependencies.push({
name: "traceloop-sdk",
version: "^0.15.11",
});
// Copy router code
await copyRouterCode(root, tools ?? []);
}
// Copy multiagents overrides
if (template === "multiagent") {
await copy("**", path.join(root), {
cwd: path.join(compPath, "multiagent", "python"),
});
}
if (template === "multiagent" || template === "reflex") {
if (useCase) {
const sourcePath =
template === "multiagent"
? path.join(compPath, "agents", "python", useCase)
: path.join(compPath, "reflex", useCase);
await copy("**", path.join(root), {
parents: true,
cwd: sourcePath,
rename: assetRelocator,
});
} else {
console.log(
red(
`There is no use case selected for ${template} template. Please pick a use case to use via --use-case flag.`,
),
);
process.exit(1);
}
}
if (observability && observability !== "none") {
const templateObservabilityPath = path.join(
templatesDir,
"components",
"observability",
"python",
observability,
"opentelemetry",
);
await copy("**", path.join(root, "app"), {
cwd: templateObservabilityPath,
});
}
};
const installLlamaIndexServerTemplate = async ({
root,
useCase,
useLlamaParse,
}: Pick<InstallTemplateArgs, "root" | "useCase" | "useLlamaParse">) => {
if (!useCase) {
console.log(
red(
`There is no use case selected. Please pick a use case to use via --use-case flag.`,
),
);
process.exit(1);
}
await copy("workflow.py", path.join(root, "app"), {
parents: true,
cwd: path.join(templatesDir, "components", "workflows", "python", useCase),
});
// Copy custom UI component code
await copy(`*`, path.join(root, "components"), {
parents: true,
cwd: path.join(templatesDir, "components", "ui", "workflows", useCase),
});
if (useLlamaParse) {
await copy("index.py", path.join(root, "app"), {
parents: true,
cwd: path.join(
templatesDir,
"components",
"vectordbs",
"llamaindexserver",
"llamacloud",
"python",
),
});
// TODO: Consider moving generate.py to app folder.
await copy("generate.py", path.join(root), {
parents: true,
cwd: path.join(
templatesDir,
"components",
"vectordbs",
"llamaindexserver",
"llamacloud",
"python",
),
});
}
// Copy README.md
await copy("README-template.md", path.join(root), {
parents: true,
cwd: path.join(templatesDir, "components", "workflows", "python", useCase),
rename: assetRelocator,
});
};
export const installPythonTemplate = async ({
appName,
root,
template,
framework,
vectorDb,
postInstallAction,
modelConfig,
dataSources,
tools,
useLlamaParse,
useCase,
observability,
}: Pick<
InstallTemplateArgs,
| "appName"
| "root"
| "template"
| "framework"
| "vectorDb"
| "postInstallAction"
| "modelConfig"
| "dataSources"
| "tools"
| "useLlamaParse"
| "useCase"
| "observability"
>) => {
console.log("\nInitializing Python project with template:", template, "\n");
let templatePath;
if (template === "reflex") {
templatePath = path.join(templatesDir, "types", "reflex");
} else {
templatePath = path.join(templatesDir, "types", template, framework);
}
await copy("**", root, {
parents: true,
cwd: templatePath,
rename: assetRelocator,
});
if (template === "llamaindexserver") {
await installLlamaIndexServerTemplate({
root,
useCase,
useLlamaParse,
});
} else {
await installLegacyPythonTemplate({
root,
template,
vectorDb,
dataSources,
tools,
useCase,
observability,
});
}
console.log("Adding additional dependencies");
const addOnDependencies = getAdditionalDependencies(
modelConfig,
vectorDb,
dataSources,
tools,
template,
);
await addDependencies(root, addOnDependencies);
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
installPythonDependencies();
}
// Copy deployment files for python
await copy("**", root, {
cwd: path.join(compPath, "deployments", "python"),
});
};
+73 -66
View File
@@ -1,81 +1,88 @@
import { SpawnOptions, spawn } from "child_process";
import { TemplateFramework, TemplateType } from "./types";
import { ChildProcess, SpawnOptions, spawn } from "child_process";
import path from "path";
import { TemplateFramework } from "./types";
const createProcess = (
command: string,
args: string[],
options: SpawnOptions,
): Promise<void> => {
return new Promise((resolve, reject) => {
spawn(command, args, {
...options,
shell: true,
) => {
return spawn(command, args, {
...options,
shell: true,
})
.on("exit", function (code) {
if (code !== 0) {
console.log(`Child process exited with code=${code}`);
process.exit(1);
}
})
.on("exit", function (code) {
if (code !== 0) {
console.log(`Child process exited with code=${code}`);
reject(code);
} else {
resolve();
}
})
.on("error", function (err) {
console.log("Error when running child process: ", err);
reject(err);
});
});
.on("error", function (err) {
console.log("Error when running chill process: ", err);
process.exit(1);
});
};
export function runReflexApp(appPath: string, port: number) {
const commandArgs = [
"run",
"reflex",
"run",
"--frontend-port",
port.toString(),
];
return createProcess("poetry", commandArgs, {
stdio: "inherit",
cwd: appPath,
});
}
export function runFastAPIApp(appPath: string, port: number) {
return createProcess("poetry", ["run", "dev"], {
stdio: "inherit",
cwd: appPath,
env: { ...process.env, APP_PORT: `${port}` },
});
}
export function runTSApp(appPath: string, port: number) {
return createProcess("npm", ["run", "dev"], {
stdio: "inherit",
cwd: appPath,
env: { ...process.env, PORT: `${port}` },
});
}
// eslint-disable-next-line max-params
export async function runApp(
appPath: string,
template: TemplateType,
frontend: boolean,
framework: TemplateFramework,
port?: number,
): Promise<void> {
try {
// Start the app
const defaultPort =
framework === "nextjs" || template === "reflex" ? 3000 : 8000;
externalPort?: number,
): Promise<any> {
let backendAppProcess: ChildProcess;
let frontendAppProcess: ChildProcess | undefined;
const frontendPort = port || 3000;
let backendPort = externalPort || 8000;
const appRunner =
template === "reflex"
? runReflexApp
: framework === "fastapi"
? runFastAPIApp
: runTSApp;
await appRunner(appPath, port || defaultPort);
} catch (error) {
console.error("Failed to run app:", error);
throw error;
// Callback to kill app processes
process.on("exit", () => {
console.log("Killing app processes...");
backendAppProcess.kill();
frontendAppProcess?.kill();
});
let backendCommand = "";
let backendArgs: string[];
if (framework === "fastapi") {
backendCommand = "poetry";
backendArgs = [
"run",
"uvicorn",
"main:app",
"--host=0.0.0.0",
"--port=" + backendPort,
];
} else if (framework === "nextjs") {
backendCommand = "npm";
backendArgs = ["run", "dev"];
backendPort = frontendPort;
} else {
backendCommand = "npm";
backendArgs = ["run", "dev"];
}
if (frontend) {
return new Promise((resolve, reject) => {
backendAppProcess = createProcess(backendCommand, backendArgs, {
stdio: "inherit",
cwd: path.join(appPath, "backend"),
env: { ...process.env, PORT: `${backendPort}` },
});
frontendAppProcess = createProcess("npm", ["run", "dev"], {
stdio: "inherit",
cwd: path.join(appPath, "frontend"),
env: { ...process.env, PORT: `${frontendPort}` },
});
});
} else {
return new Promise((resolve, reject) => {
backendAppProcess = createProcess(backendCommand, backendArgs, {
stdio: "inherit",
cwd: path.join(appPath),
env: { ...process.env, PORT: `${backendPort}` },
});
});
}
}
+8 -238
View File
@@ -2,25 +2,15 @@ import fs from "fs/promises";
import path from "path";
import { red } from "picocolors";
import yaml from "yaml";
import { EnvVar } from "./env-variables";
import { makeDir } from "./make-dir";
import { TemplateFramework } from "./types";
export const TOOL_SYSTEM_PROMPT_ENV_VAR = "TOOL_SYSTEM_PROMPT";
export enum ToolType {
LLAMAHUB = "llamahub",
LOCAL = "local",
}
export type Tool = {
display: string;
name: string;
config?: Record<string, any>;
dependencies?: ToolDependencies[];
supportedFrameworks?: Array<TemplateFramework>;
type: ToolType;
envVars?: EnvVar[];
};
export type ToolDependencies = {
@@ -30,7 +20,7 @@ export type ToolDependencies = {
export const supportedTools: Tool[] = [
{
display: "Google Search",
display: "Google Search (configuration required after installation)",
name: "google.GoogleSearchToolSpec",
config: {
engine:
@@ -41,40 +31,10 @@ export const supportedTools: Tool[] = [
dependencies: [
{
name: "llama-index-tools-google",
version: "^0.3.0",
version: "0.1.2",
},
],
supportedFrameworks: ["fastapi"],
type: ToolType.LLAMAHUB,
envVars: [
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for google search tool.",
value: `You are a Google search agent. You help users to get information from Google search.`,
},
],
},
{
// For python app, we will use a local DuckDuckGo search tool (instead of DuckDuckGo search tool in LlamaHub)
// to get the same results as the TS app.
display: "DuckDuckGo Search",
name: "duckduckgo",
dependencies: [
{
name: "duckduckgo-search",
version: "^6.3.5",
},
],
supportedFrameworks: ["fastapi"], // TODO: Re-enable this tool once the duck-duck-scrape TypeScript library works again
type: ToolType.LOCAL,
envVars: [
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for DuckDuckGo search tool.",
value: `You have access to the duckduckgo search tool. Use it to get information from the web to answer user questions.
For better results, you can specify the region parameter to get results from a specific region but it's optional.`,
},
],
},
{
display: "Wikipedia",
@@ -82,178 +42,10 @@ For better results, you can specify the region parameter to get results from a s
dependencies: [
{
name: "llama-index-tools-wikipedia",
version: "^0.3.0",
version: "0.1.2",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LLAMAHUB,
},
{
display: "Weather",
name: "weather",
dependencies: [],
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LOCAL,
},
{
display: "Document generator",
name: "document_generator",
supportedFrameworks: ["fastapi", "nextjs", "express"],
dependencies: [
{
name: "xhtml2pdf",
version: "^0.2.14",
},
{
name: "markdown",
version: "^3.7",
},
],
type: ToolType.LOCAL,
envVars: [
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for document generator tool.",
value: `If user request for a report or a post, use document generator tool to create a file and reply with the link to the file.`,
},
],
},
{
display: "Code Interpreter",
name: "interpreter",
dependencies: [
{
name: "e2b_code_interpreter",
version: "1.1.1",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LOCAL,
envVars: [
{
name: "E2B_API_KEY",
description:
"E2B_API_KEY key is required to run code interpreter tool. Get it here: https://e2b.dev/docs/getting-started/api-key",
},
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for code interpreter tool.",
value: `-You are a Python interpreter that can run any python code in a secure environment.
- The python code runs in a Jupyter notebook. Every time you call the 'interpreter' tool, the python code is executed in a separate cell.
- You are given tasks to complete and you run python code to solve them.
- It's okay to make multiple calls to interpreter tool. If you get an error or the result is not what you expected, you can call the tool again. Don't give up too soon!
- Plot visualizations using matplotlib or any other visualization library directly in the notebook.
- You can install any pip package (if it exists) by running a cell with pip install.`,
},
],
},
{
display: "Artifact Code Generator",
name: "artifact",
// Using pre-release version of e2b_code_interpreter
// TODO: Update to stable version when 0.0.11 is released
dependencies: [
{
name: "e2b_code_interpreter",
version: "1.1.1",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LOCAL,
envVars: [
{
name: "E2B_API_KEY",
description:
"E2B_API_KEY key is required to run artifact code generator tool. Get it here: https://e2b.dev/docs/getting-started/api-key",
},
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for artifact code generator tool.",
value:
"You are a code assistant that can generate and execute code using its tools. Don't generate code yourself, use the provided tools instead. Do not show the code or sandbox url in chat, just describe the steps to build the application based on the code that is generated by your tools. Do not describe how to run the code, just the steps to build the application.",
},
],
},
{
display: "OpenAPI action",
name: "openapi_action.OpenAPIActionToolSpec",
dependencies: [
{
name: "llama-index-tools-openapi",
version: "0.2.0",
},
{
name: "jsonschema",
version: "^4.22.0",
},
{
name: "llama-index-tools-requests",
version: "0.2.0",
},
],
config: {
openapi_uri: "The URL or file path of the OpenAPI schema",
},
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LOCAL,
},
{
display: "Image Generator",
name: "img_gen",
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LOCAL,
envVars: [
{
name: "STABILITY_API_KEY",
description:
"STABILITY_API_KEY key is required to run image generator. Get it here: https://platform.stability.ai/account/keys",
},
],
},
{
display: "Azure Code Interpreter",
name: "azure_code_interpreter.AzureCodeInterpreterToolSpec",
supportedFrameworks: ["fastapi", "nextjs", "express"],
type: ToolType.LLAMAHUB,
dependencies: [
{
name: "llama-index-tools-azure-code-interpreter",
version: "0.2.0",
},
],
envVars: [
{
name: "AZURE_POOL_MANAGEMENT_ENDPOINT",
description:
"Please follow this guideline to create and get the pool management endpoint: https://learn.microsoft.com/azure/container-apps/sessions?tabs=azure-cli",
},
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for Azure code interpreter tool.",
value: `-You are a Python interpreter that can run any python code in a secure environment.
- The python code runs in a Jupyter notebook. Every time you call the 'interpreter' tool, the python code is executed in a separate cell.
- You are given tasks to complete and you run python code to solve them.
- It's okay to make multiple calls to interpreter tool. If you get an error or the result is not what you expected, you can call the tool again. Don't give up too soon!
- Plot visualizations using matplotlib or any other visualization library directly in the notebook.
- You can install any pip package (if it exists) by running a cell with pip install.`,
},
],
},
{
display: "Form Filling",
name: "form_filling",
supportedFrameworks: ["fastapi"],
type: ToolType.LOCAL,
dependencies: [
{
name: "pandas",
version: "^2.2.3",
},
{
name: "tabulate",
version: "^0.9.0",
},
],
},
];
@@ -280,15 +72,9 @@ export const getTools = (toolsName: string[]): Tool[] => {
return tools;
};
export const toolRequiresConfig = (tool: Tool): boolean => {
const hasConfig = Object.keys(tool.config || {}).length > 0;
const hasEmptyEnvVar = tool.envVars?.some((envVar) => !envVar.value) ?? false;
return hasConfig || hasEmptyEnvVar;
};
export const toolsRequireConfig = (tools?: Tool[]): boolean => {
if (tools) {
return tools?.some(toolRequiresConfig);
return tools?.some((tool) => Object.keys(tool.config || {}).length > 0);
}
return false;
};
@@ -303,19 +89,10 @@ export const writeToolsConfig = async (
tools: Tool[] = [],
type: ConfigFileType = ConfigFileType.YAML,
) => {
const configContent: {
[key in ToolType]: Record<string, any>;
} = {
local: {},
llamahub: {},
};
if (tools.length === 0) return; // no tools selected, no config need
const configContent: Record<string, any> = {};
tools.forEach((tool) => {
if (tool.type === ToolType.LLAMAHUB) {
configContent.llamahub[tool.name] = tool.config ?? {};
}
if (tool.type === ToolType.LOCAL) {
configContent.local[tool.name] = tool.config ?? {};
}
configContent[tool.name] = tool.config ?? {};
});
const configPath = path.join(root, "config");
await makeDir(configPath);
@@ -325,16 +102,9 @@ export const writeToolsConfig = async (
yaml.stringify(configContent),
);
} else {
// For Typescript, we treat llamahub tools as local tools
const tsConfigContent = {
local: {
...configContent.local,
...configContent.llamahub,
},
};
await fs.writeFile(
path.join(configPath, "tools.json"),
JSON.stringify(tsConfigContent, null, 2),
JSON.stringify(configContent, null, 2),
);
}
};
+9 -42
View File
@@ -1,31 +1,15 @@
import { PackageManager } from "../helpers/get-pkg-manager";
import { Tool } from "./tools";
export type ModelProvider =
| "openai"
| "groq"
| "ollama"
| "anthropic"
| "gemini"
| "mistral"
| "azure-openai"
| "huggingface"
| "t-systems";
export type ModelProvider = "openai" | "ollama";
export type ModelConfig = {
provider: ModelProvider;
apiKey?: string;
model: string;
embeddingModel: string;
dimensions: number;
isConfigured(): boolean;
};
export type TemplateType =
| "streaming"
| "community"
| "llamapack"
| "multiagent"
| "reflex"
| "llamaindexserver";
export type TemplateType = "streaming" | "community" | "llamapack";
export type TemplateFramework = "nextjs" | "express" | "fastapi";
export type TemplateUI = "html" | "shadcn";
export type TemplateVectorDB =
@@ -35,10 +19,7 @@ export type TemplateVectorDB =
| "pinecone"
| "milvus"
| "astra"
| "qdrant"
| "chroma"
| "llamacloud"
| "weaviate";
| "qdrant";
export type TemplatePostInstallAction =
| "none"
| "VSCode"
@@ -49,25 +30,11 @@ export type TemplateDataSource = {
config: TemplateDataSourceConfig;
};
export type TemplateDataSourceType = "file" | "web" | "db";
export type TemplateObservability = "none" | "traceloop" | "llamatrace";
export type TemplateUseCase =
| "financial_report"
| "blog"
| "deep_research"
| "form_filling"
| "extractor"
| "contract_review"
| "agentic_rag";
export type TemplateObservability = "none" | "opentelemetry";
// Config for both file and folder
export type FileSourceConfig =
| {
path: string;
filename?: string;
}
| {
url: URL;
filename?: string;
};
export type FileSourceConfig = {
path: string;
};
export type WebSourceConfig = {
baseUrl?: string;
prefix?: string;
@@ -99,15 +66,15 @@ export interface InstallTemplateArgs {
framework: TemplateFramework;
ui: TemplateUI;
dataSources: TemplateDataSource[];
customApiPath?: string;
modelConfig: ModelConfig;
llamaCloudKey?: string;
useLlamaParse?: boolean;
communityProjectConfig?: CommunityProjectConfig;
llamapack?: string;
vectorDb?: TemplateVectorDB;
port?: number;
externalPort?: number;
postInstallAction?: TemplatePostInstallAction;
tools?: Tool[];
observability?: TemplateObservability;
useCase?: TemplateUseCase;
}
+53 -351
View File
@@ -1,111 +1,47 @@
import fs from "fs/promises";
import os from "os";
import path from "path";
import { bold, cyan, red, yellow } from "picocolors";
import { bold, cyan } from "picocolors";
import { assetRelocator, copy } from "../helpers/copy";
import { callPackageManager } from "../helpers/install";
import { templatesDir } from "./dir";
import { PackageManager } from "./get-pkg-manager";
import { InstallTemplateArgs, ModelProvider, TemplateVectorDB } from "./types";
import { InstallTemplateArgs } from "./types";
const installLlamaIndexServerTemplate = async ({
root,
useCase,
vectorDb,
}: Pick<InstallTemplateArgs, "root" | "useCase" | "vectorDb">) => {
if (!useCase) {
console.log(
red(
`There is no use case selected. Please pick a use case to use via --use-case flag.`,
),
);
process.exit(1);
}
if (!vectorDb) {
console.log(
red(
`There is no vector db selected. Please pick a vector db to use via --vector-db flag.`,
),
);
process.exit(1);
}
await copy("workflow.ts", path.join(root, "src", "app"), {
parents: true,
cwd: path.join(
templatesDir,
"components",
"workflows",
"typescript",
useCase,
),
});
// copy workflow UI components to output/components folder
await copy("*", path.join(root, "components"), {
parents: true,
cwd: path.join(templatesDir, "components", "ui", "workflows", useCase),
});
if (vectorDb === "llamacloud") {
await copy("generate.ts", path.join(root, "src"), {
parents: true,
cwd: path.join(
templatesDir,
"components",
"vectordbs",
"llamaindexserver",
"llamacloud",
"typescript",
),
});
await copy("index.ts", path.join(root, "src", "app"), {
parents: true,
cwd: path.join(
templatesDir,
"components",
"vectordbs",
"llamaindexserver",
"llamacloud",
"typescript",
),
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,
});
};
const installLegacyTSTemplate = async ({
/**
* Install a LlamaIndex internal template to a given `root` directory.
*/
export const installTSTemplate = async ({
appName,
root,
packageManager,
isOnline,
template,
backend,
framework,
ui,
vectorDb,
postInstallAction,
backend,
observability,
tools,
dataSources,
useLlamaParse,
useCase,
modelConfig,
relativeEngineDestPath,
}: InstallTemplateArgs & {
backend: boolean;
relativeEngineDestPath: string;
}) => {
}: InstallTemplateArgs & { backend: boolean }) => {
console.log(bold(`Using ${packageManager}.`));
/**
* Copy the template files to the target directory.
*/
console.log("\nInitializing project with template:", template, "\n");
const templatePath = path.join(templatesDir, "types", template, framework);
const copySource = ["**"];
await copy(copySource, root, {
parents: true,
cwd: templatePath,
rename: assetRelocator,
});
/**
* If next.js is used, update its configuration if necessary
*/
@@ -121,9 +57,11 @@ const installLegacyTSTemplate = async ({
console.log("\nUsing static site generation\n");
} else {
if (vectorDb === "milvus") {
nextConfigJson.serverExternalPackages =
nextConfigJson.serverExternalPackages ?? [];
nextConfigJson.serverExternalPackages.push("@zilliz/milvus2-sdk-node");
nextConfigJson.experimental.serverComponentsExternalPackages =
nextConfigJson.experimental.serverComponentsExternalPackages ?? [];
nextConfigJson.experimental.serverComponentsExternalPackages.push(
"@zilliz/milvus2-sdk-node",
);
}
}
await fs.writeFile(
@@ -132,7 +70,7 @@ const installLegacyTSTemplate = async ({
);
const webpackConfigOtelFile = path.join(root, "webpack.config.o11y.mjs");
if (observability === "traceloop") {
if (observability === "opentelemetry") {
const webpackConfigDefaultFile = path.join(root, "webpack.config.mjs");
await fs.rm(webpackConfigDefaultFile);
await fs.rename(webpackConfigOtelFile, webpackConfigDefaultFile);
@@ -160,79 +98,19 @@ const installLegacyTSTemplate = async ({
}
const compPath = path.join(templatesDir, "components");
const relativeEngineDestPath =
framework === "nextjs"
? path.join("app", "api", "chat")
: path.join("src", "controllers");
const enginePath = path.join(root, relativeEngineDestPath, "engine");
// copy llamaindex code for TS templates
await copy("**", path.join(root, relativeEngineDestPath, "llamaindex"), {
parents: true,
cwd: path.join(compPath, "llamaindex", "typescript"),
});
// copy vector db component
if (vectorDb === "llamacloud") {
console.log(
`\nUsing managed index from LlamaCloud. Ensure the ${yellow("LLAMA_CLOUD_* environment variables are set correctly.")}`,
);
} else {
console.log("\nUsing vector DB:", vectorDb ?? "none");
}
console.log("\nUsing vector DB:", vectorDb, "\n");
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "vectordbs", "typescript", vectorDb ?? "none"),
});
if (template === "multiagent") {
const multiagentPath = path.join(compPath, "multiagent", "typescript");
// copy workflow code for multiagent template
await copy("**", path.join(root, relativeEngineDestPath, "workflow"), {
parents: true,
cwd: path.join(multiagentPath, "workflow"),
});
// Copy use case code for multiagent template
if (useCase) {
console.log("\nCopying use case:", useCase, "\n");
const useCasePath = path.join(compPath, "agents", "typescript", useCase);
const useCaseCodePath = path.join(useCasePath, "workflow");
// Copy use case codes
await copy("**", path.join(root, relativeEngineDestPath, "workflow"), {
parents: true,
cwd: useCaseCodePath,
rename: assetRelocator,
});
// Copy use case files to project root
await copy("*.*", path.join(root), {
parents: true,
cwd: useCasePath,
rename: assetRelocator,
});
} else {
console.log(
red(
`There is no use case selected for ${template} template. Please pick a use case to use via --use-case flag.`,
),
);
process.exit(1);
}
if (framework === "nextjs") {
// patch route.ts file
await copy("**", path.join(root, relativeEngineDestPath), {
parents: true,
cwd: path.join(multiagentPath, "nextjs"),
});
} else if (framework === "express") {
// patch chat.controller.ts file
await copy("**", path.join(root, relativeEngineDestPath), {
parents: true,
cwd: path.join(multiagentPath, "express"),
});
}
}
// copy loader component (TS only supports llama_parse and file for now)
const loaderFolder = useLlamaParse ? "llama_parse" : "file";
await copy("**", enginePath, {
@@ -240,19 +118,10 @@ const installLegacyTSTemplate = async ({
cwd: path.join(compPath, "loaders", "typescript", loaderFolder),
});
// copy provider settings
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "providers", "typescript", modelConfig.provider),
});
// Select and copy engine code based on data sources and tools
let engine;
tools = tools ?? [];
// multiagent template always uses agent engine
if (template === "multiagent") {
engine = "agent";
} else if (dataSources.length > 0 && tools.length === 0) {
if (dataSources.length > 0 && tools.length === 0) {
console.log("\nNo tools selected - use optimized context chat engine\n");
engine = "chat";
} else {
@@ -263,11 +132,6 @@ const installLegacyTSTemplate = async ({
cwd: path.join(compPath, "engines", "typescript", engine),
});
// copy settings to engine folder
await copy("**", enginePath, {
cwd: path.join(compPath, "settings", "typescript"),
});
/**
* Copy the selected UI files to the target directory and reference it.
*/
@@ -294,75 +158,6 @@ const installLegacyTSTemplate = async ({
await fs.rm(path.join(root, "app", "api"), { recursive: true });
await fs.rm(path.join(root, "config"), { recursive: true, force: true });
}
};
/**
* Install a LlamaIndex internal template to a given `root` directory.
*/
export const installTSTemplate = async ({
appName,
root,
packageManager,
isOnline,
template,
framework,
ui,
vectorDb,
postInstallAction,
backend,
observability,
tools,
dataSources,
useLlamaParse,
useCase,
modelConfig,
}: InstallTemplateArgs & { backend: boolean }) => {
console.log(bold(`Using ${packageManager}.`));
/**
* Copy the template files to the target directory.
*/
console.log("\nInitializing project with template:", template, "\n");
const templatePath = path.join(templatesDir, "types", template, framework);
const copySource = ["**"];
await copy(copySource, root, {
parents: true,
cwd: templatePath,
rename: assetRelocator,
});
const relativeEngineDestPath =
framework === "nextjs"
? path.join("app", "api", "chat")
: path.join("src", "controllers");
if (template === "llamaindexserver") {
await installLlamaIndexServerTemplate({
root,
useCase,
vectorDb,
});
} else {
await installLegacyTSTemplate({
appName,
root,
packageManager,
isOnline,
template,
backend,
framework,
ui,
vectorDb,
observability,
tools,
dataSources,
useLlamaParse,
useCase,
modelConfig,
relativeEngineDestPath,
});
}
const packageJson = await updatePackageJson({
root,
@@ -372,80 +167,16 @@ export const installTSTemplate = async ({
framework,
ui,
observability,
vectorDb,
backend,
modelConfig,
template,
});
if (
backend &&
(postInstallAction === "runApp" || postInstallAction === "dependencies")
) {
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
await installTSDependencies(packageJson, packageManager, isOnline);
}
};
const providerDependencies: {
[key in ModelProvider]?: Record<string, string>;
} = {
openai: {
"@llamaindex/openai": "^0.2.0",
},
gemini: {
"@llamaindex/google": "^0.2.0",
},
ollama: {
"@llamaindex/ollama": "^0.1.0",
},
mistral: {
"@llamaindex/mistral": "^0.2.0",
},
"azure-openai": {
"@llamaindex/openai": "^0.2.0",
},
groq: {
"@llamaindex/groq": "^0.0.61",
"@llamaindex/huggingface": "^0.1.0", // groq uses huggingface as default embedding model
},
anthropic: {
"@llamaindex/anthropic": "^0.3.0",
"@llamaindex/huggingface": "^0.1.0", // anthropic uses huggingface as default embedding model
},
};
const vectorDbDependencies: Record<TemplateVectorDB, Record<string, string>> = {
astra: {
"@llamaindex/astra": "^0.0.5",
},
chroma: {
"@llamaindex/chroma": "^0.0.5",
},
llamacloud: {},
milvus: {
"@zilliz/milvus2-sdk-node": "^2.4.6",
"@llamaindex/milvus": "^0.1.0",
},
mongo: {
mongodb: "6.7.0",
"@llamaindex/mongodb": "^0.0.5",
},
none: {},
pg: {
pg: "^8.12.0",
pgvector: "^0.2.0",
"@llamaindex/postgres": "^0.0.33",
},
pinecone: {
"@llamaindex/pinecone": "^0.0.5",
},
qdrant: {
"@qdrant/js-client-rest": "^1.11.0",
"@llamaindex/qdrant": "^0.1.0",
},
weaviate: {
"@llamaindex/weaviate": "^0.0.5",
},
// Copy deployment files for typescript
await copy("**", root, {
cwd: path.join(compPath, "deployments", "typescript"),
});
};
async function updatePackageJson({
@@ -456,24 +187,11 @@ async function updatePackageJson({
framework,
ui,
observability,
vectorDb,
backend,
modelConfig,
template,
}: Pick<
InstallTemplateArgs,
| "root"
| "appName"
| "dataSources"
| "framework"
| "ui"
| "observability"
| "vectorDb"
| "modelConfig"
| "template"
"root" | "appName" | "dataSources" | "framework" | "ui" | "observability"
> & {
relativeEngineDestPath: string;
backend: boolean;
}): Promise<any> {
const packageJsonFile = path.join(root, "package.json");
const packageJson: any = JSON.parse(
@@ -482,7 +200,7 @@ async function updatePackageJson({
packageJson.name = appName;
packageJson.version = "0.1.0";
if (relativeEngineDestPath && template !== "llamaindexserver") {
if (relativeEngineDestPath) {
// TODO: move script to {root}/scripts for all frameworks
// add generate script if using context engine
packageJson.scripts = {
@@ -509,32 +227,16 @@ async function updatePackageJson({
"remark-gfm": undefined,
"remark-math": undefined,
"react-markdown": undefined,
"highlight.js": undefined,
"react-syntax-highlighter": undefined,
};
packageJson.devDependencies = {
...packageJson.devDependencies,
"@types/react-syntax-highlighter": undefined,
};
}
if (backend) {
packageJson.dependencies = {
...packageJson.dependencies,
"@llamaindex/readers": "^2.0.0",
};
if (vectorDb && vectorDb in vectorDbDependencies) {
packageJson.dependencies = {
...packageJson.dependencies,
...vectorDbDependencies[vectorDb],
};
}
if (modelConfig.provider && modelConfig.provider in providerDependencies) {
packageJson.dependencies = {
...packageJson.dependencies,
...providerDependencies[modelConfig.provider],
};
}
}
if (observability === "traceloop") {
if (observability === "opentelemetry") {
packageJson.dependencies = {
...packageJson.dependencies,
"@traceloop/node-server-sdk": "^0.5.19",
+82 -103
View File
@@ -1,6 +1,7 @@
/* eslint-disable import/no-extraneous-dependencies */
import { execSync } from "child_process";
import { Command } from "commander";
import Commander from "commander";
import Conf from "conf";
import fs from "fs";
import path from "path";
import { bold, cyan, green, red, yellow } from "picocolors";
@@ -8,19 +9,14 @@ import prompts from "prompts";
import terminalLink from "terminal-link";
import checkForUpdate from "update-check";
import { createApp } from "./create-app";
import { EXAMPLE_FILE, getDataSources } from "./helpers/datasources";
import { getDataSources } from "./helpers/datasources";
import { getPkgManager } from "./helpers/get-pkg-manager";
import { isFolderEmpty } from "./helpers/is-folder-empty";
import { initializeGlobalAgent } from "./helpers/proxy";
import { runApp } from "./helpers/run-app";
import { getTools } from "./helpers/tools";
import { validateNpmName } from "./helpers/validate-pkg";
import packageJson from "./package.json";
import { askQuestions } from "./questions/index";
import { QuestionArgs } from "./questions/types";
import { onPromptState } from "./questions/utils";
// Run the initialization function
initializeGlobalAgent();
import { QuestionArgs, askQuestions, onPromptState } from "./questions";
let projectPath: string = "";
@@ -29,14 +25,12 @@ const handleSigTerm = () => process.exit(0);
process.on("SIGINT", handleSigTerm);
process.on("SIGTERM", handleSigTerm);
const program = new Command(packageJson.name)
const program = new Commander.Command(packageJson.name)
.version(packageJson.version)
.arguments("[project-directory]")
.usage(`${green("[project-directory]")} [options]`)
.arguments("<project-directory>")
.usage(`${green("<project-directory>")} [options]`)
.action((name) => {
if (name) {
projectPath = name;
}
projectPath = name;
})
.option(
"--use-npm",
@@ -57,6 +51,13 @@ const program = new Command(packageJson.name)
`
Explicitly tell the CLI to bootstrap the application using Yarn
`,
)
.option(
"--reset-preferences",
`
Explicitly tell the CLI to reset any stored preferences
`,
)
.option(
@@ -85,20 +86,6 @@ const program = new Command(packageJson.name)
`
Select to use an example PDF as data source.
`,
)
.option(
"--web-source <url>",
`
Specify a website URL to use as a data source.
`,
)
.option(
"--db-source <connection-string>",
`
Specify a database connection string to use as a data source.
`,
)
.option(
@@ -119,14 +106,7 @@ const program = new Command(packageJson.name)
"--frontend",
`
Generate a frontend for your backend.
`,
)
.option(
"--no-frontend",
`
Do not generate a frontend for your backend.
Whether to generate a frontend for your backend.
`,
)
.option(
@@ -134,6 +114,13 @@ const program = new Command(packageJson.name)
`
Select UI port.
`,
)
.option(
"--external-port <external>",
`
Select external port.
`,
)
.option(
@@ -156,13 +143,6 @@ const program = new Command(packageJson.name)
Specify the tools you want to use by providing a comma-separated list. For example, 'wikipedia.WikipediaToolSpec,google.GoogleSearchToolSpec'. Use 'none' to not using any tools.
`,
(tools, _) => {
if (tools === "none") {
return [];
} else {
return getTools(tools.split(","));
}
},
)
.option(
"--use-llama-parse",
@@ -189,75 +169,48 @@ const program = new Command(packageJson.name)
"--ask-models",
`
Allow interactive selection of LLM and embedding models of different model providers.
`,
false,
)
.option(
"--pro",
`
Allow interactive selection of all features.
`,
false,
)
.option(
"--use-case <useCase>",
`
Select which use case to use for the multi-agent template (e.g: financial_report, blog).
Select LLM and embedding models.
`,
)
.allowUnknownOption()
.parse(process.argv);
const options = program.opts();
if (
process.argv.includes("--no-llama-parse") ||
options.template === "reflex"
) {
options.useLlamaParse = false;
if (process.argv.includes("--no-frontend")) {
program.frontend = false;
}
if (process.argv.includes("--tools")) {
if (program.tools === "none") {
program.tools = [];
} else {
program.tools = getTools(program.tools.split(","));
}
}
if (process.argv.includes("--no-llama-parse")) {
program.useLlamaParse = false;
}
program.askModels = process.argv.includes("--ask-models");
if (process.argv.includes("--no-files")) {
options.dataSources = [];
} else if (process.argv.includes("--example-file")) {
options.dataSources = getDataSources(options.files, options.exampleFile);
} else if (process.argv.includes("--llamacloud")) {
options.dataSources = [EXAMPLE_FILE];
options.vectorDb = "llamacloud";
} else if (process.argv.includes("--web-source")) {
options.dataSources = [
{
type: "web",
config: {
baseUrl: options.webSource,
prefix: options.webSource,
depth: 1,
},
},
];
} else if (process.argv.includes("--db-source")) {
options.dataSources = [
{
type: "db",
config: {
uri: options.dbSource,
queries: options.dbQuery || "SELECT * FROM mytable",
},
},
];
program.dataSources = [];
} else {
program.dataSources = getDataSources(program.files, program.exampleFile);
}
const packageManager = !!options.useNpm
const packageManager = !!program.useNpm
? "npm"
: !!options.usePnpm
: !!program.usePnpm
? "pnpm"
: !!options.useYarn
: !!program.useYarn
? "yarn"
: getPkgManager();
async function run(): Promise<void> {
const conf = new Conf({ projectName: "create-llama" });
if (program.resetPreferences) {
conf.clear();
console.log(`Preferences reset successfully`);
return;
}
if (typeof projectPath === "string") {
projectPath = projectPath.trim();
}
@@ -320,15 +273,35 @@ async function run(): Promise<void> {
process.exit(1);
}
const answers = await askQuestions(options as unknown as QuestionArgs);
const preferences = (conf.get("preferences") || {}) as QuestionArgs;
await askQuestions(
program as unknown as QuestionArgs,
preferences,
program.openAiKey,
);
await createApp({
...answers,
template: program.template,
framework: program.framework,
ui: program.ui,
appPath: resolvedProjectPath,
packageManager,
frontend: program.frontend,
modelConfig: program.modelConfig,
llamaCloudKey: program.llamaCloudKey,
communityProjectConfig: program.communityProjectConfig,
llamapack: program.llamapack,
vectorDb: program.vectorDb,
externalPort: program.externalPort,
postInstallAction: program.postInstallAction,
dataSources: program.dataSources,
tools: program.tools,
useLlamaParse: program.useLlamaParse,
observability: program.observability,
});
conf.set("preferences", preferences);
if (answers.postInstallAction === "VSCode") {
if (program.postInstallAction === "VSCode") {
console.log(`Starting VSCode in ${root}...`);
try {
execSync(`code . --new-window --goto README.md`, {
@@ -352,9 +325,15 @@ Please check ${cyan(
)} for more information.`,
);
}
} else if (answers.postInstallAction === "runApp") {
} else if (program.postInstallAction === "runApp") {
console.log(`Running app in ${root}...`);
await runApp(root, answers.template, answers.framework, options.port);
await runApp(
root,
program.frontend,
program.framework,
program.port,
program.externalPort,
);
}
}
-141
View File
@@ -1,141 +0,0 @@
# LlamaIndex Server
LlamaIndexServer is a FastAPI-based application that allows you to quickly launch your [LlamaIndex Workflows](https://docs.llamaindex.ai/en/stable/module_guides/workflow/#workflows) and [Agent Workflows](https://docs.llamaindex.ai/en/stable/understanding/agent/multi_agent/) as an API server with an optional chat UI. It provides a complete environment for running LlamaIndex workflows with both API endpoints and a user interface for interaction.
## Features
- Serving a workflow as a chatbot
- Built on FastAPI for high performance and easy API development
- Optional built-in chat UI with extendable UI components
- Prebuilt development code
## Installation
```bash
pip install llama-index-server
```
## Quick Start
```python
# main.py
from llama_index.core.agent.workflow import AgentWorkflow
from llama_index.core.workflow import Workflow
from llama_index.core.tools import FunctionTool
from llama_index.server import LlamaIndexServer
# Define a factory function that returns a Workflow or AgentWorkflow
def create_workflow() -> Workflow:
def fetch_weather(city: str) -> str:
return f"The weather in {city} is sunny"
return AgentWorkflow.from_tools(
tools=[
FunctionTool.from_defaults(
fn=fetch_weather,
)
]
)
# Create an API server for the workflow
app = LlamaIndexServer(
workflow_factory=create_workflow, # Supports Workflow or AgentWorkflow
env="dev", # Enable development mode
ui_config={ # Configure the chat UI, optional
"app_title": "Weather Bot",
"starter_questions": ["What is the weather in LA?", "Will it rain in SF?"],
},
verbose=True
)
```
## Running the Server
- In the same directory as `main.py`, run the following command to start the server:
```bash
fastapi dev
```
- Making a request to the server:
```bash
curl -X POST "http://localhost:8000/api/chat" -H "Content-Type: application/json" -d '{"message": "What is the weather in Tokyo?"}'
```
- See the API documentation at `http://localhost:8000/docs`
- Access the chat UI at `http://localhost:8000/` (Make sure you set the `env="dev"` or `include_ui=True` in the server configuration)
## Configuration Options
The LlamaIndexServer accepts the following configuration parameters:
- `workflow_factory`: A callable that creates a workflow instance for each request
- `logger`: Optional logger instance (defaults to uvicorn logger)
- `use_default_routers`: Whether to include default routers (chat, static file serving)
- `env`: Environment setting ('dev' enables CORS and UI by default)
- `ui_config`: UI configuration as a dictionary or UIConfig object with options:
- `enabled`: Whether to enable the chat UI (default: True)
- `app_title`: The title of the chat application (default: "LlamaIndex Server")
- `starter_questions`: List of starter questions for the chat UI (default: None)
- `ui_path`: Path for downloaded UI static files (default: ".ui")
- `component_dir`: The directory for custom UI components rendering events emitted by the workflow. The default is None, which does not render custom UI components.
- `llamacloud_index_selector`: Whether to show the LlamaCloud index selector in the chat UI (default: False). Requires `LLAMA_CLOUD_API_KEY` to be set.
- `verbose`: Enable verbose logging
- `api_prefix`: API route prefix (default: "/api")
- `server_url`: The deployment URL of the server (default is None)
## Default Routers and Features
### Chat Router
The server includes a default chat router at `/api/chat` for handling chat interactions.
### Static File Serving
- The server automatically mounts the `data` and `output` folders at `{server_url}{api_prefix}/files/data` (default: `/api/files/data`) and `{server_url}{api_prefix}/files/output` (default: `/api/files/output`) respectively.
- Your workflows can use both folders to store and access files. As a convention, the `data` folder is used for documents that are ingested and the `output` folder is used for documents that are generated by the workflow.
- The example workflows from `create-llama` (see below) are following this pattern.
### Chat UI
When enabled, the server provides a chat interface at the root path (`/`) with:
- Configurable starter questions
- Real-time chat interface
- API endpoint integration
### Custom UI Components
You can add custom UI components for your workflow by providing `component_dir` config and adding custom .jsx or .tsx files to the directory.
See [Custom UI Components](docs/custom_ui_component.md) for more details.
## Development Mode
In development mode (`env="dev"`), the server:
- Enables CORS for all origins
- Automatically includes the chat UI
- Provides more verbose logging
## API Endpoints
The server provides the following default endpoints:
- `/api/chat`: Chat interaction endpoint
- `/api/files/data/*`: Access to data directory files
- `/api/files/output/*`: Access to output directory files
## Best Practices
1. Always provide a workflow factory that creates fresh workflow instances
2. Use environment variables for sensitive configuration
3. Enable verbose logging during development
4. Configure CORS appropriately for your deployment environment
5. Use starter questions to guide users in the chat UI
## Getting Started with a New Project
Want to start a new project with LlamaIndexServer? Check out our [create-llama](https://github.com/run-llama/create-llama) tool to quickly generate a new project with LlamaIndexServer.
@@ -1,103 +0,0 @@
# Custom UI Components
The LlamaIndex server provides support for rendering workflow events using custom UI components, allowing you to extend and customize the chat interface.
## Overview
Custom UI components are a powerful feature that enables you to:
- Add custom interface elements to the chat UI using React JSX or TSX files
- Extend the default chat interface functionality
- Create specialized visualizations or interactions
## Configuration
### Workflow events
To display custom UI components, your workflow needs to emit `UIEvent` events with data that conforms to the data model of your custom UI component.
```python
from llama_index.server import UIEvent
from pydantic import BaseModel, Field
from typing import Literal, Any
# Define a Pydantic model for your event data
class DeepResearchEventData(BaseModel):
id: str = Field(description="The unique identifier for the event")
type: Literal["retrieval", "analysis"] = Field(description="DeepResearch has two main stages: retrieval and analysis")
status: Literal["pending", "completed", "failed"] = Field(description="The current status of the event")
content: str = Field(description="The textual content of the event")
# In your workflow, emit the data model with UIEvent
ctx.write_event_to_stream(
UIEvent(
type="deep_research_event",
data=DeepResearchEventData(
id="123",
type="retrieval",
status="pending",
content="Retrieving data...",
),
)
)
```
### Server Setup
1. Initialize the LlamaIndex server with a component directory:
```python
server = LlamaIndexServer(
workflow_factory=your_workflow,
ui_config={
"component_dir": "path/to/components",
},
include_ui=True
)
```
2. Add the custom component code to the directory following the naming pattern:
- File Extension: `.jsx` and `.tsx` for React components
- File Name: Should match the event type from your workflow (e.g., `deep_research_event.jsx` for handling `deep_research_event` type that you defined in your workflow). If there are TSX and JSX files with the same name, the TSX file will be used.
- Component Name: Export a default React component named `Component` that receives props from the event data
Example component structure:
```jsx
function Component({ events }) {
// Your component logic here
return (
// Your UI code here
);
}
```
### Generate UI Component
We provide a `generate_event_component` function that uses LLMs to automatically generate UI components for your workflow events.
```python
from llama_index.server.gen_ui import generate_event_component
from llama_index.llms.openai import OpenAI
# Generate a component using the event class you defined in your workflow
from your_workflow import DeepResearchEvent
ui_code = await generate_event_component(
event_cls=DeepResearchEvent,
llm=OpenAI(model="gpt-4.1"), # Default LLM is Claude 3.7 Sonnet if not provided
)
# Alternatively, generate from your workflow file
ui_code = await generate_event_component(
workflow_file="your_workflow.py",
)
print(ui_code)
# Save the generated code to a file for use in your project
with open("deep_research_event.jsx", "w") as f:
f.write(ui_code)
```
> **Tip:** For optimal results, add descriptive documentation to each field in your event data class. This helps the LLM better understand your data structure and generate more appropriate UI components. We also recommend using GPT 4.1, Claude 3.7 Sonnet and Gemini 2.5 Pro for better results.
@@ -1,4 +0,0 @@
from .api.models import UIEvent
from .server import LlamaIndexServer, UIConfig
__all__ = ["LlamaIndexServer", "UIConfig", "UIEvent"]
@@ -1,13 +0,0 @@
from llama_index.server.api.callbacks.base import EventCallback
from llama_index.server.api.callbacks.llamacloud import LlamaCloudFileDownload
from llama_index.server.api.callbacks.source_nodes import SourceNodesFromToolCall
from llama_index.server.api.callbacks.suggest_next_questions import (
SuggestNextQuestions,
)
__all__ = [
"EventCallback",
"SourceNodesFromToolCall",
"SuggestNextQuestions",
"LlamaCloudFileDownload",
]
@@ -1,31 +0,0 @@
import logging
from abc import ABC, abstractmethod
from typing import Any
logger = logging.getLogger("uvicorn")
class EventCallback(ABC):
"""
Base class for event callbacks during event streaming.
"""
async def run(self, event: Any) -> Any:
"""
Called for each event in the stream.
Default behavior: pass through the event unchanged.
"""
return event
async def on_complete(self, final_response: str) -> Any:
"""
Called when the stream is complete.
Default behavior: return None.
"""
return None
@abstractmethod
def from_default(self, *args: Any, **kwargs: Any) -> "EventCallback":
"""
Create a new instance of the processor from default values.
"""
@@ -1,39 +0,0 @@
import logging
from typing import Any, List
from fastapi import BackgroundTasks
from llama_index.core.schema import NodeWithScore
from llama_index.server.api.callbacks.base import EventCallback
from llama_index.server.services.llamacloud.file import LlamaCloudFileService
logger = logging.getLogger("uvicorn")
class LlamaCloudFileDownload(EventCallback):
"""
Processor for handling LlamaCloud file downloads from source nodes.
"""
def __init__(self, background_tasks: BackgroundTasks) -> None:
self.background_tasks = background_tasks
async def run(self, event: Any) -> Any:
if hasattr(event, "to_response"):
event_response = event.to_response()
if event_response.get("type") == "sources" and hasattr(event, "nodes"):
await self._process_response_nodes(event.nodes)
return event
async def _process_response_nodes(self, source_nodes: List[NodeWithScore]) -> None:
try:
LlamaCloudFileService.download_files_from_nodes(
source_nodes, self.background_tasks
)
except ImportError:
pass
@classmethod
def from_default(
cls, background_tasks: BackgroundTasks
) -> "LlamaCloudFileDownload":
return cls(background_tasks=background_tasks)
@@ -1,32 +0,0 @@
from typing import Any
from llama_index.core.agent.workflow.workflow_events import ToolCallResult
from llama_index.server.api.callbacks.base import EventCallback
from llama_index.server.api.models import SourceNodesEvent
class SourceNodesFromToolCall(EventCallback):
"""
Extract source nodes from the query tool output.
Args:
query_tool_name: The name of the tool that queries the index.
default is "query_index"
"""
def __init__(self, query_tool_name: str = "query_index"):
self.query_tool_name = query_tool_name
def transform_tool_call_result(self, event: ToolCallResult) -> SourceNodesEvent:
source_nodes = event.tool_output.raw_output.source_nodes
return SourceNodesEvent(nodes=source_nodes)
async def run(self, event: Any) -> Any:
if isinstance(event, ToolCallResult):
if event.tool_name == self.query_tool_name:
return event, self.transform_tool_call_result(event)
return event
@classmethod
def from_default(cls, *args: Any, **kwargs: Any) -> "SourceNodesFromToolCall":
return cls()
@@ -1,69 +0,0 @@
import logging
from typing import Any, AsyncGenerator, List, Optional
from llama_index.core.workflow.handler import WorkflowHandler
from llama_index.server.api.callbacks.base import EventCallback
logger = logging.getLogger("uvicorn")
class StreamHandler:
"""
Streams events from a workflow handler through a chain of callbacks.
"""
def __init__(
self,
workflow_handler: WorkflowHandler,
callbacks: Optional[List[EventCallback]] = None,
):
self.workflow_handler = workflow_handler
self.callbacks = callbacks or []
self.accumulated_text = ""
async def cancel_run(self) -> None:
"""Cancel the workflow handler."""
await self.workflow_handler.cancel_run()
async def stream_events(self) -> AsyncGenerator[Any, None]:
"""Stream events through the processor chain."""
try:
async for event in self.workflow_handler.stream_events():
events_to_process = [event]
for callback in self.callbacks:
next_events: list[Any] = []
for evt in events_to_process:
callback_output = await callback.run(evt)
if isinstance(callback_output, (list, tuple)):
next_events.extend(callback_output)
elif callback_output is not None:
next_events.append(callback_output)
events_to_process = next_events
# Yield all processed events
for evt in events_to_process:
yield evt
# After all events are processed, call on_complete for each callback
for callback in self.callbacks:
result = await callback.on_complete(self.accumulated_text)
if result:
yield result
except Exception:
# Make sure to cancel the workflow on error
await self.workflow_handler.cancel_run()
raise
def accumulate_text(self, text: str) -> None:
"""Accumulate text from the workflow handler."""
self.accumulated_text += text
@classmethod
def from_default(
cls,
handler: WorkflowHandler,
callbacks: Optional[List[EventCallback]] = None,
) -> "StreamHandler":
"""Create a new instance with the given workflow handler and callbacks."""
return cls(workflow_handler=handler, callbacks=callbacks)
@@ -1,45 +0,0 @@
import logging
from typing import Any, Optional
from llama_index.server.api.callbacks.base import EventCallback
from llama_index.server.api.models import ChatRequest
from llama_index.server.services.suggest_next_question import (
SuggestNextQuestionsService,
)
logger = logging.getLogger("uvicorn")
class SuggestNextQuestions(EventCallback):
"""Processor for generating next question suggestions."""
def __init__(
self, chat_request: ChatRequest, logger: Optional[logging.Logger] = None
):
self.chat_request = chat_request
self.accumulated_text = ""
if logger:
self.logger = logger
else:
self.logger = logging.getLogger("uvicorn")
async def on_complete(self, final_response: str) -> Any:
if final_response == "":
self.logger.warning(
"SuggestNextQuestions is enabled but final response is empty, make sure your content generator accumulates text"
)
return None
questions = await SuggestNextQuestionsService.run(
self.chat_request.messages, final_response
)
if questions:
return {
"type": "suggested_questions",
"data": questions,
}
return None
@classmethod
def from_default(cls, chat_request: ChatRequest) -> "SuggestNextQuestions":
return cls(chat_request=chat_request)
@@ -1,153 +0,0 @@
import logging
import os
from enum import Enum
from typing import Any, Dict, List, Optional
from llama_index.core.schema import NodeWithScore
from llama_index.core.types import ChatMessage, MessageRole
from llama_index.core.workflow import Event
from llama_index.server.settings import server_settings
from pydantic import BaseModel, Field, field_validator
logger = logging.getLogger("uvicorn")
class ChatConfig(BaseModel):
next_question_suggestions: bool = Field(
default=True,
description="Whether to suggest next questions",
)
class ChatAPIMessage(BaseModel):
role: MessageRole
content: str
def to_llamaindex_message(self) -> ChatMessage:
return ChatMessage(role=self.role, content=self.content)
class ChatRequest(BaseModel):
messages: List[ChatAPIMessage]
data: Optional[Any] = None
config: Optional[ChatConfig] = ChatConfig()
@field_validator("messages")
def validate_messages(cls, v: List[ChatAPIMessage]) -> List[ChatAPIMessage]:
if v[-1].role != MessageRole.USER:
raise ValueError("Last message must be from user")
return v
class AgentRunEventType(Enum):
TEXT = "text"
PROGRESS = "progress"
class AgentRunEvent(Event):
name: str
msg: str
event_type: AgentRunEventType = AgentRunEventType.TEXT
data: Optional[dict] = None
def to_response(self) -> dict:
return {
"type": "agent",
"data": {
"agent": self.name,
"type": self.event_type.value,
"text": self.msg,
"data": self.data,
},
}
class SourceNodesEvent(Event):
nodes: List[NodeWithScore]
def to_response(self) -> dict:
return {
"type": "sources",
"data": {
"nodes": [
SourceNodes.from_source_node(node).model_dump()
for node in self.nodes
]
},
}
class SourceNodes(BaseModel):
id: str
metadata: Dict[str, Any]
score: Optional[float]
text: str
url: Optional[str]
@classmethod
def from_source_node(cls, source_node: NodeWithScore) -> "SourceNodes":
metadata = source_node.node.metadata
url = cls.get_url_from_metadata(metadata)
return cls(
id=source_node.node.node_id,
metadata=metadata,
score=source_node.score,
text=source_node.node.text, # type: ignore
url=url,
)
@classmethod
def get_url_from_metadata(
cls,
metadata: Dict[str, Any],
data_dir: Optional[str] = None,
) -> Optional[str]:
url_prefix = server_settings.file_server_url_prefix
if data_dir is None:
data_dir = "data"
file_name = metadata.get("file_name")
if file_name and url_prefix:
# file_name exists and file server is configured
pipeline_id = metadata.get("pipeline_id")
if pipeline_id:
# file is from LlamaCloud
file_name = f"{pipeline_id}${file_name}"
return f"{url_prefix}/output/llamacloud/{file_name}"
is_private = metadata.get("private", "false") == "true"
if is_private:
# file is a private upload
return f"{url_prefix}/output/uploaded/{file_name}"
# file is from calling the 'generate' script
# Get the relative path of file_path to data_dir
file_path = metadata.get("file_path")
data_dir = os.path.abspath(data_dir)
if file_path and data_dir:
relative_path = os.path.relpath(file_path, data_dir)
return f"{url_prefix}/data/{relative_path}"
# fallback to URL in metadata (e.g. for websites)
return metadata.get("URL")
@classmethod
def from_source_nodes(
cls, source_nodes: List[NodeWithScore]
) -> List["SourceNodes"]:
return [cls.from_source_node(node) for node in source_nodes]
class ComponentDefinition(BaseModel):
type: str
code: str
filename: str
class UIEvent(Event):
type: str
data: BaseModel
def to_response(self) -> dict:
return {
"type": self.type,
"data": self.data.model_dump(),
}
@@ -1,4 +0,0 @@
from llama_index.server.api.routers.chat import chat_router
from llama_index.server.api.routers.ui import custom_components_router
__all__ = ["chat_router", "custom_components_router"]
@@ -1,144 +0,0 @@
import asyncio
import inspect
import logging
import os
from typing import AsyncGenerator, Callable, Union
from fastapi import APIRouter, BackgroundTasks, HTTPException
from fastapi.responses import StreamingResponse
from llama_index.core.agent.workflow.workflow_events import AgentStream
from llama_index.core.workflow import StopEvent, Workflow
from llama_index.server.api.callbacks import (
SourceNodesFromToolCall,
SuggestNextQuestions,
)
from llama_index.server.api.callbacks.base import EventCallback
from llama_index.server.api.callbacks.llamacloud import LlamaCloudFileDownload
from llama_index.server.api.callbacks.stream_handler import StreamHandler
from llama_index.server.api.models import ChatRequest
from llama_index.server.api.utils.vercel_stream import VercelStreamResponse
from llama_index.server.services.llamacloud import LlamaCloudFileService
def chat_router(
workflow_factory: Callable[..., Workflow],
logger: logging.Logger,
) -> APIRouter:
router = APIRouter(prefix="/chat")
@router.post("")
async def chat(
request: ChatRequest,
background_tasks: BackgroundTasks,
) -> StreamingResponse:
try:
user_message = request.messages[-1].to_llamaindex_message()
chat_history = [
message.to_llamaindex_message() for message in request.messages[:-1]
]
# detect if the workflow factory has chat_request as a parameter
factory_sig = inspect.signature(workflow_factory)
if "chat_request" in factory_sig.parameters:
workflow = workflow_factory(chat_request=request)
else:
workflow = workflow_factory()
workflow_handler = workflow.run(
user_msg=user_message.content,
chat_history=chat_history,
)
callbacks: list[EventCallback] = [
SourceNodesFromToolCall(),
LlamaCloudFileDownload(background_tasks),
]
if request.config and request.config.next_question_suggestions:
callbacks.append(SuggestNextQuestions(request))
stream_handler = StreamHandler(
workflow_handler=workflow_handler,
callbacks=callbacks,
)
return VercelStreamResponse(
content_generator=_stream_content(stream_handler, request, logger),
)
except Exception as e:
logger.error(e)
raise HTTPException(status_code=500, detail=str(e))
if LlamaCloudFileService.is_configured():
@router.get("/config/llamacloud")
async def chat_llama_cloud_config() -> dict:
if not os.getenv("LLAMA_CLOUD_API_KEY"):
raise HTTPException(
status_code=500, detail="LlamaCloud API KEY is not configured"
)
projects = LlamaCloudFileService.get_all_projects_with_pipelines()
pipeline = os.getenv("LLAMA_CLOUD_INDEX_NAME")
project = os.getenv("LLAMA_CLOUD_PROJECT_NAME")
pipeline_config = None
if pipeline and project:
pipeline_config = {
"pipeline": pipeline,
"project": project,
}
return {
"projects": projects,
"pipeline": pipeline_config,
}
return router
async def _stream_content(
handler: StreamHandler,
request: ChatRequest,
logger: logging.Logger,
) -> AsyncGenerator[str, None]:
async def _text_stream(
event: Union[AgentStream, StopEvent],
) -> AsyncGenerator[str, None]:
if isinstance(event, AgentStream):
# Normally, if the stream is a tool call, the delta is always empty
# so it's not a text stream.
if len(event.tool_calls) == 0:
yield event.delta
elif isinstance(event, StopEvent):
if isinstance(event.result, str):
yield event.result
elif isinstance(event.result, AsyncGenerator):
async for chunk in event.result:
if isinstance(chunk, str):
yield chunk
elif hasattr(chunk, "delta") and chunk.delta:
yield chunk.delta
stream_started = False
try:
async for event in handler.stream_events():
if not stream_started:
# Start the stream with an empty message
stream_started = True
yield VercelStreamResponse.convert_text("")
# Handle different types of events
if isinstance(event, (AgentStream, StopEvent)):
async for chunk in _text_stream(event):
handler.accumulate_text(chunk)
yield VercelStreamResponse.convert_text(chunk)
elif isinstance(event, dict):
yield VercelStreamResponse.convert_data(event)
elif hasattr(event, "to_response"):
event_response = event.to_response()
yield VercelStreamResponse.convert_data(event_response)
else:
yield VercelStreamResponse.convert_data(event.model_dump())
except asyncio.CancelledError:
logger.warning("Client cancelled the request!")
await handler.cancel_run()
except Exception as e:
logger.error(f"Error in stream response: {e}")
yield VercelStreamResponse.convert_error(str(e))
await handler.cancel_run()
@@ -1,20 +0,0 @@
import logging
from typing import List
from fastapi import APIRouter
from llama_index.server.api.models import ComponentDefinition
from llama_index.server.services.custom_ui import CustomUI
def custom_components_router(
component_dir: str,
logger: logging.Logger,
) -> APIRouter:
router = APIRouter(prefix="/components")
@router.get("")
async def components() -> List[ComponentDefinition]:
custom_ui = CustomUI(component_dir=component_dir, logger=logger)
return custom_ui.get_components()
return router
@@ -1,44 +0,0 @@
import json
import logging
from typing import Any, AsyncGenerator, Union
from fastapi.responses import StreamingResponse
logger = logging.getLogger("uvicorn")
class VercelStreamResponse(StreamingResponse):
"""
Converts preprocessed events into Vercel-compatible streaming response format.
"""
TEXT_PREFIX = "0:"
DATA_PREFIX = "8:"
ERROR_PREFIX = "3:"
def __init__(
self,
content_generator: AsyncGenerator[str, None],
*args: Any,
**kwargs: Any,
):
super().__init__(content_generator, *args, **kwargs)
@classmethod
def convert_text(cls, token: str) -> str:
"""Convert text event to Vercel format."""
# Escape newlines and double quotes to avoid breaking the stream
token = json.dumps(token)
return f"{cls.TEXT_PREFIX}{token}\n"
@classmethod
def convert_data(cls, data: Union[dict, str]) -> str:
"""Convert data event to Vercel format."""
data_str = json.dumps(data) if isinstance(data, dict) else data
return f"{cls.DATA_PREFIX}[{data_str}]\n"
@classmethod
def convert_error(cls, error: str) -> str:
"""Convert error event to Vercel format."""
error_str = json.dumps(error)
return f"{cls.ERROR_PREFIX}{error_str}\n"
@@ -1,55 +0,0 @@
import logging
import shutil
from pathlib import Path
from typing import Optional
import requests
CHAT_UI_VERSION = "0.1.2"
def download_chat_ui(
logger: Optional[logging.Logger] = None, target_path: str = ".ui"
) -> None:
if logger is None:
logger = logging.getLogger("uvicorn")
path = Path(target_path)
temp_dir = _download_package(_get_download_link(CHAT_UI_VERSION))
_copy_ui_files(temp_dir, path)
logger.info("Chat UI downloaded and copied to static folder")
def _get_download_link(version: str) -> str:
"""Get the download link for the chat UI from the npm registry."""
return f"https://registry.npmjs.org/@llamaindex/server/-/server-{version}.tgz"
def _download_package(url: str) -> Path:
"""Download tar.gz file and extract all files into a temporary directory."""
import io
import tarfile
import tempfile
response = requests.get(url, headers={"User-Agent": "Mozilla/5.0"})
content = response.content
temp_dir = Path(tempfile.mkdtemp())
with tarfile.open(fileobj=io.BytesIO(content), mode="r:gz") as tar:
tar.extractall(path=temp_dir)
return temp_dir
def _copy_ui_files(temp_dir: Path, target_path: Path) -> None:
"""Copy files from the .next directory to the static directory."""
target_path.mkdir(parents=True, exist_ok=True)
next_dir = temp_dir / "package/dist/static"
if next_dir.exists():
for item in next_dir.iterdir():
dest = target_path / item.name
if item.is_dir():
shutil.copytree(item, dest, dirs_exist_ok=True)
else:
shutil.copy2(item, dest)
@@ -1,4 +0,0 @@
from .main import generate_event_component
from .parse_workflow_code import get_workflow_event_schemas
__all__ = ["generate_event_component", "get_workflow_event_schemas"]
@@ -1,424 +0,0 @@
import re
from typing import Any, Dict, List, Optional, Type
from llama_index.core.llms import LLM
from llama_index.core.prompts import PromptTemplate
from llama_index.core.workflow import (
Context,
Event,
StartEvent,
StopEvent,
Workflow,
step,
)
from llama_index.server.gen_ui.parse_workflow_code import get_workflow_event_schemas
from pydantic import BaseModel
from rich.console import Console
from rich.live import Live
from rich.panel import Panel
class PlanningEvent(Event):
"""
Event for planning the UI.
"""
events: List[Dict[str, Any]]
class WriteAggregationEvent(Event):
"""
Event for aggregating events.
"""
events: List[Dict[str, Any]]
ui_description: str
class WriteUIComponentEvent(Event):
"""
Event for writing UI component.
"""
events: List[Dict[str, Any]]
aggregation_function: Optional[str]
ui_description: str
class RefineGeneratedCodeEvent(Event):
"""
Refine the generated code.
"""
generated_code: str
aggregation_function_context: Optional[str]
events: List[Dict[str, Any]]
class ExtractEventSchemaEvent(Event):
"""
Extract the event schema from the event.
"""
events: List[Any]
class AggregatePrediction(BaseModel):
"""
Prediction for aggregating events or not.
If need_aggregation is True, the aggregation_function will be provided.
"""
need_aggregation: bool
aggregation_function: Optional[str]
class GenUIWorkflow(Workflow):
"""
Generate UI component for event from workflow.
"""
code_structure: str = """
```jsx
// Note: Only shadcn/ui and lucide-react and tailwind css are allowed.
// shadcn import pattern: import { ComponentName } from "@/components/ui/<component_path>";
// e.g: import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
// import { Button } from "@/components/ui/button";
// import cn from "@/lib/utils"; // clsx is not supported
// export the component
export default function Component({ events }) {
// logic for aggregating events (if needed)
const aggregateEvents = () => {
// code for aggregating events here
}
// State handling
// e.g: const [state, setState] = useState({});
return (
// UI code here
)
}
```
"""
def __init__(self, llm: LLM, **kwargs: Any):
super().__init__(**kwargs)
self.llm = llm
self.console = Console()
self._live: Optional[Live] = None
self._completed_steps: List[str] = []
self._current_step: Optional[str] = None
def update_status(self, message: str, completed: bool = False) -> None:
"""Show completed and current steps in a panel."""
if completed:
if self._current_step:
self._completed_steps.append(self._current_step)
self._current_step = None
else:
self._current_step = message
if self._live is None:
self._live = Live("", console=self.console, refresh_per_second=4)
self._live.start()
# Build status display
status_lines = []
for completed_step in self._completed_steps:
status_lines.append(f"[green]✓[/green] {completed_step}")
if self._current_step:
status_lines.append(f"[yellow]⋯[/yellow] {self._current_step}")
self._live.update(Panel("\n".join(status_lines)))
@step
async def start(self, ctx: Context, ev: StartEvent) -> PlanningEvent:
events = ev.events
if not events:
raise ValueError(
"events is required, provide list of filtered events to generate UI components for"
)
await ctx.set("events", events)
self.update_status("Planning the UI")
return PlanningEvent(events=events)
@step
async def planning(self, ctx: Context, ev: PlanningEvent) -> WriteAggregationEvent:
prompt_template = """
# Your role
You are a designer who is designing a UI for given events that are emitted from a backend workflow.
Here are the events that you need to work on: {events}
# Task
Your task is to analyze the event schema and data and provide a description that how the UI would look like.
The UI should be beautiful, no monotonous, and visually pleasing.
Focus on the elements and the layout, don't ask too much on the styles (transition, dark mode, responsive, etc...).
e.g: Assume that the backend produce list of events with animal name, action, and status.
```
A card-based layout displaying animal actions:
- Each card shows an animal's image at the top
- Below the image: animal name as the card title
- Action details in the card body with an icon (eating 🍖, sleeping 😴, playing 🎾)
- Status badge in the corner showing if action is ongoing/completed
- Expandable section for additional details
- Soft color scheme based on action type
```
Don't be verbose, just return the description for the UI based on the event schema and data.
"""
response = await self.llm.acomplete(
PromptTemplate(prompt_template).format(events=ev.events),
formatted=True,
)
await ctx.set("ui_description", response.text)
self.update_status("Planning the UI", completed=True)
# Update the planning description to the console
self.console.print(
Panel(
response.text,
title="UI Description",
border_style="cyan",
)
)
self.update_status("Generating aggregation function")
return WriteAggregationEvent(
events=ev.events,
ui_description=response.text,
)
@step
async def generate_event_aggregations(
self, ctx: Context, ev: WriteAggregationEvent
) -> WriteUIComponentEvent:
prompt_template = """
# Your role
You are a frontend developer who is developing a React component for given events that are emitted from a backend workflow.
Here are the events that you need to work on: {events}
Here is the description of the UI:
```
{ui_description}
```
# Task
Based on the description of the UI and the list of events, write the aggregation function that will be used to aggregate the events.
Take into account that the list of events grows with time. At the beginning, there is only one event in the list, and events are incrementally added.
To render the events in a visually pleasing way, try to aggregate them by their attributes and render the aggregates instead of just rendering a list of all events.
Don't add computation to the aggregation function, just group the events by their attributes.
Make sure that the aggregation should reflect the description of the UI and the grouped events are not duplicated, make it as simple as possible to avoid unnecessary issues.
# Answer with the following format:
```jsx
const aggregateEvents = () => {
// code for aggregating events here if needed otherwise let the jsx code block empty
}
```
"""
response = await self.llm.acomplete(
PromptTemplate(prompt_template).format(
events=ev.events,
ui_description=ev.ui_description,
),
formatted=True,
)
await ctx.set("aggregation_context", response.text)
self.update_status("Generating aggregation function", completed=True)
self.update_status("Generating UI components")
return WriteUIComponentEvent(
events=ev.events,
aggregation_function=response.text,
ui_description=ev.ui_description,
)
@step
async def write_ui_component(
self, ctx: Context, ev: WriteUIComponentEvent
) -> RefineGeneratedCodeEvent:
prompt_template = """
# Your role
You are a frontend developer who is developing a React component using shadcn/ui (@/components/ui/<component_name>) and lucide-react for the UI.
You are given a list of events and other context.
Your task is to write a beautiful UI for the events that will be included in a chat UI.
# Context:
Here are the events that you need to work on: {events}
{aggregation_function_context}
Here is the description of the UI:
```
{ui_description}
```
# Requirements:
- Write beautiful UI components for the events using shadcn/ui and lucide-react.
- The component text/label should be specified for each event type.
# Instructions:
## Event and schema notice
- Based on the provided list of events, determine their types and attributes.
- It's normal that the schema is applied to all events, but the events might completely different which some of schema attributes aren't used.
- You should make the component visually distinct for each event type.
e.g: A simple cat schema
```{"type": "cat", "action": ["jump", "run", "meow"], "jump": {"height": 10, "distance": 20}, "run": {"distance": 100}}```
You should display the jump, run and meow actions in different ways. don't try to render "height" for the "run" and "meow" action.
## UI notice
- Use shadcn/ui and lucide-react and tailwind CSS for the UI.
- Be careful on state handling, make sure the update should be updated in the state and there is no duplicate state.
- For a long content, consider to use markdown along with dropdown to show the full content.
e.g:
```jsx
import { Markdown } from "@llamaindex/chat-ui/widgets";
<Markdown content={content} />
```
- Try to make the component placement not monotonous, consider use row/column/flex/grid layout.
"""
aggregation_function_context = (
f"\nBefore rendering the events, we're using the following aggregation function: {ev.aggregation_function}"
if ev.aggregation_function
else ""
)
prompt = PromptTemplate(prompt_template).format(
events=ev.events,
aggregation_function_context=aggregation_function_context,
code_structure=self.code_structure,
ui_description=ev.ui_description,
)
response = await self.llm.acomplete(prompt, formatted=True)
self.update_status("Generating UI components", completed=True)
self.update_status("Refining generated code")
return RefineGeneratedCodeEvent(
generated_code=response.text,
events=ev.events,
aggregation_function_context=aggregation_function_context,
)
@step
async def refine_code(
self, ctx: Context, ev: RefineGeneratedCodeEvent
) -> StopEvent:
prompt_template = """
# Your role
You are a frontend developer who is developing a React component for given events that are emitted from a backend workflow.
Your task is to assemble the pieces of code into a complete code segment that follows the specified code structure.
# Context:
## Here is the generated code:
{generated_code}
{aggregation_function_context}
## The generated code should follow the following structure:
{code_structure}
# Requirements:
- Refine the code if needed to ensure there are no potential bugs.
- Be careful on code placement, make sure it doesn't call any undefined code.
- Make sure the import statements are correct.
e.g: import { Button, Card, Accordion } from "@/components/ui" is correct because Button, Card are defined in different shadcn/ui components.
-> correction: import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
- Don't be verbose, only return the code, wrap it in ```jsx <code>```
"""
prompt = PromptTemplate(prompt_template).format(
generated_code=ev.generated_code,
code_structure=self.code_structure,
aggregation_function_context=ev.aggregation_function_context,
)
response = await self.llm.acomplete(prompt, formatted=True)
# Extract code from response, handling case where code block is missing
code_match = re.search(r"```jsx(.*)```", response.text, re.DOTALL)
if code_match is None:
# If no code block found, use full response
code = response.text
else:
code = code_match.group(1).strip()
self.update_status("Refining generated code", completed=True)
if self._live is not None:
self._live.stop()
self._live = None
return StopEvent(
result=code,
)
async def generate_event_component(
workflow_file: Optional[str] = None,
event_cls: Optional[Type[BaseModel]] = None,
llm: Optional[LLM] = None,
) -> str:
"""
Generate UI component for events from workflow.
Either workflow_file or event_cls must be provided.
Args:
workflow_file: The path to the workflow file that contains the event to generate UI for. e.g: `app/workflow.py`.
event_cls: A Pydantic class to generate UI for. e.g: `DeepResearchEvent`.
llm: The LLM to use for the generation. Default is Anthropic's Claude 3.7 Sonnet.
We recommend using these LLMs:
- Anthropic's Claude 3.7 Sonnet
- OpenAI's GPT-4.1
- Google Gemini 2.5 Pro
Returns:
The generated UI component code.
"""
if workflow_file is None and event_cls is None:
raise ValueError(
"Either workflow_file or event_cls must be provided. Please provide one of them."
)
if workflow_file is not None and event_cls is not None:
raise ValueError(
"Only one of workflow_file or event_cls can be provided. Please provide only one of them."
)
if llm is None:
from llama_index.llms.anthropic import Anthropic
llm = Anthropic(model="claude-3-7-sonnet-latest", max_tokens=8192)
console = Console()
# Get event schemas
if workflow_file is not None:
# Get event schemas from the input file
console.rule("[bold blue]Analyzing Events[/bold blue]")
event_schemas = get_workflow_event_schemas(workflow_file)
if len(event_schemas) == 0:
console.print(
Panel(
"[red]No events found that are used with write_event_to_stream[/red]",
title="❌ Error",
border_style="red",
)
)
raise RuntimeError(
"No events found that are used with write_event_to_stream. Please check the workflow file."
)
elif event_cls is not None:
event_schemas = [
{"type": event_cls.__name__, "schema": event_cls.model_json_schema()}
]
# Generate UI component from event schemas
console.rule("[bold blue]Generate UI Components[/bold blue]")
workflow = GenUIWorkflow(llm=llm, timeout=500.0)
code = await workflow.run(events=event_schemas)
console.print(
Panel(
"[green]UI component has been generated successfully![/green]\n",
title="✨ Complete",
border_style="green",
)
)
return code
@@ -1,93 +0,0 @@
import ast
import importlib
import inspect
import os
import sys
from typing import Any, Dict, List
class EventAnalyzer(ast.NodeVisitor):
"""
Parse the workflow code to find UIEvent instances passed to write_event_to_stream.
"""
def __init__(self) -> None:
self.found_ui_event = False
def visit_Call(self, node: ast.Call) -> None:
# Check for ctx.write_event_to_stream call with UIEvent arg
if (
isinstance(node.func, ast.Attribute)
and isinstance(node.func.value, ast.Name)
and node.func.attr == "write_event_to_stream"
and node.args
and isinstance(node.args[0], ast.Call)
and isinstance(node.args[0].func, ast.Name)
and node.args[0].func.id == "UIEvent"
):
self.found_ui_event = True
self.generic_visit(node)
def get_workflow_event_schemas(file_path: str) -> List[Dict[str, Any]]:
"""
Find UIEvent instances passed to write_event_to_stream and return their data type schema.
"""
# Get absolute path for module importing
abs_file_path = os.path.abspath(file_path)
project_root = os.path.dirname(os.path.dirname(abs_file_path))
# Convert file path to module name
rel_path = os.path.relpath(abs_file_path, project_root)
module_name = rel_path.replace(os.sep, ".").replace(".py", "")
# Temporarily modify sys.path to allow imports
original_path = list(sys.path)
if project_root not in sys.path:
sys.path.insert(0, project_root)
try:
# Import the module
module = importlib.import_module(module_name)
importlib.reload(module)
except ImportError as e:
print(f"Error importing module {module_name}: {e}")
sys.path = original_path
return []
finally:
# Restore original path
if project_root in sys.path and project_root not in original_path:
sys.path.remove(project_root)
# Parse the file to check for UIEvent usage
try:
with open(file_path, "r") as f:
tree = ast.parse(f.read())
except (FileNotFoundError, SyntaxError) as e:
print(f"Error parsing {file_path}: {e}")
return []
# Check if UIEvent is passed to write_event_to_stream
analyzer = EventAnalyzer()
analyzer.visit(tree)
schema_list = []
# Only proceed if UIEvent was found and the module has the class
if analyzer.found_ui_event and hasattr(module, "UIEvent"):
# Look for class names containing "EventData" in the module
for name, obj in inspect.getmembers(module):
if (
inspect.isclass(obj)
and name.endswith("EventData")
and hasattr(obj, "model_json_schema")
):
try:
schema = obj.model_json_schema()
if schema:
schema_list.append(schema)
except Exception:
pass
return schema_list
@@ -1,230 +0,0 @@
import json
import logging
import os
from typing import Any, Callable, Optional, Union
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from llama_index.core.workflow import Workflow
from llama_index.server.api.routers import chat_router, custom_components_router
from llama_index.server.chat_ui import download_chat_ui
from llama_index.server.settings import server_settings
from pydantic import BaseModel, Field
class UIConfig(BaseModel):
enabled: bool = Field(default=True, description="Whether to enable the chat UI")
app_title: str = Field(
default="LlamaIndex Server", description="The title of the chat UI"
)
starter_questions: Optional[list[str]] = Field(
default=None, description="The starter questions for the chat UI"
)
llamacloud_index_selector: bool = Field(
default=False,
description="Whether to show the LlamaCloud index selector in the chat UI (need to set the LLAMA_CLOUD_API_KEY environment variable)",
)
ui_path: str = Field(
default=".ui", description="The path that stores static files for the chat UI"
)
component_dir: Optional[str] = Field(
default=None, description="The directory to custom UI components code"
)
def get_config_content(self) -> str:
return json.dumps(
{
"CHAT_API": f"{server_settings.api_url}/chat",
"STARTER_QUESTIONS": self.starter_questions or [],
"LLAMA_CLOUD_API": f"{server_settings.api_url}/chat/config/llamacloud"
if self.llamacloud_index_selector and os.getenv("LLAMA_CLOUD_API_KEY")
else None,
"APP_TITLE": self.app_title,
"COMPONENTS_API": f"{server_settings.api_url}/components"
if self.component_dir
else None,
},
indent=2,
)
class LlamaIndexServer(FastAPI):
workflow_factory: Callable[..., Workflow]
verbose: bool = False
ui_config: UIConfig
def __init__(
self,
workflow_factory: Callable[..., Workflow],
logger: Optional[logging.Logger] = None,
use_default_routers: Optional[bool] = True,
env: Optional[str] = None,
ui_config: Optional[Union[UIConfig, dict]] = None,
server_url: Optional[str] = None,
api_prefix: Optional[str] = None,
verbose: bool = False,
*args: Any,
**kwargs: Any,
):
"""
Initialize the LlamaIndexServer.
Args:
workflow_factory: A factory function that creates a workflow instance for each request.
logger: The logger to use.
use_default_routers: Whether to use the default routers (chat, mount `data` and `output` directories).
env: The environment to run the server in.
ui_config: The configuration for the chat UI.
server_url: The URL of the server.
api_prefix: The prefix for the API endpoints.
verbose: Whether to show verbose logs.
"""
super().__init__(*args, **kwargs)
self.workflow_factory = workflow_factory
self.logger = logger or logging.getLogger("uvicorn")
self.verbose = verbose
self.use_default_routers = use_default_routers or True
if ui_config is None:
self.ui_config = UIConfig()
elif isinstance(ui_config, dict):
self.ui_config = UIConfig(**ui_config)
else:
self.ui_config = ui_config
# Update the settings
if server_url:
server_settings.set_url(server_url)
if api_prefix:
server_settings.set_api_prefix(api_prefix)
if self.use_default_routers:
self.add_default_routers()
if str(env).lower() == "dev":
self.allow_cors("*")
if self.ui_config.enabled is None:
self.ui_config.enabled = True
if self.ui_config.enabled is None:
self.ui_config.enabled = False
if self.ui_config.enabled:
self.mount_ui()
# Default routers
def add_default_routers(self) -> None:
self.add_chat_router()
self.mount_data_dir()
self.mount_output_dir()
def add_chat_router(self) -> None:
"""
Add the chat router.
"""
self.include_router(
chat_router(
self.workflow_factory,
self.logger,
),
prefix=server_settings.api_prefix,
)
def add_components_router(self) -> None:
"""
Add the UI router.
"""
if self.ui_config.component_dir is None:
raise ValueError("component_dir must be specified to add components router")
self.include_router(
custom_components_router(self.ui_config.component_dir, self.logger),
prefix=server_settings.api_prefix,
)
def mount_ui(self) -> None:
"""
Mount the UI.
"""
# Check if the static folder exists
if self.ui_config.enabled:
# Component dir
if self.ui_config.component_dir:
if not os.path.exists(self.ui_config.component_dir):
os.makedirs(self.ui_config.component_dir)
self.add_components_router()
# UI static files
if not os.path.exists(self.ui_config.ui_path):
os.makedirs(self.ui_config.ui_path)
self.logger.warning(
f"UI files not found, downloading UI to {self.ui_config.ui_path}"
)
download_chat_ui(logger=self.logger, target_path=self.ui_config.ui_path)
self._mount_static_files(
directory=self.ui_config.ui_path, path="/", html=True
)
self._override_ui_config()
def _override_ui_config(self) -> None:
"""
Override the UI config by writing a complete configuration file.
"""
try:
config_path = os.path.join(self.ui_config.ui_path, "config.js")
if not os.path.exists(config_path):
self.logger.error("Config file not found")
return
config_content = (
f"window.LLAMAINDEX = {self.ui_config.get_config_content()};"
)
with open(config_path, "w") as f:
f.write(config_content)
except Exception as e:
self.logger.error(f"Error overriding UI config: {e}")
def mount_data_dir(self, data_dir: str = "data") -> None:
"""
Mount the data directory.
"""
self._mount_static_files(
directory=data_dir,
path=f"{server_settings.api_prefix}/files/data",
html=True,
)
def mount_output_dir(self, output_dir: str = "output") -> None:
"""
Mount the output directory.
"""
self._mount_static_files(
directory=output_dir,
path=f"{server_settings.api_prefix}/files/output",
html=True,
)
def _mount_static_files(
self, directory: str, path: str, html: bool = False
) -> None:
"""
Mount static files from a directory if it exists.
"""
if os.path.exists(directory):
self.logger.info(f"Mounting static files '{directory}' at '{path}'")
self.mount(
path,
StaticFiles(directory=directory, check_dir=False, html=html),
name=f"{directory}-static",
)
def allow_cors(self, origin: str = "*") -> None:
"""
Allow CORS for a specific origin.
"""
self.add_middleware(
CORSMiddleware,
allow_origins=[origin],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@@ -1,81 +0,0 @@
import logging
import os
from typing import List, Optional
from llama_index.server.api.models import ComponentDefinition
class CustomUI:
def __init__(
self, component_dir: str, logger: Optional[logging.Logger] = None
) -> None:
self.component_dir = component_dir
self.logger = logger or logging.getLogger(__name__)
def get_components(self) -> List[ComponentDefinition]:
"""
List all js files in the component directory and return a list of ComponentDefinition objects.
Ignores files that fail to load and logs the error.
TSX files take precedence over JSX files when duplicate component names are found.
"""
components_dict: dict[str, ComponentDefinition] = {}
if not os.path.exists(self.component_dir):
self.logger.warning(
f"Component directory {self.component_dir} does not exist"
)
return []
try:
for file in os.listdir(self.component_dir):
if not file.endswith((".jsx", ".tsx")):
continue
component_name = file.split(".")[0]
file_path = os.path.join(self.component_dir, file)
file_ext = os.path.splitext(file)[1]
try:
with open(file_path, "r", encoding="utf-8") as f:
code = f.read()
new_component = ComponentDefinition(
type=component_name,
code=code,
filename=file,
)
if component_name in components_dict:
existing_ext = os.path.splitext(
components_dict[component_name].filename
)[1]
# If existing is TSX and new is JSX, skip and warn
if existing_ext == ".tsx" and file_ext == ".jsx":
self.logger.warning(
f"Skipping duplicate JSX component {file} as TSX version already exists"
)
continue
# If both are same extension, warn and skip
if existing_ext == file_ext:
self.logger.warning(
f"Skipping duplicate component {file} with same extension"
)
continue
# If existing is JSX and new is TSX, replace and warn
if existing_ext == ".jsx" and file_ext == ".tsx":
self.logger.warning(
f"Replacing JSX component {components_dict[component_name].filename} with TSX version {file}"
)
components_dict[component_name] = new_component
continue
components_dict[component_name] = new_component
except Exception as e:
self.logger.error(f"Failed to load component {file}: {str(e)}")
continue
except Exception as e:
self.logger.error(f"Error reading component directory: {str(e)}")
return list(components_dict.values())
@@ -1,117 +0,0 @@
import logging
import os
import re
import uuid
from pathlib import Path
from typing import List, Optional, Union
from llama_index.server.settings import server_settings
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
PRIVATE_STORE_PATH = str(Path("output", "uploaded"))
TOOL_STORE_PATH = str(Path("output", "tools"))
LLAMA_CLOUD_STORE_PATH = str(Path("output", "llamacloud"))
class DocumentFile(BaseModel):
id: str
name: str # Stored file name
type: Optional[str] = None
size: Optional[int] = None
url: Optional[str] = None
path: Optional[str] = Field(
None,
description="The stored file path. Used internally in the server.",
exclude=True,
)
refs: Optional[List[str]] = Field(
None, description="The document ids in the index."
)
class FileService:
"""
To store the files uploaded by the user.
"""
@classmethod
def save_file(
cls,
content: Union[bytes, str],
file_name: str,
save_dir: Optional[str] = None,
) -> DocumentFile:
"""
Save the content to a file in the local file server (accessible via URL).
Args:
content (bytes | str): The content to save, either bytes or string.
file_name (str): The original name of the file.
save_dir (Optional[str]): The relative path from the current working directory. Defaults to the `output/uploaded` directory.
Returns:
The metadata of the saved file.
"""
if save_dir is None:
save_dir = os.path.join("output", "uploaded")
file_id = str(uuid.uuid4())
name, extension = os.path.splitext(file_name)
extension = extension.lstrip(".")
sanitized_name = _sanitize_file_name(name)
if extension == "":
raise ValueError("File is not supported!")
new_file_name = f"{sanitized_name}_{file_id}.{extension}"
file_path = os.path.join(save_dir, new_file_name)
if isinstance(content, str):
content = content.encode()
try:
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "wb") as file:
file.write(content)
except PermissionError as e:
logger.error(f"Permission denied when writing to file {file_path}: {e!s}")
raise
except OSError as e:
logger.error(f"IO error occurred when writing to file {file_path}: {e!s}")
raise
except Exception as e:
logger.error(f"Unexpected error when writing to file {file_path}: {e!s}")
raise
logger.info(f"Saved file to {file_path}")
file_size = os.path.getsize(file_path)
file_url = (
f"{server_settings.file_server_url_prefix}/{save_dir}/{new_file_name}"
)
return DocumentFile(
id=file_id,
name=new_file_name,
type=extension,
size=file_size,
path=file_path,
url=file_url,
refs=None,
)
@classmethod
def get_file_url(cls, file_name: str, save_dir: Optional[str] = None) -> str:
"""
Get the URL of a file.
"""
if save_dir is None:
save_dir = os.path.join("output", "uploaded")
return f"{server_settings.file_server_url_prefix}/{save_dir}/{file_name}"
def _sanitize_file_name(file_name: str) -> str:
"""
Sanitize the file name by replacing all non-alphanumeric characters with underscores.
"""
return re.sub(r"[^a-zA-Z0-9.]", "_", file_name)
@@ -1,11 +0,0 @@
from .file import LlamaCloudFileService
from .generate import load_to_llamacloud
from .index import LlamaCloudIndex, get_client, get_index
__all__ = [
"LlamaCloudFileService",
"LlamaCloudIndex",
"get_client",
"get_index",
"load_to_llamacloud",
]
@@ -1,184 +0,0 @@
import logging
import os
import time
import typing
from io import BytesIO
from typing import Any, Dict, List, Optional, Set, Tuple, Union
import requests
from fastapi import BackgroundTasks
from llama_cloud import ManagedIngestionStatus, PipelineFileCreateCustomMetadataValue
from llama_index.core.schema import NodeWithScore
from llama_index.server.api.models import SourceNodes
from llama_index.server.services.llamacloud.index import get_client
from pydantic import BaseModel
logger = logging.getLogger("uvicorn")
class LlamaCloudFile(BaseModel):
file_name: str
pipeline_id: str
def __eq__(self, other: Any) -> bool:
if not isinstance(other, LlamaCloudFile):
return NotImplemented
return (
self.file_name == other.file_name and self.pipeline_id == other.pipeline_id
)
def __hash__(self) -> int:
return hash((self.file_name, self.pipeline_id))
class LlamaCloudFileService:
LOCAL_STORE_PATH = "output/llamacloud"
DOWNLOAD_FILE_NAME_TPL = "{pipeline_id}${filename}"
@classmethod
def get_all_projects_with_pipelines(cls) -> List[Dict[str, Any]]:
try:
client = get_client()
projects = client.projects.list_projects()
pipelines = client.pipelines.search_pipelines()
return [
{
**(project.dict()),
"pipelines": [
{"id": p.id, "name": p.name}
for p in pipelines
if p.project_id == project.id
],
}
for project in projects
]
except Exception as error:
logger.error(f"Error listing projects and pipelines: {error}")
return []
@classmethod
def add_file_to_pipeline(
cls,
project_id: str,
pipeline_id: str,
upload_file: Union[typing.IO, Tuple[str, BytesIO]],
custom_metadata: Optional[Dict[str, PipelineFileCreateCustomMetadataValue]],
wait_for_processing: bool = True,
) -> str:
client = get_client()
file = client.files.upload_file(project_id=project_id, upload_file=upload_file)
file_id = file.id
files = [
{
"file_id": file_id,
"custom_metadata": {"file_id": file_id, **(custom_metadata or {})},
}
]
files = client.pipelines.add_files_to_pipeline_api(pipeline_id, request=files)
if not wait_for_processing:
return file_id
# Wait 2s for the file to be processed
max_attempts = 20
attempt = 0
while attempt < max_attempts:
result = client.pipelines.get_pipeline_file_status(
file_id=file_id, pipeline_id=pipeline_id
)
if result.status == ManagedIngestionStatus.ERROR:
raise Exception(f"File processing failed: {str(result)}")
if result.status == ManagedIngestionStatus.SUCCESS:
# File is ingested - return the file id
return file_id
attempt += 1
time.sleep(0.1) # Sleep for 100ms
raise Exception(
f"File processing did not complete after {max_attempts} attempts."
)
@classmethod
def download_pipeline_file(
cls,
file: LlamaCloudFile,
force_download: bool = False,
) -> None:
client = get_client()
file_name = file.file_name
pipeline_id = file.pipeline_id
# Check is the file already exists
downloaded_file_path = cls._get_file_path(file_name, pipeline_id)
if os.path.exists(downloaded_file_path) and not force_download:
logger.debug(f"File {file_name} already exists in local storage")
return
try:
logger.info(f"Downloading file {file_name} for pipeline {pipeline_id}")
files = client.pipelines.list_pipeline_files(pipeline_id)
if not files or not isinstance(files, list):
raise Exception("No files found in LlamaCloud")
for file_entry in files:
if file_entry.name == file_name:
file_id = file_entry.file_id
project_id = file_entry.project_id
file_detail = client.files.read_file_content(
file_id, project_id=project_id
)
cls._download_file(file_detail.url, downloaded_file_path)
break
except Exception as error:
logger.info(f"Error fetching file from LlamaCloud: {error}")
@classmethod
def download_files_from_nodes(
cls, nodes: List[NodeWithScore], background_tasks: BackgroundTasks
) -> None:
files = cls._get_files_to_download(nodes)
for file in files:
logger.info(f"Adding download of {file.file_name} to background tasks")
background_tasks.add_task(cls.download_pipeline_file, file)
@classmethod
def _get_files_to_download(cls, nodes: List[NodeWithScore]) -> Set[LlamaCloudFile]:
source_nodes = SourceNodes.from_source_nodes(nodes)
llama_cloud_files = [
LlamaCloudFile(
file_name=node.metadata.get("file_name"), # type: ignore
pipeline_id=node.metadata.get("pipeline_id"), # type: ignore
)
for node in source_nodes
if (
node.metadata.get("pipeline_id") is not None
and node.metadata.get("file_name") is not None
)
]
# Remove duplicates and return
return set(llama_cloud_files)
@classmethod
def _get_file_name(cls, name: str, pipeline_id: str) -> str:
return cls.DOWNLOAD_FILE_NAME_TPL.format(pipeline_id=pipeline_id, filename=name)
@classmethod
def _get_file_path(cls, name: str, pipeline_id: str) -> str:
return os.path.join(cls.LOCAL_STORE_PATH, cls._get_file_name(name, pipeline_id))
@classmethod
def _download_file(cls, url: str, local_file_path: str) -> None:
logger.info(f"Saving file to {local_file_path}")
# Create directory if it doesn't exist
os.makedirs(cls.LOCAL_STORE_PATH, exist_ok=True)
# Download the file
with requests.get(url, stream=True) as r:
r.raise_for_status()
with open(local_file_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
logger.info("File downloaded successfully")
@classmethod
def is_configured(cls) -> bool:
try:
return os.environ.get("LLAMA_CLOUD_API_KEY") is not None
except Exception:
return False
@@ -1,56 +0,0 @@
import logging
from typing import Optional
from tqdm import tqdm
from llama_index.core.readers import SimpleDirectoryReader
from llama_index.indices.managed.llama_cloud import LlamaCloudIndex
from llama_index.server.services.llamacloud.file import LlamaCloudFileService
def load_to_llamacloud(
index: LlamaCloudIndex,
data_dir: Optional[str] = None,
recursive: Optional[bool] = None,
logger: Optional[logging.Logger] = None,
) -> None:
if logger is None:
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
logger.info("Generate index for the provided data")
# use SimpleDirectoryReader to retrieve the files to process
reader = SimpleDirectoryReader(
data_dir or "data",
recursive=recursive or True,
)
files_to_process = reader.input_files
# add each file to the LlamaCloud pipeline
error_files = []
for input_file in tqdm(
files_to_process,
desc="Processing files",
unit="file",
):
with open(input_file, "rb") as f:
logger.debug(
f"Adding file {input_file} to pipeline {index.name} in project {index.project_name}"
)
try:
LlamaCloudFileService.add_file_to_pipeline(
index.project.id,
index.pipeline.id,
f,
custom_metadata={},
wait_for_processing=False,
)
except Exception as e:
error_files.append(input_file)
logger.error(f"Error adding file {input_file}: {e}")
if error_files:
logger.error(f"Failed to add the following files: {error_files}")
logger.info("Finished generating the index")
@@ -1,164 +0,0 @@
import logging
import os
from typing import TYPE_CHECKING, Any, Optional
from llama_cloud import PipelineType
from llama_index.core.callbacks import CallbackManager
from llama_index.core.ingestion.api_utils import (
get_client as llama_cloud_get_client,
)
from llama_index.core.settings import Settings
from llama_index.indices.managed.llama_cloud import LlamaCloudIndex
from llama_index.server.api.models import ChatRequest
from pydantic import BaseModel, Field, field_validator
if TYPE_CHECKING:
from llama_cloud.client import LlamaCloud
logger = logging.getLogger("uvicorn")
class LlamaCloudConfig(BaseModel):
# Private attributes
api_key: str = Field(
exclude=True, # Exclude from the model representation
)
base_url: Optional[str] = Field(
exclude=True,
)
organization_id: Optional[str] = Field(
exclude=True,
)
# Configuration attributes, can be set by the user
pipeline: str = Field(
description="The name of the pipeline to use",
)
project: str = Field(
description="The name of the LlamaCloud project",
)
def __init__(self, **kwargs: Any) -> None:
if "api_key" not in kwargs:
kwargs["api_key"] = os.getenv("LLAMA_CLOUD_API_KEY")
if "base_url" not in kwargs:
kwargs["base_url"] = os.getenv("LLAMA_CLOUD_BASE_URL")
if "organization_id" not in kwargs:
kwargs["organization_id"] = os.getenv("LLAMA_CLOUD_ORGANIZATION_ID")
if "pipeline" not in kwargs:
kwargs["pipeline"] = os.getenv("LLAMA_CLOUD_INDEX_NAME")
if "project" not in kwargs:
kwargs["project"] = os.getenv("LLAMA_CLOUD_PROJECT_NAME")
super().__init__(**kwargs)
# Validate and throw error if the env variables are not set before starting the app
@field_validator("pipeline", "project", "api_key", mode="before")
@classmethod
def validate_fields(cls, value: Any) -> Any:
if value is None:
raise ValueError(
"Please set LLAMA_CLOUD_INDEX_NAME, LLAMA_CLOUD_PROJECT_NAME and LLAMA_CLOUD_API_KEY"
" to your environment variables or config them in .env file"
)
return value
def to_client_kwargs(self) -> dict:
return {
"api_key": self.api_key,
"base_url": self.base_url,
}
class IndexConfig(BaseModel):
llama_cloud_pipeline_config: LlamaCloudConfig = Field(
default_factory=lambda: LlamaCloudConfig(),
alias="llamaCloudPipeline",
)
callback_manager: Optional[CallbackManager] = Field(
default=None,
)
def to_index_kwargs(self) -> dict:
return {
"name": self.llama_cloud_pipeline_config.pipeline,
"project_name": self.llama_cloud_pipeline_config.project,
"api_key": self.llama_cloud_pipeline_config.api_key,
"base_url": self.llama_cloud_pipeline_config.base_url,
"organization_id": self.llama_cloud_pipeline_config.organization_id,
"callback_manager": self.callback_manager,
}
@classmethod
def from_default(cls, chat_request: Optional[ChatRequest] = None) -> "IndexConfig":
default_config = cls()
if chat_request is not None and chat_request.data is not None:
llamacloud_config = chat_request.data.get("llamaCloudPipeline")
if llamacloud_config is not None:
default_config.llama_cloud_pipeline_config.pipeline = llamacloud_config[
"pipeline"
]
default_config.llama_cloud_pipeline_config.project = llamacloud_config[
"project"
]
return default_config
def get_index(
chat_request: Optional[ChatRequest] = None,
create_if_missing: bool = False,
) -> Optional[LlamaCloudIndex]:
config = IndexConfig.from_default(chat_request)
# Check whether the index exists
try:
index = LlamaCloudIndex(**config.to_index_kwargs())
return index
except ValueError:
logger.warning("Index not found")
if create_if_missing:
logger.info("Creating index")
_create_index(config)
return LlamaCloudIndex(**config.to_index_kwargs())
return None
def get_client() -> "LlamaCloud":
config = LlamaCloudConfig()
return llama_cloud_get_client(**config.to_client_kwargs())
def _create_index(
config: IndexConfig,
) -> None:
client = get_client()
pipeline_name = config.llama_cloud_pipeline_config.pipeline
pipelines = client.pipelines.search_pipelines(
pipeline_name=pipeline_name,
pipeline_type=PipelineType.MANAGED.value,
)
if len(pipelines) == 0:
from llama_index.embeddings.openai import OpenAIEmbedding
if not isinstance(Settings.embed_model, OpenAIEmbedding):
raise ValueError(
"Creating a new pipeline with a non-OpenAI embedding model is not supported."
)
client.pipelines.upsert_pipeline(
request={
"name": pipeline_name,
"embedding_config": {
"type": "OPENAI_EMBEDDING",
"component": {
"api_key": os.getenv("OPENAI_API_KEY"), # editable
"model_name": Settings.embed_model.model_name
or "text-embedding-3-small",
},
},
"transform_config": {
"mode": "auto",
"config": {
"chunk_size": Settings.chunk_size, # editable
"chunk_overlap": Settings.chunk_overlap, # editable
},
},
},
)
@@ -1,95 +0,0 @@
import logging
import os
import re
from typing import List, Optional, Union
from llama_index.core.prompts import PromptTemplate
from llama_index.core.settings import Settings
from llama_index.server.api.models import ChatAPIMessage
logger = logging.getLogger("uvicorn")
class SuggestNextQuestionsService:
"""
Suggest the next questions that user might ask based on the conversation history.
"""
prompt = PromptTemplate(
r"""
You're a helpful assistant! Your task is to suggest the next questions that user might interested in to keep the conversation going.
Here is the conversation history
---------------------
{conversation}
---------------------
Given the conversation history, please give me 3 questions that user might ask next!
Your answer should be wrapped in three sticks without any index numbers and follows the following format:
\`\`\`
<question 1>
<question 2>
<question 3>
\`\`\`
"""
)
@classmethod
def get_configured_prompt(cls) -> PromptTemplate:
prompt = os.getenv("NEXT_QUESTION_PROMPT", None)
if not prompt:
return cls.prompt
return PromptTemplate(prompt)
@classmethod
async def suggest_next_questions_all_messages(
cls,
messages: List[ChatAPIMessage],
) -> Optional[List[str]]:
"""
Suggest the next questions that user might ask based on the conversation history.
"""
prompt_template = cls.get_configured_prompt()
try:
# Reduce the cost by only using the last two messages
last_user_message = None
last_assistant_message = None
for message in reversed(messages):
if message.role == "user":
last_user_message = f"User: {message.content}"
elif message.role == "assistant":
last_assistant_message = f"Assistant: {message.content}"
if last_user_message and last_assistant_message:
break
conversation: str = f"{last_user_message}\n{last_assistant_message}"
# Call the LLM and parse questions from the output
prompt = prompt_template.format(conversation=conversation)
output = await Settings.llm.acomplete(prompt)
return cls._extract_questions(output.text)
except Exception as e:
logger.error(f"Error when generating next question: {e}")
return None
@classmethod
def _extract_questions(cls, text: str) -> Union[List[str], None]:
content_match = re.search(r"```(.*?)```", text, re.DOTALL)
content = content_match.group(1) if content_match else None
if not content:
return None
return [q.strip() for q in content.split("\n") if q.strip()]
@classmethod
async def run(
cls,
chat_history: List[ChatAPIMessage],
response: str,
) -> Optional[List[str]]:
"""
Suggest the next questions that user might ask based on the chat history and the last response.
"""
messages = [
*chat_history,
ChatAPIMessage(role="assistant", content=response), # type: ignore
]
return await cls.suggest_next_questions_all_messages(messages)
@@ -1,47 +0,0 @@
from pydantic import Field, validator
from pydantic_settings import BaseSettings
class ServerSettings(BaseSettings):
url: str = Field(
default="",
description="The deployment URL of the server, to be referenced by tools and file services",
)
api_prefix: str = Field(
default="/api",
description="The prefix for the API endpoints",
)
@property
def file_server_url_prefix(self) -> str:
return f"{self.url}{self.api_prefix}/files"
@property
def api_url(self) -> str:
return f"{self.url}{self.api_prefix}"
@validator("url")
def validate_url(cls, v: str) -> str:
if v.endswith("/"):
raise ValueError("URL must not end with a '/'")
return v
@validator("api_prefix")
def validate_api_prefix(cls, v: str) -> str:
if not v.startswith("/"):
raise ValueError("API prefix must start with a '/'")
return v
def set_url(self, v: str) -> None:
self.url = v
self.validate_url(v) # type: ignore
def set_api_prefix(self, v: str) -> None:
self.api_prefix = v
self.validate_api_prefix(v) # type: ignore
class Config:
env_file_encoding = "utf-8"
server_settings = ServerSettings()
@@ -1,242 +0,0 @@
import logging
import os
import re
from enum import Enum
from io import BytesIO
from llama_index.core.tools.function_tool import FunctionTool
OUTPUT_DIR = "output/tools"
class DocumentType(Enum):
PDF = "pdf"
HTML = "html"
COMMON_STYLES = """
body {
font-family: Arial, sans-serif;
line-height: 1.3;
color: #333;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 1em;
margin-bottom: 0.5em;
}
p {
margin-bottom: 0.7em;
}
code {
background-color: #f4f4f4;
padding: 2px 4px;
border-radius: 4px;
}
pre {
background-color: #f4f4f4;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
}
table {
border-collapse: collapse;
width: 100%;
margin-bottom: 1em;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
font-weight: bold;
}
"""
HTML_SPECIFIC_STYLES = """
body {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
"""
PDF_SPECIFIC_STYLES = """
@page {
size: letter;
margin: 2cm;
}
body {
font-size: 11pt;
}
h1 { font-size: 18pt; }
h2 { font-size: 16pt; }
h3 { font-size: 14pt; }
h4, h5, h6 { font-size: 12pt; }
pre, code {
font-family: Courier, monospace;
font-size: 0.9em;
}
"""
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
{common_styles}
{specific_styles}
</style>
</head>
<body>
{content}
</body>
</html>
"""
class DocumentGenerator:
def __init__(self, file_server_url_prefix: str):
if not file_server_url_prefix:
raise ValueError("file_server_url_prefix is required")
self.file_server_url_prefix = file_server_url_prefix
@classmethod
def _generate_html_content(cls, original_content: str) -> str:
"""
Generate HTML content from the original markdown content.
"""
try:
import markdown # type: ignore
except ImportError:
raise ImportError(
"Failed to import required modules. Please install markdown."
)
# Convert markdown to HTML with fenced code and table extensions
return markdown.markdown(original_content, extensions=["fenced_code", "tables"])
@classmethod
def _generate_pdf(cls, html_content: str) -> BytesIO:
"""
Generate a PDF from the HTML content.
"""
try:
from xhtml2pdf import pisa
except ImportError:
raise ImportError(
"Failed to import required modules. Please install xhtml2pdf."
)
pdf_html = HTML_TEMPLATE.format(
common_styles=COMMON_STYLES,
specific_styles=PDF_SPECIFIC_STYLES,
content=html_content,
)
buffer = BytesIO()
pdf = pisa.pisaDocument(
BytesIO(pdf_html.encode("UTF-8")), buffer, encoding="UTF-8"
)
if pdf.err:
logging.error(f"PDF generation failed: {pdf.err}")
raise ValueError("PDF generation failed")
buffer.seek(0)
return buffer
@classmethod
def _generate_html(cls, html_content: str) -> str:
"""
Generate a complete HTML document with the given HTML content.
"""
return HTML_TEMPLATE.format(
common_styles=COMMON_STYLES,
specific_styles=HTML_SPECIFIC_STYLES,
content=html_content,
)
def generate_document(
self, original_content: str, document_type: str, file_name: str
) -> str:
"""
To generate document as PDF or HTML file.
Parameters:
original_content: str (markdown style)
document_type: str (pdf or html) specify the type of the file format based on the use case
file_name: str (name of the document file) must be a valid file name, no extensions needed
Returns:
str (URL to the document file): A file URL ready to serve.
"""
try:
doc_type = DocumentType(document_type.lower())
except ValueError:
raise ValueError(
f"Invalid document type: {document_type}. Must be 'pdf' or 'html'."
)
# Always generate html content first
html_content = self._generate_html_content(original_content)
# Based on the type of document, generate the corresponding file
if doc_type == DocumentType.PDF:
content = self._generate_pdf(html_content)
file_extension = "pdf"
elif doc_type == DocumentType.HTML:
content = BytesIO(self._generate_html(html_content).encode("utf-8"))
file_extension = "html"
else:
raise ValueError(f"Unexpected document type: {document_type}")
file_name = self._validate_file_name(file_name)
file_path = os.path.join(OUTPUT_DIR, f"{file_name}.{file_extension}")
self._write_to_file(content, file_path)
return (
f"{self.file_server_url_prefix}/{OUTPUT_DIR}/{file_name}.{file_extension}"
)
@staticmethod
def _write_to_file(content: BytesIO, file_path: str) -> None:
"""
Write the content to a file.
"""
try:
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "wb") as file:
file.write(content.getvalue())
except Exception:
raise
@staticmethod
def _validate_file_name(file_name: str) -> str:
"""
Validate the file name.
"""
# Don't allow directory traversal
if os.path.isabs(file_name):
raise ValueError("File name is not allowed.")
# Don't allow special characters
if re.match(r"^[a-zA-Z0-9_.-]+$", file_name):
return file_name
else:
raise ValueError("File name is not allowed to contain special characters.")
@classmethod
def _validate_packages(cls) -> None:
try:
import markdown # noqa: F401
import xhtml2pdf # noqa: F401
except ImportError:
raise ImportError(
"Failed to import required modules. Please install markdown and xhtml2pdf "
"using `pip install markdown xhtml2pdf`"
)
def to_tool(self) -> FunctionTool:
self._validate_packages()
return FunctionTool.from_defaults(self.generate_document)
@@ -1,3 +0,0 @@
from .query import get_query_engine_tool
__all__ = ["get_query_engine_tool"]
@@ -1,49 +0,0 @@
import os
from typing import Any, Optional
from llama_index.core.base.base_query_engine import BaseQueryEngine
from llama_index.core.tools.query_engine import QueryEngineTool
from llama_index.core.indices.base import BaseIndex
def create_query_engine(index: BaseIndex, **kwargs: Any) -> BaseQueryEngine:
"""
Create a query engine for the given index.
Args:
index: The index to create a query engine for.
params (optional): Additional parameters for the query engine, e.g: similarity_top_k
"""
top_k = int(os.getenv("TOP_K", 0))
if top_k != 0 and kwargs.get("filters") is None:
kwargs["similarity_top_k"] = top_k
return index.as_query_engine(**kwargs)
def get_query_engine_tool(
index: BaseIndex,
name: Optional[str] = None,
description: Optional[str] = None,
**kwargs: Any,
) -> QueryEngineTool:
"""
Get a query engine tool for the given index.
Args:
index: The index to create a query engine for.
name (optional): The name of the tool.
description (optional): The description of the tool.
"""
if name is None:
name = "query_index"
if description is None:
description = (
"Use this tool to retrieve information about the text corpus from an index."
)
query_engine = create_query_engine(index, **kwargs)
return QueryEngineTool.from_defaults(
query_engine=query_engine,
name=name,
description=description,
)
@@ -1,13 +0,0 @@
from datetime import timedelta
from cachetools import TTLCache, cached # type: ignore
from llama_index.core.storage import StorageContext
@cached(
TTLCache(maxsize=10, ttl=timedelta(minutes=5).total_seconds()),
key=lambda *args, **kwargs: "global_storage_context",
)
def get_storage_context(persist_dir: str) -> StorageContext:
return StorageContext.from_defaults(persist_dir=persist_dir)
@@ -1,216 +0,0 @@
import base64
import logging
import os
import uuid
from typing import Any, List, Optional
from llama_index.core.tools import FunctionTool
from llama_index.server.services.file import DocumentFile, FileService
from pydantic import BaseModel
logger = logging.getLogger("uvicorn")
class InterpreterExtraResult(BaseModel):
type: str
content: Optional[str] = None
filename: Optional[str] = None
url: Optional[str] = None
class E2BToolOutput(BaseModel):
is_error: bool
logs: "Logs" # type: ignore # noqa: F821
error_message: Optional[str] = None
results: List[InterpreterExtraResult] = []
retry_count: int = 0
class E2BCodeInterpreter:
output_dir = "output/tools"
uploaded_files_dir = "output/uploaded"
interpreter: Optional["Sandbox"] = None # type: ignore # noqa: F821
def __init__(
self,
api_key: str,
output_dir: Optional[str] = None,
uploaded_files_dir: Optional[str] = None,
):
"""
Args:
api_key: The API key for the E2B Code Interpreter.
output_dir: The directory for the output files. Default is `output/tools`.
uploaded_files_dir: The directory for the files to be uploaded to the sandbox. Default is `output/uploaded`.
"""
self._validate_package()
if not api_key:
raise ValueError(
"api_key is required to run code interpreter. Get it here: https://e2b.dev/docs/getting-started/api-key"
)
self.api_key = api_key
self.output_dir = output_dir or "output/tools"
self.uploaded_files_dir = uploaded_files_dir or "output/uploaded"
@classmethod
def _validate_package(cls) -> None:
try:
from e2b_code_interpreter import Sandbox # noqa: F401
from e2b_code_interpreter.models import Logs # noqa: F401
except ImportError:
raise ImportError(
"e2b_code_interpreter is not installed. Please install it using `pip install e2b-code-interpreter`."
)
def __del__(self) -> None:
"""
Kill the interpreter when the tool is no longer in use.
"""
if self.interpreter is not None:
self.interpreter.kill()
def _init_interpreter(self, sandbox_files: List[str] = []) -> None:
"""
Lazily initialize the interpreter.
"""
from e2b_code_interpreter import Sandbox
logger.info(f"Initializing interpreter with {len(sandbox_files)} files")
self.interpreter = Sandbox(api_key=self.api_key)
if len(sandbox_files) > 0:
for file_path in sandbox_files:
file_name = os.path.basename(file_path)
local_file_path = os.path.join(self.uploaded_files_dir, file_name)
with open(local_file_path, "rb") as f:
content = f.read()
if self.interpreter and self.interpreter.files:
self.interpreter.files.write(file_path, content)
logger.info(f"Uploaded {len(sandbox_files)} files to sandbox")
def _save_to_disk(self, base64_data: str, ext: str) -> DocumentFile:
buffer = base64.b64decode(base64_data)
# Output from e2b doesn't have a name. Create a random name for it.
filename = f"e2b_file_{uuid.uuid4()}.{ext}"
return FileService.save_file(
buffer, file_name=filename, save_dir=self.output_dir
)
def _parse_result(self, result: Any) -> List[InterpreterExtraResult]:
"""
The result could include multiple formats (e.g. png, svg, etc.) but encoded in base64
We save each result to disk and return saved file metadata (extension, filename, url).
"""
if not result:
return []
output = []
try:
formats = result.formats()
results = [result[format] for format in formats]
for ext, data in zip(formats, results):
if ext in ["png", "svg", "jpeg", "pdf"]:
document_file = self._save_to_disk(data, ext)
output.append(
InterpreterExtraResult(
type=ext,
filename=document_file.name,
url=document_file.url,
)
)
else:
# Try serialize data to string
try:
data = str(data)
except Exception as e:
data = f"Error when serializing data: {e}"
output.append(
InterpreterExtraResult(
type=ext,
content=data,
)
)
except Exception as error:
logger.exception(error, exc_info=True)
logger.error("Error when parsing output from E2b interpreter tool", error)
return output
def interpret(
self,
code: str,
sandbox_files: List[str] = [],
retry_count: int = 0,
) -> E2BToolOutput:
"""
Execute Python code in a Jupyter notebook cell. The tool will return the result, stdout, stderr, display_data, and error.
If the code needs to use a file, ALWAYS pass the file path in the sandbox_files argument.
You have a maximum of 3 retries to get the code to run successfully.
Parameters:
code (str): The Python code to be executed in a single cell.
sandbox_files (List[str]): List of local file paths to be used by the code. The tool will throw an error if a file is not found.
retry_count (int): Number of times the tool has been retried.
"""
from e2b_code_interpreter.models import Logs
if retry_count > 2:
return E2BToolOutput(
is_error=True,
logs=Logs(
stdout="",
stderr="",
display_data="",
error="",
),
error_message="Failed to execute the code after 3 retries. Explain the error to the user and suggest a fix.",
retry_count=retry_count,
)
if self.interpreter is None:
self._init_interpreter(sandbox_files)
if self.interpreter:
logger.info(
f"\n{'=' * 50}\n> Running following AI-generated code:\n{code}\n{'=' * 50}"
)
exec = self.interpreter.run_code(code)
if exec.error:
error_message = f"The code failed to execute successfully. Error: {exec.error}. Try to fix the code and run again."
logger.error(error_message)
# Calling the generated code caused an error. Kill the interpreter and return the error to the LLM so it can try to fix the error
try:
self.interpreter.kill() # type: ignore
except Exception:
pass
finally:
self.interpreter = None
output = E2BToolOutput(
is_error=True,
logs=exec.logs,
results=[],
error_message=error_message,
retry_count=retry_count + 1,
)
else:
if len(exec.results) == 0:
output = E2BToolOutput(is_error=False, logs=exec.logs, results=[])
else:
results = self._parse_result(exec.results[0])
output = E2BToolOutput(
is_error=False,
logs=exec.logs,
results=results,
retry_count=retry_count + 1,
)
return output
else:
raise ValueError("Interpreter is not initialized.")
def to_tool(self) -> FunctionTool:
self._validate_package()
return FunctionTool.from_defaults(self.interpret)
@@ -1,253 +0,0 @@
import logging
import uuid
from abc import ABC, abstractmethod
from typing import Any, AsyncGenerator, Optional
from pydantic import BaseModel, ConfigDict
from llama_index.core.base.llms.types import ChatMessage, ChatResponse
from llama_index.core.llms.function_calling import FunctionCallingLLM
from llama_index.core.tools import (
BaseTool,
FunctionTool,
ToolOutput,
ToolSelection,
)
from llama_index.core.workflow import Context
from llama_index.server.api.models import AgentRunEvent, AgentRunEventType
from llama_index.core.agent.workflow.workflow_events import ToolCall, ToolCallResult
logger = logging.getLogger("uvicorn")
class ToolCallOutput(BaseModel):
tool_call_id: str
tool_output: ToolOutput
class ContextAwareTool(FunctionTool, ABC):
@abstractmethod
async def acall(self, ctx: Context, input: Any) -> ToolOutput: # type: ignore
pass
class ChatWithToolsResponse(BaseModel):
"""
A tool call response from chat_with_tools.
"""
tool_calls: Optional[list[ToolSelection]]
tool_call_message: Optional[ChatMessage]
generator: Optional[AsyncGenerator[ChatResponse | None, None]]
model_config = ConfigDict(arbitrary_types_allowed=True)
def is_calling_different_tools(self) -> bool:
tool_names = {tool_call.tool_name for tool_call in self.tool_calls or []}
return len(tool_names) > 1
def has_tool_calls(self) -> bool:
return self.tool_calls is not None and len(self.tool_calls) > 0
def tool_name(self) -> str:
if not self.has_tool_calls():
raise ValueError("No tool calls")
if self.is_calling_different_tools():
raise ValueError("Calling different tools")
return self.tool_calls[0].tool_name # type: ignore
async def full_response(self) -> str:
assert self.generator is not None
full_response = ""
async for chunk in self.generator:
content = chunk.delta # type: ignore
if content:
full_response += content
return full_response
async def chat_with_tools( # type: ignore
llm: FunctionCallingLLM,
tools: list[BaseTool],
chat_history: list[ChatMessage],
) -> ChatWithToolsResponse:
"""
Request LLM to call tools or not.
This function doesn't change the memory.
"""
generator = _tool_call_generator(llm, tools, chat_history)
is_tool_call = await generator.__anext__()
if is_tool_call:
# Last chunk is the full response
# Wait for the last chunk
full_response = None
async for chunk in generator:
full_response = chunk
assert isinstance(full_response, ChatResponse)
return ChatWithToolsResponse(
tool_calls=llm.get_tool_calls_from_response(full_response),
tool_call_message=full_response.message,
generator=None,
)
else:
return ChatWithToolsResponse(
tool_calls=None,
tool_call_message=None,
generator=generator, # type: ignore
)
async def call_tools(
ctx: Context,
agent_name: str,
tools: list[BaseTool],
tool_calls: list[ToolSelection],
emit_agent_events: bool = True,
) -> list[ToolCallOutput]:
"""
Call tools and return the tool call responses.
"""
if len(tool_calls) == 0:
return []
tools_by_name = {tool.metadata.get_name(): tool for tool in tools}
if len(tool_calls) == 1:
if emit_agent_events:
ctx.write_event_to_stream(
AgentRunEvent(
name=agent_name,
msg=f"{tool_calls[0].tool_name}: {tool_calls[0].tool_kwargs}",
)
)
return [
await call_tool(ctx, tools_by_name[tool_calls[0].tool_name], tool_calls[0])
]
# Multiple tool calls, show progress
tool_call_outputs: list[ToolCallOutput] = []
progress_id = str(uuid.uuid4())
total_steps = len(tool_calls)
if emit_agent_events:
ctx.write_event_to_stream(
AgentRunEvent(
name=agent_name,
msg=f"Making {total_steps} tool calls",
)
)
for i, tool_call in enumerate(tool_calls):
tool = tools_by_name.get(tool_call.tool_name)
if not tool:
tool_call_outputs.append(
ToolCallOutput(
tool_call_id=tool_call.tool_id,
tool_output=ToolOutput(
is_error=True,
content=f"Tool {tool_call.tool_name} does not exist",
tool_name=tool_call.tool_name,
raw_input=tool_call.tool_kwargs,
raw_output={
"error": f"Tool {tool_call.tool_name} does not exist",
},
),
)
)
continue
tool_call_output = await call_tool(
ctx,
tool,
tool_call,
)
if emit_agent_events:
ctx.write_event_to_stream(
AgentRunEvent(
name=agent_name,
msg=f"{tool_call.tool_name}: {tool_call.tool_kwargs}",
event_type=AgentRunEventType.PROGRESS,
data={
"id": progress_id,
"total": total_steps,
"current": i,
},
)
)
tool_call_outputs.append(tool_call_output)
return tool_call_outputs
async def call_tool(
ctx: Context,
tool: BaseTool,
tool_call: ToolSelection,
) -> ToolCallOutput:
ctx.write_event_to_stream(
ToolCall(
tool_name=tool_call.tool_name,
tool_id=tool_call.tool_id,
tool_kwargs=tool_call.tool_kwargs,
)
)
try:
if isinstance(tool, ContextAwareTool):
if ctx is None:
raise ValueError("Context is required for context aware tool")
# inject context for calling an context aware tool
output = await tool.acall(ctx=ctx, **tool_call.tool_kwargs)
else:
output = await tool.acall(**tool_call.tool_kwargs) # type: ignore
except Exception as e:
logger.error(f"Got error in tool {tool_call.tool_name}: {e!s}")
output = ToolOutput(
is_error=True,
content=f"Error: {e!s}",
tool_name=tool.metadata.get_name(),
raw_input=tool_call.tool_kwargs,
raw_output={
"error": str(e),
},
)
ctx.write_event_to_stream(
ToolCallResult(
tool_name=tool_call.tool_name,
tool_kwargs=tool_call.tool_kwargs,
tool_id=tool_call.tool_id,
tool_output=output,
return_direct=False,
)
)
return ToolCallOutput(
tool_call_id=tool_call.tool_id,
tool_output=output,
)
async def _tool_call_generator(
llm: FunctionCallingLLM,
tools: list[BaseTool],
chat_history: list[ChatMessage],
) -> AsyncGenerator[ChatResponse | bool, None]:
response_stream = await llm.astream_chat_with_tools(
tools,
chat_history=chat_history,
allow_parallel_tool_calls=False,
)
full_response = None
yielded_indicator = False
async for chunk in response_stream:
if "tool_calls" not in chunk.message.additional_kwargs:
# Yield a boolean to indicate whether the response is a tool call
if not yielded_indicator:
yield False
yielded_indicator = True
# if not a tool call, yield the chunks!
yield chunk # type: ignore
elif not yielded_indicator:
# Yield the indicator for a tool call
yield True
yielded_indicator = True
full_response = chunk
if full_response:
yield full_response # type: ignore
-6165
View File
File diff suppressed because it is too large Load Diff
-65
View File
@@ -1,65 +0,0 @@
[build-system]
build-backend = "poetry.core.masonry.api"
requires = ["poetry-core"]
[tool.codespell]
check-filenames = true
check-hidden = true
# Feel free to un-skip examples, and experimental, you will just need to
# work through many typos (--write-changes and --interactive will help)
skip = "*.csv,*.html,*.json,*.jsonl,*.pdf,*.txt,*.ipynb"
[tool.mypy]
disallow_untyped_defs = true
# Remove venv skip when integrated with pre-commit
exclude = ["_static", "build", "examples", "notebooks", "venv"]
ignore_missing_imports = true
namespace_packages = true
explicit_package_bases = true
python_version = "3.10"
[tool.poetry]
authors = ["Your Name <you@example.com>"]
description = "llama-index fastapi server"
exclude = ["**/BUILD"]
license = "MIT"
name = "llama-index-server"
packages = [{include = "llama_index/"}]
readme = "README.md"
version = "0.1.12"
[tool.poetry.dependencies]
python = ">=3.9,<4.0"
fastapi = {extras = ["standard"], version = "^0.115.11"}
cachetools = "^5.5.2"
requests = "^2.32.3"
pydantic-settings = "^2.8.1"
llama-index-core = "^0.12.28"
llama-index-readers-file = "^0.4.6"
llama-index-indices-managed-llama-cloud = "0.6.3"
[tool.poetry.group.dev.dependencies]
black = {extras = ["jupyter"], version = "<=23.9.1,>=23.7.0"}
codespell = {extras = ["toml"], version = ">=v2.2.6"}
e2b-code-interpreter = "^1.1.1"
ipython = "8.10.0"
jupyter = "^1.0.0"
markdown = "^3.7"
mypy = "1.15.0"
pre-commit = "3.2.0"
pylint = "2.15.10"
pytest = "^8.3.5"
pytest-asyncio = "^0.25.3"
pytest-mock = "3.11.1"
ruff = "0.0.292"
tree-sitter-languages = "^1.8.0"
types-Deprecated = ">=0.1.0"
types-PyYAML = "^6.0.12.12"
types-protobuf = "^4.24.0.4"
types-redis = "4.5.5.0"
types-requests = "2.28.11.8" # TODO: unpin when mypy>0.991
types-setuptools = "67.1.0.0"
xhtml2pdf = "^0.2.17"
pytest-cov = "^6.0.0"
llama-cloud = "^0.1.17"
@@ -1,149 +0,0 @@
import logging
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from llama_index.core.workflow import StopEvent, Workflow
from llama_index.core.workflow.handler import WorkflowHandler
from llama_index.server.api.models import ChatAPIMessage, ChatRequest
from llama_index.server.api.routers.chat import chat_router
@pytest.fixture()
def logger():
return logging.getLogger("test")
@pytest.fixture()
def chat_request():
"""Create a simple chat request with one user message."""
return ChatRequest(
messages=[ChatAPIMessage(role="user", content="Hello, how are you?")]
)
@pytest.fixture()
def mock_workflow():
"""Create a mock workflow that returns a simple response."""
workflow = MagicMock(spec=Workflow)
handler = AsyncMock(spec=WorkflowHandler)
# Setup the handler to stream a simple response event
async def mock_stream_events():
yield StopEvent(result="I'm doing well, thank you for asking!")
handler.stream_events.return_value = mock_stream_events()
workflow.run.return_value = handler
return workflow
@pytest.fixture()
def workflow_factory(mock_workflow):
"""Create a factory function that returns our mock workflow."""
def factory(verbose=False):
return mock_workflow
return factory
@pytest.mark.asyncio()
async def test_chat_router(chat_request, workflow_factory, logger):
"""Test that the chat router handles a request correctly."""
# Create a FastAPI app and mount our router
app = FastAPI()
router = chat_router(workflow_factory, logger)
app.include_router(router)
# Make a request to the chat endpoint
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as client:
response = await client.post("/chat", json=chat_request.model_dump())
# Check response status
assert response.status_code == 200
# For streaming responses we don't check the content-type header directly
# Instead, check that we get the expected content in the response body
# The response is a stream, so we need to collect the chunks
content = response.content.decode()
# Verify content structure follows expected format
assert "0:" in content # Text prefix for VercelStreamResponse
# Verify if the response contains the expected message
assert "I'm doing well" in content
# Verify the mock workflow was called correctly
mock_workflow = workflow_factory()
mock_workflow.run.assert_called_once()
# Verify the workflow was called with the correct arguments
call_args = mock_workflow.run.call_args[1]
assert call_args["user_msg"] == "Hello, how are you?"
assert isinstance(call_args["chat_history"], list)
assert len(call_args["chat_history"]) == 0 # No history for first message
@pytest.mark.asyncio()
async def test_chat_with_agent_workflow(logger):
"""Test that the chat router works with a workflow that mimics an agent workflow."""
# Create a simple workflow that mimics an agent workflow
mock_workflow = MagicMock(spec=Workflow)
handler = AsyncMock(spec=WorkflowHandler)
# Setup the handler to stream a simple response about weather
async def mock_stream_events():
yield StopEvent(
result="The weather in New York is sunny. I used the weather tool to get this information."
)
handler.stream_events.return_value = mock_stream_events()
mock_workflow.run.return_value = handler
# Create a factory function that returns our mock workflow
def workflow_factory(verbose=False):
return mock_workflow
# Create a FastAPI app and mount our router
app = FastAPI()
router = chat_router(workflow_factory, logger)
app.include_router(router)
# Create a chat request asking about weather
chat_request = ChatRequest(
messages=[
ChatAPIMessage(role="user", content="What's the weather in New York?")
]
)
# Make a request to the chat endpoint
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as client:
response = await client.post("/chat", json=chat_request.model_dump())
# Check response status
assert response.status_code == 200
# The response is a stream, so we need to collect the chunks
content = response.content.decode()
# Verify content structure follows expected format
assert "0:" in content # Text prefix for VercelStreamResponse
# Verify the response content contains expected keywords
assert "weather" in content and "New York" in content and "sunny" in content
# Verify the mock workflow was called correctly
mock_workflow.run.assert_called_once()
# Verify the workflow was called with the correct arguments
call_args = mock_workflow.run.call_args[1]
assert call_args["user_msg"] == "What's the weather in New York?"
assert isinstance(call_args["chat_history"], list)
assert len(call_args["chat_history"]) == 0 # No history for first message
@@ -1,249 +0,0 @@
import asyncio
import logging
from unittest.mock import AsyncMock, MagicMock
import pytest
from llama_index.core.agent.workflow.workflow_events import AgentStream
from llama_index.core.workflow import StopEvent
from llama_index.core.workflow.handler import WorkflowHandler
from llama_index.server.api.models import ChatAPIMessage, ChatRequest
from llama_index.server.api.routers.chat import _stream_content
from llama_index.server.api.utils.vercel_stream import VercelStreamResponse
@pytest.fixture()
def logger():
return logging.getLogger("test")
@pytest.fixture()
def chat_request():
return ChatRequest(messages=[ChatAPIMessage(role="user", content="test message")])
@pytest.fixture()
def mock_workflow_handler():
handler = AsyncMock(spec=WorkflowHandler)
handler.accumulate_text = MagicMock()
return handler
class TestEventStream:
@pytest.mark.asyncio()
async def test_stream_content_with_agent_stream(
self, mock_workflow_handler, chat_request, logger
):
# Setup
mock_workflow_handler.stream_events.return_value = (
self._mock_agent_stream_events()
)
# Execute
result = [
chunk
async for chunk in _stream_content(
mock_workflow_handler, chat_request, logger
)
]
# Assert
assert len(result) == 3 # Empty start + 2 text chunks
assert result[0] == VercelStreamResponse.convert_text("")
assert result[1] == VercelStreamResponse.convert_text("Hello")
assert result[2] == VercelStreamResponse.convert_text(" World")
@pytest.mark.asyncio()
async def test_stream_content_with_stop_event_string(
self, mock_workflow_handler, chat_request, logger
):
# Setup
mock_workflow_handler.stream_events.return_value = (
self._mock_stop_event_string()
)
# Execute
result = [
chunk
async for chunk in _stream_content(
mock_workflow_handler, chat_request, logger
)
]
# Assert
assert len(result) == 2 # Empty start + result string
assert result[0] == VercelStreamResponse.convert_text("")
assert result[1] == VercelStreamResponse.convert_text("Final answer")
@pytest.mark.asyncio()
async def test_stream_content_with_stop_event_delta_objects(
self, mock_workflow_handler, chat_request, logger
):
# Setup
mock_workflow_handler.stream_events.return_value = (
self._mock_stop_event_delta_objects()
)
# Execute
result = [
chunk
async for chunk in _stream_content(
mock_workflow_handler, chat_request, logger
)
]
# Assert
assert len(result) == 3 # Empty start + 2 delta chunks
assert result[0] == VercelStreamResponse.convert_text("")
assert result[1] == VercelStreamResponse.convert_text("Delta 1")
assert result[2] == VercelStreamResponse.convert_text("Delta 2")
@pytest.mark.asyncio()
async def test_stream_content_with_event_with_to_response(
self, mock_workflow_handler, chat_request, logger
):
# Setup
mock_workflow_handler.stream_events.return_value = (
self._mock_event_with_to_response()
)
# Execute
result = [
chunk
async for chunk in _stream_content(
mock_workflow_handler, chat_request, logger
)
]
# Assert
assert len(result) == 2 # Empty start + event with to_response
assert result[0] == VercelStreamResponse.convert_text("")
assert result[1] == VercelStreamResponse.convert_data({"event_type": "test"})
@pytest.mark.asyncio()
async def test_stream_content_with_event_with_model_dump(
self, mock_workflow_handler, chat_request, logger
):
# Setup
mock_workflow_handler.stream_events.return_value = (
self._mock_event_with_model_dump()
)
# Execute
result = [
chunk
async for chunk in _stream_content(
mock_workflow_handler, chat_request, logger
)
]
# Assert
assert len(result) == 2 # Empty start + event with model_dump
assert result[0] == VercelStreamResponse.convert_text("")
assert result[1] == VercelStreamResponse.convert_data(None)
@pytest.mark.asyncio()
async def test_stream_content_with_cancelled_error(
self, mock_workflow_handler, chat_request, logger
):
# Setup
mock_workflow_handler.stream_events.side_effect = asyncio.CancelledError()
logger.warning = MagicMock()
# Execute
result = [
chunk
async for chunk in _stream_content(
mock_workflow_handler, chat_request, logger
)
]
# Assert
assert len(result) == 0
mock_workflow_handler.cancel_run.assert_called_once()
logger.warning.assert_called_once()
@pytest.mark.asyncio()
async def test_stream_content_with_exception(
self, mock_workflow_handler, chat_request, logger
):
# Setup
error_message = "Test error"
mock_workflow_handler.stream_events.side_effect = Exception(error_message)
logger.error = MagicMock()
# Execute
result = [
chunk
async for chunk in _stream_content(
mock_workflow_handler, chat_request, logger
)
]
# Assert
assert len(result) == 1
assert result[0] == VercelStreamResponse.convert_error(error_message)
mock_workflow_handler.cancel_run.assert_called_once()
logger.error.assert_called_once()
async def _mock_agent_stream_events(self):
yield AgentStream(
delta="Hello", response="", current_agent_name="", tool_calls=[], raw=""
)
yield AgentStream(
delta=" World", response="", current_agent_name="", tool_calls=[], raw=""
)
async def _mock_agent_stream_with_empty_deltas(self):
yield AgentStream(
delta=" ", # Empty delta with spaces - should be filtered
response="",
current_agent_name="",
tool_calls=[],
raw="",
)
yield AgentStream(
delta="Valid delta",
response="",
current_agent_name="",
tool_calls=[],
raw="",
)
yield AgentStream(
delta="\n", # Newline-only delta - should be filtered
response="",
current_agent_name="",
tool_calls=[],
raw="",
)
async def _mock_stop_event_string(self):
yield StopEvent(result="Final answer")
async def _mock_stop_event_delta_objects(self):
async def generator():
# Create proper objects with delta attribute that can be serialized
class ObjectWithDelta:
def __init__(self, delta_value) -> None:
self.delta = delta_value
yield ObjectWithDelta("Delta 1")
yield ObjectWithDelta("Delta 2")
yield StopEvent(result=generator())
async def _mock_dict_event(self):
yield {"key": "value"}
async def _mock_event_with_to_response(self):
event = MagicMock()
event.to_response.return_value = {"event_type": "test"}
yield event
async def _mock_event_with_model_dump(self):
event = MagicMock()
event.model_dump.return_value = {"name": "test_event"}
# Override to_response to return None - this means convert_data(None) will be called
event.to_response = MagicMock(return_value=None)
# The model_dump value is ignored when to_response returns None
yield event
@@ -1,205 +0,0 @@
import os
import uuid
from unittest.mock import mock_open, patch
import pytest
from llama_index.server.services.file import FileService, _sanitize_file_name
class TestFileService:
def test_sanitize_file_name(self):
# Test with normal alphanumeric name
assert _sanitize_file_name("test123") == "test123"
# Test with spaces
assert _sanitize_file_name("test file") == "test_file"
# Test with special characters
assert _sanitize_file_name("test@file!name") == "test_file_name"
# Test with path-like characters
assert _sanitize_file_name("test/file/name") == "test_file_name"
# Test with dots (should be preserved)
assert _sanitize_file_name("test.file.name") == "test.file.name"
@patch("uuid.uuid4")
@patch("os.path.getsize")
@patch("builtins.open", new_callable=mock_open)
@patch("os.makedirs")
def test_save_file_string_content(
self, mock_makedirs, mock_file_open, mock_getsize, mock_uuid
):
# Setup
test_uuid = "12345678-1234-5678-1234-567812345678"
mock_uuid.return_value = uuid.UUID(test_uuid)
mock_getsize.return_value = 11 # Length of "Hello World"
# Execute
result = FileService.save_file(
content="Hello World", file_name="test.txt", save_dir="test_dir"
)
# Assert
expected_path = os.path.join("test_dir", f"test_{test_uuid}.txt")
mock_makedirs.assert_called_once_with(
os.path.dirname(expected_path), exist_ok=True
)
mock_file_open.assert_called_once_with(expected_path, "wb")
mock_file_open().write.assert_called_once_with(b"Hello World")
assert result.id == test_uuid
assert result.name == f"test_{test_uuid}.txt"
assert result.type == "txt"
assert result.size == 11
assert result.path == expected_path
assert result.url.endswith(expected_path.replace(os.path.sep, "/"))
assert result.refs is None
@patch("uuid.uuid4")
@patch("os.path.getsize")
@patch("builtins.open", new_callable=mock_open)
@patch("os.makedirs")
def test_save_file_bytes_content(
self, mock_makedirs, mock_file_open, mock_getsize, mock_uuid
):
# Setup
test_uuid = "12345678-1234-5678-1234-567812345678"
mock_uuid.return_value = uuid.UUID(test_uuid)
mock_getsize.return_value = 11 # Length of "Hello World"
# Execute
result = FileService.save_file(
content=b"Hello World", file_name="test.txt", save_dir="test_dir"
)
# Assert
expected_path = os.path.join("test_dir", f"test_{test_uuid}.txt")
mock_makedirs.assert_called_once_with(
os.path.dirname(expected_path), exist_ok=True
)
mock_file_open.assert_called_once_with(expected_path, "wb")
mock_file_open().write.assert_called_once_with(b"Hello World")
assert result.path == expected_path
assert result.type == "txt"
@patch("uuid.uuid4")
@patch("os.path.getsize")
@patch("builtins.open", new_callable=mock_open)
@patch("os.makedirs")
def test_save_file_with_special_characters(
self, mock_makedirs, mock_file_open, mock_getsize, mock_uuid
):
# Setup
test_uuid = "12345678-1234-5678-1234-567812345678"
mock_uuid.return_value = uuid.UUID(test_uuid)
mock_getsize.return_value = 11
# Execute
result = FileService.save_file(
content="Hello World", file_name="test@file!.txt", save_dir="test_dir"
)
# Assert
expected_path = os.path.join("test_dir", f"test_file__{test_uuid}.txt")
mock_makedirs.assert_called_once_with(
os.path.dirname(expected_path), exist_ok=True
)
mock_file_open.assert_called_once_with(expected_path, "wb")
assert result.path == expected_path
assert result.name == f"test_file__{test_uuid}.txt"
@patch("uuid.uuid4")
@patch("os.path.getsize")
@patch("builtins.open", new_callable=mock_open)
@patch("os.makedirs")
def test_save_file_default_directory(
self, mock_makedirs, mock_file_open, mock_getsize, mock_uuid
):
# Setup
test_uuid = "12345678-1234-5678-1234-567812345678"
mock_uuid.return_value = uuid.UUID(test_uuid)
mock_getsize.return_value = 11
# Execute
result = FileService.save_file(content="Hello World", file_name="test.txt")
# Assert
expected_path = os.path.join("output", "uploaded", f"test_{test_uuid}.txt")
mock_makedirs.assert_called_once_with(
os.path.dirname(expected_path), exist_ok=True
)
assert result.path == expected_path
@patch("uuid.uuid4")
@patch("os.getenv")
@patch("os.path.getsize")
@patch("builtins.open", new_callable=mock_open)
@patch("os.makedirs")
def test_save_file_custom_url_prefix(
self, mock_makedirs, mock_file_open, mock_getsize, mock_getenv, mock_uuid
):
# Setup
test_uuid = "12345678-1234-5678-1234-567812345678"
mock_uuid.return_value = uuid.UUID(test_uuid)
mock_getsize.return_value = 11
mock_getenv.return_value = "/api/files"
# Execute
result = FileService.save_file(
content="Hello World", file_name="test.txt", save_dir="test_dir"
)
# Assert
expected_path = os.path.join("test_dir", f"test_{test_uuid}.txt")
mock_makedirs.assert_called_once_with(
os.path.dirname(expected_path), exist_ok=True
)
mock_file_open.assert_called_once_with(expected_path, "wb")
assert result.path == expected_path
# URL paths must use forward slashes, even on Windows
expected_url = f"/api/files/test_dir/test_{test_uuid}.txt"
assert result.url == expected_url
def test_save_file_no_extension(self):
# Test that saving a file without extension raises ValueError
with pytest.raises(ValueError, match="File is not supported!"):
FileService.save_file(
content="Hello World", file_name="test", save_dir="test_dir"
)
@patch("uuid.uuid4")
@patch("os.path.getsize")
@patch("builtins.open")
@patch("os.makedirs")
def test_save_file_permission_error(
self, mock_makedirs, mock_file_open, mock_getsize, mock_uuid
):
# Setup
test_uuid = "12345678-1234-5678-1234-567812345678"
mock_uuid.return_value = uuid.UUID(test_uuid)
mock_file_open.side_effect = PermissionError("Permission denied")
# Execute and Assert
with pytest.raises(PermissionError):
FileService.save_file(
content="Hello World", file_name="test.txt", save_dir="test_dir"
)
@patch("uuid.uuid4")
@patch("os.path.getsize")
@patch("builtins.open")
@patch("os.makedirs")
def test_save_file_io_error(
self, mock_makedirs, mock_file_open, mock_getsize, mock_uuid
):
# Setup
test_uuid = "12345678-1234-5678-1234-567812345678"
mock_uuid.return_value = uuid.UUID(test_uuid)
mock_file_open.side_effect = OSError("IO Error")
# Execute and Assert
with pytest.raises(IOError):
FileService.save_file(
content="Hello World", file_name="test.txt", save_dir="test_dir"
)
@@ -1,298 +0,0 @@
import json
import os
import shutil
import pytest
from httpx import ASGITransport, AsyncClient
from llama_index.core.agent.workflow import AgentWorkflow
from llama_index.core.llms import MockLLM
from llama_index.server import LlamaIndexServer, UIConfig
def fetch_weather(city: str) -> str:
"""Fetch the weather for a given city."""
return f"The weather in {city} is sunny."
def _agent_workflow() -> AgentWorkflow:
# Use MockLLM instead of default OpenAI
mock_llm = MockLLM()
return AgentWorkflow.from_tools_or_functions(
tools_or_functions=[fetch_weather],
verbose=True,
llm=mock_llm,
)
@pytest.fixture()
def server() -> LlamaIndexServer:
"""Fixture to create a LlamaIndexServer instance."""
return LlamaIndexServer(
workflow_factory=_agent_workflow,
verbose=True,
use_default_routers=True,
mount_ui=False,
env="dev",
)
@pytest.mark.asyncio()
async def test_server_has_chat_route(server: LlamaIndexServer) -> None:
"""Test that the server has the chat API route."""
chat_route_exists = any("/api/chat" in str(route) for route in server.routes)
assert chat_route_exists, "Chat API route not found in server routes"
@pytest.mark.asyncio()
async def test_server_swagger_docs(server: LlamaIndexServer) -> None:
"""Test that the server serves Swagger UI docs."""
async with AsyncClient(
transport=ASGITransport(app=server), base_url="http://test"
) as ac:
response = await ac.get("/docs")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
assert "Swagger UI" in response.text
@pytest.mark.asyncio()
async def test_ui_is_downloaded(server: LlamaIndexServer) -> None:
"""
Test if the UI is downloaded and mounted correctly.
"""
# Clean up any existing static directory first
if os.path.exists(".ui"):
shutil.rmtree(".ui")
# Create a new server with UI enabled
ui_config = UIConfig(
enabled=True,
app_title="Test UI",
starter_questions=["What's the weather like?"],
)
ui_server = LlamaIndexServer(
workflow_factory=_agent_workflow,
verbose=True,
use_default_routers=True,
env="dev",
ui_config=ui_config,
)
# Verify that static directory was created with index.html
assert os.path.exists("./.ui"), "Static directory was not created"
assert os.path.isdir("./.ui"), "Static path is not a directory"
assert os.path.exists("./.ui/index.html"), "index.html was not downloaded"
# Check if the config.js was created with correct content
config_path = os.path.join(".ui", "config.js")
assert os.path.exists(config_path), "config.js was not created"
with open(config_path, "r") as f:
config_content = f.read()
assert "window.LLAMAINDEX =" in config_content
config_json = json.loads(
config_content.replace("window.LLAMAINDEX = ", "").rstrip(";")
)
assert config_json["CHAT_API"] == "/api/chat"
assert config_json["STARTER_QUESTIONS"] == ["What's the weather like?"]
assert config_json["LLAMA_CLOUD_API"] is None
assert config_json["APP_TITLE"] == "Test UI"
# Check if the UI is mounted and accessible
async with AsyncClient(
transport=ASGITransport(app=ui_server), base_url="http://test"
) as ac:
response = await ac.get("/")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
# Clean up after test
shutil.rmtree("./.ui")
@pytest.mark.asyncio()
async def test_ui_is_accessible(server: LlamaIndexServer) -> None:
"""
Test if the UI is accessible.
"""
# Manually trigger UI mounting
server.mount_ui()
async with AsyncClient(
transport=ASGITransport(app=server), base_url="http://test"
) as ac:
response = await ac.get("/")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
@pytest.mark.asyncio()
async def test_ui_config_customization() -> None:
"""
Test if UI configuration can be customized.
"""
custom_config = UIConfig(
enabled=True,
app_title="Custom App",
starter_questions=["Question 1", "Question 2"],
ui_path=".custom_ui",
)
server = LlamaIndexServer(
workflow_factory=_agent_workflow, verbose=True, ui_config=custom_config
)
assert server.ui_config.app_title == "Custom App"
assert server.ui_config.starter_questions == ["Question 1", "Question 2"]
assert server.ui_config.ui_path == ".custom_ui"
# Clean up if directory was created
if os.path.exists(".custom_ui"):
shutil.rmtree(".custom_ui")
@pytest.mark.asyncio()
async def test_ui_config_from_dict() -> None:
"""
Test if UI configuration can be initialized from a dictionary.
"""
ui_config_dict = {
"enabled": True,
"app_title": "Dict Config App",
"starter_questions": ["Dict Q1", "Dict Q2"],
"ui_path": ".dict_ui",
}
server = LlamaIndexServer(
workflow_factory=_agent_workflow,
verbose=True,
ui_config=ui_config_dict,
)
# Verify the config was properly converted to UIConfig object
assert isinstance(server.ui_config, UIConfig)
assert server.ui_config.app_title == "Dict Config App"
assert server.ui_config.starter_questions == ["Dict Q1", "Dict Q2"]
assert server.ui_config.ui_path == ".dict_ui"
# Verify the config.js is created with correct content
server.mount_ui()
config_path = os.path.join(".dict_ui", "config.js")
assert os.path.exists(config_path), "config.js was not created"
with open(config_path, "r") as f:
config_content = f.read()
assert "window.LLAMAINDEX =" in config_content
config_json = json.loads(
config_content.replace("window.LLAMAINDEX = ", "").rstrip(";")
)
assert config_json["APP_TITLE"] == "Dict Config App"
assert config_json["STARTER_QUESTIONS"] == ["Dict Q1", "Dict Q2"]
assert config_json["CHAT_API"] == "/api/chat"
assert config_json["LLAMA_CLOUD_API"] is None
# Clean up
if os.path.exists(".dict_ui"):
shutil.rmtree(".dict_ui")
async def test_component_dir_creation(server: LlamaIndexServer) -> None:
"""
Test if the component directory is created when specified and doesn't exist.
"""
import os
import shutil
test_component_dir = "./test_components"
# Clean up any existing directory
if os.path.exists(test_component_dir):
shutil.rmtree(test_component_dir)
# Create server with component directory
_ = LlamaIndexServer(
workflow_factory=_agent_workflow,
verbose=True,
ui_config={
"component_dir": test_component_dir,
"include_ui": True,
},
)
# Verify directory was created
assert os.path.exists(test_component_dir), "Component directory was not created"
assert os.path.isdir(test_component_dir), "Component path is not a directory"
# Clean up after test
shutil.rmtree(test_component_dir)
@pytest.mark.asyncio()
async def test_component_router_addition(server: LlamaIndexServer, tmp_path) -> None:
"""
Test if the component router is added when component directory is specified.
"""
test_component_dir = tmp_path / "test_components"
# Create server with component directory
component_server = LlamaIndexServer(
workflow_factory=_agent_workflow,
verbose=True,
ui_config={
"component_dir": str(test_component_dir),
"include_ui": True,
},
)
# Verify component route exists
component_route_exists = any(
route.path == "/api/components" for route in component_server.routes
)
assert component_route_exists, "Component API route not found in server routes"
@pytest.mark.asyncio()
async def test_ui_config_includes_components_api(
server: LlamaIndexServer, tmp_path
) -> None:
"""
Test if the UI config includes components API when component directory is set.
"""
test_component_dir = tmp_path / "test_components"
# Create server with component directory
component_server = LlamaIndexServer(
workflow_factory=_agent_workflow,
verbose=True,
ui_config={
"component_dir": str(test_component_dir),
"include_ui": True,
},
)
# Check if components API is in UI config
ui_config = component_server.ui_config
assert "COMPONENTS_API" in ui_config.get_config_content(), (
"Components API not found in UI config"
)
@pytest.mark.asyncio()
async def test_component_router_requires_component_dir(
server: LlamaIndexServer,
) -> None:
"""
Test that adding components router without component_dir raises an error.
"""
server_without_component_dir = LlamaIndexServer(
workflow_factory=_agent_workflow,
verbose=True,
ui_config={
"include_ui": True,
},
)
with pytest.raises(
ValueError, match="component_dir must be specified to add components router"
):
server_without_component_dir.add_components_router()
@@ -1,89 +0,0 @@
from io import BytesIO
from unittest.mock import MagicMock, patch
import pytest
from llama_index.server.tools.document_generator import (
OUTPUT_DIR,
DocumentGenerator,
)
class TestDocumentGenerator:
def test_validate_file_name(self) -> None:
# Valid names
assert (
DocumentGenerator("/api/files")._validate_file_name("valid-name")
== "valid-name"
)
# Invalid names
with pytest.raises(ValueError):
DocumentGenerator("/api/files")._validate_file_name("/invalid/path")
@patch("os.makedirs")
@patch("builtins.open")
def test_write_to_file(self, mock_open, mock_makedirs): # type: ignore
content = BytesIO(b"test")
DocumentGenerator("/api/files")._write_to_file(content, "path/file.txt")
mock_makedirs.assert_called_once()
mock_open.assert_called_once()
mock_open.return_value.__enter__.return_value.write.assert_called_once_with(
b"test"
)
@patch("markdown.markdown")
def test_html_generation(self, mock_markdown): # type: ignore
mock_markdown.return_value = "<h1>Test</h1>"
# Test HTML content generation
assert (
DocumentGenerator("/api/files")._generate_html_content("# Test")
== "<h1>Test</h1>"
)
# Test full HTML generation
html = DocumentGenerator("/api/files")._generate_html("<h1>Test</h1>")
assert "<!DOCTYPE html>" in html
assert "<h1>Test</h1>" in html
@patch("xhtml2pdf.pisa.pisaDocument")
def test_pdf_generation(self, mock_pisa): # type: ignore
# Success case
mock_pisa.return_value = MagicMock(err=None)
assert isinstance(
DocumentGenerator("/api/files")._generate_pdf("test"), BytesIO
)
# Error case
mock_pisa.return_value = MagicMock(err="Error")
with pytest.raises(ValueError):
DocumentGenerator("/api/files")._generate_pdf("test")
@patch.multiple(
DocumentGenerator,
_generate_html_content=MagicMock(return_value="<h1>Test</h1>"),
_generate_html=MagicMock(
return_value="<html><body><h1>Test</h1></body></html>"
),
_generate_pdf=MagicMock(return_value=BytesIO(b"pdf")),
_write_to_file=MagicMock(),
)
def test_generate_document(self): # type: ignore
# HTML generation
url = DocumentGenerator("/api/files").generate_document(
"# Test", "html", "test-doc"
)
assert url == f"/api/files/{OUTPUT_DIR}/test-doc.html"
# PDF generation
url = DocumentGenerator("/api/files").generate_document(
"# Test", "pdf", "test-doc"
)
assert url == f"/api/files/{OUTPUT_DIR}/test-doc.pdf"
# Invalid type
with pytest.raises(ValueError):
DocumentGenerator("/api/files").generate_document(
"# Test", "invalid", "test-doc"
)
@@ -1,65 +0,0 @@
from unittest.mock import MagicMock
import pytest
from e2b_code_interpreter.models import Execution, Logs
from llama_index.server.tools.interpreter import E2BCodeInterpreter
class TestE2BCodeInterpreter:
@pytest.fixture()
def sandbox(self): # type: ignore
"""Create a mock Sandbox with no API key requirement."""
mock_sandbox = MagicMock()
mock_sandbox.files = MagicMock()
mock_sandbox.files.write = MagicMock()
mock_sandbox.run_code = MagicMock()
return mock_sandbox
@pytest.fixture()
def code_interpreter(self, sandbox): # type: ignore
"""Create E2BCodeInterpreter that uses the mock Sandbox."""
interpreter = E2BCodeInterpreter(api_key="dummy_key")
interpreter.interpreter = sandbox
return interpreter
def test_interpret_success(self, code_interpreter, sandbox) -> None: # type: ignore
"""Test successful code execution."""
# Mock execution result
mock_execution = Execution()
mock_execution.error = None
mock_execution.results = []
mock_execution.logs = Logs(
stdout="stdout", stderr="", display_data="", error=""
)
sandbox.run_code.return_value = mock_execution
# Run the code
result = code_interpreter.interpret("print('hello')")
# Verify
sandbox.run_code.assert_called_once_with("print('hello')")
assert result.is_error is False
assert result.logs == mock_execution.logs
def test_interpret_error(self, code_interpreter, sandbox) -> None: # type: ignore
"""Test error in code execution."""
# Mock execution result with error
mock_execution = Execution()
mock_execution.error = "Test error"
mock_execution.logs = Logs(
stdout="", stderr="error", display_data="", error="Test error"
)
sandbox.run_code.return_value = mock_execution
# Run the code
result = code_interpreter.interpret("bad code")
# Verify
assert result.is_error is True
assert "Error: Test error" in result.error_message
sandbox.kill.assert_called_once()
def test_to_tool(self, code_interpreter) -> None: # type: ignore
"""Test tool conversion."""
tool = code_interpreter.to_tool()
assert tool.fn == code_interpreter.interpret
+6 -8
View File
@@ -1,6 +1,6 @@
{
"name": "create-llama",
"version": "0.5.4",
"version": "0.1.0",
"description": "Create LlamaIndex-powered apps with one command",
"keywords": [
"rag",
@@ -9,7 +9,7 @@
],
"repository": {
"type": "git",
"url": "https://github.com/run-llama/create-llama",
"url": "https://github.com/run-llama/LlamaIndexTS",
"directory": "packages/create-llama"
},
"license": "MIT",
@@ -25,8 +25,6 @@
"clean": "rimraf --glob ./dist ./templates/**/__pycache__ ./templates/**/node_modules ./templates/**/poetry.lock",
"dev": "ncc build ./index.ts -w -o dist/",
"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",
@@ -43,22 +41,22 @@
"@types/cross-spawn": "6.0.0",
"@types/fs-extra": "11.0.4",
"@types/node": "^20.11.7",
"@types/prompts": "2.4.2",
"@types/prompts": "2.0.1",
"@types/tar": "6.1.5",
"@types/validate-npm-package-name": "3.0.0",
"async-retry": "1.3.1",
"async-sema": "3.0.1",
"ci-info": "github:watson/ci-info#f43f6a1cefff47fb361c88cf4b943fdbcaafe540",
"commander": "12.1.0",
"commander": "2.20.0",
"conf": "10.2.0",
"cross-spawn": "7.0.3",
"fast-glob": "3.3.1",
"fs-extra": "11.2.0",
"global-agent": "^3.0.0",
"got": "10.7.0",
"ollama": "^0.5.0",
"ora": "^8.0.1",
"picocolors": "1.0.0",
"prompts": "2.4.2",
"prompts": "2.1.0",
"smol-toml": "^1.1.4",
"tar": "6.1.15",
"terminal-link": "^3.0.0",
+289 -276
View File
File diff suppressed because it is too large Load Diff
+678
View File
@@ -0,0 +1,678 @@
import { execSync } from "child_process";
import ciInfo from "ci-info";
import fs from "fs";
import path from "path";
import { blue, green, red } from "picocolors";
import prompts from "prompts";
import { InstallAppArgs } from "./create-app";
import {
TemplateDataSource,
TemplateDataSourceType,
TemplateFramework,
} from "./helpers";
import { COMMUNITY_OWNER, COMMUNITY_REPO } from "./helpers/constant";
import { EXAMPLE_FILE } from "./helpers/datasources";
import { templatesDir } from "./helpers/dir";
import { getAvailableLlamapackOptions } from "./helpers/llama-pack";
import { askModelConfig, isModelConfigured } from "./helpers/providers";
import { getProjectOptions } from "./helpers/repo";
import { supportedTools, toolsRequireConfig } from "./helpers/tools";
export type QuestionArgs = Omit<
InstallAppArgs,
"appPath" | "packageManager"
> & {
askModels?: boolean;
};
const supportedContextFileTypes = [
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".csv",
];
const MACOS_FILE_SELECTION_SCRIPT = `
osascript -l JavaScript -e '
a = Application.currentApplication();
a.includeStandardAdditions = true;
a.chooseFile({ withPrompt: "Please select files to process:", multipleSelectionsAllowed: true }).map(file => file.toString())
'`;
const MACOS_FOLDER_SELECTION_SCRIPT = `
osascript -l JavaScript -e '
a = Application.currentApplication();
a.includeStandardAdditions = true;
a.chooseFolder({ withPrompt: "Please select folders to process:", multipleSelectionsAllowed: true }).map(folder => folder.toString())
'`;
const WINDOWS_FILE_SELECTION_SCRIPT = `
Add-Type -AssemblyName System.Windows.Forms
$openFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$openFileDialog.InitialDirectory = [Environment]::GetFolderPath('Desktop')
$openFileDialog.Multiselect = $true
$result = $openFileDialog.ShowDialog()
if ($result -eq 'OK') {
$openFileDialog.FileNames
}
`;
const WINDOWS_FOLDER_SELECTION_SCRIPT = `
Add-Type -AssemblyName System.windows.forms
$folderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog
$dialogResult = $folderBrowser.ShowDialog()
if ($dialogResult -eq [System.Windows.Forms.DialogResult]::OK)
{
$folderBrowser.SelectedPath
}
`;
const defaults: Omit<QuestionArgs, "modelConfig"> = {
template: "streaming",
framework: "nextjs",
ui: "shadcn",
frontend: false,
llamaCloudKey: "",
useLlamaParse: false,
communityProjectConfig: undefined,
llamapack: "",
postInstallAction: "dependencies",
dataSources: [],
tools: [],
};
export const questionHandlers = {
onCancel: () => {
console.error("Exiting.");
process.exit(1);
},
};
const getVectorDbChoices = (framework: TemplateFramework) => {
const choices = [
{
title: "No, just store the data in the file system",
value: "none",
},
{ title: "MongoDB", value: "mongo" },
{ title: "PostgreSQL", value: "pg" },
{ title: "Pinecone", value: "pinecone" },
{ title: "Milvus", value: "milvus" },
{ title: "Astra", value: "astra" },
{ title: "Qdrant", value: "qdrant" },
];
const vectordbLang = framework === "fastapi" ? "python" : "typescript";
const compPath = path.join(templatesDir, "components");
const vectordbPath = path.join(compPath, "vectordbs", vectordbLang);
const availableChoices = fs
.readdirSync(vectordbPath)
.filter((file) => fs.statSync(path.join(vectordbPath, file)).isDirectory());
const displayedChoices = choices.filter((choice) =>
availableChoices.includes(choice.value),
);
return displayedChoices;
};
export const getDataSourceChoices = (
framework: TemplateFramework,
selectedDataSource: TemplateDataSource[],
) => {
const choices = [];
if (selectedDataSource.length > 0) {
choices.push({
title: "No",
value: "no",
});
}
if (selectedDataSource === undefined || selectedDataSource.length === 0) {
choices.push({
title: "No data, just a simple chat or agent",
value: "none",
});
choices.push({
title: "Use an example PDF",
value: "exampleFile",
});
}
choices.push(
{
title: `Use local files (${supportedContextFileTypes.join(", ")})`,
value: "file",
},
{
title:
process.platform === "win32"
? "Use a local folder"
: "Use local folders",
value: "folder",
},
);
if (framework === "fastapi") {
choices.push({
title: "Use website content (requires Chrome)",
value: "web",
});
choices.push({
title: "Use data from a database (Mysql, PostgreSQL)",
value: "db",
});
}
return choices;
};
const selectLocalContextData = async (type: TemplateDataSourceType) => {
try {
let selectedPath: string = "";
let execScript: string;
let execOpts: any = {};
switch (process.platform) {
case "win32": // Windows
execScript =
type === "file"
? WINDOWS_FILE_SELECTION_SCRIPT
: WINDOWS_FOLDER_SELECTION_SCRIPT;
execOpts = { shell: "powershell.exe" };
break;
case "darwin": // MacOS
execScript =
type === "file"
? MACOS_FILE_SELECTION_SCRIPT
: MACOS_FOLDER_SELECTION_SCRIPT;
break;
default: // Unsupported OS
console.log(red("Unsupported OS error!"));
process.exit(1);
}
selectedPath = execSync(execScript, execOpts).toString().trim();
const paths =
process.platform === "win32"
? selectedPath.split("\r\n")
: selectedPath.split(", ");
for (const p of paths) {
if (
fs.statSync(p).isFile() &&
!supportedContextFileTypes.includes(path.extname(p))
) {
console.log(
red(
`Please select a supported file type: ${supportedContextFileTypes}`,
),
);
process.exit(1);
}
}
return paths;
} catch (error) {
console.log(
red(
"Got an error when trying to select local context data! Please try again or select another data source option.",
),
);
process.exit(1);
}
};
export const onPromptState = (state: any) => {
if (state.aborted) {
// If we don't re-enable the terminal cursor before exiting
// the program, the cursor will remain hidden
process.stdout.write("\x1B[?25h");
process.stdout.write("\n");
process.exit(1);
}
};
export const askQuestions = async (
program: QuestionArgs,
preferences: QuestionArgs,
openAiKey?: string,
) => {
const getPrefOrDefault = <K extends keyof Omit<QuestionArgs, "modelConfig">>(
field: K,
): Omit<QuestionArgs, "modelConfig">[K] =>
preferences[field] ?? defaults[field];
// Ask for next action after installation
async function askPostInstallAction() {
if (program.postInstallAction === undefined) {
if (ciInfo.isCI) {
program.postInstallAction = getPrefOrDefault("postInstallAction");
} else {
const actionChoices = [
{
title: "Just generate code (~1 sec)",
value: "none",
},
{
title: "Start in VSCode (~1 sec)",
value: "VSCode",
},
{
title: "Generate code and install dependencies (~2 min)",
value: "dependencies",
},
];
const modelConfigured = isModelConfigured(program.modelConfig);
// If using LlamaParse, require LlamaCloud API key
const llamaCloudKeyConfigured = program.useLlamaParse
? program.llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
: true;
const hasVectorDb = program.vectorDb && program.vectorDb !== "none";
// Can run the app if all tools do not require configuration
if (
!hasVectorDb &&
modelConfigured &&
llamaCloudKeyConfigured &&
!toolsRequireConfig(program.tools) &&
!program.llamapack
) {
actionChoices.push({
title:
"Generate code, install dependencies, and run the app (~2 min)",
value: "runApp",
});
}
const { action } = await prompts(
{
type: "select",
name: "action",
message: "How would you like to proceed?",
choices: actionChoices,
initial: 1,
},
questionHandlers,
);
program.postInstallAction = action;
}
}
}
if (!program.template) {
if (ciInfo.isCI) {
program.template = getPrefOrDefault("template");
} else {
const styledRepo = blue(
`https://github.com/${COMMUNITY_OWNER}/${COMMUNITY_REPO}`,
);
const { template } = await prompts(
{
type: "select",
name: "template",
message: "Which template would you like to use?",
choices: [
{ title: "Chat", value: "streaming" },
{
title: `Community template from ${styledRepo}`,
value: "community",
},
{
title: "Example using a LlamaPack",
value: "llamapack",
},
],
initial: 0,
},
questionHandlers,
);
program.template = template;
preferences.template = template;
}
}
if (program.template === "community") {
const projectOptions = await getProjectOptions(
COMMUNITY_OWNER,
COMMUNITY_REPO,
);
const { communityProjectConfig } = await prompts(
{
type: "select",
name: "communityProjectConfig",
message: "Select community template",
choices: projectOptions.map(({ title, value }) => ({
title,
value: JSON.stringify(value), // serialize value to string in terminal
})),
initial: 0,
},
questionHandlers,
);
const projectConfig = JSON.parse(communityProjectConfig);
program.communityProjectConfig = projectConfig;
preferences.communityProjectConfig = projectConfig;
return; // early return - no further questions needed for community projects
}
if (program.template === "llamapack") {
const availableLlamaPacks = await getAvailableLlamapackOptions();
const { llamapack } = await prompts(
{
type: "select",
name: "llamapack",
message: "Select LlamaPack",
choices: availableLlamaPacks.map((pack) => ({
title: pack.name,
value: pack.folderPath,
})),
initial: 0,
},
questionHandlers,
);
program.llamapack = llamapack;
preferences.llamapack = llamapack;
await askPostInstallAction();
return; // early return - no further questions needed for llamapack projects
}
if (!program.framework) {
if (ciInfo.isCI) {
program.framework = getPrefOrDefault("framework");
} else {
const choices = [
{ title: "NextJS", value: "nextjs" },
{ title: "Express", value: "express" },
{ title: "FastAPI (Python)", value: "fastapi" },
];
const { framework } = await prompts(
{
type: "select",
name: "framework",
message: "Which framework would you like to use?",
choices,
initial: 0,
},
questionHandlers,
);
program.framework = framework;
preferences.framework = framework;
}
}
if (program.framework === "express" || program.framework === "fastapi") {
// if a backend-only framework is selected, ask whether we should create a frontend
// (only for streaming backends)
if (program.frontend === undefined) {
if (ciInfo.isCI) {
program.frontend = getPrefOrDefault("frontend");
} else {
const styledNextJS = blue("NextJS");
const styledBackend = green(
program.framework === "express"
? "Express "
: program.framework === "fastapi"
? "FastAPI (Python) "
: "",
);
const { frontend } = await prompts({
onState: onPromptState,
type: "toggle",
name: "frontend",
message: `Would you like to generate a ${styledNextJS} frontend for your ${styledBackend}backend?`,
initial: getPrefOrDefault("frontend"),
active: "Yes",
inactive: "No",
});
program.frontend = Boolean(frontend);
preferences.frontend = Boolean(frontend);
}
}
} else {
program.frontend = false;
}
if (program.framework === "nextjs" || program.frontend) {
if (!program.ui) {
program.ui = defaults.ui;
}
}
if (!program.observability) {
if (ciInfo.isCI) {
program.observability = getPrefOrDefault("observability");
} else {
const { observability } = await prompts(
{
type: "select",
name: "observability",
message: "Would you like to set up observability?",
choices: [
{ title: "No", value: "none" },
{ title: "OpenTelemetry", value: "opentelemetry" },
],
initial: 0,
},
questionHandlers,
);
program.observability = observability;
preferences.observability = observability;
}
}
if (!program.modelConfig) {
const modelConfig = await askModelConfig({
openAiKey,
askModels: program.askModels ?? false,
});
program.modelConfig = modelConfig;
preferences.modelConfig = modelConfig;
}
if (!program.dataSources) {
if (ciInfo.isCI) {
program.dataSources = getPrefOrDefault("dataSources");
} else {
program.dataSources = [];
// continue asking user for data sources if none are initially provided
while (true) {
const firstQuestion = program.dataSources.length === 0;
const { selectedSource } = await prompts(
{
type: "select",
name: "selectedSource",
message: firstQuestion
? "Which data source would you like to use?"
: "Would you like to add another data source?",
choices: getDataSourceChoices(
program.framework,
program.dataSources,
),
initial: firstQuestion ? 1 : 0,
},
questionHandlers,
);
if (selectedSource === "no" || selectedSource === "none") {
// user doesn't want another data source or any data source
break;
}
switch (selectedSource) {
case "exampleFile": {
program.dataSources.push(EXAMPLE_FILE);
break;
}
case "file":
case "folder": {
const selectedPaths = await selectLocalContextData(selectedSource);
for (const p of selectedPaths) {
program.dataSources.push({
type: "file",
config: {
path: p,
},
});
}
break;
}
case "web": {
const { baseUrl } = await prompts(
{
type: "text",
name: "baseUrl",
message: "Please provide base URL of the website: ",
initial: "https://www.llamaindex.ai",
validate: (value: string) => {
if (!value.includes("://")) {
value = `https://${value}`;
}
const urlObj = new URL(value);
if (
urlObj.protocol !== "https:" &&
urlObj.protocol !== "http:"
) {
return `URL=${value} has invalid protocol, only allow http or https`;
}
return true;
},
},
questionHandlers,
);
program.dataSources.push({
type: "web",
config: {
baseUrl,
prefix: baseUrl,
depth: 1,
},
});
break;
}
case "db": {
const dbPrompts: prompts.PromptObject<string>[] = [
{
type: "text",
name: "uri",
message:
"Please enter the connection string (URI) for the database.",
initial: "mysql+pymysql://user:pass@localhost:3306/mydb",
validate: (value: string) => {
if (!value) {
return "Please provide a valid connection string";
} else if (
!(
value.startsWith("mysql+pymysql://") ||
value.startsWith("postgresql+psycopg://")
)
) {
return "The connection string must start with 'mysql+pymysql://' for MySQL or 'postgresql+psycopg://' for PostgreSQL";
}
return true;
},
},
// Only ask for a query, user can provide more complex queries in the config file later
{
type: (prev) => (prev ? "text" : null),
name: "queries",
message: "Please enter the SQL query to fetch data:",
initial: "SELECT * FROM mytable",
},
];
program.dataSources.push({
type: "db",
config: await prompts(dbPrompts, questionHandlers),
});
}
}
}
}
}
// Asking for LlamaParse if user selected file or folder data source
if (
program.dataSources.some((ds) => ds.type === "file") &&
program.useLlamaParse === undefined
) {
if (ciInfo.isCI) {
program.useLlamaParse = getPrefOrDefault("useLlamaParse");
program.llamaCloudKey = getPrefOrDefault("llamaCloudKey");
} else {
const { useLlamaParse } = await prompts(
{
type: "toggle",
name: "useLlamaParse",
message:
"Would you like to use LlamaParse (improved parser for RAG - requires API key)?",
initial: false,
active: "yes",
inactive: "no",
},
questionHandlers,
);
program.useLlamaParse = useLlamaParse;
// Ask for LlamaCloud API key
if (useLlamaParse && program.llamaCloudKey === undefined) {
const { llamaCloudKey } = await prompts(
{
type: "text",
name: "llamaCloudKey",
message:
"Please provide your LlamaIndex Cloud API key (leave blank to skip):",
},
questionHandlers,
);
program.llamaCloudKey = llamaCloudKey;
}
}
}
if (program.dataSources.length > 0 && !program.vectorDb) {
if (ciInfo.isCI) {
program.vectorDb = getPrefOrDefault("vectorDb");
} else {
const { vectorDb } = await prompts(
{
type: "select",
name: "vectorDb",
message: "Would you like to use a vector database?",
choices: getVectorDbChoices(program.framework),
initial: 0,
},
questionHandlers,
);
program.vectorDb = vectorDb;
preferences.vectorDb = vectorDb;
}
}
if (!program.tools) {
if (ciInfo.isCI) {
program.tools = getPrefOrDefault("tools");
} else {
const options = supportedTools.filter((t) =>
t.supportedFrameworks?.includes(program.framework),
);
const toolChoices = options.map((tool) => ({
title: tool.display,
value: tool.name,
}));
const { toolsName } = await prompts({
type: "multiselect",
name: "toolsName",
message:
"Would you like to build an agent using tools? If so, select the tools here, otherwise just press enter",
choices: toolChoices,
});
const tools = toolsName?.map((tool: string) =>
supportedTools.find((t) => t.name === tool),
);
program.tools = tools;
preferences.tools = tools;
}
}
await askPostInstallAction();
};
export const toChoice = (value: string) => {
return { title: value, value };
};
-30
View File
@@ -1,30 +0,0 @@
import { askModelConfig } from "../helpers/providers";
import { QuestionArgs, QuestionResults } from "./types";
const defaults: Omit<QuestionArgs, "modelConfig"> = {
template: "streaming",
framework: "nextjs",
ui: "shadcn",
frontend: false,
llamaCloudKey: "",
useLlamaParse: false,
communityProjectConfig: undefined,
llamapack: "",
postInstallAction: "dependencies",
dataSources: [],
tools: [],
};
export async function getCIQuestionResults(
program: QuestionArgs,
): Promise<QuestionResults> {
return {
...defaults,
...program,
modelConfig: await askModelConfig({
openAiKey: program.openAiKey,
askModels: false,
framework: program.framework,
}),
};
}
-64
View File
@@ -1,64 +0,0 @@
import {
TemplateDataSource,
TemplateFramework,
TemplateType,
} from "../helpers";
import { supportedContextFileTypes } from "./utils";
export const getDataSourceChoices = (
framework: TemplateFramework,
selectedDataSource: TemplateDataSource[],
template?: TemplateType,
) => {
const choices = [];
if (selectedDataSource.length > 0) {
choices.push({
title: "No",
value: "no",
});
}
if (selectedDataSource === undefined || selectedDataSource.length === 0) {
choices.push({
title: "No datasource",
value: "none",
});
choices.push({
title:
process.platform !== "linux"
? "Use an example PDF"
: "Use an example PDF (you can add your own data files later)",
value: "exampleFile",
});
}
// Linux has many distros so we won't support file/folder picker for now
if (process.platform !== "linux") {
choices.push(
{
title: `Use local files (${supportedContextFileTypes.join(", ")})`,
value: "file",
},
{
title:
process.platform === "win32"
? "Use a local folder"
: "Use local folders",
value: "folder",
},
);
}
if (framework === "fastapi" && template !== "reflex") {
choices.push({
title: "Use website content (requires Chrome)",
value: "web",
});
choices.push({
title: "Use data from a database (Mysql, PostgreSQL)",
value: "db",
});
}
return choices;
};
-21
View File
@@ -1,21 +0,0 @@
import ciInfo from "ci-info";
import { getCIQuestionResults } from "./ci";
import { askProQuestions } from "./questions";
import { askSimpleQuestions } from "./simple";
import { QuestionArgs, QuestionResults } from "./types";
export const isCI = ciInfo.isCI || process.env.PLAYWRIGHT_TEST === "1";
export const askQuestions = async (
args: QuestionArgs,
): Promise<QuestionResults> => {
if (isCI) {
return await getCIQuestionResults(args);
} else if (args.pro) {
// TODO: refactor pro questions to return a result object
await askProQuestions(args);
return args as unknown as QuestionResults;
}
const results = await askSimpleQuestions(args);
return results;
};
-459
View File
@@ -1,459 +0,0 @@
import { blue } from "picocolors";
import prompts from "prompts";
import { isCI } from ".";
import { COMMUNITY_OWNER, COMMUNITY_REPO } from "../helpers/constant";
import { EXAMPLE_FILE, EXAMPLE_GDPR } from "../helpers/datasources";
import { getAvailableLlamapackOptions } from "../helpers/llama-pack";
import { askModelConfig } from "../helpers/providers";
import { getProjectOptions } from "../helpers/repo";
import { supportedTools, toolRequiresConfig } from "../helpers/tools";
import { getDataSourceChoices } from "./datasources";
import { getVectorDbChoices } from "./stores";
import { QuestionArgs } from "./types";
import {
askPostInstallAction,
onPromptState,
questionHandlers,
selectLocalContextData,
} from "./utils";
export const askProQuestions = async (program: QuestionArgs) => {
if (!program.template) {
const styledRepo = blue(
`https://github.com/${COMMUNITY_OWNER}/${COMMUNITY_REPO}`,
);
const { template } = await prompts(
{
type: "select",
name: "template",
message: "Which template would you like to use?",
choices: [
{ title: "Agentic RAG (e.g. chat with docs)", value: "streaming" },
{
title: "Multi-agent app (using workflows)",
value: "multiagent",
},
{ title: "Fullstack python template with Reflex", value: "reflex" },
{
title: `Community template from ${styledRepo}`,
value: "community",
},
{
title: "Example using a LlamaPack",
value: "llamapack",
},
],
initial: 0,
},
questionHandlers,
);
program.template = template;
}
if (program.template === "community") {
const projectOptions = await getProjectOptions(
COMMUNITY_OWNER,
COMMUNITY_REPO,
);
const { communityProjectConfig } = await prompts(
{
type: "select",
name: "communityProjectConfig",
message: "Select community template",
choices: projectOptions.map(({ title, value }) => ({
title,
value: JSON.stringify(value), // serialize value to string in terminal
})),
initial: 0,
},
questionHandlers,
);
const projectConfig = JSON.parse(communityProjectConfig);
program.communityProjectConfig = projectConfig;
return; // early return - no further questions needed for community projects
}
if (program.template === "llamapack") {
const availableLlamaPacks = await getAvailableLlamapackOptions();
const { llamapack } = await prompts(
{
type: "select",
name: "llamapack",
message: "Select LlamaPack",
choices: availableLlamaPacks.map((pack) => ({
title: pack.name,
value: pack.folderPath,
})),
initial: 0,
},
questionHandlers,
);
program.llamapack = llamapack;
if (!program.postInstallAction) {
program.postInstallAction = await askPostInstallAction(program);
}
return; // early return - no further questions needed for llamapack projects
}
if (program.template === "reflex") {
// Reflex template only supports FastAPI, empty data sources, and llamacloud
// So we just use example file for extractor template, this allows user to choose vector database later
program.dataSources = [EXAMPLE_FILE];
program.framework = "fastapi";
// Ask for which Reflex use case to use
const { useCase } = await prompts(
{
type: "select",
name: "useCase",
message: "Which use case would you like to build?",
choices: [
{ title: "Structured Extractor", value: "extractor" },
{
title: "Contract review (using Workflow)",
value: "contract_review",
},
],
initial: 0,
},
questionHandlers,
);
program.useCase = useCase;
}
if (!program.framework) {
const choices = [
{ title: "NextJS", value: "nextjs" },
{ title: "Express", value: "express" },
{ title: "FastAPI (Python)", value: "fastapi" },
];
const { framework } = await prompts(
{
type: "select",
name: "framework",
message: "Which framework would you like to use?",
choices,
initial: 0,
},
questionHandlers,
);
program.framework = framework;
}
if (
program.framework === "fastapi" &&
(program.template === "streaming" || program.template === "multiagent")
) {
// if a backend-only framework is selected, ask whether we should create a frontend
if (program.frontend === undefined) {
const styledNextJS = blue("NextJS");
const { frontend } = await prompts({
onState: onPromptState,
type: "toggle",
name: "frontend",
message: `Would you like to generate a ${styledNextJS} frontend for your FastAPI backend?`,
initial: false,
active: "Yes",
inactive: "No",
});
program.frontend = Boolean(frontend);
}
} else {
program.frontend = false;
}
if (program.framework === "nextjs" || program.frontend) {
if (!program.ui) {
program.ui = "shadcn";
}
}
if (!program.observability && program.template === "streaming") {
const { observability } = await prompts(
{
type: "select",
name: "observability",
message: "Would you like to set up observability?",
choices: [
{ title: "No", value: "none" },
...(program.framework === "fastapi"
? [{ title: "LlamaTrace", value: "llamatrace" }]
: []),
{ title: "Traceloop", value: "traceloop" },
],
initial: 0,
},
questionHandlers,
);
program.observability = observability;
}
if (
(program.template === "reflex" || program.template === "multiagent") &&
!program.useCase
) {
const choices =
program.template === "reflex"
? [
{ title: "Structured Extractor", value: "extractor" },
{
title: "Contract review (using Workflow)",
value: "contract_review",
},
]
: [
{
title: "Financial report (generate a financial report)",
value: "financial_report",
},
{
title: "Form filling (fill missing value in a CSV file)",
value: "form_filling",
},
{ title: "Blog writer (Write a blog post)", value: "blog" },
];
const { useCase } = await prompts(
{
type: "select",
name: "useCase",
message: "Which use case would you like to use?",
choices,
initial: 0,
},
questionHandlers,
);
program.useCase = useCase;
}
// Configure framework and data sources for Reflex template
if (program.template === "reflex") {
program.framework = "fastapi";
program.dataSources =
program.useCase === "extractor" ? [EXAMPLE_FILE] : [EXAMPLE_GDPR];
}
if (!program.modelConfig) {
const modelConfig = await askModelConfig({
openAiKey: program.openAiKey,
askModels: program.askModels ?? false,
framework: program.framework,
});
program.modelConfig = modelConfig;
}
if (!program.vectorDb) {
const { vectorDb } = await prompts(
{
type: "select",
name: "vectorDb",
message: "Would you like to use a vector database?",
choices: getVectorDbChoices(program.framework),
initial: 0,
},
questionHandlers,
);
program.vectorDb = vectorDb;
}
if (program.vectorDb === "llamacloud" && program.dataSources.length === 0) {
// When using a LlamaCloud index and no data sources are provided, just copy an example file
program.dataSources = [EXAMPLE_FILE];
}
if (!program.dataSources) {
program.dataSources = [];
// continue asking user for data sources if none are initially provided
while (true) {
const firstQuestion = program.dataSources.length === 0;
const choices = getDataSourceChoices(
program.framework,
program.dataSources,
program.template,
);
if (choices.length === 0) break;
const { selectedSource } = await prompts(
{
type: "select",
name: "selectedSource",
message: firstQuestion
? "Which data source would you like to use?"
: "Would you like to add another data source?",
choices,
initial: firstQuestion ? 1 : 0,
},
questionHandlers,
);
if (selectedSource === "no" || selectedSource === "none") {
// user doesn't want another data source or any data source
break;
}
switch (selectedSource) {
case "exampleFile": {
program.dataSources.push(EXAMPLE_FILE);
break;
}
case "file":
case "folder": {
const selectedPaths = await selectLocalContextData(selectedSource);
for (const p of selectedPaths) {
program.dataSources.push({
type: "file",
config: {
path: p,
},
});
}
break;
}
case "web": {
const { baseUrl } = await prompts(
{
type: "text",
name: "baseUrl",
message: "Please provide base URL of the website: ",
initial: "https://www.llamaindex.ai",
validate: (value: string) => {
if (!value.includes("://")) {
value = `https://${value}`;
}
const urlObj = new URL(value);
if (
urlObj.protocol !== "https:" &&
urlObj.protocol !== "http:"
) {
return `URL=${value} has invalid protocol, only allow http or https`;
}
return true;
},
},
questionHandlers,
);
program.dataSources.push({
type: "web",
config: {
baseUrl,
prefix: baseUrl,
depth: 1,
},
});
break;
}
case "db": {
const dbPrompts: prompts.PromptObject<string>[] = [
{
type: "text",
name: "uri",
message:
"Please enter the connection string (URI) for the database.",
initial: "mysql+pymysql://user:pass@localhost:3306/mydb",
validate: (value: string) => {
if (!value) {
return "Please provide a valid connection string";
} else if (
!(
value.startsWith("mysql+pymysql://") ||
value.startsWith("postgresql+psycopg://")
)
) {
return "The connection string must start with 'mysql+pymysql://' for MySQL or 'postgresql+psycopg://' for PostgreSQL";
}
return true;
},
},
// Only ask for a query, user can provide more complex queries in the config file later
{
type: (prev) => (prev ? "text" : null),
name: "queries",
message: "Please enter the SQL query to fetch data:",
initial: "SELECT * FROM mytable",
},
];
program.dataSources.push({
type: "db",
config: await prompts(dbPrompts, questionHandlers),
});
break;
}
}
}
}
const isUsingLlamaCloud = program.vectorDb === "llamacloud";
// Asking for LlamaParse if user selected file data source
if (isUsingLlamaCloud) {
// default to use LlamaParse if using LlamaCloud
program.useLlamaParse = true;
} else {
// Reflex template doesn't support LlamaParse right now (cannot use asyncio loop in Reflex)
if (program.useLlamaParse === undefined && program.template !== "reflex") {
// if already set useLlamaParse, don't ask again
if (program.dataSources.some((ds) => ds.type === "file")) {
const { useLlamaParse } = await prompts(
{
type: "toggle",
name: "useLlamaParse",
message:
"Would you like to use LlamaParse (improved parser for RAG - requires API key)?",
initial: false,
active: "Yes",
inactive: "No",
},
questionHandlers,
);
program.useLlamaParse = useLlamaParse;
}
}
}
// Ask for LlamaCloud API key when using a LlamaCloud index or LlamaParse
if (isUsingLlamaCloud || program.useLlamaParse) {
if (!program.llamaCloudKey && !isCI) {
// if already set, don't ask again
// Ask for LlamaCloud API key
const { llamaCloudKey } = await prompts(
{
type: "text",
name: "llamaCloudKey",
message:
"Please provide your LlamaCloud API key (leave blank to skip):",
},
questionHandlers,
);
program.llamaCloudKey = llamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
}
}
if (
!program.tools &&
(program.template === "streaming" || program.template === "multiagent")
) {
const options = supportedTools.filter((t) =>
t.supportedFrameworks?.includes(program.framework),
);
const toolChoices = options.map((tool) => ({
title: `${tool.display}${toolRequiresConfig(tool) ? " (needs configuration)" : ""}`,
value: tool.name,
}));
const { toolsName } = await prompts({
type: "multiselect",
name: "toolsName",
message:
"Would you like to build an agent using tools? If so, select the tools here, otherwise just press enter",
choices: toolChoices,
});
const tools = toolsName?.map((tool: string) =>
supportedTools.find((t) => t.name === tool),
);
program.tools = tools;
}
if (!program.postInstallAction) {
program.postInstallAction = await askPostInstallAction(program);
}
};
-166
View File
@@ -1,166 +0,0 @@
import prompts from "prompts";
import { EXAMPLE_10K_SEC_FILES, EXAMPLE_FILE } from "../helpers/datasources";
import { askModelConfig } from "../helpers/providers";
import { getTools } from "../helpers/tools";
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 SimpleAnswers = {
appType: AppType;
language: TemplateFramework;
useLlamaCloud: boolean;
llamaCloudKey?: string;
};
export const askSimpleQuestions = async (
args: PureQuestionArgs,
): Promise<QuestionResults> => {
const { appType } = await prompts(
{
type: "select",
name: "appType",
message: "What use case do you want to build?",
choices: [
{
title: "Agentic RAG",
value: "agentic_rag",
description:
"Chatbot that answers questions based on provided documents.",
},
{
title: "Financial Report",
value: "financial_report",
description:
"Agent that analyzes data and generates visualizations by using a code interpreter.",
},
{
title: "Deep Research",
value: "deep_research",
description:
"Researches and analyzes provided documents from multiple perspectives, generating a comprehensive report with citations to support key findings and insights.",
},
],
},
questionHandlers,
);
let language: TemplateFramework = "fastapi";
let llamaCloudKey = args.llamaCloudKey;
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(
{
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
const { llamaCloudKey: newLlamaCloudKey } = await prompts(
{
type: "text",
name: "llamaCloudKey",
message:
"Please provide your LlamaCloud API key (leave blank to skip):",
},
questionHandlers,
);
llamaCloudKey = newLlamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
}
const results = await convertAnswers(args, {
appType,
language,
useLlamaCloud,
llamaCloudKey,
});
results.postInstallAction = await askPostInstallAction(results);
return results;
};
const convertAnswers = async (
args: PureQuestionArgs,
answers: SimpleAnswers,
): Promise<QuestionResults> => {
const MODEL_GPT4o: ModelConfig = {
provider: "openai",
apiKey: args.openAiKey,
model: "gpt-4o",
embeddingModel: "text-embedding-3-large",
dimensions: 1536,
isConfigured(): boolean {
return !!args.openAiKey;
},
};
const lookup: Record<
AppType,
Pick<QuestionResults, "template" | "tools" | "dataSources" | "useCase"> & {
modelConfig?: ModelConfig;
}
> = {
agentic_rag: {
template: "llamaindexserver",
dataSources: [EXAMPLE_FILE],
},
financial_report: {
template: "llamaindexserver",
dataSources: EXAMPLE_10K_SEC_FILES,
tools: getTools(["interpreter", "document_generator"]),
modelConfig: MODEL_GPT4o,
},
deep_research: {
template: "llamaindexserver",
dataSources: EXAMPLE_10K_SEC_FILES,
tools: [],
modelConfig: MODEL_GPT4o,
},
};
const results = lookup[answers.appType];
return {
framework: answers.language,
useCase: answers.appType,
ui: "shadcn",
llamaCloudKey: answers.llamaCloudKey,
useLlamaParse: answers.useLlamaCloud,
vectorDb: answers.useLlamaCloud ? "llamacloud" : "none",
...results,
modelConfig:
results.modelConfig ??
(await askModelConfig({
openAiKey: args.openAiKey,
askModels: args.askModels ?? false,
framework: answers.language,
})),
frontend: true,
};
};
-36
View File
@@ -1,36 +0,0 @@
import fs from "fs";
import path from "path";
import { TemplateFramework } from "../helpers";
import { templatesDir } from "../helpers/dir";
export const getVectorDbChoices = (framework: TemplateFramework) => {
const choices = [
{
title: "No, just store the data in the file system",
value: "none",
},
{ title: "MongoDB", value: "mongo" },
{ title: "PostgreSQL", value: "pg" },
{ title: "Pinecone", value: "pinecone" },
{ title: "Milvus", value: "milvus" },
{ title: "Astra", value: "astra" },
{ title: "Qdrant", value: "qdrant" },
{ title: "ChromaDB", value: "chroma" },
{ title: "Weaviate", value: "weaviate" },
{ title: "LlamaCloud (use Managed Index)", value: "llamacloud" },
];
const vectordbLang = framework === "fastapi" ? "python" : "typescript";
const compPath = path.join(templatesDir, "components");
const vectordbPath = path.join(compPath, "vectordbs", vectordbLang);
const availableChoices = fs
.readdirSync(vectordbPath)
.filter((file) => fs.statSync(path.join(vectordbPath, file)).isDirectory());
const displayedChoices = choices.filter((choice) =>
availableChoices.includes(choice.value),
);
return displayedChoices;
};
-15
View File
@@ -1,15 +0,0 @@
import { InstallAppArgs } from "../create-app";
export type QuestionResults = Omit<
InstallAppArgs,
"appPath" | "packageManager"
>;
export type PureQuestionArgs = {
askModels?: boolean;
pro?: boolean;
openAiKey?: string;
llamaCloudKey?: string;
};
export type QuestionArgs = QuestionResults & PureQuestionArgs;
-178
View File
@@ -1,178 +0,0 @@
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
import { red } from "picocolors";
import prompts from "prompts";
import { TemplateDataSourceType, TemplatePostInstallAction } from "../helpers";
import { toolsRequireConfig } from "../helpers/tools";
import { QuestionResults } from "./types";
export const supportedContextFileTypes = [
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".csv",
];
const MACOS_FILE_SELECTION_SCRIPT = `
osascript -l JavaScript -e '
a = Application.currentApplication();
a.includeStandardAdditions = true;
a.chooseFile({ withPrompt: "Please select files to process:", multipleSelectionsAllowed: true }).map(file => file.toString())
'`;
const MACOS_FOLDER_SELECTION_SCRIPT = `
osascript -l JavaScript -e '
a = Application.currentApplication();
a.includeStandardAdditions = true;
a.chooseFolder({ withPrompt: "Please select folders to process:", multipleSelectionsAllowed: true }).map(folder => folder.toString())
'`;
const WINDOWS_FILE_SELECTION_SCRIPT = `
Add-Type -AssemblyName System.Windows.Forms
$openFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$openFileDialog.InitialDirectory = [Environment]::GetFolderPath('Desktop')
$openFileDialog.Multiselect = $true
$result = $openFileDialog.ShowDialog()
if ($result -eq 'OK') {
$openFileDialog.FileNames
}
`;
const WINDOWS_FOLDER_SELECTION_SCRIPT = `
Add-Type -AssemblyName System.windows.forms
$folderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog
$dialogResult = $folderBrowser.ShowDialog()
if ($dialogResult -eq [System.Windows.Forms.DialogResult]::OK)
{
$folderBrowser.SelectedPath
}
`;
export const selectLocalContextData = async (type: TemplateDataSourceType) => {
try {
let selectedPath: string = "";
let execScript: string;
let execOpts: any = {};
switch (process.platform) {
case "win32": // Windows
execScript =
type === "file"
? WINDOWS_FILE_SELECTION_SCRIPT
: WINDOWS_FOLDER_SELECTION_SCRIPT;
execOpts = { shell: "powershell.exe" };
break;
case "darwin": // MacOS
execScript =
type === "file"
? MACOS_FILE_SELECTION_SCRIPT
: MACOS_FOLDER_SELECTION_SCRIPT;
break;
default: // Unsupported OS
console.log(red("Unsupported OS error!"));
process.exit(1);
}
selectedPath = execSync(execScript, execOpts).toString().trim();
const paths =
process.platform === "win32"
? selectedPath.split("\r\n")
: selectedPath.split(", ");
for (const p of paths) {
if (
fs.statSync(p).isFile() &&
!supportedContextFileTypes.includes(path.extname(p))
) {
console.log(
red(
`Please select a supported file type: ${supportedContextFileTypes}`,
),
);
process.exit(1);
}
}
return paths;
} catch (error) {
console.log(
red(
"Got an error when trying to select local context data! Please try again or select another data source option.",
),
);
process.exit(1);
}
};
export const onPromptState = (state: any) => {
if (state.aborted) {
// If we don't re-enable the terminal cursor before exiting
// the program, the cursor will remain hidden
process.stdout.write("\x1B[?25h");
process.stdout.write("\n");
process.exit(1);
}
};
export const toChoice = (value: string) => {
return { title: value, value };
};
export const questionHandlers = {
onCancel: () => {
console.error("Exiting.");
process.exit(1);
},
};
// Ask for next action after installation
export async function askPostInstallAction(
args: QuestionResults,
): Promise<TemplatePostInstallAction> {
const actionChoices = [
{
title: "Just generate code (~1 sec)",
value: "none",
},
{
title: "Start in VSCode (~1 sec)",
value: "VSCode",
},
{
title: "Generate code and install dependencies (~2 min)",
value: "dependencies",
},
];
const modelConfigured = !args.llamapack && args.modelConfig.isConfigured();
// If using LlamaParse, require LlamaCloud API key
const llamaCloudKeyConfigured = args.useLlamaParse
? args.llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
: true;
const hasVectorDb = args.vectorDb && args.vectorDb !== "none";
// Can run the app if all tools do not require configuration
if (
!hasVectorDb &&
modelConfigured &&
llamaCloudKeyConfigured &&
!toolsRequireConfig(args.tools)
) {
actionChoices.push({
title: "Generate code, install dependencies, and run the app (~2 min)",
value: "runApp",
});
}
const { action } = await prompts(
{
type: "select",
name: "action",
message: "How would you like to proceed?",
choices: actionChoices,
initial: 1,
},
questionHandlers,
);
return action;
}
@@ -1,4 +1,3 @@
__pycache__
poetry.lock
storage
.env
output
+18
View File
@@ -0,0 +1,18 @@
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, startup the backend as described in the [backend README](./backend/README.md).
Second, run the development server of the frontend as described in the [frontend README](./frontend/README.md).
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## 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) - learn about LlamaIndex (Typescript features).
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!

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