Spaces:
Sleeping
Sleeping
| """ | |
| SQLite-backed persistent Q&A history. | |
| Thread-safe (uses a connection per call + check_same_thread=False pattern). | |
| Automatically creates the schema on first use. | |
| """ | |
| import logging | |
| import sqlite3 | |
| import threading | |
| from contextlib import contextmanager | |
| from pathlib import Path | |
| from typing import Optional | |
| from . import config | |
| logger = logging.getLogger(__name__) | |
| _init_lock = threading.Lock() | |
| _initialized = False | |
| def _ensure_parent_dir(path: str) -> None: | |
| p = Path(path).resolve() | |
| p.parent.mkdir(parents=True, exist_ok=True) | |
| def _connect(): | |
| """Yield a short-lived SQLite connection with row access by name.""" | |
| conn = sqlite3.connect(config.DB_PATH, timeout=10, check_same_thread=False) | |
| conn.row_factory = sqlite3.Row | |
| try: | |
| yield conn | |
| conn.commit() | |
| finally: | |
| conn.close() | |
| def init_db() -> None: | |
| """Create tables if they don't exist. Idempotent and thread-safe.""" | |
| global _initialized | |
| with _init_lock: | |
| if _initialized: | |
| return | |
| _ensure_parent_dir(config.DB_PATH) | |
| with _connect() as conn: | |
| conn.executescript( | |
| """ | |
| CREATE TABLE IF NOT EXISTS qa_history ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| created_at TEXT NOT NULL DEFAULT (datetime('now')), | |
| source_url TEXT, | |
| source_type TEXT, | |
| product_title TEXT, | |
| question TEXT NOT NULL, | |
| answer TEXT NOT NULL, | |
| confidence REAL NOT NULL, | |
| confidence_level TEXT NOT NULL, | |
| inference_ms INTEGER | |
| ); | |
| CREATE INDEX IF NOT EXISTS idx_history_created | |
| ON qa_history(created_at DESC); | |
| """ | |
| ) | |
| _initialized = True | |
| logger.info(f"SQLite history ready at {config.DB_PATH}") | |
| def save_qa( | |
| question: str, | |
| answer: str, | |
| confidence: float, | |
| confidence_level: str, | |
| inference_ms: int, | |
| source_url: Optional[str] = None, | |
| source_type: Optional[str] = None, | |
| product_title: Optional[str] = None, | |
| ) -> int: | |
| init_db() | |
| with _connect() as conn: | |
| cur = conn.execute( | |
| """ | |
| INSERT INTO qa_history | |
| (source_url, source_type, product_title, | |
| question, answer, confidence, confidence_level, inference_ms) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?) | |
| """, | |
| (source_url, source_type, product_title, | |
| question, answer, confidence, confidence_level, inference_ms), | |
| ) | |
| return cur.lastrowid | |
| def list_history(limit: int = None) -> list: | |
| init_db() | |
| limit = limit or config.HISTORY_LIMIT | |
| with _connect() as conn: | |
| rows = conn.execute( | |
| """ | |
| SELECT id, created_at, source_url, source_type, product_title, | |
| question, answer, confidence, confidence_level, inference_ms | |
| FROM qa_history | |
| ORDER BY id DESC | |
| LIMIT ? | |
| """, | |
| (limit,), | |
| ).fetchall() | |
| return [dict(r) for r in rows] | |
| def delete_entry(entry_id: int) -> bool: | |
| init_db() | |
| with _connect() as conn: | |
| cur = conn.execute("DELETE FROM qa_history WHERE id = ?", (entry_id,)) | |
| return cur.rowcount > 0 | |
| def clear_history() -> int: | |
| init_db() | |
| with _connect() as conn: | |
| cur = conn.execute("DELETE FROM qa_history") | |
| return cur.rowcount | |