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
713 lines (603 loc) • 30.2 kB
text/typescript
import { Chat, GroupChat, Message, MessageMedia } from "whatsapp-web.js";
import { AIAnswer, AiMessage, AIProvider, AIService, OperationResult, ToolExecutionContext } from "../interfaces/ai-interfaces";
import { AIConfig, CONFIG } from "../config";
import WspWeb from "./wsp-web";
import OpenAISvc from "../services/openai-service";
import logger from "../logger";
import { bufferToStream, extractAnswer, getAuthorId, includeName, parseCommand, parseIfJson, sanitizeForLog, sleep } from "../utils";
import { ChatConfiguration, chatConfigurationManager } from "../config/chat-configurations";
import { getTools } from "../config/functions";
import { convertIaMessagesLang } from "./message-conversion";
import CustomOpenAISvc from "../services/openai-custom-service";
import OpenaiCustomService from "../services/openai-custom-service";
import AnthropicSvc from "../services/anthropic-service";
import { CVoices, elevenTTS } from "../services/elevenlabs-service";
import Reminders from "../services/reminder-service";
import MemoryService from "../services/memory-service";
import wspWeb from "./wsp-web";
class RobotoClass {
// Per-chat promise queue that serializes message processing.
// Each chat's promise chain ensures only one message is processed at a time.
private chatProcessingQueue = new Map<string, Promise<void>>();
// Per-chat typing flag used by sendStateTyping to know when to stop.
private chatTypingFlags = new Map<string, boolean>();
private botEnabled = true;
private chatImageRetry = new Map<string, number>();
private readonly MAX_IMAGE_RETRY_KEYS = 500;
// Rate limiting: key -> timestamps[] for sliding window.
private rateLimitTracker = new Map<string, number[]>();
private readonly MAX_RATE_LIMIT_TRACKER_KEYS = 500;
constructor() {
}
public async readWspMessage(wspMessage: Message) {
let chatData: Chat | undefined;
let chatId: string | undefined;
let resolveCurrent: (() => void) | undefined;
try {
chatData = await wspMessage.getChat();
chatId = chatData.id._serialized;
const contactData = await wspMessage.getContact();
const chatConfig = await chatConfigurationManager.getChatConfig(chatData.id._serialized, chatData.name);
const botName = chatConfig.botName;
const isAdmin = CONFIG.BotConfig.adminNumbers.includes(contactData.number);
if (this.isCommand(wspMessage, isAdmin)) {
if (CONFIG.BotConfig.restrictedNumbers.includes(contactData.number)) {
logger.debug(`[readWspMessage] Restricted number ${contactData.number} attempted command. Ignored.`);
return false;
}
return this.commandSelect(wspMessage, chatId, isAdmin);
}
const shouldProcess = await this.shouldProcessMessage(wspMessage, chatData, botName);
if (!shouldProcess) return false;
// Rate limiting: skip AI call if author/chat exceeds the configured window.
// Admins bypass rate limiting.
const authorId = getAuthorId(wspMessage);
if (!isAdmin && !this.checkRateLimit(chatData.isGroup ? chatId : authorId)) {
logger.debug(`[readWspMessage] Rate limited: ${chatData.isGroup ? 'chat' : 'author'} ${chatId}`);
return false;
}
// Serialize processing per chat using a promise queue.
// This replaces the polling-based busyChats Set with a deterministic queue.
const previous = this.chatProcessingQueue.get(chatId) ?? Promise.resolve();
const current = new Promise<void>((resolve) => { resolveCurrent = resolve; });
this.chatProcessingQueue.set(chatId, current);
await previous;
// Start typing indicator as a fire-and-forget task.
// It stops when chatTypingFlags is cleared in finally.
this.chatTypingFlags.set(chatId, true);
this.sendStateTyping(chatData).catch((e) => {
logger.error(`[sendStateTyping] Error for chat ${chatId}: ${e.message}`);
});
const memoriesContext = await MemoryService.getMemoryContext(chatId, !chatData.isGroup? getAuthorId(wspMessage): null, chatData.isGroup);
const systemPrompt = CONFIG.getSystemPrompt(chatConfig, memoriesContext);
const aiMessages: AiMessage[] = await WspWeb.generateMessageArray(wspMessage, chatData, chatConfig, this.hasChatCache(chatId));
const toolContext: ToolExecutionContext = {
chatId: chatData.id._serialized,
chatName: chatData.name,
messageId: wspMessage.id._serialized,
authorId: getAuthorId(wspMessage),
isGroup: chatData.isGroup,
imageMessageIds: aiMessages
.flatMap(m => m.content)
.filter(c => c.type === 'image' && c.msg_id)
.map(c => c.msg_id!)
};
const aiResponse = await this.sendMessageToAi(aiMessages, systemPrompt, chatConfig, true, toolContext);
let chatResponse: AIAnswer = extractAnswer(aiResponse, botName);
if (!chatResponse || !chatResponse.message) {
if (!aiResponse || aiResponse.trim() === '') {
logger.warn(`[readWspMessage] AI response was empty for chat ${chatId}`);
} else {
logger.warn(`[readWspMessage] Failed to extract answer from AI response for chat ${chatId}`);
}
return false;
}
// If the response includes emoji reaction, react to the message
if (chatResponse.emojiReact)
wspMessage.react(chatResponse.emojiReact);
return WspWeb.returnResponse(wspMessage, chatResponse.message, chatData.isGroup);
} catch (e) {
//TODO Handle Error
logger.error(`[readWspMessage] ErrorMessage: ${JSON.stringify(sanitizeForLog(e))}`);
logger.error('[readWspMessage] Chat context is being reset due to errors');
if (chatId) this.deleteChatCache(chatId);
return false;
} finally {
// Clean up typing flag so sendStateTyping stops its loop.
if (chatId) {
this.chatTypingFlags.delete(chatId);
}
// Resolve the current promise so the next message in the queue can proceed.
resolveCurrent?.();
// clearState is an external API call that may fail; catch errors to avoid
// unhandled rejections that would crash the process.
if (chatData) {
try {
await chatData.clearState();
} catch (e: any) {
logger.error(`[clearState] Error for chat ${chatId}: ${e.message}`);
}
}
}
}
/**
* Resolves the AI service instance for the given provider.
* Mapping: OPENAI → OpenAISvc, CLAUDE → AnthropicSvc, everything else → CustomOpenAISvc.
*/
private resolveAIService(provider = AIConfig.ChatConfig.provider): AIService {
switch (provider) {
case AIProvider.OPENAI:
return OpenAISvc;
case AIProvider.CLAUDE:
return AnthropicSvc;
default:
return CustomOpenAISvc;
}
}
public async sendMessageToAi(aiMessages: AiMessage[], systemPrompt, chatConfig: ChatConfiguration, withTools = true, toolContext?: ToolExecutionContext){
const messagesList = convertIaMessagesLang(aiMessages) as any;
const chat = await wspWeb.getWspClient().getChatById(chatConfig.chatId);
const tools = withTools ? getTools(chat) : undefined;
return await this.resolveAIService().sendMessage(messagesList, systemPrompt, chatConfig, tools, toolContext);
}
public async handleFunction(functionName: string, functionArgs: any, context?: ToolExecutionContext): Promise<OperationResult> {
try {
const args = parseIfJson(functionArgs);
// Sanitize args for logging via standard sanitizer.
logger.info(`[Assistant->handleFunction] Executing function: ${functionName} with args: ${JSON.stringify(sanitizeForLog(args))}`);
const handler = this.getToolHandler(functionName);
if (!handler) {
logger.warn(`[Assistant->handleFunction] Unknown function requested: ${functionName}`);
return { success: false, result: `Unknown tool: ${functionName}` };
}
return await handler(args, context);
} catch (e) {
logger.error(JSON.stringify(sanitizeForLog(e)));
return {success: false, result: `Error executing function ${functionName}: ${e.message}`};
}
}
private getToolHandler(functionName: string): ((args: any, context?: ToolExecutionContext) => Promise<OperationResult>) | null {
const handlers: Record<string, (args: any, context?: ToolExecutionContext) => Promise<OperationResult>> = {
generate_image: async (args: any, context?: ToolExecutionContext) => {
// Overwrite model-supplied IDs with server-side context.
if (context) {
args.msg_id = context.messageId;
args.chatId = context.chatId;
// Only allow image_msg_ids that are present in the current context.
if (args.image_msg_ids?.length > 0) {
args.image_msg_ids = args.image_msg_ids.filter((id: string) =>
context.imageMessageIds.includes(id)
);
}
}
const imageRetryCount = this.chatImageRetry.get(args.chatId);
try {
await this.createImage(args);
this.chatImageRetry.delete(args.chatId);
return { success: true, result: 'The image has been generated and sent to the chat.'};
} catch (e){
logger.error(`[${e.code}]: ${e.message}`);
if (imageRetryCount >= 3 || e.code == '400' || !e.message.toLowerCase().includes('safety system')) {
this.chatImageRetry.delete(args.chatId);
return {success: false, result: `Error generating image: ${e.message}.`};
}
// FIFO eviction: cap chatImageRetry to prevent unbounded growth.
if (!this.chatImageRetry.has(args.chatId) && this.chatImageRetry.size >= this.MAX_IMAGE_RETRY_KEYS) {
const firstKey = this.chatImageRetry.keys().next().value;
if (firstKey !== undefined) this.chatImageRetry.delete(firstKey);
}
this.chatImageRetry.set(args.chatId, imageRetryCount ? imageRetryCount + 1 : 1);
const match = e.message.match(/safety_violations=\[([^\]]*)\]/);
const safety_violations = match ? match[1] : null;
return { success: false, result: `Safety filters blocked the request (safety_violations:${safety_violations}). Please call generate_image again with a different phrasing. Rephrase the prompt to avoid sensitive content.`};
}
},
generate_speech: async (args: any, context?: ToolExecutionContext) => {
// Use server-side messageId instead of model-supplied msg_id.
const msgId = context?.messageId ?? args.msg_id;
const {input, instructions, voice_gender} = args;
await this.sendAudioResponse(msgId, {instructions, messageToSay: input, voiceGender: voice_gender});
return {success: true, result: 'The audio has been generated and sent to the user. You should respond with { "message" : null } to avoid duplicate messages'};
},
reminder_manager: async (args, context?: ToolExecutionContext) => {
return await Reminders.processFunctionCall(args, context);
},
user_memory_manager: async (args, context?: ToolExecutionContext) => {
// Overwrite model-supplied IDs with server-side context to prevent
// the model from reading/writing another user's memory.
if (context) {
args.chat_id = context.chatId;
args.author_id = context.authorId;
}
return await MemoryService.processFunctionCall(args, context);
},
group_memory_manager: async (args, context?: ToolExecutionContext) => {
// Overwrite model-supplied chat_id; only allowed in groups.
if (context) {
if (!context.isGroup) {
return { success: false, result: 'Group memory is only available in group chats.' };
}
args.chat_id = context.chatId;
}
return await MemoryService.processFunctionCall(args, context);
}
};
return handlers[functionName] ?? null;
}
private async shouldProcessMessage(wspMessage: Message, chatData: Chat, botName: string){
if(!this.botEnabled) return false;
if(wspMessage.fromMe || getAuthorId(wspMessage).includes("0@c.us")) return false;
const contactData = await wspMessage.getContact();
if(process.env.DEBUG == "1") {
if (!CONFIG.BotConfig.adminNumbers.includes(contactData.number)) return false;
}
if(CONFIG.BotConfig.restrictedNumbers.includes(contactData.number)){
logger.debug(`Number ${contactData.number} is in the restricted list. Message ignored`);
return false;
}
const isSelfMention = wspMessage.hasQuotedMsg ? (await wspMessage.getQuotedMessage()).fromMe : false;
const isMentioned = includeName(wspMessage.body, botName);
if (!isSelfMention && !isMentioned && chatData.isGroup) return false;
const isOldMessage = wspMessage.timestamp * 1000 < (Date.now() - 10 * 60000);
return !isOldMessage;
}
/**
* Opportunistic pruning: removes entries from rateLimitTracker whose
* timestamps have all expired outside the current window.
*/
private pruneRateLimitTracker(windowStart: number): void {
for (const [key, timestamps] of this.rateLimitTracker) {
const active = timestamps.filter(t => t > windowStart);
if (active.length === 0) {
this.rateLimitTracker.delete(key);
} else if (active.length !== timestamps.length) {
this.rateLimitTracker.set(key, active);
}
}
}
/**
* Rate limit check using a sliding window per key (authorId for direct, chatId for groups).
* Returns true if the request is allowed, false if rate limited.
* Admins bypass rate limiting (checked in caller).
*/
private checkRateLimit(key: string): boolean {
const max = CONFIG.BotConfig.rateLimitMax;
if (!max || max <= 0) return true; // Disabled
const windowSec = CONFIG.BotConfig.rateLimitWindowSec;
const now = Date.now();
const windowStart = now - windowSec * 1000;
// Opportunistic pruning: clean up expired entries across the map.
this.pruneRateLimitTracker(windowStart);
let timestamps = this.rateLimitTracker.get(key);
if (!timestamps) {
// FIFO eviction: cap rateLimitTracker to prevent unbounded growth.
if (this.rateLimitTracker.size >= this.MAX_RATE_LIMIT_TRACKER_KEYS) {
const firstKey = this.rateLimitTracker.keys().next().value;
if (firstKey !== undefined) this.rateLimitTracker.delete(firstKey);
}
timestamps = [now];
this.rateLimitTracker.set(key, timestamps);
return true;
}
// Remove expired entries for current key (already pruned above, but filter for safety).
timestamps = timestamps.filter(t => t > windowStart);
if (timestamps.length >= max) {
this.rateLimitTracker.set(key, timestamps);
return false;
}
timestamps.push(now);
this.rateLimitTracker.set(key, timestamps);
return true;
}
/**
* Selects and executes an action based on the command in the message.
*
* This function acts as a command dispatcher that interprets commands prefixed with "-"
* and routes them to the appropriate handler methods. Currently supports:
*
* - "-image [prompt]": Generates images using AI (if enabled in config)
* - "-chatconfig [subcommand]": Manages chat-specific configurations
* - "-reset": Resets the conversation context with the AI
*
* The function parses the command and its arguments from the message body
* and executes the corresponding functionality.
*
* @param message - The Message object containing the command
* @param chatId
* @param isAdmin
* @returns A promise resolving when the command processing is complete
*/
private async commandSelect(message: Message, chatId: string, isAdmin = false) {
const {command, commandMessage} = parseCommand(message.body);
if(!isAdmin && !this.botEnabled) return false;
switch (command) {
case "chatconfig":
return await this.handleChatConfigCommand(message, commandMessage!);
case "reset":
this.deleteChatCache(chatId);
return await message.react('👍');
case "memory":
return await this.handleMemoryCommand(message, commandMessage!);
case "enable":
if(!isAdmin) return false;
this.botEnabled = true;
return message.reply('Bot enabled.')
case "disable":
if(!isAdmin) return false;
this.botEnabled = false;
return message.reply('Bot disabled. No message will be answered')
default:
return true;
}
}
private isCommand(wspMessage: Message, isAdmin: boolean = false){
const commands = ['-chatconfig', '-reset'];
if(CONFIG.BotConfig.memoriesEnabled) commands.push('-memory');
if(isAdmin) commands.push('-enable','-disable');
return commands.includes(wspMessage.body.split(' ')[0]);
}
/**
* Creates and sends an AI-generated image in response to a text prompt.
*
* This function uses OpenAI's DALL-E model to generate an image based on
* the provided text prompt. The resulting image is sent as a reply to the
* original message.
*
* @returns A promise that resolves when the image is sent
* @param args
*/
private async createImage(args: {
prompt: string,
msg_id: string,
chatId: string,
background: string,
output_format: "png" | "jpg" | "webp" | "jpeg",
send_as: "image"| "sticker",
size: any,
image_msg_ids: string[],
quality: 'low'|'medium' | 'high'| 'auto',
}) {
const wspClient = WspWeb.getWspClient();
const wspMsg = await wspClient.getMessageById(args.msg_id);
let imageStreams = null;
let images;
// Normalize defaults for optional fields
const outputFormat = args.output_format || 'png';
// Normalize jpeg/jpg to a consistent internal format
const normalizedFormat = outputFormat === 'jpeg' ? 'jpg' : outputFormat;
const sendAs = args.send_as || 'image';
const background = args.background && args.background !== 'auto' ? args.background : undefined;
const quality = args.quality && args.quality !== 'auto' ? args.quality : undefined;
if (args.image_msg_ids?.length > 0) {
imageStreams = await Promise.all(
args.image_msg_ids.map(async (imgMsgId: string) => {
const imgMsg = await wspClient.getMessageById(imgMsgId);
const media = await WspWeb.extractMedia(imgMsg);
if (media.errorMedia) throw new Error(`Image reference error: ${media.errorMedia}`);
const buffer = Buffer.from(media.mediaData!.data, 'base64');
return bufferToStream(buffer);
})
);
}
if (AIConfig.ImageConfig.provider == AIProvider.OPENAI) {
images = await OpenAISvc.generateImage({
prompt: args.prompt,
imageStreams: imageStreams,
background: background as any,
output_format: normalizedFormat as any,
quality: quality as any
});
} else {
images = await OpenaiCustomService.generateImage(args.prompt);
}
if (!images || !Array.isArray(images) || images.length === 0 || !images[0].b64_json) {
throw new Error('Image generation returned no valid image data.');
}
// Determine MIME type: map jpg->jpeg for MessageMedia, but keep png/webp as-is
const mimeSuffix = normalizedFormat === 'jpg' ? 'jpeg' : normalizedFormat;
const media = new MessageMedia(`image/${mimeSuffix}`, images[0].b64_json, `image.${normalizedFormat}`);
const isSticker = sendAs == 'sticker'
const message = await WspWeb.getWspClient().sendMessage(args.chatId, media, {sendMediaAsSticker: isSticker});
this.addMessageToCache(message, args.chatId);
}
private async sendAudioResponse(msg_id: string, params: {messageToSay: string, instructions?: string, responseFormat?: string, voiceGender?: string}): Promise<void> {
try {
let base64Audio;
const wspMsg = await WspWeb.getWspClient().getMessageById(msg_id);
if (AIConfig.SpeechConfig.provider == AIProvider.ELEVENLABS) {
const voice = params.voiceGender == 'male'? CVoices.GEORGE : CVoices.SARAH;
base64Audio = await elevenTTS(params.messageToSay, voice);
} else {
const voice = params.voiceGender == 'male'? 'ash' : 'nova';
const audioBuffer = await OpenAISvc.speech(params.messageToSay, params.responseFormat, voice, params.instructions);
base64Audio = audioBuffer.toString('base64');
}
if (!base64Audio) {
throw new Error('Speech generation returned no audio data.');
}
let audioMedia = new MessageMedia('audio/mp3', base64Audio, 'voice.mp3');
// Reply to the message with the synthesized speech audio.
await wspMsg.reply(audioMedia, undefined, {sendAudioAsVoice: true});
} catch (e: any) {
logger.error(`Error in speak function: ${e.message}`);
throw e;
}
}
/**
* Handles chat configuration commands for customizing bot behavior per chat.
*
* This function manages the following subcommands:
* - prompt: Sets a custom personality/behavior for the bot in the current chat
* - botname: Changes the bot's name for the current chat
* - remove: Removes custom configurations and reverts to defaults
* - show: Displays current custom configuration
*
* In group chats, only administrators can modify configurations.
*
* @param message - The Message object containing the command
* @param commandText - The text of the command without the prefix
* @returns A promise resolving to the sent reply message
*/
private async handleChatConfigCommand(message: Message, commandText: string) {
const chat = await message.getChat();
const isGroup = chat.isGroup;
if (isGroup) {
const groupChat = chat as GroupChat;
const participant = await groupChat.participants.find(p => p.id._serialized === message.author);
const isAdmin = participant?.isAdmin || participant?.isSuperAdmin;
if (!isAdmin) {
return message.reply("Only group administrators can change the bot's configuration in groups.");
}
}
const parts = commandText.split(' ');
const subCommand = parts[0].toLowerCase();
switch (subCommand) {
case 'prompt':
case 'botname':
const value = parts.slice(1).join(' ');
if (!value) return message.reply(`Please provide a ${subCommand === 'prompt' ? 'prompt description' : 'name for the bot'}.`);
const existingConfig = await chatConfigurationManager.getChatConfig(chat.id._serialized, chat.name);
const updateOptions: any = {
promptInfo: existingConfig?.promptInfo || CONFIG.BotConfig.promptInfo,
botName: existingConfig?.botName
};
if (subCommand === 'prompt') updateOptions.promptInfo = value;
else updateOptions.botName = value;
const updatedConfig = await chatConfigurationManager.updateChatConfig(
chat.id._serialized,
chat.name,
isGroup,
updateOptions
);
return message.reply(
subCommand === 'prompt'
? `✅ Updated prompt for this ${isGroup ? 'group' : 'chat'}. The bot now: ${updatedConfig.promptInfo}`
: `✅ Bot name for this ${isGroup ? 'group' : 'chat'} has been set to: ${updatedConfig.botName}`
);
case 'remove':
const removed = await chatConfigurationManager.removeChatConfig(chat.id._serialized);
return message.reply(
removed
? `✅ The custom prompt and bot name have been removed. The bot will use the default configuration.`
: `This ${isGroup ? 'group' : 'chat'} did not have a custom configuration.`
);
case 'show':
const currentConfig = await chatConfigurationManager.getChatConfig(chat.id._serialized, chat.name);
if (!currentConfig) return message.reply(`This ${isGroup ? 'group' : 'chat'} does not have a custom configuration.`);
let response = currentConfig.promptInfo? `Current personality: ${currentConfig.promptInfo}`:``;
if (currentConfig.botName) response += `\nBot name: ${currentConfig.botName ?? CONFIG.BotConfig.botName}`;
return message.reply(response);
default:
return message.reply(
"Available commands:\n" +
`- *-chatconfig prompt [description]*: Sets the bot's personality for this ${isGroup ? 'group' : 'chat'}\n` +
`- *-chatconfig botname [name]*: Sets the bot's name for this ${isGroup ? 'group' : 'chat'}\n` +
"- *-chatconfig remove*: Removes the custom configuration\n" +
"- *-chatconfig show*: Displays the current configuration"
);
}
}
private async handleMemoryCommand(message: Message, commandText: string) {
const chat = await message.getChat();
const authorId = getAuthorId(message);
const parts = commandText.split(' ');
const subCommand = parts[0]?.toLowerCase();
switch (subCommand) {
case 'show':
const memory = await MemoryService.getMemory(chat.id._serialized, authorId);
if (!memory) {
return message.reply("I don't have any information saved about you.");
}
let response = `📋 *Information I have saved about you:*\n`;
if (memory.real_name) response += `👤 Real name: ${memory.real_name}\n`;
if (memory.age) response += `👤 Age: ${memory.age}\n`;
if (memory.profession) response += `💼 Profession: ${memory.profession}\n`;
if (memory.location) response += `📍 Location: ${memory.location}\n`;
if (memory.interests?.length) response += `🎯 Interests: ${memory.interests.join(', ')}\n`;
if (memory.likes?.length) response += `👍 Likes: ${memory.likes.join(', ')}\n`;
if (memory.dislikes?.length) response += `👎 Dislikes: ${memory.dislikes.join(', ')}\n`;
if (memory.running_jokes?.length) response += `😄 Running jokes: ${memory.running_jokes.join(', ')}\n`;
if (memory.nicknames?.length) response += `🏷️ Nicknames: ${memory.nicknames.join(', ')}\n`;
if (memory.notes?.length) response += `📝 Notes: ${memory.notes.join(', ')}\n`;
return message.reply(response);
case 'group':
if (!chat.isGroup) {
return message.reply("This command is only available in group chats.");
}
const groupMemory = await MemoryService.getMemory(chat.id._serialized);
if (!groupMemory) {
return message.reply("I don't have any group information saved yet.");
}
let groupResponse = `📋 *Group Memory for ${chat.name}:*\n`;
if (groupMemory.group_interests?.length) groupResponse += `🎯 Group interests: ${groupMemory.group_interests.join(', ')}\n`;
if (groupMemory.recurring_topics?.length) groupResponse += `💬 Recurring topics: ${groupMemory.recurring_topics.join(', ')}\n`;
if (groupMemory.group_likes?.length) groupResponse += `👍 Group likes: ${groupMemory.group_likes.join(', ')}\n`;
if (groupMemory.group_dislikes?.length) groupResponse += `👎 Group dislikes: ${groupMemory.group_dislikes.join(', ')}\n`;
if (groupMemory.group_running_jokes?.length) groupResponse += `😄 Group jokes: ${groupMemory.group_running_jokes.join(', ')}\n`;
if (groupMemory.group_notes?.length) groupResponse += `📝 Group notes: ${groupMemory.group_notes.join(', ')}\n`;
if (groupMemory.group_jargon && Object.keys(groupMemory.group_jargon).length > 0) {
const jargonText = Object.entries(groupMemory.group_jargon).map(([term, meaning]) => `${term}: ${meaning}`).join(', ');
groupResponse += `🗣️ Group jargon: ${jargonText}\n`;
}
return message.reply(groupResponse);
case 'clear':
await MemoryService.processFunctionCall({
action: 'clear',
chat_id: chat.id._serialized,
author_id: authorId
});
return message.reply("✅ Your personal information has been removed from my memory.");
case 'cleargroup':
if (!chat.isGroup) {
return message.reply("This command is only available in group chats.");
}
const clearResult = await MemoryService.processFunctionCall({
action: 'clear',
chat_id: chat.id._serialized
});
if (!clearResult.success) {
return message.reply(`❌ Failed to clear group memory: ${clearResult.result}`);
}
return message.reply("✅ Group memory has been cleared.");
default:
const commands = [
"Available memory commands:",
"• *-memory show*: Shows your personal information",
"• *-memory clear*: Clears your personal information"
];
if (chat.isGroup) {
commands.push("• *-memory group*: Shows group memory");
commands.push("• *-memory cleargroup*: Clears group memory");
}
return message.reply(commands.join('\n'));
}
}
private async addMessageToCache(wspMessage: Message, chatId: string) {
try {
const aiMessage = await WspWeb.convertWspMsgToAiMsg(wspMessage);
const items = convertIaMessagesLang([aiMessage]) as any;
return this.resolveAIService().addMessageToCache(items[0], chatId);
} catch (e) {
logger.error(`Error adding message to cache: ${e}`);
}
}
private deleteChatCache(chatId: string){
return this.resolveAIService().deleteChatCache(chatId);
}
private hasChatCache(chatId: string){
return this.resolveAIService().hasChatCache(chatId);
}
private async sendStateTyping(chatData: Chat){
const chatId = chatData.id._serialized;
while(this.chatTypingFlags.has(chatId)){
try {
await chatData.sendStateTyping();
} catch (e: any) {
logger.error(`[sendStateTyping] Error sending typing state for chat ${chatId}: ${e.message}`);
}
await sleep(2000);
}
}
}
const Roboto = new RobotoClass();
export default Roboto;