mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'staging' into tooltips-vol1
This commit is contained in:
@@ -3448,6 +3448,10 @@
|
|||||||
<span class="fa-solid fa-circle-question note-link-span"></span>
|
<span class="fa-solid fa-circle-question note-link-span"></span>
|
||||||
</a>
|
</a>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="checkbox_label" for="forbid_external_images" title="Disalow embedded images from other domains in chat messages.">
|
||||||
|
<input id="forbid_external_images" type="checkbox" />
|
||||||
|
<span data-i18n="Forbid External Images">Forbid External Images</span>
|
||||||
|
</label>
|
||||||
<label data-newbie-hidden class="checkbox_label" for="allow_name2_display">
|
<label data-newbie-hidden class="checkbox_label" for="allow_name2_display">
|
||||||
<input id="allow_name2_display" type="checkbox" />
|
<input id="allow_name2_display" type="checkbox" />
|
||||||
<span data-i18n="Allow {{char}}: in bot messages">Show {{char}}: in responses</span>
|
<span data-i18n="Allow {{char}}: in bot messages">Show {{char}}: in responses</span>
|
||||||
@@ -4388,7 +4392,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="world_entry_form_control">
|
<div class="world_entry_form_control">
|
||||||
<small class="textAlignCenter">Logic</small>
|
<small class="textAlignCenter">Logic</small>
|
||||||
<select name="entryLogicType" class="widthFitContent margin0">
|
<select name="entryLogicType" class="text_pole widthFitContent margin0">
|
||||||
<option value="0">AND ANY</option>
|
<option value="0">AND ANY</option>
|
||||||
<option value="3">AND ALL</option>
|
<option value="3">AND ALL</option>
|
||||||
<option value="1">NOT ALL</option>
|
<option value="1">NOT ALL</option>
|
||||||
@@ -4407,6 +4411,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div name="perEntryOverridesBlock" class="flex-container wide100p alignitemscenter">
|
||||||
|
<div class="world_entry_form_control flex1">
|
||||||
|
<small class="textAlignCenter">Scan Depth</small>
|
||||||
|
<input class="text_pole" name="scanDepth" type="number" placeholder="Use global setting" max="100">
|
||||||
|
</div>
|
||||||
|
<div class="world_entry_form_control flex1">
|
||||||
|
<small class="textAlignCenter">Case-Sensitive</small>
|
||||||
|
<select name="caseSensitive" class="text_pole widthNatural margin0">
|
||||||
|
<option value="null">Use global setting</option>
|
||||||
|
<option value="true">Yes</option>
|
||||||
|
<option value="false">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="world_entry_form_control flex1">
|
||||||
|
<small class="textAlignCenter">Match Whole Words</small>
|
||||||
|
<select name="matchWholeWords" class="text_pole widthNatural margin0">
|
||||||
|
<option value="null">Use global setting</option>
|
||||||
|
<option value="true">Yes</option>
|
||||||
|
<option value="false">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div name="contentAndCharFilterBlock" class="world_entry_thin_controls flex2">
|
<div name="contentAndCharFilterBlock" class="world_entry_thin_controls flex2">
|
||||||
<div class="world_entry_form_control flex1">
|
<div class="world_entry_form_control flex1">
|
||||||
<label for="content ">
|
<label for="content ">
|
||||||
|
@@ -296,6 +296,25 @@ DOMPurify.addHook('uponSanitizeAttribute', (_, data, config) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
|
||||||
|
if (!config['MESSAGE_SANITIZE']) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (node.tagName) {
|
||||||
|
case 'IMG': {
|
||||||
|
const isExternalUrl = (url) => (url.indexOf('://') > 0 || url.indexOf('//') === 0) && !url.startsWith(window.location.origin);
|
||||||
|
const src = node.getAttribute('src');
|
||||||
|
|
||||||
|
if (power_user.forbid_external_images && isExternalUrl(src)) {
|
||||||
|
console.warn('External image blocked', src);
|
||||||
|
node.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// API OBJECT FOR EXTERNAL WIRING
|
// API OBJECT FOR EXTERNAL WIRING
|
||||||
window['SillyTavern'] = {};
|
window['SillyTavern'] = {};
|
||||||
|
|
||||||
@@ -1447,6 +1466,7 @@ async function printMessages() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function clearChat() {
|
async function clearChat() {
|
||||||
|
closeMessageEditor();
|
||||||
count_view_mes = 0;
|
count_view_mes = 0;
|
||||||
extension_prompts = {};
|
extension_prompts = {};
|
||||||
if (is_delete_mode) {
|
if (is_delete_mode) {
|
||||||
|
@@ -76,7 +76,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-container">
|
<div class="flex-container">
|
||||||
<div class="wi-enter-footer-text flex-container flexFlowColumn flexNoGap alignitemsstart">
|
<div class="flex1 wi-enter-footer-text flex-container flexFlowColumn flexNoGap alignitemsstart">
|
||||||
<small>Affects</small>
|
<small>Affects</small>
|
||||||
<div>
|
<div>
|
||||||
<label class="checkbox flex-container">
|
<label class="checkbox flex-container">
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="wi-enter-footer-text flex-container flexFlowColumn flexNoGap alignitemsstart">
|
<div class="flex1 wi-enter-footer-text flex-container flexFlowColumn flexNoGap alignitemsstart">
|
||||||
<small>Other Options</small>
|
<small>Other Options</small>
|
||||||
<label class="checkbox flex-container">
|
<label class="checkbox flex-container">
|
||||||
<input type="checkbox" name="disabled" />
|
<input type="checkbox" name="disabled" />
|
||||||
@@ -120,13 +120,6 @@
|
|||||||
<span data-i18n="Substitute Regex">Substitute Regex (?)</span>
|
<span data-i18n="Substitute Regex">Substitute Regex (?)</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-container flexFlowColumn alignitemsstart">
|
|
||||||
<small>Replacement Strategy</small>
|
|
||||||
<select name="replace_strategy_select" class="margin0">
|
|
||||||
<option value="0">Replace</option>
|
|
||||||
<option value="1">Overlay (currently broken)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -19,14 +19,6 @@ const regex_placement = {
|
|||||||
SLASH_COMMAND: 3,
|
SLASH_COMMAND: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @enum {number} How the regex script should replace the matched string
|
|
||||||
*/
|
|
||||||
const regex_replace_strategy = {
|
|
||||||
REPLACE: 0,
|
|
||||||
OVERLAY: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a regular expression from a string.
|
* Instantiates a regular expression from a string.
|
||||||
* @param {string} input The input string.
|
* @param {string} input The input string.
|
||||||
@@ -153,86 +145,3 @@ function filterString(rawString, trimStrings, { characterOverride } = {}) {
|
|||||||
|
|
||||||
return finalString;
|
return finalString;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Substitutes regex-specific and normal parameters
|
|
||||||
* @param {string} rawString
|
|
||||||
* @param {string} regexMatch
|
|
||||||
* @param {RegexSubstituteParams} params The parameters to use for the regex substitution
|
|
||||||
* @returns {string} The substituted string
|
|
||||||
* @typedef {{characterOverride?: string, replaceStrategy?: number}} RegexSubstituteParams The parameters to use for the regex substitution
|
|
||||||
*/
|
|
||||||
function substituteRegexParams(rawString, regexMatch, { characterOverride, replaceStrategy } = {}) {
|
|
||||||
let finalString = rawString;
|
|
||||||
finalString = substituteParams(finalString, undefined, characterOverride);
|
|
||||||
|
|
||||||
let overlaidMatch = regexMatch;
|
|
||||||
// TODO: Maybe move the for loops into a separate function?
|
|
||||||
if (replaceStrategy === regex_replace_strategy.OVERLAY) {
|
|
||||||
const splitReplace = finalString.split('{{match}}');
|
|
||||||
|
|
||||||
// There's a prefix
|
|
||||||
if (splitReplace[0]) {
|
|
||||||
// Fetch the prefix
|
|
||||||
const splicedPrefix = spliceSymbols(splitReplace[0], false);
|
|
||||||
|
|
||||||
// Sequentially remove all occurrences of prefix from start of split
|
|
||||||
const splitMatch = overlaidMatch.split(splicedPrefix);
|
|
||||||
let sliceNum = 0;
|
|
||||||
for (let index = 0; index < splitMatch.length; index++) {
|
|
||||||
if (splitMatch[index].length === 0) {
|
|
||||||
sliceNum++;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
overlaidMatch = splitMatch.slice(sliceNum, splitMatch.length).join(splicedPrefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
// There's a suffix
|
|
||||||
if (splitReplace[1]) {
|
|
||||||
// Fetch the suffix
|
|
||||||
const splicedSuffix = spliceSymbols(splitReplace[1], true);
|
|
||||||
|
|
||||||
// Sequential removal of all suffix occurrences from end of split
|
|
||||||
const splitMatch = overlaidMatch.split(splicedSuffix);
|
|
||||||
let sliceNum = 0;
|
|
||||||
for (let index = splitMatch.length - 1; index >= 0; index--) {
|
|
||||||
if (splitMatch[index].length === 0) {
|
|
||||||
sliceNum++;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
overlaidMatch = splitMatch.slice(0, splitMatch.length - sliceNum).join(splicedSuffix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only one match is replaced. This is by design
|
|
||||||
finalString = finalString.replace('{{match}}', overlaidMatch) || finalString.replace('{{match}}', regexMatch);
|
|
||||||
|
|
||||||
return finalString;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Splices common sentence symbols and whitespace from the beginning and end of a string.
|
|
||||||
* Using a for loop due to sequential ordering.
|
|
||||||
* @param {string} rawString The raw string to splice
|
|
||||||
* @param {boolean} isSuffix String is a suffix
|
|
||||||
* @returns {string} The spliced string
|
|
||||||
*/
|
|
||||||
function spliceSymbols(rawString, isSuffix) {
|
|
||||||
let offset = 0;
|
|
||||||
|
|
||||||
for (const ch of isSuffix ? rawString.split('').reverse() : rawString) {
|
|
||||||
if (ch.match(/[^\w.,?'!]/)) {
|
|
||||||
offset++;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return isSuffix ? rawString.substring(0, rawString.length - offset) : rawString.substring(offset);
|
|
||||||
}
|
|
||||||
|
@@ -141,9 +141,6 @@ async function onRegexEditorOpenClick(existingId) {
|
|||||||
editorHtml
|
editorHtml
|
||||||
.find('input[name="substitute_regex"]')
|
.find('input[name="substitute_regex"]')
|
||||||
.prop('checked', existingScript.substituteRegex ?? false);
|
.prop('checked', existingScript.substituteRegex ?? false);
|
||||||
editorHtml
|
|
||||||
.find('select[name="replace_strategy_select"]')
|
|
||||||
.val(existingScript.replaceStrategy ?? 0);
|
|
||||||
|
|
||||||
existingScript.placement.forEach((element) => {
|
existingScript.placement.forEach((element) => {
|
||||||
editorHtml
|
editorHtml
|
||||||
@@ -181,7 +178,6 @@ async function onRegexEditorOpenClick(existingId) {
|
|||||||
replaceString: editorHtml.find('.regex_replace_string').val(),
|
replaceString: editorHtml.find('.regex_replace_string').val(),
|
||||||
trimStrings: String(editorHtml.find('.regex_trim_strings').val()).split('\n').filter((e) => e.length !== 0) || [],
|
trimStrings: String(editorHtml.find('.regex_trim_strings').val()).split('\n').filter((e) => e.length !== 0) || [],
|
||||||
substituteRegex: editorHtml.find('input[name="substitute_regex"]').prop('checked'),
|
substituteRegex: editorHtml.find('input[name="substitute_regex"]').prop('checked'),
|
||||||
replaceStrategy: Number(editorHtml.find('select[name="replace_strategy_select"]').find(':selected').val()) ?? 0,
|
|
||||||
};
|
};
|
||||||
const rawTestString = String(editorHtml.find('#regex_test_input').val());
|
const rawTestString = String(editorHtml.find('#regex_test_input').val());
|
||||||
const result = runRegexScript(testScript, rawTestString);
|
const result = runRegexScript(testScript, rawTestString);
|
||||||
@@ -224,11 +220,6 @@ async function onRegexEditorOpenClick(existingId) {
|
|||||||
editorHtml
|
editorHtml
|
||||||
.find('input[name="substitute_regex"]')
|
.find('input[name="substitute_regex"]')
|
||||||
.prop('checked'),
|
.prop('checked'),
|
||||||
replaceStrategy:
|
|
||||||
parseInt(editorHtml
|
|
||||||
.find('select[name="replace_strategy_select"]')
|
|
||||||
.find(':selected')
|
|
||||||
.val()) ?? 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
saveRegexScript(newRegexScript, existingScriptIndex);
|
saveRegexScript(newRegexScript, existingScriptIndex);
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { eventSource, event_types, extension_prompt_types, getCurrentChatId, getRequestHeaders, is_send_press, saveSettingsDebounced, setExtensionPrompt, substituteParams } from '../../../script.js';
|
import { eventSource, event_types, extension_prompt_types, getCurrentChatId, getRequestHeaders, is_send_press, saveSettingsDebounced, setExtensionPrompt, substituteParams } from '../../../script.js';
|
||||||
import { ModuleWorkerWrapper, extension_settings, getContext, renderExtensionTemplate } from '../../extensions.js';
|
import { ModuleWorkerWrapper, extension_settings, getContext, modules, renderExtensionTemplate } from '../../extensions.js';
|
||||||
import { collapseNewlines } from '../../power-user.js';
|
import { collapseNewlines } from '../../power-user.js';
|
||||||
import { SECRET_KEYS, secret_state } from '../../secrets.js';
|
import { SECRET_KEYS, secret_state } from '../../secrets.js';
|
||||||
import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive } from '../../utils.js';
|
import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive } from '../../utils.js';
|
||||||
@@ -152,8 +152,25 @@ async function synchronizeChat(batchSize = 5) {
|
|||||||
|
|
||||||
return newVectorItems.length - batchSize;
|
return newVectorItems.length - batchSize;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
/**
|
||||||
|
* Gets the error message for a given cause
|
||||||
|
* @param {string} cause Error cause key
|
||||||
|
* @returns {string} Error message
|
||||||
|
*/
|
||||||
|
function getErrorMessage(cause) {
|
||||||
|
switch (cause) {
|
||||||
|
case 'api_key_missing':
|
||||||
|
return 'API key missing. Save it in the "API Connections" panel.';
|
||||||
|
case 'extras_module_missing':
|
||||||
|
return 'Extras API must provide an "embeddings" module.';
|
||||||
|
default:
|
||||||
|
return 'Check server console for more details';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.error('Vectors: Failed to synchronize chat', error);
|
console.error('Vectors: Failed to synchronize chat', error);
|
||||||
const message = error.cause === 'api_key_missing' ? 'API key missing. Save it in the "API Connections" panel.' : 'Check server console for more details';
|
|
||||||
|
const message = getErrorMessage(error.cause);
|
||||||
toastr.error(message, 'Vectorization failed');
|
toastr.error(message, 'Vectorization failed');
|
||||||
return -1;
|
return -1;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -411,6 +428,18 @@ async function getSavedHashes(collectionId) {
|
|||||||
return hashes;
|
return hashes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add headers for the Extras API source.
|
||||||
|
* @param {object} headers Headers object
|
||||||
|
*/
|
||||||
|
function addExtrasHeaders(headers) {
|
||||||
|
console.log(`Vector source is extras, populating API URL: ${extension_settings.apiUrl}`);
|
||||||
|
Object.assign(headers, {
|
||||||
|
'X-Extras-Url': extension_settings.apiUrl,
|
||||||
|
'X-Extras-Key': extension_settings.apiKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts vector items into a collection
|
* Inserts vector items into a collection
|
||||||
* @param {string} collectionId - The collection to insert into
|
* @param {string} collectionId - The collection to insert into
|
||||||
@@ -424,9 +453,18 @@ async function insertVectorItems(collectionId, items) {
|
|||||||
throw new Error('Vectors: API key missing', { cause: 'api_key_missing' });
|
throw new Error('Vectors: API key missing', { cause: 'api_key_missing' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settings.source === 'extras' && !modules.includes('embeddings')) {
|
||||||
|
throw new Error('Vectors: Embeddings module missing', { cause: 'extras_module_missing' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = getRequestHeaders();
|
||||||
|
if (settings.source === 'extras') {
|
||||||
|
addExtrasHeaders(headers);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/vector/insert', {
|
const response = await fetch('/api/vector/insert', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: getRequestHeaders(),
|
headers: headers,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
collectionId: collectionId,
|
collectionId: collectionId,
|
||||||
items: items,
|
items: items,
|
||||||
@@ -468,9 +506,14 @@ async function deleteVectorItems(collectionId, hashes) {
|
|||||||
* @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results
|
* @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results
|
||||||
*/
|
*/
|
||||||
async function queryCollection(collectionId, searchText, topK) {
|
async function queryCollection(collectionId, searchText, topK) {
|
||||||
|
const headers = getRequestHeaders();
|
||||||
|
if (settings.source === 'extras') {
|
||||||
|
addExtrasHeaders(headers);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/vector/query', {
|
const response = await fetch('/api/vector/query', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: getRequestHeaders(),
|
headers: headers,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
collectionId: collectionId,
|
collectionId: collectionId,
|
||||||
searchText: searchText,
|
searchText: searchText,
|
||||||
|
@@ -11,6 +11,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<select id="vectors_source" class="text_pole">
|
<select id="vectors_source" class="text_pole">
|
||||||
<option value="transformers">Local (Transformers)</option>
|
<option value="transformers">Local (Transformers)</option>
|
||||||
|
<option value="extras">Extras</option>
|
||||||
<option value="openai">OpenAI</option>
|
<option value="openai">OpenAI</option>
|
||||||
<option value="palm">Google MakerSuite (PaLM)</option>
|
<option value="palm">Google MakerSuite (PaLM)</option>
|
||||||
<option value="mistral">MistralAI</option>
|
<option value="mistral">MistralAI</option>
|
||||||
|
@@ -1160,6 +1160,7 @@ function tryParseStreamingError(response, decoded) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkQuotaError(data);
|
checkQuotaError(data);
|
||||||
|
checkModerationError(data);
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
toastr.error(data.error.message || response.statusText, 'Chat Completion API');
|
toastr.error(data.error.message || response.statusText, 'Chat Completion API');
|
||||||
@@ -1187,6 +1188,15 @@ function checkQuotaError(data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkModerationError(data) {
|
||||||
|
const moderationError = data?.error?.message?.includes('requires moderation');
|
||||||
|
if (moderationError) {
|
||||||
|
const moderationReason = `Reasons: ${data?.error?.metadata?.reasons?.join(', ') ?? '(N/A)'}`;
|
||||||
|
const flaggedText = data?.error?.metadata?.flagged_input ?? '(N/A)';
|
||||||
|
toastr.info(flaggedText, moderationReason, { timeOut: 10000 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function sendWindowAIRequest(messages, signal, stream) {
|
async function sendWindowAIRequest(messages, signal, stream) {
|
||||||
if (!('ai' in window)) {
|
if (!('ai' in window)) {
|
||||||
return showWindowExtensionError();
|
return showWindowExtensionError();
|
||||||
@@ -1688,6 +1698,7 @@ async function sendOpenAIRequest(type, messages, signal) {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
checkQuotaError(data);
|
checkQuotaError(data);
|
||||||
|
checkModerationError(data);
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
toastr.error(data.error.message || response.statusText, 'API returned an error');
|
toastr.error(data.error.message || response.statusText, 'API returned an error');
|
||||||
|
@@ -237,6 +237,7 @@ let power_user = {
|
|||||||
compact_input_area: true,
|
compact_input_area: true,
|
||||||
auto_connect: false,
|
auto_connect: false,
|
||||||
auto_load_chat: false,
|
auto_load_chat: false,
|
||||||
|
forbid_external_images: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let themes = [];
|
let themes = [];
|
||||||
@@ -1529,6 +1530,7 @@ function loadPowerUserSettings(settings, data) {
|
|||||||
$('#reduced_motion').prop('checked', power_user.reduced_motion);
|
$('#reduced_motion').prop('checked', power_user.reduced_motion);
|
||||||
$('#auto-connect-checkbox').prop('checked', power_user.auto_connect);
|
$('#auto-connect-checkbox').prop('checked', power_user.auto_connect);
|
||||||
$('#auto-load-chat-checkbox').prop('checked', power_user.auto_load_chat);
|
$('#auto-load-chat-checkbox').prop('checked', power_user.auto_load_chat);
|
||||||
|
$('#forbid_external_images').prop('checked', power_user.forbid_external_images);
|
||||||
|
|
||||||
for (const theme of themes) {
|
for (const theme of themes) {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
@@ -3234,6 +3236,12 @@ $(document).ready(() => {
|
|||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#forbid_external_images').on('input', function () {
|
||||||
|
power_user.forbid_external_images = !!$(this).prop('checked');
|
||||||
|
saveSettingsDebounced();
|
||||||
|
reloadCurrentChat();
|
||||||
|
});
|
||||||
|
|
||||||
$(document).on('click', '#debug_table [data-debug-function]', function () {
|
$(document).on('click', '#debug_table [data-debug-function]', function () {
|
||||||
const functionId = $(this).data('debug-function');
|
const functionId = $(this).data('debug-function');
|
||||||
const functionRecord = debug_functions.find(f => f.functionId === functionId);
|
const functionRecord = debug_functions.find(f => f.functionId === functionId);
|
||||||
|
@@ -664,7 +664,7 @@ function randValuesCallback(from, to, args) {
|
|||||||
if (args.round == 'floor') {
|
if (args.round == 'floor') {
|
||||||
return Math.floor(value);
|
return Math.floor(value);
|
||||||
}
|
}
|
||||||
return value;
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerVariableCommands() {
|
export function registerVariableCommands() {
|
||||||
|
@@ -70,6 +70,135 @@ const SORT_ORDER_KEY = 'world_info_sort_order';
|
|||||||
const METADATA_KEY = 'world_info';
|
const METADATA_KEY = 'world_info';
|
||||||
|
|
||||||
const DEFAULT_DEPTH = 4;
|
const DEFAULT_DEPTH = 4;
|
||||||
|
const MAX_SCAN_DEPTH = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a scanning buffer for one evaluation of World Info.
|
||||||
|
*/
|
||||||
|
class WorldInfoBuffer {
|
||||||
|
// Typedef area
|
||||||
|
/** @typedef {{scanDepth?: number, caseSensitive?: boolean, matchWholeWords?: boolean}} WIScanEntry The entry that triggered the scan */
|
||||||
|
// End typedef area
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string[]} Array of messages sorted by ascending depth
|
||||||
|
*/
|
||||||
|
#depthBuffer = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string[]} Array of strings added by recursive scanning
|
||||||
|
*/
|
||||||
|
#recurseBuffer = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {number} The skew of the global scan depth. Used in "min activations"
|
||||||
|
*/
|
||||||
|
#skew = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the buffer with the given messages.
|
||||||
|
* @param {string[]} messages Array of messages to add to the buffer
|
||||||
|
*/
|
||||||
|
constructor(messages) {
|
||||||
|
this.#initDepthBuffer(messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates the buffer with the given messages.
|
||||||
|
* @param {string[]} messages Array of messages to add to the buffer
|
||||||
|
* @returns {void} Hardly seen nothing down here
|
||||||
|
*/
|
||||||
|
#initDepthBuffer(messages) {
|
||||||
|
for (let depth = 0; depth < MAX_SCAN_DEPTH; depth++) {
|
||||||
|
if (messages[depth]) {
|
||||||
|
this.#depthBuffer[depth] = messages[depth].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a string that respects the case sensitivity setting
|
||||||
|
* @param {string} str The string to transform
|
||||||
|
* @param {WIScanEntry} entry The entry that triggered the scan
|
||||||
|
* @returns {string} The transformed string
|
||||||
|
*/
|
||||||
|
#transformString(str, entry) {
|
||||||
|
const caseSensitive = entry.caseSensitive ?? world_info_case_sensitive;
|
||||||
|
return caseSensitive ? str : str.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all messages up to the given depth + recursion buffer.
|
||||||
|
* @param {WIScanEntry} entry The entry that triggered the scan
|
||||||
|
* @returns {string} A slice of buffer until the given depth (inclusive)
|
||||||
|
*/
|
||||||
|
get(entry) {
|
||||||
|
let depth = entry.scanDepth ?? (world_info_depth + this.#skew);
|
||||||
|
|
||||||
|
if (depth < 0) {
|
||||||
|
console.error(`Invalid WI scan depth ${depth}. Must be >= 0`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth > MAX_SCAN_DEPTH) {
|
||||||
|
console.warn(`Invalid WI scan depth ${depth}. Truncating to ${MAX_SCAN_DEPTH}`);
|
||||||
|
depth = MAX_SCAN_DEPTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = this.#depthBuffer.slice(0, depth).join('\n');
|
||||||
|
|
||||||
|
if (this.#recurseBuffer.length > 0) {
|
||||||
|
result += '\n' + this.#recurseBuffer.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.#transformString(result, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches the given string against the buffer.
|
||||||
|
* @param {string} haystack The string to search in
|
||||||
|
* @param {string} needle The string to search for
|
||||||
|
* @param {WIScanEntry} entry The entry that triggered the scan
|
||||||
|
* @returns {boolean} True if the string was found in the buffer
|
||||||
|
*/
|
||||||
|
matchKeys(haystack, needle, entry) {
|
||||||
|
const transformedString = this.#transformString(needle, entry);
|
||||||
|
const matchWholeWords = entry.matchWholeWords ?? world_info_match_whole_words;
|
||||||
|
|
||||||
|
if (matchWholeWords) {
|
||||||
|
const keyWords = transformedString.split(/\s+/);
|
||||||
|
|
||||||
|
if (keyWords.length > 1) {
|
||||||
|
return haystack.includes(transformedString);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const regex = new RegExp(`\\b${escapeRegex(transformedString)}\\b`);
|
||||||
|
if (regex.test(haystack)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return haystack.includes(transformedString);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a message to the recursion buffer.
|
||||||
|
* @param {string} message The message to add
|
||||||
|
*/
|
||||||
|
addRecurse(message) {
|
||||||
|
this.#recurseBuffer.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an increment to depth skew.
|
||||||
|
*/
|
||||||
|
addSkew() {
|
||||||
|
this.#skew++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getWorldInfoSettings() {
|
export function getWorldInfoSettings() {
|
||||||
return {
|
return {
|
||||||
@@ -790,6 +919,9 @@ const originalDataKeyMap = {
|
|||||||
'key': 'keys',
|
'key': 'keys',
|
||||||
'keysecondary': 'secondary_keys',
|
'keysecondary': 'secondary_keys',
|
||||||
'selective': 'selective',
|
'selective': 'selective',
|
||||||
|
'matchWholeWords': 'extensions.match_whole_words',
|
||||||
|
'caseSensitive': 'extensions.case_sensitive',
|
||||||
|
'scanDepth': 'extensions.scan_depth',
|
||||||
};
|
};
|
||||||
|
|
||||||
function setOriginalDataValue(data, uid, key, value) {
|
function setOriginalDataValue(data, uid, key, value) {
|
||||||
@@ -1167,7 +1299,7 @@ function getWorldEntry(name, data, entry) {
|
|||||||
probabilityInput.data('uid', entry.uid);
|
probabilityInput.data('uid', entry.uid);
|
||||||
probabilityInput.on('input', function () {
|
probabilityInput.on('input', function () {
|
||||||
const uid = $(this).data('uid');
|
const uid = $(this).data('uid');
|
||||||
const value = parseInt($(this).val());
|
const value = Number($(this).val());
|
||||||
|
|
||||||
data.entries[uid].probability = !isNaN(value) ? value : null;
|
data.entries[uid].probability = !isNaN(value) ? value : null;
|
||||||
|
|
||||||
@@ -1370,6 +1502,57 @@ function getWorldEntry(name, data, entry) {
|
|||||||
updateEditor(navigation_option.previous);
|
updateEditor(navigation_option.previous);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// scan depth
|
||||||
|
const scanDepthInput = template.find('input[name="scanDepth"]');
|
||||||
|
scanDepthInput.data('uid', entry.uid);
|
||||||
|
scanDepthInput.on('input', function () {
|
||||||
|
const uid = $(this).data('uid');
|
||||||
|
const isEmpty = $(this).val() === '';
|
||||||
|
const value = Number($(this).val());
|
||||||
|
|
||||||
|
// Clamp if necessary
|
||||||
|
if (value < 0) {
|
||||||
|
$(this).val(0).trigger('input');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value > MAX_SCAN_DEPTH) {
|
||||||
|
$(this).val(MAX_SCAN_DEPTH).trigger('input');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.entries[uid].scanDepth = !isEmpty && !isNaN(value) && value >= 0 && value < MAX_SCAN_DEPTH ? Math.floor(value) : null;
|
||||||
|
setOriginalDataValue(data, uid, 'extensions.scan_depth', data.entries[uid].scanDepth);
|
||||||
|
saveWorldInfo(name, data);
|
||||||
|
});
|
||||||
|
scanDepthInput.val(entry.scanDepth ?? null).trigger('input');
|
||||||
|
|
||||||
|
// case sensitive select
|
||||||
|
const caseSensitiveSelect = template.find('select[name="caseSensitive"]');
|
||||||
|
caseSensitiveSelect.data('uid', entry.uid);
|
||||||
|
caseSensitiveSelect.on('input', function () {
|
||||||
|
const uid = $(this).data('uid');
|
||||||
|
const value = $(this).val();
|
||||||
|
|
||||||
|
data.entries[uid].caseSensitive = value === 'null' ? null : value === 'true';
|
||||||
|
setOriginalDataValue(data, uid, 'extensions.case_sensitive', data.entries[uid].caseSensitive);
|
||||||
|
saveWorldInfo(name, data);
|
||||||
|
});
|
||||||
|
caseSensitiveSelect.val((entry.caseSensitive === null || entry.caseSensitive === undefined) ? 'null' : entry.caseSensitive ? 'true' : 'false').trigger('input');
|
||||||
|
|
||||||
|
// match whole words select
|
||||||
|
const matchWholeWordsSelect = template.find('select[name="matchWholeWords"]');
|
||||||
|
matchWholeWordsSelect.data('uid', entry.uid);
|
||||||
|
matchWholeWordsSelect.on('input', function () {
|
||||||
|
const uid = $(this).data('uid');
|
||||||
|
const value = $(this).val();
|
||||||
|
|
||||||
|
data.entries[uid].matchWholeWords = value === 'null' ? null : value === 'true';
|
||||||
|
setOriginalDataValue(data, uid, 'extensions.match_whole_words', data.entries[uid].matchWholeWords);
|
||||||
|
saveWorldInfo(name, data);
|
||||||
|
});
|
||||||
|
matchWholeWordsSelect.val((entry.matchWholeWords === null || entry.matchWholeWords === undefined) ? 'null' : entry.matchWholeWords ? 'true' : 'false').trigger('input');
|
||||||
|
|
||||||
template.find('.inline-drawer-content').css('display', 'none'); //entries start collapsed
|
template.find('.inline-drawer-content').css('display', 'none'); //entries start collapsed
|
||||||
|
|
||||||
function updatePosOrdDisplay(uid) {
|
function updatePosOrdDisplay(uid) {
|
||||||
@@ -1428,6 +1611,9 @@ const newEntryTemplate = {
|
|||||||
useProbability: true,
|
useProbability: true,
|
||||||
depth: DEFAULT_DEPTH,
|
depth: DEFAULT_DEPTH,
|
||||||
group: '',
|
group: '',
|
||||||
|
scanDepth: null,
|
||||||
|
caseSensitive: null,
|
||||||
|
matchWholeWords: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function createWorldInfoEntry(name, data, fromSlashCommand = false) {
|
function createWorldInfoEntry(name, data, fromSlashCommand = false) {
|
||||||
@@ -1585,11 +1771,6 @@ async function createNewWorldInfo(worldInfoName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets a string that respects the case sensitivity setting
|
|
||||||
function transformString(str) {
|
|
||||||
return world_info_case_sensitive ? str : str.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCharacterLore() {
|
async function getCharacterLore() {
|
||||||
const character = characters[this_chid];
|
const character = characters[this_chid];
|
||||||
const name = character?.name;
|
const name = character?.name;
|
||||||
@@ -1711,11 +1892,10 @@ async function getSortedEntries() {
|
|||||||
|
|
||||||
async function checkWorldInfo(chat, maxContext) {
|
async function checkWorldInfo(chat, maxContext) {
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
const messagesToLookBack = world_info_depth * 2 || 1;
|
const buffer = new WorldInfoBuffer(chat);
|
||||||
|
|
||||||
// Combine the chat
|
// Combine the chat
|
||||||
let textToScan = chat.slice(0, messagesToLookBack).join('');
|
let minActivationMsgIndex = world_info_depth; // tracks chat index to satisfy `world_info_min_activations`
|
||||||
let minActivationMsgIndex = messagesToLookBack; // tracks chat index to satisfy `world_info_min_activations`
|
|
||||||
|
|
||||||
// Add the depth or AN if enabled
|
// Add the depth or AN if enabled
|
||||||
// Put this code here since otherwise, the chat reference is modified
|
// Put this code here since otherwise, the chat reference is modified
|
||||||
@@ -1723,14 +1903,11 @@ async function checkWorldInfo(chat, maxContext) {
|
|||||||
if (context.extensionPrompts[key]?.scan) {
|
if (context.extensionPrompts[key]?.scan) {
|
||||||
const prompt = getExtensionPromptByName(key);
|
const prompt = getExtensionPromptByName(key);
|
||||||
if (prompt) {
|
if (prompt) {
|
||||||
textToScan = `${prompt}\n${textToScan}`;
|
buffer.addRecurse(prompt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform the resulting string
|
|
||||||
textToScan = transformString(textToScan);
|
|
||||||
|
|
||||||
let needsToScan = true;
|
let needsToScan = true;
|
||||||
let token_budget_overflowed = false;
|
let token_budget_overflowed = false;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -1809,10 +1986,11 @@ async function checkWorldInfo(chat, maxContext) {
|
|||||||
|
|
||||||
primary: for (let key of entry.key) {
|
primary: for (let key of entry.key) {
|
||||||
const substituted = substituteParams(key);
|
const substituted = substituteParams(key);
|
||||||
|
const textToScan = buffer.get(entry);
|
||||||
|
|
||||||
console.debug(`${entry.uid}: ${substituted}`);
|
console.debug(`${entry.uid}: ${substituted}`);
|
||||||
|
|
||||||
if (substituted && matchKeys(textToScan, substituted.trim())) {
|
if (substituted && buffer.matchKeys(textToScan, substituted.trim(), entry)) {
|
||||||
console.debug(`WI UID ${entry.uid} found by primary match: ${substituted}.`);
|
console.debug(`WI UID ${entry.uid} found by primary match: ${substituted}.`);
|
||||||
|
|
||||||
//selective logic begins
|
//selective logic begins
|
||||||
@@ -1826,7 +2004,7 @@ async function checkWorldInfo(chat, maxContext) {
|
|||||||
let hasAllMatch = true;
|
let hasAllMatch = true;
|
||||||
secondary: for (let keysecondary of entry.keysecondary) {
|
secondary: for (let keysecondary of entry.keysecondary) {
|
||||||
const secondarySubstituted = substituteParams(keysecondary);
|
const secondarySubstituted = substituteParams(keysecondary);
|
||||||
const hasSecondaryMatch = secondarySubstituted && matchKeys(textToScan, secondarySubstituted.trim());
|
const hasSecondaryMatch = secondarySubstituted && buffer.matchKeys(textToScan, secondarySubstituted.trim(), entry);
|
||||||
console.debug(`WI UID:${entry.uid}: Filtering for secondary keyword - "${secondarySubstituted}".`);
|
console.debug(`WI UID:${entry.uid}: Filtering for secondary keyword - "${secondarySubstituted}".`);
|
||||||
|
|
||||||
if (hasSecondaryMatch) {
|
if (hasSecondaryMatch) {
|
||||||
@@ -1926,9 +2104,8 @@ async function checkWorldInfo(chat, maxContext) {
|
|||||||
.filter(x => !failedProbabilityChecks.has(x))
|
.filter(x => !failedProbabilityChecks.has(x))
|
||||||
.filter(x => !x.preventRecursion)
|
.filter(x => !x.preventRecursion)
|
||||||
.map(x => x.content).join('\n');
|
.map(x => x.content).join('\n');
|
||||||
const currentlyActivatedText = transformString(text);
|
buffer.addRecurse(text);
|
||||||
textToScan = (currentlyActivatedText + '\n' + textToScan);
|
allActivatedText = (text + '\n' + allActivatedText);
|
||||||
allActivatedText = (currentlyActivatedText + '\n' + allActivatedText);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// world_info_min_activations
|
// world_info_min_activations
|
||||||
@@ -1941,8 +2118,8 @@ async function checkWorldInfo(chat, maxContext) {
|
|||||||
) || (minActivationMsgIndex >= chat.length);
|
) || (minActivationMsgIndex >= chat.length);
|
||||||
if (!over_max) {
|
if (!over_max) {
|
||||||
needsToScan = true;
|
needsToScan = true;
|
||||||
textToScan = transformString(chat.slice(minActivationMsgIndex, minActivationMsgIndex + 1).join(''));
|
|
||||||
minActivationMsgIndex += 1;
|
minActivationMsgIndex += 1;
|
||||||
|
buffer.addSkew();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2069,29 +2246,6 @@ function filterByInclusionGroups(newEntries, allActivatedEntries) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchKeys(haystack, needle) {
|
|
||||||
const transformedString = transformString(needle);
|
|
||||||
|
|
||||||
if (world_info_match_whole_words) {
|
|
||||||
const keyWords = transformedString.split(/\s+/);
|
|
||||||
|
|
||||||
if (keyWords.length > 1) {
|
|
||||||
return haystack.includes(transformedString);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const regex = new RegExp(`\\b${escapeRegex(transformedString)}\\b`);
|
|
||||||
if (regex.test(haystack)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
return haystack.includes(transformedString);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertAgnaiMemoryBook(inputObj) {
|
function convertAgnaiMemoryBook(inputObj) {
|
||||||
const outputObj = { entries: {} };
|
const outputObj = { entries: {} };
|
||||||
|
|
||||||
@@ -2210,6 +2364,9 @@ function convertCharacterBook(characterBook) {
|
|||||||
depth: entry.extensions?.depth ?? DEFAULT_DEPTH,
|
depth: entry.extensions?.depth ?? DEFAULT_DEPTH,
|
||||||
selectiveLogic: entry.extensions?.selectiveLogic ?? world_info_logic.AND_ANY,
|
selectiveLogic: entry.extensions?.selectiveLogic ?? world_info_logic.AND_ANY,
|
||||||
group: entry.extensions?.group ?? '',
|
group: entry.extensions?.group ?? '',
|
||||||
|
scanDepth: entry.extensions?.scan_depth ?? null,
|
||||||
|
caseSensitive: entry.extensions?.case_sensitive ?? null,
|
||||||
|
matchWholeWords: entry.extensions?.match_whole_words ?? null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2245,7 +2402,7 @@ export function checkEmbeddedWorld(chid) {
|
|||||||
const checkKey = `AlertWI_${characters[chid].avatar}`;
|
const checkKey = `AlertWI_${characters[chid].avatar}`;
|
||||||
const worldName = characters[chid]?.data?.extensions?.world;
|
const worldName = characters[chid]?.data?.extensions?.world;
|
||||||
if (!localStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) {
|
if (!localStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) {
|
||||||
localStorage.setItem(checkKey, 1);
|
localStorage.setItem(checkKey, 'true');
|
||||||
|
|
||||||
if (power_user.world_import_dialog) {
|
if (power_user.world_import_dialog) {
|
||||||
const html = `<h3>This character has an embedded World/Lorebook.</h3>
|
const html = `<h3>This character has an embedded World/Lorebook.</h3>
|
||||||
|
@@ -538,7 +538,6 @@ hr {
|
|||||||
background-color: var(--SmartThemeChatTintColor);
|
background-color: var(--SmartThemeChatTintColor);
|
||||||
-webkit-backdrop-filter: blur(var(--SmartThemeBlurStrength));
|
-webkit-backdrop-filter: blur(var(--SmartThemeBlurStrength));
|
||||||
text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
|
text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
|
||||||
scrollbar-width: thin;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
}
|
}
|
||||||
@@ -979,7 +978,6 @@ textarea {
|
|||||||
font-size: var(--mainFontSize);
|
font-size: var(--mainFontSize);
|
||||||
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
|
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
scrollbar-width: thin;
|
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
max-height: 90svh;
|
max-height: 90svh;
|
||||||
}
|
}
|
||||||
@@ -3125,7 +3123,6 @@ a {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
scrollbar-width: thin;
|
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
const TASK = 'feature-extraction';
|
const TASK = 'feature-extraction';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Gets the vectorized text in form of an array of numbers.
|
||||||
* @param {string} text - The text to vectorize
|
* @param {string} text - The text to vectorize
|
||||||
* @returns {Promise<number[]>} - The vectorized text in form of an array of numbers
|
* @returns {Promise<number[]>} - The vectorized text in form of an array of numbers
|
||||||
*/
|
*/
|
||||||
@@ -12,6 +13,20 @@ async function getTransformersVector(text) {
|
|||||||
return vector;
|
return vector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the vectorized texts in form of an array of arrays of numbers.
|
||||||
|
* @param {string[]} texts - The texts to vectorize
|
||||||
|
* @returns {Promise<number[][]>} - The vectorized texts in form of an array of arrays of numbers
|
||||||
|
*/
|
||||||
|
async function getTransformersBatchVector(texts) {
|
||||||
|
const result = [];
|
||||||
|
for (const text of texts) {
|
||||||
|
result.push(await getTransformersVector(text));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getTransformersVector,
|
getTransformersVector,
|
||||||
|
getTransformersBatchVector,
|
||||||
};
|
};
|
||||||
|
@@ -831,7 +831,7 @@ router.post('/generate', jsonParser, function (request, response) {
|
|||||||
let json = await fetchResponse.json();
|
let json = await fetchResponse.json();
|
||||||
response.send(json);
|
response.send(json);
|
||||||
console.log(json);
|
console.log(json);
|
||||||
console.log(json?.choices[0]?.message);
|
console.log(json?.choices?.[0]?.message);
|
||||||
} else if (fetchResponse.status === 429 && retries > 0) {
|
} else if (fetchResponse.status === 429 && retries > 0) {
|
||||||
console.log(`Out of quota, retrying in ${Math.round(timeout / 1000)}s`);
|
console.log(`Out of quota, retrying in ${Math.round(timeout / 1000)}s`);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@@ -388,6 +388,9 @@ function convertWorldInfoToCharacterBook(name, entries) {
|
|||||||
selectiveLogic: entry.selectiveLogic ?? 0,
|
selectiveLogic: entry.selectiveLogic ?? 0,
|
||||||
group: entry.group ?? '',
|
group: entry.group ?? '',
|
||||||
prevent_recursion: entry.preventRecursion ?? false,
|
prevent_recursion: entry.preventRecursion ?? false,
|
||||||
|
scan_depth: entry.scanDepth ?? null,
|
||||||
|
match_whole_words: entry.matchWholeWords ?? null,
|
||||||
|
case_sensitive: entry.caseSensitive ?? null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -7,16 +7,19 @@ const { jsonParser } = require('../express-common');
|
|||||||
/**
|
/**
|
||||||
* Gets the vector for the given text from the given source.
|
* Gets the vector for the given text from the given source.
|
||||||
* @param {string} source - The source of the vector
|
* @param {string} source - The source of the vector
|
||||||
|
* @param {Object} sourceSettings - Settings for the source, if it needs any
|
||||||
* @param {string} text - The text to get the vector for
|
* @param {string} text - The text to get the vector for
|
||||||
* @returns {Promise<number[]>} - The vector for the text
|
* @returns {Promise<number[]>} - The vector for the text
|
||||||
*/
|
*/
|
||||||
async function getVector(source, text) {
|
async function getVector(source, sourceSettings, text) {
|
||||||
switch (source) {
|
switch (source) {
|
||||||
case 'mistral':
|
case 'mistral':
|
||||||
case 'openai':
|
case 'openai':
|
||||||
return require('../openai-vectors').getOpenAIVector(text, source);
|
return require('../openai-vectors').getOpenAIVector(text, source);
|
||||||
case 'transformers':
|
case 'transformers':
|
||||||
return require('../embedding').getTransformersVector(text);
|
return require('../embedding').getTransformersVector(text);
|
||||||
|
case 'extras':
|
||||||
|
return require('../extras-vectors').getExtrasVector(text, sourceSettings.extrasUrl, sourceSettings.extrasKey);
|
||||||
case 'palm':
|
case 'palm':
|
||||||
return require('../makersuite-vectors').getMakerSuiteVector(text);
|
return require('../makersuite-vectors').getMakerSuiteVector(text);
|
||||||
}
|
}
|
||||||
@@ -24,6 +27,29 @@ async function getVector(source, text) {
|
|||||||
throw new Error(`Unknown vector source ${source}`);
|
throw new Error(`Unknown vector source ${source}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the vector for the given text batch from the given source.
|
||||||
|
* @param {string} source - The source of the vector
|
||||||
|
* @param {Object} sourceSettings - Settings for the source, if it needs any
|
||||||
|
* @param {string[]} texts - The array of texts to get the vector for
|
||||||
|
* @returns {Promise<number[][]>} - The array of vectors for the texts
|
||||||
|
*/
|
||||||
|
async function getBatchVector(source, sourceSettings, texts) {
|
||||||
|
switch (source) {
|
||||||
|
case 'mistral':
|
||||||
|
case 'openai':
|
||||||
|
return require('../openai-vectors').getOpenAIBatchVector(texts, source);
|
||||||
|
case 'transformers':
|
||||||
|
return require('../embedding').getTransformersBatchVector(texts);
|
||||||
|
case 'extras':
|
||||||
|
return require('../extras-vectors').getExtrasBatchVector(texts, sourceSettings.extrasUrl, sourceSettings.extrasKey);
|
||||||
|
case 'palm':
|
||||||
|
return require('../makersuite-vectors').getMakerSuiteBatchVector(texts);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown vector source ${source}`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the index for the vector collection
|
* Gets the index for the vector collection
|
||||||
* @param {string} collectionId - The collection ID
|
* @param {string} collectionId - The collection ID
|
||||||
@@ -45,19 +71,20 @@ async function getIndex(collectionId, source, create = true) {
|
|||||||
* Inserts items into the vector collection
|
* Inserts items into the vector collection
|
||||||
* @param {string} collectionId - The collection ID
|
* @param {string} collectionId - The collection ID
|
||||||
* @param {string} source - The source of the vector
|
* @param {string} source - The source of the vector
|
||||||
|
* @param {Object} sourceSettings - Settings for the source, if it needs any
|
||||||
* @param {{ hash: number; text: string; index: number; }[]} items - The items to insert
|
* @param {{ hash: number; text: string; index: number; }[]} items - The items to insert
|
||||||
*/
|
*/
|
||||||
async function insertVectorItems(collectionId, source, items) {
|
async function insertVectorItems(collectionId, source, sourceSettings, items) {
|
||||||
const store = await getIndex(collectionId, source);
|
const store = await getIndex(collectionId, source);
|
||||||
|
|
||||||
await store.beginUpdate();
|
await store.beginUpdate();
|
||||||
|
|
||||||
for (const item of items) {
|
const vectors = await getBatchVector(source, sourceSettings, items.map(x => x.text));
|
||||||
const text = item.text;
|
|
||||||
const hash = item.hash;
|
for (let i = 0; i < items.length; i++) {
|
||||||
const index = item.index;
|
const item = items[i];
|
||||||
const vector = await getVector(source, text);
|
const vector = vectors[i];
|
||||||
await store.upsertItem({ vector: vector, metadata: { hash, text, index } });
|
await store.upsertItem({ vector: vector, metadata: { hash: item.hash, text: item.text, index: item.index } });
|
||||||
}
|
}
|
||||||
|
|
||||||
await store.endUpdate();
|
await store.endUpdate();
|
||||||
@@ -101,13 +128,14 @@ async function deleteVectorItems(collectionId, source, hashes) {
|
|||||||
* Gets the hashes of the items in the vector collection that match the search text
|
* Gets the hashes of the items in the vector collection that match the search text
|
||||||
* @param {string} collectionId - The collection ID
|
* @param {string} collectionId - The collection ID
|
||||||
* @param {string} source - The source of the vector
|
* @param {string} source - The source of the vector
|
||||||
|
* @param {Object} sourceSettings - Settings for the source, if it needs any
|
||||||
* @param {string} searchText - The text to search for
|
* @param {string} searchText - The text to search for
|
||||||
* @param {number} topK - The number of results to return
|
* @param {number} topK - The number of results to return
|
||||||
* @returns {Promise<{hashes: number[], metadata: object[]}>} - The metadata of the items that match the search text
|
* @returns {Promise<{hashes: number[], metadata: object[]}>} - The metadata of the items that match the search text
|
||||||
*/
|
*/
|
||||||
async function queryCollection(collectionId, source, searchText, topK) {
|
async function queryCollection(collectionId, source, sourceSettings, searchText, topK) {
|
||||||
const store = await getIndex(collectionId, source);
|
const store = await getIndex(collectionId, source);
|
||||||
const vector = await getVector(source, searchText);
|
const vector = await getVector(source, sourceSettings, searchText);
|
||||||
|
|
||||||
const result = await store.queryItems(vector, topK);
|
const result = await store.queryItems(vector, topK);
|
||||||
const metadata = result.map(x => x.item.metadata);
|
const metadata = result.map(x => x.item.metadata);
|
||||||
@@ -115,6 +143,28 @@ async function queryCollection(collectionId, source, searchText, topK) {
|
|||||||
return { metadata, hashes };
|
return { metadata, hashes };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts settings for the vectorization sources from the HTTP request headers.
|
||||||
|
* @param {string} source - Which source to extract settings for.
|
||||||
|
* @param {object} request - The HTTP request object.
|
||||||
|
* @returns {object} - An object that can be used as `sourceSettings` in functions that take that parameter.
|
||||||
|
*/
|
||||||
|
function getSourceSettings(source, request) {
|
||||||
|
// Extras API settings to connect to the Extras embeddings provider
|
||||||
|
let extrasUrl = '';
|
||||||
|
let extrasKey = '';
|
||||||
|
if (source === 'extras') {
|
||||||
|
extrasUrl = String(request.headers['x-extras-url']);
|
||||||
|
extrasKey = String(request.headers['x-extras-key']);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceSettings = {
|
||||||
|
extrasUrl: extrasUrl,
|
||||||
|
extrasKey: extrasKey
|
||||||
|
};
|
||||||
|
return sourceSettings;
|
||||||
|
}
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.post('/query', jsonParser, async (req, res) => {
|
router.post('/query', jsonParser, async (req, res) => {
|
||||||
@@ -127,8 +177,9 @@ router.post('/query', jsonParser, async (req, res) => {
|
|||||||
const searchText = String(req.body.searchText);
|
const searchText = String(req.body.searchText);
|
||||||
const topK = Number(req.body.topK) || 10;
|
const topK = Number(req.body.topK) || 10;
|
||||||
const source = String(req.body.source) || 'transformers';
|
const source = String(req.body.source) || 'transformers';
|
||||||
|
const sourceSettings = getSourceSettings(source, req);
|
||||||
|
|
||||||
const results = await queryCollection(collectionId, source, searchText, topK);
|
const results = await queryCollection(collectionId, source, sourceSettings, searchText, topK);
|
||||||
return res.json(results);
|
return res.json(results);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -145,8 +196,9 @@ router.post('/insert', jsonParser, async (req, res) => {
|
|||||||
const collectionId = String(req.body.collectionId);
|
const collectionId = String(req.body.collectionId);
|
||||||
const items = req.body.items.map(x => ({ hash: x.hash, text: x.text, index: x.index }));
|
const items = req.body.items.map(x => ({ hash: x.hash, text: x.text, index: x.index }));
|
||||||
const source = String(req.body.source) || 'transformers';
|
const source = String(req.body.source) || 'transformers';
|
||||||
|
const sourceSettings = getSourceSettings(source, req);
|
||||||
|
|
||||||
await insertVectorItems(collectionId, source, items);
|
await insertVectorItems(collectionId, source, sourceSettings, items);
|
||||||
return res.sendStatus(200);
|
return res.sendStatus(200);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
78
src/extras-vectors.js
Normal file
78
src/extras-vectors.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
const fetch = require('node-fetch').default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the vector for the given text from SillyTavern-extras
|
||||||
|
* @param {string[]} texts - The array of texts to get the vectors for
|
||||||
|
* @param {string} apiUrl - The Extras API URL
|
||||||
|
* @param {string} apiKey - The Extras API key, or empty string if API key not enabled
|
||||||
|
* @returns {Promise<number[][]>} - The array of vectors for the texts
|
||||||
|
*/
|
||||||
|
async function getExtrasBatchVector(texts, apiUrl, apiKey) {
|
||||||
|
return getExtrasVectorImpl(texts, apiUrl, apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the vector for the given text from SillyTavern-extras
|
||||||
|
* @param {string} text - The text to get the vector for
|
||||||
|
* @param {string} apiUrl - The Extras API URL
|
||||||
|
* @param {string} apiKey - The Extras API key, or empty string if API key not enabled
|
||||||
|
* @returns {Promise<number[]>} - The vector for the text
|
||||||
|
*/
|
||||||
|
async function getExtrasVector(text, apiUrl, apiKey) {
|
||||||
|
return getExtrasVectorImpl(text, apiUrl, apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the vector for the given text from SillyTavern-extras
|
||||||
|
* @param {string|string[]} text - The text or texts to get the vector(s) for
|
||||||
|
* @param {string} apiUrl - The Extras API URL
|
||||||
|
* @param {string} apiKey - The Extras API key, or empty string if API key not enabled *
|
||||||
|
* @returns {Promise<Array>} - The vector for a single text if input is string, or the array of vectors for multiple texts if input is string[]
|
||||||
|
*/
|
||||||
|
async function getExtrasVectorImpl(text, apiUrl, apiKey) {
|
||||||
|
let url;
|
||||||
|
try {
|
||||||
|
url = new URL(apiUrl);
|
||||||
|
url.pathname = '/api/embeddings/compute';
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log('Failed to set up Extras API call:', error);
|
||||||
|
console.log('Extras API URL given was:', apiUrl);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Include the Extras API key, if enabled
|
||||||
|
if (apiKey && apiKey.length > 0) {
|
||||||
|
Object.assign(headers, {
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers,
|
||||||
|
body: JSON.stringify({
|
||||||
|
text: text, // The backend accepts {string|string[]} for one or multiple text items, respectively.
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
console.log('Extras request failed', response.statusText, text);
|
||||||
|
throw new Error('Extras request failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const vector = data.embedding; // `embedding`: number[] (one text item), or number[][] (multiple text items).
|
||||||
|
|
||||||
|
return vector;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getExtrasVector,
|
||||||
|
getExtrasBatchVector,
|
||||||
|
};
|
@@ -1,6 +1,17 @@
|
|||||||
const fetch = require('node-fetch').default;
|
const fetch = require('node-fetch').default;
|
||||||
const { SECRET_KEYS, readSecret } = require('./endpoints/secrets');
|
const { SECRET_KEYS, readSecret } = require('./endpoints/secrets');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the vector for the given text from gecko model
|
||||||
|
* @param {string[]} texts - The array of texts to get the vector for
|
||||||
|
* @returns {Promise<number[][]>} - The array of vectors for the texts
|
||||||
|
*/
|
||||||
|
async function getMakerSuiteBatchVector(texts) {
|
||||||
|
const promises = texts.map(text => getMakerSuiteVector(text));
|
||||||
|
const vectors = await Promise.all(promises);
|
||||||
|
return vectors;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the vector for the given text from PaLM gecko model
|
* Gets the vector for the given text from PaLM gecko model
|
||||||
* @param {string} text - The text to get the vector for
|
* @param {string} text - The text to get the vector for
|
||||||
@@ -40,4 +51,5 @@ async function getMakerSuiteVector(text) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getMakerSuiteVector,
|
getMakerSuiteVector,
|
||||||
|
getMakerSuiteBatchVector,
|
||||||
};
|
};
|
||||||
|
@@ -3,7 +3,7 @@ const { SECRET_KEYS, readSecret } = require('./endpoints/secrets');
|
|||||||
|
|
||||||
const SOURCES = {
|
const SOURCES = {
|
||||||
'mistral': {
|
'mistral': {
|
||||||
secretKey: SECRET_KEYS.MISTRAL,
|
secretKey: SECRET_KEYS.MISTRALAI,
|
||||||
url: 'api.mistral.ai',
|
url: 'api.mistral.ai',
|
||||||
model: 'mistral-embed',
|
model: 'mistral-embed',
|
||||||
},
|
},
|
||||||
@@ -15,12 +15,12 @@ const SOURCES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the vector for the given text from an OpenAI compatible endpoint.
|
* Gets the vector for the given text batch from an OpenAI compatible endpoint.
|
||||||
* @param {string} text - The text to get the vector for
|
* @param {string[]} texts - The array of texts to get the vector for
|
||||||
* @param {string} source - The source of the vector
|
* @param {string} source - The source of the vector
|
||||||
* @returns {Promise<number[]>} - The vector for the text
|
* @returns {Promise<number[][]>} - The array of vectors for the texts
|
||||||
*/
|
*/
|
||||||
async function getOpenAIVector(text, source) {
|
async function getOpenAIBatchVector(texts, source) {
|
||||||
const config = SOURCES[source];
|
const config = SOURCES[source];
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
@@ -43,7 +43,7 @@ async function getOpenAIVector(text, source) {
|
|||||||
Authorization: `Bearer ${key}`,
|
Authorization: `Bearer ${key}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
input: text,
|
input: texts,
|
||||||
model: config.model,
|
model: config.model,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -55,16 +55,31 @@ async function getOpenAIVector(text, source) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const vector = data?.data[0]?.embedding;
|
|
||||||
|
|
||||||
if (!Array.isArray(vector)) {
|
if (!Array.isArray(data?.data)) {
|
||||||
console.log('API response was not an array');
|
console.log('API response was not an array');
|
||||||
throw new Error('API response was not an array');
|
throw new Error('API response was not an array');
|
||||||
}
|
}
|
||||||
|
|
||||||
return vector;
|
// Sort data by x.index to ensure the order is correct
|
||||||
|
data.data.sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
|
const vectors = data.data.map(x => x.embedding);
|
||||||
|
return vectors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the vector for the given text from an OpenAI compatible endpoint.
|
||||||
|
* @param {string} text - The text to get the vector for
|
||||||
|
* @param {string} source - The source of the vector
|
||||||
|
* @returns {Promise<number[]>} - The vector for the text
|
||||||
|
*/
|
||||||
|
async function getOpenAIVector(text, source) {
|
||||||
|
const vectors = await getOpenAIBatchVector([text], source);
|
||||||
|
return vectors[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getOpenAIVector,
|
getOpenAIVector,
|
||||||
|
getOpenAIBatchVector,
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user