Breaking change: per-agent collections replaced by a single 'memories' collection with metadata-based access control. - All memories live in one collection - 'agent' metadata tracks the creator - 'private' (bool, default False) controls visibility - Query returns: shared (private=False) + own private (agent=asker) - Save endpoint accepts optional 'private' field - Version bumped to 0.2.0
160 lines
4.9 KiB
Python
160 lines
4.9 KiB
Python
"""ChromaDB-backed memory store with shared/private visibility.
|
||
|
||
All memories live in a single collection. Each memory has two key
|
||
metadata fields:
|
||
|
||
- ``agent`` (str) — the agent that created it
|
||
- ``private`` (bool) — ``False`` by default (visible to all agents)
|
||
|
||
When querying, the store returns:
|
||
|
||
1. **All shared memories** (``private=False``) — every agent sees these.
|
||
2. **Only the querying agent's own private memories** (``private=True``
|
||
and ``agent=<asker>``).
|
||
|
||
This gives a clean hierarchy: shared knowledge by default, with an
|
||
opt-in privacy mechanism for per-agent internal notes.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import uuid
|
||
|
||
import chromadb
|
||
from chromadb.config import Settings
|
||
|
||
from embedder import Embedder
|
||
|
||
COLLECTION_NAME = "memories"
|
||
|
||
|
||
class MemoryStore:
|
||
"""Persistent vector store using ChromaDB.
|
||
|
||
… with an optional ``private`` flag (default ``False``) so agents
|
||
can keep sensitive memories to themselves.
|
||
"""
|
||
|
||
def __init__(self, data_dir: str) -> None:
|
||
self.client = chromadb.PersistentClient(
|
||
path=data_dir,
|
||
settings=Settings(anonymized_telemetry=False),
|
||
)
|
||
self.embedder = Embedder.get_instance()
|
||
|
||
# ------------------------------------------------------------------
|
||
# Internal helpers
|
||
# ------------------------------------------------------------------
|
||
|
||
def _collection(self):
|
||
"""Return the single shared collection (create if missing)."""
|
||
return self.client.get_or_create_collection(
|
||
name=COLLECTION_NAME,
|
||
metadata={"description": "Shared + private agent memories"},
|
||
)
|
||
|
||
# ------------------------------------------------------------------
|
||
# Public API
|
||
# ------------------------------------------------------------------
|
||
|
||
def save(
|
||
self,
|
||
agent_name: str,
|
||
text: str,
|
||
metadata: dict | None = None,
|
||
) -> str:
|
||
"""Embed and store *text*.
|
||
|
||
Parameters
|
||
----------
|
||
agent_name
|
||
The agent creating this memory (stored in metadata).
|
||
text
|
||
The memory content.
|
||
metadata
|
||
Optional extra fields. If it contains a ``private`` key
|
||
that will be passed to ChromaDB as-is; otherwise ``private``
|
||
defaults to ``False``. The ``agent`` key is always
|
||
overwritten with *agent_name*.
|
||
|
||
Returns
|
||
-------
|
||
The auto-generated memory UUID.
|
||
"""
|
||
collection = self._collection()
|
||
memory_id = str(uuid.uuid4())
|
||
embedding = self.embedder.embed(text)
|
||
|
||
meta: dict = dict(metadata or {})
|
||
meta.setdefault("private", False)
|
||
meta["agent"] = agent_name
|
||
|
||
collection.add(
|
||
embeddings=[embedding],
|
||
documents=[text],
|
||
metadatas=[meta],
|
||
ids=[memory_id],
|
||
)
|
||
return memory_id
|
||
|
||
def query(
|
||
self,
|
||
agent_name: str,
|
||
query_text: str,
|
||
top_n: int = 5,
|
||
) -> list[dict]:
|
||
"""Return semantically similar memories visible to *agent_name*.
|
||
|
||
Visibility rules
|
||
----------------
|
||
- All non‑private memories (``private=False``) are returned
|
||
regardless of which agent created them.
|
||
- Private memories (``private=True``) are only returned for the
|
||
agent that owns them (``agent == agent_name``).
|
||
|
||
Each result dict contains
|
||
- ``text`` — the stored memory text
|
||
- ``metadata`` — metadata (minus the raw ``text`` field)
|
||
- ``distance`` — cosine distance from the query
|
||
"""
|
||
collection = self._collection()
|
||
query_embedding = self.embedder.embed(query_text)
|
||
|
||
# ── ChromaDB where filter ──────────────────────────────────
|
||
# Show shared memories + the asking agent's own private ones.
|
||
where_filter = {
|
||
"$or": [
|
||
{"private": {"$eq": False}},
|
||
{"agent": {"$eq": agent_name}},
|
||
]
|
||
}
|
||
|
||
results = collection.query(
|
||
query_embeddings=[query_embedding],
|
||
n_results=top_n,
|
||
where=where_filter,
|
||
)
|
||
|
||
formatted: list[dict] = []
|
||
if results["documents"] and results["documents"][0]:
|
||
for i, doc in enumerate(results["documents"][0]):
|
||
distance = (
|
||
results["distances"][0][i]
|
||
if results.get("distances")
|
||
else None
|
||
)
|
||
meta = (
|
||
{k: v for k, v in results["metadatas"][0][i].items()}
|
||
if results.get("metadatas")
|
||
else {}
|
||
)
|
||
meta.pop("text", None)
|
||
|
||
formatted.append({
|
||
"text": doc,
|
||
"metadata": meta,
|
||
"distance": distance,
|
||
})
|
||
|
||
return formatted
|