SillyTavern/src/endpoints/prompt-converters.js

223 lines
9.2 KiB
JavaScript
Raw Normal View History

/**
* Convert a prompt from the ChatML objects to the format used by Claude.
* @param {object[]} messages Array of messages
* @param {boolean} addAssistantPostfix Add Assistant postfix.
* @param {string} addAssistantPrefill Add Assistant prefill after the assistant postfix.
2023-12-19 18:44:52 +01:00
* @param {boolean} withSysPromptSupport Indicates if the Claude model supports the system prompt format.
* @param {boolean} useSystemPrompt Indicates if the system prompt format should be used.
* @param {boolean} excludePrefixes Exlude Human/Assistant prefixes.
* @param {string} addSysHumanMsg Add Human message between system prompt and assistant.
* @returns {string} Prompt for Claude
* @copyright Prompt Conversion script taken from RisuAI by kwaroran (GPLv3).
*/
function convertClaudePrompt(messages, addAssistantPostfix, addAssistantPrefill, withSysPromptSupport, useSystemPrompt, addSysHumanMsg, excludePrefixes) {
2023-11-24 21:58:20 +01:00
2023-12-15 19:15:48 +01:00
//Prepare messages for claude.
2023-12-22 23:37:28 +01:00
//When 'Exclude Human/Assistant prefixes' checked, setting messages role to the 'system'(last message is exception).
2023-12-15 12:10:53 +01:00
if (messages.length > 0) {
if (excludePrefixes) {
2023-12-22 23:25:48 +01:00
messages.slice(0, -1).forEach(message => message.role = 'system');
} else {
messages[0].role = 'system';
}
2023-12-15 19:15:48 +01:00
//Add the assistant's message to the end of messages.
2023-12-15 12:10:53 +01:00
if (addAssistantPostfix) {
messages.push({
2023-12-15 19:15:48 +01:00
role: 'assistant',
2023-12-16 13:12:06 +01:00
content: addAssistantPrefill || '',
2023-12-15 12:10:53 +01:00
});
2023-11-21 21:11:26 +01:00
}
2023-12-15 19:15:48 +01:00
// Find the index of the first message with an assistant role and check for a "'user' role/Human:" before it.
2023-12-15 12:10:53 +01:00
let hasUser = false;
const firstAssistantIndex = messages.findIndex((message, i) => {
2023-12-16 13:12:06 +01:00
if (i >= 0 && (message.role === 'user' || message.content.includes('\n\nHuman: '))) {
2023-12-15 12:10:53 +01:00
hasUser = true;
}
return message.role === 'assistant' && i > 0;
});
2023-12-22 23:25:48 +01:00
// When 2.1+ and 'Use system prompt' checked, switches to the system prompt format by setting the first message's role to the 'system'.
2023-12-16 13:12:06 +01:00
// Inserts the human's message before the first the assistant one, if there are no such message or prefix found.
2023-12-19 18:44:52 +01:00
if (withSysPromptSupport && useSystemPrompt) {
2023-12-15 12:10:53 +01:00
messages[0].role = 'system';
if (firstAssistantIndex > 0 && addSysHumanMsg && !hasUser) {
2023-12-15 12:10:53 +01:00
messages.splice(firstAssistantIndex, 0, {
role: 'user',
content: addSysHumanMsg,
2023-12-15 12:10:53 +01:00
});
}
} else {
2023-12-15 19:15:48 +01:00
// Otherwise, use the default message format by setting the first message's role to 'user'(compatible with all claude models including 2.1.)
2023-12-15 12:10:53 +01:00
messages[0].role = 'user';
2023-12-15 19:58:03 +01:00
// Fix messages order for default message format when(messages > Context Size) by merging two messages with "\n\nHuman: " prefixes into one, before the first Assistant's message.
2023-12-22 23:37:28 +01:00
if (firstAssistantIndex > 0 && !excludePrefixes) {
2023-12-15 19:58:03 +01:00
messages[firstAssistantIndex - 1].role = firstAssistantIndex - 1 !== 0 && messages[firstAssistantIndex - 1].role === 'user' ? 'FixHumMsg' : messages[firstAssistantIndex - 1].role;
2023-12-15 12:10:53 +01:00
}
}
}
2023-12-18 01:32:25 +01:00
// Convert messages to the prompt.
let requestPrompt = messages.map((v, i) => {
2023-12-26 12:45:39 +01:00
// Set prefix according to the role. Also, when "Exclude Human/Assistant prefixes" is checked, names are added via the system prefix.
2023-12-15 19:58:03 +01:00
let prefix = {
2023-12-15 19:15:48 +01:00
'assistant': '\n\nAssistant: ',
'user': '\n\nHuman: ',
2023-12-22 23:25:48 +01:00
'system': i === 0 ? '' : v.name === 'example_assistant' ? '\n\nA: ' : v.name === 'example_user' ? '\n\nH: ' : excludePrefixes && v.name ? `\n\n${v.name}: ` : '\n\n',
2023-12-15 19:58:03 +01:00
'FixHumMsg': '\n\nFirst message: ',
}[v.role] ?? '';
// Claude doesn't support message names, so we'll just add them to the message content.
return `${prefix}${v.name && v.role !== 'system' ? `${v.name}: ` : ''}${v.content}`;
}).join('');
return requestPrompt;
}
2023-12-30 20:04:37 +01:00
/**
* Convert ChatML objects into working with Anthropic's new Messaging API.
* @param {object[]} messages Array of messages
* @param {string} prefillString User determined prefill string
* @param {boolean} useSysPrompt See if we want to use a system prompt
* @param {string} humanMsgFix Add Human message between system prompt and assistant.
2023-12-30 20:04:37 +01:00
*/
function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFix) {
2023-12-30 20:04:37 +01:00
let systemPrompt = '';
if (useSysPrompt) {
// Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array.
let i;
for (i = 0; i < messages.length; i++) {
if (messages[i].role !== 'system') {
break;
}
systemPrompt += `${messages[i].content}\n\n`;
2023-12-30 20:04:37 +01:00
}
messages.splice(0, i);
2023-12-30 20:04:37 +01:00
// Check if the first message in the array is of type user, if not, interject with humanMsgFix or a blank message.
if (messages.length > 0 && messages[0].role !== 'user') {
messages.unshift({
role: 'user',
content: humanMsgFix || '',
});
}
2023-12-30 20:04:37 +01:00
}
// Now replace all further messages that have the role 'system' with the role 'user'. (or all if we're not using one)
messages.forEach((message) => {
2023-12-30 20:04:37 +01:00
if (message.role === 'system') {
message.role = 'user';
}
});
// Since the messaging endpoint only supports user assistant roles in turns, we have to merge messages with the same role if they follow eachother
let mergedMessages = [];
messages.forEach((message) => {
if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role) {
mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content;
} else {
mergedMessages.push(message);
}
});
// Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling
if (prefillString) {
mergedMessages.push({
2023-12-30 20:04:37 +01:00
role: 'assistant',
content: prefillString,
2023-12-30 20:04:37 +01:00
});
}
return { messages: mergedMessages, systemPrompt: systemPrompt.trim() };
2023-12-30 20:04:37 +01:00
}
2023-12-14 19:05:27 +01:00
/**
* Convert a prompt from the ChatML objects to the format used by Google MakerSuite models.
* @param {object[]} messages Array of messages
* @param {string} model Model name
* @returns {object[]} Prompt for Google MakerSuite models
*/
2023-12-14 17:01:42 +01:00
function convertGooglePrompt(messages, model) {
// This is a 1x1 transparent PNG
const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
const contents = [];
let lastRole = '';
let currentText = '';
2023-12-14 16:28:54 +01:00
2023-12-14 17:01:42 +01:00
const isMultimodal = model === 'gemini-pro-vision';
2023-12-14 16:28:54 +01:00
if (isMultimodal) {
const combinedText = messages.map((message) => {
const role = message.role === 'assistant' ? 'MODEL: ' : 'USER: ';
return role + message.content;
}).join('\n\n').trim();
const imageEntry = messages.find((message) => message.content?.[1]?.image_url);
const imageData = imageEntry?.content?.[1]?.image_url?.data ?? PNG_PIXEL;
2023-12-14 16:28:54 +01:00
contents.push({
parts: [
{ text: combinedText },
{
inlineData: {
mimeType: 'image/png',
data: imageData,
2023-12-14 16:28:54 +01:00
},
},
],
role: 'user',
});
} else {
messages.forEach((message, index) => {
const role = message.role === 'assistant' ? 'model' : 'user';
if (lastRole === role) {
currentText += '\n\n' + message.content;
} else {
if (currentText !== '') {
contents.push({
parts: [{ text: currentText.trim() }],
role: lastRole,
});
}
currentText = message.content;
lastRole = role;
}
if (index === messages.length - 1) {
contents.push({
parts: [{ text: currentText.trim() }],
role: lastRole,
});
}
2023-12-14 16:28:54 +01:00
});
}
return contents;
}
/**
* Convert a prompt from the ChatML objects to the format used by Text Completion API.
* @param {object[]} messages Array of messages
* @returns {string} Prompt for Text Completion API
*/
function convertTextCompletionPrompt(messages) {
if (typeof messages === 'string') {
return messages;
}
const messageStrings = [];
messages.forEach(m => {
if (m.role === 'system' && m.name === undefined) {
messageStrings.push('System: ' + m.content);
}
else if (m.role === 'system' && m.name !== undefined) {
messageStrings.push(m.name + ': ' + m.content);
}
else {
messageStrings.push(m.role + ': ' + m.content);
}
});
return messageStrings.join('\n') + '\nassistant:';
}
module.exports = {
convertClaudePrompt,
2023-12-30 20:04:37 +01:00
convertClaudeMessages,
convertGooglePrompt,
convertTextCompletionPrompt,
2023-12-02 20:11:06 +01:00
};