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`)
Signed| @@ -8,7 +8,7 @@ import { isJsonSchemaSupported } from '../../textgen-settings.js'; | |||
| 8 | import { debounce_timeout } from '../../constants.js'; | 8 | import { debounce_timeout } from '../../constants.js'; |
| 9 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; | 9 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| 10 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; | 10 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| 11 | import { ARGUMENT_TYPE, SlashCommandArgument } from '../../slash-commands/SlashCommandArgument.js'; | 11 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| 12 | import { isFunctionCallingSupported } from '../../openai.js'; | 12 | import { isFunctionCallingSupported } from '../../openai.js'; |
| 13 | import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; | 13 | import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 14 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; | 14 | import { 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} */ | ||
| 55 | const EXPRESSION_API = { | 56 | const 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 | } |
| 922 | 923 | ||
| 923 | async function classifyCommand(_, text) { | 924 | async 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 | } |
| 928 | 933 | ||
| 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 | } |
| 933 | 940 | ||
| 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 | } |
| 1110 | 1117 | ||
| 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 } = {}) { | ||
| 1112 | // Return if text is undefined, saving a costly fetch request | 1128 | // 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 | } |
| 1116 | 1132 | ||
| @@ -1121,7 +1137,7 @@ async function getExpressionLabel(text) { | |||
| 1121 | text = sampleClassifyText(text); | 1137 | text = sampleClassifyText(text); |
| 1122 | 1138 | ||
| 1123 | try { | 1139 | try { |
| 1124 | switch (extension_settings.expressions.api) { | 1140 | switch (expressionsApi) { |
| 1125 | // Local BERT pipeline | 1141 | // 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 | } |
| 1146 | 1162 | ||
| 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 | } |
| 1340 | 1356 | ||
| 1341 | async function getExpressionsList() { | 1357 | export async function getExpressionsList() { |
| 1342 | // Return cached list if available | 1358 | // 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> |