# Copyright (c) 2004 DoCoMo Euro-Labs GmbH (Munich, Germany). # Copyright (c) 2000-2004 LOGILAB S.A. (Paris, FRANCE). # # http://www.docomolab-euro.com/ -- mailto:tarlano@docomolab-euro.com # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """some demos and no more used actions """ __revision__ = '$Id: Chat.py,v 1.11 2004/04/02 10:07:31 syt Exp $' import re from narval.public import AL_NS, TYPE_NS from narval.reader import REGISTRY from narval.interfaces.base import IOpen, IData, IURL from narval.elements.core import StartPlanElement from narval.elements.base import CategoryElement, DataElement, FileElement, \ EventElement from narval.actions.Learning import get_classifier, get_log_file, log_sentence, \ do_log_command, YES, NO, UNSURE MOD_XML = ''' ''' % AL_NS # remember sentences / context for the learning_categorize action LEARNING_SENTENCES = {} # remember (sentences, category) / context for the controled_categorize action CONTROLED_SENTENCES = {} def extract_command(sentence, myid, mtype): """try to extract a command from a sentence. If found, return it as a string (try to normalize it), else return None :type sentence: str :param sentence: the user sentence :type myid: str :param myid: the bot jabber user id :rtype: str or None :return: the normalized command or None if the sentence was not a command """ if re.match('%s\s*:' % myid, sentence): # and then check we are understanding it command = sentence.split(':', 1)[1].strip().lower() elif mtype != 'groupchat': command = sentence.strip() else: return None # normalize command if command in ('log', 'write'): return ('log',) if command in ('ignore', 'pass', 'skip'): return ('ignore',) if command in ('unlearn', 'stupid', 'balot', 'grrr'): return ('unlearn',) if command.startswith('search') or command.startswith('grep'): return ('search', command.split(' ', 1)[1].strip()) if command.startswith('date') or command.startswith('meeting'): return ('date', command.split(' ', 1)[1].strip()) if mtype != 'groupchat': return None return (command,) def categorize(inputs): """a sentence categorization action used after learning this action produce only optional elements: - an answer to inform about the action taken - an optional IFile element with the sentence if it should be logged """ o = {} ctrl = inputs['control'] msg = inputs['msg'] text = msg.get_body() # if the user asked to log, we log if get_classifier(inputs['datafile']).categorize(text) == YES: o['data'] = log_sentence(text, msg.get_from(), msg.get_from_user(), msg.get_type(), inputs['discussion-log-base']) if ctrl and ctrl.verbose: o['answer'] = msg.build_reply('%s:sentence logged' % msg.get_from_user()) return o MOD_XML = MOD_XML + """ Process a chat sentence IIMessage(elmt) and elmt.type == 'incoming' isinstance(elmt, BotConfigurationElement) elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:datafile' IURL(elmt) elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:discussion-log-base' IURL(elmt) IIMessage(elmt) and elmt.type == 'outgoing' IData(elmt) """ def learning_categorize(inputs): """a categorize action used for learning the result for a sentence is delayed until the next sentence, since the categorisation is done according to the user's feedback : 1) check if the sentence is a user instruction (ie ask to log) 2.1) if it is, output the latest remembered sentence with a "log" category 2.2) if it is not, output the latest remembered sentence with a "no log" category 3) remember the sentence this action produce only optional elements: - an answer to inform about the action taken - the latest sentence and its category if any - an optional IFile element with the latest sentence if it should be logged """ o = {} ctrl = inputs['control'] myuser = ctrl and ctrl.myuser verbose = ctrl and ctrl.verbose msg = inputs['msg'] text = msg.get_body() from_user = msg.get_from_user() latest = LEARNING_SENTENCES.get(msg.context) if latest is None: LEARNING_SENTENCES[msg.context] = text if verbose: o['answer'] = msg.build_reply('remembering for the next time') return o category = None command = extract_command(text, msg.get_to(), msg.get_type()) if command is not None: # this is an order, check it comes from our user if myuser and from_user != myuser: return {'answer': msg.build_reply( '%s: only %s can give me some order' % (from_user, myuser))} # and then check we are understanding it if command != 'log': return {'answer': msg.build_reply('i don\'t understand this order')} category = "log" LEARNING_SENTENCES[msg.context] = None else: # this is not a command, remember it for the next time LEARNING_SENTENCES[msg.context] = text # now we should at least output the categorization element o['category'] = cat_elmt = CategoryElement(name=category) if category is None: o['data'] = tolog = DataElement tolog.data = '<%s> %s\n' % (from_user, text) if verbose: o['answer'] = msg.build_reply('ignored and learnt %r' % latest) else: # if the user asked to log, we log o['data'] = log_sentence(latest, msg.get_from(), from_user, msg.get_type(), inputs['discussion-log-base']) if verbose: o['answer'] = msg.build_reply('logged and learnt %r' % latest) return o MOD_XML = MOD_XML + """ Process a chat sentence in the learning process IIMessage(elmt) and elmt.type == 'incoming' elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:discussion-log-base' IURL(elmt) isinstance(elmt, BotConfigurationElement) IIMessage(elmt) and elmt.type == 'outgoing' ICategory(elmt) IData(elmt) """ def controled_categorize(inputs): """a sentence categorization action used after learning, but with user interaction: - the user can ask to log /unlearn a phrase explicitly - the user set the confidence thresholds, and the bot will ask for user feedback when its confidence level is betwen the two specified treshold Each categorized sentence is learned to improve the knowledge base. this action produce only optional elements: - an answer to inform about the action taken - an optional IFile element with the sentence if it should be logged """ o = {} ctrl = inputs['control'] myuser = ctrl.myuser msg = inputs['msg'] text = msg.get_body() from_user = msg.get_from_user() from_id = msg.get_from() latest_sentence, latest_category = CONTROLED_SENTENCES.get(msg.context, (None, None)) classifier = get_classifier(inputs['datafile']) category, answer = None, None command = extract_command(text, msg.get_to(), msg.get_type()) if command is not None: # this is an order, related to the latest sentence processed # check it comes from our user if myuser and from_user != myuser: return {'answer': msg.build_reply( '%s: only %s can give me some order' % (from_user, myuser))} # special case of the search command (returned as a tuple) if command[0] == 'search': # trick: create a Ifile with no data, so it shouldn't be take # as something to log in latter actions # FIXME: this rely on the log action's prototype... logfile = get_log_file(from_id, msg.get_type(), inputs['discussion-log-base']) data = DataElement() data.data = command[1] data.setattr((TYPE_NS, 'name'), 'uri:memory:search-pattern') return {'search': data, 'data': logfile} if command[0] == 'date': try: o['event'] = event = EventElement() event.from_time, other = event.extract_time_from_string(command[1]) event.subject = ' '.join(other.split()) except Exception, ex: import sys log_traceback(LOG_ERR, sys.exc_info()) return {'answer': msg.build_reply( '%s: error while parsing event: %r' % (from_user, ex))} command = ('log',) latest_sentence, latest_category = text, None elif latest_sentence is None: return {'answer': msg.build_reply( '%s: nothing to process !' % from_user)} try: category, answer = do_log_command(command[0], classifier, latest_sentence, latest_category, ctrl.verbose) text = latest_sentence CONTROLED_SENTENCES[msg.context] = (None, None) except Exception, ex: category, answer = None, str(ex) else: category = classifier.fuzzy_categorize(text) if category >= ctrl.min_treshold and category <= ctrl.max_treshold: category = UNSURE answer = "don't know what to do (%.2f)... please tell me" % category else: if category < ctrl.min_treshold: category = NO else: # category > ctrl.max_treshold category = YES #classifier.learn(text, category) if ctrl.verbose: answer = category and 'logged' or 'ignored' # this is not a command, remember it for the next time CONTROLED_SENTENCES[msg.context] = (text, category) if answer is not None: o['answer'] = msg.build_reply(answer) if category == YES: # if the user asked to log, we log o['data'] = log_sentence(text, from_id, from_user, msg.get_type(), inputs['discussion-log-base']) return o MOD_XML = MOD_XML + """ Process a chat sentence IIMessage(elmt) and elmt.type == 'incoming' isinstance(elmt, BotConfigurationElement) elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:datafile' IURL(elmt) elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:discussion-log-base' IURL(elmt) IIMessage(elmt) and elmt.type == 'outgoing' IFile(elmt) IData(elmt) and elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:search-pattern' IEvent(elmt) """ def learn(inputs): """train the classifier according to a sentence and a predetermined category 'log' -> YES whatever else -> NO """ category = (inputs['category'] == 'log') and YES or NO get_classifier(inputs['datafile']).learn(IData(inputs['data']).data, category) return {} MOD_XML = MOD_XML + """ Process a chat sentence IData(elmt) ICategory(elmt) elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:datafile' IURL(elmt) """ # ############################################################################# CONTEXTS = {} from_regex = re.compile("\s*from\s+(\w*)") _predifined_answer = { 'hello' : 'hello ! How are you ?', 'hi': 'hi ! Glad to see you !', 'bye': 'don\'t leave me', 'stupid': 'I hope I\'ll do better next time...', 'bonjour' : 'bonjour !', 'salut': 'salut !', } def demo_f(inputs): """get raw_phrase and interprete it, then build an answer to be sent by jabber """ log(LOG_DEBUG, 'in HERE') msg = inputs['input'] context = msg.context output = {'output': msg} request = msg.request text = str(request.body) if text[:10] == 'start-plan': recipe = text.split()[-1] answer = "Starting plan %s" % recipe output['startplan'] = StartPlanElement(recipe=recipe) elif text.find('from ') >= 0 : name = from_regex.findall(text)[0] recipe, data = create_recipe(name, request['from'], request['to']) output['recipe'] = REGISTRY.from_string(data) output['startplan'] = StartPlanElement(recipe=recipe) answer = 'No problem, I\'ll tell you as soon as a mail from %s has arrived' % name elif text[:10] == 'let\'s play': g = Guesser() answer = g.intro() CONTEXTS[context] = g elif text.find('stop')>=0 or text.find('lost')>=0 or text.find('won')>=0: try: del CONTEXTS[context] answer = 'Once again I won :-)' except: answer = 'But we are not playing !' elif text.find('clear')>=0: del CONTEXTS[context] answer = 'alright, I\'ve cleared the context' elif CONTEXTS.has_key(context): try: answer = str(CONTEXTS[context].guess_what(text)) except Exception, e: answer = str(e) else: answer = _predifined_answer.get( text.lower(), 'unfortunately I don\'t know what to answer you here') # log(LOG_DEBUG, answer) msg = msg.build_reply(answer) return output MOD_XML = MOD_XML + """ Jabber demo isinstance(elmt, JabberRequest) elmt.type == 'incoming' isinstance(elmt, JabberRequest) and elmt.type == 'outgoing' isinstance(elmt, StartPlanElement) isinstance(elmt, RecipeElement) """ def process_sentence_f(inputs): """get a chat sentence and interprete it, then build an answer to be sent by jabber or log the input sentence to a file or do nothing """ msg = inputs['msg'] context = msg.context text = msg.get_body() url = IURL(inputs['logfile']) keywords = IOpen(inputs['keyword']).open() myid = msg.get_to() answer = None output = {} if (text == 'stop' or text.startswith(myid) and text.find('stop') > 1) \ and CONTEXTS.has_key(context): del CONTEXTS[context] answer = 'alright, I stop logging what you say' elif text == 'write' or text.startswith(myid) and text.find('write') > 1: CONTEXTS[context] = 1 answer = 'ok boss' elif CONTEXTS.has_key(context) or should_log(text, keywords): output['logdata'] = tolog = FileElement fromid = msg.get_from() tolog.address = '%s.%s' % (url.address, fromid) tolog.mode = 'a' tolog.data = '%s: %s\n' % (fromid, text) tolog.encoding = url.encoding if answer is not None: output['answer'] = msg.build_reply(answer) return output def should_log(sentence, keywords): """return true if the given sentence should be logged""" words = sentence.split() keywords = [w.strip() for w in list(keywords)] for word in words: if word in keywords: return True return False MOD_XML += """ Process a chat sentence IIMessage(elmt) and elmt.type == 'incoming' elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:logfile' IURL(elmt) elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:keywords' IOpen(elmt) IIMessage(elmt) and elmt.type == 'outgoing' implements(elmt, IFile) and elmt.mode == 'a' """ MOD_XML += "" # waitmail recipe generator ################################################### def create_recipe(waited_name, from_id, to_id): recipe_name = "process_email_from_%s" % waited_name recipe = "generated.%s" % recipe_name return recipe, """ message/email/headers[contains(string(from),"%s")] message/email/@type="incoming" jabber:jabber-request jabber:jabber-request An event occurs You have just received a mail from %s """%(recipe_name, waited_name, from_id, to_id, waited_name) MIN = 0 MAX = 65535 class Guesser: """class to guess a number, keeping latest guess""" def __init__(self): self._min = None self._max = None self._last = None def intro(self): """display the game's rules""" self.new_value() return '''think to a number between %s and %s, I\'ll try to guess it in the fewer questions as possible. What about %s ? Tell me if your number is bigger by typing >, smaller by typing < or equal by typing =. '''% (MIN,MAX, self._last) def guess_what(self, msg): """update state and return an answer""" if msg == '<': self._max = self._last elif msg == '>': self._min = self._last elif msg == '=' or self._min == self._max: return self._last else: raise Exception('Answer only with >, < or = please') self.new_value() return self._last def new_value(self): """return a value equals to (current max- current min) / 2""" if self._last is None: self._last = (MAX-MIN)/2 elif self._min is None: self._last = MIN elif self._max is None: self._last = MAX else: self._last = (self._max+self._min)/2 if __name__ == '__main__': g = Guesser() print g.intro() import sys line = sys.stdin.readline() while line: line = line.strip() print g.guess_what(line) line = sys.stdin.readline()