Implement function tool calling for OpenAI
| @@ -4408,7 +4408,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4408 | 4408 | |
| 4409 | 4409 | if (ToolManager.isFunctionCallingSupported() && Array.isArray(streamingProcessor.toolCalls) && streamingProcessor.toolCalls.length) { |
| 4410 | 4410 | const invocations = await ToolManager.checkFunctionToolCalls(streamingProcessor.toolCalls); |
| 4411 | 4411 | if (Array.isArray(invocations) && invocations.length) { |
| 4412 | 4412 | const lastMessage = chat[chat.length - 1]; |
| 4413 | 4413 | const shouldDeleteMessage = ['', '...'].includes(lastMessage?.mes) && ['', '...'].includes(streamingProcessor.result); |
| 4414 | 4414 | if (shouldDeleteMessage) { |
| @@ -4457,7 +4457,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4457 | 4457 | |
| 4458 | 4458 | if (ToolManager.isFunctionCallingSupported()) { |
| 4459 | 4459 | const invocations = await ToolManager.checkFunctionToolCalls(data); |
| 4460 | 4460 | if (Array.isArray(invocations) && invocations.length) { |
| 4461 | 4461 | ToolManager.saveFunctionToolInvocations(invocations); |
| 4462 | 4462 | return Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName }, dryRun); |
| 4463 | 4463 | } |
| @@ -454,7 +454,8 @@ function setOpenAIMessages(chat) { | ||
| 454 | 454 | if (role == 'user' && oai_settings.wrap_in_quotes) content = `"${content}"`; |
| 455 | 455 | const name = chat[j]['name']; |
| 456 | 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 | 459 | j++; |
| 459 | 460 | } |
| 460 | 461 | |
| @@ -702,6 +703,7 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul | ||
| 702 | 703 | } |
| 703 | 704 | |
| 704 | 705 | const imageInlining = isImageInliningSupported(); |
| 706 | + const toolCalling = ToolManager.isFunctionCallingSupported(); | |
| 705 | 707 | |
| 706 | 708 | // Insert chat messages as long as there is budget available |
| 707 | 709 | const chatPool = [...messages].reverse(); |
| @@ -723,6 +725,24 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul | ||
| 723 | 725 | await chatMessage.addImage(chatPrompt.image); |
| 724 | 726 | } |
| 725 | 727 | |
| 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 | 746 | if (chatCompletion.canAfford(chatMessage)) { |
| 727 | 747 | if (type === 'continue' && oai_settings.continue_prefill && chatPrompt === firstNonInjected) { |
| 728 | 748 | // 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 | 2213 | content; |
| 2194 | 2214 | /** @type {string} */ |
| 2195 | 2215 | name; |
| 2216 | + /** @type {object} */ | |
| 2217 | + tool_call = null; | |
| 2196 | 2218 | |
| 2197 | 2219 | /** |
| 2198 | 2220 | * @constructor |
| @@ -2217,6 +2239,22 @@ class Message { | ||
| 2217 | 2239 | } |
| 2218 | 2240 | } |
| 2219 | 2241 | |
| 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 | 2258 | setName(name) { |
| 2221 | 2259 | this.name = name; |
| 2222 | 2260 | this.tokens = tokenHandler.count({ role: this.role, content: this.content, name: this.name }); |
| @@ -2564,7 +2602,7 @@ export class ChatCompletion { | ||
| 2564 | 2602 | this.checkTokenBudget(message, message.identifier); |
| 2565 | 2603 | |
| 2566 | 2604 | const index = this.findMessageIndex(identifier); |
| 2567 | 2605 | if (message.content || message.tool_calls) { |
| 2568 | 2606 | if ('start' === position) this.messages.collection[index].collection.unshift(message); |
| 2569 | 2607 | else if ('end' === position) this.messages.collection[index].collection.push(message); |
| 2570 | 2608 | else if (typeof position === 'number') this.messages.collection[index].collection.splice(position, 0, message); |
| @@ -2633,8 +2671,14 @@ export class ChatCompletion { | ||
| 2633 | 2671 | for (let item of this.messages.collection) { |
| 2634 | 2672 | if (item instanceof MessageCollection) { |
| 2635 | 2673 | chat.push(...item.getChat()); |
| 2636 | 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 | 2682 | chat.push(message); |
| 2639 | 2683 | } else { |
| 2640 | 2684 | this.log(`Skipping invalid or empty message in collection: ${JSON.stringify(item)}`); |
| @@ -307,7 +307,7 @@ export class ToolManager { | ||
| 307 | 307 | |
| 308 | 308 | if (oaiCompat.includes(oai_settings.chat_completion_source)) { |
| 309 | 309 | if (!Array.isArray(toolCalls)) { |
| 310 | 310 | return []; |
| 311 | 311 | } |
| 312 | 312 | |
| 313 | 313 | for (const toolCall of toolCalls) { |
| @@ -363,7 +363,7 @@ export class ToolManager { | ||
| 363 | 363 | |
| 364 | 364 | /** |
| 365 | 365 | * Saves function tool invocations to the last user chat message extra metadata. |
| 366 | 366 | * @param {ToolInvocation[]} invocations Successful tool invocations |
| 367 | 367 | */ |
| 368 | 368 | static saveFunctionToolInvocations(invocations) { |
| 369 | 369 | for (let index = chat.length - 1; index >= 0; index--) { |
| @@ -373,7 +373,6 @@ export class ToolManager { | ||
| 373 | 373 | message.extra = {}; |
| 374 | 374 | } |
| 375 | 375 | message.extra.tool_invocations = invocations; |
| 376 | - debugger; | |
| 377 | 376 | break; |
| 378 | 377 | } |
| 379 | 378 | } |