Skip to content

Commit

Permalink
chore: add example ai chat bot using wapi.js and fix app crash on API…
Browse files Browse the repository at this point in the history
… error parsing (#37)
  • Loading branch information
sarthakjdev committed Jul 20, 2024
1 parent e9f68d2 commit 918da57
Show file tree
Hide file tree
Showing 38 changed files with 5,930 additions and 5,289 deletions.
16 changes: 15 additions & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,24 @@ jobs:
node-version: 20

- name: Install pnpm
uses: pnpm/action-setup@v2.2.4
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

Expand Down
15 changes: 14 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,23 @@ jobs:
registry-url: https://registry.npmjs.org/

- name: Install pnpm
uses: pnpm/action-setup@v2.2.4
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 i --frozen-lockfile

Expand Down
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.
5 changes: 5 additions & 0 deletions apps/wapi-ai-chatbot/.env.example
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=
2 changes: 2 additions & 0 deletions apps/wapi-ai-chatbot/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist
.eslintrc.js
4 changes: 4 additions & 0 deletions apps/wapi-ai-chatbot/.eslintrc.js
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']
}
1 change: 1 addition & 0 deletions apps/wapi-ai-chatbot/.prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@wapijs/prettier-config/config.node')
63 changes: 63 additions & 0 deletions apps/wapi-ai-chatbot/build.mjs
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.
16 changes: 16 additions & 0 deletions apps/wapi-ai-chatbot/environment.d.ts
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 { }
10 changes: 10 additions & 0 deletions apps/wapi-ai-chatbot/nodemon.json
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"
}
45 changes: 45 additions & 0 deletions apps/wapi-ai-chatbot/package.json
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": "sarthak@softlancer.co",
"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": "pnpm@9.1.0",
"pnpm": {
"patchedDependencies": {
"@microsoft/tsdoc-config@0.16.2": "patches/@microsoft__tsdoc-config@0.16.2.patch"
}
}
}
42 changes: 42 additions & 0 deletions apps/wapi-ai-chatbot/src/index.ts
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)
})
9 changes: 9 additions & 0 deletions apps/wapi-ai-chatbot/src/types.ts
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
}
27 changes: 27 additions & 0 deletions apps/wapi-ai-chatbot/src/utils/cache.ts
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'
})
}
24 changes: 24 additions & 0 deletions apps/wapi-ai-chatbot/src/utils/client.ts
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
})
79 changes: 79 additions & 0 deletions apps/wapi-ai-chatbot/src/utils/gpt.ts
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.'
}
}
Loading

0 comments on commit 918da57

Please sign in to comment.