chat-console 0.1.1__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.
app/database.py ADDED
@@ -0,0 +1,285 @@
1
+ import sqlite3
2
+ import json
3
+ from datetime import datetime
4
+ from typing import List, Dict, Any, Optional
5
+ from .config import DB_PATH
6
+
7
+ def init_db():
8
+ """Initialize the database with required tables"""
9
+ conn = sqlite3.connect(DB_PATH)
10
+ cursor = conn.cursor()
11
+
12
+ # Create tables
13
+ cursor.execute('''
14
+ CREATE TABLE IF NOT EXISTS conversations (
15
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16
+ title TEXT NOT NULL,
17
+ model TEXT NOT NULL,
18
+ created_at TIMESTAMP NOT NULL,
19
+ updated_at TIMESTAMP NOT NULL,
20
+ style TEXT NOT NULL DEFAULT 'default',
21
+ tags TEXT
22
+ )
23
+ ''')
24
+
25
+ cursor.execute('''
26
+ CREATE TABLE IF NOT EXISTS messages (
27
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
28
+ conversation_id INTEGER NOT NULL,
29
+ role TEXT NOT NULL,
30
+ content TEXT NOT NULL,
31
+ timestamp TIMESTAMP NOT NULL,
32
+ FOREIGN KEY (conversation_id) REFERENCES conversations (id) ON DELETE CASCADE
33
+ )
34
+ ''')
35
+
36
+ # Create indexes and full-text search
37
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_conversation_id ON messages (conversation_id)')
38
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_conversations_created ON conversations (created_at)')
39
+ cursor.execute('CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(content, content=messages, content_rowid=id)')
40
+
41
+ # Create FTS triggers
42
+ cursor.execute('''
43
+ CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
44
+ INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
45
+ END
46
+ ''')
47
+
48
+ cursor.execute('''
49
+ CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN
50
+ INSERT INTO messages_fts(messages_fts, rowid, content) VALUES('delete', old.id, old.content);
51
+ INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
52
+ END
53
+ ''')
54
+
55
+ cursor.execute('''
56
+ CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN
57
+ INSERT INTO messages_fts(messages_fts, rowid, content) VALUES('delete', old.id, old.content);
58
+ END
59
+ ''')
60
+
61
+ conn.commit()
62
+ conn.close()
63
+
64
+ class ChatDatabase:
65
+ def __init__(self):
66
+ init_db()
67
+
68
+ def _get_connection(self):
69
+ """Get a database connection with row factory"""
70
+ conn = sqlite3.connect(DB_PATH)
71
+ conn.row_factory = sqlite3.Row
72
+ return conn
73
+
74
+ def create_conversation(self, title: str, model: str, style: str = "default", tags: List[str] = None) -> int:
75
+ """Create a new conversation and return its ID"""
76
+ now = datetime.now().isoformat()
77
+ conn = self._get_connection()
78
+ cursor = conn.cursor()
79
+
80
+ tags_json = json.dumps(tags) if tags else None
81
+
82
+ cursor.execute(
83
+ 'INSERT INTO conversations (title, model, created_at, updated_at, style, tags) VALUES (?, ?, ?, ?, ?, ?)',
84
+ (title, model, now, now, style, tags_json)
85
+ )
86
+ conversation_id = cursor.lastrowid
87
+ conn.commit()
88
+ conn.close()
89
+
90
+ return conversation_id
91
+
92
+ def add_message(self, conversation_id: int, role: str, content: str) -> int:
93
+ """Add a message to a conversation and return the message ID"""
94
+ now = datetime.now().isoformat()
95
+ conn = self._get_connection()
96
+ cursor = conn.cursor()
97
+
98
+ # Add the message
99
+ cursor.execute(
100
+ 'INSERT INTO messages (conversation_id, role, content, timestamp) VALUES (?, ?, ?, ?)',
101
+ (conversation_id, role, content, now)
102
+ )
103
+ message_id = cursor.lastrowid
104
+
105
+ # Update the conversation's updated_at timestamp
106
+ cursor.execute(
107
+ 'UPDATE conversations SET updated_at = ? WHERE id = ?',
108
+ (now, conversation_id)
109
+ )
110
+
111
+ conn.commit()
112
+ conn.close()
113
+
114
+ return message_id
115
+
116
+ def get_conversation(self, conversation_id: int) -> Dict[str, Any]:
117
+ """Get a conversation by ID, including all messages"""
118
+ conn = self._get_connection()
119
+ cursor = conn.cursor()
120
+
121
+ # Get conversation data
122
+ cursor.execute('SELECT * FROM conversations WHERE id = ?', (conversation_id,))
123
+ conversation_row = cursor.fetchone()
124
+
125
+ if not conversation_row:
126
+ conn.close()
127
+ return None
128
+
129
+ conversation = dict(conversation_row)
130
+
131
+ # Get all messages for this conversation
132
+ cursor.execute('SELECT * FROM messages WHERE conversation_id = ? ORDER BY timestamp', (conversation_id,))
133
+ messages = [dict(row) for row in cursor.fetchall()]
134
+
135
+ conversation['messages'] = messages
136
+
137
+ # Parse tags if present
138
+ if conversation['tags']:
139
+ conversation['tags'] = json.loads(conversation['tags'])
140
+ else:
141
+ conversation['tags'] = []
142
+
143
+ conn.close()
144
+ return conversation
145
+
146
+ def get_all_conversations(self, limit: int = 100, offset: int = 0) -> List[Dict[str, Any]]:
147
+ """Get all conversations with pagination"""
148
+ conn = self._get_connection()
149
+ cursor = conn.cursor()
150
+
151
+ cursor.execute(
152
+ 'SELECT * FROM conversations ORDER BY updated_at DESC LIMIT ? OFFSET ?',
153
+ (limit, offset)
154
+ )
155
+
156
+ conversations = []
157
+ for row in cursor.fetchall():
158
+ conversation = dict(row)
159
+ if conversation['tags']:
160
+ conversation['tags'] = json.loads(conversation['tags'])
161
+ else:
162
+ conversation['tags'] = []
163
+
164
+ # Get message count
165
+ cursor.execute(
166
+ 'SELECT COUNT(*) as count FROM messages WHERE conversation_id = ?',
167
+ (conversation['id'],)
168
+ )
169
+ count_row = cursor.fetchone()
170
+ conversation['message_count'] = count_row['count']
171
+
172
+ conversations.append(conversation)
173
+
174
+ conn.close()
175
+ return conversations
176
+
177
+ def search_conversations(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:
178
+ """Search for conversations containing the query in messages"""
179
+ conn = self._get_connection()
180
+ cursor = conn.cursor()
181
+ results = []
182
+
183
+ try:
184
+ # First get matching message IDs using FTS
185
+ cursor.execute('''
186
+ SELECT DISTINCT conversation_id, content as matched_content
187
+ FROM messages_fts
188
+ JOIN messages ON messages_fts.rowid = messages.id
189
+ WHERE messages_fts MATCH ?
190
+ ORDER BY rank
191
+ LIMIT ?
192
+ ''', (query, limit))
193
+
194
+ matching_messages = cursor.fetchall()
195
+ if not matching_messages:
196
+ return []
197
+
198
+ # Get conversation IDs and their matched content
199
+ conv_ids = [row[0] for row in matching_messages]
200
+ matched_contents = {row[0]: row[1] for row in matching_messages}
201
+
202
+ # Get conversation data efficiently
203
+ placeholders = ','.join('?' * len(conv_ids))
204
+ cursor.execute(f'''
205
+ SELECT c.*, COUNT(m.id) as message_count
206
+ FROM conversations c
207
+ LEFT JOIN messages m ON c.id = m.conversation_id
208
+ WHERE c.id IN ({placeholders})
209
+ GROUP BY c.id
210
+ ORDER BY c.updated_at DESC
211
+ ''', conv_ids)
212
+
213
+ for row in cursor.fetchall():
214
+ conversation = dict(row)
215
+ if conversation['tags']:
216
+ conversation['tags'] = json.loads(conversation['tags'])
217
+ else:
218
+ conversation['tags'] = []
219
+
220
+ # Use the matched content from FTS results
221
+ conversation['preview'] = matched_contents.get(conversation['id'], '')
222
+ conversation['message_count'] = row['message_count']
223
+
224
+ results.append(conversation)
225
+
226
+ except sqlite3.Error as e:
227
+ print(f"Database error during search: {e}")
228
+ except Exception as e:
229
+ print(f"Error during search: {e}")
230
+ finally:
231
+ conn.close()
232
+
233
+ return results
234
+
235
+ def update_conversation(self, conversation_id: int, title: str = None, tags: List[str] = None, style: str = None, model: str = None):
236
+ """Update conversation metadata"""
237
+ conn = self._get_connection()
238
+ cursor = conn.cursor()
239
+
240
+ updates = []
241
+ params = []
242
+
243
+ if title is not None:
244
+ updates.append("title = ?")
245
+ params.append(title)
246
+
247
+ if tags is not None:
248
+ updates.append("tags = ?")
249
+ params.append(json.dumps(tags))
250
+
251
+ if style is not None:
252
+ updates.append("style = ?")
253
+ params.append(style)
254
+
255
+ if model is not None:
256
+ updates.append("model = ?")
257
+ params.append(model)
258
+
259
+ if not updates:
260
+ conn.close()
261
+ return
262
+
263
+ # Add updated_at
264
+ updates.append("updated_at = ?")
265
+ params.append(datetime.now().isoformat())
266
+
267
+ # Add conversation_id
268
+ params.append(conversation_id)
269
+
270
+ query = f"UPDATE conversations SET {', '.join(updates)} WHERE id = ?"
271
+ cursor.execute(query, params)
272
+
273
+ conn.commit()
274
+ conn.close()
275
+
276
+ def delete_conversation(self, conversation_id: int):
277
+ """Delete a conversation and all its messages"""
278
+ conn = self._get_connection()
279
+ cursor = conn.cursor()
280
+
281
+ # Messages will be deleted via ON DELETE CASCADE
282
+ cursor.execute('DELETE FROM conversations WHERE id = ?', (conversation_id,))
283
+
284
+ conn.commit()
285
+ conn.close()