Beispiel #1
0
def load_dyn_properties():
    global dyn_properties
    logger.info('Loading location & weather properties')
    location = get_location()
    if location:
        dyn_properties.update(location)
        city = location.get('city')
        neighborhood = location.get('neighborhood')
        base_location = ''
        if city:
            base_location = city
        elif 'country_name' in location:
            base_location = location.get('country_name')
        elif 'country' in location:
            base_location = location.get('country')
        if base_location:
            if neighborhood:
                location_str = '%s, %s' % (neighborhood, base_location)
            else:
                location_str = base_location
            dyn_properties['location'] = location_str

    weather_prop = None
    if location:
        weather = get_weather('{city}'.format(city=location['city']))
        weather_prop = parse_weather(weather)
    if weather_prop:
        dyn_properties.update(weather_prop)
    if dyn_properties:
        logger.info("Dynamic properties {}".format(dyn_properties))
    else:
        logger.warn("No dynamic properties")
Beispiel #2
0
def load_dyn_properties():
    global dyn_properties
    location = get_location()
    if location:
        if 'city' in location:
            dyn_properties['location'] = location.get('city')
        elif 'country_name' in location:
            dyn_properties['location'] = location.get('country_name')

    weather_prop = None
    if location:
        weather = get_weather('{city},{country}'.format(
            city=location['city'], country=location['country_code']))
        weather_prop = parse_weather(weather)
    if weather_prop:
        dyn_properties.update(weather_prop)
    logger.info("Update dynamic properties {}".format(dyn_properties))
Beispiel #3
0
def load_dyn_properties():
    global dyn_properties
    location = get_location()
    if location:
        if 'city' in location:
            dyn_properties['location'] = location.get('city')
        elif 'country_name' in location:
            dyn_properties['location'] = location.get('country_name')

    weather_prop = None
    if location:
        weather = get_weather(
            '{city},{country}'.format(
                city=location['city'], country=location['country_code']))
        weather_prop = parse_weather(weather)
    if weather_prop:
        dyn_properties.update(weather_prop)
    logger.info("Update dynamic properties {}".format(dyn_properties))
Beispiel #4
0
def _ask_characters(characters, question, lang, sid, query, request_id,
                    **kwargs):
    sess = session_manager.get_session(sid)
    if sess is None:
        return

    used_charaters = []
    data = sess.get_session_data()
    user = getattr(data, 'user')
    botname = getattr(data, 'botname')
    weights = get_weights(characters, sess)
    weighted_characters = zip(characters, weights)
    logger.info("Weights {}".format(weights))

    _question = preprocessing(question, lang, sess)
    response = {}
    hit_character = None
    answer = None
    cross_trace = []
    cached_responses = defaultdict(list)

    control = get_character('control')
    if control is not None:
        _response = control.respond(_question, lang, sess, query, request_id)
        _answer = _response.get('text')
        if _answer == '[tell me more]':
            cross_trace.append((control.id, 'control', _response.get('trace')
                                or 'No trace'))
            if sess.last_used_character:
                if sess.cache.that_question is None:
                    sess.cache.that_question = sess.cache.last_question
                context = sess.last_used_character.get_context(sess)
                if 'continue' in context and context.get('continue'):
                    _answer, res = shorten(context.get('continue'), 140)
                    response['text'] = answer = _answer
                    response['botid'] = sess.last_used_character.id
                    response['botname'] = sess.last_used_character.name
                    sess.last_used_character.set_context(
                        sess, {'continue': res})
                    hit_character = sess.last_used_character
                    cross_trace.append((sess.last_used_character.id,
                                        'continuation', 'Non-empty'))
                else:
                    _question = sess.cache.that_question.lower().strip()
                    cross_trace.append(
                        (sess.last_used_character.id, 'continuation', 'Empty'))
        elif _answer.startswith('[weather]'):
            template = _answer.replace('[weather]', '')
            cross_trace.append((control.id, 'control', _response.get('trace')
                                or 'No trace'))
            context = control.get_context(sess)
            if context:
                location = context.get('querylocation')
                prop = parse_weather(get_weather(location))
                if prop:
                    try:
                        _answer = template.format(location=location, **prop)
                        if _answer:
                            answer = _answer
                            response['text'] = _answer
                            response['botid'] = control.id
                            response['botname'] = control.name
                    except Exception as ex:
                        cross_trace.append(
                            (control.id, 'control', 'No answer'))
                        logger.error(ex)
                        logger.error(traceback.format_exc())
                else:
                    cross_trace.append((control.id, 'control', 'No answer'))
        elif _answer in OPERATOR_MAP.keys():
            opt = OPERATOR_MAP[_answer]
            cross_trace.append((control.id, 'control', _response.get('trace')
                                or 'No trace'))
            context = control.get_context(sess)
            if context:
                item1 = context.get('item1')
                item2 = context.get('item2')
                item1 = words2num(item1)
                item2 = words2num(item2)
                if item1 is not None and item2 is not None:
                    try:
                        result = opt(item1, item2)
                        img = math.modf(result)[0]
                        if img < 1e-6:
                            result_str = '{:d}'.format(int(result))
                        else:
                            result_str = 'about {:.4f}'.format(result)
                        if result > 1e20:
                            answer = "The number is too big. You should use a calculator."
                        else:
                            answer = "The answer is {result}".format(
                                result=result_str)
                    except ZeroDivisionError:
                        answer = "Oh, the answer is not a number"
                    except Exception as ex:
                        logger.error(ex)
                        logger.error(traceback.format_exc())
                        answer = "Sorry, something goes wrong. I can't calculate it."
                    response['text'] = answer
                    response['botid'] = control.id
                    response['botname'] = control.name
                else:
                    cross_trace.append((control.id, 'control', 'No answer'))
        else:
            if _answer and not re.findall(r'\[.*\].*', _answer):
                cross_trace.append(
                    (control.id, 'control', _response.get('trace')
                     or 'No trace'))
                hit_character = control
                answer = _answer
                response = _response
            else:
                cross_trace.append((control.id, 'control', 'No answer'))
            for c in characters:
                try:
                    c.remove_context(sess, 'continue')
                except NotImplementedError:
                    pass
            sess.cache.that_question = None

    def _ask_character(stage,
                       character,
                       weight,
                       good_match=False,
                       reuse=False):
        logger.info("Asking character {} \"{}\" in stage {}".format(
            character.id, _question, stage))

        if not reuse and character.id in used_charaters:
            cross_trace.append((character.id, stage, 'Skip used tier'))
            return False, None, None

        if character.id in used_charaters and character.type == TYPE_CS:
            cross_trace.append((character.id, stage, 'Skip CS tier'))
            return False, None, None

        used_charaters.append(character.id)
        answer = None
        answered = False

        if weight == 0:
            cross_trace.append((character.id, stage, 'Disabled'))
            logger.warn("Character \"{}\" in stage {} is disabled".format(
                character.id, stage))
            return False, None, None

        response = character.respond(_question, lang, sess, query, request_id)
        answer = str_cleanup(response.get('text', ''))
        trace = response.get('trace')

        if answer:
            if 'pickup' in character.id:
                cached_responses['pickup'].append(
                    (response, answer, character))
                return False, None, None
            if good_match:
                if response.get('exact_match') or response.get('ok_match'):
                    logger.info("{} has good match".format(character.id))
                    answered = True
                else:
                    if not response.get('bad'):
                        logger.info("{} has no good match".format(
                            character.id))
                        cross_trace.append(
                            (character.id, stage,
                             'No good match. Answer: {}, Trace: {}'.format(
                                 answer, trace)))
                        cached_responses['nogoodmatch'].append(
                            (response, answer, character))
            elif response.get('bad'):
                cross_trace.append(
                    (character.id, stage,
                     'Bad answer. Answer: {}, Trace: {}'.format(answer,
                                                                trace)))
                cached_responses['bad'].append((response, answer, character))
            elif DISABLE_QUIBBLE and response.get('quibble'):
                cross_trace.append(
                    (character.id, stage,
                     'Quibble answer. Answer: {}, Trace: {}'.format(
                         answer, trace)))
                cached_responses['quibble'].append(
                    (response, answer, character))
            elif response.get('gambit'):
                if random.random() > 0.3:
                    cross_trace.append(
                        (character.id, stage,
                         'Ignore gambit answer. Answer: {}, Trace: {}'.format(
                             answer, trace)))
                    cached_responses['gambit'].append(
                        (response, answer, character))
                else:
                    answered = True
            else:
                answered = True
            if answered:
                if random.random() < weight:
                    cross_trace.append(
                        (character.id, stage, 'Trace: {}'.format(trace)))
                else:
                    answered = False
                    cross_trace.append(
                        (character.id, stage,
                         'Pass through. Answer: {}, Weight: {}, Trace: {}'.
                         format(answer, weight, trace)))
                    logger.info("{} has no answer".format(character.id))
                    if 'markov' not in character.id:
                        cached_responses['pass'].append(
                            (response, answer, character))
                    else:
                        cached_responses['?'].append(
                            (response, answer, character))
        else:
            if response.get('repeat'):
                answer = response.get('repeat')
                cross_trace.append(
                    (character.id, stage,
                     'Repetitive answer. Answer: {}, Trace: {}'.format(
                         answer, trace)))
                cached_responses['repeat'].append(
                    (response, answer, character))
            else:
                logger.info("{} has no answer".format(character.id))
                cross_trace.append((character.id, stage,
                                    'No answer. Trace: {}'.format(trace)))
        return answered, answer, response

    # If the last input is a question, then try to use the same tier to
    # answer it.
    if not answer:
        if sess.open_character in characters:
            answered, _answer, _response = _ask_character('question',
                                                          sess.open_character,
                                                          1,
                                                          good_match=True)
            if answered:
                hit_character = sess.open_character
                answer = _answer
                response = _response

    # Try the first tier to see if there is good match
    if not answer:
        c, weight = weighted_characters[0]
        answered, _answer, _response = _ask_character('priority',
                                                      c,
                                                      weight,
                                                      good_match=True)
        if answered:
            hit_character = c
            answer = _answer
            response = _response

    # Select tier that is designed to be proper to answer the question
    if not answer:
        for c, weight in weighted_characters:
            if c.is_favorite(_question):
                answered, _answer, _response = _ask_character('favorite', c, 1)
                if answered:
                    hit_character = c
                    answer = _answer
                    response = _response

    # Check the last used character
    if not answer:
        if sess.last_used_character and sess.last_used_character.dynamic_level:
            for c, weight in weighted_characters:
                if sess.last_used_character.id == c.id:
                    answered, _answer, _response = _ask_character(
                        'last used', c, weight)
                    if answered:
                        hit_character = c
                        answer = _answer
                        response = _response
                    break

    # Check the loop
    if not answer:
        for c, weight in weighted_characters:
            answered, _answer, _response = _ask_character('loop',
                                                          c,
                                                          weight,
                                                          reuse=True)
            if answered:
                hit_character = c
                answer = _answer
                response = _response
                break

    if not answer:
        for response_type in [
                'pass', 'nogoodmatch', 'quibble', 'repeat', 'gambit', 'pickup',
                '?'
        ]:
            if cached_responses.get(response_type):
                response, answer, hit_character = cached_responses.get(
                    response_type)[0]
                if response_type == 'repeat':
                    pass
                response['text'] = answer
                cross_trace.append(
                    (hit_character.id, response_type, response.get('trace')
                     or 'No trace'))
                break

    if answer and re.match('.*{.*}.*', answer):
        logger.info("Template answer {}".format(answer))
        try:
            response['orig_text'] = answer
            answer = render(answer)
            response['text'] = answer
        except Exception as ex:
            answer = ''
            response['text'] = ''
            logger.error("Error in rendering template, {}".format(ex))

    dummy_character = get_character('dummy', lang)
    if not answer and dummy_character:
        if response.get('repeat'):
            response = dummy_character.respond("REPEAT_ANSWER", lang, sid,
                                               query)
        else:
            response = dummy_character.respond("NO_ANSWER", lang, sid, query)
        hit_character = dummy_character
        answer = str_cleanup(response.get('text', ''))

    if not query and hit_character is not None:
        response['AnsweredBy'] = hit_character.id
        sess.last_used_character = hit_character

        if is_question(answer.lower().strip()):
            if hit_character.dynamic_level:
                sess.open_character = hit_character
                logger.info("Set open dialog character {}".format(
                    hit_character.id))
        else:
            sess.open_character = None

    response['ModQuestion'] = _question
    response['trace'] = cross_trace
    return response
Beispiel #5
0
def _ask_characters(characters, question, lang, sid, query):
    sess = session_manager.get_session(sid)
    if sess is None:
        return

    used_charaters = []
    data = sess.get_session_data()
    user = getattr(data, 'user')
    botname = getattr(data, 'botname')
    weights = get_weights(characters, sess)
    weighted_characters = zip(characters, weights)
    logger.info("Weights {}".format(weighted_characters))

    _question = preprocessing(question)
    response = {}
    hit_character = None
    answer = None
    cross_trace = []

    cached_responses = defaultdict(list)

    reduction = get_character('reduction')
    if reduction is not None:
        _response = reduction.respond(_question, lang, sess, query=True)
        reducted_text = _response.get('text')
        if reducted_text:
            _question = reducted_text

    control = get_character('control')
    if control is not None:
        _response = control.respond(_question, lang, sess, query)
        _answer = _response.get('text')
        if _answer == '[tell me more]':
            cross_trace.append((control.id, 'control', _response.get('trace') or 'No trace'))
            if sess.last_used_character:
                if sess.cache.that_question is None:
                    sess.cache.that_question = sess.cache.last_question
                context = sess.last_used_character.get_context(sess)
                if 'continue' in context and context.get('continue'):
                    _answer, res = shorten(context.get('continue'), 140)
                    response['text'] = answer = _answer
                    response['botid'] = sess.last_used_character.id
                    response['botname'] = sess.last_used_character.name
                    sess.last_used_character.set_context(sess, {'continue': res})
                    hit_character = sess.last_used_character
                    cross_trace.append((sess.last_used_character.id, 'continuation', 'Non-empty'))
                else:
                    _question = sess.cache.that_question.lower().strip()
                    cross_trace.append((sess.last_used_character.id, 'continuation', 'Empty'))
        elif _answer.startswith('[weather]'):
            template = _answer.replace('[weather]', '')
            cross_trace.append((control.id, 'control', _response.get('trace') or 'No trace'))
            context = control.get_context(sess)
            if context:
                location = context.get('querylocation')
                prop = parse_weather(get_weather(location))
                if prop:
                    try:
                        _answer = template.format(location=location, **prop)
                        if _answer:
                            answer = _answer
                            response['text'] = _answer
                            response['botid'] = control.id
                            response['botname'] = control.name
                    except Exception as ex:
                        cross_trace.append((control.id, 'control', 'No answer'))
                        logger.error(ex)
                else:
                    cross_trace.append((control.id, 'control', 'No answer'))
        elif _answer in OPERATOR_MAP.keys():
            opt = OPERATOR_MAP[_answer]
            cross_trace.append((control.id, 'control', _response.get('trace') or 'No trace'))
            context = control.get_context(sess)
            if context:
                item1 = context.get('item1')
                item2 = context.get('item2')
                item1 = words2num(item1)
                item2 = words2num(item2)
                if item1 is not None and item2 is not None:
                    try:
                        result = opt(item1, item2)
                        img = math.modf(result)[0]
                        if img < 1e-6:
                            result_str = '{:d}'.format(int(result))
                        else:
                            result_str = 'about {:.4f}'.format(result)
                        if result > 1e20:
                            answer = "The number is too big. You should use a calculator."
                        else:
                            answer = "The answer is {result}".format(result=result_str)
                    except ZeroDivisionError:
                        answer = "Oh, the answer is not a number"
                    except Exception as ex:
                        logger.error(ex)
                        answer = "Sorry, something goes wrong. I can't calculate it."
                    response['text'] = answer
                    response['botid'] = control.id
                    response['botname'] = control.name
                else:
                    cross_trace.append((control.id, 'control', 'No answer'))
        else:
            if _answer and not re.findall(r'\[.*\].*', _answer):
                cross_trace.append((control.id, 'control', _response.get('trace') or 'No trace'))
                hit_character = control
                answer = _answer
                response = _response
            else:
                cross_trace.append((control.id, 'control', 'No answer'))
            for c in characters:
                try:
                    c.remove_context(sess, 'continue')
                except NotImplementedError:
                    pass
            sess.cache.that_question = None

    # If the last input is a question, then try to use the same tier to
    # answer it.
    if not answer:
        if sess.open_character and sess.open_character in characters \
                and sess.open_character.weight != 0:
            logger.info("Using open dialog character {}".format(sess.open_character.id))
            response = sess.open_character.respond(_question, lang, sess, query)
            used_charaters.append(sess.open_character.id)
            _answer = str_cleanup(response.get('text', ''))
            if _answer:
                if response.get('repeat'):
                    cross_trace.append((sess.open_character.id, 'question', 'Repetitive answer'))
                    cached_responses['repeat'].append((response, response.get('repeat'), sess.open_character))
                elif response.get('bad'):
                    cross_trace.append((sess.open_character.id, 'question', 'Bad answer: {}'.format(response.get('trace'))))
                    cached_responses['bad'].append((response, _answer, sess.open_character))
                else:
                    answer = _answer
                    hit_character = sess.open_character
                    cross_trace.append((sess.open_character.id, 'question', response.get('trace') or 'No trace'))
            else:
                cross_trace.append((sess.open_character.id, 'question', 'No answer'))

    # Try the first tier to see if there is good match
    if not answer:
        c, weight = weighted_characters[0]
        if c.id == botname and weight != 0:
            _response = c.respond(_question, lang, sess, query=True)
            _answer = str_cleanup(_response.get('text', ''))
            if _response.get('exact_match') or _response.get('ok_match'):
                logger.info("{} has good match".format(c.id))
                response = c.respond(_question, lang, sess, query)
                _answer = str_cleanup(response.get('text', ''))
                used_charaters.append(c.id)
                if _answer:
                    if random.random() < weight:
                        hit_character = c
                        answer = _answer
                        cross_trace.append((c.id, 'priority', response.get('trace') or 'No trace'))
                    else:
                        cross_trace.append((c.id, 'priority', 'Pass through'))
                        cached_responses['pass'].append((response, _answer, c))
                elif response.get('repeat'):
                    cross_trace.append((c.id, 'priority', 'Repetitive answer'))
                    cached_responses['repeat'].append((response, response.get('repeat'), c))
                else:
                    cross_trace.append((c.id, 'priority', 'No answer'))
            elif _answer:
                logger.info("{} has no good match".format(c.id))
                cross_trace.append((c.id, 'priority', 'No good match: {}'.format(_response.get('trace') or 'No trace')))
                cached_responses['nogoodmatch'].append((_response, _answer, c))
            else:
                cross_trace.append((c.id, 'priority', 'No answer'))

    # Select tier that is designed to be proper to answer the question
    if not answer:
        for c, weight in weighted_characters:
            if weight != 0 and c.is_favorite(_question):
                _response = c.respond(_question, lang, sess, query)
                _answer = str_cleanup(_response.get('text'))
                if _answer:
                    hit_character = c
                    cross_trace.append((c.id, 'favorite', response.get('trace') or 'No trace'))
                    answer = _answer
                    response = _response
                    break
                else:
                    if _response.get('repeat'):
                        cross_trace.append((c.id, 'favorite', 'Repetitive answer'))
                        cached_responses['repeat'].append((_response, _response.get('repeat'), c))
                    else:
                        cross_trace.append((c.id, 'favorite', 'No answer'))

    # Check the last used character
    if not answer:
        if sess.last_used_character and sess.last_used_character.dynamic_level \
                and sess.last_used_character.weight != 0:
            if sess.last_used_character.id in used_charaters:
                cross_trace.append((sess.last_used_character.id, 'last used', 'Skip used tier'))
            else:
                for c, weight in weighted_characters:
                    if sess.last_used_character.id == c.id:
                        _response = c.respond(_question, lang, sess, query=True)
                        _answer = str_cleanup(_response.get('text', ''))
                        if _response.get('exact_match') or _response.get('ok_match'):
                            logger.info("Last used tier {} has good match".format(c.id))
                            if sess.last_used_character.type == TYPE_CS:
                                response = _response
                            else:
                                response = c.respond(_question, lang, sess, query)
                                _answer = str_cleanup(response.get('text', ''))
                            used_charaters.append(c.id)
                            if _answer:
                                if random.random() < weight:
                                    hit_character = c
                                    answer = _answer
                                    cross_trace.append((c.id, 'last used', response.get('trace') or 'No trace'))
                                else:
                                    cross_trace.append((c.id, 'last used', 'Pass through'))
                                    cached_responses['pass'].append((response, _answer, c))
                            else:
                                if response.get('repeat'):
                                    cross_trace.append((c.id, 'last used', 'Repetitive answer'))
                                    cached_responses['repeat'].append((response, response.get('repeat'), c))
                                else:
                                    cross_trace.append((c.id, 'last used', 'No answer'))
                        elif _answer:
                            logger.info("{} has no good match".format(c.id))
                            cross_trace.append((c.id, 'last used', 'No good match: {}'.format(_response.get('trace') or 'No trace')))
                            cached_responses['nogoodmatch'].append((_response, _answer, c))
                        else:
                            cross_trace.append((c.id, 'last used', 'No answer'))
                        break

    # Check the loop
    if not answer:
        for c, weight in weighted_characters:
            if weight == 0:
                logger.info("Ignore zero weighted character {}".format(c.id))
                continue

            if c.id in used_charaters:
                logger.info("Ignore used tiers {}".format(c.id))
                cross_trace.append((c.id, 'loop', 'Skip used tier'))
                continue

            response = c.respond(_question, lang, sess, query)
            used_charaters.append(c.id)
            assert isinstance(response, dict), "Response must be a dict"

            _answer = str_cleanup(response.get('text', ''))
            if not _answer:
                if response.get('repeat'):
                    cross_trace.append((c.id, 'loop', 'Repetitive answer'))
                    cached_responses['repeat'].append((response, response.get('repeat'), c))
                else:
                    cross_trace.append((c.id, 'loop', 'No answer'))
                continue

            if response.get('bad'):
                cross_trace.append((c.id, 'loop', 'Bad answer: {}'.format(response.get('trace'))))
                cached_responses['bad'].append((response, _answer, c))
                continue

            if DISABLE_QUIBBLE and response.get('quibble'):
                logger.info("Ignore quibbled answer by {}".format(c.id))
                cross_trace.append((c.id, 'loop', 'Quibble answer'))
                cached_responses['quibble'].append((response, _answer, c))
                continue

            if response.get('gambit'):
                if random.random() > 0.3:
                    cached_responses['gambit'].append((response, _answer, c))
                    cross_trace.append((c.id, 'loop', 'Ignore gambit answer'))
                    logger.info("Ignore gambit response")
                    continue

            if 'pickup' in c.id:
                cached_responses['pickup'].append((response, _answer, c))

            # Each tier has weight*100% chance to be selected.
            # If the chance goes to the last tier, it will be selected anyway.
            if random.random() < weight:
                answer = _answer
                hit_character = c
                cross_trace.append((c.id, 'loop', response.get('trace') or 'No trace'))
                break
            else:
                cross_trace.append((c.id, 'loop', 'Pass through'))
                if 'pickup' not in c.id and 'markov' not in c.id:
                    cached_responses['pass'].append((response, _answer, c))
                else:
                    cached_responses['?'].append((response, _answer, c))

    if not answer:
        for response_type in ['pass', 'nogoodmatch', 'quibble', 'repeat', 'gambit', 'pickup', 'bad', '?']:
            if cached_responses.get(response_type):
                response, answer, hit_character = cached_responses.get(response_type)[0]
                if response_type == 'repeat':
                    if len(answer) < 80:
                        answer = "Again. " + answer
                    elif 80 < len(answer) < 200:
                        answer = "Let me say again. " + answer
                    else:
                        continue
                response['text'] = answer
                cross_trace.append(
                    (hit_character.id, response_type,
                    response.get('trace') or 'No trace'))
                break

    dummy_character = get_character('dummy', lang)
    if not answer and dummy_character:
        if response.get('repeat'):
            response = dummy_character.respond("REPEAT_ANSWER", lang, sid, query)
        else:
            response = dummy_character.respond("NO_ANSWER", lang, sid, query)
        hit_character = dummy_character
        answer = str_cleanup(response.get('text', ''))

    if not query and hit_character is not None:
        sess.add(question, answer, AnsweredBy=hit_character.id,
                    User=user, BotName=botname, Trace=cross_trace,
                    Revision=REVISION, Lang=lang, ModQuestion=_question)

        sess.last_used_character = hit_character

        if answer.lower().strip().endswith('?'):
            if hit_character.dynamic_level:
                sess.open_character = hit_character
                logger.info("Set open dialog character {}".format(
                            hit_character.id))
        else:
            sess.open_character = None

    response['trace'] = cross_trace
    return response
Beispiel #6
0
def _ask_characters(characters, question, lang, sid, query, request_id, **kwargs):
    sess = session_manager.get_session(sid)
    if sess is None:
        return

    used_charaters = []
    data = sess.get_session_data()
    user = getattr(data, 'user')
    botname = getattr(data, 'botname')
    weights = get_weights(characters, sess)
    weighted_characters = zip(characters, weights)
    logger.info("Weights {}".format(weighted_characters))

    _question = preprocessing(question, lang, sess)
    response = {}
    hit_character = None
    answer = None
    cross_trace = []
    cached_responses = defaultdict(list)

    control = get_character('control')
    if control is not None:
        _response = control.respond(_question, lang, sess, query, request_id)
        _answer = _response.get('text')
        if _answer == '[tell me more]':
            cross_trace.append((control.id, 'control', _response.get('trace') or 'No trace'))
            if sess.last_used_character:
                if sess.cache.that_question is None:
                    sess.cache.that_question = sess.cache.last_question
                context = sess.last_used_character.get_context(sess)
                if 'continue' in context and context.get('continue'):
                    _answer, res = shorten(context.get('continue'), 140)
                    response['text'] = answer = _answer
                    response['botid'] = sess.last_used_character.id
                    response['botname'] = sess.last_used_character.name
                    sess.last_used_character.set_context(sess, {'continue': res})
                    hit_character = sess.last_used_character
                    cross_trace.append((sess.last_used_character.id, 'continuation', 'Non-empty'))
                else:
                    _question = sess.cache.that_question.lower().strip()
                    cross_trace.append((sess.last_used_character.id, 'continuation', 'Empty'))
        elif _answer.startswith('[weather]'):
            template = _answer.replace('[weather]', '')
            cross_trace.append((control.id, 'control', _response.get('trace') or 'No trace'))
            context = control.get_context(sess)
            if context:
                location = context.get('querylocation')
                prop = parse_weather(get_weather(location))
                if prop:
                    try:
                        _answer = template.format(location=location, **prop)
                        if _answer:
                            answer = _answer
                            response['text'] = _answer
                            response['botid'] = control.id
                            response['botname'] = control.name
                    except Exception as ex:
                        cross_trace.append((control.id, 'control', 'No answer'))
                        logger.error(ex)
                else:
                    cross_trace.append((control.id, 'control', 'No answer'))
        elif _answer in OPERATOR_MAP.keys():
            opt = OPERATOR_MAP[_answer]
            cross_trace.append((control.id, 'control', _response.get('trace') or 'No trace'))
            context = control.get_context(sess)
            if context:
                item1 = context.get('item1')
                item2 = context.get('item2')
                item1 = words2num(item1)
                item2 = words2num(item2)
                if item1 is not None and item2 is not None:
                    try:
                        result = opt(item1, item2)
                        img = math.modf(result)[0]
                        if img < 1e-6:
                            result_str = '{:d}'.format(int(result))
                        else:
                            result_str = 'about {:.4f}'.format(result)
                        if result > 1e20:
                            answer = "The number is too big. You should use a calculator."
                        else:
                            answer = "The answer is {result}".format(result=result_str)
                    except ZeroDivisionError:
                        answer = "Oh, the answer is not a number"
                    except Exception as ex:
                        logger.error(ex)
                        answer = "Sorry, something goes wrong. I can't calculate it."
                    response['text'] = answer
                    response['botid'] = control.id
                    response['botname'] = control.name
                else:
                    cross_trace.append((control.id, 'control', 'No answer'))
        else:
            if _answer and not re.findall(r'\[.*\].*', _answer):
                cross_trace.append((control.id, 'control', _response.get('trace') or 'No trace'))
                hit_character = control
                answer = _answer
                response = _response
            else:
                cross_trace.append((control.id, 'control', 'No answer'))
            for c in characters:
                try:
                    c.remove_context(sess, 'continue')
                except NotImplementedError:
                    pass
            sess.cache.that_question = None

    def _ask_character(stage, character, weight, good_match=False, reuse=False):
        logger.info("Asking character {} in stage {}".format(character.id, stage))

        if not reuse and character.id in used_charaters:
            cross_trace.append((character.id, stage, 'Skip used tier'))
            return False, None, None

        if character.id in used_charaters and character.type == TYPE_CS:
            cross_trace.append((character.id, stage, 'Skip CS tier'))
            return False, None, None

        used_charaters.append(character.id)
        answer = None
        answered = False

        if weight == 0:
            cross_trace.append((character.id, stage, 'Disabled'))
            return False, None, None

        response = character.respond(_question, lang, sess, query, request_id)
        answer = str_cleanup(response.get('text', ''))
        trace = response.get('trace')

        if answer:
            if 'pickup' in character.id:
                cached_responses['pickup'].append((response, answer, character))
                return False, None, None
            if good_match:
                if response.get('exact_match') or response.get('ok_match'):
                    logger.info("{} has good match".format(character.id))
                    answered = True
                else:
                    logger.info("{} has no good match".format(character.id))
                    cross_trace.append((character.id, stage, 'No good match. Answer: {}, Trace: {}'.format(answer, trace)))
                    cached_responses['nogoodmatch'].append((response, answer, character))
            elif response.get('bad'):
                cross_trace.append((character.id, stage, 'Bad answer. Answer: {}, Trace: {}'.format(answer, trace)))
                cached_responses['bad'].append((response, answer, character))
            elif DISABLE_QUIBBLE and response.get('quibble'):
                cross_trace.append((character.id, stage, 'Quibble answer. Answer: {}, Trace: {}'.format(answer, trace)))
                cached_responses['quibble'].append((response, answer, character))
            elif response.get('gambit'):
                if random.random() > 0.3:
                    cross_trace.append((character.id, stage, 'Ignore gambit answer. Answer: {}, Trace: {}'.format(answer, trace)))
                    cached_responses['gambit'].append((response, answer, character))
                else:
                    answered = True
            else:
                answered = True
            if answered:
                if random.random() < weight:
                    cross_trace.append((character.id, stage, 'Trace: {}'.format(trace)))
                else:
                    answered = False
                    cross_trace.append((character.id, stage, 'Pass through. Answer: {}, Weight: {}, Trace: {}'.format(answer, weight, trace)))
                    logger.info("{} has no answer".format(character.id))
                    if 'markov' not in character.id:
                        cached_responses['pass'].append((response, answer, character))
                    else:
                        cached_responses['?'].append((response, answer, character))
        else:
            if response.get('repeat'):
                answer = response.get('repeat')
                cross_trace.append((character.id, stage, 'Repetitive answer. Answer: {}, Trace: {}'.format(answer, trace)))
                cached_responses['repeat'].append((response, answer, character))
            else:
                logger.info("{} has no answer".format(character.id))
                cross_trace.append((character.id, stage, 'No answer. Trace: {}'.format(trace)))
        return answered, answer, response

    # If the last input is a question, then try to use the same tier to
    # answer it.
    if not answer:
        if sess.open_character in characters:
            answered, _answer, _response = _ask_character(
                'question', sess.open_character, 1, good_match=True)
            if answered:
                hit_character = sess.open_character
                answer = _answer
                response = _response

    # Try the first tier to see if there is good match
    if not answer:
        c, weight = weighted_characters[0]
        answered, _answer, _response = _ask_character(
            'priority', c, weight, good_match=True)
        if answered:
            hit_character = c
            answer = _answer
            response = _response

    # Select tier that is designed to be proper to answer the question
    if not answer:
        for c, weight in weighted_characters:
            if c.is_favorite(_question):
                answered, _answer, _response = _ask_character(
                    'favorite', c, 1)
                if answered:
                    hit_character = c
                    answer = _answer
                    response = _response

    # Check the last used character
    if not answer:
        if sess.last_used_character and sess.last_used_character.dynamic_level:
            for c, weight in weighted_characters:
                if sess.last_used_character.id == c.id:
                    answered, _answer, _response = _ask_character(
                        'last used', c, weight)
                    if answered:
                        hit_character = c
                        answer = _answer
                        response = _response
                    break

    # Check the loop
    if not answer:
        for c, weight in weighted_characters:
            answered, _answer, _response = _ask_character(
                'loop', c, weight, reuse=True)
            if answered:
                hit_character = c
                answer = _answer
                response = _response
                break

    if not answer:
        for response_type in ['pass', 'nogoodmatch', 'quibble', 'repeat', 'gambit', 'pickup', 'bad', '?']:
            if cached_responses.get(response_type):
                response, answer, hit_character = cached_responses.get(response_type)[0]
                if response_type == 'repeat':
                    if len(answer) < 80:
                        answer = "Again. " + answer
                    elif 80 < len(answer) < 200:
                        answer = "Let me say again. " + answer
                    else:
                        continue
                response['text'] = answer
                cross_trace.append(
                    (hit_character.id, response_type,
                    response.get('trace') or 'No trace'))
                break

    if answer and re.match('.*{.*}.*', answer):
        logger.info("Template answer {}".format(answer))
        try:
            response['orig_text'] = answer
            answer = render(answer)
            response['text'] = answer
        except Exception as ex:
            answer = ''
            response['text'] = ''
            logger.error("Error in rendering template, {}".format(ex))

    dummy_character = get_character('dummy', lang)
    if not answer and dummy_character:
        if response.get('repeat'):
            response = dummy_character.respond("REPEAT_ANSWER", lang, sid, query)
        else:
            response = dummy_character.respond("NO_ANSWER", lang, sid, query)
        hit_character = dummy_character
        answer = str_cleanup(response.get('text', ''))

    if not query and hit_character is not None:
        sess.add(question, answer, AnsweredBy=hit_character.id,
                    User=user, BotName=botname, Trace=cross_trace,
                    Revision=REVISION, Lang=lang, ModQuestion=_question,
                    RequestId=request_id,Marker=kwargs.get('marker'))

        sess.last_used_character = hit_character

        if is_question(answer.lower().strip()):
            if hit_character.dynamic_level:
                sess.open_character = hit_character
                logger.info("Set open dialog character {}".format(
                            hit_character.id))
        else:
            sess.open_character = None

    response['trace'] = cross_trace
    return response