533 lines
21 KiB
JavaScript
533 lines
21 KiB
JavaScript
require('./polyfill.js');
|
|
|
|
/**
|
|
* Convert a prompt from the ChatML objects to the format used by Claude.
|
|
* Mainly deprecated. Only used for counting tokens.
|
|
* @param {object[]} messages Array of messages
|
|
* @param {boolean} addAssistantPostfix Add Assistant postfix.
|
|
* @param {string} addAssistantPrefill Add Assistant prefill after the assistant postfix.
|
|
* @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) {
|
|
|
|
//Prepare messages for claude.
|
|
//When 'Exclude Human/Assistant prefixes' checked, setting messages role to the 'system'(last message is exception).
|
|
if (messages.length > 0) {
|
|
if (excludePrefixes) {
|
|
messages.slice(0, -1).forEach(message => message.role = 'system');
|
|
} else {
|
|
messages[0].role = 'system';
|
|
}
|
|
//Add the assistant's message to the end of messages.
|
|
if (addAssistantPostfix) {
|
|
messages.push({
|
|
role: 'assistant',
|
|
content: addAssistantPrefill || '',
|
|
});
|
|
}
|
|
// Find the index of the first message with an assistant role and check for a "'user' role/Human:" before it.
|
|
let hasUser = false;
|
|
const firstAssistantIndex = messages.findIndex((message, i) => {
|
|
if (i >= 0 && (message.role === 'user' || message.content.includes('\n\nHuman: '))) {
|
|
hasUser = true;
|
|
}
|
|
return message.role === 'assistant' && i > 0;
|
|
});
|
|
// When 2.1+ and 'Use system prompt' checked, switches to the system prompt format by setting the first message's role to the 'system'.
|
|
// Inserts the human's message before the first the assistant one, if there are no such message or prefix found.
|
|
if (withSysPromptSupport && useSystemPrompt) {
|
|
messages[0].role = 'system';
|
|
if (firstAssistantIndex > 0 && addSysHumanMsg && !hasUser) {
|
|
messages.splice(firstAssistantIndex, 0, {
|
|
role: 'user',
|
|
content: addSysHumanMsg,
|
|
});
|
|
}
|
|
} else {
|
|
// Otherwise, use the default message format by setting the first message's role to 'user'(compatible with all claude models including 2.1.)
|
|
messages[0].role = 'user';
|
|
// 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.
|
|
if (firstAssistantIndex > 0 && !excludePrefixes) {
|
|
messages[firstAssistantIndex - 1].role = firstAssistantIndex - 1 !== 0 && messages[firstAssistantIndex - 1].role === 'user' ? 'FixHumMsg' : messages[firstAssistantIndex - 1].role;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert messages to the prompt.
|
|
let requestPrompt = messages.map((v, i) => {
|
|
// Set prefix according to the role. Also, when "Exclude Human/Assistant prefixes" is checked, names are added via the system prefix.
|
|
let prefix = {
|
|
'assistant': '\n\nAssistant: ',
|
|
'user': '\n\nHuman: ',
|
|
'system': i === 0 ? '' : v.name === 'example_assistant' ? '\n\nA: ' : v.name === 'example_user' ? '\n\nH: ' : excludePrefixes && v.name ? `\n\n${v.name}: ` : '\n\n',
|
|
'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;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* @param {string} charName Character name
|
|
* @param {string} userName User name
|
|
*/
|
|
function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFix, charName = '', userName = '') {
|
|
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;
|
|
}
|
|
// Append example names if not already done by the frontend (e.g. for group chats).
|
|
if (userName && messages[i].name === 'example_user') {
|
|
if (!messages[i].content.startsWith(`${userName}: `)) {
|
|
messages[i].content = `${userName}: ${messages[i].content}`;
|
|
}
|
|
}
|
|
if (charName && messages[i].name === 'example_assistant') {
|
|
if (!messages[i].content.startsWith(`${charName}: `)) {
|
|
messages[i].content = `${charName}: ${messages[i].content}`;
|
|
}
|
|
}
|
|
systemPrompt += `${messages[i].content}\n\n`;
|
|
}
|
|
|
|
messages.splice(0, i);
|
|
|
|
// Check if the first message in the array is of type user, if not, interject with humanMsgFix or a blank message.
|
|
// Also prevents erroring out if the messages array is empty.
|
|
if (messages.length === 0 || (messages.length > 0 && messages[0].role !== 'user')) {
|
|
messages.unshift({
|
|
role: 'user',
|
|
content: humanMsgFix || '[Start a new chat]',
|
|
});
|
|
}
|
|
}
|
|
// 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) => {
|
|
if (message.role === 'system') {
|
|
if (userName && message.name === 'example_user') {
|
|
message.content = `${userName}: ${message.content}`;
|
|
}
|
|
if (charName && message.name === 'example_assistant') {
|
|
message.content = `${charName}: ${message.content}`;
|
|
}
|
|
message.role = 'user';
|
|
}
|
|
});
|
|
|
|
// Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling
|
|
if (prefillString) {
|
|
messages.push({
|
|
role: 'assistant',
|
|
content: prefillString.trimEnd(),
|
|
});
|
|
}
|
|
|
|
// Since the messaging endpoint only supports user assistant roles in turns, we have to merge messages with the same role if they follow eachother
|
|
// Also handle multi-modality, holy slop.
|
|
let mergedMessages = [];
|
|
messages.forEach((message) => {
|
|
const imageEntry = message.content?.[1]?.image_url;
|
|
const imageData = imageEntry?.url;
|
|
const mimeType = imageData?.split(';')?.[0].split(':')?.[1];
|
|
const base64Data = imageData?.split(',')?.[1];
|
|
|
|
// Take care of name properties since claude messages don't support them
|
|
if (message.name) {
|
|
if (Array.isArray(message.content)) {
|
|
message.content[0].text = `${message.name}: ${message.content[0].text}`;
|
|
} else {
|
|
message.content = `${message.name}: ${message.content}`;
|
|
}
|
|
delete message.name;
|
|
}
|
|
|
|
if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role) {
|
|
if (Array.isArray(message.content)) {
|
|
if (Array.isArray(mergedMessages[mergedMessages.length - 1].content)) {
|
|
mergedMessages[mergedMessages.length - 1].content[0].text += '\n\n' + message.content[0].text;
|
|
} else {
|
|
mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content[0].text;
|
|
}
|
|
} else {
|
|
if (Array.isArray(mergedMessages[mergedMessages.length - 1].content)) {
|
|
mergedMessages[mergedMessages.length - 1].content[0].text += '\n\n' + message.content;
|
|
} else {
|
|
mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content;
|
|
}
|
|
}
|
|
} else {
|
|
mergedMessages.push(message);
|
|
}
|
|
if (imageData) {
|
|
mergedMessages[mergedMessages.length - 1].content = [
|
|
{ type: 'text', text: mergedMessages[mergedMessages.length - 1].content[0]?.text || mergedMessages[mergedMessages.length - 1].content },
|
|
{
|
|
type: 'image', source: {
|
|
type: 'base64',
|
|
media_type: mimeType,
|
|
data: base64Data,
|
|
},
|
|
},
|
|
];
|
|
}
|
|
});
|
|
|
|
return { messages: mergedMessages, systemPrompt: systemPrompt.trim() };
|
|
}
|
|
|
|
/**
|
|
* Convert a prompt from the ChatML objects to the format used by Cohere.
|
|
* @param {object[]} messages Array of messages
|
|
* @param {string} charName Character name
|
|
* @param {string} userName User name
|
|
* @returns {{systemPrompt: string, chatHistory: object[], userPrompt: string}} Prompt for Cohere
|
|
*/
|
|
function convertCohereMessages(messages, charName = '', userName = '') {
|
|
const roleMap = {
|
|
'system': 'SYSTEM',
|
|
'user': 'USER',
|
|
'assistant': 'CHATBOT',
|
|
};
|
|
const placeholder = '[Start a new chat]';
|
|
let systemPrompt = '';
|
|
|
|
// 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;
|
|
}
|
|
// Append example names if not already done by the frontend (e.g. for group chats).
|
|
if (userName && messages[i].name === 'example_user') {
|
|
if (!messages[i].content.startsWith(`${userName}: `)) {
|
|
messages[i].content = `${userName}: ${messages[i].content}`;
|
|
}
|
|
}
|
|
if (charName && messages[i].name === 'example_assistant') {
|
|
if (!messages[i].content.startsWith(`${charName}: `)) {
|
|
messages[i].content = `${charName}: ${messages[i].content}`;
|
|
}
|
|
}
|
|
systemPrompt += `${messages[i].content}\n\n`;
|
|
}
|
|
|
|
messages.splice(0, i);
|
|
|
|
if (messages.length === 0) {
|
|
messages.unshift({
|
|
role: 'user',
|
|
content: placeholder,
|
|
});
|
|
}
|
|
|
|
const lastNonSystemMessageIndex = messages.findLastIndex(msg => msg.role === 'user' || msg.role === 'assistant');
|
|
const userPrompt = messages.slice(lastNonSystemMessageIndex).map(msg => msg.content).join('\n\n') || placeholder;
|
|
|
|
const chatHistory = messages.slice(0, lastNonSystemMessageIndex).map(msg => {
|
|
return {
|
|
role: roleMap[msg.role] || 'USER',
|
|
message: msg.content,
|
|
};
|
|
});
|
|
|
|
return { systemPrompt: systemPrompt.trim(), chatHistory, userPrompt };
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @param {boolean} useSysPrompt Use system prompt
|
|
* @param {string} charName Character name
|
|
* @param {string} userName User name
|
|
* @returns {{contents: *[], system_instruction: {parts: {text: string}}}} Prompt for Google MakerSuite models
|
|
*/
|
|
function convertGooglePrompt(messages, model, useSysPrompt = false, charName = '', userName = '') {
|
|
// This is a 1x1 transparent PNG
|
|
const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
|
|
|
|
const visionSupportedModels = [
|
|
'gemini-1.5-flash-latest',
|
|
'gemini-1.5-pro-latest',
|
|
'gemini-1.0-pro-vision-latest',
|
|
'gemini-pro-vision',
|
|
];
|
|
|
|
const dummyRequiredModels = [
|
|
'gemini-1.0-pro-vision-latest',
|
|
'gemini-pro-vision',
|
|
];
|
|
|
|
const isMultimodal = visionSupportedModels.includes(model);
|
|
let hasImage = false;
|
|
|
|
let sys_prompt = '';
|
|
if (useSysPrompt) {
|
|
while (messages.length > 1 && messages[0].role === 'system') {
|
|
// Append example names if not already done by the frontend (e.g. for group chats).
|
|
if (userName && messages[0].name === 'example_user') {
|
|
if (!messages[0].content.startsWith(`${userName}: `)) {
|
|
messages[0].content = `${userName}: ${messages[0].content}`;
|
|
}
|
|
}
|
|
if (charName && messages[0].name === 'example_assistant') {
|
|
if (!messages[0].content.startsWith(`${charName}: `)) {
|
|
messages[0].content = `${charName}: ${messages[0].content}`;
|
|
}
|
|
}
|
|
sys_prompt += `${messages[0].content}\n\n`;
|
|
messages.shift();
|
|
}
|
|
}
|
|
|
|
const system_instruction = { parts: { text: sys_prompt.trim() } };
|
|
|
|
const contents = [];
|
|
messages.forEach((message, index) => {
|
|
// fix the roles
|
|
if (message.role === 'system') {
|
|
message.role = 'user';
|
|
} else if (message.role === 'assistant') {
|
|
message.role = 'model';
|
|
}
|
|
|
|
// similar story as claude
|
|
if (message.name) {
|
|
if (Array.isArray(message.content)) {
|
|
message.content[0].text = `${message.name}: ${message.content[0].text}`;
|
|
} else {
|
|
message.content = `${message.name}: ${message.content}`;
|
|
}
|
|
delete message.name;
|
|
}
|
|
|
|
//create the prompt parts
|
|
const parts = [];
|
|
if (typeof message.content === 'string') {
|
|
parts.push({ text: message.content });
|
|
} else if (Array.isArray(message.content)) {
|
|
message.content.forEach((part) => {
|
|
if (part.type === 'text') {
|
|
parts.push({ text: part.text });
|
|
} else if (part.type === 'image_url' && isMultimodal) {
|
|
parts.push({
|
|
inlineData: {
|
|
mimeType: 'image/png',
|
|
data: part.image_url.url,
|
|
},
|
|
});
|
|
hasImage = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// merge consecutive messages with the same role
|
|
if (index > 0 && message.role === contents[contents.length - 1].role) {
|
|
contents[contents.length - 1].parts[0].text += '\n\n' + parts[0].text;
|
|
} else {
|
|
contents.push({
|
|
role: message.role,
|
|
parts: parts,
|
|
});
|
|
}
|
|
});
|
|
|
|
// pro 1.5 doesn't require a dummy image to be attached, other vision models do
|
|
if (isMultimodal && dummyRequiredModels.includes(model) && !hasImage) {
|
|
contents[0].parts.push({
|
|
inlineData: {
|
|
mimeType: 'image/png',
|
|
data: PNG_PIXEL,
|
|
},
|
|
});
|
|
}
|
|
|
|
return { contents: contents, system_instruction: system_instruction };
|
|
}
|
|
|
|
/**
|
|
* Convert a prompt from the ChatML objects to the format used by MistralAI.
|
|
* @param {object[]} messages Array of messages
|
|
* @param {string} model Model name
|
|
* @param {string} charName Character name
|
|
* @param {string} userName User name
|
|
*/
|
|
function convertMistralMessages(messages, model, charName = '', userName = '') {
|
|
if (!Array.isArray(messages)) {
|
|
return [];
|
|
}
|
|
|
|
//large seems to be throwing a 500 error if we don't make the first message a user role, most likely a bug since the other models won't do this
|
|
if (model.includes('large')) {
|
|
messages[0].role = 'user';
|
|
}
|
|
|
|
//must send a user role as last message
|
|
const lastMsg = messages[messages.length - 1];
|
|
if (messages.length > 0 && lastMsg && (lastMsg.role === 'system' || lastMsg.role === 'assistant')) {
|
|
if (lastMsg.role === 'assistant' && lastMsg.name) {
|
|
lastMsg.content = lastMsg.name + ': ' + lastMsg.content;
|
|
} else if (lastMsg.role === 'system') {
|
|
lastMsg.content = '[INST] ' + lastMsg.content + ' [/INST]';
|
|
}
|
|
lastMsg.role = 'user';
|
|
}
|
|
|
|
//system prompts can be stacked at the start, but any futher sys prompts after the first user/assistant message will break the model
|
|
let encounteredNonSystemMessage = false;
|
|
messages.forEach(msg => {
|
|
if (msg.role === 'system' && msg.name === 'example_assistant') {
|
|
if (charName) {
|
|
msg.content = `${charName}: ${msg.content}`;
|
|
}
|
|
delete msg.name;
|
|
}
|
|
|
|
if (msg.role === 'system' && msg.name === 'example_user') {
|
|
if (userName) {
|
|
msg.content = `${userName}: ${msg.content}`;
|
|
}
|
|
delete msg.name;
|
|
}
|
|
|
|
if (msg.name) {
|
|
msg.content = `${msg.name}: ${msg.content}`;
|
|
delete msg.name;
|
|
}
|
|
|
|
if ((msg.role === 'user' || msg.role === 'assistant') && !encounteredNonSystemMessage) {
|
|
encounteredNonSystemMessage = true;
|
|
}
|
|
|
|
if (encounteredNonSystemMessage && msg.role === 'system') {
|
|
msg.role = 'user';
|
|
//unsure if the instruct version is what they've deployed on their endpoints and if this will make a difference or not.
|
|
//it should be better than just sending the message as a user role without context though
|
|
msg.content = '[INST] ' + msg.content + ' [/INST]';
|
|
}
|
|
});
|
|
|
|
return messages;
|
|
}
|
|
|
|
/**
|
|
* 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:';
|
|
}
|
|
|
|
/**
|
|
* Convert OpenAI Chat Completion tools to the format used by Cohere.
|
|
* @param {object[]} tools OpenAI Chat Completion tool definitions
|
|
*/
|
|
function convertCohereTools(tools) {
|
|
if (!Array.isArray(tools) || tools.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const jsonSchemaToPythonTypes = {
|
|
'string': 'str',
|
|
'number': 'float',
|
|
'integer': 'int',
|
|
'boolean': 'bool',
|
|
'array': 'list',
|
|
'object': 'dict',
|
|
};
|
|
|
|
const cohereTools = [];
|
|
|
|
for (const tool of tools) {
|
|
if (tool?.type !== 'function') {
|
|
console.log(`Unsupported tool type: ${tool.type}`);
|
|
continue;
|
|
}
|
|
|
|
const name = tool?.function?.name;
|
|
const description = tool?.function?.description;
|
|
const properties = tool?.function?.parameters?.properties;
|
|
const required = tool?.function?.parameters?.required;
|
|
const parameters = {};
|
|
|
|
if (!name) {
|
|
console.log('Tool name is missing');
|
|
continue;
|
|
}
|
|
|
|
if (!description) {
|
|
console.log('Tool description is missing');
|
|
}
|
|
|
|
if (!properties || typeof properties !== 'object') {
|
|
console.log(`No properties found for tool: ${tool?.function?.name}`);
|
|
continue;
|
|
}
|
|
|
|
for (const property in properties) {
|
|
const parameterDefinition = properties[property];
|
|
const description = parameterDefinition.description || (parameterDefinition.enum ? JSON.stringify(parameterDefinition.enum) : '');
|
|
const type = jsonSchemaToPythonTypes[parameterDefinition.type] || 'str';
|
|
const isRequired = Array.isArray(required) && required.includes(property);
|
|
parameters[property] = {
|
|
description: description,
|
|
type: type,
|
|
required: isRequired,
|
|
};
|
|
}
|
|
|
|
const cohereTool = {
|
|
name: tool.function.name,
|
|
description: tool.function.description,
|
|
parameter_definitions: parameters,
|
|
};
|
|
|
|
cohereTools.push(cohereTool);
|
|
}
|
|
|
|
return cohereTools;
|
|
}
|
|
|
|
module.exports = {
|
|
convertClaudePrompt,
|
|
convertClaudeMessages,
|
|
convertGooglePrompt,
|
|
convertTextCompletionPrompt,
|
|
convertCohereMessages,
|
|
convertMistralMessages,
|
|
convertCohereTools,
|
|
};
|