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,516 @@
1
+ """Module servicer implementation for DigitalKin."""
2
+
3
+ from argparse import ArgumentParser, Namespace
4
+ from collections.abc import AsyncGenerator
5
+ from typing import Any
6
+
7
+ import grpc
8
+ from agentic_mesh_protocol.module.v1 import (
9
+ information_pb2,
10
+ lifecycle_pb2,
11
+ module_service_pb2_grpc,
12
+ monitoring_pb2,
13
+ )
14
+ from google.protobuf import json_format, struct_pb2
15
+
16
+ from digitalkin.core.job_manager.base_job_manager import BaseJobManager
17
+ from digitalkin.grpc_servers.utils.exceptions import ServicerError
18
+ from digitalkin.logger import logger
19
+ from digitalkin.models.core.job_manager_models import JobManagerMode
20
+ from digitalkin.models.module.module import ModuleStatus
21
+ from digitalkin.modules._base_module import BaseModule
22
+ from digitalkin.services.services_models import ServicesMode
23
+ from digitalkin.services.setup.default_setup import DefaultSetup
24
+ from digitalkin.services.setup.grpc_setup import GrpcSetup
25
+ from digitalkin.services.setup.setup_strategy import SetupStrategy
26
+ from digitalkin.utils.arg_parser import ArgParser
27
+ from digitalkin.utils.development_mode_action import DevelopmentModeMappingAction
28
+
29
+
30
+ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
31
+ """Implementation of the ModuleService.
32
+
33
+ This servicer handles interactions with a DigitalKin module.
34
+
35
+ Attributes:
36
+ module: The module instance being served.
37
+ active_jobs: Dictionary tracking active module jobs.
38
+ """
39
+
40
+ args: Namespace
41
+ setup: SetupStrategy
42
+ job_manager: BaseJobManager
43
+
44
+ def _add_parser_args(self, parser: ArgumentParser) -> None:
45
+ super()._add_parser_args(parser)
46
+ parser.add_argument(
47
+ "-d",
48
+ "--dev-mode",
49
+ env_var="SERVICE_MODE",
50
+ choices=ServicesMode.__members__,
51
+ default="local",
52
+ action=DevelopmentModeMappingAction,
53
+ dest="services_mode",
54
+ help="Define Module Service configurations for endpoints",
55
+ )
56
+ parser.add_argument(
57
+ "-jm",
58
+ "--job-manager",
59
+ type=JobManagerMode,
60
+ choices=list(JobManagerMode),
61
+ default=JobManagerMode.SINGLE,
62
+ dest="job_manager_mode",
63
+ help="Define Module job manager configurations for load balancing",
64
+ )
65
+
66
+ def __init__(self, module_class: type[BaseModule]) -> None:
67
+ """Initialize the module servicer.
68
+
69
+ Args:
70
+ module_class: The module type to serve.
71
+ """
72
+ super().__init__()
73
+ module_class.discover()
74
+ self.module_class = module_class
75
+ job_manager_class = self.args.job_manager_mode.get_manager_class()
76
+ self.job_manager = job_manager_class(module_class, self.args.services_mode)
77
+
78
+ logger.debug(
79
+ "ModuleServicer initialized with job manager: %s",
80
+ self.args.job_manager_mode,
81
+ extra={"job_manager": self.job_manager},
82
+ )
83
+ self.setup = GrpcSetup() if self.args.services_mode == ServicesMode.REMOTE else DefaultSetup()
84
+
85
+ async def ConfigSetupModule( # noqa: N802
86
+ self,
87
+ request: lifecycle_pb2.ConfigSetupModuleRequest,
88
+ context: grpc.aio.ServicerContext,
89
+ ) -> lifecycle_pb2.ConfigSetupModuleResponse:
90
+ """Configure the module setup.
91
+
92
+ Args:
93
+ request: The configuration request.
94
+ context: The gRPC context.
95
+
96
+ Returns:
97
+ A response indicating success or failure.
98
+
99
+ Raises:
100
+ ServicerError: if the setup data is not returned or job creation fails.
101
+ """
102
+ logger.info(
103
+ "ConfigSetupVersion called for module: '%s'",
104
+ self.module_class.__name__,
105
+ extra={
106
+ "module_class": self.module_class,
107
+ "setup_version": request.setup_version,
108
+ "mission_id": request.mission_id,
109
+ },
110
+ )
111
+ # Process the module input
112
+ # TODO: Secret should be used here as well
113
+ setup_version = request.setup_version
114
+ config_setup_data = self.module_class.create_config_setup_model(json_format.MessageToDict(request.content))
115
+ setup_version_data = await self.module_class.create_setup_model(
116
+ json_format.MessageToDict(request.setup_version.content),
117
+ config_fields=True,
118
+ )
119
+
120
+ if not setup_version_data:
121
+ msg = "No setup data returned."
122
+ raise ServicerError(msg)
123
+
124
+ if not config_setup_data:
125
+ msg = "No config setup data returned."
126
+ raise ServicerError(msg)
127
+
128
+ # create a task to run the module in background
129
+ job_id = await self.job_manager.create_config_setup_instance_job(
130
+ config_setup_data,
131
+ request.mission_id,
132
+ setup_version.setup_id,
133
+ setup_version.id,
134
+ )
135
+
136
+ if job_id is None:
137
+ context.set_code(grpc.StatusCode.NOT_FOUND)
138
+ context.set_details("Failed to create module instance")
139
+ return lifecycle_pb2.ConfigSetupModuleResponse(success=False)
140
+
141
+ updated_setup_data = await self.job_manager.generate_config_setup_module_response(job_id)
142
+ logger.info("Setup updated")
143
+ logger.debug(f"Updated setup data: {updated_setup_data=}")
144
+ setup_version.content = json_format.ParseDict(
145
+ updated_setup_data,
146
+ struct_pb2.Struct(),
147
+ ignore_unknown_fields=True,
148
+ )
149
+ return lifecycle_pb2.ConfigSetupModuleResponse(success=True, setup_version=setup_version)
150
+
151
+ async def StartModule( # noqa: N802
152
+ self,
153
+ request: lifecycle_pb2.StartModuleRequest,
154
+ context: grpc.aio.ServicerContext,
155
+ ) -> AsyncGenerator[lifecycle_pb2.StartModuleResponse, Any]:
156
+ """Start a module execution.
157
+
158
+ Args:
159
+ request: Iterator of start module requests.
160
+ context: The gRPC context.
161
+
162
+ Yields:
163
+ Responses during module execution.
164
+
165
+ Raises:
166
+ ServicerError: the necessary query didn't work.
167
+ """
168
+ logger.info(
169
+ "StartModule called for module: '%s'",
170
+ self.module_class.__name__,
171
+ extra={"module_class": self.module_class, "setup_id": request.setup_id, "mission_id": request.mission_id},
172
+ )
173
+ # Process the module input
174
+ # TODO: Check failure of input data format
175
+ input_data = self.module_class.create_input_model(json_format.MessageToDict(request.input))
176
+
177
+ setup_data_class = self.setup.get_setup(
178
+ setup_dict={
179
+ "setup_id": request.setup_id,
180
+ "mission_id": request.mission_id,
181
+ }
182
+ )
183
+
184
+ if not setup_data_class:
185
+ msg = "No setup data returned."
186
+ raise ServicerError(msg)
187
+
188
+ setup_data = await self.module_class.create_setup_model(setup_data_class.current_setup_version.content)
189
+
190
+ # create a task to run the module in background
191
+ job_id = await self.job_manager.create_module_instance_job(
192
+ input_data,
193
+ setup_data,
194
+ mission_id=request.mission_id,
195
+ setup_id=setup_data_class.current_setup_version.setup_id,
196
+ setup_version_id=setup_data_class.current_setup_version.id,
197
+ )
198
+
199
+ if job_id is None:
200
+ context.set_code(grpc.StatusCode.NOT_FOUND)
201
+ context.set_details("Failed to create module instance")
202
+ yield lifecycle_pb2.StartModuleResponse(success=False)
203
+ return
204
+
205
+ try:
206
+ async with self.job_manager.generate_stream_consumer(job_id) as stream: # type: ignore
207
+ async for message in stream:
208
+ if message.get("error", None) is not None:
209
+ logger.error("Error in output_data", extra={"message": message})
210
+ context.set_code(message["error"]["code"])
211
+ context.set_details(message["error"]["error_message"])
212
+ yield lifecycle_pb2.StartModuleResponse(success=False, job_id=job_id)
213
+ break
214
+
215
+ if message.get("exception", None) is not None:
216
+ logger.error("Exception in output_data", extra={"message": message})
217
+ context.set_code(message["short_description"])
218
+ context.set_details(message["exception"])
219
+ yield lifecycle_pb2.StartModuleResponse(success=False, job_id=job_id)
220
+ break
221
+
222
+ logger.info("Yielding message from job %s: %s", job_id, message)
223
+ proto = json_format.ParseDict(message, struct_pb2.Struct(), ignore_unknown_fields=True)
224
+ yield lifecycle_pb2.StartModuleResponse(success=True, output=proto, job_id=job_id)
225
+
226
+ if message.get("root", {}).get("protocol") == "end_of_stream":
227
+ logger.info(
228
+ "End of stream signal received",
229
+ extra={"job_id": job_id, "mission_id": request.mission_id},
230
+ )
231
+ break
232
+ finally:
233
+ await self.job_manager.wait_for_completion(job_id)
234
+ await self.job_manager.clean_session(job_id, mission_id=request.mission_id)
235
+
236
+ logger.info("Job %s finished", job_id)
237
+
238
+ async def StopModule( # noqa: N802
239
+ self,
240
+ request: lifecycle_pb2.StopModuleRequest,
241
+ context: grpc.ServicerContext,
242
+ ) -> lifecycle_pb2.StopModuleResponse:
243
+ """Stop a running module execution.
244
+
245
+ Args:
246
+ request: The stop module request.
247
+ context: The gRPC context.
248
+
249
+ Returns:
250
+ A response indicating success or failure.
251
+ """
252
+ logger.debug("StopModule called for module: '%s'", self.module_class.__name__)
253
+
254
+ response: bool = await self.job_manager.stop_module(request.job_id)
255
+ if not response:
256
+ message = f"Job {request.job_id} not found"
257
+ logger.warning(message)
258
+ context.set_code(grpc.StatusCode.NOT_FOUND)
259
+ context.set_details(message)
260
+ return lifecycle_pb2.StopModuleResponse(success=False)
261
+
262
+ logger.debug("Job %s stopped successfully", request.job_id, extra={"job_id": request.job_id})
263
+ return lifecycle_pb2.StopModuleResponse(success=True)
264
+
265
+ async def GetModuleStatus( # noqa: N802
266
+ self,
267
+ request: monitoring_pb2.GetModuleStatusRequest,
268
+ context: grpc.ServicerContext,
269
+ ) -> monitoring_pb2.GetModuleStatusResponse:
270
+ """Get the status of a module.
271
+
272
+ Args:
273
+ request: The get module status request.
274
+ context: The gRPC context.
275
+
276
+ Returns:
277
+ A response with the module status.
278
+ """
279
+ logger.debug("GetModuleStatus called for module: '%s'", self.module_class.__name__)
280
+
281
+ if not request.job_id:
282
+ logger.debug("Job %s status: '%s'", request.job_id, ModuleStatus.NOT_FOUND)
283
+ return monitoring_pb2.GetModuleStatusResponse(
284
+ success=False,
285
+ status=ModuleStatus.NOT_FOUND.name,
286
+ job_id=request.job_id,
287
+ )
288
+
289
+ status = await self.job_manager.get_module_status(request.job_id)
290
+
291
+ if status is None:
292
+ message = f"Job {request.job_id} not found"
293
+ logger.warning(message)
294
+ context.set_code(grpc.StatusCode.NOT_FOUND)
295
+ context.set_details(message)
296
+ return monitoring_pb2.GetModuleStatusResponse()
297
+
298
+ logger.debug("Job %s status: '%s'", request.job_id, status)
299
+ return monitoring_pb2.GetModuleStatusResponse(
300
+ success=True,
301
+ status=status.name,
302
+ job_id=request.job_id,
303
+ )
304
+
305
+ async def GetModuleJobs( # noqa: N802
306
+ self,
307
+ request: monitoring_pb2.GetModuleJobsRequest, # noqa: ARG002
308
+ context: grpc.ServicerContext, # noqa: ARG002
309
+ ) -> monitoring_pb2.GetModuleJobsResponse:
310
+ """Get information about the module's jobs.
311
+
312
+ Args:
313
+ request: The get module jobs request.
314
+ context: The gRPC context.
315
+
316
+ Returns:
317
+ A response with information about active jobs.
318
+ """
319
+ logger.debug("GetModuleJobs called for module: '%s'", self.module_class.__name__)
320
+
321
+ modules = await self.job_manager.list_modules()
322
+
323
+ # Create job info objects for each active job
324
+ return monitoring_pb2.GetModuleJobsResponse(
325
+ jobs=[
326
+ monitoring_pb2.JobInfo(
327
+ job_id=job_id,
328
+ job_status=job_data["status"].name,
329
+ )
330
+ for job_id, job_data in modules.items()
331
+ ],
332
+ )
333
+
334
+ async def GetModuleInput( # noqa: N802
335
+ self,
336
+ request: information_pb2.GetModuleInputRequest,
337
+ context: grpc.ServicerContext,
338
+ ) -> information_pb2.GetModuleInputResponse:
339
+ """Get information about the module's expected input.
340
+
341
+ Args:
342
+ request: The get module input request.
343
+ context: The gRPC context.
344
+
345
+ Returns:
346
+ A response with the module's input schema.
347
+ """
348
+ logger.debug("GetModuleInput called for module: '%s'", self.module_class.__name__)
349
+
350
+ # Get input schema if available
351
+ try:
352
+ # Convert schema to proto format
353
+ input_schema_proto = await self.module_class.get_input_format(
354
+ llm_format=request.llm_format,
355
+ )
356
+ input_format_struct = json_format.Parse(
357
+ text=input_schema_proto,
358
+ message=struct_pb2.Struct(), # pylint: disable=no-member
359
+ ignore_unknown_fields=True,
360
+ )
361
+ except NotImplementedError as e:
362
+ logger.warning(e)
363
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
364
+ context.set_details(str(e))
365
+ return information_pb2.GetModuleInputResponse()
366
+
367
+ return information_pb2.GetModuleInputResponse(
368
+ success=True,
369
+ input_schema=input_format_struct,
370
+ )
371
+
372
+ async def GetModuleOutput( # noqa: N802
373
+ self,
374
+ request: information_pb2.GetModuleOutputRequest,
375
+ context: grpc.ServicerContext,
376
+ ) -> information_pb2.GetModuleOutputResponse:
377
+ """Get information about the module's expected output.
378
+
379
+ Args:
380
+ request: The get module output request.
381
+ context: The gRPC context.
382
+
383
+ Returns:
384
+ A response with the module's output schema.
385
+ """
386
+ logger.debug("GetModuleOutput called for module: '%s'", self.module_class.__name__)
387
+
388
+ # Get output schema if available
389
+ try:
390
+ # Convert schema to proto format
391
+ output_schema_proto = await self.module_class.get_output_format(
392
+ llm_format=request.llm_format,
393
+ )
394
+ output_format_struct = json_format.Parse(
395
+ text=output_schema_proto,
396
+ message=struct_pb2.Struct(), # pylint: disable=no-member
397
+ ignore_unknown_fields=True,
398
+ )
399
+ except NotImplementedError as e:
400
+ logger.warning(e)
401
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
402
+ context.set_details(str(e))
403
+ return information_pb2.GetModuleOutputResponse()
404
+
405
+ return information_pb2.GetModuleOutputResponse(
406
+ success=True,
407
+ output_schema=output_format_struct,
408
+ )
409
+
410
+ async def GetModuleSetup( # noqa: N802
411
+ self,
412
+ request: information_pb2.GetModuleSetupRequest,
413
+ context: grpc.ServicerContext,
414
+ ) -> information_pb2.GetModuleSetupResponse:
415
+ """Get information about the module's setup and configuration.
416
+
417
+ Args:
418
+ request: The get module setup request.
419
+ context: The gRPC context.
420
+
421
+ Returns:
422
+ A response with the module's setup information.
423
+ """
424
+ logger.debug("GetModuleSetup called for module: '%s'", self.module_class.__name__)
425
+
426
+ # Get setup schema if available
427
+ try:
428
+ # Convert schema to proto format
429
+ setup_schema_proto = await self.module_class.get_setup_format(llm_format=request.llm_format)
430
+ setup_format_struct = json_format.Parse(
431
+ text=setup_schema_proto,
432
+ message=struct_pb2.Struct(), # pylint: disable=no-member
433
+ ignore_unknown_fields=True,
434
+ )
435
+ except NotImplementedError as e:
436
+ logger.warning(e)
437
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
438
+ context.set_details(str(e))
439
+ return information_pb2.GetModuleSetupResponse()
440
+
441
+ return information_pb2.GetModuleSetupResponse(
442
+ success=True,
443
+ setup_schema=setup_format_struct,
444
+ )
445
+
446
+ async def GetModuleSecret( # noqa: N802
447
+ self,
448
+ request: information_pb2.GetModuleSecretRequest,
449
+ context: grpc.ServicerContext,
450
+ ) -> information_pb2.GetModuleSecretResponse:
451
+ """Get information about the module's secrets.
452
+
453
+ Args:
454
+ request: The get module secret request.
455
+ context: The gRPC context.
456
+
457
+ Returns:
458
+ A response with the module's secret schema.
459
+ """
460
+ logger.info("GetModuleSecret called for module: '%s'", self.module_class.__name__)
461
+
462
+ # Get secret schema if available
463
+ try:
464
+ # Convert schema to proto format
465
+ secret_schema_proto = await self.module_class.get_secret_format(llm_format=request.llm_format)
466
+ secret_format_struct = json_format.Parse(
467
+ text=secret_schema_proto,
468
+ message=struct_pb2.Struct(), # pylint: disable=no-member
469
+ ignore_unknown_fields=True,
470
+ )
471
+ except NotImplementedError as e:
472
+ logger.warning(e)
473
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
474
+ context.set_details(str(e))
475
+ return information_pb2.GetModuleSecretResponse()
476
+
477
+ return information_pb2.GetModuleSecretResponse(
478
+ success=True,
479
+ secret_schema=secret_format_struct,
480
+ )
481
+
482
+ async def GetConfigSetupModule( # noqa: N802
483
+ self,
484
+ request: information_pb2.GetConfigSetupModuleRequest,
485
+ context: grpc.ServicerContext,
486
+ ) -> information_pb2.GetConfigSetupModuleResponse:
487
+ """Get information about the module's setup and configuration.
488
+
489
+ Args:
490
+ request: The get module setup request.
491
+ context: The gRPC context.
492
+
493
+ Returns:
494
+ A response with the module's setup information.
495
+ """
496
+ logger.debug("GetConfigSetupModule called for module: '%s'", self.module_class.__name__)
497
+
498
+ # Get setup schema if available
499
+ try:
500
+ # Convert schema to proto format
501
+ config_setup_schema_proto = await self.module_class.get_config_setup_format(llm_format=request.llm_format)
502
+ config_setup_format_struct = json_format.Parse(
503
+ text=config_setup_schema_proto,
504
+ message=struct_pb2.Struct(), # pylint: disable=no-member
505
+ ignore_unknown_fields=True,
506
+ )
507
+ except NotImplementedError as e:
508
+ logger.warning(e)
509
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
510
+ context.set_details(str(e))
511
+ return information_pb2.GetConfigSetupModuleResponse()
512
+
513
+ return information_pb2.GetConfigSetupModuleResponse(
514
+ success=True,
515
+ config_setup_schema=config_setup_format_struct,
516
+ )
@@ -0,0 +1 @@
1
+ """gRPC servers utilities package."""
@@ -0,0 +1,29 @@
1
+ """Exceptions for the DigitalKin gRPC package."""
2
+
3
+
4
+ class DigitalKinError(Exception):
5
+ """Base exception for all DigitalKin errors."""
6
+
7
+
8
+ class ServerError(DigitalKinError):
9
+ """Base class for server-related errors."""
10
+
11
+
12
+ class ConfigurationError(ServerError):
13
+ """Error related to server configuration."""
14
+
15
+
16
+ class ServicerError(ServerError):
17
+ """Error related to servicer operations."""
18
+
19
+
20
+ class SecurityError(ServerError):
21
+ """Error related to security configuration."""
22
+
23
+
24
+ class ServerStateError(ServerError):
25
+ """Error related to server state (e.g., already started, not started)."""
26
+
27
+
28
+ class ReflectionError(ServerError):
29
+ """Error related to gRPC reflection service."""
@@ -0,0 +1,88 @@
1
+ """Client wrapper to ease channel creation with specific ServerConfig."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import grpc
7
+
8
+ from digitalkin.grpc_servers.utils.exceptions import ServerError
9
+ from digitalkin.logger import logger
10
+ from digitalkin.models.grpc_servers.models import ClientConfig, SecurityMode
11
+
12
+
13
+ class GrpcClientWrapper:
14
+ """gRPC client shared by the different services."""
15
+
16
+ stub: Any
17
+
18
+ @staticmethod
19
+ def _init_channel(config: ClientConfig) -> grpc.Channel:
20
+ """Create an appropriate channel to the registry server.
21
+
22
+ Returns:
23
+ A gRPC channel for communication with the registry.
24
+
25
+ Raises:
26
+ ValueError: If credentials are required but not provided.
27
+ """
28
+ if config.security == SecurityMode.SECURE and config.credentials is not None:
29
+ # Secure channel
30
+ root_certificates = Path(config.credentials.root_cert_path).read_bytes()
31
+
32
+ # mTLS channel
33
+ private_key = None
34
+ certificate_chain = None
35
+ if config.credentials.client_cert_path is not None and config.credentials.client_key_path is not None:
36
+ private_key = Path(config.credentials.client_key_path).read_bytes()
37
+ certificate_chain = Path(config.credentials.client_cert_path).read_bytes()
38
+
39
+ # Create channel credentials
40
+ channel_credentials = grpc.ssl_channel_credentials(
41
+ root_certificates=root_certificates,
42
+ certificate_chain=certificate_chain,
43
+ private_key=private_key,
44
+ )
45
+
46
+ return grpc.secure_channel(config.address, channel_credentials, options=config.channel_options)
47
+ # Insecure channel
48
+ return grpc.insecure_channel(config.address, options=config.channel_options)
49
+
50
+ def exec_grpc_query(self, query_endpoint: str, request: Any) -> Any: # noqa: ANN401
51
+ """Execute a gRPC query with from the query's rpc endpoint name.
52
+
53
+ Arguments:
54
+ query_endpoint: rpc query name
55
+ request: gRPC object to match the rpc query
56
+
57
+ Returns:
58
+ corresponding gRPC reponse.
59
+
60
+ Raises:
61
+ ServerError: gRPC error catching with status code and details
62
+ """
63
+ service_name = getattr(self, "service_name", "unknown")
64
+ try:
65
+ logger.debug(
66
+ "Sending gRPC request to %s",
67
+ query_endpoint,
68
+ extra={"request": str(request), "service": service_name},
69
+ )
70
+ response = getattr(self.stub, query_endpoint)(request)
71
+ logger.debug(
72
+ "Received gRPC response from %s",
73
+ query_endpoint,
74
+ extra={"response": str(response), "service": service_name},
75
+ )
76
+ except grpc.RpcError as e:
77
+ status_code = e.code().name if hasattr(e, "code") else "UNKNOWN"
78
+ details = e.details() if hasattr(e, "details") else str(e)
79
+ msg = f"[{status_code}] {details}"
80
+ logger.error(
81
+ "gRPC %s failed: %s",
82
+ query_endpoint,
83
+ msg,
84
+ extra={"service": service_name},
85
+ )
86
+ raise ServerError(msg) from e
87
+ else:
88
+ return response
@@ -0,0 +1,53 @@
1
+ """Shared error handling utilities for gRPC services."""
2
+
3
+ from collections.abc import Generator
4
+ from contextlib import contextmanager
5
+ from typing import Any
6
+
7
+ from digitalkin.grpc_servers.utils.exceptions import ServerError
8
+ from digitalkin.logger import logger
9
+
10
+
11
+ class GrpcErrorHandlerMixin:
12
+ """Mixin class providing common gRPC error handling functionality."""
13
+
14
+ @contextmanager
15
+ def handle_grpc_errors( # noqa: PLR6301
16
+ self,
17
+ operation: str,
18
+ service_error_class: type[Exception] | None = None,
19
+ ) -> Generator[Any, Any, Any]:
20
+ """Handle gRPC errors for the given operation.
21
+
22
+ Args:
23
+ operation: Name of the operation being performed.
24
+ service_error_class: Optional specific service exception class to raise.
25
+ If not provided, uses the generic ServerError.
26
+
27
+ Yields:
28
+ Context for the operation.
29
+
30
+ Raises:
31
+ ServerError: For gRPC-related errors.
32
+ service_error_class: For service-specific errors if provided.
33
+ """
34
+ if service_error_class is None:
35
+ service_error_class = ServerError
36
+
37
+ try:
38
+ yield
39
+ except service_error_class as e:
40
+ # Re-raise service-specific errors as-is
41
+ msg = f"{service_error_class.__name__} in {operation}: {e}"
42
+ logger.exception(msg)
43
+ raise service_error_class(msg) from e
44
+ except ServerError as e:
45
+ # Handle gRPC server errors
46
+ msg = f"gRPC {operation} failed: {e}"
47
+ logger.exception(msg)
48
+ raise ServerError(msg) from e
49
+ except Exception as e:
50
+ # Handle unexpected errors
51
+ msg = f"Unexpected error in {operation}: {e}"
52
+ logger.exception(msg)
53
+ raise service_error_class(msg) from e