diff --git a/.idea/jarvis-server-v2.iml b/.idea/jarvis-server-v2.iml
index dc6ae03..8b60aca 100644
--- a/.idea/jarvis-server-v2.iml
+++ b/.idea/jarvis-server-v2.iml
@@ -10,6 +10,11 @@
+
+
+
+
+
diff --git a/jarvis/api.py b/jarvis/api.py
index fc1df0e..1f39f56 100644
--- a/jarvis/api.py
+++ b/jarvis/api.py
@@ -4,6 +4,8 @@ import tempfile
import requests
from flask import request, Flask
+from jarvis.skills import intent_manager
+
app = Flask(__name__)
@@ -16,7 +18,22 @@ def process_audio_request_android():
audio_temp_file.write(request.data)
print(audio_temp_file.name)
- return {"transcription": text_recognition_whisperasr(audio_temp_file.name), "answer": "WIP"}
+ text = text_recognition_whisperasr(audio_temp_file.name)
+
+ # TODO: send to each skill to answer the questions
+
+ return {"transcription": text, "answer": "I'm still learning how to respond to that..."}
+
+
+@app.route("/process_text", methods=['POST'])
+def process_text():
+ print("[" + request.remote_addr + "] - New TXT request")
+
+ text = request.values['text']
+
+ answer = intent_manager.recognise(text, request.headers.get('Client-Ip'), request.headers.get('Client-Port'))
+
+ return {"transcription": text, "answer": answer}
# send request to whisper-asr server (docker)
diff --git a/jarvis/get_path_file.py b/jarvis/get_path_file.py
new file mode 100644
index 0000000..e69de29
diff --git a/jarvis/skills/__init__.py b/jarvis/skills/__init__.py
new file mode 100644
index 0000000..a02553c
--- /dev/null
+++ b/jarvis/skills/__init__.py
@@ -0,0 +1,159 @@
+import glob
+import os
+import random
+import threading
+import types
+
+from .. import get_path_file
+from ..skills import intent_manager
+from ..utils import languages_utils
+
+
+class Skill:
+ def __init__(self, name, data, required_config: list = None):
+ if required_config is not None:
+ self.required_config = required_config
+
+ self.name = name
+
+ self.client_ip = data['client_ip']
+ self.client_port = data['client_port']
+
+ path = self.__module__.split(".")
+ self.category = path[2]
+ self.skill_folder = path[3]
+
+ self.path = os.path.dirname(get_path_file.__file__) + "/skills/" + self.category + "/" + self.skill_folder
+
+ if self.has_required_config():
+ self.on_load()
+
+ def on_load(self):
+ pass
+
+ def speak(self, sentence):
+ # TODO: implement
+ print(sentence)
+ # client_utils.speak(sentence, self.client_ip, self.client_port)
+
+ def speak_dialog(self, dialog, data=None):
+ if data is None:
+ data = {}
+
+ file = self.path + "/dialog/" + languages_utils.get_language() + "/" + dialog + ".dialog"
+ random_line = get_random_line_from_file(file)
+
+ for key, val in data.items():
+ if "{{" + key + "}}" in random_line or "{" + key + "}" in random_line:
+ random_line = random_line.replace("{{" + key + "}}", val)
+ random_line = random_line.replace("{" + key + "}", val)
+
+ self.speak(random_line)
+
+ return "Error, dialog not found for : " + dialog
+
+ def speak_dialog_threaded(self, dialog, data=None):
+ thread = threading.Thread(target=self.speak_dialog, args=[dialog, data])
+ thread.start()
+
+ def register(self):
+ if self.has_required_config():
+ self.register_entities_adapt()
+ self.register_regex()
+ print("[" + self.name + "] Registered entity/entities and regex(s)")
+ else:
+ print("[WARN] Skipped skill " + self.name + " : please check your config.")
+
+ def register_entities_adapt(self):
+ path = self.path + "/vocab/" + languages_utils.get_language() + "/*.voc"
+
+ all_lines_by_file_dict = get_lines_of_all_files_in_path(path, return_as_dict_with_filename=True)
+
+ for filename in all_lines_by_file_dict:
+ for line in all_lines_by_file_dict.get(filename):
+ intent_manager.register_entity_adapt(line, filename, self.name)
+
+ def register_regex(self):
+ path = self.path + "/regex/" + languages_utils.get_language() + "/*.rx"
+
+ result = get_lines_of_all_files_in_path(path)
+ for line in result:
+ intent_manager.register_regex_adapt(line, self.name)
+
+ def has_required_config(self):
+ if hasattr(self, "required_config") and self.required_config is not None:
+ for field in self.required_config:
+ # TODO: implement
+ # if config_utils.get_in_config(field) is None:
+ return False
+ return True
+
+
+def get_random_line_from_file(filepath):
+ if os.path.exists(filepath):
+ with open(filepath, "r") as infile:
+ random_line = random.choice(infile.readlines())
+ infile.close()
+ return random_line
+ else:
+ print("File " + filepath + " doesn't exist...")
+
+
+def get_all_lines_from_file(filepath):
+ if os.path.exists(filepath):
+ with open(file=filepath, mode="r") as infile:
+ lines = []
+
+ for line in infile.readlines():
+ lines.append(line.removesuffix('\n'))
+
+ infile.close()
+
+ return lines
+ else:
+ print("File " + filepath + " doesn't exist...")
+
+
+def get_lines_of_all_files_in_path(path, return_as_dict_with_filename=False):
+ files = glob.glob(path, recursive=True)
+ result = dict()
+ result_list = []
+
+ for file in files:
+ lines = get_all_lines_from_file(file)
+
+ if return_as_dict_with_filename:
+ filename = file.split("/")[-1].split('.')[0]
+ result[filename] = lines
+ else:
+ result_list.extend(lines)
+
+ if return_as_dict_with_filename:
+ return result
+
+ return result_list
+
+
+def get_array_for_intent_file(filename, category, skill_folder):
+ path = os.path.dirname(get_path_file.__file__) + "/skills/" + category + "/" + skill_folder
+ path = path + "/vocab/" + languages_utils.get_language() + "/" + filename
+
+ return get_all_lines_from_file(path)
+
+
+class SkillRegistering(type):
+ def __init__(cls, name, bases, attrs):
+ for key, val in attrs.items():
+ if type(val) is types.FunctionType and not str(val).__contains__("__"):
+ intent_type = getattr(val, "_type", None)
+
+ if intent_type is not None:
+ properties = getattr(val, "_data", None)
+
+ if properties is not None:
+ if intent_type == 'adapt':
+ intent = properties[0]
+ intent_name = intent.name
+
+ intent_manager.intents_handlers_adapt[f"{intent_name}"] = [getattr(cls, key), name, key,
+ attrs['__module__']]
diff --git a/jarvis/skills/decorators.py b/jarvis/skills/decorators.py
new file mode 100644
index 0000000..4d349a5
--- /dev/null
+++ b/jarvis/skills/decorators.py
@@ -0,0 +1,26 @@
+def intent_handler(*args):
+ """
+ Creates an attribute on the method, so it can
+ be discovered by the metaclass
+ """
+
+ def decorator(f):
+ f._type = "adapt"
+ f._data = args
+ return f
+
+ return decorator
+
+
+def intent_file_handler(*args):
+ """
+ Creates an attribute on the method, so it can
+ be discovered by the metaclass
+ """
+
+ def decorator(f):
+ f._type = "padatious"
+ f._data = args
+ return f
+
+ return decorator
diff --git a/jarvis/skills/entertainement/jokes/__init__.py b/jarvis/skills/entertainement/jokes/__init__.py
new file mode 100644
index 0000000..0994469
--- /dev/null
+++ b/jarvis/skills/entertainement/jokes/__init__.py
@@ -0,0 +1,38 @@
+from jarvis.skills import Skill, SkillRegistering
+from jarvis.skills.decorators import intent_file_handler
+from jarvis.skills.entertainement.jokes.providers import jokes_french_provider, jokes_english_provider
+from jarvis.utils import languages_utils
+
+
+class JokesSkill(Skill, metaclass=SkillRegistering):
+ def __init__(self, data=dict):
+ super().__init__("JokesSkill", data)
+
+ @intent_file_handler("tell_a_joke.intent", "TellAJokeIntent")
+ def handle_joke(self, data):
+ self.speak(get_joke(False))
+
+
+def get_joke(nsfw, lang=languages_utils.get_language()):
+ """
+ Returns a joke in the good language
+
+ Args:
+ lang: use language from config by default, you can specify a custom language here
+ nsfw: should include nsfw jokes
+
+ Returns:
+ array
+ """
+ return "really nice joke"
+
+ if lang.startswith("fr-"):
+ return jokes_french_provider.get_joke(nsfw)
+ elif lang.startswith("en-"):
+ return jokes_english_provider.get_joke(nsfw)
+ else:
+ return ['Error', "I don't know any jokes in your language..."]
+
+
+def create_skill(data):
+ return JokesSkill(data)
diff --git a/jarvis/skills/entertainement/jokes/providers/__init__.py b/jarvis/skills/entertainement/jokes/providers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/jarvis/skills/entertainement/jokes/providers/jokes_english_provider.py b/jarvis/skills/entertainement/jokes/providers/jokes_english_provider.py
new file mode 100644
index 0000000..6b7987b
--- /dev/null
+++ b/jarvis/skills/entertainement/jokes/providers/jokes_english_provider.py
@@ -0,0 +1,25 @@
+import requests
+
+
+def get_joke(nsfw=False):
+ """
+ Returns a joke in 2 parts
+
+ Args:
+ nsfw: include nsfw jokes?
+
+ Returns: array
+ """
+
+ url = 'https://v2.jokeapi.dev/joke/Any?blacklistFlags=nsfw,religious,political,racist,sexist,explicit&type=twopart'
+
+ if nsfw:
+ url = "https://v2.jokeapi.dev/joke/Any?type=twopart"
+
+ response = requests.get(url)
+
+ data = response.json()
+ joke = data['setup']
+ answer = data['delivery']
+
+ return [joke, answer]
diff --git a/jarvis/skills/entertainement/jokes/providers/jokes_french_provider.py b/jarvis/skills/entertainement/jokes/providers/jokes_french_provider.py
new file mode 100644
index 0000000..a7acb38
--- /dev/null
+++ b/jarvis/skills/entertainement/jokes/providers/jokes_french_provider.py
@@ -0,0 +1,27 @@
+import requests
+
+
+def get_joke(nsfw=False):
+ """
+ Returns a joke in 2 parts
+
+ Args:
+ nsfw: include nsfw jokes?
+
+ Returns: array
+ """
+
+ url = 'https://www.blagues-api.fr/api/random'
+
+ if nsfw:
+ url = url + "?disallow=dark&disallow=limit"
+
+ # please register on www.blagues-api.fr and set a token in your secrets file
+ response = requests.get(url, headers={
+ 'Authorization': 'Bearer ' + """ config_utils.get_in_secret('JOKES_FRENCH_API_TOKEN') """})
+
+ data = response.json()
+ joke = data['joke']
+ answer = data['answer']
+
+ return [joke, answer]
diff --git a/jarvis/skills/entertainement/jokes/vocab/fr-fr/tell_a_joke.intent b/jarvis/skills/entertainement/jokes/vocab/fr-fr/tell_a_joke.intent
new file mode 100644
index 0000000..52b6160
--- /dev/null
+++ b/jarvis/skills/entertainement/jokes/vocab/fr-fr/tell_a_joke.intent
@@ -0,0 +1,3 @@
+raconte (moi|nous|) (voir|) une blague
+fais (moi|nous) rire
+illumine (ma|notre) journée
diff --git a/jarvis/skills/intent_manager.py b/jarvis/skills/intent_manager.py
new file mode 100644
index 0000000..a1adf0c
--- /dev/null
+++ b/jarvis/skills/intent_manager.py
@@ -0,0 +1,119 @@
+import importlib
+
+from adapt.engine import DomainIntentDeterminationEngine
+
+adapt_engine = DomainIntentDeterminationEngine()
+
+intents_handlers_adapt = dict()
+intents_handlers_padatious = dict()
+
+
+def register_entity_adapt(entity_value, entity_type, domain):
+ adapt_engine.register_entity(entity_value=entity_value, entity_type=entity_type, domain=domain)
+ # print("[Adapt]: Added entity with type " + entity_type + " for " + domain)
+
+
+def register_regex_adapt(regex, domain):
+ adapt_engine.register_regex_entity(regex, domain)
+ # print("[Adapt]: Added new regex for " + domain)
+
+
+def register_intent_adapt(intent, domain):
+ adapt_engine.register_intent_parser(intent, domain=domain)
+ print("[Adapt]: Registered new intent " + intent.name + " for skill " + domain + ".")
+
+
+def load_all_skills():
+ for handler in intents_handlers_adapt:
+ function_handler = intents_handlers_adapt.get(handler)
+ intent_builder = getattr(function_handler[0], "_data", [])[0]
+ skill_name = function_handler[1]
+ register_intent_adapt(intent_builder.build(), domain=skill_name)
+
+
+def handle(intent_name, data):
+ module_path_str = None
+ handler_method_name = None
+
+ if intent_name in intents_handlers_adapt:
+ handler_method_name = intents_handlers_adapt.get(intent_name)[2]
+ module_path_str = intents_handlers_adapt.get(intent_name)[3]
+
+ if module_path_str is not None and handler_method_name is not None:
+ # import the create_skill method from the skill using the skill module path
+ create_skill_method = import_method_from_string(module_path_str, "create_skill")
+
+ skill_init_data = {'client_ip': data['client_ip'], 'client_port': data['client_port']}
+
+ # create a new object of the right skill for the utterance
+ skill = create_skill_method(skill_init_data)
+
+ # import and call the handler method from the skill
+ getattr(skill, handler_method_name)(data=data)
+
+
+def import_method_from_string(file, method_name):
+ """
+ Add the possibility to import method dynamically using a string like "skills.daily.date_and_time.intent" as file and
+ "what_time_is_it" as method_name
+ """
+ mod = importlib.import_module(file)
+ met = getattr(mod, method_name)
+
+ return met
+
+
+def recognise(sentence, client_ip=None, client_port=None):
+ sentence = sentence.lower()
+ print(sentence)
+
+ data = dict()
+ data['client_ip'] = client_ip
+ data['client_port'] = client_port
+ data['utterance'] = sentence
+
+ best_intent_adapt = get_best_intent_adapt(sentence)
+
+ confidence_adapt = get_confidence(best_intent_adapt)
+
+ if confidence_adapt < 0.2:
+
+ # TODO: implement fallback to something like wolfram alpha or smart assistant
+ # Nothing found at all
+ return "I didn't understand..."
+ else:
+ return handle_adapt_intent(data, best_intent_adapt)
+
+
+def get_confidence(intent):
+ if intent is None:
+ return 0
+
+ if 'confidence' in intent:
+ return intent['confidence']
+ elif hasattr(intent, 'conf'):
+ return intent.conf
+ else:
+ return 0
+
+
+def get_best_intent_adapt(sentence):
+ if len(intents_handlers_adapt) > 0:
+ try:
+ best_intents = adapt_engine.determine_intent(sentence, 100)
+ best_intent = next(best_intents)
+
+ return best_intent
+
+ except StopIteration:
+ pass
+
+ return None # No match (Adapt)
+
+
+def handle_adapt_intent(data, intent):
+ for key, val in intent.items():
+ if key != 'intent_type' and key != 'target' and key != 'confidence':
+ data[key] = val
+ handle(intent['intent_type'], data=data)
+ return intent
diff --git a/jarvis/start.py b/jarvis/start.py
index f9f9a69..df996af 100644
--- a/jarvis/start.py
+++ b/jarvis/start.py
@@ -1,4 +1,15 @@
import api
+import lingua_franca
+
+from jarvis.skills.entertainement.jokes import JokesSkill
if __name__ == '__main__':
+
+ # Load lingua franca in the memory
+ lingua_franca.load_language(lang="fr")
+
+ # Load skills
+ JokesSkill().register()
+
+ # Start the api endpoint
api.start_server()
diff --git a/jarvis/utils/__init__.py b/jarvis/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/jarvis/utils/languages_utils.py b/jarvis/utils/languages_utils.py
new file mode 100644
index 0000000..b3a2cfa
--- /dev/null
+++ b/jarvis/utils/languages_utils.py
@@ -0,0 +1,2 @@
+def get_language():
+ return "fr"
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 4b217fb..ef36775 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,4 @@
requests~=2.28.1
Flask~=2.2.2
+adapt-parser==1.0.0
+lingua-franca~=0.4.3