diff --git a/MANIFEST.in b/MANIFEST.in index 4b3f6f17a..c1188d244 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,6 +4,7 @@ include requirements.txt recursive-include tests * recursive-include chatterbot/corpus/* *.json +recursive-include chatterbot/adapters/* *.json recursive-exclude * *.pyc recursive-exclude * *.py~ diff --git a/chatterbot/adapters/plugins/__init__.py b/chatterbot/adapters/plugins/__init__.py new file mode 100644 index 000000000..885b7faf6 --- /dev/null +++ b/chatterbot/adapters/plugins/__init__.py @@ -0,0 +1,3 @@ +from .plugin import PluginAdapter +from .evaluate_mathematically import EvaluateMathematically +from .plugin_chooser import PluginChooser diff --git a/chatterbot/adapters/plugins/data/math_words_EN.json b/chatterbot/adapters/plugins/data/math_words_EN.json new file mode 100644 index 000000000..4058fb65c --- /dev/null +++ b/chatterbot/adapters/plugins/data/math_words_EN.json @@ -0,0 +1,46 @@ +{ + "numbers" : { + "one" : 1, + "two" : 2, + "three" : 3, + "four" : 4, + "five" : 5, + "six" : 6, + "seven" : 7, + "eight" : 8, + "nine" : 9, + "ten" : 10, + "eleven" : 11, + "twelve" : 12, + "thirteen" : 13, + "fourteen" : 14, + "fifteen" : 15, + "sixteen" : 16, + "seventeen" : 17, + "eighteen" : 18, + "nineteen" : 19, + "twenty" : 20, + "thirty" : 30, + "forty" : 40, + "fifty" : 50, + "sixty" : 60, + "seventy" : 70, + "eighty" : 80, + "ninety" : 90 + }, + "words" : { + "plus" : "+", + "divided by" : "/", + "minus" : "-", + "times" : "*", + "squared" : "^ 2", + "to the power of" : "^" + }, + "scales" : { + "hundred" : "* 100", + "thousand" : "* 1000", + "million" : "* 1000000", + "billion" : "* 1000000000", + "trillion" : "* 1000000000000" + } +} diff --git a/chatterbot/adapters/plugins/evaluate_mathematically.py b/chatterbot/adapters/plugins/evaluate_mathematically.py new file mode 100644 index 000000000..5f313b673 --- /dev/null +++ b/chatterbot/adapters/plugins/evaluate_mathematically.py @@ -0,0 +1,185 @@ +from .plugin import PluginAdapter +import re +import os, json +import decimal + +class EvaluateMathematically(PluginAdapter): + + def should_answer(self, input_text): + """ + Determines whether it is appropriate for this plugin + to respond to the user input. + """ + + response = self.process( input_text ) + + if response is False: + return False + else: + return True + + + def process(self, input_text): + """ + Takes a statement string. + Returns the simplified statement string + with the mathematical terms "solved". + """ + + # Getting the mathematical terms within the input statement + expression = self.simplify_chunks( self.normalize( input_text ) ) + + # Returning important information + try: + expression += '= ' + str( eval( expression ) ) + + return expression + except: + return False + + + def simplify_chunks(self, input_text): + """ + Separates the incoming text. + """ + + string = '' + + for chunk in input_text.split(): + + is_chunk_integer = self.is_integer( chunk ) + + if is_chunk_integer is False: + is_chunk_float = self.is_float( chunk ) + + if is_chunk_float is False: + is_chunk_operator = self.is_operator( chunk ) + + if not is_chunk_operator is False: + string += str( is_chunk_operator ) + ' ' + else: + string += str( is_chunk_float ) + ' ' + else: + string += str( is_chunk_integer ) + ' ' + + return string + + + def is_float(self, string): + """ + If the string is a float, returns + the float of the string. Otherwise, + it returns False. + """ + + try: + return decimal.Decimal(string) + except decimal.DecimalException: + return False + + + def is_integer(self, string): + """ + If the string is an integer, returns + the int of the string. Otherwise, + it returns False. + """ + + try: + return int( string ) + except: + return False + + + def is_operator(self, string): + """ + If the string is an operator, returns + said operator. Otherwise, it returns + false. + """ + + if string in "+-/*^()": + return string + else: + return False + + + def normalize(self, string): + """ + Normalizes input text, reducing errors + and improper calculations. + """ + + # If the string is empty, just return it + if len( string ) is 0: + return string + + # Setting all words to lowercase + string = string.lower() + + # Removing punctuation + if not string[-1].isalnum(): + string = string[ : -1 ] + + # Removing words + string = self.substitute_words( string ) + + # Returning normalized text + return string + + def load_data( self, language ): + """ + Load language-specific data + """ + + if language == "english": + with open(os.path.join(os.path.dirname(__file__), 'data', "math_words_EN.json")) as data_file: + data = json.load(data_file) + self.data = data + + + def substitute_words(self, string): + """ + Substitutes numbers for words. + """ + + self.load_data( "english" ) + + condensed_string = '_'.join( string.split() ) + + for word in self.data[ "words" ]: + condensed_string = re.sub( '_'.join( word.split( ' ' ) ), self.data[ "words" ][ word ], condensed_string ) + + for number in self.data[ "numbers" ]: + condensed_string = re.sub( number, str( self.data[ "numbers" ][ number ] ), condensed_string ) + + for scale in self.data[ "scales" ]: + condensed_string = re.sub( "_" + scale, " " + self.data[ "scales" ][ scale ], condensed_string) + + condensed_string = condensed_string.split( '_' ) + for chunk_index in range( 0, len( condensed_string ) ): + value = "" + + try: + value = str( eval( condensed_string[ chunk_index ] ) ) + + condensed_string[ chunk_index ] = value + except: + pass + + for chunk_index in range( 0, len( condensed_string ) ): + if self.is_integer( condensed_string[ chunk_index ] ) or self.is_float( condensed_string[ chunk_index ] ): + i = 1 + start_index = chunk_index + end_index = -1 + while( chunk_index + i < len( condensed_string ) and ( self.is_integer( condensed_string[ chunk_index + i ] ) or self.is_float( condensed_string[ chunk_index + i ] ) ) ): + end_index = chunk_index + i + i += 1 + + for sub_chunk in range( start_index, end_index ): + condensed_string[ sub_chunk ] += " +" + + condensed_string[ start_index ] = "( " + condensed_string[ start_index ] + condensed_string[ end_index ] += " )" + + return ' '.join( condensed_string ) diff --git a/chatterbot/adapters/plugins/plugin.py b/chatterbot/adapters/plugins/plugin.py new file mode 100644 index 000000000..556d2b4b6 --- /dev/null +++ b/chatterbot/adapters/plugins/plugin.py @@ -0,0 +1,17 @@ +from chatterbot.adapters.exceptions import AdapterNotImplementedError + + +class PluginAdapter(object): + """ + This is an abstract class that represents the interface + that all plugins should implement. + """ + + def __init__(self, **kwargs): + pass + + def process(self, text): + raise AdapterNotImplementedError() + + def should_answer(self, text): + raise AdapterNotImplementedError() diff --git a/chatterbot/adapters/plugins/plugin_chooser.py b/chatterbot/adapters/plugins/plugin_chooser.py new file mode 100644 index 000000000..75ff611a8 --- /dev/null +++ b/chatterbot/adapters/plugins/plugin_chooser.py @@ -0,0 +1,28 @@ +from .evaluate_mathematically import EvaluateMathematically + +class PluginChooser(): + + def __init__( self, **kwargs ): + """ + Initializes all plugins & initial variables. + """ + + self.plugins = [ + EvaluateMathematically(**kwargs) + ] + + + def choose( self, input_statement ): + """ + Used to determine whether a plugin should be used + to "answer" or reply to the user input. + """ + + # Testing each plugin to determine whether it should be used to answer user input + for plugin in self.plugins: + # If it should, get the response and return that + if plugin.should_answer( input_statement.text ): + return plugin.process( input_statement.text ) + + # Otherwise, return that no plugin was found that should respond + return False diff --git a/chatterbot/chatterbot.py b/chatterbot/chatterbot.py index 847775dda..e4f805b83 100644 --- a/chatterbot/chatterbot.py +++ b/chatterbot/chatterbot.py @@ -19,6 +19,9 @@ def __init__(self, name, **kwargs): "chatterbot.adapters.io.TerminalAdapter" ) + PluginChooser = import_module("chatterbot.adapters.plugins.PluginChooser") + self.plugin_chooser = PluginChooser(**kwargs) + StorageAdapter = import_module(storage_adapter) self.storage = StorageAdapter(**kwargs) @@ -80,6 +83,12 @@ def get_response(self, input_text): """ input_statement = Statement(input_text) + # Applying plugin logic to see whether the chatbot should respond in this way + plugin_response = self.plugin_chooser.choose( input_statement ) + + if not plugin_response is False: + return self.io.process_response( Statement( plugin_response ) ) + # If no responses exist, return the input statement if not self.storage.count(): self.storage.update(input_statement) @@ -152,4 +161,3 @@ def train(self, conversation=None, *args, **kwargs): self.trainer.train_from_corpora(corpora) else: self.trainer.train_from_list(conversation) - diff --git a/tests/base_case.py b/tests/base_case.py index ba5d2870c..cc993ea74 100644 --- a/tests/base_case.py +++ b/tests/base_case.py @@ -70,4 +70,3 @@ def setUp(self): self.chatbot.train(data1) self.chatbot.train(data2) self.chatbot.train(data3) - diff --git a/tests/logic_adapter_tests/test_closest_meaning.py b/tests/logic_adapter_tests/test_closest_meaning.py index 4512ca2e1..9c5f16a57 100644 --- a/tests/logic_adapter_tests/test_closest_meaning.py +++ b/tests/logic_adapter_tests/test_closest_meaning.py @@ -27,4 +27,3 @@ def test_get_closest_statement(self): close = self.adapter.get(statement, possible_choices) self.assertEqual("This is a lovely bog.", close) - diff --git a/tests/test_chatbot_output.py b/tests/test_chatbot_output.py index c75146310..10f608ccf 100644 --- a/tests/test_chatbot_output.py +++ b/tests/test_chatbot_output.py @@ -177,6 +177,27 @@ def test_second_response_format(self): self.assertEqual(len(statement_object.in_response_to), 1) self.assertIn("Hi", statement_object.in_response_to) + def test_evaluate_mathematically(self): + self.chatbot.storage.update(self.test_statement) + + response = self.chatbot.get_response("What is 100 + 54?") + second_response = self.chatbot.get_response("What is 100 * 20") + third_response = self.chatbot.get_response("What is 100 + ( 1000 * 2 )?") + fourth_response = self.chatbot.get_response("What is four plus 100 + ( 100 * 2 )?") + fifth_response = self.chatbot.get_response("What is one hundred + four hundred?") + sixth_response = self.chatbot.get_response("What is 100 divided by 100?") + seventh_response = self.chatbot.get_response("What is one thousand two hundred four divided by one hundred?") + eighth_response = self.chatbot.get_response("What is -100.5 * 20?") + ninth_response = self.chatbot.get_response("What is -105 * 5") + + self.assertEqual(response, "( 100 + 54 ) = 154") + self.assertEqual(second_response, "( 100 * 20 ) = 2000") + self.assertEqual(third_response, "( 100 + ( ( 1000 * ( 2 ) ) ) ) = 2100") + self.assertEqual(fourth_response, "( 4 + ( 100 + ( ( 100 * ( 2 ) ) ) ) ) = 304") + self.assertEqual(fifth_response, "( 100 + 400 ) = 500") + self.assertEqual(eighth_response, "( -100.5 * 20 ) = -2010.0") + self.assertEqual(ninth_response, "( -105 * 5 ) = -525") + class ChatterBotStorageIntegrationTests(UntrainedChatBotTestCase): @@ -207,4 +228,3 @@ def test_database_is_not_updated_when_read_only(self): self.assertFalse(exists_before) self.assertFalse(exists_after) -