digitalkin 0.3.2.dev20__py3-none-any.whl → 0.3.2.dev22__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 CHANGED
@@ -5,4 +5,4 @@ from importlib.metadata import PackageNotFoundError, version
5
5
  try:
6
6
  __version__ = version("digitalkin")
7
7
  except PackageNotFoundError:
8
- __version__ = "0.3.2.dev20"
8
+ __version__ = "0.3.2.dev22"
@@ -165,7 +165,7 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
165
165
  if isinstance(updated_setup_data, ModuleCodeModel):
166
166
  logger.error(
167
167
  "Config setup failed",
168
- extra={"job_id": job_id, "code": updated_setup_data.code, "message": updated_setup_data.message},
168
+ extra={"job_id": job_id, "code": updated_setup_data.code, "error_message": updated_setup_data.message},
169
169
  )
170
170
  context.set_code(grpc.StatusCode.INTERNAL)
171
171
  context.set_details(updated_setup_data.message or "Config setup failed")
@@ -178,7 +178,7 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
178
178
  extra={
179
179
  "job_id": job_id,
180
180
  "code": updated_setup_data["code"],
181
- "message": updated_setup_data.get("message"),
181
+ "error_message": updated_setup_data.get("message"),
182
182
  },
183
183
  )
184
184
  context.set_code(grpc.StatusCode.INTERNAL)
@@ -14,8 +14,7 @@ from digitalkin.models.module.tool_cache import (
14
14
  )
15
15
  from digitalkin.models.module.tool_reference import (
16
16
  ToolReference,
17
- ToolReferenceConfig,
18
- ToolSelectionMode,
17
+ ToolReferenceInput,
19
18
  )
20
19
  from digitalkin.models.module.utility import (
21
20
  EndOfStreamOutput,
@@ -36,8 +35,7 @@ __all__ = [
36
35
  "ToolModuleInfo",
37
36
  "ToolParameter",
38
37
  "ToolReference",
39
- "ToolReferenceConfig",
40
- "ToolSelectionMode",
38
+ "ToolReferenceInput",
41
39
  "UtilityProtocol",
42
40
  "UtilityRegistry",
43
41
  ]
@@ -227,21 +227,19 @@ class ModuleContext:
227
227
  llm_format=llm_format,
228
228
  )
229
229
 
230
- async def create_openai_style_tools(self, module_id: str) -> list[dict[str, Any]]:
230
+ def create_openai_style_tools(self, slug: 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
- module_id: Module ID to look up (checks cache first, then registry).
237
+ slug: 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
- tool_module_info = await self.tool_cache.get(
243
- module_id, registry=self.registry, communication=self.communication
244
- )
242
+ tool_module_info = self.tool_cache.get(slug)
245
243
  if not tool_module_info:
246
244
  return []
247
245
 
@@ -250,8 +248,8 @@ class ModuleContext:
250
248
  "type": "function",
251
249
  "function": {
252
250
  "module_id": tool_module_info.module_id,
253
- "toolkit_name": tool_module_info.name or "undefined",
254
- "name": tool_def.name,
251
+ "toolkit_name": tool_module_info.tool_name or "undefined",
252
+ "name": tool_module_info.slug + "_" + tool_def.name,
255
253
  "description": tool_def.description,
256
254
  "parameters": ModuleContext._build_parameters_schema(tool_def.parameters),
257
255
  },
@@ -277,9 +275,9 @@ class ModuleContext:
277
275
 
278
276
  def create_tool_functions(
279
277
  self,
280
- module_id: str,
278
+ slug: str,
281
279
  ) -> list[tuple[ToolDefinition, Callable[..., AsyncGenerator[dict, None]]]]:
282
- """Create tool functions for all protocols in a tool module.
280
+ """Create tool functions for all protocols in a tool setup.
283
281
 
284
282
  Returns an async generator per ToolDefinition that calls the remote tool
285
283
  module via gRPC with the protocol auto-injected.
@@ -288,12 +286,12 @@ class ModuleContext:
288
286
  in sync contexts like __init__ methods.
289
287
 
290
288
  Args:
291
- module_id: Module ID to look up in cache.
289
+ slug: Setup ID to look up in cache.
292
290
 
293
291
  Returns:
294
292
  List of (ToolDefinition, async_generator_function) tuples. Empty if not found.
295
293
  """
296
- tool_module_info = self.tool_cache.entries.get(module_id)
294
+ tool_module_info = self.tool_cache.entries.get(slug)
297
295
  if not tool_module_info:
298
296
  return []
299
297
 
@@ -29,7 +29,6 @@ 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")
33
32
  _clean_model_cache: ClassVar[dict[tuple[type, bool, bool], type]] = {}
34
33
  resolved_tools: dict[str, ToolModuleInfo] = Field(
35
34
  default_factory=dict,
@@ -347,7 +346,7 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
347
346
  resolved_tools: Cache of already resolved tools.
348
347
  """
349
348
  if isinstance(field_value, ToolReference):
350
- await cls._resolve_single_tool_reference(
349
+ await cls._resolve_tool_reference(
351
350
  field_name,
352
351
  field_value,
353
352
  registry,
@@ -377,7 +376,7 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
377
376
  )
378
377
 
379
378
  @classmethod
380
- async def _resolve_single_tool_reference(
379
+ async def _resolve_tool_reference(
381
380
  cls,
382
381
  field_name: str,
383
382
  tool_ref: ToolReference,
@@ -385,7 +384,7 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
385
384
  communication: "CommunicationStrategy",
386
385
  resolved_tools: dict[str, ToolModuleInfo],
387
386
  ) -> None:
388
- """Resolve a single ToolReference.
387
+ """Resolve a ToolReference (may contain multiple selected tools).
389
388
 
390
389
  Args:
391
390
  field_name: Name of the field for logging.
@@ -394,21 +393,23 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
394
393
  communication: Communication service for module schemas.
395
394
  resolved_tools: Cache of already resolved tools.
396
395
  """
397
- logger.info("Resolving ToolReference '%s' with setup_id='%s'", field_name, tool_ref.config.setup_id)
396
+ logger.info("Resolving ToolReference '%s' with %d selected tools", field_name, len(tool_ref.selected_tools))
398
397
 
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
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 = [tool for tool in tool_ref.selected_tools if tool.slug and tool.slug not in resolved_tools]
403
+
404
+ if not tools_to_resolve:
405
+ logger.info("All tools for '%s' already cached", field_name)
406
+ return
406
407
 
407
408
  try:
408
- info = await tool_ref.resolve(registry, communication)
409
- if info and info.setup_id:
409
+ infos = await tool_ref.resolve(registry, communication)
410
+ for info in infos:
410
411
  resolved_tools[info.setup_id] = info
411
- logger.info("Resolved ToolReference '%s' -> %s", field_name, tool_ref.tool_module_info)
412
+ logger.info("Resolved tool '%s' -> module_id=%s", info.setup_id, info.module_id)
412
413
  except Exception:
413
414
  logger.exception("Failed to resolve ToolReference '%s'", field_name)
414
415
 
@@ -430,7 +431,7 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
430
431
  """
431
432
  for item in items:
432
433
  if isinstance(item, ToolReference):
433
- await cls._resolve_single_tool_reference(
434
+ await cls._resolve_tool_reference(
434
435
  "list_item",
435
436
  item,
436
437
  registry,
@@ -463,7 +464,7 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
463
464
  """
464
465
  for item in mapping.values():
465
466
  if isinstance(item, ToolReference):
466
- await cls._resolve_single_tool_reference(
467
+ await cls._resolve_tool_reference(
467
468
  "dict_value",
468
469
  item,
469
470
  registry,
@@ -500,10 +501,10 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
500
501
  if field_value is None:
501
502
  continue
502
503
  if isinstance(field_value, ToolReference):
503
- module_info = self.resolved_tools.get(field_value.slug or "") or field_value.tool_module_info
504
- if module_info:
505
- self.resolved_tools[module_info.setup_id] = module_info
506
- cache.add(module_info.module_id, module_info)
504
+ for tool in field_value.selected_tools:
505
+ tool_module_info = self.resolved_tools.get(tool.slug or "")
506
+ if tool_module_info:
507
+ cache.add(tool_module_info)
507
508
  elif isinstance(field_value, BaseModel):
508
509
  self._build_tool_cache_recursive(field_value, cache)
509
510
  elif isinstance(field_value, list):
@@ -520,10 +521,10 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
520
521
  """
521
522
  for item in items:
522
523
  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)
524
+ for tool in item.selected_tools:
525
+ tool_module_info = self.resolved_tools.get(tool.slug or "")
526
+ if tool_module_info:
527
+ cache.add(tool_module_info)
527
528
  elif isinstance(item, BaseModel):
528
529
  self._build_tool_cache_recursive(item, cache)
529
530
 
@@ -536,9 +537,9 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
536
537
  """
537
538
  for item in mapping.values():
538
539
  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)
540
+ for tool in item.selected_tools:
541
+ tool_module_info = self.resolved_tools.get(tool.slug or "")
542
+ if tool_module_info:
543
+ cache.add(tool_module_info)
543
544
  elif isinstance(item, BaseModel):
544
545
  self._build_tool_cache_recursive(item, cache)
@@ -6,7 +6,16 @@ from pydantic import BaseModel, Field
6
6
 
7
7
  from digitalkin.logger import logger
8
8
  from digitalkin.models.services.registry import ModuleInfo
9
- from digitalkin.services.registry import RegistryStrategy
9
+
10
+
11
+ class SelectedTool(BaseModel):
12
+ """Selected tool information."""
13
+
14
+ setup_id: str = ""
15
+ module_id: str = ""
16
+ slug: str = ""
17
+ name: str = ""
18
+
10
19
 
11
20
  if TYPE_CHECKING:
12
21
  from digitalkin.services.communication import CommunicationStrategy
@@ -51,8 +60,14 @@ class ToolDefinition(BaseModel):
51
60
  class ToolModuleInfo(ModuleInfo):
52
61
  """Module info for tool modules."""
53
62
 
54
- tools: list[ToolDefinition]
55
- setup_id: str
63
+ tools: list[ToolDefinition] = Field(default_factory=list)
64
+ tool_name: str = ""
65
+ setup_id: str = ""
66
+
67
+ @property
68
+ def slug(self) -> str:
69
+ """Module ID."""
70
+ return self.setup_id + "_" + self.tool_name
56
71
 
57
72
 
58
73
  class ToolCache(BaseModel):
@@ -60,53 +75,35 @@ class ToolCache(BaseModel):
60
75
 
61
76
  entries: dict[str, ToolModuleInfo] = Field(default_factory=dict)
62
77
 
63
- def add(self, setup_id: str, tool_module_info: ToolModuleInfo) -> None:
78
+ def add(self, tool_module_info: ToolModuleInfo) -> None:
64
79
  """Add a tool to the cache.
65
80
 
66
81
  Args:
67
- setup_id: Field name from SetupModel used as cache key.
68
82
  tool_module_info: Resolved tool module information.
69
83
  """
70
- self.entries[setup_id] = tool_module_info
84
+ self.entries[tool_module_info.slug] = tool_module_info
71
85
  logger.debug(
72
86
  "Tool cached",
73
- extra={"setup_id": setup_id, "module_id": tool_module_info.module_id},
87
+ extra={
88
+ "slug": tool_module_info.slug,
89
+ "module_id": tool_module_info.module_id,
90
+ "setup_id": tool_module_info.setup_id,
91
+ },
74
92
  )
75
93
 
76
- async def get(
94
+ def get(
77
95
  self,
78
- setup_id: str,
79
- *,
80
- registry: RegistryStrategy | None = None,
81
- communication: "CommunicationStrategy | None" = None,
96
+ slug: str,
82
97
  ) -> ToolModuleInfo | None:
83
98
  """Get a tool from cache, optionally querying registry on miss.
84
99
 
85
100
  Args:
86
- setup_id: Field name to look up.
87
- registry: Optional registry to query on cache miss.
88
- communication: Optional communication strategy for schema fetching.
101
+ slug: Field name to look up.
89
102
 
90
103
  Returns:
91
104
  ToolModuleInfo if found, None otherwise.
92
105
  """
93
- cached = self.entries.get(setup_id)
94
- if cached:
95
- return cached
96
-
97
- if registry and communication:
98
- try:
99
- setup_info = registry.get_setup(setup_id)
100
- if setup_info and setup_info.module_id:
101
- info = registry.discover_by_id(setup_info.module_id)
102
- if info:
103
- tool_info = await module_info_to_tool_module_info(info, setup_id, communication)
104
- self.add(setup_id, tool_info)
105
- return tool_info
106
- except Exception:
107
- logger.exception("Registry lookup failed", extra={"setup_id": setup_id})
108
-
109
- return None
106
+ return self.entries.get(slug)
110
107
 
111
108
  def clear(self) -> None:
112
109
  """Clear all cache entries."""
@@ -123,7 +120,7 @@ class ToolCache(BaseModel):
123
120
 
124
121
  async def module_info_to_tool_module_info(
125
122
  module_info: ModuleInfo,
126
- setup_id: str,
123
+ tool: SelectedTool,
127
124
  communication: "CommunicationStrategy",
128
125
  *,
129
126
  llm_format: bool = True,
@@ -135,7 +132,7 @@ async def module_info_to_tool_module_info(
135
132
 
136
133
  Args:
137
134
  module_info: Module info from registry.
138
- setup_id: Setup ID from tool configuration.
135
+ tool: Selected tool information.
139
136
  communication: Communication strategy for gRPC calls.
140
137
  llm_format: Use LLM-friendly schema format.
141
138
 
@@ -160,11 +157,12 @@ async def module_info_to_tool_module_info(
160
157
  address=module_info.address,
161
158
  port=module_info.port,
162
159
  version=module_info.version,
163
- name=module_info.name,
160
+ module_name=module_info.module_name,
164
161
  documentation=module_info.documentation,
165
162
  status=module_info.status,
166
163
  tools=tools,
167
- setup_id=setup_id,
164
+ setup_id=tool.setup_id,
165
+ tool_name=tool.name,
168
166
  )
169
167
 
170
168
 
@@ -1,136 +1,103 @@
1
1
  """Tool reference types for module configuration."""
2
2
 
3
- from enum import Enum
3
+ from typing import Annotated
4
4
 
5
- from pydantic import BaseModel, Field, PrivateAttr, model_validator
5
+ from pydantic import BaseModel, BeforeValidator, Field
6
+ from pydantic.json_schema import GetJsonSchemaHandler, JsonSchemaValue
7
+ from pydantic_core import CoreSchema
6
8
 
7
- from digitalkin.models.module.tool_cache import ToolModuleInfo, module_info_to_tool_module_info
9
+ from digitalkin.models.module.tool_cache import (
10
+ SelectedTool,
11
+ ToolModuleInfo,
12
+ module_info_to_tool_module_info,
13
+ )
8
14
  from digitalkin.services.communication.communication_strategy import CommunicationStrategy
9
15
  from digitalkin.services.registry import RegistryStrategy
10
16
 
11
17
 
12
- class ToolSelectionMode(str, Enum):
13
- """Tool selection mode."""
14
-
15
- TAG = "tag"
16
- FIXED = "fixed"
17
- DISCOVERABLE = "discoverable"
18
-
19
-
20
- class ToolReferenceConfig(BaseModel):
21
- """Tool selection configuration. The module_id serves as both identifier and cache key."""
22
-
23
- mode: ToolSelectionMode = Field(default=ToolSelectionMode.FIXED)
24
- setup_id: str = Field(default="")
25
- module_id: str = Field(default="")
26
- tag: str = Field(default="")
27
- organization_id: str = Field(default="")
28
-
29
- @model_validator(mode="after")
30
- def validate_config(self) -> "ToolReferenceConfig":
31
- """Validate required fields based on mode.
32
-
33
- Returns:
34
- Self if validation passes.
35
-
36
- Raises:
37
- ValueError: If required field is missing for the mode.
38
- """
39
- if self.mode == ToolSelectionMode.FIXED and not self.setup_id:
40
- msg = "setup_id required when mode is FIXED"
41
- raise ValueError(msg)
42
- if self.mode == ToolSelectionMode.TAG and not self.tag:
43
- msg = "tag required when mode is TAG"
44
- raise ValueError(msg)
45
- return self
46
-
47
-
48
18
  class ToolReference(BaseModel):
49
- """Reference to a tool module, resolved via registry during config setup."""
50
-
51
- config: ToolReferenceConfig
52
- _cached_info: ToolModuleInfo | None = PrivateAttr(default=None)
19
+ """Tool selection configuration and reference.
20
+
21
+ The mode determines validation requirements and resolution behavior:
22
+ - FIXED: Requires setup_id, resolves to exact tool
23
+ - MODULE: Requires module_id, returns constraint for frontend selection
24
+ - TAG: Requires tag, returns constraint for frontend selection
25
+ - DISCOVERABLE: Optional module_id/tag constraints, returns constraint info
26
+ """
27
+
28
+ selected_tools: list[SelectedTool] = Field(default=[], description="Tools selected by the user.")
29
+ setup_ids: list[str] = Field(default=[], description="Setup IDs for the user to choose from.")
30
+ module_ids: list[str] = Field(default=[], description="Module IDs for the user to choose from.")
31
+ tags: list[str] = Field(default=[], description="Tags for the user to choose from.")
32
+ max_tools: int = Field(default=0, description="Maximum tools to select. 0 for unlimited.")
33
+
34
+ async def resolve(self, registry: RegistryStrategy, communication: CommunicationStrategy) -> list[ToolModuleInfo]:
35
+ """Resolve this reference using the registry.
53
36
 
54
- @property
55
- def slug(self) -> str:
56
- """Cache key (same as module_id).
37
+ Args:
38
+ registry: Registry service for module discovery.
39
+ communication: Communication service for module schemas.
57
40
 
58
41
  Returns:
59
- Module ID used as cache key.
42
+ List of ToolModuleInfo if resolved.
60
43
  """
61
- return self.config.setup_id
62
-
63
- @property
64
- def module_id(self) -> str:
65
- """Module identifier.
44
+ resolved: list[ToolModuleInfo] = []
45
+ for tool in self.selected_tools:
46
+ setup = registry.get_setup(tool.setup_id)
47
+ if setup and setup.module_id:
48
+ info = registry.discover_by_id(setup.module_id)
49
+ tool.slug = tool.setup_id
50
+ tool.module_id = setup.module_id
51
+ tool.name = setup.name
52
+ if info:
53
+ resolved.append(await module_info_to_tool_module_info(info, tool, communication))
66
54
 
67
- Returns:
68
- Module ID or empty string if not set.
69
- """
70
- return self.config.module_id
55
+ return resolved
71
56
 
72
- @property
73
- def setup_id(self) -> str:
74
- """Setup identifier.
75
57
 
76
- Returns:
77
- Setup ID or empty string if not set.
78
- """
79
- return self.config.setup_id
58
+ def _convert_to_tool_reference(v: object) -> "ToolReference | object":
59
+ """Convert list of setup IDs to ToolReference.
80
60
 
81
- @property
82
- def tool_module_info(self) -> ToolModuleInfo | None:
83
- """Resolved module information.
61
+ Args:
62
+ v: Input value, either a list of setup IDs or passthrough.
84
63
 
85
- Returns:
86
- ToolModuleInfo if resolved, None otherwise.
87
- """
88
- return self._cached_info
64
+ Returns:
65
+ ToolReference if input is list, otherwise original value.
66
+ """
67
+ if isinstance(v, list):
68
+ return ToolReference(selected_tools=[SelectedTool(setup_id=sid, slug=sid) for sid in v])
69
+ return v
89
70
 
90
- @property
91
- def is_resolved(self) -> bool:
92
- """Whether this reference has been resolved.
93
71
 
94
- Returns:
95
- True if resolved, False otherwise.
96
- """
97
- return self._cached_info is not None
72
+ class _ToolReferenceInputSchema:
73
+ """Custom JSON schema generator that wraps ToolReference in anyOf with array option."""
98
74
 
99
- async def resolve(self, registry: RegistryStrategy, communication: CommunicationStrategy) -> ToolModuleInfo | None:
100
- """Resolve this reference using the registry.
75
+ @staticmethod
76
+ def __get_pydantic_json_schema__( # noqa: PLW3201
77
+ schema: CoreSchema,
78
+ handler: GetJsonSchemaHandler,
79
+ ) -> JsonSchemaValue:
80
+ """Generate JSON schema accepting both list[str] and ToolReference.
101
81
 
102
82
  Args:
103
- registry: Registry service for module discovery.
104
- communication: Communication service for module schemas.
83
+ schema: The core schema from Pydantic.
84
+ handler: Handler to generate JSON schema from core schema.
105
85
 
106
86
  Returns:
107
- ToolModuleInfo if resolved, None for DISCOVERABLE mode or if not found.
87
+ JSON schema with anyOf accepting array or ToolReference.
108
88
  """
109
- if self.config.mode == ToolSelectionMode.DISCOVERABLE:
110
- return None
111
-
112
- if self.config.mode == ToolSelectionMode.FIXED and self.config.setup_id:
113
- setup = registry.get_setup(self.config.setup_id)
114
- if setup and setup.module_id:
115
- self.config.module_id = setup.module_id
116
- info = registry.discover_by_id(self.config.module_id)
117
- if info:
118
- tool_module_info = await module_info_to_tool_module_info(info, self.config.setup_id, communication)
119
- self._cached_info = tool_module_info
120
- return tool_module_info
121
-
122
- if self.config.mode == ToolSelectionMode.TAG and self.config.tag:
123
- results = registry.search(
124
- name=self.config.tag,
125
- module_type="tool",
126
- organization_id=self.config.organization_id,
127
- )
128
- if results:
129
- tool_module_info = await module_info_to_tool_module_info(
130
- results[0], self.config.setup_id, communication
131
- )
132
- self._cached_info = tool_module_info
133
- self.config.module_id = tool_module_info.module_id
134
- return tool_module_info
135
-
136
- return None
89
+ json_schema = handler(schema)
90
+ return {
91
+ "anyOf": [
92
+ {"type": "array", "items": {"type": "string"}},
93
+ json_schema,
94
+ ]
95
+ }
96
+
97
+
98
+ ToolReferenceInput = Annotated[
99
+ ToolReference,
100
+ BeforeValidator(_convert_to_tool_reference),
101
+ _ToolReferenceInputSchema,
102
+ ]
103
+ """Type alias for ToolReference fields that accept list[str] input from frontend."""
@@ -26,12 +26,12 @@ class RegistryModuleType(str, Enum):
26
26
  class ModuleInfo(BaseModel):
27
27
  """Module information from registry."""
28
28
 
29
- module_id: str
30
- module_type: RegistryModuleType
31
- address: str
32
- port: int
33
- version: str
34
- name: str = ""
29
+ module_id: str = ""
30
+ module_type: RegistryModuleType = RegistryModuleType.UNSPECIFIED
31
+ address: str = ""
32
+ port: int = 0
33
+ version: str = ""
34
+ module_name: str = ""
35
35
  documentation: str | None = None
36
36
  status: RegistryModuleStatus | None = None
37
37
 
@@ -74,7 +74,7 @@ class GrpcRegistry(RegistryStrategy, GrpcClientWrapper, GrpcErrorHandlerMixin):
74
74
  address=descriptor.address,
75
75
  port=descriptor.port,
76
76
  version=descriptor.version,
77
- name=descriptor.name,
77
+ module_name=descriptor.name,
78
78
  documentation=descriptor.documentation or None,
79
79
  )
80
80
 
@@ -17,24 +17,31 @@ class SchemaSplitter:
17
17
  Tuple of (jsonschema, uischema).
18
18
  """
19
19
  defs_ui: dict[str, dict[str, Any]] = {}
20
- if "$defs" in combined_schema:
21
- for def_name, def_value in combined_schema["$defs"].items():
20
+ schema_defs = combined_schema.get("$defs", {})
21
+ if schema_defs:
22
+ for def_name, def_value in schema_defs.items():
22
23
  if isinstance(def_value, dict):
23
24
  defs_ui[def_name] = {}
24
- cls._extract_ui_properties(def_value, defs_ui[def_name])
25
+ cls._extract_ui_properties(def_value, defs_ui[def_name], schema_defs)
25
26
 
26
27
  json_schema: dict[str, Any] = {}
27
28
  ui_schema: dict[str, Any] = {}
28
- cls._process_object(combined_schema, json_schema, ui_schema, defs_ui)
29
+ cls._process_object(combined_schema, json_schema, ui_schema, defs_ui, schema_defs)
29
30
  return json_schema, ui_schema
30
31
 
31
32
  @classmethod
32
- def _extract_ui_properties(cls, source: dict[str, Any], ui_target: dict[str, Any]) -> None: # noqa: C901
33
+ def _extract_ui_properties(
34
+ cls,
35
+ source: dict[str, Any],
36
+ ui_target: dict[str, Any],
37
+ defs: dict[str, Any] | None = None,
38
+ ) -> None:
33
39
  """Extract ui:* properties from source into ui_target recursively.
34
40
 
35
41
  Args:
36
42
  source: Source dict to extract from.
37
43
  ui_target: Target dict for ui properties.
44
+ defs: The $defs dictionary to resolve $ref references.
38
45
  """
39
46
  for key, value in source.items():
40
47
  if key.startswith("ui:"):
@@ -43,20 +50,26 @@ class SchemaSplitter:
43
50
  for prop_name, prop_value in value.items():
44
51
  if isinstance(prop_value, dict):
45
52
  prop_ui: dict[str, Any] = {}
46
- cls._extract_ui_properties(prop_value, prop_ui)
53
+ cls._extract_ui_properties(prop_value, prop_ui, defs)
47
54
  if prop_ui:
48
55
  ui_target[prop_name] = prop_ui
49
56
  elif key == "items" and isinstance(value, dict):
50
57
  items_ui: dict[str, Any] = {}
51
- cls._extract_ui_properties(value, items_ui)
58
+ cls._extract_ui_properties(value, items_ui, defs)
52
59
  if items_ui:
53
60
  ui_target["items"] = items_ui
54
61
  elif key == "allOf" and isinstance(value, list):
55
62
  for item in value:
56
63
  if isinstance(item, dict):
57
- cls._extract_ui_properties(item, ui_target)
64
+ cls._extract_ui_properties(item, ui_target, defs)
58
65
  elif key in {"if", "then", "else"} and isinstance(value, dict):
59
- cls._extract_ui_properties(value, ui_target)
66
+ cls._extract_ui_properties(value, ui_target, defs)
67
+ elif key == "$ref" and isinstance(value, str) and defs is not None:
68
+ # Resolve $ref and extract UI properties from the referenced definition
69
+ if value.startswith("#/$defs/"):
70
+ def_name = value[8:]
71
+ if def_name in defs:
72
+ cls._extract_ui_properties(defs[def_name], ui_target, defs)
60
73
 
61
74
  @classmethod
62
75
  def _process_object( # noqa: C901, PLR0912
@@ -65,6 +78,7 @@ class SchemaSplitter:
65
78
  json_target: dict[str, Any],
66
79
  ui_target: dict[str, Any],
67
80
  defs_ui: dict[str, dict[str, Any]],
81
+ schema_defs: dict[str, Any] | None = None,
68
82
  ) -> None:
69
83
  """Process an object, splitting json and ui properties.
70
84
 
@@ -73,6 +87,7 @@ class SchemaSplitter:
73
87
  json_target: Target dict for json schema.
74
88
  ui_target: Target dict for ui schema.
75
89
  defs_ui: Pre-extracted UI properties from $defs.
90
+ schema_defs: The original $defs dictionary to resolve $ref in _extract_ui_properties.
76
91
  """
77
92
  for key, value in source.items():
78
93
  if key.startswith("ui:"):
@@ -113,14 +128,14 @@ class SchemaSplitter:
113
128
  cls._strip_ui_properties(item, item_json)
114
129
  json_target["allOf"].append(item_json)
115
130
  # Extract UI properties from allOf item
116
- cls._extract_ui_properties(item, ui_target)
131
+ cls._extract_ui_properties(item, ui_target, schema_defs)
117
132
  else:
118
133
  json_target["allOf"].append(item)
119
134
  elif key in {"if", "then", "else"} and isinstance(value, dict):
120
135
  json_target[key] = {}
121
136
  cls._strip_ui_properties(value, json_target[key])
122
137
  # Extract UI properties from conditional
123
- cls._extract_ui_properties(value, ui_target)
138
+ cls._extract_ui_properties(value, ui_target, schema_defs)
124
139
  elif key == "hidden":
125
140
  # Strip hidden key from json schema
126
141
  continue
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: digitalkin
3
- Version: 0.3.2.dev20
3
+ Version: 0.3.2.dev22
4
4
  Summary: SDK to build kin used in DigitalKin
5
5
  Author-email: "DigitalKin.ai" <contact@digitalkin.ai>
6
6
  License: Attribution-NonCommercial-ShareAlike 4.0 International
@@ -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=dN3fPmuqWC0wGuJWF7p4H8rHGbT_gGfHvNrei8I6TU8,196
10
+ digitalkin/__version__.py,sha256=dZgyJRHQFh_03su8ZElZY21agbb3n1gwA2QNWlIpunk,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=KsWXPwCQ0s2pygn0cq89KndJleAkjvkvLFs1Z2fWQIk,21705
31
+ digitalkin/grpc_servers/module_servicer.py,sha256=yhS5zOxxOTh3aXNVZBbgomRtFa5k6Q7jm-PZh54Gus8,21717
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
@@ -50,18 +50,18 @@ digitalkin/models/core/task_monitor.py,sha256=CW-jydSgXMV464W0pqfar0HpgqlSxqdujm
50
50
  digitalkin/models/grpc_servers/__init__.py,sha256=0tA71nPSXgRrh9DoLvx-TSwZXdYIRUEItoadpTL1cTo,42
51
51
  digitalkin/models/grpc_servers/models.py,sha256=gRX94eL71a5mLIie-lCOwE7a0As_AuGduxPPzTHbAe4,13797
52
52
  digitalkin/models/grpc_servers/types.py,sha256=rQ78s4nAet2jy-NIDj_PUWriT0kuGHr_w6ELjmjgBao,539
53
- digitalkin/models/module/__init__.py,sha256=e2a_AUmobkpyITQKvMkDaDxvb-GOMHhDF9fn0q5_EnQ,959
53
+ digitalkin/models/module/__init__.py,sha256=XlFlLGmLUYPV4ZOgBxfGclq9wupGDXNUyWB4EB6iTXA,909
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=QDdjZdhIJpvU_2Tn7kkJsZ1givB4dM1-ksopdF4VySw,12176
56
+ digitalkin/models/module/module_context.py,sha256=kwaAobsA82Du6L0XH9Is9u6Qj1rEjhYqVk00FonMaw4,12087
57
57
  digitalkin/models/module/module_types.py,sha256=C9azCNBk76xMa-Mww8_6AiwQR8MLAsEyUOvBYxytovI,739
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
58
+ digitalkin/models/module/setup_types.py,sha256=8xEI6DvCYBHSOsuoeuZjtBL5rPsR_7DwWFxQnn2bo3c,19249
59
+ digitalkin/models/module/tool_cache.py,sha256=xAMSyY73aduYGPz6C54g0YMAa2OnXPN6QRS-W4TK_js,6568
60
+ digitalkin/models/module/tool_reference.py,sha256=rdkke5ydAgXcFfu34yxiHyaiRGcclnNGwPJK-01B2t8,3829
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
- digitalkin/models/services/registry.py,sha256=mFehnPAVLGimodHquNrltXbH_aE0jEa-PxfyNm6J38E,1828
64
+ digitalkin/models/services/registry.py,sha256=bCdVpsiu8bqKL-7P5r3OPmllZj4MAtpsfsdlsg5TAww,1887
65
65
  digitalkin/models/services/storage.py,sha256=wp7F-AvTsU46ujGPcguqM5kUKRZx4399D4EGAAJt2zs,1143
66
66
  digitalkin/modules/__init__.py,sha256=vTQk8DWopxQSJ17BjE5dNhq247Rou55iQLJdBxoPUmo,296
67
67
  digitalkin/modules/_base_module.py,sha256=0XC0aQAxlNfvz0KK9ut7K0JbZql3cZMU4aeg7ISEsD0,21971
@@ -97,7 +97,7 @@ digitalkin/services/identity/identity_strategy.py,sha256=skappBbds1_qa0Gr24FGrNX
97
97
  digitalkin/services/registry/__init__.py,sha256=WPGQM3U-QvMXhsaOy9BN0kVMU3QkPFwAMT3lGmTR-Ko,835
98
98
  digitalkin/services/registry/default_registry.py,sha256=tOqw9Ve9w_BzhqrZmHuUl5Ps-J_KTEwYg3tu1gNIHmw,4258
99
99
  digitalkin/services/registry/exceptions.py,sha256=tAcVXioCzDqfBvxB_P0uQpaK_LDLrFb0KpymROuqs-8,1371
100
- digitalkin/services/registry/grpc_registry.py,sha256=uBQbETfB3Gheteo2CWZ4l3Ho4QvSOJ0OCoRgzy7ZrVM,13079
100
+ digitalkin/services/registry/grpc_registry.py,sha256=Myrt2WqmRQG1yLu2voUOLH4vp3Kwf_WitOBG3YwhtB4,13086
101
101
  digitalkin/services/registry/registry_models.py,sha256=DJEwMJg5_BewpgHDtY8xIGWj9jA9H07iYgHLCv81giY,331
102
102
  digitalkin/services/registry/registry_strategy.py,sha256=oxCm5mQcO1PSwMwOtEv8dwzQx_F0uGKgiWeVMeiV51s,2905
103
103
  digitalkin/services/setup/__init__.py,sha256=t6xcvEWqTbcRZstBFK9cESEqaZKvpW14VtYygxIqfYQ,65
@@ -122,10 +122,10 @@ digitalkin/utils/development_mode_action.py,sha256=2hznh0ajW_4ZTysfoc0Y49161f_PQ
122
122
  digitalkin/utils/dynamic_schema.py,sha256=y5csxjuqVHjWDpnTUzxbcUuI_wou9-ibRVHQlBs_btY,15275
123
123
  digitalkin/utils/llm_ready_schema.py,sha256=JjMug_lrQllqFoanaC091VgOqwAd-_YzcpqFlS7p778,2375
124
124
  digitalkin/utils/package_discover.py,sha256=sa6Zp5Kape1Zr4iYiNrnZxiHDnqM06ODk6yfWHom53w,13465
125
- digitalkin/utils/schema_splitter.py,sha256=7KFiRUhFE9HsH_Z8p0POSbIhiJlgZEeSxL_7TDY0n1U,10374
126
- digitalkin-0.3.2.dev20.dist-info/licenses/LICENSE,sha256=Ies4HFv2r2hzDRakJYxk3Y60uDFLiG-orIgeTpstnIo,20327
125
+ digitalkin/utils/schema_splitter.py,sha256=6PeDrEBGEFyVEPgzGnwcCZuwxLobThmuPwJhGlRpU_0,11137
126
+ digitalkin-0.3.2.dev22.dist-info/licenses/LICENSE,sha256=Ies4HFv2r2hzDRakJYxk3Y60uDFLiG-orIgeTpstnIo,20327
127
127
  modules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
128
- modules/archetype_with_tools_module.py,sha256=PXTS6IXmC_OjxTmVrL_pYVI0MKwXjD5I1UJO_2xa10Q,7632
128
+ modules/archetype_with_tools_module.py,sha256=kJkVhAFWG0aDDqzupOXOnV3l8j3z5bEdWos_6Z9rUP8,7303
129
129
  modules/cpu_intensive_module.py,sha256=GZlirQDZdYuXrI46sv1q4RNAHZjL4EptHVQTvgK9zz8,8363
130
130
  modules/dynamic_setup_module.py,sha256=tKvUWZdlYZkfAgKR0mLuFcLiFGKpVgpsz10LeJ6B2QI,11410
131
131
  modules/minimal_llm_module.py,sha256=N9aIzZQI-miyH4AB4xTmGHpMvdSLnYyXNOD4Z3YFzis,11216
@@ -138,7 +138,7 @@ monitoring/digitalkin_observability/prometheus.py,sha256=gDmM9ySaVwPAe7Yg84pLxmE
138
138
  monitoring/tests/test_metrics.py,sha256=ugnYfAwqBPO6zA8z4afKTlyBWECTivacYSN-URQCn2E,5856
139
139
  services/filesystem_module.py,sha256=U4dgqtuDadaXz8PJ1d_uQ_1EPncBqudAQCLUICF9yL4,7421
140
140
  services/storage_module.py,sha256=Wz2MzLvqs2D_bnBBgtnujYcAKK2V2KFMk8K21RoepSE,6972
141
- digitalkin-0.3.2.dev20.dist-info/METADATA,sha256=EosqD8N82EhYPNyjWKp-_n2az8875Qp8iRfdZph5uB4,29725
142
- digitalkin-0.3.2.dev20.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
143
- digitalkin-0.3.2.dev20.dist-info/top_level.txt,sha256=AYVIesKrO0jnedQ-Muog9JBehG81WeTCNeOFoJgwsgE,51
144
- digitalkin-0.3.2.dev20.dist-info/RECORD,,
141
+ digitalkin-0.3.2.dev22.dist-info/METADATA,sha256=6Zn0jPPta8ZRH7ohcui_18g4PsaoCBO6bSi59U4kg9o,29725
142
+ digitalkin-0.3.2.dev22.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
143
+ digitalkin-0.3.2.dev22.dist-info/top_level.txt,sha256=AYVIesKrO0jnedQ-Muog9JBehG81WeTCNeOFoJgwsgE,51
144
+ digitalkin-0.3.2.dev22.dist-info/RECORD,,
@@ -8,11 +8,7 @@ from pydantic import BaseModel, Field
8
8
  from digitalkin.models.grpc_servers.models import ClientConfig, SecurityMode, ServerMode
9
9
  from digitalkin.models.module.module_context import ModuleContext
10
10
  from digitalkin.models.module.setup_types import SetupModel
11
- from digitalkin.models.module.tool_reference import (
12
- ToolReference,
13
- ToolReferenceConfig,
14
- ToolSelectionMode,
15
- )
11
+ from digitalkin.models.module.tool_reference import ToolReference
16
12
  from digitalkin.modules._base_module import BaseModule # noqa: PLC2701
17
13
  from digitalkin.services.services_models import ServicesStrategy
18
14
 
@@ -64,29 +60,21 @@ class ArchetypeSetup(SetupModel):
64
60
 
65
61
  search_tool: ToolReference = Field(
66
62
  default_factory=lambda: ToolReference(
67
- config=ToolReferenceConfig(
68
- mode=ToolSelectionMode.FIXED,
69
- module_id="search-tool-v1",
70
- )
63
+ module_ids=["search-tool-v1"],
71
64
  ),
72
65
  json_schema_extra={"config": True},
73
66
  )
74
67
 
75
68
  calculator_tool: ToolReference = Field(
76
69
  default_factory=lambda: ToolReference(
77
- config=ToolReferenceConfig(
78
- mode=ToolSelectionMode.TAG,
79
- tag="math-calculator",
80
- )
70
+ tags=["math-calculator"],
81
71
  ),
82
72
  json_schema_extra={"config": True},
83
73
  )
84
74
 
85
75
  dynamic_tool: ToolReference = Field(
86
76
  default_factory=lambda: ToolReference(
87
- config=ToolReferenceConfig(
88
- mode=ToolSelectionMode.DISCOVERABLE,
89
- )
77
+ tags=["discoverable"],
90
78
  ),
91
79
  json_schema_extra={"config": True},
92
80
  )