File size: 5,261 Bytes
0827183
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# 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
            ),
        )