diff --git a/default/content/index.json b/default/content/index.json index 4caa21c14..49e2dd12c 100644 --- a/default/content/index.json +++ b/default/content/index.json @@ -786,5 +786,13 @@ { "filename": "presets/context/DeepSeek-V2.5.json", "type": "context" + }, + { + "filename": "presets/reasoning/DeepSeek.json", + "type": "reasoning" + }, + { + "filename": "presets/reasoning/Blank.json", + "type": "reasoning" } ] diff --git a/default/content/presets/reasoning/Blank.json b/default/content/presets/reasoning/Blank.json new file mode 100644 index 000000000..bdb2fa032 --- /dev/null +++ b/default/content/presets/reasoning/Blank.json @@ -0,0 +1,6 @@ +{ + "name": "Blank", + "prefix": "", + "suffix": "", + "separator": "" +} diff --git a/default/content/presets/reasoning/DeepSeek.json b/default/content/presets/reasoning/DeepSeek.json new file mode 100644 index 000000000..d2f78c0ce --- /dev/null +++ b/default/content/presets/reasoning/DeepSeek.json @@ -0,0 +1,6 @@ +{ + "name": "DeepSeek", + "prefix": "\n", + "suffix": "\n", + "separator": "\n\n" +} diff --git a/public/index.html b/public/index.html index 500dcf469..a122b0082 100644 --- a/public/index.html +++ b/public/index.html @@ -3917,6 +3917,19 @@ Reasoning Formatting +
+ +
+ + + + + + + + +
+
Prefix diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index 20019e8e9..fbcf69c7f 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -39,6 +39,7 @@ const CC_COMMANDS = [ 'proxy', 'stop-strings', 'start-reply-with', + 'reasoning-template', ]; const TC_COMMANDS = [ @@ -54,6 +55,7 @@ const TC_COMMANDS = [ 'tokenizer', 'stop-strings', 'start-reply-with', + 'reasoning-template', ]; const FANCY_NAMES = { @@ -70,6 +72,7 @@ const FANCY_NAMES = { 'tokenizer': 'Tokenizer', 'stop-strings': 'Custom Stopping Strings', 'start-reply-with': 'Start Reply With', + 'reasoning-template': 'Reasoning Template', }; /** @@ -154,6 +157,7 @@ const profilesProvider = () => [ * @property {string} [tokenizer] Tokenizer * @property {string} [stop-strings] Custom Stopping Strings * @property {string} [start-reply-with] Start Reply With + * @property {string} [reasoning-template] Reasoning Template * @property {string[]} [exclude] Commands to exclude */ diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 6eabf2273..d27d62c31 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -55,6 +55,7 @@ import { POPUP_TYPE, callGenericPopup } from './popup.js'; import { loadSystemPrompts } from './sysprompt.js'; import { fuzzySearchCategories } from './filters.js'; import { accountStorage } from './util/AccountStorage.js'; +import { DEFAULT_REASONING_TEMPLATE, loadReasoningTemplates } from './reasoning.js'; export { loadPowerUserSettings, @@ -257,6 +258,7 @@ let power_user = { }, reasoning: { + name: DEFAULT_REASONING_TEMPLATE, auto_parse: false, add_to_prompts: false, auto_expand: false, @@ -1624,6 +1626,7 @@ async function loadPowerUserSettings(settings, data) { await loadInstructMode(data); await loadContextSettings(); await loadSystemPrompts(data); + await loadReasoningTemplates(data); loadMaxContextUnlocked(); switchWaifuMode(); switchSpoilerMode(); diff --git a/public/scripts/preset-manager.js b/public/scripts/preset-manager.js index c582497fa..cdff9501c 100644 --- a/public/scripts/preset-manager.js +++ b/public/scripts/preset-manager.js @@ -21,7 +21,7 @@ import { groups, selected_group } from './group-chats.js'; import { instruct_presets } from './instruct-mode.js'; import { kai_settings } from './kai-settings.js'; import { convertNovelPreset } from './nai-settings.js'; -import { openai_settings, openai_setting_names, oai_settings } from './openai.js'; +import { openai_settings, openai_setting_names } from './openai.js'; import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; import { context_presets, getContextSettings, power_user } from './power-user.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; @@ -38,6 +38,7 @@ import { } from './textgen-settings.js'; import { download, parseJsonFile, waitUntilCondition } from './utils.js'; import { t } from './i18n.js'; +import { reasoning_templates } from './reasoning.js'; const presetManagers = {}; @@ -168,6 +169,20 @@ class PresetManager { }, isValid: (data) => PresetManager.isPossiblyTextCompletionData(data), }, + 'reasoning': { + name: 'Reasoning Formatting', + getData: () => { + const manager = getPresetManager('reasoning'); + const name = manager.getSelectedPresetName(); + return manager.getPresetSettings(name); + }, + setData: (data) => { + const manager = getPresetManager('reasoning'); + const name = data.name; + return manager.savePreset(name, data); + }, + isValid: (data) => PresetManager.isPossiblyReasoningData(data), + }, }; static isPossiblyInstructData(data) { @@ -190,6 +205,11 @@ class PresetManager { return data && textCompletionProps.every(prop => Object.keys(data).includes(prop)); } + static isPossiblyReasoningData(data) { + const reasoningProps = ['name', 'prefix', 'suffix', 'separator']; + return data && reasoningProps.every(prop => Object.keys(data).includes(prop)); + } + /** * Imports master settings from JSON data. * @param {object} data Data to import @@ -227,6 +247,12 @@ class PresetManager { return await getPresetManager('textgenerationwebui').savePreset(fileName, data); } + // 5. Reasoning Template + if (this.isPossiblyReasoningData(data)) { + toastr.info(t`Importing as reasoning template...`, t`Reasoning template detected`); + return await getPresetManager('reasoning').savePreset(data.name, data); + } + const validSections = []; for (const [key, section] of Object.entries(this.masterSections)) { if (key in data && section.isValid(data[key])) { @@ -478,6 +504,10 @@ class PresetManager { presets = system_prompts; preset_names = system_prompts.map(x => x.name); break; + case 'reasoning': + presets = reasoning_templates; + preset_names = reasoning_templates.map(x => x.name); + break; default: console.warn(`Unknown API ID ${api}`); } @@ -490,7 +520,7 @@ class PresetManager { } isAdvancedFormatting() { - return this.apiId == 'context' || this.apiId == 'instruct' || this.apiId == 'sysprompt'; + return ['context', 'instruct', 'sysprompt', 'reasoning'].includes(this.apiId); } updateList(name, preset) { @@ -553,6 +583,11 @@ class PresetManager { sysprompt_preset['name'] = name || power_user.sysprompt.preset; return sysprompt_preset; } + case 'reasoning': { + const reasoning_preset = structuredClone(power_user.reasoning); + reasoning_preset['name'] = name || power_user.reasoning.preset; + return reasoning_preset; + } default: console.warn(`Unknown API ID ${apiId}`); return {}; @@ -599,6 +634,13 @@ class PresetManager { 'include_reasoning', 'global_banned_tokens', 'send_banned_tokens', + + // Reasoning exclusions + 'auto_parse', + 'add_to_prompts', + 'auto_expand', + 'show_hidden', + 'max_additions', ]; const settings = Object.assign({}, getSettingsByApiId(this.apiId)); diff --git a/public/scripts/reasoning.js b/public/scripts/reasoning.js index 54ee800f2..b63b26f63 100644 --- a/public/scripts/reasoning.js +++ b/public/scripts/reasoning.js @@ -7,14 +7,46 @@ import { getCurrentLocale, t, translate } from './i18n.js'; import { MacrosParser } from './macros.js'; import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js'; import { Popup } from './popup.js'; -import { power_user } from './power-user.js'; +import { performFuzzySearch, power_user } from './power-user.js'; +import { getPresetManager } from './preset-manager.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; -import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty, trimSpaces } from './utils.js'; +import { copyText, escapeRegex, isFalseBoolean, isTrueBoolean, setDatasetProperty, trimSpaces } from './utils.js'; + +/** + * @typedef {object} ReasoningTemplate + * @property {string} name - The name of the template + * @property {string} prefix - Reasoning prefix + * @property {string} suffix - Reasoning suffix + * @property {string} separator - Reasoning separator + */ + +/** + * @type {ReasoningTemplate[]} List of reasoning templates + */ +export const reasoning_templates = []; + +export const DEFAULT_REASONING_TEMPLATE = 'DeepSeek'; + +/** + * @type {Record>} List of UI elements for reasoning settings + * @readonly + */ +const UI = { + $select: $('#reasoning_select'), + $suffix: $('#reasoning_suffix'), + $prefix: $('#reasoning_prefix'), + $separator: $('#reasoning_separator'), + $autoParse: $('#reasoning_auto_parse'), + $autoExpand: $('#reasoning_auto_expand'), + $showHidden: $('#reasoning_show_hidden'), + $addToPrompts: $('#reasoning_add_to_prompts'), + $maxAdditions: $('#reasoning_max_additions'), +}; /** * Enum representing the type of the reasoning for a message (where it came from) @@ -61,7 +93,7 @@ export function extractReasoningFromData(data, { mainApi = null, ignoreShowThoughts = false, textGenType = null, - chatCompletionSource = null + chatCompletionSource = null, } = {}) { switch (mainApi ?? main_api) { case 'textgenerationwebui': @@ -669,57 +701,102 @@ export class PromptReasoning { } function loadReasoningSettings() { - $('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts); - $('#reasoning_add_to_prompts').on('change', function () { + UI.$addToPrompts.prop('checked', power_user.reasoning.add_to_prompts); + UI.$addToPrompts.on('change', function () { power_user.reasoning.add_to_prompts = !!$(this).prop('checked'); saveSettingsDebounced(); }); - $('#reasoning_prefix').val(power_user.reasoning.prefix); - $('#reasoning_prefix').on('input', function () { + UI.$prefix.val(power_user.reasoning.prefix); + UI.$prefix.on('input', function () { power_user.reasoning.prefix = String($(this).val()); saveSettingsDebounced(); }); - $('#reasoning_suffix').val(power_user.reasoning.suffix); - $('#reasoning_suffix').on('input', function () { + UI.$suffix.val(power_user.reasoning.suffix); + UI.$suffix.on('input', function () { power_user.reasoning.suffix = String($(this).val()); saveSettingsDebounced(); }); - $('#reasoning_separator').val(power_user.reasoning.separator); - $('#reasoning_separator').on('input', function () { + UI.$separator.val(power_user.reasoning.separator); + UI.$separator.on('input', function () { power_user.reasoning.separator = String($(this).val()); saveSettingsDebounced(); }); - $('#reasoning_max_additions').val(power_user.reasoning.max_additions); - $('#reasoning_max_additions').on('input', function () { + UI.$maxAdditions.val(power_user.reasoning.max_additions); + UI.$maxAdditions.on('input', function () { power_user.reasoning.max_additions = Number($(this).val()); saveSettingsDebounced(); }); - $('#reasoning_auto_parse').prop('checked', power_user.reasoning.auto_parse); - $('#reasoning_auto_parse').on('change', function () { + UI.$autoParse.prop('checked', power_user.reasoning.auto_parse); + UI.$autoParse.on('change', function () { power_user.reasoning.auto_parse = !!$(this).prop('checked'); saveSettingsDebounced(); }); - $('#reasoning_auto_expand').prop('checked', power_user.reasoning.auto_expand); - $('#reasoning_auto_expand').on('change', function () { + UI.$autoExpand.prop('checked', power_user.reasoning.auto_expand); + UI.$autoExpand.on('change', function () { power_user.reasoning.auto_expand = !!$(this).prop('checked'); toggleReasoningAutoExpand(); saveSettingsDebounced(); }); toggleReasoningAutoExpand(); - $('#reasoning_show_hidden').prop('checked', power_user.reasoning.show_hidden); - $('#reasoning_show_hidden').on('change', function () { + UI.$showHidden.prop('checked', power_user.reasoning.show_hidden); + UI.$showHidden.on('change', function () { power_user.reasoning.show_hidden = !!$(this).prop('checked'); $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null); saveSettingsDebounced(); }); $('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null); + + UI.$select.on('change', async function () { + const name = String($(this).val()); + const template = reasoning_templates.find(p => p.name === name); + if (!template) { + return; + } + + UI.$prefix.val(template.prefix); + UI.$suffix.val(template.suffix); + UI.$separator.val(template.separator); + + power_user.reasoning.name = name; + power_user.reasoning.prefix = template.prefix; + power_user.reasoning.suffix = template.suffix; + power_user.reasoning.separator = template.separator; + + saveSettingsDebounced(); + }); +} + +function selectReasoningTemplateCallback(args, name) { + if (!name) { + return power_user.reasoning.name ?? ''; + } + + const quiet = isTrueBoolean(args?.quiet); + const templateNames = reasoning_templates.map(preset => preset.name); + let foundName = templateNames.find(x => x.toLowerCase() === name.toLowerCase()); + + if (!foundName) { + const result = performFuzzySearch('reasoning-templates', templateNames, [], name); + + if (result.length === 0) { + !quiet && toastr.warning(`Reasoning template "${name}" not found`); + return ''; + } + + foundName = result[0].item; + } + + UI.$select.val(foundName).trigger('change'); + !quiet && toastr.success(`Reasoning template "${foundName}" selected`); + return foundName; + } function registerReasoningSlashCommands() { @@ -853,6 +930,42 @@ function registerReasoningSlashCommands() { : parsedReasoning.reasoning; }, })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'reasoning-template', + aliases: ['reasoning-formatting', 'reasoning-preset'], + callback: selectReasoningTemplateCallback, + returns: 'template name', + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'quiet', + description: 'Suppress the toast message on template change', + typeList: [ARGUMENT_TYPE.BOOLEAN], + defaultValue: 'false', + enumList: commonEnumProviders.boolean('trueFalse')(), + }), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'reasoning template name', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: () => reasoning_templates.map(x => new SlashCommandEnumValue(x.name, null, enumTypes.enum, enumIcons.preset)), + }), + ], + helpString: ` +
+ Selects a reasoning template by name, using fuzzy search to find the closest match. + Gets the current template if no name is provided. +
+
+ Example: +
    +
  • +
    /reasoning-template DeepSeek
    +
  • +
+
+ `, + })); } function registerReasoningMacros() { @@ -1212,6 +1325,53 @@ function registerReasoningAppEvents() { } } +/** + * Loads reasoning templates from the settings data. + * @param {object} data Settings data + * @param {ReasoningTemplate[]} data.reasoning Reasoning templates + * @returns {Promise} + */ +export async function loadReasoningTemplates(data) { + if (data.reasoning !== undefined) { + reasoning_templates.splice(0, reasoning_templates.length, ...data.reasoning); + } + + for (const template of reasoning_templates) { + $('