Merge pull request #3766 from bmen25124/custom_request_stop_string_cleanup Added stop string cleanup, better stopping string param
Signed| @@ -2,7 +2,7 @@ import { getPresetManager } from './preset-manager.js'; | |||
| 2 | import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js'; | 2 | import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js'; |
| 3 | import { getTextGenServer } from './textgen-settings.js'; | 3 | import { getTextGenServer } from './textgen-settings.js'; |
| 4 | import { extractReasoningFromData } from './reasoning.js'; | 4 | import { extractReasoningFromData } from './reasoning.js'; |
| 5 | import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types } from './instruct-mode.js'; | 5 | import { formatInstructModeChat, formatInstructModePrompt, getInstructStoppingSequences, names_behavior_types } from './instruct-mode.js'; |
| 6 | import { getStreamingReply, tryParseStreamingError } from './openai.js'; | 6 | import { getStreamingReply, tryParseStreamingError } from './openai.js'; |
| 7 | import EventSourceStream from './sse-stream.js'; | 7 | import EventSourceStream from './sse-stream.js'; |
| 8 | 8 | ||
| @@ -190,6 +190,7 @@ export class TextCompletionService { | |||
| 190 | * @param {Object} options - Configuration options | 190 | * @param {Object} options - Configuration options |
| 191 | * @param {string?} [options.presetName] - Name of the preset to use for generation settings | 191 | * @param {string?} [options.presetName] - Name of the preset to use for generation settings |
| 192 | * @param {string?} [options.instructName] - Name of instruct preset for message formatting | 192 | * @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 response | 194 | * @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 AsyncGenerator | 196 | * @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 | } |
| 224 | 225 | ||
| 226 | |||
| 227 | /** @type {InstructSettings | undefined} */ | ||
| 228 | let instructPreset; | ||
| 225 | // Handle instruct formatting if requested | 229 | // 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 original | 234 | // 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 | } | ||
| 234 | 240 | ||
| 235 | // Format messages using instruct formatting | 241 | // 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-ignore | 288 | // @ts-ignore |
| 284 | const data = this.createRequestData(requestData); | 289 | const data = this.createRequestData(requestData); |
| 285 | 290 | ||
| 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 | } |
| 288 | 347 | ||
| 289 | /** | 348 | /** |
| @@ -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 | }; |
| 289 | 290 | ||
| 290 | static getAllowedTypes() { | 291 | static getAllowedTypes() { |
| @@ -298,11 +299,17 @@ export class ConnectionManagerRequestService { | |||
| 298 | * @param {string} profileId | 299 | * @param {string} profileId |
| 299 | * @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt | 300 | * @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt |
| 300 | * @param {number} maxTokens | 301 | * @param {number} maxTokens |
| 301 | * @param {{stream?: boolean, signal?: AbortSignal, extractData?: boolean, includePreset?: boolean, includeInstruct?: boolean}} custom - default values are true | 302 | * @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 AsyncGenerator | 309 | * @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 }; |
| 306 | 313 | ||
| 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: { |
| @@ -243,9 +243,14 @@ export function autoSelectInstructPreset(modelId) { | |||
| 243 | 243 | ||
| 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 | */ |
| 248 | export function getInstructStoppingSequences() { | 251 | export 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 string | 260 | // 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 string | 263 | // 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 string | 265 | // 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" setting | 269 | // 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() { | |||
| 270 | 275 | ||
| 271 | const result = []; | 276 | const result = []; |
| 272 | 277 | ||
| 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') || ''; | ||
| 281 | 287 | ||
| 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 | } |
| 294 | 300 | ||
| 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 | } |