forked from wassengerhq/whatsapp-chatgpt-bot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
bot.js
176 lines (148 loc) · 6.03 KB
/
bot.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import OpenAI from 'openai'
import config from './config.js'
import { state } from './store.js'
import * as actions from './actions.js'
// Initialize OpenAI client
const ai = new OpenAI({ apiKey: config.openaiKey })
// Determine if a given inbound message can be replied by the AI bot
function canReply ({ data, device }) {
const { chat } = data
// Skip if chat is already assigned to an team member
if (chat.owner && chat.owner.agent) {
return false
}
// Ignore messages from group chats
if (chat.type !== 'chat') {
return false
}
// Skip replying chat if it has one of the configured labels, when applicable
if (config.skipChatWithLabels && config.skipChatWithLabels.length && chat.labels && chat.labels.length) {
if (config.skipChatWithLabels.some(label => chat.labels.includes(label))) {
return false
}
}
// Only reply to chats that were whitelisted, when applicable
if (config.numbersWhitelist && config.numbersWhitelist.length && chat.fromNumber) {
if (config.numbersWhitelist.some(number => number === chat.fromNumber || chat.fromNumber.slice(1) === number)) {
return true
} else {
return false
}
}
// Skip replying to chats that were explicitly blacklisted, when applicable
if (config.numbersBlacklist && config.numbersBlacklist.length && chat.fromNumber) {
if (config.numbersBlacklist.some(number => number === chat.fromNumber || chat.fromNumber.slice(1) === number)) {
return false
}
}
// Skip replying chats that were archived, when applicable
if (config.skipArchivedChats && (chat.status === 'archived' || chat.waStatus === 'archived')) {
return false
}
// Always ignore replying to banned chats/contacts
if ((chat.status === 'banned' || chat.waStatus === 'banned ')) {
return false
}
return true
}
// Send message back to the user and perform post-message required actions like
// adding labels to the chat or updating the chat's contact metadata
function replyMessage ({ data, device }) {
return async ({ message, ...params }) => {
const { phone } = data.chat.contact
await actions.sendMessage({
phone,
device: device.id,
message,
...params
})
// Add bot-managed chat labels, if required
if (config.setLabelsOnBotChats.length) {
const labels = config.setLabelsOnBotChats.filter(label => (data.chat.labels || []).includes(label))
if (labels.length) {
await actions.updateChatLabels({ data, device, labels })
}
}
// Add bot-managed chat metadata, if required
if (config.setMetadataOnBotChats.length) {
const metadata = config.setMetadataOnBotChats.filter(entry => entry && entry.key && entry.value).map(({ key, value }) => ({ key, value }))
await actions.updateChatMetadata({ data, device, metadata })
}
}
}
// Process message received from the user on every new inbound webhook event
export async function processMessage ({ data, device } = {}) {
// Can reply to this message?
if (!canReply({ data, device })) {
return console.log('[info] Skip message due to chat already assigned or not eligible to reply:', data.fromNumber, data.date, data.body)
}
const reply = replyMessage({ data, device })
const { chat } = data
const body = data?.body?.trim().slice(0, 1000)
console.log('[info] New inbound message received:', chat.id, body || '<empty message>')
// First inbound message, reply with a welcome message
if (!data.chat.lastOutboundMessageAt || data.meta.isFirstMessage) {
const message = `${config.welcomeMessage}\n\n${config.defaultMessage}}`
return await reply({ message })
}
if (!body) {
// Default to unknown command response
const unknownCommand = `${config.unknownCommandMessage}\n\n${config.defaultMessage}`
await reply({ message: unknownCommand })
}
// Assign the chat to an random agent
if (/^human|person|help|stop$/i.test(body) || /^human/i.test(body)) {
actions.assignChatToAgent({ data, device }).catch(err => {
console.error('[error] failed to assign chat to user:', data.chat.id, err.message)
})
return await reply({
message: `This chat was assigned to a member of our support team. You will be contacted shortly.`,
})
}
// Generate response using AI
if (!state[data.chat.id]) {
console.log('[info] fetch previous messages history for chat:', data.chat.id)
await actions.pullChatMessages({ data, device })
}
// Compose chat previous messages to context awareness better AI responses
const previousMessages = Object.values(state[data.chat.id] || {})
.reverse()
.slice(0, 40)
.map(message => ({
role: message.flow === 'inbound' ? 'user' : 'assistant',
content: message.body
}))
.filter(message => message.content).slice(-20)
const messages = [
{ role: 'system', content: config.botInstructions },
...previousMessages,
{ role: 'user', content: body }
]
// Generate response using AI
const completion = await ai.chat.completions.create({
messages,
temperature: 0.2,
model: config.openaiModel,
user: `${device.id}_${chat.id}`,
functions: config.openaiFunctions || []
})
// Reply with the AI generated message
if (completion.choices && completion.choices.length) {
const [response] = completion.choices
// If response is a function call, return the custom result
if (response.message.function_call && response.message.function_call.name) {
const func = config.functions[response.message.function_call.name]
if (typeof func === 'function') {
const message = await func({ response, data, device, messages })
await reply({ message })
} else {
console.error('[warning] missing function call in config.functions', response.message.function_call.name)
}
}
// Otherwise forward the AI generate message
return await reply({ message: response.message.content || config.unknownCommandMessage })
}
// Unknown default response
const unknownCommand = `${config.unknownCommandMessage}\n\n${config.defaultMessage}`
await reply({ message: unknownCommand })
}