stark-engine 4.3.0__tar.gz → 4.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {stark_engine-4.3.0 → stark_engine-4.4.0}/PKG-INFO +3 -2
- {stark_engine-4.3.0 → stark_engine-4.4.0}/pyproject.toml +4 -2
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/core/command.py +16 -23
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/core/commands_context.py +9 -3
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/core/commands_context_processor.py +6 -2
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/core/commands_manager.py +8 -3
- stark_engine-4.4.0/stark/core/health_check.py +70 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/core/parsing.py +118 -25
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/core/patterns/pattern.py +0 -1
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/core/processors/search_processor.py +21 -7
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/core/processors/spacy_ner_processor.py +16 -5
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/core/types/object.py +7 -2
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/core/types/slots.py +22 -2
- stark_engine-4.4.0/stark/core/types/string.py +9 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/core/types/word.py +4 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/general/json_encoder.py +1 -1
- stark_engine-4.4.0/stark/general/localisation/__init__.py +5 -0
- stark_engine-4.4.0/stark/general/localisation/language_code.py +13 -0
- stark_engine-4.4.0/stark/general/localisation/locale_string.py +159 -0
- stark_engine-4.4.0/stark/general/localisation/localizable_string.py +40 -0
- stark_engine-4.4.0/stark/general/localisation/localizer.py +105 -0
- stark_engine-4.4.0/stark/general/localisation/strings.py +45 -0
- stark_engine-4.3.0/stark/core/health_check.py +0 -51
- stark_engine-4.3.0/stark/core/types/string.py +0 -5
- {stark_engine-4.3.0 → stark_engine-4.4.0}/LICENSE.md +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/README.md +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/__init__.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/core/__init__.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/core/patterns/__init__.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/core/patterns/rules.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/core/processors/__init__.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/core/types/__init__.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/core/types/location.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/general/blockage_detector.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/general/cache.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/general/classproperty.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/general/dependencies.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/interfaces/gcloud.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/interfaces/protocols.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/interfaces/silero.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/interfaces/vosk.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/common/span.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/dictionary/!examples.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/dictionary/__init__.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/dictionary/dictionary.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/dictionary/models.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/dictionary/nl_dictionary_name.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/dictionary/storage/__init__.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/dictionary/storage/storage_memory.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/dictionary/storage/storage_sqlite.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/levenshtein/__init__.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/levenshtein/levenshtein.pyi +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/levenshtein/levenshtein.pyx +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/phonetic/simplephone.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/phonetic/transcription/__init__.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/phonetic/transcription/epitran.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/phonetic/transcription/espeak.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/phonetic/transcription/ipa2lat.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/phonetic/transcription/protocol.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/sliding_window_parser.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/tools/strtools.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/voice_assistant/__init__.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/voice_assistant/mode.py +0 -0
- {stark_engine-4.3.0 → stark_engine-4.4.0}/stark/voice_assistant/voice_assistant.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stark-engine
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.4.0
|
|
4
4
|
Summary: S.T.A.R.K - Speech and Text Algorithmic Recognition Kit. Modern framework for creating powerfull voice assistants.
|
|
5
5
|
License: CC BY-NC-SA 4.0
|
|
6
6
|
License-File: LICENSE.md
|
|
@@ -19,7 +19,8 @@ Provides-Extra: silero
|
|
|
19
19
|
Provides-Extra: sound
|
|
20
20
|
Provides-Extra: spacy
|
|
21
21
|
Provides-Extra: vosk
|
|
22
|
-
Requires-Dist:
|
|
22
|
+
Requires-Dist: anyio (>=4.0.0,<5.0.0)
|
|
23
|
+
Requires-Dist: asyncer (>=0.0.8,<1.0.0)
|
|
23
24
|
Requires-Dist: google-cloud-texttospeech (>=2.14.1,<3.0.0) ; extra == "gcloud" or extra == "all"
|
|
24
25
|
Requires-Dist: numpy (>=2.3.3,<3.0.0) ; extra == "silero" or extra == "all"
|
|
25
26
|
Requires-Dist: pydantic (>=2.0.0,<3.0.0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "stark-engine"
|
|
3
|
-
version = "4.
|
|
3
|
+
version = "4.4.0"
|
|
4
4
|
description = "S.T.A.R.K - Speech and Text Algorithmic Recognition Kit. Modern framework for creating powerfull voice assistants."
|
|
5
5
|
authors = ["MarkParker5 <mark@parker-programs.com>"]
|
|
6
6
|
license = "CC BY-NC-SA 4.0"
|
|
@@ -39,7 +39,8 @@ keywords = [
|
|
|
39
39
|
[tool.poetry.dependencies]
|
|
40
40
|
python = ">=3.12,<3.15"
|
|
41
41
|
pydantic = "^2.0.0"
|
|
42
|
-
asyncer = "
|
|
42
|
+
asyncer = ">=0.0.8,<1.0.0"
|
|
43
|
+
anyio = ">=4.0.0,<5.0.0"
|
|
43
44
|
numpy = "^2.3.3"
|
|
44
45
|
|
|
45
46
|
# Optional dependencies
|
|
@@ -85,6 +86,7 @@ coverage = "^7.11.0"
|
|
|
85
86
|
faker = "^37.11.0"
|
|
86
87
|
pympler = "^1.1"
|
|
87
88
|
psutil = "^7.1.1"
|
|
89
|
+
trio = "^0.33.0"
|
|
88
90
|
|
|
89
91
|
[build-system]
|
|
90
92
|
requires = ["poetry-core"]
|
|
@@ -29,12 +29,11 @@ import asyncer
|
|
|
29
29
|
from pydantic import BaseModel, Field
|
|
30
30
|
|
|
31
31
|
from ..general.classproperty import classproperty
|
|
32
|
+
from ..general.localisation import LanguageCode, LocalizableString
|
|
32
33
|
from .patterns import Pattern
|
|
33
34
|
|
|
34
35
|
ResponseOptions = (
|
|
35
|
-
Optional["Response"]
|
|
36
|
-
| Generator[Optional["Response"], None, None]
|
|
37
|
-
| AsyncGenerator[Optional["Response"], None]
|
|
36
|
+
Optional["Response"] | Generator[Optional["Response"], None, None] | AsyncGenerator[Optional["Response"], None]
|
|
38
37
|
)
|
|
39
38
|
AwaitResponse = Awaitable[ResponseOptions]
|
|
40
39
|
AsyncCommandRunner = Callable[..., AwaitResponse]
|
|
@@ -44,16 +43,21 @@ CommandRunner = TypeVar("CommandRunner", bound=SyncCommandRunner | AsyncCommandR
|
|
|
44
43
|
|
|
45
44
|
class Command(Generic[CommandRunner]):
|
|
46
45
|
name: str
|
|
47
|
-
|
|
46
|
+
patterns: dict[str, Pattern]
|
|
48
47
|
_runner: CommandRunner
|
|
49
48
|
|
|
50
|
-
def __init__(self, name: str,
|
|
51
|
-
assert isinstance(
|
|
49
|
+
def __init__(self, name: str, patterns: dict[str, Pattern], runner: CommandRunner):
|
|
50
|
+
assert patterns and all(isinstance(p, Pattern) for p in patterns.values())
|
|
52
51
|
self.name = name
|
|
53
|
-
self.
|
|
52
|
+
self.patterns = patterns
|
|
54
53
|
self._runner = runner
|
|
55
54
|
update_wrapper(self, runner)
|
|
56
55
|
|
|
56
|
+
def get_pattern(self, language_code: LanguageCode) -> Pattern:
|
|
57
|
+
if language_code in self.patterns:
|
|
58
|
+
return self.patterns[language_code]
|
|
59
|
+
return self.patterns["base"]
|
|
60
|
+
|
|
57
61
|
def run(
|
|
58
62
|
self,
|
|
59
63
|
parameters_dict: dict[str, Any] | None = None,
|
|
@@ -74,9 +78,7 @@ class Command(Generic[CommandRunner]):
|
|
|
74
78
|
|
|
75
79
|
runner: AsyncCommandRunner
|
|
76
80
|
|
|
77
|
-
if inspect.iscoroutinefunction(self._runner) or inspect.isasyncgen(
|
|
78
|
-
self._runner
|
|
79
|
-
):
|
|
81
|
+
if inspect.iscoroutinefunction(self._runner) or inspect.isasyncgen(self._runner):
|
|
80
82
|
# async functions (coroutines) and async generators are remain as is
|
|
81
83
|
runner = cast(AsyncCommandRunner, self._runner)
|
|
82
84
|
else:
|
|
@@ -84,21 +86,12 @@ class Command(Generic[CommandRunner]):
|
|
|
84
86
|
# async generators are not supported yet by asyncer.asyncify (https://github.com/tiangolo/asyncer/discussions/86)
|
|
85
87
|
runner = asyncer.asyncify(cast(SyncCommandRunner, self._runner))
|
|
86
88
|
|
|
87
|
-
if any(
|
|
88
|
-
p.kind == p.VAR_KEYWORD
|
|
89
|
-
for p in inspect.signature(self._runner).parameters.values()
|
|
90
|
-
):
|
|
89
|
+
if any(p.kind == p.VAR_KEYWORD for p in inspect.signature(self._runner).parameters.values()):
|
|
91
90
|
# if command runner accepts **kwargs, pass all parameters
|
|
92
91
|
coroutine = runner(**parameters)
|
|
93
92
|
else:
|
|
94
93
|
# otherwise pass only parameters that are in command runner signature to prevent TypeError: got an unexpected keyword argument
|
|
95
|
-
coroutine = runner(
|
|
96
|
-
**{
|
|
97
|
-
k: v
|
|
98
|
-
for k, v in parameters.items()
|
|
99
|
-
if k in self._runner.__code__.co_varnames
|
|
100
|
-
}
|
|
101
|
-
)
|
|
94
|
+
coroutine = runner(**{k: v for k, v in parameters.items() if k in self._runner.__code__.co_varnames})
|
|
102
95
|
|
|
103
96
|
@wraps(runner)
|
|
104
97
|
async def coroutine_wrapper() -> ResponseOptions:
|
|
@@ -139,8 +132,8 @@ class ResponseStatus(Enum):
|
|
|
139
132
|
|
|
140
133
|
|
|
141
134
|
class Response(BaseModel):
|
|
142
|
-
voice: str = ""
|
|
143
|
-
text: str = ""
|
|
135
|
+
voice: str | LocalizableString = ""
|
|
136
|
+
text: str | LocalizableString = ""
|
|
144
137
|
status: ResponseStatus = ResponseStatus.success
|
|
145
138
|
needs_user_input: bool = False
|
|
146
139
|
commands: list[Command] = []
|
|
@@ -15,6 +15,7 @@ from stark.core.commands_context_processor import (
|
|
|
15
15
|
)
|
|
16
16
|
from stark.core.parsing import PatternParser
|
|
17
17
|
from stark.core.types.object import Object
|
|
18
|
+
from stark.general.localisation import LocaleString, Localizer
|
|
18
19
|
|
|
19
20
|
from ..general.dependencies import DependencyManager, default_dependency_manager
|
|
20
21
|
from .command import (
|
|
@@ -57,11 +58,12 @@ class CommandsContext:
|
|
|
57
58
|
commands_manager: CommandsManager,
|
|
58
59
|
dependency_manager: DependencyManager = default_dependency_manager,
|
|
59
60
|
processors: list[Any] | None = None,
|
|
61
|
+
localizer: Localizer | None = None,
|
|
60
62
|
):
|
|
61
63
|
assert isinstance(task_group, TaskGroup), task_group
|
|
62
64
|
assert isinstance(commands_manager, CommandsManager)
|
|
63
65
|
assert isinstance(dependency_manager, DependencyManager)
|
|
64
|
-
self.pattern_parser = PatternParser()
|
|
66
|
+
self.pattern_parser = PatternParser(localizer=localizer)
|
|
65
67
|
self.commands_manager = commands_manager
|
|
66
68
|
self.context_queue = [self.root_context]
|
|
67
69
|
self._response_queue = []
|
|
@@ -88,7 +90,9 @@ class CommandsContext:
|
|
|
88
90
|
def root_context(self):
|
|
89
91
|
return CommandsContextLayer(self.commands_manager.commands, {})
|
|
90
92
|
|
|
91
|
-
async def process_string(self, string: str):
|
|
93
|
+
async def process_string(self, string: str | LocaleString):
|
|
94
|
+
string = LocaleString(string) if not isinstance(string, LocaleString) else string
|
|
95
|
+
|
|
92
96
|
if not self.context_queue:
|
|
93
97
|
self.context_queue.append(self.root_context)
|
|
94
98
|
|
|
@@ -100,7 +104,9 @@ class CommandsContext:
|
|
|
100
104
|
|
|
101
105
|
for processor in self.processors:
|
|
102
106
|
logger.debug(f"Processing context {processor=} with {string=} {recognized_entities=} {self.context_queue=}")
|
|
103
|
-
search_results, context_pops = await processor.process_string(
|
|
107
|
+
search_results, context_pops = await processor.process_string(
|
|
108
|
+
string, self, recognized_entities
|
|
109
|
+
)
|
|
104
110
|
if search_results:
|
|
105
111
|
# Pop contexts as directed by processor
|
|
106
112
|
for _ in range(context_pops):
|
|
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
|
|
|
8
8
|
from stark.core.command import Command
|
|
9
9
|
from stark.core.parsing import RecognizedEntity
|
|
10
10
|
from stark.core.types.object import Object
|
|
11
|
+
from stark.general.localisation import LocaleString
|
|
11
12
|
|
|
12
13
|
from .commands_manager import SearchResult
|
|
13
14
|
|
|
@@ -42,7 +43,10 @@ class CommandsContextProcessor(ABC):
|
|
|
42
43
|
"""
|
|
43
44
|
|
|
44
45
|
async def process_string(
|
|
45
|
-
self,
|
|
46
|
+
self,
|
|
47
|
+
string: LocaleString,
|
|
48
|
+
context: CommandsContext,
|
|
49
|
+
recognized_entities: list[RecognizedEntity],
|
|
46
50
|
) -> tuple[list[SearchResult], int]:
|
|
47
51
|
"""
|
|
48
52
|
Processes the entire context queue.
|
|
@@ -72,7 +76,7 @@ class CommandsContextProcessor(ABC):
|
|
|
72
76
|
|
|
73
77
|
async def process_context_layer(
|
|
74
78
|
self,
|
|
75
|
-
string:
|
|
79
|
+
string: LocaleString,
|
|
76
80
|
context: CommandsContext,
|
|
77
81
|
context_layer: CommandsContextLayer,
|
|
78
82
|
recognized_entities: list[RecognizedEntity],
|
|
@@ -31,9 +31,14 @@ class CommandsManager:
|
|
|
31
31
|
return command
|
|
32
32
|
return None
|
|
33
33
|
|
|
34
|
-
def new(self, pattern_str: str, hidden: bool = False):
|
|
34
|
+
def new(self, pattern_str: str | dict[str, str], hidden: bool = False):
|
|
35
35
|
def creator(runner: CommandRunner) -> Command:
|
|
36
|
-
|
|
36
|
+
if isinstance(pattern_str, dict):
|
|
37
|
+
patterns = {lang: Pattern(p) for lang, p in pattern_str.items()}
|
|
38
|
+
if 'base' not in patterns:
|
|
39
|
+
patterns['base'] = next(iter(patterns.values()))
|
|
40
|
+
else:
|
|
41
|
+
patterns = {'base': Pattern(pattern_str)}
|
|
37
42
|
|
|
38
43
|
# take the main type from Optionals
|
|
39
44
|
annotations = dict()
|
|
@@ -60,7 +65,7 @@ class CommandsManager:
|
|
|
60
65
|
|
|
61
66
|
# create command
|
|
62
67
|
|
|
63
|
-
cmd = Command(f"{self.name}.{runner.__name__}",
|
|
68
|
+
cmd = Command(f"{self.name}.{runner.__name__}", patterns, runner)
|
|
64
69
|
|
|
65
70
|
if not hidden:
|
|
66
71
|
self.commands.append(cmd)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
from stark.core.command import Command
|
|
4
|
+
from stark.core.parsing import PatternParser, _LOCALIZER_KEY_REGEX
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def health_check(pattern_parser: PatternParser, commands: list[Command]) -> None:
|
|
8
|
+
# TODO: update schema and validation; handle optional parameters; handle short form where type is defined in object
|
|
9
|
+
|
|
10
|
+
# 1. Registered types: pattern parameters and annotations match; all used types are registered
|
|
11
|
+
for reg_type in pattern_parser.parameter_types_by_name.values():
|
|
12
|
+
for lang, type_pattern in reg_type.type.patterns.items():
|
|
13
|
+
pattern_params = set(type_pattern.parameters.keys())
|
|
14
|
+
class_params = set(reg_type.type.__annotations__.keys())
|
|
15
|
+
assert pattern_params <= class_params, (
|
|
16
|
+
f"Pattern parameters {pattern_params} do not match class annotations {class_params} for {reg_type.type.__name__} (language '{lang}')"
|
|
17
|
+
)
|
|
18
|
+
# 2. Registered types: All referenced parameter types are registered
|
|
19
|
+
for param_name in pattern_params:
|
|
20
|
+
assert type_pattern.parameters[param_name].type_name in pattern_parser.parameter_types_by_name, (
|
|
21
|
+
f"Unknown parameter type '{param_name}' in pattern '{type_pattern}' of type '{reg_type.type.__name__}' (language '{lang}'). Known types: {sorted(pattern_parser.parameter_types_by_name.keys())}"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# 3. No duplicate parameter types registered
|
|
25
|
+
type_names = list(pattern_parser.parameter_types_by_name.keys())
|
|
26
|
+
if len(type_names) != len(set(type_names)):
|
|
27
|
+
duplicates = {name for name in type_names if type_names.count(name) > 1}
|
|
28
|
+
assert not duplicates, f"Duplicate parameter type names found: {sorted(duplicates)}"
|
|
29
|
+
|
|
30
|
+
# 4. Commands: validate @key references and parameter types
|
|
31
|
+
for command in commands:
|
|
32
|
+
for lang, cmd_pattern in command.patterns.items():
|
|
33
|
+
if pattern_parser.localizer and pattern_parser._has_localizer_keys(cmd_pattern):
|
|
34
|
+
for key in _LOCALIZER_KEY_REGEX.findall(cmd_pattern._origin):
|
|
35
|
+
pattern_parser.localizer.verify_key_exists(key)
|
|
36
|
+
for param in command.get_pattern("base").parameters.values():
|
|
37
|
+
assert param.type_name in pattern_parser.parameter_types_by_name, (
|
|
38
|
+
f"Unknown parameter type '{param.type_name}' in pattern '{command.get_pattern("base")}' of command '{command.name}'. Known types: {sorted(pattern_parser.parameter_types_by_name.keys())}"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# 5. Commands: all pattern params are expected by the runner func
|
|
42
|
+
for command in commands:
|
|
43
|
+
pattern_params = set(command.get_pattern("base").parameters.keys())
|
|
44
|
+
runner_params = set(command._runner.__annotations__.keys())
|
|
45
|
+
assert pattern_params <= runner_params, f"Command '{command.name}' function missing parameters: {pattern_params - runner_params}"
|
|
46
|
+
|
|
47
|
+
# 6. Pattern syntax validity (TODO)
|
|
48
|
+
# for command in commands:
|
|
49
|
+
# assert command.get_pattern("base").is_valid(), f"Invalid pattern syntax: {command.get_pattern("base").origin}"
|
|
50
|
+
|
|
51
|
+
# 7. Try compiling all patterns to catch syntax errors early
|
|
52
|
+
for reg_type in pattern_parser.parameter_types_by_name.values():
|
|
53
|
+
for lang, p in reg_type.type.patterns.items():
|
|
54
|
+
try:
|
|
55
|
+
pattern_parser._compile_pattern(p, language_code=lang)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
warnings.warn(f"Failed to compile pattern '{p}' of type '{reg_type.type.__name__}' (language '{lang}'): {e}")
|
|
58
|
+
for command in commands:
|
|
59
|
+
for lang, p in command.patterns.items():
|
|
60
|
+
try:
|
|
61
|
+
pattern_parser._compile_pattern(p, language_code=lang)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
warnings.warn(f"Failed to compile pattern '{p}' of command '{command.name}' (language '{lang}'): {e}")
|
|
64
|
+
|
|
65
|
+
# 8. No unused types
|
|
66
|
+
used_types = {param.type_name for command in commands for param in command.get_pattern("base").parameters.values()}
|
|
67
|
+
for type_name in pattern_parser.parameter_types_by_name:
|
|
68
|
+
# assert type_name in used_types, f"Registered type {type_name} is not used in any pattern"
|
|
69
|
+
if type_name not in used_types:
|
|
70
|
+
warnings.warn(f"Registered type {type_name} is not used in any pattern")
|
|
@@ -11,6 +11,7 @@ from stark.core.types import Object
|
|
|
11
11
|
from stark.core.types.string import String
|
|
12
12
|
from stark.core.types.word import Word
|
|
13
13
|
from stark.general.cache import alru_cache
|
|
14
|
+
from stark.general.localisation import LanguageCode, LocaleString, Localizer
|
|
14
15
|
|
|
15
16
|
type ObjectType = type[Object]
|
|
16
17
|
|
|
@@ -19,6 +20,8 @@ import logging
|
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
21
22
|
|
|
23
|
+
_LOCALIZER_KEY_REGEX = re.compile(r"@(?P<key>[A-Za-z_][A-Za-z0-9_]*)")
|
|
24
|
+
|
|
22
25
|
|
|
23
26
|
class ParseError(Exception):
|
|
24
27
|
pass
|
|
@@ -62,7 +65,13 @@ class RecognizedEntity:
|
|
|
62
65
|
|
|
63
66
|
|
|
64
67
|
class ObjectParser(ABC):
|
|
65
|
-
|
|
68
|
+
localizer: Localizer | None = None
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def patterns(self) -> dict[str, Pattern] | None:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
async def did_parse(self, obj: Object, from_string: LocaleString) -> str:
|
|
66
75
|
"""
|
|
67
76
|
This method is called after parsing from string and setting parameters found in pattern.
|
|
68
77
|
You will very rarely, if ever, need to call this method directly.
|
|
@@ -80,9 +89,11 @@ class ObjectParser(ABC):
|
|
|
80
89
|
|
|
81
90
|
class PatternParser:
|
|
82
91
|
parameter_types_by_name: dict[str, RegisteredParameterType] = {}
|
|
92
|
+
localizer: Localizer | None
|
|
83
93
|
|
|
84
|
-
def __init__(self):
|
|
94
|
+
def __init__(self, localizer: Localizer | None = None):
|
|
85
95
|
self.parameter_types_by_name = {}
|
|
96
|
+
self.localizer = localizer
|
|
86
97
|
self.register_parameter_type(Word)
|
|
87
98
|
self.register_parameter_type(String)
|
|
88
99
|
|
|
@@ -92,35 +103,70 @@ class PatternParser:
|
|
|
92
103
|
parser: ObjectParser | None = None,
|
|
93
104
|
# TODO: consider adding lang parameter
|
|
94
105
|
):
|
|
95
|
-
assert issubclass(object_type, Object),
|
|
106
|
+
assert issubclass(object_type, Object), (
|
|
107
|
+
f'Can`t add parameter type "{object_type.__name__}": it is not a subclass of Object'
|
|
108
|
+
)
|
|
96
109
|
exists_type = self.parameter_types_by_name.get(object_type.__name__)
|
|
97
|
-
assert exists_type is None,
|
|
110
|
+
assert exists_type is None, (
|
|
111
|
+
f"Duplicate parameter type: can`t add parameter type '{object_type.__name__}' because it already exists"
|
|
112
|
+
)
|
|
113
|
+
resolved_parser = parser or ObjectParser()
|
|
114
|
+
resolved_parser.localizer = self.localizer
|
|
98
115
|
self.parameter_types_by_name[object_type.__name__] = RegisteredParameterType(
|
|
99
|
-
name=object_type.__name__, type=object_type, parser=
|
|
116
|
+
name=object_type.__name__, type=object_type, parser=resolved_parser
|
|
100
117
|
)
|
|
118
|
+
self._validate_localizer_keys(object_type)
|
|
119
|
+
|
|
120
|
+
def _validate_localizer_keys(self, object_type: ObjectType):
|
|
121
|
+
for pattern in object_type.patterns.values():
|
|
122
|
+
keys = _LOCALIZER_KEY_REGEX.findall(pattern._origin)
|
|
123
|
+
if not keys:
|
|
124
|
+
continue
|
|
125
|
+
if not self.localizer:
|
|
126
|
+
raise ValueError(
|
|
127
|
+
f"Pattern '{pattern._origin}' of type '{object_type.__name__}' contains @key references but no Localizer is configured on PatternParser"
|
|
128
|
+
)
|
|
129
|
+
for key in keys:
|
|
130
|
+
self.localizer.verify_key_exists(key)
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def _has_localizer_keys(pattern: Pattern) -> bool:
|
|
134
|
+
return bool(_LOCALIZER_KEY_REGEX.search(pattern._origin))
|
|
101
135
|
|
|
102
|
-
async def parse_object_name(self, class_name: str, from_string: str) -> ParseResult:
|
|
136
|
+
async def parse_object_name(self, class_name: str, from_string: str | LocaleString) -> ParseResult:
|
|
103
137
|
return await self.parse_object(self.parameter_types_by_name[class_name].type, from_string)
|
|
104
138
|
|
|
105
|
-
async def parse_object(self, object_type: ObjectType, from_string: str) -> ParseResult:
|
|
139
|
+
async def parse_object(self, object_type: ObjectType, from_string: str | LocaleString) -> ParseResult:
|
|
106
140
|
async for obj in self.parse_objects(object_type, from_string):
|
|
107
141
|
return obj # take the first successful parsed result, usually the only one
|
|
108
142
|
else:
|
|
109
143
|
raise ParseError(f"Failed to parse object of type {object_type.__name__} from string '{from_string}'")
|
|
110
144
|
|
|
111
|
-
|
|
112
|
-
|
|
145
|
+
def _resolve_pattern(self, object_type: ObjectType, language_code: LanguageCode) -> Pattern:
|
|
146
|
+
parser = self.parameter_types_by_name[object_type.__name__].parser
|
|
147
|
+
patterns = parser.patterns or object_type.patterns
|
|
148
|
+
if language_code in patterns:
|
|
149
|
+
return patterns[language_code]
|
|
150
|
+
return patterns["base"]
|
|
151
|
+
|
|
152
|
+
async def parse_objects(
|
|
153
|
+
self, object_type: ObjectType, from_string: str | LocaleString
|
|
154
|
+
) -> AsyncGenerator[ParseResult]:
|
|
155
|
+
from_string = from_string if isinstance(from_string, LocaleString) else LocaleString(from_string)
|
|
156
|
+
language_code = from_string.language_code
|
|
157
|
+
pattern = self._resolve_pattern(object_type, language_code)
|
|
158
|
+
object_matches = await self.match(pattern, from_string)
|
|
113
159
|
|
|
114
160
|
for object_pattern_match in object_matches:
|
|
115
|
-
string = object_pattern_match.substring
|
|
161
|
+
string = from_string._with(object_pattern_match.substring) # TODO: review _with usage
|
|
116
162
|
|
|
117
163
|
parser: ObjectParser = self.parameter_types_by_name[object_type.__name__].parser
|
|
118
164
|
obj = object_type(string) # temp/default value, may be overridden by did_parse
|
|
119
165
|
|
|
120
166
|
matched_subparams = object_pattern_match.parameters
|
|
121
|
-
assert matched_subparams.keys() <= {p.name for p in
|
|
167
|
+
assert matched_subparams.keys() <= {p.name for p in pattern.parameters.values() if not p.optional}
|
|
122
168
|
|
|
123
|
-
for name in
|
|
169
|
+
for name in pattern.parameters:
|
|
124
170
|
value = matched_subparams[name]
|
|
125
171
|
setattr(obj, name, value)
|
|
126
172
|
|
|
@@ -141,14 +187,22 @@ class PatternParser:
|
|
|
141
187
|
yield ParseResult(obj, substring)
|
|
142
188
|
|
|
143
189
|
@alru_cache(maxsize=256, ttl=60 * 10) # TODO: env vars + disable option
|
|
144
|
-
async def _did_parse(self, obj: Object, parser: ObjectParser, string:
|
|
190
|
+
async def _did_parse(self, obj: Object, parser: ObjectParser, string: LocaleString) -> str:
|
|
145
191
|
substring = await parser.did_parse(obj, string)
|
|
192
|
+
substring = string._with(substring) if not isinstance(substring, LocaleString) else substring
|
|
146
193
|
substring = await obj.did_parse(substring)
|
|
147
194
|
return substring
|
|
148
195
|
|
|
149
|
-
async def match(
|
|
196
|
+
async def match(
|
|
197
|
+
self,
|
|
198
|
+
pattern: Pattern,
|
|
199
|
+
string: str | LocaleString,
|
|
200
|
+
recognized_entities: list[RecognizedEntity] | None = None,
|
|
201
|
+
) -> list[MatchResult]:
|
|
202
|
+
string = string if isinstance(string, LocaleString) else LocaleString(string)
|
|
203
|
+
language_code = string.language_code
|
|
150
204
|
recognized_entities = recognized_entities or []
|
|
151
|
-
compiled = self._compile_pattern(pattern)
|
|
205
|
+
compiled = self._compile_pattern(pattern, language_code=language_code)
|
|
152
206
|
logger.debug(f'Starting looking for "{pattern=}" "{compiled=}" in "{string}"')
|
|
153
207
|
|
|
154
208
|
matches = []
|
|
@@ -159,7 +213,9 @@ class PatternParser:
|
|
|
159
213
|
|
|
160
214
|
new_match = None
|
|
161
215
|
command_substr = string[match.start() : match.end()]
|
|
162
|
-
new_match, parsed_parameters = await self._parse_parameters_for_match(
|
|
216
|
+
new_match, parsed_parameters = await self._parse_parameters_for_match(
|
|
217
|
+
pattern, command_substr, recognized_entities, language_code
|
|
218
|
+
)
|
|
163
219
|
assert new_match is not None, "new_match should not be None"
|
|
164
220
|
new_match = new_match or match
|
|
165
221
|
|
|
@@ -186,7 +242,10 @@ class PatternParser:
|
|
|
186
242
|
substring=command_str,
|
|
187
243
|
start=start,
|
|
188
244
|
end=end,
|
|
189
|
-
parameters={
|
|
245
|
+
parameters={
|
|
246
|
+
name: (parameter.parsed_obj if parameter else None)
|
|
247
|
+
for name, parameter in all_parameters.items()
|
|
248
|
+
},
|
|
190
249
|
)
|
|
191
250
|
)
|
|
192
251
|
logger.debug(f"Match result: {matches[-1]}")
|
|
@@ -198,7 +257,11 @@ class PatternParser:
|
|
|
198
257
|
return sorted(re.finditer(compiled, string), key=lambda match: match.start())
|
|
199
258
|
|
|
200
259
|
async def _parse_parameters_for_match(
|
|
201
|
-
self,
|
|
260
|
+
self,
|
|
261
|
+
pattern: Pattern,
|
|
262
|
+
string: LocaleString,
|
|
263
|
+
recognized_entities: list[RecognizedEntity],
|
|
264
|
+
language_code: LanguageCode,
|
|
202
265
|
) -> tuple[re.Match | None, dict[str, ParameterMatch]]:
|
|
203
266
|
parsed_parameters: dict[str, ParameterMatch] = {}
|
|
204
267
|
logger.debug(f'Captured candidate "{string}"')
|
|
@@ -214,7 +277,7 @@ class PatternParser:
|
|
|
214
277
|
prefill = {name: parameter.parsed_substr for name, parameter in parsed_parameters.items()}
|
|
215
278
|
|
|
216
279
|
# re-run regex only in the current command_str
|
|
217
|
-
compiled = self._compile_pattern(pattern, prefill=prefill)
|
|
280
|
+
compiled = self._compile_pattern(pattern, prefill=prefill, language_code=language_code)
|
|
218
281
|
new_matches = list(re.finditer(compiled, string))
|
|
219
282
|
|
|
220
283
|
logger.debug(f'Re capturing parameters {string} prefill={prefill} compiled="{compiled}"')
|
|
@@ -269,7 +332,10 @@ class PatternParser:
|
|
|
269
332
|
),
|
|
270
333
|
)
|
|
271
334
|
parameter_match = await self._parse_single_parameter(
|
|
272
|
-
pattern,
|
|
335
|
+
pattern,
|
|
336
|
+
parameter_name,
|
|
337
|
+
string._with(match_str_groups[parameter_name].strip()),
|
|
338
|
+
recognized_entities,
|
|
273
339
|
)
|
|
274
340
|
if parameter_match is not None:
|
|
275
341
|
parsed_parameters[parameter_name] = parameter_match
|
|
@@ -277,7 +343,11 @@ class PatternParser:
|
|
|
277
343
|
return new_match, parsed_parameters
|
|
278
344
|
|
|
279
345
|
async def _parse_single_parameter(
|
|
280
|
-
self,
|
|
346
|
+
self,
|
|
347
|
+
pattern: Pattern,
|
|
348
|
+
parameter_name: str,
|
|
349
|
+
raw_param_substr: LocaleString,
|
|
350
|
+
recognized_entities: list[RecognizedEntity],
|
|
281
351
|
) -> ParameterMatch | None:
|
|
282
352
|
param = pattern.group_name_to_param[parameter_name]
|
|
283
353
|
parameter_reg_type = self.parameter_types_by_name[param.type_name]
|
|
@@ -287,7 +357,7 @@ class PatternParser:
|
|
|
287
357
|
# check recognized entities, update substring if found match
|
|
288
358
|
for entity in recognized_entities:
|
|
289
359
|
if entity.substring in raw_param_substr and entity.type is parameter_reg_type.type:
|
|
290
|
-
raw_param_substr = entity.substring
|
|
360
|
+
raw_param_substr = raw_param_substr._with(entity.substring)
|
|
291
361
|
logger.debug(f"Adjusting for recognized entity: '{entity.substring}'")
|
|
292
362
|
|
|
293
363
|
# parse the object
|
|
@@ -321,11 +391,27 @@ class PatternParser:
|
|
|
321
391
|
pattern: Pattern,
|
|
322
392
|
group_prefix: str = "",
|
|
323
393
|
prefill: dict[str, str] | None = None,
|
|
394
|
+
language_code: LanguageCode = "base",
|
|
324
395
|
) -> str: # transform Pattern to classic regex with named groups
|
|
325
396
|
prefill = prefill or {}
|
|
326
397
|
|
|
327
398
|
pattern_str: str = pattern._origin
|
|
328
399
|
|
|
400
|
+
if self._has_localizer_keys(pattern):
|
|
401
|
+
if not self.localizer:
|
|
402
|
+
raise ValueError(
|
|
403
|
+
f"Pattern '{pattern._origin}' contains @key references but no Localizer is configured on PatternParser"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
def _resolve_key(m: re.Match) -> str:
|
|
407
|
+
key = m.group("key")
|
|
408
|
+
resolved = self.localizer.get_recognizable(key, language_code)
|
|
409
|
+
if resolved is None:
|
|
410
|
+
raise KeyError(f"Localizer key '@{key}' not found for language '{language_code}'")
|
|
411
|
+
return resolved
|
|
412
|
+
|
|
413
|
+
pattern_str = _LOCALIZER_KEY_REGEX.sub(_resolve_key, pattern_str)
|
|
414
|
+
|
|
329
415
|
# transform core expressions to regex
|
|
330
416
|
|
|
331
417
|
for rule in rules_list:
|
|
@@ -341,17 +427,24 @@ class PatternParser:
|
|
|
341
427
|
for parameter in pattern.parameters.values():
|
|
342
428
|
parameter_type = self.parameter_types_by_name[parameter.type_name].type
|
|
343
429
|
|
|
344
|
-
arg_declaration =
|
|
430
|
+
arg_declaration = (
|
|
431
|
+
f"\\${parameter.name}\\:{parameter_type.__name__}" # NOTE: special chars escaped for regex
|
|
432
|
+
)
|
|
345
433
|
if parameter.name in prefill:
|
|
346
434
|
arg_pattern = prefill[parameter.name] # TODO: review wether re.escape is needed
|
|
347
435
|
else: # replace pattern annotation with compiled regex
|
|
348
436
|
parameter.group_name = ((group_prefix + "_") if group_prefix else "") + parameter.name
|
|
349
|
-
|
|
437
|
+
param_pattern = self._resolve_pattern(parameter_type, language_code)
|
|
438
|
+
arg_pattern = self._compile_pattern(
|
|
439
|
+
param_pattern, group_prefix=parameter.group_name, language_code=language_code
|
|
440
|
+
).replace("\\", r"\\")
|
|
350
441
|
|
|
351
442
|
if parameter_type.greedy and arg_pattern[-1] in {"*", "+", "}", "?"}:
|
|
352
443
|
# compensate greedy did_parse with non-greedy regex, so it won't consume next params during initial regex
|
|
353
444
|
arg_pattern += "?"
|
|
354
|
-
if pattern_str.endswith(
|
|
445
|
+
if pattern_str.endswith(
|
|
446
|
+
f"${parameter.name}:{parameter_type.__name__}"
|
|
447
|
+
): # NOTE: regex chars are NOT escaped
|
|
355
448
|
# compensate the last non_greedy regex to allow consuming till the end of the string
|
|
356
449
|
# middle greedy params are limited/stretched by neighbor params
|
|
357
450
|
arg_pattern += "$"
|
|
@@ -5,6 +5,7 @@ from typing import override
|
|
|
5
5
|
from asyncer import SoonValue, create_task_group
|
|
6
6
|
|
|
7
7
|
from stark.core.parsing import MatchResult, PatternParser, RecognizedEntity
|
|
8
|
+
from stark.general.localisation import LocaleString
|
|
8
9
|
|
|
9
10
|
from ..command import Command
|
|
10
11
|
from ..commands_context import CommandsContext, CommandsContextLayer
|
|
@@ -15,11 +16,13 @@ from ..commands_manager import SearchResult
|
|
|
15
16
|
class SearchProcessor(CommandsContextProcessor):
|
|
16
17
|
async def search(
|
|
17
18
|
self,
|
|
18
|
-
string: str,
|
|
19
|
+
string: str | LocaleString,
|
|
19
20
|
pattern_parser: PatternParser,
|
|
20
21
|
commands: list[Command],
|
|
21
22
|
recognized_entities: list[RecognizedEntity],
|
|
22
23
|
) -> list[SearchResult]:
|
|
24
|
+
string = string if isinstance(string, LocaleString) else LocaleString(string)
|
|
25
|
+
language_code = string.language_code
|
|
23
26
|
results: list[SearchResult] = []
|
|
24
27
|
futures: list[tuple[Command, SoonValue[list[MatchResult]]]] = []
|
|
25
28
|
|
|
@@ -29,7 +32,9 @@ class SearchProcessor(CommandsContextProcessor):
|
|
|
29
32
|
futures.append(
|
|
30
33
|
(
|
|
31
34
|
command,
|
|
32
|
-
group.soonify(pattern_parser.match)(
|
|
35
|
+
group.soonify(pattern_parser.match)(
|
|
36
|
+
command.get_pattern(language_code), string, recognized_entities
|
|
37
|
+
),
|
|
33
38
|
)
|
|
34
39
|
)
|
|
35
40
|
|
|
@@ -46,19 +51,28 @@ class SearchProcessor(CommandsContextProcessor):
|
|
|
46
51
|
|
|
47
52
|
# copy to prevent affecting iteration by removing items; slice makes copy automatically
|
|
48
53
|
for prev, current in zip(results.copy(), results[1:]): # TODO: concurrent; optimize using cache
|
|
49
|
-
if
|
|
54
|
+
if (
|
|
55
|
+
prev.match_result.start == current.match_result.start
|
|
56
|
+
or prev.match_result.end > current.match_result.start
|
|
57
|
+
):
|
|
50
58
|
# constrain prev end to current start
|
|
51
59
|
prev_cut = await pattern_parser.match(
|
|
52
|
-
prev.command.
|
|
60
|
+
prev.command.get_pattern(language_code),
|
|
61
|
+
string[prev.match_result.start : current.match_result.start],
|
|
62
|
+
recognized_entities,
|
|
53
63
|
)
|
|
54
64
|
# constrain current start to prev end
|
|
55
65
|
current_cut = await pattern_parser.match(
|
|
56
|
-
current.command.
|
|
66
|
+
current.command.get_pattern(language_code),
|
|
67
|
+
string[prev.match_result.end : current.match_result.end],
|
|
68
|
+
recognized_entities,
|
|
57
69
|
)
|
|
58
70
|
|
|
59
71
|
# less index = more priority to save full match
|
|
60
72
|
priority1, priority2 = (prev, current) if prev.index < current.index else (current, prev)
|
|
61
|
-
priority1_cut, priority2_cut = (
|
|
73
|
+
priority1_cut, priority2_cut = (
|
|
74
|
+
(prev_cut, current_cut) if prev.index < current.index else (current_cut, prev_cut)
|
|
75
|
+
)
|
|
62
76
|
|
|
63
77
|
if new_matches := priority2_cut: # if can cut less priority
|
|
64
78
|
priority2.match_result = new_matches[0]
|
|
@@ -74,7 +88,7 @@ class SearchProcessor(CommandsContextProcessor):
|
|
|
74
88
|
@override
|
|
75
89
|
async def process_context_layer(
|
|
76
90
|
self,
|
|
77
|
-
string:
|
|
91
|
+
string: LocaleString,
|
|
78
92
|
context: CommandsContext,
|
|
79
93
|
context_layer: CommandsContextLayer,
|
|
80
94
|
recognized_entities: list[RecognizedEntity],
|