From 6b656bf3806e0b15820b7b7f2949e0660cc488a7 Mon Sep 17 00:00:00 2001 From: kingbri Date: Fri, 12 Apr 2024 01:49:09 -0400 Subject: [PATCH 01/10] Expressions: Classify using LLM Rather than using a separate BERT model to classify the last message, use the LLM itself to get the classified expression label as a JSON and set that as the current sprite. Doing this should take more information into consideration and cut down on extra processing. This is made possible by the use of constrained generation with JSON schemas. Only available to TabbyAPI since it's the only backend that supports the use of JSON schemas, but there can hopefully be a way to use this with other backends as well. Intercepts the generation and sets top_k = 1 (for greedy sampling) and the json_schema to an emotion enum. Doing this also prevents reingestion of the entire context every time a message is sent and then asked to be classified, which doesn't compromise the chat experience. Signed-off-by: kingbri --- .../scripts/extensions/expressions/index.js | 166 ++++++++++++------ .../extensions/expressions/settings.html | 14 +- 2 files changed, 122 insertions(+), 58 deletions(-) diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index 15ad253be..fcd85f5c9 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -1,4 +1,4 @@ -import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced } from '../../../script.js'; +import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHeaders, saveSettingsDebounced } 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'; @@ -43,6 +43,11 @@ const DEFAULT_EXPRESSIONS = [ 'surprise', 'neutral', ]; +const EXPRESSION_API = { + local: 0, + extras: 1, + llm: 2, +} let expressionsList = null; let lastCharacter = undefined; @@ -55,7 +60,7 @@ let lastServerResponseTime = 0; export let lastExpression = {}; function isTalkingHeadEnabled() { - return extension_settings.expressions.talkinghead && !extension_settings.expressions.local; + return extension_settings.expressions.talkinghead && extension_settings.expressions.api == EXPRESSION_API.extras; } /** @@ -585,10 +590,10 @@ function handleImageChange() { async function moduleWorker() { const context = getContext(); - // Hide and disable Talkinghead while in local mode - $('#image_type_block').toggle(!extension_settings.expressions.local); + // Hide and disable Talkinghead while not in extras + $('#image_type_block').toggle(extension_settings.expressions.api == EXPRESSION_API.extras); - if (extension_settings.expressions.local && extension_settings.expressions.talkinghead) { + if (extension_settings.expressions.api != EXPRESSION_API.extras && extension_settings.expressions.talkinghead) { $('#image_type_toggle').prop('checked', false); setTalkingHeadState(false); } @@ -628,7 +633,7 @@ async function moduleWorker() { } const offlineMode = $('.expression_settings .offline_mode'); - if (!modules.includes('classify') && !extension_settings.expressions.local) { + if (!modules.includes('classify') && extension_settings.expressions.api == EXPRESSION_API.extras) { $('#open_chat_expressions').show(); $('#no_chat_expressions').hide(); offlineMode.css('display', 'block'); @@ -821,7 +826,7 @@ function setTalkingHeadState(newState) { extension_settings.expressions.talkinghead = newState; // Store setting saveSettingsDebounced(); - if (extension_settings.expressions.local) { + if (extension_settings.expressions.api == EXPRESSION_API.local) { return; } @@ -900,7 +905,7 @@ async function classifyCommand(_, text) { return ''; } - if (!modules.includes('classify') && !extension_settings.expressions.local) { + if (!modules.includes('classify') && extension_settings.expressions.api == EXPRESSION_API.extras) { toastr.warning('Text classification is disabled or not available'); return ''; } @@ -971,9 +976,32 @@ function sampleClassifyText(text) { return result.trim(); } +function onTextGenSettingsReady(args) { + // Only call if inside an API call + if (inApiCall) { + const emotions = DEFAULT_EXPRESSIONS.filter((e) => e != 'talkinghead') + Object.assign(args, { + top_k: 1, + json_schema: { + $schema: "http://json-schema.org/draft-04/schema#", + type: "object", + properties: { + emotion: { + type: "string", + enum: emotions + } + }, + required: [ + "emotion" + ] + } + }); + } +} + async function getExpressionLabel(text) { // Return if text is undefined, saving a costly fetch request - if ((!modules.includes('classify') && !extension_settings.expressions.local) || !text) { + if ((!modules.includes('classify') && extension_settings.expressions.api == EXPRESSION_API.extras) || !text) { return getFallbackExpression(); } @@ -984,36 +1012,44 @@ async function getExpressionLabel(text) { text = sampleClassifyText(text); try { - if (extension_settings.expressions.local) { - // Local transformers pipeline - const apiResult = await fetch('/api/extra/classify', { - method: 'POST', - headers: getRequestHeaders(), - body: JSON.stringify({ text: text }), - }); + switch (extension_settings.expressions.api) { + case EXPRESSION_API.local: + // Local BERT pipeline + const localResult = await fetch('/api/extra/classify', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ text: text }), + }); - if (apiResult.ok) { - const data = await apiResult.json(); - return data.classification[0].label; - } - } else { - // Extras - const url = new URL(getApiUrl()); - url.pathname = '/api/classify'; + if (localResult.ok) { + const data = await localResult.json(); + return data.classification[0].label; + } - const apiResult = await doExtrasFetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Bypass-Tunnel-Reminder': 'bypass', - }, - body: JSON.stringify({ text: text }), - }); + break; + case EXPRESSION_API.llm: + // Using LLM + const emotionResponse = await generateQuietPrompt('', false); + const parsedEmotion = JSON.parse(emotionResponse); + return parsedEmotion.emotion; + default: + // Extras + const url = new URL(getApiUrl()); + url.pathname = '/api/classify'; - if (apiResult.ok) { - const data = await apiResult.json(); - return data.classification[0].label; - } + const extrasResult = await doExtrasFetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Bypass-Tunnel-Reminder': 'bypass', + }, + body: JSON.stringify({ text: text }), + }); + + if (extrasResult.ok) { + const data = await extrasResult.json(); + return data.classification[0].label; + } } } catch (error) { console.log(error); @@ -1177,23 +1213,12 @@ async function getExpressionsList() { */ async function resolveExpressionsList() { // get something for offline mode (default images) - if (!modules.includes('classify') && !extension_settings.expressions.local) { + if (!modules.includes('classify') && extension_settings.expressions.api == EXPRESSION_API.extras) { return DEFAULT_EXPRESSIONS; } try { - if (extension_settings.expressions.local) { - const apiResult = await fetch('/api/extra/classify/labels', { - method: 'POST', - headers: getRequestHeaders(), - }); - - if (apiResult.ok) { - const data = await apiResult.json(); - expressionsList = data.labels; - return expressionsList; - } - } else { + if (extension_settings.expressions.api == EXPRESSION_API.extras) { const url = new URL(getApiUrl()); url.pathname = '/api/classify/labels'; @@ -1204,6 +1229,17 @@ async function getExpressionsList() { if (apiResult.ok) { + const data = await apiResult.json(); + expressionsList = data.labels; + return expressionsList; + } + } else { + const apiResult = await fetch('/api/extra/classify/labels', { + method: 'POST', + headers: getRequestHeaders(), + }); + + if (apiResult.ok) { const data = await apiResult.json(); expressionsList = data.labels; return expressionsList; @@ -1444,6 +1480,15 @@ async function onClickExpressionRemoveCustom() { moduleWorker(); } +function onExperesionApiChanged() { + const tempApi = this.value; + if (tempApi) { + extension_settings.expressions.api = Number(tempApi); + moduleWorker(); + saveSettingsDebounced(); + } +} + function onExpressionFallbackChanged() { const expression = this.value; if (expression) { @@ -1556,6 +1601,7 @@ async function onClickExpressionOverrideButton() { // Refresh sprites list. Assume the override path has been properly handled. try { + inApiCall = true; $('#visual-novel-wrapper').empty(); await validateImages(overridePath.length === 0 ? currentLastMessage.name : overridePath, true); const expression = await getExpressionLabel(currentLastMessage.mes); @@ -1563,6 +1609,8 @@ async function onClickExpressionOverrideButton() { forceUpdateVisualNovelMode(); } catch (error) { console.debug(`Setting expression override for ${avatarFileName} failed with error: ${error}`); + } finally { + inApiCall = false; } } @@ -1699,6 +1747,17 @@ async function fetchImagesNoCache() { return await Promise.allSettled(promises); } +function migrateSettings() { + if (Object.keys(extension_settings.expressions).includes('local')) { + if (extension_settings.expressions.local) { + extension_settings.expressions.api == EXPRESSION_API.local; + } + + delete extension_settings.expressions.local; + saveSettingsDebounced(); + } +} + (async function () { function addExpressionImage() { const html = ` @@ -1730,11 +1789,6 @@ async function fetchImagesNoCache() { extension_settings.expressions.translate = !!$(this).prop('checked'); saveSettingsDebounced(); }); - $('#expression_local').prop('checked', extension_settings.expressions.local).on('input', function () { - extension_settings.expressions.local = !!$(this).prop('checked'); - moduleWorker(); - saveSettingsDebounced(); - }); $('#expression_override_cleanup_button').on('click', onClickExpressionOverrideRemoveAllButton); $(document).on('dragstart', '.expression', (e) => { e.preventDefault(); @@ -1753,10 +1807,12 @@ async function fetchImagesNoCache() { }); await renderAdditionalExpressionSettings(); + $('#expression_api').val(extension_settings.expressions.api || 0); $('#expression_custom_add').on('click', onClickExpressionAddCustom); $('#expression_custom_remove').on('click', onClickExpressionRemoveCustom); $('#expression_fallback').on('change', onExpressionFallbackChanged); + $('#expression_api').on('change', onExperesionApiChanged); } // Pause Talkinghead to save resources when the ST tab is not visible or the window is minimized. @@ -1789,6 +1845,7 @@ async function fetchImagesNoCache() { addExpressionImage(); addVisualNovelMode(); + migrateSettings(); await addSettings(); const wrapper = new ModuleWorkerWrapper(moduleWorker); const updateFunction = wrapper.update.bind(wrapper); @@ -1828,6 +1885,7 @@ async function fetchImagesNoCache() { }); eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced); eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced); + eventSource.on(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady); registerSlashCommand('sprite', setSpriteSlashCommand, ['emote'], '(spriteId) – force sets the sprite for the current character', true, true); registerSlashCommand('spriteoverride', setSpriteSetCommand, ['costume'], '(optional folder) – sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.', true, true); registerSlashCommand('lastsprite', (_, value) => lastExpression[value.trim()] ?? '', [], '(charName) – Returns the last set sprite / expression for the named character.', true, true); diff --git a/public/scripts/extensions/expressions/settings.html b/public/scripts/extensions/expressions/settings.html index b0b3b0bd3..0746d0327 100644 --- a/public/scripts/extensions/expressions/settings.html +++ b/public/scripts/extensions/expressions/settings.html @@ -6,10 +6,6 @@
- +
+ + Select the API for classifying expressions. + +
Set the default and fallback expression being used when no matching expression is found. From bea63a2efe363a3542fa9b99d920f4f3a157ebe1 Mon Sep 17 00:00:00 2001 From: kingbri Date: Fri, 12 Apr 2024 01:59:44 -0400 Subject: [PATCH 02/10] Expressions: Remove TalkingHead from choices This is a sub-choice under extras. Signed-off-by: kingbri --- public/scripts/extensions/expressions/settings.html | 1 - 1 file changed, 1 deletion(-) diff --git a/public/scripts/extensions/expressions/settings.html b/public/scripts/extensions/expressions/settings.html index 0746d0327..0831a34d3 100644 --- a/public/scripts/extensions/expressions/settings.html +++ b/public/scripts/extensions/expressions/settings.html @@ -25,7 +25,6 @@ -
From 8c4dd6ce1ea40c627b9f25915d935577e84486aa Mon Sep 17 00:00:00 2001 From: kingbri Date: Fri, 12 Apr 2024 09:57:09 -0400 Subject: [PATCH 03/10] Expressions: Ignore stopping strings Don't use stopping strings when triggering a classify generation. Signed-off-by: kingbri --- public/scripts/extensions/expressions/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index fcd85f5c9..1bf07179c 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -982,6 +982,9 @@ function onTextGenSettingsReady(args) { const emotions = DEFAULT_EXPRESSIONS.filter((e) => e != 'talkinghead') Object.assign(args, { top_k: 1, + stop: [], + stopping_strings: [], + custom_token_bans: [], json_schema: { $schema: "http://json-schema.org/draft-04/schema#", type: "object", From 913085ba749b73aee1acc297860166f653a378d0 Mon Sep 17 00:00:00 2001 From: kingbri Date: Sat, 13 Apr 2024 01:29:54 -0400 Subject: [PATCH 04/10] Expressions: Let the user know if classification failed Send a message if the classifer fails and fallback to the default expression. Signed-off-by: kingbri --- public/scripts/extensions/expressions/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index 1bf07179c..629ff44d0 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -1055,7 +1055,8 @@ async function getExpressionLabel(text) { } } } catch (error) { - console.log(error); + toastr.info("Could not classify expression. Check the console or your backend for more information.") + console.error(error); return getFallbackExpression(); } } From b02394008c76c2be8434ca09be96cf4f4cb3b09b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 14 Apr 2024 15:40:10 +0300 Subject: [PATCH 05/10] Fix settings migration. Add lint rule. --- .eslintrc.js | 2 ++ .../scripts/extensions/expressions/index.js | 28 +++++++++---------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 6e926dee9..981379f24 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -60,6 +60,8 @@ module.exports = { 'no-trailing-spaces': 'error', 'object-curly-spacing': ['error', 'always'], 'space-infix-ops': 'error', + 'no-unused-expressions': ['error', { allowShortCircuit: true, allowTernary: true }], + 'no-cond-assign': 'error', // These rules should eventually be enabled. 'no-async-promise-executor': 'off', diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index 629ff44d0..426be18b8 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -47,7 +47,7 @@ const EXPRESSION_API = { local: 0, extras: 1, llm: 2, -} +}; let expressionsList = null; let lastCharacter = undefined; @@ -979,25 +979,25 @@ function sampleClassifyText(text) { function onTextGenSettingsReady(args) { // Only call if inside an API call if (inApiCall) { - const emotions = DEFAULT_EXPRESSIONS.filter((e) => e != 'talkinghead') + const emotions = DEFAULT_EXPRESSIONS.filter((e) => e != 'talkinghead'); Object.assign(args, { top_k: 1, stop: [], stopping_strings: [], custom_token_bans: [], json_schema: { - $schema: "http://json-schema.org/draft-04/schema#", - type: "object", + $schema: 'http://json-schema.org/draft-04/schema#', + type: 'object', properties: { emotion: { - type: "string", - enum: emotions - } + type: 'string', + enum: emotions, + }, }, required: [ - "emotion" - ] - } + 'emotion', + ], + }, }); } } @@ -1055,7 +1055,7 @@ async function getExpressionLabel(text) { } } } catch (error) { - toastr.info("Could not classify expression. Check the console or your backend for more information.") + toastr.info('Could not classify expression. Check the console or your backend for more information.'); console.error(error); return getFallbackExpression(); } @@ -1236,7 +1236,7 @@ async function getExpressionsList() { const data = await apiResult.json(); expressionsList = data.labels; return expressionsList; - } + } } else { const apiResult = await fetch('/api/extra/classify/labels', { method: 'POST', @@ -1754,7 +1754,7 @@ async function fetchImagesNoCache() { function migrateSettings() { if (Object.keys(extension_settings.expressions).includes('local')) { if (extension_settings.expressions.local) { - extension_settings.expressions.api == EXPRESSION_API.local; + extension_settings.expressions.api = EXPRESSION_API.local; } delete extension_settings.expressions.local; @@ -1811,7 +1811,7 @@ function migrateSettings() { }); await renderAdditionalExpressionSettings(); - $('#expression_api').val(extension_settings.expressions.api || 0); + $('#expression_api').val(extension_settings.expressions.api || EXPRESSION_API.extras); $('#expression_custom_add').on('click', onClickExpressionAddCustom); $('#expression_custom_remove').on('click', onClickExpressionRemoveCustom); From 3e609192894d758654125d2222785488fb3e3abb Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 14 Apr 2024 17:13:54 +0300 Subject: [PATCH 06/10] Specify LLM prompt in case JSON schema is not supported --- .../scripts/extensions/expressions/index.js | 84 +++++++++++++++---- .../extensions/expressions/settings.html | 5 ++ public/scripts/textgen-settings.js | 5 ++ 3 files changed, 80 insertions(+), 14 deletions(-) diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index 426be18b8..e50ac9c71 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -5,13 +5,15 @@ import { loadMovingUIState, power_user } from '../../power-user.js'; import { registerSlashCommand } from '../../slash-commands.js'; import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence } from '../../utils.js'; import { hideMutedSprites } from '../../group-chats.js'; +import { isJsonSchemaSupported } from '../../textgen-settings.js'; export { MODULE_NAME }; const MODULE_NAME = 'expressions'; const UPDATE_INTERVAL = 2000; -const STREAMING_UPDATE_INTERVAL = 6000; +const STREAMING_UPDATE_INTERVAL = 10000; const TALKINGCHECK_UPDATE_INTERVAL = 500; const DEFAULT_FALLBACK_EXPRESSION = 'joy'; +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', 'admiration', @@ -976,9 +978,49 @@ function sampleClassifyText(text) { return result.trim(); } +/** + * Gets the classification prompt for the LLM API. + * @param {string[]} labels A list of labels to search for. + * @returns {Promise} Prompt for the LLM API. + */ +async function getLlmPrompt(labels) { + if (isJsonSchemaSupported()) { + return ''; + } + + const prompt = String(extension_settings.expressions.llmPrompt).replace(/{{labels}}/gi, labels.map(x => `"${x}"`).join(', ')); + return prompt; +} + +/** + * Parses the emotion response from the LLM API. + * @param {string} emotionResponse The response from the LLM API. + * @param {string[]} labels A list of labels to search for. + * @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; + + } catch { + const fuse = new Fuse([emotionResponse]); + for (const label of labels) { + const result = fuse.search(label); + if (result.length > 0) { + return label; + } + } + } + + return fallbackExpression; +} + function onTextGenSettingsReady(args) { // Only call if inside an API call - if (inApiCall) { + if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) { const emotions = DEFAULT_EXPRESSIONS.filter((e) => e != 'talkinghead'); Object.assign(args, { top_k: 1, @@ -1016,8 +1058,8 @@ async function getExpressionLabel(text) { try { switch (extension_settings.expressions.api) { - case EXPRESSION_API.local: - // Local BERT pipeline + // Local BERT pipeline + case EXPRESSION_API.local: { const localResult = await fetch('/api/extra/classify', { method: 'POST', headers: getRequestHeaders(), @@ -1028,15 +1070,16 @@ async function getExpressionLabel(text) { const data = await localResult.json(); return data.classification[0].label; } - - break; - case EXPRESSION_API.llm: - // Using LLM - const emotionResponse = await generateQuietPrompt('', false); - const parsedEmotion = JSON.parse(emotionResponse); - return parsedEmotion.emotion; - default: - // Extras + } break; + // Using LLM + case EXPRESSION_API.llm: { + const expressionsList = await getExpressionsList(); + const prompt = await getLlmPrompt(expressionsList); + const emotionResponse = await generateQuietPrompt(prompt, false, false); + return parseLlmResponse(emotionResponse, expressionsList); + } + // Extras + default: { const url = new URL(getApiUrl()); url.pathname = '/api/classify'; @@ -1053,6 +1096,7 @@ async function getExpressionLabel(text) { const data = await extrasResult.json(); return data.classification[0].label; } + } break; } } catch (error) { toastr.info('Could not classify expression. Check the console or your backend for more information.'); @@ -1488,6 +1532,7 @@ function onExperesionApiChanged() { const tempApi = this.value; if (tempApi) { extension_settings.expressions.api = Number(tempApi); + $('.expression_llm_prompt_block').toggle(extension_settings.expressions.api === EXPRESSION_API.llm); moduleWorker(); saveSettingsDebounced(); } @@ -1760,6 +1805,11 @@ function migrateSettings() { delete extension_settings.expressions.local; saveSettingsDebounced(); } + + if (extension_settings.expressions.llmPrompt === undefined) { + extension_settings.expressions.llmPrompt = DEFAULT_LLM_PROMPT; + saveSettingsDebounced(); + } } (async function () { @@ -1811,7 +1861,13 @@ function migrateSettings() { }); await renderAdditionalExpressionSettings(); - $('#expression_api').val(extension_settings.expressions.api || EXPRESSION_API.extras); + $('#expression_api').val(extension_settings.expressions.api ?? EXPRESSION_API.extras); + $('.expression_llm_prompt_block').toggle(extension_settings.expressions.api === EXPRESSION_API.llm); + $('#expression_llm_prompt').val(extension_settings.expressions.llmPrompt ?? ''); + $('#expression_llm_prompt').on('input', function () { + extension_settings.expressions.llmPrompt = $(this).val(); + saveSettingsDebounced(); + }); $('#expression_custom_add').on('click', onClickExpressionAddCustom); $('#expression_custom_remove').on('click', onClickExpressionRemoveCustom); diff --git a/public/scripts/extensions/expressions/settings.html b/public/scripts/extensions/expressions/settings.html index 0831a34d3..9857ae3d5 100644 --- a/public/scripts/extensions/expressions/settings.html +++ b/public/scripts/extensions/expressions/settings.html @@ -27,6 +27,11 @@
+
+ + Will be used if the API doesn't support JSON schemas. + +
Set the default and fallback expression being used when no matching expression is found. diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index 4f8156a38..aa85d1f99 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -3,6 +3,7 @@ import { event_types, getRequestHeaders, getStoppingStrings, + main_api, max_context, saveSettingsDebounced, setGenerationParamsFromPreset, @@ -978,6 +979,10 @@ function getModel() { return undefined; } +export function isJsonSchemaSupported() { + return settings.type === TABBY && main_api === 'textgenerationwebui'; +} + export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) { const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet'; let params = { From fd0c16bf1205686124c3596f73dde658956cfb8a Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 14 Apr 2024 17:26:58 +0300 Subject: [PATCH 07/10] Don't unblock generation if a parallel stream is still running after quiet gens --- public/script.js | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/public/script.js b/public/script.js index a20a04ccb..a92eb3387 100644 --- a/public/script.js +++ b/public/script.js @@ -3065,14 +3065,14 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu if (interruptedByCommand) { //$("#send_textarea").val('').trigger('input'); - unblockGeneration(); + unblockGeneration(type); return Promise.resolve(); } } if (main_api == 'kobold' && kai_settings.streaming_kobold && !kai_flags.can_use_streaming) { toastr.error('Streaming is enabled, but the version of Kobold used does not support token streaming.', undefined, { timeOut: 10000, preventDuplicates: true }); - unblockGeneration(); + unblockGeneration(type); return Promise.resolve(); } @@ -3081,12 +3081,12 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu textgen_settings.legacy_api && (textgen_settings.type === OOBA || textgen_settings.type === APHRODITE)) { toastr.error('Streaming is not supported for the Legacy API. Update Ooba and use new API to enable streaming.', undefined, { timeOut: 10000, preventDuplicates: true }); - unblockGeneration(); + unblockGeneration(type); return Promise.resolve(); } if (isHordeGenerationNotAllowed()) { - unblockGeneration(); + unblockGeneration(type); return Promise.resolve(); } @@ -3122,7 +3122,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu setCharacterName(''); } else { console.log('No enabled members found'); - unblockGeneration(); + unblockGeneration(type); return Promise.resolve(); } } @@ -3296,7 +3296,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu if (aborted) { console.debug('Generation aborted by extension interceptors'); - unblockGeneration(); + unblockGeneration(type); return Promise.resolve(); } } else { @@ -3310,7 +3310,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu adjustedParams = await adjustHordeGenerationParams(max_context, amount_gen); } catch { - unblockGeneration(); + unblockGeneration(type); return Promise.resolve(); } if (horde_settings.auto_adjust_context_length) { @@ -4097,7 +4097,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu await eventSource.emit(event_types.IMPERSONATE_READY, getMessage); } else if (type == 'quiet') { - unblockGeneration(); + unblockGeneration(type); return getMessage; } else { @@ -4165,7 +4165,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu console.debug('/api/chats/save called by /Generate'); await saveChatConditional(); - unblockGeneration(); + unblockGeneration(type); streamingProcessor = null; if (type !== 'quiet') { @@ -4183,7 +4183,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu generatedPromptCache = ''; - unblockGeneration(); + unblockGeneration(type); console.log(exception); streamingProcessor = null; throw exception; @@ -4253,7 +4253,16 @@ function flushWIDepthInjections() { } } -function unblockGeneration() { +/** + * Unblocks the UI after a generation is complete. + * @param {string} [type] Generation type (optional) + */ +function unblockGeneration(type) { + // Don't unblock if a parallel stream is still running + if (type === 'quiet' && streamingProcessor && !streamingProcessor.isFinished) { + return; + } + is_send_press = false; activateSendButtons(); showSwipeButtons(); From 6dca8b28fecc32ead8e7d28aeea9507213de1c74 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 14 Apr 2024 19:46:33 +0300 Subject: [PATCH 08/10] Fix talkinghead check --- public/scripts/extensions/expressions/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index e50ac9c71..83c9cf81b 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -828,7 +828,7 @@ function setTalkingHeadState(newState) { extension_settings.expressions.talkinghead = newState; // Store setting saveSettingsDebounced(); - if (extension_settings.expressions.api == EXPRESSION_API.local) { + if (extension_settings.expressions.api == EXPRESSION_API.local || extension_settings.expressions.api == EXPRESSION_API.llm) { return; } From 0ff5d0b5f10a59dd8657d849811f6e3f4159ce0b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 14 Apr 2024 19:53:40 +0300 Subject: [PATCH 09/10] Substitute macro is LLM prompt --- public/scripts/extensions/expressions/index.js | 11 +++++++++-- public/scripts/extensions/expressions/settings.html | 9 +++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index 83c9cf81b..35109357e 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -1,4 +1,4 @@ -import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHeaders, saveSettingsDebounced } from '../../../script.js'; +import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHeaders, 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'; @@ -988,7 +988,9 @@ async function getLlmPrompt(labels) { return ''; } - const prompt = String(extension_settings.expressions.llmPrompt).replace(/{{labels}}/gi, labels.map(x => `"${x}"`).join(', ')); + const labelsString = labels.map(x => `"${x}"`).join(', '); + const prompt = substituteParams(String(extension_settings.expressions.llmPrompt)) + .replace(/{{labels}}/gi, labelsString); return prompt; } @@ -1868,6 +1870,11 @@ function migrateSettings() { extension_settings.expressions.llmPrompt = $(this).val(); saveSettingsDebounced(); }); + $('#expression_llm_prompt_restore').on('click', function () { + $('#expression_llm_prompt').val(DEFAULT_LLM_PROMPT); + extension_settings.expressions.llmPrompt = DEFAULT_LLM_PROMPT; + saveSettingsDebounced(); + }); $('#expression_custom_add').on('click', onClickExpressionAddCustom); $('#expression_custom_remove').on('click', onClickExpressionRemoveCustom); diff --git a/public/scripts/extensions/expressions/settings.html b/public/scripts/extensions/expressions/settings.html index 9857ae3d5..4a7347a74 100644 --- a/public/scripts/extensions/expressions/settings.html +++ b/public/scripts/extensions/expressions/settings.html @@ -28,9 +28,14 @@
- + Will be used if the API doesn't support JSON schemas. - +
From f5955bdd37450943bb346bbf921076a2b3dc9a44 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 14 Apr 2024 20:02:34 +0300 Subject: [PATCH 10/10] Throw if couldn't classify --- public/scripts/extensions/expressions/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index 35109357e..3ab2d0eb0 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -1006,7 +1006,6 @@ function parseLlmResponse(emotionResponse, labels) { try { const parsedEmotion = JSON.parse(emotionResponse); return parsedEmotion?.emotion ?? fallbackExpression; - } catch { const fuse = new Fuse([emotionResponse]); for (const label of labels) { @@ -1017,7 +1016,7 @@ function parseLlmResponse(emotionResponse, labels) { } } - return fallbackExpression; + throw new Error('Could not parse emotion response ' + emotionResponse); } function onTextGenSettingsReady(args) {