Skip to content

Commit

Permalink
Merge pull request #319 from OpenVoiceOS/release-0.6.0a1
Browse files Browse the repository at this point in the history
Release 0.6.0a1
  • Loading branch information
JarbasAl authored Dec 6, 2024
2 parents bffe411 + e0b1fce commit 47a91f2
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 33 deletions.
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# Changelog

## [0.5.6a1](https://github.com/OpenVoiceOS/ovos-utils/tree/0.5.6a1) (2024-12-04)
## [0.6.0a1](https://github.com/OpenVoiceOS/ovos-utils/tree/0.6.0a1) (2024-12-06)

[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/0.5.5...0.5.6a1)
[Full Changelog](https://github.com/OpenVoiceOS/ovos-utils/compare/0.5.6...0.6.0a1)

**Implemented enhancements:**

- feat: support \[optional\] syntax [\#312](https://github.com/OpenVoiceOS/ovos-utils/issues/312)

**Merged pull requests:**

- refactor:move\_to\_extras [\#315](https://github.com/OpenVoiceOS/ovos-utils/pull/315) ([JarbasAl](https://github.com/JarbasAl))
- feat: extend dialog/intent templates [\#317](https://github.com/OpenVoiceOS/ovos-utils/pull/317) ([JarbasAl](https://github.com/JarbasAl))



Expand Down
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)

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))

# 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
6 changes: 3 additions & 3 deletions ovos_utils/version.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# START_VERSION_BLOCK
VERSION_MAJOR = 0
VERSION_MINOR = 5
VERSION_BUILD = 6
VERSION_ALPHA = 0
VERSION_MINOR = 6
VERSION_BUILD = 0
VERSION_ALPHA = 1
# END_VERSION_BLOCK
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()

0 comments on commit 47a91f2

Please sign in to comment.