Ejemplo n.º 1
0
def train(parser, args):
    if bool(args.input_files) == bool(args.data):
        parser.error(
            'You must specify one of input_files or --data (but not both)')

    cont = IntentContainer(args.intent_cache)
    if args.data:
        cont.apply_training_args(args.data)
    else:
        for fn in args.input_files:
            obj_name, ext = splitext(basename(fn))
            if ext == '.intent':
                cont.load_intent(obj_name, fn)
            elif ext == '.entity':
                cont.load_entity(obj_name, fn)
            else:
                parser.error('Unknown file extension: {}'.format(ext))
    kwargs = inspect.signature(cont.train).bind(*(args.args or [])).arguments
    kwargs.update(args.kwargs or {})
    kwargs.setdefault('debug', True)
    kwargs.setdefault('single_thread', args.single_thread)
    kwargs.setdefault('force', args.force)
    if cont.train(**kwargs):
        return 0
    return 10  # timeout
Ejemplo n.º 2
0
def create_container():
    container = IntentContainer('intent_cache')
    dir = os.getenv('VOCAB_DIR', '/qabot/vocab/en-us/')
    for file in os.listdir(dir):
        print(file)
        if file.endswith(".intent"):
            container.load_intent(basename(file), join(dir, file))
        elif file.endswith(".entity"):
            container.load_entity(basename(file), join(dir, file))

    container.train()
    return container
Ejemplo n.º 3
0
class PadatiousFileIntent(IntentPlugin):
    """Interface for Padatious intent engine"""
    def __init__(self, rt):
        super().__init__(rt)
        self.container = IntentContainer(
            join(rt.paths.user_config, 'intent_cache'))

    def register(self, intent: Any, skill_name: str, intent_id: str):
        file_name = join(self.rt.paths.skill_locale(skill_name),
                         intent + '.intent')
        self.container.load_intent(name=intent_id, file_name=file_name)

    def register_entity(self, entity: Any, entity_id: str, skill_name: str):
        file_name = join(self.rt.paths.skill_locale(skill_name),
                         entity + '.intent')
        self.container.load_intent(name=entity_id, file_name=file_name)

    def unregister(self, intent_id: str):
        self.container.remove_intent(intent_id)

    def unregister_entity(self, entity_id: str):
        self.container.remove_entity(entity_id)

    def compile(self):
        log.info('Training...')
        self.container.train()
        log.info('Training complete!')

    def calc_intents(self, query):
        return [
            IntentMatch(intent_id=data.name,
                        confidence=data.conf,
                        matches=data.matches,
                        query=query)
            for data in self.container.calc_intents(query)
        ]
Ejemplo n.º 4
0
class DuckDuckGoSkill(CommonQuerySkill):
    def __init__(self):
        super().__init__()
        self.translator = google_translator()
        self.tx_cache = {}  # avoid translating twice
        self.duck_cache = {}
        self.rake = Rake()  # only english for now

        # for usage in tell me more / follow up questions
        self.idx = 0
        self.results = []
        self.image = None

        # subparser, intents just for this skill
        # not part of main intent service
        intent_cache = expanduser(
            self.config_core['padatious']['intent_cache'])
        self.intents = IntentContainer(intent_cache)

    def initialize(self):
        self.load_intents()
        # check for conflicting skills just in case
        # done after all skills loaded to ensure proper shutdown
        self.add_event("mycroft.skills.initialized",
                       self.blacklist_default_skill)

    def load_intents(self):
        # TODO intents for other infobox fields
        for intent in ["who", "birthdate"]:
            path = self.find_resource(intent + '.intent', "locale")
            if path:
                self.intents.load_intent(intent, path)

        self.intents.train(single_thread=True)

    def get_intro_message(self):
        # blacklist conflicting skills on install
        self.blacklist_default_skill()

    def blacklist_default_skill(self):
        # load the current list of already blacklisted skills
        blacklist = self.config_core["skills"]["blacklisted_skills"]

        # check the folder name (skill_id) of the skill you want to replace
        skill_id = "mycroft-fallback-duck-duck-go.mycroftai"

        # add the skill to the blacklist
        if skill_id not in blacklist:
            self.log.debug("Blacklisting official mycroft skill")
            blacklist.append(skill_id)

            # load the user config file (~/.mycroft/mycroft.conf)
            conf = LocalConf(USER_CONFIG)
            if "skills" not in conf:
                conf["skills"] = {}

            # update the blacklist field
            conf["skills"]["blacklisted_skills"] = blacklist

            # save the user config file
            conf.store()

        # tell the intent service to unload the skill in case it was loaded already
        # this should avoid the need to restart
        self.bus.emit(Message("detach_skill", {"skill_id": skill_id}))

    def stop(self):
        self.gui.release()

    # intents
    @intent_handler("search_duck.intent")
    def handle_search(self, message):
        query = message.data["query"]
        summary = self.ask_the_duck(query)
        if summary:
            self.speak_result()
        else:
            answer, _, _ = self.parse_subintents(query)
            if answer:
                self.speakr(answer)
            else:
                self.speak_dialog("no_answer")

    @intent_handler(
        IntentBuilder("DuckMore").require("More").require("DuckKnows"))
    def handle_tell_more(self, message):
        """ Follow up query handler, "tell me more"."""
        query = message.data["DuckKnows"]
        data, related_queries = self.get_infobox(query)
        # TODO maybe do something with the infobox data ?
        self.speak_result()

    # common query
    def parse_subintents(self, utt):
        # Get response from intents, this is a subparser that will handle
        # queries about the infobox returned by duckduckgo
        # eg. when was {person} born

        match = self.intents.calc_intent(utt)
        level = CQSMatchLevel.CATEGORY
        data = match.matches
        intent = match.name
        score = match.conf
        data["intent"] = intent
        data["score"] = score
        query = utt

        if score > 0.8:
            level = CQSMatchLevel.EXACT
        elif score > 0.5:
            level = CQSMatchLevel.CATEGORY
        elif score > 0.3:
            level = CQSMatchLevel.GENERAL
        else:
            intent = None

        self.log.debug("DuckDuckGo Intent: " + str(intent))
        if "person" in data:
            query = data["person"]

        summary = self.ask_the_duck(query)
        answer = summary
        if summary:
            answer = self.results[0]
            infobox, related_queries = self.get_infobox(query)
            self.log.debug("DuckDuckGo infobox: " + str(infobox))
            data["infobox"] = infobox
            data["related_queries"] = related_queries

            if intent == "birthdate":
                answer = infobox.get("born")

            data["query"] = query
            data["answer"] = answer
            data["image"] = self.image
        if not answer:
            level = CQSMatchLevel.GENERAL
        return answer, level, data

    def CQS_match_query_phrase(self, utt):
        self.log.debug("DuckDuckGo query: " + utt)

        answer, match, data = self.parse_subintents(utt)
        if answer:
            self.idx += 1
            return (utt, match, answer, data)

        # extract most relevant keyword
        utt = self.translate(utt, "en", self.lang)
        keywords = self.rake.extract_keywords(utt)

        self.log.debug("Extracted keywords: " + str(keywords))
        # TODO better selection / merging of top keywords with same
        #  confidence??
        for kw in keywords:
            query = kw[0]
            self.log.debug("Selected keyword: " + query)

            summary = self.ask_the_duck(query, translate=False)
            if summary:
                self.idx += 1
                return (utt, CQSMatchLevel.GENERAL, self.results[0], {
                    'query': query,
                    'answer': self.results[0],
                    "keywords": keywords,
                    "image": self.image
                })

    def CQS_action(self, phrase, data):
        """ If selected show gui """
        self.display_ddg(data["answer"], data["image"])

    # duck duck go api
    def ask_the_duck(self, query, translate=True):
        if translate:
            # Automatic translation to English
            utt = self.translate(query, "en", self.lang)
        else:
            utt = query

        # cache so we dont hit the api twice for the same query
        if query not in self.duck_cache:
            self.duck_cache[query] = requests.get("https://api.duckduckgo.com",
                                                  params={
                                                      "format": "json",
                                                      "q": utt
                                                  }).json()
        data = self.duck_cache[query]

        # GUI
        self.gui.clear()  # clear previous answer just in case
        title = data.get("Heading")
        self.image = data.get("Image", "")

        # summary
        summary = data.get("AbstractText")

        if not summary:
            return None

        self.log.debug("DuckDuckGo answer: " + summary)

        # context for follow up questions
        # TODO intents for this, with this context intents can look up all data
        self.set_context("DuckKnows", query)
        self.idx = 0
        self.results = summary.split(". ")
        return summary

    def display_ddg(self, summary, image):
        if image.startswith("/"):
            image = "https://duckduckgo.com" + image
        self.gui['summary'] = summary
        self.gui['imgLink'] = image
        self.gui.show_page("DuckDelegate.qml", override_idle=60)

    def speak_result(self):
        if self.idx + 1 > len(self.results):
            # TODO ask user if he wants to hear about related topics
            self.speak_dialog("thats all")
            self.remove_context("ddg")
            self.idx = 0
        else:
            if self.image:
                self.display_ddg(self.results[self.idx], self.image)
            self.speak(self.results[self.idx])
            self.idx += 1

    def get_infobox(self, query):
        if query not in self.duck_cache:
            self.ask_the_duck(query)
        data = self.duck_cache[query]
        # info
        related_topics = [t.get("Text") for t in data.get("RelatedTopics", [])]
        infobox = {}
        infodict = data.get("Infobox") or {}
        for entry in infodict.get("content", []):
            k = entry["label"].lower().strip()
            infobox[k] = entry["value"]
        return infobox, related_topics

    def translate(self, utterance, lang_tgt=None, lang_src="en"):
        lang_tgt = lang_tgt or self.lang

        # if langs are the same do nothing
        if not lang_tgt.startswith(lang_src):
            if lang_tgt not in self.tx_cache:
                self.tx_cache[lang_tgt] = {}
            # if translated before, dont translate again
            if utterance in self.tx_cache[lang_tgt]:
                # get previous translated value
                translated_utt = self.tx_cache[lang_tgt][utterance]
            else:
                # translate this utterance
                translated_utt = self.translator.translate(
                    utterance, lang_tgt=lang_tgt, lang_src=lang_src).strip()
                # save the translation if we need it again
                self.tx_cache[lang_tgt][utterance] = translated_utt
            self.log.debug("translated {src} -- {tgt}".format(
                src=utterance, tgt=translated_utt))
        else:
            translated_utt = utterance.strip()
        return translated_utt
Ejemplo n.º 5
0
class PadatiousExtractor(IntentExtractor):
    keyword_based = False

    def __init__(self, cache_dir=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # TODO xdg data_dir
        data_dir = expanduser(self.config.get("data_dir", "~/.padatious"))
        cache_dir = cache_dir or join(data_dir, "padatious")
        self.lock = Lock()
        self.container = IntentContainer(cache_dir)
        self.registered_intents = []

    def detach_intent(self, intent_name):
        if intent_name in self.registered_intents:
            LOG.debug("Detaching padatious intent: " + intent_name)
            with self.lock:
                self.container.remove_intent(intent_name)
            self.registered_intents.remove(intent_name)

    def detach_skill(self, skill_id):
        LOG.debug("Detaching padatious skill: " + str(skill_id))
        remove_list = [i for i in self.registered_intents if skill_id in i]
        for i in remove_list:
            self.detach_intent(i)

    def register_entity(self, entity_name, samples=None, reload_cache=True):
        samples = samples or [entity_name]
        with self.lock:
            self.container.add_entity(entity_name,
                                      samples,
                                      reload_cache=reload_cache)

    def register_intent(self, intent_name, samples=None, reload_cache=True):
        samples = samples or [intent_name]
        if intent_name not in self._intent_samples:
            self._intent_samples[intent_name] = samples
        else:
            self._intent_samples[intent_name] += samples
        with self.lock:
            self.container.add_intent(intent_name,
                                      samples,
                                      reload_cache=reload_cache)
        self.registered_intents.append(intent_name)

    def register_entity_from_file(self,
                                  entity_name,
                                  file_name,
                                  reload_cache=True):
        with self.lock:
            self.container.load_entity(entity_name,
                                       file_name,
                                       reload_cache=reload_cache)

    def register_intent_from_file(self,
                                  intent_name,
                                  file_name,
                                  single_thread=True,
                                  timeout=120,
                                  reload_cache=True,
                                  force_training=True):
        try:
            with self.lock:
                self.container.load_intent(intent_name,
                                           file_name,
                                           reload_cache=reload_cache)
            self.registered_intents.append(intent_name)
            success = self._train(single_thread=single_thread,
                                  timeout=timeout,
                                  force_training=force_training)
            if success:
                LOG.debug(file_name + " trained successfully")
            else:
                LOG.error(file_name + " FAILED TO TRAIN")

        except Exception as e:
            LOG.exception(e)

    def _get_remainder(self, intent, utterance):
        if intent["name"] in self.intent_samples:
            return get_utterance_remainder(
                utterance, samples=self.intent_samples[intent["name"]])
        return utterance

    def calc_intent(self, utterance, min_conf=None):
        min_conf = min_conf or self.config.get("padatious_min_conf", 0.65)
        utterance = utterance.strip().lower()
        with self.lock:
            intent = self.container.calc_intent(utterance).__dict__
        if intent["conf"] < min_conf:
            return {
                "intent_type": "unknown",
                "entities": {},
                "conf": 0,
                "intent_engine": "padatious",
                "utterance": utterance,
                "utterance_remainder": utterance
            }
        intent["utterance_remainder"] = self._get_remainder(intent, utterance)
        intent["entities"] = intent.pop("matches")
        intent["intent_engine"] = "padatious"
        intent["intent_type"] = intent.pop("name")
        intent["utterance"] = intent.pop("sent")

        if isinstance(intent["utterance"], list):
            intent["utterance"] = " ".join(intent["utterance"])
        return intent

    def intent_scores(self, utterance):
        utterance = utterance.strip().lower()
        intents = [i.__dict__ for i in self.container.calc_intents(utterance)]
        for idx, intent in enumerate(intents):
            intent["utterance_remainder"] = self._get_remainder(
                intent, utterance)
            intents[idx]["entities"] = intents[idx].pop("matches")
            intents[idx]["intent_type"] = intents[idx].pop("name")
            intent["intent_engine"] = "padatious"
            intent["utterance"] = intent.pop("sent")
            if isinstance(intents[idx]["utterance"], list):
                intents[idx]["utterance"] = " ".join(intents[idx]["utterance"])
        return intents

    def calc_intents(self, utterance, min_conf=None):
        min_conf = min_conf or self.config.get("padatious_min_conf", 0.65)
        utterance = utterance.strip().lower()
        bucket = {}
        for ut in self.segmenter.segment(utterance):
            intent = self.calc_intent(ut)
            if intent["conf"] < min_conf:
                bucket[ut] = None
            else:
                bucket[ut] = intent
        return bucket

    def calc_intents_list(self, utterance):
        utterance = utterance.strip().lower()
        bucket = {}
        for ut in self.segmenter.segment(utterance):
            bucket[ut] = self.filter_intents(ut)
        return bucket

    def manifest(self):
        # TODO vocab, skill ids, intent_data
        return {"intent_names": self.registered_intents}

    def _train(self, single_thread=True, timeout=120, force_training=True):
        with self.lock:
            return self.container.train(single_thread=single_thread,
                                        timeout=timeout,
                                        force=force_training,
                                        debug=True)
Ejemplo n.º 6
0
class CountriesSkill(CommonQuerySkill):
    def __init__(self):
        super(CountriesSkill, self).__init__()
        if "map_style" not in self.settings:
            self.settings["map_style"] = "ortho"
        self.countries_data = {}
        self.country_codes = {}
        self.regions = [
            u'Asia', u'Europe', u'Africa', u'Oceania', u'Americas', u'Polar'
        ]
        self.subregions = [
            u'Southern Asia', u'Northern Europe', u'Southern Europe',
            u'Northern Africa', u'Polynesia', u'Middle Africa', u'Caribbean',
            u'South America', u'Western Asia', u'Australia and New Zealand',
            u'Western Europe', u'Eastern Europe', u'Central America',
            u'Western Africa', u'Northern America', u'Southern Africa',
            u'Eastern Africa', u'South-Eastern Asia', u'Eastern Asia',
            u'Melanesia', u'Micronesia', u'Central Asia'
        ]
        self.get_country_data()

        intent_cache = expanduser(
            self.config_core['padatious']['intent_cache'])

        self.intents = IntentContainer(intent_cache)

    def initialize(self):
        if cartopy is None:
            self.log.info(
                "Map plots are disabled, additional requirements needed")
            self.log.info(
                "https://scitools.org.uk/cartopy/docs/latest/installing.html")
        self.load_intents()

    # CommonQuery Padatious subparser
    def load_intents(self):
        for intent in [
                "country_area", "country_borders", "country_capital",
                "country_currency", "country_in_region", "country_languages",
                "country_num", "country_population", "country_region",
                "country_timezones", "denonym", "where_language_spoken"
        ]:
            path = self.find_resource(intent + '.intent', "vocab")
            if path:
                self.intents.load_intent(intent, path)

        self.intents.train(single_thread=True)

    def intent2answer(self, intent, data):
        # Get response from intents
        response = None
        if intent == "country_area":
            response = self.handle_country_area(data)
        elif intent == "country_timezones":
            response = self.handle_country_timezones(data)
        elif intent == "where_language_spoken":
            response = self.handle_language_where(data)
        elif intent == "denonym":
            response = self.handle_country_denonym(data)
        elif intent == "country_region":
            response = self.handle_country_where(data)
        elif intent == "country_population":
            response = self.handle_country_population(data)
        elif intent == "country_borders":
            response = self.handle_country_borders(data)
        elif intent == "country_capital":
            response = self.handle_country_capital(data)
        elif intent == "country_currency":
            response = self.handle_country_currency(data)
        elif intent == "country_in_region":
            response = self.handle_country_in_region(data)
        elif intent == "country_languages":
            response = self.handle_country_languages(data)
        elif intent == "country_num":
            response = self.handle_country_number(data)
        return response

    def CQS_match_query_phrase(self, phrase):
        """Analyze phrase to see if it is a play-able phrase with this skill.

                Needs to be implemented by the skill.

                Arguments:
                    phrase (str): User phrase, "What is an aardwark"

                Returns:
                    (match, CQSMatchLevel[, callback_data]) or None: Tuple containing
                         a string with the appropriate matching phrase, the PlayMatch
                         type, and optionally data to return in the callback if the
                         match is selected.
        """
        response = None
        match = self.intents.calc_intent(phrase)
        level = CQSMatchLevel.CATEGORY
        data = match.matches
        intent = match.name
        score = match.conf
        data["intent"] = intent
        data["score"] = score

        if score > 0.8:
            level = CQSMatchLevel.EXACT
        elif score > 0.5:
            level = CQSMatchLevel.CATEGORY
        elif score > 0.3:
            level = CQSMatchLevel.GENERAL
        else:
            intent = None

        if intent:
            # Validate extracted entities
            country = data.get("country")
            region = data.get("region")
            language = data.get("language")

            if country:
                data["query"] = country
                # ensure we really have a country name
                response = self.dialog_renderer.render("bad_country", {})
                match, score = match_one(country.lower(),
                                         list(self.countries_data.keys()))
                self.log.debug("Country fuzzy match: {n}, Score: {s}".format(
                    n=match, s=score))
                if score > 0.5:
                    country = match
                    data.update(self.countries_data[country])
                else:
                    countries = self.search_country(country)
                    if not len(countries) > 0:
                        level = CQSMatchLevel.GENERAL
                    else:
                        country = countries[0]["name"]
                        data.update(countries[0])
                        # TODO disambiguation
                        if len(countries) > 1:
                            data["disambiguation"] = countries[1:]
                            self.log.debug("multiple matches found: " +
                                           str([c["name"] for c in countries]))
                data["country"] = country  # normalized from match

            if language:
                data["query"] = language
                # ensure we really have a language name
                words = language.split(" ")
                clean_up = ["is"]
                # remove words commonly caught by mistake in padatious
                language = " ".join(
                    [word for word in words if word not in clean_up])
                lang_code = langcodes.find_name(
                    'language', language, langcodes.standardize_tag(self.lang))
                lang_code = str(lang_code)
                self.log.debug("Detected lang code: " + lang_code)
                if not lang_code:
                    return None
                data["lang_code"] = lang_code
                # TODO
                countries = self.search_country_by_language(lang_code)
                data["country_list"] = countries

            if region:
                data["query"] = region
                # ensure we really have a region name
                response = self.dialog_renderer.render("bad_region")
                countries = None
                match, score = match_one(region, self.regions)
                data["region_score"] = score

                if score > 0.5:
                    region = match
                    countries = self.search_country_by_region(region)

                match, score2 = match_one(region, self.subregions)
                data["subregion_score"] = score2
                if score2 > score:
                    region = match
                    countries = self.search_country_by_subregion(region)

                if score > 0.8 and not country:
                    level = CQSMatchLevel.EXACT
                elif score > 0.5 and not country:
                    level = CQSMatchLevel.CATEGORY
                elif score > 0.3 and not country:
                    level = CQSMatchLevel.GENERAL

                data["region"] = region
                self.log.debug("Detected region: " + region)
                data["country_list"] = countries

            # Get response from intents
            response = self.intent2answer(intent, data) or response

            if response:
                return (phrase, level, response, data)
        return None

    def CQS_action(self, phrase, data):
        """Take additional action IF the skill is selected.

        The speech is handled by the common query but if the chosen skill
        wants to display media, set a context or prepare for sending
        information info over e-mail this can be implemented here.

        Args:
            phrase (str): User phrase uttered after "Play", e.g. "some music"
            data (dict): Callback data specified in match_query_phrase()
        """
        projection = cartopy.crs.PlateCarree()
        if data.get("country"):
            title = data["country"]
            if self.settings["map_style"] == "ortho":
                country = self.countries_data[data["country"].lower()]
                lat = country["lat"]
                lon = country["long"]
                projection = cartopy.crs.Orthographic(lon, lat)

            image = self.plot_country(data["country"], projection=projection)
            self.gui.show_image(image, fill='PreserveAspectFit', title=title)
        elif data.get("region"):
            title = data["region"]
            countries = data["country_list"]

            if self.settings["map_style"] == "ortho":
                country = self.countries_data[countries[0]["name"].lower()]
                lat = country["lat"]
                lon = country["long"]
                projection = cartopy.crs.Orthographic(lon, lat)

            image = self.plot_region(data["region"], projection=projection)
            self.gui.show_image(image, fill='PreserveAspectFit', title=title)

        elif data.get("country_list"):

            countries = data["country_list"]

            # TODO allow this somehow, will not show all countries
            #if self.settings["map_style"] == "ortho":
            #    country = self.countries_data[countries[0]["name"].lower()]
            #    lat = country["lat"]
            #    lon = country["long"]
            #    projection = cartopy.crs.Orthographic(lon, lat)

            title = data.get("region") \
                    or data.get("language") \
                    or data.get("lang_code") \
                    or " "
            countries = [c["name"] for c in countries]
            image = self.plot_countries(countries,
                                        projection=projection,
                                        name=title,
                                        region=data.get("region"))
            self.gui.show_image(image,
                                fill='PreserveAspectFit',
                                title=title,
                                caption=", ".join(countries))

    # gui
    @staticmethod
    def _get_country_geometry(query, region=None, min_score=0.7):
        best_score = 0
        best_match = None

        shapename = 'admin_0_countries'
        countries_shp = cartopy.io.shapereader.natural_earth(
            resolution='110m', category='cultural', name=shapename)

        for country in cartopy.io.shapereader.Reader(countries_shp).records():
            country_name = country.attributes['NAME'].lower()
            country_long_name = country.attributes['NAME_LONG'].lower()

            reg = country.attributes["REGION_WB"].lower()
            subregion = country.attributes["SUBREGION"].lower()
            continent = country.attributes["CONTINENT"].lower()

            match, score = match_one(query.lower(),
                                     [country_long_name, country_name])

            if region:
                _, score2 = match_one(region.lower(),
                                      [reg, subregion, continent])
                score = (score + score2) / 2

            if score > best_score:
                best_score = score
                best_match = country.geometry

        if best_score < min_score:
            best_match = None

        return best_match

    def _get_region_countries(self, query, min_score=0.7):
        countries = []
        region, score = match_one(query, self.regions)

        if score > min_score - 0.15:
            countries = self.search_country_by_region(region)
        if score < min_score:
            region, score = match_one(query, self.subregions)
            if score > min_score:
                countries = self.search_country_by_subregion(region)

        return [c["name"] for c in countries]

    @staticmethod
    def _get_region_geometries(query, min_score=0.8):
        shapename = 'admin_0_countries'
        countries_shp = cartopy.io.shapereader.natural_earth(
            resolution='110m', category='cultural', name=shapename)
        geoms = []
        for country in cartopy.io.shapereader.Reader(countries_shp).records():
            continent = country.attributes["CONTINENT"].lower()
            region = country.attributes["REGION_WB"].lower()
            subregion = country.attributes["SUBREGION"].lower()

            match, score = match_one(query.lower(),
                                     [region, subregion, continent])

            if score > min_score or \
                    (query.lower() in region.lower() and score >= 0.5):
                geoms.append(country.geometry)
        return geoms

    def plot_country(self, query, projection=None, rgb=None):
        if cartopy is None:
            return

        output = join(gettempdir(),
                      query + self.settings["map_style"] + ".png")
        if isfile(output):
            return output

        ax = plt.axes(projection=projection)
        ax.stock_img()
        ax.coastlines()

        r, g, b = rgb or (255, 0, 0)
        color = (r / 255, g / 255, b / 255)

        geometry = self._get_country_geometry(query)
        if geometry:
            geometries = [geometry]
            ax.add_geometries(geometries,
                              cartopy.crs.PlateCarree(),
                              facecolor=color)

            plt.savefig(output, bbox_inches='tight', facecolor="black")
            plt.close()
            return output
        return None

    def plot_countries(self,
                       countries,
                       projection=None,
                       rgb=None,
                       name=None,
                       region=None):
        if cartopy is None:
            return
        name = name or "_".join([c[:2] for c in countries])

        output = join(gettempdir(),
                      name + self.settings["map_style"] + "_countries.png")

        if isfile(output):
            return output

        projection = projection or cartopy.crs.PlateCarree()

        ax = plt.axes(projection=projection)
        ax.stock_img()
        ax.coastlines()

        r, g, b = rgb or (255, 0, 0)
        color = (r / 255, g / 255, b / 255)

        geometries = []
        for query in countries:
            geometry = self._get_country_geometry(query, region)
            if geometry:
                geometries.append(geometry)

        ax.add_geometries(geometries,
                          cartopy.crs.PlateCarree(),
                          facecolor=color)

        plt.savefig(output, bbox_inches='tight', facecolor="black")
        plt.close()
        return output

    def plot_region(self, query, projection=None, rgb=None):
        if cartopy is None:
            return

        output = join(gettempdir(),
                      query + self.settings["map_style"] + "_region.png")
        if isfile(output):
            return output

        ax = plt.axes(projection=projection)
        ax.stock_img()
        ax.coastlines()

        r, g, b = rgb or (255, 0, 0)
        color = (r / 255, g / 255, b / 255)

        geometries = self._get_region_geometries(query)
        if not geometries:
            countries = self._get_region_countries(query)
            return self.plot_countries(countries,
                                       projection, (r, g, b),
                                       name=query,
                                       region=query)
        ax.add_geometries(geometries,
                          cartopy.crs.PlateCarree(),
                          facecolor=color)

        plt.savefig(output, bbox_inches='tight', facecolor="black")
        plt.close()
        return output

    # intents
    def handle_country_where(self, data):
        name = data["country"]
        region = data["region"]
        sub = data["subregion"]
        if region in sub:
            r = sub
        else:
            r = sub + ", " + region
        return self.dialog_renderer.render("country_location", {
            "country": name,
            "region": r
        })

    def handle_country_currency(self, data):
        country = data["country"]
        coins = self.countries_data[country]["currencies"]
        coins = ", ".join([self.pretty_currency(c) for c in coins])
        return self.dialog_renderer.render("currency", {
            "country": country,
            "coin": coins
        })

    def handle_country_in_region(self, data):
        region = data["region"]
        countries = data["country_list"]

        if len(countries):
            return "; ".join([c["name"] for c in countries])

        else:
            return self.dialog_renderer.render("bad_region")

    def handle_language_where(self, data):
        lang_code = data["lang_code"]
        lang_name = data["language"]
        countries = data["country_list"]
        if len(countries):
            # TODO dialog files
            return ", ".join([c["name"] for c in countries])
        else:
            return self.dialog_renderer.render("bad_country")

    def handle_country_languages(self, data):
        country = data["country"]
        if country in self.countries_data.keys():
            langs = self.countries_data[country]["languages"]
            return ", ".join([lang for lang in langs])
        else:
            return self.dialog_renderer.render("bad_country")

    def handle_country_timezones(self, data):
        country = data["country"]
        if country in self.countries_data.keys():
            timezones = ", ".join(self.countries_data[country]["timezones"])
            return self.dialog_renderer.render("timezones", {
                "country": country,
                "timezones": timezones
            })
        else:
            return self.dialog_renderer.render("bad_country")

    def handle_country_area(self, data):
        country = data["country"]
        if country in self.countries_data.keys():
            area = self.countries_data[country]["area"]
            # TODO convert units
            area = pronounce_number(float(area), lang=self.lang)
            return self.dialog_renderer.render("area", {
                "country": country,
                "number": area
            })
        else:
            return self.dialog_renderer.render("bad_country")

    def handle_country_population(self, data):
        country = data["country"]
        if country in self.countries_data.keys():
            population = self.countries_data[country]["population"]
            area = pronounce_number(int(population), lang=self.lang)
            return self.dialog_renderer.render("population", {
                "country": country,
                "number": area
            })
        else:
            return self.dialog_renderer.render("bad_country")

    def handle_country_borders(self, data):
        country = data["country"]
        if country in self.countries_data.keys():
            borders = self.countries_data[country]["borders"]
            borders = ", ".join([self.country_codes[b] for b in borders])
            return self.dialog_renderer.render("borders", {
                "country": country,
                "borders": borders
            })
        else:
            return self.dialog_renderer.render("bad_country")

    def handle_country_capital(self, data):
        country = data["country"]
        if country in self.countries_data.keys():
            capital = self.countries_data[country]["capital"]
            return self.dialog_renderer.render("capital", {
                "country": country,
                "capital": capital
            })
        else:
            return self.dialog_renderer.render("bad_country")

    def handle_country_denonym(self, data):
        country = data["country"]
        if country in self.countries_data.keys():
            denonym = self.countries_data[country]["demonym"]
            return self.dialog_renderer.render("denonym", {
                "country": country,
                "denonym": denonym
            })

        else:
            return self.dialog_renderer.render("bad_country")

    def handle_country_number(self, data):
        number = pronounce_number(len(self.countries_data), lang=self.lang)
        return self.dialog_renderer.render("country_number",
                                           {"number": number})

    # country api
    @staticmethod
    def pretty_currency(currency_code):
        currency_code = currency_code.upper()
        if currency_code in CURRENCY.keys():
            return CURRENCY[currency_code].name
        return currency_code

    @staticmethod
    def get_all_countries():
        return CountryApi.get_all()

    def get_country_data(self):
        countries = self.get_all_countries()
        for c in countries:
            name = c["name"].lower()
            self.countries_data[name] = {}
            self.countries_data[name]["timezones"] = c["timezones"]
            self.countries_data[name]["demonym"] = c["demonym"]
            self.countries_data[name]["currencies"] = c["currencies"]
            self.countries_data[name]["alpha2Code"] = c["alpha2Code"]
            self.country_codes[c["alpha2Code"]] = name
            self.countries_data[name]["alpha3Code"] = c["alpha3Code"]
            self.country_codes[c["alpha3Code"]] = name
            self.countries_data[name]["area"] = str(c["area"])
            self.countries_data[name]["languages"] = [
                langcodes.LanguageData(language=l).language_name()
                for l in c["languages"]
            ]
            self.countries_data[name]["lang_codes"] = [
                langcodes.standardize_tag(l) for l in c["languages"]
            ]
            self.countries_data[name]["capital"] = c["capital"]
            self.countries_data[name]["borders"] = c["borders"]
            self.countries_data[name]["nativeName"] = c["nativeName"]
            self.countries_data[name]["population"] = str(c["population"])
            self.countries_data[name]["region"] = c["region"]
            self.countries_data[name]["subregion"] = c["subregion"]
            if len(c["latlng"]):
                self.countries_data[name]["lat"], \
                self.countries_data[name]["long"] = c["latlng"]

    @staticmethod
    def search_country(name):
        try:
            return CountryApi.get_countries_by_name(name)
        except:
            return []

    @staticmethod
    def search_country_by_code(code):
        try:
            return CountryApi.get_countries_by_country_codes([code])
        except:
            return []

    @staticmethod
    def search_country_by_language(lang_code):
        try:
            return CountryApi.get_countries_by_language(lang_code)
        except:
            return []

    @staticmethod
    def search_country_by_region(region):
        try:
            return CountryApi.get_countries_by_region(region)
        except:
            return []

    @staticmethod
    def search_country_by_subregion(subregion):
        try:
            return CountryApi.get_countries_by_subregion(subregion)
        except:
            return []