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,355 @@
1
+ """
2
+ MCP (Model Context Protocol) operations module.
3
+
4
+ This module handles:
5
+ - Recording MCP tool transactions for Cyber Security monitoring
6
+ - Retrieving transaction audit trails
7
+ - Managing MCP server enabled/disabled states per conversation
8
+ - Exporting transaction data for security audits
9
+ """
10
+
11
+ import sqlite3
12
+ import logging
13
+ from datetime import datetime
14
+ from typing import List, Dict, Optional
15
+
16
+
17
+ def record_mcp_transaction(conn: sqlite3.Connection, conversation_id: int,
18
+ user_prompt: str, tool_name: str, tool_server: str,
19
+ tool_input: str, tool_response: str, is_error: bool = False,
20
+ execution_time_ms: Optional[int] = None,
21
+ message_id: Optional[int] = None, user_guid: str = None) -> int:
22
+ """
23
+ Record an MCP tool transaction for Cyber Security monitoring and audit trails.
24
+
25
+ Args:
26
+ conn: Database connection
27
+ conversation_id: ID of the conversation
28
+ user_prompt: The user's original prompt that triggered the tool call
29
+ tool_name: Name of the MCP tool called
30
+ tool_server: Name of the MCP server
31
+ tool_input: JSON string of tool input parameters
32
+ tool_response: Response from the tool
33
+ is_error: Whether the transaction resulted in an error
34
+ execution_time_ms: Execution time in milliseconds
35
+ message_id: Optional ID of the related message
36
+ user_guid: User GUID for multi-user support
37
+
38
+ Returns:
39
+ ID of the newly created transaction record
40
+ """
41
+ cursor = conn.cursor()
42
+ now = datetime.now()
43
+
44
+ cursor.execute('''
45
+ INSERT INTO mcp_transactions
46
+ (conversation_id, message_id, user_prompt, tool_name, tool_server,
47
+ tool_input, tool_response, is_error, execution_time_ms, transaction_timestamp, user_guid)
48
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
49
+ ''', (conversation_id, message_id, user_prompt, tool_name, tool_server,
50
+ tool_input, tool_response, 1 if is_error else 0, execution_time_ms, now, user_guid))
51
+
52
+ conn.commit()
53
+ transaction_id = cursor.lastrowid
54
+ logging.info(f"Recorded MCP transaction: {tool_server}.{tool_name} (ID: {transaction_id})")
55
+ return transaction_id
56
+
57
+
58
+ def get_mcp_transactions(conn: sqlite3.Connection, conversation_id: Optional[int] = None,
59
+ tool_name: Optional[str] = None, tool_server: Optional[str] = None,
60
+ limit: Optional[int] = None, user_guid: str = None) -> List[Dict]:
61
+ """
62
+ Retrieve MCP transactions with optional filtering.
63
+
64
+ Args:
65
+ conn: Database connection
66
+ conversation_id: Optional filter by conversation ID
67
+ tool_name: Optional filter by tool name
68
+ tool_server: Optional filter by server name
69
+ limit: Optional limit on number of results
70
+ user_guid: User GUID for multi-user support
71
+
72
+ Returns:
73
+ List of transaction dictionaries
74
+ """
75
+ cursor = conn.cursor()
76
+
77
+ query = '''
78
+ SELECT id, conversation_id, message_id, user_prompt, tool_name, tool_server,
79
+ tool_input, tool_response, is_error, execution_time_ms, transaction_timestamp
80
+ FROM mcp_transactions
81
+ WHERE user_guid = ?
82
+ '''
83
+ params = [user_guid]
84
+
85
+ if conversation_id is not None:
86
+ query += ' AND conversation_id = ?'
87
+ params.append(conversation_id)
88
+
89
+ if tool_name is not None:
90
+ query += ' AND tool_name = ?'
91
+ params.append(tool_name)
92
+
93
+ if tool_server is not None:
94
+ query += ' AND tool_server = ?'
95
+ params.append(tool_server)
96
+
97
+ query += ' ORDER BY transaction_timestamp DESC'
98
+
99
+ if limit is not None:
100
+ query += ' LIMIT ?'
101
+ params.append(limit)
102
+
103
+ cursor.execute(query, params)
104
+
105
+ transactions = []
106
+ for row in cursor.fetchall():
107
+ transactions.append({
108
+ 'id': row['id'],
109
+ 'conversation_id': row['conversation_id'],
110
+ 'message_id': row['message_id'],
111
+ 'user_prompt': row['user_prompt'],
112
+ 'tool_name': row['tool_name'],
113
+ 'tool_server': row['tool_server'],
114
+ 'tool_input': row['tool_input'],
115
+ 'tool_response': row['tool_response'],
116
+ 'is_error': bool(row['is_error']),
117
+ 'execution_time_ms': row['execution_time_ms'],
118
+ 'transaction_timestamp': row['transaction_timestamp']
119
+ })
120
+
121
+ return transactions
122
+
123
+
124
+ def get_mcp_transaction_stats(conn: sqlite3.Connection, user_guid: str = None) -> Dict:
125
+ """
126
+ Get statistics about MCP transactions for Cyber Security monitoring.
127
+
128
+ Args:
129
+ conn: Database connection
130
+ user_guid: User GUID for multi-user support
131
+
132
+ Returns:
133
+ Dictionary with transaction statistics
134
+ """
135
+ cursor = conn.cursor()
136
+
137
+ # Total transactions
138
+ cursor.execute('SELECT COUNT(*) as total FROM mcp_transactions WHERE user_guid = ?',
139
+ (user_guid,))
140
+ total = cursor.fetchone()['total']
141
+
142
+ # Error count
143
+ cursor.execute('SELECT COUNT(*) as errors FROM mcp_transactions WHERE is_error = 1 AND user_guid = ?',
144
+ (user_guid,))
145
+ errors = cursor.fetchone()['errors']
146
+
147
+ # Most used tools
148
+ cursor.execute('''
149
+ SELECT tool_server || '.' || tool_name as tool, COUNT(*) as count
150
+ FROM mcp_transactions
151
+ WHERE user_guid = ?
152
+ GROUP BY tool_server, tool_name
153
+ ORDER BY count DESC
154
+ LIMIT 10
155
+ ''', (user_guid,))
156
+ top_tools = [{'tool': row['tool'], 'count': row['count']} for row in cursor.fetchall()]
157
+
158
+ # Recent transactions by conversation
159
+ cursor.execute('''
160
+ SELECT c.name, COUNT(t.id) as count
161
+ FROM mcp_transactions t
162
+ JOIN conversations c ON t.conversation_id = c.id
163
+ WHERE t.user_guid = ? AND c.user_guid = ?
164
+ GROUP BY c.id
165
+ ORDER BY count DESC
166
+ LIMIT 10
167
+ ''', (user_guid, user_guid))
168
+ top_conversations = [{'conversation': row['name'], 'count': row['count']} for row in cursor.fetchall()]
169
+
170
+ return {
171
+ 'total_transactions': total,
172
+ 'error_count': errors,
173
+ 'error_rate': (errors / total * 100) if total > 0 else 0,
174
+ 'top_tools': top_tools,
175
+ 'top_conversations': top_conversations
176
+ }
177
+
178
+
179
+ def export_mcp_transactions_to_csv(conn: sqlite3.Connection, file_path: str,
180
+ conversation_id: Optional[int] = None,
181
+ user_guid: str = None) -> bool:
182
+ """
183
+ Export MCP transactions to CSV for Cyber Security audit.
184
+
185
+ Args:
186
+ conn: Database connection
187
+ file_path: Path to save the CSV file
188
+ conversation_id: Optional filter by conversation ID
189
+ user_guid: User GUID for multi-user support
190
+
191
+ Returns:
192
+ True if successful, False otherwise
193
+ """
194
+ try:
195
+ import csv
196
+
197
+ transactions = get_mcp_transactions(conn, conversation_id=conversation_id,
198
+ user_guid=user_guid)
199
+
200
+ with open(file_path, 'w', newline='', encoding='utf-8') as csvfile:
201
+ fieldnames = [
202
+ 'id', 'transaction_timestamp', 'conversation_id', 'tool_server',
203
+ 'tool_name', 'user_prompt', 'tool_input', 'tool_response',
204
+ 'is_error', 'execution_time_ms'
205
+ ]
206
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
207
+
208
+ writer.writeheader()
209
+ for txn in transactions:
210
+ writer.writerow({
211
+ 'id': txn['id'],
212
+ 'transaction_timestamp': txn['transaction_timestamp'],
213
+ 'conversation_id': txn['conversation_id'],
214
+ 'tool_server': txn['tool_server'],
215
+ 'tool_name': txn['tool_name'],
216
+ 'user_prompt': txn['user_prompt'][:100] + '...' if len(txn['user_prompt']) > 100 else txn['user_prompt'],
217
+ 'tool_input': txn['tool_input'],
218
+ 'tool_response': txn['tool_response'][:200] + '...' if len(txn['tool_response']) > 200 else txn['tool_response'],
219
+ 'is_error': txn['is_error'],
220
+ 'execution_time_ms': txn['execution_time_ms']
221
+ })
222
+
223
+ logging.info(f"Exported {len(transactions)} MCP transactions to {file_path}")
224
+ return True
225
+
226
+ except Exception as e:
227
+ logging.error(f"Failed to export MCP transactions: {e}")
228
+ return False
229
+
230
+
231
+ def get_enabled_mcp_servers(conn: sqlite3.Connection, conversation_id: int,
232
+ user_guid: str = None) -> List[str]:
233
+ """
234
+ Get list of enabled MCP servers for a conversation.
235
+ If no records exist, all servers are considered enabled by default.
236
+
237
+ Args:
238
+ conn: Database connection
239
+ conversation_id: Conversation ID
240
+ user_guid: User GUID for multi-user support
241
+
242
+ Returns:
243
+ List of enabled server names (empty list if all disabled)
244
+ """
245
+ cursor = conn.cursor()
246
+ cursor.execute('''
247
+ SELECT server_name
248
+ FROM conversation_mcp_servers
249
+ WHERE conversation_id = ? AND enabled = 1 AND user_guid = ?
250
+ ''', (conversation_id, user_guid))
251
+
252
+ return [row['server_name'] for row in cursor.fetchall()]
253
+
254
+
255
+ def is_mcp_server_enabled(conn: sqlite3.Connection, conversation_id: int,
256
+ server_name: str, user_guid: str = None) -> bool:
257
+ """
258
+ Check if an MCP server is enabled for a conversation.
259
+ Returns True by default if no record exists (all servers enabled by default).
260
+
261
+ Args:
262
+ conn: Database connection
263
+ conversation_id: Conversation ID
264
+ server_name: Name of the MCP server
265
+ user_guid: User GUID for multi-user support
266
+
267
+ Returns:
268
+ True if enabled, False if disabled
269
+ """
270
+ cursor = conn.cursor()
271
+ cursor.execute('''
272
+ SELECT enabled
273
+ FROM conversation_mcp_servers
274
+ WHERE conversation_id = ? AND server_name = ? AND user_guid = ?
275
+ ''', (conversation_id, server_name, user_guid))
276
+
277
+ row = cursor.fetchone()
278
+ if row is None:
279
+ # No record exists, default to enabled
280
+ return True
281
+ return bool(row['enabled'])
282
+
283
+
284
+ def set_mcp_server_enabled(conn: sqlite3.Connection, conversation_id: int,
285
+ server_name: str, enabled: bool, user_guid: str = None) -> bool:
286
+ """
287
+ Enable or disable an MCP server for a conversation.
288
+
289
+ Args:
290
+ conn: Database connection
291
+ conversation_id: Conversation ID
292
+ server_name: Name of the MCP server
293
+ enabled: True to enable, False to disable
294
+ user_guid: User GUID for multi-user support
295
+
296
+ Returns:
297
+ True if successful, False otherwise
298
+ """
299
+ try:
300
+ cursor = conn.cursor()
301
+ now = datetime.now().isoformat()
302
+
303
+ cursor.execute('''
304
+ INSERT INTO conversation_mcp_servers
305
+ (conversation_id, server_name, enabled, updated_at, user_guid)
306
+ VALUES (?, ?, ?, ?, ?)
307
+ ON CONFLICT(conversation_id, server_name) DO UPDATE SET
308
+ enabled = excluded.enabled,
309
+ updated_at = excluded.updated_at
310
+ ''', (conversation_id, server_name, int(enabled), now, user_guid))
311
+
312
+ conn.commit()
313
+ logging.info(f"MCP server '{server_name}' {'enabled' if enabled else 'disabled'} for conversation {conversation_id}")
314
+ return True
315
+
316
+ except Exception as e:
317
+ logging.error(f"Failed to update MCP server state: {e}")
318
+ conn.rollback()
319
+ return False
320
+
321
+
322
+ def get_all_mcp_server_states(conn: sqlite3.Connection, conversation_id: int,
323
+ all_server_names: List[str], user_guid: str = None) -> List[Dict]:
324
+ """
325
+ Get enabled/disabled state for all MCP servers.
326
+ Servers with no record are considered enabled by default.
327
+
328
+ Args:
329
+ conn: Database connection
330
+ conversation_id: Conversation ID
331
+ all_server_names: List of all available MCP server names
332
+ user_guid: User GUID for multi-user support
333
+
334
+ Returns:
335
+ List of dicts with 'server_name' and 'enabled' keys
336
+ """
337
+ cursor = conn.cursor()
338
+ cursor.execute('''
339
+ SELECT server_name, enabled
340
+ FROM conversation_mcp_servers
341
+ WHERE conversation_id = ? AND user_guid = ?
342
+ ''', (conversation_id, user_guid))
343
+
344
+ # Create a dict of server states
345
+ server_states = {row['server_name']: bool(row['enabled']) for row in cursor.fetchall()}
346
+
347
+ # Build result list with all servers, defaulting to enabled
348
+ result = []
349
+ for server_name in all_server_names:
350
+ result.append({
351
+ 'server_name': server_name,
352
+ 'enabled': server_states.get(server_name, True) # Default to enabled
353
+ })
354
+
355
+ return result
@@ -0,0 +1,161 @@
1
+ """
2
+ Message operations module.
3
+
4
+ This module handles:
5
+ - Adding messages to conversations
6
+ - Retrieving conversation messages
7
+ - Message rollup management
8
+ - Message token tracking
9
+ """
10
+
11
+ import sqlite3
12
+ import logging
13
+ from datetime import datetime
14
+ from typing import List, Dict
15
+
16
+
17
+ def add_message(conn: sqlite3.Connection, conversation_id: int, role: str,
18
+ content: str, token_count: int, user_guid: str = None) -> int:
19
+ """
20
+ Add a message to a conversation.
21
+
22
+ Args:
23
+ conn: Database connection
24
+ conversation_id: ID of the conversation
25
+ role: Message role (user, assistant, system)
26
+ content: Message content
27
+ token_count: Number of tokens in the message
28
+ user_guid: User GUID for multi-user support
29
+
30
+ Returns:
31
+ ID of the newly created message
32
+ """
33
+ cursor = conn.cursor()
34
+ now = datetime.now()
35
+
36
+ cursor.execute('''
37
+ INSERT INTO messages (conversation_id, role, content, token_count, timestamp, user_guid)
38
+ VALUES (?, ?, ?, ?, ?, ?)
39
+ ''', (conversation_id, role, content, token_count, now, user_guid))
40
+
41
+ # Update conversation total tokens and last_updated
42
+ cursor.execute('''
43
+ UPDATE conversations
44
+ SET total_tokens = total_tokens + ?,
45
+ last_updated = ?
46
+ WHERE id = ? AND user_guid = ?
47
+ ''', (token_count, now, conversation_id, user_guid))
48
+
49
+ conn.commit()
50
+ message_id = cursor.lastrowid
51
+ logging.debug(f"Added message {message_id} to conversation {conversation_id}")
52
+ return message_id
53
+
54
+
55
+ def get_conversation_messages(conn: sqlite3.Connection, conversation_id: int,
56
+ include_rolled_up: bool = False, user_guid: str = None) -> List[Dict]:
57
+ """
58
+ Retrieve messages for a conversation.
59
+
60
+ Args:
61
+ conn: Database connection
62
+ conversation_id: ID of the conversation
63
+ include_rolled_up: Whether to include rolled-up messages
64
+ user_guid: User GUID for multi-user support
65
+
66
+ Returns:
67
+ List of message dictionaries
68
+ """
69
+ cursor = conn.cursor()
70
+
71
+ if include_rolled_up:
72
+ query = '''
73
+ SELECT id, role, content, token_count, timestamp, is_rolled_up
74
+ FROM messages
75
+ WHERE conversation_id = ? AND user_guid = ?
76
+ ORDER BY timestamp ASC
77
+ '''
78
+ else:
79
+ query = '''
80
+ SELECT id, role, content, token_count, timestamp, is_rolled_up
81
+ FROM messages
82
+ WHERE conversation_id = ? AND user_guid = ? AND is_rolled_up = 0
83
+ ORDER BY timestamp ASC
84
+ '''
85
+
86
+ cursor.execute(query, (conversation_id, user_guid))
87
+
88
+ messages = []
89
+ for row in cursor.fetchall():
90
+ messages.append({
91
+ 'id': row['id'],
92
+ 'role': row['role'],
93
+ 'content': row['content'],
94
+ 'token_count': row['token_count'],
95
+ 'timestamp': row['timestamp'],
96
+ 'is_rolled_up': bool(row['is_rolled_up'])
97
+ })
98
+
99
+ return messages
100
+
101
+
102
+ def mark_messages_as_rolled_up(conn: sqlite3.Connection, message_ids: List[int],
103
+ user_guid: str = None):
104
+ """
105
+ Mark messages as rolled up.
106
+
107
+ Args:
108
+ conn: Database connection
109
+ message_ids: List of message IDs to mark
110
+ user_guid: User GUID for multi-user support (for safety filtering)
111
+ """
112
+ cursor = conn.cursor()
113
+ placeholders = ','.join('?' * len(message_ids))
114
+ # Add user_guid filtering for security
115
+ cursor.execute(f'''
116
+ UPDATE messages
117
+ SET is_rolled_up = 1
118
+ WHERE id IN ({placeholders}) AND user_guid = ?
119
+ ''', message_ids + [user_guid])
120
+ conn.commit()
121
+ logging.info(f"Marked {len(message_ids)} messages as rolled up")
122
+
123
+
124
+ def record_rollup(conn: sqlite3.Connection, conversation_id: int,
125
+ original_message_count: int, summarised_content: str,
126
+ original_token_count: int, summarised_token_count: int,
127
+ user_guid: str = None):
128
+ """
129
+ Record a rollup operation in history.
130
+
131
+ Args:
132
+ conn: Database connection
133
+ conversation_id: ID of the conversation
134
+ original_message_count: Number of messages that were summarised
135
+ summarised_content: The summary content
136
+ original_token_count: Original token count
137
+ summarised_token_count: Token count after summarisation
138
+ user_guid: User GUID for multi-user support
139
+ """
140
+ cursor = conn.cursor()
141
+ now = datetime.now()
142
+
143
+ cursor.execute('''
144
+ INSERT INTO rollup_history
145
+ (conversation_id, original_message_count, summarised_content,
146
+ original_token_count, summarised_token_count, rollup_timestamp, user_guid)
147
+ VALUES (?, ?, ?, ?, ?, ?, ?)
148
+ ''', (conversation_id, original_message_count, summarised_content,
149
+ original_token_count, summarised_token_count, now, user_guid))
150
+
151
+ # Update conversation total tokens
152
+ token_reduction = original_token_count - summarised_token_count
153
+ cursor.execute('''
154
+ UPDATE conversations
155
+ SET total_tokens = total_tokens - ?
156
+ WHERE id = ? AND user_guid = ?
157
+ ''', (token_reduction, conversation_id, user_guid))
158
+
159
+ conn.commit()
160
+ logging.info(f"Recorded rollup for conversation {conversation_id}, "
161
+ f"reduced tokens by {token_reduction}")