From 8ed98bcb0713d7e6c2a0efe498d469e4a4a9511c Mon Sep 17 00:00:00 2001
From: Marcus Schiesser <mail@marcusschiesser.de>
Date: Wed, 1 Nov 2023 12:33:15 +0700
Subject: [PATCH] feat: add streaming express example and align with
 non-streaming one

---
 packages/create-llama/templates/index.ts      |  2 +-
 .../simple/express/README-template.md         | 11 +++---
 .../templates/simple/express/index.ts         |  4 +--
 .../templates/simple/express/package.json     | 12 ++++---
 .../{llm.controller.ts => chat.controller.ts} | 16 ++++-----
 .../routes/{llm.route.ts => chat.route.ts}    |  2 +-
 .../templates/simple/express/tsconfig.json    |  2 --
 .../streaming/express/README-template.md      | 34 ++++++++++++++++++
 .../templates/streaming/express/eslintrc.json |  3 ++
 .../templates/streaming/express/index.ts      | 17 +++++++++
 .../templates/streaming/express/package.json  | 25 +++++++++++++
 .../src/controllers/chat.controller.ts        | 36 +++++++++++++++++++
 .../src/controllers/llamaindex-stream.ts      | 35 ++++++++++++++++++
 .../express/src/routes/chat.route.ts          |  8 +++++
 .../templates/streaming/express/tsconfig.json |  9 +++++
 15 files changed, 188 insertions(+), 28 deletions(-)
 rename packages/create-llama/templates/simple/express/src/controllers/{llm.controller.ts => chat.controller.ts} (67%)
 rename packages/create-llama/templates/simple/express/src/routes/{llm.route.ts => chat.route.ts} (70%)
 create mode 100644 packages/create-llama/templates/streaming/express/README-template.md
 create mode 100644 packages/create-llama/templates/streaming/express/eslintrc.json
 create mode 100644 packages/create-llama/templates/streaming/express/index.ts
 create mode 100644 packages/create-llama/templates/streaming/express/package.json
 create mode 100644 packages/create-llama/templates/streaming/express/src/controllers/chat.controller.ts
 create mode 100644 packages/create-llama/templates/streaming/express/src/controllers/llamaindex-stream.ts
 create mode 100644 packages/create-llama/templates/streaming/express/src/routes/chat.route.ts
 create mode 100644 packages/create-llama/templates/streaming/express/tsconfig.json

diff --git a/packages/create-llama/templates/index.ts b/packages/create-llama/templates/index.ts
index 5bf8a7d3b..6f338b635 100644
--- a/packages/create-llama/templates/index.ts
+++ b/packages/create-llama/templates/index.ts
@@ -72,7 +72,7 @@ export const installTemplate = async ({
     const routeFile = path.join(
       root,
       relativeEngineDestPath,
-      framework === "nextjs" ? "route.ts" : "llm.controller.ts",
+      framework === "nextjs" ? "route.ts" : "chat.controller.ts",
     );
     const routeFileContent = await fs.readFile(routeFile, "utf8");
     const newContent = routeFileContent.replace(
diff --git a/packages/create-llama/templates/simple/express/README-template.md b/packages/create-llama/templates/simple/express/README-template.md
index 2b4d11531..2da2865d5 100644
--- a/packages/create-llama/templates/simple/express/README-template.md
+++ b/packages/create-llama/templates/simple/express/README-template.md
@@ -14,18 +14,15 @@ Second, run the development server:
 npm run dev
 ```
 
-Then call the express API endpoint `/api/llm` to see the result:
+Then call the express API endpoint `/api/chat` to see the result:
 
 ```
-curl --location 'localhost:3000/api/llm' \
+curl --location 'localhost:3000/api/chat' \
 --header 'Content-Type: application/json' \
---data '{
-    "message": "Hello",
-    "chatHistory": []
-}'
+--data '{ "messages": [{ "role": "user", "content": "Hello" }] }'
 ```
 
-You can start editing the API by modifying `src/controllers/llm.controller.ts`. The endpoint auto-updates as you save the file.
+You can start editing the API by modifying `src/controllers/chat.controller.ts`. The endpoint auto-updates as you save the file.
 
 ## Learn More
 
diff --git a/packages/create-llama/templates/simple/express/index.ts b/packages/create-llama/templates/simple/express/index.ts
index 157555572..b73784295 100644
--- a/packages/create-llama/templates/simple/express/index.ts
+++ b/packages/create-llama/templates/simple/express/index.ts
@@ -1,5 +1,5 @@
 import express, { Express, Request, Response } from "express";
-import llmRouter from "./src/routes/llm.route";
+import chatRouter from "./src/routes/chat.route";
 
 const app: Express = express();
 const port = 3000;
@@ -10,7 +10,7 @@ app.get("/", (req: Request, res: Response) => {
   res.send("LlamaIndex Express Server");
 });
 
-app.use("/api/llm", llmRouter);
+app.use("/api/chat", chatRouter);
 
 app.listen(port, () => {
   console.log(`⚡️[server]: Server is running at http://localhost:${port}`);
diff --git a/packages/create-llama/templates/simple/express/package.json b/packages/create-llama/templates/simple/express/package.json
index 3b57d610e..1e20573d8 100644
--- a/packages/create-llama/templates/simple/express/package.json
+++ b/packages/create-llama/templates/simple/express/package.json
@@ -1,11 +1,12 @@
 {
   "name": "llama-index-express",
   "version": "1.0.0",
-  "main": "index.js",
+  "main": "dist/index.js",
+  "type": "module",
   "scripts": {
-    "build": "tsc",
+    "build": "tsup index.ts --format esm --dts",
     "start": "node dist/index.js",
-    "dev": "concurrently \"tsc --watch\" \"nodemon -q dist/index.js\""
+    "dev": "concurrently \"tsup index.ts --format esm --dts --watch\" \"nodemon -q dist/index.js\""
   },
   "dependencies": {
     "express": "^4",
@@ -15,8 +16,9 @@
     "@types/express": "^4",
     "@types/node": "^20",
     "concurrently": "^8",
+    "eslint": "^8",
     "nodemon": "^3",
-    "typescript": "^5",
-    "eslint": "^8"
+    "tsup": "^7",
+    "typescript": "^5"
   }
 }
\ No newline at end of file
diff --git a/packages/create-llama/templates/simple/express/src/controllers/llm.controller.ts b/packages/create-llama/templates/simple/express/src/controllers/chat.controller.ts
similarity index 67%
rename from packages/create-llama/templates/simple/express/src/controllers/llm.controller.ts
rename to packages/create-llama/templates/simple/express/src/controllers/chat.controller.ts
index 7647b649b..b11e10502 100644
--- a/packages/create-llama/templates/simple/express/src/controllers/llm.controller.ts
+++ b/packages/create-llama/templates/simple/express/src/controllers/chat.controller.ts
@@ -4,16 +4,12 @@ import { createChatEngine } from "../../../../engines/context";
 
 export const chat = async (req: Request, res: Response, next: NextFunction) => {
   try {
-    const {
-      message,
-      chatHistory,
-    }: {
-      message: string;
-      chatHistory: ChatMessage[];
-    } = req.body;
-    if (!message || !chatHistory) {
+    const { messages }: { messages: ChatMessage[] } = req.body;
+    const lastMessage = messages.pop();
+    if (!messages || !lastMessage || lastMessage.role !== "user") {
       return res.status(400).json({
-        error: "message, chatHistory are required in the request body",
+        error:
+          "messages are required in the request body and the last message must be from the user",
       });
     }
 
@@ -23,7 +19,7 @@ export const chat = async (req: Request, res: Response, next: NextFunction) => {
 
     const chatEngine = await createChatEngine(llm);
 
-    const response = await chatEngine.chat(message, chatHistory);
+    const response = await chatEngine.chat(lastMessage.content, messages);
     const result: ChatMessage = {
       role: "assistant",
       content: response.response,
diff --git a/packages/create-llama/templates/simple/express/src/routes/llm.route.ts b/packages/create-llama/templates/simple/express/src/routes/chat.route.ts
similarity index 70%
rename from packages/create-llama/templates/simple/express/src/routes/llm.route.ts
rename to packages/create-llama/templates/simple/express/src/routes/chat.route.ts
index 3711c71b9..bdfeb0853 100644
--- a/packages/create-llama/templates/simple/express/src/routes/llm.route.ts
+++ b/packages/create-llama/templates/simple/express/src/routes/chat.route.ts
@@ -1,5 +1,5 @@
 import express from "express";
-import { chat } from "../controllers/llm.controller";
+import { chat } from "../controllers/chat.controller";
 
 const llmRouter = express.Router();
 
diff --git a/packages/create-llama/templates/simple/express/tsconfig.json b/packages/create-llama/templates/simple/express/tsconfig.json
index fd70902d6..57c4a6281 100644
--- a/packages/create-llama/templates/simple/express/tsconfig.json
+++ b/packages/create-llama/templates/simple/express/tsconfig.json
@@ -1,8 +1,6 @@
 {
   "compilerOptions": {
     "target": "es2016",
-    "module": "commonjs",
-    "outDir": "./dist",
     "esModuleInterop": true,
     "forceConsistentCasingInFileNames": true,
     "strict": true,
diff --git a/packages/create-llama/templates/streaming/express/README-template.md b/packages/create-llama/templates/streaming/express/README-template.md
new file mode 100644
index 000000000..2da2865d5
--- /dev/null
+++ b/packages/create-llama/templates/streaming/express/README-template.md
@@ -0,0 +1,34 @@
+This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Express](https://expressjs.com/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
+
+## Getting Started
+
+First, install the dependencies:
+
+```
+npm install
+```
+
+Second, run the development server:
+
+```
+npm run dev
+```
+
+Then call the express API endpoint `/api/chat` to see the result:
+
+```
+curl --location 'localhost:3000/api/chat' \
+--header 'Content-Type: application/json' \
+--data '{ "messages": [{ "role": "user", "content": "Hello" }] }'
+```
+
+You can start editing the API by modifying `src/controllers/chat.controller.ts`. The endpoint auto-updates as you save the file.
+
+## Learn More
+
+To learn more about LlamaIndex, take a look at the following resources:
+
+- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
+- [LlamaIndexTS Documentation](https://ts.llamaindex.ai) - learn about LlamaIndex (Typescript features).
+
+You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
diff --git a/packages/create-llama/templates/streaming/express/eslintrc.json b/packages/create-llama/templates/streaming/express/eslintrc.json
new file mode 100644
index 000000000..c19581799
--- /dev/null
+++ b/packages/create-llama/templates/streaming/express/eslintrc.json
@@ -0,0 +1,3 @@
+{
+  "extends": "eslint:recommended"
+}
\ No newline at end of file
diff --git a/packages/create-llama/templates/streaming/express/index.ts b/packages/create-llama/templates/streaming/express/index.ts
new file mode 100644
index 000000000..b73784295
--- /dev/null
+++ b/packages/create-llama/templates/streaming/express/index.ts
@@ -0,0 +1,17 @@
+import express, { Express, Request, Response } from "express";
+import chatRouter from "./src/routes/chat.route";
+
+const app: Express = express();
+const port = 3000;
+
+app.use(express.json());
+
+app.get("/", (req: Request, res: Response) => {
+  res.send("LlamaIndex Express Server");
+});
+
+app.use("/api/chat", chatRouter);
+
+app.listen(port, () => {
+  console.log(`⚡️[server]: Server is running at http://localhost:${port}`);
+});
diff --git a/packages/create-llama/templates/streaming/express/package.json b/packages/create-llama/templates/streaming/express/package.json
new file mode 100644
index 000000000..ac90ee8a6
--- /dev/null
+++ b/packages/create-llama/templates/streaming/express/package.json
@@ -0,0 +1,25 @@
+{
+  "name": "llama-index-express-streaming",
+  "version": "1.0.0",
+  "main": "dist/index.js",
+  "type": "module",
+  "scripts": {
+    "build": "tsup index.ts --format esm --dts",
+    "start": "node dist/index.js",
+    "dev": "concurrently \"tsup index.ts --format esm --dts --watch\" \"nodemon -q dist/index.js\""
+  },
+  "dependencies": {
+    "ai": "^2",
+    "express": "^4",
+    "llamaindex": "0.0.31"
+  },
+  "devDependencies": {
+    "@types/express": "^4",
+    "@types/node": "^20",
+    "concurrently": "^8",
+    "eslint": "^8",
+    "nodemon": "^3",
+    "tsup": "^7",
+    "typescript": "^5"
+  }
+}
\ No newline at end of file
diff --git a/packages/create-llama/templates/streaming/express/src/controllers/chat.controller.ts b/packages/create-llama/templates/streaming/express/src/controllers/chat.controller.ts
new file mode 100644
index 000000000..58f96b035
--- /dev/null
+++ b/packages/create-llama/templates/streaming/express/src/controllers/chat.controller.ts
@@ -0,0 +1,36 @@
+import { streamToResponse } from "ai";
+import { NextFunction, Request, Response } from "express";
+import { ChatMessage, OpenAI } from "llamaindex";
+import { createChatEngine } from "../../../../engines/context";
+import { LlamaIndexStream } from "./llamaindex-stream";
+
+export const chat = async (req: Request, res: Response, next: NextFunction) => {
+  try {
+    const { messages }: { messages: ChatMessage[] } = req.body;
+    const lastMessage = messages.pop();
+    if (!messages || !lastMessage || lastMessage.role !== "user") {
+      return res.status(400).json({
+        error:
+          "messages are required in the request body and the last message must be from the user",
+      });
+    }
+
+    const llm = new OpenAI({
+      model: "gpt-3.5-turbo",
+    });
+
+    const chatEngine = await createChatEngine(llm);
+
+    const response = await chatEngine.chat(lastMessage.content, messages, true);
+
+    // Transform the response into a readable stream
+    const stream = LlamaIndexStream(response);
+
+    streamToResponse(stream, res);
+  } catch (error) {
+    console.error("[LlamaIndex]", error);
+    return res.status(500).json({
+      error: (error as Error).message,
+    });
+  }
+};
diff --git a/packages/create-llama/templates/streaming/express/src/controllers/llamaindex-stream.ts b/packages/create-llama/templates/streaming/express/src/controllers/llamaindex-stream.ts
new file mode 100644
index 000000000..12328de87
--- /dev/null
+++ b/packages/create-llama/templates/streaming/express/src/controllers/llamaindex-stream.ts
@@ -0,0 +1,35 @@
+import {
+  createCallbacksTransformer,
+  createStreamDataTransformer,
+  trimStartOfStreamHelper,
+  type AIStreamCallbacksAndOptions,
+} from "ai";
+
+function createParser(res: AsyncGenerator<any>) {
+  const trimStartOfStream = trimStartOfStreamHelper();
+  return new ReadableStream<string>({
+    async pull(controller): Promise<void> {
+      const { value, done } = await res.next();
+      if (done) {
+        controller.close();
+        return;
+      }
+
+      const text = trimStartOfStream(value ?? "");
+      if (text) {
+        controller.enqueue(text);
+      }
+    },
+  });
+}
+
+export function LlamaIndexStream(
+  res: AsyncGenerator<any>,
+  callbacks?: AIStreamCallbacksAndOptions,
+): ReadableStream {
+  return createParser(res)
+    .pipeThrough(createCallbacksTransformer(callbacks))
+    .pipeThrough(
+      createStreamDataTransformer(callbacks?.experimental_streamData),
+    );
+}
diff --git a/packages/create-llama/templates/streaming/express/src/routes/chat.route.ts b/packages/create-llama/templates/streaming/express/src/routes/chat.route.ts
new file mode 100644
index 000000000..bdfeb0853
--- /dev/null
+++ b/packages/create-llama/templates/streaming/express/src/routes/chat.route.ts
@@ -0,0 +1,8 @@
+import express from "express";
+import { chat } from "../controllers/chat.controller";
+
+const llmRouter = express.Router();
+
+llmRouter.route("/").post(chat);
+
+export default llmRouter;
diff --git a/packages/create-llama/templates/streaming/express/tsconfig.json b/packages/create-llama/templates/streaming/express/tsconfig.json
new file mode 100644
index 000000000..bf2e3ec6e
--- /dev/null
+++ b/packages/create-llama/templates/streaming/express/tsconfig.json
@@ -0,0 +1,9 @@
+{
+  "compilerOptions": {
+    "target": "es2016",
+    "esModuleInterop": true,
+    "forceConsistentCasingInFileNames": true,
+    "strict": true,
+    "skipLibCheck": true
+  }
+}
\ No newline at end of file
-- 
GitLab