New tool calling framework
| @@ -1365,44 +1365,3 @@ declare namespace moment { | ||
| 1365 | 1365 | declare global { |
| 1366 | 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 | - */ | |
| 1372 | -interface 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 | - */ | |
| 1394 | -declare 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 | - */ | |
| 1399 | -interface 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 | -} | |
| @@ -246,6 +246,7 @@ import { initInputMarkdown } from './scripts/input-md-formatting.js'; | ||
| 246 | 246 | import { AbortReason } from './scripts/util/AbortReason.js'; |
| 247 | 247 | import { initSystemPrompts } from './scripts/sysprompt.js'; |
| 248 | 248 | import { registerExtensionSlashCommands as initExtensionSlashCommands } from './scripts/extensions-slashcommands.js'; |
| 249 | +import { ToolManager } from './scripts/tool-calling.js'; | |
| 249 | 250 | |
| 250 | 251 | //exporting functions and vars for mods |
| 251 | 252 | export { |
| @@ -463,8 +464,6 @@ export const event_types = { | ||
| 463 | 464 | FILE_ATTACHMENT_DELETED: 'file_attachment_deleted', |
| 464 | 465 | WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate', |
| 465 | 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 | 467 | ONLINE_STATUS_CHANGED: 'online_status_changed', |
| 469 | 468 | IMAGE_SWIPED: 'image_swiped', |
| 470 | 469 | CONNECTION_PROFILE_LOADED: 'connection_profile_loaded', |
| @@ -2921,6 +2920,7 @@ class StreamingProcessor { | ||
| 2921 | 2920 | this.swipes = []; |
| 2922 | 2921 | /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */ |
| 2923 | 2922 | this.messageLogprobs = []; |
| 2923 | + this.toolCalls = []; | |
| 2924 | 2924 | } |
| 2925 | 2925 | |
| 2926 | 2926 | #checkDomElements(messageId) { |
| @@ -3139,7 +3139,7 @@ class StreamingProcessor { | ||
| 3139 | 3139 | } |
| 3140 | 3140 | |
| 3141 | 3141 | /** |
| 3142 | 3142 | * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[] }, void, void>} |
| 3143 | 3143 | */ |
| 3144 | 3144 | *nullStreamingGeneration() { |
| 3145 | 3145 | throw new Error('Generation function for streaming is not hooked up'); |
| @@ -3161,12 +3161,13 @@ class StreamingProcessor { | ||
| 3161 | 3161 | try { |
| 3162 | 3162 | const sw = new Stopwatch(1000 / power_user.streaming_fps); |
| 3163 | 3163 | const timestamps = []; |
| 3164 | 3164 | for await (const { text, swipes, logprobs, toolCalls } of this.generator()) { |
| 3165 | 3165 | timestamps.push(Date.now()); |
| 3166 | 3166 | if (this.isStopped) { |
| 3167 | 3167 | return; |
| 3168 | 3168 | } |
| 3169 | 3169 | |
| 3170 | + this.toolCalls = toolCalls; | |
| 3170 | 3171 | this.result = text; |
| 3171 | 3172 | this.swipes = Array.from(swipes ?? []); |
| 3172 | 3173 | if (logprobs) { |
| @@ -4405,6 +4406,20 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4405 | 4406 | getMessage = continue_mag + getMessage; |
| 4406 | 4407 | } |
| 4407 | 4408 | |
| 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 | 4423 | if (streamingProcessor && !streamingProcessor.isStopped && streamingProcessor.isFinished) { |
| 4409 | 4424 | await streamingProcessor.onFinishStreaming(streamingProcessor.messageId, getMessage); |
| 4410 | 4425 | streamingProcessor = null; |
| @@ -4440,6 +4455,14 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4440 | 4455 | throw new Error(data?.response); |
| 4441 | 4456 | } |
| 4442 | 4457 | |
| 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 | 4466 | //const getData = await response.json(); |
| 4444 | 4467 | let getMessage = extractMessageFromData(data); |
| 4445 | 4468 | let title = extractTitleFromData(data); |
| @@ -7853,7 +7876,7 @@ function openAlternateGreetings() { | ||
| 7853 | 7876 | if (menu_type !== 'create') { |
| 7854 | 7877 | await createOrEditCharacter(); |
| 7855 | 7878 | } |
| 7856 | 7879 | }, |
| 7857 | 7880 | }); |
| 7858 | 7881 | |
| 7859 | 7882 | for (let index = 0; index < getArray().length; index++) { |
| @@ -8130,6 +8153,8 @@ window['SillyTavern'].getContext = function () { | ||
| 8130 | 8153 | registerHelper: () => { }, |
| 8131 | 8154 | registerMacro: MacrosParser.registerMacro.bind(MacrosParser), |
| 8132 | 8155 | unregisterMacro: MacrosParser.unregisterMacro.bind(MacrosParser), |
| 8156 | + registerFunctionTool: ToolManager.registerFunctionTool.bind(ToolManager), | |
| 8157 | + unregisterFunctionTool: ToolManager.unregisterFunctionTool.bind(ToolManager), | |
| 8133 | 8158 | registerDebugFunction: registerDebugFunction, |
| 8134 | 8159 | /** @deprecated Use renderExtensionTemplateAsync instead. */ |
| 8135 | 8160 | renderExtensionTemplate: renderExtensionTemplate, |
| @@ -9,7 +9,6 @@ import { debounce_timeout } from '../../constants.js'; | ||
| 9 | 9 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| 10 | 10 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| 11 | 11 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| 12 | -import { isFunctionCallingSupported } from '../../openai.js'; | |
| 13 | 12 | import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 14 | 13 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 15 | 14 | import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; |
| @@ -21,7 +20,6 @@ const UPDATE_INTERVAL = 2000; | ||
| 21 | 20 | const STREAMING_UPDATE_INTERVAL = 10000; |
| 22 | 21 | const TALKINGCHECK_UPDATE_INTERVAL = 500; |
| 23 | 22 | const DEFAULT_FALLBACK_EXPRESSION = 'joy'; |
| 24 | -const FUNCTION_NAME = 'set_emotion'; | |
| 25 | 23 | const 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}}'; |
| 26 | 24 | const DEFAULT_EXPRESSIONS = [ |
| 27 | 25 | 'talkinghead', |
| @@ -1017,10 +1015,6 @@ async function getLlmPrompt(labels) { | ||
| 1017 | 1015 | return ''; |
| 1018 | 1016 | } |
| 1019 | 1017 | |
| 1020 | - if (isFunctionCallingSupported()) { | |
| 1021 | - return ''; | |
| 1022 | - } | |
| 1023 | - | |
| 1024 | 1018 | const labelsString = labels.map(x => `"${x}"`).join(', '); |
| 1025 | 1019 | const prompt = substituteParamsExtended(String(extension_settings.expressions.llmPrompt), { labels: labelsString }); |
| 1026 | 1020 | return prompt; |
| @@ -1056,41 +1050,6 @@ function parseLlmResponse(emotionResponse, labels) { | ||
| 1056 | 1050 | throw new Error('Could not parse emotion response ' + emotionResponse); |
| 1057 | 1051 | } |
| 1058 | 1052 | |
| 1059 | -/** | |
| 1060 | - * Registers the function tool for the LLM API. | |
| 1061 | - * @param {FunctionToolRegister} args Function tool register arguments. | |
| 1062 | - */ | |
| 1063 | -function 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 | - | |
| 1094 | 1053 | function onTextGenSettingsReady(args) { |
| 1095 | 1054 | // Only call if inside an API call |
| 1096 | 1055 | if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) { |
| @@ -1164,18 +1123,9 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin | ||
| 1164 | 1123 | |
| 1165 | 1124 | const expressionsList = await getExpressionsList(); |
| 1166 | 1125 | const prompt = substituteParamsExtended(customPrompt, { labels: expressionsList }) || await getLlmPrompt(expressionsList); |
| 1167 | - let functionResult = null; | |
| 1168 | 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 | 1127 | const emotionResponse = await generateRaw(text, main_api, false, false, prompt); |
| 1178 | 1128 | return parseLlmResponse(functionResult || emotionResponse, expressionsList); |
| 1179 | 1129 | } |
| 1180 | 1130 | // Extras |
| 1181 | 1131 | default: { |
| @@ -70,6 +70,7 @@ import { renderTemplateAsync } from './templates.js'; | ||
| 70 | 70 | import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; |
| 71 | 71 | import { Popup, POPUP_RESULT } from './popup.js'; |
| 72 | 72 | import { t } from './i18n.js'; |
| 73 | +import { ToolManager } from './tool-calling.js'; | |
| 73 | 74 | |
| 74 | 75 | export { |
| 75 | 76 | openai_messages_count, |
| @@ -1863,8 +1864,8 @@ async function sendOpenAIRequest(type, messages, signal) { | ||
| 1863 | 1864 | generate_data['seed'] = oai_settings.seed; |
| 1864 | 1865 | } |
| 1865 | 1866 | |
| 1866 | 1867 | if (isFunctionCallingSupported()!canMultiSwipe && !streamToolManager.isFunctionCallingSupported()) { |
| 1867 | 1868 | await registerFunctionToolsToolManager.registerFunctionToolsOpenAI(type, generate_data); |
| 1868 | 1869 | } |
| 1869 | 1870 | |
| 1870 | 1871 | if (isOAI && oai_settings.openai_model.startsWith('o1-')) { |
| @@ -1911,6 +1912,7 @@ async function sendOpenAIRequest(type, messages, signal) { | ||
| 1911 | 1912 | return async function* streamData() { |
| 1912 | 1913 | let text = ''; |
| 1913 | 1914 | const swipes = []; |
| 1915 | + const toolCalls = []; | |
| 1914 | 1916 | while (true) { |
| 1915 | 1917 | const { done, value } = await reader.read(); |
| 1916 | 1918 | if (done) return; |
| @@ -1926,7 +1928,9 @@ async function sendOpenAIRequest(type, messages, signal) { | ||
| 1926 | 1928 | text += getStreamingReply(parsed); |
| 1927 | 1929 | } |
| 1928 | 1930 | |
| 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 | 1952 | delay(1).then(() => saveLogprobsForActiveMessage(logprobs, null)); |
| 1949 | 1953 | } |
| 1950 | 1954 | |
| 1951 | - if (isFunctionCallingSupported()) { | |
| 1952 | - await checkFunctionToolCalls(data); | |
| 1953 | - } | |
| 1954 | - | |
| 1955 | 1955 | return data; |
| 1956 | 1956 | } |
| 1957 | 1957 | } |
| 1958 | 1958 | |
| 1959 | -/** | |
| 1960 | - * Register function tools for the next chat completion request. | |
| 1961 | - * @param {string} type Generation type | |
| 1962 | - * @param {object} data Generation data | |
| 1963 | - */ | |
| 1964 | -async 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 | - | |
| 2005 | -async 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 | - | |
| 2071 | -export 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 | - | |
| 2092 | 1959 | function getStreamingReply(data) { |
| 2093 | 1960 | if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) { |
| 2094 | 1961 | return data?.delta?.text || ''; |
| @@ -4019,7 +3886,7 @@ async function onModelChange() { | ||
| 4019 | 3886 | $('#openai_max_context').attr('max', max_32k); |
| 4020 | 3887 | } else if (value === 'text-bison-001') { |
| 4021 | 3888 | $('#openai_max_context').attr('max', max_8k); |
| 4022 | 3889 | // The ultra endpoints are possibly dead: |
| 4023 | 3890 | } else if (value.includes('gemini-1.0-ultra') || value === 'gemini-ultra') { |
| 4024 | 3891 | $('#openai_max_context').attr('max', max_32k); |
| 4025 | 3892 | } else { |
| @@ -0,0 +1,381 @@ | ||
| 1 | +import { chat, main_api } from '../script.js'; | |
| 2 | +import { 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 | + */ | |
| 15 | +class 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 | + */ | |
| 82 | +export 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 | +} | |
| @@ -121,18 +121,20 @@ async function sendClaudeRequest(request, response) { | ||
| 121 | 121 | ? [{ type: 'text', text: convertedPrompt.systemPrompt, cache_control: { type: 'ephemeral' } }] |
| 122 | 122 | : convertedPrompt.systemPrompt; |
| 123 | 123 | } |
| 124 | + /* | |
| 124 | 125 | if (Array.isArray(request.body.tools) && request.body.tools.length > 0) { |
| 125 | 126 | // Claude doesn't do prefills on function calls, and doesn't allow empty messages |
| 126 | 127 | if (convertedPrompt.messages.length && convertedPrompt.messages[convertedPrompt.messages.length - 1].role === 'assistant') { |
| 127 | 128 | convertedPrompt.messages.push({ role: 'user', content: '.' }); |
| 128 | 129 | } |
| 129 | 130 | additionalHeaders['anthropic-beta'] = 'tools-2024-05-16'; |
| 130 | 131 | requestBody.tool_choice = { type: request.body.tool_choice === 'required' ? 'any' : 'auto' }; |
| 131 | 132 | requestBody.tools = request.body.tools |
| 132 | 133 | .filter(tool => tool.type === 'function') |
| 133 | 134 | .map(tool => tool.function) |
| 134 | 135 | .map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters })); |
| 135 | 136 | } |
| 137 | + */ | |
| 136 | 138 | if (enableSystemPromptCache) { |
| 137 | 139 | additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31'; |
| 138 | 140 | } |
| @@ -479,7 +481,7 @@ async function sendMistralAIRequest(request, response) { | ||
| 479 | 481 | |
| 480 | 482 | if (Array.isArray(request.body.tools) && request.body.tools.length > 0) { |
| 481 | 483 | requestBody['tools'] = request.body.tools; |
| 482 | 484 | requestBody['tool_choice'] = request.body.tool_choice === 'required' ? 'any' : 'auto'; |
| 483 | 485 | } |
| 484 | 486 | |
| 485 | 487 | const config = { |
| @@ -549,11 +551,13 @@ async function sendCohereRequest(request, response) { | ||
| 549 | 551 | }); |
| 550 | 552 | } |
| 551 | 553 | |
| 554 | + /* | |
| 552 | 555 | if (Array.isArray(request.body.tools) && request.body.tools.length > 0) { |
| 553 | 556 | tools.push(...convertCohereTools(request.body.tools)); |
| 554 | 557 | // Can't have both connectors and tools in the same request |
| 555 | 558 | connectors.splice(0, connectors.length); |
| 556 | 559 | } |
| 560 | + */ | |
| 557 | 561 | |
| 558 | 562 | // https://docs.cohere.com/reference/chat |
| 559 | 563 | const requestBody = { |
| @@ -910,18 +914,6 @@ router.post('/generate', jsonParser, function (request, response) { | ||
| 910 | 914 | apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ); |
| 911 | 915 | headers = {}; |
| 912 | 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 | 917 | } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ZEROONEAI) { |
| 926 | 918 | apiUrl = API_01AI; |
| 927 | 919 | apiKey = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI); |
| @@ -958,7 +950,7 @@ router.post('/generate', jsonParser, function (request, response) { | ||
| 958 | 950 | controller.abort(); |
| 959 | 951 | }); |
| 960 | 952 | |
| 961 | - if (!isTextCompletion) { | |
| 953 | + if (!isTextCompletion && Array.isArray(request.body.tools) && request.body.tools.length > 0) { | |
| 962 | 954 | bodyParams['tools'] = request.body.tools; |
| 963 | 955 | bodyParams['tool_choice'] = request.body.tool_choice; |
| 964 | 956 | } |