aradhyapavan commited on
Commit
821e3a9
·
verified ·
1 Parent(s): 8d1e3d0

Update modules/database.py

Browse files
Files changed (1) hide show
  1. modules/database.py +300 -259
modules/database.py CHANGED
@@ -1,259 +1,300 @@
1
- #!/usr/bin/env python3
2
- """
3
- Database module for Multi-Personality Chat Bot
4
- Handles SQLite database operations for chat history and analytics.
5
- """
6
-
7
- import sqlite3
8
- import logging
9
- from datetime import datetime
10
- from typing import List, Dict, Optional, Any
11
-
12
- logger = logging.getLogger(__name__)
13
-
14
- class ChatDatabase:
15
- """Simple SQLite database for chat storage"""
16
-
17
- def __init__(self, db_path: str = "chat_data.db"):
18
- """Initialize database connection"""
19
- self.db_path = db_path
20
- self.initialize_database()
21
-
22
- def initialize_database(self):
23
- """Create database tables if they don't exist"""
24
- try:
25
- with sqlite3.connect(self.db_path) as conn:
26
- cursor = conn.cursor()
27
-
28
- # Create messages table
29
- cursor.execute("""
30
- CREATE TABLE IF NOT EXISTS messages (
31
- id INTEGER PRIMARY KEY AUTOINCREMENT,
32
- username TEXT NOT NULL,
33
- message TEXT NOT NULL,
34
- personality_type TEXT NOT NULL,
35
- sender_type TEXT NOT NULL CHECK(sender_type IN ('user', 'bot')),
36
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
37
- session_id TEXT,
38
- response_time REAL
39
- )
40
- """)
41
-
42
- # Lightweight migration: ensure expected columns exist on older DBs
43
- self._ensure_messages_columns(conn)
44
-
45
- # Create personality_stats table
46
- cursor.execute("""
47
- CREATE TABLE IF NOT EXISTS personality_stats (
48
- personality_type TEXT PRIMARY KEY,
49
- total_messages INTEGER DEFAULT 0,
50
- avg_response_time REAL DEFAULT 0.0,
51
- last_used DATETIME DEFAULT CURRENT_TIMESTAMP
52
- )
53
- """)
54
-
55
- conn.commit()
56
- logger.info("Database initialized successfully")
57
-
58
- except Exception as e:
59
- logger.error(f"Database initialization error: {str(e)}")
60
-
61
- def _ensure_messages_columns(self, conn: sqlite3.Connection) -> None:
62
- """Ensure required columns exist in messages table for backward compatibility."""
63
- try:
64
- cursor = conn.cursor()
65
- cursor.execute("PRAGMA table_info(messages)")
66
- cols = {row[1]: row for row in cursor.fetchall()} # name -> info
67
-
68
- # Columns to ensure exist: name -> SQL add column clause
69
- required_columns = {
70
- 'personality_type': "ALTER TABLE messages ADD COLUMN personality_type TEXT",
71
- 'sender_type': "ALTER TABLE messages ADD COLUMN sender_type TEXT",
72
- 'session_id': "ALTER TABLE messages ADD COLUMN session_id TEXT",
73
- 'response_time': "ALTER TABLE messages ADD COLUMN response_time REAL",
74
- 'timestamp': "ALTER TABLE messages ADD COLUMN timestamp DATETIME DEFAULT CURRENT_TIMESTAMP",
75
- }
76
-
77
- for name, alter_sql in required_columns.items():
78
- if name not in cols:
79
- try:
80
- cursor.execute(alter_sql)
81
- logger.info(f"Added missing column to messages table: {name}")
82
- except Exception as e:
83
- # If the column exists but PRAGMA didn't report it correctly or another race, log and continue
84
- logger.warning(f"Could not add column '{name}' to messages: {e}")
85
- conn.commit()
86
- except Exception as e:
87
- logger.error(f"Error ensuring messages columns: {e}")
88
-
89
- def save_message(self, username: str, message: str, personality_type: str,
90
- sender_type: str, session_id: str = None, response_time: float = None) -> bool:
91
- """Save a message to the database"""
92
- try:
93
- with sqlite3.connect(self.db_path) as conn:
94
- cursor = conn.cursor()
95
-
96
- cursor.execute("""
97
- INSERT INTO messages (username, message, personality_type, sender_type, session_id, response_time)
98
- VALUES (?, ?, ?, ?, ?, ?)
99
- """, (username, message, personality_type, sender_type, session_id, response_time))
100
-
101
- conn.commit()
102
- return True
103
-
104
- except sqlite3.OperationalError as e:
105
- # Auto-migrate if schema is missing expected columns, then retry once
106
- if "no column named" in str(e).lower():
107
- logger.warning(f"Schema issue detected ('{e}'). Attempting to migrate and retry insert...")
108
- try:
109
- with sqlite3.connect(self.db_path) as conn:
110
- self._ensure_messages_columns(conn)
111
- cursor = conn.cursor()
112
- cursor.execute(
113
- """
114
- INSERT INTO messages (username, message, personality_type, sender_type, session_id, response_time)
115
- VALUES (?, ?, ?, ?, ?, ?)
116
- """,
117
- (username, message, personality_type, sender_type, session_id, response_time),
118
- )
119
- conn.commit()
120
- logger.info("Insert succeeded after schema migration")
121
- return True
122
- except Exception as e2:
123
- logger.error(f"Retry after migration failed: {e2}")
124
- return False
125
- else:
126
- logger.error(f"Operational DB error saving message: {e}")
127
- return False
128
- except Exception as e:
129
- logger.error(f"Unexpected DB error saving message: {str(e)}")
130
- return False
131
-
132
- def get_recent_messages(self, personality_type: str = None, limit: int = 50) -> List[Dict[str, Any]]:
133
- """Get recent messages, optionally filtered by personality type"""
134
- try:
135
- with sqlite3.connect(self.db_path) as conn:
136
- cursor = conn.cursor()
137
-
138
- if personality_type:
139
- cursor.execute("""
140
- SELECT username, message, personality_type, sender_type, timestamp
141
- FROM messages
142
- WHERE personality_type = ?
143
- ORDER BY timestamp DESC
144
- LIMIT ?
145
- """, (personality_type, limit))
146
- else:
147
- cursor.execute("""
148
- SELECT username, message, personality_type, sender_type, timestamp
149
- FROM messages
150
- ORDER BY timestamp DESC
151
- LIMIT ?
152
- """, (limit,))
153
-
154
- messages = []
155
- for row in cursor.fetchall():
156
- messages.append({
157
- 'username': row[0],
158
- 'message': row[1],
159
- 'personality_type': row[2],
160
- 'sender_type': row[3],
161
- 'timestamp': row[4]
162
- })
163
-
164
- return messages
165
-
166
- except Exception as e:
167
- logger.error(f"Error retrieving messages: {str(e)}")
168
- return []
169
-
170
- def clear_personality_chat(self, personality_type: str, username: str = None) -> bool:
171
- """Clear chat history for a personality (optionally for a specific user)"""
172
- try:
173
- with sqlite3.connect(self.db_path) as conn:
174
- cursor = conn.cursor()
175
-
176
- if username:
177
- cursor.execute("""
178
- DELETE FROM messages
179
- WHERE personality_type = ? AND username = ?
180
- """, (personality_type, username))
181
- else:
182
- cursor.execute("""
183
- DELETE FROM messages
184
- WHERE personality_type = ?
185
- """, (personality_type,))
186
-
187
- conn.commit()
188
- return True
189
-
190
- except Exception as e:
191
- logger.error(f"Error clearing chat: {str(e)}")
192
- return False
193
-
194
- def get_personality_stats(self, personality_type: str) -> Dict[str, Any]:
195
- """Get statistics for a specific personality"""
196
- try:
197
- with sqlite3.connect(self.db_path) as conn:
198
- cursor = conn.cursor()
199
-
200
- # Get message count
201
- cursor.execute("""
202
- SELECT COUNT(*) FROM messages
203
- WHERE personality_type = ? AND sender_type = 'bot'
204
- """, (personality_type,))
205
-
206
- message_count = cursor.fetchone()[0]
207
-
208
- # Get average response time
209
- cursor.execute("""
210
- SELECT AVG(response_time) FROM messages
211
- WHERE personality_type = ? AND sender_type = 'bot' AND response_time IS NOT NULL
212
- """, (personality_type,))
213
-
214
- avg_response_time = cursor.fetchone()[0] or 0.0
215
-
216
- return {
217
- 'personality_type': personality_type,
218
- 'total_messages': message_count,
219
- 'avg_response_time': round(avg_response_time, 2)
220
- }
221
-
222
- except Exception as e:
223
- logger.error(f"Error getting personality stats: {str(e)}")
224
- return {
225
- 'personality_type': personality_type,
226
- 'total_messages': 0,
227
- 'avg_response_time': 0.0
228
- }
229
-
230
- def get_all_stats(self) -> Dict[str, Any]:
231
- """Get overall statistics"""
232
- try:
233
- with sqlite3.connect(self.db_path) as conn:
234
- cursor = conn.cursor()
235
-
236
- # Total messages
237
- cursor.execute("SELECT COUNT(*) FROM messages")
238
- total_messages = cursor.fetchone()[0]
239
-
240
- # Messages by personality
241
- cursor.execute("""
242
- SELECT personality_type, COUNT(*)
243
- FROM messages
244
- GROUP BY personality_type
245
- """)
246
-
247
- personality_breakdown = dict(cursor.fetchall())
248
-
249
- return {
250
- 'total_messages': total_messages,
251
- 'personality_breakdown': personality_breakdown
252
- }
253
-
254
- except Exception as e:
255
- logger.error(f"Error getting all stats: {str(e)}")
256
- return {
257
- 'total_messages': 0,
258
- 'personality_breakdown': {}
259
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Database module for Multi-Personality Chat Bot
4
+ Handles SQLite database operations for chat history and analytics.
5
+ """
6
+
7
+ import sqlite3
8
+ import os
9
+ import tempfile
10
+ import logging
11
+ from datetime import datetime
12
+ from typing import List, Dict, Optional, Any
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ class ChatDatabase:
17
+ """Simple SQLite database for chat storage"""
18
+
19
+ def __init__(self, db_path: str = None):
20
+ """Initialize database connection"""
21
+ # Determine database path from env or default to ./data/chat_data.db
22
+ resolved_path = db_path or os.getenv("DB_PATH") or os.path.join(os.getcwd(), "data", "chat_data.db")
23
+ # Ensure parent directory exists and is writable
24
+ try:
25
+ parent_dir = os.path.dirname(resolved_path) or "."
26
+ os.makedirs(parent_dir, exist_ok=True)
27
+ except Exception as e:
28
+ logger.warning(f"Could not create DB directory '{resolved_path}': {e}")
29
+ self.db_path = resolved_path
30
+ self.initialize_database()
31
+
32
+ def initialize_database(self):
33
+ """Create database tables if they don't exist"""
34
+ def _create_schema(conn: sqlite3.Connection):
35
+ cursor = conn.cursor()
36
+
37
+ # Create messages table
38
+ cursor.execute("""
39
+ CREATE TABLE IF NOT EXISTS messages (
40
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
41
+ username TEXT NOT NULL,
42
+ message TEXT NOT NULL,
43
+ personality_type TEXT NOT NULL,
44
+ sender_type TEXT NOT NULL CHECK(sender_type IN ('user', 'bot')),
45
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
46
+ session_id TEXT,
47
+ response_time REAL
48
+ )
49
+ """)
50
+
51
+ # Lightweight migration: ensure expected columns exist on older DBs
52
+ self._ensure_messages_columns(conn)
53
+
54
+ # Create personality_stats table
55
+ cursor.execute("""
56
+ CREATE TABLE IF NOT EXISTS personality_stats (
57
+ personality_type TEXT PRIMARY KEY,
58
+ total_messages INTEGER DEFAULT 0,
59
+ avg_response_time REAL DEFAULT 0.0,
60
+ last_used DATETIME DEFAULT CURRENT_TIMESTAMP
61
+ )
62
+ """)
63
+
64
+ conn.commit()
65
+ logger.info("Database initialized successfully")
66
+
67
+ # First attempt
68
+ try:
69
+ with sqlite3.connect(self.db_path) as conn:
70
+ _create_schema(conn)
71
+ except sqlite3.OperationalError as e:
72
+ if "unable to open database file" in str(e).lower():
73
+ # Fallback: try to recreate directory and retry
74
+ try:
75
+ parent_dir = os.path.dirname(self.db_path) or "."
76
+ os.makedirs(parent_dir, exist_ok=True)
77
+ with sqlite3.connect(self.db_path) as conn:
78
+ _create_schema(conn)
79
+ return
80
+ except Exception:
81
+ # Final fallback: use temp directory
82
+ tmp_dir = tempfile.gettempdir()
83
+ fallback_path = os.path.join(tmp_dir, "chat_data.db")
84
+ try:
85
+ os.makedirs(tmp_dir, exist_ok=True)
86
+ except Exception:
87
+ pass
88
+ try:
89
+ with sqlite3.connect(fallback_path) as conn:
90
+ self.db_path = fallback_path
91
+ _create_schema(conn)
92
+ logger.warning(f"DB path fallback in use: {fallback_path}")
93
+ return
94
+ except Exception as e2:
95
+ logger.error(f"Database initialization error (fallback failed): {e2}")
96
+ return
97
+ else:
98
+ logger.error(f"Database initialization error: {str(e)}")
99
+ except Exception as e:
100
+ logger.error(f"Database initialization error: {str(e)}")
101
+
102
+ def _ensure_messages_columns(self, conn: sqlite3.Connection) -> None:
103
+ """Ensure required columns exist in messages table for backward compatibility."""
104
+ try:
105
+ cursor = conn.cursor()
106
+ cursor.execute("PRAGMA table_info(messages)")
107
+ cols = {row[1]: row for row in cursor.fetchall()} # name -> info
108
+
109
+ # Columns to ensure exist: name -> SQL add column clause
110
+ required_columns = {
111
+ 'personality_type': "ALTER TABLE messages ADD COLUMN personality_type TEXT",
112
+ 'sender_type': "ALTER TABLE messages ADD COLUMN sender_type TEXT",
113
+ 'session_id': "ALTER TABLE messages ADD COLUMN session_id TEXT",
114
+ 'response_time': "ALTER TABLE messages ADD COLUMN response_time REAL",
115
+ 'timestamp': "ALTER TABLE messages ADD COLUMN timestamp DATETIME DEFAULT CURRENT_TIMESTAMP",
116
+ }
117
+
118
+ for name, alter_sql in required_columns.items():
119
+ if name not in cols:
120
+ try:
121
+ cursor.execute(alter_sql)
122
+ logger.info(f"Added missing column to messages table: {name}")
123
+ except Exception as e:
124
+ # If the column exists but PRAGMA didn't report it correctly or another race, log and continue
125
+ logger.warning(f"Could not add column '{name}' to messages: {e}")
126
+ conn.commit()
127
+ except Exception as e:
128
+ logger.error(f"Error ensuring messages columns: {e}")
129
+
130
+ def save_message(self, username: str, message: str, personality_type: str,
131
+ sender_type: str, session_id: str = None, response_time: float = None) -> bool:
132
+ """Save a message to the database"""
133
+ try:
134
+ with sqlite3.connect(self.db_path) as conn:
135
+ cursor = conn.cursor()
136
+
137
+ cursor.execute("""
138
+ INSERT INTO messages (username, message, personality_type, sender_type, session_id, response_time)
139
+ VALUES (?, ?, ?, ?, ?, ?)
140
+ """, (username, message, personality_type, sender_type, session_id, response_time))
141
+
142
+ conn.commit()
143
+ return True
144
+
145
+ except sqlite3.OperationalError as e:
146
+ # Auto-migrate if schema is missing expected columns, then retry once
147
+ if "no column named" in str(e).lower():
148
+ logger.warning(f"Schema issue detected ('{e}'). Attempting to migrate and retry insert...")
149
+ try:
150
+ with sqlite3.connect(self.db_path) as conn:
151
+ self._ensure_messages_columns(conn)
152
+ cursor = conn.cursor()
153
+ cursor.execute(
154
+ """
155
+ INSERT INTO messages (username, message, personality_type, sender_type, session_id, response_time)
156
+ VALUES (?, ?, ?, ?, ?, ?)
157
+ """,
158
+ (username, message, personality_type, sender_type, session_id, response_time),
159
+ )
160
+ conn.commit()
161
+ logger.info("Insert succeeded after schema migration")
162
+ return True
163
+ except Exception as e2:
164
+ logger.error(f"Retry after migration failed: {e2}")
165
+ return False
166
+ else:
167
+ logger.error(f"Operational DB error saving message: {e}")
168
+ return False
169
+ except Exception as e:
170
+ logger.error(f"Unexpected DB error saving message: {str(e)}")
171
+ return False
172
+
173
+ def get_recent_messages(self, personality_type: str = None, limit: int = 50) -> List[Dict[str, Any]]:
174
+ """Get recent messages, optionally filtered by personality type"""
175
+ try:
176
+ with sqlite3.connect(self.db_path) as conn:
177
+ cursor = conn.cursor()
178
+
179
+ if personality_type:
180
+ cursor.execute("""
181
+ SELECT username, message, personality_type, sender_type, timestamp
182
+ FROM messages
183
+ WHERE personality_type = ?
184
+ ORDER BY timestamp DESC
185
+ LIMIT ?
186
+ """, (personality_type, limit))
187
+ else:
188
+ cursor.execute("""
189
+ SELECT username, message, personality_type, sender_type, timestamp
190
+ FROM messages
191
+ ORDER BY timestamp DESC
192
+ LIMIT ?
193
+ """, (limit,))
194
+
195
+ messages = []
196
+ for row in cursor.fetchall():
197
+ messages.append({
198
+ 'username': row[0],
199
+ 'message': row[1],
200
+ 'personality_type': row[2],
201
+ 'sender_type': row[3],
202
+ 'timestamp': row[4]
203
+ })
204
+
205
+ return messages
206
+
207
+ except Exception as e:
208
+ logger.error(f"Error retrieving messages: {str(e)}")
209
+ return []
210
+
211
+ def clear_personality_chat(self, personality_type: str, username: str = None) -> bool:
212
+ """Clear chat history for a personality (optionally for a specific user)"""
213
+ try:
214
+ with sqlite3.connect(self.db_path) as conn:
215
+ cursor = conn.cursor()
216
+
217
+ if username:
218
+ cursor.execute("""
219
+ DELETE FROM messages
220
+ WHERE personality_type = ? AND username = ?
221
+ """, (personality_type, username))
222
+ else:
223
+ cursor.execute("""
224
+ DELETE FROM messages
225
+ WHERE personality_type = ?
226
+ """, (personality_type,))
227
+
228
+ conn.commit()
229
+ return True
230
+
231
+ except Exception as e:
232
+ logger.error(f"Error clearing chat: {str(e)}")
233
+ return False
234
+
235
+ def get_personality_stats(self, personality_type: str) -> Dict[str, Any]:
236
+ """Get statistics for a specific personality"""
237
+ try:
238
+ with sqlite3.connect(self.db_path) as conn:
239
+ cursor = conn.cursor()
240
+
241
+ # Get message count
242
+ cursor.execute("""
243
+ SELECT COUNT(*) FROM messages
244
+ WHERE personality_type = ? AND sender_type = 'bot'
245
+ """, (personality_type,))
246
+
247
+ message_count = cursor.fetchone()[0]
248
+
249
+ # Get average response time
250
+ cursor.execute("""
251
+ SELECT AVG(response_time) FROM messages
252
+ WHERE personality_type = ? AND sender_type = 'bot' AND response_time IS NOT NULL
253
+ """, (personality_type,))
254
+
255
+ avg_response_time = cursor.fetchone()[0] or 0.0
256
+
257
+ return {
258
+ 'personality_type': personality_type,
259
+ 'total_messages': message_count,
260
+ 'avg_response_time': round(avg_response_time, 2)
261
+ }
262
+
263
+ except Exception as e:
264
+ logger.error(f"Error getting personality stats: {str(e)}")
265
+ return {
266
+ 'personality_type': personality_type,
267
+ 'total_messages': 0,
268
+ 'avg_response_time': 0.0
269
+ }
270
+
271
+ def get_all_stats(self) -> Dict[str, Any]:
272
+ """Get overall statistics"""
273
+ try:
274
+ with sqlite3.connect(self.db_path) as conn:
275
+ cursor = conn.cursor()
276
+
277
+ # Total messages
278
+ cursor.execute("SELECT COUNT(*) FROM messages")
279
+ total_messages = cursor.fetchone()[0]
280
+
281
+ # Messages by personality
282
+ cursor.execute("""
283
+ SELECT personality_type, COUNT(*)
284
+ FROM messages
285
+ GROUP BY personality_type
286
+ """)
287
+
288
+ personality_breakdown = dict(cursor.fetchall())
289
+
290
+ return {
291
+ 'total_messages': total_messages,
292
+ 'personality_breakdown': personality_breakdown
293
+ }
294
+
295
+ except Exception as e:
296
+ logger.error(f"Error getting all stats: {str(e)}")
297
+ return {
298
+ 'total_messages': 0,
299
+ 'personality_breakdown': {}
300
+ }