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/__init__.py +6 -0
- app/api/__init__.py +1 -0
- app/api/anthropic.py +92 -0
- app/api/base.py +74 -0
- app/api/ollama.py +116 -0
- app/api/openai.py +78 -0
- app/config.py +127 -0
- app/database.py +285 -0
- app/main.py +599 -0
- app/models.py +83 -0
- app/ui/__init__.py +1 -0
- app/ui/chat_interface.py +345 -0
- app/ui/chat_list.py +336 -0
- app/ui/model_selector.py +296 -0
- app/ui/search.py +308 -0
- app/ui/styles.py +275 -0
- app/utils.py +202 -0
- chat_console-0.1.1.dist-info/LICENSE +21 -0
- chat_console-0.1.1.dist-info/METADATA +111 -0
- chat_console-0.1.1.dist-info/RECORD +23 -0
- chat_console-0.1.1.dist-info/WHEEL +5 -0
- chat_console-0.1.1.dist-info/entry_points.txt +3 -0
- chat_console-0.1.1.dist-info/top_level.txt +1 -0
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()
|