dtSpark 1.0.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. dtSpark/__init__.py +0 -0
  2. dtSpark/_description.txt +1 -0
  3. dtSpark/_full_name.txt +1 -0
  4. dtSpark/_licence.txt +21 -0
  5. dtSpark/_metadata.yaml +6 -0
  6. dtSpark/_name.txt +1 -0
  7. dtSpark/_version.txt +1 -0
  8. dtSpark/aws/__init__.py +7 -0
  9. dtSpark/aws/authentication.py +296 -0
  10. dtSpark/aws/bedrock.py +578 -0
  11. dtSpark/aws/costs.py +318 -0
  12. dtSpark/aws/pricing.py +580 -0
  13. dtSpark/cli_interface.py +2645 -0
  14. dtSpark/conversation_manager.py +3050 -0
  15. dtSpark/core/__init__.py +12 -0
  16. dtSpark/core/application.py +3355 -0
  17. dtSpark/core/context_compaction.py +735 -0
  18. dtSpark/daemon/__init__.py +104 -0
  19. dtSpark/daemon/__main__.py +10 -0
  20. dtSpark/daemon/action_monitor.py +213 -0
  21. dtSpark/daemon/daemon_app.py +730 -0
  22. dtSpark/daemon/daemon_manager.py +289 -0
  23. dtSpark/daemon/execution_coordinator.py +194 -0
  24. dtSpark/daemon/pid_file.py +169 -0
  25. dtSpark/database/__init__.py +482 -0
  26. dtSpark/database/autonomous_actions.py +1191 -0
  27. dtSpark/database/backends.py +329 -0
  28. dtSpark/database/connection.py +122 -0
  29. dtSpark/database/conversations.py +520 -0
  30. dtSpark/database/credential_prompt.py +218 -0
  31. dtSpark/database/files.py +205 -0
  32. dtSpark/database/mcp_ops.py +355 -0
  33. dtSpark/database/messages.py +161 -0
  34. dtSpark/database/schema.py +673 -0
  35. dtSpark/database/tool_permissions.py +186 -0
  36. dtSpark/database/usage.py +167 -0
  37. dtSpark/files/__init__.py +4 -0
  38. dtSpark/files/manager.py +322 -0
  39. dtSpark/launch.py +39 -0
  40. dtSpark/limits/__init__.py +10 -0
  41. dtSpark/limits/costs.py +296 -0
  42. dtSpark/limits/tokens.py +342 -0
  43. dtSpark/llm/__init__.py +17 -0
  44. dtSpark/llm/anthropic_direct.py +446 -0
  45. dtSpark/llm/base.py +146 -0
  46. dtSpark/llm/context_limits.py +438 -0
  47. dtSpark/llm/manager.py +177 -0
  48. dtSpark/llm/ollama.py +578 -0
  49. dtSpark/mcp_integration/__init__.py +5 -0
  50. dtSpark/mcp_integration/manager.py +653 -0
  51. dtSpark/mcp_integration/tool_selector.py +225 -0
  52. dtSpark/resources/config.yaml.template +631 -0
  53. dtSpark/safety/__init__.py +22 -0
  54. dtSpark/safety/llm_service.py +111 -0
  55. dtSpark/safety/patterns.py +229 -0
  56. dtSpark/safety/prompt_inspector.py +442 -0
  57. dtSpark/safety/violation_logger.py +346 -0
  58. dtSpark/scheduler/__init__.py +20 -0
  59. dtSpark/scheduler/creation_tools.py +599 -0
  60. dtSpark/scheduler/execution_queue.py +159 -0
  61. dtSpark/scheduler/executor.py +1152 -0
  62. dtSpark/scheduler/manager.py +395 -0
  63. dtSpark/tools/__init__.py +4 -0
  64. dtSpark/tools/builtin.py +833 -0
  65. dtSpark/web/__init__.py +20 -0
  66. dtSpark/web/auth.py +152 -0
  67. dtSpark/web/dependencies.py +37 -0
  68. dtSpark/web/endpoints/__init__.py +17 -0
  69. dtSpark/web/endpoints/autonomous_actions.py +1125 -0
  70. dtSpark/web/endpoints/chat.py +621 -0
  71. dtSpark/web/endpoints/conversations.py +353 -0
  72. dtSpark/web/endpoints/main_menu.py +547 -0
  73. dtSpark/web/endpoints/streaming.py +421 -0
  74. dtSpark/web/server.py +578 -0
  75. dtSpark/web/session.py +167 -0
  76. dtSpark/web/ssl_utils.py +195 -0
  77. dtSpark/web/static/css/dark-theme.css +427 -0
  78. dtSpark/web/static/js/actions.js +1101 -0
  79. dtSpark/web/static/js/chat.js +614 -0
  80. dtSpark/web/static/js/main.js +496 -0
  81. dtSpark/web/static/js/sse-client.js +242 -0
  82. dtSpark/web/templates/actions.html +408 -0
  83. dtSpark/web/templates/base.html +93 -0
  84. dtSpark/web/templates/chat.html +814 -0
  85. dtSpark/web/templates/conversations.html +350 -0
  86. dtSpark/web/templates/goodbye.html +81 -0
  87. dtSpark/web/templates/login.html +90 -0
  88. dtSpark/web/templates/main_menu.html +983 -0
  89. dtSpark/web/templates/new_conversation.html +191 -0
  90. dtSpark/web/web_interface.py +137 -0
  91. dtspark-1.0.4.dist-info/METADATA +187 -0
  92. dtspark-1.0.4.dist-info/RECORD +96 -0
  93. dtspark-1.0.4.dist-info/WHEEL +5 -0
  94. dtspark-1.0.4.dist-info/entry_points.txt +3 -0
  95. dtspark-1.0.4.dist-info/licenses/LICENSE +21 -0
  96. dtspark-1.0.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,547 @@
1
+ """
2
+ Main menu API endpoints.
3
+
4
+ Provides REST API for main menu operations:
5
+ - Re-gather AWS Bedrock costs
6
+ - Get account information
7
+ - Get MCP server status
8
+ - Application status
9
+
10
+
11
+ """
12
+
13
+ import logging
14
+ from typing import Optional
15
+
16
+ from fastapi import APIRouter, Depends, Request, HTTPException
17
+ from pydantic import BaseModel
18
+
19
+ from ..dependencies import get_current_session
20
+
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ router = APIRouter()
25
+
26
+
27
+ class AccountInfo(BaseModel):
28
+ """Account information for configured provider."""
29
+ provider: str
30
+ user_arn: Optional[str] = None
31
+ account_id: Optional[str] = None
32
+ region: Optional[str] = None
33
+ user_guid: str
34
+ auth_method: Optional[str] = None
35
+
36
+
37
+ class CostInfo(BaseModel):
38
+ """Cost information for a specific period."""
39
+ total: float
40
+ models: dict[str, dict] # model_id -> {cost, percentage}
41
+
42
+
43
+ class MCPServerInfo(BaseModel):
44
+ """MCP server information."""
45
+ name: str
46
+ transport: str
47
+ connected: bool
48
+ tool_count: int
49
+
50
+
51
+ class ProviderModelInfo(BaseModel):
52
+ """Model information for a provider."""
53
+ model_id: str
54
+ display_name: str
55
+ description: Optional[str] = None
56
+
57
+
58
+ class ProviderInfo(BaseModel):
59
+ """Information about a configured LLM provider."""
60
+ name: str
61
+ type: str # 'aws', 'anthropic', 'ollama'
62
+ enabled: bool
63
+ status: str # 'connected', 'error', 'disabled'
64
+ models: list[ProviderModelInfo] = []
65
+ auth_method: Optional[str] = None
66
+ region: Optional[str] = None
67
+ base_url: Optional[str] = None
68
+
69
+
70
+ @router.get("/account")
71
+ async def get_account_info(
72
+ request: Request,
73
+ session_id: str = Depends(get_current_session),
74
+ ) -> AccountInfo:
75
+ """
76
+ Get account information for the configured provider.
77
+
78
+ Returns:
79
+ AccountInfo with provider-specific details
80
+ """
81
+ try:
82
+ app_instance = request.app.state.app_instance
83
+
84
+ # Get LLM manager to determine active provider
85
+ llm_manager = getattr(app_instance, 'llm_manager', None)
86
+ user_guid = getattr(app_instance, 'user_guid', 'unknown')
87
+
88
+ if llm_manager and llm_manager.active_provider:
89
+ active_provider = llm_manager.active_provider.lower()
90
+
91
+ # AWS Bedrock
92
+ if 'bedrock' in active_provider or 'aws' in active_provider:
93
+ auth = getattr(app_instance, 'authenticator', None)
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
+ )
121
+
122
+ # No provider configured - return basic info
123
+ return AccountInfo(
124
+ provider='none',
125
+ user_guid=user_guid,
126
+ )
127
+
128
+ except HTTPException:
129
+ raise
130
+ except Exception as e:
131
+ logger.error(f"Error getting account info: {e}")
132
+ raise HTTPException(status_code=500, detail=str(e))
133
+
134
+
135
+ @router.get("/providers")
136
+ async def get_providers(
137
+ request: Request,
138
+ session_id: str = Depends(get_current_session),
139
+ ) -> list[ProviderInfo]:
140
+ """
141
+ Get all configured LLM providers and their available models.
142
+
143
+ Returns:
144
+ List of ProviderInfo with provider details and available models
145
+ """
146
+ try:
147
+ app_instance = request.app.state.app_instance
148
+ providers = []
149
+
150
+ # Get LLM manager which has all registered providers
151
+ llm_manager = getattr(app_instance, 'llm_manager', None)
152
+
153
+ if llm_manager and hasattr(llm_manager, 'providers'):
154
+ for provider_name, service in llm_manager.providers.items():
155
+ models = []
156
+ status = 'connected'
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
+ ))
215
+
216
+ return providers
217
+
218
+ except Exception as e:
219
+ logger.error(f"Error getting providers: {e}")
220
+ raise HTTPException(status_code=500, detail=str(e))
221
+
222
+
223
+ @router.get("/costs/last-month")
224
+ async def get_last_month_costs(
225
+ request: Request,
226
+ session_id: str = Depends(get_current_session),
227
+ ) -> CostInfo:
228
+ """
229
+ Get AWS Bedrock costs for the last month.
230
+
231
+ Returns:
232
+ CostInfo with total cost and per-model breakdown
233
+ """
234
+ try:
235
+ app_instance = request.app.state.app_instance
236
+
237
+ # Get costs from app instance (cached from startup)
238
+ if not hasattr(app_instance, 'bedrock_costs') or not app_instance.bedrock_costs:
239
+ return CostInfo(total=0.0, models={})
240
+
241
+ last_month_data = app_instance.bedrock_costs.get('last_month', {})
242
+
243
+ # Extract total and models from the cost data structure
244
+ total = last_month_data.get('total', 0.0)
245
+ models_breakdown = last_month_data.get('breakdown', {})
246
+
247
+ # Format per-model costs
248
+ models = {}
249
+ for model_id, cost in models_breakdown.items():
250
+ percentage = (cost / total * 100) if total > 0 else 0
251
+
252
+ models[model_id] = {
253
+ 'cost': cost,
254
+ 'percentage': round(percentage, 2),
255
+ }
256
+
257
+ return CostInfo(
258
+ total=round(total, 2),
259
+ models=models,
260
+ )
261
+
262
+ except Exception as e:
263
+ logger.error(f"Error getting last month costs: {e}")
264
+ raise HTTPException(status_code=500, detail=str(e))
265
+
266
+
267
+ @router.get("/costs/last-24-hours")
268
+ async def get_last_24_hours_costs(
269
+ request: Request,
270
+ session_id: str = Depends(get_current_session),
271
+ ) -> CostInfo:
272
+ """
273
+ Get AWS Bedrock costs for the last 24 hours.
274
+
275
+ Returns:
276
+ CostInfo with total cost and per-model breakdown
277
+ """
278
+ try:
279
+ app_instance = request.app.state.app_instance
280
+
281
+ # Get costs from app instance (cached from startup)
282
+ if not hasattr(app_instance, 'bedrock_costs') or not app_instance.bedrock_costs:
283
+ return CostInfo(total=0.0, models={})
284
+
285
+ last_24h_data = app_instance.bedrock_costs.get('last_24h', {})
286
+
287
+ # Extract total and models from the cost data structure
288
+ total = last_24h_data.get('total', 0.0)
289
+ models_breakdown = last_24h_data.get('breakdown', {})
290
+
291
+ # Format per-model costs
292
+ models = {}
293
+ for model_id, cost in models_breakdown.items():
294
+ percentage = (cost / total * 100) if total > 0 else 0
295
+
296
+ models[model_id] = {
297
+ 'cost': cost,
298
+ 'percentage': round(percentage, 2),
299
+ }
300
+
301
+ return CostInfo(
302
+ total=round(total, 2),
303
+ models=models,
304
+ )
305
+
306
+ except Exception as e:
307
+ logger.error(f"Error getting last 24 hours costs: {e}")
308
+ raise HTTPException(status_code=500, detail=str(e))
309
+
310
+
311
+ @router.post("/costs/refresh")
312
+ async def refresh_costs(
313
+ request: Request,
314
+ session_id: str = Depends(get_current_session),
315
+ ) -> dict:
316
+ """
317
+ Refresh AWS Bedrock cost information.
318
+
319
+ Returns:
320
+ Status message
321
+ """
322
+ try:
323
+ app_instance = request.app.state.app_instance
324
+
325
+ # Re-gather costs
326
+ if hasattr(app_instance, 'cost_tracker') and app_instance.cost_tracker:
327
+ app_instance.bedrock_costs = app_instance.cost_tracker.get_bedrock_costs()
328
+
329
+ return {
330
+ "status": "success",
331
+ "message": "Costs refreshed successfully",
332
+ }
333
+ else:
334
+ return {
335
+ "status": "error",
336
+ "message": "Cost tracker not available",
337
+ }
338
+
339
+ except Exception as e:
340
+ logger.error(f"Error refreshing costs: {e}")
341
+ raise HTTPException(status_code=500, detail=str(e))
342
+
343
+
344
+ @router.get("/mcp/servers")
345
+ async def get_mcp_servers(
346
+ request: Request,
347
+ session_id: str = Depends(get_current_session),
348
+ ) -> list[MCPServerInfo]:
349
+ """
350
+ Get MCP server status and information.
351
+
352
+ Returns:
353
+ List of MCPServerInfo with connection status and tool counts
354
+ """
355
+ try:
356
+ app_instance = request.app.state.app_instance
357
+
358
+ # Check if MCP is enabled
359
+ if not hasattr(app_instance, 'mcp_manager') or not app_instance.mcp_manager:
360
+ return []
361
+
362
+ servers = []
363
+ mcp_manager = app_instance.mcp_manager
364
+
365
+ # Get all tools from MCP servers (async call)
366
+ all_tools = await mcp_manager.list_all_tools()
367
+
368
+ # Count tools by server
369
+ tool_counts = {}
370
+ for tool in all_tools:
371
+ server = tool.get('server', 'unknown')
372
+ tool_counts[server] = tool_counts.get(server, 0) + 1
373
+
374
+ # Get information for each server
375
+ if hasattr(mcp_manager, 'clients'):
376
+ for server_name, client in mcp_manager.clients.items():
377
+ # Determine transport type
378
+ transport = 'stdio'
379
+ if hasattr(client, 'config') and hasattr(client.config, 'transport'):
380
+ transport = client.config.transport
381
+
382
+ servers.append(
383
+ MCPServerInfo(
384
+ name=server_name,
385
+ transport=transport,
386
+ connected=client.connected if hasattr(client, 'connected') else True,
387
+ tool_count=tool_counts.get(server_name, 0),
388
+ )
389
+ )
390
+
391
+ return servers
392
+
393
+ except Exception as e:
394
+ logger.error(f"Error getting MCP servers: {e}")
395
+ raise HTTPException(status_code=500, detail=str(e))
396
+
397
+
398
+ @router.get("/tools")
399
+ async def get_all_tools(
400
+ request: Request,
401
+ session_id: str = Depends(get_current_session),
402
+ ) -> dict:
403
+ """
404
+ Get all available tools (MCP + embedded).
405
+
406
+ Returns:
407
+ Dictionary with 'tools' list containing all available tools
408
+ """
409
+ try:
410
+ app_instance = request.app.state.app_instance
411
+ all_tools = []
412
+
413
+ # Get MCP tools
414
+ if hasattr(app_instance, 'mcp_manager') and app_instance.mcp_manager:
415
+ try:
416
+ mcp_tools = await app_instance.mcp_manager.list_all_tools()
417
+ for tool in mcp_tools:
418
+ all_tools.append({
419
+ 'name': tool.get('name', 'unknown'),
420
+ 'description': tool.get('description', ''),
421
+ 'server': tool.get('server', ''),
422
+ 'source': 'mcp',
423
+ })
424
+ except Exception as e:
425
+ logger.warning(f"Error getting MCP tools: {e}")
426
+
427
+ # Get embedded tools
428
+ if hasattr(app_instance, 'conversation_manager') and app_instance.conversation_manager:
429
+ try:
430
+ embedded = app_instance.conversation_manager.get_embedded_tools()
431
+ for tool in embedded:
432
+ tool_spec = tool.get('toolSpec', {})
433
+ all_tools.append({
434
+ 'name': tool_spec.get('name', 'unknown'),
435
+ 'description': tool_spec.get('description', ''),
436
+ 'server': '',
437
+ 'source': 'embedded',
438
+ })
439
+ except Exception as e:
440
+ logger.warning(f"Error getting embedded tools: {e}")
441
+
442
+ return {'tools': all_tools}
443
+
444
+ except Exception as e:
445
+ logger.error(f"Error getting tools: {e}")
446
+ raise HTTPException(status_code=500, detail=str(e))
447
+
448
+
449
+ @router.get("/mcp/tools")
450
+ async def get_mcp_tools(
451
+ request: Request,
452
+ server: Optional[str] = None,
453
+ session_id: str = Depends(get_current_session),
454
+ ) -> list[dict]:
455
+ """
456
+ Get available tools from MCP servers.
457
+
458
+ Args:
459
+ server: Optional server name to filter tools by
460
+
461
+ Returns:
462
+ List of tool definitions with name, description, and input schema
463
+ """
464
+ try:
465
+ app_instance = request.app.state.app_instance
466
+
467
+ # Check if MCP is enabled
468
+ if not hasattr(app_instance, 'mcp_manager') or not app_instance.mcp_manager:
469
+ return []
470
+
471
+ mcp_manager = app_instance.mcp_manager
472
+
473
+ # Get all tools from MCP servers
474
+ all_tools = await mcp_manager.list_all_tools()
475
+
476
+ # Filter by server if specified
477
+ if server:
478
+ all_tools = [t for t in all_tools if t.get('server') == server]
479
+
480
+ # Format tool information
481
+ tools = []
482
+ for tool in all_tools:
483
+ tools.append({
484
+ 'name': tool.get('name', 'unknown'),
485
+ 'description': tool.get('description', ''),
486
+ 'server': tool.get('server', 'unknown'),
487
+ 'input_schema': tool.get('inputSchema', {}),
488
+ })
489
+
490
+ return tools
491
+
492
+ except Exception as e:
493
+ logger.error(f"Error getting MCP tools: {e}")
494
+ raise HTTPException(status_code=500, detail=str(e))
495
+
496
+
497
+ @router.get("/daemon/status")
498
+ async def get_daemon_status(request: Request):
499
+ """
500
+ Get daemon status.
501
+
502
+ Returns:
503
+ Dictionary with daemon status information
504
+ """
505
+ try:
506
+ app_instance = request.app.state.app_instance
507
+
508
+ # Check if daemon is running using PID file
509
+ daemon_running = False
510
+ daemon_pid = None
511
+
512
+ try:
513
+ from dtSpark.daemon.pid_file import PIDFile
514
+ from dtPyAppFramework.settings import Settings
515
+
516
+ settings = app_instance.settings if hasattr(app_instance, 'settings') else Settings()
517
+ pid_file_path = settings.get('daemon.pid_file', './daemon.pid')
518
+ pid_file = PIDFile(pid_file_path)
519
+
520
+ daemon_running = pid_file.is_running()
521
+ if daemon_running:
522
+ daemon_pid = pid_file.read_pid()
523
+
524
+ except Exception as e:
525
+ logger.warning(f"Error checking daemon status: {e}")
526
+
527
+ # Count scheduled actions
528
+ scheduled_count = 0
529
+ try:
530
+ actions = app_instance.database.get_all_actions(include_disabled=False)
531
+ scheduled_count = sum(1 for a in actions if a.get('schedule_type') != 'manual')
532
+ except Exception:
533
+ pass
534
+
535
+ return {
536
+ 'daemon_running': daemon_running,
537
+ 'daemon_pid': daemon_pid,
538
+ 'scheduled_actions_count': scheduled_count,
539
+ 'warning': None if daemon_running else (
540
+ f"Daemon is not running - {scheduled_count} scheduled action(s) will not execute"
541
+ if scheduled_count > 0 else None
542
+ )
543
+ }
544
+
545
+ except Exception as e:
546
+ logger.error(f"Error getting daemon status: {e}")
547
+ raise HTTPException(status_code=500, detail=str(e))