feat: supersede di memorie false (qmem_correct, include_superseded, playbook sez. 11)
- gateway: supersedes_id validato (404/409), backlink superseded_by+superseded_at+supersede_reason sul vecchio record, indici keyword, search esclude i superseduti di default (include_superseded per lineage), risposta con lineage - estensione: tool qmem_correct (memory_id o query), qmem_store con supersedes_id/supersede_reason, qmem_search con include_superseded - README/playbook aggiornati, versione 1.1.0
This commit is contained in:
+40
-7
@@ -8,8 +8,8 @@ agente (attuale o futuro) con la chiave può consultare e aggiungere
|
||||
informazioni liberamente. L'agent_id è solo metadata di provenienza.
|
||||
|
||||
Endpoints:
|
||||
POST /v1/memories → crea un record di memoria
|
||||
POST /v1/memories:search → ricerca semantica con filtri
|
||||
POST /v1/memories → crea un record (con supersedes_id corregge un record esistente)
|
||||
POST /v1/memories:search → ricerca semantica con filtri (include_superseded per la lineage)
|
||||
GET /v1/memories/{id} → recupera per UUID
|
||||
DELETE /v1/memories/{id} → elimina per UUID
|
||||
GET /v1/status → health + statistiche
|
||||
@@ -48,7 +48,7 @@ MAX_TEXT_LEN = int(os.environ.get("MAX_TEXT_LEN", "8000"))
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||
log = logging.getLogger("memory-gateway")
|
||||
|
||||
app = FastAPI(title="Memory Gateway", version="2.0.0")
|
||||
app = FastAPI(title="Memory Gateway", version="2.1.0")
|
||||
qdrant = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY)
|
||||
|
||||
# Rate limit in-memory: {key: [timestamps]}
|
||||
@@ -67,6 +67,7 @@ class MemoryIn(BaseModel):
|
||||
source: Optional[str] = Field(default=None, max_length=256)
|
||||
expires_at: Optional[str] = None # ISO 8601
|
||||
supersedes_id: Optional[str] = None
|
||||
supersede_reason: Optional[str] = Field(default=None, max_length=512)
|
||||
|
||||
|
||||
class SearchIn(BaseModel):
|
||||
@@ -74,6 +75,7 @@ class SearchIn(BaseModel):
|
||||
kind: Optional[Literal["decision", "fact", "episode", "preference"]] = None
|
||||
project_id: Optional[str] = None
|
||||
scope: Optional[Literal["agent", "project", "org"]] = None
|
||||
include_superseded: bool = False
|
||||
top_k: int = Field(default=5, ge=1, le=20)
|
||||
|
||||
|
||||
@@ -142,7 +144,7 @@ def startup() -> None:
|
||||
collection_name=COLLECTION,
|
||||
vectors_config=qm.VectorParams(size=EMBED_DIM, distance=qm.Distance.COSINE),
|
||||
)
|
||||
for field in ("agent_id", "project_id", "scope", "kind"):
|
||||
for field in ("agent_id", "project_id", "scope", "kind", "supersedes_id", "superseded_by"):
|
||||
qdrant.create_payload_index(
|
||||
collection_name=COLLECTION,
|
||||
field_name=field,
|
||||
@@ -159,6 +161,17 @@ def startup() -> None:
|
||||
@app.post("/v1/memories")
|
||||
async def add_memory(body: MemoryIn, key: str = Depends(require_auth)) -> dict:
|
||||
memory_id = str(uuid.uuid4())
|
||||
|
||||
# Supersede: il nuovo record corregge uno esistente, che resta in archivio marcato
|
||||
superseded_id: Optional[str] = None
|
||||
if body.supersedes_id:
|
||||
old = qdrant.retrieve(collection_name=COLLECTION, ids=[body.supersedes_id], with_payload=True)
|
||||
if not old:
|
||||
raise HTTPException(status_code=404, detail="Memoria da supersedere non trovata")
|
||||
if old[0].payload.get("superseded_by"):
|
||||
raise HTTPException(status_code=409, detail="La memoria è già stata superseduta: correggi la versione attiva")
|
||||
superseded_id = body.supersedes_id
|
||||
|
||||
vector = await embed(body.text)
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
@@ -170,15 +183,29 @@ async def add_memory(body: MemoryIn, key: str = Depends(require_auth)) -> dict:
|
||||
"source": body.source,
|
||||
"created_at": _now_iso(),
|
||||
"expires_at": _parse_ts(body.expires_at),
|
||||
"supersedes_id": body.supersedes_id,
|
||||
"supersedes_id": superseded_id,
|
||||
"supersede_reason": body.supersede_reason,
|
||||
"embedding_model": EMBED_MODEL,
|
||||
}
|
||||
qdrant.upsert(
|
||||
collection_name=COLLECTION,
|
||||
points=[qm.PointStruct(id=memory_id, vector=vector, payload=payload)],
|
||||
)
|
||||
_audit(key, "create", memory_id=memory_id, kind=body.kind, agent_id=payload["agent_id"])
|
||||
return {"memory_id": memory_id, "created_at": payload["created_at"]}
|
||||
|
||||
if superseded_id:
|
||||
qdrant.set_payload(
|
||||
collection_name=COLLECTION,
|
||||
payload={
|
||||
"superseded_by": memory_id,
|
||||
"superseded_at": _now_iso(),
|
||||
"supersede_reason": body.supersede_reason,
|
||||
},
|
||||
points=[superseded_id],
|
||||
)
|
||||
_audit(key, "supersede", old_id=superseded_id, new_id=memory_id, kind=body.kind, agent_id=payload["agent_id"])
|
||||
else:
|
||||
_audit(key, "create", memory_id=memory_id, kind=body.kind, agent_id=payload["agent_id"])
|
||||
return {"memory_id": memory_id, "created_at": payload["created_at"], "supersedes_id": superseded_id}
|
||||
|
||||
|
||||
@app.post("/v1/memories:search")
|
||||
@@ -192,6 +219,9 @@ async def search_memories(body: SearchIn, key: str = Depends(require_auth)) -> d
|
||||
must.append(qm.FieldCondition(key="project_id", match=qm.MatchValue(value=body.project_id)))
|
||||
if body.scope:
|
||||
must.append(qm.FieldCondition(key="scope", match=qm.MatchValue(value=body.scope)))
|
||||
if not body.include_superseded:
|
||||
# default: esclude i record già corretti (superseded_by presente)
|
||||
must.append(qm.IsEmptyCondition(is_empty=qm.PayloadField(key="superseded_by")))
|
||||
|
||||
hits = qdrant.search(
|
||||
collection_name=COLLECTION,
|
||||
@@ -211,6 +241,9 @@ async def search_memories(body: SearchIn, key: str = Depends(require_auth)) -> d
|
||||
"project_id": h.payload.get("project_id"),
|
||||
"created_at": h.payload.get("created_at"),
|
||||
"source": h.payload.get("source"),
|
||||
"supersedes_id": h.payload.get("supersedes_id"),
|
||||
"superseded_by": h.payload.get("superseded_by"),
|
||||
"supersede_reason": h.payload.get("supersede_reason"),
|
||||
}
|
||||
for h in hits
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user