|
1 | | -import abc |
2 | | -import itertools |
3 | 1 | from os.path import exists |
4 | 2 | from threading import RLock |
5 | | -from typing import List, Tuple, Optional |
| 3 | +from typing import List, Optional |
6 | 4 | import warnings |
7 | 5 | from ovos_bus_client.message import Message, dig_for_message |
8 | 6 | from ovos_bus_client.util import get_mycroft_bus |
9 | 7 | from ovos_utils.log import LOG, log_deprecation |
10 | 8 |
|
11 | | - |
12 | | -class _IntentMeta(abc.ABCMeta): |
13 | | - def __instancecheck__(self, instance): |
14 | | - check = super().__instancecheck__(instance) |
15 | | - if not check: |
16 | | - try: |
17 | | - # backwards compat isinstancechecks |
18 | | - from adapt.intent import Intent as _I |
19 | | - check = isinstance(instance, _I) |
20 | | - except ImportError: |
21 | | - pass |
22 | | - return check |
23 | | - |
24 | | - |
25 | | -class Intent(metaclass=_IntentMeta): |
26 | | - def __init__(self, name="", requires=None, at_least_one=None, optional=None, excludes=None): |
27 | | - """Create Intent object |
28 | | -
|
29 | | - Args: |
30 | | - name(str): Name for Intent |
31 | | - requires(list): Entities that are required |
32 | | - at_least_one(list): One of these Entities are required |
33 | | - optional(list): Optional Entities used by the intent |
34 | | - """ |
35 | | - self.name = name |
36 | | - self.requires = requires or [] |
37 | | - self.at_least_one = at_least_one or [] |
38 | | - self.optional = optional or [] |
39 | | - self.excludes = excludes or [] |
40 | | - |
41 | | - def validate(self, tags, confidence): |
42 | | - """Using this method removes tags from the result of validate_with_tags |
43 | | -
|
44 | | - Returns: |
45 | | - intent(intent): Results from validate_with_tags |
46 | | - """ |
47 | | - intent, tags = self.validate_with_tags(tags, confidence) |
48 | | - return intent |
49 | | - |
50 | | - def validate_with_tags(self, tags, confidence): |
51 | | - """Validate whether tags has required entites for this intent to fire |
52 | | -
|
53 | | - Args: |
54 | | - tags(list): Tags and Entities used for validation |
55 | | - confidence(float): The weight associate to the parse result, |
56 | | - as indicated by the parser. This is influenced by a parser |
57 | | - that uses edit distance or context. |
58 | | -
|
59 | | - Returns: |
60 | | - intent, tags: Returns intent and tags used by the intent on |
61 | | - failure to meat required entities then returns intent with |
62 | | - confidence |
63 | | - of 0.0 and an empty list for tags. |
64 | | - """ |
65 | | - result = {'intent_type': self.name} |
66 | | - intent_confidence = 0.0 |
67 | | - local_tags = tags[:] |
68 | | - used_tags = [] |
69 | | - |
70 | | - # Check excludes first |
71 | | - for exclude_type in self.excludes: |
72 | | - exclude_tag, _canonical_form, _tag_confidence = \ |
73 | | - self._find_first_tag(local_tags, exclude_type) |
74 | | - if exclude_tag: |
75 | | - result['confidence'] = 0.0 |
76 | | - return result, [] |
77 | | - |
78 | | - for require_type, attribute_name in self.requires: |
79 | | - required_tag, canonical_form, tag_confidence = \ |
80 | | - self._find_first_tag(local_tags, require_type) |
81 | | - if not required_tag: |
82 | | - result['confidence'] = 0.0 |
83 | | - return result, [] |
84 | | - |
85 | | - result[attribute_name] = canonical_form |
86 | | - if required_tag in local_tags: |
87 | | - local_tags.remove(required_tag) |
88 | | - used_tags.append(required_tag) |
89 | | - intent_confidence += tag_confidence |
90 | | - |
91 | | - if len(self.at_least_one) > 0: |
92 | | - best_resolution = self._resolve_one_of(local_tags, self.at_least_one) |
93 | | - if not best_resolution: |
94 | | - result['confidence'] = 0.0 |
95 | | - return result, [] |
96 | | - else: |
97 | | - for key in best_resolution: |
98 | | - # TODO: at least one should support aliases |
99 | | - result[key] = best_resolution[key][0].get('key') |
100 | | - intent_confidence += 1.0 * best_resolution[key][0]['entities'][0].get('confidence', 1.0) |
101 | | - used_tags.append(best_resolution[key][0]) |
102 | | - if best_resolution in local_tags: |
103 | | - local_tags.remove(best_resolution[key][0]) |
104 | | - |
105 | | - for optional_type, attribute_name in self.optional: |
106 | | - optional_tag, canonical_form, tag_confidence = \ |
107 | | - self._find_first_tag(local_tags, optional_type) |
108 | | - if not optional_tag or attribute_name in result: |
109 | | - continue |
110 | | - result[attribute_name] = canonical_form |
111 | | - if optional_tag in local_tags: |
112 | | - local_tags.remove(optional_tag) |
113 | | - used_tags.append(optional_tag) |
114 | | - intent_confidence += tag_confidence |
115 | | - |
116 | | - total_confidence = (intent_confidence / len(tags) * confidence) \ |
117 | | - if tags else 0.0 |
118 | | - |
119 | | - CLIENT_ENTITY_NAME = 'Client' # TODO - ??? what is this magic string |
120 | | - |
121 | | - target_client, canonical_form, confidence = \ |
122 | | - self._find_first_tag(local_tags, CLIENT_ENTITY_NAME) |
123 | | - |
124 | | - result['target'] = target_client.get('key') if target_client else None |
125 | | - result['confidence'] = total_confidence |
126 | | - |
127 | | - return result, used_tags |
128 | | - |
129 | | - @classmethod |
130 | | - def _resolve_one_of(cls, tags, at_least_one): |
131 | | - """Search through all combinations of at_least_one rules to find a |
132 | | - combination that is covered by tags |
133 | | -
|
134 | | - Args: |
135 | | - tags(list): List of tags with Entities to search for Entities |
136 | | - at_least_one(list): List of Entities to find in tags |
137 | | -
|
138 | | - Returns: |
139 | | - object: |
140 | | - returns None if no match is found but returns any match as an object |
141 | | - """ |
142 | | - for possible_resolution in itertools.product(*at_least_one): |
143 | | - resolution = {} |
144 | | - pr = possible_resolution[:] |
145 | | - for entity_type in pr: |
146 | | - last_end_index = -1 |
147 | | - if entity_type in resolution: |
148 | | - last_end_index = resolution[entity_type][-1].get('end_token') |
149 | | - tag, value, c = cls._find_first_tag(tags, entity_type, |
150 | | - after_index=last_end_index) |
151 | | - if not tag: |
152 | | - break |
153 | | - else: |
154 | | - if entity_type not in resolution: |
155 | | - resolution[entity_type] = [] |
156 | | - resolution[entity_type].append(tag) |
157 | | - # Check if this is a valid resolution (all one_of rules matched) |
158 | | - if len(resolution) == len(possible_resolution): |
159 | | - return resolution |
160 | | - |
161 | | - return None |
162 | | - |
163 | | - @staticmethod |
164 | | - def _find_first_tag(tags, entity_type, after_index=-1): |
165 | | - """Searches tags for entity type after given index |
166 | | -
|
167 | | - Args: |
168 | | - tags(list): a list of tags with entity types to be compared to |
169 | | - entity_type |
170 | | - entity_type(str): This is he entity type to be looking for in tags |
171 | | - after_index(int): the start token must be greater than this. |
172 | | -
|
173 | | - Returns: |
174 | | - ( tag, v, confidence ): |
175 | | - tag(str): is the tag that matched |
176 | | - v(str): ? the word that matched? |
177 | | - confidence(float): is a measure of accuracy. 1 is full confidence |
178 | | - and 0 is none. |
179 | | - """ |
180 | | - for tag in tags: |
181 | | - for entity in tag.get('entities'): |
182 | | - for v, t in entity.get('data'): |
183 | | - if t.lower() == entity_type.lower() and \ |
184 | | - (tag.get('start_token', 0) > after_index or \ |
185 | | - tag.get('from_context', False)): |
186 | | - return tag, v, entity.get('confidence') |
187 | | - |
188 | | - return None, None, None |
189 | | - |
190 | | - |
191 | | -class _IntentBuilderMeta(abc.ABCMeta): |
192 | | - def __instancecheck__(self, instance): |
193 | | - check = super().__instancecheck__(instance) |
194 | | - if not check: |
195 | | - try: |
196 | | - # backwards compat isinstancechecks |
197 | | - from adapt.intent import IntentBuilder as _IB |
198 | | - check = isinstance(instance, _IB) |
199 | | - except ImportError: |
200 | | - pass |
201 | | - return check |
202 | | - |
203 | | - |
204 | | -class IntentBuilder(metaclass=_IntentBuilderMeta): |
205 | | - """ |
206 | | - IntentBuilder, used to construct intent parsers. |
207 | | -
|
208 | | - Attributes: |
209 | | - at_least_one(list): A list of Entities where one is required. |
210 | | - These are separated into lists so you can have one of (A or B) and |
211 | | - then require one of (D or F). |
212 | | - requires(list): A list of Required Entities |
213 | | - optional(list): A list of optional Entities |
214 | | - excludes(list): A list of forbidden Entities |
215 | | - name(str): Name of intent |
216 | | -
|
217 | | - Notes: |
218 | | - This is designed to allow construction of intents in one line. |
219 | | -
|
220 | | - Example: |
221 | | - IntentBuilder("Intent")\ |
222 | | - .requires("A")\ |
223 | | - .one_of("C","D")\ |
224 | | - .optional("G").build() |
225 | | - """ |
226 | | - |
227 | | - def __init__(self, intent_name): |
228 | | - """ |
229 | | - Constructor |
230 | | -
|
231 | | - Args: |
232 | | - intent_name(str): the name of the intents that this parser |
233 | | - parses/validates |
234 | | - """ |
235 | | - self.at_least_one = [] |
236 | | - self.requires = [] |
237 | | - self.excludes = [] |
238 | | - self.optional = [] |
239 | | - self.name = intent_name |
240 | | - |
241 | | - def one_of(self, *args): |
242 | | - """ |
243 | | - The intent parser should require one of the provided entity types to |
244 | | - validate this clause. |
245 | | -
|
246 | | - Args: |
247 | | - args(args): *args notation list of entity names |
248 | | -
|
249 | | - Returns: |
250 | | - self: to continue modifications. |
251 | | - """ |
252 | | - self.at_least_one.append(args) |
253 | | - return self |
254 | | - |
255 | | - def require(self, entity_type, attribute_name=None): |
256 | | - """ |
257 | | - The intent parser should require an entity of the provided type. |
258 | | -
|
259 | | - Args: |
260 | | - entity_type(str): an entity type |
261 | | - attribute_name(str): the name of the attribute on the parsed intent. |
262 | | - Defaults to match entity_type. |
263 | | -
|
264 | | - Returns: |
265 | | - self: to continue modifications. |
266 | | - """ |
267 | | - if not attribute_name: |
268 | | - attribute_name = entity_type |
269 | | - self.requires += [(entity_type, attribute_name)] |
270 | | - return self |
271 | | - |
272 | | - def exclude(self, entity_type): |
273 | | - """ |
274 | | - The intent parser must not contain an entity of the provided type. |
275 | | -
|
276 | | - Args: |
277 | | - entity_type(str): an entity type |
278 | | -
|
279 | | - Returns: |
280 | | - self: to continue modifications. |
281 | | - """ |
282 | | - self.excludes.append(entity_type) |
283 | | - return self |
284 | | - |
285 | | - def optionally(self, entity_type, attribute_name=None): |
286 | | - """ |
287 | | - Parsed intents from this parser can optionally include an entity of the |
288 | | - provided type. |
289 | | -
|
290 | | - Args: |
291 | | - entity_type(str): an entity type |
292 | | - attribute_name(str): the name of the attribute on the parsed intent. |
293 | | - Defaults to match entity_type. |
294 | | -
|
295 | | - Returns: |
296 | | - self: to continue modifications. |
297 | | - """ |
298 | | - if not attribute_name: |
299 | | - attribute_name = entity_type |
300 | | - self.optional += [(entity_type, attribute_name)] |
301 | | - return self |
302 | | - |
303 | | - def build(self): |
304 | | - """ |
305 | | - Constructs an intent from the builder's specifications. |
306 | | -
|
307 | | - :return: an Intent instance. |
308 | | - """ |
309 | | - return Intent(self.name, self.requires, |
310 | | - self.at_least_one, self.optional, |
311 | | - self.excludes) |
| 9 | +# OVOS-INTENT-4 keyword-intent *definition* primitives. The canonical, adapt-free |
| 10 | +# implementations live in ovos-spec-tools; they are re-exported here so skills keep |
| 11 | +# their long-standing `from ovos_workshop.intents import IntentBuilder` import while |
| 12 | +# the single source of truth is the spec. The `IntentServiceInterface` producer |
| 13 | +# (and the munge_* helpers) below consume these definitions. |
| 14 | +from ovos_spec_tools import Intent, IntentBuilder, open_intent_envelope |
312 | 15 |
|
313 | 16 |
|
314 | 17 | def to_alnum(skill_id: str) -> str: |
@@ -673,15 +376,3 @@ def __contains__(self, val): |
673 | 376 | Checks if an intent name has been registered. |
674 | 377 | """ |
675 | 378 | return val in [i[0] for i in self.registered_intents] |
676 | | - |
677 | | - |
678 | | -def open_intent_envelope(message): |
679 | | - """ |
680 | | - Convert dictionary received over messagebus to Intent. |
681 | | - """ |
682 | | - intent_dict = message.data |
683 | | - return Intent(intent_dict.get('name'), |
684 | | - intent_dict.get('requires'), |
685 | | - intent_dict.get('at_least_one'), |
686 | | - intent_dict.get('optional'), |
687 | | - intent_dict.get('excludes')) |
0 commit comments