From 307ee0e9c7b56169a6e184fcbe95c5205d817c90 Mon Sep 17 00:00:00 2001 From: Swen Gross Date: Sun, 7 May 2023 00:26:32 +0200 Subject: [PATCH 01/28] add package data (#9) --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 37bb263..b34ab30 100644 --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ def required(requirements_file): name='padacioso', version=get_version(), packages=['padacioso'], + package_data={'': package_files('padacioso')}, url='https://github.com/OpenVoiceOS/padacioso', license='apache-2.0', author='jarbasai', From 145c27bd9890e57a28414b1b9b1ba67718576b4e Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 6 May 2023 22:26:54 +0000 Subject: [PATCH 02/28] Increment Version to 0.2.1a1 --- padacioso/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/padacioso/version.py b/padacioso/version.py index 516076b..253cebe 100644 --- a/padacioso/version.py +++ b/padacioso/version.py @@ -2,6 +2,6 @@ # START_VERSION_BLOCK VERSION_MAJOR = 0 VERSION_MINOR = 2 -VERSION_BUILD = 0 -VERSION_ALPHA = 0 +VERSION_BUILD = 1 +VERSION_ALPHA = 1 # END_VERSION_BLOCK From 8888ffe95731edafad02e9603559abd24bb363a4 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 6 May 2023 22:27:23 +0000 Subject: [PATCH 03/28] Update Changelog --- CHANGELOG.md | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adc0cc4..4aa9639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,12 @@ # Changelog -## [V0.1.3a2](https://github.com/OpenVoiceOS/padacioso/tree/V0.1.3a2) (2023-05-05) +## [0.2.1a1](https://github.com/OpenVoiceOS/padacioso/tree/0.2.1a1) (2023-05-06) -[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.1.3a1...V0.1.3a2) +[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.0...0.2.1a1) **Merged pull requests:** -- Optimization and Confidence Adjustments [\#7](https://github.com/OpenVoiceOS/padacioso/pull/7) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.1.3a1](https://github.com/OpenVoiceOS/padacioso/tree/V0.1.3a1) (2023-05-03) - -[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/0.1.1...V0.1.3a1) - -**Fixed bugs:** - -- Lowercase entity names in intent matches for backwards-compat. [\#5](https://github.com/OpenVoiceOS/padacioso/pull/5) ([NeonDaniel](https://github.com/NeonDaniel)) -- Normalize braces around entities for compat with existing intents [\#4](https://github.com/OpenVoiceOS/padacioso/pull/4) ([NeonDaniel](https://github.com/NeonDaniel)) - -**Closed issues:** - -- Normalize entities for compat. [\#3](https://github.com/OpenVoiceOS/padacioso/issues/3) - -**Merged pull requests:** - -- Automate releases and Update tests [\#6](https://github.com/OpenVoiceOS/padacioso/pull/6) ([NeonDaniel](https://github.com/NeonDaniel)) +- add package data [\#9](https://github.com/OpenVoiceOS/padacioso/pull/9) ([emphasize](https://github.com/emphasize)) From 596dafc7d48f3be153f85248f1968d288dc89205 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Wed, 10 May 2023 11:15:33 -0700 Subject: [PATCH 04/28] Optimize intent matching (#10) * Update IntentContainer to keep `Matcher` objects instead of re-initializing at runtime * Move `Matcher` init to intent creation time * Sort intent regex at load instead of runtime * Add tests for intent/entity add/remove Raise exceptions when trying to re-register an existing intent/entity Remove unused `intents` and `entities` parameters in `IntentContainer` --- padacioso/__init__.py | 99 ++++++++++++++++++++++++++++++++++-------- test/test_padacioso.py | 36 +++++++++++++++ 2 files changed, 117 insertions(+), 18 deletions(-) diff --git a/padacioso/__init__.py b/padacioso/__init__.py index 24a91c6..5c45a0e 100644 --- a/padacioso/__init__.py +++ b/padacioso/__init__.py @@ -1,5 +1,6 @@ import simplematch +from typing import List, Iterator, Optional from padacioso.bracket_expansion import expand_parentheses, clean_braces try: @@ -12,16 +13,19 @@ class IntentContainer: def __init__(self, fuzz=False): self.intent_samples, self.entity_samples = {}, {} - self.intents, self.entities = {}, {} + # self.intents, self.entities = {}, {} self.fuzz = fuzz + self._cased_matchers = dict() + self._uncased_matchers = dict() - def add_intent(self, name, lines): - expanded = [] - for l in lines: - expanded += expand_parentheses(clean_braces(l)) - self.intent_samples[name] = list(set(expanded)) - - def _get_fuzzed(self, sample): + @staticmethod + def _get_fuzzed(sample: str) -> List[str]: + """ + Get fuzzy match examples by allowing a wildcard in place of each + specified word. + @param sample: Utterance example to mutate + @return: list of fuzzy string alternatives to `sample` + """ fuzzed = [] words = sample.split(" ") for idx in range(0, len(words)): @@ -32,32 +36,81 @@ def _get_fuzzed(self, sample): fuzzed.append(" ".join(new_words)) return fuzzed + [f"* {sample}", f"{sample} *"] - def remove_intent(self, name): + def add_intent(self, name: str, lines: List[str]): + """ + Add an intent with examples. + @param name: name of intent to add + @param lines: list of intent regexes + """ if name in self.intent_samples: - del self.intent_samples[name] + raise RuntimeError(f"Attempted to re-register existing intent: " + f"{name}") + expanded = [] + for l in lines: + expanded += expand_parentheses(clean_braces(l)) + regexes = list(set(expanded)) + regexes.sort(key=len, reverse=True) + self.intent_samples[name] = regexes + for r in regexes: + self._cased_matchers[r] = \ + simplematch.Matcher(r, case_sensitive=True) + self._uncased_matchers[r] = \ + simplematch.Matcher(r, case_sensitive=False) + + def remove_intent(self, name: str): + """ + Remove an intent + @param name: name of intent to remove + """ + if name in self.intent_samples: + regexes = self.intent_samples.pop(name) + for rx in regexes: + if rx in self._cased_matchers: + self._cased_matchers.pop(rx) + if rx in self._uncased_matchers: + self._uncased_matchers.pop(rx) - def add_entity(self, name, lines): + def add_entity(self, name: str, lines: List[str]): + """ + Add an entity with examples. + @param name: name of entity to add + @param lines: list of entity examples + """ + if name in self.entity_samples: + raise RuntimeError(f"Attempted to re-register existing entity: " + f"{name}") name = name.lower() expanded = [] for l in lines: expanded += expand_parentheses(l) self.entity_samples[name] = expanded - def remove_entity(self, name): + def remove_entity(self, name: str): + """ + Remove an entity + @param name: name of entity to remove + """ name = name.lower() if name in self.entity_samples: del self.entity_samples[name] - def calc_intents(self, query): + def calc_intents(self, query: str) -> Iterator[dict]: + """ + Determine possible intents for a given query + @param query: input to evaluate for an intent match + @return: yields dict intent matches + """ for intent_name, regexes in self.intent_samples.items(): - regexes = sorted(regexes, key=len, reverse=True) for r in regexes: penalty = 0 if "*" in r: # penalize wildcards penalty = 0.15 - - entities = simplematch.match(r, query, case_sensitive=True) + if r not in self._cased_matchers: + LOG.warning(f"{r} not initialized") + self._cased_matchers[r] = \ + simplematch.Matcher(r, case_sensitive=True) + entities = self._cased_matchers[r].match(query) if entities is not None: for k, v in entities.items(): if k not in self.entity_samples: @@ -71,7 +124,11 @@ def calc_intents(self, query): "name": intent_name} break - entities = simplematch.match(r, query, case_sensitive=False) + if r not in self._uncased_matchers: + LOG.warning(f"{r} not initialized") + self._uncased_matchers[r] = \ + simplematch.Matcher(r, case_sensitive=False) + entities = self._uncased_matchers[r].match(query) if entities is not None: # penalize case mismatch penalty += 0.05 @@ -88,6 +145,7 @@ def calc_intents(self, query): break if self.fuzz: + LOG.debug(f"Fallback to fuzzy match") penalty += 0.25 for f in self._get_fuzzed(r): entities = simplematch.match(f, query, @@ -98,7 +156,12 @@ def calc_intents(self, query): "name": intent_name} break - def calc_intent(self, query): + def calc_intent(self, query: str) -> Optional[dict]: + """ + Determine the best intent match for a given query + @param query: input to evaluate for an intent + @return: dict matched intent (or None) + """ match = max( self.calc_intents(query), key=lambda x: x["conf"], diff --git a/test/test_padacioso.py b/test/test_padacioso.py index c779553..f62c51b 100644 --- a/test/test_padacioso.py +++ b/test/test_padacioso.py @@ -167,3 +167,39 @@ def test_fuzz(self): self.assertEqual(intent["name"], "test2") self.assertEqual(intent["entities"], {'thing': 'Mycroft'}) + def test_add_remove_intent(self): + container = IntentContainer() + # Add intent valid + container.add_intent("hello", ["hi", "hello", "howdy", + "how (are you|do you do)"]) + self.assertEqual(len(container.intent_samples['hello']), 5) + self.assertEqual(len(container._cased_matchers), 5) + self.assertEqual(len(container._cased_matchers), + len(container._uncased_matchers)) + # Add intent already defined + with self.assertRaises(RuntimeError): + container.add_intent("hello", ["invalid"]) + # Add second intent + container.add_intent("test", ["test(ing|)"]) + self.assertEqual(len(container.intent_samples['test']), 2) + self.assertEqual(len(container._cased_matchers), + len(container._uncased_matchers)) + # Remove intent + container.remove_intent("test") + self.assertNotIn("test", container.intent_samples) + self.assertEqual(len(container.intent_samples['hello']), 5) + self.assertEqual(len(container._cased_matchers), 5) + self.assertEqual(len(container._cased_matchers), + len(container._uncased_matchers)) + + def test_add_remove_entity(self): + container = IntentContainer() + # Add entity valid + container.add_entity("entity", ["test(ing|)", "another test"]) + self.assertEqual(len(container.entity_samples["entity"]), 3) + # Add entity already defined + with self.assertRaises(RuntimeError): + container.add_entity("entity", ["invalid"]) + # Remove entity + container.remove_entity("entity") + self.assertNotIn("entity", container.entity_samples.keys()) From 0995299b60885cbe1687a6e2980756b4e38c6e72 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 11 May 2023 22:19:51 +0000 Subject: [PATCH 05/28] Increment Version to 0.2.1a2 --- padacioso/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padacioso/version.py b/padacioso/version.py index 253cebe..9ec8458 100644 --- a/padacioso/version.py +++ b/padacioso/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 2 VERSION_BUILD = 1 -VERSION_ALPHA = 1 +VERSION_ALPHA = 2 # END_VERSION_BLOCK From fd0b09bd99c7fd7cba6bac053f043055b7ff8844 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 11 May 2023 22:20:21 +0000 Subject: [PATCH 06/28] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aa9639..029e7b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.2.1a1](https://github.com/OpenVoiceOS/padacioso/tree/0.2.1a1) (2023-05-06) +## [0.2.1a2](https://github.com/OpenVoiceOS/padacioso/tree/0.2.1a2) (2023-05-11) -[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.0...0.2.1a1) +[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a1...0.2.1a2) + +**Merged pull requests:** + +- Optimize intent matching [\#10](https://github.com/OpenVoiceOS/padacioso/pull/10) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.2.1a1](https://github.com/OpenVoiceOS/padacioso/tree/V0.2.1a1) (2023-05-06) + +[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.0...V0.2.1a1) **Merged pull requests:** From 987677a426c63fc76b2dddcf5326bdb431f3ea87 Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Thu, 11 May 2023 22:39:49 +0000 Subject: [PATCH 07/28] Increment Version to 0.2.1a3 --- padacioso/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padacioso/version.py b/padacioso/version.py index 9ec8458..25c7a0d 100644 --- a/padacioso/version.py +++ b/padacioso/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 2 VERSION_BUILD = 1 -VERSION_ALPHA = 2 +VERSION_ALPHA = 3 # END_VERSION_BLOCK From 26e006fb1480fe5fffa6dbe5f443519ede95b982 Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Thu, 11 May 2023 22:40:18 +0000 Subject: [PATCH 08/28] Update Changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 029e7b6..2d522e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [0.2.1a2](https://github.com/OpenVoiceOS/padacioso/tree/0.2.1a2) (2023-05-11) +## [V0.2.1a2](https://github.com/OpenVoiceOS/padacioso/tree/V0.2.1a2) (2023-05-11) -[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a1...0.2.1a2) +[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a1...V0.2.1a2) **Merged pull requests:** From 9e2cfab3672912aef9a3639119171b633546b622 Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Fri, 12 May 2023 00:25:40 +0000 Subject: [PATCH 09/28] Increment Version to 0.2.1a4 --- padacioso/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padacioso/version.py b/padacioso/version.py index 25c7a0d..98d42fa 100644 --- a/padacioso/version.py +++ b/padacioso/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 2 VERSION_BUILD = 1 -VERSION_ALPHA = 3 +VERSION_ALPHA = 4 # END_VERSION_BLOCK From 02da04c52ab0eaf8e15e7ea1ff9a83bfc51533f6 Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Fri, 12 May 2023 00:26:05 +0000 Subject: [PATCH 10/28] Update Changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d522e9..05d0dd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [V0.2.1a3](https://github.com/OpenVoiceOS/padacioso/tree/V0.2.1a3) (2023-05-11) + +[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a2...V0.2.1a3) + ## [V0.2.1a2](https://github.com/OpenVoiceOS/padacioso/tree/V0.2.1a2) (2023-05-11) [Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a1...V0.2.1a2) From 1c07dbb2822a1ee856be1f411f20bcb46b1c3738 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Tue, 16 May 2023 09:21:08 -0700 Subject: [PATCH 11/28] Adds support for Padatious `:0` syntax with unit tests (#12) Closes #11 --- padacioso/__init__.py | 16 ++++++++++++++-- padacioso/bracket_expansion.py | 21 +++++++++++++++++++++ test/test_padacioso.py | 26 ++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/padacioso/__init__.py b/padacioso/__init__.py index 5c45a0e..6ba9d91 100644 --- a/padacioso/__init__.py +++ b/padacioso/__init__.py @@ -1,7 +1,7 @@ import simplematch from typing import List, Iterator, Optional -from padacioso.bracket_expansion import expand_parentheses, clean_braces +from padacioso.bracket_expansion import expand_parentheses, normalize_example try: from ovos_utils.log import LOG @@ -18,6 +18,10 @@ def __init__(self, fuzz=False): self._cased_matchers = dict() self._uncased_matchers = dict() + if "word" not in simplematch.types: + LOG.debug(f"Registering `word` type") + _init_sm_word_type() + @staticmethod def _get_fuzzed(sample: str) -> List[str]: """ @@ -47,7 +51,7 @@ def add_intent(self, name: str, lines: List[str]): f"{name}") expanded = [] for l in lines: - expanded += expand_parentheses(clean_braces(l)) + expanded += expand_parentheses(normalize_example(l)) regexes = list(set(expanded)) regexes.sort(key=len, reverse=True) self.intent_samples[name] = regexes @@ -172,3 +176,11 @@ def calc_intent(self, query: str) -> Optional[dict]: match['entities'][entity.lower()] = entities LOG.debug(match) return match + + +def _init_sm_word_type(): + """ + Registers a `word` type with SimpleMatch to support Padatious `:0` syntax + """ + regex = r"[a-zA-Z0-9]+" + simplematch.register_type("word", regex) diff --git a/padacioso/bracket_expansion.py b/padacioso/bracket_expansion.py index b688d40..e49b268 100644 --- a/padacioso/bracket_expansion.py +++ b/padacioso/bracket_expansion.py @@ -195,3 +195,24 @@ def clean_braces(example: str) -> str: """ clean = example.replace('{{', '{').replace('}}', '}') return clean + + +def translate_padatious(example: str) -> str: + """ + Translate Padatious `:0` syntax to standard regex + @param example: input intent example + @return: parsed intent example with Padatious syntax replaced with regex + """ + if ':0' not in example: + return example + tokens = example.split() + i = 0 + for idx, token in enumerate(tokens): + if token == ":0": + tokens[idx] = '{' + f'word{i}:word' + '}' + i += 1 + return " ".join(tokens) + + +def normalize_example(example: str) -> str: + return clean_braces(translate_padatious(example)) diff --git a/test/test_padacioso.py b/test/test_padacioso.py index f62c51b..d4dfd5a 100644 --- a/test/test_padacioso.py +++ b/test/test_padacioso.py @@ -203,3 +203,29 @@ def test_add_remove_entity(self): # Remove entity container.remove_entity("entity") self.assertNotIn("entity", container.entity_samples.keys()) + + def test_translate_padatious(self): + from padacioso.bracket_expansion import translate_padatious + intent = ":0 :0 what time is it" + self.assertEqual(translate_padatious(intent), + "{word0:word} {word1:word} what time is it") + + def test_add_padatious_wildcard_intent(self): + container = IntentContainer() + container.add_intent("test_single_wildcard", [":0 what time is it"]) + match = container.calc_intent("neon what time is it") + self.assertEqual(match['name'], 'test_single_wildcard') + self.assertEqual(match['entities']['word0'], 'neon') + + match = container.calc_intent("neon neon what time is it") + self.assertIsNone(match['name']) + + container.add_intent("test_double_wildcard", [":0 :0 how are you"]) + match = container.calc_intent("neon how are you") + self.assertIsNone(match['name']) + + match = container.calc_intent("neon neon how are you") + self.assertEqual(match['name'], 'test_double_wildcard') + self.assertEqual(match['entities']['word0'], 'neon') + self.assertEqual(match['entities']['word1'], 'neon') + From d24e5de01590aeb1b3eb045c709c39bfdeedf7f6 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 16 May 2023 16:21:28 +0000 Subject: [PATCH 12/28] Increment Version to 0.2.1a5 --- padacioso/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padacioso/version.py b/padacioso/version.py index 98d42fa..c9dfd4e 100644 --- a/padacioso/version.py +++ b/padacioso/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 2 VERSION_BUILD = 1 -VERSION_ALPHA = 4 +VERSION_ALPHA = 5 # END_VERSION_BLOCK From dbca43925f35e3b3b682800520be8a678f3f2bc5 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 16 May 2023 16:21:52 +0000 Subject: [PATCH 13/28] Update Changelog --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05d0dd0..f1833b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [0.2.1a5](https://github.com/OpenVoiceOS/padacioso/tree/0.2.1a5) (2023-05-16) + +[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a4...0.2.1a5) + +**Implemented enhancements:** + +- Adds support for Padatious `:0` syntax with unit tests [\#12](https://github.com/OpenVoiceOS/padacioso/pull/12) ([NeonDaniel](https://github.com/NeonDaniel)) + +**Closed issues:** + +- Handle `:0` as `*` [\#11](https://github.com/OpenVoiceOS/padacioso/issues/11) + +## [V0.2.1a4](https://github.com/OpenVoiceOS/padacioso/tree/V0.2.1a4) (2023-05-12) + +[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a3...V0.2.1a4) + ## [V0.2.1a3](https://github.com/OpenVoiceOS/padacioso/tree/V0.2.1a3) (2023-05-11) [Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a2...V0.2.1a3) From b1c3574d42d36c683e7764582ed851c97cd88a40 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 2 Jun 2023 22:44:04 +0100 Subject: [PATCH 14/28] remove spam LOG (#14) --- padacioso/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/padacioso/__init__.py b/padacioso/__init__.py index 6ba9d91..17bd25d 100644 --- a/padacioso/__init__.py +++ b/padacioso/__init__.py @@ -149,7 +149,6 @@ def calc_intents(self, query: str) -> Iterator[dict]: break if self.fuzz: - LOG.debug(f"Fallback to fuzzy match") penalty += 0.25 for f in self._get_fuzzed(r): entities = simplematch.match(f, query, From bfe0e3d91b6d2955b752b984069cfa7a38f2089c Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 2 Jun 2023 21:44:21 +0000 Subject: [PATCH 15/28] Increment Version to 0.2.1a6 --- padacioso/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padacioso/version.py b/padacioso/version.py index c9dfd4e..d2a44d9 100644 --- a/padacioso/version.py +++ b/padacioso/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 2 VERSION_BUILD = 1 -VERSION_ALPHA = 5 +VERSION_ALPHA = 6 # END_VERSION_BLOCK From ff3254725a310c889dc39f0a8955602b73c48042 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 2 Jun 2023 21:44:44 +0000 Subject: [PATCH 16/28] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1833b7..c375b88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.2.1a5](https://github.com/OpenVoiceOS/padacioso/tree/0.2.1a5) (2023-05-16) +## [0.2.1a6](https://github.com/OpenVoiceOS/padacioso/tree/0.2.1a6) (2023-06-02) -[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a4...0.2.1a5) +[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a5...0.2.1a6) + +**Fixed bugs:** + +- remove spam LOG [\#14](https://github.com/OpenVoiceOS/padacioso/pull/14) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.2.1a5](https://github.com/OpenVoiceOS/padacioso/tree/V0.2.1a5) (2023-05-16) + +[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a4...V0.2.1a5) **Implemented enhancements:** From 069fe2e2db2d0bdc135b4010e72842869d650f27 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 12 Jul 2023 13:11:33 +0100 Subject: [PATCH 17/28] feat/context + excluded keywords (#17) --- padacioso/__init__.py | 193 +++++++++++++++++++++++++++++------------- 1 file changed, 135 insertions(+), 58 deletions(-) diff --git a/padacioso/__init__.py b/padacioso/__init__.py index 17bd25d..5757031 100644 --- a/padacioso/__init__.py +++ b/padacioso/__init__.py @@ -1,22 +1,30 @@ +import concurrent.futures +from typing import List, Iterator, Optional + import simplematch -from typing import List, Iterator, Optional from padacioso.bracket_expansion import expand_parentheses, normalize_example try: from ovos_utils.log import LOG except ImportError: import logging + LOG = logging.getLogger('padacioso') class IntentContainer: - def __init__(self, fuzz=False): + def __init__(self, fuzz=False, n_workers=4): self.intent_samples, self.entity_samples = {}, {} # self.intents, self.entities = {}, {} self.fuzz = fuzz - self._cased_matchers = dict() - self._uncased_matchers = dict() + self.workers = n_workers + self._cased_matchers = {} + self._uncased_matchers = {} + self.available_contexts = {} + self.required_contexts = {} + self.excluded_keywords = {} + self.excluded_contexts = {} if "word" not in simplematch.types: LOG.debug(f"Registering `word` type") @@ -98,66 +106,97 @@ def remove_entity(self, name: str): if name in self.entity_samples: del self.entity_samples[name] + def _filter(self, query: str): + # filter intents based on context/excluded keywords + excluded_intents = [] + for intent_name, samples in self.excluded_keywords.items(): + if any(s in query for s in samples): + excluded_intents.append(intent_name) + for intent_name, contexts in self.required_contexts.items(): + if intent_name not in self.available_contexts: + excluded_intents.append(intent_name) + elif any(context not in self.available_contexts[intent_name] + for context in contexts): + excluded_intents.append(intent_name) + for intent_name, contexts in self.excluded_contexts.items(): + if intent_name not in self.available_contexts: + continue + if any(context in self.available_contexts[intent_name] + for context in contexts): + excluded_intents.append(intent_name) + return excluded_intents + + def _match(self, query, intent_name, regexes): + for r in regexes: + penalty = 0 + if "*" in r: + # penalize wildcards + penalty = 0.15 + if r not in self._cased_matchers: + LOG.warning(f"{r} not initialized") + self._cased_matchers[r] = \ + simplematch.Matcher(r, case_sensitive=True) + entities = self._cased_matchers[r].match(query) + if entities is not None: + for k, v in entities.items(): + if k not in self.entity_samples: + # penalize unregistered entities + penalty += 0.04 + elif str(v) not in self.entity_samples[k]: + # penalize parsed entity value not in samples + penalty += 0.1 + return {"entities": entities or {}, + "conf": 1 - penalty, + "name": intent_name} + + if r not in self._uncased_matchers: + LOG.warning(f"{r} not initialized") + self._uncased_matchers[r] = \ + simplematch.Matcher(r, case_sensitive=False) + entities = self._uncased_matchers[r].match(query) + if entities is not None: + # penalize case mismatch + penalty += 0.05 + for k, v in entities.items(): + if k not in self.entity_samples: + # penalize unregistered entities + penalty += 0.05 + elif str(v) not in self.entity_samples[k]: + # penalize parsed entity value not in samples + penalty += 0.1 + return {"entities": entities or {}, + "conf": 1 - penalty, + "name": intent_name} + + if self.fuzz: + penalty += 0.25 + + for s in self._get_fuzzed(r): + entities = simplematch.match(s, query, case_sensitive=False) + if entities is not None: + return {"entities": entities or {}, + "conf": 1 - penalty, + "name": intent_name} + def calc_intents(self, query: str) -> Iterator[dict]: """ Determine possible intents for a given query @param query: input to evaluate for an intent match @return: yields dict intent matches """ - for intent_name, regexes in self.intent_samples.items(): - for r in regexes: - penalty = 0 - if "*" in r: - # penalize wildcards - penalty = 0.15 - if r not in self._cased_matchers: - LOG.warning(f"{r} not initialized") - self._cased_matchers[r] = \ - simplematch.Matcher(r, case_sensitive=True) - entities = self._cased_matchers[r].match(query) - if entities is not None: - for k, v in entities.items(): - if k not in self.entity_samples: - # penalize unregistered entities - penalty += 0.04 - elif str(v) not in self.entity_samples[k]: - # penalize parsed entity value not in samples - penalty += 0.1 - yield {"entities": entities or {}, - "conf": 1 - penalty, - "name": intent_name} - break - - if r not in self._uncased_matchers: - LOG.warning(f"{r} not initialized") - self._uncased_matchers[r] = \ - simplematch.Matcher(r, case_sensitive=False) - entities = self._uncased_matchers[r].match(query) - if entities is not None: - # penalize case mismatch - penalty += 0.05 - for k, v in entities.items(): - if k not in self.entity_samples: - # penalize unregistered entities - penalty += 0.05 - elif str(v) not in self.entity_samples[k]: - # penalize parsed entity value not in samples - penalty += 0.1 - yield {"entities": entities or {}, - "conf": 1 - penalty, - "name": intent_name} - break - - if self.fuzz: - penalty += 0.25 - for f in self._get_fuzzed(r): - entities = simplematch.match(f, query, - case_sensitive=False) - if entities is not None: - yield {"entities": entities or {}, - "conf": 1 - penalty, - "name": intent_name} - break + # filter intents based on context/excluded keywords + excluded_intents = self._filter(query) + + # do the work in parallel instead of sequentially + with concurrent.futures.ProcessPoolExecutor(max_workers=self.workers) as executor: + future_to_source = { + executor.submit(self._match, query, intent_name, regexes): intent_name + for intent_name, regexes in self.intent_samples.items() if intent_name not in excluded_intents + } + for future in concurrent.futures.as_completed(future_to_source): + res = future.result() + if res is not None: + yield res def calc_intent(self, query: str) -> Optional[dict]: """ @@ -176,6 +215,44 @@ def calc_intent(self, query: str) -> Optional[dict]: LOG.debug(match) return match + def exclude_keywords(self, intent_name, samples): + if intent_name not in self.excluded_keywords: + self.excluded_keywords[intent_name] = samples + else: + self.excluded_keywords[intent_name] += samples + + def set_context(self, intent_name, context_name, context_val=None): + if intent_name not in self.available_contexts: + self.available_contexts[intent_name] = {} + self.available_contexts[intent_name][context_name] = context_val + + def exclude_context(self, intent_name, context_name): + if intent_name not in self.excluded_contexts: + self.excluded_contexts[intent_name] = [context_name] + else: + self.excluded_contexts[intent_name].append(context_name) + + def unexclude_context(self, intent_name, context_name): + if intent_name in self.excluded_contexts: + self.excluded_contexts[intent_name] = [c for c in self.excluded_contexts[intent_name] + if context_name != c] + + def unset_context(self, intent_name, context_name): + if intent_name in self.available_contexts: + if context_name in self.available_contexts[intent_name]: + self.available_contexts[intent_name].pop(context_name) + + def require_context(self, intent_name, context_name): + if intent_name not in self.required_contexts: + self.required_contexts[intent_name] = [context_name] + else: + self.required_contexts[intent_name].append(context_name) + + def unrequire_context(self, intent_name, context_name): + if intent_name in self.required_contexts: + self.required_contexts[intent_name] = [c for c in self.required_contexts[intent_name] + if context_name != c] + def _init_sm_word_type(): """ From eb28aeb6e581137b2cda28e4ef8fc64b9da3de1f Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 12 Jul 2023 12:11:52 +0000 Subject: [PATCH 18/28] Increment Version to 0.2.1a7 --- padacioso/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padacioso/version.py b/padacioso/version.py index d2a44d9..4f48be7 100644 --- a/padacioso/version.py +++ b/padacioso/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 2 VERSION_BUILD = 1 -VERSION_ALPHA = 6 +VERSION_ALPHA = 7 # END_VERSION_BLOCK From 63e4df5f9cf293b7b6d535ee77b2f5325c7a5b31 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 12 Jul 2023 12:12:24 +0000 Subject: [PATCH 19/28] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c375b88..97da653 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.2.1a6](https://github.com/OpenVoiceOS/padacioso/tree/0.2.1a6) (2023-06-02) +## [0.2.1a7](https://github.com/OpenVoiceOS/padacioso/tree/0.2.1a7) (2023-07-12) -[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a5...0.2.1a6) +[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a6...0.2.1a7) + +**Implemented enhancements:** + +- feat/context + excluded keywords [\#17](https://github.com/OpenVoiceOS/padacioso/pull/17) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.2.1a6](https://github.com/OpenVoiceOS/padacioso/tree/V0.2.1a6) (2023-06-02) + +[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a5...V0.2.1a6) **Fixed bugs:** From 0b74a74914404f9161a1d66ba6a7a9b64648453a Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 12 Jul 2023 14:09:50 +0100 Subject: [PATCH 20/28] feat/disambiguation (#18) --- padacioso/__init__.py | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/padacioso/__init__.py b/padacioso/__init__.py index 5757031..8865b89 100644 --- a/padacioso/__init__.py +++ b/padacioso/__init__.py @@ -173,9 +173,19 @@ def _match(self, query, intent_name, regexes): for s in self._get_fuzzed(r): entities = simplematch.match(s, query, case_sensitive=False) + fuzzy_penalty = penalty + if "*" in s: # very loose regex + fuzzy_penalty += 0.1 + if "{" in s: # capture group + fuzzy_penalty += 0.05 + + # depending on length + diff = max(len(s) - len(query), 0) + fuzzy_penalty += diff * 0.01 + if entities is not None: return {"entities": entities or {}, - "conf": 1 - penalty, + "conf": max(1 - fuzzy_penalty, 0), "name": intent_name} def calc_intents(self, query: str) -> Iterator[dict]: @@ -204,11 +214,29 @@ def calc_intent(self, query: str) -> Optional[dict]: @param query: input to evaluate for an intent @return: dict matched intent (or None) """ - match = max( - self.calc_intents(query), - key=lambda x: x["conf"], - default={'name': None, 'entities': {}} - ) + match = {'name': None, 'entities': {}} + intents = [i for i in self.calc_intents(query) if i is not None and i.get("name")] + if len(intents) == 0: + LOG.info("No match") + return match + + best_conf = max(x.get("conf", 0) for x in intents if x.get("name")) + ties = [i for i in intents if i.get("conf", 0) == best_conf] + + if len(ties) > 1: + LOG.debug(f"tied intents: {ties}") + no_entities = [i for i in intents if not i.get("entities")] + entities = [i for i in intents if i.get("entities")] + if entities and no_entities: + LOG.debug(f"excluding {entities}") + ties = no_entities # prefer more strict regexes + + if len(ties) > 1: + # TODO - how to untie? + LOG.info(f"tied intents: {ties}") + + match = ties[0] + for entity in set(match['entities'].keys()): entities = match['entities'].pop(entity) match['entities'][entity.lower()] = entities From a7b532ffdc9a0968da68d66c4b0d95f9f53cbb3d Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 12 Jul 2023 13:10:08 +0000 Subject: [PATCH 21/28] Increment Version to 0.2.1a8 --- padacioso/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padacioso/version.py b/padacioso/version.py index 4f48be7..98ba501 100644 --- a/padacioso/version.py +++ b/padacioso/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 2 VERSION_BUILD = 1 -VERSION_ALPHA = 7 +VERSION_ALPHA = 8 # END_VERSION_BLOCK From b5593bd133a5235d785845bb4f9e44652b4c56d7 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 12 Jul 2023 13:10:35 +0000 Subject: [PATCH 22/28] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97da653..eb3ece7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.2.1a7](https://github.com/OpenVoiceOS/padacioso/tree/0.2.1a7) (2023-07-12) +## [0.2.1a8](https://github.com/OpenVoiceOS/padacioso/tree/0.2.1a8) (2023-07-12) -[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a6...0.2.1a7) +[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a7...0.2.1a8) + +**Implemented enhancements:** + +- feat/disambiguation [\#18](https://github.com/OpenVoiceOS/padacioso/pull/18) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.2.1a7](https://github.com/OpenVoiceOS/padacioso/tree/V0.2.1a7) (2023-07-12) + +[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a6...V0.2.1a7) **Implemented enhancements:** From 9ff79557a31db79f5afee974a79898f5da57cbf3 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Thu, 13 Jul 2023 22:21:36 +0100 Subject: [PATCH 23/28] fix/fuzzy_match scores (#19) --- padacioso/__init__.py | 64 +++++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/padacioso/__init__.py b/padacioso/__init__.py index 8865b89..82dbd6a 100644 --- a/padacioso/__init__.py +++ b/padacioso/__init__.py @@ -7,11 +7,23 @@ try: from ovos_utils.log import LOG + from ovos_utils.parse import fuzzy_match # uses rapidfuzz for performance except ImportError: import logging LOG = logging.getLogger('padacioso') + from difflib import SequenceMatcher + + + def fuzzy_match(x, against): + """Perform a 'fuzzy' comparison between two strings. + Returns: + float: match percentage -- 1.0 for perfect match, + down to 0.0 for no match at all. + """ + return SequenceMatcher(None, x, against).ratio() + class IntentContainer: def __init__(self, fuzz=False, n_workers=4): @@ -168,25 +180,33 @@ def _match(self, query, intent_name, regexes): "conf": 1 - penalty, "name": intent_name} - if self.fuzz: - penalty += 0.25 - + if self.fuzz: + for r in regexes: + penalty = 0.25 for s in self._get_fuzzed(r): - entities = simplematch.match(s, query, case_sensitive=False) - fuzzy_penalty = penalty - if "*" in s: # very loose regex - fuzzy_penalty += 0.1 - if "{" in s: # capture group - fuzzy_penalty += 0.05 - - # depending on length - diff = max(len(s) - len(query), 0) - fuzzy_penalty += diff * 0.01 - - if entities is not None: - return {"entities": entities or {}, - "conf": max(1 - fuzzy_penalty, 0), - "name": intent_name} + entities = self._fuzzy_score(query, s, penalty) + if entities: + entities["name"] = intent_name + return entities + + def _fuzzy_score(self, query, s, penalty=0.25): + entities = simplematch.match(s, query, case_sensitive=False) + + fuzzy_penalty = penalty + if "*" in s: # very loose regex + fuzzy_penalty += 0.1 + if "{" in s: # capture group + fuzzy_penalty += 0.05 + # depending on length + diff = max(len(s) - len(query), 0) + fuzzy_penalty += diff * 0.01 + base_score = 1 - max(1 - fuzzy_penalty, 0) + fuzzy_score = fuzzy_match(s, query) + score = (fuzzy_score + base_score) / 2 + + if entities is not None: + return {"entities": entities or {}, + "conf": (fuzzy_score + base_score) / 2} def calc_intents(self, query: str) -> Iterator[dict]: """ @@ -223,14 +243,6 @@ def calc_intent(self, query: str) -> Optional[dict]: best_conf = max(x.get("conf", 0) for x in intents if x.get("name")) ties = [i for i in intents if i.get("conf", 0) == best_conf] - if len(ties) > 1: - LOG.debug(f"tied intents: {ties}") - no_entities = [i for i in intents if not i.get("entities")] - entities = [i for i in intents if i.get("entities")] - if entities and no_entities: - LOG.debug(f"excluding {entities}") - ties = no_entities # prefer more strict regexes - if len(ties) > 1: # TODO - how to untie? LOG.info(f"tied intents: {ties}") From e6237a7873f4d4de3c42df013551e21896021ec9 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 13 Jul 2023 21:21:52 +0000 Subject: [PATCH 24/28] Increment Version to 0.2.1a9 --- padacioso/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padacioso/version.py b/padacioso/version.py index 98ba501..ff84460 100644 --- a/padacioso/version.py +++ b/padacioso/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 2 VERSION_BUILD = 1 -VERSION_ALPHA = 8 +VERSION_ALPHA = 9 # END_VERSION_BLOCK From 31567dcca732943ef44d997fb1cb32bda56a5210 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 13 Jul 2023 21:22:18 +0000 Subject: [PATCH 25/28] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb3ece7..2c3cdea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.2.1a8](https://github.com/OpenVoiceOS/padacioso/tree/0.2.1a8) (2023-07-12) +## [0.2.1a9](https://github.com/OpenVoiceOS/padacioso/tree/0.2.1a9) (2023-07-13) -[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a7...0.2.1a8) +[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a8...0.2.1a9) + +**Fixed bugs:** + +- fix/fuzzy\_match scores [\#19](https://github.com/OpenVoiceOS/padacioso/pull/19) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.2.1a8](https://github.com/OpenVoiceOS/padacioso/tree/V0.2.1a8) (2023-07-12) + +[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a7...V0.2.1a8) **Implemented enhancements:** From 6be3e2d2a48500d66044dd1c03b6e6d890025fe9 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Thu, 13 Jul 2023 22:42:42 +0100 Subject: [PATCH 26/28] Update notify_matrix.yml --- .github/workflows/notify_matrix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/notify_matrix.yml b/.github/workflows/notify_matrix.yml index 0aa64a0..9e90164 100644 --- a/.github/workflows/notify_matrix.yml +++ b/.github/workflows/notify_matrix.yml @@ -20,4 +20,4 @@ jobs: token: ${{ secrets.MATRIX_TOKEN }} channel: '!WjxEKjjINpyBRPFgxl:krbel.duckdns.org' message: | - new padacioso PR merged! https://github.com/OpenVoiceOS/ovos_utils/pull/${{ github.event.number }} + new padacioso PR merged! https://github.com/OpenVoiceOS/padacioso/pull/${{ github.event.number }} From ee6f929b4292ac30722003f9a9a4d9b4c0635fa0 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 29 Dec 2023 03:31:33 +0000 Subject: [PATCH 27/28] Increment Version to 0.2.1 --- padacioso/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/padacioso/version.py b/padacioso/version.py index ff84460..40001a7 100644 --- a/padacioso/version.py +++ b/padacioso/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 2 VERSION_BUILD = 1 -VERSION_ALPHA = 9 +VERSION_ALPHA = 0 # END_VERSION_BLOCK From 8d342ee8abac09bcf33b51f6f578afde87694924 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 29 Dec 2023 03:31:58 +0000 Subject: [PATCH 28/28] Update Changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c3cdea..f8736f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [0.2.1a9](https://github.com/OpenVoiceOS/padacioso/tree/0.2.1a9) (2023-07-13) +## [V0.2.1a9](https://github.com/OpenVoiceOS/padacioso/tree/V0.2.1a9) (2023-07-13) -[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a8...0.2.1a9) +[Full Changelog](https://github.com/OpenVoiceOS/padacioso/compare/V0.2.1a8...V0.2.1a9) **Fixed bugs:**