diff --git a/.github/workflows/sync_tx.yml b/.github/workflows/sync_tx.yml new file mode 100644 index 0000000..2fd378e --- /dev/null +++ b/.github/workflows/sync_tx.yml @@ -0,0 +1,32 @@ +name: Run script on merge to dev by gitlocalize-app + +on: + workflow_dispatch: + push: + branches: + - dev + +jobs: + run-script: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v2 + with: + ref: dev + fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: 3.9 + + - name: Run script if merged by gitlocalize-app[bot] + if: github.event_name == 'push' && github.event.head_commit.author.username == 'gitlocalize-app[bot]' + run: | + python scripts/sync_translations.py + + - name: Commit to dev + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: Update translations + branch: dev diff --git a/ovos_persona/__init__.py b/ovos_persona/__init__.py index 8e614ea..bed0ad2 100644 --- a/ovos_persona/__init__.py +++ b/ovos_persona/__init__.py @@ -1,6 +1,6 @@ import json import os -from os.path import dirname +from os.path import join, dirname from typing import Optional, Dict, List, Union from ovos_bus_client.client import MessageBusClient @@ -9,10 +9,12 @@ from ovos_config.locations import get_xdg_config_save_path from ovos_plugin_manager.persona import find_persona_plugins from ovos_plugin_manager.solvers import find_question_solver_plugins -from ovos_plugin_manager.templates.pipeline import PipelineStageMatcher, IntentHandlerMatch, ConfidenceMatcherPipeline +from ovos_plugin_manager.templates.pipeline import PipelineStageConfidenceMatcher, IntentHandlerMatch from ovos_utils.fakebus import FakeBus +from ovos_utils.lang import standardize_lang_tag, get_language_dir from ovos_utils.log import LOG from ovos_workshop.app import OVOSAbstractApplication +from padacioso import IntentContainer from ovos_persona.solvers import QuestionSolversService @@ -46,18 +48,58 @@ def chat(self, messages: list = None, lang: str = None) -> str: return self.solvers.spoken_answer(prompt, lang) -class PersonaService(PipelineStageMatcher, OVOSAbstractApplication): +class PersonaService(PipelineStageConfidenceMatcher, OVOSAbstractApplication): + intents = ["ask.intent", "summon.intent"] + intent_matchers = {} + def __init__(self, bus: Optional[Union[MessageBusClient, FakeBus]] = None, config: Optional[Dict] = None): config = config or Configuration().get("persona", {}) OVOSAbstractApplication.__init__( self, bus=bus or FakeBus(), skill_id="persona.openvoiceos", resources_dir=f"{dirname(__file__)}") - PipelineStageMatcher.__init__(self, bus, config) + PipelineStageConfidenceMatcher.__init__(self, bus, config) self.personas = {} self.blacklist = self.config.get("persona_blacklist") or [] self.load_personas(self.config.get("personas_path")) + self.active_persona = None self.add_event('persona:answer', self.handle_persona_answer) + self.add_event('persona:summon', self.handle_persona_summon) + self.add_event('persona:release', self.handle_persona_release) + + @classmethod + def load_resource_files(cls): + intents = {} + langs = Configuration().get('secondary_langs', []) + [Configuration().get('lang', "en-US")] + langs = set([standardize_lang_tag(l) for l in langs]) + for lang in langs: + lang = standardize_lang_tag(lang) + intents[lang] = {} + locale_folder = get_language_dir(join(dirname(__file__), "locale"), lang) + if locale_folder is not None: + for f in os.listdir(locale_folder): + path = join(locale_folder, f) + if f in cls.intents: + with open(path) as intent: + samples = intent.read().split("\n") + for idx, s in enumerate(samples): + samples[idx] = s.replace("{{", "{").replace("}}", "}") + intents[lang][f] = samples + return intents + + @classmethod + def load_intent_files(cls): + intent_files = cls.load_resource_files() + + for lang, intent_data in intent_files.items(): + lang = standardize_lang_tag(lang) + cls.intent_matchers[lang] = IntentContainer() + for intent_name in cls.intents: + samples = intent_data.get(intent_name) + if samples: + LOG.debug(f"registering OCP intent: {intent_name}") + cls.intent_matchers[lang].add_intent( + intent_name.replace(".intent", ""), samples) @property def default_persona(self) -> Optional[str]: @@ -96,15 +138,63 @@ def deregister_persona(self, name): # Chatbot API def chatbox_ask(self, prompt: str, persona: Optional[str] = None, lang: Optional[str] = None) -> Optional[str]: - persona = persona or self.default_persona + persona = persona or self.active_persona or self.default_persona if persona not in self.personas: LOG.error(f"unknown persona, choose one of {self.personas.keys()}") return None messages = [{"role": "user", "content": prompt}] return self.personas[persona].chat(messages, lang) - def match(self, utterances: List[str], lang: Optional[str] = None, message: Optional[Message] = None) -> Optional[IntentHandlerMatch]: + # Abstract methods + def match_high(self, utterances: List[str], lang: Optional[str] = None, + message: Optional[Message] = None) -> Optional[IntentHandlerMatch]: """ + Recommended before common query + + Args: + utterances (list): list of utterances + lang (string): 4 letter ISO language code + message (Message): message to use to generate reply + + Returns: + IntentMatch if handled otherwise None. + """ + match = self.intent_matchers[lang].calc_intent(utterances[0].lower()) + + if match["name"]: + LOG.info(f"Persona exact match: {match}") + persona = match["entities"].pop("persona") + if match["name"] == "summon": + return IntentHandlerMatch(match_type='persona:summon', + match_data={"persona": persona}, + skill_id="persona.openvoiceos", + utterance=utterances[0]) + elif match["name"] == "ask": + utterance = match["entities"].pop("query") + ans = self.chatbox_ask(utterance, + lang=lang, + persona=persona) + if ans: + return IntentHandlerMatch(match_type='persona:answer', + match_data={"answer": ans, + "persona": persona}, + skill_id="persona.openvoiceos", + utterance=utterances[0]) + + if self.active_persona and self.voc_match(utterances[0], "Release", lang): + return IntentHandlerMatch(match_type='persona:release', + match_data={"persona": self.active_persona}, + skill_id="persona.openvoiceos", + utterance=utterances[0]) + + def match_medium(self, utterances: List[str], lang: str, message: Message) -> None: + return self.match_high(utterances, lang, message) + + def match_low(self, utterances: List[str], lang: Optional[str] = None, + message: Optional[Message] = None) -> Optional[IntentHandlerMatch]: + """ + Recommended before fallback low + Args: utterances (list): list of utterances lang (string): 4 letter ISO language code @@ -124,13 +214,23 @@ def handle_persona_answer(self, message): utt = message.data["answer"] self.speak(utt) + def handle_persona_summon(self, message): + persona = message.data["persona"] + if persona not in self.personas: + self.speak_dialog("unknown_persona") + else: + self.active_persona = persona + + def handle_persona_release(self, message): + self.active_persona = None + if __name__ == "__main__": b = PersonaService(FakeBus(), config={"personas_path": "/home/miro/PycharmProjects/ovos-persona/personas"}) print(b.personas) - print(b.match(["what is the speed of light"])) + print(b.match_low(["what is the speed of light"])) # The speed of light has a value of about 300 million meters per second # The telephone was invented by Alexander Graham Bell diff --git a/ovos_persona/locale/en-us/Release.voc b/ovos_persona/locale/en-us/Release.voc index 249d3b0..78e26cf 100644 --- a/ovos_persona/locale/en-us/Release.voc +++ b/ovos_persona/locale/en-us/Release.voc @@ -1,61 +1,14 @@ -stop talking -shut up -go away -go back to your grave -disable persona -deactivate persona -stop talking -be quiet -end the conversation -go silent -cease communication -pause -terminate -halt -finish -conclude -shush -quiet down -stop chatting -halt the interaction -close the dialogue -end the discussion -wrap it up -cease the conversation -quiet -discontinue -shut your mouth -stop responding -cease your response -hush -stop the chatter -keep quiet -stop communicating -enough talking -go away +(finish|close|cease|exit|end|quit|abort|terminate|discontinue|disengage|disable|deactivate) (chatbot|persona|AI|LLM) activity +(cease|finish|exit|close|end|quit|abort|terminate|discontinue|disengage|disable|deactivate) (the|) (interaction|conversation|discussion) +go (away|back to your grave|dormant|into hibernation) +halt (the|) (interaction|conversation|discussion) leave me alone -exit the conversation -step back +enough talking +put (chatbot|persona|AI|LLM) to sleep +(release|remove|halt) (chatbot|persona|AI|LLM) retreat -withdraw -disengage -go back to your grave return to the abyss -go dormant -go into hibernation -disable persona -deactivate persona -turn off persona -switch off persona -put persona to sleep -remove persona -end persona's activity -release persona -disable chatbot -deactivate chatbot -turn off chatbot -switch off chatbot -put chatbot to sleep -remove chatbot -end chatbot's activity -release chatbot \ No newline at end of file +shut (up|your mouth) +stop (chatting|communicating|responding|talking) +(turn|switch) off (chatbot|persona|AI|LLM) +terminate \ No newline at end of file diff --git a/ovos_persona/locale/en-us/ask.intent b/ovos_persona/locale/en-us/ask.intent index 473ca58..ff3b755 100644 --- a/ovos_persona/locale/en-us/ask.intent +++ b/ovos_persona/locale/en-us/ask.intent @@ -1,47 +1,8 @@ -ask {name} about {utterance} -what does {name} (say|think) about {utterance} -ask {name} about {utterance} -what does {name} say about {utterance} -what does {name} think about {utterance} -get {name}'s opinion on {utterance} -ask {name} for their thoughts on {utterance} -inquire {name} about {utterance} -find out what {name} thinks about {utterance} -ask {name} their perspective on {utterance} -request {name}'s view on {utterance} -seek {name}'s opinion on {utterance} -question {name} regarding {utterance} -interrogate {name} about {utterance} -discover {name}'s thoughts on {utterance} -probe {name} for their thoughts on {utterance} -ask {name} for insights on {utterance} -pose a question to {name} about {utterance} -query {name} about {utterance} -explore {name}'s viewpoint on {utterance} -inquire of {name} their opinion on {utterance} -ask {name} what they think about {utterance} -request {name}'s perspective on {utterance} -ask {name} for their take on {utterance} -seek {name}'s viewpoint on {utterance} -question {name} on {utterance} -ask {name} for their thoughts regarding {utterance} -ask {name} to share their opinion on {utterance} -find out what {name} says about {utterance} -get {name}'s take on {utterance} -ask {name} for their viewpoint on {utterance} -inquire {name} for their perspective on {utterance} -request {name}'s stance on {utterance} -seek {name}'s thoughts on {utterance} -ask {name} for their reaction to {utterance} -ask {name} for their interpretation of {utterance} -ask {name} what they have to say about {utterance} -ask {name} for their analysis of {utterance} -question {name} for their views on {utterance} -ask {name} for their impression of {utterance} -inquire {name} for their thoughts on {utterance} -ask {name} for their standpoint on {utterance} -request {name}'s feedback on {utterance} -seek {name}'s interpretation of {utterance} -ask {name} for their comment on {utterance} -ask {name} for their understanding of {utterance} -ask {name} for their perception of {utterance} \ No newline at end of file +ask {persona} about {utterance} +ask {persona} for (their|) (insights|analysis|comment|impression|interpretation|perception|reaction|standpoint|take|thoughts|understanding|viewpoint) (on|of) {utterance} +ask {persona} (about|to share|) their (perspective|opinion) (about|of|on) {utterance} +ask {persona} what they (have to say|think) about {utterance} +what does {persona} (think|say) about {utterance} +(query|probe|request|seek|question|ask|inquire|interrogate) {persona} for their views on {utterance} +(query|probe|request|seek|question|ask|inquire|interrogate|pose a question to) {persona} (regarding|about) {utterance} +(query|probe|request|seek|question|ask|inquire|get) {persona} (perspective|opinion|thoughts|viewpoint|interpretation|feedback|stance|view) (of|on) {utterance} \ No newline at end of file diff --git a/ovos_persona/locale/en-us/summon.intent b/ovos_persona/locale/en-us/summon.intent index fd7d826..d61e391 100644 --- a/ovos_persona/locale/en-us/summon.intent +++ b/ovos_persona/locale/en-us/summon.intent @@ -1,59 +1,5 @@ -summon {name} -i want to talk with {name} -summon {name} ghost -activate {name} persona -bring forth {name} -conjure {name} -invoke {name} -open communication with {name} -start a conversation with {name} -reach out to {name} -summon the {name} chatbot -initiate interaction with {name} -activate the {name} persona -awaken {name} -summon {name} now -start a chat with {name} -bring {name} to life -activate the {name} bot -awaken the {name} chatbot -open a conversation with {name} -connect me with {name} -initiate a dialogue with {name} -enable {name} persona -talk to {name} -get me in touch with {name} -reach {name} -let me interact with {name} -summon the {name} AI -invoke the {name} chatbot -call upon {name} -engage {name} persona -request {name}'s presence -establish communication with {name} -begin a discussion with {name} -converse with {name} -access {name} chatbot -invite {name} to chat -bring the {name} AI online -interact with {name} -activate the {name} AI -start chatting with {name} -awake {name} persona -open a chat with {name} -commence a conversation with {name} -engage in a dialogue with {name} -turn on {name} bot -summon the {name} virtual assistant -initiate contact with {name} -activate the {name} virtual agent -start an interaction with {name} -bring the {name} chatbot into action -communicate with {name} -begin a chat with {name} -request the presence of {name} -start a discussion with {name} -enter a conversation with {name} -summon the {name} AI assistant -activate {name} virtual assistant -start an exchange with {name} \ No newline at end of file +(enable|access|activate|awake|awaken|bring forth|call upon|conjure|summon|invoke|engage) (the|) {persona} (virtual|) (AI|bot|persona|agent|assistant|chatbot|) +(start|initiate|begin|enter|establish) (a|) (chat|discussion|conversation|dialogue) with {persona} +let me (chat|talk|interact) (to|with) {persona} +(connect me|i want to talk) (to|with) {persona} +(initiate|start|begin|enter|open) (chat|talk|interaction|communication|conversation|dialogue) (to|with) {persona} \ No newline at end of file diff --git a/scripts/sync_translations.py b/scripts/sync_translations.py new file mode 100644 index 0000000..791c848 --- /dev/null +++ b/scripts/sync_translations.py @@ -0,0 +1,53 @@ +"""this script should run in every PR originated from @gitlocalize-app +""" + +import json +from os.path import dirname +import os + +locale = f"{dirname(dirname(__file__))}/ovos_persona/locale" +tx = f"{dirname(dirname(__file__))}/translations" + + +for lang in os.listdir(tx): + intents = f"{tx}/{lang}/intents.json" + dialogs = f"{tx}/{lang}/dialogs.json" + vocs = f"{tx}/{lang}/vocabs.json" + regexes = f"{tx}/{lang}/regexes.json" + os.makedirs(f"{locale}/{lang.lower()}", exist_ok=True) + if os.path.isfile(intents): + with open(intents) as f: + data = json.load(f) + for fid, samples in data.items(): + if samples: + samples = [s for s in samples if s] # s may be None + with open(f"{locale}/{lang.lower()}/{fid}", "w") as f: + f.write("\n".join(sorted(samples))) + + if os.path.isfile(dialogs): + with open(dialogs) as f: + data = json.load(f) + for fid, samples in data.items(): + if samples: + samples = [s for s in samples if s] # s may be None + with open(f"{locale}/{lang.lower()}/{fid}", "w") as f: + f.write("\n".join(sorted(samples))) + + if os.path.isfile(vocs): + with open(vocs) as f: + data = json.load(f) + for fid, samples in data.items(): + if samples: + samples = [s for s in samples if s] # s may be None + with open(f"{locale}/{lang.lower()}/{fid}", "w") as f: + f.write("\n".join(sorted(samples))) + + if os.path.isfile(regexes): + with open(regexes) as f: + data = json.load(f) + for fid, samples in data.items(): + if samples: + samples = [s for s in samples if s] # s may be None + with open(f"{locale}/{lang.lower()}/{fid}", "w") as f: + f.write("\n".join(sorted(samples))) + diff --git a/translations/en-us/intents.json b/translations/en-us/intents.json new file mode 100644 index 0000000..a193944 --- /dev/null +++ b/translations/en-us/intents.json @@ -0,0 +1,19 @@ +{ + "summon.intent": [ + "(enable|access|activate|awake|awaken|bring forth|call upon|conjure|summon|invoke|engage) (the|) {persona} (virtual|) (AI|bot|persona|agent|assistant|chatbot|)", + "(start|initiate|begin|enter|establish) (a|) (chat|discussion|conversation|dialogue) with {persona}", + "let me (chat|talk|interact) (to|with) {persona}", + "(connect me|i want to talk) (to|with) {persona}", + "(initiate|start|begin|enter|open) (chat|talk|interaction|communication|conversation|dialogue) (to|with) {persona}" + ], + "ask.intent": [ + "ask {persona} about {utterance}", + "ask {persona} for (their|) (insights|analysis|comment|impression|interpretation|perception|reaction|standpoint|take|thoughts|understanding|viewpoint) (on|of) {utterance}", + "ask {persona} (about|to share|) their (perspective|opinion) (about|of|on) {utterance}", + "ask {persona} what they (have to say|think) about {utterance}", + "what does {persona} (think|say) about {utterance}", + "(query|probe|request|seek|question|ask|inquire|interrogate) {persona} for their views on {utterance}", + "(query|probe|request|seek|question|ask|inquire|interrogate|pose a question to) {persona} (regarding|about) {utterance}", + "(query|probe|request|seek|question|ask|inquire|get) {persona} (perspective|opinion|thoughts|viewpoint|interpretation|feedback|stance|view) (of|on) {utterance}" + ] +} \ No newline at end of file diff --git a/translations/en-us/vocabs.json b/translations/en-us/vocabs.json new file mode 100644 index 0000000..a2159ff --- /dev/null +++ b/translations/en-us/vocabs.json @@ -0,0 +1,18 @@ +{ + "Release.voc": [ + "(finish|close|cease|exit|end|quit|abort|terminate|discontinue|disengage|disable|deactivate) (chatbot|persona|AI|LLM) activity", + "(cease|finish|exit|close|end|quit|abort|terminate|discontinue|disengage|disable|deactivate) (the|) (interaction|conversation|discussion)", + "go (away|back to your grave|dormant|into hibernation)", + "halt (the|) (interaction|conversation|discussion)", + "leave me alone", + "enough talking", + "put (chatbot|persona|AI|LLM) to sleep", + "(release|remove|halt) (chatbot|persona|AI|LLM)", + "retreat", + "return to the abyss", + "shut (up|your mouth)", + "stop (chatting|communicating|responding|talking)", + "(turn|switch) off (chatbot|persona|AI|LLM)", + "terminate" + ] +} \ No newline at end of file