Merge branch 'staging' into tags-as-folders-enhancements

This commit is contained in:
Wolfsblvt 2024-03-06 01:07:22 +01:00
commit 8e184254c8
28 changed files with 551 additions and 2439 deletions

2
.github/readme.md vendored
View File

@ -12,7 +12,7 @@ Based on a fork of [TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8
2. Missing extensions after the update? Since the 1.10.6 release version, most of the previously built-in extensions have been converted to downloadable add-ons. You can download them via the built-in "Download Extensions and Assets" menu in the extensions panel (stacked blocks icon in the top bar).
3. Having unstyled underscores instead of italics? Since the 1.11.6 release version, the handling of underscores in Markdown text processing was changed. The asterisk italics formatting will continue working as expected. Now double or triple underscores can be used for representing underlined text. Text surrounded by a single underscore is no longer interpreted as italics but left unformatted. To revert to the old behavior, please import and enable the following Regex script: [underscore_italics.json](https://github.com/SillyTavern/SillyTavern/files/14463077/underscore_italics.json)
3. Unsupported platform: android arm LEtime-web. 32-bit Android requires an external dependency that can't be installed with npm. Use the following command to install it: `pkg install esbuild`. Then run the usual installation steps.
### Brought to you by Cohee, RossAscends, and the SillyTavern community

View File

@ -623,7 +623,6 @@
"show_external_models": false,
"proxy_password": "",
"assistant_prefill": "",
"use_ai21_tokenizer": false,
"exclude_assistant": false
"use_ai21_tokenizer": false
}
}

31
package-lock.json generated
View File

@ -13,6 +13,7 @@
"@agnai/sentencepiece-js": "^1.1.1",
"@agnai/web-tokenizers": "^0.1.3",
"@dqbd/tiktoken": "^1.0.13",
"@zeldafan0225/ai_horde": "^4.0.1",
"bing-translate-api": "^2.9.1",
"body-parser": "^1.20.2",
"command-exists": "^1.2.9",
@ -770,6 +771,11 @@
"node": ">=10"
}
},
"node_modules/@thunder04/supermap": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@thunder04/supermap/-/supermap-3.0.4.tgz",
"integrity": "sha512-LiPOoZ/a0L9I9+a0F3Ba4VMQIkZ4gAG0hBT9eqMWc+pjohSsYpxiMbyego+UxUk4nzh9yCHVWjYx3o6B7EGPhg=="
},
"node_modules/@tokenizer/token": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
@ -832,6 +838,16 @@
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
"dev": true
},
"node_modules/@zeldafan0225/ai_horde": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@zeldafan0225/ai_horde/-/ai_horde-4.0.1.tgz",
"integrity": "sha512-mf1cknnBYzKCvgH4KAkdVY3J7sLkR2b79W6I9ZEA2aJCyua28bpZzNaCDSHKKyaNj+0wyHViC+L53X32jw9pMg==",
"dependencies": {
"@thunder04/supermap": "^3.0.2",
"centra": "^2.5.0",
"esbuild": "^0.12.28"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@ -1172,6 +1188,12 @@
"node": ">=6"
}
},
"node_modules/centra": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/centra/-/centra-2.6.0.tgz",
"integrity": "sha512-dgh+YleemrT8u85QL11Z6tYhegAs3MMxsaWAq/oXeAmYJ7VxL3SI9TZtnfaEvNDMAPolj25FXIb3S+HCI4wQaQ==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info."
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@ -1679,6 +1701,15 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/esbuild": {
"version": "0.12.29",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz",
"integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==",
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
}
},
"node_modules/escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",

View File

@ -3,6 +3,7 @@
"@agnai/sentencepiece-js": "^1.1.1",
"@agnai/web-tokenizers": "^0.1.3",
"@dqbd/tiktoken": "^1.0.13",
"@zeldafan0225/ai_horde": "^4.0.1",
"bing-translate-api": "^2.9.1",
"body-parser": "^1.20.2",
"command-exists": "^1.2.9",

View File

@ -391,10 +391,6 @@
flex-wrap: wrap;
height: unset;
}
#extensionsMenuButton {
order: 1;
}
}
/*iOS specific*/

View File

@ -1654,12 +1654,12 @@
</span>
</div>
</div>
<div class="range-block" data-source="openai,openrouter,makersuite,custom">
<div class="range-block" data-source="openai,openrouter,makersuite,claude,custom">
<label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
<input id="openai_image_inlining" type="checkbox" />
<span data-i18n="Send inline images">Send inline images</span>
<div id="image_inlining_hint" class="flexBasis100p toggle-description justifyLeft">
Sends images in prompts if the model supports it (e.g. GPT-4V or Llava 13B).
Sends images in prompts if the model supports it (e.g. GPT-4V, Claude 3 or Llava 13B).
Use the <code><i class="fa-solid fa-paperclip"></i></code> action on any message or the
<code><i class="fa-solid fa-wand-magic-sparkles"></i></code> menu to attach an image file to the chat.
</div>
@ -1682,26 +1682,7 @@
</div>
</div>
<div data-newbie-hidden class="range-block" data-source="claude">
<label for="claude_exclude_prefixes" title="Exclude Human/Assistant prefixes" class="checkbox_label widthFreeExpand">
<input id="claude_exclude_prefixes" type="checkbox" />
<span data-i18n="Exclude Human/Assistant prefixes">Exclude Human/Assistant prefixes</span>
</label>
<div class="toggle-description justifyLeft marginBot5">
<span data-i18n="Exclude Human/Assistant prefixes from being added to the prompt.">
Exclude Human/Assistant prefixes from being added to the prompt, except very first/last message, system prompt Human message and Assistant suffix.
Requires 'Add character names' checked.
</span>
</div>
<label for="exclude_assistant" title="Exclude Assistant suffix" class="checkbox_label widthFreeExpand">
<input id="exclude_assistant" type="checkbox" />
<span data-i18n="Exclude Assistant suffix">Exclude Assistant suffix</span>
</label>
<div class="toggle-description justifyLeft marginBot5">
<span data-i18n="Exclude the assistant suffix from being added to the end of prompt.">
Exclude the assistant suffix from being added to the end of prompt. Requires jailbreak with 'Assistant:' in it.
</span>
</div>
<div id="claude_assistant_prefill_block" class="wide100p">
<div class="wide100p">
<span id="claude_assistant_prefill_text" data-i18n="Assistant Prefill">Assistant Prefill</span>
<textarea id="claude_assistant_prefill" class="text_pole textarea_compact" name="assistant_prefill autoSetHeight" rows="3" maxlength="10000" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
</div>
@ -1712,19 +1693,18 @@
</span>
</label>
<div class="toggle-description justifyLeft marginBot5">
<span data-i18n="Exclude the 'Human: ' prefix from being added to the beginning of the prompt.">
Exclude the 'Human: ' prefix from being added to the beginning of the prompt.
Instead, place it between the system prompt and the first message with the role 'assistant' (right before 'Chat History' by default).
<span data-i18n="Send the system prompt for supported models.">
Send the system prompt for supported models. If disabled, the user message is added to the beginning of the prompt.
</span>
</div>
<div id="claude_human_sysprompt_message_block" class="wide100p">
<div class="range-block-title openai_restorable">
<span data-i18n="Human: first message">Human: first message</span>
<div id="claude_human_sysprompt_message_restore" title="Restore Human: first message" class="right_menu_button">
<span data-i18n="User first message">User first message</span>
<div id="claude_human_sysprompt_message_restore" title="Restore User first message" class="right_menu_button">
<div class="fa-solid fa-clock-rotate-left"></div>
</div>
</div>
<textarea id="claude_human_sysprompt_textarea" class="text_pole textarea_compact" rows="4" maxlength="10000" data-i18n="[placeholder]Human message" placeholder="Human message, instruction, etc.&#10;Adds nothing when empty, i.e. requires a new prompt with the role 'user' or manually adding the 'Human: ' prefix."></textarea>
<textarea id="claude_human_sysprompt_textarea" class="text_pole textarea_compact" rows="4" maxlength="10000" data-i18n="[placeholder]Human message" placeholder="Human message, instruction, etc.&#10;Adds nothing when empty, i.e. requires a new prompt with the role 'user'."></textarea>
</div>
</div>
</div>
@ -2347,24 +2327,14 @@
<div>
<h4 data-i18n="Claude Model">Claude Model</h4>
<select id="model_claude_select">
<optgroup label="Latest">
<option value="claude-2">claude-2</option>
<option value="claude-v1">claude-v1</option>
<option value="claude-v1-100k">claude-v1-100k</option>
<option value="claude-instant-v1">claude-instant-v1</option>
<option value="claude-instant-v1-100k">claude-instant-v1-100k</option>
</optgroup>
<optgroup label="Sub-versions">
<optgroup label="Versions">
<option value="claude-3-opus-20240229">claude-3-opus-20240229</option>
<option value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
<option value="claude-2.1">claude-2.1</option>
<option value="claude-2.0">claude-2.0</option>
<option value="claude-v1.3">claude-v1.3</option>
<option value="claude-v1.3-100k">claude-v1.3-100k</option>
<option value="claude-v1.2">claude-v1.2</option>
<option value="claude-v1.0">claude-v1.0</option>
<option value="claude-1.3">claude-1.3</option>
<option value="claude-instant-1.2">claude-instant-1.2</option>
<option value="claude-instant-v1.1">claude-instant-v1.1</option>
<option value="claude-instant-v1.1-100k">claude-instant-v1.1-100k</option>
<option value="claude-instant-v1.0">claude-instant-v1.0</option>
<option value="claude-instant-1.1">claude-instant-1.1</option>
</optgroup>
</select>
</div>
@ -2440,6 +2410,10 @@
<div class="marginTopBot5">
<label for="openrouter_force_instruct" class="checkbox_label">
<input id="openrouter_force_instruct" type="checkbox" />
<span class="flex-container alignItemsBaseline" title="This option is outdated and will be removed in the future. To use instruct formatting, please switch to OpenRouter under Text Completion API instead.">
<i class="fa-solid fa-circle-exclamation neutral_warning"></i>
<b data-i18n="LEGACY">LEGACY</b>
</span>
<span data-i18n="Force Instruct Mode formatting">Force Instruct Mode formatting</span>
</label>
<div class="toggle-description justifyLeft wide100p">
@ -2752,6 +2726,10 @@
<input id="instruct_macro" type="checkbox" />
<span data-i18n="Replace Macro in Sequences">Replace Macro in Sequences</span>
</label>
<label for="instruct_skip_examples" class="checkbox_label">
<input id="instruct_skip_examples" type="checkbox" />
<span data-i18n="Skip Example Dialogues Formatting">Skip Example Dialogues Formatting</span>
</label>
<label for="instruct_names" class="checkbox_label">
<input id="instruct_names" type="checkbox" />
<span data-i18n="Include Names">Include Names</span>
@ -3588,7 +3566,7 @@
<span data-i18n="Spoiler Free Mode">Spoiler Free Mode</span>
</label>
</div>
<div name="ChatMessageHandlingToggles" >
<div name="ChatMessageHandlingToggles">
<h4 data-i18n="Chat/Message Handling">Chat/Message Handling</h4>
<div data-newbie-hidden class="flex-container alignitemscenter">
<span data-i18n="Send on Enter">
@ -3617,7 +3595,7 @@
<input id="swipes-checkbox" type="checkbox" />
<span data-i18n="Swipes">Swipes</span><i class="fa-solid fa-desktop"></i><i class="fa-solid fa-mobile-screen-button"></i>
</label>
<label data-newbie-hidden class="checkbox_label" for="gestures-checkbox" title="Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC.", data-i18n="[title]Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC">
<label data-newbie-hidden class="checkbox_label" for="gestures-checkbox" title="Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC." , data-i18n="[title]Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC">
<input id="gestures-checkbox" type="checkbox" />
<span data-i18n="Gestures">Gestures</span>
<i class="fa-solid fa-mobile-screen-button"></i>
@ -3700,7 +3678,7 @@
</div>
</div>
<div data-newbie-hidden class="flex-container">
<div id="reload_chat" class="menu_button whitespacenowrap" data-i18n="[title]Reload and redraw the currently open chat" title="Reload and redraw the currently open chat." >
<div id="reload_chat" class="menu_button whitespacenowrap" data-i18n="[title]Reload and redraw the currently open chat" title="Reload and redraw the currently open chat.">
<span data-i18n="Reload Chat">Reload Chat</span>
</div>
<div id="debug_menu" class="menu_button whitespacenowrap" data-i18n="Debug Menu">
@ -3990,6 +3968,12 @@
<option id="renameCharButton" data-i18n="Rename">
Rename
</option>
<option id="character_source" data-i18n="Link to Source">
Link to Source
</option>
<option id="replace_update" data-i18n="Replace / Update">
Replace / Update
</option>
<!--<option id="dupe_button">
Duplicate
</option>
@ -4014,45 +3998,49 @@
</div>
</div>
<hr>
<div id="description_div" class="marginBot5 flex-container alignitemscenter">
<span data-i18n="Character Description">Description</span>
<i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="description_textarea" title="Expand the editor"></i>
<a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#character-description" class="notes-link" target="_blank">
<span class="fa-solid fa-circle-question note-link-span"></span>
</a>
</div>
<textarea id="description_textarea" data-i18n="[placeholder]Describe your character's physical and mental traits here." placeholder="Describe your character's physical and mental traits here." class="marginBot5" name="description" placeholder=""></textarea>
<div class="extension_token_counter">
Tokens: <span data-token-counter="description_textarea" data-token-permanent="true">counting...</span>
</div>
<div id="first_message_div" class="marginBot5 title_restorable">
<div class="flex-container alignitemscenter">
<span data-i18n="First message">First message</span>
<i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="firstmessage_textarea" title="Expand the editor"></i>
<a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#first-message" class="notes-link" target="_blank">
<span class="fa-solid fa-circle-question note-link-span"></span>
</a>
</div>
<div class="menu_button menu_button_icon open_alternate_greetings margin0" title="Click to set additional greeting messages" data-i18n="[title]Click to set additional greeting messages">
<span data-i18n="Alt. Greetings">
Alt. Greetings
</span>
</div>
</div>
<textarea id="firstmessage_textarea" data-i18n="[placeholder]This will be the first message from the character that starts every chat." placeholder="This will be the first message from the character that starts every chat." class="marginBot5" name="first_mes" placeholder=""></textarea>
<div class="extension_token_counter">
Tokens: <span data-token-counter="firstmessage_textarea">counting...</span>
</div>
<div id="spoiler_free_desc">
<div id="spoiler_free_desc" class="flex-container flexFlowColumn flex1">
<div id="creators_notes_div" class="marginBot5 title_restorable">
<span data-i18n="Creator's Notes">Creator's Notes</span>
<div id="spoiler_free_desc_button" class="menu_button fa-solid fa-eye" title="Show / Hide Description and First Message" data-i18n="[title]Show / Hide Description and First Message"></div>
</a>
</div>
<hr>
<div id="creator_notes_spoiler" data-i18n="[placeholder]Creator's Notes" placeholder="Creator's Notes" class="marginBot5" name="creator_notes_spoiler"></div>
<div id="creator_notes_spoiler" class="flex1" data-i18n="[placeholder]Creator's Notes" paceholder="Creator's Notes" class="marginBot5" name="creator_notes_spoiler"></div>
<!-- A button to show / hide description_div and description_textarea and first_message_div and firstmessage_textarea-->
</div>
<div id="descriptionWrapper" class="flex-container flexFlowColumn flex1">
<hr>
<div id="description_div" class="marginBot5 flex-container alignitemscenter">
<span data-i18n="Character Description">Description</span>
<i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="description_textarea" title="Expand the editor"></i>
<a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#character-description" class="notes-link" target="_blank">
<span class="fa-solid fa-circle-question note-link-span"></span>
</a>
</div>
<textarea id="description_textarea" data-i18n="[placeholder]Describe your character's physical and mental traits here." placeholder="Describe your character's physical and mental traits here." class="marginBot5" name="description" placeholder=""></textarea>
<div class="extension_token_counter">
Tokens: <span data-token-counter="description_textarea" data-token-permanent="true">counting...</span>
</div>
</div>
<div id="firstMessageWrapper" class="flex-container flexFlowColumn flex1">
<div id="first_message_div" class="marginBot5 title_restorable">
<div class="flex-container alignitemscenter">
<span data-i18n="First message">First message</span>
<i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="firstmessage_textarea" title="Expand the editor"></i>
<a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#first-message" class="notes-link" target="_blank">
<span class="fa-solid fa-circle-question note-link-span"></span>
</a>
</div>
<div class="menu_button menu_button_icon open_alternate_greetings margin0" title="Click to set additional greeting messages" data-i18n="[title]Click to set additional greeting messages">
<span data-i18n="Alt. Greetings">
Alt. Greetings
</span>
</div>
</div>
<textarea id="firstmessage_textarea" data-i18n="[placeholder]This will be the first message from the character that starts every chat." placeholder="This will be the first message from the character that starts every chat." class="marginBot5" name="first_mes" placeholder=""></textarea>
<div class="extension_token_counter">
Tokens: <span data-token-counter="firstmessage_textarea">counting...</span>
</div>
</div>
<!-- these divs are invisible and used for server communication purposes -->
<div id="hidden-divs">
<input id="character_json_data" name="json_data" type="hidden">
@ -4185,6 +4173,7 @@
<input multiple type="file" id="character_import_file" accept=".json, image/png, .yaml, .yml" name="avatar">
<input id="character_import_file_type" name="file_type" class="text_pole" maxlength="999" size="2" value="" autocomplete="off">
</form>
<input type="file" id="character_replace_file" accept="image/png" name="replace_avatar" hidden>
</div>
<div name="Character List Panel" id="rm_characters_block" class="right_menu">
<div id="charListFixedTop">
@ -4295,7 +4284,7 @@
</div>
<div class="flex-container flexnowrap">
<div class="flex1">
<h4 data-i18n="Creator's Notes">Creator's Notes</h4>
<h4 class="flex-box" data-i18n="Creator's Notes">Creator's Notes<i class="editor_maximize fa-solid fa-maximize" data-for="creator_notes_textarea" title="Expand the editor"></i></h4>
<textarea id="creator_notes_textarea" name="creator_notes" data-i18n="[placeholder](Describe the bot, give use tips, or list the chat models it has been tested on. This will be displayed in the character list.)" placeholder="(Describe the bot, give use tips, or list the chat models it has been tested on. This will be displayed in the character list.)" form="form_create" class="text_pole" autocomplete="off" rows="4" maxlength="20000"></textarea>
</div>
<div class="flex1">
@ -4418,7 +4407,7 @@
<span id="ChatHistoryCharName"></span><span data-i18n="Chat History">Chat History</span>
<a href="https://docs.sillytavern.app/usage/core-concepts/chatfilemanagement/#chat-import" class="notes-link" target="_blank"><span class="fa-solid fa-circle-question note-link-span"></span></a>
</div>
<div id="newChatFromManageScreenButton" class="menu_button menu_button_icon" >
<div id="newChatFromManageScreenButton" class="menu_button menu_button_icon">
<i class="fa-solid fa-plus"></i>
<span data-i18n="New Chat">New Chat</span>
</div>

View File

@ -148,6 +148,7 @@ import {
getBase64Async,
humanFileSize,
Stopwatch,
isValidUrl,
} from './scripts/utils.js';
import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js';
@ -178,6 +179,7 @@ import {
} from './scripts/secrets.js';
import { EventEmitter } from './lib/eventemitter.js';
import { markdownExclusionExt } from './scripts/showdown-exclusion.js';
import { markdownUnderscoreExt } from './scripts/showdown-underscore.js';
import { NOTE_MODULE_NAME, initAuthorsNote, metadata_keys, setFloatingPrompt, shouldWIAddPrompt } from './scripts/authors-note.js';
import { registerPromptManagerMigration } from './scripts/PromptManager.js';
import { getRegexedString, regex_placement } from './scripts/extensions/regex/engine.js';
@ -696,6 +698,7 @@ function reloadMarkdownProcessor(render_formulas = false) {
parseImgDimensions: true,
tables: true,
underline: true,
extensions: [markdownUnderscoreExt()],
});
}
@ -1352,6 +1355,22 @@ export async function getOneCharacter(avatarUrl) {
}
}
function getCharacterSource(chId = this_chid) {
const character = characters[chId];
if (!character) {
return '';
}
const chubId = characters[this_chid]?.data?.extensions?.chub?.full_path;
if (chubId) {
return `https://chub.ai/characters/${chubId}`;
}
return '';
}
async function getCharacters() {
var response = await fetch('/api/characters/all', {
method: 'POST',
@ -2463,12 +2482,12 @@ export function getCharacterCardFields() {
return result;
}
const scenarioText = chat_metadata['scenario'] || characters[this_chid].scenario;
result.description = baseChatReplace(characters[this_chid].description.trim(), name1, name2);
result.personality = baseChatReplace(characters[this_chid].personality.trim(), name1, name2);
const scenarioText = chat_metadata['scenario'] || characters[this_chid]?.scenario;
result.description = baseChatReplace(characters[this_chid].description?.trim(), name1, name2);
result.personality = baseChatReplace(characters[this_chid].personality?.trim(), name1, name2);
result.scenario = baseChatReplace(scenarioText.trim(), name1, name2);
result.mesExamples = baseChatReplace(characters[this_chid].mes_example.trim(), name1, name2);
result.persona = baseChatReplace(power_user.persona_description.trim(), name1, name2);
result.mesExamples = baseChatReplace(characters[this_chid].mes_example?.trim(), name1, name2);
result.persona = baseChatReplace(power_user.persona_description?.trim(), name1, name2);
result.system = power_user.prefer_character_prompt ? baseChatReplace(characters[this_chid].data?.system_prompt?.trim(), name1, name2) : '';
result.jailbreak = power_user.prefer_character_jailbreak ? baseChatReplace(characters[this_chid].data?.post_history_instructions?.trim(), name1, name2) : '';
@ -4490,6 +4509,10 @@ function parseAndSaveLogprobs(data, continueFrom) {
* @returns {string} Extracted message
*/
function extractMessageFromData(data) {
if (typeof data === 'string') {
return data;
}
switch (main_api) {
case 'kobold':
return data.results[0].text;
@ -6417,7 +6440,7 @@ export function select_selected_character(chid) {
$('#description_textarea').val(characters[chid].description);
$('#character_world').val(characters[chid].data?.extensions?.world || '');
$('#creator_notes_textarea').val(characters[chid].data?.creator_notes || characters[chid].creatorcomment);
$('#creator_notes_spoiler').text(characters[chid].data?.creator_notes || characters[chid].creatorcomment);
$('#creator_notes_spoiler').html(DOMPurify.sanitize(converter.makeHtml(characters[chid].data?.creator_notes || characters[chid].creatorcomment), { MESSAGE_SANITIZE: true }));
$('#character_version_textarea').val(characters[chid].data?.character_version || '');
$('#system_prompt_textarea').val(characters[chid].data?.system_prompt || '');
$('#post_history_instructions_textarea').val(characters[chid].data?.post_history_instructions || '');
@ -6487,7 +6510,7 @@ function select_rm_create() {
$('#description_textarea').val(create_save.description);
$('#character_world').val(create_save.world);
$('#creator_notes_textarea').val(create_save.creator_notes);
$('#creator_notes_spoiler').text(create_save.creator_notes);
$('#creator_notes_spoiler').html(DOMPurify.sanitize(converter.makeHtml(create_save.creator_notes), { MESSAGE_SANITIZE: true }));
$('#post_history_instructions_textarea').val(create_save.post_history_instructions);
$('#system_prompt_textarea').val(create_save.system_prompt);
$('#tags_textarea').val(create_save.tags);
@ -9764,6 +9787,40 @@ jQuery(async function () {
await importEmbeddedWorldInfo();
saveCharacterDebounced();
break;
case 'character_source': {
const source = getCharacterSource(this_chid);
if (source && isValidUrl(source)) {
const url = new URL(source);
const confirm = await callPopup(`Open ${url.hostname} in a new tab?`, 'confirm');
if (confirm) {
window.open(source, '_blank');
}
} else {
toastr.info('This character doesn\'t seem to have a source.');
}
} break;
case 'replace_update': {
const confirm = await callPopup('<p><b>Choose a new character card to replace this character with.</b></p><p>All chats, assets and group memberships will be preserved, but local changes to the character data will be lost.</p><p>Proceed?</p>', 'confirm', '');
if (confirm) {
async function uploadReplacementCard(e) {
const file = e.target.files[0];
if (!file) {
return;
}
try {
const cloneFile = new File([file], characters[this_chid].avatar, { type: file.type });
const chatFile = characters[this_chid]['chat'];
await processDroppedFiles([cloneFile], true);
await openCharacterChat(chatFile);
} catch {
toastr.error('Failed to replace the character card.', 'Something went wrong');
}
}
$('#character_replace_file').off('change').on('change', uploadReplacementCard).trigger('click');
}
} break;
/*case 'delete_button':
popup_type = "del_ch";
callPopup(`
@ -9930,10 +9987,10 @@ jQuery(async function () {
const html = `<h3>Enter the URL of the content to import</h3>
Supported sources:<br>
<ul class="justifyLeft">
<li>Chub characters (direct link or id)<br>Example: <tt>Anonymous/example-character</tt></li>
<li>Chub lorebooks (direct link or id)<br>Example: <tt>lorebooks/bartleby/example-lorebook</tt></li>
<li>JanitorAI character (direct link or id)<br>Example: <tt>https://janitorai.com/characters/ddd1498a-a370-4136-b138-a8cd9461fdfe_character-aqua-the-useless-goddess</tt></li>
<li>Pygmalion.chat character (link)<br>Example: <tt>https://pygmalion.chat/character/a7ca95a1-0c88-4e23-91b3-149db1e78ab9</tt></li>
<li>Chub Character (Direct Link or ID)<br>Example: <tt>Anonymous/example-character</tt></li>
<li>Chub Lorebook (Direct Link or ID)<br>Example: <tt>lorebooks/bartleby/example-lorebook</tt></li>
<li>JanitorAI Character (Direct Link or UUID)<br>Example: <tt>ddd1498a-a370-4136-b138-a8cd9461fdfe_character-aqua-the-useless-goddess</tt></li>
<li>Pygmalion.chat Character (Direct Link or UUID)<br>Example: <tt>a7ca95a1-0c88-4e23-91b3-149db1e78ab9</tt></li>
<li>More coming soon...</li>
<ul>`;
const input = await callPopup(html, 'input', '', { okButton: 'Import', rows: 4 });
@ -9944,13 +10001,23 @@ jQuery(async function () {
}
const url = input.trim();
console.debug('Custom content import started', url);
var request;
const request = await fetch('/api/content/import', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ url }),
});
if (isValidUrl(url)) {
console.debug('Custom content import started for URL: ', url);
request = await fetch('/api/content/importURL', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ url }),
});
} else {
console.debug('Custom content import started for Char UUID: ', url);
request = await fetch('/api/content/importUUID', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ url }),
});
}
if (!request.ok) {
toastr.info(request.statusText, 'Custom content import failed');

View File

@ -284,6 +284,7 @@ jQuery(function () {
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && secret_state[SECRET_KEYS.MAKERSUITE]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && secret_state[SECRET_KEYS.CLAUDE]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ollama' && textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'llamacpp' && textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ooba' && textgenerationwebui_settings.server_urls[textgen_types.OOBA]) ||
@ -316,7 +317,8 @@ jQuery(function () {
$('#caption_multimodal_model').val(extension_settings.caption.multimodal_model);
$('#caption_multimodal_block [data-type]').each(function () {
const type = $(this).data('type');
$(this).toggle(type === extension_settings.caption.multimodal_api);
const types = type.split(',');
$(this).toggle(types.includes(extension_settings.caption.multimodal_api));
});
$('#caption_multimodal_api').on('change', () => {
const api = String($('#caption_multimodal_api').val());
@ -343,7 +345,7 @@ jQuery(function () {
<label for="caption_source">Source</label>
<select id="caption_source" class="text_pole">
<option value="local">Local</option>
<option value="multimodal">Multimodal (OpenAI / llama / Google)</option>
<option value="multimodal">Multimodal (OpenAI / Anthropic / llama / Google)</option>
<option value="extras">Extras</option>
<option value="horde">Horde</option>
</select>
@ -355,6 +357,7 @@ jQuery(function () {
<option value="ooba">Text Generation WebUI (oobabooga)</option>
<option value="ollama">Ollama</option>
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="openrouter">OpenRouter</option>
<option value="google">Google MakerSuite</option>
<option value="custom">Custom (OpenAI-compatible)</option>
@ -364,6 +367,8 @@ jQuery(function () {
<label for="caption_multimodal_model">Model</label>
<select id="caption_multimodal_model" class="flex1 text_pole">
<option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
<option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
<option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
<option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
<option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
<option data-type="openrouter" value="haotian-liu/llava-13b">haotian-liu/llava-13b</option>
@ -375,7 +380,7 @@ jQuery(function () {
<option data-type="custom" value="custom_current">[Currently selected]</option>
</select>
</div>
<label data-type="openai" class="checkbox_label flexBasis100p" for="caption_allow_reverse_proxy" title="Allow using reverse proxy if defined and valid.">
<label data-type="openai,anthropic" class="checkbox_label flexBasis100p" for="caption_allow_reverse_proxy" title="Allow using reverse proxy if defined and valid.">
<input id="caption_allow_reverse_proxy" type="checkbox" class="checkbox">
Allow reverse proxy
</label>

View File

@ -23,6 +23,7 @@ export async function getMultimodalCaption(base64Img, prompt) {
// OpenRouter has a payload limit of ~2MB. Google is 4MB, but we love democracy.
// Ooba requires all images to be JPEGs.
const isGoogle = extension_settings.caption.multimodal_api === 'google';
const isClaude = extension_settings.caption.multimodal_api === 'anthropic';
const isOllama = extension_settings.caption.multimodal_api === 'ollama';
const isLlamaCpp = extension_settings.caption.multimodal_api === 'llamacpp';
const isCustom = extension_settings.caption.multimodal_api === 'custom';
@ -39,7 +40,7 @@ export async function getMultimodalCaption(base64Img, prompt) {
}
const useReverseProxy =
extension_settings.caption.multimodal_api === 'openai'
(extension_settings.caption.multimodal_api === 'openai' || extension_settings.caption.multimodal_api === 'anthropic')
&& extension_settings.caption.allow_reverse_proxy
&& oai_settings.reverse_proxy
&& isValidUrl(oai_settings.reverse_proxy);
@ -87,6 +88,8 @@ export async function getMultimodalCaption(base64Img, prompt) {
switch (extension_settings.caption.multimodal_api) {
case 'google':
return '/api/google/caption-image';
case 'anthropic':
return '/api/anthropic/caption-image';
case 'llamacpp':
return '/api/backends/text-completions/llamacpp/caption-image';
case 'ollama':

View File

@ -30,6 +30,7 @@ const controls = [
{ id: 'instruct_last_output_sequence', property: 'last_output_sequence', isCheckbox: false },
{ id: 'instruct_activation_regex', property: 'activation_regex', isCheckbox: false },
{ id: 'instruct_bind_to_context', property: 'bind_to_context', isCheckbox: true },
{ id: 'instruct_skip_examples', property: 'skip_examples', isCheckbox: true },
];
/**
@ -45,6 +46,10 @@ export function loadInstructMode(data) {
power_user.instruct.names_force_groups = true;
}
if (power_user.instruct.skip_examples === undefined) {
power_user.instruct.skip_examples = false;
}
controls.forEach(control => {
const $element = $(`#${control.id}`);
@ -302,6 +307,10 @@ export function formatInstructModeSystemPrompt(systemPrompt){
* @returns {string} Formatted example messages string.
*/
export function formatInstructModeExamples(mesExamples, name1, name2) {
if (power_user.instruct.skip_examples) {
return mesExamples;
}
const includeNames = power_user.instruct.names || (!!selected_group && power_user.instruct.names_force_groups);
let inputSequence = power_user.instruct.input_sequence;

View File

@ -213,7 +213,7 @@ const default_settings = {
scenario_format: default_scenario_format,
personality_format: default_personality_format,
openai_model: 'gpt-3.5-turbo',
claude_model: 'claude-instant-v1',
claude_model: 'claude-2.1',
google_model: 'gemini-pro',
ai21_model: 'j2-ultra',
mistralai_model: 'mistral-medium-latest',
@ -239,9 +239,7 @@ const default_settings = {
human_sysprompt_message: default_claude_human_sysprompt_message,
use_ai21_tokenizer: false,
use_google_tokenizer: false,
exclude_assistant: false,
claude_use_sysprompt: false,
claude_exclude_prefixes: false,
use_alt_scale: false,
squash_system_messages: false,
image_inlining: false,
@ -282,7 +280,7 @@ const oai_settings = {
scenario_format: default_scenario_format,
personality_format: default_personality_format,
openai_model: 'gpt-3.5-turbo',
claude_model: 'claude-instant-v1',
claude_model: 'claude-2.1',
google_model: 'gemini-pro',
ai21_model: 'j2-ultra',
mistralai_model: 'mistral-medium-latest',
@ -308,9 +306,7 @@ const oai_settings = {
human_sysprompt_message: default_claude_human_sysprompt_message,
use_ai21_tokenizer: false,
use_google_tokenizer: false,
exclude_assistant: false,
claude_use_sysprompt: false,
claude_exclude_prefixes: false,
use_alt_scale: false,
squash_system_messages: false,
image_inlining: false,
@ -1634,13 +1630,11 @@ async function sendOpenAIRequest(type, messages, signal) {
if (isClaude) {
generate_data['top_k'] = Number(oai_settings.top_k_openai);
generate_data['exclude_assistant'] = oai_settings.exclude_assistant;
generate_data['claude_use_sysprompt'] = oai_settings.claude_use_sysprompt;
generate_data['claude_exclude_prefixes'] = oai_settings.claude_exclude_prefixes;
generate_data['stop'] = getCustomStoppingStrings(); // Claude shouldn't have limits on stop strings.
generate_data['human_sysprompt_message'] = substituteParams(oai_settings.human_sysprompt_message);
// Don't add a prefill on quiet gens (summarization)
if (!isQuiet && !oai_settings.exclude_assistant) {
if (!isQuiet) {
generate_data['assistant_prefill'] = substituteParams(oai_settings.assistant_prefill);
}
}
@ -1751,7 +1745,7 @@ async function sendOpenAIRequest(type, messages, signal) {
function getStreamingReply(data) {
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
return data?.completion || '';
return data?.delta?.text || '';
} else if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
return data?.candidates?.[0]?.content?.parts?.[0]?.text || '';
} else {
@ -2564,9 +2558,7 @@ function loadOpenAISettings(data, settings) {
if (settings.openai_model !== undefined) oai_settings.openai_model = settings.openai_model;
if (settings.use_ai21_tokenizer !== undefined) { oai_settings.use_ai21_tokenizer = !!settings.use_ai21_tokenizer; oai_settings.use_ai21_tokenizer ? ai21_max = 8191 : ai21_max = 9200; }
if (settings.use_google_tokenizer !== undefined) oai_settings.use_google_tokenizer = !!settings.use_google_tokenizer;
if (settings.exclude_assistant !== undefined) oai_settings.exclude_assistant = !!settings.exclude_assistant;
if (settings.claude_use_sysprompt !== undefined) oai_settings.claude_use_sysprompt = !!settings.claude_use_sysprompt;
if (settings.claude_exclude_prefixes !== undefined) oai_settings.claude_exclude_prefixes = !!settings.claude_exclude_prefixes;
if (settings.use_alt_scale !== undefined) { oai_settings.use_alt_scale = !!settings.use_alt_scale; updateScaleForm(); }
$('#stream_toggle').prop('checked', oai_settings.stream_openai);
$('#api_url_scale').val(oai_settings.api_url_scale);
@ -2604,9 +2596,7 @@ function loadOpenAISettings(data, settings) {
$('#openai_external_category').toggle(oai_settings.show_external_models);
$('#use_ai21_tokenizer').prop('checked', oai_settings.use_ai21_tokenizer);
$('#use_google_tokenizer').prop('checked', oai_settings.use_google_tokenizer);
$('#exclude_assistant').prop('checked', oai_settings.exclude_assistant);
$('#claude_use_sysprompt').prop('checked', oai_settings.claude_use_sysprompt);
$('#claude_exclude_prefixes').prop('checked', oai_settings.claude_exclude_prefixes);
$('#scale-alt').prop('checked', oai_settings.use_alt_scale);
$('#openrouter_use_fallback').prop('checked', oai_settings.openrouter_use_fallback);
$('#openrouter_force_instruct').prop('checked', oai_settings.openrouter_force_instruct);
@ -2828,9 +2818,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
human_sysprompt_message: settings.human_sysprompt_message,
use_ai21_tokenizer: settings.use_ai21_tokenizer,
use_google_tokenizer: settings.use_google_tokenizer,
exclude_assistant: settings.exclude_assistant,
claude_use_sysprompt: settings.claude_use_sysprompt,
claude_exclude_prefixes: settings.claude_exclude_prefixes,
use_alt_scale: settings.use_alt_scale,
squash_system_messages: settings.squash_system_messages,
image_inlining: settings.image_inlining,
@ -3205,9 +3193,7 @@ function onSettingsPresetChange() {
human_sysprompt_message: ['#claude_human_sysprompt_textarea', 'human_sysprompt_message', false],
use_ai21_tokenizer: ['#use_ai21_tokenizer', 'use_ai21_tokenizer', true],
use_google_tokenizer: ['#use_google_tokenizer', 'use_google_tokenizer', true],
exclude_assistant: ['#exclude_assistant', 'exclude_assistant', true],
claude_use_sysprompt: ['#claude_use_sysprompt', 'claude_use_sysprompt', true],
claude_exclude_prefixes: ['#claude_exclude_prefixes', 'claude_exclude_prefixes', true],
use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true],
squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true],
image_inlining: ['#openai_image_inlining', 'image_inlining', true],
@ -3330,8 +3316,15 @@ async function onModelChange() {
let value = String($(this).val() || '');
if ($(this).is('#model_claude_select')) {
if (value.includes('-v')) {
value = value.replace('-v', '-');
} else if (value === '' || value === 'claude-2') {
value = default_settings.claude_model;
}
console.log('Claude model changed to', value);
oai_settings.claude_model = value;
$('#model_claude_select').val(oai_settings.claude_model);
}
if ($(this).is('#model_windowai_select')) {
@ -3439,7 +3432,7 @@ async function onModelChange() {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', max_200k);
}
else if (value == 'claude-2.1' || value == 'claude-2') {
else if (value == 'claude-2.1' || value.startsWith('claude-3')) {
$('#openai_max_context').attr('max', max_200k);
}
else if (value.endsWith('100k') || value.startsWith('claude-2') || value === 'claude-instant-1.2') {
@ -3735,7 +3728,6 @@ function toggleChatCompletionForms() {
});
if (chat_completion_sources.CLAUDE == oai_settings.chat_completion_source) {
$('#claude_assistant_prefill_block').toggle(!oai_settings.exclude_assistant);
$('#claude_human_sysprompt_message_block').toggle(oai_settings.claude_use_sysprompt);
}
}
@ -3829,6 +3821,7 @@ export function isImageInliningSupported() {
const gpt4v = 'gpt-4-vision';
const geminiProV = 'gemini-pro-vision';
const claude = 'claude-3';
const llava = 'llava';
if (!oai_settings.image_inlining) {
@ -3840,6 +3833,8 @@ export function isImageInliningSupported() {
return oai_settings.openai_model.includes(gpt4v);
case chat_completion_sources.MAKERSUITE:
return oai_settings.google_model.includes(geminiProV);
case chat_completion_sources.CLAUDE:
return oai_settings.claude_model.includes(claude);
case chat_completion_sources.OPENROUTER:
return !oai_settings.openrouter_force_instruct && (oai_settings.openrouter_model.includes(gpt4v) || oai_settings.openrouter_model.includes(llava));
case chat_completion_sources.CUSTOM:
@ -4075,23 +4070,12 @@ $(document).ready(async function () {
saveSettingsDebounced();
});
$('#exclude_assistant').on('change', function () {
oai_settings.exclude_assistant = !!$('#exclude_assistant').prop('checked');
$('#claude_assistant_prefill_block').toggle(!oai_settings.exclude_assistant);
saveSettingsDebounced();
});
$('#claude_use_sysprompt').on('change', function () {
oai_settings.claude_use_sysprompt = !!$('#claude_use_sysprompt').prop('checked');
$('#claude_human_sysprompt_message_block').toggle(oai_settings.claude_use_sysprompt);
saveSettingsDebounced();
});
$('#claude_exclude_prefixes').on('change', function () {
oai_settings.claude_exclude_prefixes = !!$('#claude_exclude_prefixes').prop('checked');
saveSettingsDebounced();
});
$('#names_in_completion').on('change', function () {
oai_settings.names_in_completion = !!$('#names_in_completion').prop('checked');
saveSettingsDebounced();

View File

@ -918,27 +918,24 @@ function switchWaifuMode() {
function switchSpoilerMode() {
if (power_user.spoiler_free_mode) {
$('#description_div').hide();
$('#description_textarea').hide();
$('#firstmessage_textarea').hide();
$('#first_message_div').hide();
$('#spoiler_free_desc').show();
$('#descriptionWrapper').hide();
$('#firstMessageWrapper').hide();
$('#spoiler_free_desc').addClass('flex1');
$('#creator_notes_spoiler').show();
}
else {
$('#description_div').show();
$('#description_textarea').show();
$('#firstmessage_textarea').show();
$('#first_message_div').show();
$('#spoiler_free_desc').hide();
$('#descriptionWrapper').show();
$('#firstMessageWrapper').show();
$('#spoiler_free_desc').removeClass('flex1');
$('#creator_notes_spoiler').hide();
}
}
function peekSpoilerMode() {
$('#description_div').toggle();
$('#description_textarea').toggle();
$('#firstmessage_textarea').toggle();
$('#first_message_div').toggle();
$('#descriptionWrapper').toggle();
$('#firstMessageWrapper').toggle();
$('#creator_notes_spoiler').toggle();
$('#spoiler_free_desc').toggleClass('flex1');
}
@ -3348,7 +3345,7 @@ $(document).ready(() => {
$('#ui_preset_import_file').trigger('click');
});
$('#ui_preset_import_file').on('change', async function() {
$('#ui_preset_import_file').on('change', async function () {
const inputElement = this instanceof HTMLInputElement && this;
try {

View File

@ -0,0 +1,27 @@
// Showdown extension that replaces words surrounded by singular underscores with <em> tags
export const markdownUnderscoreExt = () => {
try {
if (!canUseNegativeLookbehind()) {
console.log('Showdown-underscore extension: Negative lookbehind not supported. Skipping.');
return [];
}
return [{
type: 'lang',
regex: new RegExp('\\b(?<!_)_(?!_)(.*?)(?<!_)_(?!_)\\b', 'g'),
replace: '<em>$1</em>',
}];
} catch (e) {
console.error('Error in Showdown-underscore extension:', e);
return [];
}
};
function canUseNegativeLookbehind() {
try {
new RegExp('(?<!_)');
return true;
} catch (e) {
return false;
}
}

View File

@ -173,8 +173,7 @@ parser.addCommand('gen', generateCallback, [], '<span class="monospace">(lock=on
parser.addCommand('genraw', generateRawCallback, [], '<span class="monospace">(lock=on/off [prompt])</span> generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating. Does not include chat history or character card. Use instruct=off to skip instruct formatting, e.g. <tt>/genraw instruct=off Why is the sky blue?</tt>. Use stop=... with a JSON-serialized array to add one-time custom stop strings, e.g. <tt>/genraw stop=["\\n"] Say hi</tt>', true, true);
parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '<span class="monospace">(text)</span> adds a swipe to the last chat message.', true, true);
parser.addCommand('abort', abortCallback, [], ' aborts the slash command batch execution', true, true);
parser.addCommand('fuzzy', fuzzyCallback, [], 'list=["a","b","c"] (search value) performs a fuzzy match of the provided search using the provided list of value and passes the closest match to the next command through the pipe.', true, true);
parser.addCommand('pass', (_, arg) => arg, ['return'], '<span class="monospace">(text)</span> passes the text to the next command through the pipe.', true, true);
parser.addCommand('fuzzy', fuzzyCallback, [], 'list=["a","b","c"] threshold=0.4 (text to search) performs a fuzzy match of each items of list within the text to search. If any item matches then its name is returned. If no item list matches the text to search then no value is returned. The optional threshold (default is 0.4) allows some control over the matching. A low value (min 0.0) means the match is very strict. At 1.0 (max) the match is very loose and probably matches anything. The returned value passes to the next command through the pipe.', true, true); parser.addCommand('pass', (_, arg) => arg, ['return'], '<span class="monospace">(text)</span> passes the text to the next command through the pipe.', true, true);
parser.addCommand('delay', delayCallback, ['wait', 'sleep'], '<span class="monospace">(milliseconds)</span> delays the next command in the pipe by the specified number of milliseconds.', true, true);
parser.addCommand('input', inputCallback, ['prompt'], '<span class="monospace">(default="string" large=on/off wide=on/off okButton="string" rows=number [text])</span> Shows a popup with the provided text and an input field. The default argument is the default value of the input field, and the text argument is the text to display.', true, true);
parser.addCommand('run', runCallback, ['call', 'exec'], '<span class="monospace">[key1=value key2=value ...] ([qrSet.]qrLabel)</span> runs a Quick Reply with the specified name from a currently active preset or from another preset, named arguments can be referenced in a QR with {{arg::key}}.', true, true);
@ -502,8 +501,17 @@ async function inputCallback(args, prompt) {
return result || '';
}
function fuzzyCallback(args, value) {
if (!value) {
/**
* Each item in "args.list" is searched within "search_item" using fuzzy search. If any matches it returns the matched "item".
* @param {FuzzyCommandArgs} args - arguments containing "list" (JSON array) and optionaly "threshold" (float between 0.0 and 1.0)
* @param {string} searchInValue - the string where items of list are searched
* @returns {string} - the matched item from the list
* @typedef {{list: string, threshold: string}} FuzzyCommandArgs - arguments for /fuzzy command
* @example /fuzzy list=["down","left","up","right"] "he looks up" | /echo // should return "up"
* @link https://www.fusejs.io/
*/
function fuzzyCallback(args, searchInValue) {
if (!searchInValue) {
console.warn('WARN: No argument provided for /fuzzy command');
return '';
}
@ -520,14 +528,37 @@ function fuzzyCallback(args, value) {
return '';
}
const fuse = new Fuse(list, {
const params = {
includeScore: true,
findAllMatches: true,
ignoreLocation: true,
threshold: 0.7,
});
const result = fuse.search(value);
return result[0]?.item;
threshold: 0.4,
};
// threshold determines how strict is the match, low threshold value is very strict, at 1 (nearly?) everything matches
if ('threshold' in args) {
params.threshold = parseFloat(resolveVariable(args.threshold));
if (isNaN(params.threshold)) {
console.warn('WARN: \'threshold\' argument must be a float between 0.0 and 1.0 for /fuzzy command');
return '';
}
if (params.threshold < 0) {
params.threshold = 0;
}
if (params.threshold > 1) {
params.threshold = 1;
}
}
const fuse = new Fuse([searchInValue], params);
// each item in the "list" is searched within "search_item", if any matches it returns the matched "item"
for (const searchItem of list) {
const result = fuse.search(searchItem);
if (result.length > 0) {
console.info('fuzzyCallback Matched: ' + searchItem);
return searchItem;
}
}
return '';
} catch {
console.warn('WARN: Invalid list argument provided for /fuzzy command');
return '';
@ -1584,7 +1615,7 @@ async function executeSlashCommands(text, unescape = false) {
?.replace(/\\\|/g, '|')
?.replace(/\\\{/g, '{')
?.replace(/\\\}/g, '}')
;
;
}
for (const [key, value] of Object.entries(result.args)) {
@ -1593,7 +1624,7 @@ async function executeSlashCommands(text, unescape = false) {
.replace(/\\\|/g, '|')
.replace(/\\\{/g, '{')
.replace(/\\\}/g, '}')
;
;
}
}

View File

@ -266,7 +266,7 @@ function countWords(str) {
* @param {string} oldMesssage - The old message that's being processed.
*/
async function statMesProcess(line, type, characters, this_chid, oldMesssage) {
if (this_chid === undefined) {
if (this_chid === undefined || characters[this_chid] === undefined) {
return;
}
await getStats();

View File

@ -970,7 +970,7 @@ export async function saveBase64AsFile(base64Data, characterName, filename = '',
const requestBody = {
image: dataURL,
ch_name: characterName,
filename: filename,
filename: String(filename).replace(/\./g, '_'),
};
// Send the data URL to your backend using fetch

View File

@ -1064,6 +1064,19 @@ select {
order: 3;
}
#character_popup .editor_maximize {
cursor: pointer;
margin: 5px;
opacity: 0.75;
filter: grayscale(1);
-webkit-transition: all 250ms ease-in-out;
transition: all 250ms ease-in-out;
}
#character_popup .editor_maximize:hover {
opacity: 1;
}
.text_pole::placeholder {
color: rgb(139, 139, 139);
}
@ -1074,6 +1087,11 @@ select {
white-space: nowrap;
}
#creator_notes_spoiler {
border: 0;
font-size: calc(var(--mainFontSize)*.8);
}
@media screen and (max-width: 1000px) {
#form_create textarea {
flex-grow: 1;
@ -3835,4 +3853,4 @@ a {
height: 100vh;
z-index: 9999;
}
}
}

View File

@ -384,9 +384,9 @@ app.post('/uploadimage', jsonParser, async (request, response) => {
}
// if character is defined, save to a sub folder for that character
let pathToNewFile = path.join(DIRECTORIES.userImages, filename);
let pathToNewFile = path.join(DIRECTORIES.userImages, sanitize(filename));
if (request.body.ch_name) {
pathToNewFile = path.join(DIRECTORIES.userImages, request.body.ch_name, filename);
pathToNewFile = path.join(DIRECTORIES.userImages, sanitize(request.body.ch_name), sanitize(filename));
}
ensureDirectoryExistence(pathToNewFile);
@ -505,6 +505,9 @@ app.use('/api/openai', require('./src/endpoints/openai').router);
//Google API
app.use('/api/google', require('./src/endpoints/google').router);
//Anthropic API
app.use('/api/anthropic', require('./src/endpoints/anthropic').router);
// Tokenizers
app.use('/api/tokenizers', require('./src/endpoints/tokenizers').router);

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 ZeldaFan0225
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

2192
src/ai_horde/index.d.ts vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,3 +0,0 @@
import AIHorde from './index.js'
export default AIHorde
export { AIHorde }

View File

@ -0,0 +1,67 @@
const { readSecret, SECRET_KEYS } = require('./secrets');
const fetch = require('node-fetch').default;
const express = require('express');
const { jsonParser } = require('../express-common');
const router = express.Router();
router.post('/caption-image', jsonParser, async (request, response) => {
try {
const mimeType = request.body.image.split(';')[0].split(':')[1];
const base64Data = request.body.image.split(',')[1];
const baseUrl = request.body.reverse_proxy ? request.body.reverse_proxy : 'https://api.anthropic.com/v1';
const url = `${baseUrl}/messages`;
const body = {
model: request.body.model,
messages: [
{
'role': 'user', 'content': [
{
'type': 'image',
'source': {
'type': 'base64',
'media_type': mimeType,
'data': base64Data,
},
},
{ 'type': 'text', 'text': request.body.prompt },
],
},
],
max_tokens: 800,
};
console.log('Multimodal captioning request', body);
const result = await fetch(url, {
body: JSON.stringify(body),
method: 'POST',
headers: {
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01',
'x-api-key': request.body.reverse_proxy ? request.body.proxy_password : readSecret(SECRET_KEYS.CLAUDE),
},
timeout: 0,
});
if (!result.ok) {
console.log(`Claude API returned error: ${result.status} ${result.statusText}`);
return response.status(result.status).send({ error: true });
}
const generateResponseJson = await result.json();
const caption = generateResponseJson.content[0].text;
console.log('Claude response:', generateResponseJson);
if (!caption) {
return response.status(500).send('No caption found');
}
return response.json({ caption });
} catch (error) {
console.error(error);
response.status(500).send('Internal server error');
}
});
module.exports = { router };

View File

@ -5,7 +5,7 @@ const { Readable } = require('stream');
const { jsonParser } = require('../../express-common');
const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants');
const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util');
const { convertClaudePrompt, convertGooglePrompt, convertTextCompletionPrompt } = require('../prompt-converters');
const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt } = require('../prompt-converters');
const { readSecret, SECRET_KEYS } = require('../secrets');
const { getTokenizerModel, getSentencepiceTokenizer, getTiktokenTokenizer, sentencepieceTokenizers, TEXT_COMPLETION_MODELS } = require('../tokenizers');
@ -34,45 +34,8 @@ async function sendClaudeRequest(request, response) {
request.socket.on('close', function () {
controller.abort();
});
const isSysPromptSupported = request.body.model === 'claude-2' || request.body.model === 'claude-2.1';
const requestPrompt = convertClaudePrompt(request.body.messages, !request.body.exclude_assistant, request.body.assistant_prefill, isSysPromptSupported, request.body.claude_use_sysprompt, request.body.human_sysprompt_message, request.body.claude_exclude_prefixes);
// Check Claude messages sequence and prefixes presence.
let sequenceError = [];
const sequence = requestPrompt.split('\n').filter(x => x.startsWith('Human:') || x.startsWith('Assistant:'));
const humanFound = sequence.some(line => line.startsWith('Human:'));
const assistantFound = sequence.some(line => line.startsWith('Assistant:'));
let humanErrorCount = 0;
let assistantErrorCount = 0;
for (let i = 0; i < sequence.length - 1; i++) {
if (sequence[i].startsWith(sequence[i + 1].split(':')[0])) {
if (sequence[i].startsWith('Human:')) {
humanErrorCount++;
} else if (sequence[i].startsWith('Assistant:')) {
assistantErrorCount++;
}
}
}
if (!humanFound) {
sequenceError.push(`${divider}\nWarning: No 'Human:' prefix found in the prompt.\n${divider}`);
}
if (!assistantFound) {
sequenceError.push(`${divider}\nWarning: No 'Assistant: ' prefix found in the prompt.\n${divider}`);
}
if (sequence[0] && !sequence[0].startsWith('Human:')) {
sequenceError.push(`${divider}\nWarning: The messages sequence should start with 'Human:' prefix.\nMake sure you have '\\n\\nHuman:' prefix at the very beggining of the prompt, or after the system prompt.\n${divider}`);
}
if (humanErrorCount > 0 || assistantErrorCount > 0) {
sequenceError.push(`${divider}\nWarning: Detected incorrect Prefix sequence(s).`);
sequenceError.push(`Incorrect "Human:" prefix(es): ${humanErrorCount}.\nIncorrect "Assistant: " prefix(es): ${assistantErrorCount}.`);
sequenceError.push('Check the prompt above and fix it in the SillyTavern.');
sequenceError.push('\nThe correct sequence in the console should look like this:\n(System prompt msg) <-(for the sysprompt format only, else have \\n\\n above the first human\'s message.)');
sequenceError.push(`\\n + <-----(Each message beginning with the "Assistant:/Human:" prefix must have \\n\\n before it.)\n\\n +\nHuman: \\n +\n\\n +\nAssistant: \\n +\n...\n\\n +\nHuman: \\n +\n\\n +\nAssistant: \n${divider}`);
}
let use_system_prompt = (request.body.model.startsWith('claude-2') || request.body.model.startsWith('claude-3')) && request.body.claude_use_sysprompt;
let converted_prompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, use_system_prompt, request.body.human_sysprompt_message);
// Add custom stop sequences
const stopSequences = ['\n\nHuman:', '\n\nSystem:', '\n\nAssistant:'];
if (Array.isArray(request.body.stop)) {
@ -80,23 +43,21 @@ async function sendClaudeRequest(request, response) {
}
const requestBody = {
prompt: requestPrompt,
messages: converted_prompt.messages,
model: request.body.model,
max_tokens_to_sample: request.body.max_tokens,
max_tokens: request.body.max_tokens,
stop_sequences: stopSequences,
temperature: request.body.temperature,
top_p: request.body.top_p,
top_k: request.body.top_k,
stream: request.body.stream,
};
if (use_system_prompt) {
requestBody.system = converted_prompt.systemPrompt;
}
console.log('Claude request:', requestBody);
sequenceError.forEach(sequenceError => {
console.log(color.red(sequenceError));
});
const generateResponse = await fetch(apiUrl + '/complete', {
const generateResponse = await fetch(apiUrl + '/messages', {
method: 'POST',
signal: controller.signal,
body: JSON.stringify(requestBody),
@ -118,7 +79,7 @@ async function sendClaudeRequest(request, response) {
}
const generateResponseJson = await generateResponse.json();
const responseText = generateResponseJson.completion;
const responseText = generateResponseJson.content[0].text;
console.log('Claude response:', generateResponseJson);
// Wrap it back to OAI format

View File

@ -357,7 +357,7 @@ function getUuidFromUrl(url) {
const router = express.Router();
router.post('/import', jsonParser, async (request, response) => {
router.post('/importURL', jsonParser, async (request, response) => {
if (!request.body.url) {
return response.sendStatus(400);
}
@ -413,6 +413,49 @@ router.post('/import', jsonParser, async (request, response) => {
}
});
router.post('/importUUID', jsonParser, async (request, response) => {
if (!request.body.url) {
return response.sendStatus(400);
}
try {
const uuid = request.body.url;
let result;
const isJannny = uuid.includes('_character');
const isPygmalion = (!isJannny && uuid.length == 36);
const uuidType = uuid.includes('lorebook') ? 'lorebook' : 'character';
if (isPygmalion) {
console.log('Downloading Pygmalion character:', uuid);
result = await downloadPygmalionCharacter(uuid);
} else if (isJannny) {
console.log('Downloading Janitor character:', uuid.split('_')[0]);
result = await downloadJannyCharacter(uuid.split('_')[0]);
} else {
if (uuidType === 'character') {
console.log('Downloading chub character:', uuid);
result = await downloadChubCharacter(uuid);
}
else if (uuidType === 'lorebook') {
console.log('Downloading chub lorebook:', uuid);
result = await downloadChubLorebook(uuid);
}
else {
return response.sendStatus(404);
}
}
if (result.fileType) response.set('Content-Type', result.fileType);
response.set('Content-Disposition', `attachment; filename="${result.fileName}"`);
response.set('X-Custom-Content-Type', uuidType);
return response.send(result.buffer);
} catch (error) {
console.log('Importing custom content failed', error);
return response.sendStatus(500);
}
});
module.exports = {
checkForNewContent,
getDefaultPresets,

View File

@ -1,6 +1,6 @@
const fetch = require('node-fetch').default;
const express = require('express');
const AIHorde = require('../ai_horde');
const AIHorde = require('@zeldafan0225/ai_horde');
const { getVersion, delay, Cache } = require('../util');
const { readSecret, SECRET_KEYS } = require('./secrets');
const { jsonParser } = require('../express-common');

View File

@ -71,6 +71,102 @@ function convertClaudePrompt(messages, addAssistantPostfix, addAssistantPrefill,
return requestPrompt;
}
/**
* Convert ChatML objects into working with Anthropic's new Messaging API.
* @param {object[]} messages Array of messages
* @param {string} prefillString User determined prefill string
* @param {boolean} useSysPrompt See if we want to use a system prompt
* @param {string} humanMsgFix Add Human message between system prompt and assistant.
*/
function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFix) {
let systemPrompt = '';
if (useSysPrompt) {
// Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array.
let i;
for (i = 0; i < messages.length; i++) {
if (messages[i].role !== 'system') {
break;
}
systemPrompt += `${messages[i].content}\n\n`;
}
messages.splice(0, i);
// Check if the first message in the array is of type user, if not, interject with humanMsgFix or a blank message.
if (messages.length > 0 && messages[0].role !== 'user') {
messages.unshift({
role: 'user',
content: humanMsgFix || '[Start a new chat]',
});
}
}
// Now replace all further messages that have the role 'system' with the role 'user'. (or all if we're not using one)
messages.forEach((message) => {
if (message.role === 'system') {
message.role = 'user';
}
});
// Since the messaging endpoint only supports user assistant roles in turns, we have to merge messages with the same role if they follow eachother
// Also handle multi-modality, holy slop.
let mergedMessages = [];
messages.forEach((message) => {
const imageEntry = message.content[1]?.image_url;
const imageData = imageEntry?.url;
const mimeType = imageData?.split(';')[0].split(':')[1];
const base64Data = imageData?.split(',')[1];
if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role) {
if(Array.isArray(message.content)) {
if(Array.isArray(mergedMessages[mergedMessages.length - 1].content)) {
mergedMessages[mergedMessages.length - 1].content[0].text += '\n\n' + message.content[0].text;
} else {
mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content[0].text;
}
} else {
if(Array.isArray(mergedMessages[mergedMessages.length - 1].content)) {
mergedMessages[mergedMessages.length - 1].content[0].text += '\n\n' + message.content;
} else {
mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content;
}
}
} else {
mergedMessages.push(message);
}
if (imageData) {
mergedMessages[mergedMessages.length - 1].content = [
{ type: 'text', text: mergedMessages[mergedMessages.length - 1].content[0]?.text || mergedMessages[mergedMessages.length - 1].content },
{
type: 'image', source: {
type: 'base64',
media_type: mimeType,
data: base64Data,
},
},
];
}
});
// Take care of name properties since claude messages don't support them
mergedMessages.forEach((message) => {
if (message.name) {
message.content = `${message.name}: ${message.content}`;
delete message.name;
}
});
// Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling
if (prefillString) {
mergedMessages.push({
role: 'assistant',
content: prefillString,
});
}
return { messages: mergedMessages, systemPrompt: systemPrompt.trim() };
}
/**
* Convert a prompt from the ChatML objects to the format used by Google MakerSuite models.
* @param {object[]} messages Array of messages
@ -160,6 +256,7 @@ function convertTextCompletionPrompt(messages) {
module.exports = {
convertClaudePrompt,
convertClaudeMessages,
convertGooglePrompt,
convertTextCompletionPrompt,
};

View File

@ -249,6 +249,7 @@ async function loadClaudeTokenizer(modelPath) {
}
function countClaudeTokens(tokenizer, messages) {
// Should be fine if we use the old conversion method instead of the messages API one i think?
const convertedPrompt = convertClaudePrompt(messages, false, false, false);
// Fallback to strlen estimation