invictatill-ai / memory.py
CI Bot
deploy: clean deploy with LFS binary tracking
550cb8d
Raw
History Blame Contribute Delete
44.5 kB
# core/memory.py
import sqlite3
from datetime import datetime
import threading
import os
import json
import secrets
import hashlib
import time
import re
from werkzeug.security import generate_password_hash, check_password_hash
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
class ConversationMemory:
def __init__(self, db_path=None):
self.db_path = db_path or "/data/invicta_data/memory.db"
os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
self.lock = threading.Lock()
self._init_tables()
def _get_connection(self):
"""Create a new SQLite connection with WAL mode and busy timeout."""
conn = sqlite3.connect(
self.db_path,
check_same_thread=False,
timeout=30.0,
isolation_level=None
)
conn.execute("PRAGMA foreign_keys = ON")
conn.execute("PRAGMA journal_mode = WAL")
conn.execute("PRAGMA synchronous = NORMAL")
conn.execute("PRAGMA temp_store = MEMORY")
conn.execute("PRAGMA mmap_size = 30000000000")
conn.execute("PRAGMA busy_timeout = 30000")
conn.row_factory = sqlite3.Row
return conn
def _get_cursor(self):
"""Legacy compatibility wrapper."""
conn = self._get_connection()
return conn, conn.cursor()
def _execute_with_retry(self, sql, params=(), max_retries=5, sleep_base=0.05):
"""Execute SQL with automatic retry on database lock."""
for attempt in range(max_retries):
conn = None
try:
conn = self._get_connection()
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute(sql, params)
conn.commit()
return cur
except sqlite3.OperationalError as e:
if conn:
try:
conn.rollback()
except Exception:
pass
if "database is locked" in str(e).lower() and attempt < max_retries - 1:
time.sleep(sleep_base * (attempt + 1))
continue
raise
finally:
if conn:
try:
conn.close()
except Exception:
pass
return None
def _init_tables(self):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
google_id TEXT,
email TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS auth_tokens (
token TEXT PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TEXT DEFAULT (datetime('now')),
last_used TEXT DEFAULT (datetime('now'))
);
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS chat_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS conversations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
session_id INTEGER REFERENCES chat_sessions(id) ON DELETE CASCADE,
role TEXT NOT NULL,
content TEXT NOT NULL,
timestamp TEXT DEFAULT (datetime('now'))
);
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS user_profile (
user_id INTEGER NOT NULL DEFAULT 0 REFERENCES users(id) ON DELETE CASCADE,
key TEXT NOT NULL,
value TEXT,
category TEXT DEFAULT 'general',
updated_at TEXT DEFAULT (datetime('now')),
PRIMARY KEY (user_id, key)
);
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS user_learning (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
category TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
confidence REAL DEFAULT 0.5,
times_observed INTEGER DEFAULT 1,
first_seen TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
UNIQUE(user_id, category, key)
);
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS user_insights (
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
preferred_formality TEXT DEFAULT 'casual',
preferred_response_length TEXT DEFAULT 'medium',
preferred_tone TEXT DEFAULT 'friendly',
topics_of_interest TEXT DEFAULT '[]',
communication_patterns TEXT DEFAULT '{}',
total_interactions INTEGER DEFAULT 0,
last_analyzed TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
key_hash TEXT NOT NULL,
name TEXT DEFAULT 'API Key',
prefix TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now')),
last_used TEXT,
revoked INTEGER DEFAULT 0,
rate_limit INTEGER DEFAULT 60
);
""")
cur.execute("CREATE INDEX IF NOT EXISTS idx_conversations_user ON conversations(user_id);")
cur.execute("CREATE INDEX IF NOT EXISTS idx_conversations_session ON conversations(session_id);")
cur.execute("CREATE INDEX IF NOT EXISTS idx_conversations_user_session ON conversations(user_id, session_id);")
cur.execute("CREATE INDEX IF NOT EXISTS idx_conversations_timestamp ON conversations(timestamp);")
cur.execute("CREATE INDEX IF NOT EXISTS idx_learning_user ON user_learning(user_id);")
cur.execute("CREATE INDEX IF NOT EXISTS idx_learning_category ON user_learning(user_id, category);")
cur.execute("CREATE INDEX IF NOT EXISTS idx_learning_unique ON user_learning(user_id, category, key);")
cur.execute("CREATE INDEX IF NOT EXISTS idx_auth_tokens_user ON auth_tokens(user_id);")
cur.execute("CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id);")
cur.execute("CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash);")
conn.commit()
except Exception as e:
conn.rollback()
if "already exists" not in str(e).lower() and "duplicate" not in str(e).lower():
raise
finally:
conn.close()
# ── Auth Tokens ────────────────────────────────────────────────────────
def create_token(self, user_id):
token = secrets.token_urlsafe(32)
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute(
"INSERT INTO auth_tokens (token, user_id, created_at, last_used) VALUES (?, ?, ?, ?)",
(token, user_id, datetime.now().isoformat(sep=' ', timespec='seconds'), datetime.now().isoformat(sep=' ', timespec='seconds'))
)
conn.commit()
finally:
conn.close()
return token
def validate_token(self, token):
if not token or len(token) < 10:
return None
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("SELECT user_id FROM auth_tokens WHERE token = ?", (token,))
row = cur.fetchone()
return row["user_id"] if row else None
finally:
conn.close()
def touch_token(self, token):
"""Update token last_used with retry logic for concurrent access."""
now = datetime.now().isoformat(sep=' ', timespec='seconds')
for attempt in range(5):
conn = None
try:
conn = self._get_connection()
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute(
"UPDATE auth_tokens SET last_used = ? WHERE token = ?",
(now, token)
)
conn.commit()
return
except sqlite3.OperationalError as e:
if conn:
try:
conn.rollback()
except Exception:
pass
if "database is locked" in str(e).lower() and attempt < 4:
time.sleep(0.05 * (attempt + 1))
continue
raise
finally:
if conn:
try:
conn.close()
except Exception:
pass
def delete_token(self, token):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute("DELETE FROM auth_tokens WHERE token = ?", (token,))
conn.commit()
finally:
conn.close()
def revoke_user_tokens(self, user_id):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute("DELETE FROM auth_tokens WHERE user_id = ?", (user_id,))
conn.commit()
finally:
conn.close()
def cleanup_old_tokens(self, max_age_days=30):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute(
"DELETE FROM auth_tokens WHERE last_used < datetime('now', '-{} days')".format(max_age_days)
)
conn.commit()
finally:
conn.close()
# ── User Auth ─────────────────────────────────────────────────────────
def create_user(self, username, password):
"""Create a user with validation. Returns True on success, False on failure."""
# Defense-in-depth validations (also enforced in app.py)
if not username or not password:
return False
if len(username) < 3 or len(username) > 30:
return False
if not re.match(r'^[a-zA-Z0-9_\-]+$', username):
return False
if username.lower().startswith("__guest_"):
return False
if len(password) < 8:
return False
if not re.search(r'[A-Z]', password) or not re.search(r'[a-z]', password) or not re.search(r'[0-9]', password):
return False
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
pw_hash = generate_password_hash(password)
# Store username lowercase for case-normalization
cur.execute(
"INSERT INTO users (username, password_hash) VALUES (?, ?)",
(username.strip().lower(), pw_hash)
)
conn.commit()
return True
except sqlite3.IntegrityError:
conn.rollback()
return False
finally:
conn.close()
def verify_user(self, username, password):
if not username or not password:
return None
conn = self._get_connection()
try:
cur = conn.cursor()
# Case-normalized lookup
cur.execute(
"SELECT id, password_hash FROM users WHERE username = ?", (username.strip().lower(),)
)
row = cur.fetchone()
if row and check_password_hash(row["password_hash"], password):
return row["id"]
return None
finally:
conn.close()
def get_username(self, user_id):
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("SELECT username FROM users WHERE id = ?", (user_id,))
row = cur.fetchone()
return row["username"] if row else None
finally:
conn.close()
def is_guest_user(self, user_id):
username = self.get_username(user_id)
return username.startswith("__guest_") if username else True
def create_guest_user(self, username=None, password=None):
if not username:
username = f"__guest_{secrets.token_hex(8)}"
if not password:
password = secrets.token_hex(32)
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
pw_hash = generate_password_hash(password)
cur.execute(
"INSERT INTO users (username, password_hash) VALUES (?, ?)",
(username, pw_hash)
)
user_id = cur.lastrowid
conn.commit()
return user_id
except sqlite3.IntegrityError:
conn.rollback()
return None
finally:
conn.close()
def find_or_create_google_user(self, google_id, name, email):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute("SELECT id FROM users WHERE google_id = ?", (google_id,))
row = cur.fetchone()
if row:
conn.commit()
return row["id"]
cur.execute("SELECT id FROM users WHERE email = ?", (email,))
row = cur.fetchone()
if row:
cur.execute("UPDATE users SET google_id = ? WHERE id = ?", (google_id, row["id"]))
conn.commit()
return row["id"]
base_username = name.replace(" ", "_").lower()
base_username = ''.join(c for c in base_username if c.isalnum() or c == '_')
if not base_username:
base_username = "user"
username = base_username
counter = 1
while True:
cur.execute("SELECT id FROM users WHERE username = ?", (username,))
if not cur.fetchone():
break
username = f"{base_username}_{counter}"
counter += 1
random_pw = secrets.token_hex(32)
pw_hash = generate_password_hash(random_pw)
cur.execute(
"INSERT INTO users (username, password_hash, google_id, email) VALUES (?, ?, ?, ?)",
(username, pw_hash, google_id, email)
)
user_id = cur.lastrowid
conn.commit()
return user_id
except sqlite3.IntegrityError:
conn.rollback()
return None
finally:
conn.close()
def transfer_guest_data(self, guest_user_id, real_user_id):
"""
Transfer guest user data to real user account.
Handles UNIQUE constraint conflicts by merging learning data.
"""
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
# 1. Transfer conversations
cur.execute(
"UPDATE conversations SET user_id = ? WHERE user_id = ?",
(real_user_id, guest_user_id)
)
# 2. Transfer chat sessions
cur.execute(
"UPDATE chat_sessions SET user_id = ? WHERE user_id = ?",
(real_user_id, guest_user_id)
)
# 3. Transfer user_profile (ON CONFLICT handles duplicates)
cur.execute(
"SELECT user_id, key, value, category, updated_at FROM user_profile WHERE user_id = ?",
(guest_user_id,)
)
for row in cur.fetchall():
cur.execute(
"""INSERT INTO user_profile (user_id, key, value, category, updated_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT (user_id, key) DO UPDATE SET
value = excluded.value,
category = excluded.category,
updated_at = excluded.updated_at""",
(real_user_id, row["key"], row["value"], row["category"], row["updated_at"])
)
cur.execute("DELETE FROM user_profile WHERE user_id = ?", (guest_user_id,))
# 4. Transfer user_learning with conflict resolution
cur.execute(
"SELECT category, key, value, confidence, times_observed, first_seen, updated_at "
"FROM user_learning WHERE user_id = ?",
(guest_user_id,)
)
guest_rows = cur.fetchall()
for row in guest_rows:
cur.execute(
"SELECT id, confidence, times_observed, value FROM user_learning "
"WHERE user_id = ? AND category = ? AND key = ?",
(real_user_id, row["category"], row["key"])
)
existing = cur.fetchone()
if existing:
new_confidence = max(float(existing["confidence"]), float(row["confidence"]))
new_times = int(existing["times_observed"]) + int(row["times_observed"])
new_value = row["value"] if float(row["confidence"]) >= float(existing["confidence"]) else existing["value"]
cur.execute(
"UPDATE user_learning SET value = ?, confidence = ?, times_observed = ?, updated_at = ? "
"WHERE id = ?",
(new_value, new_confidence, new_times,
datetime.now().isoformat(sep=' ', timespec='seconds'), existing["id"])
)
else:
cur.execute(
"INSERT INTO user_learning (user_id, category, key, value, confidence, "
"times_observed, first_seen, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(real_user_id, row["category"], row["key"], row["value"],
row["confidence"], row["times_observed"], row["first_seen"],
datetime.now().isoformat(sep=' ', timespec='seconds'))
)
cur.execute("DELETE FROM user_learning WHERE user_id = ?", (guest_user_id,))
# 5. Transfer user_insights (ON CONFLICT handles duplicates)
cur.execute(
"SELECT * FROM user_insights WHERE user_id = ?",
(guest_user_id,)
)
guest_insights = cur.fetchone()
if guest_insights:
insights_dict = dict(guest_insights)
insights_dict.pop("user_id", None)
cols = [k for k in insights_dict.keys()]
vals = [insights_dict[k] for k in cols]
placeholders = ",".join(["?"] * len(vals))
updates = ",".join([f"{c}=excluded.{c}" for c in cols])
cur.execute(
f"""INSERT INTO user_insights (user_id, {','.join(cols)})
VALUES (? , {placeholders})
ON CONFLICT (user_id) DO UPDATE SET {updates}""",
[real_user_id] + vals
)
cur.execute("DELETE FROM user_insights WHERE user_id = ?", (guest_user_id,))
# 6. Transfer API keys
cur.execute(
"UPDATE api_keys SET user_id = ? WHERE user_id = ?",
(real_user_id, guest_user_id)
)
# 7. Delete guest auth tokens
cur.execute("DELETE FROM auth_tokens WHERE user_id = ?", (guest_user_id,))
# 8. Delete guest user
cur.execute("DELETE FROM users WHERE id = ?", (guest_user_id,))
conn.commit()
print(f"✅ Transferred guest {guest_user_id} → user {real_user_id}")
except Exception as e:
conn.rollback()
print(f"❌ Transfer error: {e}")
raise
finally:
conn.close()
def cleanup_guest_users(self, max_age_days=14):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute(
"SELECT id FROM users WHERE username LIKE '__guest_%' AND created_at < datetime('now', '-{} days')".format(max_age_days)
)
guest_ids = [row["id"] for row in cur.fetchall()]
if not guest_ids:
conn.commit()
return
placeholders = ','.join('?' * len(guest_ids))
cur.execute(f"DELETE FROM conversations WHERE user_id IN ({placeholders})", guest_ids)
cur.execute(f"DELETE FROM chat_sessions WHERE user_id IN ({placeholders})", guest_ids)
cur.execute(f"DELETE FROM user_learning WHERE user_id IN ({placeholders})", guest_ids)
cur.execute(f"DELETE FROM user_insights WHERE user_id IN ({placeholders})", guest_ids)
cur.execute(f"DELETE FROM auth_tokens WHERE user_id IN ({placeholders})", guest_ids)
cur.execute(f"DELETE FROM api_keys WHERE user_id IN ({placeholders})", guest_ids)
cur.execute(f"DELETE FROM user_profile WHERE user_id IN ({placeholders})", guest_ids)
cur.execute(f"DELETE FROM users WHERE id IN ({placeholders})", guest_ids)
conn.commit()
print(f"🧹 Cleaned up {len(guest_ids)} old guest users")
finally:
conn.close()
# ── Sessions ─────────────────────────────────────────────────────────
def create_session(self, user_id, title=None):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute(
"INSERT INTO chat_sessions (user_id, title) VALUES (?, ?)",
(user_id, title or "New Chat")
)
session_id = cur.lastrowid
conn.commit()
return session_id
finally:
conn.close()
def rename_session(self, session_id, title):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute("UPDATE chat_sessions SET title = ? WHERE id = ?", (title[:60], session_id))
conn.commit()
finally:
conn.close()
def rename_session_if_default(self, session_id, title, user_id):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute(
"SELECT title FROM chat_sessions WHERE id = ? AND user_id = ?",
(session_id, user_id)
)
row = cur.fetchone()
if row and (row["title"] == "New Chat" or not row["title"]):
cur.execute("UPDATE chat_sessions SET title = ? WHERE id = ?", (title[:60], session_id))
conn.commit()
else:
conn.commit()
finally:
conn.close()
def delete_session(self, session_id, user_id):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute("DELETE FROM conversations WHERE session_id = ? AND user_id = ?", (session_id, user_id))
cur.execute("DELETE FROM chat_sessions WHERE id = ? AND user_id = ?", (session_id, user_id))
conn.commit()
finally:
conn.close()
def get_sessions(self, user_id):
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute(
"""SELECT cs.id, cs.title, cs.created_at,
COUNT(c.id) as msg_count
FROM chat_sessions cs
LEFT JOIN conversations c ON c.session_id = cs.id
WHERE cs.user_id = ?
GROUP BY cs.id
ORDER BY cs.created_at DESC
LIMIT 50""",
(user_id,)
)
rows = cur.fetchall()
return [dict(r) for r in rows]
finally:
conn.close()
# ── Chat Messages ────────────────────────────────────────────────────
def save(self, role, content, user_id=None, session_id=None):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute(
"INSERT INTO conversations (user_id, session_id, role, content, timestamp) VALUES (?, ?, ?, ?, ?)",
(user_id, session_id, role, content, datetime.now().isoformat(sep=' ', timespec='seconds'))
)
conn.commit()
finally:
conn.close()
def get_recent(self, user_id, limit=20, session_id=None):
conn = self._get_connection()
try:
cur = conn.cursor()
if session_id:
cur.execute(
"SELECT role, content, timestamp FROM conversations "
"WHERE user_id = ? AND session_id = ? ORDER BY id DESC LIMIT ?",
(user_id, session_id, limit)
)
else:
cur.execute(
"SELECT role, content, timestamp FROM conversations "
"WHERE user_id = ? ORDER BY id DESC LIMIT ?",
(user_id, limit)
)
rows = cur.fetchall()
return list(reversed([(r["role"], r["content"], r["timestamp"]) for r in rows]))
finally:
conn.close()
def get_session_messages(self, session_id, user_id):
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute(
"SELECT role, content, timestamp FROM conversations "
"WHERE session_id = ? AND user_id = ? ORDER BY id ASC",
(session_id, user_id)
)
rows = cur.fetchall()
return [dict(r) for r in rows]
finally:
conn.close()
def get_chat_history_list(self, user_id):
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute(
"SELECT id, content FROM conversations "
"WHERE user_id = ? AND role = 'user' ORDER BY id DESC LIMIT 20",
(user_id,)
)
rows = cur.fetchall()
return [(r["id"], r["content"]) for r in rows]
finally:
conn.close()
def search_history(self, user_id, query, limit=10):
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute(
"SELECT role, content, timestamp FROM conversations "
"WHERE user_id = ? AND content LIKE ? ORDER BY id DESC LIMIT ?",
(user_id, f"%{query}%", limit)
)
rows = cur.fetchall()
return [dict(r) for r in rows]
finally:
conn.close()
def get_user_message_count(self, user_id):
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute(
"SELECT COUNT(*) as c FROM conversations WHERE user_id = ? AND role = 'user'",
(user_id,)
)
row = cur.fetchone()
return row["c"] if row else 0
finally:
conn.close()
# ── User Profile (user-specific) ─────────────────────────────────────
def update_user_profile(self, user_id, key, value, category="general"):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute("""
INSERT INTO user_profile (user_id, key, value, category, updated_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT (user_id, key) DO UPDATE SET
value = excluded.value, category = excluded.category, updated_at = excluded.updated_at
""", (user_id, key, str(value), category, datetime.now().isoformat(sep=' ', timespec='seconds')))
conn.commit()
finally:
conn.close()
def get_user_profile(self, user_id):
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute(
"SELECT key, value FROM user_profile WHERE user_id = ? ORDER BY key",
(user_id,)
)
rows = cur.fetchall()
return {r["key"]: r["value"] for r in rows}
finally:
conn.close()
def delete_profile_key(self, user_id, key):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute("DELETE FROM user_profile WHERE user_id = ? AND key = ?", (user_id, key))
conn.commit()
finally:
conn.close()
def clear_profile(self, user_id):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute("DELETE FROM user_profile WHERE user_id = ?", (user_id,))
conn.commit()
finally:
conn.close()
# ── Self-Learning ──────────────────────────────────────────────────────
def store_learning(self, user_id, category, key, value, confidence=0.5):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute(
"SELECT id, confidence, times_observed FROM user_learning "
"WHERE user_id = ? AND category = ? AND key = ?",
(user_id, category, key)
)
existing = cur.fetchone()
if existing:
new_confidence = min(existing["confidence"] + 0.1, 2.0)
new_times = existing["times_observed"] + 1
cur.execute(
"UPDATE user_learning SET confidence = ?, times_observed = ?, value = ?, updated_at = ? WHERE id = ?",
(new_confidence, new_times, value, datetime.now().isoformat(sep=' ', timespec='seconds'), existing["id"])
)
else:
cur.execute(
"INSERT INTO user_learning (user_id, category, key, value, confidence, first_seen, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
(user_id, category, key, value, confidence, datetime.now().isoformat(sep=' ', timespec='seconds'), datetime.now().isoformat(sep=' ', timespec='seconds'))
)
conn.commit()
finally:
conn.close()
def get_user_learning(self, user_id, category=None):
conn = self._get_connection()
try:
cur = conn.cursor()
if category:
cur.execute(
"SELECT category, key, value, confidence, times_observed FROM user_learning WHERE user_id = ? AND category = ? ORDER BY confidence DESC",
(user_id, category)
)
else:
cur.execute(
"SELECT category, key, value, confidence, times_observed FROM user_learning WHERE user_id = ? ORDER BY confidence DESC",
(user_id,)
)
rows = cur.fetchall()
return [dict(r) for r in rows]
finally:
conn.close()
def get_learning_by_category(self, user_id, category):
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute(
"SELECT key, value FROM user_learning WHERE user_id = ? AND category = ? ORDER BY confidence DESC",
(user_id, category)
)
rows = cur.fetchall()
return {r["key"]: r["value"] for r in rows}
finally:
conn.close()
def get_top_learning_topics(self, user_id, limit=10):
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute(
"SELECT key, value, confidence, times_observed FROM user_learning "
"WHERE user_id = ? AND category = 'interest' ORDER BY times_observed DESC, confidence DESC LIMIT ?",
(user_id, limit)
)
rows = cur.fetchall()
return [dict(r) for r in rows]
finally:
conn.close()
def prune_old_learning(self, user_id, max_entries=500):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute("SELECT COUNT(*) as c FROM user_learning WHERE user_id = ?", (user_id,))
count = cur.fetchone()["c"]
if count > max_entries:
cur.execute(
"DELETE FROM user_learning WHERE id IN (SELECT id FROM user_learning WHERE user_id = ? ORDER BY confidence ASC, times_observed ASC LIMIT ?)",
(user_id, count - max_entries)
)
conn.commit()
else:
conn.commit()
finally:
conn.close()
# ── User Insights ────────────────────────────────────────────────────
def update_user_insights(self, user_id, insights_dict):
with self.lock:
allowed_keys = ["preferred_formality", "preferred_response_length", "preferred_tone", "topics_of_interest", "communication_patterns", "total_interactions"]
insert_cols = [k for k in insights_dict if k in allowed_keys]
if not insert_cols:
return
insert_vals = [json.dumps(insights_dict[k]) if isinstance(insights_dict[k], (dict, list)) else str(insights_dict[k]) for k in insert_cols]
insert_cols.append("updated_at")
insert_vals.append(datetime.now().isoformat(sep=' ', timespec='seconds'))
updates = [f"{column} = excluded.{column}" for column in insert_cols]
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
sql = f"""INSERT INTO user_insights (user_id, {', '.join(insert_cols)})
VALUES (?, {', '.join(['?'] * len(insert_vals))})
ON CONFLICT (user_id) DO UPDATE SET {', '.join(updates)}"""
cur.execute(sql, [user_id] + insert_vals)
conn.commit()
finally:
conn.close()
def get_user_insights(self, user_id):
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("SELECT * FROM user_insights WHERE user_id = ?", (user_id,))
row = cur.fetchone()
if row:
result = dict(row)
for field in ["topics_of_interest", "communication_patterns"]:
if result.get(field):
try:
result[field] = json.loads(result[field])
except (json.JSONDecodeError, TypeError):
result[field] = [] if field == "topics_of_interest" else {}
return result
return None
finally:
conn.close()
def increment_interaction_count(self, user_id):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute(
"""INSERT INTO user_insights (user_id, total_interactions, updated_at)
VALUES (?, 1, ?)
ON CONFLICT (user_id) DO UPDATE SET
total_interactions = total_interactions + 1, updated_at = excluded.updated_at""",
(user_id, datetime.now().isoformat(sep=' ', timespec='seconds'))
)
conn.commit()
finally:
conn.close()
# ── API Keys ───────────────────────────────────────────────────────────
def create_api_key(self, user_id, name="API Key"):
raw_key = "invicta_sk_" + secrets.token_urlsafe(32)
key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
prefix = raw_key[:14] + "..."
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute(
"INSERT INTO api_keys (user_id, key_hash, name, prefix, created_at) VALUES (?, ?, ?, ?, ?)",
(user_id, key_hash, name, prefix, datetime.now().isoformat(sep=' ', timespec='seconds'))
)
key_id = cur.lastrowid
conn.commit()
return key_id, raw_key
finally:
conn.close()
def validate_api_key(self, raw_key):
if not raw_key or not raw_key.startswith("invicta_sk_"):
return None
key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute(
"SELECT id, user_id, revoked FROM api_keys WHERE key_hash = ?",
(key_hash,)
)
row = cur.fetchone()
if row and not row["revoked"]:
cur.execute(
"UPDATE api_keys SET last_used = ? WHERE id = ?",
(datetime.now().isoformat(sep=' ', timespec='seconds'), row["id"])
)
conn.commit()
return row["user_id"]
return None
finally:
conn.close()
def get_api_keys(self, user_id):
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute(
"SELECT id, name, prefix, created_at, last_used, revoked, rate_limit FROM api_keys WHERE user_id = ? ORDER BY created_at DESC",
(user_id,)
)
rows = cur.fetchall()
return [dict(r) for r in rows]
finally:
conn.close()
def revoke_api_key(self, key_id, user_id):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute(
"UPDATE api_keys SET revoked = 1 WHERE id = ? AND user_id = ?",
(key_id, user_id)
)
conn.commit()
return cur.rowcount > 0
finally:
conn.close()
def delete_api_key(self, key_id, user_id):
with self.lock:
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("BEGIN IMMEDIATE")
cur.execute("DELETE FROM api_keys WHERE id = ? AND user_id = ?", (key_id, user_id))
conn.commit()
finally:
conn.close()
# ── Stats ──────────────────────────────────────────────────────────────
def get_stats(self, user_id):
conn = self._get_connection()
try:
cur = conn.cursor()
cur.execute("SELECT COUNT(*) as c FROM conversations WHERE user_id = ?", (user_id,))
total = cur.fetchone()["c"]
cur.execute("SELECT COUNT(*) as c FROM chat_sessions WHERE user_id = ?", (user_id,))
sessions = cur.fetchone()["c"]
cur.execute("SELECT COUNT(*) as c FROM user_learning WHERE user_id = ?", (user_id,))
learning = cur.fetchone()["c"]
return {"total_messages": total, "total_sessions": sessions, "learning_observations": learning}
finally:
conn.close()