dtSpark 1.1.0a3__py3-none-any.whl → 1.1.0a7__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/_version.txt +1 -1
- dtSpark/aws/authentication.py +1 -1
- dtSpark/aws/bedrock.py +238 -239
- dtSpark/aws/costs.py +9 -5
- dtSpark/aws/pricing.py +25 -21
- dtSpark/cli_interface.py +77 -68
- dtSpark/conversation_manager.py +54 -47
- dtSpark/core/application.py +114 -91
- dtSpark/core/context_compaction.py +241 -226
- dtSpark/daemon/__init__.py +36 -22
- dtSpark/daemon/action_monitor.py +46 -17
- dtSpark/daemon/daemon_app.py +126 -104
- dtSpark/daemon/daemon_manager.py +59 -23
- dtSpark/daemon/pid_file.py +3 -2
- dtSpark/database/autonomous_actions.py +3 -0
- dtSpark/database/credential_prompt.py +52 -54
- dtSpark/files/manager.py +6 -12
- dtSpark/limits/__init__.py +1 -1
- dtSpark/limits/tokens.py +2 -2
- dtSpark/llm/anthropic_direct.py +246 -141
- dtSpark/llm/ollama.py +3 -1
- dtSpark/mcp_integration/manager.py +4 -4
- dtSpark/mcp_integration/tool_selector.py +83 -77
- dtSpark/resources/config.yaml.template +11 -0
- dtSpark/safety/patterns.py +45 -46
- dtSpark/safety/prompt_inspector.py +8 -1
- dtSpark/scheduler/creation_tools.py +273 -181
- dtSpark/scheduler/executor.py +503 -221
- dtSpark/tools/builtin.py +70 -53
- dtSpark/web/endpoints/autonomous_actions.py +12 -9
- dtSpark/web/endpoints/chat.py +8 -6
- dtSpark/web/endpoints/conversations.py +18 -9
- dtSpark/web/endpoints/main_menu.py +132 -105
- dtSpark/web/endpoints/streaming.py +2 -2
- dtSpark/web/server.py +70 -5
- dtSpark/web/ssl_utils.py +3 -3
- dtSpark/web/static/css/dark-theme.css +8 -29
- dtSpark/web/static/js/chat.js +6 -8
- dtSpark/web/static/js/main.js +8 -8
- dtSpark/web/static/js/sse-client.js +130 -122
- dtSpark/web/templates/actions.html +5 -5
- dtSpark/web/templates/base.html +15 -0
- dtSpark/web/templates/chat.html +10 -10
- dtSpark/web/templates/conversations.html +6 -2
- dtSpark/web/templates/goodbye.html +2 -2
- dtSpark/web/templates/main_menu.html +19 -17
- dtSpark/web/web_interface.py +2 -2
- {dtspark-1.1.0a3.dist-info → dtspark-1.1.0a7.dist-info}/METADATA +9 -2
- dtspark-1.1.0a7.dist-info/RECORD +96 -0
- dtspark-1.1.0a3.dist-info/RECORD +0 -96
- {dtspark-1.1.0a3.dist-info → dtspark-1.1.0a7.dist-info}/WHEEL +0 -0
- {dtspark-1.1.0a3.dist-info → dtspark-1.1.0a7.dist-info}/entry_points.txt +0 -0
- {dtspark-1.1.0a3.dist-info → dtspark-1.1.0a7.dist-info}/licenses/LICENSE +0 -0
- {dtspark-1.1.0a3.dist-info → dtspark-1.1.0a7.dist-info}/top_level.txt +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Tools for prompt-driven autonomous action creation.
|
|
3
3
|
|
|
4
4
|
Exposes tools that an LLM can use to create scheduled actions through
|
|
5
|
-
natural conversation.
|
|
5
|
+
natural conversation.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from typing import List, Dict, Any, Optional
|
|
@@ -256,7 +256,27 @@ def _list_available_tools(mcp_manager, config: Optional[Dict[str, Any]] = None)
|
|
|
256
256
|
tools = []
|
|
257
257
|
errors = []
|
|
258
258
|
|
|
259
|
-
|
|
259
|
+
_collect_builtin_tools(tools, errors, config)
|
|
260
|
+
_collect_mcp_tools(tools, errors, mcp_manager)
|
|
261
|
+
|
|
262
|
+
result = {
|
|
263
|
+
'tools': tools,
|
|
264
|
+
'count': len(tools),
|
|
265
|
+
'message': f"Found {len(tools)} available tools"
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if errors:
|
|
269
|
+
result['warnings'] = errors
|
|
270
|
+
|
|
271
|
+
return result
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _collect_builtin_tools(
|
|
275
|
+
tools: List[Dict[str, Any]],
|
|
276
|
+
errors: List[str],
|
|
277
|
+
config: Optional[Dict[str, Any]]
|
|
278
|
+
) -> None:
|
|
279
|
+
"""Collect built-in tools (including filesystem tools if enabled in config)."""
|
|
260
280
|
try:
|
|
261
281
|
from dtSpark.tools import builtin
|
|
262
282
|
builtin_config = config or {}
|
|
@@ -271,60 +291,65 @@ def _list_available_tools(mcp_manager, config: Optional[Dict[str, Any]] = None)
|
|
|
271
291
|
logger.warning(f"Failed to get built-in tools: {e}")
|
|
272
292
|
errors.append(f"Builtin tools error: {e}")
|
|
273
293
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if loop and not loop.is_closed():
|
|
284
|
-
# Use the existing loop
|
|
285
|
-
mcp_tools = loop.run_until_complete(mcp_manager.list_all_tools())
|
|
286
|
-
else:
|
|
287
|
-
# Create new loop
|
|
288
|
-
mcp_tools = mcp_manager.list_all_tools()
|
|
289
|
-
if asyncio.iscoroutine(mcp_tools):
|
|
290
|
-
try:
|
|
291
|
-
loop = asyncio.get_event_loop()
|
|
292
|
-
if loop.is_closed():
|
|
293
|
-
loop = asyncio.new_event_loop()
|
|
294
|
-
asyncio.set_event_loop(loop)
|
|
295
|
-
except RuntimeError:
|
|
296
|
-
loop = asyncio.new_event_loop()
|
|
297
|
-
asyncio.set_event_loop(loop)
|
|
298
|
-
mcp_tools = loop.run_until_complete(mcp_tools)
|
|
299
|
-
|
|
300
|
-
mcp_count = 0
|
|
301
|
-
for tool in mcp_tools:
|
|
302
|
-
tools.append({
|
|
303
|
-
'name': tool.get('name', 'unknown'),
|
|
304
|
-
'description': tool.get('description', 'No description available'),
|
|
305
|
-
'source': tool.get('server', 'mcp')
|
|
306
|
-
})
|
|
307
|
-
mcp_count += 1
|
|
308
|
-
logger.debug(f"Loaded {mcp_count} MCP tools")
|
|
309
|
-
else:
|
|
310
|
-
logger.warning("MCP manager does not have list_all_tools method")
|
|
311
|
-
errors.append("MCP manager missing list_all_tools method")
|
|
312
|
-
except Exception as e:
|
|
313
|
-
logger.warning(f"Failed to get MCP tools: {e}", exc_info=True)
|
|
314
|
-
errors.append(f"MCP tools error: {e}")
|
|
315
|
-
else:
|
|
294
|
+
|
|
295
|
+
def _collect_mcp_tools(
|
|
296
|
+
tools: List[Dict[str, Any]],
|
|
297
|
+
errors: List[str],
|
|
298
|
+
mcp_manager
|
|
299
|
+
) -> None:
|
|
300
|
+
"""Collect tools from the MCP manager, handling async resolution."""
|
|
301
|
+
if not mcp_manager:
|
|
316
302
|
logger.debug("No MCP manager provided")
|
|
303
|
+
return
|
|
317
304
|
|
|
318
|
-
|
|
319
|
-
'tools': tools,
|
|
320
|
-
'count': len(tools),
|
|
321
|
-
'message': f"Found {len(tools)} available tools"
|
|
322
|
-
}
|
|
305
|
+
logger.debug(f"MCP manager present: {type(mcp_manager)}")
|
|
323
306
|
|
|
324
|
-
if
|
|
325
|
-
|
|
307
|
+
if not hasattr(mcp_manager, 'list_all_tools'):
|
|
308
|
+
logger.warning("MCP manager does not have list_all_tools method")
|
|
309
|
+
errors.append("MCP manager missing list_all_tools method")
|
|
310
|
+
return
|
|
326
311
|
|
|
327
|
-
|
|
312
|
+
try:
|
|
313
|
+
mcp_tools = _resolve_mcp_tools(mcp_manager)
|
|
314
|
+
mcp_count = 0
|
|
315
|
+
for tool in mcp_tools:
|
|
316
|
+
tools.append({
|
|
317
|
+
'name': tool.get('name', 'unknown'),
|
|
318
|
+
'description': tool.get('description', 'No description available'),
|
|
319
|
+
'source': tool.get('server', 'mcp')
|
|
320
|
+
})
|
|
321
|
+
mcp_count += 1
|
|
322
|
+
logger.debug(f"Loaded {mcp_count} MCP tools")
|
|
323
|
+
except Exception as e:
|
|
324
|
+
logger.warning(f"Failed to get MCP tools: {e}", exc_info=True)
|
|
325
|
+
errors.append(f"MCP tools error: {e}")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _resolve_mcp_tools(mcp_manager) -> list:
|
|
329
|
+
"""Resolve MCP tools, handling both sync and async code paths."""
|
|
330
|
+
loop = getattr(mcp_manager, '_initialization_loop', None)
|
|
331
|
+
|
|
332
|
+
if loop and not loop.is_closed():
|
|
333
|
+
return loop.run_until_complete(mcp_manager.list_all_tools())
|
|
334
|
+
|
|
335
|
+
mcp_tools = mcp_manager.list_all_tools()
|
|
336
|
+
if asyncio.iscoroutine(mcp_tools):
|
|
337
|
+
loop = _get_or_create_event_loop()
|
|
338
|
+
mcp_tools = loop.run_until_complete(mcp_tools)
|
|
339
|
+
return mcp_tools
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _get_or_create_event_loop() -> asyncio.AbstractEventLoop:
|
|
343
|
+
"""Return an open event loop, creating one if necessary."""
|
|
344
|
+
try:
|
|
345
|
+
loop = asyncio.get_event_loop()
|
|
346
|
+
if not loop.is_closed():
|
|
347
|
+
return loop
|
|
348
|
+
except RuntimeError:
|
|
349
|
+
pass
|
|
350
|
+
loop = asyncio.new_event_loop()
|
|
351
|
+
asyncio.set_event_loop(loop)
|
|
352
|
+
return loop
|
|
328
353
|
|
|
329
354
|
|
|
330
355
|
def _validate_schedule(schedule_type: str, schedule_value: str) -> Dict[str, Any]:
|
|
@@ -345,89 +370,119 @@ def _validate_schedule(schedule_type: str, schedule_value: str) -> Dict[str, Any
|
|
|
345
370
|
return {'valid': False, 'error': 'schedule_value is required'}
|
|
346
371
|
|
|
347
372
|
if schedule_type == 'one_off':
|
|
348
|
-
|
|
349
|
-
# Try multiple datetime formats
|
|
350
|
-
dt = None
|
|
351
|
-
formats = ['%Y-%m-%d %H:%M', '%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S']
|
|
352
|
-
for fmt in formats:
|
|
353
|
-
try:
|
|
354
|
-
dt = datetime.strptime(schedule_value, fmt)
|
|
355
|
-
break
|
|
356
|
-
except ValueError:
|
|
357
|
-
continue
|
|
358
|
-
|
|
359
|
-
if dt is None:
|
|
360
|
-
return {
|
|
361
|
-
'valid': False,
|
|
362
|
-
'error': f'Invalid datetime format. Use YYYY-MM-DD HH:MM (e.g., "2025-12-20 14:30")'
|
|
363
|
-
}
|
|
373
|
+
return _validate_one_off_schedule(schedule_value)
|
|
364
374
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
375
|
+
if schedule_type == 'recurring':
|
|
376
|
+
return _validate_recurring_schedule(schedule_value)
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
'valid': False,
|
|
380
|
+
'error': f'Unknown schedule type: {schedule_type}. Use "one_off" or "recurring"'
|
|
381
|
+
}
|
|
370
382
|
|
|
383
|
+
|
|
384
|
+
def _validate_one_off_schedule(schedule_value: str) -> Dict[str, Any]:
|
|
385
|
+
"""Validate a one-off (single execution) schedule value."""
|
|
386
|
+
try:
|
|
387
|
+
dt = _parse_datetime(schedule_value)
|
|
388
|
+
|
|
389
|
+
if dt is None:
|
|
371
390
|
return {
|
|
372
|
-
'valid':
|
|
373
|
-
'
|
|
374
|
-
'parsed': dt.isoformat(),
|
|
375
|
-
'human_readable': dt.strftime('%A, %d %B %Y at %H:%M')
|
|
391
|
+
'valid': False,
|
|
392
|
+
'error': 'Invalid datetime format. Use YYYY-MM-DD HH:MM (e.g., "2025-12-20 14:30")'
|
|
376
393
|
}
|
|
377
394
|
|
|
378
|
-
|
|
379
|
-
return {
|
|
395
|
+
if dt <= datetime.now():
|
|
396
|
+
return {
|
|
397
|
+
'valid': False,
|
|
398
|
+
'error': 'Date must be in the future'
|
|
399
|
+
}
|
|
380
400
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
401
|
+
return {
|
|
402
|
+
'valid': True,
|
|
403
|
+
'schedule_type': 'one_off',
|
|
404
|
+
'parsed': dt.isoformat(),
|
|
405
|
+
'human_readable': dt.strftime('%A, %d %B %Y at %H:%M')
|
|
406
|
+
}
|
|
384
407
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if len(parts) != 5:
|
|
388
|
-
return {
|
|
389
|
-
'valid': False,
|
|
390
|
-
'error': (
|
|
391
|
-
'Cron expression must have 5 fields: minute hour day month day_of_week. '
|
|
392
|
-
'Example: "0 8 * * MON-FRI" for weekdays at 8am'
|
|
393
|
-
)
|
|
394
|
-
}
|
|
408
|
+
except Exception as e:
|
|
409
|
+
return {'valid': False, 'error': f'Invalid datetime: {e}'}
|
|
395
410
|
|
|
396
|
-
minute, hour, day, month, dow = parts
|
|
397
411
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
412
|
+
def _parse_datetime(value: str) -> Optional[datetime]:
|
|
413
|
+
"""Attempt to parse a datetime string using common formats."""
|
|
414
|
+
formats = ['%Y-%m-%d %H:%M', '%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S']
|
|
415
|
+
for fmt in formats:
|
|
416
|
+
try:
|
|
417
|
+
return datetime.strptime(value, fmt)
|
|
418
|
+
except ValueError:
|
|
419
|
+
continue
|
|
420
|
+
return None
|
|
406
421
|
|
|
407
|
-
# Get next run time to confirm it works
|
|
408
|
-
next_run = trigger.get_next_fire_time(None, datetime.now())
|
|
409
422
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
'human_readable': _cron_to_human(schedule_value),
|
|
415
|
-
'next_run': next_run.isoformat() if next_run else None
|
|
416
|
-
}
|
|
423
|
+
def _validate_recurring_schedule(schedule_value: str) -> Dict[str, Any]:
|
|
424
|
+
"""Validate a recurring (cron-based) schedule value."""
|
|
425
|
+
try:
|
|
426
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
417
427
|
|
|
418
|
-
|
|
428
|
+
parts = schedule_value.split()
|
|
429
|
+
if len(parts) != 5:
|
|
419
430
|
return {
|
|
420
431
|
'valid': False,
|
|
421
|
-
'error':
|
|
432
|
+
'error': (
|
|
433
|
+
'Cron expression must have 5 fields: minute hour day month day_of_week. '
|
|
434
|
+
'Example: "0 8 * * MON-FRI" for weekdays at 8am'
|
|
435
|
+
)
|
|
422
436
|
}
|
|
423
437
|
|
|
424
|
-
|
|
438
|
+
minute, hour, day, month, dow = parts
|
|
439
|
+
|
|
440
|
+
trigger = CronTrigger(
|
|
441
|
+
minute=minute,
|
|
442
|
+
hour=hour,
|
|
443
|
+
day=day,
|
|
444
|
+
month=month,
|
|
445
|
+
day_of_week=dow
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
next_run = trigger.get_next_fire_time(None, datetime.now())
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
'valid': True,
|
|
452
|
+
'schedule_type': 'recurring',
|
|
453
|
+
'cron_expression': schedule_value,
|
|
454
|
+
'human_readable': _cron_to_human(schedule_value),
|
|
455
|
+
'next_run': next_run.isoformat() if next_run else None
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
except Exception as e:
|
|
425
459
|
return {
|
|
426
460
|
'valid': False,
|
|
427
|
-
'error': f'
|
|
461
|
+
'error': f'Invalid cron expression: {e}'
|
|
428
462
|
}
|
|
429
463
|
|
|
430
464
|
|
|
465
|
+
# Mapping of day-of-week values to human-readable names.
|
|
466
|
+
_DOW_MAP = {
|
|
467
|
+
'MON-FRI': 'Weekdays (Monday to Friday)',
|
|
468
|
+
'1-5': 'Weekdays (Monday to Friday)',
|
|
469
|
+
'SAT,SUN': 'Weekends',
|
|
470
|
+
'0,6': 'Weekends',
|
|
471
|
+
'6,0': 'Weekends',
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
# Single-day mappings (value or uppercase alias -> label).
|
|
475
|
+
_SINGLE_DOW_MAP = {
|
|
476
|
+
'0': 'Every Sunday', 'SUN': 'Every Sunday',
|
|
477
|
+
'1': 'Every Monday', 'MON': 'Every Monday',
|
|
478
|
+
'2': 'Every Tuesday', 'TUE': 'Every Tuesday',
|
|
479
|
+
'3': 'Every Wednesday', 'WED': 'Every Wednesday',
|
|
480
|
+
'4': 'Every Thursday', 'THU': 'Every Thursday',
|
|
481
|
+
'5': 'Every Friday', 'FRI': 'Every Friday',
|
|
482
|
+
'6': 'Every Saturday', 'SAT': 'Every Saturday',
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
|
|
431
486
|
def _cron_to_human(cron: str) -> str:
|
|
432
487
|
"""
|
|
433
488
|
Convert cron expression to human-readable string.
|
|
@@ -444,47 +499,62 @@ def _cron_to_human(cron: str) -> str:
|
|
|
444
499
|
|
|
445
500
|
minute, hour, day, month, dow = parts
|
|
446
501
|
|
|
447
|
-
|
|
502
|
+
time_str = _format_cron_time(minute, hour)
|
|
503
|
+
freq = _describe_cron_frequency(minute, hour, day, month, dow)
|
|
504
|
+
|
|
505
|
+
if freq is None:
|
|
506
|
+
return f"Cron: {cron}"
|
|
507
|
+
|
|
508
|
+
# Interval-based frequencies already include timing info
|
|
509
|
+
if freq.startswith("Every ") and ("minutes" in freq or "hours" in freq):
|
|
510
|
+
return freq
|
|
511
|
+
|
|
512
|
+
return f"{freq} at {time_str}"
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _format_cron_time(minute: str, hour: str) -> str:
|
|
516
|
+
"""Build a human-readable time string from cron minute and hour fields."""
|
|
448
517
|
if minute == '0':
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
518
|
+
return f"{hour}:00"
|
|
519
|
+
if minute.isdigit():
|
|
520
|
+
return f"{hour}:{minute.zfill(2)}"
|
|
521
|
+
return f"{hour}:{minute}"
|
|
522
|
+
|
|
454
523
|
|
|
455
|
-
|
|
524
|
+
def _describe_cron_frequency(
|
|
525
|
+
minute: str, hour: str, day: str, month: str, dow: str
|
|
526
|
+
) -> Optional[str]:
|
|
527
|
+
"""
|
|
528
|
+
Derive a human-readable frequency description from cron fields.
|
|
529
|
+
|
|
530
|
+
Returns None when no known pattern matches.
|
|
531
|
+
"""
|
|
532
|
+
# Every day
|
|
456
533
|
if dow == '*' and day == '*' and month == '*':
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
elif dow == '6' or dow.upper() == 'SAT':
|
|
475
|
-
freq = "Every Saturday"
|
|
476
|
-
elif day != '*' and month == '*':
|
|
477
|
-
freq = f"Day {day} of each month"
|
|
478
|
-
elif '/' in minute:
|
|
534
|
+
return "Every day"
|
|
535
|
+
|
|
536
|
+
# Multi-day patterns (weekdays, weekends)
|
|
537
|
+
if dow in _DOW_MAP:
|
|
538
|
+
return _DOW_MAP[dow]
|
|
539
|
+
|
|
540
|
+
# Single named/numbered day of week
|
|
541
|
+
dow_label = _SINGLE_DOW_MAP.get(dow) or _SINGLE_DOW_MAP.get(dow.upper())
|
|
542
|
+
if dow_label:
|
|
543
|
+
return dow_label
|
|
544
|
+
|
|
545
|
+
# Day-of-month pattern
|
|
546
|
+
if day != '*' and month == '*':
|
|
547
|
+
return f"Day {day} of each month"
|
|
548
|
+
|
|
549
|
+
# Interval-based patterns
|
|
550
|
+
if '/' in minute:
|
|
479
551
|
interval = minute.split('/')[1]
|
|
480
552
|
return f"Every {interval} minutes"
|
|
481
|
-
|
|
553
|
+
if '/' in hour:
|
|
482
554
|
interval = hour.split('/')[1]
|
|
483
555
|
return f"Every {interval} hours"
|
|
484
|
-
else:
|
|
485
|
-
return f"Cron: {cron}"
|
|
486
556
|
|
|
487
|
-
return
|
|
557
|
+
return None
|
|
488
558
|
|
|
489
559
|
|
|
490
560
|
def _create_action(
|
|
@@ -532,13 +602,8 @@ def _create_action(
|
|
|
532
602
|
'error': f'An action named "{params["name"]}" already exists'
|
|
533
603
|
}
|
|
534
604
|
|
|
535
|
-
|
|
536
|
-
# The system_prompt becomes the instructions, action_prompt is the actual task
|
|
537
|
-
full_prompt = params['action_prompt']
|
|
538
|
-
if params.get('system_prompt'):
|
|
539
|
-
full_prompt = f"[System Instructions]\n{params['system_prompt']}\n\n[Task]\n{params['action_prompt']}"
|
|
605
|
+
full_prompt = _build_full_prompt(params)
|
|
540
606
|
|
|
541
|
-
# Create action using database wrapper method
|
|
542
607
|
action_id = database.create_action(
|
|
543
608
|
name=params['name'],
|
|
544
609
|
description=params['description'],
|
|
@@ -551,36 +616,16 @@ def _create_action(
|
|
|
551
616
|
max_tokens=params.get('max_tokens', 8192)
|
|
552
617
|
)
|
|
553
618
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
tool_permissions = [
|
|
558
|
-
{
|
|
559
|
-
'tool_name': t,
|
|
560
|
-
'server_name': None,
|
|
561
|
-
'permission_state': 'allowed'
|
|
562
|
-
}
|
|
563
|
-
for t in tool_names
|
|
564
|
-
]
|
|
565
|
-
database.set_action_tool_permissions_batch(action_id, tool_permissions)
|
|
566
|
-
|
|
567
|
-
# Schedule the action
|
|
568
|
-
next_run = None
|
|
569
|
-
if scheduler_manager:
|
|
570
|
-
try:
|
|
571
|
-
scheduler_manager.schedule_action(
|
|
572
|
-
action_id=action_id,
|
|
573
|
-
action_name=params['name'],
|
|
574
|
-
schedule_type=params['schedule_type'],
|
|
575
|
-
schedule_config=schedule_config,
|
|
576
|
-
user_guid=user_guid
|
|
577
|
-
)
|
|
578
|
-
next_run = scheduler_manager.get_next_run_time(action_id)
|
|
579
|
-
except Exception as e:
|
|
580
|
-
logger.warning(f"Failed to schedule action: {e}")
|
|
619
|
+
_set_tool_permissions(database, action_id, params.get('tool_names', []))
|
|
620
|
+
|
|
621
|
+
next_run = _schedule_action(scheduler_manager, action_id, params, schedule_config, user_guid)
|
|
581
622
|
|
|
582
623
|
# Build success message
|
|
583
|
-
schedule_desc =
|
|
624
|
+
schedule_desc = (
|
|
625
|
+
_cron_to_human(params['schedule_value'])
|
|
626
|
+
if params['schedule_type'] == 'recurring'
|
|
627
|
+
else params['schedule_value']
|
|
628
|
+
)
|
|
584
629
|
|
|
585
630
|
return {
|
|
586
631
|
'success': True,
|
|
@@ -597,3 +642,50 @@ def _create_action(
|
|
|
597
642
|
'success': False,
|
|
598
643
|
'error': str(e)
|
|
599
644
|
}
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _build_full_prompt(params: Dict[str, Any]) -> str:
|
|
648
|
+
"""Combine system_prompt and action_prompt into a single prompt for storage."""
|
|
649
|
+
full_prompt = params['action_prompt']
|
|
650
|
+
if params.get('system_prompt'):
|
|
651
|
+
full_prompt = f"[System Instructions]\n{params['system_prompt']}\n\n[Task]\n{params['action_prompt']}"
|
|
652
|
+
return full_prompt
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _set_tool_permissions(database, action_id: str, tool_names: List[str]) -> None:
|
|
656
|
+
"""Set tool permissions for the newly created action."""
|
|
657
|
+
if not tool_names:
|
|
658
|
+
return
|
|
659
|
+
tool_permissions = [
|
|
660
|
+
{
|
|
661
|
+
'tool_name': t,
|
|
662
|
+
'server_name': None,
|
|
663
|
+
'permission_state': 'allowed'
|
|
664
|
+
}
|
|
665
|
+
for t in tool_names
|
|
666
|
+
]
|
|
667
|
+
database.set_action_tool_permissions_batch(action_id, tool_permissions)
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def _schedule_action(
|
|
671
|
+
scheduler_manager,
|
|
672
|
+
action_id: str,
|
|
673
|
+
params: Dict[str, Any],
|
|
674
|
+
schedule_config: Dict[str, Any],
|
|
675
|
+
user_guid: str
|
|
676
|
+
):
|
|
677
|
+
"""Schedule the action and return the next run time, or None on failure."""
|
|
678
|
+
if not scheduler_manager:
|
|
679
|
+
return None
|
|
680
|
+
try:
|
|
681
|
+
scheduler_manager.schedule_action(
|
|
682
|
+
action_id=action_id,
|
|
683
|
+
action_name=params['name'],
|
|
684
|
+
schedule_type=params['schedule_type'],
|
|
685
|
+
schedule_config=schedule_config,
|
|
686
|
+
user_guid=user_guid
|
|
687
|
+
)
|
|
688
|
+
return scheduler_manager.get_next_run_time(action_id)
|
|
689
|
+
except Exception as e:
|
|
690
|
+
logger.warning(f"Failed to schedule action: {e}")
|
|
691
|
+
return None
|