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,230 @@
|
|
|
1
|
+
"""Tool cache for resolved tool references."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from digitalkin.logger import logger
|
|
8
|
+
from digitalkin.models.services.registry import ModuleInfo
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SelectedTool(BaseModel):
|
|
12
|
+
"""Selected tool information."""
|
|
13
|
+
|
|
14
|
+
setup_id: str = ""
|
|
15
|
+
module_id: str = ""
|
|
16
|
+
slug: str = ""
|
|
17
|
+
name: str = ""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from digitalkin.services.communication import CommunicationStrategy
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ToolParameter(BaseModel):
|
|
25
|
+
"""Definition of a single tool parameter.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
name: Parameter name.
|
|
29
|
+
type: JSON Schema type (string, integer, number, boolean, array, object).
|
|
30
|
+
description: Parameter description for the LLM.
|
|
31
|
+
required: Whether this parameter is required.
|
|
32
|
+
enum: Optional list of allowed values.
|
|
33
|
+
items: Optional schema for array item types.
|
|
34
|
+
properties: Optional schema for object properties.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
type: str
|
|
39
|
+
description: str
|
|
40
|
+
required: bool = True
|
|
41
|
+
enum: list[str] | None = None
|
|
42
|
+
items: dict[str, Any] | None = None
|
|
43
|
+
properties: dict[str, Any] | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ToolDefinition(BaseModel):
|
|
47
|
+
"""Complete definition of an LLM tool with grouped parameters.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
name: Tool name (from protocol const or trigger class name).
|
|
51
|
+
description: Tool description (from trigger docstring).
|
|
52
|
+
parameters: List of parameter definitions.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
name: str
|
|
56
|
+
description: str
|
|
57
|
+
parameters: list[ToolParameter] = Field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ToolModuleInfo(ModuleInfo):
|
|
61
|
+
"""Module info for tool modules."""
|
|
62
|
+
|
|
63
|
+
tools: list[ToolDefinition] = Field(default_factory=list)
|
|
64
|
+
tool_name: str = ""
|
|
65
|
+
setup_id: str = ""
|
|
66
|
+
cost_config: dict[str, Any] = Field(default_factory=dict)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def slug(self) -> str:
|
|
70
|
+
"""Module ID."""
|
|
71
|
+
return self.setup_id + "_" + self.tool_name
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ToolCache(BaseModel):
|
|
75
|
+
"""Registry cache storing resolved tool references by setup field name."""
|
|
76
|
+
|
|
77
|
+
entries: dict[str, ToolModuleInfo] = Field(default_factory=dict)
|
|
78
|
+
|
|
79
|
+
def add(self, tool_module_info: ToolModuleInfo) -> None:
|
|
80
|
+
"""Add a tool to the cache.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
tool_module_info: Resolved tool module information.
|
|
84
|
+
"""
|
|
85
|
+
self.entries[tool_module_info.slug] = tool_module_info
|
|
86
|
+
logger.debug(
|
|
87
|
+
"Tool cached",
|
|
88
|
+
extra={
|
|
89
|
+
"slug": tool_module_info.slug,
|
|
90
|
+
"module_id": tool_module_info.module_id,
|
|
91
|
+
"setup_id": tool_module_info.setup_id,
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def get(
|
|
96
|
+
self,
|
|
97
|
+
slug: str,
|
|
98
|
+
) -> ToolModuleInfo | None:
|
|
99
|
+
"""Get a tool from cache, optionally querying registry on miss.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
slug: Field name to look up.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
ToolModuleInfo if found, None otherwise.
|
|
106
|
+
"""
|
|
107
|
+
return self.entries.get(slug)
|
|
108
|
+
|
|
109
|
+
def clear(self) -> None:
|
|
110
|
+
"""Clear all cache entries."""
|
|
111
|
+
self.entries.clear()
|
|
112
|
+
|
|
113
|
+
def list_tools(self) -> list[str]:
|
|
114
|
+
"""List all cached tool names.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of setup field names in cache.
|
|
118
|
+
"""
|
|
119
|
+
return list(self.entries.keys())
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def module_info_to_tool_module_info(
|
|
123
|
+
module_info: ModuleInfo,
|
|
124
|
+
setup_id: str,
|
|
125
|
+
tool_name: str,
|
|
126
|
+
communication: "CommunicationStrategy",
|
|
127
|
+
*,
|
|
128
|
+
llm_format: bool = True,
|
|
129
|
+
) -> ToolModuleInfo:
|
|
130
|
+
"""Convert ModuleInfo to ToolModuleInfo by fetching schemas via gRPC.
|
|
131
|
+
|
|
132
|
+
Fetches the module's input schema and extracts tool definitions from
|
|
133
|
+
the discriminated union structure.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
module_info: Module info from registry.
|
|
137
|
+
setup_id: Setup ID of the selected tool.
|
|
138
|
+
tool_name: Name of the tool.
|
|
139
|
+
communication: Communication strategy for gRPC calls.
|
|
140
|
+
llm_format: Use LLM-friendly schema format.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
ToolModuleInfo with tools extracted from input schema.
|
|
144
|
+
"""
|
|
145
|
+
schemas = await communication.get_module_schemas(
|
|
146
|
+
module_info.address,
|
|
147
|
+
module_info.port,
|
|
148
|
+
llm_format=llm_format,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
input_schema = schemas.get("input", {})
|
|
152
|
+
if llm_format:
|
|
153
|
+
input_schema = input_schema.get("json_schema", input_schema)
|
|
154
|
+
|
|
155
|
+
tools = _extract_tools_from_schema(input_schema)
|
|
156
|
+
cost_config = schemas.get("cost", {})
|
|
157
|
+
|
|
158
|
+
return ToolModuleInfo(
|
|
159
|
+
module_id=module_info.module_id,
|
|
160
|
+
module_type=module_info.module_type,
|
|
161
|
+
address=module_info.address,
|
|
162
|
+
port=module_info.port,
|
|
163
|
+
version=module_info.version,
|
|
164
|
+
module_name=module_info.module_name,
|
|
165
|
+
documentation=module_info.documentation,
|
|
166
|
+
status=module_info.status,
|
|
167
|
+
tools=tools,
|
|
168
|
+
setup_id=setup_id,
|
|
169
|
+
tool_name=tool_name,
|
|
170
|
+
cost_config=cost_config,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _extract_tools_from_schema(schema: dict[str, Any]) -> list[ToolDefinition]:
|
|
175
|
+
"""Extract tool definitions from a discriminated union input schema.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
schema: JSON schema with $defs containing protocol-based types.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
List of ToolDefinition with parameters grouped by trigger.
|
|
182
|
+
"""
|
|
183
|
+
tools: list[ToolDefinition] = []
|
|
184
|
+
defs = schema.get("$defs", {})
|
|
185
|
+
|
|
186
|
+
# Skip SDK utility protocols
|
|
187
|
+
utility_protocols = {"HealthcheckPingInput", "HealthcheckServicesInput", "HealthcheckStatusInput"}
|
|
188
|
+
|
|
189
|
+
for def_name, def_schema in defs.items():
|
|
190
|
+
if def_name in utility_protocols:
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
properties = def_schema.get("properties", {})
|
|
194
|
+
protocol_prop = properties.get("protocol", {})
|
|
195
|
+
|
|
196
|
+
# Skip if no protocol const (not a tool input type)
|
|
197
|
+
if "const" not in protocol_prop:
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
# Extract tool-level info from trigger
|
|
201
|
+
tool_name = protocol_prop.get("const", def_name)
|
|
202
|
+
tool_description = def_schema.get("description", "")
|
|
203
|
+
|
|
204
|
+
required_fields = set(def_schema.get("required", []))
|
|
205
|
+
parameters: list[ToolParameter] = []
|
|
206
|
+
|
|
207
|
+
for prop_name, prop_info in properties.items():
|
|
208
|
+
if prop_name in {"protocol", "created_at"}:
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
param = ToolParameter(
|
|
212
|
+
name=prop_name,
|
|
213
|
+
type=prop_info.get("type", "string"),
|
|
214
|
+
description=prop_info.get("description", ""),
|
|
215
|
+
required=prop_name in required_fields,
|
|
216
|
+
enum=prop_info.get("enum"),
|
|
217
|
+
items=prop_info.get("items"),
|
|
218
|
+
properties=prop_info.get("properties"),
|
|
219
|
+
)
|
|
220
|
+
parameters.append(param)
|
|
221
|
+
|
|
222
|
+
tools.append(
|
|
223
|
+
ToolDefinition(
|
|
224
|
+
name=tool_name,
|
|
225
|
+
description=tool_description,
|
|
226
|
+
parameters=parameters,
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
return tools
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Tool reference types for module configuration."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from pydantic import AfterValidator, BaseModel, BeforeValidator, Field, PlainSerializer
|
|
6
|
+
from pydantic.annotated_handlers import GetJsonSchemaHandler
|
|
7
|
+
from pydantic.json_schema import JsonSchemaValue
|
|
8
|
+
from pydantic_core import CoreSchema
|
|
9
|
+
|
|
10
|
+
from digitalkin.models.module.tool_cache import ToolModuleInfo, module_info_to_tool_module_info
|
|
11
|
+
from digitalkin.services.communication.communication_strategy import CommunicationStrategy
|
|
12
|
+
from digitalkin.services.registry import RegistryStrategy
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ToolReference(BaseModel):
|
|
16
|
+
"""Tool selection containing setup IDs."""
|
|
17
|
+
|
|
18
|
+
selected_tools: list[str] = Field(default=[], description="Setup IDs of selected tools.")
|
|
19
|
+
|
|
20
|
+
async def resolve(self, registry: RegistryStrategy, communication: CommunicationStrategy) -> list[ToolModuleInfo]:
|
|
21
|
+
"""Resolve selected tools using the registry.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
registry: Registry service for module discovery.
|
|
25
|
+
communication: Communication service for module schemas.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
List of ToolModuleInfo for resolved tools.
|
|
29
|
+
"""
|
|
30
|
+
resolved: list[ToolModuleInfo] = []
|
|
31
|
+
for setup_id in self.selected_tools:
|
|
32
|
+
setup = registry.get_setup(setup_id)
|
|
33
|
+
if setup and setup.module_id:
|
|
34
|
+
info = registry.discover_by_id(setup.module_id)
|
|
35
|
+
if info:
|
|
36
|
+
resolved.append(await module_info_to_tool_module_info(info, setup_id, setup.name, communication))
|
|
37
|
+
return resolved
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _ToolReferenceInputSchema:
|
|
41
|
+
"""Custom JSON schema generator with configurable maxItems and ui:options."""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
setup_ids: list[str],
|
|
46
|
+
module_ids: list[str] | None,
|
|
47
|
+
tag_ids: list[str],
|
|
48
|
+
categories: list[str],
|
|
49
|
+
max_tools: int = 0,
|
|
50
|
+
min_tools: int = 0,
|
|
51
|
+
) -> None:
|
|
52
|
+
self.setup_ids = setup_ids
|
|
53
|
+
self.module_ids = module_ids
|
|
54
|
+
self.tag_ids = tag_ids
|
|
55
|
+
self.max_tools = max_tools
|
|
56
|
+
self.min_tools = min_tools
|
|
57
|
+
self.categories = categories
|
|
58
|
+
|
|
59
|
+
def __get_pydantic_json_schema__( # noqa: PLW3201
|
|
60
|
+
self,
|
|
61
|
+
_schema: CoreSchema,
|
|
62
|
+
_handler: GetJsonSchemaHandler,
|
|
63
|
+
) -> JsonSchemaValue:
|
|
64
|
+
"""Generate JSON schema as array for UI, hiding ToolReference complexity.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
JSON schema as array with ui:widget toolSelect.
|
|
68
|
+
"""
|
|
69
|
+
json_schema: dict[str, object] = {
|
|
70
|
+
"type": "array",
|
|
71
|
+
"items": {"type": "string"},
|
|
72
|
+
}
|
|
73
|
+
if self.max_tools > 0:
|
|
74
|
+
json_schema["maxItems"] = self.max_tools
|
|
75
|
+
if self.min_tools > 0:
|
|
76
|
+
json_schema["minItems"] = self.min_tools
|
|
77
|
+
json_schema["ui:widget"] = "toolSelect"
|
|
78
|
+
json_schema["ui:options"] = {
|
|
79
|
+
"setupIds": self.setup_ids,
|
|
80
|
+
"tagIds": self.tag_ids,
|
|
81
|
+
"categories": self.categories,
|
|
82
|
+
"moduleIds": self.module_ids or [],
|
|
83
|
+
"showModules": self.module_ids is not None,
|
|
84
|
+
}
|
|
85
|
+
return json_schema
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def tool_reference_input(
|
|
89
|
+
setup_ids: list[str] = [],
|
|
90
|
+
module_ids: list[str] | None = [],
|
|
91
|
+
tag_ids: list[str] = [],
|
|
92
|
+
categories: list[str] = [],
|
|
93
|
+
max_tools: int = 0,
|
|
94
|
+
min_tools: int = 0,
|
|
95
|
+
) -> type[ToolReference]:
|
|
96
|
+
"""Create ToolReferenceInput type with schema options and validation.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
setup_ids: Setup IDs for the user to choose from.
|
|
100
|
+
module_ids: Module IDs for the user to choose from.
|
|
101
|
+
tag_ids: Tag IDs for the user to choose from.
|
|
102
|
+
categories: Categories for the user to choose from.
|
|
103
|
+
max_tools: Maximum tools allowed. 0 for unlimited.
|
|
104
|
+
min_tools: Minimum tools required. 0 for no minimum.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Annotated type for use in Pydantic models.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def convert_to_tool_reference(v: object) -> ToolReference | object:
|
|
111
|
+
"""Convert list of setup IDs to ToolReference.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
ToolReference if input is list, otherwise original value.
|
|
115
|
+
"""
|
|
116
|
+
if isinstance(v, list):
|
|
117
|
+
return ToolReference(selected_tools=v)
|
|
118
|
+
return v
|
|
119
|
+
|
|
120
|
+
def validate_tools_count(v: ToolReference) -> ToolReference:
|
|
121
|
+
"""Validate selected_tools count against min/max constraints.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
The validated ToolReference.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
ValueError: If count is below min_tools or above max_tools.
|
|
128
|
+
"""
|
|
129
|
+
count = len(v.selected_tools)
|
|
130
|
+
if min_tools > 0 and count < min_tools:
|
|
131
|
+
msg = f"At least {min_tools} tools required, got {count}"
|
|
132
|
+
raise ValueError(msg)
|
|
133
|
+
if max_tools > 0 and count > max_tools:
|
|
134
|
+
msg = f"At most {max_tools} tools allowed, got {count}"
|
|
135
|
+
raise ValueError(msg)
|
|
136
|
+
return v
|
|
137
|
+
|
|
138
|
+
def serialize_to_list(v: ToolReference) -> list[str]:
|
|
139
|
+
"""Serialize ToolReference as plain list for frontend compatibility.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
List of setup IDs.
|
|
143
|
+
"""
|
|
144
|
+
return v.selected_tools
|
|
145
|
+
|
|
146
|
+
return Annotated[ # type: ignore[return-value]
|
|
147
|
+
ToolReference,
|
|
148
|
+
BeforeValidator(convert_to_tool_reference),
|
|
149
|
+
AfterValidator(validate_tools_count),
|
|
150
|
+
PlainSerializer(serialize_to_list, return_type=list[str]),
|
|
151
|
+
_ToolReferenceInputSchema(
|
|
152
|
+
setup_ids=setup_ids,
|
|
153
|
+
module_ids=module_ids,
|
|
154
|
+
tag_ids=tag_ids,
|
|
155
|
+
categories=categories,
|
|
156
|
+
max_tools=max_tools,
|
|
157
|
+
min_tools=min_tools,
|
|
158
|
+
),
|
|
159
|
+
Field(default=[]),
|
|
160
|
+
]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Utility protocols for SDK-provided functionality.
|
|
2
|
+
|
|
3
|
+
These protocols are automatically available to all modules and don't need to be
|
|
4
|
+
explicitly included in module output unions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Any, ClassVar, Literal
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
from digitalkin.models.module.base_types import DataTrigger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UtilityProtocol(DataTrigger):
|
|
16
|
+
"""Base class for SDK-provided utility protocols.
|
|
17
|
+
|
|
18
|
+
All SDK utility protocols inherit from this class to enable:
|
|
19
|
+
- Easy identification of SDK vs user-defined protocols
|
|
20
|
+
- Auto-injection capability
|
|
21
|
+
- Consistent behavior across the SDK
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EndOfStreamOutput(UtilityProtocol):
|
|
26
|
+
"""Signal that the stream has ended."""
|
|
27
|
+
|
|
28
|
+
protocol: Literal["end_of_stream"] = "end_of_stream" # type: ignore
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ModuleStartInfoOutput(UtilityProtocol):
|
|
32
|
+
"""Output sent when module starts with execution context.
|
|
33
|
+
|
|
34
|
+
This protocol is sent as the first message when a module starts,
|
|
35
|
+
providing the client with essential execution context information.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
protocol: Literal["module_start_info"] = "module_start_info" # type: ignore
|
|
39
|
+
job_id: str = Field(..., description="Unique job identifier")
|
|
40
|
+
mission_id: str = Field(..., description="Mission identifier")
|
|
41
|
+
setup_id: str = Field(..., description="Setup identifier")
|
|
42
|
+
setup_version_id: str = Field(..., description="Setup version identifier")
|
|
43
|
+
module_id: str = Field(..., description="Module identifier")
|
|
44
|
+
module_name: str = Field(..., description="Human-readable module name")
|
|
45
|
+
started_at: str = Field(
|
|
46
|
+
default_factory=lambda: datetime.now(tz=timezone.utc).isoformat(),
|
|
47
|
+
description="ISO timestamp when module started",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class HealthcheckPingInput(UtilityProtocol):
|
|
52
|
+
"""Input for healthcheck ping request."""
|
|
53
|
+
|
|
54
|
+
protocol: Literal["healthcheck_ping"] = "healthcheck_ping" # type: ignore
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class HealthcheckPingOutput(UtilityProtocol):
|
|
58
|
+
"""Output for healthcheck ping response.
|
|
59
|
+
|
|
60
|
+
Simple alive check that returns "pong" status.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
protocol: Literal["healthcheck_ping"] = "healthcheck_ping" # type: ignore
|
|
64
|
+
status: Literal["pong"] = "pong"
|
|
65
|
+
latency_ms: float | None = Field(
|
|
66
|
+
default=None,
|
|
67
|
+
description="Round-trip latency in milliseconds",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ServiceHealthStatus(BaseModel):
|
|
72
|
+
"""Health status of a single service."""
|
|
73
|
+
|
|
74
|
+
name: str = Field(..., description="Name of the service")
|
|
75
|
+
status: Literal["healthy", "unhealthy", "unknown"] = Field(
|
|
76
|
+
...,
|
|
77
|
+
description="Health status of the service",
|
|
78
|
+
)
|
|
79
|
+
message: str | None = Field(
|
|
80
|
+
default=None,
|
|
81
|
+
description="Optional message about the service status",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class HealthcheckServicesInput(UtilityProtocol):
|
|
86
|
+
"""Input for healthcheck services request."""
|
|
87
|
+
|
|
88
|
+
protocol: Literal["healthcheck_services"] = "healthcheck_services" # type: ignore
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class HealthcheckServicesOutput(UtilityProtocol):
|
|
92
|
+
"""Output for healthcheck services response.
|
|
93
|
+
|
|
94
|
+
Reports the health status of all configured services.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
protocol: Literal["healthcheck_services"] = "healthcheck_services" # type: ignore
|
|
98
|
+
services: list[ServiceHealthStatus] = Field(
|
|
99
|
+
...,
|
|
100
|
+
description="List of service health statuses",
|
|
101
|
+
)
|
|
102
|
+
overall_status: Literal["healthy", "degraded", "unhealthy"] = Field(
|
|
103
|
+
...,
|
|
104
|
+
description="Overall health status based on all services",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class HealthcheckStatusInput(UtilityProtocol):
|
|
109
|
+
"""Input for healthcheck status request."""
|
|
110
|
+
|
|
111
|
+
protocol: Literal["healthcheck_status"] = "healthcheck_status" # type: ignore
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class HealthcheckStatusOutput(UtilityProtocol):
|
|
115
|
+
"""Output for healthcheck status response.
|
|
116
|
+
|
|
117
|
+
Comprehensive module status including uptime, active jobs, and metadata.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
protocol: Literal["healthcheck_status"] = "healthcheck_status" # type: ignore
|
|
121
|
+
module_name: str = Field(..., description="Name of the module")
|
|
122
|
+
module_status: str = Field(..., description="Current status of the module")
|
|
123
|
+
uptime_seconds: float | None = Field(
|
|
124
|
+
default=None,
|
|
125
|
+
description="Module uptime in seconds",
|
|
126
|
+
)
|
|
127
|
+
active_jobs: int = Field(
|
|
128
|
+
default=0,
|
|
129
|
+
description="Number of currently active jobs",
|
|
130
|
+
)
|
|
131
|
+
metadata: dict[str, Any] = Field(
|
|
132
|
+
default_factory=dict,
|
|
133
|
+
description="Additional metadata about the module",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class UtilityRegistry:
|
|
138
|
+
"""Registry for SDK-provided built-in triggers.
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
builtin_triggers = UtilityRegistry.get_builtin_triggers()
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
_builtin_triggers: ClassVar[tuple | None] = None
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def get_builtin_triggers(cls) -> tuple:
|
|
148
|
+
"""Get all SDK-provided built-in trigger handlers.
|
|
149
|
+
|
|
150
|
+
Uses lazy loading to avoid circular imports with the modules package.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Tuple of TriggerHandler subclasses for built-in functionality.
|
|
154
|
+
"""
|
|
155
|
+
if cls._builtin_triggers is None:
|
|
156
|
+
from digitalkin.modules.triggers.healthcheck_ping_trigger import HealthcheckPingTrigger # noqa: PLC0415
|
|
157
|
+
from digitalkin.modules.triggers.healthcheck_services_trigger import ( # noqa: PLC0415
|
|
158
|
+
HealthcheckServicesTrigger,
|
|
159
|
+
)
|
|
160
|
+
from digitalkin.modules.triggers.healthcheck_status_trigger import HealthcheckStatusTrigger # noqa: PLC0415
|
|
161
|
+
|
|
162
|
+
cls._builtin_triggers = (
|
|
163
|
+
HealthcheckPingTrigger,
|
|
164
|
+
HealthcheckServicesTrigger,
|
|
165
|
+
HealthcheckStatusTrigger,
|
|
166
|
+
)
|
|
167
|
+
return cls._builtin_triggers
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import Annotated, Any, Literal
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
8
8
|
|
|
@@ -35,6 +35,27 @@ class CostConfig(BaseModel):
|
|
|
35
35
|
rate: float
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
class QuantityLimit(BaseModel):
|
|
39
|
+
"""Cost limit based on quantity (e.g., max 10000 tokens)."""
|
|
40
|
+
|
|
41
|
+
limit_type: Literal["quantity"] = "quantity"
|
|
42
|
+
name: str
|
|
43
|
+
type: CostTypeEnum
|
|
44
|
+
max_value: float
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AmountLimit(BaseModel):
|
|
48
|
+
"""Cost limit based on cost amount in dollars (e.g., max $1.00)."""
|
|
49
|
+
|
|
50
|
+
limit_type: Literal["amount"] = "amount"
|
|
51
|
+
name: str
|
|
52
|
+
type: CostTypeEnum
|
|
53
|
+
max_value: float
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
CostLimit = Annotated[QuantityLimit | AmountLimit, Field(discriminator="limit_type")]
|
|
57
|
+
|
|
58
|
+
|
|
38
59
|
class CostEvent(BaseModel):
|
|
39
60
|
"""Pydantic model that represents a cost event registered during service execution.
|
|
40
61
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Registry data models."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RegistryModuleStatus(str, Enum):
|
|
10
|
+
"""Module status in the registry."""
|
|
11
|
+
|
|
12
|
+
UNSPECIFIED = "unspecified"
|
|
13
|
+
READY = "ready"
|
|
14
|
+
ACTIVE = "active"
|
|
15
|
+
ARCHIVED = "archived"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RegistryModuleType(str, Enum):
|
|
19
|
+
"""Module type in the registry."""
|
|
20
|
+
|
|
21
|
+
UNSPECIFIED = "unspecified"
|
|
22
|
+
ARCHETYPE = "archetype"
|
|
23
|
+
TOOL = "tool"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ModuleInfo(BaseModel):
|
|
27
|
+
"""Module information from registry."""
|
|
28
|
+
|
|
29
|
+
module_id: str = ""
|
|
30
|
+
module_type: RegistryModuleType = RegistryModuleType.UNSPECIFIED
|
|
31
|
+
address: str = ""
|
|
32
|
+
port: int = 0
|
|
33
|
+
version: str = ""
|
|
34
|
+
module_name: str = ""
|
|
35
|
+
documentation: str | None = None
|
|
36
|
+
status: RegistryModuleStatus | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RegistrySetupStatus(str, Enum):
|
|
40
|
+
"""Setup status in the registry."""
|
|
41
|
+
|
|
42
|
+
UNSPECIFIED = "unspecified"
|
|
43
|
+
DRAFT = "draft"
|
|
44
|
+
WAITING_FOR_APPROVAL = "waiting_for_approval"
|
|
45
|
+
READY = "ready"
|
|
46
|
+
PAUSED = "paused"
|
|
47
|
+
FAILED = "failed"
|
|
48
|
+
ARCHIVED = "archived"
|
|
49
|
+
NEEDS_CONFIGURATION = "needs_configuration"
|
|
50
|
+
CONFIGURATION_FAILED = "configuration_failed"
|
|
51
|
+
CONFIGURATION_SUCCEEDED = "configuration_succeeded"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RegistryVisibility(str, Enum):
|
|
55
|
+
"""Visibility in the registry."""
|
|
56
|
+
|
|
57
|
+
UNSPECIFIED = "unspecified"
|
|
58
|
+
PUBLIC = "public"
|
|
59
|
+
PRIVATE = "private"
|
|
60
|
+
INTERNAL = "internal"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class SetupInfo(BaseModel):
|
|
64
|
+
"""Setup information from registry."""
|
|
65
|
+
|
|
66
|
+
setup_id: str
|
|
67
|
+
name: str
|
|
68
|
+
documentation: str | None = None
|
|
69
|
+
status: RegistrySetupStatus | None = None
|
|
70
|
+
visibility: RegistryVisibility | None = None
|
|
71
|
+
organization_id: str | None = None
|
|
72
|
+
owner_id: str | None = None
|
|
73
|
+
card_id: str | None = None
|
|
74
|
+
module_id: str | None = None
|
|
75
|
+
setup_version_id: str | None = None
|
|
76
|
+
setup_version: str | None = None
|
|
77
|
+
config: dict[str, Any] | None = None
|
digitalkin/modules/__init__.py
CHANGED
|
@@ -4,4 +4,8 @@ from digitalkin.modules.archetype_module import ArchetypeModule
|
|
|
4
4
|
from digitalkin.modules.tool_module import ToolModule
|
|
5
5
|
from digitalkin.modules.trigger_handler import TriggerHandler
|
|
6
6
|
|
|
7
|
-
__all__ = [
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ArchetypeModule",
|
|
9
|
+
"ToolModule",
|
|
10
|
+
"TriggerHandler",
|
|
11
|
+
]
|