Пример #1
0
def _answer_phonenum4name_query(q: Query, result: Result) -> AnswerTuple:
    """ Answer query of the form "hvað er síminn hjá [íslenskt mannsnafn]?" """
    res = query_ja_api(result.qkey)

    nþgf = NounPhrase(result.qkey).dative or result.qkey

    # Verify that we have a sane response with at least 1 result
    if not res or not res.get("people") or not res["people"].get("items"):
        return gen_answer("Ekki tókst að fletta upp {0}.".format(nþgf))

    # Check if we have a single canonical match from API
    allp = res["people"]["items"]
    single = len(allp) == 1
    first = allp[0]
    fname = first["name"]
    if not single:
        # Many found with that name, generate smart message asking for disambiguation
        name_components = result.qkey.split()
        one_name_only = len(name_components) == 1
        with GreynirBin.get_db() as bdb:
            fn = name_components[0].title()
            gender = bdb.lookup_name_gender(fn)
        msg = (
            "Það fundust {0} með það nafn. Prófaðu að tilgreina {1}heimilisfang"
            .format(
                "margar" if gender == "kvk" else "margir",
                "fullt nafn og " if one_name_only else "",
            ))
        # Try to generate example, e.g. "Jón Jónssón á Smáragötu"
        for i in allp:
            try:
                street_nf = i["address_nominative"].split()[0]
                street_þgf = i["address"].split()[0]
                msg = msg + " t.d. {0} {1} {2}".format(
                    fname, iceprep_for_street(street_nf), street_þgf)
                break
            except (KeyError, ValueError) as e:
                logging.warning("Exception: " + str(e))
                continue
        return gen_answer(msg)

    # Scan API call result, try to find the best phone nuber to provide
    phone_number = _best_number(first)
    if not phone_number:
        return gen_answer("Ég finn ekki símanúmerið hjá {0}".format(nþgf))

    # Sanitize number and generate answer
    phone_number = phone_number.replace("-", "").replace(" ", "")
    answ = phone_number
    fn = NounPhrase(fname).dative or fname
    voice = "Síminn hjá {0} er {1}".format(fn, " ".join(list(phone_number)))

    q.set_context(dict(phone_number=phone_number, name=fname))
    q.set_source(_JA_SOURCE)

    return dict(answer=answ), answ, voice
Пример #2
0
 def check_pp_with_place(self, match: SimpleTree) -> None:
     """ Check whether the correct preposition is being used with a place name """
     place = match.NP.lemma
     correct_preposition = IcelandicPlaces.lookup_preposition(place)
     if correct_preposition is None:
         # This is not a known or likely place name
         return
     preposition = match.P.lemma
     if correct_preposition == preposition:
         # Correct: return
         return
     start, end = match.span
     suggest = match.tidy_text.replace(preposition, correct_preposition, 1)
     text = "Rétt er að rita '{0}'".format(suggest)
     detail = (
         "Ýmist eru notaðar forsetningarnar 'í' eða 'á' með nöfnum "
         "staða, bæja og borga. Í tilviki {0:ef} er notuð forsetningin '{1}'."
         .format(NounPhrase(place), correct_preposition))
     self._ann.append(
         Annotation(
             start=start,
             end=end,
             code="P_WRONG_PLACE_PP",
             text=text,
             detail=detail,
             suggest=suggest,
         ))
Пример #3
0
def _capital_query(country: str, q: Query):
    """ Generate answer to question concerning a country capital. """

    # Get country code
    cc = isocode_for_country_name(country)
    if not cc:
        logging.warning("No CC for country {0}".format(country))
        return False

    # Find capital city, given the country code
    capital = capital_for_cc(cc)
    if not capital:
        return False

    # Use the Icelandic name for the city
    ice_cname = icelandic_city_name(capital["name_ascii"])

    # Look up genitive country name for voice description
    country_gen = NounPhrase(country).genitive or country
    answer = ice_cname
    response = dict(answer=answer)
    voice = "Höfuðborg {0} er {1}".format(country_gen, answer)

    q.set_answer(response, answer, voice)
    q.set_key("Höfuðborg {0}".format(country_gen))
    q.set_context(dict(subject=ice_cname))

    return True
Пример #4
0
def nom2dat(w: str) -> str:
    """ Look up the dative of an Icelandic noun given its nominative form. """
    try:
        d = NounPhrase(w).dative
        return d or w
    except Exception:
        pass
    return w
Пример #5
0
def decline_np(phrase, idf_tag):
    kasus_map = {
        "n": "nominative",
        "o": "accusative",
        "þ": "dative",
        "e": "genitive"
    }
    np = NounPhrase(phrase)
    return getattr(np, kasus_map[idf_tag])
Пример #6
0
def _addr2nom(address: str) -> str:
    """ Convert location name to nominative form. """
    if address is None or address == "":
        return address
    try:
        nom = NounPhrase(cap_first(address)).nominative or address
    except Exception:
        nom = address
    return nom
Пример #7
0
def handle_plain_text(q: Query) -> bool:
    """ Handle a plain text query requesting a call to a telephone number. """
    ql = q.query_lower.strip().rstrip("?")

    pfx = None
    number = None

    for rx in _PHONECALL_REGEXES:
        m = re.search(rx, ql)
        if m:
            pfx = m.group(1)
            telsubj = m.group(2).strip()
            break
    else:
        return False

    # Special handling if context
    if telsubj in _CONTEXT_SUBJ:
        ctx = q.fetch_context()
        if ctx is None or "phone_number" not in ctx:
            a = gen_answer("Ég veit ekki við hvern þú átt")
        else:
            q.set_url("tel:{0}".format(ctx["phone_number"]))
            answer = "Skal gert"
            a = (dict(answer=answer), answer, "")
    # Only number digits
    else:
        clean_num = re.sub(r"[^0-9]", "", telsubj).strip()
        if len(clean_num) < 3:
            # The number is clearly not a valid phone number
            a = gen_answer("{0} er ekki gilt símanúmer.".format(telsubj))
        elif re.search(r"^[\d|\s]+$", clean_num):
            # At this point we have what looks like a legitimate phone number.
            # Send tel: url to trigger phone call in client
            q.set_url("tel:{0}".format(clean_num))
            answer = "Skal gert"
            a = (dict(answer=answer), answer, "")
            q.set_beautified_query("{0}{1}".format(pfx, clean_num))
        else:
            # This is a named subject
            subj_þgf = NounPhrase(telsubj.title()).dative or telsubj
            a = gen_answer("Ég veit ekki símanúmerið hjá {0}".format(subj_þgf))

    q.set_answer(*a)
    q.set_qtype(_TELEPHONE_QTYPE)
    q.query_is_command()

    return True
Пример #8
0
def main() -> None:
    pc = dict(POSTCODES)
    pc_keys = pc.keys()
    pp = pprint.PrettyPrinter(indent=4)

    req = requests.get(POSTCODES_REMOTE_URL, allow_redirects=True)
    f = StringIO(req.text)

    changed = False
    reader = csv.DictReader(f, delimiter=";")
    for r in reader:
        # CSV file from postur.is only contains postcode placenames in
        # the dative form (þgf.). Try to lemmatise to nominative (nf.) using Reynir.
        postcode = int(r["Póstnúmer"])
        if postcode not in pc_keys:
            logging.warning(
                "Postcode '{0}' did not already exist in data.".format(
                    postcode))
            changed = True

        tp = r["Tegund"]
        p_dat = _clean_name(r["Staður"])
        p_nom = NounPhrase(p_dat).nominative
        if not p_nom:
            logging.warning("Unable to decline placename '{0}'".format(p_dat))
            p_nom = p_dat

        if pc[postcode]["stadur_nf"] != p_nom:
            pc[postcode]["stadur_nf"] = p_nom
            print("{0} --> {1}".format(pc[postcode]["stadur_nf"], p_nom))
            changed = True

        if pc[postcode]["stadur_tgf"] != p_dat:
            pc[postcode]["stadur_tgf"] = p_dat
            print("{0} --> {1}".format(pc[postcode]["stadur_tgf"], p_dat))
            changed = True

        if pc[postcode]["tegund"] != tp:
            pc[postcode]["tegund"] = tp
            print("{0} --> {1}".format(pc[postcode]["tegund"], tp))
            changed = True

    if not changed:
        print("No change since last update")
    else:
        pp.pprint(pc)
Пример #9
0
def answ_address(placename: str, loc: LatLonTuple, qtype: str) -> AnswerTuple:
    """ Generate answer to a question concerning the address of a place. """
    # Look up placename in places API
    res = query_places_api(placename,
                           userloc=loc,
                           fields="formatted_address,name,geometry")

    if (not res or res["status"] != "OK" or "candidates" not in res
            or not res["candidates"]):
        return gen_answer(_PLACES_API_ERRMSG)

    # Use top result in Iceland
    place = _top_candidate(res["candidates"])
    if not place:
        return gen_answer(_NOT_IN_ICELAND_ERRMSG)

    # Remove superfluous "Ísland" in addr string
    addr = re.sub(r", Ísland$", "", place["formatted_address"])
    # Get street name without number to get preposition
    street_name = addr.split()[0].rstrip(",")
    maybe_postcode = re.search(r"^\d\d\d", street_name) is not None
    prep = "í" if maybe_postcode else iceprep_for_street(street_name)
    # Split addr into street name w. number, and remainder
    street_addr = addr.split(",")[0]
    remaining = re.sub(r"^{0}".format(street_addr), "", addr)
    # Get street name in dative case
    addr_þgf = NounPhrase(street_addr).dative or street_addr
    # Assemble final address
    final_addr = "{0}{1}".format(addr_þgf, remaining)

    # Create answer
    answer = final_addr
    voice = "{0} er {1} {2}".format(placename, prep,
                                    numbers_to_neutral(final_addr))
    response = dict(answer=answer)

    return response, answer, voice
Пример #10
0
def answ_openhours(placename: str, loc: LatLonTuple,
                   qtype: str) -> AnswerTuple:
    """ Generate answer to a question concerning the opening hours of a place. """
    # Look up placename in places API
    res = query_places_api(
        placename,
        userloc=loc,
        fields="opening_hours,place_id,formatted_address,geometry",
    )
    if (res is None or res["status"] != "OK" or "candidates" not in res
            or not res["candidates"]):
        return gen_answer(_PLACES_API_ERRMSG)

    # Use top result
    place = _top_candidate(res["candidates"])
    if place is None:
        return gen_answer(_NOT_IN_ICELAND_ERRMSG)

    if "opening_hours" not in place:
        return gen_answer("Ekki tókst að sækja opnunartíma fyrir " +
                          icequote(placename))

    place_id = place["place_id"]
    is_open = place["opening_hours"]["open_now"]
    # needs_disambig = len(res["candidates"]) > 1
    fmt_addr = place["formatted_address"]

    # Look up place ID in Place Details API to get more information
    res = query_place_details(place_id, fields="opening_hours,name")
    if not res or res.get("status") != "OK" or "result" not in res:
        return gen_answer(_PLACES_API_ERRMSG)

    now = datetime.utcnow()
    wday = now.weekday()
    answer = voice = ""

    try:
        name = res["result"]["name"]
        name_gender = NounPhrase(name).gender or "hk"

        # Generate placename w. street, e.g. "Forréttabarinn á Nýlendugötu"
        street = fmt_addr.split()[0].rstrip(",")
        street_þgf = NounPhrase(street).dative or street

        name = "{0} {1} {2}".format(name, iceprep_for_street(street),
                                    street_þgf)

        # Get correct "open" adjective for place name
        open_adj_map = {"kk": "opinn", "kvk": "opin", "hk": "opið"}
        open_adj = open_adj_map.get(name_gender) or "opið"

        # Get opening hours for current weekday
        # TODO: Handle when place is closed (no entry in periods)
        periods = res["result"]["opening_hours"]["periods"]
        if len(periods) == 1 or wday >= len(periods):
            # Open 24 hours a day
            today_desc = p_desc = "{0} er {1} allan sólarhringinn".format(
                name, open_adj)
        else:
            # Get period
            p = periods[wday]
            opens = p["open"]["time"]
            closes = p["close"]["time"]

            # Format correctly, e.g. "12:00 - 19:00"
            openstr = opens[:2] + ":" + opens[2:]
            closestr = closes[:2] + ":" + opens[2:]
            p_desc = "{0} - {1}".format(openstr, closestr)
            p_voice = p_desc.replace("-", "til")

            today_desc = "Í dag er {0} {1} frá {2}".format(
                name, open_adj, p_voice)
    except Exception as e:
        logging.warning(
            "Exception generating answer for opening hours: {0}".format(e))
        return gen_answer(_PLACES_API_ERRMSG)

    # Generate answer
    if qtype == "OpeningHours":
        answer = p_desc
        voice = today_desc
    # Is X open? Is X closed?
    elif qtype == "IsOpen" or qtype == "IsClosed":
        yes_no = ("Já" if ((is_open and qtype == "IsOpen") or
                           (not is_open and qtype == "IsClosed")) else "Nei")
        answer = "{0}. {1}.".format(yes_no, today_desc)
        voice = answer

    response = dict(answer=answer)

    return response, answer, voice
Пример #11
0
def _format_flight_answer(flights: FlightList) -> Dict[str, str]:
    """
    Takes in a list of flights and returns a dict
    containing a formatted answer and text for a voice line.

    Each flight should contain the attributes:
        'No':           Flight number
        'DisplayName':  Name of airport/city
        'api_airport':  Name of Icelandic airport/city
        'flight_time':  Time of departure/arrival
        'Departure':    True if departing from api_airport, else False
        'Status':       Info on flight status (e.g. whether it's cancelled)
    """
    airport: str
    api_airport: str
    flight_dt: Optional[datetime]
    answers: List[str] = []
    voice_lines: List[str] = []

    for flight in flights:
        airport = icelandic_city_name(capitalize_placename(flight.get("DisplayName", "")))
        api_airport = icelandic_city_name(capitalize_placename(flight.get("api_airport", "")))

        flight_dt = flight.get("flight_time")
        if flight_dt is None or airport == "" or api_airport == "":
            continue
        flight_date_str = flight_dt.strftime("%-d. %B")
        flight_time_str = flight_dt.strftime("%H:%M")

        if flight.get("Departure"):
            airport = NounPhrase(airport).genitive or airport
            api_airport = NounPhrase(api_airport).dative or api_airport

            # Catch cancelled flights
            if (
                isinstance(flight.get("Status"), str)
                and "aflýst" in str(flight["Status"]).lower()
            ):
                line = f"Flugi {flight.get('No')} frá {api_airport} til {airport} er aflýst."
            else:
                line = (
                    f"Flug {flight.get('No')} til {airport} "
                    f"flýgur frá {api_airport} {flight_date_str} "
                    f"klukkan {flight_time_str} að staðartíma."
                )
        else:
            airport = NounPhrase(airport).dative or airport
            prep = iceprep_for_placename(api_airport)
            api_airport = NounPhrase(api_airport).dative or api_airport

            if (
                isinstance(flight.get("Status"), str)
                and "aflýst" in str(flight["Status"]).lower()
            ):
                line = f"Flugi {flight.get('No')} frá {airport} til {api_airport} er aflýst."
            else:
                line = (
                    f"Flug {flight.get('No')} frá {airport} "
                    f"lendir {prep} {api_airport} {flight_date_str} "
                    f"klukkan {flight_time_str} að staðartíma."
                )

        voice_line = re.sub(r" \d+\. ", " " + _DAY_INDEX_ACC[flight_dt.day] + " ", line)

        answers.append(line)
        voice_lines.append(voice_line)

    return {
        "answer": "<br/>".join(answers).strip(),
        "voice": _BREAK_SSML.join(voice_lines).strip(),
    }
Пример #12
0
        q.query_is_command()
        return True

    return False


def _addr2str(addr: Dict[str, str], case: str = "nf") -> str:
    """ Format address canonically given dict w. address info. """
    assert case in ["nf", "þgf"]
    prep = iceprep_for_placename(addr["placename"])
    astr = "{0} {1} {2} {3}".format(
        addr["street"], addr["number"], prep, addr["placename"]
    )
    if case == "þgf":
        try:
            n = NounPhrase(astr)
            if n:
                astr = n.dative or astr
        except Exception:
            pass
    return numbers_to_neutral(astr)


_WHATS_MY_ADDR = frozenset(
    (
        "hvar á ég heima",
        "hvar á ég eiginlega heima",
        "veistu hvar ég á heima",
        "veist þú hvar ég á heima",
        "hvar bý ég",
        "hvar bý ég eiginlega",
Пример #13
0
def QGeoSubject(node, params, result):
    n = capitalize_placename(_preprocess(result._text))
    nom = NounPhrase(n).nominative or n
    result.subject = nom
Пример #14
0
            f"Invalid case: '{case}'. Valid cases are: {', '.join(CASES.keys())}"
        )

    if force_number and force_number not in SING_OR_PLUR:
        return _err(
            f"Invalid force_number parameter: '{force_number}'. Valid numbers are: {', '.join(SING_OR_PLUR)}"
        )

    resp: Dict[str, Union[str, bool, Dict[str, str]]] = dict(q=q)

    kwargs: Dict[str, Any] = dict()
    if force_number:
        kwargs["force_number"] = force_number

    try:
        n = NounPhrase(q, **kwargs)

        cases: Dict[str, str] = dict()
        if case:
            cases[case] = getattr(n, CASES[case])
        else:
            # Default to returning all cases
            c: Optional[str] = n.nominative
            if c is not None:
                cases["nf"] = c
            c = n.accusative
            if c is not None:
                cases["þf"] = c
            c = n.dative
            if c is not None:
                cases["þgf"] = c
Пример #15
0
def _process_result(result: Result) -> Dict[str, str]:
    """
    Return formatted description of arrival/departure
    time of flights to or from an Icelandic airport,
    based on info in result dict.
    """
    airport: str  # Icelandic or foreign airport/country
    api_airport: str  # Always an Icelandic airport, as the ISAVIA API only covers them

    departing: bool = result["departure"]
    if departing:
        # Departures (from Keflavík by default)
        api_airport = result.get("from_loc", "keflavík").lower()
        # Wildcard matches any flight (if airport wasn't specified)
        airport = result.get("to_loc", "*").lower()
    else:
        # Arrivals (to Keflavík by default)
        api_airport = result.get("to_loc", "keflavík").lower()
        airport = result.get("from_loc", "*").lower()

    from_date: datetime
    to_date: datetime
    days: int = result.get("day_count", 5)  # Check 5 days into future by default
    from_date = result.get("from_date", datetime.now(timezone.utc))
    to_date = result.get("to_date", datetime.now(timezone.utc) + timedelta(days=days))

    # Normalize airport/city names
    airport = _LOCATION_ABBREV_MAP.get(airport, airport)
    airport = NounPhrase(airport).nominative or airport

    api_airport = _LOCATION_ABBREV_MAP.get(api_airport, api_airport)
    api_airport = NounPhrase(api_airport).nominative or api_airport

    # Translate Icelandic airport to its IATA code
    iata_code: str = _AIRPORT_TO_IATA_MAP.get(api_airport, api_airport)

    # TODO: Currently module only fetches one flight,
    # modifications to the grammar could allow fetching of more flights at once
    flight_count: int = result.get("flight_count", 1)

    flight_data: FlightList
    # Check first if function result in cache, else fetch data from API
    if departing in _FLIGHT_CACHE:
        flight_data = _FLIGHT_CACHE[departing]
    else:
        flight_data = _fetch_flight_data(from_date, to_date, iata_code, departing)

    flight_data = _filter_flight_data(flight_data, airport, api_airport, flight_count)

    answ: Dict[str, str] = dict()
    if len(flight_data) > 0:
        # (Format month names in Icelandic)
        with changedlocale(category="LC_TIME"):
            answ = _format_flight_answer(flight_data)
    else:
        to_airp: str
        from_airp: str
        if departing:
            to_airp, from_airp = airport, api_airport
        else:
            from_airp, to_airp = airport, api_airport

        to_airp = icelandic_city_name(capitalize_placename(to_airp))
        from_airp = icelandic_city_name(capitalize_placename(from_airp))

        from_airp = NounPhrase(from_airp).dative or from_airp
        to_airp = NounPhrase(to_airp).genitive or to_airp

        if from_airp == "*":
            answ["answer"] = f"Ekkert flug fannst til {to_airp} næstu {days} daga."
        elif to_airp == "*":
            answ["answer"] = f"Ekkert flug fannst frá {from_airp} næstu {days} daga."
        else:
            answ["answer"] = (
                f"Ekkert flug fannst "
                f"frá {from_airp} "
                f"til {to_airp} "
                f"næstu {days} daga."
            )
        answ["voice"] = answ["answer"]

    return answ
Пример #16
0
    changed = False
    reader = csv.DictReader(f, delimiter=";")
    for r in reader:
        # CSV file from postur.is only contains postcode placenames in
        # the dative form (þgf.). Try to lemmatise to nominative (nf.) using Reynir.
        postcode = int(r["Póstnúmer"])
        if postcode not in pc_keys:
            logging.warning(
                "Postcode '{0}' did not already exist in data.".format(
                    postcode))
            changed = True

        tp = r["Tegund"]
        p_dat = _clean_name(r["Staður"])
        p_nom = NounPhrase(p_dat).nominative
        if not p_nom:
            logging.warning("Unable to decline placename '{0}'".format(p_dat))
            p_nom = p_dat

        if pc[postcode]["stadur_nf"] != p_nom:
            pc[postcode]["stadur_nf"] = p_nom
            print("{0} --> {1}".format(pc[postcode]["stadur_nf"], p_nom))
            changed = True

        if pc[postcode]["stadur_tgf"] != p_dat:
            pc[postcode]["stadur_tgf"] = p_dat
            print("{0} --> {1}".format(pc[postcode]["stadur_tgf"], p_dat))
            changed = True

        if pc[postcode]["tegund"] != tp:
Пример #17
0
def handle_plain_text(q: Query) -> bool:
    """Handle a plain text query, contained in the q parameter
    which is an instance of the query.Query class.
    Returns True if the query was handled, and in that case
    the appropriate properties on the Query instance have
    been set, such as the answer and the query type (qtype).
    If the query is not recognized, returns False."""
    ql = q.query_lower.rstrip("?")

    # Timezone being asked about
    tz = None
    # Whether user asked for the time in a particular location
    specific_desc = None

    if ql in _TIME_QUERIES:
        # Use location to determine time zone
        tz = timezone4loc(q.location, fallback="IS")
    else:
        locq = [x for x in _TIME_IN_LOC_QUERIES if ql.startswith(x.lower())]
        if not locq:
            return False  # Not matching any time queries
        # This is a query about the time in a particular location, i.e. country or city
        # Cut away question prefix, leaving only loc name
        loc = ql[len(locq[0]):].strip()
        if not loc:
            return False  # No location string
        # Intelligently capitalize country/city/location name
        loc = capitalize_placename(loc)

        # Look up nominative
        loc_nom = NounPhrase(loc).nominative or loc
        prep = "í"

        # Check if loc is a recognised country or city name
        cc = isocode_for_country_name(loc_nom)
        if cc and cc in country_timezones:
            # Look up country timezone
            # Use the first timezone although some countries have more than one
            # The timezone list returned by pytz is ordered by "dominance"
            tz = country_timezones[cc][0]
            prep = iceprep_for_cc(cc)
        else:
            # It's not a country name, look up in city database
            info = lookup_city_info(loc_nom)
            if info:
                top = info[0]
                location = (
                    cast(float, top.get("lat_wgs84")),
                    cast(float, top.get("long_wgs84")),
                )
                tz = timezone4loc(location)
                prep = iceprep_for_placename(loc_nom)

        if tz:
            # "Klukkan í Lundúnum er" - Used for voice answer
            dat = NounPhrase(loc_nom).dative or loc
            specific_desc = "Klukkan {0} {1} er".format(prep, dat)
        else:
            # Unable to find the specified location
            q.set_qtype(_TIME_QTYPE)
            q.set_key(loc)
            q.set_answer(
                *gen_answer("Ég gat ekki flett upp staðsetningunni {0}".format(
                    icequote(loc))))
            return True

    # We have a timezone. Return formatted answer.
    if tz:
        now = datetime.now(timezone(tz))

        desc = specific_desc or "Klukkan er"

        # Create displayable answer
        answer = "{0:02}:{1:02}".format(now.hour, now.minute)
        # A detailed response object is usually a list or a dict
        response = dict(answer=answer)
        # A voice answer is a plain string that will be
        # passed as-is to a voice synthesizer
        voice = "{0} {1}:{2:02}.".format(desc, now.hour, now.minute)

        q.set_qtype(_TIME_QTYPE)
        q.set_key(tz)  # Query key is the timezone
        q.set_answer(response, answer, voice)
        return True

    return False