digitalkin 0.3.2.dev2__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 (131) hide show
  1. base_server/__init__.py +1 -0
  2. base_server/mock/__init__.py +5 -0
  3. base_server/mock/mock_pb2.py +39 -0
  4. base_server/mock/mock_pb2_grpc.py +102 -0
  5. base_server/server_async_insecure.py +125 -0
  6. base_server/server_async_secure.py +143 -0
  7. base_server/server_sync_insecure.py +103 -0
  8. base_server/server_sync_secure.py +122 -0
  9. digitalkin/__init__.py +8 -0
  10. digitalkin/__version__.py +8 -0
  11. digitalkin/core/__init__.py +1 -0
  12. digitalkin/core/common/__init__.py +9 -0
  13. digitalkin/core/common/factories.py +156 -0
  14. digitalkin/core/job_manager/__init__.py +1 -0
  15. digitalkin/core/job_manager/base_job_manager.py +288 -0
  16. digitalkin/core/job_manager/single_job_manager.py +354 -0
  17. digitalkin/core/job_manager/taskiq_broker.py +311 -0
  18. digitalkin/core/job_manager/taskiq_job_manager.py +541 -0
  19. digitalkin/core/task_manager/__init__.py +1 -0
  20. digitalkin/core/task_manager/base_task_manager.py +539 -0
  21. digitalkin/core/task_manager/local_task_manager.py +108 -0
  22. digitalkin/core/task_manager/remote_task_manager.py +87 -0
  23. digitalkin/core/task_manager/surrealdb_repository.py +266 -0
  24. digitalkin/core/task_manager/task_executor.py +249 -0
  25. digitalkin/core/task_manager/task_session.py +406 -0
  26. digitalkin/grpc_servers/__init__.py +1 -0
  27. digitalkin/grpc_servers/_base_server.py +486 -0
  28. digitalkin/grpc_servers/module_server.py +208 -0
  29. digitalkin/grpc_servers/module_servicer.py +516 -0
  30. digitalkin/grpc_servers/utils/__init__.py +1 -0
  31. digitalkin/grpc_servers/utils/exceptions.py +29 -0
  32. digitalkin/grpc_servers/utils/grpc_client_wrapper.py +88 -0
  33. digitalkin/grpc_servers/utils/grpc_error_handler.py +53 -0
  34. digitalkin/grpc_servers/utils/utility_schema_extender.py +97 -0
  35. digitalkin/logger.py +157 -0
  36. digitalkin/mixins/__init__.py +19 -0
  37. digitalkin/mixins/base_mixin.py +10 -0
  38. digitalkin/mixins/callback_mixin.py +24 -0
  39. digitalkin/mixins/chat_history_mixin.py +110 -0
  40. digitalkin/mixins/cost_mixin.py +76 -0
  41. digitalkin/mixins/file_history_mixin.py +93 -0
  42. digitalkin/mixins/filesystem_mixin.py +46 -0
  43. digitalkin/mixins/logger_mixin.py +51 -0
  44. digitalkin/mixins/storage_mixin.py +79 -0
  45. digitalkin/models/__init__.py +8 -0
  46. digitalkin/models/core/__init__.py +1 -0
  47. digitalkin/models/core/job_manager_models.py +36 -0
  48. digitalkin/models/core/task_monitor.py +70 -0
  49. digitalkin/models/grpc_servers/__init__.py +1 -0
  50. digitalkin/models/grpc_servers/models.py +275 -0
  51. digitalkin/models/grpc_servers/types.py +24 -0
  52. digitalkin/models/module/__init__.py +25 -0
  53. digitalkin/models/module/module.py +40 -0
  54. digitalkin/models/module/module_context.py +149 -0
  55. digitalkin/models/module/module_types.py +393 -0
  56. digitalkin/models/module/utility.py +146 -0
  57. digitalkin/models/services/__init__.py +10 -0
  58. digitalkin/models/services/cost.py +54 -0
  59. digitalkin/models/services/registry.py +42 -0
  60. digitalkin/models/services/storage.py +44 -0
  61. digitalkin/modules/__init__.py +11 -0
  62. digitalkin/modules/_base_module.py +517 -0
  63. digitalkin/modules/archetype_module.py +23 -0
  64. digitalkin/modules/tool_module.py +23 -0
  65. digitalkin/modules/trigger_handler.py +48 -0
  66. digitalkin/modules/triggers/__init__.py +12 -0
  67. digitalkin/modules/triggers/healthcheck_ping_trigger.py +45 -0
  68. digitalkin/modules/triggers/healthcheck_services_trigger.py +63 -0
  69. digitalkin/modules/triggers/healthcheck_status_trigger.py +52 -0
  70. digitalkin/py.typed +0 -0
  71. digitalkin/services/__init__.py +30 -0
  72. digitalkin/services/agent/__init__.py +6 -0
  73. digitalkin/services/agent/agent_strategy.py +19 -0
  74. digitalkin/services/agent/default_agent.py +13 -0
  75. digitalkin/services/base_strategy.py +22 -0
  76. digitalkin/services/communication/__init__.py +7 -0
  77. digitalkin/services/communication/communication_strategy.py +76 -0
  78. digitalkin/services/communication/default_communication.py +101 -0
  79. digitalkin/services/communication/grpc_communication.py +223 -0
  80. digitalkin/services/cost/__init__.py +14 -0
  81. digitalkin/services/cost/cost_strategy.py +100 -0
  82. digitalkin/services/cost/default_cost.py +114 -0
  83. digitalkin/services/cost/grpc_cost.py +138 -0
  84. digitalkin/services/filesystem/__init__.py +7 -0
  85. digitalkin/services/filesystem/default_filesystem.py +417 -0
  86. digitalkin/services/filesystem/filesystem_strategy.py +252 -0
  87. digitalkin/services/filesystem/grpc_filesystem.py +317 -0
  88. digitalkin/services/identity/__init__.py +6 -0
  89. digitalkin/services/identity/default_identity.py +15 -0
  90. digitalkin/services/identity/identity_strategy.py +14 -0
  91. digitalkin/services/registry/__init__.py +27 -0
  92. digitalkin/services/registry/default_registry.py +141 -0
  93. digitalkin/services/registry/exceptions.py +47 -0
  94. digitalkin/services/registry/grpc_registry.py +306 -0
  95. digitalkin/services/registry/registry_models.py +43 -0
  96. digitalkin/services/registry/registry_strategy.py +98 -0
  97. digitalkin/services/services_config.py +200 -0
  98. digitalkin/services/services_models.py +65 -0
  99. digitalkin/services/setup/__init__.py +1 -0
  100. digitalkin/services/setup/default_setup.py +219 -0
  101. digitalkin/services/setup/grpc_setup.py +343 -0
  102. digitalkin/services/setup/setup_strategy.py +145 -0
  103. digitalkin/services/snapshot/__init__.py +6 -0
  104. digitalkin/services/snapshot/default_snapshot.py +39 -0
  105. digitalkin/services/snapshot/snapshot_strategy.py +30 -0
  106. digitalkin/services/storage/__init__.py +7 -0
  107. digitalkin/services/storage/default_storage.py +228 -0
  108. digitalkin/services/storage/grpc_storage.py +214 -0
  109. digitalkin/services/storage/storage_strategy.py +273 -0
  110. digitalkin/services/user_profile/__init__.py +12 -0
  111. digitalkin/services/user_profile/default_user_profile.py +55 -0
  112. digitalkin/services/user_profile/grpc_user_profile.py +69 -0
  113. digitalkin/services/user_profile/user_profile_strategy.py +40 -0
  114. digitalkin/utils/__init__.py +29 -0
  115. digitalkin/utils/arg_parser.py +92 -0
  116. digitalkin/utils/development_mode_action.py +51 -0
  117. digitalkin/utils/dynamic_schema.py +483 -0
  118. digitalkin/utils/llm_ready_schema.py +75 -0
  119. digitalkin/utils/package_discover.py +357 -0
  120. digitalkin-0.3.2.dev2.dist-info/METADATA +602 -0
  121. digitalkin-0.3.2.dev2.dist-info/RECORD +131 -0
  122. digitalkin-0.3.2.dev2.dist-info/WHEEL +5 -0
  123. digitalkin-0.3.2.dev2.dist-info/licenses/LICENSE +430 -0
  124. digitalkin-0.3.2.dev2.dist-info/top_level.txt +4 -0
  125. modules/__init__.py +0 -0
  126. modules/cpu_intensive_module.py +280 -0
  127. modules/dynamic_setup_module.py +338 -0
  128. modules/minimal_llm_module.py +347 -0
  129. modules/text_transform_module.py +203 -0
  130. services/filesystem_module.py +200 -0
  131. services/storage_module.py +206 -0
@@ -0,0 +1,406 @@
1
+ """Task session easing task lifecycle management."""
2
+
3
+ import asyncio
4
+ import datetime
5
+ from collections.abc import AsyncGenerator
6
+
7
+ from digitalkin.core.task_manager.surrealdb_repository import SurrealDBConnection
8
+ from digitalkin.logger import logger
9
+ from digitalkin.models.core.task_monitor import (
10
+ CancellationReason,
11
+ HeartbeatMessage,
12
+ SignalMessage,
13
+ SignalType,
14
+ TaskStatus,
15
+ )
16
+ from digitalkin.modules._base_module import BaseModule
17
+
18
+
19
+ class TaskSession:
20
+ """Task Session with lifecycle management.
21
+
22
+ The Session defined the whole lifecycle of a task as an epheneral context.
23
+ """
24
+
25
+ db: SurrealDBConnection
26
+ module: BaseModule
27
+
28
+ status: TaskStatus
29
+ signal_queue: AsyncGenerator | None
30
+
31
+ task_id: str
32
+ mission_id: str
33
+ signal_record_id: str | None
34
+ heartbeat_record_id: str | None
35
+
36
+ started_at: datetime.datetime | None
37
+ completed_at: datetime.datetime | None
38
+
39
+ is_cancelled: asyncio.Event
40
+ cancellation_reason: CancellationReason
41
+ _paused: asyncio.Event
42
+ _heartbeat_interval: datetime.timedelta
43
+ _last_heartbeat: datetime.datetime
44
+
45
+ def __init__(
46
+ self,
47
+ task_id: str,
48
+ mission_id: str,
49
+ db: SurrealDBConnection,
50
+ module: BaseModule,
51
+ heartbeat_interval: datetime.timedelta = datetime.timedelta(seconds=2),
52
+ queue_maxsize: int = 1000,
53
+ ) -> None:
54
+ """Initialize Task Session.
55
+
56
+ Args:
57
+ task_id: Unique task identifier
58
+ mission_id: Mission identifier
59
+ db: SurrealDB connection
60
+ module: Module instance
61
+ heartbeat_interval: Interval between heartbeats
62
+ queue_maxsize: Maximum size for the queue (0 = unlimited)
63
+ """
64
+ self.db = db
65
+ self.module = module
66
+
67
+ self.status = TaskStatus.PENDING
68
+ # Bounded queue to prevent unbounded memory growth (max 1000 items)
69
+ self.queue: asyncio.Queue = asyncio.Queue(maxsize=queue_maxsize)
70
+
71
+ self.task_id = task_id
72
+ self.mission_id = mission_id
73
+
74
+ self.heartbeat = None
75
+ self.started_at = None
76
+ self.completed_at = None
77
+
78
+ self.signal_record_id = None
79
+ self.heartbeat_record_id = None
80
+
81
+ self.is_cancelled = asyncio.Event()
82
+ self.cancellation_reason = CancellationReason.UNKNOWN
83
+ self._paused = asyncio.Event()
84
+ self._heartbeat_interval = heartbeat_interval
85
+
86
+ logger.info(
87
+ "TaskContext initialized for task: '%s'",
88
+ task_id,
89
+ extra={"task_id": task_id, "mission_id": mission_id, "heartbeat_interval": heartbeat_interval},
90
+ )
91
+
92
+ @property
93
+ def cancelled(self) -> bool:
94
+ """Task cancellation status."""
95
+ return self.is_cancelled.is_set()
96
+
97
+ @property
98
+ def paused(self) -> bool:
99
+ """Task paused status."""
100
+ return self._paused.is_set()
101
+
102
+ async def send_heartbeat(self) -> bool:
103
+ """Rate-limited heartbeat with connection resilience.
104
+
105
+ Returns:
106
+ bool: True if heartbeat was successful, False otherwise
107
+ """
108
+ heartbeat = HeartbeatMessage(
109
+ task_id=self.task_id,
110
+ mission_id=self.mission_id,
111
+ timestamp=datetime.datetime.now(datetime.timezone.utc),
112
+ )
113
+
114
+ if self.heartbeat_record_id is None:
115
+ try:
116
+ success = await self.db.create("heartbeats", heartbeat.model_dump())
117
+ if "code" not in success:
118
+ self.heartbeat_record_id = success.get("id") # type: ignore
119
+ self._last_heartbeat = heartbeat.timestamp
120
+ return True
121
+ except Exception as e:
122
+ logger.error(
123
+ "Heartbeat exception for task: '%s'",
124
+ self.task_id,
125
+ extra={"task_id": self.task_id, "error": str(e)},
126
+ exc_info=True,
127
+ )
128
+ logger.error(
129
+ "Initial heartbeat failed for task: '%s'",
130
+ self.task_id,
131
+ extra={"task_id": self.task_id},
132
+ )
133
+ return False
134
+
135
+ if (heartbeat.timestamp - self._last_heartbeat) < self._heartbeat_interval:
136
+ logger.debug(
137
+ "Heartbeat skipped due to rate limiting for task: '%s' | delta=%s",
138
+ self.task_id,
139
+ heartbeat.timestamp - self._last_heartbeat,
140
+ )
141
+ return True
142
+
143
+ try:
144
+ success = await self.db.merge("heartbeats", self.heartbeat_record_id, heartbeat.model_dump())
145
+ if "code" not in success:
146
+ self._last_heartbeat = heartbeat.timestamp
147
+ return True
148
+ except Exception as e:
149
+ logger.error(
150
+ "Heartbeat exception for task: '%s'",
151
+ self.task_id,
152
+ extra={"task_id": self.task_id, "error": str(e)},
153
+ exc_info=True,
154
+ )
155
+ logger.warning(
156
+ "Heartbeat failed for task: '%s'",
157
+ self.task_id,
158
+ extra={"task_id": self.task_id},
159
+ )
160
+ return False
161
+
162
+ async def generate_heartbeats(self) -> None:
163
+ """Periodic heartbeat generator with cancellation support."""
164
+ logger.debug(
165
+ "Heartbeat generator started for task: '%s'",
166
+ self.task_id,
167
+ extra={"task_id": self.task_id, "mission_id": self.mission_id},
168
+ )
169
+ while not self.cancelled:
170
+ logger.debug(
171
+ "Heartbeat tick for task: '%s', cancelled=%s",
172
+ self.task_id,
173
+ self.cancelled,
174
+ extra={"task_id": self.task_id, "mission_id": self.mission_id},
175
+ )
176
+ success = await self.send_heartbeat()
177
+ if not success:
178
+ logger.error(
179
+ "Heartbeat failed, cancelling task: '%s'",
180
+ self.task_id,
181
+ extra={"task_id": self.task_id, "mission_id": self.mission_id},
182
+ )
183
+ await self._handle_cancel(CancellationReason.HEARTBEAT_FAILURE)
184
+ break
185
+ await asyncio.sleep(self._heartbeat_interval.total_seconds())
186
+
187
+ async def wait_if_paused(self) -> None:
188
+ """Block execution if task is paused."""
189
+ if self._paused.is_set():
190
+ logger.info(
191
+ "Task paused, waiting for resume: '%s'",
192
+ self.task_id,
193
+ extra={"task_id": self.task_id},
194
+ )
195
+ await self._paused.wait()
196
+
197
+ async def listen_signals(self) -> None: # noqa: C901
198
+ """Enhanced signal listener with comprehensive handling.
199
+
200
+ Raises:
201
+ CancelledError: Asyncio when task cancelling
202
+ """
203
+ logger.info(
204
+ "Signal listener started for task: '%s'",
205
+ self.task_id,
206
+ extra={"task_id": self.task_id},
207
+ )
208
+ if self.signal_record_id is None:
209
+ self.signal_record_id = (await self.db.select_by_task_id("tasks", self.task_id)).get("id")
210
+
211
+ live_id, live_signals = await self.db.start_live("tasks")
212
+ try:
213
+ async for signal in live_signals:
214
+ logger.debug("Signal received for task '%s': %s", self.task_id, signal)
215
+ if self.cancelled:
216
+ break
217
+
218
+ if signal is None or signal["id"] == self.signal_record_id or "payload" not in signal:
219
+ continue
220
+
221
+ if signal["action"] == "cancel":
222
+ await self._handle_cancel(CancellationReason.SIGNAL)
223
+ elif signal["action"] == "pause":
224
+ await self._handle_pause()
225
+ elif signal["action"] == "resume":
226
+ await self._handle_resume()
227
+ elif signal["action"] == "status":
228
+ await self._handle_status_request()
229
+
230
+ except asyncio.CancelledError:
231
+ logger.debug(
232
+ "Signal listener cancelled for task: '%s'",
233
+ self.task_id,
234
+ extra={"task_id": self.task_id},
235
+ )
236
+ raise
237
+ except Exception as e:
238
+ logger.error(
239
+ "Signal listener fatal error for task: '%s'",
240
+ self.task_id,
241
+ extra={"task_id": self.task_id, "error": str(e)},
242
+ exc_info=True,
243
+ )
244
+ finally:
245
+ await self.db.stop_live(live_id)
246
+ logger.info(
247
+ "Signal listener stopped for task: '%s'",
248
+ self.task_id,
249
+ extra={"task_id": self.task_id},
250
+ )
251
+
252
+ async def _handle_cancel(self, reason: CancellationReason = CancellationReason.UNKNOWN) -> None:
253
+ """Idempotent cancellation with acknowledgment and reason tracking.
254
+
255
+ Args:
256
+ reason: The reason for cancellation (signal, heartbeat failure, cleanup, etc.)
257
+ """
258
+ if self.is_cancelled.is_set():
259
+ logger.debug(
260
+ "Cancel ignored - task already cancelled: '%s' (existing reason: %s, new reason: %s)",
261
+ self.task_id,
262
+ self.cancellation_reason.value,
263
+ reason.value,
264
+ extra={
265
+ "task_id": self.task_id,
266
+ "mission_id": self.mission_id,
267
+ "existing_reason": self.cancellation_reason.value,
268
+ "new_reason": reason.value,
269
+ },
270
+ )
271
+ return
272
+
273
+ self.cancellation_reason = reason
274
+ self.status = TaskStatus.CANCELLED
275
+ self.is_cancelled.set()
276
+
277
+ # Log with appropriate level based on reason
278
+ if reason in {CancellationReason.SUCCESS_CLEANUP, CancellationReason.FAILURE_CLEANUP}:
279
+ logger.debug(
280
+ "Task cancelled (cleanup): '%s', reason: %s",
281
+ self.task_id,
282
+ reason.value,
283
+ extra={
284
+ "task_id": self.task_id,
285
+ "mission_id": self.mission_id,
286
+ "cancellation_reason": reason.value,
287
+ },
288
+ )
289
+ else:
290
+ logger.info(
291
+ "Task cancelled: '%s', reason: %s",
292
+ self.task_id,
293
+ reason.value,
294
+ extra={
295
+ "task_id": self.task_id,
296
+ "mission_id": self.mission_id,
297
+ "cancellation_reason": reason.value,
298
+ },
299
+ )
300
+
301
+ # Resume if paused so cancellation can proceed
302
+ if self._paused.is_set():
303
+ self._paused.set()
304
+
305
+ await self.db.update(
306
+ "tasks",
307
+ self.signal_record_id, # type: ignore
308
+ SignalMessage(
309
+ task_id=self.task_id,
310
+ mission_id=self.mission_id,
311
+ action=SignalType.ACK_CANCEL,
312
+ status=self.status,
313
+ ).model_dump(),
314
+ )
315
+
316
+ async def _handle_pause(self) -> None:
317
+ """Pause task execution."""
318
+ if not self._paused.is_set():
319
+ logger.info(
320
+ "Pausing task: '%s'",
321
+ self.task_id,
322
+ extra={"task_id": self.task_id},
323
+ )
324
+ self._paused.set()
325
+
326
+ await self.db.update(
327
+ "tasks",
328
+ self.signal_record_id, # type: ignore
329
+ SignalMessage(
330
+ task_id=self.task_id,
331
+ mission_id=self.mission_id,
332
+ action=SignalType.ACK_PAUSE,
333
+ status=self.status,
334
+ ).model_dump(),
335
+ )
336
+
337
+ async def _handle_resume(self) -> None:
338
+ """Resume paused task."""
339
+ if self._paused.is_set():
340
+ logger.info(
341
+ "Resuming task: '%s'",
342
+ self.task_id,
343
+ extra={"task_id": self.task_id},
344
+ )
345
+ self._paused.clear()
346
+
347
+ await self.db.update(
348
+ "tasks",
349
+ self.signal_record_id, # type: ignore
350
+ SignalMessage(
351
+ task_id=self.task_id,
352
+ mission_id=self.mission_id,
353
+ action=SignalType.ACK_RESUME,
354
+ status=self.status,
355
+ ).model_dump(),
356
+ )
357
+
358
+ async def _handle_status_request(self) -> None:
359
+ """Send current task status."""
360
+ await self.db.update(
361
+ "tasks",
362
+ self.signal_record_id, # type: ignore
363
+ SignalMessage(
364
+ mission_id=self.mission_id,
365
+ task_id=self.task_id,
366
+ status=self.status,
367
+ action=SignalType.ACK_STATUS,
368
+ ).model_dump(),
369
+ )
370
+
371
+ logger.debug(
372
+ "Status report sent for task: '%s'",
373
+ self.task_id,
374
+ extra={"task_id": self.task_id},
375
+ )
376
+
377
+ async def cleanup(self) -> None:
378
+ """Clean up task session resources.
379
+
380
+ This includes:
381
+ - Clearing queue to free memory
382
+ - Stopping module
383
+ - Closing database connection
384
+ - Clearing module reference
385
+ """
386
+ # Clear queue to free memory
387
+ try:
388
+ while not self.queue.empty():
389
+ self.queue.get_nowait()
390
+ except asyncio.QueueEmpty:
391
+ pass
392
+
393
+ # Stop module
394
+ try:
395
+ await self.module.stop()
396
+ except Exception:
397
+ logger.exception(
398
+ "Error stopping module during cleanup",
399
+ extra={"mission_id": self.mission_id, "task_id": self.task_id},
400
+ )
401
+
402
+ # Close DB connection (kills all live queries)
403
+ await self.db.close()
404
+
405
+ # Clear module reference to allow garbage collection
406
+ self.module = None # type: ignore
@@ -0,0 +1 @@
1
+ """This package contains the gRPC server and client implementations."""