From 5eaab539c980d9622ac631d110919486333625dd Mon Sep 17 00:00:00 2001 From: WQYeo Date: Tue, 18 Jun 2024 19:09:49 +0800 Subject: [PATCH 1/2] add routes to expressJS --- src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 0fd9125..5935c75 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { WebSocketServer, WebSocket } from 'ws'; import tokenRoutes from './api/routes/tokenRoutes' +import conversationRoutes from './api/routes/conversationRoutes' import chatMessageConsumer from './api/consumers/chatMessageConsumer'; const app = express(); @@ -18,7 +19,7 @@ app.use(cors({ app.use(express.json()); app.use('/api-chat/token', tokenRoutes); - +app.use('/api-chat/', conversationRoutes); app.get('/api-chat/ping', (req, res) => { res.send('Pong!'); From d1974dcb2f63b60066f65c73cef763705929c542 Mon Sep 17 00:00:00 2001 From: WQYeo Date: Tue, 25 Jun 2024 03:10:36 +0800 Subject: [PATCH 2/2] bugfix on chatting API --- .env.example | 5 +- docker-compose-prod.yml | 14 ++++ docker-compose.yml | 13 ++++ package-lock.json | 64 +++++++++++++++---- package.json | 2 + src/api/consumers/chatMessageConsumer.ts | 33 +++++----- .../controllers/getConversationMessages.ts | 2 +- .../controllers/getTokenCountController.ts | 7 +- src/api/helpers/trimString.ts | 2 +- .../selectConversationsByUserID.ts | 2 +- src/api/services/fetchUserInformation.ts | 6 +- src/api/services/openAI/chatStream.ts | 2 +- .../openAI/generateConversationTitle.ts | 1 + src/index.ts | 40 ++++-------- 14 files changed, 125 insertions(+), 68 deletions(-) diff --git a/.env.example b/.env.example index 30e9d2b..50c6794 100644 --- a/.env.example +++ b/.env.example @@ -25,6 +25,9 @@ DEFAULT_STARTING_TOKEN=25000 # Database # ############ +# For database explorer to connect to. +POSTGRESQL_PORT=44902 + # This is for postgreSQL connection credentials POSTGRES_USERNAME=test POSTGRES_PASSWORD=test @@ -41,5 +44,5 @@ QDRANT_API_KEY=asdasdasd # For communication with User service; Should be same as whats set in user service's USER_SERVICE_API_TOKEN=abcdefg -USER_SERVICE_API_DOMAIN=localhost:44801 +USER_SERVICE_API_DOMAIN=user-api:8000 USER_SERVICE_SSL_ENABLED=false \ No newline at end of file diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index 06f6c1e..f4e6bd6 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -30,6 +30,8 @@ services: - chat-db - chat-vector-db - chat-cache + networks: + - muimi_container_network # database chat-db: @@ -39,8 +41,12 @@ services: POSTGRES_USER: ${POSTGRES_USERNAME} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_MAIN_DB} + ports: + - "${POSTGRESQL_PORT}:5432" volumes: - ./pgdata:/var/lib/postgresql/data:z + networks: + - muimi_container_network chat-vector-db: image: qdrant/qdrant:v1.9.4 @@ -48,6 +54,8 @@ services: QDRANT__SERVICE__API_KEY: ${QDRANT_API_KEY} volumes: - "./qdrant_storage:/qdrant/storage:z" + networks: + - muimi_container_network chat-cache: image: redis:7.2.4 @@ -55,3 +63,9 @@ services: # Runs on 6379 Docker volumes: - ./redisdata:/data:z + networks: + - muimi_container_network + +networks: + muimi_container_network: + external: true \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index ed9c000..0b6c660 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,8 @@ services: - chat-db - chat-vector-db - chat-cache + networks: + - muimi_container_network chat-vector-db: image: qdrant/qdrant:v1.9.4 @@ -35,6 +37,8 @@ services: QDRANT__SERVICE__API_KEY: ${QDRANT_API_KEY} volumes: - "./qdrant_storage:/qdrant/storage:z" + networks: + - muimi_container_network # database chat-db: @@ -44,8 +48,12 @@ services: POSTGRES_USER: ${POSTGRES_USERNAME} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_MAIN_DB} + ports: + - "${POSTGRESQL_PORT}:5432" volumes: - ./pgdata:/var/lib/postgresql/data:z + networks: + - muimi_container_network chat-cache: image: redis:7.2.4 @@ -53,4 +61,9 @@ services: # Runs on 6379 Docker volumes: - ./redisdata:/data:z + networks: + - muimi_container_network +networks: + muimi_container_network: + external: true diff --git a/package-lock.json b/package-lock.json index 58311d0..00d1602 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@qdrant/js-client-rest": "^1.9.0", "@types/crypto-js": "^4.2.2", + "@types/express-ws": "^3.0.4", "@types/ws": "^8.5.10", "argon2": "^0.40.3", "axios": "^1.7.2", @@ -18,6 +19,7 @@ "crypto-js": "^4.2.0", "drizzle-orm": "^0.30.10", "express": "^4.19.2", + "express-ws": "^5.0.2", "openai": "^4.47.3", "pg": "^8.11.5", "postgres": "^3.4.4", @@ -980,7 +982,6 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -990,7 +991,6 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -1013,7 +1013,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -1025,7 +1024,6 @@ "version": "4.19.3", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.3.tgz", "integrity": "sha512-KOzM7MhcBFlmnlr/fzISFF5vGWVSvN6fTd4T+ExOt08bA/dA5kpSzY52nMsI1KDFmUREpJelPYyuslLRSjjgCg==", - "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -1033,17 +1031,25 @@ "@types/send": "*" } }, + "node_modules/@types/express-ws": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/express-ws/-/express-ws-3.0.4.tgz", + "integrity": "sha512-Yjj18CaivG5KndgcvzttWe8mPFinPCHJC2wvyQqVzA7hqeufM8EtWMj6mpp5omg3s8XALUexhOu8aXAyi/DyJQ==", + "dependencies": { + "@types/express": "*", + "@types/express-serve-static-core": "*", + "@types/ws": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/node": { "version": "20.13.0", @@ -1076,20 +1082,17 @@ "node_modules/@types/qs": { "version": "6.9.15", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", - "dev": true + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -1099,7 +1102,6 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, "dependencies": { "@types/http-errors": "*", "@types/node": "*", @@ -1945,6 +1947,40 @@ "node": ">= 0.10.0" } }, + "node_modules/express-ws": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/express-ws/-/express-ws-5.0.2.tgz", + "integrity": "sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==", + "dependencies": { + "ws": "^7.4.6" + }, + "engines": { + "node": ">=4.5.0" + }, + "peerDependencies": { + "express": "^4.0.0 || ^5.0.0-alpha.1" + } + }, + "node_modules/express-ws/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", diff --git a/package.json b/package.json index c9f9d56..71d85d8 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dependencies": { "@qdrant/js-client-rest": "^1.9.0", "@types/crypto-js": "^4.2.2", + "@types/express-ws": "^3.0.4", "@types/ws": "^8.5.10", "argon2": "^0.40.3", "axios": "^1.7.2", @@ -25,6 +26,7 @@ "crypto-js": "^4.2.0", "drizzle-orm": "^0.30.10", "express": "^4.19.2", + "express-ws": "^5.0.2", "openai": "^4.47.3", "pg": "^8.11.5", "postgres": "^3.4.4", diff --git a/src/api/consumers/chatMessageConsumer.ts b/src/api/consumers/chatMessageConsumer.ts index 6b0fa43..559efba 100644 --- a/src/api/consumers/chatMessageConsumer.ts +++ b/src/api/consumers/chatMessageConsumer.ts @@ -1,4 +1,4 @@ -import { WebSocket } from 'ws'; +import { RawData, WebSocket } from 'ws'; import insertLog from '../repositories/insertLog'; /** @@ -36,9 +36,9 @@ function getSenderUserUUID(data: any): Promise { } // TODO: Support custom system messages... -export default async function chatMessageConsumer(socketClient: WebSocket, content: string) { +export default async function chatMessageConsumer(socketClient: WebSocket, content: RawData) { try { - const data = JSON.parse(content) + const data = JSON.parse(content.toString()) const userUUID = await getSenderUserUUID(data) if (userUUID == null) { @@ -146,26 +146,27 @@ export default async function chatMessageConsumer(socketClient: WebSocket, conte // Start streaming in the message chunk by chunk const botReplyStream = await chatStream(openAiClient, messageChain, chatModel, userUUID) for await (const chunk of botReplyStream) { - const chunkContent = chunk.choices[0].delta.content - botReply += chunkContent - socketClient.send(JSON.stringify({ - status: "CHUNK", - message: "This is a chunk of the bot's message...", - chunk_content: chunkContent - })); - - // Last chunk, fetch the token cost. - if (chunk.choices[0].finish_reason == "stop") { + if (chunk.usage != null) { promptTokenCost = chunk.usage!.prompt_tokens completionTokenCost = chunk.usage!.completion_tokens + // Because a chunk with usage has no message... + continue; } - } - // TODO: Sequencial undo in case of error.... + if (chunk.choices[0].finish_reason != "stop") { + const chunkContent = chunk.choices[0].delta.content + botReply += chunkContent + socketClient.send(JSON.stringify({ + status: "CHUNK", + message: "This is a chunk of the bot's message...", + chunk_content: chunkContent + })); + } + } let userMessageObject; let botMessageObject - await db.transaction(async (tsx) => { + await db.transaction(async () => { if (!userAccountModel.freeTokenUsage) { await updateAccountTokenByUUID(userUUID, userAccountModel.token - promptTokenCost - completionTokenCost) } diff --git a/src/api/controllers/getConversationMessages.ts b/src/api/controllers/getConversationMessages.ts index 1403905..85505fe 100644 --- a/src/api/controllers/getConversationMessages.ts +++ b/src/api/controllers/getConversationMessages.ts @@ -51,7 +51,7 @@ export default async function getConversationMessages(req: Request, res: Respons "WARNING" ); return res.status(401).json({ - status: "BAD_TOKEN", + status: "BAD_SESSION", message: "Session token is bad, relogin!", }); } diff --git a/src/api/controllers/getTokenCountController.ts b/src/api/controllers/getTokenCountController.ts index ef98ecb..4d58fd8 100644 --- a/src/api/controllers/getTokenCountController.ts +++ b/src/api/controllers/getTokenCountController.ts @@ -12,6 +12,7 @@ export default async function getTokenCountController(req: Request, res: Respons url: REDIS_CONNECTION_STRING }); + console.log("Connecting to Redis"); client.on('error', async (err: any) => { console.log('Connecting Redis Client Error', err); await insertLog(`Error connecting to Redis :: ${err}`); @@ -32,20 +33,22 @@ export default async function getTokenCountController(req: Request, res: Respons message: 'Missing required parameters' }); } - // See if token exists in cache, if not, re-validate from user service, // then flag token as valid for 1 minute. let userUUID = await client.get(`${username}_${sessionToken}`); if (userUUID == null) { + console.log("User UUID not found in cache, revalidating from API"); let userInformation = await fetchUserInformation(sessionToken, userAgent, username) if (userInformation.status != "SUCCESS") { + console.log(`Failed to fetch user information with status :: ${userInformation.status}`) await insertLog(`Failed to fetch user information with status :: ${userInformation.status}`, "WARNING") return res.status(401).json({ - status: 'BAD_TOKEN', + status: 'BAD_SESSION', message: 'Session token is bad, relogin!' }); } + console.log(`User UUID Revalidated :: ${userInformation.uuid}`); userUUID = userInformation.uuid; await client.set(`${username}_${sessionToken}`, userUUID, { EX: 60, diff --git a/src/api/helpers/trimString.ts b/src/api/helpers/trimString.ts index 0a0b603..c541c27 100644 --- a/src/api/helpers/trimString.ts +++ b/src/api/helpers/trimString.ts @@ -1,3 +1,3 @@ -function trimString(str: string, maxLength: number): string { +export default function trimString(str: string, maxLength: number): string { return str.length > maxLength ? str.substring(0, maxLength) : str; } \ No newline at end of file diff --git a/src/api/repositories/selectConversationsByUserID.ts b/src/api/repositories/selectConversationsByUserID.ts index 92e7162..fa36788 100644 --- a/src/api/repositories/selectConversationsByUserID.ts +++ b/src/api/repositories/selectConversationsByUserID.ts @@ -10,7 +10,7 @@ export default async function selectConversationsByUserID(userID: string, select .where(and( eq(conversation.accountID, userID), eq(conversation.deleted, selectDeleted), - eq(conversation.archived, selectArchived) + selectArchived ? undefined : eq(conversation.archived, false) ) ) .orderBy(desc(conversation.creationDate)); diff --git a/src/api/services/fetchUserInformation.ts b/src/api/services/fetchUserInformation.ts index f377fe5..aceaf86 100644 --- a/src/api/services/fetchUserInformation.ts +++ b/src/api/services/fetchUserInformation.ts @@ -6,14 +6,16 @@ import { USER_SERVICE_API_CONFIG } from 'src/configs/userServiceApiConfig'; export default async function fetchUserInformation(sessionToken: string, userAgent: string, username: string) : Promise { try { const url = `${USER_SERVICE_API_CONFIG.SSL_ENABLED ? 'https://' : 'http://'}${USER_SERVICE_API_CONFIG.BASE_DOMAIN}/api-user/service-user-info?username=${username}`; - + console.log(url) const response = await axios.get(url, { headers: { 'session-token': sessionToken, 'service-token': USER_SERVICE_API_CONFIG.API_TOKEN, 'user-agent': userAgent }, - proxy: false + validateStatus: (status) => { + return status >= 200 && status < 500 + } }); const data = response.data; diff --git a/src/api/services/openAI/chatStream.ts b/src/api/services/openAI/chatStream.ts index 31f2ca0..d14b0e5 100644 --- a/src/api/services/openAI/chatStream.ts +++ b/src/api/services/openAI/chatStream.ts @@ -20,6 +20,6 @@ export default async function chatStream(openAiClient: OpenAI, messageChain: Cha messages: messageChain, stream: true, user: userID, - stream_options: {"include_usage": true} + stream_options: {include_usage: true} }); } \ No newline at end of file diff --git a/src/api/services/openAI/generateConversationTitle.ts b/src/api/services/openAI/generateConversationTitle.ts index da15e60..90aa018 100644 --- a/src/api/services/openAI/generateConversationTitle.ts +++ b/src/api/services/openAI/generateConversationTitle.ts @@ -1,6 +1,7 @@ import OpenAI from "openai"; import insertLog from "src/api/repositories/insertLog"; import { TITLE_CREATOR_BOT_MODEL } from "src/configs/titleCreatorBotModel"; +import trimString from "src/api/helpers/trimString"; /** * diff --git a/src/index.ts b/src/index.ts index 5935c75..8e85b14 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,16 @@ -import express, { Request, Response } from 'express'; +import express from 'express'; import cors from 'cors'; -import { createServer } from 'http'; -import { WebSocketServer, WebSocket } from 'ws'; - import tokenRoutes from './api/routes/tokenRoutes' import conversationRoutes from './api/routes/conversationRoutes' import chatMessageConsumer from './api/consumers/chatMessageConsumer'; const app = express(); + +import expressWs from 'express-ws'; +expressWs(app); + const port = 3000; app.use(cors({ @@ -25,34 +26,15 @@ app.get('/api-chat/ping', (req, res) => { res.send('Pong!'); }); -const server = createServer(app); -const wss = new WebSocketServer({ noServer: true }); +const wsRouter = express.Router(); -// Handle WebSocket connections -wss.on('connection', (ws: WebSocket) => { - console.log('Client connected to /api-chat/chat'); - - ws.on('message', (message: string) => { - chatMessageConsumer(ws, message) - }); - - ws.on('close', () => { - console.log('Client disconnected'); - }); +wsRouter.ws("/", (ws, req) => { + ws.on("message", (msg) => { + chatMessageConsumer(ws, msg) + }); }); -// Middleware to handle upgrade requests to WebSocket on a specific path -server.on('upgrade', (request, socket, head) => { - const pathname = new URL(request.url!, `http://${request.headers.host}`).pathname; - - if (pathname === '/api-chat/chat') { - wss.handleUpgrade(request, socket, head, (ws: any) => { - wss.emit('connection', ws, request); - }); - } else { - socket.destroy(); - } -}); +app.use('/api-chat/chat', wsRouter); app.listen(port, () => { console.log(`Server is running on port ${port}`);