Files
memory-server/store.py
Lucy 4a1ab6ae50 Refactor: single collection with shared/private visibility
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
2026-06-25 15:04:13 +02:00

160 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 nonprivate 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