New tool calling framework

8006795897c54180823323357ad5a74c8660fe25

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

6 files changed, +426 -252Showing whitespace changes
public/global.d.ts+0 -41
@@ -1365,44 +1365,3 @@ declare namespace moment {
13651365declare global {
13661366 const moment: typeof moment;
13671367}
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-}
public/script.js+30 -5
@@ -246,6 +246,7 @@ import { initInputMarkdown } from './scripts/input-md-formatting.js';
246246import { AbortReason } from './scripts/util/AbortReason.js';
247247import { initSystemPrompts } from './scripts/sysprompt.js';
248248import { registerExtensionSlashCommands as initExtensionSlashCommands } from './scripts/extensions-slashcommands.js';
249+import { ToolManager } from './scripts/tool-calling.js';
249250
250251//exporting functions and vars for mods
251252export {
@@ -463,8 +464,6 @@ export const event_types = {
463464 FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',
464465 WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',
465466 OPEN_CHARACTER_LIBRARY: 'open_character_library',
466- LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register',
467- LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call',
468467 ONLINE_STATUS_CHANGED: 'online_status_changed',
469468 IMAGE_SWIPED: 'image_swiped',
470469 CONNECTION_PROFILE_LOADED: 'connection_profile_loaded',
@@ -2921,6 +2920,7 @@ class StreamingProcessor {
29212920 this.swipes = [];
29222921 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
29232922 this.messageLogprobs = [];
2923+ this.toolCalls = [];
29242924 }
29252925
29262926 #checkDomElements(messageId) {
@@ -3139,7 +3139,7 @@ class StreamingProcessor {
31393139 }
31403140
31413141 /**
31423142 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[] }, void, void>}
31433143 */
31443144 *nullStreamingGeneration() {
31453145 throw new Error('Generation function for streaming is not hooked up');
@@ -3161,12 +3161,13 @@ class StreamingProcessor {
31613161 try {
31623162 const sw = new Stopwatch(1000 / power_user.streaming_fps);
31633163 const timestamps = [];
31643164 for await (const { text, swipes, logprobs, toolCalls } of this.generator()) {
31653165 timestamps.push(Date.now());
31663166 if (this.isStopped) {
31673167 return;
31683168 }
31693169
3170+ this.toolCalls = toolCalls;
31703171 this.result = text;
31713172 this.swipes = Array.from(swipes ?? []);
31723173 if (logprobs) {
@@ -4405,6 +4406,20 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
44054406 getMessage = continue_mag + getMessage;
44064407 }
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+
44084423 if (streamingProcessor && !streamingProcessor.isStopped && streamingProcessor.isFinished) {
44094424 await streamingProcessor.onFinishStreaming(streamingProcessor.messageId, getMessage);
44104425 streamingProcessor = null;
@@ -4440,6 +4455,14 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
44404455 throw new Error(data?.response);
44414456 }
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+
44434466 //const getData = await response.json();
44444467 let getMessage = extractMessageFromData(data);
44454468 let title = extractTitleFromData(data);
@@ -7853,7 +7876,7 @@ function openAlternateGreetings() {
78537876 if (menu_type !== 'create') {
78547877 await createOrEditCharacter();
78557878 }
78567879 },
78577880 });
78587881
78597882 for (let index = 0; index < getArray().length; index++) {
@@ -8130,6 +8153,8 @@ window['SillyTavern'].getContext = function () {
81308153 registerHelper: () => { },
81318154 registerMacro: MacrosParser.registerMacro.bind(MacrosParser),
81328155 unregisterMacro: MacrosParser.unregisterMacro.bind(MacrosParser),
8156+ registerFunctionTool: ToolManager.registerFunctionTool.bind(ToolManager),
8157+ unregisterFunctionTool: ToolManager.unregisterFunctionTool.bind(ToolManager),
81338158 registerDebugFunction: registerDebugFunction,
81348159 /** @deprecated Use renderExtensionTemplateAsync instead. */
81358160 renderExtensionTemplate: renderExtensionTemplate,
public/scripts/extensions/expressions/index.js+1 -51
@@ -9,7 +9,6 @@ import { debounce_timeout } from '../../constants.js';
99import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
1010import { SlashCommand } from '../../slash-commands/SlashCommand.js';
1111import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
12-import { isFunctionCallingSupported } from '../../openai.js';
1312import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
1413import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
1514import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
@@ -21,7 +20,6 @@ const UPDATE_INTERVAL = 2000;
2120const STREAMING_UPDATE_INTERVAL = 10000;
2221const TALKINGCHECK_UPDATE_INTERVAL = 500;
2322const DEFAULT_FALLBACK_EXPRESSION = 'joy';
24-const FUNCTION_NAME = 'set_emotion';
2523const 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}}';
2624const DEFAULT_EXPRESSIONS = [
2725 'talkinghead',
@@ -1017,10 +1015,6 @@ async function getLlmPrompt(labels) {
10171015 return '';
10181016 }
10191017
1020- if (isFunctionCallingSupported()) {
1021- return '';
1022- }
1023-
10241018 const labelsString = labels.map(x => `"${x}"`).join(', ');
10251019 const prompt = substituteParamsExtended(String(extension_settings.expressions.llmPrompt), { labels: labelsString });
10261020 return prompt;
@@ -1056,41 +1050,6 @@ function parseLlmResponse(emotionResponse, labels) {
10561050 throw new Error('Could not parse emotion response ' + emotionResponse);
10571051}
10581052
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-
10941053function onTextGenSettingsReady(args) {
10951054 // Only call if inside an API call
10961055 if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) {
@@ -1164,18 +1123,9 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
11641123
11651124 const expressionsList = await getExpressionsList();
11661125 const prompt = substituteParamsExtended(customPrompt, { labels: expressionsList }) || await getLlmPrompt(expressionsList);
1167- let functionResult = null;
11681126 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- });
11771127 const emotionResponse = await generateRaw(text, main_api, false, false, prompt);
11781128 return parseLlmResponse(functionResult || emotionResponse, expressionsList);
11791129 }
11801130 // Extras
11811131 default: {
public/scripts/openai.js+7 -140
@@ -70,6 +70,7 @@ import { renderTemplateAsync } from './templates.js';
7070import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
7171import { Popup, POPUP_RESULT } from './popup.js';
7272import { t } from './i18n.js';
73+import { ToolManager } from './tool-calling.js';
7374
7475export {
7576 openai_messages_count,
@@ -1863,8 +1864,8 @@ async function sendOpenAIRequest(type, messages, signal) {
18631864 generate_data['seed'] = oai_settings.seed;
18641865 }
18651866
18661867 if (isFunctionCallingSupported()!canMultiSwipe && !streamToolManager.isFunctionCallingSupported()) {
18671868 await registerFunctionToolsToolManager.registerFunctionToolsOpenAI(type, generate_data);
18681869 }
18691870
18701871 if (isOAI && oai_settings.openai_model.startsWith('o1-')) {
@@ -1911,6 +1912,7 @@ async function sendOpenAIRequest(type, messages, signal) {
19111912 return async function* streamData() {
19121913 let text = '';
19131914 const swipes = [];
1915+ const toolCalls = [];
19141916 while (true) {
19151917 const { done, value } = await reader.read();
19161918 if (done) return;
@@ -1926,7 +1928,9 @@ async function sendOpenAIRequest(type, messages, signal) {
19261928 text += getStreamingReply(parsed);
19271929 }
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 };
19301934 }
19311935 };
19321936 }
@@ -1948,147 +1952,10 @@ async function sendOpenAIRequest(type, messages, signal) {
19481952 delay(1).then(() => saveLogprobsForActiveMessage(logprobs, null));
19491953 }
19501954
1951- if (isFunctionCallingSupported()) {
1952- await checkFunctionToolCalls(data);
1953- }
1954-
19551955 return data;
19561956 }
19571957}
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- */
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-
20921959function getStreamingReply(data) {
20931960 if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
20941961 return data?.delta?.text || '';
public/scripts/tool-calling.js+381 -0
@@ -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+}
src/endpoints/backends/chat-completions.js+7 -15
@@ -121,18 +121,20 @@ async function sendClaudeRequest(request, response) {
121121 ? [{ type: 'text', text: convertedPrompt.systemPrompt, cache_control: { type: 'ephemeral' } }]
122122 : convertedPrompt.systemPrompt;
123123 }
124+ /*
124125 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
125126 // Claude doesn't do prefills on function calls, and doesn't allow empty messages
126127 if (convertedPrompt.messages.length && convertedPrompt.messages[convertedPrompt.messages.length - 1].role === 'assistant') {
127128 convertedPrompt.messages.push({ role: 'user', content: '.' });
128129 }
129130 additionalHeaders['anthropic-beta'] = 'tools-2024-05-16';
130131 requestBody.tool_choice = { type: request.body.tool_choice === 'required' ? 'any' : 'auto' };
131132 requestBody.tools = request.body.tools
132133 .filter(tool => tool.type === 'function')
133134 .map(tool => tool.function)
134135 .map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters }));
135136 }
137+ */
136138 if (enableSystemPromptCache) {
137139 additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';
138140 }
@@ -479,7 +481,7 @@ async function sendMistralAIRequest(request, response) {
479481
480482 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
481483 requestBody['tools'] = request.body.tools;
482484 requestBody['tool_choice'] = request.body.tool_choice === 'required' ? 'any' : 'auto';
483485 }
484486
485487 const config = {
@@ -549,11 +551,13 @@ async function sendCohereRequest(request, response) {
549551 });
550552 }
551553
554+ /*
552555 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
553556 tools.push(...convertCohereTools(request.body.tools));
554557 // Can't have both connectors and tools in the same request
555558 connectors.splice(0, connectors.length);
556559 }
560+ */
557561
558562 // https://docs.cohere.com/reference/chat
559563 const requestBody = {
@@ -910,18 +914,6 @@ router.post('/generate', jsonParser, function (request, response) {
910914 apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);
911915 headers = {};
912916 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- }
925917 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ZEROONEAI) {
926918 apiUrl = API_01AI;
927919 apiKey = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);
@@ -958,7 +950,7 @@ router.post('/generate', jsonParser, function (request, response) {
958950 controller.abort();
959951 });
960952
961- if (!isTextCompletion) {
953+ if (!isTextCompletion && Array.isArray(request.body.tools) && request.body.tools.length > 0) {
962954 bodyParams['tools'] = request.body.tools;
963955 bodyParams['tool_choice'] = request.body.tool_choice;
964956 }