From de2b9c352b246044ec7a2f72dc466a66504ab66c Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 6 Mar 2018 13:59:08 -0500 Subject: [PATCH] Local call --- chatter/__pycache__/__init__.cpython-36.pyc | Bin 0 -> 279 bytes chatter/__pycache__/chatter.cpython-36.pyc | Bin 0 -> 3944 bytes chatter/chatter.py | 17 +- chatter/source/__init__.py | 13 + chatter/source/__main__.py | 23 + .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 431 bytes .../__pycache__/adapters.cpython-36.pyc | Bin 0 -> 2088 bytes .../__pycache__/chatterbot.cpython-36.pyc | Bin 0 -> 4567 bytes .../__pycache__/comparisons.cpython-36.pyc | Bin 0 -> 10029 bytes .../__pycache__/constants.cpython-36.pyc | Bin 0 -> 279 bytes .../__pycache__/conversation.cpython-36.pyc | Bin 0 -> 7190 bytes .../__pycache__/preprocessors.cpython-36.pyc | Bin 0 -> 1352 bytes .../response_selection.cpython-36.pyc | Bin 0 -> 2260 bytes .../__pycache__/trainers.cpython-36.pyc | Bin 0 -> 12169 bytes .../source/__pycache__/utils.cpython-36.pyc | Bin 0 -> 4635 bytes chatter/source/adapters.py | 47 ++ chatter/source/chatterbot.py | 173 ++++ chatter/source/comparisons.py | 331 ++++++++ chatter/source/constants.py | 15 + chatter/source/conversation.py | 229 ++++++ chatter/source/corpus.py | 11 + chatter/source/ext/__init__.py | 0 .../ext/__pycache__/__init__.cpython-36.pyc | Bin 0 -> 128 bytes .../source/ext/django_chatterbot/__init__.py | 3 + .../ext/django_chatterbot/abstract_models.py | 261 ++++++ chatter/source/ext/django_chatterbot/admin.py | 31 + chatter/source/ext/django_chatterbot/apps.py | 8 + .../source/ext/django_chatterbot/factories.py | 42 + .../django_chatterbot/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/train.py | 29 + .../migrations/0001_initial.py | 39 + .../migrations/0002_statement_extra_data.py | 21 + .../0003_change_occurrence_default.py | 20 + .../migrations/0004_rename_in_response_to.py | 26 + .../migrations/0005_statement_created_at.py | 24 + .../migrations/0006_create_conversation.py | 33 + .../migrations/0007_response_created_at.py | 24 + .../migrations/0008_update_conversations.py | 32 + .../django_chatterbot/migrations/0009_tags.py | 35 + .../migrations/0010_statement_text.py | 20 + .../migrations/0011_blank_extra_data.py | 20 + .../django_chatterbot/migrations/__init__.py | 0 .../source/ext/django_chatterbot/models.py | 34 + .../source/ext/django_chatterbot/settings.py | 19 + chatter/source/ext/django_chatterbot/urls.py | 11 + chatter/source/ext/django_chatterbot/views.py | 118 +++ chatter/source/ext/sqlalchemy_app/__init__.py | 0 .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 143 bytes .../__pycache__/models.cpython-36.pyc | Bin 0 -> 3947 bytes .../__pycache__/types.cpython-36.pyc | Bin 0 -> 807 bytes chatter/source/ext/sqlalchemy_app/models.py | 132 +++ chatter/source/ext/sqlalchemy_app/types.py | 21 + chatter/source/filters.py | 47 ++ chatter/source/input/__init__.py | 18 + .../input/__pycache__/__init__.cpython-36.pyc | Bin 0 -> 565 bytes .../input/__pycache__/gitter.cpython-36.pyc | Bin 0 -> 5019 bytes .../input/__pycache__/hipchat.cpython-36.pyc | Bin 0 -> 2838 bytes .../__pycache__/input_adapter.cpython-36.pyc | Bin 0 -> 1291 bytes .../input/__pycache__/mailgun.cpython-36.pyc | Bin 0 -> 1965 bytes .../__pycache__/microsoft.cpython-36.pyc | Bin 0 -> 3569 bytes .../input/__pycache__/terminal.cpython-36.pyc | Bin 0 -> 820 bytes ...variable_input_type_adapter.cpython-36.pyc | Bin 0 -> 2104 bytes chatter/source/input/gitter.py | 176 ++++ chatter/source/input/hipchat.py | 113 +++ chatter/source/input/input_adapter.py | 33 + chatter/source/input/mailgun.py | 61 ++ chatter/source/input/microsoft.py | 115 +++ chatter/source/input/terminal.py | 18 + .../input/variable_input_type_adapter.py | 65 ++ chatter/source/logic/__init__.py | 20 + .../logic/__pycache__/__init__.cpython-36.pyc | Bin 0 -> 704 bytes .../__pycache__/best_match.cpython-36.pyc | Bin 0 -> 2159 bytes .../__pycache__/logic_adapter.cpython-36.pyc | Bin 0 -> 4037 bytes .../__pycache__/low_confidence.cpython-36.pyc | Bin 0 -> 1964 bytes .../mathematical_evaluation.cpython-36.pyc | Bin 0 -> 2138 bytes .../__pycache__/multi_adapter.cpython-36.pyc | Bin 0 -> 4798 bytes .../no_knowledge_adapter.cpython-36.pyc | Bin 0 -> 1062 bytes .../specific_response.cpython-36.pyc | Bin 0 -> 1416 bytes .../__pycache__/time_adapter.cpython-36.pyc | Bin 0 -> 3068 bytes chatter/source/logic/best_match.py | 84 ++ chatter/source/logic/logic_adapter.py | 100 +++ chatter/source/logic/low_confidence.py | 58 ++ .../source/logic/mathematical_evaluation.py | 67 ++ chatter/source/logic/multi_adapter.py | 153 ++++ chatter/source/logic/no_knowledge_adapter.py | 26 + chatter/source/logic/specific_response.py | 38 + chatter/source/logic/time_adapter.py | 91 +++ chatter/source/output/__init__.py | 15 + .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 480 bytes .../output/__pycache__/gitter.cpython-36.pyc | Bin 0 -> 2915 bytes .../output/__pycache__/hipchat.cpython-36.pyc | Bin 0 -> 2191 bytes .../output/__pycache__/mailgun.cpython-36.pyc | Bin 0 -> 1621 bytes .../__pycache__/microsoft.cpython-36.pyc | Bin 0 -> 3347 bytes .../__pycache__/output_adapter.cpython-36.pyc | Bin 0 -> 974 bytes .../__pycache__/terminal.cpython-36.pyc | Bin 0 -> 757 bytes chatter/source/output/gitter.py | 85 ++ chatter/source/output/hipchat.py | 67 ++ chatter/source/output/mailgun.py | 49 ++ chatter/source/output/microsoft.py | 109 +++ chatter/source/output/output_adapter.py | 20 + chatter/source/output/terminal.py | 16 + chatter/source/parsing.py | 751 ++++++++++++++++++ chatter/source/preprocessors.py | 60 ++ chatter/source/response_selection.py | 71 ++ chatter/source/storage/__init__.py | 12 + .../__pycache__/__init__.cpython-36.pyc | Bin 0 -> 417 bytes .../__pycache__/django_storage.cpython-36.pyc | Bin 0 -> 6425 bytes .../__pycache__/mongodb.cpython-36.pyc | Bin 0 -> 9350 bytes .../__pycache__/sql_storage.cpython-36.pyc | Bin 0 -> 9766 bytes .../storage_adapter.cpython-36.pyc | Bin 0 -> 6517 bytes chatter/source/storage/django_storage.py | 220 +++++ chatter/source/storage/mongodb.py | 394 +++++++++ chatter/source/storage/sql_storage.py | 403 ++++++++++ chatter/source/storage/storage_adapter.py | 171 ++++ chatter/source/trainers.py | 426 ++++++++++ chatter/source/utils.py | 200 +++++ 117 files changed, 6208 insertions(+), 6 deletions(-) create mode 100644 chatter/__pycache__/__init__.cpython-36.pyc create mode 100644 chatter/__pycache__/chatter.cpython-36.pyc create mode 100644 chatter/source/__init__.py create mode 100644 chatter/source/__main__.py create mode 100644 chatter/source/__pycache__/__init__.cpython-36.pyc create mode 100644 chatter/source/__pycache__/adapters.cpython-36.pyc create mode 100644 chatter/source/__pycache__/chatterbot.cpython-36.pyc create mode 100644 chatter/source/__pycache__/comparisons.cpython-36.pyc create mode 100644 chatter/source/__pycache__/constants.cpython-36.pyc create mode 100644 chatter/source/__pycache__/conversation.cpython-36.pyc create mode 100644 chatter/source/__pycache__/preprocessors.cpython-36.pyc create mode 100644 chatter/source/__pycache__/response_selection.cpython-36.pyc create mode 100644 chatter/source/__pycache__/trainers.cpython-36.pyc create mode 100644 chatter/source/__pycache__/utils.cpython-36.pyc create mode 100644 chatter/source/adapters.py create mode 100644 chatter/source/chatterbot.py create mode 100644 chatter/source/comparisons.py create mode 100644 chatter/source/constants.py create mode 100644 chatter/source/conversation.py create mode 100644 chatter/source/corpus.py create mode 100644 chatter/source/ext/__init__.py create mode 100644 chatter/source/ext/__pycache__/__init__.cpython-36.pyc create mode 100644 chatter/source/ext/django_chatterbot/__init__.py create mode 100644 chatter/source/ext/django_chatterbot/abstract_models.py create mode 100644 chatter/source/ext/django_chatterbot/admin.py create mode 100644 chatter/source/ext/django_chatterbot/apps.py create mode 100644 chatter/source/ext/django_chatterbot/factories.py create mode 100644 chatter/source/ext/django_chatterbot/management/__init__.py create mode 100644 chatter/source/ext/django_chatterbot/management/commands/__init__.py create mode 100644 chatter/source/ext/django_chatterbot/management/commands/train.py create mode 100644 chatter/source/ext/django_chatterbot/migrations/0001_initial.py create mode 100644 chatter/source/ext/django_chatterbot/migrations/0002_statement_extra_data.py create mode 100644 chatter/source/ext/django_chatterbot/migrations/0003_change_occurrence_default.py create mode 100644 chatter/source/ext/django_chatterbot/migrations/0004_rename_in_response_to.py create mode 100644 chatter/source/ext/django_chatterbot/migrations/0005_statement_created_at.py create mode 100644 chatter/source/ext/django_chatterbot/migrations/0006_create_conversation.py create mode 100644 chatter/source/ext/django_chatterbot/migrations/0007_response_created_at.py create mode 100644 chatter/source/ext/django_chatterbot/migrations/0008_update_conversations.py create mode 100644 chatter/source/ext/django_chatterbot/migrations/0009_tags.py create mode 100644 chatter/source/ext/django_chatterbot/migrations/0010_statement_text.py create mode 100644 chatter/source/ext/django_chatterbot/migrations/0011_blank_extra_data.py create mode 100644 chatter/source/ext/django_chatterbot/migrations/__init__.py create mode 100644 chatter/source/ext/django_chatterbot/models.py create mode 100644 chatter/source/ext/django_chatterbot/settings.py create mode 100644 chatter/source/ext/django_chatterbot/urls.py create mode 100644 chatter/source/ext/django_chatterbot/views.py create mode 100644 chatter/source/ext/sqlalchemy_app/__init__.py create mode 100644 chatter/source/ext/sqlalchemy_app/__pycache__/__init__.cpython-36.pyc create mode 100644 chatter/source/ext/sqlalchemy_app/__pycache__/models.cpython-36.pyc create mode 100644 chatter/source/ext/sqlalchemy_app/__pycache__/types.cpython-36.pyc create mode 100644 chatter/source/ext/sqlalchemy_app/models.py create mode 100644 chatter/source/ext/sqlalchemy_app/types.py create mode 100644 chatter/source/filters.py create mode 100644 chatter/source/input/__init__.py create mode 100644 chatter/source/input/__pycache__/__init__.cpython-36.pyc create mode 100644 chatter/source/input/__pycache__/gitter.cpython-36.pyc create mode 100644 chatter/source/input/__pycache__/hipchat.cpython-36.pyc create mode 100644 chatter/source/input/__pycache__/input_adapter.cpython-36.pyc create mode 100644 chatter/source/input/__pycache__/mailgun.cpython-36.pyc create mode 100644 chatter/source/input/__pycache__/microsoft.cpython-36.pyc create mode 100644 chatter/source/input/__pycache__/terminal.cpython-36.pyc create mode 100644 chatter/source/input/__pycache__/variable_input_type_adapter.cpython-36.pyc create mode 100644 chatter/source/input/gitter.py create mode 100644 chatter/source/input/hipchat.py create mode 100644 chatter/source/input/input_adapter.py create mode 100644 chatter/source/input/mailgun.py create mode 100644 chatter/source/input/microsoft.py create mode 100644 chatter/source/input/terminal.py create mode 100644 chatter/source/input/variable_input_type_adapter.py create mode 100644 chatter/source/logic/__init__.py create mode 100644 chatter/source/logic/__pycache__/__init__.cpython-36.pyc create mode 100644 chatter/source/logic/__pycache__/best_match.cpython-36.pyc create mode 100644 chatter/source/logic/__pycache__/logic_adapter.cpython-36.pyc create mode 100644 chatter/source/logic/__pycache__/low_confidence.cpython-36.pyc create mode 100644 chatter/source/logic/__pycache__/mathematical_evaluation.cpython-36.pyc create mode 100644 chatter/source/logic/__pycache__/multi_adapter.cpython-36.pyc create mode 100644 chatter/source/logic/__pycache__/no_knowledge_adapter.cpython-36.pyc create mode 100644 chatter/source/logic/__pycache__/specific_response.cpython-36.pyc create mode 100644 chatter/source/logic/__pycache__/time_adapter.cpython-36.pyc create mode 100644 chatter/source/logic/best_match.py create mode 100644 chatter/source/logic/logic_adapter.py create mode 100644 chatter/source/logic/low_confidence.py create mode 100644 chatter/source/logic/mathematical_evaluation.py create mode 100644 chatter/source/logic/multi_adapter.py create mode 100644 chatter/source/logic/no_knowledge_adapter.py create mode 100644 chatter/source/logic/specific_response.py create mode 100644 chatter/source/logic/time_adapter.py create mode 100644 chatter/source/output/__init__.py create mode 100644 chatter/source/output/__pycache__/__init__.cpython-36.pyc create mode 100644 chatter/source/output/__pycache__/gitter.cpython-36.pyc create mode 100644 chatter/source/output/__pycache__/hipchat.cpython-36.pyc create mode 100644 chatter/source/output/__pycache__/mailgun.cpython-36.pyc create mode 100644 chatter/source/output/__pycache__/microsoft.cpython-36.pyc create mode 100644 chatter/source/output/__pycache__/output_adapter.cpython-36.pyc create mode 100644 chatter/source/output/__pycache__/terminal.cpython-36.pyc create mode 100644 chatter/source/output/gitter.py create mode 100644 chatter/source/output/hipchat.py create mode 100644 chatter/source/output/mailgun.py create mode 100644 chatter/source/output/microsoft.py create mode 100644 chatter/source/output/output_adapter.py create mode 100644 chatter/source/output/terminal.py create mode 100644 chatter/source/parsing.py create mode 100644 chatter/source/preprocessors.py create mode 100644 chatter/source/response_selection.py create mode 100644 chatter/source/storage/__init__.py create mode 100644 chatter/source/storage/__pycache__/__init__.cpython-36.pyc create mode 100644 chatter/source/storage/__pycache__/django_storage.cpython-36.pyc create mode 100644 chatter/source/storage/__pycache__/mongodb.cpython-36.pyc create mode 100644 chatter/source/storage/__pycache__/sql_storage.cpython-36.pyc create mode 100644 chatter/source/storage/__pycache__/storage_adapter.cpython-36.pyc create mode 100644 chatter/source/storage/django_storage.py create mode 100644 chatter/source/storage/mongodb.py create mode 100644 chatter/source/storage/sql_storage.py create mode 100644 chatter/source/storage/storage_adapter.py create mode 100644 chatter/source/trainers.py create mode 100644 chatter/source/utils.py diff --git a/chatter/__pycache__/__init__.cpython-36.pyc b/chatter/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b01e7cda67d98e13d52e17e52a26b17ad5a2bebf GIT binary patch literal 279 zcmXr!<>ktq(;F4Uz`*brh~a<<$Z`PUVhJFT!jQt4!;s4u#mLBz!j!_?!Vtxj!V=7& z$@-EJD6PqOi`_XRv7{umC>g{D0T9g$#LhrmECeKK7_t~j7;6}s8Jj_-FaUKk`e`!V zVoyv-iBHZ?F9O*JG>JJWzhost5i5}QOUc&r zyk0@&E!N`HlF|YekToC^7#NG#fux@%a}+yNIoM>dDA-gaf(@wh7Kcr4eoARhsvXG5 IU^94_0dGAwIRF3v literal 0 HcmV?d00001 diff --git a/chatter/__pycache__/chatter.cpython-36.pyc b/chatter/__pycache__/chatter.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b2d8af6b9b2e543ab6c01d6872983dc7355907fa GIT binary patch literal 3944 zcmZu!&5ztj6|b`0?$7Dz>4fa;2gr+Hmj>+&n}pSBgwX8HZa!ccg=8R$6=Zo_o*wtO z?M_v7GTroWU?RjN2ysL!BrcKQ$REH3{sN9Z!fF47ocO(Rw`Yjxmdnpoub%6D{+@5F zt+jq#{KmbJF^_ z;WrFjOPk}C-vaH5dfFa${End;>DsvKcMVT7T_3Oe>*Jo^7w@2EXlJ0FB(xk9*3DwpxwPZJ4%M2?EA6!l4A$x`rdJXdV{Z8B&t(Eg}S9HFx#G2@umGyE(e1-NH zsXeT2i2k!0_VvUSaTRhm#5!zotyD?Ja{l9%hu^_*qP!>{dYV*IUg$+o`7qa>#@&0G zq^YOF^u$Y(lh_-_ zbKC__I`9^CpB_jD-m&~lcOY-vW`V~t@-pg(Q!&q;D~Uh{1!PF)*v6R6*lyi zB&TQ$V;w{YiNd8Z8OjDt*O$ie+Y0N(WDwq@rF9t(Avc!6FwGCcwCG+;kd=yhsAgG| zu|@sb34Q_WP<&_JUbZJMJ-Lni7x6A0Vh>l)WRc#$Y4XZz+TjJjx>;M z(6VnPh7@2%R75xV~pL|3yeiv^rRw>+Y zGND~{*h*tVQ5T7daw&@Dty~@^0vTGY-8ne8_vvXAPe|aPE?=jU*pWV!Uo})d)zTx1 z(vRYi-mX+Qj7=Vt!<)pg5iIkza?Cd|t!OiI9sZ8pD++z}Rn$KJnnFt8-$42(@B7EeY$)E^zb zdVaU8DZ5}-T*@n& z6f^-zSn6#~{i@ogtRiS`gw)OZkUW%4+i63|he5lnbGM^5G^-eDM>RLlE)DtPZM>Kx%+9}Mz&1f~q2H?(vJve2U@Waue~ZuC zE9=n8$~0tv$}|LIWK0th3lL-)ps{ssmBDakQ)pZ^$ih}TX3oAa8W{;YVM+mnMuE4_ z?d5()*W|I`N6B9WSk^N$!1L9NF|#}0tt@v?_8L?$sY)Iq0tMMH4YT1C`jpiaHPlq2 zYTN$&owB@nsXK0)SB>v;67&Yqvs_X+eG)nu1uiH^fD0H@{kKx+H17 z@u`%E|2&GQFbi+zTLV{KhvAHmxGf!<5^l8_5@b z=Q2p3curBqU#(j51~@;*rKkimw{Mko$+0$3nflgzZRKi{`Blmj7*mlj{{1Tm*d1LJ zS>JdTK7+pi$s_B;mM`Evi+Zb3{Wa94*8s`jYyg4*y$kqHeIfvN1iAoR>c?!X+0rDD*v*Y^rB@7A)bylQHTV`XNGW za-|*wB9DSV68OmPU?|!LI2~jXWS4nF$s(yH6}6$vrxSdQka?G52 zhnOihLwd)dK2eUG|^p+-8jmv3G%JcGU@4tbXbf(uEw zu{`CnCE225(AoSp0uqsd_(QIm&jB>|)yx)_qzk^nRrm&qV)JR>ua^&7CP&d~nXdxV jWtHs}9rgd|P&B?@TJ#6>&1p(NgZHckTJz5G>RJB<{}Jf} literal 0 HcmV?d00001 diff --git a/chatter/chatter.py b/chatter/chatter.py index c7ff84a..9437030 100644 --- a/chatter/chatter.py +++ b/chatter/chatter.py @@ -7,8 +7,8 @@ from discord.ext import commands from redbot.core import Config from redbot.core.bot import Red -from .chatterbot import ChatBot -from .chatterbot.trainers import ListTrainer +from .source import ChatBot +from .source.trainers import ListTrainer from datetime import datetime,timedelta @@ -31,6 +31,8 @@ class Chatter: self.config.register_global(**default_global) self.config.register_guild(**default_guild) + + self.loop = asyncio.get_event_loop() async def _get_conversation(self, ctx, in_channel: discord.TextChannel): """ @@ -59,12 +61,13 @@ class Chatter: return out - async def _train(self, data): + def _train(self, data): try: self.chatbot.train(data) except: return False return True + @commands.group() async def chatter(self, ctx: commands.Context): """ @@ -90,12 +93,14 @@ class Chatter: conversation = await self._get_conversation(ctx, channel) - await ctx.send("Gather successful! Training begins now") if not conversation: await ctx.send("Failed to gather training data") return - - if await self._train(conversation): + + await ctx.send("Gather successful! Training begins now\n(**This will take a long time, be patient**)") + future = await self.loop.run_in_executor(None, self._train, conversation) + + if future: await ctx.send("Training successful!") else: await ctx.send("Error occurred :(") diff --git a/chatter/source/__init__.py b/chatter/source/__init__.py new file mode 100644 index 0000000..2ea55f6 --- /dev/null +++ b/chatter/source/__init__.py @@ -0,0 +1,13 @@ +""" +ChatterBot is a machine learning, conversational dialog engine. +""" +from .chatterbot import ChatBot + +__version__ = '0.8.4' +__author__ = 'Gunther Cox' +__email__ = 'gunthercx@gmail.com' +__url__ = 'https://github.com/gunthercox/ChatterBot' + +__all__ = ( + 'ChatBot', +) diff --git a/chatter/source/__main__.py b/chatter/source/__main__.py new file mode 100644 index 0000000..a27f483 --- /dev/null +++ b/chatter/source/__main__.py @@ -0,0 +1,23 @@ +import sys + + +if __name__ == '__main__': + import importlib + + if '--version' in sys.argv: + chatterbot = importlib.import_module('chatterbot') + print(chatterbot.__version__) + + if 'list_nltk_data' in sys.argv: + import os + import nltk.data + + data_directories = [] + + # Find each data directory in the NLTK path that has content + for path in nltk.data.path: + if os.path.exists(path): + if os.listdir(path): + data_directories.append(path) + + print(os.linesep.join(data_directories)) diff --git a/chatter/source/__pycache__/__init__.cpython-36.pyc b/chatter/source/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b9aa8f4ffa1604d2d368000b99247712e72ab30a GIT binary patch literal 431 zcmYjMyH3L}6t&azm9}Ew2Rt?sO_0ifDufD^U}FI>SdlC@wh~K;T{#X4nfMTXfQ|3T z%ET`);iRp=md`!D_xhgWayY*x_|&v((Csus`7=%8?nRw*JxNMQ&GcCXFoDp#z` S_;t@tD5e%%ICWgu?cOhc6Mzf= literal 0 HcmV?d00001 diff --git a/chatter/source/__pycache__/adapters.cpython-36.pyc b/chatter/source/__pycache__/adapters.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b4f47b3f2f7afdee9621da93f9358e126da1e40b GIT binary patch literal 2088 zcmb7F-HzKt6!zFo>}0!i=@zLHNQ?l9rl=99T&*g!+f}J5AU9oc5v^o7p0kM;$2K#Q z?Zyfc((YsQ1-Roqcmv;Z#Vc^dIpc}5TSZVJ&9Bd#pYMF<qEQ&XUv2{(qpfA zKlX($I@iv)gBrUcK#c)(QDcDTQ1tNJgAmU>(MJnAb#Nr&QiH5c`W$~pTrEnFNfs-` z&kD)oEaPz_!p$wHVc&=zl(CHGJS(PCkO%m(T-rxiQL z&^0@EwRgp?+?5mg=FYuhxbFxD-Sb9ux6xk*$t>2BLJzvNHw9e>bf4zax`(IljSqDg zMR`1jD3TspkYsP**1m%5tnU8wI+j!QnHuyf`yL$LQv>LN}SPc_1}57ZxK*25mWpVO1z-5@Lk) zvIHZ5-w(Le*}60uFbkdRq2p$6Kx?KucvtTz(2=!vmqIjg2i5)>kQ?7`(U%f)NDn^2 z=@xwh;R*kmA%Q!hO9U1!Vii^o*DCG$10c)9sL(IxWd?J|HHha@7P9)I4dE!~aG5}< zQ{*EeIRP%?RDs~HXOI)Hv8D9fXydb3@f^XL0Dew4iZ`I0EXy>(Vpw7xQiZ4rJZh6W z1ey{k-~zG&i-+TPxGW(-&&YbKkHlIZt>qMb!J3cPbtE%t0aVWBJtC$*6e7X+rWquK7sjx?+GCW_G zN0qhAdFQHWUtm|lT3X~S+nfIl(_fHK5)#1vCN^vv4Sa*1d>=iQ#Adlq(*_!|Gdock-biY2)b~v@r-X!l*xS2j f?gyTvG}YZ=a*kDR?zSnZ!>0{pzM;$+5{BMCYoQ4{ literal 0 HcmV?d00001 diff --git a/chatter/source/__pycache__/chatterbot.cpython-36.pyc b/chatter/source/__pycache__/chatterbot.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b790ba13c4f8ff75d1698cc7572153a9fadb551 GIT binary patch literal 4567 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 literal 0 HcmV?d00001 diff --git a/chatter/source/__pycache__/constants.cpython-36.pyc b/chatter/source/__pycache__/constants.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1dd4ef4684db4a09b967d20ab5194a973f64252 GIT binary patch literal 279 zcmXr!<>lJYFej>#k%8ec0}=r8861GPSOiFJjJ*A+?dmbiaP&$Wu3llIrso#3_wy(hJ(1);jSu2v z@P0kO@4-%-1?_H>Wx-CA1=&s{1rK^6-${8E>}7b#Q#CS4WiRT+0}=3eYfDHh6lCR+ zK`-T^n=Px6Z3c?yM|Zl`ab+Hwq1CZ)5H`4_1`+}HM7}4JAm0%|H_q}P-3sz(I}5f_ ziH{)8?4cK@IZal$G>Gn*=x+ValEmH5UR_^#_(1gf5AUW2H-GWk!}d;;=R%hE%`|`5 zPLsz%W>Fre$#Va&@V7-C(ppnQBk|O72hX2m)p9wet@<&<8C~jN*YR?2b;#bdKA1bQ z>4+URw2v9H9(nIV!Z@%!SThG+<2;DWS+iWm367hmiUsDd0fa$hjSt;w+LHD!yr|z7 z32!=*$k5*$j8#0)O^wF+5$Eb?7x0!5gDkhso;h4Os@3!gKMa$oC&I9(hap6|*QMu1 z7(UvIx}%<{Fyv`F3?&IvPE$i`R~p(~maO#MU*XT;et|FW=W(AE7x`6w?TK~F zHs)YsU*a#o#$FJYU~}{QMM&zUV%i9QAe7M7#wr*{(U&5F>SvH)2FZ0rkoD7Dkp%b{ z=Z7}}>J>>_1gTWh`#Unq1f(0mUecaO6A`qdB*>z}k-3ywMclZI>nFIfPtkDLbk{m^ z4jVZevSW6{-nEh|u9e%nb-Bb~Tg-ajO6>Qnq0@0Vy9O_^YsrU)vqRQ#xic|ioQ!y?^%NvpPe};}wY_}n=IdG) zXX7LUllp0ri7-!#x;V&X6!IvKnvKHA4l^{lEarcn#BI2Zw-VJ#naZL;#tG95<76u> zJT9mU#*QTlr=RxK4)PMVl=EncI_!BX=EUz}D$7$DZHuO>4dW^e@OJkjxt&Sk>x7Du zw#P}Fhv5t8RBqH_zGJfnn`NFe%WT~7WP5bCXKS84m^aF?Tu%OcZ`W!vNyOl$EOu-z z%1&WWU`H6fPE;seV1rB5eN^v>ZD`aw4X>ji$#kp*l8N0KTwXO(17h3C*R$K_?+-SR zT8H5eDg>N3sO~vGgyHS8w$$Cm;nIPLck0kfco?$G zC)bLtSezL7SW?#rWg6M+I9>9T$lBZ*WyTA+LAnNtI;zke!kE8CMAqLVFK`-9k z$%#T@IKt&NsO(1)o*a-P>kHA|A<$vy6A?b_r;-OFnT*duc2zkEL6V!nt~d;C1&jSO zQ$cspETp?NLAQcmE-kjw{ly!>;=MN(n>T``MQlp1Z@2WTVE}z>jXoO4MIYaJXR-Og zn2{?uMbxAHR{-aPA_R;Ab}K38aa##W9@1HrJ60kaYu8GC*jF=GAVB%d+KPC+x;_Wi z&qO`~Y2b5TwBxN990e8QRmt&@n;t=x!a;9QQ!7kb+bqElMVSf|Kf^4o!E=MFV{1D( z$an{GsDwt=X%<0=(ko<3xlMtw?~S^x+# zYOn^}N2*tb#uDUF_ibO*Rg%s*zhDb^VMlhoA z%YzhC+Vk?F7AIUBOsa5$w6h$%i4`(h*YfKWIGD%gT$e4dd3J^Qw#x4DL^MUsVvZ8|-n0L0B2Bag1_U+GF!mrr`c34I9_5K+#Fp0uV_XRF8o-2{d|Nsa!=$@>GuGky&^$RuTXlM3*~9lO!*pEI4QD=dWMnOM;N1nz29I6^~`63YhyW0 zj6PjduaKyMk*F3(RFn>mgk^Ui8YLo?7Do509^x+4N{J-d>un0D*cA65wvj)ecs5Gm zRkFS*U@(-T8-~D~Dk4_V64tHEyppUxYuIL8ZQ15E&{3N#967L}P5TPi)1f-(}vVjm13UK zeWN?-yuT&A@X=T^4QNH(9=vFdsHy`ehZ{KPU<#CS{fPA!sP(zC2j#5)Hf)&HpHB@Fypf{PSDs_}5XHGtn z?6Yj7CB}Rt4>nG>nl*)+RZQ1k$x#;5nUJw64V4^r3C*3xT)Bz~pWw;}a#}S4)IRvx zU<>x(@|dS)MZ-$+`gAE)Qm`ir11s3zSB6{#Qu{I`(1_TlswxVs2J5;K^hTNoW|I*M zXp~uj0PH^ME72wr`~5iI3Eu1=SfrI%)k+uf9~g)7(KGP)Rihb(^j6#!M~5CuRg8v29l5vit_>81L$tMDl6{46)k|4N8xfu#oM4@_~@VN3Nbo>dIJ~|sLo=@EA zS@sDEo=HAQ^EDl(=G&5(_U8%-uY4{EDgmd{{Seb&1axpYLrsGkI+I$pa#)q)t3udK z^!^c-s=)bn-I4UA)x12p43+6BYkiqsze3H|sG+bTo78-ln(tFX3A?hW!Y{R6Qbt`& z8|b4pJC!anz~^vfFQQQex|e)M{Z0FxKkr}jXZ=h5T*Ftu>V*Cg|L@RLYK;D!T6+Gx zvK{p1qP&4+20vCtxHiTh#)HA{m*q8p#29H=X(bAJj-#UXW=2_w{iKMhp7szAsHfRo ztA~mjCF9)bvYvh9GOfhQ&vF-)v>LoE)x$jUyd3p&T^-8_Yk5QG*v{S?x+*>Jj!FH! zQ9VpY9ai|1pXZ3&s84%H&fv^Yr~)7diVdS&#UlAl(9^8Tb=*+KMAu#(cjcRSRX(($b*7s3{UBrIqxdn2xhlCtn(ZjY7jM^i5Jt(BNOV zN~!@3W^F*Nc|)~91ua<4gcCmBd(zsK{l=^!*Yg+Jl-!~3;NKK8r59AZZZ+M)(j`)`_G literal 0 HcmV?d00001 diff --git a/chatter/source/__pycache__/preprocessors.cpython-36.pyc b/chatter/source/__pycache__/preprocessors.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..954526ba6ea33d347a3a74e163407d5e8def4234 GIT binary patch literal 1352 zcmZ`(&2HO95Z*tEv|_oAQxph#=)#8pYGYq=36daa;lOD6;}&&L6ovqT;;v$I{wa1v8tU{6VCnKK@ct%FGI+r8hZ;ig@!fofJ%n2KO_AqL|I8HGN%}I!0(*ZNywyg*0HJQI9dSv zY1CohIGVMAnFCdyNo~0_dDck5Pi?=x+r9`=Cp3*$Bcjww`~7WG8w z`;h0D2j!Am6xvfZjPyNldefU7Z+xo$&>Jtz(rm%b4Q?-{zRp7k86QVcLbvcHPDnzw zi2ev7cR2SD1Lr@%=9cr|@tk%PdV9sah2EXAz%J{^6nZ|qlfZn>j~C2BEaC;8Gn;g` znO=va?V$X+6b9%nawYC`H;C}X@xklY@1^1Lm$o>|N|;iZOFu4mWoWMcQ53Vbsw*kfpJ39ROm5VZy7lrE;Z)>IHENbm!@9A*E-MaxMGMgTc0o2B$rTVp+y85x{w?s_Yhc0X}Hq%q2wVKQdqVapQT-UV_S|a zC%LN^3fB+N56~~rw|*YK_UWLXpfBx=e5>82NBaSyMPscc&1f{g86WQLMZf*{%k+CM(BOb!jpiC3c1OlOH^Wn1XH zE`;6EutW*A5S37ghAppio#hovSRogA$`YQ`VC*2efb)|lDf)wL1l@&x1ikqN1g9&9 zdn;FyCDqP3U3s0ibh&#@DS30ZqhFHmKVJErzw|l+vq2Yh?h9Yt>HL+`1uOUEo$pA8 zzWd2@N=U?@P3lA?C3sP{I&L(auLMUX>@l-{?hS6Gg@kK5Wo4q%46Y@qhFwfcSRh!6 zzMn{aFzD5%wfMK%i)s_{EjB#e61Y89iqH4<%q3|p+OvVY~vGN z=#vY$T5|U2ctEOJZ0s2A|D*ZhFKA=G9|0SSyjt`-3!%S-u~2;&$5m2_IPSf?n#+D4 zrsB01aVpzNk4OFWw_@4v*(G=p)0-xMpxSPo-q`pysObYV4<>gk1-)+!{7Te&;NPEz zN0ZqzQP#7sru~%XoTGjsYgRs-GKKtFEA%O=q4}j!ocQ((>ah2bm?ioqvz&FNe9*70|L~$$=B~V z^-m4`2Jm^VfNmRI$W-%s9XE!k{1@hD4>o`gy6jfWMzbHY=7-ffbq^FK#yC(IJLRJY zP+*zsgSk||W$zZE8oO!-Cn6NPD3G{8T!+!Xy)kO>`2^lK$Pn3$Xh^R)>K2SGGHB8w z^AP@+PXsBUpi-g%8tp;BbUSzHb9Wi&(H1@qjsYJpIeN$OIkJT}sW`;=pApAzz$V*= z@_uE795|qT0EIkH#o7H0Ny%F}*WZCHipF6dC}Uq?xwwiZbrUD{L5#gEl+5z0c)Dfk zsL^SGdIY;0B#`W0qnAj%j~byg$1#^_91o_kG#S_wPH&-Vpf$XPpOuogh4>s1G?Zdd J3qK0?{|4lgqILiP literal 0 HcmV?d00001 diff --git a/chatter/source/__pycache__/trainers.cpython-36.pyc b/chatter/source/__pycache__/trainers.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2c3633dcd30189f03765dae0886a1e46e23a94a1 GIT binary patch literal 12169 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 max_similarity): + max_similarity = similarity + + if max_possible_similarity == 0: + return 0 + + return max_similarity / max_possible_similarity + + +class SentimentComparison(Comparator): + """ + Calculate the similarity of two statements based on the closeness of + the sentiment value calculated for each statement. + """ + + def initialize_nltk_vader_lexicon(self): + """ + Download the NLTK vader lexicon for sentiment analysis + that is required for this algorithm to run. + """ + from .utils import nltk_download_corpus + + nltk_download_corpus('sentiment/vader_lexicon') + + def compare(self, statement, other_statement): + """ + Return the similarity of two statements based on + their calculated sentiment values. + + :return: The percent of similarity between the sentiment value. + :rtype: float + """ + from nltk.sentiment.vader import SentimentIntensityAnalyzer + + sentiment_analyzer = SentimentIntensityAnalyzer() + statement_polarity = sentiment_analyzer.polarity_scores(statement.text.lower()) + statement2_polarity = sentiment_analyzer.polarity_scores(other_statement.text.lower()) + + statement_greatest_polarity = 'neu' + statement_greatest_score = -1 + for polarity in sorted(statement_polarity): + if statement_polarity[polarity] > statement_greatest_score: + statement_greatest_polarity = polarity + statement_greatest_score = statement_polarity[polarity] + + statement2_greatest_polarity = 'neu' + statement2_greatest_score = -1 + for polarity in sorted(statement2_polarity): + if statement2_polarity[polarity] > statement2_greatest_score: + statement2_greatest_polarity = polarity + statement2_greatest_score = statement2_polarity[polarity] + + # Check if the polarity if of a different type + if statement_greatest_polarity != statement2_greatest_polarity: + return 0 + + values = [statement_greatest_score, statement2_greatest_score] + difference = max(values) - min(values) + + return 1.0 - difference + + +class JaccardSimilarity(Comparator): + """ + Calculates the similarity of two statements based on the Jaccard index. + + The Jaccard index is composed of a numerator and denominator. + In the numerator, we count the number of items that are shared between the sets. + In the denominator, we count the total number of items across both sets. + Let's say we define sentences to be equivalent if 50% or more of their tokens are equivalent. + Here are two sample sentences: + + The young cat is hungry. + The cat is very hungry. + + When we parse these sentences to remove stopwords, we end up with the following two sets: + + {young, cat, hungry} + {cat, very, hungry} + + In our example above, our intersection is {cat, hungry}, which has count of two. + The union of the sets is {young, cat, very, hungry}, which has a count of four. + Therefore, our `Jaccard similarity index`_ is two divided by four, or 50%. + Given our similarity threshold above, we would consider this to be a match. + + .. _`Jaccard similarity index`: https://en.wikipedia.org/wiki/Jaccard_index + """ + + SIMILARITY_THRESHOLD = 0.5 + + def initialize_nltk_wordnet(self): + """ + Download the NLTK wordnet corpora that is required for this algorithm + to run only if the corpora has not already been downloaded. + """ + from .utils import nltk_download_corpus + + nltk_download_corpus('corpora/wordnet') + + def compare(self, statement, other_statement): + """ + Return the calculated similarity of two + statements based on the Jaccard index. + """ + from nltk.corpus import wordnet + import nltk + import string + + a = statement.text.lower() + b = other_statement.text.lower() + + # Get default English stopwords and extend with punctuation + stopwords = nltk.corpus.stopwords.words('english') + stopwords.extend(string.punctuation) + stopwords.append('') + lemmatizer = nltk.stem.wordnet.WordNetLemmatizer() + + def get_wordnet_pos(pos_tag): + if pos_tag[1].startswith('J'): + return (pos_tag[0], wordnet.ADJ) + elif pos_tag[1].startswith('V'): + return (pos_tag[0], wordnet.VERB) + elif pos_tag[1].startswith('N'): + return (pos_tag[0], wordnet.NOUN) + elif pos_tag[1].startswith('R'): + return (pos_tag[0], wordnet.ADV) + else: + return (pos_tag[0], wordnet.NOUN) + + ratio = 0 + pos_a = map(get_wordnet_pos, nltk.pos_tag(nltk.tokenize.word_tokenize(a))) + pos_b = map(get_wordnet_pos, nltk.pos_tag(nltk.tokenize.word_tokenize(b))) + lemma_a = [ + lemmatizer.lemmatize( + token.strip(string.punctuation), + pos + ) for token, pos in pos_a if pos == wordnet.NOUN and token.strip( + string.punctuation + ) not in stopwords + ] + lemma_b = [ + lemmatizer.lemmatize( + token.strip(string.punctuation), + pos + ) for token, pos in pos_b if pos == wordnet.NOUN and token.strip( + string.punctuation + ) not in stopwords + ] + + # Calculate Jaccard similarity + try: + numerator = len(set(lemma_a).intersection(lemma_b)) + denominator = float(len(set(lemma_a).union(lemma_b))) + ratio = numerator / denominator + except Exception as e: + print('Error', e) + return ratio >= self.SIMILARITY_THRESHOLD + + +# ---------------------------------------- # + + +levenshtein_distance = LevenshteinDistance() +synset_distance = SynsetDistance() +sentiment_comparison = SentimentComparison() +jaccard_similarity = JaccardSimilarity() diff --git a/chatter/source/constants.py b/chatter/source/constants.py new file mode 100644 index 0000000..3a5ae7d --- /dev/null +++ b/chatter/source/constants.py @@ -0,0 +1,15 @@ +""" +ChatterBot constants +""" + +''' +The maximum length of characters that the text of a statement can contain. +This should be enforced on a per-model basis by the data model for each +storage adapter. +''' +STATEMENT_TEXT_MAX_LENGTH = 400 + +# The maximum length of characters that the name of a tag can contain +TAG_NAME_MAX_LENGTH = 50 + +DEFAULT_DJANGO_APP_NAME = 'django_chatterbot' diff --git a/chatter/source/conversation.py b/chatter/source/conversation.py new file mode 100644 index 0000000..ea674aa --- /dev/null +++ b/chatter/source/conversation.py @@ -0,0 +1,229 @@ +class StatementMixin(object): + """ + This class has shared methods used to + normalize different statement models. + """ + + def get_tags(self): + """ + Return the list of tags for this statement. + """ + return self.tags + + def add_tags(self, tags): + """ + Add a list of strings to the statement as tags. + """ + for tag in tags: + self.tags.append(tag) + + +class Statement(StatementMixin): + """ + A statement represents a single spoken entity, sentence or + phrase that someone can say. + """ + + def __init__(self, text, **kwargs): + import sys + + # Try not to allow non-string types to be passed to statements + try: + text = str(text) + except UnicodeEncodeError: + pass + + # Prefer decoded utf8-strings in Python 2.7 + if sys.version_info[0] < 3: + try: + text = text.decode('utf-8') + except UnicodeEncodeError: + pass + + self.text = text + self.tags = kwargs.pop('tags', []) + self.in_response_to = kwargs.pop('in_response_to', []) + + self.extra_data = kwargs.pop('extra_data', {}) + + # This is the confidence with which the chat bot believes + # this is an accurate response. This value is set when the + # statement is returned by the chat bot. + self.confidence = 0 + + self.storage = None + + def __str__(self): + return self.text + + def __repr__(self): + return '' % (self.text) + + def __hash__(self): + return hash(self.text) + + def __eq__(self, other): + if not other: + return False + + if isinstance(other, Statement): + return self.text == other.text + + return self.text == other + + def save(self): + """ + Save the statement in the database. + """ + self.storage.update(self) + + def add_extra_data(self, key, value): + """ + This method allows additional data to be stored on the statement object. + + Typically this data is something that pertains just to this statement. + For example, a value stored here might be the tagged parts of speech for + each word in the statement text. + + - key = 'pos_tags' + - value = [('Now', 'RB'), ('for', 'IN'), ('something', 'NN'), ('different', 'JJ')] + + :param key: The key to use in the dictionary of extra data. + :type key: str + + :param value: The value to set for the specified key. + """ + self.extra_data[key] = value + + def add_response(self, response): + """ + Add the response to the list of statements that this statement is in response to. + If the response is already in the list, increment the occurrence count of that response. + + :param response: The response to add. + :type response: `Response` + """ + if not isinstance(response, Response): + raise Statement.InvalidTypeException( + 'A {} was received when a {} instance was expected'.format( + type(response), + type(Response('')) + ) + ) + + updated = False + for index in range(0, len(self.in_response_to)): + if response.text == self.in_response_to[index].text: + self.in_response_to[index].occurrence += 1 + updated = True + + if not updated: + self.in_response_to.append(response) + + def remove_response(self, response_text): + """ + Removes a response from the statement's response list based + on the value of the response text. + + :param response_text: The text of the response to be removed. + :type response_text: str + """ + for response in self.in_response_to: + if response_text == response.text: + self.in_response_to.remove(response) + return True + return False + + def get_response_count(self, statement): + """ + Find the number of times that the statement has been used + as a response to the current statement. + + :param statement: The statement object to get the count for. + :type statement: `Statement` + + :returns: Return the number of times the statement has been used as a response. + :rtype: int + """ + for response in self.in_response_to: + if statement.text == response.text: + return response.occurrence + + return 0 + + def serialize(self): + """ + :returns: A dictionary representation of the statement object. + :rtype: dict + """ + data = {} + + data['text'] = self.text + data['in_response_to'] = [] + data['extra_data'] = self.extra_data + + for response in self.in_response_to: + data['in_response_to'].append(response.serialize()) + + return data + + @property + def response_statement_cache(self): + """ + This property is to allow ChatterBot Statement objects to + be swappable with Django Statement models. + """ + return self.in_response_to + + class InvalidTypeException(Exception): + + def __init__(self, value='Received an unexpected value type.'): + self.value = value + + def __str__(self): + return repr(self.value) + + +class Response(object): + """ + A response represents an entity which response to a statement. + """ + + def __init__(self, text, **kwargs): + from datetime import datetime + from dateutil import parser as date_parser + + self.text = text + self.created_at = kwargs.get('created_at', datetime.now()) + self.occurrence = kwargs.get('occurrence', 1) + + if not isinstance(self.created_at, datetime): + self.created_at = date_parser.parse(self.created_at) + + def __str__(self): + return self.text + + def __repr__(self): + return '' % (self.text) + + def __hash__(self): + return hash(self.text) + + def __eq__(self, other): + if not other: + return False + + if isinstance(other, Response): + return self.text == other.text + + return self.text == other + + def serialize(self): + data = {} + + data['text'] = self.text + data['created_at'] = self.created_at.isoformat() + + data['occurrence'] = self.occurrence + + return data diff --git a/chatter/source/corpus.py b/chatter/source/corpus.py new file mode 100644 index 0000000..65da8eb --- /dev/null +++ b/chatter/source/corpus.py @@ -0,0 +1,11 @@ +""" +Seamlessly import the external chatterbot corpus module. +View the corpus on GitHub at https://github.com/gunthercox/chatterbot-corpus +""" + +from chatterbot_corpus import Corpus + + +__all__ = ( + 'Corpus', +) diff --git a/chatter/source/ext/__init__.py b/chatter/source/ext/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chatter/source/ext/__pycache__/__init__.cpython-36.pyc b/chatter/source/ext/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..502c4b066a8f476fff837a4d99a51b86a816c363 GIT binary patch literal 128 zcmXr!<>lJYFei!u2p)q77+?f49Dul(1xTbY1T$zd`mJOr0tq9CU%Jj#F(IkB1u<^< z6}n-@G07Q;B_*jvF~#|%Maii#sTCzL@$s2?nI-Y@dIgoYIBatBQ%ZAE?Lfv912F>t DF?${P literal 0 HcmV?d00001 diff --git a/chatter/source/ext/django_chatterbot/__init__.py b/chatter/source/ext/django_chatterbot/__init__.py new file mode 100644 index 0000000..c683f59 --- /dev/null +++ b/chatter/source/ext/django_chatterbot/__init__.py @@ -0,0 +1,3 @@ +default_app_config = ( + 'chatter.source.ext.django_chatterbot.apps.DjangoChatterBotConfig' +) diff --git a/chatter/source/ext/django_chatterbot/abstract_models.py b/chatter/source/ext/django_chatterbot/abstract_models.py new file mode 100644 index 0000000..4531186 --- /dev/null +++ b/chatter/source/ext/django_chatterbot/abstract_models.py @@ -0,0 +1,261 @@ +from ...conversation import StatementMixin +from ... import constants +from django.db import models +from django.apps import apps +from django.utils import timezone +from django.conf import settings + + +DJANGO_APP_NAME = constants.DEFAULT_DJANGO_APP_NAME +STATEMENT_MODEL = 'Statement' +RESPONSE_MODEL = 'Response' + +if hasattr(settings, 'CHATTERBOT'): + """ + Allow related models to be overridden in the project settings. + Default to the original settings if one is not defined. + """ + DJANGO_APP_NAME = settings.CHATTERBOT.get( + 'django_app_name', + DJANGO_APP_NAME + ) + STATEMENT_MODEL = settings.CHATTERBOT.get( + 'statement_model', + STATEMENT_MODEL + ) + RESPONSE_MODEL = settings.CHATTERBOT.get( + 'response_model', + RESPONSE_MODEL + ) + + +class AbstractBaseStatement(models.Model, StatementMixin): + """ + The abstract base statement allows other models to + be created using the attributes that exist on the + default models. + """ + + text = models.CharField( + unique=True, + blank=False, + null=False, + max_length=constants.STATEMENT_TEXT_MAX_LENGTH + ) + + extra_data = models.CharField( + max_length=500, + blank=True + ) + + # This is the confidence with which the chat bot believes + # this is an accurate response. This value is set when the + # statement is returned by the chat bot. + confidence = 0 + + class Meta: + abstract = True + + def __str__(self): + if len(self.text.strip()) > 60: + return '{}...'.format(self.text[:57]) + elif len(self.text.strip()) > 0: + return self.text + return '' + + def __init__(self, *args, **kwargs): + super(AbstractBaseStatement, self).__init__(*args, **kwargs) + + # Responses to be saved if the statement is updated with the storage adapter + self.response_statement_cache = [] + + @property + def in_response_to(self): + """ + Return the response objects that are for this statement. + """ + ResponseModel = apps.get_model(DJANGO_APP_NAME, RESPONSE_MODEL) + return ResponseModel.objects.filter(statement=self) + + def add_extra_data(self, key, value): + """ + Add extra data to the extra_data field. + """ + import json + + if not self.extra_data: + self.extra_data = '{}' + + extra_data = json.loads(self.extra_data) + extra_data[key] = value + + self.extra_data = json.dumps(extra_data) + + def add_tags(self, tags): + """ + Add a list of strings to the statement as tags. + (Overrides the method from StatementMixin) + """ + for tag in tags: + self.tags.create( + name=tag + ) + + def add_response(self, statement): + """ + Add a response to this statement. + """ + self.response_statement_cache.append(statement) + + def remove_response(self, response_text): + """ + Removes a response from the statement's response list based + on the value of the response text. + + :param response_text: The text of the response to be removed. + :type response_text: str + """ + is_deleted = False + response = self.in_response.filter(response__text=response_text) + + if response.exists(): + is_deleted = True + + return is_deleted + + def get_response_count(self, statement): + """ + Find the number of times that the statement has been used + as a response to the current statement. + + :param statement: The statement object to get the count for. + :type statement: chatterbot.conversation.Statement + + :returns: Return the number of times the statement has been used as a response. + :rtype: int + """ + return self.in_response.filter(response__text=statement.text).count() + + def serialize(self): + """ + :returns: A dictionary representation of the statement object. + :rtype: dict + """ + import json + data = {} + + if not self.extra_data: + self.extra_data = '{}' + + data['text'] = self.text + data['in_response_to'] = [] + data['extra_data'] = json.loads(self.extra_data) + + for response in self.in_response.all(): + data['in_response_to'].append(response.serialize()) + + return data + + +class AbstractBaseResponse(models.Model): + """ + The abstract base response allows other models to + be created using the attributes that exist on the + default models. + """ + + statement = models.ForeignKey( + STATEMENT_MODEL, + related_name='in_response', + on_delete=models.CASCADE + ) + + response = models.ForeignKey( + STATEMENT_MODEL, + related_name='responses', + on_delete=models.CASCADE + ) + + created_at = models.DateTimeField( + default=timezone.now, + help_text='The date and time that this response was created at.' + ) + + class Meta: + abstract = True + + @property + def occurrence(self): + """ + Return a count of the number of times this response has occurred. + """ + ResponseModel = apps.get_model(DJANGO_APP_NAME, RESPONSE_MODEL) + + return ResponseModel.objects.filter( + statement__text=self.statement.text, + response__text=self.response.text + ).count() + + def __str__(self): + statement = self.statement.text + response = self.response.text + return '{} => {}'.format( + statement if len(statement) <= 20 else statement[:17] + '...', + response if len(response) <= 40 else response[:37] + '...' + ) + + def serialize(self): + """ + :returns: A dictionary representation of the statement object. + :rtype: dict + """ + data = {} + + data['text'] = self.response.text + data['created_at'] = self.created_at.isoformat() + data['occurrence'] = self.occurrence + + return data + + +class AbstractBaseConversation(models.Model): + """ + The abstract base conversation allows other models to + be created using the attributes that exist on the + default models. + """ + + responses = models.ManyToManyField( + RESPONSE_MODEL, + related_name='conversations', + help_text='The responses in this conversation.' + ) + + class Meta: + abstract = True + + def __str__(self): + return str(self.id) + + +class AbstractBaseTag(models.Model): + """ + The abstract base tag allows other models to + be created using the attributes that exist on the + default models. + """ + + name = models.SlugField( + max_length=constants.TAG_NAME_MAX_LENGTH + ) + + statements = models.ManyToManyField( + STATEMENT_MODEL, + related_name='tags' + ) + + class Meta: + abstract = True + + def __str__(self): + return self.name diff --git a/chatter/source/ext/django_chatterbot/admin.py b/chatter/source/ext/django_chatterbot/admin.py new file mode 100644 index 0000000..193c264 --- /dev/null +++ b/chatter/source/ext/django_chatterbot/admin.py @@ -0,0 +1,31 @@ +from django.contrib import admin +from .models import ( + Statement, Response, Conversation, Tag +) + + +class StatementAdmin(admin.ModelAdmin): + list_display = ('text', ) + list_filter = ('text', ) + search_fields = ('text', ) + + +class ResponseAdmin(admin.ModelAdmin): + list_display = ('statement', 'response', 'occurrence', ) + search_fields = ['statement__text', 'response__text'] + + +class ConversationAdmin(admin.ModelAdmin): + list_display = ('id', ) + + +class TagAdmin(admin.ModelAdmin): + list_display = ('name', ) + list_filter = ('name', ) + search_fields = ('name', ) + + +admin.site.register(Statement, StatementAdmin) +admin.site.register(Response, ResponseAdmin) +admin.site.register(Conversation, ConversationAdmin) +admin.site.register(Tag, TagAdmin) diff --git a/chatter/source/ext/django_chatterbot/apps.py b/chatter/source/ext/django_chatterbot/apps.py new file mode 100644 index 0000000..b873e3e --- /dev/null +++ b/chatter/source/ext/django_chatterbot/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class DjangoChatterBotConfig(AppConfig): + + name = 'chatter.source.ext.django_chatterbot' + label = 'django_chatterbot' + verbose_name = 'Django ChatterBot' diff --git a/chatter/source/ext/django_chatterbot/factories.py b/chatter/source/ext/django_chatterbot/factories.py new file mode 100644 index 0000000..7367b58 --- /dev/null +++ b/chatter/source/ext/django_chatterbot/factories.py @@ -0,0 +1,42 @@ +""" +These factories are used to generate fake data for testing. +""" +import factory +from . import models +from ... import constants +from factory.django import DjangoModelFactory + + +class StatementFactory(DjangoModelFactory): + + text = factory.Faker( + 'text', + max_nb_chars=constants.STATEMENT_TEXT_MAX_LENGTH + ) + + class Meta: + model = models.Statement + + +class ResponseFactory(DjangoModelFactory): + + statement = factory.SubFactory(StatementFactory) + + response = factory.SubFactory(StatementFactory) + + class Meta: + model = models.Response + + +class ConversationFactory(DjangoModelFactory): + + class Meta: + model = models.Conversation + + +class TagFactory(DjangoModelFactory): + + name = factory.Faker('word') + + class Meta: + model = models.Tag diff --git a/chatter/source/ext/django_chatterbot/management/__init__.py b/chatter/source/ext/django_chatterbot/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chatter/source/ext/django_chatterbot/management/commands/__init__.py b/chatter/source/ext/django_chatterbot/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chatter/source/ext/django_chatterbot/management/commands/train.py b/chatter/source/ext/django_chatterbot/management/commands/train.py new file mode 100644 index 0000000..d4810b8 --- /dev/null +++ b/chatter/source/ext/django_chatterbot/management/commands/train.py @@ -0,0 +1,29 @@ +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + """ + A Django management command for calling a + chat bot's training method. + """ + + help = 'Trains the database used by the chat bot' + can_import_settings = True + + def handle(self, *args, **options): + from ..... import ChatBot + from ... import settings + + chatterbot = ChatBot(**settings.CHATTERBOT) + + chatterbot.train(chatterbot.training_data) + + # Django 1.8 does not define SUCCESS + if hasattr(self.style, 'SUCCESS'): + style = self.style.SUCCESS + else: + style = self.style.NOTICE + + self.stdout.write(style('Starting training...')) + training_class = chatterbot.trainer.__class__.__name__ + self.stdout.write(style('ChatterBot trained using "%s"' % training_class)) diff --git a/chatter/source/ext/django_chatterbot/migrations/0001_initial.py b/chatter/source/ext/django_chatterbot/migrations/0001_initial.py new file mode 100644 index 0000000..9c20907 --- /dev/null +++ b/chatter/source/ext/django_chatterbot/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name='Response', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('occurrence', models.PositiveIntegerField(default=0)), + ], + ), + migrations.CreateModel( + name='Statement', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.CharField(max_length=255, unique=True)), + ], + ), + migrations.AddField( + model_name='response', + name='response', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='django_chatterbot.Statement'), + ), + migrations.AddField( + model_name='response', + name='statement', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='in_response_to', to='django_chatterbot.Statement'), + ), + ] diff --git a/chatter/source/ext/django_chatterbot/migrations/0002_statement_extra_data.py b/chatter/source/ext/django_chatterbot/migrations/0002_statement_extra_data.py new file mode 100644 index 0000000..5ed2f4a --- /dev/null +++ b/chatter/source/ext/django_chatterbot/migrations/0002_statement_extra_data.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-10-30 12:13 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_chatterbot', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='statement', + name='extra_data', + field=models.CharField(default='{}', max_length=500), + preserve_default=False, + ), + ] diff --git a/chatter/source/ext/django_chatterbot/migrations/0003_change_occurrence_default.py b/chatter/source/ext/django_chatterbot/migrations/0003_change_occurrence_default.py new file mode 100644 index 0000000..8da6869 --- /dev/null +++ b/chatter/source/ext/django_chatterbot/migrations/0003_change_occurrence_default.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2016-12-12 00:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_chatterbot', '0002_statement_extra_data'), + ] + + operations = [ + migrations.AlterField( + model_name='response', + name='occurrence', + field=models.PositiveIntegerField(default=1), + ), + ] diff --git a/chatter/source/ext/django_chatterbot/migrations/0004_rename_in_response_to.py b/chatter/source/ext/django_chatterbot/migrations/0004_rename_in_response_to.py new file mode 100644 index 0000000..7860d49 --- /dev/null +++ b/chatter/source/ext/django_chatterbot/migrations/0004_rename_in_response_to.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2016-12-04 23:52 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_chatterbot', '0003_change_occurrence_default'), + ] + + operations = [ + migrations.AlterField( + model_name='response', + name='statement', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='in_response', to='django_chatterbot.Statement'), + ), + migrations.AlterField( + model_name='response', + name='response', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='django_chatterbot.Statement'), + ), + ] diff --git a/chatter/source/ext/django_chatterbot/migrations/0005_statement_created_at.py b/chatter/source/ext/django_chatterbot/migrations/0005_statement_created_at.py new file mode 100644 index 0000000..7b38f00 --- /dev/null +++ b/chatter/source/ext/django_chatterbot/migrations/0005_statement_created_at.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.1 on 2016-12-29 19:20 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_chatterbot', '0004_rename_in_response_to'), + ] + + operations = [ + migrations.AddField( + model_name='statement', + name='created_at', + field=models.DateTimeField( + default=django.utils.timezone.now, + help_text='The date and time that this statement was created at.' + ), + ), + ] diff --git a/chatter/source/ext/django_chatterbot/migrations/0006_create_conversation.py b/chatter/source/ext/django_chatterbot/migrations/0006_create_conversation.py new file mode 100644 index 0000000..1cf95d9 --- /dev/null +++ b/chatter/source/ext/django_chatterbot/migrations/0006_create_conversation.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2017-01-17 07:02 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_chatterbot', '0005_statement_created_at'), + ] + + operations = [ + migrations.CreateModel( + name='Conversation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.AlterField( + model_name='statement', + name='created_at', + field=models.DateTimeField(default=django.utils.timezone.now, help_text='The date and time that this statement was created at.'), + ), + migrations.AddField( + model_name='conversation', + name='statements', + field=models.ManyToManyField(help_text='The statements in this conversation.', related_name='conversation', to='django_chatterbot.Statement'), + ), + ] diff --git a/chatter/source/ext/django_chatterbot/migrations/0007_response_created_at.py b/chatter/source/ext/django_chatterbot/migrations/0007_response_created_at.py new file mode 100644 index 0000000..1a0b5ac --- /dev/null +++ b/chatter/source/ext/django_chatterbot/migrations/0007_response_created_at.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-07-18 00:16 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_chatterbot', '0006_create_conversation'), + ] + + operations = [ + migrations.AddField( + model_name='response', + name='created_at', + field=models.DateTimeField( + default=django.utils.timezone.now, + help_text='The date and time that this response was created at.' + ), + ), + ] diff --git a/chatter/source/ext/django_chatterbot/migrations/0008_update_conversations.py b/chatter/source/ext/django_chatterbot/migrations/0008_update_conversations.py new file mode 100644 index 0000000..f3bd720 --- /dev/null +++ b/chatter/source/ext/django_chatterbot/migrations/0008_update_conversations.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-07-18 11:25 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_chatterbot', '0007_response_created_at'), + ] + + operations = [ + migrations.RemoveField( + model_name='conversation', + name='statements', + ), + migrations.RemoveField( + model_name='response', + name='occurrence', + ), + migrations.RemoveField( + model_name='statement', + name='created_at', + ), + migrations.AddField( + model_name='conversation', + name='responses', + field=models.ManyToManyField(help_text='The responses in this conversation.', related_name='conversations', to='django_chatterbot.Response'), + ), + ] diff --git a/chatter/source/ext/django_chatterbot/migrations/0009_tags.py b/chatter/source/ext/django_chatterbot/migrations/0009_tags.py new file mode 100644 index 0000000..ee71713 --- /dev/null +++ b/chatter/source/ext/django_chatterbot/migrations/0009_tags.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11a1 on 2017-07-07 00:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_chatterbot', '0008_update_conversations'), + ] + + operations = [ + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.SlugField()), + ], + options={ + 'abstract': False, + }, + ), + migrations.AlterField( + model_name='statement', + name='text', + field=models.CharField(max_length=255, unique=True), + ), + migrations.AddField( + model_name='tag', + name='statements', + field=models.ManyToManyField(related_name='tags', to='django_chatterbot.Statement'), + ), + ] diff --git a/chatter/source/ext/django_chatterbot/migrations/0010_statement_text.py b/chatter/source/ext/django_chatterbot/migrations/0010_statement_text.py new file mode 100644 index 0000000..84940a7 --- /dev/null +++ b/chatter/source/ext/django_chatterbot/migrations/0010_statement_text.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-08-16 00:56 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_chatterbot', '0009_tags'), + ] + + operations = [ + migrations.AlterField( + model_name='statement', + name='text', + field=models.CharField(max_length=400, unique=True), + ), + ] diff --git a/chatter/source/ext/django_chatterbot/migrations/0011_blank_extra_data.py b/chatter/source/ext/django_chatterbot/migrations/0011_blank_extra_data.py new file mode 100644 index 0000000..4f7b327 --- /dev/null +++ b/chatter/source/ext/django_chatterbot/migrations/0011_blank_extra_data.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-08-20 13:55 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_chatterbot', '0010_statement_text'), + ] + + operations = [ + migrations.AlterField( + model_name='statement', + name='extra_data', + field=models.CharField(blank=True, max_length=500), + ), + ] diff --git a/chatter/source/ext/django_chatterbot/migrations/__init__.py b/chatter/source/ext/django_chatterbot/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chatter/source/ext/django_chatterbot/models.py b/chatter/source/ext/django_chatterbot/models.py new file mode 100644 index 0000000..ac51c06 --- /dev/null +++ b/chatter/source/ext/django_chatterbot/models.py @@ -0,0 +1,34 @@ +from .abstract_models import ( + AbstractBaseConversation, AbstractBaseResponse, + AbstractBaseStatement, AbstractBaseTag +) + + +class Statement(AbstractBaseStatement): + """ + A statement represents a single spoken entity, sentence or + phrase that someone can say. + """ + pass + + +class Response(AbstractBaseResponse): + """ + A connection between a statement and anther statement + that response to it. + """ + pass + + +class Conversation(AbstractBaseConversation): + """ + A sequence of statements representing a conversation. + """ + pass + + +class Tag(AbstractBaseTag): + """ + A label that categorizes a statement. + """ + pass diff --git a/chatter/source/ext/django_chatterbot/settings.py b/chatter/source/ext/django_chatterbot/settings.py new file mode 100644 index 0000000..802b77d --- /dev/null +++ b/chatter/source/ext/django_chatterbot/settings.py @@ -0,0 +1,19 @@ +""" +Default ChatterBot settings for Django. +""" +from django.conf import settings +from ... import constants + + +CHATTERBOT_SETTINGS = getattr(settings, 'CHATTERBOT', {}) + +CHATTERBOT_DEFAULTS = { + 'name': 'ChatterBot', + 'storage_adapter': 'chatter.source.storage.DjangoStorageAdapter', + 'input_adapter': 'chatter.source.input.VariableInputTypeAdapter', + 'output_adapter': 'chatter.source.output.OutputAdapter', + 'django_app_name': constants.DEFAULT_DJANGO_APP_NAME +} + +CHATTERBOT = CHATTERBOT_DEFAULTS.copy() +CHATTERBOT.update(CHATTERBOT_SETTINGS) diff --git a/chatter/source/ext/django_chatterbot/urls.py b/chatter/source/ext/django_chatterbot/urls.py new file mode 100644 index 0000000..079005d --- /dev/null +++ b/chatter/source/ext/django_chatterbot/urls.py @@ -0,0 +1,11 @@ +from django.conf.urls import url +from .views import ChatterBotView + + +urlpatterns = [ + url( + r'^$', + ChatterBotView.as_view(), + name='chatterbot', + ), +] diff --git a/chatter/source/ext/django_chatterbot/views.py b/chatter/source/ext/django_chatterbot/views.py new file mode 100644 index 0000000..d73408e --- /dev/null +++ b/chatter/source/ext/django_chatterbot/views.py @@ -0,0 +1,118 @@ +import json +from django.views.generic import View +from django.http import JsonResponse +from ... import ChatBot +from . import settings + + +class ChatterBotViewMixin(object): + """ + Subclass this mixin for access to the 'chatterbot' attribute. + """ + + chatterbot = ChatBot(**settings.CHATTERBOT) + + def validate(self, data): + """ + Validate the data recieved from the client. + + * The data should contain a text attribute. + """ + from django.core.exceptions import ValidationError + + if 'text' not in data: + raise ValidationError('The attribute "text" is required.') + + def get_conversation(self, request): + """ + Return the conversation for the session if one exists. + Create a new conversation if one does not exist. + """ + from .models import Conversation, Response + + class Obj(object): + def __init__(self): + self.id = None + self.statements = [] + + conversation = Obj() + + conversation.id = request.session.get('conversation_id', 0) + existing_conversation = False + try: + Conversation.objects.get(id=conversation.id) + existing_conversation = True + + except Conversation.DoesNotExist: + conversation_id = self.chatterbot.storage.create_conversation() + request.session['conversation_id'] = conversation_id + conversation.id = conversation_id + + if existing_conversation: + responses = Response.objects.filter( + conversations__id=conversation.id + ) + + for response in responses: + conversation.statements.append(response.statement.serialize()) + conversation.statements.append(response.response.serialize()) + + return conversation + + +class ChatterBotView(ChatterBotViewMixin, View): + """ + Provide an API endpoint to interact with ChatterBot. + """ + + def post(self, request, *args, **kwargs): + """ + Return a response to the statement in the posted data. + """ + input_data = json.loads(request.read().decode('utf-8')) + + self.validate(input_data) + + conversation = self.get_conversation(request) + + response = self.chatterbot.get_response(input_data, conversation.id) + response_data = response.serialize() + + return JsonResponse(response_data, status=200) + + def get(self, request, *args, **kwargs): + """ + Return data corresponding to the current conversation. + """ + conversation = self.get_conversation(request) + + data = { + 'detail': 'You should make a POST request to this endpoint.', + 'name': self.chatterbot.name, + 'conversation': conversation.statements + } + + # Return a method not allowed response + return JsonResponse(data, status=405) + + def patch(self, request, *args, **kwargs): + """ + The patch method is not allowed for this endpoint. + """ + data = { + 'detail': 'You should make a POST request to this endpoint.' + } + + # Return a method not allowed response + return JsonResponse(data, status=405) + + def delete(self, request, *args, **kwargs): + """ + The delete method is not allowed for this endpoint. + """ + data = { + 'detail': 'You should make a POST request to this endpoint.' + } + + # Return a method not allowed response + return JsonResponse(data, status=405) diff --git a/chatter/source/ext/sqlalchemy_app/__init__.py b/chatter/source/ext/sqlalchemy_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chatter/source/ext/sqlalchemy_app/__pycache__/__init__.cpython-36.pyc b/chatter/source/ext/sqlalchemy_app/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2faf4fc963ccf25e6957e68aad72eb4bdfc8813e GIT binary patch literal 143 zcmXr!<>lJYFei!u2p)q77+?f49Dul(1xTbY1T$zd`mJOr0tq9CUpCHGF(IkB1u<^< z6}n-@G07Q;B_*jvF~#|%Maii#sTCzL#f3SEImsERxs~yW1qCtj@tJvHTytG*dkux=6xDAX;tQm)o3wZa+h@NxyEX2?zP6|`0|D6uJ8t5 zVf9PhUFAA!@YUlrZm=ddd5N_y^a~TCwi?aT=)!XEfWiV>1cgPu3JP~IUSi7_FY{Ht zgdO)Vv%*#}v#NI72j#UZ%6Am4ySGw)fOGfQeVn_m&OKxg_O#B!H>A2wS(bYKLBwTw zD~U$&Kvq5)WW3LXw730?_rjRV+OtIPus?XgPi5`*Vem5Iz0)D@=&~+&@4!#_8AR@7=ITsy4W_#$x0u1q*V={dmUx+0n1z4o(sXTBhJq@xx(goBuW{e;FKP~c zn*+xm^@$B<4$0v_$~1Kj6XEQgM4M4?#N$&Zreo>4Iug*8=|?SP$P9@9uNLZBpV`yHh31ALjD2zHy@U@n{85&`NpG|GeJ1SsO<6Rg}O07c8C;a0d!al*;~6o4V(rOGW{ zgLrdTsS*rgSDn>aKIL=j;mXQpDSi~Y-F)F~_HE_|D&r2S#!#|O;ccmfJ3iZ)&- zZn!>Kl(oV_Dcrl7@*o*7IJm4F_`yrT52gJvPlrH8E?dRkoG$?u?ed1na0L8>-KSB4 z!fA_`lVO?8jMoY)wgFV+J>y^9#0uYu!ZdRT4g7wNM%huA-gF|uhJq0>A@A8EMieT2 zF!`)@uAk}0+TV>6YcIzN#EFAd$Y`v8AWbls(i*we*R>CcdyvG#jgK%^Xi(P1?_4Xb z&oc<#Pocde@S}7CoyigC!lE^5Qyv}Opf2sn;hhO>&6+mIWVsNG+_;RQH266>q(RCA z&UL)lIy2EX&JA5V(}BPyxw6u)Mf$}I9Q)EdH($MtetBE_?C!aBW*%40j5F)lM6p@r z(zpPvlZ`)VXC^FScLhNVH1(al`5~I(KL59(#UmUS?@^`V_d1B*$J`%KCMte3tK!1T zZq>6P)^SwU0Pe!~n4kHusg+?UYe^7{grH#SRAn8W`~?@Of_@^CLpWZh@1m=KS#rBf0=LgTiIYyR%wmS$Mt@oK?vedbiR)A}zc+62X1P@8&6+6whZWbreqKA`HC zR9#!=+C=05v^S+kig31#%NcrEw@q91(3X`_XRwzX_9-3v9Mx^uOOFRo)>as-^XhT! zLSq(ib`G<3LSytB=&PrOH(3P_j^Va=n_Xctl2OZ@XAL-PQyFx;t#D*wi}xJZJ@dms z3K-7KN&{LmjonP@>EOjoYP<7Q#Q zb@IL3zeust^rPrm#}t&N$qLe*z-L@mMnDCCMYpr?KiiHVK+xBKGzZC-vW7&1q(CmJ zG6ve&%PCU<3w^3H7ck))_QW*Rhr7$jeo~_r-y0M^)tFjB)<*rQ!(e%qBTQe(2 zDP|^aW);Pw{9RC;bqsO^X5kT5h~<`8g|JB0x2D2(eCO(ooo1Rpqu+GGZ+SZ(PM@6? z<=Y=o@iuPzSJD)UHeQ~-()APCm?n%@RJ?=D;s>a@o${^5R_6IW?Yb@Ywz2RR6qQ|5 z?B$UsXujKd3r}{D(+Wc6>l{)jk0Fl`6zy<}V&{4yVnKXoCb6y@%)L9c6Yjh|^`Vm| ztCWGtDkO!cA+J$+njn~wr3~LtsZiP5Pcb3pX||jwj8r+QwCLMI+A+_30&MX;+D3`n x6fE!1kVL#qC&*^ZUaXm zR33vD;KuX#2&a7oPK?vC6fx0^M;`n0_s!&VI1Imk{+j&22>nER7C?RuZk|C9NZb&~6dKyMx{*7p^bkuCoBhyvQxT|IqV7+-6 zKA(V_F$94HDbOul(0?XQ*wPKr$7_5Qh+uW z?Lt-2thI9YW@ObpjhO^e^8SwDOW+*Yg)IFkSR=tLpK&9%Og*rM=A-GI=eg=Nc7F0a z?m22&;|`Znn_N{auja~yxydVId6h{hTkQ^@tI*jvaDkD<-1U~cXr$|loEI{)`T!8W zA$7^{|^o_bX`EZ(M3vR8{sZkB6O684B&2_^=0~(NkbbHe{NJf7ZjXO2k|92g1cV-XE U_egs{H+Z$Bb>^pH{Dl$y4OXYpX#fBK literal 0 HcmV?d00001 diff --git a/chatter/source/ext/sqlalchemy_app/models.py b/chatter/source/ext/sqlalchemy_app/models.py new file mode 100644 index 0000000..9f1b0d3 --- /dev/null +++ b/chatter/source/ext/sqlalchemy_app/models.py @@ -0,0 +1,132 @@ +from sqlalchemy import Table, Column, Integer, DateTime, ForeignKey, PickleType +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from sqlalchemy.ext.declarative import declared_attr, declarative_base + +from ...constants import TAG_NAME_MAX_LENGTH, STATEMENT_TEXT_MAX_LENGTH +from .types import UnicodeString +from ...conversation import StatementMixin + + +class ModelBase(object): + """ + An augmented base class for SqlAlchemy models. + """ + + @declared_attr + def __tablename__(cls): + """ + Return the lowercase class name as the name of the table. + """ + return cls.__name__.lower() + + id = Column( + Integer, + primary_key=True, + autoincrement=True + ) + + +Base = declarative_base(cls=ModelBase) + + +tag_association_table = Table( + 'tag_association', + Base.metadata, + Column('tag_id', Integer, ForeignKey('tag.id')), + Column('statement_id', Integer, ForeignKey('statement.id')) +) + + +class Tag(Base): + """ + A tag that describes a statement. + """ + + name = Column(UnicodeString(TAG_NAME_MAX_LENGTH)) + + +class Statement(Base, StatementMixin): + """ + A Statement represents a sentence or phrase. + """ + + text = Column(UnicodeString(STATEMENT_TEXT_MAX_LENGTH), unique=True) + + tags = relationship( + 'Tag', + secondary=lambda: tag_association_table, + backref='statements' + ) + + extra_data = Column(PickleType) + + in_response_to = relationship( + 'Response', + back_populates='statement_table' + ) + + def get_tags(self): + """ + Return a list of tags for this statement. + """ + return [tag.name for tag in self.tags] + + def get_statement(self): + from ...conversation import Statement as StatementObject + from ...conversation import Response as ResponseObject + + statement = StatementObject( + self.text, + tags=[tag.name for tag in self.tags], + extra_data=self.extra_data + ) + for response in self.in_response_to: + statement.add_response( + ResponseObject(text=response.text, occurrence=response.occurrence) + ) + return statement + + +class Response(Base): + """ + Response, contains responses related to a given statement. + """ + + text = Column(UnicodeString(STATEMENT_TEXT_MAX_LENGTH)) + + created_at = Column( + DateTime(timezone=True), + server_default=func.now() + ) + + occurrence = Column(Integer, default=1) + + statement_text = Column(UnicodeString(STATEMENT_TEXT_MAX_LENGTH), ForeignKey('statement.text')) + + statement_table = relationship( + 'Statement', + back_populates='in_response_to', + cascade='all', + uselist=False + ) + + +conversation_association_table = Table( + 'conversation_association', + Base.metadata, + Column('conversation_id', Integer, ForeignKey('conversation.id')), + Column('statement_id', Integer, ForeignKey('statement.id')) +) + + +class Conversation(Base): + """ + A conversation. + """ + + statements = relationship( + 'Statement', + secondary=lambda: conversation_association_table, + backref='conversations' + ) diff --git a/chatter/source/ext/sqlalchemy_app/types.py b/chatter/source/ext/sqlalchemy_app/types.py new file mode 100644 index 0000000..b48f4f6 --- /dev/null +++ b/chatter/source/ext/sqlalchemy_app/types.py @@ -0,0 +1,21 @@ +from sqlalchemy.types import TypeDecorator, Unicode + + +class UnicodeString(TypeDecorator): + """ + Type for unicode strings. + """ + + impl = Unicode + + def process_bind_param(self, value, dialect): + """ + Coerce Python bytestrings to unicode before + saving them to the database. + """ + import sys + + if sys.version_info[0] < 3: + if isinstance(value, str): + value = value.decode('utf-8') + return value diff --git a/chatter/source/filters.py b/chatter/source/filters.py new file mode 100644 index 0000000..9a07a09 --- /dev/null +++ b/chatter/source/filters.py @@ -0,0 +1,47 @@ +""" +Filters set the base query that gets passed to the storage adapter. +""" + + +class Filter(object): + """ + A base filter object from which all other + filters should be subclassed. + """ + + def filter_selection(self, chatterbot, conversation_id): + """ + Because this is the base filter class, this method just + returns the storage adapter's base query. Other filters + are expected to override this method. + """ + return chatterbot.storage.base_query + + +class RepetitiveResponseFilter(Filter): + """ + A filter that eliminates possibly repetitive responses to prevent + a chat bot from repeating statements that it has recently said. + """ + + def filter_selection(self, chatterbot, conversation_id): + + text_of_recent_responses = [] + + # TODO: Add a larger quantity of response history + latest_response = chatterbot.storage.get_latest_response(conversation_id) + if latest_response: + text_of_recent_responses.append(latest_response.text) + + # Return the query with no changes if there are no statements to exclude + if not text_of_recent_responses: + return super(RepetitiveResponseFilter, self).filter_selection( + chatterbot, + conversation_id + ) + + query = chatterbot.storage.base_query.statement_text_not_in( + text_of_recent_responses + ) + + return query diff --git a/chatter/source/input/__init__.py b/chatter/source/input/__init__.py new file mode 100644 index 0000000..34d9568 --- /dev/null +++ b/chatter/source/input/__init__.py @@ -0,0 +1,18 @@ +from .input_adapter import InputAdapter +from .microsoft import Microsoft +from .gitter import Gitter +from .hipchat import HipChat +from .mailgun import Mailgun +from .terminal import TerminalAdapter +from .variable_input_type_adapter import VariableInputTypeAdapter + + +__all__ = ( + 'InputAdapter', + 'Microsoft', + 'Gitter', + 'HipChat', + 'Mailgun', + 'TerminalAdapter', + 'VariableInputTypeAdapter', +) diff --git a/chatter/source/input/__pycache__/__init__.cpython-36.pyc b/chatter/source/input/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..360dbeb75312529fb444a290e94c9d0785503107 GIT binary patch literal 565 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 Z9M7OAY2Uc-6{vndZq_nHM`8R~{{y(ly~h9m literal 0 HcmV?d00001 diff --git a/chatter/source/input/__pycache__/hipchat.cpython-36.pyc b/chatter/source/input/__pycache__/hipchat.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9ce7312ccceaec0f249caf4785ee1a9c04a7c46f GIT binary patch literal 2838 zcmai0TaOz_6|U-wyX|Su%+6-XX1O)6fQ*!}AkqqjTW;(wLW+QlK(Yq4nyzxDownPl zDraUK`^k~^iTwqDc;y%HU+R^o`3tOKiaH;bK-6wUAj=8maP1V=mN{l=CazXe7Uy%@^jr$mdYF@|wx+ z6O$V`l`~V2p>6Y9K|g>X*@z1^@&q6GU$8M3p73AAqd){Ae8ong=sU;ws3W?fhnYz1 zi3I=g7c5{Sa&zBCpO^E8XSrEUdnwxeSz3}iY2Nsg8oW#Ms;bX*nx~`*FCWwfYTD$p zLZ+rppGxCaH%l?!xUSQ&s;5}i?vbjxo*p~Rf(}oOpr=#qq3bU(7-sm2ZM=y$W-I%g znjAt3H+=1{e1U8G3*Kb!VeZJScwaEO$j3H0E9V6co}Ja&*c;oYyeOpBnW>-2*=rp- zqN2l%dRkZ63#rQS<;k-9K;}xS^yPYan9t``S>&dyXTyoEX9Lgn?=Q?*t;(gFT_z9f znZe!PKDnIB!3_{*F_%ikFl*y1D`%z2GV23&)y16Z(Le}VUmIm3^7c=)u?Iqq^F?Jc z4NtM14Hk=1*k1dXL5`JtzL45z8$D^2qOEqP@!odEwVLL}MrSe?QfYgDot+nDUcrdA zM@km(H=Dvl{#4T3Ah3a!)!25Po#*PQRv(amH}dNT%^HIUiEfq^RSr0_f3SZaKfHT- zBB%4yPwR`@zrS-z#{uh3b-hr9JasHPZLi^YeyR3hxsR?t#K8EGmvH(WdNJ=|Oni^` zg5_b$!s9J~Z*Y)!_>gSe#Q+Xa=K?YHXK&4ow_$7V;u|Y2{9Ei1`{NNf<4uS+!7a7{ zN1vyEU@H%N377XA!6}%2C(T05C~m4!o3tJynq)c!i_!+Kw8bk0UQJxkva4Ui1z@r_ z%`Y+~H7-!w5b0y8tyc;F`{9dHp5v9noEVp#VS`#-Zb=}G^>YDL^>x^?fyhm6qphug zY^iVIO*4F(q_1N54!R~ZSrG7m$9~7#2b{~BTj<`pcSWcb6u#6#^c{>pz=Ploj4QtK zHa;L@69*dp0|#ud3J`!4X8!VI2iuu3bA5L>JU>4_-rjOiPluu|^e~^7FMc$fN`#9T zz6*w(spZEn*F)$NMni}#OuqoKw_7xR_HsSo>La*2=%`!xYeMQKNe490D;u~#4I4NQ z9OR|QYL(6MsZ<|BWwea}+C$Zs*+NwmU3y;6w0s-w?_ zyvEALqr!&Uh?k%2ZsRwqrkc{0;Qs^ur=z1bIOIWZ zi1Gwdw0)o8uTZJo1+*-^K`K3YVN{+`&S)awYnW978h%Iv6&giBuLy0sk3^HbUNVT9 zAnEQt1MJ=m*{bz}N=36-wo@1#AoX7_9*#6NFl6=3G-dnq1|yUoe5U*s4rq!#BrIh6 z*bjJz6K4{ygMP#h5mX6S6r&C-KiIP94I3aWF=o)C(n{fs(%Q?wOtGj)PO|LzBCpyv zQI;VOWtl5FE@?W-H^}@1c5J^bWreIN=HaFeP*Z$YK$wxr0UXtanIadJoq+(19i|1R zGIy}%^|c(SSd_(FbPEWj^V_}^rZ0I*n*9YR!QUt)Rnxabi?nMAQ$1~iqht5~1F1Ry z`~B?ocKc9d!}hK`dHP|i9RnwD`*vBXUEI5r(fszC!_RhYsAMd(jtcvAcTLwSM8@|n zcVKhH?Sh^Pcshy-db*%EdM0?{eZ^BBbUc6_5B!NgATvC}*x4(CU{Ebjres1?YwqW* z_>rlfZ_F+Jip2-w$$0k)@op%@Nuo`X_x|p{F(=6Uve#?@AkVrOKGG#E27P ztFA*G35qsSv7WG`o8-J%QB??4H7BmQBsU6Z(FL^d*5anivtkBH;hJdgtTb%tcN2_y z9Rwl`Z^K*n2;RT}XyZr!@Aopi)eLMcigK2TD6;h^$|N79PH#lf#}P~CmTDAn8AnmU zg1QamE&+7osvJt?=ErRvfT$7T)q>)z)vUPW-4VP~kX*Tuw6(=ctK8SC@cUO%TYFxD Odg)$V?R*=!;r{`@n`6-c literal 0 HcmV?d00001 diff --git a/chatter/source/input/__pycache__/mailgun.cpython-36.pyc b/chatter/source/input/__pycache__/mailgun.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ca78aff4162f3bccd6985dd42cf155407d25021 GIT binary patch literal 1965 zcmZ`)TaOzx6t>5nTbgZ4FQVmA7I6`g=<-m65JCvFs1*|IOHnJ07LA6nlTJF9)Sk56 zC6SO)sZZq>0Pp+){)=CE>R;fA?>NbJDZ-KMqjPM}cfNDUjh&srZ-4%t{Twm&7dtg1 z~nz(fvn-nOFob&GWJ@tj+U3 z*x!5mpB;X$o7v&b?(y|IHxA2tMUTiG+HP)2edzf++<@Ji9hm_>I>fXjFn&P{IsJ~$ zZ|U7)JYksFCHVW8o?(SJ*KEmqKIPM31-`*AIK9G_0eF;3Ov446Mk}r)_IMmDf@!j% z^bAz=t5ESX0fv(ULqRn3{&D)#@+18a31*!D2TmMmI}$GZQBSHiOAA|qkgA=yu&$b_ zAAyx(-rsZ4tT07mN2yDUelXV-4B6fBU#^MO-1eQ(Dqmy245K}dh4Cb~#3dhK zk9fe3-`djqKQ~X<;7X!L_?+7~otUxDXZJSQiuUVgiolriR_}V+qIshk@Q3mt9I74 zRyzq+3n%IJ%XC~Or?y0RR`8~MhdiKE0~w&<0#@YNo2QBR&z+LWnFb{V(Xfwc-@vj- zI!Yx_0hbvH94+P<^)*z^hKy74##B=3QDSpAG&p*eG6yE4AB3ok@y;@!r0N z6E5C>c9LFvIfOei)0OZY4GInSV_Y~1-7ugm9t6jiw-o&A>}WuJjanu8+91!HPR(nQ zhk5>BUep^)oad@5^V~NepKacywKIDJaA(ujN?q4DcM=^0^cC0*Ri5vo#I{G52pJ61 zOCR&kYb53W<3gi(`h8$qmb{eW;Bs`U-hY#QAG4zO|NojVGV4J^6x~ Ml+T5s&)}i>2Q(7iUjP6A literal 0 HcmV?d00001 diff --git a/chatter/source/input/__pycache__/microsoft.cpython-36.pyc b/chatter/source/input/__pycache__/microsoft.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..58f5b4e365c6ff9dccb0e43c356638796ffa0f29 GIT binary patch literal 3569 zcmai0-ICl!6>j~_Xl7^E>m|hAI6q1pn~Y(1LJUPPRLaJ|;0s(SyhSM0R;grl@2+Mw z(zv_jU5_<4jH_~$JOOae6YvT=0T;UE%CEo`-)YU?ngle}J*`%s?$h5n=R4>2=4SUV zfB$Rv;TB{6Vr!3!@m)0aIXcd`m9dc(S{9A%%pN(RgR#Ti%pG~5hq1?7S!>h|+ZOu` z<8AK0WZW0t%n5zodCdCVSJ;X5t$Mr6)1=@c%2F+4oGHwCDidP-%4$~h9_Hgx-{bKZ z3trjHWaqJtwHS$9C$!PxAw}s?U!W5#v^WcGZiUWQ?8xFacV7CT%U$lhWFb`KZTz=5 zRKkCoclieXeJI@Hn^1UDbf9qeE9SCr14?hzo!_O2EL3r%C;#jQc<$vvO6mo1qhFv; zV!UKoai)SexQ{nz{i{M_nbL8dh(H&?vCu}k5ygCS6%`!GVg#|}DFI2AD$S3BTc4*= zBsxoT5hPiPn+IcA=preyTl;3W6RpSU&S1ccMD0H*^pT85;;fLT`$;hxh_1EkQCZP?>}k>@Z(A%oz>bg~_f$61=hIxX_SP!;*a zEA`Rdr+v5HyjSXzLZ%b*anggJhS~NGo{vS{x|bwktm}4i5=$j?xRn(7Ga;2({E*k4 zIlF%u(NK+V?~m`_ zIXn=f@!^BweD4pp4}m3x+o38-nTSIZUWcPa$nKAy%gZ)0l^4^1qP|oCabJMIPQ^39`%b;l%mZF7>hJyb;NXkLX5p_Frj%E(L*J3t z@JC*t#xSp_T@sM*VzTMpqtD-up2b-Tu7C^9uPKA&k8m|4?nK~GI!xnGwjR= zqIMJ3ldCT%WTyint93u1{NU232Yp*!g$8vCQIj4$pImD|m73UcT?Ap;?>62>*csoh z-7)r9t+yG6zfsVnS zwk#LEM4r{PdT5ikS2)8SuLaQ(>;{0noMq+BHCXl+@-xgMa5h`)fdV z=8WtNyWm#Cxw}lB>kY84zstbG9`W!lt32Mh%64H4s(<+*;nEZfQdKL%h}w4AyV0%qV|Ei^zhtORN(5AQT;q)bVQ;t*?LzBhoZ# zG>zuUeFoV%gAi-DRu{dov;#{!UE8;bKenyO`zwoo$6iZ9p}2C;jM+mBI3kI&sdIj- zvehRrI)mYy%8?&c4!2>vk2sQVj8fsFq ziLwWB4@Vpvqr=m?Tf17E>&e@wDH52HOjIf%4NaLki=wC{e~iVFw3Kb?R+t-B@)}m^ z5ZM@^a3dFI3)JMtB=QM$7bB6SfR+S49L2Mqk2}$=>b#U+Oa2BR+wIp zOGsq#5KZ4jGiF+^1Wgsl)~i6mWAe^w({CZ&NmH){c$P#Q$T(Gk2WKZD56WDek5Lui zJumV-RU*4970_0utRl&Vf(_%NHk3sBx|BIY(maUq)=+s<%#py-HRv+bjB8M8uBZ-b z;?sIW6*E}76o@^_3i7AaU4+;qz5?@#_F>lK=Ks9!D%~y0LHfU-f+qHcM*%&kVArk` zV`+}=Ta6kQHK^MWl8}rdxu(U9|EGnKc+_uE<&&eFMdj0t5R$w^$r9DQD0*7P+58Jm zK<$eYD9g*AQMb~;j3_J0574D(iip;CUE6oe92n+P`u=n3?ojs&>da=85DdXL5%3ed z)}5s*81Z^@UYaB#%b>*aXyEYW|DZK?yV=dCKhSu)uGMEQ~~}k8@xUv8=A!513BU6{09@nnO*<@ literal 0 HcmV?d00001 diff --git a/chatter/source/input/__pycache__/terminal.cpython-36.pyc b/chatter/source/input/__pycache__/terminal.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ed2c5bda1bd9f62964425b486997a2b74b624e07 GIT binary patch literal 820 zcmZuv!H&}~5Ve!EO{;d{$OS1k)C0?5Z!3g$Sp?#;NbHJ(70I$ASuGA zBrK7XrReyUuuNv)V$|QcRL<0m&^w_un-vhJg|AJkRUrobB3z452(9gnFV3I_Pfu+q zf{pOX8iRd+H$XbuU0uT)>K*yvfWpo&aGYrH6)b-MDUc2MoCf+S-B7Sa*T?Y!I(e;x zj9i^p?tw32&&A4F^B)DguuR+pDBT#+GV3$1^s3KXA;o+O3vKo3Uz3p73%7`uhw9|g<7p=3E@_wLM0Efr6NH(VH`~M8}@GtB+ zj6LVP5k_&|&p0<$b{gwB=kGe9cPA6hrL8!3IHBlrFQK5zAz`mkInS~r?wl{D$hzu6 z=YZAWqPKCuwI61NgnJ@58HY-J{&+C?1V5oi*qp{E{RL|> B)an2L literal 0 HcmV?d00001 diff --git a/chatter/source/input/__pycache__/variable_input_type_adapter.cpython-36.pyc b/chatter/source/input/__pycache__/variable_input_type_adapter.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..593d96453311903361d12f7c9b3981a789211425 GIT binary patch literal 2104 zcmbtVUvJ|?5Z|@ej^idL(1Gp_9bljw7>Nk!koY4G^$#t&6Qm~+Dr7~nT<_-Uo@0mI zO|K2f3+*%F3&1;{fbX)eJoPKw6Ek+2Yf-CvV56Pv%x`|Y;>$KQWOj~j&i zLoPH1<2C5&a}bS7yfZ| z2D?c}%`1~4OG_@|JkvrZxjLiuMC)NOp6GR+jKO|p)y8H|Ct8d|p;O#{-jRQ zIf*PzBb!?h<96f-TQK1aF*+bSB~hcs6nTKo<4r)<6iqrn@0BWZz zF?VN(KsG^4Kke^}Km+*TmwAw>peS`9MOr>7vZ>%fqJktiO!ABe878}0+6mjnR>#V; z4uw=%S;Sc}ERDaHD512>iYG9wrD^OZD$y`-cQzhvZT5|ysjN^sDN5r>*9SvRO_G0UeeU3AN{7-e+2dL3Un7hGMCfhjPi zPc31fd2;s*0CXQ=r>qfvBK(BTnJtHHUyHd zqPU5|Me#a{*Fc1hyo2L6P~dT+MQ)Mqm4_<5wRj8WO?xcMR47&Fa1~4n9TDo=K4s87 z?C^7~>Ah>ybID1H`_CKFcn!LG7sREa_PEX2DTUH@xC_PXa440qIsM|=C@-Vk^mjj4 zv?Ub-nPdv8HCN&mmHq2m!28w}Y=>p3vE5Iy*YZ2|PfA{d4czfrE z@7Mc1D8l-+=`4LbU&Y$3=ou_xK7)F_T)~|sqwod7zKIF*LBKmg;+g@p3-7E0bv_ZH zRo^nE^uPD%#c-3Ej^hXD1Dz2%-MIDuFH33X++B)~5;pw5gi#~!D^`bq(K1Ymv6P4{ zq%}^g=nIr<#4(hA9It`-9O3%^L0EZ%^vHKGsB0hxMKTg`Z2UMLm3)%pxE04gPm+8w z10Z;r#<6@4!M%^-1Ra9|@214LZrH%H4JUdc;cOBw#1M&JP}54w f0w4VZU*h_Gz%5tBG2T~odcMY6tl^mCFX3f~Vl(X6;=P4N9;j}lq~xNIHpgF&L5 z32j|BC+wDne}6Wi-Ez~zw0Sau*H2D(K@eMVp-px2)*%?Oq%)JU zO&tl53W)wkd)e#vlfQoaef0Mw$N9&( z@c6L52d}yV3v(DLohca+;xLzar91UT9&x^Kn9qWj4hy)q@+KV2-76rTHOi9S-=I7#&>)v4rqE-RH%*r}PQ6i<(-;w%+a zdRB=as!py_Evy}V%G10Q3e2Zdw9oMf%<;6SW^5NK# z!R{b3zT)NBY+HzIPR!-bIdDSh(RPzg7o6$0Sj#Nd%vPr_`v9Ro9k~NP|9-Up$V5kjX|@J;2XpZJQ+f~ z64@!YU>Ugb3hv**1&iA(*I;KZ<+RFXQsi9ODX1*BgT>CKDi<`m>I87NWBwoTfz_$W ziU9BJ1Yl(1)nww=?!;fYE#HGlIEhfg++WEOI4{pzZExb~xb{|Hq18pEd%$$GkiZ;B zT1_A9`e4)QRqulotzLpD;ATCL9Y1oHD-V=X!$ykQXIT$9N^L z%ytt=m!Bn_M1T*11}0b0;M-`hzYfka*~IJ5*FhAu0NR5~4e@|r3AyaYB=LZ51W4%h zNKCFZBqT`?x{IqDh_&|*V5-DfR?#WXGLvN4RIqu8`(Bnkozt>CiL#7|Jj~xt^fc4 literal 0 HcmV?d00001 diff --git a/chatter/source/logic/__pycache__/logic_adapter.cpython-36.pyc b/chatter/source/logic/__pycache__/logic_adapter.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca2c83dc3f77a55fa699a41ccc248bb67e6a76de GIT binary patch literal 4037 zcma)9-HzMF6(&VW;%c?FyRp$EaN2>}UZ`yrDUyp~VH9y~w2?sE8}Gc)IW=bY~xet2*& z_}f4KKK|V;$N7)5b@;gd7^nIv2JUb-buxGCx(@GfFYRRB*faNj>Sx_?*L8mB@GkGY zad=O7Yj51+{ZE`x@C9Z&Be%L!=1E*|5vGY2GD_7K9s5K75s&701(|~+n-@}tS;5Ox z#5A)xNE>}e_fm>ajjPLZ`W}K49D|Xq8R?8oJA9* zWfW@`r;$=j&mzsFm`kBVuIX8@B!^AYC>Ca}k%-cirL;7QtjUV0Sy85(!R}OKn0quZ z;>YtyMj2B&(&lv-7a3k9s>s7>na4UQ@?$6@7%%kuc|me!6`;rFk)APKuxK-%ZRWFP z{*l><4Ey@yiI_%ZN|qJu>?{>8MXqLABzedaD4ECN>}*$cKrt_JB|;_ONzymWgq{`L zsvQ9?E5KR-W(B^fUb{M-3mr}qsdU&ZOd648G5lwaE=CXMK2G&l4BFA|%2{_VI#Xxm z{>4Q&Ba~7v^Ivz&y_a^g-fIeTf91aD$!9C~qR;)c``XvR%GZ1Aj(nyEE4*2`+<%A! zFmg*H*WHm@wsPHv*p1d%b=IAg6Yg8-m1mxY+(8ibP9&*{oVa@v+IW9>-Qw|kUl}mt zcN%D}Ic0QEbyYbRQueTG6@+1uCprwr`&Jq$&>Dqw~6*l=u(0`7*9+}L~_B3 zX`7KIy(WUmGG_&Hie1h`ZpehJVv|A>{aAc;LMgw=l~G56z@6$og#|e;iPB^lnZ#={ zUX5IN8$$A}cDPG-_b?pcRK&=RzZ~sY`&A3J2+&MA4FU`|uihmL)&_+q&L0M^ks)h< z^x3W7qnHpbCs3Jou6i${v=j=Dq+As=>RwS3vDKI*K7yb!B>}!$etSpXjZ|#xfhb*D z|Ijq4m9V3QIqN0q^nh>PMS5;Wl) z3nqCxIi4k`ASNROc2q1Ef>UU;630?6=HeIuRHW_8=LX&e^u0DSucE$>&BNLr>I7_% z>@DBk0ews8X4Hq6Y9r#$u0=ok7u?dS^IL110O}hoqI|0~vISDQuAkFFHgzLE2g(g6 znX*Ud%mG|$(iRktPzm#C0tk^_Chkzq;agNv%SUi@yI8wGQ>H05jeE8;kE-ysMYoy( zXooa}n#lB7amk`MF7bve6}#;~3x;?#N6oZ-#C5e0h!b(d9&Zd$aRH@SBn2y#4OKg> zU6J;h2t{2=U3RCk$XJG=o}l&E)kAFocB6H5*8(^qmY3N?NGz@|1p@N}gXd3(x}ePm zL|E9n)gvt~NsBGKsJx~1BrD8j$@vUYTUuQ0hmP5!YhEVbL!&xWfx={Q9pVjP9#c+i zV&s^8($uEwiL?2sX_Zb;hgjVq{>Jn*Cx!l1)5Qt?RLVlu$tB-G>|af*eyyh=Ov6*( zTHf7Bsal|=SRybIG&3ljG$oXK(ODCjxywJbAlGGve4a-ccw=gSK-fvNn7h8V#Ib>; zX>x&1Ld3VQ5k(0HnmeaBri9^QQktjD%#kM#$WW@Q)IreJVR&=NsRp)!lgy`(x1`d$ z!sHv77&7hQ;qGIc>N^1T?z@he{C(FgLd)7{xag5#qK0^>0{#1Z}E<4|Zs;^^<7^&}4>f(Q~V= z;hiQp7>u|Q9Hu=v!+Q&hyZq7D+i-JJvZIHM7aJ`ozLifTC29ZvPA68s4ShK1l?~GPJ#VAt{)uy*4Jyiq%2g zYQ`c>u}s@H!7>VSY1$`hndww_Fa!g?GrS-40zcRf-Wmp`p*|V)$@*!D(vF;vvv&rH;Gz9A{uz;hqD|4bqILZra6VDa literal 0 HcmV?d00001 diff --git a/chatter/source/logic/__pycache__/low_confidence.cpython-36.pyc b/chatter/source/logic/__pycache__/low_confidence.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a156f975c773c368c8d6a98d04fe57acb967698f GIT binary patch literal 1964 zcmaJ>&2Aev5GJ{+m8{B+lcq%v{o$#Nj95q!Bq$8S{c+GDKpPZw(8Iz7wO&%v+SF1Z zDJwSChe8H&YJomLd+!tUCE6F@wY~Tia_VqbKXzh-0!Lhu!pvLjS$vfz%-wM;D}?1hB(F;al!*hhCv*_I^;DO4r_4@qi+z8c>Nslx~NTqIO2^x z)Lyv&ZPdnYeVk{h;)2P{3Y|!EL8^n5J)2lD6uG^?vz@PmvEL;&9i(1)K7!f8+<}Ea zG3F>H9LK>6)WhDX^F|zcht5%4dx1g}N8mu@ZhxyLyDINxT;!?vk|!hYKpo6CDU2V4 z9qZiCgmTeK#?n$P%t+-%&`D+ous_K91DY;gD77X7k-vVlRLu;{m8D0yn&h2ksj_u6 zNp#n2Hmg&Fg8$C=IrQcW&-K#vnS7Ss+BORdiO zeW49}fz70fu5(HKQ56+jQ1o8s;e+KF*q?>W<{_7E7^}S#N_(HmY#m4eJFj^)*MhJ; zhJw?BlgbaN`dM0fBlH!6a}{QU6sgTr&W6GcR0Y5l%V{EIQdzzfjz=R|;=Fit)*5MX=p&bvyOE^COtex8UD=55gjg3p6E%q=yRpE5;xX0}gDPk^-@? z{0ewDC1=Fe3Sy%v(c1;-<1@l*JUT}Od4#6$2CH)fY^*|M}aG|2~h`U(Lhm zM~^=lQlqp!xl12Yu1YqIa}JrdNzOY9pM4e8VD5RJ-v!Y}1^Q|23|sJ(qhIiENCrKBHe=Bmk)Ng_MeL3X#Z>VeDlZq`zde?%4OfEu};N) znOBe#wycNF=;UQ{TV}?lYB<{Y*gN9AiqPq&*Ogv}u`JIlW1VfOAjxJ27IW=(!_~}q zB(#Qpgj{2O*9DM2cYA4-Ensxb%w)NTB^53J3b~uF!fD!p3xO&<7ga^5-+|Wwkrl>L znHa-Z8NObHZM_b{BWcz^Abg9gkq|e+CW*q+58fD2>f`pqBkb9<3)4IVQJ_P}#}s%0 zD_j604uL~wK|u}!ScgYQ-{rUnSdd4Y`2Qk+tOcjX3+`8q3dlc;ZdnA^_bHn77P_xB zv8D3WPUZWOAKG(Dmkqh5Z^GK$s7h^7Jtc&$xy*>Z1=rf4_RzT-Sw0$D23e_kyIW1A zGSJuA&^LD1m^Xq7ch9{Nd{-Z7l?r3puw(o-ikiV%(83K8k<<6Dg@3M6_k78dpy4oYf;H_q;97voF%)a!TU=Riu6eKsU;8IYIx_ARuB0v@EqNT{Sc9>FMrN zRojl$77ItpDlAyBfjwWqNAL%|WtCrG#l6+@Fj3N0)s*XT@99(Lp1!%V68`?zAJI2m zLjECV9uLN!Len2XCy1aKnbL?-A{^mn&eV-u7<X*co^(^C@pEt)8JMm!RA086*I@HIDMIprozc(#4OvkHbq z&i@81A%kaUEZM8~unb``-r{5>S6XV8=4EA=DVUIeewyaehB+Kd$fFo^tkN(@HRF1c z<|D@0<4S7G3ai{OB_CI*f>3~JjFBEmCW>5c+CX0pnJ%u`IIz!x#aQylD4rN8*iaQy zyWAq-xqyhKQaP+0re(SW@~cD)krT>Y_N@t=c9^`MzduPuW#SKx6XzDd^;aysUEXOPk{d+w{L~`6OA-denr6_|WFJU4r6*1e6zo$V#JNRk2Wgo&K!kHVU0li+bD3*4*LZ3*!pl+> zrAnb{?bVoZsu!o`MFZ*v@s$OL6o$*lUTc>FYO4f(-U8~5UH0+pu0wSRChV-e>}YLfaIrip;Z8LO-yT({_{IhJjc-G$ zw_zf>`fZ`jFg@BSCNf{z`u0&GOLW>n$4W)LgIO?D6k5BCQB%>1Em{$`z8HmPa$=Re zq$RZmbMHgbSD+()-wE81hBTm_yXu6F1LI3xNLOj!Ill55ZOxu`%NF}>_94Qzc`k4dt{m8OmTH@wYYENh1<&NLtXXV F{{RCSIO6~S literal 0 HcmV?d00001 diff --git a/chatter/source/logic/__pycache__/multi_adapter.cpython-36.pyc b/chatter/source/logic/__pycache__/multi_adapter.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3517c8a2a8983cf847c7a4eab4cd11f3c52419e GIT binary patch literal 4798 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 St%pzAl%9rKH}EkY$NvBhYAS;O literal 0 HcmV?d00001 diff --git a/chatter/source/logic/__pycache__/specific_response.cpython-36.pyc b/chatter/source/logic/__pycache__/specific_response.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..179e73d4e32ae123107987e7a83c6de8df23387a GIT binary patch literal 1416 zcmah}PjAyO6t|tEO|y z;>I_CJD-5>k}Icu1x~!@q@_YkSn{j${{H-a&wa4E8h!ooExzLu@{=qr_$E(an|mN6 zku)b08dFLnlU~jyUhGlwl1N_$r$h$IJMm&)c6P|1dj=6?K=mdom_m{xW(azR_~J|=o> zw%iA1S?BA{F0A@w*ZJMOJ!AFW-hgjxEPOcM-B;YXadhK$KLMe$@mOgCUzy=KHi9Qj zCpH(MkiM)e)c=3}#;+-$-QASrZx7E+pCSjikYH^03T*QXge8{F$O$`OBQm33C{PV8 z&%KF%%qVCM0_mNAhv>Moe?pJF8Jjh_t|UM}uzkiL6L8<#v58d^RoFWE)2Vu8=~_Tn zV5+IoI)Iou6e24!D@5H-%i;sHD6v^t=q?()v9dbE)7qRaVdfPEe(f8TkLpfClMB(k z7KjjOo){y^rUQb|i2Cg4_Ia?DV*ZWn7UWnUyDAYQ6|4v) zYfdN~QjNo--HY}dVtoM=bp+&%GO3DGwO7zr(eW?v-4^&wuo)zlKsO^6vZI^lL2n~q zY+zK1WTIeHA|WQFta3c}g?L{jd28tkAyZH#nkw#u{IibN-WkiMH@jgZMGF&Y*9f G_PyT-s%3Wo literal 0 HcmV?d00001 diff --git a/chatter/source/logic/__pycache__/time_adapter.cpython-36.pyc b/chatter/source/logic/__pycache__/time_adapter.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9cbf60e9877903fb02b85e750e79d18b1473232d GIT binary patch literal 3068 zcmcgu&u`nv6`mm}ic)09>$u(|yC~R2gN?FlW4plug<%A3leQ?323y4F0)hjE;fy58 z6e-V8w#8DHD!}Gcpg<43wD;Z%^#ACQ*Pik(P9se|OebGM}nc`#C0~sZuh{ee!g_ZFCM!&OW~4uc0r~Nb0o;d`smZ{QthCS-ST<`xjB<>1&{PD1af;e?gVA zoWEiVYh?9V$^XOwHUyf$HV5!ZK57v72y&t+Jkc7s3+ojNTr<-a*F;Bbh)r=_Y^`RR z;ytnblD%T1<|DS?;>N1yQty2>UqWdIqh4v#*xV_tkyknk_z|=O1Dt>GM6IB@^CU#9 ze-X~4ewc(>$NgBU%Bw+8Tt6R%OUU@K_T#*`F_bg^-N7A^`Lk^555qHQutvvU)T1Y9 zcD^3{U?potuPl_qK(S>8V2J=ri&S16^Ws)aB1=R8GlPtfqZ277#Rs(-oKSv0Jj)cW zrypfWri$$tehFY~pyX;V#jSie8%tVlJX^~TrFv2P_^rLb1u~Ol*@+KjNK385S@Dx0 z_VkwI%3+emc$g&EL#%7Z;`|aK7EnQ~^pqg7gKEG^_N+7KCD@>E{ndpO!2ggTpJURYVcCq*S`^OQ&1hP5SIDkkVCqzKUbbvv)q@n%E z>`nWSu340gCl5X$;S*(1Mp%eQZlRyxKKeuX!ox zBk)rtb=Ni?R8B50@`|S*Z%50d|BfR>&MQZ{?W&=t6RDJo!OE*o1_4R(6V;?admwW~ zmtM7~+sIWIr&T+;yieVR+NK$INw-F;*Ga+`GLa%c_6U0|qZDYF!>3jd808=U3`kR% zGa{^*bGVBJ`H{~6tg;!hv>PQ>*PMgJRB%;v~SI9xX3 zDAxO+k&(RsxeVCcxwy9;f5e{Mo;ONo z=neOk0XFeyvDuH6&V%zziMxN8cZ*Jxr8z*LsaM?&UqnLo2g7)Dl8n=Aath(;+4;q+ zC|XfAO>?8v9EL`|>s8Jui_@!+(i6NlDmR==WGX7RpQ&+}SB)e)2U|DH8eMM*l`S|_ zdA0w&7MRF5rhC_^x%O1|ygJKP4!K^ri6jzP-Uh=p26xx7;uk3TLsZOh>|NUd`W(J% zZ}MG>N}J#0x2@viZ(!})@8Ko#=>E_#8st4j!z4$%&?q_P))I3-n?vc4;%?FzqQ6XN zKpxOm+M||0dLxtqV?b8WE?vaD@#Hl@oNm<9950nI9u>t7J(RA#g(wRY-kskZ-1)74 zXVSe^QS{EF(s*iQsrnczD59atMjlgi7>dqV{fMfYsCwS&s?RD9IO)@#as9KjPtTx8 zcALxV-_o@c%SF4SyJ1eK4woWct~^lH1kb4}gl`z2YED#!H closest_match.confidence: + statement.confidence = confidence + closest_match = statement + + return closest_match + + def can_process(self, statement): + """ + Check that the chatbot's storage adapter is available to the logic + adapter and there is at least one statement in the database. + """ + return self.chatbot.storage.count() + + def process(self, input_statement): + + # Select the closest match to the input statement + closest_match = self.get(input_statement) + self.logger.info('Using "{}" as a close match to "{}"'.format( + input_statement.text, closest_match.text + )) + + # Get all statements that are in response to the closest match + response_list = self.chatbot.storage.filter( + in_response_to__contains=closest_match.text + ) + + if response_list: + self.logger.info( + 'Selecting response from {} optimal responses.'.format( + len(response_list) + ) + ) + response = self.select_response(input_statement, response_list) + response.confidence = closest_match.confidence + self.logger.info('Response selected. Using "{}"'.format(response.text)) + else: + response = self.chatbot.storage.get_random() + self.logger.info( + 'No response to "{}" found. Selecting a random response.'.format( + closest_match.text + ) + ) + + # Set confidence to zero because a random response is selected + response.confidence = 0 + + return response diff --git a/chatter/source/logic/logic_adapter.py b/chatter/source/logic/logic_adapter.py new file mode 100644 index 0000000..df2c143 --- /dev/null +++ b/chatter/source/logic/logic_adapter.py @@ -0,0 +1,100 @@ +from __future__ import unicode_literals +from ..adapters import Adapter +from ..utils import import_module + + +class LogicAdapter(Adapter): + """ + This is an abstract class that represents the interface + that all logic adapters should implement. + + :param statement_comparison_function: The dot-notated import path to a statement comparison function. + Defaults to ``levenshtein_distance``. + + :param response_selection_method: The a response selection method. + Defaults to ``get_first_response``. + """ + + def __init__(self, **kwargs): + super(LogicAdapter, self).__init__(**kwargs) + from ..comparisons import levenshtein_distance + from ..response_selection import get_first_response + + # Import string module parameters + if 'statement_comparison_function' in kwargs: + import_path = kwargs.get('statement_comparison_function') + if isinstance(import_path, str): + kwargs['statement_comparison_function'] = import_module(import_path) + + if 'response_selection_method' in kwargs: + import_path = kwargs.get('response_selection_method') + if isinstance(import_path, str): + kwargs['response_selection_method'] = import_module(import_path) + + # By default, compare statements using Levenshtein distance + self.compare_statements = kwargs.get( + 'statement_comparison_function', + levenshtein_distance + ) + + # By default, select the first available response + self.select_response = kwargs.get( + 'response_selection_method', + get_first_response + ) + + def get_initialization_functions(self): + """ + Return a dictionary of functions to be run once when the chat bot is instantiated. + """ + return self.compare_statements.get_initialization_functions() + + def initialize(self): + for function in self.get_initialization_functions().values(): + function() + + def can_process(self, statement): + """ + A preliminary check that is called to determine if a + logic adapter can process a given statement. By default, + this method returns true but it can be overridden in + child classes as needed. + + :rtype: bool + """ + return True + + def process(self, statement): + """ + Override this method and implement your logic for selecting a response to an input statement. + + A confidence value and the selected response statement should be returned. + The confidence value represents a rating of how accurate the logic adapter + expects the selected response to be. Confidence scores are used to select + the best response from multiple logic adapters. + + The confidence value should be a number between 0 and 1 where 0 is the + lowest confidence level and 1 is the highest. + + :param statement: An input statement to be processed by the logic adapter. + :type statement: Statement + + :rtype: Statement + """ + raise self.AdapterMethodNotImplementedError() + + @property + def class_name(self): + """ + Return the name of the current logic adapter class. + This is typically used for logging and debugging. + """ + return str(self.__class__.__name__) + + class EmptyDatasetException(Exception): + + def __init__(self, value='An empty set was received when at least one statement was expected.'): + self.value = value + + def __str__(self): + return repr(self.value) diff --git a/chatter/source/logic/low_confidence.py b/chatter/source/logic/low_confidence.py new file mode 100644 index 0000000..fb5435c --- /dev/null +++ b/chatter/source/logic/low_confidence.py @@ -0,0 +1,58 @@ +from __future__ import unicode_literals +from ..conversation import Statement +from .best_match import BestMatch + + +class LowConfidenceAdapter(BestMatch): + """ + Returns a default response with a high confidence + when a high confidence response is not known. + + :kwargs: + * *threshold* (``float``) -- + The low confidence value that triggers this adapter. + Defaults to 0.65. + * *default_response* (``str``) or (``iterable``)-- + The response returned by this logic adaper. + * *response_selection_method* (``str``) or (``callable``) + The a response selection method. + Defaults to ``get_first_response``. + """ + + def __init__(self, **kwargs): + super(LowConfidenceAdapter, self).__init__(**kwargs) + + self.confidence_threshold = kwargs.get('threshold', 0.65) + + default_responses = kwargs.get( + 'default_response', "I'm sorry, I do not understand." + ) + + # Convert a single string into a list + if isinstance(default_responses, str): + default_responses = [ + default_responses + ] + + self.default_responses = [ + Statement(text=default) for default in default_responses + ] + + def process(self, input_statement): + """ + Return a default response with a high confidence if + a high confidence response is not known. + """ + # Select the closest match to the input statement + closest_match = self.get(input_statement) + + # Choose a response from the list of options + response = self.select_response(input_statement, self.default_responses) + + # Confidence should be high only if it is less than the threshold + if closest_match.confidence < self.confidence_threshold: + response.confidence = 1 + else: + response.confidence = 0 + + return response diff --git a/chatter/source/logic/mathematical_evaluation.py b/chatter/source/logic/mathematical_evaluation.py new file mode 100644 index 0000000..2a65fdc --- /dev/null +++ b/chatter/source/logic/mathematical_evaluation.py @@ -0,0 +1,67 @@ +from __future__ import unicode_literals +from . import LogicAdapter +from ..conversation import Statement + + +class MathematicalEvaluation(LogicAdapter): + """ + The MathematicalEvaluation logic adapter parses input to determine + whether the user is asking a question that requires math to be done. + If so, the equation is extracted from the input and returned with + the evaluated result. + + For example: + User: 'What is three plus five?' + Bot: 'Three plus five equals eight' + + :kwargs: + * *language* (``str``) -- + The language is set to 'ENG' for English by default. + """ + + def __init__(self, **kwargs): + super(MathematicalEvaluation, self).__init__(**kwargs) + + self.language = kwargs.get('language', 'ENG') + self.cache = {} + + def can_process(self, statement): + """ + Determines whether it is appropriate for this + adapter to respond to the user input. + """ + response = self.process(statement) + self.cache[statement.text] = response + return response.confidence == 1 + + def process(self, statement): + """ + Takes a statement string. + Returns the equation from the statement with the mathematical terms solved. + """ + from mathparse import mathparse + + input_text = statement.text + + # Use the result cached by the process method if it exists + if input_text in self.cache: + cached_result = self.cache[input_text] + self.cache = {} + return cached_result + + # Getting the mathematical terms within the input statement + expression = mathparse.extract_expression(input_text, language=self.language) + + response = Statement(text=expression) + + try: + response.text += ' = ' + str( + mathparse.parse(expression, language=self.language) + ) + + # The confidence is 1 if the expression could be evaluated + response.confidence = 1 + except mathparse.PostfixTokenEvaluationException: + response.confidence = 0 + + return response diff --git a/chatter/source/logic/multi_adapter.py b/chatter/source/logic/multi_adapter.py new file mode 100644 index 0000000..150f6c3 --- /dev/null +++ b/chatter/source/logic/multi_adapter.py @@ -0,0 +1,153 @@ +from __future__ import unicode_literals +from collections import Counter +from .. import utils +from .logic_adapter import LogicAdapter + + +class MultiLogicAdapter(LogicAdapter): + """ + MultiLogicAdapter allows ChatterBot to use multiple logic + adapters. It has methods that allow ChatterBot to add an + adapter, set the chat bot, and process an input statement + to get a response. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + # Logic adapters added by the chat bot + self.adapters = [] + + # Required logic adapters that must always be present + self.system_adapters = [] + + def get_initialization_functions(self): + """ + Get the initialization functions for each logic adapter. + """ + functions_dict = {} + + # Iterate over each adapter and get its initialization functions + for logic_adapter in self.get_adapters(): + functions = logic_adapter.get_initialization_functions() + functions_dict.update(functions) + + return functions_dict + + def process(self, statement): + """ + Returns the output of a selection of logic adapters + for a given input statement. + + :param statement: The input statement to be processed. + """ + results = [] + result = None + max_confidence = -1 + + for adapter in self.get_adapters(): + if adapter.can_process(statement): + + output = adapter.process(statement) + results.append((output.confidence, output, )) + + self.logger.info( + '{} selected "{}" as a response with a confidence of {}'.format( + adapter.class_name, output.text, output.confidence + ) + ) + + if output.confidence > max_confidence: + result = output + max_confidence = output.confidence + else: + self.logger.info( + 'Not processing the statement using {}'.format(adapter.class_name) + ) + + # If multiple adapters agree on the same statement, + # then that statement is more likely to be the correct response + if len(results) >= 3: + statements = [s[1] for s in results] + count = Counter(statements) + most_common = count.most_common() + if most_common[0][1] > 1: + result = most_common[0][0] + max_confidence = self.get_greatest_confidence(result, results) + + result.confidence = max_confidence + return result + + def get_greatest_confidence(self, statement, options): + """ + Returns the greatest confidence value for a statement that occurs + multiple times in the set of options. + + :param statement: A statement object. + :param options: A tuple in the format of (confidence, statement). + """ + values = [] + for option in options: + if option[1] == statement: + values.append(option[0]) + + return max(values) + + def get_adapters(self): + """ + Return a list of all logic adapters being used, including system logic adapters. + """ + adapters = [] + adapters.extend(self.adapters) + adapters.extend(self.system_adapters) + return adapters + + def add_adapter(self, adapter, **kwargs): + """ + Appends a logic adapter to the list of logic adapters being used. + + :param adapter: The logic adapter to be added. + :type adapter: `LogicAdapter` + """ + utils.validate_adapter_class(adapter, LogicAdapter) + adapter = utils.initialize_class(adapter, **kwargs) + self.adapters.append(adapter) + + def insert_logic_adapter(self, logic_adapter, insert_index, **kwargs): + """ + Adds a logic adapter at a specified index. + + :param logic_adapter: The string path to the logic adapter to add. + :type logic_adapter: str + + :param insert_index: The index to insert the logic adapter into the list at. + :type insert_index: int + """ + utils.validate_adapter_class(logic_adapter, LogicAdapter) + + NewAdapter = utils.import_module(logic_adapter) + adapter = NewAdapter(**kwargs) + + self.adapters.insert(insert_index, adapter) + + def remove_logic_adapter(self, adapter_name): + """ + Removes a logic adapter from the chat bot. + + :param adapter_name: The class name of the adapter to remove. + :type adapter_name: str + """ + for index, adapter in enumerate(self.adapters): + if adapter_name == type(adapter).__name__: + del self.adapters[index] + return True + return False + + def set_chatbot(self, chatbot): + """ + Set the chatbot for each of the contained logic adapters. + """ + super(MultiLogicAdapter, self).set_chatbot(chatbot) + + for adapter in self.get_adapters(): + adapter.set_chatbot(chatbot) diff --git a/chatter/source/logic/no_knowledge_adapter.py b/chatter/source/logic/no_knowledge_adapter.py new file mode 100644 index 0000000..59b11fd --- /dev/null +++ b/chatter/source/logic/no_knowledge_adapter.py @@ -0,0 +1,26 @@ +from __future__ import unicode_literals +from .logic_adapter import LogicAdapter + + +class NoKnowledgeAdapter(LogicAdapter): + """ + This is a system adapter that is automatically added + to the list of logic adapters during initialization. + This adapter is placed at the beginning of the list + to be given the highest priority. + """ + + def process(self, statement): + """ + If there are no known responses in the database, + then a confidence of 1 should be returned with + the input statement. + Otherwise, a confidence of 0 should be returned. + """ + + if self.chatbot.storage.count(): + statement.confidence = 0 + else: + statement.confidence = 1 + + return statement diff --git a/chatter/source/logic/specific_response.py b/chatter/source/logic/specific_response.py new file mode 100644 index 0000000..2ed6da1 --- /dev/null +++ b/chatter/source/logic/specific_response.py @@ -0,0 +1,38 @@ +from __future__ import unicode_literals +from .logic_adapter import LogicAdapter + + +class SpecificResponseAdapter(LogicAdapter): + """ + Return a specific response to a specific input. + + :kwargs: + * *input_text* (``str``) -- + The input text that triggers this logic adapter. + * *output_text* (``str``) -- + The output text returned by this logic adapter. + """ + + def __init__(self, **kwargs): + super(SpecificResponseAdapter, self).__init__(**kwargs) + from ..conversation import Statement + + self.input_text = kwargs.get('input_text') + + output_text = kwargs.get('output_text') + self.response_statement = Statement(output_text) + + def can_process(self, statement): + if statement == self.input_text: + return True + + return False + + def process(self, statement): + + if statement == self.input_text: + self.response_statement.confidence = 1 + else: + self.response_statement.confidence = 0 + + return self.response_statement diff --git a/chatter/source/logic/time_adapter.py b/chatter/source/logic/time_adapter.py new file mode 100644 index 0000000..3de4001 --- /dev/null +++ b/chatter/source/logic/time_adapter.py @@ -0,0 +1,91 @@ +from __future__ import unicode_literals +from datetime import datetime +from .logic_adapter import LogicAdapter + + +class TimeLogicAdapter(LogicAdapter): + """ + The TimeLogicAdapter returns the current time. + + :kwargs: + * *positive* (``list``) -- + The time-related questions used to identify time questions. + Defaults to a list of English sentences. + * *negative* (``list``) -- + The non-time-related questions used to identify time questions. + Defaults to a list of English sentences. + """ + + def __init__(self, **kwargs): + super(TimeLogicAdapter, self).__init__(**kwargs) + from nltk import NaiveBayesClassifier + + self.positive = kwargs.get('positive', [ + 'what time is it', + 'hey what time is it', + 'do you have the time', + 'do you know the time', + 'do you know what time it is', + 'what is the time' + ]) + + self.negative = kwargs.get('negative', [ + 'it is time to go to sleep', + 'what is your favorite color', + 'i had a great time', + 'thyme is my favorite herb', + 'do you have time to look at my essay', + 'how do you have the time to do all this' + 'what is it' + ]) + + labeled_data = ( + [(name, 0) for name in self.negative] + + [(name, 1) for name in self.positive] + ) + + train_set = [ + (self.time_question_features(text), n) for (text, n) in labeled_data + ] + + self.classifier = NaiveBayesClassifier.train(train_set) + + def time_question_features(self, text): + """ + Provide an analysis of significant features in the string. + """ + features = {} + + # A list of all words from the known sentences + all_words = " ".join(self.positive + self.negative).split() + + # A list of the first word in each of the known sentence + all_first_words = [] + for sentence in self.positive + self.negative: + all_first_words.append( + sentence.split(' ', 1)[0] + ) + + for word in text.split(): + features['first_word({})'.format(word)] = (word in all_first_words) + + for word in text.split(): + features['contains({})'.format(word)] = (word in all_words) + + for letter in 'abcdefghijklmnopqrstuvwxyz': + features['count({})'.format(letter)] = text.lower().count(letter) + features['has({})'.format(letter)] = (letter in text.lower()) + + return features + + def process(self, statement): + from ..conversation import Statement + + now = datetime.now() + + time_features = self.time_question_features(statement.text.lower()) + confidence = self.classifier.classify(time_features) + response = Statement('The current time is ' + now.strftime('%I:%M %p')) + + response.confidence = confidence + return response diff --git a/chatter/source/output/__init__.py b/chatter/source/output/__init__.py new file mode 100644 index 0000000..0d64ca4 --- /dev/null +++ b/chatter/source/output/__init__.py @@ -0,0 +1,15 @@ +from .output_adapter import OutputAdapter +from .microsoft import Microsoft +from .terminal import TerminalAdapter +from .mailgun import Mailgun +from .gitter import Gitter +from .hipchat import HipChat + +__all__ = ( + 'OutputAdapter', + 'Microsoft', + 'TerminalAdapter', + 'Mailgun', + 'Gitter', + 'HipChat', +) diff --git a/chatter/source/output/__pycache__/__init__.cpython-36.pyc b/chatter/source/output/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f996edcc8f3204f723ebfd6d186b8d54c332ee4 GIT binary patch literal 480 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 P{!45(gULp;Pe=YgxX)Q@ literal 0 HcmV?d00001 diff --git a/chatter/source/output/__pycache__/mailgun.cpython-36.pyc b/chatter/source/output/__pycache__/mailgun.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3295a1afa669c9d2179aeb0cb5819f229aa9f4c9 GIT binary patch literal 1621 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 literal 0 HcmV?d00001 diff --git a/chatter/source/output/__pycache__/output_adapter.cpython-36.pyc b/chatter/source/output/__pycache__/output_adapter.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..478d6c14fd5452c82f9edb75f3aef1088f1d9f8e GIT binary patch literal 974 zcmZuvL2KJE6qaIVO-%zk?=W-?Y++E?A&k*bSlJ~Vtn6R{La|_Z7Bbf|;<9jlp&Cy(n%7e}DxE~Sj&TzVCxByA1B>0Y|ksQiM z#&(KSGDg!c3%^QW6rKYgH;*J3gH~{x$rC#nyrAD4OpwB1Uzk-V6I;E zNrqL{Y?K5Bw;r?v*@CTWoq1)nD3ss6!2B#{!Z8VjLMWxL5yXP4jCR1hVMsIo@SY(+ zWt8^Q7G#lylEMgIit`+&3!Z(ZSO^ubFxaMs0xmm|h?gtJO7OXnOlf2~l&i|BfD$y; zRy%J>wFAi>$(<&9$XO+3kR!jB8hOOoC5N8LC5H+4c6lVLOBT#FJoZxYT+^s@;26V7jHQS>)QoJ*5& zZjbK;KS;Nu5DR@1s%d*>6vu-$vOohyy;^63sy2M^xOE^PG@~~i`PV!Z literal 0 HcmV?d00001 diff --git a/chatter/source/output/__pycache__/terminal.cpython-36.pyc b/chatter/source/output/__pycache__/terminal.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98a931b434864eea4a3c7c4b6d728af61b2c222e GIT binary patch literal 757 zcmZ8f&2H2%5Vqr_UBWKxnNw~k67*6oAcR_0YHtOJibE913b9Lr>^RttC@T&~?PGA@ z%8lpol~Z4V6En^(T6LuPiD&%HH{%>G7Wwb*KdPUUkUwNcQizX0-4if^2&#yt6{SQZ zf~iEaicxY(L@KfyA~MOYS(S>tcck3Eg&U-#VLA3)YlP&g^U^lT-9i&gEZ&TM82zzm z21vA6z7+<+5vY3vMv{sOQYC^`Ok{$I^oBx{1e)xHhv(Aju5Z)^W?kh4$Z_F1Jt$c; z6MEq<8(%a^nJ=zbK@1&V8((-6%Uh#0(rmnhN^8cCm*64ZfFH!Fvle-wpie+opl%7q zlW+7By@ti02WE8rJl;VvZ*A8@E5NhT4W@ShY~uImqP3An^-!OftEcY|>vo!Q>pvJ@o4D}$ z|5U@*upF#u0gG>`A(q2uCuvUC#hvvY+$}HZS($|y=Y69k=OO1@8!;-3=bV2Y8@0*o zb1qEFxm^Huq`YJ1c$R@djqPV?n#2>t2h@wvj}}lZ?max1M&X<1wMaNt?_lC27J' % (self.name, from_address), + 'to': recipients, + 'subject': subject, + 'text': text + }) + + def process_response(self, statement, session_id=None): + """ + Send the response statement as an email. + """ + subject = 'Message from %s' % (self.name) + + self.send_message( + subject, + statement.text, + self.from_address, + self.recipients + ) + + return statement diff --git a/chatter/source/output/microsoft.py b/chatter/source/output/microsoft.py new file mode 100644 index 0000000..177dc35 --- /dev/null +++ b/chatter/source/output/microsoft.py @@ -0,0 +1,109 @@ +from __future__ import unicode_literals +import json +from .output_adapter import OutputAdapter + + +class Microsoft(OutputAdapter): + """ + An output adapter that allows a ChatterBot instance to send + responses to a Microsoft bot using *Direct Line client protocol*. + """ + + def __init__(self, **kwargs): + super(Microsoft, self).__init__(**kwargs) + + self.directline_host = kwargs.get( + 'directline_host', + 'https://directline.botframework.com' + ) + self.direct_line_token_or_secret = kwargs.get( + 'direct_line_token_or_secret' + ) + self.conversation_id = kwargs.get('conversation_id') + + authorization_header = 'BotConnector {}'.format( + self.direct_line_token_or_secret + ) + + self.headers = { + 'Authorization': authorization_header, + 'Content-Type': 'application/json' + } + + def _validate_status_code(self, response): + status_code = response.status_code + if status_code not in [200, 204]: + raise self.HTTPStatusException('{} status code recieved'.format(status_code)) + + def get_most_recent_message(self): + """ + Return the most recently sent message. + """ + import requests + endpoint = '{host}/api/conversations/{id}/messages'.format( + host=self.directline_host, + id=self.conversation_id + ) + + response = requests.get( + endpoint, + headers=self.headers, + verify=False + ) + + self.logger.info('{} retrieving most recent messages {}'.format( + response.status_code, endpoint + )) + + self._validate_status_code(response) + + data = response.json() + + if data['messages']: + last_msg = int(data['watermark']) + return data['messages'][last_msg - 1] + return None + + def send_message(self, conversation_id, message): + """ + Send a message to a HipChat room. + https://www.hipchat.com/docs/apiv2/method/send_message + """ + import requests + + message_url = "{host}/api/conversations/{conversationId}/messages".format( + host=self.directline_host, + conversationId=conversation_id + ) + + response = requests.post( + message_url, + headers=self.headers, + data=json.dumps({ + 'message': message + }) + ) + + self.logger.info('{} sending message {}'.format( + response.status_code, message_url + )) + self._validate_status_code(response) + # Microsoft return 204 on operation succeeded and no content was returned. + return self.get_most_recent_message() + + def process_response(self, statement, session_id=None): + data = self.send_message(self.conversation_id, statement.text) + self.logger.info('processing user response {}'.format(data)) + return statement + + class HTTPStatusException(Exception): + """ + Exception raised when unexpected non-success HTTP + status codes are returned in a response. + """ + + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) diff --git a/chatter/source/output/output_adapter.py b/chatter/source/output/output_adapter.py new file mode 100644 index 0000000..880cb18 --- /dev/null +++ b/chatter/source/output/output_adapter.py @@ -0,0 +1,20 @@ +from ..adapters import Adapter + + +class OutputAdapter(Adapter): + """ + A generic class that can be overridden by a subclass to provide extended + functionality, such as delivering a response to an API endpoint. + """ + + def process_response(self, statement, session_id=None): + """ + Override this method in a subclass to implement customized functionality. + + :param statement: The statement that the chat bot has produced in response to some input. + + :param session_id: The unique id of the current chat session. + + :returns: The response statement. + """ + return statement diff --git a/chatter/source/output/terminal.py b/chatter/source/output/terminal.py new file mode 100644 index 0000000..f189aba --- /dev/null +++ b/chatter/source/output/terminal.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals +from .output_adapter import OutputAdapter + + +class TerminalAdapter(OutputAdapter): + """ + A simple adapter that allows ChatterBot to + communicate through the terminal. + """ + + def process_response(self, statement, session_id=None): + """ + Print the response to the user's input. + """ + print(statement.text) + return statement.text diff --git a/chatter/source/parsing.py b/chatter/source/parsing.py new file mode 100644 index 0000000..cf955ff --- /dev/null +++ b/chatter/source/parsing.py @@ -0,0 +1,751 @@ +# -*- coding: utf-8 -*- +import re +from datetime import timedelta, datetime +import calendar + +# Variations of dates that the parser can capture +year_variations = ['year', 'years', 'yrs'] +day_variations = ['days', 'day'] +minute_variations = ['minute', 'minutes', 'mins'] +hour_variations = ['hrs', 'hours', 'hour'] +week_variations = ['weeks', 'week', 'wks'] +month_variations = ['month', 'months'] + +# Variables used for RegEx Matching +day_names = 'monday|tuesday|wednesday|thursday|friday|saturday|sunday' +month_names_long = ( + 'january|february|march|april|may|june|july|august|september|october|november|december' +) +month_names = month_names_long + '|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec' +day_nearest_names = 'today|yesterday|tomorrow|tonight|tonite' +numbers = ( + '(^a(?=\s)|one|two|three|four|five|six|seven|eight|nine|ten|' + 'eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|' + 'eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|' + 'eighty|ninety|hundred|thousand)' +) +re_dmy = '(' + '|'.join(day_variations + minute_variations + year_variations + week_variations + month_variations) + ')' +re_duration = '(before|after|earlier|later|ago|from\snow)' +re_year = '(19|20)\d{2}|^(19|20)\d{2}' +re_timeframe = 'this|coming|next|following|previous|last|end\sof\sthe' +re_ordinal = 'st|nd|rd|th|first|second|third|fourth|fourth|' + re_timeframe +re_time = r'(?P\d{1,2})(\:(?P\d{1,2})|(?Pam|pm))' +re_separator = 'of|at|on' + +# A list tuple of regular expressions / parser fn to match +# Start with the widest match and narrow it down because the order of the match in this list matters +regex = [ + ( + re.compile( + r''' + ( + ((?P%s)[,\s]\s*)? #Matches Monday, 12 Jan 2012, 12 Jan 2012 etc + (?P\d{1,2}) # Matches a digit + (%s)? + [-\s] # One or more space + (?P%s) # Matches any month name + [-\s] # Space + (?P%s) # Year + ((\s|,\s|\s(%s))?\s*(%s))? + ) + ''' % (day_names, re_ordinal, month_names, re_year, re_separator, re_time), + (re.VERBOSE | re.IGNORECASE) + ), + lambda m, base_date: datetime( + int(m.group('year') if m.group('year') else base_date.year), + HASHMONTHS[m.group('month').strip().lower()], + int(m.group('day') if m.group('day') else 1), + ) + timedelta(**convert_time_to_hour_minute( + m.group('hour'), + m.group('minute'), + m.group('convention') + )) + ), + ( + re.compile( + r''' + ( + ((?P%s)[,\s][-\s]*)? #Matches Monday, Jan 12 2012, Jan 12 2012 etc + (?P%s) # Matches any month name + [-\s] # Space + ((?P\d{1,2})) # Matches a digit + (%s)? + ([-\s](?P%s))? # Year + ((\s|,\s|\s(%s))?\s*(%s))? + ) + ''' % (day_names, month_names, re_ordinal, re_year, re_separator, re_time), + (re.VERBOSE | re.IGNORECASE) + ), + lambda m, base_date: datetime( + int(m.group('year') if m.group('year') else base_date.year), + HASHMONTHS[m.group('month').strip().lower()], + int(m.group('day') if m.group('day') else 1) + ) + timedelta(**convert_time_to_hour_minute( + m.group('hour'), + m.group('minute'), + m.group('convention') + )) + ), + ( + re.compile( + r''' + ( + (?P%s) # Matches any month name + [-\s] # One or more space + (?P\d{1,2}) # Matches a digit + (%s)? + [-\s]\s*? + (?P%s) # Year + ((\s|,\s|\s(%s))?\s*(%s))? + ) + ''' % (month_names, re_ordinal, re_year, re_separator, re_time), + (re.VERBOSE | re.IGNORECASE) + ), + lambda m, base_date: datetime( + int(m.group('year') if m.group('year') else base_date.year), + HASHMONTHS[m.group('month').strip().lower()], + int(m.group('day') if m.group('day') else 1), + ) + timedelta(**convert_time_to_hour_minute( + m.group('hour'), + m.group('minute'), + m.group('convention') + )) + ), + ( + re.compile( + r''' + ( + ((?P\d+|(%s[-\s]?)+)\s)? # Matches any number or string 25 or twenty five + (?P%s)s?\s # Matches days, months, years, weeks, minutes + (?P%s) # before, after, earlier, later, ago, from now + (\s*(?P(%s)))? + ((\s|,\s|\s(%s))?\s*(%s))? + ) + ''' % (numbers, re_dmy, re_duration, day_nearest_names, re_separator, re_time), + (re.VERBOSE | re.IGNORECASE) + ), + lambda m, base_date: date_from_duration( + base_date, + m.group('number'), + m.group('unit').lower(), + m.group('duration').lower(), + m.group('base_time') + ) + timedelta(**convert_time_to_hour_minute( + m.group('hour'), + m.group('minute'), + m.group('convention') + )) + ), + ( + re.compile( + r''' + ( + (?P%s) # First quarter of 2014 + \s+ + quarter\sof + \s+ + (?P%s) + ) + ''' % (re_ordinal, re_year), + (re.VERBOSE | re.IGNORECASE) + ), + lambda m, base_date: date_from_quarter( + base_date, + HASHORDINALS[m.group('ordinal').lower()], + int(m.group('year') if m.group('year') else base_date.year) + ) + ), + ( + re.compile( + r''' + ( + (?P\d+) + (?P%s) # 1st January 2012 + ((\s|,\s|\s(%s))?\s*)? + (?P%s) + ([,\s]\s*(?P%s))? + ) + ''' % (re_ordinal, re_separator, month_names, re_year), + (re.VERBOSE | re.IGNORECASE) + ), + lambda m, base_date: datetime( + int(m.group('year') if m.group('year') else base_date.year), + int(HASHMONTHS[m.group('month').lower()] if m.group('month') else 1), + int(m.group('ordinal_value') if m.group('ordinal_value') else 1), + ) + ), + ( + re.compile( + r''' + ( + (?P%s) + \s+ + (?P\d+) + (?P%s) # January 1st 2012 + ([,\s]\s*(?P%s))? + ) + ''' % (month_names, re_ordinal, re_year), + (re.VERBOSE | re.IGNORECASE) + ), + lambda m, base_date: datetime( + int(m.group('year') if m.group('year') else base_date.year), + int(HASHMONTHS[m.group('month').lower()] if m.group('month') else 1), + int(m.group('ordinal_value') if m.group('ordinal_value') else 1), + ) + ), + ( + re.compile( + r''' + (?P