dtSpark 1.0.4__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 (96) hide show
  1. dtSpark/__init__.py +0 -0
  2. dtSpark/_description.txt +1 -0
  3. dtSpark/_full_name.txt +1 -0
  4. dtSpark/_licence.txt +21 -0
  5. dtSpark/_metadata.yaml +6 -0
  6. dtSpark/_name.txt +1 -0
  7. dtSpark/_version.txt +1 -0
  8. dtSpark/aws/__init__.py +7 -0
  9. dtSpark/aws/authentication.py +296 -0
  10. dtSpark/aws/bedrock.py +578 -0
  11. dtSpark/aws/costs.py +318 -0
  12. dtSpark/aws/pricing.py +580 -0
  13. dtSpark/cli_interface.py +2645 -0
  14. dtSpark/conversation_manager.py +3050 -0
  15. dtSpark/core/__init__.py +12 -0
  16. dtSpark/core/application.py +3355 -0
  17. dtSpark/core/context_compaction.py +735 -0
  18. dtSpark/daemon/__init__.py +104 -0
  19. dtSpark/daemon/__main__.py +10 -0
  20. dtSpark/daemon/action_monitor.py +213 -0
  21. dtSpark/daemon/daemon_app.py +730 -0
  22. dtSpark/daemon/daemon_manager.py +289 -0
  23. dtSpark/daemon/execution_coordinator.py +194 -0
  24. dtSpark/daemon/pid_file.py +169 -0
  25. dtSpark/database/__init__.py +482 -0
  26. dtSpark/database/autonomous_actions.py +1191 -0
  27. dtSpark/database/backends.py +329 -0
  28. dtSpark/database/connection.py +122 -0
  29. dtSpark/database/conversations.py +520 -0
  30. dtSpark/database/credential_prompt.py +218 -0
  31. dtSpark/database/files.py +205 -0
  32. dtSpark/database/mcp_ops.py +355 -0
  33. dtSpark/database/messages.py +161 -0
  34. dtSpark/database/schema.py +673 -0
  35. dtSpark/database/tool_permissions.py +186 -0
  36. dtSpark/database/usage.py +167 -0
  37. dtSpark/files/__init__.py +4 -0
  38. dtSpark/files/manager.py +322 -0
  39. dtSpark/launch.py +39 -0
  40. dtSpark/limits/__init__.py +10 -0
  41. dtSpark/limits/costs.py +296 -0
  42. dtSpark/limits/tokens.py +342 -0
  43. dtSpark/llm/__init__.py +17 -0
  44. dtSpark/llm/anthropic_direct.py +446 -0
  45. dtSpark/llm/base.py +146 -0
  46. dtSpark/llm/context_limits.py +438 -0
  47. dtSpark/llm/manager.py +177 -0
  48. dtSpark/llm/ollama.py +578 -0
  49. dtSpark/mcp_integration/__init__.py +5 -0
  50. dtSpark/mcp_integration/manager.py +653 -0
  51. dtSpark/mcp_integration/tool_selector.py +225 -0
  52. dtSpark/resources/config.yaml.template +631 -0
  53. dtSpark/safety/__init__.py +22 -0
  54. dtSpark/safety/llm_service.py +111 -0
  55. dtSpark/safety/patterns.py +229 -0
  56. dtSpark/safety/prompt_inspector.py +442 -0
  57. dtSpark/safety/violation_logger.py +346 -0
  58. dtSpark/scheduler/__init__.py +20 -0
  59. dtSpark/scheduler/creation_tools.py +599 -0
  60. dtSpark/scheduler/execution_queue.py +159 -0
  61. dtSpark/scheduler/executor.py +1152 -0
  62. dtSpark/scheduler/manager.py +395 -0
  63. dtSpark/tools/__init__.py +4 -0
  64. dtSpark/tools/builtin.py +833 -0
  65. dtSpark/web/__init__.py +20 -0
  66. dtSpark/web/auth.py +152 -0
  67. dtSpark/web/dependencies.py +37 -0
  68. dtSpark/web/endpoints/__init__.py +17 -0
  69. dtSpark/web/endpoints/autonomous_actions.py +1125 -0
  70. dtSpark/web/endpoints/chat.py +621 -0
  71. dtSpark/web/endpoints/conversations.py +353 -0
  72. dtSpark/web/endpoints/main_menu.py +547 -0
  73. dtSpark/web/endpoints/streaming.py +421 -0
  74. dtSpark/web/server.py +578 -0
  75. dtSpark/web/session.py +167 -0
  76. dtSpark/web/ssl_utils.py +195 -0
  77. dtSpark/web/static/css/dark-theme.css +427 -0
  78. dtSpark/web/static/js/actions.js +1101 -0
  79. dtSpark/web/static/js/chat.js +614 -0
  80. dtSpark/web/static/js/main.js +496 -0
  81. dtSpark/web/static/js/sse-client.js +242 -0
  82. dtSpark/web/templates/actions.html +408 -0
  83. dtSpark/web/templates/base.html +93 -0
  84. dtSpark/web/templates/chat.html +814 -0
  85. dtSpark/web/templates/conversations.html +350 -0
  86. dtSpark/web/templates/goodbye.html +81 -0
  87. dtSpark/web/templates/login.html +90 -0
  88. dtSpark/web/templates/main_menu.html +983 -0
  89. dtSpark/web/templates/new_conversation.html +191 -0
  90. dtSpark/web/web_interface.py +137 -0
  91. dtspark-1.0.4.dist-info/METADATA +187 -0
  92. dtspark-1.0.4.dist-info/RECORD +96 -0
  93. dtspark-1.0.4.dist-info/WHEEL +5 -0
  94. dtspark-1.0.4.dist-info/entry_points.txt +3 -0
  95. dtspark-1.0.4.dist-info/licenses/LICENSE +21 -0
  96. dtspark-1.0.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,395 @@
1
+ """
2
+ Action Scheduler Manager module.
3
+
4
+ Provides APScheduler wrapper for scheduling autonomous actions.
5
+
6
+
7
+ """
8
+
9
+ import logging
10
+ from datetime import datetime
11
+ from typing import Dict, Any, Optional, Callable, List
12
+ from pathlib import Path
13
+
14
+ try:
15
+ from apscheduler.schedulers.background import BackgroundScheduler
16
+ from apscheduler.jobstores.memory import MemoryJobStore
17
+ from apscheduler.triggers.date import DateTrigger
18
+ from apscheduler.triggers.cron import CronTrigger
19
+ from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_EXECUTED, JobExecutionEvent
20
+ APSCHEDULER_AVAILABLE = True
21
+ except ImportError:
22
+ APSCHEDULER_AVAILABLE = False
23
+ BackgroundScheduler = None
24
+ MemoryJobStore = None
25
+ DateTrigger = None
26
+ CronTrigger = None
27
+
28
+
29
+ class ActionSchedulerManager:
30
+ """
31
+ Manages scheduled autonomous actions using APScheduler.
32
+
33
+ Uses BackgroundScheduler with SQLAlchemyJobStore for persistence.
34
+ Supports both one-off (DateTrigger) and recurring (CronTrigger) schedules.
35
+ """
36
+
37
+ def __init__(self, db_path: str, execution_callback: Callable[[int, str], None]):
38
+ """
39
+ Initialise the action scheduler manager.
40
+
41
+ Args:
42
+ db_path: Path to SQLite database for job persistence
43
+ execution_callback: Callback function(action_id, user_guid) to execute actions
44
+ """
45
+ if not APSCHEDULER_AVAILABLE:
46
+ raise ImportError(
47
+ "APScheduler is required for autonomous actions. "
48
+ "Install it with: pip install APScheduler>=3.10.0"
49
+ )
50
+
51
+ self.db_path = db_path
52
+ self.execution_callback = execution_callback
53
+ self.scheduler: Optional[BackgroundScheduler] = None
54
+ self._is_running = False
55
+
56
+ logging.info("ActionSchedulerManager initialised (using in-memory job store)")
57
+
58
+ def initialise(self):
59
+ """
60
+ Initialise the APScheduler with in-memory job store.
61
+
62
+ We use MemoryJobStore instead of SQLAlchemyJobStore because:
63
+ 1. We already persist action configurations in our own database
64
+ 2. On startup, we reload all actions from our database
65
+ 3. MemoryJobStore avoids pickling issues with callbacks
66
+ """
67
+ if self.scheduler is not None:
68
+ logging.warning("Scheduler already initialised")
69
+ return
70
+
71
+ job_stores = {
72
+ 'default': MemoryJobStore()
73
+ }
74
+
75
+ job_defaults = {
76
+ 'coalesce': True, # Combine missed runs into one
77
+ 'max_instances': 1, # Only one instance of each job at a time
78
+ 'misfire_grace_time': 3600 # Allow 1 hour grace time for missed jobs
79
+ }
80
+
81
+ self.scheduler = BackgroundScheduler(
82
+ jobstores=job_stores,
83
+ job_defaults=job_defaults,
84
+ timezone='UTC'
85
+ )
86
+
87
+ # Add event listeners for logging
88
+ self.scheduler.add_listener(self._on_job_executed, EVENT_JOB_EXECUTED)
89
+ self.scheduler.add_listener(self._on_job_error, EVENT_JOB_ERROR)
90
+
91
+ logging.info("APScheduler initialised with in-memory job store")
92
+
93
+ def start(self):
94
+ """
95
+ Start the scheduler.
96
+ """
97
+ if self.scheduler is None:
98
+ raise RuntimeError("Scheduler not initialised. Call initialise() first.")
99
+
100
+ if self._is_running:
101
+ logging.warning("Scheduler already running")
102
+ return
103
+
104
+ self.scheduler.start()
105
+ self._is_running = True
106
+ logging.info("Action scheduler started")
107
+
108
+ def stop(self):
109
+ """
110
+ Stop the scheduler gracefully.
111
+ """
112
+ if self.scheduler is None or not self._is_running:
113
+ return
114
+
115
+ self.scheduler.shutdown(wait=True)
116
+ self._is_running = False
117
+ logging.info("Action scheduler stopped")
118
+
119
+ def is_running(self) -> bool:
120
+ """Check if the scheduler is running."""
121
+ return self._is_running
122
+
123
+ def schedule_action(self, action_id: int, action_name: str,
124
+ schedule_type: str, schedule_config: Dict[str, Any],
125
+ user_guid: str) -> bool:
126
+ """
127
+ Schedule an action for execution.
128
+
129
+ Args:
130
+ action_id: ID of the action
131
+ action_name: Name of the action (for job ID)
132
+ schedule_type: 'one_off' or 'recurring'
133
+ schedule_config: Schedule configuration
134
+ user_guid: User GUID for execution context
135
+
136
+ Returns:
137
+ True if scheduled successfully
138
+ """
139
+ if self.scheduler is None:
140
+ logging.error("Cannot schedule action: scheduler not initialised")
141
+ return False
142
+
143
+ job_id = f"action_{action_id}"
144
+
145
+ try:
146
+ # Remove existing job if any
147
+ self._remove_job_if_exists(job_id)
148
+
149
+ if schedule_type == 'one_off':
150
+ trigger = self._create_date_trigger(schedule_config)
151
+ elif schedule_type == 'recurring':
152
+ trigger = self._create_cron_trigger(schedule_config)
153
+ else:
154
+ logging.error(f"Unknown schedule type: {schedule_type}")
155
+ return False
156
+
157
+ if trigger is None:
158
+ return False
159
+
160
+ self.scheduler.add_job(
161
+ func=self._execute_action_wrapper,
162
+ trigger=trigger,
163
+ id=job_id,
164
+ name=action_name,
165
+ args=[action_id, user_guid],
166
+ replace_existing=True
167
+ )
168
+
169
+ next_run = self.scheduler.get_job(job_id).next_run_time
170
+ logging.info(f"Scheduled action {action_id} ({action_name}), next run: {next_run}")
171
+ return True
172
+
173
+ except Exception as e:
174
+ logging.error(f"Failed to schedule action {action_id}: {e}")
175
+ return False
176
+
177
+ def unschedule_action(self, action_id: int) -> bool:
178
+ """
179
+ Remove a scheduled action.
180
+
181
+ Args:
182
+ action_id: ID of the action
183
+
184
+ Returns:
185
+ True if removed successfully
186
+ """
187
+ if self.scheduler is None:
188
+ return False
189
+
190
+ job_id = f"action_{action_id}"
191
+ return self._remove_job_if_exists(job_id)
192
+
193
+ def run_action_now(self, action_id: int, user_guid: str) -> bool:
194
+ """
195
+ Trigger an action to run immediately.
196
+
197
+ Args:
198
+ action_id: ID of the action
199
+ user_guid: User GUID for execution context
200
+
201
+ Returns:
202
+ True if triggered successfully
203
+ """
204
+ if self.scheduler is None:
205
+ logging.error("Cannot run action: scheduler not initialised")
206
+ return False
207
+
208
+ try:
209
+ # Add a one-time job that runs immediately
210
+ job_id = f"action_{action_id}_manual"
211
+ self._remove_job_if_exists(job_id)
212
+
213
+ self.scheduler.add_job(
214
+ func=self._execute_action_wrapper,
215
+ trigger='date', # Run immediately
216
+ id=job_id,
217
+ name=f"Manual run of action {action_id}",
218
+ args=[action_id, user_guid]
219
+ )
220
+
221
+ logging.info(f"Triggered manual run of action {action_id}")
222
+ return True
223
+
224
+ except Exception as e:
225
+ logging.error(f"Failed to trigger manual run of action {action_id}: {e}")
226
+ return False
227
+
228
+ def get_next_run_time(self, action_id: int) -> Optional[datetime]:
229
+ """
230
+ Get the next scheduled run time for an action.
231
+
232
+ Args:
233
+ action_id: ID of the action
234
+
235
+ Returns:
236
+ Next run time or None if not scheduled
237
+ """
238
+ if self.scheduler is None:
239
+ return None
240
+
241
+ job_id = f"action_{action_id}"
242
+ job = self.scheduler.get_job(job_id)
243
+
244
+ if job:
245
+ return job.next_run_time
246
+ return None
247
+
248
+ def reload_all_actions(self, actions: List[Dict[str, Any]]):
249
+ """
250
+ Reload all enabled actions from database.
251
+
252
+ Args:
253
+ actions: List of action dictionaries
254
+ """
255
+ if self.scheduler is None:
256
+ logging.warning("Cannot reload actions: scheduler not initialised")
257
+ return
258
+
259
+ loaded_count = 0
260
+ for action in actions:
261
+ if action.get('is_enabled', False):
262
+ success = self.schedule_action(
263
+ action_id=action['id'],
264
+ action_name=action['name'],
265
+ schedule_type=action['schedule_type'],
266
+ schedule_config=action['schedule_config'],
267
+ user_guid=action.get('user_guid', '')
268
+ )
269
+ if success:
270
+ loaded_count += 1
271
+
272
+ logging.info(f"Reloaded {loaded_count} scheduled actions")
273
+
274
+ def get_scheduled_jobs(self) -> List[Dict[str, Any]]:
275
+ """
276
+ Get list of all scheduled jobs.
277
+
278
+ Returns:
279
+ List of job information dictionaries
280
+ """
281
+ if self.scheduler is None:
282
+ return []
283
+
284
+ jobs = []
285
+ for job in self.scheduler.get_jobs():
286
+ jobs.append({
287
+ 'id': job.id,
288
+ 'name': job.name,
289
+ 'next_run_time': job.next_run_time,
290
+ 'trigger': str(job.trigger)
291
+ })
292
+ return jobs
293
+
294
+ def _execute_action_wrapper(self, action_id: int, user_guid: str):
295
+ """
296
+ Wrapper function called by APScheduler to execute an action.
297
+
298
+ Args:
299
+ action_id: ID of the action
300
+ user_guid: User GUID for execution context
301
+ """
302
+ try:
303
+ logging.info(f"Scheduler triggering action {action_id} for user {user_guid}")
304
+ self.execution_callback(action_id, user_guid)
305
+ except Exception as e:
306
+ logging.error(f"Error in action execution wrapper for action {action_id}: {e}")
307
+ raise
308
+
309
+ def _create_date_trigger(self, config: Dict[str, Any]) -> Optional[DateTrigger]:
310
+ """
311
+ Create a DateTrigger for one-off scheduling.
312
+
313
+ Args:
314
+ config: Configuration with 'run_date' key
315
+
316
+ Returns:
317
+ DateTrigger or None if invalid
318
+ """
319
+ try:
320
+ run_date = config.get('run_date')
321
+ if isinstance(run_date, str):
322
+ run_date = datetime.fromisoformat(run_date)
323
+ elif not isinstance(run_date, datetime):
324
+ logging.error(f"Invalid run_date format: {run_date}")
325
+ return None
326
+
327
+ return DateTrigger(run_date=run_date)
328
+
329
+ except Exception as e:
330
+ logging.error(f"Failed to create date trigger: {e}")
331
+ return None
332
+
333
+ def _create_cron_trigger(self, config: Dict[str, Any]) -> Optional[CronTrigger]:
334
+ """
335
+ Create a CronTrigger for recurring scheduling.
336
+
337
+ Args:
338
+ config: Configuration with cron fields or 'cron_expression' key
339
+
340
+ Returns:
341
+ CronTrigger or None if invalid
342
+ """
343
+ try:
344
+ # Support both individual fields and expression
345
+ if 'cron_expression' in config:
346
+ # Parse cron expression (minute hour day month day_of_week)
347
+ parts = config['cron_expression'].split()
348
+ if len(parts) >= 5:
349
+ return CronTrigger(
350
+ minute=parts[0],
351
+ hour=parts[1],
352
+ day=parts[2],
353
+ month=parts[3],
354
+ day_of_week=parts[4]
355
+ )
356
+
357
+ # Individual fields
358
+ return CronTrigger(
359
+ minute=config.get('minute', '*'),
360
+ hour=config.get('hour', '*'),
361
+ day=config.get('day', '*'),
362
+ month=config.get('month', '*'),
363
+ day_of_week=config.get('day_of_week', '*')
364
+ )
365
+
366
+ except Exception as e:
367
+ logging.error(f"Failed to create cron trigger: {e}")
368
+ return None
369
+
370
+ def _remove_job_if_exists(self, job_id: str) -> bool:
371
+ """
372
+ Remove a job if it exists.
373
+
374
+ Args:
375
+ job_id: Job ID to remove
376
+
377
+ Returns:
378
+ True if removed, False if didn't exist
379
+ """
380
+ try:
381
+ if self.scheduler.get_job(job_id):
382
+ self.scheduler.remove_job(job_id)
383
+ logging.debug(f"Removed existing job: {job_id}")
384
+ return True
385
+ except Exception as e:
386
+ logging.debug(f"Job {job_id} not found or could not be removed: {e}")
387
+ return False
388
+
389
+ def _on_job_executed(self, event: 'JobExecutionEvent'):
390
+ """Handle successful job execution event."""
391
+ logging.info(f"Job {event.job_id} executed successfully")
392
+
393
+ def _on_job_error(self, event: 'JobExecutionEvent'):
394
+ """Handle job error event."""
395
+ logging.error(f"Job {event.job_id} raised an exception: {event.exception}")
@@ -0,0 +1,4 @@
1
+ """Built-in tools module."""
2
+ from .builtin import get_builtin_tools, execute_builtin_tool
3
+
4
+ __all__ = ['get_builtin_tools', 'execute_builtin_tool']