digitalkin 0.3.2.dev2__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.
Files changed (131) hide show
  1. base_server/__init__.py +1 -0
  2. base_server/mock/__init__.py +5 -0
  3. base_server/mock/mock_pb2.py +39 -0
  4. base_server/mock/mock_pb2_grpc.py +102 -0
  5. base_server/server_async_insecure.py +125 -0
  6. base_server/server_async_secure.py +143 -0
  7. base_server/server_sync_insecure.py +103 -0
  8. base_server/server_sync_secure.py +122 -0
  9. digitalkin/__init__.py +8 -0
  10. digitalkin/__version__.py +8 -0
  11. digitalkin/core/__init__.py +1 -0
  12. digitalkin/core/common/__init__.py +9 -0
  13. digitalkin/core/common/factories.py +156 -0
  14. digitalkin/core/job_manager/__init__.py +1 -0
  15. digitalkin/core/job_manager/base_job_manager.py +288 -0
  16. digitalkin/core/job_manager/single_job_manager.py +354 -0
  17. digitalkin/core/job_manager/taskiq_broker.py +311 -0
  18. digitalkin/core/job_manager/taskiq_job_manager.py +541 -0
  19. digitalkin/core/task_manager/__init__.py +1 -0
  20. digitalkin/core/task_manager/base_task_manager.py +539 -0
  21. digitalkin/core/task_manager/local_task_manager.py +108 -0
  22. digitalkin/core/task_manager/remote_task_manager.py +87 -0
  23. digitalkin/core/task_manager/surrealdb_repository.py +266 -0
  24. digitalkin/core/task_manager/task_executor.py +249 -0
  25. digitalkin/core/task_manager/task_session.py +406 -0
  26. digitalkin/grpc_servers/__init__.py +1 -0
  27. digitalkin/grpc_servers/_base_server.py +486 -0
  28. digitalkin/grpc_servers/module_server.py +208 -0
  29. digitalkin/grpc_servers/module_servicer.py +516 -0
  30. digitalkin/grpc_servers/utils/__init__.py +1 -0
  31. digitalkin/grpc_servers/utils/exceptions.py +29 -0
  32. digitalkin/grpc_servers/utils/grpc_client_wrapper.py +88 -0
  33. digitalkin/grpc_servers/utils/grpc_error_handler.py +53 -0
  34. digitalkin/grpc_servers/utils/utility_schema_extender.py +97 -0
  35. digitalkin/logger.py +157 -0
  36. digitalkin/mixins/__init__.py +19 -0
  37. digitalkin/mixins/base_mixin.py +10 -0
  38. digitalkin/mixins/callback_mixin.py +24 -0
  39. digitalkin/mixins/chat_history_mixin.py +110 -0
  40. digitalkin/mixins/cost_mixin.py +76 -0
  41. digitalkin/mixins/file_history_mixin.py +93 -0
  42. digitalkin/mixins/filesystem_mixin.py +46 -0
  43. digitalkin/mixins/logger_mixin.py +51 -0
  44. digitalkin/mixins/storage_mixin.py +79 -0
  45. digitalkin/models/__init__.py +8 -0
  46. digitalkin/models/core/__init__.py +1 -0
  47. digitalkin/models/core/job_manager_models.py +36 -0
  48. digitalkin/models/core/task_monitor.py +70 -0
  49. digitalkin/models/grpc_servers/__init__.py +1 -0
  50. digitalkin/models/grpc_servers/models.py +275 -0
  51. digitalkin/models/grpc_servers/types.py +24 -0
  52. digitalkin/models/module/__init__.py +25 -0
  53. digitalkin/models/module/module.py +40 -0
  54. digitalkin/models/module/module_context.py +149 -0
  55. digitalkin/models/module/module_types.py +393 -0
  56. digitalkin/models/module/utility.py +146 -0
  57. digitalkin/models/services/__init__.py +10 -0
  58. digitalkin/models/services/cost.py +54 -0
  59. digitalkin/models/services/registry.py +42 -0
  60. digitalkin/models/services/storage.py +44 -0
  61. digitalkin/modules/__init__.py +11 -0
  62. digitalkin/modules/_base_module.py +517 -0
  63. digitalkin/modules/archetype_module.py +23 -0
  64. digitalkin/modules/tool_module.py +23 -0
  65. digitalkin/modules/trigger_handler.py +48 -0
  66. digitalkin/modules/triggers/__init__.py +12 -0
  67. digitalkin/modules/triggers/healthcheck_ping_trigger.py +45 -0
  68. digitalkin/modules/triggers/healthcheck_services_trigger.py +63 -0
  69. digitalkin/modules/triggers/healthcheck_status_trigger.py +52 -0
  70. digitalkin/py.typed +0 -0
  71. digitalkin/services/__init__.py +30 -0
  72. digitalkin/services/agent/__init__.py +6 -0
  73. digitalkin/services/agent/agent_strategy.py +19 -0
  74. digitalkin/services/agent/default_agent.py +13 -0
  75. digitalkin/services/base_strategy.py +22 -0
  76. digitalkin/services/communication/__init__.py +7 -0
  77. digitalkin/services/communication/communication_strategy.py +76 -0
  78. digitalkin/services/communication/default_communication.py +101 -0
  79. digitalkin/services/communication/grpc_communication.py +223 -0
  80. digitalkin/services/cost/__init__.py +14 -0
  81. digitalkin/services/cost/cost_strategy.py +100 -0
  82. digitalkin/services/cost/default_cost.py +114 -0
  83. digitalkin/services/cost/grpc_cost.py +138 -0
  84. digitalkin/services/filesystem/__init__.py +7 -0
  85. digitalkin/services/filesystem/default_filesystem.py +417 -0
  86. digitalkin/services/filesystem/filesystem_strategy.py +252 -0
  87. digitalkin/services/filesystem/grpc_filesystem.py +317 -0
  88. digitalkin/services/identity/__init__.py +6 -0
  89. digitalkin/services/identity/default_identity.py +15 -0
  90. digitalkin/services/identity/identity_strategy.py +14 -0
  91. digitalkin/services/registry/__init__.py +27 -0
  92. digitalkin/services/registry/default_registry.py +141 -0
  93. digitalkin/services/registry/exceptions.py +47 -0
  94. digitalkin/services/registry/grpc_registry.py +306 -0
  95. digitalkin/services/registry/registry_models.py +43 -0
  96. digitalkin/services/registry/registry_strategy.py +98 -0
  97. digitalkin/services/services_config.py +200 -0
  98. digitalkin/services/services_models.py +65 -0
  99. digitalkin/services/setup/__init__.py +1 -0
  100. digitalkin/services/setup/default_setup.py +219 -0
  101. digitalkin/services/setup/grpc_setup.py +343 -0
  102. digitalkin/services/setup/setup_strategy.py +145 -0
  103. digitalkin/services/snapshot/__init__.py +6 -0
  104. digitalkin/services/snapshot/default_snapshot.py +39 -0
  105. digitalkin/services/snapshot/snapshot_strategy.py +30 -0
  106. digitalkin/services/storage/__init__.py +7 -0
  107. digitalkin/services/storage/default_storage.py +228 -0
  108. digitalkin/services/storage/grpc_storage.py +214 -0
  109. digitalkin/services/storage/storage_strategy.py +273 -0
  110. digitalkin/services/user_profile/__init__.py +12 -0
  111. digitalkin/services/user_profile/default_user_profile.py +55 -0
  112. digitalkin/services/user_profile/grpc_user_profile.py +69 -0
  113. digitalkin/services/user_profile/user_profile_strategy.py +40 -0
  114. digitalkin/utils/__init__.py +29 -0
  115. digitalkin/utils/arg_parser.py +92 -0
  116. digitalkin/utils/development_mode_action.py +51 -0
  117. digitalkin/utils/dynamic_schema.py +483 -0
  118. digitalkin/utils/llm_ready_schema.py +75 -0
  119. digitalkin/utils/package_discover.py +357 -0
  120. digitalkin-0.3.2.dev2.dist-info/METADATA +602 -0
  121. digitalkin-0.3.2.dev2.dist-info/RECORD +131 -0
  122. digitalkin-0.3.2.dev2.dist-info/WHEEL +5 -0
  123. digitalkin-0.3.2.dev2.dist-info/licenses/LICENSE +430 -0
  124. digitalkin-0.3.2.dev2.dist-info/top_level.txt +4 -0
  125. modules/__init__.py +0 -0
  126. modules/cpu_intensive_module.py +280 -0
  127. modules/dynamic_setup_module.py +338 -0
  128. modules/minimal_llm_module.py +347 -0
  129. modules/text_transform_module.py +203 -0
  130. services/filesystem_module.py +200 -0
  131. services/storage_module.py +206 -0
@@ -0,0 +1,54 @@
1
+ """Pydantic models for cost service."""
2
+
3
+ from datetime import datetime, timezone
4
+ from enum import Enum
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class CostTypeEnum(Enum):
11
+ """Enumeration of supported cost types."""
12
+
13
+ TOKEN_INPUT = "token_input"
14
+ TOKEN_OUTPUT = "token_output"
15
+ API_CALL = "api_call"
16
+ STORAGE = "storage"
17
+ TIME = "time"
18
+ CUSTOM = "custom"
19
+
20
+
21
+ class CostConfig(BaseModel):
22
+ """Pydantic model that defines a cost configuration.
23
+
24
+ :param cost_name: Name of the cost (unique identifier in the service).
25
+ :param cost_type: The type/category of the cost.
26
+ :param description: A short description of the cost.
27
+ :param unit: The unit of measurement (e.g. token, call, MB).
28
+ :param rate: The cost per unit (e.g. dollars per token).
29
+ """
30
+
31
+ name: str
32
+ type: CostTypeEnum
33
+ description: str | None = None
34
+ unit: str
35
+ rate: float
36
+
37
+
38
+ class CostEvent(BaseModel):
39
+ """Pydantic model that represents a cost event registered during service execution.
40
+
41
+ # DEPRECATED
42
+ :param cost_name: Identifier for the cost configuration.
43
+ :param cost_type: The type of cost.
44
+ :param usage: The amount or units consumed.
45
+ :param cost_amount: The computed cost amount; if not provided it is computed as usage*rate.
46
+ :param timestamp: The time when the cost event was recorded.
47
+ :param metadata: Additional contextual information about the cost event.
48
+ """
49
+
50
+ name: str
51
+ usage: float
52
+ amount: float
53
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
54
+ metadata: dict[str, Any] | None = None
@@ -0,0 +1,42 @@
1
+ """Registry data models."""
2
+
3
+ from enum import Enum
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class RegistryModuleStatus(str, Enum):
9
+ """Module status in the registry."""
10
+
11
+ UNSPECIFIED = "unspecified"
12
+ READY = "ready"
13
+ ACTIVE = "active"
14
+ ARCHIVED = "archived"
15
+
16
+
17
+ class RegistryModuleType(str, Enum):
18
+ """Module type in the registry."""
19
+
20
+ UNSPECIFIED = "unspecified"
21
+ ARCHETYPE = "archetype"
22
+ TOOL = "tool"
23
+
24
+
25
+ class ModuleInfo(BaseModel):
26
+ """Module information from registry."""
27
+
28
+ module_id: str
29
+ module_type: RegistryModuleType
30
+ address: str
31
+ port: int
32
+ version: str
33
+ name: str = ""
34
+ documentation: str | None = None
35
+ status: RegistryModuleStatus | None = None
36
+
37
+
38
+ class ModuleStatusInfo(BaseModel):
39
+ """Module status response."""
40
+
41
+ module_id: str
42
+ status: RegistryModuleStatus
@@ -0,0 +1,44 @@
1
+ """Storage model."""
2
+
3
+ from enum import Enum
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class BaseRole(str, Enum):
10
+ """Officially supported Role Enum for chat messages."""
11
+
12
+ ASSISTANT = "assistant"
13
+ USER = "user"
14
+ SYSTEM = "system"
15
+
16
+
17
+ Role = BaseRole | str
18
+
19
+
20
+ class BaseMessage(BaseModel):
21
+ """Base Model representing a simple message in the chat history."""
22
+
23
+ role: Role = Field(..., description="Role of the message sender")
24
+ content: Any = Field(..., description="The content of the message | preferably a BaseModel.")
25
+
26
+
27
+ class ChatHistory(BaseModel):
28
+ """Storage chat history model for the OpenAI Archetype module."""
29
+
30
+ messages: list[BaseMessage] = Field(..., description="List of messages in the chat history")
31
+
32
+
33
+ class FileModel(BaseModel):
34
+ """File model."""
35
+
36
+ file_id: str = Field(..., description="ID of the file")
37
+ name: str = Field(..., description="Name of the file")
38
+ metadata: dict[str, Any] = Field(..., description="Metadata of the file")
39
+
40
+
41
+ class FileHistory(BaseModel):
42
+ """File history model."""
43
+
44
+ files: list[FileModel] = Field(..., description="List of files")
@@ -0,0 +1,11 @@
1
+ """Module package for DigitalKin."""
2
+
3
+ from digitalkin.modules.archetype_module import ArchetypeModule
4
+ from digitalkin.modules.tool_module import ToolModule
5
+ from digitalkin.modules.trigger_handler import TriggerHandler
6
+
7
+ __all__ = [
8
+ "ArchetypeModule",
9
+ "ToolModule",
10
+ "TriggerHandler",
11
+ ]
@@ -0,0 +1,517 @@
1
+ """BaseModule is the abstract base for all modules in the DigitalKin SDK."""
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ from abc import ABC, abstractmethod
7
+ from collections.abc import Callable, Coroutine
8
+ from typing import Any, ClassVar, Generic
9
+
10
+ from digitalkin.grpc_servers.utils.utility_schema_extender import UtilitySchemaExtender
11
+ from digitalkin.logger import logger
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,
16
+ InputModelT,
17
+ OutputModelT,
18
+ SecretModelT,
19
+ SetupModelT,
20
+ )
21
+ from digitalkin.models.module.utility import EndOfStreamOutput
22
+ from digitalkin.models.services.storage import BaseRole
23
+ from digitalkin.modules.trigger_handler import TriggerHandler
24
+ from digitalkin.services.services_config import ServicesConfig, ServicesStrategy
25
+ from digitalkin.utils.llm_ready_schema import llm_ready_schema
26
+ from digitalkin.utils.package_discover import ModuleDiscoverer
27
+
28
+
29
+ class BaseModule( # noqa: PLR0904
30
+ ABC,
31
+ Generic[
32
+ InputModelT,
33
+ OutputModelT,
34
+ SetupModelT,
35
+ SecretModelT,
36
+ ],
37
+ ):
38
+ """BaseModule is the abstract base for all modules in the DigitalKin SDK."""
39
+
40
+ name: str
41
+ description: str
42
+
43
+ setup_format: type[SetupModelT]
44
+ input_format: type[InputModelT]
45
+ output_format: type[OutputModelT]
46
+ secret_format: type[SecretModelT]
47
+ metadata: ClassVar[dict[str, Any]]
48
+
49
+ context: ModuleContext
50
+ triggers_discoverer: ClassVar[ModuleDiscoverer]
51
+
52
+ # service config params
53
+ services_config_strategies: ClassVar[dict[str, ServicesStrategy | None]] = {}
54
+ services_config_params: ClassVar[dict[str, dict[str, Any | None] | None]] = {}
55
+ services_config: ServicesConfig
56
+
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
+ """
80
+ logger.debug("Service initialisation: %s", self.services_config_strategies.keys())
81
+ return {
82
+ service_name: self.services_config.init_strategy(
83
+ service_name,
84
+ mission_id,
85
+ setup_id,
86
+ setup_version_id,
87
+ )
88
+ for service_name in self.services_config.valid_strategy_names()
89
+ }
90
+
91
+ def __init__(
92
+ self,
93
+ job_id: str,
94
+ mission_id: str,
95
+ setup_id: str,
96
+ setup_version_id: str,
97
+ ) -> None:
98
+ """Initialize the module."""
99
+ self._status = ModuleStatus.CREATED
100
+
101
+ # Initialize minimum context
102
+ self.context = ModuleContext(
103
+ # Initialize services configuration
104
+ **self._init_strategies(mission_id, setup_id, setup_version_id),
105
+ session={
106
+ "setup_id": setup_id,
107
+ "mission_id": mission_id,
108
+ "setup_version_id": setup_version_id,
109
+ "job_id": job_id,
110
+ },
111
+ callbacks={"logger": logger},
112
+ )
113
+
114
+ @property
115
+ def status(self) -> ModuleStatus:
116
+ """Get the module status.
117
+
118
+ Returns:
119
+ The module status
120
+ """
121
+ return self._status
122
+
123
+ @classmethod
124
+ async def get_secret_format(cls, *, llm_format: bool) -> str:
125
+ """Get the JSON schema of the secret format model.
126
+
127
+ Args:
128
+ llm_format: If True, return LLM-optimized schema format with inlined
129
+ references and simplified structure.
130
+
131
+ Returns:
132
+ The JSON schema of the secret format as a JSON string.
133
+
134
+ Raises:
135
+ NotImplementedError: If the `secret_format` class attribute is not defined.
136
+ """
137
+ if cls.secret_format is not None:
138
+ if llm_format:
139
+ return json.dumps(llm_ready_schema(cls.secret_format), indent=2)
140
+ return json.dumps(cls.secret_format.model_json_schema(), indent=2)
141
+ msg = f"{cls.__name__}' class does not define a 'secret_format'."
142
+ raise NotImplementedError(msg)
143
+
144
+ @classmethod
145
+ async def get_input_format(cls, *, llm_format: bool) -> str:
146
+ """Get the JSON schema of the input format model.
147
+
148
+ Args:
149
+ llm_format: If True, return LLM-optimized schema format with inlined
150
+ references and simplified structure.
151
+
152
+ Returns:
153
+ The JSON schema of the input format as a JSON string.
154
+
155
+ Raises:
156
+ NotImplementedError: If the `input_format` class attribute is not defined.
157
+ """
158
+ if cls.input_format is None:
159
+ msg = f"{cls.__name__}' class does not define an 'input_format'."
160
+ raise NotImplementedError(msg)
161
+
162
+ extended_model = UtilitySchemaExtender.create_extended_input_model(cls.input_format)
163
+
164
+ if llm_format:
165
+ return json.dumps(llm_ready_schema(extended_model), indent=2)
166
+ return json.dumps(extended_model.model_json_schema(), indent=2)
167
+
168
+ @classmethod
169
+ async def get_output_format(cls, *, llm_format: bool) -> str:
170
+ """Get the JSON schema of the output format model.
171
+
172
+ Args:
173
+ llm_format: If True, return LLM-optimized schema format with inlined
174
+ references and simplified structure.
175
+
176
+ Returns:
177
+ The JSON schema of the output format as a JSON string.
178
+
179
+ Raises:
180
+ NotImplementedError: If the `output_format` class attribute is not defined.
181
+ """
182
+ if cls.output_format is None:
183
+ msg = f"'{cls.__name__}' class does not define an 'output_format'."
184
+ raise NotImplementedError(msg)
185
+
186
+ extended_model = UtilitySchemaExtender.create_extended_output_model(cls.output_format)
187
+
188
+ if llm_format:
189
+ return json.dumps(llm_ready_schema(extended_model), indent=2)
190
+ return json.dumps(extended_model.model_json_schema(), indent=2)
191
+
192
+ @classmethod
193
+ async def get_config_setup_format(cls, *, llm_format: bool) -> str:
194
+ """Gets the JSON schema of the config setup format model.
195
+
196
+ The config setup format is used only to initialize the module with configuration
197
+ data. It includes fields marked with `json_schema_extra={"config": True}` and
198
+ excludes hidden runtime fields.
199
+
200
+ Dynamic schema fields are always resolved when generating the schema, as this
201
+ method is typically called during module discovery or schema generation where
202
+ fresh values are needed.
203
+
204
+ Args:
205
+ llm_format: If True, return LLM-optimized schema format with inlined
206
+ references and simplified structure.
207
+
208
+ Returns:
209
+ The JSON schema of the config setup format as a JSON string.
210
+
211
+ Raises:
212
+ NotImplementedError: If the `setup_format` class attribute is not defined.
213
+ """
214
+ if cls.setup_format is not None:
215
+ setup_format = await cls.setup_format.get_clean_model(config_fields=True, hidden_fields=False, force=True)
216
+ if llm_format:
217
+ return json.dumps(llm_ready_schema(setup_format), indent=2)
218
+ return json.dumps(setup_format.model_json_schema(), indent=2)
219
+ msg = "'%s' class does not define an 'config_setup_format'."
220
+ raise NotImplementedError(msg)
221
+
222
+ @classmethod
223
+ async def get_setup_format(cls, *, llm_format: bool) -> str:
224
+ """Gets the JSON schema of the setup format model.
225
+
226
+ The setup format is used at runtime and includes hidden fields but excludes
227
+ config-only fields. This is the schema used when running the module.
228
+
229
+ Dynamic schema fields are always resolved when generating the schema, as this
230
+ method is typically called during module discovery or schema generation where
231
+ fresh values are needed.
232
+
233
+ Args:
234
+ llm_format: If True, return LLM-optimized schema format with inlined
235
+ references and simplified structure.
236
+
237
+ Returns:
238
+ The JSON schema of the setup format as a JSON string.
239
+
240
+ Raises:
241
+ NotImplementedError: If the `setup_format` class attribute is not defined.
242
+ """
243
+ if cls.setup_format is not None:
244
+ setup_format = await cls.setup_format.get_clean_model(config_fields=False, hidden_fields=True, force=True)
245
+ if llm_format:
246
+ return json.dumps(llm_ready_schema(setup_format), indent=2)
247
+ return json.dumps(setup_format.model_json_schema(), indent=2)
248
+ msg = "'%s' class does not define an 'setup_format'."
249
+ raise NotImplementedError(msg)
250
+
251
+ @classmethod
252
+ def create_config_setup_model(cls, config_setup_data: dict[str, Any]) -> SetupModelT:
253
+ """Create the setup model from the setup data.
254
+
255
+ Args:
256
+ config_setup_data: The setup data to create the model from.
257
+
258
+ Returns:
259
+ The setup model.
260
+ """
261
+ return cls.setup_format(**config_setup_data)
262
+
263
+ @classmethod
264
+ def create_input_model(cls, input_data: dict[str, Any]) -> InputModelT:
265
+ """Create the input model from the input data.
266
+
267
+ Args:
268
+ input_data: The input data to create the model from.
269
+
270
+ Returns:
271
+ The input model.
272
+ """
273
+ return cls.input_format(**input_data)
274
+
275
+ @classmethod
276
+ async def create_setup_model(cls, setup_data: dict[str, Any], *, config_fields: bool = False) -> SetupModelT:
277
+ """Create the setup model from the setup data.
278
+
279
+ Creates a filtered setup model instance based on the provided data.
280
+ Uses `get_clean_model()` internally to get the appropriate model class
281
+ with field filtering applied.
282
+
283
+ Args:
284
+ setup_data: The setup data to create the model from.
285
+ config_fields: If True, include only fields with json_schema_extra["config"] == True.
286
+
287
+ Returns:
288
+ An instance of the setup model with the provided data.
289
+ """
290
+ model_cls = await cls.setup_format.get_clean_model(config_fields=config_fields, hidden_fields=True)
291
+ return model_cls(**setup_data)
292
+
293
+ @classmethod
294
+ def create_secret_model(cls, secret_data: dict[str, Any]) -> SecretModelT:
295
+ """Create the secret model from the secret data.
296
+
297
+ Args:
298
+ secret_data: The secret data to create the model from.
299
+
300
+ Returns:
301
+ The secret model.
302
+ """
303
+ return cls.secret_format(**secret_data)
304
+
305
+ @classmethod
306
+ def create_output_model(cls, output_data: dict[str, Any]) -> OutputModelT:
307
+ """Create the output model from the output data.
308
+
309
+ Args:
310
+ output_data: The output data to create the model from.
311
+
312
+ Returns:
313
+ The output model.
314
+ """
315
+ return cls.output_format(**output_data)
316
+
317
+ @classmethod
318
+ def discover(cls) -> None:
319
+ """Discover and register all TriggerHandler subclasses in the specified package or current directory.
320
+
321
+ Dynamically import all Python modules in the specified package or current directory,
322
+ triggering class registrations for subclasses of TriggerHandler whose names end with 'Trigger'.
323
+
324
+ If a package is provided, all .py files within its path are imported; otherwise, the current
325
+ working directory is searched. For each imported module, any class matching the criteria is
326
+ registered via cls.register(). Errors during import are logged at debug level.
327
+
328
+ Built-in healthcheck handlers (ping, services, status) are automatically registered
329
+ to provide standard healthcheck functionality for all modules.
330
+ """
331
+ from digitalkin.models.module.utility import UtilityRegistry # noqa: PLC0415
332
+
333
+ cls.triggers_discoverer.discover_modules()
334
+
335
+ # Auto-register built-in SDK triggers (healthcheck, etc.)
336
+ for trigger_cls in UtilityRegistry.get_builtin_triggers():
337
+ cls.triggers_discoverer.register_trigger(trigger_cls)
338
+
339
+ logger.debug("discovered: %s", cls.triggers_discoverer)
340
+
341
+ @classmethod
342
+ def register(cls, handler_cls: type[TriggerHandler]) -> type[TriggerHandler]:
343
+ """Dynamically register the trigger class.
344
+
345
+ Args:
346
+ handler_cls: type of the trigger handler to register.
347
+
348
+ Returns:
349
+ type of the trigger handler.
350
+ """
351
+ return cls.triggers_discoverer.register_trigger(handler_cls)
352
+
353
+ async def run_config_setup( # noqa: PLR6301
354
+ self,
355
+ context: ModuleContext, # noqa: ARG002
356
+ config_setup_data: SetupModelT,
357
+ ) -> SetupModelT:
358
+ """Run config setup the module.
359
+
360
+ The config setup is used to initialize the setup with configuration data.
361
+ This method is typically used to set up the module with necessary configuration before running it,
362
+ especially for processing data like files.
363
+ The function needs to save the setup in the storage.
364
+ The module will be initialize with the setup and not the config setup.
365
+ This method is optional, the config setup and setup can be the same.
366
+
367
+ Returns:
368
+ The updated setup model after running the config setup.
369
+ """
370
+ return config_setup_data
371
+
372
+ @abstractmethod
373
+ async def initialize(self, context: ModuleContext, setup_data: SetupModelT) -> None:
374
+ """Initialize the module."""
375
+ raise NotImplementedError
376
+
377
+ async def run(
378
+ self,
379
+ input_data: InputModelT,
380
+ setup_data: SetupModelT,
381
+ ) -> None:
382
+ """Run the module with the given input and setup data.
383
+
384
+ This method validates the input data, determines the protocol from the input,
385
+ and dispatches the request to the corresponding trigger handler. The trigger handler
386
+ is responsible for processing the input and invoking the callback with the result.
387
+
388
+ Triggers:
389
+ - The method is triggered when a module run is requested with specific input and setup data.
390
+ - The protocol specified in the input determines which trigger handler is invoked.
391
+
392
+ Args:
393
+ input_data (InputModelT): The input data to be processed by the module.
394
+ setup_data (SetupModelT): The setup or configuration data required for the module.
395
+
396
+ Raises:
397
+ ValueError: If no handler for the protocol is found.
398
+ """
399
+ input_instance = self.input_format.model_validate(input_data)
400
+ handler_instance = self.triggers_discoverer.get_trigger(
401
+ input_instance.root.protocol,
402
+ input_instance.root,
403
+ )
404
+
405
+ await handler_instance.handle(
406
+ input_instance.root,
407
+ setup_data,
408
+ self.context,
409
+ )
410
+
411
+ @abstractmethod
412
+ async def cleanup(self) -> None:
413
+ """Run the module."""
414
+ raise NotImplementedError
415
+
416
+ async def _run_lifecycle(
417
+ self,
418
+ input_data: InputModelT,
419
+ setup_data: SetupModelT,
420
+ ) -> None:
421
+ """Run the module lifecycle.
422
+
423
+ Raises:
424
+ asyncio.CancelledError: If the module is cancelled
425
+ """
426
+ try:
427
+ logger.info("Starting module %s", self.name, extra=self.context.session.current_ids())
428
+ await self.run(input_data, setup_data)
429
+ logger.info("Module %s finished", self.name, extra=self.context.session.current_ids())
430
+ except asyncio.CancelledError:
431
+ self._status = ModuleStatus.CANCELLED
432
+ logger.error("Module %s cancelled", self.name, extra=self.context.session.current_ids())
433
+ except Exception:
434
+ self._status = ModuleStatus.FAILED
435
+ logger.exception("Error inside module %s", self.name, extra=self.context.session.current_ids())
436
+ else:
437
+ self._status = ModuleStatus.STOPPING
438
+
439
+ async def start(
440
+ self,
441
+ input_data: InputModelT,
442
+ setup_data: SetupModelT,
443
+ callback: Callable[[OutputModelT | ModuleCodeModel], Coroutine[Any, Any, None]],
444
+ done_callback: Callable | None = None,
445
+ ) -> None:
446
+ """Start the module."""
447
+ try:
448
+ self.context.callbacks.send_message = callback
449
+ logger.info(f"Inititalize module {self.context.session.job_id}")
450
+ await self.initialize(self.context, setup_data)
451
+ except Exception as e:
452
+ self._status = ModuleStatus.FAILED
453
+ short_description = "Error initializing module"
454
+ logger.exception("%s: %s", short_description, e)
455
+ await callback(
456
+ ModuleCodeModel(
457
+ code="Error",
458
+ short_description=short_description,
459
+ message=str(e),
460
+ )
461
+ )
462
+ if done_callback is not None:
463
+ await done_callback(None)
464
+ await self.stop()
465
+ return
466
+
467
+ try:
468
+ logger.debug("Init the discovered input handlers.")
469
+ self.triggers_discoverer.init_handlers(self.context)
470
+ logger.debug(f"Run lifecycle {self.context.session.job_id}")
471
+ await self._run_lifecycle(input_data, setup_data)
472
+ except Exception:
473
+ self._status = ModuleStatus.FAILED
474
+ logger.exception("Error during module lifecyle")
475
+ finally:
476
+ await self.stop()
477
+
478
+ async def stop(self) -> None:
479
+ """Stop the module."""
480
+ logger.info("Stopping module %s | job_id=%s", self.name, self.context.session.job_id)
481
+ try:
482
+ self._status = ModuleStatus.STOPPING
483
+ logger.debug("Module %s stopped", self.name)
484
+ await self.cleanup()
485
+ await self.context.callbacks.send_message(
486
+ DataModel(
487
+ root=EndOfStreamOutput(),
488
+ annotations={"role": BaseRole.SYSTEM},
489
+ )
490
+ )
491
+ self._status = ModuleStatus.STOPPED
492
+ logger.debug("Module %s cleaned", self.name)
493
+ except Exception:
494
+ self._status = ModuleStatus.FAILED
495
+ logger.exception("Error stopping module")
496
+
497
+ async def start_config_setup(
498
+ self,
499
+ config_setup_data: SetupModelT,
500
+ callback: Callable[[SetupModelT | ModuleCodeModel], Coroutine[Any, Any, None]],
501
+ ) -> None:
502
+ """Start the module."""
503
+ try:
504
+ logger.info("Run Config Setup lifecycle", extra=self.context.session.current_ids())
505
+ self._status = ModuleStatus.RUNNING
506
+ self.context.callbacks.set_config_setup = callback
507
+ content = await self.run_config_setup(self.context, config_setup_data)
508
+
509
+ wrapper = config_setup_data.model_dump()
510
+ wrapper["content"] = content.model_dump()
511
+ setup_model = await self.create_setup_model(wrapper)
512
+ await callback(setup_model)
513
+ self._status = ModuleStatus.STOPPING
514
+ except Exception:
515
+ logger.error("Error during module lifecyle")
516
+ self._status = ModuleStatus.FAILED
517
+ logger.exception("Error during module lifecyle", extra=self.context.session.current_ids())
@@ -0,0 +1,23 @@
1
+ """ArchetypeModule extends BaseModule to implement specific module types."""
2
+
3
+ from abc import ABC
4
+
5
+ from digitalkin.models.module.module_types import (
6
+ InputModelT,
7
+ OutputModelT,
8
+ SecretModelT,
9
+ SetupModelT,
10
+ )
11
+ from digitalkin.modules._base_module import BaseModule
12
+
13
+
14
+ class ArchetypeModule(
15
+ BaseModule[
16
+ InputModelT,
17
+ OutputModelT,
18
+ SetupModelT,
19
+ SecretModelT,
20
+ ],
21
+ ABC,
22
+ ):
23
+ """ArchetypeModule extends BaseModule to implement specific module types."""