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,1101 @@
1
+ /**
2
+ * Autonomous Actions JavaScript for Spark web interface
3
+ *
4
+ * Handles action management, runs viewing, and API interactions
5
+ */
6
+
7
+ // Store for current context
8
+ let currentActionId = null;
9
+ let currentRunId = null;
10
+ let availableModels = [];
11
+ let availableTools = [];
12
+
13
+ // =============================================================================
14
+ // LOAD AND DISPLAY ACTIONS
15
+ // =============================================================================
16
+
17
+ /**
18
+ * Load all actions from API
19
+ */
20
+ async function loadActions() {
21
+ const tbody = document.getElementById('actions-tbody');
22
+ const includeDisabled = document.getElementById('showDisabled').checked;
23
+
24
+ try {
25
+ const response = await fetch(`/api/actions?include_disabled=${includeDisabled}`);
26
+ if (!response.ok) throw new Error('Failed to load actions');
27
+
28
+ const actions = await response.json();
29
+
30
+ if (actions.length === 0) {
31
+ tbody.innerHTML = `
32
+ <tr>
33
+ <td colspan="9" class="text-center text-muted py-4">
34
+ <i class="bi bi-inbox"></i> No actions found.
35
+ <button class="btn btn-link" onclick="showCreateModal()">Create your first action</button>
36
+ </td>
37
+ </tr>
38
+ `;
39
+ return;
40
+ }
41
+
42
+ tbody.innerHTML = actions.map(action => `
43
+ <tr class="${!action.is_enabled ? 'table-secondary' : ''}">
44
+ <td>${action.id}</td>
45
+ <td>
46
+ <a href="#" onclick="viewAction(${action.id}); return false;">
47
+ ${escapeHtml(action.name)}
48
+ </a>
49
+ </td>
50
+ <td><small class="text-muted">${escapeHtml(action.model_id)}</small></td>
51
+ <td>${formatSchedule(action)}</td>
52
+ <td>
53
+ <span class="badge ${action.context_mode === 'cumulative' ? 'bg-info' : 'bg-secondary'}">
54
+ ${action.context_mode}
55
+ </span>
56
+ </td>
57
+ <td>
58
+ ${action.is_enabled
59
+ ? '<span class="badge bg-success">Enabled</span>'
60
+ : '<span class="badge bg-danger">Disabled</span>'}
61
+ </td>
62
+ <td>${action.last_run_at ? formatTimestamp(action.last_run_at) : '<span class="text-muted">Never</span>'}</td>
63
+ <td>${formatFailures(action)}</td>
64
+ <td>
65
+ <div class="btn-group btn-group-sm">
66
+ <button class="btn btn-outline-primary" onclick="runActionNow(${action.id})" title="Run now">
67
+ <i class="bi bi-play-fill"></i>
68
+ </button>
69
+ <button class="btn btn-outline-secondary" onclick="viewActionRuns(${action.id}, '${escapeHtml(action.name)}')" title="View runs">
70
+ <i class="bi bi-clock-history"></i>
71
+ </button>
72
+ <button class="btn btn-outline-secondary" onclick="editAction(${action.id})" title="Edit">
73
+ <i class="bi bi-pencil"></i>
74
+ </button>
75
+ ${action.is_enabled
76
+ ? `<button class="btn btn-outline-warning" onclick="disableAction(${action.id})" title="Disable">
77
+ <i class="bi bi-pause-fill"></i>
78
+ </button>`
79
+ : `<button class="btn btn-outline-success" onclick="enableAction(${action.id})" title="Enable">
80
+ <i class="bi bi-play"></i>
81
+ </button>`}
82
+ <button class="btn btn-outline-danger" onclick="confirmDeleteAction(${action.id}, '${escapeHtml(action.name)}')" title="Delete">
83
+ <i class="bi bi-trash"></i>
84
+ </button>
85
+ </div>
86
+ </td>
87
+ </tr>
88
+ `).join('');
89
+
90
+ } catch (error) {
91
+ console.error('Error loading actions:', error);
92
+ tbody.innerHTML = `
93
+ <tr>
94
+ <td colspan="9" class="text-center text-danger py-4">
95
+ <i class="bi bi-exclamation-triangle"></i> Failed to load actions
96
+ </td>
97
+ </tr>
98
+ `;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Load recent runs across all actions
104
+ */
105
+ async function loadRecentRuns() {
106
+ const tbody = document.getElementById('runs-tbody');
107
+
108
+ try {
109
+ const response = await fetch('/api/actions/runs/recent?limit=10');
110
+ if (!response.ok) throw new Error('Failed to load recent runs');
111
+
112
+ const runs = await response.json();
113
+
114
+ if (runs.length === 0) {
115
+ tbody.innerHTML = `
116
+ <tr>
117
+ <td colspan="7" class="text-center text-muted py-4">
118
+ <i class="bi bi-inbox"></i> No runs yet
119
+ </td>
120
+ </tr>
121
+ `;
122
+ return;
123
+ }
124
+
125
+ tbody.innerHTML = runs.map(run => `
126
+ <tr>
127
+ <td>${run.id}</td>
128
+ <td>${escapeHtml(run.action_name || 'Unknown')}</td>
129
+ <td>${formatTimestamp(run.started_at)}</td>
130
+ <td>${formatRunStatus(run.status)}</td>
131
+ <td>${formatDuration(run.started_at, run.completed_at)}</td>
132
+ <td>${formatTokens(run.input_tokens, run.output_tokens)}</td>
133
+ <td>
134
+ <button class="btn btn-sm btn-outline-secondary" onclick="viewRunDetails(${run.action_id}, ${run.id})">
135
+ <i class="bi bi-eye"></i> View
136
+ </button>
137
+ </td>
138
+ </tr>
139
+ `).join('');
140
+
141
+ } catch (error) {
142
+ console.error('Error loading recent runs:', error);
143
+ tbody.innerHTML = `
144
+ <tr>
145
+ <td colspan="7" class="text-center text-danger py-4">
146
+ <i class="bi bi-exclamation-triangle"></i> Failed to load recent runs
147
+ </td>
148
+ </tr>
149
+ `;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Check for failed actions and show warning
155
+ */
156
+ async function checkFailedActions() {
157
+ try {
158
+ const response = await fetch('/api/actions/status/failed-count');
159
+ if (!response.ok) return;
160
+
161
+ const data = await response.json();
162
+ const warning = document.getElementById('failed-warning');
163
+ const text = document.getElementById('failed-count-text');
164
+
165
+ if (data.failed_count > 0) {
166
+ text.textContent = `${data.failed_count} action(s) have been auto-disabled due to failures.`;
167
+ warning.classList.remove('d-none');
168
+ } else {
169
+ warning.classList.add('d-none');
170
+ }
171
+ } catch (error) {
172
+ console.error('Error checking failed actions:', error);
173
+ }
174
+ }
175
+
176
+ // =============================================================================
177
+ // ACTION CRUD OPERATIONS
178
+ // =============================================================================
179
+
180
+ /**
181
+ * Show create action modal
182
+ */
183
+ function showCreateModal() {
184
+ currentActionId = null;
185
+ document.getElementById('actionModalTitle').textContent = 'Create Action';
186
+ document.getElementById('actionForm').reset();
187
+ document.getElementById('actionId').value = '';
188
+
189
+ // Set default run date to tomorrow at 9am
190
+ const tomorrow = new Date();
191
+ tomorrow.setDate(tomorrow.getDate() + 1);
192
+ tomorrow.setHours(9, 0, 0, 0);
193
+ document.getElementById('runDate').value = tomorrow.toISOString().slice(0, 16);
194
+
195
+ updateScheduleConfig();
196
+ resetToolPermissions();
197
+
198
+ const modal = new bootstrap.Modal(document.getElementById('actionModal'));
199
+ modal.show();
200
+ }
201
+
202
+ /**
203
+ * Edit an existing action
204
+ */
205
+ async function editAction(actionId) {
206
+ try {
207
+ const response = await fetch(`/api/actions/${actionId}`);
208
+ if (!response.ok) throw new Error('Failed to load action');
209
+
210
+ const action = await response.json();
211
+ currentActionId = actionId;
212
+
213
+ document.getElementById('actionModalTitle').textContent = 'Edit Action';
214
+ document.getElementById('actionId').value = actionId;
215
+ document.getElementById('actionName').value = action.name;
216
+ document.getElementById('actionDescription').value = action.description;
217
+ document.getElementById('actionPrompt').value = action.action_prompt;
218
+ document.getElementById('actionModel').value = action.model_id;
219
+ document.getElementById('scheduleType').value = action.schedule_type;
220
+ document.getElementById('contextMode').value = action.context_mode;
221
+ document.getElementById('maxFailures').value = action.max_failures;
222
+
223
+ updateScheduleConfig();
224
+
225
+ // Set schedule config
226
+ if (action.schedule_type === 'one_off' && action.schedule_config?.run_date) {
227
+ document.getElementById('runDate').value = action.schedule_config.run_date.slice(0, 16);
228
+ } else if (action.schedule_type === 'recurring' && action.schedule_config?.cron_expression) {
229
+ document.getElementById('cronExpression').value = action.schedule_config.cron_expression;
230
+ }
231
+
232
+ // Set tool permissions
233
+ setToolPermissions(action.tool_permissions || []);
234
+
235
+ const modal = new bootstrap.Modal(document.getElementById('actionModal'));
236
+ modal.show();
237
+
238
+ } catch (error) {
239
+ console.error('Error loading action:', error);
240
+ showToast('Failed to load action', 'error');
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Save action (create or update)
246
+ */
247
+ async function saveAction() {
248
+ const actionId = document.getElementById('actionId').value;
249
+ const isEdit = !!actionId;
250
+
251
+ // Gather form data
252
+ const scheduleType = document.getElementById('scheduleType').value;
253
+ let scheduleConfig = {};
254
+
255
+ if (scheduleType === 'one_off') {
256
+ const runDate = document.getElementById('runDate').value;
257
+ if (!runDate) {
258
+ showToast('Please select a run date', 'error');
259
+ return;
260
+ }
261
+ scheduleConfig = { run_date: new Date(runDate).toISOString() };
262
+ } else {
263
+ const cronExpr = document.getElementById('cronExpression').value.trim();
264
+ if (!cronExpr) {
265
+ showToast('Please enter a cron expression', 'error');
266
+ return;
267
+ }
268
+ scheduleConfig = { cron_expression: cronExpr };
269
+ }
270
+
271
+ const data = {
272
+ name: document.getElementById('actionName').value.trim(),
273
+ description: document.getElementById('actionDescription').value.trim(),
274
+ action_prompt: document.getElementById('actionPrompt').value.trim(),
275
+ model_id: document.getElementById('actionModel').value,
276
+ schedule_type: scheduleType,
277
+ schedule_config: scheduleConfig,
278
+ context_mode: document.getElementById('contextMode').value,
279
+ max_failures: parseInt(document.getElementById('maxFailures').value),
280
+ tool_permissions: getToolPermissions()
281
+ };
282
+
283
+ // Validate required fields
284
+ if (!data.name || !data.description || !data.action_prompt || !data.model_id) {
285
+ showToast('Please fill in all required fields', 'error');
286
+ return;
287
+ }
288
+
289
+ try {
290
+ const url = isEdit ? `/api/actions/${actionId}` : '/api/actions';
291
+ const method = isEdit ? 'PUT' : 'POST';
292
+
293
+ const response = await fetch(url, {
294
+ method: method,
295
+ headers: { 'Content-Type': 'application/json' },
296
+ body: JSON.stringify(data)
297
+ });
298
+
299
+ if (!response.ok) {
300
+ const error = await response.json();
301
+ throw new Error(error.detail || 'Failed to save action');
302
+ }
303
+
304
+ showToast(isEdit ? 'Action updated successfully' : 'Action created successfully', 'success');
305
+
306
+ // Close modal and reload
307
+ bootstrap.Modal.getInstance(document.getElementById('actionModal')).hide();
308
+ loadActions();
309
+ checkFailedActions();
310
+
311
+ } catch (error) {
312
+ console.error('Error saving action:', error);
313
+ showToast(error.message, 'error');
314
+ }
315
+ }
316
+
317
+ /**
318
+ * View action details
319
+ */
320
+ async function viewAction(actionId) {
321
+ try {
322
+ const response = await fetch(`/api/actions/${actionId}`);
323
+ if (!response.ok) throw new Error('Failed to load action');
324
+
325
+ const action = await response.json();
326
+
327
+ const body = document.getElementById('viewActionBody');
328
+ body.innerHTML = `
329
+ <div class="row">
330
+ <div class="col-md-6">
331
+ <h6>Name</h6>
332
+ <p>${escapeHtml(action.name)}</p>
333
+
334
+ <h6>Description</h6>
335
+ <p>${escapeHtml(action.description)}</p>
336
+
337
+ <h6>Model</h6>
338
+ <p><code>${escapeHtml(action.model_id)}</code></p>
339
+
340
+ <h6>Schedule</h6>
341
+ <p>${formatSchedule(action)}</p>
342
+ </div>
343
+ <div class="col-md-6">
344
+ <h6>Context Mode</h6>
345
+ <p><span class="badge ${action.context_mode === 'cumulative' ? 'bg-info' : 'bg-secondary'}">${action.context_mode}</span></p>
346
+
347
+ <h6>Status</h6>
348
+ <p>${action.is_enabled ? '<span class="badge bg-success">Enabled</span>' : '<span class="badge bg-danger">Disabled</span>'}</p>
349
+
350
+ <h6>Failures</h6>
351
+ <p>${action.failure_count} / ${action.max_failures}</p>
352
+
353
+ <h6>Last Run</h6>
354
+ <p>${action.last_run_at ? formatTimestamp(action.last_run_at) : 'Never'}</p>
355
+
356
+ <h6>Next Run</h6>
357
+ <p>${action.next_run_at ? formatTimestamp(action.next_run_at) : 'Not scheduled'}</p>
358
+ </div>
359
+ </div>
360
+
361
+ <h6>Action Prompt</h6>
362
+ <pre class="bg-dark p-3 rounded">${escapeHtml(action.action_prompt)}</pre>
363
+
364
+ ${action.tool_permissions && action.tool_permissions.length > 0 ? `
365
+ <h6>Tool Permissions</h6>
366
+ <ul class="list-unstyled">
367
+ ${action.tool_permissions.map(tp => `
368
+ <li>
369
+ <i class="bi ${tp.permission_state === 'allowed' ? 'bi-check-circle text-success' : 'bi-x-circle text-danger'}"></i>
370
+ ${escapeHtml(tp.tool_name)}
371
+ ${tp.server_name ? `<small class="text-muted">(${escapeHtml(tp.server_name)})</small>` : ''}
372
+ </li>
373
+ `).join('')}
374
+ </ul>
375
+ ` : ''}
376
+ `;
377
+
378
+ const modal = new bootstrap.Modal(document.getElementById('viewActionModal'));
379
+ modal.show();
380
+
381
+ } catch (error) {
382
+ console.error('Error viewing action:', error);
383
+ showToast('Failed to load action details', 'error');
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Enable an action
389
+ */
390
+ async function enableAction(actionId) {
391
+ try {
392
+ const response = await fetch(`/api/actions/${actionId}/enable`, { method: 'POST' });
393
+ if (!response.ok) throw new Error('Failed to enable action');
394
+
395
+ showToast('Action enabled', 'success');
396
+ loadActions();
397
+ checkFailedActions();
398
+
399
+ } catch (error) {
400
+ console.error('Error enabling action:', error);
401
+ showToast('Failed to enable action', 'error');
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Disable an action
407
+ */
408
+ async function disableAction(actionId) {
409
+ try {
410
+ const response = await fetch(`/api/actions/${actionId}/disable`, { method: 'POST' });
411
+ if (!response.ok) throw new Error('Failed to disable action');
412
+
413
+ showToast('Action disabled', 'success');
414
+ loadActions();
415
+
416
+ } catch (error) {
417
+ console.error('Error disabling action:', error);
418
+ showToast('Failed to disable action', 'error');
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Run action now (manual trigger)
424
+ */
425
+ async function runActionNow(actionId) {
426
+ try {
427
+ showToast('Running action...', 'info');
428
+
429
+ const response = await fetch(`/api/actions/${actionId}/run-now`, { method: 'POST' });
430
+ if (!response.ok) throw new Error('Failed to trigger action');
431
+
432
+ showToast('Action triggered successfully', 'success');
433
+
434
+ // Reload after a short delay to show the new run
435
+ setTimeout(() => {
436
+ loadActions();
437
+ loadRecentRuns();
438
+ }, 2000);
439
+
440
+ } catch (error) {
441
+ console.error('Error running action:', error);
442
+ showToast('Failed to trigger action', 'error');
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Show delete confirmation
448
+ */
449
+ function confirmDeleteAction(actionId, actionName) {
450
+ currentActionId = actionId;
451
+ document.getElementById('deleteActionName').textContent = actionName;
452
+
453
+ document.getElementById('confirmDeleteBtn').onclick = () => deleteAction(actionId);
454
+
455
+ const modal = new bootstrap.Modal(document.getElementById('confirmDeleteModal'));
456
+ modal.show();
457
+ }
458
+
459
+ /**
460
+ * Delete an action
461
+ */
462
+ async function deleteAction(actionId) {
463
+ try {
464
+ const response = await fetch(`/api/actions/${actionId}`, { method: 'DELETE' });
465
+ if (!response.ok) throw new Error('Failed to delete action');
466
+
467
+ showToast('Action deleted', 'success');
468
+
469
+ bootstrap.Modal.getInstance(document.getElementById('confirmDeleteModal')).hide();
470
+ loadActions();
471
+ loadRecentRuns();
472
+ checkFailedActions();
473
+
474
+ } catch (error) {
475
+ console.error('Error deleting action:', error);
476
+ showToast('Failed to delete action', 'error');
477
+ }
478
+ }
479
+
480
+ // =============================================================================
481
+ // RUNS MANAGEMENT
482
+ // =============================================================================
483
+
484
+ /**
485
+ * View runs for a specific action
486
+ */
487
+ async function viewActionRuns(actionId, actionName) {
488
+ currentActionId = actionId;
489
+ document.getElementById('actionRunsTitle').textContent = `Runs: ${actionName}`;
490
+
491
+ const tbody = document.getElementById('actionRunsTbody');
492
+ tbody.innerHTML = '<tr><td colspan="6" class="text-center"><div class="spinner-border spinner-border-sm"></div> Loading...</td></tr>';
493
+
494
+ const modal = new bootstrap.Modal(document.getElementById('actionRunsModal'));
495
+ modal.show();
496
+
497
+ try {
498
+ const response = await fetch(`/api/actions/${actionId}/runs?limit=50`);
499
+ if (!response.ok) throw new Error('Failed to load runs');
500
+
501
+ const runs = await response.json();
502
+
503
+ if (runs.length === 0) {
504
+ tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted">No runs yet</td></tr>';
505
+ return;
506
+ }
507
+
508
+ tbody.innerHTML = runs.map(run => `
509
+ <tr>
510
+ <td>${run.id}</td>
511
+ <td>${formatTimestamp(run.started_at)}</td>
512
+ <td>${run.completed_at ? formatTimestamp(run.completed_at) : '-'}</td>
513
+ <td>${formatRunStatus(run.status)}</td>
514
+ <td>${formatTokens(run.input_tokens, run.output_tokens)}</td>
515
+ <td>
516
+ <button class="btn btn-sm btn-outline-secondary" onclick="viewRunDetails(${actionId}, ${run.id})">
517
+ <i class="bi bi-eye"></i> View
518
+ </button>
519
+ </td>
520
+ </tr>
521
+ `).join('');
522
+
523
+ } catch (error) {
524
+ console.error('Error loading runs:', error);
525
+ tbody.innerHTML = '<tr><td colspan="6" class="text-center text-danger">Failed to load runs</td></tr>';
526
+ }
527
+ }
528
+
529
+ /**
530
+ * View details of a specific run
531
+ */
532
+ async function viewRunDetails(actionId, runId) {
533
+ currentActionId = actionId;
534
+ currentRunId = runId;
535
+
536
+ const body = document.getElementById('runDetailsBody');
537
+ body.innerHTML = '<div class="text-center"><div class="spinner-border"></div> Loading...</div>';
538
+
539
+ const modal = new bootstrap.Modal(document.getElementById('runDetailsModal'));
540
+ modal.show();
541
+
542
+ try {
543
+ const response = await fetch(`/api/actions/${actionId}/runs/${runId}`);
544
+ if (!response.ok) throw new Error('Failed to load run details');
545
+
546
+ const run = await response.json();
547
+
548
+ body.innerHTML = `
549
+ <div class="row mb-3">
550
+ <div class="col-md-3">
551
+ <h6>Status</h6>
552
+ <p>${formatRunStatus(run.status)}</p>
553
+ </div>
554
+ <div class="col-md-3">
555
+ <h6>Started</h6>
556
+ <p>${formatTimestamp(run.started_at)}</p>
557
+ </div>
558
+ <div class="col-md-3">
559
+ <h6>Completed</h6>
560
+ <p>${run.completed_at ? formatTimestamp(run.completed_at) : '-'}</p>
561
+ </div>
562
+ <div class="col-md-3">
563
+ <h6>Tokens</h6>
564
+ <p>${formatTokens(run.input_tokens, run.output_tokens)}</p>
565
+ </div>
566
+ </div>
567
+
568
+ ${run.error_message ? `
569
+ <div class="alert alert-danger">
570
+ <h6><i class="bi bi-exclamation-triangle"></i> Error</h6>
571
+ <pre class="mb-0">${escapeHtml(run.error_message)}</pre>
572
+ </div>
573
+ ` : ''}
574
+
575
+ <h6>Result</h6>
576
+ <div class="card">
577
+ <div class="card-body markdown-content" style="max-height: 400px; overflow-y: auto;">
578
+ ${run.result_html || (run.result_text ? `<pre>${escapeHtml(run.result_text)}</pre>` : '<p class="text-muted">No result</p>')}
579
+ </div>
580
+ </div>
581
+ `;
582
+
583
+ } catch (error) {
584
+ console.error('Error loading run details:', error);
585
+ body.innerHTML = '<div class="alert alert-danger">Failed to load run details</div>';
586
+ }
587
+ }
588
+
589
+ /**
590
+ * Export run result
591
+ */
592
+ async function exportRun(format) {
593
+ if (!currentActionId || !currentRunId) return;
594
+
595
+ try {
596
+ const response = await fetch(`/api/actions/${currentActionId}/runs/${currentRunId}/export?format=${format}`);
597
+ if (!response.ok) throw new Error('Failed to export run');
598
+
599
+ const blob = await response.blob();
600
+ const url = URL.createObjectURL(blob);
601
+ const link = document.createElement('a');
602
+ link.href = url;
603
+ link.download = `run_${currentRunId}.${format === 'markdown' ? 'md' : format}`;
604
+ link.click();
605
+ URL.revokeObjectURL(url);
606
+
607
+ showToast('Export downloaded', 'success');
608
+
609
+ } catch (error) {
610
+ console.error('Error exporting run:', error);
611
+ showToast('Failed to export run', 'error');
612
+ }
613
+ }
614
+
615
+ // =============================================================================
616
+ // HELPERS AND FORMATTING
617
+ // =============================================================================
618
+
619
+ /**
620
+ * Load available models for dropdown
621
+ */
622
+ async function loadModels() {
623
+ try {
624
+ const response = await fetch('/api/models');
625
+ if (!response.ok) throw new Error('Failed to load models');
626
+
627
+ availableModels = await response.json();
628
+
629
+ const select = document.getElementById('actionModel');
630
+ select.innerHTML = '<option value="">Select a model...</option>';
631
+
632
+ availableModels.forEach(model => {
633
+ const option = document.createElement('option');
634
+ option.value = model.id;
635
+ option.textContent = `${model.name} (${model.provider})`;
636
+ select.appendChild(option);
637
+ });
638
+
639
+ } catch (error) {
640
+ console.error('Error loading models:', error);
641
+ }
642
+ }
643
+
644
+ /**
645
+ * Load available tools for permissions
646
+ */
647
+ async function loadTools() {
648
+ try {
649
+ const response = await fetch('/api/tools');
650
+ if (!response.ok) throw new Error('Failed to load tools');
651
+
652
+ const data = await response.json();
653
+ availableTools = data.tools || [];
654
+
655
+ resetToolPermissions();
656
+
657
+ } catch (error) {
658
+ console.error('Error loading tools:', error);
659
+ document.getElementById('toolPermissions').innerHTML = '<p class="text-muted">Failed to load tools</p>';
660
+ }
661
+ }
662
+
663
+ /**
664
+ * Reset tool permissions checkboxes
665
+ */
666
+ function resetToolPermissions() {
667
+ const container = document.getElementById('toolPermissions');
668
+
669
+ if (availableTools.length === 0) {
670
+ container.innerHTML = '<p class="text-muted mb-0">No tools available</p>';
671
+ return;
672
+ }
673
+
674
+ container.innerHTML = availableTools.map(tool => `
675
+ <div class="form-check">
676
+ <input class="form-check-input tool-permission" type="checkbox"
677
+ id="tool-${tool.name}" data-tool="${tool.name}" data-server="${tool.server || ''}">
678
+ <label class="form-check-label" for="tool-${tool.name}">
679
+ ${escapeHtml(tool.name)}
680
+ ${tool.server ? `<small class="text-muted">(${escapeHtml(tool.server)})</small>` : ''}
681
+ </label>
682
+ </div>
683
+ `).join('');
684
+ }
685
+
686
+ /**
687
+ * Set tool permissions from action data
688
+ */
689
+ function setToolPermissions(permissions) {
690
+ // First reset all
691
+ document.querySelectorAll('.tool-permission').forEach(cb => cb.checked = false);
692
+
693
+ // Then set the allowed ones
694
+ permissions.forEach(perm => {
695
+ if (perm.permission_state === 'allowed') {
696
+ const cb = document.querySelector(`.tool-permission[data-tool="${perm.tool_name}"]`);
697
+ if (cb) cb.checked = true;
698
+ }
699
+ });
700
+ }
701
+
702
+ /**
703
+ * Get tool permissions from checkboxes
704
+ */
705
+ function getToolPermissions() {
706
+ const permissions = [];
707
+
708
+ document.querySelectorAll('.tool-permission:checked').forEach(cb => {
709
+ permissions.push({
710
+ tool_name: cb.dataset.tool,
711
+ server_name: cb.dataset.server || null,
712
+ permission_state: 'allowed'
713
+ });
714
+ });
715
+
716
+ return permissions;
717
+ }
718
+
719
+ /**
720
+ * Update schedule config visibility
721
+ */
722
+ function updateScheduleConfig() {
723
+ const scheduleType = document.getElementById('scheduleType').value;
724
+ document.getElementById('oneOffConfig').classList.toggle('d-none', scheduleType !== 'one_off');
725
+ document.getElementById('recurringConfig').classList.toggle('d-none', scheduleType !== 'recurring');
726
+ }
727
+
728
+ /**
729
+ * Format schedule for display
730
+ */
731
+ function formatSchedule(action) {
732
+ if (action.schedule_type === 'one_off') {
733
+ const date = action.schedule_config?.run_date;
734
+ return date ? `<i class="bi bi-calendar-event"></i> ${formatTimestamp(date)}` : 'One-off';
735
+ } else {
736
+ const cron = action.schedule_config?.cron_expression;
737
+ return cron ? `<i class="bi bi-arrow-repeat"></i> <code>${escapeHtml(cron)}</code>` : 'Recurring';
738
+ }
739
+ }
740
+
741
+ /**
742
+ * Format failures with colour coding
743
+ */
744
+ function formatFailures(action) {
745
+ const ratio = action.failure_count / action.max_failures;
746
+ let badgeClass = 'bg-secondary';
747
+ if (ratio >= 1) badgeClass = 'bg-danger';
748
+ else if (ratio >= 0.5) badgeClass = 'bg-warning text-dark';
749
+
750
+ return `<span class="badge ${badgeClass}">${action.failure_count}/${action.max_failures}</span>`;
751
+ }
752
+
753
+ /**
754
+ * Format run status badge
755
+ */
756
+ function formatRunStatus(status) {
757
+ const badges = {
758
+ 'completed': '<span class="badge bg-success">Completed</span>',
759
+ 'running': '<span class="badge bg-primary">Running</span>',
760
+ 'failed': '<span class="badge bg-danger">Failed</span>'
761
+ };
762
+ return badges[status] || `<span class="badge bg-secondary">${status}</span>`;
763
+ }
764
+
765
+ /**
766
+ * Format duration between two timestamps
767
+ */
768
+ function formatDuration(start, end) {
769
+ if (!start || !end) return '-';
770
+
771
+ const startDate = new Date(start);
772
+ const endDate = new Date(end);
773
+ const diffMs = endDate - startDate;
774
+
775
+ if (diffMs < 1000) return '<1s';
776
+ if (diffMs < 60000) return `${Math.round(diffMs / 1000)}s`;
777
+ if (diffMs < 3600000) return `${Math.round(diffMs / 60000)}m`;
778
+ return `${Math.round(diffMs / 3600000)}h`;
779
+ }
780
+
781
+ /**
782
+ * Format token counts
783
+ */
784
+ function formatTokens(input, output) {
785
+ if (!input && !output) return '-';
786
+ return `${(input || 0).toLocaleString()} / ${(output || 0).toLocaleString()}`;
787
+ }
788
+
789
+ /**
790
+ * Format timestamp for display
791
+ */
792
+ function formatTimestamp(timestamp) {
793
+ if (!timestamp) return '-';
794
+ try {
795
+ const date = new Date(timestamp);
796
+ return date.toLocaleString();
797
+ } catch (e) {
798
+ return timestamp;
799
+ }
800
+ }
801
+
802
+ /**
803
+ * Escape HTML to prevent XSS
804
+ */
805
+ function escapeHtml(text) {
806
+ if (typeof text !== 'string') return text;
807
+ const div = document.createElement('div');
808
+ div.textContent = text;
809
+ return div.innerHTML;
810
+ }
811
+
812
+
813
+ // =============================================================================
814
+ // AI-ASSISTED ACTION CREATION
815
+ // =============================================================================
816
+
817
+ // State for AI creation session
818
+ let aiCreationId = null;
819
+ let aiCreatedActionId = null;
820
+
821
+ /**
822
+ * Show the AI creation modal
823
+ */
824
+ function showAICreateModal() {
825
+ // Reset form and state
826
+ aiCreationId = null;
827
+ aiCreatedActionId = null;
828
+ document.getElementById('aiActionName').value = '';
829
+ document.getElementById('aiActionDescription').value = '';
830
+ document.getElementById('aiActionModel').value = '';
831
+ document.getElementById('aiChatMessages').innerHTML = '';
832
+
833
+ // Populate model dropdown from the main form's dropdown
834
+ const mainModelSelect = document.getElementById('actionModel');
835
+ const aiModelSelect = document.getElementById('aiActionModel');
836
+ aiModelSelect.innerHTML = mainModelSelect.innerHTML;
837
+
838
+ // Show setup form, hide chat interface
839
+ document.getElementById('aiCreateSetup').classList.remove('d-none');
840
+ document.getElementById('aiCreateChat').classList.add('d-none');
841
+ document.getElementById('aiCreateLoading').classList.add('d-none');
842
+
843
+ // Show footer with cancel button
844
+ document.getElementById('aiCreateFooter').classList.remove('d-none');
845
+
846
+ const modal = new bootstrap.Modal(document.getElementById('aiCreateModal'));
847
+ modal.show();
848
+ }
849
+
850
+ /**
851
+ * Start the AI-assisted creation session
852
+ */
853
+ async function startAICreation() {
854
+ const name = document.getElementById('aiActionName').value.trim();
855
+ const description = document.getElementById('aiActionDescription').value.trim();
856
+ const modelId = document.getElementById('aiActionModel').value;
857
+
858
+ // Validate inputs
859
+ if (!name) {
860
+ showToast('Please enter an action name', 'error');
861
+ return;
862
+ }
863
+ if (!description) {
864
+ showToast('Please enter a description', 'error');
865
+ return;
866
+ }
867
+ if (!modelId) {
868
+ showToast('Please select a model', 'error');
869
+ return;
870
+ }
871
+
872
+ // Show loading
873
+ document.getElementById('aiCreateSetup').classList.add('d-none');
874
+ document.getElementById('aiCreateLoading').classList.remove('d-none');
875
+
876
+ try {
877
+ const response = await fetch('/api/actions/ai-create/start', {
878
+ method: 'POST',
879
+ headers: { 'Content-Type': 'application/json' },
880
+ body: JSON.stringify({
881
+ name: name,
882
+ description: description,
883
+ model_id: modelId
884
+ })
885
+ });
886
+
887
+ if (!response.ok) {
888
+ const error = await response.json();
889
+ throw new Error(error.detail || 'Failed to start AI creation');
890
+ }
891
+
892
+ const data = await response.json();
893
+ aiCreationId = data.creation_id;
894
+
895
+ // Update chat header
896
+ document.getElementById('aiChatActionName').textContent = name;
897
+ document.getElementById('aiChatModelName').textContent = modelId;
898
+
899
+ // Show chat interface
900
+ document.getElementById('aiCreateLoading').classList.add('d-none');
901
+ document.getElementById('aiCreateChat').classList.remove('d-none');
902
+
903
+ // Add initial AI response to chat
904
+ addAIChatMessage(data.response, 'assistant');
905
+
906
+ // Focus on input
907
+ document.getElementById('aiChatInput').focus();
908
+
909
+ // Check if action was created immediately
910
+ if (data.completed) {
911
+ handleAICreationComplete(data.action_id);
912
+ }
913
+
914
+ } catch (error) {
915
+ console.error('Error starting AI creation:', error);
916
+ showToast(error.message, 'error');
917
+
918
+ // Show setup form again
919
+ document.getElementById('aiCreateLoading').classList.add('d-none');
920
+ document.getElementById('aiCreateSetup').classList.remove('d-none');
921
+ }
922
+ }
923
+
924
+ /**
925
+ * Send a message in the AI creation chat
926
+ */
927
+ async function sendAICreationMessage() {
928
+ const input = document.getElementById('aiChatInput');
929
+ const message = input.value.trim();
930
+
931
+ if (!message) return;
932
+ if (!aiCreationId) {
933
+ showToast('No active creation session', 'error');
934
+ return;
935
+ }
936
+
937
+ // Add user message to chat
938
+ addAIChatMessage(message, 'user');
939
+ input.value = '';
940
+
941
+ // Disable input while waiting
942
+ input.disabled = true;
943
+ document.querySelector('#aiChatInputArea button').disabled = true;
944
+
945
+ // Show typing indicator
946
+ const typingDiv = document.createElement('div');
947
+ typingDiv.id = 'aiTypingIndicator';
948
+ typingDiv.className = 'mb-2 text-muted small';
949
+ typingDiv.innerHTML = '<i class="bi bi-three-dots"></i> AI is thinking...';
950
+ document.getElementById('aiChatMessages').appendChild(typingDiv);
951
+ scrollChatToBottom();
952
+
953
+ try {
954
+ const response = await fetch(`/api/actions/ai-create/${aiCreationId}/message`, {
955
+ method: 'POST',
956
+ headers: { 'Content-Type': 'application/json' },
957
+ body: JSON.stringify({ message: message })
958
+ });
959
+
960
+ // Remove typing indicator
961
+ const indicator = document.getElementById('aiTypingIndicator');
962
+ if (indicator) indicator.remove();
963
+
964
+ if (!response.ok) {
965
+ const error = await response.json();
966
+ throw new Error(error.detail || 'Failed to send message');
967
+ }
968
+
969
+ const data = await response.json();
970
+
971
+ // Add AI response to chat
972
+ if (data.response) {
973
+ addAIChatMessage(data.response, 'assistant');
974
+ }
975
+
976
+ // Check if action was created
977
+ if (data.completed) {
978
+ handleAICreationComplete(data.action_id);
979
+ } else {
980
+ // Re-enable input
981
+ input.disabled = false;
982
+ document.querySelector('#aiChatInputArea button').disabled = false;
983
+ input.focus();
984
+ }
985
+
986
+ } catch (error) {
987
+ console.error('Error sending message:', error);
988
+ showToast(error.message, 'error');
989
+
990
+ // Remove typing indicator
991
+ const indicator = document.getElementById('aiTypingIndicator');
992
+ if (indicator) indicator.remove();
993
+
994
+ // Re-enable input
995
+ input.disabled = false;
996
+ document.querySelector('#aiChatInputArea button').disabled = false;
997
+ }
998
+ }
999
+
1000
+ /**
1001
+ * Handle completion of AI creation
1002
+ */
1003
+ function handleAICreationComplete(actionId) {
1004
+ aiCreatedActionId = actionId;
1005
+
1006
+ // Update status badge
1007
+ document.getElementById('aiChatStatus').textContent = 'Complete';
1008
+ document.getElementById('aiChatStatus').classList.remove('bg-primary');
1009
+ document.getElementById('aiChatStatus').classList.add('bg-success');
1010
+
1011
+ // Hide input, show completion message
1012
+ document.getElementById('aiChatInputArea').classList.add('d-none');
1013
+ document.getElementById('aiChatComplete').classList.remove('d-none');
1014
+
1015
+ // Update footer
1016
+ document.getElementById('aiCreateFooter').innerHTML = `
1017
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
1018
+ <button type="button" class="btn btn-primary" onclick="viewCreatedAction()">
1019
+ <i class="bi bi-eye"></i> View Action
1020
+ </button>
1021
+ `;
1022
+
1023
+ // Refresh actions list
1024
+ loadActions();
1025
+ }
1026
+
1027
+ /**
1028
+ * Cancel the AI creation session
1029
+ */
1030
+ async function cancelAICreation() {
1031
+ if (aiCreationId) {
1032
+ try {
1033
+ await fetch(`/api/actions/ai-create/${aiCreationId}`, { method: 'DELETE' });
1034
+ } catch (error) {
1035
+ console.error('Error cancelling creation:', error);
1036
+ }
1037
+ }
1038
+
1039
+ aiCreationId = null;
1040
+ aiCreatedActionId = null;
1041
+
1042
+ // Close modal
1043
+ const modal = bootstrap.Modal.getInstance(document.getElementById('aiCreateModal'));
1044
+ if (modal) modal.hide();
1045
+ }
1046
+
1047
+ /**
1048
+ * View the action that was just created
1049
+ */
1050
+ function viewCreatedAction() {
1051
+ if (aiCreatedActionId) {
1052
+ // Close AI modal
1053
+ const modal = bootstrap.Modal.getInstance(document.getElementById('aiCreateModal'));
1054
+ if (modal) modal.hide();
1055
+
1056
+ // View the action
1057
+ viewAction(aiCreatedActionId);
1058
+ }
1059
+ }
1060
+
1061
+ /**
1062
+ * Add a message to the AI chat
1063
+ */
1064
+ function addAIChatMessage(text, role) {
1065
+ const container = document.getElementById('aiChatMessages');
1066
+
1067
+ const messageDiv = document.createElement('div');
1068
+ messageDiv.className = `mb-2 p-2 rounded ${role === 'user' ? 'bg-primary text-white ms-5' : 'bg-white border me-5'}`;
1069
+
1070
+ // Simple markdown rendering for AI responses
1071
+ if (role === 'assistant') {
1072
+ // Convert markdown bold and line breaks
1073
+ let html = escapeHtml(text);
1074
+ html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
1075
+ html = html.replace(/\n/g, '<br>');
1076
+ messageDiv.innerHTML = html;
1077
+ } else {
1078
+ messageDiv.textContent = text;
1079
+ }
1080
+
1081
+ container.appendChild(messageDiv);
1082
+ scrollChatToBottom();
1083
+ }
1084
+
1085
+ /**
1086
+ * Scroll the chat container to the bottom
1087
+ */
1088
+ function scrollChatToBottom() {
1089
+ const container = document.getElementById('aiChatMessages');
1090
+ container.scrollTop = container.scrollHeight;
1091
+ }
1092
+
1093
+ /**
1094
+ * Handle Enter key press in AI chat input
1095
+ */
1096
+ function handleAIChatKeypress(event) {
1097
+ if (event.key === 'Enter' && !event.shiftKey) {
1098
+ event.preventDefault();
1099
+ sendAICreationMessage();
1100
+ }
1101
+ }