Skip to content

Commit 918da57

Browse files
authored
chore: add example ai chat bot using wapi.js and fix app crash on API error parsing (#37)
1 parent e9f68d2 commit 918da57

38 files changed

+5930
-5289
lines changed

.github/workflows/lint.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,24 @@ jobs:
2323
node-version: 20
2424

2525
- name: Install pnpm
26-
uses: pnpm/action-setup@v2.2.4
26+
uses: pnpm/action-setup@v4
27+
2728
with:
2829
run_install: false
2930

31+
- name: Get pnpm store directory
32+
shell: bash
33+
run: |
34+
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
35+
36+
- uses: actions/cache@v4
37+
name: Setup pnpm cache
38+
with:
39+
path: ${{ env.STORE_PATH }}
40+
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
41+
restore-keys: |
42+
${{ runner.os }}-pnpm-store-
43+
3044
- name: Install Dependencies
3145
run: pnpm install --frozen-lockfile
3246

.github/workflows/release.yaml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,23 @@ jobs:
4040
registry-url: https://registry.npmjs.org/
4141

4242
- name: Install pnpm
43-
uses: pnpm/action-setup@v2.2.4
43+
uses: pnpm/action-setup@v4
4444
with:
4545
run_install: false
4646

47+
- name: Get pnpm store directory
48+
shell: bash
49+
run: |
50+
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
51+
52+
- uses: actions/cache@v4
53+
name: Setup pnpm cache
54+
with:
55+
path: ${{ env.STORE_PATH }}
56+
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
57+
restore-keys: |
58+
${{ runner.os }}-pnpm-store-
59+
4760
- name: Install dependencies
4861
run: pnpm i --frozen-lockfile
4962

File renamed without changes.

apps/example-chat-bot/environment.d.ts

Whitespace-only changes.

apps/wapi-ai-chatbot/.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
WHATSAPP_API_ACCESS_TOKEN=
2+
WHATSAPP_PHONE_NUMBER_ID=
3+
WHATSAPP_BUSINESS_ACCOUNT_ID=
4+
WHATSAPP_WEBHOOK_SECRET=
5+
OPEN_AI_API_KEY=

apps/wapi-ai-chatbot/.eslintignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist
2+
.eslintrc.js

apps/wapi-ai-chatbot/.eslintrc.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/** @type {import("eslint").Linter.Config} */
2+
module.exports = {
3+
extends: ['@wapijs/eslint-config/config.node.js']
4+
}

apps/wapi-ai-chatbot/.prettierrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('@wapijs/prettier-config/config.node')

apps/wapi-ai-chatbot/build.mjs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/* eslint-disable no-console */
2+
import { nodeExternalsPlugin } from 'esbuild-node-externals'
3+
import esbuildPluginTsc from 'esbuild-plugin-tsc'
4+
import { context, build } from 'esbuild'
5+
import { TsconfigPathsPlugin } from '@esbuild-plugins/tsconfig-paths'
6+
7+
// Define common options for both development and production builds
8+
const commonOptions = {
9+
entryPoints: ['./src/index.ts'],
10+
target: 'es6',
11+
format: 'cjs',
12+
splitting: false,
13+
outdir: './dist',
14+
platform: 'node',
15+
bundle: true,
16+
plugins: [
17+
nodeExternalsPlugin(),
18+
TsconfigPathsPlugin({ tsconfig: './tsconfig.json' }),
19+
esbuildPluginTsc({
20+
tsconfigPath: './tsconfig.json',
21+
force: true
22+
})
23+
]
24+
}
25+
26+
// Development Build
27+
async function buildDevCode() {
28+
const devOptions = {
29+
...commonOptions,
30+
minify: false // Don't minify in development
31+
}
32+
33+
const buildContext = await context(devOptions)
34+
35+
// Add watch mode for development
36+
await buildContext.watch()
37+
}
38+
39+
// Production Build
40+
async function buildProdCode() {
41+
const prodOptions = {
42+
...commonOptions,
43+
minify: true // Minify in production
44+
}
45+
46+
await build(prodOptions)
47+
}
48+
49+
async function buildCode() {
50+
if (process.argv.includes('--watch')) {
51+
// If '--watch' argument is provided, run development build
52+
await buildDevCode()
53+
console.log('Built code in development watch mode.')
54+
} else {
55+
// Otherwise, run production build
56+
await buildProdCode()
57+
console.log('Production Build Ready!')
58+
}
59+
}
60+
61+
buildCode().catch(() => {
62+
process.exit(1)
63+
})

apps/wapi-ai-chatbot/constants.ts

Whitespace-only changes.

apps/wapi-ai-chatbot/environment.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
declare global {
2+
namespace NodeJS {
3+
interface ProcessEnv {
4+
WHATSAPP_API_ACCESS_TOKEN: string
5+
WHATSAPP_PHONE_NUMBER_ID: string
6+
WHATSAPP_BUSINESS_ACCOUNT_ID: string
7+
WHATSAPP_WEBHOOK_SECRET: string
8+
OPEN_AI_API_KEY: string
9+
OPEN_AI_ORG_ID: string
10+
OPEN_AI_PROJECT_ID: string
11+
12+
}
13+
}
14+
}
15+
16+
export { }

apps/wapi-ai-chatbot/nodemon.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"watch": [
3+
"./dist/src/index.js"
4+
],
5+
"ignore": [
6+
"node_modules/*.*"
7+
],
8+
"ext": "js",
9+
"exec": "sleep 2 && NODE_ENV=development node -r dotenv/config ./dist/index.js dotenv_config_path=./.env.dev"
10+
}

apps/wapi-ai-chatbot/package.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "wapi-gpt",
3+
"version": "0.0.1",
4+
"scripts": {
5+
"watch": "pnpm tsc --watch",
6+
"dev": "concurrently 'pnpm build:dev' 'nodemon -L'",
7+
"build:dev": "node build.mjs --watch",
8+
"build:prod": "NODE_ENV=production node ./build.mjs",
9+
"lint": "pnpm eslint .",
10+
"pretty": "pnpm prettier --write \"src/**/*.ts\"",
11+
"clean-install": "rm -rf ./node_modules && pnpm install --frozen-lockfile"
12+
},
13+
"author": {
14+
"name": "Sarthak Jain",
15+
"email": "sarthak@softlancer.co",
16+
"url": "https://linkedin.com/in/sarthakjdev"
17+
},
18+
"license": "MIT",
19+
"dependencies": {
20+
"@wapijs/wapi.js": "workspace:*",
21+
"cache-manager": "^4.0.0",
22+
"ms": "^2.1.3",
23+
"openai": "^4.52.7"
24+
},
25+
"devDependencies": {
26+
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
27+
"@types/cache-manager": "^4.0.6",
28+
"@types/node": "^20.12.12",
29+
"@wapijs/eslint-config": "workspace:*",
30+
"@wapijs/prettier-config": "workspace:*",
31+
"@wapijs/typescript-config": "workspace:*",
32+
"esbuild": "^0.19.8",
33+
"esbuild-node-externals": "^1.11.0",
34+
"esbuild-plugin-tsc": "^0.4.0",
35+
"index.js": "link:esbuild-plugin-tsc/src/index.js",
36+
"nodemon": "^3.0.2",
37+
"typescript": "5.4.5"
38+
},
39+
"packageManager": "pnpm@9.1.0",
40+
"pnpm": {
41+
"patchedDependencies": {
42+
"@microsoft/tsdoc-config@0.16.2": "patches/@microsoft__tsdoc-config@0.16.2.patch"
43+
}
44+
}
45+
}

apps/wapi-ai-chatbot/src/index.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { TextMessage, type TextMessageEvent } from '@wapijs/wapi.js'
2+
import { whatsappClient } from './utils/client'
3+
import { askAi } from './utils/gpt'
4+
5+
async function init() {
6+
try {
7+
whatsappClient.on('Ready', () => {
8+
console.log('Client is ready')
9+
})
10+
11+
whatsappClient.on('Error', error => {
12+
console.error('Error', error.message)
13+
})
14+
15+
whatsappClient.on('TextMessage', async (event: TextMessageEvent) => {
16+
const aiResponse = await askAi(event.text.data.text, event.context.from)
17+
const response = await event.reply({
18+
message: new TextMessage({
19+
text: aiResponse
20+
})
21+
})
22+
console.log({ response })
23+
})
24+
25+
whatsappClient.initiate()
26+
} catch (error) {
27+
console.error(error)
28+
// ! TODO: you may prefer to send a notification to your slack channel or email here
29+
}
30+
}
31+
32+
init().catch(error => console.error(error))
33+
34+
process.on('unhandledRejection', error => {
35+
console.error('unhandledRejection', error)
36+
process.exit(1)
37+
})
38+
39+
process.on('uncaughtException', error => {
40+
console.error('uncaughtException', error)
41+
process.exit(1)
42+
})

apps/wapi-ai-chatbot/src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export enum AiConversationRoleEnum {
2+
User = 'user',
3+
Ai = 'assistant'
4+
}
5+
6+
export type ConversationMessageType = {
7+
role: AiConversationRoleEnum
8+
content: string
9+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { caching } from 'cache-manager'
2+
3+
const cacheStore = caching({
4+
store: 'memory'
5+
})
6+
7+
export async function cacheData(params: { key: string; data: any; ttl?: number }) {
8+
const { key, ttl, data } = params
9+
await cacheStore.set(key, data, { ...(ttl ? { ttl: ttl } : {}) })
10+
}
11+
12+
export async function getCachedData<T>(key: string): Promise<T> {
13+
const response = await cacheStore.get(key)
14+
console.log(response)
15+
return response as T
16+
}
17+
18+
export function computeCacheKey(params: { id: string; context: string }) {
19+
return `${params.id}-${params.context}`
20+
}
21+
22+
export function getConversationContextCacheKey(phoneNumber: string) {
23+
return computeCacheKey({
24+
id: phoneNumber,
25+
context: 'conversation'
26+
})
27+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Client } from '@wapijs/wapi.js'
2+
3+
const WHATSAPP_BUSINESS_ACCOUNT_ID = process.env.WHATSAPP_BUSINESS_ACCOUNT_ID
4+
const WHATSAPP_API_ACCESS_TOKEN = process.env.WHATSAPP_API_ACCESS_TOKEN
5+
const WHATSAPP_PHONE_NUMBER_ID = process.env.WHATSAPP_PHONE_NUMBER_ID
6+
const WHATSAPP_WEBHOOK_SECRET = process.env.WHATSAPP_WEBHOOK_SECRET
7+
8+
if (
9+
!WHATSAPP_API_ACCESS_TOKEN ||
10+
!WHATSAPP_BUSINESS_ACCOUNT_ID ||
11+
!WHATSAPP_PHONE_NUMBER_ID ||
12+
!WHATSAPP_WEBHOOK_SECRET
13+
) {
14+
throw new Error('Configs not defined!')
15+
}
16+
17+
export const whatsappClient = new Client({
18+
apiAccessToken: WHATSAPP_API_ACCESS_TOKEN,
19+
businessAccountId: WHATSAPP_BUSINESS_ACCOUNT_ID,
20+
phoneNumberId: WHATSAPP_PHONE_NUMBER_ID,
21+
port: 8080,
22+
webhookEndpoint: '/webhook',
23+
webhookSecret: WHATSAPP_WEBHOOK_SECRET
24+
})

apps/wapi-ai-chatbot/src/utils/gpt.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { OpenAI } from 'openai'
2+
import { cacheData, computeCacheKey, getCachedData, getConversationContextCacheKey } from './cache'
3+
import { AiConversationRoleEnum, type ConversationMessageType } from '~/types'
4+
5+
const openAiApiKey = process.env.OPEN_AI_API_KEY
6+
const organizationId = process.env.OPEN_AI_ORG_ID
7+
const projectId = process.env.OPEN_AI_PROJECT_ID
8+
9+
if (!openAiApiKey || !organizationId || !projectId) {
10+
throw new Error('OPEN_AI_API_KEY not defined!')
11+
}
12+
13+
const OpenApiClient = new OpenAI({
14+
apiKey: openAiApiKey,
15+
project: projectId,
16+
organization: organizationId
17+
})
18+
19+
export async function askAi(message: string, fromPhoneNumber: string): Promise<string> {
20+
try {
21+
const contextCacheKey = getConversationContextCacheKey(fromPhoneNumber)
22+
const context = await getCachedData<ConversationMessageType[]>(contextCacheKey)
23+
24+
let response = 'Sorry, I am not able to understand that.'
25+
const responseCacheKey = computeCacheKey({
26+
context: 'response_cache',
27+
id: message.trim()
28+
})
29+
const cachedResponse = await getCachedData<string>(responseCacheKey)
30+
if (cachedResponse) {
31+
response = cachedResponse
32+
} else {
33+
const chatCompletion = await OpenApiClient.chat.completions.create({
34+
messages: [
35+
{
36+
role: 'system',
37+
content: `You are an intelligent assistant helping a user with their queries. You can provide information, answer questions, and help the user complete tasks.
38+
You are a developer relation engineer for a product named as Wapi.js.
39+
Wapi.js is an SDK to build chat bots and to integrate WhatsApp API with your application very easily.
40+
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.
41+
Users can use the SDK to build chatbots for customer support, marketing, notifications, and more! Leverage WhatsApp Business API.
42+
You are required to give very to the point and very clear answers to the user queries.
43+
Also, you can reveal your identity, ack like a human, and provide information about the product.
44+
Never ever accept that you are an AI or a chatbot or an intelligent assistant.
45+
`
46+
},
47+
...(context ? context : []),
48+
{
49+
role: 'user',
50+
content: message
51+
}
52+
],
53+
model: 'gpt-4'
54+
})
55+
console.log(JSON.stringify({ chatCompletion }))
56+
const aiResponse = chatCompletion.choices[0].message.content
57+
if (aiResponse) response = aiResponse
58+
}
59+
60+
await cacheData({
61+
key: contextCacheKey,
62+
data: [
63+
...(context ? context : []),
64+
{
65+
role: AiConversationRoleEnum.User,
66+
content: message
67+
},
68+
{
69+
role: AiConversationRoleEnum.Ai,
70+
content: response
71+
}
72+
]
73+
})
74+
return response
75+
} catch (error) {
76+
console.log({ error })
77+
return 'Sorry, I am not able to understand that.'
78+
}
79+
}

0 commit comments

Comments
 (0)