digitalkin 0.3.2.dev7__py3-none-any.whl → 0.3.2.dev8__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.
- digitalkin/__version__.py +1 -1
- digitalkin/grpc_servers/module_servicer.py +0 -11
- digitalkin/grpc_servers/utils/utility_schema_extender.py +2 -1
- digitalkin/models/module/module_context.py +136 -23
- digitalkin/models/module/setup_types.py +168 -257
- digitalkin/models/module/tool_cache.py +27 -187
- digitalkin/models/module/tool_reference.py +42 -45
- digitalkin/models/services/registry.py +0 -7
- digitalkin/modules/_base_module.py +74 -52
- digitalkin/services/registry/__init__.py +1 -1
- digitalkin/services/registry/default_registry.py +1 -1
- digitalkin/services/registry/grpc_registry.py +1 -1
- digitalkin/services/registry/registry_models.py +1 -29
- digitalkin/services/registry/registry_strategy.py +1 -1
- {digitalkin-0.3.2.dev7.dist-info → digitalkin-0.3.2.dev8.dist-info}/METADATA +1 -1
- {digitalkin-0.3.2.dev7.dist-info → digitalkin-0.3.2.dev8.dist-info}/RECORD +26 -20
- {digitalkin-0.3.2.dev7.dist-info → digitalkin-0.3.2.dev8.dist-info}/top_level.txt +1 -0
- modules/archetype_with_tools_module.py +244 -0
- 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
- digitalkin/models/module/module_helpers.py +0 -189
- {digitalkin-0.3.2.dev7.dist-info → digitalkin-0.3.2.dev8.dist-info}/WHEEL +0 -0
- {digitalkin-0.3.2.dev7.dist-info → digitalkin-0.3.2.dev8.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
"""Setup model types with dynamic schema resolution."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
1
|
+
"""Setup model types with dynamic schema resolution and tool reference support."""
|
|
4
2
|
|
|
5
3
|
import copy
|
|
6
4
|
import types
|
|
7
5
|
import typing
|
|
8
6
|
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar, cast, get_args, get_origin
|
|
9
7
|
|
|
10
|
-
from pydantic import BaseModel, ConfigDict,
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, create_model
|
|
11
9
|
|
|
12
10
|
from digitalkin.logger import logger
|
|
13
11
|
from digitalkin.models.module.tool_cache import ToolCache
|
|
14
12
|
from digitalkin.models.module.tool_reference import ToolReference
|
|
13
|
+
from digitalkin.models.services.registry import ModuleInfo
|
|
15
14
|
from digitalkin.utils.dynamic_schema import (
|
|
16
15
|
DynamicField,
|
|
17
16
|
get_fetchers,
|
|
@@ -28,25 +27,67 @@ SetupModelT = TypeVar("SetupModelT", bound="SetupModel")
|
|
|
28
27
|
|
|
29
28
|
|
|
30
29
|
class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
31
|
-
"""Base
|
|
30
|
+
"""Base setup model with dynamic schema and tool cache support."""
|
|
31
|
+
|
|
32
|
+
_clean_model_cache: ClassVar[dict[tuple[type, bool, bool], type]] = {}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
runtime value generation.
|
|
34
|
+
def __init_subclass__(cls, **kwargs: Any) -> None: # noqa: ANN401
|
|
35
|
+
"""Inject hidden companion fields for ToolReference annotations.
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
Args:
|
|
38
|
+
**kwargs: Keyword arguments passed to parent.
|
|
39
|
+
"""
|
|
40
|
+
super().__init_subclass__(**kwargs)
|
|
41
|
+
cls._inject_tool_cache_fields()
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
@classmethod
|
|
44
|
+
def _inject_tool_cache_fields(cls) -> None:
|
|
45
|
+
"""Inject hidden companion fields for ToolReference annotations."""
|
|
46
|
+
annotations = getattr(cls, "__annotations__", {})
|
|
47
|
+
new_annotations: dict[str, Any] = {}
|
|
48
|
+
|
|
49
|
+
for field_name, annotation in annotations.items():
|
|
50
|
+
if cls._is_tool_reference_annotation(annotation):
|
|
51
|
+
cache_field_name = f"{field_name}_cache"
|
|
52
|
+
if cache_field_name not in annotations:
|
|
53
|
+
# Check if it's a list type
|
|
54
|
+
origin = get_origin(annotation)
|
|
55
|
+
if origin is list:
|
|
56
|
+
new_annotations[cache_field_name] = list[ModuleInfo]
|
|
57
|
+
setattr(
|
|
58
|
+
cls,
|
|
59
|
+
cache_field_name,
|
|
60
|
+
Field(default_factory=list, json_schema_extra={"hidden": True}),
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
new_annotations[cache_field_name] = ModuleInfo | None
|
|
64
|
+
setattr(
|
|
65
|
+
cls,
|
|
66
|
+
cache_field_name,
|
|
67
|
+
Field(default=None, json_schema_extra={"hidden": True}),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if new_annotations:
|
|
71
|
+
cls.__annotations__ = {**annotations, **new_annotations}
|
|
42
72
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"""
|
|
73
|
+
@classmethod
|
|
74
|
+
def _is_tool_reference_annotation(cls, annotation: object) -> bool:
|
|
75
|
+
"""Check if annotation is ToolReference or Optional[ToolReference].
|
|
47
76
|
|
|
48
|
-
|
|
49
|
-
|
|
77
|
+
Args:
|
|
78
|
+
annotation: Type annotation to check.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
True if annotation is or contains ToolReference.
|
|
82
|
+
"""
|
|
83
|
+
origin = get_origin(annotation)
|
|
84
|
+
if origin is typing.Union or origin is types.UnionType:
|
|
85
|
+
return any(
|
|
86
|
+
arg is ToolReference or (isinstance(arg, type) and issubclass(arg, ToolReference))
|
|
87
|
+
for arg in get_args(annotation)
|
|
88
|
+
if arg is not type(None)
|
|
89
|
+
)
|
|
90
|
+
return annotation is ToolReference or (isinstance(annotation, type) and issubclass(annotation, ToolReference))
|
|
50
91
|
|
|
51
92
|
@classmethod
|
|
52
93
|
async def get_clean_model(
|
|
@@ -55,30 +96,17 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
55
96
|
config_fields: bool,
|
|
56
97
|
hidden_fields: bool,
|
|
57
98
|
force: bool = False,
|
|
58
|
-
) -> type[SetupModelT]:
|
|
59
|
-
"""
|
|
60
|
-
|
|
61
|
-
This method filters fields based on their `json_schema_extra` metadata:
|
|
62
|
-
- Fields with `{"config": True}` are included only when `config_fields=True`
|
|
63
|
-
- Fields with `{"hidden": True}` are included only when `hidden_fields=True`
|
|
64
|
-
|
|
65
|
-
When `force=True`, fields with dynamic schema providers will have their
|
|
66
|
-
providers called to fetch fresh values for schema metadata like enums.
|
|
67
|
-
This includes recursively processing nested BaseModel fields.
|
|
99
|
+
) -> "type[SetupModelT]":
|
|
100
|
+
"""Build filtered model based on json_schema_extra metadata.
|
|
68
101
|
|
|
69
102
|
Args:
|
|
70
|
-
config_fields:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
These are typically runtime-only fields not shown in initial config.
|
|
74
|
-
force: If True, refresh dynamic schema fields by calling their providers.
|
|
75
|
-
Use this when you need up-to-date values from external sources like
|
|
76
|
-
databases or APIs. Default is False for performance.
|
|
103
|
+
config_fields: Include fields with json_schema_extra["config"] = True.
|
|
104
|
+
hidden_fields: Include fields with json_schema_extra["hidden"] = True.
|
|
105
|
+
force: Refresh dynamic schema fields by calling providers.
|
|
77
106
|
|
|
78
107
|
Returns:
|
|
79
|
-
|
|
108
|
+
New BaseModel subclass with filtered fields.
|
|
80
109
|
"""
|
|
81
|
-
# Check cache for non-forced requests
|
|
82
110
|
cache_key = (cls, config_fields, hidden_fields)
|
|
83
111
|
if not force and cache_key in cls._clean_model_cache:
|
|
84
112
|
return cast("type[SetupModelT]", cls._clean_model_cache[cache_key])
|
|
@@ -90,68 +118,53 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
90
118
|
is_config = bool(extra.get("config", False)) if isinstance(extra, dict) else False
|
|
91
119
|
is_hidden = bool(extra.get("hidden", False)) if isinstance(extra, dict) else False
|
|
92
120
|
|
|
93
|
-
# Skip config unless explicitly included
|
|
94
121
|
if is_config and not config_fields:
|
|
95
|
-
logger.debug("Skipping '%s' (config-only)", name)
|
|
96
122
|
continue
|
|
97
|
-
|
|
98
|
-
# Skip hidden unless explicitly included
|
|
99
123
|
if is_hidden and not hidden_fields:
|
|
100
|
-
logger.debug("Skipping '%s' (hidden-only)", name)
|
|
101
124
|
continue
|
|
102
125
|
|
|
103
|
-
# Refresh dynamic schema fields when force=True
|
|
104
126
|
current_field_info = field_info
|
|
105
127
|
current_annotation = field_info.annotation
|
|
106
128
|
|
|
107
129
|
if force:
|
|
108
|
-
# Check if this field has DynamicField metadata
|
|
109
130
|
if has_dynamic(field_info):
|
|
110
131
|
current_field_info = await cls._refresh_field_schema(name, field_info)
|
|
111
132
|
|
|
112
|
-
# Check if the annotation is a nested BaseModel that might have dynamic fields
|
|
113
133
|
nested_model = cls._get_base_model_type(current_annotation)
|
|
114
134
|
if nested_model is not None:
|
|
115
135
|
refreshed_nested = await cls._refresh_nested_model(nested_model)
|
|
116
136
|
if refreshed_nested is not nested_model:
|
|
117
|
-
# Update annotation to use refreshed nested model
|
|
118
137
|
current_annotation = refreshed_nested
|
|
119
|
-
# Create new field_info with updated annotation (deep copy for safety)
|
|
120
138
|
current_field_info = copy.deepcopy(current_field_info)
|
|
121
139
|
current_field_info.annotation = current_annotation
|
|
122
140
|
|
|
123
141
|
clean_fields[name] = (current_annotation, current_field_info)
|
|
124
142
|
|
|
125
|
-
# Dynamically create a model e.g. "SetupModel"
|
|
126
143
|
m = create_model(
|
|
127
144
|
f"{cls.__name__}",
|
|
128
|
-
__base__=
|
|
145
|
+
__base__=SetupModel,
|
|
129
146
|
__config__=ConfigDict(arbitrary_types_allowed=True),
|
|
130
147
|
**clean_fields,
|
|
131
148
|
)
|
|
132
149
|
|
|
133
|
-
# Cache for non-forced requests
|
|
134
150
|
if not force:
|
|
135
151
|
cls._clean_model_cache[cache_key] = m
|
|
136
152
|
|
|
137
153
|
return cast("type[SetupModelT]", m)
|
|
138
154
|
|
|
139
155
|
@classmethod
|
|
140
|
-
def _get_base_model_type(cls, annotation: type | None) -> type[BaseModel] | None:
|
|
141
|
-
"""Extract BaseModel type from
|
|
142
|
-
|
|
143
|
-
Handles direct types, Optional, Union, list, dict, set, tuple, and other generics.
|
|
156
|
+
def _get_base_model_type(cls, annotation: "type | None") -> "type[BaseModel] | None":
|
|
157
|
+
"""Extract BaseModel type from annotation.
|
|
144
158
|
|
|
145
159
|
Args:
|
|
146
|
-
annotation:
|
|
160
|
+
annotation: Type annotation to inspect.
|
|
147
161
|
|
|
148
162
|
Returns:
|
|
149
|
-
|
|
163
|
+
BaseModel subclass if found, None otherwise.
|
|
150
164
|
"""
|
|
151
165
|
if annotation is None:
|
|
152
166
|
return None
|
|
153
167
|
|
|
154
|
-
# Direct BaseModel subclass check
|
|
155
168
|
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
|
156
169
|
return annotation
|
|
157
170
|
|
|
@@ -166,43 +179,41 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
166
179
|
def _extract_base_model_from_args(
|
|
167
180
|
cls,
|
|
168
181
|
origin: type,
|
|
169
|
-
args: tuple[type, ...],
|
|
170
|
-
) -> type[BaseModel] | None:
|
|
182
|
+
args: "tuple[type, ...]",
|
|
183
|
+
) -> "type[BaseModel] | None":
|
|
171
184
|
"""Extract BaseModel from generic type arguments.
|
|
172
185
|
|
|
173
186
|
Args:
|
|
174
|
-
origin:
|
|
175
|
-
args:
|
|
187
|
+
origin: Generic origin type (list, dict, Union, etc.).
|
|
188
|
+
args: Type arguments.
|
|
176
189
|
|
|
177
190
|
Returns:
|
|
178
|
-
|
|
191
|
+
BaseModel subclass if found, None otherwise.
|
|
179
192
|
"""
|
|
180
|
-
# Union/Optional: check each arg (supports both typing.Union and types.UnionType)
|
|
181
|
-
# Python 3.10+ uses types.UnionType for X | Y syntax
|
|
182
193
|
if origin is typing.Union or origin is types.UnionType:
|
|
183
194
|
return cls._find_base_model_in_args(args)
|
|
184
195
|
|
|
185
|
-
# list, set, frozenset: check first arg
|
|
186
196
|
if origin in {list, set, frozenset} and args:
|
|
187
197
|
return cls._check_base_model(args[0])
|
|
188
198
|
|
|
189
|
-
# dict: check value type (second arg)
|
|
190
199
|
dict_value_index = 1
|
|
191
200
|
if origin is dict and len(args) > dict_value_index:
|
|
192
201
|
return cls._check_base_model(args[dict_value_index])
|
|
193
202
|
|
|
194
|
-
# tuple: check first non-ellipsis arg
|
|
195
203
|
if origin is tuple:
|
|
196
204
|
return cls._find_base_model_in_args(args, skip_ellipsis=True)
|
|
197
205
|
|
|
198
206
|
return None
|
|
199
207
|
|
|
200
208
|
@classmethod
|
|
201
|
-
def _check_base_model(cls, arg: type) -> type[BaseModel] | None:
|
|
209
|
+
def _check_base_model(cls, arg: type) -> "type[BaseModel] | None":
|
|
202
210
|
"""Check if arg is a BaseModel subclass.
|
|
203
211
|
|
|
212
|
+
Args:
|
|
213
|
+
arg: Type to check.
|
|
214
|
+
|
|
204
215
|
Returns:
|
|
205
|
-
The
|
|
216
|
+
The type if it's a BaseModel subclass, None otherwise.
|
|
206
217
|
"""
|
|
207
218
|
if isinstance(arg, type) and issubclass(arg, BaseModel):
|
|
208
219
|
return arg
|
|
@@ -211,14 +222,18 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
211
222
|
@classmethod
|
|
212
223
|
def _find_base_model_in_args(
|
|
213
224
|
cls,
|
|
214
|
-
args: tuple[type, ...],
|
|
225
|
+
args: "tuple[type, ...]",
|
|
215
226
|
*,
|
|
216
227
|
skip_ellipsis: bool = False,
|
|
217
|
-
) -> type[BaseModel] | None:
|
|
218
|
-
"""Find first BaseModel in args.
|
|
228
|
+
) -> "type[BaseModel] | None":
|
|
229
|
+
"""Find first BaseModel in type args.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
args: Type arguments to search.
|
|
233
|
+
skip_ellipsis: Skip ellipsis in tuple types.
|
|
219
234
|
|
|
220
235
|
Returns:
|
|
221
|
-
|
|
236
|
+
First BaseModel subclass found, None otherwise.
|
|
222
237
|
"""
|
|
223
238
|
for arg in args:
|
|
224
239
|
if arg is type(None):
|
|
@@ -231,16 +246,14 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
231
246
|
return None
|
|
232
247
|
|
|
233
248
|
@classmethod
|
|
234
|
-
async def _refresh_nested_model(cls, model_cls: type[BaseModel]) -> type[BaseModel]:
|
|
249
|
+
async def _refresh_nested_model(cls, model_cls: "type[BaseModel]") -> "type[BaseModel]":
|
|
235
250
|
"""Refresh dynamic fields in a nested BaseModel.
|
|
236
251
|
|
|
237
|
-
Creates a new model class with all DynamicField metadata resolved.
|
|
238
|
-
|
|
239
252
|
Args:
|
|
240
|
-
model_cls:
|
|
253
|
+
model_cls: Nested model class to refresh.
|
|
241
254
|
|
|
242
255
|
Returns:
|
|
243
|
-
|
|
256
|
+
New model class with refreshed fields, or original if no changes.
|
|
244
257
|
"""
|
|
245
258
|
has_changes = False
|
|
246
259
|
clean_fields: dict[str, Any] = {}
|
|
@@ -249,12 +262,10 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
249
262
|
current_field_info = field_info
|
|
250
263
|
current_annotation = field_info.annotation
|
|
251
264
|
|
|
252
|
-
# Check if field has DynamicField metadata
|
|
253
265
|
if has_dynamic(field_info):
|
|
254
266
|
current_field_info = await cls._refresh_field_schema(name, field_info)
|
|
255
267
|
has_changes = True
|
|
256
268
|
|
|
257
|
-
# Recursively check nested models
|
|
258
269
|
nested_model = cls._get_base_model_type(current_annotation)
|
|
259
270
|
if nested_model is not None:
|
|
260
271
|
refreshed_nested = await cls._refresh_nested_model(nested_model)
|
|
@@ -269,8 +280,6 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
269
280
|
if not has_changes:
|
|
270
281
|
return model_cls
|
|
271
282
|
|
|
272
|
-
# Create new model with refreshed fields
|
|
273
|
-
logger.debug("Creating refreshed nested model for '%s'", model_cls.__name__)
|
|
274
283
|
return create_model(
|
|
275
284
|
model_cls.__name__,
|
|
276
285
|
__base__=BaseModel,
|
|
@@ -279,132 +288,84 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
279
288
|
)
|
|
280
289
|
|
|
281
290
|
@classmethod
|
|
282
|
-
async def _refresh_field_schema(cls, field_name: str, field_info: FieldInfo) -> FieldInfo:
|
|
283
|
-
"""Refresh
|
|
284
|
-
|
|
285
|
-
This method calls all dynamic providers registered for a field (via Annotated
|
|
286
|
-
metadata) and creates a new FieldInfo with the resolved values. The original
|
|
287
|
-
field_info is not modified.
|
|
288
|
-
|
|
289
|
-
Uses `resolve_safe()` for structured error handling, allowing partial success
|
|
290
|
-
when some fetchers fail. Successfully resolved values are still applied.
|
|
291
|
+
async def _refresh_field_schema(cls, field_name: str, field_info: "FieldInfo") -> "FieldInfo":
|
|
292
|
+
"""Refresh field's json_schema_extra with values from dynamic providers.
|
|
291
293
|
|
|
292
294
|
Args:
|
|
293
|
-
field_name:
|
|
294
|
-
field_info:
|
|
295
|
+
field_name: Name of field being refreshed.
|
|
296
|
+
field_info: Original FieldInfo with dynamic providers.
|
|
295
297
|
|
|
296
298
|
Returns:
|
|
297
|
-
|
|
298
|
-
`json_schema_extra` containing resolved values and Dynamic metadata removed.
|
|
299
|
-
|
|
300
|
-
Note:
|
|
301
|
-
If all fetchers fail, the original field_info is returned unchanged.
|
|
302
|
-
If some fetchers fail, successfully resolved values are still applied.
|
|
299
|
+
New FieldInfo with resolved values, or original if all fetchers fail.
|
|
303
300
|
"""
|
|
304
301
|
fetchers = get_fetchers(field_info)
|
|
305
302
|
|
|
306
303
|
if not fetchers:
|
|
307
304
|
return field_info
|
|
308
305
|
|
|
309
|
-
fetcher_keys = list(fetchers.keys())
|
|
310
|
-
logger.debug(
|
|
311
|
-
"Refreshing dynamic schema for field '%s' with fetchers: %s",
|
|
312
|
-
field_name,
|
|
313
|
-
fetcher_keys,
|
|
314
|
-
extra={"field_name": field_name, "fetcher_keys": fetcher_keys},
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
# Resolve all fetchers with structured error handling
|
|
318
306
|
result = await resolve_safe(fetchers)
|
|
319
307
|
|
|
320
|
-
# Log any errors that occurred with full details
|
|
321
308
|
if result.errors:
|
|
322
309
|
for key, error in result.errors.items():
|
|
323
310
|
logger.warning(
|
|
324
|
-
"Failed to resolve '%s' for field '%s': %s
|
|
311
|
+
"Failed to resolve '%s' for field '%s': %s",
|
|
325
312
|
key,
|
|
326
313
|
field_name,
|
|
327
|
-
|
|
328
|
-
str(error) or "(no message)",
|
|
329
|
-
extra={
|
|
330
|
-
"field_name": field_name,
|
|
331
|
-
"fetcher_key": key,
|
|
332
|
-
"error_type": type(error).__name__,
|
|
333
|
-
"error_message": str(error),
|
|
334
|
-
"error_repr": repr(error),
|
|
335
|
-
},
|
|
314
|
+
error,
|
|
336
315
|
)
|
|
337
316
|
|
|
338
|
-
# If no values were resolved, return original field_info
|
|
339
317
|
if not result.values:
|
|
340
|
-
logger.warning(
|
|
341
|
-
"All fetchers failed for field '%s', keeping original",
|
|
342
|
-
field_name,
|
|
343
|
-
)
|
|
344
318
|
return field_info
|
|
345
319
|
|
|
346
|
-
# Build new json_schema_extra with resolved values merged
|
|
347
320
|
extra = field_info.json_schema_extra or {}
|
|
348
321
|
new_extra = {**extra, **result.values} if isinstance(extra, dict) else result.values
|
|
349
322
|
|
|
350
|
-
# Create a deep copy of the FieldInfo to avoid shared mutable state
|
|
351
323
|
new_field_info = copy.deepcopy(field_info)
|
|
352
324
|
new_field_info.json_schema_extra = new_extra
|
|
353
|
-
|
|
354
|
-
# Remove Dynamic from metadata (it's been resolved)
|
|
355
|
-
new_metadata = [m for m in new_field_info.metadata if not isinstance(m, DynamicField)]
|
|
356
|
-
new_field_info.metadata = new_metadata
|
|
357
|
-
|
|
358
|
-
logger.debug(
|
|
359
|
-
"Refreshed '%s' with dynamic values: %s",
|
|
360
|
-
field_name,
|
|
361
|
-
list(result.values.keys()),
|
|
362
|
-
)
|
|
325
|
+
new_field_info.metadata = [m for m in new_field_info.metadata if not isinstance(m, DynamicField)]
|
|
363
326
|
|
|
364
327
|
return new_field_info
|
|
365
328
|
|
|
366
|
-
def resolve_tool_references(self, registry: RegistryStrategy) -> None:
|
|
367
|
-
"""Resolve all ToolReference fields
|
|
368
|
-
|
|
369
|
-
Recursively walks through all fields, including nested BaseModel instances,
|
|
370
|
-
and resolves any ToolReference fields using the provided registry.
|
|
329
|
+
def resolve_tool_references(self, registry: "RegistryStrategy") -> None:
|
|
330
|
+
"""Resolve all ToolReference fields recursively.
|
|
371
331
|
|
|
372
332
|
Args:
|
|
373
|
-
registry: Registry service
|
|
333
|
+
registry: Registry service for module discovery.
|
|
374
334
|
"""
|
|
335
|
+
logger.info("Starting resolve_tool_references")
|
|
375
336
|
self._resolve_tool_references_recursive(self, registry)
|
|
337
|
+
logger.info("Finished resolve_tool_references")
|
|
376
338
|
|
|
377
339
|
@classmethod
|
|
378
340
|
def _resolve_tool_references_recursive(
|
|
379
341
|
cls,
|
|
380
342
|
model_instance: BaseModel,
|
|
381
|
-
registry: RegistryStrategy,
|
|
343
|
+
registry: "RegistryStrategy",
|
|
382
344
|
) -> None:
|
|
383
|
-
"""Recursively resolve ToolReference fields in a model
|
|
345
|
+
"""Recursively resolve ToolReference fields in a model.
|
|
384
346
|
|
|
385
347
|
Args:
|
|
386
|
-
model_instance:
|
|
387
|
-
registry: Registry service
|
|
348
|
+
model_instance: Model instance to process.
|
|
349
|
+
registry: Registry service for resolution.
|
|
388
350
|
"""
|
|
389
351
|
for field_name, field_value in model_instance.__dict__.items():
|
|
390
352
|
if field_value is None:
|
|
391
353
|
continue
|
|
392
|
-
|
|
393
354
|
cls._resolve_field_value(field_name, field_value, registry)
|
|
394
355
|
|
|
395
356
|
@classmethod
|
|
396
357
|
def _resolve_field_value(
|
|
397
358
|
cls,
|
|
398
359
|
field_name: str,
|
|
399
|
-
field_value: BaseModel | ToolReference | list | dict,
|
|
400
|
-
registry: RegistryStrategy,
|
|
360
|
+
field_value: "BaseModel | ToolReference | list | dict",
|
|
361
|
+
registry: "RegistryStrategy",
|
|
401
362
|
) -> None:
|
|
402
|
-
"""Resolve a single field value
|
|
363
|
+
"""Resolve a single field value based on its type.
|
|
403
364
|
|
|
404
365
|
Args:
|
|
405
|
-
field_name: Name of the field
|
|
406
|
-
field_value:
|
|
407
|
-
registry: Registry service
|
|
366
|
+
field_name: Name of the field.
|
|
367
|
+
field_value: Value to process.
|
|
368
|
+
registry: Registry service for resolution.
|
|
408
369
|
"""
|
|
409
370
|
if isinstance(field_value, ToolReference):
|
|
410
371
|
cls._resolve_single_tool_reference(field_name, field_value, registry)
|
|
@@ -420,40 +381,29 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
420
381
|
cls,
|
|
421
382
|
field_name: str,
|
|
422
383
|
tool_ref: ToolReference,
|
|
423
|
-
registry: RegistryStrategy,
|
|
384
|
+
registry: "RegistryStrategy",
|
|
424
385
|
) -> None:
|
|
425
|
-
"""Resolve a single ToolReference
|
|
386
|
+
"""Resolve a single ToolReference.
|
|
426
387
|
|
|
427
388
|
Args:
|
|
428
389
|
field_name: Name of the field for logging.
|
|
429
|
-
tool_ref:
|
|
430
|
-
registry: Registry service
|
|
390
|
+
tool_ref: ToolReference to resolve.
|
|
391
|
+
registry: Registry service for resolution.
|
|
431
392
|
"""
|
|
393
|
+
logger.info("Resolving ToolReference '%s' with module_id='%s'", field_name, tool_ref.config.module_id)
|
|
432
394
|
try:
|
|
433
395
|
tool_ref.resolve(registry)
|
|
434
|
-
logger.
|
|
435
|
-
"Resolved ToolReference field '%s'",
|
|
436
|
-
field_name,
|
|
437
|
-
extra={"field_name": field_name, "mode": tool_ref.config.mode.value},
|
|
438
|
-
)
|
|
396
|
+
logger.info("Resolved ToolReference '%s' -> %s", field_name, tool_ref.module_info)
|
|
439
397
|
except Exception:
|
|
440
|
-
logger.exception(
|
|
441
|
-
"Failed to resolve ToolReference field '%s'",
|
|
442
|
-
field_name,
|
|
443
|
-
extra={"field_name": field_name, "config": tool_ref.config.model_dump()},
|
|
444
|
-
)
|
|
398
|
+
logger.exception("Failed to resolve ToolReference '%s'", field_name)
|
|
445
399
|
|
|
446
400
|
@classmethod
|
|
447
|
-
def _resolve_list_items(
|
|
448
|
-
cls,
|
|
449
|
-
items: list,
|
|
450
|
-
registry: RegistryStrategy,
|
|
451
|
-
) -> None:
|
|
401
|
+
def _resolve_list_items(cls, items: list, registry: "RegistryStrategy") -> None:
|
|
452
402
|
"""Resolve ToolReference instances in a list.
|
|
453
403
|
|
|
454
404
|
Args:
|
|
455
405
|
items: List of items to process.
|
|
456
|
-
registry: Registry service
|
|
406
|
+
registry: Registry service for resolution.
|
|
457
407
|
"""
|
|
458
408
|
for item in items:
|
|
459
409
|
if isinstance(item, ToolReference):
|
|
@@ -462,16 +412,12 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
462
412
|
cls._resolve_tool_references_recursive(item, registry)
|
|
463
413
|
|
|
464
414
|
@classmethod
|
|
465
|
-
def _resolve_dict_values(
|
|
466
|
-
|
|
467
|
-
mapping: dict,
|
|
468
|
-
registry: RegistryStrategy,
|
|
469
|
-
) -> None:
|
|
470
|
-
"""Resolve ToolReference instances in a dict's values.
|
|
415
|
+
def _resolve_dict_values(cls, mapping: dict, registry: "RegistryStrategy") -> None:
|
|
416
|
+
"""Resolve ToolReference instances in dict values.
|
|
471
417
|
|
|
472
418
|
Args:
|
|
473
419
|
mapping: Dict to process.
|
|
474
|
-
registry: Registry service
|
|
420
|
+
registry: Registry service for resolution.
|
|
475
421
|
"""
|
|
476
422
|
for item in mapping.values():
|
|
477
423
|
if isinstance(item, ToolReference):
|
|
@@ -479,95 +425,60 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
479
425
|
elif isinstance(item, BaseModel):
|
|
480
426
|
cls._resolve_tool_references_recursive(item, registry)
|
|
481
427
|
|
|
482
|
-
@property
|
|
483
|
-
def tool_cache(self) -> ToolCache:
|
|
484
|
-
"""Get the tool cache for this setup instance.
|
|
485
|
-
|
|
486
|
-
Returns:
|
|
487
|
-
The ToolCache containing resolved tools.
|
|
488
|
-
"""
|
|
489
|
-
return self._tool_cache
|
|
490
|
-
|
|
491
428
|
def build_tool_cache(self) -> ToolCache:
|
|
492
|
-
"""Build
|
|
493
|
-
|
|
494
|
-
This should be called during run_config_setup after resolve_tool_references.
|
|
495
|
-
It walks all ToolReference fields and adds resolved ones to the cache.
|
|
429
|
+
"""Build tool cache from resolved ToolReferences, populating companion fields.
|
|
496
430
|
|
|
497
431
|
Returns:
|
|
498
|
-
|
|
432
|
+
ToolCache with field names as keys and ModuleInfo as values.
|
|
499
433
|
"""
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
return self._tool_cache
|
|
434
|
+
logger.info("Building tool cache")
|
|
435
|
+
cache = ToolCache()
|
|
436
|
+
self._build_tool_cache_recursive(self, cache)
|
|
437
|
+
logger.info("Tool cache built: %d entries", len(cache.entries))
|
|
438
|
+
return cache
|
|
506
439
|
|
|
507
|
-
def _build_tool_cache_recursive(self, model_instance: BaseModel) -> None:
|
|
508
|
-
"""Recursively build tool cache
|
|
440
|
+
def _build_tool_cache_recursive(self, model_instance: BaseModel, cache: ToolCache) -> None: # noqa: C901, PLR0912
|
|
441
|
+
"""Recursively build tool cache and populate companion fields.
|
|
509
442
|
|
|
510
443
|
Args:
|
|
511
|
-
model_instance:
|
|
444
|
+
model_instance: Model instance to process.
|
|
445
|
+
cache: ToolCache to populate.
|
|
512
446
|
"""
|
|
513
447
|
for field_name, field_value in model_instance.__dict__.items():
|
|
514
448
|
if field_value is None:
|
|
515
449
|
continue
|
|
516
450
|
|
|
517
|
-
if isinstance(field_value, ToolReference):
|
|
518
|
-
|
|
451
|
+
if isinstance(field_value, ToolReference) and field_value.module_info:
|
|
452
|
+
cache_field_name = f"{field_name}_cache"
|
|
453
|
+
if cache_field_name in type(model_instance).model_fields:
|
|
454
|
+
setattr(model_instance, cache_field_name, field_value.module_info)
|
|
455
|
+
cache.add(field_value.module_info.module_id, field_value.module_info)
|
|
456
|
+
logger.debug("Added tool to cache: %s", field_value.module_info.module_id)
|
|
519
457
|
elif isinstance(field_value, BaseModel):
|
|
520
|
-
self._build_tool_cache_recursive(field_value)
|
|
458
|
+
self._build_tool_cache_recursive(field_value, cache)
|
|
521
459
|
elif isinstance(field_value, list):
|
|
522
|
-
|
|
460
|
+
cache_field_name = f"{field_name}_cache"
|
|
461
|
+
cached_infos = getattr(model_instance, cache_field_name, None) or []
|
|
462
|
+
resolved_infos: list[ModuleInfo] = []
|
|
463
|
+
|
|
464
|
+
for idx, item in enumerate(field_value):
|
|
465
|
+
if isinstance(item, ToolReference):
|
|
466
|
+
# Use resolved info or fallback to cached
|
|
467
|
+
module_info = item.module_info or (cached_infos[idx] if idx < len(cached_infos) else None)
|
|
468
|
+
if module_info:
|
|
469
|
+
resolved_infos.append(module_info)
|
|
470
|
+
cache.add(module_info.module_id, module_info)
|
|
471
|
+
logger.debug("Added tool to cache: %s", module_info.module_id)
|
|
472
|
+
elif isinstance(item, BaseModel):
|
|
473
|
+
self._build_tool_cache_recursive(item, cache)
|
|
474
|
+
|
|
475
|
+
# Update companion field with resolved infos
|
|
476
|
+
if resolved_infos and cache_field_name in type(model_instance).model_fields:
|
|
477
|
+
setattr(model_instance, cache_field_name, resolved_infos)
|
|
523
478
|
elif isinstance(field_value, dict):
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
field_name: Name of the field (used as fallback slug).
|
|
531
|
-
tool_ref: The ToolReference instance.
|
|
532
|
-
"""
|
|
533
|
-
if tool_ref.module_info:
|
|
534
|
-
# Use slug from config, or field name as fallback
|
|
535
|
-
slug = tool_ref.slug or field_name
|
|
536
|
-
self._tool_cache.add(slug, tool_ref.module_info)
|
|
537
|
-
|
|
538
|
-
def _build_tool_cache_from_list(self, items: list) -> None:
|
|
539
|
-
"""Build tool cache from list items.
|
|
540
|
-
|
|
541
|
-
Args:
|
|
542
|
-
items: List of items to process.
|
|
543
|
-
"""
|
|
544
|
-
for idx, item in enumerate(items):
|
|
545
|
-
if isinstance(item, ToolReference):
|
|
546
|
-
self._add_tool_reference_to_cache(f"list_{idx}", item)
|
|
547
|
-
elif isinstance(item, BaseModel):
|
|
548
|
-
self._build_tool_cache_recursive(item)
|
|
549
|
-
|
|
550
|
-
def _build_tool_cache_from_dict(self, mapping: dict) -> None:
|
|
551
|
-
"""Build tool cache from dict values.
|
|
552
|
-
|
|
553
|
-
Args:
|
|
554
|
-
mapping: Dict to process.
|
|
555
|
-
"""
|
|
556
|
-
for key, item in mapping.items():
|
|
557
|
-
if isinstance(item, ToolReference):
|
|
558
|
-
self._add_tool_reference_to_cache(str(key), item)
|
|
559
|
-
elif isinstance(item, BaseModel):
|
|
560
|
-
self._build_tool_cache_recursive(item)
|
|
561
|
-
|
|
562
|
-
def validate_tool_cache(self, registry: RegistryStrategy) -> list[str]:
|
|
563
|
-
"""Validate all cached tools are still available.
|
|
564
|
-
|
|
565
|
-
Should be called during initialize to ensure tools are still valid.
|
|
566
|
-
|
|
567
|
-
Args:
|
|
568
|
-
registry: Registry to validate against.
|
|
569
|
-
|
|
570
|
-
Returns:
|
|
571
|
-
List of slugs that are no longer valid.
|
|
572
|
-
"""
|
|
573
|
-
return self._tool_cache.validate(registry)
|
|
479
|
+
for item in field_value.values():
|
|
480
|
+
if isinstance(item, ToolReference) and item.module_info:
|
|
481
|
+
cache.add(item.module_info.module_id, item.module_info)
|
|
482
|
+
logger.debug("Added tool to cache: %s", item.module_info.module_id)
|
|
483
|
+
elif isinstance(item, BaseModel):
|
|
484
|
+
self._build_tool_cache_recursive(item, cache)
|