Пример #1
0
def QArNumberWord(node, params, result: Result):
    result._canonical = result._text
    if "context_reference" in result or "error_context_reference" in result:
        # Already pushed the context reference
        # ('það', 'því'): we're done
        return
    d = result.find_descendant(t_base="tala")
    if d:
        add_num(terminal_num(d), result)
    else:
        add_num(result._nominative, result)
Пример #2
0
def QArLastResult(node, params, result: Result):
    """Reference to previous result, usually via the words
    'það' or 'því' ('Hvað er það sinnum sautján?')"""
    q = result.state.get("query")
    ctx = None if q is None else q.fetch_context()
    if ctx is None or "result" not in ctx:
        # There is a reference to a previous result
        # which is not available: flag an error
        result.error_context_reference = True
    else:
        add_num(ctx["result"], result)
        result.context_reference = True
Пример #3
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")
Пример #4
0
def Prósenta(node, params, result: Result):
    # Find percentage terminal
    d = result.find_descendant(t_base="prósenta")
    if d:
        add_num(terminal_num(d), result)
    else:
        # We shouldn't be here. Something went horriby wrong somewhere.
        raise ValueError("No auxiliary information in percentage token")
Пример #5
0
def add_num(num, result: Result):
    """ Add a number to accumulated number args """
    if "numbers" not in result:
        result.numbers = []
    if isinstance(num, str):
        result.numbers.append(parse_num(num))
    else:
        result.numbers.append(num)
Пример #6
0
def QArStd(node, params, result: Result):
    # Used later for formatting voice answer string,
    # e.g. "[tveir plús tveir] er [fjórir]"
    result.desc = (
        result._canonical.replace("+", " plús ")
        .replace("-", " mínus ")
        .replace("/", " deilt með ")
        .replace(" x ", " sinnum ")
    )
Пример #7
0
def query_which_route(query: Query, session: Session, result: Result):
    """ Which routes stop at a given bus stop """
    stop_name = cast(str, result.stop_name)  # 'Einarsnes', 'Fiskislóð'...

    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"])
            result.qkey = stop_name
        else:
            answer = voice_answer = "Ég veit ekki við hvaða stað þú átt."
            response = dict(answer=answer)
            return response, answer, voice_answer

    bus_noun = result.bus_noun  # 'strætó', 'vagn', 'leið'...
    stops = straeto.BusStop.named(stop_name, fuzzy=True)
    if not stops:
        a = [stop_name, "þekkist ekki."]
        va = ["Ég", "þekki", "ekki", "biðstöðina", stop_name.capitalize()]
    else:
        routes = set()
        if query.location:
            straeto.BusStop.sort_by_proximity(stops, query.location)
        stop = stops[0]
        for route_id in stop.visits.keys():
            number = straeto.BusRoute.lookup(route_id).number
            routes.add(number)
        va = [bus_noun, "númer"]
        a = va[:]
        nroutes = len(routes)
        cnt = 0
        for rn in sorted(routes, key=lambda t: int(t)):
            if cnt:
                sep = "og" if cnt + 1 == nroutes else ","
                va.append(sep)
                a.append(sep)
            # We convert inflectable numbers to their text equivalents
            # since the speech engine can't be relied upon to get the
            # inflection of numbers right
            va.append(numbers_to_neutral(rn))
            a.append(rn)
            cnt += 1
        tail = ["stoppar á", to_dative(stop.name)]
        va.extend(tail)
        a.extend(tail)
        # Store a location coordinate and a bus stop name in the context
        query.set_context({"location": stop.location, "bus_stop": stop.name})

    voice_answer = correct_spaces(" ".join(va) + ".")
    answer = correct_spaces(" ".join(a))
    answer = cap_first(answer)
    response = dict(answer=answer)
    return response, answer, voice_answer
Пример #8
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")
Пример #9
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")
Пример #10
0
def FsMeðFallstjórn(node: Node, params: ParamList, result: Result) -> None:
    """Forsetningarliðir haldast óbreyttir,
    þ.e. þeim á ekki að breyta í nefnifall"""
    result._nominative = result._text
Пример #11
0
def QWhatIsEntity(node: Node, params: ParamList, result: Result) -> None:
    result.qtype = "WhatIs"
    result.qkey = result._nominative
Пример #12
0
def calc_arithmetic(query: Query, result: Result) -> Optional[AnswerTuple]:
    """ Calculate the answer to an arithmetic query """
    operator = result.operator
    nums = result.numbers
    desc = result.desc

    if "error_context_reference" in result:
        # Used 'það' or 'því' without context
        return gen_answer("Ég veit ekki til hvers þú vísar.")

    # Ensure that we have the right number of
    # number args for the operation in question
    assert _OP_NUM_ARGS[operator] == len(nums)

    # Global namespace for eval
    # Block access to all builtins
    eval_globals: Dict[str, Any] = {"__builtins__": None}

    # Square root calculation
    if operator == "sqrt":
        if len(str(nums[0])) > 100:
            return gen_answer("Þessi tala er of há.")
        # Allow sqrt function in eval namespace
        eval_globals["sqrt"] = math.sqrt
        s = "sqrt({0})".format(nums[0])

    # Pow
    elif operator == "pow":
        # Cap max pow
        if nums[1] > 50:
            return gen_answer("Þetta er of hátt veldi.")
        # Allow pow function in eval namespace
        eval_globals["pow"] = pow
        s = "pow({0},{1})".format(nums[0], nums[1])

    # Percent
    elif operator == "percent":
        s = "({0} * {1}) / 100.0".format(nums[0], nums[1])

    # Fraction
    elif operator == "fraction":
        s = "{0} * {1}".format(nums[0], nums[1])

    # Add VAT to sum
    elif operator == "with_vat":
        s = "{0} * {1}".format(nums[0], _VAT_MULT)

    # Subtract VAT from sum
    elif operator == "without_vat":
        s = "{0} / {1}".format(nums[0], _VAT_MULT)

    # Addition, subtraction, multiplication, division
    elif operator in _STD_OPERATORS:
        math_op = _STD_OPERATORS[operator]

        # Check for division by zero
        if math_op == "/" and nums[1] == 0:
            return gen_answer("Það er ekki hægt að deila með núlli.")

        s = "{0} {1} {2}".format(nums[0], math_op, nums[1])
    else:
        logging.warning("Unknown operator: {0}".format(operator))
        return None

    # Set arithmetic expression as query key
    result.qkey = s

    # Run eval on expression
    res: float = eval(s, eval_globals, {})

    if isinstance(res, float):
        # Convert result to Icelandic decimal format
        answer = iceformat_float(res)
    else:
        answer = str(res)

    response = dict(answer=answer, result=res)
    voice_answer = "{0} er {1}".format(desc, answer)

    return response, answer, voice_answer
Пример #13
0
def QArMinusOperator(node, params, result: Result):
    result.operator = "minus"
Пример #14
0
def QArPercentOperator(node, params, result: Result):
    result.operator = "percent"
Пример #15
0
def QArithmetic(node, params, result: Result):
    # Set query type
    result.qtype = _ARITHMETIC_QTYPE
Пример #16
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
Пример #17
0
def QArSquareRootOperator(node, params, result: Result):
    result.operator = "sqrt"
Пример #18
0
def QArPercent(node, params, result: Result):
    result.desc = result._canonical
Пример #19
0
def QArDivisionOperator(node, params, result: Result):
    result.operator = "divide"
Пример #20
0
def QArSqrt(node, params, result: Result):
    result.desc = result._canonical
Пример #21
0
def QArMultiplicationOperator(node, params, result: Result):
    """ 'Hvað er 17 sinnum 34?' """
    result.operator = "multiply"
Пример #22
0
def QArCurrencyOrNum(node, params, result: Result):
    amount = node.first_child(lambda n: n.has_t_base("amount"))
    if amount is not None:
        # Found an amount terminal node
        result.amount, curr = amount.contained_amount
        add_num(result.amount, result)
Пример #23
0
def EfLiður(node: Node, params: ParamList, result: Result) -> None:
    """Eignarfallsliðir haldast óbreyttir,
    þ.e. þeim á ekki að breyta í nefnifall"""
    result._nominative = result._text
Пример #24
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
Пример #25
0
def QArPiQuery(node, params, result: Result):
    result.qtype = "PI"
Пример #26
0
def QArFractionOperator(node, params, result: Result):
    result.operator = "fraction"
Пример #27
0
def QArWithoutVAT(node, params, result: Result):
    result.operator = "without_vat"
Пример #28
0
def QArVAT(node, params, result: Result):
    result.desc = result._canonical
    result.qtype = "VSK"
Пример #29
0
def QArFraction(node, params, result: Result):
    result.desc = result._canonical
Пример #30
0
def QArPowOperator(node, params, result: Result):
    result.operator = "pow"