ChromaVectorBackend -- optional concrete VectorBackend using ChromaDB.
Requires the chroma optional dependency group:
pip install symfonic-core[chroma]
All methods are async, wrapping synchronous ChromaDB calls with
asyncio.to_thread for non-blocking operation.
ChromaVectorBackend
ChromaVectorBackend(
collection_name: str = "memory",
persist_directory: str | None = None,
embedding_provider: EmbeddingProvider | None = None,
)
VectorBackend implementation backed by ChromaDB.
Tenant isolation is enforced via metadata filtering on tenant_id.
All synchronous ChromaDB operations are wrapped with asyncio.to_thread.
Source code in src/symfonic/memory/backends/chroma.py
| def __init__(
self,
collection_name: str = "memory",
persist_directory: str | None = None,
embedding_provider: EmbeddingProvider | None = None,
) -> None:
self._embedding_provider = embedding_provider
client_settings: dict[str, Any] = {}
if persist_directory:
client_settings["persist_directory"] = persist_directory
self._client = chromadb.Client() if not persist_directory else (
chromadb.PersistentClient(path=persist_directory)
)
self._collection = self._client.get_or_create_collection(
name=collection_name,
metadata={"hnsw:space": "cosine"},
)
|
add
async
add(
scope: TenantScope,
ids: list[str],
embeddings: list[list[float]],
metadatas: list[dict[str, Any]],
documents: list[str],
) -> None
Add vectors with tenant- and scope-path-scoped metadata.
Source code in src/symfonic/memory/backends/chroma.py
| async def add(
self,
scope: TenantScope,
ids: list[str],
embeddings: list[list[float]],
metadatas: list[dict[str, Any]],
documents: list[str],
) -> None:
"""Add vectors with tenant- and scope-path-scoped metadata."""
scope_path = materialise_scope_path(scope)
enriched_metadatas = [
{**m, "tenant_id": scope.tenant_id, SCOPE_PATH_KEY: scope_path}
for m in metadatas
]
await asyncio.to_thread(
self._collection.add,
ids=ids,
embeddings=embeddings,
metadatas=enriched_metadatas,
documents=documents,
)
|
count
async
count(scope: TenantScope) -> int
Return the number of stored vectors for the tenant scope.
Without this method EpisodicLayer.count_events() falls back to 0,
silently disabling consolidation phases gated on episodic volume for
Chroma-backed deployments. collection.count() is not tenant-aware,
so filter by tenant metadata. Round-2 fix.
Source code in src/symfonic/memory/backends/chroma.py
| async def count(self, scope: TenantScope) -> int:
"""Return the number of stored vectors for the tenant scope.
Without this method EpisodicLayer.count_events() falls back to 0,
silently disabling consolidation phases gated on episodic volume for
Chroma-backed deployments. ``collection.count()`` is not tenant-aware,
so filter by tenant metadata. Round-2 fix.
"""
result = await asyncio.to_thread(
self._collection.get,
where={"tenant_id": scope.tenant_id},
include=[],
)
ids = result.get("ids", []) if result else []
return len(ids)
|
delete
async
delete(scope: TenantScope, ids: list[str]) -> None
Delete vectors by their IDs, scoped to the tenant.
Source code in src/symfonic/memory/backends/chroma.py
| async def delete(self, scope: TenantScope, ids: list[str]) -> None:
"""Delete vectors by their IDs, scoped to the tenant."""
await asyncio.to_thread(
self._collection.delete,
ids=ids,
where={"tenant_id": scope.tenant_id},
)
|
search
async
search(
scope: TenantScope,
query_embedding: list[float],
top_k: int = 5,
) -> list[dict[str, Any]]
Search for similar vectors within the tenant scope.
Source code in src/symfonic/memory/backends/chroma.py
| async def search(
self,
scope: TenantScope,
query_embedding: list[float],
top_k: int = 5,
) -> list[dict[str, Any]]:
"""Search for similar vectors within the tenant scope."""
results = await asyncio.to_thread(
self._collection.query,
query_embeddings=[query_embedding],
n_results=top_k,
where=_prefix_where(scope),
)
output: list[dict[str, Any]] = []
if results and results.get("ids"):
ids = results["ids"][0]
distances = results.get("distances", [[]])[0]
metadatas = results.get("metadatas", [[]])[0]
documents = results.get("documents", [[]])[0]
for i, doc_id in enumerate(ids):
score = 1.0 - distances[i] if i < len(distances) else 0.0
output.append({
"id": doc_id,
"score": score,
"metadata": metadatas[i] if i < len(metadatas) else {},
"document": documents[i] if i < len(documents) else "",
})
return output
|