Exemple #1
0
def sentence(state: QueryStateDict, result: Result) -> None:
    """ Called when sentence processing is complete """
    q = state["query"]
    if "qtype" in result:
        # Successfully matched a query type
        q.set_qtype(result.qtype)

        try:
            r: Optional[AnswerTuple]
            if result.qtype == "PI":
                r = pi_answer(q, result)
            else:
                r = calc_arithmetic(q, result)
            if r is not None:
                q.set_answer(*r)
                q.set_key(result.get("qkey"))
                if "result" in r[0]:
                    # Pass the result into a query context having
                    # the 'result' property
                    res: float = cast(Any, r[0])["result"]
                    ctx = cast(ContextDict, dict(result=res))
                    q.set_context(ctx)
            else:
                raise Exception("Failed to answer arithmetic query")
        except Exception as e:
            logging.warning("Exception in arithmetic module: {0}".format(e))
            q.set_error("E_EXCEPTION: {0}".format(e))
    else:
        q.set_error("E_QUERY_NOT_UNDERSTOOD")
Exemple #2
0
def sentence(state: QueryStateDict, result: Result) -> None:
    """ Called when sentence processing is complete """
    q: Query = state["query"]
    if "qtype" in result and result.qtype == "Unit":
        # Successfully matched a query type
        # If no number is mentioned in the query
        # ('Hvað eru margir metrar í kílómetra?')
        # we use 1.0
        val_from = result.get("number", 1.0)
        # Convert between units, getting also the base SI unit and quantity
        valid, val, si_unit, si_quantity = _convert(
            val_from, result.unit_from, result.unit_to
        )
        if not valid:
            answer = voice_answer = "Það er ekki hægt að umbreyta {0} í {1}.".format(
                to_dative(result.unit_from), to_accusative(result.unit_to)
            )
            response = dict(answer=answer)
        else:
            answer = iceformat_float(val)
            if (0.0 < val < 1.0e-3) or (val > 0.0 and answer == "0"):
                answer = "næstum núll " + result.unit_to_nf
                val = 0.0
            else:
                answer += " " + result.unit_to_nf
            verb = "eru"
            if int(val_from) % 10 == 1 and int(val_from) % 100 != 11:
                # 'Einn lítri er...', 'Tuttugu og einn lítri er...',
                # but on the other hand 'Ellefu lítrar eru...'
                verb = "er"
            elif "hálf" in result.desc:
                # Hack to reply 'Hálfur kílómetri er 500 metrar'
                verb = "er"
            unit_to = result.unit_to
            response = dict(answer=answer)
            voice_answer = "{0} {1} {2}.".format(result.desc, verb, answer).capitalize()
            # Store the resulting quantity in the query context
            q.set_context(
                {
                    "quantity": {
                        "unit": unit_to,
                        "value": val,
                        "si_unit": si_unit,
                        "si_value": si_quantity,
                    }
                }
            )
        q.set_key(result.unit_to)
        q.set_answer(response, answer, voice_answer)
        q.set_qtype(_UNIT_QTYPE)
        q.lowercase_beautified_query()
        return

    q.set_error("E_QUERY_NOT_UNDERSTOOD")
Exemple #3
0
def sentence(state: QueryStateDict, result: Result) -> None:
    """Called when sentence processing is complete."""
    q: Query = state["query"]
    if (
        "qtype" in result
        and result["qtype"] == _FLIGHTS_QTYPE
        and isinstance(result.get("departure"), bool)
    ):
        try:
            answ: Dict[str, str] = _process_result(result)
            q.set_qtype(_FLIGHTS_QTYPE)
            q.set_source("Isavia")
            q.set_answer(answ, answ["answer"], answ["voice"])
            return
        except Exception as e:
            logging.warning(
                "Exception generating answer from flight data: {0}".format(e)
            )
            q.set_error("E_EXCEPTION: {0}".format(e))
    else:
        q.set_error("E_QUERY_NOT_UNDERSTOOD")
Exemple #4
0
def query_arrival_time(query: Query, session: Session, result: Result):
    """ Answers a query for the arrival time of a bus """

    # Examples:
    # 'Hvenær kemur strætó númer 12?'
    # 'Hvenær kemur leið sautján á Hlemm?'
    # 'Hvenær kemur næsti strætó í Einarsnes?'

    # Retrieve the client location, if available, and the name
    # of the bus stop, if given
    stop_name: Optional[str] = result.get("stop_name")
    stop: Optional[straeto.BusStop] = None
    location: Optional[Tuple[float, float]] = None

    if stop_name in {"þar", "þangað"}:
        # Referring to a bus stop mentioned earlier
        ctx = query.fetch_context()
        if ctx and "bus_stop" in ctx:
            stop_name = cast(str, ctx["bus_stop"])
        else:
            answer = voice_answer = "Ég veit ekki við hvaða stað þú átt."
            response = dict(answer=answer)
            return response, answer, voice_answer

    if not stop_name:
        location = query.location
        if location is None:
            answer = "Staðsetning óþekkt"
            response = dict(answer=answer)
            voice_answer = "Ég veit ekki hvar þú ert."
            return response, answer, voice_answer

    # Obtain today's bus schedule
    global SCHEDULE_TODAY
    with SCHEDULE_LOCK:
        if SCHEDULE_TODAY is None or not SCHEDULE_TODAY.is_valid_today:
            # We don't have today's schedule: create it
            SCHEDULE_TODAY = straeto.BusSchedule()

    # Obtain the set of stops that the user may be referring to
    stops: List[straeto.BusStop] = []
    if stop_name:
        stops = straeto.BusStop.named(stop_name, fuzzy=True)
        if query.location is not None:
            # If we know the location of the client, sort the
            # list of potential stops by proximity to the client
            straeto.BusStop.sort_by_proximity(stops, query.location)
    else:
        # Obtain the closest stops (at least within 400 meters radius)
        assert location is not None
        stops = cast(
            List[straeto.BusStop],
            straeto.BusStop.closest_to_list(location, n=2, within_radius=0.4),
        )
        if not stops:
            # This will fetch the single closest stop, regardless of distance
            stops = [
                cast(straeto.BusStop, straeto.BusStop.closest_to(location))
            ]

    # Handle the case where no bus number was specified (i.e. is 'Any')
    if result.bus_number == "Any" and stops:
        stop = stops[0]
        routes = sorted(
            (straeto.BusRoute.lookup(rid).number
             for rid in stop.visits.keys()),
            key=lambda r: int(r),
        )
        if len(routes) != 1:
            # More than one route possible: ask user to clarify
            route_seq = natlang_seq(list(map(str, routes)))
            answer = (" ".join(
                ["Leiðir", route_seq, "stoppa á",
                 to_dative(stop.name)]) + ". Spurðu um eina þeirra.")
            voice_answer = (" ".join([
                "Leiðir",
                numbers_to_neutral(route_seq),
                "stoppa á",
                to_dative(stop.name),
            ]) + ". Spurðu um eina þeirra.")
            response = dict(answer=answer)
            return response, answer, voice_answer
        # Only one route: use it as the query subject
        bus_number = routes[0]
        bus_name = "strætó númer {0}".format(bus_number)
    else:
        bus_number = result.bus_number if "bus_number" in result else 0
        bus_name = result.bus_name if "bus_name" in result else "Óþekkt"

    # Prepare results
    bus_name = cap_first(bus_name)
    va = [bus_name]
    a = []
    arrivals = []
    arrivals_dict = {}
    arrives = False
    route_number = str(bus_number)

    # First, check the closest stop
    # !!! TODO: Prepare a different area_priority parameter depending
    # !!! on the user's location; i.e. if she is in Eastern Iceland,
    # !!! route '1' would mean 'AL.1' instead of 'ST.1'.
    if stops:
        for stop in stops:
            arrivals_dict, arrives = SCHEDULE_TODAY.arrivals(
                route_number, stop)
            if arrives:
                break
        arrivals = list(arrivals_dict.items())
        a = ["Á", to_accusative(stop.name), "í átt að"]

    if arrivals:
        # Get a predicted arrival time for each direction from the
        # real-time bus location server
        prediction = SCHEDULE_TODAY.predicted_arrival(route_number, stop)
        now = datetime.utcnow()
        hms_now = (now.hour, now.minute + (now.second // 30), 0)
        first = True

        # We may get three (or more) arrivals if there are more than two
        # endpoints for the bus route in the schedule. To minimize
        # confusion, we only include the two endpoints that have the
        # earliest arrival times and skip any additional ones.
        arrivals = sorted(arrivals, key=lambda t: t[1][0])[:2]

        for direction, times in arrivals:
            if not first:
                va.append(", og")
                a.append(". Í átt að")
            va.extend(["í átt að", to_dative(direction)])
            a.append(to_dative(direction))
            deviation = []
            if prediction and direction in prediction:
                # We have a predicted arrival time
                hms_sched = times[0]
                hms_pred = prediction[direction][0]
                # Calculate the difference between the prediction and
                # now, and skip it if it is 1 minute or less
                diff = hms_diff(hms_pred, hms_now)
                if abs(diff) <= 1:
                    deviation = [", en er að fara núna"]
                else:
                    # Calculate the difference in minutes between the
                    # schedule and the prediction, with a positive number
                    # indicating a delay
                    diff = hms_diff(hms_pred, hms_sched)
                    if diff < -1:
                        # More than one minute ahead of schedule
                        if diff < -5:
                            # More than 5 minutes ahead
                            deviation = [
                                ", en kemur sennilega fyrr, eða",
                                hms_fmt(hms_pred),
                            ]
                        else:
                            # Two to five minutes ahead
                            deviation = [
                                ", en er",
                                str(-diff),
                                "mínútum á undan áætlun",
                            ]
                    elif diff >= 3:
                        # 3 minutes or more behind schedule
                        deviation = [
                            ", en kemur sennilega ekki fyrr en",
                            hms_fmt(hms_pred),
                        ]
            if first:
                assert stop is not None
                if deviation:
                    va.extend(["á að koma á", to_accusative(stop.name)])
                else:
                    va.extend(["kemur á", to_accusative(stop.name)])
            va.append("klukkan")
            a.append("klukkan")
            if len(times) == 1 or (len(times) > 1
                                   and hms_diff(times[0], hms_now) >= 10):
                # Either we have only one arrival time, or the next arrival is
                # at least 10 minutes away: only pronounce one time
                hms = times[0]
                time_text = hms_fmt(hms)
            else:
                # Return two or more times
                time_text = " og ".join(hms_fmt(hms) for hms in times)
            va.append(time_text)
            a.append(time_text)
            va.extend(deviation)
            a.extend(deviation)
            first = False

    elif arrives:
        # The given bus has already completed its scheduled halts at this stop today
        assert stops
        stop = stops[0]
        reply = ["kemur ekki aftur á", to_accusative(stop.name), "í dag"]
        va.extend(reply)
        a = [bus_name] + reply

    elif stops:
        # The given bus doesn't stop at all at either of the two closest stops
        stop = stops[0]
        va.extend(["stoppar ekki á", to_dative(stop.name)])
        a = [bus_name, "stoppar ekki á", to_dative(stop.name)]

    else:
        # The bus stop name is not recognized
        va = a = [stop_name.capitalize(), "er ekki biðstöð"]

    if stop is not None:
        # Store a location coordinate and a bus stop name in the context
        query.set_context({"location": stop.location, "bus_stop": stop.name})

    # Hack: Since we know that the query string contains no uppercase words,
    # adjust it accordingly; otherwise it may erroneously contain capitalized
    # words such as Vagn and Leið.
    bq = query.beautified_query
    for t in (
        ("Vagn ", "vagn "),
        ("Vagni ", "vagni "),
        ("Vagns ", "vagns "),
        ("Leið ", "leið "),
        ("Leiðar ", "leiðar "),
    ):
        bq = bq.replace(*t)
    query.set_beautified_query(bq)

    def assemble(x):
        """ Intelligently join answer string components. """
        return (" ".join(x) + ".").replace(" .", ".").replace(" ,", ",")

    voice_answer = assemble(va)
    answer = assemble(a)
    response = dict(answer=answer)
    return response, answer, voice_answer
Exemple #5
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