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,1125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Autonomous Actions API endpoints.
|
|
3
|
+
|
|
4
|
+
Provides REST API for managing autonomous actions:
|
|
5
|
+
- List, create, update, delete actions
|
|
6
|
+
- View action runs and export results
|
|
7
|
+
- Enable/disable actions
|
|
8
|
+
- Trigger manual runs
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Optional, List
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
|
|
17
|
+
from fastapi import APIRouter, Depends, Request, HTTPException, Query
|
|
18
|
+
from fastapi.responses import PlainTextResponse, HTMLResponse
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
from ..dependencies import get_current_session
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
router = APIRouter()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_datetime(dt_value):
|
|
30
|
+
"""Parse datetime from string or return datetime object."""
|
|
31
|
+
if dt_value is None:
|
|
32
|
+
return None
|
|
33
|
+
if isinstance(dt_value, datetime):
|
|
34
|
+
return dt_value
|
|
35
|
+
if isinstance(dt_value, str):
|
|
36
|
+
try:
|
|
37
|
+
return datetime.fromisoformat(dt_value.replace('Z', '+00:00'))
|
|
38
|
+
except:
|
|
39
|
+
try:
|
|
40
|
+
return datetime.strptime(dt_value, '%Y-%m-%d %H:%M:%S.%f')
|
|
41
|
+
except:
|
|
42
|
+
return None
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Pydantic Models
|
|
47
|
+
|
|
48
|
+
class ScheduleConfig(BaseModel):
|
|
49
|
+
"""Schedule configuration for an action."""
|
|
50
|
+
run_date: Optional[str] = None # For one_off
|
|
51
|
+
cron_expression: Optional[str] = None # For recurring
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ToolPermission(BaseModel):
|
|
55
|
+
"""Tool permission for an action."""
|
|
56
|
+
tool_name: str
|
|
57
|
+
server_name: Optional[str] = None
|
|
58
|
+
permission_state: str = "allowed"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ActionCreate(BaseModel):
|
|
62
|
+
"""Request model for creating an action."""
|
|
63
|
+
name: str = Field(..., min_length=1, max_length=100)
|
|
64
|
+
description: str = Field(..., min_length=1, max_length=500)
|
|
65
|
+
action_prompt: str = Field(..., min_length=1)
|
|
66
|
+
model_id: str = Field(..., min_length=1)
|
|
67
|
+
schedule_type: str = Field(..., pattern="^(one_off|recurring)$")
|
|
68
|
+
schedule_config: ScheduleConfig
|
|
69
|
+
context_mode: str = Field(default="fresh", pattern="^(fresh|cumulative)$")
|
|
70
|
+
max_failures: int = Field(default=3, ge=1, le=100)
|
|
71
|
+
tool_permissions: Optional[List[ToolPermission]] = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ActionUpdate(BaseModel):
|
|
75
|
+
"""Request model for updating an action."""
|
|
76
|
+
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
|
77
|
+
description: Optional[str] = Field(None, min_length=1, max_length=500)
|
|
78
|
+
action_prompt: Optional[str] = Field(None, min_length=1)
|
|
79
|
+
schedule_type: Optional[str] = Field(None, pattern="^(one_off|recurring)$")
|
|
80
|
+
schedule_config: Optional[ScheduleConfig] = None
|
|
81
|
+
context_mode: Optional[str] = Field(None, pattern="^(fresh|cumulative)$")
|
|
82
|
+
max_failures: Optional[int] = Field(None, ge=1, le=100)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ActionSummary(BaseModel):
|
|
86
|
+
"""Summary information for an action."""
|
|
87
|
+
id: int
|
|
88
|
+
name: str
|
|
89
|
+
description: str
|
|
90
|
+
model_id: str
|
|
91
|
+
schedule_type: str
|
|
92
|
+
schedule_config: dict
|
|
93
|
+
context_mode: str
|
|
94
|
+
is_enabled: bool
|
|
95
|
+
failure_count: int
|
|
96
|
+
max_failures: int
|
|
97
|
+
last_run_at: Optional[datetime]
|
|
98
|
+
next_run_at: Optional[datetime]
|
|
99
|
+
created_at: datetime
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ActionDetail(BaseModel):
|
|
103
|
+
"""Detailed information for an action."""
|
|
104
|
+
id: int
|
|
105
|
+
name: str
|
|
106
|
+
description: str
|
|
107
|
+
action_prompt: str
|
|
108
|
+
model_id: str
|
|
109
|
+
schedule_type: str
|
|
110
|
+
schedule_config: dict
|
|
111
|
+
context_mode: str
|
|
112
|
+
is_enabled: bool
|
|
113
|
+
failure_count: int
|
|
114
|
+
max_failures: int
|
|
115
|
+
last_run_at: Optional[datetime]
|
|
116
|
+
next_run_at: Optional[datetime]
|
|
117
|
+
created_at: datetime
|
|
118
|
+
tool_permissions: List[dict]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ActionRunSummary(BaseModel):
|
|
122
|
+
"""Summary information for an action run."""
|
|
123
|
+
id: int
|
|
124
|
+
action_id: int
|
|
125
|
+
action_name: str
|
|
126
|
+
started_at: datetime
|
|
127
|
+
completed_at: Optional[datetime]
|
|
128
|
+
status: str
|
|
129
|
+
input_tokens: int
|
|
130
|
+
output_tokens: int
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class ActionRunDetail(BaseModel):
|
|
134
|
+
"""Detailed information for an action run."""
|
|
135
|
+
id: int
|
|
136
|
+
action_id: int
|
|
137
|
+
action_name: str
|
|
138
|
+
started_at: datetime
|
|
139
|
+
completed_at: Optional[datetime]
|
|
140
|
+
status: str
|
|
141
|
+
result_text: Optional[str]
|
|
142
|
+
result_html: Optional[str]
|
|
143
|
+
error_message: Optional[str]
|
|
144
|
+
input_tokens: int
|
|
145
|
+
output_tokens: int
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# Endpoints
|
|
149
|
+
|
|
150
|
+
@router.get("/actions")
|
|
151
|
+
async def list_actions(
|
|
152
|
+
request: Request,
|
|
153
|
+
include_disabled: bool = Query(True, description="Include disabled actions"),
|
|
154
|
+
session_id: str = Depends(get_current_session),
|
|
155
|
+
) -> List[ActionSummary]:
|
|
156
|
+
"""
|
|
157
|
+
List all autonomous actions.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
include_disabled: Whether to include disabled actions
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List of ActionSummary objects
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
app_instance = request.app.state.app_instance
|
|
167
|
+
database = app_instance.database
|
|
168
|
+
|
|
169
|
+
actions = database.get_all_actions(include_disabled=include_disabled)
|
|
170
|
+
|
|
171
|
+
return [
|
|
172
|
+
ActionSummary(
|
|
173
|
+
id=action['id'],
|
|
174
|
+
name=action['name'],
|
|
175
|
+
description=action['description'],
|
|
176
|
+
model_id=action['model_id'],
|
|
177
|
+
schedule_type=action['schedule_type'],
|
|
178
|
+
schedule_config=action.get('schedule_config', {}),
|
|
179
|
+
context_mode=action['context_mode'],
|
|
180
|
+
is_enabled=action['is_enabled'],
|
|
181
|
+
failure_count=action['failure_count'],
|
|
182
|
+
max_failures=action['max_failures'],
|
|
183
|
+
last_run_at=parse_datetime(action.get('last_run_at')),
|
|
184
|
+
next_run_at=parse_datetime(action.get('next_run_at')),
|
|
185
|
+
created_at=parse_datetime(action['created_at']),
|
|
186
|
+
)
|
|
187
|
+
for action in actions
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(f"Error listing actions: {e}")
|
|
192
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@router.get("/actions/{action_id}")
|
|
196
|
+
async def get_action(
|
|
197
|
+
action_id: int,
|
|
198
|
+
request: Request,
|
|
199
|
+
session_id: str = Depends(get_current_session),
|
|
200
|
+
) -> ActionDetail:
|
|
201
|
+
"""
|
|
202
|
+
Get detailed information about an action.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
action_id: ID of the action
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
ActionDetail object
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
app_instance = request.app.state.app_instance
|
|
212
|
+
database = app_instance.database
|
|
213
|
+
|
|
214
|
+
action = database.get_action(action_id)
|
|
215
|
+
if not action:
|
|
216
|
+
raise HTTPException(status_code=404, detail="Action not found")
|
|
217
|
+
|
|
218
|
+
tool_permissions = database.get_action_tool_permissions(action_id)
|
|
219
|
+
|
|
220
|
+
return ActionDetail(
|
|
221
|
+
id=action['id'],
|
|
222
|
+
name=action['name'],
|
|
223
|
+
description=action['description'],
|
|
224
|
+
action_prompt=action['action_prompt'],
|
|
225
|
+
model_id=action['model_id'],
|
|
226
|
+
schedule_type=action['schedule_type'],
|
|
227
|
+
schedule_config=action.get('schedule_config', {}),
|
|
228
|
+
context_mode=action['context_mode'],
|
|
229
|
+
is_enabled=action['is_enabled'],
|
|
230
|
+
failure_count=action['failure_count'],
|
|
231
|
+
max_failures=action['max_failures'],
|
|
232
|
+
last_run_at=parse_datetime(action.get('last_run_at')),
|
|
233
|
+
next_run_at=parse_datetime(action.get('next_run_at')),
|
|
234
|
+
created_at=parse_datetime(action['created_at']),
|
|
235
|
+
tool_permissions=tool_permissions,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
except HTTPException:
|
|
239
|
+
raise
|
|
240
|
+
except Exception as e:
|
|
241
|
+
logger.error(f"Error getting action {action_id}: {e}")
|
|
242
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@router.post("/actions")
|
|
246
|
+
async def create_action(
|
|
247
|
+
action_data: ActionCreate,
|
|
248
|
+
request: Request,
|
|
249
|
+
session_id: str = Depends(get_current_session),
|
|
250
|
+
) -> ActionDetail:
|
|
251
|
+
"""
|
|
252
|
+
Create a new autonomous action.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
action_data: Action creation data
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
ActionDetail for the created action
|
|
259
|
+
"""
|
|
260
|
+
try:
|
|
261
|
+
app_instance = request.app.state.app_instance
|
|
262
|
+
database = app_instance.database
|
|
263
|
+
|
|
264
|
+
# Check for duplicate name
|
|
265
|
+
existing = database.get_action_by_name(action_data.name)
|
|
266
|
+
if existing:
|
|
267
|
+
raise HTTPException(status_code=400, detail="An action with this name already exists")
|
|
268
|
+
|
|
269
|
+
# Create action
|
|
270
|
+
action_id = database.create_action(
|
|
271
|
+
name=action_data.name,
|
|
272
|
+
description=action_data.description,
|
|
273
|
+
action_prompt=action_data.action_prompt,
|
|
274
|
+
model_id=action_data.model_id,
|
|
275
|
+
schedule_type=action_data.schedule_type,
|
|
276
|
+
schedule_config=action_data.schedule_config.dict(),
|
|
277
|
+
context_mode=action_data.context_mode,
|
|
278
|
+
max_failures=action_data.max_failures,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Set tool permissions if provided
|
|
282
|
+
if action_data.tool_permissions:
|
|
283
|
+
permissions = [p.dict() for p in action_data.tool_permissions]
|
|
284
|
+
database.set_action_tool_permissions_batch(action_id, permissions)
|
|
285
|
+
|
|
286
|
+
# Schedule the action if scheduler is available
|
|
287
|
+
if hasattr(app_instance, 'action_scheduler') and app_instance.action_scheduler:
|
|
288
|
+
app_instance.action_scheduler.schedule_action(
|
|
289
|
+
action_id=action_id,
|
|
290
|
+
action_name=action_data.name,
|
|
291
|
+
schedule_type=action_data.schedule_type,
|
|
292
|
+
schedule_config=action_data.schedule_config.dict(),
|
|
293
|
+
user_guid=database.user_guid
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Return the created action
|
|
297
|
+
return await get_action(action_id, request, session_id)
|
|
298
|
+
|
|
299
|
+
except HTTPException:
|
|
300
|
+
raise
|
|
301
|
+
except Exception as e:
|
|
302
|
+
logger.error(f"Error creating action: {e}")
|
|
303
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@router.put("/actions/{action_id}")
|
|
307
|
+
async def update_action(
|
|
308
|
+
action_id: int,
|
|
309
|
+
action_data: ActionUpdate,
|
|
310
|
+
request: Request,
|
|
311
|
+
session_id: str = Depends(get_current_session),
|
|
312
|
+
) -> ActionDetail:
|
|
313
|
+
"""
|
|
314
|
+
Update an action.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
action_id: ID of the action to update
|
|
318
|
+
action_data: Update data
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Updated ActionDetail
|
|
322
|
+
"""
|
|
323
|
+
try:
|
|
324
|
+
app_instance = request.app.state.app_instance
|
|
325
|
+
database = app_instance.database
|
|
326
|
+
|
|
327
|
+
action = database.get_action(action_id)
|
|
328
|
+
if not action:
|
|
329
|
+
raise HTTPException(status_code=404, detail="Action not found")
|
|
330
|
+
|
|
331
|
+
# Build updates dict
|
|
332
|
+
updates = {}
|
|
333
|
+
if action_data.name is not None:
|
|
334
|
+
updates['name'] = action_data.name
|
|
335
|
+
if action_data.description is not None:
|
|
336
|
+
updates['description'] = action_data.description
|
|
337
|
+
if action_data.action_prompt is not None:
|
|
338
|
+
updates['action_prompt'] = action_data.action_prompt
|
|
339
|
+
if action_data.schedule_type is not None:
|
|
340
|
+
updates['schedule_type'] = action_data.schedule_type
|
|
341
|
+
if action_data.schedule_config is not None:
|
|
342
|
+
updates['schedule_config'] = action_data.schedule_config.dict()
|
|
343
|
+
if action_data.context_mode is not None:
|
|
344
|
+
updates['context_mode'] = action_data.context_mode
|
|
345
|
+
if action_data.max_failures is not None:
|
|
346
|
+
updates['max_failures'] = action_data.max_failures
|
|
347
|
+
|
|
348
|
+
if updates:
|
|
349
|
+
database.update_action(action_id, updates)
|
|
350
|
+
|
|
351
|
+
# Reschedule if schedule changed
|
|
352
|
+
if 'schedule_type' in updates or 'schedule_config' in updates:
|
|
353
|
+
if hasattr(app_instance, 'action_scheduler') and app_instance.action_scheduler:
|
|
354
|
+
updated_action = database.get_action(action_id)
|
|
355
|
+
app_instance.action_scheduler.schedule_action(
|
|
356
|
+
action_id=action_id,
|
|
357
|
+
action_name=updated_action['name'],
|
|
358
|
+
schedule_type=updated_action['schedule_type'],
|
|
359
|
+
schedule_config=updated_action['schedule_config'],
|
|
360
|
+
user_guid=database.user_guid
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
return await get_action(action_id, request, session_id)
|
|
364
|
+
|
|
365
|
+
except HTTPException:
|
|
366
|
+
raise
|
|
367
|
+
except Exception as e:
|
|
368
|
+
logger.error(f"Error updating action {action_id}: {e}")
|
|
369
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@router.delete("/actions/{action_id}")
|
|
373
|
+
async def delete_action(
|
|
374
|
+
action_id: int,
|
|
375
|
+
request: Request,
|
|
376
|
+
session_id: str = Depends(get_current_session),
|
|
377
|
+
) -> dict:
|
|
378
|
+
"""
|
|
379
|
+
Delete an action.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
action_id: ID of the action to delete
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
Status message
|
|
386
|
+
"""
|
|
387
|
+
try:
|
|
388
|
+
app_instance = request.app.state.app_instance
|
|
389
|
+
database = app_instance.database
|
|
390
|
+
|
|
391
|
+
action = database.get_action(action_id)
|
|
392
|
+
if not action:
|
|
393
|
+
raise HTTPException(status_code=404, detail="Action not found")
|
|
394
|
+
|
|
395
|
+
# Unschedule the action
|
|
396
|
+
if hasattr(app_instance, 'action_scheduler') and app_instance.action_scheduler:
|
|
397
|
+
app_instance.action_scheduler.unschedule_action(action_id)
|
|
398
|
+
|
|
399
|
+
# Delete from database
|
|
400
|
+
database.delete_action(action_id)
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
"status": "success",
|
|
404
|
+
"message": f"Action '{action['name']}' deleted successfully",
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
except HTTPException:
|
|
408
|
+
raise
|
|
409
|
+
except Exception as e:
|
|
410
|
+
logger.error(f"Error deleting action {action_id}: {e}")
|
|
411
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@router.post("/actions/{action_id}/enable")
|
|
415
|
+
async def enable_action(
|
|
416
|
+
action_id: int,
|
|
417
|
+
request: Request,
|
|
418
|
+
session_id: str = Depends(get_current_session),
|
|
419
|
+
) -> dict:
|
|
420
|
+
"""
|
|
421
|
+
Enable a disabled action.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
action_id: ID of the action
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Status message
|
|
428
|
+
"""
|
|
429
|
+
try:
|
|
430
|
+
app_instance = request.app.state.app_instance
|
|
431
|
+
database = app_instance.database
|
|
432
|
+
|
|
433
|
+
action = database.get_action(action_id)
|
|
434
|
+
if not action:
|
|
435
|
+
raise HTTPException(status_code=404, detail="Action not found")
|
|
436
|
+
|
|
437
|
+
database.enable_action(action_id)
|
|
438
|
+
|
|
439
|
+
# Reschedule the action
|
|
440
|
+
if hasattr(app_instance, 'action_scheduler') and app_instance.action_scheduler:
|
|
441
|
+
app_instance.action_scheduler.schedule_action(
|
|
442
|
+
action_id=action_id,
|
|
443
|
+
action_name=action['name'],
|
|
444
|
+
schedule_type=action['schedule_type'],
|
|
445
|
+
schedule_config=action['schedule_config'],
|
|
446
|
+
user_guid=database.user_guid
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
"status": "success",
|
|
451
|
+
"message": f"Action '{action['name']}' enabled",
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
except HTTPException:
|
|
455
|
+
raise
|
|
456
|
+
except Exception as e:
|
|
457
|
+
logger.error(f"Error enabling action {action_id}: {e}")
|
|
458
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@router.post("/actions/{action_id}/disable")
|
|
462
|
+
async def disable_action(
|
|
463
|
+
action_id: int,
|
|
464
|
+
request: Request,
|
|
465
|
+
session_id: str = Depends(get_current_session),
|
|
466
|
+
) -> dict:
|
|
467
|
+
"""
|
|
468
|
+
Disable an action.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
action_id: ID of the action
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
Status message
|
|
475
|
+
"""
|
|
476
|
+
try:
|
|
477
|
+
app_instance = request.app.state.app_instance
|
|
478
|
+
database = app_instance.database
|
|
479
|
+
|
|
480
|
+
action = database.get_action(action_id)
|
|
481
|
+
if not action:
|
|
482
|
+
raise HTTPException(status_code=404, detail="Action not found")
|
|
483
|
+
|
|
484
|
+
database.disable_action(action_id)
|
|
485
|
+
|
|
486
|
+
# Unschedule the action
|
|
487
|
+
if hasattr(app_instance, 'action_scheduler') and app_instance.action_scheduler:
|
|
488
|
+
app_instance.action_scheduler.unschedule_action(action_id)
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
"status": "success",
|
|
492
|
+
"message": f"Action '{action['name']}' disabled",
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
except HTTPException:
|
|
496
|
+
raise
|
|
497
|
+
except Exception as e:
|
|
498
|
+
logger.error(f"Error disabling action {action_id}: {e}")
|
|
499
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
@router.post("/actions/{action_id}/run-now")
|
|
503
|
+
async def run_action_now(
|
|
504
|
+
action_id: int,
|
|
505
|
+
request: Request,
|
|
506
|
+
session_id: str = Depends(get_current_session),
|
|
507
|
+
) -> dict:
|
|
508
|
+
"""
|
|
509
|
+
Trigger an action to run immediately.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
action_id: ID of the action
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
Status message
|
|
516
|
+
"""
|
|
517
|
+
try:
|
|
518
|
+
app_instance = request.app.state.app_instance
|
|
519
|
+
database = app_instance.database
|
|
520
|
+
|
|
521
|
+
action = database.get_action(action_id)
|
|
522
|
+
if not action:
|
|
523
|
+
raise HTTPException(status_code=404, detail="Action not found")
|
|
524
|
+
|
|
525
|
+
# Check if action is currently locked by another process (e.g., daemon)
|
|
526
|
+
from dtSpark.database.autonomous_actions import get_action_lock_info
|
|
527
|
+
lock_info = get_action_lock_info(
|
|
528
|
+
conn=database.conn,
|
|
529
|
+
action_id=action_id,
|
|
530
|
+
user_guid=database.user_guid
|
|
531
|
+
)
|
|
532
|
+
if lock_info and lock_info.get('locked_by'):
|
|
533
|
+
raise HTTPException(
|
|
534
|
+
status_code=409,
|
|
535
|
+
detail=f"Action is currently being executed by another process ({lock_info['locked_by']})"
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
# Trigger immediate execution
|
|
539
|
+
if hasattr(app_instance, 'action_scheduler') and app_instance.action_scheduler:
|
|
540
|
+
success = app_instance.action_scheduler.run_action_now(
|
|
541
|
+
action_id=action_id,
|
|
542
|
+
user_guid=database.user_guid
|
|
543
|
+
)
|
|
544
|
+
if not success:
|
|
545
|
+
raise HTTPException(status_code=500, detail="Failed to trigger action")
|
|
546
|
+
else:
|
|
547
|
+
raise HTTPException(status_code=503, detail="Action scheduler not available")
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
"status": "success",
|
|
551
|
+
"message": f"Action '{action['name']}' triggered for immediate execution",
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
except HTTPException:
|
|
555
|
+
raise
|
|
556
|
+
except Exception as e:
|
|
557
|
+
logger.error(f"Error triggering action {action_id}: {e}")
|
|
558
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
@router.get("/actions/{action_id}/runs")
|
|
562
|
+
async def list_action_runs(
|
|
563
|
+
action_id: int,
|
|
564
|
+
request: Request,
|
|
565
|
+
limit: int = Query(50, ge=1, le=100),
|
|
566
|
+
offset: int = Query(0, ge=0),
|
|
567
|
+
session_id: str = Depends(get_current_session),
|
|
568
|
+
) -> List[ActionRunSummary]:
|
|
569
|
+
"""
|
|
570
|
+
List runs for a specific action.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
action_id: ID of the action
|
|
574
|
+
limit: Maximum number of runs to return
|
|
575
|
+
offset: Offset for pagination
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
List of ActionRunSummary objects
|
|
579
|
+
"""
|
|
580
|
+
try:
|
|
581
|
+
app_instance = request.app.state.app_instance
|
|
582
|
+
database = app_instance.database
|
|
583
|
+
|
|
584
|
+
action = database.get_action(action_id)
|
|
585
|
+
if not action:
|
|
586
|
+
raise HTTPException(status_code=404, detail="Action not found")
|
|
587
|
+
|
|
588
|
+
runs = database.get_action_runs(action_id, limit=limit, offset=offset)
|
|
589
|
+
|
|
590
|
+
return [
|
|
591
|
+
ActionRunSummary(
|
|
592
|
+
id=run['id'],
|
|
593
|
+
action_id=run['action_id'],
|
|
594
|
+
action_name=run.get('action_name', action['name']),
|
|
595
|
+
started_at=parse_datetime(run['started_at']),
|
|
596
|
+
completed_at=parse_datetime(run.get('completed_at')),
|
|
597
|
+
status=run['status'],
|
|
598
|
+
input_tokens=run.get('input_tokens', 0),
|
|
599
|
+
output_tokens=run.get('output_tokens', 0),
|
|
600
|
+
)
|
|
601
|
+
for run in runs
|
|
602
|
+
]
|
|
603
|
+
|
|
604
|
+
except HTTPException:
|
|
605
|
+
raise
|
|
606
|
+
except Exception as e:
|
|
607
|
+
logger.error(f"Error listing runs for action {action_id}: {e}")
|
|
608
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@router.get("/actions/{action_id}/runs/{run_id}")
|
|
612
|
+
async def get_action_run(
|
|
613
|
+
action_id: int,
|
|
614
|
+
run_id: int,
|
|
615
|
+
request: Request,
|
|
616
|
+
session_id: str = Depends(get_current_session),
|
|
617
|
+
) -> ActionRunDetail:
|
|
618
|
+
"""
|
|
619
|
+
Get detailed information about a specific run.
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
action_id: ID of the action
|
|
623
|
+
run_id: ID of the run
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
ActionRunDetail object
|
|
627
|
+
"""
|
|
628
|
+
try:
|
|
629
|
+
app_instance = request.app.state.app_instance
|
|
630
|
+
database = app_instance.database
|
|
631
|
+
|
|
632
|
+
run = database.get_action_run(run_id)
|
|
633
|
+
if not run or run['action_id'] != action_id:
|
|
634
|
+
raise HTTPException(status_code=404, detail="Run not found")
|
|
635
|
+
|
|
636
|
+
return ActionRunDetail(
|
|
637
|
+
id=run['id'],
|
|
638
|
+
action_id=run['action_id'],
|
|
639
|
+
action_name=run.get('action_name', 'Unknown'),
|
|
640
|
+
started_at=parse_datetime(run['started_at']),
|
|
641
|
+
completed_at=parse_datetime(run.get('completed_at')),
|
|
642
|
+
status=run['status'],
|
|
643
|
+
result_text=run.get('result_text'),
|
|
644
|
+
result_html=run.get('result_html'),
|
|
645
|
+
error_message=run.get('error_message'),
|
|
646
|
+
input_tokens=run.get('input_tokens', 0),
|
|
647
|
+
output_tokens=run.get('output_tokens', 0),
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
except HTTPException:
|
|
651
|
+
raise
|
|
652
|
+
except Exception as e:
|
|
653
|
+
logger.error(f"Error getting run {run_id}: {e}")
|
|
654
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
@router.get("/actions/{action_id}/runs/{run_id}/export")
|
|
658
|
+
async def export_run_result(
|
|
659
|
+
action_id: int,
|
|
660
|
+
run_id: int,
|
|
661
|
+
request: Request,
|
|
662
|
+
format: str = Query("text", pattern="^(text|html|markdown)$"),
|
|
663
|
+
session_id: str = Depends(get_current_session),
|
|
664
|
+
):
|
|
665
|
+
"""
|
|
666
|
+
Export run result in specified format.
|
|
667
|
+
|
|
668
|
+
Args:
|
|
669
|
+
action_id: ID of the action
|
|
670
|
+
run_id: ID of the run
|
|
671
|
+
format: Export format (text, html, markdown)
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
Exported content in requested format
|
|
675
|
+
"""
|
|
676
|
+
try:
|
|
677
|
+
app_instance = request.app.state.app_instance
|
|
678
|
+
database = app_instance.database
|
|
679
|
+
|
|
680
|
+
run = database.get_action_run(run_id)
|
|
681
|
+
if not run or run['action_id'] != action_id:
|
|
682
|
+
raise HTTPException(status_code=404, detail="Run not found")
|
|
683
|
+
|
|
684
|
+
if format == "html":
|
|
685
|
+
content = run.get('result_html') or f"<pre>{run.get('result_text', 'No result')}</pre>"
|
|
686
|
+
return HTMLResponse(content=content)
|
|
687
|
+
|
|
688
|
+
elif format == "markdown":
|
|
689
|
+
result = run.get('result_text', 'No result')
|
|
690
|
+
header = f"# Action Run {run_id}\n\n"
|
|
691
|
+
header += f"**Action:** {run.get('action_name', 'Unknown')}\n"
|
|
692
|
+
header += f"**Status:** {run['status']}\n"
|
|
693
|
+
header += f"**Started:** {run.get('started_at', 'N/A')}\n"
|
|
694
|
+
header += f"**Completed:** {run.get('completed_at', 'N/A')}\n\n"
|
|
695
|
+
header += "## Result\n\n"
|
|
696
|
+
content = header + result
|
|
697
|
+
return PlainTextResponse(content=content, media_type="text/markdown")
|
|
698
|
+
|
|
699
|
+
else: # text
|
|
700
|
+
content = run.get('result_text', 'No result')
|
|
701
|
+
return PlainTextResponse(content=content)
|
|
702
|
+
|
|
703
|
+
except HTTPException:
|
|
704
|
+
raise
|
|
705
|
+
except Exception as e:
|
|
706
|
+
logger.error(f"Error exporting run {run_id}: {e}")
|
|
707
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
@router.get("/actions/runs/recent")
|
|
711
|
+
async def list_recent_runs(
|
|
712
|
+
request: Request,
|
|
713
|
+
limit: int = Query(20, ge=1, le=100),
|
|
714
|
+
session_id: str = Depends(get_current_session),
|
|
715
|
+
) -> List[ActionRunSummary]:
|
|
716
|
+
"""
|
|
717
|
+
List recent runs across all actions.
|
|
718
|
+
|
|
719
|
+
Args:
|
|
720
|
+
limit: Maximum number of runs to return
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
List of ActionRunSummary objects
|
|
724
|
+
"""
|
|
725
|
+
try:
|
|
726
|
+
app_instance = request.app.state.app_instance
|
|
727
|
+
database = app_instance.database
|
|
728
|
+
|
|
729
|
+
runs = database.get_recent_action_runs(limit=limit)
|
|
730
|
+
|
|
731
|
+
return [
|
|
732
|
+
ActionRunSummary(
|
|
733
|
+
id=run['id'],
|
|
734
|
+
action_id=run['action_id'],
|
|
735
|
+
action_name=run.get('action_name', 'Unknown'),
|
|
736
|
+
started_at=parse_datetime(run['started_at']),
|
|
737
|
+
completed_at=parse_datetime(run.get('completed_at')),
|
|
738
|
+
status=run['status'],
|
|
739
|
+
input_tokens=run.get('input_tokens', 0),
|
|
740
|
+
output_tokens=run.get('output_tokens', 0),
|
|
741
|
+
)
|
|
742
|
+
for run in runs
|
|
743
|
+
]
|
|
744
|
+
|
|
745
|
+
except Exception as e:
|
|
746
|
+
logger.error(f"Error listing recent runs: {e}")
|
|
747
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
@router.get("/actions/status/failed-count")
|
|
751
|
+
async def get_failed_action_count(
|
|
752
|
+
request: Request,
|
|
753
|
+
session_id: str = Depends(get_current_session),
|
|
754
|
+
) -> dict:
|
|
755
|
+
"""
|
|
756
|
+
Get count of failed/disabled actions.
|
|
757
|
+
|
|
758
|
+
Returns:
|
|
759
|
+
Count of failed actions
|
|
760
|
+
"""
|
|
761
|
+
try:
|
|
762
|
+
app_instance = request.app.state.app_instance
|
|
763
|
+
database = app_instance.database
|
|
764
|
+
|
|
765
|
+
count = database.get_failed_action_count()
|
|
766
|
+
|
|
767
|
+
return {
|
|
768
|
+
"failed_count": count,
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
except Exception as e:
|
|
772
|
+
logger.error(f"Error getting failed action count: {e}")
|
|
773
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
# =============================================================================
|
|
777
|
+
# AI-ASSISTED ACTION CREATION
|
|
778
|
+
# =============================================================================
|
|
779
|
+
|
|
780
|
+
class AICreationStart(BaseModel):
|
|
781
|
+
"""Request model for starting AI-assisted action creation."""
|
|
782
|
+
name: str = Field(..., min_length=1, max_length=100)
|
|
783
|
+
description: str = Field(..., min_length=1, max_length=500)
|
|
784
|
+
model_id: str = Field(..., min_length=1)
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
class AICreationMessage(BaseModel):
|
|
788
|
+
"""Request model for sending a message in AI creation chat."""
|
|
789
|
+
message: str = Field(..., min_length=1)
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
# Store for active creation sessions (in-memory, per-session)
|
|
793
|
+
_creation_sessions = {}
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
@router.post("/actions/ai-create/start")
|
|
797
|
+
async def start_ai_creation(
|
|
798
|
+
request: Request,
|
|
799
|
+
data: AICreationStart,
|
|
800
|
+
session_id: str = Depends(get_current_session),
|
|
801
|
+
) -> dict:
|
|
802
|
+
"""
|
|
803
|
+
Start an AI-assisted action creation session.
|
|
804
|
+
|
|
805
|
+
This initialises a chat session with the LLM to help create an action.
|
|
806
|
+
|
|
807
|
+
Args:
|
|
808
|
+
data: Action name, description, and model to use
|
|
809
|
+
|
|
810
|
+
Returns:
|
|
811
|
+
Session information and initial LLM response
|
|
812
|
+
"""
|
|
813
|
+
import json
|
|
814
|
+
from dtSpark.scheduler.creation_tools import (
|
|
815
|
+
ACTION_CREATION_SYSTEM_PROMPT,
|
|
816
|
+
get_action_creation_tools,
|
|
817
|
+
execute_creation_tool
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
try:
|
|
821
|
+
app_instance = request.app.state.app_instance
|
|
822
|
+
|
|
823
|
+
# Validate model exists
|
|
824
|
+
models = app_instance.llm_manager.list_all_models()
|
|
825
|
+
model_exists = any(m['id'] == data.model_id for m in models)
|
|
826
|
+
if not model_exists:
|
|
827
|
+
raise HTTPException(status_code=400, detail=f"Model not found: {data.model_id}")
|
|
828
|
+
|
|
829
|
+
# Create unique creation session ID
|
|
830
|
+
import secrets
|
|
831
|
+
creation_id = secrets.token_hex(16)
|
|
832
|
+
|
|
833
|
+
# Set the model for this creation session
|
|
834
|
+
app_instance.llm_manager.set_model(data.model_id)
|
|
835
|
+
app_instance.bedrock_service = app_instance.llm_manager.get_active_service()
|
|
836
|
+
|
|
837
|
+
# Initial message to the LLM with the action name and description
|
|
838
|
+
initial_prompt = (
|
|
839
|
+
f"I want to create an autonomous action with the following details:\n\n"
|
|
840
|
+
f"**Name:** {data.name}\n"
|
|
841
|
+
f"**Description:** {data.description}\n\n"
|
|
842
|
+
f"Please help me configure this action. Ask me any questions needed to "
|
|
843
|
+
f"understand what the action should do, when it should run, and what tools it needs."
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
# Initialise conversation messages
|
|
847
|
+
messages = [
|
|
848
|
+
{'role': 'user', 'content': [{'type': 'text', 'text': initial_prompt}]}
|
|
849
|
+
]
|
|
850
|
+
|
|
851
|
+
# Get creation tools
|
|
852
|
+
creation_tools = get_action_creation_tools()
|
|
853
|
+
tools_for_api = [{'toolSpec': t} for t in creation_tools]
|
|
854
|
+
|
|
855
|
+
# Invoke the LLM
|
|
856
|
+
response = app_instance.llm_manager.invoke_model(
|
|
857
|
+
messages=messages,
|
|
858
|
+
system=ACTION_CREATION_SYSTEM_PROMPT,
|
|
859
|
+
tools=tools_for_api,
|
|
860
|
+
max_tokens=4096,
|
|
861
|
+
temperature=0.7
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
if response.get('error'):
|
|
865
|
+
raise HTTPException(
|
|
866
|
+
status_code=500,
|
|
867
|
+
detail=f"LLM error: {response.get('error_message', 'Unknown error')}"
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
# Extract response text
|
|
871
|
+
response_text = ""
|
|
872
|
+
content_blocks = response.get('content_blocks', [])
|
|
873
|
+
for block in content_blocks:
|
|
874
|
+
if block.get('type') == 'text':
|
|
875
|
+
response_text += block.get('text', '')
|
|
876
|
+
|
|
877
|
+
# If no content_blocks, try direct content
|
|
878
|
+
if not response_text and response.get('content'):
|
|
879
|
+
response_text = response.get('content', '')
|
|
880
|
+
|
|
881
|
+
# Add assistant response to messages
|
|
882
|
+
messages.append({
|
|
883
|
+
'role': 'assistant',
|
|
884
|
+
'content': content_blocks if content_blocks else [{'type': 'text', 'text': response_text}]
|
|
885
|
+
})
|
|
886
|
+
|
|
887
|
+
# Store session state
|
|
888
|
+
_creation_sessions[creation_id] = {
|
|
889
|
+
'name': data.name,
|
|
890
|
+
'description': data.description,
|
|
891
|
+
'model_id': data.model_id,
|
|
892
|
+
'messages': messages,
|
|
893
|
+
'created': datetime.now().isoformat(),
|
|
894
|
+
'completed': False,
|
|
895
|
+
'action_id': None
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return {
|
|
899
|
+
'creation_id': creation_id,
|
|
900
|
+
'response': response_text,
|
|
901
|
+
'completed': False
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
except HTTPException:
|
|
905
|
+
raise
|
|
906
|
+
except Exception as e:
|
|
907
|
+
logger.error(f"Error starting AI creation: {e}", exc_info=True)
|
|
908
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
@router.post("/actions/ai-create/{creation_id}/message")
|
|
912
|
+
async def send_ai_creation_message(
|
|
913
|
+
creation_id: str,
|
|
914
|
+
request: Request,
|
|
915
|
+
data: AICreationMessage,
|
|
916
|
+
session_id: str = Depends(get_current_session),
|
|
917
|
+
) -> dict:
|
|
918
|
+
"""
|
|
919
|
+
Send a message in an AI creation chat session.
|
|
920
|
+
|
|
921
|
+
Args:
|
|
922
|
+
creation_id: The creation session ID
|
|
923
|
+
data: The user's message
|
|
924
|
+
|
|
925
|
+
Returns:
|
|
926
|
+
LLM response and completion status
|
|
927
|
+
"""
|
|
928
|
+
import json
|
|
929
|
+
from dtSpark.scheduler.creation_tools import (
|
|
930
|
+
ACTION_CREATION_SYSTEM_PROMPT,
|
|
931
|
+
get_action_creation_tools,
|
|
932
|
+
execute_creation_tool
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
try:
|
|
936
|
+
# Get session state
|
|
937
|
+
if creation_id not in _creation_sessions:
|
|
938
|
+
raise HTTPException(status_code=404, detail="Creation session not found")
|
|
939
|
+
|
|
940
|
+
session_state = _creation_sessions[creation_id]
|
|
941
|
+
|
|
942
|
+
if session_state.get('completed'):
|
|
943
|
+
return {
|
|
944
|
+
'response': 'This action has already been created.',
|
|
945
|
+
'completed': True,
|
|
946
|
+
'action_id': session_state.get('action_id')
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
app_instance = request.app.state.app_instance
|
|
950
|
+
|
|
951
|
+
# Pre-fetch available tools (same as /tools endpoint)
|
|
952
|
+
available_tools = []
|
|
953
|
+
# Get MCP tools
|
|
954
|
+
if hasattr(app_instance, 'mcp_manager') and app_instance.mcp_manager:
|
|
955
|
+
try:
|
|
956
|
+
mcp_tools = await app_instance.mcp_manager.list_all_tools()
|
|
957
|
+
for tool in mcp_tools:
|
|
958
|
+
available_tools.append({
|
|
959
|
+
'name': tool.get('name', 'unknown'),
|
|
960
|
+
'description': tool.get('description', 'No description available'),
|
|
961
|
+
'source': tool.get('server', 'mcp')
|
|
962
|
+
})
|
|
963
|
+
except Exception as e:
|
|
964
|
+
logger.warning(f"Error getting MCP tools for AI creation: {e}")
|
|
965
|
+
# Get embedded tools
|
|
966
|
+
if hasattr(app_instance, 'conversation_manager') and app_instance.conversation_manager:
|
|
967
|
+
try:
|
|
968
|
+
embedded = app_instance.conversation_manager.get_embedded_tools()
|
|
969
|
+
for tool in embedded:
|
|
970
|
+
# Embedded tools are wrapped in toolSpec format
|
|
971
|
+
tool_spec = tool.get('toolSpec', tool)
|
|
972
|
+
available_tools.append({
|
|
973
|
+
'name': tool_spec.get('name', 'unknown'),
|
|
974
|
+
'description': tool_spec.get('description', 'No description available'),
|
|
975
|
+
'source': 'embedded'
|
|
976
|
+
})
|
|
977
|
+
except Exception as e:
|
|
978
|
+
logger.warning(f"Error getting embedded tools for AI creation: {e}")
|
|
979
|
+
|
|
980
|
+
# Set the model for this creation session
|
|
981
|
+
app_instance.llm_manager.set_model(session_state['model_id'])
|
|
982
|
+
app_instance.bedrock_service = app_instance.llm_manager.get_active_service()
|
|
983
|
+
|
|
984
|
+
# Add user message
|
|
985
|
+
messages = session_state['messages']
|
|
986
|
+
messages.append({
|
|
987
|
+
'role': 'user',
|
|
988
|
+
'content': [{'type': 'text', 'text': data.message}]
|
|
989
|
+
})
|
|
990
|
+
|
|
991
|
+
# Get creation tools
|
|
992
|
+
creation_tools = get_action_creation_tools()
|
|
993
|
+
tools_for_api = [{'toolSpec': t} for t in creation_tools]
|
|
994
|
+
|
|
995
|
+
# Tool execution loop
|
|
996
|
+
max_iterations = 10
|
|
997
|
+
iteration = 0
|
|
998
|
+
final_response = ""
|
|
999
|
+
action_created = False
|
|
1000
|
+
created_action_id = None
|
|
1001
|
+
|
|
1002
|
+
while iteration < max_iterations:
|
|
1003
|
+
iteration += 1
|
|
1004
|
+
|
|
1005
|
+
# Invoke the LLM
|
|
1006
|
+
response = app_instance.llm_manager.invoke_model(
|
|
1007
|
+
messages=messages,
|
|
1008
|
+
system=ACTION_CREATION_SYSTEM_PROMPT,
|
|
1009
|
+
tools=tools_for_api,
|
|
1010
|
+
max_tokens=4096,
|
|
1011
|
+
temperature=0.7
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
if response.get('error'):
|
|
1015
|
+
raise HTTPException(
|
|
1016
|
+
status_code=500,
|
|
1017
|
+
detail=f"LLM error: {response.get('error_message', 'Unknown error')}"
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
# Extract response content
|
|
1021
|
+
content_blocks = response.get('content_blocks', [])
|
|
1022
|
+
stop_reason = response.get('stop_reason', 'end_turn')
|
|
1023
|
+
|
|
1024
|
+
# Check for tool use
|
|
1025
|
+
tool_use_blocks = [b for b in content_blocks if b.get('type') == 'tool_use']
|
|
1026
|
+
|
|
1027
|
+
if tool_use_blocks:
|
|
1028
|
+
# Add assistant response with tool calls
|
|
1029
|
+
messages.append({
|
|
1030
|
+
'role': 'assistant',
|
|
1031
|
+
'content': content_blocks
|
|
1032
|
+
})
|
|
1033
|
+
|
|
1034
|
+
# Execute tools
|
|
1035
|
+
tool_results = []
|
|
1036
|
+
for tool_block in tool_use_blocks:
|
|
1037
|
+
tool_name = tool_block.get('name')
|
|
1038
|
+
tool_input = tool_block.get('input', {})
|
|
1039
|
+
tool_id = tool_block.get('id')
|
|
1040
|
+
|
|
1041
|
+
# Execute the creation tool
|
|
1042
|
+
result = execute_creation_tool(
|
|
1043
|
+
tool_name=tool_name,
|
|
1044
|
+
tool_input=tool_input,
|
|
1045
|
+
mcp_manager=app_instance.mcp_manager,
|
|
1046
|
+
database=app_instance.database,
|
|
1047
|
+
scheduler_manager=getattr(app_instance, 'scheduler_manager', None),
|
|
1048
|
+
model_id=session_state['model_id'],
|
|
1049
|
+
user_guid=getattr(app_instance.database, 'user_guid', None),
|
|
1050
|
+
config=getattr(app_instance, 'config', None),
|
|
1051
|
+
available_tools=available_tools
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
tool_results.append({
|
|
1055
|
+
'type': 'tool_result',
|
|
1056
|
+
'tool_use_id': tool_id,
|
|
1057
|
+
'content': json.dumps(result) if isinstance(result, dict) else str(result)
|
|
1058
|
+
})
|
|
1059
|
+
|
|
1060
|
+
# Check if action was created
|
|
1061
|
+
if tool_name == 'create_autonomous_action' and result.get('success'):
|
|
1062
|
+
action_created = True
|
|
1063
|
+
created_action_id = result.get('action_id')
|
|
1064
|
+
|
|
1065
|
+
# Add tool results to messages
|
|
1066
|
+
messages.append({
|
|
1067
|
+
'role': 'user',
|
|
1068
|
+
'content': tool_results
|
|
1069
|
+
})
|
|
1070
|
+
|
|
1071
|
+
# Continue loop to get LLM's response to tool results
|
|
1072
|
+
continue
|
|
1073
|
+
|
|
1074
|
+
else:
|
|
1075
|
+
# No tool use - extract text response
|
|
1076
|
+
for block in content_blocks:
|
|
1077
|
+
if block.get('type') == 'text':
|
|
1078
|
+
final_response += block.get('text', '')
|
|
1079
|
+
|
|
1080
|
+
# Add final assistant response to messages
|
|
1081
|
+
messages.append({
|
|
1082
|
+
'role': 'assistant',
|
|
1083
|
+
'content': content_blocks if content_blocks else [{'type': 'text', 'text': final_response}]
|
|
1084
|
+
})
|
|
1085
|
+
|
|
1086
|
+
break
|
|
1087
|
+
|
|
1088
|
+
# Update session state
|
|
1089
|
+
session_state['messages'] = messages
|
|
1090
|
+
if action_created:
|
|
1091
|
+
session_state['completed'] = True
|
|
1092
|
+
session_state['action_id'] = created_action_id
|
|
1093
|
+
|
|
1094
|
+
return {
|
|
1095
|
+
'response': final_response,
|
|
1096
|
+
'completed': action_created,
|
|
1097
|
+
'action_id': created_action_id
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
except HTTPException:
|
|
1101
|
+
raise
|
|
1102
|
+
except Exception as e:
|
|
1103
|
+
logger.error(f"Error in AI creation message: {e}", exc_info=True)
|
|
1104
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
@router.delete("/actions/ai-create/{creation_id}")
|
|
1108
|
+
async def cancel_ai_creation(
|
|
1109
|
+
creation_id: str,
|
|
1110
|
+
request: Request,
|
|
1111
|
+
session_id: str = Depends(get_current_session),
|
|
1112
|
+
) -> dict:
|
|
1113
|
+
"""
|
|
1114
|
+
Cancel an AI creation session.
|
|
1115
|
+
|
|
1116
|
+
Args:
|
|
1117
|
+
creation_id: The creation session ID to cancel
|
|
1118
|
+
|
|
1119
|
+
Returns:
|
|
1120
|
+
Confirmation message
|
|
1121
|
+
"""
|
|
1122
|
+
if creation_id in _creation_sessions:
|
|
1123
|
+
del _creation_sessions[creation_id]
|
|
1124
|
+
|
|
1125
|
+
return {'status': 'cancelled', 'message': 'Creation session cancelled'}
|