Skip to content
This repository was archived by the owner on Jun 26, 2025. It is now read-only.

Commit f7f3375

Browse files
committed
Merge branch 'release/0.3.5'
2 parents bcfba8a + df417e8 commit f7f3375

File tree

7 files changed

+151
-155
lines changed

7 files changed

+151
-155
lines changed

README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
Create Virtual Assistants with Python
2+
=====================================
3+
4+
[![image](https://img.shields.io/pypi/v/flask-assistant.svg)](https://pypi.python.org/pypi/flask-assistant)
5+
[![image](https://travis-ci.org/treethought/flask-assistant.svg?branch=master)](https://travis-ci.org/treethought/flask-assistant) ![image](https://img.shields.io/badge/python-2.7,%203.5,%203.6,%203.7-blue.svg) [![image](https://img.shields.io/badge/discord-join%20chat-green.svg)](https://discord.gg/m6YHGyJ)
6+
7+
A flask extension serving as a framework to easily create virtual assistants using [Dialogflow](https://dialogflow.com/docs) which may be integrated
8+
with platforms such as [Actions on
9+
Google](https://developers.google.com/actions/develop/apiai/) (Google
10+
Assistant).
11+
12+
Flask-Assistant allows you to focus on building the core business logic
13+
of conversational user interfaces while utilizing Dialogflow's Natural
14+
Language Processing to interact with users.
15+
16+
**Now supports Dialogflow V2!**
17+
18+
This project is heavily inspired and based on John Wheeler's
19+
[Flask-ask](https://github.com/johnwheeler/flask-ask) for the Alexa
20+
Skills Kit.
21+
22+
Features
23+
--------
24+
25+
> - Mapping of user-triggered Intents to action functions
26+
> - Context support for crafting dialogue dependent on the user's requests
27+
> - Define prompts for missing parameters when they are not present in the users request or past active contexts
28+
> - A convenient syntax resembling Flask's decoratored routing
29+
> - Rich Responses for Google Assistant
30+
31+
Hello World
32+
-----------
33+
34+
```python
35+
from flask import Flask
36+
from flask_assistant import Assistant, ask
37+
38+
app = Flask(__name__)
39+
assist = Assistant(app, project_id='GOOGLE_CLOUD_PROJECT_ID')
40+
41+
@assist.action('Demo')
42+
def hello_world():
43+
speech = 'Microphone check 1, 2 what is this?'
44+
return ask(speech)
45+
46+
if __name__ == '__main__':
47+
app.run(debug=True)
48+
```
49+
50+
How-To
51+
------
52+
53+
> 1. Create an Assistant object with a Flask app.
54+
> 2. Use action decorators to map intents to the
55+
> proper action function.
56+
> 3. Use action view functions to return ask or tell responses.
57+
58+
Documentation
59+
-------------
60+
61+
- Check out the [Quick
62+
Start](http://flask-assistant.readthedocs.io/en/latest/quick_start.html)
63+
to jump right in
64+
- View the full
65+
[documentation](http://flask-assistant.readthedocs.io/en/latest/)

README.rst

Lines changed: 0 additions & 98 deletions
This file was deleted.

docs/source/index.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ A Minimal Assistant
4545
from flask import Flask
4646
from flask_assistant import Assistant, tell
4747
48+
# to see the full request and response objects
49+
# set logging level to DEBUG
50+
import logging
51+
logging.getLogger('flask_assistant').setLevel(logging.DEBUG)
52+
4853
app = Flask(__name__)
4954
assist = Assistant(app, project_id='GOOGLE_CLOUD_PROJECT_ID')
5055

flask_assistant/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import logging
22

33
logger = logging.getLogger("flask_assistant")
4-
logger.addHandler(logging.StreamHandler())
4+
handler = logging.StreamHandler()
5+
formatter = logging.Formatter(
6+
"%(asctime)s:%(name)s:%(levelname)s: %(message)s", "%Y-%m-%d %H:%M:%S"
7+
)
8+
handler.setFormatter(formatter)
9+
logger.addHandler(handler)
10+
11+
512
if logger.level == logging.NOTSET:
613
logger.setLevel(logging.INFO)
714

flask_assistant/core.py

Lines changed: 65 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -320,12 +320,26 @@ def _dialogflow_request(self, verify=True):
320320

321321
return _dialogflow_request_payload
322322

323-
def _dump_view_info(self, view_func=lambda: None):
324-
_infodump(
325-
"Result: Matched {} intent to {} func".format(
326-
self.intent, view_func.__name__
327-
)
328-
)
323+
def _dump_request(self,):
324+
summary = {
325+
"Intent": self.intent,
326+
"Incoming Contexts": [c.name for c in self.context_manager.active],
327+
"Source": self.request["originalDetectIntentRequest"].get("source"),
328+
"Missing Params": self._missing_params,
329+
"Received Params": self.request["queryResult"]["parameters"],
330+
}
331+
msg = "Request: " + json.dumps(summary, indent=2, sort_keys=True)
332+
logger.info(msg)
333+
334+
def _dump_result(self, view_func, result):
335+
summary = {
336+
"Intent": self.intent,
337+
"Outgoing Contexts": [c.name for c in self.context_manager.active],
338+
"Matched Action": view_func.__name__,
339+
"Response Speech": result._speech,
340+
}
341+
msg = "Result: " + json.dumps(summary, indent=2, sort_keys=True)
342+
logger.info(msg)
329343

330344
def _parse_session_id(self):
331345
return self.request["session"].split("/sessions/")[1]
@@ -336,7 +350,7 @@ def _flask_assitant_view_func(self, nlp_result=None, *args, **kwargs):
336350
else: # called as webhook
337351
self.request = self._dialogflow_request(verify=False)
338352

339-
_dbgdump(self.request)
353+
logger.debug(json.dumps(self.request, indent=2))
340354

341355
try:
342356
self.intent = self.request["queryResult"]["intent"]["displayName"]
@@ -359,15 +373,23 @@ def _flask_assitant_view_func(self, nlp_result=None, *args, **kwargs):
359373
self.access_token = original_request["user"].get("accessToken")
360374

361375
self._update_contexts()
376+
self._dump_request()
362377

363378
view_func = self._match_view_func()
364-
_dbgdump("Matched view func - {}".format(self.intent, view_func))
379+
if view_func is None:
380+
logger.error("Failed to match an action function")
381+
return "", 400
382+
383+
logger.info("Matched action function: {}".format(view_func.__name__))
365384
result = self._map_intent_to_view_func(view_func)()
366385

367386
if result is not None:
368387
if isinstance(result, _Response):
369-
return result.render_response()
388+
self._dump_result(view_func, result)
389+
resp = result.render_response()
390+
return resp
370391
return result
392+
logger.error("Action func returned empty response")
371393
return "", 400
372394

373395
def _update_contexts(self):
@@ -378,6 +400,13 @@ def _update_contexts(self):
378400
def _match_view_func(self):
379401
view_func = None
380402

403+
intent_actions = self._intent_action_funcs.get(self.intent, [])
404+
if len(intent_actions) == 0:
405+
logger.critical(
406+
"No action funcs defined for intent: {}".format(self.intent)
407+
)
408+
return view_func
409+
381410
if self.has_live_context():
382411
view_func = self._choose_context_view()
383412

@@ -386,21 +415,21 @@ def _match_view_func(self):
386415
if prompts:
387416
param_choice = self._missing_params.pop()
388417
view_func = prompts.get(param_choice)
418+
logger.debug(
419+
"Matching prompt func {} for missing param {}".format(
420+
view_func.__name__, param_choice
421+
)
422+
)
389423

390-
if not view_func and len(self._intent_action_funcs[self.intent]) == 1:
424+
if not view_func and len(intent_actions) == 1:
391425
view_func = self._intent_action_funcs[self.intent][0]
392426

393-
# TODO: Do not match func if context not satisfied
427+
# TODO: Do not match func if context not satisfied
394428

395-
if not view_func:
396-
view_func = self._intent_action_funcs[self.intent][0]
397-
msg = "No view func matched. Received intent {} with parameters {}. ".format(
398-
self.intent, self.request["queryResult"]["parameters"]
399-
)
400-
msg += "Required args {}, context_in {}, matched view func {}.".format(
401-
self._func_args(view_func), self.context_in, view_func.__name__
402-
)
403-
_errordump(msg)
429+
if not view_func and len(intent_actions) > 1:
430+
view_func = intent_actions[0]
431+
msg = "Multiple actions defined but no context was applied, will use first action func"
432+
logger.warning(msg)
404433

405434
return view_func
406435

@@ -525,25 +554,28 @@ def _context_views(self):
525554

526555
for func in self._func_contexts:
527556
if self._context_satified(func):
557+
logger.debug("{} context conditions satisified".format(func.__name__))
528558
possible_views.append(func)
529559
return possible_views
530560

531561
def _choose_context_view(self):
532562
choice = None
533563
for view in self._context_views:
534564
if view in self._intent_action_funcs[self.intent]:
565+
logger.debug(
566+
"Matched {} based on active contexts".format(view.__name__)
567+
)
535568
choice = view
536569
if choice:
537570
return choice
538571
else:
539-
msg = "No view matched for intent {} with contexts {}".format(
540-
self.intent, self.context_in
541-
)
542-
msg += "(Registered context views: {}, ".format(self._context_views)
543-
msg += "Intent action funcs: {})".format(
544-
[f.__name__ for f in self._intent_action_funcs[self.intent]]
545-
)
546-
_errordump(msg)
572+
active_contexts = [c.name for c in self.context_manager.active]
573+
intent_actions = [
574+
f.__name__ for f in self._intent_action_funcs[self.intent]
575+
]
576+
msg = "No {} action func matched based on active contexts"
577+
578+
logger.debug(msg)
547579

548580
@property
549581
def _missing_params(self): # TODO: fill missing slot from default\
@@ -609,24 +641,9 @@ def _map_params_to_view_args(self, arg_names): # TODO map to correct name
609641
def _map_arg_from_context(self, arg_name):
610642
for context_obj in self.context_in:
611643
if arg_name in context_obj["parameters"]:
644+
logger.debug(
645+
"Retrieved {} param value from {} context".format(
646+
arg_name, context_obj["name"]
647+
)
648+
)
612649
return context_obj["parameters"][arg_name]
613-
614-
615-
def _dbgdump(obj, indent=2, default=None, cls=None):
616-
msg = json.dumps(obj, indent=indent, default=default, cls=cls)
617-
logger.debug(msg)
618-
619-
620-
def _infodump(obj, indent=2, default=None, cls=None):
621-
msg = json.dumps(obj, indent=indent, default=default, cls=cls)
622-
logger.info(msg)
623-
624-
625-
def _warndump(obj, indent=2, default=None, cls=None):
626-
msg = json.dumps(obj, indent=indent, default=default, cls=cls)
627-
logger.warning(msg)
628-
629-
630-
def _errordump(obj, indent=2, default=None, cls=None):
631-
msg = json.dumps(obj, indent=indent, default=default, cls=cls)
632-
logger.error(msg)

0 commit comments

Comments
 (0)