diff --git a/tools/run-mypy b/tools/run-mypy index 5b33910..fceb4fc 100755 --- a/tools/run-mypy +++ b/tools/run-mypy @@ -69,6 +69,8 @@ force_include = [ "zulip_bots/zulip_bots/bots/wikipedia/test_wikipedia.py", "zulip_bots/zulip_bots/bots/yoda/yoda.py", "zulip_bots/zulip_bots/bots/yoda/test_yoda.py", + "zulip_bots/zulip_bots/bots/dialogflow/dialogflow.py", + "zulip_bots/zulip_bots/bots/dialogflow/test_dialogflow.py" ] parser = argparse.ArgumentParser(description="Run mypy on files tracked by git.") diff --git a/zulip_bots/setup.py b/zulip_bots/setup.py index f372aa8..03eb9a1 100755 --- a/zulip_bots/setup.py +++ b/zulip_bots/setup.py @@ -53,7 +53,8 @@ setuptools_info = dict( 'BeautifulSoup4', # for bots/googlesearch 'lxml', # for bots/googlesearch 'requests', # for bots/link_shortener - 'python-chess[engine,gaviota]' # for bots/chess + 'python-chess[engine,gaviota]', # for bots/chess + 'apiai' # for bots/dialogflow ], ) diff --git a/zulip_bots/zulip_bots/bots/dialogflow/__init__.py b/zulip_bots/zulip_bots/bots/dialogflow/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zulip_bots/zulip_bots/bots/dialogflow/dialogflow.conf b/zulip_bots/zulip_bots/bots/dialogflow/dialogflow.conf new file mode 100644 index 0000000..5de662c --- /dev/null +++ b/zulip_bots/zulip_bots/bots/dialogflow/dialogflow.conf @@ -0,0 +1,3 @@ +[dialogflow] +key=YOUR_PUBLIC_API_KEY_HERE +bot_info=BOT_INFO_HERE diff --git a/zulip_bots/zulip_bots/bots/dialogflow/dialogflow.py b/zulip_bots/zulip_bots/bots/dialogflow/dialogflow.py new file mode 100644 index 0000000..c3838b7 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/dialogflow/dialogflow.py @@ -0,0 +1,57 @@ +# See readme.md for instructions on running this code. +from __future__ import print_function +import logging +from six.moves.urllib import parse +import json + +import apiai + +from typing import Dict, Any, List + +help_message = '''DialogFlow bot +This bot will interact with dialogflow bots. +Simply send this bot a message, and it will respond depending on the configured bot's behaviour. +''' + +def get_bot_result(message_content: str, config: Dict[str, str], sender_id: str) -> str: + if message_content.strip() == '' or message_content.strip() == 'help': + return config['bot_info'] + ai = apiai.ApiAI(config['key']) + try: + request = ai.text_request() + request.session_id = sender_id + request.query = message_content + response = request.getresponse() + res_str = response.read().decode('utf8', 'ignore') + res_json = json.loads(res_str) + if res_json['status']['errorType'] != 'success': + return 'Error {}: {}.'.format(res_json['status']['code'], res_json['status']['errorDetails']) + if res_json['result']['fulfillment']['speech'] == '': + if res_json['alternateResult']['fulfillment']['speech'] == '': + return 'Error. No result.' + return res_json['alternateResult']['fulfillment']['speech'] + return res_json['result']['fulfillment']['speech'] + except Exception as e: + logging.exception(str(e)) + return 'Error. {}.'.format(str(e)) + +class DialogFlowHandler(object): + ''' + This plugin allows users to easily add their own + DialogFlow bots to zulip + ''' + + def initialize(self, bot_handler: Any) -> None: + self.config_info = bot_handler.get_config_info('dialogflow') + + def usage(self) -> str: + return ''' + This plugin will allow users to easily add their own + DialogFlow bots to zulip + ''' + + def handle_message(self, message: Dict[str, str], bot_handler: Any) -> None: + result = get_bot_result(message['content'], self.config_info, message['sender_id']) + bot_handler.send_reply(message, result) + +handler_class = DialogFlowHandler diff --git a/zulip_bots/zulip_bots/bots/dialogflow/doc.md b/zulip_bots/zulip_bots/bots/dialogflow/doc.md new file mode 100644 index 0000000..d4a0c28 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/dialogflow/doc.md @@ -0,0 +1,24 @@ +# DialogFlow bot +This bot allows users to easily add their own DialogFlow bots to zulip. + +## Setup +To add your DialogFlow bot: +Add the V1 Client access token from your agent's settings in the DialogFlow console to +`dialogflow.conf`, and write a short sentence describing what your bot does in the same file +as `bot_info`. + +## Usage + +Run this bot as described +[here](https://zulipchat.com/api/running-bots#running-a-bot). + +Mention the bot in order to say things to it. + +For example: `@weather What is the weather today?` + + +## Limitations +When creating your DialogFlow bot, please consider these things: + +- Empty input will not be sent to the bot. +- Only text can be sent to, and recieved from the bot. diff --git a/zulip_bots/zulip_bots/bots/dialogflow/fixtures/test_403.json b/zulip_bots/zulip_bots/bots/dialogflow/fixtures/test_403.json new file mode 100644 index 0000000..c34201e --- /dev/null +++ b/zulip_bots/zulip_bots/bots/dialogflow/fixtures/test_403.json @@ -0,0 +1,12 @@ +{ + "response": { + "status": { + "errorType": "fail", + "code": "403", + "errorDetails": "Access Denied" + } + }, + "request": { + "query": "hello" + } + } diff --git a/zulip_bots/zulip_bots/bots/dialogflow/fixtures/test_alternate_result.json b/zulip_bots/zulip_bots/bots/dialogflow/fixtures/test_alternate_result.json new file mode 100644 index 0000000..eb867e5 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/dialogflow/fixtures/test_alternate_result.json @@ -0,0 +1,20 @@ +{ + "response": { + "result": { + "fulfillment": { + "speech": "" + } + }, + "alternateResult": { + "fulfillment": { + "speech": "alternate result" + } + }, + "status": { + "errorType": "success" + } + }, + "request": { + "query": "hello" + } + } diff --git a/zulip_bots/zulip_bots/bots/dialogflow/fixtures/test_empty_response.json b/zulip_bots/zulip_bots/bots/dialogflow/fixtures/test_empty_response.json new file mode 100644 index 0000000..8982a1b --- /dev/null +++ b/zulip_bots/zulip_bots/bots/dialogflow/fixtures/test_empty_response.json @@ -0,0 +1,20 @@ +{ + "response": { + "result": { + "fulfillment": { + "speech": "" + } + }, + "alternateResult": { + "fulfillment": { + "speech": "" + } + }, + "status": { + "errorType": "success" + } + }, + "request": { + "query": "hello" + } + } diff --git a/zulip_bots/zulip_bots/bots/dialogflow/fixtures/test_exception.json b/zulip_bots/zulip_bots/bots/dialogflow/fixtures/test_exception.json new file mode 100644 index 0000000..ca74e82 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/dialogflow/fixtures/test_exception.json @@ -0,0 +1,15 @@ +{ + "response": { + "foo": { + "fulfillment": { + "speech": "how are you?" + } + }, + "bar": { + "errorType": "success" + } + }, + "request": { + "query": "hello" + } + } diff --git a/zulip_bots/zulip_bots/bots/dialogflow/fixtures/test_normal.json b/zulip_bots/zulip_bots/bots/dialogflow/fixtures/test_normal.json new file mode 100644 index 0000000..ae07633 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/dialogflow/fixtures/test_normal.json @@ -0,0 +1,15 @@ +{ + "response": { + "result": { + "fulfillment": { + "speech": "how are you?" + } + }, + "status": { + "errorType": "success" + } + }, + "request": { + "query": "hello" + } +} diff --git a/zulip_bots/zulip_bots/bots/dialogflow/test_dialogflow.py b/zulip_bots/zulip_bots/bots/dialogflow/test_dialogflow.py new file mode 100644 index 0000000..f7ddc4b --- /dev/null +++ b/zulip_bots/zulip_bots/bots/dialogflow/test_dialogflow.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python + +from zulip_bots.test_lib import StubBotTestCase, read_bot_fixture_data + +from contextlib import contextmanager + +from unittest.mock import patch + +from typing import Any, ByteString + +import json + +class MockTextRequest(): + def __init__(self) -> None: + self.session_id = "" + self.query = "" + self.response = "" + + def getresponse(self) -> Any: + return MockHttplibRequest(self.response) + +class MockHttplibRequest(): + def __init__(self, response: str) -> None: + self.response = response + + def read(self) -> ByteString: + return json.dumps(self.response).encode() + +@contextmanager +def mock_dialogflow(test_name: str, bot_name: str) -> Any: + response_data = read_bot_fixture_data(bot_name, test_name) + df_request = response_data.get('request') + df_response = response_data.get('response') + + with patch('apiai.ApiAI.text_request') as mock_text_request: + request = MockTextRequest() + request.response = df_response + mock_text_request.return_value = request + yield + +class TestDialogFlowBot(StubBotTestCase): + bot_name = 'dialogflow' + + def _test(self, test_name: str, message: str, response: str) -> None: + with self.mock_config_info({'key': 'abcdefg', 'bot_info': 'bot info foo bar'}), \ + mock_dialogflow(test_name, 'dialogflow'): + self.verify_reply(message, response) + + def test_normal(self) -> None: + self._test('test_normal', 'hello', 'how are you?') + + def test_403(self) -> None: + self._test('test_403', 'hello', 'Error 403: Access Denied.') + + def test_empty_response(self) -> None: + self._test('test_empty_response', 'hello', 'Error. No result.') + + def test_exception(self) -> None: + with patch('logging.exception'): + self._test('test_exception', 'hello', 'Error. \'status\'.') + + def test_help(self) -> None: + self._test('test_normal', 'help', 'bot info foo bar') + self._test('test_normal', '', 'bot info foo bar') + + def test_alternate_response(self) -> None: + self._test('test_alternate_result', 'hello', 'alternate result') + + def test_bot_responds_to_empty_message(self) -> None: + with self.mock_config_info({'key': 'abcdefg', 'bot_info': 'bot info foo bar'}): + pass diff --git a/zulip_bots/zulip_bots/test_lib.py b/zulip_bots/zulip_bots/test_lib.py index f537a6d..5625b94 100755 --- a/zulip_bots/zulip_bots/test_lib.py +++ b/zulip_bots/zulip_bots/test_lib.py @@ -114,6 +114,7 @@ class StubBotTestCase(TestCase): display_recipient='foo_stream', sender_email='foo@example.com', sender_full_name='Foo Test User', + sender_id='123', content=content, ) return message