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

163 lines (125 loc) 6.36 kB
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;