mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-19 23:13:36 -04:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26359a0ac9 | |||
| 4039d3d1ea | |||
| 3ec5163304 | |||
| 878cfc2ca1 | |||
| 9b5835b71c | |||
| 04a9c71759 | |||
| 0bfdbc1dfe | |||
| fbcaebcbcf | |||
| b6dd7a9acb | |||
| 09e3022ad6 | |||
| 9f739b9834 | |||
| c06ec4f14c | |||
| e7d30b1c69 | |||
| e974c8ef11 | |||
| 8890e27a14 | |||
| 072e69b465 | |||
| 83a648df0a | |||
| dcf52abdba | |||
| 9a09e8c7e2 | |||
| a4a55239e9 | |||
| c5c7eee04d | |||
| 8b89ac547f | |||
| f43399cc18 |
@@ -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
|
||||
|
||||
@@ -1,5 +1,49 @@
|
||||
# create-llama
|
||||
|
||||
## 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
|
||||
|
||||
- 9a09e8c: Fix Vercel deployment
|
||||
|
||||
## 0.1.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c5c7eee: Make components reusable for chat-llamaindex
|
||||
|
||||
## 0.1.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f43399c: Add metadatafilters to context chat engine (Typescript)
|
||||
|
||||
## 0.1.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
+23
-9
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+98
-116
@@ -11,128 +11,110 @@ import type {
|
||||
} 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 templateType: TemplateType = "streaming";
|
||||
const templateFramework: TemplateFramework = process.env.FRAMEWORK
|
||||
? (process.env.FRAMEWORK as TemplateFramework)
|
||||
: "fastapi";
|
||||
const dataSource: string = process.env.DATASOURCE
|
||||
? process.env.DATASOURCE
|
||||
: "--example-file";
|
||||
const templateUI: TemplateUI = "shadcn";
|
||||
const templatePostInstallAction: TemplatePostInstallAction = "runApp";
|
||||
|
||||
const llamaCloudProjectName = "create-llama";
|
||||
const llamaCloudIndexName = "e2e-test";
|
||||
|
||||
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";
|
||||
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.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("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("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();
|
||||
});
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// clean processes
|
||||
test.afterAll(async () => {
|
||||
appProcess?.kill();
|
||||
});
|
||||
});
|
||||
|
||||
+52
-59
@@ -18,48 +18,6 @@ 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,
|
||||
@@ -142,25 +100,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 +117,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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+66
-16
@@ -2,9 +2,10 @@ 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";
|
||||
@@ -160,6 +161,17 @@ const getVectorDBEnvs = (
|
||||
description:
|
||||
"The organization ID for the LlamaCloud project (uses default organization if not specified - Python only)",
|
||||
},
|
||||
...(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 +198,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 [];
|
||||
}
|
||||
@@ -450,18 +479,35 @@ const getTemplateEnvs = (template?: TemplateType): EnvVar[] => {
|
||||
}
|
||||
};
|
||||
|
||||
const getObservabilityEnvs = (
|
||||
observability?: TemplateObservability,
|
||||
): EnvVar[] => {
|
||||
if (observability === "llamatrace") {
|
||||
return [
|
||||
{
|
||||
name: "PHOENIX_API_KEY",
|
||||
description:
|
||||
"API key for LlamaTrace observability. Retrieve from https://llamatrace.com/login",
|
||||
},
|
||||
];
|
||||
}
|
||||
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,16 +517,14 @@ 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),
|
||||
...getObservabilityEnvs(opts.observability),
|
||||
getSystemPromptEnv(opts.tools),
|
||||
];
|
||||
// Render and write env file
|
||||
@@ -493,6 +537,7 @@ export const createFrontendEnvFile = async (
|
||||
root: string,
|
||||
opts: {
|
||||
customApiPath?: string;
|
||||
vectorDb?: TemplateVectorDB;
|
||||
},
|
||||
) => {
|
||||
const defaultFrontendEnvs = [
|
||||
@@ -503,6 +548,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);
|
||||
|
||||
+2
-10
@@ -168,16 +168,7 @@ 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) {
|
||||
@@ -209,6 +200,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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
+22
-6
@@ -84,6 +84,13 @@ const getAdditionalDependencies = (
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "weaviate": {
|
||||
dependencies.push({
|
||||
name: "llama-index-vector-stores-weaviate",
|
||||
version: "^1.0.2",
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add data source dependencies
|
||||
@@ -380,18 +387,27 @@ export const installPythonTemplate = async ({
|
||||
tools,
|
||||
);
|
||||
|
||||
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,
|
||||
|
||||
+3
-2
@@ -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;
|
||||
|
||||
@@ -70,7 +70,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 +248,7 @@ async function updatePackageJson({
|
||||
};
|
||||
}
|
||||
|
||||
if (observability === "opentelemetry") {
|
||||
if (observability === "traceloop") {
|
||||
packageJson.dependencies = {
|
||||
...packageJson.dependencies,
|
||||
"@traceloop/node-server-sdk": "^0.5.19",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-llama",
|
||||
"version": "0.1.25",
|
||||
"version": "0.1.32",
|
||||
"description": "Create LlamaIndex-powered apps with one command",
|
||||
"keywords": [
|
||||
"rag",
|
||||
|
||||
+5
-1
@@ -103,6 +103,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";
|
||||
@@ -486,7 +487,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,
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ from app.engine.tools import ToolFactory
|
||||
from app.engine.index import get_index
|
||||
|
||||
|
||||
def get_chat_engine(filters=None):
|
||||
def get_chat_engine(filters=None, params=None):
|
||||
system_prompt = os.getenv("SYSTEM_PROMPT")
|
||||
top_k = os.getenv("TOP_K", "3")
|
||||
tools = []
|
||||
|
||||
@@ -3,11 +3,11 @@ from app.engine.index import get_index
|
||||
from fastapi import HTTPException
|
||||
|
||||
|
||||
def get_chat_engine(filters=None):
|
||||
def get_chat_engine(filters=None, params=None):
|
||||
system_prompt = os.getenv("SYSTEM_PROMPT")
|
||||
top_k = os.getenv("TOP_K", 3)
|
||||
|
||||
index = get_index()
|
||||
index = get_index(params)
|
||||
if index is None:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
function generateFilters(documentIds: string[]): MetadataFilters | undefined {
|
||||
// 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,8 +1,9 @@
|
||||
import { ContextChatEngine, Settings } from "llamaindex";
|
||||
import { getDataSource } from "./index";
|
||||
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`,
|
||||
@@ -10,12 +11,12 @@ export async function createChatEngine(documentIds?: string[]) {
|
||||
}
|
||||
const retriever = index.asRetriever({
|
||||
similarityTopK: process.env.TOP_K ? parseInt(process.env.TOP_K) : 3,
|
||||
filters: generateFilters(documentIds || []),
|
||||
});
|
||||
|
||||
return new ContextChatEngine({
|
||||
chatModel: Settings.llm,
|
||||
retriever,
|
||||
// disable as a custom system prompt disables the generated context
|
||||
// systemPrompt: process.env.SYSTEM_PROMPT,
|
||||
systemPrompt: process.env.SYSTEM_PROMPT,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,28 +1,16 @@
|
||||
import {
|
||||
BaseNode,
|
||||
Document,
|
||||
IngestionPipeline,
|
||||
Metadata,
|
||||
Settings,
|
||||
SimpleNodeParser,
|
||||
storageContextFromDefaults,
|
||||
VectorStoreIndex,
|
||||
} from "llamaindex";
|
||||
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
|
||||
import { getDataSource } from "../../engine";
|
||||
|
||||
export async function runPipeline(documents: Document[], filename: string) {
|
||||
const currentIndex = await getDataSource();
|
||||
|
||||
// Update documents with metadata
|
||||
for (const document of documents) {
|
||||
document.metadata = {
|
||||
...document.metadata,
|
||||
file_name: filename,
|
||||
private: "true", // to separate from other public documents
|
||||
};
|
||||
}
|
||||
|
||||
export async function runPipeline(
|
||||
currentIndex: VectorStoreIndex | LlamaCloudIndex,
|
||||
documents: Document[],
|
||||
) {
|
||||
if (currentIndex instanceof LlamaCloudIndex) {
|
||||
// LlamaCloudIndex processes the documents automatically
|
||||
// so we don't need ingestion pipeline, just insert the documents directly
|
||||
@@ -41,25 +29,10 @@ export async function runPipeline(documents: Document[], filename: string) {
|
||||
],
|
||||
});
|
||||
const nodes = await pipeline.run({ documents });
|
||||
await addNodesToVectorStore(nodes, currentIndex);
|
||||
await currentIndex.insertNodes(nodes);
|
||||
currentIndex.storageContext.docStore.persist();
|
||||
console.log("Added nodes to the vector store.");
|
||||
}
|
||||
|
||||
return documents.map((document) => document.id_);
|
||||
}
|
||||
|
||||
async function addNodesToVectorStore(
|
||||
nodes: BaseNode<Metadata>[],
|
||||
currentIndex: VectorStoreIndex | null,
|
||||
) {
|
||||
if (currentIndex) {
|
||||
await currentIndex.insertNodes(nodes);
|
||||
} else {
|
||||
// Not using vectordb and haven't generated local index yet
|
||||
const storageContext = await storageContextFromDefaults({
|
||||
persistDir: "./cache",
|
||||
});
|
||||
currentIndex = await VectorStoreIndex.init({ nodes, storageContext });
|
||||
}
|
||||
currentIndex.storageContext.docStore.persist();
|
||||
console.log("Added nodes to the vector store.");
|
||||
}
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
import { VectorStoreIndex } from "llamaindex";
|
||||
import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
|
||||
import { loadDocuments, saveDocument } from "./helper";
|
||||
import { runPipeline } from "./pipeline";
|
||||
|
||||
export async function uploadDocument(raw: string): Promise<string[]> {
|
||||
export async function uploadDocument(
|
||||
index: VectorStoreIndex | LlamaCloudIndex,
|
||||
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);
|
||||
return await runPipeline(documents, filename);
|
||||
|
||||
// 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
|
||||
};
|
||||
}
|
||||
|
||||
return await runPipeline(index, documents);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { StreamData } from "ai";
|
||||
import {
|
||||
CallbackManager,
|
||||
Metadata,
|
||||
MetadataMode,
|
||||
NodeWithScore,
|
||||
ToolCall,
|
||||
ToolOutput,
|
||||
@@ -15,10 +16,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",
|
||||
|
||||
@@ -7,19 +7,51 @@ 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 {
|
||||
type LlamaCloudFile = {
|
||||
name: string;
|
||||
file_id: string;
|
||||
project_id: string;
|
||||
}
|
||||
};
|
||||
|
||||
type LLamaCloudProject = {
|
||||
id: string;
|
||||
organization_id: string;
|
||||
name: string;
|
||||
is_default: boolean;
|
||||
};
|
||||
|
||||
type LLamaCloudPipeline = {
|
||||
id: string;
|
||||
name: string;
|
||||
project_id: string;
|
||||
};
|
||||
|
||||
export class LLamaCloudFileService {
|
||||
private static readonly headers = {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${process.env.LLAMA_CLOUD_API_KEY}`,
|
||||
};
|
||||
|
||||
public static async getAllProjectsWithPipelines() {
|
||||
try {
|
||||
const projects = await LLamaCloudFileService.getAllProjects();
|
||||
const pipelines = await LLamaCloudFileService.getAllPipelines();
|
||||
return projects.map((project) => ({
|
||||
...project,
|
||||
pipelines: pipelines.filter((p) => p.project_id === project.id),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Error listing projects and pipelines:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public static async downloadFiles(nodes: NodeWithScore<Metadata>[]) {
|
||||
const files = this.nodesToDownloadFiles(nodes);
|
||||
const files = LLamaCloudFileService.nodesToDownloadFiles(nodes);
|
||||
if (!files.length) return;
|
||||
console.log("Downloading files from LlamaCloud...");
|
||||
for (const file of files) {
|
||||
await this.downloadFile(file.pipelineId, file.fileName);
|
||||
await LLamaCloudFileService.downloadFile(file.pipelineId, file.fileName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,13 +91,19 @@ export class LLamaCloudFileService {
|
||||
|
||||
private static async downloadFile(pipelineId: string, fileName: string) {
|
||||
try {
|
||||
const downloadedName = this.toDownloadedName(pipelineId, fileName);
|
||||
const downloadedName = LLamaCloudFileService.toDownloadedName(
|
||||
pipelineId,
|
||||
fileName,
|
||||
);
|
||||
const downloadedPath = path.join(LLAMA_CLOUD_OUTPUT_DIR, downloadedName);
|
||||
|
||||
// Check if file already exists
|
||||
if (fs.existsSync(downloadedPath)) return;
|
||||
|
||||
const urlToDownload = await this.getFileUrlByName(pipelineId, fileName);
|
||||
const urlToDownload = await LLamaCloudFileService.getFileUrlByName(
|
||||
pipelineId,
|
||||
fileName,
|
||||
);
|
||||
if (!urlToDownload) throw new Error("File not found in LlamaCloud");
|
||||
|
||||
const file = fs.createWriteStream(downloadedPath);
|
||||
@@ -93,10 +131,13 @@ export class LLamaCloudFileService {
|
||||
pipelineId: string,
|
||||
name: string,
|
||||
): Promise<string | null> {
|
||||
const files = await this.getAllFiles(pipelineId);
|
||||
const files = await LLamaCloudFileService.getAllFiles(pipelineId);
|
||||
const file = files.find((file) => file.name === name);
|
||||
if (!file) return null;
|
||||
return await this.getFileUrlById(file.project_id, file.file_id);
|
||||
return await LLamaCloudFileService.getFileUrlById(
|
||||
file.project_id,
|
||||
file.file_id,
|
||||
);
|
||||
}
|
||||
|
||||
private static async getFileUrlById(
|
||||
@@ -104,11 +145,10 @@ export class LLamaCloudFileService {
|
||||
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 response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: LLamaCloudFileService.headers,
|
||||
});
|
||||
const data = (await response.json()) as { url: string };
|
||||
return data.url;
|
||||
}
|
||||
@@ -117,12 +157,31 @@ export class LLamaCloudFileService {
|
||||
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 response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: LLamaCloudFileService.headers,
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
private static async getAllProjects(): Promise<LLamaCloudProject[]> {
|
||||
const url = `${LLAMA_CLOUD_BASE_URL}/projects`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: LLamaCloudFileService.headers,
|
||||
});
|
||||
const data = (await response.json()) as LLamaCloudProject[];
|
||||
return data;
|
||||
}
|
||||
|
||||
private static async getAllPipelines(): Promise<LLamaCloudPipeline[]> {
|
||||
const url = `${LLAMA_CLOUD_BASE_URL}/pipelines`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: LLamaCloudFileService.headers,
|
||||
});
|
||||
const data = (await response.json()) as LLamaCloudPipeline[];
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ from llama_index.indices.managed.llama_cloud import LlamaCloudIndex
|
||||
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
def get_index():
|
||||
name = os.getenv("LLAMA_CLOUD_INDEX_NAME")
|
||||
project_name = os.getenv("LLAMA_CLOUD_PROJECT_NAME")
|
||||
def get_index(params=None):
|
||||
configParams = params or {}
|
||||
pipelineConfig = configParams.get("llamaCloudPipeline", {})
|
||||
name = pipelineConfig.get("pipeline", os.getenv("LLAMA_CLOUD_INDEX_NAME"))
|
||||
project_name = pipelineConfig.get("project", 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")
|
||||
|
||||
@@ -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 "nin" 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=["true"],
|
||||
operator="nin", # type: ignore
|
||||
)
|
||||
selected_doc_filter = MetadataFilter(
|
||||
key="doc_id",
|
||||
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
|
||||
@@ -17,7 +17,7 @@ def get_storage_context(persist_dir: str) -> StorageContext:
|
||||
return StorageContext.from_defaults(persist_dir=persist_dir)
|
||||
|
||||
|
||||
def get_index():
|
||||
def get_index(params=None):
|
||||
storage_dir = os.getenv("STORAGE_DIR", "storage")
|
||||
# check if storage already exists
|
||||
if not os.path.exists(storage_dir):
|
||||
|
||||
@@ -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,33 @@
|
||||
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,12 +1,26 @@
|
||||
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,
|
||||
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: ["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",
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
|
||||
@@ -4,7 +4,6 @@ const REQUIRED_ENV_VARS = [
|
||||
"MILVUS_ADDRESS",
|
||||
"MILVUS_USERNAME",
|
||||
"MILVUS_PASSWORD",
|
||||
"MILVUS_COLLECTION",
|
||||
];
|
||||
|
||||
export function getMilvusClient() {
|
||||
@@ -19,8 +18,13 @@ export function getMilvusClient() {
|
||||
});
|
||||
}
|
||||
|
||||
export function checkRequiredEnvVars() {
|
||||
const missingEnvVars = REQUIRED_ENV_VARS.filter((envVar) => {
|
||||
export function checkRequiredEnvVars(opts?: { checkCollectionEnv?: boolean }) {
|
||||
const shouldCheckCollectionEnv = opts?.checkCollectionEnv ?? true; // default to true
|
||||
const requiredEnvVars = [...REQUIRED_ENV_VARS]; // create a copy of the array
|
||||
if (shouldCheckCollectionEnv) {
|
||||
requiredEnvVars.push("MILVUS_COLLECTION");
|
||||
}
|
||||
const missingEnvVars = requiredEnvVars.filter((envVar) => {
|
||||
return !process.env[envVar];
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/* 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 {
|
||||
MongoDBAtlasVectorSearch,
|
||||
VectorStoreIndex,
|
||||
storageContextFromDefaults,
|
||||
} from "llamaindex";
|
||||
import { MongoClient } from "mongodb";
|
||||
import { getDocuments } from "./loader";
|
||||
import { initSettings } from "./settings";
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
import { VectorStoreIndex } from "llamaindex";
|
||||
import { MongoDBAtlasVectorSearch } from "llamaindex/storage/vectorStore/MongoDBAtlasVectorSearch";
|
||||
import { MongoDBAtlasVectorSearch, VectorStoreIndex } from "llamaindex";
|
||||
import { MongoClient } from "mongodb";
|
||||
import { checkRequiredEnvVars } from "./shared";
|
||||
|
||||
export async function getDataSource() {
|
||||
export async function getDataSource(params?: any) {
|
||||
checkRequiredEnvVars();
|
||||
const client = new MongoClient(process.env.MONGO_URI!);
|
||||
const store = new MongoDBAtlasVectorSearch({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"dotenv": "^16.3.1",
|
||||
"duck-duck-scrape": "^2.2.5",
|
||||
"express": "^4.18.2",
|
||||
"llamaindex": "0.5.8",
|
||||
"llamaindex": "0.5.14",
|
||||
"pdf2json": "3.0.5",
|
||||
"ajv": "^8.12.0",
|
||||
"@e2b/code-interpreter": "^0.0.5",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Request, Response } from "express";
|
||||
import { LLamaCloudFileService } from "./llamaindex/streaming/service";
|
||||
|
||||
export const chatConfig = async (_req: Request, res: Response) => {
|
||||
let starterQuestions = undefined;
|
||||
@@ -12,3 +13,14 @@ export const chatConfig = async (_req: Request, res: Response) => {
|
||||
starterQuestions,
|
||||
});
|
||||
};
|
||||
|
||||
export const chatLlamaCloudConfig = async (_req: Request, res: Response) => {
|
||||
const config = {
|
||||
projects: await LLamaCloudFileService.getAllProjectsWithPipelines(),
|
||||
pipeline: {
|
||||
pipeline: process.env.LLAMA_CLOUD_INDEX_NAME,
|
||||
project: process.env.LLAMA_CLOUD_PROJECT_NAME,
|
||||
},
|
||||
};
|
||||
return res.status(200).json(config);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Request, Response } from "express";
|
||||
import { getDataSource } from "./engine";
|
||||
import { uploadDocument } from "./llamaindex/documents/upload";
|
||||
|
||||
export const chatUpload = async (req: Request, res: Response) => {
|
||||
@@ -8,5 +9,6 @@ export const chatUpload = async (req: Request, res: Response) => {
|
||||
error: "base64 is required in the request body",
|
||||
});
|
||||
}
|
||||
return res.status(200).json(await uploadDocument(base64));
|
||||
const index = await getDataSource();
|
||||
return res.status(200).json(await uploadDocument(index, base64));
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ export const chat = async (req: Request, res: Response) => {
|
||||
const vercelStreamData = new StreamData();
|
||||
const streamTimeout = createStreamTimeout(vercelStreamData);
|
||||
try {
|
||||
const { messages }: { messages: Message[] } = req.body;
|
||||
const { messages, data }: { messages: Message[]; data?: any } = req.body;
|
||||
const userMessage = messages.pop();
|
||||
if (!messages || !userMessage || userMessage.role !== "user") {
|
||||
return res.status(400).json({
|
||||
@@ -46,7 +46,7 @@ export const chat = async (req: Request, res: Response) => {
|
||||
},
|
||||
);
|
||||
const ids = retrieveDocumentIds(allAnnotations);
|
||||
const chatEngine = await createChatEngine(ids);
|
||||
const chatEngine = await createChatEngine(ids, data);
|
||||
|
||||
// Convert message content from Vercel/AI format to LlamaIndex/OpenAI format
|
||||
const userMessageContent = convertMessageContent(
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
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] };
|
||||
|
||||
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,5 +1,8 @@
|
||||
import express, { Router } from "express";
|
||||
import { chatConfig } from "../controllers/chat-config.controller";
|
||||
import {
|
||||
chatConfig,
|
||||
chatLlamaCloudConfig,
|
||||
} from "../controllers/chat-config.controller";
|
||||
import { chatRequest } from "../controllers/chat-request.controller";
|
||||
import { chatUpload } from "../controllers/chat-upload.controller";
|
||||
import { chat } from "../controllers/chat.controller";
|
||||
@@ -11,6 +14,7 @@ initSettings();
|
||||
llmRouter.route("/").post(chat);
|
||||
llmRouter.route("/request").post(chatRequest);
|
||||
llmRouter.route("/config").get(chatConfig);
|
||||
llmRouter.route("/config/llamacloud").get(chatLlamaCloudConfig);
|
||||
llmRouter.route("/upload").post(chatUpload);
|
||||
|
||||
export default llmRouter;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request, status
|
||||
from llama_index.core.chat_engine.types import BaseChatEngine, NodeWithScore
|
||||
from llama_index.core.llms import MessageRole
|
||||
from llama_index.core.vector_stores.types import MetadataFilter, MetadataFilters
|
||||
|
||||
from app.api.routers.events import EventCallbackHandler
|
||||
from app.api.routers.models import (
|
||||
ChatConfig,
|
||||
ChatData,
|
||||
Message,
|
||||
Result,
|
||||
@@ -18,6 +15,7 @@ from app.api.routers.models import (
|
||||
from app.api.routers.vercel_response import VercelStreamResponse
|
||||
from app.api.services.llama_cloud import LLamaCloudFileService
|
||||
from app.engine import get_chat_engine
|
||||
from app.engine.query_filter import generate_filters
|
||||
|
||||
chat_router = r = APIRouter()
|
||||
|
||||
@@ -52,8 +50,11 @@ async def chat(
|
||||
|
||||
doc_ids = data.get_chat_document_ids()
|
||||
filters = generate_filters(doc_ids)
|
||||
logger.info("Creating chat engine with filters", filters.dict())
|
||||
chat_engine = get_chat_engine(filters=filters)
|
||||
params = data.data or {}
|
||||
logger.info(
|
||||
f"Creating chat engine with filters: {str(filters)}",
|
||||
)
|
||||
chat_engine = get_chat_engine(filters=filters, params=params)
|
||||
|
||||
event_handler = EventCallbackHandler()
|
||||
chat_engine.callback_manager.handlers.append(event_handler) # type: ignore
|
||||
@@ -70,38 +71,6 @@ async def chat(
|
||||
) from e
|
||||
|
||||
|
||||
def generate_filters(doc_ids):
|
||||
if len(doc_ids) > 0:
|
||||
filters = MetadataFilters(
|
||||
filters=[
|
||||
MetadataFilter(
|
||||
key="private",
|
||||
value=["true"],
|
||||
operator="nin", # type: ignore
|
||||
),
|
||||
MetadataFilter(
|
||||
key="doc_id",
|
||||
value=doc_ids,
|
||||
operator="in", # type: ignore
|
||||
),
|
||||
],
|
||||
condition="or", # type: ignore
|
||||
)
|
||||
else:
|
||||
filters = MetadataFilters(
|
||||
# Use the "NIN" - "not in" operator to include all public documents (don't have the private key set)
|
||||
filters=[
|
||||
MetadataFilter(
|
||||
key="private",
|
||||
value=["true"],
|
||||
operator="nin", # type: ignore
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
# non-streaming endpoint - delete if not needed
|
||||
@r.post("/request")
|
||||
async def chat_request(
|
||||
@@ -116,12 +85,3 @@ async def chat_request(
|
||||
result=Message(role=MessageRole.ASSISTANT, content=response.response),
|
||||
nodes=SourceNodes.from_source_nodes(response.source_nodes),
|
||||
)
|
||||
|
||||
|
||||
@r.get("/config")
|
||||
async def chat_config() -> ChatConfig:
|
||||
starter_questions = None
|
||||
conversation_starters = os.getenv("CONVERSATION_STARTERS")
|
||||
if conversation_starters and conversation_starters.strip():
|
||||
starter_questions = conversation_starters.strip().split("\n")
|
||||
return ChatConfig(starter_questions=starter_questions)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.routers.models import ChatConfig
|
||||
from app.api.services.llama_cloud import LLamaCloudFileService
|
||||
|
||||
config_router = r = APIRouter()
|
||||
|
||||
|
||||
@r.get("")
|
||||
async def chat_config() -> ChatConfig:
|
||||
starter_questions = None
|
||||
conversation_starters = os.getenv("CONVERSATION_STARTERS")
|
||||
if conversation_starters and conversation_starters.strip():
|
||||
starter_questions = conversation_starters.strip().split("\n")
|
||||
return ChatConfig(starter_questions=starter_questions)
|
||||
|
||||
|
||||
@r.get("/llamacloud")
|
||||
async def chat_llama_cloud_config():
|
||||
projects = LLamaCloudFileService.get_all_projects_with_pipelines()
|
||||
pipeline = os.getenv("LLAMA_CLOUD_INDEX_NAME")
|
||||
project = os.getenv("LLAMA_CLOUD_PROJECT_NAME")
|
||||
pipeline_config = (
|
||||
pipeline
|
||||
and project
|
||||
and {
|
||||
"pipeline": pipeline,
|
||||
"project": project,
|
||||
}
|
||||
or None
|
||||
)
|
||||
return {
|
||||
"projects": projects,
|
||||
"pipeline": pipeline_config,
|
||||
}
|
||||
@@ -75,6 +75,7 @@ class Message(BaseModel):
|
||||
|
||||
class ChatData(BaseModel):
|
||||
messages: List[Message]
|
||||
data: Any = None
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
@@ -237,7 +238,7 @@ class ChatConfig(BaseModel):
|
||||
starter_questions: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
description="List of starter questions",
|
||||
serialization_alias="starterQuestions"
|
||||
serialization_alias="starterQuestions",
|
||||
)
|
||||
|
||||
class Config:
|
||||
|
||||
@@ -80,7 +80,7 @@ class VercelStreamResponse(StreamingResponse):
|
||||
"type": "sources",
|
||||
"data": {
|
||||
"nodes": [
|
||||
SourceNodes.from_source_node(node).dict()
|
||||
SourceNodes.from_source_node(node).model_dump()
|
||||
for node in response.source_nodes
|
||||
]
|
||||
},
|
||||
|
||||
@@ -14,6 +14,32 @@ class LLamaCloudFileService:
|
||||
|
||||
DOWNLOAD_FILE_NAME_TPL = "{pipeline_id}${filename}"
|
||||
|
||||
@classmethod
|
||||
def get_all_projects(cls) -> List[Dict[str, Any]]:
|
||||
url = f"{cls.LLAMA_CLOUD_URL}/projects"
|
||||
return cls._make_request(url)
|
||||
|
||||
@classmethod
|
||||
def get_all_pipelines(cls) -> List[Dict[str, Any]]:
|
||||
url = f"{cls.LLAMA_CLOUD_URL}/pipelines"
|
||||
return cls._make_request(url)
|
||||
|
||||
@classmethod
|
||||
def get_all_projects_with_pipelines(cls) -> List[Dict[str, Any]]:
|
||||
try:
|
||||
projects = cls.get_all_projects()
|
||||
pipelines = cls.get_all_pipelines()
|
||||
return [
|
||||
{
|
||||
**project,
|
||||
"pipelines": [p 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 _get_files(cls, pipeline_id: str) -> List[Dict[str, Any]]:
|
||||
url = f"{cls.LLAMA_CLOUD_URL}/pipelines/{pipeline_id}/files"
|
||||
|
||||
@@ -6,7 +6,7 @@ from app.engine.vectordb import get_vector_store
|
||||
logger = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
def get_index():
|
||||
def get_index(params=None):
|
||||
logger.info("Connecting vector store...")
|
||||
store = get_vector_store()
|
||||
# Load the index from the vector store
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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
|
||||
)
|
||||
selected_doc_filter = MetadataFilter(
|
||||
key="doc_id",
|
||||
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
|
||||
@@ -4,17 +4,18 @@ load_dotenv()
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import uvicorn
|
||||
from app.api.routers.chat import chat_router
|
||||
from app.api.routers.chat_config import config_router
|
||||
from app.api.routers.upload import file_upload_router
|
||||
from app.observability import init_observability
|
||||
from app.settings import init_settings
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import RedirectResponse
|
||||
from app.api.routers.chat import chat_router
|
||||
from app.api.routers.upload import file_upload_router
|
||||
from app.settings import init_settings
|
||||
from app.observability import init_observability
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
init_settings()
|
||||
@@ -54,6 +55,7 @@ mount_static_files("data", "/api/files/data")
|
||||
mount_static_files("output", "/api/files/output")
|
||||
|
||||
app.include_router(chat_router, prefix="/api/chat")
|
||||
app.include_router(config_router, prefix="/api/chat/config")
|
||||
app.include_router(file_upload_router, prefix="/api/chat/upload")
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { LLamaCloudFileService } from "../../llamaindex/streaming/service";
|
||||
|
||||
/**
|
||||
* This API is to get config from the backend envs and expose them to the frontend
|
||||
*/
|
||||
export async function GET() {
|
||||
const config = {
|
||||
projects: await LLamaCloudFileService.getAllProjectsWithPipelines(),
|
||||
pipeline: {
|
||||
pipeline: process.env.LLAMA_CLOUD_INDEX_NAME,
|
||||
project: process.env.LLAMA_CLOUD_PROJECT_NAME,
|
||||
},
|
||||
};
|
||||
return NextResponse.json(config, { status: 200 });
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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] };
|
||||
|
||||
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",
|
||||
};
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { messages }: { messages: Message[] } = body;
|
||||
const { messages, data }: { messages: Message[]; data?: any } = body;
|
||||
const userMessage = messages.pop();
|
||||
if (!messages || !userMessage || userMessage.role !== "user") {
|
||||
return NextResponse.json(
|
||||
@@ -59,7 +59,7 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
);
|
||||
const ids = retrieveDocumentIds(allAnnotations);
|
||||
const chatEngine = await createChatEngine(ids);
|
||||
const chatEngine = await createChatEngine(ids, data);
|
||||
|
||||
// Convert message content from Vercel/AI format to LlamaIndex/OpenAI format
|
||||
const userMessageContent = convertMessageContent(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getDataSource } from "../engine";
|
||||
import { initSettings } from "../engine/settings";
|
||||
import { uploadDocument } from "../llamaindex/documents/upload";
|
||||
|
||||
@@ -16,7 +17,13 @@ export async function POST(request: NextRequest) {
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
return NextResponse.json(await uploadDocument(base64));
|
||||
const index = await getDataSource();
|
||||
if (!index) {
|
||||
throw new Error(
|
||||
`StorageContext is empty - call 'npm run generate' to generate the storage first`,
|
||||
);
|
||||
}
|
||||
return NextResponse.json(await uploadDocument(index, base64));
|
||||
} catch (error) {
|
||||
console.error("[Upload API]", error);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useChat } from "ai/react";
|
||||
import { useState } from "react";
|
||||
import { ChatInput, ChatMessages } from "./ui/chat";
|
||||
import { useClientConfig } from "./ui/chat/hooks/use-config";
|
||||
|
||||
export default function ChatSection() {
|
||||
const { backend } = useClientConfig();
|
||||
const [requestData, setRequestData] = useState<any>();
|
||||
const {
|
||||
messages,
|
||||
input,
|
||||
@@ -17,6 +19,7 @@ export default function ChatSection() {
|
||||
append,
|
||||
setInput,
|
||||
} = useChat({
|
||||
body: { data: requestData },
|
||||
api: `${backend}/api/chat`,
|
||||
headers: {
|
||||
"Content-Type": "application/json", // using JSON because of vercel/ai 2.2.26
|
||||
@@ -45,6 +48,7 @@ export default function ChatSection() {
|
||||
messages={messages}
|
||||
append={append}
|
||||
setInput={setInput}
|
||||
setRequestData={setRequestData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Input } from "../input";
|
||||
import UploadImagePreview from "../upload-image-preview";
|
||||
import { ChatHandler } from "./chat.interface";
|
||||
import { useFile } from "./hooks/use-file";
|
||||
import { LlamaCloudSelector } from "./widgets/LlamaCloudSelector";
|
||||
|
||||
const ALLOWED_EXTENSIONS = ["png", "jpg", "jpeg", "csv", "pdf", "txt", "docx"];
|
||||
|
||||
@@ -21,7 +22,10 @@ export default function ChatInput(
|
||||
| "messages"
|
||||
| "setInput"
|
||||
| "append"
|
||||
>,
|
||||
> & {
|
||||
requestParams?: any;
|
||||
setRequestData?: React.Dispatch<any>;
|
||||
},
|
||||
) {
|
||||
const {
|
||||
imageUrl,
|
||||
@@ -64,7 +68,7 @@ export default function ChatInput(
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await uploadFile(file);
|
||||
await uploadFile(file, props.requestParams);
|
||||
props.onFileUpload?.(file);
|
||||
} catch (error: any) {
|
||||
props.onFileError?.(error.message);
|
||||
@@ -107,6 +111,10 @@ export default function ChatInput(
|
||||
disabled: props.isLoading,
|
||||
}}
|
||||
/>
|
||||
{process.env.NEXT_PUBLIC_USE_LLAMACLOUD === "true" &&
|
||||
props.setRequestData && (
|
||||
<LlamaCloudSelector setRequestData={props.setRequestData} />
|
||||
)}
|
||||
<Button type="submit" disabled={props.isLoading || !props.input.trim()}>
|
||||
Send message
|
||||
</Button>
|
||||
|
||||
+158
-90
@@ -1,123 +1,191 @@
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { Check, Copy, FileText } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useMemo } from "react";
|
||||
import { Button } from "../../button";
|
||||
import { FileIcon } from "../../document-preview";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "../../hover-card";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { useCopyToClipboard } from "../hooks/use-copy-to-clipboard";
|
||||
import { SourceData } from "../index";
|
||||
import { DocumentFileType, SourceData, SourceNode } from "../index";
|
||||
import PdfDialog from "../widgets/PdfDialog";
|
||||
|
||||
const SCORE_THRESHOLD = 0.3;
|
||||
const SCORE_THRESHOLD = 0.25;
|
||||
|
||||
function SourceNumberButton({ index }: { index: number }) {
|
||||
return (
|
||||
<div className="text-xs w-5 h-5 rounded-full bg-gray-100 mb-2 flex items-center justify-center hover:text-white hover:bg-primary hover:cursor-pointer">
|
||||
{index + 1}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type NodeInfo = {
|
||||
id: string;
|
||||
url?: string;
|
||||
type Document = {
|
||||
url: string;
|
||||
sources: SourceNode[];
|
||||
};
|
||||
|
||||
export function ChatSources({ data }: { data: SourceData }) {
|
||||
const sources: NodeInfo[] = useMemo(() => {
|
||||
// aggregate nodes by url or file_path (get the highest one by score)
|
||||
const nodesByPath: { [path: string]: NodeInfo } = {};
|
||||
|
||||
const documents: Document[] = useMemo(() => {
|
||||
// group nodes by document (a document must have a URL)
|
||||
const nodesByUrl: Record<string, SourceNode[]> = {};
|
||||
data.nodes
|
||||
.filter((node) => (node.score ?? 1) > SCORE_THRESHOLD)
|
||||
.filter((node) => isValidUrl(node.url))
|
||||
.sort((a, b) => (b.score ?? 1) - (a.score ?? 1))
|
||||
.forEach((node) => {
|
||||
const nodeInfo = {
|
||||
id: node.id,
|
||||
url: node.url,
|
||||
};
|
||||
const key = nodeInfo.url ?? nodeInfo.id; // use id as key for UNKNOWN type
|
||||
if (!nodesByPath[key]) {
|
||||
nodesByPath[key] = nodeInfo;
|
||||
}
|
||||
const key = node.url!.replace(/\/$/, ""); // remove trailing slash
|
||||
nodesByUrl[key] ??= [];
|
||||
nodesByUrl[key].push(node);
|
||||
});
|
||||
|
||||
return Object.values(nodesByPath);
|
||||
// convert to array of documents
|
||||
return Object.entries(nodesByUrl).map(([url, sources]) => ({
|
||||
url,
|
||||
sources,
|
||||
}));
|
||||
}, [data.nodes]);
|
||||
|
||||
if (sources.length === 0) return null;
|
||||
if (documents.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="space-x-2 text-sm">
|
||||
<span className="font-semibold">Sources:</span>
|
||||
<div className="inline-flex gap-1 items-center">
|
||||
{sources.map((nodeInfo: NodeInfo, index: number) => {
|
||||
if (nodeInfo.url?.endsWith(".pdf")) {
|
||||
return (
|
||||
<PdfDialog
|
||||
key={nodeInfo.id}
|
||||
documentId={nodeInfo.id}
|
||||
url={nodeInfo.url!}
|
||||
trigger={<SourceNumberButton index={index} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={nodeInfo.id}>
|
||||
<HoverCard>
|
||||
<HoverCardTrigger>
|
||||
<SourceNumberButton index={index} />
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-[320px]">
|
||||
<NodeInfo nodeInfo={nodeInfo} />
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
);
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="font-semibold text-lg">Sources:</div>
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
{documents.map((document) => {
|
||||
return <DocumentInfo key={document.url} document={document} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NodeInfo({ nodeInfo }: { nodeInfo: NodeInfo }) {
|
||||
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 1000 });
|
||||
|
||||
if (nodeInfo.url) {
|
||||
// this is a node generated by the web loader or file loader,
|
||||
// add a link to view its URL and a button to copy the URL to the clipboard
|
||||
return (
|
||||
<div className="flex items-center my-2">
|
||||
<a
|
||||
className="hover:text-blue-900 truncate"
|
||||
href={nodeInfo.url}
|
||||
target="_blank"
|
||||
>
|
||||
<span>{nodeInfo.url}</span>
|
||||
</a>
|
||||
<Button
|
||||
onClick={() => copyToClipboard(nodeInfo.url!)}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-12 w-12 shrink-0"
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// node generated by unknown loader, implement renderer by analyzing logged out metadata
|
||||
function SourceNumberButton({ index }: { index: number }) {
|
||||
return (
|
||||
<p>
|
||||
Sorry, unknown node type. Please add a new renderer in the NodeInfo
|
||||
component.
|
||||
</p>
|
||||
<div className="text-xs w-5 h-5 rounded-full bg-gray-100 flex items-center justify-center hover:text-white hover:bg-primary ">
|
||||
{index + 1}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DocumentInfo({ document }: { document: Document }) {
|
||||
if (!document.sources.length) return null;
|
||||
const { url, sources } = document;
|
||||
const fileName = sources[0].metadata.file_name as string | undefined;
|
||||
const fileExt = fileName?.split(".").pop();
|
||||
const fileImage = fileExt ? FileIcon[fileExt as DocumentFileType] : null;
|
||||
|
||||
const DocumentDetail = (
|
||||
<div
|
||||
key={url}
|
||||
className="h-28 w-48 flex flex-col justify-between p-4 border rounded-md shadow-md cursor-pointer"
|
||||
>
|
||||
<p
|
||||
title={fileName}
|
||||
className={cn(
|
||||
fileName ? "truncate" : "text-blue-900 break-words",
|
||||
"text-left",
|
||||
)}
|
||||
>
|
||||
{fileName ?? url}
|
||||
</p>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="space-x-2 flex">
|
||||
{sources.map((node: SourceNode, index: number) => {
|
||||
return (
|
||||
<div key={node.id}>
|
||||
<HoverCard>
|
||||
<HoverCardTrigger
|
||||
className="cursor-default"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<SourceNumberButton index={index} />
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-[400px]">
|
||||
<NodeInfo nodeInfo={node} />
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{fileImage ? (
|
||||
<div className="relative h-8 w-8 shrink-0 overflow-hidden rounded-md">
|
||||
<Image
|
||||
className="h-full w-auto"
|
||||
priority
|
||||
src={fileImage}
|
||||
alt="Icon"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<FileText className="text-gray-500" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (url.endsWith(".pdf")) {
|
||||
// open internal pdf dialog for pdf files when click document card
|
||||
return (
|
||||
<PdfDialog
|
||||
documentId={document.url}
|
||||
url={document.url}
|
||||
trigger={DocumentDetail}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// open external link when click document card for other file types
|
||||
return <div onClick={() => window.open(url, "_blank")}>{DocumentDetail}</div>;
|
||||
}
|
||||
|
||||
function NodeInfo({ nodeInfo }: { nodeInfo: SourceNode }) {
|
||||
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 1000 });
|
||||
|
||||
const pageNumber =
|
||||
// XXX: page_label is used in Python, but page_number is used by Typescript
|
||||
(nodeInfo.metadata?.page_number as number) ??
|
||||
(nodeInfo.metadata?.page_label as number) ??
|
||||
null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-semibold">
|
||||
{pageNumber ? `On page ${pageNumber}:` : "Node content:"}
|
||||
</span>
|
||||
{nodeInfo.text && (
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyToClipboard(nodeInfo.text);
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-12 w-12 shrink-0"
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{nodeInfo.text && (
|
||||
<pre className="max-h-[200px] overflow-auto whitespace-pre-line">
|
||||
“{nodeInfo.text}”
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function isValidUrl(url?: string): boolean {
|
||||
if (!url) return false;
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { Button } from "../button";
|
||||
import ChatActions from "./chat-actions";
|
||||
@@ -13,7 +13,9 @@ export default function ChatMessages(
|
||||
"messages" | "isLoading" | "reload" | "stop" | "append"
|
||||
>,
|
||||
) {
|
||||
const { starterQuestions } = useClientConfig();
|
||||
const { backend } = useClientConfig();
|
||||
const [starterQuestions, setStarterQuestions] = useState<string[]>();
|
||||
|
||||
const scrollableChatContainerRef = useRef<HTMLDivElement>(null);
|
||||
const messageLength = props.messages.length;
|
||||
const lastMessage = props.messages[messageLength - 1];
|
||||
@@ -40,6 +42,19 @@ export default function ChatMessages(
|
||||
scrollToBottom();
|
||||
}, [messageLength, lastMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!starterQuestions) {
|
||||
fetch(`${backend}/api/chat/config`)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data?.starterQuestions) {
|
||||
setStarterQuestions(data.starterQuestions);
|
||||
}
|
||||
})
|
||||
.catch((error) => console.error("Error fetching config", error));
|
||||
}
|
||||
}, [starterQuestions, backend]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-1 w-full rounded-xl bg-white p-4 shadow-xl relative overflow-y-auto"
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -48,7 +48,10 @@ export function useFile() {
|
||||
files.length && setFiles([]);
|
||||
};
|
||||
|
||||
const uploadContent = async (base64: string): Promise<string[]> => {
|
||||
const uploadContent = async (
|
||||
base64: string,
|
||||
requestParams: any = {},
|
||||
): Promise<string[]> => {
|
||||
const uploadAPI = `${backend}/api/chat/upload`;
|
||||
const response = await fetch(uploadAPI, {
|
||||
method: "POST",
|
||||
@@ -57,6 +60,7 @@ export function useFile() {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
base64,
|
||||
...requestParams,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to upload document.");
|
||||
@@ -98,7 +102,7 @@ export function useFile() {
|
||||
return content;
|
||||
};
|
||||
|
||||
const uploadFile = async (file: File) => {
|
||||
const uploadFile = async (file: File, requestParams: any = {}) => {
|
||||
if (file.type.startsWith("image/")) {
|
||||
const base64 = await readContent({ file, asUrl: true });
|
||||
return setImageUrl(base64);
|
||||
@@ -125,7 +129,7 @@ export function useFile() {
|
||||
}
|
||||
default: {
|
||||
const base64 = await readContent({ file, asUrl: true });
|
||||
const ids = await uploadContent(base64);
|
||||
const ids = await uploadContent(base64, requestParams);
|
||||
return addDoc({
|
||||
...newDoc,
|
||||
content: {
|
||||
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../select";
|
||||
import { useClientConfig } from "../hooks/use-config";
|
||||
|
||||
type LLamaCloudPipeline = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type LLamaCloudProject = {
|
||||
id: string;
|
||||
organization_id: string;
|
||||
name: string;
|
||||
is_default: boolean;
|
||||
pipelines: Array<LLamaCloudPipeline>;
|
||||
};
|
||||
|
||||
type PipelineConfig = {
|
||||
project: string; // project name
|
||||
pipeline: string; // pipeline name
|
||||
};
|
||||
|
||||
type LlamaCloudConfig = {
|
||||
projects?: LLamaCloudProject[];
|
||||
pipeline?: PipelineConfig;
|
||||
};
|
||||
|
||||
export interface LlamaCloudSelectorProps {
|
||||
setRequestData?: React.Dispatch<any>;
|
||||
onSelect?: (pipeline: PipelineConfig | undefined) => void;
|
||||
defaultPipeline?: PipelineConfig;
|
||||
shouldCheckValid?: boolean;
|
||||
}
|
||||
|
||||
export function LlamaCloudSelector({
|
||||
setRequestData,
|
||||
onSelect,
|
||||
defaultPipeline,
|
||||
shouldCheckValid = true,
|
||||
}: LlamaCloudSelectorProps) {
|
||||
const { backend } = useClientConfig();
|
||||
const [config, setConfig] = useState<LlamaCloudConfig>();
|
||||
|
||||
const updateRequestParams = useCallback(
|
||||
(pipeline?: PipelineConfig) => {
|
||||
if (setRequestData) {
|
||||
setRequestData({
|
||||
llamaCloudPipeline: pipeline,
|
||||
});
|
||||
} else {
|
||||
onSelect?.(pipeline);
|
||||
}
|
||||
},
|
||||
[onSelect, setRequestData],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env.NEXT_PUBLIC_USE_LLAMACLOUD === "true" && !config) {
|
||||
fetch(`${backend}/api/chat/config/llamacloud`)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
const pipeline = defaultPipeline ?? data.pipeline; // defaultPipeline will override pipeline in .env
|
||||
setConfig({ ...data, pipeline });
|
||||
updateRequestParams(pipeline);
|
||||
})
|
||||
.catch((error) => console.error("Error fetching config", error));
|
||||
}
|
||||
}, [backend, config, defaultPipeline, updateRequestParams]);
|
||||
|
||||
const setPipeline = (pipelineConfig?: PipelineConfig) => {
|
||||
setConfig((prevConfig: any) => ({
|
||||
...prevConfig,
|
||||
pipeline: pipelineConfig,
|
||||
}));
|
||||
updateRequestParams(pipelineConfig);
|
||||
};
|
||||
|
||||
const handlePipelineSelect = async (value: string) => {
|
||||
setPipeline(JSON.parse(value) as PipelineConfig);
|
||||
};
|
||||
|
||||
if (!config) {
|
||||
return (
|
||||
<div className="flex justify-center items-center p-3">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!isValid(config) && shouldCheckValid) {
|
||||
return (
|
||||
<p className="text-red-500">
|
||||
Invalid LlamaCloud configuration. Check console logs.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
const { projects, pipeline } = config;
|
||||
|
||||
return (
|
||||
<Select
|
||||
onValueChange={handlePipelineSelect}
|
||||
defaultValue={JSON.stringify(pipeline)}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="Select a pipeline" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{projects!.map((project: LLamaCloudProject) => (
|
||||
<SelectGroup key={project.id}>
|
||||
<SelectLabel className="capitalize">
|
||||
Project: {project.name}
|
||||
</SelectLabel>
|
||||
{project.pipelines.map((pipeline) => (
|
||||
<SelectItem
|
||||
key={pipeline.id}
|
||||
className="last:border-b"
|
||||
value={JSON.stringify({
|
||||
pipeline: pipeline.name,
|
||||
project: project.name,
|
||||
})}
|
||||
>
|
||||
<span className="pl-2">{pipeline.name}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
function isValid(config: LlamaCloudConfig): boolean {
|
||||
const { projects, pipeline } = config;
|
||||
if (!projects?.length) return false;
|
||||
if (!pipeline) return false;
|
||||
const matchedProject = projects.find(
|
||||
(project: LLamaCloudProject) => project.name === pipeline.project,
|
||||
);
|
||||
if (!matchedProject) {
|
||||
console.error(
|
||||
`LlamaCloud project ${pipeline.project} not found. Check LLAMA_CLOUD_PROJECT_NAME variable`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const pipelineExists = matchedProject.pipelines.some(
|
||||
(p) => p.name === pipeline.pipeline,
|
||||
);
|
||||
if (!pipelineExists) {
|
||||
console.error(
|
||||
`LlamaCloud pipeline ${pipeline.pipeline} not found. Check LLAMA_CLOUD_INDEX_NAME variable`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -31,7 +31,7 @@ const PdfFocusProvider = dynamic(
|
||||
export default function PdfDialog(props: PdfDialogProps) {
|
||||
return (
|
||||
<Drawer direction="left">
|
||||
<DrawerTrigger>{props.trigger}</DrawerTrigger>
|
||||
<DrawerTrigger asChild>{props.trigger}</DrawerTrigger>
|
||||
<DrawerContent className="w-3/5 mt-24 h-full max-h-[96%] ">
|
||||
<DrawerHeader className="flex justify-between">
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -64,7 +64,7 @@ export function DocumentPreview(props: DocumentPreviewProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const FileIcon: Record<DocumentFileType, string> = {
|
||||
export const FileIcon: Record<DocumentFileType, string> = {
|
||||
csv: SheetIcon,
|
||||
pdf: PdfIcon,
|
||||
docx: DocxIcon,
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
"use client";
|
||||
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"experimental": {
|
||||
"outputFileTracingIncludes": {
|
||||
"/*": ["./cache/**/*"]
|
||||
"/*": ["./cache/**/*"],
|
||||
"/api/**/*": ["./node_modules/**/*.wasm"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@llamaindex/pdf-viewer": "^1.1.3",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"ai": "^3.0.21",
|
||||
"ajv": "^8.12.0",
|
||||
@@ -24,7 +25,7 @@
|
||||
"duck-duck-scrape": "^2.2.5",
|
||||
"formdata-node": "^6.0.3",
|
||||
"got": "^14.4.1",
|
||||
"llamaindex": "0.5.8",
|
||||
"llamaindex": "0.5.14",
|
||||
"lucide-react": "^0.294.0",
|
||||
"next": "^14.2.4",
|
||||
"react": "^18.2.0",
|
||||
|
||||
Reference in New Issue
Block a user