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