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,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
|
+
}
|