Exemplo n.º 1
0
class TestResponseBuilderGraph(unittest.TestCase):

    def setUp(self):
        self.sampleconversationtrees = {
            1: {123: set([124, 126]), 124: set([125]), 125: set([]),
                126: set([127, 128]), 127: set([]), 128: set([129]), 129: set([])},
            2: {130: set([131, 132]), 131: set([133, 134]), 132: set([135]),
                133: set([]), 134: set([]), 135: set([])}}
        self.rbg = ResponseBuilderGraph()
        # self.rbg.setconversations(self.sampleconversationtrees)
        self.sd = Sampledata()
        self.messages = self.sd.getgraphmessages()

    def test_is_child(self):
        self.assertTrue(self.rbg.is_child(1, 123, 124))
        self.assertTrue(self.rbg.is_child(1, 124, 125))
        self.assertTrue(self.rbg.is_child(2, 132, 135))
        self.assertFalse(self.rbg.is_child(1, 124, 123))
        self.assertFalse(self.rbg.is_child(1, 125, 123))
        self.assertFalse(self.rbg.is_child(2, 999, 123))

    def test_add_node(self):
        self.rbg.add_node(1, 234)
        self.assertTrue(type(self.rbg.conversationtrees[1][234]) == Set)
        self.rbg.add_node(1, 123)
        self.assertTrue(self.rbg.conversationtrees[1][123] ==
                        set([124, 126]))

    def test_remove_node(self):
        remove_result = {123: set([124, 126]), 124: set([125]), 125: set([])}
        self.rbg.remove_node(1, 126)
        self.assertEqual(self.rbg.conversationtrees[1], remove_result)

        remove_result = {123: set([124, 126])}
        self.rbg.remove_node(1, 124)
        self.assertEqual(self.rbg.conversationtrees[1], remove_result)

        self.rbg.remove_node(1, 999)
        self.rbg.remove_node(999, 1)
        self.assertEqual(self.rbg.conversationtrees[1], remove_result)

    def test_remove_edge(self):
        remove_edge_result = {
            123: set([126]), 124: set([125]), 125: set([]),
            126: set([127, 128]), 127: set([]), 128: set([129]), 129: set([])}
        self.rbg.remove_edge(1, 124)
        self.assertEqual(self.rbg.conversationtrees[1], remove_edge_result)

        self.rbg.remove_edge(1, 'DOESNT EXIST')  # result should not change
        self.assertEqual(self.rbg.conversationtrees[1], remove_edge_result)

    def test_add_edge(self):
        add_edge_result = {128: set([129]), 129: set([]), 123: set([124, 125, 126]),
        124: set([125]), 125: set([]), 126: set([128, 127]), 127: set([])}
        self.rbg.add_edge(1, 123, 125)
        self.assertEqual(self.rbg.conversationtrees[1], add_edge_result)

        self.rbg.add_edge(1, 123, 124)  # already exists, result should not change
        self.assertEqual(self.rbg.conversationtrees[1], add_edge_result)

    def test_buildconversationtrees(self):
        convtreesresult = {
            1: {123: set([124, 126]), 124: set([125]), 125: set([]),
                126: set([127, 128]), 127: set([]), 128: set([129]), 129: set([])},
            2: {130: set([131, 132]), 131: set([133, 134]), 132: set([135]),
                133: set([]), 134: set([]), 135: set([])}}
        self.assertEqual(self.rbg.buildconversationtrees(self.messages), convtreesresult)

    def test_getrootnodes(self):
        rootnodes = {1: 123, 2: 130}
        self.assertEqual(self.rbg.getrootnodes(self.messages), rootnodes)

    def test_getchildnodes(self):
        self.assertEqual(self.rbg.getchildnodes(1, 123), set([124, 126]))
        self.assertEqual(self.rbg.getchildnodes(1, 999), None)
        self.assertEqual(self.rbg.getchildnodes(2, 132), set([135]))
        self.assertEqual(self.rbg.getchildnodes(999, 1), None)

    def test_getfollowupnodes(self):
        sampleconvstates = self.sd.getsampleconversationstates()
        self.rbg.conversationstates = sampleconvstates
        self.assertEqual(self.rbg.getfollowupnodes('bob'),
                         {1: set([125]), 2: set([133, 134])})
        self.assertEqual(self.rbg.getfollowupnodes('hank'),
                         {1: set([]), 2: None})
        self.assertEqual(self.rbg.getfollowupnodes('ann'),
                         {999: None})
        self.assertEqual(self.rbg.getfollowupnodes('999'),
                         {})

    def test_getresponseformatchingmessages(self):
        messageone = MessageEntity('bob', 'Hi')
        messagetwo = MessageEntity('bob', 'Not great')
        messagetree = MessageEntity('bob', 'Feeling tired')
        messagefour = MessageEntity('bob', 'I will!')
        messagefive = MessageEntity('bob', 'Good!')
        messagesix = MessageEntity('bob', '')
        messageseven = MessageEntity('bob', 'BLABLABLA999')
        messageeight = MessageEntity('bob', '130')
        messagenine = MessageEntity('bob', '135')

        # first question in a conversation
        self.assertEqual(self.rbg.getresponseformessages(messageone),
                         [{'responseText': 'Hi! :) How are you?'}])
        # the followup question
        self.assertEqual(self.rbg.getresponseformessages(messagetwo),
                         [{'responseText': 'How come?'}])
        # a repeated message should not receive another reply
        self.assertFalse(self.rbg.getresponseformessages(messagetwo))
        # the followup question after a repeated message should
        self.assertEqual(self.rbg.getresponseformessages(messagetree),
                         [{'responseText': 'Aww. Get some sleep!'}])
        # the final message in the chain
        self.assertEqual(self.rbg.getresponseformessages(messagefour),
                         [{'responseText': 'Good night!'}])
        # the second question repeated
        self.assertFalse(self.rbg.getresponseformessages(messagefive))
        # testing messages that do not occur
        self.assertFalse(self.rbg.getresponseformessages(messagesix))
        self.assertFalse(self.rbg.getresponseformessages(messageseven))
        # first message in another conversation
        self.assertEqual(self.rbg.getresponseformessages(messageeight),
                         [{'responseText': '130'}])
        self.assertEqual(self.rbg.getresponseformessages(messageone),
                         [{'responseText': 'Hi! :) How are you?'}])
        # parent messages have not yet been answered
        self.assertFalse(self.rbg.getresponseformessages(messagenine))

    # tests slightly different messages that do not match letter for letter
    def test_getresponseforpartialmatchingmessages(self):
        messageone = MessageEntity('bob', 'Hi how are you today!')
        messagetwo = MessageEntity('bob', 'Not great QQQ')
        # first question in a conversation
        self.assertEqual(self.rbg.getresponseformessages(messageone),
                         [{'responseText': 'Hi! :) How are you?'}])
        # the followup question
        self.assertEqual(self.rbg.getresponseformessages(messagetwo),
                         [{'responseText': 'How come?'}])

    def test_updateconversationstate(self):
        from datetime import time, tzinfo, datetime, timedelta
        self.rbg.updateconversationstate('bob', 1, 1)
        self.assertEqual(self.rbg.conversationstates['bob'][1]['mostrecentquestion'], 1)
        self.assertIsInstance(self.rbg.conversationstates['bob'][1]['mostrecentinteraction'], datetime)
        oldtimestamp = self.rbg.conversationstates['bob'][1]['mostrecentinteraction']
        self.rbg.updateconversationstate('bob', 1, 999)
        self.assertEqual(self.rbg.conversationstates['bob'][1]['mostrecentquestion'], 999)
        self.assertNotEqual(oldtimestamp, self.rbg.conversationstates['bob'][1]['mostrecentinteraction'])

    # test if conversationstate is correctly updated after incoming an message
    def test_updatedconvstateaftermessage(self):
        sampleconvstates = self.sd.getsampleconversationstates()
        self.rbg.conversationstates = sampleconvstates
        # valid example, should update conv state
        message = MessageEntity('bob', 'Good!')
        self.rbg.getresponseformessages(message)
        self.assertEqual(self.rbg.conversationstates['bob'][1]['mostrecentquestion'], 124)
        # invalid example, should not update conv state
        message = MessageEntity('bob', 'I will!')
        self.rbg.getresponseformessages(message)
        self.assertEqual(self.rbg.conversationstates['bob'][1]['mostrecentquestion'], 124)
        # invalid example, should not update conv state
        message = MessageEntity('bob', '999')
        self.rbg.getresponseformessages(message)
        self.assertEqual(self.rbg.conversationstates['bob'][1]['mostrecentquestion'], 124)

    def test_reinitialize(self):
        sampleconvstates = self.sd.getsampleconversationstates()
        self.rbg.conversationstates = sampleconvstates
        self.rbg.reinitialize('bob')
        self.assertEqual(self.rbg.conversationstates['bob'], {})

        self.rbg.conversationstates = sampleconvstates
        message = MessageEntity('bob', 'chatreset')
        self.rbg.getresponseformessages(message)
        self.assertEqual(self.rbg.conversationstates['bob'], {})
class ResponseBuilderGraph:

    def __init__(self):
        self.conversationstates = {}
        # TODO: replace getting test messages with DB messages.
        # insert the correct data struct messages into db first
        self.sd = Sampledata()
        self.messages = self.sd.getgraphmessages()
        self.messagesdict = self.createmessagesdictbykey(self.messages)
        self.conversationtrees = self.buildconversationtrees(self.messages)
        self.RESETMESSAGE = 'chatreset'

    # Create a dict representation of all the stored messages for quick
    # lookup and references. messagedict =
    # { msgkey: { entiremessage}, msgkey2: {entiremessage2}, .. }
    def createmessagesdictbykey(self, messages):
        messagedict = {}
        for message in messages:
            messagedict[message['key']] = message
        return messagedict

    def buildconversationtrees(self, messages):
        conversationtrees = {}
        for message in messages:
            conversationtrees.setdefault(message['conv_id'], {})
            conversationtrees[message['conv_id']][message['key']] = set(message['children'])
        return conversationtrees

    def is_child(self, conv_id, parent, child):
        try:
            return child in self.conversationtrees[conv_id][parent]
        except Exception:
            return False

    def add_node(self, conv_id, node_id):
        if node_id in self.conversationtrees[conv_id]:
            return
        else:
            self.conversationtrees[conv_id][node_id] = Set([])

    # will recursively delete a node and all its child components
    # however, it will not delete references to its edges!
    def remove_node(self, conv_id, node):
        if conv_id in self.conversationtrees:
            if node in self.conversationtrees[conv_id]:
                if self.conversationtrees[conv_id][node]:
                    for childnode in self.conversationtrees[conv_id][node]:
                        self.remove_node(conv_id, childnode)
                del self.conversationtrees[conv_id][node]

    # should be called whenever a node is removed. Otherwise, edges are kept
    # in place while the nodes no longer exist
    def remove_edge(self, conv_id, edge):
        for node in self.conversationtrees[conv_id]:
            if edge in self.conversationtrees[conv_id][node]:
                self.conversationtrees[conv_id][node].remove(edge)
                break

    def add_edge(self, conv_id, node, edge):
        if edge in self.conversationtrees[conv_id][node]:
            return
        else:
            self.conversationtrees[conv_id][node].add(edge)

    def getrootnodes(self, messages):
        rootnodes = {}
        for message in messages:
            if message['parent'] == 0:
                rootnodes[message['conv_id']] = message['key']
        return rootnodes

    # The child nodes of the most recently asked question of a user are the
    # msgs that warrant a reply. Function returns:
    # conv_id : set(keys of all childnodes eligible for a reply) }
    def getfollowupnodes(self, messageSender):
        followupnodes = {}
        convstate = self.conversationstates.setdefault(messageSender, {})
        for conv_id in convstate:
            childnodes = self.getchildnodes(conv_id, convstate[conv_id]['mostrecentquestion'])
            if childnodes is not None:
                followupnodes[conv_id] = childnodes
            else:
                followupnodes[conv_id] = None
        return followupnodes

    def getchildnodes(self, conv_id, node):
        if conv_id in self.conversationtrees:
            if node in self.conversationtrees[conv_id]:
                return self.conversationtrees[conv_id][node]
        return None

    # Flattens the eligilbe msgs: the list of rootnodes and the list with sets
    #  of followupnodes. Returns [ msgkey, msgkey, msgkey, ...]
    def geteligiblemessages(self, rootnodes, followupnodes):
        eligiblemessages = []
        for node in rootnodes:
            eligiblemessages.append(rootnodes[node])
        for childnodesets in followupnodes:
            for node in followupnodes[childnodesets]:
                eligiblemessages.append(node)
        return eligiblemessages

    # In order to find whether the incoming message warrants a response,
    # we compare it only to messages that are eligible for a response.
    # PROBLEMATIC: because of the way RE \b works, this has some problems when
    # the sentence ends/begins with special characters. Example:
    # r'\b' + 'Not great...' + r'\b' will not match 'xx Not great... xx'
    # need to test impact of stripping characters before matching, but is nasty
    # workaround
    def getmessagematches(self, incomingmessage, eligiblemessages, messagedict):
        matches = []
        for msgkey in eligiblemessages:
            loweredmessage = messagedict[msgkey]['qtext'].lower()
            if (re.search(r'\b' + loweredmessage + r'\b', incomingmessage)
                         or loweredmessage == incomingmessage):
                matches.append(msgkey)
        return matches

    def updateconversationstate(self, messageSender, conv_id, msgkey):
        self.conversationstates.setdefault(messageSender, {})
        self.conversationstates[messageSender].setdefault(conv_id, {})
        self.conversationstates[messageSender][conv_id] = {'mostrecentinteraction': datetime.utcnow(),
                                                           'mostrecentquestion': msgkey }

    def reinitialize(self, messageSender):
        if messageSender in self.conversationstates:
            self.conversationstates[messageSender] = {}
        return

    def getresponseformessages(self, message):
        returnResponses = []
        messageSender = message.getFrom()
        try:
            messagetext = message.getBody().lower()
        except Exception, e:
            print 'Fail getBody, will not work for Media messages:', e
            return returnResponses

        if messagetext == self.RESETMESSAGE:
            self.reinitialize(messageSender)
            return False

        rootnodes = self.getrootnodes(self.messages)
        followupnodes = self.getfollowupnodes(messageSender)
        eligiblemessages = self.geteligiblemessages(rootnodes, followupnodes)

        escapedmessage = message.replaceEscapedCharacters(messagetext)
        matches = self.getmessagematches(escapedmessage, eligiblemessages, self.messagesdict)

        if matches:
            for match in matches:
                self.updateconversationstate(messageSender, self.messagesdict[match]['conv_id'], match)
                returnResponses.append({'responseText' : self.messagesdict[match]['rtext']})
        return returnResponses