import { DOMPurify } from '../lib.js'; import { addOneMessage, chat, event_types, eventSource, main_api, saveChatConditional, system_avatar, systemUserName } from '../script.js'; import { chat_completion_sources, oai_settings } from './openai.js'; import { Popup } from './popup.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js'; import { isTrueBoolean } from './utils.js'; /** * @typedef {object} ToolInvocation * @property {string} id - A unique identifier for the tool invocation. * @property {string} displayName - The display name of the tool. * @property {string} name - The name of the tool. * @property {string} parameters - The parameters for the tool invocation. * @property {string} result - The result of the tool invocation. */ /** * @typedef {object} ToolInvocationResult * @property {ToolInvocation[]} invocations Successful tool invocations * @property {Error[]} errors Errors that occurred during tool invocation */ /** * @typedef {object} ToolRegistration * @property {string} name - The name of the tool. * @property {string} displayName - The display name of the tool. * @property {string} description - A description of the tool. * @property {object} parameters - The parameters for the tool. * @property {function} action - The action to perform when the tool is invoked. * @property {function} [formatMessage] - A function to format the tool call message. * @property {function} [shouldRegister] - A function to determine if the tool should be registered. */ /** * @typedef {object} ToolDefinitionOpenAI * @property {string} type - The type of the tool. * @property {object} function - The function definition. * @property {string} function.name - The name of the function. * @property {string} function.description - The description of the function. * @property {object} function.parameters - The parameters of the function. * @property {function} toString - A function to convert the tool to a string. */ /** * Assigns nested variables to a scope. * @param {import('./slash-commands/SlashCommandScope.js').SlashCommandScope} scope The scope to assign variables to. * @param {object} arg Object to assign variables from. * @param {string} prefix Prefix for the variable names. */ function assignNestedVariables(scope, arg, prefix) { Object.entries(arg).forEach(([key, value]) => { const newPrefix = `${prefix}.${key}`; if (typeof value === 'object' && value !== null) { assignNestedVariables(scope, value, newPrefix); } else { scope.letVariable(newPrefix, value); } }); } /** * Checks if a string is a valid JSON string. * @param {string} str The string to check * @returns {boolean} If the string is a valid JSON string */ function isJson(str) { try { JSON.parse(str); return true; } catch { return false; } } /** * Tries to parse a string as JSON, returning the original string if parsing fails. * @param {string} str The string to try to parse * @returns {object|string} Parsed JSON or the original string */ function tryParse(str) { try { return JSON.parse(str); } catch { return str; } } /** * Stringifies an object if it is not already a string. * @param {any} obj The object to stringify * @returns {string} A JSON string representation of the object. */ function stringify(obj) { return typeof obj === 'string' ? obj : JSON.stringify(obj); } /** * A class that represents a tool definition. */ class ToolDefinition { /** * A unique name for the tool. * @type {string} */ #name; /** * A user-friendly display name for the tool. * @type {string} */ #displayName; /** * A description of what the tool does. * @type {string} */ #description; /** * A JSON schema for the parameters that the tool accepts. * @type {object} */ #parameters; /** * A function that will be called when the tool is executed. * @type {function} */ #action; /** * A function that will be called to format the tool call toast. * @type {function} */ #formatMessage; /** * A function that will be called to determine if the tool should be registered. * @type {function} */ #shouldRegister; /** * Creates a new ToolDefinition. * @param {string} name A unique name for the tool. * @param {string} displayName A user-friendly display name for the tool. * @param {string} description A description of what the tool does. * @param {object} parameters A JSON schema for the parameters that the tool accepts. * @param {function} action A function that will be called when the tool is executed. * @param {function} formatMessage A function that will be called to format the tool call toast. * @param {function} shouldRegister A function that will be called to determine if the tool should be registered. */ constructor(name, displayName, description, parameters, action, formatMessage, shouldRegister) { this.#name = name; this.#displayName = displayName; this.#description = description; this.#parameters = parameters; this.#action = action; this.#formatMessage = formatMessage; this.#shouldRegister = shouldRegister; } /** * Converts the ToolDefinition to an OpenAI API representation * @returns {ToolDefinitionOpenAI} OpenAI API representation of the tool. */ toFunctionOpenAI() { return { type: 'function', function: { name: this.#name, description: this.#description, parameters: this.#parameters, }, toString: function () { return `
${JSON.stringify(this.function.parameters, null, 2)}
return
argument to specify the return value type.',
returns: 'A list of all registered tools.',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'The way how you want the return value to be provided',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'none',
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
forceEnum: true,
}),
],
callback: async (args) => {
/** @type {any} */
const returnType = String(args?.return ?? 'popup-html').trim().toLowerCase();
const objectToStringFunc = (tools) => Array.isArray(tools) ? tools.map(x => x.toString()).join('\n\n') : tools.toString();
const tools = ToolManager.tools.map(tool => tool.toFunctionOpenAI());
return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', tools ?? [], { objectToStringFunc });
},
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'tools-invoke',
aliases: ['tool-invoke'],
helpString: 'Invokes a registered tool by name. The parameters
argument MUST be a JSON-serialized object.',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'parameters',
description: 'The parameters to pass to the tool.',
typeList: [ARGUMENT_TYPE.DICTIONARY],
isRequired: true,
acceptsMultiple: false,
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'The name of the tool to invoke.',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
acceptsMultiple: false,
forceEnum: true,
enumProvider: toolsEnumProvider,
}),
],
callback: async (args, name) => {
const { parameters } = args;
const result = await ToolManager.invokeFunctionTool(String(name), parameters);
if (result instanceof Error) {
throw result;
}
return result;
},
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'tools-register',
aliases: ['tool-register'],
helpString: `parameters
argument MUST be a JSON-serialized object with a valid JSON schema./let key=echoSchema
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "The message to echo."
}
},
"required": [
"message"
]
}
||
/tools-register name=Echo description="Echoes a message. Call when the user is asking to repeat something" parameters={{var::echoSchema}} {: /echo {{var::arg.message}} :}
`,
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
description: 'The name of the tool.',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'description',
description: 'A description of what the tool does.',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'parameters',
description: 'The parameters for the tool.',
typeList: [ARGUMENT_TYPE.DICTIONARY],
isRequired: true,
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'displayName',
description: 'The display name of the tool.',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: false,
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'formatMessage',
description: 'The closure to be executed to format the tool call message. Must return a string.',
typeList: [ARGUMENT_TYPE.CLOSURE],
isRequired: true,
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'shouldRegister',
description: 'The closure to be executed to determine if the tool should be registered. Must return a boolean.',
typeList: [ARGUMENT_TYPE.CLOSURE],
isRequired: false,
acceptsMultiple: false,
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'The closure to be executed when the tool is invoked.',
typeList: [ARGUMENT_TYPE.CLOSURE],
isRequired: true,
acceptsMultiple: false,
}),
],
callback: async (args, action) => {
/**
* Converts a slash command closure to a function.
* @param {SlashCommandClosure} action Closure to convert to a function
* @param {function(any): any} convertResult Function to convert the result
* @returns {function} Function that executes the closure
*/
function closureToFunction(action, convertResult) {
return async (args) => {
const localClosure = action.getCopy();
localClosure.onProgress = () => { };
const scope = localClosure.scope;
if (typeof args === 'object' && args !== null) {
assignNestedVariables(scope, args, 'arg');
} else if (typeof args !== 'undefined') {
scope.letVariable('arg', args);
}
const result = await localClosure.execute();
return convertResult(result.pipe);
};
}
const { name, displayName, description, parameters, formatMessage, shouldRegister } = args;
if (!(action instanceof SlashCommandClosure)) {
throw new Error('The unnamed argument must be a closure.');
}
if (typeof name !== 'string' || !name) {
throw new Error('The "name" argument must be a non-empty string.');
}
if (typeof description !== 'string' || !description) {
throw new Error('The "description" argument must be a non-empty string.');
}
if (typeof parameters !== 'string' || !isJson(parameters)) {
throw new Error('The "parameters" argument must be a JSON-serialized object.');
}
if (displayName && typeof displayName !== 'string') {
throw new Error('The "displayName" argument must be a string.');
}
if (formatMessage && !(formatMessage instanceof SlashCommandClosure)) {
throw new Error('The "formatMessage" argument must be a closure.');
}
if (shouldRegister && !(shouldRegister instanceof SlashCommandClosure)) {
throw new Error('The "shouldRegister" argument must be a closure.');
}
const actionFunc = closureToFunction(action, x => x);
const formatMessageFunc = formatMessage instanceof SlashCommandClosure ? closureToFunction(formatMessage, x => String(x)) : null;
const shouldRegisterFunc = shouldRegister instanceof SlashCommandClosure ? closureToFunction(shouldRegister, x => isTrueBoolean(x)) : null;
ToolManager.registerFunctionTool({
name: String(name ?? ''),
displayName: String(displayName ?? ''),
description: String(description ?? ''),
parameters: JSON.parse(parameters ?? '{}'),
action: actionFunc,
formatMessage: formatMessageFunc,
shouldRegister: shouldRegisterFunc,
});
return '';
},
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'tools-unregister',
aliases: ['tool-unregister'],
helpString: 'Unregisters a tool from the tool registry.',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'The name of the tool to unregister.',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
acceptsMultiple: false,
forceEnum: true,
enumProvider: toolsEnumProvider,
}),
],
callback: async (name) => {
if (typeof name !== 'string' || !name) {
throw new Error('The unnamed argument must be a non-empty string.');
}
ToolManager.unregisterFunctionTool(name);
return '';
},
}));
}
}