Spaces:
Runtime error
Runtime error
File size: 6,478 Bytes
07ab316 | 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 | """
SQLite session store for the REIC demo UI.
Schema
------
sessions : one row per browser session
turns : one row per user/assistant turn, linked to session
Auto-expiry
-----------
Sessions inactive for more than SESSION_TTL seconds are deleted.
cleanup_loop() runs in the background every CLEANUP_INTERVAL seconds.
"""
import asyncio
import json
import logging
import time
import uuid
from pathlib import Path
import aiosqlite
logger = logging.getLogger(__name__)
DB_PATH = Path(__file__).parent / "sessions.db"
SESSION_TTL = 3600 # 1 hour in seconds
CLEANUP_INTERVAL = 300 # run cleanup every 5 minutes
# ---------------------------------------------------------------------------
# Schema bootstrap
# ---------------------------------------------------------------------------
async def init_db() -> None:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("""
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
created_at REAL NOT NULL,
last_active REAL NOT NULL
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS turns (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
metadata TEXT,
ts REAL NOT NULL
)
""")
await db.execute(
"CREATE INDEX IF NOT EXISTS idx_turns_session ON turns(session_id)"
)
await db.commit()
logger.info("DB initialised at %s", DB_PATH)
# ---------------------------------------------------------------------------
# Session lifecycle
# ---------------------------------------------------------------------------
async def create_session() -> str:
sid = str(uuid.uuid4())
now = time.time()
async with aiosqlite.connect(DB_PATH) as db:
await db.execute(
"INSERT INTO sessions (id, created_at, last_active) VALUES (?, ?, ?)",
(sid, now, now),
)
await db.commit()
return sid
async def touch_session(db: aiosqlite.Connection, session_id: str) -> None:
await db.execute(
"UPDATE sessions SET last_active = ? WHERE id = ?",
(time.time(), session_id),
)
async def session_exists(session_id: str) -> bool:
async with aiosqlite.connect(DB_PATH) as db:
async with db.execute(
"SELECT last_active FROM sessions WHERE id = ?", (session_id,)
) as cur:
row = await cur.fetchone()
if not row:
return False
if time.time() - row[0] > SESSION_TTL:
await _delete_session(db, session_id)
await db.commit()
return False
return True
# ---------------------------------------------------------------------------
# Turn storage & retrieval
# ---------------------------------------------------------------------------
async def append_turn(
session_id: str,
role: str,
content: str,
metadata: dict | None = None,
) -> None:
"""Add a user or assistant turn and refresh session TTL."""
async with aiosqlite.connect(DB_PATH) as db:
await db.execute(
"INSERT INTO turns (session_id, role, content, metadata, ts) VALUES (?, ?, ?, ?, ?)",
(session_id, role, content, json.dumps(metadata) if metadata else None, time.time()),
)
await touch_session(db, session_id)
await db.commit()
async def get_history(session_id: str) -> list[dict] | None:
"""
Return the turn list for a session, or None if the session is gone/expired.
Each element is a dict with at least {role, content}.
Assistant turns also include intent_classified (for multi-turn pinning)
and display fields (confidence, rag_examples, matched_categories, mode, turn_mode).
"""
async with aiosqlite.connect(DB_PATH) as db:
async with db.execute(
"SELECT last_active FROM sessions WHERE id = ?", (session_id,)
) as cur:
row = await cur.fetchone()
if not row:
return None
if time.time() - row[0] > SESSION_TTL:
await _delete_session(db, session_id)
await db.commit()
return None
async with db.execute(
"SELECT role, content, metadata FROM turns WHERE session_id = ? ORDER BY ts",
(session_id,),
) as cur:
rows = await cur.fetchall()
turns = []
for role, content, meta_str in rows:
turn: dict = {"role": role, "content": content}
if meta_str:
turn.update(json.loads(meta_str))
turns.append(turn)
return turns
async def clear_session_turns(session_id: str) -> None:
"""Delete all turns for a session (keeps the session row, resets TTL)."""
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("DELETE FROM turns WHERE session_id = ?", (session_id,))
await touch_session(db, session_id)
await db.commit()
# ---------------------------------------------------------------------------
# Cleanup
# ---------------------------------------------------------------------------
async def _delete_session(db: aiosqlite.Connection, session_id: str) -> None:
await db.execute("DELETE FROM turns WHERE session_id = ?", (session_id,))
await db.execute("DELETE FROM sessions WHERE id = ?", (session_id,))
async def cleanup_expired() -> int:
"""Delete all sessions inactive for more than SESSION_TTL. Returns count deleted."""
cutoff = time.time() - SESSION_TTL
async with aiosqlite.connect(DB_PATH) as db:
async with db.execute(
"SELECT id FROM sessions WHERE last_active < ?", (cutoff,)
) as cur:
expired = [r[0] for r in await cur.fetchall()]
for sid in expired:
await _delete_session(db, sid)
await db.commit()
if expired:
logger.info("Cleaned up %d expired session(s).", len(expired))
return len(expired)
async def cleanup_loop() -> None:
"""Background coroutine: run cleanup_expired() every CLEANUP_INTERVAL seconds."""
while True:
await asyncio.sleep(CLEANUP_INTERVAL)
try:
await cleanup_expired()
except Exception:
logger.exception("Error during session cleanup")
|