Compare commits

...

65 Commits

Author SHA1 Message Date
github-actions[bot] 7bce7386d5 Release 0.2.5 (#285)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-12 13:53:28 +07:00
Huu Le c011455dc4 fix cannot upload file (#286) 2024-09-12 13:51:48 +07:00
Thuc Pham 38a8be8d12 fix: filter in mongo vector store (#269) 2024-09-12 11:34:54 +07:00
github-actions[bot] 6e70eb4d11 Release 0.2.4 (#284)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-10 10:32:14 +07:00
Huu Le 917e862202 chore: fix ts syntax (#283) 2024-09-10 10:17:29 +07:00
github-actions[bot] e363bfeecc Release 0.2.3 (#281)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-09 17:18:40 +07:00
Huu Le b6da3c2419 chore: Always use file loader as default loader (#279) 2024-09-09 17:07:04 +07:00
github-actions[bot] 71fbe1b18f Release 0.2.2 (#277)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-09 14:41:21 +07:00
Huu Le 8105c5cf06 feat: Make suggest next questions configurable (#275)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-09-09 14:39:36 +07:00
github-actions[bot] c16deed864 Release 0.2.1 (#274)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-06 13:15:00 +07:00
Huu Le 6a409cbbc6 chore: bump tool package versions (#273) 2024-09-06 13:12:42 +07:00
github-actions[bot] a1892bef26 Release 0.2.0 (#272)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-05 12:18:42 +07:00
Marcus Schiesser 2f7e0220b5 docs: update changeset 2024-09-05 12:15:34 +07:00
Marcus Schiesser 435109fef0 feat: add multi-agents template based on workflows (#271)
---------
Co-authored-by: Thuc Pham <51660321+thucpn@users.noreply.github.com>
2024-09-05 12:13:39 +07:00
github-actions[bot] b1f3d5222f Release 0.1.44 (#266)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-29 16:01:59 +07:00
Marcus Schiesser e2c61884ef docs: improved wording 2024-08-29 15:55:27 +07:00
Thuc Pham fd4abb3bdd fix: keep origin upload filename (#268) 2024-08-29 15:47:50 +07:00
Huu Le bedde2bf20 Use is_empty filter (#263) 2024-08-29 15:46:31 +07:00
Huu Le 5cd12fa90d bump create-llama to 0.11 and update event handler (#260) 2024-08-29 14:24:57 +07:00
Thuc Pham 72b71952aa fix: dont use props as state in chat suggestion component (#267) 2024-08-29 11:38:15 +07:00
Thuc Pham 2f8feabcba feat: simplify CLI interface (#265) 2024-08-28 17:28:29 +07:00
github-actions[bot] a8a8c247e2 Release 0.1.43 (#264)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-28 16:50:27 +07:00
Thuc Pham 4fa2b76f3d feat: implement citation for TS (#257) 2024-08-28 16:47:00 +07:00
Thuc Pham 4ead8e14c2 fix: update nextjs config (#262) 2024-08-28 16:22:33 +07:00
github-actions[bot] 90398400c6 Release 0.1.42 (#261)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-27 14:15:18 +07:00
Marcus Schiesser 8f670a935c fix: allow relative URL in docs (#259) 2024-08-27 14:14:17 +07:00
Marcus Schiesser f04f60d555 refactor: e2e tests (#256) 2024-08-26 11:39:15 +07:00
github-actions[bot] 1ffd3c915b Release 0.1.41 (#248)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-22 16:34:41 +07:00
Marcus Schiesser 57e7638083 feat: Use the retrieval defaults from LlamaCloud (#247) 2024-08-22 16:30:04 +07:00
Marcus Schiesser 22ac2cae61 fix: add progress for no vecdb for Python 2024-08-22 11:22:09 +07:00
github-actions[bot] 8077195601 Release 0.1.40 (#245)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-20 14:59:24 +07:00
Huu Le 8ce4a8513d feat: use Reflex UI for structured extract template (#209)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-08-20 14:33:23 +07:00
github-actions[bot] 1d93775f04 Release 0.1.39 (#243)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-19 16:33:07 +07:00
Thuc Pham 3fb93c7939 feat: use llamacloud pipeline in TS (#236)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-08-19 15:49:51 +07:00
github-actions[bot] e248dc56bc Release 0.1.38 (#242)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-16 10:58:56 +07:00
Marcus Schiesser bd5e39a390 fix: files in sub folders of 'data' are not displayed (#241) 2024-08-16 10:57:44 +07:00
github-actions[bot] de2c7523dd Release 0.1.37 (#239)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-15 14:52:27 +07:00
Huu Le 9fd832c8b0 feat: In-text citing (#175)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-08-15 13:52:51 +07:00
github-actions[bot] b2c76dc7b6 Release 0.1.36 (#238)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-15 11:02:00 +07:00
Thuc Pham 2b7a5d8797 fix: optional params in file upload API (#237)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-08-15 11:00:53 +07:00
Marcus Schiesser d93ec803f5 feat: add ruff (#235)
* fix: formatting

* fix: ruff --fix

* feat: add ruff to github action

* fix: remove E402 check for some files
2024-08-15 09:38:13 +07:00
github-actions[bot] a6023b695b Release 0.1.35 (#234)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-14 17:22:49 +07:00
Marcus Schiesser 81ef7f0f93 feat: use llamacloud pipeline for private files and generate script in Python (#226)
---------
Co-authored-by: Thuc Pham <51660321+thucpn@users.noreply.github.com>
2024-08-14 17:03:16 +07:00
github-actions[bot] 8faf9170cf Release 0.1.34 (#233)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-14 14:59:19 +07:00
Huu Le c49a5e1620 chore: update wrong env name, add error handling for next question (#232) 2024-08-14 14:39:14 +07:00
github-actions[bot] 8b2de431f2 Release 0.1.33 (#229)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-13 11:06:17 +07:00
Huu Le d746c75e49 feat: Add Weaviate vector store for Typescript templates (#228) 2024-08-13 10:56:02 +07:00
Laurie Voss c87978ab96 Point the repo to the current one (#227) 2024-08-13 10:51:04 +07:00
github-actions[bot] 26359a0ac9 Release 0.1.32 (#224)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-12 17:19:22 +07:00
Huu Le 4039d3d1ea refactor: include chat configuration router in FastAPI app (#225) 2024-08-12 17:17:22 +07:00
Huu Le 3ec5163304 feat: add Weaviate vector database support for Python (#223)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-08-12 16:25:26 +07:00
Thuc Pham 878cfc2ca1 refactor: make llamacloud selector resuable (#221) 2024-08-09 12:02:43 +07:00
github-actions[bot] 9b5835b71c Release 0.1.31 (#222)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-09 11:55:58 +07:00
Thuc Pham 04a9c71759 feat: cluster nodes in document (#217)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-08-09 11:54:50 +07:00
github-actions[bot] 0bfdbc1dfe Release 0.1.30 (#214)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-08 15:59:57 +07:00
Thuc Pham fbcaebcbcf fix: use modern module resolution for express (#219) 2024-08-08 09:51:13 +02:00
Thuc Pham b6dd7a9acb fix: always send chat data when submit message (#213) 2024-08-07 15:22:33 +02:00
Marcus Schiesser 09e3022ad6 feat: add LlamaTrace support (#216) 2024-08-07 15:21:44 +02:00
Marcus Schiesser 9f739b9834 refactor: cleaned e2e runner (#215) 2024-08-07 17:41:22 +07:00
Marcus Schiesser c06ec4f14c fix: imports for MongoDB 2024-08-07 11:01:00 +02:00
Marcus Schiesser e7d30b1c69 refactor: test frameworks and datasources via matrix (#211) 2024-08-05 23:50:00 +07:00
github-actions[bot] e974c8ef11 Release 0.1.29 (#210)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-05 22:24:37 +07:00
Thuc Pham 8890e27a14 feat: implement index selector for LlamaCloud (#200)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-08-05 22:18:20 +07:00
Marcus Schiesser 072e69b465 fix: deactive llamacloud tests 2024-08-05 13:49:09 +02:00
Huu Le 83a648df0a chore: add use window.ENV.BASE_URL as backendOrigin (#205)
---------
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2024-08-02 15:50:04 +07:00
176 changed files with 5449 additions and 1751 deletions
+4
View File
@@ -18,6 +18,8 @@ jobs:
node-version: [18, 20]
python-version: ["3.11"]
os: [macos-latest, windows-latest, ubuntu-22.04]
frameworks: ["nextjs", "express", "fastapi"]
datasources: ["--no-files", "--example-file"]
defaults:
run:
shell: bash
@@ -63,6 +65,8 @@ jobs:
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
FRAMEWORK: ${{ matrix.frameworks }}
DATASOURCE: ${{ matrix.datasources }}
working-directory: .
- uses: actions/upload-artifact@v3
@@ -30,3 +30,13 @@ jobs:
- name: Run Prettier
run: pnpm run format
- name: Run Python format check
uses: chartboost/ruff-action@v1
with:
args: "format --check"
- name: Run Python lint
uses: chartboost/ruff-action@v1
with:
args: "check"
+139
View File
@@ -1,5 +1,144 @@
# create-llama
## 0.2.5
### Patch Changes
- 38a8be8: fix: filter in mongo vector store
## 0.2.4
### Patch Changes
- 917e862: Fix errors in building the frontend
## 0.2.3
### Patch Changes
- b6da3c2: Ensure the generation script always works
## 0.2.2
### Patch Changes
- 8105c5c: Add env config for next questions feature
## 0.2.1
### Patch Changes
- 6a409cb: Bump web and database reader packages
## 0.2.0
### Minor Changes
- 435109f: Add multi-agents template based on workflows
## 0.1.44
### Patch Changes
- bedde2b: Change metadata filters to use already existing documents in LlamaCloud Index
- 5cd12fa: Use one callback manager per request
- 5cd12fa: Bump llama_index version to 0.11.1
- fd4abb3: Fix to use filename for uploaded documents in NextJS
- 2f8feab: Simplify CLI interface
## 0.1.43
### Patch Changes
- 4fa2b76: feat: implement citation for TS
## 0.1.42
### Patch Changes
- 8f670a9: Allow relative URL in documents
## 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
- 9fd832c: Add in-text citation references
## 0.1.36
### Patch Changes
- 2b7a5d8: Fix: private file upload not working in Python without LlamaCloud
## 0.1.35
### Patch Changes
- 81ef7f0: Use LlamaCloud pipeline for data ingestion (private file uploads and generate script)
## 0.1.34
### Patch Changes
- c49a5e1: Add error handling for generating the next question
- c49a5e1: Fix wrong api key variable in Azure OpenAI provider
## 0.1.33
### Patch Changes
- d746c75: Add Weaviate vector store (Typescript)
## 0.1.32
### Patch Changes
- 3ec5163: Add Weaviate vector database support (Python)
## 0.1.31
### Patch Changes
- 04a9c71: Cluster nodes by document
## 0.1.30
### Patch Changes
- 09e3022: Add support for LlamaTrace (Python)
- c06ec4f: Fix imports for MongoDB
- b6dd7a9: Always send chat data when submit message
## 0.1.29
### Patch Changes
- 8890e27: Let user change indexes in LlamaCloud projects
## 0.1.28
### Patch Changes
+1 -1
View File
@@ -94,7 +94,7 @@ Need to install the following packages:
create-llama@latest
Ok to proceed? (y) y
✔ What is your project named? … my-app
✔ Which template would you like to use? Agentic RAG (single agent)
✔ Which template would you like to use? Agentic RAG (e.g. chat with docs)
✔ Which framework would you like to use? NextJS
✔ Would you like to set up observability? No
✔ Please provide your OpenAI API key (leave blank to skip): …
+23 -9
View File
@@ -9,7 +9,7 @@ import { makeDir } from "./helpers/make-dir";
import fs from "fs";
import terminalLink from "terminal-link";
import type { InstallTemplateArgs } from "./helpers";
import type { InstallTemplateArgs, TemplateObservability } from "./helpers";
import { installTemplate } from "./helpers";
import { writeDevcontainer } from "./helpers/devcontainer";
import { templatesDir } from "./helpers/dir";
@@ -142,14 +142,7 @@ export async function createApp({
)} and learn how to get started.`,
);
if (args.observability === "opentelemetry") {
console.log(
`\n${yellow("Observability")}: Visit the ${terminalLink(
"documentation",
"https://traceloop.com/docs/openllmetry/integrations",
)} to set up the environment variables and start seeing execution traces.`,
);
}
outputObservability(args.observability);
if (
dataSources.some((dataSource) => dataSource.type === "file") &&
@@ -167,3 +160,24 @@ export async function createApp({
console.log();
}
function outputObservability(observability?: TemplateObservability) {
switch (observability) {
case "traceloop":
console.log(
`\n${yellow("Observability")}: Visit the ${terminalLink(
"documentation",
"https://traceloop.com/docs/openllmetry/integrations",
)} to set up the environment variables and start seeing execution traces.`,
);
break;
case "llamatrace":
console.log(
`\n${yellow("Observability")}: LlamaTrace has been configured for your project. Visit the ${terminalLink(
"LlamaTrace dashboard",
"https://llamatrace.com/login",
)} to view your traces and monitor your application.`,
);
break;
}
}
-138
View File
@@ -1,138 +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 templateTypes: TemplateType[] = ["streaming"];
const templateFrameworks: TemplateFramework[] = [
"nextjs",
"express",
"fastapi",
];
const dataSources: string[] = ["--no-files", "--llamacloud"];
const templateUIs: TemplateUI[] = ["shadcn", "html"];
const templatePostInstallActions: TemplatePostInstallAction[] = [
"none",
"runApp",
];
const llamaCloudProjectName = "create-llama";
const llamaCloudIndexName = "e2e-test";
for (const templateType of templateTypes) {
for (const templateFramework of templateFrameworks) {
for (const dataSource of dataSources) {
for (const templateUI of templateUIs) {
for (const templatePostInstallAction of templatePostInstallActions) {
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();
});
});
}
}
}
}
}
+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 } 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 !== "nextjs" &&
templateFramework !== "express" &&
dataSource !== "--no-files"
) {
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,
});
});
});
}
+85
View File
@@ -0,0 +1,85 @@
/* 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,
TemplateUI,
} from "../helpers";
import { createTestDir, runCreateLlama, type AppType } from "./utils";
const templateFramework: TemplateFramework = "fastapi";
const dataSource: string = "--example-file";
const templateUI: TemplateUI = "shadcn";
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
const appType: AppType = "--frontend";
const userMessage = "Write a blog post about physical standards for letters";
test.describe(`Test multiagent template ${templateFramework} ${dataSource} ${templateUI} ${appType} ${templatePostInstallAction}`, async () => {
test.skip(
process.platform !== "linux" ||
process.env.FRAMEWORK !== "fastapi" ||
process.env.DATASOURCE === "--no-files",
"The multiagent template currently only works with FastAPI and files. We also only run on Linux to speed up tests.",
);
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,
"multiagent",
templateFramework,
dataSource,
vectorDb,
port,
externalPort,
templatePostInstallAction,
templateUI,
appType,
);
name = result.projectName;
appProcess = result.appProcess;
});
test("App folder should exist", async () => {
const dirExists = fs.existsSync(path.join(cwd, name));
expect(dirExists).toBeTruthy();
});
test("Frontend should have a title", async ({ page }) => {
await page.goto(`http://localhost:${port}`);
await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
});
test("Frontend should be able to submit a message and receive the start of a streamed response", async ({
page,
}) => {
await page.goto(`http://localhost:${port}`);
await page.fill("form input", userMessage);
const responsePromise = page.waitForResponse((res) =>
res.url().includes("/api/chat"),
);
await page.click("form button[type=submit]");
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
});
// clean processes
test.afterAll(async () => {
appProcess?.kill();
});
});
+119
View File
@@ -0,0 +1,119 @@
/* 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,
TemplateUI,
} from "../helpers";
import { createTestDir, runCreateLlama, type AppType } 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 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(`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,
"streaming",
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();
});
});
+67 -68
View File
@@ -18,62 +18,20 @@ export type CreateLlamaResult = {
appProcess: ChildProcess;
};
// eslint-disable-next-line max-params
export async function checkAppHasStarted(
frontend: boolean,
framework: TemplateFramework,
port: number,
externalPort: number,
timeout: number,
) {
if (frontend) {
await Promise.all([
waitPort({
host: "localhost",
port: port,
timeout,
}),
waitPort({
host: "localhost",
port: externalPort,
timeout,
}),
]).catch((err) => {
console.error(err);
throw err;
});
} else {
let wPort: number;
if (framework === "nextjs") {
wPort = port;
} else {
wPort = externalPort;
}
await waitPort({
host: "localhost",
port: wPort,
timeout,
}).catch((err) => {
console.error(err);
throw err;
});
}
}
// eslint-disable-next-line max-params
export async function runCreateLlama(
cwd: string,
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(
@@ -87,7 +45,7 @@ export async function runCreateLlama(
templateUI,
appType,
].join("-");
const command = [
const commandArgs = [
"create-llama",
name,
"--template",
@@ -95,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,
@@ -116,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,
@@ -142,25 +106,10 @@ export async function runCreateLlama(
templateFramework,
port,
externalPort,
1000 * 60 * 5,
);
} else {
// wait create-llama to exit
// we don't test install dependencies for now, so just set timeout for 10 seconds
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("create-llama timeout error"));
}, 1000 * 10);
appProcess.on("exit", (code) => {
if (code !== 0 && code !== null) {
clearTimeout(timeout);
reject(new Error("create-llama command was failed!"));
} else {
clearTimeout(timeout);
resolve(undefined);
}
});
});
// wait 10 seconds for create-llama to exit
await waitForProcess(appProcess, 1000 * 10);
}
return {
@@ -174,3 +123,53 @@ export async function createTestDir() {
await mkdir(cwd, { recursive: true });
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({
host: "localhost",
port: port,
// wait max. 5 mins for start up of app
timeout: 1000 * 60 * 5,
});
};
try {
await Promise.all(ports.map(waitForPort));
} catch (err) {
console.error(err);
throw err;
}
}
async function waitForProcess(
process: ChildProcess,
timeoutMs: number,
): Promise<void> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("Process timeout error"));
}, timeoutMs);
process.on("exit", (code) => {
clearTimeout(timeout);
if (code !== 0 && code !== null) {
reject(new Error("Process exited with non-zero code"));
} else {
resolve();
}
});
});
}
+52 -60
View File
@@ -36,74 +36,66 @@ export async function writeLoadersConfig(
dataSources: TemplateDataSource[],
useLlamaParse?: boolean,
) {
if (dataSources.length === 0) return; // no datasources, no config needed
const loaderConfig = new Document({});
// Web loader config
const loaderConfig: Record<string, any> = {};
// Always set file loader config
loaderConfig.file = createFileLoaderConfig(useLlamaParse);
if (dataSources.some((ds) => ds.type === "web")) {
const webLoaderConfig = new Document({});
// Create config for browser driver arguments
const driverArgNodeValue = webLoaderConfig.createNode([
"--no-sandbox",
"--disable-dev-shm-usage",
]);
driverArgNodeValue.commentBefore =
" The arguments to pass to the webdriver. E.g.: add --headless to run in headless mode";
webLoaderConfig.set("driver_arguments", driverArgNodeValue);
// Create config for urls
const urlConfigs = dataSources
.filter((ds) => ds.type === "web")
.map((ds) => {
const dsConfig = ds.config as WebSourceConfig;
return {
base_url: dsConfig.baseUrl,
prefix: dsConfig.prefix,
depth: dsConfig.depth,
};
});
const urlConfigNode = webLoaderConfig.createNode(urlConfigs);
urlConfigNode.commentBefore = ` base_url: The URL to start crawling with
prefix: Only crawl URLs matching the specified prefix
depth: The maximum depth for BFS traversal
You can add more websites by adding more entries (don't forget the - prefix from YAML)`;
webLoaderConfig.set("urls", urlConfigNode);
// Add web config to the loaders config
loaderConfig.set("web", webLoaderConfig);
loaderConfig.web = createWebLoaderConfig(dataSources);
}
// File loader config
if (dataSources.some((ds) => ds.type === "file")) {
// Add documentation to web loader config
const node = loaderConfig.createNode({
use_llama_parse: useLlamaParse,
});
node.commentBefore = ` use_llama_parse: Use LlamaParse if \`true\`. Needs a \`LLAMA_CLOUD_API_KEY\` from https://cloud.llamaindex.ai set as environment variable`;
loaderConfig.set("file", node);
}
// DB loader config
const dbLoaders = dataSources.filter((ds) => ds.type === "db");
if (dbLoaders.length > 0) {
const dbLoaderConfig = new Document({});
const configEntries = dbLoaders.map((ds) => {
const dsConfig = ds.config as DbSourceConfig;
return {
uri: dsConfig.uri,
queries: [dsConfig.queries],
};
});
const node = dbLoaderConfig.createNode(configEntries);
node.commentBefore = ` The configuration for the database loader, only supports MySQL and PostgreSQL databases for now.
uri: The URI for the database. E.g.: mysql+pymysql://user:password@localhost:3306/db or postgresql+psycopg2://user:password@localhost:5432/db
query: The query to fetch data from the database. E.g.: SELECT * FROM table`;
loaderConfig.set("db", node);
loaderConfig.db = createDbLoaderConfig(dbLoaders);
}
// Create a new Document with the loaderConfig
const yamlDoc = new Document(loaderConfig);
// Write loaders config
const loaderConfigPath = path.join(root, "config", "loaders.yaml");
await fs.mkdir(path.join(root, "config"), { recursive: true });
await fs.writeFile(loaderConfigPath, yaml.stringify(loaderConfig));
await fs.writeFile(loaderConfigPath, yaml.stringify(yamlDoc));
}
function createWebLoaderConfig(dataSources: TemplateDataSource[]): any {
const webLoaderConfig: Record<string, any> = {};
// Create config for browser driver arguments
webLoaderConfig.driver_arguments = [
"--no-sandbox",
"--disable-dev-shm-usage",
];
// Create config for urls
const urlConfigs = dataSources
.filter((ds) => ds.type === "web")
.map((ds) => {
const dsConfig = ds.config as WebSourceConfig;
return {
base_url: dsConfig.baseUrl,
prefix: dsConfig.prefix,
depth: dsConfig.depth,
};
});
webLoaderConfig.urls = urlConfigs;
return webLoaderConfig;
}
function createFileLoaderConfig(useLlamaParse?: boolean): any {
return {
use_llama_parse: useLlamaParse,
};
}
function createDbLoaderConfig(dbLoaders: TemplateDataSource[]): any {
return dbLoaders.map((ds) => {
const dsConfig = ds.config as DbSourceConfig;
return {
uri: dsConfig.uri,
queries: [dsConfig.queries],
};
});
}
+131 -47
View File
@@ -2,9 +2,11 @@ import fs from "fs/promises";
import path from "path";
import { TOOL_SYSTEM_PROMPT_ENV_VAR, Tool } from "./tools";
import {
InstallTemplateArgs,
ModelConfig,
TemplateDataSource,
TemplateFramework,
TemplateObservability,
TemplateType,
TemplateVectorDB,
} from "./types";
@@ -158,8 +160,19 @@ 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)
[
{
name: "NEXT_PUBLIC_USE_LLAMACLOUD",
description:
"Let's the user change indexes in LlamaCloud projects",
value: "true",
},
]
: []),
];
case "chroma":
const envs = [
@@ -186,6 +199,23 @@ Otherwise, use CHROMA_HOST and CHROMA_PORT config above`,
});
}
return envs;
case "weaviate":
return [
{
name: "WEAVIATE_CLUSTER_URL",
description:
"The URL of the Weaviate cloud cluster, see: https://weaviate.io/developers/wcs/connect",
},
{
name: "WEAVIATE_API_KEY",
description: "The API key for the Weaviate cloud cluster",
},
{
name: "WEAVIATE_INDEX_NAME",
description:
"(Optional) The collection name to use, default is LlamaIndex if not specified",
},
];
default:
return [];
}
@@ -282,7 +312,7 @@ const getModelEnvs = (modelConfig: ModelConfig): EnvVar[] => {
...(modelConfig.provider === "azure-openai"
? [
{
name: "AZURE_OPENAI_KEY",
name: "AZURE_OPENAI_API_KEY",
description: "The Azure OpenAI key to use.",
value: modelConfig.apiKey,
},
@@ -366,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",
@@ -394,7 +423,11 @@ const getToolEnvs = (tools?: Tool[]): EnvVar[] => {
return toolEnvs;
};
const getSystemPromptEnv = (tools?: Tool[]): EnvVar => {
const getSystemPromptEnv = (
tools?: Tool[],
dataSources?: TemplateDataSource[],
framework?: TemplateFramework,
): EnvVar[] => {
const defaultSystemPrompt =
"You are a helpful assistant who helps users with their questions.";
@@ -413,55 +446,102 @@ const getSystemPromptEnv = (tools?: Tool[]): EnvVar => {
? `\"${toolSystemPrompt}\"`
: defaultSystemPrompt;
return {
name: "SYSTEM_PROMPT",
description: "The system prompt for the AI model.",
value: systemPrompt,
};
const systemPromptEnv = [
{
name: "SYSTEM_PROMPT",
description: "The system prompt for the AI model.",
value: systemPrompt,
},
];
if (tools?.length == 0 && (dataSources?.length ?? 0 > 0)) {
const citationPrompt = `'You have provided information from a knowledge base that has been passed to you in nodes of information.
Each node has useful metadata such as node ID, file name, page, etc.
Please add the citation to the data node for each sentence or paragraph that you reference in the provided information.
The citation format is: . [citation:<node_id>]()
Where the <node_id> is the unique identifier of the data node.
Example:
We have two nodes:
node_id: xyz
file_name: llama.pdf
node_id: abc
file_name: animal.pdf
User question: Tell me a fun fact about Llama.
Your answer:
A baby llama is called "Cria" [citation:xyz]().
It often live in desert [citation:abc]().
It\\'s cute animal.
'`;
systemPromptEnv.push({
name: "SYSTEM_CITATION_PROMPT",
description:
"An additional system prompt to add citation when responding to user questions.",
value: citationPrompt,
});
}
return systemPromptEnv;
};
const getTemplateEnvs = (template?: TemplateType): EnvVar[] => {
if (template === "multiagent") {
const nextQuestionEnvs: EnvVar[] = [
{
name: "NEXT_QUESTION_PROMPT",
description: `Customize prompt to generate the next question suggestions based on the conversation history.
Disable this prompt to disable the next question suggestions feature.`,
value: `"You're a helpful assistant! Your task is to suggest the next question that user might ask.
Here is the conversation history
---------------------
{conversation}
---------------------
Given the conversation history, please give me 3 questions that you might ask next!
Your answer should be wrapped in three sticks which follows the following format:
\`\`\`
<question 1>
<question 2>
<question 3>
\`\`\`"`,
},
];
if (template === "multiagent" || template === "streaming") {
return nextQuestionEnvs;
}
return [];
};
const getObservabilityEnvs = (
observability?: TemplateObservability,
): EnvVar[] => {
if (observability === "llamatrace") {
return [
{
name: "MESSAGE_QUEUE_PORT",
},
{
name: "CONTROL_PLANE_PORT",
},
{
name: "HUMAN_CONSUMER_PORT",
},
{
name: "AGENT_QUERY_ENGINE_PORT",
value: "8003",
},
{
name: "AGENT_QUERY_ENGINE_DESCRIPTION",
value: "Query information from the provided data",
},
{
name: "AGENT_DUMMY_PORT",
value: "8004",
name: "PHOENIX_API_KEY",
description:
"API key for LlamaTrace observability. Retrieve from https://llamatrace.com/login",
},
];
} else {
return [];
}
return [];
};
export const createBackendEnvFile = async (
root: string,
opts: {
llamaCloudKey?: string;
vectorDb?: TemplateVectorDB;
modelConfig: ModelConfig;
framework: TemplateFramework;
dataSources?: TemplateDataSource[];
template?: TemplateType;
port?: number;
tools?: Tool[];
},
opts: Pick<
InstallTemplateArgs,
| "llamaCloudKey"
| "vectorDb"
| "modelConfig"
| "framework"
| "dataSources"
| "template"
| "externalPort"
| "tools"
| "observability"
>,
) => {
// Init env values
const envFileName = ".env";
@@ -471,17 +551,15 @@ export const createBackendEnvFile = async (
description: `The Llama Cloud API key.`,
value: opts.llamaCloudKey,
},
// Add model environment variables
// Add environment variables of each component
...getModelEnvs(opts.modelConfig),
// Add engine environment variables
...getEngineEnvs(),
// Add vector database environment variables
...getVectorDBEnvs(opts.vectorDb, opts.framework),
...getFrameworkEnvs(opts.framework, opts.port),
...getFrameworkEnvs(opts.framework, opts.externalPort),
...getToolEnvs(opts.tools),
// Add template environment variables
...getTemplateEnvs(opts.template),
getSystemPromptEnv(opts.tools),
...getObservabilityEnvs(opts.observability),
...getSystemPromptEnv(opts.tools, opts.dataSources, opts.framework),
];
// Render and write env file
const content = renderEnvVar(envVars);
@@ -493,6 +571,7 @@ export const createFrontendEnvFile = async (
root: string,
opts: {
customApiPath?: string;
vectorDb?: TemplateVectorDB;
},
) => {
const defaultFrontendEnvs = [
@@ -503,6 +582,11 @@ export const createFrontendEnvFile = async (
? opts.customApiPath
: "http://localhost:8000/api/chat",
},
{
name: "NEXT_PUBLIC_USE_LLAMACLOUD",
description: "Let's the user change indexes in LlamaCloud projects",
value: opts.vectorDb === "llamacloud" ? "true" : "false",
},
];
const content = renderEnvVar(defaultFrontendEnvs);
await fs.writeFile(path.join(root, ".env"), content);
+30 -34
View File
@@ -96,10 +96,11 @@ async function generateContextData(
}
}
const copyContextData = async (
const prepareContextData = async (
root: string,
dataSources: TemplateDataSource[],
) => {
await makeDir(path.join(root, "data"));
for (const dataSource of dataSources) {
const dataSourceConfig = dataSource?.config as FileSourceConfig;
// Copy local data
@@ -142,12 +143,15 @@ export const installTemplate = async (
if (props.framework === "fastapi") {
await installPythonTemplate(props);
// write loaders configuration (currently Python only)
await writeLoadersConfig(
props.root,
props.dataSources,
props.useLlamaParse,
);
if (props.vectorDb !== "llamacloud") {
// write loaders configuration (currently Python only)
// not needed for LlamaCloud as it has its own loaders
await writeLoadersConfig(
props.root,
props.dataSources,
props.useLlamaParse,
);
}
} else {
await installTSTemplate(props);
}
@@ -168,37 +172,28 @@ export const installTemplate = async (
props.template === "multiagent" ||
props.template === "extractor"
) {
await createBackendEnvFile(props.root, {
modelConfig: props.modelConfig,
llamaCloudKey: props.llamaCloudKey,
vectorDb: props.vectorDb,
framework: props.framework,
dataSources: props.dataSources,
port: props.externalPort,
tools: props.tools,
template: props.template,
});
await createBackendEnvFile(props.root, props);
}
if (props.dataSources.length > 0) {
await prepareContextData(
props.root,
props.dataSources.filter((ds) => ds.type === "file"),
);
if (
props.dataSources.length > 0 &&
(props.postInstallAction === "runApp" ||
props.postInstallAction === "dependencies")
) {
console.log("\nGenerating context data...\n");
await copyContextData(
props.root,
props.dataSources.filter((ds) => ds.type === "file"),
await generateContextData(
props.framework,
props.modelConfig,
props.packageManager,
props.vectorDb,
props.llamaCloudKey,
props.useLlamaParse,
);
if (
props.postInstallAction === "runApp" ||
props.postInstallAction === "dependencies"
) {
await generateContextData(
props.framework,
props.modelConfig,
props.packageManager,
props.vectorDb,
props.llamaCloudKey,
props.useLlamaParse,
);
}
}
// Create outputs directory
@@ -209,6 +204,7 @@ export const installTemplate = async (
// 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,
});
}
};
+4 -4
View File
@@ -9,6 +9,7 @@ const ALL_AZURE_OPENAI_CHAT_MODELS: Record<string, { openAIModel: string }> = {
openAIModel: "gpt-3.5-turbo-16k",
},
"gpt-4o": { openAIModel: "gpt-4o" },
"gpt-4o-mini": { openAIModel: "gpt-4o-mini" },
"gpt-4": { openAIModel: "gpt-4" },
"gpt-4-32k": { openAIModel: "gpt-4-32k" },
"gpt-4-turbo": {
@@ -26,6 +27,9 @@ const ALL_AZURE_OPENAI_CHAT_MODELS: Record<string, { openAIModel: string }> = {
"gpt-4o-2024-05-13": {
openAIModel: "gpt-4o-2024-05-13",
},
"gpt-4o-mini-2024-07-18": {
openAIModel: "gpt-4o-mini-2024-07-18",
},
};
const ALL_AZURE_OPENAI_EMBEDDING_MODELS: Record<
@@ -35,10 +39,6 @@ const ALL_AZURE_OPENAI_EMBEDDING_MODELS: Record<
openAIModel: string;
}
> = {
"text-embedding-ada-002": {
dimensions: 1536,
openAIModel: "text-embedding-ada-002",
},
"text-embedding-3-small": {
dimensions: 1536,
openAIModel: "text-embedding-3-small",
+92 -35
View File
@@ -12,6 +12,7 @@ import {
InstallTemplateArgs,
ModelConfig,
TemplateDataSource,
TemplateType,
TemplateVectorDB,
} from "./types";
@@ -26,6 +27,7 @@ const getAdditionalDependencies = (
vectorDb?: TemplateVectorDB,
dataSources?: TemplateDataSource[],
tools?: Tool[],
templateType?: TemplateType,
) => {
const dependencies: Dependency[] = [];
@@ -84,6 +86,13 @@ const getAdditionalDependencies = (
});
break;
}
case "weaviate": {
dependencies.push({
name: "llama-index-vector-stores-weaviate",
version: "^1.0.2",
});
break;
}
}
// Add data source dependencies
@@ -100,13 +109,13 @@ const getAdditionalDependencies = (
case "web":
dependencies.push({
name: "llama-index-readers-web",
version: "^0.1.6",
version: "^0.2.2",
});
break;
case "db":
dependencies.push({
name: "llama-index-readers-database",
version: "^0.1.3",
version: "^0.2.0",
});
dependencies.push({
name: "pymysql",
@@ -121,7 +130,7 @@ const getAdditionalDependencies = (
case "llamacloud":
dependencies.push({
name: "llama-index-indices-managed-llama-cloud",
version: "^0.2.7",
version: "^0.3.0",
});
break;
}
@@ -140,77 +149,99 @@ const getAdditionalDependencies = (
case "ollama":
dependencies.push({
name: "llama-index-llms-ollama",
version: "0.1.2",
version: "0.3.0",
});
dependencies.push({
name: "llama-index-embeddings-ollama",
version: "0.1.2",
version: "0.3.0",
});
break;
case "openai":
dependencies.push({
name: "llama-index-agent-openai",
version: "0.2.6",
});
if (templateType !== "multiagent") {
dependencies.push({
name: "llama-index-llms-openai",
version: "^0.2.0",
});
dependencies.push({
name: "llama-index-embeddings-openai",
version: "^0.2.3",
});
dependencies.push({
name: "llama-index-agent-openai",
version: "^0.3.0",
});
}
break;
case "groq":
// Fastembed==0.2.0 does not support python3.13 at the moment
// Fixed the python version less than 3.13
dependencies.push({
name: "python",
version: "^3.11,<3.13",
});
dependencies.push({
name: "llama-index-llms-groq",
version: "0.1.4",
version: "0.2.0",
});
dependencies.push({
name: "llama-index-embeddings-fastembed",
version: "^0.1.4",
version: "^0.2.0",
});
break;
case "anthropic":
// Fastembed==0.2.0 does not support python3.13 at the moment
// Fixed the python version less than 3.13
dependencies.push({
name: "python",
version: "^3.11,<3.13",
});
dependencies.push({
name: "llama-index-llms-anthropic",
version: "0.1.10",
version: "0.3.0",
});
dependencies.push({
name: "llama-index-embeddings-fastembed",
version: "^0.1.4",
version: "^0.2.0",
});
break;
case "gemini":
dependencies.push({
name: "llama-index-llms-gemini",
version: "0.1.10",
version: "0.3.4",
});
dependencies.push({
name: "llama-index-embeddings-gemini",
version: "0.1.6",
version: "^0.2.0",
});
break;
case "mistral":
dependencies.push({
name: "llama-index-llms-mistralai",
version: "0.1.17",
version: "0.2.1",
});
dependencies.push({
name: "llama-index-embeddings-mistralai",
version: "0.1.4",
version: "0.2.0",
});
break;
case "azure-openai":
dependencies.push({
name: "llama-index-llms-azure-openai",
version: "0.1.10",
version: "0.2.0",
});
dependencies.push({
name: "llama-index-embeddings-azure-openai",
version: "0.1.11",
version: "0.2.4",
});
break;
case "t-systems":
dependencies.push({
name: "llama-index-agent-openai",
version: "0.2.2",
version: "0.3.0",
});
dependencies.push({
name: "llama-index-llms-openai-like",
version: "0.1.3",
version: "0.2.0",
});
break;
}
@@ -220,7 +251,7 @@ const getAdditionalDependencies = (
const mergePoetryDependencies = (
dependencies: Dependency[],
existingDependencies: Record<string, Omit<Dependency, "name">>,
existingDependencies: Record<string, Omit<Dependency, "name"> | string>,
) => {
for (const dependency of dependencies) {
let value = existingDependencies[dependency.name] ?? {};
@@ -239,7 +270,13 @@ const mergePoetryDependencies = (
);
}
existingDependencies[dependency.name] = value;
// Serialize separately only if extras are provided
if (value.extras && value.extras.length > 0) {
existingDependencies[dependency.name] = value;
} else {
// Otherwise, serialize just the version string
existingDependencies[dependency.name] = value.version;
}
}
};
@@ -343,18 +380,28 @@ export const installPythonTemplate = async ({
cwd: path.join(compPath, "vectordbs", "python", vectorDb ?? "none"),
});
// Copy all loaders to enginePath
const loaderPath = path.join(enginePath, "loaders");
await copy("**", loaderPath, {
parents: true,
cwd: path.join(compPath, "loaders", "python"),
});
if (vectorDb !== "llamacloud") {
// Copy all loaders to enginePath
// Not needed for LlamaCloud as it has its own loaders
const loaderPath = path.join(enginePath, "loaders");
await copy("**", loaderPath, {
parents: true,
cwd: path.join(compPath, "loaders", "python"),
});
}
// Copy settings.py to app
await copy("**", path.join(root, "app"), {
cwd: path.join(compPath, "settings", "python"),
});
// Copy services
if (template == "streaming" || template == "multiagent") {
await copy("**", path.join(root, "app", "api", "services"), {
cwd: path.join(compPath, "services", "python"),
});
}
if (template === "streaming") {
// For the streaming template only:
// Select and copy engine code based on data sources and tools
@@ -378,20 +425,30 @@ export const installPythonTemplate = async ({
vectorDb,
dataSources,
tools,
template,
);
if (observability === "opentelemetry") {
addOnDependencies.push({
name: "traceloop-sdk",
version: "^0.15.11",
});
if (observability && observability !== "none") {
if (observability === "traceloop") {
addOnDependencies.push({
name: "traceloop-sdk",
version: "^0.15.11",
});
}
if (observability === "llamatrace") {
addOnDependencies.push({
name: "llama-index-callbacks-arize-phoenix",
version: "^0.1.6",
});
}
const templateObservabilityPath = path.join(
templatesDir,
"components",
"observability",
"python",
"opentelemetry",
observability,
);
await copy("**", path.join(root, "app"), {
cwd: templateObservabilityPath,
+59 -48
View File
@@ -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" || 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));
}
}
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);
}
+4 -4
View File
@@ -41,7 +41,7 @@ export const supportedTools: Tool[] = [
dependencies: [
{
name: "llama-index-tools-google",
version: "0.1.2",
version: "^0.2.0",
},
],
supportedFrameworks: ["fastapi"],
@@ -83,7 +83,7 @@ For better results, you can specify the region parameter to get results from a s
dependencies: [
{
name: "llama-index-tools-wikipedia",
version: "0.1.2",
version: "^0.2.0",
},
],
supportedFrameworks: ["fastapi", "express", "nextjs"],
@@ -145,7 +145,7 @@ For better results, you can specify the region parameter to get results from a s
dependencies: [
{
name: "llama-index-tools-openapi",
version: "0.1.3",
version: "0.2.0",
},
{
name: "jsonschema",
@@ -153,7 +153,7 @@ For better results, you can specify the region parameter to get results from a s
},
{
name: "llama-index-tools-requests",
version: "0.1.3",
version: "0.2.0",
},
],
config: {
+3 -2
View File
@@ -35,7 +35,8 @@ export type TemplateVectorDB =
| "astra"
| "qdrant"
| "chroma"
| "llamacloud";
| "llamacloud"
| "weaviate";
export type TemplatePostInstallAction =
| "none"
| "VSCode"
@@ -46,7 +47,7 @@ export type TemplateDataSource = {
config: TemplateDataSourceConfig;
};
export type TemplateDataSourceType = "file" | "web" | "db" | "llamacloud";
export type TemplateObservability = "none" | "opentelemetry";
export type TemplateObservability = "none" | "traceloop" | "llamatrace";
// Config for both file and folder
export type FileSourceConfig = {
path: string;
+4 -3
View File
@@ -33,7 +33,8 @@ export const installTSTemplate = async ({
* Copy the template files to the target directory.
*/
console.log("\nInitializing project with template:", template, "\n");
const templatePath = path.join(templatesDir, "types", template, framework);
const type = template === "multiagent" ? "streaming" : template; // use nextjs streaming template for multiagent
const templatePath = path.join(templatesDir, "types", type, framework);
const copySource = ["**"];
await copy(copySource, root, {
@@ -70,7 +71,7 @@ export const installTSTemplate = async ({
);
const webpackConfigOtelFile = path.join(root, "webpack.config.o11y.mjs");
if (observability === "opentelemetry") {
if (observability === "traceloop") {
const webpackConfigDefaultFile = path.join(root, "webpack.config.mjs");
await fs.rm(webpackConfigDefaultFile);
await fs.rename(webpackConfigOtelFile, webpackConfigDefaultFile);
@@ -248,7 +249,7 @@ async function updatePackageJson({
};
}
if (observability === "opentelemetry") {
if (observability === "traceloop") {
packageJson.dependencies = {
...packageJson.dependencies,
"@traceloop/node-server-sdk": "^0.5.19",
+14 -2
View File
@@ -173,7 +173,14 @@ const program = new Commander.Command(packageJson.name)
"--ask-models",
`
Select LLM and embedding models.
Allow interactive selection of LLM and embedding models of different model providers.
`,
)
.option(
"--ask-examples",
`
Allow interactive selection of community templates and LlamaPacks.
`,
)
.allowUnknownOption()
@@ -188,10 +195,14 @@ 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");
program.askExamples = process.argv.includes("--ask-examples");
if (process.argv.includes("--no-files")) {
program.dataSources = [];
} else if (process.argv.includes("--example-file")) {
@@ -341,6 +352,7 @@ Please check ${cyan(
console.log(`Running app in ${root}...`);
await runApp(
root,
program.template,
program.frontend,
program.framework,
program.port,
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "create-llama",
"version": "0.1.28",
"version": "0.2.5",
"description": "Create LlamaIndex-powered apps with one command",
"keywords": [
"rag",
@@ -9,7 +9,7 @@
],
"repository": {
"type": "git",
"url": "https://github.com/run-llama/LlamaIndexTS",
"url": "https://github.com/run-llama/create-llama",
"directory": "packages/create-llama"
},
"license": "MIT",
+54 -37
View File
@@ -28,6 +28,7 @@ export type QuestionArgs = Omit<
"appPath" | "packageManager"
> & {
askModels?: boolean;
askExamples?: boolean;
};
const supportedContextFileTypes = [
".pdf",
@@ -103,6 +104,7 @@ const getVectorDbChoices = (framework: TemplateFramework) => {
{ title: "Astra", value: "astra" },
{ title: "Qdrant", value: "qdrant" },
{ title: "ChromaDB", value: "chroma" },
{ title: "Weaviate", value: "weaviate" },
];
const vectordbLang = framework === "fastapi" ? "python" : "typescript";
@@ -171,7 +173,7 @@ export const getDataSourceChoices = (
);
}
if (framework === "fastapi") {
if (framework === "fastapi" && template !== "extractor") {
choices.push({
title: "Use website content (requires Chrome)",
value: "web",
@@ -182,7 +184,7 @@ export const getDataSourceChoices = (
});
}
if (!selectedDataSource.length) {
if (!selectedDataSource.length && template !== "extractor") {
choices.push({
title: "Use managed index from LlamaCloud",
value: "llamacloud",
@@ -285,27 +287,25 @@ export const askQuestions = async (
},
];
if (program.template !== "multiagent") {
const modelConfigured =
!program.llamapack && program.modelConfig.isConfigured();
// If using LlamaParse, require LlamaCloud API key
const llamaCloudKeyConfigured = program.useLlamaParse
? program.llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
: true;
const hasVectorDb = program.vectorDb && program.vectorDb !== "none";
// Can run the app if all tools do not require configuration
if (
!hasVectorDb &&
modelConfigured &&
llamaCloudKeyConfigured &&
!toolsRequireConfig(program.tools)
) {
actionChoices.push({
title:
"Generate code, install dependencies, and run the app (~2 min)",
value: "runApp",
});
}
const modelConfigured =
!program.llamapack && program.modelConfig.isConfigured();
// If using LlamaParse, require LlamaCloud API key
const llamaCloudKeyConfigured = program.useLlamaParse
? program.llamaCloudKey || process.env["LLAMA_CLOUD_API_KEY"]
: true;
const hasVectorDb = program.vectorDb && program.vectorDb !== "none";
// Can run the app if all tools do not require configuration
if (
!hasVectorDb &&
modelConfigured &&
llamaCloudKeyConfigured &&
!toolsRequireConfig(program.tools)
) {
actionChoices.push({
title:
"Generate code, install dependencies, and run the app (~2 min)",
value: "runApp",
});
}
const { action } = await prompts(
@@ -337,20 +337,24 @@ export const askQuestions = async (
name: "template",
message: "Which template would you like to use?",
choices: [
{ title: "Agentic RAG (single agent)", value: "streaming" },
{ title: "Agentic RAG (e.g. chat with docs)", value: "streaming" },
{
title: "Multi-agent app (using llama-agents)",
title: "Multi-agent app (using workflows)",
value: "multiagent",
},
{ title: "Structured Extractor", value: "extractor" },
{
title: `Community template from ${styledRepo}`,
value: "community",
},
{
title: "Example using a LlamaPack",
value: "llamapack",
},
...(program.askExamples
? [
{
title: `Community template from ${styledRepo}`,
value: "community",
},
{
title: "Example using a LlamaPack",
value: "llamapack",
},
]
: []),
],
initial: 0,
},
@@ -406,9 +410,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) {
@@ -437,7 +446,7 @@ export const askQuestions = async (
if (
(program.framework === "express" || program.framework === "fastapi") &&
program.template === "streaming"
(program.template === "streaming" || program.template === "multiagent")
) {
// if a backend-only framework is selected, ask whether we should create a frontend
if (program.frontend === undefined) {
@@ -486,7 +495,10 @@ export const askQuestions = async (
message: "Would you like to set up observability?",
choices: [
{ title: "No", value: "none" },
{ title: "OpenTelemetry", value: "opentelemetry" },
...(program.framework === "fastapi"
? [{ title: "LlamaTrace", value: "llamatrace" }]
: []),
{ title: "Traceloop", value: "traceloop" },
],
initial: 0,
},
@@ -625,6 +637,7 @@ export const askQuestions = async (
type: "db",
config: await prompts(dbPrompts, questionHandlers),
});
break;
}
case "llamacloud": {
program.dataSources.push({
@@ -648,7 +661,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) {
@@ -1,21 +1,25 @@
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.index import IndexConfig, get_index
from app.engine.tools import ToolFactory
from app.engine.index import get_index
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.query_engine import QueryEngineTool
def get_chat_engine(filters=None):
def get_chat_engine(filters=None, params=None, event_handlers=None):
system_prompt = os.getenv("SYSTEM_PROMPT")
top_k = os.getenv("TOP_K", "3")
top_k = int(os.getenv("TOP_K", 0))
tools = []
callback_manager = CallbackManager(handlers=event_handlers or [])
# Add query tool if index exists
index = get_index()
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(
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)
@@ -27,5 +31,6 @@ def get_chat_engine(filters=None):
llm=Settings.llm,
tools=tools,
system_prompt=system_prompt,
callback_manager=callback_manager,
verbose=True,
)
@@ -1,8 +1,6 @@
import os
import yaml
import json
import importlib
from cachetools import cached, LRUCache
from llama_index.core.tools.tool_spec.base import BaseToolSpec
from llama_index.core.tools.function_tool import FunctionTool
@@ -13,7 +11,6 @@ class ToolType:
class ToolFactory:
TOOL_SOURCE_PACKAGE_MAP = {
ToolType.LLAMAHUB: "llama_index.tools",
ToolType.LOCAL: "app.engine.tools",
@@ -3,7 +3,7 @@ import logging
import base64
import uuid
from pydantic import BaseModel
from typing import List, Tuple, Dict, Optional
from typing import List, Dict, Optional
from llama_index.core.tools import FunctionTool
from e2b_code_interpreter import CodeInterpreter
from e2b_code_interpreter.models import Logs
@@ -26,7 +26,6 @@ class E2BToolOutput(BaseModel):
class E2BCodeInterpreter:
output_dir = "output/tool"
def __init__(self, api_key: str = None):
@@ -0,0 +1,48 @@
import os
from app.engine.index import IndexConfig, get_index
from app.engine.node_postprocessors import NodeCitationProcessor
from fastapi import HTTPException
from llama_index.core.callbacks import CallbackManager
from llama_index.core.chat_engine import CondensePlusContextChatEngine
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):
system_prompt = os.getenv("SYSTEM_PROMPT")
citation_prompt = os.getenv("SYSTEM_CITATION_PROMPT", None)
top_k = int(os.getenv("TOP_K", 0))
llm = Settings.llm
memory = ChatMemoryBuffer.from_defaults(
token_limit=llm.metadata.context_window - 256
)
callback_manager = CallbackManager(handlers=event_handlers or [])
node_postprocessors = []
if citation_prompt:
node_postprocessors = [NodeCitationProcessor()]
system_prompt = f"{system_prompt}\n{citation_prompt}"
index_config = IndexConfig(callback_manager=callback_manager, **(params or {}))
index = get_index(index_config)
if index is None:
raise HTTPException(
status_code=500,
detail=str(
"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 {})
)
return CondensePlusContextChatEngine(
llm=llm,
memory=memory,
system_prompt=system_prompt,
retriever=retriever,
node_postprocessors=node_postprocessors,
callback_manager=callback_manager,
)
@@ -0,0 +1,21 @@
from typing import List, Optional
from llama_index.core import QueryBundle
from llama_index.core.postprocessor.types import BaseNodePostprocessor
from llama_index.core.schema import NodeWithScore
class NodeCitationProcessor(BaseNodePostprocessor):
"""
Append node_id into metadata for citation purpose.
Config SYSTEM_CITATION_PROMPT in your runtime environment variable to enable this feature.
"""
def _postprocess_nodes(
self,
nodes: List[NodeWithScore],
query_bundle: Optional[QueryBundle] = None,
) -> List[NodeWithScore]:
for node_score in nodes:
node_score.node.metadata["node_id"] = node_score.node.node_id
return nodes
@@ -1,21 +1,16 @@
import {
BaseToolWithCall,
MetadataFilter,
MetadataFilters,
OpenAIAgent,
QueryEngineTool,
} from "llamaindex";
import { BaseToolWithCall, OpenAIAgent, QueryEngineTool } 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";
export async function createChatEngine(documentIds?: string[]) {
export async function createChatEngine(documentIds?: string[], params?: any) {
const tools: BaseToolWithCall[] = [];
// Add a query engine tool if we have a data source
// Delete this code if you don't have a data source
const index = await getDataSource();
const index = await getDataSource(params);
if (index) {
tools.push(
new QueryEngineTool({
@@ -47,27 +42,3 @@ export async function createChatEngine(documentIds?: string[]) {
systemPrompt: process.env.SYSTEM_PROMPT,
});
}
export function generateFilters(documentIds: string[]): MetadataFilters {
// public documents don't have the "private" field or it's set to "false"
const publicDocumentsFilter: MetadataFilter = {
key: "private",
value: ["true"],
operator: "nin",
};
// if no documentIds are provided, only retrieve information from public documents
if (!documentIds.length) return { filters: [publicDocumentsFilter] };
const privateDocumentsFilter: MetadataFilter = {
key: "doc_id",
value: documentIds,
operator: "in",
};
// if documentIds are provided, retrieve information from public and private documents
return {
filters: [publicDocumentsFilter, privateDocumentsFilter],
condition: "or",
};
}
@@ -1,50 +1,32 @@
import {
ContextChatEngine,
MetadataFilter,
MetadataFilters,
Settings,
} from "llamaindex";
import { ContextChatEngine, Settings } from "llamaindex";
import { getDataSource } from "./index";
import { nodeCitationProcessor } from "./nodePostprocessors";
import { generateFilters } from "./queryFilter";
export async function createChatEngine(documentIds?: string[]) {
const index = await getDataSource();
export async function createChatEngine(documentIds?: string[], params?: any) {
const index = await getDataSource(params);
if (!index) {
throw new Error(
`StorageContext is empty - call 'npm run generate' to generate the storage first`,
);
}
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 || []),
});
const systemPrompt = process.env.SYSTEM_PROMPT;
const citationPrompt = process.env.SYSTEM_CITATION_PROMPT;
const prompt =
[systemPrompt, citationPrompt].filter((p) => p).join("\n") || undefined;
const nodePostprocessors = citationPrompt
? [nodeCitationProcessor]
: undefined;
return new ContextChatEngine({
chatModel: Settings.llm,
retriever,
systemPrompt: process.env.SYSTEM_PROMPT,
systemPrompt: prompt,
nodePostprocessors,
});
}
export function generateFilters(documentIds: string[]): MetadataFilters {
// public documents don't have the "private" field or it's set to "false"
const publicDocumentsFilter: MetadataFilter = {
key: "private",
value: ["true"],
operator: "nin",
};
// if no documentIds are provided, only retrieve information from public documents
if (!documentIds.length) return { filters: [publicDocumentsFilter] };
const privateDocumentsFilter: MetadataFilter = {
key: "doc_id",
value: documentIds,
operator: "in",
};
// if documentIds are provided, retrieve information from public and private documents
return {
filters: [publicDocumentsFilter, privateDocumentsFilter],
condition: "or",
};
}
@@ -0,0 +1,26 @@
import {
BaseNodePostprocessor,
MessageContent,
NodeWithScore,
} from "llamaindex";
class NodeCitationProcessor implements BaseNodePostprocessor {
/**
* Append node_id into metadata for citation purpose.
* Config SYSTEM_CITATION_PROMPT in your runtime environment variable to enable this feature.
*/
async postprocessNodes(
nodes: NodeWithScore[],
query?: MessageContent,
): Promise<NodeWithScore[]> {
for (const nodeScore of nodes) {
if (!nodeScore.node || !nodeScore.node.metadata) {
continue; // Skip nodes with missing properties
}
nodeScore.node.metadata["node_id"] = nodeScore.node.id_;
}
return nodes;
}
}
export const nodeCitationProcessor = new NodeCitationProcessor();
@@ -1,5 +1,4 @@
import fs from "fs";
import crypto from "node:crypto";
import { getExtractors } from "../../engine/loader";
const MIME_TYPE_TO_EXT: Record<string, string> = {
@@ -11,7 +10,24 @@ 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(
filename: string,
fileBuffer: Buffer,
mimeType: string,
) {
const documents = await loadDocuments(fileBuffer, mimeType);
await saveDocument(filename, 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,11 +38,14 @@ export async function loadDocuments(fileBuffer: Buffer, mimeType: string) {
return await reader.loadDataAsContent(fileBuffer);
}
export async function saveDocument(fileBuffer: Buffer, mimeType: string) {
async function saveDocument(
filename: string,
fileBuffer: Buffer,
mimeType: string,
) {
const fileExt = MIME_TYPE_TO_EXT[mimeType];
if (!fileExt) throw new Error(`Unsupported document type: ${mimeType}`);
const filename = `${crypto.randomUUID()}.${fileExt}`;
const filepath = `${UPLOADED_FOLDER}/${filename}`;
const fileurl = `${process.env.FILESERVER_URL_PREFIX}/${filepath}`;
@@ -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(filename, fileBuffer, mimeType);
return runPipeline(index, documents);
}
@@ -1,12 +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,
@@ -15,10 +21,11 @@ export function appendSourceData(
if (!sourceNodes?.length) return;
try {
const nodes = sourceNodes.map((node) => ({
...node.node.toMutableJSON(),
metadata: node.node.metadata,
id: node.node.id_,
score: node.score ?? null,
url: getNodeUrl(node.node.metadata),
text: node.node.getContent(MetadataMode.NONE),
}));
data.appendMessageAnnotation({
type: "sources",
@@ -82,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) => {
@@ -114,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,128 +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
interface LlamaCloudFile {
name: string;
file_id: string;
project_id: string;
}
export class LLamaCloudFileService {
public static async downloadFiles(nodes: NodeWithScore<Metadata>[]) {
const files = this.nodesToDownloadFiles(nodes);
if (!files.length) return;
console.log("Downloading files from LlamaCloud...");
for (const file of files) {
await this.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 = this.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 this.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 this.getAllFiles(pipelineId);
const file = files.find((file) => file.name === name);
if (!file) return null;
return await this.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 headers = {
Accept: "application/json",
Authorization: `Bearer ${process.env.LLAMA_CLOUD_API_KEY}`,
};
const response = await fetch(url, { method: "GET", 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 headers = {
Accept: "application/json",
Authorization: `Bearer ${process.env.LLAMA_CLOUD_API_KEY}`,
};
const response = await fetch(url, { method: "GET", headers });
const data = await response.json();
return data;
}
}
@@ -1,40 +1,28 @@
import { ChatMessage, Settings } from "llamaindex";
const NEXT_QUESTION_PROMPT_TEMPLATE = `You're a helpful assistant! Your task is to suggest the next question that user might ask.
Here is the conversation history
---------------------
$conversation
---------------------
Given the conversation history, please give me $number_of_questions questions that you might ask next!
Your answer should be wrapped in three sticks which follows the following format:
\`\`\`
<question 1>
<question 2>\`\`\`
`;
const N_QUESTIONS_TO_GENERATE = 3;
export async function generateNextQuestions(
conversation: ChatMessage[],
numberOfQuestions: number = N_QUESTIONS_TO_GENERATE,
) {
export async function generateNextQuestions(conversation: ChatMessage[]) {
const llm = Settings.llm;
const NEXT_QUESTION_PROMPT = process.env.NEXT_QUESTION_PROMPT;
if (!NEXT_QUESTION_PROMPT) {
return [];
}
// Format conversation
const conversationText = conversation
.map((message) => `${message.role}: ${message.content}`)
.join("\n");
const message = NEXT_QUESTION_PROMPT_TEMPLATE.replace(
"$conversation",
const message = NEXT_QUESTION_PROMPT.replace(
"{conversation}",
conversationText,
).replace("$number_of_questions", numberOfQuestions.toString());
);
try {
const response = await llm.complete({ prompt: message });
const questions = extractQuestions(response.text);
return questions;
} catch (error) {
console.error("Error: ", error);
throw error;
console.error("Error when generating the next questions: ", error);
return [];
}
}
+1 -3
View File
@@ -1,8 +1,6 @@
import os
import logging
from typing import List
from pydantic import BaseModel, validator
from llama_index.core.indices.vector_store import VectorStoreIndex
from pydantic import BaseModel
logger = logging.getLogger(__name__)
+4 -9
View File
@@ -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,
@@ -1,5 +1,3 @@
import os
import json
from pydantic import BaseModel, Field
@@ -10,7 +10,15 @@ export function getExtractors() {
}
export async function getDocuments() {
return await new SimpleDirectoryReader().loadData({
const documents = await new SimpleDirectoryReader().loadData({
directoryPath: DATA_DIR,
});
// Set private=false to mark the document as public (required for filtering)
for (const document of documents) {
document.metadata = {
...document.metadata,
private: "false",
};
}
return documents;
}
@@ -23,8 +23,16 @@ export function getExtractors() {
export async function getDocuments() {
const reader = new SimpleDirectoryReader();
const extractors = getExtractors();
return await reader.loadData({
const documents = await reader.loadData({
directoryPath: DATA_DIR,
fileExtToReader: extractors,
});
// Set private=false to mark the document as public (required for filtering)
for (const document of documents) {
document.metadata = {
...document.metadata,
private: "false",
};
}
return documents;
}
@@ -0,0 +1,12 @@
import llama_index.core
import os
def init_observability():
PHOENIX_API_KEY = os.getenv("PHOENIX_API_KEY")
if not PHOENIX_API_KEY:
raise ValueError("PHOENIX_API_KEY environment variable is not set")
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"api_key={PHOENIX_API_KEY}"
llama_index.core.set_global_handler(
"arize_phoenix", endpoint="https://llamatrace.com/v1/traces"
)
@@ -6,7 +6,7 @@ authors = ["Marcus Schiesser <mail@marcusschiesser.de>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11,<3.12"
python = "^3.11,<4.0"
llama-index = "^0.10.6"
llama-index-readers-file = "^0.1.3"
python-dotenv = "^1.0.0"
@@ -1,19 +1,16 @@
import base64
import mimetypes
import os
from io import BytesIO
from pathlib import Path
from typing import Dict, List
from uuid import uuid4
from typing import List, Optional, Tuple
from app.engine.index import get_index
from app.engine.index import IndexConfig, get_index
from llama_index.core import VectorStoreIndex
from llama_index.core.ingestion import IngestionPipeline
from llama_index.core.readers.file.base import (
_try_loading_included_file_formats as get_file_loaders_map,
)
from llama_index.core.readers.file.base import (
default_file_metadata_func,
)
from llama_index.core.schema import Document
from llama_index.indices.managed.llama_cloud.base import LlamaCloudIndex
from llama_index.readers.file import FlatReader
@@ -41,7 +38,7 @@ class PrivateFileService:
PRIVATE_STORE_PATH = "output/uploaded"
@staticmethod
def preprocess_base64_file(base64_content: str) -> tuple:
def preprocess_base64_file(base64_content: str) -> Tuple[bytes, str | None]:
header, data = base64_content.split(",", 1)
mime_type = header.split(";")[0].split(":", 1)[1]
extension = mimetypes.guess_extension(mime_type)
@@ -49,12 +46,9 @@ class PrivateFileService:
return base64.b64decode(data), extension
@staticmethod
def store_and_parse_file(file_data, extension) -> List[Document]:
def store_and_parse_file(file_name, file_data, extension) -> List[Document]:
# Store file to the private directory
os.makedirs(PrivateFileService.PRIVATE_STORE_PATH, exist_ok=True)
# random file name
file_name = f"{uuid4().hex}{extension}"
file_path = Path(os.path.join(PrivateFileService.PRIVATE_STORE_PATH, file_name))
# write file
@@ -78,25 +72,42 @@ class PrivateFileService:
return documents
@staticmethod
def process_file(base64_content: str) -> List[str]:
file_data, extension = PrivateFileService.preprocess_base64_file(base64_content)
documents = PrivateFileService.store_and_parse_file(file_data, extension)
def process_file(
file_name: str, base64_content: str, params: Optional[dict] = None
) -> List[str]:
if params is None:
params = {}
# Only process nodes, no store the index
pipeline = IngestionPipeline()
nodes = pipeline.run(documents=documents)
file_data, extension = PrivateFileService.preprocess_base64_file(base64_content)
# Add the nodes to the index and persist it
current_index = get_index()
index_config = IndexConfig(**params)
current_index = get_index(index_config)
# Insert the documents into the index
if isinstance(current_index, LlamaCloudIndex):
# LlamaCloudIndex is a managed index so we don't need to process the nodes
# just insert the documents
for doc in documents:
current_index.insert(doc)
from app.engine.service import LLamaCloudFileService
project_id = current_index._get_project_id()
pipeline_id = current_index._get_pipeline_id()
# LlamaCloudIndex is a managed index so we can directly use the files
upload_file = (file_name, BytesIO(file_data))
return [
LLamaCloudFileService.add_file_to_pipeline(
project_id,
pipeline_id,
upload_file,
custom_metadata={
# Set private=true to mark the document as private user docs (required for filtering)
"private": "true",
},
)
]
else:
# Only process nodes, no store the index
# First process documents into nodes
documents = PrivateFileService.store_and_parse_file(
file_name, file_data, extension
)
pipeline = IngestionPipeline()
nodes = pipeline.run(documents=documents)
@@ -109,5 +120,5 @@ class PrivateFileService:
persist_dir=os.environ.get("STORAGE_DIR", "storage")
)
# Return the document ids
return [doc.doc_id for doc in documents]
# Return the document ids
return [doc.doc_id for doc in documents]
@@ -0,0 +1,78 @@
import logging
import os
import re
from typing import List, Optional
from app.api.routers.models import Message
from llama_index.core.prompts import PromptTemplate
from llama_index.core.settings import Settings
logger = logging.getLogger("uvicorn")
class NextQuestionSuggestion:
"""
Suggest the next questions that user might ask based on the conversation history
Disable this feature by removing the NEXT_QUESTION_PROMPT environment variable
"""
@classmethod
def get_configured_prompt(cls) -> Optional[str]:
prompt = os.getenv("NEXT_QUESTION_PROMPT", None)
if not prompt:
return None
return PromptTemplate(prompt)
@classmethod
async def suggest_next_questions_all_messages(
cls,
messages: List[Message],
) -> Optional[List[str]]:
"""
Suggest the next questions that user might ask based on the conversation history
Return None if suggestion is disabled or there is an error
"""
prompt_template = cls.get_configured_prompt()
if not prompt_template:
return None
try:
# Reduce the cost by only using the last two messages
last_user_message = None
last_assistant_message = None
for message in reversed(messages):
if message.role == "user":
last_user_message = f"User: {message.content}"
elif message.role == "assistant":
last_assistant_message = f"Assistant: {message.content}"
if last_user_message and last_assistant_message:
break
conversation: str = f"{last_user_message}\n{last_assistant_message}"
# Call the LLM and parse questions from the output
prompt = prompt_template.format(conversation=conversation)
output = await Settings.llm.acomplete(prompt)
questions = cls._extract_questions(output.text)
return questions
except Exception as e:
logger.error(f"Error when generating next question: {e}")
return None
@classmethod
def _extract_questions(cls, text: str) -> List[str]:
content_match = re.search(r"```(.*?)```", text, re.DOTALL)
content = content_match.group(1) if content_match else ""
return content.strip().split("\n")
@classmethod
async def suggest_next_questions(
cls,
chat_history: List[Message],
response: str,
) -> List[str]:
"""
Suggest the next questions that user might ask based on the chat history and the last response
"""
messages = chat_history + [Message(role="assistant", content=response)]
return await cls.suggest_next_questions_all_messages(messages)
@@ -6,11 +6,13 @@ import os
DEFAULT_MODEL = "gpt-3.5-turbo"
DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large"
class TSIEmbedding(OpenAIEmbedding):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._query_engine = self._text_engine = self.model_name
def llm_config_from_env() -> Dict:
from llama_index.core.constants import DEFAULT_TEMPERATURE
@@ -32,7 +34,7 @@ def llm_config_from_env() -> Dict:
def embedding_config_from_env() -> Dict:
from llama_index.core.constants import DEFAULT_EMBEDDING_DIM
model = os.getenv("EMBEDDING_MODEL", DEFAULT_EMBEDDING_MODEL)
dimension = os.getenv("EMBEDDING_DIM", DEFAULT_EMBEDDING_DIM)
api_key = os.getenv("T_SYSTEMS_LLMHUB_API_KEY")
@@ -46,6 +48,7 @@ def embedding_config_from_env() -> Dict:
}
return config
def init_llmhub():
from llama_index.llms.openai_like import OpenAILike
@@ -58,4 +61,4 @@ def init_llmhub():
is_chat_model=True,
is_function_calling_model=False,
context_window=4096,
)
)
@@ -82,7 +82,7 @@ def init_azure_openai():
dimensions = os.getenv("EMBEDDING_DIM")
azure_config = {
"api_key": os.environ["AZURE_OPENAI_KEY"],
"api_key": os.environ["AZURE_OPENAI_API_KEY"],
"azure_endpoint": os.environ["AZURE_OPENAI_ENDPOINT"],
"api_version": os.getenv("AZURE_OPENAI_API_VERSION")
or os.getenv("OPENAI_API_VERSION"),
@@ -1,31 +1,24 @@
"use client";
import { useEffect, useMemo, useState } from "react";
export interface ChatConfig {
backend?: string;
starterQuestions?: string[];
}
function getBackendOrigin(): string {
const chatAPI = process.env.NEXT_PUBLIC_CHAT_API;
if (chatAPI) {
return new URL(chatAPI).origin;
} else {
if (typeof window !== "undefined") {
// Use BASE_URL from window.ENV
return (window as any).ENV?.BASE_URL || "";
}
return "";
}
}
export function useClientConfig(): ChatConfig {
const chatAPI = process.env.NEXT_PUBLIC_CHAT_API;
const [config, setConfig] = useState<ChatConfig>();
const backendOrigin = useMemo(() => {
return chatAPI ? new URL(chatAPI).origin : "";
}, [chatAPI]);
const configAPI = `${backendOrigin}/api/chat/config`;
useEffect(() => {
fetch(configAPI)
.then((response) => response.json())
.then((data) => setConfig({ ...data, chatAPI }))
.catch((error) => console.error("Error fetching config", error));
}, [chatAPI, configAPI]);
return {
backend: backendOrigin,
starterQuestions: config?.starterQuestions,
backend: getBackendOrigin(),
};
}
@@ -1,48 +1,47 @@
# flake8: noqa: E402
from dotenv import load_dotenv
from app.engine.index import get_index
load_dotenv()
import os
import logging
from app.settings import init_settings
from app.engine.loaders import get_documents
from llama_index.indices.managed.llama_cloud import LlamaCloudIndex
from llama_index.core.readers import SimpleDirectoryReader
from app.engine.service import LLamaCloudFileService
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
def generate_datasource():
init_settings()
logger.info("Generate index for the provided data")
name = os.getenv("LLAMA_CLOUD_INDEX_NAME")
project_name = os.getenv("LLAMA_CLOUD_PROJECT_NAME")
api_key = os.getenv("LLAMA_CLOUD_API_KEY")
base_url = os.getenv("LLAMA_CLOUD_BASE_URL")
organization_id = os.getenv("LLAMA_CLOUD_ORGANIZATION_ID")
index = get_index()
project_id = index._get_project_id()
pipeline_id = index._get_pipeline_id()
if name is None or project_name is None or api_key is None:
raise ValueError(
"Please set LLAMA_CLOUD_INDEX_NAME, LLAMA_CLOUD_PROJECT_NAME and LLAMA_CLOUD_API_KEY"
" to your environment variables or config them in .env file"
)
documents = get_documents()
# Set private=false to mark the document as public (required for filtering)
for doc in documents:
doc.metadata["private"] = "false"
LlamaCloudIndex.from_documents(
documents=documents,
name=name,
project_name=project_name,
api_key=api_key,
base_url=base_url,
organization_id=organization_id
# use SimpleDirectoryReader to retrieve the files to process
reader = SimpleDirectoryReader(
"data",
recursive=True,
)
files_to_process = reader.input_files
# add each file to the LlamaCloud pipeline
for input_file in files_to_process:
with open(input_file, "rb") as f:
logger.info(
f"Adding file {input_file} to pipeline {index.name} in project {index.project_name}"
)
LLamaCloudFileService.add_file_to_pipeline(
project_id,
pipeline_id,
f,
custom_metadata={
# Set private=false to mark the document as public (required for filtering)
"private": "false",
},
)
logger.info("Finished generating the index")
@@ -1,30 +1,87 @@
import logging
import os
from llama_index.indices.managed.llama_cloud import LlamaCloudIndex
from typing import Optional
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.indices.managed.llama_cloud import LlamaCloudIndex
from pydantic import BaseModel, Field, validator
logger = logging.getLogger("uvicorn")
def get_index():
name = os.getenv("LLAMA_CLOUD_INDEX_NAME")
project_name = os.getenv("LLAMA_CLOUD_PROJECT_NAME")
api_key = os.getenv("LLAMA_CLOUD_API_KEY")
base_url = os.getenv("LLAMA_CLOUD_BASE_URL")
organization_id = os.getenv("LLAMA_CLOUD_ORGANIZATION_ID")
if name is None or project_name is None or api_key is None:
raise ValueError(
"Please set LLAMA_CLOUD_INDEX_NAME, LLAMA_CLOUD_PROJECT_NAME and LLAMA_CLOUD_API_KEY"
" to your environment variables or config them in .env file"
)
index = LlamaCloudIndex(
name=name,
project_name=project_name,
api_key=api_key,
base_url=base_url,
organization_id=organization_id
class LlamaCloudConfig(BaseModel):
# Private attributes
api_key: str = Field(
default=os.getenv("LLAMA_CLOUD_API_KEY"),
exclude=True, # Exclude from the model representation
)
base_url: Optional[str] = Field(
default=os.getenv("LLAMA_CLOUD_BASE_URL"),
exclude=True,
)
organization_id: Optional[str] = Field(
default=os.getenv("LLAMA_CLOUD_ORGANIZATION_ID"),
exclude=True,
)
# Configuration attributes, can be set by the user
pipeline: str = Field(
description="The name of the pipeline to use",
default=os.getenv("LLAMA_CLOUD_INDEX_NAME"),
)
project: str = Field(
description="The name of the LlamaCloud project",
default=os.getenv("LLAMA_CLOUD_PROJECT_NAME"),
)
# Validate and throw error if the env variables are not set before starting the app
@validator("pipeline", "project", "api_key", pre=True, always=True)
@classmethod
def validate_env_vars(cls, value):
if value is None:
raise ValueError(
"Please set LLAMA_CLOUD_INDEX_NAME, LLAMA_CLOUD_PROJECT_NAME and LLAMA_CLOUD_API_KEY"
" to your environment variables or config them in .env file"
)
return value
def to_client_kwargs(self) -> dict:
return {
"api_key": self.api_key,
"base_url": self.base_url,
}
class IndexConfig(BaseModel):
llama_cloud_pipeline_config: LlamaCloudConfig = Field(
default=LlamaCloudConfig(),
alias="llamaCloudPipeline",
)
callback_manager: Optional[CallbackManager] = Field(
default=None,
)
def to_index_kwargs(self) -> dict:
return {
"name": self.llama_cloud_pipeline_config.pipeline,
"project_name": self.llama_cloud_pipeline_config.project,
"api_key": self.llama_cloud_pipeline_config.api_key,
"base_url": self.llama_cloud_pipeline_config.base_url,
"organization_id": self.llama_cloud_pipeline_config.organization_id,
"callback_manager": self.callback_manager,
}
def get_index(config: IndexConfig = None):
if config is None:
config = IndexConfig()
index = LlamaCloudIndex(**config.to_index_kwargs())
return index
def get_client():
config = LlamaCloudConfig()
return llama_cloud_get_client(**config.to_client_kwargs())
@@ -0,0 +1,35 @@
from llama_index.core.vector_stores.types import MetadataFilter, MetadataFilters
def generate_filters(doc_ids):
"""
Generate public/private document filters based on the doc_ids and the vector store.
"""
# Using "is_empty" filter to include the documents don't have the "private" key because they're uploaded in LlamaCloud UI
public_doc_filter = MetadataFilter(
key="private",
value=None,
operator="is_empty", # type: ignore
)
selected_doc_filter = MetadataFilter(
key="file_id", # Note: LLamaCloud uses "file_id" to reference private document ids as "doc_id" is a restricted field in LlamaCloud
value=doc_ids,
operator="in", # type: ignore
)
if len(doc_ids) > 0:
# If doc_ids are provided, we will select both public and selected documents
filters = MetadataFilters(
filters=[
public_doc_filter,
selected_doc_filter,
],
condition="or", # type: ignore
)
else:
filters = MetadataFilters(
filters=[
public_doc_filter,
]
)
return filters
@@ -0,0 +1,173 @@
from io import BytesIO
import logging
import os
import time
from typing import Any, Dict, List, Optional, Set, Tuple, Union
import typing
from fastapi import BackgroundTasks
from llama_cloud import ManagedIngestionStatus, PipelineFileCreateCustomMetadataValue
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")
class LlamaCloudFile(BaseModel):
file_name: str
pipeline_id: str
def __eq__(self, other):
if not isinstance(other, LlamaCloudFile):
return NotImplemented
return (
self.file_name == other.file_name and self.pipeline_id == other.pipeline_id
)
def __hash__(self):
return hash((self.file_name, self.pipeline_id))
class LLamaCloudFileService:
LOCAL_STORE_PATH = "output/llamacloud"
DOWNLOAD_FILE_NAME_TPL = "{pipeline_id}${filename}"
@classmethod
def get_all_projects_with_pipelines(cls) -> List[Dict[str, Any]]:
try:
client = get_client()
projects = client.projects.list_projects()
pipelines = client.pipelines.search_pipelines()
return [
{
**(project.dict()),
"pipelines": [
{"id": p.id, "name": p.name}
for p in pipelines
if p.project_id == project.id
],
}
for project in projects
]
except Exception as error:
logger.error(f"Error listing projects and pipelines: {error}")
return []
@classmethod
def add_file_to_pipeline(
cls,
project_id: str,
pipeline_id: str,
upload_file: Union[typing.IO, Tuple[str, BytesIO]],
custom_metadata: Optional[Dict[str, PipelineFileCreateCustomMetadataValue]],
) -> str:
client = get_client()
file = client.files.upload_file(project_id=project_id, upload_file=upload_file)
files = [
{
"file_id": file.id,
"custom_metadata": {"file_id": file.id, **(custom_metadata or {})},
}
]
files = client.pipelines.add_files_to_pipeline(pipeline_id, request=files)
# 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)
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
attempt += 1
time.sleep(0.1) # Sleep for 100ms
raise Exception(
f"File processing did not complete after {max_attempts} attempts."
)
@classmethod
def download_pipeline_file(
cls,
file: LlamaCloudFile,
force_download: bool = False,
):
client = get_client()
file_name = file.file_name
pipeline_id = file.pipeline_id
# Check is the file already exists
downloaded_file_path = cls._get_file_path(file_name, pipeline_id)
if os.path.exists(downloaded_file_path) and not force_download:
logger.debug(f"File {file_name} already exists in local storage")
return
try:
logger.info(f"Downloading file {file_name} for pipeline {pipeline_id}")
files = client.pipelines.list_pipeline_files(pipeline_id)
if not files or not isinstance(files, list):
raise Exception("No files found in LlamaCloud")
for file_entry in files:
if file_entry.name == file_name:
file_id = file_entry.file_id
project_id = file_entry.project_id
file_detail = client.files.read_file_content(
file_id, project_id=project_id
)
cls._download_file(file_detail.url, downloaded_file_path)
break
except Exception as error:
logger.info(f"Error fetching file from LlamaCloud: {error}")
@classmethod
def download_files_from_nodes(
cls, nodes: List[NodeWithScore], background_tasks: BackgroundTasks
):
files = cls._get_files_to_download(nodes)
for file in files:
logger.info(f"Adding download of {file.file_name} to background tasks")
background_tasks.add_task(
LLamaCloudFileService.download_pipeline_file, file
)
@classmethod
def _get_files_to_download(cls, nodes: List[NodeWithScore]) -> Set[LlamaCloudFile]:
source_nodes = SourceNodes.from_source_nodes(nodes)
llama_cloud_files = [
LlamaCloudFile(
file_name=node.metadata.get("file_name"),
pipeline_id=node.metadata.get("pipeline_id"),
)
for node in source_nodes
if (
node.metadata.get("pipeline_id") is not None
and node.metadata.get("file_name") is not None
)
]
# Remove duplicates and return
return set(llama_cloud_files)
@classmethod
def _get_file_name(cls, name: str, pipeline_id: str) -> str:
return cls.DOWNLOAD_FILE_NAME_TPL.format(pipeline_id=pipeline_id, filename=name)
@classmethod
def _get_file_path(cls, name: str, pipeline_id: str) -> str:
return os.path.join(cls.LOCAL_STORE_PATH, cls._get_file_name(name, pipeline_id))
@classmethod
def _download_file(cls, url: str, local_file_path: str):
logger.info(f"Saving file to {local_file_path}")
# Create directory if it doesn't exist
os.makedirs(cls.LOCAL_STORE_PATH, exist_ok=True)
# Download the file
with requests.get(url, stream=True) as r:
r.raise_for_status()
with open(local_file_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
logger.info("File downloaded successfully")
@@ -1,3 +1,4 @@
# flake8: noqa: E402
from dotenv import load_dotenv
load_dotenv()
@@ -26,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,30 +1,43 @@
import os
import logging
import os
from datetime import timedelta
from typing import Optional
from cachetools import cached, TTLCache
from llama_index.core.storage import StorageContext
from cachetools import TTLCache, cached
from llama_index.core.callbacks import CallbackManager
from llama_index.core.indices import load_index_from_storage
from llama_index.core.storage import StorageContext
from pydantic import BaseModel, Field
logger = logging.getLogger("uvicorn")
class IndexConfig(BaseModel):
callback_manager: Optional[CallbackManager] = Field(
default=None,
)
def get_index(config: IndexConfig = None):
if config is None:
config = IndexConfig()
storage_dir = os.getenv("STORAGE_DIR", "storage")
# check if storage already exists
if not os.path.exists(storage_dir):
return None
# load the existing index
logger.info(f"Loading index from {storage_dir}...")
storage_context = get_storage_context(storage_dir)
index = load_index_from_storage(
storage_context, callback_manager=config.callback_manager
)
logger.info(f"Finished loading index from {storage_dir}")
return index
@cached(
TTLCache(maxsize=10, ttl=timedelta(minutes=5).total_seconds()),
key=lambda *args, **kwargs: "global_storage_context",
)
def get_storage_context(persist_dir: str) -> StorageContext:
return StorageContext.from_defaults(persist_dir=persist_dir)
def get_index():
storage_dir = os.getenv("STORAGE_DIR", "storage")
# check if storage already exists
if not os.path.exists(storage_dir):
return None
# load the existing index
logger.info(f"Loading index from {storage_dir}...")
storage_context = get_storage_context(storage_dir)
index = load_index_from_storage(storage_context)
logger.info(f"Finished loading index from {storage_dir}")
return index
@@ -0,0 +1,36 @@
from llama_index.core.vector_stores.types import MetadataFilter, MetadataFilters
def generate_filters(doc_ids):
"""
Generate public/private document filters based on the doc_ids and the vector store.
"""
public_doc_filter = MetadataFilter(
key="private",
value="true",
operator="!=", # type: ignore
)
# Weaviate doesn't support "in" filter right now, so use "any" instead - it has the same behavior.
# TODO: Use "in" operator, once Weaviate supports it
selected_doc_filter = MetadataFilter(
key="doc_id",
value=doc_ids,
operator="any", # type: ignore
)
if len(doc_ids) > 0:
# If doc_ids are provided, we will select both public and selected documents
filters = MetadataFilters(
filters=[
public_doc_filter,
selected_doc_filter,
],
condition="or", # type: ignore
)
else:
filters = MetadataFilters(
filters=[
public_doc_filter,
]
)
return filters
@@ -0,0 +1,35 @@
import os
import weaviate
from llama_index.vector_stores.weaviate import WeaviateVectorStore
DEFAULT_INDEX_NAME = "LlamaIndex"
def _create_weaviate_client():
cluster_url = os.getenv("WEAVIATE_CLUSTER_URL")
api_key = os.getenv("WEAVIATE_API_KEY")
if not cluster_url or not api_key:
raise ValueError(
"Environment variables: WEAVIATE_CLUSTER_URL and WEAVIATE_API_KEY are required."
)
auth_credentials = weaviate.auth.AuthApiKey(api_key)
client = weaviate.connect_to_weaviate_cloud(cluster_url, auth_credentials)
return client
# Global variable to store the Weaviate client
client = None
def get_vector_store():
global client
if client is None:
client = _create_weaviate_client()
index_name = os.getenv("WEAVIATE_INDEX_NAME", DEFAULT_INDEX_NAME)
vector_store = WeaviateVectorStore(
weaviate_client=client,
index_name=index_name,
)
return vector_store
@@ -3,7 +3,7 @@ import { VectorStoreIndex } from "llamaindex";
import { AstraDBVectorStore } from "llamaindex/storage/vectorStore/AstraDBVectorStore";
import { checkRequiredEnvVars } from "./shared";
export async function getDataSource() {
export async function getDataSource(params?: any) {
checkRequiredEnvVars();
const store = new AstraDBVectorStore();
await store.connect(process.env.ASTRA_DB_COLLECTION!);
@@ -3,7 +3,7 @@ import { VectorStoreIndex } from "llamaindex";
import { ChromaVectorStore } from "llamaindex/storage/vectorStore/ChromaVectorStore";
import { checkRequiredEnvVars } from "./shared";
export async function getDataSource() {
export async function getDataSource(params?: any) {
checkRequiredEnvVars();
const chromaUri = `http://${process.env.CHROMA_HOST}:${process.env.CHROMA_PORT}`;
@@ -1,30 +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();
// Set private=false to mark the document as public (required for filtering)
for (const document of documents) {
document.metadata = {
...document.metadata,
private: "false",
};
async function* walk(dir: string): AsyncGenerator<string> {
const directory = await fs.opendir(dir);
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
}
}
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!`);
}
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 () => {
@@ -1,12 +1,27 @@
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
import { checkRequiredEnvVars } from "./shared";
export async function getDataSource() {
checkRequiredEnvVars();
type LlamaCloudDataSourceParams = {
llamaCloudPipeline?: {
project: string;
pipeline: string;
};
};
export async function getDataSource(params?: LlamaCloudDataSourceParams) {
const { project, pipeline } = params?.llamaCloudPipeline ?? {};
const projectName = project ?? process.env.LLAMA_CLOUD_PROJECT_NAME;
const pipelineName = pipeline ?? process.env.LLAMA_CLOUD_INDEX_NAME;
const apiKey = process.env.LLAMA_CLOUD_API_KEY;
if (!projectName || !pipelineName || !apiKey) {
throw new Error(
"Set project, pipeline, and api key in the params or as environment variables.",
);
}
const index = new LlamaCloudIndex({
name: process.env.LLAMA_CLOUD_INDEX_NAME!,
projectName: process.env.LLAMA_CLOUD_PROJECT_NAME!,
apiKey: process.env.LLAMA_CLOUD_API_KEY,
organizationId: process.env.LLAMA_CLOUD_ORGANIZATION_ID,
name: pipelineName,
projectName,
apiKey,
baseUrl: process.env.LLAMA_CLOUD_BASE_URL,
});
return index;
@@ -0,0 +1,25 @@
import { MetadataFilter, MetadataFilters } from "llamaindex";
export function generateFilters(documentIds: string[]): MetadataFilters {
// public documents don't have the "private" field or it's set to "false"
const publicDocumentsFilter: MetadataFilter = {
key: "private",
value: null,
operator: "is_empty",
};
// if no documentIds are provided, only retrieve information from public documents
if (!documentIds.length) return { filters: [publicDocumentsFilter] };
const privateDocumentsFilter: MetadataFilter = {
key: "file_id", // Note: LLamaCloud uses "file_id" to reference private document ids as "doc_id" is a restricted field in LlamaCloud
value: documentIds,
operator: "in",
};
// if documentIds are provided, retrieve information from public and private documents
return {
filters: [publicDocumentsFilter, privateDocumentsFilter],
condition: "or",
};
}
@@ -2,7 +2,7 @@ import { VectorStoreIndex } from "llamaindex";
import { MilvusVectorStore } from "llamaindex/storage/vectorStore/MilvusVectorStore";
import { checkRequiredEnvVars, getMilvusClient } from "./shared";
export async function getDataSource() {
export async function getDataSource(params?: any) {
checkRequiredEnvVars();
const milvusClient = getMilvusClient();
const store = new MilvusVectorStore({ milvusClient });
@@ -1,11 +1,11 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import * as dotenv from "dotenv";
import { VectorStoreIndex, storageContextFromDefaults } from "llamaindex";
import { MongoDBAtlasVectorSearch } from "llamaindex/storage/vectorStore/MongoDBAtlasVectorSearch";
import { storageContextFromDefaults, VectorStoreIndex } from "llamaindex";
import { MongoDBAtlasVectorSearch } from "llamaindex/storage/vectorStore/MongoDBAtlasVectorStore";
import { MongoClient } from "mongodb";
import { getDocuments } from "./loader";
import { initSettings } from "./settings";
import { checkRequiredEnvVars } from "./shared";
import { checkRequiredEnvVars, POPULATED_METADATA_FIELDS } from "./shared";
dotenv.config();
@@ -27,6 +27,12 @@ async function loadAndIndex() {
dbName: databaseName,
collectionName: vectorCollectionName, // this is where your embeddings will be stored
indexName: indexName, // this is the name of the index you will need to create
indexedMetadataFields: POPULATED_METADATA_FIELDS,
embeddingDefinition: {
dimensions: process.env.EMBEDDING_DIM
? parseInt(process.env.EMBEDDING_DIM)
: 1536,
},
});
// now create an index from all the Documents and store them in Atlas
@@ -1,17 +1,23 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { VectorStoreIndex } from "llamaindex";
import { MongoDBAtlasVectorSearch } from "llamaindex/storage/vectorStore/MongoDBAtlasVectorSearch";
import { MongoDBAtlasVectorSearch } from "llamaindex/storage/vectorStore/MongoDBAtlasVectorStore";
import { MongoClient } from "mongodb";
import { checkRequiredEnvVars } from "./shared";
import { checkRequiredEnvVars, POPULATED_METADATA_FIELDS } from "./shared";
export async function getDataSource() {
export async function getDataSource(params?: any) {
checkRequiredEnvVars();
const client = new MongoClient(process.env.MONGO_URI!);
const client = new MongoClient(process.env.MONGODB_URI!);
const store = new MongoDBAtlasVectorSearch({
mongodbClient: client,
dbName: process.env.MONGODB_DATABASE!,
collectionName: process.env.MONGODB_VECTORS!,
indexName: process.env.MONGODB_VECTOR_INDEX,
indexedMetadataFields: POPULATED_METADATA_FIELDS,
embeddingDefinition: {
dimensions: process.env.EMBEDDING_DIM
? parseInt(process.env.EMBEDDING_DIM)
: 1536,
},
});
return await VectorStoreIndex.fromVectorStore(store);
@@ -5,6 +5,8 @@ const REQUIRED_ENV_VARS = [
"MONGODB_VECTOR_INDEX",
];
export const POPULATED_METADATA_FIELDS = ["private", "doc_id"]; // for filtering in MongoDB VectorSearchIndex
export function checkRequiredEnvVars() {
const missingEnvVars = REQUIRED_ENV_VARS.filter((envVar) => {
return !process.env[envVar];
@@ -25,10 +25,6 @@ async function generateDatasource() {
persistDir: STORAGE_CACHE_DIR,
});
const documents = await getDocuments();
// Set private=false to mark the document as public (required for filtering)
documents.forEach((doc) => {
doc.metadata["private"] = "false";
});
await VectorStoreIndex.fromDocuments(documents, {
storageContext,
@@ -2,7 +2,7 @@ import { SimpleDocumentStore, VectorStoreIndex } from "llamaindex";
import { storageContextFromDefaults } from "llamaindex/storage/StorageContext";
import { STORAGE_CACHE_DIR } from "./shared";
export async function getDataSource() {
export async function getDataSource(params?: any) {
const storageContext = await storageContextFromDefaults({
persistDir: `${STORAGE_CACHE_DIR}`,
});
@@ -7,7 +7,7 @@ import {
checkRequiredEnvVars,
} from "./shared";
export async function getDataSource() {
export async function getDataSource(params?: any) {
checkRequiredEnvVars();
const pgvs = new PGVectorStore({
connectionString: process.env.PG_CONNECTION_STRING,
@@ -3,7 +3,7 @@ import { VectorStoreIndex } from "llamaindex";
import { PineconeVectorStore } from "llamaindex/storage/vectorStore/PineconeVectorStore";
import { checkRequiredEnvVars } from "./shared";
export async function getDataSource() {
export async function getDataSource(params?: any) {
checkRequiredEnvVars();
const store = new PineconeVectorStore();
return await VectorStoreIndex.fromVectorStore(store);
@@ -5,7 +5,7 @@ import { checkRequiredEnvVars, getQdrantClient } from "./shared";
dotenv.config();
export async function getDataSource() {
export async function getDataSource(params?: any) {
checkRequiredEnvVars();
const collectionName = process.env.QDRANT_COLLECTION;
const store = new QdrantVectorStore({
@@ -0,0 +1,33 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import * as dotenv from "dotenv";
import {
VectorStoreIndex,
WeaviateVectorStore,
storageContextFromDefaults,
} from "llamaindex";
import { getDocuments } from "./loader";
import { initSettings } from "./settings";
import { DEFAULT_INDEX_NAME, checkRequiredEnvVars } from "./shared";
dotenv.config();
async function loadAndIndex() {
const indexName = process.env.WEAVIATE_INDEX_NAME || DEFAULT_INDEX_NAME;
// load objects from storage and convert them into LlamaIndex Document objects
const documents = await getDocuments();
const vectorStore = new WeaviateVectorStore({ indexName });
const storageContext = await storageContextFromDefaults({ vectorStore });
await VectorStoreIndex.fromDocuments(documents, {
storageContext: storageContext,
});
console.log(`Successfully upload embeddings to Weaviate index ${indexName}.`);
}
(async () => {
checkRequiredEnvVars();
initSettings();
await loadAndIndex();
console.log("Finished generating storage.");
})();
@@ -0,0 +1,14 @@
import * as dotenv from "dotenv";
import { VectorStoreIndex } from "llamaindex";
import { WeaviateVectorStore } from "llamaindex/storage/vectorStore/WeaviateVectorStore";
import { checkRequiredEnvVars, DEFAULT_INDEX_NAME } from "./shared";
dotenv.config();
export async function getDataSource(params?: any) {
checkRequiredEnvVars();
const indexName = process.env.WEAVIATE_INDEX_NAME || DEFAULT_INDEX_NAME;
const store = new WeaviateVectorStore({ indexName });
return await VectorStoreIndex.fromVectorStore(store);
}
@@ -0,0 +1,26 @@
import { MetadataFilter, MetadataFilters } from "llamaindex";
export function generateFilters(documentIds: string[]): MetadataFilters {
// filter all documents have the private metadata key set to true
const publicDocumentsFilter: MetadataFilter = {
key: "private",
value: "true",
operator: "!=",
};
// if no documentIds are provided, only retrieve information from public documents
if (!documentIds.length) return { filters: [publicDocumentsFilter] };
// Weaviate uses 'any' instead of 'in' for the operator
const privateDocumentsFilter: MetadataFilter = {
key: "doc_id",
value: documentIds,
operator: "any",
};
// if documentIds are provided, retrieve information from public and private documents
return {
filters: [publicDocumentsFilter, privateDocumentsFilter],
condition: "or",
};
}
@@ -0,0 +1,20 @@
const REQUIRED_ENV_VARS = ["WEAVIATE_CLUSTER_URL", "WEAVIATE_API_KEY"];
export const DEFAULT_INDEX_NAME = "LlamaIndex";
export function checkRequiredEnvVars() {
const missingEnvVars = REQUIRED_ENV_VARS.filter((envVar) => {
return !process.env[envVar];
});
if (missingEnvVars.length > 0) {
console.log(
`The following environment variables are required but missing: ${missingEnvVars.join(
", ",
)}`,
);
throw new Error(
`Missing environment variables: ${missingEnvVars.join(", ")}`,
);
}
}
@@ -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
@@ -1,11 +1,13 @@
import os
from app.engine.index import get_index
from fastapi import HTTPException
from llama_index.core.settings import Settings
from app.engine.index import get_index
def get_chat_engine(filters=None):
system_prompt = os.getenv("SYSTEM_PROMPT")
top_k = os.getenv("TOP_K", 3)
def get_query_engine(output_cls):
top_k = int(os.getenv("TOP_K", 0))
index = get_index()
if index is None:
@@ -16,9 +18,10 @@ def get_chat_engine(filters=None):
),
)
return index.as_chat_engine(
similarity_top_k=int(top_k),
system_prompt=system_prompt,
chat_mode="condense_plus_context",
filters=filters,
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,31 @@
import logging
from typing import Optional
from llama_index.core.callbacks import CallbackManager
from llama_index.core.indices import VectorStoreIndex
from pydantic import BaseModel, Field
from app.engine.vectordb import get_vector_store
logger = logging.getLogger("uvicorn")
class IndexConfig(BaseModel):
callback_manager: Optional[CallbackManager] = Field(
default=None,
)
def get_index(config: IndexConfig = None):
if config is None:
config = IndexConfig()
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, callback_manager=config.callback_manager
)
logger.info("Finished load index from vector store.")
return index
@@ -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,37 @@
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).model_dump_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

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