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.
- base_server/server_async_insecure.py +6 -5
- base_server/server_async_secure.py +6 -5
- base_server/server_sync_insecure.py +5 -4
- base_server/server_sync_secure.py +5 -4
- digitalkin/__version__.py +1 -1
- digitalkin/core/job_manager/base_job_manager.py +1 -1
- digitalkin/core/job_manager/single_job_manager.py +78 -36
- digitalkin/core/job_manager/taskiq_broker.py +8 -7
- digitalkin/core/job_manager/taskiq_job_manager.py +9 -5
- digitalkin/core/task_manager/base_task_manager.py +3 -1
- digitalkin/core/task_manager/surrealdb_repository.py +13 -7
- digitalkin/core/task_manager/task_executor.py +27 -10
- digitalkin/core/task_manager/task_session.py +133 -101
- digitalkin/grpc_servers/module_server.py +95 -171
- digitalkin/grpc_servers/module_servicer.py +133 -27
- digitalkin/grpc_servers/utils/grpc_client_wrapper.py +36 -10
- digitalkin/grpc_servers/utils/utility_schema_extender.py +106 -0
- digitalkin/models/__init__.py +1 -1
- digitalkin/models/core/job_manager_models.py +0 -8
- digitalkin/models/core/task_monitor.py +23 -1
- digitalkin/models/grpc_servers/models.py +95 -8
- digitalkin/models/module/__init__.py +26 -13
- digitalkin/models/module/base_types.py +61 -0
- digitalkin/models/module/module_context.py +279 -13
- digitalkin/models/module/module_types.py +29 -109
- digitalkin/models/module/setup_types.py +547 -0
- digitalkin/models/module/tool_cache.py +230 -0
- digitalkin/models/module/tool_reference.py +160 -0
- digitalkin/models/module/utility.py +167 -0
- digitalkin/models/services/cost.py +22 -1
- digitalkin/models/services/registry.py +77 -0
- digitalkin/modules/__init__.py +5 -1
- digitalkin/modules/_base_module.py +253 -90
- digitalkin/modules/archetype_module.py +6 -1
- digitalkin/modules/tool_module.py +6 -1
- digitalkin/modules/triggers/__init__.py +8 -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/services/__init__.py +4 -0
- digitalkin/services/communication/__init__.py +7 -0
- digitalkin/services/communication/communication_strategy.py +87 -0
- digitalkin/services/communication/default_communication.py +104 -0
- digitalkin/services/communication/grpc_communication.py +264 -0
- digitalkin/services/cost/cost_strategy.py +36 -14
- digitalkin/services/cost/default_cost.py +61 -1
- digitalkin/services/cost/grpc_cost.py +98 -2
- digitalkin/services/filesystem/grpc_filesystem.py +9 -2
- digitalkin/services/registry/__init__.py +22 -1
- digitalkin/services/registry/default_registry.py +156 -4
- digitalkin/services/registry/exceptions.py +47 -0
- digitalkin/services/registry/grpc_registry.py +382 -0
- digitalkin/services/registry/registry_models.py +15 -0
- digitalkin/services/registry/registry_strategy.py +106 -4
- digitalkin/services/services_config.py +25 -3
- digitalkin/services/services_models.py +5 -1
- digitalkin/services/setup/default_setup.py +1 -1
- digitalkin/services/setup/grpc_setup.py +1 -1
- digitalkin/services/storage/grpc_storage.py +1 -1
- digitalkin/services/user_profile/__init__.py +11 -0
- digitalkin/services/user_profile/grpc_user_profile.py +2 -2
- digitalkin/services/user_profile/user_profile_strategy.py +0 -15
- digitalkin/utils/__init__.py +40 -0
- digitalkin/utils/conditional_schema.py +260 -0
- digitalkin/utils/dynamic_schema.py +487 -0
- digitalkin/utils/schema_splitter.py +290 -0
- {digitalkin-0.3.1.dev1.dist-info → digitalkin-0.3.2a2.dist-info}/METADATA +13 -13
- digitalkin-0.3.2a2.dist-info/RECORD +144 -0
- {digitalkin-0.3.1.dev1.dist-info → digitalkin-0.3.2a2.dist-info}/WHEEL +1 -1
- {digitalkin-0.3.1.dev1.dist-info → digitalkin-0.3.2a2.dist-info}/top_level.txt +1 -0
- modules/archetype_with_tools_module.py +232 -0
- modules/cpu_intensive_module.py +1 -1
- modules/dynamic_setup_module.py +338 -0
- modules/minimal_llm_module.py +1 -1
- modules/text_transform_module.py +1 -1
- monitoring/digitalkin_observability/__init__.py +46 -0
- monitoring/digitalkin_observability/http_server.py +150 -0
- monitoring/digitalkin_observability/interceptors.py +176 -0
- monitoring/digitalkin_observability/metrics.py +201 -0
- monitoring/digitalkin_observability/prometheus.py +137 -0
- monitoring/tests/test_metrics.py +172 -0
- services/filesystem_module.py +7 -5
- services/storage_module.py +4 -2
- digitalkin/grpc_servers/registry_server.py +0 -65
- digitalkin/grpc_servers/registry_servicer.py +0 -456
- digitalkin-0.3.1.dev1.dist-info/RECORD +0 -117
- {digitalkin-0.3.1.dev1.dist-info → digitalkin-0.3.2a2.dist-info}/licenses/LICENSE +0 -0
|
@@ -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)
|
digitalkin/services/__init__.py
CHANGED
|
@@ -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,87 @@
|
|
|
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 cleanup(self) -> None:
|
|
23
|
+
"""Clean up communication resources.
|
|
24
|
+
|
|
25
|
+
This method should release any held resources such as
|
|
26
|
+
gRPC channels, connection pools, etc.
|
|
27
|
+
"""
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
async def get_module_schemas(
|
|
32
|
+
self,
|
|
33
|
+
module_address: str,
|
|
34
|
+
module_port: int,
|
|
35
|
+
*,
|
|
36
|
+
llm_format: bool = False,
|
|
37
|
+
) -> dict[str, dict]:
|
|
38
|
+
"""Get module schemas (input/output/setup/secret/cost).
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
module_address: Target module address
|
|
42
|
+
module_port: Target module port
|
|
43
|
+
llm_format: Return LLM-friendly format (simplified schema).
|
|
44
|
+
Note: cost always returns actual data regardless of this flag.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Dictionary containing schemas:
|
|
48
|
+
{
|
|
49
|
+
"input": {...},
|
|
50
|
+
"output": {...},
|
|
51
|
+
"setup": {...},
|
|
52
|
+
"secret": {...},
|
|
53
|
+
"cost": {...}
|
|
54
|
+
}
|
|
55
|
+
"""
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def call_module(
|
|
60
|
+
self,
|
|
61
|
+
module_address: str,
|
|
62
|
+
module_port: int,
|
|
63
|
+
input_data: dict,
|
|
64
|
+
setup_id: str,
|
|
65
|
+
mission_id: str,
|
|
66
|
+
callback: Callable[[dict], Awaitable[None]] | None = None,
|
|
67
|
+
) -> AsyncGenerator[dict, None]:
|
|
68
|
+
"""Call a module and stream responses.
|
|
69
|
+
|
|
70
|
+
Uses Module Service StartModule RPC to execute the module.
|
|
71
|
+
Streams responses as they are generated by the module.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
module_address: Target module address
|
|
75
|
+
module_port: Target module port
|
|
76
|
+
input_data: Input data as dictionary
|
|
77
|
+
setup_id: Setup configuration ID
|
|
78
|
+
mission_id: Mission context ID
|
|
79
|
+
callback: Optional callback for each response
|
|
80
|
+
|
|
81
|
+
Yields:
|
|
82
|
+
Streaming responses from module as dictionaries
|
|
83
|
+
"""
|
|
84
|
+
# Make this an actual async generator to satisfy type checkers
|
|
85
|
+
if False: # pragma: no cover
|
|
86
|
+
yield {}
|
|
87
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
|
102
|
+
|
|
103
|
+
async def cleanup(self) -> None:
|
|
104
|
+
"""No-op for local communication."""
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""gRPC client implementation for Communication service."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import AsyncGenerator, Awaitable, Callable
|
|
5
|
+
|
|
6
|
+
import grpc
|
|
7
|
+
from agentic_mesh_protocol.module.v1 import (
|
|
8
|
+
information_pb2,
|
|
9
|
+
lifecycle_pb2,
|
|
10
|
+
module_service_pb2_grpc,
|
|
11
|
+
)
|
|
12
|
+
from google.protobuf import json_format, struct_pb2
|
|
13
|
+
|
|
14
|
+
from digitalkin.grpc_servers.utils.grpc_client_wrapper import GrpcClientWrapper
|
|
15
|
+
from digitalkin.logger import logger
|
|
16
|
+
from digitalkin.models.grpc_servers.models import ClientConfig
|
|
17
|
+
from digitalkin.services.base_strategy import BaseStrategy
|
|
18
|
+
from digitalkin.services.communication.communication_strategy import CommunicationStrategy
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GrpcCommunication(CommunicationStrategy, GrpcClientWrapper):
|
|
22
|
+
"""gRPC client for module-to-module communication.
|
|
23
|
+
|
|
24
|
+
This class provides methods to communicate with remote modules
|
|
25
|
+
using the Module Service gRPC protocol.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
mission_id: str,
|
|
31
|
+
setup_id: str,
|
|
32
|
+
setup_version_id: str,
|
|
33
|
+
client_config: ClientConfig,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Initialize the gRPC communication client.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
mission_id: Mission identifier
|
|
39
|
+
setup_id: Setup identifier
|
|
40
|
+
setup_version_id: Setup version identifier
|
|
41
|
+
client_config: Client configuration for gRPC connection
|
|
42
|
+
"""
|
|
43
|
+
BaseStrategy.__init__(self, mission_id, setup_id, setup_version_id)
|
|
44
|
+
self.client_config = client_config
|
|
45
|
+
self._channel_pool: dict[tuple[str, int], grpc.Channel] = {}
|
|
46
|
+
|
|
47
|
+
logger.debug(
|
|
48
|
+
"Initialized GrpcCommunication",
|
|
49
|
+
extra={"security": client_config.security},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def _get_or_create_channel(self, module_address: str, module_port: int) -> grpc.Channel:
|
|
53
|
+
"""Get existing channel or create new one for the target module.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
module_address: Module host address
|
|
57
|
+
module_port: Module port
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
gRPC channel for the target module
|
|
61
|
+
"""
|
|
62
|
+
key = (module_address, module_port)
|
|
63
|
+
if key not in self._channel_pool:
|
|
64
|
+
logger.debug(
|
|
65
|
+
"Creating new channel",
|
|
66
|
+
extra={"address": module_address, "port": module_port},
|
|
67
|
+
)
|
|
68
|
+
config = ClientConfig(
|
|
69
|
+
host=module_address,
|
|
70
|
+
port=module_port,
|
|
71
|
+
mode=self.client_config.mode,
|
|
72
|
+
security=self.client_config.security,
|
|
73
|
+
credentials=self.client_config.credentials,
|
|
74
|
+
channel_options=self.client_config.channel_options,
|
|
75
|
+
)
|
|
76
|
+
self._channel_pool[key] = self._init_channel(config)
|
|
77
|
+
return self._channel_pool[key]
|
|
78
|
+
|
|
79
|
+
def close_all_channels(self) -> None:
|
|
80
|
+
"""Close all pooled gRPC channels."""
|
|
81
|
+
for channel in self._channel_pool.values():
|
|
82
|
+
channel.close()
|
|
83
|
+
self._channel_pool.clear()
|
|
84
|
+
|
|
85
|
+
async def cleanup(self) -> None:
|
|
86
|
+
"""Clean up all gRPC channels."""
|
|
87
|
+
self.close_all_channels()
|
|
88
|
+
|
|
89
|
+
def _create_stub(self, module_address: str, module_port: int) -> module_service_pb2_grpc.ModuleServiceStub:
|
|
90
|
+
"""Create a new stub for the target module.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
module_address: Module host address
|
|
94
|
+
module_port: Module port
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
ModuleServiceStub for the target module
|
|
98
|
+
"""
|
|
99
|
+
channel = self._get_or_create_channel(module_address, module_port)
|
|
100
|
+
return module_service_pb2_grpc.ModuleServiceStub(channel)
|
|
101
|
+
|
|
102
|
+
async def get_module_schemas(
|
|
103
|
+
self,
|
|
104
|
+
module_address: str,
|
|
105
|
+
module_port: int,
|
|
106
|
+
*,
|
|
107
|
+
llm_format: bool = False,
|
|
108
|
+
) -> dict[str, dict]:
|
|
109
|
+
"""Get module schemas via gRPC.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
module_address: Target module address
|
|
113
|
+
module_port: Target module port
|
|
114
|
+
llm_format: Return LLM-friendly format
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Dictionary containing schemas: input, output, setup, secret, cost
|
|
118
|
+
"""
|
|
119
|
+
stub = self._create_stub(module_address, module_port)
|
|
120
|
+
|
|
121
|
+
# Create requests
|
|
122
|
+
# Note: cost always uses llm_format=False to get actual config data (rates, units)
|
|
123
|
+
# No LLM are allowed to set costs
|
|
124
|
+
input_request = information_pb2.GetModuleInputRequest(llm_format=llm_format)
|
|
125
|
+
output_request = information_pb2.GetModuleOutputRequest(llm_format=llm_format)
|
|
126
|
+
setup_request = information_pb2.GetModuleSetupRequest(llm_format=llm_format)
|
|
127
|
+
secret_request = information_pb2.GetModuleSecretRequest(llm_format=llm_format)
|
|
128
|
+
cost_request = information_pb2.GetModuleCostRequest(llm_format=False)
|
|
129
|
+
|
|
130
|
+
# Get all schemas in parallel
|
|
131
|
+
try:
|
|
132
|
+
input_response, output_response, setup_response, secret_response, cost_response = await asyncio.gather(
|
|
133
|
+
asyncio.to_thread(stub.GetModuleInput, input_request),
|
|
134
|
+
asyncio.to_thread(stub.GetModuleOutput, output_request),
|
|
135
|
+
asyncio.to_thread(stub.GetModuleSetup, setup_request),
|
|
136
|
+
asyncio.to_thread(stub.GetModuleSecret, secret_request),
|
|
137
|
+
asyncio.to_thread(stub.GetModuleCost, cost_request),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
logger.debug(
|
|
141
|
+
"Retrieved module schemas",
|
|
142
|
+
extra={
|
|
143
|
+
"module_address": module_address,
|
|
144
|
+
"module_port": module_port,
|
|
145
|
+
"llm_format": llm_format,
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
"input": json_format.MessageToDict(input_response.input_schema),
|
|
151
|
+
"output": json_format.MessageToDict(output_response.output_schema),
|
|
152
|
+
"setup": json_format.MessageToDict(setup_response.setup_schema),
|
|
153
|
+
"secret": json_format.MessageToDict(secret_response.secret_schema),
|
|
154
|
+
"cost": json_format.MessageToDict(cost_response.cost_schema),
|
|
155
|
+
}
|
|
156
|
+
except Exception:
|
|
157
|
+
logger.exception(
|
|
158
|
+
"Failed to get module schemas",
|
|
159
|
+
extra={
|
|
160
|
+
"module_address": module_address,
|
|
161
|
+
"module_port": module_port,
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
raise
|
|
165
|
+
|
|
166
|
+
async def call_module(
|
|
167
|
+
self,
|
|
168
|
+
module_address: str,
|
|
169
|
+
module_port: int,
|
|
170
|
+
input_data: dict,
|
|
171
|
+
setup_id: str,
|
|
172
|
+
mission_id: str,
|
|
173
|
+
callback: Callable[[dict], Awaitable[None]] | None = None,
|
|
174
|
+
) -> AsyncGenerator[dict, None]:
|
|
175
|
+
"""Call a module and stream responses via gRPC.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
module_address: Target module address
|
|
179
|
+
module_port: Target module port
|
|
180
|
+
input_data: Input data as dictionary
|
|
181
|
+
setup_id: Setup configuration ID
|
|
182
|
+
mission_id: Mission context ID
|
|
183
|
+
callback: Optional callback for each response
|
|
184
|
+
|
|
185
|
+
Yields:
|
|
186
|
+
Streaming responses from module as dictionaries
|
|
187
|
+
"""
|
|
188
|
+
stub = self._create_stub(module_address, module_port)
|
|
189
|
+
|
|
190
|
+
# Convert input data to protobuf Struct
|
|
191
|
+
input_struct = struct_pb2.Struct()
|
|
192
|
+
input_struct.update(input_data)
|
|
193
|
+
|
|
194
|
+
# Create request
|
|
195
|
+
request = lifecycle_pb2.StartModuleRequest(
|
|
196
|
+
input=input_struct,
|
|
197
|
+
setup_id=setup_id,
|
|
198
|
+
mission_id=mission_id,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
logger.debug(
|
|
202
|
+
"Calling module",
|
|
203
|
+
extra={
|
|
204
|
+
"module_address": module_address,
|
|
205
|
+
"module_port": module_port,
|
|
206
|
+
"setup_id": setup_id,
|
|
207
|
+
"mission_id": mission_id,
|
|
208
|
+
},
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
# Call StartModule with streaming response
|
|
213
|
+
response_stream = stub.StartModule(request)
|
|
214
|
+
|
|
215
|
+
# Stream responses
|
|
216
|
+
for response in response_stream:
|
|
217
|
+
# Convert protobuf Struct to dict
|
|
218
|
+
output_dict = json_format.MessageToDict(response.output)
|
|
219
|
+
|
|
220
|
+
# Check for end_of_stream signal
|
|
221
|
+
if output_dict.get("root", {}).get("protocol") == "end_of_stream":
|
|
222
|
+
logger.debug(
|
|
223
|
+
"End of stream received",
|
|
224
|
+
extra={
|
|
225
|
+
"module_address": module_address,
|
|
226
|
+
"module_port": module_port,
|
|
227
|
+
},
|
|
228
|
+
)
|
|
229
|
+
break
|
|
230
|
+
|
|
231
|
+
# Add job_id and success flag
|
|
232
|
+
response_dict = {
|
|
233
|
+
"success": response.success,
|
|
234
|
+
"job_id": response.job_id,
|
|
235
|
+
"output": output_dict,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
logger.debug(
|
|
239
|
+
"Received module response",
|
|
240
|
+
extra={
|
|
241
|
+
"module_address": module_address,
|
|
242
|
+
"module_port": module_port,
|
|
243
|
+
"success": response.success,
|
|
244
|
+
"job_id": response.job_id,
|
|
245
|
+
},
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Call callback if provided
|
|
249
|
+
if callback:
|
|
250
|
+
await callback(response_dict)
|
|
251
|
+
|
|
252
|
+
yield response_dict
|
|
253
|
+
|
|
254
|
+
except Exception:
|
|
255
|
+
logger.exception(
|
|
256
|
+
"Failed to call module",
|
|
257
|
+
extra={
|
|
258
|
+
"module_address": module_address,
|
|
259
|
+
"module_port": module_port,
|
|
260
|
+
"setup_id": setup_id,
|
|
261
|
+
"mission_id": mission_id,
|
|
262
|
+
},
|
|
263
|
+
)
|
|
264
|
+
raise
|
|
@@ -6,6 +6,7 @@ from typing import Literal
|
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel
|
|
8
8
|
|
|
9
|
+
from digitalkin.models.services.cost import AmountLimit, QuantityLimit
|
|
9
10
|
from digitalkin.services.base_strategy import BaseStrategy
|
|
10
11
|
|
|
11
12
|
|
|
@@ -57,23 +58,25 @@ class CostServiceError(Exception):
|
|
|
57
58
|
class CostStrategy(BaseStrategy, ABC):
|
|
58
59
|
"""Abstract base class for cost strategies."""
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def set_limits(self, limits: list[QuantityLimit | AmountLimit]) -> None:
|
|
63
|
+
"""Set cost limits for this session.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
limits: List of CostLimit objects to enforce.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def check_limit(self, cost_config_name: str, quantity: float) -> bool:
|
|
71
|
+
"""Check if adding this cost would exceed any limits.
|
|
68
72
|
|
|
69
73
|
Args:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
cost_config_name: Name of the cost config.
|
|
75
|
+
quantity: Quantity to add.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
True if within limits, False if would exceed.
|
|
74
79
|
"""
|
|
75
|
-
super().__init__(mission_id, setup_id, setup_version_id)
|
|
76
|
-
self.config = config
|
|
77
80
|
|
|
78
81
|
@abstractmethod
|
|
79
82
|
def add(
|
|
@@ -98,3 +101,22 @@ class CostStrategy(BaseStrategy, ABC):
|
|
|
98
101
|
cost_types: list[Literal["TOKEN_INPUT", "TOKEN_OUTPUT", "API_CALL", "STORAGE", "TIME", "OTHER"]] | None = None,
|
|
99
102
|
) -> list[CostData]:
|
|
100
103
|
"""Get filtered costs."""
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
def get_cost_config(self) -> list[CostConfig]:
|
|
107
|
+
"""Get cost configuration for the current setup version.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
List of CostConfig objects from the database.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
@abstractmethod
|
|
114
|
+
def set_cost_config(self, configs: list[CostConfig]) -> bool:
|
|
115
|
+
"""Store cost configuration for the current setup version.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
configs: List of CostConfig objects to store.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
True if successfully stored.
|
|
122
|
+
"""
|