AI-Study-Companion / database.py
Youssef1F's picture
Upload database.py with huggingface_hub
d92f275 verified
Raw History Blame Contribute Delete
11.5 kB
# ============================================
# database.py
# SQLite persistence for study sessions
# ============================================
import os
import json
import sqlite3
import logging
from datetime import datetime
logger = logging.getLogger(__name__)
DB_PATH = os.path.join("data", "study_companion.db")
def get_connection():
"""Get a database connection, creating tables if needed."""
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
_create_tables(conn)
return conn
def _create_tables(conn):
"""Create all tables if they don't exist."""
conn.executescript("""
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
document_name TEXT,
document_text TEXT,
summary TEXT,
keywords TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS quiz_results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT,
score INTEGER,
total INTEGER,
percentage REAL,
grade TEXT,
difficulty TEXT,
details TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (session_id) REFERENCES sessions(id)
);
CREATE TABLE IF NOT EXISTS qa_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT,
question TEXT,
answer TEXT,
confidence REAL,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (session_id) REFERENCES sessions(id)
);
CREATE TABLE IF NOT EXISTS flashcards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT,
term TEXT,
definition TEXT,
category TEXT DEFAULT 'general',
mastery_level INTEGER DEFAULT 0,
next_review TEXT,
review_count INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (session_id) REFERENCES sessions(id)
);
CREATE TABLE IF NOT EXISTS study_progress (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT,
action_type TEXT,
details TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (session_id) REFERENCES sessions(id)
);
""")
conn.commit()
# ---- Session CRUD ----
def save_session(session_id, document_name, document_text, summary=None, keywords=None):
"""Save or update a study session."""
conn = get_connection()
try:
conn.execute("""
INSERT INTO sessions (id, document_name, document_text, summary, keywords, updated_at)
VALUES (?, ?, ?, ?, ?, datetime('now'))
ON CONFLICT(id) DO UPDATE SET
document_name=excluded.document_name,
document_text=excluded.document_text,
summary=COALESCE(excluded.summary, sessions.summary),
keywords=COALESCE(excluded.keywords, sessions.keywords),
updated_at=datetime('now')
""", (session_id, document_name, document_text,
summary, json.dumps(keywords) if keywords else None))
conn.commit()
finally:
conn.close()
def load_session(session_id):
"""Load a session from database."""
conn = get_connection()
try:
row = conn.execute("SELECT * FROM sessions WHERE id = ?", (session_id,)).fetchone()
if row:
return dict(row)
return None
finally:
conn.close()
def get_all_sessions():
"""Get all sessions (for dashboard)."""
conn = get_connection()
try:
rows = conn.execute(
"SELECT id, document_name, created_at, updated_at FROM sessions ORDER BY updated_at DESC"
).fetchall()
return [dict(r) for r in rows]
finally:
conn.close()
def delete_session(session_id):
"""Delete a session and all related data."""
conn = get_connection()
try:
conn.execute("DELETE FROM flashcards WHERE session_id = ?", (session_id,))
conn.execute("DELETE FROM quiz_results WHERE session_id = ?", (session_id,))
conn.execute("DELETE FROM qa_history WHERE session_id = ?", (session_id,))
conn.execute("DELETE FROM study_progress WHERE session_id = ?", (session_id,))
conn.execute("DELETE FROM sessions WHERE id = ?", (session_id,))
conn.commit()
finally:
conn.close()
# ---- Quiz Results ----
def save_quiz_result(session_id, score, total, percentage, grade, difficulty, details):
"""Save a quiz result."""
conn = get_connection()
try:
conn.execute("""
INSERT INTO quiz_results (session_id, score, total, percentage, grade, difficulty, details)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (session_id, score, total, percentage, grade, difficulty, json.dumps(details)))
conn.commit()
_log_progress(conn, session_id, "quiz", f"Score: {score}/{total} ({grade})")
finally:
conn.close()
def get_quiz_history(session_id=None):
"""Get quiz history, optionally filtered by session."""
conn = get_connection()
try:
if session_id:
rows = conn.execute(
"SELECT * FROM quiz_results WHERE session_id = ? ORDER BY created_at DESC", (session_id,)
).fetchall()
else:
rows = conn.execute(
"SELECT * FROM quiz_results ORDER BY created_at DESC LIMIT 50"
).fetchall()
return [dict(r) for r in rows]
finally:
conn.close()
# ---- Q&A History ----
def save_qa_entry(session_id, question, answer, confidence):
"""Save a Q&A interaction."""
conn = get_connection()
try:
conn.execute("""
INSERT INTO qa_history (session_id, question, answer, confidence)
VALUES (?, ?, ?, ?)
""", (session_id, question, answer, confidence))
conn.commit()
finally:
conn.close()
# ---- Flashcards ----
def save_flashcards(session_id, flashcards):
"""Save a list of flashcards."""
conn = get_connection()
try:
now = datetime.now().isoformat()
for fc in flashcards:
conn.execute("""
INSERT INTO flashcards (session_id, term, definition, category, next_review)
VALUES (?, ?, ?, ?, ?)
""", (session_id, fc.get("term", ""), fc.get("definition", ""),
fc.get("category", "general"), now))
conn.commit()
_log_progress(conn, session_id, "flashcards", f"Generated {len(flashcards)} flashcards")
finally:
conn.close()
def get_flashcards(session_id):
"""Get all flashcards for a session."""
conn = get_connection()
try:
rows = conn.execute(
"SELECT * FROM flashcards WHERE session_id = ? ORDER BY mastery_level ASC, id ASC",
(session_id,)
).fetchall()
return [dict(r) for r in rows]
finally:
conn.close()
def get_due_flashcards(session_id):
"""Get flashcards due for review (spaced repetition)."""
conn = get_connection()
try:
now = datetime.now().isoformat()
rows = conn.execute("""
SELECT * FROM flashcards
WHERE session_id = ? AND (next_review <= ? OR next_review IS NULL)
ORDER BY mastery_level ASC, review_count ASC
""", (session_id, now)).fetchall()
return [dict(r) for r in rows]
finally:
conn.close()
def update_flashcard_mastery(flashcard_id, correct):
"""Update flashcard after review (spaced repetition algorithm)."""
conn = get_connection()
try:
row = conn.execute("SELECT * FROM flashcards WHERE id = ?", (flashcard_id,)).fetchone()
if not row:
return
mastery = row["mastery_level"]
review_count = row["review_count"] + 1
if correct:
mastery = min(mastery + 1, 5)
else:
mastery = max(mastery - 1, 0)
# Spaced repetition intervals (in hours)
intervals = {0: 0.5, 1: 4, 2: 24, 3: 72, 4: 168, 5: 720}
hours = intervals.get(mastery, 24)
from datetime import timedelta
next_review = (datetime.now() + timedelta(hours=hours)).isoformat()
conn.execute("""
UPDATE flashcards SET mastery_level = ?, review_count = ?, next_review = ?
WHERE id = ?
""", (mastery, review_count, next_review, flashcard_id))
conn.commit()
finally:
conn.close()
# ---- Study Progress ----
def _log_progress(conn, session_id, action_type, details):
"""Internal: log a study action."""
conn.execute("""
INSERT INTO study_progress (session_id, action_type, details)
VALUES (?, ?, ?)
""", (session_id, action_type, details))
conn.commit()
def get_study_stats(session_id=None):
"""Get overall study statistics for the dashboard."""
conn = get_connection()
try:
stats = {}
# Total sessions
stats["total_sessions"] = conn.execute("SELECT COUNT(*) FROM sessions").fetchone()[0]
# Quiz stats
if session_id:
quiz_rows = conn.execute(
"SELECT * FROM quiz_results WHERE session_id = ? ORDER BY created_at DESC",
(session_id,)
).fetchall()
else:
quiz_rows = conn.execute(
"SELECT * FROM quiz_results ORDER BY created_at DESC"
).fetchall()
if quiz_rows:
pcts = [r["percentage"] for r in quiz_rows]
stats["total_quizzes"] = len(quiz_rows)
stats["average_score"] = round(sum(pcts) / len(pcts), 1)
stats["best_score"] = max(pcts)
stats["worst_score"] = min(pcts)
stats["total_questions"] = sum(r["total"] for r in quiz_rows)
stats["total_correct"] = sum(r["score"] for r in quiz_rows)
# Score history for chart
stats["score_history"] = [
{"date": r["created_at"], "score": r["percentage"],
"grade": r["grade"], "difficulty": r["difficulty"]}
for r in quiz_rows[:20]
]
else:
stats["total_quizzes"] = 0
stats["average_score"] = 0
stats["score_history"] = []
# Flashcard stats
if session_id:
fc_rows = conn.execute(
"SELECT mastery_level, COUNT(*) as cnt FROM flashcards WHERE session_id = ? GROUP BY mastery_level",
(session_id,)
).fetchall()
else:
fc_rows = conn.execute(
"SELECT mastery_level, COUNT(*) as cnt FROM flashcards GROUP BY mastery_level"
).fetchall()
stats["flashcard_mastery"] = {str(r["mastery_level"]): r["cnt"] for r in fc_rows}
stats["total_flashcards"] = sum(r["cnt"] for r in fc_rows)
# QA stats
qa_count = conn.execute(
"SELECT COUNT(*) FROM qa_history" + (" WHERE session_id = ?" if session_id else ""),
(session_id,) if session_id else ()
).fetchone()[0]
stats["total_questions_asked"] = qa_count
return stats
finally:
conn.close()