Skip to content

Commit 1add1d8

Browse files
authored
refactor:upstream classes (#6)
* feat:semver * feat:semver * fix:isinstance checks compat with upstream adapt some code in the wild still imports from adapt directly * refactor:import from workshop use the OVOS workshop Intent and IntentBuilder classes , as these will be shared with other keyword based engines * workshop * fix test * fix test * fix test * fix test * fix test * fix:add_missing_Requirement
1 parent c3c66d9 commit 1add1d8

12 files changed

+11
-248
lines changed

.github/workflows/unit_tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ jobs:
5555
pip install -r test/requirements.txt
5656
- name: Run unittests
5757
run: |
58-
pytest --cov=ovos_adapt --cov-report=xml test/unittests
58+
pytest --cov=ovos_adapt --cov-report=xml ./test
5959
# NOTE: additional pytest invocations should also add the --cov-append flag
6060
# or they will overwrite previous invocations' coverage reports
6161
# (for an example, see OVOS Skill Manager's workflow)

ovos_adapt/intent.py

Lines changed: 3 additions & 244 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
__author__ = 'seanfitz'
1717

1818
import itertools
19-
20-
CLIENT_ENTITY_NAME = 'Client'
19+
from ovos_workshop.intents import Intent, IntentBuilder
2120

2221

2322
def is_entity(tag, entity_name):
@@ -44,15 +43,7 @@ def find_first_tag(tags, entity_type, after_index=-1):
4443
confidence(float): is a measure of accuracy. 1 is full confidence
4544
and 0 is none.
4645
"""
47-
for tag in tags:
48-
for entity in tag.get('entities'):
49-
for v, t in entity.get('data'):
50-
if t.lower() == entity_type.lower() and \
51-
(tag.get('start_token', 0) > after_index or \
52-
tag.get('from_context', False)):
53-
return tag, v, entity.get('confidence')
54-
55-
return None, None, None
46+
return Intent._find_first_tag(tags, entity_type, after_index)
5647

5748

5849
def find_next_tag(tags, end_index=0):
@@ -93,237 +84,5 @@ def resolve_one_of(tags, at_least_one):
9384
object:
9485
returns None if no match is found but returns any match as an object
9586
"""
87+
return Intent._resolve_one_of(tags, at_least_one)
9688

97-
for possible_resolution in choose_1_from_each(at_least_one):
98-
resolution = {}
99-
pr = possible_resolution[:]
100-
for entity_type in pr:
101-
last_end_index = -1
102-
if entity_type in resolution:
103-
last_end_index = resolution[entity_type][-1].get('end_token')
104-
tag, value, c = find_first_tag(tags, entity_type,
105-
after_index=last_end_index)
106-
if not tag:
107-
break
108-
else:
109-
if entity_type not in resolution:
110-
resolution[entity_type] = []
111-
resolution[entity_type].append(tag)
112-
# Check if this is a valid resolution (all one_of rules matched)
113-
if len(resolution) == len(possible_resolution):
114-
return resolution
115-
116-
return None
117-
118-
119-
class Intent(object):
120-
def __init__(self, name, requires, at_least_one, optional, excludes=None):
121-
"""Create Intent object
122-
123-
Args:
124-
name(str): Name for Intent
125-
requires(list): Entities that are required
126-
at_least_one(list): One of these Entities are required
127-
optional(list): Optional Entities used by the intent
128-
"""
129-
self.name = name
130-
self.requires = requires
131-
self.at_least_one = at_least_one
132-
self.optional = optional
133-
self.excludes = excludes or []
134-
135-
def validate(self, tags, confidence):
136-
"""Using this method removes tags from the result of validate_with_tags
137-
138-
Returns:
139-
intent(intent): Results from validate_with_tags
140-
"""
141-
intent, tags = self.validate_with_tags(tags, confidence)
142-
return intent
143-
144-
def validate_with_tags(self, tags, confidence):
145-
"""Validate whether tags has required entites for this intent to fire
146-
147-
Args:
148-
tags(list): Tags and Entities used for validation
149-
confidence(float): The weight associate to the parse result,
150-
as indicated by the parser. This is influenced by a parser
151-
that uses edit distance or context.
152-
153-
Returns:
154-
intent, tags: Returns intent and tags used by the intent on
155-
failure to meat required entities then returns intent with
156-
confidence
157-
of 0.0 and an empty list for tags.
158-
"""
159-
result = {'intent_type': self.name}
160-
intent_confidence = 0.0
161-
local_tags = tags[:]
162-
used_tags = []
163-
164-
# Check excludes first
165-
for exclude_type in self.excludes:
166-
exclude_tag, _canonical_form, _tag_confidence = \
167-
find_first_tag(local_tags, exclude_type)
168-
if exclude_tag:
169-
result['confidence'] = 0.0
170-
return result, []
171-
172-
for require_type, attribute_name in self.requires:
173-
required_tag, canonical_form, tag_confidence = \
174-
find_first_tag(local_tags, require_type)
175-
if not required_tag:
176-
result['confidence'] = 0.0
177-
return result, []
178-
179-
result[attribute_name] = canonical_form
180-
if required_tag in local_tags:
181-
local_tags.remove(required_tag)
182-
used_tags.append(required_tag)
183-
intent_confidence += tag_confidence
184-
185-
if len(self.at_least_one) > 0:
186-
best_resolution = resolve_one_of(local_tags, self.at_least_one)
187-
if not best_resolution:
188-
result['confidence'] = 0.0
189-
return result, []
190-
else:
191-
for key in best_resolution:
192-
# TODO: at least one should support aliases
193-
result[key] = best_resolution[key][0].get('key')
194-
intent_confidence += \
195-
1.0 * best_resolution[key][0]['entities'][0]\
196-
.get('confidence', 1.0)
197-
used_tags.append(best_resolution[key][0])
198-
if best_resolution in local_tags:
199-
local_tags.remove(best_resolution[key][0])
200-
201-
for optional_type, attribute_name in self.optional:
202-
optional_tag, canonical_form, tag_confidence = \
203-
find_first_tag(local_tags, optional_type)
204-
if not optional_tag or attribute_name in result:
205-
continue
206-
result[attribute_name] = canonical_form
207-
if optional_tag in local_tags:
208-
local_tags.remove(optional_tag)
209-
used_tags.append(optional_tag)
210-
intent_confidence += tag_confidence
211-
212-
total_confidence = (intent_confidence / len(tags) * confidence) \
213-
if tags else 0.0
214-
215-
target_client, canonical_form, confidence = \
216-
find_first_tag(local_tags, CLIENT_ENTITY_NAME)
217-
218-
result['target'] = target_client.get('key') if target_client else None
219-
result['confidence'] = total_confidence
220-
221-
return result, used_tags
222-
223-
224-
class IntentBuilder(object):
225-
"""
226-
IntentBuilder, used to construct intent parsers.
227-
228-
Attributes:
229-
at_least_one(list): A list of Entities where one is required.
230-
These are separated into lists so you can have one of (A or B) and
231-
then require one of (D or F).
232-
requires(list): A list of Required Entities
233-
optional(list): A list of optional Entities
234-
name(str): Name of intent
235-
236-
Notes:
237-
This is designed to allow construction of intents in one line.
238-
239-
Example:
240-
IntentBuilder("Intent")\
241-
.requires("A")\
242-
.one_of("C","D")\
243-
.optional("G").build()
244-
"""
245-
def __init__(self, intent_name):
246-
"""
247-
Constructor
248-
249-
Args:
250-
intent_name(str): the name of the intents that this parser
251-
parses/validates
252-
"""
253-
self.at_least_one = []
254-
self.requires = []
255-
self.excludes = []
256-
self.optional = []
257-
self.name = intent_name
258-
259-
def one_of(self, *args):
260-
"""
261-
The intent parser should require one of the provided entity types to
262-
validate this clause.
263-
264-
Args:
265-
args(args): *args notation list of entity names
266-
267-
Returns:
268-
self: to continue modifications.
269-
"""
270-
self.at_least_one.append(args)
271-
return self
272-
273-
def require(self, entity_type, attribute_name=None):
274-
"""
275-
The intent parser should require an entity of the provided type.
276-
277-
Args:
278-
entity_type(str): an entity type
279-
attribute_name(str): the name of the attribute on the parsed intent.
280-
Defaults to match entity_type.
281-
282-
Returns:
283-
self: to continue modifications.
284-
"""
285-
if not attribute_name:
286-
attribute_name = entity_type
287-
self.requires += [(entity_type, attribute_name)]
288-
return self
289-
290-
def exclude(self, entity_type):
291-
"""
292-
The intent parser must not contain an entity of the provided type.
293-
294-
Args:
295-
entity_type(str): an entity type
296-
297-
Returns:
298-
self: to continue modifications.
299-
"""
300-
self.excludes.append(entity_type)
301-
return self
302-
303-
def optionally(self, entity_type, attribute_name=None):
304-
"""
305-
Parsed intents from this parser can optionally include an entity of the
306-
provided type.
307-
308-
Args:
309-
entity_type(str): an entity type
310-
attribute_name(str): the name of the attribute on the parsed intent.
311-
Defaults to match entity_type.
312-
313-
Returns:
314-
self: to continue modifications.
315-
"""
316-
if not attribute_name:
317-
attribute_name = entity_type
318-
self.optional += [(entity_type, attribute_name)]
319-
return self
320-
321-
def build(self):
322-
"""
323-
Constructs an intent from the builder's specifications.
324-
325-
:return: an Intent instance.
326-
"""
327-
return Intent(self.name, self.requires,
328-
self.at_least_one, self.optional,
329-
self.excludes)

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
six>=1.10.0
2-
ovos-plugin-manager>=0.0.26a33
2+
ovos-plugin-manager>=0.0.26,<1.0.0
3+
ovos-workshop>=0.1.7,<1.0.0

test/requirements.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1-
flake8
2-
pytest
1+
coveralls>=1.8.2
2+
flake8>=3.7.9
3+
pytest>=5.2.4
4+
pytest-cov>=2.8.1
5+
cov-core>=1.15.0
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)