mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-16 11:04:26 -04:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b72d63090 | |||
| 6335de1174 | |||
| b9184ff59a | |||
| cd3fcd0512 | |||
| a47d778602 | |||
| 7f4ac228ee | |||
| 5263bde8e7 | |||
| 4dee65b93d | |||
| c60182a925 | |||
| 0e78ba4603 | |||
| 7652b2b388 | |||
| d18f0399e5 | |||
| 3790ca0250 | |||
| 16e6124db2 | |||
| 51dc0e4334 | |||
| 5a7216e36d | |||
| 27a1b9fdf2 | |||
| 04ddebcd64 | |||
| 3e8057a83a | |||
| 12ed570a53 | |||
| bde3daae08 | |||
| 727eb105ea | |||
| ef070c0b4b | |||
| 70f7dcacc8 | |||
| cf65162bef | |||
| 7c2a3f69a7 | |||
| c7b7672062 | |||
| cb8d535d9b | |||
| 811cb13cba | |||
| 0213fe07dd | |||
| b31fa80326 | |||
| 40c5c8412c | |||
| 0031e674c9 | |||
| 6e9184dd37 | |||
| fa28cb5d0d | |||
| 8c1087f5f1 | |||
| 27333973f1 | |||
| cf3ec97a4c | |||
| 505b8e944a | |||
| 578f7f9e50 | |||
| adc40cf770 | |||
| 7bce7386d5 | |||
| c011455dc4 | |||
| 38a8be8d12 | |||
| 6e70eb4d11 | |||
| 917e862202 | |||
| e363bfeecc | |||
| b6da3c2419 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
bump: use LlamaIndexTS 0.6.18
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Fix using LlamaCloud selector does not use the configured values in the environment (Python)
|
||||
@@ -0,0 +1,6 @@
|
||||
# coderabbit.yml
|
||||
reviews:
|
||||
path_instructions:
|
||||
- path: "templates/**"
|
||||
instructions: |
|
||||
For files under the `templates` folder, do not report 'Missing Dependencies Detected' errors.
|
||||
@@ -9,8 +9,75 @@ env:
|
||||
POETRY_VERSION: "1.6.1"
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
name: create-llama
|
||||
e2e-python:
|
||||
name: python
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
node-version: [20]
|
||||
python-version: ["3.11"]
|
||||
os: [macos-latest, windows-latest, ubuntu-22.04]
|
||||
frameworks: ["fastapi"]
|
||||
datasources: ["--no-files", "--example-file", "--llamacloud"]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
version: ${{ env.POETRY_VERSION }}
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install --with-deps
|
||||
working-directory: .
|
||||
|
||||
- name: Build create-llama
|
||||
run: pnpm run build
|
||||
working-directory: .
|
||||
|
||||
- name: Install
|
||||
run: pnpm run pack-install
|
||||
working-directory: .
|
||||
|
||||
- name: Run Playwright tests for Python
|
||||
run: pnpm run e2e:python
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
|
||||
FRAMEWORK: ${{ matrix.frameworks }}
|
||||
DATASOURCE: ${{ matrix.datasources }}
|
||||
working-directory: .
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-python
|
||||
path: ./playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
e2e-typescript:
|
||||
name: typescript
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: true
|
||||
@@ -18,7 +85,7 @@ jobs:
|
||||
node-version: [18, 20]
|
||||
python-version: ["3.11"]
|
||||
os: [macos-latest, windows-latest, ubuntu-22.04]
|
||||
frameworks: ["nextjs", "express", "fastapi"]
|
||||
frameworks: ["nextjs", "express"]
|
||||
datasources: ["--no-files", "--example-file"]
|
||||
defaults:
|
||||
run:
|
||||
@@ -60,8 +127,8 @@ jobs:
|
||||
run: pnpm run pack-install
|
||||
working-directory: .
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: pnpm run e2e
|
||||
- name: Run Playwright tests for TypeScript
|
||||
run: pnpm run e2e:typescript
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
|
||||
@@ -72,6 +139,6 @@ jobs:
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
name: playwright-report-typescript
|
||||
path: ./playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
@@ -17,6 +17,9 @@ jobs:
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pnpm format
|
||||
pnpm lint
|
||||
uvx ruff format --check templates/
|
||||
|
||||
@@ -1,5 +1,98 @@
|
||||
# create-llama
|
||||
|
||||
## 0.2.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0e78ba4: Fix: programmatically ensure index for LlamaCloud
|
||||
- 0e78ba4: Fix .env not loaded on poetry run generate
|
||||
- 7f4ac22: Don't need to run generate script for LlamaCloud
|
||||
- 5263bde: Use selected LlamaCloud index in multi-agent template
|
||||
|
||||
## 0.2.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 16e6124: Bump package for llamatrace observability
|
||||
- 3790ca0: Add multi-agent task selector for TS template
|
||||
- d18f039: Add e2b code artifact tool for the FastAPI template
|
||||
|
||||
## 0.2.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5a7216e: feat: implement artifact tool in TS
|
||||
|
||||
## 0.2.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 04ddebc: Add publisher agent to multi-agents for generating documents (PDF and HTML)
|
||||
- 04ddebc: Allow tool selection for multi-agents (Python and TS)
|
||||
|
||||
## 0.2.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 70f7dca: feat: add test deps for llamaparse
|
||||
- ef070c0: Add multi agents template for Typescript
|
||||
|
||||
## 0.2.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7c2a3f6: fix: postgres import
|
||||
|
||||
## 0.2.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- cb8d535: Fix only produces one agent event
|
||||
|
||||
## 0.2.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0213fe0: Update dependencies for vector stores and add e2e test to ensure that they work as expected.
|
||||
|
||||
## 0.2.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0031e67: Bump llama-index to 0.11.11 for the multi-agent template
|
||||
|
||||
## 0.2.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 505b8e9: bump: use latest ai package version
|
||||
- cf3ec97: Dynamically select model for Groq
|
||||
- 8c1087f: feat: enhance style for markdown
|
||||
|
||||
## 0.2.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- adc40cf: fix: vercel ai update crash sending annotations
|
||||
|
||||
## 0.2.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 38a8be8: fix: filter in mongo vector store
|
||||
|
||||
## 0.2.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 917e862: Fix errors in building the frontend
|
||||
|
||||
## 0.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b6da3c2: Ensure the generation script always works
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import {
|
||||
client,
|
||||
PipelinesService,
|
||||
ProjectsService,
|
||||
} from "@llamaindex/cloud/api";
|
||||
import { DEFAULT_BASE_URL } from "@llamaindex/core/global";
|
||||
|
||||
function initService(apiKey?: string) {
|
||||
client.setConfig({
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
throwOnError: true,
|
||||
});
|
||||
const token = apiKey ?? process.env.LLAMA_CLOUD_API_KEY;
|
||||
client.interceptors.request.use((request: any) => {
|
||||
request.headers.set("Authorization", `Bearer ${token}`);
|
||||
return request;
|
||||
});
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
"API Key is required for LlamaCloudIndex. Please set the LLAMA_CLOUD_API_KEY environment variable",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function getProjectId(projectName: string): Promise<string> {
|
||||
const { data: projects } = await ProjectsService.listProjectsApiV1ProjectsGet(
|
||||
{
|
||||
query: {
|
||||
project_name: projectName,
|
||||
},
|
||||
throwOnError: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (projects.length === 0) {
|
||||
throw new Error(
|
||||
`Unknown project name ${projectName}. Please confirm a managed project with this name exists.`,
|
||||
);
|
||||
} else if (projects.length > 1) {
|
||||
throw new Error(
|
||||
`Multiple projects found with name ${projectName}. Please specify organization_id.`,
|
||||
);
|
||||
}
|
||||
|
||||
const project = projects[0]!;
|
||||
|
||||
if (!project.id) {
|
||||
throw new Error(`No project found with name ${projectName}`);
|
||||
}
|
||||
|
||||
return project.id;
|
||||
}
|
||||
|
||||
async function deletePipelines(projectName: string) {
|
||||
try {
|
||||
initService();
|
||||
|
||||
const projectId = await getProjectId(projectName);
|
||||
|
||||
const { data: pipelines } =
|
||||
await PipelinesService.searchPipelinesApiV1PipelinesGet({
|
||||
query: { project_id: projectId },
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
console.log(`Deleting pipelines for project "${projectName}":`);
|
||||
|
||||
for (const pipeline of pipelines) {
|
||||
if (pipeline.id) {
|
||||
try {
|
||||
await PipelinesService.deletePipelineApiV1PipelinesPipelineIdDelete({
|
||||
path: { pipeline_id: pipeline.id },
|
||||
throwOnError: true,
|
||||
});
|
||||
console.log(
|
||||
`✅ Deleted pipeline: ${pipeline.name} (ID: ${pipeline.id})`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`❌ Failed to delete pipeline: ${pipeline.name} (ID: ${pipeline.id})`,
|
||||
);
|
||||
console.error(
|
||||
` Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn(`⚠️ Skipping pipeline with no ID: ${pipeline.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nDeletion process completed for project "${projectName}".`);
|
||||
console.log(`Total pipelines processed: ${pipelines.length}`);
|
||||
} catch (error) {
|
||||
console.error("Error during pipeline deletion process:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the project name from command line arguments
|
||||
const projectName = process.argv[2];
|
||||
|
||||
if (!projectName) {
|
||||
console.error("Please provide a project name as an argument.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
deletePipelines(projectName);
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@create-llama/e2e-clean",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "tsx clean.ts create-llama"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"tsx": "^4.19.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@llamaindex/cloud": "^0.2.14",
|
||||
"@llamaindex/core": "^0.2.12",
|
||||
"tiktoken": "^1.0.17"
|
||||
}
|
||||
}
|
||||
Generated
+398
@@ -0,0 +1,398 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@llamaindex/cloud':
|
||||
specifier: ^0.2.14
|
||||
version: 0.2.14(@llamaindex/core@0.2.12(tiktoken@1.0.17))(@llamaindex/env@0.1.13(tiktoken@1.0.17))
|
||||
'@llamaindex/core':
|
||||
specifier: ^0.2.12
|
||||
version: 0.2.12(tiktoken@1.0.17)
|
||||
tiktoken:
|
||||
specifier: ^1.0.17
|
||||
version: 1.0.17
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^20.0.0
|
||||
version: 20.16.11
|
||||
tsx:
|
||||
specifier: ^4.19.1
|
||||
version: 4.19.1
|
||||
|
||||
packages:
|
||||
|
||||
'@esbuild/aix-ppc64@0.23.1':
|
||||
resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.23.1':
|
||||
resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.23.1':
|
||||
resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.23.1':
|
||||
resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.23.1':
|
||||
resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.23.1':
|
||||
resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.23.1':
|
||||
resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.23.1':
|
||||
resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.23.1':
|
||||
resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.23.1':
|
||||
resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.23.1':
|
||||
resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.23.1':
|
||||
resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.23.1':
|
||||
resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.23.1':
|
||||
resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.23.1':
|
||||
resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.23.1':
|
||||
resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.23.1':
|
||||
resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-x64@0.23.1':
|
||||
resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.23.1':
|
||||
resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.23.1':
|
||||
resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/sunos-x64@0.23.1':
|
||||
resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.23.1':
|
||||
resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.23.1':
|
||||
resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.23.1':
|
||||
resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@llamaindex/cloud@0.2.14':
|
||||
resolution: {integrity: sha512-T6yy4xgTlA9ct+S48SP46F+HD+5LFIV1aLgZQd0+62doAfNnseXpI9UkvnnufcmxXgqljRhTdgHHW8UiZg43Sw==}
|
||||
peerDependencies:
|
||||
'@llamaindex/core': 0.2.12
|
||||
'@llamaindex/env': 0.1.13
|
||||
|
||||
'@llamaindex/core@0.2.12':
|
||||
resolution: {integrity: sha512-RWJ+Oh258aX8XkunTcblG8ttFKfanOZipka34wDTbmizPHt6OElF+MFIaffEJAx/P5TFCd6WjyOKXtvet6r3Jw==}
|
||||
|
||||
'@llamaindex/env@0.1.13':
|
||||
resolution: {integrity: sha512-FjCw8xfJ8Z0pevtunDM8QeYI4p8Yhar5FAnk8ljA7MTw7m8OdMRz2ET3/Pft+ANV9a+r3vO655+0GFcKM+5R5Q==}
|
||||
peerDependencies:
|
||||
'@aws-crypto/sha256-js': ^5.2.0
|
||||
'@xenova/transformers': ^2.17.2
|
||||
js-tiktoken: ^1.0.12
|
||||
pathe: ^1.1.2
|
||||
tiktoken: ^1.0.15
|
||||
peerDependenciesMeta:
|
||||
'@aws-crypto/sha256-js':
|
||||
optional: true
|
||||
'@xenova/transformers':
|
||||
optional: true
|
||||
js-tiktoken:
|
||||
optional: true
|
||||
pathe:
|
||||
optional: true
|
||||
tiktoken:
|
||||
optional: true
|
||||
|
||||
'@types/node@20.16.11':
|
||||
resolution: {integrity: sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==}
|
||||
|
||||
'@types/node@22.7.5':
|
||||
resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==}
|
||||
|
||||
esbuild@0.23.1:
|
||||
resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
get-tsconfig@4.8.1:
|
||||
resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==}
|
||||
|
||||
magic-bytes.js@1.10.0:
|
||||
resolution: {integrity: sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==}
|
||||
|
||||
resolve-pkg-maps@1.0.0:
|
||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||
|
||||
tiktoken@1.0.17:
|
||||
resolution: {integrity: sha512-UuFHqpy/DxOfNiC3otsqbx3oS6jr5uKdQhB/CvDEroZQbVHt+qAK+4JbIooabUWKU9g6PpsFylNu9Wcg4MxSGA==}
|
||||
|
||||
tsx@4.19.1:
|
||||
resolution: {integrity: sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@6.19.8:
|
||||
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
|
||||
|
||||
zod@3.23.8:
|
||||
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@esbuild/aix-ppc64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@llamaindex/cloud@0.2.14(@llamaindex/core@0.2.12(tiktoken@1.0.17))(@llamaindex/env@0.1.13(tiktoken@1.0.17))':
|
||||
dependencies:
|
||||
'@llamaindex/core': 0.2.12(tiktoken@1.0.17)
|
||||
'@llamaindex/env': 0.1.13(tiktoken@1.0.17)
|
||||
magic-bytes.js: 1.10.0
|
||||
|
||||
'@llamaindex/core@0.2.12(tiktoken@1.0.17)':
|
||||
dependencies:
|
||||
'@llamaindex/env': 0.1.13(tiktoken@1.0.17)
|
||||
'@types/node': 22.7.5
|
||||
magic-bytes.js: 1.10.0
|
||||
zod: 3.23.8
|
||||
transitivePeerDependencies:
|
||||
- '@aws-crypto/sha256-js'
|
||||
- '@xenova/transformers'
|
||||
- js-tiktoken
|
||||
- pathe
|
||||
- tiktoken
|
||||
|
||||
'@llamaindex/env@0.1.13(tiktoken@1.0.17)':
|
||||
dependencies:
|
||||
'@types/node': 22.7.5
|
||||
optionalDependencies:
|
||||
tiktoken: 1.0.17
|
||||
|
||||
'@types/node@20.16.11':
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
|
||||
'@types/node@22.7.5':
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
|
||||
esbuild@0.23.1:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.23.1
|
||||
'@esbuild/android-arm': 0.23.1
|
||||
'@esbuild/android-arm64': 0.23.1
|
||||
'@esbuild/android-x64': 0.23.1
|
||||
'@esbuild/darwin-arm64': 0.23.1
|
||||
'@esbuild/darwin-x64': 0.23.1
|
||||
'@esbuild/freebsd-arm64': 0.23.1
|
||||
'@esbuild/freebsd-x64': 0.23.1
|
||||
'@esbuild/linux-arm': 0.23.1
|
||||
'@esbuild/linux-arm64': 0.23.1
|
||||
'@esbuild/linux-ia32': 0.23.1
|
||||
'@esbuild/linux-loong64': 0.23.1
|
||||
'@esbuild/linux-mips64el': 0.23.1
|
||||
'@esbuild/linux-ppc64': 0.23.1
|
||||
'@esbuild/linux-riscv64': 0.23.1
|
||||
'@esbuild/linux-s390x': 0.23.1
|
||||
'@esbuild/linux-x64': 0.23.1
|
||||
'@esbuild/netbsd-x64': 0.23.1
|
||||
'@esbuild/openbsd-arm64': 0.23.1
|
||||
'@esbuild/openbsd-x64': 0.23.1
|
||||
'@esbuild/sunos-x64': 0.23.1
|
||||
'@esbuild/win32-arm64': 0.23.1
|
||||
'@esbuild/win32-ia32': 0.23.1
|
||||
'@esbuild/win32-x64': 0.23.1
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
get-tsconfig@4.8.1:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
magic-bytes.js@1.10.0: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
||||
tiktoken@1.0.17: {}
|
||||
|
||||
tsx@4.19.1:
|
||||
dependencies:
|
||||
esbuild: 0.23.1
|
||||
get-tsconfig: 4.8.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
undici-types@6.19.8: {}
|
||||
|
||||
zod@3.23.8: {}
|
||||
@@ -0,0 +1,237 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { exec } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import util from "util";
|
||||
import { TemplateFramework, TemplateVectorDB } from "../../helpers/types";
|
||||
import { RunCreateLlamaOptions, createTestDir, runCreateLlama } from "../utils";
|
||||
|
||||
const execAsync = util.promisify(exec);
|
||||
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
: "fastapi";
|
||||
const dataSource: string = process.env.DATASOURCE
|
||||
? process.env.DATASOURCE
|
||||
: "--example-file";
|
||||
|
||||
// TODO: add support for other templates
|
||||
|
||||
if (
|
||||
dataSource === "--example-file" // XXX: this test provides its own data source - only trigger it on one data source (usually the CI matrix will trigger multiple data sources)
|
||||
) {
|
||||
// vectorDBs, tools, and data source combinations to test
|
||||
const vectorDbs: TemplateVectorDB[] = [
|
||||
"mongo",
|
||||
"pg",
|
||||
"pinecone",
|
||||
"milvus",
|
||||
"astra",
|
||||
"qdrant",
|
||||
"chroma",
|
||||
"weaviate",
|
||||
];
|
||||
|
||||
const toolOptions = [
|
||||
"wikipedia.WikipediaToolSpec",
|
||||
"google.GoogleSearchToolSpec",
|
||||
"document_generator",
|
||||
"artifact",
|
||||
];
|
||||
|
||||
const dataSources = [
|
||||
"--example-file",
|
||||
"--web-source https://www.example.com",
|
||||
"--db-source mysql+pymysql://user:pass@localhost:3306/mydb",
|
||||
];
|
||||
|
||||
const observabilityOptions = ["llamatrace", "traceloop"];
|
||||
|
||||
test.describe("Mypy check", () => {
|
||||
test.describe.configure({ retries: 0 });
|
||||
|
||||
// Test vector databases
|
||||
for (const vectorDb of vectorDbs) {
|
||||
test(`Mypy check for vectorDB: ${vectorDb}`, async () => {
|
||||
const cwd = await createTestDir();
|
||||
const { pyprojectPath } = await createAndCheckLlamaProject({
|
||||
options: {
|
||||
cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework,
|
||||
dataSource: "--example-file",
|
||||
vectorDb,
|
||||
tools: "none",
|
||||
port: 3000,
|
||||
externalPort: 8000,
|
||||
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,
|
||||
externalPort: 8000,
|
||||
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,
|
||||
externalPort: 8000,
|
||||
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,
|
||||
externalPort: 8000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
observability,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function createAndCheckLlamaProject({
|
||||
options,
|
||||
}: {
|
||||
options: RunCreateLlamaOptions;
|
||||
}): Promise<{ pyprojectPath: string; projectPath: string }> {
|
||||
const result = await runCreateLlama(options);
|
||||
const name = result.projectName;
|
||||
const projectPath = path.join(options.cwd, name);
|
||||
|
||||
// Check if the app folder exists
|
||||
expect(fs.existsSync(projectPath)).toBeTruthy();
|
||||
|
||||
// Check if pyproject.toml exists
|
||||
const pyprojectPath = path.join(projectPath, "pyproject.toml");
|
||||
expect(fs.existsSync(pyprojectPath)).toBeTruthy();
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
POETRY_VIRTUALENVS_IN_PROJECT: "true",
|
||||
};
|
||||
|
||||
// Run poetry install
|
||||
try {
|
||||
const { stdout: installStdout, stderr: installStderr } = await execAsync(
|
||||
"poetry install",
|
||||
{ cwd: projectPath, env },
|
||||
);
|
||||
console.log("poetry install stdout:", installStdout);
|
||||
console.error("poetry install stderr:", installStderr);
|
||||
} catch (error) {
|
||||
console.error("Error running poetry install:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Run poetry run mypy
|
||||
try {
|
||||
const { stdout: mypyStdout, stderr: mypyStderr } = await execAsync(
|
||||
"poetry run mypy .",
|
||||
{ cwd: projectPath, env },
|
||||
);
|
||||
console.log("poetry run mypy stdout:", mypyStdout);
|
||||
console.error("poetry run mypy stderr:", mypyStderr);
|
||||
} catch (error) {
|
||||
console.error("Error running mypy:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// If we reach this point without throwing an error, the test passes
|
||||
expect(true).toBeTruthy();
|
||||
|
||||
return { pyprojectPath, projectPath };
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import { expect, test } from "@playwright/test";
|
||||
import { ChildProcess } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { TemplateFramework } from "../helpers";
|
||||
import { createTestDir, runCreateLlama } from "./utils";
|
||||
import { TemplateFramework } from "../../helpers";
|
||||
import { createTestDir, runCreateLlama } from "../utils";
|
||||
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
@@ -16,9 +16,8 @@ const dataSource: string = process.env.DATASOURCE
|
||||
// The extractor template currently only works with FastAPI and files (and not on Windows)
|
||||
if (
|
||||
process.platform !== "win32" &&
|
||||
templateFramework !== "nextjs" &&
|
||||
templateFramework !== "express" &&
|
||||
dataSource !== "--no-files"
|
||||
templateFramework === "fastapi" &&
|
||||
dataSource === "--example-file"
|
||||
) {
|
||||
test.describe("Test extractor template", async () => {
|
||||
let frontendPort: number;
|
||||
@@ -32,16 +31,16 @@ if (
|
||||
cwd = await createTestDir();
|
||||
frontendPort = Math.floor(Math.random() * 10000) + 10000;
|
||||
backendPort = frontendPort + 1;
|
||||
const result = await runCreateLlama(
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
"extractor",
|
||||
"fastapi",
|
||||
"--example-file",
|
||||
"none",
|
||||
frontendPort,
|
||||
backendPort,
|
||||
"runApp",
|
||||
);
|
||||
templateType: "extractor",
|
||||
templateFramework: "fastapi",
|
||||
dataSource: "--example-file",
|
||||
vectorDb: "none",
|
||||
port: frontendPort,
|
||||
externalPort: backendPort,
|
||||
postInstallAction: "runApp",
|
||||
});
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
});
|
||||
@@ -7,22 +7,22 @@ import type {
|
||||
TemplateFramework,
|
||||
TemplatePostInstallAction,
|
||||
TemplateUI,
|
||||
} from "../helpers";
|
||||
import { createTestDir, runCreateLlama, type AppType } from "./utils";
|
||||
} from "../../helpers";
|
||||
import { createTestDir, runCreateLlama, type AppType } from "../utils";
|
||||
|
||||
const templateFramework: TemplateFramework = "fastapi";
|
||||
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 appType: AppType = templateFramework === "nextjs" ? "" : "--frontend";
|
||||
const userMessage = "Write a blog post about physical standards for letters";
|
||||
|
||||
test.describe(`Test multiagent template ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
|
||||
test.skip(
|
||||
process.platform !== "linux" ||
|
||||
process.env.FRAMEWORK !== "fastapi" ||
|
||||
process.env.DATASOURCE === "--no-files",
|
||||
"The multiagent template currently only works with FastAPI and files. We also only run on Linux to speed up tests.",
|
||||
process.platform !== "linux" || process.env.DATASOURCE === "--no-files",
|
||||
"The multiagent template currently only works with files. We also only run on Linux to speed up tests.",
|
||||
);
|
||||
let port: number;
|
||||
let externalPort: number;
|
||||
@@ -36,18 +36,18 @@ test.describe(`Test multiagent template ${templateFramework} ${dataSource} ${tem
|
||||
port = Math.floor(Math.random() * 10000) + 10000;
|
||||
externalPort = port + 1;
|
||||
cwd = await createTestDir();
|
||||
const result = await runCreateLlama(
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
"multiagent",
|
||||
templateType: "multiagent",
|
||||
templateFramework,
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
externalPort,
|
||||
templatePostInstallAction,
|
||||
postInstallAction: templatePostInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
);
|
||||
});
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
});
|
||||
@@ -66,7 +66,7 @@ test.describe(`Test multiagent template ${templateFramework} ${dataSource} ${tem
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await page.fill("form input", userMessage);
|
||||
await page.fill("form textarea", userMessage);
|
||||
|
||||
const responsePromise = page.waitForResponse((res) =>
|
||||
res.url().includes("/api/chat"),
|
||||
@@ -7,8 +7,8 @@ import type {
|
||||
TemplateFramework,
|
||||
TemplatePostInstallAction,
|
||||
TemplateUI,
|
||||
} from "../helpers";
|
||||
import { createTestDir, runCreateLlama, type AppType } from "./utils";
|
||||
} from "../../helpers";
|
||||
import { createTestDir, runCreateLlama, type AppType } from "../utils";
|
||||
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
@@ -39,20 +39,20 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
|
||||
port = Math.floor(Math.random() * 10000) + 10000;
|
||||
externalPort = port + 1;
|
||||
cwd = await createTestDir();
|
||||
const result = await runCreateLlama(
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
"streaming",
|
||||
templateType: "streaming",
|
||||
templateFramework,
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
externalPort,
|
||||
templatePostInstallAction,
|
||||
postInstallAction: templatePostInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
llamaCloudProjectName,
|
||||
llamaCloudIndexName,
|
||||
);
|
||||
});
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
});
|
||||
@@ -72,7 +72,7 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
|
||||
}) => {
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await page.fill("form input", userMessage);
|
||||
await page.fill("form textarea", userMessage);
|
||||
const [response] = await Promise.all([
|
||||
page.waitForResponse(
|
||||
(res) => {
|
||||
@@ -0,0 +1,106 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { exec } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import util from "util";
|
||||
import { TemplateFramework, TemplateVectorDB } from "../../helpers/types";
|
||||
import { createTestDir, runCreateLlama } from "../utils";
|
||||
|
||||
const execAsync = util.promisify(exec);
|
||||
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
: "nextjs";
|
||||
const dataSource: string = process.env.DATASOURCE
|
||||
? process.env.DATASOURCE
|
||||
: "--example-file";
|
||||
|
||||
// vectorDBs combinations to test
|
||||
const vectorDbs: TemplateVectorDB[] = [
|
||||
"mongo",
|
||||
"pg",
|
||||
"qdrant",
|
||||
"pinecone",
|
||||
"milvus",
|
||||
"astra",
|
||||
"chroma",
|
||||
"llamacloud",
|
||||
"weaviate",
|
||||
];
|
||||
|
||||
test.describe("Test resolve TS dependencies", () => {
|
||||
// Test vector DBs without LlamaParse
|
||||
for (const vectorDb of vectorDbs) {
|
||||
const optionDescription = `vectorDb: ${vectorDb}, dataSource: ${dataSource}`;
|
||||
|
||||
test(`Vector DB test - ${optionDescription}`, async () => {
|
||||
await runTest(vectorDb, false);
|
||||
});
|
||||
}
|
||||
|
||||
// Test LlamaParse with vectorDB 'none'
|
||||
test(`LlamaParse test - vectorDb: none, dataSource: ${dataSource}, llamaParse: true`, async () => {
|
||||
await runTest("none", true);
|
||||
});
|
||||
|
||||
async function runTest(
|
||||
vectorDb: TemplateVectorDB | "none",
|
||||
useLlamaParse: boolean,
|
||||
) {
|
||||
const cwd = await createTestDir();
|
||||
|
||||
const result = await runCreateLlama({
|
||||
cwd: cwd,
|
||||
templateType: "streaming",
|
||||
templateFramework: templateFramework,
|
||||
dataSource: dataSource,
|
||||
vectorDb: vectorDb,
|
||||
port: 3000,
|
||||
externalPort: 8000,
|
||||
postInstallAction: "none",
|
||||
templateUI: undefined,
|
||||
appType: templateFramework === "nextjs" ? "" : "--no-frontend",
|
||||
llamaCloudProjectName: undefined,
|
||||
llamaCloudIndexName: undefined,
|
||||
tools: undefined,
|
||||
useLlamaParse: useLlamaParse,
|
||||
});
|
||||
const name = result.projectName;
|
||||
|
||||
// Check if the app folder exists
|
||||
const appDir = path.join(cwd, name);
|
||||
const dirExists = fs.existsSync(appDir);
|
||||
expect(dirExists).toBeTruthy();
|
||||
|
||||
// Install dependencies using pnpm
|
||||
try {
|
||||
const { stderr: installStderr } = await execAsync(
|
||||
"pnpm install --prefer-offline",
|
||||
{
|
||||
cwd: appDir,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error installing dependencies:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Run tsc type check and capture the output
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(
|
||||
"pnpm exec tsc -b --diagnostics",
|
||||
{
|
||||
cwd: appDir,
|
||||
},
|
||||
);
|
||||
// Check if there's any error output
|
||||
expect(stderr).toBeFalsy();
|
||||
|
||||
// Log the stdout for debugging purposes
|
||||
console.log("TypeScript type-check output:", stdout);
|
||||
} catch (error) {
|
||||
console.error("Error running tsc:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
+63
-21
@@ -18,21 +18,41 @@ export type CreateLlamaResult = {
|
||||
appProcess: ChildProcess;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
export async function runCreateLlama(
|
||||
cwd: string,
|
||||
templateType: TemplateType,
|
||||
templateFramework: TemplateFramework,
|
||||
dataSource: string,
|
||||
vectorDb: TemplateVectorDB,
|
||||
port: number,
|
||||
externalPort: number,
|
||||
postInstallAction: TemplatePostInstallAction,
|
||||
templateUI?: TemplateUI,
|
||||
appType?: AppType,
|
||||
llamaCloudProjectName?: string,
|
||||
llamaCloudIndexName?: string,
|
||||
): Promise<CreateLlamaResult> {
|
||||
export type RunCreateLlamaOptions = {
|
||||
cwd: string;
|
||||
templateType: TemplateType;
|
||||
templateFramework: TemplateFramework;
|
||||
dataSource: string;
|
||||
vectorDb: TemplateVectorDB;
|
||||
port: number;
|
||||
externalPort: number;
|
||||
postInstallAction: TemplatePostInstallAction;
|
||||
templateUI?: TemplateUI;
|
||||
appType?: AppType;
|
||||
llamaCloudProjectName?: string;
|
||||
llamaCloudIndexName?: string;
|
||||
tools?: string;
|
||||
useLlamaParse?: boolean;
|
||||
observability?: string;
|
||||
};
|
||||
|
||||
export async function runCreateLlama({
|
||||
cwd,
|
||||
templateType,
|
||||
templateFramework,
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
externalPort,
|
||||
postInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
llamaCloudProjectName,
|
||||
llamaCloudIndexName,
|
||||
tools,
|
||||
useLlamaParse,
|
||||
observability,
|
||||
}: 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",
|
||||
@@ -41,10 +61,23 @@ export async function runCreateLlama(
|
||||
const name = [
|
||||
templateType,
|
||||
templateFramework,
|
||||
dataSource,
|
||||
dataSource.split(" ")[0],
|
||||
templateUI,
|
||||
appType,
|
||||
].join("-");
|
||||
|
||||
// Handle different data source types
|
||||
let dataSourceArgs = [];
|
||||
if (dataSource.includes("--web-source" || "--db-source")) {
|
||||
const webSource = dataSource.split(" ")[1];
|
||||
dataSourceArgs.push("--web-source", webSource);
|
||||
} else if (dataSource.includes("--db-source")) {
|
||||
const dbSource = dataSource.split(" ")[1];
|
||||
dataSourceArgs.push("--db-source", dbSource);
|
||||
} else {
|
||||
dataSourceArgs.push(dataSource);
|
||||
}
|
||||
|
||||
const commandArgs = [
|
||||
"create-llama",
|
||||
name,
|
||||
@@ -52,7 +85,7 @@ export async function runCreateLlama(
|
||||
templateType,
|
||||
"--framework",
|
||||
templateFramework,
|
||||
dataSource,
|
||||
...dataSourceArgs,
|
||||
"--vector-db",
|
||||
vectorDb,
|
||||
"--open-ai-key",
|
||||
@@ -65,8 +98,7 @@ export async function runCreateLlama(
|
||||
"--post-install-action",
|
||||
postInstallAction,
|
||||
"--tools",
|
||||
"none",
|
||||
"--no-llama-parse",
|
||||
tools ?? "none",
|
||||
"--observability",
|
||||
"none",
|
||||
"--llama-cloud-key",
|
||||
@@ -79,6 +111,14 @@ export async function runCreateLlama(
|
||||
if (appType) {
|
||||
commandArgs.push(appType);
|
||||
}
|
||||
if (useLlamaParse) {
|
||||
commandArgs.push("--use-llama-parse");
|
||||
} else {
|
||||
commandArgs.push("--no-llama-parse");
|
||||
}
|
||||
if (observability) {
|
||||
commandArgs.push("--observability", observability);
|
||||
}
|
||||
|
||||
const command = commandArgs.join(" ");
|
||||
console.log(`running command '${command}' in ${cwd}`);
|
||||
@@ -91,11 +131,11 @@ export async function runCreateLlama(
|
||||
},
|
||||
});
|
||||
appProcess.stderr?.on("data", (data) => {
|
||||
console.log(data.toString());
|
||||
console.error(data.toString());
|
||||
});
|
||||
appProcess.on("exit", (code) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
throw new Error(`create-llama command was failed!`);
|
||||
throw new Error(`create-llama command failed with exit code ${code}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -107,6 +147,8 @@ export async function runCreateLlama(
|
||||
port,
|
||||
externalPort,
|
||||
);
|
||||
} else if (postInstallAction === "dependencies") {
|
||||
await waitForProcess(appProcess, 1000 * 60); // wait 1 min for dependencies to be resolved
|
||||
} else {
|
||||
// wait 10 seconds for create-llama to exit
|
||||
await waitForProcess(appProcess, 1000 * 10);
|
||||
|
||||
+52
-60
@@ -36,74 +36,66 @@ export async function writeLoadersConfig(
|
||||
dataSources: TemplateDataSource[],
|
||||
useLlamaParse?: boolean,
|
||||
) {
|
||||
if (dataSources.length === 0) return; // no datasources, no config needed
|
||||
const loaderConfig = new Document({});
|
||||
// Web loader config
|
||||
const loaderConfig: Record<string, any> = {};
|
||||
|
||||
// Always set file loader config
|
||||
loaderConfig.file = createFileLoaderConfig(useLlamaParse);
|
||||
|
||||
if (dataSources.some((ds) => ds.type === "web")) {
|
||||
const webLoaderConfig = new Document({});
|
||||
|
||||
// Create config for browser driver arguments
|
||||
const driverArgNodeValue = webLoaderConfig.createNode([
|
||||
"--no-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
]);
|
||||
driverArgNodeValue.commentBefore =
|
||||
" The arguments to pass to the webdriver. E.g.: add --headless to run in headless mode";
|
||||
webLoaderConfig.set("driver_arguments", driverArgNodeValue);
|
||||
|
||||
// Create config for urls
|
||||
const urlConfigs = dataSources
|
||||
.filter((ds) => ds.type === "web")
|
||||
.map((ds) => {
|
||||
const dsConfig = ds.config as WebSourceConfig;
|
||||
return {
|
||||
base_url: dsConfig.baseUrl,
|
||||
prefix: dsConfig.prefix,
|
||||
depth: dsConfig.depth,
|
||||
};
|
||||
});
|
||||
const urlConfigNode = webLoaderConfig.createNode(urlConfigs);
|
||||
urlConfigNode.commentBefore = ` base_url: The URL to start crawling with
|
||||
prefix: Only crawl URLs matching the specified prefix
|
||||
depth: The maximum depth for BFS traversal
|
||||
You can add more websites by adding more entries (don't forget the - prefix from YAML)`;
|
||||
webLoaderConfig.set("urls", urlConfigNode);
|
||||
|
||||
// Add web config to the loaders config
|
||||
loaderConfig.set("web", webLoaderConfig);
|
||||
loaderConfig.web = createWebLoaderConfig(dataSources);
|
||||
}
|
||||
|
||||
// File loader config
|
||||
if (dataSources.some((ds) => ds.type === "file")) {
|
||||
// Add documentation to web loader config
|
||||
const node = loaderConfig.createNode({
|
||||
use_llama_parse: useLlamaParse,
|
||||
});
|
||||
node.commentBefore = ` use_llama_parse: Use LlamaParse if \`true\`. Needs a \`LLAMA_CLOUD_API_KEY\` from https://cloud.llamaindex.ai set as environment variable`;
|
||||
loaderConfig.set("file", node);
|
||||
}
|
||||
|
||||
// DB loader config
|
||||
const dbLoaders = dataSources.filter((ds) => ds.type === "db");
|
||||
if (dbLoaders.length > 0) {
|
||||
const dbLoaderConfig = new Document({});
|
||||
const configEntries = dbLoaders.map((ds) => {
|
||||
const dsConfig = ds.config as DbSourceConfig;
|
||||
return {
|
||||
uri: dsConfig.uri,
|
||||
queries: [dsConfig.queries],
|
||||
};
|
||||
});
|
||||
|
||||
const node = dbLoaderConfig.createNode(configEntries);
|
||||
node.commentBefore = ` The configuration for the database loader, only supports MySQL and PostgreSQL databases for now.
|
||||
uri: The URI for the database. E.g.: mysql+pymysql://user:password@localhost:3306/db or postgresql+psycopg2://user:password@localhost:5432/db
|
||||
query: The query to fetch data from the database. E.g.: SELECT * FROM table`;
|
||||
loaderConfig.set("db", node);
|
||||
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(loaderConfig));
|
||||
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],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
+21
-26
@@ -65,7 +65,7 @@ const getVectorDBEnvs = (
|
||||
{
|
||||
name: "PG_CONNECTION_STRING",
|
||||
description:
|
||||
"For generating a connection URI, see https://docs.timescale.com/use-timescale/latest/services/create-a-service\nThe PostgreSQL connection string.",
|
||||
"For generating a connection URI, see https://supabase.com/vector\nThe PostgreSQL connection string.",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -397,12 +397,6 @@ const getEngineEnvs = (): EnvVar[] => {
|
||||
description:
|
||||
"The number of similar embeddings to return when retrieving documents.",
|
||||
},
|
||||
{
|
||||
name: "STREAM_TIMEOUT",
|
||||
description:
|
||||
"The time in milliseconds to wait for the stream to return a response.",
|
||||
value: "60000",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -426,34 +420,35 @@ const getToolEnvs = (tools?: Tool[]): EnvVar[] => {
|
||||
const getSystemPromptEnv = (
|
||||
tools?: Tool[],
|
||||
dataSources?: TemplateDataSource[],
|
||||
framework?: TemplateFramework,
|
||||
template?: TemplateType,
|
||||
): EnvVar[] => {
|
||||
const defaultSystemPrompt =
|
||||
"You are a helpful assistant who helps users with their questions.";
|
||||
|
||||
const systemPromptEnv: EnvVar[] = [];
|
||||
// build tool system prompt by merging all tool system prompts
|
||||
let toolSystemPrompt = "";
|
||||
tools?.forEach((tool) => {
|
||||
const toolSystemPromptEnv = tool.envVars?.find(
|
||||
(env) => env.name === TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
);
|
||||
if (toolSystemPromptEnv) {
|
||||
toolSystemPrompt += toolSystemPromptEnv.value + "\n";
|
||||
}
|
||||
});
|
||||
// multiagent template doesn't need system prompt
|
||||
if (template !== "multiagent") {
|
||||
let toolSystemPrompt = "";
|
||||
tools?.forEach((tool) => {
|
||||
const toolSystemPromptEnv = tool.envVars?.find(
|
||||
(env) => env.name === TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
);
|
||||
if (toolSystemPromptEnv) {
|
||||
toolSystemPrompt += toolSystemPromptEnv.value + "\n";
|
||||
}
|
||||
});
|
||||
|
||||
const systemPrompt = toolSystemPrompt
|
||||
? `\"${toolSystemPrompt}\"`
|
||||
: defaultSystemPrompt;
|
||||
const systemPrompt = toolSystemPrompt
|
||||
? `\"${toolSystemPrompt}\"`
|
||||
: defaultSystemPrompt;
|
||||
|
||||
const systemPromptEnv = [
|
||||
{
|
||||
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.
|
||||
@@ -559,7 +554,7 @@ export const createBackendEnvFile = async (
|
||||
...getToolEnvs(opts.tools),
|
||||
...getTemplateEnvs(opts.template),
|
||||
...getObservabilityEnvs(opts.observability),
|
||||
...getSystemPromptEnv(opts.tools, opts.dataSources, opts.framework),
|
||||
...getSystemPromptEnv(opts.tools, opts.dataSources, opts.template),
|
||||
];
|
||||
// Render and write env file
|
||||
const content = renderEnvVar(envVars);
|
||||
|
||||
+19
-18
@@ -96,10 +96,11 @@ async function generateContextData(
|
||||
}
|
||||
}
|
||||
|
||||
const copyContextData = async (
|
||||
const prepareContextData = async (
|
||||
root: string,
|
||||
dataSources: TemplateDataSource[],
|
||||
) => {
|
||||
await makeDir(path.join(root, "data"));
|
||||
for (const dataSource of dataSources) {
|
||||
const dataSourceConfig = dataSource?.config as FileSourceConfig;
|
||||
// Copy local data
|
||||
@@ -174,25 +175,25 @@ export const installTemplate = async (
|
||||
await createBackendEnvFile(props.root, props);
|
||||
}
|
||||
|
||||
if (props.dataSources.length > 0) {
|
||||
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 copyContextData(
|
||||
props.root,
|
||||
props.dataSources.filter((ds) => ds.type === "file"),
|
||||
await generateContextData(
|
||||
props.framework,
|
||||
props.modelConfig,
|
||||
props.packageManager,
|
||||
props.vectorDb,
|
||||
props.llamaCloudKey,
|
||||
props.useLlamaParse,
|
||||
);
|
||||
if (
|
||||
props.postInstallAction === "runApp" ||
|
||||
props.postInstallAction === "dependencies"
|
||||
) {
|
||||
await generateContextData(
|
||||
props.framework,
|
||||
props.modelConfig,
|
||||
props.packageManager,
|
||||
props.vectorDb,
|
||||
props.llamaCloudKey,
|
||||
props.useLlamaParse,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Create outputs directory
|
||||
|
||||
@@ -3,8 +3,55 @@ import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions";
|
||||
|
||||
const MODELS = ["llama3-8b", "llama3-70b", "mixtral-8x7b"];
|
||||
const DEFAULT_MODEL = MODELS[0];
|
||||
import got from "got";
|
||||
import ora from "ora";
|
||||
import { red } from "picocolors";
|
||||
|
||||
const GROQ_API_URL = "https://api.groq.com/openai/v1";
|
||||
|
||||
async function getAvailableModelChoicesGroq(apiKey: string) {
|
||||
if (!apiKey) {
|
||||
throw new Error("Need Groq API key to retrieve model choices");
|
||||
}
|
||||
|
||||
const spinner = ora("Fetching available models from Groq").start();
|
||||
try {
|
||||
const response = await got(`${GROQ_API_URL}/models`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
timeout: 5000,
|
||||
responseType: "json",
|
||||
});
|
||||
const data: any = await response.body;
|
||||
spinner.stop();
|
||||
|
||||
// Filter out the Whisper models
|
||||
return data.data
|
||||
.filter((model: any) => !model.id.toLowerCase().includes("whisper"))
|
||||
.map((el: any) => {
|
||||
return {
|
||||
title: el.id,
|
||||
value: el.id,
|
||||
};
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
spinner.stop();
|
||||
console.log(error);
|
||||
if ((error as any).response?.statusCode === 401) {
|
||||
console.log(
|
||||
red(
|
||||
"Invalid Groq API key provided! Please provide a valid key and try again!",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.log(red("Request failed: " + error));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_MODEL = "llama3-70b-8192";
|
||||
|
||||
// Use huggingface embedding models for now as Groq doesn't support embedding models
|
||||
enum HuggingFaceEmbeddingModelType {
|
||||
@@ -66,12 +113,14 @@ export async function askGroqQuestions({
|
||||
// use default model values in CI or if user should not be asked
|
||||
const useDefaults = ciInfo.isCI || !askModels;
|
||||
if (!useDefaults) {
|
||||
const modelChoices = await getAvailableModelChoicesGroq(config.apiKey!);
|
||||
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "model",
|
||||
message: "Which LLM model would you like to use?",
|
||||
choices: MODELS.map(toChoice),
|
||||
choices: modelChoices,
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
|
||||
+57
-19
@@ -36,28 +36,28 @@ const getAdditionalDependencies = (
|
||||
case "mongo": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-mongodb",
|
||||
version: "^0.1.3",
|
||||
version: "^0.3.1",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "pg": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-postgres",
|
||||
version: "^0.1.1",
|
||||
version: "^0.2.5",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "pinecone": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-pinecone",
|
||||
version: "^0.1.3",
|
||||
version: "^0.2.1",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "milvus": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-milvus",
|
||||
version: "^0.1.20",
|
||||
version: "^0.2.0",
|
||||
});
|
||||
dependencies.push({
|
||||
name: "pymilvus",
|
||||
@@ -68,28 +68,28 @@ const getAdditionalDependencies = (
|
||||
case "astra": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-astra-db",
|
||||
version: "^0.1.5",
|
||||
version: "^0.2.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "qdrant": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-qdrant",
|
||||
version: "^0.2.8",
|
||||
version: "^0.3.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "chroma": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-chroma",
|
||||
version: "^0.1.8",
|
||||
version: "^0.2.0",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "weaviate": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-weaviate",
|
||||
version: "^1.0.2",
|
||||
version: "^1.1.1",
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -123,14 +123,14 @@ const getAdditionalDependencies = (
|
||||
extras: ["rsa"],
|
||||
});
|
||||
dependencies.push({
|
||||
name: "psycopg2",
|
||||
name: "psycopg2-binary",
|
||||
version: "^2.9.9",
|
||||
});
|
||||
break;
|
||||
case "llamacloud":
|
||||
dependencies.push({
|
||||
name: "llama-index-indices-managed-llama-cloud",
|
||||
version: "^0.3.0",
|
||||
version: "^0.3.1",
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -280,6 +280,17 @@ const mergePoetryDependencies = (
|
||||
}
|
||||
};
|
||||
|
||||
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[],
|
||||
@@ -364,7 +375,12 @@ export const installPythonTemplate = async ({
|
||||
| "modelConfig"
|
||||
>) => {
|
||||
console.log("\nInitializing Python project with template:", template, "\n");
|
||||
const templatePath = path.join(templatesDir, "types", template, framework);
|
||||
let templatePath;
|
||||
if (template === "extractor") {
|
||||
templatePath = path.join(templatesDir, "types", "extractor", framework);
|
||||
} else {
|
||||
templatePath = path.join(templatesDir, "types", "streaming", framework);
|
||||
}
|
||||
await copy("**", root, {
|
||||
parents: true,
|
||||
cwd: templatePath,
|
||||
@@ -401,21 +417,43 @@ export const installPythonTemplate = async ({
|
||||
cwd: path.join(compPath, "services", "python"),
|
||||
});
|
||||
}
|
||||
|
||||
if (template === "streaming") {
|
||||
// For the streaming template only:
|
||||
// Copy engine code
|
||||
if (template === "streaming" || template === "multiagent") {
|
||||
// Select and copy engine code based on data sources and tools
|
||||
let engine;
|
||||
if (dataSources.length > 0 && (!tools || tools.length === 0)) {
|
||||
console.log("\nNo tools selected - use optimized context chat engine\n");
|
||||
engine = "chat";
|
||||
} else {
|
||||
// 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 ?? []);
|
||||
}
|
||||
|
||||
if (template === "multiagent") {
|
||||
// Copy multi-agent code
|
||||
await copy("**", path.join(root), {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "multiagent", "python"),
|
||||
rename: assetRelocator,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Adding additional dependencies");
|
||||
@@ -439,7 +477,7 @@ export const installPythonTemplate = async ({
|
||||
if (observability === "llamatrace") {
|
||||
addOnDependencies.push({
|
||||
name: "llama-index-callbacks-arize-phoenix",
|
||||
version: "^0.1.6",
|
||||
version: "^0.2.1",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+51
-1
@@ -110,13 +110,36 @@ For better results, you can specify the region parameter to get results from a s
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "Document generator",
|
||||
name: "document_generator",
|
||||
supportedFrameworks: ["fastapi", "nextjs", "express"],
|
||||
dependencies: [
|
||||
{
|
||||
name: "xhtml2pdf",
|
||||
version: "^0.2.14",
|
||||
},
|
||||
{
|
||||
name: "markdown",
|
||||
version: "^3.7",
|
||||
},
|
||||
],
|
||||
type: ToolType.LOCAL,
|
||||
envVars: [
|
||||
{
|
||||
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
description: "System prompt for document generator tool.",
|
||||
value: `If user request for a report or a post, use document generator tool to create a file and reply with the link to the file.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "Code Interpreter",
|
||||
name: "interpreter",
|
||||
dependencies: [
|
||||
{
|
||||
name: "e2b_code_interpreter",
|
||||
version: "0.0.7",
|
||||
version: "0.0.10",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
@@ -139,6 +162,33 @@ For better results, you can specify the region parameter to get results from a s
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "Artifact Code Generator",
|
||||
name: "artifact",
|
||||
// Using pre-release version of e2b_code_interpreter
|
||||
// TODO: Update to stable version when 0.0.11 is released
|
||||
dependencies: [
|
||||
{
|
||||
name: "e2b_code_interpreter",
|
||||
version: "^0.0.11b38",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
type: ToolType.LOCAL,
|
||||
envVars: [
|
||||
{
|
||||
name: "E2B_API_KEY",
|
||||
description:
|
||||
"E2B_API_KEY key is required to run artifact code generator tool. Get it here: https://e2b.dev/docs/getting-started/api-key",
|
||||
},
|
||||
{
|
||||
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
|
||||
description: "System prompt for artifact code generator tool.",
|
||||
value:
|
||||
"You are a code assistant that can generate and execute code using its tools. Don't generate code yourself, use the provided tools instead. Do not show the code or sandbox url in chat, just describe the steps to build the application based on the code that is generated by your tools. Do not describe how to run the code, just the steps to build the application.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "OpenAPI action",
|
||||
name: "openapi_action.OpenAPIActionToolSpec",
|
||||
|
||||
+71
-4
@@ -33,8 +33,7 @@ export const installTSTemplate = async ({
|
||||
* Copy the template files to the target directory.
|
||||
*/
|
||||
console.log("\nInitializing project with template:", template, "\n");
|
||||
const type = template === "multiagent" ? "streaming" : template; // use nextjs streaming template for multiagent
|
||||
const templatePath = path.join(templatesDir, "types", type, framework);
|
||||
const templatePath = path.join(templatesDir, "types", "streaming", framework);
|
||||
const copySource = ["**"];
|
||||
|
||||
await copy(copySource, root, {
|
||||
@@ -124,6 +123,30 @@ export const installTSTemplate = async ({
|
||||
cwd: path.join(compPath, "vectordbs", "typescript", vectorDb ?? "none"),
|
||||
});
|
||||
|
||||
if (template === "multiagent") {
|
||||
const multiagentPath = path.join(compPath, "multiagent", "typescript");
|
||||
|
||||
// copy workflow code for multiagent template
|
||||
await copy("**", path.join(root, relativeEngineDestPath, "workflow"), {
|
||||
parents: true,
|
||||
cwd: path.join(multiagentPath, "workflow"),
|
||||
});
|
||||
|
||||
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, {
|
||||
@@ -134,7 +157,10 @@ export const installTSTemplate = async ({
|
||||
// Select and copy engine code based on data sources and tools
|
||||
let engine;
|
||||
tools = tools ?? [];
|
||||
if (dataSources.length > 0 && tools.length === 0) {
|
||||
// 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 {
|
||||
@@ -145,6 +171,11 @@ export const installTSTemplate = async ({
|
||||
cwd: path.join(compPath, "engines", "typescript", engine),
|
||||
});
|
||||
|
||||
// copy settings to engine folder
|
||||
await copy("**", enginePath, {
|
||||
cwd: path.join(compPath, "settings", "typescript"),
|
||||
});
|
||||
|
||||
/**
|
||||
* Copy the selected UI files to the target directory and reference it.
|
||||
*/
|
||||
@@ -180,6 +211,7 @@ export const installTSTemplate = async ({
|
||||
framework,
|
||||
ui,
|
||||
observability,
|
||||
vectorDb,
|
||||
});
|
||||
|
||||
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
|
||||
@@ -200,9 +232,16 @@ async function updatePackageJson({
|
||||
framework,
|
||||
ui,
|
||||
observability,
|
||||
vectorDb,
|
||||
}: Pick<
|
||||
InstallTemplateArgs,
|
||||
"root" | "appName" | "dataSources" | "framework" | "ui" | "observability"
|
||||
| "root"
|
||||
| "appName"
|
||||
| "dataSources"
|
||||
| "framework"
|
||||
| "ui"
|
||||
| "observability"
|
||||
| "vectorDb"
|
||||
> & {
|
||||
relativeEngineDestPath: string;
|
||||
}): Promise<any> {
|
||||
@@ -249,6 +288,34 @@ async function updatePackageJson({
|
||||
};
|
||||
}
|
||||
|
||||
if (vectorDb === "pg") {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
pg: "^8.12.0",
|
||||
pgvector: "^0.2.0",
|
||||
};
|
||||
}
|
||||
|
||||
if (vectorDb === "qdrant") {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"@qdrant/js-client-rest": "^1.11.0",
|
||||
};
|
||||
}
|
||||
if (vectorDb === "mongo") {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
mongodb: "^6.7.0",
|
||||
};
|
||||
}
|
||||
|
||||
if (vectorDb === "milvus") {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"@zilliz/milvus2-sdk-node": "^2.4.6",
|
||||
};
|
||||
}
|
||||
|
||||
if (observability === "traceloop") {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
|
||||
@@ -90,6 +90,20 @@ const program = new Commander.Command(packageJson.name)
|
||||
`
|
||||
|
||||
Select to use an example PDF as data source.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--web-source <url>",
|
||||
`
|
||||
|
||||
Specify a website URL to use as a data source.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--db-source <connection-string>",
|
||||
`
|
||||
|
||||
Specify a database connection string to use as a data source.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
@@ -215,6 +229,27 @@ if (process.argv.includes("--no-files")) {
|
||||
},
|
||||
EXAMPLE_FILE,
|
||||
];
|
||||
} else if (process.argv.includes("--web-source")) {
|
||||
program.dataSources = [
|
||||
{
|
||||
type: "web",
|
||||
config: {
|
||||
baseUrl: program.webSource,
|
||||
prefix: program.webSource,
|
||||
depth: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
} else if (process.argv.includes("--db-source")) {
|
||||
program.dataSources = [
|
||||
{
|
||||
type: "db",
|
||||
config: {
|
||||
uri: program.dbSource,
|
||||
queries: program.dbQuery || "SELECT * FROM mytable",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const packageManager = !!program.useNpm
|
||||
|
||||
+4
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.16",
|
||||
"description": "Create LlamaIndex-powered apps with one command",
|
||||
"keywords": [
|
||||
"rag",
|
||||
@@ -25,6 +25,9 @@
|
||||
"clean": "rimraf --glob ./dist ./templates/**/__pycache__ ./templates/**/node_modules ./templates/**/poetry.lock",
|
||||
"dev": "ncc build ./index.ts -w -o dist/",
|
||||
"e2e": "playwright test",
|
||||
"e2e:python": "playwright test e2e/shared e2e/python",
|
||||
"e2e:typescript": "playwright test e2e/shared e2e/typescript",
|
||||
"e2e:clean": "pnpm --filter @create-llama/e2e-clean clean",
|
||||
"format": "prettier --ignore-unknown --cache --check .",
|
||||
"format:write": "prettier --ignore-unknown --write .",
|
||||
"lint": "eslint . --ignore-pattern dist --ignore-pattern e2e/cache",
|
||||
|
||||
+10
-12
@@ -141,12 +141,10 @@ export const getDataSourceChoices = (
|
||||
});
|
||||
}
|
||||
if (selectedDataSource === undefined || selectedDataSource.length === 0) {
|
||||
if (template !== "multiagent") {
|
||||
choices.push({
|
||||
title: "No datasource",
|
||||
value: "none",
|
||||
});
|
||||
}
|
||||
choices.push({
|
||||
title: "No datasource",
|
||||
value: "none",
|
||||
});
|
||||
choices.push({
|
||||
title:
|
||||
process.platform !== "linux"
|
||||
@@ -410,10 +408,7 @@ export const askQuestions = async (
|
||||
return; // early return - no further questions needed for llamapack projects
|
||||
}
|
||||
|
||||
if (program.template === "multiagent") {
|
||||
// TODO: multi-agents currently only supports FastAPI
|
||||
program.framework = preferences.framework = "fastapi";
|
||||
} else if (program.template === "extractor") {
|
||||
if (program.template === "extractor") {
|
||||
// Extractor 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];
|
||||
@@ -637,6 +632,7 @@ export const askQuestions = async (
|
||||
type: "db",
|
||||
config: await prompts(dbPrompts, questionHandlers),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "llamacloud": {
|
||||
program.dataSources.push({
|
||||
@@ -736,8 +732,10 @@ export const askQuestions = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (!program.tools && program.template === "streaming") {
|
||||
// TODO: allow to select tools also for multi-agent framework
|
||||
if (
|
||||
!program.tools &&
|
||||
(program.template === "streaming" || program.template === "multiagent")
|
||||
) {
|
||||
if (ciInfo.isCI) {
|
||||
program.tools = getPrefOrDefault("tools");
|
||||
} else {
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
from app.engine.tools import ToolFactory
|
||||
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 llama_index.core.tools.query_engine import QueryEngineTool
|
||||
|
||||
|
||||
def get_chat_engine(filters=None, params=None, event_handlers=None):
|
||||
def get_chat_engine(filters=None, params=None, event_handlers=None, **kwargs):
|
||||
system_prompt = os.getenv("SYSTEM_PROMPT")
|
||||
top_k = int(os.getenv("TOP_K", 0))
|
||||
tools = []
|
||||
tools: List[BaseTool] = []
|
||||
callback_manager = CallbackManager(handlers=event_handlers or [])
|
||||
|
||||
# Add query tool if index exists
|
||||
@@ -25,7 +27,8 @@ def get_chat_engine(filters=None, params=None, event_handlers=None):
|
||||
tools.append(query_engine_tool)
|
||||
|
||||
# Add additional tools
|
||||
tools += ToolFactory.from_env()
|
||||
configured_tools: List[BaseTool] = ToolFactory.from_env()
|
||||
tools.extend(configured_tools)
|
||||
|
||||
return AgentRunner.from_llm(
|
||||
llm=Settings.llm,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import os
|
||||
import yaml
|
||||
import importlib
|
||||
from llama_index.core.tools.tool_spec.base import BaseToolSpec
|
||||
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:
|
||||
@@ -16,7 +18,8 @@ class ToolFactory:
|
||||
ToolType.LOCAL: "app.engine.tools",
|
||||
}
|
||||
|
||||
def load_tools(tool_type: str, tool_name: str, config: dict) -> list[FunctionTool]:
|
||||
@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:
|
||||
@@ -40,14 +43,34 @@ class ToolFactory:
|
||||
raise ValueError(f"Failed to load tool {tool_name}: {e}")
|
||||
|
||||
@staticmethod
|
||||
def from_env() -> list[FunctionTool]:
|
||||
tools = []
|
||||
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, List[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():
|
||||
tools.extend(
|
||||
ToolFactory.load_tools(tool_type, tool_name, config)
|
||||
loaded_tools = ToolFactory.load_tools(
|
||||
tool_type, tool_name, config
|
||||
)
|
||||
if map_result:
|
||||
tools[tool_name] = loaded_tools # type: ignore
|
||||
else:
|
||||
tools.extend(loaded_tools) # type: ignore
|
||||
|
||||
return tools
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
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, old_code: Optional[str] = None) -> Dict:
|
||||
"""Generate a code artifact based on the input.
|
||||
|
||||
Args:
|
||||
query (str): The description of the application you want to build.
|
||||
old_code (Optional[str], optional): The existing code to be modified. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Dict: A dictionary containing the generated artifact information.
|
||||
"""
|
||||
|
||||
if old_code:
|
||||
user_message = f"{query}\n\nThe existing code is: \n```\n{old_code}\n```"
|
||||
else:
|
||||
user_message = query
|
||||
|
||||
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
|
||||
return data.model_dump()
|
||||
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)]
|
||||
@@ -0,0 +1,229 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from enum import Enum
|
||||
from io import BytesIO
|
||||
|
||||
from llama_index.core.tools.function_tool import FunctionTool
|
||||
|
||||
OUTPUT_DIR = "output/tools"
|
||||
|
||||
|
||||
class DocumentType(Enum):
|
||||
PDF = "pdf"
|
||||
HTML = "html"
|
||||
|
||||
|
||||
COMMON_STYLES = """
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.3;
|
||||
color: #333;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 0.7em;
|
||||
}
|
||||
code {
|
||||
background-color: #f4f4f4;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
pre {
|
||||
background-color: #f4f4f4;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background-color: #f2f2f2;
|
||||
font-weight: bold;
|
||||
}
|
||||
"""
|
||||
|
||||
HTML_SPECIFIC_STYLES = """
|
||||
body {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
"""
|
||||
|
||||
PDF_SPECIFIC_STYLES = """
|
||||
@page {
|
||||
size: letter;
|
||||
margin: 2cm;
|
||||
}
|
||||
body {
|
||||
font-size: 11pt;
|
||||
}
|
||||
h1 { font-size: 18pt; }
|
||||
h2 { font-size: 16pt; }
|
||||
h3 { font-size: 14pt; }
|
||||
h4, h5, h6 { font-size: 12pt; }
|
||||
pre, code {
|
||||
font-family: Courier, monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
"""
|
||||
|
||||
HTML_TEMPLATE = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
{common_styles}
|
||||
{specific_styles}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{content}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
class DocumentGenerator:
|
||||
@classmethod
|
||||
def _generate_html_content(cls, original_content: str) -> str:
|
||||
"""
|
||||
Generate HTML content from the original markdown content.
|
||||
"""
|
||||
try:
|
||||
import markdown
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Failed to import required modules. Please install markdown."
|
||||
)
|
||||
|
||||
# Convert markdown to HTML with fenced code and table extensions
|
||||
html_content = markdown.markdown(
|
||||
original_content, extensions=["fenced_code", "tables"]
|
||||
)
|
||||
return html_content
|
||||
|
||||
@classmethod
|
||||
def _generate_pdf(cls, html_content: str) -> BytesIO:
|
||||
"""
|
||||
Generate a PDF from the HTML content.
|
||||
"""
|
||||
try:
|
||||
from xhtml2pdf import pisa
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Failed to import required modules. Please install xhtml2pdf."
|
||||
)
|
||||
|
||||
pdf_html = HTML_TEMPLATE.format(
|
||||
common_styles=COMMON_STYLES,
|
||||
specific_styles=PDF_SPECIFIC_STYLES,
|
||||
content=html_content,
|
||||
)
|
||||
|
||||
buffer = BytesIO()
|
||||
pdf = pisa.pisaDocument(
|
||||
BytesIO(pdf_html.encode("UTF-8")), buffer, encoding="UTF-8"
|
||||
)
|
||||
|
||||
if pdf.err:
|
||||
logging.error(f"PDF generation failed: {pdf.err}")
|
||||
raise ValueError("PDF generation failed")
|
||||
|
||||
buffer.seek(0)
|
||||
return buffer
|
||||
|
||||
@classmethod
|
||||
def _generate_html(cls, html_content: str) -> str:
|
||||
"""
|
||||
Generate a complete HTML document with the given HTML content.
|
||||
"""
|
||||
return HTML_TEMPLATE.format(
|
||||
common_styles=COMMON_STYLES,
|
||||
specific_styles=HTML_SPECIFIC_STYLES,
|
||||
content=html_content,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def generate_document(
|
||||
cls, original_content: str, document_type: str, file_name: str
|
||||
) -> str:
|
||||
"""
|
||||
To generate document as PDF or HTML file.
|
||||
Parameters:
|
||||
original_content: str (markdown style)
|
||||
document_type: str (pdf or html) specify the type of the file format based on the use case
|
||||
file_name: str (name of the document file) must be a valid file name, no extensions needed
|
||||
Returns:
|
||||
str (URL to the document file): A file URL ready to serve.
|
||||
"""
|
||||
try:
|
||||
document_type = DocumentType(document_type.lower())
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"Invalid document type: {document_type}. Must be 'pdf' or 'html'."
|
||||
)
|
||||
# Always generate html content first
|
||||
html_content = cls._generate_html_content(original_content)
|
||||
|
||||
# Based on the type of document, generate the corresponding file
|
||||
if document_type == DocumentType.PDF:
|
||||
content = cls._generate_pdf(html_content)
|
||||
file_extension = "pdf"
|
||||
elif document_type == DocumentType.HTML:
|
||||
content = BytesIO(cls._generate_html(html_content).encode("utf-8"))
|
||||
file_extension = "html"
|
||||
else:
|
||||
raise ValueError(f"Unexpected document type: {document_type}")
|
||||
|
||||
file_name = cls._validate_file_name(file_name)
|
||||
file_path = os.path.join(OUTPUT_DIR, f"{file_name}.{file_extension}")
|
||||
|
||||
cls._write_to_file(content, file_path)
|
||||
|
||||
file_url = f"{os.getenv('FILESERVER_URL_PREFIX')}/{file_path}"
|
||||
return file_url
|
||||
|
||||
@staticmethod
|
||||
def _write_to_file(content: BytesIO, file_path: str):
|
||||
"""
|
||||
Write the content to a file.
|
||||
"""
|
||||
try:
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
with open(file_path, "wb") as file:
|
||||
file.write(content.getvalue())
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def _validate_file_name(file_name: str) -> str:
|
||||
"""
|
||||
Validate the file name.
|
||||
"""
|
||||
# Don't allow directory traversal
|
||||
if os.path.isabs(file_name):
|
||||
raise ValueError("File name is not allowed.")
|
||||
# Don't allow special characters
|
||||
if re.match(r"^[a-zA-Z0-9_.-]+$", file_name):
|
||||
return file_name
|
||||
else:
|
||||
raise ValueError("File name is not allowed to contain special characters.")
|
||||
|
||||
|
||||
def get_tools(**kwargs):
|
||||
return [FunctionTool.from_defaults(DocumentGenerator.generate_document)]
|
||||
@@ -21,16 +21,50 @@ def duckduckgo_search(
|
||||
"Please install it by running: `poetry add duckduckgo_search` or `pip install duckduckgo_search`"
|
||||
)
|
||||
|
||||
params = {
|
||||
"keywords": query,
|
||||
"region": region,
|
||||
"max_results": max_results,
|
||||
}
|
||||
results = []
|
||||
with DDGS() as ddg:
|
||||
results = list(ddg.text(**params))
|
||||
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)]
|
||||
return [
|
||||
FunctionTool.from_defaults(duckduckgo_search),
|
||||
FunctionTool.from_defaults(duckduckgo_image_search),
|
||||
]
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
import logging
|
||||
import requests
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
import requests
|
||||
from llama_index.core.tools import FunctionTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,7 +27,7 @@ class ImageGeneratorToolOutput(BaseModel):
|
||||
|
||||
class ImageGeneratorTool:
|
||||
_IMG_OUTPUT_FORMAT = "webp"
|
||||
_IMG_OUTPUT_DIR = "output/tool"
|
||||
_IMG_OUTPUT_DIR = "output/tools"
|
||||
_IMG_GEN_API = "https://api.stability.ai/v2beta/stable-image/generate/core"
|
||||
|
||||
def __init__(self, api_key: str = None):
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import os
|
||||
import logging
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Dict, Optional
|
||||
from llama_index.core.tools import FunctionTool
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from e2b_code_interpreter import CodeInterpreter
|
||||
from e2b_code_interpreter.models import Logs
|
||||
|
||||
from llama_index.core.tools import FunctionTool
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,7 +26,7 @@ class E2BToolOutput(BaseModel):
|
||||
|
||||
|
||||
class E2BCodeInterpreter:
|
||||
output_dir = "output/tool"
|
||||
output_dir = "output/tools"
|
||||
|
||||
def __init__(self, api_key: str = None):
|
||||
if api_key is None:
|
||||
|
||||
@@ -9,7 +9,7 @@ from llama_index.core.memory import ChatMemoryBuffer
|
||||
from llama_index.core.settings import Settings
|
||||
|
||||
|
||||
def get_chat_engine(filters=None, params=None, event_handlers=None):
|
||||
def get_chat_engine(filters=None, 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))
|
||||
@@ -43,6 +43,6 @@ def get_chat_engine(filters=None, params=None, event_handlers=None):
|
||||
memory=memory,
|
||||
system_prompt=system_prompt,
|
||||
retriever=retriever,
|
||||
node_postprocessors=node_postprocessors,
|
||||
node_postprocessors=node_postprocessors, # type: ignore
|
||||
callback_manager=callback_manager,
|
||||
)
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { BaseToolWithCall, OpenAIAgent, QueryEngineTool } from "llamaindex";
|
||||
import {
|
||||
BaseChatEngine,
|
||||
BaseToolWithCall,
|
||||
OpenAIAgent,
|
||||
QueryEngineTool,
|
||||
} from "llamaindex";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { getDataSource } from "./index";
|
||||
@@ -37,8 +42,10 @@ export async function createChatEngine(documentIds?: string[], params?: any) {
|
||||
tools.push(...(await createTools(toolConfig)));
|
||||
}
|
||||
|
||||
return new OpenAIAgent({
|
||||
const agent = new OpenAIAgent({
|
||||
tools,
|
||||
systemPrompt: process.env.SYSTEM_PROMPT,
|
||||
});
|
||||
}) as unknown as BaseChatEngine;
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
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;
|
||||
};
|
||||
|
||||
export type CodeGeneratorParameter = {
|
||||
requirement: string;
|
||||
oldCode?: 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,
|
||||
},
|
||||
},
|
||||
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,
|
||||
);
|
||||
return artifact as JSONValue;
|
||||
} catch (error) {
|
||||
return { isError: true };
|
||||
}
|
||||
}
|
||||
|
||||
// Generate artifact (code, environment, dependencies, etc.)
|
||||
async generateArtifact(
|
||||
query: string,
|
||||
oldCode?: string,
|
||||
): Promise<CodeArtifact> {
|
||||
const userMessage = `
|
||||
${query}
|
||||
${oldCode ? `The existing code is: \n\`\`\`${oldCode}\`\`\`` : ""}
|
||||
`;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
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({})];
|
||||
}
|
||||
@@ -5,15 +5,19 @@ import { BaseTool, ToolMetadata } from "llamaindex";
|
||||
export type DuckDuckGoParameter = {
|
||||
query: string;
|
||||
region?: string;
|
||||
maxResults?: number;
|
||||
};
|
||||
|
||||
export type DuckDuckGoToolParams = {
|
||||
metadata?: ToolMetadata<JSONSchemaType<DuckDuckGoParameter>>;
|
||||
};
|
||||
|
||||
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<DuckDuckGoParameter>> = {
|
||||
name: "duckduckgo",
|
||||
description: "Use this function to search for any query in DuckDuckGo.",
|
||||
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: {
|
||||
@@ -27,6 +31,12 @@ const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<DuckDuckGoParameter>> = {
|
||||
"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"],
|
||||
},
|
||||
@@ -42,15 +52,18 @@ export class DuckDuckGoSearchTool implements BaseTool<DuckDuckGoParameter> {
|
||||
metadata: ToolMetadata<JSONSchemaType<DuckDuckGoParameter>>;
|
||||
|
||||
constructor(params: DuckDuckGoToolParams) {
|
||||
this.metadata = params.metadata ?? DEFAULT_META_DATA;
|
||||
this.metadata = params.metadata ?? DEFAULT_SEARCH_METADATA;
|
||||
}
|
||||
|
||||
async call(input: DuckDuckGoParameter) {
|
||||
const { query, region } = input;
|
||||
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.map((result) => {
|
||||
return searchResults.results.slice(0, maxResults).map((result) => {
|
||||
return {
|
||||
title: result.title,
|
||||
description: result.description,
|
||||
@@ -59,3 +72,7 @@ export class DuckDuckGoSearchTool implements BaseTool<DuckDuckGoParameter> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getTools() {
|
||||
return [new DuckDuckGoSearchTool({})];
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<ImgGeneratorParameter>> = {
|
||||
|
||||
export class ImgGeneratorTool implements BaseTool<ImgGeneratorParameter> {
|
||||
readonly IMG_OUTPUT_FORMAT = "webp";
|
||||
readonly IMG_OUTPUT_DIR = "output/tool";
|
||||
readonly IMG_OUTPUT_DIR = "output/tools";
|
||||
readonly IMG_GEN_API =
|
||||
"https://api.stability.ai/v2beta/stable-image/generate/core";
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { BaseToolWithCall } from "llamaindex";
|
||||
import { ToolsFactory } from "llamaindex/tools/ToolsFactory";
|
||||
import { CodeGeneratorTool, CodeGeneratorToolParams } from "./code-generator";
|
||||
import {
|
||||
DocumentGenerator,
|
||||
DocumentGeneratorParams,
|
||||
} from "./document-generator";
|
||||
import { DuckDuckGoSearchTool, DuckDuckGoToolParams } from "./duckduckgo";
|
||||
import { ImgGeneratorTool, ImgGeneratorToolParams } from "./img-gen";
|
||||
import { InterpreterTool, InterpreterToolParams } from "./interpreter";
|
||||
@@ -43,6 +48,12 @@ const toolFactory: Record<string, ToolCreator> = {
|
||||
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)];
|
||||
},
|
||||
};
|
||||
|
||||
async function createLocalTools(
|
||||
|
||||
@@ -56,7 +56,7 @@ const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<InterpreterParameter>> = {
|
||||
};
|
||||
|
||||
export class InterpreterTool implements BaseTool<InterpreterParameter> {
|
||||
private readonly outputDir = "output/tool";
|
||||
private readonly outputDir = "output/tools";
|
||||
private apiKey?: string;
|
||||
private fileServerURLPrefix?: string;
|
||||
metadata: ToolMetadata<JSONSchemaType<InterpreterParameter>>;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from "fs";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { getExtractors } from "../../engine/loader";
|
||||
|
||||
const MIME_TYPE_TO_EXT: Record<string, string> = {
|
||||
@@ -15,8 +16,12 @@ export async function storeAndParseFile(
|
||||
fileBuffer: Buffer,
|
||||
mimeType: string,
|
||||
) {
|
||||
const fileExt = MIME_TYPE_TO_EXT[mimeType];
|
||||
if (!fileExt) throw new Error(`Unsupported document type: ${mimeType}`);
|
||||
|
||||
const documents = await loadDocuments(fileBuffer, mimeType);
|
||||
await saveDocument(filename, fileBuffer, mimeType);
|
||||
const filepath = path.join(UPLOADED_FOLDER, filename);
|
||||
await saveDocument(filepath, fileBuffer);
|
||||
for (const document of documents) {
|
||||
document.metadata = {
|
||||
...document.metadata,
|
||||
@@ -38,26 +43,31 @@ async function loadDocuments(fileBuffer: Buffer, mimeType: string) {
|
||||
return await reader.loadDataAsContent(fileBuffer);
|
||||
}
|
||||
|
||||
async function saveDocument(
|
||||
filename: string,
|
||||
fileBuffer: Buffer,
|
||||
mimeType: string,
|
||||
) {
|
||||
const fileExt = MIME_TYPE_TO_EXT[mimeType];
|
||||
if (!fileExt) throw new Error(`Unsupported document type: ${mimeType}`);
|
||||
|
||||
const filepath = `${UPLOADED_FOLDER}/${filename}`;
|
||||
const fileurl = `${process.env.FILESERVER_URL_PREFIX}/${filepath}`;
|
||||
|
||||
if (!fs.existsSync(UPLOADED_FOLDER)) {
|
||||
fs.mkdirSync(UPLOADED_FOLDER, { recursive: true });
|
||||
// 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.");
|
||||
}
|
||||
const fileName = path.basename(filepath);
|
||||
if (!/^[a-zA-Z0-9_.-]+$/.test(fileName)) {
|
||||
throw new Error(
|
||||
"File name is not allowed to contain any special characters.",
|
||||
);
|
||||
}
|
||||
if (!process.env.FILESERVER_URL_PREFIX) {
|
||||
throw new Error("FILESERVER_URL_PREFIX environment variable is not set.");
|
||||
}
|
||||
await fs.promises.writeFile(filepath, fileBuffer);
|
||||
|
||||
console.log(`Saved document file to ${filepath}.\nURL: ${fileurl}`);
|
||||
return {
|
||||
filename,
|
||||
filepath,
|
||||
fileurl,
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { JSONValue } from "ai";
|
||||
import { JSONValue, Message } from "ai";
|
||||
import { MessageContent, MessageContentDetail } from "llamaindex";
|
||||
|
||||
export type DocumentFileType = "csv" | "pdf" | "txt" | "docx";
|
||||
@@ -21,13 +21,20 @@ type Annotation = {
|
||||
data: object;
|
||||
};
|
||||
|
||||
export function retrieveDocumentIds(annotations?: JSONValue[]): string[] {
|
||||
if (!annotations) return [];
|
||||
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 annotations = getAllAnnotations(messages);
|
||||
if (annotations.length === 0) return [];
|
||||
|
||||
const ids: string[] = [];
|
||||
|
||||
for (const annotation of annotations) {
|
||||
const { type, data } = getValidAnnotation(annotation);
|
||||
for (const { type, data } of annotations) {
|
||||
if (
|
||||
type === "document_file" &&
|
||||
"files" in data &&
|
||||
@@ -37,9 +44,7 @@ export function retrieveDocumentIds(annotations?: JSONValue[]): string[] {
|
||||
for (const file of files) {
|
||||
if (Array.isArray(file.content.value)) {
|
||||
// it's an array, so it's an array of doc IDs
|
||||
for (const id of file.content.value) {
|
||||
ids.push(id);
|
||||
}
|
||||
ids.push(...file.content.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,24 +53,69 @@ export function retrieveDocumentIds(annotations?: JSONValue[]): string[] {
|
||||
return ids;
|
||||
}
|
||||
|
||||
export function convertMessageContent(
|
||||
content: string,
|
||||
annotations?: JSONValue[],
|
||||
): MessageContent {
|
||||
if (!annotations) return content;
|
||||
export function retrieveMessageContent(messages: Message[]): MessageContent {
|
||||
const userMessage = messages[messages.length - 1];
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: content,
|
||||
text: userMessage.content,
|
||||
},
|
||||
...convertAnnotations(annotations),
|
||||
...retrieveLatestArtifact(messages),
|
||||
...convertAnnotations(messages),
|
||||
];
|
||||
}
|
||||
|
||||
function convertAnnotations(annotations: JSONValue[]): MessageContentDetail[] {
|
||||
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[] {
|
||||
// annotations from the last user message that has annotations
|
||||
const annotations: Annotation[] =
|
||||
messages
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((message) => message.role === "user" && message.annotations)
|
||||
?.annotations?.map(getValidAnnotation) || [];
|
||||
if (annotations.length === 0) return [];
|
||||
|
||||
const content: MessageContentDetail[] = [];
|
||||
annotations.forEach((annotation: JSONValue) => {
|
||||
const { type, data } = getValidAnnotation(annotation);
|
||||
annotations.forEach(({ type, data }) => {
|
||||
// convert image
|
||||
if (type === "image" && "url" in data && typeof data.url === "string") {
|
||||
content.push({
|
||||
|
||||
@@ -69,15 +69,6 @@ export function appendToolData(
|
||||
});
|
||||
}
|
||||
|
||||
export function createStreamTimeout(stream: StreamData) {
|
||||
const timeout = Number(process.env.STREAM_TIMEOUT ?? 1000 * 60 * 5); // default to 5 minutes
|
||||
const t = setTimeout(() => {
|
||||
appendEventData(stream, `Stream timed out after ${timeout / 1000} seconds`);
|
||||
stream.close();
|
||||
}, timeout);
|
||||
return t;
|
||||
}
|
||||
|
||||
export function createCallbackManager(stream: StreamData) {
|
||||
const callbackManager = new CallbackManager();
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import {
|
||||
StreamData,
|
||||
createCallbacksTransformer,
|
||||
createStreamDataTransformer,
|
||||
trimStartOfStreamHelper,
|
||||
type AIStreamCallbacksAndOptions,
|
||||
} from "ai";
|
||||
import { ChatMessage, EngineResponse } from "llamaindex";
|
||||
import { generateNextQuestions } from "./suggestion";
|
||||
|
||||
export function LlamaIndexStream(
|
||||
response: AsyncIterable<EngineResponse>,
|
||||
data: StreamData,
|
||||
chatHistory: ChatMessage[],
|
||||
opts?: {
|
||||
callbacks?: AIStreamCallbacksAndOptions;
|
||||
},
|
||||
): ReadableStream<Uint8Array> {
|
||||
return createParser(response, data, chatHistory)
|
||||
.pipeThrough(createCallbacksTransformer(opts?.callbacks))
|
||||
.pipeThrough(createStreamDataTransformer());
|
||||
}
|
||||
|
||||
function createParser(
|
||||
res: AsyncIterable<EngineResponse>,
|
||||
data: StreamData,
|
||||
chatHistory: ChatMessage[],
|
||||
) {
|
||||
const it = res[Symbol.asyncIterator]();
|
||||
const trimStartOfStream = trimStartOfStreamHelper();
|
||||
let llmTextResponse = "";
|
||||
|
||||
return new ReadableStream<string>({
|
||||
async pull(controller): Promise<void> {
|
||||
const { value, done } = await it.next();
|
||||
if (done) {
|
||||
controller.close();
|
||||
// LLM stream is done, generate the next questions with a new LLM call
|
||||
chatHistory.push({ role: "assistant", content: llmTextResponse });
|
||||
const questions: string[] = await generateNextQuestions(chatHistory);
|
||||
if (questions.length > 0) {
|
||||
data.appendMessageAnnotation({
|
||||
type: "suggested_questions",
|
||||
data: questions,
|
||||
});
|
||||
}
|
||||
data.close();
|
||||
return;
|
||||
}
|
||||
const text = trimStartOfStream(value.delta ?? "");
|
||||
if (text) {
|
||||
llmTextResponse += text;
|
||||
controller.enqueue(text);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,20 +1,22 @@
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import yaml
|
||||
import yaml # type: ignore
|
||||
from app.engine.loaders.db import DBLoaderConfig, get_db_documents
|
||||
from app.engine.loaders.file import FileLoaderConfig, get_file_documents
|
||||
from app.engine.loaders.web import WebLoaderConfig, get_web_documents
|
||||
from llama_index.core import Document
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_configs():
|
||||
def load_configs() -> Dict[str, Any]:
|
||||
with open("config/loaders.yaml") as f:
|
||||
configs = yaml.safe_load(f)
|
||||
return configs
|
||||
|
||||
|
||||
def get_documents():
|
||||
def get_documents() -> List[Document]:
|
||||
documents = []
|
||||
config = load_configs()
|
||||
for loader_type, loader_config in config.items():
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -11,7 +12,13 @@ class DBLoaderConfig(BaseModel):
|
||||
|
||||
|
||||
def get_db_documents(configs: list[DBLoaderConfig]):
|
||||
from llama_index.readers.database import DatabaseReader
|
||||
try:
|
||||
from llama_index.readers.database import DatabaseReader
|
||||
except ImportError:
|
||||
logger.error(
|
||||
"Failed to import DatabaseReader. Make sure llama_index is installed."
|
||||
)
|
||||
raise
|
||||
|
||||
docs = []
|
||||
for entry in configs:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
@@ -8,8 +10,8 @@ class CrawlUrl(BaseModel):
|
||||
|
||||
|
||||
class WebLoaderConfig(BaseModel):
|
||||
driver_arguments: list[str] = Field(default=None)
|
||||
urls: list[CrawlUrl]
|
||||
driver_arguments: Optional[List[str]] = Field(default_factory=list)
|
||||
urls: List[CrawlUrl]
|
||||
|
||||
|
||||
def get_web_documents(config: WebLoaderConfig):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LlamaParseReader } from "llamaindex/readers/LlamaParseReader";
|
||||
import { LlamaParseReader } from "llamaindex";
|
||||
import {
|
||||
FILE_EXT_TO_READER,
|
||||
SimpleDirectoryReader,
|
||||
|
||||
+14
-11
@@ -1,16 +1,14 @@
|
||||
import asyncio
|
||||
from typing import Any, List
|
||||
|
||||
from llama_index.core.tools.types import ToolMetadata, ToolOutput
|
||||
from llama_index.core.tools.utils import create_schema_from_function
|
||||
from llama_index.core.workflow import Context, Workflow
|
||||
|
||||
from app.agents.planner import StructuredPlannerAgent
|
||||
from app.agents.single import (
|
||||
AgentRunResult,
|
||||
ContextAwareTool,
|
||||
FunctionCallingAgent,
|
||||
)
|
||||
from app.agents.planner import StructuredPlannerAgent
|
||||
from llama_index.core.tools.types import ToolMetadata, ToolOutput
|
||||
from llama_index.core.tools.utils import create_schema_from_function
|
||||
from llama_index.core.workflow import Context, StopEvent, Workflow
|
||||
|
||||
|
||||
class AgentCallTool(ContextAwareTool):
|
||||
@@ -27,18 +25,23 @@ class AgentCallTool(ContextAwareTool):
|
||||
name=name,
|
||||
description=(
|
||||
f"Use this tool to delegate a sub task to the {agent.name} agent."
|
||||
+ (f" The agent is an {agent.role}." if agent.role else "")
|
||||
+ (
|
||||
f" The agent is an {agent.description}."
|
||||
if agent.description
|
||||
else ""
|
||||
)
|
||||
),
|
||||
fn_schema=fn_schema,
|
||||
)
|
||||
|
||||
# overload the acall function with the ctx argument as it's needed for bubbling the events
|
||||
async def acall(self, ctx: Context, input: str) -> ToolOutput:
|
||||
task = asyncio.create_task(self.agent.run(input=input))
|
||||
handler = self.agent.run(input=input)
|
||||
# bubble all events while running the agent to the calling agent
|
||||
async for ev in self.agent.stream_events():
|
||||
ctx.write_event_to_stream(ev)
|
||||
ret: AgentRunResult = await task
|
||||
async for ev in handler.stream_events():
|
||||
if type(ev) is not StopEvent:
|
||||
ctx.write_event_to_stream(ev)
|
||||
ret: AgentRunResult = await handler
|
||||
response = ret.response.message.content
|
||||
return ToolOutput(
|
||||
content=str(response),
|
||||
+47
-28
@@ -1,8 +1,8 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
from enum import Enum
|
||||
from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from app.agents.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent
|
||||
from llama_index.core.agent.runner.planner import (
|
||||
DEFAULT_INITIAL_PLAN_PROMPT,
|
||||
DEFAULT_PLAN_REFINE_PROMPT,
|
||||
@@ -11,6 +11,7 @@ from llama_index.core.agent.runner.planner import (
|
||||
SubTask,
|
||||
)
|
||||
from llama_index.core.bridge.pydantic import ValidationError
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.llms.function_calling import FunctionCallingLLM
|
||||
from llama_index.core.prompts import PromptTemplate
|
||||
from llama_index.core.settings import Settings
|
||||
@@ -24,7 +25,17 @@ from llama_index.core.workflow import (
|
||||
step,
|
||||
)
|
||||
|
||||
from app.agents.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent
|
||||
INITIAL_PLANNER_PROMPT = """\
|
||||
Think step-by-step. Given a conversation, set of tools and a user request. Your responsibility is to create a plan to complete the task.
|
||||
The plan must adapt with the user request and the conversation.
|
||||
|
||||
The tools available are:
|
||||
{tools_str}
|
||||
|
||||
Conversation: {chat_history}
|
||||
|
||||
Overall Task: {task}
|
||||
"""
|
||||
|
||||
|
||||
class ExecutePlanEvent(Event):
|
||||
@@ -64,14 +75,21 @@ class StructuredPlannerAgent(Workflow):
|
||||
tools: List[BaseTool] | None = None,
|
||||
timeout: float = 360.0,
|
||||
refine_plan: bool = False,
|
||||
chat_history: Optional[List[ChatMessage]] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(*args, timeout=timeout, **kwargs)
|
||||
self.name = name
|
||||
self.refine_plan = refine_plan
|
||||
self.chat_history = chat_history
|
||||
|
||||
self.tools = tools or []
|
||||
self.planner = Planner(llm=llm, tools=self.tools, verbose=self._verbose)
|
||||
self.planner = Planner(
|
||||
llm=llm,
|
||||
tools=self.tools,
|
||||
initial_plan_prompt=INITIAL_PLANNER_PROMPT,
|
||||
verbose=self._verbose,
|
||||
)
|
||||
# The executor is keeping the memory of all tool calls and decides to call the right tool for the task
|
||||
self.executor = FunctionCallingAgent(
|
||||
name="executor",
|
||||
@@ -91,7 +109,9 @@ class StructuredPlannerAgent(Workflow):
|
||||
ctx.data["streaming"] = getattr(ev, "streaming", False)
|
||||
ctx.data["task"] = ev.input
|
||||
|
||||
plan_id, plan = await self.planner.create_plan(input=ev.input)
|
||||
plan_id, plan = await self.planner.create_plan(
|
||||
input=ev.input, chat_history=self.chat_history
|
||||
)
|
||||
ctx.data["act_plan_id"] = plan_id
|
||||
|
||||
# inform about the new plan
|
||||
@@ -108,11 +128,12 @@ class StructuredPlannerAgent(Workflow):
|
||||
ctx.data["act_plan_id"]
|
||||
)
|
||||
|
||||
ctx.data["num_sub_tasks"] = len(upcoming_sub_tasks)
|
||||
# send an event per sub task
|
||||
events = [SubTaskEvent(sub_task=sub_task) for sub_task in upcoming_sub_tasks]
|
||||
for event in events:
|
||||
ctx.send_event(event)
|
||||
if upcoming_sub_tasks:
|
||||
# Execute only the first sub-task
|
||||
# otherwise the executor will get over-lapping messages
|
||||
# alternatively, we could use one executor for all sub tasks
|
||||
next_sub_task = upcoming_sub_tasks[0]
|
||||
return SubTaskEvent(sub_task=next_sub_task)
|
||||
|
||||
return None
|
||||
|
||||
@@ -122,19 +143,19 @@ class StructuredPlannerAgent(Workflow):
|
||||
) -> SubTaskResultEvent:
|
||||
if self._verbose:
|
||||
print(f"=== Executing sub task: {ev.sub_task.name} ===")
|
||||
is_last_tasks = ctx.data["num_sub_tasks"] == self.get_remaining_subtasks(ctx)
|
||||
is_last_tasks = self.get_remaining_subtasks(ctx) == 1
|
||||
# TODO: streaming only works without plan refining
|
||||
streaming = is_last_tasks and ctx.data["streaming"] and not self.refine_plan
|
||||
task = asyncio.create_task(
|
||||
self.executor.run(
|
||||
input=ev.sub_task.input,
|
||||
streaming=streaming,
|
||||
)
|
||||
handler = self.executor.run(
|
||||
input=ev.sub_task.input,
|
||||
streaming=streaming,
|
||||
)
|
||||
# bubble all events while running the executor to the planner
|
||||
async for event in self.executor.stream_events():
|
||||
ctx.write_event_to_stream(event)
|
||||
result = await task
|
||||
async for event in handler.stream_events():
|
||||
# Don't write the StopEvent from sub task to the stream
|
||||
if type(event) is not StopEvent:
|
||||
ctx.write_event_to_stream(event)
|
||||
result: AgentRunResult = await handler
|
||||
if self._verbose:
|
||||
print("=== Done executing sub task ===\n")
|
||||
self.planner.state.add_completed_sub_task(ctx.data["act_plan_id"], ev.sub_task)
|
||||
@@ -144,22 +165,17 @@ class StructuredPlannerAgent(Workflow):
|
||||
async def gather_results(
|
||||
self, ctx: Context, ev: SubTaskResultEvent
|
||||
) -> ExecutePlanEvent | StopEvent:
|
||||
# wait for all sub tasks to finish
|
||||
num_sub_tasks = ctx.data["num_sub_tasks"]
|
||||
results = ctx.collect_events(ev, [SubTaskResultEvent] * num_sub_tasks)
|
||||
if results is None:
|
||||
return None
|
||||
result = ev
|
||||
|
||||
upcoming_sub_tasks = self.get_upcoming_sub_tasks(ctx)
|
||||
# if no more tasks to do, stop workflow and send result of last step
|
||||
if upcoming_sub_tasks == 0:
|
||||
return StopEvent(result=results[-1].result)
|
||||
return StopEvent(result=result.result)
|
||||
|
||||
if self.refine_plan:
|
||||
# store all results for refining the plan
|
||||
# store the result for refining the plan
|
||||
ctx.data["results"] = ctx.data.get("results", {})
|
||||
for result in results:
|
||||
ctx.data["results"][result.sub_task.name] = result.result
|
||||
ctx.data["results"][result.sub_task.name] = result.result
|
||||
|
||||
new_plan = await self.planner.refine_plan(
|
||||
ctx.data["task"], ctx.data["act_plan_id"], ctx.data["results"]
|
||||
@@ -215,7 +231,9 @@ class Planner:
|
||||
plan_refine_prompt = PromptTemplate(plan_refine_prompt)
|
||||
self.plan_refine_prompt = plan_refine_prompt
|
||||
|
||||
async def create_plan(self, input: str) -> Tuple[str, Plan]:
|
||||
async def create_plan(
|
||||
self, input: str, chat_history: Optional[List[ChatMessage]] = None
|
||||
) -> Tuple[str, Plan]:
|
||||
tools = self.tools
|
||||
tools_str = ""
|
||||
for tool in tools:
|
||||
@@ -227,6 +245,7 @@ class Planner:
|
||||
self.initial_plan_prompt,
|
||||
tools_str=tools_str,
|
||||
task=input,
|
||||
chat_history=chat_history,
|
||||
)
|
||||
except (ValueError, ValidationError):
|
||||
if self.verbose:
|
||||
+3
-5
@@ -5,10 +5,8 @@ from llama_index.core.llms import ChatMessage, ChatResponse
|
||||
from llama_index.core.llms.function_calling import FunctionCallingLLM
|
||||
from llama_index.core.memory import ChatMemoryBuffer
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.core.tools import ToolOutput, ToolSelection
|
||||
from llama_index.core.tools import FunctionTool, ToolOutput, ToolSelection
|
||||
from llama_index.core.tools.types import BaseTool
|
||||
from llama_index.core.tools import FunctionTool
|
||||
|
||||
from llama_index.core.workflow import (
|
||||
Context,
|
||||
Event,
|
||||
@@ -64,14 +62,14 @@ class FunctionCallingAgent(Workflow):
|
||||
timeout: float = 360.0,
|
||||
name: str,
|
||||
write_events: bool = True,
|
||||
role: Optional[str] = None,
|
||||
description: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(*args, verbose=verbose, timeout=timeout, **kwargs)
|
||||
self.tools = tools or []
|
||||
self.name = name
|
||||
self.role = role
|
||||
self.write_events = write_events
|
||||
self.description = description
|
||||
|
||||
if llm is None:
|
||||
llm = Settings.llm
|
||||
@@ -0,0 +1,46 @@
|
||||
import logging
|
||||
|
||||
from app.api.routers.events import EventCallbackHandler
|
||||
from app.api.routers.models import (
|
||||
ChatData,
|
||||
)
|
||||
from app.api.routers.vercel_response import VercelStreamResponse
|
||||
from app.engine.engine import get_chat_engine
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, status
|
||||
|
||||
chat_router = r = APIRouter()
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
@r.post("")
|
||||
async def chat(
|
||||
request: Request,
|
||||
data: ChatData,
|
||||
background_tasks: BackgroundTasks,
|
||||
):
|
||||
try:
|
||||
last_message_content = data.get_last_message_content()
|
||||
messages = data.get_history_messages(include_agent_messages=True)
|
||||
|
||||
event_handler = EventCallbackHandler()
|
||||
# The chat API supports passing private document filters and chat params
|
||||
# but agent workflow does not support them yet
|
||||
# ignore chat params and use all documents for now
|
||||
# TODO: generate filters based on doc_ids
|
||||
params = data.data or {}
|
||||
engine = get_chat_engine(chat_history=messages, params=params)
|
||||
|
||||
event_handler = engine.run(input=last_message_content, streaming=True)
|
||||
return VercelStreamResponse(
|
||||
request=request,
|
||||
chat_data=data,
|
||||
event_handler=event_handler,
|
||||
events=engine.stream_events(),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("Error in chat engine", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error in chat engine: {e}",
|
||||
) from e
|
||||
+77
-79
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
from asyncio import Task
|
||||
from abc import ABC
|
||||
from typing import AsyncGenerator, List
|
||||
|
||||
from aiostream import stream
|
||||
@@ -13,14 +13,88 @@ from fastapi.responses import StreamingResponse
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class VercelStreamResponse(StreamingResponse):
|
||||
class VercelStreamResponse(StreamingResponse, ABC):
|
||||
"""
|
||||
Class to convert the response from the chat engine to the streaming format expected by Vercel
|
||||
Base class to convert the response from the chat engine to the streaming format expected by Vercel
|
||||
"""
|
||||
|
||||
TEXT_PREFIX = "0:"
|
||||
DATA_PREFIX = "8:"
|
||||
|
||||
def __init__(self, request: Request, chat_data: ChatData, *args, **kwargs):
|
||||
self.request = request
|
||||
|
||||
stream = self._create_stream(request, chat_data, *args, **kwargs)
|
||||
content = self.content_generator(stream)
|
||||
|
||||
super().__init__(content=content)
|
||||
|
||||
async def content_generator(self, stream):
|
||||
is_stream_started = False
|
||||
|
||||
async with stream.stream() as streamer:
|
||||
async for output in streamer:
|
||||
if not is_stream_started:
|
||||
is_stream_started = True
|
||||
# Stream a blank message to start the stream
|
||||
yield self.convert_text("")
|
||||
|
||||
yield output
|
||||
|
||||
if await self.request.is_disconnected():
|
||||
break
|
||||
|
||||
def _create_stream(
|
||||
self,
|
||||
request: Request,
|
||||
chat_data: ChatData,
|
||||
event_handler: AgentRunResult | AsyncGenerator,
|
||||
events: AsyncGenerator[AgentRunEvent, None],
|
||||
verbose: bool = True,
|
||||
):
|
||||
# Yield the text response
|
||||
async def _chat_response_generator():
|
||||
result = await event_handler
|
||||
final_response = ""
|
||||
|
||||
if isinstance(result, AgentRunResult):
|
||||
for token in result.response.message.content:
|
||||
final_response += token
|
||||
yield self.convert_text(token)
|
||||
|
||||
if isinstance(result, AsyncGenerator):
|
||||
async for token in result:
|
||||
final_response += token.delta
|
||||
yield self.convert_text(token.delta)
|
||||
|
||||
# Generate next questions if next question prompt is configured
|
||||
question_data = await self._generate_next_questions(
|
||||
chat_data.messages, final_response
|
||||
)
|
||||
if question_data:
|
||||
yield self.convert_data(question_data)
|
||||
|
||||
# TODO: stream sources
|
||||
|
||||
# Yield the events from the event handler
|
||||
async def _event_generator():
|
||||
async for event in events:
|
||||
event_response = self._event_to_response(event)
|
||||
if verbose:
|
||||
logger.debug(event_response)
|
||||
if event_response is not None:
|
||||
yield self.convert_data(event_response)
|
||||
|
||||
combine = stream.merge(_chat_response_generator(), _event_generator())
|
||||
return combine
|
||||
|
||||
@staticmethod
|
||||
def _event_to_response(event: AgentRunEvent) -> dict:
|
||||
return {
|
||||
"type": "agent",
|
||||
"data": {"agent": event.name, "text": event.msg},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def convert_text(cls, token: str):
|
||||
# Escape newlines and double quotes to avoid breaking the stream
|
||||
@@ -32,82 +106,6 @@ class VercelStreamResponse(StreamingResponse):
|
||||
data_str = json.dumps(data)
|
||||
return f"{cls.DATA_PREFIX}[{data_str}]\n"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
request: Request,
|
||||
task: Task[AgentRunResult | AsyncGenerator],
|
||||
events: AsyncGenerator[AgentRunEvent, None],
|
||||
chat_data: ChatData,
|
||||
verbose: bool = True,
|
||||
):
|
||||
content = VercelStreamResponse.content_generator(
|
||||
request, task, events, chat_data, verbose
|
||||
)
|
||||
super().__init__(content=content)
|
||||
|
||||
@classmethod
|
||||
async def content_generator(
|
||||
cls,
|
||||
request: Request,
|
||||
task: Task[AgentRunResult | AsyncGenerator],
|
||||
events: AsyncGenerator[AgentRunEvent, None],
|
||||
chat_data: ChatData,
|
||||
verbose: bool = True,
|
||||
):
|
||||
# Yield the text response
|
||||
async def _chat_response_generator():
|
||||
result = await task
|
||||
final_response = ""
|
||||
|
||||
if isinstance(result, AgentRunResult):
|
||||
for token in result.response.message.content:
|
||||
final_response += token
|
||||
yield cls.convert_text(token)
|
||||
|
||||
if isinstance(result, AsyncGenerator):
|
||||
async for token in result:
|
||||
final_response += token.delta
|
||||
yield cls.convert_text(token.delta)
|
||||
|
||||
# Generate next questions if next question prompt is configured
|
||||
question_data = await cls._generate_next_questions(
|
||||
chat_data.messages, final_response
|
||||
)
|
||||
if question_data:
|
||||
yield cls.convert_data(question_data)
|
||||
|
||||
# TODO: stream sources
|
||||
|
||||
# Yield the events from the event handler
|
||||
async def _event_generator():
|
||||
async for event in events():
|
||||
event_response = cls._event_to_response(event)
|
||||
if verbose:
|
||||
logger.debug(event_response)
|
||||
if event_response is not None:
|
||||
yield cls.convert_data(event_response)
|
||||
|
||||
combine = stream.merge(_chat_response_generator(), _event_generator())
|
||||
|
||||
is_stream_started = False
|
||||
async with combine.stream() as streamer:
|
||||
if not is_stream_started:
|
||||
is_stream_started = True
|
||||
# Stream a blank message to start the stream
|
||||
yield cls.convert_text("")
|
||||
|
||||
async for output in streamer:
|
||||
yield output
|
||||
if await request.is_disconnected():
|
||||
break
|
||||
|
||||
@staticmethod
|
||||
def _event_to_response(event: AgentRunEvent) -> dict:
|
||||
return {
|
||||
"type": "agent",
|
||||
"data": {"agent": event.name, "text": event.msg},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def _generate_next_questions(chat_history: List[Message], response: str):
|
||||
questions = await NextQuestionSuggestion.suggest_next_questions(
|
||||
+10
-10
@@ -1,28 +1,28 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import List, Optional
|
||||
|
||||
from app.examples.choreography import create_choreography
|
||||
from app.examples.orchestrator import create_orchestrator
|
||||
from app.examples.workflow import create_workflow
|
||||
|
||||
|
||||
from llama_index.core.workflow import Workflow
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
|
||||
|
||||
import os
|
||||
from llama_index.core.workflow import Workflow
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
def create_agent(chat_history: Optional[List[ChatMessage]] = None) -> Workflow:
|
||||
def get_chat_engine(
|
||||
chat_history: Optional[List[ChatMessage]] = None, **kwargs
|
||||
) -> Workflow:
|
||||
# TODO: the EXAMPLE_TYPE could be passed as a chat config parameter?
|
||||
agent_type = os.getenv("EXAMPLE_TYPE", "").lower()
|
||||
match agent_type:
|
||||
case "choreography":
|
||||
agent = create_choreography(chat_history)
|
||||
agent = create_choreography(chat_history, **kwargs)
|
||||
case "orchestrator":
|
||||
agent = create_orchestrator(chat_history)
|
||||
agent = create_orchestrator(chat_history, **kwargs)
|
||||
case _:
|
||||
agent = create_workflow(chat_history)
|
||||
agent = create_workflow(chat_history, **kwargs)
|
||||
|
||||
logger.info(f"Using agent pattern: {agent_type}")
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
from textwrap import dedent
|
||||
from typing import List, Optional
|
||||
|
||||
from app.agents.multi import AgentCallingAgent
|
||||
from app.agents.single import FunctionCallingAgent
|
||||
from app.examples.publisher import create_publisher
|
||||
from app.examples.researcher import create_researcher
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
|
||||
|
||||
def create_choreography(chat_history: Optional[List[ChatMessage]] = None, **kwargs):
|
||||
researcher = create_researcher(chat_history, **kwargs)
|
||||
publisher = create_publisher(chat_history)
|
||||
reviewer = FunctionCallingAgent(
|
||||
name="reviewer",
|
||||
description="expert in reviewing blog posts, needs a written post to review",
|
||||
system_prompt="You are an expert in reviewing blog posts. You are given a task to review a blog post. Review the post for logical inconsistencies, ask critical questions, and provide suggestions for improvement. Furthermore, proofread the post for grammar and spelling errors. If the post is good, you can say 'The post is good.'",
|
||||
chat_history=chat_history,
|
||||
)
|
||||
return AgentCallingAgent(
|
||||
name="writer",
|
||||
agents=[researcher, reviewer, publisher],
|
||||
description="expert in writing blog posts, needs researched information and images to write a blog post",
|
||||
system_prompt=dedent(
|
||||
"""
|
||||
You are an expert in writing blog posts. You are given a task to write a blog post. Before starting to write the post, consult the researcher agent to get the information you need. Don't make up any information yourself.
|
||||
After creating a draft for the post, send it to the reviewer agent to receive feedback and make sure to incorporate the feedback from the reviewer.
|
||||
You can consult the reviewer and researcher a maximum of two times. Your output should contain only the blog post.
|
||||
Finally, always request the publisher to create a document (PDF, HTML) and publish the blog post.
|
||||
"""
|
||||
),
|
||||
# TODO: add chat_history support to AgentCallingAgent
|
||||
# chat_history=chat_history,
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
from textwrap import dedent
|
||||
from typing import List, Optional
|
||||
|
||||
from app.agents.multi import AgentOrchestrator
|
||||
from app.agents.single import FunctionCallingAgent
|
||||
from app.examples.publisher import create_publisher
|
||||
from app.examples.researcher import create_researcher
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
|
||||
|
||||
def create_orchestrator(chat_history: Optional[List[ChatMessage]] = None, **kwargs):
|
||||
researcher = create_researcher(chat_history, **kwargs)
|
||||
writer = FunctionCallingAgent(
|
||||
name="writer",
|
||||
description="expert in writing blog posts, need information and images to write a post",
|
||||
system_prompt=dedent(
|
||||
"""
|
||||
You are an expert in writing blog posts.
|
||||
You are given a task to write a blog post. Do not make up any information yourself.
|
||||
If you don't have the necessary information to write a blog post, reply "I need information about the topic to write the blog post".
|
||||
If you need to use images, reply "I need images about the topic to write the blog post". Do not use any dummy images made up by you.
|
||||
If you have all the information needed, write the blog post.
|
||||
"""
|
||||
),
|
||||
chat_history=chat_history,
|
||||
)
|
||||
reviewer = FunctionCallingAgent(
|
||||
name="reviewer",
|
||||
description="expert in reviewing blog posts, needs a written blog post to review",
|
||||
system_prompt=dedent(
|
||||
"""
|
||||
You are an expert in reviewing blog posts. You are given a task to review a blog post. Review the post and fix any issues found yourself. You must output a final blog post.
|
||||
A post must include at least one valid image. If not, reply "I need images about the topic to write the blog post". An image URL starting with "example" or "your website" is not valid.
|
||||
Especially check for logical inconsistencies and proofread the post for grammar and spelling errors.
|
||||
"""
|
||||
),
|
||||
chat_history=chat_history,
|
||||
)
|
||||
publisher = create_publisher(chat_history)
|
||||
return AgentOrchestrator(
|
||||
agents=[writer, reviewer, researcher, publisher],
|
||||
refine_plan=False,
|
||||
chat_history=chat_history,
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
from textwrap import dedent
|
||||
from typing import List, Tuple
|
||||
|
||||
from app.agents.single import FunctionCallingAgent
|
||||
from app.engine.tools import ToolFactory
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.tools import FunctionTool
|
||||
|
||||
|
||||
def get_publisher_tools() -> Tuple[List[FunctionTool], str, str]:
|
||||
tools = []
|
||||
# Get configured tools from the tools.yaml file
|
||||
configured_tools = ToolFactory.from_env(map_result=True)
|
||||
if "document_generator" in configured_tools.keys():
|
||||
tools.extend(configured_tools["document_generator"])
|
||||
prompt_instructions = dedent("""
|
||||
Normally, reply the blog post content to the user directly.
|
||||
But if user requested to generate a file, use the document_generator tool to generate the file and reply the link to the file.
|
||||
""")
|
||||
description = "Expert in publishing the blog post, able to publish the blog post in PDF or HTML format."
|
||||
else:
|
||||
prompt_instructions = "You don't have a tool to generate document. Please reply the content directly."
|
||||
description = "Expert in publishing the blog post"
|
||||
return tools, prompt_instructions, description
|
||||
|
||||
|
||||
def create_publisher(chat_history: List[ChatMessage]):
|
||||
tools, prompt_instructions, description = get_publisher_tools()
|
||||
return FunctionCallingAgent(
|
||||
name="publisher",
|
||||
tools=tools,
|
||||
description=description,
|
||||
system_prompt=prompt_instructions,
|
||||
chat_history=chat_history,
|
||||
)
|
||||
@@ -0,0 +1,86 @@
|
||||
import os
|
||||
from textwrap import dedent
|
||||
from typing import List
|
||||
|
||||
from app.agents.single import FunctionCallingAgent
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
from app.engine.tools import ToolFactory
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.tools import QueryEngineTool, ToolMetadata
|
||||
|
||||
|
||||
def _create_query_engine_tool(params=None) -> QueryEngineTool:
|
||||
"""
|
||||
Provide an agent worker that can be used to query the index.
|
||||
"""
|
||||
# Add query tool if index exists
|
||||
index_config = IndexConfig(**(params or {}))
|
||||
index = get_index(index_config)
|
||||
if index is None:
|
||||
return None
|
||||
top_k = int(os.getenv("TOP_K", 0))
|
||||
query_engine = index.as_query_engine(
|
||||
**({"similarity_top_k": top_k} if top_k != 0 else {})
|
||||
)
|
||||
return QueryEngineTool(
|
||||
query_engine=query_engine,
|
||||
metadata=ToolMetadata(
|
||||
name="query_index",
|
||||
description="""
|
||||
Use this tool to retrieve information about the text corpus from the index.
|
||||
""",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _get_research_tools(**kwargs) -> QueryEngineTool:
|
||||
"""
|
||||
Researcher take responsibility for retrieving information.
|
||||
Try init wikipedia or duckduckgo tool if available.
|
||||
"""
|
||||
tools = []
|
||||
query_engine_tool = _create_query_engine_tool(**kwargs)
|
||||
if query_engine_tool is not None:
|
||||
tools.append(query_engine_tool)
|
||||
researcher_tool_names = ["duckduckgo", "wikipedia.WikipediaToolSpec"]
|
||||
configured_tools = ToolFactory.from_env(map_result=True)
|
||||
for tool_name, tool in configured_tools.items():
|
||||
if tool_name in researcher_tool_names:
|
||||
tools.extend(tool)
|
||||
return tools
|
||||
|
||||
|
||||
def create_researcher(chat_history: List[ChatMessage], **kwargs):
|
||||
"""
|
||||
Researcher is an agent that take responsibility for using tools to complete a given task.
|
||||
"""
|
||||
tools = _get_research_tools(**kwargs)
|
||||
return FunctionCallingAgent(
|
||||
name="researcher",
|
||||
tools=tools,
|
||||
description="expert in retrieving any unknown content or searching for images from the internet",
|
||||
system_prompt=dedent(
|
||||
"""
|
||||
You are a researcher agent. You are given a research task.
|
||||
|
||||
If the conversation already includes the information and there is no new request for additional information from the user, you should return the appropriate content to the writer.
|
||||
Otherwise, you must use tools to retrieve information or images needed for the task.
|
||||
|
||||
It's normal for the task to include some ambiguity. You must always think carefully about the context of the user's request to understand what are the main content needs to be retrieved.
|
||||
Example:
|
||||
Request: "Create a blog post about the history of the internet, write in English and publish in PDF format."
|
||||
->Though: The main content is "history of the internet", while "write in English and publish in PDF format" is a requirement for other agents.
|
||||
Your task: Look for information in English about the history of the Internet.
|
||||
This is not your task: Create a blog post or look for how to create a PDF.
|
||||
|
||||
Next request: "Publish the blog post in HTML format."
|
||||
->Though: User just asking for a format change, the previous content is still valid.
|
||||
Your task: Return the previous content of the post to the writer. No need to do any research.
|
||||
This is not your task: Look for how to create an HTML file.
|
||||
|
||||
If you use the tools but don't find any related information, please return "I didn't find any new information for {the topic}." along with the content you found. Don't try to make up information yourself.
|
||||
If the request doesn't need any new information because it was in the conversation history, please return "The task doesn't need any new information. Please reuse the existing content in the conversation history."
|
||||
"""
|
||||
),
|
||||
chat_history=chat_history,
|
||||
)
|
||||
@@ -0,0 +1,265 @@
|
||||
from textwrap import dedent
|
||||
from typing import AsyncGenerator, List, Optional
|
||||
|
||||
from app.agents.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent
|
||||
from app.examples.publisher import create_publisher
|
||||
from app.examples.researcher import create_researcher
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.prompts import PromptTemplate
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.core.workflow import (
|
||||
Context,
|
||||
Event,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
step,
|
||||
)
|
||||
|
||||
|
||||
def create_workflow(chat_history: Optional[List[ChatMessage]] = None, **kwargs):
|
||||
researcher = create_researcher(
|
||||
chat_history=chat_history,
|
||||
**kwargs,
|
||||
)
|
||||
publisher = create_publisher(
|
||||
chat_history=chat_history,
|
||||
)
|
||||
writer = FunctionCallingAgent(
|
||||
name="writer",
|
||||
description="expert in writing blog posts, need information and images to write a post.",
|
||||
system_prompt=dedent(
|
||||
"""
|
||||
You are an expert in writing blog posts.
|
||||
You are given the task of writing a blog post based on research content provided by the researcher agent. Do not invent any information yourself.
|
||||
It's important to read the entire conversation history to write the blog post accurately.
|
||||
If you receive a review from the reviewer, update the post according to the feedback and return the new post content.
|
||||
If the content is not valid (e.g., broken link, broken image, etc.), do not use it.
|
||||
It's normal for the task to include some ambiguity, so you must define the user's initial request to write the post correctly.
|
||||
If you update the post based on the reviewer's feedback, first explain what changes you made to the post, then provide the new post content. Do not include the reviewer's comments.
|
||||
Example:
|
||||
Task: "Here is the information I found about the history of the internet:
|
||||
Create a blog post about the history of the internet, write in English, and publish in PDF format."
|
||||
-> Your task: Use the research content {...} to write a blog post in English.
|
||||
-> This is not your task: Create a PDF
|
||||
Please note that a localhost link is acceptable, but dummy links like "example.com" or "your-website.com" are not valid.
|
||||
"""
|
||||
),
|
||||
chat_history=chat_history,
|
||||
)
|
||||
reviewer = FunctionCallingAgent(
|
||||
name="reviewer",
|
||||
description="expert in reviewing blog posts, needs a written blog post to review.",
|
||||
system_prompt=dedent(
|
||||
"""
|
||||
You are an expert in reviewing blog posts.
|
||||
You are given a task to review a blog post. As a reviewer, it's important that your review aligns with the user's request. Please focus on the user's request when reviewing the post.
|
||||
Review the post for logical inconsistencies, ask critical questions, and provide suggestions for improvement.
|
||||
Furthermore, proofread the post for grammar and spelling errors.
|
||||
Only if the post is good enough for publishing should you return 'The post is good.' In all other cases, return your review.
|
||||
It's normal for the task to include some ambiguity, so you must define the user's initial request to review the post correctly.
|
||||
Please note that a localhost link is acceptable, but dummy links like "example.com" or "your-website.com" are not valid.
|
||||
Example:
|
||||
Task: "Create a blog post about the history of the internet, write in English and publish in PDF format."
|
||||
-> Your task: Review whether the main content of the post is about the history of the internet and if it is written in English.
|
||||
-> This is not your task: Create blog post, create PDF, write in English.
|
||||
"""
|
||||
),
|
||||
chat_history=chat_history,
|
||||
)
|
||||
workflow = BlogPostWorkflow(
|
||||
timeout=360, chat_history=chat_history
|
||||
) # Pass chat_history here
|
||||
workflow.add_workflows(
|
||||
researcher=researcher,
|
||||
writer=writer,
|
||||
reviewer=reviewer,
|
||||
publisher=publisher,
|
||||
)
|
||||
return workflow
|
||||
|
||||
|
||||
class ResearchEvent(Event):
|
||||
input: str
|
||||
|
||||
|
||||
class WriteEvent(Event):
|
||||
input: str
|
||||
is_good: bool = False
|
||||
|
||||
|
||||
class ReviewEvent(Event):
|
||||
input: str
|
||||
|
||||
|
||||
class PublishEvent(Event):
|
||||
input: str
|
||||
|
||||
|
||||
class BlogPostWorkflow(Workflow):
|
||||
def __init__(
|
||||
self, timeout: int = 360, chat_history: Optional[List[ChatMessage]] = None
|
||||
):
|
||||
super().__init__(timeout=timeout)
|
||||
self.chat_history = chat_history or []
|
||||
|
||||
@step()
|
||||
async def start(self, ctx: Context, ev: StartEvent) -> ResearchEvent | PublishEvent:
|
||||
# set streaming
|
||||
ctx.data["streaming"] = getattr(ev, "streaming", False)
|
||||
# start the workflow with researching about a topic
|
||||
ctx.data["task"] = ev.input
|
||||
ctx.data["user_input"] = ev.input
|
||||
|
||||
# Decision-making process
|
||||
decision = await self._decide_workflow(ev.input, self.chat_history)
|
||||
|
||||
if decision != "publish":
|
||||
return ResearchEvent(input=f"Research for this task: {ev.input}")
|
||||
else:
|
||||
chat_history_str = "\n".join(
|
||||
[f"{msg.role}: {msg.content}" for msg in self.chat_history]
|
||||
)
|
||||
return PublishEvent(
|
||||
input=f"Please publish content based on the chat history\n{chat_history_str}\n\n and task: {ev.input}"
|
||||
)
|
||||
|
||||
async def _decide_workflow(
|
||||
self, input: str, chat_history: List[ChatMessage]
|
||||
) -> str:
|
||||
prompt_template = PromptTemplate(
|
||||
dedent(
|
||||
"""
|
||||
You are an expert in decision-making, helping people write and publish blog posts.
|
||||
If the user is asking for a file or to publish content, respond with 'publish'.
|
||||
If the user requests to write or update a blog post, respond with 'not_publish'.
|
||||
|
||||
Here is the chat history:
|
||||
{chat_history}
|
||||
|
||||
The current user request is:
|
||||
{input}
|
||||
|
||||
Given the chat history and the new user request, decide whether to publish based on existing information.
|
||||
Decision (respond with either 'not_publish' or 'publish'):
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
chat_history_str = "\n".join(
|
||||
[f"{msg.role}: {msg.content}" for msg in chat_history]
|
||||
)
|
||||
prompt = prompt_template.format(chat_history=chat_history_str, input=input)
|
||||
|
||||
output = await Settings.llm.acomplete(prompt)
|
||||
decision = output.text.strip().lower()
|
||||
|
||||
return "publish" if decision == "publish" else "research"
|
||||
|
||||
@step()
|
||||
async def research(
|
||||
self, ctx: Context, ev: ResearchEvent, researcher: FunctionCallingAgent
|
||||
) -> WriteEvent:
|
||||
result: AgentRunResult = await self.run_agent(ctx, researcher, ev.input)
|
||||
content = result.response.message.content
|
||||
return WriteEvent(
|
||||
input=f"Write a blog post given this task: {ctx.data['task']} using this research content: {content}"
|
||||
)
|
||||
|
||||
@step()
|
||||
async def write(
|
||||
self, ctx: Context, ev: WriteEvent, writer: FunctionCallingAgent
|
||||
) -> ReviewEvent | StopEvent:
|
||||
MAX_ATTEMPTS = 2
|
||||
ctx.data["attempts"] = ctx.data.get("attempts", 0) + 1
|
||||
too_many_attempts = ctx.data["attempts"] > MAX_ATTEMPTS
|
||||
if too_many_attempts:
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name=writer.name,
|
||||
msg=f"Too many attempts ({MAX_ATTEMPTS}) to write the blog post. Proceeding with the current version.",
|
||||
)
|
||||
)
|
||||
if ev.is_good or too_many_attempts:
|
||||
# too many attempts or the blog post is good - stream final response if requested
|
||||
result = await self.run_agent(
|
||||
ctx,
|
||||
writer,
|
||||
f"Based on the reviewer's feedback, refine the post and return only the final version of the post. Here's the current version: {ev.input}",
|
||||
streaming=ctx.data["streaming"],
|
||||
)
|
||||
return StopEvent(result=result)
|
||||
result: AgentRunResult = await self.run_agent(ctx, writer, ev.input)
|
||||
ctx.data["result"] = result
|
||||
return ReviewEvent(input=result.response.message.content)
|
||||
|
||||
@step()
|
||||
async def review(
|
||||
self, ctx: Context, ev: ReviewEvent, reviewer: FunctionCallingAgent
|
||||
) -> WriteEvent:
|
||||
result: AgentRunResult = await self.run_agent(ctx, reviewer, ev.input)
|
||||
review = result.response.message.content
|
||||
old_content = ctx.data["result"].response.message.content
|
||||
post_is_good = "post is good" in review.lower()
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name=reviewer.name,
|
||||
msg=f"The post is {'not ' if not post_is_good else ''}good enough for publishing. Sending back to the writer{' for publication.' if post_is_good else '.'}",
|
||||
)
|
||||
)
|
||||
if post_is_good:
|
||||
return WriteEvent(
|
||||
input=f"You're blog post is ready for publication. Please respond with just the blog post. Blog post: ```{old_content}```",
|
||||
is_good=True,
|
||||
)
|
||||
else:
|
||||
return WriteEvent(
|
||||
input=dedent(
|
||||
f"""
|
||||
Improve the writing of a given blog post by using a given review.
|
||||
Blog post:
|
||||
```
|
||||
{old_content}
|
||||
```
|
||||
|
||||
Review:
|
||||
```
|
||||
{review}
|
||||
```
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
@step()
|
||||
async def publish(
|
||||
self,
|
||||
ctx: Context,
|
||||
ev: PublishEvent,
|
||||
publisher: FunctionCallingAgent,
|
||||
) -> StopEvent:
|
||||
try:
|
||||
result: AgentRunResult = await self.run_agent(ctx, publisher, ev.input)
|
||||
return StopEvent(result=result)
|
||||
except Exception as e:
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name=publisher.name,
|
||||
msg=f"Error publishing: {e}",
|
||||
)
|
||||
)
|
||||
return StopEvent(result=None)
|
||||
|
||||
async def run_agent(
|
||||
self,
|
||||
ctx: Context,
|
||||
agent: FunctionCallingAgent,
|
||||
input: str,
|
||||
streaming: bool = False,
|
||||
) -> AgentRunResult | AsyncGenerator:
|
||||
handler = agent.run(input=input, streaming=streaming)
|
||||
# bubble all events while running the executor to the planner
|
||||
async for event in handler.stream_events():
|
||||
# Don't write the StopEvent from sub task to the stream
|
||||
if type(event) is not StopEvent:
|
||||
ctx.write_event_to_stream(event)
|
||||
return await handler
|
||||
@@ -0,0 +1,41 @@
|
||||
import { StopEvent } from "@llamaindex/core/workflow";
|
||||
import { Message, streamToResponse } from "ai";
|
||||
import { Request, Response } from "express";
|
||||
import { ChatMessage, ChatResponseChunk } from "llamaindex";
|
||||
import { createWorkflow } from "./workflow/factory";
|
||||
import { toDataStream, workflowEventsToStreamData } from "./workflow/stream";
|
||||
|
||||
export const chat = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { messages }: { messages: Message[] } = req.body;
|
||||
const userMessage = messages.pop();
|
||||
if (!messages || !userMessage || userMessage.role !== "user") {
|
||||
return res.status(400).json({
|
||||
error:
|
||||
"messages are required in the request body and the last message must be from the user",
|
||||
});
|
||||
}
|
||||
|
||||
const chatHistory = messages as ChatMessage[];
|
||||
const agent = createWorkflow(chatHistory);
|
||||
const result = agent.run<AsyncGenerator<ChatResponseChunk>>(
|
||||
userMessage.content,
|
||||
) as unknown as Promise<StopEvent<AsyncGenerator<ChatResponseChunk>>>;
|
||||
|
||||
// convert the workflow events to a vercel AI stream data object
|
||||
const agentStreamData = await workflowEventsToStreamData(
|
||||
agent.streamEvents(),
|
||||
);
|
||||
// convert the workflow result to a vercel AI content stream
|
||||
const stream = toDataStream(result, {
|
||||
onFinal: () => agentStreamData.close(),
|
||||
});
|
||||
|
||||
return streamToResponse(stream, res, {}, agentStreamData);
|
||||
} catch (error) {
|
||||
console.error("[LlamaIndex]", error);
|
||||
return res.status(500).json({
|
||||
detail: (error as Error).message,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { initObservability } from "@/app/observability";
|
||||
import { StopEvent } from "@llamaindex/core/workflow";
|
||||
import { Message, StreamingTextResponse } from "ai";
|
||||
import { ChatMessage, ChatResponseChunk } from "llamaindex";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { initSettings } from "./engine/settings";
|
||||
import { createWorkflow } from "./workflow/factory";
|
||||
import { toDataStream, workflowEventsToStreamData } from "./workflow/stream";
|
||||
|
||||
initObservability();
|
||||
initSettings();
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { messages }: { messages: Message[] } = body;
|
||||
const userMessage = messages.pop();
|
||||
if (!messages || !userMessage || userMessage.role !== "user") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
"messages are required in the request body and the last message must be from the user",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const chatHistory = messages as ChatMessage[];
|
||||
const agent = createWorkflow(chatHistory);
|
||||
// TODO: fix type in agent.run in LITS
|
||||
const result = agent.run<AsyncGenerator<ChatResponseChunk>>(
|
||||
userMessage.content,
|
||||
) as unknown as Promise<StopEvent<AsyncGenerator<ChatResponseChunk>>>;
|
||||
// convert the workflow events to a vercel AI stream data object
|
||||
const agentStreamData = await workflowEventsToStreamData(
|
||||
agent.streamEvents(),
|
||||
);
|
||||
// convert the workflow result to a vercel AI content stream
|
||||
const stream = toDataStream(result, {
|
||||
onFinal: () => agentStreamData.close(),
|
||||
});
|
||||
return new StreamingTextResponse(stream, {}, agentStreamData);
|
||||
} catch (error) {
|
||||
console.error("[LlamaIndex]", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
detail: (error as Error).message,
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { ChatMessage } from "llamaindex";
|
||||
import { FunctionCallingAgent } from "./single-agent";
|
||||
import { lookupTools } from "./tools";
|
||||
|
||||
export const createResearcher = async (chatHistory: ChatMessage[]) => {
|
||||
const tools = await lookupTools([
|
||||
"query_index",
|
||||
"wikipedia_tool",
|
||||
"duckduckgo_search",
|
||||
"image_generator",
|
||||
]);
|
||||
|
||||
return new FunctionCallingAgent({
|
||||
name: "researcher",
|
||||
tools: tools,
|
||||
systemPrompt: `You are a researcher agent. You are given a research task.
|
||||
|
||||
If the conversation already includes the information and there is no new request for additional information from the user, you should return the appropriate content to the writer.
|
||||
Otherwise, you must use tools to retrieve information or images needed for the task.
|
||||
|
||||
It's normal for the task to include some ambiguity. You must always think carefully about the context of the user's request to understand what are the main content needs to be retrieved.
|
||||
Example:
|
||||
Request: "Create a blog post about the history of the internet, write in English and publish in PDF format."
|
||||
->Though: The main content is "history of the internet", while "write in English and publish in PDF format" is a requirement for other agents.
|
||||
Your task: Look for information in English about the history of the Internet.
|
||||
This is not your task: Create a blog post or look for how to create a PDF.
|
||||
|
||||
Next request: "Publish the blog post in HTML format."
|
||||
->Though: User just asking for a format change, the previous content is still valid.
|
||||
Your task: Return the previous content of the post to the writer. No need to do any research.
|
||||
This is not your task: Look for how to create an HTML file.
|
||||
|
||||
If you use the tools but don't find any related information, please return "I didn't find any new information for {the topic}." along with the content you found. Don't try to make up information yourself.
|
||||
If the request doesn't need any new information because it was in the conversation history, please return "The task doesn't need any new information. Please reuse the existing content in the conversation history.
|
||||
`,
|
||||
chatHistory,
|
||||
});
|
||||
};
|
||||
|
||||
export const createWriter = (chatHistory: ChatMessage[]) => {
|
||||
return new FunctionCallingAgent({
|
||||
name: "writer",
|
||||
systemPrompt: `You are an expert in writing blog posts.
|
||||
You are given the task of writing a blog post based on research content provided by the researcher agent. Do not invent any information yourself.
|
||||
It's important to read the entire conversation history to write the blog post accurately.
|
||||
If you receive a review from the reviewer, update the post according to the feedback and return the new post content.
|
||||
If the content is not valid (e.g., broken link, broken image, etc.), do not use it.
|
||||
It's normal for the task to include some ambiguity, so you must define the user's initial request to write the post correctly.
|
||||
If you update the post based on the reviewer's feedback, first explain what changes you made to the post, then provide the new post content. Do not include the reviewer's comments.
|
||||
Example:
|
||||
Task: "Here is the information I found about the history of the internet:
|
||||
Create a blog post about the history of the internet, write in English, and publish in PDF format."
|
||||
-> Your task: Use the research content {...} to write a blog post in English.
|
||||
-> This is not your task: Create a PDF
|
||||
Please note that a localhost link is acceptable, but dummy links like "example.com" or "your-website.com" are not valid.`,
|
||||
chatHistory,
|
||||
});
|
||||
};
|
||||
|
||||
export const createReviewer = (chatHistory: ChatMessage[]) => {
|
||||
return new FunctionCallingAgent({
|
||||
name: "reviewer",
|
||||
systemPrompt: `You are an expert in reviewing blog posts.
|
||||
You are given a task to review a blog post. As a reviewer, it's important that your review aligns with the user's request. Please focus on the user's request when reviewing the post.
|
||||
Review the post for logical inconsistencies, ask critical questions, and provide suggestions for improvement.
|
||||
Furthermore, proofread the post for grammar and spelling errors.
|
||||
Only if the post is good enough for publishing should you return 'The post is good.' In all other cases, return your review.
|
||||
It's normal for the task to include some ambiguity, so you must define the user's initial request to review the post correctly.
|
||||
Please note that a localhost link is acceptable, but dummy links like "example.com" or "your-website.com" are not valid.
|
||||
Example:
|
||||
Task: "Create a blog post about the history of the internet, write in English and publish in PDF format."
|
||||
-> Your task: Review whether the main content of the post is about the history of the internet and if it is written in English.
|
||||
-> This is not your task: Create blog post, create PDF, write in English.`,
|
||||
chatHistory,
|
||||
});
|
||||
};
|
||||
|
||||
export const createPublisher = async (chatHistory: ChatMessage[]) => {
|
||||
const tools = await lookupTools(["document_generator"]);
|
||||
let systemPrompt = `You are an expert in publishing blog posts. You are given a task to publish a blog post.
|
||||
If the writer says that there was an error, you should reply with the error and not publish the post.`;
|
||||
if (tools.length > 0) {
|
||||
systemPrompt = `${systemPrompt}.
|
||||
If the user requests to generate a file, use the document_generator tool to generate the file and reply with the link to the file.
|
||||
Otherwise, simply return the content of the post.`;
|
||||
}
|
||||
return new FunctionCallingAgent({
|
||||
name: "publisher",
|
||||
tools: tools,
|
||||
systemPrompt: systemPrompt,
|
||||
chatHistory,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,229 @@
|
||||
import {
|
||||
Context,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "@llamaindex/core/workflow";
|
||||
import { ChatMessage, ChatResponseChunk, Settings } from "llamaindex";
|
||||
import {
|
||||
createPublisher,
|
||||
createResearcher,
|
||||
createReviewer,
|
||||
createWriter,
|
||||
} from "./agents";
|
||||
import { AgentInput, AgentRunEvent } from "./type";
|
||||
|
||||
const TIMEOUT = 360 * 1000;
|
||||
const MAX_ATTEMPTS = 2;
|
||||
|
||||
class ResearchEvent extends WorkflowEvent<{ input: string }> {}
|
||||
class WriteEvent extends WorkflowEvent<{
|
||||
input: string;
|
||||
isGood: boolean;
|
||||
}> {}
|
||||
class ReviewEvent extends WorkflowEvent<{ input: string }> {}
|
||||
class PublishEvent extends WorkflowEvent<{ input: string }> {}
|
||||
|
||||
const prepareChatHistory = (chatHistory: ChatMessage[]) => {
|
||||
// By default, the chat history only contains the assistant and user messages
|
||||
// all the agents messages are stored in annotation data which is not visible to the LLM
|
||||
|
||||
const MAX_AGENT_MESSAGES = 10;
|
||||
|
||||
// Construct a new agent message from agent messages
|
||||
// Get annotations from assistant messages
|
||||
const agentAnnotations = chatHistory
|
||||
.filter((msg) => msg.role === "assistant")
|
||||
.flatMap((msg) => msg.annotations || [])
|
||||
.filter((annotation) => annotation.type === "agent")
|
||||
.slice(-MAX_AGENT_MESSAGES);
|
||||
|
||||
const agentMessages = agentAnnotations
|
||||
.map(
|
||||
(annotation) =>
|
||||
`\n<${annotation.data.agent}>\n${annotation.data.text}\n</${annotation.data.agent}>`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const agentContent = agentMessages
|
||||
? "Here is the previous conversation of agents:\n" + agentMessages
|
||||
: "";
|
||||
|
||||
if (agentContent) {
|
||||
const agentMessage: ChatMessage = {
|
||||
role: "assistant",
|
||||
content: agentContent,
|
||||
};
|
||||
return [
|
||||
...chatHistory.slice(0, -1),
|
||||
agentMessage,
|
||||
chatHistory.slice(-1)[0],
|
||||
];
|
||||
}
|
||||
return chatHistory;
|
||||
};
|
||||
|
||||
export const createWorkflow = (chatHistory: ChatMessage[]) => {
|
||||
const chatHistoryWithAgentMessages = prepareChatHistory(chatHistory);
|
||||
const runAgent = async (
|
||||
context: Context,
|
||||
agent: Workflow,
|
||||
input: AgentInput,
|
||||
) => {
|
||||
const run = agent.run(new StartEvent({ input }));
|
||||
for await (const event of agent.streamEvents()) {
|
||||
if (event.data instanceof AgentRunEvent) {
|
||||
context.writeEventToStream(event.data);
|
||||
}
|
||||
}
|
||||
return await run;
|
||||
};
|
||||
|
||||
const start = async (context: Context, ev: StartEvent) => {
|
||||
context.set("task", ev.data.input);
|
||||
|
||||
const chatHistoryStr = chatHistoryWithAgentMessages
|
||||
.map((msg) => `${msg.role}: ${msg.content}`)
|
||||
.join("\n");
|
||||
|
||||
// Decision-making process
|
||||
const decision = await decideWorkflow(ev.data.input, chatHistoryStr);
|
||||
|
||||
if (decision !== "publish") {
|
||||
return new ResearchEvent({
|
||||
input: `Research for this task: ${ev.data.input}`,
|
||||
});
|
||||
} else {
|
||||
return new PublishEvent({
|
||||
input: `Publish content based on the chat history\n${chatHistoryStr}\n\n and task: ${ev.data.input}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const decideWorkflow = async (task: string, chatHistoryStr: string) => {
|
||||
const llm = Settings.llm;
|
||||
|
||||
const prompt = `You are an expert in decision-making, helping people write and publish blog posts.
|
||||
If the user is asking for a file or to publish content, respond with 'publish'.
|
||||
If the user requests to write or update a blog post, respond with 'not_publish'.
|
||||
|
||||
Here is the chat history:
|
||||
${chatHistoryStr}
|
||||
|
||||
The current user request is:
|
||||
${task}
|
||||
|
||||
Given the chat history and the new user request, decide whether to publish based on existing information.
|
||||
Decision (respond with either 'not_publish' or 'publish'):`;
|
||||
|
||||
const output = await llm.complete({ prompt: prompt });
|
||||
const decision = output.text.trim().toLowerCase();
|
||||
return decision === "publish" ? "publish" : "research";
|
||||
};
|
||||
|
||||
const research = async (context: Context, ev: ResearchEvent) => {
|
||||
const researcher = await createResearcher(chatHistoryWithAgentMessages);
|
||||
const researchRes = await runAgent(context, researcher, {
|
||||
message: ev.data.input,
|
||||
});
|
||||
const researchResult = researchRes.data.result;
|
||||
return new WriteEvent({
|
||||
input: `Write a blog post given this task: ${context.get("task")} using this research content: ${researchResult}`,
|
||||
isGood: false,
|
||||
});
|
||||
};
|
||||
|
||||
const write = async (context: Context, ev: WriteEvent) => {
|
||||
const writer = createWriter(chatHistoryWithAgentMessages);
|
||||
|
||||
context.set("attempts", context.get("attempts", 0) + 1);
|
||||
const tooManyAttempts = context.get("attempts") > MAX_ATTEMPTS;
|
||||
if (tooManyAttempts) {
|
||||
context.writeEventToStream(
|
||||
new AgentRunEvent({
|
||||
name: "writer",
|
||||
msg: `Too many attempts (${MAX_ATTEMPTS}) to write the blog post. Proceeding with the current version.`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (ev.data.isGood || tooManyAttempts) {
|
||||
// the blog post is good or too many attempts
|
||||
// stream the final content
|
||||
const result = await runAgent(context, writer, {
|
||||
message: `Based on the reviewer's feedback, refine the post and return only the final version of the post. Here's the current version: ${ev.data.input}`,
|
||||
streaming: true,
|
||||
});
|
||||
return result as unknown as StopEvent<AsyncGenerator<ChatResponseChunk>>;
|
||||
}
|
||||
|
||||
const writeRes = await runAgent(context, writer, {
|
||||
message: ev.data.input,
|
||||
});
|
||||
const writeResult = writeRes.data.result;
|
||||
context.set("result", writeResult); // store the last result
|
||||
return new ReviewEvent({ input: writeResult });
|
||||
};
|
||||
|
||||
const review = async (context: Context, ev: ReviewEvent) => {
|
||||
const reviewer = createReviewer(chatHistoryWithAgentMessages);
|
||||
const reviewRes = await reviewer.run(
|
||||
new StartEvent<AgentInput>({ input: { message: ev.data.input } }),
|
||||
);
|
||||
const reviewResult = reviewRes.data.result;
|
||||
const oldContent = context.get("result");
|
||||
const postIsGood = reviewResult.toLowerCase().includes("post is good");
|
||||
context.writeEventToStream(
|
||||
new AgentRunEvent({
|
||||
name: "reviewer",
|
||||
msg: `The post is ${postIsGood ? "" : "not "}good enough for publishing. Sending back to the writer${
|
||||
postIsGood ? " for publication." : "."
|
||||
}`,
|
||||
}),
|
||||
);
|
||||
if (postIsGood) {
|
||||
return new WriteEvent({
|
||||
input: "",
|
||||
isGood: true,
|
||||
});
|
||||
}
|
||||
|
||||
return new WriteEvent({
|
||||
input: `Improve the writing of a given blog post by using a given review.
|
||||
Blog post:
|
||||
\`\`\`
|
||||
${oldContent}
|
||||
\`\`\`
|
||||
|
||||
Review:
|
||||
\`\`\`
|
||||
${reviewResult}
|
||||
\`\`\``,
|
||||
isGood: false,
|
||||
});
|
||||
};
|
||||
|
||||
const publish = async (context: Context, ev: PublishEvent) => {
|
||||
const publisher = await createPublisher(chatHistoryWithAgentMessages);
|
||||
|
||||
const publishResult = await runAgent(context, publisher, {
|
||||
message: `${ev.data.input}`,
|
||||
streaming: true,
|
||||
});
|
||||
return publishResult as unknown as StopEvent<
|
||||
AsyncGenerator<ChatResponseChunk>
|
||||
>;
|
||||
};
|
||||
|
||||
const workflow = new Workflow({ timeout: TIMEOUT, validate: true });
|
||||
workflow.addStep(StartEvent, start, {
|
||||
outputs: [ResearchEvent, PublishEvent],
|
||||
});
|
||||
workflow.addStep(ResearchEvent, research, { outputs: WriteEvent });
|
||||
workflow.addStep(WriteEvent, write, { outputs: [ReviewEvent, StopEvent] });
|
||||
workflow.addStep(ReviewEvent, review, { outputs: WriteEvent });
|
||||
workflow.addStep(PublishEvent, publish, { outputs: StopEvent });
|
||||
|
||||
return workflow;
|
||||
};
|
||||
@@ -0,0 +1,236 @@
|
||||
import {
|
||||
Context,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "@llamaindex/core/workflow";
|
||||
import {
|
||||
BaseToolWithCall,
|
||||
ChatMemoryBuffer,
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseChunk,
|
||||
Settings,
|
||||
ToolCall,
|
||||
ToolCallLLM,
|
||||
ToolCallLLMMessageOptions,
|
||||
callTool,
|
||||
} from "llamaindex";
|
||||
import { AgentInput, AgentRunEvent } from "./type";
|
||||
|
||||
class InputEvent extends WorkflowEvent<{
|
||||
input: ChatMessage[];
|
||||
}> {}
|
||||
|
||||
class ToolCallEvent extends WorkflowEvent<{
|
||||
toolCalls: ToolCall[];
|
||||
}> {}
|
||||
|
||||
export class FunctionCallingAgent extends Workflow {
|
||||
name: string;
|
||||
llm: ToolCallLLM;
|
||||
memory: ChatMemoryBuffer;
|
||||
tools: BaseToolWithCall[];
|
||||
systemPrompt?: string;
|
||||
writeEvents: boolean;
|
||||
role?: string;
|
||||
|
||||
constructor(options: {
|
||||
name: string;
|
||||
llm?: ToolCallLLM;
|
||||
chatHistory?: ChatMessage[];
|
||||
tools?: BaseToolWithCall[];
|
||||
systemPrompt?: string;
|
||||
writeEvents?: boolean;
|
||||
role?: string;
|
||||
verbose?: boolean;
|
||||
timeout?: number;
|
||||
}) {
|
||||
super({
|
||||
verbose: options?.verbose ?? false,
|
||||
timeout: options?.timeout ?? 360,
|
||||
});
|
||||
this.name = options?.name;
|
||||
this.llm = options.llm ?? (Settings.llm as ToolCallLLM);
|
||||
this.checkToolCallSupport();
|
||||
this.memory = new ChatMemoryBuffer({
|
||||
llm: this.llm,
|
||||
chatHistory: options.chatHistory,
|
||||
});
|
||||
this.tools = options?.tools ?? [];
|
||||
this.systemPrompt = options.systemPrompt;
|
||||
this.writeEvents = options?.writeEvents ?? true;
|
||||
this.role = options?.role;
|
||||
|
||||
// add steps
|
||||
this.addStep(StartEvent<AgentInput>, this.prepareChatHistory, {
|
||||
outputs: InputEvent,
|
||||
});
|
||||
this.addStep(InputEvent, this.handleLLMInput, {
|
||||
outputs: [ToolCallEvent, StopEvent],
|
||||
});
|
||||
this.addStep(ToolCallEvent, this.handleToolCalls, {
|
||||
outputs: InputEvent,
|
||||
});
|
||||
}
|
||||
|
||||
private get chatHistory() {
|
||||
return this.memory.getMessages();
|
||||
}
|
||||
|
||||
private async prepareChatHistory(
|
||||
ctx: Context,
|
||||
ev: StartEvent<AgentInput>,
|
||||
): Promise<InputEvent> {
|
||||
const { message, streaming } = ev.data.input;
|
||||
ctx.set("streaming", streaming);
|
||||
this.writeEvent(`Start to work on: ${message}`, ctx);
|
||||
if (this.systemPrompt) {
|
||||
this.memory.put({ role: "system", content: this.systemPrompt });
|
||||
}
|
||||
this.memory.put({ role: "user", content: message });
|
||||
return new InputEvent({ input: this.chatHistory });
|
||||
}
|
||||
|
||||
private async handleLLMInput(
|
||||
ctx: Context,
|
||||
ev: InputEvent,
|
||||
): Promise<StopEvent<string | AsyncGenerator> | ToolCallEvent> {
|
||||
if (ctx.get("streaming")) {
|
||||
return await this.handleLLMInputStream(ctx, ev);
|
||||
}
|
||||
|
||||
const result = await this.llm.chat({
|
||||
messages: this.chatHistory,
|
||||
tools: this.tools,
|
||||
});
|
||||
this.memory.put(result.message);
|
||||
|
||||
const toolCalls = this.getToolCallsFromResponse(result);
|
||||
if (toolCalls.length) {
|
||||
return new ToolCallEvent({ toolCalls });
|
||||
}
|
||||
this.writeEvent("Finished task", ctx);
|
||||
return new StopEvent({ result: result.message.content.toString() });
|
||||
}
|
||||
|
||||
private async handleLLMInputStream(
|
||||
context: Context,
|
||||
ev: InputEvent,
|
||||
): Promise<StopEvent<AsyncGenerator> | ToolCallEvent> {
|
||||
const { llm, tools, memory } = this;
|
||||
const llmArgs = { messages: this.chatHistory, tools };
|
||||
|
||||
const responseGenerator = async function* () {
|
||||
const responseStream = await llm.chat({ ...llmArgs, stream: true });
|
||||
|
||||
let fullResponse = null;
|
||||
let yieldedIndicator = false;
|
||||
for await (const chunk of responseStream) {
|
||||
const hasToolCalls = chunk.options && "toolCall" in chunk.options;
|
||||
if (!hasToolCalls) {
|
||||
if (!yieldedIndicator) {
|
||||
yield false;
|
||||
yieldedIndicator = true;
|
||||
}
|
||||
yield chunk;
|
||||
} else if (!yieldedIndicator) {
|
||||
yield true;
|
||||
yieldedIndicator = true;
|
||||
}
|
||||
|
||||
fullResponse = chunk;
|
||||
}
|
||||
|
||||
if (fullResponse) {
|
||||
memory.put({
|
||||
role: "assistant",
|
||||
content: "",
|
||||
options: fullResponse.options,
|
||||
});
|
||||
yield fullResponse;
|
||||
}
|
||||
};
|
||||
|
||||
const generator = responseGenerator();
|
||||
const isToolCall = await generator.next();
|
||||
if (isToolCall.value) {
|
||||
const fullResponse = await generator.next();
|
||||
const toolCalls = this.getToolCallsFromResponse(
|
||||
fullResponse.value as ChatResponseChunk<ToolCallLLMMessageOptions>,
|
||||
);
|
||||
return new ToolCallEvent({ toolCalls });
|
||||
}
|
||||
|
||||
this.writeEvent("Finished task", context);
|
||||
return new StopEvent({ result: generator });
|
||||
}
|
||||
|
||||
private async handleToolCalls(
|
||||
ctx: Context,
|
||||
ev: ToolCallEvent,
|
||||
): Promise<InputEvent> {
|
||||
const { toolCalls } = ev.data;
|
||||
|
||||
const toolMsgs: ChatMessage[] = [];
|
||||
|
||||
for (const call of toolCalls) {
|
||||
const targetTool = this.tools.find(
|
||||
(tool) => tool.metadata.name === call.name,
|
||||
);
|
||||
// TODO: make logger optional in callTool in framework
|
||||
const toolOutput = await callTool(targetTool, call, {
|
||||
log: () => {},
|
||||
error: console.error.bind(console),
|
||||
warn: () => {},
|
||||
});
|
||||
toolMsgs.push({
|
||||
content: JSON.stringify(toolOutput.output),
|
||||
role: "user",
|
||||
options: {
|
||||
toolResult: {
|
||||
result: toolOutput.output,
|
||||
isError: toolOutput.isError,
|
||||
id: call.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const msg of toolMsgs) {
|
||||
this.memory.put(msg);
|
||||
}
|
||||
|
||||
return new InputEvent({ input: this.memory.getMessages() });
|
||||
}
|
||||
|
||||
private writeEvent(msg: string, context: Context) {
|
||||
if (!this.writeEvents) return;
|
||||
context.writeEventToStream({
|
||||
data: new AgentRunEvent({ name: this.name, msg }),
|
||||
});
|
||||
}
|
||||
|
||||
private checkToolCallSupport() {
|
||||
const { supportToolCall } = this.llm as ToolCallLLM;
|
||||
if (!supportToolCall) throw new Error("LLM does not support tool calls");
|
||||
}
|
||||
|
||||
private getToolCallsFromResponse(
|
||||
response:
|
||||
| ChatResponse<ToolCallLLMMessageOptions>
|
||||
| ChatResponseChunk<ToolCallLLMMessageOptions>,
|
||||
): ToolCall[] {
|
||||
let options;
|
||||
if ("message" in response) {
|
||||
options = response.message.options;
|
||||
} else {
|
||||
options = response.options;
|
||||
}
|
||||
if (options && "toolCall" in options) {
|
||||
return options.toolCall as ToolCall[];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { StopEvent } from "@llamaindex/core/workflow";
|
||||
import {
|
||||
createCallbacksTransformer,
|
||||
createStreamDataTransformer,
|
||||
StreamData,
|
||||
trimStartOfStreamHelper,
|
||||
type AIStreamCallbacksAndOptions,
|
||||
} from "ai";
|
||||
import { ChatResponseChunk } from "llamaindex";
|
||||
import { AgentRunEvent } from "./type";
|
||||
|
||||
export function toDataStream(
|
||||
result: Promise<StopEvent<AsyncGenerator<ChatResponseChunk>>>,
|
||||
callbacks?: AIStreamCallbacksAndOptions,
|
||||
) {
|
||||
return toReadableStream(result)
|
||||
.pipeThrough(createCallbacksTransformer(callbacks))
|
||||
.pipeThrough(createStreamDataTransformer());
|
||||
}
|
||||
|
||||
function toReadableStream(
|
||||
result: Promise<StopEvent<AsyncGenerator<ChatResponseChunk>>>,
|
||||
) {
|
||||
const trimStartOfStream = trimStartOfStreamHelper();
|
||||
return new ReadableStream<string>({
|
||||
start(controller) {
|
||||
controller.enqueue(""); // Kickstart the stream
|
||||
},
|
||||
async pull(controller): Promise<void> {
|
||||
const stopEvent = await result;
|
||||
const generator = stopEvent.data.result;
|
||||
const { value, done } = await generator.next();
|
||||
if (done) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const text = trimStartOfStream(value.delta ?? "");
|
||||
if (text) controller.enqueue(text);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function workflowEventsToStreamData(
|
||||
events: AsyncIterable<AgentRunEvent>,
|
||||
): Promise<StreamData> {
|
||||
const streamData = new StreamData();
|
||||
|
||||
(async () => {
|
||||
for await (const event of events) {
|
||||
if (event instanceof AgentRunEvent) {
|
||||
const { name, msg } = event.data;
|
||||
if ((streamData as any).isClosed) {
|
||||
break;
|
||||
}
|
||||
streamData.appendMessageAnnotation({
|
||||
type: "agent",
|
||||
data: { agent: name, text: msg },
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return streamData;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import fs from "fs/promises";
|
||||
import { BaseToolWithCall, QueryEngineTool } from "llamaindex";
|
||||
import path from "path";
|
||||
import { getDataSource } from "../engine";
|
||||
import { createTools } from "../engine/tools/index";
|
||||
|
||||
const getQueryEngineTool = async (): Promise<QueryEngineTool | null> => {
|
||||
const index = await getDataSource();
|
||||
if (!index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const topK = process.env.TOP_K ? parseInt(process.env.TOP_K) : undefined;
|
||||
return new QueryEngineTool({
|
||||
queryEngine: index.asQueryEngine({
|
||||
similarityTopK: topK,
|
||||
}),
|
||||
metadata: {
|
||||
name: "query_index",
|
||||
description: `Use this tool to retrieve information about the text corpus from the index.`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getAvailableTools = async () => {
|
||||
const configFile = path.join("config", "tools.json");
|
||||
let toolConfig: any;
|
||||
const tools: BaseToolWithCall[] = [];
|
||||
try {
|
||||
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 queryEngineTool = await getQueryEngineTool();
|
||||
if (queryEngineTool) {
|
||||
tools.push(queryEngineTool);
|
||||
}
|
||||
|
||||
return tools;
|
||||
};
|
||||
|
||||
export const lookupTools = async (
|
||||
toolNames: string[],
|
||||
): Promise<BaseToolWithCall[]> => {
|
||||
const availableTools = await getAvailableTools();
|
||||
return availableTools.filter((tool) =>
|
||||
toolNames.includes(tool.metadata.name),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { WorkflowEvent } from "@llamaindex/core/workflow";
|
||||
|
||||
export type AgentInput = {
|
||||
message: string;
|
||||
streaming?: boolean;
|
||||
};
|
||||
|
||||
export class AgentRunEvent extends WorkflowEvent<{
|
||||
name: string;
|
||||
msg: string;
|
||||
}> {}
|
||||
@@ -0,0 +1,158 @@
|
||||
# Copyright 2024 FoundryLabs, Inc. and LlamaIndex, Inc.
|
||||
# Portions of this file are copied from the e2b project (https://github.com/e2b-dev/ai-artifacts) and then converted to Python
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from app.engine.tools.artifact import CodeArtifact
|
||||
from app.engine.utils.file_helper import save_file
|
||||
from e2b_code_interpreter import CodeInterpreter, Sandbox
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
sandbox_router = APIRouter()
|
||||
|
||||
SANDBOX_TIMEOUT = 10 * 60 # timeout in seconds
|
||||
MAX_DURATION = 60 # max duration in seconds
|
||||
|
||||
|
||||
class ExecutionResult(BaseModel):
|
||||
template: str
|
||||
stdout: List[str]
|
||||
stderr: List[str]
|
||||
runtime_error: Optional[Dict[str, Union[str, List[str]]]] = None
|
||||
output_urls: List[Dict[str, str]]
|
||||
url: Optional[str]
|
||||
|
||||
def to_response(self):
|
||||
"""
|
||||
Convert the execution result to a response object (camelCase)
|
||||
"""
|
||||
return {
|
||||
"template": self.template,
|
||||
"stdout": self.stdout,
|
||||
"stderr": self.stderr,
|
||||
"runtimeError": self.runtime_error,
|
||||
"outputUrls": self.output_urls,
|
||||
"url": self.url,
|
||||
}
|
||||
|
||||
|
||||
@sandbox_router.post("")
|
||||
async def create_sandbox(request: Request):
|
||||
request_data = await request.json()
|
||||
|
||||
try:
|
||||
artifact = CodeArtifact(**request_data["artifact"])
|
||||
except Exception:
|
||||
logger.error(f"Could not create artifact from request data: {request_data}")
|
||||
return HTTPException(
|
||||
status_code=400, detail="Could not create artifact from the request data"
|
||||
)
|
||||
|
||||
sbx = None
|
||||
|
||||
# Create an interpreter or a sandbox
|
||||
if artifact.template == "code-interpreter-multilang":
|
||||
sbx = CodeInterpreter(api_key=os.getenv("E2B_API_KEY"), timeout=SANDBOX_TIMEOUT)
|
||||
logger.debug(f"Created code interpreter {sbx}")
|
||||
else:
|
||||
sbx = Sandbox(
|
||||
api_key=os.getenv("E2B_API_KEY"),
|
||||
template=artifact.template,
|
||||
metadata={"template": artifact.template, "user_id": "default"},
|
||||
timeout=SANDBOX_TIMEOUT,
|
||||
)
|
||||
logger.debug(f"Created sandbox {sbx}")
|
||||
|
||||
# Install packages
|
||||
if artifact.has_additional_dependencies:
|
||||
if isinstance(sbx, CodeInterpreter):
|
||||
sbx.notebook.exec_cell(artifact.install_dependencies_command)
|
||||
logger.debug(
|
||||
f"Installed dependencies: {', '.join(artifact.additional_dependencies)} in code interpreter {sbx}"
|
||||
)
|
||||
elif isinstance(sbx, Sandbox):
|
||||
sbx.commands.run(artifact.install_dependencies_command)
|
||||
logger.debug(
|
||||
f"Installed dependencies: {', '.join(artifact.additional_dependencies)} in sandbox {sbx}"
|
||||
)
|
||||
|
||||
# Copy code to disk
|
||||
if isinstance(artifact.code, list):
|
||||
for file in artifact.code:
|
||||
sbx.files.write(file.file_path, file.file_content)
|
||||
logger.debug(f"Copied file to {file.file_path}")
|
||||
else:
|
||||
sbx.files.write(artifact.file_path, artifact.code)
|
||||
logger.debug(f"Copied file to {artifact.file_path}")
|
||||
|
||||
# Execute code or return a URL to the running sandbox
|
||||
if artifact.template == "code-interpreter-multilang":
|
||||
result = sbx.notebook.exec_cell(artifact.code or "")
|
||||
output_urls = _download_cell_results(result.results)
|
||||
return ExecutionResult(
|
||||
template=artifact.template,
|
||||
stdout=result.logs.stdout,
|
||||
stderr=result.logs.stderr,
|
||||
runtime_error=result.error,
|
||||
output_urls=output_urls,
|
||||
url=None,
|
||||
).to_response()
|
||||
else:
|
||||
return ExecutionResult(
|
||||
template=artifact.template,
|
||||
stdout=[],
|
||||
stderr=[],
|
||||
runtime_error=None,
|
||||
output_urls=[],
|
||||
url=f"https://{sbx.get_host(artifact.port or 80)}",
|
||||
).to_response()
|
||||
|
||||
|
||||
def _download_cell_results(cell_results: Optional[List]) -> List[Dict[str, str]]:
|
||||
"""
|
||||
To pull results from code interpreter cell and save them to disk for serving
|
||||
"""
|
||||
if not cell_results:
|
||||
return []
|
||||
|
||||
output = []
|
||||
for result in cell_results:
|
||||
try:
|
||||
formats = result.formats()
|
||||
for ext in formats:
|
||||
data = result[ext]
|
||||
|
||||
if ext in ["png", "svg", "jpeg", "pdf"]:
|
||||
file_path = f"output/tools/{uuid.uuid4()}.{ext}"
|
||||
base64_data = data
|
||||
buffer = base64.b64decode(base64_data)
|
||||
file_meta = save_file(content=buffer, file_path=file_path)
|
||||
output.append(
|
||||
{
|
||||
"type": ext,
|
||||
"filename": file_meta.filename,
|
||||
"url": file_meta.url,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing result: {str(e)}")
|
||||
|
||||
return output
|
||||
@@ -3,7 +3,7 @@ import mimetypes
|
||||
import os
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Tuple
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
from llama_index.core import VectorStoreIndex
|
||||
@@ -72,7 +72,12 @@ class PrivateFileService:
|
||||
return documents
|
||||
|
||||
@staticmethod
|
||||
def process_file(file_name: str, base64_content: str, params: Any) -> List[str]:
|
||||
def process_file(
|
||||
file_name: str, base64_content: str, params: Optional[dict] = None
|
||||
) -> List[str]:
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
file_data, extension = PrivateFileService.preprocess_base64_file(base64_content)
|
||||
|
||||
# Add the nodes to the index and persist it
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from llama_index.embeddings.openai import OpenAIEmbedding
|
||||
from llama_index.core.settings import Settings
|
||||
from typing import Dict
|
||||
import logging
|
||||
import os
|
||||
from typing import Dict
|
||||
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.embeddings.openai import OpenAIEmbedding
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_MODEL = "gpt-3.5-turbo"
|
||||
DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large"
|
||||
@@ -50,7 +54,11 @@ def embedding_config_from_env() -> Dict:
|
||||
|
||||
|
||||
def init_llmhub():
|
||||
from llama_index.llms.openai_like import OpenAILike
|
||||
try:
|
||||
from llama_index.llms.openai_like import OpenAILike
|
||||
except ImportError:
|
||||
logger.error("Failed to import OpenAILike. Make sure llama_index is installed.")
|
||||
raise
|
||||
|
||||
llm_configs = llm_config_from_env()
|
||||
embedding_configs = embedding_config_from_env()
|
||||
|
||||
@@ -33,8 +33,13 @@ def init_settings():
|
||||
|
||||
|
||||
def init_ollama():
|
||||
from llama_index.embeddings.ollama import OllamaEmbedding
|
||||
from llama_index.llms.ollama.base import DEFAULT_REQUEST_TIMEOUT, Ollama
|
||||
try:
|
||||
from llama_index.embeddings.ollama import OllamaEmbedding
|
||||
from llama_index.llms.ollama.base import DEFAULT_REQUEST_TIMEOUT, Ollama
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Ollama support is not installed. Please install it with `poetry add llama-index-llms-ollama` and `poetry add llama-index-embeddings-ollama`"
|
||||
)
|
||||
|
||||
base_url = os.getenv("OLLAMA_BASE_URL") or "http://127.0.0.1:11434"
|
||||
request_timeout = float(
|
||||
@@ -55,25 +60,29 @@ def init_openai():
|
||||
from llama_index.llms.openai import OpenAI
|
||||
|
||||
max_tokens = os.getenv("LLM_MAX_TOKENS")
|
||||
config = {
|
||||
"model": os.getenv("MODEL"),
|
||||
"temperature": float(os.getenv("LLM_TEMPERATURE", DEFAULT_TEMPERATURE)),
|
||||
"max_tokens": int(max_tokens) if max_tokens is not None else None,
|
||||
}
|
||||
Settings.llm = OpenAI(**config)
|
||||
Settings.llm = OpenAI(
|
||||
model=os.getenv("MODEL", "gpt-4o-mini"),
|
||||
temperature=float(os.getenv("LLM_TEMPERATURE", DEFAULT_TEMPERATURE)),
|
||||
max_tokens=int(max_tokens) if max_tokens is not None else None,
|
||||
)
|
||||
|
||||
dimensions = os.getenv("EMBEDDING_DIM")
|
||||
config = {
|
||||
"model": os.getenv("EMBEDDING_MODEL"),
|
||||
"dimensions": int(dimensions) if dimensions is not None else None,
|
||||
}
|
||||
Settings.embed_model = OpenAIEmbedding(**config)
|
||||
Settings.embed_model = OpenAIEmbedding(
|
||||
model=os.getenv("EMBEDDING_MODEL", "text-embedding-3-small"),
|
||||
dimensions=int(dimensions) if dimensions is not None else None,
|
||||
)
|
||||
|
||||
|
||||
def init_azure_openai():
|
||||
from llama_index.core.constants import DEFAULT_TEMPERATURE
|
||||
from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding
|
||||
from llama_index.llms.azure_openai import AzureOpenAI
|
||||
|
||||
try:
|
||||
from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding
|
||||
from llama_index.llms.azure_openai import AzureOpenAI
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Azure OpenAI support is not installed. Please install it with `poetry add llama-index-llms-azure-openai` and `poetry add llama-index-embeddings-azure-openai`"
|
||||
)
|
||||
|
||||
llm_deployment = os.environ["AZURE_OPENAI_LLM_DEPLOYMENT"]
|
||||
embedding_deployment = os.environ["AZURE_OPENAI_EMBEDDING_DEPLOYMENT"]
|
||||
@@ -105,40 +114,50 @@ def init_azure_openai():
|
||||
|
||||
|
||||
def init_fastembed():
|
||||
"""
|
||||
Use Qdrant Fastembed as the local embedding provider.
|
||||
"""
|
||||
from llama_index.embeddings.fastembed import FastEmbedEmbedding
|
||||
try:
|
||||
from llama_index.embeddings.fastembed import FastEmbedEmbedding
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"FastEmbed support is not installed. Please install it with `poetry add llama-index-embeddings-fastembed`"
|
||||
)
|
||||
|
||||
embed_model_map: Dict[str, str] = {
|
||||
# Small and multilingual
|
||||
"all-MiniLM-L6-v2": "sentence-transformers/all-MiniLM-L6-v2",
|
||||
# Large and multilingual
|
||||
"paraphrase-multilingual-mpnet-base-v2": "sentence-transformers/paraphrase-multilingual-mpnet-base-v2", # noqa: E501
|
||||
"paraphrase-multilingual-mpnet-base-v2": "sentence-transformers/paraphrase-multilingual-mpnet-base-v2",
|
||||
}
|
||||
|
||||
embedding_model = os.getenv("EMBEDDING_MODEL")
|
||||
if embedding_model is None:
|
||||
raise ValueError("EMBEDDING_MODEL environment variable is not set")
|
||||
|
||||
# This will download the model automatically if it is not already downloaded
|
||||
Settings.embed_model = FastEmbedEmbedding(
|
||||
model_name=embed_model_map[os.getenv("EMBEDDING_MODEL")]
|
||||
model_name=embed_model_map[embedding_model]
|
||||
)
|
||||
|
||||
|
||||
def init_groq():
|
||||
from llama_index.llms.groq import Groq
|
||||
try:
|
||||
from llama_index.llms.groq import Groq
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Groq support is not installed. Please install it with `poetry add llama-index-llms-groq`"
|
||||
)
|
||||
|
||||
model_map: Dict[str, str] = {
|
||||
"llama3-8b": "llama3-8b-8192",
|
||||
"llama3-70b": "llama3-70b-8192",
|
||||
"mixtral-8x7b": "mixtral-8x7b-32768",
|
||||
}
|
||||
|
||||
Settings.llm = Groq(model=model_map[os.getenv("MODEL")])
|
||||
Settings.llm = Groq(model=os.getenv("MODEL"))
|
||||
# Groq does not provide embeddings, so we use FastEmbed instead
|
||||
init_fastembed()
|
||||
|
||||
|
||||
def init_anthropic():
|
||||
from llama_index.llms.anthropic import Anthropic
|
||||
try:
|
||||
from llama_index.llms.anthropic import Anthropic
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Anthropic support is not installed. Please install it with `poetry add llama-index-llms-anthropic`"
|
||||
)
|
||||
|
||||
model_map: Dict[str, str] = {
|
||||
"claude-3-opus": "claude-3-opus-20240229",
|
||||
@@ -154,8 +173,13 @@ def init_anthropic():
|
||||
|
||||
|
||||
def init_gemini():
|
||||
from llama_index.embeddings.gemini import GeminiEmbedding
|
||||
from llama_index.llms.gemini import Gemini
|
||||
try:
|
||||
from llama_index.embeddings.gemini import GeminiEmbedding
|
||||
from llama_index.llms.gemini import Gemini
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Gemini support is not installed. Please install it with `poetry add llama-index-llms-gemini` and `poetry add llama-index-embeddings-gemini`"
|
||||
)
|
||||
|
||||
model_name = f"models/{os.getenv('MODEL')}"
|
||||
embed_model_name = f"models/{os.getenv('EMBEDDING_MODEL')}"
|
||||
|
||||
+1
-7
@@ -138,14 +138,8 @@ function initGroq() {
|
||||
"all-mpnet-base-v2": "Xenova/all-mpnet-base-v2",
|
||||
};
|
||||
|
||||
const modelMap: Record<string, string> = {
|
||||
"llama3-8b": "llama3-8b-8192",
|
||||
"llama3-70b": "llama3-70b-8192",
|
||||
"mixtral-8x7b": "mixtral-8x7b-32768",
|
||||
};
|
||||
|
||||
Settings.llm = new Groq({
|
||||
model: modelMap[process.env.MODEL!],
|
||||
model: process.env.MODEL!,
|
||||
});
|
||||
|
||||
Settings.embedModel = new HuggingFaceEmbedding({
|
||||
@@ -15,6 +15,6 @@ def get_vector_store():
|
||||
token=token,
|
||||
api_endpoint=endpoint,
|
||||
collection_name=collection,
|
||||
embedding_dimension=int(os.getenv("EMBEDDING_DIM")),
|
||||
embedding_dimension=int(os.getenv("EMBEDDING_DIM", 768)),
|
||||
)
|
||||
return store
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
|
||||
from llama_index.vector_stores.chroma import ChromaVectorStore
|
||||
|
||||
|
||||
@@ -18,7 +19,7 @@ def get_vector_store():
|
||||
)
|
||||
store = ChromaVectorStore.from_params(
|
||||
host=os.getenv("CHROMA_HOST"),
|
||||
port=int(os.getenv("CHROMA_PORT")),
|
||||
port=os.getenv("CHROMA_PORT", "8001"),
|
||||
collection_name=collection_name,
|
||||
)
|
||||
return store
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
# flake8: noqa: E402
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from app.engine.index import get_index
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from llama_cloud import PipelineType
|
||||
|
||||
from app.settings import init_settings
|
||||
from llama_index.core.settings import Settings
|
||||
|
||||
|
||||
from app.engine.index import get_client, get_index
|
||||
|
||||
import logging
|
||||
from llama_index.core.readers import SimpleDirectoryReader
|
||||
from app.engine.service import LLamaCloudFileService
|
||||
@@ -13,10 +20,49 @@ logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger()
|
||||
|
||||
|
||||
def ensure_index(index):
|
||||
project_id = index._get_project_id()
|
||||
client = get_client()
|
||||
pipelines = client.pipelines.search_pipelines(
|
||||
project_id=project_id,
|
||||
pipeline_name=index.name,
|
||||
pipeline_type=PipelineType.MANAGED.value,
|
||||
)
|
||||
if len(pipelines) == 0:
|
||||
from llama_index.embeddings.openai import OpenAIEmbedding
|
||||
|
||||
if not isinstance(Settings.embed_model, OpenAIEmbedding):
|
||||
raise ValueError(
|
||||
"Creating a new pipeline with a non-OpenAI embedding model is not supported."
|
||||
)
|
||||
client.pipelines.upsert_pipeline(
|
||||
project_id=project_id,
|
||||
request={
|
||||
"name": index.name,
|
||||
"embedding_config": {
|
||||
"type": "OPENAI_EMBEDDING",
|
||||
"component": {
|
||||
"api_key": os.getenv("OPENAI_API_KEY"), # editable
|
||||
"model_name": os.getenv("EMBEDDING_MODEL"),
|
||||
},
|
||||
},
|
||||
"transform_config": {
|
||||
"mode": "auto",
|
||||
"config": {
|
||||
"chunk_size": Settings.chunk_size, # editable
|
||||
"chunk_overlap": Settings.chunk_overlap, # editable
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def generate_datasource():
|
||||
init_settings()
|
||||
logger.info("Generate index for the provided data")
|
||||
|
||||
index = get_index()
|
||||
ensure_index(index)
|
||||
project_id = index._get_project_id()
|
||||
pipeline_id = index._get_pipeline_id()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from llama_index.core.ingestion.api_utils import (
|
||||
get_client as llama_cloud_get_client,
|
||||
)
|
||||
from llama_index.indices.managed.llama_cloud import LlamaCloudIndex
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
@@ -15,31 +15,39 @@ logger = logging.getLogger("uvicorn")
|
||||
class LlamaCloudConfig(BaseModel):
|
||||
# Private attributes
|
||||
api_key: str = Field(
|
||||
default=os.getenv("LLAMA_CLOUD_API_KEY"),
|
||||
exclude=True, # Exclude from the model representation
|
||||
)
|
||||
base_url: Optional[str] = Field(
|
||||
default=os.getenv("LLAMA_CLOUD_BASE_URL"),
|
||||
exclude=True,
|
||||
)
|
||||
organization_id: Optional[str] = Field(
|
||||
default=os.getenv("LLAMA_CLOUD_ORGANIZATION_ID"),
|
||||
exclude=True,
|
||||
)
|
||||
# Configuration attributes, can be set by the user
|
||||
pipeline: str = Field(
|
||||
description="The name of the pipeline to use",
|
||||
default=os.getenv("LLAMA_CLOUD_INDEX_NAME"),
|
||||
)
|
||||
project: str = Field(
|
||||
description="The name of the LlamaCloud project",
|
||||
default=os.getenv("LLAMA_CLOUD_PROJECT_NAME"),
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if "api_key" not in kwargs:
|
||||
kwargs["api_key"] = os.getenv("LLAMA_CLOUD_API_KEY")
|
||||
if "base_url" not in kwargs:
|
||||
kwargs["base_url"] = os.getenv("LLAMA_CLOUD_BASE_URL")
|
||||
if "organization_id" not in kwargs:
|
||||
kwargs["organization_id"] = os.getenv("LLAMA_CLOUD_ORGANIZATION_ID")
|
||||
if "pipeline" not in kwargs:
|
||||
kwargs["pipeline"] = os.getenv("LLAMA_CLOUD_INDEX_NAME")
|
||||
if "project" not in kwargs:
|
||||
kwargs["project"] = os.getenv("LLAMA_CLOUD_PROJECT_NAME")
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Validate and throw error if the env variables are not set before starting the app
|
||||
@validator("pipeline", "project", "api_key", pre=True, always=True)
|
||||
@field_validator("pipeline", "project", "api_key", mode="before")
|
||||
@classmethod
|
||||
def validate_env_vars(cls, value):
|
||||
def validate_fields(cls, value):
|
||||
if value is None:
|
||||
raise ValueError(
|
||||
"Please set LLAMA_CLOUD_INDEX_NAME, LLAMA_CLOUD_PROJECT_NAME and LLAMA_CLOUD_API_KEY"
|
||||
@@ -56,7 +64,7 @@ class LlamaCloudConfig(BaseModel):
|
||||
|
||||
class IndexConfig(BaseModel):
|
||||
llama_cloud_pipeline_config: LlamaCloudConfig = Field(
|
||||
default=LlamaCloudConfig(),
|
||||
default_factory=lambda: LlamaCloudConfig(),
|
||||
alias="llamaCloudPipeline",
|
||||
)
|
||||
callback_manager: Optional[CallbackManager] = Field(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
|
||||
from llama_index.vector_stores.milvus import MilvusVectorStore
|
||||
|
||||
|
||||
@@ -15,6 +16,6 @@ def get_vector_store():
|
||||
user=os.getenv("MILVUS_USERNAME"),
|
||||
password=os.getenv("MILVUS_PASSWORD"),
|
||||
collection_name=collection,
|
||||
dim=int(os.getenv("EMBEDDING_DIM")),
|
||||
dim=int(os.getenv("EMBEDDING_DIM", 768)),
|
||||
)
|
||||
return store
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
|
||||
from cachetools import TTLCache, cached
|
||||
from cachetools import TTLCache, cached # type: ignore
|
||||
from llama_index.core.callbacks import CallbackManager
|
||||
from llama_index.core.indices import load_index_from_storage
|
||||
from llama_index.core.storage import StorageContext
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import * as dotenv from "dotenv";
|
||||
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
|
||||
import { AstraDBVectorStore } from "llamaindex/storage/vectorStore/AstraDBVectorStore";
|
||||
import { AstraDBVectorStore } from "llamaindex/vector-store/AstraDBVectorStore";
|
||||
import { getDocuments } from "./loader";
|
||||
import { initSettings } from "./settings";
|
||||
import { checkRequiredEnvVars } from "./shared";
|
||||
@@ -15,13 +15,12 @@ async function loadAndIndex() {
|
||||
// create vector store and a collection
|
||||
const collectionName = process.env.ASTRA_DB_COLLECTION!;
|
||||
const vectorStore = new AstraDBVectorStore();
|
||||
await vectorStore.create(collectionName, {
|
||||
await vectorStore.createAndConnect(collectionName, {
|
||||
vector: {
|
||||
dimension: parseInt(process.env.EMBEDDING_DIM!),
|
||||
metric: "cosine",
|
||||
},
|
||||
});
|
||||
await vectorStore.connect(collectionName);
|
||||
|
||||
// create index from documents and store them in Astra
|
||||
console.log("Start creating embeddings...");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import { VectorStoreIndex } from "llamaindex";
|
||||
import { AstraDBVectorStore } from "llamaindex/storage/vectorStore/AstraDBVectorStore";
|
||||
import { AstraDBVectorStore } from "llamaindex/vector-store/AstraDBVectorStore";
|
||||
import { checkRequiredEnvVars } from "./shared";
|
||||
|
||||
export async function getDataSource(params?: any) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import * as dotenv from "dotenv";
|
||||
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
|
||||
import { ChromaVectorStore } from "llamaindex/storage/vectorStore/ChromaVectorStore";
|
||||
import { ChromaVectorStore } from "llamaindex/vector-store/ChromaVectorStore";
|
||||
import { getDocuments } from "./loader";
|
||||
import { initSettings } from "./settings";
|
||||
import { checkRequiredEnvVars } from "./shared";
|
||||
@@ -16,7 +16,7 @@ async function loadAndIndex() {
|
||||
const chromaUri = `http://${process.env.CHROMA_HOST}:${process.env.CHROMA_PORT}`;
|
||||
|
||||
const vectorStore = new ChromaVectorStore({
|
||||
collectionName: process.env.CHROMA_COLLECTION,
|
||||
collectionName: process.env.CHROMA_COLLECTION!,
|
||||
chromaClientParams: { path: chromaUri },
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import { VectorStoreIndex } from "llamaindex";
|
||||
import { ChromaVectorStore } from "llamaindex/storage/vectorStore/ChromaVectorStore";
|
||||
import { ChromaVectorStore } from "llamaindex/vector-store/ChromaVectorStore";
|
||||
import { checkRequiredEnvVars } from "./shared";
|
||||
|
||||
export async function getDataSource(params?: any) {
|
||||
@@ -8,7 +8,7 @@ export async function getDataSource(params?: any) {
|
||||
const chromaUri = `http://${process.env.CHROMA_HOST}:${process.env.CHROMA_PORT}`;
|
||||
|
||||
const store = new ChromaVectorStore({
|
||||
collectionName: process.env.CHROMA_COLLECTION,
|
||||
collectionName: process.env.CHROMA_COLLECTION!,
|
||||
chromaClientParams: { path: chromaUri },
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { MetadataFilter, MetadataFilters } from "llamaindex";
|
||||
import { CloudRetrieveParams, MetadataFilter } from "llamaindex";
|
||||
|
||||
export function generateFilters(documentIds: string[]): MetadataFilters {
|
||||
export function generateFilters(documentIds: string[]) {
|
||||
// public documents don't have the "private" field or it's set to "false"
|
||||
const publicDocumentsFilter: MetadataFilter = {
|
||||
key: "private",
|
||||
value: null,
|
||||
operator: "is_empty",
|
||||
};
|
||||
|
||||
// if no documentIds are provided, only retrieve information from public documents
|
||||
if (!documentIds.length) return { filters: [publicDocumentsFilter] };
|
||||
if (!documentIds.length)
|
||||
return {
|
||||
filters: [publicDocumentsFilter],
|
||||
} as CloudRetrieveParams["filters"];
|
||||
|
||||
const privateDocumentsFilter: MetadataFilter = {
|
||||
key: "file_id", // Note: LLamaCloud uses "file_id" to reference private document ids as "doc_id" is a restricted field in LlamaCloud
|
||||
@@ -21,5 +23,5 @@ export function generateFilters(documentIds: string[]): MetadataFilters {
|
||||
return {
|
||||
filters: [publicDocumentsFilter, privateDocumentsFilter],
|
||||
condition: "or",
|
||||
};
|
||||
} as CloudRetrieveParams["filters"];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import * as dotenv from "dotenv";
|
||||
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
|
||||
import { MilvusVectorStore } from "llamaindex/storage/vectorStore/MilvusVectorStore";
|
||||
import { MilvusVectorStore } from "llamaindex/vector-store/MilvusVectorStore";
|
||||
import { getDocuments } from "./loader";
|
||||
import { initSettings } from "./settings";
|
||||
import { checkRequiredEnvVars, getMilvusClient } from "./shared";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { VectorStoreIndex } from "llamaindex";
|
||||
import { MilvusVectorStore } from "llamaindex/storage/vectorStore/MilvusVectorStore";
|
||||
import { MilvusVectorStore } from "llamaindex/vector-store/MilvusVectorStore";
|
||||
import { checkRequiredEnvVars, getMilvusClient } from "./shared";
|
||||
|
||||
export async function getDataSource(params?: any) {
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import * as dotenv from "dotenv";
|
||||
import {
|
||||
MongoDBAtlasVectorSearch,
|
||||
VectorStoreIndex,
|
||||
storageContextFromDefaults,
|
||||
} from "llamaindex";
|
||||
import { storageContextFromDefaults, VectorStoreIndex } from "llamaindex";
|
||||
import { MongoDBAtlasVectorSearch } from "llamaindex/vector-store/MongoDBAtlasVectorStore";
|
||||
import { MongoClient } from "mongodb";
|
||||
import { getDocuments } from "./loader";
|
||||
import { initSettings } from "./settings";
|
||||
import { checkRequiredEnvVars } from "./shared";
|
||||
import { checkRequiredEnvVars, POPULATED_METADATA_FIELDS } from "./shared";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -30,6 +27,12 @@ async function loadAndIndex() {
|
||||
dbName: databaseName,
|
||||
collectionName: vectorCollectionName, // this is where your embeddings will be stored
|
||||
indexName: indexName, // this is the name of the index you will need to create
|
||||
indexedMetadataFields: POPULATED_METADATA_FIELDS,
|
||||
embeddingDefinition: {
|
||||
dimensions: process.env.EMBEDDING_DIM
|
||||
? parseInt(process.env.EMBEDDING_DIM)
|
||||
: 1536,
|
||||
},
|
||||
});
|
||||
|
||||
// now create an index from all the Documents and store them in Atlas
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import { MongoDBAtlasVectorSearch, VectorStoreIndex } from "llamaindex";
|
||||
import { VectorStoreIndex } from "llamaindex";
|
||||
import { MongoDBAtlasVectorSearch } from "llamaindex/vector-store/MongoDBAtlasVectorStore";
|
||||
import { MongoClient } from "mongodb";
|
||||
import { checkRequiredEnvVars } from "./shared";
|
||||
import { checkRequiredEnvVars, POPULATED_METADATA_FIELDS } from "./shared";
|
||||
|
||||
export async function getDataSource(params?: any) {
|
||||
checkRequiredEnvVars();
|
||||
const client = new MongoClient(process.env.MONGO_URI!);
|
||||
const client = new MongoClient(process.env.MONGODB_URI!);
|
||||
const store = new MongoDBAtlasVectorSearch({
|
||||
mongodbClient: client,
|
||||
dbName: process.env.MONGODB_DATABASE!,
|
||||
collectionName: process.env.MONGODB_VECTORS!,
|
||||
indexName: process.env.MONGODB_VECTOR_INDEX,
|
||||
indexedMetadataFields: POPULATED_METADATA_FIELDS,
|
||||
embeddingDefinition: {
|
||||
dimensions: process.env.EMBEDDING_DIM
|
||||
? parseInt(process.env.EMBEDDING_DIM)
|
||||
: 1536,
|
||||
},
|
||||
});
|
||||
|
||||
return await VectorStoreIndex.fromVectorStore(store);
|
||||
|
||||
@@ -5,6 +5,8 @@ const REQUIRED_ENV_VARS = [
|
||||
"MONGODB_VECTOR_INDEX",
|
||||
];
|
||||
|
||||
export const POPULATED_METADATA_FIELDS = ["private", "doc_id"]; // for filtering in MongoDB VectorSearchIndex
|
||||
|
||||
export function checkRequiredEnvVars() {
|
||||
const missingEnvVars = REQUIRED_ENV_VARS.filter((envVar) => {
|
||||
return !process.env[envVar];
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import * as dotenv from "dotenv";
|
||||
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
|
||||
import { PGVectorStore } from "llamaindex/storage/vectorStore/PGVectorStore";
|
||||
import { PGVectorStore } from "llamaindex/vector-store/PGVectorStore";
|
||||
import { getDocuments } from "./loader";
|
||||
import { initSettings } from "./settings";
|
||||
import {
|
||||
@@ -19,7 +18,9 @@ async function loadAndIndex() {
|
||||
|
||||
// create postgres vector store
|
||||
const vectorStore = new PGVectorStore({
|
||||
connectionString: process.env.PG_CONNECTION_STRING,
|
||||
clientConfig: {
|
||||
connectionString: process.env.PG_CONNECTION_STRING,
|
||||
},
|
||||
schemaName: PGVECTOR_SCHEMA,
|
||||
tableName: PGVECTOR_TABLE,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import { VectorStoreIndex } from "llamaindex";
|
||||
import { PGVectorStore } from "llamaindex/storage/vectorStore/PGVectorStore";
|
||||
import { PGVectorStore } from "llamaindex/vector-store/PGVectorStore";
|
||||
import {
|
||||
PGVECTOR_SCHEMA,
|
||||
PGVECTOR_TABLE,
|
||||
@@ -10,7 +9,9 @@ import {
|
||||
export async function getDataSource(params?: any) {
|
||||
checkRequiredEnvVars();
|
||||
const pgvs = new PGVectorStore({
|
||||
connectionString: process.env.PG_CONNECTION_STRING,
|
||||
clientConfig: {
|
||||
connectionString: process.env.PG_CONNECTION_STRING,
|
||||
},
|
||||
schemaName: PGVECTOR_SCHEMA,
|
||||
tableName: PGVECTOR_TABLE,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import * as dotenv from "dotenv";
|
||||
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
|
||||
import { PineconeVectorStore } from "llamaindex/storage/vectorStore/PineconeVectorStore";
|
||||
import { PineconeVectorStore } from "llamaindex/vector-store/PineconeVectorStore";
|
||||
import { getDocuments } from "./loader";
|
||||
import { initSettings } from "./settings";
|
||||
import { checkRequiredEnvVars } from "./shared";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import { VectorStoreIndex } from "llamaindex";
|
||||
import { PineconeVectorStore } from "llamaindex/storage/vectorStore/PineconeVectorStore";
|
||||
import { PineconeVectorStore } from "llamaindex/vector-store/PineconeVectorStore";
|
||||
import { checkRequiredEnvVars } from "./shared";
|
||||
|
||||
export async function getDataSource(params?: any) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import * as dotenv from "dotenv";
|
||||
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
|
||||
import { QdrantVectorStore } from "llamaindex/storage/vectorStore/QdrantVectorStore";
|
||||
import { QdrantVectorStore } from "llamaindex/vector-store/QdrantVectorStore";
|
||||
import { getDocuments } from "./loader";
|
||||
import { initSettings } from "./settings";
|
||||
import { checkRequiredEnvVars, getQdrantClient } from "./shared";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as dotenv from "dotenv";
|
||||
import { VectorStoreIndex } from "llamaindex";
|
||||
import { QdrantVectorStore } from "llamaindex/storage/vectorStore/QdrantVectorStore";
|
||||
import { QdrantVectorStore } from "llamaindex/vector-store/QdrantVectorStore";
|
||||
import { checkRequiredEnvVars, getQdrantClient } from "./shared";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as dotenv from "dotenv";
|
||||
import { VectorStoreIndex } from "llamaindex";
|
||||
import { WeaviateVectorStore } from "llamaindex/storage/vectorStore/WeaviateVectorStore";
|
||||
import { WeaviateVectorStore } from "llamaindex/vector-store/WeaviateVectorStore";
|
||||
import { checkRequiredEnvVars, DEFAULT_INDEX_NAME } from "./shared";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -6,7 +6,7 @@ load_dotenv()
|
||||
import logging
|
||||
import os
|
||||
|
||||
from llama_index.core.ingestion import IngestionPipeline
|
||||
from llama_index.core.ingestion import DocstoreStrategy, IngestionPipeline
|
||||
from llama_index.core.node_parser import SentenceSplitter
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.core.storage import StorageContext
|
||||
@@ -41,7 +41,7 @@ def run_pipeline(docstore, vector_store, documents):
|
||||
Settings.embed_model,
|
||||
],
|
||||
docstore=docstore,
|
||||
docstore_strategy="upserts_and_delete",
|
||||
docstore_strategy=DocstoreStrategy.UPSERTS_AND_DELETE, # type: ignore
|
||||
vector_store=vector_store,
|
||||
)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class IndexConfig(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
def get_index(config: IndexConfig = None):
|
||||
def get_index(config: Optional[IndexConfig] = None) -> VectorStoreIndex:
|
||||
if config is None:
|
||||
config = IndexConfig()
|
||||
logger.info("Connecting vector store...")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user