-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: add example ai chat bot using wapi.js and fix app crash on API…
… error parsing (#37)
- Loading branch information
1 parent
e9f68d2
commit 918da57
Showing
38 changed files
with
5,930 additions
and
5,289 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,10 +23,24 @@ jobs: | |
node-version: 20 | ||
|
||
- name: Install pnpm | ||
uses: pnpm/[email protected] | ||
uses: pnpm/action-setup@v4 | ||
|
||
with: | ||
run_install: false | ||
|
||
- name: Get pnpm store directory | ||
shell: bash | ||
run: | | ||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV | ||
- uses: actions/cache@v4 | ||
name: Setup pnpm cache | ||
with: | ||
path: ${{ env.STORE_PATH }} | ||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} | ||
restore-keys: | | ||
${{ runner.os }}-pnpm-store- | ||
- name: Install Dependencies | ||
run: pnpm install --frozen-lockfile | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Empty file.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
WHATSAPP_API_ACCESS_TOKEN= | ||
WHATSAPP_PHONE_NUMBER_ID= | ||
WHATSAPP_BUSINESS_ACCOUNT_ID= | ||
WHATSAPP_WEBHOOK_SECRET= | ||
OPEN_AI_API_KEY= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
dist | ||
.eslintrc.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/** @type {import("eslint").Linter.Config} */ | ||
module.exports = { | ||
extends: ['@wapijs/eslint-config/config.node.js'] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require('@wapijs/prettier-config/config.node') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/* eslint-disable no-console */ | ||
import { nodeExternalsPlugin } from 'esbuild-node-externals' | ||
import esbuildPluginTsc from 'esbuild-plugin-tsc' | ||
import { context, build } from 'esbuild' | ||
import { TsconfigPathsPlugin } from '@esbuild-plugins/tsconfig-paths' | ||
|
||
// Define common options for both development and production builds | ||
const commonOptions = { | ||
entryPoints: ['./src/index.ts'], | ||
target: 'es6', | ||
format: 'cjs', | ||
splitting: false, | ||
outdir: './dist', | ||
platform: 'node', | ||
bundle: true, | ||
plugins: [ | ||
nodeExternalsPlugin(), | ||
TsconfigPathsPlugin({ tsconfig: './tsconfig.json' }), | ||
esbuildPluginTsc({ | ||
tsconfigPath: './tsconfig.json', | ||
force: true | ||
}) | ||
] | ||
} | ||
|
||
// Development Build | ||
async function buildDevCode() { | ||
const devOptions = { | ||
...commonOptions, | ||
minify: false // Don't minify in development | ||
} | ||
|
||
const buildContext = await context(devOptions) | ||
|
||
// Add watch mode for development | ||
await buildContext.watch() | ||
} | ||
|
||
// Production Build | ||
async function buildProdCode() { | ||
const prodOptions = { | ||
...commonOptions, | ||
minify: true // Minify in production | ||
} | ||
|
||
await build(prodOptions) | ||
} | ||
|
||
async function buildCode() { | ||
if (process.argv.includes('--watch')) { | ||
// If '--watch' argument is provided, run development build | ||
await buildDevCode() | ||
console.log('Built code in development watch mode.') | ||
} else { | ||
// Otherwise, run production build | ||
await buildProdCode() | ||
console.log('Production Build Ready!') | ||
} | ||
} | ||
|
||
buildCode().catch(() => { | ||
process.exit(1) | ||
}) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
declare global { | ||
namespace NodeJS { | ||
interface ProcessEnv { | ||
WHATSAPP_API_ACCESS_TOKEN: string | ||
WHATSAPP_PHONE_NUMBER_ID: string | ||
WHATSAPP_BUSINESS_ACCOUNT_ID: string | ||
WHATSAPP_WEBHOOK_SECRET: string | ||
OPEN_AI_API_KEY: string | ||
OPEN_AI_ORG_ID: string | ||
OPEN_AI_PROJECT_ID: string | ||
|
||
} | ||
} | ||
} | ||
|
||
export { } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"watch": [ | ||
"./dist/src/index.js" | ||
], | ||
"ignore": [ | ||
"node_modules/*.*" | ||
], | ||
"ext": "js", | ||
"exec": "sleep 2 && NODE_ENV=development node -r dotenv/config ./dist/index.js dotenv_config_path=./.env.dev" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
{ | ||
"name": "wapi-gpt", | ||
"version": "0.0.1", | ||
"scripts": { | ||
"watch": "pnpm tsc --watch", | ||
"dev": "concurrently 'pnpm build:dev' 'nodemon -L'", | ||
"build:dev": "node build.mjs --watch", | ||
"build:prod": "NODE_ENV=production node ./build.mjs", | ||
"lint": "pnpm eslint .", | ||
"pretty": "pnpm prettier --write \"src/**/*.ts\"", | ||
"clean-install": "rm -rf ./node_modules && pnpm install --frozen-lockfile" | ||
}, | ||
"author": { | ||
"name": "Sarthak Jain", | ||
"email": "[email protected]", | ||
"url": "https://linkedin.com/in/sarthakjdev" | ||
}, | ||
"license": "MIT", | ||
"dependencies": { | ||
"@wapijs/wapi.js": "workspace:*", | ||
"cache-manager": "^4.0.0", | ||
"ms": "^2.1.3", | ||
"openai": "^4.52.7" | ||
}, | ||
"devDependencies": { | ||
"@esbuild-plugins/tsconfig-paths": "^0.1.2", | ||
"@types/cache-manager": "^4.0.6", | ||
"@types/node": "^20.12.12", | ||
"@wapijs/eslint-config": "workspace:*", | ||
"@wapijs/prettier-config": "workspace:*", | ||
"@wapijs/typescript-config": "workspace:*", | ||
"esbuild": "^0.19.8", | ||
"esbuild-node-externals": "^1.11.0", | ||
"esbuild-plugin-tsc": "^0.4.0", | ||
"index.js": "link:esbuild-plugin-tsc/src/index.js", | ||
"nodemon": "^3.0.2", | ||
"typescript": "5.4.5" | ||
}, | ||
"packageManager": "[email protected]", | ||
"pnpm": { | ||
"patchedDependencies": { | ||
"@microsoft/[email protected]": "patches/@[email protected]" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { TextMessage, type TextMessageEvent } from '@wapijs/wapi.js' | ||
import { whatsappClient } from './utils/client' | ||
import { askAi } from './utils/gpt' | ||
|
||
async function init() { | ||
try { | ||
whatsappClient.on('Ready', () => { | ||
console.log('Client is ready') | ||
}) | ||
|
||
whatsappClient.on('Error', error => { | ||
console.error('Error', error.message) | ||
}) | ||
|
||
whatsappClient.on('TextMessage', async (event: TextMessageEvent) => { | ||
const aiResponse = await askAi(event.text.data.text, event.context.from) | ||
const response = await event.reply({ | ||
message: new TextMessage({ | ||
text: aiResponse | ||
}) | ||
}) | ||
console.log({ response }) | ||
}) | ||
|
||
whatsappClient.initiate() | ||
} catch (error) { | ||
console.error(error) | ||
// ! TODO: you may prefer to send a notification to your slack channel or email here | ||
} | ||
} | ||
|
||
init().catch(error => console.error(error)) | ||
|
||
process.on('unhandledRejection', error => { | ||
console.error('unhandledRejection', error) | ||
process.exit(1) | ||
}) | ||
|
||
process.on('uncaughtException', error => { | ||
console.error('uncaughtException', error) | ||
process.exit(1) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export enum AiConversationRoleEnum { | ||
User = 'user', | ||
Ai = 'assistant' | ||
} | ||
|
||
export type ConversationMessageType = { | ||
role: AiConversationRoleEnum | ||
content: string | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { caching } from 'cache-manager' | ||
|
||
const cacheStore = caching({ | ||
store: 'memory' | ||
}) | ||
|
||
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 async function getCachedData<T>(key: string): Promise<T> { | ||
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' | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { Client } from '@wapijs/wapi.js' | ||
|
||
const WHATSAPP_BUSINESS_ACCOUNT_ID = process.env.WHATSAPP_BUSINESS_ACCOUNT_ID | ||
const WHATSAPP_API_ACCESS_TOKEN = process.env.WHATSAPP_API_ACCESS_TOKEN | ||
const WHATSAPP_PHONE_NUMBER_ID = process.env.WHATSAPP_PHONE_NUMBER_ID | ||
const WHATSAPP_WEBHOOK_SECRET = process.env.WHATSAPP_WEBHOOK_SECRET | ||
|
||
if ( | ||
!WHATSAPP_API_ACCESS_TOKEN || | ||
!WHATSAPP_BUSINESS_ACCOUNT_ID || | ||
!WHATSAPP_PHONE_NUMBER_ID || | ||
!WHATSAPP_WEBHOOK_SECRET | ||
) { | ||
throw new Error('Configs not defined!') | ||
} | ||
|
||
export const whatsappClient = new Client({ | ||
apiAccessToken: WHATSAPP_API_ACCESS_TOKEN, | ||
businessAccountId: WHATSAPP_BUSINESS_ACCOUNT_ID, | ||
phoneNumberId: WHATSAPP_PHONE_NUMBER_ID, | ||
port: 8080, | ||
webhookEndpoint: '/webhook', | ||
webhookSecret: WHATSAPP_WEBHOOK_SECRET | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { OpenAI } from 'openai' | ||
import { cacheData, computeCacheKey, getCachedData, getConversationContextCacheKey } from './cache' | ||
import { AiConversationRoleEnum, type 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 || !organizationId || !projectId) { | ||
throw new Error('OPEN_AI_API_KEY not defined!') | ||
} | ||
|
||
const OpenApiClient = new OpenAI({ | ||
apiKey: openAiApiKey, | ||
project: projectId, | ||
organization: organizationId | ||
}) | ||
|
||
export async function askAi(message: string, fromPhoneNumber: string): Promise<string> { | ||
try { | ||
const contextCacheKey = getConversationContextCacheKey(fromPhoneNumber) | ||
const context = await getCachedData<ConversationMessageType[]>(contextCacheKey) | ||
|
||
let response = 'Sorry, I am not able to understand that.' | ||
const responseCacheKey = computeCacheKey({ | ||
context: 'response_cache', | ||
id: message.trim() | ||
}) | ||
const cachedResponse = await getCachedData<string>(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 | ||
} | ||
|
||
await cacheData({ | ||
key: contextCacheKey, | ||
data: [ | ||
...(context ? context : []), | ||
{ | ||
role: AiConversationRoleEnum.User, | ||
content: message | ||
}, | ||
{ | ||
role: AiConversationRoleEnum.Ai, | ||
content: response | ||
} | ||
] | ||
}) | ||
return response | ||
} catch (error) { | ||
console.log({ error }) | ||
return 'Sorry, I am not able to understand that.' | ||
} | ||
} |
Oops, something went wrong.