mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-02 19:14:28 -04:00
Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 97a7d9bc25 | |||
| 2f085c1c95 | |||
| fec752eb63 | |||
| 63f5f6f956 | |||
| 93e2abe301 | |||
| 28b46be22a | |||
| b618e91e99 | |||
| 91ce4e1236 | |||
| 2b85420370 | |||
| 52cc37f206 | |||
| 952b5b4908 | |||
| e8004fd711 | |||
| 48f6d849e6 | |||
| 02a9db3d40 | |||
| 8fa8c3bad8 | |||
| a221bc60f7 | |||
| 6c0fb51557 | |||
| 3589f946a9 | |||
| e2486eb080 | |||
| 66b81e5323 | |||
| 924649c025 | |||
| 1b04db917b | |||
| af9ad3c42d | |||
| 1ff6eaf3e1 | |||
| a543a27faf | |||
| 63edd74ba1 | |||
| 13a967b2a2 | |||
| 2ac4d92493 | |||
| 7e47cba4ba | |||
| bc56fa3c5f | |||
| 087c96164d | |||
| 3ff0a18876 | |||
| df1047480a | |||
| 8d89223a08 | |||
| 49a944182f | |||
| 058b3762c1 | |||
| 4c8579b04f | |||
| bb1e82cdae | |||
| f682a1c36e | |||
| b8a1ff6412 | |||
| 5fe9e17d3f | |||
| 15619d81a6 | |||
| 76742da78a | |||
| 693d7a0ea5 | |||
| 8d59ef0a6b | |||
| c62f26e31c | |||
| d3f73679b4 | |||
| 91c35cff33 | |||
| 82ac925224 | |||
| f24ee8e6f9 | |||
| 3acec88fbc | |||
| eee3230e99 | |||
| d8425e5290 | |||
| 0bc5a0d882 | |||
| bbae802bed | |||
| 25fba4381b | |||
| d0618fa2fa | |||
| f3fe3ffc9b | |||
| 6f75d4ab6e | |||
| 3242738fe4 | |||
| 17538eb0dd | |||
| d3772cb4a2 | |||
| 527075c086 | |||
| fb7d4da149 | |||
| 5c35b194bb | |||
| 85e5e7e662 | |||
| 58362542c0 | |||
| 6f44185f68 | |||
| afe9e9fc16 | |||
| 1b5a519f13 | |||
| f072308d03 | |||
| 1df8cfbdc2 | |||
| 24515393a6 | |||
| b3eb0ba7d4 | |||
| 556f33c0ab | |||
| 7a70390b00 | |||
| ad5912b41f | |||
| 76502d28e7 | |||
| f4ca602da5 | |||
| d304554f33 | |||
| 8dce9f913d | |||
| c62096c516 | |||
| 0384268543 | |||
| d9f9e3c1c3 | |||
| 1357c423a3 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
chore: bump @llamaindex/server 0.3.0 in templates
|
||||
+10
-19
@@ -22,7 +22,7 @@ jobs:
|
||||
python-version: ["3.11"]
|
||||
os: [macos-latest, windows-latest, ubuntu-22.04]
|
||||
frameworks: ["fastapi"]
|
||||
datasources: ["--no-files", "--example-file", "--llamacloud"]
|
||||
vectordbs: ["none", "llamacloud"]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -69,15 +69,16 @@ jobs:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
|
||||
FRAMEWORK: ${{ matrix.frameworks }}
|
||||
DATASOURCE: ${{ matrix.datasources }}
|
||||
VECTORDB: ${{ matrix.vectordbs }}
|
||||
PYTHONIOENCODING: utf-8
|
||||
PYTHONLEGACYWINDOWSSTDIO: utf-8
|
||||
SERVER_PACKAGE_PATH: ${{ env.SERVER_PACKAGE_PATH }}
|
||||
working-directory: packages/create-llama
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-python-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}
|
||||
name: playwright-report-python-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.vectordbs }}
|
||||
path: packages/create-llama/playwright-report/
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
@@ -88,11 +89,10 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
node-version: [20, 22]
|
||||
python-version: ["3.11"]
|
||||
node-version: [22]
|
||||
os: [macos-latest, windows-latest, ubuntu-22.04]
|
||||
frameworks: ["nextjs"]
|
||||
datasources: ["--no-files", "--example-file", "--llamacloud"]
|
||||
vectordbs: ["none", "llamacloud"]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -100,16 +100,6 @@ 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 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
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
@@ -134,18 +124,19 @@ jobs:
|
||||
working-directory: packages/create-llama
|
||||
|
||||
- name: Run Playwright tests for TypeScript
|
||||
run: pnpm run e2e:typescript
|
||||
run: |
|
||||
pnpm run e2e:ts
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
|
||||
FRAMEWORK: ${{ matrix.frameworks }}
|
||||
DATASOURCE: ${{ matrix.datasources }}
|
||||
VECTORDB: ${{ matrix.vectordbs }}
|
||||
working-directory: packages/create-llama
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-typescript-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.datasources }}-node${{ matrix.node-version }}
|
||||
name: playwright-report-typescript-${{ matrix.os }}-${{ matrix.frameworks }}-${{ matrix.vectordbs}}-node${{ matrix.node-version }}
|
||||
path: packages/create-llama/playwright-report/
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
@@ -16,6 +16,16 @@ jobs:
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -31,6 +41,9 @@ jobs:
|
||||
- name: Run Prettier
|
||||
run: pnpm run format
|
||||
|
||||
- name: Run build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Run Python format check
|
||||
uses: chartboost/ruff-action@v1
|
||||
with:
|
||||
|
||||
@@ -17,6 +17,11 @@ jobs:
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
@@ -51,8 +56,12 @@ 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,138 +0,0 @@
|
||||
name: Release llama-index-server
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "python/llama-index-server/**"
|
||||
- ".github/workflows/release_llama_index_server.yml"
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Create Release PR
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./python/llama-index-server
|
||||
if: |
|
||||
github.event_name == 'push' &&
|
||||
!startsWith(github.ref, 'refs/heads/release/llama-index-server-v') &&
|
||||
!contains(github.event.head_commit.message, 'Release: llama-index-server v')
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: uv sync --all-extras --dev
|
||||
|
||||
- name: Setup Git
|
||||
run: |
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
|
||||
- name: Bump patch version
|
||||
shell: bash
|
||||
run: |
|
||||
uvx --from=toml-cli toml set --toml-path=pyproject.toml project.version $(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version | awk -F. '{$NF = $NF + 1;}1' OFS=.)
|
||||
git add pyproject.toml
|
||||
git commit -m "chore(release): bump llama-index-server version to $(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version)"
|
||||
|
||||
- name: Get current version
|
||||
id: get_version
|
||||
shell: bash
|
||||
run: |
|
||||
version=$(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version)
|
||||
echo "current_version=${version}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create Release PR
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "Release: llama-index-server v${{ steps.get_version.outputs.current_version }}"
|
||||
title: "Release: llama-index-server v${{ steps.get_version.outputs.current_version }}"
|
||||
body: |
|
||||
This PR was automatically created to release a new version of the llama-index-server package.
|
||||
|
||||
Version: ${{ steps.get_version.outputs.current_version }}
|
||||
|
||||
Please review the changes and merge to trigger the release.
|
||||
branch: release/llama-index-server-v${{ steps.get_version.outputs.current_version }}
|
||||
base: main
|
||||
labels: release, llama-index-server
|
||||
|
||||
publish:
|
||||
name: Publish to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./python/llama-index-server
|
||||
if: |
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.merged == true &&
|
||||
startsWith(github.event.pull_request.title, 'Release: llama-index-server') &&
|
||||
startsWith(github.event.pull_request.head.ref, 'release/llama-index-server-v')
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: uv sync --all-extras
|
||||
|
||||
- name: Get current version
|
||||
id: get_version
|
||||
shell: bash
|
||||
run: |
|
||||
version=$(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version)
|
||||
echo "current_version=${version}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build package
|
||||
shell: bash
|
||||
run: uv build --no-sources
|
||||
|
||||
- name: Publish to PyPI
|
||||
shell: bash
|
||||
run: uv publish --token ${{ secrets.PYPI_TOKEN }}
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: llama-index-server-v${{ steps.get_version.outputs.current_version }}
|
||||
name: "llama-index-server v${{ steps.get_version.outputs.current_version }}"
|
||||
body: |
|
||||
Release of llama-index-server v${{ steps.get_version.outputs.current_version }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.9"
|
||||
UI_TEST: "true"
|
||||
|
||||
jobs:
|
||||
unit-test:
|
||||
@@ -19,20 +20,27 @@ jobs:
|
||||
python-version: ["3.9"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: uv sync --all-extras --dev
|
||||
run: pnpm install && pnpm build
|
||||
|
||||
- name: Run unit tests
|
||||
shell: bash
|
||||
@@ -46,20 +54,20 @@ jobs:
|
||||
working-directory: python/llama-index-server
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: uv sync --all-extras --dev
|
||||
run: pnpm install
|
||||
|
||||
- name: Run mypy
|
||||
shell: bash
|
||||
@@ -73,27 +81,56 @@ jobs:
|
||||
working-directory: python/llama-index-server
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Install build package
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install && pnpm build
|
||||
|
||||
- name: Build package
|
||||
shell: bash
|
||||
run: uv sync --all-extras
|
||||
run: uv build
|
||||
|
||||
- name: Get the absolute wheel file path and save it to the output
|
||||
shell: bash
|
||||
id: get_whl_path
|
||||
run: |
|
||||
WHL_FILE=$(readlink -f dist/*.whl)
|
||||
echo "whl_file=$WHL_FILE" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Test import
|
||||
shell: bash
|
||||
run: uv run python -c "from llama_index.server import LlamaIndexServer"
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
WHL_FILE: ${{ steps.get_whl_path.outputs.whl_file }}
|
||||
run: |
|
||||
uv run --with $WHL_FILE python -c "from llama_index.server import LlamaIndexServer"
|
||||
|
||||
- name: Check frontend resources is present
|
||||
shell: bash
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
WHL_FILE: ${{ steps.get_whl_path.outputs.whl_file }}
|
||||
run: |
|
||||
uv run --with $WHL_FILE python -c "from llama_index.server.chat_ui import check_ui_resources; check_ui_resources()"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: llama-index-server
|
||||
path: python/llama-index-server/dist/
|
||||
path: dist/
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
node_modules/
|
||||
pnpm-lock.yaml
|
||||
lib/
|
||||
dist/
|
||||
cache/
|
||||
build/
|
||||
.next/
|
||||
out/
|
||||
**/playwright-report/
|
||||
**/test-results/
|
||||
|
||||
# Python
|
||||
python/
|
||||
**/*.mypy_cache/**
|
||||
**/*.venv/**
|
||||
**/*.ruff_cache/**
|
||||
@@ -0,0 +1,184 @@
|
||||
# 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
|
||||
- **`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
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
### 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
|
||||
@@ -25,13 +25,10 @@ to start the development server. You can then visit [http://localhost:3000](http
|
||||
## What you'll get
|
||||
|
||||
- A set of pre-configured use cases to get you started, e.g. Agentic RAG, Data Analysis, Report Generation, etc.
|
||||
- A Next.js-powered front-end using components from [shadcn/ui](https://ui.shadcn.com/). The app is set up as a chat interface that can answer questions about your data or interact with your agent
|
||||
- Your choice of two back-ends:
|
||||
- **Next.js**: if you select this option, you’ll 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, you’ll 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
|
||||
- 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, you’ll 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, you’ll 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/)
|
||||
- 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:
|
||||
@@ -40,11 +37,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` (If you're using Express or Python and generate a frontend, it will be `./backend/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`.
|
||||
|
||||
The app will ingest any supported files you put in this directory. Your Next.js and Express apps use LlamaIndex.TS, so they will be able to ingest any PDF, text, CSV, Markdown, Word and HTML files. The Python backend can read even more types, including video and audio files.
|
||||
The app will ingest any supported files you put in this directory. Your Next.js apps use LlamaIndex.TS, so they will be able to ingest any PDF, text, CSV, Markdown, Word and HTML files. The Python backend can read even more types, including video and audio files.
|
||||
|
||||
Before you can use your data, you need to index it. If you're using the Next.js or Express apps, run:
|
||||
Before you can use your data, you need to index it. If you're using the Next.js apps, run:
|
||||
|
||||
```bash
|
||||
npm run generate
|
||||
@@ -60,11 +57,11 @@ uv run generate
|
||||
|
||||
## Customizing the AI models
|
||||
|
||||
The app will default to OpenAI's `gpt-4o-mini` LLM and `text-embedding-3-large` embedding model.
|
||||
The app will default to OpenAI's `gpt-4.1` LLM and `text-embedding-3-large` embedding model.
|
||||
|
||||
If you want to use different OpenAI models, add the `--ask-models` CLI parameter.
|
||||
If you want to use different models, add the `--ask-models` CLI parameter.
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
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)
|
||||
|
||||
@@ -90,11 +87,10 @@ Need to install the following packages:
|
||||
create-llama@latest
|
||||
Ok to proceed? (y) y
|
||||
✔ What is your project named? … my-app
|
||||
✔ What app do you want to build? › Agentic RAG
|
||||
✔ What use case 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)
|
||||
@@ -106,25 +102,6 @@ Ok to proceed? (y) y
|
||||
You can also pass command line arguments to set up a new project
|
||||
non-interactively. For a list of the latest options, call `create-llama --help`.
|
||||
|
||||
### Running in pro mode
|
||||
|
||||
If you prefer more advanced customization options, you can run `create-llama` in pro mode using the `--pro` flag.
|
||||
|
||||
In pro mode, instead of selecting a predefined use case, you'll be prompted to select each technical component of your project. This allows for greater flexibility in customizing your project, including:
|
||||
|
||||
- **Vector Store**: Choose from a variety of vector stores for keeping your documents, including MongoDB, Pinecone, Weaviate, Qdrant and Chroma.
|
||||
- **Tools**: Choose from a variety of agent tools (functions called by the LLM), such as:
|
||||
- Code Interpreter: Executes Python code in a secure Jupyter notebook environment
|
||||
- Artifact Code Generator: Generates code artifacts that can be run in a sandbox
|
||||
- OpenAPI Action: Facilitates requests to a provided OpenAPI schema
|
||||
- Image Generator: Creates images based on text descriptions
|
||||
- Web Search: Performs web searches to retrieve up-to-date information
|
||||
- **Data Sources**: Integrate various data sources into your chat application, including local files, websites, or database-retrieved data.
|
||||
- **Backend Options**: Besides using Next.js or FastAPI, you can also select to use Express for a more traditional Node.js application.
|
||||
- **Observability**: Choose from a variety of LLM observability tools, including LlamaTrace and Traceloop.
|
||||
|
||||
Pro mode is ideal for developers who want fine-grained control over their project's configuration and are comfortable with more technical setup options.
|
||||
|
||||
## LlamaIndex Documentation
|
||||
|
||||
- [TS/JS docs](https://ts.llamaindex.ai/)
|
||||
@@ -134,7 +111,7 @@ Pro mode is ideal for developers who want fine-grained control over their projec
|
||||
|
||||
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 TypeScript](https://github.com/run-llama/chat-ui/tree/main/packages/server)
|
||||
- [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)
|
||||
|
||||
@@ -18,20 +18,27 @@ export default tseslint.config(
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/create-llama/**"],
|
||||
rules: {
|
||||
"no-irregular-whitespace": "off",
|
||||
"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-explicit-any": [
|
||||
"error",
|
||||
{
|
||||
ignoreRestArgs: true,
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
"@typescript-eslint/no-wrapper-object-types": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
"python/**",
|
||||
"**/*.mypy_cache/**",
|
||||
"**/*.venv/**",
|
||||
"**/*.ruff_cache/**",
|
||||
"**/dist/**",
|
||||
"**/e2e/cache/**",
|
||||
"**/lib/*",
|
||||
"**/.next/**",
|
||||
"**/out/**",
|
||||
+56
-37
@@ -1,39 +1,58 @@
|
||||
{
|
||||
"name": "create-llama-monorepo",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Monorepo for create-llama",
|
||||
"keywords": [
|
||||
"rag",
|
||||
"llamaindex"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/run-llama/create-llama"
|
||||
},
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "husky",
|
||||
"new-snapshot": "pnpm -r build && changeset version --snapshot",
|
||||
"new-version": "pnpm -r build && changeset version",
|
||||
"release": "pnpm -r build && changeset publish",
|
||||
"release-snapshot": "pnpm -r build && changeset publish --tag snapshot",
|
||||
"build": "pnpm -r build",
|
||||
"e2e": "pnpm -r e2e",
|
||||
"dev": "pnpm -r dev",
|
||||
"format": "pnpm -r format",
|
||||
"format:write": "pnpm -r format:write",
|
||||
"lint": "pnpm -r lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.1",
|
||||
"husky": "^9.0.10"
|
||||
},
|
||||
"packageManager": "pnpm@9.0.5",
|
||||
"engines": {
|
||||
"node": ">=16.14.0"
|
||||
}
|
||||
"name": "create-llama-monorepo",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Monorepo for create-llama",
|
||||
"keywords": [
|
||||
"rag",
|
||||
"llamaindex"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/run-llama/create-llama"
|
||||
},
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"python/*"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "pnpm -r dev",
|
||||
"build": "pnpm -r build",
|
||||
"e2e": "pnpm -r e2e",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --ignore-unknown --cache --check .",
|
||||
"format:write": "prettier --ignore-unknown --write .",
|
||||
"prepare": "husky",
|
||||
"new-snapshot": "pnpm -r build && changeset version --snapshot",
|
||||
"new-version-python": "pnpm --filter @create-llama/llama-index-server new-version",
|
||||
"new-version": "pnpm -r build && changeset version && pnpm new-version-python",
|
||||
"release-python": "pnpm --filter @create-llama/llama-index-server release",
|
||||
"release": "pnpm -r build && changeset publish && pnpm release-python",
|
||||
"release-snapshot": "pnpm -r build && changeset publish --tag snapshot"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.1",
|
||||
"bunchee": "6.4.0",
|
||||
"husky": "^9.0.10",
|
||||
"lint-staged": "^15.2.11",
|
||||
"typescript-eslint": "^8.18.0",
|
||||
"globals": "^15.12.0",
|
||||
"eslint": "9.22.0",
|
||||
"@eslint/js": "^9.25.0",
|
||||
"eslint-config-next": "^15.1.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-react": "7.37.2",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"typescript": "^5.7.3",
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19"
|
||||
},
|
||||
"packageManager": "pnpm@9.0.5",
|
||||
"engines": {
|
||||
"node": ">=16.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"max-params": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"prefer-const": "error",
|
||||
},
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
apps/docs/i18n
|
||||
apps/docs/docs/api
|
||||
pnpm-lock.yaml
|
||||
lib/
|
||||
dist/
|
||||
.docusaurus/
|
||||
@@ -1,5 +1,104 @@
|
||||
# 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
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# 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.
|
||||
@@ -1,44 +1,34 @@
|
||||
/* 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 type { InstallTemplateArgs } from "./helpers";
|
||||
import { installTemplate } from "./helpers";
|
||||
import { templatesDir } from "./helpers/dir";
|
||||
import { toolsRequireConfig } from "./helpers/tools";
|
||||
import { configVSCode } from "./helpers/vscode";
|
||||
|
||||
export type InstallAppArgs = Omit<
|
||||
InstallTemplateArgs,
|
||||
"appName" | "root" | "isOnline" | "port"
|
||||
"appName" | "root" | "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,
|
||||
useCase,
|
||||
}: InstallAppArgs): Promise<void> {
|
||||
const root = path.resolve(appPath);
|
||||
@@ -60,9 +50,6 @@ export async function createApp({
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const useYarn = packageManager === "yarn";
|
||||
const isOnline = !useYarn || (await getOnline());
|
||||
|
||||
console.log(`Creating a new LlamaIndex app in ${green(root)}.`);
|
||||
console.log();
|
||||
|
||||
@@ -71,36 +58,18 @@ export async function createApp({
|
||||
root,
|
||||
template,
|
||||
framework,
|
||||
ui,
|
||||
packageManager,
|
||||
isOnline,
|
||||
modelConfig,
|
||||
llamaCloudKey,
|
||||
communityProjectConfig,
|
||||
llamapack,
|
||||
vectorDb,
|
||||
postInstallAction,
|
||||
dataSources,
|
||||
tools,
|
||||
useLlamaParse,
|
||||
observability,
|
||||
useCase,
|
||||
};
|
||||
|
||||
// Install backend
|
||||
await installTemplate({ ...args, backend: true });
|
||||
|
||||
if (frontend && framework === "fastapi" && template !== "llamaindexserver") {
|
||||
// install frontend
|
||||
const frontendRoot = path.join(root, ".frontend");
|
||||
await makeDir(frontendRoot);
|
||||
await installTemplate({
|
||||
...args,
|
||||
root: frontendRoot,
|
||||
framework: "nextjs",
|
||||
backend: false,
|
||||
});
|
||||
}
|
||||
await installTemplate(args);
|
||||
|
||||
await configVSCode(root, templatesDir, framework);
|
||||
|
||||
@@ -110,18 +79,6 @@ export async function createApp({
|
||||
console.log();
|
||||
}
|
||||
|
||||
if (toolsRequireConfig(tools) && template !== "llamaindexserver") {
|
||||
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}`);
|
||||
|
||||
@@ -132,8 +89,6 @@ export async function createApp({
|
||||
)} and learn how to get started.`,
|
||||
);
|
||||
|
||||
outputObservability(args.observability);
|
||||
|
||||
if (
|
||||
dataSources.some((dataSource) => dataSource.type === "file") &&
|
||||
process.platform === "linux"
|
||||
@@ -150,24 +105,3 @@ export async function createApp({
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,181 +3,44 @@ import { exec } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import util from "util";
|
||||
import { TemplateFramework, TemplateVectorDB } from "../../helpers/types";
|
||||
import { TemplateFramework, 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 = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
: "fastapi";
|
||||
const dataSource: string = process.env.DATASOURCE
|
||||
? process.env.DATASOURCE
|
||||
: "--example-file";
|
||||
const templateFramework: TemplateFramework = "fastapi";
|
||||
const vectorDb: TemplateVectorDB = process.env.VECTORDB
|
||||
? (process.env.VECTORDB as TemplateVectorDB)
|
||||
: "none";
|
||||
|
||||
// TODO: add support for other templates
|
||||
const useCases: TemplateUseCase[] = vectorDb === "llamacloud" ? [
|
||||
"agentic_rag", "deep_research", "financial_report"
|
||||
] : ALL_PYTHON_USE_CASES
|
||||
|
||||
if (
|
||||
dataSource === "--example-file" // XXX: this test provides its own data source - only trigger it on one data source (usually the CI matrix will trigger multiple data sources)
|
||||
) {
|
||||
// vectorDBs, tools, and data source combinations to test
|
||||
const vectorDbs: TemplateVectorDB[] = [
|
||||
"mongo",
|
||||
"pg",
|
||||
"pinecone",
|
||||
"milvus",
|
||||
"astra",
|
||||
"qdrant",
|
||||
"chroma",
|
||||
"weaviate",
|
||||
];
|
||||
test.describe("Mypy check", () => {
|
||||
test.describe.configure({ retries: 0 });
|
||||
|
||||
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 () => {
|
||||
test.describe("LlamaIndexServer", async () => {
|
||||
for (const useCase of useCases) {
|
||||
test(`should pass mypy for use case: ${useCase}`, async () => {
|
||||
const cwd = await createTestDir();
|
||||
const { pyprojectPath } = await createAndCheckLlamaProject({
|
||||
await createAndCheckLlamaProject({
|
||||
options: {
|
||||
cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework,
|
||||
dataSource: "--example-file",
|
||||
vectorDb,
|
||||
tools: "none",
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
observability: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
|
||||
if (vectorDb !== "none") {
|
||||
if (vectorDb === "pg") {
|
||||
expect(pyprojectContent).toContain(
|
||||
"llama-index-vector-stores-postgres",
|
||||
);
|
||||
} else {
|
||||
expect(pyprojectContent).toContain(
|
||||
`llama-index-vector-stores-${vectorDb}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Test tools
|
||||
for (const tool of toolOptions) {
|
||||
test(`Mypy check for tool: ${tool}`, async () => {
|
||||
const cwd = await createTestDir();
|
||||
const { pyprojectPath } = await createAndCheckLlamaProject({
|
||||
options: {
|
||||
cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework,
|
||||
dataSource: "--example-file",
|
||||
vectorDb: "none",
|
||||
tools: tool,
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
observability: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const pyprojectContent = fs.readFileSync(pyprojectPath, "utf-8");
|
||||
if (tool === "wikipedia.WikipediaToolSpec") {
|
||||
expect(pyprojectContent).toContain("wikipedia");
|
||||
}
|
||||
if (tool === "google.GoogleSearchToolSpec") {
|
||||
expect(pyprojectContent).toContain("google");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Test data sources
|
||||
for (const dataSource of dataSources) {
|
||||
const dataSourceType = dataSource.split(" ")[0];
|
||||
test(`Mypy check for data source: ${dataSourceType}`, async () => {
|
||||
const cwd = await createTestDir();
|
||||
const { pyprojectPath } = await createAndCheckLlamaProject({
|
||||
options: {
|
||||
cwd,
|
||||
templateType: "streaming",
|
||||
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,
|
||||
useCase,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function createAndCheckLlamaProject({
|
||||
options,
|
||||
|
||||
@@ -1,54 +1,49 @@
|
||||
/* 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";
|
||||
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";
|
||||
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
: "fastapi";
|
||||
const dataSource: string = "--example-file";
|
||||
const templateUI: TemplateUI = "shadcn";
|
||||
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
|
||||
const appType: AppType = "--frontend";
|
||||
const userMessage = "Write a blog post about physical standards for letters";
|
||||
const templateUseCases = ["financial_report", "agentic_rag", "deep_research"];
|
||||
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";
|
||||
|
||||
for (const useCase of templateUseCases) {
|
||||
test.describe(`Test use case ${useCase} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
|
||||
test.skip(
|
||||
process.platform !== "linux" ||
|
||||
process.env.DATASOURCE === "--no-files" ||
|
||||
templateFramework === "express",
|
||||
"The llamaindexserver template currently only works with nextjs, fastapi. We also only run on Linux to speed up tests.",
|
||||
);
|
||||
const userMessage = "Write a blog post about physical standards for letters";
|
||||
|
||||
for (const useCase of allUseCases) {
|
||||
test.describe(`Test use case ${useCase} ${templateFramework} ${vectorDb}`, async () => {
|
||||
let port: number;
|
||||
let cwd: string;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
// Only test without using vector db for now
|
||||
const vectorDb = "none";
|
||||
|
||||
test.beforeAll(async () => {
|
||||
port = Math.floor(Math.random() * 10000) + 10000;
|
||||
cwd = await createTestDir();
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
templateType: "llamaindexserver",
|
||||
templateFramework,
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
postInstallAction: templatePostInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
postInstallAction: isPythonLlamaDeploy ? "dependencies" : "runApp",
|
||||
useCase,
|
||||
llamaCloudProjectName,
|
||||
llamaCloudIndexName,
|
||||
});
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
@@ -61,9 +56,10 @@ for (const useCase of templateUseCases) {
|
||||
|
||||
test("Frontend should have a title", async ({ page }) => {
|
||||
test.skip(
|
||||
templatePostInstallAction !== "runApp" ||
|
||||
templateFramework === "express",
|
||||
isPythonLlamaDeploy,
|
||||
"Skip frontend tests for Python LllamaDeploy",
|
||||
);
|
||||
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible({
|
||||
timeout: 5 * 60 * 1000,
|
||||
@@ -74,11 +70,10 @@ for (const useCase of templateUseCases) {
|
||||
page,
|
||||
}) => {
|
||||
test.skip(
|
||||
templatePostInstallAction !== "runApp" ||
|
||||
useCase === "financial_report" ||
|
||||
useCase === "financial_report" ||
|
||||
useCase === "deep_research" ||
|
||||
templateFramework === "express",
|
||||
"Skip chat tests for financial report and deep research.",
|
||||
isPythonLlamaDeploy,
|
||||
"Skip chat tests for financial report and deep research. Also skip for Python LlamaDeploy",
|
||||
);
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await page.fill("form textarea", userMessage);
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { ChildProcess } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { TemplateFramework, TemplateUseCase } from "../../helpers";
|
||||
import { createTestDir, runCreateLlama } from "../utils";
|
||||
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
: "fastapi";
|
||||
const dataSource: string = process.env.DATASOURCE
|
||||
? process.env.DATASOURCE
|
||||
: "--example-file";
|
||||
const templateUseCases: TemplateUseCase[] = ["extractor", "contract_review"];
|
||||
|
||||
// The reflex template currently only works with FastAPI and files (and not on Windows)
|
||||
if (
|
||||
process.platform !== "win32" &&
|
||||
templateFramework === "fastapi" &&
|
||||
dataSource === "--example-file"
|
||||
) {
|
||||
for (const useCase of templateUseCases) {
|
||||
test.describe(`Test reflex template ${useCase} ${templateFramework} ${dataSource}`, async () => {
|
||||
let appPort: number;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
let cwd: string;
|
||||
|
||||
// Create reflex app
|
||||
test.beforeAll(async () => {
|
||||
cwd = await createTestDir();
|
||||
appPort = Math.floor(Math.random() * 10000) + 10000;
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
templateType: "reflex",
|
||||
templateFramework: "fastapi",
|
||||
dataSource: "--example-file",
|
||||
vectorDb: "none",
|
||||
port: appPort,
|
||||
postInstallAction: "runApp",
|
||||
useCase,
|
||||
});
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
appProcess.kill();
|
||||
});
|
||||
|
||||
test("App folder should exist", async () => {
|
||||
const dirExists = fs.existsSync(path.join(cwd, name));
|
||||
expect(dirExists).toBeTruthy();
|
||||
});
|
||||
test("Frontend should have a title", async ({ page }) => {
|
||||
await page.goto(`http://localhost:${appPort}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible({
|
||||
timeout: 2000 * 60,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { ChildProcess } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import type {
|
||||
TemplateFramework,
|
||||
TemplatePostInstallAction,
|
||||
TemplateUI,
|
||||
} from "../../helpers";
|
||||
import { createTestDir, runCreateLlama, type AppType } from "../utils";
|
||||
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
: "fastapi";
|
||||
const dataSource: string = process.env.DATASOURCE
|
||||
? process.env.DATASOURCE
|
||||
: "--example-file";
|
||||
const templateUI: TemplateUI = "shadcn";
|
||||
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
|
||||
|
||||
const llamaCloudProjectName = "create-llama";
|
||||
const llamaCloudIndexName = "e2e-test";
|
||||
|
||||
const appType: AppType = templateFramework === "fastapi" ? "--frontend" : "";
|
||||
const userMessage =
|
||||
dataSource !== "--no-files" ? "Physical standard for letters" : "Hello";
|
||||
|
||||
test.describe(`Test streaming template ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
|
||||
const isNode18 = process.version.startsWith("v18");
|
||||
const isLlamaCloud = dataSource === "--llamacloud";
|
||||
// llamacloud is using File API which is not supported on node 18
|
||||
if (isNode18 && isLlamaCloud) {
|
||||
test.skip(true, "Skipping tests for Node 18 and LlamaCloud data source");
|
||||
}
|
||||
|
||||
let port: number;
|
||||
let cwd: string;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
// Only test without using vector db for now
|
||||
const vectorDb = "none";
|
||||
|
||||
test.beforeAll(async () => {
|
||||
port = Math.floor(Math.random() * 10000) + 10000;
|
||||
cwd = await createTestDir();
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework,
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
postInstallAction: templatePostInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
llamaCloudProjectName,
|
||||
llamaCloudIndexName,
|
||||
});
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
});
|
||||
|
||||
test("App folder should exist", async () => {
|
||||
const dirExists = fs.existsSync(path.join(cwd, name));
|
||||
expect(dirExists).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Frontend should have a title", async ({ page }) => {
|
||||
test.skip(
|
||||
templatePostInstallAction !== "runApp" || templateFramework === "express",
|
||||
);
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Frontend should be able to submit a message and receive a response", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.skip(
|
||||
templatePostInstallAction !== "runApp" || templateFramework === "express",
|
||||
);
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await page.fill("form textarea", userMessage);
|
||||
const [response] = await Promise.all([
|
||||
page.waitForResponse(
|
||||
(res) => {
|
||||
return res.url().includes("/api/chat") && res.status() === 200;
|
||||
},
|
||||
{
|
||||
timeout: 1000 * 60,
|
||||
},
|
||||
),
|
||||
page.click("form button[type=submit]"),
|
||||
]);
|
||||
const text = await response.text();
|
||||
console.log("AI response when submitting message: ", text);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Backend frameworks should response when calling non-streaming chat API", async ({
|
||||
request,
|
||||
}) => {
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
test.skip(templateFramework === "nextjs");
|
||||
const response = await request.post(
|
||||
`http://localhost:${port}/api/chat/request`,
|
||||
{
|
||||
data: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: userMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
const text = await response.text();
|
||||
console.log("AI response when calling API: ", text);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
// clean processes
|
||||
test.afterAll(async () => {
|
||||
appProcess?.kill();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
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();
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -3,103 +3,88 @@ import { exec } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import util from "util";
|
||||
import { TemplateFramework, TemplateVectorDB } from "../../helpers/types";
|
||||
import {
|
||||
TemplateFramework,
|
||||
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 = 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",
|
||||
];
|
||||
const templateFramework: TemplateFramework = "nextjs";
|
||||
const vectorDb: TemplateVectorDB = process.env.VECTORDB
|
||||
? (process.env.VECTORDB as TemplateVectorDB)
|
||||
: "none";
|
||||
|
||||
test.describe("Test resolve TS dependencies", () => {
|
||||
// Test vector DBs without LlamaParse
|
||||
for (const vectorDb of vectorDbs) {
|
||||
const optionDescription = `vectorDb: ${vectorDb}, dataSource: ${dataSource}`;
|
||||
test.describe.configure({ retries: 0 });
|
||||
|
||||
test(`Vector DB test - ${optionDescription}`, async () => {
|
||||
await runTest(vectorDb, false);
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Test LlamaParse with vectorDB 'none'
|
||||
test(`LlamaParse test - vectorDb: none, dataSource: ${dataSource}, llamaParse: true`, async () => {
|
||||
await runTest("none", true);
|
||||
});
|
||||
|
||||
async function runTest(
|
||||
vectorDb: TemplateVectorDB | "none",
|
||||
useLlamaParse: boolean,
|
||||
) {
|
||||
const cwd = await createTestDir();
|
||||
|
||||
const result = await runCreateLlama({
|
||||
cwd: cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework: templateFramework,
|
||||
dataSource: dataSource,
|
||||
vectorDb: vectorDb,
|
||||
port: 3000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: templateFramework === "nextjs" ? "" : "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
tools: undefined,
|
||||
useLlamaParse: useLlamaParse,
|
||||
});
|
||||
const name = result.projectName;
|
||||
|
||||
// Check if the app folder exists
|
||||
const appDir = path.join(cwd, name);
|
||||
const dirExists = fs.existsSync(appDir);
|
||||
expect(dirExists).toBeTruthy();
|
||||
|
||||
// Install dependencies using pnpm
|
||||
try {
|
||||
const { stderr: installStderr } = await execAsync(
|
||||
"pnpm install --prefer-offline --ignore-workspace",
|
||||
{
|
||||
cwd: appDir,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error installing dependencies:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Run tsc type check and capture the output
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(
|
||||
"pnpm exec tsc -b --diagnostics",
|
||||
{
|
||||
cwd: appDir,
|
||||
},
|
||||
);
|
||||
// Check if there's any error output
|
||||
expect(stderr).toBeFalsy();
|
||||
|
||||
// Log the stdout for debugging purposes
|
||||
console.log("TypeScript type-check output:", stdout);
|
||||
} catch (error) {
|
||||
console.error("Error running tsc:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,9 @@ 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;
|
||||
@@ -20,72 +16,36 @@ export type CreateLlamaResult = {
|
||||
|
||||
export type RunCreateLlamaOptions = {
|
||||
cwd: string;
|
||||
templateType: TemplateType;
|
||||
templateFramework: TemplateFramework;
|
||||
dataSource: string;
|
||||
vectorDb: TemplateVectorDB;
|
||||
port: number;
|
||||
postInstallAction: TemplatePostInstallAction;
|
||||
templateUI?: TemplateUI;
|
||||
appType?: AppType;
|
||||
useCase: string;
|
||||
llamaCloudProjectName?: string;
|
||||
llamaCloudIndexName?: string;
|
||||
tools?: string;
|
||||
useLlamaParse?: boolean;
|
||||
observability?: string;
|
||||
useCase?: string;
|
||||
};
|
||||
|
||||
export async function runCreateLlama({
|
||||
cwd,
|
||||
templateType,
|
||||
templateFramework,
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
postInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
useCase,
|
||||
llamaCloudProjectName,
|
||||
llamaCloudIndexName,
|
||||
tools,
|
||||
useLlamaParse,
|
||||
observability,
|
||||
useCase,
|
||||
}: RunCreateLlamaOptions): Promise<CreateLlamaResult> {
|
||||
if (!process.env.OPENAI_API_KEY || !process.env.LLAMA_CLOUD_API_KEY) {
|
||||
throw new Error(
|
||||
"Setting the OPENAI_API_KEY and LLAMA_CLOUD_API_KEY is mandatory to run tests",
|
||||
);
|
||||
}
|
||||
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 name = [templateFramework, useCase, vectorDb].join("-");
|
||||
const commandArgs = [
|
||||
"create-llama",
|
||||
name,
|
||||
"--template",
|
||||
templateType,
|
||||
"--framework",
|
||||
templateFramework,
|
||||
...dataSourceArgs,
|
||||
"--vector-db",
|
||||
vectorDb,
|
||||
"--use-npm",
|
||||
@@ -93,35 +53,10 @@ export async function runCreateLlama({
|
||||
port,
|
||||
"--post-install-action",
|
||||
postInstallAction,
|
||||
"--tools",
|
||||
tools ?? "none",
|
||||
"--observability",
|
||||
"none",
|
||||
"--use-case",
|
||||
useCase,
|
||||
];
|
||||
|
||||
if (templateUI) {
|
||||
commandArgs.push("--ui", templateUI);
|
||||
}
|
||||
if (appType) {
|
||||
commandArgs.push(appType);
|
||||
}
|
||||
if (useLlamaParse) {
|
||||
commandArgs.push("--use-llama-parse");
|
||||
} else {
|
||||
commandArgs.push("--no-llama-parse");
|
||||
}
|
||||
if (observability) {
|
||||
commandArgs.push("--observability", observability);
|
||||
}
|
||||
if (
|
||||
(templateType === "multiagent" ||
|
||||
templateType === "reflex" ||
|
||||
templateType === "llamaindexserver") &&
|
||||
useCase
|
||||
) {
|
||||
commandArgs.push("--use-case", useCase);
|
||||
}
|
||||
|
||||
const command = commandArgs.join(" ");
|
||||
console.log(`running command '${command}' in ${cwd}`);
|
||||
const appProcess = exec(command, {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
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,4 +1,3 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { async as glob } from "fast-glob";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import yaml, { Document } from "yaml";
|
||||
import { templatesDir } from "./dir";
|
||||
import { DbSourceConfig, TemplateDataSource, WebSourceConfig } from "./types";
|
||||
import { TemplateDataSource } from "./types";
|
||||
|
||||
export const EXAMPLE_FILE: TemplateDataSource = {
|
||||
type: "file",
|
||||
@@ -51,92 +49,3 @@ export const AI_REPORTS: TemplateDataSource = {
|
||||
filename: "EPRS_ATA_2024_760392_EN.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,29 +1,17 @@
|
||||
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";
|
||||
|
||||
const DEFAULT_SYSTEM_PROMPT =
|
||||
"You are a helpful assistant who helps users with their questions.";
|
||||
|
||||
const DATA_SOURCES_PROMPT =
|
||||
"You have access to a knowledge base including the facts that you should start with to find the answer for the user question. Use the query engine tool to retrieve the facts from the knowledge base.";
|
||||
|
||||
export type EnvVar = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
value?: string;
|
||||
};
|
||||
import { USE_CASE_CONFIGS } from "./use-case";
|
||||
|
||||
const renderEnvVar = (envVars: EnvVar[]): string => {
|
||||
return envVars.reduce(
|
||||
@@ -181,7 +169,7 @@ const getVectorDBEnvs = (
|
||||
]
|
||||
: []),
|
||||
];
|
||||
case "chroma":
|
||||
case "chroma": {
|
||||
const envs = [
|
||||
{
|
||||
name: "CHROMA_COLLECTION",
|
||||
@@ -206,6 +194,7 @@ Otherwise, use CHROMA_HOST and CHROMA_PORT config above`,
|
||||
});
|
||||
}
|
||||
return envs;
|
||||
}
|
||||
case "weaviate":
|
||||
return [
|
||||
{
|
||||
@@ -236,13 +225,16 @@ Otherwise, use CHROMA_HOST and CHROMA_PORT config above`,
|
||||
}
|
||||
};
|
||||
|
||||
const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
|
||||
const getModelEnvs = (
|
||||
modelConfig: ModelConfig,
|
||||
framework: TemplateFramework,
|
||||
template: TemplateType,
|
||||
useCase: TemplateUseCase,
|
||||
): EnvVar[] => {
|
||||
const isPythonLlamaDeploy =
|
||||
framework === "fastapi" && template === "llamaindexserver";
|
||||
|
||||
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.",
|
||||
@@ -253,15 +245,25 @@ const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
|
||||
description: "Name of the embedding model to use.",
|
||||
value: modelConfig.embeddingModel,
|
||||
},
|
||||
{
|
||||
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).",
|
||||
},
|
||||
...(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 ?? []),
|
||||
...(modelConfig.provider === "openai"
|
||||
? [
|
||||
{
|
||||
@@ -269,14 +271,18 @@ const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
|
||||
description: "The OpenAI API key to use.",
|
||||
value: modelConfig.apiKey,
|
||||
},
|
||||
{
|
||||
name: "LLM_TEMPERATURE",
|
||||
description: "Temperature for sampling from the model.",
|
||||
},
|
||||
{
|
||||
name: "LLM_MAX_TOKENS",
|
||||
description: "Maximum number of tokens to generate.",
|
||||
},
|
||||
...(isPythonLlamaDeploy
|
||||
? []
|
||||
: [
|
||||
{
|
||||
name: "LLM_TEMPERATURE",
|
||||
description: "Temperature for sampling from the model.",
|
||||
},
|
||||
{
|
||||
name: "LLM_MAX_TOKENS",
|
||||
description: "Maximum number of tokens to generate.",
|
||||
},
|
||||
]),
|
||||
]
|
||||
: []),
|
||||
...(modelConfig.provider === "anthropic"
|
||||
@@ -385,26 +391,12 @@ const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
|
||||
|
||||
const getFrameworkEnvs = (
|
||||
framework: TemplateFramework,
|
||||
template: TemplateType,
|
||||
template?: TemplateType,
|
||||
port?: number,
|
||||
): EnvVar[] => {
|
||||
const sPort = port?.toString() || "8000";
|
||||
const result: EnvVar[] =
|
||||
template !== "llamaindexserver"
|
||||
? [
|
||||
{
|
||||
name: "FILESERVER_URL_PREFIX",
|
||||
description:
|
||||
"FILESERVER_URL_PREFIX is the URL prefix of the server storing the images generated by the interpreter.",
|
||||
value:
|
||||
framework === "nextjs"
|
||||
? // FIXME: if we are using nextjs, port should be 3000
|
||||
"http://localhost:3000/api/files"
|
||||
: `http://localhost:${sPort}/api/files`,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
if (framework === "fastapi") {
|
||||
const result: EnvVar[] = [];
|
||||
if (framework === "fastapi" && template !== "llamaindexserver") {
|
||||
result.push(
|
||||
...[
|
||||
{
|
||||
@@ -420,149 +412,10 @@ const getFrameworkEnvs = (
|
||||
],
|
||||
);
|
||||
}
|
||||
if (framework === "nextjs" && template !== "llamaindexserver") {
|
||||
result.push({
|
||||
name: "NEXT_PUBLIC_CHAT_API",
|
||||
description:
|
||||
"The API for the chat endpoint. Set when using a custom backend (e.g. Express). Use full URL like http://localhost:8000/api/chat",
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
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 systemPromptEnv: EnvVar[] = [];
|
||||
// build tool system prompt by merging all tool system prompts
|
||||
// multiagent template doesn't need system prompt
|
||||
if (template !== "multiagent") {
|
||||
let toolSystemPrompt = "";
|
||||
tools?.forEach((tool) => {
|
||||
const toolSystemPromptEnv = tool.envVars?.find(
|
||||
(env) => env.name === TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
);
|
||||
if (toolSystemPromptEnv) {
|
||||
toolSystemPrompt += toolSystemPromptEnv.value + "\n";
|
||||
}
|
||||
});
|
||||
|
||||
const systemPrompt =
|
||||
'"' +
|
||||
DEFAULT_SYSTEM_PROMPT +
|
||||
(dataSources?.length ? `\n${DATA_SOURCES_PROMPT}` : "") +
|
||||
(toolSystemPrompt ? `\n${toolSystemPrompt}` : "") +
|
||||
'"';
|
||||
|
||||
systemPromptEnv.push({
|
||||
name: "SYSTEM_PROMPT",
|
||||
description: "The system prompt for the AI model.",
|
||||
value: systemPrompt,
|
||||
});
|
||||
}
|
||||
if (tools?.length == 0 && (dataSources?.length ?? 0 > 0)) {
|
||||
const citationPrompt = `'You have provided information from a knowledge base that has been passed to you in nodes of information.
|
||||
Each node has useful metadata such as node ID, file name, page, etc.
|
||||
Please add the citation to the data node for each sentence or paragraph that you reference in the provided information.
|
||||
The citation format is: . [citation:<node_id>]()
|
||||
Where the <node_id> is the unique identifier of the data node.
|
||||
|
||||
Example:
|
||||
We have two nodes:
|
||||
node_id: xyz
|
||||
file_name: llama.pdf
|
||||
|
||||
node_id: abc
|
||||
file_name: animal.pdf
|
||||
|
||||
User question: Tell me a fun fact about Llama.
|
||||
Your answer:
|
||||
A baby llama is called "Cria" [citation:xyz]().
|
||||
It often live in desert [citation:abc]().
|
||||
It\\'s cute animal.
|
||||
'`;
|
||||
systemPromptEnv.push({
|
||||
name: "SYSTEM_CITATION_PROMPT",
|
||||
description:
|
||||
"An additional system prompt to add citation when responding to user questions.",
|
||||
value: citationPrompt,
|
||||
});
|
||||
}
|
||||
|
||||
return systemPromptEnv;
|
||||
};
|
||||
|
||||
const getTemplateEnvs = (template?: TemplateType): EnvVar[] => {
|
||||
const nextQuestionEnvs: EnvVar[] = [
|
||||
{
|
||||
name: "NEXT_QUESTION_PROMPT",
|
||||
description: `Customize prompt to generate the next question suggestions based on the conversation history.
|
||||
Disable this prompt to disable the next question suggestions feature.`,
|
||||
value: `"You're a helpful assistant! Your task is to suggest the next question that user might ask.
|
||||
Here is the conversation history
|
||||
---------------------
|
||||
{conversation}
|
||||
---------------------
|
||||
Given the conversation history, please give me 3 questions that user might ask next!
|
||||
Your answer should be wrapped in three sticks which follows the following format:
|
||||
\`\`\`
|
||||
<question 1>
|
||||
<question 2>
|
||||
<question 3>
|
||||
\`\`\`"`,
|
||||
},
|
||||
];
|
||||
|
||||
if (template === "multiagent" || template === "streaming") {
|
||||
return nextQuestionEnvs;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const getObservabilityEnvs = (
|
||||
observability?: TemplateObservability,
|
||||
): EnvVar[] => {
|
||||
if (observability === "llamatrace") {
|
||||
return [
|
||||
{
|
||||
name: "PHOENIX_API_KEY",
|
||||
description:
|
||||
"API key for LlamaTrace observability. Retrieve from https://llamatrace.com/login",
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const createBackendEnvFile = async (
|
||||
root: string,
|
||||
opts: Pick<
|
||||
@@ -574,9 +427,8 @@ export const createBackendEnvFile = async (
|
||||
| "dataSources"
|
||||
| "template"
|
||||
| "port"
|
||||
| "tools"
|
||||
| "observability"
|
||||
| "useLlamaParse"
|
||||
| "useCase"
|
||||
>,
|
||||
) => {
|
||||
// Init env values
|
||||
@@ -592,45 +444,27 @@ export const createBackendEnvFile = async (
|
||||
]
|
||||
: []),
|
||||
...getVectorDBEnvs(opts.vectorDb, opts.framework, opts.template),
|
||||
...getToolEnvs(opts.tools),
|
||||
...getFrameworkEnvs(opts.framework, opts.template, opts.port),
|
||||
// Add environment variables of each component
|
||||
...(opts.template === "llamaindexserver"
|
||||
? [
|
||||
{
|
||||
name: "OPENAI_API_KEY",
|
||||
description: "The OpenAI API key to use.",
|
||||
value: opts.modelConfig.apiKey,
|
||||
},
|
||||
]
|
||||
: [
|
||||
// don't use this stuff for llama-indexserver
|
||||
...getModelEnvs(opts.modelConfig),
|
||||
...getEngineEnvs(),
|
||||
...getTemplateEnvs(opts.template),
|
||||
...getObservabilityEnvs(opts.observability),
|
||||
...getSystemPromptEnv(opts.tools, opts.dataSources, opts.template),
|
||||
]),
|
||||
...getModelEnvs(
|
||||
opts.modelConfig,
|
||||
opts.framework,
|
||||
opts.template,
|
||||
opts.useCase,
|
||||
),
|
||||
];
|
||||
// Render and write env file
|
||||
const content = renderEnvVar(envVars);
|
||||
await fs.writeFile(path.join(root, envFileName), content);
|
||||
|
||||
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);
|
||||
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,4 +1,3 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
@@ -4,14 +4,10 @@ import path from "path";
|
||||
import picocolors, { cyan } from "picocolors";
|
||||
|
||||
import fsExtra from "fs-extra";
|
||||
import { writeLoadersConfig } from "./datasources";
|
||||
import { createBackendEnvFile, createFrontendEnvFile } from "./env-variables";
|
||||
import { createBackendEnvFile } from "./env-variables";
|
||||
import { PackageManager } from "./get-pkg-manager";
|
||||
import { installLlamapackProject } from "./llama-pack";
|
||||
import { makeDir } from "./make-dir";
|
||||
import { installPythonTemplate } from "./python";
|
||||
import { downloadAndExtractRepo } from "./repo";
|
||||
import { ConfigFileType, writeToolsConfig } from "./tools";
|
||||
import {
|
||||
FileSourceConfig,
|
||||
InstallTemplateArgs,
|
||||
@@ -56,6 +52,7 @@ const checkForGenerateScript = (
|
||||
async function generateContextData(
|
||||
framework: TemplateFramework,
|
||||
modelConfig: ModelConfig,
|
||||
dataSources: TemplateDataSource[],
|
||||
packageManager?: PackageManager,
|
||||
vectorDb?: TemplateVectorDB,
|
||||
llamaCloudKey?: string,
|
||||
@@ -96,7 +93,12 @@ async function generateContextData(
|
||||
}
|
||||
} else {
|
||||
console.log(`Running ${runGenerate} to generate the context data.`);
|
||||
await callPackageManager(packageManager, true, ["run", "generate"]);
|
||||
|
||||
const shouldRunGenerate = dataSources.length > 0;
|
||||
|
||||
if (shouldRunGenerate) {
|
||||
await callPackageManager(packageManager, true, ["run", "generate"]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -115,8 +117,13 @@ const downloadFile = async (url: string, destPath: string) => {
|
||||
const prepareContextData = async (
|
||||
root: string,
|
||||
dataSources: TemplateDataSource[],
|
||||
isPythonLlamaDeploy: boolean,
|
||||
) => {
|
||||
await makeDir(path.join(root, "data"));
|
||||
const dataDir = isPythonLlamaDeploy
|
||||
? path.join(root, "ui", "data")
|
||||
: path.join(root, "data");
|
||||
|
||||
await makeDir(dataDir);
|
||||
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
|
||||
@@ -126,8 +133,7 @@ const prepareContextData = async (
|
||||
dataSourceConfig.url.toString(),
|
||||
);
|
||||
const destPath = path.join(
|
||||
root,
|
||||
"data",
|
||||
dataDir,
|
||||
dataSourceConfig.filename ??
|
||||
path.basename(dataSourceConfig.url.toString()),
|
||||
);
|
||||
@@ -135,107 +141,57 @@ const prepareContextData = async (
|
||||
} else {
|
||||
// Copy local data
|
||||
console.log("Copying data from path:", dataSourceConfig.path);
|
||||
const destPath = path.join(
|
||||
root,
|
||||
"data",
|
||||
path.basename(dataSourceConfig.path),
|
||||
);
|
||||
const destPath = path.join(dataDir, path.basename(dataSourceConfig.path));
|
||||
await fsExtra.copy(dataSourceConfig.path, destPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 },
|
||||
) => {
|
||||
export const installTemplate = async (props: InstallTemplateArgs) => {
|
||||
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);
|
||||
} else {
|
||||
await installTSTemplate(props);
|
||||
}
|
||||
|
||||
// write configurations
|
||||
if (props.template !== "llamaindexserver") {
|
||||
await writeToolsConfig(
|
||||
props.root,
|
||||
props.tools,
|
||||
props.framework === "fastapi" ? ConfigFileType.YAML : ConfigFileType.JSON,
|
||||
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(
|
||||
props.root,
|
||||
props.dataSources.filter((ds) => ds.type === "file"),
|
||||
isPythonLlamaDeploy,
|
||||
);
|
||||
|
||||
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.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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (props.backend) {
|
||||
// This is a backend, so we need to copy the test data and create the env file.
|
||||
|
||||
// Copy the environment file to the target directory.
|
||||
if (props.template !== "community" && props.template !== "llamapack") {
|
||||
await createBackendEnvFile(props.root, props);
|
||||
}
|
||||
|
||||
await 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
|
||||
if (!isPythonLlamaDeploy) {
|
||||
// Create outputs directory (llama-deploy doesn't need this)
|
||||
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,4 +1,3 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import spawn from "cross-spawn";
|
||||
import { yellow } from "picocolors";
|
||||
import type { PackageManager } from "./get-pkg-manager";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { blue, green } from "picocolors";
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
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();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
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,36 +0,0 @@
|
||||
/* 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,17 +31,9 @@ const EMBEDDING_MODELS: Record<HuggingFaceEmbeddingModelType, ModelData> = {
|
||||
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
|
||||
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
|
||||
|
||||
type AnthropicQuestionsParams = {
|
||||
apiKey?: string;
|
||||
askModels: boolean;
|
||||
};
|
||||
|
||||
export async function askAnthropicQuestions({
|
||||
askModels,
|
||||
apiKey,
|
||||
}: AnthropicQuestionsParams): Promise<ModelConfigParams> {
|
||||
export async function askAnthropicQuestions(): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey,
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: DEFAULT_DIMENSIONS,
|
||||
@@ -69,35 +61,33 @@ export async function askAnthropicQuestions({
|
||||
config.apiKey = key || process.env.ANTHROPIC_API_KEY;
|
||||
}
|
||||
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which LLM model would you like to use?",
|
||||
choices: MODELS.map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.model = model;
|
||||
const { 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, ModelConfigQuestionsParams } from ".";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers } from "../../questions/utils";
|
||||
|
||||
const ALL_AZURE_OPENAI_CHAT_MODELS: Record<string, { openAIModel: string }> = {
|
||||
@@ -51,12 +51,9 @@ const ALL_AZURE_OPENAI_EMBEDDING_MODELS: Record<
|
||||
const DEFAULT_MODEL = "gpt-4o";
|
||||
const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large";
|
||||
|
||||
export async function askAzureQuestions({
|
||||
openAiKey,
|
||||
askModels,
|
||||
}: ModelConfigQuestionsParams): Promise<ModelConfigParams> {
|
||||
export async function askAzureQuestions(): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey: openAiKey || process.env.AZURE_OPENAI_KEY,
|
||||
apiKey: process.env.AZURE_OPENAI_KEY,
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: getDimensions(DEFAULT_EMBEDDING_MODEL),
|
||||
@@ -66,32 +63,30 @@ export async function askAzureQuestions({
|
||||
},
|
||||
};
|
||||
|
||||
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 { 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,7 +2,15 @@ import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions/utils";
|
||||
|
||||
const MODELS = ["gemini-1.5-pro-latest", "gemini-pro", "gemini-pro-vision"];
|
||||
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",
|
||||
];
|
||||
type ModelData = {
|
||||
dimensions: number;
|
||||
};
|
||||
@@ -15,17 +23,9 @@ const DEFAULT_MODEL = MODELS[0];
|
||||
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
|
||||
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
|
||||
|
||||
type GeminiQuestionsParams = {
|
||||
apiKey?: string;
|
||||
askModels: boolean;
|
||||
};
|
||||
|
||||
export async function askGeminiQuestions({
|
||||
askModels,
|
||||
apiKey,
|
||||
}: GeminiQuestionsParams): Promise<ModelConfigParams> {
|
||||
export async function askGeminiQuestions(): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey,
|
||||
apiKey: process.env.GOOGLE_API_KEY,
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: DEFAULT_DIMENSIONS,
|
||||
@@ -53,32 +53,30 @@ export async function askGeminiQuestions({
|
||||
config.apiKey = key || process.env.GOOGLE_API_KEY;
|
||||
}
|
||||
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which LLM model would you like to use?",
|
||||
choices: MODELS.map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.model = model;
|
||||
const { 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,17 +71,9 @@ const EMBEDDING_MODELS: Record<HuggingFaceEmbeddingModelType, ModelData> = {
|
||||
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
|
||||
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
|
||||
|
||||
type GroqQuestionsParams = {
|
||||
apiKey?: string;
|
||||
askModels: boolean;
|
||||
};
|
||||
|
||||
export async function askGroqQuestions({
|
||||
askModels,
|
||||
apiKey,
|
||||
}: GroqQuestionsParams): Promise<ModelConfigParams> {
|
||||
export async function askGroqQuestions(): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey,
|
||||
apiKey: process.env.GROQ_API_KEY,
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: DEFAULT_DIMENSIONS,
|
||||
@@ -109,37 +101,35 @@ export async function askGroqQuestions({
|
||||
config.apiKey = key || process.env.GROQ_API_KEY;
|
||||
}
|
||||
|
||||
if (askModels) {
|
||||
const modelChoices = await getAvailableModelChoicesGroq(config.apiKey!);
|
||||
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,13 +21,7 @@ const DEFAULT_MODEL = MODELS[0];
|
||||
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
|
||||
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
|
||||
|
||||
type HuggingfaceQuestionsParams = {
|
||||
askModels: boolean;
|
||||
};
|
||||
|
||||
export async function askHuggingfaceQuestions({
|
||||
askModels,
|
||||
}: HuggingfaceQuestionsParams): Promise<ModelConfigParams> {
|
||||
export async function askHuggingfaceQuestions(): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
@@ -37,32 +31,30 @@ export async function askHuggingfaceQuestions({
|
||||
},
|
||||
};
|
||||
|
||||
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 { 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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import prompts from "prompts";
|
||||
import { questionHandlers } from "../../questions/utils";
|
||||
import { ModelConfig, ModelProvider, TemplateFramework } from "../types";
|
||||
import { ModelConfig, TemplateFramework } from "../types";
|
||||
import { askAnthropicQuestions } from "./anthropic";
|
||||
import { askAzureQuestions } from "./azure";
|
||||
import { askGeminiQuestions } from "./gemini";
|
||||
@@ -11,81 +11,68 @@ 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" },
|
||||
];
|
||||
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 } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "provider",
|
||||
message: "Which model provider would you like to use",
|
||||
choices: choices,
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
modelProvider = provider;
|
||||
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({ askModels });
|
||||
modelConfig = await askOllamaQuestions();
|
||||
break;
|
||||
case "groq":
|
||||
modelConfig = await askGroqQuestions({ askModels });
|
||||
modelConfig = await askGroqQuestions();
|
||||
break;
|
||||
case "anthropic":
|
||||
modelConfig = await askAnthropicQuestions({ askModels });
|
||||
modelConfig = await askAnthropicQuestions();
|
||||
break;
|
||||
case "gemini":
|
||||
modelConfig = await askGeminiQuestions({ askModels });
|
||||
modelConfig = await askGeminiQuestions();
|
||||
break;
|
||||
case "mistral":
|
||||
modelConfig = await askMistralQuestions({ askModels });
|
||||
modelConfig = await askMistralQuestions();
|
||||
break;
|
||||
case "azure-openai":
|
||||
modelConfig = await askAzureQuestions({ askModels });
|
||||
modelConfig = await askAzureQuestions();
|
||||
break;
|
||||
case "t-systems":
|
||||
modelConfig = await askLLMHubQuestions({ askModels });
|
||||
modelConfig = await askLLMHubQuestions();
|
||||
break;
|
||||
case "huggingface":
|
||||
modelConfig = await askHuggingfaceQuestions({ askModels });
|
||||
modelConfig = await askHuggingfaceQuestions();
|
||||
break;
|
||||
default:
|
||||
modelConfig = await askOpenAIQuestions({
|
||||
openAiKey,
|
||||
askModels,
|
||||
});
|
||||
modelConfig = await askOpenAIQuestions();
|
||||
}
|
||||
return {
|
||||
...modelConfig,
|
||||
|
||||
@@ -31,17 +31,9 @@ const LLMHUB_EMBEDDING_MODELS = [
|
||||
"text-embedding-bge-m3",
|
||||
];
|
||||
|
||||
type LLMHubQuestionsParams = {
|
||||
apiKey?: string;
|
||||
askModels: boolean;
|
||||
};
|
||||
|
||||
export async function askLLMHubQuestions({
|
||||
askModels,
|
||||
apiKey,
|
||||
}: LLMHubQuestionsParams): Promise<ModelConfigParams> {
|
||||
export async function askLLMHubQuestions(): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey,
|
||||
apiKey: process.env.T_SYSTEMS_LLMHUB_API_KEY,
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: getDimensions(DEFAULT_EMBEDDING_MODEL),
|
||||
@@ -61,11 +53,10 @@ export async function askLLMHubQuestions({
|
||||
{
|
||||
type: "text",
|
||||
name: "key",
|
||||
message: askModels
|
||||
? "Please provide your LLMHub API key (or leave blank to use T_SYSTEMS_LLMHUB_API_KEY env variable):"
|
||||
: "Please provide your LLMHub API key (leave blank to skip):",
|
||||
message:
|
||||
"Please provide your LLMHub API key (or leave blank to use T_SYSTEMS_LLMHUB_API_KEY env variable):",
|
||||
validate: (value: string) => {
|
||||
if (askModels && !value) {
|
||||
if (!value) {
|
||||
if (process.env.T_SYSTEMS_LLMHUB_API_KEY) {
|
||||
return true;
|
||||
}
|
||||
@@ -79,32 +70,30 @@ export async function askLLMHubQuestions({
|
||||
config.apiKey = key || process.env.T_SYSTEMS_LLMHUB_API_KEY;
|
||||
}
|
||||
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which LLM model would you like to use?",
|
||||
choices: await getAvailableModelChoices(false, config.apiKey),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.model = model;
|
||||
const { 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,17 +14,9 @@ const DEFAULT_MODEL = MODELS[0];
|
||||
const DEFAULT_EMBEDDING_MODEL = Object.keys(EMBEDDING_MODELS)[0];
|
||||
const DEFAULT_DIMENSIONS = Object.values(EMBEDDING_MODELS)[0].dimensions;
|
||||
|
||||
type MistralQuestionsParams = {
|
||||
apiKey?: string;
|
||||
askModels: boolean;
|
||||
};
|
||||
|
||||
export async function askMistralQuestions({
|
||||
askModels,
|
||||
apiKey,
|
||||
}: MistralQuestionsParams): Promise<ModelConfigParams> {
|
||||
export async function askMistralQuestions(): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey,
|
||||
apiKey: process.env.MISTRAL_API_KEY,
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: DEFAULT_DIMENSIONS,
|
||||
@@ -52,32 +44,30 @@ export async function askMistralQuestions({
|
||||
config.apiKey = key || process.env.MISTRAL_API_KEY;
|
||||
}
|
||||
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which LLM model would you like to use?",
|
||||
choices: MODELS.map(toChoice),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.model = model;
|
||||
const { 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,13 +17,7 @@ const EMBEDDING_MODELS: Record<string, ModelData> = {
|
||||
};
|
||||
const DEFAULT_EMBEDDING_MODEL: string = Object.keys(EMBEDDING_MODELS)[0];
|
||||
|
||||
type OllamaQuestionsParams = {
|
||||
askModels: boolean;
|
||||
};
|
||||
|
||||
export async function askOllamaQuestions({
|
||||
askModels,
|
||||
}: OllamaQuestionsParams): Promise<ModelConfigParams> {
|
||||
export async function askOllamaQuestions(): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
@@ -33,34 +27,32 @@ export async function askOllamaQuestions({
|
||||
},
|
||||
};
|
||||
|
||||
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 { 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,8 +2,7 @@ import got from "got";
|
||||
import ora from "ora";
|
||||
import { red } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams, ModelConfigQuestionsParams } from ".";
|
||||
import { isCI } from "../../questions";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers } from "../../questions/utils";
|
||||
|
||||
const OPENAI_API_URL = "https://api.openai.com/v1";
|
||||
@@ -11,12 +10,9 @@ 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({
|
||||
openAiKey,
|
||||
askModels,
|
||||
}: ModelConfigQuestionsParams): Promise<ModelConfigParams> {
|
||||
export async function askOpenAIQuestions(): Promise<ModelConfigParams> {
|
||||
const config: ModelConfigParams = {
|
||||
apiKey: openAiKey,
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
model: DEFAULT_MODEL,
|
||||
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
||||
dimensions: getDimensions(DEFAULT_EMBEDDING_MODEL),
|
||||
@@ -31,16 +27,15 @@ export async function askOpenAIQuestions({
|
||||
},
|
||||
};
|
||||
|
||||
if (!config.apiKey && !isCI) {
|
||||
if (!config.apiKey) {
|
||||
const { key } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "key",
|
||||
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):",
|
||||
message:
|
||||
"Please provide your OpenAI API key (or leave blank to use OPENAI_API_KEY env variable):",
|
||||
validate: (value: string) => {
|
||||
if (askModels && !value) {
|
||||
if (!value) {
|
||||
if (process.env.OPENAI_API_KEY) {
|
||||
return true;
|
||||
}
|
||||
@@ -54,32 +49,30 @@ export async function askOpenAIQuestions({
|
||||
config.apiKey = key || process.env.OPENAI_API_KEY;
|
||||
}
|
||||
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which LLM model would you like to use?",
|
||||
choices: await getAvailableModelChoices(false, config.apiKey),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
config.model = model;
|
||||
const { 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;
|
||||
}
|
||||
|
||||
@@ -7,33 +7,33 @@ import { isUvAvailable, tryUvSync } from "./uv";
|
||||
|
||||
import { assetRelocator, copy } from "./copy";
|
||||
import { templatesDir } from "./dir";
|
||||
import { Tool } from "./tools";
|
||||
import {
|
||||
InstallTemplateArgs,
|
||||
ModelConfig,
|
||||
TemplateDataSource,
|
||||
TemplateObservability,
|
||||
TemplateType,
|
||||
TemplateVectorDB,
|
||||
} from "./types";
|
||||
|
||||
interface Dependency {
|
||||
name: string;
|
||||
version?: string;
|
||||
extras?: string[];
|
||||
constraints?: Record<string, string>;
|
||||
}
|
||||
import { Dependency, InstallTemplateArgs } from "./types";
|
||||
import { USE_CASE_CONFIGS } from "./use-case";
|
||||
|
||||
const getAdditionalDependencies = (
|
||||
modelConfig: ModelConfig,
|
||||
vectorDb?: TemplateVectorDB,
|
||||
dataSources?: TemplateDataSource[],
|
||||
tools?: Tool[],
|
||||
templateType?: TemplateType,
|
||||
observability?: TemplateObservability,
|
||||
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": {
|
||||
@@ -93,6 +93,10 @@ const getAdditionalDependencies = (
|
||||
name: "llama-index-vector-stores-chroma",
|
||||
version: ">=0.4.0,<0.5.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "onnxruntime",
|
||||
version: "<1.22.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "weaviate": {
|
||||
@@ -146,14 +150,6 @@ const getAdditionalDependencies = (
|
||||
}
|
||||
}
|
||||
|
||||
// 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({
|
||||
@@ -166,20 +162,14 @@ const getAdditionalDependencies = (
|
||||
});
|
||||
break;
|
||||
case "openai":
|
||||
if (templateType !== "multiagent") {
|
||||
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",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-agent-openai",
|
||||
version: ">=0.4.0,<0.5.0",
|
||||
});
|
||||
}
|
||||
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({
|
||||
@@ -203,12 +193,12 @@ const getAdditionalDependencies = (
|
||||
break;
|
||||
case "gemini":
|
||||
dependencies.push({
|
||||
name: "llama-index-llms-gemini",
|
||||
version: ">=0.4.0,<0.5.0",
|
||||
name: "llama-index-llms-google-genai",
|
||||
version: ">=0.2.0,<0.3.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "llama-index-embeddings-gemini",
|
||||
version: ">=0.3.0,<0.4.0",
|
||||
name: "llama-index-embeddings-google-genai",
|
||||
version: ">=0.2.0,<0.3.0",
|
||||
});
|
||||
break;
|
||||
case "mistral":
|
||||
@@ -258,35 +248,18 @@ const getAdditionalDependencies = (
|
||||
break;
|
||||
}
|
||||
|
||||
if (observability && observability !== "none") {
|
||||
if (observability === "traceloop") {
|
||||
dependencies.push({
|
||||
name: "traceloop-sdk",
|
||||
version: ">=0.15.11,<0.16.0",
|
||||
});
|
||||
}
|
||||
if (observability === "llamatrace") {
|
||||
dependencies.push({
|
||||
name: "llama-index-callbacks-arize-phoenix",
|
||||
version: ">=0.3.0,<0.4.0",
|
||||
});
|
||||
}
|
||||
// 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;
|
||||
};
|
||||
|
||||
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[],
|
||||
@@ -427,132 +400,15 @@ export const installPythonDependencies = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const installLegacyPythonTemplate = async ({
|
||||
root,
|
||||
template,
|
||||
vectorDb,
|
||||
dataSources,
|
||||
tools,
|
||||
useCase,
|
||||
observability,
|
||||
}: Pick<
|
||||
InstallTemplateArgs,
|
||||
| "root"
|
||||
| "template"
|
||||
| "vectorDb"
|
||||
| "dataSources"
|
||||
| "tools"
|
||||
| "useCase"
|
||||
| "observability"
|
||||
>) => {
|
||||
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 router code
|
||||
await copyRouterCode(root, tools ?? []);
|
||||
}
|
||||
|
||||
// Copy multiagents overrides
|
||||
if (template === "multiagent") {
|
||||
await copy("**", path.join(root), {
|
||||
cwd: path.join(compPath, "multiagent", "python"),
|
||||
});
|
||||
}
|
||||
|
||||
if (template === "multiagent" || template === "reflex") {
|
||||
if (useCase) {
|
||||
const sourcePath =
|
||||
template === "multiagent"
|
||||
? path.join(compPath, "agents", "python", useCase)
|
||||
: path.join(compPath, "reflex", useCase);
|
||||
|
||||
await copy("**", path.join(root), {
|
||||
parents: true,
|
||||
cwd: sourcePath,
|
||||
rename: assetRelocator,
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
red(
|
||||
`There is no use case selected for ${template} template. Please pick a use case to use via --use-case flag.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (observability && observability !== "none") {
|
||||
const templateObservabilityPath = path.join(
|
||||
templatesDir,
|
||||
"components",
|
||||
"observability",
|
||||
"python",
|
||||
observability,
|
||||
);
|
||||
await copy("**", path.join(root, "app"), {
|
||||
cwd: templateObservabilityPath,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const installLlamaIndexServerTemplate = async ({
|
||||
root,
|
||||
useCase,
|
||||
useLlamaParse,
|
||||
}: Pick<InstallTemplateArgs, "root" | "useCase" | "useLlamaParse">) => {
|
||||
modelConfig,
|
||||
}: Pick<
|
||||
InstallTemplateArgs,
|
||||
"root" | "useCase" | "useLlamaParse" | "modelConfig"
|
||||
>) => {
|
||||
if (!useCase) {
|
||||
console.log(
|
||||
red(
|
||||
@@ -562,31 +418,54 @@ const installLlamaIndexServerTemplate = async ({
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await copy("*.py", path.join(root, "app"), {
|
||||
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", "workflows", "python", useCase),
|
||||
cwd: path.join(templatesDir, "components", "use-cases", "python", useCase),
|
||||
});
|
||||
|
||||
// Copy custom UI component code
|
||||
await copy(`*`, path.join(root, "components"), {
|
||||
// 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: path.join(templatesDir, "components", "ui", "workflows", useCase),
|
||||
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("index.py", path.join(root, "app"), {
|
||||
parents: true,
|
||||
cwd: path.join(
|
||||
templatesDir,
|
||||
"components",
|
||||
"vectordbs",
|
||||
"llamaindexserver",
|
||||
"llamacloud",
|
||||
"python",
|
||||
),
|
||||
});
|
||||
// TODO: Consider moving generate.py to app folder.
|
||||
await copy("generate.py", path.join(root), {
|
||||
await copy("**", srcDir, {
|
||||
parents: true,
|
||||
cwd: path.join(
|
||||
templatesDir,
|
||||
@@ -601,9 +480,15 @@ const installLlamaIndexServerTemplate = async ({
|
||||
// Copy README.md
|
||||
await copy("README-template.md", path.join(root), {
|
||||
parents: true,
|
||||
cwd: path.join(templatesDir, "components", "workflows", "python", useCase),
|
||||
cwd: path.join(templatesDir, "components", "use-cases", "python", useCase),
|
||||
rename: assetRelocator,
|
||||
});
|
||||
|
||||
// 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 ({
|
||||
@@ -615,10 +500,8 @@ export const installPythonTemplate = async ({
|
||||
postInstallAction,
|
||||
modelConfig,
|
||||
dataSources,
|
||||
tools,
|
||||
useLlamaParse,
|
||||
useCase,
|
||||
observability,
|
||||
}: Pick<
|
||||
InstallTemplateArgs,
|
||||
| "appName"
|
||||
@@ -629,18 +512,11 @@ export const installPythonTemplate = async ({
|
||||
| "postInstallAction"
|
||||
| "modelConfig"
|
||||
| "dataSources"
|
||||
| "tools"
|
||||
| "useLlamaParse"
|
||||
| "useCase"
|
||||
| "observability"
|
||||
>) => {
|
||||
console.log("\nInitializing Python project with template:", template, "\n");
|
||||
let templatePath;
|
||||
if (template === "reflex") {
|
||||
templatePath = path.join(templatesDir, "types", "reflex");
|
||||
} else {
|
||||
templatePath = path.join(templatesDir, "types", template, framework);
|
||||
}
|
||||
const templatePath = path.join(templatesDir, "types", template, framework);
|
||||
await copy("**", root, {
|
||||
parents: true,
|
||||
cwd: templatePath,
|
||||
@@ -652,27 +528,21 @@ export const installPythonTemplate = async ({
|
||||
root,
|
||||
useCase,
|
||||
useLlamaParse,
|
||||
modelConfig,
|
||||
});
|
||||
} else {
|
||||
await installLegacyPythonTemplate({
|
||||
root,
|
||||
template,
|
||||
vectorDb,
|
||||
dataSources,
|
||||
tools,
|
||||
useCase,
|
||||
observability,
|
||||
});
|
||||
throw new Error(`Template ${template} not supported`);
|
||||
}
|
||||
|
||||
console.log("Adding additional dependencies");
|
||||
const addOnDependencies = getAdditionalDependencies(
|
||||
const addOnDependencies = getAdditionalDependencies({
|
||||
framework,
|
||||
template,
|
||||
useCase,
|
||||
modelConfig,
|
||||
vectorDb,
|
||||
dataSources,
|
||||
tools,
|
||||
template,
|
||||
);
|
||||
});
|
||||
|
||||
await addDependencies(root, addOnDependencies);
|
||||
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SpawnOptions, spawn } from "child_process";
|
||||
import { SpawnOptions, exec, spawn } from "child_process";
|
||||
import waitPort from "wait-port";
|
||||
import { TemplateFramework, TemplateType } from "./types";
|
||||
|
||||
const createProcess = (
|
||||
@@ -26,31 +27,12 @@ const createProcess = (
|
||||
});
|
||||
};
|
||||
|
||||
export function runReflexApp(appPath: string, port: number) {
|
||||
const commandArgs = [
|
||||
"run",
|
||||
"reflex",
|
||||
"run",
|
||||
"--frontend-port",
|
||||
port.toString(),
|
||||
];
|
||||
return createProcess("uv", commandArgs, {
|
||||
stdio: "inherit",
|
||||
cwd: appPath,
|
||||
});
|
||||
}
|
||||
|
||||
export function runFastAPIApp(
|
||||
appPath: string,
|
||||
port: number,
|
||||
template: TemplateType,
|
||||
) {
|
||||
let commandArgs: string[];
|
||||
if (template === "streaming") {
|
||||
commandArgs = ["run", "dev"];
|
||||
} else {
|
||||
commandArgs = ["run", "fastapi", "dev", "--port", `${port}`];
|
||||
}
|
||||
const commandArgs = ["run", "fastapi", "dev", "--port", `${port}`];
|
||||
return createProcess("uv", commandArgs, {
|
||||
stdio: "inherit",
|
||||
cwd: appPath,
|
||||
@@ -66,6 +48,58 @@ export function runTSApp(appPath: string, port: number) {
|
||||
});
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -74,15 +108,14 @@ export async function runApp(
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Start the app
|
||||
const defaultPort =
|
||||
framework === "nextjs" || template === "reflex" ? 3000 : 8000;
|
||||
const defaultPort = framework === "nextjs" ? 3000 : 8000;
|
||||
|
||||
const appRunner =
|
||||
template === "reflex"
|
||||
? runReflexApp
|
||||
: framework === "fastapi"
|
||||
? runFastAPIApp
|
||||
: runTSApp;
|
||||
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);
|
||||
|
||||
@@ -1,340 +0,0 @@
|
||||
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.3.0,<0.4.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,<7.0.0",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi"], // TODO: Re-enable this tool once the duck-duck-scrape TypeScript library works again
|
||||
type: ToolType.LOCAL,
|
||||
envVars: [
|
||||
{
|
||||
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
description: "System prompt for DuckDuckGo search tool.",
|
||||
value: `You have access to the duckduckgo search tool. Use it to get information from the web to answer user questions.
|
||||
For better results, you can specify the region parameter to get results from a specific region but it's optional.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "Wikipedia",
|
||||
name: "wikipedia.WikipediaToolSpec",
|
||||
dependencies: [
|
||||
{
|
||||
name: "llama-index-tools-wikipedia",
|
||||
version: ">=0.3.0,<0.4.0",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
type: ToolType.LLAMAHUB,
|
||||
},
|
||||
{
|
||||
display: "Weather",
|
||||
name: "weather",
|
||||
dependencies: [],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
type: ToolType.LOCAL,
|
||||
},
|
||||
{
|
||||
display: "Document generator",
|
||||
name: "document_generator",
|
||||
supportedFrameworks: ["fastapi", "nextjs", "express"],
|
||||
dependencies: [
|
||||
{
|
||||
name: "xhtml2pdf",
|
||||
version: ">=0.2.14,<0.3.0",
|
||||
},
|
||||
{
|
||||
name: "markdown",
|
||||
version: ">=3.7.0,<4.0.0",
|
||||
},
|
||||
],
|
||||
type: ToolType.LOCAL,
|
||||
envVars: [
|
||||
{
|
||||
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
description: "System prompt for document generator tool.",
|
||||
value: `If user request for a report or a post, use document generator tool to create a file and reply with the link to the file.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "Code Interpreter",
|
||||
name: "interpreter",
|
||||
dependencies: [
|
||||
{
|
||||
name: "e2b_code_interpreter",
|
||||
version: ">=1.1.1,<1.2.0",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
type: ToolType.LOCAL,
|
||||
envVars: [
|
||||
{
|
||||
name: "E2B_API_KEY",
|
||||
description:
|
||||
"E2B_API_KEY key is required to run code interpreter tool. Get it here: https://e2b.dev/docs/getting-started/api-key",
|
||||
},
|
||||
{
|
||||
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
description: "System prompt for code interpreter tool.",
|
||||
value: `-You are a Python interpreter that can run any python code in a secure environment.
|
||||
- The python code runs in a Jupyter notebook. Every time you call the 'interpreter' tool, the python code is executed in a separate cell.
|
||||
- You are given tasks to complete and you run python code to solve them.
|
||||
- It's okay to make multiple calls to interpreter tool. If you get an error or the result is not what you expected, you can call the tool again. Don't give up too soon!
|
||||
- Plot visualizations using matplotlib or any other visualization library directly in the notebook.
|
||||
- You can install any pip package (if it exists) by running a cell with pip install.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "Artifact Code Generator",
|
||||
name: "artifact",
|
||||
// Using pre-release version of e2b_code_interpreter
|
||||
// TODO: Update to stable version when 0.0.11 is released
|
||||
dependencies: [
|
||||
{
|
||||
name: "e2b_code_interpreter",
|
||||
version: ">=1.1.1,<1.2.0",
|
||||
},
|
||||
],
|
||||
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,<5.0.0",
|
||||
},
|
||||
{
|
||||
name: "llama-index-tools-requests",
|
||||
version: "0.2.0",
|
||||
},
|
||||
],
|
||||
config: {
|
||||
openapi_uri: "The URL or file path of the OpenAPI schema",
|
||||
},
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
type: ToolType.LOCAL,
|
||||
},
|
||||
{
|
||||
display: "Image Generator",
|
||||
name: "img_gen",
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
type: ToolType.LOCAL,
|
||||
envVars: [
|
||||
{
|
||||
name: "STABILITY_API_KEY",
|
||||
description:
|
||||
"STABILITY_API_KEY key is required to run image generator. Get it here: https://platform.stability.ai/account/keys",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "Azure Code Interpreter",
|
||||
name: "azure_code_interpreter.AzureCodeInterpreterToolSpec",
|
||||
supportedFrameworks: ["fastapi", "nextjs", "express"],
|
||||
type: ToolType.LLAMAHUB,
|
||||
dependencies: [
|
||||
{
|
||||
name: "llama-index-tools-azure-code-interpreter",
|
||||
version: "0.2.0",
|
||||
},
|
||||
],
|
||||
envVars: [
|
||||
{
|
||||
name: "AZURE_POOL_MANAGEMENT_ENDPOINT",
|
||||
description:
|
||||
"Please follow this guideline to create and get the pool management endpoint: https://learn.microsoft.com/azure/container-apps/sessions?tabs=azure-cli",
|
||||
},
|
||||
{
|
||||
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
description: "System prompt for Azure code interpreter tool.",
|
||||
value: `-You are a Python interpreter that can run any python code in a secure environment.
|
||||
- The python code runs in a Jupyter notebook. Every time you call the 'interpreter' tool, the python code is executed in a separate cell.
|
||||
- You are given tasks to complete and you run python code to solve them.
|
||||
- It's okay to make multiple calls to interpreter tool. If you get an error or the result is not what you expected, you can call the tool again. Don't give up too soon!
|
||||
- Plot visualizations using matplotlib or any other visualization library directly in the notebook.
|
||||
- You can install any pip package (if it exists) by running a cell with pip install.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "Form Filling",
|
||||
name: "form_filling",
|
||||
supportedFrameworks: ["fastapi"],
|
||||
type: ToolType.LOCAL,
|
||||
dependencies: [
|
||||
{
|
||||
name: "pandas",
|
||||
version: ">=2.2.3,<3.0.0",
|
||||
},
|
||||
{
|
||||
name: "tabulate",
|
||||
version: ">=0.9.0,<1.0.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 {
|
||||
// For Typescript, we treat llamahub tools as local tools
|
||||
const tsConfigContent = {
|
||||
local: {
|
||||
...configContent.local,
|
||||
...configContent.llamahub,
|
||||
},
|
||||
};
|
||||
await fs.writeFile(
|
||||
path.join(configPath, "tools.json"),
|
||||
JSON.stringify(tsConfigContent, null, 2),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PackageManager } from "../helpers/get-pkg-manager";
|
||||
import { Tool } from "./tools";
|
||||
|
||||
export type ModelProvider =
|
||||
| "openai"
|
||||
@@ -19,15 +18,8 @@ export type ModelConfig = {
|
||||
dimensions: number;
|
||||
isConfigured(): boolean;
|
||||
};
|
||||
export type TemplateType =
|
||||
| "streaming"
|
||||
| "community"
|
||||
| "llamapack"
|
||||
| "multiagent"
|
||||
| "reflex"
|
||||
| "llamaindexserver";
|
||||
export type TemplateType = "llamaindexserver";
|
||||
export type TemplateFramework = "nextjs" | "express" | "fastapi";
|
||||
export type TemplateUI = "html" | "shadcn";
|
||||
export type TemplateVectorDB =
|
||||
| "none"
|
||||
| "mongo"
|
||||
@@ -49,16 +41,14 @@ export type TemplateDataSource = {
|
||||
config: TemplateDataSourceConfig;
|
||||
};
|
||||
export type TemplateDataSourceType = "file" | "web" | "db";
|
||||
export type TemplateObservability = "none" | "traceloop" | "llamatrace";
|
||||
export type TemplateUseCase =
|
||||
| "financial_report"
|
||||
| "blog"
|
||||
| "deep_research"
|
||||
| "form_filling"
|
||||
| "extractor"
|
||||
| "contract_review"
|
||||
| "agentic_rag"
|
||||
| "artifacts";
|
||||
| "code_generator"
|
||||
| "document_generator"
|
||||
| "hitl";
|
||||
|
||||
// Config for both file and folder
|
||||
export type FileSourceConfig =
|
||||
| {
|
||||
@@ -84,31 +74,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;
|
||||
communityProjectConfig?: CommunityProjectConfig;
|
||||
llamapack?: string;
|
||||
vectorDb?: TemplateVectorDB;
|
||||
useLlamaParse: boolean;
|
||||
vectorDb: TemplateVectorDB;
|
||||
port?: number;
|
||||
postInstallAction?: TemplatePostInstallAction;
|
||||
tools?: Tool[];
|
||||
observability?: TemplateObservability;
|
||||
useCase?: TemplateUseCase;
|
||||
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>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "fs/promises";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { bold, cyan, red, yellow } from "picocolors";
|
||||
import { bold, cyan, red } from "picocolors";
|
||||
import { assetRelocator, copy } from "../helpers/copy";
|
||||
import { callPackageManager } from "../helpers/install";
|
||||
import { templatesDir } from "./dir";
|
||||
@@ -12,7 +12,12 @@ const installLlamaIndexServerTemplate = async ({
|
||||
root,
|
||||
useCase,
|
||||
vectorDb,
|
||||
}: Pick<InstallTemplateArgs, "root" | "useCase" | "vectorDb">) => {
|
||||
modelConfig,
|
||||
dataSources,
|
||||
}: Pick<
|
||||
InstallTemplateArgs,
|
||||
"root" | "useCase" | "vectorDb" | "modelConfig" | "dataSources"
|
||||
>) => {
|
||||
if (!useCase) {
|
||||
console.log(
|
||||
red(
|
||||
@@ -31,268 +36,62 @@ const installLlamaIndexServerTemplate = async ({
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await copy("workflow.ts", path.join(root, "src", "app"), {
|
||||
parents: true,
|
||||
// copy model provider settings to app folder
|
||||
await copy("**", path.join(root, "src", "app"), {
|
||||
cwd: path.join(
|
||||
templatesDir,
|
||||
"components",
|
||||
"workflows",
|
||||
"providers",
|
||||
"typescript",
|
||||
useCase,
|
||||
modelConfig.provider,
|
||||
),
|
||||
});
|
||||
|
||||
// copy workflow UI components to output/components folder
|
||||
await copy("*", path.join(root, "components"), {
|
||||
parents: true,
|
||||
cwd: path.join(templatesDir, "components", "ui", "workflows", useCase),
|
||||
});
|
||||
|
||||
if (vectorDb === "llamacloud") {
|
||||
await copy("generate.ts", path.join(root, "src"), {
|
||||
parents: true,
|
||||
cwd: path.join(
|
||||
templatesDir,
|
||||
"components",
|
||||
"vectordbs",
|
||||
"llamaindexserver",
|
||||
"llamacloud",
|
||||
"typescript",
|
||||
),
|
||||
});
|
||||
|
||||
await copy("index.ts", path.join(root, "src", "app"), {
|
||||
parents: true,
|
||||
cwd: path.join(
|
||||
templatesDir,
|
||||
"components",
|
||||
"vectordbs",
|
||||
"llamaindexserver",
|
||||
"llamacloud",
|
||||
"typescript",
|
||||
),
|
||||
rename: () => "data.ts",
|
||||
});
|
||||
}
|
||||
// Copy README.md
|
||||
await copy("README-template.md", path.join(root), {
|
||||
parents: true,
|
||||
await copy("**", path.join(root), {
|
||||
cwd: path.join(
|
||||
templatesDir,
|
||||
"components",
|
||||
"workflows",
|
||||
"use-cases",
|
||||
"typescript",
|
||||
useCase,
|
||||
),
|
||||
rename: assetRelocator,
|
||||
});
|
||||
};
|
||||
|
||||
const installLegacyTSTemplate = async ({
|
||||
root,
|
||||
template,
|
||||
backend,
|
||||
framework,
|
||||
ui,
|
||||
vectorDb,
|
||||
observability,
|
||||
tools,
|
||||
dataSources,
|
||||
useLlamaParse,
|
||||
useCase,
|
||||
modelConfig,
|
||||
relativeEngineDestPath,
|
||||
}: InstallTemplateArgs & {
|
||||
backend: boolean;
|
||||
relativeEngineDestPath: string;
|
||||
}) => {
|
||||
/**
|
||||
* 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 enginePath = path.join(root, relativeEngineDestPath, "engine");
|
||||
|
||||
// copy llamaindex code for TS templates
|
||||
await copy("**", path.join(root, relativeEngineDestPath, "llamaindex"), {
|
||||
// copy workflow UI components to components folder in root
|
||||
await copy("*", path.join(root, "components"), {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "llamaindex", "typescript"),
|
||||
cwd: path.join(templatesDir, "components", "ui", "use-cases", useCase),
|
||||
});
|
||||
|
||||
// copy vector db component
|
||||
// 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") {
|
||||
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"), {
|
||||
await copy("**", path.join(root, "src"), {
|
||||
parents: true,
|
||||
cwd: path.join(multiagentPath, "workflow"),
|
||||
});
|
||||
|
||||
// Copy use case code for multiagent template
|
||||
if (useCase) {
|
||||
console.log("\nCopying use case:", useCase, "\n");
|
||||
const useCasePath = path.join(compPath, "agents", "typescript", useCase);
|
||||
const useCaseCodePath = path.join(useCasePath, "workflow");
|
||||
|
||||
// Copy use case codes
|
||||
await copy("**", path.join(root, relativeEngineDestPath, "workflow"), {
|
||||
parents: true,
|
||||
cwd: useCaseCodePath,
|
||||
rename: assetRelocator,
|
||||
});
|
||||
|
||||
// Copy use case files to project root
|
||||
await copy("*.*", path.join(root), {
|
||||
parents: true,
|
||||
cwd: useCasePath,
|
||||
rename: assetRelocator,
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
red(
|
||||
`There is no use case selected for ${template} template. Please pick a use case to use via --use-case flag.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (framework === "nextjs") {
|
||||
// patch route.ts file
|
||||
await copy("**", path.join(root, relativeEngineDestPath), {
|
||||
parents: true,
|
||||
cwd: path.join(multiagentPath, "nextjs"),
|
||||
});
|
||||
} else if (framework === "express") {
|
||||
// patch chat.controller.ts file
|
||||
await copy("**", path.join(root, relativeEngineDestPath), {
|
||||
parents: true,
|
||||
cwd: path.join(multiagentPath, "express"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// copy loader component (TS only supports llama_parse and file for now)
|
||||
const loaderFolder = useLlamaParse ? "llama_parse" : "file";
|
||||
await copy("**", enginePath, {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "loaders", "typescript", loaderFolder),
|
||||
});
|
||||
|
||||
// copy provider settings
|
||||
await copy("**", enginePath, {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "providers", "typescript", modelConfig.provider),
|
||||
});
|
||||
|
||||
// Select and copy engine code based on data sources and tools
|
||||
let engine;
|
||||
tools = tools ?? [];
|
||||
// multiagent template always uses agent engine
|
||||
if (template === "multiagent") {
|
||||
engine = "agent";
|
||||
} else if (dataSources.length > 0 && tools.length === 0) {
|
||||
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,
|
||||
cwd: path.join(
|
||||
templatesDir,
|
||||
"components",
|
||||
"vectordbs",
|
||||
"llamaindexserver",
|
||||
"llamacloud",
|
||||
"typescript",
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
/** 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 });
|
||||
// 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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -303,20 +102,14 @@ export const installTSTemplate = async ({
|
||||
appName,
|
||||
root,
|
||||
packageManager,
|
||||
isOnline,
|
||||
template,
|
||||
framework,
|
||||
ui,
|
||||
vectorDb,
|
||||
postInstallAction,
|
||||
backend,
|
||||
observability,
|
||||
tools,
|
||||
dataSources,
|
||||
useLlamaParse,
|
||||
useCase,
|
||||
modelConfig,
|
||||
}: InstallTemplateArgs & { backend: boolean }) => {
|
||||
}: InstallTemplateArgs) => {
|
||||
console.log(bold(`Using ${packageManager}.`));
|
||||
|
||||
/**
|
||||
@@ -332,57 +125,39 @@ export const installTSTemplate = async ({
|
||||
rename: assetRelocator,
|
||||
});
|
||||
|
||||
const relativeEngineDestPath =
|
||||
framework === "nextjs"
|
||||
? path.join("app", "api", "chat")
|
||||
: path.join("src", "controllers");
|
||||
|
||||
if (template === "llamaindexserver") {
|
||||
await installLlamaIndexServerTemplate({
|
||||
root,
|
||||
useCase,
|
||||
vectorDb,
|
||||
});
|
||||
} else {
|
||||
await installLegacyTSTemplate({
|
||||
appName,
|
||||
root,
|
||||
packageManager,
|
||||
isOnline,
|
||||
template,
|
||||
backend,
|
||||
framework,
|
||||
ui,
|
||||
vectorDb,
|
||||
observability,
|
||||
tools,
|
||||
dataSources,
|
||||
useLlamaParse,
|
||||
useCase,
|
||||
modelConfig,
|
||||
relativeEngineDestPath,
|
||||
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,
|
||||
dataSources,
|
||||
relativeEngineDestPath,
|
||||
framework,
|
||||
ui,
|
||||
observability,
|
||||
vectorDb,
|
||||
backend,
|
||||
modelConfig,
|
||||
template,
|
||||
});
|
||||
|
||||
if (
|
||||
backend &&
|
||||
(postInstallAction === "runApp" || postInstallAction === "dependencies")
|
||||
) {
|
||||
await installTSDependencies(packageJson, packageManager, isOnline);
|
||||
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
|
||||
await installTSDependencies(packageJson, packageManager, true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -390,7 +165,7 @@ const providerDependencies: {
|
||||
[key in ModelProvider]?: Record<string, string>;
|
||||
} = {
|
||||
openai: {
|
||||
"@llamaindex/openai": "^0.2.0",
|
||||
"@llamaindex/openai": "~0.4.0",
|
||||
},
|
||||
gemini: {
|
||||
"@llamaindex/google": "^0.2.0",
|
||||
@@ -451,30 +226,12 @@ const vectorDbDependencies: Record<TemplateVectorDB, Record<string, string>> = {
|
||||
async function updatePackageJson({
|
||||
root,
|
||||
appName,
|
||||
dataSources,
|
||||
relativeEngineDestPath,
|
||||
framework,
|
||||
ui,
|
||||
observability,
|
||||
vectorDb,
|
||||
backend,
|
||||
modelConfig,
|
||||
template,
|
||||
}: Pick<
|
||||
InstallTemplateArgs,
|
||||
| "root"
|
||||
| "appName"
|
||||
| "dataSources"
|
||||
| "framework"
|
||||
| "ui"
|
||||
| "observability"
|
||||
| "vectorDb"
|
||||
| "modelConfig"
|
||||
| "template"
|
||||
> & {
|
||||
relativeEngineDestPath: string;
|
||||
backend: boolean;
|
||||
}): Promise<any> {
|
||||
"root" | "appName" | "vectorDb" | "modelConfig"
|
||||
>): Promise<any> {
|
||||
const packageJsonFile = path.join(root, "package.json");
|
||||
const packageJson: any = JSON.parse(
|
||||
await fs.readFile(packageJsonFile, "utf8"),
|
||||
@@ -482,67 +239,22 @@ async function updatePackageJson({
|
||||
packageJson.name = appName;
|
||||
packageJson.version = "0.1.0";
|
||||
|
||||
if (relativeEngineDestPath && template !== "llamaindexserver") {
|
||||
// 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",
|
||||
)}`,
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"@llamaindex/readers": "~3.1.4",
|
||||
};
|
||||
|
||||
if (vectorDb && vectorDb in vectorDbDependencies) {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
...vectorDbDependencies[vectorDb],
|
||||
};
|
||||
}
|
||||
|
||||
if (framework === "nextjs" && ui === "html") {
|
||||
// remove shadcn dependencies if html ui is selected
|
||||
if (modelConfig.provider && modelConfig.provider in providerDependencies) {
|
||||
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 (backend) {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"@llamaindex/readers": "^2.0.0",
|
||||
};
|
||||
|
||||
if (vectorDb && vectorDb in vectorDbDependencies) {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
...vectorDbDependencies[vectorDb],
|
||||
};
|
||||
}
|
||||
|
||||
if (modelConfig.provider && modelConfig.provider in providerDependencies) {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
...providerDependencies[modelConfig.provider],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (observability === "traceloop") {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"@traceloop/node-server-sdk": "^0.5.19",
|
||||
};
|
||||
|
||||
packageJson.devDependencies = {
|
||||
...packageJson.devDependencies,
|
||||
"node-loader": "^2.0.0",
|
||||
...providerDependencies[modelConfig.provider],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
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",
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import validateProjectName from "validate-npm-package-name";
|
||||
|
||||
export function validateNpmName(name: string): {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { execSync } from "child_process";
|
||||
import { Command } from "commander";
|
||||
import fs from "fs";
|
||||
@@ -8,12 +7,10 @@ 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";
|
||||
@@ -57,13 +54,6 @@ 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(
|
||||
@@ -71,62 +61,6 @@ 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(
|
||||
@@ -148,27 +82,6 @@ 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(
|
||||
@@ -178,26 +91,12 @@ 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",
|
||||
`
|
||||
|
||||
Allow interactive selection of LLM and embedding models of different model providers.
|
||||
`,
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--pro",
|
||||
`
|
||||
|
||||
Allow interactive selection of all features.
|
||||
`,
|
||||
false,
|
||||
)
|
||||
@@ -205,7 +104,7 @@ const program = new Command(packageJson.name)
|
||||
"--use-case <useCase>",
|
||||
`
|
||||
|
||||
Select which use case to use for the multi-agent template (e.g: financial_report, blog).
|
||||
Select which use case to use for the template (e.g: financial_report, blog).
|
||||
`,
|
||||
)
|
||||
.allowUnknownOption()
|
||||
@@ -213,42 +112,6 @@ const program = new Command(packageJson.name)
|
||||
|
||||
const options = program.opts();
|
||||
|
||||
if (
|
||||
process.argv.includes("--no-llama-parse") ||
|
||||
options.template === "reflex"
|
||||
) {
|
||||
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
|
||||
@@ -257,6 +120,9 @@ 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();
|
||||
@@ -320,7 +186,7 @@ async function run(): Promise<void> {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const answers = await askQuestions(options as unknown as QuestionArgs);
|
||||
const answers = await askQuestions(cliArgs);
|
||||
|
||||
await createApp({
|
||||
...answers,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.5.12",
|
||||
"version": "0.6.3",
|
||||
"description": "Create LlamaIndex-powered apps with one command",
|
||||
"keywords": [
|
||||
"rag",
|
||||
@@ -30,10 +30,7 @@
|
||||
"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",
|
||||
"e2e:ts": "playwright test e2e/shared e2e/typescript",
|
||||
"pack-install": "bash ./scripts/pack.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -47,7 +44,6 @@
|
||||
"@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",
|
||||
@@ -66,10 +62,6 @@
|
||||
"yaml": "2.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"@playwright/test": "^1.41.1",
|
||||
"@vercel/ncc": "0.38.1",
|
||||
"rimraf": "^5.0.5",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: ["prettier-plugin-organize-imports"],
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { askModelConfig } from "../helpers/providers";
|
||||
import { QuestionArgs, QuestionResults } from "./types";
|
||||
|
||||
const defaults: Omit<QuestionArgs, "modelConfig"> = {
|
||||
template: "streaming",
|
||||
framework: "nextjs",
|
||||
ui: "shadcn",
|
||||
frontend: false,
|
||||
llamaCloudKey: "",
|
||||
useLlamaParse: false,
|
||||
communityProjectConfig: undefined,
|
||||
llamapack: "",
|
||||
postInstallAction: "dependencies",
|
||||
dataSources: [],
|
||||
tools: [],
|
||||
};
|
||||
|
||||
export async function getCIQuestionResults(
|
||||
program: QuestionArgs,
|
||||
): Promise<QuestionResults> {
|
||||
return {
|
||||
...defaults,
|
||||
...program,
|
||||
modelConfig: await askModelConfig({
|
||||
openAiKey: program.openAiKey,
|
||||
askModels: false,
|
||||
framework: program.framework,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import {
|
||||
TemplateDataSource,
|
||||
TemplateFramework,
|
||||
TemplateType,
|
||||
} from "../helpers";
|
||||
import { supportedContextFileTypes } from "./utils";
|
||||
|
||||
export const getDataSourceChoices = (
|
||||
framework: TemplateFramework,
|
||||
selectedDataSource: TemplateDataSource[],
|
||||
template?: TemplateType,
|
||||
) => {
|
||||
const choices = [];
|
||||
|
||||
if (selectedDataSource.length > 0) {
|
||||
choices.push({
|
||||
title: "No",
|
||||
value: "no",
|
||||
});
|
||||
}
|
||||
if (selectedDataSource === undefined || selectedDataSource.length === 0) {
|
||||
choices.push({
|
||||
title: "No datasource",
|
||||
value: "none",
|
||||
});
|
||||
choices.push({
|
||||
title:
|
||||
process.platform !== "linux"
|
||||
? "Use an example PDF"
|
||||
: "Use an example PDF (you can add your own data files later)",
|
||||
value: "exampleFile",
|
||||
});
|
||||
}
|
||||
|
||||
// Linux has many distros so we won't support file/folder picker for now
|
||||
if (process.platform !== "linux") {
|
||||
choices.push(
|
||||
{
|
||||
title: `Use local files (${supportedContextFileTypes.join(", ")})`,
|
||||
value: "file",
|
||||
},
|
||||
{
|
||||
title:
|
||||
process.platform === "win32"
|
||||
? "Use a local folder"
|
||||
: "Use local folders",
|
||||
value: "folder",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (framework === "fastapi" && template !== "reflex") {
|
||||
choices.push({
|
||||
title: "Use website content (requires Chrome)",
|
||||
value: "web",
|
||||
});
|
||||
choices.push({
|
||||
title: "Use data from a database (Mysql, PostgreSQL)",
|
||||
value: "db",
|
||||
});
|
||||
}
|
||||
|
||||
return choices;
|
||||
};
|
||||
@@ -1,21 +1,162 @@
|
||||
import ciInfo from "ci-info";
|
||||
import { getCIQuestionResults } from "./ci";
|
||||
import { askProQuestions } from "./questions";
|
||||
import { askSimpleQuestions } from "./simple";
|
||||
import prompts from "prompts";
|
||||
import { askModelConfig } from "../helpers/providers";
|
||||
import {
|
||||
TemplateFramework,
|
||||
TemplateUseCase,
|
||||
TemplateVectorDB,
|
||||
} from "../helpers/types";
|
||||
import { QuestionArgs, QuestionResults } from "./types";
|
||||
|
||||
export const isCI = ciInfo.isCI || process.env.PLAYWRIGHT_TEST === "1";
|
||||
import { useCaseConfiguration } from "./usecases";
|
||||
import { askPostInstallAction, questionHandlers } from "./utils";
|
||||
|
||||
export const askQuestions = async (
|
||||
args: QuestionArgs,
|
||||
): Promise<QuestionResults> => {
|
||||
if (isCI) {
|
||||
return await getCIQuestionResults(args);
|
||||
} else if (args.pro) {
|
||||
// TODO: refactor pro questions to return a result object
|
||||
await askProQuestions(args);
|
||||
return args as unknown as QuestionResults;
|
||||
const {
|
||||
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");
|
||||
}
|
||||
const results = await askSimpleQuestions(args);
|
||||
return results;
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,459 +0,0 @@
|
||||
import { blue } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { isCI } from ".";
|
||||
import { COMMUNITY_OWNER, COMMUNITY_REPO } from "../helpers/constant";
|
||||
import { EXAMPLE_FILE, EXAMPLE_GDPR } from "../helpers/datasources";
|
||||
import { getAvailableLlamapackOptions } from "../helpers/llama-pack";
|
||||
import { askModelConfig } from "../helpers/providers";
|
||||
import { getProjectOptions } from "../helpers/repo";
|
||||
import { supportedTools, toolRequiresConfig } from "../helpers/tools";
|
||||
import { getDataSourceChoices } from "./datasources";
|
||||
import { getVectorDbChoices } from "./stores";
|
||||
import { QuestionArgs } from "./types";
|
||||
import {
|
||||
askPostInstallAction,
|
||||
onPromptState,
|
||||
questionHandlers,
|
||||
selectLocalContextData,
|
||||
} from "./utils";
|
||||
|
||||
export const askProQuestions = async (program: QuestionArgs) => {
|
||||
if (!program.template) {
|
||||
const styledRepo = blue(
|
||||
`https://github.com/${COMMUNITY_OWNER}/${COMMUNITY_REPO}`,
|
||||
);
|
||||
const { template } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "template",
|
||||
message: "Which template would you like to use?",
|
||||
choices: [
|
||||
{ title: "Agentic RAG (e.g. chat with docs)", value: "streaming" },
|
||||
{
|
||||
title: "Multi-agent app (using workflows)",
|
||||
value: "multiagent",
|
||||
},
|
||||
{ title: "Fullstack python template with Reflex", value: "reflex" },
|
||||
{
|
||||
title: `Community template from ${styledRepo}`,
|
||||
value: "community",
|
||||
},
|
||||
{
|
||||
title: "Example using a LlamaPack",
|
||||
value: "llamapack",
|
||||
},
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.template = template;
|
||||
}
|
||||
|
||||
if (program.template === "community") {
|
||||
const projectOptions = await getProjectOptions(
|
||||
COMMUNITY_OWNER,
|
||||
COMMUNITY_REPO,
|
||||
);
|
||||
const { communityProjectConfig } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "communityProjectConfig",
|
||||
message: "Select community template",
|
||||
choices: projectOptions.map(({ title, value }) => ({
|
||||
title,
|
||||
value: JSON.stringify(value), // serialize value to string in terminal
|
||||
})),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
const projectConfig = JSON.parse(communityProjectConfig);
|
||||
program.communityProjectConfig = projectConfig;
|
||||
return; // early return - no further questions needed for community projects
|
||||
}
|
||||
|
||||
if (program.template === "llamapack") {
|
||||
const availableLlamaPacks = await getAvailableLlamapackOptions();
|
||||
const { llamapack } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "llamapack",
|
||||
message: "Select LlamaPack",
|
||||
choices: availableLlamaPacks.map((pack) => ({
|
||||
title: pack.name,
|
||||
value: pack.folderPath,
|
||||
})),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.llamapack = llamapack;
|
||||
if (!program.postInstallAction) {
|
||||
program.postInstallAction = await askPostInstallAction(program);
|
||||
}
|
||||
return; // early return - no further questions needed for llamapack projects
|
||||
}
|
||||
|
||||
if (program.template === "reflex") {
|
||||
// Reflex template only supports FastAPI, empty data sources, and llamacloud
|
||||
// So we just use example file for extractor template, this allows user to choose vector database later
|
||||
program.dataSources = [EXAMPLE_FILE];
|
||||
program.framework = "fastapi";
|
||||
// Ask for which Reflex use case to use
|
||||
const { useCase } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "useCase",
|
||||
message: "Which use case would you like to build?",
|
||||
choices: [
|
||||
{ title: "Structured Extractor", value: "extractor" },
|
||||
{
|
||||
title: "Contract review (using Workflow)",
|
||||
value: "contract_review",
|
||||
},
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.useCase = useCase;
|
||||
}
|
||||
|
||||
if (!program.framework) {
|
||||
const choices = [
|
||||
{ title: "NextJS", value: "nextjs" },
|
||||
{ title: "Express", value: "express" },
|
||||
{ title: "FastAPI (Python)", value: "fastapi" },
|
||||
];
|
||||
|
||||
const { framework } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "framework",
|
||||
message: "Which framework would you like to use?",
|
||||
choices,
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.framework = framework;
|
||||
}
|
||||
|
||||
if (
|
||||
program.framework === "fastapi" &&
|
||||
(program.template === "streaming" || program.template === "multiagent")
|
||||
) {
|
||||
// if a backend-only framework is selected, ask whether we should create a frontend
|
||||
if (program.frontend === undefined) {
|
||||
const styledNextJS = blue("NextJS");
|
||||
const { frontend } = await prompts({
|
||||
onState: onPromptState,
|
||||
type: "toggle",
|
||||
name: "frontend",
|
||||
message: `Would you like to generate a ${styledNextJS} frontend for your FastAPI backend?`,
|
||||
initial: false,
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
});
|
||||
program.frontend = Boolean(frontend);
|
||||
}
|
||||
} else {
|
||||
program.frontend = false;
|
||||
}
|
||||
|
||||
if (program.framework === "nextjs" || program.frontend) {
|
||||
if (!program.ui) {
|
||||
program.ui = "shadcn";
|
||||
}
|
||||
}
|
||||
|
||||
if (!program.observability && program.template === "streaming") {
|
||||
const { observability } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "observability",
|
||||
message: "Would you like to set up observability?",
|
||||
choices: [
|
||||
{ title: "No", value: "none" },
|
||||
...(program.framework === "fastapi"
|
||||
? [{ title: "LlamaTrace", value: "llamatrace" }]
|
||||
: []),
|
||||
{ title: "Traceloop", value: "traceloop" },
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
program.observability = observability;
|
||||
}
|
||||
|
||||
if (
|
||||
(program.template === "reflex" || program.template === "multiagent") &&
|
||||
!program.useCase
|
||||
) {
|
||||
const choices =
|
||||
program.template === "reflex"
|
||||
? [
|
||||
{ title: "Structured Extractor", value: "extractor" },
|
||||
{
|
||||
title: "Contract review (using Workflow)",
|
||||
value: "contract_review",
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: "Financial report (generate a financial report)",
|
||||
value: "financial_report",
|
||||
},
|
||||
{
|
||||
title: "Form filling (fill missing value in a CSV file)",
|
||||
value: "form_filling",
|
||||
},
|
||||
{ title: "Blog writer (Write a blog post)", value: "blog" },
|
||||
];
|
||||
|
||||
const { useCase } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "useCase",
|
||||
message: "Which use case would you like to use?",
|
||||
choices,
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.useCase = useCase;
|
||||
}
|
||||
|
||||
// Configure framework and data sources for Reflex template
|
||||
if (program.template === "reflex") {
|
||||
program.framework = "fastapi";
|
||||
|
||||
program.dataSources =
|
||||
program.useCase === "extractor" ? [EXAMPLE_FILE] : [EXAMPLE_GDPR];
|
||||
}
|
||||
|
||||
if (!program.modelConfig) {
|
||||
const modelConfig = await askModelConfig({
|
||||
openAiKey: program.openAiKey,
|
||||
askModels: program.askModels ?? false,
|
||||
framework: program.framework,
|
||||
});
|
||||
program.modelConfig = modelConfig;
|
||||
}
|
||||
|
||||
if (!program.vectorDb) {
|
||||
const { vectorDb } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "vectorDb",
|
||||
message: "Would you like to use a vector database?",
|
||||
choices: getVectorDbChoices(program.framework),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.vectorDb = vectorDb;
|
||||
}
|
||||
|
||||
if (program.vectorDb === "llamacloud" && program.dataSources.length === 0) {
|
||||
// When using a LlamaCloud index and no data sources are provided, just copy an example file
|
||||
program.dataSources = [EXAMPLE_FILE];
|
||||
}
|
||||
|
||||
if (!program.dataSources) {
|
||||
program.dataSources = [];
|
||||
// continue asking user for data sources if none are initially provided
|
||||
while (true) {
|
||||
const firstQuestion = program.dataSources.length === 0;
|
||||
const choices = getDataSourceChoices(
|
||||
program.framework,
|
||||
program.dataSources,
|
||||
program.template,
|
||||
);
|
||||
if (choices.length === 0) break;
|
||||
const { selectedSource } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "selectedSource",
|
||||
message: firstQuestion
|
||||
? "Which data source would you like to use?"
|
||||
: "Would you like to add another data source?",
|
||||
choices,
|
||||
initial: firstQuestion ? 1 : 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
if (selectedSource === "no" || selectedSource === "none") {
|
||||
// user doesn't want another data source or any data source
|
||||
break;
|
||||
}
|
||||
switch (selectedSource) {
|
||||
case "exampleFile": {
|
||||
program.dataSources.push(EXAMPLE_FILE);
|
||||
break;
|
||||
}
|
||||
case "file":
|
||||
case "folder": {
|
||||
const selectedPaths = await selectLocalContextData(selectedSource);
|
||||
for (const p of selectedPaths) {
|
||||
program.dataSources.push({
|
||||
type: "file",
|
||||
config: {
|
||||
path: p,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "web": {
|
||||
const { baseUrl } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "baseUrl",
|
||||
message: "Please provide base URL of the website: ",
|
||||
initial: "https://www.llamaindex.ai",
|
||||
validate: (value: string) => {
|
||||
if (!value.includes("://")) {
|
||||
value = `https://${value}`;
|
||||
}
|
||||
const urlObj = new URL(value);
|
||||
if (
|
||||
urlObj.protocol !== "https:" &&
|
||||
urlObj.protocol !== "http:"
|
||||
) {
|
||||
return `URL=${value} has invalid protocol, only allow http or https`;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
program.dataSources.push({
|
||||
type: "web",
|
||||
config: {
|
||||
baseUrl,
|
||||
prefix: baseUrl,
|
||||
depth: 1,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "db": {
|
||||
const dbPrompts: prompts.PromptObject<string>[] = [
|
||||
{
|
||||
type: "text",
|
||||
name: "uri",
|
||||
message:
|
||||
"Please enter the connection string (URI) for the database.",
|
||||
initial: "mysql+pymysql://user:pass@localhost:3306/mydb",
|
||||
validate: (value: string) => {
|
||||
if (!value) {
|
||||
return "Please provide a valid connection string";
|
||||
} else if (
|
||||
!(
|
||||
value.startsWith("mysql+pymysql://") ||
|
||||
value.startsWith("postgresql+psycopg://")
|
||||
)
|
||||
) {
|
||||
return "The connection string must start with 'mysql+pymysql://' for MySQL or 'postgresql+psycopg://' for PostgreSQL";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
// Only ask for a query, user can provide more complex queries in the config file later
|
||||
{
|
||||
type: (prev) => (prev ? "text" : null),
|
||||
name: "queries",
|
||||
message: "Please enter the SQL query to fetch data:",
|
||||
initial: "SELECT * FROM mytable",
|
||||
},
|
||||
];
|
||||
program.dataSources.push({
|
||||
type: "db",
|
||||
config: await prompts(dbPrompts, questionHandlers),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isUsingLlamaCloud = program.vectorDb === "llamacloud";
|
||||
|
||||
// Asking for LlamaParse if user selected file data source
|
||||
if (isUsingLlamaCloud) {
|
||||
// default to use LlamaParse if using LlamaCloud
|
||||
program.useLlamaParse = true;
|
||||
} else {
|
||||
// Reflex template doesn't support LlamaParse right now (cannot use asyncio loop in Reflex)
|
||||
if (program.useLlamaParse === undefined && program.template !== "reflex") {
|
||||
// if already set useLlamaParse, don't ask again
|
||||
if (program.dataSources.some((ds) => ds.type === "file")) {
|
||||
const { useLlamaParse } = await prompts(
|
||||
{
|
||||
type: "toggle",
|
||||
name: "useLlamaParse",
|
||||
message:
|
||||
"Would you like to use LlamaParse (improved parser for RAG - requires API key)?",
|
||||
initial: false,
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.useLlamaParse = useLlamaParse;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ask for LlamaCloud API key when using a LlamaCloud index or LlamaParse
|
||||
if (isUsingLlamaCloud || program.useLlamaParse) {
|
||||
if (!program.llamaCloudKey && !isCI) {
|
||||
// if already set, don't ask again
|
||||
// Ask for LlamaCloud API key
|
||||
const { llamaCloudKey } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "llamaCloudKey",
|
||||
message:
|
||||
"Please provide your LlamaCloud API key (leave blank to skip):",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.llamaCloudKey = llamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!program.tools &&
|
||||
(program.template === "streaming" || program.template === "multiagent")
|
||||
) {
|
||||
const options = supportedTools.filter((t) =>
|
||||
t.supportedFrameworks?.includes(program.framework),
|
||||
);
|
||||
const toolChoices = options.map((tool) => ({
|
||||
title: `${tool.display}${toolRequiresConfig(tool) ? " (needs configuration)" : ""}`,
|
||||
value: tool.name,
|
||||
}));
|
||||
const { toolsName } = await prompts({
|
||||
type: "multiselect",
|
||||
name: "toolsName",
|
||||
message:
|
||||
"Would you like to build an agent using tools? If so, select the tools here, otherwise just press enter",
|
||||
choices: toolChoices,
|
||||
});
|
||||
const tools = toolsName?.map((tool: string) =>
|
||||
supportedTools.find((t) => t.name === tool),
|
||||
);
|
||||
program.tools = tools;
|
||||
}
|
||||
|
||||
if (!program.postInstallAction) {
|
||||
program.postInstallAction = await askPostInstallAction(program);
|
||||
}
|
||||
};
|
||||
@@ -1,182 +0,0 @@
|
||||
import prompts from "prompts";
|
||||
import { EXAMPLE_10K_SEC_FILES, EXAMPLE_FILE } from "../helpers/datasources";
|
||||
import { askModelConfig } from "../helpers/providers";
|
||||
import { getTools } from "../helpers/tools";
|
||||
import { ModelConfig, TemplateFramework } from "../helpers/types";
|
||||
import { PureQuestionArgs, QuestionResults } from "./types";
|
||||
import { askPostInstallAction, questionHandlers } from "./utils";
|
||||
|
||||
type AppType =
|
||||
| "agentic_rag"
|
||||
| "financial_report"
|
||||
| "deep_research"
|
||||
| "artifacts";
|
||||
|
||||
type SimpleAnswers = {
|
||||
appType: AppType;
|
||||
language: TemplateFramework;
|
||||
useLlamaCloud: boolean;
|
||||
llamaCloudKey?: string;
|
||||
};
|
||||
|
||||
export const askSimpleQuestions = async (
|
||||
args: PureQuestionArgs,
|
||||
): Promise<QuestionResults> => {
|
||||
const { appType } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "appType",
|
||||
message: "What use case do you want to build?",
|
||||
choices: [
|
||||
{
|
||||
title: "Agentic RAG",
|
||||
value: "agentic_rag",
|
||||
description:
|
||||
"Chatbot that answers questions based on provided documents.",
|
||||
},
|
||||
{
|
||||
title: "Financial Report",
|
||||
value: "financial_report",
|
||||
description:
|
||||
"Agent that analyzes data and generates visualizations by using a code interpreter.",
|
||||
},
|
||||
{
|
||||
title: "Deep Research",
|
||||
value: "deep_research",
|
||||
description:
|
||||
"Researches and analyzes provided documents from multiple perspectives, generating a comprehensive report with citations to support key findings and insights.",
|
||||
},
|
||||
{
|
||||
title: "Artifacts",
|
||||
value: "artifacts",
|
||||
description:
|
||||
"Build your own Vercel's v0 or OpenAI's canvas-styled UI.",
|
||||
},
|
||||
],
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
let language: TemplateFramework = "fastapi";
|
||||
let llamaCloudKey = args.llamaCloudKey;
|
||||
|
||||
let useLlamaCloud = false;
|
||||
|
||||
if (appType !== "artifacts") {
|
||||
const { language: newLanguage } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "language",
|
||||
message: "What language do you want to use?",
|
||||
choices: [
|
||||
{ title: "Python (FastAPI)", value: "fastapi" },
|
||||
{ title: "Typescript (NextJS)", value: "nextjs" },
|
||||
],
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
language = newLanguage;
|
||||
}
|
||||
|
||||
const { useLlamaCloud: newUseLlamaCloud } = await prompts(
|
||||
{
|
||||
type: "toggle",
|
||||
name: "useLlamaCloud",
|
||||
message: "Do you want to use LlamaCloud services?",
|
||||
initial: false,
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
hint: "see https://www.llamaindex.ai/enterprise for more info",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
useLlamaCloud = newUseLlamaCloud;
|
||||
|
||||
if (useLlamaCloud && !llamaCloudKey) {
|
||||
// Ask for LlamaCloud API key, if not set
|
||||
const { llamaCloudKey: newLlamaCloudKey } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "llamaCloudKey",
|
||||
message:
|
||||
"Please provide your LlamaCloud API key (leave blank to skip):",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
llamaCloudKey = newLlamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
|
||||
}
|
||||
|
||||
const results = await convertAnswers(args, {
|
||||
appType,
|
||||
language,
|
||||
useLlamaCloud,
|
||||
llamaCloudKey,
|
||||
});
|
||||
|
||||
results.postInstallAction = await askPostInstallAction(results);
|
||||
return results;
|
||||
};
|
||||
|
||||
const convertAnswers = async (
|
||||
args: PureQuestionArgs,
|
||||
answers: SimpleAnswers,
|
||||
): Promise<QuestionResults> => {
|
||||
const MODEL_GPT41: ModelConfig = {
|
||||
provider: "openai",
|
||||
apiKey: args.openAiKey,
|
||||
model: "gpt-4.1",
|
||||
embeddingModel: "text-embedding-3-large",
|
||||
dimensions: 1536,
|
||||
isConfigured(): boolean {
|
||||
return !!args.openAiKey;
|
||||
},
|
||||
};
|
||||
const lookup: Record<
|
||||
AppType,
|
||||
Pick<QuestionResults, "template" | "tools" | "dataSources" | "useCase"> & {
|
||||
modelConfig?: ModelConfig;
|
||||
}
|
||||
> = {
|
||||
agentic_rag: {
|
||||
template: "llamaindexserver",
|
||||
dataSources: [EXAMPLE_FILE],
|
||||
},
|
||||
financial_report: {
|
||||
template: "llamaindexserver",
|
||||
dataSources: EXAMPLE_10K_SEC_FILES,
|
||||
tools: getTools(["interpreter", "document_generator"]),
|
||||
modelConfig: MODEL_GPT41,
|
||||
},
|
||||
deep_research: {
|
||||
template: "llamaindexserver",
|
||||
dataSources: EXAMPLE_10K_SEC_FILES,
|
||||
tools: [],
|
||||
modelConfig: MODEL_GPT41,
|
||||
},
|
||||
artifacts: {
|
||||
template: "llamaindexserver",
|
||||
dataSources: [],
|
||||
tools: [],
|
||||
modelConfig: MODEL_GPT41,
|
||||
},
|
||||
};
|
||||
|
||||
const results = lookup[answers.appType];
|
||||
return {
|
||||
framework: answers.language,
|
||||
useCase: answers.appType,
|
||||
ui: "shadcn",
|
||||
llamaCloudKey: answers.llamaCloudKey,
|
||||
useLlamaParse: answers.useLlamaCloud,
|
||||
vectorDb: answers.useLlamaCloud ? "llamacloud" : "none",
|
||||
...results,
|
||||
modelConfig:
|
||||
results.modelConfig ??
|
||||
(await askModelConfig({
|
||||
openAiKey: args.openAiKey,
|
||||
askModels: args.askModels ?? false,
|
||||
framework: answers.language,
|
||||
})),
|
||||
frontend: true,
|
||||
};
|
||||
};
|
||||
@@ -1,15 +1,22 @@
|
||||
import { InstallAppArgs } from "../create-app";
|
||||
import {
|
||||
TemplateFramework,
|
||||
TemplatePostInstallAction,
|
||||
TemplateUseCase,
|
||||
TemplateVectorDB,
|
||||
} from "../helpers";
|
||||
|
||||
export type QuestionResults = Omit<
|
||||
InstallAppArgs,
|
||||
"appPath" | "packageManager"
|
||||
>;
|
||||
|
||||
export type PureQuestionArgs = {
|
||||
export type QuestionArgs = {
|
||||
useCase?: TemplateUseCase;
|
||||
framework?: TemplateFramework;
|
||||
askModels?: boolean;
|
||||
pro?: boolean;
|
||||
openAiKey?: string;
|
||||
llamaCloudKey?: string;
|
||||
port?: number;
|
||||
postInstallAction?: TemplatePostInstallAction;
|
||||
vectorDb?: TemplateVectorDB;
|
||||
};
|
||||
|
||||
export type QuestionArgs = QuestionResults & PureQuestionArgs;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
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(),
|
||||
},
|
||||
};
|
||||
@@ -4,7 +4,6 @@ import path from "path";
|
||||
import { red } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { TemplateDataSourceType, TemplatePostInstallAction } from "../helpers";
|
||||
import { toolsRequireConfig } from "../helpers/tools";
|
||||
import { QuestionResults } from "./types";
|
||||
|
||||
export const supportedContextFileTypes = [
|
||||
@@ -127,7 +126,7 @@ export const questionHandlers = {
|
||||
|
||||
// Ask for next action after installation
|
||||
export async function askPostInstallAction(
|
||||
args: QuestionResults,
|
||||
args: Omit<QuestionResults, "postInstallAction">,
|
||||
): Promise<TemplatePostInstallAction> {
|
||||
const actionChoices = [
|
||||
{
|
||||
@@ -144,19 +143,14 @@ export async function askPostInstallAction(
|
||||
},
|
||||
];
|
||||
|
||||
const modelConfigured = !args.llamapack && args.modelConfig.isConfigured();
|
||||
const modelConfigured = args.modelConfig.isConfigured();
|
||||
// If using LlamaParse, require LlamaCloud API key
|
||||
const llamaCloudKeyConfigured = args.useLlamaParse
|
||||
? args.llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
|
||||
: true;
|
||||
const hasVectorDb = args.vectorDb && args.vectorDb !== "none";
|
||||
// Can run the app if all tools do not require configuration
|
||||
if (
|
||||
!hasVectorDb &&
|
||||
modelConfigured &&
|
||||
llamaCloudKeyConfigured &&
|
||||
!toolsRequireConfig(args.tools)
|
||||
) {
|
||||
if (!hasVectorDb && modelConfigured && llamaCloudKeyConfigured) {
|
||||
actionChoices.push({
|
||||
title: "Generate code, install dependencies, and run the app (~2 min)",
|
||||
value: "runApp",
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from llama_index.core.agent import AgentRunner
|
||||
from llama_index.core.callbacks import CallbackManager
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.core.tools import BaseTool
|
||||
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
from app.engine.tools import ToolFactory
|
||||
from app.engine.tools.query_engine import get_query_engine_tool
|
||||
|
||||
|
||||
def get_chat_engine(params=None, event_handlers=None, **kwargs):
|
||||
system_prompt = os.getenv("SYSTEM_PROMPT")
|
||||
tools: List[BaseTool] = []
|
||||
callback_manager = CallbackManager(handlers=event_handlers or [])
|
||||
|
||||
# Add query tool if index exists
|
||||
index_config = IndexConfig(callback_manager=callback_manager, **(params or {}))
|
||||
index = get_index(index_config)
|
||||
if index is not None:
|
||||
query_engine_tool = get_query_engine_tool(index, **kwargs)
|
||||
tools.append(query_engine_tool)
|
||||
|
||||
# Add additional tools
|
||||
configured_tools: List[BaseTool] = ToolFactory.from_env()
|
||||
tools.extend(configured_tools)
|
||||
|
||||
return AgentRunner.from_llm(
|
||||
llm=Settings.llm,
|
||||
tools=tools,
|
||||
system_prompt=system_prompt,
|
||||
callback_manager=callback_manager,
|
||||
verbose=True,
|
||||
)
|
||||
@@ -1,78 +0,0 @@
|
||||
import importlib
|
||||
import os
|
||||
from typing import Dict, List, Union
|
||||
|
||||
import yaml # type: ignore
|
||||
from llama_index.core.tools.function_tool import FunctionTool
|
||||
from llama_index.core.tools.tool_spec.base import BaseToolSpec
|
||||
|
||||
|
||||
class ToolType:
|
||||
LLAMAHUB = "llamahub"
|
||||
LOCAL = "local"
|
||||
|
||||
|
||||
class ToolFactory:
|
||||
TOOL_SOURCE_PACKAGE_MAP = {
|
||||
ToolType.LLAMAHUB: "llama_index.tools",
|
||||
ToolType.LOCAL: "app.engine.tools",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def load_tools(tool_type: str, tool_name: str, config: dict) -> List[FunctionTool]:
|
||||
source_package = ToolFactory.TOOL_SOURCE_PACKAGE_MAP[tool_type]
|
||||
try:
|
||||
if "ToolSpec" in tool_name:
|
||||
tool_package, tool_cls_name = tool_name.split(".")
|
||||
module_name = f"{source_package}.{tool_package}"
|
||||
module = importlib.import_module(module_name)
|
||||
tool_class = getattr(module, tool_cls_name)
|
||||
tool_spec: BaseToolSpec = tool_class(**config)
|
||||
return tool_spec.to_tool_list()
|
||||
else:
|
||||
module = importlib.import_module(f"{source_package}.{tool_name}")
|
||||
tools = module.get_tools(**config)
|
||||
if not all(isinstance(tool, FunctionTool) for tool in tools):
|
||||
raise ValueError(
|
||||
f"The module {module} does not contain valid tools"
|
||||
)
|
||||
return tools
|
||||
except ImportError as e:
|
||||
raise ValueError(f"Failed to import tool {tool_name}: {e}")
|
||||
except AttributeError as e:
|
||||
raise ValueError(f"Failed to load tool {tool_name}: {e}")
|
||||
|
||||
@staticmethod
|
||||
def from_env(
|
||||
map_result: bool = False,
|
||||
) -> Union[Dict[str, List[FunctionTool]], List[FunctionTool]]:
|
||||
"""
|
||||
Load tools from the configured file.
|
||||
|
||||
Args:
|
||||
map_result: If True, return a map of tool names to their corresponding tools.
|
||||
|
||||
Returns:
|
||||
A dictionary of tool names to lists of FunctionTools if map_result is True,
|
||||
otherwise a list of FunctionTools.
|
||||
"""
|
||||
tools: Union[Dict[str, FunctionTool], List[FunctionTool]] = (
|
||||
{} if map_result else []
|
||||
)
|
||||
|
||||
if os.path.exists("config/tools.yaml"):
|
||||
with open("config/tools.yaml", "r") as f:
|
||||
tool_configs = yaml.safe_load(f)
|
||||
for tool_type, config_entries in tool_configs.items():
|
||||
for tool_name, config in config_entries.items():
|
||||
loaded_tools = ToolFactory.load_tools(
|
||||
tool_type, tool_name, config
|
||||
)
|
||||
if map_result:
|
||||
tools.update( # type: ignore
|
||||
{tool.metadata.name: tool for tool in loaded_tools}
|
||||
)
|
||||
else:
|
||||
tools.extend(loaded_tools) # type: ignore
|
||||
|
||||
return tools
|
||||
@@ -1,111 +0,0 @@
|
||||
import logging
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from llama_index.core.base.llms.types import ChatMessage
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.core.tools import FunctionTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Prompt based on https://github.com/e2b-dev/ai-artifacts
|
||||
CODE_GENERATION_PROMPT = """You are a skilled software engineer. You do not make mistakes. Generate an artifact. You can install additional dependencies. You can use one of the following templates:
|
||||
|
||||
1. code-interpreter-multilang: "Runs code as a Jupyter notebook cell. Strong data analysis angle. Can use complex visualisation to explain results.". File: script.py. Dependencies installed: python, jupyter, numpy, pandas, matplotlib, seaborn, plotly. Port: none.
|
||||
|
||||
2. nextjs-developer: "A Next.js 13+ app that reloads automatically. Using the pages router.". File: pages/index.tsx. Dependencies installed: nextjs@14.2.5, typescript, @types/node, @types/react, @types/react-dom, postcss, tailwindcss, shadcn. Port: 3000.
|
||||
|
||||
3. vue-developer: "A Vue.js 3+ app that reloads automatically. Only when asked specifically for a Vue app.". File: app.vue. Dependencies installed: vue@latest, nuxt@3.13.0, tailwindcss. Port: 3000.
|
||||
|
||||
4. streamlit-developer: "A streamlit app that reloads automatically.". File: app.py. Dependencies installed: streamlit, pandas, numpy, matplotlib, request, seaborn, plotly. Port: 8501.
|
||||
|
||||
5. gradio-developer: "A gradio app. Gradio Blocks/Interface should be called demo.". File: app.py. Dependencies installed: gradio, pandas, numpy, matplotlib, request, seaborn, plotly. Port: 7860.
|
||||
|
||||
Make sure to use the correct syntax for the programming language you're using.
|
||||
"""
|
||||
|
||||
|
||||
class CodeArtifact(BaseModel):
|
||||
commentary: str = Field(
|
||||
...,
|
||||
description="Describe what you're about to do and the steps you want to take for generating the artifact in great detail.",
|
||||
)
|
||||
template: str = Field(
|
||||
..., description="Name of the template used to generate the artifact."
|
||||
)
|
||||
title: str = Field(..., description="Short title of the artifact. Max 3 words.")
|
||||
description: str = Field(
|
||||
..., description="Short description of the artifact. Max 1 sentence."
|
||||
)
|
||||
additional_dependencies: List[str] = Field(
|
||||
...,
|
||||
description="Additional dependencies required by the artifact. Do not include dependencies that are already included in the template.",
|
||||
)
|
||||
has_additional_dependencies: bool = Field(
|
||||
...,
|
||||
description="Detect if additional dependencies that are not included in the template are required by the artifact.",
|
||||
)
|
||||
install_dependencies_command: str = Field(
|
||||
...,
|
||||
description="Command to install additional dependencies required by the artifact.",
|
||||
)
|
||||
port: Optional[int] = Field(
|
||||
...,
|
||||
description="Port number used by the resulted artifact. Null when no ports are exposed.",
|
||||
)
|
||||
file_path: str = Field(
|
||||
..., description="Relative path to the file, including the file name."
|
||||
)
|
||||
code: str = Field(
|
||||
...,
|
||||
description="Code generated by the artifact. Only runnable code is allowed.",
|
||||
)
|
||||
|
||||
|
||||
class CodeGeneratorTool:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def artifact(
|
||||
self,
|
||||
query: str,
|
||||
sandbox_files: Optional[List[str]] = None,
|
||||
old_code: Optional[str] = None,
|
||||
) -> Dict:
|
||||
"""Generate a code artifact based on the provided input.
|
||||
|
||||
Args:
|
||||
query (str): A description of the application you want to build.
|
||||
sandbox_files (Optional[List[str]], optional): A list of sandbox file paths. Defaults to None. Include these files if the code requires them.
|
||||
old_code (Optional[str], optional): The existing code to be modified. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Dict: A dictionary containing information about the generated artifact.
|
||||
"""
|
||||
|
||||
if old_code:
|
||||
user_message = f"{query}\n\nThe existing code is: \n```\n{old_code}\n```"
|
||||
else:
|
||||
user_message = query
|
||||
if sandbox_files:
|
||||
user_message += f"\n\nThe provided files are: \n{str(sandbox_files)}"
|
||||
|
||||
messages: List[ChatMessage] = [
|
||||
ChatMessage(role="system", content=CODE_GENERATION_PROMPT),
|
||||
ChatMessage(role="user", content=user_message),
|
||||
]
|
||||
try:
|
||||
sllm = Settings.llm.as_structured_llm(output_cls=CodeArtifact) # type: ignore
|
||||
response = sllm.chat(messages)
|
||||
data: CodeArtifact = response.raw
|
||||
data_dict = data.model_dump()
|
||||
if sandbox_files:
|
||||
data_dict["files"] = sandbox_files
|
||||
return data_dict
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate artifact: {str(e)}")
|
||||
raise e
|
||||
|
||||
|
||||
def get_tools(**kwargs):
|
||||
return [FunctionTool.from_defaults(fn=CodeGeneratorTool().artifact)]
|
||||
@@ -1,70 +0,0 @@
|
||||
from llama_index.core.tools.function_tool import FunctionTool
|
||||
|
||||
|
||||
def duckduckgo_search(
|
||||
query: str,
|
||||
region: str = "wt-wt",
|
||||
max_results: int = 10,
|
||||
):
|
||||
"""
|
||||
Use this function to search for any query in DuckDuckGo.
|
||||
Args:
|
||||
query (str): The query to search in DuckDuckGo.
|
||||
region Optional(str): The region to be used for the search in [country-language] convention, ex us-en, uk-en, ru-ru, etc...
|
||||
max_results Optional(int): The maximum number of results to be returned. Default is 10.
|
||||
"""
|
||||
try:
|
||||
from duckduckgo_search import DDGS
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"duckduckgo_search package is required to use this function."
|
||||
"Please install it by running: `poetry add duckduckgo_search` or `pip install duckduckgo_search`"
|
||||
)
|
||||
|
||||
results = []
|
||||
with DDGS() as ddg:
|
||||
results = list(
|
||||
ddg.text(
|
||||
keywords=query,
|
||||
region=region,
|
||||
max_results=max_results,
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def duckduckgo_image_search(
|
||||
query: str,
|
||||
region: str = "wt-wt",
|
||||
max_results: int = 10,
|
||||
):
|
||||
"""
|
||||
Use this function to search for images in DuckDuckGo.
|
||||
Args:
|
||||
query (str): The query to search in DuckDuckGo.
|
||||
region Optional(str): The region to be used for the search in [country-language] convention, ex us-en, uk-en, ru-ru, etc...
|
||||
max_results Optional(int): The maximum number of results to be returned. Default is 10.
|
||||
"""
|
||||
try:
|
||||
from duckduckgo_search import DDGS
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"duckduckgo_search package is required to use this function."
|
||||
"Please install it by running: `poetry add duckduckgo_search` or `pip install duckduckgo_search`"
|
||||
)
|
||||
with DDGS() as ddg:
|
||||
results = list(
|
||||
ddg.images(
|
||||
keywords=query,
|
||||
region=region,
|
||||
max_results=max_results,
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def get_tools(**kwargs):
|
||||
return [
|
||||
FunctionTool.from_defaults(duckduckgo_search),
|
||||
FunctionTool.from_defaults(duckduckgo_image_search),
|
||||
]
|
||||
-224
@@ -1,224 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from textwrap import dedent
|
||||
from typing import Optional
|
||||
|
||||
import pandas as pd
|
||||
from app.services.file import FileService
|
||||
from llama_index.core import Settings
|
||||
from llama_index.core.prompts import PromptTemplate
|
||||
from llama_index.core.tools import FunctionTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MissingCell(BaseModel):
|
||||
"""
|
||||
A missing cell in a table.
|
||||
"""
|
||||
|
||||
row_index: int = Field(description="The index of the row of the missing cell")
|
||||
column_index: int = Field(description="The index of the column of the missing cell")
|
||||
question_to_answer: str = Field(
|
||||
description="The question to answer to fill the missing cell"
|
||||
)
|
||||
|
||||
|
||||
class MissingCells(BaseModel):
|
||||
"""
|
||||
A list of missing cells.
|
||||
"""
|
||||
|
||||
missing_cells: list[MissingCell] = Field(description="The missing cells")
|
||||
|
||||
|
||||
class CellValue(BaseModel):
|
||||
row_index: int = Field(description="The row index of the cell")
|
||||
column_index: int = Field(description="The column index of the cell")
|
||||
value: str = Field(
|
||||
description="The value of the cell. Should be a concise value (numerical value or specific value)"
|
||||
)
|
||||
|
||||
|
||||
class FormFillingTool:
|
||||
"""
|
||||
Fill out missing cells in a CSV file using information from the knowledge base.
|
||||
"""
|
||||
|
||||
save_dir: str = os.path.join("output", "tools")
|
||||
|
||||
# Default prompt for extracting questions
|
||||
# Replace the default prompt with a custom prompt by setting the EXTRACT_QUESTIONS_PROMPT environment variable.
|
||||
_default_extract_questions_prompt = dedent(
|
||||
"""
|
||||
You are a data analyst. You are given a table with missing cells.
|
||||
Your task is to identify the missing cells and the questions needed to fill them.
|
||||
IMPORTANT: Column indices should be 0-based, where the first data column is index 1
|
||||
(index 0 is typically the row names/index column).
|
||||
|
||||
# Instructions:
|
||||
- Understand the entire content of the table and the topics of the table.
|
||||
- Identify the missing cells and the meaning of the data in the cells.
|
||||
- For each missing cell, provide the row index and the correct column index (remember: first data column is 1).
|
||||
- For each missing cell, provide the question needed to fill the cell (it's important to provide the question that is relevant to the topic of the table).
|
||||
- Since the cell's value should be concise, the question should request a numerical answer or a specific value.
|
||||
|
||||
# Example:
|
||||
# | | Name | Age | City |
|
||||
# |----|------|-----|------|
|
||||
# | 0 | John | | Paris|
|
||||
# | 1 | Mary | | |
|
||||
# | 2 | | 30 | |
|
||||
#
|
||||
# Your thoughts:
|
||||
# - The table is about people's names, ages, and cities.
|
||||
# - Row: 1, Column: 1 (Age column), Question: "How old is Mary? Please provide only the numerical answer."
|
||||
# - Row: 1, Column: 2 (City column), Question: "In which city does Mary live? Please provide only the city name."
|
||||
|
||||
|
||||
Please provide your answer in the requested format.
|
||||
# Here is your task:
|
||||
|
||||
- Table content:
|
||||
{table_content}
|
||||
|
||||
- Your answer:
|
||||
"""
|
||||
)
|
||||
|
||||
def extract_questions(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
file_content: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Use this tool to extract missing cells in a CSV file and generate questions to fill them.
|
||||
Pass either the path to the CSV file or the content of the CSV file.
|
||||
|
||||
Args:
|
||||
file_path (Optional[str]): The local file path to the CSV file to extract missing cells from (Don't pass a sandbox path).
|
||||
file_content (Optional[str]): The content of the CSV file to extract missing cells from.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the missing cells and their corresponding questions.
|
||||
"""
|
||||
extract_questions_prompt = os.getenv(
|
||||
"EXTRACT_QUESTIONS_PROMPT", self._default_extract_questions_prompt
|
||||
)
|
||||
if file_path is None and file_content is None:
|
||||
raise ValueError("Either `file_path` or `file_content` must be provided")
|
||||
|
||||
table_content = None
|
||||
|
||||
if file_path:
|
||||
file_name, file_extension = self._get_file_name_and_extension(
|
||||
file_path, file_content
|
||||
)
|
||||
|
||||
try:
|
||||
df = pd.read_csv(file_path)
|
||||
except FileNotFoundError as e:
|
||||
return {
|
||||
"error": str(e),
|
||||
"message": "Please check and update the file path and ensure it's a local path - not a sandbox path.",
|
||||
}
|
||||
|
||||
table_content = df.to_markdown()
|
||||
if table_content is None:
|
||||
raise ValueError("Could not convert the table to markdown")
|
||||
if file_content:
|
||||
table_content = file_content
|
||||
|
||||
if table_content is None:
|
||||
raise ValueError("Table content not found")
|
||||
|
||||
response: MissingCells = Settings.llm.structured_predict(
|
||||
output_cls=MissingCells,
|
||||
prompt=PromptTemplate(extract_questions_prompt),
|
||||
table_content=table_content,
|
||||
)
|
||||
return response.model_dump()
|
||||
|
||||
def fill_form(
|
||||
self,
|
||||
cell_values: list[CellValue],
|
||||
file_path: Optional[str] = None,
|
||||
file_content: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Use this tool to fill cell values into a CSV file.
|
||||
Requires cell values to be used for filling out, as well as either the path to the CSV file or the content of the CSV file.
|
||||
|
||||
Args:
|
||||
cell_values (list[CellValue]): The cell values used to fill out the CSV file (call `extract_questions` and query engine to construct the cell values).
|
||||
file_path (Optional[str]): The local file path to the CSV file that should be filled out (not as sandbox path).
|
||||
file_content (Optional[str]): The content of the CSV file that should be filled out.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the content and metadata of the filled-out file.
|
||||
"""
|
||||
file_name, file_extension = self._get_file_name_and_extension(
|
||||
file_path, file_content
|
||||
)
|
||||
df = pd.read_csv(file_path)
|
||||
|
||||
# Fill the dataframe with the cell values
|
||||
filled_df = df.copy()
|
||||
for cell_value in cell_values:
|
||||
if not isinstance(cell_value, CellValue):
|
||||
cell_value = CellValue(**cell_value)
|
||||
filled_df.iloc[cell_value.row_index, cell_value.column_index] = (
|
||||
cell_value.value
|
||||
)
|
||||
|
||||
# Save the filled table to a new CSV file
|
||||
csv_content: str = filled_df.to_csv(index=False)
|
||||
file_metadata = FileService.save_file(
|
||||
content=csv_content,
|
||||
file_name=f"{file_name}_filled.csv",
|
||||
save_dir=self.save_dir,
|
||||
)
|
||||
|
||||
new_content: str = filled_df.to_markdown()
|
||||
result = {
|
||||
"filled_content": new_content,
|
||||
"filled_file": file_metadata,
|
||||
}
|
||||
return result
|
||||
|
||||
def _get_file_name_and_extension(
|
||||
self, file_path: Optional[str], file_content: Optional[str]
|
||||
) -> tuple[str, str]:
|
||||
if file_path is None and file_content is None:
|
||||
raise ValueError("Either `file_path` or `file_content` must be provided")
|
||||
|
||||
if file_path is None:
|
||||
file_name = str(uuid.uuid4())
|
||||
file_extension = ".csv"
|
||||
else:
|
||||
file_name, file_extension = os.path.splitext(file_path)
|
||||
if file_extension != ".csv":
|
||||
raise ValueError("Form filling is only supported for CSV files")
|
||||
|
||||
return file_name, file_extension
|
||||
|
||||
def _save_output(self, file_name: str, output: str) -> dict:
|
||||
"""
|
||||
Save the output to a file.
|
||||
"""
|
||||
file_metadata = FileService.save_file(
|
||||
content=output,
|
||||
file_name=file_name,
|
||||
save_dir=self.save_dir,
|
||||
)
|
||||
return file_metadata.model_dump()
|
||||
|
||||
|
||||
def get_tools(**kwargs):
|
||||
tool = FormFillingTool()
|
||||
return [
|
||||
FunctionTool.from_defaults(tool.extract_questions),
|
||||
FunctionTool.from_defaults(tool.fill_form),
|
||||
]
|
||||
@@ -1,109 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
import requests # type: ignore
|
||||
from llama_index.core.tools import FunctionTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImageGeneratorToolOutput(BaseModel):
|
||||
is_success: bool = Field(
|
||||
...,
|
||||
description="Whether the image generation was successful.",
|
||||
)
|
||||
image_url: Optional[str] = Field(
|
||||
None,
|
||||
description="The URL of the generated image.",
|
||||
)
|
||||
error_message: Optional[str] = Field(
|
||||
None,
|
||||
description="The error message if the image generation failed.",
|
||||
)
|
||||
|
||||
|
||||
class ImageGeneratorTool:
|
||||
_IMG_OUTPUT_FORMAT = "webp"
|
||||
_IMG_OUTPUT_DIR = "output/tools"
|
||||
_IMG_GEN_API = "https://api.stability.ai/v2beta/stable-image/generate/core"
|
||||
|
||||
def __init__(self, api_key: str = None):
|
||||
if not api_key:
|
||||
api_key = os.getenv("STABILITY_API_KEY")
|
||||
self._api_key = api_key
|
||||
self.fileserver_url_prefix = os.getenv("FILESERVER_URL_PREFIX")
|
||||
if self._api_key is None:
|
||||
raise ValueError(
|
||||
"STABILITY_API_KEY key is required to run image generator. Get it here: https://platform.stability.ai/account/keys"
|
||||
)
|
||||
if self.fileserver_url_prefix is None:
|
||||
raise ValueError("FILESERVER_URL_PREFIX is required.")
|
||||
|
||||
def _prepare_output_dir(self):
|
||||
"""
|
||||
Create the output directory if it doesn't exist
|
||||
"""
|
||||
if not os.path.exists(self._IMG_OUTPUT_DIR):
|
||||
os.makedirs(self._IMG_OUTPUT_DIR, exist_ok=True)
|
||||
|
||||
def _save_image(self, image_data: bytes):
|
||||
self._prepare_output_dir()
|
||||
filename = f"{uuid.uuid4()}.{self._IMG_OUTPUT_FORMAT}"
|
||||
output_path = os.path.join(self._IMG_OUTPUT_DIR, filename)
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(image_data)
|
||||
url = f"{os.getenv('FILESERVER_URL_PREFIX')}/{self._IMG_OUTPUT_DIR}/{filename}"
|
||||
logger.info(f"Saved image to {output_path}.\nURL: {url}")
|
||||
return url
|
||||
|
||||
def _call_stability_api(self, prompt: str):
|
||||
headers = {
|
||||
"authorization": f"Bearer {self._api_key}",
|
||||
"accept": "image/*",
|
||||
}
|
||||
data = {
|
||||
"prompt": prompt,
|
||||
"output_format": self._IMG_OUTPUT_FORMAT,
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
self._IMG_GEN_API,
|
||||
headers=headers,
|
||||
files={"none": ""},
|
||||
data=data,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
return response
|
||||
|
||||
def generate_image(self, prompt: str) -> ImageGeneratorToolOutput:
|
||||
"""
|
||||
Use this tool to generate an image based on the prompt.
|
||||
Args:
|
||||
prompt (str): The prompt to generate the image from.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Call the Stability API
|
||||
response = self._call_stability_api(prompt)
|
||||
|
||||
# Save the image and get the URL
|
||||
image_url = self._save_image(response.content)
|
||||
|
||||
return ImageGeneratorToolOutput(
|
||||
is_success=True,
|
||||
image_url=image_url,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(e, exc_info=True)
|
||||
return ImageGeneratorToolOutput(
|
||||
is_success=False,
|
||||
error_message=str(e),
|
||||
)
|
||||
|
||||
|
||||
def get_tools(**kwargs):
|
||||
return [FunctionTool.from_defaults(ImageGeneratorTool(**kwargs).generate_image)]
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from llama_index.tools.openapi import OpenAPIToolSpec
|
||||
from llama_index.tools.requests import RequestsToolSpec
|
||||
|
||||
|
||||
class OpenAPIActionToolSpec(OpenAPIToolSpec, RequestsToolSpec):
|
||||
"""
|
||||
A combination of OpenAPI and Requests tool specs that can parse OpenAPI specs and make requests.
|
||||
|
||||
openapi_uri: str: The file path or URL to the OpenAPI spec.
|
||||
domain_headers: dict: Whitelist domains and the headers to use.
|
||||
"""
|
||||
|
||||
spec_functions = OpenAPIToolSpec.spec_functions + RequestsToolSpec.spec_functions
|
||||
# Cached parsed specs by URI
|
||||
_specs: Dict[str, Tuple[Dict, List[str]]] = {}
|
||||
|
||||
def __init__(self, openapi_uri: str, domain_headers: dict = None, **kwargs):
|
||||
if domain_headers is None:
|
||||
domain_headers = {}
|
||||
if openapi_uri not in self._specs:
|
||||
openapi_spec, servers = self._load_openapi_spec(openapi_uri)
|
||||
self._specs[openapi_uri] = (openapi_spec, servers)
|
||||
else:
|
||||
openapi_spec, servers = self._specs[openapi_uri]
|
||||
|
||||
# Add the servers to the domain headers if they are not already present
|
||||
for server in servers:
|
||||
if server not in domain_headers:
|
||||
domain_headers[server] = {}
|
||||
|
||||
OpenAPIToolSpec.__init__(self, spec=openapi_spec)
|
||||
RequestsToolSpec.__init__(self, domain_headers)
|
||||
|
||||
@staticmethod
|
||||
def _load_openapi_spec(uri: str) -> Tuple[Dict, List[str]]:
|
||||
"""
|
||||
Load an OpenAPI spec from a URI.
|
||||
|
||||
Args:
|
||||
uri (str): A file path or URL to the OpenAPI spec.
|
||||
|
||||
Returns:
|
||||
List[Document]: A list of Document objects.
|
||||
"""
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import yaml # type: ignore
|
||||
|
||||
if uri.startswith("http"):
|
||||
import requests # type: ignore
|
||||
|
||||
response = requests.get(uri)
|
||||
if response.status_code != 200:
|
||||
raise ValueError(
|
||||
"Could not initialize OpenAPIActionToolSpec: "
|
||||
f"Failed to load OpenAPI spec from {uri}, status code: {response.status_code}"
|
||||
)
|
||||
spec = yaml.safe_load(response.text)
|
||||
elif uri.startswith("file"):
|
||||
filepath = urlparse(uri).path
|
||||
with open(filepath, "r") as file:
|
||||
spec = yaml.safe_load(file)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Could not initialize OpenAPIActionToolSpec: Invalid OpenAPI URI provided. "
|
||||
"Only HTTP and file path are supported."
|
||||
)
|
||||
# Add the servers to the whitelist
|
||||
try:
|
||||
servers = [
|
||||
urlparse(server["url"]).netloc for server in spec.get("servers", [])
|
||||
]
|
||||
except KeyError as e:
|
||||
raise ValueError(
|
||||
"Could not initialize OpenAPIActionToolSpec: Invalid OpenAPI spec provided. "
|
||||
"Could not get `servers` from the spec."
|
||||
) from e
|
||||
return spec, servers
|
||||
-187
@@ -1,187 +0,0 @@
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional, Sequence
|
||||
|
||||
from llama_index.core import get_response_synthesizer
|
||||
from llama_index.core.base.base_query_engine import BaseQueryEngine
|
||||
from llama_index.core.base.response.schema import RESPONSE_TYPE, Response
|
||||
from llama_index.core.multi_modal_llms import MultiModalLLM
|
||||
from llama_index.core.prompts.base import BasePromptTemplate
|
||||
from llama_index.core.prompts.default_prompt_selectors import (
|
||||
DEFAULT_TEXT_QA_PROMPT_SEL,
|
||||
)
|
||||
from llama_index.core.query_engine.multi_modal import _get_image_and_text_nodes
|
||||
from llama_index.core.response_synthesizers.base import BaseSynthesizer, QueryTextType
|
||||
from llama_index.core.schema import (
|
||||
ImageNode,
|
||||
NodeWithScore,
|
||||
)
|
||||
from llama_index.core.tools.query_engine import QueryEngineTool
|
||||
from llama_index.core.types import RESPONSE_TEXT_TYPE
|
||||
|
||||
from app.settings import get_multi_modal_llm
|
||||
|
||||
|
||||
def create_query_engine(index, **kwargs) -> BaseQueryEngine:
|
||||
"""
|
||||
Create a query engine for the given index.
|
||||
|
||||
Args:
|
||||
index: The index to create a query engine for.
|
||||
params (optional): Additional parameters for the query engine, e.g: similarity_top_k
|
||||
"""
|
||||
|
||||
top_k = int(os.getenv("TOP_K", 0))
|
||||
if top_k != 0 and kwargs.get("filters") is None:
|
||||
kwargs["similarity_top_k"] = top_k
|
||||
multimodal_llm = get_multi_modal_llm()
|
||||
if multimodal_llm:
|
||||
kwargs["response_synthesizer"] = MultiModalSynthesizer(
|
||||
multimodal_model=multimodal_llm,
|
||||
)
|
||||
|
||||
# If index is index is LlamaCloudIndex
|
||||
# use auto_routed mode for better query results
|
||||
if index.__class__.__name__ == "LlamaCloudIndex":
|
||||
if kwargs.get("retrieval_mode") is None:
|
||||
kwargs["retrieval_mode"] = "auto_routed"
|
||||
if multimodal_llm:
|
||||
kwargs["retrieve_image_nodes"] = True
|
||||
return index.as_query_engine(**kwargs)
|
||||
|
||||
|
||||
def get_query_engine_tool(
|
||||
index,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> QueryEngineTool:
|
||||
"""
|
||||
Get a query engine tool for the given index.
|
||||
|
||||
Args:
|
||||
index: The index to create a query engine for.
|
||||
name (optional): The name of the tool.
|
||||
description (optional): The description of the tool.
|
||||
"""
|
||||
if name is None:
|
||||
name = "query_index"
|
||||
if description is None:
|
||||
description = (
|
||||
"Use this tool to retrieve information about the text corpus from an index."
|
||||
)
|
||||
query_engine = create_query_engine(index, **kwargs)
|
||||
return QueryEngineTool.from_defaults(
|
||||
query_engine=query_engine,
|
||||
name=name,
|
||||
description=description,
|
||||
)
|
||||
|
||||
|
||||
class MultiModalSynthesizer(BaseSynthesizer):
|
||||
"""
|
||||
A synthesizer that summarizes text nodes and uses a multi-modal LLM to generate a response.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
multimodal_model: MultiModalLLM,
|
||||
response_synthesizer: Optional[BaseSynthesizer] = None,
|
||||
text_qa_template: Optional[BasePromptTemplate] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._multi_modal_llm = multimodal_model
|
||||
self._response_synthesizer = response_synthesizer or get_response_synthesizer()
|
||||
self._text_qa_template = text_qa_template or DEFAULT_TEXT_QA_PROMPT_SEL
|
||||
|
||||
def _get_prompts(self, **kwargs) -> Dict[str, Any]:
|
||||
return {
|
||||
"text_qa_template": self._text_qa_template,
|
||||
}
|
||||
|
||||
def _update_prompts(self, prompts: Dict[str, Any]) -> None:
|
||||
if "text_qa_template" in prompts:
|
||||
self._text_qa_template = prompts["text_qa_template"]
|
||||
|
||||
async def aget_response(
|
||||
self,
|
||||
*args,
|
||||
**response_kwargs: Any,
|
||||
) -> RESPONSE_TEXT_TYPE:
|
||||
return await self._response_synthesizer.aget_response(*args, **response_kwargs)
|
||||
|
||||
def get_response(self, *args, **kwargs) -> RESPONSE_TEXT_TYPE:
|
||||
return self._response_synthesizer.get_response(*args, **kwargs)
|
||||
|
||||
async def asynthesize(
|
||||
self,
|
||||
query: QueryTextType,
|
||||
nodes: List[NodeWithScore],
|
||||
additional_source_nodes: Optional[Sequence[NodeWithScore]] = None,
|
||||
**response_kwargs: Any,
|
||||
) -> RESPONSE_TYPE:
|
||||
image_nodes, text_nodes = _get_image_and_text_nodes(nodes)
|
||||
|
||||
if len(image_nodes) == 0:
|
||||
return await self._response_synthesizer.asynthesize(query, text_nodes)
|
||||
|
||||
# Summarize the text nodes to avoid exceeding the token limit
|
||||
text_response = str(
|
||||
await self._response_synthesizer.asynthesize(query, text_nodes)
|
||||
)
|
||||
|
||||
fmt_prompt = self._text_qa_template.format(
|
||||
context_str=text_response,
|
||||
query_str=query.query_str, # type: ignore
|
||||
)
|
||||
|
||||
llm_response = await self._multi_modal_llm.acomplete(
|
||||
prompt=fmt_prompt,
|
||||
image_documents=[
|
||||
image_node.node
|
||||
for image_node in image_nodes
|
||||
if isinstance(image_node.node, ImageNode)
|
||||
],
|
||||
)
|
||||
|
||||
return Response(
|
||||
response=str(llm_response),
|
||||
source_nodes=nodes,
|
||||
metadata={"text_nodes": text_nodes, "image_nodes": image_nodes},
|
||||
)
|
||||
|
||||
def synthesize(
|
||||
self,
|
||||
query: QueryTextType,
|
||||
nodes: List[NodeWithScore],
|
||||
additional_source_nodes: Optional[Sequence[NodeWithScore]] = None,
|
||||
**response_kwargs: Any,
|
||||
) -> RESPONSE_TYPE:
|
||||
image_nodes, text_nodes = _get_image_and_text_nodes(nodes)
|
||||
|
||||
if len(image_nodes) == 0:
|
||||
return self._response_synthesizer.synthesize(query, text_nodes)
|
||||
|
||||
# Summarize the text nodes to avoid exceeding the token limit
|
||||
text_response = str(self._response_synthesizer.synthesize(query, text_nodes))
|
||||
|
||||
fmt_prompt = self._text_qa_template.format(
|
||||
context_str=text_response,
|
||||
query_str=query.query_str, # type: ignore
|
||||
)
|
||||
|
||||
llm_response = self._multi_modal_llm.complete(
|
||||
prompt=fmt_prompt,
|
||||
image_documents=[
|
||||
image_node.node
|
||||
for image_node in image_nodes
|
||||
if isinstance(image_node.node, ImageNode)
|
||||
],
|
||||
)
|
||||
|
||||
return Response(
|
||||
response=str(llm_response),
|
||||
source_nodes=nodes,
|
||||
metadata={"text_nodes": text_nodes, "image_nodes": image_nodes},
|
||||
)
|
||||
@@ -1,74 +0,0 @@
|
||||
"""Open Meteo weather map tool spec."""
|
||||
|
||||
import logging
|
||||
|
||||
import pytz # type: ignore
|
||||
import requests # type: ignore
|
||||
from llama_index.core.tools import FunctionTool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenMeteoWeather:
|
||||
geo_api = "https://geocoding-api.open-meteo.com/v1"
|
||||
weather_api = "https://api.open-meteo.com/v1"
|
||||
|
||||
@classmethod
|
||||
def _get_geo_location(cls, location: str) -> dict:
|
||||
"""Get geo location from location name."""
|
||||
params = {"name": location, "count": 10, "language": "en", "format": "json"}
|
||||
response = requests.get(f"{cls.geo_api}/search", params=params)
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to fetch geo location: {response.status_code}")
|
||||
else:
|
||||
data = response.json()
|
||||
result = data["results"][0]
|
||||
geo_location = {
|
||||
"id": result["id"],
|
||||
"name": result["name"],
|
||||
"latitude": result["latitude"],
|
||||
"longitude": result["longitude"],
|
||||
}
|
||||
return geo_location
|
||||
|
||||
@classmethod
|
||||
def get_weather_information(cls, location: str) -> dict:
|
||||
"""Use this function to get the weather of any given location.
|
||||
Note that the weather code should follow WMO Weather interpretation codes (WW):
|
||||
0: Clear sky
|
||||
1, 2, 3: Mainly clear, partly cloudy, and overcast
|
||||
45, 48: Fog and depositing rime fog
|
||||
51, 53, 55: Drizzle: Light, moderate, and dense intensity
|
||||
56, 57: Freezing Drizzle: Light and dense intensity
|
||||
61, 63, 65: Rain: Slight, moderate and heavy intensity
|
||||
66, 67: Freezing Rain: Light and heavy intensity
|
||||
71, 73, 75: Snow fall: Slight, moderate, and heavy intensity
|
||||
77: Snow grains
|
||||
80, 81, 82: Rain showers: Slight, moderate, and violent
|
||||
85, 86: Snow showers slight and heavy
|
||||
95: Thunderstorm: Slight or moderate
|
||||
96, 99: Thunderstorm with slight and heavy hail
|
||||
"""
|
||||
logger.info(
|
||||
f"Calling open-meteo api to get weather information of location: {location}"
|
||||
)
|
||||
geo_location = cls._get_geo_location(location)
|
||||
timezone = pytz.timezone("UTC").zone
|
||||
params = {
|
||||
"latitude": geo_location["latitude"],
|
||||
"longitude": geo_location["longitude"],
|
||||
"current": "temperature_2m,weather_code",
|
||||
"hourly": "temperature_2m,weather_code",
|
||||
"daily": "weather_code",
|
||||
"timezone": timezone,
|
||||
}
|
||||
response = requests.get(f"{cls.weather_api}/forecast", params=params)
|
||||
if response.status_code != 200:
|
||||
raise Exception(
|
||||
f"Failed to fetch weather information: {response.status_code}"
|
||||
)
|
||||
return response.json()
|
||||
|
||||
|
||||
def get_tools(**kwargs):
|
||||
return [FunctionTool.from_defaults(OpenMeteoWeather.get_weather_information)]
|
||||
@@ -1,47 +0,0 @@
|
||||
import os
|
||||
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
from app.engine.node_postprocessors import NodeCitationProcessor
|
||||
from fastapi import HTTPException
|
||||
from llama_index.core.callbacks import CallbackManager
|
||||
from llama_index.core.chat_engine import CondensePlusContextChatEngine
|
||||
from llama_index.core.memory import ChatMemoryBuffer
|
||||
from llama_index.core.settings import Settings
|
||||
|
||||
|
||||
def get_chat_engine(params=None, event_handlers=None, **kwargs):
|
||||
system_prompt = os.getenv("SYSTEM_PROMPT")
|
||||
citation_prompt = os.getenv("SYSTEM_CITATION_PROMPT", None)
|
||||
top_k = int(os.getenv("TOP_K", 0))
|
||||
llm = Settings.llm
|
||||
memory = ChatMemoryBuffer.from_defaults(
|
||||
token_limit=llm.metadata.context_window - 256
|
||||
)
|
||||
callback_manager = CallbackManager(handlers=event_handlers or [])
|
||||
|
||||
node_postprocessors = []
|
||||
if citation_prompt:
|
||||
node_postprocessors = [NodeCitationProcessor()]
|
||||
system_prompt = f"{system_prompt}\n{citation_prompt}"
|
||||
|
||||
index_config = IndexConfig(callback_manager=callback_manager, **(params or {}))
|
||||
index = get_index(index_config)
|
||||
if index is None:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=str(
|
||||
"StorageContext is empty - call 'uv run generate' to generate the storage first"
|
||||
),
|
||||
)
|
||||
if top_k != 0 and kwargs.get("similarity_top_k") is None:
|
||||
kwargs["similarity_top_k"] = top_k
|
||||
retriever = index.as_retriever(**kwargs)
|
||||
|
||||
return CondensePlusContextChatEngine(
|
||||
llm=llm,
|
||||
memory=memory,
|
||||
system_prompt=system_prompt,
|
||||
retriever=retriever,
|
||||
node_postprocessors=node_postprocessors, # type: ignore
|
||||
callback_manager=callback_manager,
|
||||
)
|
||||
@@ -1,21 +0,0 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from llama_index.core import QueryBundle
|
||||
from llama_index.core.postprocessor.types import BaseNodePostprocessor
|
||||
from llama_index.core.schema import NodeWithScore
|
||||
|
||||
|
||||
class NodeCitationProcessor(BaseNodePostprocessor):
|
||||
"""
|
||||
Append node_id into metadata for citation purpose.
|
||||
Config SYSTEM_CITATION_PROMPT in your runtime environment variable to enable this feature.
|
||||
"""
|
||||
|
||||
def _postprocess_nodes(
|
||||
self,
|
||||
nodes: List[NodeWithScore],
|
||||
query_bundle: Optional[QueryBundle] = None,
|
||||
) -> List[NodeWithScore]:
|
||||
for node_score in nodes:
|
||||
node_score.node.metadata["node_id"] = node_score.node.node_id
|
||||
return nodes
|
||||
@@ -1,36 +0,0 @@
|
||||
import { BaseChatEngine, BaseToolWithCall, LLMAgent } from "llamaindex";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { getDataSource } from "./index";
|
||||
import { createTools } from "./tools";
|
||||
import { createQueryEngineTool } from "./tools/query-engine";
|
||||
|
||||
export async function createChatEngine(documentIds?: string[], params?: any) {
|
||||
const tools: BaseToolWithCall[] = [];
|
||||
|
||||
// Add a query engine tool if we have a data source
|
||||
// Delete this code if you don't have a data source
|
||||
const index = await getDataSource(params);
|
||||
if (index) {
|
||||
tools.push(createQueryEngineTool(index, { documentIds }));
|
||||
}
|
||||
|
||||
const configFile = path.join("config", "tools.json");
|
||||
let toolConfig: any;
|
||||
try {
|
||||
// add tools from config file if it exists
|
||||
toolConfig = JSON.parse(await fs.readFile(configFile, "utf8"));
|
||||
} catch (e) {
|
||||
console.info(`Could not read ${configFile} file. Using no tools.`);
|
||||
}
|
||||
if (toolConfig) {
|
||||
tools.push(...(await createTools(toolConfig)));
|
||||
}
|
||||
|
||||
const agent = new LLMAgent({
|
||||
tools,
|
||||
systemPrompt: process.env.SYSTEM_PROMPT,
|
||||
}) as unknown as BaseChatEngine;
|
||||
|
||||
return agent;
|
||||
}
|
||||
-146
@@ -1,146 +0,0 @@
|
||||
import type { JSONSchemaType } from "ajv";
|
||||
import {
|
||||
BaseTool,
|
||||
ChatMessage,
|
||||
JSONValue,
|
||||
Settings,
|
||||
ToolMetadata,
|
||||
} from "llamaindex";
|
||||
|
||||
// prompt based on https://github.com/e2b-dev/ai-artifacts
|
||||
const CODE_GENERATION_PROMPT = `You are a skilled software engineer. You do not make mistakes. Generate an artifact. You can install additional dependencies. You can use one of the following templates:\n
|
||||
|
||||
1. code-interpreter-multilang: "Runs code as a Jupyter notebook cell. Strong data analysis angle. Can use complex visualisation to explain results.". File: script.py. Dependencies installed: python, jupyter, numpy, pandas, matplotlib, seaborn, plotly. Port: none.
|
||||
|
||||
2. nextjs-developer: "A Next.js 13+ app that reloads automatically. Using the pages router.". File: pages/index.tsx. Dependencies installed: nextjs@14.2.5, typescript, @types/node, @types/react, @types/react-dom, postcss, tailwindcss, shadcn. Port: 3000.
|
||||
|
||||
3. vue-developer: "A Vue.js 3+ app that reloads automatically. Only when asked specifically for a Vue app.". File: app.vue. Dependencies installed: vue@latest, nuxt@3.13.0, tailwindcss. Port: 3000.
|
||||
|
||||
4. streamlit-developer: "A streamlit app that reloads automatically.". File: app.py. Dependencies installed: streamlit, pandas, numpy, matplotlib, request, seaborn, plotly. Port: 8501.
|
||||
|
||||
5. gradio-developer: "A gradio app. Gradio Blocks/Interface should be called demo.". File: app.py. Dependencies installed: gradio, pandas, numpy, matplotlib, request, seaborn, plotly. Port: 7860.
|
||||
|
||||
Provide detail information about the artifact you're about to generate in the following JSON format with the following keys:
|
||||
|
||||
commentary: Describe what you're about to do and the steps you want to take for generating the artifact in great detail.
|
||||
template: Name of the template used to generate the artifact.
|
||||
title: Short title of the artifact. Max 3 words.
|
||||
description: Short description of the artifact. Max 1 sentence.
|
||||
additional_dependencies: Additional dependencies required by the artifact. Do not include dependencies that are already included in the template.
|
||||
has_additional_dependencies: Detect if additional dependencies that are not included in the template are required by the artifact.
|
||||
install_dependencies_command: Command to install additional dependencies required by the artifact.
|
||||
port: Port number used by the resulted artifact. Null when no ports are exposed.
|
||||
file_path: Relative path to the file, including the file name.
|
||||
code: Code generated by the artifact. Only runnable code is allowed.
|
||||
|
||||
Make sure to use the correct syntax for the programming language you're using. Make sure to generate only one code file. If you need to use CSS, make sure to include the CSS in the code file using Tailwind CSS syntax.
|
||||
`;
|
||||
|
||||
// detail information to execute code
|
||||
export type CodeArtifact = {
|
||||
commentary: string;
|
||||
template: string;
|
||||
title: string;
|
||||
description: string;
|
||||
additional_dependencies: string[];
|
||||
has_additional_dependencies: boolean;
|
||||
install_dependencies_command: string;
|
||||
port: number | null;
|
||||
file_path: string;
|
||||
code: string;
|
||||
files?: string[];
|
||||
};
|
||||
|
||||
export type CodeGeneratorParameter = {
|
||||
requirement: string;
|
||||
oldCode?: string;
|
||||
sandboxFiles?: string[];
|
||||
};
|
||||
|
||||
export type CodeGeneratorToolParams = {
|
||||
metadata?: ToolMetadata<JSONSchemaType<CodeGeneratorParameter>>;
|
||||
};
|
||||
|
||||
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<CodeGeneratorParameter>> =
|
||||
{
|
||||
name: "artifact",
|
||||
description: `Generate a code artifact based on the input. Don't call this tool if the user has not asked for code generation. E.g. if the user asks to write a description or specification, don't call this tool.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
requirement: {
|
||||
type: "string",
|
||||
description: "The description of the application you want to build.",
|
||||
},
|
||||
oldCode: {
|
||||
type: "string",
|
||||
description: "The existing code to be modified",
|
||||
nullable: true,
|
||||
},
|
||||
sandboxFiles: {
|
||||
type: "array",
|
||||
description:
|
||||
"A list of sandbox file paths. Include these files if the code requires them.",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
required: ["requirement"],
|
||||
},
|
||||
};
|
||||
|
||||
export class CodeGeneratorTool implements BaseTool<CodeGeneratorParameter> {
|
||||
metadata: ToolMetadata<JSONSchemaType<CodeGeneratorParameter>>;
|
||||
|
||||
constructor(params?: CodeGeneratorToolParams) {
|
||||
this.metadata = params?.metadata || DEFAULT_META_DATA;
|
||||
}
|
||||
|
||||
async call(input: CodeGeneratorParameter) {
|
||||
try {
|
||||
const artifact = await this.generateArtifact(
|
||||
input.requirement,
|
||||
input.oldCode,
|
||||
input.sandboxFiles, // help the generated code use exact files
|
||||
);
|
||||
if (input.sandboxFiles) {
|
||||
artifact.files = input.sandboxFiles;
|
||||
}
|
||||
return artifact as JSONValue;
|
||||
} catch (error) {
|
||||
return { isError: true };
|
||||
}
|
||||
}
|
||||
|
||||
// Generate artifact (code, environment, dependencies, etc.)
|
||||
async generateArtifact(
|
||||
query: string,
|
||||
oldCode?: string,
|
||||
attachments?: string[],
|
||||
): Promise<CodeArtifact> {
|
||||
const userMessage = `
|
||||
${query}
|
||||
${oldCode ? `The existing code is: \n\`\`\`${oldCode}\`\`\`` : ""}
|
||||
${attachments ? `The attachments are: \n${attachments.join("\n")}` : ""}
|
||||
`;
|
||||
const messages: ChatMessage[] = [
|
||||
{ role: "system", content: CODE_GENERATION_PROMPT },
|
||||
{ role: "user", content: userMessage },
|
||||
];
|
||||
try {
|
||||
const response = await Settings.llm.chat({ messages });
|
||||
const content = response.message.content.toString();
|
||||
const jsonContent = content
|
||||
.replace(/^```json\s*|\s*```$/g, "")
|
||||
.replace(/^`+|`+$/g, "")
|
||||
.trim();
|
||||
const artifact = JSON.parse(jsonContent) as CodeArtifact;
|
||||
return artifact;
|
||||
} catch (error) {
|
||||
console.log("Failed to generate artifact", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
-142
@@ -1,142 +0,0 @@
|
||||
import { JSONSchemaType } from "ajv";
|
||||
import { BaseTool, ToolMetadata } from "llamaindex";
|
||||
import { marked } from "marked";
|
||||
import path from "node:path";
|
||||
import { saveDocument } from "../../llamaindex/documents/helper";
|
||||
|
||||
const OUTPUT_DIR = "output/tools";
|
||||
|
||||
type DocumentParameter = {
|
||||
originalContent: string;
|
||||
fileName: string;
|
||||
};
|
||||
|
||||
const DEFAULT_METADATA: ToolMetadata<JSONSchemaType<DocumentParameter>> = {
|
||||
name: "document_generator",
|
||||
description:
|
||||
"Generate HTML document from markdown content. Return a file url to the document",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
originalContent: {
|
||||
type: "string",
|
||||
description: "The original markdown content to convert.",
|
||||
},
|
||||
fileName: {
|
||||
type: "string",
|
||||
description: "The name of the document file (without extension).",
|
||||
},
|
||||
},
|
||||
required: ["originalContent", "fileName"],
|
||||
},
|
||||
};
|
||||
|
||||
const COMMON_STYLES = `
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.3;
|
||||
color: #333;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 0.7em;
|
||||
}
|
||||
code {
|
||||
background-color: #f4f4f4;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
pre {
|
||||
background-color: #f4f4f4;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background-color: #f2f2f2;
|
||||
font-weight: bold;
|
||||
}
|
||||
img {
|
||||
max-width: 90%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 1em auto;
|
||||
border-radius: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const HTML_SPECIFIC_STYLES = `
|
||||
body {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
const HTML_TEMPLATE = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
${COMMON_STYLES}
|
||||
${HTML_SPECIFIC_STYLES}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{content}}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
export interface DocumentGeneratorParams {
|
||||
metadata?: ToolMetadata<JSONSchemaType<DocumentParameter>>;
|
||||
}
|
||||
|
||||
export class DocumentGenerator implements BaseTool<DocumentParameter> {
|
||||
metadata: ToolMetadata<JSONSchemaType<DocumentParameter>>;
|
||||
|
||||
constructor(params: DocumentGeneratorParams) {
|
||||
this.metadata = params.metadata ?? DEFAULT_METADATA;
|
||||
}
|
||||
|
||||
private static async generateHtmlContent(
|
||||
originalContent: string,
|
||||
): Promise<string> {
|
||||
return await marked(originalContent);
|
||||
}
|
||||
|
||||
private static generateHtmlDocument(htmlContent: string): string {
|
||||
return HTML_TEMPLATE.replace("{{content}}", htmlContent);
|
||||
}
|
||||
|
||||
async call(input: DocumentParameter): Promise<string> {
|
||||
const { originalContent, fileName } = input;
|
||||
|
||||
const htmlContent =
|
||||
await DocumentGenerator.generateHtmlContent(originalContent);
|
||||
const fileContent = DocumentGenerator.generateHtmlDocument(htmlContent);
|
||||
|
||||
const filePath = path.join(OUTPUT_DIR, `${fileName}.html`);
|
||||
|
||||
return `URL: ${await saveDocument(filePath, fileContent)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getTools(): BaseTool[] {
|
||||
return [new DocumentGenerator({})];
|
||||
}
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
import { JSONSchemaType } from "ajv";
|
||||
import { search } from "duck-duck-scrape";
|
||||
import { BaseTool, ToolMetadata } from "llamaindex";
|
||||
|
||||
export type DuckDuckGoParameter = {
|
||||
query: string;
|
||||
region?: string;
|
||||
maxResults?: number;
|
||||
};
|
||||
|
||||
export type DuckDuckGoToolParams = {
|
||||
metadata?: ToolMetadata<JSONSchemaType<DuckDuckGoParameter>>;
|
||||
};
|
||||
|
||||
const DEFAULT_SEARCH_METADATA: ToolMetadata<
|
||||
JSONSchemaType<DuckDuckGoParameter>
|
||||
> = {
|
||||
name: "duckduckgo_search",
|
||||
description:
|
||||
"Use this function to search for information (only text) in the internet using DuckDuckGo.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "The query to search in DuckDuckGo.",
|
||||
},
|
||||
region: {
|
||||
type: "string",
|
||||
description:
|
||||
"Optional, The region to be used for the search in [country-language] convention, ex us-en, uk-en, ru-ru, etc...",
|
||||
nullable: true,
|
||||
},
|
||||
maxResults: {
|
||||
type: "number",
|
||||
description:
|
||||
"Optional, The maximum number of results to be returned. Default is 10.",
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
};
|
||||
|
||||
type DuckDuckGoSearchResult = {
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export class DuckDuckGoSearchTool implements BaseTool<DuckDuckGoParameter> {
|
||||
metadata: ToolMetadata<JSONSchemaType<DuckDuckGoParameter>>;
|
||||
|
||||
constructor(params: DuckDuckGoToolParams) {
|
||||
this.metadata = params.metadata ?? DEFAULT_SEARCH_METADATA;
|
||||
}
|
||||
|
||||
async call(input: DuckDuckGoParameter) {
|
||||
const { query, region, maxResults = 10 } = input;
|
||||
const options = region ? { region } : {};
|
||||
// Temporarily sleep to reduce overloading the DuckDuckGo
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
const searchResults = await search(query, options);
|
||||
|
||||
return searchResults.results.slice(0, maxResults).map((result) => {
|
||||
return {
|
||||
title: result.title,
|
||||
description: result.description,
|
||||
url: result.url,
|
||||
} as DuckDuckGoSearchResult;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getTools() {
|
||||
return [new DuckDuckGoSearchTool({})];
|
||||
}
|
||||
-296
@@ -1,296 +0,0 @@
|
||||
import { JSONSchemaType } from "ajv";
|
||||
import fs from "fs";
|
||||
import { BaseTool, Settings, ToolMetadata } from "llamaindex";
|
||||
import Papa from "papaparse";
|
||||
import path from "path";
|
||||
import { saveDocument } from "../../llamaindex/documents/helper";
|
||||
|
||||
type ExtractMissingCellsParameter = {
|
||||
filePath: string;
|
||||
};
|
||||
|
||||
export type MissingCell = {
|
||||
rowIndex: number;
|
||||
columnIndex: number;
|
||||
question: string;
|
||||
};
|
||||
|
||||
const CSV_EXTRACTION_PROMPT = `You are a data analyst. You are given a table with missing cells.
|
||||
Your task is to identify the missing cells and the questions needed to fill them.
|
||||
IMPORTANT: Column indices should be 0-based
|
||||
|
||||
# Instructions:
|
||||
- Understand the entire content of the table and the topics of the table.
|
||||
- Identify the missing cells and the meaning of the data in the cells.
|
||||
- For each missing cell, provide the row index and the correct column index (remember: first data column is 1).
|
||||
- For each missing cell, provide the question needed to fill the cell (it's important to provide the question that is relevant to the topic of the table).
|
||||
- Since the cell's value should be concise, the question should request a numerical answer or a specific value.
|
||||
- Finally, only return the answer in JSON format with the following schema:
|
||||
{
|
||||
"missing_cells": [
|
||||
{
|
||||
"rowIndex": number,
|
||||
"columnIndex": number,
|
||||
"question": string
|
||||
}
|
||||
]
|
||||
}
|
||||
- If there are no missing cells, return an empty array.
|
||||
- The answer is only the JSON object, nothing else and don't wrap it inside markdown code block.
|
||||
|
||||
# Example:
|
||||
# | | Name | Age | City |
|
||||
# |----|------|-----|------|
|
||||
# | 0 | John | | Paris|
|
||||
# | 1 | Mary | | |
|
||||
# | 2 | | 30 | |
|
||||
#
|
||||
# Your thoughts:
|
||||
# - The table is about people's names, ages, and cities.
|
||||
# - Row: 1, Column: 2 (Age column), Question: "How old is Mary? Please provide only the numerical answer."
|
||||
# - Row: 1, Column: 3 (City column), Question: "In which city does Mary live? Please provide only the city name."
|
||||
# Your answer:
|
||||
# {
|
||||
# "missing_cells": [
|
||||
# {
|
||||
# "rowIndex": 1,
|
||||
# "columnIndex": 2,
|
||||
# "question": "How old is Mary? Please provide only the numerical answer."
|
||||
# },
|
||||
# {
|
||||
# "rowIndex": 1,
|
||||
# "columnIndex": 3,
|
||||
# "question": "In which city does Mary live? Please provide only the city name."
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
|
||||
|
||||
# Here is your task:
|
||||
|
||||
- Table content:
|
||||
{table_content}
|
||||
|
||||
- Your answer:
|
||||
`;
|
||||
|
||||
const DEFAULT_METADATA: ToolMetadata<
|
||||
JSONSchemaType<ExtractMissingCellsParameter>
|
||||
> = {
|
||||
name: "extract_missing_cells",
|
||||
description: `Use this tool to extract missing cells in a CSV file and generate questions to fill them. This tool only works with local file path.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
filePath: {
|
||||
type: "string",
|
||||
description: "The local file path to the CSV file.",
|
||||
},
|
||||
},
|
||||
required: ["filePath"],
|
||||
},
|
||||
};
|
||||
|
||||
export interface ExtractMissingCellsParams {
|
||||
metadata?: ToolMetadata<JSONSchemaType<ExtractMissingCellsParameter>>;
|
||||
}
|
||||
|
||||
export class ExtractMissingCellsTool
|
||||
implements BaseTool<ExtractMissingCellsParameter>
|
||||
{
|
||||
metadata: ToolMetadata<JSONSchemaType<ExtractMissingCellsParameter>>;
|
||||
defaultExtractionPrompt: string;
|
||||
|
||||
constructor(params: ExtractMissingCellsParams) {
|
||||
this.metadata = params.metadata ?? DEFAULT_METADATA;
|
||||
this.defaultExtractionPrompt = CSV_EXTRACTION_PROMPT;
|
||||
}
|
||||
|
||||
private readCsvFile(filePath: string): Promise<string[][]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(filePath, "utf8", (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedData = Papa.parse<string[]>(data, {
|
||||
skipEmptyLines: false,
|
||||
});
|
||||
|
||||
if (parsedData.errors.length) {
|
||||
reject(parsedData.errors);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure all rows have the same number of columns as the header
|
||||
const maxColumns = parsedData.data[0].length;
|
||||
const paddedRows = parsedData.data.map((row) => {
|
||||
return [...row, ...Array(maxColumns - row.length).fill("")];
|
||||
});
|
||||
|
||||
resolve(paddedRows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private formatToMarkdownTable(data: string[][]): string {
|
||||
if (data.length === 0) return "";
|
||||
|
||||
const maxColumns = data[0].length;
|
||||
|
||||
const headerRow = `| ${data[0].join(" | ")} |`;
|
||||
const separatorRow = `| ${Array(maxColumns).fill("---").join(" | ")} |`;
|
||||
|
||||
const dataRows = data.slice(1).map((row) => {
|
||||
return `| ${row.join(" | ")} |`;
|
||||
});
|
||||
|
||||
return [headerRow, separatorRow, ...dataRows].join("\n");
|
||||
}
|
||||
|
||||
async call(input: ExtractMissingCellsParameter): Promise<MissingCell[]> {
|
||||
const { filePath } = input;
|
||||
let tableContent: string[][];
|
||||
try {
|
||||
tableContent = await this.readCsvFile(filePath);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to read CSV file. Make sure that you are reading a local file path (not a sandbox path).`,
|
||||
);
|
||||
}
|
||||
|
||||
const prompt = this.defaultExtractionPrompt.replace(
|
||||
"{table_content}",
|
||||
this.formatToMarkdownTable(tableContent),
|
||||
);
|
||||
|
||||
const llm = Settings.llm;
|
||||
const response = await llm.complete({
|
||||
prompt,
|
||||
});
|
||||
const rawAnswer = response.text;
|
||||
const parsedResponse = JSON.parse(rawAnswer) as {
|
||||
missing_cells: MissingCell[];
|
||||
};
|
||||
if (!parsedResponse.missing_cells) {
|
||||
throw new Error(
|
||||
"The answer is not in the correct format. There should be a missing_cells array.",
|
||||
);
|
||||
}
|
||||
const answer = parsedResponse.missing_cells;
|
||||
|
||||
return answer;
|
||||
}
|
||||
}
|
||||
|
||||
type FillMissingCellsParameter = {
|
||||
filePath: string;
|
||||
cells: {
|
||||
rowIndex: number;
|
||||
columnIndex: number;
|
||||
answer: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
const FILL_CELLS_METADATA: ToolMetadata<
|
||||
JSONSchemaType<FillMissingCellsParameter>
|
||||
> = {
|
||||
name: "fill_missing_cells",
|
||||
description: `Use this tool to fill missing cells in a CSV file with provided answers. This tool only works with local file path.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
filePath: {
|
||||
type: "string",
|
||||
description: "The local file path to the CSV file.",
|
||||
},
|
||||
cells: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
rowIndex: { type: "number" },
|
||||
columnIndex: { type: "number" },
|
||||
answer: { type: "string" },
|
||||
},
|
||||
required: ["rowIndex", "columnIndex", "answer"],
|
||||
},
|
||||
description: "Array of cells to fill with their answers",
|
||||
},
|
||||
},
|
||||
required: ["filePath", "cells"],
|
||||
},
|
||||
};
|
||||
|
||||
export interface FillMissingCellsParams {
|
||||
metadata?: ToolMetadata<JSONSchemaType<FillMissingCellsParameter>>;
|
||||
}
|
||||
|
||||
export class FillMissingCellsTool
|
||||
implements BaseTool<FillMissingCellsParameter>
|
||||
{
|
||||
metadata: ToolMetadata<JSONSchemaType<FillMissingCellsParameter>>;
|
||||
|
||||
constructor(params: FillMissingCellsParams = {}) {
|
||||
this.metadata = params.metadata ?? FILL_CELLS_METADATA;
|
||||
}
|
||||
|
||||
async call(input: FillMissingCellsParameter): Promise<string> {
|
||||
const { filePath, cells } = input;
|
||||
|
||||
// Read the CSV file
|
||||
const fileContent = await new Promise<string>((resolve, reject) => {
|
||||
fs.readFile(filePath, "utf8", (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Parse CSV with PapaParse
|
||||
const parseResult = Papa.parse<string[]>(fileContent, {
|
||||
header: false, // Ensure the header is not treated as a separate object
|
||||
skipEmptyLines: false, // Ensure empty lines are not skipped
|
||||
});
|
||||
|
||||
if (parseResult.errors.length) {
|
||||
throw new Error(
|
||||
"Failed to parse CSV file: " + parseResult.errors[0].message,
|
||||
);
|
||||
}
|
||||
|
||||
const rows = parseResult.data;
|
||||
|
||||
// Fill the cells with answers
|
||||
for (const cell of cells) {
|
||||
// Adjust rowIndex to start from 1 for data rows
|
||||
const adjustedRowIndex = cell.rowIndex + 1;
|
||||
if (
|
||||
adjustedRowIndex < rows.length &&
|
||||
cell.columnIndex < rows[adjustedRowIndex].length
|
||||
) {
|
||||
rows[adjustedRowIndex][cell.columnIndex] = cell.answer;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert back to CSV format
|
||||
const updatedContent = Papa.unparse(rows, {
|
||||
delimiter: parseResult.meta.delimiter,
|
||||
});
|
||||
|
||||
// Use the helper function to write the file
|
||||
const parsedPath = path.parse(filePath);
|
||||
const newFileName = `${parsedPath.name}-filled${parsedPath.ext}`;
|
||||
const newFilePath = path.join("output/tools", newFileName);
|
||||
|
||||
const newFileUrl = await saveDocument(newFilePath, updatedContent);
|
||||
|
||||
return (
|
||||
"Successfully filled missing cells in the CSV file. File URL to show to the user: " +
|
||||
newFileUrl
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import type { JSONSchemaType } from "ajv";
|
||||
import { FormData } from "formdata-node";
|
||||
import fs from "fs";
|
||||
import got from "got";
|
||||
import { BaseTool, ToolMetadata } from "llamaindex";
|
||||
import path from "node:path";
|
||||
import { Readable } from "stream";
|
||||
|
||||
export type ImgGeneratorParameter = {
|
||||
prompt: string;
|
||||
};
|
||||
|
||||
export type ImgGeneratorToolParams = {
|
||||
metadata?: ToolMetadata<JSONSchemaType<ImgGeneratorParameter>>;
|
||||
};
|
||||
|
||||
export type ImgGeneratorToolOutput = {
|
||||
isSuccess: boolean;
|
||||
imageUrl?: string;
|
||||
errorMessage?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<ImgGeneratorParameter>> = {
|
||||
name: "image_generator",
|
||||
description: `Use this function to generate an image based on the prompt.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
prompt: {
|
||||
type: "string",
|
||||
description: "The prompt to generate the image",
|
||||
},
|
||||
},
|
||||
required: ["prompt"],
|
||||
},
|
||||
};
|
||||
|
||||
export class ImgGeneratorTool implements BaseTool<ImgGeneratorParameter> {
|
||||
readonly IMG_OUTPUT_FORMAT = "webp";
|
||||
readonly IMG_OUTPUT_DIR = "output/tools";
|
||||
readonly IMG_GEN_API =
|
||||
"https://api.stability.ai/v2beta/stable-image/generate/core";
|
||||
|
||||
metadata: ToolMetadata<JSONSchemaType<ImgGeneratorParameter>>;
|
||||
|
||||
constructor(params?: ImgGeneratorToolParams) {
|
||||
this.checkRequiredEnvVars();
|
||||
this.metadata = params?.metadata || DEFAULT_META_DATA;
|
||||
}
|
||||
|
||||
async call(input: ImgGeneratorParameter): Promise<ImgGeneratorToolOutput> {
|
||||
return await this.generateImage(input.prompt);
|
||||
}
|
||||
|
||||
private generateImage = async (
|
||||
prompt: string,
|
||||
): Promise<ImgGeneratorToolOutput> => {
|
||||
try {
|
||||
const buffer = await this.promptToImgBuffer(prompt);
|
||||
const imageUrl = this.saveImage(buffer);
|
||||
return { isSuccess: true, imageUrl };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
isSuccess: false,
|
||||
errorMessage: "Failed to generate image. Please try again.",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
private promptToImgBuffer = async (prompt: string) => {
|
||||
const form = new FormData();
|
||||
form.append("prompt", prompt);
|
||||
form.append("output_format", this.IMG_OUTPUT_FORMAT);
|
||||
const buffer = await got
|
||||
.post(this.IMG_GEN_API, {
|
||||
// Not sure why it shows an type error when passing form to body
|
||||
// Although I follow document: https://github.com/sindresorhus/got/blob/main/documentation/2-options.md#body
|
||||
// Tt still works fine, so I make casting to unknown to avoid the typescript warning
|
||||
// Found a similar issue: https://github.com/sindresorhus/got/discussions/1877
|
||||
body: form as unknown as Buffer | Readable | string,
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.STABILITY_API_KEY}`,
|
||||
Accept: "image/*",
|
||||
},
|
||||
})
|
||||
.buffer();
|
||||
return buffer;
|
||||
};
|
||||
|
||||
private saveImage = (buffer: Buffer) => {
|
||||
const filename = `${crypto.randomUUID()}.${this.IMG_OUTPUT_FORMAT}`;
|
||||
const outputPath = path.join(this.IMG_OUTPUT_DIR, filename);
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
const url = `${process.env.FILESERVER_URL_PREFIX}/${this.IMG_OUTPUT_DIR}/${filename}`;
|
||||
console.log(`Saved image to ${outputPath}.\nURL: ${url}`);
|
||||
return url;
|
||||
};
|
||||
|
||||
private checkRequiredEnvVars = () => {
|
||||
if (!process.env.STABILITY_API_KEY) {
|
||||
throw new Error(
|
||||
"STABILITY_API_KEY key is required to run image generator. Get it here: https://platform.stability.ai/account/keys",
|
||||
);
|
||||
}
|
||||
if (!process.env.FILESERVER_URL_PREFIX) {
|
||||
throw new Error(
|
||||
"FILESERVER_URL_PREFIX is required to display file output after generation",
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import { BaseToolWithCall } from "llamaindex";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { CodeGeneratorTool, CodeGeneratorToolParams } from "./code-generator";
|
||||
import {
|
||||
DocumentGenerator,
|
||||
DocumentGeneratorParams,
|
||||
} from "./document-generator";
|
||||
import { DuckDuckGoSearchTool, DuckDuckGoToolParams } from "./duckduckgo";
|
||||
import {
|
||||
ExtractMissingCellsParams,
|
||||
ExtractMissingCellsTool,
|
||||
FillMissingCellsParams,
|
||||
FillMissingCellsTool,
|
||||
} from "./form-filling";
|
||||
import { ImgGeneratorTool, ImgGeneratorToolParams } from "./img-gen";
|
||||
import { InterpreterTool, InterpreterToolParams } from "./interpreter";
|
||||
import { OpenAPIActionTool } from "./openapi-action";
|
||||
import { WeatherTool, WeatherToolParams } from "./weather";
|
||||
import { WikipediaTool, WikipediaToolParams } from "./wikipedia";
|
||||
|
||||
type ToolCreator = (config: unknown) => Promise<BaseToolWithCall[]>;
|
||||
|
||||
export async function createTools(toolConfig: {
|
||||
local: Record<string, unknown>;
|
||||
llamahub: any;
|
||||
}): Promise<BaseToolWithCall[]> {
|
||||
// add local tools from the 'tools' folder (if configured)
|
||||
const tools = await createLocalTools(toolConfig.local);
|
||||
return tools;
|
||||
}
|
||||
|
||||
const toolFactory: Record<string, ToolCreator> = {
|
||||
"wikipedia.WikipediaToolSpec": async (config: unknown) => {
|
||||
return [new WikipediaTool(config as WikipediaToolParams)];
|
||||
},
|
||||
weather: async (config: unknown) => {
|
||||
return [new WeatherTool(config as WeatherToolParams)];
|
||||
},
|
||||
interpreter: async (config: unknown) => {
|
||||
return [new InterpreterTool(config as InterpreterToolParams)];
|
||||
},
|
||||
"openapi_action.OpenAPIActionToolSpec": async (config: unknown) => {
|
||||
const { openapi_uri, domain_headers } = config as {
|
||||
openapi_uri: string;
|
||||
domain_headers: Record<string, Record<string, string>>;
|
||||
};
|
||||
const openAPIActionTool = new OpenAPIActionTool(
|
||||
openapi_uri,
|
||||
domain_headers,
|
||||
);
|
||||
return await openAPIActionTool.toToolFunctions();
|
||||
},
|
||||
duckduckgo: async (config: unknown) => {
|
||||
return [new DuckDuckGoSearchTool(config as DuckDuckGoToolParams)];
|
||||
},
|
||||
img_gen: async (config: unknown) => {
|
||||
return [new ImgGeneratorTool(config as ImgGeneratorToolParams)];
|
||||
},
|
||||
artifact: async (config: unknown) => {
|
||||
return [new CodeGeneratorTool(config as CodeGeneratorToolParams)];
|
||||
},
|
||||
document_generator: async (config: unknown) => {
|
||||
return [new DocumentGenerator(config as DocumentGeneratorParams)];
|
||||
},
|
||||
form_filling: async (config: unknown) => {
|
||||
return [
|
||||
new ExtractMissingCellsTool(config as ExtractMissingCellsParams),
|
||||
new FillMissingCellsTool(config as FillMissingCellsParams),
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
async function createLocalTools(
|
||||
localConfig: Record<string, unknown>,
|
||||
): Promise<BaseToolWithCall[]> {
|
||||
const tools: BaseToolWithCall[] = [];
|
||||
|
||||
for (const [key, toolConfig] of Object.entries(localConfig)) {
|
||||
if (key in toolFactory) {
|
||||
const newTools = await toolFactory[key](toolConfig);
|
||||
tools.push(...newTools);
|
||||
}
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
export async function getConfiguredTools(
|
||||
configPath?: string,
|
||||
): Promise<BaseToolWithCall[]> {
|
||||
const configFile = path.join(configPath ?? "config", "tools.json");
|
||||
const toolConfig = JSON.parse(await fs.readFile(configFile, "utf8"));
|
||||
const tools = await createTools(toolConfig);
|
||||
return tools;
|
||||
}
|
||||
|
||||
export async function getTool(
|
||||
toolName: string,
|
||||
): Promise<BaseToolWithCall | undefined> {
|
||||
const tools = await getConfiguredTools();
|
||||
return tools.find((tool) => tool.metadata.name === toolName);
|
||||
}
|
||||
-248
@@ -1,248 +0,0 @@
|
||||
import { Logs, Result, Sandbox } from "@e2b/code-interpreter";
|
||||
import type { JSONSchemaType } from "ajv";
|
||||
import fs from "fs";
|
||||
import { BaseTool, ToolMetadata } from "llamaindex";
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
|
||||
export type InterpreterParameter = {
|
||||
code: string;
|
||||
sandboxFiles?: string[];
|
||||
retryCount?: number;
|
||||
};
|
||||
|
||||
export type InterpreterToolParams = {
|
||||
metadata?: ToolMetadata<JSONSchemaType<InterpreterParameter>>;
|
||||
apiKey?: string;
|
||||
fileServerURLPrefix?: string;
|
||||
};
|
||||
|
||||
export type InterpreterToolOutput = {
|
||||
isError: boolean;
|
||||
logs: Logs;
|
||||
text?: string;
|
||||
extraResult: InterpreterExtraResult[];
|
||||
retryCount?: number;
|
||||
};
|
||||
|
||||
type InterpreterExtraType =
|
||||
| "html"
|
||||
| "markdown"
|
||||
| "svg"
|
||||
| "png"
|
||||
| "jpeg"
|
||||
| "pdf"
|
||||
| "latex"
|
||||
| "json"
|
||||
| "javascript";
|
||||
|
||||
export type InterpreterExtraResult = {
|
||||
type: InterpreterExtraType;
|
||||
content?: string;
|
||||
filename?: string;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<InterpreterParameter>> = {
|
||||
name: "interpreter",
|
||||
description: `Execute python code in a Jupyter notebook cell and return any result, stdout, stderr, display_data, and error.
|
||||
If the code needs to use a file, ALWAYS pass the file path in the sandbox_files argument.
|
||||
You have a maximum of 3 retries to get the code to run successfully.
|
||||
`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
code: {
|
||||
type: "string",
|
||||
description: "The python code to execute in a single cell.",
|
||||
},
|
||||
sandboxFiles: {
|
||||
type: "array",
|
||||
description:
|
||||
"List of local file paths to be used by the code. The tool will throw an error if a file is not found.",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
nullable: true,
|
||||
},
|
||||
retryCount: {
|
||||
type: "number",
|
||||
description: "The number of times the tool has been retried",
|
||||
default: 0,
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
required: ["code"],
|
||||
},
|
||||
};
|
||||
|
||||
export class InterpreterTool implements BaseTool<InterpreterParameter> {
|
||||
private readonly outputDir = "output/tools";
|
||||
private readonly uploadedFilesDir = "output/uploaded";
|
||||
private apiKey?: string;
|
||||
private fileServerURLPrefix?: string;
|
||||
metadata: ToolMetadata<JSONSchemaType<InterpreterParameter>>;
|
||||
codeInterpreter?: Sandbox;
|
||||
|
||||
constructor(params?: InterpreterToolParams) {
|
||||
this.metadata = params?.metadata || DEFAULT_META_DATA;
|
||||
this.apiKey = params?.apiKey || process.env.E2B_API_KEY;
|
||||
this.fileServerURLPrefix =
|
||||
params?.fileServerURLPrefix || process.env.FILESERVER_URL_PREFIX;
|
||||
|
||||
if (!this.apiKey) {
|
||||
throw new Error(
|
||||
"E2B_API_KEY key is required to run code interpreter. Get it here: https://e2b.dev/docs/getting-started/api-key",
|
||||
);
|
||||
}
|
||||
if (!this.fileServerURLPrefix) {
|
||||
throw new Error(
|
||||
"FILESERVER_URL_PREFIX is required to display file output from sandbox",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async initInterpreter(input: InterpreterParameter) {
|
||||
if (!this.codeInterpreter) {
|
||||
this.codeInterpreter = await Sandbox.create({
|
||||
apiKey: this.apiKey,
|
||||
});
|
||||
// upload files to sandbox when it's initialized
|
||||
if (input.sandboxFiles) {
|
||||
console.log(`Uploading ${input.sandboxFiles.length} files to sandbox`);
|
||||
try {
|
||||
for (const filePath of input.sandboxFiles) {
|
||||
const fileName = path.basename(filePath);
|
||||
const localFilePath = path.join(this.uploadedFilesDir, fileName);
|
||||
const content = fs.readFileSync(localFilePath);
|
||||
|
||||
const arrayBuffer = new Uint8Array(content).buffer;
|
||||
await this.codeInterpreter?.files.write(filePath, arrayBuffer);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Got error when uploading files to sandbox", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.codeInterpreter;
|
||||
}
|
||||
|
||||
public async codeInterpret(
|
||||
input: InterpreterParameter,
|
||||
): Promise<InterpreterToolOutput> {
|
||||
console.log(
|
||||
`Sandbox files: ${input.sandboxFiles}. Retry count: ${input.retryCount}`,
|
||||
);
|
||||
|
||||
if (input.retryCount && input.retryCount >= 3) {
|
||||
return {
|
||||
isError: true,
|
||||
logs: {
|
||||
stdout: [],
|
||||
stderr: [],
|
||||
},
|
||||
text: "Max retries reached",
|
||||
extraResult: [],
|
||||
};
|
||||
}
|
||||
|
||||
console.log(
|
||||
`\n${"=".repeat(50)}\n> Running following AI-generated code:\n${input.code}\n${"=".repeat(50)}`,
|
||||
);
|
||||
const interpreter = await this.initInterpreter(input);
|
||||
const exec = await interpreter.runCode(input.code);
|
||||
if (exec.error) console.error("[Code Interpreter error]", exec.error);
|
||||
const extraResult = await this.getExtraResult(exec.results[0]);
|
||||
const result: InterpreterToolOutput = {
|
||||
isError: !!exec.error,
|
||||
logs: exec.logs,
|
||||
text: exec.text,
|
||||
extraResult,
|
||||
retryCount: input.retryCount ? input.retryCount + 1 : 1,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
async call(input: InterpreterParameter): Promise<InterpreterToolOutput> {
|
||||
const result = await this.codeInterpret(input);
|
||||
return result;
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.codeInterpreter?.kill();
|
||||
}
|
||||
|
||||
private async getExtraResult(
|
||||
res?: Result,
|
||||
): Promise<InterpreterExtraResult[]> {
|
||||
if (!res) return [];
|
||||
const output: InterpreterExtraResult[] = [];
|
||||
|
||||
try {
|
||||
const formats = res.formats(); // formats available for the result. Eg: ['png', ...]
|
||||
const results = formats.map((f) => res[f as keyof Result]); // get base64 data for each format
|
||||
|
||||
// save base64 data to file and return the url
|
||||
for (let i = 0; i < formats.length; i++) {
|
||||
const ext = formats[i];
|
||||
const data = results[i];
|
||||
switch (ext) {
|
||||
case "png":
|
||||
case "jpeg":
|
||||
case "svg":
|
||||
case "pdf":
|
||||
const { filename } = this.saveToDisk(data, ext);
|
||||
output.push({
|
||||
type: ext as InterpreterExtraType,
|
||||
filename,
|
||||
url: this.getFileUrl(filename),
|
||||
});
|
||||
break;
|
||||
default:
|
||||
output.push({
|
||||
type: ext as InterpreterExtraType,
|
||||
content: data,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error when parsing e2b response", error);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// Consider saving to cloud storage instead but it may cost more for you
|
||||
// See: https://e2b.dev/docs/sandbox/api/filesystem#write-to-file
|
||||
private saveToDisk(
|
||||
base64Data: string,
|
||||
ext: string,
|
||||
): {
|
||||
outputPath: string;
|
||||
filename: string;
|
||||
} {
|
||||
const filename = `${crypto.randomUUID()}.${ext}`; // generate a unique filename
|
||||
const buffer = Buffer.from(base64Data, "base64");
|
||||
const outputPath = this.getOutputPath(filename);
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
console.log(`Saved file to ${outputPath}`);
|
||||
return {
|
||||
outputPath,
|
||||
filename,
|
||||
};
|
||||
}
|
||||
|
||||
private getOutputPath(filename: string): string {
|
||||
// if outputDir doesn't exist, create it
|
||||
if (!fs.existsSync(this.outputDir)) {
|
||||
fs.mkdirSync(this.outputDir, { recursive: true });
|
||||
}
|
||||
return path.join(this.outputDir, filename);
|
||||
}
|
||||
|
||||
private getFileUrl(filename: string): string {
|
||||
return `${this.fileServerURLPrefix}/${this.outputDir}/${filename}`;
|
||||
}
|
||||
}
|
||||
-164
@@ -1,164 +0,0 @@
|
||||
import SwaggerParser from "@apidevtools/swagger-parser";
|
||||
import { JSONSchemaType } from "ajv";
|
||||
import got from "got";
|
||||
import { FunctionTool, JSONValue, ToolMetadata } from "llamaindex";
|
||||
|
||||
interface DomainHeaders {
|
||||
[key: string]: { [header: string]: string };
|
||||
}
|
||||
|
||||
type Input = {
|
||||
url: string;
|
||||
params: object;
|
||||
};
|
||||
|
||||
type APIInfo = {
|
||||
description: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export class OpenAPIActionTool {
|
||||
// cache the loaded specs by URL
|
||||
private static specs: Record<string, any> = {};
|
||||
|
||||
private readonly INVALID_URL_PROMPT =
|
||||
"This url did not include a hostname or scheme. Please determine the complete URL and try again.";
|
||||
|
||||
private createLoadSpecMetaData = (info: APIInfo) => {
|
||||
return {
|
||||
name: "load_openapi_spec",
|
||||
description: `Use this to retrieve the OpenAPI spec for the API named ${info.title} with the following description: ${info.description}. Call it before making any requests to the API.`,
|
||||
};
|
||||
};
|
||||
|
||||
private readonly createMethodCallMetaData = (
|
||||
method: "POST" | "PATCH" | "GET",
|
||||
info: APIInfo,
|
||||
) => {
|
||||
return {
|
||||
name: `${method.toLowerCase()}_request`,
|
||||
description: `Use this to call the ${method} method on the API named ${info.title}`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
url: {
|
||||
type: "string",
|
||||
description: `The url to make the ${method} request against`,
|
||||
},
|
||||
params: {
|
||||
type: "object",
|
||||
description:
|
||||
method === "GET"
|
||||
? "the URL parameters to provide with the get request"
|
||||
: `the key-value pairs to provide with the ${method} request`,
|
||||
},
|
||||
},
|
||||
required: ["url"],
|
||||
},
|
||||
} as ToolMetadata<JSONSchemaType<Input>>;
|
||||
};
|
||||
|
||||
constructor(
|
||||
public openapi_uri: string,
|
||||
public domainHeaders: DomainHeaders = {},
|
||||
) {}
|
||||
|
||||
async loadOpenapiSpec(url: string): Promise<any> {
|
||||
const api = await SwaggerParser.validate(url);
|
||||
return {
|
||||
servers: "servers" in api ? api.servers : "",
|
||||
info: { description: api.info.description, title: api.info.title },
|
||||
endpoints: api.paths,
|
||||
};
|
||||
}
|
||||
|
||||
async getRequest(input: Input): Promise<JSONValue> {
|
||||
if (!this.validUrl(input.url)) {
|
||||
return this.INVALID_URL_PROMPT;
|
||||
}
|
||||
try {
|
||||
const data = await got
|
||||
.get(input.url, {
|
||||
headers: this.getHeadersForUrl(input.url),
|
||||
searchParams: input.params as URLSearchParams,
|
||||
})
|
||||
.json();
|
||||
return data as JSONValue;
|
||||
} catch (error) {
|
||||
return error as JSONValue;
|
||||
}
|
||||
}
|
||||
|
||||
async postRequest(input: Input): Promise<JSONValue> {
|
||||
if (!this.validUrl(input.url)) {
|
||||
return this.INVALID_URL_PROMPT;
|
||||
}
|
||||
try {
|
||||
const res = await got.post(input.url, {
|
||||
headers: this.getHeadersForUrl(input.url),
|
||||
json: input.params,
|
||||
});
|
||||
return res.body as JSONValue;
|
||||
} catch (error) {
|
||||
return error as JSONValue;
|
||||
}
|
||||
}
|
||||
|
||||
async patchRequest(input: Input): Promise<JSONValue> {
|
||||
if (!this.validUrl(input.url)) {
|
||||
return this.INVALID_URL_PROMPT;
|
||||
}
|
||||
try {
|
||||
const res = await got.patch(input.url, {
|
||||
headers: this.getHeadersForUrl(input.url),
|
||||
json: input.params,
|
||||
});
|
||||
return res.body as JSONValue;
|
||||
} catch (error) {
|
||||
return error as JSONValue;
|
||||
}
|
||||
}
|
||||
|
||||
public async toToolFunctions() {
|
||||
if (!OpenAPIActionTool.specs[this.openapi_uri]) {
|
||||
console.log(`Loading spec for URL: ${this.openapi_uri}`);
|
||||
const spec = await this.loadOpenapiSpec(this.openapi_uri);
|
||||
OpenAPIActionTool.specs[this.openapi_uri] = spec;
|
||||
}
|
||||
const spec = OpenAPIActionTool.specs[this.openapi_uri];
|
||||
// TODO: read endpoints with parameters from spec and create one tool for each endpoint
|
||||
// For now, we just create a tool for each HTTP method which does not work well for passing parameters
|
||||
return [
|
||||
FunctionTool.from(() => {
|
||||
return spec;
|
||||
}, this.createLoadSpecMetaData(spec.info)),
|
||||
FunctionTool.from(
|
||||
this.getRequest.bind(this),
|
||||
this.createMethodCallMetaData("GET", spec.info),
|
||||
),
|
||||
FunctionTool.from(
|
||||
this.postRequest.bind(this),
|
||||
this.createMethodCallMetaData("POST", spec.info),
|
||||
),
|
||||
FunctionTool.from(
|
||||
this.patchRequest.bind(this),
|
||||
this.createMethodCallMetaData("PATCH", spec.info),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private validUrl(url: string): boolean {
|
||||
const parsed = new URL(url);
|
||||
return !!parsed.protocol && !!parsed.hostname;
|
||||
}
|
||||
|
||||
private getDomain(url: string): string {
|
||||
const parsed = new URL(url);
|
||||
return parsed.hostname;
|
||||
}
|
||||
|
||||
private getHeadersForUrl(url: string): { [header: string]: string } {
|
||||
const domain = this.getDomain(url);
|
||||
return this.domainHeaders[domain] || {};
|
||||
}
|
||||
}
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
import {
|
||||
BaseQueryEngine,
|
||||
CloudRetrieveParams,
|
||||
LlamaCloudIndex,
|
||||
MetadataFilters,
|
||||
QueryEngineTool,
|
||||
VectorStoreIndex,
|
||||
} from "llamaindex";
|
||||
import { generateFilters } from "../queryFilter";
|
||||
|
||||
interface QueryEngineParams {
|
||||
documentIds?: string[];
|
||||
topK?: number;
|
||||
}
|
||||
|
||||
export function createQueryEngineTool(
|
||||
index: VectorStoreIndex | LlamaCloudIndex,
|
||||
params?: QueryEngineParams,
|
||||
name?: string,
|
||||
description?: string,
|
||||
): QueryEngineTool {
|
||||
return new QueryEngineTool({
|
||||
queryEngine: createQueryEngine(index, params),
|
||||
metadata: {
|
||||
name: name || "query_engine",
|
||||
description:
|
||||
description ||
|
||||
`Use this tool to retrieve information about the text corpus from an index.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function createQueryEngine(
|
||||
index: VectorStoreIndex | LlamaCloudIndex,
|
||||
params?: QueryEngineParams,
|
||||
): BaseQueryEngine {
|
||||
const baseQueryParams = {
|
||||
similarityTopK:
|
||||
params?.topK ??
|
||||
(process.env.TOP_K ? parseInt(process.env.TOP_K) : undefined),
|
||||
};
|
||||
|
||||
if (index instanceof LlamaCloudIndex) {
|
||||
return index.asQueryEngine({
|
||||
...baseQueryParams,
|
||||
retrieval_mode: "auto_routed",
|
||||
preFilters: generateFilters(
|
||||
params?.documentIds || [],
|
||||
) as CloudRetrieveParams["filters"],
|
||||
});
|
||||
}
|
||||
|
||||
return index.asQueryEngine({
|
||||
...baseQueryParams,
|
||||
preFilters: generateFilters(params?.documentIds || []) as MetadataFilters,
|
||||
});
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import type { JSONSchemaType } from "ajv";
|
||||
import { BaseTool, ToolMetadata } from "llamaindex";
|
||||
|
||||
interface GeoLocation {
|
||||
id: string;
|
||||
name: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export type WeatherParameter = {
|
||||
location: string;
|
||||
};
|
||||
|
||||
export type WeatherToolParams = {
|
||||
metadata?: ToolMetadata<JSONSchemaType<WeatherParameter>>;
|
||||
};
|
||||
|
||||
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<WeatherParameter>> = {
|
||||
name: "get_weather_information",
|
||||
description: `
|
||||
Use this function to get the weather of any given location.
|
||||
Note that the weather code should follow WMO Weather interpretation codes (WW):
|
||||
0: Clear sky
|
||||
1, 2, 3: Mainly clear, partly cloudy, and overcast
|
||||
45, 48: Fog and depositing rime fog
|
||||
51, 53, 55: Drizzle: Light, moderate, and dense intensity
|
||||
56, 57: Freezing Drizzle: Light and dense intensity
|
||||
61, 63, 65: Rain: Slight, moderate and heavy intensity
|
||||
66, 67: Freezing Rain: Light and heavy intensity
|
||||
71, 73, 75: Snow fall: Slight, moderate, and heavy intensity
|
||||
77: Snow grains
|
||||
80, 81, 82: Rain showers: Slight, moderate, and violent
|
||||
85, 86: Snow showers slight and heavy
|
||||
95: Thunderstorm: Slight or moderate
|
||||
96, 99: Thunderstorm with slight and heavy hail
|
||||
`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: {
|
||||
type: "string",
|
||||
description: "The location to get the weather information",
|
||||
},
|
||||
},
|
||||
required: ["location"],
|
||||
},
|
||||
};
|
||||
|
||||
export class WeatherTool implements BaseTool<WeatherParameter> {
|
||||
metadata: ToolMetadata<JSONSchemaType<WeatherParameter>>;
|
||||
|
||||
private getGeoLocation = async (location: string): Promise<GeoLocation> => {
|
||||
const apiUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${location}&count=10&language=en&format=json`;
|
||||
const response = await fetch(apiUrl);
|
||||
const data = await response.json();
|
||||
const { id, name, latitude, longitude } = data.results[0];
|
||||
return { id, name, latitude, longitude };
|
||||
};
|
||||
|
||||
private getWeatherByLocation = async (location: string) => {
|
||||
console.log(
|
||||
"Calling open-meteo api to get weather information of location:",
|
||||
location,
|
||||
);
|
||||
const { latitude, longitude } = await this.getGeoLocation(location);
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const apiUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,weather_code&hourly=temperature_2m,weather_code&daily=weather_code&timezone=${timezone}`;
|
||||
const response = await fetch(apiUrl);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
constructor(params?: WeatherToolParams) {
|
||||
this.metadata = params?.metadata || DEFAULT_META_DATA;
|
||||
}
|
||||
|
||||
async call(input: WeatherParameter) {
|
||||
return await this.getWeatherByLocation(input.location);
|
||||
}
|
||||
}
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
import type { JSONSchemaType } from "ajv";
|
||||
import type { BaseTool, ToolMetadata } from "llamaindex";
|
||||
import { default as wiki } from "wikipedia";
|
||||
|
||||
type WikipediaParameter = {
|
||||
query: string;
|
||||
lang?: string;
|
||||
};
|
||||
|
||||
export type WikipediaToolParams = {
|
||||
metadata?: ToolMetadata<JSONSchemaType<WikipediaParameter>>;
|
||||
};
|
||||
|
||||
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<WikipediaParameter>> = {
|
||||
name: "wikipedia_tool",
|
||||
description: "A tool that uses a query engine to search Wikipedia.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "The query to search for",
|
||||
},
|
||||
lang: {
|
||||
type: "string",
|
||||
description: "The language to search in",
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
};
|
||||
|
||||
export class WikipediaTool implements BaseTool<WikipediaParameter> {
|
||||
private readonly DEFAULT_LANG = "en";
|
||||
metadata: ToolMetadata<JSONSchemaType<WikipediaParameter>>;
|
||||
|
||||
constructor(params?: WikipediaToolParams) {
|
||||
this.metadata = params?.metadata || DEFAULT_META_DATA;
|
||||
}
|
||||
|
||||
async loadData(
|
||||
page: string,
|
||||
lang: string = this.DEFAULT_LANG,
|
||||
): Promise<string> {
|
||||
wiki.setLang(lang);
|
||||
const pageResult = await wiki.page(page, { autoSuggest: false });
|
||||
const content = await pageResult.content();
|
||||
return content;
|
||||
}
|
||||
|
||||
async call({
|
||||
query,
|
||||
lang = this.DEFAULT_LANG,
|
||||
}: WikipediaParameter): Promise<string> {
|
||||
const searchResult = await wiki.search(query);
|
||||
if (searchResult.results.length === 0) return "No search results.";
|
||||
return await this.loadData(searchResult.results[0].title, lang);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { ContextChatEngine, Settings } from "llamaindex";
|
||||
import { getDataSource } from "./index";
|
||||
import { nodeCitationProcessor } from "./nodePostprocessors";
|
||||
import { generateFilters } from "./queryFilter";
|
||||
|
||||
export async function createChatEngine(documentIds?: string[], params?: any) {
|
||||
const index = await getDataSource(params);
|
||||
if (!index) {
|
||||
throw new Error(
|
||||
`StorageContext is empty - call 'npm run generate' to generate the storage first`,
|
||||
);
|
||||
}
|
||||
const retriever = index.asRetriever({
|
||||
similarityTopK: process.env.TOP_K ? parseInt(process.env.TOP_K) : undefined,
|
||||
filters: generateFilters(documentIds || []),
|
||||
});
|
||||
|
||||
const systemPrompt = process.env.SYSTEM_PROMPT;
|
||||
const citationPrompt = process.env.SYSTEM_CITATION_PROMPT;
|
||||
const prompt =
|
||||
[systemPrompt, citationPrompt].filter((p) => p).join("\n") || undefined;
|
||||
const nodePostprocessors = citationPrompt
|
||||
? [nodeCitationProcessor]
|
||||
: undefined;
|
||||
|
||||
return new ContextChatEngine({
|
||||
chatModel: Settings.llm,
|
||||
retriever,
|
||||
systemPrompt: prompt,
|
||||
nodePostprocessors,
|
||||
});
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
import {
|
||||
BaseNodePostprocessor,
|
||||
MessageContent,
|
||||
NodeWithScore,
|
||||
} from "llamaindex";
|
||||
|
||||
class NodeCitationProcessor implements BaseNodePostprocessor {
|
||||
/**
|
||||
* Append node_id into metadata for citation purpose.
|
||||
* Config SYSTEM_CITATION_PROMPT in your runtime environment variable to enable this feature.
|
||||
*/
|
||||
async postprocessNodes(
|
||||
nodes: NodeWithScore[],
|
||||
query?: MessageContent,
|
||||
): Promise<NodeWithScore[]> {
|
||||
for (const nodeScore of nodes) {
|
||||
if (!nodeScore.node || !nodeScore.node.metadata) {
|
||||
continue; // Skip nodes with missing properties
|
||||
}
|
||||
nodeScore.node.metadata["node_id"] = nodeScore.node.id_;
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
}
|
||||
|
||||
export const nodeCitationProcessor = new NodeCitationProcessor();
|
||||
@@ -1,105 +0,0 @@
|
||||
import { Document } from "llamaindex";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { getExtractors } from "../../engine/loader";
|
||||
import { DocumentFile } from "../streaming/annotations";
|
||||
|
||||
const MIME_TYPE_TO_EXT: Record<string, string> = {
|
||||
"application/pdf": "pdf",
|
||||
"text/plain": "txt",
|
||||
"text/csv": "csv",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
||||
"docx",
|
||||
};
|
||||
|
||||
export const UPLOADED_FOLDER = "output/uploaded";
|
||||
|
||||
export async function storeAndParseFile(
|
||||
name: string,
|
||||
fileBuffer: Buffer,
|
||||
mimeType: string,
|
||||
): Promise<DocumentFile> {
|
||||
const file = await storeFile(name, fileBuffer, mimeType);
|
||||
const documents: Document[] = await parseFile(fileBuffer, name, mimeType);
|
||||
// Update document IDs in the file metadata
|
||||
file.refs = documents.map((document) => document.id_ as string);
|
||||
return file;
|
||||
}
|
||||
|
||||
export async function storeFile(
|
||||
name: string,
|
||||
fileBuffer: Buffer,
|
||||
mimeType: string,
|
||||
) {
|
||||
const fileExt = MIME_TYPE_TO_EXT[mimeType];
|
||||
if (!fileExt) throw new Error(`Unsupported document type: ${mimeType}`);
|
||||
|
||||
const fileId = crypto.randomUUID();
|
||||
const newFilename = `${sanitizeFileName(name)}_${fileId}.${fileExt}`;
|
||||
const filepath = path.join(UPLOADED_FOLDER, newFilename);
|
||||
const fileUrl = await saveDocument(filepath, fileBuffer);
|
||||
return {
|
||||
id: fileId,
|
||||
name: newFilename,
|
||||
size: fileBuffer.length,
|
||||
type: fileExt,
|
||||
url: fileUrl,
|
||||
refs: [] as string[],
|
||||
} as DocumentFile;
|
||||
}
|
||||
|
||||
export async function parseFile(
|
||||
fileBuffer: Buffer,
|
||||
filename: string,
|
||||
mimeType: string,
|
||||
) {
|
||||
const documents = await loadDocuments(fileBuffer, mimeType);
|
||||
for (const document of documents) {
|
||||
document.metadata = {
|
||||
...document.metadata,
|
||||
file_name: filename,
|
||||
private: "true", // to separate private uploads from public documents
|
||||
};
|
||||
}
|
||||
return documents;
|
||||
}
|
||||
|
||||
async function loadDocuments(fileBuffer: Buffer, mimeType: string) {
|
||||
const extractors = getExtractors();
|
||||
const reader = extractors[MIME_TYPE_TO_EXT[mimeType]];
|
||||
|
||||
if (!reader) {
|
||||
throw new Error(`Unsupported document type: ${mimeType}`);
|
||||
}
|
||||
console.log(`Processing uploaded document of type: ${mimeType}`);
|
||||
return await reader.loadDataAsContent(fileBuffer);
|
||||
}
|
||||
|
||||
// Save document to file server and return the file url
|
||||
export async function saveDocument(filepath: string, content: string | Buffer) {
|
||||
if (path.isAbsolute(filepath)) {
|
||||
throw new Error("Absolute file paths are not allowed.");
|
||||
}
|
||||
if (!process.env.FILESERVER_URL_PREFIX) {
|
||||
throw new Error("FILESERVER_URL_PREFIX environment variable is not set.");
|
||||
}
|
||||
|
||||
const dirPath = path.dirname(filepath);
|
||||
await fs.promises.mkdir(dirPath, { recursive: true });
|
||||
|
||||
if (typeof content === "string") {
|
||||
await fs.promises.writeFile(filepath, content, "utf-8");
|
||||
} else {
|
||||
await fs.promises.writeFile(filepath, content);
|
||||
}
|
||||
|
||||
const fileurl = `${process.env.FILESERVER_URL_PREFIX}/${filepath}`;
|
||||
console.log(`Saved document to ${filepath}. Reachable at URL: ${fileurl}`);
|
||||
return fileurl;
|
||||
}
|
||||
|
||||
function sanitizeFileName(fileName: string) {
|
||||
// Remove file extension and sanitize
|
||||
return fileName.split(".")[0].replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
import {
|
||||
Document,
|
||||
IngestionPipeline,
|
||||
Settings,
|
||||
SimpleNodeParser,
|
||||
storageContextFromDefaults,
|
||||
VectorStoreIndex,
|
||||
} from "llamaindex";
|
||||
|
||||
export async function runPipeline(
|
||||
currentIndex: VectorStoreIndex | null,
|
||||
documents: Document[],
|
||||
) {
|
||||
// Use ingestion pipeline to process the documents into nodes and add them to the vector store
|
||||
const pipeline = new IngestionPipeline({
|
||||
transformations: [
|
||||
new SimpleNodeParser({
|
||||
chunkSize: Settings.chunkSize,
|
||||
chunkOverlap: Settings.chunkOverlap,
|
||||
}),
|
||||
Settings.embedModel,
|
||||
],
|
||||
});
|
||||
const nodes = await pipeline.run({ documents });
|
||||
if (currentIndex) {
|
||||
await currentIndex.insertNodes(nodes);
|
||||
currentIndex.storageContext.docStore.persist();
|
||||
console.log("Added nodes to the vector store.");
|
||||
return documents.map((document) => document.id_);
|
||||
} else {
|
||||
// Initialize a new index with the documents
|
||||
console.log(
|
||||
"Got empty index, created new index with the uploaded documents",
|
||||
);
|
||||
const persistDir = process.env.STORAGE_CACHE_DIR;
|
||||
if (!persistDir) {
|
||||
throw new Error("STORAGE_CACHE_DIR environment variable is required!");
|
||||
}
|
||||
const storageContext = await storageContextFromDefaults({
|
||||
persistDir,
|
||||
});
|
||||
const newIndex = await VectorStoreIndex.fromDocuments(documents, {
|
||||
storageContext,
|
||||
});
|
||||
await newIndex.storageContext.docStore.persist();
|
||||
return documents.map((document) => document.id_);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Document, LLamaCloudFileService, VectorStoreIndex } from "llamaindex";
|
||||
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
|
||||
import { DocumentFile } from "../streaming/annotations";
|
||||
import { parseFile, storeFile } from "./helper";
|
||||
import { runPipeline } from "./pipeline";
|
||||
|
||||
export async function uploadDocument(
|
||||
index: VectorStoreIndex | LlamaCloudIndex | null,
|
||||
name: string,
|
||||
raw: string,
|
||||
): Promise<DocumentFile> {
|
||||
const [header, content] = raw.split(",");
|
||||
const mimeType = header.replace("data:", "").replace(";base64", "");
|
||||
const fileBuffer = Buffer.from(content, "base64");
|
||||
|
||||
// Store file
|
||||
const fileMetadata = await storeFile(name, fileBuffer, mimeType);
|
||||
|
||||
// Do not index csv files
|
||||
if (mimeType === "text/csv") {
|
||||
return fileMetadata;
|
||||
}
|
||||
let documentIds: string[] = [];
|
||||
if (index instanceof LlamaCloudIndex) {
|
||||
// trigger LlamaCloudIndex API to upload the file and run the pipeline
|
||||
const projectId = await index.getProjectId();
|
||||
const pipelineId = await index.getPipelineId();
|
||||
try {
|
||||
documentIds = [
|
||||
await LLamaCloudFileService.addFileToPipeline(
|
||||
projectId,
|
||||
pipelineId,
|
||||
new File([fileBuffer], name, { type: mimeType }),
|
||||
{ private: "true" },
|
||||
),
|
||||
];
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof ReferenceError &&
|
||||
error.message.includes("File is not defined")
|
||||
) {
|
||||
throw new Error(
|
||||
"File class is not supported in the current Node.js version. Please use Node.js 20 or higher.",
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
// run the pipeline for other vector store indexes
|
||||
const documents: Document[] = await parseFile(
|
||||
fileBuffer,
|
||||
fileMetadata.name,
|
||||
mimeType,
|
||||
);
|
||||
documentIds = await runPipeline(index, documents);
|
||||
}
|
||||
|
||||
// Update file metadata with document IDs
|
||||
fileMetadata.refs = documentIds;
|
||||
return fileMetadata;
|
||||
}
|
||||
-251
@@ -1,251 +0,0 @@
|
||||
import { JSONValue, Message } from "ai";
|
||||
import {
|
||||
ChatMessage,
|
||||
MessageContent,
|
||||
MessageContentDetail,
|
||||
MessageType,
|
||||
} from "llamaindex";
|
||||
import { UPLOADED_FOLDER } from "../documents/helper";
|
||||
|
||||
export type DocumentFileType = "csv" | "pdf" | "txt" | "docx";
|
||||
|
||||
export type DocumentFile = {
|
||||
id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
url: string;
|
||||
refs?: string[];
|
||||
};
|
||||
|
||||
type Annotation = {
|
||||
type: string;
|
||||
data: object;
|
||||
};
|
||||
|
||||
export function isValidMessages(messages: Message[]): boolean {
|
||||
const lastMessage =
|
||||
messages && messages.length > 0 ? messages[messages.length - 1] : null;
|
||||
return lastMessage !== null && lastMessage.role === "user";
|
||||
}
|
||||
|
||||
export function retrieveDocumentIds(messages: Message[]): string[] {
|
||||
// retrieve document Ids from the annotations of all messages (if any)
|
||||
const documentFiles = retrieveDocumentFiles(messages);
|
||||
return documentFiles.map((file) => file.refs || []).flat();
|
||||
}
|
||||
|
||||
export function retrieveDocumentFiles(messages: Message[]): DocumentFile[] {
|
||||
const annotations = getAllAnnotations(messages);
|
||||
if (annotations.length === 0) return [];
|
||||
|
||||
const files: DocumentFile[] = [];
|
||||
for (const { type, data } of annotations) {
|
||||
if (
|
||||
type === "document_file" &&
|
||||
"files" in data &&
|
||||
Array.isArray(data.files)
|
||||
) {
|
||||
files.push(...data.files);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
export function retrieveMessageContent(messages: Message[]): MessageContent {
|
||||
const userMessage = messages[messages.length - 1];
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: userMessage.content,
|
||||
},
|
||||
...retrieveLatestArtifact(messages),
|
||||
...convertAnnotations(messages),
|
||||
];
|
||||
}
|
||||
|
||||
export function convertToChatHistory(messages: Message[]): ChatMessage[] {
|
||||
if (!messages || !Array.isArray(messages)) {
|
||||
return [];
|
||||
}
|
||||
const agentHistory = retrieveAgentHistoryMessage(messages);
|
||||
if (agentHistory) {
|
||||
const previousMessages = messages.slice(0, -1);
|
||||
return [...previousMessages, agentHistory].map((msg) => ({
|
||||
role: msg.role as MessageType,
|
||||
content: msg.content,
|
||||
}));
|
||||
}
|
||||
return messages.map((msg) => ({
|
||||
role: msg.role as MessageType,
|
||||
content: msg.content,
|
||||
}));
|
||||
}
|
||||
|
||||
function retrieveAgentHistoryMessage(
|
||||
messages: Message[],
|
||||
maxAgentMessages = 10,
|
||||
): ChatMessage | null {
|
||||
const agentAnnotations = getAnnotations<{ agent: string; text: string }>(
|
||||
messages,
|
||||
{ role: "assistant", type: "agent" },
|
||||
).slice(-maxAgentMessages);
|
||||
|
||||
if (agentAnnotations.length > 0) {
|
||||
const messageContent =
|
||||
"Here is the previous conversation of agents:\n" +
|
||||
agentAnnotations.map((annotation) => annotation.data.text).join("\n");
|
||||
return {
|
||||
role: "assistant",
|
||||
content: messageContent,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getFileContent(file: DocumentFile): string {
|
||||
let defaultContent = `=====File: ${file.name}=====\n`;
|
||||
// Include file URL if it's available
|
||||
const urlPrefix = process.env.FILESERVER_URL_PREFIX;
|
||||
let urlContent = "";
|
||||
if (urlPrefix) {
|
||||
if (file.url) {
|
||||
urlContent = `File URL: ${file.url}\n`;
|
||||
} else {
|
||||
urlContent = `File URL (instruction: do not update this file URL yourself): ${urlPrefix}/output/uploaded/${file.name}\n`;
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"Warning: FILESERVER_URL_PREFIX not set in environment variables. Can't use file server",
|
||||
);
|
||||
}
|
||||
defaultContent += urlContent;
|
||||
|
||||
// Include document IDs if it's available
|
||||
if (file.refs) {
|
||||
defaultContent += `Document IDs: ${file.refs}\n`;
|
||||
}
|
||||
// Include sandbox file paths
|
||||
const sandboxFilePath = `/tmp/${file.name}`;
|
||||
defaultContent += `Sandbox file path (instruction: only use sandbox path for artifact or code interpreter tool): ${sandboxFilePath}\n`;
|
||||
|
||||
// Include local file path
|
||||
const localFilePath = `${UPLOADED_FOLDER}/${file.name}`;
|
||||
defaultContent += `Local file path (instruction: use for local tool that requires a local path): ${localFilePath}\n`;
|
||||
|
||||
return defaultContent;
|
||||
}
|
||||
|
||||
function getAllAnnotations(messages: Message[]): Annotation[] {
|
||||
return messages.flatMap((message) =>
|
||||
(message.annotations ?? []).map((annotation) =>
|
||||
getValidAnnotation(annotation),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// get latest artifact from annotations to append to the user message
|
||||
function retrieveLatestArtifact(messages: Message[]): MessageContentDetail[] {
|
||||
const annotations = getAllAnnotations(messages);
|
||||
if (annotations.length === 0) return [];
|
||||
|
||||
for (const { type, data } of annotations.reverse()) {
|
||||
if (
|
||||
type === "tools" &&
|
||||
"toolCall" in data &&
|
||||
"toolOutput" in data &&
|
||||
typeof data.toolCall === "object" &&
|
||||
typeof data.toolOutput === "object" &&
|
||||
data.toolCall !== null &&
|
||||
data.toolOutput !== null &&
|
||||
"name" in data.toolCall &&
|
||||
data.toolCall.name === "artifact"
|
||||
) {
|
||||
const toolOutput = data.toolOutput as { output?: { code?: string } };
|
||||
if (toolOutput.output?.code) {
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: `The existing code is:\n\`\`\`\n${toolOutput.output.code}\n\`\`\``,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function convertAnnotations(messages: Message[]): MessageContentDetail[] {
|
||||
// get all annotations from user messages
|
||||
const annotations: Annotation[] = messages
|
||||
.filter((message) => message.role === "user" && message.annotations)
|
||||
.flatMap((message) => message.annotations?.map(getValidAnnotation) || []);
|
||||
if (annotations.length === 0) return [];
|
||||
|
||||
const content: MessageContentDetail[] = [];
|
||||
annotations.forEach(({ type, data }) => {
|
||||
// convert image
|
||||
if (type === "image" && "url" in data && typeof data.url === "string") {
|
||||
content.push({
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: data.url,
|
||||
},
|
||||
});
|
||||
}
|
||||
// convert the content of files to a text message
|
||||
if (
|
||||
type === "document_file" &&
|
||||
"files" in data &&
|
||||
Array.isArray(data.files)
|
||||
) {
|
||||
const fileContent = data.files.map(getFileContent).join("\n");
|
||||
content.push({
|
||||
type: "text",
|
||||
text: fileContent,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
function getValidAnnotation(annotation: JSONValue): Annotation {
|
||||
if (
|
||||
!(
|
||||
annotation &&
|
||||
typeof annotation === "object" &&
|
||||
"type" in annotation &&
|
||||
typeof annotation.type === "string" &&
|
||||
"data" in annotation &&
|
||||
annotation.data &&
|
||||
typeof annotation.data === "object"
|
||||
)
|
||||
) {
|
||||
throw new Error("Client sent invalid annotation. Missing data and type");
|
||||
}
|
||||
return { type: annotation.type, data: annotation.data };
|
||||
}
|
||||
|
||||
// validate and get all annotations of a specific type or role from the frontend messages
|
||||
export function getAnnotations<
|
||||
T extends Annotation["data"] = Annotation["data"],
|
||||
>(
|
||||
messages: Message[],
|
||||
options?: {
|
||||
role?: Message["role"]; // message role
|
||||
type?: Annotation["type"]; // annotation type
|
||||
},
|
||||
): {
|
||||
type: string;
|
||||
data: T;
|
||||
}[] {
|
||||
const messagesByRole = options?.role
|
||||
? messages.filter((msg) => msg.role === options?.role)
|
||||
: messages;
|
||||
const annotations = getAllAnnotations(messagesByRole);
|
||||
const annotationsByType = options?.type
|
||||
? annotations.filter((a) => a.type === options.type)
|
||||
: annotations;
|
||||
return annotationsByType as { type: string; data: T }[];
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
import { StreamData } from "ai";
|
||||
import {
|
||||
CallbackManager,
|
||||
LLamaCloudFileService,
|
||||
Metadata,
|
||||
MetadataMode,
|
||||
NodeWithScore,
|
||||
ToolCall,
|
||||
ToolOutput,
|
||||
} from "llamaindex";
|
||||
import path from "node:path";
|
||||
import { DATA_DIR } from "../../engine/loader";
|
||||
import { downloadFile } from "./file";
|
||||
|
||||
const LLAMA_CLOUD_DOWNLOAD_FOLDER = "output/llamacloud";
|
||||
|
||||
export function appendSourceData(
|
||||
data: StreamData,
|
||||
sourceNodes?: NodeWithScore<Metadata>[],
|
||||
) {
|
||||
if (!sourceNodes?.length) return;
|
||||
try {
|
||||
const nodes = sourceNodes.map((node) => ({
|
||||
metadata: node.node.metadata,
|
||||
id: node.node.id_,
|
||||
score: node.score ?? null,
|
||||
url: getNodeUrl(node.node.metadata),
|
||||
text: node.node.getContent(MetadataMode.NONE),
|
||||
}));
|
||||
data.appendMessageAnnotation({
|
||||
type: "sources",
|
||||
data: {
|
||||
nodes,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error appending source data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export function appendEventData(data: StreamData, title?: string) {
|
||||
if (!title) return;
|
||||
data.appendMessageAnnotation({
|
||||
type: "events",
|
||||
data: {
|
||||
title,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function appendToolData(
|
||||
data: StreamData,
|
||||
toolCall: ToolCall,
|
||||
toolOutput: ToolOutput,
|
||||
) {
|
||||
data.appendMessageAnnotation({
|
||||
type: "tools",
|
||||
data: {
|
||||
toolCall: {
|
||||
id: toolCall.id,
|
||||
name: toolCall.name,
|
||||
input: toolCall.input,
|
||||
},
|
||||
toolOutput: {
|
||||
output: toolOutput.output,
|
||||
isError: toolOutput.isError,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function createCallbackManager(stream: StreamData) {
|
||||
const callbackManager = new CallbackManager();
|
||||
|
||||
callbackManager.on("retrieve-end", (data) => {
|
||||
const { nodes, query } = data.detail;
|
||||
appendSourceData(stream, nodes);
|
||||
appendEventData(stream, `Retrieving context for query: '${query.query}'`);
|
||||
appendEventData(
|
||||
stream,
|
||||
`Retrieved ${nodes.length} sources to use as context for the query`,
|
||||
);
|
||||
downloadFilesFromNodes(nodes); // don't await to avoid blocking chat streaming
|
||||
});
|
||||
|
||||
callbackManager.on("llm-tool-call", (event) => {
|
||||
const { name, input } = event.detail.toolCall;
|
||||
const inputString = Object.entries(input)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join(", ");
|
||||
appendEventData(
|
||||
stream,
|
||||
`Using tool: '${name}' with inputs: '${inputString}'`,
|
||||
);
|
||||
});
|
||||
|
||||
callbackManager.on("llm-tool-result", (event) => {
|
||||
const { toolCall, toolResult } = event.detail;
|
||||
appendToolData(stream, toolCall, toolResult);
|
||||
});
|
||||
|
||||
return callbackManager;
|
||||
}
|
||||
|
||||
function getNodeUrl(metadata: Metadata) {
|
||||
if (!process.env.FILESERVER_URL_PREFIX) {
|
||||
console.warn(
|
||||
"FILESERVER_URL_PREFIX is not set. File URLs will not be generated.",
|
||||
);
|
||||
}
|
||||
const fileName = metadata["file_name"];
|
||||
if (fileName && process.env.FILESERVER_URL_PREFIX) {
|
||||
// file_name exists and file server is configured
|
||||
const pipelineId = metadata["pipeline_id"];
|
||||
if (pipelineId) {
|
||||
const name = toDownloadedName(pipelineId, fileName);
|
||||
return `${process.env.FILESERVER_URL_PREFIX}/${LLAMA_CLOUD_DOWNLOAD_FOLDER}/${name}`;
|
||||
}
|
||||
const isPrivate = metadata["private"] === "true";
|
||||
if (isPrivate) {
|
||||
return `${process.env.FILESERVER_URL_PREFIX}/output/uploaded/${fileName}`;
|
||||
}
|
||||
const filePath = metadata["file_path"];
|
||||
const dataDir = path.resolve(DATA_DIR);
|
||||
|
||||
if (filePath && dataDir) {
|
||||
const relativePath = path.relative(dataDir, filePath);
|
||||
return `${process.env.FILESERVER_URL_PREFIX}/data/${relativePath}`;
|
||||
}
|
||||
}
|
||||
// fallback to URL in metadata (e.g. for websites)
|
||||
return metadata["URL"];
|
||||
}
|
||||
|
||||
async function downloadFilesFromNodes(nodes: NodeWithScore<Metadata>[]) {
|
||||
try {
|
||||
const files = nodesToLlamaCloudFiles(nodes);
|
||||
for (const { pipelineId, fileName, downloadedName } of files) {
|
||||
const downloadUrl = await LLamaCloudFileService.getFileUrl(
|
||||
pipelineId,
|
||||
fileName,
|
||||
);
|
||||
if (downloadUrl) {
|
||||
await downloadFile(
|
||||
downloadUrl,
|
||||
downloadedName,
|
||||
LLAMA_CLOUD_DOWNLOAD_FOLDER,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error downloading files from nodes:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function nodesToLlamaCloudFiles(nodes: NodeWithScore<Metadata>[]) {
|
||||
const files: Array<{
|
||||
pipelineId: string;
|
||||
fileName: string;
|
||||
downloadedName: string;
|
||||
}> = [];
|
||||
for (const node of nodes) {
|
||||
const pipelineId = node.node.metadata["pipeline_id"];
|
||||
const fileName = node.node.metadata["file_name"];
|
||||
if (!pipelineId || !fileName) continue;
|
||||
const isDuplicate = files.some(
|
||||
(f) => f.pipelineId === pipelineId && f.fileName === fileName,
|
||||
);
|
||||
if (!isDuplicate) {
|
||||
files.push({
|
||||
pipelineId,
|
||||
fileName,
|
||||
downloadedName: toDownloadedName(pipelineId, fileName),
|
||||
});
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function toDownloadedName(pipelineId: string, fileName: string) {
|
||||
return `${pipelineId}$${fileName}`;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import https from "node:https";
|
||||
import path from "node:path";
|
||||
|
||||
export async function downloadFile(
|
||||
urlToDownload: string,
|
||||
filename: string,
|
||||
folder = "output/uploaded",
|
||||
) {
|
||||
try {
|
||||
const downloadedPath = path.join(folder, filename);
|
||||
|
||||
// Check if file already exists
|
||||
if (fs.existsSync(downloadedPath)) return;
|
||||
|
||||
const file = fs.createWriteStream(downloadedPath);
|
||||
https
|
||||
.get(urlToDownload, (response) => {
|
||||
response.pipe(file);
|
||||
file.on("finish", () => {
|
||||
file.close(() => {
|
||||
console.log("File downloaded successfully");
|
||||
});
|
||||
});
|
||||
})
|
||||
.on("error", (err) => {
|
||||
fs.unlink(downloadedPath, () => {
|
||||
console.error("Error downloading file:", err);
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Error downloading file: ${error}`);
|
||||
}
|
||||
}
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
import { ChatMessage, Settings } from "llamaindex";
|
||||
|
||||
export async function generateNextQuestions(conversation: ChatMessage[]) {
|
||||
const llm = Settings.llm;
|
||||
const NEXT_QUESTION_PROMPT = process.env.NEXT_QUESTION_PROMPT;
|
||||
if (!NEXT_QUESTION_PROMPT) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Format conversation
|
||||
const conversationText = conversation
|
||||
.map((message) => `${message.role}: ${message.content}`)
|
||||
.join("\n");
|
||||
const message = NEXT_QUESTION_PROMPT.replace(
|
||||
"{conversation}",
|
||||
conversationText,
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await llm.complete({ prompt: message });
|
||||
const questions = extractQuestions(response.text);
|
||||
return questions;
|
||||
} catch (error) {
|
||||
console.error("Error when generating the next questions: ", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: instead of parsing the LLM's result we can use structured predict, once LITS supports it
|
||||
function extractQuestions(text: string): string[] {
|
||||
// Extract the text inside the triple backticks
|
||||
// @ts-ignore
|
||||
const contentMatch = text.match(/```(.*?)```/s);
|
||||
const content = contentMatch ? contentMatch[1] : "";
|
||||
|
||||
// Split the content by newlines to get each question
|
||||
const questions = content
|
||||
.split("\n")
|
||||
.map((question) => question.trim())
|
||||
.filter((question) => question !== "");
|
||||
|
||||
return questions;
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class EventCallback(ABC):
|
||||
"""
|
||||
Base class for event callbacks during event streaming.
|
||||
"""
|
||||
|
||||
async def run(self, event: Any) -> Any:
|
||||
"""
|
||||
Called for each event in the stream.
|
||||
Default behavior: pass through the event unchanged.
|
||||
"""
|
||||
return event
|
||||
|
||||
async def on_complete(self, final_response: str) -> Any:
|
||||
"""
|
||||
Called when the stream is complete.
|
||||
Default behavior: return None.
|
||||
"""
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
def from_default(self, *args, **kwargs) -> "EventCallback":
|
||||
"""
|
||||
Create a new instance of the processor from default values.
|
||||
"""
|
||||
pass
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
import logging
|
||||
from typing import Any, List
|
||||
|
||||
from fastapi import BackgroundTasks
|
||||
from llama_index.core.schema import NodeWithScore
|
||||
|
||||
from app.api.callbacks.base import EventCallback
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class LlamaCloudFileDownload(EventCallback):
|
||||
"""
|
||||
Processor for handling LlamaCloud file downloads from source nodes.
|
||||
Only work if LlamaCloud service code is available.
|
||||
"""
|
||||
|
||||
def __init__(self, background_tasks: BackgroundTasks):
|
||||
self.background_tasks = background_tasks
|
||||
|
||||
async def run(self, event: Any) -> Any:
|
||||
if hasattr(event, "to_response"):
|
||||
event_response = event.to_response()
|
||||
if event_response.get("type") == "sources" and hasattr(event, "nodes"):
|
||||
await self._process_response_nodes(event.nodes)
|
||||
return event
|
||||
|
||||
async def _process_response_nodes(self, source_nodes: List[NodeWithScore]):
|
||||
try:
|
||||
from app.engine.service import LLamaCloudFileService # type: ignore
|
||||
|
||||
LLamaCloudFileService.download_files_from_nodes(
|
||||
source_nodes, self.background_tasks
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def from_default(
|
||||
cls, background_tasks: BackgroundTasks
|
||||
) -> "LlamaCloudFileDownload":
|
||||
return cls(background_tasks=background_tasks)
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from app.api.callbacks.base import EventCallback
|
||||
from app.api.routers.models import ChatData
|
||||
from app.api.services.suggestion import NextQuestionSuggestion
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class SuggestNextQuestions(EventCallback):
|
||||
"""Processor for generating next question suggestions."""
|
||||
|
||||
def __init__(self, chat_data: ChatData):
|
||||
self.chat_data = chat_data
|
||||
self.accumulated_text = ""
|
||||
|
||||
async def on_complete(self, final_response: str) -> Any:
|
||||
if final_response == "":
|
||||
return None
|
||||
|
||||
questions = await NextQuestionSuggestion.suggest_next_questions(
|
||||
self.chat_data.messages, final_response
|
||||
)
|
||||
if questions:
|
||||
return {
|
||||
"type": "suggested_questions",
|
||||
"data": questions,
|
||||
}
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_default(cls, chat_data: ChatData) -> "SuggestNextQuestions":
|
||||
return cls(chat_data=chat_data)
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from llama_index.core.workflow.handler import WorkflowHandler
|
||||
|
||||
from app.api.callbacks.base import EventCallback
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class StreamHandler:
|
||||
"""
|
||||
Streams events from a workflow handler through a chain of callbacks.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workflow_handler: WorkflowHandler,
|
||||
callbacks: Optional[List[EventCallback]] = None,
|
||||
):
|
||||
self.workflow_handler = workflow_handler
|
||||
self.callbacks = callbacks or []
|
||||
self.accumulated_text = ""
|
||||
|
||||
def vercel_stream(self):
|
||||
"""Create a streaming response with Vercel format."""
|
||||
from app.api.routers.vercel_response import VercelStreamResponse
|
||||
|
||||
return VercelStreamResponse(stream_handler=self)
|
||||
|
||||
async def cancel_run(self):
|
||||
"""Cancel the workflow handler."""
|
||||
await self.workflow_handler.cancel_run()
|
||||
|
||||
async def stream_events(self):
|
||||
"""Stream events through the processor chain."""
|
||||
try:
|
||||
async for event in self.workflow_handler.stream_events():
|
||||
# Process the event through each processor
|
||||
for callback in self.callbacks:
|
||||
event = await callback.run(event)
|
||||
yield event
|
||||
|
||||
# After all events are processed, call on_complete for each callback
|
||||
for callback in self.callbacks:
|
||||
result = await callback.on_complete(self.accumulated_text)
|
||||
if result:
|
||||
yield result
|
||||
|
||||
except Exception as e:
|
||||
# Make sure to cancel the workflow on error
|
||||
await self.workflow_handler.cancel_run()
|
||||
raise e
|
||||
|
||||
async def accumulate_text(self, text: str):
|
||||
"""Accumulate text from the workflow handler."""
|
||||
self.accumulated_text += text
|
||||
|
||||
@classmethod
|
||||
def from_default(
|
||||
cls,
|
||||
handler: WorkflowHandler,
|
||||
callbacks: Optional[List[EventCallback]] = None,
|
||||
) -> "StreamHandler":
|
||||
"""Create a new instance with the given workflow handler and callbacks."""
|
||||
return cls(workflow_handler=handler, callbacks=callbacks)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user