Skip to content
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
61 changes: 58 additions & 3 deletions ovos_workshop/resource_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from ovos_config.locations import get_xdg_data_save_path
from ovos_utils import flatten_list
from ovos_utils.bracket_expansion import expand_template
from ovos_spec_tools import inline_keywords
from ovos_utils.dialog import MustacheDialogRenderer, load_dialogs
from ovos_utils.log import LOG

Expand Down Expand Up @@ -442,6 +443,9 @@ class IntentFile(ResourceFile):
def __init__(self, resource_type, resource_name):
super().__init__(resource_type, resource_name)
self.data = None
# {name: members} of the sibling .voc files, so an inline <name>
# reference (OVOS-INTENT-1 §3.7) resolves in place at load time.
self.vocabularies = None

def load(self, entities: bool = True) -> List[str]:
"""Load file containing intents.
Expand All @@ -454,8 +458,12 @@ def load(self, entities: bool = True) -> List[str]:
intents = []
if self.file_path is not None:
for line in self._read():
line = line.replace("{{", "{").replace("}}", "}")
intents.extend(flatten_list(expand_template(line.lower())))
line = line.replace("{{", "{").replace("}}", "}").lower()
if self.vocabularies and "<" in line:
# OVOS-INTENT-1 §3.7: inline every <name> reference from the
# sibling vocabulary as an (a|b|c) group before expanding.
line = inline_keywords(line, self.vocabularies)
intents.extend(flatten_list(expand_template(line)))
if not entities:
intents = [re.sub(r'{.*?}\s?', '', intent).strip() for intent in intents]
elif self.data:
Expand Down Expand Up @@ -555,6 +563,12 @@ class BlacklistFile(ResourceFile):
"""Defines a blacklist file, a slot-free list of phrases that should
prevent the sibling intent (same base name) from matching."""

def __init__(self, resource_type, resource_name):
super().__init__(resource_type, resource_name)
# {name: members} of the sibling .voc files, so an inline <name>
# reference (OVOS-INTENT-1 §3.7) resolves in place at load time.
self.vocabularies = None

def load(self) -> List[str]:
"""Load the phrases contained in a blacklist file.

Expand All @@ -564,7 +578,14 @@ def load(self) -> List[str]:
phrases = []
if self.file_path is not None:
for line in self._read():
phrases.append(line.lower())
line = line.lower()
if self.vocabularies and "<" in line:
# OVOS-INTENT-1 §3.7: inline every <name> reference from the
# sibling vocabulary and enumerate the resulting phrases.
line = inline_keywords(line, self.vocabularies)
phrases.extend(flatten_list(expand_template(line)))
else:
phrases.append(line)
return phrases


Expand Down Expand Up @@ -681,8 +702,41 @@ def load_intent_file(self, name: str,
"""
intent_file = IntentFile(self.types.intent, name)
intent_file.data = data
intent_file.vocabularies = self._locale_vocabularies()
return intent_file.load(entities)

def _locale_vocabularies(self) -> Dict[str, List[str]]:
"""Build a ``{name: members}`` map from every ``.voc`` in this
language's locale tree.

Feeds the inline ``<name>`` resolution required by OVOS-INTENT-1 §3.7:
a reference in an ``.intent`` or ``.blacklist`` expands from the sibling
vocabulary of that name. User overrides take precedence over the skill's
own resources, which take precedence over workshop-shipped resources.
"""
vocabularies: Dict[str, List[str]] = {}
vocab_type = self.types.vocabulary
for base in (vocab_type.user_directory,
vocab_type.base_directory,
vocab_type.workshop_directory):
if not base:
continue
for directory, _, files in walk(str(base)):
for file_name in files:
if not file_name.endswith(".voc"):
continue
name = file_name[:-len(".voc")].lower()
if name in vocabularies:
continue
members = []
with open(Path(directory, file_name)) as voc_file:
for line in (ln.strip() for ln in voc_file):
if line and not line.startswith("#"):
members.append(line.lower())
if members:
vocabularies[name] = members
return vocabularies

def load_blacklist_file(self, name: str) -> List[str]:
"""
Loads the phrases contained in a blacklist file.
Expand All @@ -696,6 +750,7 @@ def load_blacklist_file(self, name: str) -> List[str]:
A list of blacklisted phrases
"""
blacklist_file = BlacklistFile(self.types.blacklist, name)
blacklist_file.vocabularies = self._locale_vocabularies()
return blacklist_file.load()

def locate_qml_file(self, name: str) -> str:
Expand Down
52 changes: 52 additions & 0 deletions test/unittests/test_inline_vocab_refs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import unittest
from tempfile import TemporaryDirectory
from pathlib import Path


class TestInlineVocabReferences(unittest.TestCase):
"""OVOS-INTENT-1 §3.7: an inline ``<name>`` reference in a ``.intent`` or
``.blacklist`` file must expand in place from the sibling ``.voc`` of that
name found in the same locale directory."""

def _skill_dir(self, files: dict) -> str:
tmp = TemporaryDirectory()
self.addCleanup(tmp.cleanup)
locale = Path(tmp.name, "locale", "en-us")
locale.mkdir(parents=True)
for name, content in files.items():
(locale / name).write_text(content)
return tmp.name

def _resources(self, skill_dir):
from ovos_workshop.resource_files import SkillResources
return SkillResources(skill_dir, "en-us", skill_id="test.skill")

def test_intent_inline_vocab(self):
skill_dir = self._skill_dir({
"thing.voc": "lamp\nlight\n",
"foo.intent": "turn on the <thing>\n",
})
intents = self._resources(skill_dir).load_intent_file("foo.intent")
self.assertIn("turn on the lamp", intents)
self.assertIn("turn on the light", intents)

def test_blacklist_inline_vocab(self):
skill_dir = self._skill_dir({
"pronoun.voc": "it\nthem\n",
"bar.blacklist": "stop <pronoun>\n",
})
phrases = self._resources(skill_dir).load_blacklist_file("bar.blacklist")
self.assertIn("stop it", phrases)
self.assertIn("stop them", phrases)

def test_intent_without_inline_ref_unchanged(self):
skill_dir = self._skill_dir({
"foo.intent": "turn on the (lamp|light)\n",
})
intents = self._resources(skill_dir).load_intent_file("foo.intent")
self.assertIn("turn on the lamp", intents)
self.assertIn("turn on the light", intents)


if __name__ == "__main__":
unittest.main()
Loading