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
text/typescript
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.`);
}
}