diff --git a/tools/run-mypy b/tools/run-mypy index dbbfa70..e2b4a70 100755 --- a/tools/run-mypy +++ b/tools/run-mypy @@ -67,6 +67,8 @@ force_include = [ "zulip_bots/zulip_bots/bots/chess/test_chess.py", "zulip_bots/zulip_bots/bots/xkcd/xkcd.py", "zulip_bots/zulip_bots/bots/xkcd/test_xkcd.py", + "zulip_bots/zulip_bots/bots/witai/witai.py", + "zulip_bots/zulip_bots/bots/witai/test_witai.py", "zulip_bots/zulip_bots/bots/wikipedia/wikipedia.py", "zulip_bots/zulip_bots/bots/wikipedia/test_wikipedia.py", "zulip_bots/zulip_bots/bots/yoda/yoda.py", diff --git a/zulip_bots/setup.py b/zulip_bots/setup.py index fc07041..c3e02dc 100755 --- a/zulip_bots/setup.py +++ b/zulip_bots/setup.py @@ -54,6 +54,7 @@ setuptools_info = dict( 'lxml', # for bots/googlesearch 'requests', # for bots/link_shortener 'python-chess[engine,gaviota]', # for bots/chess + 'wit', # for bots/witai 'apiai' # for bots/dialogflow ], ) diff --git a/zulip_bots/zulip_bots/bots/witai/doc.md b/zulip_bots/zulip_bots/bots/witai/doc.md new file mode 100644 index 0000000..97a9141 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/witai/doc.md @@ -0,0 +1,52 @@ +# Wit.ai Bot + +Wit.ai Bot uses [wit.ai](https://wit.ai/) to parse natural language. + +## Usage + + 1. Go to https://wit.ai/ and sign up. + + 2. Create your Wit.ai app, or follow + [this quickstart guide](https://wit.ai/docs/quickstart). + + 3. Create a `.conf` file containing a `token` field for your Wit.ai token, + and a `help_message` field for a message to display to confused users, + e.g., + + ``` + [witai] + token = QWERTYUIOP1234 + help_message = Ask me about my favorite food! + ``` + + 4. Create a new file named `witai_handler.py`, and inside of it, create a + function called `handle` with one parameter `response`. Inside of `handle`, + write code for whatever you want to do with the Wit.ai response. It should + return a `string` to respond to the user with. For example, + + ```python + def handle(response): + if response['entities']['intent'][0]['value'] == 'favorite_food': + return 'pizza' + if response['entities']['intent'][0]['value'] == 'favorite_drink': + return 'coffee' + ``` + + 5. Add `witai_handler.py`'s location as `handler_location` in your + configuration file, e.g., + + ``` + [witai] + token = QWERTYUIOP1234 + handler_location = /Users/you/witai_handler_directory/witai_handler.py + ``` + + 6. Call + + ```bash + zulip-run-bot witai \ + --config-file \ + --bot-config-file + ``` + + to start the bot. diff --git a/zulip_bots/zulip_bots/bots/witai/test_witai.py b/zulip_bots/zulip_bots/bots/witai/test_witai.py new file mode 100644 index 0000000..acd73ea --- /dev/null +++ b/zulip_bots/zulip_bots/bots/witai/test_witai.py @@ -0,0 +1,59 @@ +from mock import patch +import sys +from typing import Dict, Any, Optional +from zulip_bots.test_lib import BotTestCase, get_bot_message_handler, StubBotHandler + +class TestWitaiBot(BotTestCase): + bot_name = 'witai' + + MOCK_CONFIG_INFO = { + 'token': '12345678', + 'handler_location': '/Users/abcd/efgh', + 'help_message': 'Qwertyuiop!' + } + + MOCK_WITAI_RESPONSE = { + '_text': 'What is your favorite food?', + 'entities': { + 'intent': [{ + 'confidence': 1.0, + 'value': 'favorite_food' + }] + } + } + + def test_normal(self) -> None: + with self.mock_config_info(self.MOCK_CONFIG_INFO): + get_bot_message_handler(self.bot_name).initialize(StubBotHandler()) + + with patch('wit.Wit.message') as message: + message.return_value = self.MOCK_WITAI_RESPONSE + + with patch('zulip_bots.bots.witai.witai.get_handle') as handler: + handler.return_value = mock_handle + + self.verify_reply( + 'What is your favorite food?', + 'pizza' + ) + + # This overrides the default one in `BotTestCase`. + def test_bot_responds_to_empty_message(self) -> None: + with self.mock_config_info(self.MOCK_CONFIG_INFO): + get_bot_message_handler(self.bot_name).initialize(StubBotHandler()) + + with patch('wit.Wit.message') as message: + message.return_value = self.MOCK_WITAI_RESPONSE + + with patch('zulip_bots.bots.witai.witai.get_handle') as handler: + handler.return_value = mock_handle + + self.verify_reply('', 'Qwertyuiop!') + +def mock_handle(res: Dict[str, Any]) -> Optional[str]: + if res['entities']['intent'][0]['value'] == 'favorite_food': + return 'pizza' + if res['entities']['intent'][0]['value'] == 'favorite_drink': + return 'coffee' + + return None diff --git a/zulip_bots/zulip_bots/bots/witai/witai.conf b/zulip_bots/zulip_bots/bots/witai/witai.conf new file mode 100644 index 0000000..7fa478d --- /dev/null +++ b/zulip_bots/zulip_bots/bots/witai/witai.conf @@ -0,0 +1,4 @@ +[witai] +token = +handler_location = +help_message = diff --git a/zulip_bots/zulip_bots/bots/witai/witai.py b/zulip_bots/zulip_bots/bots/witai/witai.py new file mode 100644 index 0000000..3407cb7 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/witai/witai.py @@ -0,0 +1,78 @@ +# See readme.md for instructions on running this code. + +from typing import Dict, Any, Optional, Callable +import wit +import sys +import importlib.util + +class WitaiHandler(object): + def usage(self) -> str: + return ''' + Wit.ai bot uses pywit API to interact with Wit.ai. In order to use + Wit.ai bot, `witai.conf` must be set up. See `doc.md` for more details. + ''' + + def initialize(self, bot_handler: Any) -> None: + config = bot_handler.get_config_info('witai') + + token = config.get('token') + if not token: + raise KeyError('No `token` was specified') + + # `handler_location` should be the location of a module which contains + # the function `handle`. See `doc.md` for more details. + handler_location = config.get('handler_location') + if not handler_location: + raise KeyError('No `handler_location` was specified') + self.handle = get_handle(handler_location) + + help_message = config.get('help_message') + if not help_message: + raise KeyError('No `help_message` was specified') + self.help_message = help_message + + self.client = wit.Wit(token) + + def handle_message(self, message: Dict[str, str], bot_handler: Any) -> None: + if message['content'] == '' or message['content'] == 'help': + bot_handler.send_reply(message, self.help_message) + return + + try: + res = self.client.message(message['content']) + message_for_user = self.handle(res) + + if message_for_user: + bot_handler.send_reply(message, message_for_user) + except wit.wit.WitError: + bot_handler.send_reply(message, 'Sorry, I don\'t know how to respond to that!') + except Exception as e: + bot_handler.send_reply(message, 'Sorry, there was an internal error.') + print(e) + return + +handler_class = WitaiHandler + +def get_handle(location: str) -> Callable[[Dict[str, Any]], Optional[str]]: + '''Returns a function to be used when generating a response from Wit.ai + bot. This function is the function named `handle` in the module at the + given `location`. For an example of a `handle` function, see `doc.md`. + + For example, + + handle = get_handle('/Users/someuser/witai_handler.py') # Get the handler function. + res = witai_client.message(message['content']) # Get the Wit.ai response. + message_res = self.handle(res) # Handle the response and find what to show the user. + bot_handler.send_reply(message, message_res) # Send it to the user. + + Parameters: + - location: The absolute path to the module to look for `handle` in. + ''' + try: + spec = importlib.util.spec_from_file_location('module.name', location) + handler = importlib.util.module_from_spec(spec) + spec.loader.exec_module(handler) + return handler.handle # type: ignore + except Exception as e: + print(e) + return None