Merge branch 'staging' into feat/AdditionalLogins

5765ea8500f7ac99e8b03dba538468f0652a5ae4

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

32 files changed, +1584 -506Showing whitespace changes
.eslintrc.js+11 -8
@@ -59,15 +59,18 @@ module.exports = {
59 },59 },
60 },60 },
61 ],61 ],
62 // There are various vendored libraries that shouldn't be linted
63 ignorePatterns: [62 ignorePatterns: [
64 'public/lib/**/*',63 '**/node_modules/**',
65 '*.min.js',64 '**/dist/**',
66 'src/ai_horde/**/*',65 '**/.git/**',
67 'plugins/**/*',66 'public/lib/**',
68 'data/**/*',67 'backups/**',
69 'backups/**/*',68 'data/**',
70 'node_modules/**/*',69 'cache/**',
70 'src/tokenizers/**',
71 'docker/**',
72 'plugins/**',
73 '**/*.min.js',
71 ],74 ],
72 rules: {75 rules: {
73 'no-unused-vars': ['error', { args: 'none' }],76 'no-unused-vars': ['error', { args: 'none' }],
default/config.yaml+2 -0
@@ -134,6 +134,8 @@ extras:
134 speechToTextModel: Xenova/whisper-small134 speechToTextModel: Xenova/whisper-small
135 textToSpeechModel: Xenova/speecht5_tts135 textToSpeechModel: Xenova/speecht5_tts
136# -- OPENAI CONFIGURATION --136# -- OPENAI CONFIGURATION --
137# A placeholder message to use in strict prompt post-processing mode when the prompt doesn't start with a user message
138promptPlaceholder: "[Start a new chat]"
137openai:139openai:
138 # Will send a random user ID to OpenAI completion API140 # Will send a random user ID to OpenAI completion API
139 randomizeUserId: false141 randomizeUserId: false
jsconfig.json+9 -10
@@ -11,15 +11,14 @@
11 "resolveJsonModule": true11 "resolveJsonModule": true
12 },12 },
13 "exclude": [13 "exclude": [
14 "node_modules",14 "**/node_modules/**",
15 "**/node_modules/*",15 "**/dist/**",
16 "public/lib",16 "**/.git/**",
17 "backups/*",17 "public/lib/**",
18 "data/*",18 "backups/**",
19 "**/dist/*",19 "data/**",
20 "dist/*",20 "cache/**",
21 "cache/*",21 "src/tokenizers/**",
22 "src/tokenizers/*",22 "docker/**"
23 "docker/*",
24 ]23 ]
25}24}
package-lock.json+34 -12
@@ -59,9 +59,10 @@
59 "sillytavern": "server.js"59 "sillytavern": "server.js"
60 },60 },
61 "devDependencies": {61 "devDependencies": {
62 "@types/dompurify": "^3.0.5",
62 "@types/jquery": "^3.5.29",63 "@types/jquery": "^3.5.29",
63 "eslint": "^8.57.0",64 "@types/toastr": "^2.1.43",
64 "jquery": "^3.6.4"65 "eslint": "^8.57.0"
65 },66 },
66 "engines": {67 "engines": {
67 "node": ">= 18"68 "node": ">= 18"
@@ -960,6 +961,16 @@
960 "@types/responselike": "^1.0.0"961 "@types/responselike": "^1.0.0"
961 }962 }
962 },963 },
964 "node_modules/@types/dompurify": {
965 "version": "3.0.5",
966 "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
967 "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
968 "dev": true,
969 "license": "MIT",
970 "dependencies": {
971 "@types/trusted-types": "*"
972 }
973 },
963 "node_modules/@types/http-cache-semantics": {974 "node_modules/@types/http-cache-semantics": {
964 "version": "4.0.2",975 "version": "4.0.2",
965 "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz",976 "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz",
@@ -967,10 +978,11 @@
967 "license": "MIT"978 "license": "MIT"
968 },979 },
969 "node_modules/@types/jquery": {980 "node_modules/@types/jquery": {
970 "version": "3.5.29",981 "version": "3.5.31",
971 "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.29.tgz",982 "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.31.tgz",
972 "integrity": "sha512-oXQQC9X9MOPRrMhPHHOsXqeQDnWeCDT3PelUIg/Oy8FAbzSZtFHRjc7IpbfFVmpLtJ+UOoywpRsuO5Jxjybyeg==",983 "integrity": "sha512-rf/iB+cPJ/YZfMwr+FVuQbm7IaWC4y3FVYfVDxRGqmUCFjjPII0HWaP0vTPJGp6m4o13AXySCcMbWfrWtBFAKw==",
973 "dev": true,984 "dev": true,
985 "license": "MIT",
974 "dependencies": {986 "dependencies": {
975 "@types/sizzle": "*"987 "@types/sizzle": "*"
976 }988 }
@@ -1021,6 +1033,23 @@
1021 "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==",1033 "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==",
1022 "dev": true1034 "dev": true
1023 },1035 },
1036 "node_modules/@types/toastr": {
1037 "version": "2.1.43",
1038 "resolved": "https://registry.npmjs.org/@types/toastr/-/toastr-2.1.43.tgz",
1039 "integrity": "sha512-sLC2fr2OXeE1iyhUixpQ64wQ2tA26awmLidn4tXTLBz4yP/VhtYUKHpmiIyDtztKkHjucdiTLH8F5uRRyhNi2Q==",
1040 "dev": true,
1041 "license": "MIT",
1042 "dependencies": {
1043 "@types/jquery": "*"
1044 }
1045 },
1046 "node_modules/@types/trusted-types": {
1047 "version": "2.0.7",
1048 "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
1049 "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
1050 "dev": true,
1051 "license": "MIT"
1052 },
1024 "node_modules/@ungap/structured-clone": {1053 "node_modules/@ungap/structured-clone": {
1025 "version": "1.2.0",1054 "version": "1.2.0",
1026 "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",1055 "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@@ -3881,13 +3910,6 @@
3881 "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",3910 "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
3882 "license": "BSD-3-Clause"3911 "license": "BSD-3-Clause"
3883 },3912 },
3884 "node_modules/jquery": {
3885 "version": "3.7.0",
3886 "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz",
3887 "integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==",
3888 "dev": true,
3889 "license": "MIT"
3890 },
3891 "node_modules/js-yaml": {3913 "node_modules/js-yaml": {
3892 "version": "4.1.0",3914 "version": "4.1.0",
3893 "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",3915 "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
package.json+3 -2
@@ -85,8 +85,9 @@
85 },85 },
86 "main": "server.js",86 "main": "server.js",
87 "devDependencies": {87 "devDependencies": {
88 "@types/dompurify": "^3.0.5",
88 "@types/jquery": "^3.5.29",89 "@types/jquery": "^3.5.29",
89 "eslint": "^8.57.0",90 "@types/toastr": "^2.1.43",
90 "jquery": "^3.6.4"91 "eslint": "^8.57.0"
91 }92 }
92}93}
public/css/select2-overrides.css+2 -1
@@ -17,7 +17,7 @@
1717
18.select2-container .select2-selection .select2-selection__clear {18.select2-container .select2-selection .select2-selection__clear {
19 color: var(--SmartThemeBodyColor);19 color: var(--SmartThemeBodyColor);
20 font-size: 24px;20 font-size: 20px;
21 padding: 0;21 padding: 0;
22 position: absolute;22 position: absolute;
23 right: 5px;23 right: 5px;
@@ -50,6 +50,7 @@
50 padding: revert;50 padding: revert;
51 border-right: 1px solid var(--SmartThemeBorderColor);51 border-right: 1px solid var(--SmartThemeBorderColor);
52 font-size: 1.1em;52 font-size: 1.1em;
53 line-height: 1;
53}54}
5455
55.select2-container .select2-selection--multiple .select2-selection__choice__display {56.select2-container .select2-selection--multiple .select2-selection__choice__display {
public/css/st-tailwind.css+4 -0
@@ -612,3 +612,7 @@ ul.li-padding-bot5 li {
612ul.li-padding-bot10 li {612ul.li-padding-bot10 li {
613 padding-bottom: 10px;613 padding-bottom: 10px;
614}614}
615
616.wordBreakAll {
617 word-break: break-all;
618}
public/css/toggle-dependent.css+11 -0
@@ -463,3 +463,14 @@ body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint {
463label[for="trim_spaces"]:has(input:checked) i.warning {463label[for="trim_spaces"]:has(input:checked) i.warning {
464 display: none;464 display: none;
465}465}
466
467#claude_function_prefill_warning {
468 display: none;
469}
470
471#openai_settings:has(#openai_function_calling:checked):has(#claude_assistant_prefill:not(:placeholder-shown), #claude_assistant_impersonation:not(:placeholder-shown)) #claude_function_prefill_warning {
472 display: flex;
473 align-items: center;
474 gap: 5px;
475 margin: 10px 0;
476}
public/global.d.ts+0 -42
@@ -1,5 +1,4 @@
1// Global namespace modules1// Global namespace modules
2declare var DOMPurify;
3declare var droll;2declare var droll;
4declare var Handlebars;3declare var Handlebars;
5declare var hljs;4declare var hljs;
@@ -1365,44 +1364,3 @@ declare namespace moment {
1365declare global {1364declare global {
1366 const moment: typeof moment;1365 const moment: typeof moment;
1367}1366}
1368
1369/**
1370 * Callback data for the `LLM_FUNCTION_TOOL_REGISTER` event type that is triggered when a function tool can be registered.
1371 */
1372interface 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 */
1394declare 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 */
1399interface 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/index.html+6 -1
@@ -1873,6 +1873,10 @@
1873 </div>1873 </div>
1874 <textarea id="claude_assistant_impersonation" class="text_pole textarea_compact autoSetHeight" name="assistant_impersonation" rows="2" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>1874 <textarea id="claude_assistant_impersonation" class="text_pole textarea_compact autoSetHeight" name="assistant_impersonation" rows="2" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
1875 </div>1875 </div>
1876 <div id="claude_function_prefill_warning">
1877 <i class="fa-solid fa-circle-info"></i>
1878 <span>Prefills won't work when function calling is enabled and any tools are registered.</span>
1879 </div>
1876 <label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand">1880 <label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand">
1877 <input id="claude_use_sysprompt" type="checkbox" />1881 <input id="claude_use_sysprompt" type="checkbox" />
1878 <span data-i18n="Use system prompt (Claude 2.1+ only)">1882 <span data-i18n="Use system prompt (Claude 2.1+ only)">
@@ -3098,7 +3102,8 @@
3098 <h4 data-i18n="Prompt Post-Processing">Prompt Post-Processing</h4>3102 <h4 data-i18n="Prompt Post-Processing">Prompt Post-Processing</h4>
3099 <select id="custom_prompt_post_processing" class="text_pole" title="Applies additional processing to the prompt before sending it to the API." data-i18n="[title]Applies additional processing to the prompt before sending it to the API.">3103 <select id="custom_prompt_post_processing" class="text_pole" title="Applies additional processing to the prompt before sending it to the API." data-i18n="[title]Applies additional processing to the prompt before sending it to the API.">
3100 <option data-i18n="prompt_post_processing_none" value="">None</option>3104 <option data-i18n="prompt_post_processing_none" value="">None</option>
3101 <option value="claude">Claude</option>3105 <option value="merge">Merge consecutive roles</option>
3106 <option value="strict">Strict (user first, alternating roles)</option>
3102 </select>3107 </select>
3103 </form>3108 </form>
3104 <div id="01ai_form" data-source="01ai">3109 <div id="01ai_form" data-source="01ai">
public/jsconfig.json+5 -5
@@ -8,16 +8,16 @@
8 "allowSyntheticDefaultImports": true8 "allowSyntheticDefaultImports": true
9 },9 },
10 "exclude": [10 "exclude": [
11 "node_modules"11 "**/node_modules/**",
12 "**/dist/**",
13 "**/.git/**",
14 "lib/**",
15 "**/*.min.js"
12 ],16 ],
13 "typeAcquisition": {17 "typeAcquisition": {
14 "include": [18 "include": [
15 "jquery",
16 "@popperjs/core",19 "@popperjs/core",
17 "toastr",
18 "showdown",20 "showdown",
19 "dompurify",
20 "moment",
21 "seedrandom",21 "seedrandom",
22 "showdown-katex",22 "showdown-katex",
23 "droll",23 "droll",
public/script.js+78 -10
@@ -246,6 +246,7 @@ import { initInputMarkdown } from './scripts/input-md-formatting.js';
246import { AbortReason } from './scripts/util/AbortReason.js';246import { AbortReason } from './scripts/util/AbortReason.js';
247import { initSystemPrompts } from './scripts/sysprompt.js';247import { initSystemPrompts } from './scripts/sysprompt.js';
248import { registerExtensionSlashCommands as initExtensionSlashCommands } from './scripts/extensions-slashcommands.js';248import { registerExtensionSlashCommands as initExtensionSlashCommands } from './scripts/extensions-slashcommands.js';
249import { ToolManager } from './scripts/tool-calling.js';
249250
250//exporting functions and vars for mods251//exporting functions and vars for mods
251export {252export {
@@ -470,11 +471,11 @@ export const event_types = {
470 FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',471 FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',
471 WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',472 WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',
472 OPEN_CHARACTER_LIBRARY: 'open_character_library',473 OPEN_CHARACTER_LIBRARY: 'open_character_library',
473 LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register',
474 LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call',
475 ONLINE_STATUS_CHANGED: 'online_status_changed',474 ONLINE_STATUS_CHANGED: 'online_status_changed',
476 IMAGE_SWIPED: 'image_swiped',475 IMAGE_SWIPED: 'image_swiped',
477 CONNECTION_PROFILE_LOADED: 'connection_profile_loaded',476 CONNECTION_PROFILE_LOADED: 'connection_profile_loaded',
477 TOOL_CALLS_PERFORMED: 'tool_calls_performed',
478 TOOL_CALLS_RENDERED: 'tool_calls_rendered',
478};479};
479480
480export const eventSource = new EventEmitter();481export const eventSource = new EventEmitter();
@@ -947,6 +948,7 @@ async function firstLoadInit() {
947 initSystemPrompts();948 initSystemPrompts();
948 initExtensions();949 initExtensions();
949 initExtensionSlashCommands();950 initExtensionSlashCommands();
951 ToolManager.initToolSlashCommands();
950 await initPresetManager();952 await initPresetManager();
951 await getSystemMessages();953 await getSystemMessages();
952 sendSystemMessage(system_message_types.WELCOME);954 sendSystemMessage(system_message_types.WELCOME);
@@ -2027,7 +2029,7 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
2027 // Return the original match if no quotes are found2029 // Return the original match if no quotes are found
2028 return match;2030 return match;
2029 }2031 }
2030 }2032 },
2031 );2033 );
20322034
2033 // Restore double quotes in tags2035 // Restore double quotes in tags
@@ -2372,6 +2374,10 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2372 newMessage.addClass('smallSysMes');2374 newMessage.addClass('smallSysMes');
2373 }2375 }
23742376
2377 if (Array.isArray(mes?.extra?.tool_invocations)) {
2378 newMessage.addClass('toolCall');
2379 }
2380
2375 //shows or hides the Prompt display button2381 //shows or hides the Prompt display button
2376 let mesIdToFind = type === 'swipe' ? params.mesId - 1 : params.mesId; //Number(newMessage.attr('mesId'));2382 let mesIdToFind = type === 'swipe' ? params.mesId - 1 : params.mesId; //Number(newMessage.attr('mesId'));
23772383
@@ -2965,6 +2971,7 @@ class StreamingProcessor {
2965 this.swipes = [];2971 this.swipes = [];
2966 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */2972 /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
2967 this.messageLogprobs = [];2973 this.messageLogprobs = [];
2974 this.toolCalls = [];
2968 }2975 }
29692976
2970 #checkDomElements(messageId) {2977 #checkDomElements(messageId) {
@@ -2976,6 +2983,13 @@ class StreamingProcessor {
2976 }2983 }
2977 }2984 }
29782985
2986 #updateMessageBlockVisibility() {
2987 if (this.messageDom instanceof HTMLElement && Array.isArray(this.toolCalls) && this.toolCalls.length > 0) {
2988 const shouldHide = ['', '...'].includes(this.result);
2989 this.messageDom.classList.toggle('displayNone', shouldHide);
2990 }
2991 }
2992
2979 showMessageButtons(messageId) {2993 showMessageButtons(messageId) {
2980 if (messageId == -1) {2994 if (messageId == -1) {
2981 return;2995 return;
@@ -3041,6 +3055,7 @@ class StreamingProcessor {
3041 }3055 }
3042 else {3056 else {
3043 this.#checkDomElements(messageId);3057 this.#checkDomElements(messageId);
3058 this.#updateMessageBlockVisibility();
3044 const currentTime = new Date();3059 const currentTime = new Date();
3045 // Don't waste time calculating token count for streaming3060 // Don't waste time calculating token count for streaming
3046 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0;3061 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0;
@@ -3183,7 +3198,7 @@ class StreamingProcessor {
3183 }3198 }
31843199
3185 /**3200 /**
3186 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs }, void, void>}3201 * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[] }, void, void>}
3187 */3202 */
3188 *nullStreamingGeneration() {3203 *nullStreamingGeneration() {
3189 throw new Error('Generation function for streaming is not hooked up');3204 throw new Error('Generation function for streaming is not hooked up');
@@ -3205,12 +3220,13 @@ class StreamingProcessor {
3205 try {3220 try {
3206 const sw = new Stopwatch(1000 / power_user.streaming_fps);3221 const sw = new Stopwatch(1000 / power_user.streaming_fps);
3207 const timestamps = [];3222 const timestamps = [];
3208 for await (const { text, swipes, logprobs } of this.generator()) {3223 for await (const { text, swipes, logprobs, toolCalls } of this.generator()) {
3209 timestamps.push(Date.now());3224 timestamps.push(Date.now());
3210 if (this.isStopped) {3225 if (this.isStopped) {
3211 return;3226 return;
3212 }3227 }
32133228
3229 this.toolCalls = toolCalls;
3214 this.result = text;3230 this.result = text;
3215 this.swipes = Array.from(swipes ?? []);3231 this.swipes = Array.from(swipes ?? []);
3216 if (logprobs) {3232 if (logprobs) {
@@ -3393,9 +3409,9 @@ function removeLastMessage() {
3393 * @param {GenerateOptions} options Generation options3409 * @param {GenerateOptions} options Generation options
3394 * @param {boolean} dryRun Whether to actually generate a message or just assemble the prompt3410 * @param {boolean} dryRun Whether to actually generate a message or just assemble the prompt
3395 * @returns {Promise<any>} Returns a promise that resolves when the text is done generating.3411 * @returns {Promise<any>} Returns a promise that resolves when the text is done generating.
3396 * @typedef {{automatic_trigger?: boolean, force_name2?: boolean, quiet_prompt?: string, quietToLoud?: boolean, skipWIAN?: boolean, force_chid?: number, signal?: AbortSignal, quietImage?: string, quietName?: string }} GenerateOptions3412 * @typedef {{automatic_trigger?: boolean, force_name2?: boolean, quiet_prompt?: string, quietToLoud?: boolean, skipWIAN?: boolean, force_chid?: number, signal?: AbortSignal, quietImage?: string, quietName?: string, depth?: number }} GenerateOptions
3397 */3413 */
3398export async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName } = {}, dryRun = false) {3414export async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName, depth = 0 } = {}, dryRun = false) {
3399 console.log('Generate entered');3415 console.log('Generate entered');
3400 setGenerationProgress(0);3416 setGenerationProgress(0);
3401 generation_started = new Date();3417 generation_started = new Date();
@@ -3614,7 +3630,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
3614 }3630 }
36153631
3616 // Collect messages with usable content3632 // Collect messages with usable content
3617 let coreChat = chat.filter(x => !x.is_system);3633 const canUseTools = ToolManager.isToolCallingSupported();
3634 const canPerformToolCalls = !dryRun && ToolManager.canPerformToolCalls(type) && depth < ToolManager.RECURSE_LIMIT;
3635 let coreChat = chat.filter(x => !x.is_system || (canUseTools && Array.isArray(x.extra?.tool_invocations)));
3618 if (type === 'swipe') {3636 if (type === 'swipe') {
3619 coreChat.pop();3637 coreChat.pop();
3620 }3638 }
@@ -4449,7 +4467,31 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4449 getMessage = continue_mag + getMessage;4467 getMessage = continue_mag + getMessage;
4450 }4468 }
44514469
4452 if (streamingProcessor && !streamingProcessor.isStopped && streamingProcessor.isFinished) {4470 const isStreamFinished = streamingProcessor && !streamingProcessor.isStopped && streamingProcessor.isFinished;
4471 const isStreamWithToolCalls = streamingProcessor && Array.isArray(streamingProcessor.toolCalls) && streamingProcessor.toolCalls.length;
4472 if (canPerformToolCalls && isStreamFinished && isStreamWithToolCalls) {
4473 const lastMessage = chat[chat.length - 1];
4474 const hasToolCalls = ToolManager.hasToolCalls(streamingProcessor.toolCalls);
4475 const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(lastMessage?.mes) && ['', '...'].includes(streamingProcessor?.result);
4476 hasToolCalls && shouldDeleteMessage && await deleteLastMessage();
4477 const invocationResult = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls);
4478 if (hasToolCalls) {
4479 if (!invocationResult.invocations.length && shouldDeleteMessage) {
4480 ToolManager.showToolCallError(invocationResult.errors);
4481 unblockGeneration(type);
4482 generatedPromptCache = '';
4483 streamingProcessor = null;
4484 return;
4485 }
4486
4487 streamingProcessor = null;
4488 depth = depth + 1;
4489 await ToolManager.saveFunctionToolInvocations(invocationResult.invocations);
4490 return Generate('normal', { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName, depth }, dryRun);
4491 }
4492 }
4493
4494 if (isStreamFinished) {
4453 await streamingProcessor.onFinishStreaming(streamingProcessor.messageId, getMessage);4495 await streamingProcessor.onFinishStreaming(streamingProcessor.messageId, getMessage);
4454 streamingProcessor = null;4496 streamingProcessor = null;
4455 triggerAutoContinue(messageChunk, isImpersonate);4497 triggerAutoContinue(messageChunk, isImpersonate);
@@ -4523,6 +4565,25 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4523 parseAndSaveLogprobs(data, continue_mag);4565 parseAndSaveLogprobs(data, continue_mag);
4524 }4566 }
45254567
4568 if (canPerformToolCalls) {
4569 const hasToolCalls = ToolManager.hasToolCalls(data);
4570 const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(getMessage);
4571 hasToolCalls && shouldDeleteMessage && await deleteLastMessage();
4572 const invocationResult = await ToolManager.invokeFunctionTools(data);
4573 if (hasToolCalls) {
4574 if (!invocationResult.invocations.length && shouldDeleteMessage) {
4575 ToolManager.showToolCallError(invocationResult.errors);
4576 unblockGeneration(type);
4577 generatedPromptCache = '';
4578 return;
4579 }
4580
4581 depth = depth + 1;
4582 await ToolManager.saveFunctionToolInvocations(invocationResult.invocations);
4583 return Generate('normal', { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName, depth }, dryRun);
4584 }
4585 }
4586
4526 if (type !== 'quiet') {4587 if (type !== 'quiet') {
4527 playMessageSound();4588 playMessageSound();
4528 }4589 }
@@ -6737,7 +6798,10 @@ function updateMessage(div) {
6737 text = getRegexedString(6798 text = getRegexedString(
6738 text,6799 text,
6739 regexPlacement,6800 regexPlacement,
6740 { characterOverride: mes.extra?.type === 'narrator' ? undefined : mes.name },6801 {
6802 characterOverride: mes.extra?.type === 'narrator' ? undefined : mes.name,
6803 isEdit: true,
6804 },
6741 );6805 );
67426806
67436807
@@ -8174,6 +8238,10 @@ window['SillyTavern'].getContext = function () {
8174 registerHelper: () => { },8238 registerHelper: () => { },
8175 registerMacro: MacrosParser.registerMacro.bind(MacrosParser),8239 registerMacro: MacrosParser.registerMacro.bind(MacrosParser),
8176 unregisterMacro: MacrosParser.unregisterMacro.bind(MacrosParser),8240 unregisterMacro: MacrosParser.unregisterMacro.bind(MacrosParser),
8241 registerFunctionTool: ToolManager.registerFunctionTool.bind(ToolManager),
8242 unregisterFunctionTool: ToolManager.unregisterFunctionTool.bind(ToolManager),
8243 isToolCallingSupported: ToolManager.isToolCallingSupported.bind(ToolManager),
8244 canPerformToolCalls: ToolManager.canPerformToolCalls.bind(ToolManager),
8177 registerDebugFunction: registerDebugFunction,8245 registerDebugFunction: registerDebugFunction,
8178 /** @deprecated Use renderExtensionTemplateAsync instead. */8246 /** @deprecated Use renderExtensionTemplateAsync instead. */
8179 renderExtensionTemplate: renderExtensionTemplate,8247 renderExtensionTemplate: renderExtensionTemplate,
public/scripts/extensions/assets/index.js+9 -0
@@ -184,6 +184,7 @@ function downloadAssetsList(url) {
184 const url = isValidUrl(asset['url']) ? asset['url'] : '';184 const url = isValidUrl(asset['url']) ? asset['url'] : '';
185 const title = assetType === 'extension' ? `Extension repo/guide: ${url}` : 'Preview in browser';185 const title = assetType === 'extension' ? `Extension repo/guide: ${url}` : 'Preview in browser';
186 const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';186 const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
187 const toolTag = assetType === 'extension' && asset['tool'];
187188
188 const assetBlock = $('<i></i>')189 const assetBlock = $('<i></i>')
189 .append(element)190 .append(element)
@@ -193,12 +194,20 @@ function downloadAssetsList(url) {
193 <a class="asset_preview" href="${url}" target="_blank" title="${title}">194 <a class="asset_preview" href="${url}" target="_blank" title="${title}">
194 <i class="fa-solid fa-sm ${previewIcon}"></i>195 <i class="fa-solid fa-sm ${previewIcon}"></i>
195 </a>196 </a>
197 ${toolTag ? '<span class="tag" title="Adds a function tool"><i class="fa-solid fa-sm fa-wrench"></i> Tool</span>' : ''}
196 </span>198 </span>
197 <small class="asset-description">199 <small class="asset-description">
198 ${description}200 ${description}
199 </small>201 </small>
200 </div>`);202 </div>`);
201203
204 assetBlock.find('.tag').on('click', function (e) {
205 const a = document.createElement('a');
206 a.href = 'https://docs.sillytavern.app/for-contributors/function-calling/';
207 a.target = '_blank';
208 a.click();
209 });
210
202 if (assetType === 'character') {211 if (assetType === 'character') {
203 if (asset.highlight) {212 if (asset.highlight) {
204 assetBlock.find('.asset-name').append('<i class="fa-solid fa-sm fa-trophy"></i>');213 assetBlock.find('.asset-name').append('<i class="fa-solid fa-sm fa-trophy"></i>');
public/scripts/extensions/assets/style.css+9 -0
@@ -164,3 +164,12 @@
164 gap: 5px;164 gap: 5px;
165 align-items: center;165 align-items: center;
166}166}
167
168.asset-name .tag {
169 gap: 5px;
170 align-items: baseline;
171 font-size: calc(var(--mainFontSize)* 0.8);
172 cursor: pointer;
173 opacity: 0.9;
174 margin-left: 2px;
175}
public/scripts/extensions/expressions/index.js+1 -51
@@ -9,7 +9,6 @@ import { debounce_timeout } from '../../constants.js';
9import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';9import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
10import { SlashCommand } from '../../slash-commands/SlashCommand.js';10import { SlashCommand } from '../../slash-commands/SlashCommand.js';
11import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';11import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
12import { isFunctionCallingSupported } from '../../openai.js';
13import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';12import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
14import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';13import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
15import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';14import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
@@ -21,7 +20,6 @@ const UPDATE_INTERVAL = 2000;
21const STREAMING_UPDATE_INTERVAL = 10000;20const STREAMING_UPDATE_INTERVAL = 10000;
22const TALKINGCHECK_UPDATE_INTERVAL = 500;21const TALKINGCHECK_UPDATE_INTERVAL = 500;
23const DEFAULT_FALLBACK_EXPRESSION = 'joy';22const DEFAULT_FALLBACK_EXPRESSION = 'joy';
24const FUNCTION_NAME = 'set_emotion';
25const 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}}';23const 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}}';
26const DEFAULT_EXPRESSIONS = [24const DEFAULT_EXPRESSIONS = [
27 'talkinghead',25 'talkinghead',
@@ -1017,10 +1015,6 @@ async function getLlmPrompt(labels) {
1017 return '';1015 return '';
1018 }1016 }
10191017
1020 if (isFunctionCallingSupported()) {
1021 return '';
1022 }
1023
1024 const labelsString = labels.map(x => `"${x}"`).join(', ');1018 const labelsString = labels.map(x => `"${x}"`).join(', ');
1025 const prompt = substituteParamsExtended(String(extension_settings.expressions.llmPrompt), { labels: labelsString });1019 const prompt = substituteParamsExtended(String(extension_settings.expressions.llmPrompt), { labels: labelsString });
1026 return prompt;1020 return prompt;
@@ -1056,41 +1050,6 @@ function parseLlmResponse(emotionResponse, labels) {
1056 throw new Error('Could not parse emotion response ' + emotionResponse);1050 throw new Error('Could not parse emotion response ' + emotionResponse);
1057}1051}
10581052
1059/**
1060 * Registers the function tool for the LLM API.
1061 * @param {FunctionToolRegister} args Function tool register arguments.
1062 */
1063function 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
1094function onTextGenSettingsReady(args) {1053function onTextGenSettingsReady(args) {
1095 // Only call if inside an API call1054 // Only call if inside an API call
1096 if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) {1055 if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) {
@@ -1164,18 +1123,9 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
11641123
1165 const expressionsList = await getExpressionsList();1124 const expressionsList = await getExpressionsList();
1166 const prompt = substituteParamsExtended(customPrompt, { labels: expressionsList }) || await getLlmPrompt(expressionsList);1125 const prompt = substituteParamsExtended(customPrompt, { labels: expressionsList }) || await getLlmPrompt(expressionsList);
1167 let functionResult = null;
1168 eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);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 const emotionResponse = await generateRaw(text, main_api, false, false, prompt);1127 const emotionResponse = await generateRaw(text, main_api, false, false, prompt);
1178 return parseLlmResponse(functionResult || emotionResponse, expressionsList);1128 return parseLlmResponse(emotionResponse, expressionsList);
1179 }1129 }
1180 // Extras1130 // Extras
1181 default: {1131 default: {
public/scripts/extensions/regex/editor.html+1 -1
@@ -117,7 +117,7 @@
117 <input type="checkbox" name="disabled" />117 <input type="checkbox" name="disabled" />
118 <span data-i18n="Disabled">Disabled</span>118 <span data-i18n="Disabled">Disabled</span>
119 </label>119 </label>
120 <label class="checkbox flex-container">120 <label class="checkbox flex-container" title="Run the regex script when the message belonging a to specified role(s) is edited.">
121 <input type="checkbox" name="run_on_edit" />121 <input type="checkbox" name="run_on_edit" />
122 <span data-i18n="Run On Edit">Run On Edit</span>122 <span data-i18n="Run On Edit">Run On Edit</span>
123 </label>123 </label>
public/scripts/extensions/regex/engine.js+7 -2
@@ -44,9 +44,9 @@ function getScopedRegex() {
44 * @param {regex_placement} placement The placement of the string44 * @param {regex_placement} placement The placement of the string
45 * @param {RegexParams} params The parameters to use for the regex script45 * @param {RegexParams} params The parameters to use for the regex script
46 * @returns {string} The regexed string46 * @returns {string} The regexed string
47 * @typedef {{characterOverride?: string, isMarkdown?: boolean, isPrompt?: boolean, depth?: number }} RegexParams The parameters to use for the regex script47 * @typedef {{characterOverride?: string, isMarkdown?: boolean, isPrompt?: boolean, isEdit?: boolean, depth?: number }} RegexParams The parameters to use for the regex script
48 */48 */
49function getRegexedString(rawString, placement, { characterOverride, isMarkdown, isPrompt, depth } = {}) {49function getRegexedString(rawString, placement, { characterOverride, isMarkdown, isPrompt, isEdit, depth } = {}) {
50 // WTF have you passed me?50 // WTF have you passed me?
51 if (typeof rawString !== 'string') {51 if (typeof rawString !== 'string') {
52 console.warn('getRegexedString: rawString is not a string. Returning empty string.');52 console.warn('getRegexedString: rawString is not a string. Returning empty string.');
@@ -68,6 +68,11 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
68 // Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand68 // Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand
69 (!script.markdownOnly && !script.promptOnly && !isMarkdown)69 (!script.markdownOnly && !script.promptOnly && !isMarkdown)
70 ) {70 ) {
71 if (isEdit && !script.runOnEdit) {
72 console.debug(`getRegexedString: Skipping script ${script.scriptName} because it does not run on edit`);
73 return;
74 }
75
71 // Check if the depth is within the min/max depth76 // Check if the depth is within the min/max depth
72 if (typeof depth === 'number' && depth >= 0) {77 if (typeof depth === 'number' && depth >= 0) {
73 if (!isNaN(script.minDepth) && script.minDepth !== null && script.minDepth >= 0 && depth < script.minDepth) {78 if (!isNaN(script.minDepth) && script.minDepth !== null && script.minDepth >= 0 && depth < script.minDepth) {
public/scripts/extensions/stable-diffusion/index.js+67 -4
@@ -32,6 +32,7 @@ import { debounce_timeout } from '../../constants.js';
32import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';32import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
33import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';33import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
34import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';34import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
35import { ToolManager } from '../../tool-calling.js';
35export { MODULE_NAME };36export { MODULE_NAME };
3637
37const MODULE_NAME = 'sd';38const MODULE_NAME = 'sd';
@@ -62,9 +63,11 @@ const initiators = {
62 interactive: 'interactive',63 interactive: 'interactive',
63 wand: 'wand',64 wand: 'wand',
64 swipe: 'swipe',65 swipe: 'swipe',
66 tool: 'tool',
65};67};
6668
67const generationMode = {69const generationMode = {
70 TOOL: -2,
68 MESSAGE: -1,71 MESSAGE: -1,
69 CHARACTER: 0,72 CHARACTER: 0,
70 USER: 1,73 USER: 1,
@@ -87,6 +90,7 @@ const multimodalMap = {
87};90};
8891
89const modeLabels = {92const modeLabels = {
93 [generationMode.TOOL]: 'Function Tool Prompt Description',
90 [generationMode.MESSAGE]: 'Chat Message Template',94 [generationMode.MESSAGE]: 'Chat Message Template',
91 [generationMode.CHARACTER]: 'Character ("Yourself")',95 [generationMode.CHARACTER]: 'Character ("Yourself")',
92 [generationMode.FACE]: 'Portrait ("Your Face")',96 [generationMode.FACE]: 'Portrait ("Your Face")',
@@ -124,8 +128,12 @@ const messageTrigger = {
124};128};
125129
126const promptTemplates = {130const promptTemplates = {
127 // Not really a prompt template, rather an outcome message template131 // Not really a prompt template, rather an outcome message template and function tool prompt
128 [generationMode.MESSAGE]: '[{{char}} sends a picture that contains: {{prompt}}].',132 [generationMode.MESSAGE]: '[{{char}} sends a picture that contains: {{prompt}}].',
133 [generationMode.TOOL]: [
134 'The text prompt used to generate the image.',
135 'Must represent an exhaustive description of the desired image that will allow an artist or a photographer to perfectly recreate it.',
136 ].join(' '),
129 [generationMode.CHARACTER]: 'In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, clothing, occupation, physical features and appearances. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'full body portrait,\'',137 [generationMode.CHARACTER]: 'In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, clothing, occupation, physical features and appearances. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'full body portrait,\'',
130 //face-specific prompt138 //face-specific prompt
131 [generationMode.FACE]: 'In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, facial features and expressions, occupation, hair and hair accessories (if any), what they are wearing on their upper body (if anything). Do not describe anything below their neck. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'close up facial portrait,\'',139 [generationMode.FACE]: 'In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, facial features and expressions, occupation, hair and hair accessories (if any), what they are wearing on their upper body (if anything). Do not describe anything below their neck. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'close up facial portrait,\'',
@@ -226,6 +234,7 @@ const defaultSettings = {
226 multimodal_captioning: false,234 multimodal_captioning: false,
227 snap: false,235 snap: false,
228 free_extend: false,236 free_extend: false,
237 function_tool: false,
229238
230 prompts: promptTemplates,239 prompts: promptTemplates,
231240
@@ -291,6 +300,10 @@ const defaultSettings = {
291const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);300const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);
292301
293function processTriggers(chat, _, abort) {302function processTriggers(chat, _, abort) {
303 if (extension_settings.sd.function_tool && ToolManager.isToolCallingSupported()) {
304 return;
305 }
306
294 if (!extension_settings.sd.interactive_mode) {307 if (!extension_settings.sd.interactive_mode) {
295 return;308 return;
296 }309 }
@@ -447,6 +460,7 @@ async function loadSettings() {
447 $('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible);460 $('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible);
448 $('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset);461 $('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset);
449 $('#sd_huggingface_model_id').val(extension_settings.sd.huggingface_model_id);462 $('#sd_huggingface_model_id').val(extension_settings.sd.huggingface_model_id);
463 $('#sd_function_tool').prop('checked', extension_settings.sd.function_tool);
450464
451 for (const style of extension_settings.sd.styles) {465 for (const style of extension_settings.sd.styles) {
452 const option = document.createElement('option');466 const option = document.createElement('option');
@@ -461,6 +475,7 @@ async function loadSettings() {
461475
462 toggleSourceControls();476 toggleSourceControls();
463 addPromptTemplates();477 addPromptTemplates();
478 registerFunctionTool();
464479
465 await loadSettingOptions();480 await loadSettingOptions();
466}481}
@@ -524,6 +539,9 @@ function addPromptTemplates() {
524 .on('click', () => {539 .on('click', () => {
525 textarea.val(promptTemplates[name]);540 textarea.val(promptTemplates[name]);
526 extension_settings.sd.prompts[name] = promptTemplates[name];541 extension_settings.sd.prompts[name] = promptTemplates[name];
542 if (String(name) === String(generationMode.TOOL)) {
543 registerFunctionTool();
544 }
527 saveSettingsDebounced();545 saveSettingsDebounced();
528 });546 });
529 const container = $('<div></div>')547 const container = $('<div></div>')
@@ -910,6 +928,12 @@ async function onSourceChange() {
910 await loadSettingOptions();928 await loadSettingOptions();
911}929}
912930
931function onFunctionToolInput() {
932 extension_settings.sd.function_tool = !!$(this).prop('checked');
933 saveSettingsDebounced();
934 registerFunctionTool();
935}
936
913async function onOpenAiStyleSelect() {937async function onOpenAiStyleSelect() {
914 extension_settings.sd.openai_style = String($('#sd_openai_style').find(':selected').val());938 extension_settings.sd.openai_style = String($('#sd_openai_style').find(':selected').val());
915 saveSettingsDebounced();939 saveSettingsDebounced();
@@ -2290,9 +2314,9 @@ async function generatePicture(initiator, args, trigger, message, callback) {
2290 eventSource.emit(event_types.FORCE_SET_BACKGROUND, { url: imgUrl, path: imagePath });2314 eventSource.emit(event_types.FORCE_SET_BACKGROUND, { url: imgUrl, path: imagePath });
22912315
2292 if (typeof callbackOriginal === 'function') {2316 if (typeof callbackOriginal === 'function') {
2293 callbackOriginal(prompt, imagePath, generationType, negativePromptPrefix, initiator);2317 await callbackOriginal(prompt, imagePath, generationType, negativePromptPrefix, initiator);
2294 } else {2318 } else {
2295 sendMessage(prompt, imagePath, generationType, negativePromptPrefix, initiator);2319 await sendMessage(prompt, imagePath, generationType, negativePromptPrefix, initiator);
2296 }2320 }
2297 };2321 };
2298 }2322 }
@@ -2621,7 +2645,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
26212645
2622 const filename = `${characterName}_${humanizedDateTime()}`;2646 const filename = `${characterName}_${humanizedDateTime()}`;
2623 const base64Image = await saveBase64AsFile(result.data, characterName, filename, result.format);2647 const base64Image = await saveBase64AsFile(result.data, characterName, filename, result.format);
2624 callback ? callback(prompt, base64Image, generationType, additionalNegativePrefix, initiator) : sendMessage(prompt, base64Image, generationType, additionalNegativePrefix, initiator);2648 callback
2649 ? await callback(prompt, base64Image, generationType, additionalNegativePrefix, initiator)
2650 : await sendMessage(prompt, base64Image, generationType, additionalNegativePrefix, initiator);
2625 return base64Image;2651 return base64Image;
2626}2652}
26272653
@@ -3822,6 +3848,42 @@ function applyCommandArguments(args) {
3822 return currentSettings;3848 return currentSettings;
3823}3849}
38243850
3851function registerFunctionTool() {
3852 if (!extension_settings.sd.function_tool) {
3853 return ToolManager.unregisterFunctionTool('GenerateImage');
3854 }
3855
3856 ToolManager.registerFunctionTool({
3857 name: 'GenerateImage',
3858 displayName: 'Generate Image',
3859 description: [
3860 'Generate an image from a given text prompt.',
3861 'Use when a user asks for an image, a selfie, to picture a scene, etc.',
3862 ].join(' '),
3863 parameters: Object.freeze({
3864 $schema: 'http://json-schema.org/draft-04/schema#',
3865 type: 'object',
3866 properties: {
3867 prompt: {
3868 type: 'string',
3869 description: extension_settings.sd.prompts[generationMode.TOOL] || promptTemplates[generationMode.TOOL],
3870 },
3871 },
3872 required: [
3873 'prompt',
3874 ],
3875 }),
3876 action: async (args) => {
3877 if (!isValidState()) throw new Error('Image generation is not configured.');
3878 if (!args) throw new Error('Missing arguments');
3879 if (!args.prompt) throw new Error('Missing prompt');
3880 const url = await generatePicture(initiators.tool, {}, args.prompt);
3881 return encodeURI(url);
3882 },
3883 formatMessage: () => 'Generating an image...',
3884 });
3885}
3886
3825jQuery(async () => {3887jQuery(async () => {
3826 await addSDGenButtons();3888 await addSDGenButtons();
38273889
@@ -4175,6 +4237,7 @@ jQuery(async () => {
4175 $('#sd_stability_key').on('click', onStabilityKeyClick);4237 $('#sd_stability_key').on('click', onStabilityKeyClick);
4176 $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);4238 $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);
4177 $('#sd_huggingface_model_id').on('input', onHFModelInput);4239 $('#sd_huggingface_model_id').on('input', onHFModelInput);
4240 $('#sd_function_tool').on('input', onFunctionToolInput);
41784241
4179 if (!CSS.supports('field-sizing', 'content')) {4242 if (!CSS.supports('field-sizing', 'content')) {
4180 $('.sd_settings .inline-drawer-toggle').on('click', function () {4243 $('.sd_settings .inline-drawer-toggle').on('click', function () {
public/scripts/extensions/stable-diffusion/settings.html+5 -1
@@ -14,9 +14,13 @@
14 <input id="sd_refine_mode" type="checkbox" />14 <input id="sd_refine_mode" type="checkbox" />
15 <span data-i18n="sd_refine_mode_txt">Edit prompts before generation</span>15 <span data-i18n="sd_refine_mode_txt">Edit prompts before generation</span>
16 </label>16 </label>
17 <label for="sd_function_tool" class="checkbox_label" data-i18n="[title]sd_function_tool" title="Use the function tool to automatically detect intents to generate images.">
18 <input id="sd_function_tool" type="checkbox" />
19 <span data-i18n="sd_function_tool_txt">Use function tool</span>
20 </label>
17 <label for="sd_interactive_mode" class="checkbox_label" data-i18n="[title]sd_interactive_mode" title="Automatically generate images when sending messages like 'send me a picture of cat'.">21 <label for="sd_interactive_mode" class="checkbox_label" data-i18n="[title]sd_interactive_mode" title="Automatically generate images when sending messages like 'send me a picture of cat'.">
18 <input id="sd_interactive_mode" type="checkbox" />22 <input id="sd_interactive_mode" type="checkbox" />
19 <span data-i18n="sd_interactive_mode_txt">Interactive mode</span>23 <span data-i18n="sd_interactive_mode_txt">Use interactive mode</span>
20 </label>24 </label>
21 <label for="sd_multimodal_captioning" class="checkbox_label" data-i18n="[title]sd_multimodal_captioning" title="Use multimodal captioning to generate prompts for user and character portraits based on their avatars.">25 <label for="sd_multimodal_captioning" class="checkbox_label" data-i18n="[title]sd_multimodal_captioning" title="Use multimodal captioning to generate prompts for user and character portraits based on their avatars.">
22 <input id="sd_multimodal_captioning" type="checkbox" />26 <input id="sd_multimodal_captioning" type="checkbox" />
public/scripts/group-chats.js+1 -1
@@ -70,12 +70,12 @@ import {
70 animation_duration,70 animation_duration,
71 depth_prompt_role_default,71 depth_prompt_role_default,
72 shouldAutoContinue,72 shouldAutoContinue,
73 this_chid,
74} from '../script.js';73} from '../script.js';
75import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js';74import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js';
76import { FILTER_TYPES, FilterHelper } from './filters.js';75import { FILTER_TYPES, FilterHelper } from './filters.js';
77import { isExternalMediaAllowed } from './chats.js';76import { isExternalMediaAllowed } from './chats.js';
78import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';77import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
78import { t } from './i18n.js';
7979
80export {80export {
81 selected_group,81 selected_group,
public/scripts/instruct-mode.js+0 -2
@@ -554,11 +554,9 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
554 * @param {string} name Preset name.554 * @param {string} name Preset name.
555 */555 */
556function selectMatchingContextTemplate(name) {556function selectMatchingContextTemplate(name) {
557 let foundMatch = false;
558 for (const context_preset of context_presets) {557 for (const context_preset of context_presets) {
559 // If context template matches the instruct preset558 // If context template matches the instruct preset
560 if (context_preset.name === name) {559 if (context_preset.name === name) {
561 foundMatch = true;
562 selectContextPreset(context_preset.name, { isAuto: true });560 selectContextPreset(context_preset.name, { isAuto: true });
563 break;561 break;
564 }562 }
public/scripts/kai-settings.js+1 -1
@@ -188,7 +188,7 @@ export async function generateKoboldWithStreaming(generate_data, signal) {
188 if (data?.token) {188 if (data?.token) {
189 text += data.token;189 text += data.token;
190 }190 }
191 yield { text, swipes: [] };191 yield { text, swipes: [], toolCalls: [] };
192 }192 }
193 };193 };
194}194}
public/scripts/nai-settings.js+1 -1
@@ -746,7 +746,7 @@ export async function generateNovelWithStreaming(generate_data, signal) {
746 text += data.token;746 text += data.token;
747 }747 }
748748
749 yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs) };749 yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [] };
750 }750 }
751 };751 };
752}752}
public/scripts/openai.js+82 -149
@@ -70,6 +70,7 @@ import { renderTemplateAsync } from './templates.js';
70import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';70import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
71import { Popup, POPUP_RESULT } from './popup.js';71import { Popup, POPUP_RESULT } from './popup.js';
72import { t } from './i18n.js';72import { t } from './i18n.js';
73import { ToolManager } from './tool-calling.js';
7374
74export {75export {
75 openai_messages_count,76 openai_messages_count,
@@ -198,7 +199,10 @@ const continue_postfix_types = {
198199
199const custom_prompt_post_processing_types = {200const custom_prompt_post_processing_types = {
200 NONE: '',201 NONE: '',
202 /** @deprecated Use MERGE instead. */
201 CLAUDE: 'claude',203 CLAUDE: 'claude',
204 MERGE: 'merge',
205 STRICT: 'strict',
202};206};
203207
204const sensitiveFields = [208const sensitiveFields = [
@@ -453,7 +457,8 @@ function setOpenAIMessages(chat) {
453 if (role == 'user' && oai_settings.wrap_in_quotes) content = `"${content}"`;457 if (role == 'user' && oai_settings.wrap_in_quotes) content = `"${content}"`;
454 const name = chat[j]['name'];458 const name = chat[j]['name'];
455 const image = chat[j]?.extra?.image;459 const image = chat[j]?.extra?.image;
456 messages[i] = { 'role': role, 'content': content, name: name, 'image': image };460 const invocations = chat[j]?.extra?.tool_invocations;
461 messages[i] = { 'role': role, 'content': content, name: name, 'image': image, 'invocations': invocations };
457 j++;462 j++;
458 }463 }
459464
@@ -701,6 +706,7 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
701 }706 }
702707
703 const imageInlining = isImageInliningSupported();708 const imageInlining = isImageInliningSupported();
709 const canUseTools = ToolManager.isToolCallingSupported();
704710
705 // Insert chat messages as long as there is budget available711 // Insert chat messages as long as there is budget available
706 const chatPool = [...messages].reverse();712 const chatPool = [...messages].reverse();
@@ -722,6 +728,24 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
722 await chatMessage.addImage(chatPrompt.image);728 await chatMessage.addImage(chatPrompt.image);
723 }729 }
724730
731 if (canUseTools && Array.isArray(chatPrompt.invocations)) {
732 /** @type {import('./tool-calling.js').ToolInvocation[]} */
733 const invocations = chatPrompt.invocations;
734 const toolCallMessage = new Message(chatMessage.role, undefined, 'toolCall-' + chatMessage.identifier);
735 const toolResultMessages = invocations.slice().reverse().map((invocation) => new Message('tool', invocation.result || '[No content]', invocation.id));
736 toolCallMessage.setToolCalls(invocations);
737 if (chatCompletion.canAffordAll([toolCallMessage, ...toolResultMessages])) {
738 for (const resultMessage of toolResultMessages) {
739 chatCompletion.insertAtStart(resultMessage, 'chatHistory');
740 }
741 chatCompletion.insertAtStart(toolCallMessage, 'chatHistory');
742 } else {
743 break;
744 }
745
746 continue;
747 }
748
725 if (chatCompletion.canAfford(chatMessage)) {749 if (chatCompletion.canAfford(chatMessage)) {
726 if (type === 'continue' && oai_settings.continue_prefill && chatPrompt === firstNonInjected) {750 if (type === 'continue' && oai_settings.continue_prefill && chatPrompt === firstNonInjected) {
727 // 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 message751 // 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
@@ -1262,7 +1286,7 @@ export async function prepareOpenAIMessages({
1262 const eventData = { chat, dryRun };1286 const eventData = { chat, dryRun };
1263 await eventSource.emit(event_types.CHAT_COMPLETION_PROMPT_READY, eventData);1287 await eventSource.emit(event_types.CHAT_COMPLETION_PROMPT_READY, eventData);
12641288
1265 openai_messages_count = chat.filter(x => x?.role === 'user' || x?.role === 'assistant')?.length || 0;1289 openai_messages_count = chat.filter(x => !x?.tool_calls && (x?.role === 'user' || x?.role === 'assistant'))?.length || 0;
12661290
1267 return [chat, promptManager.tokenHandler.counts];1291 return [chat, promptManager.tokenHandler.counts];
1268}1292}
@@ -1687,7 +1711,6 @@ async function sendOpenAIRequest(type, messages, signal) {
1687 messages = messages.filter(msg => msg && typeof msg === 'object');1711 messages = messages.filter(msg => msg && typeof msg === 'object');
16881712
1689 let logit_bias = {};1713 let logit_bias = {};
1690 const messageId = getNextMessageId(type);
1691 const isClaude = oai_settings.chat_completion_source == chat_completion_sources.CLAUDE;1714 const isClaude = oai_settings.chat_completion_source == chat_completion_sources.CLAUDE;
1692 const isOpenRouter = oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER;1715 const isOpenRouter = oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER;
1693 const isScale = oai_settings.chat_completion_source == chat_completion_sources.SCALE;1716 const isScale = oai_settings.chat_completion_source == chat_completion_sources.SCALE;
@@ -1860,8 +1883,8 @@ async function sendOpenAIRequest(type, messages, signal) {
1860 generate_data['seed'] = oai_settings.seed;1883 generate_data['seed'] = oai_settings.seed;
1861 }1884 }
18621885
1863 if (isFunctionCallingSupported() && !stream) {1886 if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {
1864 await registerFunctionTools(type, generate_data);1887 await ToolManager.registerFunctionToolsOpenAI(generate_data);
1865 }1888 }
18661889
1867 if (isOAI && oai_settings.openai_model.startsWith('o1-')) {1890 if (isOAI && oai_settings.openai_model.startsWith('o1-')) {
@@ -1908,6 +1931,7 @@ async function sendOpenAIRequest(type, messages, signal) {
1908 return async function* streamData() {1931 return async function* streamData() {
1909 let text = '';1932 let text = '';
1910 const swipes = [];1933 const swipes = [];
1934 const toolCalls = [];
1911 while (true) {1935 while (true) {
1912 const { done, value } = await reader.read();1936 const { done, value } = await reader.read();
1913 if (done) return;1937 if (done) return;
@@ -1923,7 +1947,9 @@ async function sendOpenAIRequest(type, messages, signal) {
1923 text += getStreamingReply(parsed);1947 text += getStreamingReply(parsed);
1924 }1948 }
19251949
1926 yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed) };1950 ToolManager.parseToolCalls(toolCalls, parsed);
1951
1952 yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls };
1927 }1953 }
1928 };1954 };
1929 }1955 }
@@ -1945,147 +1971,10 @@ async function sendOpenAIRequest(type, messages, signal) {
1945 delay(1).then(() => saveLogprobsForActiveMessage(logprobs, null));1971 delay(1).then(() => saveLogprobsForActiveMessage(logprobs, null));
1946 }1972 }
19471973
1948 if (isFunctionCallingSupported()) {
1949 await checkFunctionToolCalls(data);
1950 }
1951
1952 return data;1974 return data;
1953 }1975 }
1954}1976}
19551977
1956/**
1957 * Register function tools for the next chat completion request.
1958 * @param {string} type Generation type
1959 * @param {object} data Generation data
1960 */
1961async function registerFunctionTools(type, data) {
1962 let toolChoice = 'auto';
1963 const tools = [];
1964
1965 /**
1966 * @type {registerFunctionTool}
1967 */
1968 const registerFunctionTool = (name, description, parameters, required) => {
1969 tools.push({
1970 type: 'function',
1971 function: {
1972 name,
1973 description,
1974 parameters,
1975 },
1976 });
1977
1978 if (required) {
1979 toolChoice = 'required';
1980 }
1981 };
1982
1983 /**
1984 * @type {FunctionToolRegister}
1985 */
1986 const args = {
1987 type,
1988 data,
1989 registerFunctionTool,
1990 };
1991
1992 await eventSource.emit(event_types.LLM_FUNCTION_TOOL_REGISTER, args);
1993
1994 if (tools.length) {
1995 console.log('Registered function tools:', tools);
1996
1997 data['tools'] = tools;
1998 data['tool_choice'] = toolChoice;
1999 }
2000}
2001
2002async function checkFunctionToolCalls(data) {
2003 const oaiCompat = [
2004 chat_completion_sources.OPENAI,
2005 chat_completion_sources.CUSTOM,
2006 chat_completion_sources.MISTRALAI,
2007 chat_completion_sources.OPENROUTER,
2008 chat_completion_sources.GROQ,
2009 ];
2010 if (oaiCompat.includes(oai_settings.chat_completion_source)) {
2011 if (!Array.isArray(data?.choices)) {
2012 return;
2013 }
2014
2015 // Find a choice with 0-index
2016 const choice = data.choices.find(choice => choice.index === 0);
2017
2018 if (!choice) {
2019 return;
2020 }
2021
2022 const toolCalls = choice.message.tool_calls;
2023
2024 if (!Array.isArray(toolCalls)) {
2025 return;
2026 }
2027
2028 for (const toolCall of toolCalls) {
2029 if (typeof toolCall.function !== 'object') {
2030 continue;
2031 }
2032
2033 /** @type {FunctionToolCall} */
2034 const args = toolCall.function;
2035 console.log('Function tool call:', toolCall);
2036 await eventSource.emit(event_types.LLM_FUNCTION_TOOL_CALL, args);
2037 }
2038 }
2039
2040 if ([chat_completion_sources.CLAUDE].includes(oai_settings.chat_completion_source)) {
2041 if (!Array.isArray(data?.content)) {
2042 return;
2043 }
2044
2045 for (const content of data.content) {
2046 if (content.type === 'tool_use') {
2047 /** @type {FunctionToolCall} */
2048 const args = { name: content.name, arguments: JSON.stringify(content.input) };
2049 await eventSource.emit(event_types.LLM_FUNCTION_TOOL_CALL, args);
2050 }
2051 }
2052 }
2053
2054 if ([chat_completion_sources.COHERE].includes(oai_settings.chat_completion_source)) {
2055 if (!Array.isArray(data?.tool_calls)) {
2056 return;
2057 }
2058
2059 for (const toolCall of data.tool_calls) {
2060 /** @type {FunctionToolCall} */
2061 const args = { name: toolCall.name, arguments: JSON.stringify(toolCall.parameters) };
2062 console.log('Function tool call:', toolCall);
2063 await eventSource.emit(event_types.LLM_FUNCTION_TOOL_CALL, args);
2064 }
2065 }
2066}
2067
2068export function isFunctionCallingSupported() {
2069 if (main_api !== 'openai') {
2070 return false;
2071 }
2072
2073 if (!oai_settings.function_calling) {
2074 return false;
2075 }
2076
2077 const supportedSources = [
2078 chat_completion_sources.OPENAI,
2079 chat_completion_sources.COHERE,
2080 chat_completion_sources.CUSTOM,
2081 chat_completion_sources.MISTRALAI,
2082 chat_completion_sources.CLAUDE,
2083 chat_completion_sources.OPENROUTER,
2084 chat_completion_sources.GROQ,
2085 ];
2086 return supportedSources.includes(oai_settings.chat_completion_source);
2087}
2088
2089function getStreamingReply(data) {1978function getStreamingReply(data) {
2090 if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {1979 if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
2091 return data?.delta?.text || '';1980 return data?.delta?.text || '';
@@ -2323,6 +2212,8 @@ class Message {
2323 content;2212 content;
2324 /** @type {string} */2213 /** @type {string} */
2325 name;2214 name;
2215 /** @type {object} */
2216 tool_call = null;
23262217
2327 /**2218 /**
2328 * @constructor2219 * @constructor
@@ -2347,6 +2238,22 @@ class Message {
2347 }2238 }
2348 }2239 }
23492240
2241 /**
2242 * Reconstruct the message from a tool invocation.
2243 * @param {import('./tool-calling.js').ToolInvocation[]} invocations
2244 */
2245 setToolCalls(invocations) {
2246 this.tool_calls = invocations.map(i => ({
2247 id: i.id,
2248 type: 'function',
2249 function: {
2250 arguments: i.parameters,
2251 name: i.name,
2252 },
2253 }));
2254 this.tokens = tokenHandler.count({ role: this.role, tool_calls: JSON.stringify(this.tool_calls) });
2255 }
2256
2350 setName(name) {2257 setName(name) {
2351 this.name = name;2258 this.name = name;
2352 this.tokens = tokenHandler.count({ role: this.role, content: this.content, name: this.name });2259 this.tokens = tokenHandler.count({ role: this.role, content: this.content, name: this.name });
@@ -2483,13 +2390,20 @@ class MessageCollection {
2483 }2390 }
24842391
2485 /**2392 /**
2486 * Get chat in the format of {role, name, content}.2393 * Get chat in the format of {role, name, content, tool_calls}.
2487 * @returns {Array} Array of objects with role, name, and content properties.2394 * @returns {Array} Array of objects with role, name, and content properties.
2488 */2395 */
2489 getChat() {2396 getChat() {
2490 return this.collection.reduce((acc, message) => {2397 return this.collection.reduce((acc, message) => {
2491 const name = message.name;2398 if (message.content || message.tool_calls) {
2492 if (message.content) acc.push({ role: message.role, ...(name && { name }), content: message.content });2399 acc.push({
2400 role: message.role,
2401 content: message.content,
2402 ...(message.name && { name: message.name }),
2403 ...(message.tool_calls && { tool_calls: message.tool_calls }),
2404 ...(message.role === 'tool' && { tool_call_id: message.identifier }),
2405 });
2406 }
2493 return acc;2407 return acc;
2494 }, []);2408 }, []);
2495 }2409 }
@@ -2694,7 +2608,7 @@ export class ChatCompletion {
2694 this.checkTokenBudget(message, message.identifier);2608 this.checkTokenBudget(message, message.identifier);
26952609
2696 const index = this.findMessageIndex(identifier);2610 const index = this.findMessageIndex(identifier);
2697 if (message.content) {2611 if (message.content || message.tool_calls) {
2698 if ('start' === position) this.messages.collection[index].collection.unshift(message);2612 if ('start' === position) this.messages.collection[index].collection.unshift(message);
2699 else if ('end' === position) this.messages.collection[index].collection.push(message);2613 else if ('end' === position) this.messages.collection[index].collection.push(message);
2700 else if (typeof position === 'number') this.messages.collection[index].collection.splice(position, 0, message);2614 else if (typeof position === 'number') this.messages.collection[index].collection.splice(position, 0, message);
@@ -2735,6 +2649,15 @@ export class ChatCompletion {
2735 }2649 }
27362650
2737 /**2651 /**
2652 * Checks if the token budget can afford the tokens of all the specified messages.
2653 * @param {Message[]} messages - The messages to check for affordability.
2654 * @returns {boolean} True if the budget can afford all the messages, false otherwise.
2655 */
2656 canAffordAll(messages) {
2657 return 0 <= this.tokenBudget - messages.reduce((total, message) => total + message.getTokens(), 0);
2658 }
2659
2660 /**
2738 * Checks if a message with the specified identifier exists in the collection.2661 * Checks if a message with the specified identifier exists in the collection.
2739 *2662 *
2740 * @param {string} identifier - The identifier to check for existence.2663 * @param {string} identifier - The identifier to check for existence.
@@ -2763,8 +2686,14 @@ export class ChatCompletion {
2763 for (let item of this.messages.collection) {2686 for (let item of this.messages.collection) {
2764 if (item instanceof MessageCollection) {2687 if (item instanceof MessageCollection) {
2765 chat.push(...item.getChat());2688 chat.push(...item.getChat());
2766 } else if (item instanceof Message && item.content) {2689 } else if (item instanceof Message && (item.content || item.tool_calls)) {
2767 const message = { role: item.role, content: item.content, ...(item.name ? { name: item.name } : {}) };2690 const message = {
2691 role: item.role,
2692 content: item.content,
2693 ...(item.name ? { name: item.name } : {}),
2694 ...(item.tool_calls ? { tool_calls: item.tool_calls } : {}),
2695 ...(item.role === 'tool' ? { tool_call_id: item.identifier } : {}),
2696 };
2768 chat.push(message);2697 chat.push(message);
2769 } else {2698 } else {
2770 this.log(`Skipping invalid or empty message in collection: ${JSON.stringify(item)}`);2699 this.log(`Skipping invalid or empty message in collection: ${JSON.stringify(item)}`);
@@ -3118,6 +3047,10 @@ function loadOpenAISettings(data, settings) {
3118 setNamesBehaviorControls();3047 setNamesBehaviorControls();
3119 setContinuePostfixControls();3048 setContinuePostfixControls();
31203049
3050 if (oai_settings.custom_prompt_post_processing === custom_prompt_post_processing_types.CLAUDE) {
3051 oai_settings.custom_prompt_post_processing = custom_prompt_post_processing_types.MERGE;
3052 }
3053
3121 $('#chat_completion_source').val(oai_settings.chat_completion_source).trigger('change');3054 $('#chat_completion_source').val(oai_settings.chat_completion_source).trigger('change');
3122 $('#oai_max_context_unlocked').prop('checked', oai_settings.max_context_unlocked);3055 $('#oai_max_context_unlocked').prop('checked', oai_settings.max_context_unlocked);
3123 $('#custom_prompt_post_processing').val(oai_settings.custom_prompt_post_processing);3056 $('#custom_prompt_post_processing').val(oai_settings.custom_prompt_post_processing);
public/scripts/slash-commands/SlashCommandReturnHelper.js+1 -1
@@ -58,7 +58,7 @@ export const slashCommandReturnHelper = {
58 case 'toast-html': {58 case 'toast-html': {
59 const htmlOrNotHtml = shouldHtml ? DOMPurify.sanitize((new showdown.Converter()).makeHtml(stringValue)) : escapeHtml(stringValue);59 const htmlOrNotHtml = shouldHtml ? DOMPurify.sanitize((new showdown.Converter()).makeHtml(stringValue)) : escapeHtml(stringValue);
6060
61 if (type.startsWith('popup')) await callGenericPopup(htmlOrNotHtml, POPUP_TYPE.TEXT);61 if (type.startsWith('popup')) await callGenericPopup(htmlOrNotHtml, POPUP_TYPE.TEXT, '', { allowVerticalScrolling: true, wide: true });
62 if (type.startsWith('chat')) sendSystemMessage(system_message_types.GENERIC, htmlOrNotHtml);62 if (type.startsWith('chat')) sendSystemMessage(system_message_types.GENERIC, htmlOrNotHtml);
63 if (type.startsWith('toast')) toastr.info(htmlOrNotHtml, null, { escapeHtml: !shouldHtml });63 if (type.startsWith('toast')) toastr.info(htmlOrNotHtml, null, { escapeHtml: !shouldHtml });
6464
public/scripts/textgen-settings.js+2 -1
@@ -916,6 +916,7 @@ async function generateTextGenWithStreaming(generate_data, signal) {
916 /** @type {import('./logprobs.js').TokenLogprobs | null} */916 /** @type {import('./logprobs.js').TokenLogprobs | null} */
917 let logprobs = null;917 let logprobs = null;
918 const swipes = [];918 const swipes = [];
919 const toolCalls = [];
919 while (true) {920 while (true) {
920 const { done, value } = await reader.read();921 const { done, value } = await reader.read();
921 if (done) return;922 if (done) return;
@@ -934,7 +935,7 @@ async function generateTextGenWithStreaming(generate_data, signal) {
934 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);935 logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);
935 }936 }
936937
937 yield { text, swipes, logprobs };938 yield { text, swipes, logprobs, toolCalls };
938 }939 }
939 };940 };
940}941}
public/scripts/tool-calling.js+891 -0
@@ -0,0 +1,891 @@
1import { addOneMessage, chat, event_types, eventSource, main_api, saveChatConditional, system_avatar, systemUserName } from '../script.js';
2import { chat_completion_sources, oai_settings } from './openai.js';
3import { Popup } from './popup.js';
4import { SlashCommand } from './slash-commands/SlashCommand.js';
5import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
6import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
7import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
8import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
9import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
10import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
11
12/**
13 * @typedef {object} ToolInvocation
14 * @property {string} id - A unique identifier for the tool invocation.
15 * @property {string} displayName - The display name of the tool.
16 * @property {string} name - The name of the tool.
17 * @property {string} parameters - The parameters for the tool invocation.
18 * @property {string} result - The result of the tool invocation.
19 */
20
21/**
22 * @typedef {object} ToolInvocationResult
23 * @property {ToolInvocation[]} invocations Successful tool invocations
24 * @property {Error[]} errors Errors that occurred during tool invocation
25 */
26
27/**
28 * @typedef {object} ToolRegistration
29 * @property {string} name - The name of the tool.
30 * @property {string} displayName - The display name of the tool.
31 * @property {string} description - A description of the tool.
32 * @property {object} parameters - The parameters for the tool.
33 * @property {function} action - The action to perform when the tool is invoked.
34 * @property {function} formatMessage - A function to format the tool call message.
35 */
36
37/**
38 * @typedef {object} ToolDefinitionOpenAI
39 * @property {string} type - The type of the tool.
40 * @property {object} function - The function definition.
41 * @property {string} function.name - The name of the function.
42 * @property {string} function.description - The description of the function.
43 * @property {object} function.parameters - The parameters of the function.
44 * @property {function} toString - A function to convert the tool to a string.
45 */
46
47/**
48 * Assigns nested variables to a scope.
49 * @param {import('./slash-commands/SlashCommandScope.js').SlashCommandScope} scope The scope to assign variables to.
50 * @param {object} arg Object to assign variables from.
51 * @param {string} prefix Prefix for the variable names.
52 */
53function assignNestedVariables(scope, arg, prefix) {
54 Object.entries(arg).forEach(([key, value]) => {
55 const newPrefix = `${prefix}.${key}`;
56 if (typeof value === 'object' && value !== null) {
57 assignNestedVariables(scope, value, newPrefix);
58 } else {
59 scope.letVariable(newPrefix, value);
60 }
61 });
62}
63
64/**
65 * Checks if a string is a valid JSON string.
66 * @param {string} str The string to check
67 * @returns {boolean} If the string is a valid JSON string
68 */
69function isJson(str) {
70 try {
71 JSON.parse(str);
72 return true;
73 } catch {
74 return false;
75 }
76}
77
78/**
79 * Tries to parse a string as JSON, returning the original string if parsing fails.
80 * @param {string} str The string to try to parse
81 * @returns {object|string} Parsed JSON or the original string
82 */
83function tryParse(str) {
84 try {
85 return JSON.parse(str);
86 } catch {
87 return str;
88 }
89}
90
91/**
92 * Stringifies an object if it is not already a string.
93 * @param {any} obj The object to stringify
94 * @returns {string} A JSON string representation of the object.
95 */
96function stringify(obj) {
97 return typeof obj === 'string' ? obj : JSON.stringify(obj);
98}
99
100/**
101 * A class that represents a tool definition.
102 */
103class ToolDefinition {
104 /**
105 * A unique name for the tool.
106 * @type {string}
107 */
108 #name;
109
110 /**
111 * A user-friendly display name for the tool.
112 * @type {string}
113 */
114 #displayName;
115
116 /**
117 * A description of what the tool does.
118 * @type {string}
119 */
120 #description;
121
122 /**
123 * A JSON schema for the parameters that the tool accepts.
124 * @type {object}
125 */
126 #parameters;
127
128 /**
129 * A function that will be called when the tool is executed.
130 * @type {function}
131 */
132 #action;
133
134 /**
135 * A function that will be called to format the tool call toast.
136 * @type {function}
137 */
138 #formatMessage;
139
140 /**
141 * Creates a new ToolDefinition.
142 * @param {string} name A unique name for the tool.
143 * @param {string} displayName A user-friendly display name for the tool.
144 * @param {string} description A description of what the tool does.
145 * @param {object} parameters A JSON schema for the parameters that the tool accepts.
146 * @param {function} action A function that will be called when the tool is executed.
147 * @param {function} formatMessage A function that will be called to format the tool call toast.
148 */
149 constructor(name, displayName, description, parameters, action, formatMessage) {
150 this.#name = name;
151 this.#displayName = displayName;
152 this.#description = description;
153 this.#parameters = parameters;
154 this.#action = action;
155 this.#formatMessage = formatMessage;
156 }
157
158 /**
159 * Converts the ToolDefinition to an OpenAI API representation
160 * @returns {ToolDefinitionOpenAI} OpenAI API representation of the tool.
161 */
162 toFunctionOpenAI() {
163 return {
164 type: 'function',
165 function: {
166 name: this.#name,
167 description: this.#description,
168 parameters: this.#parameters,
169 },
170 toString: function () {
171 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>`;
172 },
173 };
174 }
175
176 /**
177 * Invokes the tool with the given parameters.
178 * @param {object} parameters The parameters to pass to the tool.
179 * @returns {Promise<any>} The result of the tool's action function.
180 */
181 async invoke(parameters) {
182 return await this.#action(parameters);
183 }
184
185 /**
186 * Formats a message with the tool invocation.
187 * @param {object} parameters The parameters to pass to the tool.
188 * @returns {string} The formatted message.
189 */
190 formatMessage(parameters) {
191 return typeof this.#formatMessage === 'function'
192 ? this.#formatMessage(parameters)
193 : `Invoking tool: ${this.#displayName || this.#name}`;
194 }
195
196 get displayName() {
197 return this.#displayName;
198 }
199}
200
201/**
202 * A class that manages the registration and invocation of tools.
203 */
204export class ToolManager {
205 /**
206 * A map of tool names to tool definitions.
207 * @type {Map<string, ToolDefinition>}
208 */
209 static #tools = new Map();
210
211 static #INPUT_DELTA_KEY = '__input_json_delta';
212
213 /**
214 * The maximum number of times to recurse when parsing tool calls.
215 * @type {number}
216 */
217 static RECURSE_LIMIT = 5;
218
219 /**
220 * Returns an Array of all tools that have been registered.
221 * @type {ToolDefinition[]}
222 */
223 static get tools() {
224 return Array.from(this.#tools.values());
225 }
226
227 /**
228 * Registers a new tool with the tool registry.
229 * @param {ToolRegistration} tool The tool to register.
230 */
231 static registerFunctionTool({ name, displayName, description, parameters, action, formatMessage }) {
232 // Convert WIP arguments
233 if (typeof arguments[0] !== 'object') {
234 [name, description, parameters, action] = arguments;
235 }
236
237 if (this.#tools.has(name)) {
238 console.warn(`A tool with the name "${name}" has already been registered. The definition will be overwritten.`);
239 }
240
241 const definition = new ToolDefinition(name, displayName, description, parameters, action, formatMessage);
242 this.#tools.set(name, definition);
243 console.log('[ToolManager] Registered function tool:', definition);
244 }
245
246 /**
247 * Removes a tool from the tool registry.
248 * @param {string} name The name of the tool to unregister.
249 */
250 static unregisterFunctionTool(name) {
251 if (!this.#tools.has(name)) {
252 return;
253 }
254
255 this.#tools.delete(name);
256 console.log(`[ToolManager] Unregistered function tool: ${name}`);
257 }
258
259 /**
260 * Invokes a tool by name. Returns the result of the tool's action function.
261 * @param {string} name The name of the tool to invoke.
262 * @param {object} parameters Function parameters. For example, if the tool requires a "name" parameter, you would pass {name: "value"}.
263 * @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.
264 */
265 static async invokeFunctionTool(name, parameters) {
266 try {
267 if (!this.#tools.has(name)) {
268 throw new Error(`No tool with the name "${name}" has been registered.`);
269 }
270
271 const invokeParameters = typeof parameters === 'string' ? JSON.parse(parameters) : parameters;
272 const tool = this.#tools.get(name);
273 const result = await tool.invoke(invokeParameters);
274 return typeof result === 'string' ? result : JSON.stringify(result);
275 } catch (error) {
276 console.error(`An error occurred while invoking the tool "${name}":`, error);
277
278 if (error instanceof Error) {
279 error.cause = name;
280 return error;
281 }
282
283 return new Error('Unknown error occurred while invoking the tool.', { cause: name });
284 }
285 }
286
287 /**
288 * Formats a message for a tool call by name.
289 * @param {string} name The name of the tool to format the message for.
290 * @param {object} parameters Function tool call parameters.
291 * @returns {string} The formatted message for the tool call.
292 */
293 static formatToolCallMessage(name, parameters) {
294 if (!this.#tools.has(name)) {
295 return `Invoked unknown tool: ${name}`;
296 }
297
298 try {
299 const tool = this.#tools.get(name);
300 const formatParameters = typeof parameters === 'string' ? JSON.parse(parameters) : parameters;
301 return tool.formatMessage(formatParameters);
302 } catch (error) {
303 console.error(`An error occurred while formatting the tool call message for "${name}":`, error);
304 return `Invoking tool: ${name}`;
305 }
306 }
307
308 /**
309 * Gets the display name of a tool by name.
310 * @param {string} name
311 * @returns {string} The display name of the tool.
312 */
313 static getDisplayName(name) {
314 if (!this.#tools.has(name)) {
315 return name;
316 }
317
318 const tool = this.#tools.get(name);
319 return tool.displayName || name;
320 }
321
322 /**
323 * Register function tools for the next chat completion request.
324 * @param {object} data Generation data
325 */
326 static async registerFunctionToolsOpenAI(data) {
327 const tools = [];
328
329 for (const tool of ToolManager.tools) {
330 tools.push(tool.toFunctionOpenAI());
331 }
332
333 if (tools.length) {
334 console.log('Registered function tools:', tools);
335
336 data['tools'] = tools;
337 data['tool_choice'] = 'auto';
338 }
339 }
340
341 /**
342 * Utility function to parse tool calls from a parsed response.
343 * @param {any[]} toolCalls The tool calls to update.
344 * @param {any} parsed The parsed response from the OpenAI API.
345 * @returns {void}
346 */
347 static parseToolCalls(toolCalls, parsed) {
348 if (Array.isArray(parsed?.choices)) {
349 for (const choice of parsed.choices) {
350 const choiceIndex = (typeof choice.index === 'number') ? choice.index : null;
351 const choiceDelta = choice.delta;
352
353 if (choiceIndex === null || !choiceDelta) {
354 continue;
355 }
356
357 const toolCallDeltas = choiceDelta?.tool_calls;
358
359 if (!Array.isArray(toolCallDeltas)) {
360 continue;
361 }
362
363 if (!Array.isArray(toolCalls[choiceIndex])) {
364 toolCalls[choiceIndex] = [];
365 }
366
367 for (const toolCallDelta of toolCallDeltas) {
368 const toolCallIndex = (typeof toolCallDelta?.index === 'number') ? toolCallDelta.index : toolCallDeltas.indexOf(toolCallDelta);
369
370 if (isNaN(toolCallIndex) || toolCallIndex < 0) {
371 continue;
372 }
373
374 if (toolCalls[choiceIndex][toolCallIndex] === undefined) {
375 toolCalls[choiceIndex][toolCallIndex] = {};
376 }
377
378 const targetToolCall = toolCalls[choiceIndex][toolCallIndex];
379
380 ToolManager.#applyToolCallDelta(targetToolCall, toolCallDelta);
381 }
382 }
383 }
384 if (typeof parsed?.content_block === 'object') {
385 const choiceIndex = 0;
386 const toolCallIndex = parsed?.index ?? 0;
387
388 if (parsed?.content_block?.type === 'tool_use') {
389 if (!Array.isArray(toolCalls[choiceIndex])) {
390 toolCalls[choiceIndex] = [];
391 }
392 if (toolCalls[choiceIndex][toolCallIndex] === undefined) {
393 toolCalls[choiceIndex][toolCallIndex] = {};
394 }
395 const targetToolCall = toolCalls[choiceIndex][toolCallIndex];
396 ToolManager.#applyToolCallDelta(targetToolCall, parsed.content_block);
397 }
398 }
399 if (typeof parsed?.delta === 'object') {
400 const choiceIndex = 0;
401 const toolCallIndex = parsed?.index ?? 0;
402 const targetToolCall = toolCalls[choiceIndex]?.[toolCallIndex];
403 if (targetToolCall) {
404 if (parsed?.delta?.type === 'input_json_delta') {
405 const jsonDelta = parsed?.delta?.partial_json;
406 if (!targetToolCall[this.#INPUT_DELTA_KEY]) {
407 targetToolCall[this.#INPUT_DELTA_KEY] = '';
408 }
409 targetToolCall[this.#INPUT_DELTA_KEY] += jsonDelta;
410 }
411 }
412 }
413 if (parsed?.type === 'content_block_stop') {
414 const choiceIndex = 0;
415 const toolCallIndex = parsed?.index ?? 0;
416 const targetToolCall = toolCalls[choiceIndex]?.[toolCallIndex];
417 if (targetToolCall) {
418 const jsonDeltaString = targetToolCall[this.#INPUT_DELTA_KEY];
419 if (jsonDeltaString) {
420 try {
421 const jsonDelta = { input: JSON.parse(jsonDeltaString) };
422 delete targetToolCall[this.#INPUT_DELTA_KEY];
423 ToolManager.#applyToolCallDelta(targetToolCall, jsonDelta);
424 } catch (error) {
425 console.warn('Failed to apply input JSON delta:', error);
426 }
427 }
428 }
429 }
430 }
431
432 /**
433 * Apply a tool call delta to a target object.
434 * @param {object} target The target object to apply the delta to
435 * @param {object} delta The delta object to apply
436 */
437 static #applyToolCallDelta(target, delta) {
438 for (const key in delta) {
439 if (!Object.prototype.hasOwnProperty.call(delta, key)) continue;
440 if (key === '__proto__' || key === 'constructor') continue;
441
442 const deltaValue = delta[key];
443 const targetValue = target[key];
444
445 if (deltaValue === null || deltaValue === undefined) {
446 target[key] = deltaValue;
447 continue;
448 }
449
450 if (typeof deltaValue === 'string') {
451 if (typeof targetValue === 'string') {
452 // Concatenate strings
453 target[key] = targetValue + deltaValue;
454 } else {
455 target[key] = deltaValue;
456 }
457 } else if (typeof deltaValue === 'object' && !Array.isArray(deltaValue)) {
458 if (typeof targetValue !== 'object' || targetValue === null || Array.isArray(targetValue)) {
459 target[key] = {};
460 }
461 // Recursively apply deltas to nested objects
462 ToolManager.#applyToolCallDelta(target[key], deltaValue);
463 } else {
464 // Assign other types directly
465 target[key] = deltaValue;
466 }
467 }
468 }
469
470 /**
471 * Checks if tool calling is supported for the current settings and generation type.
472 * @returns {boolean} Whether tool calling is supported for the given type
473 */
474 static isToolCallingSupported() {
475 if (main_api !== 'openai' || !oai_settings.function_calling) {
476 return false;
477 }
478
479 const supportedSources = [
480 chat_completion_sources.OPENAI,
481 chat_completion_sources.CUSTOM,
482 chat_completion_sources.MISTRALAI,
483 chat_completion_sources.CLAUDE,
484 chat_completion_sources.OPENROUTER,
485 chat_completion_sources.GROQ,
486 ];
487 return supportedSources.includes(oai_settings.chat_completion_source);
488 }
489
490 /**
491 * Checks if tool calls can be performed for the current settings and generation type.
492 * @param {string} type Generation type
493 * @returns {boolean} Whether tool calls can be performed for the given type
494 */
495 static canPerformToolCalls(type) {
496 const noToolCallTypes = ['impersonate', 'quiet', 'continue'];
497 const isSupported = ToolManager.isToolCallingSupported();
498 return isSupported && !noToolCallTypes.includes(type);
499 }
500
501 /**
502 * Utility function to get tool calls from the response data.
503 * @param {any} data Response data
504 * @returns {any[]} Tool calls from the response data
505 */
506 static #getToolCallsFromData(data) {
507 const isClaudeToolCall = c => Array.isArray(c) ? c.filter(x => x).every(isClaudeToolCall) : c?.input && c?.name && c?.id;
508 const convertClaudeToolCall = c => ({ id: c.id, function: { name: c.name, arguments: c.input } });
509
510 // Parsed tool calls from streaming data
511 if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0])) {
512 return isClaudeToolCall(data[0]) ? data[0].filter(x => x).map(convertClaudeToolCall) : data[0];
513 }
514
515 // Parsed tool calls from non-streaming data
516 if (Array.isArray(data?.choices)) {
517 // Find a choice with 0-index
518 const choice = data.choices.find(choice => choice.index === 0);
519
520 if (choice) {
521 return choice.message.tool_calls;
522 }
523 }
524
525 // Claude tool calls to OpenAI tool calls
526 if (Array.isArray(data?.content)) {
527 const content = data.content.filter(c => c.type === 'tool_use').map(convertClaudeToolCall);
528
529 if (content) {
530 return content;
531 }
532 }
533 }
534
535 /**
536 * Checks if the response data contains tool calls.
537 * @param {object} data Response data
538 * @returns {boolean} Whether the response data contains tool calls
539 */
540 static hasToolCalls(data) {
541 const toolCalls = ToolManager.#getToolCallsFromData(data);
542 return Array.isArray(toolCalls) && toolCalls.length > 0;
543 }
544
545 /**
546 * Check for function tool calls in the response data and invoke them.
547 * @param {any} data Reply data
548 * @returns {Promise<ToolInvocationResult>} Successful tool invocations
549 */
550 static async invokeFunctionTools(data) {
551 /** @type {ToolInvocationResult} */
552 const result = {
553 invocations: [],
554 errors: [],
555 };
556 const toolCalls = ToolManager.#getToolCallsFromData(data);
557
558 if (!Array.isArray(toolCalls)) {
559 return result;
560 }
561
562 for (const toolCall of toolCalls) {
563 if (typeof toolCall.function !== 'object') {
564 continue;
565 }
566
567 console.log('Function tool call:', toolCall);
568 const id = toolCall.id;
569 const parameters = toolCall.function.arguments;
570 const name = toolCall.function.name;
571 const displayName = ToolManager.getDisplayName(name);
572
573 const message = ToolManager.formatToolCallMessage(name, parameters);
574 const toast = message && toastr.info(message, 'Tool Calling', { timeOut: 0 });
575 const toolResult = await ToolManager.invokeFunctionTool(name, parameters);
576 toastr.clear(toast);
577 console.log('Function tool result:', result);
578
579 // Save a successful invocation
580 if (toolResult instanceof Error) {
581 result.errors.push(toolResult);
582 continue;
583 }
584
585 const invocation = {
586 id,
587 displayName,
588 name,
589 parameters: stringify(parameters),
590 result: toolResult,
591 };
592 result.invocations.push(invocation);
593 }
594
595 return result;
596 }
597
598 /**
599 * Groups tool names by count.
600 * @param {string[]} toolNames Tool names
601 * @returns {string} Grouped tool names
602 */
603 static #groupToolNames(toolNames) {
604 const toolCounts = toolNames.reduce((acc, name) => {
605 acc[name] = (acc[name] || 0) + 1;
606 return acc;
607 }, {});
608 return Object.entries(toolCounts).map(([name, count]) => count > 1 ? `${name} (${count})` : name).join(', ');
609 }
610
611 /**
612 * Formats a message with tool invocations.
613 * @param {ToolInvocation[]} invocations Tool invocations.
614 * @returns {string} Formatted message with tool invocations.
615 */
616 static #formatToolInvocationMessage(invocations) {
617 const data = structuredClone(invocations);
618 const detailsElement = document.createElement('details');
619 const summaryElement = document.createElement('summary');
620 const preElement = document.createElement('pre');
621 const codeElement = document.createElement('code');
622 codeElement.classList.add('language-json');
623 data.forEach(i => {
624 i.parameters = tryParse(i.parameters);
625 i.result = tryParse(i.result);
626 });
627 codeElement.textContent = JSON.stringify(data, null, 2);
628 const toolNames = data.map(i => i.displayName || i.name);
629 summaryElement.textContent = `Tool calls: ${this.#groupToolNames(toolNames)}`;
630 preElement.append(codeElement);
631 detailsElement.append(summaryElement, preElement);
632 return detailsElement.outerHTML;
633 }
634
635 /**
636 * Saves function tool invocations to the last user chat message extra metadata.
637 * @param {ToolInvocation[]} invocations Successful tool invocations
638 */
639 static async saveFunctionToolInvocations(invocations) {
640 if (!Array.isArray(invocations) || invocations.length === 0) {
641 return;
642 }
643 const message = {
644 name: systemUserName,
645 force_avatar: system_avatar,
646 is_system: true,
647 is_user: false,
648 mes: ToolManager.#formatToolInvocationMessage(invocations),
649 extra: {
650 isSmallSys: true,
651 tool_invocations: invocations,
652 },
653 };
654 chat.push(message);
655 await eventSource.emit(event_types.TOOL_CALLS_PERFORMED, invocations);
656 addOneMessage(message);
657 await eventSource.emit(event_types.TOOL_CALLS_RENDERED, invocations);
658 await saveChatConditional();
659 }
660
661 /**
662 * Shows an error message for tool calls.
663 * @param {Error[]} errors Errors that occurred during tool invocation
664 * @returns {void}
665 */
666 static showToolCallError(errors) {
667 toastr.error('An error occurred while invoking function tools. Click here for more details.', 'Tool Calling', {
668 onclick: () => Popup.show.text('Tool Calling Errors', DOMPurify.sanitize(errors.map(e => `${e.cause}: ${e.message}`).join('<br>'))),
669 timeOut: 5000,
670 });
671 }
672
673 static initToolSlashCommands() {
674 const toolsEnumProvider = () => ToolManager.tools.map(tool => {
675 const toolOpenAI = tool.toFunctionOpenAI();
676 return new SlashCommandEnumValue(toolOpenAI.function.name, toolOpenAI.function.description, enumTypes.enum, enumIcons.closure);
677 });
678
679 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
680 name: 'tools-list',
681 aliases: ['tool-list'],
682 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.',
683 returns: 'A list of all registered tools.',
684 namedArgumentList: [
685 SlashCommandNamedArgument.fromProps({
686 name: 'return',
687 description: 'The way how you want the return value to be provided',
688 typeList: [ARGUMENT_TYPE.STRING],
689 defaultValue: 'none',
690 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
691 forceEnum: true,
692 }),
693 ],
694 callback: async (args) => {
695 /** @type {any} */
696 const returnType = String(args?.return ?? 'popup-html').trim().toLowerCase();
697 const objectToStringFunc = (tools) => Array.isArray(tools) ? tools.map(x => x.toString()).join('\n\n') : tools.toString();
698 const tools = ToolManager.tools.map(tool => tool.toFunctionOpenAI());
699 return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', tools ?? [], { objectToStringFunc });
700 },
701 }));
702
703 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
704 name: 'tools-invoke',
705 aliases: ['tool-invoke'],
706 helpString: 'Invokes a registered tool by name. The <code>parameters</code> argument MUST be a JSON-serialized object.',
707 namedArgumentList: [
708 SlashCommandNamedArgument.fromProps({
709 name: 'parameters',
710 description: 'The parameters to pass to the tool.',
711 typeList: [ARGUMENT_TYPE.DICTIONARY],
712 isRequired: true,
713 acceptsMultiple: false,
714 }),
715 ],
716 unnamedArgumentList: [
717 SlashCommandArgument.fromProps({
718 description: 'The name of the tool to invoke.',
719 typeList: [ARGUMENT_TYPE.STRING],
720 isRequired: true,
721 acceptsMultiple: false,
722 forceEnum: true,
723 enumProvider: toolsEnumProvider,
724 }),
725 ],
726 callback: async (args, name) => {
727 const { parameters } = args;
728
729 const result = await ToolManager.invokeFunctionTool(String(name), parameters);
730 if (result instanceof Error) {
731 throw result;
732 }
733
734 return result;
735 },
736 }));
737
738 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
739 name: 'tools-register',
740 aliases: ['tool-register'],
741 helpString: `<div>Registers a new tool with the tool registry.</div>
742 <ul>
743 <li>The <code>parameters</code> argument MUST be a JSON-serialized object with a valid JSON schema.</li>
744 <li>The unnamed argument MUST be a closure that accepts the function parameters as local script variables.</li>
745 </ul>
746 <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> for more information.</div>
747 <div>Example:</div>
748 <pre><code>/let key=echoSchema
749{
750 "$schema": "http://json-schema.org/draft-04/schema#",
751 "type": "object",
752 "properties": {
753 "message": {
754 "type": "string",
755 "description": "The message to echo."
756 }
757 },
758 "required": [
759 "message"
760 ]
761}
762||
763/tools-register name=Echo description="Echoes a message. Call when the user is asking to repeat something" parameters={{var::echoSchema}} {: /echo {{var::arg.message}} :}</code></pre>`,
764 namedArgumentList: [
765 SlashCommandNamedArgument.fromProps({
766 name: 'name',
767 description: 'The name of the tool.',
768 typeList: [ARGUMENT_TYPE.STRING],
769 isRequired: true,
770 acceptsMultiple: false,
771 }),
772 SlashCommandNamedArgument.fromProps({
773 name: 'description',
774 description: 'A description of what the tool does.',
775 typeList: [ARGUMENT_TYPE.STRING],
776 isRequired: true,
777 acceptsMultiple: false,
778 }),
779 SlashCommandNamedArgument.fromProps({
780 name: 'parameters',
781 description: 'The parameters for the tool.',
782 typeList: [ARGUMENT_TYPE.DICTIONARY],
783 isRequired: true,
784 acceptsMultiple: false,
785 }),
786 SlashCommandNamedArgument.fromProps({
787 name: 'displayName',
788 description: 'The display name of the tool.',
789 typeList: [ARGUMENT_TYPE.STRING],
790 isRequired: false,
791 acceptsMultiple: false,
792 }),
793 SlashCommandNamedArgument.fromProps({
794 name: 'formatMessage',
795 description: 'The closure to be executed to format the tool call message. Must return a string.',
796 typeList: [ARGUMENT_TYPE.CLOSURE],
797 isRequired: true,
798 acceptsMultiple: false,
799 }),
800 ],
801 unnamedArgumentList: [
802 SlashCommandArgument.fromProps({
803 description: 'The closure to be executed when the tool is invoked.',
804 typeList: [ARGUMENT_TYPE.CLOSURE],
805 isRequired: true,
806 acceptsMultiple: false,
807 }),
808 ],
809 callback: async (args, action) => {
810 /**
811 * Converts a slash command closure to a function.
812 * @param {SlashCommandClosure} action Closure to convert to a function
813 * @returns {function} Function that executes the closure
814 */
815 function closureToFunction(action) {
816 return async (args) => {
817 const localClosure = action.getCopy();
818 localClosure.onProgress = () => { };
819 const scope = localClosure.scope;
820 if (typeof args === 'object' && args !== null) {
821 assignNestedVariables(scope, args, 'arg');
822 } else if (typeof args !== 'undefined') {
823 scope.letVariable('arg', args);
824 }
825 const result = await localClosure.execute();
826 return result.pipe;
827 };
828 }
829
830 const { name, displayName, description, parameters, formatMessage } = args;
831
832 if (!(action instanceof SlashCommandClosure)) {
833 throw new Error('The unnamed argument must be a closure.');
834 }
835 if (typeof name !== 'string' || !name) {
836 throw new Error('The "name" argument must be a non-empty string.');
837 }
838 if (typeof description !== 'string' || !description) {
839 throw new Error('The "description" argument must be a non-empty string.');
840 }
841 if (typeof parameters !== 'string' || !isJson(parameters)) {
842 throw new Error('The "parameters" argument must be a JSON-serialized object.');
843 }
844 if (displayName && typeof displayName !== 'string') {
845 throw new Error('The "displayName" argument must be a string.');
846 }
847 if (formatMessage && !(formatMessage instanceof SlashCommandClosure)) {
848 throw new Error('The "formatMessage" argument must be a closure.');
849 }
850
851 const actionFunc = closureToFunction(action);
852 const formatMessageFunc = formatMessage instanceof SlashCommandClosure ? closureToFunction(formatMessage) : null;
853
854 ToolManager.registerFunctionTool({
855 name: String(name ?? ''),
856 displayName: String(displayName ?? ''),
857 description: String(description ?? ''),
858 parameters: JSON.parse(parameters ?? '{}'),
859 action: actionFunc,
860 formatMessage: formatMessageFunc,
861 });
862
863 return '';
864 },
865 }));
866
867 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
868 name: 'tools-unregister',
869 aliases: ['tool-unregister'],
870 helpString: 'Unregisters a tool from the tool registry.',
871 unnamedArgumentList: [
872 SlashCommandArgument.fromProps({
873 description: 'The name of the tool to unregister.',
874 typeList: [ARGUMENT_TYPE.STRING],
875 isRequired: true,
876 acceptsMultiple: false,
877 forceEnum: true,
878 enumProvider: toolsEnumProvider,
879 }),
880 ],
881 callback: async (name) => {
882 if (typeof name !== 'string' || !name) {
883 throw new Error('The unnamed argument must be a non-empty string.');
884 }
885
886 ToolManager.unregisterFunctionTool(name);
887 return '';
888 },
889 }));
890 }
891}
public/style.css+10 -0
@@ -424,6 +424,16 @@ small {
424 text-align: center;424 text-align: center;
425}425}
426426
427.mes.smallSysMes pre {
428 text-align: initial;
429 word-break: break-all;
430 margin-top: 5px;
431}
432
433.mes.smallSysMes summary {
434 cursor: pointer;
435}
436
427.mes.smallSysMes .mes_text p:last-child {437.mes.smallSysMes .mes_text p:last-child {
428 margin: 0;438 margin: 0;
429}439}
src/endpoints/backends/chat-completions.js+23 -31
@@ -4,7 +4,7 @@ const fetch = require('node-fetch').default;
4const { jsonParser } = require('../../express-common');4const { jsonParser } = require('../../express-common');
5const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants');5const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants');
6const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util');6const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util');
7const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages, convertMistralMessages, convertCohereTools, convertAI21Messages } = require('../../prompt-converters');7const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages, convertMistralMessages, convertAI21Messages, mergeMessages } = require('../../prompt-converters');
8const CohereStream = require('../../cohere-stream');8const CohereStream = require('../../cohere-stream');
99
10const { readSecret, SECRET_KEYS } = require('../secrets');10const { readSecret, SECRET_KEYS } = require('../secrets');
@@ -31,8 +31,11 @@ const API_AI21 = 'https://api.ai21.com/studio/v1';
31 */31 */
32function postProcessPrompt(messages, type, charName, userName) {32function postProcessPrompt(messages, type, charName, userName) {
33 switch (type) {33 switch (type) {
34 case 'merge':
34 case 'claude':35 case 'claude':
35 return convertClaudeMessages(messages, '', false, '', charName, userName).messages;36 return mergeMessages(messages, charName, userName, false);
37 case 'strict':
38 return mergeMessages(messages, charName, userName, true);
36 default:39 default:
37 return messages;40 return messages;
38 }41 }
@@ -84,7 +87,7 @@ async function sendClaudeRequest(request, response) {
84 const apiUrl = new URL(request.body.reverse_proxy || API_CLAUDE).toString();87 const apiUrl = new URL(request.body.reverse_proxy || API_CLAUDE).toString();
85 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE);88 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE);
86 const divider = '-'.repeat(process.stdout.columns);89 const divider = '-'.repeat(process.stdout.columns);
87 const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false);90 const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false) && request.body.model.startsWith('claude-3');
8891
89 if (!apiKey) {92 if (!apiKey) {
90 console.log(color.red(`Claude API key is missing.\n${divider}`));93 console.log(color.red(`Claude API key is missing.\n${divider}`));
@@ -98,8 +101,9 @@ async function sendClaudeRequest(request, response) {
98 controller.abort();101 controller.abort();
99 });102 });
100 const additionalHeaders = {};103 const additionalHeaders = {};
104 const useTools = request.body.model.startsWith('claude-3') && Array.isArray(request.body.tools) && request.body.tools.length > 0;
101 const useSystemPrompt = (request.body.model.startsWith('claude-2') || request.body.model.startsWith('claude-3')) && request.body.claude_use_sysprompt;105 const useSystemPrompt = (request.body.model.startsWith('claude-2') || request.body.model.startsWith('claude-3')) && request.body.claude_use_sysprompt;
102 const convertedPrompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, useSystemPrompt, request.body.human_sysprompt_message, request.body.char_name, request.body.user_name);106 const convertedPrompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, useSystemPrompt, useTools, request.body.human_sysprompt_message, request.body.char_name, request.body.user_name);
103 // Add custom stop sequences107 // Add custom stop sequences
104 const stopSequences = [];108 const stopSequences = [];
105 if (Array.isArray(request.body.stop)) {109 if (Array.isArray(request.body.stop)) {
@@ -107,7 +111,7 @@ async function sendClaudeRequest(request, response) {
107 }111 }
108112
109 const requestBody = {113 const requestBody = {
110 /** @type {any} */ system: '',114 /** @type {any} */ system: [],
111 messages: convertedPrompt.messages,115 messages: convertedPrompt.messages,
112 model: request.body.model,116 model: request.body.model,
113 max_tokens: request.body.max_tokens,117 max_tokens: request.body.max_tokens,
@@ -118,23 +122,29 @@ async function sendClaudeRequest(request, response) {
118 stream: request.body.stream,122 stream: request.body.stream,
119 };123 };
120 if (useSystemPrompt) {124 if (useSystemPrompt) {
121 requestBody.system = enableSystemPromptCache125 if (enableSystemPromptCache && Array.isArray(convertedPrompt.systemPrompt) && convertedPrompt.systemPrompt.length) {
122 ? [{ type: 'text', text: convertedPrompt.systemPrompt, cache_control: { type: 'ephemeral' } }]126 convertedPrompt.systemPrompt[convertedPrompt.systemPrompt.length - 1]['cache_control'] = { type: 'ephemeral' };
123 : convertedPrompt.systemPrompt;127 }
128
129 requestBody.system = convertedPrompt.systemPrompt;
124 } else {130 } else {
125 delete requestBody.system;131 delete requestBody.system;
126 }132 }
127 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {133 if (useTools) {
128 // Claude doesn't do prefills on function calls, and doesn't allow empty messages134 // Claude doesn't do prefills on function calls, and doesn't allow empty messages
129 if (convertedPrompt.messages.length && convertedPrompt.messages[convertedPrompt.messages.length - 1].role === 'assistant') {135 if (convertedPrompt.messages.length && convertedPrompt.messages[convertedPrompt.messages.length - 1].role === 'assistant') {
130 convertedPrompt.messages.push({ role: 'user', content: '.' });136 convertedPrompt.messages.push({ role: 'user', content: '.' });
131 }137 }
132 additionalHeaders['anthropic-beta'] = 'tools-2024-05-16';138 additionalHeaders['anthropic-beta'] = 'tools-2024-05-16';
133 requestBody.tool_choice = { type: request.body.tool_choice === 'required' ? 'any' : 'auto' };139 requestBody.tool_choice = { type: request.body.tool_choice };
134 requestBody.tools = request.body.tools140 requestBody.tools = request.body.tools
135 .filter(tool => tool.type === 'function')141 .filter(tool => tool.type === 'function')
136 .map(tool => tool.function)142 .map(tool => tool.function)
137 .map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters }));143 .map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters }));
144
145 if (enableSystemPromptCache && requestBody.tools.length) {
146 requestBody.tools[requestBody.tools.length - 1]['cache_control'] = { type: 'ephemeral' };
147 }
138 }148 }
139 if (enableSystemPromptCache) {149 if (enableSystemPromptCache) {
140 additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';150 additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';
@@ -483,7 +493,7 @@ async function sendMistralAIRequest(request, response) {
483493
484 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {494 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
485 requestBody['tools'] = request.body.tools;495 requestBody['tools'] = request.body.tools;
486 requestBody['tool_choice'] = request.body.tool_choice === 'required' ? 'any' : 'auto';496 requestBody['tool_choice'] = request.body.tool_choice;
487 }497 }
488498
489 const config = {499 const config = {
@@ -553,12 +563,6 @@ async function sendCohereRequest(request, response) {
553 });563 });
554 }564 }
555565
556 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
557 tools.push(...convertCohereTools(request.body.tools));
558 // Can't have both connectors and tools in the same request
559 connectors.splice(0, connectors.length);
560 }
561
562 // https://docs.cohere.com/reference/chat566 // https://docs.cohere.com/reference/chat
563 const requestBody = {567 const requestBody = {
564 stream: Boolean(request.body.stream),568 stream: Boolean(request.body.stream),
@@ -908,24 +912,12 @@ router.post('/generate', jsonParser, function (request, response) {
908 apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY);912 apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY);
909 headers = {};913 headers = {};
910 bodyParams = {};914 bodyParams = {};
911 request.body.messages = postProcessPrompt(request.body.messages, 'claude', request.body.char_name, request.body.user_name);915 request.body.messages = postProcessPrompt(request.body.messages, 'strict', request.body.char_name, request.body.user_name);
912 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) {916 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) {
913 apiUrl = API_GROQ;917 apiUrl = API_GROQ;
914 apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);918 apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);
915 headers = {};919 headers = {};
916 bodyParams = {};920 bodyParams = {};
917
918 // 'required' tool choice is not supported by Groq
919 if (request.body.tool_choice === 'required') {
920 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
921 request.body.tool_choice = request.body.tools.length > 1
922 ? 'auto' :
923 { type: 'function', function: { name: request.body.tools[0]?.function?.name } };
924
925 } else {
926 request.body.tool_choice = 'none';
927 }
928 }
929 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ZEROONEAI) {921 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ZEROONEAI) {
930 apiUrl = API_01AI;922 apiUrl = API_01AI;
931 apiKey = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);923 apiKey = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);
@@ -962,7 +954,7 @@ router.post('/generate', jsonParser, function (request, response) {
962 controller.abort();954 controller.abort();
963 });955 });
964956
965 if (!isTextCompletion) {957 if (!isTextCompletion && Array.isArray(request.body.tools) && request.body.tools.length > 0) {
966 bodyParams['tools'] = request.body.tools;958 bodyParams['tools'] = request.body.tools;
967 bodyParams['tool_choice'] = request.body.tool_choice;959 bodyParams['tool_choice'] = request.body.tool_choice;
968 }960 }
src/endpoints/novelai.js+1 -1
@@ -51,7 +51,7 @@ const eratoRepPenWhitelist = [
51 6, 1, 11, 13, 25, 198, 12, 9, 8, 279, 264, 459, 323, 477, 539, 912, 374, 574, 1051, 1550, 1587, 4536, 5828, 15058,51 6, 1, 11, 13, 25, 198, 12, 9, 8, 279, 264, 459, 323, 477, 539, 912, 374, 574, 1051, 1550, 1587, 4536, 5828, 15058,
52 3287, 3250, 1461, 1077, 813, 11074, 872, 1202, 1436, 7846, 1288, 13434, 1053, 8434, 617, 9167, 1047, 19117, 706,52 3287, 3250, 1461, 1077, 813, 11074, 872, 1202, 1436, 7846, 1288, 13434, 1053, 8434, 617, 9167, 1047, 19117, 706,
53 12775, 649, 4250, 527, 7784, 690, 2834, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 1210, 1359, 608, 220, 596, 956,53 12775, 649, 4250, 527, 7784, 690, 2834, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 1210, 1359, 608, 220, 596, 956,
54 3077, 44886, 4265, 3358, 2351, 2846, 311, 389, 315, 304, 520, 505, 43054 3077, 44886, 4265, 3358, 2351, 2846, 311, 389, 315, 304, 520, 505, 430,
55];55];
5656
57// Ban the dinkus and asterism57// Ban the dinkus and asterism
src/endpoints/search.js+70 -49
@@ -22,58 +22,15 @@ const visitHeaders = {
22 'Sec-Fetch-User': '?1',22 'Sec-Fetch-User': '?1',
23};23};
2424
25router.post('/serpapi', jsonParser, async (request, response) => {
26 try {
27 const key = readSecret(request.user.directories, SECRET_KEYS.SERPAPI);
28
29 if (!key) {
30 console.log('No SerpApi key found');
31 return response.sendStatus(400);
32 }
33
34 const { query } = request.body;
35 const result = await fetch(`https://serpapi.com/search.json?q=${encodeURIComponent(query)}&api_key=${key}`);
36
37 console.log('SerpApi query', query);
38
39 if (!result.ok) {
40 const text = await result.text();
41 console.log('SerpApi request failed', result.statusText, text);
42 return response.status(500).send(text);
43 }
44
45 const data = await result.json();
46 return response.json(data);
47 } catch (error) {
48 console.log(error);
49 return response.sendStatus(500);
50 }
51});
52
53/**25/**
54 * Get the transcript of a YouTube video26 * Extract the transcript of a YouTube video
55 * @copyright https://github.com/Kakulukian/youtube-transcript (MIT License)27 * @param {string} videoPageBody HTML of the video page
28 * @param {string} lang Language code
29 * @returns {Promise<string>} Transcript text
56 */30 */
57router.post('/transcript', jsonParser, async (request, response) => {31async function extractTranscript(videoPageBody, lang) {
58 try {
59 const he = require('he');32 const he = require('he');
60 const RE_XML_TRANSCRIPT = /<text start="([^"]*)" dur="([^"]*)">([^<]*)<\/text>/g;33 const RE_XML_TRANSCRIPT = /<text start="([^"]*)" dur="([^"]*)">([^<]*)<\/text>/g;
61 const id = request.body.id;
62 const lang = request.body.lang;
63
64 if (!id) {
65 console.log('Id is required for /transcript');
66 return response.sendStatus(400);
67 }
68
69 const videoPageResponse = await fetch(`https://www.youtube.com/watch?v=${id}`, {
70 headers: {
71 ...(lang && { 'Accept-Language': lang }),
72 'User-Agent': visitHeaders['User-Agent'],
73 },
74 });
75
76 const videoPageBody = await videoPageResponse.text();
77 const splittedHTML = videoPageBody.split('"captions":');34 const splittedHTML = videoPageBody.split('"captions":');
7835
79 if (splittedHTML.length <= 1) {36 if (splittedHTML.length <= 1) {
@@ -128,8 +85,72 @@ router.post('/transcript', jsonParser, async (request, response) => {
128 }));85 }));
129 // The text is double-encoded86 // The text is double-encoded
130 const transcriptText = transcript.map((line) => he.decode(he.decode(line.text))).join(' ');87 const transcriptText = transcript.map((line) => he.decode(he.decode(line.text))).join(' ');
88 return transcriptText;
89}
90
91router.post('/serpapi', jsonParser, async (request, response) => {
92 try {
93 const key = readSecret(request.user.directories, SECRET_KEYS.SERPAPI);
94
95 if (!key) {
96 console.log('No SerpApi key found');
97 return response.sendStatus(400);
98 }
99
100 const { query } = request.body;
101 const result = await fetch(`https://serpapi.com/search.json?q=${encodeURIComponent(query)}&api_key=${key}`);
102
103 console.log('SerpApi query', query);
104
105 if (!result.ok) {
106 const text = await result.text();
107 console.log('SerpApi request failed', result.statusText, text);
108 return response.status(500).send(text);
109 }
110
111 const data = await result.json();
112 return response.json(data);
113 } catch (error) {
114 console.log(error);
115 return response.sendStatus(500);
116 }
117});
118
119/**
120 * Get the transcript of a YouTube video
121 * @copyright https://github.com/Kakulukian/youtube-transcript (MIT License)
122 */
123router.post('/transcript', jsonParser, async (request, response) => {
124 try {
125 const id = request.body.id;
126 const lang = request.body.lang;
127 const json = request.body.json;
128
129 if (!id) {
130 console.log('Id is required for /transcript');
131 return response.sendStatus(400);
132 }
131133
132 return response.send(transcriptText);134 const videoPageResponse = await fetch(`https://www.youtube.com/watch?v=${id}`, {
135 headers: {
136 ...(lang && { 'Accept-Language': lang }),
137 'User-Agent': visitHeaders['User-Agent'],
138 },
139 });
140
141 const videoPageBody = await videoPageResponse.text();
142
143 try {
144 const transcriptText = await extractTranscript(videoPageBody, lang);
145 return json
146 ? response.json({ transcript: transcriptText, html: videoPageBody })
147 : response.send(transcriptText);
148 } catch (error) {
149 if (json) {
150 return response.json({ html: videoPageBody, transcript: '' });
151 }
152 throw error;
153 }
133 } catch (error) {154 } catch (error) {
134 console.log(error);155 console.log(error);
135 return response.sendStatus(500);156 return response.sendStatus(500);
src/prompt-converters.js+237 -119
@@ -1,5 +1,8 @@
1require('./polyfill.js');1require('./polyfill.js');
2const { getConfigValue } = require('./util.js');2const { getConfigValue } = require('./util.js');
3const crypto = require('crypto');
4
5const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get started.');
36
4/**7/**
5 * Convert a prompt from the ChatML objects to the format used by Claude.8 * Convert a prompt from the ChatML objects to the format used by Claude.
@@ -19,6 +22,14 @@ function convertClaudePrompt(messages, addAssistantPostfix, addAssistantPrefill,
19 //Prepare messages for claude.22 //Prepare messages for claude.
20 //When 'Exclude Human/Assistant prefixes' checked, setting messages role to the 'system'(last message is exception).23 //When 'Exclude Human/Assistant prefixes' checked, setting messages role to the 'system'(last message is exception).
21 if (messages.length > 0) {24 if (messages.length > 0) {
25 messages.forEach((m) => {
26 if (!m.content) {
27 m.content = '';
28 }
29 if (m.tool_calls) {
30 m.content += JSON.stringify(m.tool_calls);
31 }
32 });
22 if (excludePrefixes) {33 if (excludePrefixes) {
23 messages.slice(0, -1).forEach(message => message.role = 'system');34 messages.slice(0, -1).forEach(message => message.role = 'system');
24 } else {35 } else {
@@ -80,12 +91,13 @@ function convertClaudePrompt(messages, addAssistantPostfix, addAssistantPrefill,
80 * @param {object[]} messages Array of messages91 * @param {object[]} messages Array of messages
81 * @param {string} prefillString User determined prefill string92 * @param {string} prefillString User determined prefill string
82 * @param {boolean} useSysPrompt See if we want to use a system prompt93 * @param {boolean} useSysPrompt See if we want to use a system prompt
94 * @param {boolean} useTools See if we want to use tools
83 * @param {string} humanMsgFix Add Human message between system prompt and assistant.95 * @param {string} humanMsgFix Add Human message between system prompt and assistant.
84 * @param {string} charName Character name96 * @param {string} charName Character name
85 * @param {string} userName User name97 * @param {string} userName User name
86 */98 */
87function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFix, charName = '', userName = '') {99function convertClaudeMessages(messages, prefillString, useSysPrompt, useTools, humanMsgFix, charName = '', userName = '') {
88 let systemPrompt = '';100 let systemPrompt = [];
89 if (useSysPrompt) {101 if (useSysPrompt) {
90 // Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array.102 // Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array.
91 let i;103 let i;
@@ -104,7 +116,7 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
104 messages[i].content = `${charName}: ${messages[i].content}`;116 messages[i].content = `${charName}: ${messages[i].content}`;
105 }117 }
106 }118 }
107 systemPrompt += `${messages[i].content}\n\n`;119 systemPrompt.push({ type: 'text', text: messages[i].content });
108 }120 }
109121
110 messages.splice(0, i);122 messages.splice(0, i);
@@ -114,12 +126,32 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
114 if (messages.length === 0 || (messages.length > 0 && messages[0].role !== 'user')) {126 if (messages.length === 0 || (messages.length > 0 && messages[0].role !== 'user')) {
115 messages.unshift({127 messages.unshift({
116 role: 'user',128 role: 'user',
117 content: humanMsgFix || '[Start a new chat]',129 content: humanMsgFix || PROMPT_PLACEHOLDER,
118 });130 });
119 }131 }
120 }132 }
133
121 // Now replace all further messages that have the role 'system' with the role 'user'. (or all if we're not using one)134 // Now replace all further messages that have the role 'system' with the role 'user'. (or all if we're not using one)
135 const parse = (str) => typeof str === 'string' ? JSON.parse(str) : str;
122 messages.forEach((message) => {136 messages.forEach((message) => {
137 if (message.role === 'assistant' && message.tool_calls) {
138 message.content = message.tool_calls.map((tc) => ({
139 type: 'tool_use',
140 id: tc.id,
141 name: tc.function.name,
142 input: parse(tc.function.arguments),
143 }));
144 }
145
146 if (message.role === 'tool') {
147 message.role = 'user';
148 message.content = [{
149 type: 'tool_result',
150 tool_use_id: message.tool_call_id,
151 content: message.content,
152 }];
153 }
154
123 if (message.role === 'system') {155 if (message.role === 'system') {
124 if (userName && message.name === 'example_user') {156 if (userName && message.name === 'example_user') {
125 message.content = `${userName}: ${message.content}`;157 message.content = `${userName}: ${message.content}`;
@@ -128,68 +160,116 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
128 message.content = `${charName}: ${message.content}`;160 message.content = `${charName}: ${message.content}`;
129 }161 }
130 message.role = 'user';162 message.role = 'user';
163
164 // Delete name here so it doesn't get added later
165 delete message.name;
131 }166 }
132 });
133167
134 // Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling168 // Convert everything to an array of it would be easier to work with
135 if (prefillString) {169 if (typeof message.content === 'string') {
136 messages.push({170 // Take care of name properties since claude messages don't support them
137 role: 'assistant',171 if (message.name) {
138 content: prefillString.trimEnd(),172 message.content = `${message.name}: ${message.content}`;
139 });
140 }173 }
141174
142 // Since the messaging endpoint only supports user assistant roles in turns, we have to merge messages with the same role if they follow eachother175 message.content = [{ type: 'text', text: message.content }];
143 // Also handle multi-modality, holy slop.176 } else if (Array.isArray(message.content)) {
144 let mergedMessages = [];177 message.content = message.content.map((content) => {
145 messages.forEach((message) => {178 if (content.type === 'image_url') {
146 const imageEntry = message.content?.[1]?.image_url;179 const imageEntry = content?.image_url;
147 const imageData = imageEntry?.url;180 const imageData = imageEntry?.url;
148 const mimeType = imageData?.split(';')?.[0].split(':')?.[1];181 const mimeType = imageData?.split(';')?.[0].split(':')?.[1];
149 const base64Data = imageData?.split(',')?.[1];182 const base64Data = imageData?.split(',')?.[1];
150183
151 // Take care of name properties since claude messages don't support them184 return {
185 type: 'image',
186 source: {
187 type: 'base64',
188 media_type: mimeType,
189 data: base64Data,
190 },
191 };
192 }
193
194 if (content.type === 'text') {
152 if (message.name) {195 if (message.name) {
153 if (Array.isArray(message.content)) {196 content.text = `${message.name}: ${content.text}`;
154 message.content[0].text = `${message.name}: ${message.content[0].text}`;197 }
155 } else {198
156 message.content = `${message.name}: ${message.content}`;199 return content;
200 }
201
202 return content;
203 });
157 }204 }
205
206 // Remove offending properties
158 delete message.name;207 delete message.name;
208 delete message.tool_calls;
209 delete message.tool_call_id;
210 });
211
212 // Images in assistant messages should be moved to the next user message
213 for (let i = 0; i < messages.length; i++) {
214 if (messages[i].role === 'assistant' && messages[i].content.some(c => c.type === 'image')) {
215 // Find the next user message
216 let j = i + 1;
217 while (j < messages.length && messages[j].role !== 'user') {
218 j++;
159 }219 }
160220
161 if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role) {221 // Move the images
162 if (Array.isArray(message.content)) {222 if (j >= messages.length) {
163 if (Array.isArray(mergedMessages[mergedMessages.length - 1].content)) {223 // If there is no user message after the assistant message, add a new one
164 mergedMessages[mergedMessages.length - 1].content[0].text += '\n\n' + message.content[0].text;224 messages.splice(i + 1, 0, { role: 'user', content: [] });
165 } else {
166 mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content[0].text;
167 }225 }
168 } else {226
169 if (Array.isArray(mergedMessages[mergedMessages.length - 1].content)) {227 messages[j].content.push(...messages[i].content.filter(c => c.type === 'image'));
170 mergedMessages[mergedMessages.length - 1].content[0].text += '\n\n' + message.content;228 messages[i].content = messages[i].content.filter(c => c.type !== 'image');
171 } else {
172 mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content;
173 }229 }
174 }230 }
231
232 // Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling
233 if (prefillString) {
234 messages.push({
235 role: 'assistant',
236 // Dangling whitespace are not allowed for prefilling
237 content: [{ type: 'text', text: prefillString.trimEnd() }],
238 });
239 }
240
241 // Since the messaging endpoint only supports user assistant roles in turns, we have to merge messages with the same role if they follow eachother
242 // Also handle multi-modality, holy slop.
243 let mergedMessages = [];
244 messages.forEach((message) => {
245 if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role) {
246 mergedMessages[mergedMessages.length - 1].content.push(...message.content);
175 } else {247 } else {
176 mergedMessages.push(message);248 mergedMessages.push(message);
177 }249 }
178 if (imageData) {250 });
179 mergedMessages[mergedMessages.length - 1].content = [251
180 { type: 'text', text: mergedMessages[mergedMessages.length - 1].content[0]?.text || mergedMessages[mergedMessages.length - 1].content },252 if (!useTools) {
181 {253 mergedMessages.forEach((message) => {
182 type: 'image', source: {254 message.content.forEach((content) => {
183 type: 'base64',255 if (content.type === 'tool_use') {
184 media_type: mimeType,256 content.type = 'text';
185 data: base64Data,257 content.text = JSON.stringify(content.input);
186 },258 delete content.id;
187 },259 delete content.name;
188 ];260 delete content.input;
261 }
262 if (content.type === 'tool_result') {
263 content.type = 'text';
264 content.text = content.content;
265 delete content.tool_use_id;
266 delete content.content;
189 }267 }
190 });268 });
269 });
270 }
191271
192 return { messages: mergedMessages, systemPrompt: systemPrompt.trim() };272 return { messages: mergedMessages, systemPrompt: systemPrompt };
193}273}
194274
195/**275/**
@@ -205,7 +285,6 @@ function convertCohereMessages(messages, charName = '', userName = '') {
205 'user': 'USER',285 'user': 'USER',
206 'assistant': 'CHATBOT',286 'assistant': 'CHATBOT',
207 };287 };
208 const placeholder = '[Start a new chat]';
209 let systemPrompt = '';288 let systemPrompt = '';
210289
211 // Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array.290 // Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array.
@@ -233,12 +312,12 @@ function convertCohereMessages(messages, charName = '', userName = '') {
233 if (messages.length === 0) {312 if (messages.length === 0) {
234 messages.unshift({313 messages.unshift({
235 role: 'user',314 role: 'user',
236 content: placeholder,315 content: PROMPT_PLACEHOLDER,
237 });316 });
238 }317 }
239318
240 const lastNonSystemMessageIndex = messages.findLastIndex(msg => msg.role === 'user' || msg.role === 'assistant');319 const lastNonSystemMessageIndex = messages.findLastIndex(msg => msg.role === 'user' || msg.role === 'assistant');
241 const userPrompt = messages.slice(lastNonSystemMessageIndex).map(msg => msg.content).join('\n\n') || placeholder;320 const userPrompt = messages.slice(lastNonSystemMessageIndex).map(msg => msg.content).join('\n\n') || PROMPT_PLACEHOLDER;
242321
243 const chatHistory = messages.slice(0, lastNonSystemMessageIndex).map(msg => {322 const chatHistory = messages.slice(0, lastNonSystemMessageIndex).map(msg => {
244 return {323 return {
@@ -414,7 +493,7 @@ function convertAI21Messages(messages, charName = '', userName = '') {
414 if (messages.length === 0) {493 if (messages.length === 0) {
415 messages.unshift({494 messages.unshift({
416 role: 'user',495 role: 'user',
417 content: '[Start a new chat]',496 content: PROMPT_PLACEHOLDER,
418 });497 });
419 }498 }
420499
@@ -466,8 +545,18 @@ function convertMistralMessages(messages, charName = '', userName = '') {
466 lastMsg.prefix = true;545 lastMsg.prefix = true;
467 }546 }
468547
548 const sanitizeToolId = (id) => crypto.createHash('sha512').update(id).digest('hex').slice(0, 9);
549
469 // Doesn't support completion names, so prepend if not already done by the frontend (e.g. for group chats).550 // Doesn't support completion names, so prepend if not already done by the frontend (e.g. for group chats).
470 messages.forEach(msg => {551 messages.forEach(msg => {
552 if ('tool_calls' in msg && Array.isArray(msg.tool_calls)) {
553 msg.tool_calls.forEach(tool => {
554 tool.id = sanitizeToolId(tool.id);
555 });
556 }
557 if ('tool_call_id' in msg && msg.role === 'tool') {
558 msg.tool_call_id = sanitizeToolId(msg.tool_call_id);
559 }
471 if (msg.role === 'system' && msg.name === 'example_assistant') {560 if (msg.role === 'system' && msg.name === 'example_assistant') {
472 if (charName && !msg.content.startsWith(`${charName}: `)) {561 if (charName && !msg.content.startsWith(`${charName}: `)) {
473 msg.content = `${charName}: ${msg.content}`;562 msg.content = `${charName}: ${msg.content}`;
@@ -488,6 +577,28 @@ function convertMistralMessages(messages, charName = '', userName = '') {
488 }577 }
489 });578 });
490579
580 // If user role message immediately follows a tool message, append it to the last user message
581 const fixToolMessages = () => {
582 let rerun = true;
583 while (rerun) {
584 rerun = false;
585 messages.forEach((message, i) => {
586 if (i === messages.length - 1) {
587 return;
588 }
589 if (message.role === 'tool' && messages[i + 1].role === 'user') {
590 const lastUserMessage = messages.slice(0, i).findLastIndex(m => m.role === 'user' && m.content);
591 if (lastUserMessage !== -1) {
592 messages[lastUserMessage].content += '\n\n' + messages[i + 1].content;
593 messages.splice(i + 1, 1);
594 rerun = true;
595 }
596 }
597 });
598 }
599 };
600 fixToolMessages();
601
491 // If system role message immediately follows an assistant message, change its role to user602 // If system role message immediately follows an assistant message, change its role to user
492 for (let i = 0; i < messages.length - 1; i++) {603 for (let i = 0; i < messages.length - 1; i++) {
493 if (messages[i].role === 'assistant' && messages[i + 1].role === 'system') {604 if (messages[i].role === 'assistant' && messages[i + 1].role === 'system') {
@@ -499,6 +610,83 @@ function convertMistralMessages(messages, charName = '', userName = '') {
499}610}
500611
501/**612/**
613 * Merge messages with the same consecutive role, removing names if they exist.
614 * @param {any[]} messages Messages to merge
615 * @param {string} charName Character name
616 * @param {string} userName User name
617 * @param {boolean} strict Enable strict mode: only allow one system message at the start, force user first message
618 * @returns {any[]} Merged messages
619 */
620function mergeMessages(messages, charName, userName, strict) {
621 let mergedMessages = [];
622
623 // Remove names from the messages
624 messages.forEach((message) => {
625 if (!message.content) {
626 message.content = '';
627 }
628 if (message.role === 'system' && message.name === 'example_assistant') {
629 if (charName && !message.content.startsWith(`${charName}: `)) {
630 message.content = `${charName}: ${message.content}`;
631 }
632 }
633 if (message.role === 'system' && message.name === 'example_user') {
634 if (userName && !message.content.startsWith(`${userName}: `)) {
635 message.content = `${userName}: ${message.content}`;
636 }
637 }
638 if (message.name && message.role !== 'system') {
639 if (!message.content.startsWith(`${message.name}: `)) {
640 message.content = `${message.name}: ${message.content}`;
641 }
642 }
643 if (message.role === 'tool') {
644 message.role = 'user';
645 }
646 delete message.name;
647 delete message.tool_calls;
648 delete message.tool_call_id;
649 });
650
651 // Squash consecutive messages with the same role
652 messages.forEach((message) => {
653 if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role && message.content) {
654 mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content;
655 } else {
656 mergedMessages.push(message);
657 }
658 });
659
660 // Prevent erroring out if the messages array is empty.
661 if (messages.length === 0) {
662 messages.unshift({
663 role: 'user',
664 content: PROMPT_PLACEHOLDER,
665 });
666 }
667
668 if (strict) {
669 for (let i = 0; i < mergedMessages.length; i++) {
670 // Force mid-prompt system messages to be user messages
671 if (i > 0 && mergedMessages[i].role === 'system') {
672 mergedMessages[i].role = 'user';
673 }
674 }
675 if (mergedMessages.length) {
676 if (mergedMessages[0].role === 'system' && (mergedMessages.length === 1 || mergedMessages[1].role !== 'user')) {
677 mergedMessages.splice(1, 0, { role: 'user', content: PROMPT_PLACEHOLDER });
678 }
679 else if (mergedMessages[0].role !== 'system' && mergedMessages[0].role !== 'user') {
680 mergedMessages.unshift({ role: 'user', content: PROMPT_PLACEHOLDER });
681 }
682 }
683 return mergeMessages(mergedMessages, charName, userName, false);
684 }
685
686 return mergedMessages;
687}
688
689/**
502 * Convert a prompt from the ChatML objects to the format used by Text Completion API.690 * Convert a prompt from the ChatML objects to the format used by Text Completion API.
503 * @param {object[]} messages Array of messages691 * @param {object[]} messages Array of messages
504 * @returns {string} Prompt for Text Completion API692 * @returns {string} Prompt for Text Completion API
@@ -523,76 +711,6 @@ function convertTextCompletionPrompt(messages) {
523 return messageStrings.join('\n') + '\nassistant:';711 return messageStrings.join('\n') + '\nassistant:';
524}712}
525713
526/**
527 * Convert OpenAI Chat Completion tools to the format used by Cohere.
528 * @param {object[]} tools OpenAI Chat Completion tool definitions
529 */
530function convertCohereTools(tools) {
531 if (!Array.isArray(tools) || tools.length === 0) {
532 return [];
533 }
534
535 const jsonSchemaToPythonTypes = {
536 'string': 'str',
537 'number': 'float',
538 'integer': 'int',
539 'boolean': 'bool',
540 'array': 'list',
541 'object': 'dict',
542 };
543
544 const cohereTools = [];
545
546 for (const tool of tools) {
547 if (tool?.type !== 'function') {
548 console.log(`Unsupported tool type: ${tool.type}`);
549 continue;
550 }
551
552 const name = tool?.function?.name;
553 const description = tool?.function?.description;
554 const properties = tool?.function?.parameters?.properties;
555 const required = tool?.function?.parameters?.required;
556 const parameters = {};
557
558 if (!name) {
559 console.log('Tool name is missing');
560 continue;
561 }
562
563 if (!description) {
564 console.log('Tool description is missing');
565 }
566
567 if (!properties || typeof properties !== 'object') {
568 console.log(`No properties found for tool: ${tool?.function?.name}`);
569 continue;
570 }
571
572 for (const property in properties) {
573 const parameterDefinition = properties[property];
574 const description = parameterDefinition.description || (parameterDefinition.enum ? JSON.stringify(parameterDefinition.enum) : '');
575 const type = jsonSchemaToPythonTypes[parameterDefinition.type] || 'str';
576 const isRequired = Array.isArray(required) && required.includes(property);
577 parameters[property] = {
578 description: description,
579 type: type,
580 required: isRequired,
581 };
582 }
583
584 const cohereTool = {
585 name: tool.function.name,
586 description: tool.function.description,
587 parameter_definitions: parameters,
588 };
589
590 cohereTools.push(cohereTool);
591 }
592
593 return cohereTools;
594}
595
596module.exports = {714module.exports = {
597 convertClaudePrompt,715 convertClaudePrompt,
598 convertClaudeMessages,716 convertClaudeMessages,
@@ -600,6 +718,6 @@ module.exports = {
600 convertTextCompletionPrompt,718 convertTextCompletionPrompt,
601 convertCohereMessages,719 convertCohereMessages,
602 convertMistralMessages,720 convertMistralMessages,
603 convertCohereTools,
604 convertAI21Messages,721 convertAI21Messages,
722 mergeMessages,
605};723};