mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge pull request #415 from ouoertheo/ouoertheo/objectives1
Objective Extension - Agent based AI inspired by BabyAGI and AutoGPT
This commit is contained in:
@ -423,6 +423,7 @@ export const event_types = {
|
|||||||
MESSAGE_EDITED: 'message_edited',
|
MESSAGE_EDITED: 'message_edited',
|
||||||
MESSAGE_DELETED: 'message_deleted',
|
MESSAGE_DELETED: 'message_deleted',
|
||||||
IMPERSONATE_READY: 'impersonate_ready',
|
IMPERSONATE_READY: 'impersonate_ready',
|
||||||
|
CHAT_CHANGED: 'chat_id_changed',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const eventSource = new EventEmitter();
|
export const eventSource = new EventEmitter();
|
||||||
@ -3582,6 +3583,7 @@ async function getChatResult() {
|
|||||||
if (chat.length === 1) {
|
if (chat.length === 1) {
|
||||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, (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) {
|
async function openCharacterChat(file_name) {
|
||||||
|
@ -55,6 +55,7 @@ const extension_settings = {
|
|||||||
sd: {},
|
sd: {},
|
||||||
chromadb: {},
|
chromadb: {},
|
||||||
translate: {},
|
translate: {},
|
||||||
|
objective: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
let modules = [];
|
let modules = [];
|
||||||
|
351
public/scripts/extensions/objective/index.js
Normal file
351
public/scripts/extensions/objective/index.js
Normal file
@ -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 = `
|
||||||
|
<div id="objective-task-label-${taskIndex}" class="flex1 checkbox_label">
|
||||||
|
<span>${taskIndex}</span>
|
||||||
|
<input id="objective-task-complete-${taskIndex}" type="checkbox">
|
||||||
|
<span class="text_pole" style="display: block" id="objective-task-description-${taskIndex}" contenteditable>${taskDescription}</span>
|
||||||
|
</div><br>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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 = `
|
||||||
|
<div class="objective-settings">
|
||||||
|
<div class="inline-drawer">
|
||||||
|
<div class="inline-drawer-toggle inline-drawer-header">
|
||||||
|
<b>Objective</b>
|
||||||
|
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||||
|
</div>
|
||||||
|
<div class="inline-drawer-content">
|
||||||
|
<label for="objective-text"><small>Enter an objective and generate tasks. The AI will attempt to complete tasks autonomously</small></label>
|
||||||
|
<textarea id="objective-text" type="text" class="text_pole textarea_compact" rows="4"></textarea>
|
||||||
|
<label class="checkbox_label"><input id="objective-generate" class="menu_button" type="submit" value="Generate Tasks" />
|
||||||
|
<small>Automatically generate tasks for Objective. Takes a moment.</small></label></br>
|
||||||
|
<label class="checkbox_label"><input id="objective-hide-tasks" type="checkbox"> Hide Tasks</label><br>
|
||||||
|
<div id="objective-tasks"> </div>
|
||||||
|
<label for="objective-chat-depth">In-chat @ Depth</label>
|
||||||
|
<input id="objective-chat-depth" class="text_pole widthUnset" type="number" min="0" max="99" /><br>
|
||||||
|
<label for="objective-check-frequency">Task Check Frequency</label>
|
||||||
|
<input id="objective-check-frequency" class="text_pole widthUnset" type="number" min="" max="99" /><small> (0 = disabled) </small><br>
|
||||||
|
<span> Messages until next AI task completion check <span id="objective-counter">0</span></span>
|
||||||
|
<hr class="sysHR">
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
$('#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)
|
||||||
|
});
|
||||||
|
});
|
11
public/scripts/extensions/objective/manifest.json
Normal file
11
public/scripts/extensions/objective/manifest.json
Normal file
@ -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": ""
|
||||||
|
}
|
4
public/scripts/extensions/objective/style.css
Normal file
4
public/scripts/extensions/objective/style.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#objective-counter {
|
||||||
|
font-weight: 600;
|
||||||
|
color: orange;
|
||||||
|
}
|
@ -295,3 +295,4 @@ GNU Affero General Public License for more details.**
|
|||||||
* AI Horde client library by ZeldaFan0225: https://github.com/ZeldaFan0225/ai_horde
|
* AI Horde client library by ZeldaFan0225: https://github.com/ZeldaFan0225/ai_horde
|
||||||
* Linux startup script by AlpinDale
|
* Linux startup script by AlpinDale
|
||||||
* Thanks paniphons for providing a FAQ document
|
* Thanks paniphons for providing a FAQ document
|
||||||
|
* TTS and Objective extensions by Ouoertheo
|
Reference in New Issue
Block a user