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
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, ))
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
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
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])
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
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
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)
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
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
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(), }
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",
def QGeoSubject(node, params, result): n = capitalize_placename(_preprocess(result._text)) nom = NounPhrase(n).nominative or n result.subject = nom
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
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
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:
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