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,353 @@
1
+ """
2
+ Conversation management API endpoints.
3
+
4
+ Provides REST API for conversation operations:
5
+ - List conversations
6
+ - Create conversation
7
+ - Delete conversation
8
+ - Get conversation details
9
+
10
+
11
+ """
12
+
13
+ import logging
14
+ import tempfile
15
+ import os
16
+ from typing import Optional, List
17
+ from datetime import datetime
18
+
19
+ from fastapi import APIRouter, Depends, Request, HTTPException, UploadFile, File, Form
20
+ from pydantic import BaseModel
21
+
22
+ from ..dependencies import get_current_session
23
+
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ router = APIRouter()
28
+
29
+
30
+ def parse_datetime(dt_value):
31
+ """Parse datetime from string or return datetime object."""
32
+ if dt_value is None:
33
+ return None
34
+ if isinstance(dt_value, datetime):
35
+ return dt_value
36
+ if isinstance(dt_value, str):
37
+ # SQLite returns timestamps as strings
38
+ try:
39
+ return datetime.fromisoformat(dt_value.replace('Z', '+00:00'))
40
+ except:
41
+ return datetime.strptime(dt_value, '%Y-%m-%d %H:%M:%S.%f')
42
+ return None
43
+
44
+
45
+ class ConversationSummary(BaseModel):
46
+ """Summary information for a conversation."""
47
+ id: int
48
+ name: str
49
+ model_id: str
50
+ created_at: datetime
51
+ message_count: int
52
+ last_message_at: Optional[datetime]
53
+
54
+
55
+ class ConversationDetail(BaseModel):
56
+ """Detailed information for a conversation."""
57
+ id: int
58
+ name: str
59
+ model_id: str
60
+ instructions: Optional[str]
61
+ created_at: datetime
62
+ message_count: int
63
+ tokens_sent: int
64
+ tokens_received: int
65
+ total_tokens: int
66
+ attached_files: List[str]
67
+ warnings: Optional[List[str]] = None
68
+
69
+
70
+ @router.get("/conversations")
71
+ async def list_conversations(
72
+ request: Request,
73
+ session_id: str = Depends(get_current_session),
74
+ ) -> List[ConversationSummary]:
75
+ """
76
+ List all conversations.
77
+
78
+ Returns:
79
+ List of ConversationSummary objects
80
+ """
81
+ try:
82
+ app_instance = request.app.state.app_instance
83
+
84
+ # Get active conversations via conversation manager
85
+ conversations = app_instance.conversation_manager.get_active_conversations()
86
+
87
+ summaries = []
88
+ for conv in conversations:
89
+ summaries.append(
90
+ ConversationSummary(
91
+ id=conv['id'],
92
+ name=conv['name'],
93
+ model_id=conv['model_id'],
94
+ created_at=parse_datetime(conv['created_at']),
95
+ message_count=conv.get('message_count', 0),
96
+ last_message_at=parse_datetime(conv.get('last_message_at')),
97
+ )
98
+ )
99
+
100
+ return summaries
101
+
102
+ except Exception as e:
103
+ logger.error(f"Error listing conversations: {e}")
104
+ raise HTTPException(status_code=500, detail=str(e))
105
+
106
+
107
+ @router.get("/conversations/{conversation_id}")
108
+ async def get_conversation(
109
+ conversation_id: int,
110
+ request: Request,
111
+ session_id: str = Depends(get_current_session),
112
+ ) -> ConversationDetail:
113
+ """
114
+ Get detailed information about a conversation.
115
+
116
+ Args:
117
+ conversation_id: ID of the conversation
118
+
119
+ Returns:
120
+ ConversationDetail object
121
+ """
122
+ try:
123
+ app_instance = request.app.state.app_instance
124
+ database = app_instance.database
125
+
126
+ # Get conversation
127
+ conv = database.get_conversation(conversation_id)
128
+ if not conv:
129
+ raise HTTPException(status_code=404, detail="Conversation not found")
130
+
131
+ # Get attached files
132
+ files = database.get_conversation_files(conversation_id)
133
+ file_names = [f['filename'] for f in files]
134
+
135
+ return ConversationDetail(
136
+ id=conv['id'],
137
+ name=conv['name'],
138
+ model_id=conv['model_id'],
139
+ instructions=conv.get('instructions'),
140
+ created_at=parse_datetime(conv['created_at']),
141
+ message_count=conv.get('message_count', 0),
142
+ tokens_sent=conv.get('tokens_sent', 0),
143
+ tokens_received=conv.get('tokens_received', 0),
144
+ total_tokens=conv.get('tokens_sent', 0) + conv.get('tokens_received', 0),
145
+ attached_files=file_names,
146
+ )
147
+
148
+ except HTTPException:
149
+ raise
150
+ except Exception as e:
151
+ logger.error(f"Error getting conversation {conversation_id}: {e}")
152
+ raise HTTPException(status_code=500, detail=str(e))
153
+
154
+
155
+ @router.post("/conversations")
156
+ async def create_conversation(
157
+ request: Request,
158
+ name: str = Form(...),
159
+ model_id: str = Form(...),
160
+ instructions: Optional[str] = Form(None),
161
+ files: Optional[List[UploadFile]] = File(None),
162
+ session_id: str = Depends(get_current_session),
163
+ ) -> ConversationDetail:
164
+ """
165
+ Create a new conversation.
166
+
167
+ Args:
168
+ name: Conversation name
169
+ model_id: Model ID to use
170
+ instructions: Optional system instructions
171
+ files: Optional file attachments
172
+ session_id: Validated session ID
173
+
174
+ Returns:
175
+ ConversationDetail for the created conversation
176
+ """
177
+ try:
178
+ app_instance = request.app.state.app_instance
179
+ database = app_instance.database
180
+ conversation_manager = app_instance.conversation_manager
181
+
182
+ # Create conversation in database
183
+ conversation_id = database.create_conversation(
184
+ name=name,
185
+ model_id=model_id,
186
+ instructions=instructions,
187
+ )
188
+
189
+ # Load the conversation to set it as current
190
+ conversation_manager.load_conversation(conversation_id)
191
+
192
+ # Set the model from the conversation and update service references
193
+ app_instance.llm_manager.set_model(model_id)
194
+ app_instance.bedrock_service = app_instance.llm_manager.get_active_service()
195
+ conversation_manager.update_service(app_instance.bedrock_service)
196
+
197
+ # Handle file uploads if provided
198
+ attached_file_paths = []
199
+ upload_errors = []
200
+ if files:
201
+ temp_files = []
202
+ try:
203
+ # Save uploaded files to temporary locations
204
+ for upload_file in files:
205
+ # Validate filename and extension
206
+ if not upload_file.filename:
207
+ upload_errors.append("File uploaded without a filename")
208
+ logger.warning("Received file upload without filename")
209
+ continue
210
+
211
+ # Extract file extension
212
+ suffix = os.path.splitext(upload_file.filename)[1]
213
+ if not suffix:
214
+ upload_errors.append(f"File '{upload_file.filename}' has no file extension")
215
+ logger.warning(f"File '{upload_file.filename}' uploaded without extension")
216
+ continue
217
+
218
+ # Check if extension is supported (using FileManager's validation)
219
+ from dtSpark.files import FileManager
220
+ if suffix.lower() not in (FileManager.SUPPORTED_TEXT_FILES |
221
+ FileManager.SUPPORTED_CODE_FILES |
222
+ FileManager.SUPPORTED_DOCUMENT_FILES |
223
+ FileManager.SUPPORTED_IMAGE_FILES):
224
+ upload_errors.append(f"File type '{suffix}' is not supported for '{upload_file.filename}'")
225
+ logger.warning(f"Unsupported file type '{suffix}' for file '{upload_file.filename}'")
226
+ continue
227
+
228
+ # Create temporary file with proper extension
229
+ temp_fd, temp_path = tempfile.mkstemp(suffix=suffix)
230
+
231
+ # Write uploaded content to temp file
232
+ with os.fdopen(temp_fd, 'wb') as f:
233
+ content = await upload_file.read()
234
+ f.write(content)
235
+
236
+ temp_files.append(temp_path)
237
+ logger.info(f"Saved uploaded file {upload_file.filename} to {temp_path}")
238
+
239
+ # Attach files using conversation manager
240
+ if temp_files:
241
+ success = conversation_manager.attach_files(temp_files)
242
+ if success:
243
+ # Get attached files from database
244
+ db_files = database.get_conversation_files(conversation_id)
245
+ attached_file_paths = [f['file_path'] for f in db_files]
246
+ logger.info(f"Attached {len(attached_file_paths)} file(s) to conversation {conversation_id}")
247
+ else:
248
+ logger.warning("Some files failed to attach")
249
+
250
+ except Exception as e:
251
+ logger.error(f"Error processing file uploads: {e}")
252
+ # Continue anyway - files are optional
253
+
254
+ finally:
255
+ # Clean up temporary files
256
+ for temp_path in temp_files:
257
+ try:
258
+ if os.path.exists(temp_path):
259
+ os.unlink(temp_path)
260
+ except:
261
+ pass
262
+
263
+ # Get the created conversation details
264
+ conv = database.get_conversation(conversation_id)
265
+
266
+ return ConversationDetail(
267
+ id=conv['id'],
268
+ name=conv['name'],
269
+ model_id=conv['model_id'],
270
+ instructions=conv.get('instructions'),
271
+ created_at=parse_datetime(conv['created_at']),
272
+ message_count=0,
273
+ tokens_sent=0,
274
+ tokens_received=0,
275
+ total_tokens=0,
276
+ attached_files=attached_file_paths,
277
+ warnings=upload_errors if upload_errors else None,
278
+ )
279
+
280
+ except Exception as e:
281
+ logger.error(f"Error creating conversation: {e}")
282
+ raise HTTPException(status_code=500, detail=str(e))
283
+
284
+
285
+ @router.delete("/conversations/{conversation_id}")
286
+ async def delete_conversation(
287
+ conversation_id: int,
288
+ request: Request,
289
+ session_id: str = Depends(get_current_session),
290
+ ) -> dict:
291
+ """
292
+ Delete a conversation.
293
+
294
+ Args:
295
+ conversation_id: ID of the conversation to delete
296
+
297
+ Returns:
298
+ Status message
299
+ """
300
+ try:
301
+ app_instance = request.app.state.app_instance
302
+ database = app_instance.database
303
+
304
+ # Check if conversation exists
305
+ conv = database.get_conversation(conversation_id)
306
+ if not conv:
307
+ raise HTTPException(status_code=404, detail="Conversation not found")
308
+
309
+ # Delete conversation
310
+ database.delete_conversation(conversation_id)
311
+
312
+ return {
313
+ "status": "success",
314
+ "message": f"Conversation '{conv['name']}' deleted successfully",
315
+ }
316
+
317
+ except HTTPException:
318
+ raise
319
+ except Exception as e:
320
+ logger.error(f"Error deleting conversation {conversation_id}: {e}")
321
+ raise HTTPException(status_code=500, detail=str(e))
322
+
323
+
324
+ @router.get("/models")
325
+ async def list_models(
326
+ request: Request,
327
+ session_id: str = Depends(get_current_session),
328
+ ) -> List[dict]:
329
+ """
330
+ Get available models.
331
+
332
+ Returns:
333
+ List of available models with their IDs and names
334
+ """
335
+ try:
336
+ app_instance = request.app.state.app_instance
337
+
338
+ # Get available models from LLM manager
339
+ models = app_instance.llm_manager.list_all_models()
340
+
341
+ return [
342
+ {
343
+ "id": model.get('id', model.get('name', 'unknown')),
344
+ "name": model.get('name', 'Unknown'),
345
+ "provider": model.get('provider', 'Unknown'),
346
+ "model_maker": model.get('model_maker'), # Optional: model creator (for Bedrock models)
347
+ }
348
+ for model in models
349
+ ]
350
+
351
+ except Exception as e:
352
+ logger.error(f"Error listing models: {e}")
353
+ raise HTTPException(status_code=500, detail=str(e))