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.
- dtSpark/__init__.py +0 -0
- dtSpark/_description.txt +1 -0
- dtSpark/_full_name.txt +1 -0
- dtSpark/_licence.txt +21 -0
- dtSpark/_metadata.yaml +6 -0
- dtSpark/_name.txt +1 -0
- dtSpark/_version.txt +1 -0
- dtSpark/aws/__init__.py +7 -0
- dtSpark/aws/authentication.py +296 -0
- dtSpark/aws/bedrock.py +578 -0
- dtSpark/aws/costs.py +318 -0
- dtSpark/aws/pricing.py +580 -0
- dtSpark/cli_interface.py +2645 -0
- dtSpark/conversation_manager.py +3050 -0
- dtSpark/core/__init__.py +12 -0
- dtSpark/core/application.py +3355 -0
- dtSpark/core/context_compaction.py +735 -0
- dtSpark/daemon/__init__.py +104 -0
- dtSpark/daemon/__main__.py +10 -0
- dtSpark/daemon/action_monitor.py +213 -0
- dtSpark/daemon/daemon_app.py +730 -0
- dtSpark/daemon/daemon_manager.py +289 -0
- dtSpark/daemon/execution_coordinator.py +194 -0
- dtSpark/daemon/pid_file.py +169 -0
- dtSpark/database/__init__.py +482 -0
- dtSpark/database/autonomous_actions.py +1191 -0
- dtSpark/database/backends.py +329 -0
- dtSpark/database/connection.py +122 -0
- dtSpark/database/conversations.py +520 -0
- dtSpark/database/credential_prompt.py +218 -0
- dtSpark/database/files.py +205 -0
- dtSpark/database/mcp_ops.py +355 -0
- dtSpark/database/messages.py +161 -0
- dtSpark/database/schema.py +673 -0
- dtSpark/database/tool_permissions.py +186 -0
- dtSpark/database/usage.py +167 -0
- dtSpark/files/__init__.py +4 -0
- dtSpark/files/manager.py +322 -0
- dtSpark/launch.py +39 -0
- dtSpark/limits/__init__.py +10 -0
- dtSpark/limits/costs.py +296 -0
- dtSpark/limits/tokens.py +342 -0
- dtSpark/llm/__init__.py +17 -0
- dtSpark/llm/anthropic_direct.py +446 -0
- dtSpark/llm/base.py +146 -0
- dtSpark/llm/context_limits.py +438 -0
- dtSpark/llm/manager.py +177 -0
- dtSpark/llm/ollama.py +578 -0
- dtSpark/mcp_integration/__init__.py +5 -0
- dtSpark/mcp_integration/manager.py +653 -0
- dtSpark/mcp_integration/tool_selector.py +225 -0
- dtSpark/resources/config.yaml.template +631 -0
- dtSpark/safety/__init__.py +22 -0
- dtSpark/safety/llm_service.py +111 -0
- dtSpark/safety/patterns.py +229 -0
- dtSpark/safety/prompt_inspector.py +442 -0
- dtSpark/safety/violation_logger.py +346 -0
- dtSpark/scheduler/__init__.py +20 -0
- dtSpark/scheduler/creation_tools.py +599 -0
- dtSpark/scheduler/execution_queue.py +159 -0
- dtSpark/scheduler/executor.py +1152 -0
- dtSpark/scheduler/manager.py +395 -0
- dtSpark/tools/__init__.py +4 -0
- dtSpark/tools/builtin.py +833 -0
- dtSpark/web/__init__.py +20 -0
- dtSpark/web/auth.py +152 -0
- dtSpark/web/dependencies.py +37 -0
- dtSpark/web/endpoints/__init__.py +17 -0
- dtSpark/web/endpoints/autonomous_actions.py +1125 -0
- dtSpark/web/endpoints/chat.py +621 -0
- dtSpark/web/endpoints/conversations.py +353 -0
- dtSpark/web/endpoints/main_menu.py +547 -0
- dtSpark/web/endpoints/streaming.py +421 -0
- dtSpark/web/server.py +578 -0
- dtSpark/web/session.py +167 -0
- dtSpark/web/ssl_utils.py +195 -0
- dtSpark/web/static/css/dark-theme.css +427 -0
- dtSpark/web/static/js/actions.js +1101 -0
- dtSpark/web/static/js/chat.js +614 -0
- dtSpark/web/static/js/main.js +496 -0
- dtSpark/web/static/js/sse-client.js +242 -0
- dtSpark/web/templates/actions.html +408 -0
- dtSpark/web/templates/base.html +93 -0
- dtSpark/web/templates/chat.html +814 -0
- dtSpark/web/templates/conversations.html +350 -0
- dtSpark/web/templates/goodbye.html +81 -0
- dtSpark/web/templates/login.html +90 -0
- dtSpark/web/templates/main_menu.html +983 -0
- dtSpark/web/templates/new_conversation.html +191 -0
- dtSpark/web/web_interface.py +137 -0
- dtspark-1.0.4.dist-info/METADATA +187 -0
- dtspark-1.0.4.dist-info/RECORD +96 -0
- dtspark-1.0.4.dist-info/WHEEL +5 -0
- dtspark-1.0.4.dist-info/entry_points.txt +3 -0
- dtspark-1.0.4.dist-info/licenses/LICENSE +21 -0
- 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}")
|