Merge pull request #2774 from SillyTavern/classify-slash-commands-with-api Expanded classify slash commands (add Classifier API to `/classify` and allow prompt override, new `classify-expressions`)

b5b01a1a5c3c0c7e4b339488636302fdbba71f9c

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

Signed
1 files changed, +72 -14Ignore whitespace
public/scripts/extensions/expressions/index.js+72 -14
@@ -8,7 +8,7 @@ import { isJsonSchemaSupported } from '../../textgen-settings.js';
8import { debounce_timeout } from '../../constants.js';8import { 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 } from '../../slash-commands/SlashCommandArgument.js';11import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
12import { isFunctionCallingSupported } from '../../openai.js';12import { isFunctionCallingSupported } from '../../openai.js';
13import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';13import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
14import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';14import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
@@ -52,6 +52,7 @@ const DEFAULT_EXPRESSIONS = [
52 'surprise',52 'surprise',
53 'neutral',53 'neutral',
54];54];
55/** @enum {number} */
55const EXPRESSION_API = {56const EXPRESSION_API = {
56 local: 0,57 local: 0,
57 extras: 1,58 extras: 1,
@@ -920,18 +921,24 @@ async function setSpriteSetCommand(_, folder) {
920 return '';921 return '';
921}922}
922923
923async function classifyCommand(_, text) {924async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ { api = null, prompt = null }, text) {
924 if (!text) {925 if (!text) {
925 console.log('No text provided');926 toastr.warning('No text provided');
927 return '';
928 }
929 if (api && !Object.keys(EXPRESSION_API).includes(api)) {
930 toastr.warning('Invalid API provided');
926 return '';931 return '';
927 }932 }
928933
929 if (!modules.includes('classify') && extension_settings.expressions.api == EXPRESSION_API.extras) {934 const expressionApi = EXPRESSION_API[api] || extension_settings.expressions.api;
935
936 if (!modules.includes('classify') && expressionApi == EXPRESSION_API.extras) {
930 toastr.warning('Text classification is disabled or not available');937 toastr.warning('Text classification is disabled or not available');
931 return '';938 return '';
932 }939 }
933940
934 const label = getExpressionLabel(text);941 const label = await getExpressionLabel(text, expressionApi, { customPrompt: prompt });
935 console.debug(`Classification result for "${text}": ${label}`);942 console.debug(`Classification result for "${text}": ${label}`);
936 return label;943 return label;
937}944}
@@ -1108,9 +1115,18 @@ function onTextGenSettingsReady(args) {
1108 }1115 }
1109}1116}
11101117
1111async function getExpressionLabel(text) {1118/**
1119 * Retrieves the label of an expression via classification based on the provided text.
1120 * Optionally allows to override the expressions API being used.
1121 * @param {string} text - The text to classify and retrieve the expression label for.
1122 * @param {EXPRESSION_API} [expressionsApi=extension_settings.expressions.api] - The expressions API to use for classification.
1123 * @param {object} [options={}] - Optional arguments.
1124 * @param {string?} [options.customPrompt=null] - The custom prompt to use for classification.
1125 * @returns {Promise<string>} - The label of the expression.
1126 */
1127export async function getExpressionLabel(text, expressionsApi = extension_settings.expressions.api, { customPrompt = null } = {}) {
1112 // Return if text is undefined, saving a costly fetch request1128 // Return if text is undefined, saving a costly fetch request
1113 if ((!modules.includes('classify') && extension_settings.expressions.api == EXPRESSION_API.extras) || !text) {1129 if ((!modules.includes('classify') && expressionsApi == EXPRESSION_API.extras) || !text) {
1114 return getFallbackExpression();1130 return getFallbackExpression();
1115 }1131 }
11161132
@@ -1121,7 +1137,7 @@ async function getExpressionLabel(text) {
1121 text = sampleClassifyText(text);1137 text = sampleClassifyText(text);
11221138
1123 try {1139 try {
1124 switch (extension_settings.expressions.api) {1140 switch (expressionsApi) {
1125 // Local BERT pipeline1141 // Local BERT pipeline
1126 case EXPRESSION_API.local: {1142 case EXPRESSION_API.local: {
1127 const localResult = await fetch('/api/extra/classify', {1143 const localResult = await fetch('/api/extra/classify', {
@@ -1145,7 +1161,7 @@ async function getExpressionLabel(text) {
1145 }1161 }
11461162
1147 const expressionsList = await getExpressionsList();1163 const expressionsList = await getExpressionsList();
1148 const prompt = await getLlmPrompt(expressionsList);1164 const prompt = substituteParamsExtended(String(customPrompt), { labels: expressionsList }) || await getLlmPrompt(expressionsList);
1149 let functionResult = null;1165 let functionResult = null;
1150 eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);1166 eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);
1151 eventSource.once(event_types.LLM_FUNCTION_TOOL_REGISTER, onFunctionToolRegister);1167 eventSource.once(event_types.LLM_FUNCTION_TOOL_REGISTER, onFunctionToolRegister);
@@ -1338,7 +1354,7 @@ function getCachedExpressions() {
1338 return [...expressionsList, ...extension_settings.expressions.custom].filter(onlyUnique);1354 return [...expressionsList, ...extension_settings.expressions.custom].filter(onlyUnique);
1339}1355}
13401356
1341async function getExpressionsList() {1357export async function getExpressionsList() {
1342 // Return cached list if available1358 // Return cached list if available
1343 if (Array.isArray(expressionsList)) {1359 if (Array.isArray(expressionsList)) {
1344 return getCachedExpressions();1360 return getCachedExpressions();
@@ -2069,7 +2085,7 @@ function migrateSettings() {
2069 }),2085 }),
2070 ],2086 ],
2071 helpString: 'Force sets the sprite for the current character.',2087 helpString: 'Force sets the sprite for the current character.',
2072 returns: 'label',2088 returns: 'the currently set sprite label after setting it.',
2073 }));2089 }));
2074 SlashCommandParser.addCommandObject(SlashCommand.fromProps({2090 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2075 name: 'spriteoverride',2091 name: 'spriteoverride',
@@ -2085,7 +2101,7 @@ function migrateSettings() {
2085 SlashCommandParser.addCommandObject(SlashCommand.fromProps({2101 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2086 name: 'lastsprite',2102 name: 'lastsprite',
2087 callback: (_, value) => lastExpression[String(value).trim()] ?? '',2103 callback: (_, value) => lastExpression[String(value).trim()] ?? '',
2088 returns: 'sprite',2104 returns: 'the last set sprite / expression for the named character.',
2089 unnamedArgumentList: [2105 unnamedArgumentList: [
2090 SlashCommandArgument.fromProps({2106 SlashCommandArgument.fromProps({
2091 description: 'character name',2107 description: 'character name',
@@ -2101,11 +2117,50 @@ function migrateSettings() {
2101 callback: toggleTalkingHeadCommand,2117 callback: toggleTalkingHeadCommand,
2102 aliases: ['talkinghead'],2118 aliases: ['talkinghead'],
2103 helpString: 'Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.',2119 helpString: 'Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.',
2104 returns: ARGUMENT_TYPE.BOOLEAN,2120 returns: 'the current state of the <i>Image Type - talkinghead (extras)</i> on/off.',
2121 }));
2122 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2123 name: 'classify-expressions',
2124 aliases: ['expressions'],
2125 callback: async (args) => {
2126 const list = await getExpressionsList();
2127 switch (String(args.format).toLowerCase()) {
2128 case 'json':
2129 return JSON.stringify(list);
2130 default:
2131 return list.join(', ');
2132 }
2133 },
2134 namedArgumentList: [
2135 SlashCommandNamedArgument.fromProps({
2136 name: 'format',
2137 description: 'The format to return the list in: comma-separated plain text or JSON array. Default is plain text.',
2138 typeList: [ARGUMENT_TYPE.STRING],
2139 enumList: [
2140 new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '),
2141 new SlashCommandEnumValue('json', null, enumTypes.enum, '[]'),
2142 ],
2143 }),
2144 ],
2145 returns: 'The comma-separated list of available expressions, including custom expressions.',
2146 helpString: 'Returns a list of available expressions, including custom expressions.',
2105 }));2147 }));
2106 SlashCommandParser.addCommandObject(SlashCommand.fromProps({2148 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2107 name: 'classify',2149 name: 'classify',
2108 callback: classifyCommand,2150 callback: classifyCallback,
2151 namedArgumentList: [
2152 SlashCommandNamedArgument.fromProps({
2153 name: 'api',
2154 description: 'The Classifier API to classify with. If not specified, the configured one will be used.',
2155 typeList: [ARGUMENT_TYPE.STRING],
2156 enumList: Object.keys(EXPRESSION_API).map(api => new SlashCommandEnumValue(api, null, enumTypes.enum)),
2157 }),
2158 SlashCommandNamedArgument.fromProps({
2159 name: 'prompt',
2160 description: 'Custom prompt for classification. Only relevant if Classifier API is set to LLM.',
2161 typeList: [ARGUMENT_TYPE.STRING],
2162 }),
2163 ],
2109 unnamedArgumentList: [2164 unnamedArgumentList: [
2110 new SlashCommandArgument(2165 new SlashCommandArgument(
2111 'text', [ARGUMENT_TYPE.STRING], true,2166 'text', [ARGUMENT_TYPE.STRING], true,
@@ -2117,6 +2172,9 @@ function migrateSettings() {
2117 Performs an emotion classification of the given text and returns a label.2172 Performs an emotion classification of the given text and returns a label.
2118 </div>2173 </div>
2119 <div>2174 <div>
2175 Allows to specify which Classifier API to perform the classification with.
2176 </div>
2177 <div>
2120 <strong>Example:</strong>2178 <strong>Example:</strong>
2121 <ul>2179 <ul>
2122 <li>2180 <li>