Example #1
0
class Chatbot():

    def __init__(self):
        self.botname = rospy.get_param('botname', 'sophia')
        self.client = Client(
            HR_CHATBOT_AUTHKEY, response_listener=self,
            botname=self.botname, stdout=Console())
        self.client.chatbot_url = rospy.get_param(
            'chatbot_url', 'http://localhost:8001')
        # chatbot now saves a bit of simple state to handle sentiment analysis
        # after formulating a response it saves it in a buffer if S.A. active
        # It has a simple state transition - initialized in wait_client
        # after getting client if S.A. active go to wait_emo
        # in affect_express call back publish response and reset to wait_client
        self._response_buffer = ''
        self._state = 'wait_client'
        # argumment must be  to activate sentiment analysis
        self._sentiment_active = False
        # sentiment dictionary
        self.polarity = Polarity()
        self._polarity_threshold = 0.2
        self.speech = False
        self.enable = True
        self.mute = False
        self.node_name = rospy.get_name()

        self.input_stack = []
        self.condition = threading.Condition()
        self.respond_worker = threading.Thread(target=self.process_input)
        self.respond_worker.daemon = True
        self.respond_worker.start()
        self.delay_response = rospy.get_param('delay_response', False)
        self.delay_time = rospy.get_param('delay_time', 5)

        rospy.Subscriber('chatbot_speech', ChatMessage, self._request_callback)
        rospy.Subscriber('speech_events', String, self._speech_event_callback)
        self.tts_ctrl_pub = rospy.Publisher(
            'tts_control', String, queue_size=1)

        self._response_publisher = rospy.Publisher(
            'chatbot_responses', String, queue_size=1)

        # send communication non-verbal blink message to behavior
        self._blink_publisher = rospy.Publisher(
            'chatbot_blink', String, queue_size=1)

        # Perceived emotional content; and emotion to express
        # Perceived: based on what chatbot heard, this is how robot should
        # feel.  Expressed: the emotional content that the chatbot should
        # put into what it says.
        self._affect_publisher = rospy.Publisher(
            'chatbot_affect_perceive', String, queue_size=1)

        # Echo chat messages as plain strings.
        self._echo_publisher = rospy.Publisher(
            'perceived_text', String, queue_size=1)
        rospy.Subscriber('chatbot_speech', ChatMessage, self._echo_callback)
        rospy.set_param('node_status/chatbot', 'running')

        # the first message gets lost with using topic_tools
        try:
            rospy.wait_for_service('tts_select', 5)
            rospy.sleep(0.1)
            self._response_publisher.publish(String(' '))
        except Exception as ex:
            logger.error(ex)


    def sentiment_active(self, active):
        self._sentiment_active = active

    def ask(self, questions, query=False):
        question = ' '.join(questions)
        lang = rospy.get_param('lang', None)
        if lang:
            self.client.lang = lang

        # XXX: replace his/her name -> my name
        question = question.replace('his name', 'my name')
        question = question.replace('her name', 'my name')
        question = question.replace('His name', 'My name')
        question = question.replace('Her name', 'My name')

        persons = rospy.get_param('/face_recognizer/current_persons', '')
        if persons:
            person = persons.split('|')[0]
            person = person.title()
            self.client.set_context('queryname={}'.format(person))
            logger.info("Set queryname to {}".format(person))
        else:
            self.client.remove_context('queryname')
            logger.info("Remove queryname")
        self.client.ask(question, query)

    def _speech_event_callback(self, msg):
        if msg.data == 'start':
            self.speech = True
        if msg.data == 'stop':
            rospy.sleep(2)
            self.speech = False

    def _request_callback(self, chat_message):
        if not self.enable:
            logger.info("Chatbot is disabled")
            return
        if 'shut up' in chat_message.utterance.lower():
            logger.info("Robot's talking wants to be interruptted")
            self.tts_ctrl_pub.publish("shutup")
            rospy.sleep(0.5)
            self._affect_publisher.publish(String('sad'))
            if not self.mute:
                self._response_publisher.publish(String('Okay'))
            return

        # Handle chatbot command
        cmd, arg, line = self.client.parseline(chat_message.utterance)
        func = None
        try:
            func = getattr(self.client, 'do_' + cmd)
        except AttributeError as ex:
            pass
        if func:
            try:
                func(arg)
            except Exception as ex:
                logger.error("Executing command {} error {}".format(func, ex))
            return

        # blink that we heard something, request, probability defined in
        # callback
        self._blink_publisher.publish('chat_heard')

        if self.delay_response:
            with self.condition:
                logger.info("Add input: {}".format(chat_message.utterance))
                self.input_stack.append((time.clock(), chat_message))
                self.condition.notify_all()
        else:
            self.ask([chat_message.utterance])

    def process_input(self):
        while True:
            time.sleep(0.1)
            with self.condition:
                if not self.input_stack:
                    continue
                num_input = len(self.input_stack)
                questions = [i[1].utterance for i in self.input_stack]
                question = ' '.join(questions)
                logger.info("Current input: {}".format(question))
                self.condition.wait(max(1, self.delay_time-len(self.input_stack)))
                if len(self.input_stack) > num_input:
                    continue
                self.ask(questions)
                del self.input_stack[:]

    def on_response(self, sid, response):
        if response is None:
            logger.error("No response")
            return

        if sid != self.client.session:
            logger.error("Session id doesn't match")
            return

        logger.info("Get response {}".format(response))
        text = response.get('text')
        emotion = response.get('emotion')

        # Add space after punctuation for multi-sentence responses
        text = text.replace('?', '? ')
        text = text.replace('_', ' ')

        # if sentiment active save state and wait for affect_express to publish response
        # otherwise publish and let tts handle it
        if self._sentiment_active:
            emo = String()
            if emotion:
                emo.data = emotion
                self._affect_publisher.publish(emo)
                rospy.loginfo(
                    '[#][PERCEIVE ACTION][EMOTION] {}'.format(emo.data))
                logger.info('Chatbot perceived emo: {}'.format(emo.data))
            else:
                p = self.polarity.get_polarity(text)
                logger.info('Polarity for "{}" is {}'.format(
                    text.encode('utf-8'), p))
                # change emotion if polarity magnitude exceeds threshold defined in constructor
                # otherwise let top level behaviors control
                if p > self._polarity_threshold:
                    emo.data = 'happy'
                    self._affect_publisher.publish(emo)
                    rospy.loginfo(
                        '[#][PERCEIVE ACTION][EMOTION] {}'.format(emo.data))
                    logger.info(
                        'Chatbot perceived emo: {}'.format(emo.data))
                    # Currently response is independant of message received so no need to wait
                    # Leave it for Opencog to handle responses later on.
                elif p < 0 and abs(p) > self._polarity_threshold:
                    emo.data = 'frustrated'
                    self._affect_publisher.publish(emo)
                    rospy.loginfo(
                        '[#][PERCEIVE ACTION][EMOTION] {}'.format(emo.data))
                    logger.info(
                        'Chatbot perceived emo: {}'.format(emo.data))
                    # Currently response is independant of message received so no need to wait
                    # Leave it for Opencog to handle responses later on.

        if not self.mute:
            self._blink_publisher.publish('chat_saying')
            self._response_publisher.publish(String(text))

        if rospy.has_param('{}/context'.format(self.node_name)):
            rospy.delete_param('{}/context'.format(self.node_name))
        context = self.client.get_context()
        context['sid'] = self.client.session
        for k, v in context.iteritems():
            rospy.set_param('{}/context/{}'.format(self.node_name, k), v)
            logger.info("Set param {}={}".format(k, v))

    # Just repeat the chat message, as a plain string.
    def _echo_callback(self, chat_message):
        message = String()
        message.data = chat_message.utterance
        self._echo_publisher.publish(message)

    def reconfig(self, config, level):
        self.sentiment_active(config.sentiment)
        self.client.chatbot_url = config.chatbot_url
        self.enable = config.enable
        self.delay_response = config.delay_response
        self.delay_time = config.delay_time
        self.client.ignore_indicator = config.ignore_indicator
        if config.set_that:
            self.client.do_said(config.set_that)
            config.set_that = ''

        if config.set_context:
            self.client.set_context(config.set_context)
        self.mute = config.mute
        tiers = config.groups.groups.Weights.parameters.keys()
        tiers.remove('reset')
        logger.info(config)
        if not config.reset:
            try:
                weights = ','.join(
                    ['{}={}'.format(id, config.get(id)) for id in tiers]
                )
                self.client.set_weights(weights)
                logger.info("Set weights {}".format(self.client.weights))
            except Exception as ex:
                logger.error(ex)

        if config.reset:
            try:
                self.client.set_weights(None)
                weights = self.client.get_weights()
                if not weights:
                    logger.error("Can't get weights from server")
                else:
                    logger.info("Get weights {}".format(weights))
                for id in tiers:
                    if id in weights:
                        setattr(config, id, weights[id])
            except Exception as ex:
                logger.error(ex)
            config.reset = False
        return config
Example #2
0
    def run(self):
        while True:
            time.sleep(0.2)
            messages = self.sc.rtm_read()
            if not messages:
                continue
            for message in messages:
                if message['type'] != u'message':
                    continue
                if message.get('subtype') == u'bot_message':
                    continue
                usr_obj = self.sc.api_call('users.info',
                                           token=SLACKTEST_TOKEN,
                                           user=message['user'])
                if not usr_obj['ok']:
                    continue
                profile = usr_obj['user']['profile']
                name = profile.get('first_name') or profile.get('email')
                question = message.get('text')
                channel = message.get('channel')

                sid = self.session_manager.get_sid(name, self.botname)
                session = self.session_manager.get_session(sid)
                if session is not None:
                    assert hasattr(session.session_context, 'client')
                    client = session.session_context.client
                else:
                    client = Client(HR_CHATBOT_AUTHKEY,
                                    username=name,
                                    botname=self.botname,
                                    host=self.host,
                                    port=self.port,
                                    response_listener=self)
                    client.set_marker('Slack')
                    if self.weights:
                        client.set_weights(self.weights)
                    self.session_manager.add_session(name, self.botname,
                                                     client.session)
                    session = self.session_manager.get_session(client.session)
                    if session is not None:
                        session.session_context.client = client
                        session.session_context.channel = channel
                        self.info(
                            channel,
                            "Session <{url}/v2.0/session_history?session={sid}&Auth={auth}|{sid}>"
                            .format(url=CHATBOT_SERVER_URL,
                                    sid=session.sid,
                                    auth=HR_CHATBOT_AUTHKEY))
                    else:
                        self.error(channel, "Can't get session")
                        continue

                logger.info("Question {}".format(question))
                if question in [':+1:', ':slightly_smiling_face:', ':)', 'gd']:
                    ret, _ = client._rate('good')
                    if ret:
                        logger.info("Rate good")
                        answer = 'Thanks for rating'
                        color = 'good'
                    else:
                        logger.info("Rate failed")
                        answer = 'Rating failed'
                        color = 'danger'
                    attachments = [{
                        'title': answer,
                        'color': color,
                        'fallback': answer
                    }]
                    self.send_message(channel, attachments)
                    continue
                if question in [':-1:', ':disappointed:', ':(', 'bd']:
                    ret, _ = client._rate('bad')
                    if ret:
                        logger.info("Rate bad")
                        answer = 'Thanks for rating'
                        color = 'good'
                    else:
                        logger.info("Rate failed")
                        answer = 'Rating failed'
                        color = 'danger'
                    attachments = [{
                        'title': answer,
                        'color': color,
                        'fallback': answer
                    }]
                    self.send_message(channel, attachments)
                    continue

                try:
                    client.ask(question)
                except Exception as ex:
                    self.error(channel, ex.message)

                # session could change after ask
                if client.session != session.sid:
                    self.session_manager.remove_session(session.sid)
                    self.session_manager.add_session(name, self.botname,
                                                     client.session)
                    session = self.session_manager.get_session(client.session)
                    session.session_context.client = client
                    session.session_context.channel = channel
                    self.info(
                        channel,
                        "Session <{url}/v2.0/session_history?session={sid}&Auth={auth}|{sid}>"
                        .format(url=CHATBOT_SERVER_URL,
                                sid=session.sid,
                                auth=HR_CHATBOT_AUTHKEY))
                    logger.info("Session is updated")
Example #3
0
    def run(self):
        while True:
            time.sleep(0.2)
            messages = self.sc.rtm_read()
            if not messages:
                continue
            for message in messages:
                if message['type'] != u'message':
                    continue
                if message.get('subtype') == u'bot_message':
                    continue
                usr_obj = self.sc.api_call(
                    'users.info', token=SLACKTEST_TOKEN, user=message['user'])
                if not usr_obj['ok']:
                    continue
                profile = usr_obj['user']['profile']
                name = profile.get('first_name') or profile.get('email')
                question = message.get('text')
                channel = message.get('channel')

                sid = self.session_manager.get_sid(name, self.botname)
                session = self.session_manager.get_session(sid)
                if session is not None:
                    assert hasattr(session.sdata, 'client')
                    client = session.sdata.client
                else:
                    client = Client(HR_CHATBOT_AUTHKEY, username=name,
                        botname=self.botname, host=self.host, port=self.port,
                        response_listener=self)
                    client.set_marker('Slack')
                    if self.weights:
                        client.set_weights(self.weights)
                    self.session_manager.add_session(name, self.botname, client.session)
                    session = self.session_manager.get_session(client.session)
                    if session is not None:
                        session.sdata.client = client
                        session.sdata.channel = channel
                        self.info(channel, "Session <{url}/v1.1/session_history?session={sid}&Auth={auth}|{sid}>".format(
                            url=CHATBOT_SERVER_URL, sid=session.sid, auth=HR_CHATBOT_AUTHKEY))
                    else:
                        self.error(channel, "Can't get session")
                        continue

                logger.info("Question {}".format(question))
                if question in [':+1:', ':slightly_smiling_face:', ':)', 'gd']:
                    ret, _ = client._rate('good')
                    if ret:
                        logger.info("Rate good")
                        answer = 'Thanks for rating'
                        color = 'good'
                    else:
                        logger.info("Rate failed")
                        answer = 'Rating failed'
                        color = 'danger'
                    attachments = [{
                        'title': answer,
                        'color': color,
                        'fallback': answer
                    }]
                    self.send_message(channel, attachments)
                    continue
                if question in [':-1:', ':disappointed:', ':(', 'bd']:
                    ret, _ = client._rate('bad')
                    if ret:
                        logger.info("Rate bad")
                        answer = 'Thanks for rating'
                        color = 'good'
                    else:
                        logger.info("Rate failed")
                        answer = 'Rating failed'
                        color = 'danger'
                    attachments = [{
                        'title': answer,
                        'color': color,
                        'fallback': answer
                    }]
                    self.send_message(channel, attachments)
                    continue

                try:
                    client.ask(question)
                except Exception as ex:
                    self.error(channel, ex.message)

                # session could change after ask
                if client.session != session.sid:
                    self.session_manager.remove_session(session.sid)
                    self.session_manager.add_session(
                        name, self.botname, client.session)
                    session = self.session_manager.get_session(client.session)
                    session.sdata.client = client
                    session.sdata.channel = channel
                    self.info(channel, "Session <{url}/v1.1/session_history?session={sid}&Auth={auth}|{sid}>".format(
                        url=CHATBOT_SERVER_URL, sid=session.sid, auth=HR_CHATBOT_AUTHKEY))
                    logger.info("Session is updated")