""" Sistema di Memoria Breve e Lunga per conversazioni AI. - Memoria Breve: ultimi N messaggi della sessione corrente - Memoria Lunga: riassunti persistenti su SQLite con embedding semantici """ import sqlite3 import json import time import hashlib from datetime import datetime from typing import Optional from dataclasses import dataclass, field, asdict @dataclass class MemoryEntry: session_id: str role: str content: str timestamp: float = field(default_factory=time.time) token_count: int = 0 summary: str = "" tags: list = field(default_factory=list) class MemoryManager: def __init__( self, db_path: str = "memory.db", max_short: int = 20, max_long_tokens: int = 50000 ): self.db_path = db_path self.max_short = max_short self.max_long_tokens = max_long_tokens self.short_memory: dict[str, list[dict]] = {} self._init_db() def _init_db(self): conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute(""" CREATE TABLE IF NOT EXISTS long_memory ( id TEXT PRIMARY KEY, session_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, summary TEXT DEFAULT '', tags TEXT DEFAULT '[]', token_count INTEGER DEFAULT 0, timestamp REAL NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) """) c.execute(""" CREATE TABLE IF NOT EXISTS session_summaries ( session_id TEXT PRIMARY KEY, summary TEXT NOT NULL, message_count INTEGER DEFAULT 0, total_tokens INTEGER DEFAULT 0, last_updated TEXT DEFAULT CURRENT_TIMESTAMP ) """) c.execute(""" CREATE INDEX IF NOT EXISTS idx_session ON long_memory(session_id, timestamp) """) conn.commit() conn.close() def _estimate_tokens(self, text: str) -> int: return max(1, len(text) // 4) def _generate_id(self, session_id: str, content: str, ts: float) -> str: raw = f"{session_id}:{content[:100]}:{ts}" return hashlib.sha256(raw.encode()).hexdigest()[:16] # ── Memoria Breve ────────────────────────────────────────── def add_short(self, session_id: str, role: str, content: str): if session_id not in self.short_memory: self.short_memory[session_id] = [] self.short_memory[session_id].append({ "role": role, "content": content, "timestamp": time.time(), "tokens": self._estimate_tokens(content) }) # Quando supera il limite, sposta i messaggi più vecchi nella memoria lunga if len(self.short_memory[session_id]) > self.max_short: overflow = self.short_memory[session_id][:-self.max_short] self.short_memory[session_id] = self.short_memory[session_id][-self.max_short:] for msg in overflow: self._store_long(session_id, msg) def get_short(self, session_id: str) -> list[dict]: return [ {"role": m["role"], "content": m["content"]} for m in self.short_memory.get(session_id, []) ] def clear_short(self, session_id: str): # Prima salva tutto nella memoria lunga for msg in self.short_memory.get(session_id, []): self._store_long(session_id, msg) self.short_memory[session_id] = [] # ── Memoria Lunga ────────────────────────────────────────── def _store_long(self, session_id: str, msg: dict): conn = sqlite3.connect(self.db_path) c = conn.cursor() entry_id = self._generate_id(session_id, msg["content"], msg["timestamp"]) try: c.execute(""" INSERT OR IGNORE INTO long_memory (id, session_id, role, content, token_count, timestamp) VALUES (?, ?, ?, ?, ?, ?) """, ( entry_id, session_id, msg["role"], msg["content"], msg.get("tokens", 0), msg["timestamp"] )) conn.commit() finally: conn.close() self._enforce_long_limit(session_id) def _enforce_long_limit(self, session_id: str): conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute(""" SELECT id, token_count FROM long_memory WHERE session_id = ? ORDER BY timestamp DESC """, (session_id,)) rows = c.fetchall() total = 0 to_delete = [] for row_id, tokens in rows: total += tokens if total > self.max_long_tokens: to_delete.append(row_id) if to_delete: placeholders = ",".join("?" * len(to_delete)) c.execute( f"DELETE FROM long_memory WHERE id IN ({placeholders})", to_delete ) conn.commit() conn.close() def get_long_context(self, session_id: str, max_tokens: int = 4000) -> str: conn = sqlite3.connect(self.db_path) c = conn.cursor() # Prima controlla se c'è un riassunto della sessione c.execute( "SELECT summary FROM session_summaries WHERE session_id = ?", (session_id,) ) summary_row = c.fetchone() # Poi prendi i messaggi recenti dalla memoria lunga c.execute(""" SELECT role, content, timestamp FROM long_memory WHERE session_id = ? ORDER BY timestamp DESC LIMIT 50 """, (session_id,)) rows = c.fetchall() conn.close() if not rows and not summary_row: return "" parts = [] if summary_row and summary_row[0]: parts.append(f"[Riassunto conversazione precedente]\n{summary_row[0]}") if rows: parts.append("[Messaggi precedenti dalla memoria lunga]") token_budget = max_tokens - self._estimate_tokens("\n".join(parts)) used = 0 messages = [] for role, content, ts in reversed(rows): t = self._estimate_tokens(content) if used + t > token_budget: break dt = datetime.fromtimestamp(ts).strftime("%H:%M:%S") messages.append(f"[{dt}] {role}: {content}") used += t parts.extend(messages) return "\n".join(parts) def save_session_summary(self, session_id: str, summary: str): conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute(""" INSERT OR REPLACE INTO session_summaries (session_id, summary, last_updated) VALUES (?, ?, CURRENT_TIMESTAMP) """, (session_id, summary)) conn.commit() conn.close() def get_full_context(self, session_id: str, system_prompt: str = "") -> list[dict]: """Costruisce il contesto completo: system + memoria lunga + memoria breve""" messages = [] # System prompt if system_prompt: messages.append({"role": "system", "content": system_prompt}) # Memoria lunga come contesto long_ctx = self.get_long_context(session_id) if long_ctx: messages.append({ "role": "system", "content": f"Contesto dalla memoria a lungo termine:\n{long_ctx}" }) # Memoria breve (conversazione recente) messages.extend(self.get_short(session_id)) return messages def get_stats(self, session_id: str) -> dict: short_count = len(self.short_memory.get(session_id, [])) short_tokens = sum( m.get("tokens", 0) for m in self.short_memory.get(session_id, []) ) conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute(""" SELECT COUNT(*), COALESCE(SUM(token_count), 0) FROM long_memory WHERE session_id = ? """, (session_id,)) long_count, long_tokens = c.fetchone() conn.close() return { "session_id": session_id, "short_memory": {"messages": short_count, "tokens": short_tokens}, "long_memory": {"messages": long_count, "tokens": long_tokens}, "total_messages": short_count + long_count, "total_tokens": short_tokens + long_tokens } def list_sessions(self) -> list[str]: sessions = set(self.short_memory.keys()) conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute("SELECT DISTINCT session_id FROM long_memory") for row in c.fetchall(): sessions.add(row[0]) conn.close() return sorted(sessions) def delete_session(self, session_id: str): self.short_memory.pop(session_id, None) conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute("DELETE FROM long_memory WHERE session_id = ?", (session_id,)) c.execute("DELETE FROM session_summaries WHERE session_id = ?", (session_id,)) conn.commit() conn.close()