dtSpark 1.1.0a3__py3-none-any.whl → 1.1.0a6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dtSpark/_version.txt +1 -1
- dtSpark/aws/authentication.py +1 -1
- dtSpark/aws/bedrock.py +238 -239
- dtSpark/aws/costs.py +9 -5
- dtSpark/aws/pricing.py +25 -21
- dtSpark/cli_interface.py +69 -62
- dtSpark/conversation_manager.py +54 -47
- dtSpark/core/application.py +112 -91
- dtSpark/core/context_compaction.py +241 -226
- dtSpark/daemon/__init__.py +36 -22
- dtSpark/daemon/action_monitor.py +46 -17
- dtSpark/daemon/daemon_app.py +126 -104
- dtSpark/daemon/daemon_manager.py +59 -23
- dtSpark/daemon/pid_file.py +3 -2
- dtSpark/database/autonomous_actions.py +3 -0
- dtSpark/database/credential_prompt.py +52 -54
- dtSpark/files/manager.py +6 -12
- dtSpark/limits/__init__.py +1 -1
- dtSpark/limits/tokens.py +2 -2
- dtSpark/llm/anthropic_direct.py +246 -141
- dtSpark/llm/ollama.py +3 -1
- dtSpark/mcp_integration/manager.py +4 -4
- dtSpark/mcp_integration/tool_selector.py +83 -77
- dtSpark/resources/config.yaml.template +10 -0
- dtSpark/safety/patterns.py +45 -46
- dtSpark/safety/prompt_inspector.py +8 -1
- dtSpark/scheduler/creation_tools.py +273 -181
- dtSpark/scheduler/executor.py +503 -221
- dtSpark/tools/builtin.py +70 -53
- dtSpark/web/endpoints/autonomous_actions.py +12 -9
- dtSpark/web/endpoints/chat.py +8 -6
- dtSpark/web/endpoints/conversations.py +11 -9
- dtSpark/web/endpoints/main_menu.py +132 -105
- dtSpark/web/endpoints/streaming.py +2 -2
- dtSpark/web/server.py +65 -5
- dtSpark/web/ssl_utils.py +3 -3
- dtSpark/web/static/css/dark-theme.css +8 -29
- dtSpark/web/static/js/chat.js +6 -8
- dtSpark/web/static/js/main.js +8 -8
- dtSpark/web/static/js/sse-client.js +130 -122
- dtSpark/web/templates/actions.html +5 -5
- dtSpark/web/templates/base.html +13 -0
- dtSpark/web/templates/chat.html +10 -10
- dtSpark/web/templates/conversations.html +2 -2
- dtSpark/web/templates/goodbye.html +2 -2
- dtSpark/web/templates/main_menu.html +17 -17
- dtSpark/web/web_interface.py +2 -2
- {dtspark-1.1.0a3.dist-info → dtspark-1.1.0a6.dist-info}/METADATA +9 -2
- dtspark-1.1.0a6.dist-info/RECORD +96 -0
- dtspark-1.1.0a3.dist-info/RECORD +0 -96
- {dtspark-1.1.0a3.dist-info → dtspark-1.1.0a6.dist-info}/WHEEL +0 -0
- {dtspark-1.1.0a3.dist-info → dtspark-1.1.0a6.dist-info}/entry_points.txt +0 -0
- {dtspark-1.1.0a3.dist-info → dtspark-1.1.0a6.dist-info}/licenses/LICENSE +0 -0
- {dtspark-1.1.0a3.dist-info → dtspark-1.1.0a6.dist-info}/top_level.txt +0 -0
|
@@ -80,50 +80,17 @@ async def get_account_info(
|
|
|
80
80
|
"""
|
|
81
81
|
try:
|
|
82
82
|
app_instance = request.app.state.app_instance
|
|
83
|
-
|
|
84
|
-
# Get LLM manager to determine active provider
|
|
85
83
|
llm_manager = getattr(app_instance, 'llm_manager', None)
|
|
86
84
|
user_guid = getattr(app_instance, 'user_guid', 'unknown')
|
|
87
85
|
|
|
88
86
|
if llm_manager and llm_manager.active_provider:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if
|
|
93
|
-
|
|
94
|
-
if auth:
|
|
95
|
-
account_info = auth.get_account_info()
|
|
96
|
-
if account_info:
|
|
97
|
-
return AccountInfo(
|
|
98
|
-
provider='aws',
|
|
99
|
-
user_arn=account_info.get('user_arn'),
|
|
100
|
-
account_id=account_info.get('account_id'),
|
|
101
|
-
region=account_info.get('region'),
|
|
102
|
-
user_guid=user_guid,
|
|
103
|
-
auth_method=account_info.get('auth_method'),
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
# Anthropic
|
|
107
|
-
elif 'anthropic' in active_provider:
|
|
108
|
-
return AccountInfo(
|
|
109
|
-
provider='anthropic',
|
|
110
|
-
user_guid=user_guid,
|
|
111
|
-
auth_method='api_key',
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
# Ollama
|
|
115
|
-
elif 'ollama' in active_provider:
|
|
116
|
-
return AccountInfo(
|
|
117
|
-
provider='ollama',
|
|
118
|
-
user_guid=user_guid,
|
|
119
|
-
auth_method='local',
|
|
120
|
-
)
|
|
87
|
+
result = _build_account_info_for_provider(
|
|
88
|
+
app_instance, llm_manager.active_provider.lower(), user_guid
|
|
89
|
+
)
|
|
90
|
+
if result:
|
|
91
|
+
return result
|
|
121
92
|
|
|
122
|
-
|
|
123
|
-
return AccountInfo(
|
|
124
|
-
provider='none',
|
|
125
|
-
user_guid=user_guid,
|
|
126
|
-
)
|
|
93
|
+
return AccountInfo(provider='none', user_guid=user_guid)
|
|
127
94
|
|
|
128
95
|
except HTTPException:
|
|
129
96
|
raise
|
|
@@ -132,6 +99,40 @@ async def get_account_info(
|
|
|
132
99
|
raise HTTPException(status_code=500, detail=str(e))
|
|
133
100
|
|
|
134
101
|
|
|
102
|
+
def _build_account_info_for_provider(
|
|
103
|
+
app_instance, active_provider: str, user_guid: str
|
|
104
|
+
) -> Optional[AccountInfo]:
|
|
105
|
+
"""Build AccountInfo for the given active provider, or return None if unavailable."""
|
|
106
|
+
if 'bedrock' in active_provider or 'aws' in active_provider:
|
|
107
|
+
return _build_aws_account_info(app_instance, user_guid)
|
|
108
|
+
|
|
109
|
+
if 'anthropic' in active_provider:
|
|
110
|
+
return AccountInfo(provider='anthropic', user_guid=user_guid, auth_method='api_key')
|
|
111
|
+
|
|
112
|
+
if 'ollama' in active_provider:
|
|
113
|
+
return AccountInfo(provider='ollama', user_guid=user_guid, auth_method='local')
|
|
114
|
+
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _build_aws_account_info(app_instance, user_guid: str) -> Optional[AccountInfo]:
|
|
119
|
+
"""Build AccountInfo from the AWS authenticator, or return None."""
|
|
120
|
+
auth = getattr(app_instance, 'authenticator', None)
|
|
121
|
+
if not auth:
|
|
122
|
+
return None
|
|
123
|
+
account_info = auth.get_account_info()
|
|
124
|
+
if not account_info:
|
|
125
|
+
return None
|
|
126
|
+
return AccountInfo(
|
|
127
|
+
provider='aws',
|
|
128
|
+
user_arn=account_info.get('user_arn'),
|
|
129
|
+
account_id=account_info.get('account_id'),
|
|
130
|
+
region=account_info.get('region'),
|
|
131
|
+
user_guid=user_guid,
|
|
132
|
+
auth_method=account_info.get('auth_method'),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
135
136
|
@router.get("/providers")
|
|
136
137
|
async def get_providers(
|
|
137
138
|
request: Request,
|
|
@@ -147,71 +148,11 @@ async def get_providers(
|
|
|
147
148
|
app_instance = request.app.state.app_instance
|
|
148
149
|
providers = []
|
|
149
150
|
|
|
150
|
-
# Get LLM manager which has all registered providers
|
|
151
151
|
llm_manager = getattr(app_instance, 'llm_manager', None)
|
|
152
|
-
|
|
153
152
|
if llm_manager and hasattr(llm_manager, 'providers'):
|
|
154
153
|
for provider_name, service in llm_manager.providers.items():
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
provider_type = 'unknown'
|
|
158
|
-
auth_method = None
|
|
159
|
-
region = None
|
|
160
|
-
base_url = None
|
|
161
|
-
|
|
162
|
-
# Determine provider type
|
|
163
|
-
provider_name_lower = provider_name.lower()
|
|
164
|
-
if 'bedrock' in provider_name_lower or 'aws' in provider_name_lower:
|
|
165
|
-
provider_type = 'aws'
|
|
166
|
-
auth = getattr(app_instance, 'authenticator', None)
|
|
167
|
-
if auth:
|
|
168
|
-
account_info = auth.get_account_info()
|
|
169
|
-
if account_info:
|
|
170
|
-
auth_method = account_info.get('auth_method')
|
|
171
|
-
region = account_info.get('region')
|
|
172
|
-
elif 'anthropic' in provider_name_lower:
|
|
173
|
-
provider_type = 'anthropic'
|
|
174
|
-
auth_method = 'api_key'
|
|
175
|
-
elif 'ollama' in provider_name_lower:
|
|
176
|
-
provider_type = 'ollama'
|
|
177
|
-
auth_method = 'local'
|
|
178
|
-
base_url = getattr(service, 'base_url', 'http://localhost:11434')
|
|
179
|
-
|
|
180
|
-
# Get available models
|
|
181
|
-
try:
|
|
182
|
-
if hasattr(service, 'list_available_models'):
|
|
183
|
-
available_models = service.list_available_models()
|
|
184
|
-
elif hasattr(service, 'list_models'):
|
|
185
|
-
available_models = service.list_models()
|
|
186
|
-
else:
|
|
187
|
-
available_models = []
|
|
188
|
-
|
|
189
|
-
for model in available_models:
|
|
190
|
-
if isinstance(model, dict):
|
|
191
|
-
model_id = model.get('id') or model.get('modelId') or model.get('name') or str(model)
|
|
192
|
-
display_name = model.get('display_name') or model.get('modelName') or model_id
|
|
193
|
-
else:
|
|
194
|
-
model_id = str(model)
|
|
195
|
-
display_name = model_id
|
|
196
|
-
|
|
197
|
-
models.append(ProviderModelInfo(
|
|
198
|
-
model_id=model_id,
|
|
199
|
-
display_name=display_name,
|
|
200
|
-
))
|
|
201
|
-
except Exception as e:
|
|
202
|
-
logger.warning(f"Failed to list models from {provider_name}: {e}")
|
|
203
|
-
status = 'error'
|
|
204
|
-
|
|
205
|
-
providers.append(ProviderInfo(
|
|
206
|
-
name=provider_name,
|
|
207
|
-
type=provider_type,
|
|
208
|
-
enabled=True,
|
|
209
|
-
status=status,
|
|
210
|
-
models=models,
|
|
211
|
-
auth_method=auth_method,
|
|
212
|
-
region=region,
|
|
213
|
-
base_url=base_url,
|
|
214
|
-
))
|
|
154
|
+
provider_info = _build_provider_info(app_instance, provider_name, service)
|
|
155
|
+
providers.append(provider_info)
|
|
215
156
|
|
|
216
157
|
return providers
|
|
217
158
|
|
|
@@ -220,6 +161,88 @@ async def get_providers(
|
|
|
220
161
|
raise HTTPException(status_code=500, detail=str(e))
|
|
221
162
|
|
|
222
163
|
|
|
164
|
+
def _build_provider_info(app_instance, provider_name: str, service) -> ProviderInfo:
|
|
165
|
+
"""Build a ProviderInfo for a single registered provider."""
|
|
166
|
+
provider_type, auth_method, region, base_url = _detect_provider_type(
|
|
167
|
+
app_instance, provider_name, service
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
models, status = _list_provider_models(provider_name, service)
|
|
171
|
+
|
|
172
|
+
return ProviderInfo(
|
|
173
|
+
name=provider_name,
|
|
174
|
+
type=provider_type,
|
|
175
|
+
enabled=True,
|
|
176
|
+
status=status,
|
|
177
|
+
models=models,
|
|
178
|
+
auth_method=auth_method,
|
|
179
|
+
region=region,
|
|
180
|
+
base_url=base_url,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _detect_provider_type(app_instance, provider_name: str, service) -> tuple:
|
|
185
|
+
"""Detect the provider type, auth method, region, and base URL from the provider name."""
|
|
186
|
+
provider_name_lower = provider_name.lower()
|
|
187
|
+
auth_method = None
|
|
188
|
+
region = None
|
|
189
|
+
base_url = None
|
|
190
|
+
|
|
191
|
+
if 'bedrock' in provider_name_lower or 'aws' in provider_name_lower:
|
|
192
|
+
provider_type = 'aws'
|
|
193
|
+
auth = getattr(app_instance, 'authenticator', None)
|
|
194
|
+
if auth:
|
|
195
|
+
account_info = auth.get_account_info()
|
|
196
|
+
if account_info:
|
|
197
|
+
auth_method = account_info.get('auth_method')
|
|
198
|
+
region = account_info.get('region')
|
|
199
|
+
elif 'anthropic' in provider_name_lower:
|
|
200
|
+
provider_type = 'anthropic'
|
|
201
|
+
auth_method = 'api_key'
|
|
202
|
+
elif 'ollama' in provider_name_lower:
|
|
203
|
+
provider_type = 'ollama'
|
|
204
|
+
auth_method = 'local'
|
|
205
|
+
base_url = getattr(service, 'base_url', 'http://localhost:11434')
|
|
206
|
+
else:
|
|
207
|
+
provider_type = 'unknown'
|
|
208
|
+
|
|
209
|
+
return provider_type, auth_method, region, base_url
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _list_provider_models(provider_name: str, service) -> tuple:
|
|
213
|
+
"""List available models from a provider service. Returns (models, status)."""
|
|
214
|
+
models = []
|
|
215
|
+
status = 'connected'
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
if hasattr(service, 'list_available_models'):
|
|
219
|
+
available_models = service.list_available_models()
|
|
220
|
+
elif hasattr(service, 'list_models'):
|
|
221
|
+
available_models = service.list_models()
|
|
222
|
+
else:
|
|
223
|
+
available_models = []
|
|
224
|
+
|
|
225
|
+
for model in available_models:
|
|
226
|
+
models.append(_parse_model_info(model))
|
|
227
|
+
except Exception as e:
|
|
228
|
+
logger.warning(f"Failed to list models from {provider_name}: {e}")
|
|
229
|
+
status = 'error'
|
|
230
|
+
|
|
231
|
+
return models, status
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _parse_model_info(model) -> ProviderModelInfo:
|
|
235
|
+
"""Parse a model entry (dict or string) into a ProviderModelInfo."""
|
|
236
|
+
if isinstance(model, dict):
|
|
237
|
+
model_id = model.get('id') or model.get('modelId') or model.get('name') or str(model)
|
|
238
|
+
display_name = model.get('display_name') or model.get('modelName') or model_id
|
|
239
|
+
else:
|
|
240
|
+
model_id = str(model)
|
|
241
|
+
display_name = model_id
|
|
242
|
+
|
|
243
|
+
return ProviderModelInfo(model_id=model_id, display_name=display_name)
|
|
244
|
+
|
|
245
|
+
|
|
223
246
|
@router.get("/costs/last-month")
|
|
224
247
|
async def get_last_month_costs(
|
|
225
248
|
request: Request,
|
|
@@ -607,14 +630,18 @@ async def get_daemon_status(request: Request):
|
|
|
607
630
|
except Exception:
|
|
608
631
|
pass
|
|
609
632
|
|
|
633
|
+
if not daemon_running and scheduled_count > 0:
|
|
634
|
+
warning_message = (
|
|
635
|
+
f"Daemon is not running - {scheduled_count} scheduled action(s) will not execute"
|
|
636
|
+
)
|
|
637
|
+
else:
|
|
638
|
+
warning_message = None
|
|
639
|
+
|
|
610
640
|
return {
|
|
611
641
|
'daemon_running': daemon_running,
|
|
612
642
|
'daemon_pid': daemon_pid,
|
|
613
643
|
'scheduled_actions_count': scheduled_count,
|
|
614
|
-
'warning':
|
|
615
|
-
f"Daemon is not running - {scheduled_count} scheduled action(s) will not execute"
|
|
616
|
-
if scheduled_count > 0 else None
|
|
617
|
-
)
|
|
644
|
+
'warning': warning_message,
|
|
618
645
|
}
|
|
619
646
|
|
|
620
647
|
except Exception as e:
|
|
@@ -151,7 +151,7 @@ class StreamingManager:
|
|
|
151
151
|
"content": result.get('content', ''),
|
|
152
152
|
}),
|
|
153
153
|
}
|
|
154
|
-
except:
|
|
154
|
+
except ValueError:
|
|
155
155
|
pass
|
|
156
156
|
|
|
157
157
|
elif role == 'assistant' and content.strip().startswith('['):
|
|
@@ -179,7 +179,7 @@ class StreamingManager:
|
|
|
179
179
|
"input": block.get('input', {}),
|
|
180
180
|
}),
|
|
181
181
|
}
|
|
182
|
-
except:
|
|
182
|
+
except ValueError:
|
|
183
183
|
pass
|
|
184
184
|
|
|
185
185
|
last_message_count = len(current_messages)
|
dtSpark/web/server.py
CHANGED
|
@@ -9,6 +9,7 @@ import os
|
|
|
9
9
|
import sys
|
|
10
10
|
import socket
|
|
11
11
|
import logging
|
|
12
|
+
import time
|
|
12
13
|
import webbrowser
|
|
13
14
|
import signal
|
|
14
15
|
import asyncio
|
|
@@ -252,6 +253,48 @@ def create_app(
|
|
|
252
253
|
loop = asyncio.get_running_loop()
|
|
253
254
|
loop.set_exception_handler(_suppress_connection_reset_errors)
|
|
254
255
|
|
|
256
|
+
# Set to hold references to background tasks (prevents garbage collection)
|
|
257
|
+
_background_tasks = set()
|
|
258
|
+
|
|
259
|
+
# Browser heartbeat state
|
|
260
|
+
app.state.last_heartbeat = 0.0
|
|
261
|
+
|
|
262
|
+
@app.post("/api/heartbeat")
|
|
263
|
+
async def heartbeat():
|
|
264
|
+
"""Receive browser heartbeat ping."""
|
|
265
|
+
app.state.last_heartbeat = time.time()
|
|
266
|
+
return JSONResponse({"status": "ok"})
|
|
267
|
+
|
|
268
|
+
@app.on_event("startup")
|
|
269
|
+
async def start_heartbeat_monitor():
|
|
270
|
+
"""Start background task to monitor browser heartbeat."""
|
|
271
|
+
if not heartbeat_enabled:
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
async def _monitor_heartbeat():
|
|
275
|
+
# Initial grace period - wait for browser to connect and send first heartbeat
|
|
276
|
+
grace_period = heartbeat_timeout * 2
|
|
277
|
+
logger.info(
|
|
278
|
+
f"Browser heartbeat monitor started (interval={heartbeat_interval}s, "
|
|
279
|
+
f"timeout={heartbeat_timeout}s, grace={grace_period}s)"
|
|
280
|
+
)
|
|
281
|
+
await asyncio.sleep(grace_period)
|
|
282
|
+
|
|
283
|
+
while True:
|
|
284
|
+
await asyncio.sleep(heartbeat_interval)
|
|
285
|
+
last = app.state.last_heartbeat
|
|
286
|
+
if last > 0 and (time.time() - last) > heartbeat_timeout:
|
|
287
|
+
logger.info(
|
|
288
|
+
f"No browser heartbeat for {heartbeat_timeout}s - "
|
|
289
|
+
f"shutting down (browser likely closed)"
|
|
290
|
+
)
|
|
291
|
+
os.kill(os.getpid(), signal.SIGTERM)
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
task = asyncio.create_task(_monitor_heartbeat())
|
|
295
|
+
_background_tasks.add(task)
|
|
296
|
+
task.add_done_callback(_background_tasks.discard)
|
|
297
|
+
|
|
255
298
|
# Get template and static directories
|
|
256
299
|
web_dir = Path(__file__).parent
|
|
257
300
|
templates_dir = web_dir / "templates"
|
|
@@ -260,11 +303,24 @@ def create_app(
|
|
|
260
303
|
# Setup templates
|
|
261
304
|
templates = Jinja2Templates(directory=str(templates_dir))
|
|
262
305
|
|
|
306
|
+
# Determine feature flags from app instance
|
|
307
|
+
actions_enabled = getattr(app_instance, 'actions_enabled', False)
|
|
308
|
+
|
|
309
|
+
# Read heartbeat configuration
|
|
310
|
+
from dtPyAppFramework.settings import Settings as _Settings
|
|
311
|
+
_hb_settings = _Settings()
|
|
312
|
+
heartbeat_enabled = _hb_settings.get('interface.web.browser_heartbeat.enabled', True)
|
|
313
|
+
heartbeat_interval = _hb_settings.get('interface.web.browser_heartbeat.interval_seconds', 15)
|
|
314
|
+
heartbeat_timeout = _hb_settings.get('interface.web.browser_heartbeat.timeout_seconds', 60)
|
|
315
|
+
|
|
263
316
|
# Add global template variables for app name and version
|
|
264
317
|
templates.env.globals['app_name'] = full_name()
|
|
265
318
|
templates.env.globals['app_version'] = version()
|
|
266
319
|
templates.env.globals['app_description'] = description()
|
|
267
320
|
templates.env.globals['agent_name'] = agent_name()
|
|
321
|
+
templates.env.globals['actions_enabled'] = actions_enabled
|
|
322
|
+
templates.env.globals['heartbeat_enabled'] = heartbeat_enabled
|
|
323
|
+
templates.env.globals['heartbeat_interval_ms'] = heartbeat_interval * 1000
|
|
268
324
|
|
|
269
325
|
# Mount static files
|
|
270
326
|
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
|
@@ -428,13 +484,14 @@ def create_app(
|
|
|
428
484
|
logger.info("Shutdown request received via API")
|
|
429
485
|
# Send shutdown signal to the process
|
|
430
486
|
# Use a background task to allow the response to be sent first
|
|
431
|
-
import asyncio
|
|
432
487
|
async def shutdown_server():
|
|
433
488
|
await asyncio.sleep(0.5) # Give time for response to be sent
|
|
434
489
|
logger.info("Shutting down web server...")
|
|
435
490
|
os.kill(os.getpid(), signal.SIGTERM)
|
|
436
491
|
|
|
437
|
-
asyncio.create_task(shutdown_server())
|
|
492
|
+
task = asyncio.create_task(shutdown_server())
|
|
493
|
+
_background_tasks.add(task)
|
|
494
|
+
task.add_done_callback(_background_tasks.discard)
|
|
438
495
|
return JSONResponse({"status": "shutdown initiated"})
|
|
439
496
|
|
|
440
497
|
@app.get("/menu", response_class=HTMLResponse)
|
|
@@ -456,13 +513,14 @@ def create_app(
|
|
|
456
513
|
chat_router,
|
|
457
514
|
streaming_router,
|
|
458
515
|
)
|
|
459
|
-
from .endpoints.autonomous_actions import router as autonomous_actions_router
|
|
460
|
-
|
|
461
516
|
app.include_router(main_menu_router, prefix="/api", tags=["Main Menu"])
|
|
462
517
|
app.include_router(conversations_router, prefix="/api", tags=["Conversations"])
|
|
463
518
|
app.include_router(chat_router, prefix="/api", tags=["Chat"])
|
|
464
519
|
app.include_router(streaming_router, prefix="/api", tags=["Streaming"])
|
|
465
|
-
|
|
520
|
+
|
|
521
|
+
if actions_enabled:
|
|
522
|
+
from .endpoints.autonomous_actions import router as autonomous_actions_router
|
|
523
|
+
app.include_router(autonomous_actions_router, prefix="/api", tags=["Autonomous Actions"])
|
|
466
524
|
|
|
467
525
|
# Add template routes for conversations and chat
|
|
468
526
|
@app.get("/conversations", response_class=HTMLResponse)
|
|
@@ -515,6 +573,8 @@ def create_app(
|
|
|
515
573
|
@app.get("/actions", response_class=HTMLResponse)
|
|
516
574
|
async def actions_page(request: Request, session_id: str = Depends(get_session)):
|
|
517
575
|
"""Display autonomous actions management page."""
|
|
576
|
+
if not actions_enabled:
|
|
577
|
+
return RedirectResponse(url="/menu", status_code=303)
|
|
518
578
|
return templates.TemplateResponse(
|
|
519
579
|
"actions.html",
|
|
520
580
|
{
|
dtSpark/web/ssl_utils.py
CHANGED
|
@@ -10,7 +10,7 @@ import os.path
|
|
|
10
10
|
import socket
|
|
11
11
|
import ipaddress
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from datetime import datetime, timedelta
|
|
13
|
+
from datetime import datetime, timedelta, timezone
|
|
14
14
|
from typing import Tuple, Optional
|
|
15
15
|
|
|
16
16
|
from dtPyAppFramework.paths import ApplicationPaths
|
|
@@ -72,8 +72,8 @@ def generate_self_signed_certificate(
|
|
|
72
72
|
.issuer_name(issuer)
|
|
73
73
|
.public_key(private_key.public_key())
|
|
74
74
|
.serial_number(x509.random_serial_number())
|
|
75
|
-
.not_valid_before(datetime.
|
|
76
|
-
.not_valid_after(datetime.
|
|
75
|
+
.not_valid_before(datetime.now(timezone.utc))
|
|
76
|
+
.not_valid_after(datetime.now(timezone.utc) + timedelta(days=validity_days))
|
|
77
77
|
.add_extension(
|
|
78
78
|
x509.SubjectAlternativeName([
|
|
79
79
|
x509.DNSName(hostname),
|
|
@@ -94,7 +94,8 @@ body {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
.chat-message .message-content pre {
|
|
97
|
-
background-color:
|
|
97
|
+
background-color: #0d1117;
|
|
98
|
+
border: 1px solid #30363d;
|
|
98
99
|
padding: 1rem;
|
|
99
100
|
border-radius: 0.5rem;
|
|
100
101
|
overflow-x: auto;
|
|
@@ -195,16 +196,19 @@ body {
|
|
|
195
196
|
|
|
196
197
|
/* Code blocks in markdown */
|
|
197
198
|
.markdown-content pre {
|
|
198
|
-
background-color:
|
|
199
|
+
background-color: #0d1117;
|
|
200
|
+
border: 1px solid #30363d;
|
|
199
201
|
padding: 1rem;
|
|
200
202
|
border-radius: 0.5rem;
|
|
201
203
|
overflow-x: auto;
|
|
204
|
+
margin: 1rem 0;
|
|
202
205
|
}
|
|
203
206
|
|
|
204
207
|
.markdown-content code {
|
|
205
|
-
background-color: rgba(
|
|
208
|
+
background-color: rgba(110, 118, 129, 0.3);
|
|
206
209
|
padding: 0.2rem 0.4rem;
|
|
207
210
|
border-radius: 0.25rem;
|
|
211
|
+
font-size: 0.875em;
|
|
208
212
|
}
|
|
209
213
|
|
|
210
214
|
/* Alerts */
|
|
@@ -324,7 +328,7 @@ body {
|
|
|
324
328
|
border: 1px solid #f44336;
|
|
325
329
|
border-radius: 0.5rem;
|
|
326
330
|
padding: 1rem;
|
|
327
|
-
color: #
|
|
331
|
+
color: #ffd7d3;
|
|
328
332
|
}
|
|
329
333
|
|
|
330
334
|
.mermaid-error .text-danger {
|
|
@@ -339,14 +343,6 @@ body {
|
|
|
339
343
|
padding: 0;
|
|
340
344
|
}
|
|
341
345
|
|
|
342
|
-
.chat-message .message-content pre {
|
|
343
|
-
background-color: #0d1117;
|
|
344
|
-
border: 1px solid #30363d;
|
|
345
|
-
border-radius: 0.5rem;
|
|
346
|
-
padding: 1rem;
|
|
347
|
-
overflow-x: auto;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
346
|
.chat-message .message-content code:not(.hljs) {
|
|
351
347
|
background-color: rgba(110, 118, 129, 0.3);
|
|
352
348
|
padding: 0.2rem 0.4rem;
|
|
@@ -354,29 +350,12 @@ body {
|
|
|
354
350
|
font-size: 0.875em;
|
|
355
351
|
}
|
|
356
352
|
|
|
357
|
-
/* Markdown content styling */
|
|
358
|
-
.markdown-content pre {
|
|
359
|
-
background-color: #0d1117;
|
|
360
|
-
border: 1px solid #30363d;
|
|
361
|
-
border-radius: 0.5rem;
|
|
362
|
-
padding: 1rem;
|
|
363
|
-
overflow-x: auto;
|
|
364
|
-
margin: 1rem 0;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
353
|
.markdown-content pre code {
|
|
368
354
|
background-color: transparent;
|
|
369
355
|
padding: 0;
|
|
370
356
|
border-radius: 0;
|
|
371
357
|
}
|
|
372
358
|
|
|
373
|
-
.markdown-content code {
|
|
374
|
-
background-color: rgba(110, 118, 129, 0.3);
|
|
375
|
-
padding: 0.2rem 0.4rem;
|
|
376
|
-
border-radius: 0.25rem;
|
|
377
|
-
font-size: 0.875em;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
359
|
.markdown-content blockquote {
|
|
381
360
|
border-left: 4px solid var(--spark-cyan);
|
|
382
361
|
padding-left: 1rem;
|
dtSpark/web/static/js/chat.js
CHANGED
|
@@ -50,8 +50,6 @@ async function loadChatHistory(conversationId) {
|
|
|
50
50
|
* @param {string} timestamp - Message timestamp (optional)
|
|
51
51
|
*/
|
|
52
52
|
function appendMessage(role, content, timestamp = null) {
|
|
53
|
-
const messagesContainer = document.getElementById('chat-messages');
|
|
54
|
-
|
|
55
53
|
// Check if content contains tool results
|
|
56
54
|
if (content.startsWith('[TOOL_RESULTS]')) {
|
|
57
55
|
appendToolResults(content, timestamp);
|
|
@@ -107,7 +105,7 @@ function appendRegularMessage(role, content, timestamp = null) {
|
|
|
107
105
|
messageDiv.className = `chat-message ${role}`;
|
|
108
106
|
|
|
109
107
|
// Generate unique ID for the copy button
|
|
110
|
-
const copyBtnId = 'copy-btn-' + Date.now() + '-' + Math.random().toString(36).
|
|
108
|
+
const copyBtnId = 'copy-btn-' + Date.now() + '-' + Math.random().toString(36).substring(2, 11);
|
|
111
109
|
|
|
112
110
|
// Create message header with copy icon
|
|
113
111
|
const header = document.createElement('div');
|
|
@@ -308,8 +306,8 @@ function removeStatus(idOrElement) {
|
|
|
308
306
|
document.getElementById(idOrElement) :
|
|
309
307
|
idOrElement;
|
|
310
308
|
|
|
311
|
-
if (element
|
|
312
|
-
element.
|
|
309
|
+
if (element?.parentNode) {
|
|
310
|
+
element.remove();
|
|
313
311
|
}
|
|
314
312
|
}
|
|
315
313
|
|
|
@@ -360,7 +358,7 @@ function updateStreamingMessage(content, messageElement = null) {
|
|
|
360
358
|
messageElement.className = 'chat-message assistant';
|
|
361
359
|
|
|
362
360
|
// Generate unique ID for the copy button
|
|
363
|
-
const copyBtnId = 'stream-copy-btn-' + Date.now() + '-' + Math.random().toString(36).
|
|
361
|
+
const copyBtnId = 'stream-copy-btn-' + Date.now() + '-' + Math.random().toString(36).substring(2, 11);
|
|
364
362
|
|
|
365
363
|
messageElement.innerHTML = `
|
|
366
364
|
<div class="message-header d-flex justify-content-between align-items-center">
|
|
@@ -463,7 +461,7 @@ function showToast(message, type = 'info') {
|
|
|
463
461
|
const toast = document.createElement('div');
|
|
464
462
|
toast.id = toastId;
|
|
465
463
|
toast.className = 'toast';
|
|
466
|
-
toast.
|
|
464
|
+
toast.role = 'alert';
|
|
467
465
|
|
|
468
466
|
let bgClass = 'bg-primary';
|
|
469
467
|
if (type === 'success') bgClass = 'bg-success';
|
|
@@ -550,7 +548,7 @@ async function showToolPermissionDialog(requestId, toolName, toolDescription) {
|
|
|
550
548
|
const buttons = modalElement.querySelectorAll('.permission-btn');
|
|
551
549
|
buttons.forEach(button => {
|
|
552
550
|
button.addEventListener('click', async () => {
|
|
553
|
-
const response = button.
|
|
551
|
+
const response = button.dataset.response;
|
|
554
552
|
|
|
555
553
|
// Send response to server
|
|
556
554
|
try {
|
dtSpark/web/static/js/main.js
CHANGED
|
@@ -74,7 +74,7 @@ if (typeof marked !== 'undefined') {
|
|
|
74
74
|
|
|
75
75
|
// Handle mermaid diagrams
|
|
76
76
|
if (lang === 'mermaid') {
|
|
77
|
-
const id = 'mermaid-' + Math.random().toString(36).
|
|
77
|
+
const id = 'mermaid-' + Math.random().toString(36).substring(2, 11);
|
|
78
78
|
return `<div class="mermaid-container"><pre class="mermaid" id="${id}">${escapeHtmlForMermaid(code)}</pre></div>`;
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -157,7 +157,7 @@ async function copySvgToClipboard(svgElement, button) {
|
|
|
157
157
|
bgRect.setAttribute('width', '100%');
|
|
158
158
|
bgRect.setAttribute('height', '100%');
|
|
159
159
|
bgRect.setAttribute('fill', '#1a1a1a');
|
|
160
|
-
svgClone.
|
|
160
|
+
svgClone.prepend(bgRect);
|
|
161
161
|
|
|
162
162
|
// Serialise SVG to string
|
|
163
163
|
const serializer = new XMLSerializer();
|
|
@@ -255,7 +255,7 @@ async function renderMermaidDiagrams(container) {
|
|
|
255
255
|
|
|
256
256
|
for (const block of mermaidBlocks) {
|
|
257
257
|
try {
|
|
258
|
-
const id = block.id || 'mermaid-' + Math.random().toString(36).
|
|
258
|
+
const id = block.id || 'mermaid-' + Math.random().toString(36).substring(2, 11);
|
|
259
259
|
const code = block.textContent;
|
|
260
260
|
|
|
261
261
|
// Render the diagram
|
|
@@ -287,7 +287,7 @@ async function renderMermaidDiagrams(container) {
|
|
|
287
287
|
}
|
|
288
288
|
};
|
|
289
289
|
|
|
290
|
-
block.
|
|
290
|
+
block.replaceWith(wrapper);
|
|
291
291
|
} catch (e) {
|
|
292
292
|
console.error('Mermaid rendering error:', e);
|
|
293
293
|
// Show error message in the block
|
|
@@ -370,9 +370,9 @@ function showToast(message, type = 'info') {
|
|
|
370
370
|
// Create toast
|
|
371
371
|
const toast = document.createElement('div');
|
|
372
372
|
toast.className = `toast align-items-center text-white bg-${type === 'error' ? 'danger' : type} border-0`;
|
|
373
|
-
toast.
|
|
374
|
-
toast.
|
|
375
|
-
toast.
|
|
373
|
+
toast.role = 'alert';
|
|
374
|
+
toast.ariaLive = 'assertive';
|
|
375
|
+
toast.ariaAtomic = 'true';
|
|
376
376
|
|
|
377
377
|
toast.innerHTML = `
|
|
378
378
|
<div class="d-flex">
|
|
@@ -432,7 +432,7 @@ function downloadFile(content, filename, mimeType = 'text/plain') {
|
|
|
432
432
|
link.download = filename;
|
|
433
433
|
document.body.appendChild(link);
|
|
434
434
|
link.click();
|
|
435
|
-
|
|
435
|
+
link.remove();
|
|
436
436
|
URL.revokeObjectURL(url);
|
|
437
437
|
}
|
|
438
438
|
|