digitalkin 0.3.1.dev2__py3-none-any.whl → 0.3.2a3__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 +7 -6
- 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 +29 -7
- digitalkin/core/task_manager/task_executor.py +46 -12
- digitalkin/core/task_manager/task_session.py +132 -102
- digitalkin/grpc_servers/module_server.py +95 -171
- digitalkin/grpc_servers/module_servicer.py +121 -19
- 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 +28 -392
- 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 +188 -63
- 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 +15 -3
- digitalkin/utils/conditional_schema.py +260 -0
- digitalkin/utils/dynamic_schema.py +4 -0
- digitalkin/utils/schema_splitter.py +290 -0
- {digitalkin-0.3.1.dev2.dist-info → digitalkin-0.3.2a3.dist-info}/METADATA +12 -12
- digitalkin-0.3.2a3.dist-info/RECORD +144 -0
- {digitalkin-0.3.1.dev2.dist-info → digitalkin-0.3.2a3.dist-info}/WHEEL +1 -1
- {digitalkin-0.3.1.dev2.dist-info → digitalkin-0.3.2a3.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 +5 -29
- 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.dev2.dist-info/RECORD +0 -119
- {digitalkin-0.3.1.dev2.dist-info → digitalkin-0.3.2a3.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
+
"""
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from typing import Literal
|
|
4
4
|
|
|
5
5
|
from digitalkin.logger import logger
|
|
6
|
+
from digitalkin.models.services.cost import AmountLimit, QuantityLimit
|
|
6
7
|
from digitalkin.services.cost.cost_strategy import (
|
|
7
8
|
CostConfig,
|
|
8
9
|
CostData,
|
|
@@ -24,8 +25,46 @@ class DefaultCost(CostStrategy):
|
|
|
24
25
|
setup_version_id: The ID of the setup version this strategy is associated with
|
|
25
26
|
config: The configuration dictionary for the cost
|
|
26
27
|
"""
|
|
27
|
-
super().__init__(mission_id=mission_id, setup_id=setup_id, setup_version_id=setup_version_id
|
|
28
|
+
super().__init__(mission_id=mission_id, setup_id=setup_id, setup_version_id=setup_version_id)
|
|
29
|
+
self.config = config
|
|
28
30
|
self.db: dict[str, list[CostData]] = {}
|
|
31
|
+
self._limits: dict[str, QuantityLimit | AmountLimit] = {}
|
|
32
|
+
self._accumulated: dict[str, float] = {}
|
|
33
|
+
|
|
34
|
+
def set_limits(self, limits: list[QuantityLimit | AmountLimit]) -> None:
|
|
35
|
+
"""Set cost limits for this session.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
limits: List of CostLimit objects to enforce.
|
|
39
|
+
"""
|
|
40
|
+
self._limits = {limit.name: limit for limit in limits}
|
|
41
|
+
self._accumulated = {}
|
|
42
|
+
|
|
43
|
+
def check_limit(self, cost_config_name: str, quantity: float) -> bool:
|
|
44
|
+
"""Check if adding this cost would exceed any limits.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
cost_config_name: Name of the cost config.
|
|
48
|
+
quantity: Quantity to add.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if within limits, False if would exceed.
|
|
52
|
+
"""
|
|
53
|
+
limit = self._limits.get(cost_config_name)
|
|
54
|
+
if limit is None:
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
cost_config = self.config.get(cost_config_name)
|
|
58
|
+
if cost_config is None:
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
if limit.limit_type == "quantity":
|
|
62
|
+
current = self._accumulated.get(f"{cost_config_name}_quantity", 0)
|
|
63
|
+
return current + quantity <= limit.max_value
|
|
64
|
+
|
|
65
|
+
current = self._accumulated.get(f"{cost_config_name}_amount", 0)
|
|
66
|
+
projected = cost_config.rate * quantity
|
|
67
|
+
return current + projected <= limit.max_value
|
|
29
68
|
|
|
30
69
|
def add(
|
|
31
70
|
self,
|
|
@@ -112,3 +151,24 @@ class DefaultCost(CostStrategy):
|
|
|
112
151
|
for cost in self.db[self.mission_id]
|
|
113
152
|
if (names and cost.name in names) or (cost_types and cost.cost_type in cost_types)
|
|
114
153
|
]
|
|
154
|
+
|
|
155
|
+
def get_cost_config(self) -> list[CostConfig]:
|
|
156
|
+
"""Get cost configuration from in-memory config.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
List of CostConfig objects from the config dictionary.
|
|
160
|
+
"""
|
|
161
|
+
return list(self.config.values())
|
|
162
|
+
|
|
163
|
+
def set_cost_config(self, configs: list[CostConfig]) -> bool:
|
|
164
|
+
"""Store cost configuration in memory.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
configs: List of CostConfig objects to store.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
True if successfully stored.
|
|
171
|
+
"""
|
|
172
|
+
self.config = {config.cost_name: config for config in configs}
|
|
173
|
+
logger.debug("Cost configs stored in memory: %s", self.config)
|
|
174
|
+
return True
|