diff --git a/package-lock.json b/package-lock.json index a5b4a8e..2dfc0e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@isaacs/ttlcache": "^1.4.1", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.18.3", @@ -58,6 +59,14 @@ "node": ">=12" } }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", + "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", + "engines": { + "node": ">=12" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", diff --git a/package.json b/package.json index cf290e4..7f031c7 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ }, "homepage": "https://github.com/senacor/chatbot-poc-backend#readme", "dependencies": { + "@isaacs/ttlcache": "^1.4.1", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.18.3", diff --git a/src/chat/get-init.ts b/src/chat/get-init.ts index 268192b..660dfee 100644 --- a/src/chat/get-init.ts +++ b/src/chat/get-init.ts @@ -1,12 +1,15 @@ import { z } from "zod"; import { makeGetEndpoint } from "../middleware/validation/makeGetEndpoint.js"; +import { addMessages, getUserVisibleMessages } from "../util/messageStore.js"; +import { IDENTITY_HEADER } from "./index.js"; //TODO: Rework type inference of fileReader export const init = makeGetEndpoint(z.any(), async (_request, response) => { - - return response - .status(200) - .send([ + const identity = _request.header(IDENTITY_HEADER); + if (!identity) { + return response.status(400).send(`Missing ${IDENTITY_HEADER} header.`) + } + addMessages(identity, [ { role: 'system', content: "Ask the user to provide the content of the file they want to chat about. Talk only about the content of the provided file." @@ -16,4 +19,7 @@ export const init = makeGetEndpoint(z.any(), async (_request, response) => { content: "Hallo! Bitte laden Sie die Datei hoch, über die Sie chatten möchten." } ]); + return response + .status(200) + .send(getUserVisibleMessages(identity)); }); \ No newline at end of file diff --git a/src/chat/index.ts b/src/chat/index.ts index 3ddef1a..e40175e 100644 --- a/src/chat/index.ts +++ b/src/chat/index.ts @@ -12,6 +12,7 @@ import fileUpload from "./upload-file.js"; const PROMPT_FILE_NAME = "Prompt_Baufinanzierung.docx" const OPENAI_MODEL: ChatCompletionCreateParamsBase["model"] = "gpt-3.5-turbo"; const RESPONSE_FORMAT: ChatCompletionCreateParams.ResponseFormat = {type: "text"}; +const IDENTITY_HEADER = 'X-Identity'; const chatRouter = express.Router(); @@ -29,6 +30,7 @@ export { PROMPT_FILE_NAME, OPENAI_MODEL, RESPONSE_FORMAT, + IDENTITY_HEADER, chatRouter, openai, } diff --git a/src/chat/post-new-message.ts b/src/chat/post-new-message.ts index 027e314..0d3a061 100644 --- a/src/chat/post-new-message.ts +++ b/src/chat/post-new-message.ts @@ -1,19 +1,17 @@ import { z } from "zod"; import { makePostEndpoint } from "../middleware/validation/makePostEndpoint.js"; -import { OPENAI_MODEL, RESPONSE_FORMAT, openai } from "./index.js"; -import { ChatCompletionMessageParam } from "openai/resources/index.js"; +import { IDENTITY_HEADER, OPENAI_MODEL, RESPONSE_FORMAT, openai } from "./index.js"; +import {ChatCompletionMessageParam } from "openai/resources/index.js"; +import { addMessage, getMessages, getUserVisibleMessages } from "../util/messageStore.js"; const ChatCompletionRole = z.union([z.literal('user'), z.literal('system'), z.literal('assistant')]); export type ChatCompletionRole = z.infer; -const MessageHistory = z.object({ - messages: z.array( +const MessageHistory = z.object({ role: ChatCompletionRole, content: z.string(), - }) - ).nonempty() -}); + }); type MessageHistory = z.infer; const makeApiRequest = async (messages: ChatCompletionMessageParam[]) => { @@ -24,17 +22,26 @@ const makeApiRequest = async (messages: ChatCompletionMessageParam[]) => { }); } -const processMessages = async (messages: ChatCompletionMessageParam[], response: any) => { +const processMessages = async (messages: ChatCompletionMessageParam[], response: any, identity: string) => { const completion = await makeApiRequest(messages); const chatResponse = completion.choices[0]; console.log(chatResponse); if(!chatResponse){ return response.status(500).send("Got no response from the bot"); } - return response.status(200).send(chatResponse.message); + addMessage(identity, chatResponse.message); + return response.status(200).send(getUserVisibleMessages(identity)); + } + export const newMessage = makePostEndpoint(MessageHistory, async (request, response) => { - const messages = request.body.messages; - processMessages(messages, response); + const message = request.body; + const identity = request.header(IDENTITY_HEADER); + if (!identity) { + return response.status(400).send(`Missing ${IDENTITY_HEADER} header.`) + } + addMessage(identity, message); + const messages = getMessages(identity) ?? []; //TODO handle no messages + return processMessages(messages, response, identity); }); diff --git a/src/util/messageStore.ts b/src/util/messageStore.ts new file mode 100644 index 0000000..39fd233 --- /dev/null +++ b/src/util/messageStore.ts @@ -0,0 +1,22 @@ +import { ChatCompletionMessageParam } from "openai/resources"; +import TTLCache from '@isaacs/ttlcache'; + +const sessionStore = new TTLCache({ max: 10000, ttl: 50 * 60 * 1000 }); + +export const addMessage = (key: string, message: ChatCompletionMessageParam) => { + const value = sessionStore.get(key) ?? []; + sessionStore.set(key, [...value, message]); +} + +export const addMessages = (key: string, messages: ChatCompletionMessageParam[]) => { + const value = sessionStore.get(key) ?? []; + sessionStore.set(key, value.concat(messages)); +} + +export const getMessages = (key: string) => { + return sessionStore.get(key); +} + +export const getUserVisibleMessages = (key: string) => { + return getMessages(key)?.filter(message => message.role === 'user' || message.role === 'assistant' && !message.tool_calls); +} \ No newline at end of file