Skip to content

Commit 91ce4e1

Browse files
authored
feat: support file server for python llamadeploy (#703)
* feat: support file server for python llamadeploy * Create wise-ways-knock.md * release chat-ui
1 parent 2b85420 commit 91ce4e1

File tree

9 files changed

+124
-14
lines changed

9 files changed

+124
-14
lines changed

.changeset/wise-ways-knock.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@llamaindex/server": patch
3+
---
4+
5+
feat: support file server for python llamadeploy
Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,70 @@
11
import fs from "fs";
2+
import { LLamaCloudFileService } from "llamaindex";
23
import { NextRequest, NextResponse } from "next/server";
34
import { promisify } from "util";
5+
import { downloadFile } from "../helpers";
46

57
export async function GET(
68
request: NextRequest,
79
{ params }: { params: Promise<{ slug: string[] }> },
810
) {
11+
const isUsingLlamaCloud = !!process.env.LLAMA_CLOUD_API_KEY;
912
const filePath = (await params).slug.join("/");
1013

1114
if (!filePath.startsWith("output") && !filePath.startsWith("data")) {
1215
return NextResponse.json({ error: "No permission" }, { status: 400 });
1316
}
1417

1518
const decodedFilePath = decodeURIComponent(filePath);
16-
const fileExists = await promisify(fs.exists)(decodedFilePath);
1719

20+
// if using llama cloud and file not exists, download it
21+
if (isUsingLlamaCloud) {
22+
const fileExists = await promisify(fs.exists)(decodedFilePath);
23+
if (!fileExists) {
24+
const { pipeline_id, file_name } =
25+
getLlamaCloudPipelineIdAndFileName(decodedFilePath);
26+
27+
if (pipeline_id && file_name) {
28+
// get the file url from llama cloud
29+
const downloadUrl = await LLamaCloudFileService.getFileUrl(
30+
pipeline_id,
31+
file_name,
32+
);
33+
if (!downloadUrl) {
34+
return NextResponse.json(
35+
{
36+
error: `Cannot create LlamaCloud download url for pipeline_id=${pipeline_id}, file_name=${file_name}`,
37+
},
38+
{ status: 404 },
39+
);
40+
}
41+
42+
// download the LlamaCloud file to local
43+
await downloadFile(downloadUrl, decodedFilePath);
44+
console.log("File downloaded successfully to: ", decodedFilePath);
45+
}
46+
}
47+
}
48+
49+
const fileExists = await promisify(fs.exists)(decodedFilePath);
1850
if (fileExists) {
1951
const fileBuffer = await promisify(fs.readFile)(decodedFilePath);
2052
return new NextResponse(fileBuffer);
2153
} else {
2254
return NextResponse.json({ error: "File not found" }, { status: 404 });
2355
}
2456
}
57+
58+
function getLlamaCloudPipelineIdAndFileName(filePath: string) {
59+
const fileName = filePath.split("/").pop() ?? ""; // fileName is the last slug part (pipeline_id$file_name)
60+
61+
const delimiterIndex = fileName.indexOf("$"); // delimiter is the first dollar sign in the fileName
62+
if (delimiterIndex === -1) {
63+
return { pipeline_id: "", file_name: "" };
64+
}
65+
66+
const pipeline_id = fileName.slice(0, delimiterIndex); // before delimiter
67+
const file_name = fileName.slice(delimiterIndex + 1); // after delimiter
68+
69+
return { pipeline_id, file_name };
70+
}

packages/server/next/app/api/files/helpers.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import crypto from "node:crypto";
22
import fs from "node:fs";
3+
import https from "node:https";
34
import path from "node:path";
45

56
import { type ServerFile } from "@llamaindex/server";
@@ -55,3 +56,37 @@ async function saveFile(filepath: string, content: string | Buffer) {
5556
function sanitizeFileName(fileName: string) {
5657
return fileName.replace(/[^a-zA-Z0-9_-]/g, "_");
5758
}
59+
export async function downloadFile(
60+
urlToDownload: string,
61+
downloadedPath: string,
62+
): Promise<void> {
63+
return new Promise((resolve, reject) => {
64+
const dir = path.dirname(downloadedPath);
65+
fs.mkdirSync(dir, { recursive: true });
66+
const file = fs.createWriteStream(downloadedPath);
67+
68+
https
69+
.get(urlToDownload, (response) => {
70+
if (response.statusCode !== 200) {
71+
reject(
72+
new Error(`Failed to download file: Status ${response.statusCode}`),
73+
);
74+
return;
75+
}
76+
77+
response.pipe(file);
78+
79+
file.on("finish", () => {
80+
file.close();
81+
resolve();
82+
});
83+
84+
file.on("error", (err) => {
85+
fs.unlink(downloadedPath, () => reject(err));
86+
});
87+
})
88+
.on("error", (err) => {
89+
fs.unlink(downloadedPath, () => reject(err));
90+
});
91+
});
92+
}

packages/server/next/app/components/ui/chat/chat-section.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export default function ChatSection() {
3838
});
3939

4040
const useChatWorkflowHandler = useChatWorkflow({
41+
fileServerUrl: getConfig("FILE_SERVER_URL"),
4142
deployment,
4243
workflow,
4344
onError: handleError,

packages/server/next/app/components/ui/chat/chat-starter.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ import { getConfig } from "../lib/utils";
66

77
export function ChatStarter({ className }: { className?: string }) {
88
const { append, messages, requestData } = useChatUI();
9+
const starterQuestionsFromConfig = getConfig("STARTER_QUESTIONS");
10+
911
const starterQuestions =
10-
getConfig("STARTER_QUESTIONS") ??
11-
JSON.parse(process.env.NEXT_PUBLIC_STARTER_QUESTIONS || "[]");
12+
Array.isArray(starterQuestionsFromConfig) &&
13+
starterQuestionsFromConfig?.length > 0
14+
? starterQuestionsFromConfig
15+
: JSON.parse(process.env.NEXT_PUBLIC_STARTER_QUESTIONS || "[]");
1216

1317
if (starterQuestions.length === 0 || messages.length > 0) return null;
1418
return (

packages/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
"@babel/traverse": "^7.27.0",
6969
"@babel/types": "^7.27.0",
7070
"@hookform/resolvers": "^5.0.1",
71-
"@llamaindex/chat-ui": "0.5.12",
71+
"@llamaindex/chat-ui": "0.5.16",
7272
"@radix-ui/react-accordion": "^1.2.3",
7373
"@radix-ui/react-alert-dialog": "^1.1.7",
7474
"@radix-ui/react-aspect-ratio": "^1.1.3",

packages/server/src/server.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type { LlamaDeployConfig, LlamaIndexServerOptions } from "./types";
1212
const nextDir = path.join(__dirname, "..", "server");
1313
const configFile = path.join(__dirname, "..", "server", "public", "config.js");
1414
const nextConfigFile = path.join(nextDir, "next.config.ts");
15-
const layoutFile = path.join(nextDir, "app", "layout.tsx");
1615
const constantsFile = path.join(nextDir, "app", "constants.ts");
1716
const dev = process.env.NODE_ENV !== "production";
1817

@@ -24,6 +23,8 @@ export class LlamaIndexServer {
2423
layoutDir: string;
2524
suggestNextQuestions: boolean;
2625
llamaDeploy?: LlamaDeployConfig | undefined;
26+
serverUrl: string;
27+
fileServer: string;
2728

2829
constructor(options: LlamaIndexServerOptions) {
2930
const { workflow, suggestNextQuestions, ...nextAppOptions } = options;
@@ -33,17 +34,27 @@ export class LlamaIndexServer {
3334
this.componentsDir = options.uiConfig?.componentsDir;
3435
this.layoutDir = options.uiConfig?.layoutDir ?? "layout";
3536
this.suggestNextQuestions = suggestNextQuestions ?? true;
37+
3638
this.llamaDeploy = options.uiConfig?.llamaDeploy;
39+
this.serverUrl = options.uiConfig?.serverUrl || ""; // use current host if not set
40+
41+
const isUsingLlamaCloud = !!getEnv("LLAMA_CLOUD_API_KEY");
42+
const defaultFileServer = isUsingLlamaCloud ? "output/llamacloud" : "data";
43+
this.fileServer = options.fileServer ?? defaultFileServer;
3744

3845
if (this.llamaDeploy) {
3946
if (!this.llamaDeploy.deployment || !this.llamaDeploy.workflow) {
4047
throw new Error(
4148
"LlamaDeploy requires deployment and workflow to be set",
4249
);
4350
}
44-
if (options.uiConfig?.devMode) {
45-
// workflow file is in llama-deploy src, so we should disable devmode
46-
throw new Error("Devmode is not supported when enabling LlamaDeploy");
51+
const { devMode, llamaCloudIndexSelector, enableFileUpload } =
52+
options.uiConfig ?? {};
53+
54+
if (devMode || llamaCloudIndexSelector || enableFileUpload) {
55+
throw new Error(
56+
"`devMode`, `llamaCloudIndexSelector`, and `enableFileUpload` are not supported when enabling LlamaDeploy",
57+
);
4758
}
4859
} else {
4960
// if llamaDeploy is not set but workflowFactory is not defined, we should throw an error
@@ -103,6 +114,11 @@ export default {
103114
const enableFileUpload = uiConfig?.enableFileUpload ?? false;
104115
const uploadApi = enableFileUpload ? `${basePath}/api/files` : undefined;
105116

117+
// construct file server url for LlamaDeploy
118+
// eg. for Non-LlamaCloud: localhost:3000/deployments/chat/ui/api/files/data
119+
// eg. for LlamaCloud: localhost:3000/deployments/chat/ui/api/files/output/llamacloud
120+
const fileServerUrl = `${this.serverUrl}${basePath}/api/files/${this.fileServer}`;
121+
106122
// content in javascript format
107123
const content = `
108124
window.LLAMAINDEX = {
@@ -115,7 +131,8 @@ export default {
115131
SUGGEST_NEXT_QUESTIONS: ${JSON.stringify(this.suggestNextQuestions)},
116132
UPLOAD_API: ${JSON.stringify(uploadApi)},
117133
DEPLOYMENT: ${JSON.stringify(this.llamaDeploy?.deployment)},
118-
WORKFLOW: ${JSON.stringify(this.llamaDeploy?.workflow)}
134+
WORKFLOW: ${JSON.stringify(this.llamaDeploy?.workflow)},
135+
FILE_SERVER_URL: ${JSON.stringify(fileServerUrl)}
119136
}
120137
`;
121138
fs.writeFileSync(configFile, content);

packages/server/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ export type UIConfig = {
2525
devMode?: boolean;
2626
enableFileUpload?: boolean;
2727
llamaDeploy?: LlamaDeployConfig;
28+
serverUrl?: string;
2829
};
2930

3031
export type LlamaIndexServerOptions = NextAppOptions & {
3132
workflow?: WorkflowFactory;
3233
uiConfig?: UIConfig;
34+
fileServer?: string;
3335
suggestNextQuestions?: boolean;
3436
};

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)