digitalkin 0.3.2.dev17__py3-none-any.whl → 0.3.2.dev19__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 +27 -2
- digitalkin/models/module/module_context.py +10 -14
- digitalkin/models/module/setup_types.py +141 -102
- digitalkin/models/module/tool_cache.py +15 -11
- digitalkin/models/module/tool_reference.py +4 -2
- digitalkin/modules/_base_module.py +1 -15
- digitalkin/utils/__init__.py +14 -0
- digitalkin/utils/conditional_schema.py +260 -0
- digitalkin/utils/schema_splitter.py +6 -0
- {digitalkin-0.3.2.dev17.dist-info → digitalkin-0.3.2.dev19.dist-info}/METADATA +1 -1
- {digitalkin-0.3.2.dev17.dist-info → digitalkin-0.3.2.dev19.dist-info}/RECORD +15 -14
- {digitalkin-0.3.2.dev17.dist-info → digitalkin-0.3.2.dev19.dist-info}/WHEEL +1 -1
- {digitalkin-0.3.2.dev17.dist-info → digitalkin-0.3.2.dev19.dist-info}/licenses/LICENSE +0 -0
- {digitalkin-0.3.2.dev17.dist-info → digitalkin-0.3.2.dev19.dist-info}/top_level.txt +0 -0
digitalkin/__version__.py
CHANGED
|
@@ -17,7 +17,7 @@ from digitalkin.core.job_manager.base_job_manager import BaseJobManager
|
|
|
17
17
|
from digitalkin.grpc_servers.utils.exceptions import ServicerError
|
|
18
18
|
from digitalkin.logger import logger
|
|
19
19
|
from digitalkin.models.core.job_manager_models import JobManagerMode
|
|
20
|
-
from digitalkin.models.module.module import ModuleStatus
|
|
20
|
+
from digitalkin.models.module.module import ModuleCodeModel, ModuleStatus
|
|
21
21
|
from digitalkin.modules._base_module import BaseModule
|
|
22
22
|
from digitalkin.services.registry import GrpcRegistry, RegistryStrategy
|
|
23
23
|
from digitalkin.services.services_models import ServicesMode
|
|
@@ -159,7 +159,32 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
|
|
|
159
159
|
return lifecycle_pb2.ConfigSetupModuleResponse(success=False)
|
|
160
160
|
|
|
161
161
|
updated_setup_data = await self.job_manager.generate_config_setup_module_response(job_id)
|
|
162
|
-
logger.info("Setup
|
|
162
|
+
logger.info("Setup response received", extra={"job_id": job_id})
|
|
163
|
+
|
|
164
|
+
# Check if response is an error
|
|
165
|
+
if isinstance(updated_setup_data, ModuleCodeModel):
|
|
166
|
+
logger.error(
|
|
167
|
+
"Config setup failed",
|
|
168
|
+
extra={"job_id": job_id, "code": updated_setup_data.code, "message": updated_setup_data.message},
|
|
169
|
+
)
|
|
170
|
+
context.set_code(grpc.StatusCode.INTERNAL)
|
|
171
|
+
context.set_details(updated_setup_data.message or "Config setup failed")
|
|
172
|
+
return lifecycle_pb2.ConfigSetupModuleResponse(success=False)
|
|
173
|
+
|
|
174
|
+
if isinstance(updated_setup_data, dict) and "code" in updated_setup_data:
|
|
175
|
+
# ModuleCodeModel was serialized to dict
|
|
176
|
+
logger.error(
|
|
177
|
+
"Config setup failed",
|
|
178
|
+
extra={
|
|
179
|
+
"job_id": job_id,
|
|
180
|
+
"code": updated_setup_data["code"],
|
|
181
|
+
"message": updated_setup_data.get("message"),
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
context.set_code(grpc.StatusCode.INTERNAL)
|
|
185
|
+
context.set_details(updated_setup_data.get("message") or "Config setup failed")
|
|
186
|
+
return lifecycle_pb2.ConfigSetupModuleResponse(success=False)
|
|
187
|
+
|
|
163
188
|
logger.debug("Updated setup data", extra={"job_id": job_id, "setup_data": updated_setup_data})
|
|
164
189
|
setup_version.content = json_format.ParseDict(
|
|
165
190
|
updated_setup_data,
|
|
@@ -8,7 +8,7 @@ from typing import Any
|
|
|
8
8
|
from zoneinfo import ZoneInfo
|
|
9
9
|
|
|
10
10
|
from digitalkin.logger import logger
|
|
11
|
-
from digitalkin.models.module.tool_cache import ToolCache, ToolDefinition, ToolParameter
|
|
11
|
+
from digitalkin.models.module.tool_cache import ToolCache, ToolDefinition, ToolModuleInfo, ToolParameter
|
|
12
12
|
from digitalkin.services.agent.agent_strategy import AgentStrategy
|
|
13
13
|
from digitalkin.services.communication.communication_strategy import CommunicationStrategy
|
|
14
14
|
from digitalkin.services.cost.cost_strategy import CostStrategy
|
|
@@ -227,20 +227,20 @@ class ModuleContext:
|
|
|
227
227
|
llm_format=llm_format,
|
|
228
228
|
)
|
|
229
229
|
|
|
230
|
-
async def create_openai_style_tools(self,
|
|
230
|
+
async def create_openai_style_tools(self, module_id: str) -> list[dict[str, Any]]:
|
|
231
231
|
"""Create OpenAI-style function calling schemas for a tool module.
|
|
232
232
|
|
|
233
233
|
Uses tool cache (fast path) with registry fallback. Returns one schema
|
|
234
234
|
per ToolDefinition (protocol) in the module.
|
|
235
235
|
|
|
236
236
|
Args:
|
|
237
|
-
|
|
237
|
+
module_id: Module ID to look up (checks cache first, then registry).
|
|
238
238
|
|
|
239
239
|
Returns:
|
|
240
240
|
List of OpenAI-style tool schemas, one per protocol. Empty if not found.
|
|
241
241
|
"""
|
|
242
242
|
tool_module_info = await self.tool_cache.get(
|
|
243
|
-
|
|
243
|
+
module_id, registry=self.registry, communication=self.communication
|
|
244
244
|
)
|
|
245
245
|
if not tool_module_info:
|
|
246
246
|
return []
|
|
@@ -299,13 +299,11 @@ class ModuleContext:
|
|
|
299
299
|
|
|
300
300
|
communication = self.communication
|
|
301
301
|
session = self.session
|
|
302
|
-
address = tool_module_info.address
|
|
303
|
-
port = tool_module_info.port
|
|
304
302
|
|
|
305
303
|
result = []
|
|
306
304
|
for tool_def in tool_module_info.tools:
|
|
307
305
|
# Capture tool_def in closure via separate method
|
|
308
|
-
fn = ModuleContext._create_single_tool_function(communication, session,
|
|
306
|
+
fn = ModuleContext._create_single_tool_function(communication, session, tool_module_info, tool_def)
|
|
309
307
|
result.append((tool_def, fn))
|
|
310
308
|
|
|
311
309
|
return result
|
|
@@ -314,8 +312,7 @@ class ModuleContext:
|
|
|
314
312
|
def _create_single_tool_function(
|
|
315
313
|
communication: CommunicationStrategy,
|
|
316
314
|
session: Session,
|
|
317
|
-
|
|
318
|
-
port: int,
|
|
315
|
+
tool_module_info: ToolModuleInfo,
|
|
319
316
|
tool_def: ToolDefinition,
|
|
320
317
|
) -> Callable[..., AsyncGenerator[dict, None]]:
|
|
321
318
|
"""Create a single tool function for a specific protocol.
|
|
@@ -323,8 +320,7 @@ class ModuleContext:
|
|
|
323
320
|
Args:
|
|
324
321
|
communication: Communication strategy for gRPC calls.
|
|
325
322
|
session: Current session with setup_id and mission_id.
|
|
326
|
-
|
|
327
|
-
port: Module port.
|
|
323
|
+
tool_module_info: Tool module information containing address and port.
|
|
328
324
|
tool_def: Tool definition with protocol name.
|
|
329
325
|
|
|
330
326
|
Returns:
|
|
@@ -336,10 +332,10 @@ class ModuleContext:
|
|
|
336
332
|
kwargs["protocol"] = protocol
|
|
337
333
|
wrapped_input = {"root": kwargs}
|
|
338
334
|
async for response in communication.call_module(
|
|
339
|
-
module_address=address,
|
|
340
|
-
module_port=port,
|
|
335
|
+
module_address=tool_module_info.address,
|
|
336
|
+
module_port=tool_module_info.port,
|
|
341
337
|
input_data=wrapped_input,
|
|
342
|
-
setup_id=
|
|
338
|
+
setup_id=tool_module_info.setup_id,
|
|
343
339
|
mission_id=session.mission_id,
|
|
344
340
|
):
|
|
345
341
|
yield response
|
|
@@ -29,65 +29,12 @@ SetupModelT = TypeVar("SetupModelT", bound="SetupModel")
|
|
|
29
29
|
class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
30
30
|
"""Base setup model with dynamic schema and tool cache support."""
|
|
31
31
|
|
|
32
|
+
model_config = ConfigDict(extra="allow")
|
|
32
33
|
_clean_model_cache: ClassVar[dict[tuple[type, bool, bool], type]] = {}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
""
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
**kwargs: Keyword arguments passed to parent.
|
|
39
|
-
"""
|
|
40
|
-
super().__init_subclass__(**kwargs)
|
|
41
|
-
cls._inject_tool_cache_fields()
|
|
42
|
-
|
|
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[ToolModuleInfo]
|
|
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] = ToolModuleInfo | 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}
|
|
72
|
-
|
|
73
|
-
@classmethod
|
|
74
|
-
def _is_tool_reference_annotation(cls, annotation: object) -> bool:
|
|
75
|
-
"""Check if annotation is ToolReference or Optional[ToolReference].
|
|
76
|
-
|
|
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))
|
|
34
|
+
resolved_tools: dict[str, ToolModuleInfo] = Field(
|
|
35
|
+
default_factory=dict,
|
|
36
|
+
json_schema_extra={"hidden": True},
|
|
37
|
+
)
|
|
91
38
|
|
|
92
39
|
@classmethod
|
|
93
40
|
async def get_clean_model(
|
|
@@ -285,7 +232,7 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
285
232
|
if not has_changes:
|
|
286
233
|
return model_cls
|
|
287
234
|
|
|
288
|
-
root_extra =
|
|
235
|
+
root_extra = model_cls.model_config.get("json_schema_extra", {})
|
|
289
236
|
|
|
290
237
|
return create_model(
|
|
291
238
|
model_cls.__name__,
|
|
@@ -346,7 +293,12 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
346
293
|
communication: Communication service for module schemas.
|
|
347
294
|
"""
|
|
348
295
|
logger.info("Starting resolve_tool_references")
|
|
349
|
-
await self._resolve_tool_references_recursive(
|
|
296
|
+
await self._resolve_tool_references_recursive(
|
|
297
|
+
self,
|
|
298
|
+
registry,
|
|
299
|
+
communication,
|
|
300
|
+
self.resolved_tools,
|
|
301
|
+
)
|
|
350
302
|
logger.info("Finished resolve_tool_references")
|
|
351
303
|
|
|
352
304
|
@classmethod
|
|
@@ -355,6 +307,7 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
355
307
|
model_instance: BaseModel,
|
|
356
308
|
registry: "RegistryStrategy",
|
|
357
309
|
communication: "CommunicationStrategy",
|
|
310
|
+
resolved_tools: dict[str, ToolModuleInfo],
|
|
358
311
|
) -> None:
|
|
359
312
|
"""Recursively resolve ToolReference fields in a model.
|
|
360
313
|
|
|
@@ -362,11 +315,18 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
362
315
|
model_instance: Model instance to process.
|
|
363
316
|
registry: Registry service for resolution.
|
|
364
317
|
communication: Communication service for module schemas.
|
|
318
|
+
resolved_tools: Cache of already resolved tools.
|
|
365
319
|
"""
|
|
366
320
|
for field_name, field_value in model_instance.__dict__.items():
|
|
367
321
|
if field_value is None:
|
|
368
322
|
continue
|
|
369
|
-
await cls._resolve_field_value(
|
|
323
|
+
await cls._resolve_field_value(
|
|
324
|
+
field_name,
|
|
325
|
+
field_value,
|
|
326
|
+
registry,
|
|
327
|
+
communication,
|
|
328
|
+
resolved_tools,
|
|
329
|
+
)
|
|
370
330
|
|
|
371
331
|
@classmethod
|
|
372
332
|
async def _resolve_field_value(
|
|
@@ -375,6 +335,7 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
375
335
|
field_value: "BaseModel | ToolReference | list | dict",
|
|
376
336
|
registry: "RegistryStrategy",
|
|
377
337
|
communication: "CommunicationStrategy",
|
|
338
|
+
resolved_tools: dict[str, ToolModuleInfo],
|
|
378
339
|
) -> None:
|
|
379
340
|
"""Resolve a single field value based on its type.
|
|
380
341
|
|
|
@@ -383,15 +344,37 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
383
344
|
field_value: Value to process.
|
|
384
345
|
registry: Registry service for resolution.
|
|
385
346
|
communication: Communication service for module schemas.
|
|
347
|
+
resolved_tools: Cache of already resolved tools.
|
|
386
348
|
"""
|
|
387
349
|
if isinstance(field_value, ToolReference):
|
|
388
|
-
await cls._resolve_single_tool_reference(
|
|
350
|
+
await cls._resolve_single_tool_reference(
|
|
351
|
+
field_name,
|
|
352
|
+
field_value,
|
|
353
|
+
registry,
|
|
354
|
+
communication,
|
|
355
|
+
resolved_tools,
|
|
356
|
+
)
|
|
389
357
|
elif isinstance(field_value, BaseModel):
|
|
390
|
-
await cls._resolve_tool_references_recursive(
|
|
358
|
+
await cls._resolve_tool_references_recursive(
|
|
359
|
+
field_value,
|
|
360
|
+
registry,
|
|
361
|
+
communication,
|
|
362
|
+
resolved_tools,
|
|
363
|
+
)
|
|
391
364
|
elif isinstance(field_value, list):
|
|
392
|
-
await cls._resolve_list_items(
|
|
365
|
+
await cls._resolve_list_items(
|
|
366
|
+
field_value,
|
|
367
|
+
registry,
|
|
368
|
+
communication,
|
|
369
|
+
resolved_tools,
|
|
370
|
+
)
|
|
393
371
|
elif isinstance(field_value, dict):
|
|
394
|
-
await cls._resolve_dict_values(
|
|
372
|
+
await cls._resolve_dict_values(
|
|
373
|
+
field_value,
|
|
374
|
+
registry,
|
|
375
|
+
communication,
|
|
376
|
+
resolved_tools,
|
|
377
|
+
)
|
|
395
378
|
|
|
396
379
|
@classmethod
|
|
397
380
|
async def _resolve_single_tool_reference(
|
|
@@ -400,6 +383,7 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
400
383
|
tool_ref: ToolReference,
|
|
401
384
|
registry: "RegistryStrategy",
|
|
402
385
|
communication: "CommunicationStrategy",
|
|
386
|
+
resolved_tools: dict[str, ToolModuleInfo],
|
|
403
387
|
) -> None:
|
|
404
388
|
"""Resolve a single ToolReference.
|
|
405
389
|
|
|
@@ -408,17 +392,33 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
408
392
|
tool_ref: ToolReference to resolve.
|
|
409
393
|
registry: Registry service for resolution.
|
|
410
394
|
communication: Communication service for module schemas.
|
|
395
|
+
resolved_tools: Cache of already resolved tools.
|
|
411
396
|
"""
|
|
412
397
|
logger.info("Resolving ToolReference '%s' with setup_id='%s'", field_name, tool_ref.config.setup_id)
|
|
398
|
+
|
|
399
|
+
slug = tool_ref.slug
|
|
400
|
+
if slug:
|
|
401
|
+
cached = resolved_tools.get(slug)
|
|
402
|
+
if cached:
|
|
403
|
+
tool_ref._cached_info = cached # noqa: SLF001
|
|
404
|
+
logger.info("ToolReference '%s' resolved from cache -> %s", field_name, cached)
|
|
405
|
+
return
|
|
406
|
+
|
|
413
407
|
try:
|
|
414
|
-
await tool_ref.resolve(registry, communication)
|
|
408
|
+
info = await tool_ref.resolve(registry, communication)
|
|
409
|
+
if info and info.setup_id:
|
|
410
|
+
resolved_tools[info.setup_id] = info
|
|
415
411
|
logger.info("Resolved ToolReference '%s' -> %s", field_name, tool_ref.tool_module_info)
|
|
416
412
|
except Exception:
|
|
417
413
|
logger.exception("Failed to resolve ToolReference '%s'", field_name)
|
|
418
414
|
|
|
419
415
|
@classmethod
|
|
420
416
|
async def _resolve_list_items(
|
|
421
|
-
cls,
|
|
417
|
+
cls,
|
|
418
|
+
items: list,
|
|
419
|
+
registry: "RegistryStrategy",
|
|
420
|
+
communication: "CommunicationStrategy",
|
|
421
|
+
resolved_tools: dict[str, ToolModuleInfo],
|
|
422
422
|
) -> None:
|
|
423
423
|
"""Resolve ToolReference instances in a list.
|
|
424
424
|
|
|
@@ -426,16 +426,32 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
426
426
|
items: List of items to process.
|
|
427
427
|
registry: Registry service for resolution.
|
|
428
428
|
communication: Communication service for module schemas.
|
|
429
|
+
resolved_tools: Cache of already resolved tools.
|
|
429
430
|
"""
|
|
430
431
|
for item in items:
|
|
431
432
|
if isinstance(item, ToolReference):
|
|
432
|
-
await cls._resolve_single_tool_reference(
|
|
433
|
+
await cls._resolve_single_tool_reference(
|
|
434
|
+
"list_item",
|
|
435
|
+
item,
|
|
436
|
+
registry,
|
|
437
|
+
communication,
|
|
438
|
+
resolved_tools,
|
|
439
|
+
)
|
|
433
440
|
elif isinstance(item, BaseModel):
|
|
434
|
-
await cls._resolve_tool_references_recursive(
|
|
441
|
+
await cls._resolve_tool_references_recursive(
|
|
442
|
+
item,
|
|
443
|
+
registry,
|
|
444
|
+
communication,
|
|
445
|
+
resolved_tools,
|
|
446
|
+
)
|
|
435
447
|
|
|
436
448
|
@classmethod
|
|
437
449
|
async def _resolve_dict_values(
|
|
438
|
-
cls,
|
|
450
|
+
cls,
|
|
451
|
+
mapping: dict,
|
|
452
|
+
registry: "RegistryStrategy",
|
|
453
|
+
communication: "CommunicationStrategy",
|
|
454
|
+
resolved_tools: dict[str, ToolModuleInfo],
|
|
439
455
|
) -> None:
|
|
440
456
|
"""Resolve ToolReference instances in dict values.
|
|
441
457
|
|
|
@@ -443,63 +459,86 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
|
|
|
443
459
|
mapping: Dict to process.
|
|
444
460
|
registry: Registry service for resolution.
|
|
445
461
|
communication: Communication service for module schemas.
|
|
462
|
+
resolved_tools: Cache of already resolved tools.
|
|
446
463
|
"""
|
|
447
464
|
for item in mapping.values():
|
|
448
465
|
if isinstance(item, ToolReference):
|
|
449
|
-
await cls._resolve_single_tool_reference(
|
|
466
|
+
await cls._resolve_single_tool_reference(
|
|
467
|
+
"dict_value",
|
|
468
|
+
item,
|
|
469
|
+
registry,
|
|
470
|
+
communication,
|
|
471
|
+
resolved_tools,
|
|
472
|
+
)
|
|
450
473
|
elif isinstance(item, BaseModel):
|
|
451
|
-
await cls._resolve_tool_references_recursive(
|
|
474
|
+
await cls._resolve_tool_references_recursive(
|
|
475
|
+
item,
|
|
476
|
+
registry,
|
|
477
|
+
communication,
|
|
478
|
+
resolved_tools,
|
|
479
|
+
)
|
|
452
480
|
|
|
453
481
|
def build_tool_cache(self) -> ToolCache:
|
|
454
|
-
"""Build tool cache from resolved ToolReferences
|
|
482
|
+
"""Build tool cache from resolved ToolReferences.
|
|
455
483
|
|
|
456
484
|
Returns:
|
|
457
485
|
ToolCache with field names as keys and ToolModuleInfo as values.
|
|
458
486
|
"""
|
|
459
|
-
logger.info("Building tool cache")
|
|
460
487
|
cache = ToolCache()
|
|
461
488
|
self._build_tool_cache_recursive(self, cache)
|
|
462
489
|
logger.info("Tool cache built: %d entries", len(cache.entries))
|
|
463
490
|
return cache
|
|
464
491
|
|
|
465
|
-
def _build_tool_cache_recursive(self, model_instance: BaseModel, cache: ToolCache) -> None:
|
|
466
|
-
"""Recursively build tool cache
|
|
492
|
+
def _build_tool_cache_recursive(self, model_instance: BaseModel, cache: ToolCache) -> None:
|
|
493
|
+
"""Recursively build tool cache from ToolReferences.
|
|
467
494
|
|
|
468
495
|
Args:
|
|
469
496
|
model_instance: Model instance to process.
|
|
470
497
|
cache: ToolCache to populate.
|
|
471
498
|
"""
|
|
472
|
-
for
|
|
499
|
+
for field_value in model_instance.__dict__.values():
|
|
473
500
|
if field_value is None:
|
|
474
501
|
continue
|
|
475
502
|
if isinstance(field_value, ToolReference):
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
cached_info = getattr(model_instance, cache_field_name, None)
|
|
479
|
-
module_info = field_value.tool_module_info or cached_info
|
|
503
|
+
module_info = self.resolved_tools.get(field_value.slug or "") or field_value.tool_module_info
|
|
480
504
|
if module_info:
|
|
481
|
-
|
|
482
|
-
setattr(model_instance, cache_field_name, module_info)
|
|
505
|
+
self.resolved_tools[module_info.setup_id] = module_info
|
|
483
506
|
cache.add(module_info.module_id, module_info)
|
|
484
|
-
logger.debug("Added tool to cache: %s", module_info.module_id)
|
|
485
507
|
elif isinstance(field_value, BaseModel):
|
|
486
508
|
self._build_tool_cache_recursive(field_value, cache)
|
|
487
509
|
elif isinstance(field_value, list):
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
510
|
+
self._process_list_items(field_value, cache)
|
|
511
|
+
elif isinstance(field_value, dict):
|
|
512
|
+
self._process_dict_values(field_value, cache)
|
|
513
|
+
|
|
514
|
+
def _process_list_items(self, items: list, cache: ToolCache) -> None:
|
|
515
|
+
"""Process list items for ToolReferences.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
items: List to process.
|
|
519
|
+
cache: ToolCache to populate.
|
|
520
|
+
"""
|
|
521
|
+
for item in items:
|
|
522
|
+
if isinstance(item, ToolReference):
|
|
523
|
+
module_info = self.resolved_tools.get(item.slug or "") or item.tool_module_info
|
|
524
|
+
if module_info:
|
|
525
|
+
self.resolved_tools[module_info.setup_id] = module_info
|
|
526
|
+
cache.add(module_info.module_id, module_info)
|
|
527
|
+
elif isinstance(item, BaseModel):
|
|
528
|
+
self._build_tool_cache_recursive(item, cache)
|
|
529
|
+
|
|
530
|
+
def _process_dict_values(self, mapping: dict, cache: ToolCache) -> None:
|
|
531
|
+
"""Process dict values for ToolReferences.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
mapping: Dict to process.
|
|
535
|
+
cache: ToolCache to populate.
|
|
536
|
+
"""
|
|
537
|
+
for item in mapping.values():
|
|
538
|
+
if isinstance(item, ToolReference):
|
|
539
|
+
module_info = self.resolved_tools.get(item.slug or "") or item.tool_module_info
|
|
540
|
+
if module_info:
|
|
541
|
+
self.resolved_tools[module_info.setup_id] = module_info
|
|
542
|
+
cache.add(module_info.module_id, module_info)
|
|
543
|
+
elif isinstance(item, BaseModel):
|
|
544
|
+
self._build_tool_cache_recursive(item, cache)
|
|
@@ -52,6 +52,7 @@ class ToolModuleInfo(ModuleInfo):
|
|
|
52
52
|
"""Module info for tool modules."""
|
|
53
53
|
|
|
54
54
|
tools: list[ToolDefinition]
|
|
55
|
+
setup_id: str
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
class ToolCache(BaseModel):
|
|
@@ -59,22 +60,22 @@ class ToolCache(BaseModel):
|
|
|
59
60
|
|
|
60
61
|
entries: dict[str, ToolModuleInfo] = Field(default_factory=dict)
|
|
61
62
|
|
|
62
|
-
def add(self,
|
|
63
|
+
def add(self, setup_id: str, tool_module_info: ToolModuleInfo) -> None:
|
|
63
64
|
"""Add a tool to the cache.
|
|
64
65
|
|
|
65
66
|
Args:
|
|
66
|
-
|
|
67
|
+
setup_id: Field name from SetupModel used as cache key.
|
|
67
68
|
tool_module_info: Resolved tool module information.
|
|
68
69
|
"""
|
|
69
|
-
self.entries[
|
|
70
|
+
self.entries[setup_id] = tool_module_info
|
|
70
71
|
logger.debug(
|
|
71
72
|
"Tool cached",
|
|
72
|
-
extra={"
|
|
73
|
+
extra={"setup_id": setup_id, "module_id": tool_module_info.module_id},
|
|
73
74
|
)
|
|
74
75
|
|
|
75
76
|
async def get(
|
|
76
77
|
self,
|
|
77
|
-
|
|
78
|
+
setup_id: str,
|
|
78
79
|
*,
|
|
79
80
|
registry: RegistryStrategy | None = None,
|
|
80
81
|
communication: "CommunicationStrategy | None" = None,
|
|
@@ -82,28 +83,28 @@ class ToolCache(BaseModel):
|
|
|
82
83
|
"""Get a tool from cache, optionally querying registry on miss.
|
|
83
84
|
|
|
84
85
|
Args:
|
|
85
|
-
|
|
86
|
+
setup_id: Field name to look up.
|
|
86
87
|
registry: Optional registry to query on cache miss.
|
|
87
88
|
communication: Optional communication strategy for schema fetching.
|
|
88
89
|
|
|
89
90
|
Returns:
|
|
90
91
|
ToolModuleInfo if found, None otherwise.
|
|
91
92
|
"""
|
|
92
|
-
cached = self.entries.get(
|
|
93
|
+
cached = self.entries.get(setup_id)
|
|
93
94
|
if cached:
|
|
94
95
|
return cached
|
|
95
96
|
|
|
96
97
|
if registry and communication:
|
|
97
98
|
try:
|
|
98
|
-
setup_info = registry.get_setup(
|
|
99
|
+
setup_info = registry.get_setup(setup_id)
|
|
99
100
|
if setup_info and setup_info.module_id:
|
|
100
101
|
info = registry.discover_by_id(setup_info.module_id)
|
|
101
102
|
if info:
|
|
102
|
-
tool_info = await module_info_to_tool_module_info(info, communication)
|
|
103
|
-
self.add(
|
|
103
|
+
tool_info = await module_info_to_tool_module_info(info, setup_id, communication)
|
|
104
|
+
self.add(setup_id, tool_info)
|
|
104
105
|
return tool_info
|
|
105
106
|
except Exception:
|
|
106
|
-
logger.exception("Registry lookup failed", extra={"
|
|
107
|
+
logger.exception("Registry lookup failed", extra={"setup_id": setup_id})
|
|
107
108
|
|
|
108
109
|
return None
|
|
109
110
|
|
|
@@ -122,6 +123,7 @@ class ToolCache(BaseModel):
|
|
|
122
123
|
|
|
123
124
|
async def module_info_to_tool_module_info(
|
|
124
125
|
module_info: ModuleInfo,
|
|
126
|
+
setup_id: str,
|
|
125
127
|
communication: "CommunicationStrategy",
|
|
126
128
|
*,
|
|
127
129
|
llm_format: bool = True,
|
|
@@ -133,6 +135,7 @@ async def module_info_to_tool_module_info(
|
|
|
133
135
|
|
|
134
136
|
Args:
|
|
135
137
|
module_info: Module info from registry.
|
|
138
|
+
setup_id: Setup ID from tool configuration.
|
|
136
139
|
communication: Communication strategy for gRPC calls.
|
|
137
140
|
llm_format: Use LLM-friendly schema format.
|
|
138
141
|
|
|
@@ -161,6 +164,7 @@ async def module_info_to_tool_module_info(
|
|
|
161
164
|
documentation=module_info.documentation,
|
|
162
165
|
status=module_info.status,
|
|
163
166
|
tools=tools,
|
|
167
|
+
setup_id=setup_id,
|
|
164
168
|
)
|
|
165
169
|
|
|
166
170
|
|
|
@@ -115,7 +115,7 @@ class ToolReference(BaseModel):
|
|
|
115
115
|
self.config.module_id = setup.module_id
|
|
116
116
|
info = registry.discover_by_id(self.config.module_id)
|
|
117
117
|
if info:
|
|
118
|
-
tool_module_info = await module_info_to_tool_module_info(info, communication)
|
|
118
|
+
tool_module_info = await module_info_to_tool_module_info(info, self.config.setup_id, communication)
|
|
119
119
|
self._cached_info = tool_module_info
|
|
120
120
|
return tool_module_info
|
|
121
121
|
|
|
@@ -126,7 +126,9 @@ class ToolReference(BaseModel):
|
|
|
126
126
|
organization_id=self.config.organization_id,
|
|
127
127
|
)
|
|
128
128
|
if results:
|
|
129
|
-
tool_module_info = await module_info_to_tool_module_info(
|
|
129
|
+
tool_module_info = await module_info_to_tool_module_info(
|
|
130
|
+
results[0], self.config.setup_id, communication
|
|
131
|
+
)
|
|
130
132
|
self._cached_info = tool_module_info
|
|
131
133
|
self.config.module_id = tool_module_info.module_id
|
|
132
134
|
return tool_module_info
|
|
@@ -556,21 +556,7 @@ class BaseModule( # noqa: PLR0904
|
|
|
556
556
|
await self._resolve_tools(config_setup_data)
|
|
557
557
|
updated_config = await self.run_config_setup(self.context, config_setup_data)
|
|
558
558
|
|
|
559
|
-
|
|
560
|
-
wrapper = config_setup_data.model_dump()
|
|
561
|
-
wrapper["content"] = updated_config.model_dump()
|
|
562
|
-
|
|
563
|
-
# Debug logging
|
|
564
|
-
content = wrapper.get("content", {})
|
|
565
|
-
logger.info(
|
|
566
|
-
"Config setup wrapper: keys=%s, content_keys=%s, tools_cache=%s",
|
|
567
|
-
list(wrapper.keys()),
|
|
568
|
-
list(content.keys()) if isinstance(content, dict) else "N/A",
|
|
569
|
-
content.get("tools_cache") if isinstance(content, dict) else "N/A",
|
|
570
|
-
extra=self.context.session.current_ids(),
|
|
571
|
-
)
|
|
572
|
-
|
|
573
|
-
setup_model = await self.create_setup_model(wrapper)
|
|
559
|
+
setup_model = await self.create_setup_model(updated_config.model_dump())
|
|
574
560
|
await callback(setup_model)
|
|
575
561
|
self._status = ModuleStatus.STOPPING
|
|
576
562
|
except Exception:
|
digitalkin/utils/__init__.py
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
"""General utils folder."""
|
|
2
2
|
|
|
3
|
+
from digitalkin.utils.conditional_schema import (
|
|
4
|
+
Conditional,
|
|
5
|
+
ConditionalField,
|
|
6
|
+
ConditionalSchemaMixin,
|
|
7
|
+
get_conditional_metadata,
|
|
8
|
+
has_conditional,
|
|
9
|
+
)
|
|
3
10
|
from digitalkin.utils.dynamic_schema import (
|
|
4
11
|
DEFAULT_TIMEOUT,
|
|
5
12
|
Dynamic,
|
|
@@ -14,13 +21,20 @@ from digitalkin.utils.dynamic_schema import (
|
|
|
14
21
|
)
|
|
15
22
|
|
|
16
23
|
__all__ = [
|
|
24
|
+
# Dynamic schema
|
|
17
25
|
"DEFAULT_TIMEOUT",
|
|
26
|
+
# Conditional schema
|
|
27
|
+
"Conditional",
|
|
28
|
+
"ConditionalField",
|
|
29
|
+
"ConditionalSchemaMixin",
|
|
18
30
|
"Dynamic",
|
|
19
31
|
"DynamicField",
|
|
20
32
|
"Fetcher",
|
|
21
33
|
"ResolveResult",
|
|
34
|
+
"get_conditional_metadata",
|
|
22
35
|
"get_dynamic_metadata",
|
|
23
36
|
"get_fetchers",
|
|
37
|
+
"has_conditional",
|
|
24
38
|
"has_dynamic",
|
|
25
39
|
"resolve",
|
|
26
40
|
"resolve_safe",
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Conditional field visibility for react-jsonschema-form.
|
|
2
|
+
|
|
3
|
+
This module provides a clean way to mark fields as conditional using Annotated metadata,
|
|
4
|
+
generating JSON Schema with if/then clauses for react-jsonschema-form.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
from typing import Annotated, Literal
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
from digitalkin.utils import Conditional, ConditionalSchemaMixin
|
|
10
|
+
|
|
11
|
+
class Tools(ConditionalSchemaMixin, BaseModel):
|
|
12
|
+
web_search_enabled: bool = Field(...)
|
|
13
|
+
|
|
14
|
+
web_search_engine: Annotated[
|
|
15
|
+
Literal["duckduckgo", "tavily"],
|
|
16
|
+
Conditional(trigger="web_search_enabled", show_when=True),
|
|
17
|
+
] = Field(...)
|
|
18
|
+
|
|
19
|
+
See Also:
|
|
20
|
+
- Documentation: docs/api/conditional_schema.md
|
|
21
|
+
- Tests: tests/utils/test_conditional_schema.py
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
28
|
+
|
|
29
|
+
from pydantic import BaseModel
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from pydantic.annotated_handlers import GetJsonSchemaHandler
|
|
33
|
+
from pydantic.fields import FieldInfo
|
|
34
|
+
from pydantic.json_schema import JsonSchemaValue
|
|
35
|
+
from pydantic_core.core_schema import CoreSchema
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ConditionalField:
|
|
40
|
+
"""Metadata for conditional field visibility.
|
|
41
|
+
|
|
42
|
+
Use with typing.Annotated to mark fields that should only appear
|
|
43
|
+
when a trigger field has a specific value.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
trigger: Name of the field that controls visibility.
|
|
47
|
+
show_when: Value(s) that trigger field must have to show this field.
|
|
48
|
+
Can be a boolean, string, or list of strings for multiple values.
|
|
49
|
+
required_when_shown: Whether field is required when visible. Defaults to True.
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
# Boolean condition
|
|
53
|
+
web_search_engine: Annotated[
|
|
54
|
+
str,
|
|
55
|
+
Conditional(trigger="web_search_enabled", show_when=True),
|
|
56
|
+
] = Field(...)
|
|
57
|
+
|
|
58
|
+
# Enum condition
|
|
59
|
+
advanced_option: Annotated[
|
|
60
|
+
str,
|
|
61
|
+
Conditional(trigger="mode", show_when="advanced"),
|
|
62
|
+
] = Field(...)
|
|
63
|
+
|
|
64
|
+
# Multiple values condition
|
|
65
|
+
shared_feature: Annotated[
|
|
66
|
+
bool,
|
|
67
|
+
Conditional(trigger="mode", show_when=["standard", "advanced"]),
|
|
68
|
+
] = Field(...)
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
trigger: str
|
|
72
|
+
show_when: bool | str | list[str]
|
|
73
|
+
required_when_shown: bool = True
|
|
74
|
+
|
|
75
|
+
def __post_init__(self) -> None:
|
|
76
|
+
"""Normalize single-item lists to scalar values."""
|
|
77
|
+
if isinstance(self.show_when, list) and len(self.show_when) == 1:
|
|
78
|
+
self.show_when = self.show_when[0]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Short alias for cleaner API
|
|
82
|
+
Conditional = ConditionalField
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_conditional_metadata(field_info: FieldInfo) -> ConditionalField | None:
|
|
86
|
+
"""Extract ConditionalField from field metadata.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
field_info: The Pydantic FieldInfo object to inspect.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The ConditionalField metadata instance if found, None otherwise.
|
|
93
|
+
"""
|
|
94
|
+
for meta in field_info.metadata:
|
|
95
|
+
if isinstance(meta, ConditionalField):
|
|
96
|
+
return meta
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def has_conditional(field_info: FieldInfo) -> bool:
|
|
101
|
+
"""Check if field has ConditionalField metadata.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
field_info: The Pydantic FieldInfo object to check.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
True if the field has ConditionalField metadata, False otherwise.
|
|
108
|
+
"""
|
|
109
|
+
return get_conditional_metadata(field_info) is not None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _collect_conditions(
|
|
113
|
+
model_fields: dict[str, FieldInfo],
|
|
114
|
+
props: dict[str, Any],
|
|
115
|
+
) -> tuple[dict[tuple[str, Any], list[tuple[str, bool]]], set[str]]:
|
|
116
|
+
"""Collect conditional fields grouped by trigger and show_when value.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
model_fields: The model's field definitions.
|
|
120
|
+
props: The schema properties dict.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Tuple of (conditions dict, fields to remove set).
|
|
124
|
+
"""
|
|
125
|
+
conditions: dict[tuple[str, Any], list[tuple[str, bool]]] = {}
|
|
126
|
+
fields_to_remove: set[str] = set()
|
|
127
|
+
|
|
128
|
+
for field_name, field_info in model_fields.items():
|
|
129
|
+
cond = get_conditional_metadata(field_info)
|
|
130
|
+
if cond is None or field_name not in props:
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
show_key = tuple(cond.show_when) if isinstance(cond.show_when, list) else cond.show_when
|
|
134
|
+
key = (cond.trigger, show_key)
|
|
135
|
+
|
|
136
|
+
if key not in conditions:
|
|
137
|
+
conditions[key] = []
|
|
138
|
+
conditions[key].append((field_name, cond.required_when_shown))
|
|
139
|
+
fields_to_remove.add(field_name)
|
|
140
|
+
|
|
141
|
+
return conditions, fields_to_remove
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _build_if_clause(trigger: str, *, show_when: bool | str | tuple[str, ...]) -> dict[str, Any]:
|
|
145
|
+
"""Build the if clause for a conditional.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
trigger: The trigger field name.
|
|
149
|
+
show_when: The value(s) that trigger visibility.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
The if clause dict.
|
|
153
|
+
"""
|
|
154
|
+
if isinstance(show_when, tuple):
|
|
155
|
+
return {"properties": {trigger: {"enum": list(show_when)}}, "required": [trigger]}
|
|
156
|
+
return {"properties": {trigger: {"const": show_when}}, "required": [trigger]}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _resolve_field_schema(
|
|
160
|
+
field_schema: dict[str, Any],
|
|
161
|
+
handler: GetJsonSchemaHandler,
|
|
162
|
+
) -> dict[str, Any]:
|
|
163
|
+
"""Resolve $ref in field schema if present.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
field_schema: The field's schema dict.
|
|
167
|
+
handler: The JSON schema handler for resolving refs.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
The resolved schema dict.
|
|
171
|
+
"""
|
|
172
|
+
if "$ref" not in field_schema:
|
|
173
|
+
return field_schema
|
|
174
|
+
|
|
175
|
+
resolved = handler.resolve_ref_schema(field_schema)
|
|
176
|
+
extra = {k: v for k, v in field_schema.items() if k != "$ref"}
|
|
177
|
+
return {**resolved, **extra}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class ConditionalSchemaMixin(BaseModel):
|
|
181
|
+
"""Mixin for automatic conditional field processing in JSON schema.
|
|
182
|
+
|
|
183
|
+
Inherit from this mixin to automatically generate JSON Schema with
|
|
184
|
+
if/then clauses for fields marked with ConditionalField metadata.
|
|
185
|
+
|
|
186
|
+
The mixin processes Annotated fields with Conditional metadata and:
|
|
187
|
+
1. Removes conditional fields from main properties
|
|
188
|
+
2. Adds them to allOf with if/then clauses
|
|
189
|
+
3. Groups multiple fields with the same condition together
|
|
190
|
+
|
|
191
|
+
Example:
|
|
192
|
+
class Config(ConditionalSchemaMixin, BaseModel):
|
|
193
|
+
mode: Literal["basic", "advanced"] = Field(...)
|
|
194
|
+
|
|
195
|
+
advanced_option: Annotated[
|
|
196
|
+
str,
|
|
197
|
+
Conditional(trigger="mode", show_when="advanced"),
|
|
198
|
+
] = Field(...)
|
|
199
|
+
|
|
200
|
+
# Generates schema with:
|
|
201
|
+
# {
|
|
202
|
+
# "properties": {"mode": {...}},
|
|
203
|
+
# "allOf": [{
|
|
204
|
+
# "if": {"properties": {"mode": {"const": "advanced"}}},
|
|
205
|
+
# "then": {"properties": {"advanced_option": {...}}}
|
|
206
|
+
# }]
|
|
207
|
+
# }
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
model_fields: ClassVar[dict[str, FieldInfo]] # type: ignore[misc]
|
|
211
|
+
|
|
212
|
+
@classmethod
|
|
213
|
+
def __get_pydantic_json_schema__( # noqa: PLW3201
|
|
214
|
+
cls,
|
|
215
|
+
core_schema: CoreSchema,
|
|
216
|
+
handler: GetJsonSchemaHandler,
|
|
217
|
+
) -> JsonSchemaValue:
|
|
218
|
+
"""Generate JSON schema with conditional field handling.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
core_schema: The Pydantic core schema.
|
|
222
|
+
handler: The JSON schema handler for resolving refs.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
The JSON schema with if/then clauses for conditional fields.
|
|
226
|
+
"""
|
|
227
|
+
schema = handler(core_schema)
|
|
228
|
+
props = schema.get("properties", {})
|
|
229
|
+
if not props:
|
|
230
|
+
return schema
|
|
231
|
+
|
|
232
|
+
conditions, fields_to_remove = _collect_conditions(cls.model_fields, props)
|
|
233
|
+
if not conditions:
|
|
234
|
+
return schema
|
|
235
|
+
|
|
236
|
+
all_of = schema.setdefault("allOf", [])
|
|
237
|
+
|
|
238
|
+
for (trigger, show_when), field_list in conditions.items():
|
|
239
|
+
then_props: dict[str, Any] = {}
|
|
240
|
+
then_required: list[str] = []
|
|
241
|
+
|
|
242
|
+
for field_name, required in field_list:
|
|
243
|
+
then_props[field_name] = _resolve_field_schema(props[field_name], handler)
|
|
244
|
+
if required:
|
|
245
|
+
then_required.append(field_name)
|
|
246
|
+
|
|
247
|
+
if_clause = _build_if_clause(trigger, show_when=show_when)
|
|
248
|
+
then_clause: dict[str, Any] = {"properties": then_props}
|
|
249
|
+
if then_required:
|
|
250
|
+
then_clause["required"] = then_required
|
|
251
|
+
|
|
252
|
+
all_of.append({"if": if_clause, "then": then_clause})
|
|
253
|
+
|
|
254
|
+
for field_name in fields_to_remove:
|
|
255
|
+
del props[field_name]
|
|
256
|
+
|
|
257
|
+
if "required" in schema:
|
|
258
|
+
schema["required"] = [r for r in schema["required"] if r not in fields_to_remove]
|
|
259
|
+
|
|
260
|
+
return schema
|
|
@@ -55,6 +55,8 @@ class SchemaSplitter:
|
|
|
55
55
|
for item in value:
|
|
56
56
|
if isinstance(item, dict):
|
|
57
57
|
cls._extract_ui_properties(item, ui_target)
|
|
58
|
+
elif key in {"if", "then", "else"} and isinstance(value, dict):
|
|
59
|
+
cls._extract_ui_properties(value, ui_target)
|
|
58
60
|
|
|
59
61
|
@classmethod
|
|
60
62
|
def _process_object( # noqa: C901, PLR0912
|
|
@@ -107,11 +109,15 @@ class SchemaSplitter:
|
|
|
107
109
|
item_json: dict[str, Any] = {}
|
|
108
110
|
cls._strip_ui_properties(item, item_json)
|
|
109
111
|
json_target["allOf"].append(item_json)
|
|
112
|
+
# Extract UI properties from allOf item
|
|
113
|
+
cls._extract_ui_properties(item, ui_target)
|
|
110
114
|
else:
|
|
111
115
|
json_target["allOf"].append(item)
|
|
112
116
|
elif key in {"if", "then", "else"} and isinstance(value, dict):
|
|
113
117
|
json_target[key] = {}
|
|
114
118
|
cls._strip_ui_properties(value, json_target[key])
|
|
119
|
+
# Extract UI properties from conditional
|
|
120
|
+
cls._extract_ui_properties(value, ui_target)
|
|
115
121
|
else:
|
|
116
122
|
json_target[key] = value
|
|
117
123
|
|
|
@@ -7,7 +7,7 @@ base_server/mock/__init__.py,sha256=YZFT-F1l_TpvJYuIPX-7kTeE1CfOjhx9YmNRXVoi-jQ,
|
|
|
7
7
|
base_server/mock/mock_pb2.py,sha256=sETakcS3PAAm4E-hTCV1jIVaQTPEAIoVVHupB8Z_k7Y,1843
|
|
8
8
|
base_server/mock/mock_pb2_grpc.py,sha256=BbOT70H6q3laKgkHfOx1QdfmCS_HxCY4wCOX84YAdG4,3180
|
|
9
9
|
digitalkin/__init__.py,sha256=7LLBAba0th-3SGqcpqFO-lopWdUkVLKzLZiMtB-mW3M,162
|
|
10
|
-
digitalkin/__version__.py,sha256=
|
|
10
|
+
digitalkin/__version__.py,sha256=udtzuXsaIao_ZAPzuFlc1q_JtSznP6hI13v4Zl8o1Zo,196
|
|
11
11
|
digitalkin/logger.py,sha256=8ze_tjt2G6mDTuQcsf7-UTXWP3UHZ7LZVSs_iqF4rX4,4685
|
|
12
12
|
digitalkin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
digitalkin/core/__init__.py,sha256=FJRcJ-B1Viyn-38L8XpOpZ8KOnf1I7PCDOAmKXLQhqc,71
|
|
@@ -28,7 +28,7 @@ digitalkin/core/task_manager/task_session.py,sha256=5jw21bT_SPXUzWE7tk6YG62EXqlR
|
|
|
28
28
|
digitalkin/grpc_servers/__init__.py,sha256=ZIRMJ1Lcas8yQ106GCup6hn2UBOsx1sNk8ap0lpEDnY,72
|
|
29
29
|
digitalkin/grpc_servers/_base_server.py,sha256=ZVeCDwI7w7fFbPTXPkeJb_SOuLfd2T7za3T4oCu2UWY,18680
|
|
30
30
|
digitalkin/grpc_servers/module_server.py,sha256=Ec3izzV2YpdN8rGs_cX-iVulQ00FkLR5dBflHlQ8a6Y,7849
|
|
31
|
-
digitalkin/grpc_servers/module_servicer.py,sha256=
|
|
31
|
+
digitalkin/grpc_servers/module_servicer.py,sha256=KsWXPwCQ0s2pygn0cq89KndJleAkjvkvLFs1Z2fWQIk,21705
|
|
32
32
|
digitalkin/grpc_servers/utils/__init__.py,sha256=ZnAIb_F8z4NhtPypqkdmzgRSzolKnJTk3oZx5GfWH5Y,38
|
|
33
33
|
digitalkin/grpc_servers/utils/exceptions.py,sha256=LtaDtlqXCeT6iqApogs4pbtezotOVeg4fhnFzGBvFsY,692
|
|
34
34
|
digitalkin/grpc_servers/utils/grpc_client_wrapper.py,sha256=nGG8QdKnBH0UG9qbKrlPwIvcvPgW3osw7O3cImxisPE,3279
|
|
@@ -53,18 +53,18 @@ digitalkin/models/grpc_servers/types.py,sha256=rQ78s4nAet2jy-NIDj_PUWriT0kuGHr_w
|
|
|
53
53
|
digitalkin/models/module/__init__.py,sha256=e2a_AUmobkpyITQKvMkDaDxvb-GOMHhDF9fn0q5_EnQ,959
|
|
54
54
|
digitalkin/models/module/base_types.py,sha256=oIylVNqo0idTFj4dRgCt7P19daNZ-AlvgCPpL9TJvto,1850
|
|
55
55
|
digitalkin/models/module/module.py,sha256=k0W8vfJJFth8XdDzkHm32SyTuSf3h2qF0hSrxAfGF1s,956
|
|
56
|
-
digitalkin/models/module/module_context.py,sha256=
|
|
56
|
+
digitalkin/models/module/module_context.py,sha256=QDdjZdhIJpvU_2Tn7kkJsZ1givB4dM1-ksopdF4VySw,12176
|
|
57
57
|
digitalkin/models/module/module_types.py,sha256=C9azCNBk76xMa-Mww8_6AiwQR8MLAsEyUOvBYxytovI,739
|
|
58
|
-
digitalkin/models/module/setup_types.py,sha256=
|
|
59
|
-
digitalkin/models/module/tool_cache.py,sha256=
|
|
60
|
-
digitalkin/models/module/tool_reference.py,sha256=
|
|
58
|
+
digitalkin/models/module/setup_types.py,sha256=Xb_KZ5vKcpLqkbPdQfooqtI6TFp2cklvdXLUBa55c30,19346
|
|
59
|
+
digitalkin/models/module/tool_cache.py,sha256=5e30A_GxT2W-w1LZFmVUqOxDjPcrZ8s_eW7p9impO64,7153
|
|
60
|
+
digitalkin/models/module/tool_reference.py,sha256=eIWJrT6syyEaXAWRXIlWYTst-j0XuvtU_va9m3tj_KU,4470
|
|
61
61
|
digitalkin/models/module/utility.py,sha256=gnbYfWpXGbomUI0fWf7T-Qm_VvT-LXDv1OuA9zObwVg,5589
|
|
62
62
|
digitalkin/models/services/__init__.py,sha256=jhfVw6egq0OcHmos_fypH9XFehbHTBw09wluVFVFEyw,226
|
|
63
63
|
digitalkin/models/services/cost.py,sha256=9PXvd5RrIk9vCrRjcUGQ9ZyAokEbwLg4s0RfnE-aLP4,1616
|
|
64
64
|
digitalkin/models/services/registry.py,sha256=mFehnPAVLGimodHquNrltXbH_aE0jEa-PxfyNm6J38E,1828
|
|
65
65
|
digitalkin/models/services/storage.py,sha256=wp7F-AvTsU46ujGPcguqM5kUKRZx4399D4EGAAJt2zs,1143
|
|
66
66
|
digitalkin/modules/__init__.py,sha256=vTQk8DWopxQSJ17BjE5dNhq247Rou55iQLJdBxoPUmo,296
|
|
67
|
-
digitalkin/modules/_base_module.py,sha256=
|
|
67
|
+
digitalkin/modules/_base_module.py,sha256=0XC0aQAxlNfvz0KK9ut7K0JbZql3cZMU4aeg7ISEsD0,21971
|
|
68
68
|
digitalkin/modules/archetype_module.py,sha256=XC9tl1Yr6QlbPn_x0eov6UUZwQgwW--BYPPMYVJH_NU,505
|
|
69
69
|
digitalkin/modules/tool_module.py,sha256=GBis7bKCkvWFCYLRvaS9oZVmLBBve1w8BhVnKOU2sCc,506
|
|
70
70
|
digitalkin/modules/trigger_handler.py,sha256=qPNMi-8NHqscOxciHeaXtpwjXApT3YzjMF23zQAjaZY,1770
|
|
@@ -115,14 +115,15 @@ digitalkin/services/user_profile/__init__.py,sha256=RKEZCsgCHS7fmswhWgUoQd6vZ_1p
|
|
|
115
115
|
digitalkin/services/user_profile/default_user_profile.py,sha256=46DH_VBCHKXJVyagVcc8kH5sLwRK54Fe_0ahqYJ1maA,1847
|
|
116
116
|
digitalkin/services/user_profile/grpc_user_profile.py,sha256=xDiUC5Ceofa6QtGPmqJV3ik5j8HDHc1zxtpia49rlRw,2780
|
|
117
117
|
digitalkin/services/user_profile/user_profile_strategy.py,sha256=CH8kT__1MUwA21k5djjmB5ZZ6pYg57OWbe_7owBCgwU,681
|
|
118
|
-
digitalkin/utils/__init__.py,sha256=
|
|
118
|
+
digitalkin/utils/__init__.py,sha256=_RXXALotIr_JToSQdhwDi3eizghKz9KrQtvC8Um80H8,808
|
|
119
119
|
digitalkin/utils/arg_parser.py,sha256=wzscRlE1Qp1gGl-lAJlkkwnbU1O2oezj6BwK_BZFBIk,3158
|
|
120
|
+
digitalkin/utils/conditional_schema.py,sha256=ZyGvY-DWkM_d0OuLElUpqnvUeDgZ2hXX3dFnKXbgL5k,8373
|
|
120
121
|
digitalkin/utils/development_mode_action.py,sha256=2hznh0ajW_4ZTysfoc0Y49161f_PQPATRgNk8NAn1_o,1623
|
|
121
122
|
digitalkin/utils/dynamic_schema.py,sha256=y5csxjuqVHjWDpnTUzxbcUuI_wou9-ibRVHQlBs_btY,15275
|
|
122
123
|
digitalkin/utils/llm_ready_schema.py,sha256=JjMug_lrQllqFoanaC091VgOqwAd-_YzcpqFlS7p778,2375
|
|
123
124
|
digitalkin/utils/package_discover.py,sha256=sa6Zp5Kape1Zr4iYiNrnZxiHDnqM06ODk6yfWHom53w,13465
|
|
124
|
-
digitalkin/utils/schema_splitter.py,sha256=
|
|
125
|
-
digitalkin-0.3.2.
|
|
125
|
+
digitalkin/utils/schema_splitter.py,sha256=9PHC-bvEDQudyYZNgXyjFtp7EJlmw4C_gPCJ-JmGDk0,9704
|
|
126
|
+
digitalkin-0.3.2.dev19.dist-info/licenses/LICENSE,sha256=Ies4HFv2r2hzDRakJYxk3Y60uDFLiG-orIgeTpstnIo,20327
|
|
126
127
|
modules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
127
128
|
modules/archetype_with_tools_module.py,sha256=PXTS6IXmC_OjxTmVrL_pYVI0MKwXjD5I1UJO_2xa10Q,7632
|
|
128
129
|
modules/cpu_intensive_module.py,sha256=GZlirQDZdYuXrI46sv1q4RNAHZjL4EptHVQTvgK9zz8,8363
|
|
@@ -137,7 +138,7 @@ monitoring/digitalkin_observability/prometheus.py,sha256=gDmM9ySaVwPAe7Yg84pLxmE
|
|
|
137
138
|
monitoring/tests/test_metrics.py,sha256=ugnYfAwqBPO6zA8z4afKTlyBWECTivacYSN-URQCn2E,5856
|
|
138
139
|
services/filesystem_module.py,sha256=U4dgqtuDadaXz8PJ1d_uQ_1EPncBqudAQCLUICF9yL4,7421
|
|
139
140
|
services/storage_module.py,sha256=Wz2MzLvqs2D_bnBBgtnujYcAKK2V2KFMk8K21RoepSE,6972
|
|
140
|
-
digitalkin-0.3.2.
|
|
141
|
-
digitalkin-0.3.2.
|
|
142
|
-
digitalkin-0.3.2.
|
|
143
|
-
digitalkin-0.3.2.
|
|
141
|
+
digitalkin-0.3.2.dev19.dist-info/METADATA,sha256=HBJG5Mpck63wVwh73EffNv54xKZpapsRcSgOsu2izKs,29725
|
|
142
|
+
digitalkin-0.3.2.dev19.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
143
|
+
digitalkin-0.3.2.dev19.dist-info/top_level.txt,sha256=AYVIesKrO0jnedQ-Muog9JBehG81WeTCNeOFoJgwsgE,51
|
|
144
|
+
digitalkin-0.3.2.dev19.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|