Skip to content

Feature/multiturn support #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 52 additions & 17 deletions flask_clova/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ def session_ended():

return f

def intent(self, intent_name, mapping=None, convert=None, default=None):
def intent(self, intent_name, mapping=None, convert=None, default=None, follow_up=None):
"""Decorator routes an CEK IntentRequest and provides the slot parameters to the wrapped function.

Functions decorated as an intent are registered as the view function for the Intent's URL,
Expand All @@ -229,19 +229,29 @@ def weather(city):
default {dict} -- Provides default values for Intent slots if CEK reuqest
returns no corresponding slot, or a slot with an empty value
default: {}

follow_up {list} -- List parent nodes to follow up
"""
if mapping is None:
mapping = dict()
if convert is None:
convert = dict()
if default is None:
default = dict()
if follow_up is None:
follow_up = ["__private_default"]

def decorator(f):
self._intent_view_funcs[intent_name] = f
self._intent_mappings[intent_name] = mapping
self._intent_converts[intent_name] = convert
self._intent_defaults[intent_name] = default
if intent_name not in self._intent_view_funcs:
self._intent_view_funcs[intent_name] = dict()
self._intent_mappings[intent_name] = dict()
self._intent_converts[intent_name] = dict()
self._intent_defaults[intent_name] = dict()
for fintent in follow_up:
self._intent_view_funcs[intent_name][fintent] = f
self._intent_mappings[intent_name][fintent] = mapping
self._intent_converts[intent_name][fintent] = convert
self._intent_defaults[intent_name][fintent] = default

return f
return decorator
Expand Down Expand Up @@ -350,6 +360,7 @@ def _flask_view_func(self, *args, **kwargs):
result = self._map_intent_to_view_func(self.request.intent)()

if result is not None:
self._private_contexts_handler(request.intent)
if isinstance(result, models._Response):
result = result.render_response()
response = make_response(result)
Expand All @@ -358,30 +369,48 @@ def _flask_view_func(self, *args, **kwargs):
logger.warning(request_type + " handler is not defined.")
return "", 400

def _private_contexts_handler(self, context):
if session.sessionAttributes is None or '__private_contexts' not in session.sessionAttributes:
session.sessionAttributes['__private_contexts'] = list()
session.sessionAttributes['__private_contexts'].append(context)

def _map_intent_to_view_func(self, intent):
"""Provides appropiate parameters to the intent functions."""
arg_values = []

if intent.name in self._intent_view_funcs:
view_func = self._intent_view_funcs[intent.name]
contexts = self.session.sessionAttributes.get('__private_contexts', {})
follow_up_context = {'name': '__private_default'}
current_intents = self._intent_view_funcs[intent.name]
# consider parent intent
for context in contexts:
parent_intent = context.get('name')
if parent_intent in current_intents:
follow_up_context = context
break
view_func = current_intents[follow_up_context.get('name')]

argspec = inspect.getfullargspec(view_func)
arg_names = argspec.args
arg_values = self._map_params_to_view_args(intent.name, arg_names, follow_up_context)

elif self._default_intent_view_func is not None:
view_func = self._default_intent_view_func
else:
raise NotImplementedError('Intent "{}" not found and no default intent specified.'.format(intent.name))

argspec = inspect.getfullargspec(view_func)
arg_names = argspec.args
arg_values = self._map_params_to_view_args(intent.name, arg_names)

return partial(view_func, *arg_values)

def _map_params_to_view_args(self, view_name, arg_names):
def _map_params_to_view_args(self, view_name, arg_names, follow_up_context):
"""
find and invoke appropriate function
"""

arg_values = []
convert = self._intent_converts.get(view_name)
default = self._intent_defaults.get(view_name)
mapping = self._intent_mappings.get(view_name)
fname = follow_up_context.get('name')
convert = self._intent_converts[view_name][fname]
default = self._intent_defaults[view_name][fname]
mapping = self._intent_mappings[view_name][fname]

convert_errors = {}

Expand All @@ -393,9 +422,15 @@ def _map_params_to_view_args(self, view_name, arg_names):
slot_object = getattr(intent.slots, slot_key)
request_data[slot_object.name] = getattr(slot_object, 'value', None)

else:
for param_name in self.request:
request_data[param_name] = getattr(self.request, param_name, None)
# else:
# for param_name in self.request:
# request_data[param_name] = getattr(self.request, param_name, None)

follow_up_slots = follow_up_context.get('slots')
if follow_up_slots is not None:
for slot_key in follow_up_slots.keys():
slot_object = follow_up_slots.get(slot_key)
request_data[slot_object.get('name')] = slot_object.get('value')

for arg_name in arg_names:
param_or_slot = mapping.get(arg_name, arg_name)
Expand Down
80 changes: 78 additions & 2 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import mock

from flask import Flask
from flask_clova import Clova, session, request
from flask_clova import Clova, session, request, question

@unittest.skipIf(six.PY2, "Not yet supported on Python 2.x")
class SmokeTestUsingSamples(unittest.TestCase):
Expand Down Expand Up @@ -167,5 +167,81 @@ def session_func():

self.assertEqual(counter.call_count, 1)

def test_follow_up_intent(self):
@self.clova.intent(
'parent_intent'
)
def parent_intent(p_value):
return question("go to follow up intent")

child_mock = mock.MagicMock()
@self.clova.intent(
'child_intent',
follow_up=['parent_intent']
)
def child_intent(p_value, c_value):
child_mock()
self.assertEqual(p_value, "from parent")
self.assertEqual(c_value, "from child")
return "ok"

orphan_mock = mock.MagicMock()
@self.clova.intent(
'child_intent',
follow_up=['other_parent']
)
def orphan_intent():
orphan_mock()
return "ok"

req_p = {
"version": "0.1.0",
"session": {},
"context": {},
"request": {
"type": "IntentRequest",
"intent": {
"name": "parent_intent",
"slots": {
'p_value': {
'name': 'p_value',
'value': 'from parent'
}
}
}
}
}

req_c = {
"version": "0.1.0",
"session": {},
"context": {},
"request": {
"type": "IntentRequest",
"intent": {
"name": "child_intent",
"slots": {
'c_value': {
'name': 'c_value',
'value': 'from child'
}
}
}
}
}

with self.app.test_client() as client:
rv = client.post('/', json=req_p)
self.assertEqual('200 OK', rv.status)

req_c['session']['sessionAttributes'] = rv.json.get('sessionAttributes')
rv = client.post('/', json=req_c)
self.assertEqual('200 OK', rv.status)

self.assertEqual(child_mock.call_count, 1)
self.assertEqual(orphan_mock.call_count, 0)



if __name__ == "__main__":
unittest.run()
unittest.main()