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.
Files changed (87) hide show
  1. base_server/server_async_insecure.py +6 -5
  2. base_server/server_async_secure.py +6 -5
  3. base_server/server_sync_insecure.py +5 -4
  4. base_server/server_sync_secure.py +5 -4
  5. digitalkin/__version__.py +1 -1
  6. digitalkin/core/job_manager/base_job_manager.py +1 -1
  7. digitalkin/core/job_manager/single_job_manager.py +78 -36
  8. digitalkin/core/job_manager/taskiq_broker.py +8 -7
  9. digitalkin/core/job_manager/taskiq_job_manager.py +9 -5
  10. digitalkin/core/task_manager/base_task_manager.py +3 -1
  11. digitalkin/core/task_manager/surrealdb_repository.py +13 -7
  12. digitalkin/core/task_manager/task_executor.py +27 -10
  13. digitalkin/core/task_manager/task_session.py +133 -101
  14. digitalkin/grpc_servers/module_server.py +95 -171
  15. digitalkin/grpc_servers/module_servicer.py +133 -27
  16. digitalkin/grpc_servers/utils/grpc_client_wrapper.py +36 -10
  17. digitalkin/grpc_servers/utils/utility_schema_extender.py +106 -0
  18. digitalkin/models/__init__.py +1 -1
  19. digitalkin/models/core/job_manager_models.py +0 -8
  20. digitalkin/models/core/task_monitor.py +23 -1
  21. digitalkin/models/grpc_servers/models.py +95 -8
  22. digitalkin/models/module/__init__.py +26 -13
  23. digitalkin/models/module/base_types.py +61 -0
  24. digitalkin/models/module/module_context.py +279 -13
  25. digitalkin/models/module/module_types.py +29 -109
  26. digitalkin/models/module/setup_types.py +547 -0
  27. digitalkin/models/module/tool_cache.py +230 -0
  28. digitalkin/models/module/tool_reference.py +160 -0
  29. digitalkin/models/module/utility.py +167 -0
  30. digitalkin/models/services/cost.py +22 -1
  31. digitalkin/models/services/registry.py +77 -0
  32. digitalkin/modules/__init__.py +5 -1
  33. digitalkin/modules/_base_module.py +253 -90
  34. digitalkin/modules/archetype_module.py +6 -1
  35. digitalkin/modules/tool_module.py +6 -1
  36. digitalkin/modules/triggers/__init__.py +8 -0
  37. digitalkin/modules/triggers/healthcheck_ping_trigger.py +45 -0
  38. digitalkin/modules/triggers/healthcheck_services_trigger.py +63 -0
  39. digitalkin/modules/triggers/healthcheck_status_trigger.py +52 -0
  40. digitalkin/services/__init__.py +4 -0
  41. digitalkin/services/communication/__init__.py +7 -0
  42. digitalkin/services/communication/communication_strategy.py +87 -0
  43. digitalkin/services/communication/default_communication.py +104 -0
  44. digitalkin/services/communication/grpc_communication.py +264 -0
  45. digitalkin/services/cost/cost_strategy.py +36 -14
  46. digitalkin/services/cost/default_cost.py +61 -1
  47. digitalkin/services/cost/grpc_cost.py +98 -2
  48. digitalkin/services/filesystem/grpc_filesystem.py +9 -2
  49. digitalkin/services/registry/__init__.py +22 -1
  50. digitalkin/services/registry/default_registry.py +156 -4
  51. digitalkin/services/registry/exceptions.py +47 -0
  52. digitalkin/services/registry/grpc_registry.py +382 -0
  53. digitalkin/services/registry/registry_models.py +15 -0
  54. digitalkin/services/registry/registry_strategy.py +106 -4
  55. digitalkin/services/services_config.py +25 -3
  56. digitalkin/services/services_models.py +5 -1
  57. digitalkin/services/setup/default_setup.py +1 -1
  58. digitalkin/services/setup/grpc_setup.py +1 -1
  59. digitalkin/services/storage/grpc_storage.py +1 -1
  60. digitalkin/services/user_profile/__init__.py +11 -0
  61. digitalkin/services/user_profile/grpc_user_profile.py +2 -2
  62. digitalkin/services/user_profile/user_profile_strategy.py +0 -15
  63. digitalkin/utils/__init__.py +40 -0
  64. digitalkin/utils/conditional_schema.py +260 -0
  65. digitalkin/utils/dynamic_schema.py +487 -0
  66. digitalkin/utils/schema_splitter.py +290 -0
  67. {digitalkin-0.3.1.dev1.dist-info → digitalkin-0.3.2a2.dist-info}/METADATA +13 -13
  68. digitalkin-0.3.2a2.dist-info/RECORD +144 -0
  69. {digitalkin-0.3.1.dev1.dist-info → digitalkin-0.3.2a2.dist-info}/WHEEL +1 -1
  70. {digitalkin-0.3.1.dev1.dist-info → digitalkin-0.3.2a2.dist-info}/top_level.txt +1 -0
  71. modules/archetype_with_tools_module.py +232 -0
  72. modules/cpu_intensive_module.py +1 -1
  73. modules/dynamic_setup_module.py +338 -0
  74. modules/minimal_llm_module.py +1 -1
  75. modules/text_transform_module.py +1 -1
  76. monitoring/digitalkin_observability/__init__.py +46 -0
  77. monitoring/digitalkin_observability/http_server.py +150 -0
  78. monitoring/digitalkin_observability/interceptors.py +176 -0
  79. monitoring/digitalkin_observability/metrics.py +201 -0
  80. monitoring/digitalkin_observability/prometheus.py +137 -0
  81. monitoring/tests/test_metrics.py +172 -0
  82. services/filesystem_module.py +7 -5
  83. services/storage_module.py +4 -2
  84. digitalkin/grpc_servers/registry_server.py +0 -65
  85. digitalkin/grpc_servers/registry_servicer.py +0 -456
  86. digitalkin-0.3.1.dev1.dist-info/RECORD +0 -117
  87. {digitalkin-0.3.1.dev1.dist-info → digitalkin-0.3.2a2.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)