# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. from typing import List, Union from recognizers_number import NumberModel, NumberRecognizer, OrdinalModel from recognizers_text import Culture from .choice import Choice from .find import Find from .find_choices_options import FindChoicesOptions from .found_choice import FoundChoice from .model_result import ModelResult class ChoiceRecognizers: """Contains methods for matching user input against a list of choices.""" @staticmethod def recognize_choices( utterance: str, choices: List[Union[str, Choice]], options: FindChoicesOptions = None, ) -> List[ModelResult]: """ Matches user input against a list of choices. This is layered above the `Find.find_choices()` function, and adds logic to let the user specify their choice by index (they can say "one" to pick `choice[0]`) or ordinal position (they can say "the second one" to pick `choice[1]`.) The user's utterance is recognized in the following order: - By name using `find_choices()` - By 1's based ordinal position. - By 1's based index position. Parameters: ----------- utterance: The input. choices: The list of choices. options: (Optional) Options to control the recognition strategy. Returns: -------- A list of found choices, sorted by most relevant first. """ if utterance is None: utterance = "" # Normalize list of choices choices_list = [ Choice(value=choice) if isinstance(choice, str) else choice for choice in choices ] # Try finding choices by text search first # - We only want to use a single strategy for returning results to avoid issues where utterances # like the "the third one" or "the red one" or "the first division book" would miss-recognize as # a numerical index or ordinal as well. locale = options.locale if (options and options.locale) else Culture.English matched = Find.find_choices(utterance, choices_list, options) if not matched: matches = [] if not options or options.recognize_ordinals: # Next try finding by ordinal matches = ChoiceRecognizers._recognize_ordinal(utterance, locale) for match in matches: ChoiceRecognizers._match_choice_by_index( choices_list, matched, match ) if not matches and (not options or options.recognize_numbers): # Then try by numerical index matches = ChoiceRecognizers._recognize_number(utterance, locale) for match in matches: ChoiceRecognizers._match_choice_by_index( choices_list, matched, match ) # Sort any found matches by their position within the utterance. # - The results from find_choices() are already properly sorted so we just need this # for ordinal & numerical lookups. matched = sorted(matched, key=lambda model_result: model_result.start) return matched @staticmethod def _recognize_ordinal(utterance: str, culture: str) -> List[ModelResult]: model: OrdinalModel = NumberRecognizer(culture).get_ordinal_model(culture) return list( map(ChoiceRecognizers._found_choice_constructor, model.parse(utterance)) ) @staticmethod def _match_choice_by_index( choices: List[Choice], matched: List[ModelResult], match: ModelResult ): try: index: int = int(match.resolution.value) - 1 if 0 <= index < len(choices): choice = choices[index] matched.append( ModelResult( start=match.start, end=match.end, type_name="choice", text=match.text, resolution=FoundChoice( value=choice.value, index=index, score=1.0 ), ) ) except: # noop here, as in dotnet/node repos pass @staticmethod def _recognize_number(utterance: str, culture: str) -> List[ModelResult]: model: NumberModel = NumberRecognizer(culture).get_number_model(culture) return list( map(ChoiceRecognizers._found_choice_constructor, model.parse(utterance)) ) @staticmethod def _found_choice_constructor(value_model: ModelResult) -> ModelResult: return ModelResult( start=value_model.start, end=value_model.end, type_name="choice", text=value_model.text, resolution=FoundChoice( value=value_model.resolution["value"], index=0, score=1.0 ), )