Compare commits

...

16 Commits

Author SHA1 Message Date
thucpn 3eb05facd1 Merge branch 'lee/add-workflow-editor' into tp/support-dev-mode-for-backend-ts-server 2025-05-15 11:17:47 +07:00
thucpn e55c5cc939 Merge branch 'lee/add-workflow-editor' into tp/support-dev-mode-for-backend-ts-server 2025-05-15 10:03:24 +07:00
thucpn 972fe0d1c1 remove typescript deps 2025-05-15 09:56:11 +07:00
thucpn 7a6a753dda use npx tsc 2025-05-15 09:55:52 +07:00
thucpn 6529cc3693 Merge branch 'lee/add-workflow-editor' into tp/support-dev-mode-for-backend-ts-server 2025-05-14 16:29:34 +07:00
thucpn 2fec57c895 Merge branch 'lee/add-workflow-editor' into tp/support-dev-mode-for-backend-ts-server 2025-05-14 16:11:42 +07:00
thucpn fecec469fd fix format 2025-05-14 16:02:32 +07:00
thucpn 9b0abfe4f0 use temp file to avoid server restart 2025-05-14 16:02:27 +07:00
thucpn e699b274d3 fix: format 2025-05-14 15:36:01 +07:00
thucpn 9eb3226468 validate typescript file 2025-05-14 15:02:58 +07:00
thucpn b888e874c6 Merge branch 'lee/add-workflow-editor' into tp/support-dev-mode-for-backend-ts-server 2025-05-14 14:25:46 +07:00
thucpn 3f0ee7706a Merge branch 'lee/add-workflow-editor' into tp/support-dev-mode-for-backend-ts-server 2025-05-14 14:05:28 +07:00
thucpn c8367b8a33 update message 2025-05-14 13:52:07 +07:00
thucpn 174b448570 Merge branch 'lee/add-workflow-editor' into tp/support-dev-mode-for-backend-ts-server 2025-05-14 11:39:19 +07:00
thucpn 978cc93038 Merge branch 'lee/add-workflow-editor' into tp/support-dev-mode-for-backend-ts-server 2025-05-13 18:26:51 +07:00
thucpn 135629492c feat: support dev mode for backend ts server 2025-05-13 18:12:06 +07:00
6 changed files with 154 additions and 3 deletions
@@ -0,0 +1,21 @@
This example shows how to use the dev mode of the server.
First, we need to set `devMode` to `true` in the `uiConfig` of the server.
```ts
new LlamaIndexServer({
workflow: workflowFactory,
uiConfig: {
appTitle: "Calculator",
devMode: true,
},
port: 6000,
}).start();
```
Export OpenAI API key and start the server in dev mode.
```bash
export OPENAI_API_KEY=<your-openai-api-key>
npx tsx watch index.ts
```
+15
View File
@@ -0,0 +1,15 @@
import { LlamaIndexServer } from "@llamaindex/server";
import { workflowFactory } from "./src/app/workflow";
new LlamaIndexServer({
workflow: workflowFactory,
uiConfig: {
appTitle: "Calculator",
devMode: true,
starterQuestions: [
"What is the weather in Tokyo?",
"What is the weather in New York?",
],
},
port: 6005,
}).start();
@@ -0,0 +1,16 @@
import { agent } from "@llamaindex/workflow";
import { tool } from "llamaindex";
import { z } from "zod";
export const workflowFactory = async () => {
return agent({
tools: [
tool({
name: "weather",
description: "Get the weather in a specific city",
parameters: z.object({ city: z.string() }),
execute: ({ city }) => `The weather in ${city} is sunny`,
}),
],
});
};
+84 -1
View File
@@ -1,7 +1,9 @@
import { exec } from "child_process";
import fs from "fs";
import type { IncomingMessage, ServerResponse } from "http";
import path from "path";
import { promisify } from "util";
import { sendJSONResponse } from "../utils/request";
import { parseRequestBody, sendJSONResponse } from "../utils/request";
export const handleServeFiles = async (
req: IncomingMessage,
@@ -21,3 +23,84 @@ export const handleServeFiles = async (
return sendJSONResponse(res, 404, { error: "File not found" });
}
};
const DEFAULT_WORKFLOW_FILE_PATH = "src/app/workflow.ts"; // TODO: we can make it as a parameter in server later
export const getWorkflowFile = async (
req: IncomingMessage,
res: ServerResponse,
filePath: string = DEFAULT_WORKFLOW_FILE_PATH,
) => {
const fileExists = await promisify(fs.exists)(filePath);
if (!fileExists) {
return sendJSONResponse(res, 404, {
detail: `Dev mode is currently in beta. It only supports updating workflow file at ${DEFAULT_WORKFLOW_FILE_PATH}`,
});
}
const content = await promisify(fs.readFile)(filePath, "utf-8");
const last_modified = fs.statSync(filePath).mtime.getTime();
sendJSONResponse(res, 200, { content, file_path: filePath, last_modified });
};
export const updateWorkflowFile = async (
req: IncomingMessage,
res: ServerResponse,
filePath: string = DEFAULT_WORKFLOW_FILE_PATH,
) => {
const body = await parseRequestBody(req);
const { content } = body as { content: string };
const fileExists = await promisify(fs.exists)(filePath);
if (!fileExists) {
return sendJSONResponse(res, 404, {
detail: `Dev mode is currently in beta. It only supports updating workflow file at ${DEFAULT_WORKFLOW_FILE_PATH}`,
});
}
try {
const resolvedFilePath = path.resolve(DEFAULT_WORKFLOW_FILE_PATH);
const result = await validateTypeScriptFile(resolvedFilePath, content);
if (!result.isValid) {
return sendJSONResponse(res, 400, {
detail: result.errors.join("\n"),
});
}
await promisify(fs.writeFile)(filePath, content);
sendJSONResponse(res, 200, { content });
} catch (error) {
console.error("Error updating workflow file:", error);
sendJSONResponse(res, 500, { error: "Failed to update workflow file" });
}
};
// use typescript package to validate the file syntax and imports
async function validateTypeScriptFile(filePath: string, content: string) {
// Update workflow file directly will cause the server restart immediately.
// So we create a temporary file with the same content in the same directory as the workflow file
// This file will be used to validate the file syntax and imports. It will be deleted after validation.
const tempFilePath = path.join(
path.dirname(filePath),
`workflow_${Date.now()}.ts`,
);
fs.writeFileSync(tempFilePath, content);
const errors = [];
try {
const tscCommand = `npx tsc ${tempFilePath} --noEmit --skipLibCheck true`;
await promisify(exec)(tscCommand);
} catch (error) {
const errorMessage = (error as { stdout: string })?.stdout;
errors.push(errorMessage);
} finally {
// Clean up temporary file
if (fs.existsSync(tempFilePath)) fs.unlinkSync(tempFilePath);
}
return {
isValid: errors.length === 0,
errors: errors,
};
}
+17 -2
View File
@@ -9,8 +9,13 @@ import { promisify } from "util";
import { handleChat } from "./handlers/chat";
import { getLlamaCloudConfig } from "./handlers/cloud";
import { getComponents } from "./handlers/components";
import { handleServeFiles } from "./handlers/files";
import {
getWorkflowFile,
handleServeFiles,
updateWorkflowFile,
} from "./handlers/files";
import type { LlamaIndexServerOptions } from "./types";
const nextDir = path.join(__dirname, "..", "server");
const configFile = path.join(__dirname, "..", "server", "public", "config.js");
const dev = process.env.NODE_ENV !== "production";
@@ -44,6 +49,7 @@ export class LlamaIndexServer {
? "/api/chat/config/llamacloud"
: undefined;
const componentsApi = this.componentsDir ? "/api/components" : undefined;
const devMode = uiConfig?.devMode ?? false;
// content in javascript format
const content = `
@@ -52,7 +58,8 @@ export class LlamaIndexServer {
APP_TITLE: ${JSON.stringify(appTitle)},
LLAMA_CLOUD_API: ${JSON.stringify(llamaCloudApi)},
STARTER_QUESTIONS: ${JSON.stringify(starterQuestions)},
COMPONENTS_API: ${JSON.stringify(componentsApi)}
COMPONENTS_API: ${JSON.stringify(componentsApi)},
DEV_MODE: ${JSON.stringify(devMode)}
}
`;
fs.writeFileSync(configFile, content);
@@ -96,6 +103,14 @@ export class LlamaIndexServer {
return getLlamaCloudConfig(req, res);
}
if (pathname === "/api/dev/files/workflow" && req.method === "GET") {
return getWorkflowFile(req, res);
}
if (pathname === "/api/dev/files/workflow" && req.method === "PUT") {
return updateWorkflowFile(req, res);
}
const handle = this.app.getRequestHandler();
handle(req, res, parsedUrl);
});
+1
View File
@@ -17,6 +17,7 @@ export type UIConfig = {
starterQuestions?: string[];
componentsDir?: string;
llamaCloudIndexSelector?: boolean;
devMode?: boolean;
};
export type LlamaIndexServerOptions = NextAppOptions & {