浏览代码

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 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, 
 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
 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.
-```
-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 __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.
 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
 
-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.
 
 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
 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
 ```
-python kalliope.py start
+kalliope start
 ```
 
 ## ARGUMENTS
@@ -19,7 +18,7 @@ Start Kalliope main program
 
 Example of use
 ```
-python kalliope.py start
+kalliope start
 ```
 
 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
 ```
-python kalliope.py gui
+kalliope gui
 ```
 
 ## OPTIONS
@@ -43,7 +42,7 @@ Run a specific synapse from the brain file.
 
 Example of use
 ```
-python kalliope.py start --run-synapse "say hello"
+kalliope start --run-synapse "say-hello"
 ```
 
 ### --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
 ```
-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:
 ```
-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
@@ -67,5 +66,5 @@ Show debug output in the console
 
 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`.
 
 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
@@ -110,7 +111,7 @@ As this is multi-lines, we can put the content in a file and use a `file_templat
 ## Overridable parameters
 
 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
 

+ 32 - 0
Docs/signals.md

@@ -10,6 +10,7 @@ signals:
 
 ## Order
 
+### Simple order
 An **order** signal is a word, or a sentence caught by the microphone and processed by the STT engine.
 
 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
 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
 
 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
 
 [![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)
 

+ 1 - 0
Tests/__init__.py

@@ -8,3 +8,4 @@ from test_settings_loader import TestSettingLoader
 from test_singleton import TestSingleton
 from test_tts_module import TestTTSModule
 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:
             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)
             if tts_object is None:
                 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
 
-        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):
         SynapseLauncher.start_synapse(name=name, brain=self.brain)
@@ -189,17 +193,17 @@ class NeuronModule(object):
             return content_file.read()
 
     @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
-        :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:
         """
-        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
     def get_audio_from_stt(callback):