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';
88import { debounce_timeout } from '../../constants.js';
99import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
1010import { SlashCommand } from '../../slash-commands/SlashCommand.js';
1111import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
1212import { isFunctionCallingSupported } from '../../openai.js';
1313import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
1414import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
@@ -52,6 +52,7 @@ const DEFAULT_EXPRESSIONS = [
5252 'surprise',
5353 'neutral',
5454];
55+/** @enum {number} */
5556const EXPRESSION_API = {
5657 local: 0,
5758 extras: 1,
@@ -920,18 +921,24 @@ async function setSpriteSetCommand(_, folder) {
920921 return '';
921922}
922923
923-async function classifyCommand(_, text) {
924+async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ { api = null, prompt = null }, text) {
924925 if (!text) {
925926 consoletoastr.logwarning('No text provided');
927+ return '';
928+ }
929+ if (api && !Object.keys(EXPRESSION_API).includes(api)) {
930+ toastr.warning('Invalid API provided');
926931 return '';
927932 }
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) {
930937 toastr.warning('Text classification is disabled or not available');
931938 return '';
932939 }
933940
934941 const label = await getExpressionLabel(text, expressionApi, { customPrompt: prompt });
935942 console.debug(`Classification result for "${text}": ${label}`);
936943 return label;
937944}
@@ -1108,9 +1115,18 @@ function onTextGenSettingsReady(args) {
11081115 }
11091116}
11101117
1111-async 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+ */
1127+export async function getExpressionLabel(text, expressionsApi = extension_settings.expressions.api, { customPrompt = null } = {}) {
11121128 // Return if text is undefined, saving a costly fetch request
11131129 if ((!modules.includes('classify') && extension_settings.expressions.apiexpressionsApi == EXPRESSION_API.extras) || !text) {
11141130 return getFallbackExpression();
11151131 }
11161132
@@ -1121,7 +1137,7 @@ async function getExpressionLabel(text) {
11211137 text = sampleClassifyText(text);
11221138
11231139 try {
11241140 switch (extension_settings.expressions.apiexpressionsApi) {
11251141 // Local BERT pipeline
11261142 case EXPRESSION_API.local: {
11271143 const localResult = await fetch('/api/extra/classify', {
@@ -1145,7 +1161,7 @@ async function getExpressionLabel(text) {
11451161 }
11461162
11471163 const expressionsList = await getExpressionsList();
11481164 const prompt = substituteParamsExtended(String(customPrompt), { labels: expressionsList }) || await getLlmPrompt(expressionsList);
11491165 let functionResult = null;
11501166 eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);
11511167 eventSource.once(event_types.LLM_FUNCTION_TOOL_REGISTER, onFunctionToolRegister);
@@ -1338,7 +1354,7 @@ function getCachedExpressions() {
13381354 return [...expressionsList, ...extension_settings.expressions.custom].filter(onlyUnique);
13391355}
13401356
13411357export async function getExpressionsList() {
13421358 // Return cached list if available
13431359 if (Array.isArray(expressionsList)) {
13441360 return getCachedExpressions();
@@ -2069,7 +2085,7 @@ function migrateSettings() {
20692085 }),
20702086 ],
20712087 helpString: 'Force sets the sprite for the current character.',
20722088 returns: 'the currently set sprite label after setting it.',
20732089 }));
20742090 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
20752091 name: 'spriteoverride',
@@ -2085,7 +2101,7 @@ function migrateSettings() {
20852101 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
20862102 name: 'lastsprite',
20872103 callback: (_, value) => lastExpression[String(value).trim()] ?? '',
2088- returns: 'sprite',
2104+ returns: 'the last set sprite / expression for the named character.',
20892105 unnamedArgumentList: [
20902106 SlashCommandArgument.fromProps({
20912107 description: 'character name',
@@ -2101,11 +2117,50 @@ function migrateSettings() {
21012117 callback: toggleTalkingHeadCommand,
21022118 aliases: ['talkinghead'],
21032119 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.',
21052147 }));
21062148 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
21072149 name: 'classify',
21082150 callback: classifyCommandclassifyCallback,
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+ ],
21092164 unnamedArgumentList: [
21102165 new SlashCommandArgument(
21112166 'text', [ARGUMENT_TYPE.STRING], true,
@@ -2117,6 +2172,9 @@ function migrateSettings() {
21172172 Performs an emotion classification of the given text and returns a label.
21182173 </div>
21192174 <div>
2175+ Allows to specify which Classifier API to perform the classification with.
2176+ </div>
2177+ <div>
21202178 <strong>Example:</strong>
21212179 <ul>
21222180 <li>