-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: decompose connector into multiple platform-specific adapters
- Loading branch information
Showing
14 changed files
with
554 additions
and
349 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from .base import BaseAdapter | ||
from .alice import AliceAdapter | ||
from .facebook import FacebookAdapter | ||
from .text import TextAdapter | ||
from .tg import TelegramAdapter | ||
from .vk import VkAdapter |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
from typing import Dict, Optional | ||
|
||
from ..adapters.base import BaseAdapter, Context, Response, logger | ||
from ..dialog.names import SOURCES, REQUEST_TYPES, COMMANDS | ||
from ..interfaces.yandex import YandexRequest, YandexResponse | ||
from ..utils.text import encode_uri | ||
from ..utils.content_manager import YandexImageAPI | ||
|
||
|
||
class AliceAdapter(BaseAdapter): | ||
SOURCE = SOURCES.ALICE | ||
|
||
def __init__(self, native_state=False, image_manager: Optional[YandexImageAPI] = None, **kwargs): | ||
super(AliceAdapter, self).__init__(**kwargs) | ||
self.native_state = native_state | ||
self.image_manager: Optional[YandexImageAPI] = image_manager | ||
|
||
def make_context(self, message: Dict, **kwargs) -> Context: | ||
metadata = {} | ||
|
||
if set(message.keys()) == {'body'}: | ||
message = message['body'] | ||
try: | ||
sess = message['session'] | ||
except KeyError: | ||
raise KeyError(f'The key "session" not found in message among keys {list(message.keys())}.') | ||
if sess.get('user', {}).get('user_id'): | ||
# the new user_id, which is persistent across applications | ||
user_id = self.SOURCE + '_auth__' + sess['user']['user_id'] | ||
else: | ||
# the old user id, that changes across applications | ||
user_id = self.SOURCE + '__' + sess['user_id'] | ||
try: | ||
message_text = message['request'].get('command', '') | ||
except KeyError: | ||
raise KeyError(f'The key "request" not found in message among keys {list(message.keys())}.') | ||
metadata['new_session'] = message.get('session', {}).get('new', False) | ||
|
||
ctx = Context( | ||
user_object=None, | ||
message_text=message_text, | ||
metadata=metadata, | ||
user_id=user_id, | ||
source=self.SOURCE, | ||
raw_message=message, | ||
) | ||
|
||
ctx.request_type = message['request'].get('type', REQUEST_TYPES.SIMPLE_UTTERANCE) | ||
ctx.payload = message['request'].get('payload', {}) | ||
try: | ||
ctx.yandex = YandexRequest.from_dict(message) | ||
except Exception as e: | ||
logger.warning('Could not deserialize Yandex request: got exception "{}".'.format(e)) | ||
|
||
return ctx | ||
|
||
def make_response(self, response: Response, original_message=None, **kwargs): | ||
|
||
directives = {} | ||
if response.commands: | ||
for command in response.commands: | ||
if command == COMMANDS.REQUEST_GEOLOCATION: | ||
directives[COMMANDS.REQUEST_GEOLOCATION] = {} | ||
|
||
result = { | ||
"version": original_message['version'], | ||
"response": { | ||
"end_session": response.has_exit_command, | ||
"text": response.text | ||
} | ||
} | ||
if self.native_state and response.updated_user_object: | ||
if self.native_state == 'session': | ||
result['session_state'] = response.updated_user_object | ||
elif self.native_state == 'application': | ||
result['application_state'] = response.updated_user_object | ||
elif self.native_state == 'user': | ||
if original_message.get('session') and 'user' not in original_message['session']: | ||
result['application_state'] = response.updated_user_object | ||
result['user_state_update'] = response.updated_user_object | ||
else: | ||
if 'session' in response.updated_user_object: | ||
result['session_state'] = response.updated_user_object['session'] | ||
if 'application' in response.updated_user_object: | ||
result['application_state'] = response.updated_user_object['application'] | ||
if 'user' in response.updated_user_object: | ||
result['user_state_update'] = response.updated_user_object['user'] | ||
if response.raw_response is not None: | ||
if isinstance(response.raw_response, YandexResponse): | ||
result = response.raw_response.to_dict() | ||
else: | ||
result['response'] = response.raw_response | ||
return result | ||
if response.voice is not None and response.voice != response.text: | ||
result['response']['tts'] = response.voice.replace('\n', ' ') | ||
buttons = response.links or [] | ||
for button in buttons: | ||
# avoid cyrillic characters in urls - they are not supported by Alice | ||
if 'url' in button: | ||
button['url'] = encode_uri(button['url']) | ||
if response.suggests: | ||
buttons = buttons + [{'title': suggest} for suggest in response.suggests] | ||
for button in buttons: | ||
if not isinstance(button.get('hide'), bool): | ||
button['hide'] = True | ||
result['response']['buttons'] = buttons | ||
if response.image_id: | ||
result['response']['card'] = { | ||
'type': 'BigImage', | ||
'image_id': response.image_id, | ||
'description': response.text | ||
} | ||
elif response.image_url and self.image_manager: | ||
image_id = self.image_manager.get_image_id_by_url(response.image_url) | ||
if image_id: | ||
result['response']['card'] = { | ||
'type': 'BigImage', | ||
'image_id': image_id, | ||
'description': response.text | ||
} | ||
if response.gallery is not None: | ||
result['response']['card'] = response.gallery.to_dict() | ||
if response.image is not None: | ||
result['response']['card'] = response.image.to_dict() | ||
if response.show_item_meta is not None: | ||
result['response']['show_item_meta'] = response.show_item_meta | ||
if directives: | ||
result['response']['directives'] = directives | ||
return result | ||
|
||
def uses_native_state(self, context: Context) -> bool: | ||
""" Whether dialog state can be extracted directly from a context""" | ||
return bool(self.native_state) | ||
|
||
def get_native_state(self, context: Context) -> Optional[Dict]: | ||
""" Return native dialog state if it is possible""" | ||
if not self.native_state: | ||
return | ||
message = context.raw_message | ||
state = message.get('state', {}) | ||
|
||
if self.native_state == 'session': | ||
user_object = state.get('session') | ||
elif self.native_state == 'user': | ||
user_object = state.get('user') | ||
# for unauthorized users, use application state instead | ||
if message.get('session') and 'user' not in message['session']: | ||
user_object = state.get('application') | ||
elif self.native_state == 'application': | ||
user_object = state.get('application') | ||
else: | ||
user_object = state | ||
return user_object |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import copy | ||
import logging | ||
from typing import Optional, Dict | ||
|
||
from ..dialog.serialized_message import SerializedMessage | ||
from ..dialog import Context, Response | ||
|
||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class BaseAdapter: | ||
""" A base class for adapters that encode and decode messages into a single format """ | ||
|
||
def make_context(self, message, **kwargs) -> Context: | ||
""" Get a raw `message` in a platform-specific format and encode it into a unified `Context` """ | ||
raise NotImplementedError() | ||
|
||
def make_response(self, response: Response, original_message=None, **kwargs): | ||
""" Get a unified `Response` object and decode it into the platform-specific response """ | ||
raise NotImplementedError() | ||
|
||
def serialize_context(self, context: Context, data=None, **kwargs) -> Optional[SerializedMessage]: | ||
if data is None: | ||
data = context.raw_message | ||
if context.request_id is not None: | ||
kwargs['request_id'] = context.request_id | ||
return SerializedMessage( | ||
text=context.message_text, | ||
user_id=context.user_id, | ||
from_user=True, | ||
data=data, | ||
source=context.source, | ||
**kwargs | ||
) | ||
|
||
def serialize_response(self, data, context: Context, response: Response, **kwargs) -> Optional[SerializedMessage]: | ||
data = copy.deepcopy(data) | ||
if context.request_id is not None: | ||
kwargs['request_id'] = context.request_id | ||
if response.label: | ||
kwargs['label'] = response.label | ||
return SerializedMessage( | ||
text=response.text, | ||
user_id=context.user_id, | ||
from_user=False, | ||
data=data, | ||
source=context.source, | ||
**kwargs | ||
) | ||
|
||
def uses_native_state(self, context: Context) -> bool: | ||
""" Whether dialog state can be extracted directly from a context""" | ||
return False | ||
|
||
def get_native_state(self, context: Context) -> Optional[Dict]: | ||
""" Return native dialog state if it is possible""" | ||
return |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
from typing import Dict | ||
|
||
from tgalice.dialog.names import SOURCES, REQUEST_TYPES | ||
from tgalice.adapters.base import BaseAdapter, Context, Response, logger | ||
|
||
|
||
class FacebookAdapter(BaseAdapter): | ||
SOURCE = SOURCES.FACEBOOK | ||
|
||
def make_context(self, message: Dict, **kwargs) -> Context: | ||
ctx = Context( | ||
user_object=None, | ||
message_text=message.get('message', {}).get('text', ''), | ||
metadata={}, | ||
user_id=self.SOURCE + '__' + message['sender']['id'], | ||
source=self.SOURCE, | ||
raw_message=message, | ||
) | ||
|
||
if not message.get('message', {}).get('text', ''): | ||
payload = message.get('postback', {}).get('payload') | ||
if payload is not None: | ||
ctx.payload = payload | ||
ctx.request_type = REQUEST_TYPES.BUTTON_PRESSED # todo: check if it is really the case | ||
# todo: do something in case of attachments (message['message'].get('attachments')) | ||
# if user sends us a GIF, photo,video, or any other non-text item | ||
|
||
return ctx | ||
|
||
def make_response(self, response: Response, original_message=None, **kwargs): | ||
if response.raw_response is not None: | ||
return response.raw_response | ||
result = {'text': response.text} | ||
if response.suggests or response.links: | ||
links = [{'type': 'web_url', 'title': link['title'], 'url': link['url']} for link in response.links] | ||
suggests = [{'type': 'postback', 'title': s, 'payload': s} for s in response.suggests] | ||
result = { | ||
"attachment": { | ||
"type": "template", | ||
"payload": { | ||
"template_type": "button", | ||
"text": response.text, | ||
"buttons": links + suggests | ||
} | ||
} | ||
} | ||
return result # for bot.send_message(recipient_id, result) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
from typing import Dict | ||
|
||
from tgalice.dialog.names import SOURCES, REQUEST_TYPES | ||
from tgalice.adapters.base import BaseAdapter, Context, Response, logger | ||
|
||
|
||
class TextAdapter(BaseAdapter): | ||
SOURCE = SOURCES.TEXT | ||
|
||
def make_context(self, message: Dict, **kwargs) -> Context: | ||
ctx = Context( | ||
user_object=None, | ||
message_text=message, | ||
metadata={}, | ||
user_id='the_text_user', | ||
source=self.SOURCE, | ||
raw_message=message, | ||
) | ||
return ctx | ||
|
||
def make_response(self, response: Response, original_message=None, **kwargs): | ||
result = response.text | ||
if response.voice is not None and response.voice != response.text: | ||
result = result + '\n[voice: {}]'.format(response.voice) | ||
if response.image_id: | ||
result = result + '\n[image: {}]'.format(response.image_id) | ||
if response.image_url: | ||
result = result + '\n[image: {}]'.format(response.image_url) | ||
if response.sound_url: | ||
result = result + '\n[sound: {}]'.format(response.sound_url) | ||
if len(response.links) > 0: | ||
result = result + '\n' + ', '.join( | ||
['[{}: {}]'.format(link['title'], link['url']) for link in response.links] | ||
) | ||
if len(response.suggests) > 0: | ||
result = result + '\n' + ', '.join(['[{}]'.format(s) for s in response.suggests]) | ||
if len(response.commands) > 0: | ||
result = result + '\n' + ', '.join(['{{{}}}'.format(c) for c in response.commands]) | ||
return [result, response.has_exit_command] |
Oops, something went wrong.