mirror of
https://github.com/nicolabs/nicobot.git
synced 2025-09-06 03:04:48 +02:00
~ 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:
parent
3848a22933
commit
b7215b1c1f
|
@ -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__':
|
||||
|
|
|
@ -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__':
|
||||
|
|
40
test/askbot_sample_output.json
Normal file
40
test/askbot_sample_output.json
Normal 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"]
|
||||
}]
|
||||
}
|
||||
}
|
83
test/transbot_sample_output.json
Normal file
83
test/transbot_sample_output.json
Normal 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"
|
||||
}]
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue