digitalkin 0.3.1.dev1__py3-none-any.whl → 0.3.2a2__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 (87) 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/job_manager/base_job_manager.py +1 -1
  7. digitalkin/core/job_manager/single_job_manager.py +78 -36
  8. digitalkin/core/job_manager/taskiq_broker.py +8 -7
  9. digitalkin/core/job_manager/taskiq_job_manager.py +9 -5
  10. digitalkin/core/task_manager/base_task_manager.py +3 -1
  11. digitalkin/core/task_manager/surrealdb_repository.py +13 -7
  12. digitalkin/core/task_manager/task_executor.py +27 -10
  13. digitalkin/core/task_manager/task_session.py +133 -101
  14. digitalkin/grpc_servers/module_server.py +95 -171
  15. digitalkin/grpc_servers/module_servicer.py +133 -27
  16. digitalkin/grpc_servers/utils/grpc_client_wrapper.py +36 -10
  17. digitalkin/grpc_servers/utils/utility_schema_extender.py +106 -0
  18. digitalkin/models/__init__.py +1 -1
  19. digitalkin/models/core/job_manager_models.py +0 -8
  20. digitalkin/models/core/task_monitor.py +23 -1
  21. digitalkin/models/grpc_servers/models.py +95 -8
  22. digitalkin/models/module/__init__.py +26 -13
  23. digitalkin/models/module/base_types.py +61 -0
  24. digitalkin/models/module/module_context.py +279 -13
  25. digitalkin/models/module/module_types.py +29 -109
  26. digitalkin/models/module/setup_types.py +547 -0
  27. digitalkin/models/module/tool_cache.py +230 -0
  28. digitalkin/models/module/tool_reference.py +160 -0
  29. digitalkin/models/module/utility.py +167 -0
  30. digitalkin/models/services/cost.py +22 -1
  31. digitalkin/models/services/registry.py +77 -0
  32. digitalkin/modules/__init__.py +5 -1
  33. digitalkin/modules/_base_module.py +253 -90
  34. digitalkin/modules/archetype_module.py +6 -1
  35. digitalkin/modules/tool_module.py +6 -1
  36. digitalkin/modules/triggers/__init__.py +8 -0
  37. digitalkin/modules/triggers/healthcheck_ping_trigger.py +45 -0
  38. digitalkin/modules/triggers/healthcheck_services_trigger.py +63 -0
  39. digitalkin/modules/triggers/healthcheck_status_trigger.py +52 -0
  40. digitalkin/services/__init__.py +4 -0
  41. digitalkin/services/communication/__init__.py +7 -0
  42. digitalkin/services/communication/communication_strategy.py +87 -0
  43. digitalkin/services/communication/default_communication.py +104 -0
  44. digitalkin/services/communication/grpc_communication.py +264 -0
  45. digitalkin/services/cost/cost_strategy.py +36 -14
  46. digitalkin/services/cost/default_cost.py +61 -1
  47. digitalkin/services/cost/grpc_cost.py +98 -2
  48. digitalkin/services/filesystem/grpc_filesystem.py +9 -2
  49. digitalkin/services/registry/__init__.py +22 -1
  50. digitalkin/services/registry/default_registry.py +156 -4
  51. digitalkin/services/registry/exceptions.py +47 -0
  52. digitalkin/services/registry/grpc_registry.py +382 -0
  53. digitalkin/services/registry/registry_models.py +15 -0
  54. digitalkin/services/registry/registry_strategy.py +106 -4
  55. digitalkin/services/services_config.py +25 -3
  56. digitalkin/services/services_models.py +5 -1
  57. digitalkin/services/setup/default_setup.py +1 -1
  58. digitalkin/services/setup/grpc_setup.py +1 -1
  59. digitalkin/services/storage/grpc_storage.py +1 -1
  60. digitalkin/services/user_profile/__init__.py +11 -0
  61. digitalkin/services/user_profile/grpc_user_profile.py +2 -2
  62. digitalkin/services/user_profile/user_profile_strategy.py +0 -15
  63. digitalkin/utils/__init__.py +40 -0
  64. digitalkin/utils/conditional_schema.py +260 -0
  65. digitalkin/utils/dynamic_schema.py +487 -0
  66. digitalkin/utils/schema_splitter.py +290 -0
  67. {digitalkin-0.3.1.dev1.dist-info → digitalkin-0.3.2a2.dist-info}/METADATA +13 -13
  68. digitalkin-0.3.2a2.dist-info/RECORD +144 -0
  69. {digitalkin-0.3.1.dev1.dist-info → digitalkin-0.3.2a2.dist-info}/WHEEL +1 -1
  70. {digitalkin-0.3.1.dev1.dist-info → digitalkin-0.3.2a2.dist-info}/top_level.txt +1 -0
  71. modules/archetype_with_tools_module.py +232 -0
  72. modules/cpu_intensive_module.py +1 -1
  73. modules/dynamic_setup_module.py +338 -0
  74. modules/minimal_llm_module.py +1 -1
  75. modules/text_transform_module.py +1 -1
  76. monitoring/digitalkin_observability/__init__.py +46 -0
  77. monitoring/digitalkin_observability/http_server.py +150 -0
  78. monitoring/digitalkin_observability/interceptors.py +176 -0
  79. monitoring/digitalkin_observability/metrics.py +201 -0
  80. monitoring/digitalkin_observability/prometheus.py +137 -0
  81. monitoring/tests/test_metrics.py +172 -0
  82. services/filesystem_module.py +7 -5
  83. services/storage_module.py +4 -2
  84. digitalkin/grpc_servers/registry_server.py +0 -65
  85. digitalkin/grpc_servers/registry_servicer.py +0 -456
  86. digitalkin-0.3.1.dev1.dist-info/RECORD +0 -117
  87. {digitalkin-0.3.1.dev1.dist-info → digitalkin-0.3.2a2.dist-info}/licenses/LICENSE +0 -0
@@ -1,36 +1,31 @@
1
1
  """Module gRPC server implementation for DigitalKin."""
2
2
 
3
- import uuid
4
- from pathlib import Path
3
+ from typing import TYPE_CHECKING
5
4
 
6
- import grpc
7
- from digitalkin_proto.agentic_mesh_protocol.module.v1 import (
5
+ from agentic_mesh_protocol.module.v1 import (
8
6
  module_service_pb2,
9
7
  module_service_pb2_grpc,
10
8
  )
11
- from digitalkin_proto.agentic_mesh_protocol.module_registry.v1 import (
12
- metadata_pb2,
13
- module_registry_service_pb2_grpc,
14
- registration_pb2,
15
- )
16
9
 
17
10
  from digitalkin.grpc_servers._base_server import BaseServer
18
11
  from digitalkin.grpc_servers.module_servicer import ModuleServicer
19
- from digitalkin.grpc_servers.utils.exceptions import ServerError
20
12
  from digitalkin.logger import logger
21
13
  from digitalkin.models.grpc_servers.models import (
22
14
  ClientConfig,
23
15
  ModuleServerConfig,
24
- SecurityMode,
25
16
  )
26
17
  from digitalkin.modules._base_module import BaseModule
18
+ from digitalkin.services.registry import GrpcRegistry
19
+
20
+ if TYPE_CHECKING:
21
+ from digitalkin.services.registry import RegistryStrategy
27
22
 
28
23
 
29
24
  class ModuleServer(BaseServer):
30
25
  """gRPC server for a DigitalKin module.
31
26
 
32
27
  This server exposes the module's functionality through the ModuleService gRPC interface.
33
- It can optionally register itself with a ModuleRegistry server.
28
+ It can optionally register itself with a Registry server.
34
29
 
35
30
  Attributes:
36
31
  module: The module instance being served.
@@ -49,14 +44,17 @@ class ModuleServer(BaseServer):
49
44
 
50
45
  Args:
51
46
  module_class: The module instance to be served.
52
- server_config: Server configuration including registry address if auto-registration is desired.
53
- client_config: Client configuration used by services.
47
+ server_config: Server configuration.
48
+ client_config: Client configuration used by services and registry connection.
54
49
  """
55
50
  super().__init__(server_config)
56
51
  self.module_class = module_class
57
52
  self.server_config = server_config
58
53
  self.client_config = client_config
59
54
  self.module_servicer: ModuleServicer | None = None
55
+ self.registry: RegistryStrategy | None = None
56
+
57
+ self._prepare_registry_config()
60
58
 
61
59
  def _register_servicers(self) -> None:
62
60
  """Register the module servicer with the gRPC server.
@@ -77,17 +75,34 @@ class ModuleServer(BaseServer):
77
75
  )
78
76
  logger.debug("Registered Module servicer")
79
77
 
78
+ def _prepare_registry_config(self) -> None:
79
+ """Prepare registry client config on module_class before server starts.
80
+
81
+ This ensures ServicesConfig created by JobManager will have registry config,
82
+ allowing spawned module instances to inherit the registry configuration.
83
+ """
84
+ if not self.client_config:
85
+ return
86
+
87
+ self.module_class.services_config_params["registry"] = {"client_config": self.client_config}
88
+
89
+ def _init_registry(self) -> None:
90
+ """Initialize server-level registry client for registration."""
91
+ if not self.client_config:
92
+ return
93
+
94
+ self.registry = GrpcRegistry("", "", "", self.client_config)
95
+
80
96
  def start(self) -> None:
81
97
  """Start the module server and register with the registry if configured."""
82
98
  logger.info("Starting module server", extra={"server_config": self.server_config})
83
99
  super().start()
84
100
 
85
- # If a registry address is provided, register the module
86
- if self.server_config.registry_address:
87
- try:
88
- self._register_with_registry()
89
- except Exception:
90
- logger.exception("Failed to register with registry")
101
+ try:
102
+ self._init_registry()
103
+ self._register_with_registry()
104
+ except Exception:
105
+ logger.exception("Failed to register with registry")
91
106
 
92
107
  if self.module_servicer is not None:
93
108
  logger.debug("Setup post init started", extra={"client_config": self.client_config})
@@ -97,171 +112,80 @@ class ModuleServer(BaseServer):
97
112
  """Start the module server and register with the registry if configured."""
98
113
  logger.info("Starting module server", extra={"server_config": self.server_config})
99
114
  await super().start_async()
100
- # If a registry address is provided, register the module
101
- if self.server_config.registry_address:
102
- try:
103
- self._register_with_registry()
104
- except Exception:
105
- logger.exception("Failed to register with registry")
115
+
116
+ try:
117
+ self._init_registry()
118
+ self._register_with_registry()
119
+ except Exception:
120
+ logger.exception("Failed to register with registry")
106
121
 
107
122
  if self.module_servicer is not None:
108
123
  logger.info("Setup post init started", extra={"client_config": self.client_config})
109
124
  await self.module_servicer.job_manager.start()
110
125
  self.module_servicer.setup.__post_init__(self.client_config)
111
126
 
112
- def stop(self, grace: float | None = None) -> None:
113
- """Stop the module server and deregister from the registry if needed."""
114
- # If registered with a registry, deregister
115
- if self.server_config.registry_address:
127
+ async def stop_async(self, grace: float | None = None) -> None:
128
+ """Stop the module server with async cleanup.
129
+
130
+ Deregisters from registry and stops the server. Modules also become
131
+ inactive when they stop sending heartbeats as a fallback.
132
+ """
133
+ if self.registry is not None:
116
134
  try:
117
- self._deregister_from_registry()
118
- except ServerError:
135
+ module_id = self.module_class.get_module_id()
136
+ if module_id and module_id != "unknown":
137
+ await self.registry.deregister(module_id)
138
+ except Exception:
119
139
  logger.exception("Failed to deregister from registry")
120
140
 
121
- super().stop(grace)
141
+ await super().stop_async(grace)
122
142
 
123
143
  def _register_with_registry(self) -> None:
124
- """Register this module with the registry server.
125
-
126
- Raises:
127
- ServerError: If communication with the registry server fails.
128
- """
129
- logger.debug(
130
- "Registering module with registry at %s",
131
- self.server_config.registry_address,
132
- extra={"server_config": self.server_config},
133
- )
134
-
135
- # Create appropriate channel based on security mode
136
- channel = self._create_registry_channel()
137
-
138
- with channel:
139
- # Create a stub (client)
140
- stub = module_registry_service_pb2_grpc.ModuleRegistryServiceStub(channel)
141
-
142
- # Determine module type
143
- module_type = self._determine_module_type()
144
-
145
- metadata = metadata_pb2.Metadata(
146
- name=self.module_class.metadata["name"],
147
- tags=[metadata_pb2.Tag(tag=tag) for tag in self.module_class.metadata["tags"]],
148
- description=self.module_class.metadata["description"],
144
+ """Register this module with the registry server."""
145
+ if not self.registry:
146
+ logger.debug("No registry configured, skipping registration")
147
+ return
148
+
149
+ module_id = self.module_class.get_module_id()
150
+ version = self.module_class.metadata.get("version", "0.0.0")
151
+
152
+ if not module_id or module_id == "unknown":
153
+ logger.warning(
154
+ "Module has no valid module_id, skipping registration",
155
+ extra={"module_class": self.module_class.__name__},
149
156
  )
150
-
151
- self.module_class.metadata["module_id"] = f"{self.module_class.metadata['name']}:{uuid.uuid4()}"
152
- # Create registration request
153
- request = registration_pb2.RegisterRequest(
154
- module_id=self.module_class.metadata["module_id"],
155
- version=self.module_class.metadata["version"],
156
- module_type=module_type,
157
- address=self.server_config.address,
158
- metadata=metadata,
159
- )
160
-
161
- try:
162
- # Call the register method
163
- logger.debug(
164
- "Request sent to registry for module: %s:%s",
165
- self.module_class.metadata["name"],
166
- self.module_class.metadata["module_id"],
167
- extra={"module_info": self.module_class.metadata},
168
- )
169
- response = stub.RegisterModule(request)
170
-
171
- if response.success:
172
- logger.debug("Module registered successfully")
173
- else:
174
- logger.error("Module registration failed")
175
- except grpc.RpcError:
176
- logger.exception("RPC error during registration:")
177
- raise ServerError
178
-
179
- def _deregister_from_registry(self) -> None:
180
- """Deregister this module from the registry server.
181
-
182
- Raises:
183
- ServerError: If communication with the registry server fails.
184
- """
185
- logger.debug(
186
- "Deregistering module from registry at %s",
187
- self.server_config.registry_address,
157
+ return
158
+
159
+ advertise_address = self.server_config.advertise_host or self.server_config.host
160
+
161
+ logger.info(
162
+ "Attempting to register module with registry",
163
+ extra={
164
+ "module_id": module_id,
165
+ "address": advertise_address,
166
+ "port": self.server_config.port,
167
+ "version": version,
168
+ },
188
169
  )
189
170
 
190
- # Create appropriate channel based on security mode
191
- channel = self._create_registry_channel()
192
-
193
- with channel:
194
- # Create a stub (client)
195
- stub = module_registry_service_pb2_grpc.ModuleRegistryServiceStub(channel)
171
+ result = self.registry.register(
172
+ module_id=module_id,
173
+ address=advertise_address,
174
+ port=self.server_config.port,
175
+ version=version,
176
+ )
196
177
 
197
- # Create deregistration request
198
- request = registration_pb2.DeregisterRequest(
199
- module_id=self.module_class.metadata["module_id"],
178
+ if result:
179
+ logger.info(
180
+ "Module registered successfully",
181
+ extra={
182
+ "module_id": result.module_id,
183
+ "address": advertise_address,
184
+ "port": self.server_config.port,
185
+ },
200
186
  )
201
- try:
202
- # Call the deregister method
203
- response = stub.DeregisterModule(request)
204
-
205
- if response.success:
206
- logger.debug("Module deregistered successfull")
207
- else:
208
- logger.error("Module deregistration failed")
209
- except grpc.RpcError:
210
- logger.exception("RPC error during deregistration")
211
- raise ServerError
212
-
213
- def _create_registry_channel(self) -> grpc.Channel:
214
- """Create an appropriate channel to the registry server.
215
-
216
- Returns:
217
- A gRPC channel for communication with the registry.
218
-
219
- Raises:
220
- ValueError: If credentials are required but not provided.
221
- """
222
- if (
223
- self.client_config is not None
224
- and self.client_config.security == SecurityMode.SECURE
225
- and self.client_config.credentials
226
- ):
227
- # Secure channel
228
- # Secure channel
229
- root_certificates = Path(self.client_config.credentials.root_cert_path).read_bytes()
230
-
231
- # mTLS channel
232
- private_key = None
233
- certificate_chain = None
234
- if (
235
- self.client_config.credentials.client_cert_path is not None
236
- and self.client_config.credentials.client_key_path is not None
237
- ):
238
- private_key = Path(self.client_config.credentials.client_key_path).read_bytes()
239
- certificate_chain = Path(self.client_config.credentials.client_cert_path).read_bytes()
240
-
241
- # Create channel credentials
242
- channel_credentials = grpc.ssl_channel_credentials(
243
- root_certificates=root_certificates,
244
- certificate_chain=certificate_chain,
245
- private_key=private_key,
187
+ else:
188
+ logger.warning(
189
+ "Module registration returned None (module may not exist in registry)",
190
+ extra={"module_id": module_id, "address": advertise_address},
246
191
  )
247
- return grpc.secure_channel(self.server_config.registry_address, channel_credentials)
248
- # Insecure channel
249
- return grpc.insecure_channel(self.server_config.registry_address)
250
-
251
- def _determine_module_type(self) -> str:
252
- """Determine the module type based on its class.
253
-
254
- Returns:
255
- A string representing the module type.
256
- """
257
- module_type = "UNKNOWN"
258
- class_name = self.module_class.__name__
259
-
260
- if class_name == "ToolModule":
261
- module_type = "TOOL"
262
- elif class_name == "TriggerModule":
263
- module_type = "TRIGGER"
264
- elif class_name == "ArchetypeModule":
265
- module_type = "KIN"
266
-
267
- return module_type
@@ -1,11 +1,12 @@
1
1
  """Module servicer implementation for DigitalKin."""
2
2
 
3
+ import asyncio
3
4
  from argparse import ArgumentParser, Namespace
4
5
  from collections.abc import AsyncGenerator
5
6
  from typing import Any
6
7
 
7
8
  import grpc
8
- from digitalkin_proto.agentic_mesh_protocol.module.v1 import (
9
+ from agentic_mesh_protocol.module.v1 import (
9
10
  information_pb2,
10
11
  lifecycle_pb2,
11
12
  module_service_pb2_grpc,
@@ -17,8 +18,9 @@ from digitalkin.core.job_manager.base_job_manager import BaseJobManager
17
18
  from digitalkin.grpc_servers.utils.exceptions import ServicerError
18
19
  from digitalkin.logger import logger
19
20
  from digitalkin.models.core.job_manager_models import JobManagerMode
20
- from digitalkin.models.module.module import ModuleStatus
21
+ from digitalkin.models.module.module import ModuleCodeModel, ModuleStatus
21
22
  from digitalkin.modules._base_module import BaseModule
23
+ from digitalkin.services.registry import GrpcRegistry, RegistryStrategy
22
24
  from digitalkin.services.services_models import ServicesMode
23
25
  from digitalkin.services.setup.default_setup import DefaultSetup
24
26
  from digitalkin.services.setup.grpc_setup import GrpcSetup
@@ -40,6 +42,7 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
40
42
  args: Namespace
41
43
  setup: SetupStrategy
42
44
  job_manager: BaseJobManager
45
+ _registry_cache: RegistryStrategy | None = None
43
46
 
44
47
  def _add_parser_args(self, parser: ArgumentParser) -> None:
45
48
  super()._add_parser_args(parser)
@@ -82,6 +85,26 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
82
85
  )
83
86
  self.setup = GrpcSetup() if self.args.services_mode == ServicesMode.REMOTE else DefaultSetup()
84
87
 
88
+ def _get_registry(self) -> RegistryStrategy | None:
89
+ """Get a cached registry instance if configured.
90
+
91
+ Returns:
92
+ Cached GrpcRegistry instance if registry config exists, None otherwise.
93
+ """
94
+ if self._registry_cache is not None:
95
+ return self._registry_cache
96
+
97
+ registry_config = self.module_class.services_config_params.get("registry")
98
+ if not registry_config:
99
+ return None
100
+
101
+ client_config = registry_config.get("client_config")
102
+ if not client_config:
103
+ return None
104
+
105
+ self._registry_cache = GrpcRegistry("", "", "", client_config)
106
+ return self._registry_cache
107
+
85
108
  async def ConfigSetupModule( # noqa: N802
86
109
  self,
87
110
  request: lifecycle_pb2.ConfigSetupModuleRequest,
@@ -108,11 +131,9 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
108
131
  "mission_id": request.mission_id,
109
132
  },
110
133
  )
111
- # Process the module input
112
- # TODO: Secret should be used here as well
113
134
  setup_version = request.setup_version
114
135
  config_setup_data = self.module_class.create_config_setup_model(json_format.MessageToDict(request.content))
115
- setup_version_data = self.module_class.create_setup_model(
136
+ setup_version_data = await self.module_class.create_setup_model(
116
137
  json_format.MessageToDict(request.setup_version.content),
117
138
  config_fields=True,
118
139
  )
@@ -139,8 +160,33 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
139
160
  return lifecycle_pb2.ConfigSetupModuleResponse(success=False)
140
161
 
141
162
  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=}")
163
+ logger.info("Setup response received", extra={"job_id": job_id})
164
+
165
+ # Check if response is an error
166
+ if isinstance(updated_setup_data, ModuleCodeModel):
167
+ logger.error(
168
+ "Config setup failed",
169
+ extra={"job_id": job_id, "code": updated_setup_data.code, "error_message": updated_setup_data.message},
170
+ )
171
+ context.set_code(grpc.StatusCode.INTERNAL)
172
+ context.set_details(updated_setup_data.message or "Config setup failed")
173
+ return lifecycle_pb2.ConfigSetupModuleResponse(success=False)
174
+
175
+ if isinstance(updated_setup_data, dict) and "code" in updated_setup_data:
176
+ # ModuleCodeModel was serialized to dict
177
+ logger.error(
178
+ "Config setup failed",
179
+ extra={
180
+ "job_id": job_id,
181
+ "code": updated_setup_data["code"],
182
+ "error_message": updated_setup_data.get("message"),
183
+ },
184
+ )
185
+ context.set_code(grpc.StatusCode.INTERNAL)
186
+ context.set_details(updated_setup_data.get("message") or "Config setup failed")
187
+ return lifecycle_pb2.ConfigSetupModuleResponse(success=False)
188
+
189
+ logger.debug("Updated setup data", extra={"job_id": job_id, "setup_data": updated_setup_data})
144
190
  setup_version.content = json_format.ParseDict(
145
191
  updated_setup_data,
146
192
  struct_pb2.Struct(),
@@ -185,7 +231,7 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
185
231
  msg = "No setup data returned."
186
232
  raise ServicerError(msg)
187
233
 
188
- setup_data = self.module_class.create_setup_model(setup_data_class.current_setup_version.content)
234
+ setup_data = await self.module_class.create_setup_model(setup_data_class.current_setup_version.content)
189
235
 
190
236
  # create a task to run the module in background
191
237
  job_id = await self.job_manager.create_module_instance_job(
@@ -205,6 +251,11 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
205
251
  try:
206
252
  async with self.job_manager.generate_stream_consumer(job_id) as stream: # type: ignore
207
253
  async for message in stream:
254
+ # Early detection of client disconnection
255
+ if context.cancelled():
256
+ logger.info("Client disconnected", extra={"job_id": job_id})
257
+ break
258
+
208
259
  if message.get("error", None) is not None:
209
260
  logger.error("Error in output_data", extra={"message": message})
210
261
  context.set_code(message["error"]["code"])
@@ -219,19 +270,34 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
219
270
  yield lifecycle_pb2.StartModuleResponse(success=False, job_id=job_id)
220
271
  break
221
272
 
222
- if message.get("code", None) is not None and message.get("code") == "__END_OF_STREAM__":
273
+ logger.info("Yielding message from job %s: %s", job_id, message)
274
+ proto = json_format.ParseDict(message, struct_pb2.Struct(), ignore_unknown_fields=True)
275
+ yield lifecycle_pb2.StartModuleResponse(success=True, output=proto, job_id=job_id)
276
+
277
+ if message.get("root", {}).get("protocol") == "end_of_stream":
223
278
  logger.info(
224
- "End of stream via __END_OF_STREAM__",
279
+ "End of stream signal received",
225
280
  extra={"job_id": job_id, "mission_id": request.mission_id},
226
281
  )
227
282
  break
228
-
229
- logger.info("Yielding message from job %s: %s", job_id, message)
230
- proto = json_format.ParseDict(message, struct_pb2.Struct(), ignore_unknown_fields=True)
231
- yield lifecycle_pb2.StartModuleResponse(success=True, output=proto, job_id=job_id)
232
283
  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)
284
+ try:
285
+ await asyncio.wait_for(
286
+ self.job_manager.wait_for_completion(job_id),
287
+ timeout=30.0,
288
+ )
289
+ except Exception:
290
+ logger.exception(
291
+ "Error waiting for job completion",
292
+ extra={"job_id": job_id, "mission_id": request.mission_id},
293
+ )
294
+ try:
295
+ await self.job_manager.clean_session(job_id, mission_id=request.mission_id)
296
+ except Exception:
297
+ logger.exception(
298
+ "Error cleaning session",
299
+ extra={"job_id": job_id, "mission_id": request.mission_id},
300
+ )
235
301
 
236
302
  logger.info("Job %s finished", job_id)
237
303
 
@@ -249,17 +315,19 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
249
315
  Returns:
250
316
  A response indicating success or failure.
251
317
  """
252
- logger.debug("StopModule called for module: '%s'", self.module_class.__name__)
318
+ logger.debug(
319
+ "StopModule called",
320
+ extra={"module_class": self.module_class.__name__, "job_id": request.job_id},
321
+ )
253
322
 
254
323
  response: bool = await self.job_manager.stop_module(request.job_id)
255
324
  if not response:
256
- message = f"Job {request.job_id} not found"
257
- logger.warning(message)
325
+ logger.warning("Job not found for stop request", extra={"job_id": request.job_id})
258
326
  context.set_code(grpc.StatusCode.NOT_FOUND)
259
- context.set_details(message)
327
+ context.set_details(f"Job {request.job_id} not found")
260
328
  return lifecycle_pb2.StopModuleResponse(success=False)
261
329
 
262
- logger.debug("Job %s stopped successfully", request.job_id, extra={"job_id": request.job_id})
330
+ logger.debug("Job stopped successfully", extra={"job_id": request.job_id})
263
331
  return lifecycle_pb2.StopModuleResponse(success=True)
264
332
 
265
333
  async def GetModuleStatus( # noqa: N802
@@ -350,7 +418,9 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
350
418
  # Get input schema if available
351
419
  try:
352
420
  # Convert schema to proto format
353
- input_schema_proto = self.module_class.get_input_format(llm_format=request.llm_format)
421
+ input_schema_proto = await self.module_class.get_input_format(
422
+ llm_format=request.llm_format,
423
+ )
354
424
  input_format_struct = json_format.Parse(
355
425
  text=input_schema_proto,
356
426
  message=struct_pb2.Struct(), # pylint: disable=no-member
@@ -386,7 +456,9 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
386
456
  # Get output schema if available
387
457
  try:
388
458
  # Convert schema to proto format
389
- output_schema_proto = self.module_class.get_output_format(llm_format=request.llm_format)
459
+ output_schema_proto = await self.module_class.get_output_format(
460
+ llm_format=request.llm_format,
461
+ )
390
462
  output_format_struct = json_format.Parse(
391
463
  text=output_schema_proto,
392
464
  message=struct_pb2.Struct(), # pylint: disable=no-member
@@ -422,7 +494,7 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
422
494
  # Get setup schema if available
423
495
  try:
424
496
  # Convert schema to proto format
425
- setup_schema_proto = self.module_class.get_setup_format(llm_format=request.llm_format)
497
+ setup_schema_proto = await self.module_class.get_setup_format(llm_format=request.llm_format)
426
498
  setup_format_struct = json_format.Parse(
427
499
  text=setup_schema_proto,
428
500
  message=struct_pb2.Struct(), # pylint: disable=no-member
@@ -439,7 +511,7 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
439
511
  setup_schema=setup_format_struct,
440
512
  )
441
513
 
442
- def GetModuleSecret( # noqa: N802
514
+ async def GetModuleSecret( # noqa: N802
443
515
  self,
444
516
  request: information_pb2.GetModuleSecretRequest,
445
517
  context: grpc.ServicerContext,
@@ -458,7 +530,7 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
458
530
  # Get secret schema if available
459
531
  try:
460
532
  # Convert schema to proto format
461
- secret_schema_proto = self.module_class.get_secret_format(llm_format=request.llm_format)
533
+ secret_schema_proto = await self.module_class.get_secret_format(llm_format=request.llm_format)
462
534
  secret_format_struct = json_format.Parse(
463
535
  text=secret_schema_proto,
464
536
  message=struct_pb2.Struct(), # pylint: disable=no-member
@@ -494,7 +566,7 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
494
566
  # Get setup schema if available
495
567
  try:
496
568
  # Convert schema to proto format
497
- config_setup_schema_proto = self.module_class.get_config_setup_format(llm_format=request.llm_format)
569
+ config_setup_schema_proto = await self.module_class.get_config_setup_format(llm_format=request.llm_format)
498
570
  config_setup_format_struct = json_format.Parse(
499
571
  text=config_setup_schema_proto,
500
572
  message=struct_pb2.Struct(), # pylint: disable=no-member
@@ -510,3 +582,37 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
510
582
  success=True,
511
583
  config_setup_schema=config_setup_format_struct,
512
584
  )
585
+
586
+ async def GetModuleCost( # noqa: N802
587
+ self,
588
+ request: information_pb2.GetModuleCostRequest,
589
+ context: grpc.ServicerContext,
590
+ ) -> information_pb2.GetModuleCostResponse:
591
+ """Get information about the module's cost configuration.
592
+
593
+ Args:
594
+ request: The get module cost request.
595
+ context: The gRPC context.
596
+
597
+ Returns:
598
+ A response with the module's cost schema.
599
+ """
600
+ logger.debug("GetModuleCost called for module: '%s'", self.module_class.__name__)
601
+
602
+ try:
603
+ cost_schema_proto = await self.module_class.get_cost_format(llm_format=request.llm_format)
604
+ cost_format_struct = json_format.Parse(
605
+ text=cost_schema_proto,
606
+ message=struct_pb2.Struct(),
607
+ ignore_unknown_fields=True,
608
+ )
609
+ except NotImplementedError as e:
610
+ logger.warning(e)
611
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
612
+ context.set_details(str(e))
613
+ return information_pb2.GetModuleCostResponse()
614
+
615
+ return information_pb2.GetModuleCostResponse(
616
+ success=True,
617
+ cost_schema=cost_format_struct,
618
+ )