digitalkin 0.3.1.dev2__py3-none-any.whl → 0.3.2a3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. base_server/server_async_insecure.py +6 -5
  2. base_server/server_async_secure.py +6 -5
  3. base_server/server_sync_insecure.py +5 -4
  4. base_server/server_sync_secure.py +5 -4
  5. digitalkin/__version__.py +1 -1
  6. digitalkin/core/job_manager/base_job_manager.py +1 -1
  7. digitalkin/core/job_manager/single_job_manager.py +78 -36
  8. digitalkin/core/job_manager/taskiq_broker.py +7 -6
  9. digitalkin/core/job_manager/taskiq_job_manager.py +9 -5
  10. digitalkin/core/task_manager/base_task_manager.py +3 -1
  11. digitalkin/core/task_manager/surrealdb_repository.py +29 -7
  12. digitalkin/core/task_manager/task_executor.py +46 -12
  13. digitalkin/core/task_manager/task_session.py +132 -102
  14. digitalkin/grpc_servers/module_server.py +95 -171
  15. digitalkin/grpc_servers/module_servicer.py +121 -19
  16. digitalkin/grpc_servers/utils/grpc_client_wrapper.py +36 -10
  17. digitalkin/grpc_servers/utils/utility_schema_extender.py +106 -0
  18. digitalkin/models/__init__.py +1 -1
  19. digitalkin/models/core/job_manager_models.py +0 -8
  20. digitalkin/models/core/task_monitor.py +23 -1
  21. digitalkin/models/grpc_servers/models.py +95 -8
  22. digitalkin/models/module/__init__.py +26 -13
  23. digitalkin/models/module/base_types.py +61 -0
  24. digitalkin/models/module/module_context.py +279 -13
  25. digitalkin/models/module/module_types.py +28 -392
  26. digitalkin/models/module/setup_types.py +547 -0
  27. digitalkin/models/module/tool_cache.py +230 -0
  28. digitalkin/models/module/tool_reference.py +160 -0
  29. digitalkin/models/module/utility.py +167 -0
  30. digitalkin/models/services/cost.py +22 -1
  31. digitalkin/models/services/registry.py +77 -0
  32. digitalkin/modules/__init__.py +5 -1
  33. digitalkin/modules/_base_module.py +188 -63
  34. digitalkin/modules/archetype_module.py +6 -1
  35. digitalkin/modules/tool_module.py +6 -1
  36. digitalkin/modules/triggers/__init__.py +8 -0
  37. digitalkin/modules/triggers/healthcheck_ping_trigger.py +45 -0
  38. digitalkin/modules/triggers/healthcheck_services_trigger.py +63 -0
  39. digitalkin/modules/triggers/healthcheck_status_trigger.py +52 -0
  40. digitalkin/services/__init__.py +4 -0
  41. digitalkin/services/communication/__init__.py +7 -0
  42. digitalkin/services/communication/communication_strategy.py +87 -0
  43. digitalkin/services/communication/default_communication.py +104 -0
  44. digitalkin/services/communication/grpc_communication.py +264 -0
  45. digitalkin/services/cost/cost_strategy.py +36 -14
  46. digitalkin/services/cost/default_cost.py +61 -1
  47. digitalkin/services/cost/grpc_cost.py +98 -2
  48. digitalkin/services/filesystem/grpc_filesystem.py +9 -2
  49. digitalkin/services/registry/__init__.py +22 -1
  50. digitalkin/services/registry/default_registry.py +156 -4
  51. digitalkin/services/registry/exceptions.py +47 -0
  52. digitalkin/services/registry/grpc_registry.py +382 -0
  53. digitalkin/services/registry/registry_models.py +15 -0
  54. digitalkin/services/registry/registry_strategy.py +106 -4
  55. digitalkin/services/services_config.py +25 -3
  56. digitalkin/services/services_models.py +5 -1
  57. digitalkin/services/setup/default_setup.py +1 -1
  58. digitalkin/services/setup/grpc_setup.py +1 -1
  59. digitalkin/services/storage/grpc_storage.py +1 -1
  60. digitalkin/services/user_profile/__init__.py +11 -0
  61. digitalkin/services/user_profile/grpc_user_profile.py +2 -2
  62. digitalkin/services/user_profile/user_profile_strategy.py +0 -15
  63. digitalkin/utils/__init__.py +15 -3
  64. digitalkin/utils/conditional_schema.py +260 -0
  65. digitalkin/utils/dynamic_schema.py +4 -0
  66. digitalkin/utils/schema_splitter.py +290 -0
  67. {digitalkin-0.3.1.dev2.dist-info → digitalkin-0.3.2a3.dist-info}/METADATA +12 -12
  68. digitalkin-0.3.2a3.dist-info/RECORD +144 -0
  69. {digitalkin-0.3.1.dev2.dist-info → digitalkin-0.3.2a3.dist-info}/WHEEL +1 -1
  70. {digitalkin-0.3.1.dev2.dist-info → digitalkin-0.3.2a3.dist-info}/top_level.txt +1 -0
  71. modules/archetype_with_tools_module.py +232 -0
  72. modules/cpu_intensive_module.py +1 -1
  73. modules/dynamic_setup_module.py +5 -29
  74. modules/minimal_llm_module.py +1 -1
  75. modules/text_transform_module.py +1 -1
  76. monitoring/digitalkin_observability/__init__.py +46 -0
  77. monitoring/digitalkin_observability/http_server.py +150 -0
  78. monitoring/digitalkin_observability/interceptors.py +176 -0
  79. monitoring/digitalkin_observability/metrics.py +201 -0
  80. monitoring/digitalkin_observability/prometheus.py +137 -0
  81. monitoring/tests/test_metrics.py +172 -0
  82. services/filesystem_module.py +7 -5
  83. services/storage_module.py +4 -2
  84. digitalkin/grpc_servers/registry_server.py +0 -65
  85. digitalkin/grpc_servers/registry_servicer.py +0 -456
  86. digitalkin-0.3.1.dev2.dist-info/RECORD +0 -119
  87. {digitalkin-0.3.1.dev2.dist-info → digitalkin-0.3.2a3.dist-info}/licenses/LICENSE +0 -0
@@ -13,21 +13,6 @@ class UserProfileServiceError(Exception):
13
13
  class UserProfileStrategy(BaseStrategy, ABC):
14
14
  """Abstract base class for UserProfile strategies."""
15
15
 
16
- def __init__(
17
- self,
18
- mission_id: str,
19
- setup_id: str,
20
- setup_version_id: str,
21
- ) -> None:
22
- """Initialize the strategy.
23
-
24
- Args:
25
- mission_id: The ID of the mission this strategy is associated with
26
- setup_id: The ID of the setup
27
- setup_version_id: The ID of the setup version this strategy is associated with
28
- """
29
- super().__init__(mission_id, setup_id, setup_version_id)
30
-
31
16
  @abstractmethod
32
17
  def get_user_profile(self) -> dict[str, Any]:
33
18
  """Get user profile data.
@@ -1,7 +1,15 @@
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,
12
+ Dynamic,
5
13
  DynamicField,
6
14
  Fetcher,
7
15
  ResolveResult,
@@ -12,17 +20,21 @@ from digitalkin.utils.dynamic_schema import (
12
20
  resolve_safe,
13
21
  )
14
22
 
15
- # Alias for cleaner API: `Dynamic` is shorter than `DynamicField`
16
- Dynamic = DynamicField
17
-
18
23
  __all__ = [
24
+ # Dynamic schema
19
25
  "DEFAULT_TIMEOUT",
26
+ # Conditional schema
27
+ "Conditional",
28
+ "ConditionalField",
29
+ "ConditionalSchemaMixin",
20
30
  "Dynamic",
21
31
  "DynamicField",
22
32
  "Fetcher",
23
33
  "ResolveResult",
34
+ "get_conditional_metadata",
24
35
  "get_dynamic_metadata",
25
36
  "get_fetchers",
37
+ "has_conditional",
26
38
  "has_dynamic",
27
39
  "resolve",
28
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
@@ -136,6 +136,10 @@ class DynamicField:
136
136
  return hash(tuple(sorted(self.fetchers.keys())))
137
137
 
138
138
 
139
+ # Alias for cleaner API: `Dynamic` is shorter than `DynamicField`
140
+ Dynamic = DynamicField
141
+
142
+
139
143
  def get_dynamic_metadata(field_info: FieldInfo) -> DynamicField | None:
140
144
  """Extract DynamicField metadata from a FieldInfo's metadata list.
141
145
 
@@ -0,0 +1,290 @@
1
+ """Schema splitter for react-jsonschema-form."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ class SchemaSplitter:
7
+ """Splits a combined JSON schema into jsonschema and uischema for react-jsonschema-form."""
8
+
9
+ @classmethod
10
+ def split(cls, combined_schema: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
11
+ """Split schema into (jsonschema, uischema).
12
+
13
+ Args:
14
+ combined_schema: Combined JSON schema with ui:* properties.
15
+
16
+ Returns:
17
+ Tuple of (jsonschema, uischema).
18
+ """
19
+ defs_ui: dict[str, dict[str, Any]] = {}
20
+ schema_defs = combined_schema.get("$defs", {})
21
+ if schema_defs:
22
+ for def_name, def_value in schema_defs.items():
23
+ if isinstance(def_value, dict):
24
+ defs_ui[def_name] = {}
25
+ cls._extract_ui_properties(def_value, defs_ui[def_name], schema_defs)
26
+
27
+ json_schema: dict[str, Any] = {}
28
+ ui_schema: dict[str, Any] = {}
29
+ cls._process_object(combined_schema, json_schema, ui_schema, defs_ui, schema_defs)
30
+ return json_schema, ui_schema
31
+
32
+ @classmethod
33
+ def _extract_ui_properties( # noqa: C901, PLR0912
34
+ cls,
35
+ source: dict[str, Any],
36
+ ui_target: dict[str, Any],
37
+ defs: dict[str, Any] | None = None,
38
+ ) -> None:
39
+ """Extract ui:* properties from source into ui_target recursively.
40
+
41
+ Args:
42
+ source: Source dict to extract from.
43
+ ui_target: Target dict for ui properties.
44
+ defs: The $defs dictionary to resolve $ref references.
45
+ """
46
+ for key, value in source.items():
47
+ if key.startswith("ui:"):
48
+ ui_target[key] = value
49
+ elif key == "properties" and isinstance(value, dict):
50
+ for prop_name, prop_value in value.items():
51
+ if isinstance(prop_value, dict):
52
+ prop_ui: dict[str, Any] = {}
53
+ cls._extract_ui_properties(prop_value, prop_ui, defs)
54
+ if prop_ui:
55
+ ui_target[prop_name] = prop_ui
56
+ elif key == "items" and isinstance(value, dict):
57
+ items_ui: dict[str, Any] = {}
58
+ cls._extract_ui_properties(value, items_ui, defs)
59
+ if items_ui:
60
+ ui_target["items"] = items_ui
61
+ elif key in {"allOf", "oneOf", "anyOf"} and isinstance(value, list):
62
+ # For oneOf/anyOf, create a mirrored structure in ui_target
63
+ if key in {"oneOf", "anyOf"}:
64
+ ui_target[key] = []
65
+ for item in value:
66
+ if isinstance(item, dict):
67
+ item_ui: dict[str, Any] = {}
68
+ cls._extract_ui_properties(item, item_ui, defs)
69
+ ui_target[key].append(item_ui)
70
+ else: # allOf - merge into parent
71
+ for item in value:
72
+ if isinstance(item, dict):
73
+ cls._extract_ui_properties(item, ui_target, defs)
74
+ elif key in {"if", "then", "else"} and isinstance(value, dict):
75
+ cls._extract_ui_properties(value, ui_target, defs)
76
+ elif key == "$ref" and isinstance(value, str) and defs is not None and value.startswith("#/$defs/"):
77
+ # Resolve $ref and extract UI properties from the referenced definition
78
+ def_name = value[8:]
79
+ if def_name in defs:
80
+ cls._extract_ui_properties(defs[def_name], ui_target, defs)
81
+
82
+ @classmethod
83
+ def _process_object( # noqa: C901, PLR0912, PLR0915
84
+ cls,
85
+ source: dict[str, Any],
86
+ json_target: dict[str, Any],
87
+ ui_target: dict[str, Any],
88
+ defs_ui: dict[str, dict[str, Any]],
89
+ schema_defs: dict[str, Any] | None = None,
90
+ ) -> None:
91
+ """Process an object, splitting json and ui properties.
92
+
93
+ Args:
94
+ source: Source object to process.
95
+ json_target: Target dict for json schema.
96
+ ui_target: Target dict for ui schema.
97
+ defs_ui: Pre-extracted UI properties from $defs.
98
+ schema_defs: The original $defs dictionary to resolve $ref in _extract_ui_properties.
99
+ """
100
+ for key, value in source.items():
101
+ if key.startswith("ui:"):
102
+ ui_target[key] = value
103
+ elif key == "properties" and isinstance(value, dict):
104
+ json_target["properties"] = {}
105
+ for prop_name, prop_value in value.items():
106
+ if isinstance(prop_value, dict):
107
+ # Skip hidden fields
108
+ if prop_value.get("hidden") is True:
109
+ continue
110
+ json_target["properties"][prop_name] = {}
111
+ prop_ui: dict[str, Any] = {}
112
+ cls._process_property(prop_value, json_target["properties"][prop_name], prop_ui, defs_ui)
113
+ if prop_ui:
114
+ ui_target[prop_name] = prop_ui
115
+ else:
116
+ json_target["properties"][prop_name] = prop_value
117
+ elif key == "$defs" and isinstance(value, dict):
118
+ json_target["$defs"] = {}
119
+ for def_name, def_value in value.items():
120
+ if isinstance(def_value, dict):
121
+ json_target["$defs"][def_name] = {}
122
+ cls._strip_ui_properties(def_value, json_target["$defs"][def_name])
123
+ else:
124
+ json_target["$defs"][def_name] = def_value
125
+ elif key == "items" and isinstance(value, dict):
126
+ json_target["items"] = {}
127
+ items_ui: dict[str, Any] = {}
128
+ cls._process_property(value, json_target["items"], items_ui, defs_ui)
129
+ if items_ui:
130
+ ui_target["items"] = items_ui
131
+ elif key == "allOf" and isinstance(value, list):
132
+ json_target["allOf"] = []
133
+ for item in value:
134
+ if isinstance(item, dict):
135
+ item_json_all_of: dict[str, Any] = {}
136
+ cls._strip_ui_properties(item, item_json_all_of)
137
+ json_target["allOf"].append(item_json_all_of)
138
+ # Extract UI properties from allOf item
139
+ cls._extract_ui_properties(item, ui_target, schema_defs)
140
+ else:
141
+ json_target["allOf"].append(item)
142
+ elif key in {"oneOf", "anyOf"} and isinstance(value, list):
143
+ json_target[key] = []
144
+ ui_target[key] = [] # Mirror structure in ui_schema
145
+ for item in value:
146
+ if isinstance(item, dict):
147
+ item_json_oneof_anyof: dict[str, Any] = {}
148
+ cls._strip_ui_properties(item, item_json_oneof_anyof)
149
+ json_target[key].append(item_json_oneof_anyof)
150
+ # Extract UI properties into a separate dict for this variant
151
+ item_ui_oneof_anyof: dict[str, Any] = {}
152
+ cls._extract_ui_properties(item, item_ui_oneof_anyof, schema_defs)
153
+ ui_target[key].append(item_ui_oneof_anyof)
154
+ else:
155
+ json_target[key].append(item)
156
+ elif key in {"if", "then", "else"} and isinstance(value, dict):
157
+ json_target[key] = {}
158
+ cls._strip_ui_properties(value, json_target[key])
159
+ # Extract UI properties from conditional
160
+ cls._extract_ui_properties(value, ui_target, schema_defs)
161
+ elif key == "hidden":
162
+ # Strip hidden key from json schema
163
+ continue
164
+ else:
165
+ json_target[key] = value
166
+
167
+ @classmethod
168
+ def _process_property( # noqa: C901, PLR0912
169
+ cls,
170
+ source: dict[str, Any],
171
+ json_target: dict[str, Any],
172
+ ui_target: dict[str, Any],
173
+ defs_ui: dict[str, dict[str, Any]],
174
+ ) -> None:
175
+ """Process a property, resolving $ref for UI properties.
176
+
177
+ Args:
178
+ source: Source property dict.
179
+ json_target: Target dict for json schema.
180
+ ui_target: Target dict for ui schema.
181
+ defs_ui: Pre-extracted UI properties from $defs.
182
+ """
183
+ if "$ref" in source:
184
+ ref_path = source["$ref"]
185
+ if ref_path.startswith("#/$defs/"):
186
+ def_name = ref_path[8:]
187
+ if def_name in defs_ui:
188
+ ui_target.update(defs_ui[def_name])
189
+
190
+ for key, value in source.items():
191
+ if key.startswith("ui:"):
192
+ ui_target[key] = value
193
+ elif key == "properties" and isinstance(value, dict):
194
+ json_target["properties"] = {}
195
+ for prop_name, prop_value in value.items():
196
+ if isinstance(prop_value, dict):
197
+ # Skip hidden fields
198
+ if prop_value.get("hidden") is True:
199
+ continue
200
+ json_target["properties"][prop_name] = {}
201
+ prop_ui: dict[str, Any] = {}
202
+ cls._process_property(prop_value, json_target["properties"][prop_name], prop_ui, defs_ui)
203
+ if prop_ui:
204
+ ui_target[prop_name] = prop_ui
205
+ else:
206
+ json_target["properties"][prop_name] = prop_value
207
+ elif key == "items" and isinstance(value, dict):
208
+ json_target["items"] = {}
209
+ items_ui: dict[str, Any] = {}
210
+ cls._process_property(value, json_target["items"], items_ui, defs_ui)
211
+ if items_ui:
212
+ ui_target["items"] = items_ui
213
+ elif key in {"oneOf", "anyOf"} and isinstance(value, list):
214
+ json_target[key] = []
215
+ ui_target[key] = [] # Mirror structure in ui_schema
216
+ for item in value:
217
+ if isinstance(item, dict):
218
+ item_json_oneof_anyof: dict[str, Any] = {}
219
+ cls._strip_ui_properties(item, item_json_oneof_anyof)
220
+ json_target[key].append(item_json_oneof_anyof)
221
+ # Extract UI properties for this variant
222
+ item_ui_oneof_anyof: dict[str, Any] = {}
223
+ ref_path = item.get("$ref", "")
224
+ if ref_path.startswith("#/$defs/") and ref_path[8:] in defs_ui:
225
+ item_ui_oneof_anyof.update(defs_ui[ref_path[8:]])
226
+ ui_target[key].append(item_ui_oneof_anyof)
227
+ else:
228
+ json_target[key].append(item)
229
+ elif key == "hidden":
230
+ # Strip hidden key from json schema
231
+ continue
232
+ else:
233
+ json_target[key] = value
234
+
235
+ @classmethod
236
+ def _strip_ui_properties(cls, source: dict[str, Any], json_target: dict[str, Any]) -> None: # noqa: C901, PLR0912
237
+ """Copy source to json_target, stripping ui:* properties.
238
+
239
+ Args:
240
+ source: Source dict.
241
+ json_target: Target dict without ui:* properties.
242
+ """
243
+ for key, value in source.items():
244
+ if key.startswith("ui:") or key == "hidden":
245
+ continue
246
+ if key == "properties" and isinstance(value, dict):
247
+ json_target["properties"] = {}
248
+ for prop_name, prop_value in value.items():
249
+ if isinstance(prop_value, dict):
250
+ # Skip hidden fields
251
+ if prop_value.get("hidden") is True:
252
+ continue
253
+ json_target["properties"][prop_name] = {}
254
+ cls._strip_ui_properties(prop_value, json_target["properties"][prop_name])
255
+ else:
256
+ json_target["properties"][prop_name] = prop_value
257
+ elif key == "$defs" and isinstance(value, dict):
258
+ json_target["$defs"] = {}
259
+ for def_name, def_value in value.items():
260
+ if isinstance(def_value, dict):
261
+ json_target["$defs"][def_name] = {}
262
+ cls._strip_ui_properties(def_value, json_target["$defs"][def_name])
263
+ else:
264
+ json_target["$defs"][def_name] = def_value
265
+ elif key == "items" and isinstance(value, dict):
266
+ json_target["items"] = {}
267
+ cls._strip_ui_properties(value, json_target["items"])
268
+ elif key == "allOf" and isinstance(value, list):
269
+ json_target["allOf"] = []
270
+ for item in value:
271
+ if isinstance(item, dict):
272
+ item_json_all_of: dict[str, Any] = {}
273
+ cls._strip_ui_properties(item, item_json_all_of)
274
+ json_target["allOf"].append(item_json_all_of)
275
+ else:
276
+ json_target["allOf"].append(item)
277
+ elif key in {"oneOf", "anyOf"} and isinstance(value, list):
278
+ json_target[key] = []
279
+ for item in value:
280
+ if isinstance(item, dict):
281
+ item_json_oneof_anyof: dict[str, Any] = {}
282
+ cls._strip_ui_properties(item, item_json_oneof_anyof)
283
+ json_target[key].append(item_json_oneof_anyof)
284
+ else:
285
+ json_target[key].append(item)
286
+ elif key in {"if", "then", "else"} and isinstance(value, dict):
287
+ json_target[key] = {}
288
+ cls._strip_ui_properties(value, json_target[key])
289
+ else:
290
+ json_target[key] = value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: digitalkin
3
- Version: 0.3.1.dev2
3
+ Version: 0.3.2a3
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
@@ -434,35 +434,35 @@ License: Attribution-NonCommercial-ShareAlike 4.0 International
434
434
  Creative Commons may be contacted at creativecommons.org.
435
435
  https://creativecommons.org/licenses/by-nc-sa/4.0/
436
436
 
437
- Project-URL: Homepage, https://github.com/DigitalKin-ai/digitalkin
438
437
  Project-URL: Documentation, https://github.com/DigitalKin-ai/digitalkin
438
+ Project-URL: Homepage, 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
455
- Requires-Dist: digitalkin-proto==0.2.0.dev5
455
+ Requires-Dist: agentic-mesh-protocol==0.2.1.dev1
456
456
  Requires-Dist: grpcio-health-checking>=1.76.0
457
457
  Requires-Dist: grpcio-reflection>=1.76.0
458
458
  Requires-Dist: grpcio-status>=1.76.0
459
459
  Requires-Dist: pydantic>=2.12.5
460
- Requires-Dist: surrealdb>=1.0.6
460
+ Requires-Dist: surrealdb>=1.0.7
461
461
  Provides-Extra: taskiq
462
- Requires-Dist: rstream>=0.40.0; extra == "taskiq"
462
+ Requires-Dist: rstream>=0.40.1; extra == "taskiq"
463
463
  Requires-Dist: taskiq-aio-pika>=0.5.0; extra == "taskiq"
464
- Requires-Dist: taskiq-redis>=1.1.2; extra == "taskiq"
465
- Requires-Dist: taskiq[reload]>=0.12.0; extra == "taskiq"
464
+ Requires-Dist: taskiq-redis>=1.2.0; extra == "taskiq"
465
+ Requires-Dist: taskiq[reload]>=0.12.1; extra == "taskiq"
466
466
  Dynamic: license-file
467
467
 
468
468
  # DigitalKin Python SDK