~ Status class replaced with a simple dict ~ Status object returned normalized betwee, askbot & transbot + transbot now returns a JSON status like askbot

This commit is contained in:
nicobo 2020-05-23 23:00:26 +02:00
parent 3848a22933
commit b7215b1c1f
No known key found for this signature in database
GPG key ID: 2581E71C5FA5285F
4 changed files with 196 additions and 25 deletions

View file

@ -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__':

View file

@ -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__':

View file

@ -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='<stdin>' 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='<stdin>' mode='r' encoding='UTF-8'>"
},
"result": {
"max_count": false,
"events": [{
"message": "Coucou",
"matched_patterns": []
}, {
"message": "Yes !",
"matched_patterns": ["yes"]
}]
}
}

View file

@ -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='<stdin>' 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"
}]
}
}