# 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()