stark-engine 4.2.2__tar.gz → 4.3.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.2.2 → stark_engine-4.3.0}/PKG-INFO +4 -2
- {stark_engine-4.2.2 → stark_engine-4.3.0}/pyproject.toml +5 -2
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/__init__.py +24 -20
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/core/__init__.py +1 -4
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/core/command.py +60 -23
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/core/commands_context.py +73 -48
- stark_engine-4.3.0/stark/core/commands_context_processor.py +87 -0
- stark_engine-4.3.0/stark/core/commands_manager.py +73 -0
- stark_engine-4.3.0/stark/core/health_check.py +51 -0
- stark_engine-4.3.0/stark/core/parsing.py +365 -0
- stark_engine-4.3.0/stark/core/patterns/__init__.py +2 -0
- stark_engine-4.3.0/stark/core/patterns/pattern.py +82 -0
- stark_engine-4.3.0/stark/core/processors/__init__.py +2 -0
- stark_engine-4.3.0/stark/core/processors/search_processor.py +82 -0
- stark_engine-4.3.0/stark/core/processors/spacy_ner_processor.py +102 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/core/types/__init__.py +1 -1
- stark_engine-4.3.0/stark/core/types/location.py +5 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/core/types/object.py +9 -5
- stark_engine-4.3.0/stark/core/types/slots.py +77 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/core/types/word.py +3 -3
- stark_engine-4.3.0/stark/general/cache.py +58 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/dictionary/nl_dictionary_name.py +4 -4
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/sliding_window_parser.py +4 -9
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/voice_assistant/voice_assistant.py +27 -13
- stark_engine-4.2.2/stark/core/commands_manager.py +0 -139
- stark_engine-4.2.2/stark/core/patterns/__init__.py +0 -3
- stark_engine-4.2.2/stark/core/patterns/parsing.py +0 -68
- stark_engine-4.2.2/stark/core/patterns/pattern.py +0 -328
- stark_engine-4.2.2/stark/core/types/number.py +0 -27
- stark_engine-4.2.2/stark/core/types/slots.py +0 -89
- stark_engine-4.2.2/stark/core/types/time.py +0 -31
- stark_engine-4.2.2/stark/core/types/time_interval.py +0 -78
- {stark_engine-4.2.2 → stark_engine-4.3.0}/LICENSE.md +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/README.md +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/core/patterns/rules.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/core/types/string.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/general/blockage_detector.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/general/classproperty.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/general/dependencies.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/general/json_encoder.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/interfaces/gcloud.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/interfaces/protocols.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/interfaces/silero.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/interfaces/vosk.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/common/span.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/dictionary/!examples.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/dictionary/__init__.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/dictionary/dictionary.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/dictionary/models.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/dictionary/storage/__init__.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/dictionary/storage/storage_memory.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/dictionary/storage/storage_sqlite.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/levenshtein/__init__.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/levenshtein/levenshtein.pyi +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/levenshtein/levenshtein.pyx +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/phonetic/simplephone.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/phonetic/transcription/__init__.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/phonetic/transcription/epitran.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/phonetic/transcription/espeak.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/phonetic/transcription/ipa2lat.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/phonetic/transcription/protocol.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/tools/strtools.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/voice_assistant/__init__.py +0 -0
- {stark_engine-4.2.2 → stark_engine-4.3.0}/stark/voice_assistant/mode.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stark-engine
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.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
|
|
7
7
|
Keywords: python,open-source,natural-language-processing,framework,cross-platform,natural-language,voice,voice-commands,python3,voice-recognition,speech-recognition,speech-to-text,community-project,voice-assistant,voice-interface,NLP,machine-learning,AI,text-analysis,stark,stark-place,stark-engine,mark parker
|
|
8
8
|
Author: MarkParker5
|
|
9
9
|
Author-email: mark@parker-programs.com
|
|
10
|
-
Requires-Python: >=3.12,<
|
|
10
|
+
Requires-Python: >=3.12,<3.15
|
|
11
11
|
Classifier: License :: Other/Proprietary License
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -17,6 +17,7 @@ Provides-Extra: all
|
|
|
17
17
|
Provides-Extra: gcloud
|
|
18
18
|
Provides-Extra: silero
|
|
19
19
|
Provides-Extra: sound
|
|
20
|
+
Provides-Extra: spacy
|
|
20
21
|
Provides-Extra: vosk
|
|
21
22
|
Requires-Dist: asyncer (>=0.0.2,<0.0.3)
|
|
22
23
|
Requires-Dist: google-cloud-texttospeech (>=2.14.1,<3.0.0) ; extra == "gcloud" or extra == "all"
|
|
@@ -24,6 +25,7 @@ Requires-Dist: numpy (>=2.3.3,<3.0.0) ; extra == "silero" or extra == "all"
|
|
|
24
25
|
Requires-Dist: pydantic (>=2.0.0,<3.0.0)
|
|
25
26
|
Requires-Dist: sounddevice (>=0.4.5,<0.5.0) ; extra == "gcloud" or extra == "vosk" or extra == "silero" or extra == "sound" or extra == "all"
|
|
26
27
|
Requires-Dist: soundfile (>=0.11.0,<0.12.0) ; extra == "gcloud" or extra == "sound" or extra == "all"
|
|
28
|
+
Requires-Dist: spacy (>=3.8.11,<4.0.0) ; extra == "spacy" or extra == "all"
|
|
27
29
|
Requires-Dist: torch (>=1.13.1,<2.0.0) ; extra == "silero" or extra == "all"
|
|
28
30
|
Requires-Dist: vosk (==0.3.44) ; extra == "vosk" or extra == "all"
|
|
29
31
|
Project-URL: Documentation, https://stark.markparker.me
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "stark-engine"
|
|
3
|
-
version = "4.
|
|
3
|
+
version = "4.3.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"
|
|
@@ -37,7 +37,7 @@ keywords = [
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
[tool.poetry.dependencies]
|
|
40
|
-
python = "
|
|
40
|
+
python = ">=3.12,<3.15"
|
|
41
41
|
pydantic = "^2.0.0"
|
|
42
42
|
asyncer = "^0.0.2"
|
|
43
43
|
numpy = "^2.3.3"
|
|
@@ -48,12 +48,14 @@ soundfile = { version = "^0.11.0", optional = true }
|
|
|
48
48
|
vosk = { version = "0.3.44", optional = true }
|
|
49
49
|
google-cloud-texttospeech = { version = "^2.14.1", optional = true }
|
|
50
50
|
torch = { version = "^1.13.1", optional = true }
|
|
51
|
+
spacy = { version = "^3.8.11", optional = true }
|
|
51
52
|
|
|
52
53
|
[tool.poetry.extras]
|
|
53
54
|
gcloud = ["google-cloud-texttospeech", "sounddevice", "soundfile"]
|
|
54
55
|
vosk = ["vosk", "sounddevice"]
|
|
55
56
|
silero = ["torch", "numpy", "sounddevice"]
|
|
56
57
|
sound = ["sounddevice", "soundfile"]
|
|
58
|
+
spacy = ["spacy"]
|
|
57
59
|
all = [
|
|
58
60
|
"google-cloud-texttospeech",
|
|
59
61
|
"vosk",
|
|
@@ -61,6 +63,7 @@ all = [
|
|
|
61
63
|
"numpy",
|
|
62
64
|
"sounddevice",
|
|
63
65
|
"soundfile",
|
|
66
|
+
"spacy",
|
|
64
67
|
]
|
|
65
68
|
|
|
66
69
|
[tool.poetry.group.dev.dependencies]
|
|
@@ -1,46 +1,50 @@
|
|
|
1
1
|
import asyncer
|
|
2
2
|
|
|
3
|
-
from stark.interfaces.protocols import (
|
|
4
|
-
SpeechRecognizer,
|
|
5
|
-
SpeechRecognizerDelegate,
|
|
6
|
-
SpeechSynthesizer,
|
|
7
|
-
SpeechSynthesizerResult
|
|
8
|
-
)
|
|
9
3
|
from stark.core import (
|
|
10
4
|
Command,
|
|
5
|
+
CommandsContext,
|
|
6
|
+
CommandsManager,
|
|
11
7
|
Pattern,
|
|
12
8
|
Response,
|
|
13
9
|
ResponseStatus,
|
|
14
|
-
CommandsContext,
|
|
15
|
-
CommandsManager
|
|
16
|
-
)
|
|
17
|
-
from stark.voice_assistant import (
|
|
18
|
-
VoiceAssistant,
|
|
19
|
-
Mode
|
|
20
10
|
)
|
|
11
|
+
from stark.core.commands_context_processor import CommandsContextProcessor
|
|
12
|
+
from stark.core.health_check import health_check
|
|
13
|
+
from stark.core.processors.search_processor import SearchProcessor
|
|
21
14
|
from stark.general.blockage_detector import BlockageDetector
|
|
15
|
+
from stark.interfaces.protocols import (
|
|
16
|
+
SpeechRecognizer,
|
|
17
|
+
SpeechRecognizerDelegate,
|
|
18
|
+
SpeechSynthesizer,
|
|
19
|
+
SpeechSynthesizerResult,
|
|
20
|
+
)
|
|
21
|
+
from stark.voice_assistant import Mode, VoiceAssistant
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
async def run(
|
|
25
25
|
manager: CommandsManager,
|
|
26
26
|
speech_recognizer: SpeechRecognizer,
|
|
27
|
-
speech_synthesizer: SpeechSynthesizer
|
|
27
|
+
speech_synthesizer: SpeechSynthesizer,
|
|
28
|
+
processors: list[CommandsContextProcessor] = [SearchProcessor()],
|
|
28
29
|
):
|
|
29
30
|
async with asyncer.create_task_group() as main_task_group:
|
|
30
31
|
context = CommandsContext(
|
|
31
|
-
task_group
|
|
32
|
-
commands_manager
|
|
32
|
+
task_group=main_task_group,
|
|
33
|
+
commands_manager=manager,
|
|
34
|
+
processors=processors,
|
|
33
35
|
)
|
|
34
36
|
voice_assistant = VoiceAssistant(
|
|
35
|
-
speech_recognizer
|
|
36
|
-
speech_synthesizer
|
|
37
|
-
commands_context
|
|
37
|
+
speech_recognizer=speech_recognizer,
|
|
38
|
+
speech_synthesizer=speech_synthesizer,
|
|
39
|
+
commands_context=context,
|
|
38
40
|
)
|
|
39
41
|
speech_recognizer.delegate = voice_assistant
|
|
40
42
|
context.delegate = voice_assistant
|
|
41
|
-
|
|
43
|
+
|
|
44
|
+
health_check(context.pattern_parser, manager.commands)
|
|
45
|
+
|
|
42
46
|
main_task_group.soonify(speech_recognizer.start_listening)()
|
|
43
47
|
main_task_group.soonify(context.handle_responses)()
|
|
44
|
-
|
|
48
|
+
|
|
45
49
|
detector = BlockageDetector()
|
|
46
50
|
main_task_group.soonify(detector.monitor)()
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from .patterns import Pattern, PatternParameter, rules
|
|
2
1
|
from . import types
|
|
3
2
|
from .command import (
|
|
4
3
|
AsyncResponseHandler,
|
|
@@ -13,6 +12,4 @@ from .commands_context import (
|
|
|
13
12
|
CommandsContextLayer,
|
|
14
13
|
)
|
|
15
14
|
from .commands_manager import CommandsManager
|
|
16
|
-
|
|
17
|
-
Pattern.add_parameter_type(types.String)
|
|
18
|
-
Pattern.add_parameter_type(types.Word)
|
|
15
|
+
from .patterns import Pattern, PatternParameter, rules
|
|
@@ -31,11 +31,16 @@ from pydantic import BaseModel, Field
|
|
|
31
31
|
from ..general.classproperty import classproperty
|
|
32
32
|
from .patterns import Pattern
|
|
33
33
|
|
|
34
|
-
ResponseOptions =
|
|
34
|
+
ResponseOptions = (
|
|
35
|
+
Optional["Response"]
|
|
36
|
+
| Generator[Optional["Response"], None, None]
|
|
37
|
+
| AsyncGenerator[Optional["Response"], None]
|
|
38
|
+
)
|
|
35
39
|
AwaitResponse = Awaitable[ResponseOptions]
|
|
36
40
|
AsyncCommandRunner = Callable[..., AwaitResponse]
|
|
37
|
-
SyncCommandRunner = Callable[..., Optional[
|
|
38
|
-
CommandRunner = TypeVar(
|
|
41
|
+
SyncCommandRunner = Callable[..., Optional["Response"]]
|
|
42
|
+
CommandRunner = TypeVar("CommandRunner", bound=SyncCommandRunner | AsyncCommandRunner)
|
|
43
|
+
|
|
39
44
|
|
|
40
45
|
class Command(Generic[CommandRunner]):
|
|
41
46
|
name: str
|
|
@@ -49,7 +54,12 @@ class Command(Generic[CommandRunner]):
|
|
|
49
54
|
self._runner = runner
|
|
50
55
|
update_wrapper(self, runner)
|
|
51
56
|
|
|
52
|
-
def run(
|
|
57
|
+
def run(
|
|
58
|
+
self,
|
|
59
|
+
parameters_dict: dict[str, Any] | None = None,
|
|
60
|
+
/,
|
|
61
|
+
**kwparameters: dict[str, Any],
|
|
62
|
+
) -> AwaitResponse:
|
|
53
63
|
# allow call both with and without dict unpacking
|
|
54
64
|
# e.g. command.run(foo = bar, lorem = ipsum), command.run(**parameters) and command.run(parameters)
|
|
55
65
|
# where parameters is dict {'foo': bar, 'lorem': ipsum}
|
|
@@ -64,20 +74,31 @@ class Command(Generic[CommandRunner]):
|
|
|
64
74
|
|
|
65
75
|
runner: AsyncCommandRunner
|
|
66
76
|
|
|
67
|
-
if inspect.iscoroutinefunction(self._runner) or inspect.isasyncgen(
|
|
68
|
-
|
|
77
|
+
if inspect.iscoroutinefunction(self._runner) or inspect.isasyncgen(
|
|
78
|
+
self._runner
|
|
79
|
+
):
|
|
80
|
+
# async functions (coroutines) and async generators are remain as is
|
|
69
81
|
runner = cast(AsyncCommandRunner, self._runner)
|
|
70
82
|
else:
|
|
71
83
|
# sync functions are wrapped with asyncer.asyncify to make them async (coroutines)
|
|
72
84
|
# async generators are not supported yet by asyncer.asyncify (https://github.com/tiangolo/asyncer/discussions/86)
|
|
73
85
|
runner = asyncer.asyncify(cast(SyncCommandRunner, self._runner))
|
|
74
86
|
|
|
75
|
-
if any(
|
|
87
|
+
if any(
|
|
88
|
+
p.kind == p.VAR_KEYWORD
|
|
89
|
+
for p in inspect.signature(self._runner).parameters.values()
|
|
90
|
+
):
|
|
76
91
|
# if command runner accepts **kwargs, pass all parameters
|
|
77
92
|
coroutine = runner(**parameters)
|
|
78
93
|
else:
|
|
79
94
|
# otherwise pass only parameters that are in command runner signature to prevent TypeError: got an unexpected keyword argument
|
|
80
|
-
coroutine = runner(
|
|
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
|
+
)
|
|
81
102
|
|
|
82
103
|
@wraps(runner)
|
|
83
104
|
async def coroutine_wrapper() -> ResponseOptions:
|
|
@@ -86,13 +107,15 @@ class Command(Generic[CommandRunner]):
|
|
|
86
107
|
except Exception as e:
|
|
87
108
|
logger.error(e)
|
|
88
109
|
response = Response(
|
|
89
|
-
text
|
|
90
|
-
voice
|
|
91
|
-
status
|
|
110
|
+
text=f"Command {self} raised an exception: {e.__class__.__name__}",
|
|
111
|
+
voice=f"Command {self} raised an exception: {e.__class__.__name__}",
|
|
112
|
+
status=ResponseStatus.error,
|
|
92
113
|
)
|
|
93
114
|
if inspect.isgenerator(response):
|
|
94
|
-
message =
|
|
95
|
-
|
|
115
|
+
message = (
|
|
116
|
+
f"[WARNING] Command {self} is a sync GeneratorType that is not fully supported and may block the main thread. "
|
|
117
|
+
+ "Consider using the ResponseHandler.respond() or async approach instead."
|
|
118
|
+
)
|
|
96
119
|
warnings.warn(message, UserWarning)
|
|
97
120
|
return response
|
|
98
121
|
|
|
@@ -103,7 +126,8 @@ class Command(Generic[CommandRunner]):
|
|
|
103
126
|
return self.run(*args, **kwargs)
|
|
104
127
|
|
|
105
128
|
def __repr__(self):
|
|
106
|
-
return f
|
|
129
|
+
return f"<Command {self.name}>"
|
|
130
|
+
|
|
107
131
|
|
|
108
132
|
class ResponseStatus(Enum):
|
|
109
133
|
none = auto()
|
|
@@ -113,9 +137,10 @@ class ResponseStatus(Enum):
|
|
|
113
137
|
info = auto()
|
|
114
138
|
error = auto()
|
|
115
139
|
|
|
140
|
+
|
|
116
141
|
class Response(BaseModel):
|
|
117
|
-
voice: str =
|
|
118
|
-
text: str =
|
|
142
|
+
voice: str = ""
|
|
143
|
+
text: str = ""
|
|
119
144
|
status: ResponseStatus = ResponseStatus.success
|
|
120
145
|
needs_user_input: bool = False
|
|
121
146
|
commands: list[Command] = []
|
|
@@ -124,7 +149,7 @@ class Response(BaseModel):
|
|
|
124
149
|
id: UUID = Field(default_factory=uuid4)
|
|
125
150
|
time: datetime = Field(default_factory=datetime.now)
|
|
126
151
|
|
|
127
|
-
_repeat_last: ClassVar[Response | None] = None
|
|
152
|
+
_repeat_last: ClassVar[Response | None] = None # static instance
|
|
128
153
|
|
|
129
154
|
@classproperty
|
|
130
155
|
def repeat_last(cls) -> Response:
|
|
@@ -139,12 +164,24 @@ class Response(BaseModel):
|
|
|
139
164
|
def __eq__(self, other):
|
|
140
165
|
return self.id == other.id
|
|
141
166
|
|
|
167
|
+
|
|
142
168
|
class ResponseHandler(Protocol):
|
|
143
|
-
def respond(self, response: Response):
|
|
144
|
-
|
|
145
|
-
|
|
169
|
+
def respond(self, response: Response):
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
def unrespond(self, response: Response):
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
def pop_context(self):
|
|
176
|
+
pass
|
|
177
|
+
|
|
146
178
|
|
|
147
179
|
class AsyncResponseHandler(Protocol):
|
|
148
|
-
async def respond(self, response: Response):
|
|
149
|
-
|
|
150
|
-
|
|
180
|
+
async def respond(self, response: Response):
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
async def unrespond(self, response: Response):
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
async def pop_context(self):
|
|
187
|
+
pass
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import warnings
|
|
4
|
-
from dataclasses import dataclass
|
|
5
5
|
from types import AsyncGeneratorType, GeneratorType
|
|
6
6
|
from typing import Any, Protocol, runtime_checkable
|
|
7
7
|
|
|
@@ -9,6 +9,13 @@ import anyio
|
|
|
9
9
|
from asyncer import syncify
|
|
10
10
|
from asyncer._main import TaskGroup
|
|
11
11
|
|
|
12
|
+
from stark.core.commands_context_processor import (
|
|
13
|
+
CommandsContextLayer,
|
|
14
|
+
RecognizedEntity,
|
|
15
|
+
)
|
|
16
|
+
from stark.core.parsing import PatternParser
|
|
17
|
+
from stark.core.types.object import Object
|
|
18
|
+
|
|
12
19
|
from ..general.dependencies import DependencyManager, default_dependency_manager
|
|
13
20
|
from .command import (
|
|
14
21
|
AsyncResponseHandler,
|
|
@@ -18,47 +25,58 @@ from .command import (
|
|
|
18
25
|
ResponseHandler,
|
|
19
26
|
ResponseOptions,
|
|
20
27
|
)
|
|
21
|
-
from .commands_manager import CommandsManager
|
|
28
|
+
from .commands_manager import CommandsManager
|
|
22
29
|
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
23
31
|
|
|
24
|
-
@dataclass
|
|
25
|
-
class CommandsContextLayer:
|
|
26
|
-
commands: list[Command]
|
|
27
|
-
parameters: dict[str, Any]
|
|
28
32
|
|
|
29
33
|
@runtime_checkable
|
|
30
34
|
class CommandsContextDelegate(Protocol):
|
|
31
|
-
async def commands_context_did_receive_response(self, response: Response):
|
|
32
|
-
|
|
35
|
+
async def commands_context_did_receive_response(self, response: Response):
|
|
36
|
+
pass
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
def remove_response(self, response: Response):
|
|
39
|
+
pass
|
|
35
40
|
|
|
41
|
+
|
|
42
|
+
class CommandsContext:
|
|
36
43
|
is_stopped = False
|
|
37
44
|
commands_manager: CommandsManager
|
|
38
45
|
dependency_manager: DependencyManager
|
|
46
|
+
pattern_parser: PatternParser
|
|
39
47
|
last_response: Response | None = None
|
|
40
|
-
|
|
48
|
+
context_queue: list[CommandsContextLayer]
|
|
41
49
|
|
|
42
50
|
_delegate: CommandsContextDelegate | None = None
|
|
43
51
|
_response_queue: list[Response]
|
|
44
|
-
_context_queue: list[CommandsContextLayer]
|
|
45
52
|
_task_group: TaskGroup
|
|
46
53
|
|
|
47
|
-
def __init__(
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
task_group: TaskGroup,
|
|
57
|
+
commands_manager: CommandsManager,
|
|
58
|
+
dependency_manager: DependencyManager = default_dependency_manager,
|
|
59
|
+
processors: list[Any] | None = None,
|
|
60
|
+
):
|
|
48
61
|
assert isinstance(task_group, TaskGroup), task_group
|
|
49
62
|
assert isinstance(commands_manager, CommandsManager)
|
|
50
63
|
assert isinstance(dependency_manager, DependencyManager)
|
|
64
|
+
self.pattern_parser = PatternParser()
|
|
51
65
|
self.commands_manager = commands_manager
|
|
52
|
-
self.
|
|
66
|
+
self.context_queue = [self.root_context]
|
|
53
67
|
self._response_queue = []
|
|
54
68
|
self._task_group = task_group
|
|
55
69
|
self.dependency_manager = dependency_manager
|
|
56
70
|
self.dependency_manager.add_dependency(None, AsyncResponseHandler, self)
|
|
57
71
|
self.dependency_manager.add_dependency(None, ResponseHandler, SyncResponseHandler(self))
|
|
58
|
-
self.dependency_manager.add_dependency(
|
|
72
|
+
self.dependency_manager.add_dependency("inject_dependencies", None, self.inject_dependencies)
|
|
73
|
+
from .processors import SearchProcessor
|
|
74
|
+
|
|
75
|
+
self.processors = processors if processors is not None else [SearchProcessor()]
|
|
59
76
|
|
|
60
77
|
@property
|
|
61
|
-
def delegate(self):
|
|
78
|
+
def delegate(self) -> CommandsContextDelegate:
|
|
79
|
+
assert self._delegate is not None
|
|
62
80
|
return self._delegate
|
|
63
81
|
|
|
64
82
|
@delegate.setter
|
|
@@ -71,46 +89,48 @@ class CommandsContext:
|
|
|
71
89
|
return CommandsContextLayer(self.commands_manager.commands, {})
|
|
72
90
|
|
|
73
91
|
async def process_string(self, string: str):
|
|
92
|
+
if not self.context_queue:
|
|
93
|
+
self.context_queue.append(self.root_context)
|
|
74
94
|
|
|
75
|
-
|
|
76
|
-
self._context_queue.append(self.root_context)
|
|
95
|
+
# Run the string and context queue through all the processors
|
|
77
96
|
|
|
78
|
-
|
|
79
|
-
search_results = []
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
current_context = self._context_queue[0]
|
|
83
|
-
search_results = await self.commands_manager.search(string = string, commands = current_context.commands)
|
|
97
|
+
recognized_entities: list[RecognizedEntity] = []
|
|
98
|
+
search_results: list[Any] = []
|
|
99
|
+
context_pops: int = 0
|
|
84
100
|
|
|
101
|
+
for processor in self.processors:
|
|
102
|
+
logger.debug(f"Processing context {processor=} with {string=} {recognized_entities=} {self.context_queue=}")
|
|
103
|
+
search_results, context_pops = await processor.process_string(string, self, recognized_entities)
|
|
85
104
|
if search_results:
|
|
105
|
+
# Pop contexts as directed by processor
|
|
106
|
+
for _ in range(context_pops):
|
|
107
|
+
if self.context_queue:
|
|
108
|
+
self.context_queue.pop(0)
|
|
86
109
|
break
|
|
87
|
-
|
|
88
|
-
|
|
110
|
+
else: # no results found at all
|
|
111
|
+
self.context_queue = [self.root_context] # nothing found, reset to root context
|
|
89
112
|
|
|
90
|
-
|
|
91
|
-
for match in matches:
|
|
92
|
-
search_results = [SearchResult(
|
|
93
|
-
command = self.fallback_command,
|
|
94
|
-
match_result = match,
|
|
95
|
-
index = 0
|
|
96
|
-
)]
|
|
113
|
+
# Prepare and execute found commands;
|
|
97
114
|
|
|
98
115
|
for search_result in search_results or []:
|
|
99
|
-
|
|
100
|
-
parameters =
|
|
116
|
+
current_context = self.context_queue[0]
|
|
117
|
+
parameters: dict[str, Object] = {}
|
|
118
|
+
parameters.update(current_context.parameters)
|
|
101
119
|
parameters.update(search_result.match_result.parameters)
|
|
102
120
|
parameters.update(self.dependency_manager.resolve(search_result.command._runner))
|
|
103
|
-
|
|
104
121
|
self.run_command(search_result.command, parameters)
|
|
105
122
|
|
|
123
|
+
return search_results or []
|
|
124
|
+
|
|
106
125
|
def inject_dependencies(self, runner: Command[CommandRunner] | CommandRunner) -> CommandRunner:
|
|
107
126
|
def injected_func(**kwargs) -> ResponseOptions:
|
|
108
127
|
kwargs.update(self.dependency_manager.resolve(runner._runner if isinstance(runner, Command) else runner))
|
|
109
|
-
return runner(**kwargs)
|
|
110
|
-
|
|
128
|
+
return runner(**kwargs) # type: ignore
|
|
129
|
+
|
|
130
|
+
return injected_func # type: ignore
|
|
111
131
|
|
|
112
132
|
def run_command(self, command: Command, parameters: dict[str, Any] = {}):
|
|
113
|
-
async def
|
|
133
|
+
async def command_task():
|
|
114
134
|
command_return = await command(parameters)
|
|
115
135
|
|
|
116
136
|
if isinstance(command_return, Response):
|
|
@@ -122,8 +142,10 @@ class CommandsContext:
|
|
|
122
142
|
await self.respond(response)
|
|
123
143
|
|
|
124
144
|
elif isinstance(command_return, GeneratorType):
|
|
125
|
-
message =
|
|
126
|
-
|
|
145
|
+
message = (
|
|
146
|
+
f"[WARNING] Command {command} is a sync GeneratorType that is not fully supported and may block the main thread. "
|
|
147
|
+
+ "Consider using the ResponseHandler.respond() or async approach instead."
|
|
148
|
+
)
|
|
127
149
|
warnings.warn(message, UserWarning)
|
|
128
150
|
for response in command_return:
|
|
129
151
|
if response:
|
|
@@ -133,13 +155,15 @@ class CommandsContext:
|
|
|
133
155
|
pass
|
|
134
156
|
|
|
135
157
|
else:
|
|
136
|
-
raise TypeError(
|
|
158
|
+
raise TypeError(
|
|
159
|
+
f"Command {command} returned {command_return} of type {type(command_return)} instead of Response or AsyncGeneratorType[Response]"
|
|
160
|
+
)
|
|
137
161
|
|
|
138
|
-
self._task_group.soonify(
|
|
162
|
+
self._task_group.soonify(command_task)()
|
|
139
163
|
|
|
140
164
|
# ResponseHandler
|
|
141
165
|
|
|
142
|
-
async def respond(self, response: Response):
|
|
166
|
+
async def respond(self, response: Response): # async forces to run in main thread
|
|
143
167
|
assert isinstance(response, Response)
|
|
144
168
|
self._response_queue.append(response)
|
|
145
169
|
|
|
@@ -149,15 +173,15 @@ class CommandsContext:
|
|
|
149
173
|
self.delegate.remove_response(response)
|
|
150
174
|
|
|
151
175
|
async def pop_context(self):
|
|
152
|
-
self.
|
|
176
|
+
self.context_queue.pop(0)
|
|
153
177
|
|
|
154
178
|
# Context
|
|
155
179
|
|
|
156
180
|
def pop_to_root_context(self):
|
|
157
|
-
self.
|
|
181
|
+
self.context_queue = [self.root_context]
|
|
158
182
|
|
|
159
183
|
def add_context(self, context: CommandsContextLayer):
|
|
160
|
-
self.
|
|
184
|
+
self.context_queue.insert(0, context)
|
|
161
185
|
|
|
162
186
|
# ResponseQueue
|
|
163
187
|
|
|
@@ -181,18 +205,19 @@ class CommandsContext:
|
|
|
181
205
|
|
|
182
206
|
if response.commands:
|
|
183
207
|
newContext = CommandsContextLayer(response.commands, response.parameters)
|
|
184
|
-
self.
|
|
208
|
+
self.context_queue.insert(0, newContext)
|
|
185
209
|
|
|
186
210
|
await self.delegate.commands_context_did_receive_response(response)
|
|
187
211
|
|
|
188
|
-
class SyncResponseHandler: # needs for changing thread from worker to main in commands ran with asyncify
|
|
189
212
|
|
|
213
|
+
class SyncResponseHandler: # needs for changing thread from worker to main in commands ran with asyncify
|
|
190
214
|
async_response_handler: ResponseHandler
|
|
191
215
|
|
|
192
216
|
def __init__(self, async_response_handler: ResponseHandler):
|
|
193
217
|
self.async_response_handler = async_response_handler
|
|
194
218
|
|
|
195
219
|
# ResponseHandler
|
|
220
|
+
# TODO: review
|
|
196
221
|
|
|
197
222
|
def respond(self, response: Response):
|
|
198
223
|
syncify(self.async_response_handler.respond)(response)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from abc import ABC
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from stark.core.command import Command
|
|
9
|
+
from stark.core.parsing import RecognizedEntity
|
|
10
|
+
from stark.core.types.object import Object
|
|
11
|
+
|
|
12
|
+
from .commands_manager import SearchResult
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from stark.core.commands_context import CommandsContext
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CommandsContextLayer:
|
|
20
|
+
commands: list[Command]
|
|
21
|
+
parameters: dict[str, Object]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CommandsContextProcessor(ABC):
|
|
28
|
+
"""
|
|
29
|
+
Abstract base class for processors in the CommandsContext pipeline.
|
|
30
|
+
|
|
31
|
+
Use cases:
|
|
32
|
+
- Override `process` for processors that need to view or operate on the entire context queue at once.
|
|
33
|
+
Example: NER markup for the string or AI-powered command search that considers all available contexts and commands in a single shot.
|
|
34
|
+
- Override `process_context` for processors that operate on each context layer individually.
|
|
35
|
+
Example: Classic pattern-based search.
|
|
36
|
+
|
|
37
|
+
In most cases you should only override/implement one.
|
|
38
|
+
|
|
39
|
+
The default implementation of `process` loops through the context queue, calling `process_context` for each layer.
|
|
40
|
+
If any layer returns non-empty results, processing stops and returns those results along with the number of contexts popped.
|
|
41
|
+
If all layers return empty results, all contexts are popped and an empty result list is returned.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
async def process_string(
|
|
45
|
+
self, string: str, context: CommandsContext, recognized_entities: list[RecognizedEntity]
|
|
46
|
+
) -> tuple[list[SearchResult], int]:
|
|
47
|
+
"""
|
|
48
|
+
Processes the entire context queue.
|
|
49
|
+
Returns a tuple: (results, pops)
|
|
50
|
+
- results: list of SearchResult objects found (may be empty)
|
|
51
|
+
- pops: number of context layers to pop after processing (0 if found a command in the current context)
|
|
52
|
+
|
|
53
|
+
Default behavior: loop through context_queue, calling process_context for each.
|
|
54
|
+
Stops at the first non-empty result and returns (results, pops).
|
|
55
|
+
If all contexts return empty, returns ([], len(context_queue)). If no results, pop amount doesn't matter.
|
|
56
|
+
|
|
57
|
+
If don't implement a command search (for example, implementing some type of pre-processing), return ([], 0)
|
|
58
|
+
"""
|
|
59
|
+
pops = 0
|
|
60
|
+
for layer in context.context_queue:
|
|
61
|
+
logger.debug(f"Command search processing context {layer=} with {recognized_entities=}")
|
|
62
|
+
results = await self.process_context_layer(
|
|
63
|
+
string,
|
|
64
|
+
context,
|
|
65
|
+
layer,
|
|
66
|
+
recognized_entities,
|
|
67
|
+
)
|
|
68
|
+
if results:
|
|
69
|
+
return results, pops
|
|
70
|
+
pops += 1
|
|
71
|
+
return [], pops
|
|
72
|
+
|
|
73
|
+
async def process_context_layer(
|
|
74
|
+
self,
|
|
75
|
+
string: str,
|
|
76
|
+
context: CommandsContext,
|
|
77
|
+
context_layer: CommandsContextLayer,
|
|
78
|
+
recognized_entities: list[RecognizedEntity],
|
|
79
|
+
) -> list[SearchResult]:
|
|
80
|
+
"""
|
|
81
|
+
Processes a single context layer.
|
|
82
|
+
Returns a list of SearchResult objects (may be empty).
|
|
83
|
+
|
|
84
|
+
Use case: Implement classic command search or other per-context logic.
|
|
85
|
+
If results are empty, the context will be popped by the caller.
|
|
86
|
+
"""
|
|
87
|
+
...
|