226 lines
6.0 KiB
Python
226 lines
6.0 KiB
Python
"""
|
|
Vector store abstraction layer for Honcho.
|
|
"""
|
|
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from src.config import settings
|
|
|
|
|
|
@dataclass
|
|
class VectorRecord:
|
|
"""A single vector record to be stored in the vector store."""
|
|
|
|
id: str
|
|
embedding: list[float]
|
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass
|
|
class QueryResult:
|
|
"""A single result from a vector query."""
|
|
|
|
id: str
|
|
score: float # Distance/similarity score (lower = more similar for cosine distance)
|
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
class VectorStore(ABC):
|
|
"""
|
|
Abstract base class for vector store implementations.
|
|
|
|
All vector operations are namespace-scoped. Namespaces map to:
|
|
- Document embeddings: {prefix}.{workspace}.{observer}.{observed} (per collection)
|
|
- Message embeddings: {prefix}.{workspace}.messages (per workspace)
|
|
|
|
Note: Period (.) is used as the delimiter since vector stores (Turbopuffer, LanceDB)
|
|
only allow [A-Za-z0-9-_.] in namespace names, and period is not allowed in
|
|
workspace/peer IDs (which only allow [A-Za-z0-9_-]).
|
|
"""
|
|
|
|
namespace_prefix: str
|
|
|
|
def __init__(self):
|
|
"""
|
|
Initialize the vector store.
|
|
"""
|
|
self.namespace_prefix = settings.VECTOR_STORE.NAMESPACE
|
|
|
|
# === Namespace helpers ===
|
|
def get_document_namespace(
|
|
self, workspace_name: str, observer: str, observed: str
|
|
) -> str:
|
|
"""
|
|
Get the namespace for document embeddings (per collection).
|
|
|
|
Args:
|
|
workspace_name: Name of the workspace
|
|
observer: Name of the observing peer
|
|
observed: Name of the observed peer
|
|
|
|
Returns:
|
|
Namespace string in format: {prefix}.{workspace}.{observer}.{observed}
|
|
"""
|
|
return f"{self.namespace_prefix}.{workspace_name}.{observer}.{observed}"
|
|
|
|
def get_message_namespace(self, workspace_name: str) -> str:
|
|
"""
|
|
Get the namespace for message embeddings (per workspace).
|
|
|
|
Args:
|
|
workspace_name: Name of the workspace
|
|
|
|
Returns:
|
|
Namespace string in format: {prefix}.{workspace}.messages
|
|
"""
|
|
return f"{self.namespace_prefix}.{workspace_name}.messages"
|
|
|
|
# === Core operations ===
|
|
@abstractmethod
|
|
async def upsert(
|
|
self,
|
|
namespace: str,
|
|
vector: VectorRecord,
|
|
) -> None:
|
|
"""
|
|
Upsert a single vector into the store.
|
|
|
|
Args:
|
|
namespace: The namespace to store the vector in
|
|
vector: VectorRecord containing id, embedding, and optional metadata
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def upsert_many(
|
|
self,
|
|
namespace: str,
|
|
vectors: list[VectorRecord],
|
|
) -> None:
|
|
"""
|
|
Upsert multiple vectors into the store.
|
|
|
|
Args:
|
|
namespace: The namespace to store the vectors in
|
|
vectors: List of VectorRecord objects to upsert
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def query(
|
|
self,
|
|
namespace: str,
|
|
embedding: list[float],
|
|
*,
|
|
top_k: int = 10,
|
|
filters: dict[str, Any] | None = None,
|
|
max_distance: float | None = None,
|
|
) -> list[QueryResult]:
|
|
"""
|
|
Query for similar vectors.
|
|
|
|
Args:
|
|
namespace: The namespace to query
|
|
embedding: The query embedding vector
|
|
top_k: Maximum number of results to return
|
|
filters: Optional metadata filters
|
|
max_distance: Optional maximum distance threshold (cosine distance)
|
|
|
|
Returns:
|
|
List of QueryResult objects, ordered by similarity (most similar first)
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def delete_many(self, namespace: str, ids: list[str]) -> None:
|
|
"""
|
|
Delete multiple vectors from the store.
|
|
|
|
Args:
|
|
namespace: The namespace containing the vectors
|
|
ids: List of vector identifiers to delete
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def delete_namespace(self, namespace: str) -> None:
|
|
"""
|
|
Delete an entire namespace and all its vectors.
|
|
|
|
Args:
|
|
namespace: The namespace to delete
|
|
"""
|
|
...
|
|
|
|
|
|
# Singleton instance
|
|
_vector_store_instance: VectorStore | None = None
|
|
|
|
|
|
def _create_vector_store() -> VectorStore:
|
|
"""
|
|
Create a new vector store instance based on configuration.
|
|
|
|
Returns:
|
|
The vector store instance based on configuration.
|
|
|
|
Raises:
|
|
ValueError: If the configured vector store type is invalid.
|
|
"""
|
|
store_type = settings.VECTOR_STORE.TYPE
|
|
|
|
if store_type == "turbopuffer":
|
|
from src.vector_store.turbopuffer import TurbopufferVectorStore
|
|
|
|
return TurbopufferVectorStore()
|
|
elif store_type == "lancedb":
|
|
from src.vector_store.lancedb import LanceDBVectorStore
|
|
|
|
return LanceDBVectorStore()
|
|
else:
|
|
raise ValueError(f"Unknown vector store type: {store_type}")
|
|
|
|
|
|
def get_vector_store() -> VectorStore:
|
|
"""
|
|
FastAPI dependency that provides the configured vector store instance (singleton).
|
|
|
|
This function is designed to be used as a FastAPI dependency:
|
|
vector_store: VectorStore = Depends(get_vector_store)
|
|
|
|
It can also be called directly for non-request contexts (e.g., background tasks).
|
|
|
|
Returns:
|
|
The vector store instance based on configuration.
|
|
|
|
Raises:
|
|
ValueError: If the configured vector store type is invalid.
|
|
"""
|
|
global _vector_store_instance
|
|
|
|
if _vector_store_instance is None:
|
|
_vector_store_instance = _create_vector_store()
|
|
|
|
return _vector_store_instance
|
|
|
|
|
|
def reset_vector_store() -> None:
|
|
"""
|
|
Reset the vector store singleton instance.
|
|
|
|
This is primarily useful for testing to ensure a fresh instance is created.
|
|
"""
|
|
global _vector_store_instance
|
|
_vector_store_instance = None
|
|
|
|
|
|
__all__ = [
|
|
"VectorStore",
|
|
"VectorRecord",
|
|
"QueryResult",
|
|
"get_vector_store",
|
|
"reset_vector_store",
|
|
]
|