digitalkin 0.2.25rc1__py3-none-any.whl → 0.3.0__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 (39) hide show
  1. digitalkin/__version__.py +1 -1
  2. digitalkin/grpc_servers/_base_server.py +1 -1
  3. digitalkin/grpc_servers/module_server.py +26 -42
  4. digitalkin/grpc_servers/module_servicer.py +30 -24
  5. digitalkin/grpc_servers/utils/grpc_client_wrapper.py +3 -3
  6. digitalkin/grpc_servers/utils/models.py +1 -1
  7. digitalkin/logger.py +60 -23
  8. digitalkin/mixins/__init__.py +19 -0
  9. digitalkin/mixins/base_mixin.py +10 -0
  10. digitalkin/mixins/callback_mixin.py +24 -0
  11. digitalkin/mixins/chat_history_mixin.py +108 -0
  12. digitalkin/mixins/cost_mixin.py +76 -0
  13. digitalkin/mixins/file_history_mixin.py +99 -0
  14. digitalkin/mixins/filesystem_mixin.py +47 -0
  15. digitalkin/mixins/logger_mixin.py +59 -0
  16. digitalkin/mixins/storage_mixin.py +79 -0
  17. digitalkin/models/module/__init__.py +2 -0
  18. digitalkin/models/module/module.py +9 -1
  19. digitalkin/models/module/module_context.py +90 -6
  20. digitalkin/models/module/module_types.py +5 -5
  21. digitalkin/models/module/task_monitor.py +51 -0
  22. digitalkin/models/services/__init__.py +9 -0
  23. digitalkin/models/services/storage.py +39 -5
  24. digitalkin/modules/_base_module.py +105 -74
  25. digitalkin/modules/job_manager/base_job_manager.py +12 -8
  26. digitalkin/modules/job_manager/single_job_manager.py +84 -78
  27. digitalkin/modules/job_manager/surrealdb_repository.py +225 -0
  28. digitalkin/modules/job_manager/task_manager.py +391 -0
  29. digitalkin/modules/job_manager/task_session.py +276 -0
  30. digitalkin/modules/job_manager/taskiq_job_manager.py +2 -2
  31. digitalkin/modules/tool_module.py +10 -2
  32. digitalkin/modules/trigger_handler.py +7 -6
  33. digitalkin/services/cost/__init__.py +9 -2
  34. digitalkin/services/storage/grpc_storage.py +1 -1
  35. {digitalkin-0.2.25rc1.dist-info → digitalkin-0.3.0.dist-info}/METADATA +18 -18
  36. {digitalkin-0.2.25rc1.dist-info → digitalkin-0.3.0.dist-info}/RECORD +39 -26
  37. {digitalkin-0.2.25rc1.dist-info → digitalkin-0.3.0.dist-info}/WHEEL +0 -0
  38. {digitalkin-0.2.25rc1.dist-info → digitalkin-0.3.0.dist-info}/licenses/LICENSE +0 -0
  39. {digitalkin-0.2.25rc1.dist-info → digitalkin-0.3.0.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,11 @@
1
1
  """BaseModule is the abstract base for all modules in the DigitalKin SDK."""
2
2
 
3
3
  import asyncio
4
- import contextlib
5
4
  import json
6
5
  from abc import ABC, abstractmethod
7
6
  from collections.abc import Callable, Coroutine
8
7
  from typing import Any, ClassVar, Generic
9
8
 
10
- from pydantic import BaseModel
11
-
12
9
  from digitalkin.logger import logger
13
10
  from digitalkin.models.module import (
14
11
  InputModelT,
@@ -17,28 +14,14 @@ from digitalkin.models.module import (
17
14
  SecretModelT,
18
15
  SetupModelT,
19
16
  )
17
+ from digitalkin.models.module.module import ModuleCodeModel
20
18
  from digitalkin.models.module.module_context import ModuleContext
21
19
  from digitalkin.modules.trigger_handler import TriggerHandler
22
- from digitalkin.services.agent.agent_strategy import AgentStrategy
23
- from digitalkin.services.cost.cost_strategy import CostStrategy
24
- from digitalkin.services.filesystem.filesystem_strategy import FilesystemStrategy
25
- from digitalkin.services.identity.identity_strategy import IdentityStrategy
26
- from digitalkin.services.registry.registry_strategy import RegistryStrategy
27
20
  from digitalkin.services.services_config import ServicesConfig, ServicesStrategy
28
- from digitalkin.services.snapshot.snapshot_strategy import SnapshotStrategy
29
- from digitalkin.services.storage.storage_strategy import StorageStrategy
30
21
  from digitalkin.utils.llm_ready_schema import llm_ready_schema
31
22
  from digitalkin.utils.package_discover import ModuleDiscoverer
32
23
 
33
24
 
34
- class ModuleCodeModel(BaseModel):
35
- """typed error/code model."""
36
-
37
- code: str
38
- message: str
39
- short_description: str
40
-
41
-
42
25
  class BaseModule( # noqa: PLR0904
43
26
  ABC,
44
27
  Generic[
@@ -67,33 +50,35 @@ class BaseModule( # noqa: PLR0904
67
50
  services_config_params: ClassVar[dict[str, dict[str, Any | None] | None]]
68
51
  services_config: ServicesConfig
69
52
 
70
- # services list
71
- agent: AgentStrategy
72
- cost: CostStrategy
73
- filesystem: FilesystemStrategy
74
- identity: IdentityStrategy
75
- registry: RegistryStrategy
76
- snapshot: SnapshotStrategy
77
- storage: StorageStrategy
78
-
79
53
  # runtime params
80
54
  job_id: str
81
55
  mission_id: str
82
56
  setup_id: str
83
57
  setup_version_id: str
84
- _status: ModuleStatus
85
- _task: asyncio.Task | None
86
58
 
87
- def _init_strategies(self) -> None:
88
- """Initialize the services configuration."""
89
- for service_name in self.services_config.valid_strategy_names():
90
- service = self.services_config.init_strategy(
59
+ def _init_strategies(self) -> dict[str, Any]:
60
+ """Initialize the services configuration.
61
+
62
+ Returns:
63
+ dict of services with name: Strategy
64
+ agent: AgentStrategy
65
+ cost: CostStrategy
66
+ filesystem: FilesystemStrategy
67
+ identity: IdentityStrategy
68
+ registry: RegistryStrategy
69
+ snapshot: SnapshotStrategy
70
+ storage: StorageStrategy
71
+ """
72
+ logger.debug("Service initialisation: %s", self.services_config_strategies.keys())
73
+ return {
74
+ service_name: self.services_config.init_strategy(
91
75
  service_name,
92
76
  self.mission_id,
93
77
  self.setup_id,
94
78
  self.setup_version_id,
95
79
  )
96
- setattr(self, service_name, service)
80
+ for service_name in self.services_config.valid_strategy_names()
81
+ }
97
82
 
98
83
  def __init__(
99
84
  self,
@@ -110,15 +95,16 @@ class BaseModule( # noqa: PLR0904
110
95
  # SetupVersion reference needed for the precise Kin scope as the cost
111
96
  self.setup_version_id: str = setup_version_id
112
97
  self._status = ModuleStatus.CREATED
113
- self._task: asyncio.Task | None = None
114
- # Initialize services configuration
115
- self._init_strategies()
116
98
 
117
99
  # Initialize minimum context
118
100
  self.context = ModuleContext(
119
- storage=self.storage,
120
- cost=self.cost,
121
- filesystem=self.filesystem,
101
+ # Initialize services configuration
102
+ **self._init_strategies(),
103
+ session={
104
+ "mission_id": mission_id,
105
+ "setup_version_id": setup_version_id,
106
+ "job_id": job_id,
107
+ },
122
108
  )
123
109
 
124
110
  @property
@@ -307,7 +293,11 @@ class BaseModule( # noqa: PLR0904
307
293
  """
308
294
  return cls.triggers_discoverer.register_trigger(handler_cls)
309
295
 
310
- async def run_config_setup(self, config_setup_data: SetupModelT) -> SetupModelT: # noqa: PLR6301
296
+ async def run_config_setup( # noqa: PLR6301
297
+ self,
298
+ context: ModuleContext, # noqa: ARG002
299
+ config_setup_data: SetupModelT,
300
+ ) -> SetupModelT:
311
301
  """Run config setup the module.
312
302
 
313
303
  The config setup is used to initialize the setup with configuration data.
@@ -323,7 +313,7 @@ class BaseModule( # noqa: PLR0904
323
313
  return config_setup_data
324
314
 
325
315
  @abstractmethod
326
- async def initialize(self, setup_data: SetupModelT) -> None:
316
+ async def initialize(self, context: ModuleContext, setup_data: SetupModelT) -> None:
327
317
  """Initialize the module."""
328
318
  raise NotImplementedError
329
319
 
@@ -331,7 +321,6 @@ class BaseModule( # noqa: PLR0904
331
321
  self,
332
322
  input_data: InputModelT,
333
323
  setup_data: SetupModelT,
334
- callback: Callable[[OutputModelT], Coroutine[Any, Any, None]],
335
324
  ) -> None:
336
325
  """Run the module with the given input and setup data.
337
326
 
@@ -360,7 +349,6 @@ class BaseModule( # noqa: PLR0904
360
349
  await handler_instance.handle(
361
350
  input_instance.root,
362
351
  setup_data,
363
- callback,
364
352
  self.context,
365
353
  )
366
354
 
@@ -373,7 +361,6 @@ class BaseModule( # noqa: PLR0904
373
361
  self,
374
362
  input_data: InputModelT,
375
363
  setup_data: SetupModelT,
376
- callback: Callable[[OutputModelT], Coroutine[Any, Any, None]],
377
364
  ) -> None:
378
365
  """Run the module lifecycle.
379
366
 
@@ -381,19 +368,53 @@ class BaseModule( # noqa: PLR0904
381
368
  asyncio.CancelledError: If the module is cancelled
382
369
  """
383
370
  try:
384
- logger.info("Starting module %s", self.name)
385
- await self.run(input_data, setup_data, callback)
386
- logger.info("Module %s finished", self.name)
371
+ logger.info(
372
+ "Starting module %s",
373
+ self.name,
374
+ extra={
375
+ "mission_id": self.mission_id,
376
+ "setup_id": self.setup_id,
377
+ "setup_version_id": self.setup_version_id,
378
+ "job_id": self.job_id,
379
+ },
380
+ )
381
+ await self.run(input_data, setup_data)
382
+ logger.info(
383
+ "Module %s finished",
384
+ self.name,
385
+ extra={
386
+ "mission_id": self.mission_id,
387
+ "setup_id": self.setup_id,
388
+ "setup_version_id": self.setup_version_id,
389
+ "job_id": self.job_id,
390
+ },
391
+ )
387
392
  except asyncio.CancelledError:
388
393
  self._status = ModuleStatus.CANCELLED
389
- logger.error(f"Module {self.name} cancelled")
394
+ logger.error(
395
+ "Module %s cancelled",
396
+ self.name,
397
+ extra={
398
+ "mission_id": self.mission_id,
399
+ "setup_id": self.setup_id,
400
+ "setup_version_id": self.setup_version_id,
401
+ "job_id": self.job_id,
402
+ },
403
+ )
390
404
  except Exception:
391
405
  self._status = ModuleStatus.FAILED
392
- logger.exception("Error inside module %s", self.name)
406
+ logger.exception(
407
+ "Error inside module %s",
408
+ self.name,
409
+ extra={
410
+ "mission_id": self.mission_id,
411
+ "setup_id": self.setup_id,
412
+ "setup_version_id": self.setup_version_id,
413
+ "job_id": self.job_id,
414
+ },
415
+ )
393
416
  else:
394
417
  self._status = ModuleStatus.STOPPING
395
- finally:
396
- await self.stop()
397
418
 
398
419
  async def start(
399
420
  self,
@@ -404,15 +425,17 @@ class BaseModule( # noqa: PLR0904
404
425
  ) -> None:
405
426
  """Start the module."""
406
427
  try:
407
- logger.debug("Inititalize module")
408
- await self.initialize(setup_data=setup_data)
428
+ self.context.callbacks.logger = logger
429
+ self.context.callbacks.send_message = callback
430
+ logger.info(f"Inititalize module {self.job_id}")
431
+ await self.initialize(self.context, setup_data)
409
432
  except Exception as e:
410
433
  self._status = ModuleStatus.FAILED
411
434
  short_description = "Error initializing module"
412
435
  logger.exception("%s: %s", short_description, e)
413
436
  await callback(
414
437
  ModuleCodeModel(
415
- code=str(self._status),
438
+ code="Error",
416
439
  short_description=short_description,
417
440
  message=str(e),
418
441
  )
@@ -425,32 +448,22 @@ class BaseModule( # noqa: PLR0904
425
448
  try:
426
449
  logger.debug("Init the discovered input handlers.")
427
450
  self.triggers_discoverer.init_handlers(self.context)
428
- logger.debug("Run lifecycle")
429
- self._status = ModuleStatus.RUNNING
430
- self._task = asyncio.create_task(
431
- self._run_lifecycle(input_data, setup_data, callback),
432
- name="module_lifecycle",
433
- )
434
- if done_callback is not None:
435
- self._task.add_done_callback(done_callback)
451
+ logger.debug(f"Run lifecycle {self.job_id}")
452
+ await self._run_lifecycle(input_data, setup_data)
436
453
  except Exception:
437
454
  self._status = ModuleStatus.FAILED
438
455
  logger.exception("Error during module lifecyle")
456
+ finally:
457
+ await self.stop()
439
458
 
440
459
  async def stop(self) -> None:
441
460
  """Stop the module."""
442
- logger.info("Stopping module %s with status %s", self.name, self._status)
443
- if self._status not in {ModuleStatus.RUNNING, ModuleStatus.STOPPING}:
444
- return
445
-
461
+ logger.info("Stopping module %s | job_id=%s", self.name, self.job_id)
446
462
  try:
447
463
  self._status = ModuleStatus.STOPPING
448
- if self._task and not self._task.done():
449
- self._task.cancel()
450
- with contextlib.suppress(asyncio.CancelledError):
451
- await self._task
452
464
  logger.debug("Module %s stopped", self.name)
453
465
  await self.cleanup()
466
+ await self.context.callbacks.send_message(ModuleCodeModel(code="__END_OF_STREAM__"))
454
467
  self._status = ModuleStatus.STOPPED
455
468
  logger.debug("Module %s cleaned", self.name)
456
469
  except Exception:
@@ -464,14 +477,32 @@ class BaseModule( # noqa: PLR0904
464
477
  ) -> None:
465
478
  """Start the module."""
466
479
  try:
467
- logger.info("Run Config Setup lifecycle")
480
+ logger.info(
481
+ "Run Config Setup lifecycle",
482
+ extra={
483
+ "mission_id": self.mission_id,
484
+ "setup_id": self.setup_id,
485
+ "setup_version_id": self.setup_version_id,
486
+ "job_id": self.job_id,
487
+ },
488
+ )
468
489
  self._status = ModuleStatus.RUNNING
469
- content = await self.run_config_setup(config_setup_data)
490
+ self.context.callbacks.set_config_setup = callback
491
+ content = await self.run_config_setup(self.context, config_setup_data)
470
492
 
471
493
  wrapper = config_setup_data.model_dump()
472
494
  wrapper["content"] = content.model_dump()
473
495
  await callback(self.create_setup_model(wrapper))
474
496
  self._status = ModuleStatus.STOPPING
475
497
  except Exception:
498
+ logger.error("Error during module lifecyle")
476
499
  self._status = ModuleStatus.FAILED
477
- logger.exception("Error during module lifecyle")
500
+ logger.exception(
501
+ "Error during module lifecyle",
502
+ extra={
503
+ "mission_id": self.mission_id,
504
+ "setup_id": self.setup_id,
505
+ "setup_version_id": self.setup_version_id,
506
+ "job_id": self.job_id,
507
+ },
508
+ )
@@ -5,17 +5,18 @@ from collections.abc import AsyncGenerator, AsyncIterator, Callable, Coroutine
5
5
  from contextlib import asynccontextmanager
6
6
  from typing import Any, Generic
7
7
 
8
- from digitalkin.models import ModuleStatus
9
8
  from digitalkin.models.module import InputModelT, OutputModelT, SetupModelT
9
+ from digitalkin.models.module.module import ModuleCodeModel
10
+ from digitalkin.models.module.task_monitor import TaskStatus
10
11
  from digitalkin.modules._base_module import BaseModule
11
12
  from digitalkin.services.services_config import ServicesConfig
12
13
  from digitalkin.services.services_models import ServicesMode
13
14
 
14
15
 
15
- class BaseJobManager(abc.ABC, Generic[InputModelT, SetupModelT]):
16
+ class BaseJobManager(abc.ABC, Generic[InputModelT, SetupModelT, OutputModelT]):
16
17
  """Abstract base class for managing background module jobs."""
17
18
 
18
- async def _start(self) -> None:
19
+ async def start(self) -> None:
19
20
  """Start the job manager.
20
21
 
21
22
  This method initializes any necessary resources or configurations
@@ -24,8 +25,8 @@ class BaseJobManager(abc.ABC, Generic[InputModelT, SetupModelT]):
24
25
 
25
26
  @staticmethod
26
27
  async def job_specific_callback(
27
- callback: Callable[[str, OutputModelT], Coroutine[Any, Any, None]], job_id: str
28
- ) -> Callable[[OutputModelT], Coroutine[Any, Any, None]]:
28
+ callback: Callable[[str, OutputModelT | ModuleCodeModel], Coroutine[Any, Any, None]], job_id: str
29
+ ) -> Callable[[OutputModelT | ModuleCodeModel], Coroutine[Any, Any, None]]:
29
30
  """Generate a job-specific callback function.
30
31
 
31
32
  Args:
@@ -36,7 +37,7 @@ class BaseJobManager(abc.ABC, Generic[InputModelT, SetupModelT]):
36
37
  Callable: A wrapped callback function that includes the job ID.
37
38
  """
38
39
 
39
- def callback_wrapper(output_data: OutputModelT) -> Coroutine[Any, Any, None]:
40
+ def callback_wrapper(output_data: OutputModelT | ModuleCodeModel) -> Coroutine[Any, Any, None]:
40
41
  """Wrapper for the callback function.
41
42
 
42
43
  Args:
@@ -53,12 +54,14 @@ class BaseJobManager(abc.ABC, Generic[InputModelT, SetupModelT]):
53
54
  self,
54
55
  module_class: type[BaseModule],
55
56
  services_mode: ServicesMode,
57
+ **kwargs, # noqa: ANN003
56
58
  ) -> None:
57
59
  """Initialize the job manager.
58
60
 
59
61
  Args:
60
62
  module_class: The class of the module to be managed.
61
63
  services_mode: The mode of operation for the services (e.g., ASYNC or SYNC).
64
+ **kwargs: Additional keyword arguments for the job manager.
62
65
  """
63
66
  self.module_class = module_class
64
67
 
@@ -68,6 +71,7 @@ class BaseJobManager(abc.ABC, Generic[InputModelT, SetupModelT]):
68
71
  mode=services_mode,
69
72
  )
70
73
  setattr(self.module_class, "services_config", services_config)
74
+ super().__init__(**kwargs)
71
75
 
72
76
  @abc.abstractmethod # type: ignore
73
77
  @asynccontextmanager # type: ignore
@@ -156,14 +160,14 @@ class BaseJobManager(abc.ABC, Generic[InputModelT, SetupModelT]):
156
160
  """
157
161
 
158
162
  @abc.abstractmethod
159
- async def get_module_status(self, job_id: str) -> ModuleStatus | None:
163
+ async def get_module_status(self, job_id: str) -> TaskStatus:
160
164
  """Retrieve the status of a module job.
161
165
 
162
166
  Args:
163
167
  job_id: The unique identifier of the job.
164
168
 
165
169
  Returns:
166
- ModuleStatus | None: The status of the job, or None if the job does not exist.
170
+ ModuleStatu: The status of the job.
167
171
  """
168
172
 
169
173
  @abc.abstractmethod