From d15852347cba62fbdb75a98d87bda5f60db56196 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Mon, 20 Feb 2017 19:28:32 -0800 Subject: [PATCH] Add official Redis driver for RiveScript Sessions --- .gitignore | 3 + Changes.md | 6 ++ contrib/redis/LICENSE.txt | 21 ++++++ contrib/redis/README.md | 57 ++++++++++++++++ contrib/redis/requirements.txt | 2 + contrib/redis/rivescript_redis.py | 108 ++++++++++++++++++++++++++++++ contrib/redis/setup.cfg | 2 + contrib/redis/setup.py | 29 ++++++++ eg/sessions/redis_bot.py | 18 ++++- eg/sessions/redis_storage.py | 82 ----------------------- eg/sessions/requirements.txt | 2 +- rivescript/__init__.py | 2 +- rivescript/brain.py | 2 +- 13 files changed, 247 insertions(+), 87 deletions(-) create mode 100644 contrib/redis/LICENSE.txt create mode 100644 contrib/redis/README.md create mode 100644 contrib/redis/requirements.txt create mode 100644 contrib/redis/rivescript_redis.py create mode 100644 contrib/redis/setup.cfg create mode 100644 contrib/redis/setup.py delete mode 100644 eg/sessions/redis_storage.py diff --git a/.gitignore b/.gitignore index 8d35cb3..8f7b4dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ __pycache__ *.pyc +build/ +dist/ +*.egg-info/ diff --git a/Changes.md b/Changes.md index 32be729..ed22545 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,12 @@ Revision history for the Python package RiveScript. +## 1.14.5 - Feb 20 2017 + +- Bugfix when storing the user's `last_match` variable when a `%Previous` is + active (it was storing a regexp object and not a string), to help third party + session drivers (e.g. Redis) to work. + ## 1.14.4 - Dec 14 2016 - Fix the `last_match()` function so that it returns `None` when there was no diff --git a/contrib/redis/LICENSE.txt b/contrib/redis/LICENSE.txt new file mode 100644 index 0000000..cabbc44 --- /dev/null +++ b/contrib/redis/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Noah Petherbridge + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/contrib/redis/README.md b/contrib/redis/README.md new file mode 100644 index 0000000..e61799c --- /dev/null +++ b/contrib/redis/README.md @@ -0,0 +1,57 @@ +# Redis Sessions for RiveScript + +This module installs support for using a [Redis cache](https://redis.io/) to +store user variables for RiveScript. + +```bash +pip install rivescript-redis +``` + +By default, RiveScript keeps user variables in an in-memory dictionary. This +driver allows for using a Redis cache instead. All user variables will then be +persisted to Redis automatically, which enables the bot to remember users after +a reboot. + +## Quick Start + +```python +from rivescript import RiveScript +from rivescript_redis import RedisSessionManager + +# Initialize RiveScript like normal but give it the RedisSessionManager. +bot = RiveScript( + session_manager=RedisSessionManager( + # You can customize the key prefix: this is the default. Be sure to + # include a separator like '/' at the end so the keys end up looking + # like e.g. 'rivescript/username' + prefix='rivescript/', + + # All other options are passed directly through to redis.StrictRedis() + host='localhost', + port=6379, + db=0, + ), +) + +bot.load_directory("eg/brain") +bot.sort_replies() + +# Get a reply. The user variables for 'alice' would be persisted in Redis +# at the (default) key 'rivescript/alice' +print(bot.reply("alice", "Hello robot!")) +``` + +## Example + +An example bot that uses this driver can be found in the +[`eg/sessions`](https://github.com/aichaos/rivescript-python/tree/master/eg/sessions) +directory of the `rivescript-python` project. + +## See Also + +* Documentation for [redis-py](https://redis-py.readthedocs.io/en/latest/), + the Redis client module used by this driver. + +## License + +This module is licensed under the same terms as RiveScript itself (MIT). diff --git a/contrib/redis/requirements.txt b/contrib/redis/requirements.txt new file mode 100644 index 0000000..c5a0df5 --- /dev/null +++ b/contrib/redis/requirements.txt @@ -0,0 +1,2 @@ +redis +rivescript diff --git a/contrib/redis/rivescript_redis.py b/contrib/redis/rivescript_redis.py new file mode 100644 index 0000000..5e58c57 --- /dev/null +++ b/contrib/redis/rivescript_redis.py @@ -0,0 +1,108 @@ +# RiveScript-Python +# +# This code is released under the MIT License. +# See the "LICENSE" file for more information. +# +# https://www.rivescript.com/ + +from __future__ import unicode_literals +import json +import redis +from rivescript.sessions import SessionManager + +__author__ = 'Noah Petherbridge' +__copyright__ = 'Copyright 2017, Noah Petherbridge' +__license__ = 'MIT' +__status__ = 'Beta' +__version__ = '0.1.0' + +class RedisSessionManager(SessionManager): + """A Redis powered session manager for RiveScript.""" + + def __init__(self, prefix="rivescript/", *args, **kwargs): + """Initialize the Redis session driver. + + Apart from the ``prefix`` parameter, all other options are passed + directly to the underlying Redis constructor, ``redis.StrictRedis()``. + See the documentation of redis-py for more information. Commonly used + arguments are listed below for convenience. + + Args: + prefix (string): the key to prefix all the Redis keys with. The + default is ``rivescript/``, so that for a username of ``alice`` + the key would be ``rivescript/alice``. + host (string): Hostname of the Redis server. + port (int): Port number of the Redis server. + db (int): Database number in Redis. + """ + self.client = redis.StrictRedis(*args, **kwargs) + self.prefix = prefix + self.frozen = "frozen:" + prefix + + def _key(self, username, frozen=False): + """Translate a username into a key for Redis.""" + if frozen: + return self.frozen + username + return self.prefix + username + + def _get_user(self, username): + """Custom helper method to retrieve a user's data from Redis.""" + data = self.client.get(self._key(username)) + if data is None: + return None + return json.loads(data.decode()) + + # The below functions implement the RiveScript SessionManager. + + def set(self, username, new_vars): + data = self._get_user(username) + if data is None: + data = self.default_session() + data.update(new_vars) + self.client.set(self._key(username), json.dumps(data)) + + def get(self, username, key): + data = self._get_user(username) + if data is None: + return None + return data.get(key, "undefined") + + def get_any(self, username): + return self._get_user(username) + + def get_all(self): + users = self.client.keys(self.prefix + "*") + result = dict() + for user in users: + username = users.replace(self.prefix, "") + result[username] = self._get_user(username) + return result + + def reset(self, username): + self.client.delete(self._key(username)) + + def reset_all(self): + users = self.client.keys(self.prefix + "*") + for user in users: + self.c.delete(user) + + def freeze(self, username): + data = self._get_user(username) + if data is not None: + self.client.set(self._key(username, True), json.dumps(data)) + + def thaw(self, username, action="thaw"): + data = self.client.get(self.key(username, True)) + if data is not None: + data = json.loads(data.decode()) + if action == "thaw": + self.reset(username) + self.set(username, data) + self.c.delete(self.key(username, True)) + elif action == "discard": + self.c.delete(self.key(username, True)) + elif action == "keep": + self.reset(username) + self.set(username, data) + else: + raise ValueError("unsupported thaw action") diff --git a/contrib/redis/setup.cfg b/contrib/redis/setup.cfg new file mode 100644 index 0000000..b88034e --- /dev/null +++ b/contrib/redis/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md diff --git a/contrib/redis/setup.py b/contrib/redis/setup.py new file mode 100644 index 0000000..d790fba --- /dev/null +++ b/contrib/redis/setup.py @@ -0,0 +1,29 @@ +# rivescript-python setup.py + +import rivescript_redis +from setuptools import setup + +setup( + name = 'rivescript_redis', + version = rivescript_redis.__version__, + description = 'Redis driver for RiveScript', + long_description = 'Stores user variables for RiveScript in a Redis cache', + author = 'Noah Petherbridge', + author_email = 'root@kirsle.net', + url = 'https://github.com/aichaos/rivescript-python', + license = 'MIT', + py_modules = ['rivescript_redis'], + keywords = ['rivescript'], + classifiers = [ + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3', + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Topic :: Scientific/Engineering :: Artificial Intelligence', + ], + install_requires = [ 'setuptools', 'redis', 'rivescript' ], +) + +# vim:expandtab diff --git a/eg/sessions/redis_bot.py b/eg/sessions/redis_bot.py index 95e6a4c..6417df4 100644 --- a/eg/sessions/redis_bot.py +++ b/eg/sessions/redis_bot.py @@ -3,11 +3,25 @@ from __future__ import unicode_literals, print_function, absolute_import from six.moves import input import sys + +# Manipulate sys.path to be able to import rivescript from this local git +# repository. +import os +sys.path.append(os.path.join( + os.path.dirname(__file__), + "..", "..", +)) +sys.path.append(os.path.join( + os.path.dirname(__file__), + "..", "..", + "contrib", "redis", +)) + from rivescript import RiveScript -from redis_storage import RedisSessionStorage +from rivescript_redis import RedisSessionManager bot = RiveScript( - session_manager=RedisSessionStorage(), + session_manager=RedisSessionManager(), ) bot.load_directory("../brain") bot.sort_replies() diff --git a/eg/sessions/redis_storage.py b/eg/sessions/redis_storage.py deleted file mode 100644 index 2e23eee..0000000 --- a/eg/sessions/redis_storage.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python - -from __future__ import unicode_literals -import json -import redis -from rivescript.sessions import SessionManager - -class RedisSessionStorage(SessionManager): - """A RiveScript session store backed by a Redis cache. - - Parameters: - host (str): The Redis server host, default ``localhost``. - port (int): The Redis port number, default ``6379``. - db (int): The Redis database number, default ``0``. - """ - - def __init__(self, host='localhost', port=6379, db=0): - self.c = redis.StrictRedis(host=host, port=port, db=db) - - def key(self, username, frozen=False): - """Translate a username into a key for Redis.""" - if frozen: - return "rs-users-frozen/{}".format(username) - return "rs-users/{}".format(username) - - def get_user(self, username): - """Custom method to retrieve a user's data from Redis.""" - data = self.c.get(self.key(username)) - if data is None: - return None - return json.loads(data.decode()) - - def set(self, username, vars): - data = self.get_user(username) - if data is None: - data = self.default_session() - data.update(vars) - self.c.set(self.key(username), json.dumps(data)) - - def get(self, username, key): - data = self.get_user(username) - if data is None: - return None - return data.get(key, "undefined") - - def get_any(self, username): - return self.get_user(username) - - def get_all(self): - users = self.c.keys("rs-users/*") - result = dict() - for user in users: - username = users.replace("rs-users/", "") - result[username] = self.get_user(username) - return result - - def reset(self, username): - self.c.delete(self.key(username)) - - def reset_all(self): - users = self.c.keys("rs-users/") - for user in users: - self.c.delete(user) - - def freeze(self, username): - data = self.get_user(username) - if data is not None: - self.c.set(self.key(username, True), json.dumps(data)) - - def thaw(self, username, action="thaw"): - data = self.c.get(self.key(username, True)) - if data is not None: - data = json.loads(data.decode()) - if action == "thaw": - self.set(username, data) - self.c.delete(self.key(username, True)) - elif action == "discard": - self.c.delete(self.key(username, True)) - elif action == "keep": - self.set(username, data) - else: - raise ValueError("Unsupported thaw action.") diff --git a/eg/sessions/requirements.txt b/eg/sessions/requirements.txt index 7800f0f..3a308e4 100644 --- a/eg/sessions/requirements.txt +++ b/eg/sessions/requirements.txt @@ -1 +1 @@ -redis +rivescript-redis diff --git a/rivescript/__init__.py b/rivescript/__init__.py index ed11693..1099e64 100644 --- a/rivescript/__init__.py +++ b/rivescript/__init__.py @@ -20,7 +20,7 @@ __docformat__ = 'plaintext' __all__ = ['rivescript'] -__version__ = '1.14.4' +__version__ = '1.14.5' from .rivescript import RiveScript from .exceptions import ( diff --git a/rivescript/brain.py b/rivescript/brain.py index f85ad0e..6532dde 100644 --- a/rivescript/brain.py +++ b/rivescript/brain.py @@ -246,7 +246,7 @@ def _getreply(self, user, msg, context='normal', step=0, ignore_object_errors=Tr if match: self.say("Found a match!") matched = trig[1] - matchedTrigger = subtrig + matchedTrigger = user_side["trigger"] foundMatch = True # Get the stars!