mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-02-21 22:47:41 +01:00
Merge branch 'SillyTavern:staging' into staging
This commit is contained in:
commit
678a0ee136
41
public/global.d.ts
vendored
41
public/global.d.ts
vendored
@ -1358,3 +1358,44 @@ declare namespace moment {
|
||||
declare global {
|
||||
const moment: typeof moment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback data for the `LLM_FUNCTION_TOOL_REGISTER` event type that is triggered when a function tool can be registered.
|
||||
*/
|
||||
interface FunctionToolRegister {
|
||||
/**
|
||||
* The type of generation that is being used
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* Generation data, including messages and sampling parameters
|
||||
*/
|
||||
data: Record<string, object>;
|
||||
/**
|
||||
* Callback to register an LLM function tool.
|
||||
*/
|
||||
registerFunctionTool: typeof registerFunctionTool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback data for the `LLM_FUNCTION_TOOL_REGISTER` event type that is triggered when a function tool is registered.
|
||||
* @param name Name of the function tool to register
|
||||
* @param description Description of the function tool
|
||||
* @param params JSON schema for the parameters of the function tool
|
||||
* @param required Whether the function tool should be forced to be used
|
||||
*/
|
||||
declare function registerFunctionTool(name: string, description: string, params: object, required: boolean): Promise<void>;
|
||||
|
||||
/**
|
||||
* Callback data for the `LLM_FUNCTION_TOOL_CALL` event type that is triggered when a function tool is called.
|
||||
*/
|
||||
interface FunctionToolCall {
|
||||
/**
|
||||
* Name of the function tool to call
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* JSON object with the parameters to pass to the function tool
|
||||
*/
|
||||
arguments: string;
|
||||
}
|
||||
|
@ -1739,6 +1739,16 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,cohere,mistralai,custom">
|
||||
<label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
|
||||
<input id="openai_function_calling" type="checkbox" />
|
||||
<span data-i18n="Enable function calling">Enable function calling</span>
|
||||
<div class="flexBasis100p toggle-description justifyLeft">
|
||||
Allows using <a href="https://platform.openai.com/docs/guides/function-calling" target="_blank">function tools</a>.
|
||||
Can be utilized by various extensions to provide additional functionality.
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,openrouter,makersuite,claude,custom">
|
||||
<label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
|
||||
<input id="openai_image_inlining" type="checkbox" />
|
||||
|
@ -427,6 +427,8 @@ export const event_types = {
|
||||
FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',
|
||||
WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',
|
||||
OPEN_CHARACTER_LIBRARY: 'open_character_library',
|
||||
LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register',
|
||||
LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call',
|
||||
};
|
||||
|
||||
export const eventSource = new EventEmitter();
|
||||
@ -4180,7 +4182,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
||||
const displayIncomplete = type === 'quiet' && !quietToLoud;
|
||||
getMessage = cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncomplete);
|
||||
|
||||
if (getMessage.length > 0) {
|
||||
if (getMessage.length > 0 || data.allowEmptyResponse) {
|
||||
if (isImpersonate) {
|
||||
$('#send_textarea').val(getMessage)[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||||
generatedPromptCache = '';
|
||||
|
@ -68,6 +68,7 @@
|
||||
* @property {number} depth_prompt.depth - The level of detail or nuance targeted by the prompt.
|
||||
* @property {string} depth_prompt.prompt - The actual prompt text used for deeper character interaction.
|
||||
* @property {"system" | "user" | "assistant"} depth_prompt.role - The role the character takes on during the prompted interaction (system, user, or assistant).
|
||||
* @property {RegexScriptData[]} regex_scripts - Custom regex scripts for the character.
|
||||
* // Non-standard extensions added by external tools
|
||||
* @property {string} [pygmalion_id] - The unique identifier assigned to the character by the Pygmalion.chat.
|
||||
* @property {string} [github_repo] - The gitHub repository associated with the character.
|
||||
@ -76,6 +77,23 @@
|
||||
* @property {{source: string[]}} [risuai] - The RisuAI-specific data associated with the character.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} RegexScriptData
|
||||
* @property {string} id - UUID of the script
|
||||
* @property {string} scriptName - The name of the script
|
||||
* @property {string} findRegex - The regex to find
|
||||
* @property {string} replaceString - The string to replace
|
||||
* @property {string[]} trimStrings - The strings to trim
|
||||
* @property {number[]} placement - The placement of the script
|
||||
* @property {boolean} disabled - Whether the script is disabled
|
||||
* @property {boolean} markdownOnly - Whether the script only applies to Markdown
|
||||
* @property {boolean} promptOnly - Whether the script only applies to prompts
|
||||
* @property {boolean} runOnEdit - Whether the script runs on edit
|
||||
* @property {boolean} substituteRegex - Whether the regex should be substituted
|
||||
* @property {number} minDepth - The minimum depth
|
||||
* @property {number} maxDepth - The maximum depth
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} v1CharData
|
||||
* @property {string} name - the name of the character
|
||||
|
@ -122,7 +122,9 @@ const extension_settings = {
|
||||
custom: [],
|
||||
},
|
||||
dice: {},
|
||||
/** @type {import('./char-data.js').RegexScriptData[]} */
|
||||
regex: [],
|
||||
character_allowed_regex: [],
|
||||
tts: {},
|
||||
sd: {
|
||||
prompts: {},
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHeaders, saveSettingsDebounced, substituteParams } from '../../../script.js';
|
||||
import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHeaders, online_status, saveSettingsDebounced, substituteParams } from '../../../script.js';
|
||||
import { dragElement, isMobile } from '../../RossAscends-mods.js';
|
||||
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { loadMovingUIState, power_user } from '../../power-user.js';
|
||||
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence } from '../../utils.js';
|
||||
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition } from '../../utils.js';
|
||||
import { hideMutedSprites } from '../../group-chats.js';
|
||||
import { isJsonSchemaSupported } from '../../textgen-settings.js';
|
||||
import { debounce_timeout } from '../../constants.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
import { isFunctionCallingSupported } from '../../openai.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'expressions';
|
||||
@ -16,6 +17,7 @@ const UPDATE_INTERVAL = 2000;
|
||||
const STREAMING_UPDATE_INTERVAL = 10000;
|
||||
const TALKINGCHECK_UPDATE_INTERVAL = 500;
|
||||
const DEFAULT_FALLBACK_EXPRESSION = 'joy';
|
||||
const FUNCTION_NAME = 'set_emotion';
|
||||
const DEFAULT_LLM_PROMPT = 'Pause your roleplay. Classify the emotion of the last message. Output just one word, e.g. "joy" or "anger". Choose only one of the following labels: {{labels}}';
|
||||
const DEFAULT_EXPRESSIONS = [
|
||||
'talkinghead',
|
||||
@ -1001,6 +1003,10 @@ async function getLlmPrompt(labels) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isFunctionCallingSupported()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const labelsString = labels.map(x => `"${x}"`).join(', ');
|
||||
const prompt = substituteParams(String(extension_settings.expressions.llmPrompt))
|
||||
.replace(/{{labels}}/gi, labelsString);
|
||||
@ -1014,11 +1020,16 @@ async function getLlmPrompt(labels) {
|
||||
* @returns {string} The parsed emotion or the fallback expression.
|
||||
*/
|
||||
function parseLlmResponse(emotionResponse, labels) {
|
||||
const fallbackExpression = getFallbackExpression();
|
||||
|
||||
try {
|
||||
const parsedEmotion = JSON.parse(emotionResponse);
|
||||
return parsedEmotion?.emotion ?? fallbackExpression;
|
||||
const response = parsedEmotion?.emotion?.trim()?.toLowerCase();
|
||||
|
||||
if (!response || !labels.includes(response)) {
|
||||
console.debug(`Parsed emotion response: ${response} not in labels: ${labels}`);
|
||||
throw new Error('Emotion not in labels');
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch {
|
||||
const fuse = new Fuse(labels, { includeScore: true });
|
||||
console.debug('Using fuzzy search in labels:', labels);
|
||||
@ -1032,6 +1043,41 @@ function parseLlmResponse(emotionResponse, labels) {
|
||||
throw new Error('Could not parse emotion response ' + emotionResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the function tool for the LLM API.
|
||||
* @param {FunctionToolRegister} args Function tool register arguments.
|
||||
*/
|
||||
function onFunctionToolRegister(args) {
|
||||
if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isFunctionCallingSupported()) {
|
||||
// Only trigger on quiet mode
|
||||
if (args.type !== 'quiet') {
|
||||
return;
|
||||
}
|
||||
|
||||
const emotions = DEFAULT_EXPRESSIONS.filter((e) => e != 'talkinghead');
|
||||
const jsonSchema = {
|
||||
$schema: 'http://json-schema.org/draft-04/schema#',
|
||||
type: 'object',
|
||||
properties: {
|
||||
emotion: {
|
||||
type: 'string',
|
||||
enum: emotions,
|
||||
description: `One of the following: ${JSON.stringify(emotions)}`,
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'emotion',
|
||||
],
|
||||
};
|
||||
args.registerFunctionTool(
|
||||
FUNCTION_NAME,
|
||||
substituteParams('Sets the label that best describes the current emotional state of {{char}}. Only select one of the enumerated values.'),
|
||||
jsonSchema,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function onTextGenSettingsReady(args) {
|
||||
// Only call if inside an API call
|
||||
if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) {
|
||||
@ -1087,11 +1133,27 @@ async function getExpressionLabel(text) {
|
||||
} break;
|
||||
// Using LLM
|
||||
case EXPRESSION_API.llm: {
|
||||
try {
|
||||
await waitUntilCondition(() => online_status !== 'no_connection', 3000, 250);
|
||||
} catch (error) {
|
||||
console.warn('No LLM connection. Using fallback expression', error);
|
||||
return getFallbackExpression();
|
||||
}
|
||||
|
||||
const expressionsList = await getExpressionsList();
|
||||
const prompt = await getLlmPrompt(expressionsList);
|
||||
let functionResult = null;
|
||||
eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);
|
||||
eventSource.once(event_types.LLM_FUNCTION_TOOL_REGISTER, onFunctionToolRegister);
|
||||
eventSource.once(event_types.LLM_FUNCTION_TOOL_CALL, (/** @type {FunctionToolCall} */ args) => {
|
||||
if (args.name !== FUNCTION_NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
functionResult = args?.arguments;
|
||||
});
|
||||
const emotionResponse = await generateQuietPrompt(prompt, false, false);
|
||||
return parseLlmResponse(emotionResponse, expressionsList);
|
||||
return parseLlmResponse(functionResult || emotionResponse, expressionsList);
|
||||
}
|
||||
// Extras
|
||||
default: {
|
||||
|
@ -34,7 +34,7 @@
|
||||
<i class="fa-solid fa-clock-rotate-left fa-sm"></i>
|
||||
</div>
|
||||
</label>
|
||||
<small>Will be used if the API doesn't support JSON schemas.</small>
|
||||
<small>Will be used if the API doesn't support JSON schemas or function calling.</small>
|
||||
<textarea id="expression_llm_prompt" type="text" class="text_pole textarea_compact" rows="2" placeholder="Use {{labels}} special macro."></textarea>
|
||||
</div>
|
||||
<div class="expression_fallback_block m-b-1 m-t-1">
|
||||
|
@ -1,24 +1,52 @@
|
||||
<div class="regex_settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>Regex</b>
|
||||
<b data-i18n="ext_regex_title">
|
||||
Regex
|
||||
</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<div class="flex-container">
|
||||
<div id="open_regex_editor" class="menu_button">
|
||||
<div id="open_regex_editor" class="menu_button menu_button_icon" title="New global regex script">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
<span data-i18n="ext_regex_open_editor">Open Editor</span>
|
||||
<small data-i18n="ext_regex_new_global_script">+ Global</small>
|
||||
</div>
|
||||
<div id="import_regex" class="menu_button">
|
||||
<div id="open_scoped_editor" class="menu_button menu_button_icon" title="New scoped regex script">
|
||||
<i class="fa-solid fa-address-card"></i>
|
||||
<small data-i18n="ext_regex_new_scoped_script">+ Scoped</small>
|
||||
</div>
|
||||
<div id="import_regex" class="menu_button menu_button_icon">
|
||||
<i class="fa-solid fa-file-import"></i>
|
||||
<span data-i18n="ext_regex_import_script">Import Script</span>
|
||||
<small data-i18n="ext_regex_import_script">Import</small>
|
||||
</div>
|
||||
<input type="file" id="import_regex_file" hidden accept="*.json" multiple />
|
||||
</div>
|
||||
<hr />
|
||||
<label data-i18n="ext_regex_saved_scripts">Saved Scripts</label>
|
||||
<div id="saved_regex_scripts" class="flex-container regex-script-container flexFlowColumn"></div>
|
||||
<div id="global_scripts_block" class="padding5">
|
||||
<div>
|
||||
<strong data-i18n="ext_regex_global_scripts">Global Scripts</strong>
|
||||
</div>
|
||||
<small data-i18n="ext_regex_global_scripts_desc">
|
||||
Available for all characters. Saved to local settings.
|
||||
</small>
|
||||
<div id="saved_regex_scripts" class="flex-container regex-script-container flexFlowColumn"></div>
|
||||
</div>
|
||||
<hr />
|
||||
<div id="scoped_scripts_block" class="padding5">
|
||||
<div class="flex-container alignItemsBaseline">
|
||||
<strong class="flex1" data-i18n="ext_regex_scoped_scripts">Scoped Scripts</strong>
|
||||
<label id="toggle_scoped_regex" class="checkbox flex-container" for="regex_scoped_toggle">
|
||||
<input type="checkbox" id="regex_scoped_toggle" class="enable_scoped" />
|
||||
<span class="regex-toggle-on fa-solid fa-toggle-on fa-lg" title="Disallow using scoped regex"></span>
|
||||
<span class="regex-toggle-off fa-solid fa-toggle-off fa-lg" title="Allow using scoped regex"></span>
|
||||
</label>
|
||||
</div>
|
||||
<small data-i18n="ext_regex_scoped_scripts_desc">
|
||||
Only available for this character. Saved to the card data.
|
||||
</small>
|
||||
<div id="saved_scoped_scripts" class="flex-container regex-script-container flexFlowColumn"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -56,7 +56,7 @@
|
||||
<div>
|
||||
<textarea
|
||||
class="regex_replace_string text_pole wide100p textarea_compact"
|
||||
data-i18n="[placeholder]ext_regex_replace_string_placeholder"
|
||||
data-i18n="[placeholder]ext_regex_replace_string_placeholder"
|
||||
placeholder="Use {{match}} to include the matched text from the Find Regex or $1, $2, etc. for capture groups."
|
||||
rows="2"
|
||||
></textarea>
|
||||
|
5
public/scripts/extensions/regex/embeddedScripts.html
Normal file
5
public/scripts/extensions/regex/embeddedScripts.html
Normal file
@ -0,0 +1,5 @@
|
||||
<div>
|
||||
<h3>This character has embedded regex script(s).</h3>
|
||||
<h3>Would you like to allow using them?</h3>
|
||||
<div class="m-b-1">If you want to do it later, select "Regex" from the extensions menu.</div>
|
||||
</div>
|
@ -1,4 +1,4 @@
|
||||
import { substituteParams } from '../../../script.js';
|
||||
import { characters, substituteParams, this_chid } from '../../../script.js';
|
||||
import { extension_settings } from '../../extensions.js';
|
||||
import { regexFromString } from '../../utils.js';
|
||||
export {
|
||||
@ -22,6 +22,22 @@ const regex_placement = {
|
||||
WORLD_INFO: 5,
|
||||
};
|
||||
|
||||
function getScopedRegex() {
|
||||
const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar);
|
||||
|
||||
if (!isAllowed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const scripts = characters[this_chid]?.data?.extensions?.regex_scripts;
|
||||
|
||||
if (!Array.isArray(scripts)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return scripts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parent function to fetch a regexed version of a raw string
|
||||
* @param {string} rawString The raw string to be regexed
|
||||
@ -42,7 +58,8 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
|
||||
return finalString;
|
||||
}
|
||||
|
||||
extension_settings.regex.forEach((script) => {
|
||||
const allRegex = [...(extension_settings.regex ?? []), ...(getScopedRegex() ?? [])];
|
||||
allRegex.forEach((script) => {
|
||||
if (
|
||||
// Script applies to Markdown and input is Markdown
|
||||
(script.markdownOnly && isMarkdown) ||
|
||||
@ -95,7 +112,7 @@ function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
|
||||
}
|
||||
|
||||
// Run replacement. Currently does not support the Overlay strategy
|
||||
newString = rawString.replace(findRegex, function(match) {
|
||||
newString = rawString.replace(findRegex, function (match) {
|
||||
const args = [...arguments];
|
||||
const replaceString = regexScript.replaceString.replace(/{{match}}/gi, '$0');
|
||||
const replaceWithGroups = replaceString.replaceAll(/\$(\d+)/g, (_, num) => {
|
||||
|
19
public/scripts/extensions/regex/importTarget.html
Normal file
19
public/scripts/extensions/regex/importTarget.html
Normal file
@ -0,0 +1,19 @@
|
||||
<div>
|
||||
<h3 data-i18n="ext_regex_import_target">
|
||||
Import To:
|
||||
</h3>
|
||||
<div class="flex-container flexFlowColumn wide100p padding10 justifyLeft">
|
||||
<label for="regex_import_target_global">
|
||||
<input type="radio" name="regex_import_target" id="regex_import_target_global" value="global" checked />
|
||||
<span data-i18n="ext_regex_global_scripts">
|
||||
Global Scripts
|
||||
</span>
|
||||
</label>
|
||||
<label for="regex_import_target_scoped">
|
||||
<input type="radio" name="regex_import_target" id="regex_import_target_scoped" value="scoped" />
|
||||
<span data-i18n="ext_regex_scoped_scripts">
|
||||
Scoped Scripts
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
@ -1,5 +1,6 @@
|
||||
import { callPopup, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced } from '../../../script.js';
|
||||
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { callPopup, characters, eventSource, event_types, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced, this_chid } from '../../../script.js';
|
||||
import { extension_settings, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js';
|
||||
import { selected_group } from '../../group-chats.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
@ -7,8 +8,21 @@ import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js'
|
||||
import { resolveVariable } from '../../variables.js';
|
||||
import { regex_placement, runRegexScript } from './engine.js';
|
||||
|
||||
async function saveRegexScript(regexScript, existingScriptIndex) {
|
||||
/**
|
||||
* Saves a regex script to the extension settings or character data.
|
||||
* @param {import('../../char-data.js').RegexScriptData} regexScript
|
||||
* @param {number} existingScriptIndex Index of the existing script
|
||||
* @param {boolean} isScoped Is the script scoped to a character?
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function saveRegexScript(regexScript, existingScriptIndex, isScoped) {
|
||||
// If not editing
|
||||
const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
|
||||
|
||||
// Assign a UUID if it doesn't exist
|
||||
if (!regexScript.id) {
|
||||
regexScript.id = uuidv4();
|
||||
}
|
||||
|
||||
// Is the script name undefined or empty?
|
||||
if (!regexScript.scriptName) {
|
||||
@ -16,22 +30,6 @@ async function saveRegexScript(regexScript, existingScriptIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingScriptIndex === -1) {
|
||||
// Does the script name already exist?
|
||||
if (extension_settings.regex.find((e) => e.scriptName === regexScript.scriptName)) {
|
||||
toastr.error(`Could not save regex script: A script with name ${regexScript.scriptName} already exists.`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Does the script name already exist somewhere else?
|
||||
// (If this fails, make it a .filter().map() to index array)
|
||||
const foundIndex = extension_settings.regex.findIndex((e) => e.scriptName === regexScript.scriptName);
|
||||
if (foundIndex !== existingScriptIndex && foundIndex !== -1) {
|
||||
toastr.error(`Could not save regex script: A script with name ${regexScript.scriptName} already exists.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Is a find regex present?
|
||||
if (regexScript.findRegex.length === 0) {
|
||||
toastr.warning('This regex script will not work, but was saved anyway: A find regex isn\'t present.');
|
||||
@ -43,9 +41,18 @@ async function saveRegexScript(regexScript, existingScriptIndex) {
|
||||
}
|
||||
|
||||
if (existingScriptIndex !== -1) {
|
||||
extension_settings.regex[existingScriptIndex] = regexScript;
|
||||
array[existingScriptIndex] = regexScript;
|
||||
} else {
|
||||
extension_settings.regex.push(regexScript);
|
||||
array.push(regexScript);
|
||||
}
|
||||
|
||||
if (isScoped) {
|
||||
await writeExtensionField(this_chid, 'regex_scripts', array);
|
||||
|
||||
// Add the character to the allowed list
|
||||
if (!extension_settings.character_allowed_regex.includes(characters[this_chid].avatar)) {
|
||||
extension_settings.character_allowed_regex.push(characters[this_chid].avatar);
|
||||
}
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
@ -58,12 +65,16 @@ async function saveRegexScript(regexScript, existingScriptIndex) {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRegexScript({ existingId }) {
|
||||
let scriptName = $(`#${existingId}`).find('.regex_script_name').text();
|
||||
async function deleteRegexScript({ id, isScoped }) {
|
||||
const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
|
||||
|
||||
const existingScriptIndex = extension_settings.regex.findIndex((script) => script.scriptName === scriptName);
|
||||
const existingScriptIndex = array.findIndex((script) => script.id === id);
|
||||
if (!existingScriptIndex || existingScriptIndex !== -1) {
|
||||
extension_settings.regex.splice(existingScriptIndex, 1);
|
||||
array.splice(existingScriptIndex, 1);
|
||||
|
||||
if (isScoped) {
|
||||
await writeExtensionField(this_chid, 'regex_scripts', array);
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
await loadRegexScripts();
|
||||
@ -72,19 +83,32 @@ async function deleteRegexScript({ existingId }) {
|
||||
|
||||
async function loadRegexScripts() {
|
||||
$('#saved_regex_scripts').empty();
|
||||
$('#saved_scoped_scripts').empty();
|
||||
|
||||
const scriptTemplate = $(await renderExtensionTemplateAsync('regex', 'scriptTemplate'));
|
||||
|
||||
extension_settings.regex.forEach((script) => {
|
||||
/**
|
||||
* Renders a script to the UI.
|
||||
* @param {string} container Container to render the script to
|
||||
* @param {import('../../char-data.js').RegexScriptData} script Script data
|
||||
* @param {boolean} isScoped Script is scoped to a character
|
||||
* @param {number} index Index of the script in the array
|
||||
*/
|
||||
function renderScript(container, script, isScoped, index) {
|
||||
// Have to clone here
|
||||
const scriptHtml = scriptTemplate.clone();
|
||||
scriptHtml.attr('id', uuidv4());
|
||||
const save = () => saveRegexScript(script, index, isScoped);
|
||||
|
||||
if (!script.id) {
|
||||
script.id = uuidv4();
|
||||
}
|
||||
|
||||
scriptHtml.attr('id', script.id);
|
||||
scriptHtml.find('.regex_script_name').text(script.scriptName);
|
||||
scriptHtml.find('.disable_regex').prop('checked', script.disabled ?? false)
|
||||
.on('input', function () {
|
||||
.on('input', async function () {
|
||||
script.disabled = !!$(this).prop('checked');
|
||||
reloadCurrentChat();
|
||||
saveSettingsDebounced();
|
||||
await save();
|
||||
});
|
||||
scriptHtml.find('.regex-toggle-on').on('click', function () {
|
||||
scriptHtml.find('.disable_regex').prop('checked', true).trigger('input');
|
||||
@ -93,7 +117,37 @@ async function loadRegexScripts() {
|
||||
scriptHtml.find('.disable_regex').prop('checked', false).trigger('input');
|
||||
});
|
||||
scriptHtml.find('.edit_existing_regex').on('click', async function () {
|
||||
await onRegexEditorOpenClick(scriptHtml.attr('id'));
|
||||
await onRegexEditorOpenClick(scriptHtml.attr('id'), isScoped);
|
||||
});
|
||||
scriptHtml.find('.move_to_global').on('click', async function () {
|
||||
const confirm = await callPopup('Are you sure you want to move this regex script to global?', 'confirm');
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteRegexScript({ id: script.id, isScoped: true });
|
||||
await saveRegexScript(script, -1, false);
|
||||
});
|
||||
scriptHtml.find('.move_to_scoped').on('click', async function () {
|
||||
if (this_chid === undefined) {
|
||||
toastr.error('No character selected.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected_group) {
|
||||
toastr.error('Cannot edit scoped scripts in group chats.');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirm = await callPopup('Are you sure you want to move this regex script to scoped?', 'confirm');
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteRegexScript({ id: script.id, isScoped: false });
|
||||
await saveRegexScript(script, -1, true);
|
||||
});
|
||||
scriptHtml.find('.export_regex').on('click', async function () {
|
||||
const fileName = `${script.scriptName.replace(/[\s.<>:"/\\|?*\x00-\x1F\x7F]/g, '_').toLowerCase()}.json`;
|
||||
@ -107,23 +161,36 @@ async function loadRegexScripts() {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteRegexScript({ existingId: scriptHtml.attr('id') });
|
||||
await deleteRegexScript({ id: script.id, isScoped });
|
||||
await reloadCurrentChat();
|
||||
});
|
||||
|
||||
$('#saved_regex_scripts').append(scriptHtml);
|
||||
});
|
||||
$(container).append(scriptHtml);
|
||||
}
|
||||
|
||||
extension_settings?.regex?.forEach((script, index, array) => renderScript('#saved_regex_scripts', script, false, index, array));
|
||||
characters[this_chid]?.data?.extensions?.regex_scripts?.forEach((script, index, array) => renderScript('#saved_scoped_scripts', script, true, index, array));
|
||||
|
||||
const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar);
|
||||
$('#regex_scoped_toggle').prop('checked', isAllowed);
|
||||
}
|
||||
|
||||
async function onRegexEditorOpenClick(existingId) {
|
||||
/**
|
||||
* Opens the regex editor.
|
||||
* @param {string|boolean} existingId Existing ID
|
||||
* @param {boolean} isScoped Is the script scoped to a character?
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function onRegexEditorOpenClick(existingId, isScoped) {
|
||||
const editorHtml = $(await renderExtensionTemplateAsync('regex', 'editor'));
|
||||
const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
|
||||
|
||||
// If an ID exists, fill in all the values
|
||||
let existingScriptIndex = -1;
|
||||
if (existingId) {
|
||||
const existingScriptName = $(`#${existingId}`).find('.regex_script_name').text();
|
||||
existingScriptIndex = extension_settings.regex.findIndex((script) => script.scriptName === existingScriptName);
|
||||
existingScriptIndex = array.findIndex((script) => script.id === existingId);
|
||||
if (existingScriptIndex !== -1) {
|
||||
const existingScript = extension_settings.regex[existingScriptIndex];
|
||||
const existingScript = array[existingScriptIndex];
|
||||
if (existingScript.scriptName) {
|
||||
editorHtml.find('.regex_script_name').val(existingScript.scriptName);
|
||||
} else {
|
||||
@ -173,6 +240,7 @@ async function onRegexEditorOpenClick(existingId) {
|
||||
}
|
||||
|
||||
const testScript = {
|
||||
id: uuidv4(),
|
||||
scriptName: editorHtml.find('.regex_script_name').val(),
|
||||
findRegex: editorHtml.find('.find_regex').val(),
|
||||
replaceString: editorHtml.find('.regex_replace_string').val(),
|
||||
@ -189,9 +257,10 @@ async function onRegexEditorOpenClick(existingId) {
|
||||
const popupResult = await callPopup(editorHtml, 'confirm', undefined, { okButton: 'Save' });
|
||||
if (popupResult) {
|
||||
const newRegexScript = {
|
||||
scriptName: editorHtml.find('.regex_script_name').val(),
|
||||
findRegex: editorHtml.find('.find_regex').val(),
|
||||
replaceString: editorHtml.find('.regex_replace_string').val(),
|
||||
id: existingId ? String(existingId) : uuidv4(),
|
||||
scriptName: String(editorHtml.find('.regex_script_name').val()),
|
||||
findRegex: String(editorHtml.find('.find_regex').val()),
|
||||
replaceString: String(editorHtml.find('.regex_replace_string').val()),
|
||||
trimStrings: editorHtml.find('.regex_trim_strings').val().split('\n').filter((e) => e.length !== 0) || [],
|
||||
placement:
|
||||
editorHtml
|
||||
@ -209,7 +278,7 @@ async function onRegexEditorOpenClick(existingId) {
|
||||
maxDepth: parseInt(String(editorHtml.find('input[name="max_depth"]').val())),
|
||||
};
|
||||
|
||||
saveRegexScript(newRegexScript, existingScriptIndex);
|
||||
saveRegexScript(newRegexScript, existingScriptIndex, isScoped);
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,6 +289,11 @@ function migrateSettings() {
|
||||
|
||||
// Current: If MD Display is present in placement, remove it and add new placements/MD option
|
||||
extension_settings.regex.forEach((script) => {
|
||||
if (!script.id) {
|
||||
script.id = uuidv4();
|
||||
performSave = true;
|
||||
}
|
||||
|
||||
if (script.placement.includes(regex_placement.MD_DISPLAY)) {
|
||||
script.placement = script.placement.length === 1 ?
|
||||
Object.values(regex_placement).filter((e) => e !== regex_placement.MD_DISPLAY) :
|
||||
@ -242,6 +316,11 @@ function migrateSettings() {
|
||||
}
|
||||
});
|
||||
|
||||
if (!extension_settings.character_allowed_regex) {
|
||||
extension_settings.character_allowed_regex = [];
|
||||
performSave = true;
|
||||
}
|
||||
|
||||
if (performSave) {
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
@ -260,8 +339,9 @@ function runRegexCallback(args, value) {
|
||||
}
|
||||
|
||||
const scriptName = String(resolveVariable(args.name));
|
||||
const scripts = [...(extension_settings.regex ?? []), ...(characters[this_chid]?.data?.extensions?.regex_scripts ?? [])];
|
||||
|
||||
for (const script of extension_settings.regex) {
|
||||
for (const script of scripts) {
|
||||
if (String(script.scriptName).toLowerCase() === String(scriptName).toLowerCase()) {
|
||||
if (script.disabled) {
|
||||
toastr.warning(`Regex script "${scriptName}" is disabled.`);
|
||||
@ -280,8 +360,9 @@ function runRegexCallback(args, value) {
|
||||
/**
|
||||
* Performs the import of the regex file.
|
||||
* @param {File} file Input file
|
||||
* @param {boolean} isScoped Is the script scoped to a character?
|
||||
*/
|
||||
async function onRegexImportFileChange(file) {
|
||||
async function onRegexImportFileChange(file, isScoped) {
|
||||
if (!file) {
|
||||
toastr.error('No file provided.');
|
||||
return;
|
||||
@ -294,7 +375,15 @@ async function onRegexImportFileChange(file) {
|
||||
throw new Error('No script name provided.');
|
||||
}
|
||||
|
||||
extension_settings.regex.push(regexScript);
|
||||
// Assign a new UUID
|
||||
regexScript.id = uuidv4();
|
||||
|
||||
const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
|
||||
array.push(regexScript);
|
||||
|
||||
if (isScoped) {
|
||||
await writeExtensionField(this_chid, 'regex_scripts', array);
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
await loadRegexScripts();
|
||||
@ -306,6 +395,47 @@ async function onRegexImportFileChange(file) {
|
||||
}
|
||||
}
|
||||
|
||||
function purgeEmbeddedRegexScripts( { character }){
|
||||
const avatar = character?.avatar;
|
||||
|
||||
if (avatar && extension_settings.character_allowed_regex?.includes(avatar)) {
|
||||
const index = extension_settings.character_allowed_regex.indexOf(avatar);
|
||||
if (index !== -1) {
|
||||
extension_settings.character_allowed_regex.splice(index, 1);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkEmbeddedRegexScripts() {
|
||||
const chid = this_chid;
|
||||
|
||||
if (chid !== undefined && !selected_group) {
|
||||
const avatar = characters[chid]?.avatar;
|
||||
const scripts = characters[chid]?.data?.extensions?.regex_scripts;
|
||||
|
||||
if (Array.isArray(scripts) && scripts.length > 0) {
|
||||
if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) {
|
||||
const checkKey = `AlertRegex_${characters[chid].avatar}`;
|
||||
|
||||
if (!localStorage.getItem(checkKey)) {
|
||||
localStorage.setItem(checkKey, 'true');
|
||||
const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {});
|
||||
const result = await callPopup(template, 'confirm', '', { okButton: 'Yes' });
|
||||
|
||||
if (result) {
|
||||
extension_settings.character_allowed_regex.push(avatar);
|
||||
await reloadCurrentChat();
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadRegexScripts();
|
||||
}
|
||||
|
||||
// Workaround for loading in sequence with other extensions
|
||||
// NOTE: Always puts extension at the top of the list, but this is fine since it's static
|
||||
jQuery(async () => {
|
||||
@ -321,12 +451,32 @@ jQuery(async () => {
|
||||
const settingsHtml = $(await renderExtensionTemplateAsync('regex', 'dropdown'));
|
||||
$('#extensions_settings2').append(settingsHtml);
|
||||
$('#open_regex_editor').on('click', function () {
|
||||
onRegexEditorOpenClick(false);
|
||||
onRegexEditorOpenClick(false, false);
|
||||
});
|
||||
$('#open_scoped_editor').on('click', function () {
|
||||
if (this_chid === undefined) {
|
||||
toastr.error('No character selected.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected_group) {
|
||||
toastr.error('Cannot edit scoped scripts in group chats.');
|
||||
return;
|
||||
}
|
||||
|
||||
onRegexEditorOpenClick(false, true);
|
||||
});
|
||||
$('#import_regex_file').on('change', async function () {
|
||||
let target = 'global';
|
||||
const template = $(await renderExtensionTemplateAsync('regex', 'importTarget'));
|
||||
template.find('#regex_import_target_global').on('input', () => target = 'global');
|
||||
template.find('#regex_import_target_scoped').on('input', () => target = 'scoped');
|
||||
|
||||
await callPopup(template, 'text');
|
||||
|
||||
const inputElement = this instanceof HTMLInputElement && this;
|
||||
for (const file of inputElement.files) {
|
||||
await onRegexImportFileChange(file);
|
||||
await onRegexImportFileChange(file, target === 'scoped');
|
||||
}
|
||||
inputElement.value = '';
|
||||
});
|
||||
@ -334,30 +484,75 @@ jQuery(async () => {
|
||||
$('#import_regex_file').trigger('click');
|
||||
});
|
||||
|
||||
$('#saved_regex_scripts').sortable({
|
||||
delay: getSortableDelay(),
|
||||
stop: function () {
|
||||
let newScripts = [];
|
||||
$('#saved_regex_scripts').children().each(function () {
|
||||
const scriptName = $(this).find('.regex_script_name').text();
|
||||
const existingScript = extension_settings.regex.find((e) => e.scriptName === scriptName);
|
||||
if (existingScript) {
|
||||
newScripts.push(existingScript);
|
||||
}
|
||||
});
|
||||
|
||||
extension_settings.regex = newScripts;
|
||||
saveSettingsDebounced();
|
||||
|
||||
console.debug('Regex scripts reordered');
|
||||
// TODO: Maybe reload regex scripts after move
|
||||
let sortableDatas = [
|
||||
{
|
||||
selector: '#saved_regex_scripts',
|
||||
setter: x => extension_settings.regex = x,
|
||||
getter: () => extension_settings.regex ?? [],
|
||||
},
|
||||
{
|
||||
selector: '#saved_scoped_scripts',
|
||||
setter: x => writeExtensionField(this_chid, 'regex_scripts', x),
|
||||
getter: () => characters[this_chid]?.data?.extensions?.regex_scripts ?? [],
|
||||
},
|
||||
];
|
||||
for (const { selector, setter, getter } of sortableDatas) {
|
||||
$(selector).sortable({
|
||||
delay: getSortableDelay(),
|
||||
stop: async function () {
|
||||
const oldScripts = getter();
|
||||
const newScripts = [];
|
||||
$(selector).children().each(function () {
|
||||
const id = $(this).attr('id');
|
||||
const existingScript = oldScripts.find((e) => e.id === id);
|
||||
if (existingScript) {
|
||||
newScripts.push(existingScript);
|
||||
}
|
||||
});
|
||||
|
||||
await setter(newScripts);
|
||||
saveSettingsDebounced();
|
||||
|
||||
console.debug(`Regex scripts in ${selector} reordered`);
|
||||
await loadRegexScripts();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$('#regex_scoped_toggle').on('input', function () {
|
||||
if (this_chid === undefined) {
|
||||
toastr.error('No character selected.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected_group) {
|
||||
toastr.error('Cannot edit scoped scripts in group chats.');
|
||||
return;
|
||||
}
|
||||
|
||||
const isEnable = !!$(this).prop('checked');
|
||||
const avatar = characters[this_chid].avatar;
|
||||
|
||||
if (isEnable) {
|
||||
if (!extension_settings.character_allowed_regex.includes(avatar)) {
|
||||
extension_settings.character_allowed_regex.push(avatar);
|
||||
}
|
||||
} else {
|
||||
const index = extension_settings.character_allowed_regex.indexOf(avatar);
|
||||
if (index !== -1) {
|
||||
extension_settings.character_allowed_regex.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
reloadCurrentChat();
|
||||
});
|
||||
|
||||
await loadRegexScripts();
|
||||
$('#saved_regex_scripts').sortable('enable');
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'regex',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'regex',
|
||||
callback: runRegexCallback,
|
||||
returns: 'replaced text',
|
||||
namedArgumentList: [
|
||||
@ -373,4 +568,6 @@ jQuery(async () => {
|
||||
helpString: 'Runs a Regex extension script by name on the provided string. The script must be enabled.',
|
||||
}));
|
||||
|
||||
eventSource.on(event_types.CHAT_CHANGED, checkEmbeddedRegexScripts);
|
||||
eventSource.on(event_types.CHARACTER_DELETED, purgeEmbeddedRegexScripts);
|
||||
});
|
||||
|
@ -10,6 +10,12 @@
|
||||
<div class="edit_existing_regex menu_button" data-i18n="[title]ext_regex_edit_script" title="Edit script">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</div>
|
||||
<div class="move_to_global menu_button" data-i18n="[title]ext_regex_move_to_global" title="Move to global scripts">
|
||||
<i class="fa-solid fa-arrow-up"></i>
|
||||
</div>
|
||||
<div class="move_to_scoped menu_button" data-i18n="[title]ext_regex_move_to_scoped" title="Move to scoped scripts">
|
||||
<i class="fa-solid fa-arrow-down"></i>
|
||||
</div>
|
||||
<div class="export_regex menu_button" data-i18n="[title]ext_regex_export_script" title="Export script">
|
||||
<i class="fa-solid fa-file-export"></i>
|
||||
</div>
|
||||
|
@ -14,6 +14,47 @@
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.regex-script-container:empty::after {
|
||||
content: "No scripts found";
|
||||
font-size: 0.95em;
|
||||
opacity: 0.7;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#scoped_scripts_block {
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
#scoped_scripts_block .move_to_scoped {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#global_scripts_block .move_to_global {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#scoped_scripts_block:not(:has(#regex_scoped_toggle:checked)) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.enable_scoped:checked ~ .regex-toggle-on {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.enable_scoped:checked ~ .regex-toggle-off {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.enable_scoped:not(:checked) ~ .regex-toggle-on {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.enable_scoped:not(:checked) ~ .regex-toggle-off {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.regex-script-label {
|
||||
align-items: center;
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
@ -23,7 +64,13 @@
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
input.disable_regex {
|
||||
.regex-script-label:has(.disable_regex:checked) .regex_script_name {
|
||||
text-decoration: line-through;
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
|
||||
input.disable_regex,
|
||||
input.enable_scoped {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@ -31,6 +78,12 @@ input.disable_regex {
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
filter: grayscale(0.5);
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.regex-toggle-off:hover {
|
||||
opacity: 1;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.regex-toggle-on {
|
||||
|
@ -278,6 +278,7 @@ const default_settings = {
|
||||
inline_image_quality: 'low',
|
||||
bypass_status_check: false,
|
||||
continue_prefill: false,
|
||||
function_calling: false,
|
||||
names_behavior: character_names_behavior.NONE,
|
||||
continue_postfix: continue_postfix_types.SPACE,
|
||||
custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
|
||||
@ -355,6 +356,7 @@ const oai_settings = {
|
||||
inline_image_quality: 'low',
|
||||
bypass_status_check: false,
|
||||
continue_prefill: false,
|
||||
function_calling: false,
|
||||
names_behavior: character_names_behavior.NONE,
|
||||
continue_postfix: continue_postfix_types.SPACE,
|
||||
custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
|
||||
@ -1851,6 +1853,10 @@ async function sendOpenAIRequest(type, messages, signal) {
|
||||
|
||||
await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);
|
||||
|
||||
if (isFunctionCallingSupported()) {
|
||||
await registerFunctionTools(type, generate_data);
|
||||
}
|
||||
|
||||
const generate_url = '/api/backends/chat-completions/generate';
|
||||
const response = await fetch(generate_url, {
|
||||
method: 'POST',
|
||||
@ -1907,10 +1913,125 @@ async function sendOpenAIRequest(type, messages, signal) {
|
||||
delay(1).then(() => saveLogprobsForActiveMessage(logprobs, null));
|
||||
}
|
||||
|
||||
if (isFunctionCallingSupported()) {
|
||||
await checkFunctionToolCalls(data);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register function tools for the next chat completion request.
|
||||
* @param {string} type Generation type
|
||||
* @param {object} data Generation data
|
||||
*/
|
||||
async function registerFunctionTools(type, data) {
|
||||
let toolChoice = 'auto';
|
||||
const tools = [];
|
||||
|
||||
/**
|
||||
* @type {registerFunctionTool}
|
||||
*/
|
||||
const registerFunctionTool = (name, description, parameters, required) => {
|
||||
tools.push({
|
||||
type: 'function',
|
||||
function: {
|
||||
name,
|
||||
description,
|
||||
parameters,
|
||||
},
|
||||
});
|
||||
|
||||
if (required) {
|
||||
toolChoice = 'required';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {FunctionToolRegister}
|
||||
*/
|
||||
const args = {
|
||||
type,
|
||||
data,
|
||||
registerFunctionTool,
|
||||
};
|
||||
|
||||
await eventSource.emit(event_types.LLM_FUNCTION_TOOL_REGISTER, args);
|
||||
|
||||
if (tools.length) {
|
||||
console.log('Registered function tools:', tools);
|
||||
|
||||
data['tools'] = tools;
|
||||
data['tool_choice'] = toolChoice;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkFunctionToolCalls(data) {
|
||||
if ([chat_completion_sources.OPENAI, chat_completion_sources.CUSTOM, chat_completion_sources.MISTRALAI].includes(oai_settings.chat_completion_source)) {
|
||||
if (!Array.isArray(data?.choices)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find a choice with 0-index
|
||||
const choice = data.choices.find(choice => choice.index === 0);
|
||||
|
||||
if (!choice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolCalls = choice.message.tool_calls;
|
||||
|
||||
if (!Array.isArray(toolCalls)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const toolCall of toolCalls) {
|
||||
if (typeof toolCall.function !== 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @type {FunctionToolCall} */
|
||||
const args = toolCall.function;
|
||||
console.log('Function tool call:', toolCall);
|
||||
await eventSource.emit(event_types.LLM_FUNCTION_TOOL_CALL, args);
|
||||
data.allowEmptyResponse = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ([chat_completion_sources.COHERE].includes(oai_settings.chat_completion_source)) {
|
||||
if (!Array.isArray(data?.tool_calls)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const toolCall of data.tool_calls) {
|
||||
/** @type {FunctionToolCall} */
|
||||
const args = { name: toolCall.name, arguments: JSON.stringify(toolCall.parameters) };
|
||||
console.log('Function tool call:', toolCall);
|
||||
await eventSource.emit(event_types.LLM_FUNCTION_TOOL_CALL, args);
|
||||
data.allowEmptyResponse = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isFunctionCallingSupported() {
|
||||
if (main_api !== 'openai') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!oai_settings.function_calling) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const supportedSources = [
|
||||
chat_completion_sources.OPENAI,
|
||||
chat_completion_sources.COHERE,
|
||||
chat_completion_sources.CUSTOM,
|
||||
chat_completion_sources.MISTRALAI,
|
||||
];
|
||||
return supportedSources.includes(oai_settings.chat_completion_source);
|
||||
}
|
||||
|
||||
function getStreamingReply(data) {
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
|
||||
return data?.delta?.text || '';
|
||||
@ -2781,6 +2902,7 @@ function loadOpenAISettings(data, settings) {
|
||||
oai_settings.continue_prefill = settings.continue_prefill ?? default_settings.continue_prefill;
|
||||
oai_settings.names_behavior = settings.names_behavior ?? default_settings.names_behavior;
|
||||
oai_settings.continue_postfix = settings.continue_postfix ?? default_settings.continue_postfix;
|
||||
oai_settings.function_calling = settings.function_calling ?? default_settings.function_calling;
|
||||
|
||||
// Migrate from old settings
|
||||
if (settings.names_in_completion === true) {
|
||||
@ -2849,6 +2971,7 @@ function loadOpenAISettings(data, settings) {
|
||||
$('#openrouter_providers_chat').val(oai_settings.openrouter_providers).trigger('change');
|
||||
$('#squash_system_messages').prop('checked', oai_settings.squash_system_messages);
|
||||
$('#continue_prefill').prop('checked', oai_settings.continue_prefill);
|
||||
$('#openai_function_calling').prop('checked', oai_settings.function_calling);
|
||||
if (settings.impersonation_prompt !== undefined) oai_settings.impersonation_prompt = settings.impersonation_prompt;
|
||||
|
||||
$('#impersonation_prompt_textarea').val(oai_settings.impersonation_prompt);
|
||||
@ -3132,6 +3255,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
|
||||
bypass_status_check: settings.bypass_status_check,
|
||||
continue_prefill: settings.continue_prefill,
|
||||
continue_postfix: settings.continue_postfix,
|
||||
function_calling: settings.function_calling,
|
||||
seed: settings.seed,
|
||||
n: settings.n,
|
||||
};
|
||||
@ -3518,6 +3642,7 @@ function onSettingsPresetChange() {
|
||||
inline_image_quality: ['#openai_inline_image_quality', 'inline_image_quality', false],
|
||||
continue_prefill: ['#continue_prefill', 'continue_prefill', true],
|
||||
continue_postfix: ['#continue_postfix', 'continue_postfix', false],
|
||||
function_calling: ['#openai_function_calling', 'function_calling', true],
|
||||
seed: ['#seed_openai', 'seed', false],
|
||||
n: ['#n_openai', 'n', false],
|
||||
};
|
||||
@ -3857,7 +3982,7 @@ async function onModelChange() {
|
||||
else if (['command-r', 'command-r-plus'].includes(oai_settings.cohere_model)) {
|
||||
$('#openai_max_context').attr('max', max_128k);
|
||||
}
|
||||
else if(['c4ai-aya-23'].includes(oai_settings.cohere_model)) {
|
||||
else if (['c4ai-aya-23'].includes(oai_settings.cohere_model)) {
|
||||
$('#openai_max_context').attr('max', max_8k);
|
||||
}
|
||||
else {
|
||||
@ -4448,7 +4573,8 @@ function runProxyCallback(_, value) {
|
||||
return foundName;
|
||||
}
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'proxy',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'proxy',
|
||||
callback: runProxyCallback,
|
||||
returns: 'current proxy',
|
||||
namedArgumentList: [],
|
||||
@ -4785,6 +4911,11 @@ $(document).ready(async function () {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#openai_function_calling').on('input', function () {
|
||||
oai_settings.function_calling = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#seed_openai').on('input', function () {
|
||||
oai_settings.seed = Number($(this).val());
|
||||
saveSettingsDebounced();
|
||||
|
@ -3581,7 +3581,7 @@ export async function importWorldInfo(file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const worldName = file.name.substr(0, file.name.lastIndexOf("."));
|
||||
const worldName = file.name.substr(0, file.name.lastIndexOf('.'));
|
||||
const sanitizedWorldName = await getSanitizedFilename(worldName);
|
||||
const allowed = await checkOverwriteExistingData('World Info', world_names, sanitizedWorldName, { interactive: true, actionName: 'Import', deleteAction: (existingName) => deleteWorldInfo(existingName) });
|
||||
if (!allowed) {
|
||||
|
@ -5,7 +5,7 @@ const Readable = require('stream').Readable;
|
||||
const { jsonParser } = require('../../express-common');
|
||||
const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants');
|
||||
const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util');
|
||||
const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages, convertMistralMessages } = require('../../prompt-converters');
|
||||
const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages, convertMistralMessages, convertCohereTools } = require('../../prompt-converters');
|
||||
|
||||
const { readSecret, SECRET_KEYS } = require('../secrets');
|
||||
const { getTokenizerModel, getSentencepiceTokenizer, getTiktokenTokenizer, sentencepieceTokenizers, TEXT_COMPLETION_MODELS } = require('../tokenizers');
|
||||
@ -486,6 +486,11 @@ async function sendMistralAIRequest(request, response) {
|
||||
'random_seed': request.body.seed === -1 ? undefined : request.body.seed,
|
||||
};
|
||||
|
||||
if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
|
||||
requestBody['tools'] = request.body.tools;
|
||||
requestBody['tool_choice'] = request.body.tool_choice === 'required' ? 'any' : 'auto';
|
||||
}
|
||||
|
||||
const config = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -544,6 +549,7 @@ async function sendCohereRequest(request, response) {
|
||||
try {
|
||||
const convertedHistory = convertCohereMessages(request.body.messages, request.body.char_name, request.body.user_name);
|
||||
const connectors = [];
|
||||
const tools = [];
|
||||
|
||||
if (request.body.websearch) {
|
||||
connectors.push({
|
||||
@ -551,6 +557,12 @@ async function sendCohereRequest(request, response) {
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
|
||||
tools.push(...convertCohereTools(request.body.tools));
|
||||
// Can't have both connectors and tools in the same request
|
||||
connectors.splice(0, connectors.length);
|
||||
}
|
||||
|
||||
// https://docs.cohere.com/reference/chat
|
||||
const requestBody = {
|
||||
stream: Boolean(request.body.stream),
|
||||
@ -569,8 +581,7 @@ async function sendCohereRequest(request, response) {
|
||||
prompt_truncation: 'AUTO_PRESERVE_ORDER',
|
||||
connectors: connectors,
|
||||
documents: [],
|
||||
tools: [],
|
||||
tool_results: [],
|
||||
tools: tools,
|
||||
search_queries_only: false,
|
||||
};
|
||||
|
||||
@ -920,6 +931,11 @@ router.post('/generate', jsonParser, function (request, response) {
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
if (!isTextCompletion) {
|
||||
bodyParams['tools'] = request.body.tools;
|
||||
bodyParams['tool_choice'] = request.body.tool_choice;
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
'messages': isTextCompletion === false ? request.body.messages : undefined,
|
||||
'prompt': isTextCompletion === true ? textPrompt : undefined,
|
||||
|
@ -308,6 +308,10 @@ function getTokenizerModel(requestModel) {
|
||||
return 'yi';
|
||||
}
|
||||
|
||||
if (requestModel.includes('gemini')) {
|
||||
return 'gpt-4o';
|
||||
}
|
||||
|
||||
// default
|
||||
return 'gpt-3.5-turbo';
|
||||
}
|
||||
|
@ -451,6 +451,76 @@ function convertTextCompletionPrompt(messages) {
|
||||
return messageStrings.join('\n') + '\nassistant:';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert OpenAI Chat Completion tools to the format used by Cohere.
|
||||
* @param {object[]} tools OpenAI Chat Completion tool definitions
|
||||
*/
|
||||
function convertCohereTools(tools) {
|
||||
if (!Array.isArray(tools) || tools.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const jsonSchemaToPythonTypes = {
|
||||
'string': 'str',
|
||||
'number': 'float',
|
||||
'integer': 'int',
|
||||
'boolean': 'bool',
|
||||
'array': 'list',
|
||||
'object': 'dict',
|
||||
};
|
||||
|
||||
const cohereTools = [];
|
||||
|
||||
for (const tool of tools) {
|
||||
if (tool?.type !== 'function') {
|
||||
console.log(`Unsupported tool type: ${tool.type}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = tool?.function?.name;
|
||||
const description = tool?.function?.description;
|
||||
const properties = tool?.function?.parameters?.properties;
|
||||
const required = tool?.function?.parameters?.required;
|
||||
const parameters = {};
|
||||
|
||||
if (!name) {
|
||||
console.log('Tool name is missing');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!description) {
|
||||
console.log('Tool description is missing');
|
||||
}
|
||||
|
||||
if (!properties || typeof properties !== 'object') {
|
||||
console.log(`No properties found for tool: ${tool?.function?.name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const property in properties) {
|
||||
const parameterDefinition = properties[property];
|
||||
const description = parameterDefinition.description || (parameterDefinition.enum ? JSON.stringify(parameterDefinition.enum) : '');
|
||||
const type = jsonSchemaToPythonTypes[parameterDefinition.type] || 'str';
|
||||
const isRequired = Array.isArray(required) && required.includes(property);
|
||||
parameters[property] = {
|
||||
description: description,
|
||||
type: type,
|
||||
required: isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
const cohereTool = {
|
||||
name: tool.function.name,
|
||||
description: tool.function.description,
|
||||
parameter_definitions: parameters,
|
||||
};
|
||||
|
||||
cohereTools.push(cohereTool);
|
||||
}
|
||||
|
||||
return cohereTools;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
convertClaudePrompt,
|
||||
convertClaudeMessages,
|
||||
@ -458,4 +528,5 @@ module.exports = {
|
||||
convertTextCompletionPrompt,
|
||||
convertCohereMessages,
|
||||
convertMistralMessages,
|
||||
convertCohereTools,
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user