Skip to content

Commit

Permalink
Merge branch 'release/0.3.5'
Browse files Browse the repository at this point in the history
  • Loading branch information
treethought committed Feb 11, 2019
2 parents bcfba8a + df417e8 commit f7f3375
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 155 deletions.
65 changes: 65 additions & 0 deletions README.md
@@ -0,0 +1,65 @@
Create Virtual Assistants with Python
=====================================

[![image](https://img.shields.io/pypi/v/flask-assistant.svg)](https://pypi.python.org/pypi/flask-assistant)
[![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)

A flask extension serving as a framework to easily create virtual assistants using [Dialogflow](https://dialogflow.com/docs) which may be integrated
with platforms such as [Actions on
Google](https://developers.google.com/actions/develop/apiai/) (Google
Assistant).

Flask-Assistant allows you to focus on building the core business logic
of conversational user interfaces while utilizing Dialogflow's Natural
Language Processing to interact with users.

**Now supports Dialogflow V2!**

This project is heavily inspired and based on John Wheeler's
[Flask-ask](https://github.com/johnwheeler/flask-ask) for the Alexa
Skills Kit.

Features
--------

> - Mapping of user-triggered Intents to action functions
> - Context support for crafting dialogue dependent on the user's requests
> - Define prompts for missing parameters when they are not present in the users request or past active contexts
> - A convenient syntax resembling Flask's decoratored routing
> - Rich Responses for Google Assistant
Hello World
-----------

```python
from flask import Flask
from flask_assistant import Assistant, ask

app = Flask(__name__)
assist = Assistant(app, project_id='GOOGLE_CLOUD_PROJECT_ID')

@assist.action('Demo')
def hello_world():
speech = 'Microphone check 1, 2 what is this?'
return ask(speech)

if __name__ == '__main__':
app.run(debug=True)
```

How-To
------

> 1. Create an Assistant object with a Flask app.
> 2. Use action decorators to map intents to the
> proper action function.
> 3. Use action view functions to return ask or tell responses.
Documentation
-------------

- Check out the [Quick
Start](http://flask-assistant.readthedocs.io/en/latest/quick_start.html)
to jump right in
- View the full
[documentation](http://flask-assistant.readthedocs.io/en/latest/)
98 changes: 0 additions & 98 deletions README.rst

This file was deleted.

5 changes: 5 additions & 0 deletions docs/source/index.rst
Expand Up @@ -45,6 +45,11 @@ A Minimal Assistant
from flask import Flask
from flask_assistant import Assistant, tell
# to see the full request and response objects
# set logging level to DEBUG
import logging
logging.getLogger('flask_assistant').setLevel(logging.DEBUG)
app = Flask(__name__)
assist = Assistant(app, project_id='GOOGLE_CLOUD_PROJECT_ID')
Expand Down
9 changes: 8 additions & 1 deletion flask_assistant/__init__.py
@@ -1,7 +1,14 @@
import logging

logger = logging.getLogger("flask_assistant")
logger.addHandler(logging.StreamHandler())
handler = logging.StreamHandler()
formatter = logging.Formatter(
"%(asctime)s:%(name)s:%(levelname)s: %(message)s", "%Y-%m-%d %H:%M:%S"
)
handler.setFormatter(formatter)
logger.addHandler(handler)


if logger.level == logging.NOTSET:
logger.setLevel(logging.INFO)

Expand Down
113 changes: 65 additions & 48 deletions flask_assistant/core.py
Expand Up @@ -320,12 +320,26 @@ def _dialogflow_request(self, verify=True):

return _dialogflow_request_payload

def _dump_view_info(self, view_func=lambda: None):
_infodump(
"Result: Matched {} intent to {} func".format(
self.intent, view_func.__name__
)
)
def _dump_request(self,):
summary = {
"Intent": self.intent,
"Incoming Contexts": [c.name for c in self.context_manager.active],
"Source": self.request["originalDetectIntentRequest"].get("source"),
"Missing Params": self._missing_params,
"Received Params": self.request["queryResult"]["parameters"],
}
msg = "Request: " + json.dumps(summary, indent=2, sort_keys=True)
logger.info(msg)

def _dump_result(self, view_func, result):
summary = {
"Intent": self.intent,
"Outgoing Contexts": [c.name for c in self.context_manager.active],
"Matched Action": view_func.__name__,
"Response Speech": result._speech,
}
msg = "Result: " + json.dumps(summary, indent=2, sort_keys=True)
logger.info(msg)

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

_dbgdump(self.request)
logger.debug(json.dumps(self.request, indent=2))

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

self._update_contexts()
self._dump_request()

view_func = self._match_view_func()
_dbgdump("Matched view func - {}".format(self.intent, view_func))
if view_func is None:
logger.error("Failed to match an action function")
return "", 400

logger.info("Matched action function: {}".format(view_func.__name__))
result = self._map_intent_to_view_func(view_func)()

if result is not None:
if isinstance(result, _Response):
return result.render_response()
self._dump_result(view_func, result)
resp = result.render_response()
return resp
return result
logger.error("Action func returned empty response")
return "", 400

def _update_contexts(self):
Expand All @@ -378,6 +400,13 @@ def _update_contexts(self):
def _match_view_func(self):
view_func = None

intent_actions = self._intent_action_funcs.get(self.intent, [])
if len(intent_actions) == 0:
logger.critical(
"No action funcs defined for intent: {}".format(self.intent)
)
return view_func

if self.has_live_context():
view_func = self._choose_context_view()

Expand All @@ -386,21 +415,21 @@ def _match_view_func(self):
if prompts:
param_choice = self._missing_params.pop()
view_func = prompts.get(param_choice)
logger.debug(
"Matching prompt func {} for missing param {}".format(
view_func.__name__, param_choice
)
)

if not view_func and len(self._intent_action_funcs[self.intent]) == 1:
if not view_func and len(intent_actions) == 1:
view_func = self._intent_action_funcs[self.intent][0]

# TODO: Do not match func if context not satisfied
# TODO: Do not match func if context not satisfied

if not view_func:
view_func = self._intent_action_funcs[self.intent][0]
msg = "No view func matched. Received intent {} with parameters {}. ".format(
self.intent, self.request["queryResult"]["parameters"]
)
msg += "Required args {}, context_in {}, matched view func {}.".format(
self._func_args(view_func), self.context_in, view_func.__name__
)
_errordump(msg)
if not view_func and len(intent_actions) > 1:
view_func = intent_actions[0]
msg = "Multiple actions defined but no context was applied, will use first action func"
logger.warning(msg)

return view_func

Expand Down Expand Up @@ -525,25 +554,28 @@ def _context_views(self):

for func in self._func_contexts:
if self._context_satified(func):
logger.debug("{} context conditions satisified".format(func.__name__))
possible_views.append(func)
return possible_views

def _choose_context_view(self):
choice = None
for view in self._context_views:
if view in self._intent_action_funcs[self.intent]:
logger.debug(
"Matched {} based on active contexts".format(view.__name__)
)
choice = view
if choice:
return choice
else:
msg = "No view matched for intent {} with contexts {}".format(
self.intent, self.context_in
)
msg += "(Registered context views: {}, ".format(self._context_views)
msg += "Intent action funcs: {})".format(
[f.__name__ for f in self._intent_action_funcs[self.intent]]
)
_errordump(msg)
active_contexts = [c.name for c in self.context_manager.active]
intent_actions = [
f.__name__ for f in self._intent_action_funcs[self.intent]
]
msg = "No {} action func matched based on active contexts"

logger.debug(msg)

@property
def _missing_params(self): # TODO: fill missing slot from default\
Expand Down Expand Up @@ -609,24 +641,9 @@ def _map_params_to_view_args(self, arg_names): # TODO map to correct name
def _map_arg_from_context(self, arg_name):
for context_obj in self.context_in:
if arg_name in context_obj["parameters"]:
logger.debug(
"Retrieved {} param value from {} context".format(
arg_name, context_obj["name"]
)
)
return context_obj["parameters"][arg_name]


def _dbgdump(obj, indent=2, default=None, cls=None):
msg = json.dumps(obj, indent=indent, default=default, cls=cls)
logger.debug(msg)


def _infodump(obj, indent=2, default=None, cls=None):
msg = json.dumps(obj, indent=indent, default=default, cls=cls)
logger.info(msg)


def _warndump(obj, indent=2, default=None, cls=None):
msg = json.dumps(obj, indent=indent, default=default, cls=cls)
logger.warning(msg)


def _errordump(obj, indent=2, default=None, cls=None):
msg = json.dumps(obj, indent=indent, default=default, cls=cls)
logger.error(msg)

0 comments on commit f7f3375

Please sign in to comment.