diff --git a/public/script.js b/public/script.js index e8886e356..0281a43e5 100644 --- a/public/script.js +++ b/public/script.js @@ -423,6 +423,7 @@ export const event_types = { MESSAGE_EDITED: 'message_edited', MESSAGE_DELETED: 'message_deleted', IMPERSONATE_READY: 'impersonate_ready', + CHAT_CHANGED: 'chat_id_changed', } export const eventSource = new EventEmitter(); @@ -3582,6 +3583,7 @@ async function getChatResult() { if (chat.length === 1) { await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1)); } + await eventSource.emit(event_types.CHAT_CHANGED, (getCurrentChatId())); } async function openCharacterChat(file_name) { diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 313a002ec..cf51469dd 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -55,6 +55,7 @@ const extension_settings = { sd: {}, chromadb: {}, translate: {}, + objective: {}, }; let modules = []; diff --git a/public/scripts/extensions/objective/index.js b/public/scripts/extensions/objective/index.js new file mode 100644 index 000000000..cc3fef741 --- /dev/null +++ b/public/scripts/extensions/objective/index.js @@ -0,0 +1,351 @@ +import { callPopup, extension_prompt_types } from "../../../script.js"; +import { getContext, extension_settings } from "../../extensions.js"; +import { + substituteParams, + eventSource, + event_types, + saveSettingsDebounced +} from "../../../script.js"; + +const MODULE_NAME = "Objective" + +let globalObjective = "" +let globalTasks = [] +let currentChatId = "" +let currentTask = {} +let checkCounter = 0 + + +const objectivePrompts = { + "createTask": `Pause your roleplay and generate a list of tasks to complete an objective. Your next response must be formatted as a numbered list of plain text entries. Do not include anything but the numbered list. The list must be prioritized in the order that tasks must be completed. + + The objective that you must make a numbered task list for is: [{{objective}}]. + The tasks created should take into account the character traits of {{char}}. These tasks may or may not involve {{user}} directly. Be sure to include the objective as the final task. + + Given an example objective of 'Make me a four course dinner', here is an example output: + 1. Determine what the courses will be + 2. Find recipes for each course + 3. Go shopping for supplies with {{user}} + 4. Cook the food + 5. Get {{user}} to set the table + 6. Serve the food + 7. Enjoy eating the meal with {{user}} + `, + "checkTaskCompleted": `Pause your roleplay. Determine if this task is completed: [{{task}}]. + To do this, examine the most recent messages. Your response must only contain either true or false, nothing other words. + Example output: + true + ` +} + +const injectPrompts = { + "task": "Your current task is [{{task}}]. Balance existing roleplay with completing this task." +} + +// Background prompt generation +async function generateQuietPrompt(quiet_prompt) { + return await new Promise( + async function promptPromise(resolve, reject) { + try { + await getContext().generate('quiet', { resolve, reject, quiet_prompt, force_name2: true, }); + } + catch { + reject(); + } + }); +} + +//###############################// +//# Task Management #// +//###############################// + +// Accepts optional position. Defaults to adding to end of list. +function addTask(description, position=null) { + position = position ? position != null : position = globalTasks.length + globalTasks.splice(position, 0, { + "description": description, + "completed": false + }) + saveState() +} + +// Get a task either by index or task description. Return current task if none specified +function getTask(index=null, taskDescription=null){ + let task = {} + if (index == null && taskDescription==null) { + task = currentTask + } else if (index != null){ + task = globalObjective[index] + } else if (taskDescription != null){ + task = globalTasks.find(task => { + return true ? task.description == description : false + }) + } + return task +} + +// Complete the current task, setting next task to next incomplete task +function completeTask(task) { + task.completed = true + console.info(`Task successfully completed: ${JSON.stringify(task)}`) + setCurrentTask() + updateUiTaskList() + saveState() +} + +// Call Quiet Generate to create task list using character context, then convert to tasks. Should not be called much. +async function generateTasks() { + const prompt = substituteParams(objectivePrompts["createTask"].replace(/{{objective}}/gi, globalObjective)); + console.log(`Generating tasks for objective with prompt`) + const taskResponse = await generateQuietPrompt(prompt) + globalTasks = [] + const numberedListPattern = /^\d+\./ + + // Add numbered tasks, store them without the numbers. + for (const task of taskResponse.split('\n')) { + if (task.match(numberedListPattern) != null) { + addTask(task.replace(numberedListPattern,'').trim()) + } + } + updateUiTaskList() + console.info(`Response for Objective: '${globalObjective}' was \n'${taskResponse}', \nwhich created tasks \n${JSON.stringify(globalTasks, null, 2)} `) +} + +// Call Quiet Generate to check if a task is completed +async function checkTaskCompleted() { + // Make sure there are tasks and check is enabled + if (currentTask == {} || $('#objective-check-frequency').val() == 0){ + return + } + + // Check only at specified interval + if (checkCounter > 0){ + return + } + checkCounter = $('#objective-check-frequency').val() + + const prompt = substituteParams(objectivePrompts["checkTaskCompleted"].replace(/{{task}}/gi, currentTask.description)); + const taskResponse = (await generateQuietPrompt(prompt)).toLowerCase() + + // Check response if task complete + if (taskResponse.includes("true")){ + console.info(`Character determined task '${JSON.stringify(currentTask)} is completed.`) + completeTask(getTask()) + } else if (!(taskResponse.includes("false"))) { + console.warn(`checkTaskCompleted response did not contain true or false. taskResponse: ${taskResponse}`) + } else { + console.debug(`Checked task completion. taskResponse: ${taskResponse}`) + } +} + + +// Set a task in extensionPrompt context. Defaults to first incomplete +function setCurrentTask(index = null) { + const context = getContext(); + let currentTask = {}; + + if (index === null) { + currentTask = globalTasks.find(task => !task.completed) || {}; + } else if (index >= 0 && index < globalTasks.length) { + currentTask = globalTasks[index]; + } + + const { description } = currentTask; + const injectPromptsTask = injectPrompts["task"].replace(/{{task}}/gi, description); + + if (description) { + context.setExtensionPrompt(MODULE_NAME, injectPromptsTask, 1, $('#objective-chat-depth').val()); + console.info(`Current task in context.extensionPrompts.Objective is ${JSON.stringify(context.extensionPrompts.Objective)}`); + } else { + context.setExtensionPrompt(MODULE_NAME, ''); + console.info(`No current task`); + } + + saveState(); + } + + + +//###############################// +//# UI AND Settings #// +//###############################// + + +const defaultSettings = { + objective: "", + tasks: [], + chatDepth: 2, + checkFrequency:3, + hideTasks: false +} + +// Convenient single call. Not much at the moment. +function resetState(){ + loadSettings(); +} + +// +function saveState(){ + if (currentChatId == ""){ + currentChatId = getContext().chatId + } + extension_settings.objective[currentChatId].objective = globalObjective + extension_settings.objective[currentChatId].tasks = globalTasks + extension_settings.objective[currentChatId].checkFrequency = $('#objective-check-frequency').val() + extension_settings.objective[currentChatId].chatDepth = $('#objective-chat-depth').val() + extension_settings.objective[currentChatId].hideTasks = $('#objective-hide-tasks').prop('checked') + saveSettingsDebounced() +} + +// Dump core state +function debugObjectiveExtension(){ + console.log(JSON.stringify({ + "currentTask": currentTask, + "currentChatId": currentChatId, + "checkCounter": checkCounter, + "globalObjective": globalObjective, + "globalTasks": globalTasks, + "extension_settings": extension_settings.objective[currentChatId], + }, null, 2)) +} + +window.debugObjectiveExtension = debugObjectiveExtension + + +// Add a single task to the UI and attach event listeners for user edits +function addUiTask(taskIndex, taskComplete, taskDescription) { + const template = ` +