digitalkin 0.2.25rc0__py3-none-any.whl → 0.3.2.dev14__py3-none-any.whl
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.
- base_server/server_async_insecure.py +6 -5
- base_server/server_async_secure.py +6 -5
- base_server/server_sync_insecure.py +5 -4
- base_server/server_sync_secure.py +5 -4
- digitalkin/__version__.py +1 -1
- digitalkin/core/__init__.py +1 -0
- digitalkin/core/common/__init__.py +9 -0
- digitalkin/core/common/factories.py +156 -0
- digitalkin/core/job_manager/__init__.py +1 -0
- digitalkin/{modules → core}/job_manager/base_job_manager.py +138 -32
- digitalkin/core/job_manager/single_job_manager.py +373 -0
- digitalkin/{modules → core}/job_manager/taskiq_broker.py +121 -26
- digitalkin/core/job_manager/taskiq_job_manager.py +541 -0
- digitalkin/core/task_manager/__init__.py +1 -0
- digitalkin/core/task_manager/base_task_manager.py +539 -0
- digitalkin/core/task_manager/local_task_manager.py +108 -0
- digitalkin/core/task_manager/remote_task_manager.py +87 -0
- digitalkin/core/task_manager/surrealdb_repository.py +266 -0
- digitalkin/core/task_manager/task_executor.py +249 -0
- digitalkin/core/task_manager/task_session.py +368 -0
- digitalkin/grpc_servers/__init__.py +1 -19
- digitalkin/grpc_servers/_base_server.py +3 -3
- digitalkin/grpc_servers/module_server.py +120 -195
- digitalkin/grpc_servers/module_servicer.py +81 -44
- digitalkin/grpc_servers/utils/__init__.py +1 -0
- digitalkin/grpc_servers/utils/exceptions.py +0 -8
- digitalkin/grpc_servers/utils/grpc_client_wrapper.py +25 -9
- digitalkin/grpc_servers/utils/grpc_error_handler.py +53 -0
- digitalkin/grpc_servers/utils/utility_schema_extender.py +100 -0
- digitalkin/logger.py +64 -27
- digitalkin/mixins/__init__.py +19 -0
- digitalkin/mixins/base_mixin.py +10 -0
- digitalkin/mixins/callback_mixin.py +24 -0
- digitalkin/mixins/chat_history_mixin.py +110 -0
- digitalkin/mixins/cost_mixin.py +76 -0
- digitalkin/mixins/file_history_mixin.py +93 -0
- digitalkin/mixins/filesystem_mixin.py +46 -0
- digitalkin/mixins/logger_mixin.py +51 -0
- digitalkin/mixins/storage_mixin.py +79 -0
- digitalkin/models/__init__.py +1 -1
- digitalkin/models/core/__init__.py +1 -0
- digitalkin/{modules/job_manager → models/core}/job_manager_models.py +3 -11
- digitalkin/models/core/task_monitor.py +74 -0
- digitalkin/models/grpc_servers/__init__.py +1 -0
- digitalkin/{grpc_servers/utils → models/grpc_servers}/models.py +92 -7
- digitalkin/models/module/__init__.py +18 -11
- digitalkin/models/module/base_types.py +61 -0
- digitalkin/models/module/module.py +9 -1
- digitalkin/models/module/module_context.py +282 -6
- digitalkin/models/module/module_types.py +29 -105
- digitalkin/models/module/setup_types.py +490 -0
- digitalkin/models/module/tool_cache.py +68 -0
- digitalkin/models/module/tool_reference.py +117 -0
- digitalkin/models/module/utility.py +167 -0
- digitalkin/models/services/__init__.py +9 -0
- digitalkin/models/services/cost.py +1 -0
- digitalkin/models/services/registry.py +35 -0
- digitalkin/models/services/storage.py +39 -5
- digitalkin/modules/__init__.py +5 -1
- digitalkin/modules/_base_module.py +265 -167
- digitalkin/modules/archetype_module.py +6 -1
- digitalkin/modules/tool_module.py +16 -3
- digitalkin/modules/trigger_handler.py +7 -6
- digitalkin/modules/triggers/__init__.py +8 -0
- digitalkin/modules/triggers/healthcheck_ping_trigger.py +45 -0
- digitalkin/modules/triggers/healthcheck_services_trigger.py +63 -0
- digitalkin/modules/triggers/healthcheck_status_trigger.py +52 -0
- digitalkin/services/__init__.py +4 -0
- digitalkin/services/communication/__init__.py +7 -0
- digitalkin/services/communication/communication_strategy.py +76 -0
- digitalkin/services/communication/default_communication.py +101 -0
- digitalkin/services/communication/grpc_communication.py +234 -0
- digitalkin/services/cost/__init__.py +9 -2
- digitalkin/services/cost/grpc_cost.py +9 -42
- digitalkin/services/filesystem/default_filesystem.py +0 -2
- digitalkin/services/filesystem/grpc_filesystem.py +10 -39
- digitalkin/services/registry/__init__.py +22 -1
- digitalkin/services/registry/default_registry.py +135 -4
- digitalkin/services/registry/exceptions.py +47 -0
- digitalkin/services/registry/grpc_registry.py +306 -0
- digitalkin/services/registry/registry_models.py +15 -0
- digitalkin/services/registry/registry_strategy.py +88 -4
- digitalkin/services/services_config.py +25 -3
- digitalkin/services/services_models.py +5 -1
- digitalkin/services/setup/default_setup.py +6 -7
- digitalkin/services/setup/grpc_setup.py +52 -15
- digitalkin/services/storage/grpc_storage.py +4 -4
- digitalkin/services/user_profile/__init__.py +12 -0
- digitalkin/services/user_profile/default_user_profile.py +55 -0
- digitalkin/services/user_profile/grpc_user_profile.py +69 -0
- digitalkin/services/user_profile/user_profile_strategy.py +25 -0
- digitalkin/utils/__init__.py +28 -0
- digitalkin/utils/arg_parser.py +1 -1
- digitalkin/utils/development_mode_action.py +2 -2
- digitalkin/utils/dynamic_schema.py +483 -0
- digitalkin/utils/package_discover.py +1 -2
- digitalkin/utils/schema_splitter.py +207 -0
- {digitalkin-0.2.25rc0.dist-info → digitalkin-0.3.2.dev14.dist-info}/METADATA +11 -30
- digitalkin-0.3.2.dev14.dist-info/RECORD +143 -0
- {digitalkin-0.2.25rc0.dist-info → digitalkin-0.3.2.dev14.dist-info}/top_level.txt +1 -0
- modules/archetype_with_tools_module.py +244 -0
- modules/cpu_intensive_module.py +1 -1
- modules/dynamic_setup_module.py +338 -0
- modules/minimal_llm_module.py +1 -1
- modules/text_transform_module.py +1 -1
- monitoring/digitalkin_observability/__init__.py +46 -0
- monitoring/digitalkin_observability/http_server.py +150 -0
- monitoring/digitalkin_observability/interceptors.py +176 -0
- monitoring/digitalkin_observability/metrics.py +201 -0
- monitoring/digitalkin_observability/prometheus.py +137 -0
- monitoring/tests/test_metrics.py +172 -0
- services/filesystem_module.py +7 -5
- services/storage_module.py +4 -2
- digitalkin/grpc_servers/registry_server.py +0 -65
- digitalkin/grpc_servers/registry_servicer.py +0 -456
- digitalkin/grpc_servers/utils/factory.py +0 -180
- digitalkin/modules/job_manager/single_job_manager.py +0 -294
- digitalkin/modules/job_manager/taskiq_job_manager.py +0 -290
- digitalkin-0.2.25rc0.dist-info/RECORD +0 -89
- /digitalkin/{grpc_servers/utils → models/grpc_servers}/types.py +0 -0
- {digitalkin-0.2.25rc0.dist-info → digitalkin-0.3.2.dev14.dist-info}/WHEEL +0 -0
- {digitalkin-0.2.25rc0.dist-info → digitalkin-0.3.2.dev14.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,42 +1,29 @@
|
|
|
1
1
|
"""BaseModule is the abstract base for all modules in the DigitalKin SDK."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import contextlib
|
|
5
4
|
import json
|
|
5
|
+
import os
|
|
6
6
|
from abc import ABC, abstractmethod
|
|
7
7
|
from collections.abc import Callable, Coroutine
|
|
8
8
|
from typing import Any, ClassVar, Generic
|
|
9
9
|
|
|
10
|
-
from
|
|
11
|
-
|
|
10
|
+
from digitalkin.grpc_servers.utils.utility_schema_extender import UtilitySchemaExtender
|
|
12
11
|
from digitalkin.logger import logger
|
|
13
|
-
from digitalkin.models.module import
|
|
12
|
+
from digitalkin.models.module.module import ModuleCodeModel, ModuleStatus
|
|
13
|
+
from digitalkin.models.module.module_context import ModuleContext
|
|
14
|
+
from digitalkin.models.module.module_types import (
|
|
15
|
+
DataModel,
|
|
14
16
|
InputModelT,
|
|
15
|
-
ModuleStatus,
|
|
16
17
|
OutputModelT,
|
|
17
18
|
SecretModelT,
|
|
18
19
|
SetupModelT,
|
|
19
20
|
)
|
|
20
|
-
from digitalkin.models.module.
|
|
21
|
+
from digitalkin.models.module.utility import EndOfStreamOutput, ModuleStartInfoOutput, UtilityProtocol
|
|
22
|
+
from digitalkin.models.services.storage import BaseRole
|
|
21
23
|
from digitalkin.modules.trigger_handler import TriggerHandler
|
|
22
|
-
from digitalkin.services.agent.agent_strategy import AgentStrategy
|
|
23
|
-
from digitalkin.services.cost.cost_strategy import CostStrategy
|
|
24
|
-
from digitalkin.services.filesystem.filesystem_strategy import FilesystemStrategy
|
|
25
|
-
from digitalkin.services.identity.identity_strategy import IdentityStrategy
|
|
26
|
-
from digitalkin.services.registry.registry_strategy import RegistryStrategy
|
|
27
24
|
from digitalkin.services.services_config import ServicesConfig, ServicesStrategy
|
|
28
|
-
from digitalkin.services.snapshot.snapshot_strategy import SnapshotStrategy
|
|
29
|
-
from digitalkin.services.storage.storage_strategy import StorageStrategy
|
|
30
|
-
from digitalkin.utils.llm_ready_schema import llm_ready_schema
|
|
31
25
|
from digitalkin.utils.package_discover import ModuleDiscoverer
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class ModuleCodeModel(BaseModel):
|
|
35
|
-
"""typed error/code model."""
|
|
36
|
-
|
|
37
|
-
code: str
|
|
38
|
-
message: str
|
|
39
|
-
short_description: str
|
|
26
|
+
from digitalkin.utils.schema_splitter import SchemaSplitter
|
|
40
27
|
|
|
41
28
|
|
|
42
29
|
class BaseModule( # noqa: PLR0904
|
|
@@ -63,37 +50,44 @@ class BaseModule( # noqa: PLR0904
|
|
|
63
50
|
triggers_discoverer: ClassVar[ModuleDiscoverer]
|
|
64
51
|
|
|
65
52
|
# service config params
|
|
66
|
-
services_config_strategies: ClassVar[dict[str, ServicesStrategy | None]]
|
|
67
|
-
services_config_params: ClassVar[dict[str, dict[str, Any | None] | None]]
|
|
53
|
+
services_config_strategies: ClassVar[dict[str, ServicesStrategy | None]] = {}
|
|
54
|
+
services_config_params: ClassVar[dict[str, dict[str, Any | None] | None]] = {}
|
|
68
55
|
services_config: ServicesConfig
|
|
69
56
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
57
|
+
@classmethod
|
|
58
|
+
def get_module_id(cls) -> str:
|
|
59
|
+
"""Get the module ID from environment variable or metadata.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The module_id from DIGITALKIN_MODULE_ID env var, or metadata module_id,
|
|
63
|
+
or "unknown" if neither exists.
|
|
64
|
+
"""
|
|
65
|
+
return os.environ.get("DIGITALKIN_MODULE_ID") or cls.metadata.get("module_id", "unknown")
|
|
66
|
+
|
|
67
|
+
def _init_strategies(self, mission_id: str, setup_id: str, setup_version_id: str) -> dict[str, Any]:
|
|
68
|
+
"""Initialize the services configuration.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
dict of services with name: Strategy
|
|
72
|
+
agent: AgentStrategy
|
|
73
|
+
cost: CostStrategy
|
|
74
|
+
filesystem: FilesystemStrategy
|
|
75
|
+
identity: IdentityStrategy
|
|
76
|
+
registry: RegistryStrategy
|
|
77
|
+
snapshot: SnapshotStrategy
|
|
78
|
+
storage: StorageStrategy
|
|
79
|
+
user_profile: UserProfileStrategy
|
|
80
|
+
"""
|
|
81
|
+
logger.debug("Service initialisation: %s", self.services_config_strategies.keys())
|
|
82
|
+
return {
|
|
83
|
+
service_name: self.services_config.init_strategy(
|
|
91
84
|
service_name,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
85
|
+
mission_id,
|
|
86
|
+
setup_id,
|
|
87
|
+
setup_version_id,
|
|
95
88
|
)
|
|
96
|
-
|
|
89
|
+
for service_name in self.services_config.valid_strategy_names()
|
|
90
|
+
}
|
|
97
91
|
|
|
98
92
|
def __init__(
|
|
99
93
|
self,
|
|
@@ -103,22 +97,19 @@ class BaseModule( # noqa: PLR0904
|
|
|
103
97
|
setup_version_id: str,
|
|
104
98
|
) -> None:
|
|
105
99
|
"""Initialize the module."""
|
|
106
|
-
self.job_id: str = job_id
|
|
107
|
-
self.mission_id: str = mission_id
|
|
108
|
-
# Setup reference needed for the overall Kin scope as the filesystem context
|
|
109
|
-
self.setup_id: str = setup_id
|
|
110
|
-
# SetupVersion reference needed for the precise Kin scope as the cost
|
|
111
|
-
self.setup_version_id: str = setup_version_id
|
|
112
100
|
self._status = ModuleStatus.CREATED
|
|
113
|
-
self._task: asyncio.Task | None = None
|
|
114
|
-
# Initialize services configuration
|
|
115
|
-
self._init_strategies()
|
|
116
101
|
|
|
117
102
|
# Initialize minimum context
|
|
118
103
|
self.context = ModuleContext(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
104
|
+
# Initialize services configuration
|
|
105
|
+
**self._init_strategies(mission_id, setup_id, setup_version_id),
|
|
106
|
+
session={
|
|
107
|
+
"setup_id": setup_id,
|
|
108
|
+
"mission_id": mission_id,
|
|
109
|
+
"setup_version_id": setup_version_id,
|
|
110
|
+
"job_id": job_id,
|
|
111
|
+
},
|
|
112
|
+
callbacks={"logger": logger},
|
|
122
113
|
)
|
|
123
114
|
|
|
124
115
|
@property
|
|
@@ -131,91 +122,134 @@ class BaseModule( # noqa: PLR0904
|
|
|
131
122
|
return self._status
|
|
132
123
|
|
|
133
124
|
@classmethod
|
|
134
|
-
def get_secret_format(cls, *, llm_format: bool) -> str:
|
|
125
|
+
async def get_secret_format(cls, *, llm_format: bool) -> str:
|
|
135
126
|
"""Get the JSON schema of the secret format model.
|
|
136
127
|
|
|
137
|
-
|
|
138
|
-
|
|
128
|
+
Args:
|
|
129
|
+
llm_format: If True, return LLM-optimized schema format with inlined
|
|
130
|
+
references and simplified structure.
|
|
139
131
|
|
|
140
132
|
Returns:
|
|
141
|
-
The JSON schema of the secret format as a string.
|
|
133
|
+
The JSON schema of the secret format as a JSON string.
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
NotImplementedError: If the `secret_format` class attribute is not defined.
|
|
142
137
|
"""
|
|
143
138
|
if cls.secret_format is not None:
|
|
144
139
|
if llm_format:
|
|
145
|
-
|
|
140
|
+
result_json, result_ui = SchemaSplitter.split(cls.secret_format.model_json_schema())
|
|
141
|
+
return json.dumps({"json_schema": result_json, "ui_schema": result_ui}, indent=2)
|
|
146
142
|
return json.dumps(cls.secret_format.model_json_schema(), indent=2)
|
|
147
143
|
msg = f"{cls.__name__}' class does not define a 'secret_format'."
|
|
148
144
|
raise NotImplementedError(msg)
|
|
149
145
|
|
|
150
146
|
@classmethod
|
|
151
|
-
def get_input_format(cls, *, llm_format: bool) -> str:
|
|
147
|
+
async def get_input_format(cls, *, llm_format: bool) -> str:
|
|
152
148
|
"""Get the JSON schema of the input format model.
|
|
153
149
|
|
|
154
|
-
|
|
155
|
-
|
|
150
|
+
Args:
|
|
151
|
+
llm_format: If True, return LLM-optimized schema format with inlined
|
|
152
|
+
references and simplified structure.
|
|
156
153
|
|
|
157
154
|
Returns:
|
|
158
|
-
The JSON schema of the input format as a string.
|
|
155
|
+
The JSON schema of the input format as a JSON string.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
NotImplementedError: If the `input_format` class attribute is not defined.
|
|
159
159
|
"""
|
|
160
|
-
if cls.input_format is
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
160
|
+
if cls.input_format is None:
|
|
161
|
+
msg = f"{cls.__name__}' class does not define an 'input_format'."
|
|
162
|
+
raise NotImplementedError(msg)
|
|
163
|
+
|
|
164
|
+
extended_model = UtilitySchemaExtender.create_extended_input_model(cls.input_format)
|
|
165
|
+
|
|
166
|
+
if llm_format:
|
|
167
|
+
result_json, result_ui = SchemaSplitter.split(extended_model.model_json_schema())
|
|
168
|
+
return json.dumps({"json_schema": result_json, "ui_schema": result_ui}, indent=2)
|
|
169
|
+
return json.dumps(extended_model.model_json_schema(), indent=2)
|
|
166
170
|
|
|
167
171
|
@classmethod
|
|
168
|
-
def get_output_format(cls, *, llm_format: bool) -> str:
|
|
172
|
+
async def get_output_format(cls, *, llm_format: bool) -> str:
|
|
169
173
|
"""Get the JSON schema of the output format model.
|
|
170
174
|
|
|
171
|
-
|
|
172
|
-
|
|
175
|
+
Args:
|
|
176
|
+
llm_format: If True, return LLM-optimized schema format with inlined
|
|
177
|
+
references and simplified structure.
|
|
173
178
|
|
|
174
179
|
Returns:
|
|
175
|
-
The JSON schema of the output format as a string.
|
|
180
|
+
The JSON schema of the output format as a JSON string.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
NotImplementedError: If the `output_format` class attribute is not defined.
|
|
176
184
|
"""
|
|
177
|
-
if cls.output_format is
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
185
|
+
if cls.output_format is None:
|
|
186
|
+
msg = f"'{cls.__name__}' class does not define an 'output_format'."
|
|
187
|
+
raise NotImplementedError(msg)
|
|
188
|
+
|
|
189
|
+
extended_model = UtilitySchemaExtender.create_extended_output_model(cls.output_format)
|
|
190
|
+
|
|
191
|
+
if llm_format:
|
|
192
|
+
result_json, result_ui = SchemaSplitter.split(extended_model.model_json_schema())
|
|
193
|
+
return json.dumps({"json_schema": result_json, "ui_schema": result_ui}, indent=2)
|
|
194
|
+
return json.dumps(extended_model.model_json_schema(), indent=2)
|
|
183
195
|
|
|
184
196
|
@classmethod
|
|
185
|
-
def get_config_setup_format(cls, *, llm_format: bool) -> str:
|
|
197
|
+
async def get_config_setup_format(cls, *, llm_format: bool) -> str:
|
|
186
198
|
"""Gets the JSON schema of the config setup format model.
|
|
187
199
|
|
|
188
|
-
The config setup format is used only to initialize the module with configuration
|
|
189
|
-
|
|
200
|
+
The config setup format is used only to initialize the module with configuration
|
|
201
|
+
data. It includes fields marked with `json_schema_extra={"config": True}` and
|
|
202
|
+
excludes hidden runtime fields.
|
|
190
203
|
|
|
191
|
-
|
|
192
|
-
|
|
204
|
+
Dynamic schema fields are always resolved when generating the schema, as this
|
|
205
|
+
method is typically called during module discovery or schema generation where
|
|
206
|
+
fresh values are needed.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
llm_format: If True, return LLM-optimized schema format with inlined
|
|
210
|
+
references and simplified structure.
|
|
193
211
|
|
|
194
212
|
Returns:
|
|
195
|
-
The JSON schema of the config setup format as a string.
|
|
213
|
+
The JSON schema of the config setup format as a JSON string.
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
NotImplementedError: If the `setup_format` class attribute is not defined.
|
|
196
217
|
"""
|
|
197
218
|
if cls.setup_format is not None:
|
|
198
|
-
setup_format = cls.setup_format.get_clean_model(config_fields=True, hidden_fields=False)
|
|
219
|
+
setup_format = await cls.setup_format.get_clean_model(config_fields=True, hidden_fields=False, force=True)
|
|
199
220
|
if llm_format:
|
|
200
|
-
|
|
221
|
+
result_json, result_ui = SchemaSplitter.split(setup_format.model_json_schema())
|
|
222
|
+
return json.dumps({"json_schema": result_json, "ui_schema": result_ui}, indent=2)
|
|
201
223
|
return json.dumps(setup_format.model_json_schema(), indent=2)
|
|
202
224
|
msg = "'%s' class does not define an 'config_setup_format'."
|
|
203
225
|
raise NotImplementedError(msg)
|
|
204
226
|
|
|
205
227
|
@classmethod
|
|
206
|
-
def get_setup_format(cls, *, llm_format: bool) -> str:
|
|
228
|
+
async def get_setup_format(cls, *, llm_format: bool) -> str:
|
|
207
229
|
"""Gets the JSON schema of the setup format model.
|
|
208
230
|
|
|
209
|
-
|
|
210
|
-
|
|
231
|
+
The setup format is used at runtime and includes hidden fields but excludes
|
|
232
|
+
config-only fields. This is the schema used when running the module.
|
|
233
|
+
|
|
234
|
+
Dynamic schema fields are always resolved when generating the schema, as this
|
|
235
|
+
method is typically called during module discovery or schema generation where
|
|
236
|
+
fresh values are needed.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
llm_format: If True, return LLM-optimized schema format with inlined
|
|
240
|
+
references and simplified structure.
|
|
211
241
|
|
|
212
242
|
Returns:
|
|
213
|
-
The JSON schema of the setup format as a string.
|
|
243
|
+
The JSON schema of the setup format as a JSON string.
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
NotImplementedError: If the `setup_format` class attribute is not defined.
|
|
214
247
|
"""
|
|
215
248
|
if cls.setup_format is not None:
|
|
216
|
-
setup_format = cls.setup_format.get_clean_model(config_fields=False, hidden_fields=True)
|
|
249
|
+
setup_format = await cls.setup_format.get_clean_model(config_fields=False, hidden_fields=True, force=True)
|
|
217
250
|
if llm_format:
|
|
218
|
-
|
|
251
|
+
result_json, result_ui = SchemaSplitter.split(setup_format.model_json_schema())
|
|
252
|
+
return json.dumps({"json_schema": result_json, "ui_schema": result_ui}, indent=2)
|
|
219
253
|
return json.dumps(setup_format.model_json_schema(), indent=2)
|
|
220
254
|
msg = "'%s' class does not define an 'setup_format'."
|
|
221
255
|
raise NotImplementedError(msg)
|
|
@@ -245,17 +279,22 @@ class BaseModule( # noqa: PLR0904
|
|
|
245
279
|
return cls.input_format(**input_data)
|
|
246
280
|
|
|
247
281
|
@classmethod
|
|
248
|
-
def create_setup_model(cls, setup_data: dict[str, Any], *, config_fields: bool = False) -> SetupModelT:
|
|
282
|
+
async def create_setup_model(cls, setup_data: dict[str, Any], *, config_fields: bool = False) -> SetupModelT:
|
|
249
283
|
"""Create the setup model from the setup data.
|
|
250
284
|
|
|
285
|
+
Creates a filtered setup model instance based on the provided data.
|
|
286
|
+
Uses `get_clean_model()` internally to get the appropriate model class
|
|
287
|
+
with field filtering applied.
|
|
288
|
+
|
|
251
289
|
Args:
|
|
252
290
|
setup_data: The setup data to create the model from.
|
|
253
291
|
config_fields: If True, include only fields with json_schema_extra["config"] == True.
|
|
254
292
|
|
|
255
293
|
Returns:
|
|
256
|
-
|
|
294
|
+
An instance of the setup model with the provided data.
|
|
257
295
|
"""
|
|
258
|
-
|
|
296
|
+
model_cls = await cls.setup_format.get_clean_model(config_fields=config_fields, hidden_fields=True)
|
|
297
|
+
return model_cls(**setup_data)
|
|
259
298
|
|
|
260
299
|
@classmethod
|
|
261
300
|
def create_secret_model(cls, secret_data: dict[str, Any]) -> SecretModelT:
|
|
@@ -291,8 +330,18 @@ class BaseModule( # noqa: PLR0904
|
|
|
291
330
|
If a package is provided, all .py files within its path are imported; otherwise, the current
|
|
292
331
|
working directory is searched. For each imported module, any class matching the criteria is
|
|
293
332
|
registered via cls.register(). Errors during import are logged at debug level.
|
|
333
|
+
|
|
334
|
+
Built-in healthcheck handlers (ping, services, status) are automatically registered
|
|
335
|
+
to provide standard healthcheck functionality for all modules.
|
|
294
336
|
"""
|
|
337
|
+
from digitalkin.models.module.utility import UtilityRegistry # noqa: PLC0415
|
|
338
|
+
|
|
295
339
|
cls.triggers_discoverer.discover_modules()
|
|
340
|
+
|
|
341
|
+
# Auto-register built-in SDK triggers (healthcheck, etc.)
|
|
342
|
+
for trigger_cls in UtilityRegistry.get_builtin_triggers():
|
|
343
|
+
cls.triggers_discoverer.register_trigger(trigger_cls)
|
|
344
|
+
|
|
296
345
|
logger.debug("discovered: %s", cls.triggers_discoverer)
|
|
297
346
|
|
|
298
347
|
@classmethod
|
|
@@ -307,23 +356,8 @@ class BaseModule( # noqa: PLR0904
|
|
|
307
356
|
"""
|
|
308
357
|
return cls.triggers_discoverer.register_trigger(handler_cls)
|
|
309
358
|
|
|
310
|
-
async def run_config_setup(self, config_setup_data: SetupModelT) -> SetupModelT: # noqa: PLR6301
|
|
311
|
-
"""Run config setup the module.
|
|
312
|
-
|
|
313
|
-
The config setup is used to initialize the setup with configuration data.
|
|
314
|
-
This method is typically used to set up the module with necessary configuration before running it,
|
|
315
|
-
especially for processing data like files.
|
|
316
|
-
The function needs to save the setup in the storage.
|
|
317
|
-
The module will be initialize with the setup and not the config setup.
|
|
318
|
-
This method is optional, the config setup and setup can be the same.
|
|
319
|
-
|
|
320
|
-
Returns:
|
|
321
|
-
The updated setup model after running the config setup.
|
|
322
|
-
"""
|
|
323
|
-
return config_setup_data
|
|
324
|
-
|
|
325
359
|
@abstractmethod
|
|
326
|
-
async def initialize(self, setup_data: SetupModelT) -> None:
|
|
360
|
+
async def initialize(self, context: ModuleContext, setup_data: SetupModelT) -> None:
|
|
327
361
|
"""Initialize the module."""
|
|
328
362
|
raise NotImplementedError
|
|
329
363
|
|
|
@@ -331,22 +365,12 @@ class BaseModule( # noqa: PLR0904
|
|
|
331
365
|
self,
|
|
332
366
|
input_data: InputModelT,
|
|
333
367
|
setup_data: SetupModelT,
|
|
334
|
-
callback: Callable[[OutputModelT], Coroutine[Any, Any, None]],
|
|
335
368
|
) -> None:
|
|
336
|
-
"""Run the module
|
|
337
|
-
|
|
338
|
-
This method validates the input data, determines the protocol from the input,
|
|
339
|
-
and dispatches the request to the corresponding trigger handler. The trigger handler
|
|
340
|
-
is responsible for processing the input and invoking the callback with the result.
|
|
341
|
-
|
|
342
|
-
Triggers:
|
|
343
|
-
- The method is triggered when a module run is requested with specific input and setup data.
|
|
344
|
-
- The protocol specified in the input determines which trigger handler is invoked.
|
|
369
|
+
"""Run the module by dispatching to the appropriate trigger handler.
|
|
345
370
|
|
|
346
371
|
Args:
|
|
347
|
-
input_data
|
|
348
|
-
setup_data
|
|
349
|
-
callback (Callable[[OutputModelT], Coroutine[Any, Any, None]]): callback to be invoked to stream any result.
|
|
372
|
+
input_data: Input data to process.
|
|
373
|
+
setup_data: Configuration data for the module.
|
|
350
374
|
|
|
351
375
|
Raises:
|
|
352
376
|
ValueError: If no handler for the protocol is found.
|
|
@@ -360,7 +384,6 @@ class BaseModule( # noqa: PLR0904
|
|
|
360
384
|
await handler_instance.handle(
|
|
361
385
|
input_instance.root,
|
|
362
386
|
setup_data,
|
|
363
|
-
callback,
|
|
364
387
|
self.context,
|
|
365
388
|
)
|
|
366
389
|
|
|
@@ -369,11 +392,29 @@ class BaseModule( # noqa: PLR0904
|
|
|
369
392
|
"""Run the module."""
|
|
370
393
|
raise NotImplementedError
|
|
371
394
|
|
|
395
|
+
async def run_config_setup( # noqa: PLR6301
|
|
396
|
+
self,
|
|
397
|
+
context: ModuleContext, # noqa: ARG002
|
|
398
|
+
config_setup_data: SetupModelT,
|
|
399
|
+
) -> SetupModelT:
|
|
400
|
+
"""Run config setup the module.
|
|
401
|
+
|
|
402
|
+
The config setup is used to initialize the setup with configuration data.
|
|
403
|
+
This method is typically used to set up the module with necessary configuration before running it,
|
|
404
|
+
especially for processing data like files.
|
|
405
|
+
The function needs to save the setup in the storage.
|
|
406
|
+
The module will be initialize with the setup and not the config setup.
|
|
407
|
+
This method is optional, the config setup and setup can be the same.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
The updated setup model after running the config setup.
|
|
411
|
+
"""
|
|
412
|
+
return config_setup_data
|
|
413
|
+
|
|
372
414
|
async def _run_lifecycle(
|
|
373
415
|
self,
|
|
374
416
|
input_data: InputModelT,
|
|
375
417
|
setup_data: SetupModelT,
|
|
376
|
-
callback: Callable[[OutputModelT], Coroutine[Any, Any, None]],
|
|
377
418
|
) -> None:
|
|
378
419
|
"""Run the module lifecycle.
|
|
379
420
|
|
|
@@ -381,38 +422,56 @@ class BaseModule( # noqa: PLR0904
|
|
|
381
422
|
asyncio.CancelledError: If the module is cancelled
|
|
382
423
|
"""
|
|
383
424
|
try:
|
|
384
|
-
logger.info("Starting module %s", self.name)
|
|
385
|
-
await self.run(input_data, setup_data
|
|
386
|
-
logger.info("Module %s finished", self.name)
|
|
425
|
+
logger.info("Starting module %s", self.name, extra=self.context.session.current_ids())
|
|
426
|
+
await self.run(input_data, setup_data)
|
|
427
|
+
logger.info("Module %s finished", self.name, extra=self.context.session.current_ids())
|
|
387
428
|
except asyncio.CancelledError:
|
|
388
429
|
self._status = ModuleStatus.CANCELLED
|
|
389
|
-
logger.error(
|
|
430
|
+
logger.error("Module %s cancelled", self.name, extra=self.context.session.current_ids())
|
|
390
431
|
except Exception:
|
|
391
432
|
self._status = ModuleStatus.FAILED
|
|
392
|
-
logger.exception("Error inside module %s", self.name)
|
|
433
|
+
logger.exception("Error inside module %s", self.name, extra=self.context.session.current_ids())
|
|
393
434
|
else:
|
|
394
435
|
self._status = ModuleStatus.STOPPING
|
|
395
|
-
finally:
|
|
396
|
-
await self.stop()
|
|
397
436
|
|
|
398
437
|
async def start(
|
|
399
438
|
self,
|
|
400
439
|
input_data: InputModelT,
|
|
401
440
|
setup_data: SetupModelT,
|
|
402
|
-
callback: Callable[[OutputModelT | ModuleCodeModel], Coroutine[Any, Any, None]],
|
|
441
|
+
callback: Callable[[OutputModelT | ModuleCodeModel | DataModel[UtilityProtocol]], Coroutine[Any, Any, None]],
|
|
403
442
|
done_callback: Callable | None = None,
|
|
404
443
|
) -> None:
|
|
405
444
|
"""Start the module."""
|
|
406
445
|
try:
|
|
407
|
-
|
|
408
|
-
|
|
446
|
+
self.context.callbacks.send_message = callback
|
|
447
|
+
|
|
448
|
+
tool_cache = setup_data.build_tool_cache()
|
|
449
|
+
if tool_cache.entries:
|
|
450
|
+
self.context.tool_cache = tool_cache
|
|
451
|
+
|
|
452
|
+
await callback(
|
|
453
|
+
DataModel(
|
|
454
|
+
root=ModuleStartInfoOutput(
|
|
455
|
+
job_id=self.context.session.job_id,
|
|
456
|
+
mission_id=self.context.session.mission_id,
|
|
457
|
+
setup_id=self.context.session.setup_id,
|
|
458
|
+
setup_version_id=self.context.session.setup_version_id,
|
|
459
|
+
module_id=self.get_module_id(),
|
|
460
|
+
module_name=self.name,
|
|
461
|
+
),
|
|
462
|
+
annotations={"role": BaseRole.SYSTEM},
|
|
463
|
+
)
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
logger.info("Initialize module %s", self.context.session.job_id)
|
|
467
|
+
await self.initialize(self.context, setup_data)
|
|
409
468
|
except Exception as e:
|
|
410
469
|
self._status = ModuleStatus.FAILED
|
|
411
470
|
short_description = "Error initializing module"
|
|
412
471
|
logger.exception("%s: %s", short_description, e)
|
|
413
472
|
await callback(
|
|
414
473
|
ModuleCodeModel(
|
|
415
|
-
code=
|
|
474
|
+
code="Error",
|
|
416
475
|
short_description=short_description,
|
|
417
476
|
message=str(e),
|
|
418
477
|
)
|
|
@@ -425,53 +484,92 @@ class BaseModule( # noqa: PLR0904
|
|
|
425
484
|
try:
|
|
426
485
|
logger.debug("Init the discovered input handlers.")
|
|
427
486
|
self.triggers_discoverer.init_handlers(self.context)
|
|
428
|
-
logger.debug("Run lifecycle")
|
|
429
|
-
self.
|
|
430
|
-
self._task = asyncio.create_task(
|
|
431
|
-
self._run_lifecycle(input_data, setup_data, callback),
|
|
432
|
-
name="module_lifecycle",
|
|
433
|
-
)
|
|
434
|
-
if done_callback is not None:
|
|
435
|
-
self._task.add_done_callback(done_callback)
|
|
487
|
+
logger.debug("Run lifecycle %s", self.context.session.job_id)
|
|
488
|
+
await self._run_lifecycle(input_data, setup_data)
|
|
436
489
|
except Exception:
|
|
437
490
|
self._status = ModuleStatus.FAILED
|
|
438
491
|
logger.exception("Error during module lifecyle")
|
|
492
|
+
finally:
|
|
493
|
+
await self.stop()
|
|
439
494
|
|
|
440
495
|
async def stop(self) -> None:
|
|
441
496
|
"""Stop the module."""
|
|
442
|
-
logger.info("Stopping module %s
|
|
443
|
-
if self._status not in {ModuleStatus.RUNNING, ModuleStatus.STOPPING}:
|
|
444
|
-
return
|
|
445
|
-
|
|
497
|
+
logger.info("Stopping module %s | job_id=%s", self.name, self.context.session.job_id)
|
|
446
498
|
try:
|
|
447
499
|
self._status = ModuleStatus.STOPPING
|
|
448
|
-
if self._task and not self._task.done():
|
|
449
|
-
self._task.cancel()
|
|
450
|
-
with contextlib.suppress(asyncio.CancelledError):
|
|
451
|
-
await self._task
|
|
452
500
|
logger.debug("Module %s stopped", self.name)
|
|
453
501
|
await self.cleanup()
|
|
502
|
+
await self.context.callbacks.send_message(
|
|
503
|
+
DataModel(
|
|
504
|
+
root=EndOfStreamOutput(),
|
|
505
|
+
annotations={"role": BaseRole.SYSTEM},
|
|
506
|
+
)
|
|
507
|
+
)
|
|
454
508
|
self._status = ModuleStatus.STOPPED
|
|
455
509
|
logger.debug("Module %s cleaned", self.name)
|
|
456
510
|
except Exception:
|
|
457
511
|
self._status = ModuleStatus.FAILED
|
|
458
512
|
logger.exception("Error stopping module")
|
|
459
513
|
|
|
514
|
+
async def _resolve_tools(self, config_setup_data: SetupModelT) -> None:
|
|
515
|
+
"""Resolve tool references and build cache.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
config_setup_data: Setup data containing tool references.
|
|
519
|
+
"""
|
|
520
|
+
logger.info("Starting tool resolution", extra=self.context.session.current_ids())
|
|
521
|
+
if self.context.registry is not None:
|
|
522
|
+
config_setup_data.resolve_tool_references(self.context.registry)
|
|
523
|
+
logger.info("Tool references resolved", extra=self.context.session.current_ids())
|
|
524
|
+
else:
|
|
525
|
+
logger.warning("No registry available, skipping tool resolution", extra=self.context.session.current_ids())
|
|
526
|
+
|
|
527
|
+
tool_cache = config_setup_data.build_tool_cache()
|
|
528
|
+
self.context.tool_cache = tool_cache
|
|
529
|
+
logger.info(
|
|
530
|
+
"Tool cache built with %d entries: %s",
|
|
531
|
+
len(tool_cache.entries),
|
|
532
|
+
list(tool_cache.entries.keys()),
|
|
533
|
+
extra=self.context.session.current_ids(),
|
|
534
|
+
)
|
|
535
|
+
|
|
460
536
|
async def start_config_setup(
|
|
461
537
|
self,
|
|
462
538
|
config_setup_data: SetupModelT,
|
|
463
539
|
callback: Callable[[SetupModelT | ModuleCodeModel], Coroutine[Any, Any, None]],
|
|
464
540
|
) -> None:
|
|
465
|
-
"""
|
|
541
|
+
"""Run config setup lifecycle with tool resolution in parallel.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
config_setup_data: Initial setup data to configure.
|
|
545
|
+
callback: Callback to send the configured setup model.
|
|
546
|
+
"""
|
|
466
547
|
try:
|
|
467
|
-
logger.info("Run Config Setup lifecycle")
|
|
548
|
+
logger.info("Run Config Setup lifecycle", extra=self.context.session.current_ids())
|
|
468
549
|
self._status = ModuleStatus.RUNNING
|
|
469
|
-
|
|
550
|
+
self.context.callbacks.set_config_setup = callback
|
|
551
|
+
|
|
552
|
+
# Resolve tools first to populate companion fields, then run config setup
|
|
553
|
+
await self._resolve_tools(config_setup_data)
|
|
554
|
+
updated_config = await self.run_config_setup(self.context, config_setup_data)
|
|
470
555
|
|
|
556
|
+
# Build wrapper: original structure with updated content
|
|
471
557
|
wrapper = config_setup_data.model_dump()
|
|
472
|
-
wrapper["content"] =
|
|
473
|
-
|
|
558
|
+
wrapper["content"] = updated_config.model_dump()
|
|
559
|
+
|
|
560
|
+
# Debug logging
|
|
561
|
+
content = wrapper.get("content", {})
|
|
562
|
+
logger.info(
|
|
563
|
+
"Config setup wrapper: keys=%s, content_keys=%s, tools_cache=%s",
|
|
564
|
+
list(wrapper.keys()),
|
|
565
|
+
list(content.keys()) if isinstance(content, dict) else "N/A",
|
|
566
|
+
content.get("tools_cache") if isinstance(content, dict) else "N/A",
|
|
567
|
+
extra=self.context.session.current_ids(),
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
setup_model = await self.create_setup_model(wrapper)
|
|
571
|
+
await callback(setup_model)
|
|
474
572
|
self._status = ModuleStatus.STOPPING
|
|
475
573
|
except Exception:
|
|
476
574
|
self._status = ModuleStatus.FAILED
|
|
477
|
-
logger.exception("Error during
|
|
575
|
+
logger.exception("Error during config setup lifecycle", extra=self.context.session.current_ids())
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from abc import ABC
|
|
4
4
|
|
|
5
|
-
from digitalkin.models.module import
|
|
5
|
+
from digitalkin.models.module.module_types import (
|
|
6
|
+
InputModelT,
|
|
7
|
+
OutputModelT,
|
|
8
|
+
SecretModelT,
|
|
9
|
+
SetupModelT,
|
|
10
|
+
)
|
|
6
11
|
from digitalkin.modules._base_module import BaseModule
|
|
7
12
|
|
|
8
13
|
|