Compare commits

..

1 Commits

Author SHA1 Message Date
Marcus Schiesser 29d92d9948 feat: Generate NEXT_PUBLIC_CHAT_API for NextJS backend to specify alternative backend 2024-11-22 10:42:29 +07:00
688 changed files with 15032 additions and 53763 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Generate NEXT_PUBLIC_CHAT_API for NextJS backend to specify alternative backend
+12
View File
@@ -0,0 +1,12 @@
{
"extends": [
"prettier"
],
"rules": {
"max-params": [
"error",
4
],
"prefer-const": "error",
},
}
+40 -54
View File
@@ -1,15 +1,12 @@
name: E2E Tests for create-llama package
name: E2E Tests
on:
push:
branches: [main]
paths-ignore:
- "python/llama-index-server/**"
- ".github/workflows/*llama_index_server.yml"
pull_request:
branches: [main]
paths-ignore:
- "python/llama-index-server/**"
- ".github/workflows/*llama_index_server.yml"
env:
POETRY_VERSION: "1.6.1"
jobs:
e2e-python:
@@ -22,7 +19,7 @@ jobs:
python-version: ["3.11"]
os: [macos-latest, windows-latest, ubuntu-22.04]
frameworks: ["fastapi"]
vectordbs: ["none", "llamacloud"]
datasources: ["--no-files", "--example-file", "--llamacloud"]
defaults:
run:
shell: bash
@@ -35,10 +32,10 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Add uv to PATH # Ensure uv is available in subsequent steps
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: ${{ env.POETRY_VERSION }}
- uses: pnpm/action-setup@v3
@@ -53,15 +50,15 @@ jobs:
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
working-directory: packages/create-llama
working-directory: .
- name: Build create-llama
run: pnpm run build
working-directory: packages/create-llama
working-directory: .
- name: Install
run: pnpm run pack-install
working-directory: packages/create-llama
working-directory: .
- name: Run Playwright tests for Python
run: pnpm run e2e:python
@@ -69,18 +66,14 @@ jobs:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
FRAMEWORK: ${{ matrix.frameworks }}
VECTORDB: ${{ matrix.vectordbs }}
PYTHONIOENCODING: utf-8
PYTHONLEGACYWINDOWSSTDIO: utf-8
SERVER_PACKAGE_PATH: ${{ env.SERVER_PACKAGE_PATH }}
working-directory: packages/create-llama
DATASOURCE: ${{ matrix.datasources }}
working-directory: .
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report-python-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.vectordbs }}
path: packages/create-llama/playwright-report/
overwrite: true
name: playwright-report-python
path: ./playwright-report/
retention-days: 30
e2e-typescript:
@@ -89,10 +82,11 @@ jobs:
strategy:
fail-fast: true
matrix:
node-version: [22]
node-version: [18, 20]
python-version: ["3.11"]
os: [macos-latest, windows-latest, ubuntu-22.04]
frameworks: ["nextjs"]
vectordbs: ["none", "llamacloud"]
frameworks: ["nextjs", "express"]
datasources: ["--no-files", "--example-file", "--llamacloud"]
defaults:
run:
shell: bash
@@ -100,6 +94,16 @@ jobs:
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 }}
@@ -113,46 +117,28 @@ jobs:
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
working-directory: packages/create-llama
working-directory: .
- name: Build create-llama
run: pnpm run build
working-directory: packages/create-llama
working-directory: .
- name: Install
run: pnpm run pack-install
working-directory: packages/create-llama
- name: Build server
run: pnpm run build
working-directory: packages/server
- name: Pack @llamaindex/server package
run: |
pnpm pack --pack-destination "${{ runner.temp }}"
if [ "${{ runner.os }}" == "Windows" ]; then
file=$(find "${{ runner.temp }}" -name "llamaindex-server-*.tgz" | head -n 1)
mv "$file" "${{ runner.temp }}/llamaindex-server.tgz"
else
mv ${{ runner.temp }}/llamaindex-server-*.tgz ${{ runner.temp }}/llamaindex-server.tgz
fi
working-directory: packages/server
working-directory: .
- name: Run Playwright tests for TypeScript
run: |
pnpm run e2e:ts
run: pnpm run e2e:typescript
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
FRAMEWORK: ${{ matrix.frameworks }}
VECTORDB: ${{ matrix.vectordbs }}
SERVER_PACKAGE_PATH: ${{ runner.temp }}/llamaindex-server.tgz
working-directory: packages/create-llama
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.vectordbs}}-node${{ matrix.node-version }}
path: packages/create-llama/playwright-report/
overwrite: true
name: playwright-report-typescript
path: ./playwright-report/
retention-days: 30
@@ -16,16 +16,6 @@ jobs:
- uses: pnpm/action-setup@v3
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -41,21 +31,12 @@ jobs:
- name: Run Prettier
run: pnpm run format
- name: Run build
run: pnpm run build
- name: Run Typecheck for examples
run: pnpm run typecheck
working-directory: packages/server/examples
- name: Run Python format check
uses: chartboost/ruff-action@v1
with:
args: "format --check"
src: "python/llama-index-server"
- name: Run Python lint
uses: chartboost/ruff-action@v1
with:
args: "check"
src: "python/llama-index-server"
-9
View File
@@ -17,11 +17,6 @@ jobs:
- uses: pnpm/action-setup@v3
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v3
@@ -56,12 +51,8 @@ jobs:
with:
commit: Release ${{ steps.get-changeset-status.outputs.new-version }}
title: Release ${{ steps.get-changeset-status.outputs.new-version }}
# bump versions
version: pnpm new-version
# build package and call changeset publish
publish: pnpm release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
@@ -1,136 +0,0 @@
name: Build Package
on:
pull_request:
env:
PYTHON_VERSION: "3.9"
UI_TEST: "true"
jobs:
unit-test:
name: Unit Tests
runs-on: ${{ matrix.os }}
defaults:
run:
working-directory: python/llama-index-server
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.9"]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"
- name: Install dependencies
shell: bash
run: pnpm install && pnpm build
- name: Run unit tests
shell: bash
run: uv run pytest tests
type-check:
name: Type Check
runs-on: ubuntu-latest
defaults:
run:
working-directory: python/llama-index-server
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Install dependencies
run: pnpm install
- name: Run mypy
shell: bash
run: uv run mypy llama_index
build:
needs: [unit-test, type-check]
runs-on: ubuntu-latest
defaults:
run:
working-directory: python/llama-index-server
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"
- name: Install dependencies
run: pnpm install && pnpm build
- name: Build package
shell: bash
run: uv build
- name: Get the absolute wheel file path and save it to the output
shell: bash
id: get_whl_path
run: |
WHL_FILE=$(readlink -f dist/*.whl)
echo "whl_file=$WHL_FILE" >> $GITHUB_OUTPUT
- name: Test import
shell: bash
working-directory: ${{ github.workspace }}
env:
WHL_FILE: ${{ steps.get_whl_path.outputs.whl_file }}
run: |
uv run --with $WHL_FILE python -c "from llama_index.server import LlamaIndexServer"
- name: Check frontend resources is present
shell: bash
working-directory: ${{ github.workspace }}
env:
WHL_FILE: ${{ steps.get_whl_path.outputs.whl_file }}
run: |
uv run --with $WHL_FILE python -c "from llama_index.server.chat_ui import check_ui_resources; check_ui_resources()"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: llama-index-server
path: dist/
+18
View File
@@ -6,6 +6,9 @@ node_modules
.pnpm-store
.pnp.js
# testing
coverage
# next.js
.next/
out/
@@ -31,9 +34,24 @@ yarn-error.log*
dist/
lib/
# e2e
.cache
test-results/
playwright-report/
blob-report/
playwright/.cache/
.tsbuildinfo
e2e/cache
# intellij
**/.idea
# Python
.mypy_cache/
# build artifacts
create-llama-*.tgz
# vscode
.vscode
!.vscode/settings.json
+1 -2
View File
@@ -1,4 +1,3 @@
pnpm format
pnpm lint
uvx ruff check .
uvx ruff format . --check
uvx ruff format --check templates/
+3 -15
View File
@@ -1,18 +1,6 @@
node_modules/
apps/docs/i18n
apps/docs/docs/api
pnpm-lock.yaml
lib/
dist/
cache/
build/
.next/
out/
packages/server/server/
packages/server/project/
**/playwright-report/
**/test-results/
# Python
python/
**/*.mypy_cache/**
**/*.venv/**
**/*.ruff_cache/**
.docusaurus/
@@ -1,288 +1,5 @@
# create-llama
## 0.6.3
### Patch Changes
- fec752e: refactor: llamacloud configs
## 0.6.2
### Patch Changes
- 28b46be: chore: replace Python examples with llama-deploy
- 93e2abe: fix: unused imports and format
## 0.6.1
### Patch Changes
- 952b5b4: fix: peer deps and sourcemap issues made ts server start fail
- e8004fd: Fix: broken devcontainer due to deleted repo
## 0.6.0
### Minor Changes
- 8fa8c3b: Removed deprecated templates and simplified code
### Patch Changes
- 8fa8c3b: Feat: re-add --ask-models
## 0.5.22
### Patch Changes
- e2486eb: feat: support human in the loop for TS
## 0.5.21
### Patch Changes
- af9ad3c: feat: show document artifact after generating report
- a543a27: feat: bump chat-ui with inline artifact
## 0.5.20
### Patch Changes
- 3ff0a18: fix: default header padding
## 0.5.19
### Patch Changes
- 5fe9e17: support eject to fully customize next folder
- b8a1ff6: Support citation for agentic template (Python)
## 0.5.18
### Patch Changes
- 8d59ef0: Add layout_dir config to the generated python code
## 0.5.17
### Patch Changes
- eee3230: feat: support custom layout
## 0.5.16
### Patch Changes
- 6f75d4a: fix: unsupported language in code gen workflow
- d0618fa: Fix LlamaCloud generate script issue
## 0.5.15
### Patch Changes
- 527075c: Enable dev mode that allows updating code directly in the UI
## 0.5.14
### Patch Changes
- 1df8cfb: Split artifacts use case to document generator and code generator
- 1b5a519: chore: improve dev experience with nodemon
- b3eb0ba: Fix typing check issue
- 556f33c: fix chromadb dependency issue
- 2451539: fix: remove dead generated ai code
- 7a70390: Deprecate pro mode
## 0.5.13
### Patch Changes
- f4ca602: Add artifact use case for Typescript template
- f4ca602: Update typescript use cases to use the new workflow engine
## 0.5.12
### Patch Changes
- 241d82a: Add artifacts use case (python)
## 0.5.11
### Patch Changes
- 3960618: chore: create-llama monorepo
- 8fe5fc2: chore: add llamaindex server package
## 0.5.10
### Patch Changes
- 0a2e12a: Use uv as the default package manager
## 0.5.9
### Patch Changes
- 4bc53ac: Bump new chat ui and update deep research component
- 4bc53ac: Support generate UI for deep research use case (Typescript)
## 0.5.8
### Patch Changes
- 765181a: chore: test typescript e2e with node 20 and 22
## 0.5.7
### Patch Changes
- 5988657: chore: bump llmaindex
## 0.5.6
### Patch Changes
- d363ced: Bump llamaindex server packages
## 0.5.5
### Patch Changes
- ee85320: The default custom deep research component does not work.
## 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
-201
View File
@@ -1,201 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repository Overview
Create-llama is a monorepo containing CLI tools and server frameworks for building LlamaIndex-powered applications. The repository combines TypeScript/Node.js and Python components in a unified development environment.
## Architecture
### Monorepo Structure
- **`packages/create-llama/`**: Main CLI tool for scaffolding LlamaIndex applications
- **`packages/server/`**: TypeScript/Next.js server framework (`@llamaindex/server`)
- **`python/llama-index-server/`**: Python/FastAPI server framework
- **Root**: Workspace configuration and shared development tools
### Key Technologies
- **Package Manager**: pnpm with workspace configuration
- **Build Tools**: bunchee (TypeScript), Next.js, hatchling (Python)
- **Testing**: Playwright for e2e, pytest for Python
- **Version Management**: changesets for TypeScript packages, manual for Python
## Development Commands
### Root Level (Monorepo)
```bash
pnpm dev # Start all packages in development mode
pnpm build # Build all packages
pnpm lint # ESLint across TypeScript packages
pnpm format # Prettier formatting
pnpm e2e # Run end-to-end tests
```
### Create-llama Package
```bash
cd packages/create-llama
npm run build # Build CLI using bash script and ncc
npm run dev # Watch mode development
npm run e2e # Playwright tests for generated projects
npm run clean # Clean build artifacts and template caches
```
### TypeScript Server Package
```bash
cd packages/server
pnpm dev # Watch mode with bunchee
pnpm build # Multi-step build: ESM/CJS + Next.js + static assets
pnpm clean # Clean all build outputs
```
### Python Server Package
```bash
cd python/llama-index-server
uv run generate # Index data files
fastapi dev # Start development server with hot reload
pytest # Run test suite
```
## Template System
The CLI uses a sophisticated template system in `packages/create-llama/templates/`:
### Organization
- **`types/`**: Base project structures (streaming, reflex, llamaindexserver)
- **`components/`**: Reusable components across frameworks
- `engines/` - Chat and agent engines
- `loaders/` - File, web, database loaders
- `providers/` - AI model configurations
- `vectordbs/` - Vector database integrations
- `use-cases/` - Workflow implementations
### Development Workflow
- Templates support multiple frameworks (Next.js, Express, FastAPI)
- Component system allows mix-and-match functionality
- E2E tests validate generated projects work correctly
## Server Framework Architecture
### TypeScript Server (`@llamaindex/server`)
- **Core**: `LlamaIndexServer` class wrapping Next.js with workflow support
- **Frontend**: React-based chat UI with shadcn/ui components
- **API**: `/api/chat` endpoint with streaming responses
- **Build Process**: Complex multi-step build including static assets for Python integration
### Python Server (`llama-index-server`)
- **Core**: `LlamaIndexServer` class extending FastAPI
- **Architecture**: Workflow factory pattern for stateless request handling
- **UI Generation**: AI-powered React component generation from Pydantic schemas
- **Development**: Hot reloading support with dev mode
## Common Patterns
### Workflow Integration
Both server frameworks use factory patterns:
```typescript
// TypeScript
const server = new LlamaIndexServer({
workflow: (context) => createWorkflow(context)
});
// Python
def create_workflow(chat_request: ChatRequest) -> Workflow:
return MyWorkflow(chat_request.messages)
```
### Event System
Structured events for UI communication:
- **UIEvent**: Custom components with Pydantic/Zod schemas
- **ArtifactEvent**: Code/documents for Canvas panel
- **SourceNodesEvent**: Document sources with metadata
- **AgentRunEvent**: Tool usage and progress tracking
### File Handling
- Both servers auto-mount `data/` and `output/` directories
- LlamaCloud integration for remote file access
- Static file serving through framework-specific methods
## Testing Strategy
### E2E Testing
- Playwright tests in `packages/create-llama/e2e/`
- Tests both Python and TypeScript generated projects
- Validates CLI generation and application functionality
### Unit Testing
- Python: pytest with comprehensive API and service tests
- TypeScript: Integrated testing through build process
## Build Process
### Create-llama CLI
1. TypeScript compilation with bash script
2. ncc bundling for standalone executable
3. Template validation and caching
### Server Package Build
1. **prebuild**: Clean directories
2. **build**: bunchee compilation to ESM/CJS
3. **postbuild**: Next.js preparation and static asset generation
4. **prepare:py-static**: Python integration assets
### Release Process
```bash
pnpm release # Build all + publish npm packages + Python release
```
## Development Environment Setup
### Prerequisites
- Node.js >=16.14.0
- Python with uv package manager
- pnpm for package management
### Common Workflow
1. Clone repository and run `pnpm install`
2. For CLI development: work in `packages/create-llama/`
3. For server development: choose TypeScript or Python package
4. Use `pnpm dev` for concurrent development across packages
5. Run `pnpm e2e` to validate changes with generated projects
## Special Considerations
### Template Development
- Changes to templates require rebuilding CLI
- E2E tests validate template functionality across frameworks
- Template caching system speeds up repeated builds
### Cross-package Dependencies
- Server package builds static assets for Python integration
- Version synchronization between TypeScript and Python packages
- Shared UI components and styling across implementations
### Performance
- CLI uses caching for template operations
- Server frameworks support streaming responses
- Background processing for file operations and LlamaCloud integration
+35 -19
View File
@@ -25,10 +25,13 @@ 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 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 frameworks:
- **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 with [LlamaIndex Server for TS](https://npmjs.com/package/@llamaindex/server).
- **Python FastAPI**: if you select this option, youll get full-stack Python application powered by the [llama-index Python package](https://pypi.org/project/llama-index/) and [LlamaIndex Server for Python](https://pypi.org/project/llama-index-server/)
- 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:
- **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
- 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:
@@ -37,11 +40,11 @@ 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`.
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`).
The app will ingest any supported files you put in this directory. Your Next.js 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 apps, run:
Before you can use your data, you need to index it. If you're using the Next.js or Express apps, run:
```bash
npm run generate
@@ -52,16 +55,16 @@ Then re-start your app. Remember you'll need to re-run `generate` if you add new
If you're using the Python backend, you can trigger indexing of your data by calling:
```bash
uv run generate
poetry run generate
```
## Customizing the AI models
The app will default to OpenAI's `gpt-4.1` LLM and `text-embedding-3-large` embedding model.
The app will default to OpenAI's `gpt-4o-mini` LLM and `text-embedding-3-large` embedding model.
If you want to use different models, add the `--ask-models` CLI parameter.
If you want to use different OpenAI models, add the `--ask-models` CLI parameter.
You can also replace one of the default models with one of our [dozens of other supported LLMs](https://docs.llamaindex.ai/en/stable/module_guides/models/llms/modules.html).
You can also replace OpenAI with one of our [dozens of other supported LLMs](https://docs.llamaindex.ai/en/stable/module_guides/models/llms/modules.html).
To do so, you have to manually change the generated code (edit the `settings.ts` file for Typescript projects or the `settings.py` file for Python projects)
@@ -87,10 +90,11 @@ Need to install the following packages:
create-llama@latest
Ok to proceed? (y) y
✔ What is your project named? … my-app
✔ What use case do you want to build? Agentic RAG
✔ 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): …
✔ Please provide your OpenAI API key (leave blank to skip): …
? How would you like to proceed? - Use arrow-keys. Return to submit.
Just generate code (~1 sec)
Start in VSCode (~1 sec)
@@ -102,16 +106,28 @@ Ok to proceed? (y) y
You can also pass command line arguments to set up a new project
non-interactively. For a list of the latest options, call `create-llama --help`.
### Running in pro mode
If you prefer more advanced customization options, you can run `create-llama` in pro mode using the `--pro` flag.
In pro mode, instead of selecting a predefined use case, you'll be prompted to select each technical component of your project. This allows for greater flexibility in customizing your project, including:
- **Vector Store**: Choose from a variety of vector stores for keeping your documents, including MongoDB, Pinecone, Weaviate, Qdrant and Chroma.
- **Tools**: Choose from a variety of agent tools (functions called by the LLM), such as:
- Code Interpreter: Executes Python code in a secure Jupyter notebook environment
- Artifact Code Generator: Generates code artifacts that can be run in a sandbox
- OpenAPI Action: Facilitates requests to a provided OpenAPI schema
- Image Generator: Creates images based on text descriptions
- Web Search: Performs web searches to retrieve up-to-date information
- **Data Sources**: Integrate various data sources into your chat application, including local files, websites, or database-retrieved data.
- **Backend Options**: Besides using Next.js or FastAPI, you can also select to use Express for a more traditional Node.js application.
- **Observability**: Choose from a variety of LLM observability tools, including LlamaTrace and Traceloop.
Pro mode is ideal for developers who want fine-grained control over their project's configuration and are comfortable with more technical setup options.
## LlamaIndex Documentation
- [TS/JS docs](https://ts.llamaindex.ai/)
- [Python docs](https://docs.llamaindex.ai/en/stable/)
## LlamaIndex Server
The generated code is using the LlamaIndex Server, which serves LlamaIndex Workflows and Agent Workflows via an API server. See the following docs for more information:
- [LlamaIndex Server For TypeScript](./packages/server/README.md)
- [LlamaIndex Server For Python](./python/llama-index-server/README.md)
Inspired by and adapted from [create-next-app](https://github.com/vercel/next.js/tree/canary/packages/create-next-app)
+173
View File
@@ -0,0 +1,173 @@
/* eslint-disable import/no-extraneous-dependencies */
import path from "path";
import { green, yellow } from "picocolors";
import { tryGitInit } from "./helpers/git";
import { isFolderEmpty } from "./helpers/is-folder-empty";
import { getOnline } from "./helpers/is-online";
import { isWriteable } from "./helpers/is-writeable";
import { makeDir } from "./helpers/make-dir";
import terminalLink from "terminal-link";
import type { InstallTemplateArgs, TemplateObservability } from "./helpers";
import { installTemplate } from "./helpers";
import { writeDevcontainer } from "./helpers/devcontainer";
import { templatesDir } from "./helpers/dir";
import { toolsRequireConfig } from "./helpers/tools";
export type InstallAppArgs = Omit<
InstallTemplateArgs,
"appName" | "root" | "isOnline" | "port"
> & {
appPath: string;
frontend: boolean;
};
export async function createApp({
template,
framework,
ui,
appPath,
packageManager,
frontend,
modelConfig,
llamaCloudKey,
communityProjectConfig,
llamapack,
vectorDb,
postInstallAction,
dataSources,
tools,
useLlamaParse,
observability,
agents,
}: InstallAppArgs): Promise<void> {
const root = path.resolve(appPath);
if (!(await isWriteable(path.dirname(root)))) {
console.error(
"The application path is not writable, please check folder permissions and try again.",
);
console.error(
"It is likely you do not have write permissions for this folder.",
);
process.exit(1);
}
const appName = path.basename(root);
await makeDir(root);
if (!isFolderEmpty(root, appName)) {
process.exit(1);
}
const useYarn = packageManager === "yarn";
const isOnline = !useYarn || (await getOnline());
console.log(`Creating a new LlamaIndex app in ${green(root)}.`);
console.log();
const args = {
appName,
root,
template,
framework,
ui,
packageManager,
isOnline,
modelConfig,
llamaCloudKey,
communityProjectConfig,
llamapack,
vectorDb,
postInstallAction,
dataSources,
tools,
useLlamaParse,
observability,
agents,
};
// Install backend
await installTemplate({ ...args, backend: true });
if (frontend && framework === "fastapi") {
// install frontend
const frontendRoot = path.join(root, ".frontend");
await makeDir(frontendRoot);
await installTemplate({
...args,
root: frontendRoot,
framework: "nextjs",
backend: false,
});
}
await writeDevcontainer(root, templatesDir, framework, frontend);
process.chdir(root);
if (tryGitInit(root)) {
console.log("Initialized a git repository.");
console.log();
}
if (toolsRequireConfig(tools)) {
const configFile =
framework === "fastapi" ? "config/tools.yaml" : "config/tools.json";
console.log(
yellow(
`You have selected tools that require configuration. Please configure them in the ${terminalLink(
configFile,
`file://${root}/${configFile}`,
)} file.`,
),
);
}
console.log("");
console.log(`${green("Success!")} Created ${appName} at ${appPath}`);
console.log(
`Now have a look at the ${terminalLink(
"README.md",
`file://${root}/README.md`,
)} and learn how to get started.`,
);
outputObservability(args.observability);
if (
dataSources.some((dataSource) => dataSource.type === "file") &&
process.platform === "linux"
) {
console.log(
yellow(
`You can add your own data files to ${terminalLink(
"data",
`file://${root}/data`,
)} folder manually.`,
),
);
}
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;
}
}
+233
View File
@@ -0,0 +1,233 @@
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 };
}
+60
View File
@@ -0,0 +1,60 @@
/* 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 } 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";
// The extractor template currently only works with FastAPI and files (and not on Windows)
if (
process.platform !== "win32" &&
templateFramework === "fastapi" &&
dataSource === "--example-file"
) {
test.describe("Test extractor template", async () => {
let appPort: number;
let name: string;
let appProcess: ChildProcess;
let cwd: string;
// Create extractor app
test.beforeAll(async () => {
cwd = await createTestDir();
appPort = Math.floor(Math.random() * 10000) + 10000;
const result = await runCreateLlama({
cwd,
templateType: "extractor",
templateFramework: "fastapi",
dataSource: "--example-file",
vectorDb: "none",
port: appPort,
postInstallAction: "runApp",
});
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,
});
});
});
}
@@ -1,49 +1,52 @@
/* 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, type TemplateVectorDB } from "../../helpers";
import {
ALL_PYTHON_USE_CASES,
ALL_TYPESCRIPT_USE_CASES,
} from "../../helpers/use-case";
import { createTestDir, runCreateLlama } from "../utils";
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 vectorDb: TemplateVectorDB = process.env.VECTORDB
? (process.env.VECTORDB as TemplateVectorDB)
: "none";
const llamaCloudProjectName = "create-llama";
const llamaCloudIndexName = "e2e-test";
const allUseCases =
templateFramework === "nextjs"
? ALL_TYPESCRIPT_USE_CASES
: ALL_PYTHON_USE_CASES;
const isPythonLlamaDeploy = templateFramework === "fastapi";
const dataSource: string = "--example-file";
const templateUI: TemplateUI = "shadcn";
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
const appType: AppType = templateFramework === "fastapi" ? "--frontend" : "";
const userMessage = "Write a blog post about physical standards for letters";
const templateAgents = ["financial_report", "blog", "form_filling"];
for (const useCase of allUseCases) {
test.describe(`Test use case ${useCase} ${templateFramework} ${vectorDb}`, async () => {
for (const agents of templateAgents) {
test.describe(`Test multiagent template ${agents} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
test.skip(
process.platform !== "linux" || process.env.DATASOURCE === "--no-files",
"The multiagent template currently only works with files. 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: "multiagent",
templateFramework,
dataSource,
vectorDb,
port,
postInstallAction: isPythonLlamaDeploy ? "dependencies" : "runApp",
useCase,
llamaCloudProjectName,
llamaCloudIndexName,
postInstallAction: templatePostInstallAction,
templateUI,
appType,
agents,
});
name = result.projectName;
appProcess = result.appProcess;
@@ -56,24 +59,22 @@ for (const useCase of allUseCases) {
test("Frontend should have a title", async ({ page }) => {
test.skip(
isPythonLlamaDeploy,
"Skip frontend tests for Python LllamaDeploy",
templatePostInstallAction !== "runApp" ||
templateFramework === "express",
);
await page.goto(`http://localhost:${port}`);
await expect(page.getByText("Built by LlamaIndex")).toBeVisible({
timeout: 5 * 60 * 1000,
});
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(
useCase === "financial_report" ||
useCase === "deep_research" ||
isPythonLlamaDeploy,
"Skip chat tests for financial report and deep research. Also skip for Python LlamaDeploy",
templatePostInstallAction !== "runApp" ||
agents === "financial_report" ||
agents === "form_filling" ||
templateFramework === "express",
"Skip chat tests for financial report and form filling.",
);
await page.goto(`http://localhost:${port}`);
await page.fill("form textarea", userMessage);
@@ -85,12 +86,6 @@ for (const useCase of allUseCases) {
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();
});
+128
View File
@@ -0,0 +1,128 @@
/* 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
@@ -0,0 +1,105 @@
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;
}
}
});
@@ -6,9 +6,13 @@ import waitPort from "wait-port";
import {
TemplateFramework,
TemplatePostInstallAction,
TemplateType,
TemplateUI,
TemplateVectorDB,
} from "../helpers";
export type AppType = "--frontend" | "--no-frontend" | "";
export type CreateLlamaResult = {
projectName: string;
appProcess: ChildProcess;
@@ -16,47 +20,103 @@ export type CreateLlamaResult = {
export type RunCreateLlamaOptions = {
cwd: string;
templateType: TemplateType;
templateFramework: TemplateFramework;
dataSource: string;
vectorDb: TemplateVectorDB;
port: number;
postInstallAction: TemplatePostInstallAction;
useCase: string;
templateUI?: TemplateUI;
appType?: AppType;
llamaCloudProjectName?: string;
llamaCloudIndexName?: string;
tools?: string;
useLlamaParse?: boolean;
observability?: string;
agents?: string;
};
export async function runCreateLlama({
cwd,
templateType,
templateFramework,
dataSource,
vectorDb,
port,
postInstallAction,
useCase,
templateUI,
appType,
llamaCloudProjectName,
llamaCloudIndexName,
tools,
useLlamaParse,
observability,
agents,
}: 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",
);
}
const name = [templateFramework, useCase, vectorDb].join("-");
const name = [
templateType,
templateFramework,
dataSource.split(" ")[0],
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 = [
"create-llama",
name,
"--template",
templateType,
"--framework",
templateFramework,
...dataSourceArgs,
"--vector-db",
vectorDb,
"--use-npm",
"--use-pnpm",
"--port",
port,
"--post-install-action",
postInstallAction,
"--use-case",
useCase,
"--tools",
tools ?? "none",
"--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" && agents) {
commandArgs.push("--agents", agents);
}
const command = commandArgs.join(" ");
console.log(`running command '${command}' in ${cwd}`);
const appProcess = exec(command, {
-65
View File
@@ -1,65 +0,0 @@
import eslint from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import globals from "globals";
import tseslint from "typescript-eslint";
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
eslintConfigPrettier,
{
languageOptions: {
ecmaVersion: 2022,
sourceType: "module",
globals: {
...globals.browser,
...globals.node,
},
},
},
{
files: ["packages/create-llama/**"],
rules: {
"max-params": ["error", 4],
"prefer-const": "error",
"no-empty": "off",
"no-extra-boolean-cast": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/no-wrapper-object-types": "off",
"@typescript-eslint/ban-ts-comment": "off",
},
},
{
files: ["packages/server/**"],
rules: {
"no-irregular-whitespace": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": [
"error",
{
ignoreRestArgs: true,
},
],
},
},
{
ignores: [
"python/**",
"**/*.mypy_cache/**",
"**/*.venv/**",
"**/*.ruff_cache/**",
"**/dist/**",
"**/e2e/cache/**",
"**/lib/*",
"**/.next/**",
"**/out/**",
"**/node_modules/**",
"**/build/**",
"packages/server/server/**",
"packages/server/project/**",
"packages/server/bin/**",
],
},
);
+6
View File
@@ -0,0 +1,6 @@
export const COMMUNITY_OWNER = "run-llama";
export const COMMUNITY_REPO = "create_llama_projects";
export const LLAMA_PACK_OWNER = "run-llama";
export const LLAMA_PACK_REPO = "llama_index";
export const LLAMA_PACK_FOLDER = "llama-index-packs";
export const LLAMA_PACK_FOLDER_PATH = `${LLAMA_PACK_OWNER}/${LLAMA_PACK_REPO}/main/${LLAMA_PACK_FOLDER}`;
@@ -1,3 +1,4 @@
/* eslint-disable import/no-extraneous-dependencies */
import { async as glob } from "fast-glob";
import fs from "fs";
import path from "path";
@@ -60,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;
}
+120
View File
@@ -0,0 +1,120 @@
import fs from "fs/promises";
import path from "path";
import yaml, { Document } from "yaml";
import { templatesDir } from "./dir";
import { DbSourceConfig, TemplateDataSource, WebSourceConfig } from "./types";
export const EXAMPLE_FILE: TemplateDataSource = {
type: "file",
config: {
path: path.join(templatesDir, "components", "data", "101.pdf"),
},
};
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",
),
},
},
{
type: "file",
config: {
url: new URL(
"https://ir.tesla.com/_flysystem/s3/sec/000162828024002390/tsla-20231231-gen.pdf",
),
},
},
];
export function getDataSources(
files?: string,
exampleFile?: boolean,
): TemplateDataSource[] | undefined {
let dataSources: TemplateDataSource[] | undefined = undefined;
if (files) {
// If user specified files option, then the program should use context engine
dataSources = files.split(",").map((filePath) => ({
type: "file",
config: {
path: filePath,
},
}));
}
if (exampleFile) {
dataSources = [...(dataSources ? dataSources : []), EXAMPLE_FILE];
}
return dataSources;
}
export async function writeLoadersConfig(
root: string,
dataSources: TemplateDataSource[],
useLlamaParse?: boolean,
) {
const loaderConfig: Record<string, any> = {};
// Always set file loader config
loaderConfig.file = createFileLoaderConfig(useLlamaParse);
if (dataSources.some((ds) => ds.type === "web")) {
loaderConfig.web = createWebLoaderConfig(dataSources);
}
const dbLoaders = dataSources.filter((ds) => ds.type === "db");
if (dbLoaders.length > 0) {
loaderConfig.db = createDbLoaderConfig(dbLoaders);
}
// Create a new Document with the loaderConfig
const yamlDoc = new Document(loaderConfig);
// 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],
};
});
}
@@ -1,6 +1,5 @@
import fs from "fs";
import path from "path";
import { assetRelocator, copy } from "./copy";
import { TemplateFramework } from "./types";
function renderDevcontainerContent(
@@ -30,6 +29,7 @@ export const writeDevcontainer = async (
root: string,
templatesDir: string,
framework: TemplateFramework,
frontend: boolean,
) => {
const devcontainerDir = path.join(root, ".devcontainer");
if (fs.existsSync(devcontainerDir)) {
@@ -46,25 +46,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);
}
};
@@ -1,17 +1,23 @@
import fs from "fs/promises";
import path from "path";
import { TOOL_SYSTEM_PROMPT_ENV_VAR, Tool } from "./tools";
import {
EnvVar,
InstallTemplateArgs,
ModelConfig,
TemplateDataSource,
TemplateFramework,
TemplateObservability,
TemplateType,
TemplateUseCase,
TemplateVectorDB,
} from "./types";
import { TSYSTEMS_LLMHUB_API_URL } from "./providers/llmhub";
import { USE_CASE_CONFIGS } from "./use-case";
export type EnvVar = {
name?: string;
description?: string;
value?: string;
};
const renderEnvVar = (envVars: EnvVar[]): string => {
return envVars.reduce(
@@ -32,7 +38,6 @@ const renderEnvVar = (envVars: EnvVar[]): string => {
const getVectorDBEnvs = (
vectorDb?: TemplateVectorDB,
framework?: TemplateFramework,
template?: TemplateType,
): EnvVar[] => {
if (!vectorDb || !framework) {
return [];
@@ -157,7 +162,7 @@ const getVectorDBEnvs = (
description:
"The organization ID for the LlamaCloud project (uses default organization if not specified)",
},
...(framework === "nextjs" && template !== "llamaindexserver"
...(framework === "nextjs"
? // activate index selector per default (not needed for non-NextJS backends as it's handled by createFrontendEnvFile)
[
{
@@ -169,7 +174,7 @@ const getVectorDBEnvs = (
]
: []),
];
case "chroma": {
case "chroma":
const envs = [
{
name: "CHROMA_COLLECTION",
@@ -194,7 +199,6 @@ Otherwise, use CHROMA_HOST and CHROMA_PORT config above`,
});
}
return envs;
}
case "weaviate":
return [
{
@@ -213,28 +217,23 @@ Otherwise, use CHROMA_HOST and CHROMA_PORT config above`,
},
];
default:
return template !== "llamaindexserver"
? [
{
name: "STORAGE_CACHE_DIR",
description: "The directory to store the local storage cache.",
value: ".cache",
},
]
: [];
return [
{
name: "STORAGE_CACHE_DIR",
description: "The directory to store the local storage cache.",
value: ".cache",
},
];
}
};
const getModelEnvs = (
modelConfig: ModelConfig,
framework: TemplateFramework,
template: TemplateType,
useCase: TemplateUseCase,
): EnvVar[] => {
const isPythonLlamaDeploy =
framework === "fastapi" && template === "llamaindexserver";
const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
return [
{
name: "MODEL_PROVIDER",
description: "The provider for the AI models to use.",
value: modelConfig.provider,
},
{
name: "MODEL",
description: "The name of LLM model to use.",
@@ -245,25 +244,15 @@ const getModelEnvs = (
description: "Name of the embedding model to use.",
value: modelConfig.embeddingModel,
},
...(isPythonLlamaDeploy
? [
{
name: "NEXT_PUBLIC_STARTER_QUESTIONS",
description:
"Initial questions to display in the chat (`starterQuestions`)",
value: JSON.stringify(
USE_CASE_CONFIGS[useCase]?.starterQuestions ?? [],
),
},
]
: [
{
name: "CONVERSATION_STARTERS",
description:
"The questions to help users get started (multi-line).",
},
]),
...(USE_CASE_CONFIGS[useCase]?.additionalEnvVars ?? []),
{
name: "EMBEDDING_DIM",
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"
? [
{
@@ -271,18 +260,14 @@ const getModelEnvs = (
description: "The OpenAI API key to use.",
value: modelConfig.apiKey,
},
...(isPythonLlamaDeploy
? []
: [
{
name: "LLM_TEMPERATURE",
description: "Temperature for sampling from the model.",
},
{
name: "LLM_MAX_TOKENS",
description: "Maximum number of tokens to generate.",
},
]),
{
name: "LLM_TEMPERATURE",
description: "Temperature for sampling from the model.",
},
{
name: "LLM_MAX_TOKENS",
description: "Maximum number of tokens to generate.",
},
]
: []),
...(modelConfig.provider === "anthropic"
@@ -391,31 +376,180 @@ const getModelEnvs = (
const getFrameworkEnvs = (
framework: TemplateFramework,
template?: TemplateType,
port?: number,
): EnvVar[] => {
const sPort = port?.toString() || "8000";
const result: EnvVar[] = [];
if (framework === "fastapi" && template !== "llamaindexserver") {
const result: EnvVar[] = [
{
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.",
description: "The address to start the backend app.",
value: "0.0.0.0",
},
{
name: "APP_PORT",
description: "The port to start the FastAPI app.",
description: "The port to start the backend app.",
value: sPort,
},
],
);
}
if (framework === "nextjs") {
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;
};
const getEngineEnvs = (): EnvVar[] => {
return [
{
name: "TOP_K",
description:
"The number of similar embeddings to return when retrieving documents.",
},
];
};
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 defaultSystemPrompt =
"You are a helpful assistant who helps users with their questions.";
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 = toolSystemPrompt
? `\"${toolSystemPrompt}\"`
: defaultSystemPrompt;
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 you 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<
@@ -427,44 +561,47 @@ export const createBackendEnvFile = async (
| "dataSources"
| "template"
| "port"
| "useLlamaParse"
| "useCase"
| "tools"
| "observability"
>,
) => {
// 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),
...getFrameworkEnvs(opts.framework, opts.template, opts.port),
...getModelEnvs(
opts.modelConfig,
opts.framework,
opts.template,
opts.useCase,
),
{
name: "LLAMA_CLOUD_API_KEY",
description: `The Llama Cloud API key.`,
value: opts.llamaCloudKey,
},
// Add environment variables of each component
...getModelEnvs(opts.modelConfig),
...getEngineEnvs(),
...getVectorDBEnvs(opts.vectorDb, opts.framework),
...getFrameworkEnvs(opts.framework, opts.port),
...getToolEnvs(opts.tools),
...getTemplateEnvs(opts.template),
...getObservabilityEnvs(opts.observability),
...getSystemPromptEnv(opts.tools, opts.dataSources, opts.template),
];
// Render and write env file
const content = renderEnvVar(envVars);
const isPythonLlamaDeploy =
opts.framework === "fastapi" && opts.template === "llamaindexserver";
// each llama-deploy service will need a .env inside its directory
// this .env will be copied along with workflow code when service is deployed
// so that we need to put the .env file inside src/ instead of root
const envPath = isPythonLlamaDeploy
? path.join(root, "src", envFileName)
: path.join(root, envFileName);
await fs.writeFile(envPath, content);
await fs.writeFile(path.join(root, envFileName), content);
console.log(`Created '${envFileName}' file. Please check the settings.`);
};
export const createFrontendEnvFile = async (
root: string,
opts: {
vectorDb?: TemplateVectorDB;
},
) => {
const defaultFrontendEnvs = [
{
name: "NEXT_PUBLIC_USE_LLAMACLOUD",
description: "Let's the user change indexes in LlamaCloud projects",
value: opts.vectorDb === "llamacloud" ? "true" : "false",
},
];
const content = renderEnvVar(defaultFrontendEnvs);
await fs.writeFile(path.join(root, ".env"), content);
};
@@ -1,3 +1,4 @@
/* eslint-disable import/no-extraneous-dependencies */
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
@@ -1,13 +1,18 @@
import { callPackageManager } from "./install";
import path from "path";
import picocolors, { cyan } from "picocolors";
import { cyan } from "picocolors";
import fsExtra from "fs-extra";
import { createBackendEnvFile } from "./env-variables";
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 { installPythonTemplate } from "./python";
import { downloadAndExtractRepo } from "./repo";
import { ConfigFileType, writeToolsConfig } from "./tools";
import {
FileSourceConfig,
InstallTemplateArgs,
@@ -17,7 +22,6 @@ import {
TemplateVectorDB,
} from "./types";
import { installTSTemplate } from "./typescript";
import { isHavingUvLockFile, tryUvRun } from "./uv";
const checkForGenerateScript = (
modelConfig: ModelConfig,
@@ -37,11 +41,7 @@ const checkForGenerateScript = (
missingSettings.push("your LLAMA_CLOUD_API_KEY");
}
if (
vectorDb !== undefined &&
vectorDb !== "none" &&
vectorDb !== "llamacloud"
) {
if (vectorDb !== "none" && vectorDb !== "llamacloud") {
missingSettings.push("your Vector DB environment variables");
}
@@ -52,7 +52,6 @@ const checkForGenerateScript = (
async function generateContextData(
framework: TemplateFramework,
modelConfig: ModelConfig,
dataSources: TemplateDataSource[],
packageManager?: PackageManager,
vectorDb?: TemplateVectorDB,
llamaCloudKey?: string,
@@ -61,7 +60,7 @@ async function generateContextData(
if (packageManager) {
const runGenerate = `${cyan(
framework === "fastapi"
? "uv run generate"
? "poetry run generate"
: `${packageManager} run generate`,
)}`;
@@ -75,55 +74,39 @@ async function generateContextData(
if (!missingSettings.length) {
// If all the required environment variables are set, run the generate script
if (framework === "fastapi") {
if (isHavingUvLockFile()) {
if (isHavingPoetryLockFile()) {
console.log(`Running ${runGenerate} to generate the context data.`);
const result = tryUvRun("generate");
const result = tryPoetryRun("poetry run generate");
if (!result) {
console.log(`Failed to run ${runGenerate}.`);
process.exit(1);
}
console.log(`Generated context data`);
return;
} else {
console.log(
picocolors.yellow(
`\nWarning: uv.lock not found. Dependency installation might be incomplete. Skipping context generation.\nIf dependencies were installed, try running '${runGenerate}' manually.\n`,
),
);
}
} else {
console.log(`Running ${runGenerate} to generate the context data.`);
const shouldRunGenerate = dataSources.length > 0;
if (shouldRunGenerate) {
await callPackageManager(packageManager, true, ["run", "generate"]);
}
await callPackageManager(packageManager, true, ["run", "generate"]);
return;
}
}
const settingsMessage = `After setting ${missingSettings.join(" and ")}, run ${runGenerate} to generate the context data.`;
console.log(picocolors.yellow(`\n${settingsMessage}\n\n`));
console.log(`\n${settingsMessage}\n\n`);
}
}
const downloadFile = async (url: string, destPath: string) => {
const response = await fetch(url);
const fileBuffer = await response.arrayBuffer();
await fsExtra.writeFile(destPath, new Uint8Array(fileBuffer));
await fsExtra.writeFile(destPath, Buffer.from(fileBuffer));
};
const prepareContextData = async (
root: string,
dataSources: TemplateDataSource[],
isPythonLlamaDeploy: boolean,
) => {
const dataDir = isPythonLlamaDeploy
? path.join(root, "ui", "data")
: path.join(root, "data");
await makeDir(dataDir);
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
@@ -133,65 +116,117 @@ const prepareContextData = async (
dataSourceConfig.url.toString(),
);
const destPath = path.join(
dataDir,
dataSourceConfig.filename ??
path.basename(dataSourceConfig.url.toString()),
root,
"data",
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(dataDir, path.basename(dataSourceConfig.path));
const destPath = path.join(
root,
"data",
path.basename(dataSourceConfig.path),
);
await fsExtra.copy(dataSourceConfig.path, destPath);
}
}
};
export const installTemplate = async (props: InstallTemplateArgs) => {
const installCommunityProject = async ({
root,
communityProjectConfig,
}: Pick<InstallTemplateArgs, "root" | "communityProjectConfig">) => {
const { owner, repo, branch, filePath } = communityProjectConfig!;
console.log("\nInstalling community project:", filePath || repo);
await downloadAndExtractRepo(root, {
username: owner,
name: repo,
branch,
filePath: filePath || "",
});
};
export const installTemplate = async (
props: InstallTemplateArgs & { backend: boolean },
) => {
process.chdir(props.root);
if (props.template === "community" && props.communityProjectConfig) {
await installCommunityProject(props);
return;
}
if (props.template === "llamapack" && props.llamapack) {
await installLlamapackProject(props);
return;
}
if (props.framework === "fastapi") {
await installPythonTemplate(props);
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,
);
}
} else {
await installTSTemplate(props);
}
const isPythonLlamaDeploy =
props.framework === "fastapi" && props.template === "llamaindexserver";
// 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.
await createBackendEnvFile(props.root, props);
await prepareContextData(
// write tools configuration
await writeToolsConfig(
props.root,
props.dataSources.filter((ds) => ds.type === "file"),
isPythonLlamaDeploy,
props.tools,
props.framework === "fastapi" ? ConfigFileType.YAML : ConfigFileType.JSON,
);
if (
props.dataSources.length > 0 &&
(props.postInstallAction === "runApp" ||
props.postInstallAction === "dependencies")
) {
console.log("\nGenerating context data...\n");
await generateContextData(
props.framework,
props.modelConfig,
props.dataSources,
props.packageManager,
props.vectorDb,
props.llamaCloudKey,
props.useLlamaParse,
);
}
if (props.backend) {
// This is a backend, so we need to copy the test data and create the env file.
if (!isPythonLlamaDeploy) {
// Create outputs directory (llama-deploy doesn't need this)
// Copy the environment file to the target directory.
if (
props.template === "streaming" ||
props.template === "multiagent" ||
props.template === "extractor"
) {
await createBackendEnvFile(props.root, props);
}
await prepareContextData(
props.root,
props.dataSources.filter((ds) => ds.type === "file"),
);
if (
props.dataSources.length > 0 &&
(props.postInstallAction === "runApp" ||
props.postInstallAction === "dependencies")
) {
console.log("\nGenerating context data...\n");
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,
});
}
};
@@ -1,3 +1,4 @@
/* eslint-disable import/no-extraneous-dependencies */
import spawn from "cross-spawn";
import { yellow } from "picocolors";
import type { PackageManager } from "./get-pkg-manager";
@@ -1,3 +1,4 @@
/* eslint-disable import/no-extraneous-dependencies */
import fs from "fs";
import path from "path";
import { blue, green } from "picocolors";
+148
View File
@@ -0,0 +1,148 @@
import fs from "fs/promises";
import got from "got";
import path from "path";
import { parse } from "smol-toml";
import {
LLAMA_PACK_FOLDER,
LLAMA_PACK_FOLDER_PATH,
LLAMA_PACK_OWNER,
LLAMA_PACK_REPO,
} from "./constant";
import { copy } from "./copy";
import { templatesDir } from "./dir";
import { addDependencies, installPythonDependencies } from "./python";
import { getRepoRawContent } from "./repo";
import { InstallTemplateArgs } from "./types";
const getLlamaPackFolderSHA = async () => {
const url = `https://api.github.com/repos/${LLAMA_PACK_OWNER}/${LLAMA_PACK_REPO}/contents`;
const response = await got(url, {
responseType: "json",
});
const data = response.body as any[];
const llamaPackFolder = data.find((item) => item.name === LLAMA_PACK_FOLDER);
return llamaPackFolder.sha;
};
const getLLamaPackFolderTree = async (
sha: string,
): Promise<
Array<{
path: string;
}>
> => {
const url = `https://api.github.com/repos/${LLAMA_PACK_OWNER}/${LLAMA_PACK_REPO}/git/trees/${sha}?recursive=1`;
const response = await got(url, {
responseType: "json",
});
return (response.body as any).tree;
};
export async function getAvailableLlamapackOptions(): Promise<
{
name: string;
folderPath: string;
}[]
> {
const EXAMPLE_RELATIVE_PATH = "/examples/example.py";
const PACK_FOLDER_SUBFIX = "llama-index-packs";
const llamaPackFolderSHA = await getLlamaPackFolderSHA();
const llamaPackTree = await getLLamaPackFolderTree(llamaPackFolderSHA);
// Return options that have example files
const exampleFiles = llamaPackTree.filter((item) =>
item.path.endsWith(EXAMPLE_RELATIVE_PATH),
);
const options = exampleFiles.map((file) => {
const packFolder = file.path.substring(
0,
file.path.indexOf(EXAMPLE_RELATIVE_PATH),
);
const packName = packFolder.substring(PACK_FOLDER_SUBFIX.length + 1);
return {
name: packName,
folderPath: packFolder,
};
});
return options;
}
const copyLlamapackEmptyProject = async ({
root,
}: Pick<InstallTemplateArgs, "root">) => {
const templatePath = path.join(
templatesDir,
"components/sample-projects/llamapack",
);
await copy("**", root, {
parents: true,
cwd: templatePath,
});
};
const copyData = async ({
root,
}: Pick<InstallTemplateArgs, "root" | "llamapack">) => {
const dataPath = path.join(templatesDir, "components/data");
await copy("**", path.join(root, "data"), {
parents: true,
cwd: dataPath,
});
};
const installLlamapackExample = async ({
root,
llamapack,
}: Pick<InstallTemplateArgs, "root" | "llamapack">) => {
const exampleFileName = "example.py";
const readmeFileName = "README.md";
const projectTomlFileName = "pyproject.toml";
const exampleFilePath = `${LLAMA_PACK_FOLDER_PATH}/${llamapack}/examples/${exampleFileName}`;
const readmeFilePath = `${LLAMA_PACK_FOLDER_PATH}/${llamapack}/${readmeFileName}`;
const projectTomlFilePath = `${LLAMA_PACK_FOLDER_PATH}/${llamapack}/${projectTomlFileName}`;
// Download example.py from llamapack and save to root
const exampleContent = await getRepoRawContent(exampleFilePath);
await fs.writeFile(path.join(root, exampleFileName), exampleContent);
// Download README.md from llamapack and combine with README-template.md,
// save to root and then delete template file
const readmeContent = await getRepoRawContent(readmeFilePath);
const readmeTemplateContent = await fs.readFile(
path.join(root, "README-template.md"),
"utf-8",
);
await fs.writeFile(
path.join(root, readmeFileName),
`${readmeContent}\n${readmeTemplateContent}`,
);
await fs.unlink(path.join(root, "README-template.md"));
// Download pyproject.toml from llamapack, parse it to get package name and version,
// then add it as a dependency to current toml file in the project
const projectTomlContent = await getRepoRawContent(projectTomlFilePath);
const fileParsed = parse(projectTomlContent) as any;
const packageName = fileParsed.tool.poetry.name;
const packageVersion = fileParsed.tool.poetry.version;
await addDependencies(root, [
{
name: packageName,
version: packageVersion,
},
]);
};
export const installLlamapackProject = async ({
root,
llamapack,
postInstallAction,
}: Pick<InstallTemplateArgs, "root" | "llamapack" | "postInstallAction">) => {
console.log("\nInstalling Llamapack project:", llamapack!);
await copyLlamapackEmptyProject({ root });
await copyData({ root });
await installLlamapackExample({ root, llamapack });
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
installPythonDependencies({ noRoot: true });
}
};
+36
View File
@@ -0,0 +1,36 @@
/* eslint-disable import/no-extraneous-dependencies */
import { execSync } from "child_process";
import fs from "fs";
export function isPoetryAvailable(): boolean {
try {
execSync("poetry --version", { stdio: "ignore" });
return true;
} catch (_) {}
return false;
}
export function tryPoetryInstall(noRoot: boolean): boolean {
try {
execSync(`poetry install${noRoot ? " --no-root" : ""}`, {
stdio: "inherit",
});
return true;
} catch (_) {}
return false;
}
export function tryPoetryRun(command: string): boolean {
try {
execSync(`poetry run ${command}`, { stdio: "inherit" });
return true;
} catch (_) {}
return false;
}
export function isHavingPoetryLockFile(): boolean {
try {
return fs.existsSync("poetry.lock");
} catch (_) {}
return false;
}
@@ -31,9 +31,17 @@ const EMBEDDING_MODELS: Record<HuggingFaceEmbeddingModelType, ModelData> = {
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
export async function askAnthropicQuestions(): Promise<ModelConfigParams> {
type AnthropicQuestionsParams = {
apiKey?: string;
askModels: boolean;
};
export async function askAnthropicQuestions({
askModels,
apiKey,
}: AnthropicQuestionsParams): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey: process.env.ANTHROPIC_API_KEY,
apiKey,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: DEFAULT_DIMENSIONS,
@@ -61,33 +69,35 @@ export async function askAnthropicQuestions(): Promise<ModelConfigParams> {
config.apiKey = key || process.env.ANTHROPIC_API_KEY;
}
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;
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;
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;
}
@@ -1,5 +1,5 @@
import prompts from "prompts";
import { ModelConfigParams } from ".";
import { ModelConfigParams, ModelConfigQuestionsParams } from ".";
import { questionHandlers } from "../../questions/utils";
const ALL_AZURE_OPENAI_CHAT_MODELS: Record<string, { openAIModel: string }> = {
@@ -51,9 +51,12 @@ const ALL_AZURE_OPENAI_EMBEDDING_MODELS: Record<
const DEFAULT_MODEL = "gpt-4o";
const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large";
export async function askAzureQuestions(): Promise<ModelConfigParams> {
export async function askAzureQuestions({
openAiKey,
askModels,
}: ModelConfigQuestionsParams): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey: process.env.AZURE_OPENAI_KEY,
apiKey: openAiKey || process.env.AZURE_OPENAI_KEY,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: getDimensions(DEFAULT_EMBEDDING_MODEL),
@@ -63,30 +66,32 @@ export async function askAzureQuestions(): Promise<ModelConfigParams> {
},
};
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;
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);
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;
}
@@ -2,15 +2,7 @@ import prompts from "prompts";
import { ModelConfigParams } from ".";
import { questionHandlers, toChoice } from "../../questions/utils";
const MODELS = [
"gemini-2.5-pro",
"gemini-2.5-flash",
"gemini-2.0-flash",
"gemini-2.0-flash-lite",
"gemini-1.5-pro-latest",
"gemini-pro",
"gemini-pro-vision",
];
const MODELS = ["gemini-1.5-pro-latest", "gemini-pro", "gemini-pro-vision"];
type ModelData = {
dimensions: number;
};
@@ -23,9 +15,17 @@ const DEFAULT_MODEL = MODELS[0];
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
export async function askGeminiQuestions(): Promise<ModelConfigParams> {
type GeminiQuestionsParams = {
apiKey?: string;
askModels: boolean;
};
export async function askGeminiQuestions({
askModels,
apiKey,
}: GeminiQuestionsParams): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey: process.env.GOOGLE_API_KEY,
apiKey,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: DEFAULT_DIMENSIONS,
@@ -53,30 +53,32 @@ export async function askGeminiQuestions(): Promise<ModelConfigParams> {
config.apiKey = key || process.env.GOOGLE_API_KEY;
}
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;
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;
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;
}
@@ -71,9 +71,17 @@ const EMBEDDING_MODELS: Record<HuggingFaceEmbeddingModelType, ModelData> = {
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
export async function askGroqQuestions(): Promise<ModelConfigParams> {
type GroqQuestionsParams = {
apiKey?: string;
askModels: boolean;
};
export async function askGroqQuestions({
askModels,
apiKey,
}: GroqQuestionsParams): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey: process.env.GROQ_API_KEY,
apiKey,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: DEFAULT_DIMENSIONS,
@@ -101,35 +109,37 @@ export async function askGroqQuestions(): Promise<ModelConfigParams> {
config.apiKey = key || process.env.GROQ_API_KEY;
}
const modelChoices = await getAvailableModelChoicesGroq(config.apiKey!);
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 { 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;
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;
}
@@ -21,7 +21,13 @@ const DEFAULT_MODEL = MODELS[0];
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
export async function askHuggingfaceQuestions(): Promise<ModelConfigParams> {
type HuggingfaceQuestionsParams = {
askModels: boolean;
};
export async function askHuggingfaceQuestions({
askModels,
}: HuggingfaceQuestionsParams): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
@@ -31,30 +37,32 @@ export async function askHuggingfaceQuestions(): Promise<ModelConfigParams> {
},
};
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;
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;
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;
}
+94
View File
@@ -0,0 +1,94 @@
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 { askOllamaQuestions } from "./ollama";
import { askOpenAIQuestions } from "./openai";
const DEFAULT_MODEL_PROVIDER = "openai";
export type ModelConfigQuestionsParams = {
openAiKey?: string;
askModels: boolean;
framework?: TemplateFramework;
};
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" });
}
const { provider } = await prompts(
{
type: "select",
name: "provider",
message: "Which model provider would you like to use",
choices: choices,
initial: 0,
},
questionHandlers,
);
modelProvider = provider;
}
let modelConfig: ModelConfigParams;
switch (modelProvider) {
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,
askModels,
});
}
return {
...modelConfig,
provider: modelProvider,
};
}
@@ -31,9 +31,17 @@ const LLMHUB_EMBEDDING_MODELS = [
"text-embedding-bge-m3",
];
export async function askLLMHubQuestions(): Promise<ModelConfigParams> {
type LLMHubQuestionsParams = {
apiKey?: string;
askModels: boolean;
};
export async function askLLMHubQuestions({
askModels,
apiKey,
}: LLMHubQuestionsParams): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey: process.env.T_SYSTEMS_LLMHUB_API_KEY,
apiKey,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: getDimensions(DEFAULT_EMBEDDING_MODEL),
@@ -53,10 +61,11 @@ export async function askLLMHubQuestions(): Promise<ModelConfigParams> {
{
type: "text",
name: "key",
message:
"Please provide your LLMHub API key (or leave blank to use T_SYSTEMS_LLMHUB_API_KEY env variable):",
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 (!value) {
if (askModels && !value) {
if (process.env.T_SYSTEMS_LLMHUB_API_KEY) {
return true;
}
@@ -70,30 +79,32 @@ export async function askLLMHubQuestions(): Promise<ModelConfigParams> {
config.apiKey = key || process.env.T_SYSTEMS_LLMHUB_API_KEY;
}
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;
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);
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;
}
@@ -14,9 +14,17 @@ const DEFAULT_MODEL = MODELS[0];
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
export async function askMistralQuestions(): Promise<ModelConfigParams> {
type MistralQuestionsParams = {
apiKey?: string;
askModels: boolean;
};
export async function askMistralQuestions({
askModels,
apiKey,
}: MistralQuestionsParams): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey: process.env.MISTRAL_API_KEY,
apiKey,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: DEFAULT_DIMENSIONS,
@@ -44,30 +52,32 @@ export async function askMistralQuestions(): Promise<ModelConfigParams> {
config.apiKey = key || process.env.MISTRAL_API_KEY;
}
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;
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;
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;
}
@@ -17,7 +17,13 @@ const EMBEDDING_MODELS: Record<string, ModelData> = {
};
const DEFAULT_EMBEDDING_MODEL: string = Object.keys(EMBEDDING_MODELS)[0];
export async function askOllamaQuestions(): Promise<ModelConfigParams> {
type OllamaQuestionsParams = {
askModels: boolean;
};
export async function askOllamaQuestions({
askModels,
}: OllamaQuestionsParams): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
@@ -27,32 +33,34 @@ export async function askOllamaQuestions(): Promise<ModelConfigParams> {
},
};
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which LLM model would you like to use?",
choices: MODELS.map(toChoice),
initial: 0,
},
questionHandlers,
);
await ensureModel(model);
config.model = model;
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,
);
await ensureModel(model);
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,
);
await ensureModel(embeddingModel);
config.embeddingModel = embeddingModel;
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
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,
);
await ensureModel(embeddingModel);
config.embeddingModel = embeddingModel;
config.dimensions = EMBEDDING_MODELS[embeddingModel].dimensions;
}
return config;
}
@@ -2,7 +2,8 @@ import got from "got";
import ora from "ora";
import { red } from "picocolors";
import prompts from "prompts";
import { ModelConfigParams } from ".";
import { ModelConfigParams, ModelConfigQuestionsParams } from ".";
import { isCI } from "../../questions";
import { questionHandlers } from "../../questions/utils";
const OPENAI_API_URL = "https://api.openai.com/v1";
@@ -10,9 +11,12 @@ const OPENAI_API_URL = "https://api.openai.com/v1";
const DEFAULT_MODEL = "gpt-4o-mini";
const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large";
export async function askOpenAIQuestions(): Promise<ModelConfigParams> {
export async function askOpenAIQuestions({
openAiKey,
askModels,
}: ModelConfigQuestionsParams): Promise<ModelConfigParams> {
const config: ModelConfigParams = {
apiKey: process.env.OPENAI_API_KEY,
apiKey: openAiKey,
model: DEFAULT_MODEL,
embeddingModel: DEFAULT_EMBEDDING_MODEL,
dimensions: getDimensions(DEFAULT_EMBEDDING_MODEL),
@@ -27,15 +31,16 @@ export async function askOpenAIQuestions(): Promise<ModelConfigParams> {
},
};
if (!config.apiKey) {
if (!config.apiKey && !isCI) {
const { key } = await prompts(
{
type: "text",
name: "key",
message:
"Please provide your OpenAI API key (or leave blank to use OPENAI_API_KEY env variable):",
message: askModels
? "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) => {
if (!value) {
if (askModels && !value) {
if (process.env.OPENAI_API_KEY) {
return true;
}
@@ -49,30 +54,32 @@ export async function askOpenAIQuestions(): Promise<ModelConfigParams> {
config.apiKey = key || process.env.OPENAI_API_KEY;
}
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;
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);
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;
}
+556
View File
@@ -0,0 +1,556 @@
import fs from "fs/promises";
import path from "path";
import { cyan, red } from "picocolors";
import { parse, stringify } from "smol-toml";
import terminalLink from "terminal-link";
import { assetRelocator, copy } from "./copy";
import { templatesDir } from "./dir";
import { isPoetryAvailable, tryPoetryInstall } from "./poetry";
import { Tool } from "./tools";
import {
InstallTemplateArgs,
ModelConfig,
TemplateDataSource,
TemplateType,
TemplateVectorDB,
} from "./types";
interface Dependency {
name: string;
version?: string;
extras?: string[];
constraints?: Record<string, string>;
}
const getAdditionalDependencies = (
modelConfig: ModelConfig,
vectorDb?: TemplateVectorDB,
dataSources?: TemplateDataSource[],
tools?: Tool[],
templateType?: TemplateType,
) => {
const dependencies: Dependency[] = [];
// Add vector db dependencies
switch (vectorDb) {
case "mongo": {
dependencies.push({
name: "llama-index-vector-stores-mongodb",
version: "^0.3.1",
});
break;
}
case "pg": {
dependencies.push({
name: "llama-index-vector-stores-postgres",
version: "^0.2.5",
});
break;
}
case "pinecone": {
dependencies.push({
name: "llama-index-vector-stores-pinecone",
version: "^0.2.1",
constraints: {
python: ">=3.11,<3.13",
},
});
break;
}
case "milvus": {
dependencies.push({
name: "llama-index-vector-stores-milvus",
version: "^0.2.0",
});
dependencies.push({
name: "pymilvus",
version: "2.4.4",
});
break;
}
case "astra": {
dependencies.push({
name: "llama-index-vector-stores-astra-db",
version: "^0.2.0",
});
break;
}
case "qdrant": {
dependencies.push({
name: "llama-index-vector-stores-qdrant",
version: "^0.3.0",
constraints: {
python: ">=3.11,<3.13",
},
});
break;
}
case "chroma": {
dependencies.push({
name: "llama-index-vector-stores-chroma",
version: "^0.2.0",
});
break;
}
case "weaviate": {
dependencies.push({
name: "llama-index-vector-stores-weaviate",
version: "^1.1.1",
});
break;
}
case "llamacloud":
dependencies.push({
name: "llama-index-indices-managed-llama-cloud",
version: "^0.3.1",
});
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.2.2",
});
break;
case "db":
dependencies.push({
name: "llama-index-readers-database",
version: "^0.2.0",
});
dependencies.push({
name: "pymysql",
version: "^1.1.0",
extras: ["rsa"],
});
dependencies.push({
name: "psycopg2-binary",
version: "^2.9.9",
});
break;
}
}
}
// Add tools dependencies
console.log("Adding tools dependencies");
tools?.forEach((tool) => {
tool.dependencies?.forEach((dep) => {
dependencies.push(dep);
});
});
switch (modelConfig.provider) {
case "ollama":
dependencies.push({
name: "llama-index-llms-ollama",
version: "0.3.0",
});
dependencies.push({
name: "llama-index-embeddings-ollama",
version: "0.3.0",
});
break;
case "openai":
if (templateType !== "multiagent") {
dependencies.push({
name: "llama-index-llms-openai",
version: "^0.2.0",
});
dependencies.push({
name: "llama-index-embeddings-openai",
version: "^0.2.3",
});
dependencies.push({
name: "llama-index-agent-openai",
version: "^0.3.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",
});
break;
}
return dependencies;
};
const mergePoetryDependencies = (
dependencies: Dependency[],
existingDependencies: Record<string, Omit<Dependency, "name"> | string>,
) => {
for (const dependency of dependencies) {
let value = existingDependencies[dependency.name] ?? {};
// default string value is equal to attribute "version"
if (typeof value === "string") {
value = { version: value };
}
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,
});
}
};
export const addDependencies = async (
projectDir: string,
dependencies: Dependency[],
) => {
if (dependencies.length === 0) return;
const FILENAME = "pyproject.toml";
try {
// Parse toml file
const file = path.join(projectDir, FILENAME);
const fileContent = await fs.readFile(file, "utf8");
const fileParsed = parse(fileContent);
// Modify toml dependencies
const tool = fileParsed.tool as any;
const existingDependencies = tool.poetry.dependencies;
mergePoetryDependencies(dependencies, existingDependencies);
// Write toml file
const newFileContent = stringify(fileParsed);
await fs.writeFile(file, newFileContent);
const dependenciesString = dependencies.map((d) => d.name).join(", ");
console.log(`\nAdded ${dependenciesString} to ${cyan(FILENAME)}\n`);
} catch (error) {
console.log(
`Error while updating dependencies for Poetry project file ${FILENAME}\n`,
error,
);
}
};
export const installPythonDependencies = (
{ noRoot }: { noRoot: boolean } = { noRoot: false },
) => {
if (isPoetryAvailable()) {
console.log(
`Installing python dependencies using poetry. This may take a while...`,
);
const installSuccessful = tryPoetryInstall(noRoot);
if (!installSuccessful) {
console.error(
red(
"Installing dependencies using poetry failed. Please check error log above and try running create-llama again.",
),
);
process.exit(1);
}
} else {
console.error(
red(
`Poetry is not available in the current environment. Please check ${terminalLink(
"Poetry Installation",
`https://python-poetry.org/docs/#installation`,
)} to install poetry first, then run create-llama again.`,
),
);
process.exit(1);
}
};
export const installPythonTemplate = async ({
root,
template,
framework,
vectorDb,
dataSources,
tools,
postInstallAction,
observability,
modelConfig,
agents,
}: Pick<
InstallTemplateArgs,
| "root"
| "framework"
| "template"
| "vectorDb"
| "dataSources"
| "tools"
| "postInstallAction"
| "observability"
| "modelConfig"
| "agents"
>) => {
console.log("\nInitializing Python project with template:", template, "\n");
let templatePath;
if (template === "extractor") {
templatePath = path.join(templatesDir, "types", "extractor", framework);
} else {
templatePath = path.join(templatesDir, "types", "streaming", framework);
}
await copy("**", root, {
parents: true,
cwd: templatePath,
rename: assetRelocator,
});
const compPath = path.join(templatesDir, "components");
const enginePath = path.join(root, "app", "engine");
// Copy selected vector DB
await copy("**", enginePath, {
parents: true,
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 services
if (template == "streaming" || template == "multiagent") {
await copy("**", path.join(root, "app", "api", "services"), {
cwd: path.join(compPath, "services", "python"),
});
}
// 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";
}
}
// Copy engine code
await copy("**", enginePath, {
parents: true,
cwd: path.join(compPath, "engines", "python", engine),
});
// Copy agent code
if (template === "multiagent") {
if (agents) {
await copy("**", path.join(root), {
parents: true,
cwd: path.join(compPath, "agents", "python", agents),
rename: assetRelocator,
});
} else {
console.log(
red(
"There is no agent selected for multi-agent template. Please pick an agent to use via --agents flag.",
),
);
process.exit(1);
}
}
// Copy router code
await copyRouterCode(root, tools ?? []);
}
if (template === "multiagent") {
// Copy multi-agent code
await copy("**", path.join(root), {
parents: true,
cwd: path.join(compPath, "multiagent", "python"),
rename: assetRelocator,
});
}
console.log("Adding additional dependencies");
const addOnDependencies = getAdditionalDependencies(
modelConfig,
vectorDb,
dataSources,
tools,
template,
);
if (observability && observability !== "none") {
if (observability === "traceloop") {
addOnDependencies.push({
name: "traceloop-sdk",
version: "^0.15.11",
});
}
if (observability === "llamatrace") {
addOnDependencies.push({
name: "llama-index-callbacks-arize-phoenix",
version: "^0.2.1",
constraints: {
python: ">=3.11,<3.13",
},
});
}
const templateObservabilityPath = path.join(
templatesDir,
"components",
"observability",
"python",
observability,
);
await copy("**", path.join(root, "app"), {
cwd: templateObservabilityPath,
});
}
await addDependencies(root, addOnDependencies);
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
installPythonDependencies();
}
// Copy deployment files for python
await copy("**", root, {
cwd: path.join(compPath, "deployments", "python"),
});
};
+134
View File
@@ -0,0 +1,134 @@
import { createWriteStream, promises } from "fs";
import got from "got";
import { tmpdir } from "os";
import { join } from "path";
import { Stream } from "stream";
import tar from "tar";
import { promisify } from "util";
import { makeDir } from "./make-dir";
import { CommunityProjectConfig } from "./types";
export type RepoInfo = {
username: string;
name: string;
branch: string;
filePath: string;
};
const pipeline = promisify(Stream.pipeline);
async function downloadTar(url: string) {
const tempFile = join(tmpdir(), `next.js-cna-example.temp-${Date.now()}`);
await pipeline(got.stream(url), createWriteStream(tempFile));
return tempFile;
}
export async function downloadAndExtractRepo(
root: string,
{ username, name, branch, filePath }: RepoInfo,
) {
await makeDir(root);
const tempFile = await downloadTar(
`https://codeload.github.com/${username}/${name}/tar.gz/${branch}`,
);
await tar.x({
file: tempFile,
cwd: root,
strip: filePath ? filePath.split("/").length + 1 : 1,
filter: (p) =>
p.startsWith(
`${name}-${branch.replace(/\//g, "-")}${
filePath ? `/${filePath}/` : "/"
}`,
),
});
await promises.unlink(tempFile);
}
const getRepoInfo = async (owner: string, repo: string) => {
const repoInfoRes = await got(
`https://api.github.com/repos/${owner}/${repo}`,
{
responseType: "json",
},
);
const data = repoInfoRes.body as any;
return data;
};
export async function getProjectOptions(
owner: string,
repo: string,
): Promise<
{
value: CommunityProjectConfig;
title: string;
}[]
> {
// TODO: consider using octokit (https://github.com/octokit) if more changes are needed in the future
const getCommunityProjectConfig = async (
item: any,
): Promise<CommunityProjectConfig | null> => {
// if item is a folder, return the path with default owner, repo, and main branch
if (item.type === "dir")
return {
owner,
repo,
branch: "main",
filePath: item.path,
};
// check if it's a submodule (has size = 0 and different owner & repo)
if (item.type === "file") {
if (item.size !== 0) return null; // submodules have size = 0
// get owner and repo from git_url
const { git_url } = item;
const startIndex = git_url.indexOf("repos/") + 6;
const endIndex = git_url.indexOf("/git");
const ownerRepoStr = git_url.substring(startIndex, endIndex);
const [owner, repo] = ownerRepoStr.split("/");
// quick fetch repo info to get the default branch
const { default_branch } = await getRepoInfo(owner, repo);
// return the path with default owner, repo, and main branch (path is empty for submodules)
return {
owner,
repo,
branch: default_branch,
};
}
return null;
};
const url = `https://api.github.com/repos/${owner}/${repo}/contents`;
const response = await got(url, {
responseType: "json",
});
const data = response.body as any[];
const projectConfigs: CommunityProjectConfig[] = [];
for (const item of data) {
const communityProjectConfig = await getCommunityProjectConfig(item);
if (communityProjectConfig) projectConfigs.push(communityProjectConfig);
}
return projectConfigs.map((config) => {
return {
value: config,
title: config.filePath || config.repo, // for submodules, use repo name as title
};
});
}
export async function getRepoRawContent(repoFilePath: string) {
const url = `https://raw.githubusercontent.com/${repoFilePath}`;
const response = await got(url, {
responseType: "text",
});
return response.body;
}
+81
View File
@@ -0,0 +1,81 @@
import { SpawnOptions, spawn } from "child_process";
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,
})
.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);
});
});
};
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}` },
});
}
export async function runApp(
appPath: string,
template: string,
framework: TemplateFramework,
port?: number,
): Promise<void> {
try {
// Start the app
const defaultPort =
framework === "nextjs" || template === "extractor" ? 3000 : 8000;
const appRunner =
template === "extractor"
? runReflexApp
: framework === "fastapi"
? runFastAPIApp
: runTSApp;
await appRunner(appPath, port || defaultPort);
} catch (error) {
console.error("Failed to run app:", error);
throw error;
}
}
+361
View File
@@ -0,0 +1,361 @@
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 = {
name: string;
version?: string;
};
export const supportedTools: Tool[] = [
{
display: "Google Search",
name: "google.GoogleSearchToolSpec",
config: {
engine:
"Your search engine id, see https://developers.google.com/custom-search/v1/overview#prerequisites",
key: "Your search api key",
num: 2,
},
dependencies: [
{
name: "llama-index-tools-google",
version: "^0.2.0",
},
],
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", "nextjs", "express"],
type: ToolType.LOCAL,
envVars: [
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for DuckDuckGo search tool.",
value: `You are a DuckDuckGo search agent.
You can use the duckduckgo search tool 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",
name: "wikipedia.WikipediaToolSpec",
dependencies: [
{
name: "llama-index-tools-wikipedia",
version: "^0.2.0",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LLAMAHUB,
envVars: [
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for wiki tool.",
value: `You are a Wikipedia agent. You help users to get information from Wikipedia.`,
},
],
},
{
display: "Weather",
name: "weather",
dependencies: [],
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LOCAL,
envVars: [
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for weather tool.",
value: `You are a weather forecast agent. You help users to get the weather forecast for a given location.`,
},
],
},
{
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: "0.0.11b38",
},
],
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: "0.0.11b38",
},
],
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,
envVars: [
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for openapi action tool.",
value:
"You are an OpenAPI action agent. You help users to make requests to the provided OpenAPI schema.",
},
],
},
{
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",
},
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for image generator tool.",
value: `You are an image generator agent. You help users to generate images using the Stability API.`,
},
],
},
{
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",
},
],
},
];
export const getTool = (toolName: string): Tool | undefined => {
return supportedTools.find((tool) => tool.name === toolName);
};
export const getTools = (toolsName: string[]): Tool[] => {
const tools: Tool[] = [];
for (const toolName of toolsName) {
const tool = getTool(toolName);
if (!tool) {
console.log(
red(
`Error: Tool '${toolName}' is not supported. Supported tools are: ${supportedTools
.map((t) => t.name)
.join(", ")}`,
),
);
process.exit(1);
}
tools.push(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 false;
};
export enum ConfigFileType {
YAML = "yaml",
JSON = "json",
}
export const writeToolsConfig = async (
root: string,
tools: Tool[] = [],
type: ConfigFileType = ConfigFileType.YAML,
) => {
const configContent: {
[key in ToolType]: Record<string, any>;
} = {
local: {},
llamahub: {},
};
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 ?? {};
}
});
const configPath = path.join(root, "config");
await makeDir(configPath);
if (type === ConfigFileType.YAML) {
await fs.writeFile(
path.join(configPath, "tools.yaml"),
yaml.stringify(configContent),
);
} else {
await fs.writeFile(
path.join(configPath, "tools.json"),
JSON.stringify(configContent, null, 2),
);
}
};
@@ -1,4 +1,5 @@
import { PackageManager } from "../helpers/get-pkg-manager";
import { Tool } from "./tools";
export type ModelProvider =
| "openai"
@@ -18,8 +19,14 @@ export type ModelConfig = {
dimensions: number;
isConfigured(): boolean;
};
export type TemplateType = "llamaindexserver";
export type TemplateType =
| "extractor"
| "streaming"
| "community"
| "llamapack"
| "multiagent";
export type TemplateFramework = "nextjs" | "express" | "fastapi";
export type TemplateUI = "html" | "shadcn";
export type TemplateVectorDB =
| "none"
| "mongo"
@@ -41,23 +48,15 @@ export type TemplateDataSource = {
config: TemplateDataSourceConfig;
};
export type TemplateDataSourceType = "file" | "web" | "db";
export type TemplateUseCase =
| "financial_report"
| "deep_research"
| "agentic_rag"
| "code_generator"
| "document_generator"
| "hitl";
export type TemplateObservability = "none" | "traceloop" | "llamatrace";
export type TemplateAgents = "financial_report" | "blog" | "form_filling";
// Config for both file and folder
export type FileSourceConfig =
| {
path: string;
filename?: string;
}
| {
url: URL;
filename?: string;
};
export type WebSourceConfig = {
baseUrl?: string;
@@ -74,31 +73,31 @@ export type TemplateDataSourceConfig =
| WebSourceConfig
| DbSourceConfig;
export type CommunityProjectConfig = {
owner: string;
repo: string;
branch: string;
filePath?: string;
};
export interface InstallTemplateArgs {
appName: string;
root: string;
packageManager: PackageManager;
isOnline: boolean;
template: TemplateType;
framework: TemplateFramework;
ui: TemplateUI;
dataSources: TemplateDataSource[];
modelConfig: ModelConfig;
llamaCloudKey?: string;
useLlamaParse: boolean;
vectorDb: TemplateVectorDB;
useLlamaParse?: boolean;
communityProjectConfig?: CommunityProjectConfig;
llamapack?: string;
vectorDb?: TemplateVectorDB;
port?: number;
postInstallAction: TemplatePostInstallAction;
useCase: TemplateUseCase;
}
export type EnvVar = {
name?: string;
description?: string;
value?: string;
};
export interface Dependency {
name: string;
version?: string;
extras?: string[];
constraints?: Record<string, string>;
postInstallAction?: TemplatePostInstallAction;
tools?: Tool[];
observability?: TemplateObservability;
agents?: TemplateAgents;
}
+383
View File
@@ -0,0 +1,383 @@
import fs from "fs/promises";
import os from "os";
import path from "path";
import { bold, cyan, red, yellow } 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 } from "./types";
/**
* 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,
agents,
}: 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", "streaming", framework);
const copySource = ["**"];
await copy(copySource, root, {
parents: true,
cwd: templatePath,
rename: assetRelocator,
});
/**
* If next.js is used, update its configuration if necessary
*/
if (framework === "nextjs") {
const nextConfigJsonFile = path.join(root, "next.config.json");
const nextConfigJson: any = JSON.parse(
await fs.readFile(nextConfigJsonFile, "utf8"),
);
if (!backend) {
// update next.config.json for static site generation
nextConfigJson.output = "export";
nextConfigJson.images = { unoptimized: true };
console.log("\nUsing static site generation\n");
} else {
if (vectorDb === "milvus") {
nextConfigJson.serverExternalPackages =
nextConfigJson.serverExternalPackages ?? [];
nextConfigJson.serverExternalPackages.push("@zilliz/milvus2-sdk-node");
}
}
await fs.writeFile(
nextConfigJsonFile,
JSON.stringify(nextConfigJson, null, 2) + os.EOL,
);
const webpackConfigOtelFile = path.join(root, "webpack.config.o11y.mjs");
if (observability === "traceloop") {
const webpackConfigDefaultFile = path.join(root, "webpack.config.mjs");
await fs.rm(webpackConfigDefaultFile);
await fs.rename(webpackConfigOtelFile, webpackConfigDefaultFile);
} else {
await fs.rm(webpackConfigOtelFile);
}
}
// copy observability component
if (observability && observability !== "none") {
const chosenObservabilityPath = path.join(
templatesDir,
"components",
"observability",
"typescript",
observability,
);
const relativeObservabilityPath = framework === "nextjs" ? "app" : "src";
await copy(
"**",
path.join(root, relativeObservabilityPath, "observability"),
{ cwd: chosenObservabilityPath },
);
}
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");
}
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 agents use case code for multiagent template
if (agents) {
console.log("\nCopying agent:", agents, "\n");
const useCasePath = path.join(compPath, "agents", "typescript", agents);
const agentsCodePath = path.join(useCasePath, "workflow");
// Copy agent codes
await copy("**", path.join(root, relativeEngineDestPath, "workflow"), {
parents: true,
cwd: agentsCodePath,
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 agent selected for multi-agent template. Please pick an agent to use via --agents 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, {
parents: true,
cwd: path.join(compPath, "loaders", "typescript", loaderFolder),
});
// 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) {
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", "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.
*/
if (framework === "nextjs" && ui !== "shadcn") {
console.log("\nUsing UI:", ui, "\n");
const uiPath = path.join(compPath, "ui", ui);
const destUiPath = path.join(root, "app", "components", "ui");
// remove the default ui folder
await fs.rm(destUiPath, { recursive: true });
// copy the selected ui folder
await copy("**", destUiPath, {
parents: true,
cwd: uiPath,
rename: assetRelocator,
});
}
/** Modify frontend code to use custom API path */
if (framework === "nextjs" && !backend) {
console.log(
"\nUsing external API for frontend, removing API code and configuration\n",
);
// remove the default api folder and config folder
await fs.rm(path.join(root, "app", "api"), { recursive: true });
await fs.rm(path.join(root, "config"), { recursive: true, force: true });
}
const packageJson = await updatePackageJson({
root,
appName,
dataSources,
relativeEngineDestPath,
framework,
ui,
observability,
vectorDb,
});
if (
backend &&
(postInstallAction === "runApp" || postInstallAction === "dependencies")
) {
await installTSDependencies(packageJson, packageManager, isOnline);
}
// Copy deployment files for typescript
await copy("**", root, {
cwd: path.join(compPath, "deployments", "typescript"),
});
};
async function updatePackageJson({
root,
appName,
dataSources,
relativeEngineDestPath,
framework,
ui,
observability,
vectorDb,
}: Pick<
InstallTemplateArgs,
| "root"
| "appName"
| "dataSources"
| "framework"
| "ui"
| "observability"
| "vectorDb"
> & {
relativeEngineDestPath: string;
}): Promise<any> {
const packageJsonFile = path.join(root, "package.json");
const packageJson: any = JSON.parse(
await fs.readFile(packageJsonFile, "utf8"),
);
packageJson.name = appName;
packageJson.version = "0.1.0";
if (relativeEngineDestPath) {
// TODO: move script to {root}/scripts for all frameworks
// add generate script if using context engine
packageJson.scripts = {
...packageJson.scripts,
generate: `tsx ${path.join(
relativeEngineDestPath,
"engine",
"generate.ts",
)}`,
};
}
if (framework === "nextjs" && ui === "html") {
// remove shadcn dependencies if html ui is selected
packageJson.dependencies = {
...packageJson.dependencies,
"tailwind-merge": undefined,
"@radix-ui/react-slot": undefined,
"class-variance-authority": undefined,
clsx: undefined,
"lucide-react": undefined,
remark: undefined,
"remark-code-import": undefined,
"remark-gfm": undefined,
"remark-math": undefined,
"react-markdown": undefined,
"highlight.js": undefined,
};
}
if (vectorDb === "pg") {
packageJson.dependencies = {
...packageJson.dependencies,
pg: "^8.12.0",
pgvector: "^0.2.0",
};
}
if (vectorDb === "qdrant") {
packageJson.dependencies = {
...packageJson.dependencies,
"@qdrant/js-client-rest": "^1.11.0",
};
}
if (vectorDb === "mongo") {
packageJson.dependencies = {
...packageJson.dependencies,
mongodb: "^6.7.0",
};
}
if (vectorDb === "milvus") {
packageJson.dependencies = {
...packageJson.dependencies,
"@zilliz/milvus2-sdk-node": "^2.4.6",
};
}
if (observability === "traceloop") {
packageJson.dependencies = {
...packageJson.dependencies,
"@traceloop/node-server-sdk": "^0.5.19",
};
packageJson.devDependencies = {
...packageJson.devDependencies,
"node-loader": "^2.0.0",
};
}
await fs.writeFile(
packageJsonFile,
JSON.stringify(packageJson, null, 2) + os.EOL,
);
return packageJson;
}
async function installTSDependencies(
packageJson: any,
packageManager: PackageManager,
isOnline: boolean,
): Promise<void> {
console.log("\nInstalling dependencies:");
for (const dependency in packageJson.dependencies)
console.log(`- ${cyan(dependency)}`);
console.log("\nInstalling devDependencies:");
for (const dependency in packageJson.devDependencies)
console.log(`- ${cyan(dependency)}`);
console.log();
await callPackageManager(packageManager, isOnline).catch((error) => {
console.error("Failed to install TS dependencies. Exiting...");
process.exit(1);
});
}
@@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import validateProjectName from "validate-npm-package-name";
export function validateNpmName(name: string): {
+141 -7
View File
@@ -1,3 +1,4 @@
/* eslint-disable import/no-extraneous-dependencies */
import { execSync } from "child_process";
import { Command } from "commander";
import fs from "fs";
@@ -7,10 +8,12 @@ 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 { 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";
@@ -54,6 +57,13 @@ const program = new Command(packageJson.name)
`
Explicitly tell the CLI to bootstrap the application using Yarn
`,
)
.option(
"--template <template>",
`
Select a template to bootstrap the application with.
`,
)
.option(
@@ -61,6 +71,62 @@ const program = new Command(packageJson.name)
`
Select a framework to bootstrap the application with.
`,
)
.option(
"--files <path>",
`
Specify the path to a local file or folder for chatting.
`,
)
.option(
"--example-file",
`
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(
"--open-ai-key <key>",
`
Provide an OpenAI API key.
`,
)
.option(
"--ui <ui>",
`
Select a UI to bootstrap the application with.
`,
)
.option(
"--frontend",
`
Generate a frontend for your backend.
`,
)
.option(
"--no-frontend",
`
Do not generate a frontend for your backend.
`,
)
.option(
@@ -82,6 +148,27 @@ const program = new Command(packageJson.name)
`
Select which vector database you would like to use, such as 'none', 'pg' or 'mongo'. The default option is not to use a vector database and use the local filesystem instead ('none').
`,
)
.option(
"--tools <tools>",
`
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",
`
Enable LlamaParse.
`,
)
.option(
@@ -91,7 +178,13 @@ const program = new Command(packageJson.name)
Provide a LlamaCloud API key.
`,
)
.option(
"--observability <observability>",
`
Specify observability tools to use. Eg: none, opentelemetry
`,
)
.option(
"--ask-models",
`
@@ -101,10 +194,18 @@ const program = new Command(packageJson.name)
false,
)
.option(
"--use-case <useCase>",
"--pro",
`
Select which use case to use for the template (e.g: financial_report, blog).
Allow interactive selection of all features.
`,
false,
)
.option(
"--agents <agents>",
`
Select which agents to use for the multi-agent template (e.g: financial_report, blog).
`,
)
.allowUnknownOption()
@@ -112,6 +213,42 @@ const program = new Command(packageJson.name)
const options = program.opts();
if (
process.argv.includes("--no-llama-parse") ||
options.template === "extractor"
) {
options.useLlamaParse = false;
}
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",
},
},
];
}
const packageManager = !!options.useNpm
? "npm"
: !!options.usePnpm
@@ -120,9 +257,6 @@ const packageManager = !!options.useNpm
? "yarn"
: getPkgManager();
// options above must use all the properties of QuestionArgs
const cliArgs = options as unknown as QuestionArgs;
async function run(): Promise<void> {
if (typeof projectPath === "string") {
projectPath = projectPath.trim();
@@ -186,7 +320,7 @@ async function run(): Promise<void> {
process.exit(1);
}
const answers = await askQuestions(cliArgs);
const answers = await askQuestions(options as unknown as QuestionArgs);
await createApp({
...answers,
+63 -35
View File
@@ -1,55 +1,83 @@
{
"name": "create-llama-monorepo",
"version": "1.0.0",
"private": true,
"description": "Monorepo for create-llama",
"name": "create-llama",
"version": "0.3.15",
"description": "Create LlamaIndex-powered apps with one command",
"keywords": [
"rag",
"llamaindex"
"llamaindex",
"next.js"
],
"repository": {
"type": "git",
"url": "https://github.com/run-llama/create-llama"
"url": "https://github.com/run-llama/create-llama",
"directory": "packages/create-llama"
},
"license": "MIT",
"workspaces": [
"packages/*",
"python/*"
"bin": {
"create-llama": "./dist/index.js"
},
"files": [
"dist"
],
"scripts": {
"dev": "pnpm -r dev",
"build": "pnpm -r build",
"e2e": "pnpm -r e2e",
"lint": "eslint .",
"build": "bash ./scripts/build.sh",
"build:ncc": "pnpm run clean && ncc build ./index.ts -o ./dist/ --minify --no-cache --no-source-map-register",
"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",
"new-snapshot": "pnpm run build && changeset version --snapshot",
"new-version": "pnpm run build && changeset version",
"pack-install": "bash ./scripts/pack.sh",
"prepare": "husky",
"new-snapshot": "pnpm -r build && changeset version --snapshot",
"new-version-python": "pnpm --filter @create-llama/llama-index-server new-version",
"new-version": "pnpm -r build && changeset version && pnpm new-version-python",
"release-python": "pnpm --filter @create-llama/llama-index-server release",
"release": "pnpm -r build && changeset publish && pnpm release-python",
"release-snapshot": "pnpm -r build && changeset publish --tag snapshot"
"release": "pnpm run build && changeset publish",
"release-snapshot": "pnpm run build && changeset publish --tag snapshot"
},
"dependencies": {
"@types/async-retry": "1.4.2",
"@types/ci-info": "2.0.0",
"@types/cross-spawn": "6.0.0",
"@types/fs-extra": "11.0.4",
"@types/node": "^20.11.7",
"@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",
"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",
"smol-toml": "^1.1.4",
"tar": "6.1.15",
"terminal-link": "^3.0.0",
"update-check": "1.5.4",
"validate-npm-package-name": "3.0.0",
"yaml": "2.4.1"
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
"bunchee": "6.4.0",
"@playwright/test": "^1.41.1",
"@vercel/ncc": "0.38.1",
"eslint": "^8.56.0",
"eslint-config-prettier": "^8.10.0",
"husky": "^9.0.10",
"lint-staged": "^15.2.11",
"typescript-eslint": "^8.18.0",
"globals": "^15.12.0",
"eslint": "9.22.0",
"@eslint/js": "^9.25.0",
"eslint-config-next": "^15.1.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react": "7.37.2",
"prettier": "^3.4.2",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-tailwindcss": "^0.6.11",
"typescript": "^5.7.3",
"@types/node": "^22.9.0",
"@types/react": "^19",
"@types/react-dom": "^19"
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"rimraf": "^5.0.5",
"typescript": "^5.3.3",
"wait-port": "^1.1.0"
},
"packageManager": "pnpm@9.0.5",
"engines": {
-65
View File
@@ -1,65 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.pnp
.pnpm-store
.pnp.js
# testing
coverage
.coverage
# next.js
.next/
out/
build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# build
dist/
lib/
# e2e
.cache
test-results/
playwright-report/
blob-report/
playwright/.cache/
.tsbuildinfo
e2e/cache
# intellij
**/.idea
# Python
.mypy_cache/
venv/
.venv/
dist/
.__pycache__
__pycache__
.python-version
.ui
# build artifacts
create-llama-*.tgz
# copied from root
README.md
LICENSE.md
-108
View File
@@ -1,108 +0,0 @@
# create-llama Package
## Overview
The `create-llama` package is a CLI tool for creating LlamaIndex-powered applications with one command. It's designed as a project generator that scaffolds various types of RAG (Retrieval-Augmented Generation) applications using different frameworks, databases, and AI model providers.
## Package Structure
### Core Files
- **`index.ts`**: Main CLI entry point using Commander.js for argument parsing
- **`create-app.ts`**: Core application creation logic and orchestration
- **`package.json`**: Package configuration with binary entry point at `./dist/index.js`
### Key Directories
- **`helpers/`**: Utility functions for package management, file operations, and configuration
- **`questions/`**: Interactive prompts for user configuration
- **`templates/`**: Project templates for different frameworks and use cases
- **`e2e/`**: End-to-end tests using Playwright
## Core Functionality
### CLI Interface
The tool accepts numerous command-line options including:
- Framework selection (`--framework`: nextjs, express, fastapi)
- Template type (`--template`: streaming, multiagent, reflex, llamaindexserver)
- Model providers (OpenAI, Anthropic, Groq, Ollama, etc.)
- Vector databases (none, mongo, pg, pinecone, milvus, etc.)
- Data sources (files, web URLs, databases)
- Tools and observability options
### Application Generation Flow
1. **Project validation**: Checks project name validity and directory permissions
2. **Interactive questioning**: Prompts user for configuration if not provided via CLI
3. **Template installation**: Copies and configures appropriate templates
4. **Environment setup**: Creates `.env` files with API keys and configuration
5. **Dependencies**: Installs packages using detected/specified package manager
6. **Post-install actions**: Can run the app, open VSCode, or install dependencies
### Template System
Templates are organized by:
- **Framework**: NextJS (frontend), Express (Node backend), FastAPI (Python backend)
- **Type**: Streaming chat, multiagent workflows, Reflex UI, LlamaIndex server
- **Components**: Engines, loaders, providers, UI components, observability
### Helper Functions
Key helper modules include:
- **Installation**: Package manager detection and dependency installation
- **Data sources**: File copying, web scraping, database connection setup
- **Providers**: Model provider configuration (OpenAI, Anthropic, etc.)
- **Tools**: Integration with external tools (Wikipedia, weather, code generation)
- **Environment**: `.env` file generation with API keys and settings
## Development Commands
### Build & Development
- `npm run build`: Build the CLI using bash script
- `npm run dev`: Watch mode development build
- `npm run clean`: Clean build artifacts and temporary files
### Testing
- `npm run e2e`: Run all end-to-end tests
- `npm run e2e:python`: Test Python-specific templates
- `npm run e2e:typescript`: Test TypeScript-specific templates
### Package Management
- `npm run pack-install`: Create and install local package for testing
## Architecture Notes
### Model Configuration
The tool supports multiple AI providers with a unified `ModelConfig` interface that includes:
- Provider selection and API key management
- Model and embedding model specification
- Dimension configuration for embeddings
### Data Source Handling
Flexible data source configuration supporting:
- Local files and directories
- Web URLs with configurable crawling depth
- Database connections with custom queries
- Automatic file downloading and copying
### Template Flexibility
Templates use a component-based system allowing mix-and-match of:
- Different frameworks (NextJS, Express, FastAPI)
- Various vector databases
- Multiple observability tools
- Configurable tools and integrations
This package serves as the foundation for rapidly prototyping and deploying LlamaIndex applications across different technology stacks and use cases.
-107
View File
@@ -1,107 +0,0 @@
import path from "path";
import { green, yellow } from "picocolors";
import { tryGitInit } from "./helpers/git";
import { isFolderEmpty } from "./helpers/is-folder-empty";
import { isWriteable } from "./helpers/is-writeable";
import { makeDir } from "./helpers/make-dir";
import terminalLink from "terminal-link";
import type { InstallTemplateArgs } from "./helpers";
import { installTemplate } from "./helpers";
import { templatesDir } from "./helpers/dir";
import { configVSCode } from "./helpers/vscode";
export type InstallAppArgs = Omit<
InstallTemplateArgs,
"appName" | "root" | "port"
> & {
appPath: string;
};
export async function createApp({
template,
framework,
appPath,
packageManager,
modelConfig,
llamaCloudKey,
vectorDb,
postInstallAction,
dataSources,
useLlamaParse,
useCase,
}: InstallAppArgs): Promise<void> {
const root = path.resolve(appPath);
if (!(await isWriteable(path.dirname(root)))) {
console.error(
"The application path is not writable, please check folder permissions and try again.",
);
console.error(
"It is likely you do not have write permissions for this folder.",
);
process.exit(1);
}
const appName = path.basename(root);
await makeDir(root);
if (!isFolderEmpty(root, appName)) {
process.exit(1);
}
console.log(`Creating a new LlamaIndex app in ${green(root)}.`);
console.log();
const args = {
appName,
root,
template,
framework,
packageManager,
modelConfig,
llamaCloudKey,
vectorDb,
postInstallAction,
dataSources,
useLlamaParse,
useCase,
};
// Install backend
await installTemplate(args);
await configVSCode(root, templatesDir, framework);
process.chdir(root);
if (tryGitInit(root)) {
console.log("Initialized a git repository.");
console.log();
}
console.log("");
console.log(`${green("Success!")} Created ${appName} at ${appPath}`);
console.log(
`Now have a look at the ${terminalLink(
"README.md",
`file://${root}/README.md`,
)} and learn how to get started.`,
);
if (
dataSources.some((dataSource) => dataSource.type === "file") &&
process.platform === "linux"
) {
console.log(
yellow(
`You can add your own data files to ${terminalLink(
"data",
`file://${root}/data`,
)} folder manually.`,
),
);
}
console.log();
}
@@ -1,111 +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, TemplateUseCase, TemplateVectorDB } from "../../helpers";
import { ALL_PYTHON_USE_CASES } from "../../helpers/use-case";
import { RunCreateLlamaOptions, createTestDir, runCreateLlama } from "../utils";
const execAsync = util.promisify(exec);
const templateFramework: TemplateFramework = "fastapi";
const vectorDb: TemplateVectorDB = process.env.VECTORDB
? (process.env.VECTORDB as TemplateVectorDB)
: "none";
const useCases: TemplateUseCase[] = vectorDb === "llamacloud" ? [
"agentic_rag", "deep_research", "financial_report"
] : ALL_PYTHON_USE_CASES
test.describe("Mypy check", () => {
test.describe.configure({ retries: 0 });
test.describe("LlamaIndexServer", async () => {
for (const useCase of useCases) {
test(`should pass mypy for use case: ${useCase}`, async () => {
const cwd = await createTestDir();
await createAndCheckLlamaProject({
options: {
cwd,
templateFramework,
vectorDb,
port: 3000,
postInstallAction: "none",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
useCase,
},
});
});
}
});
});
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();
// Modify environment for the command
const commandEnv = {
...process.env,
};
console.log("Running uv venv...");
try {
const { stdout: venvStdout, stderr: venvStderr } = await execAsync(
"uv venv",
{ cwd: projectPath, env: commandEnv },
);
console.log("uv venv stdout:", venvStdout);
console.error("uv venv stderr:", venvStderr);
} catch (error) {
console.error("Error running uv venv:", error);
throw error; // Re-throw error to fail the test
}
console.log("Running uv sync...");
try {
const { stdout: syncStdout, stderr: syncStderr } = await execAsync(
"uv sync --all-extras",
{ cwd: projectPath, env: commandEnv },
);
console.log("uv sync stdout:", syncStdout);
console.error("uv sync stderr:", syncStderr);
} catch (error) {
console.error("Error running uv sync:", error);
throw error; // Re-throw error to fail the test
}
console.log("Running uv run mypy ....");
try {
const { stdout: mypyStdout, stderr: mypyStderr } = await execAsync(
"uv run mypy .",
{ cwd: projectPath, env: commandEnv },
);
console.log("uv run mypy stdout:", mypyStdout);
console.error("uv run mypy stderr:", mypyStderr);
// Assuming mypy success means no output or specific success message
// Adjust checks based on actual expected mypy output
} catch (error) {
console.error("Error running mypy:", error);
throw error;
}
// If we reach this point without throwing an error, the test passes
expect(true).toBeTruthy();
return { pyprojectPath, projectPath };
}
@@ -1,70 +0,0 @@
import { expect, test } from "@playwright/test";
import { ChildProcess, execSync } from "child_process";
import fs from "fs";
import path from "path";
import { type TemplateFramework, type TemplateVectorDB } from "../../helpers";
import { createTestDir, runCreateLlama } from "../utils";
const templateFramework: TemplateFramework = "nextjs";
const useCase = "code_generator";
const vectorDb: TemplateVectorDB = process.env.VECTORDB
? (process.env.VECTORDB as TemplateVectorDB)
: "none";
const llamaCloudProjectName = "create-llama";
const llamaCloudIndexName = "e2e-test";
const ejectDir = "next";
test.describe.skip(
`Test eject command for ${useCase} ${templateFramework} ${vectorDb}`,
async () => {
let port: number;
let cwd: string;
let name: string;
let appProcess: ChildProcess;
test.beforeAll(async () => {
port = Math.floor(Math.random() * 10000) + 10000;
cwd = await createTestDir();
const result = await runCreateLlama({
cwd,
templateFramework,
vectorDb,
port,
postInstallAction: "dependencies",
useCase,
llamaCloudProjectName,
llamaCloudIndexName,
});
name = result.projectName;
appProcess = result.appProcess;
});
test("Should successfully eject, install dependencies and build without errors", async ({
page,
}) => {
test.skip(
vectorDb === "llamacloud",
"Eject test only works with non-llamacloud",
);
// Run eject command
execSync("npm run eject", { cwd: path.join(cwd, name) });
// Verify next directory exists
const nextDirExists = fs.existsSync(path.join(cwd, name, ejectDir));
expect(nextDirExists).toBeTruthy();
// Install dependencies in next directory
execSync("npm install", { cwd: path.join(cwd, name, ejectDir) });
// Run build
execSync("npm run build", { cwd: path.join(cwd, name, ejectDir) });
});
// clean processes
test.afterAll(async () => {
appProcess?.kill();
});
},
);
@@ -1,90 +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,
TemplateUseCase,
TemplateVectorDB,
} from "../../helpers/types";
import { ALL_TYPESCRIPT_USE_CASES } from "../../helpers/use-case";
import { createTestDir, runCreateLlama } from "../utils";
const execAsync = util.promisify(exec);
const templateFramework: TemplateFramework = "nextjs";
const vectorDb: TemplateVectorDB = process.env.VECTORDB
? (process.env.VECTORDB as TemplateVectorDB)
: "none";
test.describe("Test resolve TS dependencies", () => {
test.describe.configure({ retries: 0 });
for (const useCase of ALL_TYPESCRIPT_USE_CASES) {
const optionDescription = `useCase: ${useCase}, vectorDb: ${vectorDb}`;
test.describe(`${optionDescription}`, () => {
test(`${optionDescription}`, async () => {
await runTest({
useCase: useCase,
vectorDb: vectorDb,
});
});
});
}
});
async function runTest(options: {
useCase: TemplateUseCase;
vectorDb: TemplateVectorDB;
}) {
const cwd = await createTestDir();
const result = await runCreateLlama({
cwd: cwd,
templateFramework: templateFramework,
vectorDb: options.vectorDb,
port: 3000,
postInstallAction: "none",
llamaCloudProjectName: undefined,
llamaCloudIndexName: undefined,
useCase: options.useCase,
});
const name = result.projectName;
// Check if the app folder exists
const appDir = path.join(cwd, name);
const dirExists = fs.existsSync(appDir);
expect(dirExists).toBeTruthy();
// Install dependencies using pnpm
try {
const { stderr: installStderr } = await execAsync(
"pnpm install --prefer-offline --ignore-workspace",
{
cwd: appDir,
},
);
} catch (error) {
console.error("Error installing dependencies:", error);
throw error;
}
// Run tsc type check and capture the output
try {
const { stdout, stderr } = await execAsync(
"pnpm exec tsc -b --diagnostics",
{
cwd: appDir,
},
);
// Check if there's any error output
expect(stderr).toBeFalsy();
// Log the stdout for debugging purposes
console.log("TypeScript type-check output:", stdout);
} catch (error) {
console.error("Error running tsc:", error);
throw error;
}
}
@@ -1,51 +0,0 @@
import path from "path";
import { templatesDir } from "./dir";
import { TemplateDataSource } from "./types";
export const EXAMPLE_FILE: TemplateDataSource = {
type: "file",
config: {
path: path.join(templatesDir, "components", "data", "101.pdf"),
},
};
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",
},
};
-12
View File
@@ -1,12 +0,0 @@
import { ModelConfig } from "./types";
export const getGpt41ModelConfig = (): ModelConfig => ({
provider: "openai",
apiKey: process.env.OPENAI_API_KEY,
model: "gpt-4.1",
embeddingModel: "text-embedding-3-large",
dimensions: 1536,
isConfigured(): boolean {
return !!process.env.OPENAI_API_KEY;
},
});
@@ -1,81 +0,0 @@
import prompts from "prompts";
import { questionHandlers } from "../../questions/utils";
import { ModelConfig, 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 { askOllamaQuestions } from "./ollama";
import { askOpenAIQuestions } from "./openai";
export type ModelConfigQuestionsParams = {
framework?: TemplateFramework;
};
export type ModelConfigParams = Omit<ModelConfig, "provider">;
export async function askModelConfig({
framework,
}: ModelConfigQuestionsParams): Promise<ModelConfig> {
const 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" });
}
const { provider: modelProvider } = await prompts(
{
type: "select",
name: "provider",
message: "Which model provider would you like to use",
choices: choices,
initial: 0,
},
questionHandlers,
);
let modelConfig: ModelConfigParams;
switch (modelProvider) {
case "ollama":
modelConfig = await askOllamaQuestions();
break;
case "groq":
modelConfig = await askGroqQuestions();
break;
case "anthropic":
modelConfig = await askAnthropicQuestions();
break;
case "gemini":
modelConfig = await askGeminiQuestions();
break;
case "mistral":
modelConfig = await askMistralQuestions();
break;
case "azure-openai":
modelConfig = await askAzureQuestions();
break;
case "t-systems":
modelConfig = await askLLMHubQuestions();
break;
case "huggingface":
modelConfig = await askHuggingfaceQuestions();
break;
default:
modelConfig = await askOpenAIQuestions();
}
return {
...modelConfig,
provider: modelProvider,
};
}
-552
View File
@@ -1,552 +0,0 @@
import fs from "fs/promises";
import path from "path";
import { cyan, red } from "picocolors";
import { parse, stringify } from "smol-toml";
import terminalLink from "terminal-link";
import { isUvAvailable, tryUvSync } from "./uv";
import { assetRelocator, copy } from "./copy";
import { templatesDir } from "./dir";
import { Dependency, InstallTemplateArgs } from "./types";
import { USE_CASE_CONFIGS } from "./use-case";
const getAdditionalDependencies = (
opts: Pick<
InstallTemplateArgs,
| "framework"
| "template"
| "useCase"
| "modelConfig"
| "vectorDb"
| "dataSources"
>,
) => {
const { framework, template, useCase, modelConfig, vectorDb, dataSources } =
opts;
const dependencies: Dependency[] = [];
const isPythonLlamaDeploy =
framework === "fastapi" && template === "llamaindexserver";
const useCaseDependencies =
USE_CASE_CONFIGS[useCase]?.additionalDependencies ?? [];
if (isPythonLlamaDeploy && useCaseDependencies.length > 0) {
dependencies.push(...useCaseDependencies);
}
// Add vector db dependencies
switch (vectorDb) {
case "mongo": {
dependencies.push({
name: "llama-index-vector-stores-mongodb",
version: ">=0.3.2,<0.4.0",
});
break;
}
case "pg": {
dependencies.push({
name: "llama-index-vector-stores-postgres",
version: ">=0.3.2,<0.4.0",
});
break;
}
case "pinecone": {
dependencies.push({
name: "llama-index-vector-stores-pinecone",
version: ">=0.4.1,<0.5.0",
constraints: {
python: ">=3.11,<3.13",
},
});
break;
}
case "milvus": {
dependencies.push({
name: "llama-index-vector-stores-milvus",
version: ">=0.3.0,<0.4.0",
});
dependencies.push({
name: "pymilvus",
version: ">=2.4.4,<3.0.0",
});
break;
}
case "astra": {
dependencies.push({
name: "llama-index-vector-stores-astra-db",
version: ">=0.4.0,<0.5.0",
});
break;
}
case "qdrant": {
dependencies.push({
name: "llama-index-vector-stores-qdrant",
version: ">=0.4.0,<0.5.0",
constraints: {
python: ">=3.11,<3.13",
},
});
break;
}
case "chroma": {
dependencies.push({
name: "llama-index-vector-stores-chroma",
version: ">=0.4.0,<0.5.0",
});
dependencies.push({
name: "onnxruntime",
version: "<1.22.0",
});
break;
}
case "weaviate": {
dependencies.push({
name: "llama-index-vector-stores-weaviate",
version: ">=1.2.3,<2.0.0",
});
break;
}
case "llamacloud":
dependencies.push({
name: "llama-index-indices-managed-llama-cloud",
version: ">=0.6.3,<0.7.0",
});
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,<0.9",
});
break;
case "web":
dependencies.push({
name: "llama-index-readers-web",
version: ">=0.3.0,<0.4.0",
});
break;
case "db":
dependencies.push({
name: "llama-index-readers-database",
version: ">=0.3.0,<0.4.0",
});
dependencies.push({
name: "pymysql",
version: ">=1.1.0,<2.0.0",
extras: ["rsa"],
});
dependencies.push({
name: "psycopg2-binary",
version: ">=2.9.9,<3.0.0",
});
break;
}
}
}
switch (modelConfig.provider) {
case "ollama":
dependencies.push({
name: "llama-index-llms-ollama",
version: ">=0.5.0,<0.6.0",
});
dependencies.push({
name: "llama-index-embeddings-ollama",
version: ">=0.6.0,<0.7.0",
});
break;
case "openai":
dependencies.push({
name: "llama-index-llms-openai",
version: ">=0.3.2,<0.4.0",
});
dependencies.push({
name: "llama-index-embeddings-openai",
version: ">=0.3.1,<0.4.0",
});
break;
case "groq":
dependencies.push({
name: "llama-index-llms-groq",
version: ">=0.3.0,<0.4.0",
});
dependencies.push({
name: "llama-index-embeddings-fastembed",
version: ">=0.3.0,<0.4.0",
});
break;
case "anthropic":
dependencies.push({
name: "llama-index-llms-anthropic",
version: ">=0.6.0,<0.7.0",
});
dependencies.push({
name: "llama-index-embeddings-fastembed",
version: ">=0.3.0,<0.4.0",
});
break;
case "gemini":
dependencies.push({
name: "llama-index-llms-google-genai",
version: ">=0.2.0,<0.3.0",
});
dependencies.push({
name: "llama-index-embeddings-google-genai",
version: ">=0.2.0,<0.3.0",
});
break;
case "mistral":
dependencies.push({
name: "llama-index-llms-mistralai",
version: ">=0.4.0,<0.5.0",
});
dependencies.push({
name: "llama-index-embeddings-mistralai",
version: ">=0.3.0,<0.4.0",
});
break;
case "azure-openai":
dependencies.push({
name: "llama-index-llms-azure-openai",
version: ">=0.3.0,<0.4.0",
});
dependencies.push({
name: "llama-index-embeddings-azure-openai",
version: ">=0.3.0,<0.4.0",
});
break;
case "huggingface":
dependencies.push({
name: "llama-index-llms-huggingface",
version: ">=0.5.0,<0.6.0",
});
dependencies.push({
name: "llama-index-embeddings-huggingface",
version: ">=0.5.0,<0.6.0",
});
dependencies.push({
name: "optimum",
version: ">=1.23.3,<2.0.0",
extras: ["onnxruntime"],
});
break;
case "t-systems":
dependencies.push({
name: "llama-index-agent-openai",
version: ">=0.4.0,<0.5.0",
});
dependencies.push({
name: "llama-index-llms-openai-like",
version: ">=0.3.0,<0.4.0",
});
break;
}
// If app template is llama-index-server and CI and SERVER_PACKAGE_PATH is set,
// add @llamaindex/server to dependencies
if (process.env.SERVER_PACKAGE_PATH) {
dependencies.push({
name: "llama-index-server",
version: `@file://${process.env.SERVER_PACKAGE_PATH}`,
});
}
return dependencies;
};
export const addDependencies = async (
projectDir: string,
dependencies: Dependency[],
) => {
if (dependencies.length === 0) return;
const FILENAME = "pyproject.toml";
try {
// Parse toml file
const file = path.join(projectDir, FILENAME);
const fileContent = await fs.readFile(file, "utf8");
let fileParsed: any;
try {
fileParsed = parse(fileContent);
} catch (parseError) {
console.error(`Error parsing ${FILENAME}:`, parseError);
throw new Error(
`Failed to parse ${FILENAME}. Please ensure it's valid TOML.`,
);
}
// Ensure [project] and [project.dependencies] exist
if (!fileParsed.project) {
fileParsed.project = {};
}
if (
!fileParsed.project.dependencies ||
!Array.isArray(fileParsed.project.dependencies)
) {
// If dependencies exist but aren't an array, log a warning or error.
// For now, we'll overwrite it, assuming the intent is to use the standard array format.
console.warn(
`[project.dependencies] in ${FILENAME} is not an array. It will be overwritten.`,
);
fileParsed.project.dependencies = [];
}
const existingDependencies: string[] = fileParsed.project.dependencies;
const addedDeps: string[] = [];
const updatedDeps: string[] = [];
// Add or update dependencies
for (const newDep of dependencies) {
let depString = newDep.name;
if (newDep.extras && newDep.extras.length > 0) {
depString += `[${newDep.extras.join(",")}]`;
}
if (newDep.version) {
depString += newDep.version;
}
let found = false;
for (let i = 0; i < existingDependencies.length; i++) {
const existingDepNameMatch =
existingDependencies[i].match(/^([a-zA-Z0-9._-]+)/);
if (
existingDepNameMatch &&
existingDepNameMatch[1].toLowerCase() === depString.toLowerCase()
) {
// Found existing dependency, update it
if (existingDependencies[i] !== depString) {
updatedDeps.push(`${existingDependencies[i]} -> ${depString}`);
existingDependencies[i] = depString;
}
found = true;
break;
}
}
if (!found) {
// Add new dependency
existingDependencies.push(depString);
addedDeps.push(depString);
}
// Handle python version constraints separately (if any)
if (newDep.constraints?.python) {
if (
!fileParsed.project["requires-python"] ||
fileParsed.project["requires-python"] !== newDep.constraints.python
) {
// This simple overwrite might not be ideal; merging constraints is complex.
// For now, let's just set it if the new dependency has one.
console.log(
`Setting requires-python = "${newDep.constraints.python}" from dependency ${newDep.name}`,
);
fileParsed.project["requires-python"] = newDep.constraints.python;
}
}
}
// Write toml file
const newFileContent = stringify(fileParsed);
await fs.writeFile(file, newFileContent);
if (addedDeps.length > 0) {
console.log(`\nAdded dependencies to ${cyan(FILENAME)}:`);
addedDeps.forEach((dep) => console.log(` ${dep}`));
}
if (updatedDeps.length > 0) {
console.log(`\nUpdated dependencies in ${cyan(FILENAME)}:`);
updatedDeps.forEach((dep) => console.log(` ${dep}`));
}
if (addedDeps.length > 0 || updatedDeps.length > 0) {
console.log(""); // Newline for spacing
}
} catch (error) {
console.log(
`Error while updating dependencies for Poetry project file ${FILENAME}\n`,
error,
);
}
};
export const installPythonDependencies = () => {
if (isUvAvailable()) {
console.log(
`Installing Python dependencies using uv. This may take a while...`,
);
const installSuccessful = tryUvSync();
if (!installSuccessful) {
console.error(
red(
"Installing dependencies using uv failed. Please check the error log above and ensure uv is installed correctly.",
),
);
process.exit(1);
}
} else {
console.error(
red(
`uv is not available in the current environment. Please check ${terminalLink(
"uv Installation",
`https://github.com/astral-sh/uv#installation`,
)} to install uv first, then run create-llama again.`,
),
);
process.exit(1);
}
};
const installLlamaIndexServerTemplate = async ({
root,
useCase,
useLlamaParse,
modelConfig,
}: Pick<
InstallTemplateArgs,
"root" | "useCase" | "useLlamaParse" | "modelConfig"
>) => {
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);
}
const srcDir = path.join(root, "src");
const uiDir = path.join(root, "ui");
// copy workflow code to src folder
await copy("*.py", srcDir, {
parents: true,
cwd: path.join(templatesDir, "components", "use-cases", "python", useCase),
});
// copy model provider settings to src folder
await copy("**", srcDir, {
cwd: path.join(
templatesDir,
"components",
"providers",
"python",
modelConfig.provider,
),
});
// copy ts server to ui folder
const tsProxyDir = path.join(templatesDir, "components", "ts-proxy");
await copy("package.json", uiDir, {
parents: true,
cwd: tsProxyDir,
});
const serverFileLocation = useLlamaParse
? path.join(tsProxyDir, "llamacloud")
: path.join(tsProxyDir);
await copy("index.ts", uiDir, {
parents: true,
cwd: serverFileLocation,
});
// Copy custom UI components to ui/components folder
await copy(`*`, path.join(uiDir, "components"), {
parents: true,
cwd: path.join(templatesDir, "components", "ui", "use-cases", useCase),
});
// Copy layout components to ui/layout folder
await copy("*", path.join(uiDir, "layout"), {
parents: true,
cwd: path.join(templatesDir, "components", "ui", "layout"),
});
if (useLlamaParse) {
await copy("**", srcDir, {
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", "use-cases", "python", useCase),
rename: assetRelocator,
});
// Clean up, remove generate.py and index.py for non-data use cases
if (["code_generator", "document_generator", "hitl"].includes(useCase)) {
await fs.unlink(path.join(srcDir, "generate.py"));
await fs.unlink(path.join(srcDir, "index.py"));
}
};
export const installPythonTemplate = async ({
appName,
root,
template,
framework,
vectorDb,
postInstallAction,
modelConfig,
dataSources,
useLlamaParse,
useCase,
}: Pick<
InstallTemplateArgs,
| "appName"
| "root"
| "template"
| "framework"
| "vectorDb"
| "postInstallAction"
| "modelConfig"
| "dataSources"
| "useLlamaParse"
| "useCase"
>) => {
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,
});
if (template === "llamaindexserver") {
await installLlamaIndexServerTemplate({
root,
useCase,
useLlamaParse,
modelConfig,
});
} else {
throw new Error(`Template ${template} not supported`);
}
console.log("Adding additional dependencies");
const addOnDependencies = getAdditionalDependencies({
framework,
template,
useCase,
modelConfig,
vectorDb,
dataSources,
});
await addDependencies(root, addOnDependencies);
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
installPythonDependencies();
}
};
-124
View File
@@ -1,124 +0,0 @@
import { SpawnOptions, exec, spawn } from "child_process";
import waitPort from "wait-port";
import { TemplateFramework, TemplateType } from "./types";
const createProcess = (
command: string,
args: string[],
options: SpawnOptions,
): Promise<void> => {
return new Promise((resolve, reject) => {
spawn(command, args, {
...options,
shell: true,
})
.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);
});
});
};
export function runFastAPIApp(
appPath: string,
port: number,
template: TemplateType,
) {
const commandArgs = ["run", "fastapi", "dev", "--port", `${port}`];
return createProcess("uv", commandArgs, {
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}` },
});
}
// TODO: support run multiple LlamaDeploy server in the same machine
async function runPythonLlamaDeployServer(
appPath: string,
port: number = 4501,
) {
console.log("Starting llama_deploy server...", port);
const serverProcess = exec("uv run -m llama_deploy.apiserver", {
cwd: appPath,
env: {
...process.env,
LLAMA_DEPLOY_APISERVER_PORT: `${port}`,
},
});
// Pipe output to console
serverProcess.stdout?.pipe(process.stdout);
serverProcess.stderr?.pipe(process.stderr);
// Wait for the server to be ready
console.log("Waiting for server to be ready...");
await waitPort({ port, host: "localhost", timeout: 30000 });
// create the deployment with explicit host configuration
console.log("llama_deploy server started, creating deployment...", port);
await createProcess(
"uv",
[
"run",
"llamactl",
"-s",
`http://localhost:${port}`,
"deploy",
"llama_deploy.yml",
],
{
stdio: "inherit",
cwd: appPath,
shell: true,
},
);
console.log(`Deployment created successfully!`);
// Keep the main process alive and handle cleanup
return new Promise(() => {
process.on("SIGINT", () => {
console.log("\nShutting down...");
serverProcess.kill();
process.exit(0);
});
});
}
export async function runApp(
appPath: string,
template: TemplateType,
framework: TemplateFramework,
port?: number,
): Promise<void> {
try {
// Start the app
const defaultPort = framework === "nextjs" ? 3000 : 8000;
if (template === "llamaindexserver" && framework === "fastapi") {
await runPythonLlamaDeployServer(appPath, port);
return;
}
const appRunner = framework === "fastapi" ? runFastAPIApp : runTSApp;
await appRunner(appPath, port || defaultPort, template);
} catch (error) {
console.error("Failed to run app:", error);
throw error;
}
}
-298
View File
@@ -1,298 +0,0 @@
import fs from "fs/promises";
import os from "os";
import path from "path";
import { bold, cyan, red } 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";
const installLlamaIndexServerTemplate = async ({
root,
useCase,
vectorDb,
modelConfig,
dataSources,
}: Pick<
InstallTemplateArgs,
"root" | "useCase" | "vectorDb" | "modelConfig" | "dataSources"
>) => {
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);
}
// copy model provider settings to app folder
await copy("**", path.join(root, "src", "app"), {
cwd: path.join(
templatesDir,
"components",
"providers",
"typescript",
modelConfig.provider,
),
});
await copy("**", path.join(root), {
cwd: path.join(
templatesDir,
"components",
"use-cases",
"typescript",
useCase,
),
rename: assetRelocator,
});
// copy workflow UI components to components folder in root
await copy("*", path.join(root, "components"), {
parents: true,
cwd: path.join(templatesDir, "components", "ui", "use-cases", useCase),
});
// copy layout components to layout folder in root
await copy("*", path.join(root, "layout"), {
parents: true,
cwd: path.join(templatesDir, "components", "ui", "layout"),
});
// Override generate.ts if workflow use case doesn't use custom UI
if (vectorDb === "llamacloud") {
await copy("**", path.join(root, "src"), {
parents: true,
cwd: path.join(
templatesDir,
"components",
"vectordbs",
"llamaindexserver",
"llamacloud",
"typescript",
),
});
}
// Simplify use case code
if (vectorDb === "none" && dataSources.length === 0) {
// use case without data sources doesn't use index.
// We don't need data.ts, generate.ts
await fs.rm(path.join(root, "src", "app", "data.ts"));
// TODO: split generate.ts into generate for index and generate for ui and remove generate for index here too
// then we can also remove it for llamacloud
}
};
/**
* Install a LlamaIndex internal template to a given `root` directory.
*/
export const installTSTemplate = async ({
appName,
root,
packageManager,
template,
framework,
vectorDb,
postInstallAction,
dataSources,
useCase,
modelConfig,
}: InstallTemplateArgs) => {
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 (template === "llamaindexserver") {
await installLlamaIndexServerTemplate({
root,
useCase,
vectorDb,
modelConfig,
dataSources,
});
if (vectorDb === "llamacloud") {
// replace index.ts with llamacloud/index.ts
await fs.rm(path.join(root, "src", "index.ts"));
await copy("index.ts", path.join(root, "src"), {
parents: true,
cwd: path.join(root, "src", "llamacloud"),
});
}
// remove llamacloud folder
await fs.rm(path.join(root, "src", "llamacloud"), { recursive: true });
} else {
throw new Error(`Template ${template} not supported`);
}
const packageJson = await updatePackageJson({
root,
appName,
vectorDb,
modelConfig,
});
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
await installTSDependencies(packageJson, packageManager, true);
}
};
const providerDependencies: {
[key in ModelProvider]?: Record<string, string>;
} = {
openai: {
"@llamaindex/openai": "~0.4.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",
},
};
async function updatePackageJson({
root,
appName,
vectorDb,
modelConfig,
}: Pick<
InstallTemplateArgs,
"root" | "appName" | "vectorDb" | "modelConfig"
>): Promise<any> {
const packageJsonFile = path.join(root, "package.json");
const packageJson: any = JSON.parse(
await fs.readFile(packageJsonFile, "utf8"),
);
packageJson.name = appName;
packageJson.version = "0.1.0";
packageJson.dependencies = {
...packageJson.dependencies,
"@llamaindex/readers": "~3.1.4",
};
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 having custom server package tgz file, use it for testing @llamaindex/server
const serverPackagePath = process.env.SERVER_PACKAGE_PATH;
if (serverPackagePath) {
const relativePath = path.relative(process.cwd(), serverPackagePath);
packageJson.dependencies = {
...packageJson.dependencies,
"@llamaindex/server": `file:${relativePath}`,
};
}
await fs.writeFile(
packageJsonFile,
JSON.stringify(packageJson, null, 2) + os.EOL,
);
return packageJson;
}
async function installTSDependencies(
packageJson: any,
packageManager: PackageManager,
isOnline: boolean,
): Promise<void> {
console.log("\nInstalling dependencies:");
for (const dependency in packageJson.dependencies)
console.log(`- ${cyan(dependency)}`);
console.log("\nInstalling devDependencies:");
for (const dependency in packageJson.devDependencies)
console.log(`- ${cyan(dependency)}`);
console.log();
await callPackageManager(packageManager, isOnline).catch((error) => {
console.error("Failed to install TS dependencies. Exiting...");
process.exit(1);
});
}
-84
View File
@@ -1,84 +0,0 @@
import { Dependency, EnvVar, TemplateUseCase } from "./types";
export const ALL_TYPESCRIPT_USE_CASES: TemplateUseCase[] = [
"agentic_rag",
"deep_research",
"financial_report",
"code_generator",
"document_generator",
"hitl",
];
export const ALL_PYTHON_USE_CASES: TemplateUseCase[] = [
"agentic_rag",
"deep_research",
"financial_report",
"code_generator",
"document_generator",
];
export const USE_CASE_CONFIGS: Record<
TemplateUseCase,
{
starterQuestions: string[];
additionalEnvVars?: EnvVar[];
additionalDependencies?: Dependency[];
}
> = {
agentic_rag: {
starterQuestions: [
"Letter standard in the document",
"Summarize the document",
],
},
financial_report: {
starterQuestions: [
"Compare Apple and Tesla financial performance",
"Generate a PDF report for Tesla financial",
],
additionalEnvVars: [
{
name: "E2B_API_KEY",
description: "The E2B API key to use to use code interpreter tool",
},
],
additionalDependencies: [
{
name: "e2b-code-interpreter",
version: ">=1.1.1,<2.0.0",
},
{
name: "markdown",
version: ">=3.7,<4.0",
},
{
name: "xhtml2pdf",
version: ">=0.2.17,<1.0.0",
},
],
},
deep_research: {
starterQuestions: [
"Research about Apple and Tesla",
"Financial performance of Tesla",
],
},
code_generator: {
starterQuestions: [
"Generate a code for a simple calculator",
"Generate a code for a todo list app",
],
},
document_generator: {
starterQuestions: [
"Generate a document about LlamaIndex",
"Generate a document about LLM",
],
},
hitl: {
starterQuestions: [
"List all the files in the current directory",
"Check git status",
],
},
};
-42
View File
@@ -1,42 +0,0 @@
// Migrate poetry to uv
import { execSync } from "child_process";
import fs from "fs";
import { red } from "picocolors";
export function isUvAvailable(): boolean {
try {
execSync("uv --version", { stdio: "ignore" });
return true;
} catch (_) {}
return false;
}
export function tryUvSync(): boolean {
try {
console.log("Syncing environment with pyproject.toml...");
execSync(`uv sync`, {
stdio: "inherit",
});
return true;
} catch (_) {}
return false;
}
export function tryUvRun(command: string): boolean {
try {
// Use uv run <command>
execSync(`uv run ${command}`, { stdio: "inherit" });
return true;
} catch (error) {
console.error(red(`Failed to run ${command}. Error: ${error}`));
return false;
}
}
export function isHavingUvLockFile(): boolean {
try {
// Check if uv.lock exists in the current directory
return fs.existsSync("uv.lock");
} catch (_) {}
return false;
}
-75
View File
@@ -1,75 +0,0 @@
{
"name": "create-llama",
"version": "0.6.3",
"description": "Create LlamaIndex-powered apps with one command",
"keywords": [
"rag",
"llamaindex",
"next.js"
],
"repository": {
"type": "git",
"url": "https://github.com/run-llama/create-llama",
"directory": "packages/create-llama"
},
"license": "MIT",
"bin": {
"create-llama": "./dist/index.js"
},
"files": [
"dist",
"README.md",
"LICENSE.md"
],
"scripts": {
"copy": "cp -r ../../README.md ../../LICENSE.md .",
"build": "bash ./scripts/build.sh",
"build:ncc": "pnpm run clean && ncc build ./index.ts -o ./dist/ --minify --no-cache --no-source-map-register",
"postbuild": "pnpm run copy",
"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:ts": "playwright test e2e/shared e2e/typescript",
"pack-install": "bash ./scripts/pack.sh"
},
"dependencies": {
"@types/async-retry": "1.4.2",
"@types/ci-info": "2.0.0",
"@types/cross-spawn": "6.0.0",
"@types/fs-extra": "11.0.4",
"@types/node": "^20.11.7",
"@types/prompts": "2.4.2",
"@types/tar": "6.1.5",
"@types/validate-npm-package-name": "3.0.0",
"async-retry": "1.3.1",
"async-sema": "3.0.1",
"commander": "12.1.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",
"smol-toml": "^1.1.4",
"tar": "6.1.15",
"terminal-link": "^3.0.0",
"update-check": "1.5.4",
"validate-npm-package-name": "3.0.0",
"yaml": "2.4.1"
},
"devDependencies": {
"@playwright/test": "^1.41.1",
"@vercel/ncc": "0.38.1",
"rimraf": "^5.0.5",
"typescript": "^5.3.3",
"wait-port": "^1.1.0"
},
"packageManager": "pnpm@9.0.5",
"engines": {
"node": ">=16.14.0"
}
}
-162
View File
@@ -1,162 +0,0 @@
import prompts from "prompts";
import { askModelConfig } from "../helpers/providers";
import {
TemplateFramework,
TemplateUseCase,
TemplateVectorDB,
} from "../helpers/types";
import { QuestionArgs, QuestionResults } from "./types";
import { useCaseConfiguration } from "./usecases";
import { askPostInstallAction, questionHandlers } from "./utils";
export const askQuestions = async (
args: QuestionArgs,
): Promise<QuestionResults> => {
const {
useCase: useCaseFromArgs,
framework: frameworkFromArgs,
llamaCloudKey: llamaCloudKeyFromArgs,
vectorDb: vectorDbFromArgs,
postInstallAction: postInstallActionFromArgs,
askModels: askModelsFromArgs,
} = args;
const { useCase } = await prompts(
[
{
type: useCaseFromArgs ? null : "select",
name: "useCase",
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.",
},
{
title: "Code Generator",
value: "code_generator",
description: "Build a Vercel v0 styled code generator.",
},
{
title: "Document Generator",
value: "document_generator",
description: "Build a OpenAI canvas-styled document generator.",
},
{
title: "Human in the Loop",
value: "hitl",
description:
"Build a CLI command workflow that is reviewed by a human before execution",
},
],
initial: 0,
},
],
questionHandlers,
);
const { framework } = await prompts(
{
type: frameworkFromArgs ? null : "select",
name: "framework",
message: "What language do you want to use?",
choices: [
// For Python Human in the Loop use case, please refer to this chat-ui example:
// https://github.com/run-llama/chat-ui/blob/main/examples/llamadeploy/chat/src/cli_workflow.py
...(useCase !== "hitl"
? [{ title: "Python (FastAPI)", value: "fastapi" }]
: []),
{ title: "Typescript (NextJS)", value: "nextjs" },
],
initial: 0,
},
questionHandlers,
);
const finalUseCase = (useCaseFromArgs ?? useCase) as TemplateUseCase;
const finalFramework = (frameworkFromArgs ?? framework) as TemplateFramework;
if (!finalUseCase) {
throw new Error("Use case is required");
}
if (!finalFramework) {
throw new Error("Framework is required");
}
// lookup configuration for the use case
const useCaseConfig = useCaseConfiguration[finalUseCase];
// Ask for model provider
let modelConfig = useCaseConfig.modelConfig;
if (askModelsFromArgs) {
modelConfig = await askModelConfig({
framework: finalFramework,
});
}
// Ask for LlamaCloud
let llamaCloudKey = llamaCloudKeyFromArgs ?? process.env.LLAMA_CLOUD_API_KEY;
let vectorDb: TemplateVectorDB = vectorDbFromArgs ?? "none";
if (
!vectorDbFromArgs &&
useCaseConfig.dataSources &&
!["code_generator", "document_generator", "hitl"].includes(finalUseCase) // these use cases don't use data so no need to ask for LlamaCloud
) {
const { useLlamaCloud } = await prompts(
{
type: "toggle",
name: "useLlamaCloud",
message: "Do you want to use LlamaCloud?",
active: "Yes",
inactive: "No",
initial: false,
},
questionHandlers,
);
if (useLlamaCloud && !llamaCloudKey) {
const { llamaCloudKey: llamaCloudKeyFromPrompt } = await prompts(
{
type: "text",
name: "llamaCloudKey",
message:
"Please provide your LlamaCloud API key (leave blank to skip):",
},
questionHandlers,
);
llamaCloudKey = llamaCloudKeyFromPrompt;
}
vectorDb = useLlamaCloud ? "llamacloud" : "none";
}
const result = {
...useCaseConfig,
framework: finalFramework,
useCase: finalUseCase,
modelConfig,
llamaCloudKey,
useLlamaParse: vectorDb === "llamacloud",
vectorDb,
};
const postInstallAction =
postInstallActionFromArgs ?? (await askPostInstallAction(result));
return {
...result,
postInstallAction,
};
};
-22
View File
@@ -1,22 +0,0 @@
import { InstallAppArgs } from "../create-app";
import {
TemplateFramework,
TemplatePostInstallAction,
TemplateUseCase,
TemplateVectorDB,
} from "../helpers";
export type QuestionResults = Omit<
InstallAppArgs,
"appPath" | "packageManager"
>;
export type QuestionArgs = {
useCase?: TemplateUseCase;
framework?: TemplateFramework;
askModels?: boolean;
llamaCloudKey?: string;
port?: number;
postInstallAction?: TemplatePostInstallAction;
vectorDb?: TemplateVectorDB;
};
@@ -1,42 +0,0 @@
import { EXAMPLE_10K_SEC_FILES, EXAMPLE_FILE } from "../helpers/datasources";
import { getGpt41ModelConfig } from "../helpers/models";
import { ModelConfig, TemplateUseCase } from "../helpers/types";
import { QuestionResults } from "./types";
export const useCaseConfiguration: Record<
TemplateUseCase,
Pick<QuestionResults, "template" | "dataSources"> & {
modelConfig: ModelConfig;
}
> = {
agentic_rag: {
template: "llamaindexserver",
dataSources: [EXAMPLE_FILE],
modelConfig: getGpt41ModelConfig(),
},
financial_report: {
template: "llamaindexserver",
dataSources: EXAMPLE_10K_SEC_FILES,
modelConfig: getGpt41ModelConfig(),
},
deep_research: {
template: "llamaindexserver",
dataSources: EXAMPLE_10K_SEC_FILES,
modelConfig: getGpt41ModelConfig(),
},
code_generator: {
template: "llamaindexserver",
dataSources: [],
modelConfig: getGpt41ModelConfig(),
},
document_generator: {
template: "llamaindexserver",
dataSources: [],
modelConfig: getGpt41ModelConfig(),
},
hitl: {
template: "llamaindexserver",
dataSources: [],
modelConfig: getGpt41ModelConfig(),
},
};
@@ -1,47 +0,0 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) multi-agents project using [Workflows](https://docs.llamaindex.ai/en/stable/understanding/workflows/).
## Getting Started
First, setup the environment with poetry:
> **_Note:_** This step is not needed if you are using the dev-container.
```shell
uv sync
```
Then check the parameters that have been pre-configured in the `.env` file in this directory. (E.g. you might need to configure an `OPENAI_API_KEY` if you're using OpenAI as model provider).
Second, generate the embeddings of the documents in the `./data` directory:
```shell
uv run generate
```
Third, run the development server:
```shell
uv run dev
```
## Use Case: Deep Research over own documents
The workflow performs deep research by retrieving and analyzing documents from the [data](./data) directory from multiple perspectives. The project includes a sample PDF about AI investment in 2024 to help you get started. You can also add your own documents by placing them in the data directory and running the generate script again to index them.
After starting the server, go to [http://localhost:8000](http://localhost:8000) and send a message to the agent to write a blog post.
E.g: "AI investment in 2024"
To update the workflow, you can edit the [deep_research.py](./app/workflows/deep_research.py) file.
By default, the workflow retrieves 10 results from your documents. To customize the amount of information covered in the answer, you can adjust the `TOP_K` environment variable in the `.env` file. A higher value will retrieve more results from your documents, potentially providing more comprehensive answers.
## Deployments
For production deployments, check the [DEPLOY.md](DEPLOY.md) file.
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
- [Workflows Introduction](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - learn about LlamaIndex workflows.
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
@@ -1,3 +0,0 @@
from .deep_research import create_workflow
__all__ = ["create_workflow"]
@@ -1,183 +0,0 @@
from typing import List, Literal, Optional
from llama_index.core.base.llms.types import (
CompletionResponse,
CompletionResponseAsyncGen,
)
from llama_index.core.memory.simple_composable_memory import SimpleComposableMemory
from llama_index.core.prompts import PromptTemplate
from llama_index.core.schema import MetadataMode, Node, NodeWithScore
from llama_index.core.settings import Settings
from pydantic import BaseModel, Field
class AnalysisDecision(BaseModel):
decision: Literal["research", "write", "cancel"] = Field(
description="Whether to continue research, write a report, or cancel the research after several retries"
)
research_questions: Optional[List[str]] = Field(
description="""
If the decision is to research, provide a list of questions to research that related to the user request.
Maximum 3 questions. Set to null or empty if writing a report or cancel the research.
""",
default_factory=list,
)
cancel_reason: Optional[str] = Field(
description="The reason for cancellation if the decision is to cancel research.",
default=None,
)
async def plan_research(
memory: SimpleComposableMemory,
context_nodes: List[Node],
user_request: str,
total_questions: int,
) -> AnalysisDecision:
analyze_prompt = """
You are a professor who is guiding a researcher to research a specific request/problem.
Your task is to decide on a research plan for the researcher.
The possible actions are:
+ Provide a list of questions for the researcher to investigate, with the purpose of clarifying the request.
+ Write a report if the researcher has already gathered enough research on the topic and can resolve the initial request.
+ Cancel the research if most of the answers from researchers indicate there is insufficient information to research the request. Do not attempt more than 3 research iterations or too many questions.
The workflow should be:
+ Always begin by providing some initial questions for the researcher to investigate.
+ Analyze the provided answers against the initial topic/request. If the answers are insufficient to resolve the initial request, provide additional questions for the researcher to investigate.
+ If the answers are sufficient to resolve the initial request, instruct the researcher to write a report.
Here are the context:
<Collected information>
{context_str}
</Collected information>
<Conversation context>
{conversation_context}
</Conversation context>
{enhanced_prompt}
Now, provide your decision in the required format for this user request:
<User request>
{user_request}
</User request>
"""
# Manually craft the prompt to avoid LLM hallucination
enhanced_prompt = ""
if total_questions == 0:
# Avoid writing a report without any research context
enhanced_prompt = """
The student has no questions to research. Let start by asking some questions.
"""
elif total_questions > 6:
# Avoid asking too many questions (when the data is not ready for writing a report)
enhanced_prompt = f"""
The student has researched {total_questions} questions. Should cancel the research if the context is not enough to write a report.
"""
conversation_context = "\n".join(
[f"{message.role}: {message.content}" for message in memory.get_all()]
)
context_str = "\n".join(
[node.get_content(metadata_mode=MetadataMode.LLM) for node in context_nodes]
)
res = await Settings.llm.astructured_predict(
output_cls=AnalysisDecision,
prompt=PromptTemplate(template=analyze_prompt),
user_request=user_request,
context_str=context_str,
conversation_context=conversation_context,
enhanced_prompt=enhanced_prompt,
)
return res
async def research(
question: str,
context_nodes: List[NodeWithScore],
) -> str:
prompt = """
You are a researcher who is in the process of answering the question.
The purpose is to answer the question based on the collected information, without using prior knowledge or making up any new information.
Always add citations to the sentence/point/paragraph using the id of the provided content.
The citation should follow this format: [citation:id]() where id is the id of the content.
E.g:
If we have a context like this:
<Citation id='abc-xyz'>
Baby llama is called cria
</Citation id='abc-xyz'>
And your answer uses the content, then the citation should be:
- Baby llama is called cria [citation:abc-xyz]()
Here is the provided context for the question:
<Collected information>
{context_str}
</Collected information>`
No prior knowledge, just use the provided context to answer the question: {question}
"""
context_str = "\n".join(
[_get_text_node_content_for_citation(node) for node in context_nodes]
)
res = await Settings.llm.acomplete(
prompt=prompt.format(question=question, context_str=context_str),
)
return res.text
async def write_report(
memory: SimpleComposableMemory,
user_request: str,
stream: bool = False,
) -> CompletionResponse | CompletionResponseAsyncGen:
report_prompt = """
You are a researcher writing a report based on a user request and the research context.
You have researched various perspectives related to the user request.
The report should provide a comprehensive outline covering all important points from the researched perspectives.
Create a well-structured outline for the research report that covers all the answers.
# IMPORTANT when writing in markdown format:
+ Use tables or figures where appropriate to enhance presentation.
+ Preserve all citation syntax (the `[citation:id]()` parts in the provided context). Keep these citations in the final report - no separate reference section is needed.
+ Do not add links, a table of contents, or a references section to the report.
<User request>
{user_request}
</User request>
<Research context>
{research_context}
</Research context>
Now, write a report addressing the user request based on the research provided following the format and guidelines above.
"""
research_context = "\n".join(
[f"{message.role}: {message.content}" for message in memory.get_all()]
)
llm_complete_func = (
Settings.llm.astream_complete if stream else Settings.llm.acomplete
)
res = await llm_complete_func(
prompt=report_prompt.format(
user_request=user_request,
research_context=research_context,
),
)
return res
def _get_text_node_content_for_citation(node: NodeWithScore) -> str:
"""
Construct node content for LLM with citation flag.
"""
node_id = node.node.node_id
content = f"<Citation id='{node_id}'>\n{node.get_content(metadata_mode=MetadataMode.LLM)}</Citation id='{node_id}'>"
return content
@@ -1,328 +0,0 @@
import logging
import os
import uuid
from typing import Any, Dict, List, Optional
from llama_index.core.indices.base import BaseIndex
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.memory.simple_composable_memory import SimpleComposableMemory
from llama_index.core.schema import Node
from llama_index.core.types import ChatMessage, MessageRole
from llama_index.core.workflow import (
Context,
StartEvent,
StopEvent,
Workflow,
step,
)
from app.engine.index import IndexConfig, get_index
from app.workflows.agents import plan_research, research, write_report
from app.workflows.events import SourceNodesEvent
from app.workflows.models import (
CollectAnswersEvent,
DataEvent,
PlanResearchEvent,
ReportEvent,
ResearchEvent,
)
logger = logging.getLogger("uvicorn")
logger.setLevel(logging.INFO)
def create_workflow(
params: Optional[Dict[str, Any]] = None,
**kwargs,
) -> Workflow:
index_config = IndexConfig(**params)
index = get_index(index_config)
if index is None:
raise ValueError(
"Index is not found. Try run generation script to create the index first."
)
return DeepResearchWorkflow(
index=index,
timeout=120.0,
)
class DeepResearchWorkflow(Workflow):
"""
A workflow to research and analyze documents from multiple perspectives and write a comprehensive report.
Requirements:
- An indexed documents containing the knowledge base related to the topic
Steps:
1. Retrieve information from the knowledge base
2. Analyze the retrieved information and provide questions for answering
3. Answer the questions
4. Write the report based on the research results
"""
memory: SimpleComposableMemory
context_nodes: List[Node]
index: BaseIndex
user_request: str
stream: bool = True
def __init__(
self,
index: BaseIndex,
**kwargs,
):
super().__init__(**kwargs)
self.index = index
self.context_nodes = []
self.memory = SimpleComposableMemory.from_defaults(
primary_memory=ChatMemoryBuffer.from_defaults(),
)
@step
async def retrieve(self, ctx: Context, ev: StartEvent) -> PlanResearchEvent:
"""
Initiate the workflow: memory, tools, agent
"""
self.stream = ev.get("stream", True)
self.user_request = ev.get("user_msg")
chat_history = ev.get("chat_history")
if chat_history is not None:
self.memory.put_messages(chat_history)
await ctx.set("total_questions", 0)
# Add user message to memory
self.memory.put_messages(
messages=[
ChatMessage(
role=MessageRole.USER,
content=self.user_request,
)
]
)
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "retrieve",
"state": "inprogress",
},
)
)
retriever = self.index.as_retriever(
similarity_top_k=int(os.getenv("TOP_K", 10)),
)
nodes = retriever.retrieve(self.user_request)
self.context_nodes.extend(nodes)
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "retrieve",
"state": "done",
},
)
)
# Send source nodes to the stream
# Use SourceNodesEvent to display source nodes in the UI.
ctx.write_event_to_stream(
SourceNodesEvent(
nodes=nodes,
)
)
return PlanResearchEvent()
@step
async def analyze(
self, ctx: Context, ev: PlanResearchEvent
) -> ResearchEvent | ReportEvent | StopEvent:
"""
Analyze the retrieved information
"""
logger.info("Analyzing the retrieved information")
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "analyze",
"state": "inprogress",
},
)
)
total_questions = await ctx.get("total_questions")
res = await plan_research(
memory=self.memory,
context_nodes=self.context_nodes,
user_request=self.user_request,
total_questions=total_questions,
)
if res.decision == "cancel":
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "analyze",
"state": "done",
},
)
)
return StopEvent(
result=res.cancel_reason,
)
elif res.decision == "write":
# Writing a report without any research context is not allowed.
# It's a LLM hallucination.
if total_questions == 0:
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "analyze",
"state": "done",
},
)
)
return StopEvent(
result="Sorry, I have a problem when analyzing the retrieved information. Please try again.",
)
self.memory.put(
message=ChatMessage(
role=MessageRole.ASSISTANT,
content="No more idea to analyze. We should report the answers.",
)
)
ctx.send_event(ReportEvent())
else:
total_questions += len(res.research_questions)
await ctx.set("total_questions", total_questions) # For tracking
await ctx.set(
"waiting_questions", len(res.research_questions)
) # For waiting questions to be answered
self.memory.put(
message=ChatMessage(
role=MessageRole.ASSISTANT,
content="We need to find answers to the following questions:\n"
+ "\n".join(res.research_questions),
)
)
for question in res.research_questions:
question_id = str(uuid.uuid4())
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "answer",
"state": "pending",
"id": question_id,
"question": question,
"answer": None,
},
)
)
ctx.send_event(
ResearchEvent(
question_id=question_id,
question=question,
context_nodes=self.context_nodes,
)
)
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "analyze",
"state": "done",
},
)
)
return None
@step(num_workers=2)
async def answer(self, ctx: Context, ev: ResearchEvent) -> CollectAnswersEvent:
"""
Answer the question
"""
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "answer",
"state": "inprogress",
"id": ev.question_id,
"question": ev.question,
},
)
)
try:
answer = await research(
context_nodes=ev.context_nodes,
question=ev.question,
)
except Exception as e:
logger.error(f"Error answering question {ev.question}: {e}")
answer = f"Got error when answering the question: {ev.question}"
ctx.write_event_to_stream(
DataEvent(
type="deep_research_event",
data={
"event": "answer",
"state": "done",
"id": ev.question_id,
"question": ev.question,
"answer": answer,
},
)
)
return CollectAnswersEvent(
question_id=ev.question_id,
question=ev.question,
answer=answer,
)
@step
async def collect_answers(
self, ctx: Context, ev: CollectAnswersEvent
) -> PlanResearchEvent:
"""
Collect answers to all questions
"""
num_questions = await ctx.get("waiting_questions")
results = ctx.collect_events(
ev,
expected=[CollectAnswersEvent] * num_questions,
)
if results is None:
return None
for result in results:
self.memory.put(
message=ChatMessage(
role=MessageRole.ASSISTANT,
content=f"<Question>{result.question}</Question>\n<Answer>{result.answer}</Answer>",
)
)
await ctx.set("waiting_questions", 0)
self.memory.put(
message=ChatMessage(
role=MessageRole.ASSISTANT,
content="Researched all the questions. Now, i need to analyze if it's ready to write a report or need to research more.",
)
)
return PlanResearchEvent()
@step
async def report(self, ctx: Context, ev: ReportEvent) -> StopEvent:
"""
Report the answers
"""
res = await write_report(
memory=self.memory,
user_request=self.user_request,
stream=self.stream,
)
return StopEvent(
result=res,
)
@@ -1,43 +0,0 @@
from typing import List, Literal, Optional
from llama_index.core.schema import NodeWithScore
from llama_index.core.workflow import Event
from pydantic import BaseModel
# Workflow events
class PlanResearchEvent(Event):
pass
class ResearchEvent(Event):
question_id: str
question: str
context_nodes: List[NodeWithScore]
class CollectAnswersEvent(Event):
question_id: str
question: str
answer: str
class ReportEvent(Event):
pass
# Events that are streamed to the frontend and rendered there
class DeepResearchEventData(BaseModel):
event: Literal["retrieve", "analyze", "answer"]
state: Literal["pending", "inprogress", "done", "error"]
id: Optional[str] = None
question: Optional[str] = None
answer: Optional[str] = None
class DataEvent(Event):
type: Literal["deep_research_event"]
data: DeepResearchEventData
def to_response(self):
return self.model_dump()
@@ -1,28 +0,0 @@
import { ChatMessage, ToolCallLLM } from "llamaindex";
import { getTool } from "../engine/tools";
import { FinancialReportWorkflow } from "./fin-report";
import { getQueryEngineTool } from "./tools";
const TIMEOUT = 360 * 1000;
export async function createWorkflow(options: {
chatHistory: ChatMessage[];
llm?: ToolCallLLM;
}) {
const queryEngineTool = await getQueryEngineTool();
const codeInterpreterTool = await getTool("interpreter");
const documentGeneratorTool = await getTool("document_generator");
if (!queryEngineTool || !codeInterpreterTool || !documentGeneratorTool) {
throw new Error("One or more required tools are not defined");
}
return new FinancialReportWorkflow({
chatHistory: options.chatHistory,
queryEngineTool,
codeInterpreterTool,
documentGeneratorTool,
llm: options.llm,
timeout: TIMEOUT,
});
}
@@ -1,21 +0,0 @@
import os
from llama_index.core import Settings
from llama_index.embeddings.fastembed import FastEmbedEmbedding
from llama_index.llms.anthropic import Anthropic
EMBEDDING_MODEL_MAP = {
"all-MiniLM-L6-v2": "sentence-transformers/all-MiniLM-L6-v2",
"all-mpnet-base-v2": "sentence-transformers/all-mpnet-base-v2",
}
def init_settings():
if os.getenv("ANTHROPIC_API_KEY") is None:
raise RuntimeError("ANTHROPIC_API_KEY is missing in environment variables")
Settings.llm = Anthropic(model=os.getenv("MODEL") or "claude-3-sonnet")
# This will download the model automatically if it is not already downloaded
embed_model_name = EMBEDDING_MODEL_MAP[
os.getenv("EMBEDDING_MODEL") or "all-MiniLM-L6-v2"
]
Settings.embed_model = FastEmbedEmbedding(model_name=embed_model_name)
@@ -1,40 +0,0 @@
import os
from llama_index.core import Settings
from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding
from llama_index.llms.azure_openai import AzureOpenAI
def init_settings():
api_key = os.getenv("AZURE_OPENAI_API_KEY")
endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
llm_deployment = os.getenv("AZURE_OPENAI_LLM_DEPLOYMENT")
embedding_deployment = os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT")
api_version = os.getenv("AZURE_OPENAI_API_VERSION")
if api_key is None:
raise RuntimeError("AZURE_OPENAI_API_KEY is missing in environment variables")
if endpoint is None:
raise RuntimeError("AZURE_OPENAI_ENDPOINT is missing in environment variables")
if llm_deployment is None:
raise RuntimeError(
"AZURE_OPENAI_LLM_DEPLOYMENT is missing in environment variables"
)
if embedding_deployment is None:
raise RuntimeError(
"AZURE_OPENAI_EMBEDDING_DEPLOYMENT is missing in environment variables"
)
azure_config = {
"api_key": api_key,
"azure_endpoint": endpoint,
"api_version": api_version,
}
Settings.llm = AzureOpenAI(
model="gpt-4.1", deployment_name=llm_deployment, **azure_config
)
Settings.embed_model = AzureOpenAIEmbedding(
model="text-embedding-3-large",
deployment_name=embedding_deployment,
**azure_config,
)
@@ -1,14 +0,0 @@
import os
from llama_index.core import Settings
from llama_index.embeddings.google_genai import GoogleGenAIEmbedding
from llama_index.llms.google_genai import GoogleGenAI
def init_settings():
if os.getenv("GOOGLE_API_KEY") is None:
raise RuntimeError("GOOGLE_API_KEY is missing in environment variables")
Settings.llm = GoogleGenAI(model=os.getenv("MODEL") or "gemini-2.0-flash")
Settings.embed_model = GoogleGenAIEmbedding(
model=os.getenv("EMBEDDING_MODEL") or "text-embedding-004"
)
@@ -1,21 +0,0 @@
import os
from llama_index.core import Settings
from llama_index.embeddings.fastembed import FastEmbedEmbedding
from llama_index.llms.groq import Groq
EMBEDDING_MODEL_MAP = {
"all-MiniLM-L6-v2": "sentence-transformers/all-MiniLM-L6-v2",
"all-mpnet-base-v2": "sentence-transformers/all-mpnet-base-v2",
}
def init_settings():
if os.getenv("GROQ_API_KEY") is None:
raise RuntimeError("GROQ_API_KEY is missing in environment variables")
Settings.llm = Groq(model=os.getenv("MODEL") or "llama-3.1-8b-instant")
# This will download the model automatically if it is not already downloaded
embed_model_name = EMBEDDING_MODEL_MAP[
os.getenv("EMBEDDING_MODEL") or "all-MiniLM-L6-v2"
]
Settings.embed_model = FastEmbedEmbedding(model_name=embed_model_name)
@@ -1,10 +0,0 @@
import os
from llama_index.core import Settings
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.llms.huggingface import HuggingFaceLLM
def init_settings():
Settings.llm = HuggingFaceLLM(model_name=os.getenv("MODEL"))
Settings.embed_model = HuggingFaceEmbedding(model_name=os.getenv("EMBEDDING_MODEL"))
@@ -1,16 +0,0 @@
import os
from llama_index.core import Settings
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama import Ollama
def init_settings():
if os.getenv("OLLAMA_BASE_URL") is None:
raise RuntimeError("OLLAMA_BASE_URL is missing in environment variables")
base_url = os.getenv("OLLAMA_BASE_URL") or "http://127.0.0.1:11434"
llm_model = os.getenv("MODEL") or "llama3.1"
embed_model = os.getenv("EMBEDDING_MODEL") or "nomic-embed-text"
Settings.llm = Ollama(model=llm_model, base_url=base_url)
Settings.embed_model = OllamaEmbedding(model=embed_model, base_url=base_url)
@@ -1,14 +0,0 @@
import os
from llama_index.core import Settings
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
def init_settings():
if os.getenv("OPENAI_API_KEY") is None:
raise RuntimeError("OPENAI_API_KEY is missing in environment variables")
Settings.llm = OpenAI(model=os.getenv("MODEL") or "gpt-4.1")
Settings.embed_model = OpenAIEmbedding(
model=os.getenv("EMBEDDING_MODEL") or "text-embedding-3-large"
)
@@ -1,19 +0,0 @@
import {
ALL_AVAILABLE_ANTHROPIC_MODELS,
Anthropic,
} from "@llamaindex/anthropic";
import { HuggingFaceEmbedding } from "@llamaindex/huggingface";
import { Settings } from "llamaindex";
export function initSettings() {
const embedModelMap: Record<string, string> = {
"all-MiniLM-L6-v2": "Xenova/all-MiniLM-L6-v2",
"all-mpnet-base-v2": "Xenova/all-mpnet-base-v2",
};
Settings.llm = new Anthropic({
model: process.env.MODEL as keyof typeof ALL_AVAILABLE_ANTHROPIC_MODELS,
});
Settings.embedModel = new HuggingFaceEmbedding({
modelType: embedModelMap[process.env.EMBEDDING_MODEL!],
});
}
@@ -1,49 +0,0 @@
import { OpenAI, OpenAIEmbedding } from "@llamaindex/openai";
import { Settings } from "llamaindex";
export function initSettings() {
// Map Azure OpenAI model names to OpenAI model names (only for TS)
const AZURE_OPENAI_MODEL_MAP: Record<string, string> = {
"gpt-35-turbo": "gpt-3.5-turbo",
"gpt-35-turbo-16k": "gpt-3.5-turbo-16k",
"gpt-4o": "gpt-4o",
"gpt-4": "gpt-4",
"gpt-4-32k": "gpt-4-32k",
"gpt-4-turbo": "gpt-4-turbo",
"gpt-4-turbo-2024-04-09": "gpt-4-turbo",
"gpt-4-vision-preview": "gpt-4-vision-preview",
"gpt-4-1106-preview": "gpt-4-1106-preview",
"gpt-4o-2024-05-13": "gpt-4o-2024-05-13",
};
const azureConfig = {
apiKey: process.env.AZURE_OPENAI_KEY,
endpoint: process.env.AZURE_OPENAI_ENDPOINT,
apiVersion:
process.env.AZURE_OPENAI_API_VERSION || process.env.OPENAI_API_VERSION,
};
Settings.llm = new OpenAI({
model:
AZURE_OPENAI_MODEL_MAP[process.env.MODEL ?? "gpt-35-turbo"] ??
"gpt-3.5-turbo",
maxTokens: process.env.LLM_MAX_TOKENS
? Number(process.env.LLM_MAX_TOKENS)
: undefined,
azure: {
...azureConfig,
deployment: process.env.AZURE_OPENAI_LLM_DEPLOYMENT,
},
});
Settings.embedModel = new OpenAIEmbedding({
model: process.env.EMBEDDING_MODEL,
dimensions: process.env.EMBEDDING_DIM
? parseInt(process.env.EMBEDDING_DIM)
: undefined,
azure: {
...azureConfig,
deployment: process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT,
},
});
}
@@ -1,16 +0,0 @@
import {
Gemini,
GEMINI_EMBEDDING_MODEL,
GEMINI_MODEL,
GeminiEmbedding,
} from "@llamaindex/google";
import { Settings } from "llamaindex";
export function initSettings() {
Settings.llm = new Gemini({
model: process.env.MODEL as GEMINI_MODEL,
});
Settings.embedModel = new GeminiEmbedding({
model: process.env.EMBEDDING_MODEL as GEMINI_EMBEDDING_MODEL,
});
}
@@ -1,18 +0,0 @@
import { Groq } from "@llamaindex/groq";
import { HuggingFaceEmbedding } from "@llamaindex/huggingface";
import { Settings } from "llamaindex";
export function initSettings() {
const embedModelMap: Record<string, string> = {
"all-MiniLM-L6-v2": "Xenova/all-MiniLM-L6-v2",
"all-mpnet-base-v2": "Xenova/all-mpnet-base-v2",
};
Settings.llm = new Groq({
model: process.env.MODEL!,
});
Settings.embedModel = new HuggingFaceEmbedding({
modelType: embedModelMap[process.env.EMBEDDING_MODEL!],
});
}
@@ -1,16 +0,0 @@
import {
ALL_AVAILABLE_MISTRAL_MODELS,
MistralAI,
MistralAIEmbedding,
MistralAIEmbeddingModelType,
} from "@llamaindex/mistral";
import { Settings } from "llamaindex";
export function initSettings() {
Settings.llm = new MistralAI({
model: process.env.MODEL as keyof typeof ALL_AVAILABLE_MISTRAL_MODELS,
});
Settings.embedModel = new MistralAIEmbedding({
model: process.env.EMBEDDING_MODEL as MistralAIEmbeddingModelType,
});
}
@@ -1,16 +0,0 @@
import { Ollama, OllamaEmbedding } from "@llamaindex/ollama";
import { Settings } from "llamaindex";
export function initSettings() {
const config = {
host: process.env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434",
};
Settings.llm = new Ollama({
model: process.env.MODEL ?? "",
config,
});
Settings.embedModel = new OllamaEmbedding({
model: process.env.EMBEDDING_MODEL ?? "",
config,
});
}
@@ -1,17 +0,0 @@
import { OpenAI, OpenAIEmbedding } from "@llamaindex/openai";
import { Settings } from "llamaindex";
export function initSettings() {
Settings.llm = new OpenAI({
model: process.env.MODEL ?? "gpt-4o-mini",
maxTokens: process.env.LLM_MAX_TOKENS
? Number(process.env.LLM_MAX_TOKENS)
: undefined,
});
Settings.embedModel = new OpenAIEmbedding({
model: process.env.EMBEDDING_MODEL,
dimensions: process.env.EMBEDDING_DIM
? parseInt(process.env.EMBEDDING_DIM)
: undefined,
});
}
@@ -1,59 +0,0 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Reflex](https://reflex.dev/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama) featuring automated contract review and compliance analysis use case.
## Getting Started
First, setup the environment with poetry:
> **_Note:_** This step is not needed if you are using the dev-container.
```shell
uv sync
```
Then check the parameters that have been pre-configured in the `.env` file in this directory. (E.g. you might need to configure an `OPENAI_API_KEY` if you're using OpenAI as model provider).
Second, generate the embeddings of the example document in the `./data` directory:
```shell
uv run generate
```
Third, start app with `reflex` command:
```shell
uv run reflex run
```
To deploy the application, refer to the Reflex deployment guide: https://reflex.dev/docs/hosting/deploy-quick-start/
### UI
The application provides an interactive web interface accessible at http://localhost:3000 for testing the contract review workflow.
To get started:
1. Upload a contract document:
- Use the provided [example_vendor_agreement.md](./example_vendor_agreement.md) for testing
- Or upload your own document (supported formats: PDF, TXT, Markdown, DOCX)
2. Review Process:
- The system will automatically analyze your document against compliance guidelines
- By default, it uses [GDPR](./data/gdpr.pdf) as the compliance benchmark
- Custom guidelines can be used by adding your policy documents to the `./data` directory and running `uv run generate` to update the embeddings
The interface will display the analysis results for the compliance of the contract document.
### Development
You can start editing the backend workflow by modifying the [`ContractReviewWorkflow`](./app/services/contract_reviewer.py).
For UI, you can start looking at the [`AppState`](./app/ui/states/app.py) code and navigating to the appropriate components.
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
@@ -1,47 +0,0 @@
DATA_DIR = "data"
UPLOADED_DIR = "output/uploaded"
# Workflow prompts
CONTRACT_EXTRACT_PROMPT = """\
You are given contract data below. \
Please extract out relevant information from the contract into the defined schema - the schema is defined as a function call.\
{contract_data}
"""
CONTRACT_MATCH_PROMPT = """\
Given the following contract clause and the corresponding relevant guideline text, evaluate the compliance \
and provide a JSON object that matches the ClauseComplianceCheck schema.
**Contract Clause:**
{clause_text}
**Matched Guideline Text(s):**
{guideline_text}
"""
COMPLIANCE_REPORT_SYSTEM_PROMPT = """\
You are a compliance reporting assistant. Your task is to generate a final compliance report \
based on the results of clause compliance checks against \
a given set of guidelines.
Analyze the provided compliance results and produce a structured report according to the specified schema.
Ensure that if there are no noncompliant clauses, the report clearly indicates full compliance.
"""
COMPLIANCE_REPORT_USER_PROMPT = """\
A set of clauses within a contract were checked against GDPR compliance guidelines for the following vendor: {vendor_name}.
The set of noncompliant clauses are given below.
Each section includes:
- **Clause:** The exact text of the contract clause.
- **Guideline:** The relevant GDPR guideline text.
- **Compliance Status:** Should be `False` for noncompliant clauses.
- **Notes:** Additional information or explanations.
{compliance_results}
Based on the above compliance results, generate a final compliance report following the `ComplianceReport` schema below.
If there are no noncompliant clauses, the report should indicate that the contract is fully compliant.
"""
@@ -1,85 +0,0 @@
from typing import List, Optional
from pydantic import BaseModel, Field
class ContractClause(BaseModel):
clause_text: str = Field(..., description="The exact text of the clause.")
mentions_data_processing: bool = Field(
False,
description="True if the clause involves personal data collection or usage.",
)
mentions_data_transfer: bool = Field(
False,
description="True if the clause involves transferring personal data, especially to third parties or across borders.",
)
requires_consent: bool = Field(
False,
description="True if the clause explicitly states that user consent is needed for data activities.",
)
specifies_purpose: bool = Field(
False,
description="True if the clause specifies a clear purpose for data handling or transfer.",
)
mentions_safeguards: bool = Field(
False,
description="True if the clause mentions security measures or other safeguards for data.",
)
class ContractExtraction(BaseModel):
vendor_name: Optional[str] = Field(
None, description="The vendor's name if identifiable."
)
effective_date: Optional[str] = Field(
None, description="The effective date of the agreement, if available."
)
governing_law: Optional[str] = Field(
None, description="The governing law of the contract, if stated."
)
clauses: List[ContractClause] = Field(
..., description="List of extracted clauses and their relevant indicators."
)
class GuidelineMatch(BaseModel):
guideline_text: str = Field(
...,
description="The single most relevant guideline excerpt related to this clause.",
)
similarity_score: float = Field(
...,
description="Similarity score indicating how closely the guideline matches the clause, e.g., between 0 and 1.",
)
relevance_explanation: Optional[str] = Field(
None, description="Brief explanation of why this guideline is relevant."
)
class ClauseComplianceCheck(BaseModel):
clause_text: str = Field(
..., description="The exact text of the clause from the contract."
)
matched_guideline: Optional[GuidelineMatch] = Field(
None, description="The most relevant guideline extracted via vector retrieval."
)
compliant: bool = Field(
...,
description="Indicates whether the clause is considered compliant with the referenced guideline.",
)
notes: Optional[str] = Field(
None, description="Additional commentary or recommendations."
)
class ComplianceReport(BaseModel):
vendor_name: Optional[str] = Field(
None, description="The vendor's name if identified from the contract."
)
overall_compliant: bool = Field(
..., description="Indicates if the contract is considered overall compliant."
)
summary_notes: str = Field(
...,
description="Always give a general summary or recommendations for achieving full compliance.",
)
@@ -1,360 +0,0 @@
import logging
import os
import uuid
from enum import Enum
from pathlib import Path
from typing import List
from app.config import (
COMPLIANCE_REPORT_SYSTEM_PROMPT,
COMPLIANCE_REPORT_USER_PROMPT,
CONTRACT_EXTRACT_PROMPT,
CONTRACT_MATCH_PROMPT,
)
from app.engine.index import get_index
from app.models import (
ClauseComplianceCheck,
ComplianceReport,
ContractClause,
ContractExtraction,
)
from llama_index.core import SimpleDirectoryReader
from llama_index.core.llms import LLM
from llama_index.core.prompts import ChatPromptTemplate
from llama_index.core.retrievers import BaseRetriever
from llama_index.core.settings import Settings
from llama_index.core.workflow import (
Context,
Event,
StartEvent,
StopEvent,
Workflow,
step,
)
logger = logging.getLogger(__name__)
def get_workflow():
index = get_index()
if index is None:
raise RuntimeError(
"Index not found! Please run `uv run generate` to populate an index first."
)
return ContractReviewWorkflow(
guideline_retriever=index.as_retriever(),
llm=Settings.llm,
verbose=True,
timeout=120,
)
class Step(Enum):
PARSE_CONTRACT = "parse_contract"
ANALYZE_CLAUSES = "analyze_clauses"
HANDLE_CLAUSE = "handle_clause"
GENERATE_REPORT = "generate_report"
class ContractExtractionEvent(Event):
contract_extraction: ContractExtraction
class MatchGuidelineEvent(Event):
request_id: str
clause: ContractClause
vendor_name: str
class MatchGuidelineResultEvent(Event):
result: ClauseComplianceCheck
class GenerateReportEvent(Event):
match_results: List[ClauseComplianceCheck]
class LogEvent(Event):
msg: str
step: Step
data: dict = {}
is_step_completed: bool = False
class ContractReviewWorkflow(Workflow):
"""Contract review workflow."""
def __init__(
self,
guideline_retriever: BaseRetriever,
llm: LLM | None = None,
similarity_top_k: int = 20,
**kwargs,
) -> None:
"""Init params."""
super().__init__(**kwargs)
self.guideline_retriever = guideline_retriever
self.llm = llm or Settings.llm
self.similarity_top_k = similarity_top_k
# if not exists, create
out_path = Path("output") / "workflow_output"
if not out_path.exists():
out_path.mkdir(parents=True, exist_ok=True)
os.chmod(str(out_path), 0o0777)
self.output_dir = out_path
@step
async def parse_contract(
self, ctx: Context, ev: StartEvent
) -> ContractExtractionEvent:
"""Parse the contract."""
uploaded_contract_path = Path(ev.contract_path)
contract_file_name = uploaded_contract_path.name
# Set contract file name in context
await ctx.set("contract_file_name", contract_file_name)
# Parse and read the contract to documents
docs = SimpleDirectoryReader(
input_files=[str(uploaded_contract_path)]
).load_data()
ctx.write_event_to_stream(
LogEvent(
msg=f"Loaded document: {contract_file_name}",
step=Step.PARSE_CONTRACT,
data={
"saved_path": str(uploaded_contract_path),
"parsed_data": None,
},
)
)
# Parse the contract into a structured model
# See the ContractExtraction model for information we want to extract
ctx.write_event_to_stream(
LogEvent(
msg="Extracting information from the document",
step=Step.PARSE_CONTRACT,
data={
"saved_path": str(uploaded_contract_path),
"parsed_data": None,
},
)
)
prompt = ChatPromptTemplate.from_messages([("user", CONTRACT_EXTRACT_PROMPT)])
contract_extraction = await self.llm.astructured_predict(
ContractExtraction,
prompt,
contract_data="\n".join(
[d.get_content(metadata_mode="all") for d in docs] # type: ignore
),
)
if not isinstance(contract_extraction, ContractExtraction):
raise ValueError(f"Invalid extraction from contract: {contract_extraction}")
# save output template to file
contract_extraction_path = Path(f"{self.output_dir}/{contract_file_name}.json")
with open(contract_extraction_path, "w") as fp:
fp.write(contract_extraction.model_dump_json())
ctx.write_event_to_stream(
LogEvent(
msg="Extracted successfully",
step=Step.PARSE_CONTRACT,
is_step_completed=True,
data={
"saved_path": str(contract_extraction_path),
"parsed_data": contract_extraction.model_dump_json(),
},
)
)
return ContractExtractionEvent(contract_extraction=contract_extraction)
@step
async def dispatch_guideline_match( # type: ignore
self, ctx: Context, ev: ContractExtractionEvent
) -> MatchGuidelineEvent:
"""For each clause in the contract, find relevant guidelines.
Use a map-reduce pattern, send each parsed clause as a MatchGuidelineEvent.
"""
await ctx.set("num_clauses", len(ev.contract_extraction.clauses))
await ctx.set("vendor_name", ev.contract_extraction.vendor_name)
for clause in ev.contract_extraction.clauses:
request_id = str(uuid.uuid4())
ctx.send_event(
MatchGuidelineEvent(
request_id=request_id,
clause=clause,
vendor_name=ev.contract_extraction.vendor_name or "Not identified",
)
)
ctx.write_event_to_stream(
LogEvent(
msg=f"Created {len(ev.contract_extraction.clauses)} tasks for analyzing with the guidelines",
step=Step.ANALYZE_CLAUSES,
)
)
@step
async def handle_guideline_match(
self, ctx: Context, ev: MatchGuidelineEvent
) -> MatchGuidelineResultEvent:
"""Handle matching clause against guideline."""
ctx.write_event_to_stream(
LogEvent(
msg=f"Handling clause for request {ev.request_id}",
step=Step.HANDLE_CLAUSE,
data={
"request_id": ev.request_id,
"clause_text": ev.clause.clause_text,
"is_compliant": None,
},
)
)
# retrieve matching guideline
query = f"""\
Find the relevant guideline from {ev.vendor_name} that aligns with the following contract clause:
{ev.clause.clause_text}
"""
guideline_docs = self.guideline_retriever.retrieve(query)
guideline_text = "\n\n".join([g.get_content() for g in guideline_docs])
# extract compliance from contract into a structured model
# see ClauseComplianceCheck model for the schema
prompt = ChatPromptTemplate.from_messages([("user", CONTRACT_MATCH_PROMPT)])
compliance_output = await self.llm.astructured_predict(
ClauseComplianceCheck,
prompt,
clause_text=ev.clause.model_dump_json(),
guideline_text=guideline_text,
)
if not isinstance(compliance_output, ClauseComplianceCheck):
raise ValueError(f"Invalid compliance check: {compliance_output}")
ctx.write_event_to_stream(
LogEvent(
msg=f"Completed compliance check for request {ev.request_id}",
step=Step.HANDLE_CLAUSE,
is_step_completed=True,
data={
"request_id": ev.request_id,
"clause_text": ev.clause.clause_text,
"is_compliant": compliance_output.compliant,
"result": compliance_output,
},
)
)
return MatchGuidelineResultEvent(result=compliance_output)
@step
async def gather_guideline_match(
self, ctx: Context, ev: MatchGuidelineResultEvent
) -> GenerateReportEvent | None:
"""Handle matching clause against guideline."""
num_clauses = await ctx.get("num_clauses")
events = ctx.collect_events(ev, [MatchGuidelineResultEvent] * num_clauses)
if events is None:
return None
match_results = [e.result for e in events]
# save match results
contract_file_name = await ctx.get("contract_file_name")
match_results_path = Path(
f"{self.output_dir}/match_results_{contract_file_name}.jsonl"
)
with open(match_results_path, "w") as fp:
for mr in match_results:
fp.write(mr.model_dump_json() + "\n")
ctx.write_event_to_stream(
LogEvent(
msg=f"Processed {len(match_results)} clauses",
step=Step.ANALYZE_CLAUSES,
is_step_completed=True,
data={"saved_path": str(match_results_path)},
)
)
return GenerateReportEvent(match_results=[e.result for e in events])
@step
async def generate_output(self, ctx: Context, ev: GenerateReportEvent) -> StopEvent:
ctx.write_event_to_stream(
LogEvent(
msg="Generating Compliance Report",
step=Step.GENERATE_REPORT,
data={"is_completed": False},
)
)
# if all clauses are compliant, return a compliant result
non_compliant_results = [r for r in ev.match_results if not r.compliant]
# generate compliance results string
result_tmpl = """
1. **Clause**: {clause}
2. **Guideline:** {guideline}
3. **Compliance Status:** {compliance_status}
4. **Notes:** {notes}
"""
non_compliant_strings = []
for nr in non_compliant_results:
non_compliant_strings.append(
result_tmpl.format(
clause=nr.clause_text,
guideline=nr.matched_guideline.guideline_text
if nr.matched_guideline is not None
else "No relevant guideline found",
compliance_status=nr.compliant,
notes=nr.notes,
)
)
non_compliant_str = "\n\n".join(non_compliant_strings)
prompt = ChatPromptTemplate.from_messages(
[
("system", COMPLIANCE_REPORT_SYSTEM_PROMPT),
("user", COMPLIANCE_REPORT_USER_PROMPT),
]
)
compliance_report = await self.llm.astructured_predict(
ComplianceReport,
prompt,
compliance_results=non_compliant_str,
vendor_name=await ctx.get("vendor_name"),
)
# Save compliance report to file
contract_file_name = await ctx.get("contract_file_name")
compliance_report_path = Path(
f"{self.output_dir}/report_{contract_file_name}.json"
)
with open(compliance_report_path, "w") as fp:
fp.write(compliance_report.model_dump_json())
ctx.write_event_to_stream(
LogEvent(
msg=f"Compliance report saved to {compliance_report_path}",
step=Step.GENERATE_REPORT,
is_step_completed=True,
data={
"saved_path": str(compliance_report_path),
"result": compliance_report,
},
)
)
return StopEvent(
result={
"report": compliance_report,
"non_compliant_results": non_compliant_results,
}
)

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