forked from aloverso/loanbot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
conversation_handler.py
329 lines (279 loc) · 14.6 KB
/
conversation_handler.py
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
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
import time
import send_email_to_librarian
from messenger_client import MessengerClient
class ConversationHandler(object):
"""A class that deals with the messages we receive from users."""
def __init__(self, database_client):
"""Create a new conversation handler with a given database client."""
self.database_client = database_client
self.checkout_words = ['check', 'checking', 'checked', 'check out', 'checkout', 'checking out', 'take', 'took', 'taking', 'grabbing', 'grab', 'grabbed', 'checked out', 'borrow', 'borrowed', 'want']
self.return_words = ['return', 'returned', 'returning', 'brought', 'bring', 'bringing', 'dropping', 'dropped', 'took back', 'left', 'done', 'done with', 'finished']
self.closing_words = ['thanks', 'thank', 'ok', 'bye', 'goodbye', 'good-bye', 'okay', 'cancel', 'stop', 'fuck', 'yay']
self.available_words = ['available', 'there']
self.help_words = ['how do i', 'help', 'manual', 'documentation', 'how to', 'trouble', 'confused', 'what do i do with', 'what should i do', "i don't know"]
self.NO_CONTACT = 0
self.SENT_GREETING = 1
self.WANT_CHECKOUT = 2
self.CONFIRM_TOOL = 4
self.HOW_LONG = 5
self.CLOSING = 6
self.WANT_RETURN = 7
self.CONFIRM_TOOL_RETURN = 8
self.AVAILABILITY_QUESTION = 9
self.SEND_LIST = 10
def find_tools_in_message(self, message):
"""Find the tools in a message.
Search through a message looking for names of tools from the tools database.
Return a list of tool names found, empty if none found.
"""
found_tools = []
tools_list = self.database_client.get_all_tools()
# find tools with a name or alternate_names in the message
for tool in tools_list:
if tool['name'] in message:
found_tools.append(tool)
else:
for alt_name in tool['alternate_names']:
if alt_name in message:
found_tools.append(tool)
return found_tools
def make_tool_string(self, tool_list):
"""Create a string of all tools a user is attempting to check out."""
tool_string = ''
print('temp_tools', tool_list)
for tool in tool_list:
tool_string = tool_string + tool['name'] + " and " # allow for a list of tools
# remove final and from string
tool_string = tool_string[:-5]
print('tool string:', tool_string)
return tool_string
def parse_due_date(self, message):
"""Find the due date in a loan time quick reply message.
Parses the message to store a due_date
for the tool/s the user wants to check out. uses import time
"""
# TODO: handle the case when we somehow get a different message
# than the quick reply options were expecting in a way other than
# making the due date "0"
due_date = 0
SECONDS_IN_DAY = 3600 * 24
# they want a 24 hour loan
if message == 'yes':
due_date = int(time.time()) + 120 # FIXME !!!!!! CHANGE THIS BACK TO SECONDS_IN_DAY!!!!!!
# they want a 12 hour loan
elif message == '12 hours instead':
due_date = int(time.time()) + (SECONDS_IN_DAY / 2)
# they want a 3 day loan
elif message == '3 days instead':
due_date = int(time.time()) + (SECONDS_IN_DAY * 3)
return due_date
def determine_response_for_user(self, message, user):
"""Use the user's stage to parse the message and determine how to reply.
Takes the message text string and a user (in dictionary format).
Parameters
----------
updated_user
The user dictionary, possibly changed or updated_user
response_text : string
The bot's response message
quickreply : list
A field indicating whether this should be a quickreply response
it either has the None value (not a quickreply message)
or a list of quickreply options
Returns
-------
(updated_user, response_text, quickreply)
"""
print('determine_response_for_user')
if any(word in message for word in self.closing_words):
response = "Glad to help!"
user['stage'] = self.NO_CONTACT
print(user['stage'])
return user, response, None
if any(word in message for word in self.help_words):
response = ''
tool_help_wanted = self.find_tools_in_message(message)
if len(tool_help_wanted) > 0:
resource_links = ''
for tool in tool_help_wanted:
resource_links += ' ' + tool['resource_link']
response = "The Library gave me some resources that might be helpful, see if this is useful:" + resource_links
else:
response = "😵 I have no clue how to help you with this one! I've passed your question along to the librarians. Hopefully they know what to do and will contact you soon. 😅"
# TODO: send email to librarian here
return user, response, None
# this needs to be located above the NO_CONTACT check
# because if they said anything that's NOT "view more", then
# it needs to be treated like a NO_CONTACT message context
if user['stage'] == self.SEND_LIST:
user['stage'] = self.NO_CONTACT
print(user['stage'])
if message == 'view more':
response = "Check The Library's online database for the full tool list: https://olin.tind.io/"
return user, response, None
# if the user is initiating contact
if user['stage'] == self.NO_CONTACT:
# trying to return
if any(word in message for word in self.return_words):
user['stage'] = self.WANT_RETURN
print(user['stage'])
# checking availability status
elif any(word in message for word in self.available_words):
tools_wanted = self.find_tools_in_message(message)
response_string = ''
quickreply = None
if len(tools_wanted) > 0:
unavailable_tools = []
for tool in tools_wanted:
available_modifier = ''
if tool['current_user'] is not None:
available_modifier = 'not '
unavailable_tools.append(tool)
response_string += 'the {} is {}available and '.format(tool['name'], available_modifier)
response_string = response_string[:-5]
if len(unavailable_tools) > 0:
question = 'Would you like me to ask the tool borrowers to return them?'
response_string = response_string + '. ' + question
user['temp_tools'] = unavailable_tools
user['stage'] = self.AVAILABILITY_QUESTION
print(user['stage'])
quickreply = ['yes', 'no']
else:
response_string = "SEND_LIST"
user['stage'] = self.SEND_LIST
print(user['stage'])
return user, response_string, quickreply
# checking out
elif any(word in message for word in self.checkout_words):
user['stage'] = self.WANT_CHECKOUT
print(user['stage'])
else:
# send greeting and ask what tool
response = "😄 Hi there! I'm Loan Wrangler, what can I help you with?"
# user['stage'] = self.SENT_GREETING
return user, response, None
# if the user has asked about availability and we're finding out if we should
# send a reminder to the borrowers or not
if user['stage'] == self.AVAILABILITY_QUESTION:
if message == 'yes':
for tool in user['temp_tools']:
borrower_id = tool['current_user']
borrower_sender_id = self.database_client.find_user('_id', borrower_id)['sender_id']
# this is not the best code structure
# because we have this weird situation where the user we want to send a message to
# is not the user who sent us a message
messenger_client = MessengerClient()
reminder = "Hey, someone's interested in borrowing the {} that you have checked out. If you're done with it, could you bring it back?".format(tool['name'])
messenger_client.send_message(borrower_sender_id, reminder, None)
user['stage'] = self.NO_CONTACT
print(user['stage'])
user['temp_tools'] = []
return user, "Alright, I let them know someone's looking for it! 🔎", None
else:
user['stage'] = self.NO_CONTACT
print(user['stage'])
user['temp_tools'] = []
return user, "☺️ Alrighty. Is there something else I can help with?", None
# if the user wants to check out something
if user['stage'] == self.WANT_CHECKOUT or user['stage'] == self.SENT_GREETING:
tools_wanted = self.find_tools_in_message(message)
user['temp_tools'] = tools_wanted
# if we found a tool name/s in the message
if len(tools_wanted) > 0:
tool_string = self.make_tool_string(user['temp_tools'])
print('tool string in line:', tool_string)
response = "Sounds like you want to check out a {}, is that correct?".format(tool_string)
user['stage'] = self.CONFIRM_TOOL
print(user['stage'])
return user, response, ['yes', 'no']
# if we could not identify a tool name/s in the message
else:
user['stage'] = self.NO_CONTACT
print(user['stage'])
return user, "What can I do for ya?", None
# we check that we parsed the correct tool/s...
if user['stage'] == self.CONFIRM_TOOL:
# ...if so, we find out how long the loan will be
if message == 'yes':
available = True
tools_out = []
# check if those tools are in right now
for tool in user['temp_tools']:
if tool['current_user'] is not None:
available = False
tools_out.append(tool)
if available:
response = "Great! Is a loan time of 1 day okay?"
user['stage'] = self.HOW_LONG
print(user['stage'])
return user, response, ['yes', '12 hours instead', '3 days instead']
else:
response = "😓 Sorry, the following tools are not available right now: {}".format(self.make_tool_string(tools_out))
user['stage'] = self.NO_CONTACT
print(user['stage'])
return user, response, None
# ...if not, we try again
else:
user['temp_tools'] = []
user['stage'] = self.NO_CONTACT
print(user['stage'])
return user, "😵 Sorry I misunderstood. What do you want to do?", None
# update user and tool db based on the loan time
if user['stage'] == self.HOW_LONG:
tool_string = self.make_tool_string(user['temp_tools'])
for tool in user['temp_tools']:
tool['current_user'] = user['_id']
tool['current_due_date'] = self.parse_due_date(message)
self.database_client.update_tool(tool)
user['tools'].append(tool['_id'])
# TODO: how to handle loan time if they are checking out more than one tool
# finish the interaction and reset the conversation stage
response = "😎 You're all set! I'll remind you to return the {} before it's due.".format(tool_string)
user['temp_tools'] = []
user['stage'] = self.NO_CONTACT
print(user['stage'])
return user, response, None
if user['stage'] == self.CONFIRM_TOOL_RETURN:
# ...if so, we find out how long the loan will be
if message == 'yes':
tool_string = self.make_tool_string(user['temp_tools'])
# TODO: tell them if they're trying to return something they don't have
# update tool
for tool in user['temp_tools']:
if tool['current_user'] == user['_id']:
tool['current_user'] = None
tool['current_due_date'] = None
self.database_client.update_tool(tool)
# update user tool list
for checked_out_tool_id in user['tools']:
if checked_out_tool_id == tool['_id']:
user['tools'].remove(checked_out_tool_id)
user['temp_tools'] = []
user['stage'] = self.NO_CONTACT
print(user['stage'])
return user, "✨🏆✨ Thanks!!!! I'll let The Library know the {} has returned.".format(tool_string), None
# ...if not, we try again
else:
user['temp_tools'] = []
user['stage'] = self.WANT_RETURN
print(user['stage'])
return user, "😓 Sorry I misunderstood. What tool do you want to return?", None
if user['stage'] == self.WANT_RETURN:
tools_returning = self.find_tools_in_message(message)
user['temp_tools'] = tools_returning
# if we found a tool name/s in the message
if len(tools_returning) > 0:
tool_string = self.make_tool_string(user['temp_tools'])
print('tool string in line:', tool_string)
response = "You're returning a {}, is that right?".format(tool_string)
user['stage'] = self.CONFIRM_TOOL_RETURN
print(user['stage'])
return user, response, ['yes', 'no']
# if we could not identify a tool name/s in the message
else:
user['stage'] = self.WANT_RETURN
print(user['stage'])
return user, "Which tool did you want to return?", None
print('I GOT TO THE END, OH NO')
return user
# TODO: check for cancelling