-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocument_db.py
More file actions
415 lines (364 loc) · 16.6 KB
/
Copy pathdocument_db.py
File metadata and controls
415 lines (364 loc) · 16.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
"""
ThreadBear Document Database Manager
SQLite-based storage for document metadata, sections, highlights, summaries, and tags.
"""
from __future__ import annotations
import sqlite3
import os
from datetime import datetime
from typing import List, Dict, Any, Optional
from contextlib import contextmanager
class DocumentDatabase:
def __init__(self, db_path: str = "threadbear_docs.db"):
self.db_path = db_path
self._init_database()
@contextmanager
def _get_connection(self):
"""Context manager for database connections."""
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row # Return rows as dicts
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def _init_database(self):
"""Create tables if they don't exist."""
with self._get_connection() as conn:
cursor = conn.cursor()
# Core document info
cursor.execute("""
CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
file_type TEXT,
hash TEXT,
total_tokens INTEGER,
analysis_level TEXT DEFAULT 'quick',
created_at TEXT,
updated_at TEXT
)
""")
# Auto-detected sections (chapters, pages, headers)
cursor.execute("""
CREATE TABLE IF NOT EXISTS sections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
doc_id TEXT NOT NULL,
idx INTEGER,
title TEXT,
start_pos INTEGER,
end_pos INTEGER,
tokens INTEGER,
FOREIGN KEY (doc_id) REFERENCES documents(id) ON DELETE CASCADE
)
""")
# AI-generated section summaries
cursor.execute("""
CREATE TABLE IF NOT EXISTS section_summaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
section_id INTEGER NOT NULL,
summary TEXT,
tokens INTEGER,
model TEXT,
created_at TEXT,
FOREIGN KEY (section_id) REFERENCES sections(id) ON DELETE CASCADE
)
""")
# User text selections (highlights)
cursor.execute("""
CREATE TABLE IF NOT EXISTS highlights (
id TEXT PRIMARY KEY,
doc_id TEXT NOT NULL,
start_pos INTEGER,
end_pos INTEGER,
label TEXT,
tokens INTEGER,
created_at TEXT,
FOREIGN KEY (doc_id) REFERENCES documents(id) ON DELETE CASCADE
)
""")
# Document-level summaries and analysis
cursor.execute("""
CREATE TABLE IF NOT EXISTS doc_summaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
doc_id TEXT NOT NULL,
summary_type TEXT,
content TEXT,
tokens INTEGER,
model TEXT,
created_at TEXT,
FOREIGN KEY (doc_id) REFERENCES documents(id) ON DELETE CASCADE
)
""")
# Tags (auto-generated or user-added)
cursor.execute("""
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
doc_id TEXT NOT NULL,
tag TEXT,
source TEXT,
FOREIGN KEY (doc_id) REFERENCES documents(id) ON DELETE CASCADE
)
""")
# Track what's currently selected for context
cursor.execute("""
CREATE TABLE IF NOT EXISTS context_selections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
doc_id TEXT NOT NULL,
selection_type TEXT,
selection_id TEXT,
selected INTEGER DEFAULT 1,
FOREIGN KEY (doc_id) REFERENCES documents(id) ON DELETE CASCADE
)
""")
# Create indexes for common queries
cursor.execute("CREATE INDEX IF NOT EXISTS idx_sections_doc ON sections(doc_id)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_highlights_doc ON highlights(doc_id)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tags_doc ON tags(doc_id)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_doc_summaries_doc ON doc_summaries(doc_id)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_context_selections_doc ON context_selections(doc_id)")
# Enable foreign key support
cursor.execute("PRAGMA foreign_keys = ON")
# ========== Document CRUD ==========
def add_document(self, doc_id: str, name: str, file_type: str,
hash: str, total_tokens: int) -> bool:
"""Add a new document to the database."""
with self._get_connection() as conn:
cursor = conn.cursor()
now = datetime.now().isoformat()
cursor.execute("""
INSERT INTO documents (id, name, file_type, hash, total_tokens,
analysis_level, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, 'quick', ?, ?)
""", (doc_id, name, file_type, hash, total_tokens, now, now))
return True
def get_document(self, doc_id: str) -> Optional[Dict[str, Any]]:
"""Get document by ID."""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM documents WHERE id = ?", (doc_id,))
row = cursor.fetchone()
return dict(row) if row else None
def list_documents(self) -> List[Dict[str, Any]]:
"""List all documents."""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM documents ORDER BY updated_at DESC")
return [dict(row) for row in cursor.fetchall()]
def update_document(self, doc_id: str, **kwargs) -> bool:
"""Update document fields."""
if not kwargs:
return False
with self._get_connection() as conn:
cursor = conn.cursor()
fields = ", ".join(f"{k} = ?" for k in kwargs.keys())
values = list(kwargs.values()) + [datetime.now().isoformat(), doc_id]
cursor.execute(f"""
UPDATE documents SET {fields}, updated_at = ? WHERE id = ?
""", values)
return cursor.rowcount > 0
def delete_document(self, doc_id: str) -> bool:
"""Delete document and all related data (cascades)."""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("PRAGMA foreign_keys = ON")
cursor.execute("DELETE FROM documents WHERE id = ?", (doc_id,))
return cursor.rowcount > 0
# ========== Sections ==========
def add_section(self, doc_id: str, idx: int, title: str,
start_pos: int, end_pos: int, tokens: int) -> int:
"""Add a section to a document. Returns section ID."""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO sections (doc_id, idx, title, start_pos, end_pos, tokens)
VALUES (?, ?, ?, ?, ?, ?)
""", (doc_id, idx, title, start_pos, end_pos, tokens))
return cursor.lastrowid
def get_sections(self, doc_id: str) -> List[Dict[str, Any]]:
"""Get all sections for a document."""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT s.*, ss.summary, ss.tokens as summary_tokens, ss.model as summary_model
FROM sections s
LEFT JOIN section_summaries ss ON s.id = ss.section_id
WHERE s.doc_id = ?
ORDER BY s.idx
""", (doc_id,))
return [dict(row) for row in cursor.fetchall()]
def clear_sections(self, doc_id: str) -> bool:
"""Clear all sections for a document."""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM sections WHERE doc_id = ?", (doc_id,))
return True
# ========== Section Summaries ==========
def add_section_summary(self, section_id: int, summary: str,
tokens: int, model: str) -> int:
"""Add a summary for a section. Returns summary ID."""
with self._get_connection() as conn:
cursor = conn.cursor()
now = datetime.now().isoformat()
cursor.execute("""
INSERT INTO section_summaries (section_id, summary, tokens, model, created_at)
VALUES (?, ?, ?, ?, ?)
""", (section_id, summary, tokens, model, now))
return cursor.lastrowid
def get_section_summary(self, section_id: int) -> Optional[Dict[str, Any]]:
"""Get summary for a section."""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM section_summaries WHERE section_id = ?
""", (section_id,))
row = cursor.fetchone()
return dict(row) if row else None
# ========== Highlights ==========
def add_highlight(self, highlight_id: str, doc_id: str, start_pos: int,
end_pos: int, label: str, tokens: int) -> bool:
"""Add a user highlight/selection."""
with self._get_connection() as conn:
cursor = conn.cursor()
now = datetime.now().isoformat()
cursor.execute("""
INSERT INTO highlights (id, doc_id, start_pos, end_pos, label, tokens, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (highlight_id, doc_id, start_pos, end_pos, label, tokens, now))
return True
def get_highlights(self, doc_id: str) -> List[Dict[str, Any]]:
"""Get all highlights for a document."""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM highlights WHERE doc_id = ? ORDER BY start_pos
""", (doc_id,))
return [dict(row) for row in cursor.fetchall()]
def delete_highlight(self, highlight_id: str) -> bool:
"""Delete a highlight."""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM highlights WHERE id = ?", (highlight_id,))
return cursor.rowcount > 0
# ========== Document Summaries ==========
def add_doc_summary(self, doc_id: str, summary_type: str, content: str,
tokens: int, model: str) -> int:
"""Add a document-level summary. Returns summary ID."""
with self._get_connection() as conn:
cursor = conn.cursor()
now = datetime.now().isoformat()
cursor.execute("""
INSERT INTO doc_summaries (doc_id, summary_type, content, tokens, model, created_at)
VALUES (?, ?, ?, ?, ?, ?)
""", (doc_id, summary_type, content, tokens, model, now))
return cursor.lastrowid
def get_doc_summaries(self, doc_id: str, summary_type: str = None) -> List[Dict[str, Any]]:
"""Get document summaries, optionally filtered by type."""
with self._get_connection() as conn:
cursor = conn.cursor()
if summary_type:
cursor.execute("""
SELECT * FROM doc_summaries WHERE doc_id = ? AND summary_type = ?
ORDER BY created_at DESC
""", (doc_id, summary_type))
else:
cursor.execute("""
SELECT * FROM doc_summaries WHERE doc_id = ?
ORDER BY created_at DESC
""", (doc_id,))
return [dict(row) for row in cursor.fetchall()]
# ========== Tags ==========
def add_tag(self, doc_id: str, tag: str, source: str = 'user') -> int:
"""Add a tag to a document. Returns tag ID."""
with self._get_connection() as conn:
cursor = conn.cursor()
# Check if tag already exists
cursor.execute("""
SELECT id FROM tags WHERE doc_id = ? AND tag = ?
""", (doc_id, tag))
if cursor.fetchone():
return -1 # Tag already exists
cursor.execute("""
INSERT INTO tags (doc_id, tag, source)
VALUES (?, ?, ?)
""", (doc_id, tag, source))
return cursor.lastrowid
def get_tags(self, doc_id: str) -> List[Dict[str, Any]]:
"""Get all tags for a document."""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM tags WHERE doc_id = ? ORDER BY tag
""", (doc_id,))
return [dict(row) for row in cursor.fetchall()]
def delete_tag(self, doc_id: str, tag: str) -> bool:
"""Delete a tag from a document."""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM tags WHERE doc_id = ? AND tag = ?", (doc_id, tag))
return cursor.rowcount > 0
def get_documents_by_tag(self, tag: str) -> List[Dict[str, Any]]:
"""Find all documents with a specific tag."""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT d.* FROM documents d
JOIN tags t ON d.id = t.doc_id
WHERE t.tag = ?
ORDER BY d.updated_at DESC
""", (tag,))
return [dict(row) for row in cursor.fetchall()]
# ========== Context Selections ==========
def set_selection(self, doc_id: str, selection_type: str,
selection_id: str, selected: bool) -> bool:
"""Set selection state for a document item."""
with self._get_connection() as conn:
cursor = conn.cursor()
# Delete existing then insert (simpler than upsert)
cursor.execute("""
DELETE FROM context_selections
WHERE doc_id = ? AND selection_type = ? AND selection_id = ?
""", (doc_id, selection_type, selection_id))
if selected:
cursor.execute("""
INSERT INTO context_selections (doc_id, selection_type, selection_id, selected)
VALUES (?, ?, ?, 1)
""", (doc_id, selection_type, selection_id))
return True
def get_selections(self, doc_id: str) -> List[Dict[str, Any]]:
"""Get all selections for a document."""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM context_selections WHERE doc_id = ? AND selected = 1
""", (doc_id,))
return [dict(row) for row in cursor.fetchall()]
def clear_selections(self, doc_id: str) -> bool:
"""Clear all selections for a document."""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM context_selections WHERE doc_id = ?", (doc_id,))
return True
# ========== Utility Methods ==========
def get_full_document_data(self, doc_id: str) -> Optional[Dict[str, Any]]:
"""Get complete document data including sections, highlights, summaries, tags."""
doc = self.get_document(doc_id)
if not doc:
return None
doc['sections'] = self.get_sections(doc_id)
doc['highlights'] = self.get_highlights(doc_id)
doc['summaries'] = self.get_doc_summaries(doc_id)
doc['tags'] = self.get_tags(doc_id)
doc['selections'] = self.get_selections(doc_id)
return doc
def get_all_tags(self) -> List[str]:
"""Get all unique tags across all documents."""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT DISTINCT tag FROM tags ORDER BY tag")
return [row['tag'] for row in cursor.fetchall()]
# Global instance
document_db = DocumentDatabase()