import { chat_metadata, callPopup, saveSettingsDebounced, is_send_press } from "../../../script.js";
import { getContext, extension_settings, saveMetadataDebounced } from "../../extensions.js";
import {
substituteParams,
eventSource,
event_types,
generateQuietPrompt,
} from "../../../script.js";
import { registerSlashCommand } from "../../slash-commands.js";
import { waitUntilCondition } from "../../utils.js";
import { is_group_generating, selected_group } from "../../group-chats.js";
const MODULE_NAME = "Objective"
let taskTree = null
let globalTasks = []
let currentChatId = ""
let currentObjective = null
let currentTask = null
let checkCounter = 0
let lastMessageWasSwipe = false
const defaultPrompts = {
"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
`,
'currentTask':`Your current task is [{{task}}]. Balance existing roleplay with completing this task.`
}
let objectivePrompts = defaultPrompts
//###############################//
//# Task Management #//
//###############################//
// Return the task and index or throw an error
function getTaskById(taskId){
if (taskId == null) {
throw `Null task id`
}
return getTaskByIdRecurse(taskId, taskTree)
}
function getTaskByIdRecurse(taskId, task) {
if (task.id == taskId){
return task
}
for (const childTask of task.children) {
const foundTask = getTaskByIdRecurse(taskId, childTask);
if (foundTask != null) {
return foundTask;
}
}
return null;
}
function substituteParamsPrompts(content) {
content = content.replace(/{{objective}}/gi, currentObjective.description)
content = content.replace(/{{task}}/gi, currentTask.description)
if (currentTask.parent){
content = content.replace(/{{parent}}/gi, currentTask.parent.description)
}
content = substituteParams(content)
return content
}
// Call Quiet Generate to create task list using character context, then convert to tasks. Should not be called much.
async function generateTasks() {
const prompt = substituteParamsPrompts(objectivePrompts.createTask);
console.log(`Generating tasks for objective with prompt`)
toastr.info('Generating tasks for objective', 'Please wait...');
const taskResponse = await generateQuietPrompt(prompt)
// Clear all existing objective tasks when generating
currentObjective.children = []
const numberedListPattern = /^\d+\./
// Create tasks from generated task list
for (const task of taskResponse.split('\n').map(x => x.trim())) {
if (task.match(numberedListPattern) != null) {
currentObjective.addTask(task.replace(numberedListPattern,"").trim())
}
}
updateUiTaskList();
setCurrentTask();
console.info(`Response for Objective: '${currentObjective.description}' was \n'${taskResponse}', \nwhich created tasks \n${JSON.stringify(currentObjective.children.map(v => {return v.toSaveState()}), null, 2)} `)
toastr.success(`Generated ${currentObjective.children.length} tasks`, 'Done!');
}
// Call Quiet Generate to check if a task is completed
async function checkTaskCompleted() {
// Make sure there are tasks
if (jQuery.isEmptyObject(currentTask)) {
return
}
try {
// Wait for group to finish generating
if (selected_group) {
await waitUntilCondition(() => is_group_generating === false, 1000, 10);
}
// Another extension might be doing something with the chat, so wait for it to finish
await waitUntilCondition(() => is_send_press === false, 30000, 10);
} catch {
console.debug("Failed to wait for group to finish generating")
return;
}
checkCounter = $('#objective-check-frequency').val()
toastr.info("Checking for task completion.")
const prompt = substituteParamsPrompts(objectivePrompts.checkTaskCompleted);
const taskResponse = (await generateQuietPrompt(prompt)).toLowerCase()
// Check response if task complete
if (taskResponse.includes("true")) {
console.info(`Character determined task '${currentTask.description} is completed.`)
currentTask.completeTask()
} 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}`)
}
}
function getNextIncompleteTaskRecurse(task){
if (task.completed === false // Return task if incomplete
&& task.children.length === 0 // Ensure task has no children, it's subtasks will determine completeness
&& task.parentId !== "" // Must have parent id. Only root task will be missing this and we dont want that
){
return task
}
for (const childTask of task.children) {
if (childTask.completed === true){ // Don't recurse into completed tasks
continue
}
const foundTask = getNextIncompleteTaskRecurse(childTask);
if (foundTask != null) {
return foundTask;
}
}
return null;
}
// Set a task in extensionPrompt context. Defaults to first incomplete
function setCurrentTask(taskId = null) {
const context = getContext();
// TODO: Should probably null this rather than set empty object
currentTask = {};
// Find the task, either next incomplete, or by provided taskId
if (taskId === null) {
currentTask = getNextIncompleteTaskRecurse(taskTree) || {};
} else {
currentTask = getTaskById(taskId);
}
// Don't just check for a current task, check if it has data
const description = currentTask.description || null;
if (description) {
const extensionPromptText = substituteParamsPrompts(objectivePrompts.currentTask);
// Remove highlights
$('.objective-task').css({'border-color':'','border-width':''})
// Highlight current task
let highlightTask = currentTask
while (highlightTask.parentId !== ""){
if (highlightTask.descriptionSpan){
highlightTask.descriptionSpan.css({'border-color':'yellow','border-width':'2px'});
}
const parent = getTaskById(highlightTask.parentId)
highlightTask = parent
}
// Update the extension prompt
context.setExtensionPrompt(MODULE_NAME, extensionPromptText, 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();
}
function getHighestTaskIdRecurse(task) {
let nextId = task.id;
for (const childTask of task.children) {
const childId = getHighestTaskIdRecurse(childTask);
if (childId > nextId) {
nextId = childId;
}
}
return nextId;
}
//###############################//
//# Task Class #//
//###############################//
class ObjectiveTask {
id
description
completed
parentId
children
// UI Elements
taskHtml
descriptionSpan
completedCheckbox
deleteTaskButton
addTaskButton
constructor ({id=undefined, description, completed=false, parentId=""}) {
this.description = description
this.parentId = parentId
this.children = []
this.completed = completed
// Generate a new ID if none specified
if (id==undefined){
this.id = getHighestTaskIdRecurse(taskTree) + 1
} else {
this.id=id
}
}
// Accepts optional index. Defaults to adding to end of list.
addTask(description, index = null) {
index = index != null ? index: index = this.children.length
this.children.splice(index, 0, new ObjectiveTask(
{description: description, parentId: this.id}
))
saveState()
}
getIndex(){
if (this.parentId !== null) {
const parent = getTaskById(this.parentId)
const index = parent.children.findIndex(task => task.id === this.id)
if (index === -1){
throw `getIndex failed: Task '${this.description}' not found in parent task '${parent.description}'`
}
return index
} else {
throw `getIndex failed: Task '${this.description}' has no parent`
}
}
// Used to set parent to complete when all child tasks are completed
checkParentComplete() {
let all_completed = true;
if (this.parentId !== ""){
const parent = getTaskById(this.parentId);
for (const child of parent.children){
if (!child.completed){
all_completed = false;
break;
}
}
if (all_completed){
parent.completed = true;
console.info(`Parent task '${parent.description}' completed after all child tasks complated.`)
} else {
parent.completed = false;
}
}
}
// Complete the current task, setting next task to next incomplete task
completeTask() {
this.completed = true
console.info(`Task successfully completed: ${JSON.stringify(this.description)}`)
this.checkParentComplete()
setCurrentTask()
updateUiTaskList()
}
// Add a single task to the UI and attach event listeners for user edits
addUiElement() {
const template = `
Edit prompts used by Objective for this session. You can use {{objective}} or {{task}} plus any other standard template variables. Save template to persist changes.