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,520 @@
1
+ """
2
+ Conversation CRUD operations module.
3
+
4
+ This module handles:
5
+ - Creating new conversations
6
+ - Retrieving conversation records
7
+ - Updating conversation settings
8
+ - Deleting conversations
9
+ """
10
+
11
+ import sqlite3
12
+ import logging
13
+ from datetime import datetime
14
+ from typing import List, Dict, Optional
15
+
16
+
17
+ def create_conversation(conn: sqlite3.Connection, name: str, model_id: str,
18
+ instructions: Optional[str] = None, user_guid: str = None,
19
+ compaction_threshold: Optional[float] = None) -> int:
20
+ """
21
+ Create a new conversation.
22
+
23
+ Args:
24
+ conn: Database connection
25
+ name: Name of the conversation
26
+ model_id: ID of the Bedrock model being used
27
+ instructions: Optional instructions/system prompt for the conversation
28
+ user_guid: User GUID for multi-user support
29
+ compaction_threshold: Optional compaction threshold override (0.0-1.0, NULL uses config default)
30
+
31
+ Returns:
32
+ ID of the newly created conversation
33
+ """
34
+ cursor = conn.cursor()
35
+ now = datetime.now()
36
+
37
+ cursor.execute('''
38
+ INSERT INTO conversations (name, model_id, created_at, last_updated, instructions, user_guid, compaction_threshold)
39
+ VALUES (?, ?, ?, ?, ?, ?, ?)
40
+ ''', (name, model_id, now, now, instructions, user_guid, compaction_threshold))
41
+
42
+ conn.commit()
43
+ conversation_id = cursor.lastrowid
44
+ logging.info(f"Created conversation '{name}' with ID {conversation_id} for user {user_guid} (compaction_threshold: {compaction_threshold})")
45
+ return conversation_id
46
+
47
+
48
+ def get_active_conversations(conn: sqlite3.Connection, user_guid: str = None) -> List[Dict]:
49
+ """
50
+ Retrieve all active conversations for a user.
51
+
52
+ Args:
53
+ conn: Database connection
54
+ user_guid: User GUID for multi-user support
55
+
56
+ Returns:
57
+ List of conversation dictionaries
58
+ """
59
+ cursor = conn.cursor()
60
+ cursor.execute('''
61
+ SELECT
62
+ c.id,
63
+ c.name,
64
+ c.model_id,
65
+ c.created_at,
66
+ c.last_updated,
67
+ c.total_tokens,
68
+ c.instructions,
69
+ c.tokens_sent,
70
+ c.tokens_received,
71
+ COUNT(m.id) as message_count,
72
+ MAX(m.timestamp) as last_message_at
73
+ FROM conversations c
74
+ LEFT JOIN messages m ON c.id = m.conversation_id AND m.user_guid = ?
75
+ WHERE c.is_active = 1 AND c.user_guid = ?
76
+ GROUP BY c.id
77
+ ORDER BY c.last_updated DESC
78
+ ''', (user_guid, user_guid))
79
+
80
+ conversations = []
81
+ for row in cursor.fetchall():
82
+ conversations.append({
83
+ 'id': row['id'],
84
+ 'name': row['name'],
85
+ 'model_id': row['model_id'],
86
+ 'created_at': row['created_at'],
87
+ 'last_updated': row['last_updated'],
88
+ 'total_tokens': row['total_tokens'],
89
+ 'instructions': row['instructions'],
90
+ 'tokens_sent': row['tokens_sent'] or 0,
91
+ 'tokens_received': row['tokens_received'] or 0,
92
+ 'message_count': row['message_count'],
93
+ 'last_message_at': row['last_message_at']
94
+ })
95
+
96
+ return conversations
97
+
98
+
99
+ def get_conversation(conn: sqlite3.Connection, conversation_id: int, user_guid: str = None) -> Optional[Dict]:
100
+ """
101
+ Retrieve a specific conversation for a user.
102
+
103
+ Args:
104
+ conn: Database connection
105
+ conversation_id: ID of the conversation
106
+ user_guid: User GUID for multi-user support
107
+
108
+ Returns:
109
+ Conversation dictionary or None if not found
110
+ """
111
+ cursor = conn.cursor()
112
+ cursor.execute('''
113
+ SELECT id, name, model_id, created_at, last_updated, total_tokens, instructions,
114
+ tokens_sent, tokens_received, max_tokens, compaction_threshold
115
+ FROM conversations
116
+ WHERE id = ? AND user_guid = ?
117
+ ''', (conversation_id, user_guid))
118
+
119
+ row = cursor.fetchone()
120
+ if row:
121
+ return {
122
+ 'id': row['id'],
123
+ 'name': row['name'],
124
+ 'model_id': row['model_id'],
125
+ 'created_at': row['created_at'],
126
+ 'last_updated': row['last_updated'],
127
+ 'total_tokens': row['total_tokens'],
128
+ 'instructions': row['instructions'],
129
+ 'tokens_sent': row['tokens_sent'] or 0,
130
+ 'tokens_received': row['tokens_received'] or 0,
131
+ 'max_tokens': row['max_tokens'], # NULL means use global default
132
+ 'compaction_threshold': row['compaction_threshold'] # NULL means use global default
133
+ }
134
+ return None
135
+
136
+
137
+ def get_conversation_token_count(conn: sqlite3.Connection, conversation_id: int, user_guid: str = None) -> int:
138
+ """
139
+ Get the total token count for a conversation.
140
+
141
+ Args:
142
+ conn: Database connection
143
+ conversation_id: ID of the conversation
144
+ user_guid: User GUID for multi-user support
145
+
146
+ Returns:
147
+ Total token count
148
+ """
149
+ cursor = conn.cursor()
150
+ cursor.execute('''
151
+ SELECT total_tokens FROM conversations WHERE id = ? AND user_guid = ?
152
+ ''', (conversation_id, user_guid))
153
+
154
+ row = cursor.fetchone()
155
+ return row['total_tokens'] if row else 0
156
+
157
+
158
+ def recalculate_total_tokens(conn: sqlite3.Connection, conversation_id: int,
159
+ user_guid: str = None) -> int:
160
+ """
161
+ Recalculate and update total_tokens from active (non-rolled-up) messages.
162
+
163
+ This function recalculates total_tokens by summing token_count from all
164
+ messages that are NOT marked as rolled up. This ensures accuracy after
165
+ compaction operations.
166
+
167
+ Args:
168
+ conn: Database connection
169
+ conversation_id: ID of the conversation
170
+ user_guid: User GUID for multi-user support
171
+
172
+ Returns:
173
+ The new total token count
174
+ """
175
+ cursor = conn.cursor()
176
+
177
+ # Sum tokens from active (non-rolled-up) messages only
178
+ cursor.execute('''
179
+ SELECT COALESCE(SUM(token_count), 0) as active_tokens
180
+ FROM messages
181
+ WHERE conversation_id = ? AND user_guid = ? AND is_rolled_up = 0
182
+ ''', (conversation_id, user_guid))
183
+
184
+ row = cursor.fetchone()
185
+ new_total = row['active_tokens'] if row else 0
186
+
187
+ # Update the conversation's total_tokens
188
+ cursor.execute('''
189
+ UPDATE conversations
190
+ SET total_tokens = ?
191
+ WHERE id = ? AND user_guid = ?
192
+ ''', (new_total, conversation_id, user_guid))
193
+
194
+ conn.commit()
195
+ logging.info(f"Recalculated total_tokens for conversation {conversation_id}: {new_total}")
196
+ return new_total
197
+
198
+
199
+ def delete_conversation(conn: sqlite3.Connection, conversation_id: int, user_guid: str = None) -> bool:
200
+ """
201
+ Delete a conversation and all its messages for a user.
202
+
203
+ Args:
204
+ conn: Database connection
205
+ conversation_id: ID of the conversation to delete
206
+ user_guid: User GUID for multi-user support
207
+
208
+ Returns:
209
+ True if successful, False otherwise
210
+ """
211
+ try:
212
+ cursor = conn.cursor()
213
+
214
+ # Delete all messages for this conversation (filtered by user_guid for safety)
215
+ cursor.execute('''
216
+ DELETE FROM messages
217
+ WHERE conversation_id = ? AND user_guid = ?
218
+ ''', (conversation_id, user_guid))
219
+
220
+ # Delete all rollup history for this conversation
221
+ cursor.execute('''
222
+ DELETE FROM rollup_history
223
+ WHERE conversation_id = ? AND user_guid = ?
224
+ ''', (conversation_id, user_guid))
225
+
226
+ # Delete the conversation (filtered by user_guid for security)
227
+ cursor.execute('''
228
+ DELETE FROM conversations
229
+ WHERE id = ? AND user_guid = ?
230
+ ''', (conversation_id, user_guid))
231
+
232
+ conn.commit()
233
+ logging.info(f"Deleted conversation {conversation_id} for user {user_guid}")
234
+ return True
235
+
236
+ except Exception as e:
237
+ logging.error(f"Failed to delete conversation {conversation_id}: {e}")
238
+ conn.rollback()
239
+ return False
240
+
241
+
242
+ def update_conversation_max_tokens(conn: sqlite3.Connection, conversation_id: int,
243
+ max_tokens: int, user_guid: str = None):
244
+ """
245
+ Update the max_tokens setting for a specific conversation.
246
+
247
+ Args:
248
+ conn: Database connection
249
+ conversation_id: ID of the conversation
250
+ max_tokens: Maximum tokens for this conversation (overrides global default)
251
+ user_guid: User GUID for multi-user support
252
+ """
253
+ try:
254
+ cursor = conn.cursor()
255
+ cursor.execute('''
256
+ UPDATE conversations
257
+ SET max_tokens = ?
258
+ WHERE id = ? AND user_guid = ?
259
+ ''', (max_tokens, conversation_id, user_guid))
260
+ conn.commit()
261
+ logging.info(f"Updated max_tokens for conversation {conversation_id} to {max_tokens}")
262
+ except Exception as e:
263
+ logging.error(f"Failed to update max_tokens: {e}")
264
+ conn.rollback()
265
+
266
+
267
+ def update_conversation_compaction_threshold(conn: sqlite3.Connection, conversation_id: int,
268
+ compaction_threshold: float, user_guid: str = None):
269
+ """
270
+ Update the compaction_threshold setting for a specific conversation.
271
+
272
+ Args:
273
+ conn: Database connection
274
+ conversation_id: ID of the conversation
275
+ compaction_threshold: Compaction threshold (0.0-1.0) for this conversation (overrides global default)
276
+ user_guid: User GUID for multi-user support
277
+ """
278
+ try:
279
+ cursor = conn.cursor()
280
+ cursor.execute('''
281
+ UPDATE conversations
282
+ SET compaction_threshold = ?
283
+ WHERE id = ? AND user_guid = ?
284
+ ''', (compaction_threshold, conversation_id, user_guid))
285
+ conn.commit()
286
+ logging.info(f"Updated compaction_threshold for conversation {conversation_id} to {compaction_threshold}")
287
+ except Exception as e:
288
+ logging.error(f"Failed to update compaction_threshold: {e}")
289
+ conn.rollback()
290
+
291
+
292
+ def update_conversation_instructions(conn: sqlite3.Connection, conversation_id: int,
293
+ instructions: Optional[str], user_guid: str = None):
294
+ """
295
+ Update the instructions for a specific conversation.
296
+
297
+ Args:
298
+ conn: Database connection
299
+ conversation_id: ID of the conversation
300
+ instructions: New instructions/system prompt (None to clear)
301
+ user_guid: User GUID for multi-user support
302
+ """
303
+ try:
304
+ cursor = conn.cursor()
305
+ cursor.execute('''
306
+ UPDATE conversations
307
+ SET instructions = ?
308
+ WHERE id = ? AND user_guid = ?
309
+ ''', (instructions, conversation_id, user_guid))
310
+ conn.commit()
311
+ if instructions:
312
+ logging.info(f"Updated instructions for conversation {conversation_id}")
313
+ else:
314
+ logging.info(f"Cleared instructions for conversation {conversation_id}")
315
+ except Exception as e:
316
+ logging.error(f"Failed to update instructions: {e}")
317
+ conn.rollback()
318
+
319
+
320
+ def update_token_usage(conn: sqlite3.Connection, conversation_id: int,
321
+ tokens_sent: int, tokens_received: int, model_id: str = None,
322
+ user_guid: str = None):
323
+ """
324
+ Update the API token usage for a conversation and track per-model usage.
325
+
326
+ Args:
327
+ conn: Database connection
328
+ conversation_id: ID of the conversation
329
+ tokens_sent: Number of tokens sent to the API (input tokens)
330
+ tokens_received: Number of tokens received from the API (output tokens)
331
+ model_id: Model used for this request (for per-model tracking)
332
+ user_guid: User GUID for multi-user support
333
+ """
334
+ try:
335
+ cursor = conn.cursor()
336
+
337
+ # Update overall conversation token counts
338
+ cursor.execute('''
339
+ UPDATE conversations
340
+ SET tokens_sent = tokens_sent + ?,
341
+ tokens_received = tokens_received + ?
342
+ WHERE id = ? AND user_guid = ?
343
+ ''', (tokens_sent, tokens_received, conversation_id, user_guid))
344
+
345
+ # Update per-model token usage if model_id provided
346
+ if model_id:
347
+ now = datetime.now().isoformat()
348
+
349
+ # Try to update existing record
350
+ cursor.execute('''
351
+ INSERT INTO conversation_model_usage
352
+ (conversation_id, model_id, input_tokens, output_tokens, first_used, last_used, user_guid)
353
+ VALUES (?, ?, ?, ?, ?, ?, ?)
354
+ ON CONFLICT(conversation_id, model_id) DO UPDATE SET
355
+ input_tokens = input_tokens + excluded.input_tokens,
356
+ output_tokens = output_tokens + excluded.output_tokens,
357
+ last_used = excluded.last_used
358
+ ''', (conversation_id, model_id, tokens_sent, tokens_received, now, now, user_guid))
359
+
360
+ conn.commit()
361
+ logging.debug(f"Updated token usage for conversation {conversation_id}: +{tokens_sent} sent, +{tokens_received} received (model: {model_id or 'unknown'})")
362
+
363
+ except Exception as e:
364
+ logging.error(f"Failed to update token usage: {e}")
365
+ conn.rollback()
366
+
367
+
368
+ def get_model_usage_breakdown(conn: sqlite3.Connection, conversation_id: int,
369
+ user_guid: str = None) -> List[Dict]:
370
+ """
371
+ Get per-model token usage breakdown for a conversation.
372
+
373
+ Args:
374
+ conn: Database connection
375
+ conversation_id: ID of the conversation
376
+ user_guid: User GUID for multi-user support
377
+
378
+ Returns:
379
+ List of dictionaries with model usage details
380
+ """
381
+ cursor = conn.cursor()
382
+ cursor.execute('''
383
+ SELECT model_id, input_tokens, output_tokens, first_used, last_used
384
+ FROM conversation_model_usage
385
+ WHERE conversation_id = ? AND user_guid = ?
386
+ ORDER BY first_used ASC
387
+ ''', (conversation_id, user_guid))
388
+
389
+ results = []
390
+ for row in cursor.fetchall():
391
+ results.append({
392
+ 'model_id': row['model_id'],
393
+ 'input_tokens': row['input_tokens'],
394
+ 'output_tokens': row['output_tokens'],
395
+ 'total_tokens': row['input_tokens'] + row['output_tokens'],
396
+ 'first_used': row['first_used'],
397
+ 'last_used': row['last_used']
398
+ })
399
+
400
+ return results
401
+
402
+
403
+ def get_predefined_conversation_by_name(conn: sqlite3.Connection, name: str,
404
+ user_guid: str = None) -> Optional[Dict]:
405
+ """
406
+ Retrieve a predefined conversation by name for a user.
407
+
408
+ Args:
409
+ conn: Database connection
410
+ name: Name of the predefined conversation
411
+ user_guid: User GUID for multi-user support
412
+
413
+ Returns:
414
+ Conversation dictionary or None if not found
415
+ """
416
+ cursor = conn.cursor()
417
+ cursor.execute('''
418
+ SELECT id, name, model_id, created_at, last_updated, instructions, config_hash, is_predefined
419
+ FROM conversations
420
+ WHERE name = ? AND is_predefined = 1 AND is_active = 1 AND user_guid = ?
421
+ ''', (name, user_guid))
422
+
423
+ row = cursor.fetchone()
424
+ if row:
425
+ return {
426
+ 'id': row['id'],
427
+ 'name': row['name'],
428
+ 'model_id': row['model_id'],
429
+ 'created_at': row['created_at'],
430
+ 'last_updated': row['last_updated'],
431
+ 'instructions': row['instructions'],
432
+ 'config_hash': row['config_hash'],
433
+ 'is_predefined': row['is_predefined']
434
+ }
435
+ return None
436
+
437
+
438
+ def create_predefined_conversation(conn: sqlite3.Connection, name: str, model_id: str,
439
+ instructions: Optional[str], config_hash: str,
440
+ user_guid: str = None) -> int:
441
+ """
442
+ Create a new predefined conversation.
443
+
444
+ Args:
445
+ conn: Database connection
446
+ name: Name of the conversation
447
+ model_id: ID of the model being used
448
+ instructions: Instructions/system prompt for the conversation
449
+ config_hash: Hash of the configuration to detect changes
450
+ user_guid: User GUID for multi-user support
451
+
452
+ Returns:
453
+ ID of the newly created conversation
454
+ """
455
+ cursor = conn.cursor()
456
+ now = datetime.now()
457
+
458
+ cursor.execute('''
459
+ INSERT INTO conversations (name, model_id, created_at, last_updated, instructions,
460
+ is_predefined, config_hash, user_guid)
461
+ VALUES (?, ?, ?, ?, ?, 1, ?, ?)
462
+ ''', (name, model_id, now, now, instructions, config_hash, user_guid))
463
+
464
+ conn.commit()
465
+ conversation_id = cursor.lastrowid
466
+ logging.info(f"Created predefined conversation '{name}' with ID {conversation_id} for user {user_guid}")
467
+ return conversation_id
468
+
469
+
470
+ def update_predefined_conversation(conn: sqlite3.Connection, conversation_id: int,
471
+ model_id: str, instructions: Optional[str],
472
+ config_hash: str, user_guid: str = None):
473
+ """
474
+ Update a predefined conversation's settings.
475
+
476
+ Args:
477
+ conn: Database connection
478
+ conversation_id: ID of the conversation
479
+ model_id: New model ID
480
+ instructions: New instructions/system prompt
481
+ config_hash: New config hash
482
+ user_guid: User GUID for multi-user support
483
+ """
484
+ try:
485
+ cursor = conn.cursor()
486
+ now = datetime.now()
487
+
488
+ cursor.execute('''
489
+ UPDATE conversations
490
+ SET model_id = ?, instructions = ?, config_hash = ?, last_updated = ?
491
+ WHERE id = ? AND is_predefined = 1 AND user_guid = ?
492
+ ''', (model_id, instructions, config_hash, now, conversation_id, user_guid))
493
+
494
+ conn.commit()
495
+ logging.info(f"Updated predefined conversation {conversation_id}")
496
+ except Exception as e:
497
+ logging.error(f"Failed to update predefined conversation: {e}")
498
+ conn.rollback()
499
+
500
+
501
+ def is_conversation_predefined(conn: sqlite3.Connection, conversation_id: int,
502
+ user_guid: str = None) -> bool:
503
+ """
504
+ Check if a conversation is predefined.
505
+
506
+ Args:
507
+ conn: Database connection
508
+ conversation_id: ID of the conversation
509
+ user_guid: User GUID for multi-user support
510
+
511
+ Returns:
512
+ True if conversation is predefined, False otherwise
513
+ """
514
+ cursor = conn.cursor()
515
+ cursor.execute('''
516
+ SELECT is_predefined FROM conversations WHERE id = ? AND user_guid = ?
517
+ ''', (conversation_id, user_guid))
518
+
519
+ row = cursor.fetchone()
520
+ return bool(row['is_predefined']) if row else False