New tool calling framework

8006795897c54180823323357ad5a74c8660fe25

Cohee <18619528+Cohee1207@users.noreply.github.com>

6 files changed, +427 -253Ignore whitespace
public/global.d.ts+0 -41
@@ -1365,44 +1365,3 @@ declare namespace moment {
1365declare global {1365declare global {
1366 const moment: typeof moment;1366 const moment: typeof moment;
1367}1367}
1368
1369/**
1370 * Callback data for the `LLM_FUNCTION_TOOL_REGISTER` event type that is triggered when a function tool can be registered.
1371 */
1372interface FunctionToolRegister {
1373 /**
1374 * The type of generation that is being used
1375 */
1376 type?: string;
1377 /**
1378 * Generation data, including messages and sampling parameters
1379 */
1380 data: Record<string, object>;
1381 /**
1382 * Callback to register an LLM function tool.
1383 */
1384 registerFunctionTool: typeof registerFunctionTool;
1385}
1386
1387/**
1388 * Callback data for the `LLM_FUNCTION_TOOL_REGISTER` event type that is triggered when a function tool is registered.
1389 * @param name Name of the function tool to register
1390 * @param description Description of the function tool
1391 * @param params JSON schema for the parameters of the function tool
1392 * @param required Whether the function tool should be forced to be used
1393 */
1394declare function registerFunctionTool(name: string, description: string, params: object, required: boolean): Promise<void>;
1395
1396/**
1397 * Callback data for the `LLM_FUNCTION_TOOL_CALL` event type that is triggered when a function tool is called.
1398 */
1399interface FunctionToolCall {
1400 /**
1401 * Name of the function tool to call
1402 */
1403 name: string;
1404 /**
1405 * JSON object with the parameters to pass to the function tool
1406 */
1407 arguments: string;
1408}
public/script.js+30 -5
@@ -246,6 +246,7 @@ import { initInputMarkdown } from './scripts/input-md-formatting.js';
246import { AbortReason } from './scripts/util/AbortReason.js';246import { AbortReason } from './scripts/util/AbortReason.js';
247import { initSystemPrompts } from './scripts/sysprompt.js';247import { initSystemPrompts } from './scripts/sysprompt.js';
248import { registerExtensionSlashCommands as initExtensionSlashCommands } from './scripts/extensions-slashcommands.js';248import { registerExtensionSlashCommands as initExtensionSlashCommands } from './scripts/extensions-slashcommands.js';
249import { ToolManager } from './scripts/tool-calling.js';
249250
250//exporting functions and vars for mods251//exporting functions and vars for mods
251export {252export {
@@ -463,8 +464,6 @@ export const event_types = {
463 FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',464 FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',
464 WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',465 WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',
465 OPEN_CHARACTER_LIBRARY: 'open_character_library',466 OPEN_CHARACTER_LIBRARY: 'open_character_library',
466 LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register',
467 LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call',
468 ONLINE_STATUS_CHANGED: 'online_status_changed',467 ONLINE_STATUS_CHANGED: 'online_status_changed',
469 IMAGE_SWIPED: 'image_swiped',468 IMAGE_SWIPED: 'image_swiped',
470 CONNECTION_PROFILE_LOADED: 'connection_profile_loaded',469 CONNECTION_PROFILE_LOADED: 'connection_profile_loaded',
@@ -2921,6 +2920,7 @@ class StreamingProcessor {
2921 this.swipes = [];2920 this.swipes = [];
2922 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */2921 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
2923 this.messageLogprobs = [];2922 this.messageLogprobs = [];
2923 this.toolCalls = [];
2924 }2924 }
29252925
2926 #checkDomElements(messageId) {2926 #checkDomElements(messageId) {
@@ -3139,7 +3139,7 @@ class StreamingProcessor {
3139 }3139 }
31403140
3141 /**3141 /**
3142 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs }, void, void>}3142 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[] }, void, void>}
3143 */3143 */
3144 *nullStreamingGeneration() {3144 *nullStreamingGeneration() {
3145 throw new Error('Generation function for streaming is not hooked up');3145 throw new Error('Generation function for streaming is not hooked up');
@@ -3161,12 +3161,13 @@ class StreamingProcessor {
3161 try {3161 try {
3162 const sw = new Stopwatch(1000 / power_user.streaming_fps);3162 const sw = new Stopwatch(1000 / power_user.streaming_fps);
3163 const timestamps = [];3163 const timestamps = [];
3164 for await (const { text, swipes, logprobs } of this.generator()) {3164 for await (const { text, swipes, logprobs, toolCalls } of this.generator()) {
3165 timestamps.push(Date.now());3165 timestamps.push(Date.now());
3166 if (this.isStopped) {3166 if (this.isStopped) {
3167 return;3167 return;
3168 }3168 }
31693169
3170 this.toolCalls = toolCalls;
3170 this.result = text;3171 this.result = text;
3171 this.swipes = Array.from(swipes ?? []);3172 this.swipes = Array.from(swipes ?? []);
3172 if (logprobs) {3173 if (logprobs) {
@@ -4405,6 +4406,20 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4405 getMessage = continue_mag + getMessage;4406 getMessage = continue_mag + getMessage;
4406 }4407 }
44074408
4409 if (ToolManager.isFunctionCallingSupported() && Array.isArray(streamingProcessor.toolCalls) && streamingProcessor.toolCalls.length) {
4410 const invocations = await ToolManager.checkFunctionToolCalls(streamingProcessor.toolCalls);
4411 if (invocations.length) {
4412 const lastMessage = chat[chat.length - 1];
4413 const shouldDeleteMessage = ['', '...'].includes(lastMessage?.mes) && ['', '...'].includes(streamingProcessor.result);
4414 if (shouldDeleteMessage) {
4415 await deleteLastMessage();
4416 streamingProcessor = null;
4417 }
4418 ToolManager.saveFunctionToolInvocations(invocations);
4419 return Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName }, dryRun);
4420 }
4421 }
4422
4408 if (streamingProcessor && !streamingProcessor.isStopped && streamingProcessor.isFinished) {4423 if (streamingProcessor && !streamingProcessor.isStopped && streamingProcessor.isFinished) {
4409 await streamingProcessor.onFinishStreaming(streamingProcessor.messageId, getMessage);4424 await streamingProcessor.onFinishStreaming(streamingProcessor.messageId, getMessage);
4410 streamingProcessor = null;4425 streamingProcessor = null;
@@ -4440,6 +4455,14 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4440 throw new Error(data?.response);4455 throw new Error(data?.response);
4441 }4456 }
44424457
4458 if (ToolManager.isFunctionCallingSupported()) {
4459 const invocations = await ToolManager.checkFunctionToolCalls(data);
4460 if (invocations.length) {
4461 ToolManager.saveFunctionToolInvocations(invocations);
4462 return Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName }, dryRun);
4463 }
4464 }
4465
4443 //const getData = await response.json();4466 //const getData = await response.json();
4444 let getMessage = extractMessageFromData(data);4467 let getMessage = extractMessageFromData(data);
4445 let title = extractTitleFromData(data);4468 let title = extractTitleFromData(data);
@@ -7853,7 +7876,7 @@ function openAlternateGreetings() {
7853 if (menu_type !== 'create') {7876 if (menu_type !== 'create') {
7854 await createOrEditCharacter();7877 await createOrEditCharacter();
7855 }7878 }
7856 }7879 },
7857 });7880 });
78587881
7859 for (let index = 0; index < getArray().length; index++) {7882 for (let index = 0; index < getArray().length; index++) {
@@ -8130,6 +8153,8 @@ window['SillyTavern'].getContext = function () {
8130 registerHelper: () => { },8153 registerHelper: () => { },
8131 registerMacro: MacrosParser.registerMacro.bind(MacrosParser),8154 registerMacro: MacrosParser.registerMacro.bind(MacrosParser),
8132 unregisterMacro: MacrosParser.unregisterMacro.bind(MacrosParser),8155 unregisterMacro: MacrosParser.unregisterMacro.bind(MacrosParser),
8156 registerFunctionTool: ToolManager.registerFunctionTool.bind(ToolManager),
8157 unregisterFunctionTool: ToolManager.unregisterFunctionTool.bind(ToolManager),
8133 registerDebugFunction: registerDebugFunction,8158 registerDebugFunction: registerDebugFunction,
8134 /** @deprecated Use renderExtensionTemplateAsync instead. */8159 /** @deprecated Use renderExtensionTemplateAsync instead. */
8135 renderExtensionTemplate: renderExtensionTemplate,8160 renderExtensionTemplate: renderExtensionTemplate,
public/scripts/extensions/expressions/index.js+1 -51
@@ -9,7 +9,6 @@ import { debounce_timeout } from '../../constants.js';
9import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';9import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
10import { SlashCommand } from '../../slash-commands/SlashCommand.js';10import { SlashCommand } from '../../slash-commands/SlashCommand.js';
11import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';11import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
12import { isFunctionCallingSupported } from '../../openai.js';
13import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';12import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
14import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';13import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
15import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';14import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
@@ -21,7 +20,6 @@ const UPDATE_INTERVAL = 2000;
21const STREAMING_UPDATE_INTERVAL = 10000;20const STREAMING_UPDATE_INTERVAL = 10000;
22const TALKINGCHECK_UPDATE_INTERVAL = 500;21const TALKINGCHECK_UPDATE_INTERVAL = 500;
23const DEFAULT_FALLBACK_EXPRESSION = 'joy';22const DEFAULT_FALLBACK_EXPRESSION = 'joy';
24const FUNCTION_NAME = 'set_emotion';
25const DEFAULT_LLM_PROMPT = 'Ignore previous instructions. Classify the emotion of the last message. Output just one word, e.g. "joy" or "anger". Choose only one of the following labels: {{labels}}';23const DEFAULT_LLM_PROMPT = 'Ignore previous instructions. Classify the emotion of the last message. Output just one word, e.g. "joy" or "anger". Choose only one of the following labels: {{labels}}';
26const DEFAULT_EXPRESSIONS = [24const DEFAULT_EXPRESSIONS = [
27 'talkinghead',25 'talkinghead',
@@ -1017,10 +1015,6 @@ async function getLlmPrompt(labels) {
1017 return '';1015 return '';
1018 }1016 }
10191017
1020 if (isFunctionCallingSupported()) {
1021 return '';
1022 }
1023
1024 const labelsString = labels.map(x => `"${x}"`).join(', ');1018 const labelsString = labels.map(x => `"${x}"`).join(', ');
1025 const prompt = substituteParamsExtended(String(extension_settings.expressions.llmPrompt), { labels: labelsString });1019 const prompt = substituteParamsExtended(String(extension_settings.expressions.llmPrompt), { labels: labelsString });
1026 return prompt;1020 return prompt;
@@ -1056,41 +1050,6 @@ function parseLlmResponse(emotionResponse, labels) {
1056 throw new Error('Could not parse emotion response ' + emotionResponse);1050 throw new Error('Could not parse emotion response ' + emotionResponse);
1057}1051}
10581052
1059/**
1060 * Registers the function tool for the LLM API.
1061 * @param {FunctionToolRegister} args Function tool register arguments.
1062 */
1063function onFunctionToolRegister(args) {
1064 if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isFunctionCallingSupported()) {
1065 // Only trigger on quiet mode
1066 if (args.type !== 'quiet') {
1067 return;
1068 }
1069
1070 const emotions = DEFAULT_EXPRESSIONS.filter((e) => e != 'talkinghead');
1071 const jsonSchema = {
1072 $schema: 'http://json-schema.org/draft-04/schema#',
1073 type: 'object',
1074 properties: {
1075 emotion: {
1076 type: 'string',
1077 enum: emotions,
1078 description: `One of the following: ${JSON.stringify(emotions)}`,
1079 },
1080 },
1081 required: [
1082 'emotion',
1083 ],
1084 };
1085 args.registerFunctionTool(
1086 FUNCTION_NAME,
1087 substituteParams('Sets the label that best describes the current emotional state of {{char}}. Only select one of the enumerated values.'),
1088 jsonSchema,
1089 true,
1090 );
1091 }
1092}
1093
1094function onTextGenSettingsReady(args) {1053function onTextGenSettingsReady(args) {
1095 // Only call if inside an API call1054 // Only call if inside an API call
1096 if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) {1055 if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) {
@@ -1164,18 +1123,9 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
11641123
1165 const expressionsList = await getExpressionsList();1124 const expressionsList = await getExpressionsList();
1166 const prompt = substituteParamsExtended(customPrompt, { labels: expressionsList }) || await getLlmPrompt(expressionsList);1125 const prompt = substituteParamsExtended(customPrompt, { labels: expressionsList }) || await getLlmPrompt(expressionsList);
1167 let functionResult = null;
1168 eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);1126 eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);
1169 eventSource.once(event_types.LLM_FUNCTION_TOOL_REGISTER, onFunctionToolRegister);
1170 eventSource.once(event_types.LLM_FUNCTION_TOOL_CALL, (/** @type {FunctionToolCall} */ args) => {
1171 if (args.name !== FUNCTION_NAME) {
1172 return;
1173 }
1174
1175 functionResult = args?.arguments;
1176 });
1177 const emotionResponse = await generateRaw(text, main_api, false, false, prompt);1127 const emotionResponse = await generateRaw(text, main_api, false, false, prompt);
1178 return parseLlmResponse(functionResult || emotionResponse, expressionsList);1128 return parseLlmResponse(emotionResponse, expressionsList);
1179 }1129 }
1180 // Extras1130 // Extras
1181 default: {1131 default: {
public/scripts/openai.js+8 -141
@@ -70,6 +70,7 @@ import { renderTemplateAsync } from './templates.js';
70import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';70import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
71import { Popup, POPUP_RESULT } from './popup.js';71import { Popup, POPUP_RESULT } from './popup.js';
72import { t } from './i18n.js';72import { t } from './i18n.js';
73import { ToolManager } from './tool-calling.js';
7374
74export {75export {
75 openai_messages_count,76 openai_messages_count,
@@ -1863,8 +1864,8 @@ async function sendOpenAIRequest(type, messages, signal) {
1863 generate_data['seed'] = oai_settings.seed;1864 generate_data['seed'] = oai_settings.seed;
1864 }1865 }
18651866
1866 if (isFunctionCallingSupported() && !stream) {1867 if (!canMultiSwipe && ToolManager.isFunctionCallingSupported()) {
1867 await registerFunctionTools(type, generate_data);1868 await ToolManager.registerFunctionToolsOpenAI(generate_data);
1868 }1869 }
18691870
1870 if (isOAI && oai_settings.openai_model.startsWith('o1-')) {1871 if (isOAI && oai_settings.openai_model.startsWith('o1-')) {
@@ -1911,6 +1912,7 @@ async function sendOpenAIRequest(type, messages, signal) {
1911 return async function* streamData() {1912 return async function* streamData() {
1912 let text = '';1913 let text = '';
1913 const swipes = [];1914 const swipes = [];
1915 const toolCalls = [];
1914 while (true) {1916 while (true) {
1915 const { done, value } = await reader.read();1917 const { done, value } = await reader.read();
1916 if (done) return;1918 if (done) return;
@@ -1926,7 +1928,9 @@ async function sendOpenAIRequest(type, messages, signal) {
1926 text += getStreamingReply(parsed);1928 text += getStreamingReply(parsed);
1927 }1929 }
19281930
1929 yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed) };1931 ToolManager.parseToolCalls(toolCalls, parsed);
1932
1933 yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls };
1930 }1934 }
1931 };1935 };
1932 }1936 }
@@ -1948,147 +1952,10 @@ async function sendOpenAIRequest(type, messages, signal) {
1948 delay(1).then(() => saveLogprobsForActiveMessage(logprobs, null));1952 delay(1).then(() => saveLogprobsForActiveMessage(logprobs, null));
1949 }1953 }
19501954
1951 if (isFunctionCallingSupported()) {
1952 await checkFunctionToolCalls(data);
1953 }
1954
1955 return data;1955 return data;
1956 }1956 }
1957}1957}
19581958
1959/**
1960 * Register function tools for the next chat completion request.
1961 * @param {string} type Generation type
1962 * @param {object} data Generation data
1963 */
1964async function registerFunctionTools(type, data) {
1965 let toolChoice = 'auto';
1966 const tools = [];
1967
1968 /**
1969 * @type {registerFunctionTool}
1970 */
1971 const registerFunctionTool = (name, description, parameters, required) => {
1972 tools.push({
1973 type: 'function',
1974 function: {
1975 name,
1976 description,
1977 parameters,
1978 },
1979 });
1980
1981 if (required) {
1982 toolChoice = 'required';
1983 }
1984 };
1985
1986 /**
1987 * @type {FunctionToolRegister}
1988 */
1989 const args = {
1990 type,
1991 data,
1992 registerFunctionTool,
1993 };
1994
1995 await eventSource.emit(event_types.LLM_FUNCTION_TOOL_REGISTER, args);
1996
1997 if (tools.length) {
1998 console.log('Registered function tools:', tools);
1999
2000 data['tools'] = tools;
2001 data['tool_choice'] = toolChoice;
2002 }
2003}
2004
2005async function checkFunctionToolCalls(data) {
2006 const oaiCompat = [
2007 chat_completion_sources.OPENAI,
2008 chat_completion_sources.CUSTOM,
2009 chat_completion_sources.MISTRALAI,
2010 chat_completion_sources.OPENROUTER,
2011 chat_completion_sources.GROQ,
2012 ];
2013 if (oaiCompat.includes(oai_settings.chat_completion_source)) {
2014 if (!Array.isArray(data?.choices)) {
2015 return;
2016 }
2017
2018 // Find a choice with 0-index
2019 const choice = data.choices.find(choice => choice.index === 0);
2020
2021 if (!choice) {
2022 return;
2023 }
2024
2025 const toolCalls = choice.message.tool_calls;
2026
2027 if (!Array.isArray(toolCalls)) {
2028 return;
2029 }
2030
2031 for (const toolCall of toolCalls) {
2032 if (typeof toolCall.function !== 'object') {
2033 continue;
2034 }
2035
2036 /** @type {FunctionToolCall} */
2037 const args = toolCall.function;
2038 console.log('Function tool call:', toolCall);
2039 await eventSource.emit(event_types.LLM_FUNCTION_TOOL_CALL, args);
2040 }
2041 }
2042
2043 if ([chat_completion_sources.CLAUDE].includes(oai_settings.chat_completion_source)) {
2044 if (!Array.isArray(data?.content)) {
2045 return;
2046 }
2047
2048 for (const content of data.content) {
2049 if (content.type === 'tool_use') {
2050 /** @type {FunctionToolCall} */
2051 const args = { name: content.name, arguments: JSON.stringify(content.input) };
2052 await eventSource.emit(event_types.LLM_FUNCTION_TOOL_CALL, args);
2053 }
2054 }
2055 }
2056
2057 if ([chat_completion_sources.COHERE].includes(oai_settings.chat_completion_source)) {
2058 if (!Array.isArray(data?.tool_calls)) {
2059 return;
2060 }
2061
2062 for (const toolCall of data.tool_calls) {
2063 /** @type {FunctionToolCall} */
2064 const args = { name: toolCall.name, arguments: JSON.stringify(toolCall.parameters) };
2065 console.log('Function tool call:', toolCall);
2066 await eventSource.emit(event_types.LLM_FUNCTION_TOOL_CALL, args);
2067 }
2068 }
2069}
2070
2071export function isFunctionCallingSupported() {
2072 if (main_api !== 'openai') {
2073 return false;
2074 }
2075
2076 if (!oai_settings.function_calling) {
2077 return false;
2078 }
2079
2080 const supportedSources = [
2081 chat_completion_sources.OPENAI,
2082 chat_completion_sources.COHERE,
2083 chat_completion_sources.CUSTOM,
2084 chat_completion_sources.MISTRALAI,
2085 chat_completion_sources.CLAUDE,
2086 chat_completion_sources.OPENROUTER,
2087 chat_completion_sources.GROQ,
2088 ];
2089 return supportedSources.includes(oai_settings.chat_completion_source);
2090}
2091
2092function getStreamingReply(data) {1959function getStreamingReply(data) {
2093 if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {1960 if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
2094 return data?.delta?.text || '';1961 return data?.delta?.text || '';
@@ -4019,7 +3886,7 @@ async function onModelChange() {
4019 $('#openai_max_context').attr('max', max_32k);3886 $('#openai_max_context').attr('max', max_32k);
4020 } else if (value === 'text-bison-001') {3887 } else if (value === 'text-bison-001') {
4021 $('#openai_max_context').attr('max', max_8k);3888 $('#openai_max_context').attr('max', max_8k);
4022 // The ultra endpoints are possibly dead:3889 // The ultra endpoints are possibly dead:
4023 } else if (value.includes('gemini-1.0-ultra') || value === 'gemini-ultra') {3890 } else if (value.includes('gemini-1.0-ultra') || value === 'gemini-ultra') {
4024 $('#openai_max_context').attr('max', max_32k);3891 $('#openai_max_context').attr('max', max_32k);
4025 } else {3892 } else {
public/scripts/tool-calling.js+381 -0
@@ -0,0 +1,381 @@
1import { chat, main_api } from '../script.js';
2import { chat_completion_sources, oai_settings } from './openai.js';
3
4/**
5 * @typedef {object} ToolInvocation
6 * @property {string} id - A unique identifier for the tool invocation.
7 * @property {string} name - The name of the tool.
8 * @property {string} parameters - The parameters for the tool invocation.
9 * @property {string} result - The result of the tool invocation.
10 */
11
12/**
13 * A class that represents a tool definition.
14 */
15class ToolDefinition {
16 /**
17 * A unique name for the tool.
18 * @type {string}
19 */
20 #name;
21
22 /**
23 * A description of what the tool does.
24 * @type {string}
25 */
26 #description;
27
28 /**
29 * A JSON schema for the parameters that the tool accepts.
30 * @type {object}
31 */
32 #parameters;
33
34 /**
35 * A function that will be called when the tool is executed.
36 * @type {function}
37 */
38 #action;
39
40 /**
41 * Creates a new ToolDefinition.
42 * @param {string} name A unique name for the tool.
43 * @param {string} description A description of what the tool does.
44 * @param {object} parameters A JSON schema for the parameters that the tool accepts.
45 * @param {function} action A function that will be called when the tool is executed.
46 */
47 constructor(name, description, parameters, action) {
48 this.#name = name;
49 this.#description = description;
50 this.#parameters = parameters;
51 this.#action = action;
52 }
53
54 /**
55 * Converts the ToolDefinition to an OpenAI API representation
56 * @returns {object} OpenAI API representation of the tool.
57 */
58 toFunctionOpenAI() {
59 return {
60 type: 'function',
61 function: {
62 name: this.#name,
63 description: this.#description,
64 parameters: this.#parameters,
65 },
66 };
67 }
68
69 /**
70 * Invokes the tool with the given parameters.
71 * @param {object} parameters The parameters to pass to the tool.
72 * @returns {Promise<any>} The result of the tool's action function.
73 */
74 async invoke(parameters) {
75 return await this.#action(parameters);
76 }
77}
78
79/**
80 * A class that manages the registration and invocation of tools.
81 */
82export class ToolManager {
83 /**
84 * A map of tool names to tool definitions.
85 * @type {Map<string, ToolDefinition>}
86 */
87 static #tools = new Map();
88
89 /**
90 * Returns an Array of all tools that have been registered.
91 * @type {ToolDefinition[]}
92 */
93 static get tools() {
94 return Array.from(this.#tools.values());
95 }
96
97 /**
98 * Registers a new tool with the tool registry.
99 * @param {string} name The name of the tool.
100 * @param {string} description A description of what the tool does.
101 * @param {object} parameters A JSON schema for the parameters that the tool accepts.
102 * @param {function} action A function that will be called when the tool is executed.
103 */
104 static registerFunctionTool(name, description, parameters, action) {
105 if (this.#tools.has(name)) {
106 console.warn(`A tool with the name "${name}" has already been registered. The definition will be overwritten.`);
107 }
108
109 const definition = new ToolDefinition(name, description, parameters, action);
110 this.#tools.set(name, definition);
111 }
112
113 /**
114 * Removes a tool from the tool registry.
115 * @param {string} name The name of the tool to unregister.
116 */
117 static unregisterFunctionTool(name) {
118 if (!this.#tools.has(name)) {
119 console.warn(`No tool with the name "${name}" has been registered.`);
120 return;
121 }
122
123 this.#tools.delete(name);
124 }
125
126 /**
127 * Invokes a tool by name. Returns the result of the tool's action function.
128 * @param {string} name The name of the tool to invoke.
129 * @param {object} parameters Function parameters. For example, if the tool requires a "name" parameter, you would pass {name: "value"}.
130 * @returns {Promise<string|null>} The result of the tool's action function. If an error occurs, null is returned. Non-string results are JSON-stringified.
131 */
132 static async invokeFunctionTool(name, parameters) {
133 try {
134 if (!this.#tools.has(name)) {
135 throw new Error(`No tool with the name "${name}" has been registered.`);
136 }
137
138 const invokeParameters = typeof parameters === 'string' ? JSON.parse(parameters) : parameters;
139 const tool = this.#tools.get(name);
140 const result = await tool.invoke(invokeParameters);
141 return typeof result === 'string' ? result : JSON.stringify(result);
142 } catch (error) {
143 console.error(`An error occurred while invoking the tool "${name}":`, error);
144 return null;
145 }
146 }
147
148 /**
149 * Register function tools for the next chat completion request.
150 * @param {object} data Generation data
151 */
152 static async registerFunctionToolsOpenAI(data) {
153 const tools = [];
154
155 for (const tool of ToolManager.tools) {
156 tools.push(tool.toFunctionOpenAI());
157 }
158
159 if (tools.length) {
160 console.log('Registered function tools:', tools);
161
162 data['tools'] = tools;
163 data['tool_choice'] = 'auto';
164 }
165 }
166
167 /**
168 * Utility function to parse tool calls from a parsed response.
169 * @param {any[]} toolCalls The tool calls to update.
170 * @param {any} parsed The parsed response from the OpenAI API.
171 * @returns {void}
172 */
173 static parseToolCalls(toolCalls, parsed) {
174 if (!Array.isArray(parsed?.choices)) {
175 return;
176 }
177 for (const choice of parsed.choices) {
178 const choiceIndex = (typeof choice.index === 'number') ? choice.index : null;
179 const choiceDelta = choice.delta;
180
181 if (choiceIndex === null || !choiceDelta) {
182 continue;
183 }
184
185 const toolCallDeltas = choiceDelta?.tool_calls;
186
187 if (!Array.isArray(toolCallDeltas)) {
188 continue;
189 }
190
191 if (!Array.isArray(toolCalls[choiceIndex])) {
192 toolCalls[choiceIndex] = [];
193 }
194
195 for (const toolCallDelta of toolCallDeltas) {
196 const toolCallIndex = (typeof toolCallDelta?.index === 'number') ? toolCallDelta.index : null;
197
198 if (toolCallIndex === null) {
199 continue;
200 }
201
202 if (toolCalls[choiceIndex][toolCallIndex] === undefined) {
203 toolCalls[choiceIndex][toolCallIndex] = {};
204 }
205
206 const targetToolCall = toolCalls[choiceIndex][toolCallIndex];
207
208 ToolManager.#applyToolCallDelta(targetToolCall, toolCallDelta);
209 }
210 }
211 }
212
213 static #applyToolCallDelta(target, delta) {
214 for (const key in delta) {
215 if (!delta.hasOwnProperty(key)) continue;
216
217 const deltaValue = delta[key];
218 const targetValue = target[key];
219
220 if (deltaValue === null || deltaValue === undefined) {
221 target[key] = deltaValue;
222 continue;
223 }
224
225 if (typeof deltaValue === 'string') {
226 if (typeof targetValue === 'string') {
227 // Concatenate strings
228 target[key] = targetValue + deltaValue;
229 } else {
230 target[key] = deltaValue;
231 }
232 } else if (typeof deltaValue === 'object' && !Array.isArray(deltaValue)) {
233 if (typeof targetValue !== 'object' || targetValue === null || Array.isArray(targetValue)) {
234 target[key] = {};
235 }
236 // Recursively apply deltas to nested objects
237 ToolManager.#applyToolCallDelta(target[key], deltaValue);
238 } else {
239 // Assign other types directly
240 target[key] = deltaValue;
241 }
242 }
243 }
244
245 static isFunctionCallingSupported() {
246 if (main_api !== 'openai') {
247 return false;
248 }
249
250 if (!oai_settings.function_calling) {
251 return false;
252 }
253
254 const supportedSources = [
255 chat_completion_sources.OPENAI,
256 //chat_completion_sources.COHERE,
257 chat_completion_sources.CUSTOM,
258 chat_completion_sources.MISTRALAI,
259 //chat_completion_sources.CLAUDE,
260 chat_completion_sources.OPENROUTER,
261 chat_completion_sources.GROQ,
262 ];
263 return supportedSources.includes(oai_settings.chat_completion_source);
264 }
265
266 static #getToolCallsFromData(data) {
267 // Parsed tool calls from streaming data
268 if (Array.isArray(data) && data.length > 0) {
269 return data[0];
270 }
271
272 // Parsed tool calls from non-streaming data
273 if (!Array.isArray(data?.choices)) {
274 return;
275 }
276
277 // Find a choice with 0-index
278 const choice = data.choices.find(choice => choice.index === 0);
279
280 if (!choice) {
281 return;
282 }
283
284 return choice.message.tool_calls;
285 }
286
287 /**
288 * Check for function tool calls in the response data and invoke them.
289 * @param {any} data Reply data
290 * @returns {Promise<ToolInvocation[]>} Successful tool invocations
291 */
292 static async checkFunctionToolCalls(data) {
293 if (!ToolManager.isFunctionCallingSupported()) {
294 return [];
295 }
296
297 /** @type {ToolInvocation[]} */
298 const invocations = [];
299 const toolCalls = ToolManager.#getToolCallsFromData(data);
300 const oaiCompat = [
301 chat_completion_sources.OPENAI,
302 chat_completion_sources.CUSTOM,
303 chat_completion_sources.MISTRALAI,
304 chat_completion_sources.OPENROUTER,
305 chat_completion_sources.GROQ,
306 ];
307
308 if (oaiCompat.includes(oai_settings.chat_completion_source)) {
309 if (!Array.isArray(toolCalls)) {
310 return;
311 }
312
313 for (const toolCall of toolCalls) {
314 if (typeof toolCall.function !== 'object') {
315 continue;
316 }
317
318 console.log('Function tool call:', toolCall);
319 const id = toolCall.id;
320 const parameters = toolCall.function.arguments;
321 const name = toolCall.function.name;
322
323 toastr.info('Invoking function tool: ' + name);
324 const result = await ToolManager.invokeFunctionTool(name, parameters);
325 toastr.info('Function tool result: ' + result);
326
327 // Save a successful invocation
328 if (result) {
329 invocations.push({ id, name, result, parameters });
330 }
331 }
332 }
333
334 /*
335 if ([chat_completion_sources.CLAUDE].includes(oai_settings.chat_completion_source)) {
336 if (!Array.isArray(data?.content)) {
337 return;
338 }
339
340 for (const content of data.content) {
341 if (content.type === 'tool_use') {
342 const args = { name: content.name, arguments: JSON.stringify(content.input) };
343 }
344 }
345 }
346 */
347
348 /*
349 if ([chat_completion_sources.COHERE].includes(oai_settings.chat_completion_source)) {
350 if (!Array.isArray(data?.tool_calls)) {
351 return;
352 }
353
354 for (const toolCall of data.tool_calls) {
355 const args = { name: toolCall.name, arguments: JSON.stringify(toolCall.parameters) };
356 console.log('Function tool call:', toolCall);
357 }
358 }
359 */
360
361 return invocations;
362 }
363
364 /**
365 * Saves function tool invocations to the last user chat message extra metadata.
366 * @param {ToolInvocation[]} invocations
367 */
368 static saveFunctionToolInvocations(invocations) {
369 for (let index = chat.length - 1; index >= 0; index--) {
370 const message = chat[index];
371 if (message.is_user) {
372 if (!message.extra || typeof message.extra !== 'object') {
373 message.extra = {};
374 }
375 message.extra.tool_invocations = invocations;
376 debugger;
377 break;
378 }
379 }
380 }
381}
src/endpoints/backends/chat-completions.js+7 -15
@@ -121,18 +121,20 @@ async function sendClaudeRequest(request, response) {
121 ? [{ type: 'text', text: convertedPrompt.systemPrompt, cache_control: { type: 'ephemeral' } }]121 ? [{ type: 'text', text: convertedPrompt.systemPrompt, cache_control: { type: 'ephemeral' } }]
122 : convertedPrompt.systemPrompt;122 : convertedPrompt.systemPrompt;
123 }123 }
124 /*
124 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {125 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
125 // Claude doesn't do prefills on function calls, and doesn't allow empty messages126 // Claude doesn't do prefills on function calls, and doesn't allow empty messages
126 if (convertedPrompt.messages.length && convertedPrompt.messages[convertedPrompt.messages.length - 1].role === 'assistant') {127 if (convertedPrompt.messages.length && convertedPrompt.messages[convertedPrompt.messages.length - 1].role === 'assistant') {
127 convertedPrompt.messages.push({ role: 'user', content: '.' });128 convertedPrompt.messages.push({ role: 'user', content: '.' });
128 }129 }
129 additionalHeaders['anthropic-beta'] = 'tools-2024-05-16';130 additionalHeaders['anthropic-beta'] = 'tools-2024-05-16';
130 requestBody.tool_choice = { type: request.body.tool_choice === 'required' ? 'any' : 'auto' };131 requestBody.tool_choice = { type: request.body.tool_choice };
131 requestBody.tools = request.body.tools132 requestBody.tools = request.body.tools
132 .filter(tool => tool.type === 'function')133 .filter(tool => tool.type === 'function')
133 .map(tool => tool.function)134 .map(tool => tool.function)
134 .map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters }));135 .map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters }));
135 }136 }
137 */
136 if (enableSystemPromptCache) {138 if (enableSystemPromptCache) {
137 additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';139 additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';
138 }140 }
@@ -479,7 +481,7 @@ async function sendMistralAIRequest(request, response) {
479481
480 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {482 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
481 requestBody['tools'] = request.body.tools;483 requestBody['tools'] = request.body.tools;
482 requestBody['tool_choice'] = request.body.tool_choice === 'required' ? 'any' : 'auto';484 requestBody['tool_choice'] = request.body.tool_choice;
483 }485 }
484486
485 const config = {487 const config = {
@@ -549,11 +551,13 @@ async function sendCohereRequest(request, response) {
549 });551 });
550 }552 }
551553
554 /*
552 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {555 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
553 tools.push(...convertCohereTools(request.body.tools));556 tools.push(...convertCohereTools(request.body.tools));
554 // Can't have both connectors and tools in the same request557 // Can't have both connectors and tools in the same request
555 connectors.splice(0, connectors.length);558 connectors.splice(0, connectors.length);
556 }559 }
560 */
557561
558 // https://docs.cohere.com/reference/chat562 // https://docs.cohere.com/reference/chat
559 const requestBody = {563 const requestBody = {
@@ -910,18 +914,6 @@ router.post('/generate', jsonParser, function (request, response) {
910 apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);914 apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);
911 headers = {};915 headers = {};
912 bodyParams = {};916 bodyParams = {};
913
914 // 'required' tool choice is not supported by Groq
915 if (request.body.tool_choice === 'required') {
916 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
917 request.body.tool_choice = request.body.tools.length > 1
918 ? 'auto' :
919 { type: 'function', function: { name: request.body.tools[0]?.function?.name } };
920
921 } else {
922 request.body.tool_choice = 'none';
923 }
924 }
925 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ZEROONEAI) {917 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ZEROONEAI) {
926 apiUrl = API_01AI;918 apiUrl = API_01AI;
927 apiKey = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);919 apiKey = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);
@@ -958,7 +950,7 @@ router.post('/generate', jsonParser, function (request, response) {
958 controller.abort();950 controller.abort();
959 });951 });
960952
961 if (!isTextCompletion) {953 if (!isTextCompletion && Array.isArray(request.body.tools) && request.body.tools.length > 0) {
962 bodyParams['tools'] = request.body.tools;954 bodyParams['tools'] = request.body.tools;
963 bodyParams['tool_choice'] = request.body.tool_choice;955 bodyParams['tool_choice'] = request.body.tool_choice;
964 }956 }