Spaces:
Running
Running
require('./polyfill.js'); | |
const { getConfigValue } = require('./util.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', | |
'gemini-1.5-flash-latest', | |
'gemini-1.5-flash-001', | |
'gemini-1.5-pro', | |
'gemini-1.5-pro-latest', | |
'gemini-1.5-pro-001', | |
'gemini-1.5-pro-exp-0801', | |
'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} charName Character name | |
* @param {string} userName User name | |
*/ | |
function convertMistralMessages(messages, charName = '', userName = '') { | |
if (!Array.isArray(messages)) { | |
return []; | |
} | |
// Make the last assistant message a prefill | |
const prefixEnabled = getConfigValue('mistral.enablePrefix', false); | |
const lastMsg = messages[messages.length - 1]; | |
if (prefixEnabled && messages.length > 0 && lastMsg?.role === 'assistant') { | |
lastMsg.prefix = true; | |
} | |
// Doesn't support completion names, so prepend if not already done by the frontend (e.g. for group chats). | |
messages.forEach(msg => { | |
if (msg.role === 'system' && msg.name === 'example_assistant') { | |
if (charName && !msg.content.startsWith(`${charName}: `)) { | |
msg.content = `${charName}: ${msg.content}`; | |
} | |
delete msg.name; | |
} | |
if (msg.role === 'system' && msg.name === 'example_user') { | |
if (userName && !msg.content.startsWith(`${userName}: `)) { | |
msg.content = `${userName}: ${msg.content}`; | |
} | |
delete msg.name; | |
} | |
if (msg.name && msg.role !== 'system' && !msg.content.startsWith(`${msg.name}: `)) { | |
msg.content = `${msg.name}: ${msg.content}`; | |
delete msg.name; | |
} | |
}); | |
// If system role message immediately follows an assistant message, change its role to user | |
for (let i = 0; i < messages.length - 1; i++) { | |
if (messages[i].role === 'assistant' && messages[i + 1].role === 'system') { | |
messages[i + 1].role = 'user'; | |
} | |
} | |
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, | |
}; | |