فهرست منبع

Merge pull request #32 from kalliope-project/dev

Align Master to Dev
Nicolas Marcq 8 سال پیش
والد
کامیت
ee5c97e86c

+ 1 - 1
Docs/neuron_list.md

@@ -11,7 +11,7 @@ A neuron is a module that will perform some actions attached to an order. You ca
 | [push_message](../neurons/push_message/)           | Send a push message to a remote device like Android/iOS/Windows Phone or Chrome browser | push_message      |
 | [say](../neurons/say/)                             | Make Kalliope talk by using TTS                                                         | say               |
 | [script](../neurons/script/)                       | Run an executable script                                                                | script            |
-| [shell](../neurons/command/)                       | Run a shell command                                                                     | shell             |
+| [shell](../neurons/shell/)                         | Run a shell command                                                                     | shell             |
 | [sleep](../neurons/sleep/)                         | Make Kalliope sleep for a while before continuing                                       | sleep             |
 | [systemdate](../neurons/systemdate/)               | Give the local system date and time                                                     | systemdate        |
 | [tasker_autoremote](../neurons/tasker_autoremote/) | Send a message to Android tasker app                                                    | tasker_autoremote |

+ 87 - 5
Docs/neurons.md

@@ -1,7 +1,7 @@
 # Neurons
 
-A neuron is a plugin that performs an action attached to some action. You can use it in to create a synapse.  
-You can add as many neurons as you want to a synapse. The neurons are executed one by one when the input action is triggered.
+A neuron is a plugin that performs a specific action. You use it to create a synapse.
+You can add as many neurons as you want to a synapse. The neurons are executed one by one when the input order is triggered.
 
 ## Usage
 Neurons are declared in the `neurons` section of a synapse in your brain file.
@@ -25,6 +25,89 @@ neurons:
 To know the list of required parameters, check of documentation of the neuron.
 Full list of [available neuron here](neuron_list.md)
 
+## Input values
+
+Neurons require some **parameters** from the synapse declaration to work. Those parameters, also called arguments, can be passed to the neuron in two way:
+- from the neuron declaration
+- from the captured order
+
+From the neuron declaration:
+```
+neurons:
+    - neuron_name:
+        parameter1: "value1"
+        parameter2: "value2"
+```
+
+From the captured order:
+```
+  - name: "run-neuron-with-parameter-in-order"
+    neurons:
+      - neuron_name:
+          parameter1: "value1"
+          parameter2: "value2"
+          args:
+          - parameter3
+    signals:
+      - order: "this is an order with the parameter {{ 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.
+
+
+## Output values
+
+Some neurons will return variables into a dictionary of value. Those values can be used to make your own Kalliope answer through a template.
+The objective of using a template is to let the user choosing what he wants to make Kalliope saying in its own language.
+A template is a text file that contains **variables**, which get replaced with values when the template is evaluated by 
+the [template engine](https://en.wikipedia.org/wiki/Jinja_(template_engine)), and **tags**, which control the logic of the template.
+
+The template engine used in Kalliope is [Jinja2](http://jinja.pocoo.org/docs/dev/).
+
+For example, if we look at the [documentation of the neuron systemedate](../neurons/systemdate), we can see that the neuron will return a dictionary of value like `minute`, `hours` and all other values about the current time on the system where Kalliope is installed.
+
+A simple, that only use **variables**, template would be
+```
+It is {{ hours }} and {{ minutes }} minutes.
+```
+
+Placed in a complete synapse, it looks like the following
+```
+- name: "time"
+    neurons:
+      - systemdate:
+          say_template:
+            - "It is {{ hours }} hours and {{ minutes }} minutes"
+    signals:
+      - order: "what time is it"
+```
+
+Here, we use [variables](http://jinja.pocoo.org/docs/dev/templates/#variables) from the neuron into our template file. Both variables will be interpreted by the template engine. 
+So, what the user will hear is something like `It is 9 hours and 21 minutes`.
+
+We can add some logic to a template with tags. Here a simple example with [a test tag](http://jinja.pocoo.org/docs/dev/templates/#if), that will make Kalliope change the pronounced sentence depending on the current time.
+```
+{% if hours > 8 %}
+    It is late, isn't it?
+{% else %}
+    We still have time
+{% endif %}
+```
+
+As this is multi-lines, we can put the content in a file and use a `file_template` instead of a `say_template` for more clarity.
+```
+- name: "time"
+    neurons:
+      - systemdate:
+          file_template: /path/to/file/template.j2
+    signals:
+      - order: "what time is it"
+```
+
+
+
 ## Overridable parameters
 
 For each neuron, you can override some parameters to use a specific configuration of TTS instead of the default one 
@@ -33,15 +116,14 @@ set in [settings.yml](settings.yml) file.
 ### Cache
 
 You can override the default cache configuration. By default Kalliope uses a cache to save a generated audio from a TTS engine.
-This cache is usefull to not to regenerate often used sentences that are not suppose to be changed very often. For exemple, the following sentence will not change in time, so it's more optimized to generate it once and to keep it in cash:
+This cache is usefull to manage sentences that are not suppose to be changed very often. For exemple, the following sentence will not change in time, so it's more optimized to generate it once and to keep it in cash:
 ```
 - say:
     message:
       - "Hello, sir"
 ```
 
-In some cases, especially when the neuron is based on a template, the generated audio will change at each new call of the neuron and so the usage 
-of a cache is not necessary. The best example of the case like this is the `systemdate` neuron. As the time changes every minute, the generated audio will change too and so, saving the generated audio in the cache is useless. In this case, you can override the cache usage for this neuron:
+In some cases, especially when the neuron is based on a template, the generated audio will change on each new call of the neuron and so the usage of a cache is not necessary. The best example of the case like this is the `systemdate` neuron. As the time changes every minute, the generated audio will change too and so, saving the generated audio in the cache is useless. In this case, you can override the cache usage for this neuron:
 ```
 - systemdate:
     say_template:

+ 32 - 25
core/ConfigurationManager/BrainLoader.py

@@ -34,31 +34,38 @@ class BrainLoader(object):
         :return: Brain object
         :rtype: Brain
         """
-        # get the brain with dict
-        dict_brain = cls.get_yaml_config(file_path)
-        # create a new brain
-        brain = Brain()
-        brain.brain_yaml = dict_brain
-        # create list of Synapse
-        synapses = list()
-        for synapses_dict in dict_brain:
-            if "includes" not in synapses_dict: # we don't need to check includes as it's not a synapse
-                if ConfigurationChecker().check_synape_dict(synapses_dict):
-                    # print "synapses_dict ok"
-                    name = synapses_dict["name"]
-                    neurons = cls._get_neurons(synapses_dict["neurons"])
-                    signals = cls._get_signals(synapses_dict["signals"])
-                    new_synapse = Synapse(name=name, neurons=neurons, signals=signals)
-                    synapses.append(new_synapse)
-        brain.synapses = synapses
-        if file_path is None:
-            brain.brain_file = cls._get_root_brain_path()
-        else:
-            brain.brain_file = file_path
-        # check that no synapse have the same name than another
-        if ConfigurationChecker().check_synapes(synapses):
-            return brain
-        return None
+
+        # Instantiate a brain
+        brain = Brain.Instance()
+        logger.debug("Is brain already loaded ? %r" % brain.is_loaded)
+        if brain.is_loaded is False:
+            # get the brain with dict
+            dict_brain = cls.get_yaml_config(file_path)
+
+            brain.brain_yaml = dict_brain
+            # create list of Synapse
+            synapses = list()
+            for synapses_dict in dict_brain:
+                if "includes" not in synapses_dict: # we don't need to check includes as it's not a synapse
+                    if ConfigurationChecker().check_synape_dict(synapses_dict):
+                        # print "synapses_dict ok"
+                        name = synapses_dict["name"]
+                        neurons = cls._get_neurons(synapses_dict["neurons"])
+                        signals = cls._get_signals(synapses_dict["signals"])
+                        new_synapse = Synapse(name=name, neurons=neurons, signals=signals)
+                        synapses.append(new_synapse)
+            brain.synapses = synapses
+            if file_path is None:
+                brain.brain_file = cls._get_root_brain_path()
+            else:
+                brain.brain_file = file_path
+            # check that no synapse have the same name than another
+            if not ConfigurationChecker().check_synapes(synapses):
+                brain = None
+
+            # The Brain Singleton is loaded
+            brain.is_loaded = True
+        return brain
 
     @staticmethod
     def _get_neurons(neurons_dict):

+ 22 - 1
core/ConfigurationManager/ConfigurationChecker.py

@@ -1,5 +1,7 @@
 import re
 
+from core.Utils import Utils, ModuleNotFoundError
+
 
 class InvalidSynapeName(Exception):
     pass
@@ -67,7 +69,26 @@ class ConfigurationChecker:
 
     @staticmethod
     def check_neuron_dict(neuron_dict):
-        # TODO check that the Neuron plugin exist
+        """
+        Check received neuron dict is valid:
+        - neuron exist
+        :param neuron_dict:
+        :return:
+        """
+        def check_neuron_exist(neuron_name):
+            package_name = "neurons"
+            mod = __import__(package_name, fromlist=[neuron_name])
+            try:
+                getattr(mod, neuron_name)
+            except AttributeError:
+                raise ModuleNotFoundError("The module %s does not exist in package %s" % (neuron_name, package_name))
+            return True
+
+        if isinstance(neuron_dict, dict):
+            for neuron_name in neuron_dict:
+                check_neuron_exist(neuron_name)
+        else:
+            check_neuron_exist(neuron_dict)
         return True
 
     @staticmethod

+ 35 - 24
core/ConfigurationManager/SettingLoader.py

@@ -1,7 +1,8 @@
-from YAMLLoader import YAMLLoader
 import logging
 
+from YAMLLoader import YAMLLoader
 from core.FileManager import FileManager
+from core.Models import Singleton
 from core.Models.RestAPI import RestAPI
 from core.Models.Settings import Settings
 from core.Models.Stt import Stt
@@ -43,29 +44,39 @@ class SettingLoader(object):
         Return a Settings object from settings.yml file
         :return:
         """
-        settings = cls.get_yaml_config(file_path)
-        default_stt_name = cls._get_default_speech_to_text(settings)
-        default_tts_name = cls._get_default_text_to_speech(settings)
-        default_trigger_name = cls._get_default_trigger(settings)
-        stts = cls._get_stts(settings)
-        ttss = cls._get_ttss(settings)
-        triggers = cls._get_triggers(settings)
-        random_wake_up_answers = cls._get_random_wake_up_answers(settings)
-        random_wake_up_sounds = cls._get_random_wake_up_sounds(settings)
-        rest_api = cls._get_rest_api(settings)
-        cache_path = cls._get_cache_path(settings)
-
-        # create a setting object
-        setting_object = Settings(default_stt_name=default_stt_name,
-                                  default_tts_name=default_tts_name,
-                                  default_trigger_name=default_trigger_name,
-                                  stts=stts,
-                                  ttss=ttss,
-                                  triggers=triggers,
-                                  random_wake_up_answers=random_wake_up_answers,
-                                  random_wake_up_sounds=random_wake_up_sounds,
-                                  rest_api=rest_api,
-                                  cache_path=cache_path)
+
+        # create a new setting
+        setting_object = Settings.Instance()
+        logger.debug("Is Settings already loaded ? %r" % setting_object.is_loaded)
+        if setting_object.is_loaded is False:
+
+            # Get the setting parameters
+            settings = cls.get_yaml_config(file_path)
+            default_stt_name = cls._get_default_speech_to_text(settings)
+            default_tts_name = cls._get_default_text_to_speech(settings)
+            default_trigger_name = cls._get_default_trigger(settings)
+            stts = cls._get_stts(settings)
+            ttss = cls._get_ttss(settings)
+            triggers = cls._get_triggers(settings)
+            random_wake_up_answers = cls._get_random_wake_up_answers(settings)
+            random_wake_up_sounds = cls._get_random_wake_up_sounds(settings)
+            rest_api = cls._get_rest_api(settings)
+            cache_path = cls._get_cache_path(settings)
+
+            # Load the setting singleton with the parameters
+            setting_object.default_tts_name=default_tts_name
+            setting_object.default_stt_name=default_stt_name
+            setting_object.default_trigger_name=default_trigger_name
+            setting_object.stts=stts
+            setting_object.ttss=ttss
+            setting_object.triggers=triggers
+            setting_object.random_wake_up_answers=random_wake_up_answers
+            setting_object.random_wake_up_sounds=random_wake_up_sounds
+            setting_object.rest_api=rest_api
+            setting_object.cache_path=cache_path
+            # The Settings Singleton is loaded
+            setting_object.is_loaded = True
+
         return setting_object
 
     @staticmethod

+ 2 - 1
core/ConfigurationManager/__init__.py

@@ -1,2 +1,3 @@
 from YAMLLoader import YAMLLoader
-from .SettingLoader import SettingLoader
+from SettingLoader import SettingLoader
+from BrainLoader import BrainLoader

+ 6 - 1
core/Models/Brain.py

@@ -1,6 +1,11 @@
+from core.Models import Singleton
+
+
+@Singleton
+class Brain:
 
-class Brain(object):
     def __init__(self, synapses=None, brain_file=None, brain_yaml=None):
         self.synapses = synapses
         self.brain_file = brain_file
         self.brain_yaml = brain_yaml
+        self.is_loaded = False

+ 15 - 3
core/Models/Settings.py

@@ -1,9 +1,20 @@
+from core.Models import Singleton
 
 
+@Singleton
 class Settings(object):
-    def __init__(self, default_tts_name=None, default_stt_name=None,
-                 default_trigger_name=None, ttss=None, stts=None,
-                 random_wake_up_answers=None, random_wake_up_sounds=None, triggers=None, rest_api=None, cache_path=None):
+    def __init__(self,
+                 default_tts_name=None,
+                 default_stt_name=None,
+                 default_trigger_name=None,
+                 ttss=None,
+                 stts=None,
+                 random_wake_up_answers=None,
+                 random_wake_up_sounds=None,
+                 triggers=None,
+                 rest_api=None,
+                 cache_path=None):
+
         self.default_tts_name = default_tts_name
         self.default_stt_name = default_stt_name
         self.default_trigger_name = default_trigger_name
@@ -14,3 +25,4 @@ class Settings(object):
         self.triggers = triggers
         self.rest_api = rest_api
         self.cache_path = cache_path
+        self.is_loaded = False

+ 38 - 0
core/Models/Singleton.py

@@ -0,0 +1,38 @@
+class Singleton:
+    """
+    A non-thread-safe helper class to ease implementing singletons.
+    This should be used as a decorator -- not a metaclass -- to the
+    class that should be a singleton.
+
+    The decorated class can define one `__init__` function that
+    takes only the `self` argument. Other than that, there are
+    no restrictions that apply to the decorated class.
+
+    To get the singleton instance, use the `Instance` method. Trying
+    to use `__call__` will result in a `TypeError` being raised.
+
+    Limitations: The decorated class cannot be inherited from.
+
+    """
+
+    def __init__(self, decorated):
+        self._decorated = decorated
+
+    def Instance(self):
+        """
+        Returns the singleton instance. Upon its first call, it creates a
+        new instance of the decorated class and calls its `__init__` method.
+        On all subsequent calls, the already created instance is returned.
+
+        """
+        try:
+            return self._instance
+        except AttributeError:
+            self._instance = self._decorated()
+            return self._instance
+
+    def __call__(self):
+        raise TypeError('Singletons must be accessed through `Instance()`.')
+
+    def __instancecheck__(self, inst):
+        return isinstance(inst, self._decorated)

+ 3 - 1
core/Models/__init__.py

@@ -1,5 +1,7 @@
+from Singleton import Singleton
 from Event import Event
 from Brain import Brain
 from Order import Order
 from Synapse import Synapse
-from Neuron import Neuron
+from Neuron import Neuron
+

+ 6 - 5
core/NeuronModule.py

@@ -9,7 +9,7 @@ from jinja2 import Template
 from core import OrderListener
 from core.SynapseLauncher import SynapseLauncher
 from core.Utils import Utils
-from core.ConfigurationManager import SettingLoader
+from core.ConfigurationManager import SettingLoader, BrainLoader
 
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
@@ -55,6 +55,7 @@ class NeuronModule(object):
         logger.debug("NeuronModule called from class %s with parameters: %s" % (child_name, str(kwargs)))
 
         self.settings = SettingLoader.get_settings()
+        self.brain = BrainLoader.get_brain()
 
         # check if the user has overrider the TTS
         tts = kwargs.get('tts', None)
@@ -156,6 +157,9 @@ class NeuronModule(object):
         # else:
         #     raise NoTemplateException("You must specify a say_template or a file_template")
 
+    def run_synapse_ny_name(self, name):
+        SynapseLauncher.start_synapse(name=name, brain=self.brain)
+
     @staticmethod
     def _get_content_of_file(real_file_template_path):
         with open(real_file_template_path, 'r') as content_file:
@@ -179,7 +183,4 @@ class NeuronModule(object):
         oa = OrderListener(callback=callback)
         oa.start()
 
-    @staticmethod
-    def run_synapse_ny_name(name):
-        # TODO find a way to get the current brain file. NeuronModule doesn't have any ref about it
-        SynapseLauncher.start_synapse(name=name)
+

+ 1 - 0
core/__init__.py

@@ -3,3 +3,4 @@ from core.OrderListener import OrderListener
 from core.ShellGui import ShellGui
 from core.FileManager import FileManager
 from core.Utils import Utils
+

+ 1 - 2
install/files/python_requirements.txt

@@ -1,7 +1,6 @@
 SpeechRecognition==3.4.6
 pyaudio==0.2.9
 ansible==2.1.1.0
-pygame
 python2-pythondialog==3.4.0
 jinja==1.2
 python-crontab==2.1.1
@@ -14,4 +13,4 @@ pyowm==2.5.0
 python-twitter==3.1
 flask==0.11.1
 Flask-Restful==0.3.5
-wikipedia==1.4.0
+wikipedia==1.4.0

+ 75 - 0
neurons/ansible_playbook/README.md

@@ -0,0 +1,75 @@
+# ansible_playbook
+
+## Synopsis
+
+Run an Ansible playbook. Ansible is a free-software platform for configuring and managing computers which combines multi-node software deployment, ad hoc task execution, and configuration management.
+
+Playbooks are Ansible’s configuration, deployment, and orchestration language. They can describe a policy you want your remote systems to enforce, or a set of steps in a general IT process.
+
+This neuron can be used to perform complex operation with all [modules available from Ansible](http://docs.ansible.com/ansible/modules.html).
+
+
+## Options
+
+| parameter | required | default | choices | comment                                      |
+|-----------|----------|---------|---------|----------------------------------------------|
+| task_file | YES      |         |         | path to the Playbook file that contain tasks |
+
+
+
+## Synapses example
+
+Call the playbook named playbook.yml
+```
+   - name: "Ansible-test"
+    neurons:
+      - ansible_playbook: "playbook.yml"
+      - say:
+          message: "Tache terminée"
+    signals:
+      - order: "playbook"
+```
+
+Content of the playbook. This playbook will use the [URI module](http://docs.ansible.com/ansible/uri_module.html) to interact with a webservice on a remote server.
+```
+---
+- name: Playbook
+  hosts: localhost
+  gather_facts: no
+  connection: local
+
+  tasks:   
+    - name: "Call api"
+      uri:
+          url: "http://192.168.0.17:8000/app"
+          HEADER_Content-Type: "application/json"
+          method: POST
+          user: admin
+          password: secret
+          force_basic_auth: yes
+          status_code: 201
+          body_format: json
+          body: >
+            {"app_name": "music", "state": "start"}
+```
+
+
+## Note
+
+Ansible contain a lot of modules that can be useful for Kalliope
+
+- [Notification](http://docs.ansible.com/ansible/list_of_notification_modules.html): can be used to send a message to Pushbullet, IRC channel, Rocket Chat and a lot of other notification services
+- [Files](http://docs.ansible.com/ansible/list_of_files_modules.html): can be used to perform a backup or synchronize two file path
+- [Windows](http://docs.ansible.com/ansible/list_of_windows_modules.html): Can be used to control a Windows Desktop
+
+Shell neuron or script neuron can perform same actions. Ansible is just a way to simplify some execution or enjoy some [already made plugin](http://docs.ansible.com/ansible/modules_by_category.html). 
+
+Here is the example of synapse you would use to perform a call to a web service without Ansible:
+```
+- name: "start-music"
+    neurons:
+      - shell:
+          cmd: "curl -i --user admin:secret -H \"Content-Type: application/json\" -X POST -d '{\"app_name\":\"music\",\"state\":\"start\"}' http://192.168.0.17:8000/app"      
+    signals:
+      - order: "start music rock"
+```

+ 29 - 0
neurons/gmail_checker/README.md

@@ -34,6 +34,35 @@ Simple example :
       - order: "Do I have emails"
 ```
 
+A complex example that read subject emails. This is based on a file_template
+```
+  - name: "check-email"
+    neurons:
+      - gmail_checker:
+          username: "me@gmail.com"
+          password: "my_password"
+          file_template: /templates/my_email_template.j2            
+    signals:
+      - order: "Do I have emails"
+```
+
+Here the content of the `my_email_template.j2`
+```
+You have {{ unread }} email
 
+{% set count = 1 %}
+{% if unread > 0 %}
+    {% for subject in subjects %}
+     email number {{ count }}. {{ subject }}
+     {% set count = count + 1 %}
+    {% endfor %}
+{% endif %}
+```
 ## Notes
 
+Gmail now prevent some mailbox to be accessed from tier application. If you receive a mail like the following:
+```
+Sign-in attempt prevented ... Someone just tried to sign in to your Google Account mail@gmail.com from an app that doesn't meet modern security standards.
+```
+
+You can allow this neuron to get un access to your email in your [Gmail account settings](https://www.google.com/settings/security/lesssecureapps).

+ 16 - 15
test.py

@@ -1,14 +1,8 @@
 # coding: utf8
 import logging
 
-from flask import Flask
-
-from core import OrderAnalyser
-from core import Utils
-from core.ConfigurationManager import SettingLoader
 from core.ConfigurationManager.BrainLoader import BrainLoader
-from core.Players import Mplayer
-from core.RestAPI.FlaskAPI import FlaskAPI
+from core.ConfigurationManager.SettingLoader import SettingLoader
 
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
@@ -16,17 +10,24 @@ logger.setLevel(logging.DEBUG)
 
 
 brain = BrainLoader.get_brain()
-#
-# order = "bonjour"
-# oa = OrderAnalyser(order=order, brain=brain)
-# oa.start()
-
 
+brain2 = BrainLoader.get_brain()
+brain3 = BrainLoader.get_brain()
+brain4 = BrainLoader.get_brain()
 
-app = Flask(__name__)
-flask_api = FlaskAPI(app, port=5000, brain=brain)
-flask_api.start()
 
+print brain is brain2
+print brain is brain3
+print brain is brain4
+print brain4 is brain2
 
+set = SettingLoader.get_settings()
+set2 = SettingLoader.get_settings()
+set3 = SettingLoader.get_settings()
+set4 = SettingLoader.get_settings()
 
+print set is set2
+print set is set3
+print set is set4
+print set3 is set2