2024-10-05 18:08:57 +02:00
import { addOneMessage , chat , event _types , eventSource , main _api , saveChatConditional , system _avatar , systemUserName } from '../script.js' ;
2024-10-02 00:00:48 +02:00
import { chat _completion _sources , oai _settings } from './openai.js' ;
2024-10-03 23:11:36 +02:00
import { Popup } from './popup.js' ;
2024-10-06 11:49:08 +02:00
import { SlashCommand } from './slash-commands/SlashCommand.js' ;
import { ARGUMENT _TYPE , SlashCommandArgument , SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js' ;
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js' ;
import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js' ;
import { enumTypes , SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js' ;
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js' ;
import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js' ;
2024-10-02 00:00:48 +02:00
/ * *
* @ typedef { object } ToolInvocation
* @ property { string } id - A unique identifier for the tool invocation .
2024-10-03 23:39:28 +02:00
* @ property { string } displayName - The display name of the tool .
2024-10-02 00:00:48 +02:00
* @ property { string } name - The name of the tool .
* @ property { string } parameters - The parameters for the tool invocation .
* @ property { string } result - The result of the tool invocation .
* /
2024-10-03 23:11:36 +02:00
/ * *
* @ typedef { object } ToolInvocationResult
* @ property { ToolInvocation [ ] } invocations Successful tool invocations
* @ property { Error [ ] } errors Errors that occurred during tool invocation
* /
2024-10-04 12:34:17 +02:00
/ * *
* @ typedef { object } ToolRegistration
* @ property { string } name - The name of the tool .
* @ property { string } displayName - The display name of the tool .
* @ property { string } description - A description of the tool .
* @ property { object } parameters - The parameters for the tool .
* @ property { function } action - The action to perform when the tool is invoked .
2024-10-09 02:56:24 +02:00
* @ property { function } [ formatMessage ] - A function to format the tool call message .
* @ property { function } [ shouldRegister ] - A function to determine if the tool should be registered .
2024-10-04 12:34:17 +02:00
* /
2024-10-06 11:49:08 +02:00
/ * *
* @ typedef { object } ToolDefinitionOpenAI
* @ property { string } type - The type of the tool .
* @ property { object } function - The function definition .
* @ property { string } function . name - The name of the function .
* @ property { string } function . description - The description of the function .
* @ property { object } function . parameters - The parameters of the function .
* @ property { function } toString - A function to convert the tool to a string .
* /
/ * *
* Assigns nested variables to a scope .
* @ param { import ( './slash-commands/SlashCommandScope.js' ) . SlashCommandScope } scope The scope to assign variables to .
* @ param { object } arg Object to assign variables from .
* @ param { string } prefix Prefix for the variable names .
* /
function assignNestedVariables ( scope , arg , prefix ) {
Object . entries ( arg ) . forEach ( ( [ key , value ] ) => {
const newPrefix = ` ${ prefix } . ${ key } ` ;
if ( typeof value === 'object' && value !== null ) {
assignNestedVariables ( scope , value , newPrefix ) ;
} else {
scope . letVariable ( newPrefix , value ) ;
}
} ) ;
}
/ * *
* Checks if a string is a valid JSON string .
* @ param { string } str The string to check
* @ returns { boolean } If the string is a valid JSON string
* /
function isJson ( str ) {
try {
JSON . parse ( str ) ;
return true ;
} catch {
return false ;
}
}
/ * *
* Tries to parse a string as JSON , returning the original string if parsing fails .
* @ param { string } str The string to try to parse
* @ returns { object | string } Parsed JSON or the original string
* /
function tryParse ( str ) {
try {
return JSON . parse ( str ) ;
} catch {
return str ;
}
}
2024-10-06 21:22:19 +02:00
/ * *
* Stringifies an object if it is not already a string .
* @ param { any } obj The object to stringify
* @ returns { string } A JSON string representation of the object .
* /
function stringify ( obj ) {
return typeof obj === 'string' ? obj : JSON . stringify ( obj ) ;
}
2024-10-02 00:00:48 +02:00
/ * *
* A class that represents a tool definition .
* /
class ToolDefinition {
/ * *
* A unique name for the tool .
* @ type { string }
* /
# name ;
2024-10-03 23:39:28 +02:00
/ * *
* A user - friendly display name for the tool .
* @ type { string }
* /
# displayName ;
2024-10-02 00:00:48 +02:00
/ * *
* A description of what the tool does .
* @ type { string }
* /
# description ;
/ * *
* A JSON schema for the parameters that the tool accepts .
* @ type { object }
* /
# parameters ;
/ * *
* A function that will be called when the tool is executed .
* @ type { function }
* /
# action ;
2024-10-03 23:39:28 +02:00
/ * *
* A function that will be called to format the tool call toast .
* @ type { function }
* /
# formatMessage ;
2024-10-09 02:53:32 +02:00
/ * *
* A function that will be called to determine if the tool should be registered .
* @ type { function }
* /
# shouldRegister ;
2024-10-02 00:00:48 +02:00
/ * *
* Creates a new ToolDefinition .
* @ param { string } name A unique name for the tool .
2024-10-03 23:39:28 +02:00
* @ param { string } displayName A user - friendly display name for the tool .
2024-10-02 00:00:48 +02:00
* @ param { string } description A description of what the tool does .
* @ param { object } parameters A JSON schema for the parameters that the tool accepts .
* @ param { function } action A function that will be called when the tool is executed .
2024-10-03 23:39:28 +02:00
* @ param { function } formatMessage A function that will be called to format the tool call toast .
2024-10-09 02:53:32 +02:00
* @ param { function } shouldRegister A function that will be called to determine if the tool should be registered .
2024-10-02 00:00:48 +02:00
* /
2024-10-09 02:53:32 +02:00
constructor ( name , displayName , description , parameters , action , formatMessage , shouldRegister ) {
2024-10-02 00:00:48 +02:00
this . # name = name ;
2024-10-03 23:39:28 +02:00
this . # displayName = displayName ;
2024-10-02 00:00:48 +02:00
this . # description = description ;
this . # parameters = parameters ;
this . # action = action ;
2024-10-03 23:39:28 +02:00
this . # formatMessage = formatMessage ;
2024-10-09 02:53:32 +02:00
this . # shouldRegister = shouldRegister ;
2024-10-02 00:00:48 +02:00
}
/ * *
* Converts the ToolDefinition to an OpenAI API representation
2024-10-06 11:49:08 +02:00
* @ returns { ToolDefinitionOpenAI } OpenAI API representation of the tool .
2024-10-02 00:00:48 +02:00
* /
toFunctionOpenAI ( ) {
return {
type : 'function' ,
function : {
name : this . # name ,
description : this . # description ,
parameters : this . # parameters ,
} ,
2024-10-06 19:19:58 +02:00
toString : function ( ) {
2024-10-06 12:01:14 +02:00
return ` <div><b> ${ this . function . name } </b></div><div><small> ${ this . function . description } </small></div><pre class="justifyLeft wordBreakAll"><code class="flex padding5"> ${ JSON . stringify ( this . function . parameters , null , 2 ) } </code></pre><hr> ` ;
2024-10-06 11:49:08 +02:00
} ,
2024-10-02 00:00:48 +02:00
} ;
}
/ * *
* Invokes the tool with the given parameters .
* @ param { object } parameters The parameters to pass to the tool .
* @ returns { Promise < any > } The result of the tool ' s action function .
* /
async invoke ( parameters ) {
return await this . # action ( parameters ) ;
}
2024-10-03 23:39:28 +02:00
/ * *
* Formats a message with the tool invocation .
* @ param { object } parameters The parameters to pass to the tool .
2024-10-09 02:56:24 +02:00
* @ returns { Promise < string > } The formatted message .
2024-10-03 23:39:28 +02:00
* /
2024-10-09 02:56:24 +02:00
async formatMessage ( parameters ) {
2024-10-03 23:39:28 +02:00
return typeof this . # formatMessage === 'function'
2024-10-09 02:56:24 +02:00
? await this . # formatMessage ( parameters )
2024-10-03 23:39:28 +02:00
: ` Invoking tool: ${ this . # displayName || this . # name } ` ;
}
2024-10-09 02:53:32 +02:00
async shouldRegister ( ) {
return typeof this . # shouldRegister === 'function'
? await this . # shouldRegister ( )
: true ;
}
2024-10-03 23:39:28 +02:00
get displayName ( ) {
return this . # displayName ;
}
2024-10-02 00:00:48 +02:00
}
/ * *
* A class that manages the registration and invocation of tools .
* /
export class ToolManager {
/ * *
* A map of tool names to tool definitions .
* @ type { Map < string , ToolDefinition > }
* /
static # tools = new Map ( ) ;
2024-10-04 13:31:15 +02:00
static # INPUT _DELTA _KEY = '__input_json_delta' ;
2024-10-06 23:22:27 +02:00
/ * *
* The maximum number of times to recurse when parsing tool calls .
* @ type { number }
* /
static RECURSE _LIMIT = 5 ;
2024-10-02 00:00:48 +02:00
/ * *
* Returns an Array of all tools that have been registered .
* @ type { ToolDefinition [ ] }
* /
static get tools ( ) {
return Array . from ( this . # tools . values ( ) ) ;
}
/ * *
* Registers a new tool with the tool registry .
2024-10-04 12:34:17 +02:00
* @ param { ToolRegistration } tool The tool to register .
2024-10-02 00:00:48 +02:00
* /
2024-10-09 02:53:32 +02:00
static registerFunctionTool ( { name , displayName , description , parameters , action , formatMessage , shouldRegister } ) {
2024-10-03 23:39:28 +02:00
// Convert WIP arguments
if ( typeof arguments [ 0 ] !== 'object' ) {
[ name , description , parameters , action ] = arguments ;
}
2024-10-02 00:00:48 +02:00
if ( this . # tools . has ( name ) ) {
2024-10-09 02:53:32 +02:00
console . warn ( ` [ToolManager] A tool with the name " ${ name } " has already been registered. The definition will be overwritten. ` ) ;
2024-10-02 00:00:48 +02:00
}
2024-10-09 02:53:32 +02:00
const definition = new ToolDefinition ( name , displayName , description , parameters , action , formatMessage , shouldRegister ) ;
2024-10-02 00:00:48 +02:00
this . # tools . set ( name , definition ) ;
2024-10-02 22:13:11 +02:00
console . log ( '[ToolManager] Registered function tool:' , definition ) ;
2024-10-02 00:00:48 +02:00
}
/ * *
* Removes a tool from the tool registry .
* @ param { string } name The name of the tool to unregister .
* /
static unregisterFunctionTool ( name ) {
if ( ! this . # tools . has ( name ) ) {
return ;
}
this . # tools . delete ( name ) ;
2024-10-02 22:13:11 +02:00
console . log ( ` [ToolManager] Unregistered function tool: ${ name } ` ) ;
2024-10-02 00:00:48 +02:00
}
/ * *
* Invokes a tool by name . Returns the result of the tool ' s action function .
* @ param { string } name The name of the tool to invoke .
* @ param { object } parameters Function parameters . For example , if the tool requires a "name" parameter , you would pass { name : "value" } .
2024-10-03 23:11:36 +02:00
* @ returns { Promise < string | Error > } The result of the tool ' s action function . If an error occurs , null is returned . Non - string results are JSON - stringified .
2024-10-02 00:00:48 +02:00
* /
static async invokeFunctionTool ( name , parameters ) {
try {
if ( ! this . # tools . has ( name ) ) {
throw new Error ( ` No tool with the name " ${ name } " has been registered. ` ) ;
}
const invokeParameters = typeof parameters === 'string' ? JSON . parse ( parameters ) : parameters ;
const tool = this . # tools . get ( name ) ;
const result = await tool . invoke ( invokeParameters ) ;
return typeof result === 'string' ? result : JSON . stringify ( result ) ;
} catch ( error ) {
2024-10-09 02:53:32 +02:00
console . error ( ` [ToolManager] An error occurred while invoking the tool " ${ name } ": ` , error ) ;
2024-10-03 23:11:36 +02:00
if ( error instanceof Error ) {
error . cause = name ;
2024-10-09 21:39:55 +02:00
return error . toString ( ) ;
2024-10-03 23:11:36 +02:00
}
2024-10-09 21:39:55 +02:00
return new Error ( 'Unknown error occurred while invoking the tool.' , { cause : name } ) . toString ( ) ;
2024-10-02 00:00:48 +02:00
}
}
2024-10-04 12:34:17 +02:00
/ * *
* Formats a message for a tool call by name .
* @ param { string } name The name of the tool to format the message for .
* @ param { object } parameters Function tool call parameters .
2024-10-09 02:56:24 +02:00
* @ returns { Promise < string > } The formatted message for the tool call .
2024-10-04 12:34:17 +02:00
* /
2024-10-09 02:56:24 +02:00
static async formatToolCallMessage ( name , parameters ) {
2024-10-03 23:39:28 +02:00
if ( ! this . # tools . has ( name ) ) {
return ` Invoked unknown tool: ${ name } ` ;
}
try {
const tool = this . # tools . get ( name ) ;
const formatParameters = typeof parameters === 'string' ? JSON . parse ( parameters ) : parameters ;
2024-10-09 02:56:24 +02:00
return await tool . formatMessage ( formatParameters ) ;
2024-10-03 23:39:28 +02:00
} catch ( error ) {
2024-10-09 02:53:32 +02:00
console . error ( ` [ToolManager] An error occurred while formatting the tool call message for " ${ name } ": ` , error ) ;
2024-10-03 23:39:28 +02:00
return ` Invoking tool: ${ name } ` ;
}
}
/ * *
* Gets the display name of a tool by name .
* @ param { string } name
* @ returns { string } The display name of the tool .
* /
static getDisplayName ( name ) {
if ( ! this . # tools . has ( name ) ) {
return name ;
}
const tool = this . # tools . get ( name ) ;
return tool . displayName || name ;
}
2024-10-02 00:00:48 +02:00
/ * *
* Register function tools for the next chat completion request .
* @ param { object } data Generation data
* /
static async registerFunctionToolsOpenAI ( data ) {
const tools = [ ] ;
for ( const tool of ToolManager . tools ) {
2024-10-09 02:53:32 +02:00
const register = await tool . shouldRegister ( ) ;
if ( ! register ) {
console . log ( '[ToolManager] Skipping tool registration:' , tool ) ;
continue ;
}
2024-10-02 00:00:48 +02:00
tools . push ( tool . toFunctionOpenAI ( ) ) ;
}
if ( tools . length ) {
2024-10-09 02:53:32 +02:00
console . log ( '[ToolManager] Registered function tools:' , tools ) ;
2024-10-02 00:00:48 +02:00
data [ 'tools' ] = tools ;
data [ 'tool_choice' ] = 'auto' ;
}
}
/ * *
* Utility function to parse tool calls from a parsed response .
* @ param { any [ ] } toolCalls The tool calls to update .
* @ param { any } parsed The parsed response from the OpenAI API .
* @ returns { void }
* /
static parseToolCalls ( toolCalls , parsed ) {
2024-10-09 12:49:02 +02:00
if ( ! this . isToolCallingSupported ( ) ) {
return ;
}
2024-10-04 12:39:08 +02:00
if ( Array . isArray ( parsed ? . choices ) ) {
for ( const choice of parsed . choices ) {
const choiceIndex = ( typeof choice . index === 'number' ) ? choice . index : null ;
const choiceDelta = choice . delta ;
2024-10-02 00:00:48 +02:00
2024-10-04 12:39:08 +02:00
if ( choiceIndex === null || ! choiceDelta ) {
continue ;
}
2024-10-02 00:00:48 +02:00
2024-10-04 12:39:08 +02:00
const toolCallDeltas = choiceDelta ? . tool _calls ;
2024-10-02 00:00:48 +02:00
2024-10-04 12:39:08 +02:00
if ( ! Array . isArray ( toolCallDeltas ) ) {
continue ;
}
2024-10-02 00:00:48 +02:00
2024-10-04 12:39:08 +02:00
if ( ! Array . isArray ( toolCalls [ choiceIndex ] ) ) {
toolCalls [ choiceIndex ] = [ ] ;
}
2024-10-02 00:00:48 +02:00
2024-10-04 12:39:08 +02:00
for ( const toolCallDelta of toolCallDeltas ) {
const toolCallIndex = ( typeof toolCallDelta ? . index === 'number' ) ? toolCallDelta . index : toolCallDeltas . indexOf ( toolCallDelta ) ;
2024-10-02 00:00:48 +02:00
2024-10-04 12:39:08 +02:00
if ( isNaN ( toolCallIndex ) || toolCallIndex < 0 ) {
continue ;
}
if ( toolCalls [ choiceIndex ] [ toolCallIndex ] === undefined ) {
toolCalls [ choiceIndex ] [ toolCallIndex ] = { } ;
}
const targetToolCall = toolCalls [ choiceIndex ] [ toolCallIndex ] ;
ToolManager . # applyToolCallDelta ( targetToolCall , toolCallDelta ) ;
}
}
}
2024-10-08 22:50:09 +02:00
const cohereToolEvents = [ 'message-start' , 'tool-call-start' , 'tool-call-delta' , 'tool-call-end' ] ;
if ( cohereToolEvents . includes ( parsed ? . type ) && typeof parsed ? . delta ? . message === 'object' ) {
const choiceIndex = 0 ;
const toolCallIndex = parsed ? . index ? ? 0 ;
if ( ! Array . isArray ( toolCalls [ choiceIndex ] ) ) {
toolCalls [ choiceIndex ] = [ ] ;
}
if ( toolCalls [ choiceIndex ] [ toolCallIndex ] === undefined ) {
toolCalls [ choiceIndex ] [ toolCallIndex ] = { } ;
}
const targetToolCall = toolCalls [ choiceIndex ] [ toolCallIndex ] ;
ToolManager . # applyToolCallDelta ( targetToolCall , parsed . delta . message ) ;
}
2024-10-04 12:39:08 +02:00
if ( typeof parsed ? . content _block === 'object' ) {
const choiceIndex = 0 ;
2024-10-04 13:31:15 +02:00
const toolCallIndex = parsed ? . index ? ? 0 ;
2024-10-04 12:39:08 +02:00
if ( parsed ? . content _block ? . type === 'tool_use' ) {
if ( ! Array . isArray ( toolCalls [ choiceIndex ] ) ) {
toolCalls [ choiceIndex ] = [ ] ;
2024-10-02 00:00:48 +02:00
}
if ( toolCalls [ choiceIndex ] [ toolCallIndex ] === undefined ) {
toolCalls [ choiceIndex ] [ toolCallIndex ] = { } ;
}
const targetToolCall = toolCalls [ choiceIndex ] [ toolCallIndex ] ;
2024-10-04 13:31:15 +02:00
ToolManager . # applyToolCallDelta ( targetToolCall , parsed . content _block ) ;
}
}
if ( typeof parsed ? . delta === 'object' ) {
const choiceIndex = 0 ;
const toolCallIndex = parsed ? . index ? ? 0 ;
const targetToolCall = toolCalls [ choiceIndex ] ? . [ toolCallIndex ] ;
2024-10-04 22:13:56 +02:00
if ( targetToolCall ) {
2024-10-04 13:31:15 +02:00
if ( parsed ? . delta ? . type === 'input_json_delta' ) {
const jsonDelta = parsed ? . delta ? . partial _json ;
if ( ! targetToolCall [ this . # INPUT _DELTA _KEY ] ) {
targetToolCall [ this . # INPUT _DELTA _KEY ] = '' ;
}
targetToolCall [ this . # INPUT _DELTA _KEY ] += jsonDelta ;
}
}
}
if ( parsed ? . type === 'content_block_stop' ) {
const choiceIndex = 0 ;
const toolCallIndex = parsed ? . index ? ? 0 ;
const targetToolCall = toolCalls [ choiceIndex ] ? . [ toolCallIndex ] ;
if ( targetToolCall ) {
const jsonDeltaString = targetToolCall [ this . # INPUT _DELTA _KEY ] ;
if ( jsonDeltaString ) {
try {
const jsonDelta = { input : JSON . parse ( jsonDeltaString ) } ;
delete targetToolCall [ this . # INPUT _DELTA _KEY ] ;
ToolManager . # applyToolCallDelta ( targetToolCall , jsonDelta ) ;
} catch ( error ) {
2024-10-09 02:53:32 +02:00
console . warn ( '[ToolManager] Failed to apply input JSON delta:' , error ) ;
2024-10-04 13:31:15 +02:00
}
}
2024-10-02 00:00:48 +02:00
}
}
}
2024-10-04 12:34:17 +02:00
/ * *
* Apply a tool call delta to a target object .
* @ param { object } target The target object to apply the delta to
* @ param { object } delta The delta object to apply
* /
2024-10-02 00:00:48 +02:00
static # applyToolCallDelta ( target , delta ) {
for ( const key in delta ) {
2024-10-04 12:34:17 +02:00
if ( ! Object . prototype . hasOwnProperty . call ( delta , key ) ) continue ;
2024-10-02 00:54:47 +02:00
if ( key === '__proto__' || key === 'constructor' ) continue ;
2024-10-02 00:00:48 +02:00
const deltaValue = delta [ key ] ;
const targetValue = target [ key ] ;
if ( deltaValue === null || deltaValue === undefined ) {
target [ key ] = deltaValue ;
continue ;
}
if ( typeof deltaValue === 'string' ) {
if ( typeof targetValue === 'string' ) {
// Concatenate strings
target [ key ] = targetValue + deltaValue ;
} else {
target [ key ] = deltaValue ;
}
} else if ( typeof deltaValue === 'object' && ! Array . isArray ( deltaValue ) ) {
if ( typeof targetValue !== 'object' || targetValue === null || Array . isArray ( targetValue ) ) {
target [ key ] = { } ;
}
// Recursively apply deltas to nested objects
ToolManager . # applyToolCallDelta ( target [ key ] , deltaValue ) ;
} else {
// Assign other types directly
target [ key ] = deltaValue ;
}
}
}
2024-10-02 21:17:27 +02:00
/ * *
* Checks if tool calling is supported for the current settings and generation type .
* @ returns { boolean } Whether tool calling is supported for the given type
* /
static isToolCallingSupported ( ) {
if ( main _api !== 'openai' || ! oai _settings . function _calling ) {
2024-10-02 00:00:48 +02:00
return false ;
}
const supportedSources = [
chat _completion _sources . OPENAI ,
chat _completion _sources . CUSTOM ,
chat _completion _sources . MISTRALAI ,
2024-10-04 02:41:25 +02:00
chat _completion _sources . CLAUDE ,
2024-10-02 00:00:48 +02:00
chat _completion _sources . OPENROUTER ,
chat _completion _sources . GROQ ,
2024-10-08 22:50:09 +02:00
chat _completion _sources . COHERE ,
2024-10-02 00:00:48 +02:00
] ;
return supportedSources . includes ( oai _settings . chat _completion _source ) ;
}
2024-10-02 21:17:27 +02:00
/ * *
* Checks if tool calls can be performed for the current settings and generation type .
* @ param { string } type Generation type
* @ returns { boolean } Whether tool calls can be performed for the given type
* /
static canPerformToolCalls ( type ) {
2024-10-06 21:25:23 +02:00
const noToolCallTypes = [ 'impersonate' , 'quiet' , 'continue' ] ;
2024-10-02 21:17:27 +02:00
const isSupported = ToolManager . isToolCallingSupported ( ) ;
return isSupported && ! noToolCallTypes . includes ( type ) ;
}
/ * *
* Utility function to get tool calls from the response data .
* @ param { any } data Response data
* @ returns { any [ ] } Tool calls from the response data
* /
2024-10-02 00:00:48 +02:00
static # getToolCallsFromData ( data ) {
2024-10-04 13:31:15 +02:00
const isClaudeToolCall = c => Array . isArray ( c ) ? c . filter ( x => x ) . every ( isClaudeToolCall ) : c ? . input && c ? . name && c ? . id ;
const convertClaudeToolCall = c => ( { id : c . id , function : { name : c . name , arguments : c . input } } ) ;
2024-10-02 00:00:48 +02:00
// Parsed tool calls from streaming data
2024-10-04 13:31:15 +02:00
if ( Array . isArray ( data ) && data . length > 0 && Array . isArray ( data [ 0 ] ) ) {
2024-10-08 22:50:09 +02:00
if ( isClaudeToolCall ( data [ 0 ] ) ) {
return data [ 0 ] . filter ( x => x ) . map ( convertClaudeToolCall ) ;
}
if ( typeof data [ 0 ] ? . [ 0 ] ? . tool _calls === 'object' ) {
return Array . isArray ( data [ 0 ] ? . [ 0 ] ? . tool _calls ) ? data [ 0 ] [ 0 ] . tool _calls : [ data [ 0 ] [ 0 ] . tool _calls ] ;
}
return data [ 0 ] ;
2024-10-02 00:00:48 +02:00
}
// Parsed tool calls from non-streaming data
2024-10-04 02:41:25 +02:00
if ( Array . isArray ( data ? . choices ) ) {
// Find a choice with 0-index
const choice = data . choices . find ( choice => choice . index === 0 ) ;
if ( choice ) {
return choice . message . tool _calls ;
}
2024-10-02 00:00:48 +02:00
}
2024-10-04 13:31:15 +02:00
// Claude tool calls to OpenAI tool calls
2024-10-04 02:41:25 +02:00
if ( Array . isArray ( data ? . content ) ) {
2024-10-04 13:31:15 +02:00
const content = data . content . filter ( c => c . type === 'tool_use' ) . map ( convertClaudeToolCall ) ;
if ( content ) {
return content ;
}
2024-10-02 00:00:48 +02:00
}
2024-10-08 22:50:09 +02:00
// Cohere tool calls
if ( typeof data ? . message ? . tool _calls === 'object' ) {
return Array . isArray ( data ? . message ? . tool _calls ) ? data . message . tool _calls : [ data . message . tool _calls ] ;
}
2024-10-02 00:00:48 +02:00
}
2024-10-04 22:13:56 +02:00
/ * *
* Checks if the response data contains tool calls .
* @ param { object } data Response data
* @ returns { boolean } Whether the response data contains tool calls
* /
static hasToolCalls ( data ) {
2024-10-04 23:20:06 +02:00
const toolCalls = ToolManager . # getToolCallsFromData ( data ) ;
return Array . isArray ( toolCalls ) && toolCalls . length > 0 ;
2024-10-04 22:13:56 +02:00
}
2024-10-02 00:00:48 +02:00
/ * *
* Check for function tool calls in the response data and invoke them .
* @ param { any } data Reply data
2024-10-03 23:11:36 +02:00
* @ returns { Promise < ToolInvocationResult > } Successful tool invocations
2024-10-02 00:00:48 +02:00
* /
2024-10-02 21:17:27 +02:00
static async invokeFunctionTools ( data ) {
2024-10-03 23:11:36 +02:00
/** @type {ToolInvocationResult} */
const result = {
invocations : [ ] ,
errors : [ ] ,
} ;
2024-10-02 00:00:48 +02:00
const toolCalls = ToolManager . # getToolCallsFromData ( data ) ;
2024-10-04 02:41:25 +02:00
if ( ! Array . isArray ( toolCalls ) ) {
return result ;
2024-10-02 00:00:48 +02:00
}
2024-10-04 02:41:25 +02:00
for ( const toolCall of toolCalls ) {
if ( typeof toolCall . function !== 'object' ) {
continue ;
2024-10-02 00:00:48 +02:00
}
2024-10-09 02:53:32 +02:00
console . log ( '[ToolManager] Function tool call:' , toolCall ) ;
2024-10-04 02:41:25 +02:00
const id = toolCall . id ;
const parameters = toolCall . function . arguments ;
const name = toolCall . function . name ;
const displayName = ToolManager . getDisplayName ( name ) ;
2024-10-09 02:56:24 +02:00
const message = await ToolManager . formatToolCallMessage ( name , parameters ) ;
2024-10-04 02:41:25 +02:00
const toast = message && toastr . info ( message , 'Tool Calling' , { timeOut : 0 } ) ;
const toolResult = await ToolManager . invokeFunctionTool ( name , parameters ) ;
toastr . clear ( toast ) ;
2024-10-09 02:53:32 +02:00
console . log ( '[ToolManager] Function tool result:' , result ) ;
2024-10-04 02:41:25 +02:00
// Save a successful invocation
if ( toolResult instanceof Error ) {
result . errors . push ( toolResult ) ;
continue ;
2024-10-02 00:00:48 +02:00
}
2024-10-04 02:41:25 +02:00
const invocation = {
id ,
displayName ,
name ,
2024-10-06 21:22:19 +02:00
parameters : stringify ( parameters ) ,
2024-10-04 02:41:25 +02:00
result : toolResult ,
} ;
result . invocations . push ( invocation ) ;
2024-10-02 00:00:48 +02:00
}
2024-10-03 23:11:36 +02:00
return result ;
2024-10-02 00:00:48 +02:00
}
2024-10-06 19:19:58 +02:00
/ * *
* Groups tool names by count .
* @ param { string [ ] } toolNames Tool names
* @ returns { string } Grouped tool names
* /
static # groupToolNames ( toolNames ) {
const toolCounts = toolNames . reduce ( ( acc , name ) => {
acc [ name ] = ( acc [ name ] || 0 ) + 1 ;
return acc ;
} , { } ) ;
return Object . entries ( toolCounts ) . map ( ( [ name , count ] ) => count > 1 ? ` ${ name } ( ${ count } ) ` : name ) . join ( ', ' ) ;
}
2024-10-02 22:13:11 +02:00
/ * *
* Formats a message with tool invocations .
* @ param { ToolInvocation [ ] } invocations Tool invocations .
* @ returns { string } Formatted message with tool invocations .
* /
2024-10-04 12:34:17 +02:00
static # formatToolInvocationMessage ( invocations ) {
2024-10-02 22:32:29 +02:00
const data = structuredClone ( invocations ) ;
2024-10-02 22:13:11 +02:00
const detailsElement = document . createElement ( 'details' ) ;
const summaryElement = document . createElement ( 'summary' ) ;
const preElement = document . createElement ( 'pre' ) ;
const codeElement = document . createElement ( 'code' ) ;
2024-10-02 22:32:29 +02:00
codeElement . classList . add ( 'language-json' ) ;
2024-10-05 19:54:37 +02:00
data . forEach ( i => {
i . parameters = tryParse ( i . parameters ) ;
i . result = tryParse ( i . result ) ;
} ) ;
2024-10-02 22:32:29 +02:00
codeElement . textContent = JSON . stringify ( data , null , 2 ) ;
2024-10-06 19:19:58 +02:00
const toolNames = data . map ( i => i . displayName || i . name ) ;
summaryElement . textContent = ` Tool calls: ${ this . # groupToolNames ( toolNames ) } ` ;
2024-10-02 22:13:11 +02:00
preElement . append ( codeElement ) ;
detailsElement . append ( summaryElement , preElement ) ;
return detailsElement . outerHTML ;
}
2024-10-02 00:00:48 +02:00
/ * *
* Saves function tool invocations to the last user chat message extra metadata .
2024-10-02 00:45:57 +02:00
* @ param { ToolInvocation [ ] } invocations Successful tool invocations
2024-10-02 00:00:48 +02:00
* /
2024-10-05 18:08:57 +02:00
static async saveFunctionToolInvocations ( invocations ) {
2024-10-04 02:41:25 +02:00
if ( ! Array . isArray ( invocations ) || invocations . length === 0 ) {
return ;
}
2024-10-02 21:17:27 +02:00
const message = {
name : systemUserName ,
force _avatar : system _avatar ,
is _system : true ,
is _user : false ,
2024-10-04 12:34:17 +02:00
mes : ToolManager . # formatToolInvocationMessage ( invocations ) ,
2024-10-02 21:17:27 +02:00
extra : {
isSmallSys : true ,
tool _invocations : invocations ,
} ,
} ;
chat . push ( message ) ;
2024-10-05 18:08:57 +02:00
await eventSource . emit ( event _types . TOOL _CALLS _PERFORMED , invocations ) ;
2024-10-02 21:17:27 +02:00
addOneMessage ( message ) ;
2024-10-05 18:08:57 +02:00
await eventSource . emit ( event _types . TOOL _CALLS _RENDERED , invocations ) ;
await saveChatConditional ( ) ;
2024-10-02 00:00:48 +02:00
}
2024-10-03 23:11:36 +02:00
/ * *
* Shows an error message for tool calls .
* @ param { Error [ ] } errors Errors that occurred during tool invocation
* @ returns { void }
* /
static showToolCallError ( errors ) {
toastr . error ( 'An error occurred while invoking function tools. Click here for more details.' , 'Tool Calling' , {
onclick : ( ) => Popup . show . text ( 'Tool Calling Errors' , DOMPurify . sanitize ( errors . map ( e => ` ${ e . cause } : ${ e . message } ` ) . join ( '<br>' ) ) ) ,
timeOut : 5000 ,
} ) ;
}
2024-10-06 11:49:08 +02:00
static initToolSlashCommands ( ) {
const toolsEnumProvider = ( ) => ToolManager . tools . map ( tool => {
const toolOpenAI = tool . toFunctionOpenAI ( ) ;
return new SlashCommandEnumValue ( toolOpenAI . function . name , toolOpenAI . function . description , enumTypes . enum , enumIcons . closure ) ;
} ) ;
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'tools-list' ,
aliases : [ 'tool-list' ] ,
helpString : 'Gets a list of all registered tools in the OpenAI function JSON format. Use the <code>return</code> argument to specify the return value type.' ,
returns : 'A list of all registered tools.' ,
namedArgumentList : [
SlashCommandNamedArgument . fromProps ( {
name : 'return' ,
description : 'The way how you want the return value to be provided' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
defaultValue : 'none' ,
enumList : slashCommandReturnHelper . enumList ( { allowObject : true } ) ,
forceEnum : true ,
} ) ,
] ,
callback : async ( args ) => {
/** @type {any} */
const returnType = String ( args ? . return ? ? 'popup-html' ) . trim ( ) . toLowerCase ( ) ;
2024-10-06 12:01:14 +02:00
const objectToStringFunc = ( tools ) => Array . isArray ( tools ) ? tools . map ( x => x . toString ( ) ) . join ( '\n\n' ) : tools . toString ( ) ;
2024-10-06 11:49:08 +02:00
const tools = ToolManager . tools . map ( tool => tool . toFunctionOpenAI ( ) ) ;
return await slashCommandReturnHelper . doReturn ( returnType ? ? 'popup-html' , tools ? ? [ ] , { objectToStringFunc } ) ;
} ,
} ) ) ;
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'tools-invoke' ,
aliases : [ 'tool-invoke' ] ,
helpString : 'Invokes a registered tool by name. The <code>parameters</code> argument MUST be a JSON-serialized object.' ,
namedArgumentList : [
SlashCommandNamedArgument . fromProps ( {
name : 'parameters' ,
description : 'The parameters to pass to the tool.' ,
typeList : [ ARGUMENT _TYPE . DICTIONARY ] ,
isRequired : true ,
acceptsMultiple : false ,
} ) ,
] ,
unnamedArgumentList : [
SlashCommandArgument . fromProps ( {
description : 'The name of the tool to invoke.' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
acceptsMultiple : false ,
forceEnum : true ,
enumProvider : toolsEnumProvider ,
} ) ,
] ,
callback : async ( args , name ) => {
const { parameters } = args ;
const result = await ToolManager . invokeFunctionTool ( String ( name ) , parameters ) ;
if ( result instanceof Error ) {
throw result ;
}
return result ;
} ,
} ) ) ;
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'tools-register' ,
aliases : [ 'tool-register' ] ,
helpString : ` <div>Registers a new tool with the tool registry.</div>
< ul >
< li > The < code > parameters < / c o d e > a r g u m e n t M U S T b e a J S O N - s e r i a l i z e d o b j e c t w i t h a v a l i d J S O N s c h e m a . < / l i >
< li > The unnamed argument MUST be a closure that accepts the function parameters as local script variables . < / l i >
< / u l >
< div > See < a target = "_blank" href = "https://json-schema.org/learn/" > json - schema . org < /a> and <a target="_blank" href="https:/ / platform . openai . com / docs / guides / function - calling " > OpenAI Function Calling < / a > f o r m o r e i n f o r m a t i o n . < / d i v >
< div > Example : < / d i v >
< pre > < code > / l e t k e y = e c h o S c h e m a
{
"$schema" : "http://json-schema.org/draft-04/schema#" ,
"type" : "object" ,
"properties" : {
"message" : {
"type" : "string" ,
"description" : "The message to echo."
}
} ,
"required" : [
"message"
]
}
||
/ t o o l s - r e g i s t e r n a m e = E c h o d e s c r i p t i o n = " E c h o e s a m e s s a g e . C a l l w h e n t h e u s e r i s a s k i n g t o r e p e a t s o m e t h i n g " p a r a m e t e r s = { { v a r : : e c h o S c h e m a } } { : / e c h o { { v a r : : a r g . m e s s a g e } } : } < / c o d e > < / p r e > ` ,
namedArgumentList : [
SlashCommandNamedArgument . fromProps ( {
name : 'name' ,
description : 'The name of the tool.' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
acceptsMultiple : false ,
} ) ,
SlashCommandNamedArgument . fromProps ( {
name : 'description' ,
description : 'A description of what the tool does.' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
acceptsMultiple : false ,
} ) ,
SlashCommandNamedArgument . fromProps ( {
name : 'parameters' ,
description : 'The parameters for the tool.' ,
typeList : [ ARGUMENT _TYPE . DICTIONARY ] ,
isRequired : true ,
acceptsMultiple : false ,
} ) ,
SlashCommandNamedArgument . fromProps ( {
name : 'displayName' ,
description : 'The display name of the tool.' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : false ,
acceptsMultiple : false ,
} ) ,
SlashCommandNamedArgument . fromProps ( {
name : 'formatMessage' ,
description : 'The closure to be executed to format the tool call message. Must return a string.' ,
typeList : [ ARGUMENT _TYPE . CLOSURE ] ,
isRequired : true ,
acceptsMultiple : false ,
} ) ,
] ,
unnamedArgumentList : [
SlashCommandArgument . fromProps ( {
description : 'The closure to be executed when the tool is invoked.' ,
typeList : [ ARGUMENT _TYPE . CLOSURE ] ,
isRequired : true ,
acceptsMultiple : false ,
} ) ,
] ,
callback : async ( args , action ) => {
/ * *
* Converts a slash command closure to a function .
* @ param { SlashCommandClosure } action Closure to convert to a function
* @ returns { function } Function that executes the closure
* /
function closureToFunction ( action ) {
return async ( args ) => {
const localClosure = action . getCopy ( ) ;
localClosure . onProgress = ( ) => { } ;
const scope = localClosure . scope ;
if ( typeof args === 'object' && args !== null ) {
assignNestedVariables ( scope , args , 'arg' ) ;
} else if ( typeof args !== 'undefined' ) {
scope . letVariable ( 'arg' , args ) ;
}
const result = await localClosure . execute ( ) ;
return result . pipe ;
} ;
}
const { name , displayName , description , parameters , formatMessage } = args ;
if ( ! ( action instanceof SlashCommandClosure ) ) {
throw new Error ( 'The unnamed argument must be a closure.' ) ;
}
if ( typeof name !== 'string' || ! name ) {
throw new Error ( 'The "name" argument must be a non-empty string.' ) ;
}
if ( typeof description !== 'string' || ! description ) {
throw new Error ( 'The "description" argument must be a non-empty string.' ) ;
}
if ( typeof parameters !== 'string' || ! isJson ( parameters ) ) {
throw new Error ( 'The "parameters" argument must be a JSON-serialized object.' ) ;
}
if ( displayName && typeof displayName !== 'string' ) {
throw new Error ( 'The "displayName" argument must be a string.' ) ;
}
if ( formatMessage && ! ( formatMessage instanceof SlashCommandClosure ) ) {
throw new Error ( 'The "formatMessage" argument must be a closure.' ) ;
}
const actionFunc = closureToFunction ( action ) ;
const formatMessageFunc = formatMessage instanceof SlashCommandClosure ? closureToFunction ( formatMessage ) : null ;
ToolManager . registerFunctionTool ( {
name : String ( name ? ? '' ) ,
displayName : String ( displayName ? ? '' ) ,
description : String ( description ? ? '' ) ,
parameters : JSON . parse ( parameters ? ? '{}' ) ,
action : actionFunc ,
formatMessage : formatMessageFunc ,
2024-10-09 02:56:24 +02:00
shouldRegister : async ( ) => true , // TODO: Implement shouldRegister
2024-10-06 11:49:08 +02:00
} ) ;
return '' ;
} ,
} ) ) ;
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'tools-unregister' ,
aliases : [ 'tool-unregister' ] ,
helpString : 'Unregisters a tool from the tool registry.' ,
unnamedArgumentList : [
SlashCommandArgument . fromProps ( {
description : 'The name of the tool to unregister.' ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
isRequired : true ,
acceptsMultiple : false ,
forceEnum : true ,
enumProvider : toolsEnumProvider ,
} ) ,
] ,
callback : async ( name ) => {
if ( typeof name !== 'string' || ! name ) {
throw new Error ( 'The unnamed argument must be a non-empty string.' ) ;
}
ToolManager . unregisterFunctionTool ( name ) ;
return '' ;
} ,
} ) ) ;
}
2024-10-02 00:00:48 +02:00
}