Terminal / memory /manager.py
Baida-A
Initial clean deploy (Reverse Proxy removed)
28a08e7
Raw
History Blame
5.6 kB
import logging
from .working import WorkingMemory
from .episodic import EpisodicMemory
from .semantic import SemanticMemory
from .reflection import ReflectionMemory
_logger = logging.getLogger("memory.manager")
class MemoryManager:
"""
S569: Unified Memory Manager (ARCH-K2.3).
Coordina i 4 layer di memoria dell'agente.
"""
def __init__(self, sb_client=None, chroma_client=None):
self.working = WorkingMemory()
self.episodic = EpisodicMemory()
self.semantic = SemanticMemory(sb_client, chroma_client)
self.reflection = ReflectionMemory()
async def init(self):
"""Inizializzazione asincrona (es. caricamento snapshot)."""
await self.semantic.init()
# S569: Auto-restore semantica se vuota
await self._auto_restore_semantic()
_logger.info("[MemoryManager] Layer inizializzati: working, episodic, semantic (pgvector=%s), reflection",
getattr(self.semantic, '_pgvector', False))
async def save_working(self, goal: str, plan: list, facts: list):
self.working.update(goal, plan, facts)
# S569: backup periodico della working memory su episodic
await self.save_episode("checkpoint", goal, f"Plan: {len(plan)} steps, Facts: {len(facts)}", True)
async def save_episode(self, type_: str, task: str, output: str, success: bool, tags: list | None = None):
self.episodic.add(type_, task, output, success, tags)
if self.semantic.available and task:
self.semantic.add(task, {"type": type_, "success": success})
async def search(self, query: str, n: int = 5, layer: str | None = None) -> list[dict]:
results = []
if layer in (None, "semantic") and self.semantic.available:
for h in self.semantic.search(query, n_results=n):
results.append({**h, "layer": "semantic"})
if layer in (None, "episodic"):
for ep in self.episodic.search_text(query, n=n):
results.append({
"content": f"{ep.task} β†’ {ep.output[:300]}",
"layer": "episodic",
"type": ep.type,
"success": ep.success,
})
if layer == "reflection":
lessons = self.reflection.get_relevant_lessons(query, n=n)
results.extend([{**l, "layer": "reflection"} for l in lessons])
return results[:n]
async def reflect(self, task: str, output: str, success: bool, error: str | None = None) -> dict:
if success:
self.reflection.record_success(task, output[:500])
await self.save_episode("fix", task, output, True)
else:
self.reflection.record_failure(task, error or output[:500])
await self.save_episode("error", task, error or output[:500], False)
return {
"recorded": True,
"top_patterns": self.reflection.get_top_patterns(5),
"lessons": self.reflection.get_relevant_lessons(task, 4),
}
async def _auto_restore_semantic(self) -> None:
"""Auto-restore: se la semantic memory Γ¨ vuota, carica l'ultimo snapshot da GitHub."""
import asyncio as _asyncio, os
if not self.semantic.available:
return
count = await _asyncio.to_thread(self.semantic.count)
if count > 0:
return
token = os.environ.get("GITHUB_TOKEN", "")
if not token:
return
try:
import urllib.request as _urq, json as _json, base64 as _b64
req = _urq.Request(
"https://api.github.com/repos/Baida98/AI/contents/data/semantic_snapshot.json",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github.v3+json",
"User-Agent": "agente-ai-backend",
},
)
with _urq.urlopen(req, timeout=10) as resp:
meta = _json.loads(resp.read())
records = _json.loads(_b64.b64decode(meta["content"]).decode("utf-8"))
if not records:
return
result = await _asyncio.to_thread(self.semantic.import_all, records)
_logger.info("[MemoryManager] βœ“ Auto-restore semantica: %d record da GitHub snapshot", result["imported"])
except Exception as exc:
_logger.debug("[MemoryManager] Auto-restore semantica: snapshot non disponibile (%s)", exc.__class__.__name__)
def stats(self) -> dict:
return {
"working": self.working.stats(),
"episodic": self.episodic.stats(),
"semantic": self.semantic.stats(),
"reflection": self.reflection.stats(),
}
async def clear(self, layer: str | None = None):
if layer in (None, "working"):
self.working.clear()
if layer in (None, "episodic"):
import sqlite3
if self.episodic._db:
try:
self.episodic._db.execute("DELETE FROM episodes")
self.episodic._db.commit()
except Exception as _e:
try:
self.episodic._db.rollback()
except Exception:
pass
raise RuntimeError(f"clear episodic fallito: {_e}") from _e
# ── Singleton globale β€” inizializzato in main.py _on_startup (GAP-5-fix) ─────
_global_manager: 'MemoryManager | None' = None