diff --git a/package.json b/package.json index 7bf552c..cbb2286 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "dotenv": "^11.0.0", "express": "^4.17.2", "isomorphic-fetch": "^3.0.0", + "prompt-engine": "^0.0.21", "react": "^17.0.2", "react-dom": "^17.0.2" }, diff --git a/src/server/Context.ts b/src/server/Context.ts deleted file mode 100644 index f6df31b..0000000 --- a/src/server/Context.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Class for creating and managing contexts and prompts for Codex. We define the context as a set of examples and previous interactions, whereas - * a prompt is a context plus command, which we pass to the model to get a code response. - * - * We keep track of both the text context and an array of interactions in order to facilitate "undoing" of interactions, and removal of the oldest - * interactions (see `trimContext`) when a context gets too large for the model. - */ - -type Interaction = { - command: string; - code: string; -}; - -export default class Context { - private baseContext: string; - private context: string; - private baseInteractions: Interaction[]; - private interactions: Interaction[]; - - constructor(baseContext: string) { - this.baseContext = baseContext; - this.context = baseContext; - - this.baseInteractions = this.createInteractionsFromContext(baseContext); - this.interactions = this.baseInteractions; - } - - // Returns the current cached prompt - getContext() { - return this.context; - } - - // Adds command (modelled as a comment) to a context, creating our prompt to the model - getPrompt(command: string) { - return `${this.context}\n\n/* ${command} */\n`; - } - - // Adds a new interaction (command and code) to the prompt, "remembering" it for future turns - addInteraction(command: string, code: string) { - let context = `${this.context}\n/* ${command} */\n${code}`; - this.context = context.split("\n").slice(0, -1).join("\n"); - this.interactions.push({ - command, - code - }); - } - - // Removes the last interaction, "forgetting" it - undoInteraction() { - if (this.interactions.length > 0) { - this.interactions.pop(); - this.createContextFromInteractions(); - } - } - - // Resets prompt to the original context - resetContext() { - this.context = this.baseContext; - this.interactions = this.baseInteractions; - } - - // Trims the prompt to remain under a certain length, removing interactions from the top. This is necessary to prevent the prompt from getting too long for a given model. - // The side effect is that the prompt will be truncated, "forgetting" past interactions or context. - trimContext(length: number) { - console.log("Trimming Context"); - while (this.context.length > length) { - console.log( - `Trimming oldest interaction off context: ${this.interactions[0].command}` - ); - this.interactions = this.interactions.slice(1); - this.createContextFromInteractions(); - console.log(`New prompt length: ${this.context.length}`); - } - this.createContextFromInteractions(); - } - - // Creates prompt using interactions (commands and code), modelling the commands as comments - createContextFromInteractions() { - this.context = this.interactions.reduce((prev, next) => { - return `${prev}\n/* ${next.command} */\n${next.code}`; - }, ""); - } - - // Turns a text prompt into an array of interactions (commands and code) - this is effectively the inverse of the createContextFromInteractions method - createInteractionsFromContext(baseContext) { - let interactions: Interaction[] = []; - let lines = baseContext.split("\n"); - for (let i = 0; i < lines.length; i++) { - let line = lines[i]; - if (line.startsWith("/*")) { - // get command, trimming whitespace - let command = line.substring(2, line.indexOf("*/")).trim(); - let code = ""; - for (let j = i + 1; j < lines.length; j++) { - if (lines[j].startsWith("/*")) { - break; - } else { - code += lines[j] + "\n"; - } - } - interactions.push({ - command, - code - }); - } - } - return interactions; - } -} diff --git a/src/server/app.ts b/src/server/app.ts index 5e0e759..aa8e95d 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -1,6 +1,6 @@ import express from "express"; import path from "path"; -import { context, getCompletion } from "./model"; +import { promptEngine, getCompletion } from "./model"; import cors from "cors"; const app = express(); @@ -23,10 +23,10 @@ app.post("/codegen", async (req, res) => { // Gets natural language and returns code app.get("/reset", async (_req, res) => { - context.resetContext(); + promptEngine.resetContext(); res.send( JSON.stringify({ - context: context.getContext() + context: promptEngine.buildContext() }) ); }); diff --git a/src/server/contexts/context1.ts b/src/server/contexts/context1.ts index bf15233..6780bf9 100644 --- a/src/server/contexts/context1.ts +++ b/src/server/contexts/context1.ts @@ -1,69 +1,84 @@ -export const baseContext = `/* This document contains a BabylonJS scene, natural language commands and the BabylonJS code needed to accomplish them */ - -state = {}; - -/* Make a cube */ -state.cube = BABYLON.MeshBuilder.CreateBox("cube", {size: 1}, scene); - -/* Move the cube up */ -state.cube.position.y += 1; - -/* Move it to the left */ -state.cube.position.x -= 1; - -/* Make the block teal */ -state.cube.material = new BABYLON.StandardMaterial("mat", scene); -state.cube.material.diffuseColor = new BABYLON.Color3(0, 1, 1); - -/* Now make it spin */ -state.intervals["spinningCubeInterval"] = setInterval(() => { +export const promptDescription = "This document contains a BabylonJS scene, natural language commands and the BabylonJS code needed to accomplish them"; + +export const promptExamples = [ + { + input: `Initial state`, + response: `state = {};` + }, + { + input: `Make a cube`, + response: `state.cube = BABYLON.MeshBuilder.CreateBox("cube", {size: 1}, scene);` + }, + { + input: `Move the cube up`, + response: `state.cube.position.y += 1;` + }, + { + input: `Move it to the left`, + response: `state.cube.position.x -= 1;` + }, + { + input: `Make the block teal`, + response: `state.cube.material = new BABYLON.StandardMaterial("mat", scene); +state.cube.material.diffuseColor = new BABYLON.Color3(0, 1, 1);` + }, + { + input: `Now make it spin`, + response: `state.intervals["spinningCubeInterval"] = setInterval(() => { scene.meshes[0].rotation.y += 0.02 -}, 10); - -/* Make it stop */ -clearInterval(state.intervals.spinningCubeInterval); - -/* Make it change color when the mouse is over it */ -state.cube.actionManager = new BABYLON.ActionManager(scene); - +}, 10);` + }, + { + input: `Make it stop`, + response: `clearInterval(state.intervals.spinningCubeInterval);` + }, + { + input: `Make it change color when the mouse is over it`, + response: `state.cube.actionManager = new BABYLON.ActionManager(scene); state.hoverAction = new BABYLON.ExecuteCodeAction(BABYLON.ActionManager.OnPointerOverTrigger, function () { state.cube.material = new BABYLON.StandardMaterial("mat", scene); state.cube.material.diffuseColor = new BABYLON.Color3(1, 0, 0); }); - state.unHoverAction = new BABYLON.ExecuteCodeAction(BABYLON.ActionManager.OnPointerOutTrigger, function () { state.cube.material = new BABYLON.StandardMaterial("mat", scene); state.cube.material.diffuseColor = new BABYLON.Color3(0, 1, 1); }); - state.cube.actionManager.registerAction(state.hoverAction); -state.cube.actionManager.registerAction(state.unHoverAction); - -/* Put a sphere on top of the cube */ -state.sphere = BABYLON.MeshBuilder.CreateSphere("sphere", {diameter: 1}, scene); -state.sphere.position.y = 1; - -/* Delete the sphere and the cube */ -state.sphere.dispose(); -state.cube.dispose(); - -/* make 50 cubes side by side */ -state.cubes = []; +state.cube.actionManager.registerAction(state.unHoverAction);` + }, + { + input: `Put a sphere on top of the cube`, + response: `state.sphere = BABYLON.MeshBuilder.CreateSphere("sphere", {diameter: 1}, scene); +state.sphere.position.y = 1;` + }, + { + input: `Delete the sphere and the cube`, + response: `state.sphere.dispose(); +state.cube.dispose();` + }, + { + input: `make 50 cubes side by side`, + response: `state.cubes = []; for (let i = 0; i < 50; i++) { state.cubes[i] = BABYLON.MeshBuilder.CreateBox("cube", {size: 1}, scene); state.cubes[i].position.x = i; -} - -/* stack them like stairs */ -for (let i = 0; i < 50; i++) { +}` + }, + { + input: `stack them like stairs`, + response: `for (let i = 0; i < 50; i++) { state.cubes[i].position.y = i; -} - -/* remove them */ -for (let i = 0; i < 50; i++) { +}` + }, + { + input: `remove them`, + response: `for (let i = 0; i < 50; i++) { state.cubes[i].dispose(); -} +}` + }, + { + input: `make the background red`, + response: `scene.clearColor = new BABYLON.Color3(1, 0, 0);` + } +]; -/* make the background red */ -scene.clearColor = new BABYLON.Color3(1, 0, 0); -`; diff --git a/src/server/model.ts b/src/server/model.ts index c0408a7..0e74284 100644 --- a/src/server/model.ts +++ b/src/server/model.ts @@ -1,22 +1,19 @@ require("dotenv").config(); import fetch from "isomorphic-fetch"; - +import {CodeEngine} from 'prompt-engine' // Contains the helper methods for interacting with Codex and crafting model prompts -import { baseContext } from "./contexts/context1"; -import Context from "./Context"; +import { promptDescription, promptExamples } from "./contexts/context1"; import { detectSensitiveContent } from "./contentFiltering"; const maxPromptLength = 3200; // CURRENTLY SINGLE TENANT - WOULD NEED TO UPDATE THIS TO A MAP OF TENANT IDs TO PROMPTS TO MAKE MULTI-TENANT -export const context = new Context(baseContext); +export const promptEngine: CodeEngine = new CodeEngine(promptDescription, promptExamples, { + maxTokens: maxPromptLength, + }); export async function getCompletion(command: string) { - let prompt = context.getPrompt(command); - - if (prompt.length > maxPromptLength) { - context.trimContext(maxPromptLength - command.length + 6); // The max length of the prompt, including the command, comment operators and spacing. - } + let prompt = promptEngine.buildPrompt(command); // To learn more about making requests to OpanAI API, please refer to https://beta.openai.com/docs/api-reference/making-requests. // Here we use the following endpoint pattern for engine selection. @@ -65,7 +62,7 @@ export async function getCompletion(command: string) { } else { //only allow safe interactions to be added to the context history - context.addInteraction(command, code); + promptEngine.addInteraction(command, code); } return {