浏览代码

Merge pull request #133 from kalliope-project/tests_nico

v0.3.0 Release candidate
Monf 8 年之前
父节点
当前提交
15a0db4f33
共有 10 个文件被更改,包括 246 次插入54 次删除
  1. 4 1
      Docs/brain.md
  2. 45 5
      Docs/contributing.md
  3. 8 9
      Docs/kalliope_cli.md
  4. 2 1
      Docs/neurons.md
  5. 32 0
      Docs/signals.md
  6. 2 0
      README.md
  7. 1 0
      Tests/__init__.py
  8. 1 0
      Tests/templates/template_test.j2
  9. 109 0
      Tests/test_neuron_module.py
  10. 42 38
      kalliope/core/NeuronModule.py

+ 4 - 1
Docs/brain.md

@@ -9,7 +9,7 @@ An input action, called a "[signal](signals.md)" can be:
 - **an event:** A date or a frequency (E.G: repeat each morning at 8:30)
 - **an event:** A date or a frequency (E.G: repeat each morning at 8:30)
 
 
 An output action is
 An output action is
-- **a list of neurons:** A [neuron](neurons.md) is a module or plugin that will perform some actions like simply talking, run a script, run a command or a complex Ansible playbook.
+- **a list of neurons:** A [neuron](neurons.md) is a module or plugin that will perform some actions like simply talking, run a script, run a command or call a web service.
 
 
 Brain is expressed in YAML format (see YAML Syntax) and has a minimum of syntax, which intentionally tries to not be a programming language or script, 
 Brain is expressed in YAML format (see YAML Syntax) and has a minimum of syntax, which intentionally tries to not be a programming language or script, 
 but rather a model of a configuration or a process.
 but rather a model of a configuration or a process.
@@ -134,4 +134,7 @@ You can provide a default synapse in case none of them are matching when an orde
 ## Next: Start Kalliope
 ## Next: Start Kalliope
 Now you take a look into the [CLI documentation](kalliope_cli.md) to learn how to start kalliope.
 Now you take a look into the [CLI documentation](kalliope_cli.md) to learn how to start kalliope.
 
 
+## Notes
+- What is a [neuron](neurons.md)
+- What is a [signal](signals.md)
 
 

+ 45 - 5
Docs/contributing.md

@@ -58,10 +58,10 @@ The constructor has a __**kwargs argument__ which is corresponding to the Dict o
     ```
     ```
 
 
 1. You must run unit tests with success before sending a pull request. Add new tests that cover the code you want to publish.
 1. You must run unit tests with success before sending a pull request. Add new tests that cover the code you want to publish.
-```
-cd /path/to/kalliope
-python -m unittest discover
-```
+    ```
+    cd /path/to/kalliope
+    python -m unittest discover
+    ```
 
 
 1. (*optionnal-> good practice*) The Neuron can implement a __private method _is_parameters_ok(self)__ which checks if entries are ok. *return: true if parameters are ok, raise an exception otherwise*
 1. (*optionnal-> good practice*) The Neuron can implement a __private method _is_parameters_ok(self)__ which checks if entries are ok. *return: true if parameters are ok, raise an exception otherwise*
 1. (*optionnal-> good practice*) The Neuron can __import and raise exceptions__ coming from NeuronModule:
 1. (*optionnal-> good practice*) The Neuron can __import and raise exceptions__ coming from NeuronModule:
@@ -71,9 +71,49 @@ python -m unittest discover
 1. The Neuron can use a __self.say(message) method__ to speak out some return values using the *say_template* attribute in the brain file.
 1. The Neuron can use a __self.say(message) method__ to speak out some return values using the *say_template* attribute in the brain file.
 the message variable must be a Dict of variable:values where variables can be defined as output.
 the message variable must be a Dict of variable:values where variables can be defined as output.
 
 
+1. Example of neuron structure
+    ```
+    myneuron/
+    ├── __init__.py
+    ├── myneuron.py
+    ├── README.md
+    └── tests
+        ├── __init__.py
+        └── test_myneuron.py
+    ```
+
+1. Example of neuron code
+    ```
+    class Myneuron(NeuronModule):
+    def __init__(self, **kwargs):
+        super(Myneuron, self).__init__(**kwargs)
+        # the args from the neuron configuration
+        self.arg1 = kwargs.get('arg1', None)
+        self.arg2 = kwargs.get('arg2', None)
+
+        # check if parameters have been provided
+        if self._is_parameters_ok():
+            # -------------------
+            # do amazing code
+            # -------------------            
+
+    def _is_parameters_ok(self):
+        """
+        Check if received parameters are ok to perform operations in the neuron
+        :return: true if parameters are ok, raise an exception otherwise
+
+        .. raises:: MissingParameterException
+        """
+        if self.arg1 is None:
+            raise MissingParameterException("You must specify a arg1")
+        if not isinstance(self.arg2, int):
+            raise MissingParameterException("arg2 must be an integer")
+        return True
+    ```
+
 ##### Constraints
 ##### Constraints
 
 
-1. The Neuron must (as much as possible) ensure the i18n. This means that they should __not manage a specific languages__ inside its own logic.
+1. The Neuron must (as much as possible) ensure the i18n. This means that they should __not manage a specific language__ inside its own logic.
 Only [Synapse](brain.md) by the use of [Order](signals.md) must interact with the languages. This allow a Neuron to by reused by anyone, speaking any language.
 Only [Synapse](brain.md) by the use of [Order](signals.md) must interact with the languages. This allow a Neuron to by reused by anyone, speaking any language.
 
 
 1. Respect [PEP 257](https://www.python.org/dev/peps/pep-0257/) -- Docstring conventions. For each class or method add a description with summary, input parameter, returned parameter,  type of parameter
 1. Respect [PEP 257](https://www.python.org/dev/peps/pep-0257/) -- Docstring conventions. For each class or method add a description with summary, input parameter, returned parameter,  type of parameter

+ 8 - 9
Docs/kalliope_cli.md

@@ -3,13 +3,12 @@
 ## SYNOPSIS
 ## SYNOPSIS
 This is the syntax used to run Kalliope from command line
 This is the syntax used to run Kalliope from command line
 ```
 ```
-cd /path/to/kalliope
-python kalliope.py command --option <argument>
+kalliope command --option <argument>
 ```
 ```
 
 
 For example, to start Kalliope we simply use
 For example, to start Kalliope we simply use
 ```
 ```
-python kalliope.py start
+kalliope start
 ```
 ```
 
 
 ## ARGUMENTS
 ## ARGUMENTS
@@ -19,7 +18,7 @@ Start Kalliope main program
 
 
 Example of use
 Example of use
 ```
 ```
-python kalliope.py start
+kalliope start
 ```
 ```
 
 
 To kill Kalliope, you can press "Ctrl-C" on your keyboard.
 To kill Kalliope, you can press "Ctrl-C" on your keyboard.
@@ -30,7 +29,7 @@ The GUI allows you to test your [STT](stt.md) and [TTS](tts.md) that you have co
 
 
 Example of use
 Example of use
 ```
 ```
-python kalliope.py gui
+kalliope gui
 ```
 ```
 
 
 ## OPTIONS
 ## OPTIONS
@@ -43,7 +42,7 @@ Run a specific synapse from the brain file.
 
 
 Example of use
 Example of use
 ```
 ```
-python kalliope.py start --run-synapse "say hello"
+kalliope start --run-synapse "say-hello"
 ```
 ```
 
 
 ### --brain-file BRAIN_FILE
 ### --brain-file BRAIN_FILE
@@ -53,12 +52,12 @@ Replace the default brain file from the root of the project folder by a custom o
 
 
 Example of use
 Example of use
 ```
 ```
-python kalliope.py start --brain-file /home/me/my_other_brain.yml
+kalliope start --brain-file /home/me/my_other_brain.yml
 ```
 ```
 
 
 You can combine the options together like, for example:
 You can combine the options together like, for example:
 ```
 ```
-python kalliope.py start --run-synapse "say hello" --brain-file /home/me/my_other_brain.yml
+kalliope start --run-synapse "say-hello" --brain-file /home/me/my_other_brain.yml
 ```
 ```
 
 
 ### --debug
 ### --debug
@@ -67,5 +66,5 @@ Show debug output in the console
 
 
 Example of use
 Example of use
 ```
 ```
-python kalliope.py start --debug
+kalliope start --debug
 ```
 ```

+ 2 - 1
Docs/neurons.md

@@ -55,6 +55,7 @@ From the captured order:
 Here, the spoken value captured by the TTS engine will be passed as an argument to the neuron in the variable named `parameter3`.
 Here, the spoken value captured by the TTS engine will be passed as an argument to the neuron in the variable named `parameter3`.
 
 
 Example, with the synapse declaration above, if you say "this is an order with the parameter Amy Winehouse". The neuron will receive a parameter named `parameter3` with "Amy Winehouse" as a value of this parameter.
 Example, with the synapse declaration above, if you say "this is an order with the parameter Amy Winehouse". The neuron will receive a parameter named `parameter3` with "Amy Winehouse" as a value of this parameter.
+We recommend the reading of the [signals documentation](signals.md) for a complete understanding of how arguments in a neuron work.
 
 
 
 
 ## Output values
 ## Output values
@@ -110,7 +111,7 @@ As this is multi-lines, we can put the content in a file and use a `file_templat
 ## Overridable parameters
 ## Overridable parameters
 
 
 For each neuron, you can override some parameters to use a specific configuration of TTS instead of the default one 
 For each neuron, you can override some parameters to use a specific configuration of TTS instead of the default one 
-set in [settings.yml](settings.yml) file.
+set in [settings.yml](settings.md) file.
 
 
 ### Cache
 ### Cache
 
 

+ 32 - 0
Docs/signals.md

@@ -10,6 +10,7 @@ signals:
 
 
 ## Order
 ## Order
 
 
+### Simple order
 An **order** signal is a word, or a sentence caught by the microphone and processed by the STT engine.
 An **order** signal is a word, or a sentence caught by the microphone and processed by the STT engine.
 
 
 Syntax:
 Syntax:
@@ -32,6 +33,37 @@ For example, if you say "Kalliope please do this", the SST engine can return "ca
 > For example, you have "test my umbrella" in a synapse A and "test" in a synapse B. When you'll say "test my umbrella", both synapse A and B
 > For example, you have "test my umbrella" in a synapse A and "test" in a synapse B. When you'll say "test my umbrella", both synapse A and B
 will be started by Kalliope. So keep in mind that the best practice is to use really different sentences with more than one word for your order.
 will be started by Kalliope. So keep in mind that the best practice is to use really different sentences with more than one word for your order.
 
 
+### Order with arguments
+You can add one or more arguments to an order by adding bracket to the sentence.
+
+Syntax:
+```
+signals:
+    - order: "<sentence> {{ arg_name }}"
+    - order: "<sentence> {{ arg_name }} <sentence>"
+    - order: "<sentence> {{ arg_name }} <sentence> {{ arg_name }}"
+```
+
+Example:
+```
+signals:
+    - order: "I want to listen {{ artist_name }}"
+    - order: "start the {{ episode_number }} episode"
+    - order: "give me the weather at {{ location }} for {{ date }}"
+```
+
+Here, an example order would be speaking out loud the order: "I want to listen Amy Winehouse"
+In this example, both word "Amy" and "Winehouse" will be passed as an unique argument called `artist_name` to the neuron.
+
+If you want to send more than one argument, you must split your argument with a word that Kalliope will use to recognise the start and the end of each arguments.
+For example:  "give me the weather at {{ location }} for {{ date }}"
+And the order would be: "give me the weather at Paris for tomorrow"
+And so, it will work too with: "give me the weather at St-Pierre de Chartreuse for tomorrow"
+
+See the **input values** section of the [neuron documentation](neurons) to know how to send arguments to a neuron.
+
+>**Important note:** The following syntax cannot be used: "<sentence> {{ arg_name }} {{ arg_name2 }}" as Kalliope cannot know when a block starts and when it finishes.
+
 ## Event
 ## Event
 
 
 An event is a way to schedule the launching of a synapse periodically at fixed times, dates, or intervals.
 An event is a way to schedule the launching of a synapse periodically at fixed times, dates, or intervals.

+ 2 - 0
README.md

@@ -1,6 +1,8 @@
 # Kalliope
 # Kalliope
 
 
 [![Build Status](https://travis-ci.org/kalliope-project/kalliope.svg)](https://travis-ci.org/kalliope-project/kalliope)
 [![Build Status](https://travis-ci.org/kalliope-project/kalliope.svg)](https://travis-ci.org/kalliope-project/kalliope)
+[![Gitter](https://badges.gitter.im/gitterHQ/gitter.svg)](https://gitter.im/kalliope-project/Lobby)
+
 
 
 ![logo](images/Kalliope_logo_large.png)
 ![logo](images/Kalliope_logo_large.png)
 
 

+ 1 - 0
Tests/__init__.py

@@ -8,3 +8,4 @@ from test_settings_loader import TestSettingLoader
 from test_singleton import TestSingleton
 from test_singleton import TestSingleton
 from test_tts_module import TestTTSModule
 from test_tts_module import TestTTSModule
 from test_yaml_loader import TestYAMLLoader
 from test_yaml_loader import TestYAMLLoader
+from test_neuron_module import TestNeuronModule

+ 1 - 0
Tests/templates/template_test.j2

@@ -0,0 +1 @@
+hello, this is a {{ test }}

+ 109 - 0
Tests/test_neuron_module.py

@@ -0,0 +1,109 @@
+import os
+import unittest
+import mock
+
+from kalliope.core.NeuronModule import NeuronModule, TemplateFileNotFoundException
+
+
+class TestNeuronModule(unittest.TestCase):
+
+    def setUp(self):
+        self.expected_result = "hello, this is a replaced word"
+        # this allow us to run the test from an IDE and from the root with python -m unittest Tests.TestNeuronModule
+        if "/Tests" in os.getcwd():
+            self.file_template = "templates/template_test.j2"
+        else:
+            self.file_template = "Tests/templates/template_test.j2"
+        self.say_template = "hello, this is a {{ test }}"
+        self.message = {
+            "test": "replaced word"
+        }
+        self.neuron_module_test = NeuronModule()
+
+    def tearDown(self):
+        del self.neuron_module_test
+
+    def test_get_audio_from_stt(self):
+        """
+        Test the OrderListener thread is started
+        """
+
+        with mock.patch("kalliope.core.OrderListener.start") as mock_orderListener_start:
+            def callback():
+                pass
+            NeuronModule.get_audio_from_stt(callback=callback())
+            mock_orderListener_start.assert_called_once_with()
+            mock_orderListener_start.reset_mock()
+
+    def test_update_cache_var(self):
+        """
+        Test Update the value of the cache in the provided arg list
+        """
+
+        # True -> False
+        args_dict = {
+            "cache": True
+        }
+        expected_dict = {
+            "cache": False
+        }
+        self.assertEquals(NeuronModule._update_cache_var(False, args_dict=args_dict),
+                          expected_dict,
+                          "Fail to update the cache value from True to False")
+        self.assertFalse(args_dict["cache"])
+
+        # False -> True
+        args_dict = {
+            "cache": False
+        }
+        expected_dict = {
+            "cache": True
+        }
+        self.assertEquals(NeuronModule._update_cache_var(True, args_dict=args_dict),
+                          expected_dict,
+                          "Fail to update the cache value from False to True")
+
+        self.assertTrue(args_dict["cache"])
+
+    def test_get_message_from_dict(self):
+
+        self.neuron_module_test.say_template = self.say_template
+
+        self.assertEqual(self.neuron_module_test._get_message_from_dict(self.message), self.expected_result)
+        del self.neuron_module_test
+        self.neuron_module_test = NeuronModule()
+
+        # test with file_template
+        self.neuron_module_test.file_template = self.file_template
+        self.assertEqual(self.neuron_module_test._get_message_from_dict(self.message), self.expected_result)
+        del self.neuron_module_test
+
+        # test with no say_template and no file_template
+        self.neuron_module_test = NeuronModule()
+        self.assertEqual(self.neuron_module_test._get_message_from_dict(self.message), None)
+
+    def test_get_say_template(self):
+        # test with a string
+        self.assertEqual(NeuronModule._get_say_template(self.say_template, self.message), self.expected_result)
+
+        # test with a list
+        say_template = list()
+        say_template.append("hello, this is a {{ test }} one")
+        say_template.append("hello, this is a {{ test }} two")
+        expected_result = list()
+        expected_result.append("hello, this is a replaced word one")
+        expected_result.append("hello, this is a replaced word two")
+        self.assertTrue(NeuronModule._get_say_template(say_template, self.message) in expected_result)
+
+    def test_get_file_template(self):
+        # test with a valid template
+        self.assertEqual(NeuronModule._get_file_template(self.file_template, self.message), self.expected_result)
+
+        # test raise with a non existing template
+        file_template = "does_not_exist.j2"
+        with self.assertRaises(TemplateFileNotFoundException):
+            NeuronModule._get_file_template(file_template, self.message)
+
+    def test_get_content_of_file(self):
+        expected_result = "hello, this is a {{ test }}"
+        self.assertEqual(NeuronModule._get_content_of_file(self.file_template), expected_result)

+ 42 - 38
kalliope/core/NeuronModule.py

@@ -118,7 +118,7 @@ class NeuronModule(object):
         if tts_message is not None:
         if tts_message is not None:
             logger.debug("tts_message to say: %s" % tts_message)
             logger.debug("tts_message to say: %s" % tts_message)
 
 
-            # create a tts object from the tts the user want to user
+            # 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)
             tts_object = next((x for x in self.settings.ttss if x.name == self.tts), None)
             if tts_object is None:
             if tts_object is None:
                 raise TTSModuleNotFound("The tts module name %s does not exist in settings file" % self.tts)
                 raise TTSModuleNotFound("The tts module name %s does not exist in settings file" % self.tts)
@@ -144,36 +144,40 @@ class NeuronModule(object):
         """
         """
         returned_message = None
         returned_message = None
 
 
-        if (self.say_template is not None and self.file_template is None) or \
-                (self.say_template is None and self.file_template is not None):
-
-            # the user choose a say_template option
-            if self.say_template is not None:
-                if isinstance(self.say_template, list):
-                    # then we pick randomly one template
-                    self.say_template = random.choice(self.say_template)
-                t = Template(self.say_template)
-                returned_message = t.render(**message_dict)
-
-            # trick to remobe unicode problem when loading jinja template with non ascii char
-            reload(sys)
-            sys.setdefaultencoding('utf-8')
-            # the user choose a file_template option
-            if self.file_template is not None:  # the user choose a file_template option
-                real_file_template_path = Utils.get_real_file_path(self.file_template)
-
-                if os.path.isfile(real_file_template_path):
-                    # load the content of the file as template
-                    t = Template(self._get_content_of_file(real_file_template_path))
-                    returned_message = t.render(**message_dict)
-                else:
-                    raise TemplateFileNotFoundException("Template file %s not found in templates folder"
-                                                        % real_file_template_path)
-            return returned_message
-
-        # we don't force the usage of a template. The user can choose to do nothing with returned value
-        # else:
-        #     raise NoTemplateException("You must specify a say_template or a file_template")
+        # the user chooses a say_template option
+        if self.say_template is not None:
+            returned_message = self._get_say_template(self.say_template, message_dict)
+
+        # trick to remove unicode problem when loading jinja template with non ascii char
+        reload(sys)
+        sys.setdefaultencoding('utf-8')
+
+        # the user chooses a file_template option
+        if self.file_template is not None:  # the user choose a file_template option
+            returned_message = self._get_file_template(self.file_template, message_dict)
+
+        return returned_message
+
+    @staticmethod
+    def _get_say_template(list_say_template, message_dict):
+        if isinstance(list_say_template, list):
+            # then we pick randomly one template
+            list_say_template = random.choice(list_say_template)
+        t = Template(list_say_template)
+        return t.render(**message_dict)
+
+    @classmethod
+    def _get_file_template(cls, file_template, message_dict):
+        real_file_template_path = Utils.get_real_file_path(file_template)
+        if real_file_template_path is None:
+            raise TemplateFileNotFoundException("Template file %s not found in templates folder"
+                                                % real_file_template_path)
+
+        # load the content of the file as template
+        t = Template(cls._get_content_of_file(real_file_template_path))
+        returned_message = t.render(**message_dict)
+
+        return returned_message
 
 
     def run_synapse_by_name(self, name):
     def run_synapse_by_name(self, name):
         SynapseLauncher.start_synapse(name=name, brain=self.brain)
         SynapseLauncher.start_synapse(name=name, brain=self.brain)
@@ -189,17 +193,17 @@ class NeuronModule(object):
             return content_file.read()
             return content_file.read()
 
 
     @staticmethod
     @staticmethod
-    def _update_cache_var(new_override_cache, args_list):
+    def _update_cache_var(new_override_cache, args_dict):
         """
         """
         update the value for the key "cache" in the dict args_list
         update the value for the key "cache" in the dict args_list
-        :param new_override_cache: cache bolean to set in place of the current one in args_list
-        :param args_list: arg list that contain "cache" to update
+        :param new_override_cache: cache boolean to set in place of the current one in args_list
+        :param args_dict: arg list that contain "cache" to update
         :return:
         :return:
         """
         """
-        logger.debug("args for TTS plugin before update: %s" % str(args_list))
-        args_list["cache"] = new_override_cache
-        logger.debug("args for TTS plugin after update: %s" % str(args_list))
-        return args_list
+        logger.debug("args for TTS plugin before update: %s" % str(args_dict))
+        args_dict["cache"] = new_override_cache
+        logger.debug("args for TTS plugin after update: %s" % str(args_dict))
+        return args_dict
 
 
     @staticmethod
     @staticmethod
     def get_audio_from_stt(callback):
     def get_audio_from_stt(callback):