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 = ` +
+ ${taskIndex} + + ${taskDescription} +

+ `; + + // Add the filled out template + $('#objective-tasks').append(template); + + // Add event listeners and set properties + $(`#objective-task-complete-${taskIndex}`).prop('checked', taskComplete); + $(`#objective-task-complete-${taskIndex}`).on('click', event => { + const index = Number(event.target.id.split('-').pop()); + globalTasks[index].completed = event.target.checked; + setCurrentTask(); + }); + $(`#objective-task-description-${taskIndex}`).on('keyup', event => { + const index = Number(event.target.id.split('-').pop()); + globalTasks[index].description = event.target.textContent; + }); + } + +// Populate UI task list +function updateUiTaskList() { + $('#objective-tasks').empty() + for (const index in globalTasks) { + addUiTask( + index, + globalTasks[index].completed, + globalTasks[index].description + ) + } +} + +// Trigger creation of new tasks with given objective. +async function onGenerateObjectiveClick() { + globalObjective = $('#objective-text').val() + await generateTasks() + saveState() +} + +// Update extension prompts +function onChatDepthInput() { + saveState() + setCurrentTask() // Ensure extension prompt is updated +} + +// Update how often we check for task completion +function onCheckFrequencyInput() { + saveState() +} + +function onHideTasksInput(){ + $('#objective-tasks').prop('hidden',$('#objective-hide-tasks').prop('checked')) + saveState() +} + +function loadSettings() { + // Load/Init settings for chatId + currentChatId = getContext().chatId + + // Bail on home screen + if (currentChatId == undefined) { + return + } + if (!(currentChatId in extension_settings.objective)) { + extension_settings.objective[currentChatId] = {} + Object.assign(extension_settings.objective[currentChatId], defaultSettings) + } + + // Update globals + globalObjective = extension_settings.objective[currentChatId].objective + globalTasks = extension_settings.objective[currentChatId].tasks + checkCounter = extension_settings.objective[currentChatId].checkFrequency + + // Update UI elements + $('#objective-counter').text(checkCounter) + $("#objective-text").text(globalObjective) + updateUiTaskList() + $('#objective-chat-depth').val(extension_settings.objective[currentChatId].chatDepth) + $('#objective-check-frequency').val(extension_settings.objective[currentChatId].checkFrequency) + $('#objective-hide-tasks').prop('checked',extension_settings.objective[currentChatId].hideTasks) + onHideTasksInput() + setCurrentTask() +} + +jQuery(() => { + const settingsHtml = ` +
+
+
+ Objective +
+
+
+ + +
+
+
+ +
+ + (0 = disabled)
+ Messages until next AI task completion check 0 +
+
+
`; + + $('#extensions_settings').append(settingsHtml); + $('#objective-generate').on('click', onGenerateObjectiveClick) + $('#objective-chat-depth').on('input',onChatDepthInput) + $("#objective-check-frequency").on('input',onCheckFrequencyInput) + $('#objective-hide-tasks').on('click', onHideTasksInput) + loadSettings() + + eventSource.on(event_types.CHAT_CHANGED, () => { + resetState() + }); + + eventSource.on(event_types.MESSAGE_RECEIVED, () => { + if (currentChatId == undefined){ + return + } + if ($("#objective-check-frequency").val() > 0) { + checkTaskCompleted(); + checkCounter -= 1 + } + setCurrentTask(); + $('#objective-counter').text(checkCounter) + }); +}); diff --git a/public/scripts/extensions/objective/manifest.json b/public/scripts/extensions/objective/manifest.json new file mode 100644 index 000000000..c7f9513ed --- /dev/null +++ b/public/scripts/extensions/objective/manifest.json @@ -0,0 +1,11 @@ +{ + "display_name": "Objective", + "loading_order": 5, + "requires": [], + "optional": [], + "js": "index.js", + "css": "style.css", + "author": "Ouoertheo", + "version": "0.0.1", + "homePage": "" +} \ No newline at end of file diff --git a/public/scripts/extensions/objective/style.css b/public/scripts/extensions/objective/style.css new file mode 100644 index 000000000..6cfd8f419 --- /dev/null +++ b/public/scripts/extensions/objective/style.css @@ -0,0 +1,4 @@ +#objective-counter { + font-weight: 600; + color: orange; +} \ No newline at end of file diff --git a/readme.md b/readme.md index bd371ab7c..74d4a4565 100644 --- a/readme.md +++ b/readme.md @@ -295,3 +295,4 @@ GNU Affero General Public License for more details.** * AI Horde client library by ZeldaFan0225: https://github.com/ZeldaFan0225/ai_horde * Linux startup script by AlpinDale * Thanks paniphons for providing a FAQ document +* TTS and Objective extensions by Ouoertheo \ No newline at end of file