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,599 @@
1
+ """
2
+ Tools for prompt-driven autonomous action creation.
3
+
4
+ Exposes tools that an LLM can use to create scheduled actions through
5
+ natural conversation.
6
+ """
7
+
8
+ from typing import List, Dict, Any, Optional
9
+ from datetime import datetime
10
+ import json
11
+ import logging
12
+ import asyncio
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ # System instructions for the creation LLM
18
+ ACTION_CREATION_SYSTEM_PROMPT = """You are an assistant helping the user create an autonomous scheduled action. Your role is to:
19
+
20
+ 1. UNDERSTAND the user's requirements for their scheduled task
21
+ 2. GATHER all necessary information through conversation:
22
+ - What the task should do (action prompt)
23
+ - When it should run (schedule)
24
+ - Whether it needs fresh context each run or should build on previous runs
25
+ - What tools it will need access to
26
+
27
+ 3. ASK CLARIFYING QUESTIONS if any of the following are unclear:
28
+ - The schedule timing (be specific: "Every weekday at 8am" vs "Every day")
29
+ - What specific actions the AI should take
30
+ - Whether results should persist across runs (context mode)
31
+ - What capabilities (tools) are needed
32
+
33
+ 4. INFER APPROPRIATE TOOLS based on the task description:
34
+ - Use the list_available_tools function to see what's available
35
+ - Select only the tools necessary for the described task
36
+ - If unsure, ask the user to confirm your tool selection
37
+
38
+ 5. VALIDATE the schedule using validate_schedule before creating
39
+
40
+ 6. GENERATE a suitable system prompt for the scheduled task that:
41
+ - Defines the AI's role clearly
42
+ - Provides context about the task's purpose
43
+ - Includes any constraints or requirements
44
+
45
+ 7. PRESENT A SUMMARY for confirmation before creating:
46
+ - Action name and description
47
+ - Schedule (human-readable)
48
+ - Context mode
49
+ - Selected tools
50
+ - Generated system prompt (brief summary)
51
+ - Action prompt
52
+
53
+ 8. Only call create_autonomous_action AFTER the user confirms the summary
54
+
55
+ IMPORTANT:
56
+ - If the user types "cancel", acknowledge and stop the creation process
57
+ - Be concise but thorough in your questions
58
+ - Suggest sensible defaults when appropriate (e.g., fresh context mode for most tasks)
59
+ - Explain cron expressions in plain language when presenting summaries
60
+ - Default max_failures to 3 unless the user specifies otherwise
61
+ """
62
+
63
+
64
+ def get_action_creation_tools() -> List[Dict[str, Any]]:
65
+ """
66
+ Return tool definitions for action creation.
67
+
68
+ These tools are exposed to the LLM to help create autonomous actions
69
+ through natural conversation.
70
+
71
+ Returns:
72
+ List of tool definitions in Claude API format
73
+ """
74
+ return [
75
+ {
76
+ 'name': 'list_available_tools',
77
+ 'description': (
78
+ 'List all available tools that can be assigned to an autonomous action. '
79
+ 'Use this to see what capabilities are available before selecting tools '
80
+ 'for the scheduled task.'
81
+ ),
82
+ 'input_schema': {
83
+ 'type': 'object',
84
+ 'properties': {},
85
+ 'required': []
86
+ }
87
+ },
88
+ {
89
+ 'name': 'validate_schedule',
90
+ 'description': (
91
+ 'Validate a schedule configuration before creating an action. '
92
+ 'Checks if the cron expression or datetime is valid and returns '
93
+ 'a human-readable description of when the action will run.'
94
+ ),
95
+ 'input_schema': {
96
+ 'type': 'object',
97
+ 'properties': {
98
+ 'schedule_type': {
99
+ 'type': 'string',
100
+ 'enum': ['one_off', 'recurring'],
101
+ 'description': 'Type of schedule: one_off for single execution, recurring for repeated execution'
102
+ },
103
+ 'schedule_value': {
104
+ 'type': 'string',
105
+ 'description': (
106
+ 'For one_off: datetime in format "YYYY-MM-DD HH:MM" (e.g., "2025-12-20 14:30"). '
107
+ 'For recurring: cron expression with 5 fields (minute hour day month day_of_week), '
108
+ 'e.g., "0 8 * * MON-FRI" for weekdays at 8am, "0 9 * * *" for every day at 9am.'
109
+ )
110
+ }
111
+ },
112
+ 'required': ['schedule_type', 'schedule_value']
113
+ }
114
+ },
115
+ {
116
+ 'name': 'create_autonomous_action',
117
+ 'description': (
118
+ 'Create a new autonomous action with the specified configuration. '
119
+ 'Only call this AFTER presenting a summary to the user and receiving their confirmation.'
120
+ ),
121
+ 'input_schema': {
122
+ 'type': 'object',
123
+ 'properties': {
124
+ 'name': {
125
+ 'type': 'string',
126
+ 'description': 'Unique name for the action (e.g., "Daily Cost Report")'
127
+ },
128
+ 'description': {
129
+ 'type': 'string',
130
+ 'description': 'Human-readable description of what the action does'
131
+ },
132
+ 'action_prompt': {
133
+ 'type': 'string',
134
+ 'description': 'The prompt/instructions for the AI to execute when the action runs'
135
+ },
136
+ 'system_prompt': {
137
+ 'type': 'string',
138
+ 'description': 'System instructions that define the AI\'s role and constraints for this task'
139
+ },
140
+ 'schedule_type': {
141
+ 'type': 'string',
142
+ 'enum': ['one_off', 'recurring'],
143
+ 'description': 'Type of schedule'
144
+ },
145
+ 'schedule_value': {
146
+ 'type': 'string',
147
+ 'description': 'Cron expression or datetime string'
148
+ },
149
+ 'context_mode': {
150
+ 'type': 'string',
151
+ 'enum': ['fresh', 'cumulative'],
152
+ 'description': (
153
+ 'fresh: Start with empty context each run (default). '
154
+ 'cumulative: Build on previous run\'s context.'
155
+ )
156
+ },
157
+ 'max_failures': {
158
+ 'type': 'integer',
159
+ 'description': 'Auto-disable action after this many consecutive failures (default: 3)'
160
+ },
161
+ 'max_tokens': {
162
+ 'type': 'integer',
163
+ 'description': (
164
+ 'Maximum tokens for LLM response (default: 8192). '
165
+ 'Use higher values (e.g., 16384) for tasks generating large content like reports.'
166
+ )
167
+ },
168
+ 'tool_names': {
169
+ 'type': 'array',
170
+ 'items': {'type': 'string'},
171
+ 'description': 'List of tool names the action is allowed to use'
172
+ }
173
+ },
174
+ 'required': ['name', 'description', 'action_prompt', 'system_prompt',
175
+ 'schedule_type', 'schedule_value', 'tool_names']
176
+ }
177
+ }
178
+ ]
179
+
180
+
181
+ def execute_creation_tool(
182
+ tool_name: str,
183
+ tool_input: Dict[str, Any],
184
+ mcp_manager,
185
+ database,
186
+ scheduler_manager,
187
+ model_id: str,
188
+ user_guid: str,
189
+ config: Optional[Dict[str, Any]] = None,
190
+ available_tools: Optional[List[Dict[str, Any]]] = None
191
+ ) -> Dict[str, Any]:
192
+ """
193
+ Execute an action creation tool.
194
+
195
+ Args:
196
+ tool_name: Name of the tool to execute
197
+ tool_input: Input parameters for the tool
198
+ mcp_manager: MCP manager instance for listing tools (legacy, prefer available_tools)
199
+ database: Database instance for creating actions
200
+ scheduler_manager: Scheduler manager for scheduling actions
201
+ model_id: Model ID to lock for the action
202
+ user_guid: User GUID for multi-user isolation
203
+ config: Optional application config for builtin tools
204
+ available_tools: Pre-fetched list of available tools (preferred over mcp_manager)
205
+
206
+ Returns:
207
+ Result dictionary from the tool execution
208
+ """
209
+ logger.debug(f"Executing creation tool: {tool_name} with input: {tool_input}")
210
+
211
+ try:
212
+ if tool_name == 'list_available_tools':
213
+ # Use pre-fetched tools if available, otherwise fall back to fetching
214
+ if available_tools is not None:
215
+ return {
216
+ 'tools': available_tools,
217
+ 'count': len(available_tools),
218
+ 'message': f"Found {len(available_tools)} available tools"
219
+ }
220
+ return _list_available_tools(mcp_manager, config)
221
+
222
+ elif tool_name == 'validate_schedule':
223
+ return _validate_schedule(
224
+ tool_input.get('schedule_type', ''),
225
+ tool_input.get('schedule_value', '')
226
+ )
227
+
228
+ elif tool_name == 'create_autonomous_action':
229
+ return _create_action(
230
+ tool_input,
231
+ database,
232
+ scheduler_manager,
233
+ model_id,
234
+ user_guid
235
+ )
236
+
237
+ else:
238
+ return {'error': f'Unknown tool: {tool_name}'}
239
+
240
+ except Exception as e:
241
+ logger.error(f"Error executing creation tool {tool_name}: {e}", exc_info=True)
242
+ return {'error': str(e)}
243
+
244
+
245
+ def _list_available_tools(mcp_manager, config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
246
+ """
247
+ List all available tools with descriptions.
248
+
249
+ Args:
250
+ mcp_manager: MCP manager instance
251
+ config: Optional application config for builtin tools
252
+
253
+ Returns:
254
+ Dictionary with tools list and count
255
+ """
256
+ tools = []
257
+ errors = []
258
+
259
+ # Get built-in tools (including filesystem tools if enabled in config)
260
+ try:
261
+ from dtSpark.tools import builtin
262
+ builtin_config = config or {}
263
+ for tool in builtin.get_builtin_tools(builtin_config):
264
+ tools.append({
265
+ 'name': tool['name'],
266
+ 'description': tool.get('description', 'No description available'),
267
+ 'source': 'builtin'
268
+ })
269
+ logger.debug(f"Loaded {len(tools)} builtin tools")
270
+ except Exception as e:
271
+ logger.warning(f"Failed to get built-in tools: {e}")
272
+ errors.append(f"Builtin tools error: {e}")
273
+
274
+ # Get MCP tools
275
+ if mcp_manager:
276
+ logger.debug(f"MCP manager present: {type(mcp_manager)}")
277
+ try:
278
+ # Handle async MCP manager
279
+ if hasattr(mcp_manager, 'list_all_tools'):
280
+ # Check if there's an initialization loop stored
281
+ loop = getattr(mcp_manager, '_initialization_loop', None)
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:
316
+ logger.debug("No MCP manager provided")
317
+
318
+ result = {
319
+ 'tools': tools,
320
+ 'count': len(tools),
321
+ 'message': f"Found {len(tools)} available tools"
322
+ }
323
+
324
+ if errors:
325
+ result['warnings'] = errors
326
+
327
+ return result
328
+
329
+
330
+ def _validate_schedule(schedule_type: str, schedule_value: str) -> Dict[str, Any]:
331
+ """
332
+ Validate schedule configuration.
333
+
334
+ Args:
335
+ schedule_type: 'one_off' or 'recurring'
336
+ schedule_value: Datetime string or cron expression
337
+
338
+ Returns:
339
+ Validation result with human-readable description
340
+ """
341
+ if not schedule_type:
342
+ return {'valid': False, 'error': 'schedule_type is required'}
343
+
344
+ if not schedule_value:
345
+ return {'valid': False, 'error': 'schedule_value is required'}
346
+
347
+ if schedule_type == 'one_off':
348
+ try:
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
+ }
364
+
365
+ if dt <= datetime.now():
366
+ return {
367
+ 'valid': False,
368
+ 'error': 'Date must be in the future'
369
+ }
370
+
371
+ return {
372
+ 'valid': True,
373
+ 'schedule_type': 'one_off',
374
+ 'parsed': dt.isoformat(),
375
+ 'human_readable': dt.strftime('%A, %d %B %Y at %H:%M')
376
+ }
377
+
378
+ except Exception as e:
379
+ return {'valid': False, 'error': f'Invalid datetime: {e}'}
380
+
381
+ elif schedule_type == 'recurring':
382
+ try:
383
+ from apscheduler.triggers.cron import CronTrigger
384
+
385
+ # Parse cron expression
386
+ parts = schedule_value.split()
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
+ }
395
+
396
+ minute, hour, day, month, dow = parts
397
+
398
+ # Validate by creating trigger (will raise if invalid)
399
+ trigger = CronTrigger(
400
+ minute=minute,
401
+ hour=hour,
402
+ day=day,
403
+ month=month,
404
+ day_of_week=dow
405
+ )
406
+
407
+ # Get next run time to confirm it works
408
+ next_run = trigger.get_next_fire_time(None, datetime.now())
409
+
410
+ return {
411
+ 'valid': True,
412
+ 'schedule_type': 'recurring',
413
+ 'cron_expression': schedule_value,
414
+ 'human_readable': _cron_to_human(schedule_value),
415
+ 'next_run': next_run.isoformat() if next_run else None
416
+ }
417
+
418
+ except Exception as e:
419
+ return {
420
+ 'valid': False,
421
+ 'error': f'Invalid cron expression: {e}'
422
+ }
423
+
424
+ else:
425
+ return {
426
+ 'valid': False,
427
+ 'error': f'Unknown schedule type: {schedule_type}. Use "one_off" or "recurring"'
428
+ }
429
+
430
+
431
+ def _cron_to_human(cron: str) -> str:
432
+ """
433
+ Convert cron expression to human-readable string.
434
+
435
+ Args:
436
+ cron: 5-field cron expression
437
+
438
+ Returns:
439
+ Human-readable description
440
+ """
441
+ parts = cron.split()
442
+ if len(parts) != 5:
443
+ return f"Cron: {cron}"
444
+
445
+ minute, hour, day, month, dow = parts
446
+
447
+ # Build time string
448
+ if minute == '0':
449
+ time_str = f"{hour}:00"
450
+ elif minute.isdigit():
451
+ time_str = f"{hour}:{minute.zfill(2)}"
452
+ else:
453
+ time_str = f"{hour}:{minute}"
454
+
455
+ # Build day/frequency description
456
+ if dow == '*' and day == '*' and month == '*':
457
+ freq = "Every day"
458
+ elif dow == 'MON-FRI' or dow == '1-5':
459
+ freq = "Weekdays (Monday to Friday)"
460
+ elif dow == 'SAT,SUN' or dow == '0,6' or dow == '6,0':
461
+ freq = "Weekends"
462
+ elif dow == '0' or dow.upper() == 'SUN':
463
+ freq = "Every Sunday"
464
+ elif dow == '1' or dow.upper() == 'MON':
465
+ freq = "Every Monday"
466
+ elif dow == '2' or dow.upper() == 'TUE':
467
+ freq = "Every Tuesday"
468
+ elif dow == '3' or dow.upper() == 'WED':
469
+ freq = "Every Wednesday"
470
+ elif dow == '4' or dow.upper() == 'THU':
471
+ freq = "Every Thursday"
472
+ elif dow == '5' or dow.upper() == 'FRI':
473
+ freq = "Every Friday"
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:
479
+ interval = minute.split('/')[1]
480
+ return f"Every {interval} minutes"
481
+ elif '/' in hour:
482
+ interval = hour.split('/')[1]
483
+ return f"Every {interval} hours"
484
+ else:
485
+ return f"Cron: {cron}"
486
+
487
+ return f"{freq} at {time_str}"
488
+
489
+
490
+ def _create_action(
491
+ params: Dict[str, Any],
492
+ database,
493
+ scheduler_manager,
494
+ model_id: str,
495
+ user_guid: str
496
+ ) -> Dict[str, Any]:
497
+ """
498
+ Create the autonomous action in the database and schedule it.
499
+
500
+ Args:
501
+ params: Action parameters from the LLM
502
+ database: ConversationDatabase instance
503
+ scheduler_manager: Scheduler manager instance
504
+ model_id: Model ID to lock for the action
505
+ user_guid: User GUID (not used - database uses its own user_guid)
506
+
507
+ Returns:
508
+ Result dictionary with success status
509
+ """
510
+ # Validate required fields
511
+ required = ['name', 'description', 'action_prompt', 'system_prompt',
512
+ 'schedule_type', 'schedule_value', 'tool_names']
513
+ missing = [f for f in required if not params.get(f)]
514
+ if missing:
515
+ return {
516
+ 'success': False,
517
+ 'error': f'Missing required fields: {", ".join(missing)}'
518
+ }
519
+
520
+ # Build schedule config (as dict, not JSON string)
521
+ if params['schedule_type'] == 'one_off':
522
+ schedule_config = {'run_date': params['schedule_value']}
523
+ else:
524
+ schedule_config = {'cron_expression': params['schedule_value']}
525
+
526
+ try:
527
+ # Check for duplicate name using database wrapper method
528
+ existing = database.get_action_by_name(params['name'])
529
+ if existing:
530
+ return {
531
+ 'success': False,
532
+ 'error': f'An action named "{params["name"]}" already exists'
533
+ }
534
+
535
+ # Combine system_prompt with action_prompt for storage
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']}"
540
+
541
+ # Create action using database wrapper method
542
+ action_id = database.create_action(
543
+ name=params['name'],
544
+ description=params['description'],
545
+ action_prompt=full_prompt,
546
+ model_id=model_id,
547
+ schedule_type=params['schedule_type'],
548
+ schedule_config=schedule_config,
549
+ context_mode=params.get('context_mode', 'fresh'),
550
+ max_failures=params.get('max_failures', 3),
551
+ max_tokens=params.get('max_tokens', 8192)
552
+ )
553
+
554
+ # Set tool permissions using database wrapper method
555
+ tool_names = params.get('tool_names', [])
556
+ if tool_names:
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}")
581
+
582
+ # Build success message
583
+ schedule_desc = _cron_to_human(params['schedule_value']) if params['schedule_type'] == 'recurring' else params['schedule_value']
584
+
585
+ return {
586
+ 'success': True,
587
+ 'action_id': action_id,
588
+ 'name': params['name'],
589
+ 'schedule': schedule_desc,
590
+ 'next_run': next_run.isoformat() if next_run else None,
591
+ 'message': f"Action '{params['name']}' created successfully and scheduled"
592
+ }
593
+
594
+ except Exception as e:
595
+ logger.error(f"Failed to create action: {e}", exc_info=True)
596
+ return {
597
+ 'success': False,
598
+ 'error': str(e)
599
+ }