/
actions.py
699 lines (555 loc) · 23.1 KB
/
actions.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
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
# -*- coding: utf-8 -*-
import logging
from datetime import datetime
from typing import Text, Dict, Any, List
import json
from rasa_core_sdk import Action, Tracker, ActionExecutionRejection
from rasa_core_sdk.executor import CollectingDispatcher
from rasa_core_sdk.forms import FormAction, REQUESTED_SLOT
from rasa_core_sdk.events import (
SlotSet,
UserUtteranceReverted,
ConversationPaused,
FollowupAction,
Form,
)
from demo.api import MailChimpAPI
from demo import config
from demo.gdrive_service import GDriveService
logger = logging.getLogger(__name__)
class SubscribeNewsletterForm(FormAction):
"""Asks for the user's email, call the newsletter API and sign up user"""
def name(self):
return "subscribe_newsletter_form"
@staticmethod
def required_slots(tracker):
return ["email"]
def validate(self, dispatcher, tracker, domain):
# type: (CollectingDispatcher, Tracker, Dict[Text, Any]) -> List[Dict]
"""Validate extracted value of requested slot
else reject execution of the form action
If no extraction, re-ask only if intent is enter_data
"""
# extract other slots that were not requested
# but set by corresponding entity or trigger intent mapping
slot_values = self.extract_other_slots(dispatcher, tracker, domain)
# extract requested slot
slot_to_fill = tracker.get_slot(REQUESTED_SLOT)
if slot_to_fill:
slot_values.update(self.extract_requested_slot(dispatcher, tracker, domain))
if not slot_values:
# if no email entity was picked up, but intent was
# enter_data, ask again instead of leaving the form
intent = tracker.latest_message["intent"].get("name")
if intent == "enter_data":
dispatcher.utter_template("utter_no_email", tracker)
return []
# reject to execute the form action
# if some slot was requested but nothing was extracted
# it will allow other policies to predict another action
raise ActionExecutionRejection(
self.name(),
"Failed to extract slot {0} "
"with action {1}"
"".format(slot_to_fill, self.name()),
)
# validation succeed, set slots to extracted values
return [SlotSet(slot, value) for slot, value in slot_values.items()]
def submit(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List[Dict]:
"""Once we have an email, attempt to add it to the database"""
email = tracker.get_slot("email")
client = MailChimpAPI(config.mailchimp_api_key)
# if the email is already subscribed, this returns False
added_to_list = client.subscribe_user(config.mailchimp_list, email)
# utter submit template
if added_to_list:
dispatcher.utter_template("utter_confirmationemail", tracker)
else:
dispatcher.utter_template("utter_already_subscribed", tracker)
return []
class SalesForm(FormAction):
"""Collects sales information and adds it to the spreadsheet"""
def name(self):
return "sales_form"
@staticmethod
def required_slots(tracker):
return [
"job_function",
"use_case",
"budget",
"person_name",
"company",
"business_email",
]
def slot_mappings(self):
# type: () -> Dict[Text: Union[Dict, List[Dict]]]
"""A dictionary to map required slots to
- an extracted entity
- intent: value pairs
- a whole message
or a list of them, where a first match will be picked"""
# For each slot other than email, if the entity isn't picked up, store
# the entire user utterance. In future this should be stored in a
# `<slot>_unconfirmed` slot where the user will then be asked to
# confirm this is their <slot>.
return {
"job_function": [
self.from_entity(entity="job_function"),
self.from_text(intent="enter_data"),
],
"use_case": self.from_text(intent="enter_data"),
"budget": [
self.from_entity(entity="amount-of-money"),
self.from_entity(entity="number"),
self.from_text(intent="enter_data"),
],
"person_name": [
self.from_entity(entity="name"),
self.from_text(intent="enter_data"),
],
"company": [
self.from_entity(entity="company"),
self.from_text(intent="enter_data"),
],
"business_email": self.from_entity(entity="email"),
}
def validate(self, dispatcher, tracker, domain):
# type: (CollectingDispatcher, Tracker, Dict[Text, Any]) -> List[Dict]
"""Validate extracted input, if no valid email found but intent is
enter_data, re-ask for email, otherwise let other policies take over"""
# extract other slots that were not requested
# but set by corresponding entity or trigger intent mapping
slot_values = self.extract_other_slots(dispatcher, tracker, domain)
# extract requested slot
slot_to_fill = tracker.get_slot(REQUESTED_SLOT)
if slot_to_fill:
slot_values.update(self.extract_requested_slot(dispatcher, tracker, domain))
if not slot_values:
# if no email entity was picked up, but intent was
# enter_data, ask again instead of leaving the form
intent = tracker.latest_message["intent"].get("name")
if slot_to_fill == "business_email" and intent == "enter_data":
dispatcher.utter_template("utter_no_email", tracker)
return []
# reject to execute the form action
# if some slot was requested but nothing was extracted
# it will allow other policies to predict another action
raise ActionExecutionRejection(
self.name(),
"Failed to extract slot {0} "
"with action {1}"
"".format(slot_to_fill, self.name()),
)
# validation succeed, set slots to extracted values
return [SlotSet(slot, value) for slot, value in slot_values.items()]
def submit(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List[Dict]:
"""Once we have all the information, attempt to add it to the
Google Drive database"""
import datetime
budget = tracker.get_slot("budget")
company = tracker.get_slot("company")
email = tracker.get_slot("business_email")
job_function = tracker.get_slot("job_function")
person_name = tracker.get_slot("person_name")
use_case = tracker.get_slot("use_case")
date = datetime.datetime.now().strftime("%d/%m/%Y")
sales_info = [company, use_case, budget, date, person_name, job_function, email]
gdrive = GDriveService()
try:
gdrive.store_data(sales_info)
dispatcher.utter_template("utter_confirm_salesrequest", tracker)
return []
except Exception as e:
logger.error(
"Failed to write data to gdocs. Error: {}" "".format(e.message),
exc_info=True,
)
dispatcher.utter_template("utter_salesrequest_failed", tracker)
return []
class ActionExplainSalesForm(Action):
"""Returns the explanation for the sales form questions"""
def name(self):
return "action_explain_sales_form"
def run(self, dispatcher, tracker, domain):
requested_slot = tracker.get_slot("requested_slot")
if requested_slot not in SalesForm.required_slots(tracker):
dispatcher.utter_message(
"Sorry, I didn't get that. Please rephrase or answer the question "
"above."
)
return []
dispatcher.utter_template("utter_explain_" + requested_slot, tracker)
return []
class ActionChitchat(Action):
"""Returns the chitchat utterance dependent on the intent"""
def name(self):
return "action_chitchat"
def run(self, dispatcher, tracker, domain):
intent = tracker.latest_message["intent"].get("name")
# retrieve the correct chitchat utterance dependent on the intent
if intent in [
"ask_builder",
"ask_weather",
"ask_howdoing",
"ask_whatspossible",
"ask_whatisrasa",
"ask_isbot",
"ask_howold",
"ask_languagesbot",
"ask_restaurant",
"ask_time",
"ask_wherefrom",
"ask_whoami",
"handleinsult",
"nicetomeeyou",
"telljoke",
"ask_whatismyname",
"ask_howbuilt",
"ask_whoisit",
]:
dispatcher.utter_template("utter_" + intent, tracker)
return []
class ActionFaqs(Action):
"""Returns the chitchat utterance dependent on the intent"""
def name(self):
return "action_faqs"
def run(self, dispatcher, tracker, domain):
intent = tracker.latest_message["intent"].get("name")
# retrieve the correct chitchat utterance dependent on the intent
if intent in [
"ask_faq_platform",
"ask_faq_languages",
"ask_faq_tutorialcore",
"ask_faq_tutorialnlu",
"ask_faq_opensource",
"ask_faq_voice",
"ask_faq_slots",
"ask_faq_channels",
"ask_faq_differencecorenlu",
"ask_faq_python_version",
"ask_faq_community_size",
"ask_faq_what_is_forum",
"ask_faq_tutorials",
]:
dispatcher.utter_template("utter_" + intent, tracker)
return []
class ActionPause(Action):
"""Pause the conversation"""
def name(self):
return "action_pause"
def run(self, dispatcher, tracker, domain):
return [ConversationPaused()]
class ActionStoreUnknownProduct(Action):
"""Stores unknown tools people are migrating from in a slot"""
def name(self):
return "action_store_unknown_product"
def run(self, dispatcher, tracker, domain):
# if we dont know the product the user is migrating from,
# store his last message in a slot.
return [SlotSet("unknown_product", tracker.latest_message.get("text"))]
class ActionStoreUnknownNluPart(Action):
"""Stores unknown parts of nlu which the user requests information on
in slot.
"""
def name(self):
return "action_store_unknown_nlu_part"
def run(self, dispatcher, tracker, domain):
# if we dont know the part of nlu the user wants information on,
# store his last message in a slot.
return [SlotSet("unknown_nlu_part", tracker.latest_message.get("text"))]
class ActionStoreBotLanguage(Action):
"""Takes the bot language and checks what pipelines can be used"""
def name(self):
return "action_store_bot_language"
def run(self, dispatcher, tracker, domain):
spacy_languages = [
"english",
"french",
"german",
"spanish",
"portuguese",
"french",
"italian",
"dutch",
]
language = tracker.get_slot("language")
if not language:
return [
SlotSet("language", "that language"),
SlotSet("can_use_spacy", False),
]
if language in spacy_languages:
return [SlotSet("can_use_spacy", True)]
else:
return [SlotSet("can_use_spacy", False)]
class ActionStoreEntityExtractor(Action):
"""Takes the entity which the user wants to extract and checks
what pipelines can be used.
"""
def name(self):
return "action_store_entity_extractor"
def run(self, dispatcher, tracker, domain):
spacy_entities = ["place", "date", "name", "organisation"]
duckling = [
"money",
"duration",
"distance",
"ordinals",
"time",
"amount-of-money",
"numbers",
]
entity_to_extract = next(tracker.get_latest_entity_values("entity"), None)
extractor = "ner_crf"
if entity_to_extract in spacy_entities:
extractor = "ner_spacy"
elif entity_to_extract in duckling:
extractor = "ner_duckling_http"
return [SlotSet("entity_extractor", extractor)]
class ActionSetOnboarding(Action):
"""Sets the slot 'onboarding' to true/false dependent on whether the user
has built a bot with rasa before"""
def name(self):
return "action_set_onboarding"
def run(self, dispatcher, tracker, domain):
intent = tracker.latest_message["intent"].get("name")
user_type = next(tracker.get_latest_entity_values("user_type"), None)
is_new_user = intent == "how_to_get_started" and user_type == "new"
if intent == "affirm" or is_new_user:
return [SlotSet("onboarding", True)]
elif intent == "deny":
return [SlotSet("onboarding", False)]
return []
class SuggestionForm(FormAction):
"""Accept free text input from the user for suggestions"""
def name(self):
return "suggestion_form"
@staticmethod
def required_slots(tracker):
return ["suggestion"]
def slot_mappings(self):
return {"suggestion": self.from_text()}
def submit(self, dispatcher, tracker, domain):
dispatcher.utter_template("utter_thank_suggestion", tracker)
return []
class ActionStackInstallationCommand(Action):
"""Utters the installation command for rasa depending whether
they are using `pip` or `conda`
"""
def name(self):
return "action_select_installation_command"
def run(self, dispatcher, tracker, domain):
package_manager = tracker.get_slot("package_manager")
if package_manager == "conda":
dispatcher.utter_template("utter_installation_with_conda", tracker)
else:
dispatcher.utter_template("utter_installation_with_pip", tracker)
return []
class ActionStoreProblemDescription(Action):
"""Stores the problem description in a slot."""
def name(self):
return "action_store_problem_description"
def run(self, dispatcher, tracker, domain):
problem = tracker.latest_message.get("text")
return [SlotSet("problem_description", problem)]
class ActionGreetUser(Action):
"""Greets the user with/without privacy policy"""
def name(self):
return "action_greet_user"
def run(self, dispatcher, tracker, domain):
intent = tracker.latest_message["intent"].get("name")
shown_privacy = tracker.get_slot("shown_privacy")
name_entity = next(tracker.get_latest_entity_values("name"), None)
if intent == "greet":
if shown_privacy and name_entity and name_entity.lower() != "sara":
dispatcher.utter_template("utter_greet_name", tracker, name=name_entity)
return []
elif shown_privacy:
dispatcher.utter_template("utter_greet_noname", tracker)
return []
else:
dispatcher.utter_template("utter_greet", tracker)
dispatcher.utter_template("utter_inform_privacypolicy", tracker)
dispatcher.utter_template("utter_ask_goal", tracker)
return [SlotSet("shown_privacy", True)]
elif intent[:-1] == "get_started_step" and not shown_privacy:
dispatcher.utter_template("utter_greet", tracker)
dispatcher.utter_template("utter_inform_privacypolicy", tracker)
dispatcher.utter_template("utter_" + intent, tracker)
return [SlotSet("shown_privacy", True), SlotSet("step", intent[-1])]
elif intent[:-1] == "get_started_step" and shown_privacy:
dispatcher.utter_template("utter_" + intent, tracker)
return [SlotSet("step", intent[-1])]
return []
class ActionDefaultAskAffirmation(Action):
"""Asks for an affirmation of the intent if NLU threshold is not met."""
def name(self) -> Text:
return "action_default_ask_affirmation"
def __init__(self) -> None:
import pandas as pd
self.intent_mappings = pd.read_csv("data/" "intent_description_mapping.csv")
self.intent_mappings.fillna("", inplace=True)
self.intent_mappings.entities = self.intent_mappings.entities.map(
lambda entities: {e.strip() for e in entities.split(",")}
)
def run(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List["Event"]:
intent_ranking = tracker.latest_message.get("intent_ranking", [])
if len(intent_ranking) > 1:
diff_intent_confidence = intent_ranking[0].get(
"confidence"
) - intent_ranking[1].get("confidence")
if diff_intent_confidence < 0.2:
intent_ranking = intent_ranking[:2]
else:
intent_ranking = intent_ranking[:1]
first_intent_names = [
intent.get("name", "")
for intent in intent_ranking
if intent.get("name", "") != "out_of_scope"
]
message_title = (
"Sorry, I'm not sure I've understood " "you correctly 🤔 Do you mean..."
)
entities = tracker.latest_message.get("entities", [])
entities = {e["entity"]: e["value"] for e in entities}
entities_json = json.dumps(entities)
buttons = []
for intent in first_intent_names:
logger.debug(intent)
logger.debug(entities)
buttons.append(
{
"title": self.get_button_title(intent, entities),
"payload": "/{}{}".format(intent, entities_json),
}
)
buttons.append({"title": "Something else", "payload": "/out_of_scope"})
dispatcher.utter_button_message(message_title, buttons=buttons)
return []
def get_button_title(self, intent: Text, entities: Dict[Text, Text]) -> Text:
default_utterance_query = self.intent_mappings.intent == intent
utterance_query = (
self.intent_mappings.entities == entities.keys() & default_utterance_query
)
utterances = self.intent_mappings[utterance_query].button.tolist()
if len(utterances) > 0:
button_title = utterances[0]
else:
utterances = self.intent_mappings[default_utterance_query].button.tolist()
button_title = utterances[0] if len(utterances) > 0 else intent
return button_title.format(**entities)
class ActionDefaultFallback(Action):
def name(self) -> Text:
return "action_default_fallback"
def run(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List["Event"]:
# Fallback caused by TwoStageFallbackPolicy
if (
len(tracker.events) >= 4
and tracker.events[-4].get("name") == "action_default_ask_affirmation"
):
dispatcher.utter_template("utter_restart_with_button", tracker)
return [SlotSet("feedback_value", "negative"), ConversationPaused()]
# Fallback caused by Core
else:
dispatcher.utter_template("utter_default", tracker)
return [UserUtteranceReverted()]
class CommunityEventAction(Action):
"""Utters Rasa community events."""
def __init__(self):
self.last_event_update = None
self.events = None
self.events = self._get_events()
def name(self) -> Text:
return "action_get_community_events"
def _get_events(self) -> List["CommunityEvent"]:
if self.events is None or self._are_events_expired():
from demo.community_events import get_community_events
logger.debug("Getting events from website.")
self.last_event_update = datetime.now()
self.events = get_community_events()
return self.events
def _are_events_expired(self) -> bool:
# events are expired after 1 hour
return (
self.last_event_update is None
or (datetime.now() - self.last_event_update).total_seconds() > 3600
)
def run(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List["Event"]:
intent = tracker.latest_message["intent"].get("name")
events = self._get_events()
if not events:
dispatcher.utter_template("utter_no_community_event", tracker)
elif intent == "ask_which_events":
self._utter_event_overview(dispatcher)
elif intent == "ask_when_next_event":
self._utter_next_event(tracker, dispatcher)
return []
def _utter_event_overview(self, dispatcher: CollectingDispatcher) -> None:
events = self._get_events()
event_items = ["- {} in {}".format(e.name_as_link(), e.city) for e in events]
locations = "\n".join(event_items)
dispatcher.utter_message(
"Here are the next Rasa events:\n"
"" + locations + "\nWe hope to see you at them!"
)
def _utter_next_event(
self, tracker: Tracker, dispatcher: CollectingDispatcher
) -> None:
location = next(tracker.get_latest_entity_values("location"), None)
events = self._get_events()
if location:
events_for_location = [
e for e in events if e.city == location or e.country == location
]
if not events_for_location and events:
next_event = events[0]
dispatcher.utter_template(
"utter_no_event_for_location_but_next",
tracker,
**next_event.as_kwargs(),
)
elif events_for_location:
next_event = events_for_location[0]
dispatcher.utter_template(
"utter_next_event_for_location", tracker, **next_event.as_kwargs()
)
elif events:
next_event = events[0]
dispatcher.utter_template(
"utter_next_event", tracker, **next_event.as_kwargs()
)
class ActionNextStep(Action):
def name(self):
return "action_next_step"
def run(self, dispatcher, tracker, domain):
step = int(tracker.get_slot("step")) + 1
if step in [2, 3, 4]:
dispatcher.utter_template("utter_continue_step{}".format(step), tracker)
else:
dispatcher.utter_template("utter_no_more_steps", tracker)
return []