SillyTavern/public/scripts/tool-calling.js

946 lines
36 KiB
JavaScript
Raw Normal View History

import { addOneMessage, chat, event_types, eventSource, main_api, saveChatConditional, system_avatar, systemUserName } from '../script.js';
2024-10-02 00:00:48 +02:00
import { chat_completion_sources, oai_settings } from './openai.js';
2024-10-03 23:11:36 +02:00
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';
2024-10-02 00:00:48 +02:00
/**
* @typedef {object} ToolInvocation
* @property {string} id - A unique identifier for the tool invocation.
2024-10-03 23:39:28 +02:00
* @property {string} displayName - The display name of the tool.
2024-10-02 00:00:48 +02:00
* @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.
*/
2024-10-03 23:11:36 +02:00
/**
* @typedef {object} ToolInvocationResult
* @property {ToolInvocation[]} invocations Successful tool invocations
* @property {Error[]} errors Errors that occurred during tool invocation
*/
2024-10-04 12:34:17 +02:00
/**
* @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.
2024-10-09 02:56:24 +02:00
* @property {function} [formatMessage] - A function to format the tool call message.
* @property {function} [shouldRegister] - A function to determine if the tool should be registered.
2024-10-04 12:34:17 +02:00
*/
/**
* @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);
}
2024-10-02 00:00:48 +02:00
/**
* A class that represents a tool definition.
*/
class ToolDefinition {
/**
* A unique name for the tool.
* @type {string}
*/
#name;
2024-10-03 23:39:28 +02:00
/**
* A user-friendly display name for the tool.
* @type {string}
*/
#displayName;
2024-10-02 00:00:48 +02:00
/**
* 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;
2024-10-03 23:39:28 +02:00
/**
* 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;
2024-10-02 00:00:48 +02:00
/**
* Creates a new ToolDefinition.
* @param {string} name A unique name for the tool.
2024-10-03 23:39:28 +02:00
* @param {string} displayName A user-friendly display name for the tool.
2024-10-02 00:00:48 +02:00
* @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.
2024-10-03 23:39:28 +02:00
* @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.
2024-10-02 00:00:48 +02:00
*/
constructor(name, displayName, description, parameters, action, formatMessage, shouldRegister) {
2024-10-02 00:00:48 +02:00
this.#name = name;
2024-10-03 23:39:28 +02:00
this.#displayName = displayName;
2024-10-02 00:00:48 +02:00
this.#description = description;
this.#parameters = parameters;
this.#action = action;
2024-10-03 23:39:28 +02:00
this.#formatMessage = formatMessage;
this.#shouldRegister = shouldRegister;
2024-10-02 00:00:48 +02:00
}
/**
* Converts the ToolDefinition to an OpenAI API representation
* @returns {ToolDefinitionOpenAI} OpenAI API representation of the tool.
2024-10-02 00:00:48 +02:00
*/
toFunctionOpenAI() {
return {
type: 'function',
function: {
name: this.#name,
description: this.#description,
parameters: this.#parameters,
},
2024-10-06 19:19:58 +02:00
toString: function () {
2024-10-06 12:01:14 +02:00
return `<div><b>${this.function.name}</b></div><div><small>${this.function.description}</small></div><pre class="justifyLeft wordBreakAll"><code class="flex padding5">${JSON.stringify(this.function.parameters, null, 2)}</code></pre><hr>`;
},
2024-10-02 00:00:48 +02:00
};
}
/**
* Invokes the tool with the given parameters.
* @param {object} parameters The parameters to pass to the tool.
* @returns {Promise<any>} The result of the tool's action function.
*/
async invoke(parameters) {
return await this.#action(parameters);
}
2024-10-03 23:39:28 +02:00
/**
* Formats a message with the tool invocation.
* @param {object} parameters The parameters to pass to the tool.
2024-10-09 02:56:24 +02:00
* @returns {Promise<string>} The formatted message.
2024-10-03 23:39:28 +02:00
*/
2024-10-09 02:56:24 +02:00
async formatMessage(parameters) {
2024-10-03 23:39:28 +02:00
return typeof this.#formatMessage === 'function'
2024-10-09 02:56:24 +02:00
? await this.#formatMessage(parameters)
2024-10-03 23:39:28 +02:00
: `Invoking tool: ${this.#displayName || this.#name}`;
}
async shouldRegister() {
return typeof this.#shouldRegister === 'function'
? await this.#shouldRegister()
: true;
}
2024-10-03 23:39:28 +02:00
get displayName() {
return this.#displayName;
}
2024-10-02 00:00:48 +02:00
}
/**
* A class that manages the registration and invocation of tools.
*/
export class ToolManager {
/**
* A map of tool names to tool definitions.
* @type {Map<string, ToolDefinition>}
*/
static #tools = new Map();
2024-10-04 13:31:15 +02:00
static #INPUT_DELTA_KEY = '__input_json_delta';
2024-10-06 23:22:27 +02:00
/**
* The maximum number of times to recurse when parsing tool calls.
* @type {number}
*/
static RECURSE_LIMIT = 5;
2024-10-02 00:00:48 +02:00
/**
* Returns an Array of all tools that have been registered.
* @type {ToolDefinition[]}
*/
static get tools() {
return Array.from(this.#tools.values());
}
/**
* Registers a new tool with the tool registry.
2024-10-04 12:34:17 +02:00
* @param {ToolRegistration} tool The tool to register.
2024-10-02 00:00:48 +02:00
*/
static registerFunctionTool({ name, displayName, description, parameters, action, formatMessage, shouldRegister }) {
2024-10-03 23:39:28 +02:00
// Convert WIP arguments
if (typeof arguments[0] !== 'object') {
[name, description, parameters, action] = arguments;
}
2024-10-02 00:00:48 +02:00
if (this.#tools.has(name)) {
console.warn(`[ToolManager] A tool with the name "${name}" has already been registered. The definition will be overwritten.`);
2024-10-02 00:00:48 +02:00
}
const definition = new ToolDefinition(name, displayName, description, parameters, action, formatMessage, shouldRegister);
2024-10-02 00:00:48 +02:00
this.#tools.set(name, definition);
2024-10-02 22:13:11 +02:00
console.log('[ToolManager] Registered function tool:', definition);
2024-10-02 00:00:48 +02:00
}
/**
* Removes a tool from the tool registry.
* @param {string} name The name of the tool to unregister.
*/
static unregisterFunctionTool(name) {
if (!this.#tools.has(name)) {
return;
}
this.#tools.delete(name);
2024-10-02 22:13:11 +02:00
console.log(`[ToolManager] Unregistered function tool: ${name}`);
2024-10-02 00:00:48 +02:00
}
/**
* Invokes a tool by name. Returns the result of the tool's action function.
* @param {string} name The name of the tool to invoke.
* @param {object} parameters Function parameters. For example, if the tool requires a "name" parameter, you would pass {name: "value"}.
2024-10-03 23:11:36 +02:00
* @returns {Promise<string|Error>} The result of the tool's action function. If an error occurs, null is returned. Non-string results are JSON-stringified.
2024-10-02 00:00:48 +02:00
*/
static async invokeFunctionTool(name, parameters) {
try {
if (!this.#tools.has(name)) {
throw new Error(`No tool with the name "${name}" has been registered.`);
}
const invokeParameters = typeof parameters === 'string' ? JSON.parse(parameters) : parameters;
const tool = this.#tools.get(name);
const result = await tool.invoke(invokeParameters);
return typeof result === 'string' ? result : JSON.stringify(result);
} catch (error) {
console.error(`[ToolManager] An error occurred while invoking the tool "${name}":`, error);
2024-10-03 23:11:36 +02:00
if (error instanceof Error) {
error.cause = name;
return error.toString();
2024-10-03 23:11:36 +02:00
}
return new Error('Unknown error occurred while invoking the tool.', { cause: name }).toString();
2024-10-02 00:00:48 +02:00
}
}
2024-10-04 12:34:17 +02:00
/**
* Formats a message for a tool call by name.
* @param {string} name The name of the tool to format the message for.
* @param {object} parameters Function tool call parameters.
2024-10-09 02:56:24 +02:00
* @returns {Promise<string>} The formatted message for the tool call.
2024-10-04 12:34:17 +02:00
*/
2024-10-09 02:56:24 +02:00
static async formatToolCallMessage(name, parameters) {
2024-10-03 23:39:28 +02:00
if (!this.#tools.has(name)) {
return `Invoked unknown tool: ${name}`;
}
try {
const tool = this.#tools.get(name);
const formatParameters = typeof parameters === 'string' ? JSON.parse(parameters) : parameters;
2024-10-09 02:56:24 +02:00
return await tool.formatMessage(formatParameters);
2024-10-03 23:39:28 +02:00
} catch (error) {
console.error(`[ToolManager] An error occurred while formatting the tool call message for "${name}":`, error);
2024-10-03 23:39:28 +02:00
return `Invoking tool: ${name}`;
}
}
/**
* Gets the display name of a tool by name.
* @param {string} name
* @returns {string} The display name of the tool.
*/
static getDisplayName(name) {
if (!this.#tools.has(name)) {
return name;
}
const tool = this.#tools.get(name);
return tool.displayName || name;
}
2024-10-02 00:00:48 +02:00
/**
* Register function tools for the next chat completion request.
* @param {object} data Generation data
*/
static async registerFunctionToolsOpenAI(data) {
const tools = [];
for (const tool of ToolManager.tools) {
const register = await tool.shouldRegister();
if (!register) {
console.log('[ToolManager] Skipping tool registration:', tool);
continue;
}
2024-10-02 00:00:48 +02:00
tools.push(tool.toFunctionOpenAI());
}
if (tools.length) {
console.log('[ToolManager] Registered function tools:', tools);
2024-10-02 00:00:48 +02:00
data['tools'] = tools;
data['tool_choice'] = 'auto';
}
}
/**
* Utility function to parse tool calls from a parsed response.
* @param {any[]} toolCalls The tool calls to update.
* @param {any} parsed The parsed response from the OpenAI API.
* @returns {void}
*/
static parseToolCalls(toolCalls, parsed) {
if (!this.isToolCallingSupported()) {
return;
}
2024-10-04 12:39:08 +02:00
if (Array.isArray(parsed?.choices)) {
for (const choice of parsed.choices) {
const choiceIndex = (typeof choice.index === 'number') ? choice.index : null;
const choiceDelta = choice.delta;
2024-10-02 00:00:48 +02:00
2024-10-04 12:39:08 +02:00
if (choiceIndex === null || !choiceDelta) {
continue;
}
2024-10-02 00:00:48 +02:00
2024-10-04 12:39:08 +02:00
const toolCallDeltas = choiceDelta?.tool_calls;
2024-10-02 00:00:48 +02:00
2024-10-04 12:39:08 +02:00
if (!Array.isArray(toolCallDeltas)) {
continue;
}
2024-10-02 00:00:48 +02:00
2024-10-04 12:39:08 +02:00
if (!Array.isArray(toolCalls[choiceIndex])) {
toolCalls[choiceIndex] = [];
}
2024-10-02 00:00:48 +02:00
2024-10-04 12:39:08 +02:00
for (const toolCallDelta of toolCallDeltas) {
const toolCallIndex = (typeof toolCallDelta?.index === 'number') ? toolCallDelta.index : toolCallDeltas.indexOf(toolCallDelta);
2024-10-02 00:00:48 +02:00
2024-10-04 12:39:08 +02:00
if (isNaN(toolCallIndex) || toolCallIndex < 0) {
continue;
}
if (toolCalls[choiceIndex][toolCallIndex] === undefined) {
toolCalls[choiceIndex][toolCallIndex] = {};
}
const targetToolCall = toolCalls[choiceIndex][toolCallIndex];
ToolManager.#applyToolCallDelta(targetToolCall, toolCallDelta);
}
}
}
const cohereToolEvents = ['message-start', 'tool-call-start', 'tool-call-delta', 'tool-call-end'];
if (cohereToolEvents.includes(parsed?.type) && typeof parsed?.delta?.message === 'object') {
const choiceIndex = 0;
const toolCallIndex = parsed?.index ?? 0;
if (!Array.isArray(toolCalls[choiceIndex])) {
toolCalls[choiceIndex] = [];
}
if (toolCalls[choiceIndex][toolCallIndex] === undefined) {
toolCalls[choiceIndex][toolCallIndex] = {};
}
const targetToolCall = toolCalls[choiceIndex][toolCallIndex];
ToolManager.#applyToolCallDelta(targetToolCall, parsed.delta.message);
}
2024-10-04 12:39:08 +02:00
if (typeof parsed?.content_block === 'object') {
const choiceIndex = 0;
2024-10-04 13:31:15 +02:00
const toolCallIndex = parsed?.index ?? 0;
2024-10-04 12:39:08 +02:00
if (parsed?.content_block?.type === 'tool_use') {
if (!Array.isArray(toolCalls[choiceIndex])) {
toolCalls[choiceIndex] = [];
2024-10-02 00:00:48 +02:00
}
if (toolCalls[choiceIndex][toolCallIndex] === undefined) {
toolCalls[choiceIndex][toolCallIndex] = {};
}
const targetToolCall = toolCalls[choiceIndex][toolCallIndex];
2024-10-04 13:31:15 +02:00
ToolManager.#applyToolCallDelta(targetToolCall, parsed.content_block);
}
}
if (typeof parsed?.delta === 'object') {
const choiceIndex = 0;
const toolCallIndex = parsed?.index ?? 0;
const targetToolCall = toolCalls[choiceIndex]?.[toolCallIndex];
2024-10-04 22:13:56 +02:00
if (targetToolCall) {
2024-10-04 13:31:15 +02:00
if (parsed?.delta?.type === 'input_json_delta') {
const jsonDelta = parsed?.delta?.partial_json;
if (!targetToolCall[this.#INPUT_DELTA_KEY]) {
targetToolCall[this.#INPUT_DELTA_KEY] = '';
}
targetToolCall[this.#INPUT_DELTA_KEY] += jsonDelta;
}
}
}
if (parsed?.type === 'content_block_stop') {
const choiceIndex = 0;
const toolCallIndex = parsed?.index ?? 0;
const targetToolCall = toolCalls[choiceIndex]?.[toolCallIndex];
if (targetToolCall) {
const jsonDeltaString = targetToolCall[this.#INPUT_DELTA_KEY];
if (jsonDeltaString) {
try {
const jsonDelta = { input: JSON.parse(jsonDeltaString) };
delete targetToolCall[this.#INPUT_DELTA_KEY];
ToolManager.#applyToolCallDelta(targetToolCall, jsonDelta);
} catch (error) {
console.warn('[ToolManager] Failed to apply input JSON delta:', error);
2024-10-04 13:31:15 +02:00
}
}
2024-10-02 00:00:48 +02:00
}
}
}
2024-10-04 12:34:17 +02:00
/**
* Apply a tool call delta to a target object.
* @param {object} target The target object to apply the delta to
* @param {object} delta The delta object to apply
*/
2024-10-02 00:00:48 +02:00
static #applyToolCallDelta(target, delta) {
for (const key in delta) {
2024-10-04 12:34:17 +02:00
if (!Object.prototype.hasOwnProperty.call(delta, key)) continue;
2024-10-02 00:54:47 +02:00
if (key === '__proto__' || key === 'constructor') continue;
2024-10-02 00:00:48 +02:00
const deltaValue = delta[key];
const targetValue = target[key];
if (deltaValue === null || deltaValue === undefined) {
target[key] = deltaValue;
continue;
}
if (typeof deltaValue === 'string') {
if (typeof targetValue === 'string') {
// Concatenate strings
target[key] = targetValue + deltaValue;
} else {
target[key] = deltaValue;
}
} else if (typeof deltaValue === 'object' && !Array.isArray(deltaValue)) {
if (typeof targetValue !== 'object' || targetValue === null || Array.isArray(targetValue)) {
target[key] = {};
}
// Recursively apply deltas to nested objects
ToolManager.#applyToolCallDelta(target[key], deltaValue);
} else {
// Assign other types directly
target[key] = deltaValue;
}
}
}
2024-10-02 21:17:27 +02:00
/**
* Checks if tool calling is supported for the current settings and generation type.
* @returns {boolean} Whether tool calling is supported for the given type
*/
static isToolCallingSupported() {
if (main_api !== 'openai' || !oai_settings.function_calling) {
2024-10-02 00:00:48 +02:00
return false;
}
const supportedSources = [
chat_completion_sources.OPENAI,
chat_completion_sources.CUSTOM,
chat_completion_sources.MISTRALAI,
chat_completion_sources.CLAUDE,
2024-10-02 00:00:48 +02:00
chat_completion_sources.OPENROUTER,
chat_completion_sources.GROQ,
chat_completion_sources.COHERE,
2024-10-02 00:00:48 +02:00
];
return supportedSources.includes(oai_settings.chat_completion_source);
}
2024-10-02 21:17:27 +02:00
/**
* Checks if tool calls can be performed for the current settings and generation type.
* @param {string} type Generation type
* @returns {boolean} Whether tool calls can be performed for the given type
*/
static canPerformToolCalls(type) {
2024-10-06 21:25:23 +02:00
const noToolCallTypes = ['impersonate', 'quiet', 'continue'];
2024-10-02 21:17:27 +02:00
const isSupported = ToolManager.isToolCallingSupported();
return isSupported && !noToolCallTypes.includes(type);
}
/**
* Utility function to get tool calls from the response data.
* @param {any} data Response data
* @returns {any[]} Tool calls from the response data
*/
2024-10-02 00:00:48 +02:00
static #getToolCallsFromData(data) {
2024-10-04 13:31:15 +02:00
const isClaudeToolCall = c => Array.isArray(c) ? c.filter(x => x).every(isClaudeToolCall) : c?.input && c?.name && c?.id;
const convertClaudeToolCall = c => ({ id: c.id, function: { name: c.name, arguments: c.input } });
2024-10-02 00:00:48 +02:00
// Parsed tool calls from streaming data
2024-10-04 13:31:15 +02:00
if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0])) {
if (isClaudeToolCall(data[0])) {
return data[0].filter(x => x).map(convertClaudeToolCall);
}
if (typeof data[0]?.[0]?.tool_calls === 'object') {
return Array.isArray(data[0]?.[0]?.tool_calls) ? data[0][0].tool_calls : [data[0][0].tool_calls];
}
return data[0];
2024-10-02 00:00:48 +02:00
}
// Parsed tool calls from non-streaming data
if (Array.isArray(data?.choices)) {
// Find a choice with 0-index
const choice = data.choices.find(choice => choice.index === 0);
if (choice) {
return choice.message.tool_calls;
}
2024-10-02 00:00:48 +02:00
}
2024-10-04 13:31:15 +02:00
// Claude tool calls to OpenAI tool calls
if (Array.isArray(data?.content)) {
2024-10-04 13:31:15 +02:00
const content = data.content.filter(c => c.type === 'tool_use').map(convertClaudeToolCall);
if (content) {
return content;
}
2024-10-02 00:00:48 +02:00
}
// Cohere tool calls
if (typeof data?.message?.tool_calls === 'object') {
return Array.isArray(data?.message?.tool_calls) ? data.message.tool_calls : [data.message.tool_calls];
}
2024-10-02 00:00:48 +02:00
}
2024-10-04 22:13:56 +02:00
/**
* Checks if the response data contains tool calls.
* @param {object} data Response data
* @returns {boolean} Whether the response data contains tool calls
*/
static hasToolCalls(data) {
2024-10-04 23:20:06 +02:00
const toolCalls = ToolManager.#getToolCallsFromData(data);
return Array.isArray(toolCalls) && toolCalls.length > 0;
2024-10-04 22:13:56 +02:00
}
2024-10-02 00:00:48 +02:00
/**
* Check for function tool calls in the response data and invoke them.
* @param {any} data Reply data
2024-10-03 23:11:36 +02:00
* @returns {Promise<ToolInvocationResult>} Successful tool invocations
2024-10-02 00:00:48 +02:00
*/
2024-10-02 21:17:27 +02:00
static async invokeFunctionTools(data) {
2024-10-03 23:11:36 +02:00
/** @type {ToolInvocationResult} */
const result = {
invocations: [],
errors: [],
};
2024-10-02 00:00:48 +02:00
const toolCalls = ToolManager.#getToolCallsFromData(data);
if (!Array.isArray(toolCalls)) {
return result;
2024-10-02 00:00:48 +02:00
}
for (const toolCall of toolCalls) {
if (typeof toolCall.function !== 'object') {
continue;
2024-10-02 00:00:48 +02:00
}
console.log('[ToolManager] Function tool call:', toolCall);
const id = toolCall.id;
const parameters = toolCall.function.arguments;
const name = toolCall.function.name;
const displayName = ToolManager.getDisplayName(name);
2024-10-09 02:56:24 +02:00
const message = await ToolManager.formatToolCallMessage(name, parameters);
const toast = message && toastr.info(message, 'Tool Calling', { timeOut: 0 });
const toolResult = await ToolManager.invokeFunctionTool(name, parameters);
toastr.clear(toast);
console.log('[ToolManager] Function tool result:', result);
// Save a successful invocation
if (toolResult instanceof Error) {
result.errors.push(toolResult);
continue;
2024-10-02 00:00:48 +02:00
}
const invocation = {
id,
displayName,
name,
parameters: stringify(parameters),
result: toolResult,
};
result.invocations.push(invocation);
2024-10-02 00:00:48 +02:00
}
2024-10-03 23:11:36 +02:00
return result;
2024-10-02 00:00:48 +02:00
}
2024-10-06 19:19:58 +02:00
/**
* Groups tool names by count.
* @param {string[]} toolNames Tool names
* @returns {string} Grouped tool names
*/
static #groupToolNames(toolNames) {
const toolCounts = toolNames.reduce((acc, name) => {
acc[name] = (acc[name] || 0) + 1;
return acc;
}, {});
return Object.entries(toolCounts).map(([name, count]) => count > 1 ? `${name} (${count})` : name).join(', ');
}
2024-10-02 22:13:11 +02:00
/**
* Formats a message with tool invocations.
* @param {ToolInvocation[]} invocations Tool invocations.
* @returns {string} Formatted message with tool invocations.
*/
2024-10-04 12:34:17 +02:00
static #formatToolInvocationMessage(invocations) {
2024-10-02 22:32:29 +02:00
const data = structuredClone(invocations);
2024-10-02 22:13:11 +02:00
const detailsElement = document.createElement('details');
const summaryElement = document.createElement('summary');
const preElement = document.createElement('pre');
const codeElement = document.createElement('code');
2024-10-02 22:32:29 +02:00
codeElement.classList.add('language-json');
2024-10-05 19:54:37 +02:00
data.forEach(i => {
i.parameters = tryParse(i.parameters);
i.result = tryParse(i.result);
});
2024-10-02 22:32:29 +02:00
codeElement.textContent = JSON.stringify(data, null, 2);
2024-10-06 19:19:58 +02:00
const toolNames = data.map(i => i.displayName || i.name);
summaryElement.textContent = `Tool calls: ${this.#groupToolNames(toolNames)}`;
2024-10-02 22:13:11 +02:00
preElement.append(codeElement);
detailsElement.append(summaryElement, preElement);
return detailsElement.outerHTML;
}
2024-10-02 00:00:48 +02:00
/**
* Saves function tool invocations to the last user chat message extra metadata.
* @param {ToolInvocation[]} invocations Successful tool invocations
2024-10-02 00:00:48 +02:00
*/
static async saveFunctionToolInvocations(invocations) {
if (!Array.isArray(invocations) || invocations.length === 0) {
return;
}
2024-10-02 21:17:27 +02:00
const message = {
name: systemUserName,
force_avatar: system_avatar,
is_system: true,
is_user: false,
2024-10-04 12:34:17 +02:00
mes: ToolManager.#formatToolInvocationMessage(invocations),
2024-10-02 21:17:27 +02:00
extra: {
isSmallSys: true,
tool_invocations: invocations,
},
};
chat.push(message);
await eventSource.emit(event_types.TOOL_CALLS_PERFORMED, invocations);
2024-10-02 21:17:27 +02:00
addOneMessage(message);
await eventSource.emit(event_types.TOOL_CALLS_RENDERED, invocations);
await saveChatConditional();
2024-10-02 00:00:48 +02:00
}
2024-10-03 23:11:36 +02:00
/**
* Shows an error message for tool calls.
* @param {Error[]} errors Errors that occurred during tool invocation
* @returns {void}
*/
static showToolCallError(errors) {
toastr.error('An error occurred while invoking function tools. Click here for more details.', 'Tool Calling', {
onclick: () => Popup.show.text('Tool Calling Errors', DOMPurify.sanitize(errors.map(e => `${e.cause}: ${e.message}`).join('<br>'))),
timeOut: 5000,
});
}
static initToolSlashCommands() {
const toolsEnumProvider = () => ToolManager.tools.map(tool => {
const toolOpenAI = tool.toFunctionOpenAI();
return new SlashCommandEnumValue(toolOpenAI.function.name, toolOpenAI.function.description, enumTypes.enum, enumIcons.closure);
});
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'tools-list',
aliases: ['tool-list'],
helpString: 'Gets a list of all registered tools in the OpenAI function JSON format. Use the <code>return</code> 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();
2024-10-06 12:01:14 +02:00
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 <code>parameters</code> 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: `<div>Registers a new tool with the tool registry.</div>
<ul>
<li>The <code>parameters</code> argument MUST be a JSON-serialized object with a valid JSON schema.</li>
<li>The unnamed argument MUST be a closure that accepts the function parameters as local script variables.</li>
</ul>
<div>See <a target="_blank" href="https://json-schema.org/learn/">json-schema.org</a> and <a target="_blank" href="https://platform.openai.com/docs/guides/function-calling">OpenAI Function Calling</a> for more information.</div>
<div>Example:</div>
<pre><code>/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}} :}</code></pre>`,
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,
}),
],
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
* @returns {function} Function that executes the closure
*/
function closureToFunction(action) {
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 result.pipe;
};
}
const { name, displayName, description, parameters, formatMessage } = 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.');
}
const actionFunc = closureToFunction(action);
const formatMessageFunc = formatMessage instanceof SlashCommandClosure ? closureToFunction(formatMessage) : null;
ToolManager.registerFunctionTool({
name: String(name ?? ''),
displayName: String(displayName ?? ''),
description: String(description ?? ''),
parameters: JSON.parse(parameters ?? '{}'),
action: actionFunc,
formatMessage: formatMessageFunc,
2024-10-09 02:56:24 +02:00
shouldRegister: async () => true, // TODO: Implement shouldRegister
});
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 '';
},
}));
}
2024-10-02 00:00:48 +02:00
}