Browse Source

Merge pull request #28 from kalliope-project/dev

Dev
Monf 8 years ago
parent
commit
1b0e4ef24b

+ 62 - 9
Docs/rest_api.md

@@ -4,17 +4,18 @@ Kalliope provides the REST API to manage the synapses. For configuring the API r
 
 ## Synapse API
 
-| Method | URL                      | Action               |
-|--------|--------------------------|----------------------|
-| GET    | /synapses                | List synapses        |
-| GET    | /synapses/<synapse_name> | Show synapse details |
-| POST   | /synapses/<synapse_name> | Run a synapse        |
+| Method | URL                      | Action                      |
+|--------|--------------------------|-----------------------------|
+| GET    | /synapses                | List synapses               |
+| GET    | /synapses/<synapse_name> | Show synapse details        |
+| POST   | /synapses/<synapse_name> | Run a synapse by its name   |
+| POST   | /order                   | Run a synapse from an order |
 
 ## Curl examples
 
 >**Note:** --user is only needed if `password_protected` is True
 
-### Get all synapse
+### List synapses
 
 Normal response codes: 200
 Error response codes: unauthorized(401), itemNotFound(404)
@@ -68,7 +69,7 @@ Output example:
 }
 ```
 
-### Get one synapse's detail by its name. 
+### Show synapse details 
 
 Normal response codes: 200
 Error response codes: unauthorized(401), itemNotFound(404)
@@ -100,7 +101,7 @@ Output example:
 }
 ```
 
-### Run a synapse by its name. 
+### Run a synapse by its name
 
 Normal response codes: 201
 Error response codes: unauthorized(401), itemNotFound(404)
@@ -130,4 +131,56 @@ Output example:
     ]
   }
 }
-```
+```
+
+
+Run a synapse from an order
+
+Normal response codes: 201
+Error response codes: unauthorized(401), itemNotFound(404)
+
+Curl command:
+```
+curl -i --user admin:secret -H "Content-Type: application/json" -X POST -d '{"order":"my order"}' http://localhost:5000/order
+```
+
+If the order contains accent or quotes, use a file for testing with curl
+```
+cat post.json 
+{"order":"j'aime"}
+```
+Then
+```
+curl -i --user admin:secret -H "Content-Type: application/json" -X POST --data @post.json http://localhost:5000/order/
+```
+
+Output example if the order have matched and so launched synapses:
+```
+{
+  "synapses": [
+    {
+      "name": "Say-hello", 
+      "neurons": [
+        {
+          "name": "say", 
+          "parameters": "{'message': ['Hello sir']}"
+        }
+      ], 
+      "signals": [
+        {
+          "order": "hello"
+        }
+      ]
+    }
+  ]
+}
+```
+
+If the order haven't match ny synapses:
+```
+{
+  "error": {
+    "error": "The given order doesn't match any synapses"
+  }
+}
+```

+ 1 - 1
README.md

@@ -43,7 +43,7 @@ Kalliope is easy-peasy to use, see the hello world
 | [STT](Docs/stt.md)                 | Speech to text configuration                                                                |
 | [TTS](Docs/tts.md)                 | Text to speech configuration                                                                |
 | [Triggers](Docs/trigger.md)        | Magic hotword engine used to make Kalliope listtening for an order                          |
-
+| [REST API](Docs/rest_api.md)       | Integrated REST API. Can be used to send an order                                           |
 
 ## Contributing
 

+ 1 - 0
core/ConfigurationManager/BrainLoader.py

@@ -38,6 +38,7 @@ class BrainLoader(object):
         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:

+ 2 - 2
core/CrontabManager.py

@@ -19,9 +19,9 @@ KALLIOPE_ENTRY_POINT_SCRIPT = "kalliope.py"
 
 class CrontabManager:
 
-    def __init__(self, brain_file=None):
+    def __init__(self, brain=None):
         self.my_user_cron = CronTab(user=True)
-        self.brain = BrainLoader.get_brain(file_path=brain_file)
+        self.brain = brain
         self.base_command = self._get_base_command()
 
     def load_events_in_crontab(self):

+ 3 - 9
core/MainController.py

@@ -18,22 +18,16 @@ logger = logging.getLogger("kalliope")
 
 
 class MainController:
-    def __init__(self, brain_file=None):
-        self.brain_file = brain_file
+    def __init__(self, brain=None):
+        self.brain = brain
         # get global configuration
         self.settings = SettingLoader.get_settings()
 
-        # load the brain
-        if brain_file is None:
-            self.brain = BrainLoader.get_brain()
-        else:
-            self.brain = BrainLoader.get_brain(file_path=brain_file)
-
         # run the api if the user want it
         if self.settings.rest_api.active:
             Utils.print_info("Starting REST API Listening port: %s" % self.settings.rest_api.port)
             app = Flask(__name__)
-            flask_api = FlaskAPI(app, port=self.settings.rest_api.port, brain_file=brain_file)
+            flask_api = FlaskAPI(app, port=self.settings.rest_api.port, brain=self.brain)
             flask_api.start()
 
         # create an order listener object. This last will the trigger callback before starting

+ 2 - 1
core/Models/Brain.py

@@ -1,5 +1,6 @@
 
 class Brain(object):
-    def __init__(self, synapses=None, brain_file=None):
+    def __init__(self, synapses=None, brain_file=None, brain_yaml=None):
         self.synapses = synapses
         self.brain_file = brain_file
+        self.brain_yaml = brain_yaml

+ 5 - 0
core/Models/Event.py

@@ -5,3 +5,8 @@ class Event(object):
     def __str__(self):
         return "%s: period: %s" % (self.__class__.__name__,
                                    self.period)
+
+    def serialize(self):
+        return {
+            'event': self.period
+        }

+ 6 - 0
core/Models/Neuron.py

@@ -4,3 +4,9 @@ class Neuron(object):
     def __init__(self, name=None, parameters=None):
         self.name = name
         self.parameters = parameters
+
+    def serialize(self):
+        return {
+            'name': self.name,
+            'parameters': str(self.parameters)
+        }

+ 5 - 0
core/Models/Order.py

@@ -4,3 +4,8 @@ class Order(object):
 
     def __str__(self):
         return "%s: Sentence: %s" % (self.__class__.__name__, self.sentence)
+
+    def serialize(self):
+        return {
+            'order': self.sentence
+        }

+ 7 - 0
core/Models/Synapse.py

@@ -3,3 +3,10 @@ class Synapse(object):
         self.name = name
         self.neurons = neurons
         self.signals = signals
+
+    def serialize(self):
+        return {
+            'name': self.name,
+            'neurons': [e.serialize() for e in self.neurons],
+            'signals': [e.serialize() for e in self.signals]
+        }

+ 6 - 0
core/OrderAnalyser.py

@@ -30,10 +30,13 @@ class OrderAnalyser:
     def start(self):
         synapses_found = False
         problem_in_neuron_found = False
+        # create a dict of synapses that have benn launched
+        launched_synapses = list()
         for synapse in self.brain.synapses:
             for signal in synapse.signals:
                 if type(signal) == Order:
                     if self._spelt_order_match_brain_order_via_table(signal.sentence, self.order):
+                        launched_synapses.append(synapse)
                         synapses_found = True
                         logger.debug("Order found! Run neurons: %s" % synapse.neurons)
                         Utils.print_success("Order matched in the brain. Running synapse \"%s\"" % synapse.name)
@@ -77,6 +80,9 @@ class OrderAnalyser:
         if not synapses_found:
             Utils.print_info("No synapse match the captured order: %s" % self.order)
 
+        # return the list of launched synapse
+        return launched_synapses
+
     def _associate_order_params_to_values(self, order_to_check):
         """
         Associate the variables from the order to the incoming user order

+ 54 - 7
core/RestAPI/FlaskAPI.py

@@ -1,23 +1,32 @@
 import threading
 
 from flask import jsonify
+from flask import request
+from flask_restful import abort
 
-from core.ConfigurationManager.BrainLoader import BrainLoader
+from core import OrderAnalyser
 from core.RestAPI.utils import requires_auth
 from core.SynapseLauncher import SynapseLauncher
 
 
 class FlaskAPI(threading.Thread):
-    def __init__(self, app, port=5000, brain_file=None):
+    def __init__(self, app, port=5000, brain=None):
+        """
+
+        :param app: Flask API
+        :param port: Port to listen
+        :param brain: Brain object
+        :type brain: Brain
+        """
         super(FlaskAPI, self).__init__()
         self.app = app
         self.port = port
-        self.brain_file = brain_file
-        self.brain_yaml = BrainLoader.get_yaml_config(file_path=self.brain_file)
+        self.brain = brain
 
         self.app.add_url_rule('/synapses/', view_func=self.get_synapses, methods=['GET'])
         self.app.add_url_rule('/synapses/<synapse_name>', view_func=self.get_synapse, methods=['GET'])
         self.app.add_url_rule('/synapses/<synapse_name>', view_func=self.run_synapse, methods=['POST'])
+        self.app.add_url_rule('/order/', view_func=self.run_order, methods=['POST'])
 
     def run(self):
         self.app.run(host='0.0.0.0', port="%s" % int(self.port), debug=True, threaded=True, use_reloader=False)
@@ -28,7 +37,7 @@ class FlaskAPI(threading.Thread):
         :param synapse_name:
         :return:
         """
-        all_synapse = self.brain_yaml
+        all_synapse = self.brain.brain_yaml
         for el in all_synapse:
             print el
             if el[0]["name"] in synapse_name:
@@ -40,7 +49,7 @@ class FlaskAPI(threading.Thread):
         """
         get all synapse
         """
-        data = jsonify(synapses=self.brain_yaml)
+        data = jsonify(synapses=self.brain.brain_yaml)
         return data, 200
 
     @requires_auth
@@ -76,6 +85,44 @@ class FlaskAPI(threading.Thread):
             return jsonify(error=data), 404
 
         # run the synapse
-        SynapseLauncher.start_synapse(synapse_name, brain_file=self.brain_file)
+        SynapseLauncher.start_synapse(synapse_name, brain=self.brain)
         data = jsonify(synapses=synapse_target)
         return data, 201
+
+    @requires_auth
+    def run_order(self):
+        """
+        Give an order to Kalliope via API like it was from a spoken one
+        Test with curl
+        curl -i --user admin:secret -H "Content-Type: application/json" -X POST -d '{"order":"my order"}' http://localhost:5000/order
+        In case of quotes in the order or accents, use a file
+        cat post.json:
+        {"order":"j'aime"}
+        curl -i --user admin:secret -H "Content-Type: application/json" -X POST --data @post.json http://localhost:5000/order/
+        :return:
+        """
+        if not request.get_json() or 'order' not in request.get_json():
+            abort(400)
+
+        order = request.get_json('order')
+        if order is not None:
+            # get the order
+            order_to_run = order["order"]
+            oa = OrderAnalyser(order=order_to_run, brain=self.brain)
+            launched_synapses = oa.start()
+
+            if launched_synapses:
+                # if the list is not empty, we have launched one or more synapses
+                data = jsonify(synapses=[e.serialize() for e in launched_synapses])
+                return data, 201
+            else:
+                data = {
+                    "error": "The given order doesn't match any synapses"
+                }
+                return jsonify(error=data), 400
+
+        else:
+            data = {
+                "error": "order cannot be null"
+            }
+            return jsonify(error=data), 400

+ 8 - 11
core/ShellGui.py

@@ -1,18 +1,16 @@
 # coding: utf8
 
+import locale
 import logging
-
+import signal
 import sys
 
-import signal
 from dialog import Dialog
-import locale
 
 from core import OrderListener
-from core.ConfigurationManager.BrainLoader import BrainLoader
+from core.ConfigurationManager import SettingLoader
 from core.SynapseLauncher import SynapseLauncher
 from core.Utils import Utils
-from core.ConfigurationManager import SettingLoader
 from neurons import Say
 
 logging.basicConfig()
@@ -28,9 +26,10 @@ signal.signal(signal.SIGINT, signal_handler)
 
 
 class ShellGui:
-    def __init__(self, brain_file=None):
+    def __init__(self, brain=None):
         # override brain
-        self.brain_file = brain_file
+        self.brain = brain
+
         # get settings
         self.settings = SettingLoader.get_settings()
         locale.setlocale(locale.LC_ALL, '')
@@ -155,13 +154,11 @@ class ShellGui:
         Show a list of available synapse in the brain to run it directly
         :return:
         """
-        # get the list of synapse from the brain
-        brain = BrainLoader.get_brain(file_path=self.brain_file)
 
         # create a tuple for the list menu
         choices = list()
         x = 0
-        for el in brain.synapses:
+        for el in self.brain.synapses:
             tup = (str(el.name), str(x))
             choices.append(tup)
             x += 1
@@ -173,5 +170,5 @@ class ShellGui:
             self.show_main_menu()
         if code == self.d.OK:
             logger.debug("Run synapse from GUI: %s" % tag)
-            SynapseLauncher.start_synapse(tag, brain_file=self.brain_file)
+            SynapseLauncher.start_synapse(tag, brain=self.brain)
             self.show_synapses_test_menu()

+ 4 - 7
core/SynapseLauncher.py

@@ -12,22 +12,19 @@ class SynapseLauncher(object):
         pass
 
     @classmethod
-    def start_synapse(cls, name, brain_file=None):
+    def start_synapse(cls, name, brain=None):
         """
         Start a synapse by it's name
         :param name: Name (Unique ID) of the synapse to launch
-        :param brain_file: Brain file path to load instead of the default one
+        :param brain: Brain instance
         """
         synapse_name_launch = name
         # get the brain
-        if brain_file is None:
-            brain = BrainLoader.get_brain()
-        else:
-            brain = BrainLoader.get_brain(file_path=brain_file)
+        cls.brain = brain
 
         # check if we have found and launched the synapse
         synapse_launched = False
-        for synapse in brain.synapses:
+        for synapse in cls.brain.synapses:
             if synapse.name == synapse_name_launch:
                 cls._run_synapse(synapse)
                 synapse_launched = True

+ 1 - 0
install/files/python_requirements.txt

@@ -13,4 +13,5 @@ ipaddress==1.0.16
 pyowm==2.5.0
 python-twitter==3.1
 flask==0.11.1
+Flask-Restful==0.3.5
 wikipedia==1.4.0

+ 10 - 7
kalliope.py

@@ -5,6 +5,7 @@ import logging
 
 from core import ShellGui
 from core import Utils
+from core.ConfigurationManager.BrainLoader import BrainLoader
 from core.CrontabManager import CrontabManager
 from core.MainController import MainController
 import signal
@@ -49,6 +50,11 @@ def main():
 
     # by default, no brain file is set. Use the default one: brain.yml in the root path
     brain_file = None
+    # check if user set a brain.yml file
+    if args.brain_file:
+        brain_file = args.brain_file
+    # load the brain once
+    brain = BrainLoader.get_brain(file_path=brain_file)
 
     # check the user provide a valid action
     if args.action not in ACTION_LIST:
@@ -56,17 +62,14 @@ def main():
         parser.print_help()
 
     if args.action == "start":
-        # check if user set a brain.yml file
-        if args.brain_file:
-            brain_file = args.brain_file
 
         # user set a synapse to start
         if args.run_synapse is not None:
-            SynapseLauncher.start_synapse(args.run_synapse, brain_file=brain_file)
+            SynapseLauncher.start_synapse(args.run_synapse, brain=brain)
 
         if args.run_synapse is None:
             # first, load events in crontab
-            crontab_manager = CrontabManager(brain_file=brain_file)
+            crontab_manager = CrontabManager(brain=brain)
             crontab_manager.load_events_in_crontab()
             Utils.print_success("Events loaded in crontab")
             # then start kalliope
@@ -75,10 +78,10 @@ def main():
             # catch signal for killing on Ctrl+C pressed
             signal.signal(signal.SIGINT, signal_handler)
             # start the main controller
-            MainController(brain_file=brain_file)
+            MainController(brain=brain)
 
     if args.action == "gui":
-        ShellGui(args.brain_file)
+        ShellGui(brain=brain)
 
 
 def configure_logging(debug=None):

+ 10 - 34
test.py

@@ -1,55 +1,31 @@
 # 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
 
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
 logger.setLevel(logging.DEBUG)
 
-# order = "quelle heure est-il"
-# oa = OrderAnalyser(order=order)
-# oa.start()
-
-
 
 brain = BrainLoader.get_brain()
+#
+# order = "bonjour"
+# oa = OrderAnalyser(order=order, brain=brain)
+# oa.start()
 
-order = "bonjour"
-
-oa = OrderAnalyser(order=order, brain=brain)
-
-oa.start()
 
 
-# settings = SettingLoader.get_settings()
-#
-# tts_name_to_use = "pico2wave"
-# sentence_to_say = "bonjour monsieur, je m'appelle Kalliopé"
-#
-#
-# def _get_tts_object_from_name(tts_name_to_use):
-#     """
-#     Return a Tts object from the nae of the Tss. Get parameters in settings
-#     :param tts_name_to_use:
-#     :return:
-#     """
-#     return next((x for x in settings.ttss if x.name == tts_name_to_use), None)
-#
-#
-# # create a tts object from the tts the user want to user
-# tts_object = _get_tts_object_from_name(tts_name_to_use)
-#
-# if tts_object is None:
-#     print "TTS module name %s not found in settings" % tts_name_to_use
-#
-# else:
-#     tts_module_instance = Utils.get_dynamic_class_instantiation("tts", tts_object.name.capitalize(), tts_object.parameters)
-#     tts_module_instance.say(sentence_to_say)
+app = Flask(__name__)
+flask_api = FlaskAPI(app, port=5000, brain=brain)
+flask_api.start()