Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add evaluate_mathematically #80

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -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~
3 changes: 3 additions & 0 deletions chatterbot/adapters/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .plugin import PluginAdapter
from .evaluate_mathematically import EvaluateMathematically
from .plugin_chooser import PluginChooser
46 changes: 46 additions & 0 deletions chatterbot/adapters/plugins/data/math_words_EN.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
185 changes: 185 additions & 0 deletions chatterbot/adapters/plugins/evaluate_mathematically.py
Original file line number Diff line number Diff line change
@@ -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 ) + ' '
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be simplified to:

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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exceptions might happen here that you are trying to catch?

It's generally recommended to avoid Pokemon exception handling, especially when you know what errors should be raised.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am catching all exceptions that rise from attempting to evaluate a non-numerical statement. Because I go through all chunks (including those that are not math expressions) I need to catch all exceptions that might be thrown.

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 )
17 changes: 17 additions & 0 deletions chatterbot/adapters/plugins/plugin.py
Original file line number Diff line number Diff line change
@@ -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()
28 changes: 28 additions & 0 deletions chatterbot/adapters/plugins/plugin_chooser.py
Original file line number Diff line number Diff line change
@@ -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
10 changes: 9 additions & 1 deletion chatterbot/chatterbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -152,4 +161,3 @@ def train(self, conversation=None, *args, **kwargs):
self.trainer.train_from_corpora(corpora)
else:
self.trainer.train_from_list(conversation)

1 change: 0 additions & 1 deletion tests/base_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,3 @@ def setUp(self):
self.chatbot.train(data1)
self.chatbot.train(data2)
self.chatbot.train(data3)

1 change: 0 additions & 1 deletion tests/logic_adapter_tests/test_closest_meaning.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

22 changes: 21 additions & 1 deletion tests/test_chatbot_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down Expand Up @@ -207,4 +228,3 @@ def test_database_is_not_updated_when_read_only(self):

self.assertFalse(exists_before)
self.assertFalse(exists_after)