digitalkin 0.3.2.dev7__py3-none-any.whl → 0.3.2.dev8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. digitalkin/__version__.py +1 -1
  2. digitalkin/grpc_servers/module_servicer.py +0 -11
  3. digitalkin/grpc_servers/utils/utility_schema_extender.py +2 -1
  4. digitalkin/models/module/module_context.py +136 -23
  5. digitalkin/models/module/setup_types.py +168 -257
  6. digitalkin/models/module/tool_cache.py +27 -187
  7. digitalkin/models/module/tool_reference.py +42 -45
  8. digitalkin/models/services/registry.py +0 -7
  9. digitalkin/modules/_base_module.py +74 -52
  10. digitalkin/services/registry/__init__.py +1 -1
  11. digitalkin/services/registry/default_registry.py +1 -1
  12. digitalkin/services/registry/grpc_registry.py +1 -1
  13. digitalkin/services/registry/registry_models.py +1 -29
  14. digitalkin/services/registry/registry_strategy.py +1 -1
  15. {digitalkin-0.3.2.dev7.dist-info → digitalkin-0.3.2.dev8.dist-info}/METADATA +1 -1
  16. {digitalkin-0.3.2.dev7.dist-info → digitalkin-0.3.2.dev8.dist-info}/RECORD +26 -20
  17. {digitalkin-0.3.2.dev7.dist-info → digitalkin-0.3.2.dev8.dist-info}/top_level.txt +1 -0
  18. modules/archetype_with_tools_module.py +244 -0
  19. monitoring/digitalkin_observability/__init__.py +46 -0
  20. monitoring/digitalkin_observability/http_server.py +150 -0
  21. monitoring/digitalkin_observability/interceptors.py +176 -0
  22. monitoring/digitalkin_observability/metrics.py +201 -0
  23. monitoring/digitalkin_observability/prometheus.py +137 -0
  24. monitoring/tests/test_metrics.py +172 -0
  25. digitalkin/models/module/module_helpers.py +0 -189
  26. {digitalkin-0.3.2.dev7.dist-info → digitalkin-0.3.2.dev8.dist-info}/WHEEL +0 -0
  27. {digitalkin-0.3.2.dev7.dist-info → digitalkin-0.3.2.dev8.dist-info}/licenses/LICENSE +0 -0
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.dev7"
8
+ __version__ = "0.3.2.dev8"
@@ -130,8 +130,6 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
130
130
  "mission_id": request.mission_id,
131
131
  },
132
132
  )
133
- # Process the module input
134
- # TODO: Secret should be used here as well
135
133
  setup_version = request.setup_version
136
134
  config_setup_data = self.module_class.create_config_setup_model(json_format.MessageToDict(request.content))
137
135
  setup_version_data = await self.module_class.create_setup_model(
@@ -147,15 +145,6 @@ class ModuleServicer(module_service_pb2_grpc.ModuleServiceServicer, ArgParser):
147
145
  msg = "No config setup data returned."
148
146
  raise ServicerError(msg)
149
147
 
150
- # Resolve tool references in config_setup_data if registry is configured
151
- # This also builds the tool_cache for LLM access during execution
152
- registry = self._get_registry()
153
- if registry:
154
- if hasattr(config_setup_data, "resolve_tool_references"):
155
- config_setup_data.resolve_tool_references(registry)
156
- if hasattr(config_setup_data, "build_tool_cache"):
157
- config_setup_data.build_tool_cache()
158
-
159
148
  # create a task to run the module in background
160
149
  job_id = await self.job_manager.create_config_setup_instance_job(
161
150
  config_setup_data,
@@ -3,6 +3,7 @@
3
3
  This module extends module schemas with SDK utility protocols for API responses.
4
4
  """
5
5
 
6
+ import types
6
7
  from typing import Annotated, Union, get_args, get_origin
7
8
 
8
9
  from pydantic import Field, create_model
@@ -50,7 +51,7 @@ class UtilitySchemaExtender:
50
51
  inner_args = get_args(annotation)
51
52
  if inner_args:
52
53
  return cls._extract_union_types(inner_args[0])
53
- if get_origin(annotation) is Union:
54
+ if get_origin(annotation) is Union or isinstance(annotation, types.UnionType):
54
55
  return get_args(annotation)
55
56
  return (annotation,)
56
57
 
@@ -1,12 +1,13 @@
1
1
  """Define the module context used in the triggers."""
2
2
 
3
3
  import os
4
+ from collections.abc import AsyncGenerator, Callable, Coroutine
4
5
  from datetime import tzinfo
5
6
  from types import SimpleNamespace
6
- from typing import TYPE_CHECKING, Any
7
+ from typing import Any
7
8
  from zoneinfo import ZoneInfo
8
9
 
9
- from digitalkin.models.module.module_helpers import ModuleHelpers
10
+ from digitalkin.logger import logger
10
11
  from digitalkin.models.module.tool_cache import ToolCache
11
12
  from digitalkin.services.agent.agent_strategy import AgentStrategy
12
13
  from digitalkin.services.communication.communication_strategy import CommunicationStrategy
@@ -18,9 +19,6 @@ from digitalkin.services.snapshot.snapshot_strategy import SnapshotStrategy
18
19
  from digitalkin.services.storage.storage_strategy import StorageStrategy
19
20
  from digitalkin.services.user_profile.user_profile_strategy import UserProfileStrategy
20
21
 
21
- if TYPE_CHECKING:
22
- from digitalkin.models.services.registry import ModuleInfo
23
-
24
22
 
25
23
  class Session(SimpleNamespace):
26
24
  """Session data container with mandatory setup_id and mission_id."""
@@ -101,7 +99,7 @@ class ModuleContext:
101
99
  session: Session
102
100
  callbacks: SimpleNamespace
103
101
  metadata: SimpleNamespace
104
- helpers: ModuleHelpers
102
+ helpers: SimpleNamespace
105
103
  state: SimpleNamespace = SimpleNamespace()
106
104
  tool_cache: ToolCache
107
105
 
@@ -140,7 +138,6 @@ class ModuleContext:
140
138
  callbacks: Functions allowing user to agent interaction.
141
139
  tool_cache: ToolCache with pre-resolved tool references from setup.
142
140
  """
143
- # Core services
144
141
  self.agent = agent
145
142
  self.communication = communication
146
143
  self.cost = cost
@@ -153,35 +150,151 @@ class ModuleContext:
153
150
 
154
151
  self.metadata = SimpleNamespace(**metadata)
155
152
  self.session = Session(**session)
156
- self.helpers = ModuleHelpers(context=self, **helpers)
153
+ self.helpers = SimpleNamespace(**helpers)
157
154
  self.callbacks = SimpleNamespace(**callbacks)
158
155
  self.tool_cache = tool_cache or ToolCache()
159
156
 
160
- def get_tool(self, slug: str) -> "ModuleInfo | None":
161
- """Get resolved tool info by slug.
157
+ async def call_module_by_id(
158
+ self,
159
+ module_id: str,
160
+ input_data: dict,
161
+ setup_id: str,
162
+ mission_id: str,
163
+ callback: Callable[[dict], Coroutine[Any, Any, None]] | None = None,
164
+ ) -> AsyncGenerator[dict, None]:
165
+ """Call a module by ID, discovering address/port from registry.
166
+
167
+ Args:
168
+ module_id: Module identifier to look up in registry.
169
+ input_data: Input data as dictionary.
170
+ setup_id: Setup configuration ID.
171
+ mission_id: Mission context ID.
172
+ callback: Optional callback for each response.
173
+
174
+ Yields:
175
+ Streaming responses from module as dictionaries.
176
+ """
177
+ module_info = self.registry.discover_by_id(module_id)
162
178
 
163
- Fast lookup from the pre-populated tool cache.
179
+ logger.debug(
180
+ "Calling module by ID",
181
+ extra={
182
+ "module_id": module_id,
183
+ "address": module_info.address,
184
+ "port": module_info.port,
185
+ },
186
+ )
187
+
188
+ async for response in self.communication.call_module(
189
+ module_address=module_info.address,
190
+ module_port=module_info.port,
191
+ input_data=input_data,
192
+ setup_id=setup_id,
193
+ mission_id=mission_id,
194
+ callback=callback,
195
+ ):
196
+ yield response
197
+
198
+ async def get_module_schemas_by_id(
199
+ self,
200
+ module_id: str,
201
+ *,
202
+ llm_format: bool = False,
203
+ ) -> dict[str, dict]:
204
+ """Get module schemas by ID, discovering address/port from registry.
164
205
 
165
206
  Args:
166
- slug: The tool slug to look up.
207
+ module_id: Module identifier to look up in registry.
208
+ llm_format: If True, return LLM-optimized schema format.
167
209
 
168
210
  Returns:
169
- ModuleInfo if found and valid, None otherwise.
211
+ Dictionary containing schemas: {"input": ..., "output": ..., "setup": ..., "secret": ...}
170
212
  """
171
- return self.tool_cache.get(slug)
213
+ module_info = self.registry.discover_by_id(module_id)
214
+
215
+ logger.debug(
216
+ "Getting module schemas by ID",
217
+ extra={
218
+ "module_id": module_id,
219
+ "address": module_info.address,
220
+ "port": module_info.port,
221
+ },
222
+ )
172
223
 
173
- def check_and_get_tool(self, slug: str) -> "ModuleInfo | None":
174
- """Check cache first, then query registry if not found.
224
+ return await self.communication.get_module_schemas(
225
+ module_address=module_info.address,
226
+ module_port=module_info.port,
227
+ llm_format=llm_format,
228
+ )
175
229
 
176
- This is the primary method for LLMs to discover tools. It:
177
- 1. Checks the pre-populated cache (fast path)
178
- 2. If not in cache, queries the registry
179
- 3. If found via registry, caches the result
230
+ async def create_openai_style_tool(self, tool_name: str) -> dict[str, Any] | None:
231
+ """Create OpenAI-style function calling schema for a tool.
232
+
233
+ Uses tool cache (fast path) with registry fallback. Fetches the tool's
234
+ input schema and wraps it in OpenAI function calling format.
180
235
 
181
236
  Args:
182
- slug: The tool slug to look up.
237
+ tool_name: Module ID to look up (checks cache first, then registry).
183
238
 
184
239
  Returns:
185
- ModuleInfo if found, None otherwise.
240
+ OpenAI-style tool schema if found, None otherwise.
186
241
  """
187
- return self.tool_cache.check_and_get(slug, self.registry)
242
+ module_info = self.tool_cache.get(tool_name, registry=self.registry)
243
+ if not module_info:
244
+ return None
245
+
246
+ schemas = await self.communication.get_module_schemas(
247
+ module_address=module_info.address,
248
+ module_port=module_info.port,
249
+ llm_format=True,
250
+ )
251
+
252
+ return {
253
+ "type": "function",
254
+ "function": {
255
+ "module_id": module_info.module_id,
256
+ "name": module_info.name or "undefined",
257
+ "description": module_info.documentation or "",
258
+ "parameters": schemas["input"],
259
+ },
260
+ }
261
+
262
+ def create_tool_function(
263
+ self,
264
+ module_id: str,
265
+ ) -> Callable[..., AsyncGenerator[dict, None]] | None:
266
+ """Create async generator function for a tool.
267
+
268
+ Returns an async generator that calls the remote tool module via gRPC
269
+ and yields each response as it arrives until end_of_stream or gRPC ends.
270
+
271
+ Args:
272
+ module_id: Module ID to look up (checks cache first, then registry).
273
+
274
+ Returns:
275
+ Async generator function if tool found, None otherwise.
276
+ """
277
+ module_info = self.tool_cache.get(module_id, registry=self.registry)
278
+ if not module_info:
279
+ return None
280
+
281
+ communication = self.communication
282
+ session = self.session
283
+ address = module_info.address
284
+ port = module_info.port
285
+
286
+ async def tool_function(**kwargs: Any) -> AsyncGenerator[dict, None]: # noqa: ANN401
287
+ wrapped_input = {"root": kwargs}
288
+ async for response in communication.call_module(
289
+ module_address=address,
290
+ module_port=port,
291
+ input_data=wrapped_input,
292
+ setup_id=session.setup_id,
293
+ mission_id=session.mission_id,
294
+ ):
295
+ yield response
296
+
297
+ tool_function.__name__ = module_info.name or module_info.module_id
298
+ tool_function.__doc__ = module_info.documentation or ""
299
+
300
+ return tool_function