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,547 @@
|
|
|
1
|
+
"""Setup model types with dynamic schema resolution and tool reference support."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import types
|
|
5
|
+
import typing
|
|
6
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar, cast, get_args, get_origin
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, create_model
|
|
9
|
+
|
|
10
|
+
from digitalkin.logger import logger
|
|
11
|
+
from digitalkin.models.module.tool_cache import ToolCache, ToolModuleInfo
|
|
12
|
+
from digitalkin.models.module.tool_reference import ToolReference
|
|
13
|
+
from digitalkin.utils.dynamic_schema import (
|
|
14
|
+
DynamicField,
|
|
15
|
+
get_fetchers,
|
|
16
|
+
has_dynamic,
|
|
17
|
+
resolve_safe,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from pydantic.fields import FieldInfo
|
|
22
|
+
|
|
23
|
+
from digitalkin.services.communication import CommunicationStrategy
|
|
24
|
+
from digitalkin.services.registry import RegistryStrategy
|
|
25
|
+
|
|
26
|
+
SetupModelT = TypeVar("SetupModelT", bound="SetupModel")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
30
|
+
"""Base setup model with dynamic schema and tool cache support."""
|
|
31
|
+
|
|
32
|
+
_clean_model_cache: ClassVar[dict[tuple[type, bool, bool], type]] = {}
|
|
33
|
+
resolved_tools: dict[str, ToolModuleInfo] = Field(
|
|
34
|
+
default_factory=dict,
|
|
35
|
+
json_schema_extra={"hidden": True},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
async def get_clean_model(
|
|
40
|
+
cls,
|
|
41
|
+
*,
|
|
42
|
+
config_fields: bool,
|
|
43
|
+
hidden_fields: bool,
|
|
44
|
+
force: bool = False,
|
|
45
|
+
) -> "type[SetupModelT]":
|
|
46
|
+
"""Build filtered model based on json_schema_extra metadata.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
config_fields: Include fields with json_schema_extra["config"] = True.
|
|
50
|
+
hidden_fields: Include fields with json_schema_extra["hidden"] = True.
|
|
51
|
+
force: Refresh dynamic schema fields by calling providers.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
New BaseModel subclass with filtered fields.
|
|
55
|
+
"""
|
|
56
|
+
cache_key = (cls, config_fields, hidden_fields)
|
|
57
|
+
if not force and cache_key in cls._clean_model_cache:
|
|
58
|
+
return cast("type[SetupModelT]", cls._clean_model_cache[cache_key])
|
|
59
|
+
|
|
60
|
+
clean_fields: dict[str, Any] = {}
|
|
61
|
+
|
|
62
|
+
for name, field_info in cls.model_fields.items():
|
|
63
|
+
extra = field_info.json_schema_extra or {}
|
|
64
|
+
is_config = bool(extra.get("config", False)) if isinstance(extra, dict) else False
|
|
65
|
+
is_hidden = bool(extra.get("hidden", False)) if isinstance(extra, dict) else False
|
|
66
|
+
|
|
67
|
+
if is_config and not config_fields:
|
|
68
|
+
continue
|
|
69
|
+
if is_hidden and not hidden_fields:
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
current_field_info = field_info
|
|
73
|
+
current_annotation = field_info.annotation
|
|
74
|
+
|
|
75
|
+
if force:
|
|
76
|
+
if has_dynamic(field_info):
|
|
77
|
+
current_field_info = await cls._refresh_field_schema(name, field_info)
|
|
78
|
+
|
|
79
|
+
nested_model = cls._get_base_model_type(current_annotation)
|
|
80
|
+
if nested_model is not None:
|
|
81
|
+
refreshed_nested = await cls._refresh_nested_model(nested_model)
|
|
82
|
+
if refreshed_nested is not nested_model:
|
|
83
|
+
current_annotation = refreshed_nested
|
|
84
|
+
current_field_info = copy.deepcopy(current_field_info)
|
|
85
|
+
current_field_info.annotation = current_annotation
|
|
86
|
+
|
|
87
|
+
clean_fields[name] = (current_annotation, current_field_info)
|
|
88
|
+
|
|
89
|
+
root_extra = cls.model_config.get("json_schema_extra", {})
|
|
90
|
+
|
|
91
|
+
m = create_model(
|
|
92
|
+
f"{cls.__name__}",
|
|
93
|
+
__base__=SetupModel,
|
|
94
|
+
__config__=ConfigDict(
|
|
95
|
+
arbitrary_types_allowed=True,
|
|
96
|
+
json_schema_extra=copy.deepcopy(root_extra) if isinstance(root_extra, dict) else root_extra,
|
|
97
|
+
),
|
|
98
|
+
**clean_fields,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if not force:
|
|
102
|
+
cls._clean_model_cache[cache_key] = m
|
|
103
|
+
|
|
104
|
+
return cast("type[SetupModelT]", m)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def _get_base_model_type(cls, annotation: "type | None") -> "type[BaseModel] | None":
|
|
108
|
+
"""Extract BaseModel type from annotation.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
annotation: Type annotation to inspect.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
BaseModel subclass if found, None otherwise.
|
|
115
|
+
"""
|
|
116
|
+
if annotation is None:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
|
120
|
+
return annotation
|
|
121
|
+
|
|
122
|
+
origin = get_origin(annotation)
|
|
123
|
+
if origin is None:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
args = get_args(annotation)
|
|
127
|
+
return cls._extract_base_model_from_args(origin, args)
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def _extract_base_model_from_args(
|
|
131
|
+
cls,
|
|
132
|
+
origin: type,
|
|
133
|
+
args: "tuple[type, ...]",
|
|
134
|
+
) -> "type[BaseModel] | None":
|
|
135
|
+
"""Extract BaseModel from generic type arguments.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
origin: Generic origin type (list, dict, Union, etc.).
|
|
139
|
+
args: Type arguments.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
BaseModel subclass if found, None otherwise.
|
|
143
|
+
"""
|
|
144
|
+
if origin is typing.Union or origin is types.UnionType:
|
|
145
|
+
return cls._find_base_model_in_args(args)
|
|
146
|
+
|
|
147
|
+
if origin in {list, set, frozenset} and args:
|
|
148
|
+
return cls._check_base_model(args[0])
|
|
149
|
+
|
|
150
|
+
dict_value_index = 1
|
|
151
|
+
if origin is dict and len(args) > dict_value_index:
|
|
152
|
+
return cls._check_base_model(args[dict_value_index])
|
|
153
|
+
|
|
154
|
+
if origin is tuple:
|
|
155
|
+
return cls._find_base_model_in_args(args, skip_ellipsis=True)
|
|
156
|
+
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def _check_base_model(cls, arg: type) -> "type[BaseModel] | None":
|
|
161
|
+
"""Check if arg is a BaseModel subclass.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
arg: Type to check.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
The type if it's a BaseModel subclass, None otherwise.
|
|
168
|
+
"""
|
|
169
|
+
if isinstance(arg, type) and issubclass(arg, BaseModel):
|
|
170
|
+
return arg
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def _find_base_model_in_args(
|
|
175
|
+
cls,
|
|
176
|
+
args: "tuple[type, ...]",
|
|
177
|
+
*,
|
|
178
|
+
skip_ellipsis: bool = False,
|
|
179
|
+
) -> "type[BaseModel] | None":
|
|
180
|
+
"""Find first BaseModel in type args.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
args: Type arguments to search.
|
|
184
|
+
skip_ellipsis: Skip ellipsis in tuple types.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
First BaseModel subclass found, None otherwise.
|
|
188
|
+
"""
|
|
189
|
+
for arg in args:
|
|
190
|
+
if arg is type(None):
|
|
191
|
+
continue
|
|
192
|
+
if skip_ellipsis and arg is ...:
|
|
193
|
+
continue
|
|
194
|
+
result = cls._check_base_model(arg)
|
|
195
|
+
if result is not None:
|
|
196
|
+
return result
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
@classmethod
|
|
200
|
+
async def _refresh_nested_model(cls, model_cls: "type[BaseModel]") -> "type[BaseModel]":
|
|
201
|
+
"""Refresh dynamic fields in a nested BaseModel.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
model_cls: Nested model class to refresh.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
New model class with refreshed fields, or original if no changes.
|
|
208
|
+
"""
|
|
209
|
+
has_changes = False
|
|
210
|
+
clean_fields: dict[str, Any] = {}
|
|
211
|
+
|
|
212
|
+
for name, field_info in model_cls.model_fields.items():
|
|
213
|
+
current_field_info = field_info
|
|
214
|
+
current_annotation = field_info.annotation
|
|
215
|
+
|
|
216
|
+
if has_dynamic(field_info):
|
|
217
|
+
current_field_info = await cls._refresh_field_schema(name, field_info)
|
|
218
|
+
has_changes = True
|
|
219
|
+
|
|
220
|
+
nested_model = cls._get_base_model_type(current_annotation)
|
|
221
|
+
if nested_model is not None:
|
|
222
|
+
refreshed_nested = await cls._refresh_nested_model(nested_model)
|
|
223
|
+
if refreshed_nested is not nested_model:
|
|
224
|
+
current_annotation = refreshed_nested
|
|
225
|
+
current_field_info = copy.deepcopy(current_field_info)
|
|
226
|
+
current_field_info.annotation = current_annotation
|
|
227
|
+
has_changes = True
|
|
228
|
+
|
|
229
|
+
clean_fields[name] = (current_annotation, current_field_info)
|
|
230
|
+
|
|
231
|
+
if not has_changes:
|
|
232
|
+
return model_cls
|
|
233
|
+
|
|
234
|
+
root_extra = model_cls.model_config.get("json_schema_extra", {})
|
|
235
|
+
|
|
236
|
+
return create_model(
|
|
237
|
+
model_cls.__name__,
|
|
238
|
+
__base__=BaseModel,
|
|
239
|
+
__config__=ConfigDict(
|
|
240
|
+
arbitrary_types_allowed=True,
|
|
241
|
+
json_schema_extra=copy.deepcopy(root_extra) if isinstance(root_extra, dict) else root_extra,
|
|
242
|
+
),
|
|
243
|
+
**clean_fields,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
async def _refresh_field_schema(cls, field_name: str, field_info: "FieldInfo") -> "FieldInfo":
|
|
248
|
+
"""Refresh field's json_schema_extra with values from dynamic providers.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
field_name: Name of field being refreshed.
|
|
252
|
+
field_info: Original FieldInfo with dynamic providers.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
New FieldInfo with resolved values, or original if all fetchers fail.
|
|
256
|
+
"""
|
|
257
|
+
fetchers = get_fetchers(field_info)
|
|
258
|
+
|
|
259
|
+
if not fetchers:
|
|
260
|
+
return field_info
|
|
261
|
+
|
|
262
|
+
result = await resolve_safe(fetchers)
|
|
263
|
+
|
|
264
|
+
if result.errors:
|
|
265
|
+
for key, error in result.errors.items():
|
|
266
|
+
logger.warning(
|
|
267
|
+
"Failed to resolve '%s' for field '%s': %s",
|
|
268
|
+
key,
|
|
269
|
+
field_name,
|
|
270
|
+
error,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if not result.values:
|
|
274
|
+
return field_info
|
|
275
|
+
|
|
276
|
+
extra = field_info.json_schema_extra or {}
|
|
277
|
+
new_extra = {**extra, **result.values} if isinstance(extra, dict) else result.values
|
|
278
|
+
|
|
279
|
+
new_field_info = copy.deepcopy(field_info)
|
|
280
|
+
new_field_info.json_schema_extra = new_extra
|
|
281
|
+
new_field_info.metadata = [m for m in new_field_info.metadata if not isinstance(m, DynamicField)]
|
|
282
|
+
|
|
283
|
+
return new_field_info
|
|
284
|
+
|
|
285
|
+
async def resolve_tool_references(
|
|
286
|
+
self, registry: "RegistryStrategy", communication: "CommunicationStrategy"
|
|
287
|
+
) -> None:
|
|
288
|
+
"""Resolve all ToolReference fields recursively.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
registry: Registry service for module discovery.
|
|
292
|
+
communication: Communication service for module schemas.
|
|
293
|
+
"""
|
|
294
|
+
logger.info("Starting resolve_tool_references")
|
|
295
|
+
await self._resolve_tool_references_recursive(
|
|
296
|
+
self,
|
|
297
|
+
registry,
|
|
298
|
+
communication,
|
|
299
|
+
self.resolved_tools,
|
|
300
|
+
)
|
|
301
|
+
logger.info("Finished resolve_tool_references")
|
|
302
|
+
|
|
303
|
+
@classmethod
|
|
304
|
+
async def _resolve_tool_references_recursive(
|
|
305
|
+
cls,
|
|
306
|
+
model_instance: BaseModel,
|
|
307
|
+
registry: "RegistryStrategy",
|
|
308
|
+
communication: "CommunicationStrategy",
|
|
309
|
+
resolved_tools: dict[str, ToolModuleInfo],
|
|
310
|
+
) -> None:
|
|
311
|
+
"""Recursively resolve ToolReference fields in a model.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
model_instance: Model instance to process.
|
|
315
|
+
registry: Registry service for resolution.
|
|
316
|
+
communication: Communication service for module schemas.
|
|
317
|
+
resolved_tools: Cache of already resolved tools.
|
|
318
|
+
"""
|
|
319
|
+
for field_name, field_value in model_instance.__dict__.items():
|
|
320
|
+
if field_value is None:
|
|
321
|
+
continue
|
|
322
|
+
await cls._resolve_field_value(
|
|
323
|
+
field_name,
|
|
324
|
+
field_value,
|
|
325
|
+
registry,
|
|
326
|
+
communication,
|
|
327
|
+
resolved_tools,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
@classmethod
|
|
331
|
+
async def _resolve_field_value(
|
|
332
|
+
cls,
|
|
333
|
+
field_name: str,
|
|
334
|
+
field_value: "BaseModel | ToolReference | list | dict",
|
|
335
|
+
registry: "RegistryStrategy",
|
|
336
|
+
communication: "CommunicationStrategy",
|
|
337
|
+
resolved_tools: dict[str, ToolModuleInfo],
|
|
338
|
+
) -> None:
|
|
339
|
+
"""Resolve a single field value based on its type.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
field_name: Name of the field.
|
|
343
|
+
field_value: Value to process.
|
|
344
|
+
registry: Registry service for resolution.
|
|
345
|
+
communication: Communication service for module schemas.
|
|
346
|
+
resolved_tools: Cache of already resolved tools.
|
|
347
|
+
"""
|
|
348
|
+
if isinstance(field_value, ToolReference):
|
|
349
|
+
await cls._resolve_tool_reference(
|
|
350
|
+
field_name,
|
|
351
|
+
field_value,
|
|
352
|
+
registry,
|
|
353
|
+
communication,
|
|
354
|
+
resolved_tools,
|
|
355
|
+
)
|
|
356
|
+
elif isinstance(field_value, BaseModel):
|
|
357
|
+
await cls._resolve_tool_references_recursive(
|
|
358
|
+
field_value,
|
|
359
|
+
registry,
|
|
360
|
+
communication,
|
|
361
|
+
resolved_tools,
|
|
362
|
+
)
|
|
363
|
+
elif isinstance(field_value, list):
|
|
364
|
+
await cls._resolve_list_items(
|
|
365
|
+
field_value,
|
|
366
|
+
registry,
|
|
367
|
+
communication,
|
|
368
|
+
resolved_tools,
|
|
369
|
+
)
|
|
370
|
+
elif isinstance(field_value, dict):
|
|
371
|
+
await cls._resolve_dict_values(
|
|
372
|
+
field_value,
|
|
373
|
+
registry,
|
|
374
|
+
communication,
|
|
375
|
+
resolved_tools,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
@classmethod
|
|
379
|
+
async def _resolve_tool_reference(
|
|
380
|
+
cls,
|
|
381
|
+
field_name: str,
|
|
382
|
+
tool_ref: ToolReference,
|
|
383
|
+
registry: "RegistryStrategy",
|
|
384
|
+
communication: "CommunicationStrategy",
|
|
385
|
+
resolved_tools: dict[str, ToolModuleInfo],
|
|
386
|
+
) -> None:
|
|
387
|
+
"""Resolve a ToolReference (may contain multiple selected tools).
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
field_name: Name of the field for logging.
|
|
391
|
+
tool_ref: ToolReference to resolve.
|
|
392
|
+
registry: Registry service for resolution.
|
|
393
|
+
communication: Communication service for module schemas.
|
|
394
|
+
resolved_tools: Cache of already resolved tools.
|
|
395
|
+
"""
|
|
396
|
+
logger.info("Resolving ToolReference '%s' with %d selected tools", field_name, len(tool_ref.selected_tools))
|
|
397
|
+
|
|
398
|
+
if not tool_ref.selected_tools:
|
|
399
|
+
logger.info("ToolReference '%s' has no selected tools, skipping", field_name)
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
tools_to_resolve = [
|
|
403
|
+
setup_id for setup_id in tool_ref.selected_tools if setup_id and setup_id not in resolved_tools
|
|
404
|
+
]
|
|
405
|
+
|
|
406
|
+
if not tools_to_resolve:
|
|
407
|
+
logger.info("All tools for '%s' already cached", field_name)
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
infos = await tool_ref.resolve(registry, communication)
|
|
412
|
+
for info in infos:
|
|
413
|
+
resolved_tools[info.setup_id] = info
|
|
414
|
+
logger.info("Resolved tool '%s' -> module_id=%s", info.setup_id, info.module_id)
|
|
415
|
+
except Exception:
|
|
416
|
+
logger.exception("Failed to resolve ToolReference '%s'", field_name)
|
|
417
|
+
|
|
418
|
+
@classmethod
|
|
419
|
+
async def _resolve_list_items(
|
|
420
|
+
cls,
|
|
421
|
+
items: list,
|
|
422
|
+
registry: "RegistryStrategy",
|
|
423
|
+
communication: "CommunicationStrategy",
|
|
424
|
+
resolved_tools: dict[str, ToolModuleInfo],
|
|
425
|
+
) -> None:
|
|
426
|
+
"""Resolve ToolReference instances in a list.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
items: List of items to process.
|
|
430
|
+
registry: Registry service for resolution.
|
|
431
|
+
communication: Communication service for module schemas.
|
|
432
|
+
resolved_tools: Cache of already resolved tools.
|
|
433
|
+
"""
|
|
434
|
+
for item in items:
|
|
435
|
+
if isinstance(item, ToolReference):
|
|
436
|
+
await cls._resolve_tool_reference(
|
|
437
|
+
"list_item",
|
|
438
|
+
item,
|
|
439
|
+
registry,
|
|
440
|
+
communication,
|
|
441
|
+
resolved_tools,
|
|
442
|
+
)
|
|
443
|
+
elif isinstance(item, BaseModel):
|
|
444
|
+
await cls._resolve_tool_references_recursive(
|
|
445
|
+
item,
|
|
446
|
+
registry,
|
|
447
|
+
communication,
|
|
448
|
+
resolved_tools,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
@classmethod
|
|
452
|
+
async def _resolve_dict_values(
|
|
453
|
+
cls,
|
|
454
|
+
mapping: dict,
|
|
455
|
+
registry: "RegistryStrategy",
|
|
456
|
+
communication: "CommunicationStrategy",
|
|
457
|
+
resolved_tools: dict[str, ToolModuleInfo],
|
|
458
|
+
) -> None:
|
|
459
|
+
"""Resolve ToolReference instances in dict values.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
mapping: Dict to process.
|
|
463
|
+
registry: Registry service for resolution.
|
|
464
|
+
communication: Communication service for module schemas.
|
|
465
|
+
resolved_tools: Cache of already resolved tools.
|
|
466
|
+
"""
|
|
467
|
+
for item in mapping.values():
|
|
468
|
+
if isinstance(item, ToolReference):
|
|
469
|
+
await cls._resolve_tool_reference(
|
|
470
|
+
"dict_value",
|
|
471
|
+
item,
|
|
472
|
+
registry,
|
|
473
|
+
communication,
|
|
474
|
+
resolved_tools,
|
|
475
|
+
)
|
|
476
|
+
elif isinstance(item, BaseModel):
|
|
477
|
+
await cls._resolve_tool_references_recursive(
|
|
478
|
+
item,
|
|
479
|
+
registry,
|
|
480
|
+
communication,
|
|
481
|
+
resolved_tools,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
def build_tool_cache(self) -> ToolCache:
|
|
485
|
+
"""Build tool cache from resolved ToolReferences.
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
ToolCache with field names as keys and ToolModuleInfo as values.
|
|
489
|
+
"""
|
|
490
|
+
cache = ToolCache()
|
|
491
|
+
self._build_tool_cache_recursive(self, cache)
|
|
492
|
+
logger.info("Tool cache built: %d entries", len(cache.entries))
|
|
493
|
+
return cache
|
|
494
|
+
|
|
495
|
+
def _build_tool_cache_recursive(self, model_instance: BaseModel, cache: ToolCache) -> None:
|
|
496
|
+
"""Recursively build tool cache from ToolReferences.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
model_instance: Model instance to process.
|
|
500
|
+
cache: ToolCache to populate.
|
|
501
|
+
"""
|
|
502
|
+
for field_value in model_instance.__dict__.values():
|
|
503
|
+
if field_value is None:
|
|
504
|
+
continue
|
|
505
|
+
if isinstance(field_value, ToolReference):
|
|
506
|
+
for setup_id in field_value.selected_tools:
|
|
507
|
+
tool_module_info = self.resolved_tools.get(setup_id)
|
|
508
|
+
if tool_module_info:
|
|
509
|
+
cache.add(tool_module_info)
|
|
510
|
+
elif isinstance(field_value, BaseModel):
|
|
511
|
+
self._build_tool_cache_recursive(field_value, cache)
|
|
512
|
+
elif isinstance(field_value, list):
|
|
513
|
+
self._process_list_items(field_value, cache)
|
|
514
|
+
elif isinstance(field_value, dict):
|
|
515
|
+
self._process_dict_values(field_value, cache)
|
|
516
|
+
|
|
517
|
+
def _process_list_items(self, items: list, cache: ToolCache) -> None:
|
|
518
|
+
"""Process list items for ToolReferences.
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
items: List to process.
|
|
522
|
+
cache: ToolCache to populate.
|
|
523
|
+
"""
|
|
524
|
+
for item in items:
|
|
525
|
+
if isinstance(item, ToolReference):
|
|
526
|
+
for setup_id in item.selected_tools:
|
|
527
|
+
tool_module_info = self.resolved_tools.get(setup_id)
|
|
528
|
+
if tool_module_info:
|
|
529
|
+
cache.add(tool_module_info)
|
|
530
|
+
elif isinstance(item, BaseModel):
|
|
531
|
+
self._build_tool_cache_recursive(item, cache)
|
|
532
|
+
|
|
533
|
+
def _process_dict_values(self, mapping: dict, cache: ToolCache) -> None:
|
|
534
|
+
"""Process dict values for ToolReferences.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
mapping: Dict to process.
|
|
538
|
+
cache: ToolCache to populate.
|
|
539
|
+
"""
|
|
540
|
+
for item in mapping.values():
|
|
541
|
+
if isinstance(item, ToolReference):
|
|
542
|
+
for setup_id in item.selected_tools:
|
|
543
|
+
tool_module_info = self.resolved_tools.get(setup_id)
|
|
544
|
+
if tool_module_info:
|
|
545
|
+
cache.add(tool_module_info)
|
|
546
|
+
elif isinstance(item, BaseModel):
|
|
547
|
+
self._build_tool_cache_recursive(item, cache)
|