Merge pull request #1011 from bdashore3/generate-array

Generate: Change prompt building mechanisms
This commit is contained in:
Cohee 2023-08-26 01:28:10 +03:00 committed by GitHub
commit 11ba93aa10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 407 additions and 57 deletions

View File

@ -45,6 +45,7 @@
<script src="lib/pagination.js"></script>
<script src="lib/toolcool-color-picker.js"></script>
<script src="lib/svg-inject.js"></script>
<script type="module" src="lib/structured-clone/monkey-patch.js"></script>
<script type="module" src="lib/swiped-events.js"></script>
<script type="module" src="lib/eventemitter.js"></script>
<script type="module" src="scripts/power-user.js"></script>

View File

@ -0,0 +1,15 @@
ISC License
Copyright (c) 2021, Andrea Giammarchi, @WebReflection
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

View File

@ -0,0 +1,79 @@
import {
VOID, PRIMITIVE,
ARRAY, OBJECT,
DATE, REGEXP, MAP, SET,
ERROR, BIGINT
} from './types.js';
const env = typeof self === 'object' ? self : globalThis;
const deserializer = ($, _) => {
const as = (out, index) => {
$.set(index, out);
return out;
};
const unpair = index => {
if ($.has(index))
return $.get(index);
const [type, value] = _[index];
switch (type) {
case PRIMITIVE:
case VOID:
return as(value, index);
case ARRAY: {
const arr = as([], index);
for (const index of value)
arr.push(unpair(index));
return arr;
}
case OBJECT: {
const object = as({}, index);
for (const [key, index] of value)
object[unpair(key)] = unpair(index);
return object;
}
case DATE:
return as(new Date(value), index);
case REGEXP: {
const {source, flags} = value;
return as(new RegExp(source, flags), index);
}
case MAP: {
const map = as(new Map, index);
for (const [key, index] of value)
map.set(unpair(key), unpair(index));
return map;
}
case SET: {
const set = as(new Set, index);
for (const index of value)
set.add(unpair(index));
return set;
}
case ERROR: {
const {name, message} = value;
return as(new env[name](message), index);
}
case BIGINT:
return as(BigInt(value), index);
case 'BigInt':
return as(Object(BigInt(value)), index);
}
return as(new env[type](value), index);
};
return unpair;
};
/**
* @typedef {Array<string,any>} Record a type representation
*/
/**
* Returns a deserialized value from a serialized array of Records.
* @param {Record[]} serialized a previously serialized value.
* @returns {any}
*/
export const deserialize = serialized => deserializer(new Map, serialized)(0);

View File

@ -0,0 +1,25 @@
import {deserialize} from './deserialize.js';
import {serialize} from './serialize.js';
/**
* @typedef {Array<string,any>} Record a type representation
*/
/**
* Returns an array of serialized Records.
* @param {any} any a serializable value.
* @param {{transfer?: any[], json?: boolean, lossy?: boolean}?} options an object with
* a transfer option (ignored when polyfilled) and/or non standard fields that
* fallback to the polyfill if present.
* @returns {Record[]}
*/
export default typeof structuredClone === "function" ?
/* c8 ignore start */
(any, options) => (
options && ('json' in options || 'lossy' in options) ?
deserialize(serialize(any, options)) : structuredClone(any)
) :
(any, options) => deserialize(serialize(any, options));
/* c8 ignore stop */
export {deserialize, serialize};

View File

@ -0,0 +1,21 @@
/*! (c) Andrea Giammarchi - ISC */
import {deserialize} from './deserialize.js';
import {serialize} from './serialize.js';
const {parse: $parse, stringify: $stringify} = JSON;
const options = {json: true, lossy: true};
/**
* Revive a previously stringified structured clone.
* @param {string} str previously stringified data as string.
* @returns {any} whatever was previously stringified as clone.
*/
export const parse = str => deserialize($parse(str));
/**
* Represent a structured clone value as string.
* @param {any} any some clone-able value to stringify.
* @returns {string} the value stringified.
*/
export const stringify = any => $stringify(serialize(any, options));

View File

@ -0,0 +1,6 @@
import structuredClone from './index.js';
if (!("structuredClone" in globalThis)) {
console.debug("Monkey-patching structuredClone");
globalThis.structuredClone = structuredClone;
}

View File

@ -0,0 +1,161 @@
import {
VOID, PRIMITIVE,
ARRAY, OBJECT,
DATE, REGEXP, MAP, SET,
ERROR, BIGINT
} from './types.js';
const EMPTY = '';
const {toString} = {};
const {keys} = Object;
const typeOf = value => {
const type = typeof value;
if (type !== 'object' || !value)
return [PRIMITIVE, type];
const asString = toString.call(value).slice(8, -1);
switch (asString) {
case 'Array':
return [ARRAY, EMPTY];
case 'Object':
return [OBJECT, EMPTY];
case 'Date':
return [DATE, EMPTY];
case 'RegExp':
return [REGEXP, EMPTY];
case 'Map':
return [MAP, EMPTY];
case 'Set':
return [SET, EMPTY];
}
if (asString.includes('Array'))
return [ARRAY, asString];
if (asString.includes('Error'))
return [ERROR, asString];
return [OBJECT, asString];
};
const shouldSkip = ([TYPE, type]) => (
TYPE === PRIMITIVE &&
(type === 'function' || type === 'symbol')
);
const serializer = (strict, json, $, _) => {
const as = (out, value) => {
const index = _.push(out) - 1;
$.set(value, index);
return index;
};
const pair = value => {
if ($.has(value))
return $.get(value);
let [TYPE, type] = typeOf(value);
switch (TYPE) {
case PRIMITIVE: {
let entry = value;
switch (type) {
case 'bigint':
TYPE = BIGINT;
entry = value.toString();
break;
case 'function':
case 'symbol':
if (strict)
throw new TypeError('unable to serialize ' + type);
entry = null;
break;
case 'undefined':
return as([VOID], value);
}
return as([TYPE, entry], value);
}
case ARRAY: {
if (type)
return as([type, [...value]], value);
const arr = [];
const index = as([TYPE, arr], value);
for (const entry of value)
arr.push(pair(entry));
return index;
}
case OBJECT: {
if (type) {
switch (type) {
case 'BigInt':
return as([type, value.toString()], value);
case 'Boolean':
case 'Number':
case 'String':
return as([type, value.valueOf()], value);
}
}
if (json && ('toJSON' in value))
return pair(value.toJSON());
const entries = [];
const index = as([TYPE, entries], value);
for (const key of keys(value)) {
if (strict || !shouldSkip(typeOf(value[key])))
entries.push([pair(key), pair(value[key])]);
}
return index;
}
case DATE:
return as([TYPE, value.toISOString()], value);
case REGEXP: {
const {source, flags} = value;
return as([TYPE, {source, flags}], value);
}
case MAP: {
const entries = [];
const index = as([TYPE, entries], value);
for (const [key, entry] of value) {
if (strict || !(shouldSkip(typeOf(key)) || shouldSkip(typeOf(entry))))
entries.push([pair(key), pair(entry)]);
}
return index;
}
case SET: {
const entries = [];
const index = as([TYPE, entries], value);
for (const entry of value) {
if (strict || !shouldSkip(typeOf(entry)))
entries.push(pair(entry));
}
return index;
}
}
const {message} = value;
return as([TYPE, {name: type, message}], value);
};
return pair;
};
/**
* @typedef {Array<string,any>} Record a type representation
*/
/**
* Returns an array of serialized Records.
* @param {any} value a serializable value.
* @param {{json?: boolean, lossy?: boolean}?} options an object with a `lossy` or `json` property that,
* if `true`, will not throw errors on incompatible types, and behave more
* like JSON stringify would behave. Symbol and Function will be discarded.
* @returns {Record[]}
*/
export const serialize = (value, {json, lossy} = {}) => {
const _ = [];
return serializer(!(json || lossy), !!json, new Map, _)(value), _;
};

View File

@ -0,0 +1,11 @@
export const VOID = -1;
export const PRIMITIVE = 0;
export const ARRAY = 1;
export const OBJECT = 2;
export const DATE = 3;
export const REGEXP = 4;
export const MAP = 5;
export const SET = 6;
export const ERROR = 7;
export const BIGINT = 8;
// export const SYMBOL = 9;

View File

@ -157,7 +157,7 @@ import {
} from "./scripts/secrets.js";
import { EventEmitter } from './lib/eventemitter.js';
import { markdownExclusionExt } from "./scripts/showdown-exclusion.js";
import { NOTE_MODULE_NAME, metadata_keys, setFloatingPrompt, shouldWIAddPrompt } from "./scripts/authors-note.js";
import { NOTE_MODULE_NAME, initAuthorsNote, metadata_keys, setFloatingPrompt, shouldWIAddPrompt } from "./scripts/authors-note.js";
import { getDeviceInfo } from "./scripts/RossAscends-mods.js";
import { registerPromptManagerMigration } from "./scripts/PromptManager.js";
import { getRegexedString, regex_placement } from "./scripts/extensions/regex/engine.js";
@ -2613,7 +2613,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
console.debug('generating prompt');
chatString = "";
arrMes = arrMes.reverse();
arrMes.forEach(function (item, i, arr) {//For added anchors and others
arrMes.forEach(function (item, i, arr) {// For added anchors and others
// OAI doesn't need all of this
if (main_api === 'openai') {
return;
@ -2629,25 +2629,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
}
}
if (i === 0) {
// Process those that couldn't get that far
for (let upperDepth = 100; upperDepth >= arrMes.length; upperDepth--) {
const upperAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, upperDepth);
if (upperAnchor && upperAnchor.length) {
item = upperAnchor + item;
}
}
}
const anchorDepth = Math.abs(i - arrMes.length + 1);
// NOTE: Depth injected here!
const extensionAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, anchorDepth);
if (anchorDepth > 0 && extensionAnchor && extensionAnchor.length) {
item += extensionAnchor;
}
mesSend[mesSend.length] = item;
mesSend[mesSend.length] = { message: item, extensionPrompts: [] };
});
}
@ -2662,7 +2644,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
mesExmString = pinExmString ?? mesExamplesArray.slice(0, count_exm_add).join('');
if (mesSend.length) {
mesSend[mesSend.length - 1] = modifyLastPromptLine(mesSend[mesSend.length - 1]);
mesSend[mesSend.length - 1].message = modifyLastPromptLine(mesSend[mesSend.length - 1].message);
}
}
@ -2761,6 +2743,10 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
// Fetches the combined prompt for both negative and positive prompts
const cfgGuidanceScale = getGuidanceScale();
// For prompt bit itemization
let mesSendString = '';
function getCombinedPrompt(isNegative) {
// Only return if the guidance scale doesn't exist or the value is 1
// Also don't return if constructing the neutral prompt
@ -2768,7 +2754,50 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
return;
}
let finalMesSend = [...mesSend];
// OAI has its own prompt manager. No need to do anything here
if (main_api === 'openai') {
return ''
}
// Deep clone
let finalMesSend = structuredClone(mesSend);
// TODO: Rewrite getExtensionPrompt to not require multiple for loops
// Set all extension prompts where insertion depth > mesSend length
if (finalMesSend.length) {
for (let upperDepth = 100; upperDepth >= finalMesSend.length; upperDepth--) {
const upperAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, upperDepth);
if (upperAnchor && upperAnchor.length) {
finalMesSend[0].extensionPrompts.push(upperAnchor);
}
}
}
finalMesSend.forEach((mesItem, index) => {
if (index === 0) {
return;
}
const anchorDepth = Math.abs(index - finalMesSend.length + 1);
// NOTE: Depth injected here!
const extensionAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, anchorDepth);
if (anchorDepth > 0 && extensionAnchor && extensionAnchor.length) {
mesItem.extensionPrompts.push(extensionAnchor);
}
});
// TODO: Move zero-depth anchor append to work like CFG and bias appends
if (zeroDepthAnchor && zeroDepthAnchor.length) {
if (!isMultigenEnabled() || tokens_already_generated == 0) {
console.log(/\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1)))
finalMesSend[finalMesSend.length - 1].message +=
/\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1))
? zeroDepthAnchor
: `${zeroDepthAnchor}`;
}
}
let cfgPrompt = {};
if (cfgGuidanceScale && cfgGuidanceScale?.value !== 1) {
cfgPrompt = getCfgPrompt(cfgGuidanceScale, isNegative);
@ -2776,13 +2805,13 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
if (cfgPrompt && cfgPrompt?.value) {
if (cfgPrompt?.depth === 0) {
finalMesSend[finalMesSend.length - 1] +=
/\s/.test(finalMesSend[finalMesSend.length - 1].slice(-1))
finalMesSend[finalMesSend.length - 1].message +=
/\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1))
? cfgPrompt.value
: ` ${cfgPrompt.value}`;
} else {
// TODO: Make all extension prompts use an array/splice method
finalMesSend.splice(mesSend.length - cfgPrompt.depth, 0, `${cfgPrompt.value}\n`);
finalMesSend[mesSend.length - cfgPrompt.depth].extensionPrompts.push(`${cfgPrompt.value}\n`);
}
}
@ -2790,25 +2819,20 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
// Always run with continue
if (!isInstruct && !isImpersonate && (tokens_already_generated === 0 || isContinue)) {
if (promptBias.trim().length !== 0) {
finalMesSend[finalMesSend.length - 1] +=
/\s/.test(finalMesSend[finalMesSend.length - 1].slice(-1))
finalMesSend[finalMesSend.length - 1].message +=
/\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1))
? promptBias.trimStart()
: ` ${promptBias.trimStart()}`;
}
}
// Prune from prompt cache if it exists
if (generatedPromtCache.length !== 0) {
generatedPromtCache = cleanupPromptCache(generatedPromtCache);
}
// Override for prompt bits data
if (!isNegative) {
mesSend = finalMesSend;
}
let mesSendString = finalMesSend.join('');
// Right now, everything is suffixed with a newline
mesSendString = finalMesSend.map((e) => `${e.extensionPrompts.join('')}${e.message}`).join('');
// add chat preamble
mesSendString = addChatsPreamble(mesSendString);
@ -2823,13 +2847,6 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
mesSendString +
generatedPromtCache;
// TODO: Move zero-depth anchor append to work like CFG and bias appends
if (zeroDepthAnchor && zeroDepthAnchor.length) {
if (!isMultigenEnabled() || tokens_already_generated == 0) {
combinedPrompt = appendZeroDepthAnchor(force_name2, zeroDepthAnchor, combinedPrompt);
}
}
combinedPrompt = combinedPrompt.replace(/\r/gm, '');
if (power_user.collapse_newlines) {
@ -2841,7 +2858,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
// Get the negative prompt first since it has the unmodified mesSend array
let negativePrompt = main_api == 'textgenerationwebui' ? getCombinedPrompt(true) : undefined;
let finalPromt = getCombinedPrompt(false);
let finalPrompt = getCombinedPrompt(false);
// Include the entire guidance scale object
const cfgValues = cfgGuidanceScale && cfgGuidanceScale?.value !== 1 ? ({ guidanceScale: cfgGuidanceScale, negativePrompt: negativePrompt }) : null;
@ -2865,7 +2882,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
let generate_data;
if (main_api == 'koboldhorde' || main_api == 'kobold') {
generate_data = {
prompt: finalPromt,
prompt: finalPrompt,
gui_settings: true,
max_length: amount_gen,
temperature: kai_settings.temp,
@ -2875,16 +2892,16 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
if (preset_settings != 'gui') {
const maxContext = (adjustedParams && horde_settings.auto_adjust_context_length) ? adjustedParams.maxContextLength : max_context;
generate_data = getKoboldGenerationData(finalPromt, this_settings, this_amount_gen, maxContext, isImpersonate, type);
generate_data = getKoboldGenerationData(finalPrompt, this_settings, this_amount_gen, maxContext, isImpersonate, type);
}
}
else if (main_api == 'textgenerationwebui') {
generate_data = getTextGenGenerationData(finalPromt, this_amount_gen, isImpersonate, cfgValues);
generate_data = getTextGenGenerationData(finalPrompt, this_amount_gen, isImpersonate, cfgValues);
generate_data.use_mancer = api_use_mancer_webui;
}
else if (main_api == 'novel') {
const this_settings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]];
generate_data = getNovelGenerationData(finalPromt, this_settings, this_amount_gen, isImpersonate, cfgValues);
generate_data = getNovelGenerationData(finalPrompt, this_settings, this_amount_gen, isImpersonate, cfgValues);
}
else if (main_api == 'openai') {
let [prompt, counts] = prepareOpenAIMessages({
@ -2940,10 +2957,10 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
storyString: storyString,
afterScenarioAnchor: afterScenarioAnchor,
examplesString: examplesString,
mesSendString: mesSend.join(''),
mesSendString: mesSendString,
generatedPromtCache: generatedPromtCache,
promptBias: promptBias,
finalPromt: finalPromt,
finalPromt: finalPrompt,
charDescription: charDescription,
charPersonality: charPersonality,
scenarioText: scenarioText,
@ -2970,7 +2987,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
}
}
else if (main_api == 'koboldhorde') {
generateHorde(finalPromt, generate_data, abortController.signal).then(onSuccess).catch(onError);
generateHorde(finalPrompt, generate_data, abortController.signal).then(onSuccess).catch(onError);
}
else if (main_api == 'textgenerationwebui' && isStreamingEnabled() && type !== 'quiet') {
streamingProcessor.generator = await generateTextGenWithStreaming(generate_data, streamingProcessor.abortController.signal);
@ -5844,10 +5861,22 @@ function select_rm_characters() {
printCharacters(false); // Do a quick refresh of the characters list
}
/**
* Sets a prompt injection to insert custom text into any outgoing prompt. For use in UI extensions.
* @param {string} key Prompt injection id.
* @param {string} value Prompt injection value.
* @param {number} position Insertion position. 0 is after story string, 1 is in-chat with custom depth.
* @param {number} depth Insertion depth. 0 represets the last message in context. Expected values up to 100.
*/
function setExtensionPrompt(key, value, position, depth) {
extension_prompts[key] = { value, position, depth };
extension_prompts[key] = { value: String(value), position: Number(position), depth: Number(depth) };
}
/**
* Adds or updates the metadata for the currently active chat.
* @param {Object} newValues An object with collection of new values to be added into the metadata.
* @param {boolean} reset Should a metadata be reset by this call.
*/
function updateChatMetadata(newValues, reset) {
chat_metadata = reset ? { ...newValues } : { ...chat_metadata, ...newValues };
}
@ -9035,5 +9064,6 @@ $(document).ready(function () {
});
// Added here to prevent execution before script.js is loaded and get rid of quirky timeouts
initAuthorsNote();
initRossMods();
});

View File

@ -389,10 +389,11 @@ function onChatChanged() {
$('#extension_floating_default_token_counter').text(tokenCounter3);
}
// Inject extension when extensions_activating is fired
/**
* Inject author's note options and setup event listeners.
*/
// Inserts the extension first since it's statically imported
jQuery(async () => {
await waitUntilCondition(() => eventSource !== undefined);
export function initAuthorsNote() {
$('#extension_floating_prompt').on('input', onExtensionFloatingPromptInput);
$('#extension_floating_interval').on('input', onExtensionFloatingIntervalInput);
$('#extension_floating_depth').on('input', onExtensionFloatingDepthInput);
@ -419,4 +420,4 @@ jQuery(async () => {
registerSlashCommand('freq', setNoteIntervalCommand, ['interval'], "<span class='monospace'>(number)</span> sets an author's note insertion frequency", true, true);
registerSlashCommand('pos', setNotePositionCommand, ['position'], "(<span class='monospace'>chat</span> or <span class='monospace'>scenario</span>) sets an author's note position", true, true);
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
});
}

View File

@ -296,11 +296,11 @@ function setOpenAIMessages(chat) {
}
// Add chat injections, 100 = maximum depth of injection. (Why would you ever need more?)
for (let i = 0; i < 100; i++) {
for (let i = 100; i >= 0; i--) {
const anchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, i);
if (anchor && anchor.length) {
openai_msgs.splice(i, 0, { "role": 'system', 'content': anchor.trim() })
openai_msgs.splice(i, 0, { "role": 'system', 'content': anchor.trim() });
}
}
}