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
163 lines (125 loc) • 6.36 kB
text/typescript
import logger from '../logger';
import { OpenAI } from 'openai';
import { ChatCompletionMessageParam } from 'openai/resources';
import { AIConfig, CONFIG } from '../config';
import { ChatCompletion } from 'openai/src/resources/chat/completions';
import { Tool } from "openai/resources/responses/responses";
import NodeCache from "node-cache";
import { AIRole, AIService, ToolExecutionContext } from "../interfaces/ai-interfaces";
import { cleanChatCompletionMessage, countMessages, sanitizeForLog, trimCachePreserveMessageStart } from "../utils";
import Roboto from "../bot/roboto";
import { ChatConfiguration } from "../config/chat-configurations";
class CustomOpenAIService implements AIService<ChatCompletionMessageParam, any> {
private messagesCache = new NodeCache({
stdTTL: CONFIG.BotConfig.messageCacheTtl || CONFIG.BotConfig.nodeCacheTime,
checkperiod: 600,
});
private static clientCache = new Map<string, OpenAI>();
constructor() {
}
private getClient(baseURL?: string, apiKey?: string): OpenAI {
const key = `${baseURL ?? ''}::${apiKey ?? ''}`;
let client = CustomOpenAIService.clientCache.get(key);
if (!client) {
client = new OpenAI({ baseURL, apiKey });
CustomOpenAIService.clientCache.set(key, client);
}
return client;
}
public deleteChatCache(chatId: string){
this.messagesCache.del(chatId);
}
public addMessageToCache(item: ChatCompletionMessageParam, chatId: string){
const aiMessages: ChatCompletionMessageParam[] = this.messagesCache.get(chatId) || [];
aiMessages.push(item);
this.messagesCache.set(chatId, aiMessages, CONFIG.BotConfig.nodeCacheTime);
}
public hasChatCache(chatId: string): boolean {
return this.messagesCache.has(chatId);
}
public async sendMessage(aiMessagesInputList: ChatCompletionMessageParam[], systemPrompt: string, chatConfig: ChatConfiguration, tools: any, toolContext?: ToolExecutionContext): Promise<string> {
let cycleCount = 0;
const maxCycles = 5;
const chatId = chatConfig.chatId;
const aiMessages: any[] = this.messagesCache.get(chatId) || [];
aiMessages.push(...aiMessagesInputList)
while (cycleCount < maxCycles) {
const aiResponse: OpenAI.ChatCompletionMessage = await this.sendCompletion(aiMessages, 'text', tools, systemPrompt);
const tool_calls = aiResponse.tool_calls || [];
let hasFunctionCall = tool_calls.length > 0;
const functionOutputs = [];
for (const output of tool_calls) {
if (output.type == 'function') {
aiMessages.push(cleanChatCompletionMessage(aiResponse));
hasFunctionCall = true;
const functionResult = await Roboto.handleFunction(output.function.name, output.function.arguments, toolContext);
functionOutputs.push({role: "tool", tool_call_id: output.id, content: JSON.stringify(functionResult)})
} else {
logger.error(`[CustomOpenAIService] Unknown output type received : "${output.type}". Please report this issue.`);
}
}
aiMessages.push(...functionOutputs);
cycleCount += 1;
if (!hasFunctionCall) {
aiMessages.push(cleanChatCompletionMessage(aiResponse));
const finalMsgList = trimCachePreserveMessageStart(aiMessages, chatConfig.maxMsgsLimit ?? 30);
this.messagesCache.set(chatId, finalMsgList, CONFIG.BotConfig.nodeCacheTime);
return aiResponse.content;
}
}
throw new Error(`Reached the limit of ${maxCycles} communication cycles with OpenAI.`);
}
/**
* Sends a series of messages to the OpenAI Chat Completion API and retrieves a generated completion.
* This function is designed to interact with the OpenAI API, sending it a context composed of several messages.
* It then receives a response that is generated based on this context, aiming to provide a coherent and contextually appropriate continuation or reply.
*
* Parameters:
* - messageList: An array of ChatCompletionMessageParam objects, which include the messages that form the context for the API request.
*
* Returns:
* - A promise that resolves to the generated completion string, which is the API's response based on the provided context.
*/
async sendCompletion(
messageList: ChatCompletionMessageParam[],
responseType: 'json_object'|'text' = 'text',
tools: Array<Tool>,
systemPrompt?: string
): Promise<OpenAI.ChatCompletionMessage> {
const client = this.getClient(AIConfig.ChatConfig.baseURL, AIConfig.ChatConfig.apiKey);
const hasSystemMsg = (messageList[0] as any).role == AIRole.SYSTEM;
if(systemPrompt) {
if(hasSystemMsg) messageList.shift();
messageList.unshift({role: AIRole.SYSTEM, content: systemPrompt});
}
logger.info(`[${AIConfig.ChatConfig.provider}] Sending ${countMessages(messageList)} messages`);
logger.debug(`[${AIConfig.ChatConfig.provider}] Sending Msg: ${JSON.stringify(sanitizeForLog(messageList[messageList.length - 1]))}`);
const params: any = {
model: AIConfig.ChatConfig.model,
messages: messageList,
reasoning_effort: AIConfig.ChatConfig.reasoningEffort as any,
response_format: {
type: responseType
},
tools: tools,
store: CONFIG.BotConfig.openAIStore
}
const response: ChatCompletion = await client.chat.completions.create(params);
logger.debug(`[${AIConfig.ChatConfig.provider}] ResponsesAPI Usage: Input=${response.usage?.prompt_tokens}` + ` Cached=${response.usage?.prompt_tokens_details?.cached_tokens}` + ` Output=${response.usage?.completion_tokens}`);
logger.debug(`[${AIConfig.ChatConfig.provider}] ResponsesAPI Response:` + JSON.stringify(sanitizeForLog(response.choices[0].message)));
return response.choices[0].message;
}
async generateImage(prompt) {
const client = this.getClient(AIConfig.ImageConfig.baseURL, AIConfig.ImageConfig.apiKey);
logger.debug(`[${AIConfig.ImageConfig.provider}->generateImage] Creating image (prompt: ${sanitizeForLog(prompt)?.substring(0, 100) ?? 'N/A'})`);
const response = await client.images.generate({
prompt: prompt,
model: AIConfig.ImageConfig.model,
n: 1,
size: "1024x1024"
});
return response.data;
}
}
const CustomOpenAISvc = new CustomOpenAIService();
export default CustomOpenAISvc;