From b7215b1c1f3a4f745a6fe7566d920911516f9e8e Mon Sep 17 00:00:00 2001 From: nicobo Date: Sat, 23 May 2020 23:00:26 +0200 Subject: [PATCH] ~ Status class replaced with a simple dict ~ Status object returned normalized betwee, askbot & transbot + transbot now returns a JSON status like askbot --- nicobot/askbot.py | 38 +++++++-------- nicobot/transbot.py | 60 ++++++++++++++++++++--- test/askbot_sample_output.json | 40 +++++++++++++++ test/transbot_sample_output.json | 83 ++++++++++++++++++++++++++++++++ 4 files changed, 196 insertions(+), 25 deletions(-) create mode 100644 test/askbot_sample_output.json create mode 100644 test/transbot_sample_output.json diff --git a/nicobot/askbot.py b/nicobot/askbot.py index e492cd1..7dfe9e8 100644 --- a/nicobot/askbot.py +++ b/nicobot/askbot.py @@ -46,14 +46,6 @@ class Config: }) -class Status: - - def __init__(self): - self.__dict__.update({ - 'max_count': False, - 'messages': [], - }) - class AskBot(Bot): """ @@ -66,7 +58,10 @@ class AskBot(Bot): def __init__( self, chatter, message, output=sys.stdout, err=sys.stderr, patterns=[], max_count=-1 ): # TODO Implement a global session timeout after which the bot exits - self.status = Status() + self.status = { + 'max_count': False, + 'events': [], + } self.responses_count = 0 self.chatter = chatter @@ -87,8 +82,8 @@ class AskBot(Bot): Returns the full status with exit conditions """ - status_message = { 'message':message, 'patterns':[] } - self.status.messages.append(status_message) + status_message = { 'message':message, 'matched_patterns':[] } + self.status['events'].append(status_message) self.responses_count = self.responses_count + 1 logging.info("<<< %s", message) @@ -96,21 +91,19 @@ class AskBot(Bot): # If we reached the last message or if we exceeded it (possible if we received several answers in a batch) if self.max_count>0 and self.responses_count >= self.max_count: logging.debug("Max amount of messages reached") - self.status.max_count = True + self.status['max_count'] = True # Another way to quit : pattern matching + matched = status_message['matched_patterns'] for p in self.patterns: name = p['name'] pattern = p['pattern'] - status_pattern = { 'name':name, 'pattern':pattern.pattern, 'matched':False } - status_message['patterns'].append(status_pattern) if pattern.search(message): logging.debug("Pattern '%s' matched",name) - status_pattern['matched'] = True - matched = [ p for p in status_message['patterns'] if p['matched'] ] + matched.append(name) # Check if any exit condition is met to notify the underlying chatter engine - if self.status.max_count or len(matched) > 0: + if self.status['max_count'] or len(matched) > 0: logging.debug("At least one pattern matched : exiting...") self.chatter.stop() @@ -191,8 +184,15 @@ def run( args=sys.argv[1:] ): patterns=config.patterns, max_count=config.max_count ) - status = bot.run() - print( json.dumps(vars(status)), file=sys.stdout, flush=True ) + status_args = vars(config) + # TODO Add an option to list the fields to obfuscate (nor not) + for k in [ 'jabber_password' ]: + status_args[k] = '(obfuscated)' + status_result = bot.run() + status = { 'args':vars(config), 'result':status_result } + # NOTE ensure_ascii=False + encode('utf-8').decode() is not mandatory but allows printing plain UTF-8 strings rather than \u... or \x... + # NOTE default=repr is mandatory because some objects in the args are not serializable + print( json.dumps(status,skipkeys=True,ensure_ascii=False,default=repr).encode('utf-8').decode(), file=sys.stdout, flush=True ) if __name__ == '__main__': diff --git a/nicobot/transbot.py b/nicobot/transbot.py index 0336a79..d5cc77d 100755 --- a/nicobot/transbot.py +++ b/nicobot/transbot.py @@ -95,6 +95,7 @@ def sanitizeNotPattern( string ): return re.sub( r'([^\w])', '\\\\\\1', string ) + class TransBot(Bot): """ Sample bot that translates text. @@ -124,6 +125,8 @@ class TransBot(Bot): store_path: Base directory where to cache files """ + self.status = {'events':[]} + self.ibmcloud_url = ibmcloud_url self.ibmcloud_apikey = ibmcloud_apikey self.chatter = chatter @@ -150,6 +153,11 @@ class TransBot(Bot): self.re_shutdown = shutdown_pattern + def _logEvent( self, event ): + + self.status['events'].append(event) + + def loadLanguages( self, force=False, file=None, locale='en' ): """ Loads the list of known languages. @@ -439,6 +447,7 @@ class TransBot(Bot): # as expected so we use re.search(...) if re.search( self.re_shutdown, message, re.IGNORECASE ): logging.debug("Shutdown asked") + self._logEvent({ 'type':'shutdown', 'message':message }) self.chatter.stop() ### @@ -446,19 +455,29 @@ class TransBot(Bot): # Case 'translate a message' # elif matched_translate: + status_event = { 'type':'translate', 'message':message, 'target_lang':to_lang } + self._logEvent(status_event) if to_lang: translation = self.translate( [matched_translate.group('message')],target=to_lang ) logging.debug("Got translation : %s",repr(translation)) + status_event['translation'] = translation if translation and len(translation['translations'])>0: answer = self.formatTranslation(translation,target=to_lang) logging.debug(">> %s" % answer) + status_event['answer'] = answer self.chatter.send(answer) else: # TODO Make translate throw an error with details logging.warning("Did not get a translation in %s for %s",to_lang,message) - self.chatter.send( i18n.t('all_messages',message=i18n.t('IDontKnow')) ) + answer = i18n.t('all_messages',message=i18n.t('IDontKnow')) + status_event['error'] = 'no_translation' + status_event['answer'] = answer + self.chatter.send(answer) else: logging.warning("Could not identify target language in %s",message) + answer = i18n.t('all_messages',message=i18n.t('IDontKnow')) + status_event['error'] = 'unknown_target_language' + status_event['answer'] = answer self.chatter.send( i18n.t('all_messages',message=i18n.t('IDontKnow')) ) ### @@ -467,6 +486,10 @@ class TransBot(Bot): # elif re.search( self.re_keywords, message, flags=re.IGNORECASE ): + status_translations = [] + status_event = { 'type':'keyword', 'message':message, 'translations':status_translations } + self._logEvent( status_event ) + # Selects a few random target languages each time langs = random.choices( self.languages, k=self.tries ) @@ -474,30 +497,41 @@ class TransBot(Bot): # Gets a translation in this random language translation = self.translate( [message], target=lang['language'] ) logging.debug("Got translation : %s",repr(translation)) + status_translation = { 'target_language':lang['language'], 'translation':translation } + status_translations.append(status_translation) if translation and len(translation['translations'])>0: answer = self.formatTranslation(translation,target=lang['language']) logging.debug(">> %s" % answer) + status_translation['answer'] = answer self.chatter.send(answer) # Returns as soon as one translation was done return else: + logging.debug("No translation for %s in %r",message,langs) + status_translation['error'] = 'no_translation' pass logging.warning("Could not find a translation in %s for %s",repr(langs),message) else: logging.debug("Message did not match any known pattern") + self._logEvent({ 'type':'ignored', 'message':message }) def onExit( self ): logging.debug("Exiting...") + status_shutdown = { 'type':'shutdown' } + self._logEvent(status_shutdown) # TODO Better use gettext in the end try: goodbye = i18n.t('Goodbye') if goodbye and goodbye.strip(): - sent = self.chatter.send( i18n.t('all_messages',message=goodbye) ) + text = i18n.t('all_messages',message=goodbye) + sent = self.chatter.send(text) + status_shutdown['answer'] = text + status_shutdown['timestamp'] = sent else: logging.debug("Empty 'Goodbye' text : nothing was sent") except KeyError: @@ -511,6 +545,9 @@ class TransBot(Bot): 1. Sends a hello message 2. Waits for messages to translate + + Returns the execution status of the run, as a dict : { 'events':[list_of_events] } + with list_of_events the list of input / outputs that happened, for audit purposes """ self.chatter.connect() @@ -519,7 +556,9 @@ class TransBot(Bot): try: hello = i18n.t('Hello') if hello and hello.strip(): - self.chatter.send( i18n.t('all_messages',message=hello) ) + text = i18n.t('all_messages',message=hello) + sent = self.chatter.send(text) + self._logEvent({ 'type':'startup', 'answer':text, 'timestamp':sent }) else: logging.debug("Empty 'Hello' text : nothing was sent") except KeyError: @@ -529,6 +568,7 @@ class TransBot(Bot): self.registerExitHandler() self.chatter.start(self) logging.debug("Chatter loop ended") + return self.status @@ -637,15 +677,23 @@ def run( args=sys.argv[1:] ): # Real start # - TransBot( + bot = TransBot( keywords=config.keywords, keywords_files=config.keywords_files, languages_file=config.languages_file, languages_likely=config.languages_likely, locale=lang, ibmcloud_url=config.ibmcloud_url, ibmcloud_apikey=config.ibmcloud_apikey, shutdown_pattern=config.shutdown, chatter=chatter - ).run() - + ) + status_args = vars(config) + # TODO Add an option to list the fields to obfuscate (nor not) + for k in [ 'ibmcloud_apikey', 'jabber_password' ]: + status_args[k] = '(obfuscated)' + status_result = bot.run() + status = { 'args':vars(config), 'result':status_result } + # NOTE ensure_ascii=False + encode('utf-8').decode() is not mandatory but allows printing plain UTF-8 strings rather than \u... or \x... + # NOTE default=repr is mandatory because some objects in the args are not serializable + print( json.dumps(status,skipkeys=True,ensure_ascii=False,default=repr).encode('utf-8').decode(), file=sys.stdout, flush=True ) if __name__ == '__main__': diff --git a/test/askbot_sample_output.json b/test/askbot_sample_output.json new file mode 100644 index 0000000..2fd6255 --- /dev/null +++ b/test/askbot_sample_output.json @@ -0,0 +1,40 @@ +{ + "args": { + "backend": "console", + "config_file": "/home/nicobo/nicobot/test/askbot-sample-conf/config.yml", + "config_dir": "/home/nicobo/nicobot/test/askbot-sample-conf/", + "input_file": "<_io.TextIOWrapper name='' mode='r' encoding='UTF-8'>", + "max_count": -1, + "patterns": [ + ["yes", "(?i)\\b(yes|ok)\\b"], + ["no", "(?i)\\bno\\b"], + ["cancel", "(?i)\\b(cancel|abort)\\b"] + ], + "stealth": false, + "timeout": null, + "verbosity": "debug", + "signal_username": "+33123456789", + "signal_recipients": ["+33987654321"], + "jabber_username": "bot9cd51f1a@conversations.im", + "jabber_password": "(obfuscated)", + "jabber_recipients": ["bot649ad4ad@conversations.im"], + "username": null, + "recipients": [], + "debug": false, + "signal_cli": "/opt/signal-cli/bin/signal-cli", + "signal_group": null, + "signal_stealth": false, + "message": null, + "message_file": "<_io.TextIOWrapper name='' mode='r' encoding='UTF-8'>" + }, + "result": { + "max_count": false, + "events": [{ + "message": "Coucou", + "matched_patterns": [] + }, { + "message": "Yes !", + "matched_patterns": ["yes"] + }] + } +} diff --git a/test/transbot_sample_output.json b/test/transbot_sample_output.json new file mode 100644 index 0000000..77897b8 --- /dev/null +++ b/test/transbot_sample_output.json @@ -0,0 +1,83 @@ +{ + "args": { + "backend": "console", + "config_file": "/home/nicobo/nicobot/test/transbot-sample-conf/config.yml", + "config_dir": "/home/nicobo/nicobot/test/transbot-sample-conf/", + "group": null, + "ibmcloud_url": "https://api.eu-de.language-translator.watson.cloud.ibm.com/instances/f93001df-abcd-afgh-ijkl-d9c534aeba42", + "ibmcloud_apikey": "(obfuscated)", + "input_file": "<_io.TextIOWrapper name='' mode='r' encoding='UTF-8'>", + "keywords": [], + "keywords_files": ["/home/nicobo/nicobot/test/transbot-sample-conf/hello.keywords.json", "/home/nicobo/nicobot/test/transbot-sample-conf/goodbye.keywords.json"], + "languages": [], + "languages_file": "/home/nicobo/nicobot/test/transbot-sample-conf/languages.fr.json", + "languages_likely": "/home/nicobo/nicobot/test/transbot-sample-conf/likelySubtags.json", + "locale": "fr", + "recipient": null, + "shutdown": "couché nicobot", + "signal_cli": "/opt/signal-cli/bin/signal-cli", + "signal_stealth": false, + "stealth": false, + "username": null, + "verbosity": "debug", + "signal_username": "+33123456789", + "signal_recipients": ["+33987654321"], + "jabber_username": "bot9cd51f1a@conversations.im", + "jabber_password": "(obfuscated)", + "jabber_recipients": ["bot649ad4ad@conversations.im"], + "recipients": [], + "debug": false, + "signal_group": null + }, + "result": { + "events": [{ + "type": "startup", + "answer": "🤖 nicobot paré 🤟", + "timestamp": null + }, { + "type": "keyword", + "message": "Bonjour !", + "translations": [{ + "target_language": "nn", + "translation": null, + "error": "no_translation" + }, { + "target_language": "ne", + "translation": { + "translations": [{ + "translation": "हेलो!" + }], + "word_count": 2, + "character_count": 9, + "detected_language": "fr", + "detected_language_confidence": 0.5904432605699131 + }, + "answer": "🤖 हेलो! 🇳🇵" + }] + }, { + "type": "translate", + "message": "nicobot toto en anglais", + "target_lang": "en", + "translation": null, + "error": "no_translation", + "answer": "🤖 Je ne sais pas" + }, { + "type": "translate", + "message": "nicobot traduit toto en anglais", + "target_lang": "en", + "translation": { + "translations": [{ + "translation": "Toto translated" + }], + "word_count": 2, + "character_count": 13, + "detected_language": "fr", + "detected_language_confidence": 0.5973206507749139 + }, + "answer": "🤖 Toto translated 🇺🇸" + }, { + "type": "shutdown", + "message": "couché nicobot" + }] + } +}