UNPKG

whatsapp-claude-gpt

Version:

WhatsApp-Claude-GPT is a WhatsApp chatbot that supports multiple AI providers for chat, optional image generation/editing, and voice (speech-to-text and text-to-speech). It’s built for natural, contextual conversations and can now also handle reminders an

354 lines (318 loc) 12.1 kB
import { AiMessage, AIProvider, AIRole } from "../interfaces/ai-interfaces"; import { ResponseInputItem } from "openai/resources/responses/responses"; import { ChatCompletionMessageParam } from "openai/resources"; import { MessageParam } from "@anthropic-ai/sdk/resources"; import { CONFIG } from "../config"; import { getUnsupportedMessage } from "../utils"; import logger from "../logger"; // Constants const ATTACHMENT_FALLBACK_MSG = "SYSTEM: this message is only to include the msg_id of the attached file/image. Do not mention msg_id in the chat"; // Type guards and helpers const isTextLike = (t: string) => t === "text" || t === "ASR"; const hasTextOrASR = (m: AiMessage) => m.content.some(c => isTextLike(c.type)); const toDataUri = (mimetype: string, value: string) => `data:${mimetype};base64,${value}`; type BaseMeta = { message: string; msg_id?: string | number | undefined; type?: string; author_id?: string | number; author_name?: string | null; date?: string | undefined; emojiReact?: string; }; // Builds the common metadata payload for text-like messages function buildMeta( aiMessage: AiMessage, c: any, overrides?: Partial<Pick<BaseMeta, "message" | "type">> ): BaseMeta { if(aiMessage.role == AIRole.ASSISTANT){ return { message: overrides?.message ?? c.value, emojiReact: "" } } return { message: overrides?.message ?? c.value, msg_id: c.msg_id, type: overrides?.type ?? c.type, author_id: c.author_id, author_name: aiMessage.name, date: c.dateString }; } // -- Provider specific converters (kept small by reusing helpers) -- // Claude converter function toClaude(messageList: AiMessage[]): MessageParam[] { const claudeMessageList: MessageParam[] = []; let currentRole: AIRole = AIRole.USER; let block: Array<any> = []; const pushBlock = () => { if (block.length > 0) { claudeMessageList.push({ role: currentRole as any, content: block }); block = []; } }; for (const aiMessage of messageList) { // If assistant sends an image, Claude requires user role for that block const role = aiMessage.role === AIRole.ASSISTANT && aiMessage.content.some(c => c.type === "image") ? AIRole.USER : aiMessage.role; const hasText = hasTextOrASR(aiMessage); if (role !== currentRole) { pushBlock(); currentRole = role; } for (const c of aiMessage.content) { if (isTextLike(c.type)) { // Claude text: wrap metadata as JSON string inside a "text" block block.push({ type: "text", text: JSON.stringify( buildMeta(aiMessage, c, { type: c.type }) // keep original behavior: "type":"text" ) }); } if (c.type === "image") { block.push({ type: "image", source: { data: c.value!, media_type: c.mimetype as any, type: "base64" } }); if (!hasText) { // Inject a metadata carrier if the message only has an image block.push({ type: "text", text: JSON.stringify( buildMeta(aiMessage, c, { message: ATTACHMENT_FALLBACK_MSG, type: "text" }) ) }); } } } } pushBlock(); // Claude requires the first message to be "user" if (claudeMessageList.length > 0 && claudeMessageList[0].role !== AIRole.USER) { claudeMessageList.shift(); } return claudeMessageList; } // DeepSeek converter function toDeepSeek(messageList: AiMessage[]): any[] { const deepSeekMsgList: any[] = []; for (const aiMessage of messageList) { if (aiMessage.role === AIRole.ASSISTANT) { // Roboto: single text item as stringified wrapper const textContent = aiMessage.content.find(c => isTextLike(c.type))!; const content = JSON.stringify(buildMeta(aiMessage, textContent)); deepSeekMsgList.push({ content, name: aiMessage.name!, role: aiMessage.role }); } else { // User: array of text blocks, images are not supported (send unsupported text) const content: Array<any> = []; for (const c of aiMessage.content) { if (c.type === "image" || c.type === "file") { content.push({ type: "text", text: JSON.stringify( buildMeta(aiMessage, c, { message: getUnsupportedMessage(c.type, "") }) ) }); } if (isTextLike(c.type)) { content.push({ type: "text", text: JSON.stringify(buildMeta(aiMessage, c)) }); } } deepSeekMsgList.push({ content: content, name: aiMessage.name!, role: aiMessage.role }); } } return deepSeekMsgList; } // OpenAI converter function toOpenAI(messageList: AiMessage[]): ResponseInputItem[] { const responseInputItems: ResponseInputItem[] = []; for (const aiMessage of messageList) { const fromBot = aiMessage.role === AIRole.ASSISTANT; const textType = fromBot ? "output_text" : "input_text"; const hasText = hasTextOrASR(aiMessage); const gptContent: any[] = []; for (const c of aiMessage.content) { if (c.type === "image") { gptContent.push({ type: "input_image", image_url: toDataUri(c.mimetype, c.value) }); if (!hasText) { const fallbackMsg = c.filename == 'sticker'?ATTACHMENT_FALLBACK_MSG.replace('file/image','sticker'):ATTACHMENT_FALLBACK_MSG; gptContent.push({ type: textType, text: JSON.stringify( buildMeta(aiMessage, c, { message: fallbackMsg, type: "text" }) ) }); } } else if (c.type === "file") { gptContent.push({ type: "input_file", file_data: toDataUri(c.mimetype, c.value), filename: c.filename }); if (!hasText) { gptContent.push({ type: textType, text: JSON.stringify( buildMeta(aiMessage, c, { message: ATTACHMENT_FALLBACK_MSG, type: "text" }) ) }); } } if (isTextLike(c.type)) { gptContent.push({ type: textType, text: JSON.stringify(buildMeta(aiMessage, c)) }); } } responseInputItems.push({ content: gptContent, role: aiMessage.role }); } return responseInputItems; } // Qwen converter function toQwen(messageList: AiMessage[]): any[] { const chatgptMessageList: any[] = []; for (const aiMessage of messageList) { const gptContent: Array<any> = []; const hasText = hasTextOrASR(aiMessage); for (const c of aiMessage.content) { if (isTextLike(c.type)) { gptContent.push({ type: "text", text: JSON.stringify(buildMeta(aiMessage, c)) }); } if (c.type === "image") { gptContent.push({ type: "image_url", image_url: { url: toDataUri(c.mimetype, c.value) } }); if (hasText) { gptContent.push(buildMeta(aiMessage, c)); } } } chatgptMessageList.push({ content: gptContent, name: aiMessage.name!, role: aiMessage.role }); } return chatgptMessageList; } // Custom / DeepInfra converter function toOther(messageList: AiMessage[]): any[] { const otherMsgList: any[] = []; for (const aiMessage of messageList) { const textType = aiMessage.role === AIRole.ASSISTANT ? "output_text" : "input_text"; if (aiMessage.role === AIRole.ASSISTANT) { const textContent = aiMessage.content.find(c => isTextLike(c.type))!; otherMsgList.push({ content: JSON.stringify(buildMeta(aiMessage, textContent)), name: aiMessage.name!, role: aiMessage.role }); } else { const aggregated: Array<any> = []; const hasText = hasTextOrASR(aiMessage); for (const c of aiMessage.content) { if (c.type === "image") { aggregated.push({ type: "image_url", image_url: { url: toDataUri(c.mimetype, c.value) } }); if (!hasText) { const fallbackMsg = c.filename == 'sticker'?ATTACHMENT_FALLBACK_MSG.replace('file/image','sticker'):ATTACHMENT_FALLBACK_MSG; aggregated.push({ type: textType, text: JSON.stringify( buildMeta(aiMessage, c, { message: fallbackMsg, type: "text" }) ) }); } } else if (c.type === "file") { aggregated.push( JSON.stringify( buildMeta(aiMessage, c, { message: getUnsupportedMessage(c.type, "") }) ) ); } if (isTextLike(c.type)) { aggregated.push(JSON.stringify(buildMeta(aiMessage, c))); } } otherMsgList.push({ content: aggregated[0], role: aiMessage.role }); } } return otherMsgList; } // Public API export function convertIaMessagesLang( messageList: AiMessage[] ): MessageParam[] | ChatCompletionMessageParam[] | ResponseInputItem[] { switch (CONFIG.ChatConfig.provider) { case AIProvider.CLAUDE: return toClaude(messageList); case AIProvider.DEEPSEEK: return toDeepSeek(messageList); case AIProvider.OPENAI: return toOpenAI(messageList); case AIProvider.QWEN: return toQwen(messageList); case AIProvider.CUSTOM: case AIProvider.DEEPINFRA: return toOther(messageList); default: logger.error(`CRITICAL: Unsupported chat provider: ${CONFIG.ChatConfig.provider}. No message conversion available.`); throw new Error(`Unsupported chat provider: ${CONFIG.ChatConfig.provider}. Please configure a valid CHAT_PROVIDER.`); } }