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.
Files changed (122) hide show
  1. base_server/server_async_insecure.py +6 -5
  2. base_server/server_async_secure.py +6 -5
  3. base_server/server_sync_insecure.py +5 -4
  4. base_server/server_sync_secure.py +5 -4
  5. digitalkin/__version__.py +1 -1
  6. digitalkin/core/__init__.py +1 -0
  7. digitalkin/core/common/__init__.py +9 -0
  8. digitalkin/core/common/factories.py +156 -0
  9. digitalkin/core/job_manager/__init__.py +1 -0
  10. digitalkin/{modules → core}/job_manager/base_job_manager.py +138 -32
  11. digitalkin/core/job_manager/single_job_manager.py +373 -0
  12. digitalkin/{modules → core}/job_manager/taskiq_broker.py +121 -26
  13. digitalkin/core/job_manager/taskiq_job_manager.py +541 -0
  14. digitalkin/core/task_manager/__init__.py +1 -0
  15. digitalkin/core/task_manager/base_task_manager.py +539 -0
  16. digitalkin/core/task_manager/local_task_manager.py +108 -0
  17. digitalkin/core/task_manager/remote_task_manager.py +87 -0
  18. digitalkin/core/task_manager/surrealdb_repository.py +266 -0
  19. digitalkin/core/task_manager/task_executor.py +249 -0
  20. digitalkin/core/task_manager/task_session.py +368 -0
  21. digitalkin/grpc_servers/__init__.py +1 -19
  22. digitalkin/grpc_servers/_base_server.py +3 -3
  23. digitalkin/grpc_servers/module_server.py +120 -195
  24. digitalkin/grpc_servers/module_servicer.py +81 -44
  25. digitalkin/grpc_servers/utils/__init__.py +1 -0
  26. digitalkin/grpc_servers/utils/exceptions.py +0 -8
  27. digitalkin/grpc_servers/utils/grpc_client_wrapper.py +25 -9
  28. digitalkin/grpc_servers/utils/grpc_error_handler.py +53 -0
  29. digitalkin/grpc_servers/utils/utility_schema_extender.py +100 -0
  30. digitalkin/logger.py +64 -27
  31. digitalkin/mixins/__init__.py +19 -0
  32. digitalkin/mixins/base_mixin.py +10 -0
  33. digitalkin/mixins/callback_mixin.py +24 -0
  34. digitalkin/mixins/chat_history_mixin.py +110 -0
  35. digitalkin/mixins/cost_mixin.py +76 -0
  36. digitalkin/mixins/file_history_mixin.py +93 -0
  37. digitalkin/mixins/filesystem_mixin.py +46 -0
  38. digitalkin/mixins/logger_mixin.py +51 -0
  39. digitalkin/mixins/storage_mixin.py +79 -0
  40. digitalkin/models/__init__.py +1 -1
  41. digitalkin/models/core/__init__.py +1 -0
  42. digitalkin/{modules/job_manager → models/core}/job_manager_models.py +3 -11
  43. digitalkin/models/core/task_monitor.py +74 -0
  44. digitalkin/models/grpc_servers/__init__.py +1 -0
  45. digitalkin/{grpc_servers/utils → models/grpc_servers}/models.py +92 -7
  46. digitalkin/models/module/__init__.py +18 -11
  47. digitalkin/models/module/base_types.py +61 -0
  48. digitalkin/models/module/module.py +9 -1
  49. digitalkin/models/module/module_context.py +282 -6
  50. digitalkin/models/module/module_types.py +29 -105
  51. digitalkin/models/module/setup_types.py +490 -0
  52. digitalkin/models/module/tool_cache.py +68 -0
  53. digitalkin/models/module/tool_reference.py +117 -0
  54. digitalkin/models/module/utility.py +167 -0
  55. digitalkin/models/services/__init__.py +9 -0
  56. digitalkin/models/services/cost.py +1 -0
  57. digitalkin/models/services/registry.py +35 -0
  58. digitalkin/models/services/storage.py +39 -5
  59. digitalkin/modules/__init__.py +5 -1
  60. digitalkin/modules/_base_module.py +265 -167
  61. digitalkin/modules/archetype_module.py +6 -1
  62. digitalkin/modules/tool_module.py +16 -3
  63. digitalkin/modules/trigger_handler.py +7 -6
  64. digitalkin/modules/triggers/__init__.py +8 -0
  65. digitalkin/modules/triggers/healthcheck_ping_trigger.py +45 -0
  66. digitalkin/modules/triggers/healthcheck_services_trigger.py +63 -0
  67. digitalkin/modules/triggers/healthcheck_status_trigger.py +52 -0
  68. digitalkin/services/__init__.py +4 -0
  69. digitalkin/services/communication/__init__.py +7 -0
  70. digitalkin/services/communication/communication_strategy.py +76 -0
  71. digitalkin/services/communication/default_communication.py +101 -0
  72. digitalkin/services/communication/grpc_communication.py +234 -0
  73. digitalkin/services/cost/__init__.py +9 -2
  74. digitalkin/services/cost/grpc_cost.py +9 -42
  75. digitalkin/services/filesystem/default_filesystem.py +0 -2
  76. digitalkin/services/filesystem/grpc_filesystem.py +10 -39
  77. digitalkin/services/registry/__init__.py +22 -1
  78. digitalkin/services/registry/default_registry.py +135 -4
  79. digitalkin/services/registry/exceptions.py +47 -0
  80. digitalkin/services/registry/grpc_registry.py +306 -0
  81. digitalkin/services/registry/registry_models.py +15 -0
  82. digitalkin/services/registry/registry_strategy.py +88 -4
  83. digitalkin/services/services_config.py +25 -3
  84. digitalkin/services/services_models.py +5 -1
  85. digitalkin/services/setup/default_setup.py +6 -7
  86. digitalkin/services/setup/grpc_setup.py +52 -15
  87. digitalkin/services/storage/grpc_storage.py +4 -4
  88. digitalkin/services/user_profile/__init__.py +12 -0
  89. digitalkin/services/user_profile/default_user_profile.py +55 -0
  90. digitalkin/services/user_profile/grpc_user_profile.py +69 -0
  91. digitalkin/services/user_profile/user_profile_strategy.py +25 -0
  92. digitalkin/utils/__init__.py +28 -0
  93. digitalkin/utils/arg_parser.py +1 -1
  94. digitalkin/utils/development_mode_action.py +2 -2
  95. digitalkin/utils/dynamic_schema.py +483 -0
  96. digitalkin/utils/package_discover.py +1 -2
  97. digitalkin/utils/schema_splitter.py +207 -0
  98. {digitalkin-0.2.25rc0.dist-info → digitalkin-0.3.2.dev14.dist-info}/METADATA +11 -30
  99. digitalkin-0.3.2.dev14.dist-info/RECORD +143 -0
  100. {digitalkin-0.2.25rc0.dist-info → digitalkin-0.3.2.dev14.dist-info}/top_level.txt +1 -0
  101. modules/archetype_with_tools_module.py +244 -0
  102. modules/cpu_intensive_module.py +1 -1
  103. modules/dynamic_setup_module.py +338 -0
  104. modules/minimal_llm_module.py +1 -1
  105. modules/text_transform_module.py +1 -1
  106. monitoring/digitalkin_observability/__init__.py +46 -0
  107. monitoring/digitalkin_observability/http_server.py +150 -0
  108. monitoring/digitalkin_observability/interceptors.py +176 -0
  109. monitoring/digitalkin_observability/metrics.py +201 -0
  110. monitoring/digitalkin_observability/prometheus.py +137 -0
  111. monitoring/tests/test_metrics.py +172 -0
  112. services/filesystem_module.py +7 -5
  113. services/storage_module.py +4 -2
  114. digitalkin/grpc_servers/registry_server.py +0 -65
  115. digitalkin/grpc_servers/registry_servicer.py +0 -456
  116. digitalkin/grpc_servers/utils/factory.py +0 -180
  117. digitalkin/modules/job_manager/single_job_manager.py +0 -294
  118. digitalkin/modules/job_manager/taskiq_job_manager.py +0 -290
  119. digitalkin-0.2.25rc0.dist-info/RECORD +0 -89
  120. /digitalkin/{grpc_servers/utils → models/grpc_servers}/types.py +0 -0
  121. {digitalkin-0.2.25rc0.dist-info → digitalkin-0.3.2.dev14.dist-info}/WHEEL +0 -0
  122. {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 pydantic import BaseModel
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.module_context import ModuleContext
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
- # services list
71
- agent: AgentStrategy
72
- cost: CostStrategy
73
- filesystem: FilesystemStrategy
74
- identity: IdentityStrategy
75
- registry: RegistryStrategy
76
- snapshot: SnapshotStrategy
77
- storage: StorageStrategy
78
-
79
- # runtime params
80
- job_id: str
81
- mission_id: str
82
- setup_id: str
83
- setup_version_id: str
84
- _status: ModuleStatus
85
- _task: asyncio.Task | None
86
-
87
- def _init_strategies(self) -> None:
88
- """Initialize the services configuration."""
89
- for service_name in self.services_config.valid_strategy_names():
90
- service = self.services_config.init_strategy(
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
- self.mission_id,
93
- self.setup_id,
94
- self.setup_version_id,
85
+ mission_id,
86
+ setup_id,
87
+ setup_version_id,
95
88
  )
96
- setattr(self, service_name, service)
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
- storage=self.storage,
120
- cost=self.cost,
121
- filesystem=self.filesystem,
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
- Raises:
138
- NotImplementedError: If the `secret_format` is not defined.
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
- return json.dumps(llm_ready_schema(cls.secret_format), indent=2)
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
- Raises:
155
- NotImplementedError: If the `input_format` is not defined.
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 not None:
161
- if llm_format:
162
- return json.dumps(llm_ready_schema(cls.input_format), indent=2)
163
- return json.dumps(cls.input_format.model_json_schema(), indent=2)
164
- msg = f"{cls.__name__}' class does not define an 'input_format'."
165
- raise NotImplementedError(msg)
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
- Raises:
172
- NotImplementedError: If the `output_format` is not defined.
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 not None:
178
- if llm_format:
179
- return json.dumps(llm_ready_schema(cls.output_format), indent=2)
180
- return json.dumps(cls.output_format.model_json_schema(), indent=2)
181
- msg = "'%s' class does not define an 'output_format'."
182
- raise NotImplementedError(msg)
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 data.
189
- The setup format is used to initialize an run the module with setup data.
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
- Raises:
192
- NotImplementedError: If the `setup_format` is not defined.
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
- return json.dumps(llm_ready_schema(setup_format), indent=2)
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
- Raises:
210
- NotImplementedError: If the `setup_format` is not defined.
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
- return json.dumps(llm_ready_schema(setup_format), indent=2)
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
- The setup model.
294
+ An instance of the setup model with the provided data.
257
295
  """
258
- return cls.setup_format.get_clean_model(config_fields=config_fields, hidden_fields=True)(**setup_data)
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 with the given input and setup data.
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 (InputModelT): The input data to be processed by the module.
348
- setup_data (SetupModelT): The setup or configuration data required for the module.
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, callback)
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(f"Module {self.name} cancelled")
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
- logger.debug("Inititalize module")
408
- await self.initialize(setup_data=setup_data)
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=str(self._status),
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._status = ModuleStatus.RUNNING
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 with status %s", self.name, self._status)
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
- """Start the module."""
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
- content = await self.run_config_setup(config_setup_data)
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"] = content.model_dump()
473
- await callback(self.create_setup_model(wrapper))
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 module lifecyle")
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 InputModelT, OutputModelT, SecretModelT, SetupModelT
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