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,486 @@
1
+ """Base gRPC server implementation for DigitalKin."""
2
+
3
+ import abc
4
+ import asyncio
5
+ from collections.abc import Callable
6
+ from concurrent import futures
7
+ from pathlib import Path
8
+ from typing import Any, cast
9
+
10
+ import grpc
11
+ from grpc import aio as grpc_aio
12
+
13
+ from digitalkin.grpc_servers.utils.exceptions import (
14
+ ConfigurationError,
15
+ ReflectionError,
16
+ SecurityError,
17
+ ServerStateError,
18
+ ServicerError,
19
+ )
20
+ from digitalkin.logger import logger
21
+ from digitalkin.models.grpc_servers.models import SecurityMode, ServerConfig, ServerMode
22
+ from digitalkin.models.grpc_servers.types import GrpcServer, ServiceDescriptor, T
23
+
24
+
25
+ class BaseServer(abc.ABC):
26
+ """Base class for gRPC servers in DigitalKin.
27
+
28
+ This class provides the foundation for both synchronous and asynchronous gRPC
29
+ servers used in the DigitalKin ecosystem. It supports both secure and insecure
30
+ communication modes.
31
+
32
+ Attributes:
33
+ config: The server configuration.
34
+ server: The gRPC server instance (either sync or async).
35
+ _servicers: List of registered servicers.
36
+ _service_names: List of service names for reflection.
37
+ _health_servicer: Optional health check servicer.
38
+ """
39
+
40
+ def __init__(self, config: ServerConfig) -> None:
41
+ """Initialize the base gRPC server.
42
+
43
+ Args:
44
+ config: The server configuration.
45
+ """
46
+ self.config = config
47
+ self.server: GrpcServer | None = None
48
+ self._servicers: list[Any] = []
49
+ self._service_names: list[str] = [] # Track service names for reflection
50
+ self._health_servicer: Any = None # For health checking
51
+
52
+ def register_servicer(
53
+ self,
54
+ servicer: T,
55
+ add_to_server_fn: Callable[[T, GrpcServer], None],
56
+ service_descriptor: ServiceDescriptor | None = None,
57
+ service_names: list[str] | None = None,
58
+ ) -> None:
59
+ """Register a servicer with the gRPC server and track it for reflection.
60
+
61
+ Args:
62
+ servicer: The servicer implementation instance
63
+ add_to_server_fn: The function to add the servicer to the server
64
+ service_descriptor: Optional service descriptor (pb2 DESCRIPTOR)
65
+ service_names: Optional explicit list of service full names
66
+
67
+ Raises:
68
+ ServicerError: If the server is not created before calling
69
+ """
70
+ if self.server is None:
71
+ msg = "Server must be created before registering servicers"
72
+ raise ServicerError(msg)
73
+
74
+ # Register the servicer
75
+ try:
76
+ add_to_server_fn(servicer, self.server)
77
+ self._servicers.append(servicer)
78
+ except Exception as e:
79
+ msg = f"Failed to register servicer: {e}"
80
+ raise ServicerError(msg) from e
81
+
82
+ # Add service names from explicit list
83
+ if service_names:
84
+ for name in service_names:
85
+ if name not in self._service_names:
86
+ self._service_names.append(name)
87
+ logger.debug("Registered explicit service name for reflection: %s", name)
88
+
89
+ # If a descriptor is provided, extract service names
90
+ if service_descriptor and hasattr(service_descriptor, "services_by_name"):
91
+ for service_name in service_descriptor.services_by_name:
92
+ full_name = service_descriptor.services_by_name[service_name].full_name # ignore: PLC0206
93
+ if full_name not in self._service_names:
94
+ self._service_names.append(full_name)
95
+ logger.debug("Registered service name from descriptor: %s", full_name)
96
+
97
+ @abc.abstractmethod
98
+ def _register_servicers(self) -> None:
99
+ """Register servicers with the gRPC server.
100
+
101
+ This method should be implemented by subclasses to register
102
+ the appropriate servicers for their specific functionality.
103
+
104
+ Raises:
105
+ ServicerError: If the server is not created before calling this method.
106
+ """
107
+
108
+ def _add_reflection(self) -> None:
109
+ """Add reflection service to the gRPC server if enabled.
110
+
111
+ Raises:
112
+ ReflectionError: If reflection initialization fails.
113
+ """
114
+ if not self.config.enable_reflection or self.server is None or not self._service_names:
115
+ return
116
+
117
+ try:
118
+ # Import here to avoid dependency if not used
119
+ from grpc_reflection.v1alpha import reflection # noqa: PLC0415
120
+
121
+ # Get all registered service names
122
+ service_names = self._service_names.copy()
123
+
124
+ # Add the reflection service name
125
+ reflection_service = reflection.SERVICE_NAME
126
+ service_names.append(reflection_service)
127
+
128
+ # Register services with the reflection service
129
+ # This creates a dynamic file descriptor database that can respond to
130
+ # reflection queries with detailed service information
131
+ reflection.enable_server_reflection(service_names, self.server)
132
+
133
+ logger.debug("Added gRPC reflection service with services: %s", service_names)
134
+ except ImportError:
135
+ logger.warning("Could not enable reflection: grpcio-reflection package not installed")
136
+ except Exception as e:
137
+ error_msg = f"Failed to enable reflection: {e}"
138
+ logger.warning(error_msg)
139
+ raise ReflectionError(error_msg) from e
140
+
141
+ def _add_health_service(self) -> None:
142
+ """Add health checking service to the gRPC server.
143
+
144
+ The health service allows clients to check server status.
145
+ """
146
+ if self.server is None:
147
+ return
148
+
149
+ try:
150
+ # Import here to avoid dependency if not used
151
+ from grpc_health.v1 import health_pb2, health_pb2_grpc # noqa: PLC0415
152
+ from grpc_health.v1.health import HealthServicer # noqa: PLC0415
153
+
154
+ # Create health servicer
155
+ health_servicer = HealthServicer()
156
+
157
+ # Register health servicer
158
+ health_pb2_grpc.add_HealthServicer_to_server(health_servicer, self.server)
159
+
160
+ # Add service name to reflection list
161
+ if health_pb2.DESCRIPTOR.services_by_name:
162
+ service_name = health_pb2.DESCRIPTOR.services_by_name["Health"].full_name
163
+ if service_name not in self._service_names:
164
+ self._service_names.append(service_name)
165
+
166
+ logger.debug("Added gRPC health checking service")
167
+
168
+ # Set all services as SERVING
169
+ for service_name in self._service_names:
170
+ health_servicer.set(service_name, health_pb2.HealthCheckResponse.SERVING)
171
+
172
+ # Set overall service status
173
+ health_servicer.set("", health_pb2.HealthCheckResponse.SERVING)
174
+
175
+ # Store reference to health servicer
176
+ self._health_servicer = health_servicer
177
+
178
+ except ImportError:
179
+ logger.warning("Could not enable health service: grpcio-health-checking package not installed")
180
+ except Exception as e:
181
+ logger.warning("Failed to enable health service: %s", e)
182
+
183
+ def _create_server(self) -> GrpcServer:
184
+ """Create a gRPC server instance based on the configuration.
185
+
186
+ Returns:
187
+ A configured gRPC server instance.
188
+
189
+ Raises:
190
+ ConfigurationError: If the server configuration is invalid.
191
+ """
192
+ try:
193
+ # Create the server based on mode
194
+ if self.config.mode == ServerMode.ASYNC:
195
+ server = grpc_aio.server(options=self.config.server_options)
196
+ else:
197
+ server = grpc.server(
198
+ futures.ThreadPoolExecutor(max_workers=self.config.max_workers),
199
+ options=self.config.server_options,
200
+ )
201
+
202
+ # Add the appropriate port
203
+ if self.config.security == SecurityMode.SECURE:
204
+ self._add_secure_port(server)
205
+ else:
206
+ self._add_insecure_port(server)
207
+
208
+ except Exception as e:
209
+ msg = f"Failed to create server: {e}"
210
+ raise ConfigurationError(msg) from e
211
+ else:
212
+ return server
213
+
214
+ def _add_secure_port(self, server: GrpcServer) -> None:
215
+ """Add a secure port to the server.
216
+
217
+ Args:
218
+ server: The gRPC server to add the port to.
219
+
220
+ Raises:
221
+ SecurityError: If credentials are not configured correctly.
222
+ """
223
+ if not self.config.credentials:
224
+ msg = "Credentials must be provided for secure server"
225
+ raise SecurityError(msg)
226
+
227
+ try:
228
+ # Read key and certificate files
229
+ private_key = Path(self.config.credentials.server_key_path).read_bytes()
230
+ certificate_chain = Path(self.config.credentials.server_cert_path).read_bytes()
231
+
232
+ # Read root certificate if provided
233
+ root_certificates = None
234
+ if self.config.credentials.root_cert_path:
235
+ root_certificates = Path(self.config.credentials.root_cert_path).read_bytes()
236
+ except OSError as e:
237
+ msg = f"Failed to read credential files: {e}"
238
+ raise SecurityError(msg) from e
239
+
240
+ try:
241
+ # Create server credentials
242
+ server_credentials = grpc.ssl_server_credentials(
243
+ [(private_key, certificate_chain)],
244
+ root_certificates=root_certificates,
245
+ require_client_auth=(root_certificates is not None),
246
+ )
247
+
248
+ # Add secure port to server
249
+ if self.config.mode == ServerMode.ASYNC:
250
+ async_server = cast("grpc_aio.Server", server)
251
+ async_server.add_secure_port(self.config.address, server_credentials)
252
+ else:
253
+ sync_server = cast("grpc.Server", server)
254
+ sync_server.add_secure_port(self.config.address, server_credentials)
255
+
256
+ logger.debug("Added secure port %s", self.config.address)
257
+ except Exception as e:
258
+ msg = f"Failed to configure secure port: {e}"
259
+ raise SecurityError(msg) from e
260
+
261
+ def _add_insecure_port(self, server: GrpcServer) -> None:
262
+ """Add an insecure port to the server.
263
+
264
+ Args:
265
+ server: The gRPC server to add the port to.
266
+
267
+ Raises:
268
+ ConfigurationError: If adding the insecure port fails.
269
+ """
270
+ try:
271
+ if self.config.mode == ServerMode.ASYNC:
272
+ async_server = cast("grpc_aio.Server", server)
273
+ async_server.add_insecure_port(self.config.address)
274
+ else:
275
+ sync_server = cast("grpc.Server", server)
276
+ sync_server.add_insecure_port(self.config.address)
277
+
278
+ logger.debug("Added insecure port %s", self.config.address)
279
+ except Exception as e:
280
+ msg = f"Failed to add insecure port: {e}"
281
+ raise ConfigurationError(msg) from e
282
+
283
+ def start(self) -> None:
284
+ """Start the gRPC server.
285
+
286
+ If using async mode, this will use the event loop to start the server.
287
+ If using sync mode, this will start the server in a non-blocking way.
288
+
289
+ Raises:
290
+ ServerStateError: If the server fails to start.
291
+ """
292
+ self.server = self._create_server()
293
+ self._register_servicers()
294
+
295
+ # Add health service
296
+ self._add_health_service()
297
+
298
+ # Add reflection if enabled
299
+ self._add_reflection()
300
+
301
+ # Start the server
302
+ logger.debug("Starting gRPC server on %s", self.config.address, extra={"config": self.config})
303
+ try:
304
+ if self.config.mode == ServerMode.ASYNC:
305
+ # For async server, use the event loop
306
+ loop = asyncio.get_event_loop()
307
+ if loop.is_closed():
308
+ loop = asyncio.new_event_loop()
309
+ asyncio.set_event_loop(loop)
310
+ loop.run_until_complete(self._start_async())
311
+ else:
312
+ # For sync server, directly call start
313
+ sync_server = cast("grpc.Server", self.server)
314
+ sync_server.start()
315
+ logger.debug("✅ gRPC server started on %s", self.config.address)
316
+ except Exception as e:
317
+ logger.exception("❎ Error starting server")
318
+ msg = f"Failed to start server: {e}"
319
+ raise ServerStateError(msg) from e
320
+
321
+ async def _start_async(self) -> None:
322
+ """Start the async gRPC server.
323
+
324
+ Raises:
325
+ ServerStateError: If the server is not created.
326
+ """
327
+ if self.server is None:
328
+ msg = "Server is not created"
329
+ raise ServerStateError(msg)
330
+
331
+ async_server = cast("grpc_aio.Server", self.server)
332
+ await async_server.start()
333
+
334
+ async def start_async(self) -> None:
335
+ """Start the gRPC server asynchronously.
336
+
337
+ This method should be used directly in an async context.
338
+
339
+ Raises:
340
+ ServerStateError: If the server fails to start.
341
+ """
342
+ self.server = self._create_server()
343
+ self._register_servicers()
344
+
345
+ # Add health service
346
+ self._add_health_service()
347
+
348
+ # Add reflection if enabled
349
+ self._add_reflection()
350
+
351
+ # Start the server
352
+ logger.debug("Starting gRPC server on %s", self.config.address)
353
+ try:
354
+ if self.config.mode == ServerMode.ASYNC:
355
+ await self._start_async()
356
+ else:
357
+ # For sync server in async context
358
+ sync_server = cast("grpc.Server", self.server)
359
+ sync_server.start()
360
+ logger.debug("✅ gRPC server started on %s", self.config.address)
361
+ except Exception as e:
362
+ logger.exception("❎ Error starting server")
363
+ msg = f"Failed to start server: {e}"
364
+ raise ServerStateError(msg) from e
365
+
366
+ def stop(self, grace: float | None = None) -> None:
367
+ """Stop the gRPC server.
368
+
369
+ Args:
370
+ grace: Optional grace period in seconds for existing RPCs to complete.
371
+ """
372
+ if self.server is None:
373
+ logger.warning("Attempted to stop server, but no server is running")
374
+ return
375
+
376
+ logger.debug("Stopping gRPC server...")
377
+ if self.config.mode == ServerMode.ASYNC:
378
+ # We'll use a different approach that works whether we're in a running event loop or not
379
+ try:
380
+ # Get the current event loop
381
+ loop = asyncio.get_event_loop()
382
+
383
+ if loop.is_running():
384
+ # If we're in a running event loop, we can't run_until_complete
385
+ # Just warn the user they should use stop_async
386
+ logger.warning(
387
+ "Called stop() on async server from a running event loop. "
388
+ "This might not fully shut down the server. "
389
+ "Use await stop_async() in async contexts instead."
390
+ )
391
+ # Set server to None to avoid further operations
392
+ self.server = None
393
+ logger.debug("✅ gRPC server marked as stopped")
394
+ return
395
+ # If not in a running event loop, use run_until_complete
396
+ loop.run_until_complete(self._stop_async(grace))
397
+ except RuntimeError:
398
+ # Event loop issues - try with a new loop
399
+ logger.debug("Creating new event loop for shutdown")
400
+ try:
401
+ new_loop = asyncio.new_event_loop()
402
+ asyncio.set_event_loop(new_loop)
403
+ new_loop.run_until_complete(self._stop_async(grace))
404
+ finally:
405
+ new_loop.close()
406
+ else:
407
+ # For sync server, we can just call stop
408
+ sync_server = cast("grpc.Server", self.server)
409
+ sync_server.stop(grace=grace)
410
+
411
+ logger.debug("✅ gRPC server stopped")
412
+ self.server = None
413
+
414
+ async def _stop_async(self, grace: float | None = None) -> None:
415
+ """Stop the async gRPC server.
416
+
417
+ Args:
418
+ grace: Optional grace period in seconds for existing RPCs to complete.
419
+ """
420
+ if self.server is None:
421
+ return
422
+
423
+ async_server = cast("grpc_aio.Server", self.server)
424
+ await async_server.stop(grace=grace)
425
+
426
+ async def stop_async(self, grace: float | None = None) -> None:
427
+ """Stop the gRPC server asynchronously.
428
+
429
+ This method should be used in async contexts.
430
+
431
+ Args:
432
+ grace: Optional grace period in seconds for existing RPCs to complete.
433
+ """
434
+ if self.server is None:
435
+ logger.warning("Attempted to stop server, but no server is running")
436
+ return
437
+
438
+ logger.debug("Stopping gRPC server asynchronously...")
439
+ if self.config.mode == ServerMode.ASYNC:
440
+ await self._stop_async(grace)
441
+ else:
442
+ # For sync server, we can just call stop
443
+ sync_server = cast("grpc.Server", self.server)
444
+ sync_server.stop(grace=grace)
445
+
446
+ logger.debug("✅ gRPC server stopped")
447
+ self.server = None
448
+
449
+ def wait_for_termination(self) -> None:
450
+ """Wait for the server to terminate.
451
+
452
+ In synchronous mode, this blocks until the server is terminated.
453
+ In asynchronous mode, a warning is logged suggesting to use `await_termination`.
454
+ """
455
+ if self.server is None:
456
+ logger.warning("Attempted to wait for termination, but no server is running")
457
+ return
458
+
459
+ if self.config.mode == ServerMode.SYNC:
460
+ # For sync server
461
+ sync_server = cast("grpc.Server", self.server)
462
+ sync_server.wait_for_termination()
463
+ else:
464
+ # For async server, the caller should use await_termination instead
465
+ logger.warning(
466
+ "Called wait_for_termination on async server. Use await_termination instead for async servers.",
467
+ )
468
+
469
+ async def await_termination(self) -> None:
470
+ """Wait for the async server to terminate.
471
+
472
+ This method should only be used with async servers.
473
+ """
474
+ if self.config.mode == ServerMode.SYNC:
475
+ logger.warning(
476
+ "Called await_termination on sync server. Use wait_for_termination instead for sync servers.",
477
+ )
478
+ return
479
+
480
+ if self.server is None:
481
+ logger.warning("Attempted to await termination, but no server is running")
482
+ return
483
+
484
+ # For async server
485
+ async_server = cast("grpc_aio.Server", self.server)
486
+ await async_server.wait_for_termination()
@@ -0,0 +1,208 @@
1
+ """Module gRPC server implementation for DigitalKin."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from agentic_mesh_protocol.module.v1 import (
6
+ module_service_pb2,
7
+ module_service_pb2_grpc,
8
+ )
9
+
10
+ from digitalkin.grpc_servers._base_server import BaseServer
11
+ from digitalkin.grpc_servers.module_servicer import ModuleServicer
12
+ from digitalkin.logger import logger
13
+ from digitalkin.models.grpc_servers.models import (
14
+ ClientConfig,
15
+ ModuleServerConfig,
16
+ SecurityMode,
17
+ )
18
+ from digitalkin.modules._base_module import BaseModule
19
+ from digitalkin.services.registry import GrpcRegistry
20
+
21
+ if TYPE_CHECKING:
22
+ from digitalkin.services.registry import RegistryStrategy
23
+
24
+
25
+ class ModuleServer(BaseServer):
26
+ """gRPC server for a DigitalKin module.
27
+
28
+ This server exposes the module's functionality through the ModuleService gRPC interface.
29
+ It can optionally register itself with a Registry server.
30
+
31
+ Attributes:
32
+ module: The module instance being served.
33
+ server_config: Server configuration.
34
+ client_config: Setup client configuration.
35
+ module_servicer: The gRPC servicer handling module requests.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ module_class: type[BaseModule],
41
+ server_config: ModuleServerConfig,
42
+ client_config: ClientConfig | None = None,
43
+ ) -> None:
44
+ """Initialize the module server.
45
+
46
+ Args:
47
+ module_class: The module instance to be served.
48
+ server_config: Server configuration including registry address if auto-registration is desired.
49
+ client_config: Client configuration used by services.
50
+ """
51
+ super().__init__(server_config)
52
+ self.module_class = module_class
53
+ self.server_config = server_config
54
+ self.client_config = client_config
55
+ self.module_servicer: ModuleServicer | None = None
56
+ self.registry: RegistryStrategy | None = None
57
+
58
+ self._registry_client_config: ClientConfig | None = None
59
+ if self.server_config.registry_address:
60
+ self._registry_client_config = self._build_registry_client_config()
61
+ self._prepare_registry_config()
62
+
63
+ def _register_servicers(self) -> None:
64
+ """Register the module servicer with the gRPC server.
65
+
66
+ Raises:
67
+ RuntimeError: No registered server
68
+ """
69
+ if self.server is None:
70
+ msg = "Server must be created before registering servicers"
71
+ raise RuntimeError(msg)
72
+
73
+ logger.debug("Registering module servicer for %s", self.module_class.__name__)
74
+ self.module_servicer = ModuleServicer(self.module_class)
75
+ self.register_servicer(
76
+ self.module_servicer,
77
+ module_service_pb2_grpc.add_ModuleServiceServicer_to_server,
78
+ service_descriptor=module_service_pb2.DESCRIPTOR,
79
+ )
80
+ logger.debug("Registered Module servicer")
81
+
82
+ def _prepare_registry_config(self) -> None:
83
+ """Prepare registry client config on module_class before server starts.
84
+
85
+ This ensures ServicesConfig created by JobManager will have registry config,
86
+ allowing spawned module instances to inherit the registry configuration.
87
+ """
88
+ if not self._registry_client_config:
89
+ return
90
+
91
+ self.module_class.services_config_params["registry"] = {"client_config": self._registry_client_config}
92
+
93
+ def _build_registry_client_config(self) -> ClientConfig:
94
+ """Build ClientConfig for registry from server_config.registry_address.
95
+
96
+ Returns:
97
+ ClientConfig configured for registry connection.
98
+ """
99
+ host, port = self.server_config.registry_address.rsplit(":", 1)
100
+ return ClientConfig(
101
+ host=host,
102
+ port=int(port),
103
+ mode=self.server_config.mode,
104
+ security=self.client_config.security if self.client_config else SecurityMode.INSECURE,
105
+ credentials=self.client_config.credentials if self.client_config else None,
106
+ )
107
+
108
+ def _init_registry(self) -> None:
109
+ """Initialize server-level registry client for registration.
110
+
111
+ Note: services_config_params["registry"] is already set in _prepare_registry_config()
112
+ which runs in __init__(). This method only creates the server-level client instance.
113
+ """
114
+ if not self._registry_client_config:
115
+ return
116
+
117
+ self.registry = GrpcRegistry("", "", "", self._registry_client_config)
118
+
119
+ def start(self) -> None:
120
+ """Start the module server and register with the registry if configured."""
121
+ logger.info("Starting module server", extra={"server_config": self.server_config})
122
+ super().start()
123
+
124
+ try:
125
+ self._init_registry()
126
+ self._register_with_registry()
127
+ except Exception:
128
+ logger.exception("Failed to register with registry")
129
+
130
+ if self.module_servicer is not None:
131
+ logger.debug("Setup post init started", extra={"client_config": self.client_config})
132
+ self.module_servicer.setup.__post_init__(self.client_config)
133
+
134
+ async def start_async(self) -> None:
135
+ """Start the module server and register with the registry if configured."""
136
+ logger.info("Starting module server", extra={"server_config": self.server_config})
137
+ await super().start_async()
138
+
139
+ try:
140
+ self._init_registry()
141
+ self._register_with_registry()
142
+ except Exception:
143
+ logger.exception("Failed to register with registry")
144
+
145
+ if self.module_servicer is not None:
146
+ logger.info("Setup post init started", extra={"client_config": self.client_config})
147
+ await self.module_servicer.job_manager.start()
148
+ self.module_servicer.setup.__post_init__(self.client_config)
149
+
150
+ def stop(self, grace: float | None = None) -> None:
151
+ """Stop the module server.
152
+
153
+ Modules become inactive when they stop sending heartbeats
154
+ """
155
+ super().stop(grace)
156
+
157
+ def _register_with_registry(self) -> None:
158
+ """Register this module with the registry server."""
159
+ if not self.registry:
160
+ logger.debug("No registry configured, skipping registration")
161
+ return
162
+
163
+ module_id = self.module_class.get_module_id()
164
+ version = self.module_class.metadata.get("version", "0.0.0")
165
+
166
+ if not module_id or module_id == "unknown":
167
+ logger.warning(
168
+ "Module has no valid module_id, skipping registration",
169
+ extra={"module_class": self.module_class.__name__},
170
+ )
171
+ return
172
+
173
+ logger.info(
174
+ "Attempting to register module with registry",
175
+ extra={
176
+ "module_id": module_id,
177
+ "address": self.server_config.address,
178
+ "port": self.server_config.port,
179
+ "version": version,
180
+ "registry_address": self.server_config.registry_address,
181
+ },
182
+ )
183
+
184
+ result = self.registry.register(
185
+ module_id=module_id,
186
+ address=self.server_config.address,
187
+ port=self.server_config.port,
188
+ version=version,
189
+ )
190
+
191
+ if result:
192
+ logger.info(
193
+ "Module registered successfully",
194
+ extra={
195
+ "module_id": result.module_id,
196
+ "address": self.server_config.address,
197
+ "port": self.server_config.port,
198
+ "registry_address": self.server_config.registry_address,
199
+ },
200
+ )
201
+ else:
202
+ logger.warning(
203
+ "Module registration returned None (module may not exist in registry)",
204
+ extra={
205
+ "module_id": module_id,
206
+ "registry_address": self.server_config.registry_address,
207
+ },
208
+ )