Merge pull request #3766 from bmen25124/custom_request_stop_string_cleanup Added stop string cleanup, better stopping string param

0a85178846cfd268a9fdd5fc4f78f7918155c3c8

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
3 files changed, +95 -22Showing whitespace changes
public/scripts/custom-request.js+67 -8
@@ -2,7 +2,7 @@ import { getPresetManager } from './preset-manager.js';
2import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js';2import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js';
3import { getTextGenServer } from './textgen-settings.js';3import { getTextGenServer } from './textgen-settings.js';
4import { extractReasoningFromData } from './reasoning.js';4import { extractReasoningFromData } from './reasoning.js';
5import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types } from './instruct-mode.js';5import { formatInstructModeChat, formatInstructModePrompt, getInstructStoppingSequences, names_behavior_types } from './instruct-mode.js';
6import { getStreamingReply, tryParseStreamingError } from './openai.js';6import { getStreamingReply, tryParseStreamingError } from './openai.js';
7import EventSourceStream from './sse-stream.js';7import EventSourceStream from './sse-stream.js';
88
@@ -190,6 +190,7 @@ export class TextCompletionService {
190 * @param {Object} options - Configuration options190 * @param {Object} options - Configuration options
191 * @param {string?} [options.presetName] - Name of the preset to use for generation settings191 * @param {string?} [options.presetName] - Name of the preset to use for generation settings
192 * @param {string?} [options.instructName] - Name of instruct preset for message formatting192 * @param {string?} [options.instructName] - Name of instruct preset for message formatting
193 * @param {Partial<InstructSettings>?} [options.instructSettings] - Override instruct settings
193 * @param {boolean} extractData - Whether to extract structured data from response194 * @param {boolean} extractData - Whether to extract structured data from response
194 * @param {AbortSignal?} [signal]195 * @param {AbortSignal?} [signal]
195 * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator196 * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
@@ -222,15 +223,20 @@ export class TextCompletionService {
222 }223 }
223 }224 }
224225
226
227 /** @type {InstructSettings | undefined} */
228 let instructPreset;
225 // Handle instruct formatting if requested229 // Handle instruct formatting if requested
226 if (Array.isArray(prompt) && instructName) {230 if (Array.isArray(prompt) && instructName) {
227 const instructPresetManager = getPresetManager('instruct');231 const instructPresetManager = getPresetManager('instruct');
228 let instructPreset = instructPresetManager?.getCompletionPresetByName(instructName);232 instructPreset = instructPresetManager?.getCompletionPresetByName(instructName);
229 if (instructPreset) {233 if (instructPreset) {
230 // Clone the preset to avoid modifying the original234 // Clone the preset to avoid modifying the original
231 instructPreset = structuredClone(instructPreset);235 instructPreset = structuredClone(instructPreset);
232 instructPreset.macro = false;
233 instructPreset.names_behavior = names_behavior_types.NONE;236 instructPreset.names_behavior = names_behavior_types.NONE;
237 if (options.instructSettings) {
238 Object.assign(instructPreset, options.instructSettings);
239 }
234240
235 // Format messages using instruct formatting241 // Format messages using instruct formatting
236 const formattedMessages = [];242 const formattedMessages = [];
@@ -266,10 +272,9 @@ export class TextCompletionService {
266 formattedMessages.push(messageContent);272 formattedMessages.push(messageContent);
267 }273 }
268 requestData.prompt = formattedMessages.join('');274 requestData.prompt = formattedMessages.join('');
269 if (instructPreset.output_suffix) {275 const stoppingStrings = getInstructStoppingSequences({ customInstruct: instructPreset, useStopStrings: false });
270 requestData.stop = [instructPreset.output_suffix];276 requestData.stop = stoppingStrings;
271 requestData.stopping_strings = [instructPreset.output_suffix];277 requestData.stopping_strings = stoppingStrings;
272 }
273 } else {278 } else {
274 console.warn(`Instruct preset "${instructName}" not found, using basic formatting`);279 console.warn(`Instruct preset "${instructName}" not found, using basic formatting`);
275 requestData.prompt = prompt.map(x => x.content).join('\n\n');280 requestData.prompt = prompt.map(x => x.content).join('\n\n');
@@ -283,7 +288,61 @@ export class TextCompletionService {
283 // @ts-ignore288 // @ts-ignore
284 const data = this.createRequestData(requestData);289 const data = this.createRequestData(requestData);
285290
286 return await this.sendRequest(data, extractData, signal);291 const response = await this.sendRequest(data, extractData, signal);
292 // Remove stopping strings from the end
293 if (!data.stream && extractData) {
294 /** @type {ExtractedData} */
295 // @ts-ignore
296 const extractedData = response;
297
298 let message = extractedData.content;
299
300 message = message.replace(/[^\S\r\n]+$/gm, '');
301
302 if (requestData.stopping_strings) {
303 for (const stoppingString of requestData.stopping_strings) {
304 if (stoppingString.length) {
305 for (let j = stoppingString.length; j > 0; j--) {
306 if (message.slice(-j) === stoppingString.slice(0, j)) {
307 message = message.slice(0, -j);
308 break;
309 }
310 }
311 }
312 }
313 }
314
315 if (instructPreset) {
316 [
317 instructPreset.stop_sequence,
318 instructPreset.input_sequence,
319 ].forEach(sequence => {
320 if (sequence?.trim()) {
321 const index = message.indexOf(sequence);
322 if (index !== -1) {
323 message = message.substring(0, index);
324 }
325 }
326 });
327
328 [
329 instructPreset.output_sequence,
330 instructPreset.last_output_sequence,
331 ].forEach(sequences => {
332 if (sequences) {
333 sequences.split('\n')
334 .filter(line => line.trim() !== '')
335 .forEach(line => {
336 message = message.replaceAll(line, '');
337 });
338 }
339 });
340 }
341
342 extractedData.content = message;
343 }
344
345 return response;
287 }346 }
288347
289 /**348 /**
public/scripts/extensions/shared.js+10 -2
@@ -285,6 +285,7 @@ export class ConnectionManagerRequestService {
285 extractData: true,285 extractData: true,
286 includePreset: true,286 includePreset: true,
287 includeInstruct: true,287 includeInstruct: true,
288 instructSettings: {},
288 };289 };
289290
290 static getAllowedTypes() {291 static getAllowedTypes() {
@@ -298,11 +299,17 @@ export class ConnectionManagerRequestService {
298 * @param {string} profileId299 * @param {string} profileId
299 * @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt300 * @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt
300 * @param {number} maxTokens301 * @param {number} maxTokens
301 * @param {{stream?: boolean, signal?: AbortSignal, extractData?: boolean, includePreset?: boolean, includeInstruct?: boolean}} custom - default values are true302 * @param {Object} custom
303 * @param {boolean?} [custom.stream=false]
304 * @param {AbortSignal?} [custom.signal]
305 * @param {boolean?} [custom.extractData=true]
306 * @param {boolean?} [custom.includePreset=true]
307 * @param {boolean?} [custom.includeInstruct=true]
308 * @param {Partial<InstructSettings>?} [custom.instructSettings] Override instruct settings
302 * @returns {Promise<import('../custom-request.js').ExtractedData | (() => AsyncGenerator<import('../custom-request.js').StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator309 * @returns {Promise<import('../custom-request.js').ExtractedData | (() => AsyncGenerator<import('../custom-request.js').StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
303 */310 */
304 static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams) {311 static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams) {
305 const { stream, signal, extractData, includePreset, includeInstruct } = { ...this.defaultSendRequestParams, ...custom };312 const { stream, signal, extractData, includePreset, includeInstruct, instructSettings } = { ...this.defaultSendRequestParams, ...custom };
306313
307 const context = SillyTavern.getContext();314 const context = SillyTavern.getContext();
308 if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {315 if (context.extensionSettings.disabledExtensions.includes('connection-manager')) {
@@ -346,6 +353,7 @@ export class ConnectionManagerRequestService {
346 }, {353 }, {
347 instructName: includeInstruct ? profile.instruct : undefined,354 instructName: includeInstruct ? profile.instruct : undefined,
348 presetName: includePreset ? profile.preset : undefined,355 presetName: includePreset ? profile.preset : undefined,
356 instructSettings: includeInstruct ? instructSettings : undefined,
349 }, extractData, signal);357 }, extractData, signal);
350 }358 }
351 default: {359 default: {
public/scripts/instruct-mode.js+18 -12
@@ -243,9 +243,14 @@ export function autoSelectInstructPreset(modelId) {
243243
244/**244/**
245 * Converts instruct mode sequences to an array of stopping strings.245 * Converts instruct mode sequences to an array of stopping strings.
246 * @param {Object} options
247 * @param {InstructSettings?} [options.customInstruct=null] - Custom instruct settings.
248 * @param {boolean?} [options.useStopStrings] - Decides whether to use "Chat Start" and "Example Separator"
246 * @returns {string[]} Array of instruct mode stopping strings.249 * @returns {string[]} Array of instruct mode stopping strings.
247 */250 */
248export function getInstructStoppingSequences() {251export function getInstructStoppingSequences({ customInstruct = null, useStopStrings = null } = {}) {
252 const instruct = structuredClone(customInstruct ?? power_user.instruct);
253
249 /**254 /**
250 * Adds instruct mode sequence to the result array.255 * Adds instruct mode sequence to the result array.
251 * @param {string} sequence Sequence string.256 * @param {string} sequence Sequence string.
@@ -254,7 +259,7 @@ export function getInstructStoppingSequences() {
254 function addInstructSequence(sequence) {259 function addInstructSequence(sequence) {
255 // Cohee: oobabooga's textgen always appends newline before the sequence as a stopping string260 // Cohee: oobabooga's textgen always appends newline before the sequence as a stopping string
256 // But it's a problem for Metharme which doesn't use newlines to separate them.261 // But it's a problem for Metharme which doesn't use newlines to separate them.
257 const wrap = (s) => power_user.instruct.wrap ? '\n' + s : s;262 const wrap = (s) => instruct.wrap ? '\n' + s : s;
258 // Sequence must be a non-empty string263 // Sequence must be a non-empty string
259 if (typeof sequence === 'string' && sequence.length > 0) {264 if (typeof sequence === 'string' && sequence.length > 0) {
260 // If sequence is just a whitespace or newline - we don't want to make it a stopping string265 // If sequence is just a whitespace or newline - we don't want to make it a stopping string
@@ -262,7 +267,7 @@ export function getInstructStoppingSequences() {
262 if (sequence.trim().length > 0) {267 if (sequence.trim().length > 0) {
263 const wrappedSequence = wrap(sequence);268 const wrappedSequence = wrap(sequence);
264 // Need to respect "insert macro" setting269 // Need to respect "insert macro" setting
265 const stopString = power_user.instruct.macro ? substituteParams(wrappedSequence) : wrappedSequence;270 const stopString = instruct.macro ? substituteParams(wrappedSequence) : wrappedSequence;
266 result.push(stopString);271 result.push(stopString);
267 }272 }
268 }273 }
@@ -270,14 +275,15 @@ export function getInstructStoppingSequences() {
270275
271 const result = [];276 const result = [];
272277
273 if (power_user.instruct.enabled) {278 // Since preset's don't have "enabled", we assume it's always enabled
274 const stop_sequence = power_user.instruct.stop_sequence || '';279 if (customInstruct ?? instruct.enabled) {
275 const input_sequence = power_user.instruct.input_sequence?.replace(/{{name}}/gi, name1) || '';280 const stop_sequence = instruct.stop_sequence || '';
276 const output_sequence = power_user.instruct.output_sequence?.replace(/{{name}}/gi, name2) || '';281 const input_sequence = instruct.input_sequence?.replace(/{{name}}/gi, name1) || '';
277 const first_output_sequence = power_user.instruct.first_output_sequence?.replace(/{{name}}/gi, name2) || '';282 const output_sequence = instruct.output_sequence?.replace(/{{name}}/gi, name2) || '';
278 const last_output_sequence = power_user.instruct.last_output_sequence?.replace(/{{name}}/gi, name2) || '';283 const first_output_sequence = instruct.first_output_sequence?.replace(/{{name}}/gi, name2) || '';
279 const system_sequence = power_user.instruct.system_sequence?.replace(/{{name}}/gi, 'System') || '';284 const last_output_sequence = instruct.last_output_sequence?.replace(/{{name}}/gi, name2) || '';
280 const last_system_sequence = power_user.instruct.last_system_sequence?.replace(/{{name}}/gi, 'System') || '';285 const system_sequence = instruct.system_sequence?.replace(/{{name}}/gi, 'System') || '';
286 const last_system_sequence = instruct.last_system_sequence?.replace(/{{name}}/gi, 'System') || '';
281287
282 const combined_sequence = [288 const combined_sequence = [
283 stop_sequence,289 stop_sequence,
@@ -292,7 +298,7 @@ export function getInstructStoppingSequences() {
292 combined_sequence.split('\n').filter((line, index, self) => self.indexOf(line) === index).forEach(addInstructSequence);298 combined_sequence.split('\n').filter((line, index, self) => self.indexOf(line) === index).forEach(addInstructSequence);
293 }299 }
294300
295 if (power_user.context.use_stop_strings) {301 if (useStopStrings ?? power_user.context.use_stop_strings) {
296 if (power_user.context.chat_start) {302 if (power_user.context.chat_start) {
297 result.push(`\n${substituteParams(power_user.context.chat_start)}`);303 result.push(`\n${substituteParams(power_user.context.chat_start)}`);
298 }304 }