From 5d2a1ed4690a183089123e6d0c3f8df657d7fdd7 Mon Sep 17 00:00:00 2001 From: George Sproston <103250539+gsproston-scottlogic@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:52:34 +0100 Subject: [PATCH] Phase switching (#122) * WIP: Phase rework * Phase syncing between frontend and backend * Better backend phase transitions --- backend/src/app.js | 5 +- backend/src/phase.js | 71 ------------------ backend/src/router.js | 54 +++++--------- frontend/src/App.tsx | 22 +++++- frontend/src/Phases.ts | 74 ++++++++----------- frontend/src/components/ChatBox/ChatBox.tsx | 7 +- .../PhaseSelectionBox/PhaseSelectionBox.tsx | 74 ++++++------------- frontend/src/models/chat.ts | 1 + frontend/src/models/phase.ts | 30 ++------ frontend/src/service/chatService.ts | 7 +- frontend/src/service/phaseService.ts | 43 ++--------- 11 files changed, 115 insertions(+), 273 deletions(-) delete mode 100644 backend/src/phase.js diff --git a/backend/src/app.js b/backend/src/app.js index 3188eabe3..0b0cf9240 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -5,7 +5,6 @@ const cors = require("cors"); const session = require("express-session"); const { setOpenAiApiKey } = require("./openai"); const { getInitialDefences } = require("./defence"); -const { getPhases } = require("./phase"); dotenv.config(); @@ -61,8 +60,8 @@ app.use(function (req, res, next) { if (!req.session.gptModel) { req.session.gptModel = defaultModel; } - if (!req.session.phases) { - req.session.phases = getPhases(); + if (!req.session.numPhasesCompleted) { + req.session.numPhasesCompleted = 0; } next(); }); diff --git a/backend/src/phase.js b/backend/src/phase.js deleted file mode 100644 index 8cae11da8..000000000 --- a/backend/src/phase.js +++ /dev/null @@ -1,71 +0,0 @@ -function getPhases() { - const phases = [ - { - name: "phase 0", - index: 0, - info: "placeholder", - preamble: "placeholder", - path_to_documents: "/path/to/folder", - isCurrent: false, - isComplete: false, - }, - { - name: "phase 1", - index: 1, - info: "placeholder", - preamble: "placeholder", - path_to_documents: "/path/to/folder", - isCurrent: false, - isComplete: false, - }, - { - name: "phase 2", - preamble: "placeholder", - index: 2, - info: "placeholder", - path_to_documents: "/path/to/folder", - isCurrent: false, - isComplete: false, - }, - { - name: "sandbox", - index: 3, - info: "placeholder", - preamble: "placeholder", - path_to_documents: "/path/to/folder", - isCurrent: true, - isComplete: false, - }, - ]; - return phases; -} - -function getCurrentPhase(session) { - return session.phases.find((phase) => phase.isCurrent); -} - -function switchCurrentPhase(session, phaseName) { - return session.phases.map((phase) => - phase.name === phaseName - ? { ...phase, isCurrent: true } - : { ...phase, isCurrent: false } - ); -} - -function setPhaseCompleted(session, phaseName) { - return session.phases.map((phase) => - phase.name === phaseName ? { ...phase, isComplete: true } : phase - ); -} - -function getCompletedPhases(session) { - return session.phases.filter((phase) => phase.isComplete); -} - -module.exports = { - getPhases, - getCurrentPhase, - switchCurrentPhase, - setPhaseCompleted, - getCompletedPhases, -}; diff --git a/backend/src/router.js b/backend/src/router.js index a7c09391e..c076288be 100644 --- a/backend/src/router.js +++ b/backend/src/router.js @@ -11,12 +11,6 @@ const { setOpenAiApiKey, setGptModel, } = require("./openai"); -const { - getCurrentPhase, - switchCurrentPhase, - getCompletedPhases, - setPhaseCompleted, -} = require("./phase"); const router = express.Router(); // Activate a defence @@ -84,6 +78,11 @@ router.post("/openai/chat", async (req, res, next) => { let transformedMessage = ""; // parse out the message const message = req.body?.message; + const currentPhase = req.body?.currentPhase; + + // TODO remove, this is just for demonstration purposes + console.log("Current phase " + currentPhase); + if (message) { transformedMessage = message; // see if this message triggers any defences @@ -126,8 +125,21 @@ router.post("/openai/chat", async (req, res, next) => { reply = "Missing message"; console.error(reply); } + + // TODO remove, this is just for demonstration purposes + let numPhasesCompleted = req.session.numPhasesCompleted; + if (numPhasesCompleted < 3 && currentPhase === numPhasesCompleted) { + numPhasesCompleted = currentPhase + 1; + req.session.numPhasesCompleted = numPhasesCompleted; + } + // construct response - const response = { reply, defenceInfo, transformedMessage }; + const response = { + reply, + defenceInfo, + transformedMessage, + numPhasesCompleted, + }; // log and send the reply with defence info console.log(response); res.send(response); @@ -174,34 +186,8 @@ router.get("/openai/model", (req, res, next) => { res.send(req.session.gptModel); }); -router.post("/phase/current", (req, res, next) => { - const phase = req.body?.phase; - if (phase) { - req.session.phases = switchCurrentPhase(req.session, phase); - res.status(200).send("Phase set to " + req.session.phases); - } else { - res.status(400).send("Missing phase"); - } -}); - -router.get("/phase/current", (req, res, next) => { - res.send(getCurrentPhase(req.session)); -}); - router.get("/phase/completed", (req, res, next) => { - res.send(getCompletedPhases(req.session)); -}); - -router.post("/phase/completed", (req, res, next) => { - const phase = req.body?.phase; - if (phase) { - req.session.phases = setPhaseCompleted(req.session, phase); - res.send("Phase complete status set"); - } else { - res.statusCode = 400; - res.send("Missing phase"); - } + res.send(req.session.numPhasesCompleted.toString()); }); -// Importing the router module.exports = router; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bfc792cb5..a3829bb0f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import "./App.css"; import AttackBox from "./components/AttackBox/AttackBox"; import ChatBox from "./components/ChatBox/ChatBox"; @@ -10,12 +10,17 @@ import Header from "./components/Header"; import ModelSelectionBox from "./components/ModelSelectionBox/ModelSelectionBox"; import { useState } from "react"; import { EmailInfo } from "./models/email"; +import { getCompletedPhases } from "./service/phaseService"; function App() { const [defenceBoxKey, setDefenceBoxKey] = useState(0); const [emails, setEmails] = useState([]); const [triggeredDefences, setTriggeredDefences] = useState([]); + // start on sandbox mode + const [currentPhase, setCurrentPhase] = useState(0); + const [numCompletedPhases, setNumCompletedPhases] = useState(0); + const updateTriggeredDefences = (defenceDetails: string[]) => { // set the new triggered defences setTriggeredDefences(defenceDetails); @@ -23,6 +28,13 @@ function App() { setDefenceBoxKey(defenceBoxKey + 1); }; + // called on mount + useEffect(() => { + getCompletedPhases().then((numCompletedPhases) => { + setNumCompletedPhases(numCompletedPhases); + }); + }, []); + return (
@@ -39,13 +51,19 @@ function App() {
phases
- +
sent emails
diff --git a/frontend/src/Phases.ts b/frontend/src/Phases.ts index 7c72a5f49..07cd03c20 100644 --- a/frontend/src/Phases.ts +++ b/frontend/src/Phases.ts @@ -1,50 +1,34 @@ import { Phase, PHASE_NAMES } from "./models/phase"; -const DEFAULT_SANDBOX_PHASE: Phase = new Phase( - PHASE_NAMES.SANDBOX, - 3, - "app sandbox. ask the model questions and try get sensitive information", - "preamble", - "path_to_documents", - false, - true -); - const PHASES: Phase[] = [ - new Phase( - PHASE_NAMES.PHASE_0, - 0, - "easy phase to see how to ask the model question and get answers", - "preamble", - "path_to_documents", - false, - false - ), - new Phase( - PHASE_NAMES.PHASE_1, - 1, - "harder prompt to try and trick the model into telling you the secret", - "preamble", - "path_to_documents", - false, - false - ), - new Phase( - PHASE_NAMES.PHASE_2, - 2, - "even harder and you can use any feature on the app to try and trick the model into telling you the secret", - "preamble", - "path_to_documents", - false, - false - ), - DEFAULT_SANDBOX_PHASE, + { + id: PHASE_NAMES.PHASE_0, + name: "phase 0", + preamble: "preamble", + isComplete: false, + isCurrent: false, + }, + { + id: PHASE_NAMES.PHASE_1, + name: "phase 1", + preamble: "preamble", + isComplete: false, + isCurrent: false, + }, + { + id: PHASE_NAMES.PHASE_2, + name: "phase 2", + preamble: "preamble", + isComplete: false, + isCurrent: false, + }, + { + id: PHASE_NAMES.SANDBOX, + name: "sandbox", + preamble: "preamble", + isComplete: false, + isCurrent: false, + }, ]; -function getPhaseByName(phaseName: string): Phase { - return ( - PHASES.find((phase) => phase.name === phaseName) || DEFAULT_SANDBOX_PHASE - ); -} - -export { PHASES, getPhaseByName }; +export { PHASES }; diff --git a/frontend/src/components/ChatBox/ChatBox.tsx b/frontend/src/components/ChatBox/ChatBox.tsx index dbe50776c..eb16926be 100644 --- a/frontend/src/components/ChatBox/ChatBox.tsx +++ b/frontend/src/components/ChatBox/ChatBox.tsx @@ -9,9 +9,13 @@ import { EmailInfo } from "../../models/email"; function ChatBox( this: any, { + currentPhase, + setNumCompletedPhases, setEmails, updateTriggeredDefences, }: { + currentPhase: number; + setNumCompletedPhases: (numCompletedPhases: number) => void; setEmails: (emails: EmailInfo[]) => void; updateTriggeredDefences: (defences: string[]) => void; } @@ -52,7 +56,8 @@ function ChatBox( // clear the input event.currentTarget.value = ""; - const response: ChatResponse = await sendMessage(message); + const response: ChatResponse = await sendMessage(message, currentPhase); + setNumCompletedPhases(response.numPhasesCompleted); const transformedMessage = response.transformedMessage; const isTransformed = transformedMessage !== message; // add the transformed message to the chat box if it is different from the original message diff --git a/frontend/src/components/PhaseSelectionBox/PhaseSelectionBox.tsx b/frontend/src/components/PhaseSelectionBox/PhaseSelectionBox.tsx index 0a4b9d078..06ead87a5 100644 --- a/frontend/src/components/PhaseSelectionBox/PhaseSelectionBox.tsx +++ b/frontend/src/components/PhaseSelectionBox/PhaseSelectionBox.tsx @@ -1,73 +1,41 @@ -import React, { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import { Phase } from "../../models/phase"; import { PHASES } from "../../Phases"; -import { - getCurrentPhase, - setCurrentPhase, - setCompletedPhase, -} from "../../service/phaseService"; import "./PhaseSelectionBox.css"; - -function PhaseSelectionBox(this: any) { - // start on sandbox mode - const [currPhase, setCurrPhase] = useState(PHASES[3]); - - // valid phases that the user can go to from the current phase - const [currValidPhases, setCurrValidPhases] = useState([0]); - - useEffect(() => { - const getPhaseInfo = async () => { - // get current phase from backend on refresh - const currentPhase = await getCurrentPhase(); - setCurrPhase(currentPhase); - setCurrValidPhases(getValidPhaseIndexes(currPhase)); - }; - getPhaseInfo(); - }, []); - - const getValidPhaseIndexes = (phase: Phase) => { - console.log("getting valid phases for: ", JSON.stringify(phase)); - // user can go from sandbox to phase 0 at any time - if (phase.index === 3) { - return [0, 3]; - // if current phase is complete, allow user to go to next phase - } else if (phase.isComplete) { - return [phase.index + 1, 3]; - } - // always allow sandbox mode to be chosen - else return [3]; - }; - +import { getCompletedPhases } from "../../service/phaseService"; + +function PhaseSelectionBox( + this: any, + { + currentPhase, + numCompletedPhases, + setCurrentPhase, + }: { + currentPhase: number; + numCompletedPhases: number; + setCurrentPhase: (newPhase: number) => void; + } +) { const handlePhaseChange = async (index: number) => { - if (index !== currPhase.index) { - const newPhase = PHASES[index]; - await setCurrentPhase(newPhase.name); - setCurrValidPhases(getValidPhaseIndexes(newPhase)); - setCurrPhase(newPhase); + if (index !== currentPhase) { + const newPhase = index; + console.log(`Changing phase to ${newPhase}`); + setCurrentPhase(newPhase); } }; - // if current phase is complete, show the next button as clickable - // todo: can be used when implementing the win condition - const handleCompletePhase = async () => { - await setCompletedPhase(currPhase.name); - currPhase.isComplete = true; - setCurrPhase(currPhase); - setCurrValidPhases(getValidPhaseIndexes(currPhase)); - }; - return (
{PHASES.map((phase: Phase, index: number) => { return ( diff --git a/frontend/src/models/chat.ts b/frontend/src/models/chat.ts index 6762c6091..1837ef9f4 100644 --- a/frontend/src/models/chat.ts +++ b/frontend/src/models/chat.ts @@ -24,6 +24,7 @@ interface ChatMessage { interface ChatResponse { reply: string; defenceInfo: ChatDefenceReport; + numPhasesCompleted: number; transformedMessage: string; } diff --git a/frontend/src/models/phase.ts b/frontend/src/models/phase.ts index 1384ee9b3..e1bd5832e 100644 --- a/frontend/src/models/phase.ts +++ b/frontend/src/models/phase.ts @@ -5,31 +5,13 @@ enum PHASE_NAMES { SANDBOX = "sandbox", } -// phase data for the frontend -class Phase { - constructor( - name: PHASE_NAMES, - index: number, - info: string, - preamble: string, - path_to_documents: string, - isComplete: boolean, - isCurrent: boolean - ) { - this.name = name; - this.index = index; - this.info = info; - this.preamble = preamble; - this.path_to_documents = path_to_documents; - this.isCurrent = isCurrent; - this.isComplete = isComplete; - } - name: PHASE_NAMES; - index: number; - info: string; +interface Phase { + id: PHASE_NAMES; + name: string; preamble: string; - path_to_documents: string; isCurrent: boolean; isComplete: boolean; } -export { PHASE_NAMES, Phase }; + +export { PHASE_NAMES }; +export type { Phase }; diff --git a/frontend/src/service/chatService.ts b/frontend/src/service/chatService.ts index 031c616c8..ce058c95d 100644 --- a/frontend/src/service/chatService.ts +++ b/frontend/src/service/chatService.ts @@ -8,12 +8,15 @@ const clearChat = async (): Promise => { return response.status === 200; }; -const sendMessage = async (message: string): Promise => { +const sendMessage = async ( + message: string, + currentPhase: number +): Promise => { const response = await sendRequest( PATH + "chat", "POST", { "Content-Type": "application/json" }, - JSON.stringify({ message }) + JSON.stringify({ message, currentPhase }) ); const data = await response.json(); console.log(data); diff --git a/frontend/src/service/phaseService.ts b/frontend/src/service/phaseService.ts index 39f104e2a..2a257b3a9 100644 --- a/frontend/src/service/phaseService.ts +++ b/frontend/src/service/phaseService.ts @@ -1,44 +1,11 @@ import { sendRequest } from "./backendService"; -import { Phase } from "../models/phase"; -import { getPhaseByName } from "../Phases"; const PATH = "phase/"; -const getCurrentPhase = async (): Promise => { - const repsonse = await sendRequest(PATH + "current", "GET"); - const phaseName = await repsonse.json(); - // get the phase object that matches the name - return getPhaseByName(phaseName.name); -}; - -const setCurrentPhase = async (phase: string) => { - const response = await sendRequest( - PATH + "current", - "POST", - { "Content-Type": "application/json" }, - JSON.stringify({ phase: phase }) - ); -}; - -const getCompletedPhases = async (): Promise => { +async function getCompletedPhases(): Promise { const response = await sendRequest(PATH + "completed", "GET"); - const phaseList: Phase[] = await response.json(); - const phaseNames: string[] = phaseList.map((phase: Phase) => phase.name); - return phaseNames.map((phaseName: string) => getPhaseByName(phaseName)); -}; - -const setCompletedPhase = async (phase: string) => { - const response = await sendRequest( - PATH + "completed", - "POST", - { "Content-Type": "application/json" }, - JSON.stringify({ phase: phase }) - ); -}; + const numCompletedPhases: number = await response.json(); + return numCompletedPhases; +} -export { - getCurrentPhase, - setCurrentPhase, - getCompletedPhases, - setCompletedPhase, -}; +export { getCompletedPhases };