diff --git a/apps/wapi-ai-chatbot/environment.d.ts b/apps/wapi-ai-chatbot/environment.d.ts index cb7dd20..62d84ea 100644 --- a/apps/wapi-ai-chatbot/environment.d.ts +++ b/apps/wapi-ai-chatbot/environment.d.ts @@ -6,6 +6,9 @@ declare global { WHATSAPP_BUSINESS_ACCOUNT_ID: string WHATSAPP_WEBHOOK_SECRET: string OPEN_AI_API_KEY: string + OPEN_AI_ORG_ID: string + OPEN_AI_PROJECT_ID: string + } } } diff --git a/apps/wapi-ai-chatbot/package.json b/apps/wapi-ai-chatbot/package.json index af52ed4..03fab96 100644 --- a/apps/wapi-ai-chatbot/package.json +++ b/apps/wapi-ai-chatbot/package.json @@ -18,13 +18,14 @@ "license": "MIT", "dependencies": { "@wapijs/wapi.js": "workspace:*", + "cache-manager": "^4.0.0", "ms": "^2.1.3", - "node-cache": "^5.1.2", "openai": "^4.52.7" }, "devDependencies": { "@esbuild-plugins/tsconfig-paths": "^0.1.2", - "@types/node": "^20.10.2", + "@types/cache-manager": "^4.0.6", + "@types/node": "^20.12.12", "@wapijs/eslint-config": "workspace:*", "@wapijs/prettier-config": "workspace:*", "@wapijs/typescript-config": "workspace:*", diff --git a/apps/wapi-ai-chatbot/src/index.ts b/apps/wapi-ai-chatbot/src/index.ts index 957fcba..d87114a 100644 --- a/apps/wapi-ai-chatbot/src/index.ts +++ b/apps/wapi-ai-chatbot/src/index.ts @@ -1,7 +1,7 @@ import { TextMessage, type TextMessageEvent } from '@wapijs/wapi.js' import { whatsappClient } from './utils/client' import { askAi } from './utils/gpt' -import { getCache, setCache } from './utils/cache' +import { cacheData, computeCacheKey, getCachedData } from './utils/cache' async function init() { try { @@ -14,23 +14,10 @@ async function init() { }) whatsappClient.on('TextMessage', async (event: TextMessageEvent) => { - const cachedResponse = await getCache(event.text.data.text) - console.log({ cachedResponse }) - let response = 'Sorry, I am not able to understand that.' - if (cachedResponse) { - response = String(cachedResponse) - } else { - const aiResponse = await askAi(event.text.data.text) - response = aiResponse || response - if (aiResponse) { - setCache(event.text.data.text, aiResponse) - } - } - - console.log({ response }) + const aiResponse = await askAi(event.text.data.text, event.context.from) await event.reply({ message: new TextMessage({ - text: response + text: aiResponse }) }) }) diff --git a/apps/wapi-ai-chatbot/src/types.ts b/apps/wapi-ai-chatbot/src/types.ts new file mode 100644 index 0000000..07163da --- /dev/null +++ b/apps/wapi-ai-chatbot/src/types.ts @@ -0,0 +1,9 @@ +export enum AiConversationRoleEnum { + User = 'user', + Ai = 'assistant' +} + +export type ConversationMessageType = { + role: AiConversationRoleEnum + content: string +} diff --git a/apps/wapi-ai-chatbot/src/utils/cache.ts b/apps/wapi-ai-chatbot/src/utils/cache.ts index 6236ef6..9e72e6c 100644 --- a/apps/wapi-ai-chatbot/src/utils/cache.ts +++ b/apps/wapi-ai-chatbot/src/utils/cache.ts @@ -1,11 +1,28 @@ -import NodeCache from 'node-cache' +import { AiConversationRoleEnum, ConversationMessageType } from '~/types' +import { caching } from 'cache-manager' -const cache = new NodeCache() +const cacheStore = caching({ + store: 'memory' +}) -export const setCache = (key: string, value: any, ttl: number = 3600) => { - cache.set(key, value, ttl) +export async function cacheData(params: { key: string; data: any; ttl?: number }) { + const { key, ttl, data } = params + await cacheStore.set(key, data, { ...(ttl ? { ttl: ttl } : {}) }) } -export const getCache = (key: string) => { - return cache.get(key) +export async function getCachedData(key: string): Promise { + const response = await cacheStore.get(key) + console.log(response) + return response as T +} + +export function computeCacheKey(params: { id: string; context: string }) { + return `${params.id}-${params.context}` +} + +export function getConversationContextCacheKey(phoneNumber: string) { + return computeCacheKey({ + id: phoneNumber, + context: 'conversation' + }) } diff --git a/apps/wapi-ai-chatbot/src/utils/gpt.ts b/apps/wapi-ai-chatbot/src/utils/gpt.ts index 8d336c2..c8e8a61 100644 --- a/apps/wapi-ai-chatbot/src/utils/gpt.ts +++ b/apps/wapi-ai-chatbot/src/utils/gpt.ts @@ -1,34 +1,81 @@ import { OpenAI } from 'openai' +import { cacheData, computeCacheKey, getCachedData, getConversationContextCacheKey } from './cache' +import { AiConversationRoleEnum, ConversationMessageType } from '~/types' const openAiApiKey = process.env.OPEN_AI_API_KEY +const organizationId = process.env.OPEN_AI_ORG_ID +const projectId = process.env.OPEN_AI_PROJECT_ID -if (!openAiApiKey) { +if (!openAiApiKey || !organizationId || !projectId) { throw new Error('OPEN_AI_API_KEY not defined!') } const OpenApiClient = new OpenAI({ apiKey: openAiApiKey, - project: 'proj_viwVq5nzCR5Mj4TnIw4FQoNO', - organization: 'org-wwfaGhYXIA7CBUSg6x8DbKYj' + project: projectId, + organization: organizationId }) -export async function askAi(message: string): Promise { + +export async function askAi(message: string, fromPhoneNumber: string): Promise { try { - const chatCompletion = await OpenApiClient.chat.completions.create({ - messages: [{ role: 'user', content: message }], - model: 'gpt-3.5-turbo' + const contextCacheKey = getConversationContextCacheKey(fromPhoneNumber) + const context = await getCachedData(contextCacheKey) + + let response = 'Sorry, I am not able to understand that.' + const responseCacheKey = computeCacheKey({ + context: 'response_cache', + id: message.trim() }) + const cachedResponse = await getCachedData(responseCacheKey) + if (cachedResponse) { + response = cachedResponse + } else { + const chatCompletion = await OpenApiClient.chat.completions.create({ + messages: [ + { + role: 'system', + content: `You are an intelligent assistant helping a user with their queries. You can provide information, answer questions, and help the user complete tasks. + You are a developer relation engineer for a product named as Wapi.js. + Wapi.js is an SDK to build chat bots and to integrate WhatsApp API with your application very easily. + The SDK is authored by Sarthak Jain having github at https://github.com/sarthakjdev and is an open-source SDk which is free to use. + Users can use the SDK to build chatbots for customer support, marketing, notifications, and more! Leverage WhatsApp Business API. + You are required to give very to the point and very clear answers to the user queries. + Also, you can reveal your identity, ack like a human, and provide information about the product. + Never ever accept that you are an AI or a chatbot or an intelligent assistant. + ` + }, + ...(context ? context : []), + { + role: 'user', + content: message + } + ], + model: 'gpt-4' + }) + console.log(JSON.stringify({ chatCompletion })) + const aiResponse = chatCompletion.choices[0].message.content + if (aiResponse) response = aiResponse + } - console.log(JSON.stringify({ chatCompletion })) + await cacheData({ + key: contextCacheKey, + data: [ + ...(context ? context : []), + { + role: AiConversationRoleEnum.User, + content: message + }, + { + role: AiConversationRoleEnum.Ai, + content: response + }, - return chatCompletion.choices[0].message.content + ] + }) + return response } catch (error) { console.log({ error }) - return null + return 'Sorry, I am not able to understand that.' } } - - -export async function generateTranscription(){ - -} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46d4a0d..999b2fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,12 +137,12 @@ importers: '@wapijs/wapi.js': specifier: workspace:* version: link:../../packages/wapi.js + cache-manager: + specifier: ^4.0.0 + version: 4.1.0 ms: specifier: ^2.1.3 version: 2.1.3 - node-cache: - specifier: ^5.1.2 - version: 5.1.2 openai: specifier: ^4.52.7 version: 4.52.7 @@ -150,8 +150,11 @@ importers: '@esbuild-plugins/tsconfig-paths': specifier: ^0.1.2 version: 0.1.2(esbuild@0.19.12)(typescript@5.4.5) + '@types/cache-manager': + specifier: ^4.0.6 + version: 4.0.6 '@types/node': - specifier: ^20.10.2 + specifier: ^20.12.12 version: 20.12.12 '@wapijs/eslint-config': specifier: workspace:* @@ -181,34 +184,6 @@ importers: specifier: 5.4.5 version: 5.4.5 - apps/wapi-gpt: - dependencies: - '@wapijs/wapi.js': - specifier: workspace:* - version: link:../../packages/wapi.js - openai: - specifier: ^4.52.7 - version: 4.52.7 - devDependencies: - '@esbuild-plugins/tsconfig-paths': - specifier: ^0.1.2 - version: 0.1.2(esbuild@0.19.12)(typescript@5.4.5) - '@types/node': - specifier: ^20.10.2 - version: 20.12.12 - '@wapijs/eslint-config': - specifier: workspace:* - version: link:../../packages/eslint-config - '@wapijs/prettier-config': - specifier: workspace:* - version: link:../../packages/prettier-config - '@wapijs/typescript-config': - specifier: workspace:* - version: link:../../packages/typescript-config - typescript: - specifier: 5.4.5 - version: 5.4.5 - packages/create-wapi-app: dependencies: chalk: @@ -905,6 +880,9 @@ packages: '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + '@types/cache-manager@4.0.6': + resolution: {integrity: sha512-8qL93MF05/xrzFm/LSPtzNEOE1eQF3VwGHAcQEylgp5hDSTe41jtFwbSYAPfyYcVa28y1vYSjIt0c1fLLUiC/Q==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -1258,6 +1236,9 @@ packages: ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + async@3.2.3: + resolution: {integrity: sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -1327,6 +1308,9 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cache-manager@4.1.0: + resolution: {integrity: sha512-ZGM6dLxrP65bfOZmcviWMadUOCICqpLs92+P/S5tj8onz+k+tB7Gr+SAgOUHCQtfm2gYEQDHiKeul4+tYPOJ8A==} + call-bind@1.0.7: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} @@ -1421,10 +1405,6 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} - clone@2.1.2: - resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} - engines: {node: '>=0.8'} - color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -2694,6 +2674,9 @@ packages: lodash.castarray@4.4.0: resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + lodash.escaperegexp@4.1.2: resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} @@ -2759,6 +2742,10 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} @@ -2907,10 +2894,6 @@ packages: nerf-dart@1.0.0: resolution: {integrity: sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==} - node-cache@5.1.2: - resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==} - engines: {node: '>= 8.0.0'} - node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -4366,7 +4349,7 @@ snapshots: '@types/node': 20.5.1 chalk: 4.1.2 cosmiconfig: 8.3.6(typescript@5.4.5) - cosmiconfig-typescript-loader: 4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.4.5))(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5) + cosmiconfig-typescript-loader: 4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.4.5))(ts-node@10.9.2(@types/node@20.5.1)(typescript@5.4.5))(typescript@5.4.5) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -4935,6 +4918,8 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 20.12.12 + '@types/cache-manager@4.0.6': {} + '@types/connect@3.4.38': dependencies: '@types/node': 20.12.12 @@ -5368,6 +5353,8 @@ snapshots: ast-types-flow@0.0.8: {} + async@3.2.3: {} + asynckit@0.4.0: {} autoprefixer@10.4.19(postcss@8.4.38): @@ -5452,6 +5439,12 @@ snapshots: bytes@3.1.2: {} + cache-manager@4.1.0: + dependencies: + async: 3.2.3 + lodash.clonedeep: 4.5.0 + lru-cache: 7.18.3 + call-bind@1.0.7: dependencies: es-define-property: 1.0.0 @@ -5553,8 +5546,6 @@ snapshots: clone@1.0.4: {} - clone@2.1.2: {} - color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -5655,7 +5646,7 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.4.5))(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5): + cosmiconfig-typescript-loader@4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.4.5))(ts-node@10.9.2(@types/node@20.5.1)(typescript@5.4.5))(typescript@5.4.5): dependencies: '@types/node': 20.5.1 cosmiconfig: 8.3.6(typescript@5.4.5) @@ -7137,6 +7128,8 @@ snapshots: lodash.castarray@4.4.0: {} + lodash.clonedeep@4.5.0: {} + lodash.escaperegexp@4.1.2: {} lodash.get@4.4.2: {} @@ -7190,6 +7183,8 @@ snapshots: dependencies: yallist: 4.0.0 + lru-cache@7.18.3: {} + lunr@2.3.9: {} make-error@1.3.6: {} @@ -7309,10 +7304,6 @@ snapshots: nerf-dart@1.0.0: {} - node-cache@5.1.2: - dependencies: - clone: 2.1.2 - node-domexception@1.0.0: {} node-emoji@2.1.3: