Compare commits

..

40 Commits

Author SHA1 Message Date
github-actions[bot] 860b9d46d4 Release 0.3.26 (#484)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-17 17:14:45 +07:00
Huu Le f73d46bf10 fix missing multiagent code (#483) 2025-01-17 16:59:05 +07:00
github-actions[bot] eec237c5fe Release 0.3.25 (#477)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-27 13:11:44 +07:00
Thuc Pham 5450096e96 bump: react 19 stable (#476) 2024-12-27 13:01:59 +07:00
github-actions[bot] 163492f189 Release 0.3.24 (#472)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-27 09:54:29 +07:00
Huu Le a84743c576 add LlamaCloud support for reflex template (#473) 2024-12-26 15:09:16 +07:00
Thuc Pham fc5e56efa5 bump: code interpreter v1 (#469) 2024-12-26 15:06:00 +07:00
Huu Le a7a6592441 Fix the npm issue when running a fullstack Python app (#471) 2024-12-25 10:28:50 +07:00
github-actions[bot] af21426952 Release 0.3.23 (#470)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-24 16:40:23 +07:00
Huu Le 9077cae2f5 feat: Add legal document review use case (#467) 2024-12-24 15:38:37 +07:00
github-actions[bot] 765d2c4fff Release 0.3.22 (#463)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-12 10:57:51 +07:00
Marcus Schiesser 25667d45e9 feat: Make OpenAPI spec usable by custom GPTs (#462) 2024-12-11 17:10:23 +07:00
Sergey Lyapustin d31910a303 Fixed NEXT_QUESTION_PROMPT to suggest user questions. (#461) 2024-12-09 10:46:06 +07:00
github-actions[bot] 9852e7399c Release 0.3.21 (#459)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-06 16:41:24 +07:00
Marcus Schiesser 95227a7539 feat: add simple query endpoint (#458) 2024-12-06 16:12:52 +07:00
github-actions[bot] 71f29ea85d Release 0.3.20 (#457)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-06 12:15:32 +07:00
Huu Le 27d2499aff Bump llamacloud index and fix issues (#456) 2024-12-03 17:03:30 +07:00
github-actions[bot] a07f320e6d Release 0.3.19 (#455)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-02 11:39:29 +07:00
Huu Le f9a057ddde feat: add support for multimodal indexes (#453)
---------
Co-authored-by: thucpn <thucsh2@gmail.com>
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-11-29 18:02:14 +07:00
Thuc Pham aedd73d8c0 bump: chat-ui (#454) 2024-11-29 11:57:48 +07:00
github-actions[bot] da4505aff7 Release 0.3.18 (#451)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-27 16:56:27 +07:00
Huu Le 63e961e635 Refactor query engine tool code and use auto_routed mode for LlamaCloudIndex (#450)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-11-27 16:35:50 +07:00
Thuc Pham fe90a7e7ee chore: bump ai v4 (#449) 2024-11-27 12:26:53 +07:00
Huu Le 02b2473103 feat: Improve FastAPI agentic template (#447)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-11-26 10:54:22 +07:00
github-actions[bot] f17449b90a Release 0.3.17 (#446)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-22 16:36:36 +07:00
Huu Le 28c8808ce3 feat: Add fly.io deployment (#443)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-11-22 16:34:37 +07:00
Marcus Schiesser 0a7dfcf84b feat: Generate NEXT_PUBLIC_CHAT_API for NextJS backend to specify alternative backend (#445) 2024-11-22 11:06:38 +07:00
github-actions[bot] 6e70e327d3 Release 0.3.16 (#440)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-21 11:41:02 +07:00
Huu Le 8b371d8347 chore: fix incompatible with pydantic (#442) 2024-11-21 11:38:52 +07:00
Huu Le 30fe269575 Update duckduckgo tool option (#439) 2024-11-20 17:26:42 +07:00
Marcus Schiesser 49c35b834b docs: improve python readme 2024-11-20 13:30:08 +07:00
github-actions[bot] 82c2580ee5 Release 0.3.15 (#438)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-20 12:47:24 +07:00
Huu Le fc5b266a40 Simplify FastAPI fullstack template by using one deployment (#423)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-11-20 12:38:06 +07:00
Huu Le f8f97d2c00 Add support for Python 3.13 (#436) 2024-11-20 09:58:39 +07:00
github-actions[bot] 9c2e094883 Release 0.3.14 (#425)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-19 13:36:00 +07:00
Thuc Pham 00f0b3ae03 fix: dont include new message in chat history (#432) 2024-11-18 19:07:54 +07:00
Thuc Pham 4663dec81d chore: bump react19 rc (#430) 2024-11-18 16:47:51 +07:00
Huu Le 7f14e47f56 feat: Improve CI (#431) 2024-11-18 16:41:45 +07:00
Thuc Pham 6925676013 feat: use latest chat UI (#418)
---------

Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-11-14 11:48:10 +08:00
Thuc Pham 44b34fb464 chore: update nextjs v15, react v19 and eslint v9 (#420) 2024-11-14 09:47:35 +07:00
155 changed files with 3633 additions and 1341 deletions
+91
View File
@@ -1,5 +1,96 @@
# create-llama
## 0.3.26
### Patch Changes
- f73d46b: Fix missing copy of the multiagent code
## 0.3.25
### Patch Changes
- 5450096: bump: react 19 stable
## 0.3.24
### Patch Changes
- a84743c: Change --agents paramameter to --use-case
- a84743c: Add LlamaCloud support for Reflex templates
- a7a6592: Fix the npm issue on the full-stack Python template
- fc5e56e: bump: code interpreter v1
## 0.3.23
### Patch Changes
- 9077cae: Add contract review use case (Python)
## 0.3.22
### Patch Changes
- 25667d4: Make OpenAPI spec usable by custom GPTs
## 0.3.21
### Patch Changes
- 95227a7: Add query endpoint
## 0.3.20
### Patch Changes
- 27d2499: Bump the LlamaCloud library and fix breaking changes (Python).
## 0.3.19
### Patch Changes
- f9a057d: Add support multimodal indexes (e.g. from LlamaCloud)
- aedd73d: bump: chat-ui
## 0.3.18
### Patch Changes
- fe90a7e: chore: bump ai v4
- 02b2473: Show streaming errors in Python, optimize system prompts for tool usage and set the weather tool as default for the Agentic RAG use case
- 63e961e: Use auto_routed retriever mode for LlamaCloudIndex
## 0.3.17
### Patch Changes
- 28c8808: Add fly.io deployment
- 0a7dfcf: Generate NEXT_PUBLIC_CHAT_API for NextJS backend to specify alternative backend
## 0.3.16
### Patch Changes
- 8b371d8: Set pydantic version to <2.10 to avoid incompatibility with llama-index.
- 30fe269: Deactive duckduckgo tool for TS
- 30fe269: Replace DuckDuckGo by Wikipedia tool for agentic template
## 0.3.15
### Patch Changes
- fc5b266: Improve DX for Python template (use one deployment instead of two)
- f8f97d2: Add support for python 3.13
## 0.3.14
### Patch Changes
- 00f0b3a: fix: dont include user message in chat history
- 4663dec: chore: bump react19 rc
- 44b34fb: chore: update eslint 9, nextjs 15, react 19
- 6925676: feat: use latest chat UI
## 0.3.13
### Patch Changes
+10 -22
View File
@@ -7,17 +7,16 @@ import { getOnline } from "./helpers/is-online";
import { isWriteable } from "./helpers/is-writeable";
import { makeDir } from "./helpers/make-dir";
import fs from "fs";
import terminalLink from "terminal-link";
import type { InstallTemplateArgs, TemplateObservability } from "./helpers";
import { installTemplate } from "./helpers";
import { writeDevcontainer } from "./helpers/devcontainer";
import { templatesDir } from "./helpers/dir";
import { toolsRequireConfig } from "./helpers/tools";
import { configVSCode } from "./helpers/vscode";
export type InstallAppArgs = Omit<
InstallTemplateArgs,
"appName" | "root" | "isOnline" | "customApiPath"
"appName" | "root" | "isOnline" | "port"
> & {
appPath: string;
frontend: boolean;
@@ -35,13 +34,12 @@ export async function createApp({
communityProjectConfig,
llamapack,
vectorDb,
externalPort,
postInstallAction,
dataSources,
tools,
useLlamaParse,
observability,
agents,
useCase,
}: InstallAppArgs): Promise<void> {
const root = path.resolve(appPath);
@@ -81,40 +79,30 @@ export async function createApp({
communityProjectConfig,
llamapack,
vectorDb,
externalPort,
postInstallAction,
dataSources,
tools,
useLlamaParse,
observability,
agents,
useCase,
};
if (frontend) {
// install backend
const backendRoot = path.join(root, "backend");
await makeDir(backendRoot);
await installTemplate({ ...args, root: backendRoot, backend: true });
// Install backend
await installTemplate({ ...args, backend: true });
if (frontend && framework === "fastapi") {
// install frontend
const frontendRoot = path.join(root, "frontend");
const frontendRoot = path.join(root, ".frontend");
await makeDir(frontendRoot);
await installTemplate({
...args,
root: frontendRoot,
framework: "nextjs",
customApiPath: `http://localhost:${externalPort ?? 8000}/api/chat`,
backend: false,
});
// copy readme for fullstack
await fs.promises.copyFile(
path.join(templatesDir, "README-fullstack.md"),
path.join(root, "README.md"),
);
} else {
await installTemplate({ ...args, backend: true });
}
await writeDevcontainer(root, templatesDir, framework, frontend);
await configVSCode(root, templatesDir, framework);
process.chdir(root);
if (tryGitInit(root)) {
-4
View File
@@ -63,7 +63,6 @@ if (
vectorDb,
tools: "none",
port: 3000,
externalPort: 8000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
@@ -101,7 +100,6 @@ if (
vectorDb: "none",
tools: tool,
port: 3000,
externalPort: 8000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
@@ -135,7 +133,6 @@ if (
vectorDb: "none",
tools: "none",
port: 3000,
externalPort: 8000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
@@ -169,7 +166,6 @@ if (
vectorDb: "none",
tools: "none",
port: 3000,
externalPort: 8000,
postInstallAction: "none",
templateUI: undefined,
appType: "--no-frontend",
-63
View File
@@ -1,63 +0,0 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { expect, test } from "@playwright/test";
import { ChildProcess } from "child_process";
import fs from "fs";
import path from "path";
import { TemplateFramework } from "../../helpers";
import { createTestDir, runCreateLlama } from "../utils";
const templateFramework: TemplateFramework = process.env.FRAMEWORK
? (process.env.FRAMEWORK as TemplateFramework)
: "fastapi";
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
: "--example-file";
// The extractor template currently only works with FastAPI and files (and not on Windows)
if (
process.platform !== "win32" &&
templateFramework === "fastapi" &&
dataSource === "--example-file"
) {
test.describe("Test extractor template", async () => {
let frontendPort: number;
let backendPort: number;
let name: string;
let appProcess: ChildProcess;
let cwd: string;
// Create extractor app
test.beforeAll(async () => {
cwd = await createTestDir();
frontendPort = Math.floor(Math.random() * 10000) + 10000;
backendPort = frontendPort + 1;
const result = await runCreateLlama({
cwd,
templateType: "extractor",
templateFramework: "fastapi",
dataSource: "--example-file",
vectorDb: "none",
port: frontendPort,
externalPort: backendPort,
postInstallAction: "runApp",
});
name = result.projectName;
appProcess = result.appProcess;
});
test.afterAll(async () => {
appProcess.kill();
});
test("App folder should exist", async () => {
const dirExists = fs.existsSync(path.join(cwd, name));
expect(dirExists).toBeTruthy();
});
test("Frontend should have a title", async ({ page }) => {
await page.goto(`http://localhost:${frontendPort}`);
await expect(page.getByText("Built by LlamaIndex")).toBeVisible({
timeout: 2000 * 60,
});
});
});
}
+13 -9
View File
@@ -16,18 +16,17 @@ const templateFramework: TemplateFramework = process.env.FRAMEWORK
const dataSource: string = "--example-file";
const templateUI: TemplateUI = "shadcn";
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
const appType: AppType = templateFramework === "nextjs" ? "" : "--frontend";
const appType: AppType = templateFramework === "fastapi" ? "--frontend" : "";
const userMessage = "Write a blog post about physical standards for letters";
const templateAgents = ["financial_report", "blog", "form_filling"];
const templateUseCases = ["financial_report", "blog", "form_filling"];
for (const agents of templateAgents) {
test.describe(`Test multiagent template ${agents} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
for (const useCase of templateUseCases) {
test.describe(`Test multiagent template ${useCase} ${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;
@@ -36,7 +35,6 @@ for (const agents of templateAgents) {
test.beforeAll(async () => {
port = Math.floor(Math.random() * 10000) + 10000;
externalPort = port + 1;
cwd = await createTestDir();
const result = await runCreateLlama({
cwd,
@@ -45,11 +43,10 @@ for (const agents of templateAgents) {
dataSource,
vectorDb,
port,
externalPort,
postInstallAction: templatePostInstallAction,
templateUI,
appType,
agents,
useCase,
});
name = result.projectName;
appProcess = result.appProcess;
@@ -61,6 +58,10 @@ for (const agents of templateAgents) {
});
test("Frontend should have a title", async ({ page }) => {
test.skip(
templatePostInstallAction !== "runApp" ||
templateFramework === "express",
);
await page.goto(`http://localhost:${port}`);
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
});
@@ -69,7 +70,10 @@ for (const agents of templateAgents) {
page,
}) => {
test.skip(
agents === "financial_report" || agents === "form_filling",
templatePostInstallAction !== "runApp" ||
useCase === "financial_report" ||
useCase === "form_filling" ||
templateFramework === "express",
"Skip chat tests for financial report and form filling.",
);
await page.goto(`http://localhost:${port}`);
+64
View File
@@ -0,0 +1,64 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { expect, test } from "@playwright/test";
import { ChildProcess } from "child_process";
import fs from "fs";
import path from "path";
import { TemplateFramework, TemplateUseCase } from "../../helpers";
import { createTestDir, runCreateLlama } from "../utils";
const templateFramework: TemplateFramework = process.env.FRAMEWORK
? (process.env.FRAMEWORK as TemplateFramework)
: "fastapi";
const dataSource: string = process.env.DATASOURCE
? process.env.DATASOURCE
: "--example-file";
const templateUseCases: TemplateUseCase[] = ["extractor", "contract_review"];
// The reflex template currently only works with FastAPI and files (and not on Windows)
if (
process.platform !== "win32" &&
templateFramework === "fastapi" &&
dataSource === "--example-file"
) {
for (const useCase of templateUseCases) {
test.describe(`Test reflex template ${useCase} ${templateFramework} ${dataSource}`, async () => {
let appPort: number;
let name: string;
let appProcess: ChildProcess;
let cwd: string;
// Create reflex app
test.beforeAll(async () => {
cwd = await createTestDir();
appPort = Math.floor(Math.random() * 10000) + 10000;
const result = await runCreateLlama({
cwd,
templateType: "reflex",
templateFramework: "fastapi",
dataSource: "--example-file",
vectorDb: "none",
port: appPort,
postInstallAction: "runApp",
useCase,
});
name = result.projectName;
appProcess = result.appProcess;
});
test.afterAll(async () => {
appProcess.kill();
});
test("App folder should exist", async () => {
const dirExists = fs.existsSync(path.join(cwd, name));
expect(dirExists).toBeTruthy();
});
test("Frontend should have a title", async ({ page }) => {
await page.goto(`http://localhost:${appPort}`);
await expect(page.getByText("Built by LlamaIndex")).toBeVisible({
timeout: 2000 * 60,
});
});
});
}
}
+9 -7
View File
@@ -22,7 +22,7 @@ const templatePostInstallAction: TemplatePostInstallAction = "runApp";
const llamaCloudProjectName = "create-llama";
const llamaCloudIndexName = "e2e-test";
const appType: AppType = templateFramework === "nextjs" ? "" : "--frontend";
const appType: AppType = templateFramework === "fastapi" ? "--frontend" : "";
const userMessage =
dataSource !== "--no-files" ? "Physical standard for letters" : "Hello";
@@ -35,7 +35,6 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
}
let port: number;
let externalPort: number;
let cwd: string;
let name: string;
let appProcess: ChildProcess;
@@ -44,7 +43,6 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
test.beforeAll(async () => {
port = Math.floor(Math.random() * 10000) + 10000;
externalPort = port + 1;
cwd = await createTestDir();
const result = await runCreateLlama({
cwd,
@@ -53,7 +51,6 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
dataSource,
vectorDb,
port,
externalPort,
postInstallAction: templatePostInstallAction,
templateUI,
appType,
@@ -68,8 +65,11 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
const dirExists = fs.existsSync(path.join(cwd, name));
expect(dirExists).toBeTruthy();
});
test("Frontend should have a title", async ({ page }) => {
test.skip(templatePostInstallAction !== "runApp");
test.skip(
templatePostInstallAction !== "runApp" || templateFramework === "express",
);
await page.goto(`http://localhost:${port}`);
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
});
@@ -77,7 +77,9 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
test("Frontend should be able to submit a message and receive a response", async ({
page,
}) => {
test.skip(templatePostInstallAction !== "runApp");
test.skip(
templatePostInstallAction !== "runApp" || templateFramework === "express",
);
await page.goto(`http://localhost:${port}`);
await page.fill("form textarea", userMessage);
const [response] = await Promise.all([
@@ -102,7 +104,7 @@ test.describe(`Test streaming template ${templateFramework} ${dataSource} ${temp
test.skip(templatePostInstallAction !== "runApp");
test.skip(templateFramework === "nextjs");
const response = await request.post(
`http://localhost:${externalPort}/api/chat/request`,
`http://localhost:${port}/api/chat/request`,
{
data: {
messages: [
@@ -56,7 +56,6 @@ test.describe("Test resolve TS dependencies", () => {
dataSource: dataSource,
vectorDb: vectorDb,
port: 3000,
externalPort: 8000,
postInstallAction: "none",
templateUI: undefined,
appType: templateFramework === "nextjs" ? "" : "--no-frontend",
+6 -32
View File
@@ -25,7 +25,6 @@ export type RunCreateLlamaOptions = {
dataSource: string;
vectorDb: TemplateVectorDB;
port: number;
externalPort: number;
postInstallAction: TemplatePostInstallAction;
templateUI?: TemplateUI;
appType?: AppType;
@@ -34,7 +33,7 @@ export type RunCreateLlamaOptions = {
tools?: string;
useLlamaParse?: boolean;
observability?: string;
agents?: string;
useCase?: string;
};
export async function runCreateLlama({
@@ -44,7 +43,6 @@ export async function runCreateLlama({
dataSource,
vectorDb,
port,
externalPort,
postInstallAction,
templateUI,
appType,
@@ -53,7 +51,7 @@ export async function runCreateLlama({
tools,
useLlamaParse,
observability,
agents,
useCase,
}: RunCreateLlamaOptions): Promise<CreateLlamaResult> {
if (!process.env.OPENAI_API_KEY || !process.env.LLAMA_CLOUD_API_KEY) {
throw new Error(
@@ -90,21 +88,15 @@ export async function runCreateLlama({
...dataSourceArgs,
"--vector-db",
vectorDb,
"--open-ai-key",
process.env.OPENAI_API_KEY,
"--use-pnpm",
"--use-npm",
"--port",
port,
"--external-port",
externalPort,
"--post-install-action",
postInstallAction,
"--tools",
tools ?? "none",
"--observability",
"none",
"--llama-cloud-key",
process.env.LLAMA_CLOUD_API_KEY,
];
if (templateUI) {
@@ -121,8 +113,8 @@ export async function runCreateLlama({
if (observability) {
commandArgs.push("--observability", observability);
}
if (templateType === "multiagent" && agents) {
commandArgs.push("--agents", agents);
if ((templateType === "multiagent" || templateType === "reflex") && useCase) {
commandArgs.push("--use-case", useCase);
}
const command = commandArgs.join(" ");
@@ -146,12 +138,7 @@ export async function runCreateLlama({
// Wait for app to start
if (postInstallAction === "runApp") {
await checkAppHasStarted(
appType === "--frontend",
templateFramework,
port,
externalPort,
);
await waitPorts([port]);
} else if (postInstallAction === "dependencies") {
await waitForProcess(appProcess, 1000 * 60); // wait 1 min for dependencies to be resolved
} else {
@@ -171,19 +158,6 @@ export async function createTestDir() {
return cwd;
}
// eslint-disable-next-line max-params
async function checkAppHasStarted(
frontend: boolean,
framework: TemplateFramework,
port: number,
externalPort: number,
) {
const portsToWait = frontend
? [port, externalPort]
: [framework === "nextjs" ? port : externalPort];
await waitPorts(portsToWait);
}
async function waitPorts(ports: number[]): Promise<void> {
const waitForPort = async (port: number): Promise<void> => {
await waitPort({
+3
View File
@@ -61,6 +61,9 @@ export const assetRelocator = (name: string) => {
case "README-template.md": {
return "README.md";
}
case "vscode_settings.json": {
return "settings.json";
}
default: {
return name;
}
+12
View File
@@ -18,6 +18,7 @@ export const EXAMPLE_10K_SEC_FILES: TemplateDataSource[] = [
url: new URL(
"https://s2.q4cdn.com/470004039/files/doc_earnings/2023/q4/filing/_10-K-Q4-2023-As-Filed.pdf",
),
filename: "apple_10k_report.pdf",
},
},
{
@@ -26,10 +27,21 @@ export const EXAMPLE_10K_SEC_FILES: TemplateDataSource[] = [
url: new URL(
"https://ir.tesla.com/_flysystem/s3/sec/000162828024002390/tsla-20231231-gen.pdf",
),
filename: "tesla_10k_report.pdf",
},
},
];
export const EXAMPLE_GDPR: TemplateDataSource = {
type: "file",
config: {
url: new URL(
"https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:32016R0679",
),
filename: "gdpr.pdf",
},
};
export function getDataSources(
files?: string,
exampleFile?: boolean,
+22 -17
View File
@@ -13,6 +13,12 @@ import {
import { TSYSTEMS_LLMHUB_API_URL } from "./providers/llmhub";
const DEFAULT_SYSTEM_PROMPT =
"You are a helpful assistant who helps users with their questions.";
const DATA_SOURCES_PROMPT =
"You have access to a knowledge base including the facts that you should start with to find the answer for the user question. Use the query engine tool to retrieve the facts from the knowledge base.";
export type EnvVar = {
name?: string;
description?: string;
@@ -407,6 +413,13 @@ const getFrameworkEnvs = (
],
);
}
if (framework === "nextjs") {
result.push({
name: "NEXT_PUBLIC_CHAT_API",
description:
"The API for the chat endpoint. Set when using a custom backend (e.g. Express). Use full URL like http://localhost:8000/api/chat",
});
}
return result;
};
@@ -442,9 +455,6 @@ const getSystemPromptEnv = (
dataSources?: TemplateDataSource[],
template?: TemplateType,
): EnvVar[] => {
const defaultSystemPrompt =
"You are a helpful assistant who helps users with their questions.";
const systemPromptEnv: EnvVar[] = [];
// build tool system prompt by merging all tool system prompts
// multiagent template doesn't need system prompt
@@ -459,9 +469,12 @@ const getSystemPromptEnv = (
}
});
const systemPrompt = toolSystemPrompt
? `\"${toolSystemPrompt}\"`
: defaultSystemPrompt;
const systemPrompt =
'"' +
DEFAULT_SYSTEM_PROMPT +
(dataSources?.length ? `\n${DATA_SOURCES_PROMPT}` : "") +
(toolSystemPrompt ? `\n${toolSystemPrompt}` : "") +
'"';
systemPromptEnv.push({
name: "SYSTEM_PROMPT",
@@ -512,7 +525,7 @@ Here is the conversation history
---------------------
{conversation}
---------------------
Given the conversation history, please give me 3 questions that you might ask next!
Given the conversation history, please give me 3 questions that user might ask next!
Your answer should be wrapped in three sticks which follows the following format:
\`\`\`
<question 1>
@@ -553,7 +566,7 @@ export const createBackendEnvFile = async (
| "framework"
| "dataSources"
| "template"
| "externalPort"
| "port"
| "tools"
| "observability"
>,
@@ -570,7 +583,7 @@ export const createBackendEnvFile = async (
...getModelEnvs(opts.modelConfig),
...getEngineEnvs(),
...getVectorDBEnvs(opts.vectorDb, opts.framework),
...getFrameworkEnvs(opts.framework, opts.externalPort),
...getFrameworkEnvs(opts.framework, opts.port),
...getToolEnvs(opts.tools),
...getTemplateEnvs(opts.template),
...getObservabilityEnvs(opts.observability),
@@ -585,18 +598,10 @@ export const createBackendEnvFile = async (
export const createFrontendEnvFile = async (
root: string,
opts: {
customApiPath?: string;
vectorDb?: TemplateVectorDB;
},
) => {
const defaultFrontendEnvs = [
{
name: "NEXT_PUBLIC_CHAT_API",
description: "The backend API for chat endpoint.",
value: opts.customApiPath
? opts.customApiPath
: "http://localhost:8000/api/chat",
},
{
name: "NEXT_PUBLIC_USE_LLAMACLOUD",
description: "Let's the user change indexes in LlamaCloud projects",
+3 -3
View File
@@ -118,7 +118,8 @@ const prepareContextData = async (
const destPath = path.join(
root,
"data",
path.basename(dataSourceConfig.url.toString()),
dataSourceConfig.filename ??
path.basename(dataSourceConfig.url.toString()),
);
await downloadFile(dataSourceConfig.url.toString(), destPath);
} else {
@@ -192,7 +193,7 @@ export const installTemplate = async (
if (
props.template === "streaming" ||
props.template === "multiagent" ||
props.template === "extractor"
props.template === "reflex"
) {
await createBackendEnvFile(props.root, props);
}
@@ -225,7 +226,6 @@ export const installTemplate = async (
} else {
// this is a frontend for a full-stack app, create .env file with model information
await createFrontendEnvFile(props.root, {
customApiPath: props.customApiPath,
vectorDb: props.vectorDb,
});
}
+2 -1
View File
@@ -3,6 +3,7 @@ import ora from "ora";
import { red } from "picocolors";
import prompts from "prompts";
import { ModelConfigParams, ModelConfigQuestionsParams } from ".";
import { isCI } from "../../questions";
import { questionHandlers } from "../../questions/utils";
const OPENAI_API_URL = "https://api.openai.com/v1";
@@ -30,7 +31,7 @@ export async function askOpenAIQuestions({
},
};
if (!config.apiKey) {
if (!config.apiKey && !isCI) {
const { key } = await prompts(
{
type: "text",
+68 -52
View File
@@ -20,6 +20,7 @@ interface Dependency {
name: string;
version?: string;
extras?: string[];
constraints?: Record<string, string>;
}
const getAdditionalDependencies = (
@@ -36,28 +37,31 @@ const getAdditionalDependencies = (
case "mongo": {
dependencies.push({
name: "llama-index-vector-stores-mongodb",
version: "^0.3.1",
version: "^0.6.0",
});
break;
}
case "pg": {
dependencies.push({
name: "llama-index-vector-stores-postgres",
version: "^0.2.5",
version: "^0.3.2",
});
break;
}
case "pinecone": {
dependencies.push({
name: "llama-index-vector-stores-pinecone",
version: "^0.2.1",
version: "^0.4.1",
constraints: {
python: ">=3.11,<3.13",
},
});
break;
}
case "milvus": {
dependencies.push({
name: "llama-index-vector-stores-milvus",
version: "^0.2.0",
version: "^0.3.0",
});
dependencies.push({
name: "pymilvus",
@@ -68,35 +72,38 @@ const getAdditionalDependencies = (
case "astra": {
dependencies.push({
name: "llama-index-vector-stores-astra-db",
version: "^0.2.0",
version: "^0.4.0",
});
break;
}
case "qdrant": {
dependencies.push({
name: "llama-index-vector-stores-qdrant",
version: "^0.3.0",
version: "^0.4.0",
constraints: {
python: ">=3.11,<3.13",
},
});
break;
}
case "chroma": {
dependencies.push({
name: "llama-index-vector-stores-chroma",
version: "^0.2.0",
version: "^0.4.0",
});
break;
}
case "weaviate": {
dependencies.push({
name: "llama-index-vector-stores-weaviate",
version: "^1.1.1",
version: "^1.2.3",
});
break;
}
case "llamacloud":
dependencies.push({
name: "llama-index-indices-managed-llama-cloud",
version: "^0.3.1",
version: "^0.6.3",
});
break;
}
@@ -115,13 +122,13 @@ const getAdditionalDependencies = (
case "web":
dependencies.push({
name: "llama-index-readers-web",
version: "^0.2.2",
version: "^0.3.0",
});
break;
case "db":
dependencies.push({
name: "llama-index-readers-database",
version: "^0.2.0",
version: "^0.3.0",
});
dependencies.push({
name: "pymysql",
@@ -160,15 +167,15 @@ const getAdditionalDependencies = (
if (templateType !== "multiagent") {
dependencies.push({
name: "llama-index-llms-openai",
version: "^0.2.0",
version: "^0.3.2",
});
dependencies.push({
name: "llama-index-embeddings-openai",
version: "^0.2.3",
version: "^0.3.1",
});
dependencies.push({
name: "llama-index-agent-openai",
version: "^0.3.0",
version: "^0.4.0",
});
}
break;
@@ -279,14 +286,19 @@ const mergePoetryDependencies = (
value.version = dependency.version ?? value.version;
value.extras = dependency.extras ?? value.extras;
// Merge constraints if they exist
if (dependency.constraints) {
value = { ...value, ...dependency.constraints };
}
if (value.version === undefined) {
throw new Error(
`Dependency "${dependency.name}" is missing attribute "version"!`,
);
}
// Serialize separately only if extras are provided
if (value.extras && value.extras.length > 0) {
// Serialize as object if there are any additional properties
if (Object.keys(value).length > 1) {
existingDependencies[dependency.name] = value;
} else {
// Otherwise, serialize just the version string
@@ -368,33 +380,37 @@ export const installPythonDependencies = (
};
export const installPythonTemplate = async ({
appName,
root,
template,
framework,
vectorDb,
postInstallAction,
modelConfig,
dataSources,
tools,
postInstallAction,
useLlamaParse,
useCase,
observability,
modelConfig,
agents,
}: Pick<
InstallTemplateArgs,
| "appName"
| "root"
| "framework"
| "template"
| "framework"
| "vectorDb"
| "postInstallAction"
| "modelConfig"
| "dataSources"
| "tools"
| "postInstallAction"
| "useLlamaParse"
| "useCase"
| "observability"
| "modelConfig"
| "agents"
>) => {
console.log("\nInitializing Python project with template:", template, "\n");
let templatePath;
if (template === "extractor") {
templatePath = path.join(templatesDir, "types", "extractor", framework);
if (template === "reflex") {
templatePath = path.join(templatesDir, "types", "reflex");
} else {
templatePath = path.join(templatesDir, "types", "streaming", framework);
}
@@ -460,37 +476,39 @@ 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 ?? []);
}
// Copy multiagents overrides
if (template === "multiagent") {
// Copy multi-agent code
await copy("**", path.join(root), {
parents: true,
cwd: path.join(compPath, "multiagent", "python"),
rename: assetRelocator,
});
}
if (template === "multiagent" || template === "reflex") {
if (useCase) {
const sourcePath =
template === "multiagent"
? path.join(compPath, "agents", "python", useCase)
: path.join(compPath, "reflex", useCase);
await copy("**", path.join(root), {
parents: true,
cwd: sourcePath,
rename: assetRelocator,
});
} else {
console.log(
red(
`There is no use case selected for ${template} template. Please pick a use case to use via --use-case flag.`,
),
);
process.exit(1);
}
}
console.log("Adding additional dependencies");
const addOnDependencies = getAdditionalDependencies(
@@ -512,7 +530,10 @@ export const installPythonTemplate = async ({
if (observability === "llamatrace") {
addOnDependencies.push({
name: "llama-index-callbacks-arize-phoenix",
version: "^0.2.1",
version: "^0.3.0",
constraints: {
python: ">=3.11,<3.13",
},
});
}
@@ -533,9 +554,4 @@ export const installPythonTemplate = async ({
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
installPythonDependencies();
}
// Copy deployment files for python
await copy("**", root, {
cwd: path.join(compPath, "deployments", "python"),
});
};
+46 -64
View File
@@ -1,40 +1,39 @@
import { ChildProcess, SpawnOptions, spawn } from "child_process";
import path from "path";
import { TemplateFramework } from "./types";
import { SpawnOptions, spawn } from "child_process";
import { TemplateFramework, TemplateType } from "./types";
const createProcess = (
command: string,
args: string[],
options: SpawnOptions,
) => {
return spawn(command, args, {
...options,
shell: true,
})
.on("exit", function (code) {
if (code !== 0) {
console.log(`Child process exited with code=${code}`);
process.exit(1);
}
): Promise<void> => {
return new Promise((resolve, reject) => {
spawn(command, args, {
...options,
shell: true,
})
.on("error", function (err) {
console.log("Error when running chill process: ", err);
process.exit(1);
});
.on("exit", function (code) {
if (code !== 0) {
console.log(`Child process exited with code=${code}`);
reject(code);
} else {
resolve();
}
})
.on("error", function (err) {
console.log("Error when running child process: ", err);
reject(err);
});
});
};
export function runReflexApp(
appPath: string,
frontendPort?: number,
backendPort?: number,
) {
const commandArgs = ["run", "reflex", "run"];
if (frontendPort) {
commandArgs.push("--frontend-port", frontendPort.toString());
}
if (backendPort) {
commandArgs.push("--backend-port", backendPort.toString());
}
export function runReflexApp(appPath: string, port: number) {
const commandArgs = [
"run",
"reflex",
"run",
"--frontend-port",
port.toString(),
];
return createProcess("poetry", commandArgs, {
stdio: "inherit",
cwd: appPath,
@@ -42,11 +41,10 @@ export function runReflexApp(
}
export function runFastAPIApp(appPath: string, port: number) {
const commandArgs = ["run", "uvicorn", "main:app", "--port=" + port];
return createProcess("poetry", commandArgs, {
return createProcess("poetry", ["run", "dev"], {
stdio: "inherit",
cwd: appPath,
env: { ...process.env, APP_PORT: `${port}` },
});
}
@@ -60,40 +58,24 @@ export function runTSApp(appPath: string, port: number) {
export async function runApp(
appPath: string,
template: string,
frontend: boolean,
template: TemplateType,
framework: TemplateFramework,
port?: number,
externalPort?: number,
): Promise<any> {
const processes: ChildProcess[] = [];
): Promise<void> {
try {
// Start the app
const defaultPort =
framework === "nextjs" || template === "reflex" ? 3000 : 8000;
// Callback to kill all sub processes if the main process is killed
process.on("exit", () => {
console.log("Killing app processes...");
processes.forEach((p) => p.kill());
});
// Default sub app paths
const backendPath = path.join(appPath, "backend");
const frontendPath = path.join(appPath, "frontend");
if (template === "extractor") {
processes.push(runReflexApp(appPath, port, externalPort));
const appRunner =
template === "reflex"
? runReflexApp
: framework === "fastapi"
? runFastAPIApp
: runTSApp;
await appRunner(appPath, port || defaultPort);
} catch (error) {
console.error("Failed to run app:", error);
throw error;
}
if (template === "streaming" || template === "multiagent") {
if (framework === "fastapi" || framework === "express") {
const backendRunner = framework === "fastapi" ? runFastAPIApp : runTSApp;
if (frontend) {
processes.push(backendRunner(backendPath, externalPort || 8000));
processes.push(runTSApp(frontendPath, port || 3000));
} else {
processes.push(backendRunner(appPath, externalPort || 8000));
}
} else if (framework === "nextjs") {
processes.push(runTSApp(appPath, port || 3000));
}
}
return Promise.all(processes);
}
+7 -35
View File
@@ -41,7 +41,7 @@ export const supportedTools: Tool[] = [
dependencies: [
{
name: "llama-index-tools-google",
version: "^0.2.0",
version: "^0.3.0",
},
],
supportedFrameworks: ["fastapi"],
@@ -62,17 +62,16 @@ export const supportedTools: Tool[] = [
dependencies: [
{
name: "duckduckgo-search",
version: "6.1.7",
version: "^6.3.5",
},
],
supportedFrameworks: ["fastapi", "nextjs", "express"],
supportedFrameworks: ["fastapi"], // TODO: Re-enable this tool once the duck-duck-scrape TypeScript library works again
type: ToolType.LOCAL,
envVars: [
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for DuckDuckGo search tool.",
value: `You are a DuckDuckGo search agent.
You can use the duckduckgo search tool to get information from the web to answer user questions.
value: `You have access to the duckduckgo search tool. Use it to get information from the web to answer user questions.
For better results, you can specify the region parameter to get results from a specific region but it's optional.`,
},
],
@@ -83,18 +82,11 @@ For better results, you can specify the region parameter to get results from a s
dependencies: [
{
name: "llama-index-tools-wikipedia",
version: "^0.2.0",
version: "^0.3.0",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LLAMAHUB,
envVars: [
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for wiki tool.",
value: `You are a Wikipedia agent. You help users to get information from Wikipedia.`,
},
],
},
{
display: "Weather",
@@ -102,13 +94,6 @@ For better results, you can specify the region parameter to get results from a s
dependencies: [],
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LOCAL,
envVars: [
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for weather tool.",
value: `You are a weather forecast agent. You help users to get the weather forecast for a given location.`,
},
],
},
{
display: "Document generator",
@@ -139,7 +124,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: "1.0.3",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
@@ -170,7 +155,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: "1.0.3",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
@@ -211,14 +196,6 @@ For better results, you can specify the region parameter to get results from a s
},
supportedFrameworks: ["fastapi", "express", "nextjs"],
type: ToolType.LOCAL,
envVars: [
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for openapi action tool.",
value:
"You are an OpenAPI action agent. You help users to make requests to the provided OpenAPI schema.",
},
],
},
{
display: "Image Generator",
@@ -231,11 +208,6 @@ For better results, you can specify the region parameter to get results from a s
description:
"STABILITY_API_KEY key is required to run image generator. Get it here: https://platform.stability.ai/account/keys",
},
{
name: TOOL_SYSTEM_PROMPT_ENV_VAR,
description: "System prompt for image generator tool.",
value: `You are an image generator agent. You help users to generate images using the Stability API.`,
},
],
},
{
+12 -6
View File
@@ -20,11 +20,11 @@ export type ModelConfig = {
isConfigured(): boolean;
};
export type TemplateType =
| "extractor"
| "streaming"
| "community"
| "llamapack"
| "multiagent";
| "multiagent"
| "reflex";
export type TemplateFramework = "nextjs" | "express" | "fastapi";
export type TemplateUI = "html" | "shadcn";
export type TemplateVectorDB =
@@ -49,14 +49,21 @@ export type TemplateDataSource = {
};
export type TemplateDataSourceType = "file" | "web" | "db";
export type TemplateObservability = "none" | "traceloop" | "llamatrace";
export type TemplateAgents = "financial_report" | "blog" | "form_filling";
export type TemplateUseCase =
| "financial_report"
| "blog"
| "form_filling"
| "extractor"
| "contract_review";
// Config for both file and folder
export type FileSourceConfig =
| {
path: string;
filename?: string;
}
| {
url: URL;
filename?: string;
};
export type WebSourceConfig = {
baseUrl?: string;
@@ -89,16 +96,15 @@ export interface InstallTemplateArgs {
framework: TemplateFramework;
ui: TemplateUI;
dataSources: TemplateDataSource[];
customApiPath?: string;
modelConfig: ModelConfig;
llamaCloudKey?: string;
useLlamaParse?: boolean;
communityProjectConfig?: CommunityProjectConfig;
llamapack?: string;
vectorDb?: TemplateVectorDB;
externalPort?: number;
port?: number;
postInstallAction?: TemplatePostInstallAction;
tools?: Tool[];
observability?: TemplateObservability;
agents?: TemplateAgents;
useCase?: TemplateUseCase;
}
+16 -20
View File
@@ -26,7 +26,7 @@ export const installTSTemplate = async ({
tools,
dataSources,
useLlamaParse,
agents,
useCase,
}: InstallTemplateArgs & { backend: boolean }) => {
console.log(bold(`Using ${packageManager}.`));
@@ -58,11 +58,9 @@ export const installTSTemplate = async ({
console.log("\nUsing static site generation\n");
} else {
if (vectorDb === "milvus") {
nextConfigJson.experimental.serverComponentsExternalPackages =
nextConfigJson.experimental.serverComponentsExternalPackages ?? [];
nextConfigJson.experimental.serverComponentsExternalPackages.push(
"@zilliz/milvus2-sdk-node",
);
nextConfigJson.serverExternalPackages =
nextConfigJson.serverExternalPackages ?? [];
nextConfigJson.serverExternalPackages.push("@zilliz/milvus2-sdk-node");
}
}
await fs.writeFile(
@@ -133,16 +131,16 @@ 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 useCasePath = path.join(compPath, "agents", "typescript", agents);
const agentsCodePath = path.join(useCasePath, "workflow");
// Copy use case code for multiagent template
if (useCase) {
console.log("\nCopying use case:", useCase, "\n");
const useCasePath = path.join(compPath, "agents", "typescript", useCase);
const useCaseCodePath = path.join(useCasePath, "workflow");
// Copy agent codes
// Copy use case codes
await copy("**", path.join(root, relativeEngineDestPath, "workflow"), {
parents: true,
cwd: agentsCodePath,
cwd: useCaseCodePath,
rename: assetRelocator,
});
@@ -155,7 +153,7 @@ export const installTSTemplate = async ({
} else {
console.log(
red(
"There is no agent selected for multi-agent template. Please pick an agent to use via --agents flag.",
`There is no use case selected for ${template} template. Please pick a use case to use via --use-case flag.`,
),
);
process.exit(1);
@@ -243,14 +241,12 @@ export const installTSTemplate = async ({
vectorDb,
});
if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
if (
backend &&
(postInstallAction === "runApp" || postInstallAction === "dependencies")
) {
await installTSDependencies(packageJson, packageManager, isOnline);
}
// Copy deployment files for typescript
await copy("**", root, {
cwd: path.join(compPath, "deployments", "typescript"),
});
};
async function updatePackageJson({
+29 -23
View File
@@ -1,40 +1,26 @@
import fs from "fs";
import path from "path";
import { assetRelocator, copy } from "./copy";
import { TemplateFramework } from "./types";
function renderDevcontainerContent(
templatesDir: string,
framework: TemplateFramework,
frontend: boolean,
) {
const devcontainerJson: any = JSON.parse(
fs.readFileSync(path.join(templatesDir, "devcontainer.json"), "utf8"),
);
// Modify postCreateCommand
if (frontend) {
devcontainerJson.postCreateCommand =
framework === "fastapi"
? "cd backend && poetry install && cd ../frontend && npm install"
: "cd backend && npm install && cd ../frontend && npm install";
} else {
devcontainerJson.postCreateCommand =
framework === "fastapi" ? "poetry install" : "npm install";
}
devcontainerJson.postCreateCommand =
framework === "fastapi" ? "poetry install" : "npm install";
// Modify containerEnv
if (framework === "fastapi") {
if (frontend) {
devcontainerJson.containerEnv = {
...devcontainerJson.containerEnv,
PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}/backend",
};
} else {
devcontainerJson.containerEnv = {
...devcontainerJson.containerEnv,
PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}",
};
}
devcontainerJson.containerEnv = {
...devcontainerJson.containerEnv,
PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}",
};
}
return JSON.stringify(devcontainerJson, null, 2);
@@ -44,7 +30,6 @@ export const writeDevcontainer = async (
root: string,
templatesDir: string,
framework: TemplateFramework,
frontend: boolean,
) => {
const devcontainerDir = path.join(root, ".devcontainer");
if (fs.existsSync(devcontainerDir)) {
@@ -54,7 +39,6 @@ export const writeDevcontainer = async (
const devcontainerContent = renderDevcontainerContent(
templatesDir,
framework,
frontend,
);
fs.mkdirSync(devcontainerDir);
await fs.promises.writeFile(
@@ -62,3 +46,25 @@ export const writeDevcontainer = async (
devcontainerContent,
);
};
export const copyVSCodeSettings = async (
root: string,
templatesDir: string,
) => {
const vscodeDir = path.join(root, ".vscode");
await copy("vscode_settings.json", vscodeDir, {
cwd: templatesDir,
rename: assetRelocator,
});
};
export const configVSCode = async (
root: string,
templatesDir: string,
framework: TemplateFramework,
) => {
await writeDevcontainer(root, templatesDir, framework);
if (framework === "fastapi") {
await copyVSCodeSettings(root, templatesDir);
}
};
+4 -19
View File
@@ -134,13 +134,6 @@ const program = new Command(packageJson.name)
`
Select UI port.
`,
)
.option(
"--external-port <external>",
`
Select external port.
`,
)
.option(
@@ -209,10 +202,10 @@ const program = new Command(packageJson.name)
false,
)
.option(
"--agents <agents>",
"--use-case <useCase>",
`
Select which agents to use for the multi-agent template (e.g: financial_report, blog).
Select which use case to use for the multi-agent template (e.g: financial_report, blog).
`,
)
.allowUnknownOption()
@@ -222,7 +215,7 @@ const options = program.opts();
if (
process.argv.includes("--no-llama-parse") ||
options.template === "extractor"
options.template === "reflex"
) {
options.useLlamaParse = false;
}
@@ -333,7 +326,6 @@ async function run(): Promise<void> {
...answers,
appPath: resolvedProjectPath,
packageManager,
externalPort: options.externalPort,
});
if (answers.postInstallAction === "VSCode") {
@@ -362,14 +354,7 @@ Please check ${cyan(
}
} else if (answers.postInstallAction === "runApp") {
console.log(`Running app in ${root}...`);
await runApp(
root,
answers.template,
answers.frontend,
answers.framework,
options.port,
options.externalPort,
);
await runApp(root, answers.template, answers.framework, options.port);
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-llama",
"version": "0.3.13",
"version": "0.3.26",
"description": "Create LlamaIndex-powered apps with one command",
"keywords": [
"rag",
+1 -1
View File
@@ -49,7 +49,7 @@ export const getDataSourceChoices = (
);
}
if (framework === "fastapi" && template !== "extractor") {
if (framework === "fastapi" && template !== "reflex") {
choices.push({
title: "Use website content (requires Chrome)",
value: "web",
+3 -1
View File
@@ -4,10 +4,12 @@ import { askProQuestions } from "./questions";
import { askSimpleQuestions } from "./simple";
import { QuestionArgs, QuestionResults } from "./types";
export const isCI = ciInfo.isCI || process.env.PLAYWRIGHT_TEST === "1";
export const askQuestions = async (
args: QuestionArgs,
): Promise<QuestionResults> => {
if (ciInfo.isCI || process.env.PLAYWRIGHT_TEST === "1") {
if (isCI) {
return await getCIQuestionResults(args);
} else if (args.pro) {
// TODO: refactor pro questions to return a result object
+69 -42
View File
@@ -1,7 +1,8 @@
import { blue, green } from "picocolors";
import { blue } from "picocolors";
import prompts from "prompts";
import { isCI } from ".";
import { COMMUNITY_OWNER, COMMUNITY_REPO } from "../helpers/constant";
import { EXAMPLE_FILE } from "../helpers/datasources";
import { EXAMPLE_FILE, EXAMPLE_GDPR } from "../helpers/datasources";
import { getAvailableLlamapackOptions } from "../helpers/llama-pack";
import { askModelConfig } from "../helpers/providers";
import { getProjectOptions } from "../helpers/repo";
@@ -32,7 +33,7 @@ export const askProQuestions = async (program: QuestionArgs) => {
title: "Multi-agent app (using workflows)",
value: "multiagent",
},
{ title: "Structured Extractor", value: "extractor" },
{ title: "Fullstack python template with Reflex", value: "reflex" },
{
title: `Community template from ${styledRepo}`,
value: "community",
@@ -94,11 +95,29 @@ export const askProQuestions = async (program: QuestionArgs) => {
return; // early return - no further questions needed for llamapack projects
}
if (program.template === "extractor") {
// Extractor template only supports FastAPI, empty data sources, and llamacloud
if (program.template === "reflex") {
// Reflex template only supports FastAPI, empty data sources, and llamacloud
// So we just use example file for extractor template, this allows user to choose vector database later
program.dataSources = [EXAMPLE_FILE];
program.framework = "fastapi";
// Ask for which Reflex use case to use
const { useCase } = await prompts(
{
type: "select",
name: "useCase",
message: "Which use case would you like to build?",
choices: [
{ title: "Structured Extractor", value: "extractor" },
{
title: "Contract review (using Workflow)",
value: "contract_review",
},
],
initial: 0,
},
questionHandlers,
);
program.useCase = useCase;
}
if (!program.framework) {
@@ -122,24 +141,17 @@ export const askProQuestions = async (program: QuestionArgs) => {
}
if (
(program.framework === "express" || program.framework === "fastapi") &&
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?`,
message: `Would you like to generate a ${styledNextJS} frontend for your FastAPI backend?`,
initial: false,
active: "Yes",
inactive: "No",
@@ -177,32 +189,50 @@ export const askProQuestions = async (program: QuestionArgs) => {
program.observability = observability;
}
// Ask agents
if (program.template === "multiagent" && !program.agents) {
const { agents } = await prompts(
if (
(program.template === "reflex" || program.template === "multiagent") &&
!program.useCase
) {
const choices =
program.template === "reflex"
? [
{ title: "Structured Extractor", value: "extractor" },
{
title: "Contract review (using Workflow)",
value: "contract_review",
},
]
: [
{
title: "Financial report (generate a financial report)",
value: "financial_report",
},
{
title: "Form filling (fill missing value in a CSV file)",
value: "form_filling",
},
{ title: "Blog writer (Write a blog post)", value: "blog" },
];
const { useCase } = await prompts(
{
type: "select",
name: "agents",
message: "Which agents would you like to use?",
choices: [
{
title: "Financial report (generate a financial report)",
value: "financial_report",
},
{
title: "Form filling (fill missing value in a CSV file)",
value: "form_filling",
},
{
title: "Blog writer (Write a blog post)",
value: "blog_writer",
},
],
name: "useCase",
message: "Which use case would you like to use?",
choices,
initial: 0,
},
questionHandlers,
);
program.agents = agents;
program.useCase = useCase;
}
// Configure framework and data sources for Reflex template
if (program.template === "reflex") {
program.framework = "fastapi";
program.dataSources =
program.useCase === "extractor" ? [EXAMPLE_FILE] : [EXAMPLE_GDPR];
}
if (!program.modelConfig) {
@@ -228,8 +258,8 @@ export const askProQuestions = async (program: QuestionArgs) => {
program.vectorDb = vectorDb;
}
if (program.vectorDb === "llamacloud") {
// When using a LlamaCloud index, don't ask for data sources just copy an example file
if (program.vectorDb === "llamacloud" && program.dataSources.length === 0) {
// When using a LlamaCloud index and no data sources are provided, just copy an example file
program.dataSources = [EXAMPLE_FILE];
}
@@ -360,11 +390,8 @@ export const askProQuestions = async (program: QuestionArgs) => {
// 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"
) {
// Reflex template doesn't support LlamaParse right now (cannot use asyncio loop in Reflex)
if (program.useLlamaParse === undefined && program.template !== "reflex") {
// if already set useLlamaParse, don't ask again
if (program.dataSources.some((ds) => ds.type === "file")) {
const { useLlamaParse } = await prompts(
@@ -386,7 +413,7 @@ export const askProQuestions = async (program: QuestionArgs) => {
// Ask for LlamaCloud API key when using a LlamaCloud index or LlamaParse
if (isUsingLlamaCloud || program.useLlamaParse) {
if (!program.llamaCloudKey) {
if (!program.llamaCloudKey && !isCI) {
// if already set, don't ask again
// Ask for LlamaCloud API key
const { llamaCloudKey } = await prompts(
+47 -30
View File
@@ -1,5 +1,9 @@
import prompts from "prompts";
import { EXAMPLE_10K_SEC_FILES, EXAMPLE_FILE } from "../helpers/datasources";
import {
EXAMPLE_10K_SEC_FILES,
EXAMPLE_FILE,
EXAMPLE_GDPR,
} from "../helpers/datasources";
import { askModelConfig } from "../helpers/providers";
import { getTools } from "../helpers/tools";
import { ModelConfig, TemplateFramework } from "../helpers/types";
@@ -12,6 +16,7 @@ type AppType =
| "financial_report_agent"
| "form_filling"
| "extractor"
| "contract_review"
| "data_scientist";
type SimpleAnswers = {
@@ -42,6 +47,10 @@ export const askSimpleQuestions = async (
},
{ title: "Code Artifact Agent", value: "code_artifact" },
{ title: "Information Extractor", value: "extractor" },
{
title: "Contract Review (using Workflows)",
value: "contract_review",
},
],
},
questionHandlers,
@@ -51,7 +60,7 @@ export const askSimpleQuestions = async (
let llamaCloudKey = args.llamaCloudKey;
let useLlamaCloud = false;
if (appType !== "extractor") {
if (appType !== "extractor" && appType !== "contract_review") {
const { language: newLanguage } = await prompts(
{
type: "select",
@@ -65,34 +74,34 @@ export const askSimpleQuestions = async (
questionHandlers,
);
language = newLanguage;
}
const { useLlamaCloud: newUseLlamaCloud } = await prompts(
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: "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",
type: "text",
name: "llamaCloudKey",
message:
"Please provide your LlamaCloud API key (leave blank to skip):",
},
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;
}
llamaCloudKey = newLlamaCloudKey || process.env.LLAMA_CLOUD_API_KEY;
}
const results = await convertAnswers(args, {
@@ -124,14 +133,14 @@ const convertAnswers = async (
AppType,
Pick<
QuestionResults,
"template" | "tools" | "frontend" | "dataSources" | "agents"
"template" | "tools" | "frontend" | "dataSources" | "useCase"
> & {
modelConfig?: ModelConfig;
}
> = {
rag: {
template: "streaming",
tools: getTools(["duckduckgo"]),
tools: getTools(["weather"]),
frontend: true,
dataSources: [EXAMPLE_FILE],
},
@@ -151,7 +160,7 @@ const convertAnswers = async (
},
financial_report_agent: {
template: "multiagent",
agents: "financial_report",
useCase: "financial_report",
tools: getTools(["document_generator", "interpreter"]),
dataSources: EXAMPLE_10K_SEC_FILES,
frontend: true,
@@ -159,18 +168,26 @@ const convertAnswers = async (
},
form_filling: {
template: "multiagent",
agents: "form_filling",
useCase: "form_filling",
tools: getTools(["form_filling"]),
dataSources: EXAMPLE_10K_SEC_FILES,
frontend: true,
modelConfig: MODEL_GPT4o,
},
extractor: {
template: "extractor",
template: "reflex",
useCase: "extractor",
tools: [],
frontend: false,
dataSources: [EXAMPLE_FILE],
},
contract_review: {
template: "reflex",
useCase: "contract_review",
tools: [],
frontend: false,
dataSources: [EXAMPLE_GDPR],
},
};
const results = lookup[answers.appType];
return {
+1 -1
View File
@@ -2,7 +2,7 @@ import { InstallAppArgs } from "../create-app";
export type QuestionResults = Omit<
InstallAppArgs,
"appPath" | "packageManager" | "externalPort"
"appPath" | "packageManager"
>;
export type PureQuestionArgs = {
-3
View File
@@ -1,3 +0,0 @@
__pycache__
poetry.lock
storage
-18
View File
@@ -1,18 +0,0 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) project bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
## Getting Started
First, startup the backend as described in the [backend README](./backend/README.md).
Second, run the development server of the frontend as described in the [frontend README](./frontend/README.md).
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
- [LlamaIndexTS Documentation](https://ts.llamaindex.ai) - learn about LlamaIndex (Typescript features).
You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
@@ -32,7 +32,7 @@ poetry run generate
Third, run the development server:
```shell
poetry run python main.py
poetry run dev
```
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`.
@@ -47,14 +47,18 @@ curl --location 'localhost:8000/api/chat' \
You can start editing the API by modifying `app/api/routers/chat.py` or `app/examples/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.
Open [http://localhost:8000](http://localhost:8000) with your browser to start the app.
The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`:
To start the app optimized for **production**, run:
```
ENVIRONMENT=prod poetry run python main.py
poetry run prod
```
## Deployments
For production deployments, check the [DEPLOY.md](DEPLOY.md) file.
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
@@ -1,4 +1,3 @@
import os
from textwrap import dedent
from typing import List
@@ -6,42 +5,24 @@ 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
from app.engine.tools.query_engine import get_query_engine_tool
def _create_query_engine_tool(params=None) -> QueryEngineTool:
"""
Provide an agent worker that can be used to query the index.
"""
# Add query tool if index exists
index_config = IndexConfig(**(params or {}))
index = get_index(index_config)
if index is None:
return None
top_k = int(os.getenv("TOP_K", 0))
query_engine = index.as_query_engine(
**({"similarity_top_k": top_k} if top_k != 0 else {})
)
return QueryEngineTool(
query_engine=query_engine,
metadata=ToolMetadata(
name="query_index",
description="""
Use this tool to retrieve information about the text corpus from the index.
""",
),
)
def _get_research_tools(**kwargs) -> QueryEngineTool:
def _get_research_tools(**kwargs):
"""
Researcher take responsibility for retrieving information.
Try init wikipedia or duckduckgo tool if available.
"""
tools = []
query_engine_tool = _create_query_engine_tool(**kwargs)
if query_engine_tool is not None:
tools.append(query_engine_tool)
# Create query engine tool
index_config = IndexConfig(**kwargs)
index = get_index(index_config)
if index is not None:
query_engine_tool = get_query_engine_tool(index=index)
if query_engine_tool is not None:
tools.append(query_engine_tool)
# Create duckduckgo tool
researcher_tool_names = [
"duckduckgo_search",
"duckduckgo_image_search",
@@ -317,7 +317,7 @@ class Planner:
# gather completed sub-tasks and response pairs
completed_outputs_str = ""
for sub_task_name, task_output in completed_sub_task.items():
task_str = f"{sub_task_name}:\n" f"\t{task_output!s}\n"
task_str = f"{sub_task_name}:\n\t{task_output!s}\n"
completed_outputs_str += task_str
# get a string for the remaining sub-tasks
@@ -21,7 +21,7 @@ poetry run generate
Third, run the development server:
```shell
poetry run python main.py
poetry run dev
```
The example provides one streaming API endpoint `/api/chat`.
@@ -35,14 +35,18 @@ curl --location 'localhost:8000/api/chat' \
You can start editing the API by modifying `app/api/routers/chat.py` or `app/workflows/financial_report.py`. The API auto-updates as you save the files.
Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API.
Open [http://localhost:8000](http://localhost:8000) with your browser to start the app.
The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`:
To start the app optimized for **production**, run:
```
ENVIRONMENT=prod poetry run python main.py
poetry run prod
```
## Deployments
For production deployments, check the [DEPLOY.md](DEPLOY.md) file.
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
@@ -1,8 +1,8 @@
import os
from typing import Any, Dict, List, Optional
from app.engine.index import IndexConfig, get_index
from app.engine.tools import ToolFactory
from app.engine.tools.query_engine import get_query_engine_tool
from app.workflows.events import AgentRunEvent
from app.workflows.tools import (
call_tools,
@@ -10,7 +10,6 @@ from app.workflows.tools import (
)
from llama_index.core import Settings
from llama_index.core.base.llms.types import ChatMessage, MessageRole
from llama_index.core.indices.vector_store import VectorStoreIndex
from llama_index.core.llms.function_calling import FunctionCallingLLM
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.tools import FunctionTool, QueryEngineTool, ToolSelection
@@ -27,16 +26,16 @@ from llama_index.core.workflow import (
def create_workflow(
chat_history: Optional[List[ChatMessage]] = None,
params: Optional[Dict[str, Any]] = None,
filters: Optional[List[Any]] = None,
**kwargs,
) -> Workflow:
# Create query engine tool
index_config = IndexConfig(**params)
index: VectorStoreIndex = get_index(config=index_config)
index = get_index(index_config)
if index is None:
query_engine_tool = None
else:
top_k = int(os.getenv("TOP_K", 10))
query_engine = index.as_query_engine(similarity_top_k=top_k, filters=filters)
query_engine_tool = QueryEngineTool.from_defaults(query_engine=query_engine)
raise ValueError(
"Index is not found. Try run generation script to create the index first."
)
query_engine_tool = get_query_engine_tool(index=index)
configured_tools: Dict[str, FunctionTool] = ToolFactory.from_env(map_result=True) # type: ignore
code_interpreter_tool = configured_tools.get("interpret")
@@ -109,13 +108,13 @@ class FinancialReportWorkflow(Workflow):
self.query_engine_tool = query_engine_tool
self.code_interpreter_tool = code_interpreter_tool
self.document_generator_tool = document_generator_tool
assert (
query_engine_tool is not None
), "Query engine tool is not found. Try run generation script or upload a document file first."
assert query_engine_tool is not None, (
"Query engine tool is not found. Try run generation script or upload a document file first."
)
assert code_interpreter_tool is not None, "Code interpreter tool is required"
assert (
document_generator_tool is not None
), "Document generator tool is required"
assert document_generator_tool is not None, (
"Document generator tool is required"
)
self.tools = [
self.query_engine_tool,
self.code_interpreter_tool,
@@ -16,7 +16,7 @@ Make sure you have the `OPENAI_API_KEY` set.
Second, run the development server:
```shell
poetry run python main.py
poetry run dev
```
## Use Case: Filling Financial CSV Template
@@ -41,14 +41,18 @@ curl --location 'localhost:8000/api/chat' \
You can start editing the API by modifying `app/api/routers/chat.py` or `app/workflows/form_filling.py`. The API auto-updates as you save the files.
Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API.
Open [http://localhost:8000](http://localhost:8000) with your browser to start the app.
The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`:
To start the app optimized for **production**, run:
```
ENVIRONMENT=prod poetry run python main.py
poetry run prod
```
## Deployments
For production deployments, check the [DEPLOY.md](DEPLOY.md) file.
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
@@ -1,16 +1,7 @@
import os
from typing import Any, Dict, List, Optional
from app.engine.index import IndexConfig, get_index
from app.engine.tools import ToolFactory
from app.workflows.events import AgentRunEvent
from app.workflows.tools import (
call_tools,
chat_with_tools,
)
from llama_index.core import Settings
from llama_index.core.base.llms.types import ChatMessage, MessageRole
from llama_index.core.indices.vector_store import VectorStoreIndex
from llama_index.core.llms.function_calling import FunctionCallingLLM
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.tools import FunctionTool, QueryEngineTool, ToolSelection
@@ -23,24 +14,28 @@ from llama_index.core.workflow import (
step,
)
from app.engine.index import IndexConfig, get_index
from app.engine.tools import ToolFactory
from app.engine.tools.query_engine import get_query_engine_tool
from app.workflows.events import AgentRunEvent
from app.workflows.tools import (
call_tools,
chat_with_tools,
)
def create_workflow(
chat_history: Optional[List[ChatMessage]] = None,
params: Optional[Dict[str, Any]] = None,
filters: Optional[List[Any]] = None,
**kwargs,
) -> Workflow:
if params is None:
params = {}
if filters is None:
filters = []
# Create query engine tool
index_config = IndexConfig(**params)
index: VectorStoreIndex = get_index(config=index_config)
index = get_index(index_config)
if index is None:
query_engine_tool = None
else:
top_k = int(os.getenv("TOP_K", 10))
query_engine = index.as_query_engine(similarity_top_k=top_k, filters=filters)
query_engine_tool = QueryEngineTool.from_defaults(query_engine=query_engine)
query_engine_tool = get_query_engine_tool(index=index)
configured_tools = ToolFactory.from_env(map_result=True)
extractor_tool = configured_tools.get("extract_questions") # type: ignore
@@ -1,15 +1,15 @@
import { ChatMessage } from "llamaindex";
import { getTool } from "../engine/tools";
import { FunctionCallingAgent } from "./single-agent";
import { getQueryEngineTools } from "./tools";
import { getQueryEngineTool } from "./tools";
export const createResearcher = async (chatHistory: ChatMessage[]) => {
const queryEngineTools = await getQueryEngineTools();
const queryEngineTool = await getQueryEngineTool();
const tools = [
await getTool("wikipedia_tool"),
await getTool("duckduckgo_search"),
await getTool("image_generator"),
...(queryEngineTools ? queryEngineTools : []),
queryEngineTool,
].filter((tool) => tool !== undefined);
return new FunctionCallingAgent({
@@ -1,7 +1,7 @@
import { ChatMessage, ToolCallLLM } from "llamaindex";
import { getTool } from "../engine/tools";
import { FinancialReportWorkflow } from "./fin-report";
import { getQueryEngineTools } from "./tools";
import { getQueryEngineTool } from "./tools";
const TIMEOUT = 360 * 1000;
@@ -9,11 +9,19 @@ export async function createWorkflow(options: {
chatHistory: ChatMessage[];
llm?: ToolCallLLM;
}) {
const queryEngineTool = await getQueryEngineTool();
const codeInterpreterTool = await getTool("interpreter");
const documentGeneratorTool = await getTool("document_generator");
if (!queryEngineTool || !codeInterpreterTool || !documentGeneratorTool) {
throw new Error("One or more required tools are not defined");
}
return new FinancialReportWorkflow({
chatHistory: options.chatHistory,
queryEngineTools: (await getQueryEngineTools()) || [],
codeInterpreterTool: (await getTool("interpreter"))!,
documentGeneratorTool: (await getTool("document_generator"))!,
queryEngineTool,
codeInterpreterTool,
documentGeneratorTool,
llm: options.llm,
timeout: TIMEOUT,
});
@@ -45,7 +45,7 @@ export class FinancialReportWorkflow extends Workflow<
> {
llm: ToolCallLLM;
memory: ChatMemoryBuffer;
queryEngineTools: BaseToolWithCall[];
queryEngineTool: BaseToolWithCall;
codeInterpreterTool: BaseToolWithCall;
documentGeneratorTool: BaseToolWithCall;
systemPrompt?: string;
@@ -53,7 +53,7 @@ export class FinancialReportWorkflow extends Workflow<
constructor(options: {
llm?: ToolCallLLM;
chatHistory: ChatMessage[];
queryEngineTools: BaseToolWithCall[];
queryEngineTool: BaseToolWithCall;
codeInterpreterTool: BaseToolWithCall;
documentGeneratorTool: BaseToolWithCall;
systemPrompt?: string;
@@ -70,7 +70,7 @@ export class FinancialReportWorkflow extends Workflow<
throw new Error("LLM is not a ToolCallLLM");
}
this.systemPrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
this.queryEngineTools = options.queryEngineTools;
this.queryEngineTool = options.queryEngineTool;
this.codeInterpreterTool = options.codeInterpreterTool;
this.documentGeneratorTool = options.documentGeneratorTool;
@@ -153,10 +153,11 @@ export class FinancialReportWorkflow extends Workflow<
> => {
const chatHistory = ev.data.input;
const tools = [this.codeInterpreterTool, this.documentGeneratorTool];
if (this.queryEngineTools) {
tools.push(...this.queryEngineTools);
}
const tools = [
this.codeInterpreterTool,
this.documentGeneratorTool,
this.queryEngineTool,
];
const toolCallResponse = await chatWithTools(this.llm, tools, chatHistory);
@@ -189,10 +190,7 @@ export class FinancialReportWorkflow extends Workflow<
toolCalls: toolCallResponse.toolCalls,
});
default:
if (
this.queryEngineTools &&
this.queryEngineTools.some((tool) => tool.metadata.name === toolName)
) {
if (this.queryEngineTool.metadata.name === toolName) {
return new ResearchEvent({
toolCalls: toolCallResponse.toolCalls,
});
@@ -216,7 +214,7 @@ export class FinancialReportWorkflow extends Workflow<
const { toolCalls } = ev.data;
const toolMsgs = await callTools({
tools: this.queryEngineTools,
tools: [this.queryEngineTool],
toolCalls,
ctx,
agentName: "Researcher",
@@ -1,7 +1,7 @@
import { ChatMessage, ToolCallLLM } from "llamaindex";
import { getTool } from "../engine/tools";
import { FormFillingWorkflow } from "./form-filling";
import { getQueryEngineTools } from "./tools";
import { getQueryEngineTool } from "./tools";
const TIMEOUT = 360 * 1000;
@@ -9,11 +9,18 @@ export async function createWorkflow(options: {
chatHistory: ChatMessage[];
llm?: ToolCallLLM;
}) {
const extractorTool = await getTool("extract_missing_cells");
const fillMissingCellsTool = await getTool("fill_missing_cells");
if (!extractorTool || !fillMissingCellsTool) {
throw new Error("One or more required tools are not defined");
}
return new FormFillingWorkflow({
chatHistory: options.chatHistory,
queryEngineTools: (await getQueryEngineTools()) || [],
extractorTool: (await getTool("extract_missing_cells"))!,
fillMissingCellsTool: (await getTool("fill_missing_cells"))!,
queryEngineTool: (await getQueryEngineTool()) || undefined,
extractorTool,
fillMissingCellsTool,
llm: options.llm,
timeout: TIMEOUT,
});
@@ -48,7 +48,7 @@ export class FormFillingWorkflow extends Workflow<
llm: ToolCallLLM;
memory: ChatMemoryBuffer;
extractorTool: BaseToolWithCall;
queryEngineTools?: BaseToolWithCall[];
queryEngineTool?: BaseToolWithCall;
fillMissingCellsTool: BaseToolWithCall;
systemPrompt?: string;
@@ -56,7 +56,7 @@ export class FormFillingWorkflow extends Workflow<
llm?: ToolCallLLM;
chatHistory: ChatMessage[];
extractorTool: BaseToolWithCall;
queryEngineTools?: BaseToolWithCall[];
queryEngineTool?: BaseToolWithCall;
fillMissingCellsTool: BaseToolWithCall;
systemPrompt?: string;
verbose?: boolean;
@@ -73,7 +73,7 @@ export class FormFillingWorkflow extends Workflow<
}
this.systemPrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
this.extractorTool = options.extractorTool;
this.queryEngineTools = options.queryEngineTools;
this.queryEngineTool = options.queryEngineTool;
this.fillMissingCellsTool = options.fillMissingCellsTool;
this.memory = new ChatMemoryBuffer({
@@ -156,8 +156,8 @@ export class FormFillingWorkflow extends Workflow<
const chatHistory = ev.data.input;
const tools = [this.extractorTool, this.fillMissingCellsTool];
if (this.queryEngineTools) {
tools.push(...this.queryEngineTools);
if (this.queryEngineTool) {
tools.push(this.queryEngineTool);
}
const toolCallResponse = await chatWithTools(this.llm, tools, chatHistory);
@@ -192,8 +192,8 @@ export class FormFillingWorkflow extends Workflow<
});
default:
if (
this.queryEngineTools &&
this.queryEngineTools.some((tool) => tool.metadata.name === toolName)
this.queryEngineTool &&
this.queryEngineTool.metadata.name === toolName
) {
return new FindAnswersEvent({
toolCalls: toolCallResponse.toolCalls,
@@ -232,7 +232,7 @@ export class FormFillingWorkflow extends Workflow<
ev: FindAnswersEvent,
): Promise<InputEvent> => {
const { toolCalls } = ev.data;
if (!this.queryEngineTools) {
if (!this.queryEngineTool) {
throw new Error("Query engine tool is not available");
}
ctx.sendEvent(
@@ -243,7 +243,7 @@ export class FormFillingWorkflow extends Workflow<
}),
);
const toolMsgs = await callTools({
tools: this.queryEngineTools,
tools: [this.queryEngineTool],
toolCalls,
ctx,
agentName: "Researcher",
@@ -1,18 +1,18 @@
import os
from typing import List
from app.engine.index import IndexConfig, get_index
from app.engine.tools import ToolFactory
from llama_index.core.agent import AgentRunner
from llama_index.core.callbacks import CallbackManager
from llama_index.core.settings import Settings
from llama_index.core.tools import BaseTool
from llama_index.core.tools.query_engine import QueryEngineTool
from app.engine.index import IndexConfig, get_index
from app.engine.tools import ToolFactory
from app.engine.tools.query_engine import get_query_engine_tool
def get_chat_engine(filters=None, params=None, event_handlers=None, **kwargs):
def get_chat_engine(params=None, event_handlers=None, **kwargs):
system_prompt = os.getenv("SYSTEM_PROMPT")
top_k = int(os.getenv("TOP_K", 0))
tools: List[BaseTool] = []
callback_manager = CallbackManager(handlers=event_handlers or [])
@@ -20,10 +20,7 @@ def get_chat_engine(filters=None, params=None, event_handlers=None, **kwargs):
index_config = IndexConfig(callback_manager=callback_manager, **(params or {}))
index = get_index(index_config)
if index is not None:
query_engine = index.as_query_engine(
filters=filters, **({"similarity_top_k": top_k} if top_k != 0 else {})
)
query_engine_tool = QueryEngineTool.from_defaults(query_engine=query_engine)
query_engine_tool = get_query_engine_tool(index, **kwargs)
tools.append(query_engine_tool)
# Add additional tools
@@ -5,7 +5,7 @@ import uuid
from typing import List, Optional
from app.services.file import DocumentFile, FileService
from e2b_code_interpreter import CodeInterpreter
from e2b_code_interpreter import Sandbox
from e2b_code_interpreter.models import Logs
from llama_index.core.tools import FunctionTool
from pydantic import BaseModel
@@ -61,7 +61,7 @@ class E2BCodeInterpreter:
Lazily initialize the interpreter.
"""
logger.info(f"Initializing interpreter with {len(sandbox_files)} files")
self.interpreter = CodeInterpreter(api_key=self.api_key)
self.interpreter = Sandbox(api_key=self.api_key)
if len(sandbox_files) > 0:
for file_path in sandbox_files:
file_name = os.path.basename(file_path)
@@ -159,11 +159,11 @@ class E2BCodeInterpreter:
if self.interpreter is None:
self._init_interpreter(sandbox_files)
if self.interpreter and self.interpreter.notebook:
if self.interpreter:
logger.info(
f"\n{'='*50}\n> Running following AI-generated code:\n{code}\n{'='*50}"
f"\n{'=' * 50}\n> Running following AI-generated code:\n{code}\n{'=' * 50}"
)
exec = self.interpreter.notebook.exec_cell(code)
exec = self.interpreter.run_code(code)
if exec.error:
error_message = f"The code failed to execute successfully. Error: {exec.error}. Try to fix the code and run again."
@@ -0,0 +1,187 @@
import os
from typing import Any, Dict, List, Optional, Sequence
from llama_index.core import get_response_synthesizer
from llama_index.core.base.base_query_engine import BaseQueryEngine
from llama_index.core.base.response.schema import RESPONSE_TYPE, Response
from llama_index.core.multi_modal_llms import MultiModalLLM
from llama_index.core.prompts.base import BasePromptTemplate
from llama_index.core.prompts.default_prompt_selectors import (
DEFAULT_TEXT_QA_PROMPT_SEL,
)
from llama_index.core.query_engine.multi_modal import _get_image_and_text_nodes
from llama_index.core.response_synthesizers.base import BaseSynthesizer, QueryTextType
from llama_index.core.schema import (
ImageNode,
NodeWithScore,
)
from llama_index.core.tools.query_engine import QueryEngineTool
from llama_index.core.types import RESPONSE_TEXT_TYPE
from app.settings import get_multi_modal_llm
def create_query_engine(index, **kwargs) -> BaseQueryEngine:
"""
Create a query engine for the given index.
Args:
index: The index to create a query engine for.
params (optional): Additional parameters for the query engine, e.g: similarity_top_k
"""
top_k = int(os.getenv("TOP_K", 0))
if top_k != 0 and kwargs.get("filters") is None:
kwargs["similarity_top_k"] = top_k
multimodal_llm = get_multi_modal_llm()
if multimodal_llm:
kwargs["response_synthesizer"] = MultiModalSynthesizer(
multimodal_model=multimodal_llm,
)
# If index is index is LlamaCloudIndex
# use auto_routed mode for better query results
if index.__class__.__name__ == "LlamaCloudIndex":
if kwargs.get("retrieval_mode") is None:
kwargs["retrieval_mode"] = "auto_routed"
if multimodal_llm:
kwargs["retrieve_image_nodes"] = True
return index.as_query_engine(**kwargs)
def get_query_engine_tool(
index,
name: Optional[str] = None,
description: Optional[str] = None,
**kwargs,
) -> QueryEngineTool:
"""
Get a query engine tool for the given index.
Args:
index: The index to create a query engine for.
name (optional): The name of the tool.
description (optional): The description of the tool.
"""
if name is None:
name = "query_index"
if description is None:
description = (
"Use this tool to retrieve information about the text corpus from an index."
)
query_engine = create_query_engine(index, **kwargs)
return QueryEngineTool.from_defaults(
query_engine=query_engine,
name=name,
description=description,
)
class MultiModalSynthesizer(BaseSynthesizer):
"""
A synthesizer that summarizes text nodes and uses a multi-modal LLM to generate a response.
"""
def __init__(
self,
multimodal_model: MultiModalLLM,
response_synthesizer: Optional[BaseSynthesizer] = None,
text_qa_template: Optional[BasePromptTemplate] = None,
*args,
**kwargs,
):
super().__init__(*args, **kwargs)
self._multi_modal_llm = multimodal_model
self._response_synthesizer = response_synthesizer or get_response_synthesizer()
self._text_qa_template = text_qa_template or DEFAULT_TEXT_QA_PROMPT_SEL
def _get_prompts(self, **kwargs) -> Dict[str, Any]:
return {
"text_qa_template": self._text_qa_template,
}
def _update_prompts(self, prompts: Dict[str, Any]) -> None:
if "text_qa_template" in prompts:
self._text_qa_template = prompts["text_qa_template"]
async def aget_response(
self,
*args,
**response_kwargs: Any,
) -> RESPONSE_TEXT_TYPE:
return await self._response_synthesizer.aget_response(*args, **response_kwargs)
def get_response(self, *args, **kwargs) -> RESPONSE_TEXT_TYPE:
return self._response_synthesizer.get_response(*args, **kwargs)
async def asynthesize(
self,
query: QueryTextType,
nodes: List[NodeWithScore],
additional_source_nodes: Optional[Sequence[NodeWithScore]] = None,
**response_kwargs: Any,
) -> RESPONSE_TYPE:
image_nodes, text_nodes = _get_image_and_text_nodes(nodes)
if len(image_nodes) == 0:
return await self._response_synthesizer.asynthesize(query, text_nodes)
# Summarize the text nodes to avoid exceeding the token limit
text_response = str(
await self._response_synthesizer.asynthesize(query, text_nodes)
)
fmt_prompt = self._text_qa_template.format(
context_str=text_response,
query_str=query.query_str, # type: ignore
)
llm_response = await self._multi_modal_llm.acomplete(
prompt=fmt_prompt,
image_documents=[
image_node.node
for image_node in image_nodes
if isinstance(image_node.node, ImageNode)
],
)
return Response(
response=str(llm_response),
source_nodes=nodes,
metadata={"text_nodes": text_nodes, "image_nodes": image_nodes},
)
def synthesize(
self,
query: QueryTextType,
nodes: List[NodeWithScore],
additional_source_nodes: Optional[Sequence[NodeWithScore]] = None,
**response_kwargs: Any,
) -> RESPONSE_TYPE:
image_nodes, text_nodes = _get_image_and_text_nodes(nodes)
if len(image_nodes) == 0:
return self._response_synthesizer.synthesize(query, text_nodes)
# Summarize the text nodes to avoid exceeding the token limit
text_response = str(self._response_synthesizer.synthesize(query, text_nodes))
fmt_prompt = self._text_qa_template.format(
context_str=text_response,
query_str=query.query_str, # type: ignore
)
llm_response = self._multi_modal_llm.complete(
prompt=fmt_prompt,
image_documents=[
image_node.node
for image_node in image_nodes
if isinstance(image_node.node, ImageNode)
],
)
return Response(
response=str(llm_response),
source_nodes=nodes,
metadata={"text_nodes": text_nodes, "image_nodes": image_nodes},
)
@@ -9,7 +9,7 @@ from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.settings import Settings
def get_chat_engine(filters=None, params=None, event_handlers=None, **kwargs):
def get_chat_engine(params=None, event_handlers=None, **kwargs):
system_prompt = os.getenv("SYSTEM_PROMPT")
citation_prompt = os.getenv("SYSTEM_CITATION_PROMPT", None)
top_k = int(os.getenv("TOP_K", 0))
@@ -33,10 +33,9 @@ def get_chat_engine(filters=None, params=None, event_handlers=None, **kwargs):
"StorageContext is empty - call 'poetry run generate' to generate the storage first"
),
)
retriever = index.as_retriever(
filters=filters, **({"similarity_top_k": top_k} if top_k != 0 else {})
)
if top_k != 0 and kwargs.get("similarity_top_k") is None:
kwargs["similarity_top_k"] = top_k
retriever = index.as_retriever(**kwargs)
return CondensePlusContextChatEngine(
llm=llm,
@@ -1,14 +1,9 @@
import {
BaseChatEngine,
BaseToolWithCall,
LLMAgent,
QueryEngineTool,
} from "llamaindex";
import { BaseChatEngine, BaseToolWithCall, LLMAgent } from "llamaindex";
import fs from "node:fs/promises";
import path from "node:path";
import { getDataSource } from "./index";
import { generateFilters } from "./queryFilter";
import { createTools } from "./tools";
import { createQueryEngineTool } from "./tools/query-engine";
export async function createChatEngine(documentIds?: string[], params?: any) {
const tools: BaseToolWithCall[] = [];
@@ -17,17 +12,7 @@ export async function createChatEngine(documentIds?: string[], params?: any) {
// Delete this code if you don't have a data source
const index = await getDataSource(params);
if (index) {
tools.push(
new QueryEngineTool({
queryEngine: index.asQueryEngine({
preFilters: generateFilters(documentIds || []),
}),
metadata: {
name: "data_query_engine",
description: `A query engine for documents from your data source.`,
},
}),
);
tools.push(createQueryEngineTool(index, { documentIds }));
}
const configFile = path.join("config", "tools.json");
@@ -103,6 +103,7 @@ export class CodeGeneratorTool implements BaseTool<CodeGeneratorParameter> {
const artifact = await this.generateArtifact(
input.requirement,
input.oldCode,
input.sandboxFiles, // help the generated code use exact files
);
if (input.sandboxFiles) {
artifact.files = input.sandboxFiles;
@@ -117,10 +118,12 @@ export class CodeGeneratorTool implements BaseTool<CodeGeneratorParameter> {
async generateArtifact(
query: string,
oldCode?: string,
attachments?: string[],
): Promise<CodeArtifact> {
const userMessage = `
${query}
${oldCode ? `The existing code is: \n\`\`\`${oldCode}\`\`\`` : ""}
${attachments ? `The attachments are: \n${attachments.join("\n")}` : ""}
`;
const messages: ChatMessage[] = [
{ role: "system", content: CODE_GENERATION_PROMPT },
@@ -1,4 +1,4 @@
import { CodeInterpreter, Logs, Result } from "@e2b/code-interpreter";
import { Logs, Result, Sandbox } from "@e2b/code-interpreter";
import type { JSONSchemaType } from "ajv";
import fs from "fs";
import { BaseTool, ToolMetadata } from "llamaindex";
@@ -82,7 +82,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
private apiKey?: string;
private fileServerURLPrefix?: string;
metadata: ToolMetadata<JSONSchemaType<InterpreterParameter>>;
codeInterpreter?: CodeInterpreter;
codeInterpreter?: Sandbox;
constructor(params?: InterpreterToolParams) {
this.metadata = params?.metadata || DEFAULT_META_DATA;
@@ -104,24 +104,27 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
public async initInterpreter(input: InterpreterParameter) {
if (!this.codeInterpreter) {
this.codeInterpreter = await CodeInterpreter.create({
this.codeInterpreter = await Sandbox.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);
// upload files to sandbox when it's initialized
if (input.sandboxFiles) {
console.log(`Uploading ${input.sandboxFiles.length} files to sandbox`);
try {
for (const filePath of input.sandboxFiles) {
const fileName = path.basename(filePath);
const localFilePath = path.join(this.uploadedFilesDir, fileName);
const content = fs.readFileSync(localFilePath);
const arrayBuffer = new Uint8Array(content).buffer;
await this.codeInterpreter?.files.write(filePath, arrayBuffer);
}
} catch (error) {
console.error("Got error when uploading files to sandbox", error);
}
} catch (error) {
console.error("Got error when uploading files to sandbox", error);
}
}
return this.codeInterpreter;
}
@@ -148,7 +151,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
`\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);
const exec = await interpreter.runCode(input.code);
if (exec.error) console.error("[Code Interpreter error]", exec.error);
const extraResult = await this.getExtraResult(exec.results[0]);
const result: InterpreterToolOutput = {
@@ -167,7 +170,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
}
async close() {
await this.codeInterpreter?.close();
await this.codeInterpreter?.kill();
}
private async getExtraResult(
@@ -0,0 +1,57 @@
import {
BaseQueryEngine,
CloudRetrieveParams,
LlamaCloudIndex,
MetadataFilters,
QueryEngineTool,
VectorStoreIndex,
} from "llamaindex";
import { generateFilters } from "../queryFilter";
interface QueryEngineParams {
documentIds?: string[];
topK?: number;
}
export function createQueryEngineTool(
index: VectorStoreIndex | LlamaCloudIndex,
params?: QueryEngineParams,
name?: string,
description?: string,
): QueryEngineTool {
return new QueryEngineTool({
queryEngine: createQueryEngine(index, params),
metadata: {
name: name || "query_engine",
description:
description ||
`Use this tool to retrieve information about the text corpus from an index.`,
},
});
}
function createQueryEngine(
index: VectorStoreIndex | LlamaCloudIndex,
params?: QueryEngineParams,
): BaseQueryEngine {
const baseQueryParams = {
similarityTopK:
params?.topK ??
(process.env.TOP_K ? parseInt(process.env.TOP_K) : undefined),
};
if (index instanceof LlamaCloudIndex) {
return index.asQueryEngine({
...baseQueryParams,
retrieval_mode: "auto_routed",
preFilters: generateFilters(
params?.documentIds || [],
) as CloudRetrieveParams["filters"],
});
}
return index.asQueryEngine({
...baseQueryParams,
preFilters: generateFilters(params?.documentIds || []) as MetadataFilters,
});
}
@@ -1,12 +1,13 @@
import logging
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, status
from app.api.routers.models import (
ChatData,
)
from app.api.routers.vercel_response import VercelStreamResponse
from app.engine.query_filter import generate_filters
from app.workflows import create_workflow
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, status
chat_router = r = APIRouter()
@@ -28,7 +29,9 @@ async def chat(
params = data.data or {}
workflow = create_workflow(
chat_history=messages, params=params, filters=filters
chat_history=messages,
params=params,
filters=filters,
)
event_handler = workflow.run(input=last_message_content, streaming=True)
@@ -19,6 +19,7 @@ class VercelStreamResponse(StreamingResponse):
TEXT_PREFIX = "0:"
DATA_PREFIX = "8:"
ERROR_PREFIX = "3:"
def __init__(self, request: Request, chat_data: ChatData, *args, **kwargs):
self.request = request
@@ -41,13 +42,16 @@ class VercelStreamResponse(StreamingResponse):
yield output
except asyncio.CancelledError:
logger.info("Stopping workflow")
await event_handler.cancel_run()
logger.warning("Workflow has been cancelled!")
except Exception as e:
logger.error(
f"Unexpected error in content_generator: {str(e)}", exc_info=True
)
yield self.convert_error(
"An unexpected error occurred while processing your request, preventing the creation of a final answer. Please try again."
)
finally:
await event_handler.cancel_run()
logger.info("The stream has been stopped!")
def _create_stream(
@@ -107,6 +111,11 @@ class VercelStreamResponse(StreamingResponse):
data_str = json.dumps(data)
return f"{cls.DATA_PREFIX}[{data_str}]\n"
@classmethod
def convert_error(cls, error: str):
error_str = json.dumps(error)
return f"{cls.ERROR_PREFIX}{error_str}\n"
@staticmethod
async def _generate_next_questions(chat_history: List[Message], response: str):
questions = await NextQuestionSuggestion.suggest_next_questions(
@@ -1,4 +1,4 @@
import { Message, streamToResponse } from "ai";
import { LlamaIndexAdapter, Message } from "ai";
import { Request, Response } from "express";
import {
convertToChatHistory,
@@ -28,7 +28,20 @@ export const chat = async (req: Request, res: Response) => {
const { stream, dataStream } =
await createStreamFromWorkflowContext(context);
return streamToResponse(stream, res, {}, dataStream);
const streamResponse = LlamaIndexAdapter.toDataStreamResponse(stream, {
data: dataStream,
});
if (streamResponse.body) {
const reader = streamResponse.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
res.end();
return;
}
res.write(value);
}
}
} catch (error) {
console.error("[LlamaIndex]", error);
return res.status(500).json({
@@ -1,5 +1,5 @@
import { initObservability } from "@/app/observability";
import { StreamingTextResponse, type Message } from "ai";
import { LlamaIndexAdapter, type Message } from "ai";
import { NextRequest, NextResponse } from "next/server";
import { initSettings } from "./engine/settings";
import {
@@ -41,9 +41,9 @@ export async function POST(request: NextRequest) {
});
const { stream, dataStream } =
await createStreamFromWorkflowContext(context);
// Return the two streams in one response
return new StreamingTextResponse(stream, {}, dataStream);
return LlamaIndexAdapter.toDataStreamResponse(stream, {
data: dataStream,
});
} catch (error) {
console.error("[LlamaIndex]", error);
return NextResponse.json(
@@ -3,20 +3,15 @@ import {
WorkflowContext,
WorkflowEvent,
} from "@llamaindex/workflow";
import {
StreamData,
createStreamDataTransformer,
trimStartOfStreamHelper,
} from "ai";
import { ChatResponseChunk } from "llamaindex";
import { StreamData } from "ai";
import { ChatResponseChunk, EngineResponse } from "llamaindex";
import { ReadableStream } from "stream/web";
import { AgentRunEvent } from "./type";
export async function createStreamFromWorkflowContext<Input, Output, Context>(
context: WorkflowContext<Input, Output, Context>,
): Promise<{ stream: ReadableStream<string>; dataStream: StreamData }> {
const trimStartOfStream = trimStartOfStreamHelper();
): Promise<{ stream: ReadableStream<EngineResponse>; dataStream: StreamData }> {
const dataStream = new StreamData();
const encoder = new TextEncoder();
let generator: AsyncGenerator<ChatResponseChunk> | undefined;
const closeStreams = (controller: ReadableStreamDefaultController) => {
@@ -24,10 +19,10 @@ export async function createStreamFromWorkflowContext<Input, Output, Context>(
dataStream.close();
};
const mainStream = new ReadableStream({
const stream = new ReadableStream<EngineResponse>({
async start(controller) {
// Kickstart the stream by sending an empty string
controller.enqueue(encoder.encode(""));
controller.enqueue({ delta: "" } as EngineResponse);
},
async pull(controller) {
while (!generator) {
@@ -46,17 +41,14 @@ export async function createStreamFromWorkflowContext<Input, Output, Context>(
closeStreams(controller);
return;
}
const text = trimStartOfStream(chunk.delta ?? "");
if (text) {
controller.enqueue(encoder.encode(text));
const delta = chunk.delta ?? "";
if (delta) {
controller.enqueue({ delta } as EngineResponse);
}
},
});
return {
stream: mainStream.pipeThrough(createStreamDataTransformer()),
dataStream,
};
return { stream, dataStream };
}
function handleEvent(
@@ -5,7 +5,6 @@ import {
ChatMessage,
ChatResponse,
ChatResponseChunk,
LlamaCloudIndex,
PartialToolCall,
QueryEngineTool,
ToolCall,
@@ -14,58 +13,17 @@ import {
} from "llamaindex";
import crypto from "node:crypto";
import { getDataSource } from "../engine";
import { createQueryEngineTool } from "../engine/tools/query-engine";
import { AgentRunEvent } from "./type";
export const getQueryEngineTools = async (): Promise<
QueryEngineTool[] | null
> => {
const topK = process.env.TOP_K ? parseInt(process.env.TOP_K) : undefined;
export const getQueryEngineTool = async (): Promise<QueryEngineTool | null> => {
const index = await getDataSource();
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.asQueryEngine({
similarityTopK: topK,
}),
metadata: {
name: "retriever",
description: `Use this tool to retrieve information about the text corpus from the index.`,
},
}),
];
}
return createQueryEngineTool(index);
};
/**
@@ -0,0 +1,59 @@
This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Reflex](https://reflex.dev/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama) featuring automated contract review and compliance analysis use case.
## Getting Started
First, setup the environment with poetry:
> **_Note:_** This step is not needed if you are using the dev-container.
```shell
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 example document in the `./data` directory:
```shell
poetry run generate
```
Third, start app with `reflex` command:
```shell
poetry run reflex run
```
To deploy the application, refer to the Reflex deployment guide: https://reflex.dev/docs/hosting/deploy-quick-start/
### UI
The application provides an interactive web interface accessible at http://localhost:3000 for testing the contract review workflow.
To get started:
1. Upload a contract document:
- Use the provided [example_vendor_agreement.md](./example_vendor_agreement.md) for testing
- Or upload your own document (supported formats: PDF, TXT, Markdown, DOCX)
2. Review Process:
- The system will automatically analyze your document against compliance guidelines
- By default, it uses [GDPR](./data/gdpr.pdf) as the compliance benchmark
- Custom guidelines can be used by adding your policy documents to the `./data` directory and running `poetry run generate` to update the embeddings
The interface will display the analysis results for the compliance of the contract document.
### Development
You can start editing the backend workflow by modifying the [`ContractReviewWorkflow`](./app/services/contract_reviewer.py).
For UI, you can start looking at the [`AppState`](./app/ui/states/app.py) code and navigating to the appropriate components.
## Learn More
To learn more about LlamaIndex, take a look at the following resources:
- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
@@ -0,0 +1,47 @@
DATA_DIR = "data"
UPLOADED_DIR = "output/uploaded"
# Workflow prompts
CONTRACT_EXTRACT_PROMPT = """\
You are given contract data below. \
Please extract out relevant information from the contract into the defined schema - the schema is defined as a function call.\
{contract_data}
"""
CONTRACT_MATCH_PROMPT = """\
Given the following contract clause and the corresponding relevant guideline text, evaluate the compliance \
and provide a JSON object that matches the ClauseComplianceCheck schema.
**Contract Clause:**
{clause_text}
**Matched Guideline Text(s):**
{guideline_text}
"""
COMPLIANCE_REPORT_SYSTEM_PROMPT = """\
You are a compliance reporting assistant. Your task is to generate a final compliance report \
based on the results of clause compliance checks against \
a given set of guidelines.
Analyze the provided compliance results and produce a structured report according to the specified schema.
Ensure that if there are no noncompliant clauses, the report clearly indicates full compliance.
"""
COMPLIANCE_REPORT_USER_PROMPT = """\
A set of clauses within a contract were checked against GDPR compliance guidelines for the following vendor: {vendor_name}.
The set of noncompliant clauses are given below.
Each section includes:
- **Clause:** The exact text of the contract clause.
- **Guideline:** The relevant GDPR guideline text.
- **Compliance Status:** Should be `False` for noncompliant clauses.
- **Notes:** Additional information or explanations.
{compliance_results}
Based on the above compliance results, generate a final compliance report following the `ComplianceReport` schema below.
If there are no noncompliant clauses, the report should indicate that the contract is fully compliant.
"""
@@ -0,0 +1,85 @@
from typing import List, Optional
from pydantic import BaseModel, Field
class ContractClause(BaseModel):
clause_text: str = Field(..., description="The exact text of the clause.")
mentions_data_processing: bool = Field(
False,
description="True if the clause involves personal data collection or usage.",
)
mentions_data_transfer: bool = Field(
False,
description="True if the clause involves transferring personal data, especially to third parties or across borders.",
)
requires_consent: bool = Field(
False,
description="True if the clause explicitly states that user consent is needed for data activities.",
)
specifies_purpose: bool = Field(
False,
description="True if the clause specifies a clear purpose for data handling or transfer.",
)
mentions_safeguards: bool = Field(
False,
description="True if the clause mentions security measures or other safeguards for data.",
)
class ContractExtraction(BaseModel):
vendor_name: Optional[str] = Field(
None, description="The vendor's name if identifiable."
)
effective_date: Optional[str] = Field(
None, description="The effective date of the agreement, if available."
)
governing_law: Optional[str] = Field(
None, description="The governing law of the contract, if stated."
)
clauses: List[ContractClause] = Field(
..., description="List of extracted clauses and their relevant indicators."
)
class GuidelineMatch(BaseModel):
guideline_text: str = Field(
...,
description="The single most relevant guideline excerpt related to this clause.",
)
similarity_score: float = Field(
...,
description="Similarity score indicating how closely the guideline matches the clause, e.g., between 0 and 1.",
)
relevance_explanation: Optional[str] = Field(
None, description="Brief explanation of why this guideline is relevant."
)
class ClauseComplianceCheck(BaseModel):
clause_text: str = Field(
..., description="The exact text of the clause from the contract."
)
matched_guideline: Optional[GuidelineMatch] = Field(
None, description="The most relevant guideline extracted via vector retrieval."
)
compliant: bool = Field(
...,
description="Indicates whether the clause is considered compliant with the referenced guideline.",
)
notes: Optional[str] = Field(
None, description="Additional commentary or recommendations."
)
class ComplianceReport(BaseModel):
vendor_name: Optional[str] = Field(
None, description="The vendor's name if identified from the contract."
)
overall_compliant: bool = Field(
..., description="Indicates if the contract is considered overall compliant."
)
summary_notes: str = Field(
...,
description="Always give a general summary or recommendations for achieving full compliance.",
)
@@ -0,0 +1,361 @@
import logging
import os
import uuid
from enum import Enum
from pathlib import Path
from typing import List
from llama_index.core import SimpleDirectoryReader
from llama_index.core.llms import LLM
from llama_index.core.prompts import ChatPromptTemplate
from llama_index.core.retrievers import BaseRetriever
from llama_index.core.settings import Settings
from llama_index.core.workflow import (
Context,
Event,
StartEvent,
StopEvent,
Workflow,
step,
)
from app.config import (
COMPLIANCE_REPORT_SYSTEM_PROMPT,
COMPLIANCE_REPORT_USER_PROMPT,
CONTRACT_EXTRACT_PROMPT,
CONTRACT_MATCH_PROMPT,
)
from app.engine.index import get_index
from app.models import (
ClauseComplianceCheck,
ComplianceReport,
ContractClause,
ContractExtraction,
)
logger = logging.getLogger(__name__)
def get_workflow():
index = get_index()
if index is None:
raise RuntimeError(
"Index not found! Please run `poetry run generate` to populate an index first."
)
return ContractReviewWorkflow(
guideline_retriever=index.as_retriever(),
llm=Settings.llm,
verbose=True,
timeout=120,
)
class Step(Enum):
PARSE_CONTRACT = "parse_contract"
ANALYZE_CLAUSES = "analyze_clauses"
HANDLE_CLAUSE = "handle_clause"
GENERATE_REPORT = "generate_report"
class ContractExtractionEvent(Event):
contract_extraction: ContractExtraction
class MatchGuidelineEvent(Event):
request_id: str
clause: ContractClause
vendor_name: str
class MatchGuidelineResultEvent(Event):
result: ClauseComplianceCheck
class GenerateReportEvent(Event):
match_results: List[ClauseComplianceCheck]
class LogEvent(Event):
msg: str
step: Step
data: dict = {}
is_step_completed: bool = False
class ContractReviewWorkflow(Workflow):
"""Contract review workflow."""
def __init__(
self,
guideline_retriever: BaseRetriever,
llm: LLM | None = None,
similarity_top_k: int = 20,
**kwargs,
) -> None:
"""Init params."""
super().__init__(**kwargs)
self.guideline_retriever = guideline_retriever
self.llm = llm or Settings.llm
self.similarity_top_k = similarity_top_k
# if not exists, create
out_path = Path("output") / "workflow_output"
if not out_path.exists():
out_path.mkdir(parents=True, exist_ok=True)
os.chmod(str(out_path), 0o0777)
self.output_dir = out_path
@step
async def parse_contract(
self, ctx: Context, ev: StartEvent
) -> ContractExtractionEvent:
"""Parse the contract."""
uploaded_contract_path = Path(ev.contract_path)
contract_file_name = uploaded_contract_path.name
# Set contract file name in context
await ctx.set("contract_file_name", contract_file_name)
# Parse and read the contract to documents
docs = SimpleDirectoryReader(
input_files=[str(uploaded_contract_path)]
).load_data()
ctx.write_event_to_stream(
LogEvent(
msg=f"Loaded document: {contract_file_name}",
step=Step.PARSE_CONTRACT,
data={
"saved_path": str(uploaded_contract_path),
"parsed_data": None,
},
)
)
# Parse the contract into a structured model
# See the ContractExtraction model for information we want to extract
ctx.write_event_to_stream(
LogEvent(
msg="Extracting information from the document",
step=Step.PARSE_CONTRACT,
data={
"saved_path": str(uploaded_contract_path),
"parsed_data": None,
},
)
)
prompt = ChatPromptTemplate.from_messages([("user", CONTRACT_EXTRACT_PROMPT)])
contract_extraction = await self.llm.astructured_predict(
ContractExtraction,
prompt,
contract_data="\n".join(
[d.get_content(metadata_mode="all") for d in docs] # type: ignore
),
)
if not isinstance(contract_extraction, ContractExtraction):
raise ValueError(f"Invalid extraction from contract: {contract_extraction}")
# save output template to file
contract_extraction_path = Path(f"{self.output_dir}/{contract_file_name}.json")
with open(contract_extraction_path, "w") as fp:
fp.write(contract_extraction.model_dump_json())
ctx.write_event_to_stream(
LogEvent(
msg="Extracted successfully",
step=Step.PARSE_CONTRACT,
is_step_completed=True,
data={
"saved_path": str(contract_extraction_path),
"parsed_data": contract_extraction.model_dump_json(),
},
)
)
return ContractExtractionEvent(contract_extraction=contract_extraction)
@step
async def dispatch_guideline_match( # type: ignore
self, ctx: Context, ev: ContractExtractionEvent
) -> MatchGuidelineEvent:
"""For each clause in the contract, find relevant guidelines.
Use a map-reduce pattern, send each parsed clause as a MatchGuidelineEvent.
"""
await ctx.set("num_clauses", len(ev.contract_extraction.clauses))
await ctx.set("vendor_name", ev.contract_extraction.vendor_name)
for clause in ev.contract_extraction.clauses:
request_id = str(uuid.uuid4())
ctx.send_event(
MatchGuidelineEvent(
request_id=request_id,
clause=clause,
vendor_name=ev.contract_extraction.vendor_name or "Not identified",
)
)
ctx.write_event_to_stream(
LogEvent(
msg=f"Created {len(ev.contract_extraction.clauses)} tasks for analyzing with the guidelines",
step=Step.ANALYZE_CLAUSES,
)
)
@step
async def handle_guideline_match(
self, ctx: Context, ev: MatchGuidelineEvent
) -> MatchGuidelineResultEvent:
"""Handle matching clause against guideline."""
ctx.write_event_to_stream(
LogEvent(
msg=f"Handling clause for request {ev.request_id}",
step=Step.HANDLE_CLAUSE,
data={
"request_id": ev.request_id,
"clause_text": ev.clause.clause_text,
"is_compliant": None,
},
)
)
# retrieve matching guideline
query = f"""\
Find the relevant guideline from {ev.vendor_name} that aligns with the following contract clause:
{ev.clause.clause_text}
"""
guideline_docs = self.guideline_retriever.retrieve(query)
guideline_text = "\n\n".join([g.get_content() for g in guideline_docs])
# extract compliance from contract into a structured model
# see ClauseComplianceCheck model for the schema
prompt = ChatPromptTemplate.from_messages([("user", CONTRACT_MATCH_PROMPT)])
compliance_output = await self.llm.astructured_predict(
ClauseComplianceCheck,
prompt,
clause_text=ev.clause.model_dump_json(),
guideline_text=guideline_text,
)
if not isinstance(compliance_output, ClauseComplianceCheck):
raise ValueError(f"Invalid compliance check: {compliance_output}")
ctx.write_event_to_stream(
LogEvent(
msg=f"Completed compliance check for request {ev.request_id}",
step=Step.HANDLE_CLAUSE,
is_step_completed=True,
data={
"request_id": ev.request_id,
"clause_text": ev.clause.clause_text,
"is_compliant": compliance_output.compliant,
"result": compliance_output,
},
)
)
return MatchGuidelineResultEvent(result=compliance_output)
@step
async def gather_guideline_match(
self, ctx: Context, ev: MatchGuidelineResultEvent
) -> GenerateReportEvent | None:
"""Handle matching clause against guideline."""
num_clauses = await ctx.get("num_clauses")
events = ctx.collect_events(ev, [MatchGuidelineResultEvent] * num_clauses)
if events is None:
return None
match_results = [e.result for e in events]
# save match results
contract_file_name = await ctx.get("contract_file_name")
match_results_path = Path(
f"{self.output_dir}/match_results_{contract_file_name}.jsonl"
)
with open(match_results_path, "w") as fp:
for mr in match_results:
fp.write(mr.model_dump_json() + "\n")
ctx.write_event_to_stream(
LogEvent(
msg=f"Processed {len(match_results)} clauses",
step=Step.ANALYZE_CLAUSES,
is_step_completed=True,
data={"saved_path": str(match_results_path)},
)
)
return GenerateReportEvent(match_results=[e.result for e in events])
@step
async def generate_output(self, ctx: Context, ev: GenerateReportEvent) -> StopEvent:
ctx.write_event_to_stream(
LogEvent(
msg="Generating Compliance Report",
step=Step.GENERATE_REPORT,
data={"is_completed": False},
)
)
# if all clauses are compliant, return a compliant result
non_compliant_results = [r for r in ev.match_results if not r.compliant]
# generate compliance results string
result_tmpl = """
1. **Clause**: {clause}
2. **Guideline:** {guideline}
3. **Compliance Status:** {compliance_status}
4. **Notes:** {notes}
"""
non_compliant_strings = []
for nr in non_compliant_results:
non_compliant_strings.append(
result_tmpl.format(
clause=nr.clause_text,
guideline=nr.matched_guideline.guideline_text
if nr.matched_guideline is not None
else "No relevant guideline found",
compliance_status=nr.compliant,
notes=nr.notes,
)
)
non_compliant_str = "\n\n".join(non_compliant_strings)
prompt = ChatPromptTemplate.from_messages(
[
("system", COMPLIANCE_REPORT_SYSTEM_PROMPT),
("user", COMPLIANCE_REPORT_USER_PROMPT),
]
)
compliance_report = await self.llm.astructured_predict(
ComplianceReport,
prompt,
compliance_results=non_compliant_str,
vendor_name=await ctx.get("vendor_name"),
)
# Save compliance report to file
contract_file_name = await ctx.get("contract_file_name")
compliance_report_path = Path(
f"{self.output_dir}/report_{contract_file_name}.json"
)
with open(compliance_report_path, "w") as fp:
fp.write(compliance_report.model_dump_json())
ctx.write_event_to_stream(
LogEvent(
msg=f"Compliance report saved to {compliance_report_path}",
step=Step.GENERATE_REPORT,
is_step_completed=True,
data={
"saved_path": str(compliance_report_path),
"result": compliance_report,
},
)
)
return StopEvent(
result={
"report": compliance_report,
"non_compliant_results": non_compliant_results,
}
)
@@ -0,0 +1,9 @@
from .upload import upload_component
from .workflow import guideline_component, load_contract_component, report_component
__all__ = [
"upload_component",
"load_contract_component",
"guideline_component",
"report_component",
]
@@ -0,0 +1,5 @@
from .card import card_component
__all__ = [
"card_component",
]
@@ -0,0 +1,20 @@
import reflex as rx
def card_component(
title: str,
children: rx.Component,
show_loading: bool = False,
) -> rx.Component:
return rx.card(
rx.hstack(
rx.cond(show_loading, rx.spinner(size="2")),
rx.text(title, size="4"),
align_items="center",
gap="2",
),
rx.divider(orientation="horizontal"),
rx.container(children),
width="100%",
background_color="var(--gray-3)",
)
@@ -0,0 +1,30 @@
import reflex as rx
from app.ui.components.shared import card_component
from app.ui.states.app import AppState
def upload_component() -> rx.Component:
return card_component(
title="Upload",
children=rx.container(
rx.vstack(
rx.upload(
rx.vstack(
rx.text("Drag and drop files here or click to select files"),
),
on_drop=AppState.handle_upload(
rx.upload_files(upload_id="upload1")
),
id="upload1",
border="1px dotted rgb(107,99,246)",
padding="1rem",
),
rx.cond(
AppState.uploaded_file != None, # noqa: E711
rx.text(AppState.uploaded_file.file_name), # type: ignore
rx.text("No file uploaded"),
),
),
),
)
@@ -0,0 +1,9 @@
from .guideline import guideline_component
from .load import load_contract_component
from .report import report_component
__all__ = [
"guideline_component",
"load_contract_component",
"report_component",
]
@@ -0,0 +1,113 @@
from typing import List
import reflex as rx
from app.models import ClauseComplianceCheck
from app.ui.components.shared import card_component
from app.ui.states.workflow import GuidelineData, GuidelineHandlerState, GuidelineState
def guideline_handler_component(item: List) -> rx.Component:
_id: str = item[0]
status: GuidelineData = item[1]
return rx.hover_card.root(
rx.hover_card.trigger(
rx.card(
rx.stack(
rx.container(
rx.cond(
~status.is_completed,
rx.spinner(size="1"),
rx.cond(
status.is_compliant,
rx.icon(tag="check", color="green"),
rx.icon(tag="x", color="red"),
),
),
),
rx.flex(
rx.text(status.clause_text, size="1"),
),
),
),
),
rx.hover_card.content(
rx.cond(
status.is_completed,
guideline_result_component(status.result), # type: ignore
rx.spinner(size="1"),
),
side="right",
),
)
def guideline_result_component(result: ClauseComplianceCheck) -> rx.Component:
return rx.inset(
rx.table.root(
rx.table.header(
rx.table.row(
rx.table.cell("Clause"),
rx.table.cell("Guideline"),
),
),
rx.table.body(
rx.table.row(
# rx.table.cell("Clause"),
rx.table.cell(
rx.text(result.clause_text, size="1"),
), # type: ignore
rx.table.cell(
rx.text(result.matched_guideline.guideline_text, size="1"), # type: ignore
),
),
),
),
rx.container(
rx.cond(
result.compliant,
rx.text(
result.notes, # type: ignore
size="2",
color="green",
),
rx.text(
result.notes, # type: ignore
size="2",
color="red",
),
)
),
)
def guideline_component() -> rx.Component:
return rx.cond(
GuidelineState.is_started,
card_component(
title="Analyze the document with provided guidelines",
children=rx.vstack(
rx.vstack(
rx.foreach(
GuidelineState.log,
lambda log: rx.box(
rx.text(log["msg"]),
),
),
),
rx.cond(
GuidelineHandlerState.has_data(), # type: ignore
rx.grid(
rx.foreach(
GuidelineHandlerState.data,
guideline_handler_component,
),
columns="2",
spacing="1",
),
),
),
show_loading=GuidelineState.is_running,
),
)
@@ -0,0 +1,22 @@
import reflex as rx
from app.ui.components.shared import card_component
from app.ui.states.workflow import ContractLoaderState
def load_contract_component() -> rx.Component:
return rx.cond(
ContractLoaderState.is_started,
card_component(
title="Parse contract",
children=rx.vstack(
rx.foreach(
ContractLoaderState.log,
lambda log: rx.box(
rx.text(log["msg"]),
),
),
),
show_loading=ContractLoaderState.is_running,
),
)
@@ -0,0 +1,48 @@
import reflex as rx
from app.ui.components.shared import card_component
from app.ui.states.workflow import ReportState
def report_component() -> rx.Component:
return rx.cond(
ReportState.is_running,
card_component(
title="Conclusion",
show_loading=~ReportState.is_completed, # type: ignore
children=rx.cond(
ReportState.is_completed,
rx.vstack(
rx.box(
rx.inset(
rx.table.root(
rx.table.body(
rx.table.row(
rx.table.cell("Vendor"),
rx.table.cell(ReportState.result.vendor_name), # type: ignore
),
rx.table.row(
rx.table.cell("Overall Compliance"),
rx.table.cell(
rx.cond(
ReportState.result.overall_compliant,
rx.text("Compliant", color="green"),
rx.text("Non-compliant", color="red"),
)
),
),
rx.table.row(
rx.table.cell("Summary Notes"),
rx.table.cell(ReportState.result.summary_notes), # type: ignore
),
),
),
)
),
),
rx.vstack(
rx.text("Analyzing compliance results for final conclusion..."),
),
),
),
)
@@ -0,0 +1,46 @@
import reflex as rx
from app.ui.components import (
guideline_component,
load_contract_component,
report_component,
upload_component,
)
from app.ui.templates import template
@template(
route="/",
title="Structure extractor",
)
def index() -> rx.Component:
"""The main index page."""
return rx.vstack(
rx.vstack(
rx.heading("Built by LlamaIndex", size="6"),
rx.text(
"Upload a contract to start the review process.",
),
background_color="var(--gray-3)",
align_items="left",
justify_content="left",
width="100%",
padding="1rem",
),
rx.container(
rx.vstack(
# Upload
upload_component(),
# Workflow
rx.vstack(
load_contract_component(),
guideline_component(),
report_component(),
width="100%",
),
),
width="100%",
padding="1rem",
),
width="100%",
)
@@ -0,0 +1,15 @@
from .app import AppState
from .workflow import (
ContractLoaderState,
GuidelineHandlerState,
GuidelineState,
ReportState,
)
__all__ = [
"AppState",
"ContractLoaderState",
"GuidelineHandlerState",
"GuidelineState",
"ReportState",
]
@@ -0,0 +1,94 @@
import logging
import os
from typing import List, Optional
import reflex as rx
from app.config import UPLOADED_DIR
from app.services.contract_reviewer import LogEvent, Step, get_workflow
from app.ui.states.workflow import (
ContractLoaderState,
GuidelineHandlerState,
GuidelineState,
ReportState,
)
logger = logging.getLogger(__name__)
class UploadedFile(rx.Base):
file_name: str
size: int
class AppState(rx.State):
"""
Whole main state for the app.
Handle for file upload, trigger workflow and produce workflow events.
"""
uploaded_file: Optional[UploadedFile] = None
@rx.event
async def handle_upload(self, files: List[rx.UploadFile]):
if len(files) > 1:
yield rx.toast.error(
"You can only upload one file at a time", position="top-center"
)
return
try:
file = files[0]
upload_data = await file.read()
outfile = os.path.join(UPLOADED_DIR, file.filename)
with open(outfile, "wb") as f:
f.write(upload_data)
self.uploaded_file = UploadedFile(
file_name=file.filename, size=len(upload_data)
)
yield AppState.reset_workflow
yield AppState.trigger_workflow
except Exception as e:
yield rx.toast.error(str(e), position="top-center")
@rx.event
def reset_workflow(self):
yield ContractLoaderState.reset_state
yield GuidelineState.reset_state
yield GuidelineHandlerState.reset_state
yield ReportState.reset_state
@rx.event(background=True)
async def trigger_workflow(self):
"""
Trigger backend to start reviewing the contract in a loop.
Get the event from the loop and update the state.
"""
if self.uploaded_file is None:
yield rx.toast.error("No file uploaded", position="top-center")
else:
uploaded_file_path = os.path.join(
UPLOADED_DIR, self.uploaded_file.file_name
)
try:
workflow = get_workflow()
handler = workflow.run(
contract_path=uploaded_file_path,
)
async for event in handler.stream_events():
if isinstance(event, LogEvent):
match event.step:
case Step.PARSE_CONTRACT:
yield ContractLoaderState.add_log(event)
case Step.ANALYZE_CLAUSES:
yield GuidelineState.add_log(event)
case Step.HANDLE_CLAUSE:
yield GuidelineHandlerState.add_log(event)
case Step.GENERATE_REPORT:
yield ReportState.add_log(event)
# Wait for workflow completion and propagate any exceptions
_ = await handler
except Exception as e:
logger.error(f"Error in trigger_workflow: {e}")
yield rx.toast.error(str(e), position="top-center")
yield AppState.reset_workflow
@@ -0,0 +1,137 @@
from typing import Any, Dict, List, Optional
import reflex as rx
from pydantic import BaseModel
from app.models import ClauseComplianceCheck, ComplianceReport
from app.services.contract_reviewer import LogEvent
from rxconfig import config as rx_config
class ContractLoaderState(rx.State):
is_running: bool = False
is_started: bool = False
log: List[Dict[str, Any]] = []
@rx.event
async def add_log(self, log: LogEvent):
if not self.is_started:
yield ContractLoaderState.start
self.log.append(log.model_dump())
if log.is_step_completed:
yield ContractLoaderState.stop
def has_log(self):
return len(self.log) > 0
@rx.event
def start(self):
self.is_running = True
self.is_started = True
@rx.event
def stop(self):
self.is_running = False
@rx.event
def reset_state(self):
self.is_running = False
self.is_started = False
self.log = []
class GuidelineState(rx.State):
is_running: bool = False
is_started: bool = False
log: List[Dict[str, Any]] = []
@rx.event
def add_log(self, log: LogEvent):
if not self.is_started:
yield GuidelineState.start
self.log.append(log.model_dump())
if log.is_step_completed:
yield GuidelineState.stop
def has_log(self):
return len(self.log) > 0
@rx.event
def reset_state(self):
self.is_running = False
self.is_started = False
self.log = []
@rx.event
def start(self):
self.is_running = True
self.is_started = True
@rx.event
def stop(self):
self.is_running = False
class GuidelineData(BaseModel):
is_completed: bool
is_compliant: Optional[bool]
clause_text: Optional[str]
result: Optional[ClauseComplianceCheck] = None
class GuidelineHandlerState(rx.State):
data: Dict[str, GuidelineData] = {}
def has_data(self):
return len(self.data) > 0
@rx.event
def add_log(self, log: LogEvent):
_id = log.data.get("request_id")
if _id is None:
return
is_compliant = log.data.get("is_compliant", None)
self.data[_id] = GuidelineData(
is_completed=log.is_step_completed,
is_compliant=is_compliant,
clause_text=log.data.get("clause_text", None),
result=log.data.get("result", None),
)
if log.is_step_completed:
yield self.stop(request_id=_id)
@rx.event
def reset_state(self):
self.data = {}
@rx.event
def stop(self, request_id: str):
# Update the item in the data to be completed
self.data[request_id].is_completed = True
class ReportState(rx.State):
is_running: bool = False
is_completed: bool = False
saved_path: str = ""
result: Optional[ComplianceReport] = None
@rx.var()
def download_url(self) -> str:
return f"{rx_config.api_url}/api/download/{self.saved_path}"
@rx.event
def add_log(self, log: LogEvent):
if not self.is_running:
self.is_running = True
if log.is_step_completed:
self.is_completed = True
self.saved_path = log.data.get("saved_path")
self.result = log.data.get("result")
@rx.event
def reset_state(self):
self.is_running = False
self.is_completed = False
self.saved_path = ""
self.result = None
@@ -0,0 +1,142 @@
# ACME Vendor Agreement
**Effective Date:** January 1, 2024
## Parties:
- **Client:** LlamaCo ("Client")
- **Vendor:** ACME Office Supply, Inc. ("Vendor")
## 1. Overview
This Vendor Agreement ("Agreement") sets forth the terms and conditions under which ACME Office Supply, Inc. will provide office supplies, consumables, related goods ("Products"), and associated data processing services to LlamaCo.
## 2. Definitions
- **Personal Data:** Any information relating to an identified or identifiable natural person ('data subject').
- **Processing:** Any operation performed on Personal Data, including collection, storage, modification, transfer, or deletion.
- **Data Controller:** LlamaCo, who determines the purposes and means of processing Personal Data.
- **Data Processor:** ACME Office Supply, Inc., who processes Personal Data on behalf of the Controller.
## 3. Data Protection and Privacy
### 3.1 Scope of Processing
Vendor shall process Personal Data only:
- To fulfill orders and manage deliveries
- To provide customer support services
- To maintain business records
- To comply with legal obligations
### 3.2 Data Subject Rights
Vendor shall:
- Respond to data subject requests within 30 days
- Provide data in a structured, commonly used format
- Implement measures to facilitate data portability
- Assist with data subject rights requests at no additional cost
### 3.3 Data Transfers and Storage
- Vendor maintains primary data centers in the United States
- Vendor may transfer data to any country where it maintains operations
- No prior notification required for new data storage locations
- Vendor will rely on its standard data transfer mechanisms
- Data may be processed by staff operating outside the EEA
### 3.4 Subprocessors
- Vendor may engage subprocessors without prior Client approval
- Subprocessors may be located in any jurisdiction globally
- Notice of new subprocessors provided within 30 days of engagement
- Client has no right to object to new subprocessors
## 4. Security Measures
### 4.1 Technical and Organizational Measures
Vendor shall implement appropriate measures including:
- Encryption of Personal Data in transit and at rest
- Access controls and authentication
- Regular security testing and assessments
- Employee training on data protection
- Incident response procedures
### 4.2 Data Breaches
Vendor shall:
- Notify Client of any Personal Data breach within 72 hours
- Provide details necessary to meet regulatory requirements
- Cooperate with Client's breach investigation
- Maintain records of all data breaches
## 5. Data Retention
### 5.1 Retention Period
- Personal Data retained only as long as necessary
- Standard retention period of 3 years after last transaction
- Deletion of Personal Data upon written request
- Backup copies retained for maximum of 6 months
### 5.2 Termination
Upon termination of services:
- Return all Personal Data in standard format
- Delete existing copies within 30 days
- Provide written confirmation of deletion
- Cease all processing activities
## 6. Compliance and Audit
### 6.1 Documentation
Vendor shall maintain:
- Records of all processing activities
- Security measure documentation
- Data transfer mechanisms
- Subprocessor agreements
### 6.2 Audits
- Annual compliance audits permitted
- 30 days notice required for audits
- Vendor to provide necessary documentation
- Client bears reasonable audit costs
## 7. Liability and Indemnification
### 7.1 Liability
- Vendor liable for data protection violations
- Reasonable compensation for damages
- Coverage for regulatory fines where applicable
- Joint liability as required by law
## 8. Governing Law
This Agreement shall be governed by the laws of Ireland, without regard to its conflict of laws principles.
---
IN WITNESS WHEREOF, the parties have executed this Agreement as of the Effective Date.
**LlamaCo**
By: **_
Name: [Authorized Representative]
Title: [Title]
Date: _**
**ACME Office Supply, Inc.**
By: **_
Name: [Authorized Representative]
Title: [Title]
Date: _**
@@ -0,0 +1,44 @@
[tool]
[tool.poetry]
name = "app"
version = "0.1.0"
description = ""
authors = [ "Marcus Schiesser <mail@marcusschiesser.de>" ]
readme = "README.md"
[tool.poetry.scripts]
generate = "app.engine.generate:generate_datasource"
[tool.poetry.dependencies]
python = "^3.11,<4.0"
fastapi = "^0.109.1"
python-dotenv = "^1.0.0"
pydantic = "<2.10"
llama-index = "^0.12.1"
cachetools = "^5.3.3"
reflex = "^0.6.2.post1"
[tool.poetry.dependencies.uvicorn]
extras = [ "standard" ]
version = "^0.23.2"
[tool.poetry.dependencies.docx2txt]
version = "^0.8"
[tool.poetry.dependencies.llama-index-llms-openai]
version = "^0.3.2"
[tool.poetry.dependencies.llama-index-embeddings-openai]
version = "^0.3.1"
[tool.poetry.dependencies.llama-index-agent-openai]
version = "^0.4.0"
[tool.poetry.group.dev.dependencies]
pytest-asyncio = "^0.25.0"
pytest = "^8.3.4"
[build-system]
requires = [ "poetry-core" ]
build-backend = "poetry.core.masonry.api"
@@ -2,6 +2,7 @@ import os
from typing import List
import reflex as rx
from app.engine.generate import generate_datasource
@@ -78,10 +79,10 @@ def upload_component() -> rx.Component:
UploadedFilesState.uploaded_files,
lambda file: rx.card(
rx.stack(
rx.text(file.file_name, size="sm"),
rx.text(file.file_name, size="2"),
rx.button(
"x",
size="sm",
size="2",
on_click=UploadedFilesState.remove_file(file.file_name),
),
justify="between",
@@ -0,0 +1 @@
from .index import index as index
@@ -13,7 +13,8 @@ python = "^3.11,<4.0"
fastapi = "^0.109.1"
uvicorn = { extras = ["standard"], version = "^0.23.2" }
python-dotenv = "^1.0.0"
llama-index = "^0.11.1"
pydantic = "<2.10"
llama-index = "^0.12.1"
cachetools = "^5.3.3"
reflex = "^0.6.2.post1"
+25 -28
View File
@@ -17,11 +17,11 @@ import logging
import os
import uuid
from dataclasses import asdict
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, List, Optional
from app.engine.tools.artifact import CodeArtifact
from app.services.file import FileService
from e2b_code_interpreter import CodeInterpreter, Sandbox # type: ignore
from e2b_code_interpreter import Sandbox # type: ignore
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel
@@ -60,6 +60,14 @@ class FileUpload(BaseModel):
name: str
SUPPORTED_TEMPLATES = [
"nextjs-developer",
"vue-developer",
"streamlit-developer",
"gradio-developer",
]
@sandbox_router.post("")
async def create_sandbox(request: Request):
request_data = await request.json()
@@ -79,33 +87,22 @@ async def create_sandbox(request: Request):
status_code=400, detail="Could not create artifact from the request data"
)
sbx = None
# Create an interpreter or a sandbox
if artifact.template == "code-interpreter-multilang":
sbx = CodeInterpreter(api_key=os.getenv("E2B_API_KEY"), timeout=SANDBOX_TIMEOUT)
logger.debug(f"Created code interpreter {sbx}")
else:
sbx = Sandbox(
api_key=os.getenv("E2B_API_KEY"),
template=artifact.template,
metadata={"template": artifact.template, "user_id": "default"},
timeout=SANDBOX_TIMEOUT,
)
logger.debug(f"Created sandbox {sbx}")
sbx = Sandbox(
api_key=os.getenv("E2B_API_KEY"),
template=(
None if artifact.template not in SUPPORTED_TEMPLATES else artifact.template
),
metadata={"template": artifact.template, "user_id": "default"},
timeout=SANDBOX_TIMEOUT,
)
logger.debug(f"Created sandbox {sbx}")
# Install packages
if artifact.has_additional_dependencies:
if isinstance(sbx, CodeInterpreter):
sbx.notebook.exec_cell(artifact.install_dependencies_command)
logger.debug(
f"Installed dependencies: {', '.join(artifact.additional_dependencies)} in code interpreter {sbx}"
)
elif isinstance(sbx, Sandbox):
sbx.commands.run(artifact.install_dependencies_command)
logger.debug(
f"Installed dependencies: {', '.join(artifact.additional_dependencies)} in sandbox {sbx}"
)
sbx.commands.run(artifact.install_dependencies_command)
logger.debug(
f"Installed dependencies: {', '.join(artifact.additional_dependencies)} in sandbox {sbx}"
)
# Copy files
if len(sandbox_files) > 0:
@@ -122,7 +119,7 @@ async def create_sandbox(request: Request):
# Execute code or return a URL to the running sandbox
if artifact.template == "code-interpreter-multilang":
result = sbx.notebook.exec_cell(artifact.code or "")
result = sbx.run_code(artifact.code or "")
output_urls = _download_cell_results(result.results)
runtime_error = asdict(result.error) if result.error else None
return ExecutionResult(
@@ -145,7 +142,7 @@ async def create_sandbox(request: Request):
def _upload_files(
sandbox: Union[CodeInterpreter, Sandbox],
sandbox: Sandbox,
sandbox_files: List[str] = [],
) -> None:
for file_path in sandbox_files:
@@ -60,10 +60,12 @@ class NextQuestionSuggestion:
return None
@classmethod
def _extract_questions(cls, text: str) -> List[str]:
def _extract_questions(cls, text: str) -> List[str] | None:
content_match = re.search(r"```(.*?)```", text, re.DOTALL)
content = content_match.group(1) if content_match else ""
return content.strip().split("\n")
content = content_match.group(1) if content_match else None
if not content:
return None
return [q.strip() for q in content.split("\n") if q.strip()]
@classmethod
async def suggest_next_questions(
@@ -1,8 +1,17 @@
import os
from typing import Dict
from typing import Dict, Optional
from llama_index.core.multi_modal_llms import MultiModalLLM
from llama_index.core.settings import Settings
# `Settings` does not support setting `MultiModalLLM`
# so we use a global variable to store it
_multi_modal_llm: Optional[MultiModalLLM] = None
def get_multi_modal_llm():
return _multi_modal_llm
def init_settings():
model_provider = os.getenv("MODEL_PROVIDER")
@@ -60,14 +69,21 @@ def init_openai():
from llama_index.core.constants import DEFAULT_TEMPERATURE
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
from llama_index.multi_modal_llms.openai import OpenAIMultiModal
from llama_index.multi_modal_llms.openai.utils import GPT4V_MODELS
max_tokens = os.getenv("LLM_MAX_TOKENS")
model_name = os.getenv("MODEL", "gpt-4o-mini")
Settings.llm = OpenAI(
model=os.getenv("MODEL", "gpt-4o-mini"),
model=model_name,
temperature=float(os.getenv("LLM_TEMPERATURE", DEFAULT_TEMPERATURE)),
max_tokens=int(max_tokens) if max_tokens is not None else None,
)
if model_name in GPT4V_MODELS:
global _multi_modal_llm
_multi_modal_llm = OpenAIMultiModal(model=model_name)
dimensions = os.getenv("EMBEDDING_DIM")
Settings.embed_model = OpenAIEmbedding(
model=os.getenv("EMBEDDING_MODEL", "text-embedding-3-small"),
@@ -1,5 +1,4 @@
# flake8: noqa: E402
import os
from dotenv import load_dotenv
@@ -7,62 +6,24 @@ load_dotenv()
import logging
from app.engine.index import get_client, get_index
from llama_index.core.readers import SimpleDirectoryReader
from tqdm import tqdm
from app.engine.index import get_index
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 llama_index.core.settings import Settings
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
def ensure_index(index):
project_id = index._get_project_id()
client = get_client()
pipelines = client.pipelines.search_pipelines(
project_id=project_id,
pipeline_name=index.name,
pipeline_type=PipelineType.MANAGED.value,
)
if len(pipelines) == 0:
from llama_index.embeddings.openai import OpenAIEmbedding
if not isinstance(Settings.embed_model, OpenAIEmbedding):
raise ValueError(
"Creating a new pipeline with a non-OpenAI embedding model is not supported."
)
client.pipelines.upsert_pipeline(
project_id=project_id,
request={
"name": index.name,
"embedding_config": {
"type": "OPENAI_EMBEDDING",
"component": {
"api_key": os.getenv("OPENAI_API_KEY"), # editable
"model_name": os.getenv("EMBEDDING_MODEL"),
},
},
"transform_config": {
"mode": "auto",
"config": {
"chunk_size": Settings.chunk_size, # editable
"chunk_overlap": Settings.chunk_overlap, # editable
},
},
},
)
def generate_datasource():
init_settings()
logger.info("Generate index for the provided data")
index = get_index()
ensure_index(index)
project_id = index._get_project_id()
pipeline_id = index._get_pipeline_id()
index = get_index(create_if_missing=True)
if index is None:
raise ValueError("Index not found and could not be created")
# use SimpleDirectoryReader to retrieve the files to process
reader = SimpleDirectoryReader(
@@ -72,14 +33,30 @@ def generate_datasource():
files_to_process = reader.input_files
# add each file to the LlamaCloud pipeline
for input_file in files_to_process:
error_files = []
for input_file in tqdm(
files_to_process,
desc="Processing files",
unit="file",
):
with open(input_file, "rb") as f:
logger.info(
logger.debug(
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={}
)
try:
LLamaCloudFileService.add_file_to_pipeline(
index.project.id,
index.pipeline.id,
f,
custom_metadata={},
wait_for_processing=False,
)
except Exception as e:
error_files.append(input_file)
logger.error(f"Error adding file {input_file}: {e}")
if error_files:
logger.error(f"Failed to add the following files: {error_files}")
logger.info("Finished generating the index")
@@ -2,10 +2,12 @@ import logging
import os
from typing import Optional
from llama_cloud import PipelineType
from llama_index.core.callbacks import CallbackManager
from llama_index.core.ingestion.api_utils import (
get_client as llama_cloud_get_client,
)
from llama_index.core.settings import Settings
from llama_index.indices.managed.llama_cloud import LlamaCloudIndex
from pydantic import BaseModel, Field, field_validator
@@ -82,14 +84,63 @@ class IndexConfig(BaseModel):
}
def get_index(config: IndexConfig = None):
def get_index(
config: IndexConfig = None,
create_if_missing: bool = False,
):
if config is None:
config = IndexConfig()
index = LlamaCloudIndex(**config.to_index_kwargs())
return index
# Check whether the index exists
try:
index = LlamaCloudIndex(**config.to_index_kwargs())
return index
except ValueError:
logger.warning("Index not found")
if create_if_missing:
logger.info("Creating index")
_create_index(config)
return LlamaCloudIndex(**config.to_index_kwargs())
return None
def get_client():
config = LlamaCloudConfig()
return llama_cloud_get_client(**config.to_client_kwargs())
def _create_index(
config: IndexConfig,
):
client = get_client()
pipeline_name = config.llama_cloud_pipeline_config.pipeline
pipelines = client.pipelines.search_pipelines(
pipeline_name=pipeline_name,
pipeline_type=PipelineType.MANAGED.value,
)
if len(pipelines) == 0:
from llama_index.embeddings.openai import OpenAIEmbedding
if not isinstance(Settings.embed_model, OpenAIEmbedding):
raise ValueError(
"Creating a new pipeline with a non-OpenAI embedding model is not supported."
)
client.pipelines.upsert_pipeline(
request={
"name": pipeline_name,
"embedding_config": {
"type": "OPENAI_EMBEDDING",
"component": {
"api_key": os.getenv("OPENAI_API_KEY"), # editable
"model_name": os.getenv("EMBEDDING_MODEL"),
},
},
"transform_config": {
"mode": "auto",
"config": {
"chunk_size": Settings.chunk_size, # editable
"chunk_overlap": Settings.chunk_overlap, # editable
},
},
},
)
@@ -1,18 +1,18 @@
from io import BytesIO
import logging
import os
import time
from typing import Any, Dict, List, Optional, Set, Tuple, Union
import typing
from io import BytesIO
from typing import Any, Dict, List, Optional, Set, Tuple, Union
import requests
from fastapi import BackgroundTasks
from llama_cloud import ManagedIngestionStatus, PipelineFileCreateCustomMetadataValue
from llama_index.core.schema import NodeWithScore
from pydantic import BaseModel
import requests
from app.api.routers.models import SourceNodes
from app.engine.index import get_client
from llama_index.core.schema import NodeWithScore
logger = logging.getLogger("uvicorn")
@@ -64,27 +64,34 @@ class LLamaCloudFileService:
pipeline_id: str,
upload_file: Union[typing.IO, Tuple[str, BytesIO]],
custom_metadata: Optional[Dict[str, PipelineFileCreateCustomMetadataValue]],
wait_for_processing: bool = True,
) -> str:
client = get_client()
file = client.files.upload_file(project_id=project_id, upload_file=upload_file)
file_id = file.id
files = [
{
"file_id": file.id,
"custom_metadata": {"file_id": file.id, **(custom_metadata or {})},
"file_id": file_id,
"custom_metadata": {"file_id": file_id, **(custom_metadata or {})},
}
]
files = client.pipelines.add_files_to_pipeline(pipeline_id, request=files)
if not wait_for_processing:
return file_id
# Wait 2s for the file to be processed
max_attempts = 20
attempt = 0
while attempt < max_attempts:
result = client.pipelines.get_pipeline_file_status(pipeline_id, file.id)
result = client.pipelines.get_pipeline_file_status(
file_id=file_id, pipeline_id=pipeline_id
)
if result.status == ManagedIngestionStatus.ERROR:
raise Exception(f"File processing failed: {str(result)}")
if result.status == ManagedIngestionStatus.SUCCESS:
# File is ingested - return the file id
return file.id
return file_id
attempt += 1
time.sleep(0.1) # Sleep for 100ms
raise Exception(

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