From 0b11696bed8f253223757c213162e7266733452f Mon Sep 17 00:00:00 2001 From: Chris Wilton-Magras Date: Thu, 1 Feb 2024 16:10:01 +0000 Subject: [PATCH 1/5] First set of backend changes from cloud branch --- .gitattributes | 2 +- backend/.env.example | 1 + backend/Dockerfile | 2 +- backend/docker-compose.yml | 4 ++- backend/package-lock.json | 28 ++-------------- backend/package.json | 5 ++- backend/src/app.ts | 65 ++++++++++++++++++++++++++++++-------- backend/src/document.ts | 1 + backend/src/langchain.ts | 5 +-- backend/src/models/chat.ts | 6 ++-- backend/src/openai.ts | 6 ++-- backend/tsconfig.json | 2 +- 12 files changed, 74 insertions(+), 53 deletions(-) diff --git a/.gitattributes b/.gitattributes index a6731222a..111a7a617 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,5 @@ # Always use LF for line endings * text eol=lf -# Apart from font files +# Apart from font files and images *.[ot]tf binary *.png -text diff --git a/backend/.env.example b/backend/.env.example index 7f65c5b57..905ffccfb 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,2 +1,3 @@ OPENAI_API_KEY=YOUR_API_KEY SESSION_SECRET=YOUR_SESSION_SECRET +CORS_ALLOW_ORIGIN=http://localhost:5173 diff --git a/backend/Dockerfile b/backend/Dockerfile index 43405a575..ffe2f31a3 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -5,4 +5,4 @@ RUN npm ci COPY . . EXPOSE 3001 RUN npm run build -CMD ["node", "--import=tsx/esm", "./build/server.js"] +CMD ["node", "--import=tsx", "./src/server.ts"] diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 7a44ed0cb..71a0ac5c7 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -1,8 +1,10 @@ services: prompt-injection-api: environment: - NODE_ENV: development + NODE_ENV: ${NODE_ENV:-development} OPENAI_API_KEY: ${OPENAI_API_KEY} + SESSION_SECRET: ${SESSION_SECRET} + CORS_ALLOW_ORIGIN: "${CORS_ALLOW_ORIGIN:-*}" PORT: 3001 build: . image: 'scottlogic/prompt-injection-api' diff --git a/backend/package-lock.json b/backend/package-lock.json index d0516a778..cdf0ea602 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -17,8 +17,7 @@ "memorystore": "^1.6.7", "openai": "^4.19.0", "openai-chat-tokens": "^0.2.8", - "pdf-parse": "^1.1.1", - "react": "^18.2.0" + "pdf-parse": "^1.1.1" }, "devDependencies": { "@jest/globals": "^29.7.0", @@ -5434,7 +5433,8 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "node_modules/js-yaml": { "version": "4.1.0", @@ -5901,17 +5901,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -6908,17 +6897,6 @@ "node": ">= 0.8" } }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", diff --git a/backend/package.json b/backend/package.json index cd1f231f1..10865e543 100644 --- a/backend/package.json +++ b/backend/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "type": "module", "scripts": { - "build": "tsc", + "build": "tsc --noEmit", "dev": "tsx watch -r dotenv/config src/server.ts", "start": "tsx -r dotenv/config src/server.ts", "docker:start": "docker compose up -d", @@ -23,8 +23,7 @@ "memorystore": "^1.6.7", "openai": "^4.19.0", "openai-chat-tokens": "^0.2.8", - "pdf-parse": "^1.1.1", - "react": "^18.2.0" + "pdf-parse": "^1.1.1" }, "devDependencies": { "@jest/globals": "^29.7.0", diff --git a/backend/src/app.ts b/backend/src/app.ts index 642eeef05..b43c99235 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -20,37 +20,66 @@ declare module 'express-session' { } } +// Check mandatory ENV vars +const sessionSigningSecret = process.env.SESSION_SECRET; +if (!sessionSigningSecret) { + console.error("SESSION_SECRET not found in environment vars, cannot continue!"); + process.exit(1); +} + const app = express(); const isProd = app.get('env') === 'production'; +console.log(`env=${app.get('env')}`); // for parsing application/json app.use(express.json()); +app.use( + cors({ + origin: process.env.CORS_ALLOW_ORIGIN, + credentials: true, + }) +); + +// This doesn't work with APIGW's newer HTTP API, cos it's using Forwarded +// request header, not X-Forwarded headers. +// It also doesn't work with NLB in between APIGW and ALB, cos X-Forwarded-Proto +// has only one entry: "http" +//app.set('trust proxy', 3); + // use session storage - currently in-memory, but in future use Redis in prod builds -const maxAge = 60 * 60 * 1000 * (isProd ? 1 : 8); //1 hour in prod, 8hrs in dev +const maxAge = 60 * 60 * 1000 * (isProd ? 4 : 8); //4hrs in prod, 8hrs in dev const sessionOpts: session.SessionOptions = { - store: new (memoryStoreFactory(session))({ - checkPeriod: maxAge, - }), - secret: process.env.SESSION_SECRET ?? 'secret', name: 'prompt-injection.sid', resave: false, saveUninitialized: true, + secret: sessionSigningSecret, + store: new (memoryStoreFactory(session))({ + checkPeriod: maxAge, + }), cookie: { - secure: isProd, maxAge, + /* + UI and API have different domains, until we buy a domain and put Route53 + in front of both. + Currently this means we need to use a non-secure session cookie for it to + get all the way through to/from the server, but that will be forbidden in + most browsers in 2024 - see + https://developer.mozilla.org/en-US/blog/goodbye-third-party-cookies/ + So, this workaround will soon break! The problem is that API Gateway uses + the new, standard Forwarded header to convey proxy details, whereas ALB + still uses non-standard but ubiquitous X-Forwarded- headers, so our node + server receives both sets of headers and ignores the Forwarded header + (which is our secure, trusted API Gateway proxy). Problem reported here: + https://repost.aws/questions/QUtBHMaz7IQ6aM4RCBMnJvgw/why-does-apigw-http-api-use-forwarded-header-while-other-services-still-use-x-forwarded-headers + */ + sameSite: isProd ? 'none' : 'strict', + secure: false, //isProd }, }; app.use(session(sessionOpts)); -app.use( - cors({ - credentials: true, - origin: true, - }) -); - app.use((req, _res, next) => { // initialise session variables first time if (!req.session.initialised) { @@ -61,6 +90,16 @@ app.use((req, _res, next) => { next(); }); +app.use((req, res, next) => { + if (req.path !== '/health' && isProd) { + console.log('Request:', req.path, `secure=${req.secure}`, req.headers); + res.on('finish', () => { + console.log('Response:', req.path, res.getHeaders()); + }); + } + next(); +}); + app.use('/', router); // serve the documents folder diff --git a/backend/src/document.ts b/backend/src/document.ts index f8b32d0dc..b910237aa 100644 --- a/backend/src/document.ts +++ b/backend/src/document.ts @@ -28,6 +28,7 @@ async function getDocuments(filePath: string) { '.csv': (path: string) => new CSVLoader(path), }); const docs = await loader.load(); + console.debug(`${docs.length} documents found`); // split the documents into chunks const textSplitter = new RecursiveCharacterTextSplitter({ diff --git a/backend/src/langchain.ts b/backend/src/langchain.ts index 024f0aa7a..89d46e624 100644 --- a/backend/src/langchain.ts +++ b/backend/src/langchain.ts @@ -20,11 +20,12 @@ import { // store vectorised documents for each level as array const vectorisedDocuments = (() => { - let docs: DocumentsVector[] = []; + const docs: DocumentsVector[] = []; return { get: () => docs, set: (newDocs: DocumentsVector[]) => { - docs = newDocs; + while (docs.length > 0) docs.pop(); + docs.push(...newDocs); }, }; })(); diff --git a/backend/src/models/chat.ts b/backend/src/models/chat.ts index 5a29f05c0..3e71fa19e 100644 --- a/backend/src/models/chat.ts +++ b/backend/src/models/chat.ts @@ -149,10 +149,13 @@ export type { ChatDefenceReport, ChatGptReply, ChatMalicious, + ChatModel, + ChatModelConfiguration, ChatResponse, LevelHandlerResponse, ChatHttpResponse, ChatHistoryMessage, + SingleDefenceReport, TransformedChatMessage, FunctionCallResponse, ToolCallResponse, @@ -162,8 +165,5 @@ export { CHAT_MODELS, CHAT_MESSAGE_TYPE, MODEL_CONFIG, - ChatModel, - ChatModelConfiguration, defaultChatModel, - SingleDefenceReport, }; diff --git a/backend/src/openai.ts b/backend/src/openai.ts index 680e231f6..98c199bb7 100644 --- a/backend/src/openai.ts +++ b/backend/src/openai.ts @@ -101,7 +101,7 @@ const getOpenAIKey = (() => { openAIKey = process.env.OPENAI_API_KEY; if (!openAIKey) { throw new Error( - 'OpenAI API key not found in environment vars - cannot continue!' + 'OPENAI_API_KEY not found in environment vars, cannot continue!' ); } } @@ -302,8 +302,8 @@ function getChatCompletionsFromHistory( const completions: ChatCompletionMessageParam[] = chatHistory.length > 0 ? (chatHistory - .filter((message) => message.completion !== null) - .map((message) => message.completion) as ChatCompletionMessageParam[]) + .filter((message) => message.completion !== null) + .map((message) => message.completion) as ChatCompletionMessageParam[]) : []; console.debug( diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 893a78e82..9e7f3301a 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -8,12 +8,12 @@ "strict": true, "noImplicitAny": true, "esModuleInterop": true, + "isolatedModules": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, "noUnusedLocals": true, "noUnusedParameters": true, "sourceMap": true, - "paths": { "@src/*": ["./src/*"] } From 3d914688e8a655bb10cff4a5906b5ccf2fa31d50 Mon Sep 17 00:00:00 2001 From: Chris Wilton-Magras Date: Thu, 1 Feb 2024 09:53:02 +0000 Subject: [PATCH 2/5] Split API routes into session vs non-session --- backend/src/app.ts | 119 +++-------------------------- backend/src/nonSessionRoutes.ts | 22 ++++++ backend/src/router.ts | 68 ----------------- backend/src/sessionRoutes.ts | 131 ++++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 176 deletions(-) create mode 100644 backend/src/nonSessionRoutes.ts delete mode 100644 backend/src/router.ts create mode 100644 backend/src/sessionRoutes.ts diff --git a/backend/src/app.ts b/backend/src/app.ts index b43c99235..5a965a1ec 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,113 +1,16 @@ import cors from 'cors'; -import dotenv from 'dotenv'; import express from 'express'; -import session from 'express-session'; -import memoryStoreFactory from 'memorystore'; -import { fileURLToPath } from 'node:url'; -import { importMetaUrl } from './importMetaUtils'; -import { ChatModel, defaultChatModel } from './models/chat'; -import { LevelState, getInitialLevelStates } from './models/level'; -import { router } from './router'; +import nonSessionRoutes from './nonSessionRoutes'; +import sessionRoutes from './sessionRoutes'; -dotenv.config(); - -declare module 'express-session' { - interface Session { - initialised: boolean; - chatModel: ChatModel; - levelState: LevelState[]; - } -} - -// Check mandatory ENV vars -const sessionSigningSecret = process.env.SESSION_SECRET; -if (!sessionSigningSecret) { - console.error("SESSION_SECRET not found in environment vars, cannot continue!"); - process.exit(1); -} - -const app = express(); -const isProd = app.get('env') === 'production'; -console.log(`env=${app.get('env')}`); - -// for parsing application/json -app.use(express.json()); - -app.use( - cors({ - origin: process.env.CORS_ALLOW_ORIGIN, - credentials: true, - }) -); - -// This doesn't work with APIGW's newer HTTP API, cos it's using Forwarded -// request header, not X-Forwarded headers. -// It also doesn't work with NLB in between APIGW and ALB, cos X-Forwarded-Proto -// has only one entry: "http" -//app.set('trust proxy', 3); - -// use session storage - currently in-memory, but in future use Redis in prod builds -const maxAge = 60 * 60 * 1000 * (isProd ? 4 : 8); //4hrs in prod, 8hrs in dev -const sessionOpts: session.SessionOptions = { - name: 'prompt-injection.sid', - resave: false, - saveUninitialized: true, - secret: sessionSigningSecret, - store: new (memoryStoreFactory(session))({ - checkPeriod: maxAge, - }), - cookie: { - maxAge, - /* - UI and API have different domains, until we buy a domain and put Route53 - in front of both. - Currently this means we need to use a non-secure session cookie for it to - get all the way through to/from the server, but that will be forbidden in - most browsers in 2024 - see - https://developer.mozilla.org/en-US/blog/goodbye-third-party-cookies/ - So, this workaround will soon break! The problem is that API Gateway uses - the new, standard Forwarded header to convey proxy details, whereas ALB - still uses non-standard but ubiquitous X-Forwarded- headers, so our node - server receives both sets of headers and ignores the Forwarded header - (which is our secure, trusted API Gateway proxy). Problem reported here: - https://repost.aws/questions/QUtBHMaz7IQ6aM4RCBMnJvgw/why-does-apigw-http-api-use-forwarded-header-while-other-services-still-use-x-forwarded-headers - */ - sameSite: isProd ? 'none' : 'strict', - secure: false, //isProd - }, -}; - -app.use(session(sessionOpts)); - -app.use((req, _res, next) => { - // initialise session variables first time - if (!req.session.initialised) { - req.session.chatModel = defaultChatModel; - req.session.levelState = getInitialLevelStates(); - req.session.initialised = true; - } - next(); -}); - -app.use((req, res, next) => { - if (req.path !== '/health' && isProd) { - console.log('Request:', req.path, `secure=${req.secure}`, req.headers); - res.on('finish', () => { - console.log('Response:', req.path, res.getHeaders()); - }); - } - next(); -}); - -app.use('/', router); - -// serve the documents folder -app.use( - '/documents', - express.static( - fileURLToPath(new URL('../resources/documents', importMetaUrl())) +export default express() + .use(express.json()) + .use( + cors({ + origin: process.env.CORS_ALLOW_ORIGIN, + credentials: true, + }) ) -); - -export default app; + .use('/', nonSessionRoutes) + .use('/', sessionRoutes); diff --git a/backend/src/nonSessionRoutes.ts b/backend/src/nonSessionRoutes.ts new file mode 100644 index 000000000..f168c6297 --- /dev/null +++ b/backend/src/nonSessionRoutes.ts @@ -0,0 +1,22 @@ +import express from 'express'; +import { fileURLToPath } from 'node:url'; + +import { handleGetDocuments } from './controller/documentController'; +import { handleHealthCheck } from './controller/healthController'; +import { handleGetSystemRoles } from './controller/systemRoleController'; +import { importMetaUrl } from './importMetaUtils'; + +const router = express.Router(); + +router.use( + '/documents', + express.static( + fileURLToPath(new URL('../resources/documents', importMetaUrl())) + ) +); +router.get('/documents', handleGetDocuments); +router.get('/health', handleHealthCheck); +router.get('/systemRoles', handleGetSystemRoles); + +export default router; + diff --git a/backend/src/router.ts b/backend/src/router.ts deleted file mode 100644 index 695429dd3..000000000 --- a/backend/src/router.ts +++ /dev/null @@ -1,68 +0,0 @@ -import express from 'express'; - -import { - handleChatToGPT, - handleGetChatHistory, - handleAddToChatHistory, - handleClearChatHistory, -} from './controller/chatController'; -import { - handleConfigureDefence, - handleDefenceActivation, - handleDefenceDeactivation, - handleGetDefenceStatus, - handleResetSingleDefence, -} from './controller/defenceController'; -import { handleGetDocuments } from './controller/documentController'; -import { - handleClearEmails, - handleGetEmails, -} from './controller/emailController'; -import { handleHealthCheck } from './controller/healthController'; -import { - handleConfigureModel, - handleGetModel, - handleGetValidModels, - handleSetModel, -} from './controller/modelController'; -import { handleResetProgress } from './controller/resetController'; -import { handleGetSystemRoles } from './controller/systemRoleController'; - -const router = express.Router(); - -// health -router.get('/health', handleHealthCheck); - -// defences -router.post('/defence/activate', handleDefenceActivation); -router.post('/defence/deactivate', handleDefenceDeactivation); -router.post('/defence/configure', handleConfigureDefence); -router.post('/defence/resetConfig', handleResetSingleDefence); -router.get('/defence/status', handleGetDefenceStatus); - -// emails -router.get('/email/get', handleGetEmails); -router.post('/email/clear', handleClearEmails); - -// chat -router.post('/openai/chat', handleChatToGPT); -router.get('/openai/history', handleGetChatHistory); -router.post('/openai/addHistory', handleAddToChatHistory); -router.post('/openai/clear', handleClearChatHistory); - -// model configurations -router.post('/openai/model', handleSetModel); -router.post('/openai/model/configure', handleConfigureModel); -router.get('/openai/model', handleGetModel); -router.get('/openai/validModels', handleGetValidModels); - -// system roles -router.get('/systemRoles', handleGetSystemRoles); - -// getting documents -router.get('/documents', handleGetDocuments); - -// reset progress for all levels -router.post('/reset', handleResetProgress); - -export { router }; diff --git a/backend/src/sessionRoutes.ts b/backend/src/sessionRoutes.ts new file mode 100644 index 000000000..e2185d9a4 --- /dev/null +++ b/backend/src/sessionRoutes.ts @@ -0,0 +1,131 @@ +import express from 'express'; +import session from 'express-session'; +import memoryStoreFactory from 'memorystore'; +import 'dotenv/config'; + +import { + handleChatToGPT, + handleGetChatHistory, + handleAddToChatHistory, + handleClearChatHistory, +} from './controller/chatController'; +import { + handleConfigureDefence, + handleDefenceActivation, + handleDefenceDeactivation, + handleGetDefenceStatus, + handleResetSingleDefence, +} from './controller/defenceController'; +import { + handleClearEmails, + handleGetEmails, +} from './controller/emailController'; +import { + handleConfigureModel, + handleGetModel, + handleGetValidModels, + handleSetModel, +} from './controller/modelController'; +import { handleResetProgress } from './controller/resetController'; +import { ChatModel, defaultChatModel } from './models/chat'; +import { LevelState, getInitialLevelStates } from './models/level'; + +declare module 'express-session' { + interface Session { + initialised: boolean; + chatModel: ChatModel; + levelState: LevelState[]; + } +} + +const sessionSigningSecret = process.env.SESSION_SECRET; +if (!sessionSigningSecret) { + console.error("SESSION_SECRET not found in environment vars, cannot continue!"); + process.exit(1); +} + +const router = express.Router(); + +const stage = process.env.NODE_ENV; +console.log(`env=${stage}`); +const isProd = stage === 'production'; +const cookieStaleHours = isProd ? 2 : 8; +const oneHourInMillis = 60 * 60 * 1000; +const maxAge = oneHourInMillis * cookieStaleHours; + +router.use( + session({ + name: 'prompt-injection.sid', + resave: false, + saveUninitialized: true, + secret: sessionSigningSecret, + // Session storage: currently in-memory but could use Redis in AWS + store: new (memoryStoreFactory(session))({ + checkPeriod: oneHourInMillis, + }), + proxy: isProd, + cookie: { + maxAge, + /* + https://developer.mozilla.org/en-US/blog/goodbye-third-party-cookies/ + Now that browsers have begun clamping down on non-secure Cookies, we + need to set secure=true in prod, until we can put Route53 in front of both + UI and API and get rid of APIGateway entirely. The showstopper is that + APIGateway is not adding Forwarded headers correctly, so the (secure) + session Cookie is no longer working in Prod. + See + https://repost.aws/questions/QUtBHMaz7IQ6aM4RCBMnJvgw/why-does-apigw-http-api-use-forwarded-header-while-other-services-still-use-x-forwarded-headers + */ + sameSite: isProd ? 'none' : 'strict', + secure: isProd, + }, + }) +); + +router.use((req, _res, next) => { + if (!req.session.initialised) { + req.session.chatModel = defaultChatModel; + req.session.levelState = getInitialLevelStates(); + req.session.initialised = true; + } + next(); +}); + +// defences +router.get('/defence/status', handleGetDefenceStatus); +router.post('/defence/activate', handleDefenceActivation); +router.post('/defence/deactivate', handleDefenceDeactivation); +router.post('/defence/configure', handleConfigureDefence); +router.post('/defence/resetConfig', handleResetSingleDefence); + +// emails +router.get('/email/get', handleGetEmails); +router.post('/email/clear', handleClearEmails); + +// chat +router.get('/openai/history', handleGetChatHistory); +router.post('/openai/chat', handleChatToGPT); +router.post('/openai/addHistory', handleAddToChatHistory); +router.post('/openai/clear', handleClearChatHistory); + +// model configurations +router.get('/openai/validModels', handleGetValidModels); +router.get('/openai/model', handleGetModel); +router.post('/openai/model', handleSetModel); +router.post('/openai/model/configure', handleConfigureModel); + +// reset progress for all levels +router.post('/reset', handleResetProgress); + +// Debugging: log headers in prod for primary routes +if (isProd) { + router.use('/openai', (req, res, next) => { + console.log('Request:', req.path, `secure=${req.secure}`, req.headers); + res.on('finish', () => { + console.log('Response:', req.path, res.getHeaders()); + }); + next(); + }); +} + +export default router; From 78c1288aab589c90ee26dd195ec8ed23d5639b61 Mon Sep 17 00:00:00 2001 From: Chris Wilton-Magras Date: Thu, 1 Feb 2024 16:46:58 +0000 Subject: [PATCH 3/5] fixes for linter and formatting --- backend/docker-compose.yml | 2 +- backend/src/models/chat.ts | 7 +------ backend/src/nonSessionRoutes.ts | 1 - backend/src/openai.ts | 4 ++-- backend/src/sessionRoutes.ts | 6 ++++-- 5 files changed, 8 insertions(+), 12 deletions(-) diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 71a0ac5c7..cd26aedb5 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -4,7 +4,7 @@ services: NODE_ENV: ${NODE_ENV:-development} OPENAI_API_KEY: ${OPENAI_API_KEY} SESSION_SECRET: ${SESSION_SECRET} - CORS_ALLOW_ORIGIN: "${CORS_ALLOW_ORIGIN:-*}" + CORS_ALLOW_ORIGIN: '${CORS_ALLOW_ORIGIN:-*}' PORT: 3001 build: . image: 'scottlogic/prompt-injection-api' diff --git a/backend/src/models/chat.ts b/backend/src/models/chat.ts index 3e71fa19e..69bbe1fb1 100644 --- a/backend/src/models/chat.ts +++ b/backend/src/models/chat.ts @@ -161,9 +161,4 @@ export type { ToolCallResponse, MessageTransformation, }; -export { - CHAT_MODELS, - CHAT_MESSAGE_TYPE, - MODEL_CONFIG, - defaultChatModel, -}; +export { CHAT_MODELS, CHAT_MESSAGE_TYPE, MODEL_CONFIG, defaultChatModel }; diff --git a/backend/src/nonSessionRoutes.ts b/backend/src/nonSessionRoutes.ts index f168c6297..cae39c5ee 100644 --- a/backend/src/nonSessionRoutes.ts +++ b/backend/src/nonSessionRoutes.ts @@ -19,4 +19,3 @@ router.get('/health', handleHealthCheck); router.get('/systemRoles', handleGetSystemRoles); export default router; - diff --git a/backend/src/openai.ts b/backend/src/openai.ts index 98c199bb7..e9eb06b87 100644 --- a/backend/src/openai.ts +++ b/backend/src/openai.ts @@ -302,8 +302,8 @@ function getChatCompletionsFromHistory( const completions: ChatCompletionMessageParam[] = chatHistory.length > 0 ? (chatHistory - .filter((message) => message.completion !== null) - .map((message) => message.completion) as ChatCompletionMessageParam[]) + .filter((message) => message.completion !== null) + .map((message) => message.completion) as ChatCompletionMessageParam[]) : []; console.debug( diff --git a/backend/src/sessionRoutes.ts b/backend/src/sessionRoutes.ts index e2185d9a4..af9b648fa 100644 --- a/backend/src/sessionRoutes.ts +++ b/backend/src/sessionRoutes.ts @@ -1,7 +1,7 @@ +import 'dotenv/config'; import express from 'express'; import session from 'express-session'; import memoryStoreFactory from 'memorystore'; -import 'dotenv/config'; import { handleChatToGPT, @@ -40,7 +40,9 @@ declare module 'express-session' { const sessionSigningSecret = process.env.SESSION_SECRET; if (!sessionSigningSecret) { - console.error("SESSION_SECRET not found in environment vars, cannot continue!"); + console.error( + 'SESSION_SECRET not found in environment vars, cannot continue!' + ); process.exit(1); } From 1e62b9a01dc3b2a397d9afbc6ecda010210b7534 Mon Sep 17 00:00:00 2001 From: Chris Wilton-Magras Date: Fri, 2 Feb 2024 09:08:13 +0000 Subject: [PATCH 4/5] Fix broken tests --- backend/src/document.ts | 2 +- backend/test/integration/langchain.test.ts | 51 +++++++++++----------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/backend/src/document.ts b/backend/src/document.ts index b910237aa..3c10b6812 100644 --- a/backend/src/document.ts +++ b/backend/src/document.ts @@ -1,9 +1,9 @@ -import * as fs from 'fs'; import { CSVLoader } from 'langchain/document_loaders/fs/csv'; import { DirectoryLoader } from 'langchain/document_loaders/fs/directory'; import { PDFLoader } from 'langchain/document_loaders/fs/pdf'; import { TextLoader } from 'langchain/document_loaders/fs/text'; import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter'; +import * as fs from 'node:fs'; import { DocumentMeta } from './models/document'; import { LEVEL_NAMES } from './models/level'; diff --git a/backend/test/integration/langchain.test.ts b/backend/test/integration/langchain.test.ts index 84169f5a2..83f12327b 100644 --- a/backend/test/integration/langchain.test.ts +++ b/backend/test/integration/langchain.test.ts @@ -8,6 +8,7 @@ import { } from '@jest/globals'; import { RetrievalQAChain } from 'langchain/chains'; import { ChatOpenAI } from 'langchain/chat_models/openai'; +import { Document } from 'langchain/document'; import { PromptTemplate } from 'langchain/prompts'; import { @@ -31,9 +32,10 @@ const mockPromptEvalChain = { }; const mockFromLLM = jest.fn<() => typeof mockRetrievalQAChain>(); const mockFromTemplate = jest.fn(); -const mockLoader = jest.fn(); -const mockSplitDocuments = jest.fn<() => Promise>(); const mockAsRetriever = jest.fn(); +const mockLoader = + jest.fn<() => Promise>[]>>(); +const mockSplitDocuments = jest.fn<() => Promise>(); // eslint-disable-next-line prefer-const let mockValidModels: string[] = []; @@ -49,16 +51,15 @@ jest.mock('langchain/embeddings/openai', () => { }; }); -class MockMemoryVectorStore { - asRetriever() { - mockAsRetriever(); - } -} jest.mock('langchain/vectorstores/memory', () => { return { MemoryVectorStore: { fromDocuments: jest.fn(() => - Promise.resolve(new MockMemoryVectorStore()) + Promise.resolve({ + asRetriever() { + mockAsRetriever(); + }, + }) ), }, }; @@ -115,28 +116,28 @@ jest.mock('@src/openai', () => { }; }); -beforeEach(() => { - // reset environment variables - process.env = { - OPENAI_API_KEY: 'sk-12345', - }; - - mockFromLLM.mockImplementation(() => mockRetrievalQAChain); -}); +describe('langchain integration tests ', () => { + beforeEach(() => { + // reset environment variables + process.env = { + OPENAI_API_KEY: 'sk-12345', + }; + + mockFromLLM.mockImplementation(() => mockRetrievalQAChain); + mockLoader.mockResolvedValue([]); + }); -afterEach(() => { - mockPromptEvalChain.call.mockRestore(); - mockRetrievalQAChain.call.mockRestore(); - mockFromLLM.mockRestore(); - mockFromTemplate.mockRestore(); -}); + afterEach(() => { + mockPromptEvalChain.call.mockReset(); + mockRetrievalQAChain.call.mockReset(); + mockFromLLM.mockReset(); + mockFromTemplate.mockReset(); + mockLoader.mockReset(); + }); -describe('langchain integration tests ', () => { test('GIVEN application WHEN application starts THEN document vectors are loaded for all levels', async () => { const numberOfCalls = 4 + 1; // number of levels + common - mockSplitDocuments.mockResolvedValue([]); - await initDocumentVectors(); expect(mockLoader).toHaveBeenCalledTimes(numberOfCalls); expect(mockSplitDocuments).toHaveBeenCalledTimes(numberOfCalls); From 4cf06bdf6b3b6378c01b862dc62f526bba8a9a15 Mon Sep 17 00:00:00 2001 From: Chris Wilton-Magras Date: Mon, 5 Feb 2024 10:40:51 +0000 Subject: [PATCH 5/5] Use mockReset where possible in langchain tests --- backend/test/integration/langchain.test.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/backend/test/integration/langchain.test.ts b/backend/test/integration/langchain.test.ts index 83f12327b..7c87589b9 100644 --- a/backend/test/integration/langchain.test.ts +++ b/backend/test/integration/langchain.test.ts @@ -1,5 +1,6 @@ import { afterEach, + beforeAll, beforeEach, describe, test, @@ -117,22 +118,24 @@ jest.mock('@src/openai', () => { }); describe('langchain integration tests ', () => { + beforeAll(() => { + mockFromLLM.mockImplementation(() => mockRetrievalQAChain); + mockLoader.mockResolvedValue([]); + }); + beforeEach(() => { // reset environment variables process.env = { OPENAI_API_KEY: 'sk-12345', }; - - mockFromLLM.mockImplementation(() => mockRetrievalQAChain); - mockLoader.mockResolvedValue([]); }); afterEach(() => { mockPromptEvalChain.call.mockReset(); mockRetrievalQAChain.call.mockReset(); - mockFromLLM.mockReset(); - mockFromTemplate.mockReset(); - mockLoader.mockReset(); + mockFromLLM.mockClear(); + mockFromTemplate.mockClear(); + mockLoader.mockClear(); }); test('GIVEN application WHEN application starts THEN document vectors are loaded for all levels', async () => {