mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-19 23:13:36 -04:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ffd3c915b | |||
| 57e7638083 | |||
| 22ac2cae61 | |||
| 8077195601 | |||
| 8ce4a8513d | |||
| 1d93775f04 | |||
| 3fb93c7939 | |||
| e248dc56bc | |||
| bd5e39a390 |
@@ -20,6 +20,18 @@ jobs:
|
||||
os: [macos-latest, windows-latest, ubuntu-22.04]
|
||||
frameworks: ["nextjs", "express", "fastapi"]
|
||||
datasources: ["--no-files", "--example-file"]
|
||||
templates: ["streaming", "extractor"]
|
||||
exclude:
|
||||
# The extractor template currently only works with FastAPI and files,
|
||||
# and it's not compatible with Windows at the moment.
|
||||
- templates: "extractor"
|
||||
os: windows-latest
|
||||
- templates: "extractor"
|
||||
frameworks: "nextjs"
|
||||
- templates: "extractor"
|
||||
frameworks: "express"
|
||||
- templates: "extractor"
|
||||
datasources: "--no-files"
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -65,6 +77,7 @@ jobs:
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
|
||||
TEMPLATE: ${{ matrix.templates }}
|
||||
FRAMEWORK: ${{ matrix.frameworks }}
|
||||
DATASOURCE: ${{ matrix.datasources }}
|
||||
working-directory: .
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
# create-llama
|
||||
|
||||
## 0.1.41
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 57e7638: Use the retrieval defaults from LlamaCloud
|
||||
|
||||
## 0.1.40
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8ce4a85: Add UI for extractor template
|
||||
|
||||
## 0.1.39
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3fb93c7: Use LlamaCloud pipeline for data ingestion in TS (private file uploads and generate script)
|
||||
|
||||
## 0.1.38
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- bd5e39a: Fix error that files in sub folders of 'data' are not displayed
|
||||
|
||||
## 0.1.37
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { ChildProcess } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import type {
|
||||
TemplateFramework,
|
||||
TemplatePostInstallAction,
|
||||
TemplateType,
|
||||
TemplateUI,
|
||||
} from "../helpers";
|
||||
import { createTestDir, runCreateLlama, type AppType } from "./utils";
|
||||
|
||||
const templateType: TemplateType = "streaming";
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
: "fastapi";
|
||||
const dataSource: string = process.env.DATASOURCE
|
||||
? process.env.DATASOURCE
|
||||
: "--example-file";
|
||||
const templateUI: TemplateUI = "shadcn";
|
||||
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
|
||||
|
||||
const llamaCloudProjectName = "create-llama";
|
||||
const llamaCloudIndexName = "e2e-test";
|
||||
|
||||
const appType: AppType = templateFramework === "nextjs" ? "" : "--frontend";
|
||||
const userMessage =
|
||||
dataSource !== "--no-files" ? "Physical standard for letters" : "Hello";
|
||||
test.describe(`try create-llama ${templateType} ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
|
||||
let port: number;
|
||||
let externalPort: number;
|
||||
let cwd: string;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
// Only test without using vector db for now
|
||||
const vectorDb = "none";
|
||||
|
||||
test.beforeAll(async () => {
|
||||
port = Math.floor(Math.random() * 10000) + 10000;
|
||||
externalPort = port + 1;
|
||||
cwd = await createTestDir();
|
||||
const result = await runCreateLlama(
|
||||
cwd,
|
||||
templateType,
|
||||
templateFramework,
|
||||
dataSource,
|
||||
templateUI,
|
||||
vectorDb,
|
||||
appType,
|
||||
port,
|
||||
externalPort,
|
||||
templatePostInstallAction,
|
||||
llamaCloudProjectName,
|
||||
llamaCloudIndexName,
|
||||
);
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
});
|
||||
|
||||
test("App folder should exist", async () => {
|
||||
const dirExists = fs.existsSync(path.join(cwd, name));
|
||||
expect(dirExists).toBeTruthy();
|
||||
});
|
||||
test("Frontend should have a title", async ({ page }) => {
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Frontend should be able to submit a message and receive a response", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await page.fill("form input", userMessage);
|
||||
const [response] = await Promise.all([
|
||||
page.waitForResponse(
|
||||
(res) => {
|
||||
return res.url().includes("/api/chat") && res.status() === 200;
|
||||
},
|
||||
{
|
||||
timeout: 1000 * 60,
|
||||
},
|
||||
),
|
||||
page.click("form button[type=submit]"),
|
||||
]);
|
||||
const text = await response.text();
|
||||
console.log("AI response when submitting message: ", text);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Backend frameworks should response when calling non-streaming chat API", async ({
|
||||
request,
|
||||
}) => {
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
test.skip(templateFramework === "nextjs");
|
||||
const response = await request.post(
|
||||
`http://localhost:${externalPort}/api/chat/request`,
|
||||
{
|
||||
data: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: userMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
const text = await response.text();
|
||||
console.log("AI response when calling API: ", text);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
// clean processes
|
||||
test.afterAll(async () => {
|
||||
appProcess?.kill();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/* 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 { createTestDir, runCreateLlama } from "./utils";
|
||||
|
||||
if (process.env.TEMPLATE === "extractor") {
|
||||
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,
|
||||
"extractor",
|
||||
"fastapi",
|
||||
"--example-file",
|
||||
"none",
|
||||
frontendPort,
|
||||
backendPort,
|
||||
"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,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { ChildProcess } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import type {
|
||||
TemplateFramework,
|
||||
TemplatePostInstallAction,
|
||||
TemplateType,
|
||||
TemplateUI,
|
||||
} from "../helpers";
|
||||
import { createTestDir, runCreateLlama, type AppType } from "./utils";
|
||||
|
||||
const templateType: TemplateType | undefined = process.env.TEMPLATE
|
||||
? (process.env.TEMPLATE as TemplateType)
|
||||
: undefined;
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
: "fastapi";
|
||||
const dataSource: string = process.env.DATASOURCE
|
||||
? process.env.DATASOURCE
|
||||
: "--example-file";
|
||||
const templateUI: TemplateUI = "shadcn";
|
||||
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
|
||||
|
||||
const llamaCloudProjectName = "create-llama";
|
||||
const llamaCloudIndexName = "e2e-test";
|
||||
|
||||
const appType: AppType = templateFramework === "nextjs" ? "" : "--frontend";
|
||||
const userMessage =
|
||||
dataSource !== "--no-files" ? "Physical standard for letters" : "Hello";
|
||||
|
||||
if (templateType === "streaming") {
|
||||
test.describe(`Test streaming template ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
|
||||
let port: number;
|
||||
let externalPort: number;
|
||||
let cwd: string;
|
||||
let name: string;
|
||||
let appProcess: ChildProcess;
|
||||
// Only test without using vector db for now
|
||||
const vectorDb = "none";
|
||||
|
||||
test.beforeAll(async () => {
|
||||
port = Math.floor(Math.random() * 10000) + 10000;
|
||||
externalPort = port + 1;
|
||||
cwd = await createTestDir();
|
||||
const result = await runCreateLlama(
|
||||
cwd,
|
||||
templateType,
|
||||
templateFramework,
|
||||
dataSource,
|
||||
vectorDb,
|
||||
port,
|
||||
externalPort,
|
||||
templatePostInstallAction,
|
||||
templateUI,
|
||||
appType,
|
||||
llamaCloudProjectName,
|
||||
llamaCloudIndexName,
|
||||
);
|
||||
name = result.projectName;
|
||||
appProcess = result.appProcess;
|
||||
});
|
||||
|
||||
test("App folder should exist", async () => {
|
||||
const dirExists = fs.existsSync(path.join(cwd, name));
|
||||
expect(dirExists).toBeTruthy();
|
||||
});
|
||||
test("Frontend should have a title", async ({ page }) => {
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Frontend should be able to submit a message and receive a response", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
await page.goto(`http://localhost:${port}`);
|
||||
await page.fill("form input", userMessage);
|
||||
const [response] = await Promise.all([
|
||||
page.waitForResponse(
|
||||
(res) => {
|
||||
return res.url().includes("/api/chat") && res.status() === 200;
|
||||
},
|
||||
{
|
||||
timeout: 1000 * 60,
|
||||
},
|
||||
),
|
||||
page.click("form button[type=submit]"),
|
||||
]);
|
||||
const text = await response.text();
|
||||
console.log("AI response when submitting message: ", text);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Backend frameworks should response when calling non-streaming chat API", async ({
|
||||
request,
|
||||
}) => {
|
||||
test.skip(templatePostInstallAction !== "runApp");
|
||||
test.skip(templateFramework === "nextjs");
|
||||
const response = await request.post(
|
||||
`http://localhost:${externalPort}/api/chat/request`,
|
||||
{
|
||||
data: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: userMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
const text = await response.text();
|
||||
console.log("AI response when calling API: ", text);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
// clean processes
|
||||
test.afterAll(async () => {
|
||||
appProcess?.kill();
|
||||
});
|
||||
});
|
||||
}
|
||||
+15
-9
@@ -24,14 +24,14 @@ export async function runCreateLlama(
|
||||
templateType: TemplateType,
|
||||
templateFramework: TemplateFramework,
|
||||
dataSource: string,
|
||||
templateUI: TemplateUI,
|
||||
vectorDb: TemplateVectorDB,
|
||||
appType: AppType,
|
||||
port: number,
|
||||
externalPort: number,
|
||||
postInstallAction: TemplatePostInstallAction,
|
||||
llamaCloudProjectName: string,
|
||||
llamaCloudIndexName: string,
|
||||
templateUI?: TemplateUI,
|
||||
appType?: AppType,
|
||||
llamaCloudProjectName?: string,
|
||||
llamaCloudIndexName?: string,
|
||||
): Promise<CreateLlamaResult> {
|
||||
if (!process.env.OPENAI_API_KEY || !process.env.LLAMA_CLOUD_API_KEY) {
|
||||
throw new Error(
|
||||
@@ -45,7 +45,7 @@ export async function runCreateLlama(
|
||||
templateUI,
|
||||
appType,
|
||||
].join("-");
|
||||
const command = [
|
||||
const commandArgs = [
|
||||
"create-llama",
|
||||
name,
|
||||
"--template",
|
||||
@@ -53,13 +53,10 @@ export async function runCreateLlama(
|
||||
"--framework",
|
||||
templateFramework,
|
||||
dataSource,
|
||||
"--ui",
|
||||
templateUI,
|
||||
"--vector-db",
|
||||
vectorDb,
|
||||
"--open-ai-key",
|
||||
process.env.OPENAI_API_KEY,
|
||||
appType,
|
||||
"--use-pnpm",
|
||||
"--port",
|
||||
port,
|
||||
@@ -74,7 +71,16 @@ export async function runCreateLlama(
|
||||
"none",
|
||||
"--llama-cloud-key",
|
||||
process.env.LLAMA_CLOUD_API_KEY,
|
||||
].join(" ");
|
||||
];
|
||||
|
||||
if (templateUI) {
|
||||
commandArgs.push("--ui", templateUI);
|
||||
}
|
||||
if (appType) {
|
||||
commandArgs.push(appType);
|
||||
}
|
||||
|
||||
const command = commandArgs.join(" ");
|
||||
console.log(`running command '${command}' in ${cwd}`);
|
||||
const appProcess = exec(command, {
|
||||
cwd,
|
||||
|
||||
@@ -160,7 +160,7 @@ const getVectorDBEnvs = (
|
||||
{
|
||||
name: "LLAMA_CLOUD_ORGANIZATION_ID",
|
||||
description:
|
||||
"The organization ID for the LlamaCloud project (uses default organization if not specified - Python only)",
|
||||
"The organization ID for the LlamaCloud project (uses default organization if not specified)",
|
||||
},
|
||||
...(framework === "nextjs"
|
||||
? // activate index selector per default (not needed for non-NextJS backends as it's handled by createFrontendEnvFile)
|
||||
@@ -396,7 +396,6 @@ const getEngineEnvs = (): EnvVar[] => {
|
||||
name: "TOP_K",
|
||||
description:
|
||||
"The number of similar embeddings to return when retrieving documents.",
|
||||
value: "3",
|
||||
},
|
||||
{
|
||||
name: "STREAM_TIMEOUT",
|
||||
|
||||
+59
-48
@@ -23,66 +23,77 @@ const createProcess = (
|
||||
});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
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());
|
||||
}
|
||||
return createProcess("poetry", commandArgs, {
|
||||
stdio: "inherit",
|
||||
cwd: appPath,
|
||||
});
|
||||
}
|
||||
|
||||
export function runFastAPIApp(appPath: string, port: number) {
|
||||
const commandArgs = ["run", "uvicorn", "main:app", "--port=" + port];
|
||||
|
||||
return createProcess("poetry", commandArgs, {
|
||||
stdio: "inherit",
|
||||
cwd: appPath,
|
||||
});
|
||||
}
|
||||
|
||||
export function runTSApp(appPath: string, port: number) {
|
||||
return createProcess("npm", ["run", "dev"], {
|
||||
stdio: "inherit",
|
||||
cwd: appPath,
|
||||
env: { ...process.env, PORT: `${port}` },
|
||||
});
|
||||
}
|
||||
|
||||
export async function runApp(
|
||||
appPath: string,
|
||||
template: string,
|
||||
frontend: boolean,
|
||||
framework: TemplateFramework,
|
||||
port?: number,
|
||||
externalPort?: number,
|
||||
): Promise<any> {
|
||||
let backendAppProcess: ChildProcess;
|
||||
let frontendAppProcess: ChildProcess | undefined;
|
||||
const frontendPort = port || 3000;
|
||||
let backendPort = externalPort || 8000;
|
||||
const processes: ChildProcess[] = [];
|
||||
|
||||
// Callback to kill app processes
|
||||
// Callback to kill all sub processes if the main process is killed
|
||||
process.on("exit", () => {
|
||||
console.log("Killing app processes...");
|
||||
backendAppProcess.kill();
|
||||
frontendAppProcess?.kill();
|
||||
processes.forEach((p) => p.kill());
|
||||
});
|
||||
|
||||
let backendCommand = "";
|
||||
let backendArgs: string[];
|
||||
if (framework === "fastapi") {
|
||||
backendCommand = "poetry";
|
||||
backendArgs = [
|
||||
"run",
|
||||
"uvicorn",
|
||||
"main:app",
|
||||
"--host=0.0.0.0",
|
||||
"--port=" + backendPort,
|
||||
];
|
||||
} else if (framework === "nextjs") {
|
||||
backendCommand = "npm";
|
||||
backendArgs = ["run", "dev"];
|
||||
backendPort = frontendPort;
|
||||
} else {
|
||||
backendCommand = "npm";
|
||||
backendArgs = ["run", "dev"];
|
||||
// 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));
|
||||
}
|
||||
if (template === "streaming") {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
if (frontend) {
|
||||
return new Promise((resolve, reject) => {
|
||||
backendAppProcess = createProcess(backendCommand, backendArgs, {
|
||||
stdio: "inherit",
|
||||
cwd: path.join(appPath, "backend"),
|
||||
env: { ...process.env, PORT: `${backendPort}` },
|
||||
});
|
||||
frontendAppProcess = createProcess("npm", ["run", "dev"], {
|
||||
stdio: "inherit",
|
||||
cwd: path.join(appPath, "frontend"),
|
||||
env: { ...process.env, PORT: `${frontendPort}` },
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
backendAppProcess = createProcess(backendCommand, backendArgs, {
|
||||
stdio: "inherit",
|
||||
cwd: path.join(appPath),
|
||||
env: { ...process.env, PORT: `${backendPort}` },
|
||||
});
|
||||
});
|
||||
}
|
||||
return Promise.all(processes);
|
||||
}
|
||||
|
||||
@@ -188,7 +188,10 @@ if (process.argv.includes("--tools")) {
|
||||
program.tools = getTools(program.tools.split(","));
|
||||
}
|
||||
}
|
||||
if (process.argv.includes("--no-llama-parse")) {
|
||||
if (
|
||||
process.argv.includes("--no-llama-parse") ||
|
||||
program.template === "extractor"
|
||||
) {
|
||||
program.useLlamaParse = false;
|
||||
}
|
||||
program.askModels = process.argv.includes("--ask-models");
|
||||
@@ -341,6 +344,7 @@ Please check ${cyan(
|
||||
console.log(`Running app in ${root}...`);
|
||||
await runApp(
|
||||
root,
|
||||
program.template,
|
||||
program.frontend,
|
||||
program.framework,
|
||||
program.port,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.1.37",
|
||||
"version": "0.1.41",
|
||||
"description": "Create LlamaIndex-powered apps with one command",
|
||||
"keywords": [
|
||||
"rag",
|
||||
|
||||
+13
-4
@@ -172,7 +172,7 @@ export const getDataSourceChoices = (
|
||||
);
|
||||
}
|
||||
|
||||
if (framework === "fastapi") {
|
||||
if (framework === "fastapi" && template !== "extractor") {
|
||||
choices.push({
|
||||
title: "Use website content (requires Chrome)",
|
||||
value: "web",
|
||||
@@ -183,7 +183,7 @@ export const getDataSourceChoices = (
|
||||
});
|
||||
}
|
||||
|
||||
if (!selectedDataSource.length) {
|
||||
if (!selectedDataSource.length && template !== "extractor") {
|
||||
choices.push({
|
||||
title: "Use managed index from LlamaCloud",
|
||||
value: "llamacloud",
|
||||
@@ -407,9 +407,14 @@ export const askQuestions = async (
|
||||
return; // early return - no further questions needed for llamapack projects
|
||||
}
|
||||
|
||||
if (program.template === "multiagent" || program.template === "extractor") {
|
||||
if (program.template === "multiagent") {
|
||||
// TODO: multi-agents currently only supports FastAPI
|
||||
program.framework = preferences.framework = "fastapi";
|
||||
} else if (program.template === "extractor") {
|
||||
// Extractor template only supports FastAPI, empty data sources, and llamacloud
|
||||
// So we just use example file for extractor template, this allows user to choose vector database later
|
||||
program.dataSources = [EXAMPLE_FILE];
|
||||
program.framework = preferences.framework = "fastapi";
|
||||
}
|
||||
if (!program.framework) {
|
||||
if (ciInfo.isCI) {
|
||||
@@ -652,7 +657,11 @@ export const askQuestions = async (
|
||||
// default to use LlamaParse if using LlamaCloud
|
||||
program.useLlamaParse = preferences.useLlamaParse = true;
|
||||
} else {
|
||||
if (program.useLlamaParse === undefined) {
|
||||
// Extractor template doesn't support LlamaParse and LlamaCloud right now (cannot use asyncio loop in Reflex)
|
||||
if (
|
||||
program.useLlamaParse === undefined &&
|
||||
program.template !== "extractor"
|
||||
) {
|
||||
// if already set useLlamaParse, don't ask again
|
||||
if (program.dataSources.some((ds) => ds.type === "file")) {
|
||||
if (ciInfo.isCI) {
|
||||
|
||||
+7
-6
@@ -1,21 +1,22 @@
|
||||
import os
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.core.agent import AgentRunner
|
||||
from llama_index.core.tools.query_engine import QueryEngineTool
|
||||
from app.engine.tools import ToolFactory
|
||||
|
||||
from app.engine.index import get_index
|
||||
from app.engine.tools import ToolFactory
|
||||
from llama_index.core.agent import AgentRunner
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.core.tools.query_engine import QueryEngineTool
|
||||
|
||||
|
||||
def get_chat_engine(filters=None, params=None):
|
||||
system_prompt = os.getenv("SYSTEM_PROMPT")
|
||||
top_k = os.getenv("TOP_K", "3")
|
||||
top_k = int(os.getenv("TOP_K", 0))
|
||||
tools = []
|
||||
|
||||
# Add query tool if index exists
|
||||
index = get_index()
|
||||
if index is not None:
|
||||
query_engine = index.as_query_engine(
|
||||
similarity_top_k=int(top_k), filters=filters
|
||||
filters=filters, **({"similarity_top_k": top_k} if top_k != 0 else {})
|
||||
)
|
||||
query_engine_tool = QueryEngineTool.from_defaults(query_engine=query_engine)
|
||||
tools.append(query_engine_tool)
|
||||
+2
-3
@@ -9,7 +9,7 @@ from llama_index.core.chat_engine import CondensePlusContextChatEngine
|
||||
def get_chat_engine(filters=None, params=None):
|
||||
system_prompt = os.getenv("SYSTEM_PROMPT")
|
||||
citation_prompt = os.getenv("SYSTEM_CITATION_PROMPT", None)
|
||||
top_k = int(os.getenv("TOP_K", 3))
|
||||
top_k = int(os.getenv("TOP_K", 0))
|
||||
|
||||
node_postprocessors = []
|
||||
if citation_prompt:
|
||||
@@ -26,8 +26,7 @@ def get_chat_engine(filters=None, params=None):
|
||||
)
|
||||
|
||||
retriever = index.as_retriever(
|
||||
similarity_top_k=top_k,
|
||||
filters=filters,
|
||||
filters=filters, **({"similarity_top_k": top_k} if top_k != 0 else {})
|
||||
)
|
||||
|
||||
return CondensePlusContextChatEngine.from_defaults(
|
||||
@@ -10,7 +10,7 @@ export async function createChatEngine(documentIds?: string[], params?: any) {
|
||||
);
|
||||
}
|
||||
const retriever = index.asRetriever({
|
||||
similarityTopK: process.env.TOP_K ? parseInt(process.env.TOP_K) : 3,
|
||||
similarityTopK: process.env.TOP_K ? parseInt(process.env.TOP_K) : undefined,
|
||||
filters: generateFilters(documentIds || []),
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,20 @@ const MIME_TYPE_TO_EXT: Record<string, string> = {
|
||||
|
||||
const UPLOADED_FOLDER = "output/uploaded";
|
||||
|
||||
export async function loadDocuments(fileBuffer: Buffer, mimeType: string) {
|
||||
export async function storeAndParseFile(fileBuffer: Buffer, mimeType: string) {
|
||||
const documents = await loadDocuments(fileBuffer, mimeType);
|
||||
const { filename } = await saveDocument(fileBuffer, mimeType);
|
||||
for (const document of documents) {
|
||||
document.metadata = {
|
||||
...document.metadata,
|
||||
file_name: filename,
|
||||
private: "true", // to separate private uploads from public documents
|
||||
};
|
||||
}
|
||||
return documents;
|
||||
}
|
||||
|
||||
async function loadDocuments(fileBuffer: Buffer, mimeType: string) {
|
||||
const extractors = getExtractors();
|
||||
const reader = extractors[MIME_TYPE_TO_EXT[mimeType]];
|
||||
|
||||
@@ -22,7 +35,7 @@ export async function loadDocuments(fileBuffer: Buffer, mimeType: string) {
|
||||
return await reader.loadDataAsContent(fileBuffer);
|
||||
}
|
||||
|
||||
export async function saveDocument(fileBuffer: Buffer, mimeType: string) {
|
||||
async function saveDocument(fileBuffer: Buffer, mimeType: string) {
|
||||
const fileExt = MIME_TYPE_TO_EXT[mimeType];
|
||||
if (!fileExt) throw new Error(`Unsupported document type: ${mimeType}`);
|
||||
|
||||
|
||||
@@ -5,34 +5,24 @@ import {
|
||||
SimpleNodeParser,
|
||||
VectorStoreIndex,
|
||||
} from "llamaindex";
|
||||
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
|
||||
|
||||
export async function runPipeline(
|
||||
currentIndex: VectorStoreIndex | LlamaCloudIndex,
|
||||
currentIndex: VectorStoreIndex,
|
||||
documents: Document[],
|
||||
) {
|
||||
if (currentIndex instanceof LlamaCloudIndex) {
|
||||
// LlamaCloudIndex processes the documents automatically
|
||||
// so we don't need ingestion pipeline, just insert the documents directly
|
||||
for (const document of documents) {
|
||||
await currentIndex.insert(document);
|
||||
}
|
||||
} else {
|
||||
// Use ingestion pipeline to process the documents into nodes and add them to the vector store
|
||||
const pipeline = new IngestionPipeline({
|
||||
transformations: [
|
||||
new SimpleNodeParser({
|
||||
chunkSize: Settings.chunkSize,
|
||||
chunkOverlap: Settings.chunkOverlap,
|
||||
}),
|
||||
Settings.embedModel,
|
||||
],
|
||||
});
|
||||
const nodes = await pipeline.run({ documents });
|
||||
await currentIndex.insertNodes(nodes);
|
||||
currentIndex.storageContext.docStore.persist();
|
||||
console.log("Added nodes to the vector store.");
|
||||
}
|
||||
|
||||
// Use ingestion pipeline to process the documents into nodes and add them to the vector store
|
||||
const pipeline = new IngestionPipeline({
|
||||
transformations: [
|
||||
new SimpleNodeParser({
|
||||
chunkSize: Settings.chunkSize,
|
||||
chunkOverlap: Settings.chunkOverlap,
|
||||
}),
|
||||
Settings.embedModel,
|
||||
],
|
||||
});
|
||||
const nodes = await pipeline.run({ documents });
|
||||
await currentIndex.insertNodes(nodes);
|
||||
currentIndex.storageContext.docStore.persist();
|
||||
console.log("Added nodes to the vector store.");
|
||||
return documents.map((document) => document.id_);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
import { VectorStoreIndex } from "llamaindex";
|
||||
import { LLamaCloudFileService, VectorStoreIndex } from "llamaindex";
|
||||
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
|
||||
import { loadDocuments, saveDocument } from "./helper";
|
||||
import { storeAndParseFile } from "./helper";
|
||||
import { runPipeline } from "./pipeline";
|
||||
|
||||
export async function uploadDocument(
|
||||
index: VectorStoreIndex | LlamaCloudIndex,
|
||||
filename: string,
|
||||
raw: string,
|
||||
): Promise<string[]> {
|
||||
const [header, content] = raw.split(",");
|
||||
const mimeType = header.replace("data:", "").replace(";base64", "");
|
||||
const fileBuffer = Buffer.from(content, "base64");
|
||||
const documents = await loadDocuments(fileBuffer, mimeType);
|
||||
const { filename } = await saveDocument(fileBuffer, mimeType);
|
||||
|
||||
// Update documents with metadata
|
||||
for (const document of documents) {
|
||||
document.metadata = {
|
||||
...document.metadata,
|
||||
file_name: filename,
|
||||
private: "true", // to separate private uploads from public documents
|
||||
};
|
||||
if (index instanceof LlamaCloudIndex) {
|
||||
// trigger LlamaCloudIndex API to upload the file and run the pipeline
|
||||
const projectId = await index.getProjectId();
|
||||
const pipelineId = await index.getPipelineId();
|
||||
return [
|
||||
await LLamaCloudFileService.addFileToPipeline(
|
||||
projectId,
|
||||
pipelineId,
|
||||
new File([fileBuffer], filename, { type: mimeType }),
|
||||
{ private: "true" },
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return await runPipeline(index, documents);
|
||||
// run the pipeline for other vector store indexes
|
||||
const documents = await storeAndParseFile(fileBuffer, mimeType);
|
||||
return runPipeline(index, documents);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { StreamData } from "ai";
|
||||
import {
|
||||
CallbackManager,
|
||||
LLamaCloudFileService,
|
||||
Metadata,
|
||||
MetadataMode,
|
||||
NodeWithScore,
|
||||
ToolCall,
|
||||
ToolOutput,
|
||||
} from "llamaindex";
|
||||
import { LLamaCloudFileService } from "./service";
|
||||
import path from "node:path";
|
||||
import { DATA_DIR } from "../../engine/loader";
|
||||
import { downloadFile } from "./file";
|
||||
|
||||
const LLAMA_CLOUD_DOWNLOAD_FOLDER = "output/llamacloud";
|
||||
|
||||
export function appendSourceData(
|
||||
data: StreamData,
|
||||
@@ -84,7 +89,7 @@ export function createCallbackManager(stream: StreamData) {
|
||||
stream,
|
||||
`Retrieved ${nodes.length} sources to use as context for the query`,
|
||||
);
|
||||
LLamaCloudFileService.downloadFiles(nodes); // don't await to avoid blocking chat streaming
|
||||
downloadFilesFromNodes(nodes); // don't await to avoid blocking chat streaming
|
||||
});
|
||||
|
||||
callbackManager.on("llm-tool-call", (event) => {
|
||||
@@ -116,15 +121,71 @@ function getNodeUrl(metadata: Metadata) {
|
||||
if (fileName && process.env.FILESERVER_URL_PREFIX) {
|
||||
// file_name exists and file server is configured
|
||||
const pipelineId = metadata["pipeline_id"];
|
||||
if (pipelineId && metadata["private"] == null) {
|
||||
// file is from LlamaCloud and was not ingested locally
|
||||
const name = LLamaCloudFileService.toDownloadedName(pipelineId, fileName);
|
||||
return `${process.env.FILESERVER_URL_PREFIX}/output/llamacloud/${name}`;
|
||||
if (pipelineId) {
|
||||
const name = toDownloadedName(pipelineId, fileName);
|
||||
return `${process.env.FILESERVER_URL_PREFIX}/${LLAMA_CLOUD_DOWNLOAD_FOLDER}/${name}`;
|
||||
}
|
||||
const isPrivate = metadata["private"] === "true";
|
||||
const folder = isPrivate ? "output/uploaded" : "data";
|
||||
return `${process.env.FILESERVER_URL_PREFIX}/${folder}/${fileName}`;
|
||||
if (isPrivate) {
|
||||
return `${process.env.FILESERVER_URL_PREFIX}/output/uploaded/${fileName}`;
|
||||
}
|
||||
const filePath = metadata["file_path"];
|
||||
const dataDir = path.resolve(DATA_DIR);
|
||||
|
||||
if (filePath && dataDir) {
|
||||
const relativePath = path.relative(dataDir, filePath);
|
||||
return `${process.env.FILESERVER_URL_PREFIX}/data/${relativePath}`;
|
||||
}
|
||||
}
|
||||
// fallback to URL in metadata (e.g. for websites)
|
||||
return metadata["URL"];
|
||||
}
|
||||
|
||||
async function downloadFilesFromNodes(nodes: NodeWithScore<Metadata>[]) {
|
||||
try {
|
||||
const files = nodesToLlamaCloudFiles(nodes);
|
||||
for (const { pipelineId, fileName, downloadedName } of files) {
|
||||
const downloadUrl = await LLamaCloudFileService.getFileUrl(
|
||||
pipelineId,
|
||||
fileName,
|
||||
);
|
||||
if (downloadUrl) {
|
||||
await downloadFile(
|
||||
downloadUrl,
|
||||
downloadedName,
|
||||
LLAMA_CLOUD_DOWNLOAD_FOLDER,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error downloading files from nodes:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function nodesToLlamaCloudFiles(nodes: NodeWithScore<Metadata>[]) {
|
||||
const files: Array<{
|
||||
pipelineId: string;
|
||||
fileName: string;
|
||||
downloadedName: string;
|
||||
}> = [];
|
||||
for (const node of nodes) {
|
||||
const pipelineId = node.node.metadata["pipeline_id"];
|
||||
const fileName = node.node.metadata["file_name"];
|
||||
if (!pipelineId || !fileName) continue;
|
||||
const isDuplicate = files.some(
|
||||
(f) => f.pipelineId === pipelineId && f.fileName === fileName,
|
||||
);
|
||||
if (!isDuplicate) {
|
||||
files.push({
|
||||
pipelineId,
|
||||
fileName,
|
||||
downloadedName: toDownloadedName(pipelineId, fileName),
|
||||
});
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function toDownloadedName(pipelineId: string, fileName: string) {
|
||||
return `${pipelineId}$${fileName}`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import fs from "node:fs";
|
||||
import https from "node:https";
|
||||
import path from "node:path";
|
||||
|
||||
export async function downloadFile(
|
||||
urlToDownload: string,
|
||||
filename: string,
|
||||
folder = "output/uploaded",
|
||||
) {
|
||||
try {
|
||||
const downloadedPath = path.join(folder, filename);
|
||||
|
||||
// Check if file already exists
|
||||
if (fs.existsSync(downloadedPath)) return;
|
||||
|
||||
const file = fs.createWriteStream(downloadedPath);
|
||||
https
|
||||
.get(urlToDownload, (response) => {
|
||||
response.pipe(file);
|
||||
file.on("finish", () => {
|
||||
file.close(() => {
|
||||
console.log("File downloaded successfully");
|
||||
});
|
||||
});
|
||||
})
|
||||
.on("error", (err) => {
|
||||
fs.unlink(downloadedPath, () => {
|
||||
console.error("Error downloading file:", err);
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Error downloading file: ${error}`);
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
import { Metadata, NodeWithScore } from "llamaindex";
|
||||
import fs from "node:fs";
|
||||
import https from "node:https";
|
||||
import path from "node:path";
|
||||
|
||||
const LLAMA_CLOUD_OUTPUT_DIR = "output/llamacloud";
|
||||
const LLAMA_CLOUD_BASE_URL = "https://cloud.llamaindex.ai/api/v1";
|
||||
const FILE_DELIMITER = "$"; // delimiter between pipelineId and filename
|
||||
|
||||
type LlamaCloudFile = {
|
||||
name: string;
|
||||
file_id: string;
|
||||
project_id: string;
|
||||
};
|
||||
|
||||
type LLamaCloudProject = {
|
||||
id: string;
|
||||
organization_id: string;
|
||||
name: string;
|
||||
is_default: boolean;
|
||||
};
|
||||
|
||||
type LLamaCloudPipeline = {
|
||||
id: string;
|
||||
name: string;
|
||||
project_id: string;
|
||||
};
|
||||
|
||||
export class LLamaCloudFileService {
|
||||
private static readonly headers = {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${process.env.LLAMA_CLOUD_API_KEY}`,
|
||||
};
|
||||
|
||||
public static async getAllProjectsWithPipelines() {
|
||||
try {
|
||||
const projects = await LLamaCloudFileService.getAllProjects();
|
||||
const pipelines = await LLamaCloudFileService.getAllPipelines();
|
||||
return projects.map((project) => ({
|
||||
...project,
|
||||
pipelines: pipelines.filter((p) => p.project_id === project.id),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Error listing projects and pipelines:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public static async downloadFiles(nodes: NodeWithScore<Metadata>[]) {
|
||||
const files = LLamaCloudFileService.nodesToDownloadFiles(nodes);
|
||||
if (!files.length) return;
|
||||
console.log("Downloading files from LlamaCloud...");
|
||||
for (const file of files) {
|
||||
await LLamaCloudFileService.downloadFile(file.pipelineId, file.fileName);
|
||||
}
|
||||
}
|
||||
|
||||
public static toDownloadedName(pipelineId: string, fileName: string) {
|
||||
return `${pipelineId}${FILE_DELIMITER}${fileName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will return an array of unique files to download from LlamaCloud
|
||||
* We only download files that are uploaded directly in LlamaCloud datasources (don't have `private` in metadata)
|
||||
* Files are uploaded directly in LlamaCloud datasources don't have `private` in metadata (public docs)
|
||||
* Files are uploaded from local via `generate` command will have `private=false` (public docs)
|
||||
* Files are uploaded from local via `/chat/upload` endpoint will have `private=true` (private docs)
|
||||
*
|
||||
* @param nodes
|
||||
* @returns list of unique files to download
|
||||
*/
|
||||
private static nodesToDownloadFiles(nodes: NodeWithScore<Metadata>[]) {
|
||||
const downloadFiles: Array<{
|
||||
pipelineId: string;
|
||||
fileName: string;
|
||||
}> = [];
|
||||
for (const node of nodes) {
|
||||
const isLocalFile = node.node.metadata["private"] != null;
|
||||
const pipelineId = node.node.metadata["pipeline_id"];
|
||||
const fileName = node.node.metadata["file_name"];
|
||||
if (isLocalFile || !pipelineId || !fileName) continue;
|
||||
const isDuplicate = downloadFiles.some(
|
||||
(f) => f.pipelineId === pipelineId && f.fileName === fileName,
|
||||
);
|
||||
if (!isDuplicate) {
|
||||
downloadFiles.push({ pipelineId, fileName });
|
||||
}
|
||||
}
|
||||
return downloadFiles;
|
||||
}
|
||||
|
||||
private static async downloadFile(pipelineId: string, fileName: string) {
|
||||
try {
|
||||
const downloadedName = LLamaCloudFileService.toDownloadedName(
|
||||
pipelineId,
|
||||
fileName,
|
||||
);
|
||||
const downloadedPath = path.join(LLAMA_CLOUD_OUTPUT_DIR, downloadedName);
|
||||
|
||||
// Check if file already exists
|
||||
if (fs.existsSync(downloadedPath)) return;
|
||||
|
||||
const urlToDownload = await LLamaCloudFileService.getFileUrlByName(
|
||||
pipelineId,
|
||||
fileName,
|
||||
);
|
||||
if (!urlToDownload) throw new Error("File not found in LlamaCloud");
|
||||
|
||||
const file = fs.createWriteStream(downloadedPath);
|
||||
https
|
||||
.get(urlToDownload, (response) => {
|
||||
response.pipe(file);
|
||||
file.on("finish", () => {
|
||||
file.close(() => {
|
||||
console.log("File downloaded successfully");
|
||||
});
|
||||
});
|
||||
})
|
||||
.on("error", (err) => {
|
||||
fs.unlink(downloadedPath, () => {
|
||||
console.error("Error downloading file:", err);
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Error downloading file from LlamaCloud: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private static async getFileUrlByName(
|
||||
pipelineId: string,
|
||||
name: string,
|
||||
): Promise<string | null> {
|
||||
const files = await LLamaCloudFileService.getAllFiles(pipelineId);
|
||||
const file = files.find((file) => file.name === name);
|
||||
if (!file) return null;
|
||||
return await LLamaCloudFileService.getFileUrlById(
|
||||
file.project_id,
|
||||
file.file_id,
|
||||
);
|
||||
}
|
||||
|
||||
private static async getFileUrlById(
|
||||
projectId: string,
|
||||
fileId: string,
|
||||
): Promise<string> {
|
||||
const url = `${LLAMA_CLOUD_BASE_URL}/files/${fileId}/content?project_id=${projectId}`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: LLamaCloudFileService.headers,
|
||||
});
|
||||
const data = (await response.json()) as { url: string };
|
||||
return data.url;
|
||||
}
|
||||
|
||||
private static async getAllFiles(
|
||||
pipelineId: string,
|
||||
): Promise<LlamaCloudFile[]> {
|
||||
const url = `${LLAMA_CLOUD_BASE_URL}/pipelines/${pipelineId}/files`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: LLamaCloudFileService.headers,
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
private static async getAllProjects(): Promise<LLamaCloudProject[]> {
|
||||
const url = `${LLAMA_CLOUD_BASE_URL}/projects`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: LLamaCloudFileService.headers,
|
||||
});
|
||||
const data = (await response.json()) as LLamaCloudProject[];
|
||||
return data;
|
||||
}
|
||||
|
||||
private static async getAllPipelines(): Promise<LLamaCloudPipeline[]> {
|
||||
const url = `${LLAMA_CLOUD_BASE_URL}/pipelines`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: LLamaCloudFileService.headers,
|
||||
});
|
||||
const data = (await response.json()) as LLamaCloudPipeline[];
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -2,21 +2,16 @@ import os
|
||||
import logging
|
||||
from typing import Dict
|
||||
from llama_parse import LlamaParse
|
||||
from pydantic import BaseModel, validator
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.config import DATA_DIR
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FileLoaderConfig(BaseModel):
|
||||
data_dir: str = "data"
|
||||
use_llama_parse: bool = False
|
||||
|
||||
@validator("data_dir")
|
||||
def data_dir_must_exist(cls, v):
|
||||
if not os.path.isdir(v):
|
||||
raise ValueError(f"Directory '{v}' does not exist")
|
||||
return v
|
||||
|
||||
|
||||
def llama_parse_parser():
|
||||
if os.getenv("LLAMA_CLOUD_API_KEY") is None:
|
||||
@@ -54,7 +49,7 @@ def get_file_documents(config: FileLoaderConfig):
|
||||
|
||||
file_extractor = llama_parse_extractor()
|
||||
reader = SimpleDirectoryReader(
|
||||
config.data_dir,
|
||||
DATA_DIR,
|
||||
recursive=True,
|
||||
filename_as_id=True,
|
||||
raise_on_error=True,
|
||||
|
||||
@@ -27,6 +27,7 @@ def generate_datasource():
|
||||
doc.metadata["private"] = "false"
|
||||
index = VectorStoreIndex.from_documents(
|
||||
documents,
|
||||
show_progress=True,
|
||||
)
|
||||
# store it for later
|
||||
index.storage_context.persist(storage_dir)
|
||||
|
||||
@@ -1,24 +1,44 @@
|
||||
import * as dotenv from "dotenv";
|
||||
import { LlamaCloudIndex } from "llamaindex";
|
||||
import * as fs from "fs/promises";
|
||||
import { LLamaCloudFileService } from "llamaindex";
|
||||
import * as path from "path";
|
||||
import { getDataSource } from "./index";
|
||||
import { getDocuments } from "./loader";
|
||||
import { DATA_DIR } from "./loader";
|
||||
import { initSettings } from "./settings";
|
||||
import { checkRequiredEnvVars } from "./shared";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
async function loadAndIndex() {
|
||||
const documents = await getDocuments();
|
||||
async function* walk(dir: string): AsyncGenerator<string> {
|
||||
const directory = await fs.opendir(dir);
|
||||
|
||||
await getDataSource();
|
||||
await LlamaCloudIndex.fromDocuments({
|
||||
documents,
|
||||
name: process.env.LLAMA_CLOUD_INDEX_NAME!,
|
||||
projectName: process.env.LLAMA_CLOUD_PROJECT_NAME!,
|
||||
apiKey: process.env.LLAMA_CLOUD_API_KEY,
|
||||
baseUrl: process.env.LLAMA_CLOUD_BASE_URL,
|
||||
});
|
||||
console.log(`Successfully created embeddings!`);
|
||||
for await (const dirent of directory) {
|
||||
const entryPath = path.join(dir, dirent.name);
|
||||
|
||||
if (dirent.isDirectory()) {
|
||||
yield* walk(entryPath); // Recursively walk through directories
|
||||
} else if (dirent.isFile()) {
|
||||
yield entryPath; // Yield file paths
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAndIndex() {
|
||||
const index = await getDataSource();
|
||||
const projectId = await index.getProjectId();
|
||||
const pipelineId = await index.getPipelineId();
|
||||
|
||||
// walk through the data directory and upload each file to LlamaCloud
|
||||
for await (const filePath of walk(DATA_DIR)) {
|
||||
const buffer = await fs.readFile(filePath);
|
||||
const filename = path.basename(filePath);
|
||||
const file = new File([buffer], filename);
|
||||
await LLamaCloudFileService.addFileToPipeline(projectId, pipelineId, file, {
|
||||
private: "false",
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Successfully uploaded documents to LlamaCloud!`);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
|
||||
@@ -18,6 +18,7 @@ export async function getDataSource(params?: LlamaCloudDataSourceParams) {
|
||||
);
|
||||
}
|
||||
const index = new LlamaCloudIndex({
|
||||
organizationId: process.env.LLAMA_CLOUD_ORGANIZATION_ID,
|
||||
name: pipelineName,
|
||||
projectName,
|
||||
apiKey,
|
||||
|
||||
@@ -12,7 +12,7 @@ export function generateFilters(documentIds: string[]): MetadataFilters {
|
||||
if (!documentIds.length) return { filters: [publicDocumentsFilter] };
|
||||
|
||||
const privateDocumentsFilter: MetadataFilter = {
|
||||
key: "doc_id",
|
||||
key: "file_id", // Note: LLamaCloud uses "file_id" to reference private document ids as "doc_id" is a restricted field in LlamaCloud
|
||||
value: documentIds,
|
||||
operator: "in",
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
This is a [LlamaIndex](https://www.llamaindex.ai/) project using [FastAPI](https://fastapi.tiangolo.com/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama) featuring [structured extraction](https://docs.llamaindex.ai/en/stable/examples/structured_outputs/structured_outputs/?h=structured+output).
|
||||
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 [structured extraction](https://docs.llamaindex.ai/en/stable/examples/structured_outputs/structured_outputs/?h=structured+output) in a RAG pipeline.
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -8,26 +8,38 @@ First, setup the environment with poetry:
|
||||
|
||||
```shell
|
||||
poetry install
|
||||
poetry shell
|
||||
```
|
||||
|
||||
Then check the parameters that have been pre-configured in the `.env` file in this directory. (E.g. you might need to configure an `OPENAI_API_KEY` if you're using OpenAI as model provider).
|
||||
|
||||
Second, generate the embeddings of the documents in the `./data` directory (if this folder exists - otherwise, skip this step):
|
||||
Second, generate the embeddings of the example document in the `./data` directory:
|
||||
|
||||
```shell
|
||||
poetry run generate
|
||||
```
|
||||
|
||||
Third, run the API in one command:
|
||||
Third, start app with `reflex` command:
|
||||
|
||||
```shell
|
||||
poetry run python main.py
|
||||
poetry run reflex run
|
||||
```
|
||||
|
||||
The example provides the `/api/extractor/query` API endpoint.
|
||||
To deploy the application, refer to the Reflex deployment guide: https://reflex.dev/docs/hosting/deploy-quick-start/
|
||||
|
||||
This query endpoint returns structured data in the format of the [Output](./app/api/routers/output.py) class. Modify this class to change the output format.
|
||||
### UI
|
||||
|
||||
You can now access the UI at http://localhost:3000 to test the structure extractor interactively.
|
||||
|
||||
It allows you to remove and add your own documents, modify the Pydantic model used for structured extraction, and test the RAG pipeline with different queries.
|
||||
|
||||
For example, keep the provided Pydantic model and query: "What is the maximum weight for a parcel?".
|
||||
|
||||
> Note: the Pydantic model used is the last element in the code provided by the user.
|
||||
|
||||
### API
|
||||
|
||||
Alternatively, check the API documentation at http://localhost:8000/docs. This example provides the `/api/extractor/query` API endpoint.
|
||||
Per default, the query endpoint returns structured data in the format of the model [DEFAULT_MODEL](./app/services/model.py) class. Modify this class to change the output format.
|
||||
|
||||
You can test the endpoint with the following curl request:
|
||||
|
||||
@@ -49,15 +61,9 @@ curl --location 'localhost:8000/api/extractor/query' \
|
||||
|
||||
To retrieve a response with low confidence since the question is not related to the provided document in the `./data` directory.
|
||||
|
||||
You can start editing the API endpoint by modifying [`extractor.py`](./app/api/routers/extractor.py). The endpoints auto-update as you save the file.
|
||||
### Development
|
||||
|
||||
Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser to see the Swagger UI of the API.
|
||||
|
||||
The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`:
|
||||
|
||||
```
|
||||
ENVIRONMENT=prod python main.py
|
||||
```
|
||||
You can start editing the behavior by modifying the [`ExtractorService`](./app/services/extractor.py). The app auto-updates as you save the file.
|
||||
|
||||
## Learn More
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.services.model import DEFAULT_MODEL
|
||||
|
||||
|
||||
class RequestData(BaseModel):
|
||||
query: str
|
||||
code: str = DEFAULT_MODEL
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"examples": [
|
||||
{
|
||||
"query": "What's the maximum weight for a parcel?",
|
||||
"code": DEFAULT_MODEL,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,58 +1,15 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from llama_index.core.settings import Settings
|
||||
from pydantic import BaseModel
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.routers.output import Output
|
||||
from app.engine.index import get_index
|
||||
from app.api.models import RequestData
|
||||
from app.services.extractor import ExtractorService
|
||||
|
||||
extractor_router = r = APIRouter()
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class RequestData(BaseModel):
|
||||
query: str
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"examples": [
|
||||
{"query": "What's the maximum weight for a parcel?"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@r.post("/query")
|
||||
async def query_request(
|
||||
data: RequestData,
|
||||
):
|
||||
# Create a query engine using that returns responses in the format of the Output class
|
||||
query_engine = get_query_engine(Output)
|
||||
|
||||
response = await query_engine.aquery(data.query)
|
||||
|
||||
output_data = response.response.dict()
|
||||
return Output(**output_data)
|
||||
|
||||
|
||||
def get_query_engine(output_cls: BaseModel):
|
||||
top_k = os.getenv("TOP_K", 3)
|
||||
|
||||
index = get_index()
|
||||
if index is None:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=str(
|
||||
"StorageContext is empty - call 'poetry run generate' to generate the storage first"
|
||||
),
|
||||
)
|
||||
|
||||
sllm = Settings.llm.as_structured_llm(output_cls)
|
||||
|
||||
return index.as_query_engine(
|
||||
similarity_top_k=int(top_k),
|
||||
llm=sllm,
|
||||
response_mode="tree_summarize",
|
||||
)
|
||||
async def query_request(data: RequestData):
|
||||
return await ExtractorService.extract(query=data.query, model_code=data.code)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.routers.extractor import extractor_router
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
api_router.include_router(extractor_router, prefix="/api/extractor")
|
||||
@@ -0,0 +1,22 @@
|
||||
# flake8: noqa: E402
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
import reflex as rx
|
||||
from fastapi import FastAPI
|
||||
|
||||
from app.api.routers.extractor import extractor_router
|
||||
from app.settings import init_settings
|
||||
from app.ui.pages import * # Keep this import all pages in the app # noqa: F403
|
||||
|
||||
init_settings()
|
||||
|
||||
|
||||
def add_routers(app: FastAPI):
|
||||
app.include_router(extractor_router, prefix="/api/extractor")
|
||||
|
||||
|
||||
app = rx.App()
|
||||
add_routers(app.api)
|
||||
@@ -0,0 +1 @@
|
||||
DATA_DIR = "data"
|
||||
@@ -0,0 +1 @@
|
||||
from .engine import get_query_engine as get_query_engine
|
||||
@@ -0,0 +1,27 @@
|
||||
import os
|
||||
|
||||
from fastapi import HTTPException
|
||||
from llama_index.core.settings import Settings
|
||||
|
||||
from app.engine.index import get_index
|
||||
|
||||
|
||||
def get_query_engine(output_cls):
|
||||
top_k = int(os.getenv("TOP_K", 0))
|
||||
|
||||
index = get_index()
|
||||
if index is None:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=str(
|
||||
"StorageContext is empty - call 'poetry run generate' to generate the storage first"
|
||||
),
|
||||
)
|
||||
|
||||
sllm = Settings.llm.as_structured_llm(output_cls)
|
||||
|
||||
return index.as_query_engine(
|
||||
llm=sllm,
|
||||
response_mode="tree_summarize",
|
||||
**({"similarity_top_k": top_k} if top_k != 0 else {}),
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
# flake8: noqa: E402
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from llama_index.core.ingestion import IngestionPipeline
|
||||
from llama_index.core.node_parser import SentenceSplitter
|
||||
from llama_index.core.settings import Settings
|
||||
from llama_index.core.storage import StorageContext
|
||||
from llama_index.core.storage.docstore import SimpleDocumentStore
|
||||
|
||||
from app.engine.loaders import get_documents
|
||||
from app.engine.vectordb import get_vector_store
|
||||
from app.settings import init_settings
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger()
|
||||
|
||||
STORAGE_DIR = os.getenv("STORAGE_DIR", "storage")
|
||||
|
||||
|
||||
def get_doc_store():
|
||||
# If the storage directory is there, load the document store from it.
|
||||
# If not, set up an in-memory document store since we can't load from a directory that doesn't exist.
|
||||
if os.path.exists(STORAGE_DIR):
|
||||
return SimpleDocumentStore.from_persist_dir(STORAGE_DIR)
|
||||
else:
|
||||
return SimpleDocumentStore()
|
||||
|
||||
|
||||
def run_pipeline(docstore, vector_store, documents):
|
||||
pipeline = IngestionPipeline(
|
||||
transformations=[
|
||||
SentenceSplitter(
|
||||
chunk_size=Settings.chunk_size,
|
||||
chunk_overlap=Settings.chunk_overlap,
|
||||
),
|
||||
Settings.embed_model,
|
||||
],
|
||||
docstore=docstore,
|
||||
docstore_strategy="upserts_and_delete",
|
||||
vector_store=vector_store,
|
||||
)
|
||||
|
||||
# Run the ingestion pipeline and store the results
|
||||
nodes = pipeline.run(show_progress=True, documents=documents)
|
||||
|
||||
return nodes
|
||||
|
||||
|
||||
def persist_storage(docstore, vector_store):
|
||||
storage_context = StorageContext.from_defaults(
|
||||
docstore=docstore,
|
||||
vector_store=vector_store,
|
||||
)
|
||||
storage_context.persist(STORAGE_DIR)
|
||||
|
||||
|
||||
def generate_datasource():
|
||||
init_settings()
|
||||
logger.info("Generate index for the provided data")
|
||||
|
||||
# Get the stores and documents or create new ones
|
||||
documents = get_documents()
|
||||
docstore = get_doc_store()
|
||||
vector_store = get_vector_store()
|
||||
|
||||
# Run the ingestion pipeline
|
||||
_ = run_pipeline(docstore, vector_store, documents)
|
||||
|
||||
# Build the index and persist storage
|
||||
persist_storage(docstore, vector_store)
|
||||
|
||||
logger.info("Finished generating the index")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_datasource()
|
||||
@@ -0,0 +1,17 @@
|
||||
import logging
|
||||
from llama_index.core.indices import VectorStoreIndex
|
||||
from app.engine.vectordb import get_vector_store
|
||||
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
def get_index(params=None):
|
||||
logger.info("Connecting vector store...")
|
||||
store = get_vector_store()
|
||||
# Load the index from the vector store
|
||||
# If you are using a vector store that doesn't store text,
|
||||
# you must load the index from both the vector store and the document store
|
||||
index = VectorStoreIndex.from_vector_store(store)
|
||||
logger.info("Finished load index from vector store.")
|
||||
return index
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import logging
|
||||
from llama_index.core.schema import BaseModel, Field
|
||||
from typing import List
|
||||
|
||||
from llama_index.core.schema import BaseModel, Field
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import logging
|
||||
from app.engine import get_query_engine
|
||||
from app.services.model import IMPORTS
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class InvalidModelCode(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ExtractorService:
|
||||
@staticmethod
|
||||
def _parse_code(model_code: str):
|
||||
try:
|
||||
python_code = f"{IMPORTS}\n\n{model_code}"
|
||||
logger.debug(python_code)
|
||||
namespace = {}
|
||||
exec(python_code, namespace)
|
||||
# using the last object that the user defined in `model_code` as pydantic class
|
||||
pydantic_class = namespace[list(namespace.keys())[-1]]
|
||||
class_name = pydantic_class.__name__
|
||||
logger.info(f"Using Pydantic class {class_name} for extraction")
|
||||
return pydantic_class
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
raise InvalidModelCode() from e
|
||||
|
||||
@classmethod
|
||||
async def extract(cls, query: str, model_code: str) -> str:
|
||||
schema_model = cls._parse_code(model_code)
|
||||
# Create a query engine using that returns responses in the format of the schema
|
||||
query_engine = get_query_engine(schema_model)
|
||||
response = await query_engine.aquery(query)
|
||||
output_data = response.response.dict()
|
||||
return schema_model(**output_data).json(indent=2)
|
||||
@@ -0,0 +1,22 @@
|
||||
IMPORTS = """
|
||||
from llama_index.core.schema import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
from datetime import date
|
||||
"""
|
||||
|
||||
DEFAULT_MODEL = """class Output(BaseModel):
|
||||
response: str = Field(..., description="The answer to the question.")
|
||||
page_numbers: List[int] = Field(
|
||||
...,
|
||||
description="The page numbers of the sources used to answer this question. Do not include a page number if the context is irrelevant.",
|
||||
)
|
||||
confidence: float = Field(
|
||||
...,
|
||||
ge=0,
|
||||
le=1,
|
||||
description="Confidence value between 0-1 of the correctness of the result.",
|
||||
)
|
||||
confidence_explanation: str = Field(
|
||||
..., description="Explanation for the confidence score"
|
||||
)
|
||||
"""
|
||||
@@ -0,0 +1,9 @@
|
||||
from .extractor import (
|
||||
StructuredQuery as StructuredQuery,
|
||||
extract_data_component as extract_data_component,
|
||||
)
|
||||
from .schema_editor import schema_editor_component as schema_editor_component
|
||||
from .upload import (
|
||||
UploadedFilesState as UploadedFilesState,
|
||||
upload_component as upload_component,
|
||||
)
|
||||
@@ -0,0 +1,87 @@
|
||||
import logging
|
||||
import reflex as rx
|
||||
from app.services.model import DEFAULT_MODEL
|
||||
from app.services.extractor import ExtractorService, InvalidModelCode
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class StructuredQuery(rx.State):
|
||||
query: str
|
||||
response: str
|
||||
loading: bool = False
|
||||
code: str = DEFAULT_MODEL
|
||||
error: str = None
|
||||
|
||||
@rx.background
|
||||
async def handle_query(self):
|
||||
async with self:
|
||||
if not self.query:
|
||||
self.error = "Please enter a query."
|
||||
return
|
||||
self.error = None
|
||||
self.loading = True
|
||||
|
||||
# Extract data
|
||||
# Await long operations outside the context to avoid blocking UI
|
||||
try:
|
||||
response = await ExtractorService.extract(
|
||||
query=self.query, model_code=self.code
|
||||
)
|
||||
except InvalidModelCode:
|
||||
async with self:
|
||||
self.error = "Invalid Python code"
|
||||
response = None
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
logger.error(
|
||||
f"Error occurred: {str(e)}\nStack trace:\n{traceback.format_exc()}"
|
||||
)
|
||||
async with self:
|
||||
self.error = f"Error: {str(e)}"
|
||||
response = None
|
||||
|
||||
async with self:
|
||||
self.response = response
|
||||
self.loading = False
|
||||
|
||||
|
||||
def extract_data_component() -> rx.Component:
|
||||
return rx.vstack(
|
||||
rx.cond(
|
||||
StructuredQuery.error,
|
||||
rx.callout(
|
||||
StructuredQuery.error,
|
||||
icon="triangle_alert",
|
||||
color_scheme="red",
|
||||
role="alert",
|
||||
),
|
||||
),
|
||||
rx.text_area(
|
||||
id="query",
|
||||
placeholder="Enter query",
|
||||
on_change=StructuredQuery.set_query,
|
||||
width="100%",
|
||||
height="10vh",
|
||||
),
|
||||
rx.button(
|
||||
"Query",
|
||||
on_click=StructuredQuery.handle_query,
|
||||
loading=StructuredQuery.loading,
|
||||
),
|
||||
rx.cond(
|
||||
StructuredQuery.response,
|
||||
rx.code_block(
|
||||
StructuredQuery.response,
|
||||
language="json",
|
||||
show_line_numbers=True,
|
||||
wrap_long_lines=True,
|
||||
size="3",
|
||||
resize="vertical",
|
||||
width="100%",
|
||||
height="70vh",
|
||||
),
|
||||
),
|
||||
width="100%",
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Reflex custom component Monaco."""
|
||||
|
||||
# For wrapping react guide, visit https://reflex.dev/docs/wrapping-react/overview/
|
||||
# Taken and modified from https://github.com/Lendemor/reflex-monaco
|
||||
|
||||
import reflex as rx
|
||||
|
||||
|
||||
class MonacoComponent(rx.Component):
|
||||
"""Base Monaco component."""
|
||||
|
||||
library = "@monaco-editor/react@4.6.0"
|
||||
|
||||
# The language to use for the editor.
|
||||
language: rx.Var[str]
|
||||
|
||||
# The theme to use for the editor.
|
||||
theme: rx.Var[str] = rx.color_mode_cond("light", "vs-dark") # type: ignore
|
||||
|
||||
# The width of the editor.
|
||||
line: rx.Var[int] = rx.Var.create_safe(1, _var_is_string=False)
|
||||
|
||||
# The height of the editor.
|
||||
width: rx.Var[str]
|
||||
|
||||
# The height of the editor.
|
||||
height: rx.Var[str]
|
||||
|
||||
|
||||
class MonacoEditor(MonacoComponent):
|
||||
"""The Monaco Editor component."""
|
||||
|
||||
# The React component tag.
|
||||
tag = "MonacoEditor"
|
||||
|
||||
is_default = True
|
||||
|
||||
# The default value to display in the editor.
|
||||
default_value: rx.Var[str]
|
||||
|
||||
# The default language to use for the editor.
|
||||
default_language: rx.Var[str]
|
||||
|
||||
# The path to the default file to load in the editor.
|
||||
default_path: rx.Var[str]
|
||||
|
||||
# The value to display in the editor.
|
||||
value: rx.Var[str]
|
||||
|
||||
# Triggered when the editor value changes.
|
||||
on_change: rx.EventHandler[lambda e: [e]]
|
||||
|
||||
# Triggered when the content is validated. (limited to some languages)
|
||||
on_validate: rx.EventHandler[lambda e: [e]]
|
||||
|
||||
options = {
|
||||
"minimap": {"enabled": False},
|
||||
}
|
||||
|
||||
|
||||
monaco = MonacoEditor.create
|
||||
@@ -0,0 +1,18 @@
|
||||
import reflex as rx
|
||||
|
||||
from app.ui.components.extractor import StructuredQuery
|
||||
from .monaco import monaco
|
||||
|
||||
|
||||
def schema_editor_component() -> rx.Component:
|
||||
return rx.vstack(
|
||||
rx.heading("Pydantic model", size="5"),
|
||||
monaco(
|
||||
default_language="python",
|
||||
default_value=StructuredQuery.code,
|
||||
width="100%",
|
||||
height="50vh",
|
||||
on_change=StructuredQuery.set_code,
|
||||
),
|
||||
width="100%",
|
||||
)
|
||||
@@ -0,0 +1,94 @@
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
import reflex as rx
|
||||
from app.engine.generate import generate_datasource
|
||||
|
||||
|
||||
class UploadedFile(rx.Base):
|
||||
file_name: str
|
||||
size: int
|
||||
|
||||
|
||||
class UploadedFilesState(rx.State):
|
||||
_uploaded_dir = "data"
|
||||
uploaded_files: List[UploadedFile] = []
|
||||
|
||||
async def handle_upload(self, files: list[rx.UploadFile]):
|
||||
for file in files:
|
||||
upload_data = await file.read()
|
||||
outfile = os.path.join(self._uploaded_dir, file.filename)
|
||||
with open(outfile, "wb") as f:
|
||||
f.write(upload_data)
|
||||
|
||||
new_file = UploadedFile(file_name=file.filename, size=len(upload_data))
|
||||
|
||||
self.uploaded_files.append(new_file)
|
||||
|
||||
# Run indexing
|
||||
try:
|
||||
generate_datasource()
|
||||
except Exception as e:
|
||||
print("Error generating datasource", e)
|
||||
os.remove(outfile)
|
||||
self.uploaded_files.remove(new_file)
|
||||
return rx.toast.error(
|
||||
f"Error generating index for the uploaded files. {str(e)}",
|
||||
position="top-center",
|
||||
)
|
||||
|
||||
return rx.toast.success("Files uploaded successfully", position="top-center")
|
||||
|
||||
def has_files(self) -> bool:
|
||||
return len(self.uploaded_files) > 0
|
||||
|
||||
def load_files(self):
|
||||
self.uploaded_files = []
|
||||
for file in os.listdir(self._uploaded_dir):
|
||||
file_path = os.path.join(self._uploaded_dir, file)
|
||||
if os.path.isfile(file_path):
|
||||
self.uploaded_files.append(
|
||||
UploadedFile(file_name=file, size=os.path.getsize(file_path))
|
||||
)
|
||||
|
||||
def remove_file(self, file_name: str):
|
||||
for file in self.uploaded_files:
|
||||
if file.file_name == file_name:
|
||||
self.uploaded_files.remove(file)
|
||||
os.remove(os.path.join(self._uploaded_dir, file_name))
|
||||
# Run indexing
|
||||
generate_datasource()
|
||||
break
|
||||
|
||||
|
||||
def upload_component() -> rx.Component:
|
||||
return rx.vstack(
|
||||
rx.heading("Upload", size="5"),
|
||||
rx.upload(
|
||||
rx.vstack(
|
||||
rx.text("Drag and drop files here or click to select files"),
|
||||
),
|
||||
on_drop=UploadedFilesState.handle_upload(
|
||||
rx.upload_files(upload_id="upload1")
|
||||
),
|
||||
id="upload1",
|
||||
border="1px dotted rgb(107,99,246)",
|
||||
),
|
||||
rx.foreach(
|
||||
UploadedFilesState.uploaded_files,
|
||||
lambda file: rx.card(
|
||||
rx.stack(
|
||||
rx.text(file.file_name, size="sm"),
|
||||
rx.button(
|
||||
"x",
|
||||
size="sm",
|
||||
on_click=UploadedFilesState.remove_file(file.file_name),
|
||||
),
|
||||
justify="between",
|
||||
width="100%",
|
||||
),
|
||||
width="100%",
|
||||
),
|
||||
),
|
||||
width="100%",
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
from .index import index as index
|
||||
@@ -0,0 +1,49 @@
|
||||
import reflex as rx
|
||||
|
||||
from ..components import (
|
||||
UploadedFilesState,
|
||||
extract_data_component,
|
||||
schema_editor_component,
|
||||
upload_component,
|
||||
)
|
||||
from ..templates import template
|
||||
|
||||
|
||||
@template(
|
||||
route="/",
|
||||
title="Structure extractor",
|
||||
on_load=[
|
||||
UploadedFilesState.load_files,
|
||||
],
|
||||
)
|
||||
def index() -> rx.Component:
|
||||
"""The main index page."""
|
||||
return rx.vstack(
|
||||
rx.vstack(
|
||||
rx.heading("Built by LlamaIndex", size="6"),
|
||||
rx.text(
|
||||
"Upload a file then enter a query. The response will be according to the provided Pydantic model."
|
||||
),
|
||||
background_color="var(--gray-3)",
|
||||
align_items="left",
|
||||
justify_content="left",
|
||||
width="100%",
|
||||
padding="1rem",
|
||||
),
|
||||
rx.stack(
|
||||
rx.vstack(
|
||||
upload_component(),
|
||||
rx.divider(),
|
||||
schema_editor_component(),
|
||||
width="50%",
|
||||
),
|
||||
rx.divider(orientation="vertical"),
|
||||
rx.stack(
|
||||
extract_data_component(),
|
||||
width="50%",
|
||||
),
|
||||
width="100%",
|
||||
padding="1rem",
|
||||
),
|
||||
width="100%",
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
from .template import ThemeState as ThemeState, template as template
|
||||
@@ -0,0 +1,28 @@
|
||||
import reflex as rx
|
||||
|
||||
border_radius = "var(--radius-2)"
|
||||
border = f"1px solid {rx.color('gray', 5)}"
|
||||
text_color = rx.color("gray", 11)
|
||||
gray_color = rx.color("gray", 11)
|
||||
gray_bg_color = rx.color("gray", 3)
|
||||
accent_text_color = rx.color("accent", 10)
|
||||
accent_color = rx.color("accent", 1)
|
||||
accent_bg_color = rx.color("accent", 3)
|
||||
hover_accent_color = {"_hover": {"color": accent_text_color}}
|
||||
hover_accent_bg = {"_hover": {"background_color": accent_color}}
|
||||
content_width_vw = "90vw"
|
||||
sidebar_width = "32em"
|
||||
sidebar_content_width = "16em"
|
||||
color_box_size = ["2.25rem", "2.25rem", "2.5rem"]
|
||||
|
||||
|
||||
template_page_style = {
|
||||
"padding_top": ["1em", "1em", "2em"],
|
||||
"padding_x": ["auto", "auto", "2em"],
|
||||
}
|
||||
|
||||
template_content_style = {
|
||||
"padding": "1em",
|
||||
"margin_bottom": "2em",
|
||||
"min_height": "90vh",
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Common templates used between pages in the app."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
import reflex as rx
|
||||
|
||||
from . import styles
|
||||
|
||||
# Meta tags for the app.
|
||||
default_meta = [
|
||||
{
|
||||
"name": "viewport",
|
||||
"content": "width=device-width, shrink-to-fit=no, initial-scale=1",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class ThemeState(rx.State):
|
||||
"""The state for the theme of the app."""
|
||||
|
||||
accent_color: str = "crimson"
|
||||
|
||||
gray_color: str = "gray"
|
||||
|
||||
radius: str = "large"
|
||||
|
||||
scaling: str = "100%"
|
||||
|
||||
|
||||
def template(
|
||||
route: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
meta: str | None = None,
|
||||
script_tags: list[rx.Component] | None = None,
|
||||
on_load: rx.event.EventHandler | list[rx.event.EventHandler] | None = None,
|
||||
) -> Callable[[Callable[[], rx.Component]], rx.Component]:
|
||||
"""The template for each page of the app.
|
||||
|
||||
Args:
|
||||
route: The route to reach the page.
|
||||
title: The title of the page.
|
||||
description: The description of the page.
|
||||
meta: Additional meta to add to the page.
|
||||
on_load: The event handler(s) called when the page load.
|
||||
script_tags: Scripts to attach to the page.
|
||||
|
||||
Returns:
|
||||
The template with the page content.
|
||||
"""
|
||||
|
||||
def decorator(page_content: Callable[[], rx.Component]) -> rx.Component:
|
||||
"""The template for each page of the app.
|
||||
|
||||
Args:
|
||||
page_content: The content of the page.
|
||||
|
||||
Returns:
|
||||
The template with the page content.
|
||||
"""
|
||||
# Get the meta tags for the page.
|
||||
all_meta = [*default_meta, *(meta or [])]
|
||||
|
||||
def templated_page():
|
||||
return rx.flex(
|
||||
rx.flex(
|
||||
rx.vstack(
|
||||
page_content(),
|
||||
width="100%",
|
||||
**styles.template_content_style,
|
||||
),
|
||||
width="100%",
|
||||
**styles.template_page_style,
|
||||
max_width=[
|
||||
"100%",
|
||||
"100%",
|
||||
"100%",
|
||||
"100%",
|
||||
"100%",
|
||||
],
|
||||
),
|
||||
flex_direction=[
|
||||
"column",
|
||||
"column",
|
||||
"column",
|
||||
"column",
|
||||
"column",
|
||||
"row",
|
||||
],
|
||||
width="100%",
|
||||
margin="auto",
|
||||
position="relative",
|
||||
)
|
||||
|
||||
@rx.page(
|
||||
route=route,
|
||||
title=title,
|
||||
description=description,
|
||||
meta=all_meta,
|
||||
script_tags=script_tags,
|
||||
on_load=on_load,
|
||||
)
|
||||
def theme_wrap():
|
||||
return rx.theme(
|
||||
templated_page(),
|
||||
has_background=True,
|
||||
accent_color=ThemeState.accent_color,
|
||||
gray_color=ThemeState.gray_color,
|
||||
radius=ThemeState.radius,
|
||||
scaling=ThemeState.scaling,
|
||||
)
|
||||
|
||||
return theme_wrap
|
||||
|
||||
return decorator
|
||||
@@ -0,0 +1,4 @@
|
||||
__pycache__
|
||||
storage
|
||||
.env
|
||||
output
|
||||
@@ -1,46 +0,0 @@
|
||||
# flake8: noqa: E402
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
import logging
|
||||
import os
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import RedirectResponse
|
||||
from app.api.routers.extractor import extractor_router
|
||||
from app.settings import init_settings
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
init_settings()
|
||||
|
||||
environment = os.getenv("ENVIRONMENT", "dev") # Default to 'development' if not set
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
if environment == "dev":
|
||||
logger.warning("Running in development mode - allowing CORS for all origins")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Redirect to documentation page when accessing base URL
|
||||
@app.get("/")
|
||||
async def redirect_to_docs():
|
||||
return RedirectResponse(url="/docs")
|
||||
|
||||
|
||||
app.include_router(extractor_router, prefix="/api/extractor")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app_host = os.getenv("APP_HOST", "0.0.0.0")
|
||||
app_port = int(os.getenv("APP_PORT", "8000"))
|
||||
reload = True if environment == "dev" else False
|
||||
|
||||
uvicorn.run(app="main:app", host=app_host, port=app_port, reload=reload)
|
||||
@@ -15,6 +15,7 @@ uvicorn = { extras = ["standard"], version = "^0.23.2" }
|
||||
python-dotenv = "^1.0.0"
|
||||
llama-index = "^0.10.58"
|
||||
cachetools = "^5.3.3"
|
||||
reflex = "^0.5.9"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import reflex as rx
|
||||
|
||||
config = rx.Config(
|
||||
app_name="app",
|
||||
telemetry_enabled=False,
|
||||
)
|
||||
@@ -19,7 +19,10 @@ def get_query_engine_tool() -> QueryEngineTool:
|
||||
index = get_index()
|
||||
if index is None:
|
||||
raise ValueError("Index not found. Please create an index first.")
|
||||
query_engine = index.as_query_engine(similarity_top_k=int(os.getenv("TOP_K", 3)))
|
||||
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(
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"dotenv": "^16.3.1",
|
||||
"duck-duck-scrape": "^2.2.5",
|
||||
"express": "^4.18.2",
|
||||
"llamaindex": "0.5.17",
|
||||
"llamaindex": "0.5.19",
|
||||
"pdf2json": "3.0.5",
|
||||
"ajv": "^8.12.0",
|
||||
"@e2b/code-interpreter": "^0.0.5",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Request, Response } from "express";
|
||||
import { LLamaCloudFileService } from "./llamaindex/streaming/service";
|
||||
import { LLamaCloudFileService } from "llamaindex";
|
||||
|
||||
export const chatConfig = async (_req: Request, res: Response) => {
|
||||
let starterQuestions = undefined;
|
||||
@@ -15,6 +15,11 @@ export const chatConfig = async (_req: Request, res: Response) => {
|
||||
};
|
||||
|
||||
export const chatLlamaCloudConfig = async (_req: Request, res: Response) => {
|
||||
if (!process.env.LLAMA_CLOUD_API_KEY) {
|
||||
return res.status(500).json({
|
||||
error: "env variable LLAMA_CLOUD_API_KEY is required to use LlamaCloud",
|
||||
});
|
||||
}
|
||||
const config = {
|
||||
projects: await LLamaCloudFileService.getAllProjectsWithPipelines(),
|
||||
pipeline: {
|
||||
|
||||
@@ -3,12 +3,16 @@ import { getDataSource } from "./engine";
|
||||
import { uploadDocument } from "./llamaindex/documents/upload";
|
||||
|
||||
export const chatUpload = async (req: Request, res: Response) => {
|
||||
const { base64, params }: { base64: string; params?: any } = req.body;
|
||||
if (!base64) {
|
||||
const {
|
||||
filename,
|
||||
base64,
|
||||
params,
|
||||
}: { filename: string; base64: string; params?: any } = req.body;
|
||||
if (!base64 || !filename) {
|
||||
return res.status(400).json({
|
||||
error: "base64 is required in the request body",
|
||||
error: "base64 and filename is required in the request body",
|
||||
});
|
||||
}
|
||||
const index = await getDataSource(params);
|
||||
return res.status(200).json(await uploadDocument(index, base64));
|
||||
return res.status(200).json(await uploadDocument(index, filename, base64));
|
||||
};
|
||||
|
||||
@@ -7,6 +7,8 @@ from llama_index.core.schema import NodeWithScore
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from pydantic.alias_generators import to_camel
|
||||
|
||||
from app.config import DATA_DIR
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
@@ -175,6 +177,7 @@ class SourceNodes(BaseModel):
|
||||
"Warning: FILESERVER_URL_PREFIX not set in environment variables. Can't use file server"
|
||||
)
|
||||
file_name = metadata.get("file_name")
|
||||
|
||||
if file_name and url_prefix:
|
||||
# file_name exists and file server is configured
|
||||
pipeline_id = metadata.get("pipeline_id")
|
||||
@@ -184,11 +187,17 @@ class SourceNodes(BaseModel):
|
||||
return f"{url_prefix}/output/llamacloud/{file_name}"
|
||||
is_private = metadata.get("private", "false") == "true"
|
||||
if is_private:
|
||||
# file is a private upload
|
||||
return f"{url_prefix}/output/uploaded/{file_name}"
|
||||
return f"{url_prefix}/data/{file_name}"
|
||||
else:
|
||||
# fallback to URL in metadata (e.g. for websites)
|
||||
return metadata.get("URL")
|
||||
# file is from calling the 'generate' script
|
||||
# Get the relative path of file_path to data_dir
|
||||
file_path = metadata.get("file_path")
|
||||
data_dir = os.path.abspath(DATA_DIR)
|
||||
if file_path and data_dir:
|
||||
relative_path = os.path.relpath(file_path, data_dir)
|
||||
return f"{url_prefix}/data/{relative_path}"
|
||||
# fallback to URL in metadata (e.g. for websites)
|
||||
return metadata.get("URL")
|
||||
|
||||
@classmethod
|
||||
def from_source_nodes(cls, source_nodes: List[NodeWithScore]):
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DATA_DIR = "data"
|
||||
@@ -0,0 +1 @@
|
||||
from .engine import get_chat_engine as get_chat_engine
|
||||
@@ -1,6 +1,8 @@
|
||||
# flake8: noqa: E402
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from app.config import DATA_DIR
|
||||
|
||||
load_dotenv()
|
||||
|
||||
import logging
|
||||
@@ -43,15 +45,16 @@ if environment == "dev":
|
||||
|
||||
def mount_static_files(directory, path):
|
||||
if os.path.exists(directory):
|
||||
for dir, _, _ in os.walk(directory):
|
||||
relative_path = os.path.relpath(dir, directory)
|
||||
mount_path = path if relative_path == "." else f"{path}/{relative_path}"
|
||||
logger.info(f"Mounting static files '{dir}' at {mount_path}")
|
||||
app.mount(mount_path, StaticFiles(directory=dir), name=f"{dir}-static")
|
||||
logger.info(f"Mounting static files '{directory}' at '{path}'")
|
||||
app.mount(
|
||||
path,
|
||||
StaticFiles(directory=directory, check_dir=False),
|
||||
name=f"{directory}-static",
|
||||
)
|
||||
|
||||
|
||||
# Mount the data files to serve the file viewer
|
||||
mount_static_files("data", "/api/files/data")
|
||||
mount_static_files(DATA_DIR, "/api/files/data")
|
||||
# Mount the output files from tools
|
||||
mount_static_files("output", "/api/files/output")
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { LLamaCloudFileService } from "llamaindex";
|
||||
import { NextResponse } from "next/server";
|
||||
import { LLamaCloudFileService } from "../../llamaindex/streaming/service";
|
||||
|
||||
/**
|
||||
* This API is to get config from the backend envs and expose them to the frontend
|
||||
*/
|
||||
export async function GET() {
|
||||
if (!process.env.LLAMA_CLOUD_API_KEY) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "env variable LLAMA_CLOUD_API_KEY is required to use LlamaCloud",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
const config = {
|
||||
projects: await LLamaCloudFileService.getAllProjectsWithPipelines(),
|
||||
pipeline: {
|
||||
|
||||
@@ -10,11 +10,15 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { base64, params }: { base64: string; params?: any } =
|
||||
const {
|
||||
filename,
|
||||
base64,
|
||||
params,
|
||||
}: { filename: string; base64: string; params?: any } =
|
||||
await request.json();
|
||||
if (!base64) {
|
||||
if (!base64 || !filename) {
|
||||
return NextResponse.json(
|
||||
{ error: "base64 is required in the request body" },
|
||||
{ error: "base64 and filename is required in the request body" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
@@ -24,7 +28,7 @@ export async function POST(request: NextRequest) {
|
||||
`StorageContext is empty - call 'npm run generate' to generate the storage first`,
|
||||
);
|
||||
}
|
||||
return NextResponse.json(await uploadDocument(index, base64));
|
||||
return NextResponse.json(await uploadDocument(index, filename, base64));
|
||||
} catch (error) {
|
||||
console.error("[Upload API]", error);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { readFile } from "fs/promises";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import path from "path";
|
||||
import { DATA_DIR } from "../../chat/engine/loader";
|
||||
|
||||
/**
|
||||
* This API is to get file data from allowed folders
|
||||
@@ -28,7 +29,11 @@ export async function GET(
|
||||
}
|
||||
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), folder, path.join(...pathTofile));
|
||||
const filePath = path.join(
|
||||
process.cwd(),
|
||||
folder === "data" ? DATA_DIR : folder,
|
||||
path.join(...pathTofile),
|
||||
);
|
||||
const blob = await readFile(filePath);
|
||||
|
||||
return new NextResponse(blob, {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"duck-duck-scrape": "^2.2.5",
|
||||
"formdata-node": "^6.0.3",
|
||||
"got": "^14.4.1",
|
||||
"llamaindex": "0.5.17",
|
||||
"llamaindex": "0.5.19",
|
||||
"lucide-react": "^0.294.0",
|
||||
"next": "^14.2.4",
|
||||
"react": "^18.2.0",
|
||||
|
||||
Reference in New Issue
Block a user