Implement function tool calling for OpenAI

c94c06ed4dac3c4895e4afb5b0e6c1524829e49b

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

3 files changed, +52 -9Showing whitespace changes
public/script.js+2 -2
@@ -4408,7 +4408,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
44084408
4409 if (ToolManager.isFunctionCallingSupported() && Array.isArray(streamingProcessor.toolCalls) && streamingProcessor.toolCalls.length) {4409 if (ToolManager.isFunctionCallingSupported() && Array.isArray(streamingProcessor.toolCalls) && streamingProcessor.toolCalls.length) {
4410 const invocations = await ToolManager.checkFunctionToolCalls(streamingProcessor.toolCalls);4410 const invocations = await ToolManager.checkFunctionToolCalls(streamingProcessor.toolCalls);
4411 if (invocations.length) {4411 if (Array.isArray(invocations) && invocations.length) {
4412 const lastMessage = chat[chat.length - 1];4412 const lastMessage = chat[chat.length - 1];
4413 const shouldDeleteMessage = ['', '...'].includes(lastMessage?.mes) && ['', '...'].includes(streamingProcessor.result);4413 const shouldDeleteMessage = ['', '...'].includes(lastMessage?.mes) && ['', '...'].includes(streamingProcessor.result);
4414 if (shouldDeleteMessage) {4414 if (shouldDeleteMessage) {
@@ -4457,7 +4457,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
44574457
4458 if (ToolManager.isFunctionCallingSupported()) {4458 if (ToolManager.isFunctionCallingSupported()) {
4459 const invocations = await ToolManager.checkFunctionToolCalls(data);4459 const invocations = await ToolManager.checkFunctionToolCalls(data);
4460 if (invocations.length) {4460 if (Array.isArray(invocations) && invocations.length) {
4461 ToolManager.saveFunctionToolInvocations(invocations);4461 ToolManager.saveFunctionToolInvocations(invocations);
4462 return Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName }, dryRun);4462 return Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName }, dryRun);
4463 }4463 }
public/scripts/openai.js+48 -4
@@ -454,7 +454,8 @@ function setOpenAIMessages(chat) {
454 if (role == 'user' && oai_settings.wrap_in_quotes) content = `"${content}"`;454 if (role == 'user' && oai_settings.wrap_in_quotes) content = `"${content}"`;
455 const name = chat[j]['name'];455 const name = chat[j]['name'];
456 const image = chat[j]?.extra?.image;456 const image = chat[j]?.extra?.image;
457 messages[i] = { 'role': role, 'content': content, name: name, 'image': image };457 const invocations = chat[j]?.extra?.tool_invocations;
458 messages[i] = { 'role': role, 'content': content, name: name, 'image': image, 'invocations': invocations };
458 j++;459 j++;
459 }460 }
460461
@@ -702,6 +703,7 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
702 }703 }
703704
704 const imageInlining = isImageInliningSupported();705 const imageInlining = isImageInliningSupported();
706 const toolCalling = ToolManager.isFunctionCallingSupported();
705707
706 // Insert chat messages as long as there is budget available708 // Insert chat messages as long as there is budget available
707 const chatPool = [...messages].reverse();709 const chatPool = [...messages].reverse();
@@ -723,6 +725,24 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
723 await chatMessage.addImage(chatPrompt.image);725 await chatMessage.addImage(chatPrompt.image);
724 }726 }
725727
728 if (toolCalling && Array.isArray(chatPrompt.invocations)) {
729 /** @type {import('./tool-calling.js').ToolInvocation[]} */
730 const invocations = chatPrompt.invocations.slice().reverse();
731 const toolCallMessage = new Message('assistant', undefined, 'toolCall-' + chatMessage.identifier);
732 toolCallMessage.setToolCalls(invocations);
733 if (chatCompletion.canAfford(toolCallMessage)) {
734 for (const invocation of invocations) {
735 const toolResultMessage = new Message('tool', invocation.result, invocation.id);
736 const canAfford = chatCompletion.canAfford(toolResultMessage) && chatCompletion.canAfford(toolCallMessage);
737 if (!canAfford) {
738 break;
739 }
740 chatCompletion.insertAtStart(toolResultMessage, 'chatHistory');
741 }
742 chatCompletion.insertAtStart(toolCallMessage, 'chatHistory');
743 }
744 }
745
726 if (chatCompletion.canAfford(chatMessage)) {746 if (chatCompletion.canAfford(chatMessage)) {
727 if (type === 'continue' && oai_settings.continue_prefill && chatPrompt === firstNonInjected) {747 if (type === 'continue' && oai_settings.continue_prefill && chatPrompt === firstNonInjected) {
728 // in case we are using continue_prefill and the latest message is an assistant message, we want to prepend the users assistant prefill on the message748 // in case we are using continue_prefill and the latest message is an assistant message, we want to prepend the users assistant prefill on the message
@@ -2193,6 +2213,8 @@ class Message {
2193 content;2213 content;
2194 /** @type {string} */2214 /** @type {string} */
2195 name;2215 name;
2216 /** @type {object} */
2217 tool_call = null;
21962218
2197 /**2219 /**
2198 * @constructor2220 * @constructor
@@ -2217,6 +2239,22 @@ class Message {
2217 }2239 }
2218 }2240 }
22192241
2242 /**
2243 * Reconstruct the message from a tool invocation.
2244 * @param {import('./tool-calling.js').ToolInvocation[]} invocations
2245 */
2246 setToolCalls(invocations) {
2247 this.tool_calls = invocations.map(i => ({
2248 id: i.id,
2249 type: 'function',
2250 function: {
2251 arguments: i.parameters,
2252 name: i.name,
2253 },
2254 }));
2255 this.tokens = tokenHandler.count({ role: this.role, tool_calls: JSON.stringify(this.tool_calls) });
2256 }
2257
2220 setName(name) {2258 setName(name) {
2221 this.name = name;2259 this.name = name;
2222 this.tokens = tokenHandler.count({ role: this.role, content: this.content, name: this.name });2260 this.tokens = tokenHandler.count({ role: this.role, content: this.content, name: this.name });
@@ -2564,7 +2602,7 @@ export class ChatCompletion {
2564 this.checkTokenBudget(message, message.identifier);2602 this.checkTokenBudget(message, message.identifier);
25652603
2566 const index = this.findMessageIndex(identifier);2604 const index = this.findMessageIndex(identifier);
2567 if (message.content) {2605 if (message.content || message.tool_calls) {
2568 if ('start' === position) this.messages.collection[index].collection.unshift(message);2606 if ('start' === position) this.messages.collection[index].collection.unshift(message);
2569 else if ('end' === position) this.messages.collection[index].collection.push(message);2607 else if ('end' === position) this.messages.collection[index].collection.push(message);
2570 else if (typeof position === 'number') this.messages.collection[index].collection.splice(position, 0, message);2608 else if (typeof position === 'number') this.messages.collection[index].collection.splice(position, 0, message);
@@ -2633,8 +2671,14 @@ export class ChatCompletion {
2633 for (let item of this.messages.collection) {2671 for (let item of this.messages.collection) {
2634 if (item instanceof MessageCollection) {2672 if (item instanceof MessageCollection) {
2635 chat.push(...item.getChat());2673 chat.push(...item.getChat());
2636 } else if (item instanceof Message && item.content) {2674 } else if (item instanceof Message && (item.content || item.tool_calls)) {
2637 const message = { role: item.role, content: item.content, ...(item.name ? { name: item.name } : {}) };2675 const message = {
2676 role: item.role,
2677 content: item.content,
2678 ...(item.name ? { name: item.name } : {}),
2679 ...(item.tool_calls ? { tool_calls: item.tool_calls } : {}),
2680 ...(item.role === 'tool' ? { tool_call_id: item.identifier } : {}),
2681 };
2638 chat.push(message);2682 chat.push(message);
2639 } else {2683 } else {
2640 this.log(`Skipping invalid or empty message in collection: ${JSON.stringify(item)}`);2684 this.log(`Skipping invalid or empty message in collection: ${JSON.stringify(item)}`);
public/scripts/tool-calling.js+2 -3
@@ -307,7 +307,7 @@ export class ToolManager {
307307
308 if (oaiCompat.includes(oai_settings.chat_completion_source)) {308 if (oaiCompat.includes(oai_settings.chat_completion_source)) {
309 if (!Array.isArray(toolCalls)) {309 if (!Array.isArray(toolCalls)) {
310 return;310 return [];
311 }311 }
312312
313 for (const toolCall of toolCalls) {313 for (const toolCall of toolCalls) {
@@ -363,7 +363,7 @@ export class ToolManager {
363363
364 /**364 /**
365 * Saves function tool invocations to the last user chat message extra metadata.365 * Saves function tool invocations to the last user chat message extra metadata.
366 * @param {ToolInvocation[]} invocations366 * @param {ToolInvocation[]} invocations Successful tool invocations
367 */367 */
368 static saveFunctionToolInvocations(invocations) {368 static saveFunctionToolInvocations(invocations) {
369 for (let index = chat.length - 1; index >= 0; index--) {369 for (let index = chat.length - 1; index >= 0; index--) {
@@ -373,7 +373,6 @@ export class ToolManager {
373 message.extra = {};373 message.extra = {};
374 }374 }
375 message.extra.tool_invocations = invocations;375 message.extra.tool_invocations = invocations;
376 debugger;
377 break;376 break;
378 }377 }
379 }378 }