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

feat: extend dialog/intent templates #317

Merged
merged 4 commits into from
Dec 6, 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
93 changes: 92 additions & 1 deletion ovos_utils/bracket_expansion.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,80 @@
import itertools
import re
from typing import List
from typing import List, Dict

from ovos_utils.log import deprecated


def expand_template(template: str) -> List[str]:
def expand_optional(text):
"""Replace [optional] with two options: one with and one without."""
return re.sub(r"\[([^\[\]]+)\]", lambda m: f"({m.group(1)}|)", text)

def expand_alternatives(text):
"""Expand (alternative|choices) into a list of choices."""
parts = []
for segment in re.split(r"(\([^\(\)]+\))", text):
if segment.startswith("(") and segment.endswith(")"):
options = segment[1:-1].split("|")
parts.append(options)
else:
parts.append([segment])
return itertools.product(*parts)
JarbasAl marked this conversation as resolved.
Show resolved Hide resolved

def fully_expand(texts):
"""Iteratively expand alternatives until all possibilities are covered."""
result = set(texts)
while True:
expanded = set()
for text in result:
options = list(expand_alternatives(text))
expanded.update(["".join(option).strip() for option in options])
if expanded == result: # No new expansions found
break
result = expanded
return sorted(result) # Return a sorted list for consistency

# Expand optional items first
template = expand_optional(template)

# Fully expand all combinations of alternatives
return fully_expand([template])


def expand_slots(template: str, slots: Dict[str, List[str]]) -> List[str]:
"""Expand a template by first expanding alternatives and optional components,
then substituting slot placeholders with their corresponding options.

Args:
template (str): The input string template to expand.
slots (dict): A dictionary where keys are slot names and values are lists of possible replacements.

Returns:
list[str]: A list of all expanded combinations.
"""
# Expand alternatives and optional components
base_expansions = expand_template(template)

# Process slots
all_sentences = []
for sentence in base_expansions:
matches = re.findall(r"\{([^\{\}]+)\}", sentence)
if matches:
# Create all combinations for slots in the sentence
slot_options = [slots.get(match, [f"{{{match}}}"]) for match in matches]
for combination in itertools.product(*slot_options):
filled_sentence = sentence
for slot, replacement in zip(matches, combination):
filled_sentence = filled_sentence.replace(f"{{{slot}}}", replacement)
all_sentences.append(filled_sentence)
else:
# No slots to expand
all_sentences.append(sentence)

return all_sentences


@deprecated("use 'expand_template' directly instead", "1.0.0")
def expand_parentheses(sent: List[str]) -> List[str]:
"""
['1', '(', '2', '|', '3, ')'] -> [['1', '2'], ['1', '3']]
Expand All @@ -22,6 +95,7 @@ def expand_parentheses(sent: List[str]) -> List[str]:
return SentenceTreeParser(sent).expand_parentheses()


@deprecated("use 'expand_template' directly instead", "1.0.0")
def expand_options(parentheses_line: str) -> list:
"""
Convert 'test (a|b)' -> ['test a', 'test b']
Expand All @@ -38,6 +112,7 @@ def expand_options(parentheses_line: str) -> list:
class Fragment:
"""(Abstract) empty sentence fragment"""

@deprecated("use 'expand_template' function directly instead", "1.0.0")
def __init__(self, tree):
"""
Construct a sentence tree fragment which is merely a wrapper for
Expand Down Expand Up @@ -73,6 +148,11 @@ class Word(Fragment):
Construct with a string as argument.
"""

@deprecated("use 'expand_template' function directly instead", "1.0.0")
def __init__(self, tree):
"""DEPRECATED"""
super().__init__(tree)

def expand(self):
"""
Creates one sentence that contains exactly that word.
Expand All @@ -89,6 +169,11 @@ class Sentence(Fragment):
Construct with a List<Fragment> as argument.
"""

@deprecated("use 'expand_template' function directly instead", "1.0.0")
def __init__(self, tree):
"""DEPRECATED"""
super().__init__(tree)

def expand(self):
"""
Creates a combination of all sub-sentences.
Expand All @@ -114,6 +199,11 @@ class Options(Fragment):
Construct with List<Fragment> as argument.
"""

@deprecated("use 'expand_template' function directly instead", "1.0.0")
def __init__(self, tree):
"""DEPRECATED"""
super().__init__(tree)

def expand(self):
"""
Returns all of its options as seperated sub-sentences.
Expand All @@ -133,6 +223,7 @@ class SentenceTreeParser:
['1', '(', '2', '|', '3, ')'] -> [['1', '2'], ['1', '3']]
"""

@deprecated("use 'expand_template' function directly instead", "1.0.0")
def __init__(self, tokens):
self.tokens = tokens

Expand Down
4 changes: 2 additions & 2 deletions ovos_utils/dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pathlib import Path
from typing import Optional

from ovos_utils.bracket_expansion import expand_options
from ovos_utils.bracket_expansion import expand_template
from ovos_utils.file_utils import resolve_resource_file
from ovos_utils.lang import translate_word
from ovos_utils.log import LOG, log_deprecation
Expand Down Expand Up @@ -92,7 +92,7 @@ def render(self, template_name, context=None, index=None):
line = template_functions[index % len(template_functions)]
# Replace {key} in line with matching values from context
line = line.format(**context)
line = random.choice(expand_options(line))
line = random.choice(expand_template(line))

JarbasAl marked this conversation as resolved.
Show resolved Hide resolved
# Here's where we keep track of what we've said recently. Remember,
# this is by line in the .dialog file, not by exact phrase
Expand Down
4 changes: 2 additions & 2 deletions ovos_utils/file_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer

from ovos_utils.bracket_expansion import expand_options
from ovos_utils.bracket_expansion import expand_template
from ovos_utils.log import LOG, log_deprecation


Expand Down Expand Up @@ -241,7 +241,7 @@ def read_vocab_file(path: str) -> List[List[str]]:
for line in voc_file.readlines():
if line.startswith('#') or line.strip() == '':
continue
vocab.append(expand_options(line.lower()))
vocab.append(expand_template(line.lower()))
return vocab


Expand Down
131 changes: 109 additions & 22 deletions test/unittests/test_bracket_expansion.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,118 @@
import unittest

from ovos_utils.bracket_expansion import expand_template, expand_slots

class TestBracketExpansion(unittest.TestCase):
def test_expand_parentheses(self):
from ovos_utils.bracket_expansion import expand_parentheses
# TODO

def test_expand_options(self):
from ovos_utils.bracket_expansion import expand_options
# TODO
class TestTemplateExpansion(unittest.TestCase):

def test_fragment(self):
from ovos_utils.bracket_expansion import Fragment
# TODO
def test_expand_slots(self):
# Test for expanding slots
template = "change [the ]brightness to {brightness_level} and color to {color_name}"
slots = {
"brightness_level": ["low", "medium", "high"],
"color_name": ["red", "green", "blue"]
}

def test_word(self):
from ovos_utils.bracket_expansion import Word
# TODO
expanded_sentences = expand_slots(template, slots)

def test_sentence(self):
from ovos_utils.bracket_expansion import Sentence
# TODO
expected_sentences = ['change brightness to low and color to red',
'change brightness to low and color to green',
'change brightness to low and color to blue',
'change brightness to medium and color to red',
'change brightness to medium and color to green',
'change brightness to medium and color to blue',
'change brightness to high and color to red',
'change brightness to high and color to green',
'change brightness to high and color to blue',
'change the brightness to low and color to red',
'change the brightness to low and color to green',
'change the brightness to low and color to blue',
'change the brightness to medium and color to red',
'change the brightness to medium and color to green',
'change the brightness to medium and color to blue',
'change the brightness to high and color to red',
'change the brightness to high and color to green',
'change the brightness to high and color to blue']
self.assertEqual(expanded_sentences, expected_sentences)

def test_options(self):
from ovos_utils.bracket_expansion import Options
# TODO
def test_expand_template(self):
# Test for template expansion
templates = [
"[hello,] (call me|my name is) {name}",
"Expand (alternative|choices) into a list of choices.",
"sentences have [optional] words ",
"alternative words can be (used|written)",
"sentence[s] can have (pre|suf)fixes mid word too",
"do( the | )thing(s|) (old|with) style and( no | )spaces",
"[(this|that) is optional]",
"tell me a [{joke_type}] joke",
"play {query} [in ({device_name}|{skill_name}|{zone_name})]"
]

def test_sentence_tree_parser(self):
from ovos_utils.bracket_expansion import SentenceTreeParser
# TODO
expected_outputs = {
"[hello,] (call me|my name is) {name}": [
"call me {name}",
"hello, call me {name}",
"hello, my name is {name}",
"my name is {name}"
],
"Expand (alternative|choices) into a list of choices.": [
"Expand alternative into a list of choices.",
"Expand choices into a list of choices."
],
"sentences have [optional] words ": [
"sentences have words",
"sentences have optional words"
],
"alternative words can be (used|written)": [
"alternative words can be used",
"alternative words can be written"
],
"sentence[s] can have (pre|suf)fixes mid word too": [
"sentence can have prefixes mid word too",
"sentence can have suffixes mid word too",
"sentences can have prefixes mid word too",
"sentences can have suffixes mid word too"
],
"do( the | )thing(s|) (old|with) style and( no | )spaces": [
"do the thing old style and no spaces",
"do the thing old style and spaces",
"do the thing with style and no spaces",
"do the thing with style and spaces",
"do the things old style and no spaces",
"do the things old style and spaces",
"do the things with style and no spaces",
"do the things with style and spaces",
"do thing old style and no spaces",
"do thing old style and spaces",
"do thing with style and no spaces",
"do thing with style and spaces",
"do things old style and no spaces",
"do things old style and spaces",
"do things with style and no spaces",
"do things with style and spaces"
],
"[(this|that) is optional]": [
'',
'that is optional',
'this is optional'],
"tell me a [{joke_type}] joke": [
"tell me a joke",
"tell me a {joke_type} joke"
],
"play {query} [in ({device_name}|{skill_name}|{zone_name})]": [
"play {query}",
"play {query} in {device_name}",
"play {query} in {skill_name}",
"play {query} in {zone_name}"
]
}

for template, expected_sentences in expected_outputs.items():
with self.subTest(template=template):
expanded_sentences = expand_template(template)
self.assertEqual(expanded_sentences, expected_sentences)


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