feat: support file server for python llamadeploy (#703)

* feat: support file server for python llamadeploy

* Create wise-ways-knock.md

* release chat-ui
This commit is contained in:
Thuc Pham
2025-07-10 16:38:00 +07:00
committed by GitHub
parent 2b85420370
commit 91ce4e1236
9 changed files with 124 additions and 14 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@llamaindex/server": patch
---
feat: support file server for python llamadeploy
@@ -1,11 +1,14 @@
import fs from "fs";
import { LLamaCloudFileService } from "llamaindex";
import { NextRequest, NextResponse } from "next/server";
import { promisify } from "util";
import { downloadFile } from "../helpers";
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ slug: string[] }> },
) {
const isUsingLlamaCloud = !!process.env.LLAMA_CLOUD_API_KEY;
const filePath = (await params).slug.join("/");
if (!filePath.startsWith("output") && !filePath.startsWith("data")) {
@@ -13,8 +16,37 @@ export async function GET(
}
const decodedFilePath = decodeURIComponent(filePath);
const fileExists = await promisify(fs.exists)(decodedFilePath);
// if using llama cloud and file not exists, download it
if (isUsingLlamaCloud) {
const fileExists = await promisify(fs.exists)(decodedFilePath);
if (!fileExists) {
const { pipeline_id, file_name } =
getLlamaCloudPipelineIdAndFileName(decodedFilePath);
if (pipeline_id && file_name) {
// get the file url from llama cloud
const downloadUrl = await LLamaCloudFileService.getFileUrl(
pipeline_id,
file_name,
);
if (!downloadUrl) {
return NextResponse.json(
{
error: `Cannot create LlamaCloud download url for pipeline_id=${pipeline_id}, file_name=${file_name}`,
},
{ status: 404 },
);
}
// download the LlamaCloud file to local
await downloadFile(downloadUrl, decodedFilePath);
console.log("File downloaded successfully to: ", decodedFilePath);
}
}
}
const fileExists = await promisify(fs.exists)(decodedFilePath);
if (fileExists) {
const fileBuffer = await promisify(fs.readFile)(decodedFilePath);
return new NextResponse(fileBuffer);
@@ -22,3 +54,17 @@ export async function GET(
return NextResponse.json({ error: "File not found" }, { status: 404 });
}
}
function getLlamaCloudPipelineIdAndFileName(filePath: string) {
const fileName = filePath.split("/").pop() ?? ""; // fileName is the last slug part (pipeline_id$file_name)
const delimiterIndex = fileName.indexOf("$"); // delimiter is the first dollar sign in the fileName
if (delimiterIndex === -1) {
return { pipeline_id: "", file_name: "" };
}
const pipeline_id = fileName.slice(0, delimiterIndex); // before delimiter
const file_name = fileName.slice(delimiterIndex + 1); // after delimiter
return { pipeline_id, file_name };
}
@@ -1,5 +1,6 @@
import crypto from "node:crypto";
import fs from "node:fs";
import https from "node:https";
import path from "node:path";
import { type ServerFile } from "@llamaindex/server";
@@ -55,3 +56,37 @@ async function saveFile(filepath: string, content: string | Buffer) {
function sanitizeFileName(fileName: string) {
return fileName.replace(/[^a-zA-Z0-9_-]/g, "_");
}
export async function downloadFile(
urlToDownload: string,
downloadedPath: string,
): Promise<void> {
return new Promise((resolve, reject) => {
const dir = path.dirname(downloadedPath);
fs.mkdirSync(dir, { recursive: true });
const file = fs.createWriteStream(downloadedPath);
https
.get(urlToDownload, (response) => {
if (response.statusCode !== 200) {
reject(
new Error(`Failed to download file: Status ${response.statusCode}`),
);
return;
}
response.pipe(file);
file.on("finish", () => {
file.close();
resolve();
});
file.on("error", (err) => {
fs.unlink(downloadedPath, () => reject(err));
});
})
.on("error", (err) => {
fs.unlink(downloadedPath, () => reject(err));
});
});
}
@@ -38,6 +38,7 @@ export default function ChatSection() {
});
const useChatWorkflowHandler = useChatWorkflow({
fileServerUrl: getConfig("FILE_SERVER_URL"),
deployment,
workflow,
onError: handleError,
@@ -6,9 +6,13 @@ import { getConfig } from "../lib/utils";
export function ChatStarter({ className }: { className?: string }) {
const { append, messages, requestData } = useChatUI();
const starterQuestionsFromConfig = getConfig("STARTER_QUESTIONS");
const starterQuestions =
getConfig("STARTER_QUESTIONS") ??
JSON.parse(process.env.NEXT_PUBLIC_STARTER_QUESTIONS || "[]");
Array.isArray(starterQuestionsFromConfig) &&
starterQuestionsFromConfig?.length > 0
? starterQuestionsFromConfig
: JSON.parse(process.env.NEXT_PUBLIC_STARTER_QUESTIONS || "[]");
if (starterQuestions.length === 0 || messages.length > 0) return null;
return (
+1 -1
View File
@@ -68,7 +68,7 @@
"@babel/traverse": "^7.27.0",
"@babel/types": "^7.27.0",
"@hookform/resolvers": "^5.0.1",
"@llamaindex/chat-ui": "0.5.12",
"@llamaindex/chat-ui": "0.5.16",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.7",
"@radix-ui/react-aspect-ratio": "^1.1.3",
+22 -5
View File
@@ -12,7 +12,6 @@ import type { LlamaDeployConfig, LlamaIndexServerOptions } from "./types";
const nextDir = path.join(__dirname, "..", "server");
const configFile = path.join(__dirname, "..", "server", "public", "config.js");
const nextConfigFile = path.join(nextDir, "next.config.ts");
const layoutFile = path.join(nextDir, "app", "layout.tsx");
const constantsFile = path.join(nextDir, "app", "constants.ts");
const dev = process.env.NODE_ENV !== "production";
@@ -24,6 +23,8 @@ export class LlamaIndexServer {
layoutDir: string;
suggestNextQuestions: boolean;
llamaDeploy?: LlamaDeployConfig | undefined;
serverUrl: string;
fileServer: string;
constructor(options: LlamaIndexServerOptions) {
const { workflow, suggestNextQuestions, ...nextAppOptions } = options;
@@ -33,7 +34,13 @@ export class LlamaIndexServer {
this.componentsDir = options.uiConfig?.componentsDir;
this.layoutDir = options.uiConfig?.layoutDir ?? "layout";
this.suggestNextQuestions = suggestNextQuestions ?? true;
this.llamaDeploy = options.uiConfig?.llamaDeploy;
this.serverUrl = options.uiConfig?.serverUrl || ""; // use current host if not set
const isUsingLlamaCloud = !!getEnv("LLAMA_CLOUD_API_KEY");
const defaultFileServer = isUsingLlamaCloud ? "output/llamacloud" : "data";
this.fileServer = options.fileServer ?? defaultFileServer;
if (this.llamaDeploy) {
if (!this.llamaDeploy.deployment || !this.llamaDeploy.workflow) {
@@ -41,9 +48,13 @@ export class LlamaIndexServer {
"LlamaDeploy requires deployment and workflow to be set",
);
}
if (options.uiConfig?.devMode) {
// workflow file is in llama-deploy src, so we should disable devmode
throw new Error("Devmode is not supported when enabling LlamaDeploy");
const { devMode, llamaCloudIndexSelector, enableFileUpload } =
options.uiConfig ?? {};
if (devMode || llamaCloudIndexSelector || enableFileUpload) {
throw new Error(
"`devMode`, `llamaCloudIndexSelector`, and `enableFileUpload` are not supported when enabling LlamaDeploy",
);
}
} else {
// if llamaDeploy is not set but workflowFactory is not defined, we should throw an error
@@ -103,6 +114,11 @@ export default {
const enableFileUpload = uiConfig?.enableFileUpload ?? false;
const uploadApi = enableFileUpload ? `${basePath}/api/files` : undefined;
// construct file server url for LlamaDeploy
// eg. for Non-LlamaCloud: localhost:3000/deployments/chat/ui/api/files/data
// eg. for LlamaCloud: localhost:3000/deployments/chat/ui/api/files/output/llamacloud
const fileServerUrl = `${this.serverUrl}${basePath}/api/files/${this.fileServer}`;
// content in javascript format
const content = `
window.LLAMAINDEX = {
@@ -115,7 +131,8 @@ export default {
SUGGEST_NEXT_QUESTIONS: ${JSON.stringify(this.suggestNextQuestions)},
UPLOAD_API: ${JSON.stringify(uploadApi)},
DEPLOYMENT: ${JSON.stringify(this.llamaDeploy?.deployment)},
WORKFLOW: ${JSON.stringify(this.llamaDeploy?.workflow)}
WORKFLOW: ${JSON.stringify(this.llamaDeploy?.workflow)},
FILE_SERVER_URL: ${JSON.stringify(fileServerUrl)}
}
`;
fs.writeFileSync(configFile, content);
+2
View File
@@ -25,10 +25,12 @@ export type UIConfig = {
devMode?: boolean;
enableFileUpload?: boolean;
llamaDeploy?: LlamaDeployConfig;
serverUrl?: string;
};
export type LlamaIndexServerOptions = NextAppOptions & {
workflow?: WorkflowFactory;
uiConfig?: UIConfig;
fileServer?: string;
suggestNextQuestions?: boolean;
};
+5 -5
View File
@@ -187,8 +187,8 @@ importers:
specifier: 0.3.25
version: 0.3.25
'@llamaindex/chat-ui':
specifier: 0.5.12
version: 0.5.12(@babel/runtime@7.27.0)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.1)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.37.1)(@lezer/highlight@1.2.1)(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(codemirror@6.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(yjs@13.6.27)
specifier: 0.5.16
version: 0.5.16(@babel/runtime@7.27.0)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.1)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.37.1)(@lezer/highlight@1.2.1)(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(codemirror@6.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(yjs@13.6.27)
'@llamaindex/env':
specifier: ~0.1.30
version: 0.1.30
@@ -1500,8 +1500,8 @@ packages:
zod:
optional: true
'@llamaindex/chat-ui@0.5.12':
resolution: {integrity: sha512-Gi9MDkIakTf1S3mpeOA7vzJtBDUBgD5bcQ6KiKsoEYiEVPtUCeEcMoepTU3KoCssaGe2cvjR4tN6dnaUsiEHWQ==}
'@llamaindex/chat-ui@0.5.16':
resolution: {integrity: sha512-Ty8KD5h2+8UrB4VJJQre46adQYJoaMd/bSQluOPBewmLHtKuJWazKTJCpnmf6XogU3A+odOaPKXLF0CNPQJrmg==}
peerDependencies:
react: ^18.2.0 || ^19.0.0 || ^19.0.0-rc
@@ -8546,7 +8546,7 @@ snapshots:
p-retry: 6.2.1
zod: 3.25.13
'@llamaindex/chat-ui@0.5.12(@babel/runtime@7.27.0)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.1)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.37.1)(@lezer/highlight@1.2.1)(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(codemirror@6.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(yjs@13.6.27)':
'@llamaindex/chat-ui@0.5.16(@babel/runtime@7.27.0)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.1)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.37.1)(@lezer/highlight@1.2.1)(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(codemirror@6.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(yjs@13.6.27)':
dependencies:
'@codemirror/lang-css': 6.3.1
'@codemirror/lang-html': 6.4.9