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,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database credential prompting utility.
|
|
3
|
+
|
|
4
|
+
Provides interactive prompts for missing database credentials.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import getpass
|
|
11
|
+
from typing import Optional
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.prompt import Prompt, Confirm
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
|
|
16
|
+
from .backends import DatabaseCredentials
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def prompt_for_credentials(db_type: str, existing_credentials: Optional[DatabaseCredentials] = None) -> DatabaseCredentials:
|
|
20
|
+
"""
|
|
21
|
+
Interactively prompt user for missing database credentials.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
db_type: Database type (sqlite, mysql, mariadb, postgresql, mssql)
|
|
25
|
+
existing_credentials: Existing credentials (will only prompt for missing fields)
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Complete DatabaseCredentials object
|
|
29
|
+
"""
|
|
30
|
+
console = Console()
|
|
31
|
+
db_type_lower = db_type.lower()
|
|
32
|
+
|
|
33
|
+
# Start with existing credentials or create new
|
|
34
|
+
if existing_credentials:
|
|
35
|
+
creds = existing_credentials
|
|
36
|
+
else:
|
|
37
|
+
creds = DatabaseCredentials()
|
|
38
|
+
|
|
39
|
+
# SQLite only needs path
|
|
40
|
+
if db_type_lower == 'sqlite':
|
|
41
|
+
if not creds.path:
|
|
42
|
+
creds.path = Prompt.ask(
|
|
43
|
+
"Enter SQLite database file path",
|
|
44
|
+
default="./running/conversations.db"
|
|
45
|
+
)
|
|
46
|
+
return creds
|
|
47
|
+
|
|
48
|
+
# Display information panel
|
|
49
|
+
console.print()
|
|
50
|
+
console.print(Panel(
|
|
51
|
+
f"[bold cyan]Database Connection Setup[/bold cyan]\n\n"
|
|
52
|
+
f"Database type: [yellow]{db_type}[/yellow]\n\n"
|
|
53
|
+
f"Please provide connection credentials for your database server.",
|
|
54
|
+
border_style="cyan",
|
|
55
|
+
padding=(1, 2)
|
|
56
|
+
))
|
|
57
|
+
console.print()
|
|
58
|
+
|
|
59
|
+
# Prompt for remote database credentials
|
|
60
|
+
if not creds.host:
|
|
61
|
+
creds.host = Prompt.ask(
|
|
62
|
+
"Database host",
|
|
63
|
+
default="localhost"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if not creds.port:
|
|
67
|
+
default_ports = {
|
|
68
|
+
'mysql': 3306,
|
|
69
|
+
'mariadb': 3306,
|
|
70
|
+
'postgresql': 5432,
|
|
71
|
+
'mssql': 1433,
|
|
72
|
+
'sqlserver': 1433
|
|
73
|
+
}
|
|
74
|
+
default_port = default_ports.get(db_type_lower, 3306)
|
|
75
|
+
|
|
76
|
+
port_input = Prompt.ask(
|
|
77
|
+
"Database port",
|
|
78
|
+
default=str(default_port)
|
|
79
|
+
)
|
|
80
|
+
creds.port = int(port_input)
|
|
81
|
+
|
|
82
|
+
if not creds.database:
|
|
83
|
+
creds.database = Prompt.ask(
|
|
84
|
+
"Database name",
|
|
85
|
+
default="dtawsbedrockcli"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if not creds.username:
|
|
89
|
+
creds.username = Prompt.ask("Database username")
|
|
90
|
+
|
|
91
|
+
if not creds.password:
|
|
92
|
+
# Use getpass for secure password input
|
|
93
|
+
console.print("[cyan]Database password:[/cyan] ", end="")
|
|
94
|
+
creds.password = getpass.getpass("")
|
|
95
|
+
|
|
96
|
+
# SSL option
|
|
97
|
+
use_ssl = Confirm.ask("Use SSL/TLS connection?", default=False)
|
|
98
|
+
creds.ssl = use_ssl
|
|
99
|
+
|
|
100
|
+
# MSSQL-specific: driver selection
|
|
101
|
+
if db_type_lower in ('mssql', 'sqlserver', 'mssqlserver'):
|
|
102
|
+
if not creds.driver:
|
|
103
|
+
drivers = [
|
|
104
|
+
"ODBC Driver 17 for SQL Server",
|
|
105
|
+
"ODBC Driver 18 for SQL Server",
|
|
106
|
+
"SQL Server Native Client 11.0",
|
|
107
|
+
"Custom"
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
console.print()
|
|
111
|
+
console.print("[bold cyan]Select ODBC driver:[/bold cyan]")
|
|
112
|
+
for i, driver in enumerate(drivers, 1):
|
|
113
|
+
console.print(f" [{i}] {driver}")
|
|
114
|
+
|
|
115
|
+
choice = Prompt.ask(
|
|
116
|
+
"Driver selection",
|
|
117
|
+
choices=[str(i) for i in range(1, len(drivers) + 1)],
|
|
118
|
+
default="1"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
choice_idx = int(choice) - 1
|
|
122
|
+
if choice_idx == len(drivers) - 1: # Custom
|
|
123
|
+
creds.driver = Prompt.ask("Enter custom ODBC driver name")
|
|
124
|
+
else:
|
|
125
|
+
creds.driver = drivers[choice_idx]
|
|
126
|
+
|
|
127
|
+
console.print()
|
|
128
|
+
logging.info(f"Database credentials collected for {db_type}")
|
|
129
|
+
|
|
130
|
+
return creds
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_credentials(db_type: str, credentials: DatabaseCredentials) -> tuple[bool, Optional[str]]:
|
|
134
|
+
"""
|
|
135
|
+
Test database credentials by attempting connection.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
db_type: Database type
|
|
139
|
+
credentials: Database credentials
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Tuple of (success, error_message)
|
|
143
|
+
"""
|
|
144
|
+
from .backends import create_backend
|
|
145
|
+
|
|
146
|
+
console = Console()
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
console.print()
|
|
150
|
+
console.print("[cyan]Testing database connection...[/cyan]")
|
|
151
|
+
|
|
152
|
+
backend = create_backend(db_type, credentials)
|
|
153
|
+
success = backend.test_connection()
|
|
154
|
+
backend.close()
|
|
155
|
+
|
|
156
|
+
if success:
|
|
157
|
+
console.print("[green]✓ Database connection successful![/green]")
|
|
158
|
+
return True, None
|
|
159
|
+
else:
|
|
160
|
+
error_msg = "Connection test query failed"
|
|
161
|
+
console.print(f"[red]✗ {error_msg}[/red]")
|
|
162
|
+
return False, error_msg
|
|
163
|
+
|
|
164
|
+
except ImportError as e:
|
|
165
|
+
error_msg = str(e)
|
|
166
|
+
console.print(f"[red]✗ {error_msg}[/red]")
|
|
167
|
+
return False, error_msg
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
error_msg = f"Connection failed: {str(e)}"
|
|
171
|
+
console.print(f"[red]✗ {error_msg}[/red]")
|
|
172
|
+
logging.error(f"Database connection test failed: {e}", exc_info=True)
|
|
173
|
+
return False, error_msg
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def prompt_and_validate_credentials(db_type: str,
|
|
177
|
+
existing_credentials: Optional[DatabaseCredentials] = None,
|
|
178
|
+
max_retries: int = 3) -> Optional[DatabaseCredentials]:
|
|
179
|
+
"""
|
|
180
|
+
Prompt for credentials and validate them, with retry support.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
db_type: Database type
|
|
184
|
+
existing_credentials: Existing credentials (will only prompt for missing fields)
|
|
185
|
+
max_retries: Maximum number of retry attempts
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Valid DatabaseCredentials or None if max retries exceeded
|
|
189
|
+
"""
|
|
190
|
+
console = Console()
|
|
191
|
+
creds = existing_credentials
|
|
192
|
+
|
|
193
|
+
for attempt in range(max_retries):
|
|
194
|
+
# Prompt for credentials
|
|
195
|
+
creds = prompt_for_credentials(db_type, creds)
|
|
196
|
+
|
|
197
|
+
# Test credentials
|
|
198
|
+
success, error_msg = test_credentials(db_type, creds)
|
|
199
|
+
|
|
200
|
+
if success:
|
|
201
|
+
return creds
|
|
202
|
+
|
|
203
|
+
# Failed - offer retry
|
|
204
|
+
if attempt < max_retries - 1:
|
|
205
|
+
console.print()
|
|
206
|
+
retry = Confirm.ask(
|
|
207
|
+
f"[yellow]Connection failed. Retry with different credentials? ({max_retries - attempt - 1} attempts remaining)[/yellow]",
|
|
208
|
+
default=True
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if not retry:
|
|
212
|
+
return None
|
|
213
|
+
else:
|
|
214
|
+
console.print()
|
|
215
|
+
console.print(f"[red]Maximum connection attempts ({max_retries}) exceeded.[/red]")
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
return None
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File attachment operations module.
|
|
3
|
+
|
|
4
|
+
This module handles:
|
|
5
|
+
- Adding files to conversations
|
|
6
|
+
- Retrieving attached files
|
|
7
|
+
- File deletion
|
|
8
|
+
- Tag-based file filtering
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sqlite3
|
|
12
|
+
import logging
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from typing import List, Dict, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def add_file(conn: sqlite3.Connection, conversation_id: int, filename: str,
|
|
18
|
+
file_type: str, file_size: int, content_text: Optional[str] = None,
|
|
19
|
+
content_base64: Optional[str] = None, mime_type: Optional[str] = None,
|
|
20
|
+
token_count: int = 0, tags: Optional[str] = None, user_guid: str = None) -> int:
|
|
21
|
+
"""
|
|
22
|
+
Add a file to a conversation.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
conn: Database connection
|
|
26
|
+
conversation_id: ID of the conversation
|
|
27
|
+
filename: Original filename
|
|
28
|
+
file_type: File extension (e.g., .pdf, .docx)
|
|
29
|
+
file_size: Size in bytes
|
|
30
|
+
content_text: Extracted text content
|
|
31
|
+
content_base64: Base64 encoded content (for images)
|
|
32
|
+
mime_type: MIME type (for images)
|
|
33
|
+
token_count: Token count of extracted content
|
|
34
|
+
tags: Comma-separated tags for the file
|
|
35
|
+
user_guid: User GUID for multi-user support
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
ID of the newly added file
|
|
39
|
+
"""
|
|
40
|
+
cursor = conn.cursor()
|
|
41
|
+
now = datetime.now()
|
|
42
|
+
|
|
43
|
+
cursor.execute('''
|
|
44
|
+
INSERT INTO conversation_files
|
|
45
|
+
(conversation_id, filename, file_type, file_size, content_text,
|
|
46
|
+
content_base64, mime_type, token_count, added_at, tags, user_guid)
|
|
47
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
48
|
+
''', (conversation_id, filename, file_type, file_size, content_text,
|
|
49
|
+
content_base64, mime_type, token_count, now, tags, user_guid))
|
|
50
|
+
|
|
51
|
+
conn.commit()
|
|
52
|
+
file_id = cursor.lastrowid
|
|
53
|
+
tags_str = f" with tags '{tags}'" if tags else ""
|
|
54
|
+
logging.info(f"Added file '{filename}' to conversation {conversation_id}{tags_str}")
|
|
55
|
+
return file_id
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_conversation_files(conn: sqlite3.Connection, conversation_id: int,
|
|
59
|
+
user_guid: str = None) -> List[Dict]:
|
|
60
|
+
"""
|
|
61
|
+
Retrieve all files for a conversation.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
conn: Database connection
|
|
65
|
+
conversation_id: ID of the conversation
|
|
66
|
+
user_guid: User GUID for multi-user support
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
List of file dictionaries
|
|
70
|
+
"""
|
|
71
|
+
cursor = conn.cursor()
|
|
72
|
+
cursor.execute('''
|
|
73
|
+
SELECT id, filename, file_type, file_size, content_text,
|
|
74
|
+
content_base64, mime_type, token_count, added_at, tags
|
|
75
|
+
FROM conversation_files
|
|
76
|
+
WHERE conversation_id = ? AND user_guid = ?
|
|
77
|
+
ORDER BY added_at ASC
|
|
78
|
+
''', (conversation_id, user_guid))
|
|
79
|
+
|
|
80
|
+
files = []
|
|
81
|
+
for row in cursor.fetchall():
|
|
82
|
+
files.append({
|
|
83
|
+
'id': row['id'],
|
|
84
|
+
'filename': row['filename'],
|
|
85
|
+
'file_type': row['file_type'],
|
|
86
|
+
'file_size': row['file_size'],
|
|
87
|
+
'content_text': row['content_text'],
|
|
88
|
+
'content_base64': row['content_base64'],
|
|
89
|
+
'mime_type': row['mime_type'],
|
|
90
|
+
'token_count': row['token_count'],
|
|
91
|
+
'added_at': row['added_at'],
|
|
92
|
+
'tags': row['tags']
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return files
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_files_by_tag(conn: sqlite3.Connection, conversation_id: int, tag: str,
|
|
99
|
+
user_guid: str = None) -> List[Dict]:
|
|
100
|
+
"""
|
|
101
|
+
Retrieve files for a conversation filtered by tag.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
conn: Database connection
|
|
105
|
+
conversation_id: ID of the conversation
|
|
106
|
+
tag: Tag to filter by (case-insensitive)
|
|
107
|
+
user_guid: User GUID for multi-user support
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
List of file dictionaries with matching tag
|
|
111
|
+
"""
|
|
112
|
+
cursor = conn.cursor()
|
|
113
|
+
# Use LIKE to match tag anywhere in the comma-separated tags field
|
|
114
|
+
# We add commas around the search pattern to ensure we match whole tags
|
|
115
|
+
tag_pattern = f"%{tag}%"
|
|
116
|
+
|
|
117
|
+
cursor.execute('''
|
|
118
|
+
SELECT id, filename, file_type, file_size, content_text,
|
|
119
|
+
content_base64, mime_type, token_count, added_at, tags
|
|
120
|
+
FROM conversation_files
|
|
121
|
+
WHERE conversation_id = ?
|
|
122
|
+
AND user_guid = ?
|
|
123
|
+
AND tags LIKE ?
|
|
124
|
+
ORDER BY added_at ASC
|
|
125
|
+
''', (conversation_id, user_guid, tag_pattern))
|
|
126
|
+
|
|
127
|
+
files = []
|
|
128
|
+
for row in cursor.fetchall():
|
|
129
|
+
# Additional filtering to ensure we match whole tags (not partial matches)
|
|
130
|
+
tags_list = [t.strip().lower() for t in (row['tags'] or '').split(',') if t.strip()]
|
|
131
|
+
if tag.lower() in tags_list:
|
|
132
|
+
files.append({
|
|
133
|
+
'id': row['id'],
|
|
134
|
+
'filename': row['filename'],
|
|
135
|
+
'file_type': row['file_type'],
|
|
136
|
+
'file_size': row['file_size'],
|
|
137
|
+
'content_text': row['content_text'],
|
|
138
|
+
'content_base64': row['content_base64'],
|
|
139
|
+
'mime_type': row['mime_type'],
|
|
140
|
+
'token_count': row['token_count'],
|
|
141
|
+
'added_at': row['added_at'],
|
|
142
|
+
'tags': row['tags']
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
return files
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def delete_conversation_files(conn: sqlite3.Connection, conversation_id: int,
|
|
149
|
+
user_guid: str = None):
|
|
150
|
+
"""
|
|
151
|
+
Delete all files for a conversation.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
conn: Database connection
|
|
155
|
+
conversation_id: ID of the conversation
|
|
156
|
+
user_guid: User GUID for multi-user support
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
cursor = conn.cursor()
|
|
160
|
+
cursor.execute('''
|
|
161
|
+
DELETE FROM conversation_files
|
|
162
|
+
WHERE conversation_id = ? AND user_guid = ?
|
|
163
|
+
''', (conversation_id, user_guid))
|
|
164
|
+
conn.commit()
|
|
165
|
+
logging.info(f"Deleted all files for conversation {conversation_id}")
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logging.error(f"Failed to delete files for conversation {conversation_id}: {e}")
|
|
168
|
+
conn.rollback()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def delete_file(conn: sqlite3.Connection, file_id: int, user_guid: str = None) -> bool:
|
|
172
|
+
"""
|
|
173
|
+
Delete a specific file by ID.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
conn: Database connection
|
|
177
|
+
file_id: ID of the file to delete
|
|
178
|
+
user_guid: User GUID for multi-user support (for security filtering)
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
True if successful, False otherwise
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
cursor = conn.cursor()
|
|
185
|
+
# First, get the filename for logging (filtered by user_guid for security)
|
|
186
|
+
cursor.execute('SELECT filename FROM conversation_files WHERE id = ? AND user_guid = ?',
|
|
187
|
+
(file_id, user_guid))
|
|
188
|
+
row = cursor.fetchone()
|
|
189
|
+
|
|
190
|
+
if not row:
|
|
191
|
+
logging.warning(f"File ID {file_id} not found for user {user_guid}")
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
filename = row['filename']
|
|
195
|
+
|
|
196
|
+
# Delete the file (filtered by user_guid for security)
|
|
197
|
+
cursor.execute('DELETE FROM conversation_files WHERE id = ? AND user_guid = ?',
|
|
198
|
+
(file_id, user_guid))
|
|
199
|
+
conn.commit()
|
|
200
|
+
logging.info(f"Deleted file '{filename}' (ID: {file_id})")
|
|
201
|
+
return True
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logging.error(f"Failed to delete file {file_id}: {e}")
|
|
204
|
+
conn.rollback()
|
|
205
|
+
return False
|