Spaces:
Running
Running
File size: 6,857 Bytes
28a08e7 6c6290b 28a08e7 | 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 | 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 get_context(self, query: str, code_length: int = 0, n: int = 5) -> str:
"""Return a bounded text context for consumers such as UnifiedAgentLoop.
The loop needs a context-shaped view, while the public manager API exposes
structured search results. Keep this adapter here so callers do not reach
into individual memory layers or depend on their implementation details.
"""
if not query:
return ""
hits = await self.search(query, n=n)
if not hits:
return ""
# Leave room for the current prompt/context; never inject an unbounded
# memory payload into a long-running agent loop.
max_chars = max(1000, min(4000, 4000 - max(0, code_length)))
parts: list[str] = []
used = 0
for hit in hits:
content = str(hit.get("content", "")).strip()
if not content:
continue
layer = str(hit.get("layer", "memory"))
block = f"[{layer}] {content}"
remaining = max_chars - used
if remaining <= 0:
break
parts.append(block[:remaining])
used += len(parts[-1]) + 1
return "\n".join(parts).strip()
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
|