Compare commits

..

32 Commits

Author SHA1 Message Date
github-actions[bot] b8f78612b8 Release 0.3.8 (#396)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-25 16:47:26 +07:00
Huu Le 4a8346900d feat: Add multi-agent financial report use case for TS (#394) 2024-10-25 16:44:56 +07:00
github-actions[bot] 42e63842d0 Release 0.3.7 (#395)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-25 14:34:55 +07:00
Huu Le fa803787e3 relative url (#393) 2024-10-25 14:13:34 +07:00
github-actions[bot] c5559d8e59 Release 0.3.6 (#392)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-23 17:51:46 +07:00
Huu Le 0182368744 Fix: UI streaming issue (#391) 2024-10-23 17:38:48 +07:00
github-actions[bot] ff46bd6153 Release 0.3.5 (#390)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-23 16:40:11 +07:00
Huu Le 2209409cdb Feature: Update multi-agent template to use financial report use case (#386)
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-10-23 16:36:12 +07:00
github-actions[bot] 623f8b811b Release 0.3.4 (#389)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-22 17:25:00 +07:00
Huu Le 384a1368dd Add mypy checker for importing and update CI condition (#387) 2024-10-22 17:00:52 +07:00
github-actions[bot] 189c0e3f6c Release 0.3.3 (#383)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-22 10:50:58 +07:00
Huu Le 99b8247bc9 Enhance data type (#378) 2024-10-17 16:37:14 +07:00
github-actions[bot] 74c5a15450 Release 0.3.2 (#381)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-17 11:39:38 +07:00
Marcus Schiesser 9293e330ac Update demo video in README.md 2024-10-17 11:38:22 +07:00
Marcus Schiesser 6d1b6b9372 docs: readme update for pro mode 2024-10-17 11:13:00 +07:00
github-actions[bot] a8162a9269 Release 0.3.1 (#377)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-16 15:23:09 +07:00
Huu Le f3577c50d6 add data scientist use case (directly using uploaded files) (#355)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
Co-authored-by: Thuc Pham <51660321+thucpn@users.noreply.github.com>
2024-10-16 14:00:59 +07:00
Huu Le a5f5c9dc9c fix always ask post installation action (#376) 2024-10-16 09:52:25 +07:00
Huu Le 2be68d1c7f ci: activate llamacloud for TS (#372)
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-10-15 13:40:47 +07:00
Thuc Pham 8c80cc05ce fix: enhance performance for codeblock (#347)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-10-15 12:21:08 +07:00
github-actions[bot] dfd4fd58ab Release 0.3.0 (#368)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-14 16:25:37 +07:00
Thuc Pham 0a69fe09fa fix: missing params when init Astra vectorstore (#373) 2024-10-14 16:03:41 +07:00
Marcus Schiesser de88b32208 fix: remove llamacloud for extractor 2024-10-14 15:35:59 +07:00
Marcus Schiesser ef88bff211 chore: upgrade reflex 2024-10-14 15:09:16 +07:00
Marcus Schiesser 7562cb48d6 docs: changeset 2024-10-14 13:41:22 +07:00
Marcus Schiesser 9dde6d0288 feat: simplify questions asked (#370) 2024-10-14 13:35:39 +07:00
Thuc Pham 98a82b0b25 docs: chroma env variables (#367) 2024-10-11 11:10:29 +07:00
github-actions[bot] 7db72b6f2e Release 0.2.19 (#365)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-10 18:41:25 +07:00
Thuc Pham 3d41488301 feat: use selected llamacloud for multiagent (#359) 2024-10-10 18:37:55 +07:00
github-actions[bot] 1ee05eaf4b Release 0.2.18 (#364)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-10 18:03:43 +07:00
Huu Le 75e1f6104c fix: TypeScript templates do not create a new LlamaCloud index or upload a file to an existing index. (#356) 2024-10-10 17:58:12 +07:00
Huu Le 88220f1dd2 feat: add canceling workflow for multiagent (#361) 2024-10-10 15:24:43 +07:00
110 changed files with 3364 additions and 1970 deletions
+1 -1
View File
@@ -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
+4
View File
@@ -51,3 +51,7 @@ e2e/cache
# build artifacts
create-llama-*.tgz
# vscode
.vscode
!.vscode/settings.json
+75
View File
@@ -1,5 +1,80 @@
# create-llama
## 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
+31 -41
View File
@@ -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, youll have a full-stack Next.js application that you can deploy to a host like [Vercel](https://vercel.com/) in just a few clicks. This uses [LlamaIndex.TS](https://www.npmjs.com/package/llamaindex), our TypeScript library.
- **Express**: if you want a more traditional Node.js application you can generate an Express backend. This also uses LlamaIndex.TS.
- **Python FastAPI**: if you select this option, youll get a backend powered by the [llama-index Python package](https://pypi.org/project/llama-index/), which you can deploy to a service like Render or fly.io.
- The back-end has two endpoints (one streaming, the other one non-streaming) that allow you to send the state of your chat and receive additional responses
- You add arbitrary data sources to your chat, like local files, websites, or data retrieved from a database.
- Turn your chat into an AI agent by adding tools (functions called by the LLM).
- **Python FastAPI**: if you select this option, youll get a separate backend powered by the [llama-index Python package](https://pypi.org/project/llama-index/), which you can deploy to a service like [Render](https://render.com/) or [fly.io](https://fly.io/). The separate Next.js front-end will connect to this backend.
- Each back-end has two endpoints:
- One streaming chat endpoint, that allow you to send the state of your chat and receive additional responses
- One endpoint to upload private files which can be used in your chat
- The app uses OpenAI by default, so you'll need an OpenAI API key, or you can customize it to use any of the dozens of LLMs we support.
Here's how it looks like:
@@ -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
+2
View File
@@ -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) {
+64 -60
View File
@@ -18,68 +18,72 @@ 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"];
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.",
);
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,
}) => {
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();
});
});
}
+7
View File
@@ -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;
+5
View File
@@ -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}`);
+19
View File
@@ -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,
+2 -2
View File
@@ -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
View File
@@ -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);
}
}
};
+2 -5
View File
@@ -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",
+2 -5
View File
@@ -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",
+2 -5
View File
@@ -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",
+2 -5
View File
@@ -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(
+2 -3
View File
@@ -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" },
+2 -5
View File
@@ -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",
+2 -5
View File
@@ -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",
+2 -5
View File
@@ -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",
+2 -5
View File
@@ -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
View File
@@ -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 ?? []);
}
+2 -2
View File
@@ -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"],
+10 -4
View File
@@ -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";
// 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
View File
@@ -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,
};
}
+64 -87
View File
@@ -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 -4
View File
@@ -1,6 +1,6 @@
{
"name": "create-llama",
"version": "0.2.17",
"version": "0.3.8",
"description": "Create LlamaIndex-powered apps with one command",
"keywords": [
"rag",
@@ -49,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",
@@ -59,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",
+11 -147
View File
@@ -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
View File
@@ -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 };
};
+30
View File
@@ -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,
}),
};
}
+64
View File
@@ -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;
};
+18
View File
@@ -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);
};
+404
View File
@@ -0,0 +1,404 @@
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;
}
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);
}
};
+181
View File
@@ -0,0 +1,181 @@
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"
| "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: "Code Artifact Agent", value: "code_artifact" },
{ title: "Structured extraction", value: "extractor" },
],
},
questionHandlers,
);
let language: TemplateFramework = "fastapi";
let llamaCloudKey = args.llamaCloudKey;
let useLlamaCloud = false;
if (appType !== "extractor") {
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,
},
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,
};
};
+36
View File
@@ -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;
};
+15
View File
@@ -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;
+178
View File
@@ -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;
}
@@ -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:
@@ -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!
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(
@@ -2,9 +2,9 @@ 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
from llama_index.core.chat_engine.types import ChatMessage
from llama_index.core.workflow import Workflow
@@ -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/financial_report/workflow.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,47 @@
from textwrap import dedent
from typing import List, Tuple
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
def _get_analyst_params() -> Tuple[List[type[FunctionTool]], str, str]:
tools = []
prompt_instructions = dedent(
"""
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 a textual format like 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.
"""
)
description = "Expert in analyzing financial data"
configured_tools = ToolFactory.from_env(map_result=True)
# Check if the interpreter tool is configured
if "interpreter" in configured_tools.keys():
tools.extend(configured_tools["interpreter"])
prompt_instructions += dedent("""
You are able to visualize the financial data using code interpreter tool.
It's very useful to create and include visualizations to the report (make sure you include the right code and data for the visualization).
Never include any code into the report, just the visualization.
""")
description += (
", able to visualize the financial data using code interpreter tool."
)
return tools, prompt_instructions, description
def create_analyst(chat_history: List[ChatMessage]):
tools, prompt_instructions, description = _get_analyst_params()
return FunctionCallingAgent(
name="analyst",
tools=tools,
description=description,
system_prompt=dedent(prompt_instructions),
chat_history=chat_history,
)
@@ -0,0 +1,44 @@
from textwrap import dedent
from typing import List, Tuple
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 BaseTool
def _get_reporter_params(
chat_history: List[ChatMessage],
) -> Tuple[List[type[BaseTool]], str, str]:
tools: List[type[BaseTool]] = []
description = "Expert in representing a financial report"
prompt_instructions = dedent(
"""
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 provided, add them to the right sections that are most relevant.
Use only the provided information to create the report. Do not make up any information yourself.
Finally, the report should be presented in markdown format.
"""
)
configured_tools = ToolFactory.from_env(map_result=True)
if "document_generator" in configured_tools: # type: ignore
tools.extend(configured_tools["document_generator"]) # type: ignore
prompt_instructions += (
"\nYou are also able to generate a file document (PDF/HTML) of the report."
)
description += " and generate a file document (PDF/HTML) of the report."
return tools, description, prompt_instructions
def create_reporter(chat_history: List[ChatMessage]):
tools, description, prompt_instructions = _get_reporter_params(chat_history)
return FunctionCallingAgent(
name="reporter",
tools=tools,
description=description,
system_prompt=prompt_instructions,
chat_history=chat_history,
)
@@ -0,0 +1,105 @@
import os
from textwrap import dedent
from typing import List, Optional
from app.engine.index import IndexConfig, get_index
from app.workflows.single import FunctionCallingAgent
from llama_index.core.chat_engine.types import ChatMessage
from llama_index.core.tools import BaseTool, QueryEngineTool, ToolMetadata
from llama_index.indices.managed.llama_cloud import LlamaCloudIndex
def _create_query_engine_tools(params=None) -> Optional[list[type[BaseTool]]]:
"""
Provide an agent worker that can be used to query the index.
"""
# Add query tool if index exists
index_config = IndexConfig(**(params or {}))
index = get_index(index_config)
if index is None:
return None
top_k = int(os.getenv("TOP_K", 5))
# Construct query engine tools
tools = []
# If index is LlamaCloudIndex, we need to add chunk and doc retriever tools
if isinstance(index, LlamaCloudIndex):
# Document retriever
doc_retriever = index.as_query_engine(
retriever_mode="files_via_content",
similarity_top_k=top_k,
)
chunk_retriever = index.as_query_engine(
retriever_mode="chunks",
similarity_top_k=top_k,
)
tools.append(
QueryEngineTool(
query_engine=doc_retriever,
metadata=ToolMetadata(
name="document_retriever",
description=dedent(
"""
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.
"""
),
),
)
)
tools.append(
QueryEngineTool(
query_engine=chunk_retriever,
metadata=ToolMetadata(
name="chunk_retriever",
description=dedent(
"""
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:
query_engine = index.as_query_engine(
**({"similarity_top_k": top_k} if top_k != 0 else {})
)
tools.append(
QueryEngineTool(
query_engine=query_engine,
metadata=ToolMetadata(
name="retrieve_information",
description="Use this tool to retrieve information about the text corpus from the index.",
),
)
)
return tools
def create_researcher(chat_history: List[ChatMessage], **kwargs):
"""
Researcher is an agent that take responsibility for using tools to complete a given task.
"""
tools = _create_query_engine_tools(**kwargs)
if tools is None:
raise ValueError("No tools found for researcher agent")
return FunctionCallingAgent(
name="researcher",
tools=tools,
description="expert in retrieving any unknown content from the corpus",
system_prompt=dedent(
"""
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 response with: "There is no new information, please reuse the information from the conversation."
"""
),
chat_history=chat_history,
)
@@ -0,0 +1,177 @@
from textwrap import dedent
from typing import AsyncGenerator, List, Optional
from app.agents.analyst import create_analyst
from app.agents.reporter import create_reporter
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
from llama_index.core.workflow import (
Context,
Event,
StartEvent,
StopEvent,
Workflow,
step,
)
def create_workflow(chat_history: Optional[List[ChatMessage]] = None, **kwargs):
researcher = create_researcher(
chat_history=chat_history,
**kwargs,
)
analyst = create_analyst(chat_history=chat_history)
reporter = create_reporter(chat_history=chat_history)
workflow = FinancialReportWorkflow(timeout=360, chat_history=chat_history)
workflow.add_workflows(
researcher=researcher,
analyst=analyst,
reporter=reporter,
)
return workflow
class ResearchEvent(Event):
input: str
class AnalyzeEvent(Event):
input: str
class ReportEvent(Event):
input: str
class FinancialReportWorkflow(Workflow):
def __init__(
self, timeout: int = 360, chat_history: Optional[List[ChatMessage]] = None
):
super().__init__(timeout=timeout)
self.chat_history = chat_history or []
@step()
async def start(self, ctx: Context, ev: StartEvent) -> ResearchEvent | ReportEvent:
# set streaming
ctx.data["streaming"] = getattr(ev, "streaming", False)
# start the workflow with researching about a topic
ctx.data["task"] = ev.input
ctx.data["user_input"] = ev.input
# Decision-making process
decision = await self._decide_workflow(ev.input, self.chat_history)
if decision != "publish":
return ResearchEvent(input=f"Research for this task: {ev.input}")
else:
chat_history_str = "\n".join(
[f"{msg.role}: {msg.content}" for msg in self.chat_history]
)
return ReportEvent(
input=f"Create a report based on the chat history\n{chat_history_str}\n\n and task: {ev.input}"
)
async def _decide_workflow(
self, input: str, chat_history: List[ChatMessage]
) -> str:
# TODO: Refactor this by using prompt generation
prompt_template = PromptTemplate(
dedent(
"""
You are an expert in decision-making, helping people create financial reports for the provided data.
If the user doesn't need to add or update anything, respond with 'publish'.
Otherwise, respond with 'research'.
Here is the chat history:
{chat_history}
The current user request is:
{input}
Given the chat history and the new user request, decide whether to create a report based on existing information.
Decision (respond with either 'not_publish' or 'publish'):
"""
)
)
chat_history_str = "\n".join(
[f"{msg.role}: {msg.content}" for msg in chat_history]
)
prompt = prompt_template.format(chat_history=chat_history_str, input=input)
output = await Settings.llm.acomplete(prompt)
decision = output.text.strip().lower()
return "publish" if decision == "publish" else "research"
@step()
async def research(
self, ctx: Context, ev: ResearchEvent, researcher: FunctionCallingAgent
) -> AnalyzeEvent:
result: AgentRunResult = await self.run_agent(ctx, researcher, ev.input)
content = result.response.message.content
return AnalyzeEvent(
input=dedent(
f"""
Given the following research content:
{content}
Provide a comprehensive analysis of the data for the user's request: {ctx.data["task"]}
"""
)
)
@step()
async def analyze(
self, ctx: Context, ev: AnalyzeEvent, analyst: FunctionCallingAgent
) -> ReportEvent | StopEvent:
result: AgentRunResult = await self.run_agent(ctx, analyst, ev.input)
content = result.response.message.content
return ReportEvent(
input=dedent(
f"""
Given the following analysis:
{content}
Create a report for the user's request: {ctx.data["task"]}
"""
)
)
@step()
async def report(
self, ctx: Context, ev: ReportEvent, reporter: FunctionCallingAgent
) -> StopEvent:
try:
result: AgentRunResult = await self.run_agent(
ctx, reporter, ev.input, streaming=ctx.data["streaming"]
)
return StopEvent(result=result)
except Exception as e:
ctx.write_event_to_stream(
AgentRunEvent(
name=reporter.name,
msg=f"Error creating a report: {e}",
)
)
return StopEvent(result=None)
async def run_agent(
self,
ctx: Context,
agent: FunctionCallingAgent,
input: str,
streaming: bool = False,
) -> AgentRunResult | AsyncGenerator:
handler = agent.run(input=input, streaming=streaming)
# bubble all events while running the executor to the planner
async for event in handler.stream_events():
# Don't write the StopEvent from sub task to the stream
if type(event) is not StopEvent:
ctx.write_event_to_stream(event)
return await handler
@@ -0,0 +1,12 @@
from typing import List, Optional
from app.agents.workflow import create_workflow
from llama_index.core.chat_engine.types import ChatMessage
from llama_index.core.workflow import Workflow
def get_chat_engine(
chat_history: Optional[List[ChatMessage]] = None, **kwargs
) -> Workflow:
agent_workflow = create_workflow(chat_history, **kwargs)
return agent_workflow
@@ -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",
@@ -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,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),
);
};
@@ -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."
@@ -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,70 @@
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, 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,6 +1,5 @@
import logging
from app.api.routers.events import EventCallbackHandler
from app.api.routers.models import (
ChatData,
)
@@ -23,7 +22,6 @@ 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
@@ -1,19 +1,19 @@
import asyncio
import json
import logging
from abc import ABC
from typing import AsyncGenerator, 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 app.workflows.single import AgentRunEvent, AgentRunResult
from fastapi import Request
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,26 +23,33 @@ 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,
@@ -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,
@@ -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,
@@ -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({
+45 -10
View File
@@ -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",
@@ -15,7 +15,7 @@ uvicorn = { extras = ["standard"], version = "^0.23.2" }
python-dotenv = "^1.0.0"
llama-index = "^0.11.1"
cachetools = "^5.3.3"
reflex = "^0.5.9"
reflex = "^0.6.2.post1"
[build-system]
requires = ["poetry-core"]
@@ -15,13 +15,12 @@
"dev": "concurrently \"tsup index.ts --format esm --dts --watch\" \"nodemon --watch dist/index.js\""
},
"dependencies": {
"@llamaindex/core": "^0.2.6",
"ai": "3.3.42",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"duck-duck-scrape": "^2.2.5",
"express": "^4.18.2",
"llamaindex": "0.6.18",
"llamaindex": "0.7.10",
"pdf2json": "3.0.5",
"ajv": "^8.12.0",
"@e2b/code-interpreter": "0.0.9-beta.3",
@@ -4,11 +4,11 @@ import { uploadDocument } from "./llamaindex/documents/upload";
export const chatUpload = async (req: Request, res: Response) => {
const {
filename,
name,
base64,
params,
}: { filename: string; base64: string; params?: any } = req.body;
if (!base64 || !filename) {
}: { name: string; base64: string; params?: any } = req.body;
if (!base64 || !name) {
return res.status(400).json({
error: "base64 and filename is required in the request body",
});
@@ -20,5 +20,5 @@ export const chatUpload = async (req: Request, res: Response) => {
"StorageContext is empty - call 'npm run generate' to generate the storage first",
});
}
return res.status(200).json(await uploadDocument(index, filename, base64));
return res.status(200).json(await uploadDocument(index, name, base64));
};
@@ -11,7 +11,7 @@ api_router.include_router(file_upload_router, prefix="/chat/upload")
# Dynamically adding additional routers if they exist
try:
from .sandbox import sandbox_router # noqa: F401
from .sandbox import sandbox_router # type: ignore
api_router.include_router(sandbox_router, prefix="/sandbox")
except ImportError:
@@ -1,8 +1,6 @@
import logging
from typing import List
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, status
from llama_index.core.chat_engine.types import NodeWithScore
from llama_index.core.llms import MessageRole
from app.api.routers.events import EventCallbackHandler
@@ -42,10 +40,11 @@ async def chat(
chat_engine = get_chat_engine(
filters=filters, params=params, event_handlers=[event_handler]
)
response = await chat_engine.astream_chat(last_message_content, messages)
process_response_nodes(response.source_nodes, background_tasks)
response = chat_engine.astream_chat(last_message_content, messages)
return VercelStreamResponse(request, event_handler, response, data)
return VercelStreamResponse(
request, event_handler, response, data, background_tasks
)
except Exception as e:
logger.exception("Error in chat engine", exc_info=True)
raise HTTPException(
@@ -76,17 +75,3 @@ async def chat_request(
result=Message(role=MessageRole.ASSISTANT, content=response.response),
nodes=SourceNodes.from_source_nodes(response.source_nodes),
)
def process_response_nodes(
nodes: List[NodeWithScore],
background_tasks: BackgroundTasks,
):
try:
# Start background tasks to download documents from LlamaCloud if needed
from app.engine.service import LLamaCloudFileService
LLamaCloudFileService.download_files_from_nodes(nodes, background_tasks)
except ImportError:
logger.debug("LlamaCloud is not configured. Skipping post processing of nodes")
pass
@@ -1,16 +1,53 @@
import logging
import os
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException
from app.api.routers.models import ChatConfig
config_router = r = APIRouter()
logger = logging.getLogger("uvicorn")
def _is_llama_cloud_service_configured():
try:
from app.engine.service import (
LLamaCloudFileService, # type: ignore # noqa: F401
)
return True
except ImportError:
return False
async def chat_llama_cloud_config():
from app.engine.service import LLamaCloudFileService # type: ignore
if not os.getenv("LLAMA_CLOUD_API_KEY"):
raise HTTPException(
status_code=500, detail="LlamaCloud API KEY is not configured"
)
projects = LLamaCloudFileService.get_all_projects_with_pipelines()
pipeline = os.getenv("LLAMA_CLOUD_INDEX_NAME")
project = os.getenv("LLAMA_CLOUD_PROJECT_NAME")
pipeline_config = None
if pipeline and project:
pipeline_config = {
"pipeline": pipeline,
"project": project,
}
return {
"projects": projects,
"pipeline": pipeline_config,
}
if _is_llama_cloud_service_configured():
logger.info("LlamaCloud is configured. Adding /config/llamacloud route.")
r.add_api_route("/llamacloud", chat_llama_cloud_config, methods=["GET"])
@r.get("")
async def chat_config() -> ChatConfig:
starter_questions = None
@@ -18,29 +55,3 @@ async def chat_config() -> ChatConfig:
if conversation_starters and conversation_starters.strip():
starter_questions = conversation_starters.strip().split("\n")
return ChatConfig(starter_questions=starter_questions)
try:
from app.engine.service import LLamaCloudFileService
print("LlamaCloud is configured. Adding /config/llamacloud route.")
@r.get("/llamacloud")
async def chat_llama_cloud_config():
projects = LLamaCloudFileService.get_all_projects_with_pipelines()
pipeline = os.getenv("LLAMA_CLOUD_INDEX_NAME")
project = os.getenv("LLAMA_CLOUD_PROJECT_NAME")
pipeline_config = None
if pipeline and project:
pipeline_config = {
"pipeline": pipeline,
"project": project,
}
return {
"projects": projects,
"pipeline": pipeline_config,
}
except ImportError:
print("LlamaCloud is not configured. Skipping adding /config/llamacloud route.")
pass
@@ -1,6 +1,6 @@
import logging
import os
from typing import Any, Dict, List, Literal, Optional, Union
from typing import Any, Dict, List, Optional
from llama_index.core.llms import ChatMessage, MessageRole
from llama_index.core.schema import NodeWithScore
@@ -8,27 +8,13 @@ from pydantic import BaseModel, Field, validator
from pydantic.alias_generators import to_camel
from app.config import DATA_DIR
from app.services.file import DocumentFile
logger = logging.getLogger("uvicorn")
class FileContent(BaseModel):
type: Literal["text", "ref"]
# If the file is pure text then the value is be a string
# otherwise, it's a list of document IDs
value: str | List[str]
class File(BaseModel):
id: str
content: FileContent
filename: str
filesize: int
filetype: str
class AnnotationFileData(BaseModel):
files: List[File] = Field(
files: List[DocumentFile] = Field(
default=[],
description="List of files",
)
@@ -36,19 +22,55 @@ class AnnotationFileData(BaseModel):
class Config:
json_schema_extra = {
"example": {
"csvFiles": [
"files": [
{
"content": "Name, Age\nAlice, 25\nBob, 30",
"filename": "example.csv",
"filesize": 123,
"id": "123",
"type": "text/csv",
"content": "data:text/plain;base64,aGVsbG8gd29ybGQK=",
"name": "example.txt",
}
]
}
}
alias_generator = to_camel
@staticmethod
def _get_url_llm_content(file: DocumentFile) -> Optional[str]:
url_prefix = os.getenv("FILESERVER_URL_PREFIX")
if url_prefix:
if file.url is not None:
return f"File URL: {file.url}\n"
else:
# Construct url from file name
return f"File URL (instruction: do not update this file URL yourself): {url_prefix}/output/uploaded/{file.name}\n"
else:
logger.warning(
"Warning: FILESERVER_URL_PREFIX not set in environment variables. Can't use file server"
)
return None
@classmethod
def _get_file_content(cls, file: DocumentFile) -> str:
"""
Construct content for LLM from the file metadata
"""
default_content = f"=====File: {file.name}=====\n"
# Include file URL if it's available
url_content = cls._get_url_llm_content(file)
if url_content:
default_content += url_content
# Include document IDs if it's available
if file.refs is not None:
default_content += f"Document IDs: {file.refs}\n"
# Include sandbox file path
sandbox_file_path = f"/tmp/{file.name}"
default_content += f"Sandbox file path (instruction: only use sandbox path for artifact or code interpreter tool): {sandbox_file_path}\n"
return default_content
def to_llm_content(self) -> Optional[str]:
file_contents = [self._get_file_content(file) for file in self.files]
if len(file_contents) == 0:
return None
return "Use data from following files content\n" + "\n".join(file_contents)
class AgentAnnotation(BaseModel):
agent: str
@@ -62,24 +84,13 @@ class ArtifactAnnotation(BaseModel):
class Annotation(BaseModel):
type: str
data: Union[AnnotationFileData, List[str], AgentAnnotation, ArtifactAnnotation]
data: AnnotationFileData | List[str] | AgentAnnotation | ArtifactAnnotation
def to_content(self) -> Optional[str]:
if self.type == "document_file":
if isinstance(self.data, AnnotationFileData):
# We only support generating context content for CSV files for now
csv_files = [file for file in self.data.files if file.filetype == "csv"]
if len(csv_files) > 0:
return "Use data from following CSV raw content\n" + "\n".join(
[
f"```csv\n{csv_file.content.value}\n```"
for csv_file in csv_files
]
)
else:
logger.warning(
f"Unexpected data type for document_file annotation: {type(self.data)}"
)
if self.type == "document_file" and isinstance(self.data, AnnotationFileData):
return self.data.to_llm_content()
elif self.type == "image":
raise NotImplementedError("Use image file is not supported yet!")
else:
logger.warning(
f"The annotation {self.type} is not supported for generating context content"
@@ -175,7 +186,11 @@ class ChatData(BaseModel):
):
tool_output = annotation.data.toolOutput
if tool_output and not tool_output.get("isError", False):
return tool_output.get("output", {}).get("code", None)
output = tool_output.get("output", {})
if isinstance(output, dict) and output.get("code"):
return output.get("code")
else:
return None
return None
def get_history_messages(
@@ -216,18 +231,26 @@ class ChatData(BaseModel):
Get the document IDs from the chat messages
"""
document_ids: List[str] = []
uploaded_files = self.get_document_files()
for _file in uploaded_files:
refs = getattr(_file, "refs", None)
if refs is not None:
document_ids.extend(refs)
return list(set(document_ids))
def get_document_files(self) -> List[DocumentFile]:
"""
Get the uploaded files from the chat data
"""
uploaded_files = []
for message in self.messages:
if message.role == MessageRole.USER and message.annotations is not None:
for annotation in message.annotations:
if (
annotation.type == "document_file"
and isinstance(annotation.data, AnnotationFileData)
and annotation.data.files is not None
if annotation.type == "document_file" and isinstance(
annotation.data, AnnotationFileData
):
for fi in annotation.data.files:
if fi.content.type == "ref":
document_ids += fi.content.value
return list(set(document_ids))
uploaded_files.extend(annotation.data.files)
return uploaded_files
class SourceNodes(BaseModel):
@@ -1,10 +1,11 @@
import logging
from typing import List, Any
from typing import Any
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from app.api.services.file import PrivateFileService
from app.api.routers.models import DocumentFile
from app.services.file import FileService
file_upload_router = r = APIRouter()
@@ -13,16 +14,19 @@ logger = logging.getLogger("uvicorn")
class FileUploadRequest(BaseModel):
base64: str
filename: str
name: str
params: Any = None
@r.post("")
def upload_file(request: FileUploadRequest) -> List[str]:
def upload_file(request: FileUploadRequest) -> DocumentFile:
"""
To upload a private file from the chat UI.
"""
try:
logger.info("Processing file")
return PrivateFileService.process_file(
request.filename, request.base64, request.params
logger.info(f"Processing file: {request.name}")
return FileService.process_private_file(
request.name, request.base64, request.params
)
except Exception as e:
logger.error(f"Error processing file: {e}", exc_info=True)
@@ -1,15 +1,19 @@
import json
from typing import List
import logging
from typing import Awaitable, List
from aiostream import stream
from fastapi import Request
from fastapi import BackgroundTasks, Request
from fastapi.responses import StreamingResponse
from llama_index.core.chat_engine.types import StreamingAgentChatResponse
from llama_index.core.schema import NodeWithScore
from app.api.routers.events import EventCallbackHandler
from app.api.routers.models import ChatData, Message, SourceNodes
from app.api.services.suggestion import NextQuestionSuggestion
logger = logging.getLogger("uvicorn")
class VercelStreamResponse(StreamingResponse):
"""
@@ -19,6 +23,103 @@ class VercelStreamResponse(StreamingResponse):
TEXT_PREFIX = "0:"
DATA_PREFIX = "8:"
def __init__(
self,
request: Request,
event_handler: EventCallbackHandler,
response: Awaitable[StreamingAgentChatResponse],
chat_data: ChatData,
background_tasks: BackgroundTasks,
):
content = VercelStreamResponse.content_generator(
request, event_handler, response, chat_data, background_tasks
)
super().__init__(content=content)
@classmethod
async def content_generator(
cls,
request: Request,
event_handler: EventCallbackHandler,
response: Awaitable[StreamingAgentChatResponse],
chat_data: ChatData,
background_tasks: BackgroundTasks,
):
chat_response_generator = cls._chat_response_generator(
response, background_tasks, event_handler, chat_data
)
event_generator = cls._event_generator(event_handler)
# Merge the chat response generator and the event generator
combine = stream.merge(chat_response_generator, event_generator)
is_stream_started = False
async with combine.stream() as streamer:
async for output in streamer:
if not is_stream_started:
is_stream_started = True
# Stream a blank message to start displaying the response in the UI
yield cls.convert_text("")
yield output
if await request.is_disconnected():
break
@classmethod
async def _event_generator(cls, event_handler: EventCallbackHandler):
"""
Yield the events from the event handler
"""
async for event in event_handler.async_event_gen():
event_response = event.to_response()
if event_response is not None:
yield cls.convert_data(event_response)
@classmethod
async def _chat_response_generator(
cls,
response: Awaitable[StreamingAgentChatResponse],
background_tasks: BackgroundTasks,
event_handler: EventCallbackHandler,
chat_data: ChatData,
):
"""
Yield the text response and source nodes from the chat engine
"""
# Wait for the response from the chat engine
result = await response
# Once we got a source node, start a background task to download the files (if needed)
cls._process_response_nodes(result.source_nodes, background_tasks)
# Yield the source nodes
yield cls.convert_data(
{
"type": "sources",
"data": {
"nodes": [
SourceNodes.from_source_node(node).model_dump()
for node in result.source_nodes
]
},
}
)
final_response = ""
async for token in result.async_response_gen():
final_response += token
yield cls.convert_text(token)
# Generate next questions if next question prompt is configured
question_data = await cls._generate_next_questions(
chat_data.messages, final_response
)
if question_data:
yield cls.convert_data(question_data)
# the text_generator is the leading stream, once it's finished, also finish the event stream
event_handler.is_done = True
@classmethod
def convert_text(cls, token: str):
# Escape newlines and double quotes to avoid breaking the stream
@@ -30,76 +131,23 @@ class VercelStreamResponse(StreamingResponse):
data_str = json.dumps(data)
return f"{cls.DATA_PREFIX}[{data_str}]\n"
def __init__(
self,
request: Request,
event_handler: EventCallbackHandler,
response: StreamingAgentChatResponse,
chat_data: ChatData,
@staticmethod
def _process_response_nodes(
source_nodes: List[NodeWithScore],
background_tasks: BackgroundTasks,
):
content = VercelStreamResponse.content_generator(
request, event_handler, response, chat_data
)
super().__init__(content=content)
try:
# Start background tasks to download documents from LlamaCloud if needed
from app.engine.service import LLamaCloudFileService # type: ignore
@classmethod
async def content_generator(
cls,
request: Request,
event_handler: EventCallbackHandler,
response: StreamingAgentChatResponse,
chat_data: ChatData,
):
# Yield the text response
async def _chat_response_generator():
final_response = ""
async for token in response.async_response_gen():
final_response += token
yield cls.convert_text(token)
# Generate next questions if next question prompt is configured
question_data = await cls._generate_next_questions(
chat_data.messages, final_response
LLamaCloudFileService.download_files_from_nodes(
source_nodes, background_tasks
)
if question_data:
yield cls.convert_data(question_data)
# the text_generator is the leading stream, once it's finished, also finish the event stream
event_handler.is_done = True
# Yield the source nodes
yield cls.convert_data(
{
"type": "sources",
"data": {
"nodes": [
SourceNodes.from_source_node(node).model_dump()
for node in response.source_nodes
]
},
}
except ImportError:
logger.debug(
"LlamaCloud is not configured. Skipping post processing of nodes"
)
# Yield the events from the event handler
async def _event_generator():
async for event in event_handler.async_event_gen():
event_response = event.to_response()
if event_response is not None:
yield cls.convert_data(event_response)
combine = stream.merge(_chat_response_generator(), _event_generator())
is_stream_started = False
async with combine.stream() as streamer:
async for output in streamer:
if not is_stream_started:
is_stream_started = True
# Stream a blank message to start the stream
yield cls.convert_text("")
yield output
if await request.is_disconnected():
break
pass
@staticmethod
async def _generate_next_questions(chat_history: List[Message], response: str):
@@ -1,64 +0,0 @@
import logging
import os
import uuid
from typing import Optional
from pydantic import BaseModel
logger = logging.getLogger(__name__)
class FileMetadata(BaseModel):
outputPath: str
filename: str
url: str
def save_file(
content: bytes | str,
file_name: Optional[str] = None,
file_path: Optional[str] = None,
) -> FileMetadata:
"""
Save the content to a file in the local file server (accessible via URL)
Args:
content (bytes | str): The content to save, either bytes or string.
file_name (Optional[str]): The name of the file. If not provided, a random name will be generated with .txt extension.
file_path (Optional[str]): The path to save the file to. If not provided, a random name will be generated.
Returns:
The metadata of the saved file.
"""
if file_name is not None and file_path is not None:
raise ValueError("Either file_name or file_path should be provided")
if file_path is None:
if file_name is None:
file_name = f"{uuid.uuid4()}.txt"
file_path = os.path.join(os.getcwd(), file_name)
else:
file_name = os.path.basename(file_path)
if isinstance(content, str):
content = content.encode()
try:
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "wb") as file:
file.write(content)
except PermissionError as e:
logger.error(f"Permission denied when writing to file {file_path}: {str(e)}")
raise
except IOError as e:
logger.error(f"IO error occurred when writing to file {file_path}: {str(e)}")
raise
except Exception as e:
logger.error(f"Unexpected error when writing to file {file_path}: {str(e)}")
raise
logger.info(f"Saved file to {file_path}")
return FileMetadata(
outputPath=file_path,
filename=file_name,
url=f"{os.getenv('FILESERVER_URL_PREFIX')}/{file_path}",
)
@@ -0,0 +1,300 @@
import base64
import logging
import mimetypes
import os
import re
import uuid
from io import BytesIO
from pathlib import Path
from typing import Dict, List, Optional, Tuple
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.core.tools.function_tool import FunctionTool
from llama_index.indices.managed.llama_cloud.base import LlamaCloudIndex
from llama_index.readers.file import FlatReader
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
PRIVATE_STORE_PATH = str(Path("output", "uploaded"))
TOOL_STORE_PATH = str(Path("output", "tools"))
LLAMA_CLOUD_STORE_PATH = str(Path("output", "llamacloud"))
class DocumentFile(BaseModel):
id: str
name: str # Stored file name
type: str = None
size: int = None
url: str = None
path: Optional[str] = Field(
None,
description="The stored file path. Used internally in the server.",
exclude=True,
)
refs: Optional[List[str]] = Field(
None, description="The document ids in the index."
)
class FileService:
"""
To store the files uploaded by the user and add them to the index.
"""
@classmethod
def process_private_file(
cls,
file_name: str,
base64_content: str,
params: Optional[dict] = None,
) -> DocumentFile:
"""
Store the uploaded file and index it if necessary.
"""
try:
from app.engine.index import IndexConfig, get_index
except ImportError as e:
raise ValueError("IndexConfig or get_index is not found") from e
if params is None:
params = {}
# Add the nodes to the index and persist it
index_config = IndexConfig(**params)
index = get_index(index_config)
# Preprocess and store the file
file_data, extension = cls._preprocess_base64_file(base64_content)
document_file = cls.save_file(
file_data,
file_name=file_name,
save_dir=PRIVATE_STORE_PATH,
)
tools = _get_available_tools()
code_executor_tools = ["interpreter", "artifact"]
# If the file is CSV and there is a code executor tool, we don't need to index.
if extension == "csv" and any(tool in tools for tool in code_executor_tools):
return document_file
else:
# Insert the file into the index and update document ids to the file metadata
if isinstance(index, LlamaCloudIndex):
doc_id = cls._add_file_to_llama_cloud_index(
index, document_file.name, file_data
)
# Add document ids to the file metadata
document_file.refs = [doc_id]
else:
documents = cls._load_file_to_documents(document_file)
cls._add_documents_to_vector_store_index(documents, index)
# Add document ids to the file metadata
document_file.refs = [doc.doc_id for doc in documents]
# Return the file metadata
return document_file
@classmethod
def save_file(
cls,
content: bytes | str,
file_name: str,
save_dir: Optional[str] = None,
) -> DocumentFile:
"""
Save the content to a file in the local file server (accessible via URL)
Args:
content (bytes | str): The content to save, either bytes or string.
file_name (str): The original name of the file.
save_dir (Optional[str]): The relative path from the current working directory. Defaults to the `output/uploaded` directory.
Returns:
The metadata of the saved file.
"""
if save_dir is None:
save_dir = os.path.join("output", "uploaded")
file_id = str(uuid.uuid4())
name, extension = os.path.splitext(file_name)
extension = extension.lstrip(".")
sanitized_name = _sanitize_file_name(name)
if extension == "":
raise ValueError("File is not supported!")
new_file_name = f"{sanitized_name}_{file_id}.{extension}"
file_path = os.path.join(save_dir, new_file_name)
if isinstance(content, str):
content = content.encode()
try:
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "wb") as file:
file.write(content)
except PermissionError as e:
logger.error(
f"Permission denied when writing to file {file_path}: {str(e)}"
)
raise
except IOError as e:
logger.error(
f"IO error occurred when writing to file {file_path}: {str(e)}"
)
raise
except Exception as e:
logger.error(f"Unexpected error when writing to file {file_path}: {str(e)}")
raise
logger.info(f"Saved file to {file_path}")
file_url_prefix = os.getenv("FILESERVER_URL_PREFIX")
if file_url_prefix is None:
logger.warning(
"FILESERVER_URL_PREFIX is not set, fallback to http://localhost:8000/api/files"
)
file_url_prefix = "http://localhost:8000/api/files"
file_size = os.path.getsize(file_path)
file_url = os.path.join(
file_url_prefix,
save_dir,
new_file_name,
)
return DocumentFile(
id=file_id,
name=new_file_name,
type=extension,
size=file_size,
path=file_path,
url=file_url,
refs=None,
)
@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).lstrip(".")
# File data as bytes
return base64.b64decode(data), extension
@staticmethod
def _load_file_to_documents(file: DocumentFile) -> List[Document]:
"""
Load the file from the private directory and return the documents
"""
_, extension = os.path.splitext(file.name)
extension = extension.lstrip(".")
# 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(f".{extension}")
if reader_cls is None:
raise ValueError(f"File extension {extension} is not supported")
reader = reader_cls()
if file.path is None:
raise ValueError("Document file path is not set")
documents = reader.load_data(Path(file.path))
# Add custom metadata
for doc in documents:
doc.metadata["file_name"] = file.name
doc.metadata["private"] = "true"
return documents
@staticmethod
def _add_documents_to_vector_store_index(
documents: List[Document], index: VectorStoreIndex
) -> None:
"""
Add the documents to the vector store index
"""
pipeline = IngestionPipeline()
nodes = pipeline.run(documents=documents)
# Add the nodes to the index and persist it
if index is None:
index = VectorStoreIndex(nodes=nodes)
else:
index.insert_nodes(nodes=nodes)
index.storage_context.persist(
persist_dir=os.environ.get("STORAGE_DIR", "storage")
)
@staticmethod
def _add_file_to_llama_cloud_index(
index: LlamaCloudIndex,
file_name: str,
file_data: bytes,
) -> str:
"""
Add the file to the LlamaCloud index.
LlamaCloudIndex is a managed index so we can directly use the files.
"""
try:
from app.engine.service import LLamaCloudFileService # type: ignore
except ImportError as e:
raise ValueError("LlamaCloudFileService is not found") from e
project_id = index._get_project_id()
pipeline_id = index._get_pipeline_id()
# LlamaCloudIndex is a managed index so we can directly use the files
upload_file = (file_name, BytesIO(file_data))
doc_id = LLamaCloudFileService.add_file_to_pipeline(
project_id,
pipeline_id,
upload_file,
custom_metadata={},
)
return doc_id
def _sanitize_file_name(file_name: str) -> str:
"""
Sanitize the file name by replacing all non-alphanumeric characters with underscores
"""
sanitized_name = re.sub(r"[^a-zA-Z0-9.]", "_", file_name)
return sanitized_name
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
default_loaders[".csv"] = FlatReader
return default_loaders
def _get_available_tools() -> Dict[str, List[FunctionTool]]:
try:
from app.engine.tools import ToolFactory # type: ignore
except ImportError:
logger.warning("ToolFactory not found, no tools will be available")
return {}
try:
tools = ToolFactory.from_env(map_result=True)
return tools # type: ignore
except Exception as e:
logger.error(f"Error loading tools from environment: {str(e)}")
raise ValueError(f"Failed to get available tools: {str(e)}") from e
@@ -15,7 +15,7 @@ uvicorn = { extras = ["standard"], version = "^0.23.2" }
python-dotenv = "^1.0.0"
aiostream = "^0.5.2"
cachetools = "^5.3.3"
llama-index = "0.11.6"
llama-index = "^0.11.17"
[tool.poetry.group.dev.dependencies]
mypy = "^1.8.0"
@@ -36,4 +36,8 @@ ignore_missing_imports = true
follow_imports = "silent"
implicit_optional = true
strict_optional = false
disable_error_code = ["return-value", "import-untyped", "assignment"]
disable_error_code = ["return-value", "assignment"]
[[tool.mypy.overrides]]
module = "app.*"
ignore_missing_imports = false
@@ -11,24 +11,23 @@ export const dynamic = "force-dynamic";
export async function POST(request: NextRequest) {
try {
const {
filename,
name,
base64,
params,
}: { filename: string; base64: string; params?: any } =
await request.json();
if (!base64 || !filename) {
}: {
name: string;
base64: string;
params?: any;
} = await request.json();
if (!base64 || !name) {
return NextResponse.json(
{ error: "base64 and filename is required in the request body" },
{ error: "base64 and name is required in the request body" },
{ status: 400 },
);
}
const index = await getDataSource(params);
if (!index) {
throw new Error(
`StorageContext is empty - call 'npm run generate' to generate the storage first`,
);
}
return NextResponse.json(await uploadDocument(index, filename, base64));
const documentFile = await uploadDocument(index, name, base64);
return NextResponse.json(documentFile);
} catch (error) {
console.error("[Upload API]", error);
return NextResponse.json(
@@ -19,6 +19,8 @@ import {
Result,
Sandbox,
} from "@e2b/code-interpreter";
import fs from "node:fs/promises";
import path from "node:path";
import { saveDocument } from "../chat/llamaindex/documents/helper";
type CodeArtifact = {
@@ -32,6 +34,7 @@ type CodeArtifact = {
port: number | null;
file_path: string;
code: string;
files?: string[];
};
const sandboxTimeout = 10 * 60 * 1000; // 10 minute in ms
@@ -82,6 +85,18 @@ export async function POST(req: Request) {
}
}
// Copy files
if (artifact.files) {
artifact.files.forEach(async (sandboxFilePath) => {
const fileName = path.basename(sandboxFilePath);
const localFilePath = path.join("output", "uploaded", fileName);
const fileContent = await fs.readFile(localFilePath);
await sbx.files.write(sandboxFilePath, fileContent);
console.log(`Copied file to ${sandboxFilePath} in ${sbx.sandboxID}`);
});
}
// Copy code to fs
if (artifact.code && Array.isArray(artifact.code)) {
artifact.code.forEach(async (file) => {
@@ -1,5 +1,6 @@
import { JSONValue } from "ai";
import React from "react";
import { DocumentFile } from ".";
import { Button } from "../button";
import { DocumentPreview } from "../document-preview";
import FileUploader from "../file-uploader";
@@ -65,8 +66,8 @@ export default function ChatInput(
};
const handleUploadFile = async (file: File) => {
if (imageUrl || files.length > 0) {
alert("You can only upload one file at a time.");
if (imageUrl) {
alert("You can only upload one image at a time.");
return;
}
try {
@@ -95,7 +96,7 @@ export default function ChatInput(
)}
{files.length > 0 && (
<div className="flex gap-4 w-full overflow-auto py-2">
{files.map((file) => (
{files.map((file: DocumentFile) => (
<DocumentPreview
key={file.id}
file={file}
@@ -5,7 +5,7 @@ export function ChatFiles({ data }: { data: DocumentFileData }) {
if (!data.files.length) return null;
return (
<div className="flex gap-2 items-center">
{data.files.map((file) => (
{data.files.map((file, index) => (
<DocumentPreview key={file.id} file={file} />
))}
</div>

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