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,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))
|