Merge pull request #3191 from SillyTavern/group-join-examples Fixes to group join examples parsing

71d253032929d211036284c32899ca161cafd0c6

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

Signed
4 files changed, +150 -90Showing whitespace changes
public/scripts/group-chats.js+22 -2
@@ -276,6 +276,20 @@ export function getGroupMembers(groupId = selected_group) {
276276}
277277
278278/**
279+ * Retrieves the member names of a group. If the group is not selected, an empty array is returned.
280+ * @returns {string[]} An array of character names representing the members of the group.
281+ */
282+export function getGroupNames() {
283+ if (!selected_group) {
284+ return [];
285+ }
286+ const groupMembers = groups.find(x => x.id == selected_group)?.members;
287+ return Array.isArray(groupMembers)
288+ ? groupMembers.map(x => characters.find(y => y.avatar === x)?.name).filter(x => x)
289+ : [];
290+}
291+
292+/**
279293 * Finds the character ID for a group member.
280294 * @param {string} arg 0-based member index or character name
281295 * @returns {number} 0-based character ID
@@ -423,14 +437,20 @@ export function getGroupCharacterCards(groupId, characterId) {
423437 * @param {string} value Value to replace
424438 * @param {string} characterName Name of the character
425439 * @param {string} fieldName Name of the field
440+ * @param {function(string): string} [preprocess] Preprocess function
426441 * @returns {string} Prepared text
427442 * */
428443 function replaceAndPrepareForJoin(value, characterName, fieldName, preprocess = null) {
429444 value = value.trim();
430445 if (!value) {
431446 return '';
432447 }
433448
449+ // Run preprocess function
450+ if (typeof preprocess === 'function') {
451+ value = preprocess(value);
452+ }
453+
434454 // Prepare and replace prefixes
435455 const prefix = customBaseChatReplace(group.generation_mode_join_prefix, fieldName, characterName);
436456 const suffix = customBaseChatReplace(group.generation_mode_join_suffix, fieldName, characterName);
@@ -465,7 +485,7 @@ export function getGroupCharacterCards(groupId, characterId) {
465485 descriptions.push(replaceAndPrepareForJoin(character.description, character.name, 'Description'));
466486 personalities.push(replaceAndPrepareForJoin(character.personality, character.name, 'Personality'));
467487 scenarios.push(replaceAndPrepareForJoin(character.scenario, character.name, 'Scenario'));
468488 mesExamplesArray.push(replaceAndPrepareForJoin(character.mes_example, character.name, 'Example Messages', (x) => !x.startsWith('<START>') ? `<START>\n${x}` : x));
469489 }
470490
471491 const description = descriptions.filter(x => x.length).join('\n');
public/scripts/openai.js+13 -4
@@ -33,7 +33,7 @@ import {
3333 system_message_types,
3434 this_chid,
3535} from '../script.js';
3636import { getGroupNames, selected_group } from './group-chats.js';
3737
3838import {
3939 chatCompletionDefaultPrompts,
@@ -543,11 +543,15 @@ function setupChatCompletionPromptManager(openAiSettings) {
543543 * @returns {Message[]} Array of message objects
544544 */
545545export function parseExampleIntoIndividual(messageExampleString, appendNamesForGroup = true) {
546+ const groupBotNames = getGroupNames().map(name => `${name}:`);
547+
546548 let result = []; // array of msgs
547549 let tmp = messageExampleString.split('\n');
548550 let cur_msg_lines = [];
549551 let in_user = false;
550552 let in_bot = false;
553+ let botName = name2;
554+
551555 // DRY my cock and balls :)
552556 function add_msg(name, role, system_name) {
553557 // join different newlines (we split them by \n and join by \n)
@@ -571,10 +575,14 @@ export function parseExampleIntoIndividual(messageExampleString, appendNamesForG
571575 in_user = true;
572576 // we were in the bot mode previously, add the message
573577 if (in_bot) {
574578 add_msg(name2botName, 'system', 'example_assistant');
575579 }
576580 in_bot = false;
577581 } else if (cur_str.startsWith(name2 + ':') || groupBotNames.some(n => cur_str.startsWith(n))) {
582+ if (!cur_str.startsWith(name2 + ':') && groupBotNames.length) {
583+ botName = cur_str.split(':')[0];
584+ }
585+
578586 in_bot = true;
579587 // we were in the user mode previously, add the message
580588 if (in_user) {
@@ -589,7 +597,7 @@ export function parseExampleIntoIndividual(messageExampleString, appendNamesForG
589597 if (in_user) {
590598 add_msg(name1, 'system', 'example_user');
591599 } else if (in_bot) {
592600 add_msg(name2botName, 'system', 'example_assistant');
593601 }
594602 return result;
595603}
@@ -1866,6 +1874,7 @@ async function sendOpenAIRequest(type, messages, signal) {
18661874 'n': canMultiSwipe ? oai_settings.n : undefined,
18671875 'user_name': name1,
18681876 'char_name': name2,
1877+ 'group_names': getGroupNames(),
18691878 };
18701879
18711880 // Empty array will produce a validation error
src/endpoints/backends/chat-completions.js+12 -13
@@ -27,6 +27,7 @@ import {
2727 mergeMessages,
2828 cachingAtDepthForOpenRouterClaude,
2929 cachingAtDepthForClaude,
30+ getPromptNames,
3031} from '../../prompt-converters.js';
3132
3233import { readSecret, SECRET_KEYS } from '../secrets.js';
@@ -55,17 +56,16 @@ const API_NANOGPT = 'https://nano-gpt.com/api/v1';
5556 * Applies a post-processing step to the generated messages.
5657 * @param {object[]} messages Messages to post-process
5758 * @param {string} type Prompt conversion type
58- * @param {string} charName Character name
59+ * @param {import('../../prompt-converters.js').PromptNames} names Prompt names
59- * @param {string} userName User name
6060 * @returns
6161 */
6262function postProcessPrompt(messages, type, charName, userNamenames) {
6363 switch (type) {
6464 case 'merge':
6565 case 'claude':
6666 return mergeMessages(messages, charName, userNamenames, false);
6767 case 'strict':
6868 return mergeMessages(messages, charName, userNamenames, true);
6969 default:
7070 return messages;
7171 }
@@ -101,7 +101,7 @@ async function sendClaudeRequest(request, response) {
101101 const additionalHeaders = {};
102102 const useTools = request.body.model.startsWith('claude-3') && Array.isArray(request.body.tools) && request.body.tools.length > 0;
103103 const useSystemPrompt = (request.body.model.startsWith('claude-2') || request.body.model.startsWith('claude-3')) && request.body.claude_use_sysprompt;
104104 const convertedPrompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, useSystemPrompt, useTools, request.body.char_name, getPromptNames(request.body.user_name));
105105 // Add custom stop sequences
106106 const stopSequences = [];
107107 if (Array.isArray(request.body.stop)) {
@@ -284,7 +284,7 @@ async function sendMakerSuiteRequest(request, response) {
284284 model.startsWith('gemini-exp')
285285 ) && request.body.use_makersuite_sysprompt;
286286
287287 const prompt = convertGooglePrompt(request.body.messages, model, should_use_system_prompt, request.body.char_name, getPromptNames(request.body.user_name));
288288 let body = {
289289 contents: prompt.contents,
290290 safetySettings: GEMINI_SAFETY,
@@ -384,7 +384,7 @@ async function sendAI21Request(request, response) {
384384 request.socket.on('close', function () {
385385 controller.abort();
386386 });
387387 const convertedPrompt = convertAI21Messages(request.body.messages, request.body.char_name, getPromptNames(request.body.user_name));
388388 const body = {
389389 messages: convertedPrompt,
390390 model: request.body.model,
@@ -447,7 +447,7 @@ async function sendMistralAIRequest(request, response) {
447447 }
448448
449449 try {
450450 const messages = convertMistralMessages(request.body.messages, request.body.char_name, getPromptNames(request.body.user_name));
451451 const controller = new AbortController();
452452 request.socket.removeAllListeners('close');
453453 request.socket.on('close', function () {
@@ -528,7 +528,7 @@ async function sendCohereRequest(request, response) {
528528 }
529529
530530 try {
531531 const convertedHistory = convertCohereMessages(request.body.messages, request.body.char_name, getPromptNames(request.body.user_name));
532532 const tools = [];
533533
534534 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
@@ -886,15 +886,14 @@ router.post('/generate', jsonParser, function (request, response) {
886886 request.body.messages = postProcessPrompt(
887887 request.body.messages,
888888 request.body.custom_prompt_post_processing,
889- request.body.char_name,
889+ getPromptNames(request));
890- request.body.user_name);
891890 }
892891 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.PERPLEXITY) {
893892 apiUrl = API_PERPLEXITY;
894893 apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY);
895894 headers = {};
896895 bodyParams = {};
897896 request.body.messages = postProcessPrompt(request.body.messages, 'strict', request.body.char_name, getPromptNames(request.body.user_name));
898897 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) {
899898 apiUrl = API_GROQ;
900899 apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);
src/prompt-converters.js+103 -71
@@ -4,6 +4,30 @@ import { getConfigValue } from './util.js';
44const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get started.');
55
66/**
7+ * @typedef {object} PromptNames
8+ * @property {string} charName Character name
9+ * @property {string} userName User name
10+ * @property {string[]} groupNames Group member names
11+ * @property {function(string): boolean} startsWithGroupName Check if a message starts with a group name
12+ */
13+
14+/**
15+ * Extracts the character name, user name, and group member names from the request.
16+ * @param {import('express').Request} request Express request object
17+ * @returns {PromptNames} Prompt names
18+ */
19+export function getPromptNames(request) {
20+ return {
21+ charName: String(request.body.char_name || ''),
22+ userName: String(request.body.user_name || ''),
23+ groupNames: Array.isArray(request.body.group_names) ? request.body.group_names.map(String) : [],
24+ startsWithGroupName: function (message) {
25+ return this.groupNames.some(name => message.startsWith(`${name}: `));
26+ },
27+ };
28+}
29+
30+/**
731 * Convert a prompt from the ChatML objects to the format used by Claude.
832 * Mainly deprecated. Only used for counting tokens.
933 * @param {object[]} messages Array of messages
@@ -91,10 +115,10 @@ export function convertClaudePrompt(messages, addAssistantPostfix, addAssistantP
91115 * @param {string} prefillString User determined prefill string
92116 * @param {boolean} useSysPrompt See if we want to use a system prompt
93117 * @param {boolean} useTools See if we want to use tools
94118 * @param {stringPromptNames} charName Characternames namePrompt names
95- * @param {string} userName User name
119+ * @returns {{messages: object[], systemPrompt: object[]}} Prompt for Anthropic
96120 */
97121export function convertClaudeMessages(messages, prefillString, useSysPrompt, useTools, charName, userNamenames) {
98122 let systemPrompt = [];
99123 if (useSysPrompt) {
100124 // Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array.
@@ -104,14 +128,14 @@ export function convertClaudeMessages(messages, prefillString, useSysPrompt, use
104128 break;
105129 }
106130 // Append example names if not already done by the frontend (e.g. for group chats).
107131 if (names.userName && messages[i].name === 'example_user') {
108132 if (!messages[i].content.startsWith(`${names.userName}: `)) {
109133 messages[i].content = `${names.userName}: ${messages[i].content}`;
110134 }
111135 }
112136 if (names.charName && messages[i].name === 'example_assistant') {
113137 if (!messages[i].content.startsWith(`${names.charName}: `) && !names.startsWithGroupName(messages[i].content)) {
114138 messages[i].content = `${names.charName}: ${messages[i].content}`;
115139 }
116140 }
117141 systemPrompt.push({ type: 'text', text: messages[i].content });
@@ -151,11 +175,15 @@ export function convertClaudeMessages(messages, prefillString, useSysPrompt, use
151175 }
152176
153177 if (message.role === 'system') {
154178 if (names.userName && message.name === 'example_user') {
155179 if (!message.content = .startsWith(`${names.userName}: ${message.content}`;)) {
180+ message.content = `${names.userName}: ${message.content}`;
181+ }
182+ }
183+ if (names.charName && message.name === 'example_assistant') {
184+ if (!message.content.startsWith(`${names.charName}: `) && !names.startsWithGroupName(message.content)) {
185+ message.content = `${names.charName}: ${message.content}`;
156186 }
157- if (charName && message.name === 'example_assistant') {
158- message.content = `${charName}: ${message.content}`;
159187 }
160188 message.role = 'user';
161189
@@ -274,11 +302,10 @@ export function convertClaudeMessages(messages, prefillString, useSysPrompt, use
274302/**
275303 * Convert a prompt from the ChatML objects to the format used by Cohere.
276304 * @param {object[]} messages Array of messages
277305 * @param {stringPromptNames} charName Characternames namePrompt names
278- * @param {string} userName User name
279306 * @returns {{chatHistory: object[]}} Prompt for Cohere
280307 */
281308export function convertCohereMessages(messages, charName = '', userName = ''names) {
282309 if (messages.length === 0) {
283310 messages.unshift({
284311 role: 'user',
@@ -299,13 +326,13 @@ export function convertCohereMessages(messages, charName = '', userName = '') {
299326 // No names support (who would've thought)
300327 if (msg.name) {
301328 if (msg.role == 'system' && msg.name == 'example_assistant') {
302329 if (names.charName && !msg.content.startsWith(`${names.charName}: `) && !names.startsWithGroupName(msg.content)) {
303330 msg.content = `${names.charName}: ${msg.content}`;
304331 }
305332 }
306333 if (msg.role == 'system' && msg.name == 'example_user') {
307334 if (names.userName && !msg.content.startsWith(`${names.userName}: `)) {
308335 msg.content = `${names.userName}: ${msg.content}`;
309336 }
310337 }
311338 if (msg.role !== 'system' && !msg.content.startsWith(`${msg.name}: `)) {
@@ -328,12 +355,10 @@ export function convertCohereMessages(messages, charName = '', userName = '') {
328355 * @param {object[]} messages Array of messages
329356 * @param {string} model Model name
330357 * @param {boolean} useSysPrompt Use system prompt
331358 * @param {stringPromptNames} charNamenames CharacterPrompt namenames
332- * @param {string} userName User name
333359 * @returns {{contents: *[], system_instruction: {parts: {text: string}}}} Prompt for Google MakerSuite models
334360 */
335361export function convertGooglePrompt(messages, model, useSysPrompt = false, charName = '', userName = ''names) {
336-
337362 const visionSupportedModels = [
338363 'gemini-2.0-flash-exp',
339364 'gemini-1.5-flash',
@@ -356,20 +381,19 @@ export function convertGooglePrompt(messages, model, useSysPrompt = false, charN
356381 ];
357382
358383 const isMultimodal = visionSupportedModels.includes(model);
359- let hasImage = false;
360384
361385 let sys_prompt = '';
362386 if (useSysPrompt) {
363387 while (messages.length > 1 && messages[0].role === 'system') {
364388 // Append example names if not already done by the frontend (e.g. for group chats).
365389 if (names.userName && messages[0].name === 'example_user') {
366390 if (!messages[0].content.startsWith(`${names.userName}: `)) {
367391 messages[0].content = `${names.userName}: ${messages[0].content}`;
368392 }
369393 }
370394 if (names.charName && messages[0].name === 'example_assistant') {
371395 if (!messages[0].content.startsWith(`${names.charName}: `) && !names.startsWithGroupName(messages[0].content)) {
372396 messages[0].content = `${names.charName}: ${messages[0].content}`;
373397 }
374398 }
375399 sys_prompt += `${messages[0].content}\n\n`;
@@ -388,33 +412,37 @@ export function convertGooglePrompt(messages, model, useSysPrompt = false, charN
388412 message.role = 'model';
389413 }
390414
415+ // Convert the content to an array of parts
416+ if (!Array.isArray(message.content)) {
417+ message.content = [{ type: 'text', text: String(message.content ?? '') }];
418+ }
419+
391420 // similar story as claude
392421 if (message.name) {
393- if (userName && message.name === 'example_user') {
422+ message.content.forEach((part) => {
394- message.name = userName;
423+ if (part.type !== 'text') {
424+ return;
395425 }
396426 if (charName && message.name === 'example_assistantexample_user') {
397- message.name = charName;
427+ if (names.userName && !part.text.startsWith(`${names.userName}: `)) {
428+ part.text = `${names.userName}: ${part.text}`;
398429 }
399-
430+ } else if (message.name === 'example_assistant') {
400- if (Array.isArray(message.content)) {
431+ if (names.charName && !part.text.startsWith(`${names.charName}: `) && !names.startsWithGroupName(part.text)) {
401- if (!message.content[0].text.startsWith(`${message.name}: `)) {
432+ part.text = `${names.charName}: ${part.text}`;
402- message.content[0].text = `${message.name}: ${message.content[0].text}`;
403433 }
404434 } else {
405435 if (!messagepart.contenttext.startsWith(`${message.name}: `)) {
406436 message part.contenttext = `${message.name}: ${messagepart.contenttext}`;
407437 }
408438 }
439+ });
409440
410441 delete message.name;
411442 }
412443
413444 //create the prompt parts
414445 const parts = [];
415- if (typeof message.content === 'string') {
416- parts.push({ text: message.content });
417- } else if (Array.isArray(message.content)) {
418446 message.content.forEach((part) => {
419447 if (part.type === 'text') {
420448 parts.push({ text: part.text });
@@ -427,14 +455,19 @@ export function convertGooglePrompt(messages, model, useSysPrompt = false, charN
427455 data: base64Data,
428456 },
429457 });
430- hasImage = true;
431458 }
432459 });
433- }
434460
435461 // merge consecutive messages with the same role
436462 if (index > 0 && message.role === contents[contents.length - 1].role) {
437- contents[contents.length - 1].parts[0].text += '\n\n' + parts[0].text;
463+ parts.forEach((part) => {
464+ if (part.text) {
465+ contents[contents.length - 1].parts[0].text += '\n\n' + part.text;
466+ }
467+ if (part.inlineData) {
468+ contents[contents.length - 1].parts.push(part);
469+ }
470+ });
438471 } else {
439472 contents.push({
440473 role: message.role,
@@ -449,10 +482,10 @@ export function convertGooglePrompt(messages, model, useSysPrompt = false, charN
449482/**
450483 * Convert AI21 prompt. Classic: system message squash, user/assistant message merge.
451484 * @param {object[]} messages Array of messages
452485 * @param {stringPromptNames} charNamenames CharacterPrompt namenames
453486 * @paramreturns {stringobject[]} userNamePrompt Userfor nameAI21
454487 */
455488export function convertAI21Messages(messages, charName = '', userName = ''names) {
456489 if (!Array.isArray(messages)) {
457490 return [];
458491 }
@@ -465,14 +498,14 @@ export function convertAI21Messages(messages, charName = '', userName = '') {
465498 break;
466499 }
467500 // Append example names if not already done by the frontend (e.g. for group chats).
468501 if (names.userName && messages[i].name === 'example_user') {
469502 if (!messages[i].content.startsWith(`${names.userName}: `)) {
470503 messages[i].content = `${names.userName}: ${messages[i].content}`;
471504 }
472505 }
473506 if (names.charName && messages[i].name === 'example_assistant') {
474507 if (!messages[i].content.startsWith(`${names.charName}: `) && !names.startsWithGroupName(messages[i].content)) {
475508 messages[i].content = `${names.charName}: ${messages[i].content}`;
476509 }
477510 }
478511 systemPrompt += `${messages[i].content}\n\n`;
@@ -521,10 +554,10 @@ export function convertAI21Messages(messages, charName = '', userName = '') {
521554/**
522555 * Convert a prompt from the ChatML objects to the format used by MistralAI.
523556 * @param {object[]} messages Array of messages
524557 * @param {stringPromptNames} charNamenames CharacterPrompt namenames
525558 * @paramreturns {stringobject[]} userNamePrompt Userfor nameMistralAI
526559 */
527560export function convertMistralMessages(messages, charName = '', userName = ''names) {
528561 if (!Array.isArray(messages)) {
529562 return [];
530563 }
@@ -549,15 +582,15 @@ export function convertMistralMessages(messages, charName = '', userName = '') {
549582 msg.tool_call_id = sanitizeToolId(msg.tool_call_id);
550583 }
551584 if (msg.role === 'system' && msg.name === 'example_assistant') {
552585 if (names.charName && !msg.content.startsWith(`${names.charName}: `) && !names.startsWithGroupName(msg.content)) {
553586 msg.content = `${names.charName}: ${msg.content}`;
554587 }
555588 delete msg.name;
556589 }
557590
558591 if (msg.role === 'system' && msg.name === 'example_user') {
559592 if (names.userName && !msg.content.startsWith(`${names.userName}: `)) {
560593 msg.content = `${names.userName}: ${msg.content}`;
561594 }
562595 delete msg.name;
563596 }
@@ -603,12 +636,11 @@ export function convertMistralMessages(messages, charName = '', userName = '') {
603636/**
604637 * Merge messages with the same consecutive role, removing names if they exist.
605638 * @param {any[]} messages Messages to merge
606639 * @param {stringPromptNames} charNamenames CharacterPrompt namenames
607- * @param {string} userName User name
608640 * @param {boolean} strict Enable strict mode: only allow one system message at the start, force user first message
609641 * @returns {any[]} Merged messages
610642 */
611643export function mergeMessages(messages, charName, userNamenames, strict) {
612644 let mergedMessages = [];
613645
614646 /** @type {Map<string,object>} */
@@ -636,13 +668,13 @@ export function mergeMessages(messages, charName, userName, strict) {
636668 message.content = text;
637669 }
638670 if (message.role === 'system' && message.name === 'example_assistant') {
639671 if (names.charName && !message.content.startsWith(`${names.charName}: `) && !names.startsWithGroupName(message.content)) {
640672 message.content = `${names.charName}: ${message.content}`;
641673 }
642674 }
643675 if (message.role === 'system' && message.name === 'example_user') {
644676 if (names.userName && !message.content.startsWith(`${names.userName}: `)) {
645677 message.content = `${names.userName}: ${message.content}`;
646678 }
647679 }
648680 if (message.name && message.role !== 'system') {
@@ -716,7 +748,7 @@ export function mergeMessages(messages, charName, userName, strict) {
716748 mergedMessages.unshift({ role: 'user', content: PROMPT_PLACEHOLDER });
717749 }
718750 }
719751 return mergeMessages(mergedMessages, charName, userNamenames, false);
720752 }
721753
722754 return mergedMessages;