Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor:upstream classes #6

Merged
merged 11 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
pip install -r test/requirements.txt
- name: Run unittests
run: |
pytest --cov=ovos_adapt --cov-report=xml test/unittests
pytest --cov=ovos_adapt --cov-report=xml ./test
# NOTE: additional pytest invocations should also add the --cov-append flag
# or they will overwrite previous invocations' coverage reports
# (for an example, see OVOS Skill Manager's workflow)
Expand Down
247 changes: 3 additions & 244 deletions ovos_adapt/intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
__author__ = 'seanfitz'

import itertools

CLIENT_ENTITY_NAME = 'Client'
from ovos_workshop.intents import Intent, IntentBuilder
JarbasAl marked this conversation as resolved.
Show resolved Hide resolved


def is_entity(tag, entity_name):
Expand All @@ -44,15 +43,7 @@ def find_first_tag(tags, entity_type, after_index=-1):
confidence(float): is a measure of accuracy. 1 is full confidence
and 0 is none.
"""
for tag in tags:
for entity in tag.get('entities'):
for v, t in entity.get('data'):
if t.lower() == entity_type.lower() and \
(tag.get('start_token', 0) > after_index or \
tag.get('from_context', False)):
return tag, v, entity.get('confidence')

return None, None, None
return Intent._find_first_tag(tags, entity_type, after_index)
JarbasAl marked this conversation as resolved.
Show resolved Hide resolved


def find_next_tag(tags, end_index=0):
Expand Down Expand Up @@ -93,237 +84,5 @@ def resolve_one_of(tags, at_least_one):
object:
returns None if no match is found but returns any match as an object
"""
return Intent._resolve_one_of(tags, at_least_one)
JarbasAl marked this conversation as resolved.
Show resolved Hide resolved

for possible_resolution in choose_1_from_each(at_least_one):
resolution = {}
pr = possible_resolution[:]
for entity_type in pr:
last_end_index = -1
if entity_type in resolution:
last_end_index = resolution[entity_type][-1].get('end_token')
tag, value, c = find_first_tag(tags, entity_type,
after_index=last_end_index)
if not tag:
break
else:
if entity_type not in resolution:
resolution[entity_type] = []
resolution[entity_type].append(tag)
# Check if this is a valid resolution (all one_of rules matched)
if len(resolution) == len(possible_resolution):
return resolution

return None


class Intent(object):
def __init__(self, name, requires, at_least_one, optional, excludes=None):
"""Create Intent object

Args:
name(str): Name for Intent
requires(list): Entities that are required
at_least_one(list): One of these Entities are required
optional(list): Optional Entities used by the intent
"""
self.name = name
self.requires = requires
self.at_least_one = at_least_one
self.optional = optional
self.excludes = excludes or []

def validate(self, tags, confidence):
"""Using this method removes tags from the result of validate_with_tags

Returns:
intent(intent): Results from validate_with_tags
"""
intent, tags = self.validate_with_tags(tags, confidence)
return intent

def validate_with_tags(self, tags, confidence):
"""Validate whether tags has required entites for this intent to fire

Args:
tags(list): Tags and Entities used for validation
confidence(float): The weight associate to the parse result,
as indicated by the parser. This is influenced by a parser
that uses edit distance or context.

Returns:
intent, tags: Returns intent and tags used by the intent on
failure to meat required entities then returns intent with
confidence
of 0.0 and an empty list for tags.
"""
result = {'intent_type': self.name}
intent_confidence = 0.0
local_tags = tags[:]
used_tags = []

# Check excludes first
for exclude_type in self.excludes:
exclude_tag, _canonical_form, _tag_confidence = \
find_first_tag(local_tags, exclude_type)
if exclude_tag:
result['confidence'] = 0.0
return result, []

for require_type, attribute_name in self.requires:
required_tag, canonical_form, tag_confidence = \
find_first_tag(local_tags, require_type)
if not required_tag:
result['confidence'] = 0.0
return result, []

result[attribute_name] = canonical_form
if required_tag in local_tags:
local_tags.remove(required_tag)
used_tags.append(required_tag)
intent_confidence += tag_confidence

if len(self.at_least_one) > 0:
best_resolution = resolve_one_of(local_tags, self.at_least_one)
if not best_resolution:
result['confidence'] = 0.0
return result, []
else:
for key in best_resolution:
# TODO: at least one should support aliases
result[key] = best_resolution[key][0].get('key')
intent_confidence += \
1.0 * best_resolution[key][0]['entities'][0]\
.get('confidence', 1.0)
used_tags.append(best_resolution[key][0])
if best_resolution in local_tags:
local_tags.remove(best_resolution[key][0])

for optional_type, attribute_name in self.optional:
optional_tag, canonical_form, tag_confidence = \
find_first_tag(local_tags, optional_type)
if not optional_tag or attribute_name in result:
continue
result[attribute_name] = canonical_form
if optional_tag in local_tags:
local_tags.remove(optional_tag)
used_tags.append(optional_tag)
intent_confidence += tag_confidence

total_confidence = (intent_confidence / len(tags) * confidence) \
if tags else 0.0

target_client, canonical_form, confidence = \
find_first_tag(local_tags, CLIENT_ENTITY_NAME)

result['target'] = target_client.get('key') if target_client else None
result['confidence'] = total_confidence

return result, used_tags


class IntentBuilder(object):
"""
IntentBuilder, used to construct intent parsers.

Attributes:
at_least_one(list): A list of Entities where one is required.
These are separated into lists so you can have one of (A or B) and
then require one of (D or F).
requires(list): A list of Required Entities
optional(list): A list of optional Entities
name(str): Name of intent

Notes:
This is designed to allow construction of intents in one line.

Example:
IntentBuilder("Intent")\
.requires("A")\
.one_of("C","D")\
.optional("G").build()
"""
def __init__(self, intent_name):
"""
Constructor

Args:
intent_name(str): the name of the intents that this parser
parses/validates
"""
self.at_least_one = []
self.requires = []
self.excludes = []
self.optional = []
self.name = intent_name

def one_of(self, *args):
"""
The intent parser should require one of the provided entity types to
validate this clause.

Args:
args(args): *args notation list of entity names

Returns:
self: to continue modifications.
"""
self.at_least_one.append(args)
return self

def require(self, entity_type, attribute_name=None):
"""
The intent parser should require an entity of the provided type.

Args:
entity_type(str): an entity type
attribute_name(str): the name of the attribute on the parsed intent.
Defaults to match entity_type.

Returns:
self: to continue modifications.
"""
if not attribute_name:
attribute_name = entity_type
self.requires += [(entity_type, attribute_name)]
return self

def exclude(self, entity_type):
"""
The intent parser must not contain an entity of the provided type.

Args:
entity_type(str): an entity type

Returns:
self: to continue modifications.
"""
self.excludes.append(entity_type)
return self

def optionally(self, entity_type, attribute_name=None):
"""
Parsed intents from this parser can optionally include an entity of the
provided type.

Args:
entity_type(str): an entity type
attribute_name(str): the name of the attribute on the parsed intent.
Defaults to match entity_type.

Returns:
self: to continue modifications.
"""
if not attribute_name:
attribute_name = entity_type
self.optional += [(entity_type, attribute_name)]
return self

def build(self):
"""
Constructs an intent from the builder's specifications.

:return: an Intent instance.
"""
return Intent(self.name, self.requires,
self.at_least_one, self.optional,
self.excludes)
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
six>=1.10.0
ovos-plugin-manager>=0.0.26a33
ovos-plugin-manager>=0.0.26,<1.0.0
ovos-workshop>=0.1.7,<1.0.0
7 changes: 5 additions & 2 deletions test/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
flake8
pytest
coveralls>=1.8.2
flake8>=3.7.9
pytest>=5.2.4
pytest-cov>=2.8.1
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.
File renamed without changes.
File renamed without changes.
Loading