forked from GoogleCloudPlatform/nodejs-docs-samples
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdemo_bot.js
executable file
·260 lines (213 loc) · 8.51 KB
/
demo_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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
/* *****************************************************************************
Copyright 2016 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
********************************************************************************
This is a Slack bot built using the Botkit library (http://howdy.ai/botkit). It
runs on a Kubernetes cluster, and uses one of the Google Cloud Platform's ML
APIs, the Natural Language API, to interact in a Slack channel. It does this in
two respects.
First, it uses the NL API to assess the "sentiment" of any message posted to
the channel, and if the positive or negative magnitude of the statement is
sufficiently large, it sends a 'thumbs up' or 'thumbs down' in reaction.
Second, it uses the NL API to identify the 'entities' in each posted message,
and tracks them in a database (using sqlite3). Then, at any time you can
query the NL slackbot to ask it for the top N entities used in the channel.
The README walks through how to run the NL slackbot as an app on a
Google Container Engine/Kubernetes cluster, but you can also just run the bot
locally if you like.
To do this, create a file containing your Slack token (as described in
the README), then point 'SLACK_TOKEN_PATH' to that file when you run the script:
echo my-slack-token > slack-token
SLACK_TOKEN_PATH=./slack-token node demo_bot.js
See the README.md in this directory for more information about setup and usage.
*/
'use strict';
const Botkit = require('botkit');
const fs = require('fs');
const Language = require('@google-cloud/language');
const path = require('path');
const sqlite3 = require('sqlite3').verbose();
const controller = Botkit.slackbot({ debug: false });
// create our database if it does not already exist.
const db = new sqlite3.cached.Database(path.join(__dirname, './slackDB.db'));
// comment out the line above, and instead uncomment the following, to store
// the db on a persistent disk mounted at /var/sqlite3. See the README
// section on 'using a persistent disk' for this config.
// const db = new sqlite3.cached.Database('/var/sqlite3/slackDB.db');
// the number of most frequent entities to retrieve from the db on request.
const NUM_ENTITIES = 20;
// The magnitude of sentiment of a posted text above which the bot will respond.
const SENTIMENT_THRESHOLD = 30;
const SEVEN_DAYS_AGO = 60 * 60 * 24 * 7;
const ENTITIES_BASE_SQL = `SELECT name, type, count(name) as wc
FROM entities`;
const ENTITIES_SQL = ` GROUP BY name ORDER BY wc DESC
LIMIT ${NUM_ENTITIES};`;
const TABLE_SQL = `CREATE TABLE if not exists entities (
name text,
type text,
salience real,
wiki_url text,
ts integer
);`;
function startController () {
if (!process.env.SLACK_TOKEN_PATH) {
throw new Error('Please set the SLACK_TOKEN_PATH environment variable!');
}
let token = fs.readFileSync(process.env.SLACK_TOKEN_PATH, { encoding: 'utf8' });
token = token.replace(/\s/g, '');
// Create the table that will store entity information if it does not already
// exist.
db.run(TABLE_SQL);
controller
.spawn({ token: token })
.startRTM((err) => {
if (err) {
console.error('Failed to start controller!');
console.error(err);
process.exit(1);
}
});
return controller
// If the bot gets a DM or mention with 'hello' or 'hi', it will reply. You
// can use this to sanity-check your app without needing to use the NL API.
.hears(
['hello', 'hi'],
['direct_message', 'direct_mention', 'mention'],
handleSimpleReply
)
// If the bot gets a DM or mention including "top entities", it will reply with
// a list of the top N most frequent entities used in this channel, as derived
// by the NL API.
.hears(
['top entities'],
['direct_message', 'direct_mention', 'mention'],
handleEntitiesReply
)
// For any posted message, the bot will send the text to the NL API for
// analysis.
.on('ambient', handleAmbientMessage)
.on('rtm_close', startBot);
}
function startBot (bot, cerr) {
console.error('RTM closed');
let token = fs.readFileSync(process.env.SLACK_TOKEN_PATH, { encoding: 'utf8' });
token = token.replace(/\s/g, '');
bot
.spawn({ token: token })
.startRTM((err) => {
if (err) {
console.error('Failed to start controller!');
console.error(err);
process.exit(1);
}
});
}
function handleSimpleReply (bot, message) {
bot.reply(message, 'Hello.');
}
function handleEntitiesReply (bot, message) {
bot.reply(message, 'Top entities: ');
// Query the database for the top N entities in the past week
const queryTs = Math.floor(Date.now() / 1000) - SEVEN_DAYS_AGO;
// const entitiesWeekSql = `select * from entities`;
const entitiesWeekSql = `${ENTITIES_BASE_SQL} WHERE ts > ${queryTs}${ENTITIES_SQL}`;
db.all(entitiesWeekSql, (err, topEntities) => {
if (err) {
throw err;
}
let entityInfo = '';
// Uncomment this to see the query results logged to console:
// console.log(topEntities);
topEntities.forEach((entity) => {
entityInfo += `entity: *${entity.name}*, type: ${entity.type}, count: ${entity.wc}\n`;
});
bot.reply(message, entityInfo);
});
}
function analyzeEntities (text, ts) {
// Instantiates a client
const language = Language();
// Instantiates a Document, representing the provided text
const document = language.document({
// The document text, e.g. "Hello, world!"
content: text
});
// Detects entities in the document
return document.detectEntities()
.then((results) => {
const entities = results[1].entities;
entities.forEach((entity) => {
const name = entity.name;
const type = entity.type;
const salience = entity.salience;
let wikiUrl = '';
if (entity.metadata.wikipedia_url) {
wikiUrl = entity.metadata.wikipedia_url;
}
// Uncomment this to see the entity info logged to console:
// console.log(`${name}, type: ${type}, w url: ${wikiUrl}, salience: ${salience}, ts: ${ts}`);
db.run(
'INSERT INTO entities VALUES (?, ?, ?, ?, ?);',
[name, type, salience, wikiUrl, Math.round(ts)]
);
});
return entities;
});
}
function analyzeSentiment (text) {
// Instantiates a client
const language = Language();
// Instantiates a Document, representing the provided text
const document = language.document({
// The document text, e.g. "Hello, world!"
content: text
});
// Detects the 'sentiment' of some text using the NL API
return document.detectSentiment()
.then((results) => {
const sentiment = results[0];
// Uncomment the following four lines to log the sentiment to the console:
// if (results >= SENTIMENT_THRESHOLD) {
// console.log('Sentiment: positive.');
// } else if (results <= -SENTIMENT_THRESHOLD) {
// console.log('Sentiment: negative.');
// }
return sentiment;
});
}
function handleAmbientMessage (bot, message) {
// Note: for purposes of this example, we're making two separate calls to the
// API, one to extract the entities from the message, and one to analyze the
// 'sentiment' of the message. These could be combined into one call.
return analyzeEntities(message.text, message.ts)
.then(() => analyzeSentiment(message.text))
.then((sentiment) => {
if (sentiment >= SENTIMENT_THRESHOLD) {
// We have a positive sentiment of magnitude larger than the threshold.
bot.reply(message, ':thumbsup:');
} else if (sentiment <= -SENTIMENT_THRESHOLD) {
// We have a negative sentiment of magnitude larger than the threshold.
bot.reply(message, ':thumbsdown:');
}
});
}
exports.ENTITIES_SQL = ENTITIES_SQL;
exports.TABLE_SQL = TABLE_SQL;
exports.startController = startController;
exports.handleSimpleReply = handleSimpleReply;
exports.handleEntitiesReply = handleEntitiesReply;
exports.analyzeEntities = analyzeEntities;
exports.analyzeSentiment = analyzeSentiment;
exports.handleAmbientMessage = handleAmbientMessage;
if (require.main === module) {
startController();
}