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)
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
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")
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")
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)
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 ") )
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
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")
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")
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
def QWhatIsEntity(node: Node, params: ParamList, result: Result) -> None: result.qtype = "WhatIs" result.qkey = result._nominative
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
def QArMinusOperator(node, params, result: Result): result.operator = "minus"
def QArPercentOperator(node, params, result: Result): result.operator = "percent"
def QArithmetic(node, params, result: Result): # Set query type result.qtype = _ARITHMETIC_QTYPE
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
def QArSquareRootOperator(node, params, result: Result): result.operator = "sqrt"
def QArPercent(node, params, result: Result): result.desc = result._canonical
def QArDivisionOperator(node, params, result: Result): result.operator = "divide"
def QArSqrt(node, params, result: Result): result.desc = result._canonical
def QArMultiplicationOperator(node, params, result: Result): """ 'Hvað er 17 sinnum 34?' """ result.operator = "multiply"
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)
def EfLiður(node: Node, params: ParamList, result: Result) -> None: """Eignarfallsliðir haldast óbreyttir, þ.e. þeim á ekki að breyta í nefnifall""" result._nominative = result._text
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
def QArPiQuery(node, params, result: Result): result.qtype = "PI"
def QArFractionOperator(node, params, result: Result): result.operator = "fraction"
def QArWithoutVAT(node, params, result: Result): result.operator = "without_vat"
def QArVAT(node, params, result: Result): result.desc = result._canonical result.qtype = "VSK"
def QArFraction(node, params, result: Result): result.desc = result._canonical
def QArPowOperator(node, params, result: Result): result.operator = "pow"