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
@@ -2,9 +2,22 @@
2
2
 
3
3
  from abc import ABC
4
4
 
5
- from digitalkin.models.module import InputModelT, OutputModelT, SecretModelT, SetupModelT
6
- from digitalkin.modules._base_module import BaseModule # type: ignore
5
+ from digitalkin.models.module.module_types import (
6
+ InputModelT,
7
+ OutputModelT,
8
+ SecretModelT,
9
+ SetupModelT,
10
+ )
11
+ from digitalkin.modules._base_module import BaseModule # type: ignore
7
12
 
8
13
 
9
- class ToolModule(BaseModule[InputModelT, OutputModelT, SetupModelT, SecretModelT,], ABC):
14
+ class ToolModule(
15
+ BaseModule[
16
+ InputModelT,
17
+ OutputModelT,
18
+ SetupModelT,
19
+ SecretModelT,
20
+ ],
21
+ ABC,
22
+ ):
10
23
  """ToolModule extends BaseModule to implement specific module types."""
@@ -1,14 +1,14 @@
1
1
  """Definition of the Trigger type."""
2
2
 
3
3
  from abc import ABC, abstractmethod
4
- from collections.abc import Callable, Coroutine
5
- from typing import Any, ClassVar, Generic
4
+ from typing import ClassVar, Generic
6
5
 
6
+ from digitalkin.mixins import BaseMixin
7
+ from digitalkin.models.module.module_context import ModuleContext
7
8
  from digitalkin.models.module.module_types import InputModelT, OutputModelT, SetupModelT
8
- from digitalkin.modules._base_module import ModuleContext
9
9
 
10
10
 
11
- class TriggerHandler(ABC, Generic[InputModelT, SetupModelT, OutputModelT]):
11
+ class TriggerHandler(ABC, BaseMixin, Generic[InputModelT, SetupModelT, OutputModelT]):
12
12
  """Base class for all input-trigger handlers.
13
13
 
14
14
  Each handler declares:
@@ -28,7 +28,6 @@ class TriggerHandler(ABC, Generic[InputModelT, SetupModelT, OutputModelT]):
28
28
  self,
29
29
  input_data: InputModelT,
30
30
  setup_data: SetupModelT,
31
- callback: Callable[[Any], Coroutine[Any, Any, None]],
32
31
  context: ModuleContext,
33
32
  ) -> None:
34
33
  """Asynchronously processes the input data specific to Handler and streams results via the provided callback.
@@ -36,12 +35,14 @@ class TriggerHandler(ABC, Generic[InputModelT, SetupModelT, OutputModelT]):
36
35
  Args:
37
36
  input_data (InputModelT): The input data to be processed by the handler.
38
37
  setup_data (SetupModelT): The setup or configuration data required for processing.
39
- callback (Callable[[Any], Coroutine[Any, Any, None]]): callback that stream results.
40
38
  context (ModuleContext): The context object containing module-specific information and resources.
41
39
 
42
40
  Returns:
43
41
  Any: The result of the processing, if applicable.
44
42
 
45
43
  Note:
44
+ self.send_message: : callback used to stream results.
45
+ (Callable[[OutputMdodelT], Coroutine[Any, Any, None]])
46
+
46
47
  The callback must be awaited to ensure results are streamed correctly during processing.
47
48
  """
@@ -0,0 +1,8 @@
1
+ """Built-in SDK triggers.
2
+
3
+ These triggers are automatically registered without requiring discovery.
4
+ They provide standard functionality available to all modules.
5
+
6
+ Note: These are internal triggers. External code should not import them directly.
7
+ Use UtilityRegistry.get_builtin_triggers() to access the trigger classes.
8
+ """
@@ -0,0 +1,45 @@
1
+ """Healthcheck ping trigger - simple alive check."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any, ClassVar
5
+
6
+ from digitalkin.mixins import BaseMixin
7
+ from digitalkin.models.module.module_context import ModuleContext
8
+ from digitalkin.models.module.utility import (
9
+ HealthcheckPingInput,
10
+ HealthcheckPingOutput,
11
+ )
12
+ from digitalkin.modules.trigger_handler import TriggerHandler
13
+
14
+
15
+ class HealthcheckPingTrigger(TriggerHandler, BaseMixin):
16
+ """Handler for simple ping healthcheck.
17
+
18
+ Responds immediately with "pong" status to verify the module is responsive.
19
+ """
20
+
21
+ protocol: ClassVar[str] = "healthcheck_ping"
22
+ input_format = HealthcheckPingInput
23
+ _request_time: datetime
24
+
25
+ def __init__(self, context: ModuleContext) -> None:
26
+ """Initialize the handler."""
27
+ self._request_time = datetime.now(tz=context.session.timezone)
28
+
29
+ async def handle(
30
+ self,
31
+ input_data: HealthcheckPingInput, # noqa: ARG002
32
+ setup_data: Any, # noqa: ANN401, ARG002
33
+ context: ModuleContext,
34
+ ) -> None:
35
+ """Handle ping healthcheck request.
36
+
37
+ Args:
38
+ input_data: The input trigger data (unused for healthcheck).
39
+ setup_data: The setup configuration (unused for healthcheck).
40
+ context: The module context.
41
+ """
42
+ elapsed = datetime.now(tz=context.session.timezone) - self._request_time
43
+ latency_ms = elapsed.total_seconds() * 1000
44
+ output = HealthcheckPingOutput(latency_ms=latency_ms)
45
+ await self.send_message(context, output)
@@ -0,0 +1,63 @@
1
+ """Healthcheck services trigger - reports service health."""
2
+
3
+ from typing import Any, ClassVar
4
+
5
+ from digitalkin.mixins import BaseMixin
6
+ from digitalkin.models.module.module_context import ModuleContext
7
+ from digitalkin.models.module.utility import (
8
+ HealthcheckServicesInput,
9
+ HealthcheckServicesOutput,
10
+ ServiceHealthStatus,
11
+ )
12
+ from digitalkin.modules.trigger_handler import TriggerHandler
13
+
14
+
15
+ class HealthcheckServicesTrigger(TriggerHandler, BaseMixin):
16
+ """Handler for services healthcheck.
17
+
18
+ Reports the health status of all configured services (storage, cost, filesystem, etc.).
19
+ """
20
+
21
+ protocol: ClassVar[str] = "healthcheck_services"
22
+ input_format = HealthcheckServicesInput
23
+
24
+ def __init__(self, context: ModuleContext) -> None:
25
+ """Initialize the handler."""
26
+
27
+ async def handle(
28
+ self,
29
+ input_data: HealthcheckServicesInput, # noqa: ARG002
30
+ setup_data: Any, # noqa: ANN401, ARG002
31
+ context: ModuleContext,
32
+ ) -> None:
33
+ """Handle services healthcheck request.
34
+
35
+ Args:
36
+ input_data: The input trigger data (unused for healthcheck).
37
+ setup_data: The setup configuration (unused for healthcheck).
38
+ context: The module context.
39
+ """
40
+ service_names = ["storage", "cost", "filesystem", "registry", "user_profile"]
41
+ services_status: list[ServiceHealthStatus] = []
42
+
43
+ for name in service_names:
44
+ service = getattr(context, name, None)
45
+ if service is not None:
46
+ services_status.append(ServiceHealthStatus(name=name, status="healthy"))
47
+ else:
48
+ services_status.append(ServiceHealthStatus(name=name, status="unknown", message="Not configured"))
49
+
50
+ # Determine overall status
51
+ healthy_count = sum(1 for s in services_status if s.status == "healthy")
52
+ if healthy_count == len(services_status):
53
+ overall_status = "healthy"
54
+ elif healthy_count > 0:
55
+ overall_status = "degraded"
56
+ else:
57
+ overall_status = "unhealthy"
58
+
59
+ output = HealthcheckServicesOutput(
60
+ services=services_status,
61
+ overall_status=overall_status, # type: ignore[arg-type]
62
+ )
63
+ await self.send_message(context, output)
@@ -0,0 +1,52 @@
1
+ """Healthcheck status trigger - comprehensive module status."""
2
+
3
+ import time
4
+ from typing import Any, ClassVar
5
+
6
+ from digitalkin.mixins import BaseMixin
7
+ from digitalkin.models.module.module_context import ModuleContext
8
+ from digitalkin.models.module.utility import (
9
+ HealthcheckStatusInput,
10
+ HealthcheckStatusOutput,
11
+ )
12
+ from digitalkin.modules.trigger_handler import TriggerHandler
13
+
14
+
15
+ class HealthcheckStatusTrigger(TriggerHandler, BaseMixin):
16
+ """Handler for comprehensive status healthcheck.
17
+
18
+ Reports detailed module status including uptime, active jobs, and metadata.
19
+ """
20
+
21
+ protocol: ClassVar[str] = "healthcheck_status"
22
+ input_format = HealthcheckStatusInput
23
+ _start_time: ClassVar[float] = time.time()
24
+
25
+ def __init__(self, context: ModuleContext) -> None:
26
+ """Initialize the handler."""
27
+
28
+ async def handle(
29
+ self,
30
+ input_data: HealthcheckStatusInput, # noqa: ARG002
31
+ setup_data: Any, # noqa: ANN401, ARG002
32
+ context: ModuleContext,
33
+ ) -> None:
34
+ """Handle status healthcheck request.
35
+
36
+ Args:
37
+ input_data: The input trigger data (unused for healthcheck).
38
+ setup_data: The setup configuration (unused for healthcheck).
39
+ context: The module context.
40
+ """
41
+ output = HealthcheckStatusOutput(
42
+ module_name=context.session.setup_id,
43
+ module_status="RUNNING",
44
+ uptime_seconds=time.time() - self._start_time,
45
+ active_jobs=1,
46
+ metadata={
47
+ "job_id": context.session.job_id,
48
+ "mission_id": context.session.mission_id,
49
+ "setup_version_id": context.session.setup_version_id,
50
+ },
51
+ )
52
+ await self.send_message(context, output)
@@ -1,6 +1,7 @@
1
1
  """This package contains the abstract base class for all services."""
2
2
 
3
3
  from digitalkin.services.agent import AgentStrategy, DefaultAgent
4
+ from digitalkin.services.communication import CommunicationStrategy, DefaultCommunication, GrpcCommunication
4
5
  from digitalkin.services.cost import CostStrategy, DefaultCost
5
6
  from digitalkin.services.filesystem import DefaultFilesystem, FilesystemStrategy
6
7
  from digitalkin.services.identity import DefaultIdentity, IdentityStrategy
@@ -10,8 +11,10 @@ from digitalkin.services.storage import DefaultStorage, StorageStrategy
10
11
 
11
12
  __all__ = [
12
13
  "AgentStrategy",
14
+ "CommunicationStrategy",
13
15
  "CostStrategy",
14
16
  "DefaultAgent",
17
+ "DefaultCommunication",
15
18
  "DefaultCost",
16
19
  "DefaultFilesystem",
17
20
  "DefaultIdentity",
@@ -19,6 +22,7 @@ __all__ = [
19
22
  "DefaultSnapshot",
20
23
  "DefaultStorage",
21
24
  "FilesystemStrategy",
25
+ "GrpcCommunication",
22
26
  "IdentityStrategy",
23
27
  "RegistryStrategy",
24
28
  "SnapshotStrategy",
@@ -0,0 +1,7 @@
1
+ """Communication service for module-to-module interaction."""
2
+
3
+ from digitalkin.services.communication.communication_strategy import CommunicationStrategy
4
+ from digitalkin.services.communication.default_communication import DefaultCommunication
5
+ from digitalkin.services.communication.grpc_communication import GrpcCommunication
6
+
7
+ __all__ = ["CommunicationStrategy", "DefaultCommunication", "GrpcCommunication"]
@@ -0,0 +1,76 @@
1
+ """Abstract base class for communication strategies."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import AsyncGenerator, Awaitable, Callable
5
+
6
+ from digitalkin.services.base_strategy import BaseStrategy
7
+
8
+
9
+ class CommunicationStrategy(BaseStrategy, ABC):
10
+ """Abstract base class for module-to-module communication.
11
+
12
+ This service enables:
13
+ - Archetype → Tool communication
14
+ - Archetype → Archetype communication
15
+ - Tool → Tool communication
16
+ - Any module → Any module communication
17
+
18
+ The service wraps the Module Service protocol from agentic-mesh-protocol.
19
+ """
20
+
21
+ @abstractmethod
22
+ async def get_module_schemas(
23
+ self,
24
+ module_address: str,
25
+ module_port: int,
26
+ *,
27
+ llm_format: bool = False,
28
+ ) -> dict[str, dict]:
29
+ """Get module schemas (input/output/setup/secret).
30
+
31
+ Args:
32
+ module_address: Target module address
33
+ module_port: Target module port
34
+ llm_format: Return LLM-friendly format (simplified schema)
35
+
36
+ Returns:
37
+ Dictionary containing schemas:
38
+ {
39
+ "input": {...},
40
+ "output": {...},
41
+ "setup": {...},
42
+ "secret": {...}
43
+ }
44
+ """
45
+ raise NotImplementedError
46
+
47
+ @abstractmethod
48
+ async def call_module(
49
+ self,
50
+ module_address: str,
51
+ module_port: int,
52
+ input_data: dict,
53
+ setup_id: str,
54
+ mission_id: str,
55
+ callback: Callable[[dict], Awaitable[None]] | None = None,
56
+ ) -> AsyncGenerator[dict, None]:
57
+ """Call a module and stream responses.
58
+
59
+ Uses Module Service StartModule RPC to execute the module.
60
+ Streams responses as they are generated by the module.
61
+
62
+ Args:
63
+ module_address: Target module address
64
+ module_port: Target module port
65
+ input_data: Input data as dictionary
66
+ setup_id: Setup configuration ID
67
+ mission_id: Mission context ID
68
+ callback: Optional callback for each response
69
+
70
+ Yields:
71
+ Streaming responses from module as dictionaries
72
+ """
73
+ # Make this an actual async generator to satisfy type checkers
74
+ if False: # pragma: no cover
75
+ yield {}
76
+ raise NotImplementedError
@@ -0,0 +1,101 @@
1
+ """Default communication implementation (local, for testing)."""
2
+
3
+ from collections.abc import AsyncGenerator, Awaitable, Callable
4
+
5
+ from digitalkin.logger import logger
6
+ from digitalkin.services.communication.communication_strategy import CommunicationStrategy
7
+
8
+
9
+ class DefaultCommunication(CommunicationStrategy):
10
+ """Default communication strategy (local implementation).
11
+
12
+ This implementation is primarily for testing and development.
13
+ For production, use GrpcCommunication to connect to remote modules.
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ mission_id: str,
19
+ setup_id: str,
20
+ setup_version_id: str,
21
+ ) -> None:
22
+ """Initialize the default communication service.
23
+
24
+ Args:
25
+ mission_id: Mission identifier
26
+ setup_id: Setup identifier
27
+ setup_version_id: Setup version identifier
28
+ """
29
+ super().__init__(mission_id, setup_id, setup_version_id)
30
+ logger.debug("Initialized DefaultCommunication (local)")
31
+
32
+ async def get_module_schemas( # noqa: PLR6301
33
+ self,
34
+ module_address: str,
35
+ module_port: int,
36
+ *,
37
+ llm_format: bool = False,
38
+ ) -> dict[str, dict]:
39
+ """Get module schemas (local implementation returns empty schemas).
40
+
41
+ Args:
42
+ module_address: Target module address
43
+ module_port: Target module port
44
+ llm_format: Return LLM-friendly format
45
+
46
+ Returns:
47
+ Empty schemas dictionary
48
+ """
49
+ logger.debug(
50
+ "DefaultCommunication.get_module_schemas called (returns empty)",
51
+ extra={
52
+ "module_address": module_address,
53
+ "module_port": module_port,
54
+ "llm_format": llm_format,
55
+ },
56
+ )
57
+ return {
58
+ "input": {},
59
+ "output": {},
60
+ "setup": {},
61
+ "secret": {},
62
+ }
63
+
64
+ async def call_module( # noqa: PLR6301
65
+ self,
66
+ module_address: str,
67
+ module_port: int,
68
+ input_data: dict, # noqa: ARG002
69
+ setup_id: str,
70
+ mission_id: str,
71
+ callback: Callable[[dict], Awaitable[None]] | None = None,
72
+ ) -> AsyncGenerator[dict, None]:
73
+ """Call module (local implementation yields empty response).
74
+
75
+ Args:
76
+ module_address: Target module address
77
+ module_port: Target module port
78
+ input_data: Input data
79
+ setup_id: Setup ID
80
+ mission_id: Mission ID
81
+ callback: Optional callback
82
+
83
+ Yields:
84
+ Empty response dictionary
85
+ """
86
+ logger.debug(
87
+ "DefaultCommunication.call_module called (returns empty)",
88
+ extra={
89
+ "module_address": module_address,
90
+ "module_port": module_port,
91
+ "setup_id": setup_id,
92
+ "mission_id": mission_id,
93
+ },
94
+ )
95
+
96
+ # Yield empty response
97
+ response = {"status": "error", "message": "Local communication not implemented"}
98
+ if callback:
99
+ await callback(response)
100
+ yield response
101
+ return # Explicit return for async generator
@@ -0,0 +1,234 @@
1
+ """gRPC client implementation for Communication service."""
2
+
3
+ import asyncio
4
+ from collections.abc import AsyncGenerator, Awaitable, Callable
5
+
6
+ from agentic_mesh_protocol.module.v1 import (
7
+ information_pb2,
8
+ lifecycle_pb2,
9
+ module_service_pb2_grpc,
10
+ )
11
+ from google.protobuf import json_format, struct_pb2
12
+
13
+ from digitalkin.grpc_servers.utils.grpc_client_wrapper import GrpcClientWrapper
14
+ from digitalkin.logger import logger
15
+ from digitalkin.models.grpc_servers.models import ClientConfig
16
+ from digitalkin.services.base_strategy import BaseStrategy
17
+ from digitalkin.services.communication.communication_strategy import CommunicationStrategy
18
+
19
+
20
+ class GrpcCommunication(CommunicationStrategy, GrpcClientWrapper):
21
+ """gRPC client for module-to-module communication.
22
+
23
+ This class provides methods to communicate with remote modules
24
+ using the Module Service gRPC protocol.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ mission_id: str,
30
+ setup_id: str,
31
+ setup_version_id: str,
32
+ client_config: ClientConfig,
33
+ ) -> None:
34
+ """Initialize the gRPC communication client.
35
+
36
+ Args:
37
+ mission_id: Mission identifier
38
+ setup_id: Setup identifier
39
+ setup_version_id: Setup version identifier
40
+ client_config: Client configuration for gRPC connection
41
+ """
42
+ BaseStrategy.__init__(self, mission_id, setup_id, setup_version_id)
43
+ self.client_config = client_config
44
+
45
+ logger.debug(
46
+ "Initialized GrpcCommunication",
47
+ extra={"security": client_config.security},
48
+ )
49
+
50
+ def _create_stub(self, module_address: str, module_port: int) -> module_service_pb2_grpc.ModuleServiceStub:
51
+ """Create a new stub for the target module.
52
+
53
+ Args:
54
+ module_address: Module host address
55
+ module_port: Module port
56
+
57
+ Returns:
58
+ ModuleServiceStub for the target module
59
+ """
60
+ logger.debug(
61
+ "Creating connection",
62
+ extra={"address": module_address, "port": module_port},
63
+ )
64
+
65
+ config = ClientConfig(
66
+ host=module_address,
67
+ port=module_port,
68
+ mode=self.client_config.mode,
69
+ security=self.client_config.security,
70
+ credentials=self.client_config.credentials,
71
+ channel_options=self.client_config.channel_options,
72
+ )
73
+
74
+ channel = self._init_channel(config)
75
+ return module_service_pb2_grpc.ModuleServiceStub(channel)
76
+
77
+ async def get_module_schemas(
78
+ self,
79
+ module_address: str,
80
+ module_port: int,
81
+ *,
82
+ llm_format: bool = False,
83
+ ) -> dict[str, dict]:
84
+ """Get module schemas via gRPC.
85
+
86
+ Args:
87
+ module_address: Target module address
88
+ module_port: Target module port
89
+ llm_format: Return LLM-friendly format
90
+
91
+ Returns:
92
+ Dictionary containing schemas
93
+ """
94
+ stub = self._create_stub(module_address, module_port)
95
+
96
+ # Create requests
97
+ input_request = information_pb2.GetModuleInputRequest(llm_format=llm_format)
98
+ output_request = information_pb2.GetModuleOutputRequest(llm_format=llm_format)
99
+ setup_request = information_pb2.GetModuleSetupRequest(llm_format=llm_format)
100
+ secret_request = information_pb2.GetModuleSecretRequest(llm_format=llm_format)
101
+
102
+ # Get all schemas in parallel
103
+ try:
104
+ input_response, output_response, setup_response, secret_response = await asyncio.gather(
105
+ asyncio.to_thread(stub.GetModuleInput, input_request),
106
+ asyncio.to_thread(stub.GetModuleOutput, output_request),
107
+ asyncio.to_thread(stub.GetModuleSetup, setup_request),
108
+ asyncio.to_thread(stub.GetModuleSecret, secret_request),
109
+ )
110
+
111
+ logger.debug(
112
+ "Retrieved module schemas",
113
+ extra={
114
+ "module_address": module_address,
115
+ "module_port": module_port,
116
+ "llm_format": llm_format,
117
+ },
118
+ )
119
+
120
+ return {
121
+ "input": json_format.MessageToDict(input_response.input_schema),
122
+ "output": json_format.MessageToDict(output_response.output_schema),
123
+ "setup": json_format.MessageToDict(setup_response.setup_schema),
124
+ "secret": json_format.MessageToDict(secret_response.secret_schema),
125
+ }
126
+ except Exception:
127
+ logger.exception(
128
+ "Failed to get module schemas",
129
+ extra={
130
+ "module_address": module_address,
131
+ "module_port": module_port,
132
+ },
133
+ )
134
+ raise
135
+
136
+ async def call_module(
137
+ self,
138
+ module_address: str,
139
+ module_port: int,
140
+ input_data: dict,
141
+ setup_id: str,
142
+ mission_id: str,
143
+ callback: Callable[[dict], Awaitable[None]] | None = None,
144
+ ) -> AsyncGenerator[dict, None]:
145
+ """Call a module and stream responses via gRPC.
146
+
147
+ Args:
148
+ module_address: Target module address
149
+ module_port: Target module port
150
+ input_data: Input data as dictionary
151
+ setup_id: Setup configuration ID
152
+ mission_id: Mission context ID
153
+ callback: Optional callback for each response
154
+
155
+ Yields:
156
+ Streaming responses from module as dictionaries
157
+ """
158
+ stub = self._create_stub(module_address, module_port)
159
+
160
+ # Convert input data to protobuf Struct
161
+ input_struct = struct_pb2.Struct()
162
+ input_struct.update(input_data)
163
+
164
+ # Create request
165
+ request = lifecycle_pb2.StartModuleRequest(
166
+ input=input_struct,
167
+ setup_id=setup_id,
168
+ mission_id=mission_id,
169
+ )
170
+
171
+ logger.debug(
172
+ "Calling module",
173
+ extra={
174
+ "module_address": module_address,
175
+ "module_port": module_port,
176
+ "setup_id": setup_id,
177
+ "mission_id": mission_id,
178
+ },
179
+ )
180
+
181
+ try:
182
+ # Call StartModule with streaming response
183
+ response_stream = stub.StartModule(request)
184
+
185
+ # Stream responses
186
+ for response in response_stream:
187
+ # Convert protobuf Struct to dict
188
+ output_dict = json_format.MessageToDict(response.output)
189
+
190
+ # Check for end_of_stream signal
191
+ if output_dict.get("root", {}).get("protocol") == "end_of_stream":
192
+ logger.debug(
193
+ "End of stream received",
194
+ extra={
195
+ "module_address": module_address,
196
+ "module_port": module_port,
197
+ },
198
+ )
199
+ break
200
+
201
+ # Add job_id and success flag
202
+ response_dict = {
203
+ "success": response.success,
204
+ "job_id": response.job_id,
205
+ "output": output_dict,
206
+ }
207
+
208
+ logger.debug(
209
+ "Received module response",
210
+ extra={
211
+ "module_address": module_address,
212
+ "module_port": module_port,
213
+ "success": response.success,
214
+ "job_id": response.job_id,
215
+ },
216
+ )
217
+
218
+ # Call callback if provided
219
+ if callback:
220
+ await callback(response_dict)
221
+
222
+ yield response_dict
223
+
224
+ except Exception:
225
+ logger.exception(
226
+ "Failed to call module",
227
+ extra={
228
+ "module_address": module_address,
229
+ "module_port": module_port,
230
+ "setup_id": setup_id,
231
+ "mission_id": mission_id,
232
+ },
233
+ )
234
+ raise