Browse Source

LIFO list for synapse list execution added

nico 8 years ago
parent
commit
483d6ea8f0

+ 29 - 0
kalliope/core/Models/APIResponse.py

@@ -0,0 +1,29 @@
+
+
+class APIResponse(object):
+
+    def __init__(self):
+        self.user_order = None
+        self.list_processed_matched_synapse = list()
+        self.status = None
+
+    def __str__(self):
+        returned_string = ""
+        for el in self.list_processed_matched_synapse:
+            returned_string += str(el)
+
+        return returned_string
+
+    def serialize(self):
+        """
+        This method allows to serialize in a proper way this object
+
+        :return: A dict of name and parameters
+        :rtype: Dict
+        """
+
+        return {
+            'user_order': self.user_order,
+            'list_processed_matched_synapse': [e.serialize() for e in self.list_processed_matched_synapse],
+            'status': self.status
+        }

+ 110 - 0
kalliope/core/Models/LIFOBuffer.py

@@ -0,0 +1,110 @@
+import logging
+
+from kalliope.core.NeuronLauncher import NeuronLauncher
+from kalliope.core.Models import Singleton
+from kalliope.core.Models.APIResponse import APIResponse
+
+logging.basicConfig()
+logger = logging.getLogger("kalliope")
+
+
+class LIFOBuffer(object):
+    """
+    This class is a LIFO list of synapse to process where the last synapse list to enter will be the first synapse
+    list to be processed.
+    This design is needed in order to use Kalliope from the API. 
+    Because we want to return an information when a Neuron is still processing and waiting for an answer from the user
+    like with the Neurotransmitter neuron.
+    
+    """
+    __metaclass__ = Singleton
+
+    api_response = APIResponse()
+    lifo_list = list()
+    synapse_list_added_to_lifo = False
+    logger.debug("[LIFOBuffer] LIFO buffer created")
+
+    @classmethod
+    def add_synapse_list_to_lifo(cls, matched_synapse_list):
+        """
+        Add a synapse list to process to the lifo
+        :param matched_synapse_list: List of Matched Synapse
+        :return: 
+        """
+        logger.debug("[LIFOBuffer] Add a new synapse list to process to the LIFO")
+        cls.lifo_list.append(matched_synapse_list)
+
+    @classmethod
+    def execute(cls, answer=None):
+
+        # we keep looping over the LIFO til we have synapse list to process in it
+        while cls.lifo_list:
+            logger.debug("[LIFOBuffer] number of synapse list to process: %s" % len(cls.lifo_list))
+            # if we are back here because of an synapse_list_added_to_lifo switched to true,
+            # we reset it in order to pass through all while loop again and check all synapse list and neuron list
+            cls.synapse_list_added_to_lifo = False
+            # get the last list of matched synapse in the LIFO
+            last_synapse_fifo_list = cls.lifo_list[-1]
+            # we keep processing til we have synapse in the FIFO to process
+            while last_synapse_fifo_list:
+                # a synapse list has been added to the LIFO. we break this loop to go up into the lifo list loop
+                if cls.synapse_list_added_to_lifo:
+                    break
+                # get the first matched synapse in the list
+                matched_synapse = last_synapse_fifo_list[0]
+                # add the synapse to the API response so the user will get the status
+                cls.api_response.list_processed_matched_synapse.append(matched_synapse)
+                # while we have synapse to process in the list of synapse
+                while matched_synapse.neuron_fifo_list:
+                    if cls.synapse_list_added_to_lifo:
+                        break
+                    logger.debug("[LIFOBuffer] number of neuron to process: %s" % len(matched_synapse.neuron_fifo_list))
+                    # get the first neuron in the FIFO neuron list
+                    neuron = matched_synapse.neuron_fifo_list[0]
+                    # from here, we are back into the last neuron we were processing.
+                    if answer is not None:  # we give the answer if exist to the first neuron
+                        neuron.parameters["answer"] = answer
+                        # the next neuron should not get this answer
+                        answer = None
+                    # todo fix this when we have a full client/server call. The client would be the voice or api call
+                    neuron.parameters["is_api_call"] = True
+                    # execute the neuron
+                    instantiated_neuron = NeuronLauncher.start_neuron_list_refacto(
+                        neuron=neuron,
+                        parameters_dict=matched_synapse.parameters)
+
+                    # by default, the status of an execution is "complete" if no neuron are waiting for an answer
+                    cls.api_response.status = "complete"
+                    if not instantiated_neuron.is_waiting_for_answer:  # the neuron is not waiting for an answer
+                        # we add the instantiated neuron to the neuron_module_list.
+                        # This one contains info about generated text
+                        matched_synapse.neuron_module_list.append(instantiated_neuron)
+                        # the neuron is fully processed we can remove it from the list
+                        matched_synapse.neuron_fifo_list.remove(neuron)
+                    else:
+                        print "Wait for answer mode"
+                        cls.api_response.status = "waiting_for_answer"
+                        # we prepare a json response
+                        will_be_returned = cls.api_response.serialize()
+                        # we clean up the API response object for the next call
+                        cls.api_response = APIResponse()
+                        return will_be_returned
+
+                    if instantiated_neuron.pending_synapse:  # the last executed neuron want to run a synapse
+                        # add the synapse to the lifo (inside a list as expected by the lifo)
+                        cls.add_synapse_list_to_lifo([instantiated_neuron.pending_synapse])
+                        # we have added a list of synapse to the LIFO ! this one must start over.
+                        # The following boolean will break all while loop until the execution is back to the LIFO loop
+                        cls.synapse_list_added_to_lifo = True
+
+                # we can only remove the matched synapse from the list if all neuron in it have been executed
+                if not cls.synapse_list_added_to_lifo:
+                    # remove the synapse
+                    last_synapse_fifo_list .remove(matched_synapse)
+
+            # we can only remove the list of synapse from the LIFO if all synapse in it have been executed
+            if not cls.synapse_list_added_to_lifo:
+                # remove the synapse list from the LIFO
+                cls.lifo_list.remove(last_synapse_fifo_list)
+
+        return cls.api_response.serialize()

+ 48 - 0
kalliope/core/Models/MatchedSynapse.py

@@ -0,0 +1,48 @@
+import copy
+
+from kalliope.core.NeuronParameterLoader import NeuronParameterLoader
+
+
+class MatchedSynapse(object):
+    """
+    This class represent a synapse that has matched an order send by an User.
+    """
+
+    def __init__(self, matched_synapse=None, matched_order=None, user_order=None):
+        """
+        
+        :param matched_synapse: The synapse that has matched in the brain
+        :param matched_order: The order from the synapse that have matched
+        """
+        # create a copy of the synapse. the received synapse come from the brain.
+        self.synapse = matched_synapse
+        # create a fifo list that contains all neurons to process
+        self.neuron_fifo_list = self.synapse.neurons
+        self.matched_order = matched_order
+        self.parameters = NeuronParameterLoader.get_parameters(synapse_order=self.matched_order,
+                                                               user_order=user_order)
+        # list of Neuron Module
+        self.neuron_module_list = list()
+
+    def __str__(self):
+        returned_string = str()
+        returned_string += str(self.synapse)
+        returned_string += "answers: "
+        for neuron_module in self.neuron_module_list:
+            returned_string += str(neuron_module)
+
+        return returned_string
+
+    def serialize(self):
+        """
+        This method allows to serialize in a proper way this object
+
+        :return: A dict of name and parameters
+        :rtype: Dict
+        """
+        return {
+            'synapse_name': self.synapse.name,
+            'matched_order': self.matched_order,
+            'neuron_module_list': [e.serialize() for e in self.neuron_module_list]
+        }
+

+ 1 - 5
kalliope/core/Models/Synapse.py

@@ -9,8 +9,6 @@ class Synapse(object):
         self.name = name
         self.neurons = neurons
         self.signals = signals
-        # init a list where generated tts message will be stored after running each neuron in the synapse
-        self.answers = list()
 
     def serialize(self):
         """
@@ -23,8 +21,7 @@ class Synapse(object):
         return {
             'name': self.name,
             'neurons': [e.serialize() for e in self.neurons],
-            'signals': [e.serialize() for e in self.signals],
-            'answers':  str(self.answers)
+            'signals': [e.serialize() for e in self.signals]
         }
 
     def __str__(self):
@@ -35,7 +32,6 @@ class Synapse(object):
         return_val += "\nsignals:"
         for el in self.signals:
             return_val += str(el)
-        return_val += "\nanswers: %s" % self.answers
         return return_val
 
     def __eq__(self, other):

+ 43 - 0
kalliope/core/NeuronLauncher.py

@@ -77,3 +77,46 @@ class NeuronLauncher:
                 Utils.print_danger("A problem has been found in the Synapse.")
 
         return instantiated_neuron
+
+    @classmethod
+    def start_neuron_list_refacto(cls, neuron, parameters_dict=None):
+        """
+        Execute each neuron from the received neuron_list.
+        Replace parameter if exist in the received dict of parameters_dict
+        :param neuron: Neuron object to run
+        :param parameters_dict: dict of parameter to load in each neuron if expecting a parameter
+        :return: List of the instantiated neurons (no errors detected)
+        """
+        instantiated_neuron = None
+        problem_in_neuron_found = False
+        if isinstance(neuron.parameters, dict):
+            # print neuron.parameters
+            if "args" in neuron.parameters:
+                logger.debug("The neuron waits for parameter")
+                # check that the user added parameters to his order
+                if parameters_dict is None:
+                    # we don't raise an error and break the program but we don't run the neuron
+                    problem_in_neuron_found = True
+                    Utils.print_danger("Error: The neuron %s is waiting for argument. "
+                                       "Argument found in bracket in the given order" % neuron.name)
+                else:
+                    # we add wanted arguments the existing neuron parameter dict
+                    for arg in neuron.parameters["args"]:
+                        if arg in parameters_dict:
+                            logger.debug("Parameter %s added to the current parameter "
+                                         "of the neuron: %s" % (arg, neuron.name))
+                            neuron.parameters[arg] = parameters_dict[arg]
+                        else:
+                            # we don't raise an error and break the program but
+                            # we don't run the neuron
+                            problem_in_neuron_found = True
+                            Utils.print_danger("Error: Argument \"%s\" not found in the"
+                                               " order" % arg)
+
+            # if no error detected, we run the neuron
+            if not problem_in_neuron_found:
+                instantiated_neuron = cls.start_neuron(neuron)
+            else:
+                Utils.print_danger("A problem has been found in the Synapse.")
+
+        return instantiated_neuron

+ 34 - 2
kalliope/core/NeuronModule.py

@@ -8,6 +8,7 @@ from jinja2 import Template
 from kalliope.core import OrderListener
 from kalliope.core.ConfigurationManager import SettingLoader, BrainLoader
 from kalliope.core.Models import Order
+from kalliope.core.Models.MatchedSynapse import MatchedSynapse
 from kalliope.core.NeuronLauncher import NeuronLauncher
 from kalliope.core.NeuronParameterLoader import NeuronParameterLoader
 from kalliope.core.OrderAnalyser import OrderAnalyser
@@ -91,6 +92,29 @@ class NeuronModule(object):
         self.file_template = kwargs.get('file_template', None)
         # keep the generated message
         self.tts_message = None
+        # if the current call is api one
+        self.is_api_call = kwargs.get('is_api_call', False)
+        # boolean to know id the synapse is waiting for an answer
+        self.is_waiting_for_answer = False
+        # the synapse name to add the the buffer
+        self.pending_synapse = None
+
+    def __str__(self):
+        retuned_string = ""
+        retuned_string += self.tts_message
+        return retuned_string
+
+    def serialize(self):
+        """
+        This method allows to serialize in a proper way this object
+
+        :return: A dict of name and parameters
+        :rtype: Dict
+        """
+        return {
+            'neuron_name': self.neuron_name,
+            'tts_message': self.tts_message
+        }
 
     def say(self, message):
         """
@@ -122,6 +146,7 @@ class NeuronModule(object):
         if tts_message is not None:
             logger.debug("tts_message to say: %s" % tts_message)
             self.tts_message = tts_message
+            Utils.print_success(tts_message)
 
             # create a tts object from the tts the user want to use
             tts_object = next((x for x in self.settings.ttss if x.name == self.tts), None)
@@ -189,8 +214,15 @@ class NeuronModule(object):
 
         return returned_message
 
-    def run_synapse_by_name(self, name):
-        SynapseLauncher.start_synapse(name=name, brain=self.brain)
+    def run_synapse_by_name(self, synapse_name, user_order=None, synapse_order=None):
+        """
+        call the lifo for adding a synapse to execute in the list of synapse list to process
+        :param synapse_name: 
+        :return: 
+        """
+        synapse = BrainLoader().get_brain().get_synapse_by_name(synapse_name)
+        matched_synapse = MatchedSynapse(matched_synapse=synapse, matched_order=synapse_order, user_order=user_order)
+        self.pending_synapse = matched_synapse
 
     @staticmethod
     def is_order_matching(order_said, order_match):

+ 41 - 1
kalliope/core/SynapseLauncher.py

@@ -1,5 +1,7 @@
 import logging
 
+from kalliope.core.Models.LIFOBuffer import LIFOBuffer
+from kalliope.core.Models.MatchedSynapse import MatchedSynapse
 from kalliope.core.NeuronLauncher import NeuronLauncher
 from kalliope.core.NeuronParameterLoader import NeuronParameterLoader
 from kalliope.core.OrderAnalyser import OrderAnalyser
@@ -83,7 +85,6 @@ class SynapseLauncher(object):
                                                                                 parameters_dict=parameters)
                     # add generated tts messages to the returned synapse
                     for neuron in instantiated_neuron_list:
-                        print "add message %s" % neuron.tts_message
                         tuple_el.synapse.answers.append(neuron.tts_message)
 
                     launched_synapses.append(tuple_el.synapse)
@@ -99,3 +100,42 @@ class SynapseLauncher(object):
 
         # return the launched synapse list
         return launched_synapses
+
+    @classmethod
+    def run_matching_synapse_from_order(cls, order_to_process, brain, settings, is_api_call=False):
+        """
+        
+        :param order_to_process: the spoken order sent by the user
+        :param brain: Brain object
+        :param settings: Settings object
+        :param is_api_call: if True, the current call come from the API. This info must be known by launched Neuron
+        :return: list of matched synapse
+        """
+
+        # get our single ton LIFO
+        lifo_buffer = LIFOBuffer()
+
+        # if the LIFO is not empty, so, the current order is passed to the current processing synapse as an answer
+        if len(lifo_buffer.lifo_list) > 0:
+            # the LIFO is not empty, this is an answer to a previous call
+            return lifo_buffer.execute(answer=order_to_process)
+
+        else:
+            # the LIFO is empty, this is a new call
+
+            # get a tuple of (synapse, order) that match in the brain
+            synapses_to_launch_tuple = OrderAnalyser.get_matching_synapse(order=order_to_process, brain=brain)
+
+            # we transform the tuple in a MatchedSynapse list
+            list_synapse_to_process = list()
+            for tuple_el in synapses_to_launch_tuple:
+                new_matching_synapse = MatchedSynapse(matched_synapse=tuple_el.synapse,
+                                                      matched_order=tuple_el.order,
+                                                      user_order=order_to_process)
+                list_synapse_to_process.append(new_matching_synapse)
+
+            lifo_buffer.add_synapse_list_to_lifo(list_synapse_to_process)
+
+            lifo_buffer.api_response.user_order = order_to_process
+            return lifo_buffer.execute()
+

+ 2 - 1
kalliope/core/Utils/Utils.py

@@ -87,7 +87,8 @@ class Utils(object):
         :return:
         """
         import json
-        pipe_print(json.dumps(to_print, indent=2))
+        line = json.dumps(to_print, indent=2)
+        return line.encode('utf-8')
 
     ##################
     #

+ 15 - 5
kalliope/neurons/neurotransmitter/neurotransmitter.py

@@ -14,6 +14,8 @@ class Neurotransmitter(NeuronModule):
         self.from_answer_link = kwargs.get('from_answer_link', None)
         self.default = kwargs.get('default', None)
         self.direct_link = kwargs.get('direct_link', None)
+        self.is_api_call = kwargs.get('is_api_call', False)
+        self.answer = kwargs.get('answer', None)
 
         # do some check
         if self._is_parameters_ok():
@@ -21,8 +23,14 @@ class Neurotransmitter(NeuronModule):
                 logger.debug("Neurotransmitter directly call to the synapse name: %s" % self.direct_link)
                 self.run_synapse_by_name(self.direct_link)
             else:
-                # the user is using a from_answer_link, we call the stt to get an audio
-                self.get_audio_from_stt(callback=self.callback)
+                if self.is_api_call:
+                    if self.answer is not None:
+                        self.callback(self.answer)
+                    else:
+                        self.is_waiting_for_answer = True
+                else:
+                    # the user is using a from_answer_link, we call the stt to get an audio
+                    self.get_audio_from_stt(callback=self.callback)
 
     def callback(self, audio):
         """
@@ -41,9 +49,11 @@ class Neurotransmitter(NeuronModule):
                 for answer in el["answers"]:
                     if self.is_order_matching(audio, answer):
                         logger.debug("Neurotransmitter: match answer: %s" % answer)
-                        found = self.run_synapse_by_name_with_order(order=audio,
-                                                                    synapse_name=el["synapse"],
-                                                                    order_template=answer)
+                        self.run_synapse_by_name(synapse_name=el["synapse"], user_order=audio, synapse_order=answer)
+                        # found = self.run_synapse_by_name_with_order(order=audio,
+                        #                                             synapse_name=el["synapse"],
+                        #                                             order_template=answer)
+                        found = True
                         break
             if not found:  # the answer do not correspond to any answer. We run the default synapse
                 self.run_synapse_by_name(self.default)

+ 2 - 2
setup.py

@@ -64,8 +64,8 @@ setup(
         'pyasn1>=0.2.3',
         'ansible>=2.2',
         'python2-pythondialog>=3.4.0',
-        'jinja2>=2.8,<2.9',
-        'cffi==1.9.1',
+        'jinja2>=2.8,<=2.9.6',
+        'cffi>=1.9.1',
         'ipaddress>=1.0.17',
         'flask>=0.12',
         'Flask-Restful>=0.3.5',