digitalkin 0.3.2.dev21__py3-none-any.whl → 0.3.2.dev23__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.dev21"
8
+ __version__ = "0.3.2.dev23"
@@ -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
+ tool_reference_input,
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",
41
38
  "UtilityProtocol",
42
39
  "UtilityRegistry",
40
+ "tool_reference_input",
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,173 @@
1
1
  """Tool reference types for module configuration."""
2
2
 
3
- from enum import Enum
4
-
5
- from pydantic import BaseModel, Field, PrivateAttr, model_validator
6
-
7
- from digitalkin.models.module.tool_cache import ToolModuleInfo, module_info_to_tool_module_info
3
+ from typing import Annotated
4
+
5
+ from pydantic import AfterValidator, BaseModel, BeforeValidator, Field
6
+ from pydantic.annotated_handlers import GetJsonSchemaHandler
7
+ from pydantic.json_schema import JsonSchemaValue
8
+ from pydantic_core import CoreSchema
9
+
10
+ from digitalkin.models.module.tool_cache import (
11
+ SelectedTool,
12
+ ToolModuleInfo,
13
+ module_info_to_tool_module_info,
14
+ )
8
15
  from digitalkin.services.communication.communication_strategy import CommunicationStrategy
9
16
  from digitalkin.services.registry import RegistryStrategy
10
17
 
11
18
 
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
19
  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)
20
+ """Tool selection configuration and reference."""
53
21
 
54
- @property
55
- def slug(self) -> str:
56
- """Cache key (same as module_id).
22
+ selected_tools: list[SelectedTool] = Field(default=[], description="Tools selected by the user.")
23
+ setup_ids: list[str] = Field(default=[], description="Setup IDs for the user to choose from.")
24
+ module_ids: list[str] = Field(default=[], description="Module IDs for the user to choose from.")
25
+ tags: list[str] = Field(default=[], description="Tags for the user to choose from.")
26
+ max_tools: int = Field(default=0, description="Maximum tools to select. 0 for unlimited.")
27
+ min_tools: int = Field(default=0, description="Minimum tools to select. 0 for no minimum.")
57
28
 
58
- Returns:
59
- Module ID used as cache key.
60
- """
61
- return self.config.setup_id
29
+ async def resolve(self, registry: RegistryStrategy, communication: CommunicationStrategy) -> list[ToolModuleInfo]:
30
+ """Resolve this reference using the registry.
62
31
 
63
- @property
64
- def module_id(self) -> str:
65
- """Module identifier.
32
+ Args:
33
+ registry: Registry service for module discovery.
34
+ communication: Communication service for module schemas.
66
35
 
67
36
  Returns:
68
- Module ID or empty string if not set.
37
+ List of ToolModuleInfo if resolved.
69
38
  """
70
- return self.config.module_id
39
+ resolved: list[ToolModuleInfo] = []
40
+ for tool in self.selected_tools:
41
+ setup = registry.get_setup(tool.setup_id)
42
+ if setup and setup.module_id:
43
+ info = registry.discover_by_id(setup.module_id)
44
+ tool.slug = tool.setup_id
45
+ tool.module_id = setup.module_id
46
+ tool.name = setup.name
47
+ if info:
48
+ resolved.append(await module_info_to_tool_module_info(info, tool, communication))
49
+
50
+ return resolved
51
+
52
+
53
+ class _ToolReferenceInputSchema:
54
+ """Custom JSON schema generator with configurable maxItems and ui:options."""
55
+
56
+ def __init__(
57
+ self,
58
+ setup_ids: list[str],
59
+ module_ids: list[str],
60
+ tags: list[str],
61
+ max_tools: int = 0,
62
+ min_tools: int = 0,
63
+ ) -> None:
64
+ self.setup_ids = setup_ids
65
+ self.module_ids = module_ids
66
+ self.tags = tags
67
+ self.max_tools = max_tools
68
+ self.min_tools = min_tools
69
+
70
+ def __get_pydantic_json_schema__( # noqa: PLW3201
71
+ self,
72
+ schema: CoreSchema,
73
+ handler: GetJsonSchemaHandler,
74
+ ) -> JsonSchemaValue:
75
+ """Generate JSON schema accepting both list[str] and ToolReference.
71
76
 
72
- @property
73
- def setup_id(self) -> str:
74
- """Setup identifier.
77
+ Args:
78
+ schema: The core schema from Pydantic.
79
+ handler: Handler to generate JSON schema from core schema.
75
80
 
76
81
  Returns:
77
- Setup ID or empty string if not set.
82
+ JSON schema with anyOf accepting array or ToolReference, plus ui:options.
78
83
  """
79
- return self.config.setup_id
80
-
81
- @property
82
- def tool_module_info(self) -> ToolModuleInfo | None:
83
- """Resolved module information.
84
+ json_schema = handler(schema)
85
+
86
+ array_option: dict[str, object] = {"type": "array", "items": {"type": "string"}}
87
+ if self.max_tools > 0 and self.max_tools >= self.min_tools:
88
+ array_option["maxItems"] = self.max_tools
89
+ if self.min_tools > 0 and self.min_tools <= self.max_tools:
90
+ array_option["minItems"] = self.min_tools
91
+
92
+ return {
93
+ "anyOf": [
94
+ array_option,
95
+ json_schema,
96
+ ],
97
+ "ui:options": {
98
+ "setup_ids": self.setup_ids,
99
+ "module_ids": self.module_ids,
100
+ "tags": self.tags,
101
+ "max_tools": self.max_tools,
102
+ "min_tools": self.min_tools,
103
+ },
104
+ }
105
+
106
+
107
+ def tool_reference_input(
108
+ setup_ids: list[str] = [],
109
+ module_ids: list[str] = [],
110
+ tags: list[str] = [],
111
+ max_tools: int = 0,
112
+ min_tools: int = 0,
113
+ ) -> type[ToolReference]:
114
+ """Create ToolReferenceInput type with schema options and validation.
115
+
116
+ Args:
117
+ setup_ids: Setup IDs for the user to choose from.
118
+ module_ids: Module IDs for the user to choose from.
119
+ tags: Tags for the user to choose from.
120
+ max_tools: Maximum tools allowed. 0 for unlimited.
121
+ min_tools: Minimum tools required. 0 for no minimum.
122
+
123
+ Returns:
124
+ Annotated type for use in Pydantic models.
125
+ """
126
+
127
+ def convert_to_tool_reference(v: object) -> ToolReference | object:
128
+ """Convert list of setup IDs to ToolReference with config preserved.
84
129
 
85
130
  Returns:
86
- ToolModuleInfo if resolved, None otherwise.
131
+ ToolReference if input is list, otherwise original value.
87
132
  """
88
- return self._cached_info
133
+ if isinstance(v, list):
134
+ return ToolReference(
135
+ selected_tools=[SelectedTool(setup_id=sid, slug=sid) for sid in v],
136
+ setup_ids=setup_ids,
137
+ module_ids=module_ids,
138
+ tags=tags,
139
+ max_tools=max_tools,
140
+ min_tools=min_tools,
141
+ )
142
+ return v
89
143
 
90
- @property
91
- def is_resolved(self) -> bool:
92
- """Whether this reference has been resolved.
144
+ def validate_tools_count(v: ToolReference) -> ToolReference:
145
+ """Validate selected_tools count against min/max constraints.
93
146
 
94
147
  Returns:
95
- True if resolved, False otherwise.
96
- """
97
- return self._cached_info is not None
98
-
99
- async def resolve(self, registry: RegistryStrategy, communication: CommunicationStrategy) -> ToolModuleInfo | None:
100
- """Resolve this reference using the registry.
148
+ The validated ToolReference.
101
149
 
102
- Args:
103
- registry: Registry service for module discovery.
104
- communication: Communication service for module schemas.
105
-
106
- Returns:
107
- ToolModuleInfo if resolved, None for DISCOVERABLE mode or if not found.
150
+ Raises:
151
+ ValueError: If count is below min_tools or above max_tools.
108
152
  """
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
153
+ count = len(v.selected_tools)
154
+ if min_tools > 0 and count < min_tools:
155
+ msg = f"At least {min_tools} tools required, got {count}"
156
+ raise ValueError(msg)
157
+ if max_tools > 0 and count > max_tools:
158
+ msg = f"At most {max_tools} tools allowed, got {count}"
159
+ raise ValueError(msg)
160
+ return v
161
+
162
+ return Annotated[ # type: ignore[return-value]
163
+ ToolReference,
164
+ BeforeValidator(convert_to_tool_reference),
165
+ AfterValidator(validate_tools_count),
166
+ _ToolReferenceInputSchema(
167
+ setup_ids=setup_ids,
168
+ module_ids=module_ids,
169
+ tags=tags,
170
+ max_tools=max_tools,
171
+ min_tools=min_tools,
172
+ ),
173
+ ]
@@ -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
 
@@ -30,7 +30,7 @@ class SchemaSplitter:
30
30
  return json_schema, ui_schema
31
31
 
32
32
  @classmethod
33
- def _extract_ui_properties(
33
+ def _extract_ui_properties( # noqa: C901, PLR0912
34
34
  cls,
35
35
  source: dict[str, Any],
36
36
  ui_target: dict[str, Any],
@@ -64,12 +64,11 @@ class SchemaSplitter:
64
64
  cls._extract_ui_properties(item, ui_target, defs)
65
65
  elif key in {"if", "then", "else"} and isinstance(value, dict):
66
66
  cls._extract_ui_properties(value, ui_target, defs)
67
- elif key == "$ref" and isinstance(value, str) and defs is not None:
67
+ elif key == "$ref" and isinstance(value, str) and defs is not None and value.startswith("#/$defs/"):
68
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)
69
+ def_name = value[8:]
70
+ if def_name in defs:
71
+ cls._extract_ui_properties(defs[def_name], ui_target, defs)
73
72
 
74
73
  @classmethod
75
74
  def _process_object( # noqa: C901, PLR0912
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: digitalkin
3
- Version: 0.3.2.dev21
3
+ Version: 0.3.2.dev23
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
@@ -437,18 +437,18 @@ License: Attribution-NonCommercial-ShareAlike 4.0 International
437
437
  Project-URL: Homepage, https://github.com/DigitalKin-ai/digitalkin
438
438
  Project-URL: Documentation, https://github.com/DigitalKin-ai/digitalkin
439
439
  Project-URL: Issues, https://github.com/DigitalKin-ai/digitalkin/issues
440
- Keywords: digitalkin,kin,agent,gprc,sdk
440
+ Keywords: agent,digitalkin,gprc,kin,sdk
441
441
  Classifier: Development Status :: 3 - Alpha
442
442
  Classifier: Intended Audience :: Developers
443
+ Classifier: License :: Other/Proprietary License
443
444
  Classifier: Operating System :: OS Independent
444
- Classifier: Topic :: Software Development :: Libraries
445
- Classifier: Programming Language :: Python
445
+ Classifier: Programming Language :: Python :: 3 :: Only
446
446
  Classifier: Programming Language :: Python :: 3.10
447
447
  Classifier: Programming Language :: Python :: 3.11
448
448
  Classifier: Programming Language :: Python :: 3.12
449
449
  Classifier: Programming Language :: Python :: 3.13
450
- Classifier: Programming Language :: Python :: 3 :: Only
451
- Classifier: License :: Other/Proprietary License
450
+ Classifier: Programming Language :: Python
451
+ Classifier: Topic :: Software Development :: Libraries
452
452
  Requires-Python: >=3.10
453
453
  Description-Content-Type: text/markdown
454
454
  License-File: LICENSE
@@ -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=rOjHWGOhXZoGaCKIj8TeRUY1SA0ZmJjAbwE9acxZsuU,196
10
+ digitalkin/__version__.py,sha256=RHumdYZFrCYmC-fgDaLplg-14r7IrUPHKS70yI19O3w,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=YDOsaX-4fYaZR6Cs3ksTr6pa0LSzo7lvSzWftLZJuwU,913
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=DjumRWlH18SzJLrGlsdsY1opDR7M1UfhI3Eyjj1iTu4,6176
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=6PeDrEBGEFyVEPgzGnwcCZuwxLobThmuPwJhGlRpU_0,11137
126
- digitalkin-0.3.2.dev21.dist-info/licenses/LICENSE,sha256=Ies4HFv2r2hzDRakJYxk3Y60uDFLiG-orIgeTpstnIo,20327
125
+ digitalkin/utils/schema_splitter.py,sha256=f1RGzs0wSRPn8oydO9Ojo8mYTF4TFDkRFeGUt5jbPt4,11132
126
+ digitalkin-0.3.2.dev23.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.dev21.dist-info/METADATA,sha256=TSn7ZjFiT9GYhIoAedzR7OzC2LT8_VvMS61vph1X6SA,29725
142
- digitalkin-0.3.2.dev21.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
143
- digitalkin-0.3.2.dev21.dist-info/top_level.txt,sha256=AYVIesKrO0jnedQ-Muog9JBehG81WeTCNeOFoJgwsgE,51
144
- digitalkin-0.3.2.dev21.dist-info/RECORD,,
141
+ digitalkin-0.3.2.dev23.dist-info/METADATA,sha256=GpeeavGsy23lR_dj41wBw2nXaLUD8Jhph0dIB1lhu-U,29725
142
+ digitalkin-0.3.2.dev23.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
143
+ digitalkin-0.3.2.dev23.dist-info/top_level.txt,sha256=AYVIesKrO0jnedQ-Muog9JBehG81WeTCNeOFoJgwsgE,51
144
+ digitalkin-0.3.2.dev23.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
  )