mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-19 23:13:36 -04:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e5486c75d | |||
| 35e2f4ff10 | |||
| eb55161ad5 | |||
| 000e1836a5 | |||
| 86cc257ae7 | |||
| ee0c607bf3 | |||
| 5d6479cb8f | |||
| 4c0f3e266f | |||
| 4ef29aa9b1 | |||
| 5e6fc380c2 | |||
| 07d3c31eea | |||
| dd1c275dae | |||
| 10aacd28b3 | |||
| c843f16370 | |||
| 872ec84f0a | |||
| 31a4d295aa | |||
| d136d9ebcf | |||
| 3a8da926a9 | |||
| abe35e73b3 | |||
| e3c0061a42 | |||
| a2016b772e | |||
| 81709d2ac8 | |||
| d1bcee56dd | |||
| 736be7fd94 | |||
| e826a223e7 | |||
| 5f7e7661fd | |||
| 52b4d59c96 | |||
| 9172fed2e8 | |||
| 78ccde78fc | |||
| 02510703d8 | |||
| ed59927bd0 | |||
| 9f866aa981 | |||
| b8f78612b8 | |||
| 4a8346900d | |||
| 42e63842d0 | |||
| fa803787e3 | |||
| c5559d8e59 | |||
| 0182368744 | |||
| ff46bd6153 | |||
| 2209409cdb | |||
| 623f8b811b | |||
| 384a1368dd | |||
| 189c0e3f6c | |||
| 99b8247bc9 | |||
| 74c5a15450 | |||
| 9293e330ac | |||
| 6d1b6b9372 | |||
| a8162a9269 | |||
| f3577c50d6 | |||
| a5f5c9dc9c | |||
| 2be68d1c7f | |||
| 8c80cc05ce | |||
| dfd4fd58ab | |||
| 0a69fe09fa | |||
| de88b32208 | |||
| ef88bff211 | |||
| 7562cb48d6 | |||
| 9dde6d0288 | |||
| 98a82b0b25 | |||
| 7db72b6f2e | |||
| 3d41488301 | |||
| 1ee05eaf4b | |||
| 75e1f6104c | |||
| 88220f1dd2 | |||
| 6304114ef5 |
@@ -2,4 +2,4 @@
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
bump: use LlamaIndexTS 0.6.18
|
||||
feat: bump LITS 0.8.2
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Optimize generated workflow code for Python
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
feat: use llamaindex chat-ui for nextjs frontend
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Fix using LlamaCloud selector does not use the configured values in the environment (Python)
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
python-version: ["3.11"]
|
||||
os: [macos-latest, windows-latest, ubuntu-22.04]
|
||||
frameworks: ["nextjs", "express"]
|
||||
datasources: ["--no-files", "--example-file"]
|
||||
datasources: ["--no-files", "--example-file", "--llamacloud"]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
@@ -51,3 +51,7 @@ e2e/cache
|
||||
|
||||
# build artifacts
|
||||
create-llama-*.tgz
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
!.vscode/settings.json
|
||||
|
||||
@@ -1,5 +1,93 @@
|
||||
# create-llama
|
||||
|
||||
## 0.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ed59927: Add form filling use case (Python)
|
||||
|
||||
## 0.3.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4a83469: Add multi-agent financial report for Typescript (and update LITS to 0.7.10)
|
||||
|
||||
## 0.3.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fa80378: DocumentInfo working with relative URLs
|
||||
|
||||
## 0.3.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0182368: Fix the streaming issue to prevent the UI from hanging.
|
||||
|
||||
## 0.3.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2209409: Add financial report as the default use case in the multi-agent template (Python).
|
||||
|
||||
## 0.3.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 384a136: Fix import error if the artifact tool is selected
|
||||
|
||||
## 0.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 99b8247: Simplify and unify handling file uploads
|
||||
|
||||
## 0.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6d1b6b9: Update README.md for pro mode
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f3577c5: Fix event streaming is blocked
|
||||
- f3577c5: Add upload file to sandbox (artifact and code interpreter)
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 7562cb4: Simplified default questions and added pro mode
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0a69fe0: fix: missing params when init Astra vectorstore
|
||||
- 98a82b0: docs: chroma env variables
|
||||
|
||||
## 0.2.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3d41488: feat: use selected llamacloud for multiagent
|
||||
|
||||
## 0.2.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 75e1f61: Fix cannot query public document from llamacloud
|
||||
- 88220f1: fix workflow doesn't stop when user presses stop generation button
|
||||
- 75e1f61: Fix typescript templates cannot upload file to llamacloud
|
||||
- 88220f1: Bump llama_index@0.11.17
|
||||
|
||||
## 0.2.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- cd3fcd0: bump: use LlamaIndexTS 0.6.18
|
||||
- 6335de1: Fix using LlamaCloud selector does not use the configured values in the environment (Python)
|
||||
|
||||
## 0.2.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -12,7 +12,7 @@ npx create-llama@latest
|
||||
|
||||
to get started, or watch this video for a demo session:
|
||||
|
||||
https://github.com/user-attachments/assets/dd3edc36-4453-4416-91c2-d24326c6c167
|
||||
<img src="https://github.com/user-attachments/assets/c4a7fe18-8e30-498a-96f8-78127dd706b9" width="100%">
|
||||
|
||||
Once your app is generated, run
|
||||
|
||||
@@ -24,14 +24,14 @@ to start the development server. You can then visit [http://localhost:3000](http
|
||||
|
||||
## What you'll get
|
||||
|
||||
- A set of pre-configured use cases to get you started, e.g. Agentic RAG, Data Analysis, Report Generation, etc.
|
||||
- A Next.js-powered front-end using components from [shadcn/ui](https://ui.shadcn.com/). The app is set up as a chat interface that can answer questions about your data or interact with your agent
|
||||
- Your choice of 3 back-ends:
|
||||
- Your choice of two back-ends:
|
||||
- **Next.js**: if you select this option, you’ll have a full-stack Next.js application that you can deploy to a host like [Vercel](https://vercel.com/) in just a few clicks. This uses [LlamaIndex.TS](https://www.npmjs.com/package/llamaindex), our TypeScript library.
|
||||
- **Express**: if you want a more traditional Node.js application you can generate an Express backend. This also uses LlamaIndex.TS.
|
||||
- **Python FastAPI**: if you select this option, you’ll get a backend powered by the [llama-index Python package](https://pypi.org/project/llama-index/), which you can deploy to a service like Render or fly.io.
|
||||
- The back-end has two endpoints (one streaming, the other one non-streaming) that allow you to send the state of your chat and receive additional responses
|
||||
- You add arbitrary data sources to your chat, like local files, websites, or data retrieved from a database.
|
||||
- Turn your chat into an AI agent by adding tools (functions called by the LLM).
|
||||
- **Python FastAPI**: if you select this option, you’ll get a separate backend powered by the [llama-index Python package](https://pypi.org/project/llama-index/), which you can deploy to a service like [Render](https://render.com/) or [fly.io](https://fly.io/). The separate Next.js front-end will connect to this backend.
|
||||
- Each back-end has two endpoints:
|
||||
- One streaming chat endpoint, that allow you to send the state of your chat and receive additional responses
|
||||
- One endpoint to upload private files which can be used in your chat
|
||||
- The app uses OpenAI by default, so you'll need an OpenAI API key, or you can customize it to use any of the dozens of LLMs we support.
|
||||
|
||||
Here's how it looks like:
|
||||
@@ -40,9 +40,9 @@ https://github.com/user-attachments/assets/d57af1a1-d99b-4e9c-98d9-4cbd1327eff8
|
||||
|
||||
## Using your data
|
||||
|
||||
You can supply your own data; the app will index it and answer questions. Your generated app will have a folder called `data` (If you're using Express or Python and generate a frontend, it will be `./backend/data`).
|
||||
Optionally, you can supply your own data; the app will index it and make use of it, e.g. to answer questions. Your generated app will have a folder called `data` (If you're using Express or Python and generate a frontend, it will be `./backend/data`).
|
||||
|
||||
The app will ingest any supported files you put in this directory. Your Next.js and Express apps use LlamaIndex.TS so they will be able to ingest any PDF, text, CSV, Markdown, Word and HTML files. The Python backend can read even more types, including video and audio files.
|
||||
The app will ingest any supported files you put in this directory. Your Next.js and Express apps use LlamaIndex.TS, so they will be able to ingest any PDF, text, CSV, Markdown, Word and HTML files. The Python backend can read even more types, including video and audio files.
|
||||
|
||||
Before you can use your data, you need to index it. If you're using the Next.js or Express apps, run:
|
||||
|
||||
@@ -58,10 +58,6 @@ If you're using the Python backend, you can trigger indexing of your data by cal
|
||||
poetry run generate
|
||||
```
|
||||
|
||||
## Want a front-end?
|
||||
|
||||
Optionally generate a frontend if you've selected the Python or Express back-ends. If you do so, `create-llama` will generate two folders: `frontend`, for your Next.js-based frontend code, and `backend` containing your API.
|
||||
|
||||
## Customizing the AI models
|
||||
|
||||
The app will default to OpenAI's `gpt-4o-mini` LLM and `text-embedding-3-large` embedding model.
|
||||
@@ -94,46 +90,40 @@ Need to install the following packages:
|
||||
create-llama@latest
|
||||
Ok to proceed? (y) y
|
||||
✔ What is your project named? … my-app
|
||||
✔ Which template would you like to use? › Agentic RAG (e.g. chat with docs)
|
||||
✔ Which framework would you like to use? › NextJS
|
||||
✔ Would you like to set up observability? › No
|
||||
✔ What app do you want to build? › Agentic RAG
|
||||
✔ What language do you want to use? › Python (FastAPI)
|
||||
✔ Do you want to use LlamaCloud services? … No / Yes
|
||||
✔ Please provide your LlamaCloud API key (leave blank to skip): …
|
||||
✔ Please provide your OpenAI API key (leave blank to skip): …
|
||||
✔ Which data source would you like to use? › Use an example PDF
|
||||
✔ Would you like to add another data source? › No
|
||||
✔ Would you like to use LlamaParse (improved parser for RAG - requires API key)? … no / yes
|
||||
✔ Would you like to use a vector database? › No, just store the data in the file system
|
||||
✔ Would you like to build an agent using tools? If so, select the tools here, otherwise just press enter › Weather
|
||||
? How would you like to proceed? › - Use arrow-keys. Return to submit.
|
||||
Just generate code (~1 sec)
|
||||
❯ Start in VSCode (~1 sec)
|
||||
Generate code and install dependencies (~2 min)
|
||||
Generate code, install dependencies, and run the app (~2 min)
|
||||
Just generate code (~1 sec)
|
||||
❯ Start in VSCode (~1 sec)
|
||||
Generate code and install dependencies (~2 min)
|
||||
```
|
||||
|
||||
### Running non-interactively
|
||||
|
||||
You can also pass command line arguments to set up a new project
|
||||
non-interactively. See `create-llama --help`:
|
||||
non-interactively. For a list of the latest options, call `create-llama --help`.
|
||||
|
||||
```bash
|
||||
create-llama <project-directory> [options]
|
||||
### Running in pro mode
|
||||
|
||||
Options:
|
||||
-V, --version output the version number
|
||||
If you prefer more advanced customization options, you can run `create-llama` in pro mode using the `--pro` flag.
|
||||
|
||||
--use-npm
|
||||
In pro mode, instead of selecting a predefined use case, you'll be prompted to select each technical component of your project. This allows for greater flexibility in customizing your project, including:
|
||||
|
||||
Explicitly tell the CLI to bootstrap the app using npm
|
||||
- **Vector Store**: Choose from a variety of vector stores for keeping your documents, including MongoDB, Pinecone, Weaviate, Qdrant and Chroma.
|
||||
- **Tools**: Choose from a variety of agent tools (functions called by the LLM), such as:
|
||||
- Code Interpreter: Executes Python code in a secure Jupyter notebook environment
|
||||
- Artifact Code Generator: Generates code artifacts that can be run in a sandbox
|
||||
- OpenAPI Action: Facilitates requests to a provided OpenAPI schema
|
||||
- Image Generator: Creates images based on text descriptions
|
||||
- Web Search: Performs web searches to retrieve up-to-date information
|
||||
- **Data Sources**: Integrate various data sources into your chat application, including local files, websites, or database-retrieved data.
|
||||
- **Backend Options**: Besides using Next.js or FastAPI, you can also select to use Express for a more traditional Node.js application.
|
||||
- **Observability**: Choose from a variety of LLM observability tools, including LlamaTrace and Traceloop.
|
||||
|
||||
--use-pnpm
|
||||
|
||||
Explicitly tell the CLI to bootstrap the app using pnpm
|
||||
|
||||
--use-yarn
|
||||
|
||||
Explicitly tell the CLI to bootstrap the app using Yarn
|
||||
|
||||
```
|
||||
Pro mode is ideal for developers who want fine-grained control over their project's configuration and are comfortable with more technical setup options.
|
||||
|
||||
## LlamaIndex Documentation
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ export async function createApp({
|
||||
tools,
|
||||
useLlamaParse,
|
||||
observability,
|
||||
agents,
|
||||
}: InstallAppArgs): Promise<void> {
|
||||
const root = path.resolve(appPath);
|
||||
|
||||
@@ -86,6 +87,7 @@ export async function createApp({
|
||||
tools,
|
||||
useLlamaParse,
|
||||
observability,
|
||||
agents,
|
||||
};
|
||||
|
||||
if (frontend) {
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
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);
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"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
@@ -1,398 +0,0 @@
|
||||
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: {}
|
||||
@@ -18,68 +18,80 @@ const templateUI: TemplateUI = "shadcn";
|
||||
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
|
||||
const appType: AppType = templateFramework === "nextjs" ? "" : "--frontend";
|
||||
const userMessage = "Write a blog post about physical standards for letters";
|
||||
const templateAgents = ["financial_report", "blog", "form_filling"];
|
||||
|
||||
test.describe(`Test multiagent template ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
|
||||
test.skip(
|
||||
process.platform !== "linux" || process.env.DATASOURCE === "--no-files",
|
||||
"The multiagent template currently only works with files. We also only run on Linux to speed up tests.",
|
||||
);
|
||||
let port: number;
|
||||
let externalPort: number;
|
||||
let cwd: string;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
// Only test without using vector db for now
|
||||
const vectorDb = "none";
|
||||
|
||||
test.beforeAll(async () => {
|
||||
port = Math.floor(Math.random() * 10000) + 10000;
|
||||
externalPort = port + 1;
|
||||
cwd = await createTestDir();
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
templateType: "multiagent",
|
||||
templateFramework,
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
externalPort,
|
||||
postInstallAction: templatePostInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
});
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
});
|
||||
|
||||
test("App folder should exist", async () => {
|
||||
const dirExists = fs.existsSync(path.join(cwd, name));
|
||||
expect(dirExists).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Frontend should have a title", async ({ page }) => {
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Frontend should be able to submit a message and receive the start of a streamed response", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await page.fill("form textarea", userMessage);
|
||||
|
||||
const responsePromise = page.waitForResponse((res) =>
|
||||
res.url().includes("/api/chat"),
|
||||
for (const agents of templateAgents) {
|
||||
test.describe(`Test multiagent template ${agents} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
|
||||
test.skip(
|
||||
process.platform !== "linux" || process.env.DATASOURCE === "--no-files",
|
||||
"The multiagent template currently only works with files. We also only run on Linux to speed up tests.",
|
||||
);
|
||||
test.skip(
|
||||
agents === "form_filling" && templateFramework !== "fastapi",
|
||||
"Form filling is currently only supported with FastAPI.",
|
||||
);
|
||||
let port: number;
|
||||
let externalPort: number;
|
||||
let cwd: string;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
// Only test without using vector db for now
|
||||
const vectorDb = "none";
|
||||
|
||||
await page.click("form button[type=submit]");
|
||||
test.beforeAll(async () => {
|
||||
port = Math.floor(Math.random() * 10000) + 10000;
|
||||
externalPort = port + 1;
|
||||
cwd = await createTestDir();
|
||||
const result = await runCreateLlama({
|
||||
cwd,
|
||||
templateType: "multiagent",
|
||||
templateFramework,
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
externalPort,
|
||||
postInstallAction: templatePostInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
agents,
|
||||
});
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
});
|
||||
|
||||
const response = await responsePromise;
|
||||
expect(response.ok()).toBeTruthy();
|
||||
test("App folder should exist", async () => {
|
||||
const dirExists = fs.existsSync(path.join(cwd, name));
|
||||
expect(dirExists).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Frontend should have a title", async ({ page }) => {
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Frontend should be able to submit a message and receive the start of a streamed response", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.skip(
|
||||
agents === "financial_report" || agents === "form_filling",
|
||||
"Skip chat tests for financial report and form filling.",
|
||||
);
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await page.fill("form textarea", userMessage);
|
||||
|
||||
const responsePromise = page.waitForResponse((res) =>
|
||||
res.url().includes("/api/chat"),
|
||||
);
|
||||
|
||||
await page.click("form button[type=submit]");
|
||||
|
||||
const response = await responsePromise;
|
||||
expect(response.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
// clean processes
|
||||
test.afterAll(async () => {
|
||||
appProcess?.kill();
|
||||
});
|
||||
});
|
||||
|
||||
// clean processes
|
||||
test.afterAll(async () => {
|
||||
appProcess?.kill();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,6 +27,13 @@ const userMessage =
|
||||
dataSource !== "--no-files" ? "Physical standard for letters" : "Hello";
|
||||
|
||||
test.describe(`Test streaming template ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
|
||||
const isNode18 = process.version.startsWith("v18");
|
||||
const isLlamaCloud = dataSource === "--llamacloud";
|
||||
// llamacloud is using File API which is not supported on node 18
|
||||
if (isNode18 && isLlamaCloud) {
|
||||
test.skip(true, "Skipping tests for Node 18 and LlamaCloud data source");
|
||||
}
|
||||
|
||||
let port: number;
|
||||
let externalPort: number;
|
||||
let cwd: string;
|
||||
|
||||
@@ -34,6 +34,7 @@ export type RunCreateLlamaOptions = {
|
||||
tools?: string;
|
||||
useLlamaParse?: boolean;
|
||||
observability?: string;
|
||||
agents?: string;
|
||||
};
|
||||
|
||||
export async function runCreateLlama({
|
||||
@@ -52,6 +53,7 @@ export async function runCreateLlama({
|
||||
tools,
|
||||
useLlamaParse,
|
||||
observability,
|
||||
agents,
|
||||
}: RunCreateLlamaOptions): Promise<CreateLlamaResult> {
|
||||
if (!process.env.OPENAI_API_KEY || !process.env.LLAMA_CLOUD_API_KEY) {
|
||||
throw new Error(
|
||||
@@ -119,6 +121,9 @@ export async function runCreateLlama({
|
||||
if (observability) {
|
||||
commandArgs.push("--observability", observability);
|
||||
}
|
||||
if (templateType === "multiagent" && agents) {
|
||||
commandArgs.push("--agents", agents);
|
||||
}
|
||||
|
||||
const command = commandArgs.join(" ");
|
||||
console.log(`running command '${command}' in ${cwd}`);
|
||||
|
||||
@@ -11,6 +11,25 @@ export const EXAMPLE_FILE: TemplateDataSource = {
|
||||
},
|
||||
};
|
||||
|
||||
export const EXAMPLE_10K_SEC_FILES: TemplateDataSource[] = [
|
||||
{
|
||||
type: "file",
|
||||
config: {
|
||||
url: new URL(
|
||||
"https://s2.q4cdn.com/470004039/files/doc_earnings/2023/q4/filing/_10-K-Q4-2023-As-Filed.pdf",
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "file",
|
||||
config: {
|
||||
url: new URL(
|
||||
"https://ir.tesla.com/_flysystem/s3/sec/000162828024002390/tsla-20231231-gen.pdf",
|
||||
),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function getDataSources(
|
||||
files?: string,
|
||||
exampleFile?: boolean,
|
||||
|
||||
@@ -182,11 +182,11 @@ const getVectorDBEnvs = (
|
||||
},
|
||||
{
|
||||
name: "CHROMA_HOST",
|
||||
description: "The API endpoint for your Chroma database",
|
||||
description: "The hostname for your Chroma database. Eg: localhost",
|
||||
},
|
||||
{
|
||||
name: "CHROMA_PORT",
|
||||
description: "The port for your Chroma database",
|
||||
description: "The port for your Chroma database. Eg: 8000",
|
||||
},
|
||||
];
|
||||
// TS Version doesn't support config local storage path
|
||||
|
||||
+28
-6
@@ -96,6 +96,12 @@ async function generateContextData(
|
||||
}
|
||||
}
|
||||
|
||||
const downloadFile = async (url: string, destPath: string) => {
|
||||
const response = await fetch(url);
|
||||
const fileBuffer = await response.arrayBuffer();
|
||||
await fsExtra.writeFile(destPath, Buffer.from(fileBuffer));
|
||||
};
|
||||
|
||||
const prepareContextData = async (
|
||||
root: string,
|
||||
dataSources: TemplateDataSource[],
|
||||
@@ -103,12 +109,28 @@ const prepareContextData = async (
|
||||
await makeDir(path.join(root, "data"));
|
||||
for (const dataSource of dataSources) {
|
||||
const dataSourceConfig = dataSource?.config as FileSourceConfig;
|
||||
// Copy local data
|
||||
const dataPath = dataSourceConfig.path;
|
||||
|
||||
const destPath = path.join(root, "data", path.basename(dataPath));
|
||||
console.log("Copying data from path:", dataPath);
|
||||
await fsExtra.copy(dataPath, destPath);
|
||||
// If the path is URLs, download the data and save it to the data directory
|
||||
if ("url" in dataSourceConfig) {
|
||||
console.log(
|
||||
"Downloading file from URL:",
|
||||
dataSourceConfig.url.toString(),
|
||||
);
|
||||
const destPath = path.join(
|
||||
root,
|
||||
"data",
|
||||
path.basename(dataSourceConfig.url.toString()),
|
||||
);
|
||||
await downloadFile(dataSourceConfig.url.toString(), destPath);
|
||||
} else {
|
||||
// Copy local data
|
||||
console.log("Copying data from path:", dataSourceConfig.path);
|
||||
const destPath = path.join(
|
||||
root,
|
||||
"data",
|
||||
path.basename(dataSourceConfig.path),
|
||||
);
|
||||
await fsExtra.copy(dataSourceConfig.path, destPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import ciInfo from "ci-info";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions";
|
||||
import { questionHandlers, toChoice } from "../../questions/utils";
|
||||
|
||||
const MODELS = [
|
||||
"claude-3-opus",
|
||||
@@ -70,9 +69,7 @@ export async function askAnthropicQuestions({
|
||||
config.apiKey = key || process.env.ANTHROPIC_API_KEY;
|
||||
}
|
||||
|
||||
// use default model values in CI or if user should not be asked
|
||||
const useDefaults = ciInfo.isCI || !askModels;
|
||||
if (!useDefaults) {
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import ciInfo from "ci-info";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams, ModelConfigQuestionsParams } from ".";
|
||||
import { questionHandlers } from "../../questions";
|
||||
import { questionHandlers } from "../../questions/utils";
|
||||
|
||||
const ALL_AZURE_OPENAI_CHAT_MODELS: Record<string, { openAIModel: string }> = {
|
||||
"gpt-35-turbo": { openAIModel: "gpt-3.5-turbo" },
|
||||
@@ -67,9 +66,7 @@ export async function askAzureQuestions({
|
||||
},
|
||||
};
|
||||
|
||||
// use default model values in CI or if user should not be asked
|
||||
const useDefaults = ciInfo.isCI || !askModels;
|
||||
if (!useDefaults) {
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import ciInfo from "ci-info";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions";
|
||||
import { questionHandlers, toChoice } from "../../questions/utils";
|
||||
|
||||
const MODELS = ["gemini-1.5-pro-latest", "gemini-pro", "gemini-pro-vision"];
|
||||
type ModelData = {
|
||||
@@ -54,9 +53,7 @@ export async function askGeminiQuestions({
|
||||
config.apiKey = key || process.env.GOOGLE_API_KEY;
|
||||
}
|
||||
|
||||
// use default model values in CI or if user should not be asked
|
||||
const useDefaults = ciInfo.isCI || !askModels;
|
||||
if (!useDefaults) {
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import ciInfo from "ci-info";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions";
|
||||
import { questionHandlers, toChoice } from "../../questions/utils";
|
||||
|
||||
import got from "got";
|
||||
import ora from "ora";
|
||||
@@ -110,9 +109,7 @@ export async function askGroqQuestions({
|
||||
config.apiKey = key || process.env.GROQ_API_KEY;
|
||||
}
|
||||
|
||||
// use default model values in CI or if user should not be asked
|
||||
const useDefaults = ciInfo.isCI || !askModels;
|
||||
if (!useDefaults) {
|
||||
if (askModels) {
|
||||
const modelChoices = await getAvailableModelChoicesGroq(config.apiKey!);
|
||||
|
||||
const { model } = await prompts(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import ciInfo from "ci-info";
|
||||
import prompts from "prompts";
|
||||
import { questionHandlers } from "../../questions";
|
||||
import { questionHandlers } from "../../questions/utils";
|
||||
import { ModelConfig, ModelProvider, TemplateFramework } from "../types";
|
||||
import { askAnthropicQuestions } from "./anthropic";
|
||||
import { askAzureQuestions } from "./azure";
|
||||
@@ -27,7 +26,7 @@ export async function askModelConfig({
|
||||
framework,
|
||||
}: ModelConfigQuestionsParams): Promise<ModelConfig> {
|
||||
let modelProvider: ModelProvider = DEFAULT_MODEL_PROVIDER;
|
||||
if (askModels && !ciInfo.isCI) {
|
||||
if (askModels) {
|
||||
let choices = [
|
||||
{ title: "OpenAI", value: "openai" },
|
||||
{ title: "Groq", value: "groq" },
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import ciInfo from "ci-info";
|
||||
import got from "got";
|
||||
import ora from "ora";
|
||||
import { red } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers } from "../../questions";
|
||||
import { questionHandlers } from "../../questions/utils";
|
||||
|
||||
export const TSYSTEMS_LLMHUB_API_URL =
|
||||
"https://llm-server.llmhub.t-systems.net/v2";
|
||||
@@ -80,9 +79,7 @@ export async function askLLMHubQuestions({
|
||||
config.apiKey = key || process.env.T_SYSTEMS_LLMHUB_API_KEY;
|
||||
}
|
||||
|
||||
// use default model values in CI or if user should not be asked
|
||||
const useDefaults = ciInfo.isCI || !askModels;
|
||||
if (!useDefaults) {
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import ciInfo from "ci-info";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions";
|
||||
import { questionHandlers, toChoice } from "../../questions/utils";
|
||||
|
||||
const MODELS = ["mistral-tiny", "mistral-small", "mistral-medium"];
|
||||
type ModelData = {
|
||||
@@ -53,9 +52,7 @@ export async function askMistralQuestions({
|
||||
config.apiKey = key || process.env.MISTRAL_API_KEY;
|
||||
}
|
||||
|
||||
// use default model values in CI or if user should not be asked
|
||||
const useDefaults = ciInfo.isCI || !askModels;
|
||||
if (!useDefaults) {
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import ciInfo from "ci-info";
|
||||
import ollama, { type ModelResponse } from "ollama";
|
||||
import { red } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams } from ".";
|
||||
import { questionHandlers, toChoice } from "../../questions";
|
||||
import { questionHandlers, toChoice } from "../../questions/utils";
|
||||
|
||||
type ModelData = {
|
||||
dimensions: number;
|
||||
@@ -34,9 +33,7 @@ export async function askOllamaQuestions({
|
||||
},
|
||||
};
|
||||
|
||||
// use default model values in CI or if user should not be asked
|
||||
const useDefaults = ciInfo.isCI || !askModels;
|
||||
if (!useDefaults) {
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import ciInfo from "ci-info";
|
||||
import got from "got";
|
||||
import ora from "ora";
|
||||
import { red } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { ModelConfigParams, ModelConfigQuestionsParams } from ".";
|
||||
import { questionHandlers } from "../../questions";
|
||||
import { questionHandlers } from "../../questions/utils";
|
||||
|
||||
const OPENAI_API_URL = "https://api.openai.com/v1";
|
||||
|
||||
@@ -54,9 +53,7 @@ export async function askOpenAIQuestions({
|
||||
config.apiKey = key || process.env.OPENAI_API_KEY;
|
||||
}
|
||||
|
||||
// use default model values in CI or if user should not be asked
|
||||
const useDefaults = ciInfo.isCI || !askModels;
|
||||
if (!useDefaults) {
|
||||
if (askModels) {
|
||||
const { model } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
|
||||
+26
-6
@@ -93,6 +93,12 @@ const getAdditionalDependencies = (
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "llamacloud":
|
||||
dependencies.push({
|
||||
name: "llama-index-indices-managed-llama-cloud",
|
||||
version: "^0.3.1",
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// Add data source dependencies
|
||||
@@ -127,12 +133,6 @@ const getAdditionalDependencies = (
|
||||
version: "^2.9.9",
|
||||
});
|
||||
break;
|
||||
case "llamacloud":
|
||||
dependencies.push({
|
||||
name: "llama-index-indices-managed-llama-cloud",
|
||||
version: "^0.3.1",
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -362,6 +362,7 @@ export const installPythonTemplate = async ({
|
||||
postInstallAction,
|
||||
observability,
|
||||
modelConfig,
|
||||
agents,
|
||||
}: Pick<
|
||||
InstallTemplateArgs,
|
||||
| "root"
|
||||
@@ -373,6 +374,7 @@ export const installPythonTemplate = async ({
|
||||
| "postInstallAction"
|
||||
| "observability"
|
||||
| "modelConfig"
|
||||
| "agents"
|
||||
>) => {
|
||||
console.log("\nInitializing Python project with template:", template, "\n");
|
||||
let templatePath;
|
||||
@@ -443,6 +445,24 @@ export const installPythonTemplate = async ({
|
||||
cwd: path.join(compPath, "engines", "python", engine),
|
||||
});
|
||||
|
||||
// Copy agent code
|
||||
if (template === "multiagent") {
|
||||
if (agents) {
|
||||
await copy("**", path.join(root), {
|
||||
parents: true,
|
||||
cwd: path.join(compPath, "agents", "python", agents),
|
||||
rename: assetRelocator,
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
red(
|
||||
"There is no agent selected for multi-agent template. Please pick an agent to use via --agents flag.",
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy router code
|
||||
await copyRouterCode(root, tools ?? []);
|
||||
}
|
||||
|
||||
+18
-2
@@ -139,7 +139,7 @@ For better results, you can specify the region parameter to get results from a s
|
||||
dependencies: [
|
||||
{
|
||||
name: "e2b_code_interpreter",
|
||||
version: "0.0.10",
|
||||
version: "0.0.11b38",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
@@ -170,7 +170,7 @@ For better results, you can specify the region parameter to get results from a s
|
||||
dependencies: [
|
||||
{
|
||||
name: "e2b_code_interpreter",
|
||||
version: "^0.0.11b38",
|
||||
version: "0.0.11b38",
|
||||
},
|
||||
],
|
||||
supportedFrameworks: ["fastapi", "express", "nextjs"],
|
||||
@@ -267,6 +267,22 @@ For better results, you can specify the region parameter to get results from a s
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display: "Form Filling",
|
||||
name: "form_filling",
|
||||
supportedFrameworks: ["fastapi"],
|
||||
type: ToolType.LOCAL,
|
||||
dependencies: [
|
||||
{
|
||||
name: "pandas",
|
||||
version: "^2.2.3",
|
||||
},
|
||||
{
|
||||
name: "tabulate",
|
||||
version: "^0.9.0",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const getTool = (toolName: string): Tool | undefined => {
|
||||
|
||||
+10
-4
@@ -46,12 +46,17 @@ export type TemplateDataSource = {
|
||||
type: TemplateDataSourceType;
|
||||
config: TemplateDataSourceConfig;
|
||||
};
|
||||
export type TemplateDataSourceType = "file" | "web" | "db" | "llamacloud";
|
||||
export type TemplateDataSourceType = "file" | "web" | "db";
|
||||
export type TemplateObservability = "none" | "traceloop" | "llamatrace";
|
||||
export type TemplateAgents = "financial_report" | "blog" | "form_filling";
|
||||
// Config for both file and folder
|
||||
export type FileSourceConfig = {
|
||||
path: string;
|
||||
};
|
||||
export type FileSourceConfig =
|
||||
| {
|
||||
path: string;
|
||||
}
|
||||
| {
|
||||
url: URL;
|
||||
};
|
||||
export type WebSourceConfig = {
|
||||
baseUrl?: string;
|
||||
prefix?: string;
|
||||
@@ -94,4 +99,5 @@ export interface InstallTemplateArgs {
|
||||
postInstallAction?: TemplatePostInstallAction;
|
||||
tools?: Tool[];
|
||||
observability?: TemplateObservability;
|
||||
agents?: TemplateAgents;
|
||||
}
|
||||
|
||||
+28
-7
@@ -1,7 +1,7 @@
|
||||
import fs from "fs/promises";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { bold, cyan, yellow } from "picocolors";
|
||||
import { bold, cyan, red, yellow } from "picocolors";
|
||||
import { assetRelocator, copy } from "../helpers/copy";
|
||||
import { callPackageManager } from "../helpers/install";
|
||||
import { templatesDir } from "./dir";
|
||||
@@ -26,6 +26,7 @@ export const installTSTemplate = async ({
|
||||
tools,
|
||||
dataSources,
|
||||
useLlamaParse,
|
||||
agents,
|
||||
}: InstallTemplateArgs & { backend: boolean }) => {
|
||||
console.log(bold(`Using ${packageManager}.`));
|
||||
|
||||
@@ -132,6 +133,31 @@ export const installTSTemplate = async ({
|
||||
cwd: path.join(multiagentPath, "workflow"),
|
||||
});
|
||||
|
||||
// Copy agents use case code for multiagent template
|
||||
if (agents) {
|
||||
console.log("\nCopying agent:", agents, "\n");
|
||||
|
||||
const agentsCodePath = path.join(
|
||||
compPath,
|
||||
"agents",
|
||||
"typescript",
|
||||
agents,
|
||||
);
|
||||
|
||||
await copy("**", path.join(root, relativeEngineDestPath, "workflow"), {
|
||||
parents: true,
|
||||
cwd: agentsCodePath,
|
||||
rename: assetRelocator,
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
red(
|
||||
"There is no agent selected for multi-agent template. Please pick an agent to use via --agents flag.",
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (framework === "nextjs") {
|
||||
// patch route.ts file
|
||||
await copy("**", path.join(root, relativeEngineDestPath), {
|
||||
@@ -279,12 +305,7 @@ async function updatePackageJson({
|
||||
"remark-gfm": undefined,
|
||||
"remark-math": undefined,
|
||||
"react-markdown": undefined,
|
||||
"react-syntax-highlighter": undefined,
|
||||
};
|
||||
|
||||
packageJson.devDependencies = {
|
||||
...packageJson.devDependencies,
|
||||
"@types/react-syntax-highlighter": undefined,
|
||||
"highlight.js": undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { execSync } from "child_process";
|
||||
import Commander from "commander";
|
||||
import Conf from "conf";
|
||||
import { Command } from "commander";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { bold, cyan, green, red, yellow } from "picocolors";
|
||||
@@ -17,8 +16,9 @@ import { runApp } from "./helpers/run-app";
|
||||
import { getTools } from "./helpers/tools";
|
||||
import { validateNpmName } from "./helpers/validate-pkg";
|
||||
import packageJson from "./package.json";
|
||||
import { QuestionArgs, askQuestions, onPromptState } from "./questions";
|
||||
|
||||
import { askQuestions } from "./questions/index";
|
||||
import { QuestionArgs } from "./questions/types";
|
||||
import { onPromptState } from "./questions/utils";
|
||||
// Run the initialization function
|
||||
initializeGlobalAgent();
|
||||
|
||||
@@ -29,12 +29,14 @@ const handleSigTerm = () => process.exit(0);
|
||||
process.on("SIGINT", handleSigTerm);
|
||||
process.on("SIGTERM", handleSigTerm);
|
||||
|
||||
const program = new Commander.Command(packageJson.name)
|
||||
const program = new Command(packageJson.name)
|
||||
.version(packageJson.version)
|
||||
.arguments("<project-directory>")
|
||||
.usage(`${green("<project-directory>")} [options]`)
|
||||
.arguments("[project-directory]")
|
||||
.usage(`${green("[project-directory]")} [options]`)
|
||||
.action((name) => {
|
||||
projectPath = name;
|
||||
if (name) {
|
||||
projectPath = name;
|
||||
}
|
||||
})
|
||||
.option(
|
||||
"--use-npm",
|
||||
@@ -55,13 +57,6 @@ const program = new Commander.Command(packageJson.name)
|
||||
`
|
||||
|
||||
Explicitly tell the CLI to bootstrap the application using Yarn
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--reset-preferences",
|
||||
`
|
||||
|
||||
Explicitly tell the CLI to reset any stored preferences
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
@@ -124,7 +119,14 @@ const program = new Commander.Command(packageJson.name)
|
||||
"--frontend",
|
||||
`
|
||||
|
||||
Whether to generate a frontend for your backend.
|
||||
Generate a frontend for your backend.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
"--no-frontend",
|
||||
`
|
||||
|
||||
Do not generate a frontend for your backend.
|
||||
`,
|
||||
)
|
||||
.option(
|
||||
@@ -161,6 +163,13 @@ const program = new Commander.Command(packageJson.name)
|
||||
|
||||
Specify the tools you want to use by providing a comma-separated list. For example, 'wikipedia.WikipediaToolSpec,google.GoogleSearchToolSpec'. Use 'none' to not using any tools.
|
||||
`,
|
||||
(tools, _) => {
|
||||
if (tools === "none") {
|
||||
return [];
|
||||
} else {
|
||||
return getTools(tools.split(","));
|
||||
}
|
||||
},
|
||||
)
|
||||
.option(
|
||||
"--use-llama-parse",
|
||||
@@ -189,86 +198,73 @@ const program = new Commander.Command(packageJson.name)
|
||||
|
||||
Allow interactive selection of LLM and embedding models of different model providers.
|
||||
`,
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--ask-examples",
|
||||
"--pro",
|
||||
`
|
||||
|
||||
Allow interactive selection of community templates and LlamaPacks.
|
||||
Allow interactive selection of all features.
|
||||
`,
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--agents <agents>",
|
||||
`
|
||||
|
||||
Select which agents to use for the multi-agent template (e.g: financial_report, blog).
|
||||
`,
|
||||
)
|
||||
.allowUnknownOption()
|
||||
.parse(process.argv);
|
||||
if (process.argv.includes("--no-frontend")) {
|
||||
program.frontend = false;
|
||||
}
|
||||
if (process.argv.includes("--tools")) {
|
||||
if (program.tools === "none") {
|
||||
program.tools = [];
|
||||
} else {
|
||||
program.tools = getTools(program.tools.split(","));
|
||||
}
|
||||
}
|
||||
|
||||
const options = program.opts();
|
||||
|
||||
if (
|
||||
process.argv.includes("--no-llama-parse") ||
|
||||
program.template === "extractor"
|
||||
options.template === "extractor"
|
||||
) {
|
||||
program.useLlamaParse = false;
|
||||
options.useLlamaParse = false;
|
||||
}
|
||||
program.askModels = process.argv.includes("--ask-models");
|
||||
program.askExamples = process.argv.includes("--ask-examples");
|
||||
if (process.argv.includes("--no-files")) {
|
||||
program.dataSources = [];
|
||||
options.dataSources = [];
|
||||
} else if (process.argv.includes("--example-file")) {
|
||||
program.dataSources = getDataSources(program.files, program.exampleFile);
|
||||
options.dataSources = getDataSources(options.files, options.exampleFile);
|
||||
} else if (process.argv.includes("--llamacloud")) {
|
||||
program.dataSources = [
|
||||
{
|
||||
type: "llamacloud",
|
||||
config: {},
|
||||
},
|
||||
EXAMPLE_FILE,
|
||||
];
|
||||
options.dataSources = [EXAMPLE_FILE];
|
||||
options.vectorDb = "llamacloud";
|
||||
} else if (process.argv.includes("--web-source")) {
|
||||
program.dataSources = [
|
||||
options.dataSources = [
|
||||
{
|
||||
type: "web",
|
||||
config: {
|
||||
baseUrl: program.webSource,
|
||||
prefix: program.webSource,
|
||||
baseUrl: options.webSource,
|
||||
prefix: options.webSource,
|
||||
depth: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
} else if (process.argv.includes("--db-source")) {
|
||||
program.dataSources = [
|
||||
options.dataSources = [
|
||||
{
|
||||
type: "db",
|
||||
config: {
|
||||
uri: program.dbSource,
|
||||
queries: program.dbQuery || "SELECT * FROM mytable",
|
||||
uri: options.dbSource,
|
||||
queries: options.dbQuery || "SELECT * FROM mytable",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const packageManager = !!program.useNpm
|
||||
const packageManager = !!options.useNpm
|
||||
? "npm"
|
||||
: !!program.usePnpm
|
||||
: !!options.usePnpm
|
||||
? "pnpm"
|
||||
: !!program.useYarn
|
||||
: !!options.useYarn
|
||||
? "yarn"
|
||||
: getPkgManager();
|
||||
|
||||
async function run(): Promise<void> {
|
||||
const conf = new Conf({ projectName: "create-llama" });
|
||||
|
||||
if (program.resetPreferences) {
|
||||
conf.clear();
|
||||
console.log(`Preferences reset successfully`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof projectPath === "string") {
|
||||
projectPath = projectPath.trim();
|
||||
}
|
||||
@@ -331,35 +327,16 @@ async function run(): Promise<void> {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const preferences = (conf.get("preferences") || {}) as QuestionArgs;
|
||||
await askQuestions(
|
||||
program as unknown as QuestionArgs,
|
||||
preferences,
|
||||
program.openAiKey,
|
||||
);
|
||||
const answers = await askQuestions(options as unknown as QuestionArgs);
|
||||
|
||||
await createApp({
|
||||
template: program.template,
|
||||
framework: program.framework,
|
||||
ui: program.ui,
|
||||
...answers,
|
||||
appPath: resolvedProjectPath,
|
||||
packageManager,
|
||||
frontend: program.frontend,
|
||||
modelConfig: program.modelConfig,
|
||||
llamaCloudKey: program.llamaCloudKey,
|
||||
communityProjectConfig: program.communityProjectConfig,
|
||||
llamapack: program.llamapack,
|
||||
vectorDb: program.vectorDb,
|
||||
externalPort: program.externalPort,
|
||||
postInstallAction: program.postInstallAction,
|
||||
dataSources: program.dataSources,
|
||||
tools: program.tools,
|
||||
useLlamaParse: program.useLlamaParse,
|
||||
observability: program.observability,
|
||||
externalPort: options.externalPort,
|
||||
});
|
||||
conf.set("preferences", preferences);
|
||||
|
||||
if (program.postInstallAction === "VSCode") {
|
||||
if (answers.postInstallAction === "VSCode") {
|
||||
console.log(`Starting VSCode in ${root}...`);
|
||||
try {
|
||||
execSync(`code . --new-window --goto README.md`, {
|
||||
@@ -383,15 +360,15 @@ Please check ${cyan(
|
||||
)} for more information.`,
|
||||
);
|
||||
}
|
||||
} else if (program.postInstallAction === "runApp") {
|
||||
} else if (answers.postInstallAction === "runApp") {
|
||||
console.log(`Running app in ${root}...`);
|
||||
await runApp(
|
||||
root,
|
||||
program.template,
|
||||
program.frontend,
|
||||
program.framework,
|
||||
program.port,
|
||||
program.externalPort,
|
||||
answers.template,
|
||||
answers.frontend,
|
||||
answers.framework,
|
||||
options.port,
|
||||
options.externalPort,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.2.16",
|
||||
"version": "0.3.9",
|
||||
"description": "Create LlamaIndex-powered apps with one command",
|
||||
"keywords": [
|
||||
"rag",
|
||||
@@ -27,7 +27,6 @@
|
||||
"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",
|
||||
@@ -50,8 +49,7 @@
|
||||
"async-retry": "1.3.1",
|
||||
"async-sema": "3.0.1",
|
||||
"ci-info": "github:watson/ci-info#f43f6a1cefff47fb361c88cf4b943fdbcaafe540",
|
||||
"commander": "2.20.0",
|
||||
"conf": "10.2.0",
|
||||
"commander": "12.1.0",
|
||||
"cross-spawn": "7.0.3",
|
||||
"fast-glob": "3.3.1",
|
||||
"fs-extra": "11.2.0",
|
||||
@@ -60,7 +58,7 @@
|
||||
"ollama": "^0.5.0",
|
||||
"ora": "^8.0.1",
|
||||
"picocolors": "1.0.0",
|
||||
"prompts": "2.1.0",
|
||||
"prompts": "2.4.2",
|
||||
"smol-toml": "^1.1.4",
|
||||
"tar": "6.1.15",
|
||||
"terminal-link": "^3.0.0",
|
||||
|
||||
Generated
+11
-147
@@ -42,11 +42,8 @@ importers:
|
||||
specifier: github:watson/ci-info#f43f6a1cefff47fb361c88cf4b943fdbcaafe540
|
||||
version: https://codeload.github.com/watson/ci-info/tar.gz/f43f6a1cefff47fb361c88cf4b943fdbcaafe540
|
||||
commander:
|
||||
specifier: 2.20.0
|
||||
version: 2.20.0
|
||||
conf:
|
||||
specifier: 10.2.0
|
||||
version: 10.2.0
|
||||
specifier: 12.1.0
|
||||
version: 12.1.0
|
||||
cross-spawn:
|
||||
specifier: 7.0.3
|
||||
version: 7.0.3
|
||||
@@ -72,8 +69,8 @@ importers:
|
||||
specifier: 1.0.0
|
||||
version: 1.0.0
|
||||
prompts:
|
||||
specifier: 2.1.0
|
||||
version: 2.1.0
|
||||
specifier: 2.4.2
|
||||
version: 2.4.2
|
||||
smol-toml:
|
||||
specifier: ^1.1.4
|
||||
version: 1.1.4
|
||||
@@ -336,20 +333,9 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
ajv-formats@2.1.1:
|
||||
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
||||
peerDependencies:
|
||||
ajv: ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
ajv:
|
||||
optional: true
|
||||
|
||||
ajv@6.12.6:
|
||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||
|
||||
ajv@8.13.0:
|
||||
resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==}
|
||||
|
||||
ansi-colors@4.1.3:
|
||||
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -410,10 +396,6 @@ packages:
|
||||
async-sema@3.0.1:
|
||||
resolution: {integrity: sha512-fKT2riE8EHAvJEfLJXZiATQWqZttjx1+tfgnVshCDrH8vlw4YC8aECe0B8MU184g+aVRFVgmfxFlKZKaozSrNw==}
|
||||
|
||||
atomically@1.7.0:
|
||||
resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==}
|
||||
engines: {node: '>=10.12.0'}
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -530,8 +512,9 @@ packages:
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
commander@2.20.0:
|
||||
resolution: {integrity: sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==}
|
||||
commander@12.1.0:
|
||||
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
commander@9.5.0:
|
||||
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
|
||||
@@ -540,10 +523,6 @@ packages:
|
||||
concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
conf@10.2.0:
|
||||
resolution: {integrity: sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
cross-spawn@5.1.0:
|
||||
resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}
|
||||
|
||||
@@ -576,10 +555,6 @@ packages:
|
||||
resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
debounce-fn@4.0.0:
|
||||
resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
debug@4.3.4:
|
||||
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
@@ -638,10 +613,6 @@ packages:
|
||||
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
dot-prop@6.0.1:
|
||||
resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
duplexer3@0.1.5:
|
||||
resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==}
|
||||
|
||||
@@ -664,10 +635,6 @@ packages:
|
||||
resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
env-paths@2.2.1:
|
||||
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
error-ex@1.3.2:
|
||||
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
|
||||
|
||||
@@ -788,10 +755,6 @@ packages:
|
||||
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
find-up@3.0.0:
|
||||
resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
find-up@4.1.0:
|
||||
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1057,10 +1020,6 @@ packages:
|
||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
|
||||
is-obj@2.0.0:
|
||||
resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-path-inside@3.0.3:
|
||||
resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1138,12 +1097,6 @@ packages:
|
||||
json-schema-traverse@0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
|
||||
json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
json-schema-typed@7.0.3:
|
||||
resolution: {integrity: sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
@@ -1182,10 +1135,6 @@ packages:
|
||||
resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
locate-path@3.0.0:
|
||||
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
locate-path@5.0.0:
|
||||
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1243,10 +1192,6 @@ packages:
|
||||
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
mimic-fn@3.1.0:
|
||||
resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
mimic-response@1.0.1:
|
||||
resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -1375,10 +1320,6 @@ packages:
|
||||
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
p-locate@3.0.0:
|
||||
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
p-locate@4.1.0:
|
||||
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1407,10 +1348,6 @@ packages:
|
||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
path-exists@3.0.0:
|
||||
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
path-exists@4.0.0:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1449,10 +1386,6 @@ packages:
|
||||
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
pkg-up@3.1.0:
|
||||
resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
playwright-core@1.44.0:
|
||||
resolution: {integrity: sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==}
|
||||
engines: {node: '>=16'}
|
||||
@@ -1498,8 +1431,8 @@ packages:
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
prompts@2.1.0:
|
||||
resolution: {integrity: sha512-+x5TozgqYdOwWsQFZizE/Tra3fKvAoy037kOyU6cgz84n8f6zxngLOV4O32kTwt9FcLCxAqw0P/c8rOr9y+Gfg==}
|
||||
prompts@2.4.2:
|
||||
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
pseudomap@1.0.2:
|
||||
@@ -1557,10 +1490,6 @@ packages:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
require-from-string@2.0.2:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
require-main-filename@2.0.0:
|
||||
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
|
||||
|
||||
@@ -2306,10 +2235,6 @@ snapshots:
|
||||
|
||||
acorn@8.11.3: {}
|
||||
|
||||
ajv-formats@2.1.1(ajv@8.13.0):
|
||||
optionalDependencies:
|
||||
ajv: 8.13.0
|
||||
|
||||
ajv@6.12.6:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
@@ -2317,13 +2242,6 @@ snapshots:
|
||||
json-schema-traverse: 0.4.1
|
||||
uri-js: 4.4.1
|
||||
|
||||
ajv@8.13.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
uri-js: 4.4.1
|
||||
|
||||
ansi-colors@4.1.3: {}
|
||||
|
||||
ansi-escapes@5.0.0:
|
||||
@@ -2383,8 +2301,6 @@ snapshots:
|
||||
|
||||
async-sema@3.0.1: {}
|
||||
|
||||
atomically@1.7.0: {}
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
dependencies:
|
||||
possible-typed-array-names: 1.0.0
|
||||
@@ -2506,25 +2422,12 @@ snapshots:
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
commander@2.20.0: {}
|
||||
commander@12.1.0: {}
|
||||
|
||||
commander@9.5.0: {}
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
|
||||
conf@10.2.0:
|
||||
dependencies:
|
||||
ajv: 8.13.0
|
||||
ajv-formats: 2.1.1(ajv@8.13.0)
|
||||
atomically: 1.7.0
|
||||
debounce-fn: 4.0.0
|
||||
dot-prop: 6.0.1
|
||||
env-paths: 2.2.1
|
||||
json-schema-typed: 7.0.3
|
||||
onetime: 5.1.2
|
||||
pkg-up: 3.1.0
|
||||
semver: 7.6.1
|
||||
|
||||
cross-spawn@5.1.0:
|
||||
dependencies:
|
||||
lru-cache: 4.1.5
|
||||
@@ -2568,10 +2471,6 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
is-data-view: 1.0.1
|
||||
|
||||
debounce-fn@4.0.0:
|
||||
dependencies:
|
||||
mimic-fn: 3.1.0
|
||||
|
||||
debug@4.3.4:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
@@ -2621,10 +2520,6 @@ snapshots:
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
|
||||
dot-prop@6.0.1:
|
||||
dependencies:
|
||||
is-obj: 2.0.0
|
||||
|
||||
duplexer3@0.1.5: {}
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
@@ -2644,8 +2539,6 @@ snapshots:
|
||||
ansi-colors: 4.1.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
env-paths@2.2.1: {}
|
||||
|
||||
error-ex@1.3.2:
|
||||
dependencies:
|
||||
is-arrayish: 0.2.1
|
||||
@@ -2841,10 +2734,6 @@ snapshots:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
|
||||
find-up@3.0.0:
|
||||
dependencies:
|
||||
locate-path: 3.0.0
|
||||
|
||||
find-up@4.1.0:
|
||||
dependencies:
|
||||
locate-path: 5.0.0
|
||||
@@ -3129,8 +3018,6 @@ snapshots:
|
||||
|
||||
is-number@7.0.0: {}
|
||||
|
||||
is-obj@2.0.0: {}
|
||||
|
||||
is-path-inside@3.0.3: {}
|
||||
|
||||
is-plain-obj@1.1.0: {}
|
||||
@@ -3197,10 +3084,6 @@ snapshots:
|
||||
|
||||
json-schema-traverse@0.4.1: {}
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
||||
json-schema-typed@7.0.3: {}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
json-stringify-safe@5.0.1: {}
|
||||
@@ -3239,11 +3122,6 @@ snapshots:
|
||||
pify: 4.0.1
|
||||
strip-bom: 3.0.0
|
||||
|
||||
locate-path@3.0.0:
|
||||
dependencies:
|
||||
p-locate: 3.0.0
|
||||
path-exists: 3.0.0
|
||||
|
||||
locate-path@5.0.0:
|
||||
dependencies:
|
||||
p-locate: 4.1.0
|
||||
@@ -3301,8 +3179,6 @@ snapshots:
|
||||
|
||||
mimic-fn@2.1.0: {}
|
||||
|
||||
mimic-fn@3.1.0: {}
|
||||
|
||||
mimic-response@1.0.1: {}
|
||||
|
||||
mimic-response@2.1.0: {}
|
||||
@@ -3425,10 +3301,6 @@ snapshots:
|
||||
dependencies:
|
||||
yocto-queue: 0.1.0
|
||||
|
||||
p-locate@3.0.0:
|
||||
dependencies:
|
||||
p-limit: 2.3.0
|
||||
|
||||
p-locate@4.1.0:
|
||||
dependencies:
|
||||
p-limit: 2.3.0
|
||||
@@ -3456,8 +3328,6 @@ snapshots:
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
lines-and-columns: 1.2.4
|
||||
|
||||
path-exists@3.0.0: {}
|
||||
|
||||
path-exists@4.0.0: {}
|
||||
|
||||
path-is-absolute@1.0.1: {}
|
||||
@@ -3483,10 +3353,6 @@ snapshots:
|
||||
dependencies:
|
||||
find-up: 4.1.0
|
||||
|
||||
pkg-up@3.1.0:
|
||||
dependencies:
|
||||
find-up: 3.0.0
|
||||
|
||||
playwright-core@1.44.0: {}
|
||||
|
||||
playwright@1.44.0:
|
||||
@@ -3515,7 +3381,7 @@ snapshots:
|
||||
|
||||
prettier@3.2.5: {}
|
||||
|
||||
prompts@2.1.0:
|
||||
prompts@2.4.2:
|
||||
dependencies:
|
||||
kleur: 3.0.3
|
||||
sisteransi: 1.0.5
|
||||
@@ -3585,8 +3451,6 @@ snapshots:
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
require-main-filename@2.0.0: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
-769
@@ -1,769 +0,0 @@
|
||||
import { execSync } from "child_process";
|
||||
import ciInfo from "ci-info";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { blue, green, red } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { InstallAppArgs } from "./create-app";
|
||||
import {
|
||||
TemplateDataSource,
|
||||
TemplateDataSourceType,
|
||||
TemplateFramework,
|
||||
TemplateType,
|
||||
} from "./helpers";
|
||||
import { COMMUNITY_OWNER, COMMUNITY_REPO } from "./helpers/constant";
|
||||
import { EXAMPLE_FILE } from "./helpers/datasources";
|
||||
import { templatesDir } from "./helpers/dir";
|
||||
import { getAvailableLlamapackOptions } from "./helpers/llama-pack";
|
||||
import { askModelConfig } from "./helpers/providers";
|
||||
import { getProjectOptions } from "./helpers/repo";
|
||||
import {
|
||||
supportedTools,
|
||||
toolRequiresConfig,
|
||||
toolsRequireConfig,
|
||||
} from "./helpers/tools";
|
||||
|
||||
export type QuestionArgs = Omit<
|
||||
InstallAppArgs,
|
||||
"appPath" | "packageManager"
|
||||
> & {
|
||||
askModels?: boolean;
|
||||
askExamples?: boolean;
|
||||
};
|
||||
const supportedContextFileTypes = [
|
||||
".pdf",
|
||||
".doc",
|
||||
".docx",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".csv",
|
||||
];
|
||||
const MACOS_FILE_SELECTION_SCRIPT = `
|
||||
osascript -l JavaScript -e '
|
||||
a = Application.currentApplication();
|
||||
a.includeStandardAdditions = true;
|
||||
a.chooseFile({ withPrompt: "Please select files to process:", multipleSelectionsAllowed: true }).map(file => file.toString())
|
||||
'`;
|
||||
const MACOS_FOLDER_SELECTION_SCRIPT = `
|
||||
osascript -l JavaScript -e '
|
||||
a = Application.currentApplication();
|
||||
a.includeStandardAdditions = true;
|
||||
a.chooseFolder({ withPrompt: "Please select folders to process:", multipleSelectionsAllowed: true }).map(folder => folder.toString())
|
||||
'`;
|
||||
const WINDOWS_FILE_SELECTION_SCRIPT = `
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
$openFileDialog = New-Object System.Windows.Forms.OpenFileDialog
|
||||
$openFileDialog.InitialDirectory = [Environment]::GetFolderPath('Desktop')
|
||||
$openFileDialog.Multiselect = $true
|
||||
$result = $openFileDialog.ShowDialog()
|
||||
if ($result -eq 'OK') {
|
||||
$openFileDialog.FileNames
|
||||
}
|
||||
`;
|
||||
const WINDOWS_FOLDER_SELECTION_SCRIPT = `
|
||||
Add-Type -AssemblyName System.windows.forms
|
||||
$folderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog
|
||||
$dialogResult = $folderBrowser.ShowDialog()
|
||||
if ($dialogResult -eq [System.Windows.Forms.DialogResult]::OK)
|
||||
{
|
||||
$folderBrowser.SelectedPath
|
||||
}
|
||||
`;
|
||||
|
||||
const defaults: Omit<QuestionArgs, "modelConfig"> = {
|
||||
template: "streaming",
|
||||
framework: "nextjs",
|
||||
ui: "shadcn",
|
||||
frontend: false,
|
||||
llamaCloudKey: "",
|
||||
useLlamaParse: false,
|
||||
communityProjectConfig: undefined,
|
||||
llamapack: "",
|
||||
postInstallAction: "dependencies",
|
||||
dataSources: [],
|
||||
tools: [],
|
||||
};
|
||||
|
||||
export const questionHandlers = {
|
||||
onCancel: () => {
|
||||
console.error("Exiting.");
|
||||
process.exit(1);
|
||||
},
|
||||
};
|
||||
|
||||
const getVectorDbChoices = (framework: TemplateFramework) => {
|
||||
const choices = [
|
||||
{
|
||||
title: "No, just store the data in the file system",
|
||||
value: "none",
|
||||
},
|
||||
{ title: "MongoDB", value: "mongo" },
|
||||
{ title: "PostgreSQL", value: "pg" },
|
||||
{ title: "Pinecone", value: "pinecone" },
|
||||
{ title: "Milvus", value: "milvus" },
|
||||
{ title: "Astra", value: "astra" },
|
||||
{ title: "Qdrant", value: "qdrant" },
|
||||
{ title: "ChromaDB", value: "chroma" },
|
||||
{ title: "Weaviate", value: "weaviate" },
|
||||
];
|
||||
|
||||
const vectordbLang = framework === "fastapi" ? "python" : "typescript";
|
||||
const compPath = path.join(templatesDir, "components");
|
||||
const vectordbPath = path.join(compPath, "vectordbs", vectordbLang);
|
||||
|
||||
const availableChoices = fs
|
||||
.readdirSync(vectordbPath)
|
||||
.filter((file) => fs.statSync(path.join(vectordbPath, file)).isDirectory());
|
||||
|
||||
const displayedChoices = choices.filter((choice) =>
|
||||
availableChoices.includes(choice.value),
|
||||
);
|
||||
|
||||
return displayedChoices;
|
||||
};
|
||||
|
||||
export const getDataSourceChoices = (
|
||||
framework: TemplateFramework,
|
||||
selectedDataSource: TemplateDataSource[],
|
||||
template?: TemplateType,
|
||||
) => {
|
||||
// If LlamaCloud is already selected, don't show any other options
|
||||
if (selectedDataSource.find((s) => s.type === "llamacloud")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const choices = [];
|
||||
|
||||
if (selectedDataSource.length > 0) {
|
||||
choices.push({
|
||||
title: "No",
|
||||
value: "no",
|
||||
});
|
||||
}
|
||||
if (selectedDataSource === undefined || selectedDataSource.length === 0) {
|
||||
choices.push({
|
||||
title: "No datasource",
|
||||
value: "none",
|
||||
});
|
||||
choices.push({
|
||||
title:
|
||||
process.platform !== "linux"
|
||||
? "Use an example PDF"
|
||||
: "Use an example PDF (you can add your own data files later)",
|
||||
value: "exampleFile",
|
||||
});
|
||||
}
|
||||
|
||||
// Linux has many distros so we won't support file/folder picker for now
|
||||
if (process.platform !== "linux") {
|
||||
choices.push(
|
||||
{
|
||||
title: `Use local files (${supportedContextFileTypes.join(", ")})`,
|
||||
value: "file",
|
||||
},
|
||||
{
|
||||
title:
|
||||
process.platform === "win32"
|
||||
? "Use a local folder"
|
||||
: "Use local folders",
|
||||
value: "folder",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (framework === "fastapi" && template !== "extractor") {
|
||||
choices.push({
|
||||
title: "Use website content (requires Chrome)",
|
||||
value: "web",
|
||||
});
|
||||
choices.push({
|
||||
title: "Use data from a database (Mysql, PostgreSQL)",
|
||||
value: "db",
|
||||
});
|
||||
}
|
||||
|
||||
if (!selectedDataSource.length && template !== "extractor") {
|
||||
choices.push({
|
||||
title: "Use managed index from LlamaCloud",
|
||||
value: "llamacloud",
|
||||
});
|
||||
}
|
||||
return choices;
|
||||
};
|
||||
|
||||
const selectLocalContextData = async (type: TemplateDataSourceType) => {
|
||||
try {
|
||||
let selectedPath: string = "";
|
||||
let execScript: string;
|
||||
let execOpts: any = {};
|
||||
switch (process.platform) {
|
||||
case "win32": // Windows
|
||||
execScript =
|
||||
type === "file"
|
||||
? WINDOWS_FILE_SELECTION_SCRIPT
|
||||
: WINDOWS_FOLDER_SELECTION_SCRIPT;
|
||||
execOpts = { shell: "powershell.exe" };
|
||||
break;
|
||||
case "darwin": // MacOS
|
||||
execScript =
|
||||
type === "file"
|
||||
? MACOS_FILE_SELECTION_SCRIPT
|
||||
: MACOS_FOLDER_SELECTION_SCRIPT;
|
||||
break;
|
||||
default: // Unsupported OS
|
||||
console.log(red("Unsupported OS error!"));
|
||||
process.exit(1);
|
||||
}
|
||||
selectedPath = execSync(execScript, execOpts).toString().trim();
|
||||
const paths =
|
||||
process.platform === "win32"
|
||||
? selectedPath.split("\r\n")
|
||||
: selectedPath.split(", ");
|
||||
|
||||
for (const p of paths) {
|
||||
if (
|
||||
fs.statSync(p).isFile() &&
|
||||
!supportedContextFileTypes.includes(path.extname(p))
|
||||
) {
|
||||
console.log(
|
||||
red(
|
||||
`Please select a supported file type: ${supportedContextFileTypes}`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
red(
|
||||
"Got an error when trying to select local context data! Please try again or select another data source option.",
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
export const onPromptState = (state: any) => {
|
||||
if (state.aborted) {
|
||||
// If we don't re-enable the terminal cursor before exiting
|
||||
// the program, the cursor will remain hidden
|
||||
process.stdout.write("\x1B[?25h");
|
||||
process.stdout.write("\n");
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
export const askQuestions = async (
|
||||
program: QuestionArgs,
|
||||
preferences: QuestionArgs,
|
||||
openAiKey?: string,
|
||||
) => {
|
||||
const getPrefOrDefault = <K extends keyof Omit<QuestionArgs, "modelConfig">>(
|
||||
field: K,
|
||||
): Omit<QuestionArgs, "modelConfig">[K] =>
|
||||
preferences[field] ?? defaults[field];
|
||||
|
||||
// Ask for next action after installation
|
||||
async function askPostInstallAction() {
|
||||
if (program.postInstallAction === undefined) {
|
||||
if (ciInfo.isCI) {
|
||||
program.postInstallAction = getPrefOrDefault("postInstallAction");
|
||||
} else {
|
||||
const actionChoices = [
|
||||
{
|
||||
title: "Just generate code (~1 sec)",
|
||||
value: "none",
|
||||
},
|
||||
{
|
||||
title: "Start in VSCode (~1 sec)",
|
||||
value: "VSCode",
|
||||
},
|
||||
{
|
||||
title: "Generate code and install dependencies (~2 min)",
|
||||
value: "dependencies",
|
||||
},
|
||||
];
|
||||
|
||||
const modelConfigured =
|
||||
!program.llamapack && program.modelConfig.isConfigured();
|
||||
// If using LlamaParse, require LlamaCloud API key
|
||||
const llamaCloudKeyConfigured = program.useLlamaParse
|
||||
? program.llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
|
||||
: true;
|
||||
const hasVectorDb = program.vectorDb && program.vectorDb !== "none";
|
||||
// Can run the app if all tools do not require configuration
|
||||
if (
|
||||
!hasVectorDb &&
|
||||
modelConfigured &&
|
||||
llamaCloudKeyConfigured &&
|
||||
!toolsRequireConfig(program.tools)
|
||||
) {
|
||||
actionChoices.push({
|
||||
title:
|
||||
"Generate code, install dependencies, and run the app (~2 min)",
|
||||
value: "runApp",
|
||||
});
|
||||
}
|
||||
|
||||
const { action } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "action",
|
||||
message: "How would you like to proceed?",
|
||||
choices: actionChoices,
|
||||
initial: 1,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
program.postInstallAction = action;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!program.template) {
|
||||
if (ciInfo.isCI) {
|
||||
program.template = getPrefOrDefault("template");
|
||||
} else {
|
||||
const styledRepo = blue(
|
||||
`https://github.com/${COMMUNITY_OWNER}/${COMMUNITY_REPO}`,
|
||||
);
|
||||
const { template } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "template",
|
||||
message: "Which template would you like to use?",
|
||||
choices: [
|
||||
{ title: "Agentic RAG (e.g. chat with docs)", value: "streaming" },
|
||||
{
|
||||
title: "Multi-agent app (using workflows)",
|
||||
value: "multiagent",
|
||||
},
|
||||
{ title: "Structured Extractor", value: "extractor" },
|
||||
...(program.askExamples
|
||||
? [
|
||||
{
|
||||
title: `Community template from ${styledRepo}`,
|
||||
value: "community",
|
||||
},
|
||||
{
|
||||
title: "Example using a LlamaPack",
|
||||
value: "llamapack",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.template = template;
|
||||
preferences.template = template;
|
||||
}
|
||||
}
|
||||
|
||||
if (program.template === "community") {
|
||||
const projectOptions = await getProjectOptions(
|
||||
COMMUNITY_OWNER,
|
||||
COMMUNITY_REPO,
|
||||
);
|
||||
const { communityProjectConfig } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "communityProjectConfig",
|
||||
message: "Select community template",
|
||||
choices: projectOptions.map(({ title, value }) => ({
|
||||
title,
|
||||
value: JSON.stringify(value), // serialize value to string in terminal
|
||||
})),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
const projectConfig = JSON.parse(communityProjectConfig);
|
||||
program.communityProjectConfig = projectConfig;
|
||||
preferences.communityProjectConfig = projectConfig;
|
||||
return; // early return - no further questions needed for community projects
|
||||
}
|
||||
|
||||
if (program.template === "llamapack") {
|
||||
const availableLlamaPacks = await getAvailableLlamapackOptions();
|
||||
const { llamapack } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "llamapack",
|
||||
message: "Select LlamaPack",
|
||||
choices: availableLlamaPacks.map((pack) => ({
|
||||
title: pack.name,
|
||||
value: pack.folderPath,
|
||||
})),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.llamapack = llamapack;
|
||||
preferences.llamapack = llamapack;
|
||||
await askPostInstallAction();
|
||||
return; // early return - no further questions needed for llamapack projects
|
||||
}
|
||||
|
||||
if (program.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];
|
||||
program.framework = preferences.framework = "fastapi";
|
||||
}
|
||||
if (!program.framework) {
|
||||
if (ciInfo.isCI) {
|
||||
program.framework = getPrefOrDefault("framework");
|
||||
} else {
|
||||
const choices = [
|
||||
{ title: "NextJS", value: "nextjs" },
|
||||
{ title: "Express", value: "express" },
|
||||
{ title: "FastAPI (Python)", value: "fastapi" },
|
||||
];
|
||||
|
||||
const { framework } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "framework",
|
||||
message: "Which framework would you like to use?",
|
||||
choices,
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.framework = framework;
|
||||
preferences.framework = framework;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(program.framework === "express" || program.framework === "fastapi") &&
|
||||
(program.template === "streaming" || program.template === "multiagent")
|
||||
) {
|
||||
// if a backend-only framework is selected, ask whether we should create a frontend
|
||||
if (program.frontend === undefined) {
|
||||
if (ciInfo.isCI) {
|
||||
program.frontend = getPrefOrDefault("frontend");
|
||||
} else {
|
||||
const styledNextJS = blue("NextJS");
|
||||
const styledBackend = green(
|
||||
program.framework === "express"
|
||||
? "Express "
|
||||
: program.framework === "fastapi"
|
||||
? "FastAPI (Python) "
|
||||
: "",
|
||||
);
|
||||
const { frontend } = await prompts({
|
||||
onState: onPromptState,
|
||||
type: "toggle",
|
||||
name: "frontend",
|
||||
message: `Would you like to generate a ${styledNextJS} frontend for your ${styledBackend}backend?`,
|
||||
initial: getPrefOrDefault("frontend"),
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
});
|
||||
program.frontend = Boolean(frontend);
|
||||
preferences.frontend = Boolean(frontend);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
program.frontend = false;
|
||||
}
|
||||
|
||||
if (program.framework === "nextjs" || program.frontend) {
|
||||
if (!program.ui) {
|
||||
program.ui = defaults.ui;
|
||||
}
|
||||
}
|
||||
|
||||
if (!program.observability && program.template === "streaming") {
|
||||
if (ciInfo.isCI) {
|
||||
program.observability = getPrefOrDefault("observability");
|
||||
} else {
|
||||
const { observability } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "observability",
|
||||
message: "Would you like to set up observability?",
|
||||
choices: [
|
||||
{ title: "No", value: "none" },
|
||||
...(program.framework === "fastapi"
|
||||
? [{ title: "LlamaTrace", value: "llamatrace" }]
|
||||
: []),
|
||||
{ title: "Traceloop", value: "traceloop" },
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
program.observability = observability;
|
||||
preferences.observability = observability;
|
||||
}
|
||||
}
|
||||
|
||||
if (!program.modelConfig) {
|
||||
const modelConfig = await askModelConfig({
|
||||
openAiKey,
|
||||
askModels: program.askModels ?? false,
|
||||
framework: program.framework,
|
||||
});
|
||||
program.modelConfig = modelConfig;
|
||||
preferences.modelConfig = modelConfig;
|
||||
}
|
||||
|
||||
if (!program.dataSources) {
|
||||
if (ciInfo.isCI) {
|
||||
program.dataSources = getPrefOrDefault("dataSources");
|
||||
} else {
|
||||
program.dataSources = [];
|
||||
// continue asking user for data sources if none are initially provided
|
||||
while (true) {
|
||||
const firstQuestion = program.dataSources.length === 0;
|
||||
const choices = getDataSourceChoices(
|
||||
program.framework,
|
||||
program.dataSources,
|
||||
program.template,
|
||||
);
|
||||
if (choices.length === 0) break;
|
||||
const { selectedSource } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "selectedSource",
|
||||
message: firstQuestion
|
||||
? "Which data source would you like to use?"
|
||||
: "Would you like to add another data source?",
|
||||
choices,
|
||||
initial: firstQuestion ? 1 : 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
if (selectedSource === "no" || selectedSource === "none") {
|
||||
// user doesn't want another data source or any data source
|
||||
break;
|
||||
}
|
||||
switch (selectedSource) {
|
||||
case "exampleFile": {
|
||||
program.dataSources.push(EXAMPLE_FILE);
|
||||
break;
|
||||
}
|
||||
case "file":
|
||||
case "folder": {
|
||||
const selectedPaths = await selectLocalContextData(selectedSource);
|
||||
for (const p of selectedPaths) {
|
||||
program.dataSources.push({
|
||||
type: "file",
|
||||
config: {
|
||||
path: p,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "web": {
|
||||
const { baseUrl } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "baseUrl",
|
||||
message: "Please provide base URL of the website: ",
|
||||
initial: "https://www.llamaindex.ai",
|
||||
validate: (value: string) => {
|
||||
if (!value.includes("://")) {
|
||||
value = `https://${value}`;
|
||||
}
|
||||
const urlObj = new URL(value);
|
||||
if (
|
||||
urlObj.protocol !== "https:" &&
|
||||
urlObj.protocol !== "http:"
|
||||
) {
|
||||
return `URL=${value} has invalid protocol, only allow http or https`;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
program.dataSources.push({
|
||||
type: "web",
|
||||
config: {
|
||||
baseUrl,
|
||||
prefix: baseUrl,
|
||||
depth: 1,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "db": {
|
||||
const dbPrompts: prompts.PromptObject<string>[] = [
|
||||
{
|
||||
type: "text",
|
||||
name: "uri",
|
||||
message:
|
||||
"Please enter the connection string (URI) for the database.",
|
||||
initial: "mysql+pymysql://user:pass@localhost:3306/mydb",
|
||||
validate: (value: string) => {
|
||||
if (!value) {
|
||||
return "Please provide a valid connection string";
|
||||
} else if (
|
||||
!(
|
||||
value.startsWith("mysql+pymysql://") ||
|
||||
value.startsWith("postgresql+psycopg://")
|
||||
)
|
||||
) {
|
||||
return "The connection string must start with 'mysql+pymysql://' for MySQL or 'postgresql+psycopg://' for PostgreSQL";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
// Only ask for a query, user can provide more complex queries in the config file later
|
||||
{
|
||||
type: (prev) => (prev ? "text" : null),
|
||||
name: "queries",
|
||||
message: "Please enter the SQL query to fetch data:",
|
||||
initial: "SELECT * FROM mytable",
|
||||
},
|
||||
];
|
||||
program.dataSources.push({
|
||||
type: "db",
|
||||
config: await prompts(dbPrompts, questionHandlers),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "llamacloud": {
|
||||
program.dataSources.push({
|
||||
type: "llamacloud",
|
||||
config: {},
|
||||
});
|
||||
program.dataSources.push(EXAMPLE_FILE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isUsingLlamaCloud = program.dataSources.some(
|
||||
(ds) => ds.type === "llamacloud",
|
||||
);
|
||||
|
||||
// Asking for LlamaParse if user selected file data source
|
||||
if (isUsingLlamaCloud) {
|
||||
// default to use LlamaParse if using LlamaCloud
|
||||
program.useLlamaParse = preferences.useLlamaParse = true;
|
||||
} else {
|
||||
// Extractor template doesn't support LlamaParse and LlamaCloud right now (cannot use asyncio loop in Reflex)
|
||||
if (
|
||||
program.useLlamaParse === undefined &&
|
||||
program.template !== "extractor"
|
||||
) {
|
||||
// if already set useLlamaParse, don't ask again
|
||||
if (program.dataSources.some((ds) => ds.type === "file")) {
|
||||
if (ciInfo.isCI) {
|
||||
program.useLlamaParse = getPrefOrDefault("useLlamaParse");
|
||||
} else {
|
||||
const { useLlamaParse } = await prompts(
|
||||
{
|
||||
type: "toggle",
|
||||
name: "useLlamaParse",
|
||||
message:
|
||||
"Would you like to use LlamaParse (improved parser for RAG - requires API key)?",
|
||||
initial: false,
|
||||
active: "yes",
|
||||
inactive: "no",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.useLlamaParse = useLlamaParse;
|
||||
preferences.useLlamaParse = useLlamaParse;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ask for LlamaCloud API key when using a LlamaCloud index or LlamaParse
|
||||
if (isUsingLlamaCloud || program.useLlamaParse) {
|
||||
if (!program.llamaCloudKey) {
|
||||
// if already set, don't ask again
|
||||
if (ciInfo.isCI) {
|
||||
program.llamaCloudKey = getPrefOrDefault("llamaCloudKey");
|
||||
} else {
|
||||
// Ask for LlamaCloud API key
|
||||
const { llamaCloudKey } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "llamaCloudKey",
|
||||
message:
|
||||
"Please provide your LlamaCloud API key (leave blank to skip):",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.llamaCloudKey = preferences.llamaCloudKey =
|
||||
llamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isUsingLlamaCloud) {
|
||||
// When using a LlamaCloud index, don't ask for vector database and use code in `llamacloud` folder for vector database
|
||||
const vectorDb = "llamacloud";
|
||||
program.vectorDb = vectorDb;
|
||||
preferences.vectorDb = vectorDb;
|
||||
} else if (program.dataSources.length > 0 && !program.vectorDb) {
|
||||
if (ciInfo.isCI) {
|
||||
program.vectorDb = getPrefOrDefault("vectorDb");
|
||||
} else {
|
||||
const { vectorDb } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "vectorDb",
|
||||
message: "Would you like to use a vector database?",
|
||||
choices: getVectorDbChoices(program.framework),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.vectorDb = vectorDb;
|
||||
preferences.vectorDb = vectorDb;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!program.tools &&
|
||||
(program.template === "streaming" || program.template === "multiagent")
|
||||
) {
|
||||
if (ciInfo.isCI) {
|
||||
program.tools = getPrefOrDefault("tools");
|
||||
} else {
|
||||
const options = supportedTools.filter((t) =>
|
||||
t.supportedFrameworks?.includes(program.framework),
|
||||
);
|
||||
const toolChoices = options.map((tool) => ({
|
||||
title: `${tool.display}${toolRequiresConfig(tool) ? " (needs configuration)" : ""}`,
|
||||
value: tool.name,
|
||||
}));
|
||||
const { toolsName } = await prompts({
|
||||
type: "multiselect",
|
||||
name: "toolsName",
|
||||
message:
|
||||
"Would you like to build an agent using tools? If so, select the tools here, otherwise just press enter",
|
||||
choices: toolChoices,
|
||||
});
|
||||
const tools = toolsName?.map((tool: string) =>
|
||||
supportedTools.find((t) => t.name === tool),
|
||||
);
|
||||
program.tools = tools;
|
||||
preferences.tools = tools;
|
||||
}
|
||||
}
|
||||
|
||||
await askPostInstallAction();
|
||||
};
|
||||
|
||||
export const toChoice = (value: string) => {
|
||||
return { title: value, value };
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { askModelConfig } from "../helpers/providers";
|
||||
import { QuestionArgs, QuestionResults } from "./types";
|
||||
|
||||
const defaults: Omit<QuestionArgs, "modelConfig"> = {
|
||||
template: "streaming",
|
||||
framework: "nextjs",
|
||||
ui: "shadcn",
|
||||
frontend: false,
|
||||
llamaCloudKey: "",
|
||||
useLlamaParse: false,
|
||||
communityProjectConfig: undefined,
|
||||
llamapack: "",
|
||||
postInstallAction: "dependencies",
|
||||
dataSources: [],
|
||||
tools: [],
|
||||
};
|
||||
|
||||
export async function getCIQuestionResults(
|
||||
program: QuestionArgs,
|
||||
): Promise<QuestionResults> {
|
||||
return {
|
||||
...defaults,
|
||||
...program,
|
||||
modelConfig: await askModelConfig({
|
||||
openAiKey: program.openAiKey,
|
||||
askModels: false,
|
||||
framework: program.framework,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
TemplateDataSource,
|
||||
TemplateFramework,
|
||||
TemplateType,
|
||||
} from "../helpers";
|
||||
import { supportedContextFileTypes } from "./utils";
|
||||
|
||||
export const getDataSourceChoices = (
|
||||
framework: TemplateFramework,
|
||||
selectedDataSource: TemplateDataSource[],
|
||||
template?: TemplateType,
|
||||
) => {
|
||||
const choices = [];
|
||||
|
||||
if (selectedDataSource.length > 0) {
|
||||
choices.push({
|
||||
title: "No",
|
||||
value: "no",
|
||||
});
|
||||
}
|
||||
if (selectedDataSource === undefined || selectedDataSource.length === 0) {
|
||||
choices.push({
|
||||
title: "No datasource",
|
||||
value: "none",
|
||||
});
|
||||
choices.push({
|
||||
title:
|
||||
process.platform !== "linux"
|
||||
? "Use an example PDF"
|
||||
: "Use an example PDF (you can add your own data files later)",
|
||||
value: "exampleFile",
|
||||
});
|
||||
}
|
||||
|
||||
// Linux has many distros so we won't support file/folder picker for now
|
||||
if (process.platform !== "linux") {
|
||||
choices.push(
|
||||
{
|
||||
title: `Use local files (${supportedContextFileTypes.join(", ")})`,
|
||||
value: "file",
|
||||
},
|
||||
{
|
||||
title:
|
||||
process.platform === "win32"
|
||||
? "Use a local folder"
|
||||
: "Use local folders",
|
||||
value: "folder",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (framework === "fastapi" && template !== "extractor") {
|
||||
choices.push({
|
||||
title: "Use website content (requires Chrome)",
|
||||
value: "web",
|
||||
});
|
||||
choices.push({
|
||||
title: "Use data from a database (Mysql, PostgreSQL)",
|
||||
value: "db",
|
||||
});
|
||||
}
|
||||
|
||||
return choices;
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import ciInfo from "ci-info";
|
||||
import { getCIQuestionResults } from "./ci";
|
||||
import { askProQuestions } from "./questions";
|
||||
import { askSimpleQuestions } from "./simple";
|
||||
import { QuestionArgs, QuestionResults } from "./types";
|
||||
|
||||
export const askQuestions = async (
|
||||
args: QuestionArgs,
|
||||
): Promise<QuestionResults> => {
|
||||
if (ciInfo.isCI || process.env.PLAYWRIGHT_TEST === "1") {
|
||||
return await getCIQuestionResults(args);
|
||||
} else if (args.pro) {
|
||||
// TODO: refactor pro questions to return a result object
|
||||
await askProQuestions(args);
|
||||
return args as unknown as QuestionResults;
|
||||
}
|
||||
return await askSimpleQuestions(args);
|
||||
};
|
||||
@@ -0,0 +1,432 @@
|
||||
import { blue, green } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { COMMUNITY_OWNER, COMMUNITY_REPO } from "../helpers/constant";
|
||||
import { EXAMPLE_FILE } from "../helpers/datasources";
|
||||
import { getAvailableLlamapackOptions } from "../helpers/llama-pack";
|
||||
import { askModelConfig } from "../helpers/providers";
|
||||
import { getProjectOptions } from "../helpers/repo";
|
||||
import { supportedTools, toolRequiresConfig } from "../helpers/tools";
|
||||
import { getDataSourceChoices } from "./datasources";
|
||||
import { getVectorDbChoices } from "./stores";
|
||||
import { QuestionArgs } from "./types";
|
||||
import {
|
||||
askPostInstallAction,
|
||||
onPromptState,
|
||||
questionHandlers,
|
||||
selectLocalContextData,
|
||||
} from "./utils";
|
||||
|
||||
export const askProQuestions = async (program: QuestionArgs) => {
|
||||
if (!program.template) {
|
||||
const styledRepo = blue(
|
||||
`https://github.com/${COMMUNITY_OWNER}/${COMMUNITY_REPO}`,
|
||||
);
|
||||
const { template } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "template",
|
||||
message: "Which template would you like to use?",
|
||||
choices: [
|
||||
{ title: "Agentic RAG (e.g. chat with docs)", value: "streaming" },
|
||||
{
|
||||
title: "Multi-agent app (using workflows)",
|
||||
value: "multiagent",
|
||||
},
|
||||
{ title: "Structured Extractor", value: "extractor" },
|
||||
{
|
||||
title: `Community template from ${styledRepo}`,
|
||||
value: "community",
|
||||
},
|
||||
{
|
||||
title: "Example using a LlamaPack",
|
||||
value: "llamapack",
|
||||
},
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.template = template;
|
||||
}
|
||||
|
||||
if (program.template === "community") {
|
||||
const projectOptions = await getProjectOptions(
|
||||
COMMUNITY_OWNER,
|
||||
COMMUNITY_REPO,
|
||||
);
|
||||
const { communityProjectConfig } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "communityProjectConfig",
|
||||
message: "Select community template",
|
||||
choices: projectOptions.map(({ title, value }) => ({
|
||||
title,
|
||||
value: JSON.stringify(value), // serialize value to string in terminal
|
||||
})),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
const projectConfig = JSON.parse(communityProjectConfig);
|
||||
program.communityProjectConfig = projectConfig;
|
||||
return; // early return - no further questions needed for community projects
|
||||
}
|
||||
|
||||
if (program.template === "llamapack") {
|
||||
const availableLlamaPacks = await getAvailableLlamapackOptions();
|
||||
const { llamapack } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "llamapack",
|
||||
message: "Select LlamaPack",
|
||||
choices: availableLlamaPacks.map((pack) => ({
|
||||
title: pack.name,
|
||||
value: pack.folderPath,
|
||||
})),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.llamapack = llamapack;
|
||||
if (!program.postInstallAction) {
|
||||
program.postInstallAction = await askPostInstallAction(program);
|
||||
}
|
||||
return; // early return - no further questions needed for llamapack projects
|
||||
}
|
||||
|
||||
if (program.template === "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];
|
||||
program.framework = "fastapi";
|
||||
}
|
||||
|
||||
if (!program.framework) {
|
||||
const choices = [
|
||||
{ title: "NextJS", value: "nextjs" },
|
||||
{ title: "Express", value: "express" },
|
||||
{ title: "FastAPI (Python)", value: "fastapi" },
|
||||
];
|
||||
|
||||
const { framework } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "framework",
|
||||
message: "Which framework would you like to use?",
|
||||
choices,
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.framework = framework;
|
||||
}
|
||||
|
||||
if (
|
||||
(program.framework === "express" || program.framework === "fastapi") &&
|
||||
(program.template === "streaming" || program.template === "multiagent")
|
||||
) {
|
||||
// if a backend-only framework is selected, ask whether we should create a frontend
|
||||
if (program.frontend === undefined) {
|
||||
const styledNextJS = blue("NextJS");
|
||||
const styledBackend = green(
|
||||
program.framework === "express"
|
||||
? "Express "
|
||||
: program.framework === "fastapi"
|
||||
? "FastAPI (Python) "
|
||||
: "",
|
||||
);
|
||||
const { frontend } = await prompts({
|
||||
onState: onPromptState,
|
||||
type: "toggle",
|
||||
name: "frontend",
|
||||
message: `Would you like to generate a ${styledNextJS} frontend for your ${styledBackend}backend?`,
|
||||
initial: false,
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
});
|
||||
program.frontend = Boolean(frontend);
|
||||
}
|
||||
} else {
|
||||
program.frontend = false;
|
||||
}
|
||||
|
||||
if (program.framework === "nextjs" || program.frontend) {
|
||||
if (!program.ui) {
|
||||
program.ui = "shadcn";
|
||||
}
|
||||
}
|
||||
|
||||
if (!program.observability && program.template === "streaming") {
|
||||
const { observability } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "observability",
|
||||
message: "Would you like to set up observability?",
|
||||
choices: [
|
||||
{ title: "No", value: "none" },
|
||||
...(program.framework === "fastapi"
|
||||
? [{ title: "LlamaTrace", value: "llamatrace" }]
|
||||
: []),
|
||||
{ title: "Traceloop", value: "traceloop" },
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
program.observability = observability;
|
||||
}
|
||||
|
||||
// Ask agents
|
||||
if (program.template === "multiagent" && !program.agents) {
|
||||
const { agents } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "agents",
|
||||
message: "Which agents would you like to use?",
|
||||
choices: [
|
||||
{
|
||||
title: "Financial report (generate a financial report)",
|
||||
value: "financial_report",
|
||||
},
|
||||
{
|
||||
title: "Form filling (fill missing value in a CSV file)",
|
||||
value: "form_filling",
|
||||
},
|
||||
{
|
||||
title: "Blog writer (Write a blog post)",
|
||||
value: "blog_writer",
|
||||
},
|
||||
],
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.agents = agents;
|
||||
}
|
||||
|
||||
if (!program.modelConfig) {
|
||||
const modelConfig = await askModelConfig({
|
||||
openAiKey: program.openAiKey,
|
||||
askModels: program.askModels ?? false,
|
||||
framework: program.framework,
|
||||
});
|
||||
program.modelConfig = modelConfig;
|
||||
}
|
||||
|
||||
if (!program.vectorDb) {
|
||||
const { vectorDb } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "vectorDb",
|
||||
message: "Would you like to use a vector database?",
|
||||
choices: getVectorDbChoices(program.framework),
|
||||
initial: 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.vectorDb = vectorDb;
|
||||
}
|
||||
|
||||
if (program.vectorDb === "llamacloud") {
|
||||
// When using a LlamaCloud index, don't ask for data sources just copy an example file
|
||||
program.dataSources = [EXAMPLE_FILE];
|
||||
}
|
||||
|
||||
if (!program.dataSources) {
|
||||
program.dataSources = [];
|
||||
// continue asking user for data sources if none are initially provided
|
||||
while (true) {
|
||||
const firstQuestion = program.dataSources.length === 0;
|
||||
const choices = getDataSourceChoices(
|
||||
program.framework,
|
||||
program.dataSources,
|
||||
program.template,
|
||||
);
|
||||
if (choices.length === 0) break;
|
||||
const { selectedSource } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "selectedSource",
|
||||
message: firstQuestion
|
||||
? "Which data source would you like to use?"
|
||||
: "Would you like to add another data source?",
|
||||
choices,
|
||||
initial: firstQuestion ? 1 : 0,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
if (selectedSource === "no" || selectedSource === "none") {
|
||||
// user doesn't want another data source or any data source
|
||||
break;
|
||||
}
|
||||
switch (selectedSource) {
|
||||
case "exampleFile": {
|
||||
program.dataSources.push(EXAMPLE_FILE);
|
||||
break;
|
||||
}
|
||||
case "file":
|
||||
case "folder": {
|
||||
const selectedPaths = await selectLocalContextData(selectedSource);
|
||||
for (const p of selectedPaths) {
|
||||
program.dataSources.push({
|
||||
type: "file",
|
||||
config: {
|
||||
path: p,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "web": {
|
||||
const { baseUrl } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "baseUrl",
|
||||
message: "Please provide base URL of the website: ",
|
||||
initial: "https://www.llamaindex.ai",
|
||||
validate: (value: string) => {
|
||||
if (!value.includes("://")) {
|
||||
value = `https://${value}`;
|
||||
}
|
||||
const urlObj = new URL(value);
|
||||
if (
|
||||
urlObj.protocol !== "https:" &&
|
||||
urlObj.protocol !== "http:"
|
||||
) {
|
||||
return `URL=${value} has invalid protocol, only allow http or https`;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
program.dataSources.push({
|
||||
type: "web",
|
||||
config: {
|
||||
baseUrl,
|
||||
prefix: baseUrl,
|
||||
depth: 1,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "db": {
|
||||
const dbPrompts: prompts.PromptObject<string>[] = [
|
||||
{
|
||||
type: "text",
|
||||
name: "uri",
|
||||
message:
|
||||
"Please enter the connection string (URI) for the database.",
|
||||
initial: "mysql+pymysql://user:pass@localhost:3306/mydb",
|
||||
validate: (value: string) => {
|
||||
if (!value) {
|
||||
return "Please provide a valid connection string";
|
||||
} else if (
|
||||
!(
|
||||
value.startsWith("mysql+pymysql://") ||
|
||||
value.startsWith("postgresql+psycopg://")
|
||||
)
|
||||
) {
|
||||
return "The connection string must start with 'mysql+pymysql://' for MySQL or 'postgresql+psycopg://' for PostgreSQL";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
// Only ask for a query, user can provide more complex queries in the config file later
|
||||
{
|
||||
type: (prev) => (prev ? "text" : null),
|
||||
name: "queries",
|
||||
message: "Please enter the SQL query to fetch data:",
|
||||
initial: "SELECT * FROM mytable",
|
||||
},
|
||||
];
|
||||
program.dataSources.push({
|
||||
type: "db",
|
||||
config: await prompts(dbPrompts, questionHandlers),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isUsingLlamaCloud = program.vectorDb === "llamacloud";
|
||||
|
||||
// Asking for LlamaParse if user selected file data source
|
||||
if (isUsingLlamaCloud) {
|
||||
// default to use LlamaParse if using LlamaCloud
|
||||
program.useLlamaParse = true;
|
||||
} else {
|
||||
// Extractor template doesn't support LlamaParse and LlamaCloud right now (cannot use asyncio loop in Reflex)
|
||||
if (
|
||||
program.useLlamaParse === undefined &&
|
||||
program.template !== "extractor"
|
||||
) {
|
||||
// if already set useLlamaParse, don't ask again
|
||||
if (program.dataSources.some((ds) => ds.type === "file")) {
|
||||
const { useLlamaParse } = await prompts(
|
||||
{
|
||||
type: "toggle",
|
||||
name: "useLlamaParse",
|
||||
message:
|
||||
"Would you like to use LlamaParse (improved parser for RAG - requires API key)?",
|
||||
initial: false,
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.useLlamaParse = useLlamaParse;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ask for LlamaCloud API key when using a LlamaCloud index or LlamaParse
|
||||
if (isUsingLlamaCloud || program.useLlamaParse) {
|
||||
if (!program.llamaCloudKey) {
|
||||
// if already set, don't ask again
|
||||
// Ask for LlamaCloud API key
|
||||
const { llamaCloudKey } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "llamaCloudKey",
|
||||
message:
|
||||
"Please provide your LlamaCloud API key (leave blank to skip):",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
program.llamaCloudKey = llamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!program.tools &&
|
||||
(program.template === "streaming" || program.template === "multiagent")
|
||||
) {
|
||||
const options = supportedTools.filter((t) =>
|
||||
t.supportedFrameworks?.includes(program.framework),
|
||||
);
|
||||
const toolChoices = options.map((tool) => ({
|
||||
title: `${tool.display}${toolRequiresConfig(tool) ? " (needs configuration)" : ""}`,
|
||||
value: tool.name,
|
||||
}));
|
||||
const { toolsName } = await prompts({
|
||||
type: "multiselect",
|
||||
name: "toolsName",
|
||||
message:
|
||||
"Would you like to build an agent using tools? If so, select the tools here, otherwise just press enter",
|
||||
choices: toolChoices,
|
||||
});
|
||||
const tools = toolsName?.map((tool: string) =>
|
||||
supportedTools.find((t) => t.name === tool),
|
||||
);
|
||||
program.tools = tools;
|
||||
}
|
||||
|
||||
if (!program.postInstallAction) {
|
||||
program.postInstallAction = await askPostInstallAction(program);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,197 @@
|
||||
import prompts from "prompts";
|
||||
import { EXAMPLE_10K_SEC_FILES, EXAMPLE_FILE } from "../helpers/datasources";
|
||||
import { askModelConfig } from "../helpers/providers";
|
||||
import { getTools } from "../helpers/tools";
|
||||
import { ModelConfig, TemplateFramework } from "../helpers/types";
|
||||
import { PureQuestionArgs, QuestionResults } from "./types";
|
||||
import { askPostInstallAction, questionHandlers } from "./utils";
|
||||
|
||||
type AppType =
|
||||
| "rag"
|
||||
| "code_artifact"
|
||||
| "financial_report_agent"
|
||||
| "form_filling"
|
||||
| "extractor"
|
||||
| "data_scientist";
|
||||
|
||||
type SimpleAnswers = {
|
||||
appType: AppType;
|
||||
language: TemplateFramework;
|
||||
useLlamaCloud: boolean;
|
||||
llamaCloudKey?: string;
|
||||
};
|
||||
|
||||
export const askSimpleQuestions = async (
|
||||
args: PureQuestionArgs,
|
||||
): Promise<QuestionResults> => {
|
||||
const { appType } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "appType",
|
||||
message: "What app do you want to build?",
|
||||
choices: [
|
||||
{ title: "Agentic RAG", value: "rag" },
|
||||
{ title: "Data Scientist", value: "data_scientist" },
|
||||
{
|
||||
title: "Financial Report Generator (using Workflows)",
|
||||
value: "financial_report_agent",
|
||||
},
|
||||
{
|
||||
title: "Form Filler (using Workflows)",
|
||||
value: "form_filling",
|
||||
},
|
||||
{ title: "Code Artifact Agent", value: "code_artifact" },
|
||||
{ title: "Information Extractor", value: "extractor" },
|
||||
],
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
let language: TemplateFramework = "fastapi";
|
||||
let llamaCloudKey = args.llamaCloudKey;
|
||||
let useLlamaCloud = false;
|
||||
|
||||
if (appType !== "extractor") {
|
||||
// TODO: Add TS support for form filling use case
|
||||
if (appType !== "form_filling") {
|
||||
const { language: newLanguage } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "language",
|
||||
message: "What language do you want to use?",
|
||||
choices: [
|
||||
{ title: "Python (FastAPI)", value: "fastapi" },
|
||||
{ title: "Typescript (NextJS)", value: "nextjs" },
|
||||
],
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
language = newLanguage;
|
||||
}
|
||||
|
||||
const { useLlamaCloud: newUseLlamaCloud } = await prompts(
|
||||
{
|
||||
type: "toggle",
|
||||
name: "useLlamaCloud",
|
||||
message: "Do you want to use LlamaCloud services?",
|
||||
initial: false,
|
||||
active: "Yes",
|
||||
inactive: "No",
|
||||
hint: "see https://www.llamaindex.ai/enterprise for more info",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
useLlamaCloud = newUseLlamaCloud;
|
||||
|
||||
if (useLlamaCloud && !llamaCloudKey) {
|
||||
// Ask for LlamaCloud API key, if not set
|
||||
const { llamaCloudKey: newLlamaCloudKey } = await prompts(
|
||||
{
|
||||
type: "text",
|
||||
name: "llamaCloudKey",
|
||||
message:
|
||||
"Please provide your LlamaCloud API key (leave blank to skip):",
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
llamaCloudKey = newLlamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
|
||||
}
|
||||
}
|
||||
|
||||
const results = await convertAnswers(args, {
|
||||
appType,
|
||||
language,
|
||||
useLlamaCloud,
|
||||
llamaCloudKey,
|
||||
});
|
||||
|
||||
results.postInstallAction = await askPostInstallAction(results);
|
||||
return results;
|
||||
};
|
||||
|
||||
const convertAnswers = async (
|
||||
args: PureQuestionArgs,
|
||||
answers: SimpleAnswers,
|
||||
): Promise<QuestionResults> => {
|
||||
const MODEL_GPT4o: ModelConfig = {
|
||||
provider: "openai",
|
||||
apiKey: args.openAiKey,
|
||||
model: "gpt-4o",
|
||||
embeddingModel: "text-embedding-3-large",
|
||||
dimensions: 1536,
|
||||
isConfigured(): boolean {
|
||||
return !!args.openAiKey;
|
||||
},
|
||||
};
|
||||
const lookup: Record<
|
||||
AppType,
|
||||
Pick<
|
||||
QuestionResults,
|
||||
"template" | "tools" | "frontend" | "dataSources" | "agents"
|
||||
> & {
|
||||
modelConfig?: ModelConfig;
|
||||
}
|
||||
> = {
|
||||
rag: {
|
||||
template: "streaming",
|
||||
tools: getTools(["duckduckgo"]),
|
||||
frontend: true,
|
||||
dataSources: [EXAMPLE_FILE],
|
||||
},
|
||||
data_scientist: {
|
||||
template: "streaming",
|
||||
tools: getTools(["interpreter", "document_generator"]),
|
||||
frontend: true,
|
||||
dataSources: [],
|
||||
modelConfig: MODEL_GPT4o,
|
||||
},
|
||||
code_artifact: {
|
||||
template: "streaming",
|
||||
tools: getTools(["artifact"]),
|
||||
frontend: true,
|
||||
dataSources: [],
|
||||
modelConfig: MODEL_GPT4o,
|
||||
},
|
||||
financial_report_agent: {
|
||||
template: "multiagent",
|
||||
agents: "financial_report",
|
||||
tools: getTools(["document_generator", "interpreter"]),
|
||||
dataSources: EXAMPLE_10K_SEC_FILES,
|
||||
frontend: true,
|
||||
modelConfig: MODEL_GPT4o,
|
||||
},
|
||||
form_filling: {
|
||||
template: "multiagent",
|
||||
agents: "form_filling",
|
||||
tools: getTools(["form_filling"]),
|
||||
dataSources: EXAMPLE_10K_SEC_FILES,
|
||||
frontend: true,
|
||||
modelConfig: MODEL_GPT4o,
|
||||
},
|
||||
extractor: {
|
||||
template: "extractor",
|
||||
tools: [],
|
||||
frontend: false,
|
||||
dataSources: [EXAMPLE_FILE],
|
||||
},
|
||||
};
|
||||
const results = lookup[answers.appType];
|
||||
return {
|
||||
framework: answers.language,
|
||||
ui: "shadcn",
|
||||
llamaCloudKey: answers.llamaCloudKey,
|
||||
useLlamaParse: answers.useLlamaCloud,
|
||||
llamapack: "",
|
||||
vectorDb: answers.useLlamaCloud ? "llamacloud" : "none",
|
||||
observability: "none",
|
||||
...results,
|
||||
modelConfig:
|
||||
results.modelConfig ??
|
||||
(await askModelConfig({
|
||||
openAiKey: args.openAiKey,
|
||||
askModels: args.askModels ?? false,
|
||||
framework: answers.language,
|
||||
})),
|
||||
frontend: answers.language === "nextjs" ? false : results.frontend,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { TemplateFramework } from "../helpers";
|
||||
import { templatesDir } from "../helpers/dir";
|
||||
|
||||
export const getVectorDbChoices = (framework: TemplateFramework) => {
|
||||
const choices = [
|
||||
{
|
||||
title: "No, just store the data in the file system",
|
||||
value: "none",
|
||||
},
|
||||
{ title: "MongoDB", value: "mongo" },
|
||||
{ title: "PostgreSQL", value: "pg" },
|
||||
{ title: "Pinecone", value: "pinecone" },
|
||||
{ title: "Milvus", value: "milvus" },
|
||||
{ title: "Astra", value: "astra" },
|
||||
{ title: "Qdrant", value: "qdrant" },
|
||||
{ title: "ChromaDB", value: "chroma" },
|
||||
{ title: "Weaviate", value: "weaviate" },
|
||||
{ title: "LlamaCloud (use Managed Index)", value: "llamacloud" },
|
||||
];
|
||||
|
||||
const vectordbLang = framework === "fastapi" ? "python" : "typescript";
|
||||
const compPath = path.join(templatesDir, "components");
|
||||
const vectordbPath = path.join(compPath, "vectordbs", vectordbLang);
|
||||
|
||||
const availableChoices = fs
|
||||
.readdirSync(vectordbPath)
|
||||
.filter((file) => fs.statSync(path.join(vectordbPath, file)).isDirectory());
|
||||
|
||||
const displayedChoices = choices.filter((choice) =>
|
||||
availableChoices.includes(choice.value),
|
||||
);
|
||||
|
||||
return displayedChoices;
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { InstallAppArgs } from "../create-app";
|
||||
|
||||
export type QuestionResults = Omit<
|
||||
InstallAppArgs,
|
||||
"appPath" | "packageManager" | "externalPort"
|
||||
>;
|
||||
|
||||
export type PureQuestionArgs = {
|
||||
askModels?: boolean;
|
||||
pro?: boolean;
|
||||
openAiKey?: string;
|
||||
llamaCloudKey?: string;
|
||||
};
|
||||
|
||||
export type QuestionArgs = QuestionResults & PureQuestionArgs;
|
||||
@@ -0,0 +1,178 @@
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { red } from "picocolors";
|
||||
import prompts from "prompts";
|
||||
import { TemplateDataSourceType, TemplatePostInstallAction } from "../helpers";
|
||||
import { toolsRequireConfig } from "../helpers/tools";
|
||||
import { QuestionResults } from "./types";
|
||||
|
||||
export const supportedContextFileTypes = [
|
||||
".pdf",
|
||||
".doc",
|
||||
".docx",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".csv",
|
||||
];
|
||||
|
||||
const MACOS_FILE_SELECTION_SCRIPT = `
|
||||
osascript -l JavaScript -e '
|
||||
a = Application.currentApplication();
|
||||
a.includeStandardAdditions = true;
|
||||
a.chooseFile({ withPrompt: "Please select files to process:", multipleSelectionsAllowed: true }).map(file => file.toString())
|
||||
'`;
|
||||
|
||||
const MACOS_FOLDER_SELECTION_SCRIPT = `
|
||||
osascript -l JavaScript -e '
|
||||
a = Application.currentApplication();
|
||||
a.includeStandardAdditions = true;
|
||||
a.chooseFolder({ withPrompt: "Please select folders to process:", multipleSelectionsAllowed: true }).map(folder => folder.toString())
|
||||
'`;
|
||||
|
||||
const WINDOWS_FILE_SELECTION_SCRIPT = `
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
$openFileDialog = New-Object System.Windows.Forms.OpenFileDialog
|
||||
$openFileDialog.InitialDirectory = [Environment]::GetFolderPath('Desktop')
|
||||
$openFileDialog.Multiselect = $true
|
||||
$result = $openFileDialog.ShowDialog()
|
||||
if ($result -eq 'OK') {
|
||||
$openFileDialog.FileNames
|
||||
}
|
||||
`;
|
||||
|
||||
const WINDOWS_FOLDER_SELECTION_SCRIPT = `
|
||||
Add-Type -AssemblyName System.windows.forms
|
||||
$folderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog
|
||||
$dialogResult = $folderBrowser.ShowDialog()
|
||||
if ($dialogResult -eq [System.Windows.Forms.DialogResult]::OK)
|
||||
{
|
||||
$folderBrowser.SelectedPath
|
||||
}
|
||||
`;
|
||||
|
||||
export const selectLocalContextData = async (type: TemplateDataSourceType) => {
|
||||
try {
|
||||
let selectedPath: string = "";
|
||||
let execScript: string;
|
||||
let execOpts: any = {};
|
||||
switch (process.platform) {
|
||||
case "win32": // Windows
|
||||
execScript =
|
||||
type === "file"
|
||||
? WINDOWS_FILE_SELECTION_SCRIPT
|
||||
: WINDOWS_FOLDER_SELECTION_SCRIPT;
|
||||
execOpts = { shell: "powershell.exe" };
|
||||
break;
|
||||
case "darwin": // MacOS
|
||||
execScript =
|
||||
type === "file"
|
||||
? MACOS_FILE_SELECTION_SCRIPT
|
||||
: MACOS_FOLDER_SELECTION_SCRIPT;
|
||||
break;
|
||||
default: // Unsupported OS
|
||||
console.log(red("Unsupported OS error!"));
|
||||
process.exit(1);
|
||||
}
|
||||
selectedPath = execSync(execScript, execOpts).toString().trim();
|
||||
const paths =
|
||||
process.platform === "win32"
|
||||
? selectedPath.split("\r\n")
|
||||
: selectedPath.split(", ");
|
||||
|
||||
for (const p of paths) {
|
||||
if (
|
||||
fs.statSync(p).isFile() &&
|
||||
!supportedContextFileTypes.includes(path.extname(p))
|
||||
) {
|
||||
console.log(
|
||||
red(
|
||||
`Please select a supported file type: ${supportedContextFileTypes}`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
red(
|
||||
"Got an error when trying to select local context data! Please try again or select another data source option.",
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
export const onPromptState = (state: any) => {
|
||||
if (state.aborted) {
|
||||
// If we don't re-enable the terminal cursor before exiting
|
||||
// the program, the cursor will remain hidden
|
||||
process.stdout.write("\x1B[?25h");
|
||||
process.stdout.write("\n");
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
export const toChoice = (value: string) => {
|
||||
return { title: value, value };
|
||||
};
|
||||
|
||||
export const questionHandlers = {
|
||||
onCancel: () => {
|
||||
console.error("Exiting.");
|
||||
process.exit(1);
|
||||
},
|
||||
};
|
||||
|
||||
// Ask for next action after installation
|
||||
export async function askPostInstallAction(
|
||||
args: QuestionResults,
|
||||
): Promise<TemplatePostInstallAction> {
|
||||
const actionChoices = [
|
||||
{
|
||||
title: "Just generate code (~1 sec)",
|
||||
value: "none",
|
||||
},
|
||||
{
|
||||
title: "Start in VSCode (~1 sec)",
|
||||
value: "VSCode",
|
||||
},
|
||||
{
|
||||
title: "Generate code and install dependencies (~2 min)",
|
||||
value: "dependencies",
|
||||
},
|
||||
];
|
||||
|
||||
const modelConfigured = !args.llamapack && args.modelConfig.isConfigured();
|
||||
// If using LlamaParse, require LlamaCloud API key
|
||||
const llamaCloudKeyConfigured = args.useLlamaParse
|
||||
? args.llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
|
||||
: true;
|
||||
const hasVectorDb = args.vectorDb && args.vectorDb !== "none";
|
||||
// Can run the app if all tools do not require configuration
|
||||
if (
|
||||
!hasVectorDb &&
|
||||
modelConfigured &&
|
||||
llamaCloudKeyConfigured &&
|
||||
!toolsRequireConfig(args.tools)
|
||||
) {
|
||||
actionChoices.push({
|
||||
title: "Generate code, install dependencies, and run the app (~2 min)",
|
||||
value: "runApp",
|
||||
});
|
||||
}
|
||||
|
||||
const { action } = await prompts(
|
||||
{
|
||||
type: "select",
|
||||
name: "action",
|
||||
message: "How would you like to proceed?",
|
||||
choices: actionChoices,
|
||||
initial: 1,
|
||||
},
|
||||
questionHandlers,
|
||||
);
|
||||
|
||||
return action;
|
||||
}
|
||||
+4
-9
@@ -1,5 +1,3 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) multi-agents project using [Workflows](https://docs.llamaindex.ai/en/stable/understanding/workflows/).
|
||||
|
||||
## Overview
|
||||
|
||||
This example is using three agents to generate a blog post:
|
||||
@@ -10,9 +8,9 @@ This example is using three agents to generate a blog post:
|
||||
|
||||
There are three different methods how the agents can interact to reach their goal:
|
||||
|
||||
1. [Choreography](./app/examples/choreography.py) - the agents decide themselves to delegate a task to another agent
|
||||
1. [Orchestrator](./app/examples/orchestrator.py) - a central orchestrator decides which agent should execute a task
|
||||
1. [Explicit Workflow](./app/examples/workflow.py) - a pre-defined workflow specific for the task is used to execute the tasks
|
||||
1. [Choreography](./app/agents/choreography.py) - the agents decide themselves to delegate a task to another agent
|
||||
1. [Orchestrator](./app/agents/orchestrator.py) - a central orchestrator decides which agent should execute a task
|
||||
1. [Explicit Workflow](./app/agents/workflow.py) - a pre-defined workflow specific for the task is used to execute the tasks
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -25,7 +23,6 @@ poetry install
|
||||
```
|
||||
|
||||
Then check the parameters that have been pre-configured in the `.env` file in this directory. (E.g. you might need to configure an `OPENAI_API_KEY` if you're using OpenAI as model provider).
|
||||
|
||||
Second, generate the embeddings of the documents in the `./data` directory:
|
||||
|
||||
```shell
|
||||
@@ -39,7 +36,6 @@ poetry run python main.py
|
||||
```
|
||||
|
||||
Per default, the example is using the explicit workflow. You can change the example by setting the `EXAMPLE_TYPE` environment variable to `choreography` or `orchestrator`.
|
||||
|
||||
The example provides one streaming API endpoint `/api/chat`.
|
||||
You can test the endpoint with the following curl request:
|
||||
|
||||
@@ -65,5 +61,4 @@ To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
|
||||
- [Workflows Introduction](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - learn about LlamaIndex workflows.
|
||||
|
||||
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
|
||||
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
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 app.agents.publisher import create_publisher
|
||||
from app.agents.researcher import create_researcher
|
||||
from app.workflows.multi import AgentCallingAgent
|
||||
from app.workflows.single import FunctionCallingAgent
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
|
||||
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
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 app.agents.publisher import create_publisher
|
||||
from app.agents.researcher import create_researcher
|
||||
from app.workflows.multi import AgentOrchestrator
|
||||
from app.workflows.single import FunctionCallingAgent
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
from textwrap import dedent
|
||||
from typing import List, Tuple
|
||||
|
||||
from app.agents.single import FunctionCallingAgent
|
||||
from app.engine.tools import ToolFactory
|
||||
from app.workflows.single import FunctionCallingAgent
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.tools import FunctionTool
|
||||
|
||||
@@ -11,11 +11,11 @@ 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"])
|
||||
if "generate_document" in configured_tools.keys():
|
||||
tools.append(configured_tools["generate_document"])
|
||||
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.
|
||||
But if user requested to generate a file, use the generate_document 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:
|
||||
+7
-3
@@ -2,9 +2,9 @@ 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 app.workflows.single import FunctionCallingAgent
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.tools import QueryEngineTool, ToolMetadata
|
||||
|
||||
@@ -42,11 +42,15 @@ def _get_research_tools(**kwargs) -> QueryEngineTool:
|
||||
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"]
|
||||
researcher_tool_names = [
|
||||
"duckduckgo_search",
|
||||
"duckduckgo_image_search",
|
||||
"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)
|
||||
tools.append(tool)
|
||||
return tools
|
||||
|
||||
|
||||
+6
-4
@@ -1,9 +1,9 @@
|
||||
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 app.agents.publisher import create_publisher
|
||||
from app.agents.researcher import create_researcher
|
||||
from app.workflows.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.prompts import PromptTemplate
|
||||
from llama_index.core.settings import Settings
|
||||
@@ -238,7 +238,9 @@ class BlogPostWorkflow(Workflow):
|
||||
publisher: FunctionCallingAgent,
|
||||
) -> StopEvent:
|
||||
try:
|
||||
result: AgentRunResult = await self.run_agent(ctx, publisher, ev.input)
|
||||
result: AgentRunResult = await self.run_agent(
|
||||
ctx, publisher, ev.input, streaming=ctx.data["streaming"]
|
||||
)
|
||||
return StopEvent(result=result)
|
||||
except Exception as e:
|
||||
ctx.write_event_to_stream(
|
||||
@@ -0,0 +1,3 @@
|
||||
from .blog import create_workflow
|
||||
|
||||
__all__ = ["create_workflow"]
|
||||
+7
-6
@@ -2,19 +2,20 @@ 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 app.agents.choreography import create_choreography
|
||||
from app.agents.orchestrator import create_orchestrator
|
||||
from app.agents.workflow import create_workflow as create_blog_workflow
|
||||
from llama_index.core.chat_engine.types import ChatMessage
|
||||
from llama_index.core.workflow import Workflow
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
def get_chat_engine(
|
||||
def create_workflow(
|
||||
chat_history: Optional[List[ChatMessage]] = None, **kwargs
|
||||
) -> Workflow:
|
||||
# TODO: the EXAMPLE_TYPE could be passed as a chat config parameter?
|
||||
# Chat filters are not supported yet
|
||||
kwargs.pop("filters", None)
|
||||
agent_type = os.getenv("EXAMPLE_TYPE", "").lower()
|
||||
match agent_type:
|
||||
case "choreography":
|
||||
@@ -22,7 +23,7 @@ def get_chat_engine(
|
||||
case "orchestrator":
|
||||
agent = create_orchestrator(chat_history, **kwargs)
|
||||
case _:
|
||||
agent = create_workflow(chat_history, **kwargs)
|
||||
agent = create_blog_workflow(chat_history, **kwargs)
|
||||
|
||||
logger.info(f"Using agent pattern: {agent_type}")
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
from typing import Any, List
|
||||
|
||||
from app.agents.planner import StructuredPlannerAgent
|
||||
from app.agents.single import (
|
||||
from app.workflows.planner import StructuredPlannerAgent
|
||||
from app.workflows.single import (
|
||||
AgentRunResult,
|
||||
ContextAwareTool,
|
||||
FunctionCallingAgent,
|
||||
+1
-1
@@ -2,7 +2,7 @@ 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 app.workflows.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent
|
||||
from llama_index.core.agent.runner.planner import (
|
||||
DEFAULT_INITIAL_PLAN_PROMPT,
|
||||
DEFAULT_PLAN_REFINE_PROMPT,
|
||||
+20
-9
@@ -1,4 +1,5 @@
|
||||
from abc import abstractmethod
|
||||
from enum import Enum
|
||||
from typing import Any, AsyncGenerator, List, Optional
|
||||
|
||||
from llama_index.core.llms import ChatMessage, ChatResponse
|
||||
@@ -15,7 +16,7 @@ from llama_index.core.workflow import (
|
||||
Workflow,
|
||||
step,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class InputEvent(Event):
|
||||
@@ -26,17 +27,27 @@ class ToolCallEvent(Event):
|
||||
tool_calls: list[ToolSelection]
|
||||
|
||||
|
||||
class AgentRunEventType(Enum):
|
||||
TEXT = "text"
|
||||
PROGRESS = "progress"
|
||||
|
||||
|
||||
class AgentRunEvent(Event):
|
||||
name: str
|
||||
_msg: str
|
||||
msg: str
|
||||
event_type: AgentRunEventType = Field(default=AgentRunEventType.TEXT)
|
||||
data: Optional[dict] = None
|
||||
|
||||
@property
|
||||
def msg(self):
|
||||
return self._msg
|
||||
|
||||
@msg.setter
|
||||
def msg(self, value):
|
||||
self._msg = value
|
||||
def to_response(self) -> dict:
|
||||
return {
|
||||
"type": "agent",
|
||||
"data": {
|
||||
"agent": self.name,
|
||||
"type": self.event_type.value,
|
||||
"text": self.msg,
|
||||
"data": self.data,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class AgentRunResult(BaseModel):
|
||||
@@ -0,0 +1,53 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) multi-agents project using [Workflows](https://docs.llamaindex.ai/en/stable/understanding/workflows/).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, setup the environment with poetry:
|
||||
|
||||
> **_Note:_** This step is not needed if you are using the dev-container.
|
||||
|
||||
```shell
|
||||
poetry install
|
||||
```
|
||||
|
||||
Then check the parameters that have been pre-configured in the `.env` file in this directory. (E.g. you might need to configure an `OPENAI_API_KEY` if you're using OpenAI as model provider and `E2B_API_KEY` for the [E2B's code interpreter tool](https://e2b.dev/docs)).
|
||||
|
||||
Second, generate the embeddings of the documents in the `./data` directory:
|
||||
|
||||
```shell
|
||||
poetry run generate
|
||||
```
|
||||
|
||||
Third, run the development server:
|
||||
|
||||
```shell
|
||||
poetry run python main.py
|
||||
```
|
||||
|
||||
The example provides one streaming API endpoint `/api/chat`.
|
||||
You can test the endpoint with the following curl request:
|
||||
|
||||
```
|
||||
curl --location 'localhost:8000/api/chat' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{ "messages": [{ "role": "user", "content": "Create a report comparing the finances of Apple and Tesla" }] }'
|
||||
```
|
||||
|
||||
You can start editing the API by modifying `app/api/routers/chat.py` or `app/workflows/financial_report.py`. The API auto-updates as you save the files.
|
||||
|
||||
Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API.
|
||||
|
||||
The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`:
|
||||
|
||||
```
|
||||
ENVIRONMENT=prod poetry run python main.py
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
|
||||
- [Workflows Introduction](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - learn about LlamaIndex workflows.
|
||||
|
||||
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
|
||||
@@ -0,0 +1,3 @@
|
||||
from .financial_report import create_workflow
|
||||
|
||||
__all__ = ["create_workflow"]
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
from app.engine.tools import ToolFactory
|
||||
from app.workflows.events import AgentRunEvent
|
||||
from app.workflows.tools import (
|
||||
call_tools,
|
||||
chat_with_tools,
|
||||
)
|
||||
from llama_index.core import Settings
|
||||
from llama_index.core.base.llms.types import ChatMessage, MessageRole
|
||||
from llama_index.core.indices.vector_store import VectorStoreIndex
|
||||
from llama_index.core.llms.function_calling import FunctionCallingLLM
|
||||
from llama_index.core.memory import ChatMemoryBuffer
|
||||
from llama_index.core.tools import FunctionTool, QueryEngineTool, ToolSelection
|
||||
from llama_index.core.workflow import (
|
||||
Context,
|
||||
Event,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
step,
|
||||
)
|
||||
|
||||
|
||||
def create_workflow(
|
||||
chat_history: Optional[List[ChatMessage]] = None,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
filters: Optional[List[Any]] = None,
|
||||
) -> Workflow:
|
||||
index_config = IndexConfig(**params)
|
||||
index: VectorStoreIndex = get_index(config=index_config)
|
||||
if index is None:
|
||||
query_engine_tool = None
|
||||
else:
|
||||
top_k = int(os.getenv("TOP_K", 10))
|
||||
query_engine = index.as_query_engine(similarity_top_k=top_k, filters=filters)
|
||||
query_engine_tool = QueryEngineTool.from_defaults(query_engine=query_engine)
|
||||
|
||||
configured_tools: Dict[str, FunctionTool] = ToolFactory.from_env(map_result=True) # type: ignore
|
||||
code_interpreter_tool = configured_tools.get("interpret")
|
||||
document_generator_tool = configured_tools.get("generate_document")
|
||||
|
||||
return FinancialReportWorkflow(
|
||||
query_engine_tool=query_engine_tool,
|
||||
code_interpreter_tool=code_interpreter_tool,
|
||||
document_generator_tool=document_generator_tool,
|
||||
chat_history=chat_history,
|
||||
)
|
||||
|
||||
|
||||
class InputEvent(Event):
|
||||
input: List[ChatMessage]
|
||||
response: bool = False
|
||||
|
||||
|
||||
class ResearchEvent(Event):
|
||||
input: list[ToolSelection]
|
||||
|
||||
|
||||
class AnalyzeEvent(Event):
|
||||
input: list[ToolSelection] | ChatMessage
|
||||
|
||||
|
||||
class ReportEvent(Event):
|
||||
input: list[ToolSelection]
|
||||
|
||||
|
||||
class FinancialReportWorkflow(Workflow):
|
||||
"""
|
||||
A workflow to generate a financial report using indexed documents.
|
||||
|
||||
Requirements:
|
||||
- Indexed documents containing financial data and a query engine tool to search them
|
||||
- A code interpreter tool to analyze data and generate reports
|
||||
- A document generator tool to create report files
|
||||
|
||||
Steps:
|
||||
1. LLM Input: The LLM determines the next step based on function calling.
|
||||
For example, if the model requests the query engine tool, it returns a ResearchEvent;
|
||||
if it requests document generation, it returns a ReportEvent.
|
||||
2. Research: Uses the query engine to find relevant chunks from indexed documents.
|
||||
After gathering information, it requests analysis (step 3).
|
||||
3. Analyze: Uses a custom prompt to analyze research results and can call the code
|
||||
interpreter tool for visualization or calculation. Returns results to the LLM.
|
||||
4. Report: Uses the document generator tool to create a report. Returns results to the LLM.
|
||||
"""
|
||||
|
||||
_default_system_prompt = """
|
||||
You are a financial analyst who are given a set of tools to help you.
|
||||
It's good to using appropriate tools for the user request and always use the information from the tools, don't make up anything yourself.
|
||||
For the query engine tool, you should break down the user request into a list of queries and call the tool with the queries.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
query_engine_tool: QueryEngineTool,
|
||||
code_interpreter_tool: FunctionTool,
|
||||
document_generator_tool: FunctionTool,
|
||||
llm: Optional[FunctionCallingLLM] = None,
|
||||
timeout: int = 360,
|
||||
chat_history: Optional[List[ChatMessage]] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
):
|
||||
super().__init__(timeout=timeout)
|
||||
self.system_prompt = system_prompt or self._default_system_prompt
|
||||
self.chat_history = chat_history or []
|
||||
self.query_engine_tool = query_engine_tool
|
||||
self.code_interpreter_tool = code_interpreter_tool
|
||||
self.document_generator_tool = document_generator_tool
|
||||
assert (
|
||||
query_engine_tool is not None
|
||||
), "Query engine tool is not found. Try run generation script or upload a document file first."
|
||||
assert code_interpreter_tool is not None, "Code interpreter tool is required"
|
||||
assert (
|
||||
document_generator_tool is not None
|
||||
), "Document generator tool is required"
|
||||
self.tools = [
|
||||
self.query_engine_tool,
|
||||
self.code_interpreter_tool,
|
||||
self.document_generator_tool,
|
||||
]
|
||||
self.llm: FunctionCallingLLM = llm or Settings.llm
|
||||
assert isinstance(self.llm, FunctionCallingLLM)
|
||||
self.memory = ChatMemoryBuffer.from_defaults(
|
||||
llm=self.llm, chat_history=self.chat_history
|
||||
)
|
||||
|
||||
@step()
|
||||
async def prepare_chat_history(self, ctx: Context, ev: StartEvent) -> InputEvent:
|
||||
ctx.data["input"] = ev.input
|
||||
|
||||
if self.system_prompt:
|
||||
system_msg = ChatMessage(
|
||||
role=MessageRole.SYSTEM, content=self.system_prompt
|
||||
)
|
||||
self.memory.put(system_msg)
|
||||
|
||||
# Add user input to memory
|
||||
self.memory.put(ChatMessage(role=MessageRole.USER, content=ev.input))
|
||||
|
||||
return InputEvent(input=self.memory.get())
|
||||
|
||||
@step()
|
||||
async def handle_llm_input( # type: ignore
|
||||
self,
|
||||
ctx: Context,
|
||||
ev: InputEvent,
|
||||
) -> ResearchEvent | AnalyzeEvent | ReportEvent | StopEvent:
|
||||
"""
|
||||
Handle an LLM input and decide the next step.
|
||||
"""
|
||||
# Always use the latest chat history from the input
|
||||
chat_history: list[ChatMessage] = ev.input
|
||||
|
||||
# Get tool calls
|
||||
response = await chat_with_tools(
|
||||
self.llm,
|
||||
self.tools, # type: ignore
|
||||
chat_history,
|
||||
)
|
||||
if not response.has_tool_calls():
|
||||
# If no tool call, return the response generator
|
||||
return StopEvent(result=response.generator)
|
||||
# calling different tools at the same time is not supported at the moment
|
||||
# add an error message to tell the AI to process step by step
|
||||
if response.is_calling_different_tools():
|
||||
self.memory.put(
|
||||
ChatMessage(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content="Cannot call different tools at the same time. Try calling one tool at a time.",
|
||||
)
|
||||
)
|
||||
return InputEvent(input=self.memory.get())
|
||||
self.memory.put(response.tool_call_message)
|
||||
match response.tool_name():
|
||||
case self.code_interpreter_tool.metadata.name:
|
||||
return AnalyzeEvent(input=response.tool_calls)
|
||||
case self.document_generator_tool.metadata.name:
|
||||
return ReportEvent(input=response.tool_calls)
|
||||
case self.query_engine_tool.metadata.name:
|
||||
return ResearchEvent(input=response.tool_calls)
|
||||
case _:
|
||||
raise ValueError(f"Unknown tool: {response.tool_name()}")
|
||||
|
||||
@step()
|
||||
async def research(self, ctx: Context, ev: ResearchEvent) -> AnalyzeEvent:
|
||||
"""
|
||||
Do a research to gather information for the user's request.
|
||||
A researcher should have these tools: query engine, search engine, etc.
|
||||
"""
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name="Researcher",
|
||||
msg="Starting research",
|
||||
)
|
||||
)
|
||||
tool_calls = ev.input
|
||||
|
||||
tool_messages = await call_tools(
|
||||
ctx=ctx,
|
||||
agent_name="Researcher",
|
||||
tools=[self.query_engine_tool],
|
||||
tool_calls=tool_calls,
|
||||
)
|
||||
self.memory.put_messages(tool_messages)
|
||||
return AnalyzeEvent(
|
||||
input=ChatMessage(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content="I've finished the research. Please analyze the result.",
|
||||
),
|
||||
)
|
||||
|
||||
@step()
|
||||
async def analyze(self, ctx: Context, ev: AnalyzeEvent) -> InputEvent:
|
||||
"""
|
||||
Analyze the research result.
|
||||
"""
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name="Analyst",
|
||||
msg="Starting analysis",
|
||||
)
|
||||
)
|
||||
event_requested_by_workflow_llm = isinstance(ev.input, list)
|
||||
# Requested by the workflow LLM Input step, it's a tool call
|
||||
if event_requested_by_workflow_llm:
|
||||
# Set the tool calls
|
||||
tool_calls = ev.input
|
||||
else:
|
||||
# Otherwise, it's triggered by the research step
|
||||
# Use a custom prompt and independent memory for the analyst agent
|
||||
analysis_prompt = """
|
||||
You are a financial analyst, you are given a research result and a set of tools to help you.
|
||||
Always use the given information, don't make up anything yourself. If there is not enough information, you can asking for more information.
|
||||
If you have enough numerical information, it's good to include some charts/visualizations to the report so you can use the code interpreter tool to generate a report.
|
||||
"""
|
||||
# This is handled by analyst agent
|
||||
# Clone the shared memory to avoid conflicting with the workflow.
|
||||
chat_history = self.memory.get()
|
||||
chat_history.append(
|
||||
ChatMessage(role=MessageRole.SYSTEM, content=analysis_prompt)
|
||||
)
|
||||
chat_history.append(ev.input) # type: ignore
|
||||
# Check if the analyst agent needs to call tools
|
||||
response = await chat_with_tools(
|
||||
self.llm,
|
||||
[self.code_interpreter_tool],
|
||||
chat_history,
|
||||
)
|
||||
if not response.has_tool_calls():
|
||||
# If no tool call, fallback analyst message to the workflow
|
||||
analyst_msg = ChatMessage(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content=await response.full_response(),
|
||||
)
|
||||
self.memory.put(analyst_msg)
|
||||
return InputEvent(input=self.memory.get())
|
||||
else:
|
||||
# Set the tool calls and the tool call message to the memory
|
||||
tool_calls = response.tool_calls
|
||||
self.memory.put(response.tool_call_message)
|
||||
|
||||
# Call tools
|
||||
tool_messages = await call_tools(
|
||||
ctx=ctx,
|
||||
agent_name="Analyst",
|
||||
tools=[self.code_interpreter_tool],
|
||||
tool_calls=tool_calls, # type: ignore
|
||||
)
|
||||
self.memory.put_messages(tool_messages)
|
||||
|
||||
# Fallback to the input with the latest chat history
|
||||
return InputEvent(input=self.memory.get())
|
||||
|
||||
@step()
|
||||
async def report(self, ctx: Context, ev: ReportEvent) -> InputEvent:
|
||||
"""
|
||||
Generate a report based on the analysis result.
|
||||
"""
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name="Reporter",
|
||||
msg="Starting report generation",
|
||||
)
|
||||
)
|
||||
tool_calls = ev.input
|
||||
tool_messages = await call_tools(
|
||||
ctx=ctx,
|
||||
agent_name="Reporter",
|
||||
tools=[self.document_generator_tool],
|
||||
tool_calls=tool_calls,
|
||||
)
|
||||
self.memory.put_messages(tool_messages)
|
||||
|
||||
# After the tool calls, fallback to the input with the latest chat history
|
||||
return InputEvent(input=self.memory.get())
|
||||
@@ -0,0 +1,59 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) multi-agents project using [Workflows](https://docs.llamaindex.ai/en/stable/understanding/workflows/).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, setup the environment with poetry:
|
||||
|
||||
> **_Note:_** This step is not needed if you are using the dev-container.
|
||||
|
||||
```shell
|
||||
poetry install
|
||||
```
|
||||
|
||||
Then check the parameters that have been pre-configured in the `.env` file in this directory.
|
||||
Make sure you have the `OPENAI_API_KEY` set.
|
||||
|
||||
Second, run the development server:
|
||||
|
||||
```shell
|
||||
poetry run python main.py
|
||||
```
|
||||
|
||||
## Use Case: Filling Financial CSV Template
|
||||
|
||||
To reproduce the use case, start the [frontend](../frontend/README.md) and follow these steps in the frontend:
|
||||
|
||||
1. Upload the Apple and Tesla financial reports from the [data](./data) directory. Just send an empty message.
|
||||
2. Upload the CSV file [sec_10k_template.csv](./sec_10k_template.csv) and send the message "Fill the missing cells in the CSV file".
|
||||
|
||||
The agent will fill the missing cells by retrieving the information from the uploaded financial reports and return a new CSV file with the filled cells.
|
||||
|
||||
### API endpoints
|
||||
|
||||
The example provides one streaming API endpoint `/api/chat`.
|
||||
You can test the endpoint with the following curl request:
|
||||
|
||||
```
|
||||
curl --location 'localhost:8000/api/chat' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{ "messages": [{ "role": "user", "content": "What can you do?" }] }'
|
||||
```
|
||||
|
||||
You can start editing the API by modifying `app/api/routers/chat.py` or `app/workflows/form_filling.py`. The API auto-updates as you save the files.
|
||||
|
||||
Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API.
|
||||
|
||||
The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`:
|
||||
|
||||
```
|
||||
ENVIRONMENT=prod poetry run python main.py
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about LlamaIndex, take a look at the following resources:
|
||||
|
||||
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
|
||||
- [Workflows Introduction](https://docs.llamaindex.ai/en/stable/understanding/workflows/) - learn about LlamaIndex workflows.
|
||||
|
||||
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
|
||||
@@ -0,0 +1,3 @@
|
||||
from .form_filling import create_workflow
|
||||
|
||||
__all__ = ["create_workflow"]
|
||||
@@ -0,0 +1,241 @@
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
from app.engine.tools import ToolFactory
|
||||
from app.workflows.events import AgentRunEvent
|
||||
from app.workflows.tools import (
|
||||
call_tools,
|
||||
chat_with_tools,
|
||||
)
|
||||
from llama_index.core import Settings
|
||||
from llama_index.core.base.llms.types import ChatMessage, MessageRole
|
||||
from llama_index.core.indices.vector_store import VectorStoreIndex
|
||||
from llama_index.core.llms.function_calling import FunctionCallingLLM
|
||||
from llama_index.core.memory import ChatMemoryBuffer
|
||||
from llama_index.core.tools import FunctionTool, QueryEngineTool, ToolSelection
|
||||
from llama_index.core.workflow import (
|
||||
Context,
|
||||
Event,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
step,
|
||||
)
|
||||
|
||||
|
||||
def create_workflow(
|
||||
chat_history: Optional[List[ChatMessage]] = None,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
filters: Optional[List[Any]] = None,
|
||||
) -> Workflow:
|
||||
if params is None:
|
||||
params = {}
|
||||
if filters is None:
|
||||
filters = []
|
||||
index_config = IndexConfig(**params)
|
||||
index: VectorStoreIndex = get_index(config=index_config)
|
||||
if index is None:
|
||||
query_engine_tool = None
|
||||
else:
|
||||
top_k = int(os.getenv("TOP_K", 10))
|
||||
query_engine = index.as_query_engine(similarity_top_k=top_k, filters=filters)
|
||||
query_engine_tool = QueryEngineTool.from_defaults(query_engine=query_engine)
|
||||
|
||||
configured_tools = ToolFactory.from_env(map_result=True)
|
||||
extractor_tool = configured_tools.get("extract_questions") # type: ignore
|
||||
filling_tool = configured_tools.get("fill_form") # type: ignore
|
||||
|
||||
workflow = FormFillingWorkflow(
|
||||
query_engine_tool=query_engine_tool,
|
||||
extractor_tool=extractor_tool, # type: ignore
|
||||
filling_tool=filling_tool, # type: ignore
|
||||
chat_history=chat_history,
|
||||
)
|
||||
|
||||
return workflow
|
||||
|
||||
|
||||
class InputEvent(Event):
|
||||
input: List[ChatMessage]
|
||||
response: bool = False
|
||||
|
||||
|
||||
class ExtractMissingCellsEvent(Event):
|
||||
tool_calls: list[ToolSelection]
|
||||
|
||||
|
||||
class FindAnswersEvent(Event):
|
||||
tool_calls: list[ToolSelection]
|
||||
|
||||
|
||||
class FillEvent(Event):
|
||||
tool_calls: list[ToolSelection]
|
||||
|
||||
|
||||
class FormFillingWorkflow(Workflow):
|
||||
"""
|
||||
A predefined workflow for filling missing cells in a CSV file.
|
||||
Required tools:
|
||||
- query_engine: A query engine to query for the answers to the questions.
|
||||
- extract_question: Extract missing cells in a CSV file and generate questions to fill them.
|
||||
- answer_question: Query for the answers to the questions.
|
||||
|
||||
Flow:
|
||||
1. Extract missing cells in a CSV file and generate questions to fill them.
|
||||
2. Query for the answers to the questions.
|
||||
3. Fill the missing cells with the answers.
|
||||
"""
|
||||
|
||||
_default_system_prompt = """
|
||||
You are a helpful assistant who helps fill missing cells in a CSV file.
|
||||
Only extract missing cells from CSV files.
|
||||
Only use provided data - never make up any information yourself. Fill N/A if an answer is not found.
|
||||
If there is no query engine tool or the gathered information has many N/A values indicating the questions don't match the data, respond with a warning and ask the user to upload a different file or connect to a knowledge base.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
query_engine_tool: Optional[QueryEngineTool],
|
||||
extractor_tool: FunctionTool,
|
||||
filling_tool: FunctionTool,
|
||||
llm: Optional[FunctionCallingLLM] = None,
|
||||
timeout: int = 360,
|
||||
chat_history: Optional[List[ChatMessage]] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
):
|
||||
super().__init__(timeout=timeout)
|
||||
self.system_prompt = system_prompt or self._default_system_prompt
|
||||
self.chat_history = chat_history or []
|
||||
self.query_engine_tool = query_engine_tool
|
||||
self.extractor_tool = extractor_tool
|
||||
self.filling_tool = filling_tool
|
||||
if self.extractor_tool is None or self.filling_tool is None:
|
||||
raise ValueError("Extractor and filling tools are required.")
|
||||
self.tools = [self.extractor_tool, self.filling_tool]
|
||||
if self.query_engine_tool is not None:
|
||||
self.tools.append(self.query_engine_tool) # type: ignore
|
||||
self.llm: FunctionCallingLLM = llm or Settings.llm
|
||||
if not isinstance(self.llm, FunctionCallingLLM):
|
||||
raise ValueError("FormFillingWorkflow only supports FunctionCallingLLM.")
|
||||
self.memory = ChatMemoryBuffer.from_defaults(
|
||||
llm=self.llm, chat_history=self.chat_history
|
||||
)
|
||||
|
||||
@step()
|
||||
async def start(self, ctx: Context, ev: StartEvent) -> InputEvent:
|
||||
ctx.data["input"] = ev.input
|
||||
|
||||
if self.system_prompt:
|
||||
system_msg = ChatMessage(
|
||||
role=MessageRole.SYSTEM, content=self.system_prompt
|
||||
)
|
||||
self.memory.put(system_msg)
|
||||
|
||||
user_input = ev.input
|
||||
user_msg = ChatMessage(role=MessageRole.USER, content=user_input)
|
||||
self.memory.put(user_msg)
|
||||
|
||||
chat_history = self.memory.get()
|
||||
return InputEvent(input=chat_history)
|
||||
|
||||
@step()
|
||||
async def handle_llm_input( # type: ignore
|
||||
self,
|
||||
ctx: Context,
|
||||
ev: InputEvent,
|
||||
) -> ExtractMissingCellsEvent | FillEvent | StopEvent:
|
||||
"""
|
||||
Handle an LLM input and decide the next step.
|
||||
"""
|
||||
chat_history: list[ChatMessage] = ev.input
|
||||
response = await chat_with_tools(
|
||||
self.llm,
|
||||
self.tools,
|
||||
chat_history,
|
||||
)
|
||||
if not response.has_tool_calls():
|
||||
return StopEvent(result=response.generator)
|
||||
# calling different tools at the same time is not supported at the moment
|
||||
# add an error message to tell the AI to process step by step
|
||||
if response.is_calling_different_tools():
|
||||
self.memory.put(
|
||||
ChatMessage(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content="Cannot call different tools at the same time. Try calling one tool at a time.",
|
||||
)
|
||||
)
|
||||
return InputEvent(input=self.memory.get())
|
||||
self.memory.put(response.tool_call_message)
|
||||
match response.tool_name():
|
||||
case self.extractor_tool.metadata.name:
|
||||
return ExtractMissingCellsEvent(tool_calls=response.tool_calls)
|
||||
case self.query_engine_tool.metadata.name:
|
||||
return FindAnswersEvent(tool_calls=response.tool_calls)
|
||||
case self.filling_tool.metadata.name:
|
||||
return FillEvent(tool_calls=response.tool_calls)
|
||||
case _:
|
||||
raise ValueError(f"Unknown tool: {response.tool_name()}")
|
||||
|
||||
@step()
|
||||
async def extract_missing_cells(
|
||||
self, ctx: Context, ev: ExtractMissingCellsEvent
|
||||
) -> InputEvent | FindAnswersEvent:
|
||||
"""
|
||||
Extract missing cells in a CSV file and generate questions to fill them.
|
||||
"""
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name="Extractor",
|
||||
msg="Extracting missing cells",
|
||||
)
|
||||
)
|
||||
# Call the extract questions tool
|
||||
tool_messages = await call_tools(
|
||||
agent_name="Extractor",
|
||||
tools=[self.extractor_tool],
|
||||
ctx=ctx,
|
||||
tool_calls=ev.tool_calls,
|
||||
)
|
||||
self.memory.put_messages(tool_messages)
|
||||
return InputEvent(input=self.memory.get())
|
||||
|
||||
@step()
|
||||
async def find_answers(self, ctx: Context, ev: FindAnswersEvent) -> InputEvent:
|
||||
"""
|
||||
Call answer questions tool to query for the answers to the questions.
|
||||
"""
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name="Researcher",
|
||||
msg="Finding answers for missing cells",
|
||||
)
|
||||
)
|
||||
tool_messages = await call_tools(
|
||||
ctx=ctx,
|
||||
agent_name="Researcher",
|
||||
tools=[self.query_engine_tool],
|
||||
tool_calls=ev.tool_calls,
|
||||
)
|
||||
self.memory.put_messages(tool_messages)
|
||||
return InputEvent(input=self.memory.get())
|
||||
|
||||
@step()
|
||||
async def fill_cells(self, ctx: Context, ev: FillEvent) -> InputEvent:
|
||||
"""
|
||||
Call fill cells tool to fill the missing cells with the answers.
|
||||
"""
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name="Processor",
|
||||
msg="Filling missing cells",
|
||||
)
|
||||
)
|
||||
tool_messages = await call_tools(
|
||||
agent_name="Processor",
|
||||
tools=[self.filling_tool],
|
||||
ctx=ctx,
|
||||
tool_calls=ev.tool_calls,
|
||||
)
|
||||
self.memory.put_messages(tool_messages)
|
||||
return InputEvent(input=self.memory.get())
|
||||
@@ -0,0 +1,17 @@
|
||||
Parameter,2023 Apple (AAPL),2023 Tesla (TSLA)
|
||||
Revenue,,
|
||||
Net Income,,
|
||||
Earnings Per Share (EPS),,
|
||||
Debt-to-Equity Ratio,,
|
||||
Current Ratio,,
|
||||
Gross Margin,,
|
||||
Operating Margin,,
|
||||
Net Profit Margin,,
|
||||
Inventory Turnover,,
|
||||
Accounts Receivable Turnover,,
|
||||
Capital Expenditure,,
|
||||
Research and Development Expense,,
|
||||
Market Cap,,
|
||||
Price to Earnings Ratio,,
|
||||
Dividend Yield,,
|
||||
Year-over-Year Growth Rate,,
|
||||
|
+13
-8
@@ -1,14 +1,19 @@
|
||||
import { ChatMessage } from "llamaindex";
|
||||
import { FunctionCallingAgent } from "./single-agent";
|
||||
import { lookupTools } from "./tools";
|
||||
import { getQueryEngineTool, lookupTools } from "./tools";
|
||||
|
||||
export const createResearcher = async (chatHistory: ChatMessage[]) => {
|
||||
const tools = await lookupTools([
|
||||
"query_index",
|
||||
"wikipedia_tool",
|
||||
"duckduckgo_search",
|
||||
"image_generator",
|
||||
]);
|
||||
export const createResearcher = async (
|
||||
chatHistory: ChatMessage[],
|
||||
params?: any,
|
||||
) => {
|
||||
const queryEngineTool = await getQueryEngineTool(params);
|
||||
const tools = (
|
||||
await lookupTools([
|
||||
"wikipedia_tool",
|
||||
"duckduckgo_search",
|
||||
"image_generator",
|
||||
])
|
||||
).concat(queryEngineTool ? [queryEngineTool] : []);
|
||||
|
||||
return new FunctionCallingAgent({
|
||||
name: "researcher",
|
||||
+15
-14
@@ -5,7 +5,9 @@ import {
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "@llamaindex/core/workflow";
|
||||
import { Message } from "ai";
|
||||
import { ChatMessage, ChatResponseChunk, Settings } from "llamaindex";
|
||||
import { getAnnotations } from "../llamaindex/streaming/annotations";
|
||||
import {
|
||||
createPublisher,
|
||||
createResearcher,
|
||||
@@ -25,19 +27,15 @@ class WriteEvent extends WorkflowEvent<{
|
||||
class ReviewEvent extends WorkflowEvent<{ input: string }> {}
|
||||
class PublishEvent extends WorkflowEvent<{ input: string }> {}
|
||||
|
||||
const prepareChatHistory = (chatHistory: ChatMessage[]) => {
|
||||
const prepareChatHistory = (chatHistory: Message[]): 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 agentAnnotations = getAnnotations<{ agent: string; text: string }>(
|
||||
chatHistory,
|
||||
{ role: "assistant", type: "agent" },
|
||||
).slice(-MAX_AGENT_MESSAGES);
|
||||
|
||||
const agentMessages = agentAnnotations
|
||||
.map(
|
||||
@@ -59,13 +57,13 @@ const prepareChatHistory = (chatHistory: ChatMessage[]) => {
|
||||
...chatHistory.slice(0, -1),
|
||||
agentMessage,
|
||||
chatHistory.slice(-1)[0],
|
||||
];
|
||||
] as ChatMessage[];
|
||||
}
|
||||
return chatHistory;
|
||||
return chatHistory as ChatMessage[];
|
||||
};
|
||||
|
||||
export const createWorkflow = (chatHistory: ChatMessage[]) => {
|
||||
const chatHistoryWithAgentMessages = prepareChatHistory(chatHistory);
|
||||
export const createWorkflow = (messages: Message[], params?: any) => {
|
||||
const chatHistoryWithAgentMessages = prepareChatHistory(messages);
|
||||
const runAgent = async (
|
||||
context: Context,
|
||||
agent: Workflow,
|
||||
@@ -123,7 +121,10 @@ Decision (respond with either 'not_publish' or 'publish'):`;
|
||||
};
|
||||
|
||||
const research = async (context: Context, ev: ResearchEvent) => {
|
||||
const researcher = await createResearcher(chatHistoryWithAgentMessages);
|
||||
const researcher = await createResearcher(
|
||||
chatHistoryWithAgentMessages,
|
||||
params,
|
||||
);
|
||||
const researchRes = await runAgent(context, researcher, {
|
||||
message: ev.data.input,
|
||||
});
|
||||
+4
-2
@@ -4,8 +4,10 @@ import path from "path";
|
||||
import { getDataSource } from "../engine";
|
||||
import { createTools } from "../engine/tools/index";
|
||||
|
||||
const getQueryEngineTool = async (): Promise<QueryEngineTool | null> => {
|
||||
const index = await getDataSource();
|
||||
export const getQueryEngineTool = async (
|
||||
params?: any,
|
||||
): Promise<QueryEngineTool | null> => {
|
||||
const index = await getDataSource(params);
|
||||
if (!index) {
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { ChatMessage } from "llamaindex";
|
||||
import { FunctionCallingAgent } from "./single-agent";
|
||||
import { getQueryEngineTools, lookupTools } from "./tools";
|
||||
|
||||
export const createResearcher = async (
|
||||
chatHistory: ChatMessage[],
|
||||
params?: any,
|
||||
) => {
|
||||
const queryEngineTools = await getQueryEngineTools(params);
|
||||
|
||||
if (!queryEngineTools) {
|
||||
throw new Error("Query engine tool not found");
|
||||
}
|
||||
|
||||
return new FunctionCallingAgent({
|
||||
name: "researcher",
|
||||
tools: queryEngineTools,
|
||||
systemPrompt: `You are a researcher agent. You are responsible for retrieving information from the corpus.
|
||||
## Instructions:
|
||||
+ Don't synthesize the information, just return the whole retrieved information.
|
||||
+ Don't need to retrieve the information that is already provided in the chat history and respond with: "There is no new information, please reuse the information from the conversation."
|
||||
`,
|
||||
chatHistory,
|
||||
});
|
||||
};
|
||||
|
||||
export const createAnalyst = async (chatHistory: ChatMessage[]) => {
|
||||
let systemPrompt = `You are an expert in analyzing financial data.
|
||||
You are given a task and a set of financial data to analyze. Your task is to analyze the financial data and return a report.
|
||||
Your response should include a detailed analysis of the financial data, including any trends, patterns, or insights that you find.
|
||||
Construct the analysis in textual format; including tables would be great!
|
||||
Don't need to synthesize the data, just analyze and provide your findings.
|
||||
Always use the provided information, don't make up any information yourself.`;
|
||||
const tools = await lookupTools(["interpreter"]);
|
||||
if (tools.length > 0) {
|
||||
systemPrompt = `${systemPrompt}
|
||||
You are able to visualize the financial data using code interpreter tool.
|
||||
It's very useful to create and include visualizations in the report. Never include any code in the report, just the visualization.`;
|
||||
}
|
||||
return new FunctionCallingAgent({
|
||||
name: "analyst",
|
||||
tools: tools,
|
||||
chatHistory,
|
||||
});
|
||||
};
|
||||
|
||||
export const createReporter = async (chatHistory: ChatMessage[]) => {
|
||||
const tools = await lookupTools(["document_generator"]);
|
||||
let systemPrompt = `You are a report generation assistant tasked with producing a well-formatted report given parsed context.
|
||||
Given a comprehensive analysis of the user request, your task is to synthesize the information and return a well-formatted report.
|
||||
|
||||
## Instructions
|
||||
You are responsible for representing the analysis in a well-formatted report. If tables or visualizations are provided, add them to the most relevant sections.
|
||||
Finally, the report should be presented in markdown format.`;
|
||||
if (tools.length > 0) {
|
||||
systemPrompt = `${systemPrompt}.
|
||||
You are also able to generate an HTML file of the report.`;
|
||||
}
|
||||
return new FunctionCallingAgent({
|
||||
name: "reporter",
|
||||
tools: tools,
|
||||
systemPrompt: systemPrompt,
|
||||
chatHistory,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
import {
|
||||
Context,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
WorkflowEvent,
|
||||
} from "@llamaindex/core/workflow";
|
||||
import { Message } from "ai";
|
||||
import { ChatMessage, ChatResponseChunk, Settings } from "llamaindex";
|
||||
import { getAnnotations } from "../llamaindex/streaming/annotations";
|
||||
import { createAnalyst, createReporter, createResearcher } from "./agents";
|
||||
import { AgentInput, AgentRunEvent } from "./type";
|
||||
|
||||
const TIMEOUT = 360 * 1000;
|
||||
const MAX_ATTEMPTS = 2;
|
||||
|
||||
class ResearchEvent extends WorkflowEvent<{ input: string }> {}
|
||||
class AnalyzeEvent extends WorkflowEvent<{ input: string }> {}
|
||||
class ReportEvent extends WorkflowEvent<{ input: string }> {}
|
||||
|
||||
const prepareChatHistory = (chatHistory: Message[]): 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;
|
||||
const agentAnnotations = getAnnotations<{ agent: string; text: string }>(
|
||||
chatHistory,
|
||||
{ role: "assistant", 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],
|
||||
] as ChatMessage[];
|
||||
}
|
||||
return chatHistory as ChatMessage[];
|
||||
};
|
||||
|
||||
export const createWorkflow = (messages: Message[], params?: any) => {
|
||||
const chatHistoryWithAgentMessages = prepareChatHistory(messages);
|
||||
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 ReportEvent({
|
||||
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,
|
||||
params,
|
||||
);
|
||||
const researchRes = await runAgent(context, researcher, {
|
||||
message: ev.data.input,
|
||||
});
|
||||
const researchResult = researchRes.data.result;
|
||||
return new AnalyzeEvent({
|
||||
input: `Write a blog post given this task: ${context.get("task")} using this research content: ${researchResult}`,
|
||||
});
|
||||
};
|
||||
|
||||
const analyze = async (context: Context, ev: AnalyzeEvent) => {
|
||||
const analyst = await createAnalyst(chatHistoryWithAgentMessages);
|
||||
const analyzeRes = await runAgent(context, analyst, {
|
||||
message: ev.data.input,
|
||||
});
|
||||
return new ReportEvent({
|
||||
input: `Publish content based on the chat history\n${analyzeRes.data.result}\n\n and task: ${ev.data.input}`,
|
||||
});
|
||||
};
|
||||
|
||||
const report = async (context: Context, ev: ReportEvent) => {
|
||||
const reporter = await createReporter(chatHistoryWithAgentMessages);
|
||||
|
||||
const reportResult = await runAgent(context, reporter, {
|
||||
message: `${ev.data.input}`,
|
||||
streaming: true,
|
||||
});
|
||||
return reportResult as unknown as StopEvent<
|
||||
AsyncGenerator<ChatResponseChunk>
|
||||
>;
|
||||
};
|
||||
|
||||
const workflow = new Workflow({ timeout: TIMEOUT, validate: true });
|
||||
workflow.addStep(StartEvent, start, {
|
||||
outputs: [ResearchEvent, ReportEvent],
|
||||
});
|
||||
workflow.addStep(ResearchEvent, research, { outputs: AnalyzeEvent });
|
||||
workflow.addStep(AnalyzeEvent, analyze, { outputs: ReportEvent });
|
||||
workflow.addStep(ReportEvent, report, { outputs: StopEvent });
|
||||
|
||||
return workflow;
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import fs from "fs/promises";
|
||||
import { BaseToolWithCall, LlamaCloudIndex, QueryEngineTool } from "llamaindex";
|
||||
import path from "path";
|
||||
import { getDataSource } from "../engine";
|
||||
import { createTools } from "../engine/tools/index";
|
||||
|
||||
export const getQueryEngineTools = async (
|
||||
params?: any,
|
||||
): Promise<QueryEngineTool[] | null> => {
|
||||
const topK = process.env.TOP_K ? parseInt(process.env.TOP_K) : undefined;
|
||||
|
||||
const index = await getDataSource(params);
|
||||
if (!index) {
|
||||
return null;
|
||||
}
|
||||
// index is LlamaCloudIndex use two query engine tools
|
||||
if (index instanceof LlamaCloudIndex) {
|
||||
return [
|
||||
new QueryEngineTool({
|
||||
queryEngine: index.asQueryEngine({
|
||||
similarityTopK: topK,
|
||||
retrieval_mode: "files_via_content",
|
||||
}),
|
||||
metadata: {
|
||||
name: "document_retriever",
|
||||
description: `Document retriever that retrieves entire documents from the corpus.
|
||||
ONLY use for research questions that may require searching over entire research reports.
|
||||
Will be slower and more expensive than chunk-level retrieval but may be necessary.`,
|
||||
},
|
||||
}),
|
||||
new QueryEngineTool({
|
||||
queryEngine: index.asQueryEngine({
|
||||
similarityTopK: topK,
|
||||
retrieval_mode: "chunks",
|
||||
}),
|
||||
metadata: {
|
||||
name: "chunk_retriever",
|
||||
description: `Retrieves a small set of relevant document chunks from the corpus.
|
||||
Use for research questions that want to look up specific facts from the knowledge corpus,
|
||||
and need entire documents.`,
|
||||
},
|
||||
}),
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
new QueryEngineTool({
|
||||
queryEngine: (index as any).asQueryEngine({
|
||||
similarityTopK: topK,
|
||||
}),
|
||||
metadata: {
|
||||
name: "retriever",
|
||||
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 queryEngineTools = await getQueryEngineTools();
|
||||
if (queryEngineTools) {
|
||||
tools.push(...queryEngineTools);
|
||||
}
|
||||
|
||||
return tools;
|
||||
};
|
||||
|
||||
export const lookupTools = async (
|
||||
toolNames: string[],
|
||||
): Promise<BaseToolWithCall[]> => {
|
||||
const availableTools = await getAvailableTools();
|
||||
return availableTools.filter((tool) =>
|
||||
toolNames.includes(tool.metadata.name),
|
||||
);
|
||||
};
|
||||
@@ -56,7 +56,7 @@ class ToolFactory:
|
||||
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]] = (
|
||||
tools: Union[Dict[str, FunctionTool], List[FunctionTool]] = (
|
||||
{} if map_result else []
|
||||
)
|
||||
|
||||
@@ -69,7 +69,9 @@ class ToolFactory:
|
||||
tool_type, tool_name, config
|
||||
)
|
||||
if map_result:
|
||||
tools[tool_name] = loaded_tools # type: ignore
|
||||
tools.update( # type: ignore
|
||||
{tool.metadata.name: tool for tool in loaded_tools}
|
||||
)
|
||||
else:
|
||||
tools.extend(loaded_tools) # type: ignore
|
||||
|
||||
|
||||
@@ -66,21 +66,29 @@ 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.
|
||||
def artifact(
|
||||
self,
|
||||
query: str,
|
||||
sandbox_files: Optional[List[str]] = None,
|
||||
old_code: Optional[str] = None,
|
||||
) -> Dict:
|
||||
"""Generate a code artifact based on the provided input.
|
||||
|
||||
Args:
|
||||
query (str): The description of the application you want to build.
|
||||
query (str): A description of the application you want to build.
|
||||
sandbox_files (Optional[List[str]], optional): A list of sandbox file paths. Defaults to None. Include these files if the code requires them.
|
||||
old_code (Optional[str], optional): The existing code to be modified. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Dict: A dictionary containing the generated artifact information.
|
||||
Dict: A dictionary containing information about the generated artifact.
|
||||
"""
|
||||
|
||||
if old_code:
|
||||
user_message = f"{query}\n\nThe existing code is: \n```\n{old_code}\n```"
|
||||
else:
|
||||
user_message = query
|
||||
if sandbox_files:
|
||||
user_message += f"\n\nThe provided files are: \n{str(sandbox_files)}"
|
||||
|
||||
messages: List[ChatMessage] = [
|
||||
ChatMessage(role="system", content=CODE_GENERATION_PROMPT),
|
||||
@@ -90,7 +98,10 @@ class CodeGeneratorTool:
|
||||
sllm = Settings.llm.as_structured_llm(output_cls=CodeArtifact) # type: ignore
|
||||
response = sllm.chat(messages)
|
||||
data: CodeArtifact = response.raw
|
||||
return data.model_dump()
|
||||
data_dict = data.model_dump()
|
||||
if sandbox_files:
|
||||
data_dict["files"] = sandbox_files
|
||||
return data_dict
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate artifact: {str(e)}")
|
||||
raise e
|
||||
|
||||
@@ -105,7 +105,7 @@ class DocumentGenerator:
|
||||
Generate HTML content from the original markdown content.
|
||||
"""
|
||||
try:
|
||||
import markdown
|
||||
import markdown # type: ignore
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Failed to import required modules. Please install markdown."
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from textwrap import dedent
|
||||
from typing import Optional
|
||||
|
||||
import pandas as pd
|
||||
from app.services.file import FileService
|
||||
from llama_index.core import Settings
|
||||
from llama_index.core.prompts import PromptTemplate
|
||||
from llama_index.core.tools import FunctionTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MissingCell(BaseModel):
|
||||
"""
|
||||
A missing cell in a table.
|
||||
"""
|
||||
|
||||
row_index: int = Field(description="The index of the row of the missing cell")
|
||||
column_index: int = Field(description="The index of the column of the missing cell")
|
||||
question_to_answer: str = Field(
|
||||
description="The question to answer to fill the missing cell"
|
||||
)
|
||||
|
||||
|
||||
class MissingCells(BaseModel):
|
||||
"""
|
||||
A list of missing cells.
|
||||
"""
|
||||
|
||||
missing_cells: list[MissingCell] = Field(description="The missing cells")
|
||||
|
||||
|
||||
class CellValue(BaseModel):
|
||||
row_index: int = Field(description="The row index of the cell")
|
||||
column_index: int = Field(description="The column index of the cell")
|
||||
value: str = Field(
|
||||
description="The value of the cell. Should be a concise value (numerical value or specific value)"
|
||||
)
|
||||
|
||||
|
||||
class FormFillingTool:
|
||||
"""
|
||||
Fill out missing cells in a CSV file using information from the knowledge base.
|
||||
"""
|
||||
|
||||
save_dir: str = os.path.join("output", "tools")
|
||||
|
||||
# Default prompt for extracting questions
|
||||
# Replace the default prompt with a custom prompt by setting the EXTRACT_QUESTIONS_PROMPT environment variable.
|
||||
_default_extract_questions_prompt = dedent(
|
||||
"""
|
||||
You are a data analyst. You are given a table with missing cells.
|
||||
Your task is to identify the missing cells and the questions needed to fill them.
|
||||
IMPORTANT: Column indices should be 0-based, where the first data column is index 1
|
||||
(index 0 is typically the row names/index column).
|
||||
|
||||
# Instructions:
|
||||
- Understand the entire content of the table and the topics of the table.
|
||||
- Identify the missing cells and the meaning of the data in the cells.
|
||||
- For each missing cell, provide the row index and the correct column index (remember: first data column is 1).
|
||||
- For each missing cell, provide the question needed to fill the cell (it's important to provide the question that is relevant to the topic of the table).
|
||||
- Since the cell's value should be concise, the question should request a numerical answer or a specific value.
|
||||
|
||||
# Example:
|
||||
# | | Name | Age | City |
|
||||
# |----|------|-----|------|
|
||||
# | 0 | John | | Paris|
|
||||
# | 1 | Mary | | |
|
||||
# | 2 | | 30 | |
|
||||
#
|
||||
# Your thoughts:
|
||||
# - The table is about people's names, ages, and cities.
|
||||
# - Row: 1, Column: 1 (Age column), Question: "How old is Mary? Please provide only the numerical answer."
|
||||
# - Row: 1, Column: 2 (City column), Question: "In which city does Mary live? Please provide only the city name."
|
||||
|
||||
|
||||
Please provide your answer in the requested format.
|
||||
# Here is your task:
|
||||
|
||||
- Table content:
|
||||
{table_content}
|
||||
|
||||
- Your answer:
|
||||
"""
|
||||
)
|
||||
|
||||
def extract_questions(
|
||||
self,
|
||||
file_path: Optional[str] = None,
|
||||
file_content: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Use this tool to extract missing cells in a CSV file and generate questions to fill them.
|
||||
Pass either the path to the CSV file or the content of the CSV file.
|
||||
|
||||
Args:
|
||||
file_path (Optional[str]): The local file path to the CSV file to extract missing cells from (Don't pass a sandbox path).
|
||||
file_content (Optional[str]): The content of the CSV file to extract missing cells from.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the missing cells and their corresponding questions.
|
||||
"""
|
||||
extract_questions_prompt = os.getenv(
|
||||
"EXTRACT_QUESTIONS_PROMPT", self._default_extract_questions_prompt
|
||||
)
|
||||
if file_path is None and file_content is None:
|
||||
raise ValueError("Either `file_path` or `file_content` must be provided")
|
||||
|
||||
table_content = None
|
||||
|
||||
if file_path:
|
||||
file_name, file_extension = self._get_file_name_and_extension(
|
||||
file_path, file_content
|
||||
)
|
||||
|
||||
try:
|
||||
df = pd.read_csv(file_path)
|
||||
except FileNotFoundError as e:
|
||||
return {
|
||||
"error": str(e),
|
||||
"message": "Please check and update the file path and ensure it's a local path - not a sandbox path.",
|
||||
}
|
||||
|
||||
table_content = df.to_markdown()
|
||||
if table_content is None:
|
||||
raise ValueError("Could not convert the table to markdown")
|
||||
if file_content:
|
||||
table_content = file_content
|
||||
|
||||
if table_content is None:
|
||||
raise ValueError("Table content not found")
|
||||
|
||||
response: MissingCells = Settings.llm.structured_predict(
|
||||
output_cls=MissingCells,
|
||||
prompt=PromptTemplate(extract_questions_prompt),
|
||||
table_content=table_content,
|
||||
)
|
||||
return response.model_dump()
|
||||
|
||||
def fill_form(
|
||||
self,
|
||||
cell_values: list[CellValue],
|
||||
file_path: Optional[str] = None,
|
||||
file_content: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Use this tool to fill cell values into a CSV file.
|
||||
Requires cell values to be used for filling out, as well as either the path to the CSV file or the content of the CSV file.
|
||||
|
||||
Args:
|
||||
cell_values (list[CellValue]): The cell values used to fill out the CSV file (call `extract_questions` and query engine to construct the cell values).
|
||||
file_path (Optional[str]): The local file path to the CSV file that should be filled out (not as sandbox path).
|
||||
file_content (Optional[str]): The content of the CSV file that should be filled out.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the content and metadata of the filled-out file.
|
||||
"""
|
||||
file_name, file_extension = self._get_file_name_and_extension(
|
||||
file_path, file_content
|
||||
)
|
||||
df = pd.read_csv(file_path)
|
||||
|
||||
# Fill the dataframe with the cell values
|
||||
filled_df = df.copy()
|
||||
for cell_value in cell_values:
|
||||
if not isinstance(cell_value, CellValue):
|
||||
cell_value = CellValue(**cell_value)
|
||||
filled_df.iloc[cell_value.row_index, cell_value.column_index] = (
|
||||
cell_value.value
|
||||
)
|
||||
|
||||
# Save the filled table to a new CSV file
|
||||
csv_content: str = filled_df.to_csv(index=False)
|
||||
file_metadata = FileService.save_file(
|
||||
content=csv_content,
|
||||
file_name=f"{file_name}_filled.csv",
|
||||
save_dir=self.save_dir,
|
||||
)
|
||||
|
||||
new_content: str = filled_df.to_markdown()
|
||||
result = {
|
||||
"filled_content": new_content,
|
||||
"filled_file": file_metadata,
|
||||
}
|
||||
return result
|
||||
|
||||
def _get_file_name_and_extension(
|
||||
self, file_path: Optional[str], file_content: Optional[str]
|
||||
) -> tuple[str, str]:
|
||||
if file_path is None and file_content is None:
|
||||
raise ValueError("Either `file_path` or `file_content` must be provided")
|
||||
|
||||
if file_path is None:
|
||||
file_name = str(uuid.uuid4())
|
||||
file_extension = ".csv"
|
||||
else:
|
||||
file_name, file_extension = os.path.splitext(file_path)
|
||||
if file_extension != ".csv":
|
||||
raise ValueError("Form filling is only supported for CSV files")
|
||||
|
||||
return file_name, file_extension
|
||||
|
||||
def _save_output(self, file_name: str, output: str) -> dict:
|
||||
"""
|
||||
Save the output to a file.
|
||||
"""
|
||||
file_metadata = FileService.save_file(
|
||||
content=output,
|
||||
file_name=file_name,
|
||||
save_dir=self.save_dir,
|
||||
)
|
||||
return file_metadata.model_dump()
|
||||
|
||||
|
||||
def get_tools(**kwargs):
|
||||
tool = FormFillingTool()
|
||||
return [
|
||||
FunctionTool.from_defaults(tool.extract_questions),
|
||||
FunctionTool.from_defaults(tool.fill_form),
|
||||
]
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
import requests # type: ignore
|
||||
from llama_index.core.tools import FunctionTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
@@ -2,14 +2,15 @@ import base64
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from typing import Dict, List, Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from app.services.file import DocumentFile, FileService
|
||||
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__)
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class InterpreterExtraResult(BaseModel):
|
||||
@@ -22,13 +23,16 @@ class InterpreterExtraResult(BaseModel):
|
||||
class E2BToolOutput(BaseModel):
|
||||
is_error: bool
|
||||
logs: Logs
|
||||
error_message: Optional[str] = None
|
||||
results: List[InterpreterExtraResult] = []
|
||||
retry_count: int = 0
|
||||
|
||||
|
||||
class E2BCodeInterpreter:
|
||||
output_dir = "output/tools"
|
||||
uploaded_files_dir = "output/uploaded"
|
||||
|
||||
def __init__(self, api_key: str = None):
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
if api_key is None:
|
||||
api_key = os.getenv("E2B_API_KEY")
|
||||
filesever_url_prefix = os.getenv("FILESERVER_URL_PREFIX")
|
||||
@@ -42,40 +46,45 @@ class E2BCodeInterpreter:
|
||||
)
|
||||
|
||||
self.filesever_url_prefix = filesever_url_prefix
|
||||
self.interpreter = CodeInterpreter(api_key=api_key)
|
||||
self.interpreter = None
|
||||
self.api_key = api_key
|
||||
|
||||
def __del__(self):
|
||||
self.interpreter.close()
|
||||
"""
|
||||
Kill the interpreter when the tool is no longer in use
|
||||
"""
|
||||
if self.interpreter is not None:
|
||||
self.interpreter.kill()
|
||||
|
||||
def get_output_path(self, filename: str) -> str:
|
||||
# if output directory doesn't exist, create it
|
||||
if not os.path.exists(self.output_dir):
|
||||
os.makedirs(self.output_dir, exist_ok=True)
|
||||
return os.path.join(self.output_dir, filename)
|
||||
def _init_interpreter(self, sandbox_files: List[str] = []):
|
||||
"""
|
||||
Lazily initialize the interpreter.
|
||||
"""
|
||||
logger.info(f"Initializing interpreter with {len(sandbox_files)} files")
|
||||
self.interpreter = CodeInterpreter(api_key=self.api_key)
|
||||
if len(sandbox_files) > 0:
|
||||
for file_path in sandbox_files:
|
||||
file_name = os.path.basename(file_path)
|
||||
local_file_path = os.path.join(self.uploaded_files_dir, file_name)
|
||||
with open(local_file_path, "rb") as f:
|
||||
content = f.read()
|
||||
if self.interpreter and self.interpreter.files:
|
||||
self.interpreter.files.write(file_path, content)
|
||||
logger.info(f"Uploaded {len(sandbox_files)} files to sandbox")
|
||||
|
||||
def save_to_disk(self, base64_data: str, ext: str) -> Dict:
|
||||
filename = f"{uuid.uuid4()}.{ext}" # generate a unique filename
|
||||
def _save_to_disk(self, base64_data: str, ext: str) -> DocumentFile:
|
||||
buffer = base64.b64decode(base64_data)
|
||||
output_path = self.get_output_path(filename)
|
||||
|
||||
try:
|
||||
with open(output_path, "wb") as file:
|
||||
file.write(buffer)
|
||||
except IOError as e:
|
||||
logger.error(f"Failed to write to file {output_path}: {str(e)}")
|
||||
raise e
|
||||
# Output from e2b doesn't have a name. Create a random name for it.
|
||||
filename = f"e2b_file_{uuid.uuid4()}.{ext}"
|
||||
|
||||
logger.info(f"Saved file to {output_path}")
|
||||
document_file = FileService.save_file(
|
||||
buffer, file_name=filename, save_dir=self.output_dir
|
||||
)
|
||||
|
||||
return {
|
||||
"outputPath": output_path,
|
||||
"filename": filename,
|
||||
}
|
||||
return document_file
|
||||
|
||||
def get_file_url(self, filename: str) -> str:
|
||||
return f"{self.filesever_url_prefix}/{self.output_dir}/{filename}"
|
||||
|
||||
def parse_result(self, result) -> List[InterpreterExtraResult]:
|
||||
def _parse_result(self, result) -> List[InterpreterExtraResult]:
|
||||
"""
|
||||
The result could include multiple formats (e.g. png, svg, etc.) but encoded in base64
|
||||
We save each result to disk and return saved file metadata (extension, filename, url)
|
||||
@@ -92,16 +101,20 @@ class E2BCodeInterpreter:
|
||||
for ext, data in zip(formats, results):
|
||||
match ext:
|
||||
case "png" | "svg" | "jpeg" | "pdf":
|
||||
result = self.save_to_disk(data, ext)
|
||||
filename = result["filename"]
|
||||
document_file = self._save_to_disk(data, ext)
|
||||
output.append(
|
||||
InterpreterExtraResult(
|
||||
type=ext,
|
||||
filename=filename,
|
||||
url=self.get_file_url(filename),
|
||||
filename=document_file.name,
|
||||
url=document_file.url,
|
||||
)
|
||||
)
|
||||
case _:
|
||||
# Try serialize data to string
|
||||
try:
|
||||
data = str(data)
|
||||
except Exception as e:
|
||||
data = f"Error when serializing data: {e}"
|
||||
output.append(
|
||||
InterpreterExtraResult(
|
||||
type=ext,
|
||||
@@ -114,28 +127,75 @@ class E2BCodeInterpreter:
|
||||
|
||||
return output
|
||||
|
||||
def interpret(self, code: str) -> E2BToolOutput:
|
||||
def interpret(
|
||||
self,
|
||||
code: str,
|
||||
sandbox_files: List[str] = [],
|
||||
retry_count: int = 0,
|
||||
) -> E2BToolOutput:
|
||||
"""
|
||||
Execute python code in a Jupyter notebook cell, the toll will return result, stdout, stderr, display_data, and error.
|
||||
Execute Python code in a Jupyter notebook cell. The tool will return the result, stdout, stderr, display_data, and error.
|
||||
If the code needs to use a file, ALWAYS pass the file path in the sandbox_files argument.
|
||||
You have a maximum of 3 retries to get the code to run successfully.
|
||||
|
||||
Parameters:
|
||||
code (str): The python code to be executed in a single cell.
|
||||
code (str): The Python code to be executed in a single cell.
|
||||
sandbox_files (List[str]): List of local file paths to be used by the code. The tool will throw an error if a file is not found.
|
||||
retry_count (int): Number of times the tool has been retried.
|
||||
"""
|
||||
logger.info(
|
||||
f"\n{'='*50}\n> Running following AI-generated code:\n{code}\n{'='*50}"
|
||||
)
|
||||
exec = self.interpreter.notebook.exec_cell(code)
|
||||
if retry_count > 2:
|
||||
return E2BToolOutput(
|
||||
is_error=True,
|
||||
logs=Logs(
|
||||
stdout="",
|
||||
stderr="",
|
||||
display_data="",
|
||||
error="",
|
||||
),
|
||||
error_message="Failed to execute the code after 3 retries. Explain the error to the user and suggest a fix.",
|
||||
retry_count=retry_count,
|
||||
)
|
||||
|
||||
if exec.error:
|
||||
logger.error("Error when executing code", exec.error)
|
||||
output = E2BToolOutput(is_error=True, logs=exec.logs, results=[])
|
||||
else:
|
||||
if len(exec.results) == 0:
|
||||
output = E2BToolOutput(is_error=False, logs=exec.logs, results=[])
|
||||
if self.interpreter is None:
|
||||
self._init_interpreter(sandbox_files)
|
||||
|
||||
if self.interpreter and self.interpreter.notebook:
|
||||
logger.info(
|
||||
f"\n{'='*50}\n> Running following AI-generated code:\n{code}\n{'='*50}"
|
||||
)
|
||||
exec = self.interpreter.notebook.exec_cell(code)
|
||||
|
||||
if exec.error:
|
||||
error_message = f"The code failed to execute successfully. Error: {exec.error}. Try to fix the code and run again."
|
||||
logger.error(error_message)
|
||||
# Calling the generated code caused an error. Kill the interpreter and return the error to the LLM so it can try to fix the error
|
||||
try:
|
||||
self.interpreter.kill() # type: ignore
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self.interpreter = None
|
||||
output = E2BToolOutput(
|
||||
is_error=True,
|
||||
logs=exec.logs,
|
||||
results=[],
|
||||
error_message=error_message,
|
||||
retry_count=retry_count + 1,
|
||||
)
|
||||
else:
|
||||
results = self.parse_result(exec.results[0])
|
||||
output = E2BToolOutput(is_error=False, logs=exec.logs, results=results)
|
||||
return output
|
||||
if len(exec.results) == 0:
|
||||
output = E2BToolOutput(is_error=False, logs=exec.logs, results=[])
|
||||
else:
|
||||
results = self._parse_result(exec.results[0])
|
||||
output = E2BToolOutput(
|
||||
is_error=False,
|
||||
logs=exec.logs,
|
||||
results=results,
|
||||
retry_count=retry_count + 1,
|
||||
)
|
||||
return output
|
||||
else:
|
||||
raise ValueError("Interpreter is not initialized.")
|
||||
|
||||
|
||||
def get_tools(**kwargs):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from llama_index.tools.openapi import OpenAPIToolSpec
|
||||
from llama_index.tools.requests import RequestsToolSpec
|
||||
|
||||
@@ -43,11 +44,12 @@ class OpenAPIActionToolSpec(OpenAPIToolSpec, RequestsToolSpec):
|
||||
Returns:
|
||||
List[Document]: A list of Document objects.
|
||||
"""
|
||||
import yaml
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import yaml # type: ignore
|
||||
|
||||
if uri.startswith("http"):
|
||||
import requests
|
||||
import requests # type: ignore
|
||||
|
||||
response = requests.get(uri)
|
||||
if response.status_code != 200:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Open Meteo weather map tool spec."""
|
||||
|
||||
import logging
|
||||
import requests
|
||||
import pytz
|
||||
|
||||
import pytz # type: ignore
|
||||
import requests # type: ignore
|
||||
from llama_index.core.tools import FunctionTool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -48,11 +48,13 @@ export type CodeArtifact = {
|
||||
port: number | null;
|
||||
file_path: string;
|
||||
code: string;
|
||||
files?: string[];
|
||||
};
|
||||
|
||||
export type CodeGeneratorParameter = {
|
||||
requirement: string;
|
||||
oldCode?: string;
|
||||
sandboxFiles?: string[];
|
||||
};
|
||||
|
||||
export type CodeGeneratorToolParams = {
|
||||
@@ -75,6 +77,15 @@ const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<CodeGeneratorParameter>> =
|
||||
description: "The existing code to be modified",
|
||||
nullable: true,
|
||||
},
|
||||
sandboxFiles: {
|
||||
type: "array",
|
||||
description:
|
||||
"A list of sandbox file paths. Include these files if the code requires them.",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
required: ["requirement"],
|
||||
},
|
||||
@@ -93,6 +104,9 @@ export class CodeGeneratorTool implements BaseTool<CodeGeneratorParameter> {
|
||||
input.requirement,
|
||||
input.oldCode,
|
||||
);
|
||||
if (input.sandboxFiles) {
|
||||
artifact.files = input.sandboxFiles;
|
||||
}
|
||||
return artifact as JSONValue;
|
||||
} catch (error) {
|
||||
return { isError: true };
|
||||
|
||||
@@ -7,6 +7,8 @@ import path from "node:path";
|
||||
|
||||
export type InterpreterParameter = {
|
||||
code: string;
|
||||
sandboxFiles?: string[];
|
||||
retryCount?: number;
|
||||
};
|
||||
|
||||
export type InterpreterToolParams = {
|
||||
@@ -18,7 +20,9 @@ export type InterpreterToolParams = {
|
||||
export type InterpreterToolOutput = {
|
||||
isError: boolean;
|
||||
logs: Logs;
|
||||
text?: string;
|
||||
extraResult: InterpreterExtraResult[];
|
||||
retryCount?: number;
|
||||
};
|
||||
|
||||
type InterpreterExtraType =
|
||||
@@ -41,8 +45,10 @@ export type InterpreterExtraResult = {
|
||||
|
||||
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<InterpreterParameter>> = {
|
||||
name: "interpreter",
|
||||
description:
|
||||
"Execute python code in a Jupyter notebook cell and return any result, stdout, stderr, display_data, and error.",
|
||||
description: `Execute python code in a Jupyter notebook cell and return any result, stdout, stderr, display_data, and error.
|
||||
If the code needs to use a file, ALWAYS pass the file path in the sandbox_files argument.
|
||||
You have a maximum of 3 retries to get the code to run successfully.
|
||||
`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -50,6 +56,21 @@ const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<InterpreterParameter>> = {
|
||||
type: "string",
|
||||
description: "The python code to execute in a single cell.",
|
||||
},
|
||||
sandboxFiles: {
|
||||
type: "array",
|
||||
description:
|
||||
"List of local file paths to be used by the code. The tool will throw an error if a file is not found.",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
nullable: true,
|
||||
},
|
||||
retryCount: {
|
||||
type: "number",
|
||||
description: "The number of times the tool has been retried",
|
||||
default: 0,
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
required: ["code"],
|
||||
},
|
||||
@@ -57,6 +78,7 @@ const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<InterpreterParameter>> = {
|
||||
|
||||
export class InterpreterTool implements BaseTool<InterpreterParameter> {
|
||||
private readonly outputDir = "output/tools";
|
||||
private readonly uploadedFilesDir = "output/uploaded";
|
||||
private apiKey?: string;
|
||||
private fileServerURLPrefix?: string;
|
||||
metadata: ToolMetadata<JSONSchemaType<InterpreterParameter>>;
|
||||
@@ -80,33 +102,67 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
|
||||
}
|
||||
}
|
||||
|
||||
public async initInterpreter() {
|
||||
public async initInterpreter(input: InterpreterParameter) {
|
||||
if (!this.codeInterpreter) {
|
||||
this.codeInterpreter = await CodeInterpreter.create({
|
||||
apiKey: this.apiKey,
|
||||
});
|
||||
}
|
||||
// upload files to sandbox
|
||||
if (input.sandboxFiles) {
|
||||
console.log(`Uploading ${input.sandboxFiles.length} files to sandbox`);
|
||||
try {
|
||||
for (const filePath of input.sandboxFiles) {
|
||||
const fileName = path.basename(filePath);
|
||||
const localFilePath = path.join(this.uploadedFilesDir, fileName);
|
||||
const content = fs.readFileSync(localFilePath);
|
||||
await this.codeInterpreter?.files.write(filePath, content);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Got error when uploading files to sandbox", error);
|
||||
}
|
||||
}
|
||||
return this.codeInterpreter;
|
||||
}
|
||||
|
||||
public async codeInterpret(code: string): Promise<InterpreterToolOutput> {
|
||||
public async codeInterpret(
|
||||
input: InterpreterParameter,
|
||||
): Promise<InterpreterToolOutput> {
|
||||
console.log(
|
||||
`\n${"=".repeat(50)}\n> Running following AI-generated code:\n${code}\n${"=".repeat(50)}`,
|
||||
`Sandbox files: ${input.sandboxFiles}. Retry count: ${input.retryCount}`,
|
||||
);
|
||||
const interpreter = await this.initInterpreter();
|
||||
const exec = await interpreter.notebook.execCell(code);
|
||||
|
||||
if (input.retryCount && input.retryCount >= 3) {
|
||||
return {
|
||||
isError: true,
|
||||
logs: {
|
||||
stdout: [],
|
||||
stderr: [],
|
||||
},
|
||||
text: "Max retries reached",
|
||||
extraResult: [],
|
||||
};
|
||||
}
|
||||
|
||||
console.log(
|
||||
`\n${"=".repeat(50)}\n> Running following AI-generated code:\n${input.code}\n${"=".repeat(50)}`,
|
||||
);
|
||||
const interpreter = await this.initInterpreter(input);
|
||||
const exec = await interpreter.notebook.execCell(input.code);
|
||||
if (exec.error) console.error("[Code Interpreter error]", exec.error);
|
||||
const extraResult = await this.getExtraResult(exec.results[0]);
|
||||
const result: InterpreterToolOutput = {
|
||||
isError: !!exec.error,
|
||||
logs: exec.logs,
|
||||
text: exec.text,
|
||||
extraResult,
|
||||
retryCount: input.retryCount ? input.retryCount + 1 : 1,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
async call(input: InterpreterParameter): Promise<InterpreterToolOutput> {
|
||||
const result = await this.codeInterpret(input.code);
|
||||
const result = await this.codeInterpret(input);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { Document } from "llamaindex";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { getExtractors } from "../../engine/loader";
|
||||
import { DocumentFile } from "../streaming/annotations";
|
||||
|
||||
const MIME_TYPE_TO_EXT: Record<string, string> = {
|
||||
"application/pdf": "pdf",
|
||||
"text/plain": "txt",
|
||||
"text/csv": "csv",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
||||
"docx",
|
||||
};
|
||||
@@ -12,16 +16,45 @@ const MIME_TYPE_TO_EXT: Record<string, string> = {
|
||||
const UPLOADED_FOLDER = "output/uploaded";
|
||||
|
||||
export async function storeAndParseFile(
|
||||
filename: string,
|
||||
name: string,
|
||||
fileBuffer: Buffer,
|
||||
mimeType: string,
|
||||
): Promise<DocumentFile> {
|
||||
const file = await storeFile(name, fileBuffer, mimeType);
|
||||
const documents: Document[] = await parseFile(fileBuffer, name, mimeType);
|
||||
// Update document IDs in the file metadata
|
||||
file.refs = documents.map((document) => document.id_ as string);
|
||||
return file;
|
||||
}
|
||||
|
||||
export async function storeFile(
|
||||
name: string,
|
||||
fileBuffer: Buffer,
|
||||
mimeType: string,
|
||||
) {
|
||||
const fileExt = MIME_TYPE_TO_EXT[mimeType];
|
||||
if (!fileExt) throw new Error(`Unsupported document type: ${mimeType}`);
|
||||
|
||||
const fileId = crypto.randomUUID();
|
||||
const newFilename = `${sanitizeFileName(name)}_${fileId}.${fileExt}`;
|
||||
const filepath = path.join(UPLOADED_FOLDER, newFilename);
|
||||
const fileUrl = await saveDocument(filepath, fileBuffer);
|
||||
return {
|
||||
id: fileId,
|
||||
name: newFilename,
|
||||
size: fileBuffer.length,
|
||||
type: fileExt,
|
||||
url: fileUrl,
|
||||
refs: [] as string[],
|
||||
} as DocumentFile;
|
||||
}
|
||||
|
||||
export async function parseFile(
|
||||
fileBuffer: Buffer,
|
||||
filename: string,
|
||||
mimeType: string,
|
||||
) {
|
||||
const documents = await loadDocuments(fileBuffer, mimeType);
|
||||
const filepath = path.join(UPLOADED_FOLDER, filename);
|
||||
await saveDocument(filepath, fileBuffer);
|
||||
for (const document of documents) {
|
||||
document.metadata = {
|
||||
...document.metadata,
|
||||
@@ -48,12 +81,6 @@ 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.");
|
||||
}
|
||||
@@ -71,3 +98,8 @@ export async function saveDocument(filepath: string, content: string | Buffer) {
|
||||
console.log(`Saved document to ${filepath}. Reachable at URL: ${fileurl}`);
|
||||
return fileurl;
|
||||
}
|
||||
|
||||
function sanitizeFileName(fileName: string) {
|
||||
// Remove file extension and sanitize
|
||||
return fileName.split(".")[0].replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "llamaindex";
|
||||
|
||||
export async function runPipeline(
|
||||
currentIndex: VectorStoreIndex,
|
||||
currentIndex: VectorStoreIndex | null,
|
||||
documents: Document[],
|
||||
) {
|
||||
// Use ingestion pipeline to process the documents into nodes and add them to the vector store
|
||||
@@ -21,8 +21,18 @@ export async function runPipeline(
|
||||
],
|
||||
});
|
||||
const nodes = await pipeline.run({ documents });
|
||||
await currentIndex.insertNodes(nodes);
|
||||
currentIndex.storageContext.docStore.persist();
|
||||
console.log("Added nodes to the vector store.");
|
||||
return documents.map((document) => document.id_);
|
||||
if (currentIndex) {
|
||||
await currentIndex.insertNodes(nodes);
|
||||
currentIndex.storageContext.docStore.persist();
|
||||
console.log("Added nodes to the vector store.");
|
||||
return documents.map((document) => document.id_);
|
||||
} else {
|
||||
// Initialize a new index with the documents
|
||||
const newIndex = await VectorStoreIndex.fromDocuments(documents);
|
||||
newIndex.storageContext.docStore.persist();
|
||||
console.log(
|
||||
"Got empty index, created new index with the uploaded documents",
|
||||
);
|
||||
return documents.map((document) => document.id_);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,74 @@
|
||||
import { LLamaCloudFileService, VectorStoreIndex } from "llamaindex";
|
||||
import { Document, LLamaCloudFileService, VectorStoreIndex } from "llamaindex";
|
||||
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
|
||||
import { storeAndParseFile } from "./helper";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { DocumentFile } from "../streaming/annotations";
|
||||
import { parseFile, storeFile } from "./helper";
|
||||
import { runPipeline } from "./pipeline";
|
||||
|
||||
export async function uploadDocument(
|
||||
index: VectorStoreIndex | LlamaCloudIndex,
|
||||
filename: string,
|
||||
index: VectorStoreIndex | LlamaCloudIndex | null,
|
||||
name: string,
|
||||
raw: string,
|
||||
): Promise<string[]> {
|
||||
): Promise<DocumentFile> {
|
||||
const [header, content] = raw.split(",");
|
||||
const mimeType = header.replace("data:", "").replace(";base64", "");
|
||||
const fileBuffer = Buffer.from(content, "base64");
|
||||
|
||||
// Store file
|
||||
const fileMetadata = await storeFile(name, fileBuffer, mimeType);
|
||||
|
||||
// If the file is csv and has codeExecutorTool, we don't need to index the file.
|
||||
if (mimeType === "text/csv" && (await hasCodeExecutorTool())) {
|
||||
return fileMetadata;
|
||||
}
|
||||
let documentIds: string[] = [];
|
||||
if (index instanceof LlamaCloudIndex) {
|
||||
// trigger LlamaCloudIndex API to upload the file and run the pipeline
|
||||
const projectId = await index.getProjectId();
|
||||
const pipelineId = await index.getPipelineId();
|
||||
return [
|
||||
await LLamaCloudFileService.addFileToPipeline(
|
||||
projectId,
|
||||
pipelineId,
|
||||
new File([fileBuffer], filename, { type: mimeType }),
|
||||
{ private: "true" },
|
||||
),
|
||||
];
|
||||
try {
|
||||
documentIds = [
|
||||
await LLamaCloudFileService.addFileToPipeline(
|
||||
projectId,
|
||||
pipelineId,
|
||||
new File([fileBuffer], name, { type: mimeType }),
|
||||
{ private: "true" },
|
||||
),
|
||||
];
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof ReferenceError &&
|
||||
error.message.includes("File is not defined")
|
||||
) {
|
||||
throw new Error(
|
||||
"File class is not supported in the current Node.js version. Please use Node.js 20 or higher.",
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
// run the pipeline for other vector store indexes
|
||||
const documents: Document[] = await parseFile(
|
||||
fileBuffer,
|
||||
fileMetadata.name,
|
||||
mimeType,
|
||||
);
|
||||
documentIds = await runPipeline(index, documents);
|
||||
}
|
||||
|
||||
// run the pipeline for other vector store indexes
|
||||
const documents = await storeAndParseFile(filename, fileBuffer, mimeType);
|
||||
return runPipeline(index, documents);
|
||||
// Update file metadata with document IDs
|
||||
fileMetadata.refs = documentIds;
|
||||
return fileMetadata;
|
||||
}
|
||||
|
||||
const hasCodeExecutorTool = async () => {
|
||||
const codeExecutorTools = ["interpreter", "artifact"];
|
||||
|
||||
const configFile = path.join("config", "tools.json");
|
||||
const toolConfig = JSON.parse(await fs.readFile(configFile, "utf8"));
|
||||
|
||||
const localTools = toolConfig.local || {};
|
||||
// Check if local tools contains codeExecutorTools
|
||||
return codeExecutorTools.some((tool) => localTools[tool] !== undefined);
|
||||
};
|
||||
|
||||
@@ -3,17 +3,13 @@ import { MessageContent, MessageContentDetail } from "llamaindex";
|
||||
|
||||
export type DocumentFileType = "csv" | "pdf" | "txt" | "docx";
|
||||
|
||||
export type DocumentFileContent = {
|
||||
type: "ref" | "text";
|
||||
value: string[] | string;
|
||||
};
|
||||
|
||||
export type DocumentFile = {
|
||||
id: string;
|
||||
filename: string;
|
||||
filesize: number;
|
||||
filetype: DocumentFileType;
|
||||
content: DocumentFileContent;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
url: string;
|
||||
refs?: string[];
|
||||
};
|
||||
|
||||
type Annotation = {
|
||||
@@ -29,28 +25,25 @@ export function isValidMessages(messages: Message[]): boolean {
|
||||
|
||||
export function retrieveDocumentIds(messages: Message[]): string[] {
|
||||
// retrieve document Ids from the annotations of all messages (if any)
|
||||
const documentFiles = retrieveDocumentFiles(messages);
|
||||
return documentFiles.map((file) => file.refs || []).flat();
|
||||
}
|
||||
|
||||
export function retrieveDocumentFiles(messages: Message[]): DocumentFile[] {
|
||||
const annotations = getAllAnnotations(messages);
|
||||
if (annotations.length === 0) return [];
|
||||
|
||||
const ids: string[] = [];
|
||||
|
||||
const files: DocumentFile[] = [];
|
||||
for (const { type, data } of annotations) {
|
||||
if (
|
||||
type === "document_file" &&
|
||||
"files" in data &&
|
||||
Array.isArray(data.files)
|
||||
) {
|
||||
const files = data.files as DocumentFile[];
|
||||
for (const file of files) {
|
||||
if (Array.isArray(file.content.value)) {
|
||||
// it's an array, so it's an array of doc IDs
|
||||
ids.push(...file.content.value);
|
||||
}
|
||||
}
|
||||
files.push(...data.files);
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
return files;
|
||||
}
|
||||
|
||||
export function retrieveMessageContent(messages: Message[]): MessageContent {
|
||||
@@ -65,6 +58,35 @@ export function retrieveMessageContent(messages: Message[]): MessageContent {
|
||||
];
|
||||
}
|
||||
|
||||
function getFileContent(file: DocumentFile): string {
|
||||
let defaultContent = `=====File: ${file.name}=====\n`;
|
||||
// Include file URL if it's available
|
||||
const urlPrefix = process.env.FILESERVER_URL_PREFIX;
|
||||
let urlContent = "";
|
||||
if (urlPrefix) {
|
||||
if (file.url) {
|
||||
urlContent = `File URL: ${file.url}\n`;
|
||||
} else {
|
||||
urlContent = `File URL (instruction: do not update this file URL yourself): ${urlPrefix}/output/uploaded/${file.name}\n`;
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"Warning: FILESERVER_URL_PREFIX not set in environment variables. Can't use file server",
|
||||
);
|
||||
}
|
||||
defaultContent += urlContent;
|
||||
|
||||
// Include document IDs if it's available
|
||||
if (file.refs) {
|
||||
defaultContent += `Document IDs: ${file.refs}\n`;
|
||||
}
|
||||
// Include sandbox file paths
|
||||
const sandboxFilePath = `/tmp/${file.name}`;
|
||||
defaultContent += `Sandbox file path (instruction: only use sandbox path for artifact or code interpreter tool): ${sandboxFilePath}\n`;
|
||||
|
||||
return defaultContent;
|
||||
}
|
||||
|
||||
function getAllAnnotations(messages: Message[]): Annotation[] {
|
||||
return messages.flatMap((message) =>
|
||||
(message.annotations ?? []).map((annotation) =>
|
||||
@@ -131,25 +153,11 @@ function convertAnnotations(messages: Message[]): MessageContentDetail[] {
|
||||
"files" in data &&
|
||||
Array.isArray(data.files)
|
||||
) {
|
||||
// get all CSV files and convert their whole content to one text message
|
||||
// currently CSV files are the only files where we send the whole content - we don't use an index
|
||||
const csvFiles: DocumentFile[] = data.files.filter(
|
||||
(file: DocumentFile) => file.filetype === "csv",
|
||||
);
|
||||
if (csvFiles && csvFiles.length > 0) {
|
||||
const csvContents = csvFiles.map((file: DocumentFile) => {
|
||||
const fileContent = Array.isArray(file.content.value)
|
||||
? file.content.value.join("\n")
|
||||
: file.content.value;
|
||||
return "```csv\n" + fileContent + "\n```";
|
||||
});
|
||||
const text =
|
||||
"Use the following CSV content:\n" + csvContents.join("\n\n");
|
||||
content.push({
|
||||
type: "text",
|
||||
text,
|
||||
});
|
||||
}
|
||||
const fileContent = data.files.map(getFileContent).join("\n");
|
||||
content.push({
|
||||
type: "text",
|
||||
text: fileContent,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -172,3 +180,26 @@ function getValidAnnotation(annotation: JSONValue): Annotation {
|
||||
}
|
||||
return { type: annotation.type, data: annotation.data };
|
||||
}
|
||||
|
||||
// validate and get all annotations of a specific type or role from the frontend messages
|
||||
export function getAnnotations<
|
||||
T extends Annotation["data"] = Annotation["data"],
|
||||
>(
|
||||
messages: Message[],
|
||||
options?: {
|
||||
role?: Message["role"]; // message role
|
||||
type?: Annotation["type"]; // annotation type
|
||||
},
|
||||
): {
|
||||
type: string;
|
||||
data: T;
|
||||
}[] {
|
||||
const messagesByRole = options?.role
|
||||
? messages.filter((msg) => msg.role === options?.role)
|
||||
: messages;
|
||||
const annotations = getAllAnnotations(messagesByRole);
|
||||
const annotationsByType = options?.type
|
||||
? annotations.filter((a) => a.type === options.type)
|
||||
: annotations;
|
||||
return annotationsByType as { type: string; data: T }[];
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export function createCallbackManager(stream: StreamData) {
|
||||
callbackManager.on("retrieve-end", (data) => {
|
||||
const { nodes, query } = data.detail;
|
||||
appendSourceData(stream, nodes);
|
||||
appendEventData(stream, `Retrieving context for query: '${query}'`);
|
||||
appendEventData(stream, `Retrieving context for query: '${query.query}'`);
|
||||
appendEventData(
|
||||
stream,
|
||||
`Retrieved ${nodes.length} sources to use as context for the query`,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
FILE_EXT_TO_READER,
|
||||
SimpleDirectoryReader,
|
||||
} from "llamaindex/readers/SimpleDirectoryReader";
|
||||
} from "llamaindex/readers/index";
|
||||
|
||||
export const DATA_DIR = "./data";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { LlamaParseReader } from "llamaindex";
|
||||
import {
|
||||
FILE_EXT_TO_READER,
|
||||
SimpleDirectoryReader,
|
||||
} from "llamaindex/readers/SimpleDirectoryReader";
|
||||
} from "llamaindex/readers/index";
|
||||
|
||||
export const DATA_DIR = "./data";
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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 app.engine.query_filter import generate_filters
|
||||
from app.workflows import create_workflow
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, status
|
||||
|
||||
chat_router = r = APIRouter()
|
||||
@@ -23,20 +23,20 @@ async def chat(
|
||||
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
|
||||
doc_ids = data.get_chat_document_ids()
|
||||
filters = generate_filters(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)
|
||||
workflow = create_workflow(
|
||||
chat_history=messages, params=params, filters=filters
|
||||
)
|
||||
|
||||
event_handler = workflow.run(input=last_message_content, streaming=True)
|
||||
return VercelStreamResponse(
|
||||
request=request,
|
||||
chat_data=data,
|
||||
event_handler=event_handler,
|
||||
events=engine.stream_events(),
|
||||
events=workflow.stream_events(),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("Error in chat engine", exc_info=True)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from abc import ABC
|
||||
from typing import AsyncGenerator, List
|
||||
from typing import AsyncGenerator, Awaitable, List
|
||||
|
||||
from aiostream import stream
|
||||
from app.agents.single import AgentRunEvent, AgentRunResult
|
||||
from app.api.routers.models import ChatData, Message
|
||||
from app.api.services.suggestion import NextQuestionSuggestion
|
||||
from fastapi import Request
|
||||
@@ -13,7 +12,7 @@ from fastapi.responses import StreamingResponse
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class VercelStreamResponse(StreamingResponse, ABC):
|
||||
class VercelStreamResponse(StreamingResponse):
|
||||
"""
|
||||
Base class to convert the response from the chat engine to the streaming format expected by Vercel
|
||||
"""
|
||||
@@ -23,33 +22,40 @@ class VercelStreamResponse(StreamingResponse, ABC):
|
||||
|
||||
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)
|
||||
|
||||
self.chat_data = chat_data
|
||||
content = self.content_generator(*args, **kwargs)
|
||||
super().__init__(content=content)
|
||||
|
||||
async def content_generator(self, stream):
|
||||
async def content_generator(self, event_handler, events):
|
||||
stream = self._create_stream(
|
||||
self.request, self.chat_data, event_handler, events
|
||||
)
|
||||
is_stream_started = False
|
||||
try:
|
||||
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("")
|
||||
|
||||
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
|
||||
yield output
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Stopping workflow")
|
||||
await event_handler.cancel_run()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Unexpected error in content_generator: {str(e)}", exc_info=True
|
||||
)
|
||||
finally:
|
||||
logger.info("The stream has been stopped!")
|
||||
|
||||
def _create_stream(
|
||||
self,
|
||||
request: Request,
|
||||
chat_data: ChatData,
|
||||
event_handler: AgentRunResult | AsyncGenerator,
|
||||
events: AsyncGenerator[AgentRunEvent, None],
|
||||
event_handler: Awaitable,
|
||||
events: AsyncGenerator,
|
||||
verbose: bool = True,
|
||||
):
|
||||
# Yield the text response
|
||||
@@ -57,15 +63,17 @@ class VercelStreamResponse(StreamingResponse, ABC):
|
||||
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
|
||||
final_response += str(token.delta)
|
||||
yield self.convert_text(token.delta)
|
||||
else:
|
||||
if hasattr(result, "response"):
|
||||
content = result.response.message.content
|
||||
if content:
|
||||
for token in content:
|
||||
final_response += str(token)
|
||||
yield self.convert_text(token)
|
||||
|
||||
# Generate next questions if next question prompt is configured
|
||||
question_data = await self._generate_next_questions(
|
||||
@@ -79,7 +87,7 @@ class VercelStreamResponse(StreamingResponse, ABC):
|
||||
# Yield the events from the event handler
|
||||
async def _event_generator():
|
||||
async for event in events:
|
||||
event_response = self._event_to_response(event)
|
||||
event_response = event.to_response()
|
||||
if verbose:
|
||||
logger.debug(event_response)
|
||||
if event_response is not None:
|
||||
@@ -88,13 +96,6 @@ class VercelStreamResponse(StreamingResponse, ABC):
|
||||
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
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from llama_index.core.workflow import Event
|
||||
|
||||
|
||||
class AgentRunEventType(Enum):
|
||||
TEXT = "text"
|
||||
PROGRESS = "progress"
|
||||
|
||||
|
||||
class AgentRunEvent(Event):
|
||||
name: str
|
||||
msg: str
|
||||
event_type: AgentRunEventType = AgentRunEventType.TEXT
|
||||
data: Optional[dict] = None
|
||||
|
||||
def to_response(self) -> dict:
|
||||
return {
|
||||
"type": "agent",
|
||||
"data": {
|
||||
"agent": self.name,
|
||||
"type": self.event_type.value,
|
||||
"text": self.msg,
|
||||
"data": self.data,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from app.workflows.events import AgentRunEvent
|
||||
from app.workflows.tools import ToolCallResponse, call_tools, chat_with_tools
|
||||
from llama_index.core.base.llms.types import ChatMessage
|
||||
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.types import BaseTool
|
||||
from llama_index.core.workflow import (
|
||||
Context,
|
||||
Event,
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
Workflow,
|
||||
step,
|
||||
)
|
||||
|
||||
|
||||
class InputEvent(Event):
|
||||
input: list[ChatMessage]
|
||||
|
||||
|
||||
class ToolCallEvent(Event):
|
||||
input: ToolCallResponse
|
||||
|
||||
|
||||
class FunctionCallingAgent(Workflow):
|
||||
"""
|
||||
A simple workflow to request LLM with tools independently.
|
||||
You can share the previous chat history to provide the context for the LLM.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args: Any,
|
||||
llm: FunctionCallingLLM | None = None,
|
||||
chat_history: Optional[List[ChatMessage]] = None,
|
||||
tools: List[BaseTool] | None = None,
|
||||
system_prompt: str | None = None,
|
||||
verbose: bool = False,
|
||||
timeout: float = 360.0,
|
||||
name: str,
|
||||
write_events: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(*args, verbose=verbose, timeout=timeout, **kwargs) # type: ignore
|
||||
self.tools = tools or []
|
||||
self.name = name
|
||||
self.write_events = write_events
|
||||
|
||||
if llm is None:
|
||||
llm = Settings.llm
|
||||
self.llm = llm
|
||||
if not self.llm.metadata.is_function_calling_model:
|
||||
raise ValueError("The provided LLM must support function calling.")
|
||||
|
||||
self.system_prompt = system_prompt
|
||||
|
||||
self.memory = ChatMemoryBuffer.from_defaults(
|
||||
llm=self.llm, chat_history=chat_history
|
||||
)
|
||||
self.sources = [] # type: ignore
|
||||
|
||||
@step()
|
||||
async def prepare_chat_history(self, ctx: Context, ev: StartEvent) -> InputEvent:
|
||||
# clear sources
|
||||
self.sources = []
|
||||
|
||||
# set streaming
|
||||
ctx.data["streaming"] = getattr(ev, "streaming", False)
|
||||
|
||||
# set system prompt
|
||||
if self.system_prompt is not None:
|
||||
system_msg = ChatMessage(role="system", content=self.system_prompt)
|
||||
self.memory.put(system_msg)
|
||||
|
||||
# get user input
|
||||
user_input = ev.input
|
||||
user_msg = ChatMessage(role="user", content=user_input)
|
||||
self.memory.put(user_msg)
|
||||
|
||||
if self.write_events:
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(name=self.name, msg=f"Start to work on: {user_input}")
|
||||
)
|
||||
|
||||
return InputEvent(input=self.memory.get())
|
||||
|
||||
@step()
|
||||
async def handle_llm_input(
|
||||
self,
|
||||
ctx: Context,
|
||||
ev: InputEvent,
|
||||
) -> ToolCallEvent | StopEvent:
|
||||
chat_history = ev.input
|
||||
|
||||
response = await chat_with_tools(
|
||||
self.llm,
|
||||
self.tools,
|
||||
chat_history,
|
||||
)
|
||||
is_tool_call = isinstance(response, ToolCallResponse)
|
||||
if not is_tool_call:
|
||||
if ctx.data["streaming"]:
|
||||
return StopEvent(result=response)
|
||||
else:
|
||||
full_response = ""
|
||||
async for chunk in response.generator:
|
||||
full_response += chunk.message.content
|
||||
return StopEvent(result=full_response)
|
||||
return ToolCallEvent(input=response)
|
||||
|
||||
@step()
|
||||
async def handle_tool_calls(self, ctx: Context, ev: ToolCallEvent) -> InputEvent:
|
||||
tool_calls = ev.input.tool_calls
|
||||
tool_call_message = ev.input.tool_call_message
|
||||
self.memory.put(tool_call_message)
|
||||
tool_messages = await call_tools(self.name, self.tools, ctx, tool_calls)
|
||||
self.memory.put_messages(tool_messages)
|
||||
return InputEvent(input=self.memory.get())
|
||||
@@ -0,0 +1,237 @@
|
||||
import logging
|
||||
import uuid
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, AsyncGenerator, Callable, Optional
|
||||
|
||||
from app.workflows.events import AgentRunEvent, AgentRunEventType
|
||||
from llama_index.core.base.llms.types import ChatMessage, ChatResponse, MessageRole
|
||||
from llama_index.core.llms.function_calling import FunctionCallingLLM
|
||||
from llama_index.core.tools import (
|
||||
BaseTool,
|
||||
FunctionTool,
|
||||
ToolOutput,
|
||||
ToolSelection,
|
||||
)
|
||||
from llama_index.core.workflow import Context
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class ContextAwareTool(FunctionTool, ABC):
|
||||
@abstractmethod
|
||||
async def acall(self, ctx: Context, input: Any) -> ToolOutput: # type: ignore
|
||||
pass
|
||||
|
||||
|
||||
class ResponseGenerator(BaseModel):
|
||||
"""
|
||||
A response generator from chat_with_tools.
|
||||
"""
|
||||
|
||||
generator: AsyncGenerator[ChatResponse | None, None]
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
|
||||
class ChatWithToolsResponse(BaseModel):
|
||||
"""
|
||||
A tool call response from chat_with_tools.
|
||||
"""
|
||||
|
||||
tool_calls: Optional[list[ToolSelection]]
|
||||
tool_call_message: Optional[ChatMessage]
|
||||
generator: Optional[AsyncGenerator[ChatResponse | None, None]]
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
def is_calling_different_tools(self) -> bool:
|
||||
tool_names = {tool_call.tool_name for tool_call in self.tool_calls}
|
||||
return len(tool_names) > 1
|
||||
|
||||
def has_tool_calls(self) -> bool:
|
||||
return self.tool_calls is not None and len(self.tool_calls) > 0
|
||||
|
||||
def tool_name(self) -> str:
|
||||
assert self.has_tool_calls()
|
||||
assert not self.is_calling_different_tools()
|
||||
return self.tool_calls[0].tool_name
|
||||
|
||||
async def full_response(self) -> str:
|
||||
assert self.generator is not None
|
||||
full_response = ""
|
||||
async for chunk in self.generator:
|
||||
full_response += chunk.message.content
|
||||
return full_response
|
||||
|
||||
|
||||
async def chat_with_tools( # type: ignore
|
||||
llm: FunctionCallingLLM,
|
||||
tools: list[BaseTool],
|
||||
chat_history: list[ChatMessage],
|
||||
) -> ChatWithToolsResponse:
|
||||
"""
|
||||
Request LLM to call tools or not.
|
||||
This function doesn't change the memory.
|
||||
"""
|
||||
generator = _tool_call_generator(llm, tools, chat_history)
|
||||
is_tool_call = await generator.__anext__()
|
||||
if is_tool_call:
|
||||
# Last chunk is the full response
|
||||
# Wait for the last chunk
|
||||
full_response = None
|
||||
async for chunk in generator:
|
||||
full_response = chunk
|
||||
assert isinstance(full_response, ChatResponse)
|
||||
return ChatWithToolsResponse(
|
||||
tool_calls=llm.get_tool_calls_from_response(full_response),
|
||||
tool_call_message=full_response.message,
|
||||
generator=None,
|
||||
)
|
||||
else:
|
||||
return ChatWithToolsResponse(
|
||||
tool_calls=None,
|
||||
tool_call_message=None,
|
||||
generator=generator,
|
||||
)
|
||||
|
||||
|
||||
async def call_tools(
|
||||
ctx: Context,
|
||||
agent_name: str,
|
||||
tools: list[BaseTool],
|
||||
tool_calls: list[ToolSelection],
|
||||
emit_agent_events: bool = True,
|
||||
) -> list[ChatMessage]:
|
||||
if len(tool_calls) == 0:
|
||||
return []
|
||||
|
||||
tools_by_name = {tool.metadata.get_name(): tool for tool in tools}
|
||||
if len(tool_calls) == 1:
|
||||
return [
|
||||
await call_tool(
|
||||
ctx,
|
||||
tools_by_name[tool_calls[0].tool_name],
|
||||
tool_calls[0],
|
||||
lambda msg: ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name=agent_name,
|
||||
msg=msg,
|
||||
)
|
||||
),
|
||||
)
|
||||
]
|
||||
# Multiple tool calls, show progress
|
||||
tool_msgs: list[ChatMessage] = []
|
||||
|
||||
progress_id = str(uuid.uuid4())
|
||||
total_steps = len(tool_calls)
|
||||
if emit_agent_events:
|
||||
ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name=agent_name,
|
||||
msg=f"Making {total_steps} tool calls",
|
||||
)
|
||||
)
|
||||
for i, tool_call in enumerate(tool_calls):
|
||||
tool = tools_by_name.get(tool_call.tool_name)
|
||||
if not tool:
|
||||
tool_msgs.append(
|
||||
ChatMessage(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content=f"Tool {tool_call.tool_name} does not exist",
|
||||
)
|
||||
)
|
||||
continue
|
||||
tool_msg = await call_tool(
|
||||
ctx,
|
||||
tool,
|
||||
tool_call,
|
||||
event_emitter=lambda msg: ctx.write_event_to_stream(
|
||||
AgentRunEvent(
|
||||
name=agent_name,
|
||||
msg=msg,
|
||||
event_type=AgentRunEventType.PROGRESS,
|
||||
data={
|
||||
"id": progress_id,
|
||||
"total": total_steps,
|
||||
"current": i,
|
||||
},
|
||||
)
|
||||
),
|
||||
)
|
||||
tool_msgs.append(tool_msg)
|
||||
return tool_msgs
|
||||
|
||||
|
||||
async def call_tool(
|
||||
ctx: Context,
|
||||
tool: BaseTool,
|
||||
tool_call: ToolSelection,
|
||||
event_emitter: Optional[Callable[[str], None]],
|
||||
) -> ChatMessage:
|
||||
if event_emitter:
|
||||
event_emitter(
|
||||
f"Calling tool {tool_call.tool_name}, {str(tool_call.tool_kwargs)}"
|
||||
)
|
||||
try:
|
||||
if isinstance(tool, ContextAwareTool):
|
||||
if ctx is None:
|
||||
raise ValueError("Context is required for context aware tool")
|
||||
# inject context for calling an context aware tool
|
||||
response = await tool.acall(ctx=ctx, **tool_call.tool_kwargs)
|
||||
else:
|
||||
response = await tool.acall(**tool_call.tool_kwargs) # type: ignore
|
||||
return ChatMessage(
|
||||
role=MessageRole.TOOL,
|
||||
content=str(response.raw_output),
|
||||
additional_kwargs={
|
||||
"tool_call_id": tool_call.tool_id,
|
||||
"name": tool.metadata.get_name(),
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Got error in tool {tool_call.tool_name}: {str(e)}")
|
||||
if event_emitter:
|
||||
event_emitter(f"Got error in tool {tool_call.tool_name}: {str(e)}")
|
||||
return ChatMessage(
|
||||
role=MessageRole.TOOL,
|
||||
content=f"Error: {str(e)}",
|
||||
additional_kwargs={
|
||||
"tool_call_id": tool_call.tool_id,
|
||||
"name": tool.metadata.get_name(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def _tool_call_generator(
|
||||
llm: FunctionCallingLLM,
|
||||
tools: list[BaseTool],
|
||||
chat_history: list[ChatMessage],
|
||||
) -> AsyncGenerator[ChatResponse | bool, None]:
|
||||
response_stream = await llm.astream_chat_with_tools(
|
||||
tools,
|
||||
chat_history=chat_history,
|
||||
allow_parallel_tool_calls=False,
|
||||
)
|
||||
|
||||
full_response = None
|
||||
yielded_indicator = False
|
||||
async for chunk in response_stream:
|
||||
if "tool_calls" not in chunk.message.additional_kwargs:
|
||||
# Yield a boolean to indicate whether the response is a tool call
|
||||
if not yielded_indicator:
|
||||
yield False
|
||||
yielded_indicator = True
|
||||
|
||||
# if not a tool call, yield the chunks!
|
||||
yield chunk # type: ignore
|
||||
elif not yielded_indicator:
|
||||
# Yield the indicator for a tool call
|
||||
yield True
|
||||
yielded_indicator = True
|
||||
|
||||
full_response = chunk
|
||||
|
||||
if full_response:
|
||||
yield full_response # type: ignore
|
||||
@@ -1,13 +1,13 @@
|
||||
import { StopEvent } from "@llamaindex/core/workflow";
|
||||
import { Message, streamToResponse } from "ai";
|
||||
import { Request, Response } from "express";
|
||||
import { ChatMessage, ChatResponseChunk } from "llamaindex";
|
||||
import { 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 { messages, data }: { messages: Message[]; data?: any } = req.body;
|
||||
const userMessage = messages.pop();
|
||||
if (!messages || !userMessage || userMessage.role !== "user") {
|
||||
return res.status(400).json({
|
||||
@@ -16,8 +16,7 @@ export const chat = async (req: Request, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
const chatHistory = messages as ChatMessage[];
|
||||
const agent = createWorkflow(chatHistory);
|
||||
const agent = createWorkflow(messages, data);
|
||||
const result = agent.run<AsyncGenerator<ChatResponseChunk>>(
|
||||
userMessage.content,
|
||||
) as unknown as Promise<StopEvent<AsyncGenerator<ChatResponseChunk>>>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { initObservability } from "@/app/observability";
|
||||
import { StopEvent } from "@llamaindex/core/workflow";
|
||||
import { Message, StreamingTextResponse } from "ai";
|
||||
import { ChatMessage, ChatResponseChunk } from "llamaindex";
|
||||
import { ChatResponseChunk } from "llamaindex";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { initSettings } from "./engine/settings";
|
||||
import { createWorkflow } from "./workflow/factory";
|
||||
@@ -16,7 +16,7 @@ export const dynamic = "force-dynamic";
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { messages }: { messages: Message[] } = body;
|
||||
const { messages, data }: { messages: Message[]; data?: any } = body;
|
||||
const userMessage = messages.pop();
|
||||
if (!messages || !userMessage || userMessage.role !== "user") {
|
||||
return NextResponse.json(
|
||||
@@ -28,8 +28,7 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const chatHistory = messages as ChatMessage[];
|
||||
const agent = createWorkflow(chatHistory);
|
||||
const agent = createWorkflow(messages, data);
|
||||
// TODO: fix type in agent.run in LITS
|
||||
const result = agent.run<AsyncGenerator<ChatResponseChunk>>(
|
||||
userMessage.content,
|
||||
|
||||
@@ -143,7 +143,7 @@ export class FunctionCallingAgent extends Workflow {
|
||||
fullResponse = chunk;
|
||||
}
|
||||
|
||||
if (fullResponse) {
|
||||
if (fullResponse?.options && Object.keys(fullResponse.options).length) {
|
||||
memory.put({
|
||||
role: "assistant",
|
||||
content: "",
|
||||
@@ -182,7 +182,9 @@ export class FunctionCallingAgent extends Workflow {
|
||||
// TODO: make logger optional in callTool in framework
|
||||
const toolOutput = await callTool(targetTool, call, {
|
||||
log: () => {},
|
||||
error: console.error.bind(console),
|
||||
error: (...args: unknown[]) => {
|
||||
console.error(`[Tool ${call.name} Error]:`, ...args);
|
||||
},
|
||||
warn: () => {},
|
||||
});
|
||||
toolMsgs.push({
|
||||
|
||||
@@ -16,11 +16,12 @@ import base64
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from typing import Dict, List, Optional, Union
|
||||
from dataclasses import asdict
|
||||
from typing import Any, 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 app.services.file import FileService
|
||||
from e2b_code_interpreter import CodeInterpreter, Sandbox # type: ignore
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -36,7 +37,7 @@ class ExecutionResult(BaseModel):
|
||||
template: str
|
||||
stdout: List[str]
|
||||
stderr: List[str]
|
||||
runtime_error: Optional[Dict[str, Union[str, List[str]]]] = None
|
||||
runtime_error: Optional[Dict[str, Any]] = None
|
||||
output_urls: List[Dict[str, str]]
|
||||
url: Optional[str]
|
||||
|
||||
@@ -54,15 +55,27 @@ class ExecutionResult(BaseModel):
|
||||
}
|
||||
|
||||
|
||||
class FileUpload(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
|
||||
|
||||
@sandbox_router.post("")
|
||||
async def create_sandbox(request: Request):
|
||||
request_data = await request.json()
|
||||
artifact_data = request_data.get("artifact", None)
|
||||
sandbox_files = artifact_data.get("files", [])
|
||||
|
||||
if not artifact_data:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Could not create artifact from the request data"
|
||||
)
|
||||
|
||||
try:
|
||||
artifact = CodeArtifact(**request_data["artifact"])
|
||||
artifact = CodeArtifact(**artifact_data)
|
||||
except Exception:
|
||||
logger.error(f"Could not create artifact from request data: {request_data}")
|
||||
return HTTPException(
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Could not create artifact from the request data"
|
||||
)
|
||||
|
||||
@@ -94,6 +107,10 @@ async def create_sandbox(request: Request):
|
||||
f"Installed dependencies: {', '.join(artifact.additional_dependencies)} in sandbox {sbx}"
|
||||
)
|
||||
|
||||
# Copy files
|
||||
if len(sandbox_files) > 0:
|
||||
_upload_files(sbx, sandbox_files)
|
||||
|
||||
# Copy code to disk
|
||||
if isinstance(artifact.code, list):
|
||||
for file in artifact.code:
|
||||
@@ -107,11 +124,12 @@ async def create_sandbox(request: Request):
|
||||
if artifact.template == "code-interpreter-multilang":
|
||||
result = sbx.notebook.exec_cell(artifact.code or "")
|
||||
output_urls = _download_cell_results(result.results)
|
||||
runtime_error = asdict(result.error) if result.error else None
|
||||
return ExecutionResult(
|
||||
template=artifact.template,
|
||||
stdout=result.logs.stdout,
|
||||
stderr=result.logs.stderr,
|
||||
runtime_error=result.error,
|
||||
runtime_error=runtime_error,
|
||||
output_urls=output_urls,
|
||||
url=None,
|
||||
).to_response()
|
||||
@@ -126,6 +144,19 @@ async def create_sandbox(request: Request):
|
||||
).to_response()
|
||||
|
||||
|
||||
def _upload_files(
|
||||
sandbox: Union[CodeInterpreter, Sandbox],
|
||||
sandbox_files: List[str] = [],
|
||||
) -> None:
|
||||
for file_path in sandbox_files:
|
||||
file_name = os.path.basename(file_path)
|
||||
local_file_path = os.path.join("output", "uploaded", file_name)
|
||||
with open(local_file_path, "rb") as f:
|
||||
content = f.read()
|
||||
sandbox.files.write(file_path, content)
|
||||
return None
|
||||
|
||||
|
||||
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
|
||||
@@ -141,14 +172,18 @@ def _download_cell_results(cell_results: Optional[List]) -> List[Dict[str, str]]
|
||||
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)
|
||||
file_name = f"{uuid.uuid4()}.{ext}"
|
||||
file_meta = FileService.save_file(
|
||||
content=buffer,
|
||||
file_name=file_name,
|
||||
save_dir=os.path.join("output", "tools"),
|
||||
)
|
||||
output.append(
|
||||
{
|
||||
"type": ext,
|
||||
"filename": file_meta.filename,
|
||||
"filename": file_meta.name,
|
||||
"url": file_meta.url,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import base64
|
||||
import mimetypes
|
||||
import os
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from app.engine.index import IndexConfig, get_index
|
||||
from llama_index.core import VectorStoreIndex
|
||||
from llama_index.core.ingestion import IngestionPipeline
|
||||
from llama_index.core.readers.file.base import (
|
||||
_try_loading_included_file_formats as get_file_loaders_map,
|
||||
)
|
||||
from llama_index.core.schema import Document
|
||||
from llama_index.indices.managed.llama_cloud.base import LlamaCloudIndex
|
||||
from llama_index.readers.file import FlatReader
|
||||
|
||||
|
||||
def get_llamaparse_parser():
|
||||
from app.engine.loaders import load_configs
|
||||
from app.engine.loaders.file import FileLoaderConfig, llama_parse_parser
|
||||
|
||||
config = load_configs()
|
||||
file_loader_config = FileLoaderConfig(**config["file"])
|
||||
if file_loader_config.use_llama_parse:
|
||||
return llama_parse_parser()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def default_file_loaders_map():
|
||||
default_loaders = get_file_loaders_map()
|
||||
default_loaders[".txt"] = FlatReader
|
||||
return default_loaders
|
||||
|
||||
|
||||
class PrivateFileService:
|
||||
PRIVATE_STORE_PATH = "output/uploaded"
|
||||
|
||||
@staticmethod
|
||||
def preprocess_base64_file(base64_content: str) -> Tuple[bytes, str | None]:
|
||||
header, data = base64_content.split(",", 1)
|
||||
mime_type = header.split(";")[0].split(":", 1)[1]
|
||||
extension = mimetypes.guess_extension(mime_type)
|
||||
# File data as bytes
|
||||
return base64.b64decode(data), extension
|
||||
|
||||
@staticmethod
|
||||
def store_and_parse_file(file_name, file_data, extension) -> List[Document]:
|
||||
# Store file to the private directory
|
||||
os.makedirs(PrivateFileService.PRIVATE_STORE_PATH, exist_ok=True)
|
||||
file_path = Path(os.path.join(PrivateFileService.PRIVATE_STORE_PATH, file_name))
|
||||
|
||||
# write file
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(file_data)
|
||||
|
||||
# Load file to documents
|
||||
# If LlamaParse is enabled, use it to parse the file
|
||||
# Otherwise, use the default file loaders
|
||||
reader = get_llamaparse_parser()
|
||||
if reader is None:
|
||||
reader_cls = default_file_loaders_map().get(extension)
|
||||
if reader_cls is None:
|
||||
raise ValueError(f"File extension {extension} is not supported")
|
||||
reader = reader_cls()
|
||||
documents = reader.load_data(file_path)
|
||||
# Add custom metadata
|
||||
for doc in documents:
|
||||
doc.metadata["file_name"] = file_name
|
||||
doc.metadata["private"] = "true"
|
||||
return documents
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
index_config = IndexConfig(**params)
|
||||
current_index = get_index(index_config)
|
||||
|
||||
# Insert the documents into the index
|
||||
if isinstance(current_index, LlamaCloudIndex):
|
||||
from app.engine.service import LLamaCloudFileService
|
||||
|
||||
project_id = current_index._get_project_id()
|
||||
pipeline_id = current_index._get_pipeline_id()
|
||||
# LlamaCloudIndex is a managed index so we can directly use the files
|
||||
upload_file = (file_name, BytesIO(file_data))
|
||||
return [
|
||||
LLamaCloudFileService.add_file_to_pipeline(
|
||||
project_id,
|
||||
pipeline_id,
|
||||
upload_file,
|
||||
custom_metadata={
|
||||
# Set private=true to mark the document as private user docs (required for filtering)
|
||||
"private": "true",
|
||||
},
|
||||
)
|
||||
]
|
||||
else:
|
||||
# First process documents into nodes
|
||||
documents = PrivateFileService.store_and_parse_file(
|
||||
file_name, file_data, extension
|
||||
)
|
||||
pipeline = IngestionPipeline()
|
||||
nodes = pipeline.run(documents=documents)
|
||||
|
||||
# Add the nodes to the index and persist it
|
||||
if current_index is None:
|
||||
current_index = VectorStoreIndex(nodes=nodes)
|
||||
else:
|
||||
current_index.insert_nodes(nodes=nodes)
|
||||
current_index.storage_context.persist(
|
||||
persist_dir=os.environ.get("STORAGE_DIR", "storage")
|
||||
)
|
||||
|
||||
# Return the document ids
|
||||
return [doc.doc_id for doc in documents]
|
||||
@@ -1,20 +1,18 @@
|
||||
# flake8: noqa: E402
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from llama_cloud import PipelineType
|
||||
|
||||
from app.settings import init_settings
|
||||
from llama_index.core.settings import Settings
|
||||
|
||||
import logging
|
||||
|
||||
from app.engine.index import get_client, get_index
|
||||
|
||||
import logging
|
||||
from app.engine.service import LLamaCloudFileService # type: ignore
|
||||
from app.settings import init_settings
|
||||
from llama_cloud import PipelineType
|
||||
from llama_index.core.readers import SimpleDirectoryReader
|
||||
from app.engine.service import LLamaCloudFileService
|
||||
from llama_index.core.settings import Settings
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger()
|
||||
@@ -80,13 +78,7 @@ def generate_datasource():
|
||||
f"Adding file {input_file} to pipeline {index.name} in project {index.project_name}"
|
||||
)
|
||||
LLamaCloudFileService.add_file_to_pipeline(
|
||||
project_id,
|
||||
pipeline_id,
|
||||
f,
|
||||
custom_metadata={
|
||||
# Set private=false to mark the document as public (required for filtering)
|
||||
"private": "false",
|
||||
},
|
||||
project_id, pipeline_id, f, custom_metadata={}
|
||||
)
|
||||
|
||||
logger.info("Finished generating the index")
|
||||
|
||||
@@ -5,7 +5,7 @@ def generate_filters(doc_ids):
|
||||
"""
|
||||
Generate public/private document filters based on the doc_ids and the vector store.
|
||||
"""
|
||||
# Using "is_empty" filter to include the documents don't have the "private" key because they're uploaded in LlamaCloud UI
|
||||
# public documents (ingested by "poetry run generate" or in the LlamaCloud UI) don't have the "private" field
|
||||
public_doc_filter = MetadataFilter(
|
||||
key="private",
|
||||
value=None,
|
||||
|
||||
@@ -14,7 +14,12 @@ async function loadAndIndex() {
|
||||
|
||||
// create vector store and a collection
|
||||
const collectionName = process.env.ASTRA_DB_COLLECTION!;
|
||||
const vectorStore = new AstraDBVectorStore();
|
||||
const vectorStore = new AstraDBVectorStore({
|
||||
params: {
|
||||
endpoint: process.env.ASTRA_DB_ENDPOINT!,
|
||||
token: process.env.ASTRA_DB_APPLICATION_TOKEN!,
|
||||
},
|
||||
});
|
||||
await vectorStore.createAndConnect(collectionName, {
|
||||
vector: {
|
||||
dimension: parseInt(process.env.EMBEDDING_DIM!),
|
||||
|
||||
@@ -5,7 +5,12 @@ import { checkRequiredEnvVars } from "./shared";
|
||||
|
||||
export async function getDataSource(params?: any) {
|
||||
checkRequiredEnvVars();
|
||||
const store = new AstraDBVectorStore();
|
||||
const store = new AstraDBVectorStore({
|
||||
params: {
|
||||
endpoint: process.env.ASTRA_DB_ENDPOINT!,
|
||||
token: process.env.ASTRA_DB_APPLICATION_TOKEN!,
|
||||
},
|
||||
});
|
||||
await store.connect(process.env.ASTRA_DB_COLLECTION!);
|
||||
return await VectorStoreIndex.fromVectorStore(store);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ async function* walk(dir: string): AsyncGenerator<string> {
|
||||
|
||||
async function loadAndIndex() {
|
||||
const index = await getDataSource();
|
||||
// ensure the index is available or create a new one
|
||||
await index.ensureIndex({ verbose: true });
|
||||
const projectId = await index.getProjectId();
|
||||
const pipelineId = await index.getPipelineId();
|
||||
|
||||
@@ -32,10 +34,23 @@ async function loadAndIndex() {
|
||||
for await (const filePath of walk(DATA_DIR)) {
|
||||
const buffer = await fs.readFile(filePath);
|
||||
const filename = path.basename(filePath);
|
||||
const file = new File([buffer], filename);
|
||||
await LLamaCloudFileService.addFileToPipeline(projectId, pipelineId, file, {
|
||||
private: "false",
|
||||
});
|
||||
try {
|
||||
await LLamaCloudFileService.addFileToPipeline(
|
||||
projectId,
|
||||
pipelineId,
|
||||
new File([buffer], filename),
|
||||
);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof ReferenceError &&
|
||||
error.message.includes("File is not defined")
|
||||
) {
|
||||
throw new Error(
|
||||
"File class is not supported in the current Node.js version. Please use Node.js 20 or higher.",
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Successfully uploaded documents to LlamaCloud!`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CloudRetrieveParams, MetadataFilter } from "llamaindex";
|
||||
|
||||
export function generateFilters(documentIds: string[]) {
|
||||
// public documents don't have the "private" field or it's set to "false"
|
||||
// public documents (ingested by "npm run generate" or in the LlamaCloud UI) don't have the "private" field
|
||||
const publicDocumentsFilter: MetadataFilter = {
|
||||
key: "private",
|
||||
operator: "is_empty",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user