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,482 @@
1
+ """
2
+ Database module for managing conversation storage.
3
+
4
+ Supports multiple database backends:
5
+ - SQLite (default, local file-based)
6
+ - MySQL/MariaDB (remote database server)
7
+ - PostgreSQL (remote database server)
8
+ - Microsoft SQL Server (remote database server)
9
+
10
+ This module provides functionality for:
11
+ - Creating and managing conversation records
12
+ - Storing and retrieving messages
13
+ - Managing conversation rollup history
14
+ - File attachments
15
+ - MCP transaction tracking
16
+ - Usage tracking for token management
17
+
18
+
19
+ """
20
+
21
+ import sqlite3
22
+ import logging
23
+ from datetime import datetime
24
+ from typing import List, Dict, Optional, Tuple
25
+
26
+ # Import sub-modules
27
+ from . import schema
28
+ from . import connection as conn_module
29
+ from . import conversations as conv_module
30
+ from . import messages as msg_module
31
+ from . import files as files_module
32
+ from . import mcp_ops
33
+ from . import usage as usage_module
34
+ from . import backends
35
+ from . import credential_prompt
36
+ from . import tool_permissions
37
+ from . import autonomous_actions as actions_module
38
+
39
+
40
+ class ConversationDatabase:
41
+ """
42
+ Manages SQLite database operations for conversation storage.
43
+
44
+ This class provides a unified interface to all database operations,
45
+ delegating to specialised modules for different concerns.
46
+ """
47
+
48
+ def __init__(self, user_guid: str, db_type: str = 'sqlite',
49
+ credentials=None, db_path: str = None):
50
+ """
51
+ Initialise the database connection with multi-database support.
52
+
53
+ Args:
54
+ user_guid: Unique identifier for the current user
55
+ db_type: Database type (sqlite, mysql, mariadb, postgresql, mssql)
56
+ credentials: DatabaseCredentials object for connection
57
+ db_path: Path to SQLite database file (deprecated, for backward compatibility)
58
+ """
59
+ from .backends import DatabaseCredentials
60
+
61
+ self.user_guid = user_guid
62
+ self.db_type = db_type
63
+
64
+ # Backward compatibility: if db_path provided, use SQLite
65
+ if db_path and credentials is None:
66
+ credentials = DatabaseCredentials(path=db_path)
67
+ db_type = 'sqlite'
68
+ self.db_type = 'sqlite'
69
+
70
+ # Create database connection with appropriate backend
71
+ self._conn_manager = conn_module.DatabaseConnection(
72
+ db_type=db_type,
73
+ credentials=credentials
74
+ )
75
+ self.conn = self._conn_manager.get_connection()
76
+ self.db_path = credentials.path if db_type == 'sqlite' else None
77
+
78
+ # Get backend for SQL dialect-specific operations
79
+ self.backend = self._conn_manager.get_backend()
80
+
81
+ # Initialise schema (handles different SQL dialects)
82
+ schema.initialise_schema(self.conn, self.backend)
83
+
84
+ # Migrate existing records to current user if needed
85
+ schema.migrate_user_guid(self.conn, self.user_guid)
86
+
87
+ # Conversation operations
88
+ def create_conversation(self, name: str, model_id: str,
89
+ instructions: Optional[str] = None,
90
+ compaction_threshold: Optional[float] = None) -> int:
91
+ """Create a new conversation."""
92
+ return conv_module.create_conversation(self.conn, name, model_id, instructions,
93
+ user_guid=self.user_guid,
94
+ compaction_threshold=compaction_threshold)
95
+
96
+ def get_active_conversations(self) -> List[Dict]:
97
+ """Retrieve all active conversations."""
98
+ return conv_module.get_active_conversations(self.conn, user_guid=self.user_guid)
99
+
100
+ def get_conversation(self, conversation_id: int) -> Optional[Dict]:
101
+ """Retrieve a specific conversation."""
102
+ with self._conn_manager._lock:
103
+ return conv_module.get_conversation(self.conn, conversation_id, user_guid=self.user_guid)
104
+
105
+ def get_conversation_token_count(self, conversation_id: int) -> int:
106
+ """Get the total token count for a conversation."""
107
+ return conv_module.get_conversation_token_count(self.conn, conversation_id,
108
+ user_guid=self.user_guid)
109
+
110
+ def recalculate_total_tokens(self, conversation_id: int) -> int:
111
+ """
112
+ Recalculate and update total_tokens from active (non-rolled-up) messages.
113
+
114
+ This ensures accuracy after compaction operations by summing only
115
+ the tokens from messages that haven't been rolled up.
116
+
117
+ Returns:
118
+ The new total token count
119
+ """
120
+ with self._conn_manager._lock:
121
+ return conv_module.recalculate_total_tokens(self.conn, conversation_id,
122
+ user_guid=self.user_guid)
123
+
124
+ def delete_conversation(self, conversation_id: int) -> bool:
125
+ """Delete a conversation and all its messages."""
126
+ return conv_module.delete_conversation(self.conn, conversation_id, user_guid=self.user_guid)
127
+
128
+ def update_conversation_max_tokens(self, conversation_id: int, max_tokens: int):
129
+ """Update the max_tokens setting for a specific conversation."""
130
+ conv_module.update_conversation_max_tokens(self.conn, conversation_id, max_tokens,
131
+ user_guid=self.user_guid)
132
+
133
+ def update_conversation_compaction_threshold(self, conversation_id: int,
134
+ compaction_threshold: float):
135
+ """Update the compaction_threshold setting for a specific conversation."""
136
+ conv_module.update_conversation_compaction_threshold(self.conn, conversation_id,
137
+ compaction_threshold,
138
+ user_guid=self.user_guid)
139
+
140
+ def update_conversation_instructions(self, conversation_id: int,
141
+ instructions: Optional[str]):
142
+ """Update the instructions for a specific conversation."""
143
+ conv_module.update_conversation_instructions(self.conn, conversation_id, instructions,
144
+ user_guid=self.user_guid)
145
+
146
+ def update_token_usage(self, conversation_id: int, tokens_sent: int,
147
+ tokens_received: int, model_id: str = None):
148
+ """Update the API token usage for a conversation and track per-model usage."""
149
+ with self._conn_manager._lock:
150
+ conv_module.update_token_usage(self.conn, conversation_id, tokens_sent,
151
+ tokens_received, model_id, user_guid=self.user_guid)
152
+
153
+ def get_model_usage_breakdown(self, conversation_id: int) -> List[Dict]:
154
+ """Get per-model token usage breakdown for a conversation."""
155
+ return conv_module.get_model_usage_breakdown(self.conn, conversation_id,
156
+ user_guid=self.user_guid)
157
+
158
+ def get_predefined_conversation_by_name(self, name: str) -> Optional[Dict]:
159
+ """Retrieve a predefined conversation by name."""
160
+ return conv_module.get_predefined_conversation_by_name(self.conn, name,
161
+ user_guid=self.user_guid)
162
+
163
+ def create_predefined_conversation(self, name: str, model_id: str,
164
+ instructions: Optional[str], config_hash: str) -> int:
165
+ """Create a new predefined conversation."""
166
+ return conv_module.create_predefined_conversation(self.conn, name, model_id,
167
+ instructions, config_hash,
168
+ user_guid=self.user_guid)
169
+
170
+ def update_predefined_conversation(self, conversation_id: int, model_id: str,
171
+ instructions: Optional[str], config_hash: str):
172
+ """Update a predefined conversation's settings."""
173
+ conv_module.update_predefined_conversation(self.conn, conversation_id, model_id,
174
+ instructions, config_hash,
175
+ user_guid=self.user_guid)
176
+
177
+ def is_conversation_predefined(self, conversation_id: int) -> bool:
178
+ """Check if a conversation is predefined."""
179
+ return conv_module.is_conversation_predefined(self.conn, conversation_id,
180
+ user_guid=self.user_guid)
181
+
182
+ # Message operations
183
+ def add_message(self, conversation_id: int, role: str, content: str,
184
+ token_count: int) -> int:
185
+ """Add a message to a conversation."""
186
+ with self._conn_manager._lock:
187
+ return msg_module.add_message(self.conn, conversation_id, role, content, token_count,
188
+ user_guid=self.user_guid)
189
+
190
+ def get_conversation_messages(self, conversation_id: int,
191
+ include_rolled_up: bool = False) -> List[Dict]:
192
+ """Retrieve messages for a conversation."""
193
+ with self._conn_manager._lock:
194
+ return msg_module.get_conversation_messages(self.conn, conversation_id, include_rolled_up,
195
+ user_guid=self.user_guid)
196
+
197
+ def mark_messages_as_rolled_up(self, message_ids: List[int]):
198
+ """Mark messages as rolled up."""
199
+ with self._conn_manager._lock:
200
+ msg_module.mark_messages_as_rolled_up(self.conn, message_ids, user_guid=self.user_guid)
201
+
202
+ def record_rollup(self, conversation_id: int, original_message_count: int,
203
+ summarised_content: str, original_token_count: int,
204
+ summarised_token_count: int):
205
+ """Record a rollup operation in history."""
206
+ with self._conn_manager._lock:
207
+ msg_module.record_rollup(self.conn, conversation_id, original_message_count,
208
+ summarised_content, original_token_count, summarised_token_count,
209
+ user_guid=self.user_guid)
210
+
211
+ # File operations
212
+ def add_file(self, conversation_id: int, filename: str, file_type: str, file_size: int,
213
+ content_text: Optional[str] = None, content_base64: Optional[str] = None,
214
+ mime_type: Optional[str] = None, token_count: int = 0,
215
+ tags: Optional[str] = None) -> int:
216
+ """Add a file to a conversation."""
217
+ return files_module.add_file(self.conn, conversation_id, filename, file_type,
218
+ file_size, content_text, content_base64, mime_type,
219
+ token_count, tags, user_guid=self.user_guid)
220
+
221
+ def get_conversation_files(self, conversation_id: int) -> List[Dict]:
222
+ """Retrieve all files for a conversation."""
223
+ return files_module.get_conversation_files(self.conn, conversation_id,
224
+ user_guid=self.user_guid)
225
+
226
+ def get_files_by_tag(self, conversation_id: int, tag: str) -> List[Dict]:
227
+ """Retrieve files for a conversation filtered by tag."""
228
+ return files_module.get_files_by_tag(self.conn, conversation_id, tag,
229
+ user_guid=self.user_guid)
230
+
231
+ def delete_conversation_files(self, conversation_id: int):
232
+ """Delete all files for a conversation."""
233
+ files_module.delete_conversation_files(self.conn, conversation_id, user_guid=self.user_guid)
234
+
235
+ def delete_file(self, file_id: int) -> bool:
236
+ """Delete a specific file by ID."""
237
+ return files_module.delete_file(self.conn, file_id, user_guid=self.user_guid)
238
+
239
+ # MCP operations
240
+ def record_mcp_transaction(self, conversation_id: int, user_prompt: str, tool_name: str,
241
+ tool_server: str, tool_input: str, tool_response: str,
242
+ is_error: bool = False, execution_time_ms: Optional[int] = None,
243
+ message_id: Optional[int] = None) -> int:
244
+ """Record an MCP tool transaction for Cyber Security monitoring and audit trails."""
245
+ with self._conn_manager._lock:
246
+ return mcp_ops.record_mcp_transaction(self.conn, conversation_id, user_prompt, tool_name,
247
+ tool_server, tool_input, tool_response, is_error,
248
+ execution_time_ms, message_id, user_guid=self.user_guid)
249
+
250
+ def get_mcp_transactions(self, conversation_id: Optional[int] = None,
251
+ tool_name: Optional[str] = None,
252
+ tool_server: Optional[str] = None,
253
+ limit: Optional[int] = None) -> List[Dict]:
254
+ """Retrieve MCP transactions with optional filtering."""
255
+ return mcp_ops.get_mcp_transactions(self.conn, conversation_id, tool_name,
256
+ tool_server, limit, user_guid=self.user_guid)
257
+
258
+ def get_mcp_transaction_stats(self) -> Dict:
259
+ """Get statistics about MCP transactions for Cyber Security monitoring."""
260
+ return mcp_ops.get_mcp_transaction_stats(self.conn, user_guid=self.user_guid)
261
+
262
+ def export_mcp_transactions_to_csv(self, file_path: str,
263
+ conversation_id: Optional[int] = None) -> bool:
264
+ """Export MCP transactions to CSV for Cyber Security audit."""
265
+ return mcp_ops.export_mcp_transactions_to_csv(self.conn, file_path, conversation_id,
266
+ user_guid=self.user_guid)
267
+
268
+ def get_enabled_mcp_servers(self, conversation_id: int) -> List[str]:
269
+ """Get list of enabled MCP servers for a conversation."""
270
+ return mcp_ops.get_enabled_mcp_servers(self.conn, conversation_id, user_guid=self.user_guid)
271
+
272
+ def is_mcp_server_enabled(self, conversation_id: int, server_name: str) -> bool:
273
+ """Check if an MCP server is enabled for a conversation."""
274
+ return mcp_ops.is_mcp_server_enabled(self.conn, conversation_id, server_name,
275
+ user_guid=self.user_guid)
276
+
277
+ def set_mcp_server_enabled(self, conversation_id: int, server_name: str,
278
+ enabled: bool) -> bool:
279
+ """Enable or disable an MCP server for a conversation."""
280
+ return mcp_ops.set_mcp_server_enabled(self.conn, conversation_id, server_name, enabled,
281
+ user_guid=self.user_guid)
282
+
283
+ def get_all_mcp_server_states(self, conversation_id: int,
284
+ all_server_names: List[str]) -> List[Dict]:
285
+ """Get enabled/disabled state for all MCP servers."""
286
+ return mcp_ops.get_all_mcp_server_states(self.conn, conversation_id, all_server_names,
287
+ user_guid=self.user_guid)
288
+
289
+ # Tool permissions operations
290
+ def check_tool_permission(self, conversation_id: int, tool_name: str) -> Optional[str]:
291
+ """
292
+ Check the permission state for a tool in a conversation.
293
+ Returns None if no record exists (first-time usage, should prompt).
294
+ """
295
+ return tool_permissions.check_tool_permission(self.conn, conversation_id, tool_name,
296
+ user_guid=self.user_guid)
297
+
298
+ def set_tool_permission(self, conversation_id: int, tool_name: str,
299
+ permission_state: str) -> bool:
300
+ """Set the permission state for a tool in a conversation."""
301
+ return tool_permissions.set_tool_permission(self.conn, conversation_id, tool_name,
302
+ permission_state, user_guid=self.user_guid)
303
+
304
+ def get_all_tool_permissions(self, conversation_id: int) -> List[Dict]:
305
+ """Get all tool permissions for a conversation."""
306
+ return tool_permissions.get_all_tool_permissions(self.conn, conversation_id,
307
+ user_guid=self.user_guid)
308
+
309
+ def delete_tool_permission(self, conversation_id: int, tool_name: str) -> bool:
310
+ """Delete a tool permission record (reset to first-time usage behavior)."""
311
+ return tool_permissions.delete_tool_permission(self.conn, conversation_id, tool_name,
312
+ user_guid=self.user_guid)
313
+
314
+ def is_tool_allowed(self, conversation_id: int, tool_name: str) -> Optional[bool]:
315
+ """
316
+ Check if a tool is allowed to run.
317
+ Returns None if permission should be requested from user (first-time usage).
318
+ Returns True if allowed, False if denied.
319
+ """
320
+ return tool_permissions.is_tool_allowed(self.conn, conversation_id, tool_name,
321
+ user_guid=self.user_guid)
322
+
323
+ # Autonomous action operations
324
+ def create_action(self, name: str, description: str, action_prompt: str,
325
+ model_id: str, schedule_type: str, schedule_config: Dict,
326
+ context_mode: str = 'fresh', max_failures: int = 3,
327
+ max_tokens: int = 8192) -> int:
328
+ """Create a new autonomous action."""
329
+ return actions_module.create_action(self.conn, name, description, action_prompt,
330
+ model_id, schedule_type, schedule_config,
331
+ context_mode, max_failures, max_tokens,
332
+ user_guid=self.user_guid)
333
+
334
+ def get_action(self, action_id: int) -> Optional[Dict]:
335
+ """Retrieve a specific action."""
336
+ return actions_module.get_action(self.conn, action_id, user_guid=self.user_guid)
337
+
338
+ def get_action_by_name(self, name: str) -> Optional[Dict]:
339
+ """Retrieve an action by name."""
340
+ return actions_module.get_action_by_name(self.conn, name, user_guid=self.user_guid)
341
+
342
+ def get_all_actions(self, include_disabled: bool = True) -> List[Dict]:
343
+ """Retrieve all actions."""
344
+ return actions_module.get_all_actions(self.conn, user_guid=self.user_guid,
345
+ include_disabled=include_disabled)
346
+
347
+ def update_action(self, action_id: int, updates: Dict) -> bool:
348
+ """Update an action's configuration."""
349
+ return actions_module.update_action(self.conn, action_id, updates,
350
+ user_guid=self.user_guid)
351
+
352
+ def delete_action(self, action_id: int) -> bool:
353
+ """Delete an action and all its related data."""
354
+ return actions_module.delete_action(self.conn, action_id, user_guid=self.user_guid)
355
+
356
+ def enable_action(self, action_id: int) -> bool:
357
+ """Enable a disabled action."""
358
+ return actions_module.enable_action(self.conn, action_id, user_guid=self.user_guid)
359
+
360
+ def disable_action(self, action_id: int) -> bool:
361
+ """Disable an action."""
362
+ return actions_module.disable_action(self.conn, action_id, user_guid=self.user_guid)
363
+
364
+ def increment_action_failure_count(self, action_id: int) -> Dict:
365
+ """Increment failure count and auto-disable if threshold reached."""
366
+ return actions_module.increment_failure_count(self.conn, action_id,
367
+ user_guid=self.user_guid)
368
+
369
+ def update_action_last_run(self, action_id: int,
370
+ next_run_at: Optional[datetime] = None) -> bool:
371
+ """Update last_run_at and optionally next_run_at."""
372
+ return actions_module.update_last_run(self.conn, action_id, next_run_at,
373
+ user_guid=self.user_guid)
374
+
375
+ # Action run operations
376
+ def record_action_run(self, action_id: int, status: str,
377
+ result_text: str = None, result_html: str = None,
378
+ error_message: str = None, input_tokens: int = 0,
379
+ output_tokens: int = 0, context_snapshot: str = None) -> int:
380
+ """Record a new action run."""
381
+ return actions_module.record_action_run(self.conn, action_id, status,
382
+ user_guid=self.user_guid,
383
+ result_text=result_text,
384
+ result_html=result_html,
385
+ error_message=error_message,
386
+ input_tokens=input_tokens,
387
+ output_tokens=output_tokens,
388
+ context_snapshot=context_snapshot)
389
+
390
+ def update_action_run(self, run_id: int, status: str,
391
+ result_text: str = None, result_html: str = None,
392
+ error_message: str = None, input_tokens: int = None,
393
+ output_tokens: int = None, context_snapshot: str = None) -> bool:
394
+ """Update an existing action run record."""
395
+ return actions_module.update_action_run(self.conn, run_id, status,
396
+ user_guid=self.user_guid,
397
+ result_text=result_text,
398
+ result_html=result_html,
399
+ error_message=error_message,
400
+ input_tokens=input_tokens,
401
+ output_tokens=output_tokens,
402
+ context_snapshot=context_snapshot)
403
+
404
+ def get_action_run(self, run_id: int) -> Optional[Dict]:
405
+ """Retrieve a specific action run."""
406
+ return actions_module.get_action_run(self.conn, run_id, user_guid=self.user_guid)
407
+
408
+ def get_action_runs(self, action_id: int, limit: int = 50,
409
+ offset: int = 0) -> List[Dict]:
410
+ """Retrieve runs for an action."""
411
+ return actions_module.get_action_runs(self.conn, action_id, user_guid=self.user_guid,
412
+ limit=limit, offset=offset)
413
+
414
+ def get_recent_action_runs(self, limit: int = 20) -> List[Dict]:
415
+ """Retrieve recent runs across all actions."""
416
+ return actions_module.get_recent_runs(self.conn, user_guid=self.user_guid,
417
+ limit=limit)
418
+
419
+ def get_failed_action_count(self) -> int:
420
+ """Get count of disabled actions (for home screen indicator)."""
421
+ return actions_module.get_failed_action_count(self.conn, user_guid=self.user_guid)
422
+
423
+ # Action tool permission operations
424
+ def set_action_tool_permission(self, action_id: int, tool_name: str,
425
+ server_name: str, permission_state: str) -> bool:
426
+ """Set a tool permission for an action."""
427
+ return actions_module.set_action_tool_permission(self.conn, action_id,
428
+ tool_name, server_name,
429
+ permission_state,
430
+ user_guid=self.user_guid)
431
+
432
+ def set_action_tool_permissions_batch(self, action_id: int,
433
+ permissions: List[Dict]) -> bool:
434
+ """Set multiple tool permissions for an action."""
435
+ return actions_module.set_action_tool_permissions_batch(self.conn, action_id,
436
+ permissions,
437
+ user_guid=self.user_guid)
438
+
439
+ def get_action_tool_permissions(self, action_id: int) -> List[Dict]:
440
+ """Get all tool permissions for an action."""
441
+ return actions_module.get_action_tool_permissions(self.conn, action_id,
442
+ user_guid=self.user_guid)
443
+
444
+ # Usage tracking operations
445
+ def record_usage(self, conversation_id: int, model_id: str, region: str,
446
+ input_tokens: int, output_tokens: int, cost: float,
447
+ timestamp: datetime):
448
+ """Record usage for token management and billing."""
449
+ usage_module.record_usage(self.conn, conversation_id, model_id, region,
450
+ input_tokens, output_tokens, cost, timestamp,
451
+ user_guid=self.user_guid)
452
+
453
+ def get_usage_in_window(self, window_start: datetime) -> float:
454
+ """Get total cost for usage since window_start."""
455
+ return usage_module.get_usage_in_window(self.conn, window_start, user_guid=self.user_guid)
456
+
457
+ def get_oldest_usage_in_window(self, window_start: datetime) -> Optional[datetime]:
458
+ """Get timestamp of oldest usage in the rolling window."""
459
+ return usage_module.get_oldest_usage_in_window(self.conn, window_start,
460
+ user_guid=self.user_guid)
461
+
462
+ def get_token_usage_in_window(self, window_start: datetime) -> Tuple[int, int]:
463
+ """Get total token usage (input and output separately) since window_start."""
464
+ return usage_module.get_token_usage_in_window(self.conn, window_start,
465
+ user_guid=self.user_guid)
466
+
467
+ def get_usage_summary(self, window_start: datetime) -> List[Dict]:
468
+ """Get detailed usage summary for the rolling window."""
469
+ return usage_module.get_usage_summary(self.conn, window_start, user_guid=self.user_guid)
470
+
471
+ def cleanup_old_usage(self, cutoff_date: datetime):
472
+ """Clean up usage records older than cutoff_date."""
473
+ usage_module.cleanup_old_usage(self.conn, cutoff_date, user_guid=self.user_guid)
474
+
475
+ # Connection management
476
+ def close(self):
477
+ """Close the database connection."""
478
+ self._conn_manager.close()
479
+
480
+
481
+ # Export the main class
482
+ __all__ = ['ConversationDatabase']