This commit is contained in:
Anush 2026-09-03 14:19:50 -07:00 committed by GitHub
commit 4ec68b70ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 525 additions and 7 deletions

View File

@ -307,7 +307,7 @@ LLM_OPENAI_API_KEY=your-api-key-here
# =============================================================================
# Vector Store Settings
# =============================================================================
# Vector store type: "pgvector", "turbopuffer", or "lancedb"
# Vector store type: "pgvector", "turbopuffer", "lancedb", or "qdrant"
VECTOR_STORE_TYPE=pgvector
# Migration flag: set to true when migration from pgvector is complete
@ -330,5 +330,14 @@ VECTOR_STORE_MIGRATED=false
# LanceDB-specific settings (local embedded mode)
# VECTOR_STORE_LANCEDB_PATH=./lancedb_data
# Qdrant-specific settings
# VECTOR_STORE_QDRANT_URL=http://localhost:6333
# VECTOR_STORE_QDRANT_API_KEY=your-qdrant-api-key
# VECTOR_STORE_QDRANT_PREFER_GRPC=false
# VECTOR_STORE_QDRANT_GRPC_PORT=6334
# VECTOR_STORE_QDRANT_HTTPS=false
# VECTOR_STORE_QDRANT_PREFIX=
# VECTOR_STORE_QDRANT_TIMEOUT=30
# Reconciliation interval for background sync (default: 5 minutes)
# VECTOR_STORE_RECONCILIATION_INTERVAL_SECONDS=300

View File

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
### Added
- Qdrant vector store backend (`VECTOR_STORE_TYPE=qdrant`) as an optional `qdrant` extra (#683)
## [3.1.1] - 2026-09-02
### Changed

View File

@ -279,7 +279,7 @@ DEFAULT_LOCK_TTL_SECONDS = 5
# Vector store settings
[vector_store]
# Vector store type: "pgvector", "turbopuffer", or "lancedb"
# Vector store type: "pgvector", "turbopuffer", "lancedb", or "qdrant"
TYPE = "pgvector"
# Migration flag: set to true when migration from pgvector is complete
MIGRATED = false
@ -288,4 +288,11 @@ NAMESPACE = "honcho"
# TURBOPUFFER_API_KEY = "your-turbopuffer-api-key"
# TURBOPUFFER_REGION = "us-east-1"
LANCEDB_PATH = "./lancedb_data"
# QDRANT_URL = "http://localhost:6333"
# QDRANT_API_KEY = "your-qdrant-api-key"
# QDRANT_PREFER_GRPC = false
# QDRANT_GRPC_PORT = 6334
# QDRANT_HTTPS = false
# QDRANT_PREFIX = ""
# QDRANT_TIMEOUT = 30
RECONCILIATION_INTERVAL_SECONDS = 300

View File

@ -590,7 +590,7 @@ WEBHOOK_MAX_WORKSPACE_LIMIT=10
### Vector Store
```bash
VECTOR_STORE_TYPE=pgvector # Options: pgvector, turbopuffer, lancedb
VECTOR_STORE_TYPE=pgvector # Options: pgvector, turbopuffer, lancedb, qdrant
VECTOR_STORE_MIGRATED=false
VECTOR_STORE_NAMESPACE=honcho
# Embedding dim is configured via EMBEDDING_VECTOR_DIMENSIONS — see the
@ -602,6 +602,15 @@ VECTOR_STORE_TURBOPUFFER_REGION=us-east-1
# LanceDB-specific
VECTOR_STORE_LANCEDB_PATH=./lancedb_data
# Qdrant-specific
VECTOR_STORE_QDRANT_URL=http://localhost:6333
VECTOR_STORE_QDRANT_API_KEY=your-qdrant-api-key # optional
VECTOR_STORE_QDRANT_PREFER_GRPC=false
VECTOR_STORE_QDRANT_GRPC_PORT=6334
VECTOR_STORE_QDRANT_HTTPS=false # optional, inferred from URL scheme
VECTOR_STORE_QDRANT_PREFIX= # optional, for reverse-proxy path prefix
VECTOR_STORE_QDRANT_TIMEOUT= # optional, request timeout in seconds
```
LanceDB is an optional extra and is not included in the default Docker image. Build with `docker build --build-arg INSTALL_LANCEDB=true .` (or `INSTALL_LANCEDB=true docker compose up -d --build`), or run `uv sync --extra lancedb` for manual setups. Note the extra is unavailable on Intel macOS.

View File

@ -43,6 +43,9 @@ lancedb = [
"lancedb>=0.25.3; sys_platform != \"darwin\" or platform_machine != \"x86_64\"",
"pyarrow>=19.0.0",
]
qdrant = [
"qdrant-client>=1.18.0",
]
[dependency-groups]
dev = [
"pytest>=8.2.2",

View File

@ -1428,12 +1428,12 @@ class DreamSettings(HonchoSettings):
class VectorStoreSettings(HonchoSettings):
"""Settings for vector store (pgvector, Turbopuffer, or LanceDB)."""
"""Settings for vector store (pgvector, Turbopuffer, LanceDB or Qdrant)."""
model_config = SettingsConfigDict(env_prefix="VECTOR_STORE_", extra="ignore") # pyright: ignore
# Vector store type to use
TYPE: Literal["pgvector", "turbopuffer", "lancedb"] = "pgvector"
TYPE: Literal["pgvector", "turbopuffer", "lancedb", "qdrant"] = "pgvector"
MIGRATED: bool = False
@ -1459,6 +1459,15 @@ class VectorStoreSettings(HonchoSettings):
# LanceDB-specific settings (local embedded mode)
LANCEDB_PATH: str = "./lancedb_data"
# Qdrant-specific settings
QDRANT_URL: str = "http://localhost:6333"
QDRANT_API_KEY: str | None = None
QDRANT_PREFER_GRPC: bool = False
QDRANT_GRPC_PORT: int = 6334
QDRANT_HTTPS: bool | None = None
QDRANT_PREFIX: str | None = None
QDRANT_TIMEOUT: int | None = None
RECONCILIATION_INTERVAL_SECONDS: Annotated[int, Field(default=300, gt=0)] = (
300 # 5 minutes
)

View File

@ -76,7 +76,7 @@ async def validate_embedding_schema(
dims = await _introspect_pgvector_dims_with_retry(engine, schema)
_assert_pgvector_dims_match(dims, schema=schema, target_dim=target_dim)
if s.VECTOR_STORE.TYPE in ("turbopuffer", "lancedb"):
if s.VECTOR_STORE.TYPE in ("turbopuffer", "lancedb", "qdrant"):
await _sample_external_namespaces(engine, target_dim=target_dim)

View File

@ -214,6 +214,18 @@ def _create_store_by_type(store_type: str) -> VectorStore:
) from exc
return LanceDBVectorStore()
elif store_type == "qdrant":
try:
from src.vector_store.qdrant import QdrantVectorStore
except ImportError as exc:
raise RuntimeError(
"VECTOR_STORE.TYPE is set to 'qdrant', but the 'qdrant-client' "
+ "package could not be imported. Install Honcho's 'qdrant' extra "
+ "(for example, `uv sync --extra qdrant`)"
+ f"Original import error: {exc}"
) from exc
return QdrantVectorStore()
else:
raise ValueError(f"Unknown vector store type: {store_type}")

167
src/vector_store/qdrant.py Normal file
View File

@ -0,0 +1,167 @@
"""Qdrant vector store implementation."""
import logging
import uuid
from typing import Any
from qdrant_client import AsyncQdrantClient, models
from src.config import settings
from src.exceptions import VectorStoreError
from . import VectorQueryResult, VectorRecord, VectorStore
logger = logging.getLogger(__name__)
# Qdrant only allows UUIDs and +ve integers as point IDs.
# Ref: https://qdrant.tech/documentation/manage-data/points/#point-ids
# So we convert arbitrary strings to deterministic UUIDs.
def _point_id(string_id: str) -> str:
return str(uuid.uuid5(uuid.NAMESPACE_DNS, string_id))
class QdrantVectorStore(VectorStore):
"""Qdrant implementation of VectorStore. Each namespace maps to a collection."""
_client: AsyncQdrantClient
_vector_size: int
def __init__(self) -> None:
super().__init__()
self._client = AsyncQdrantClient(
url=settings.VECTOR_STORE.QDRANT_URL,
api_key=settings.VECTOR_STORE.QDRANT_API_KEY,
prefer_grpc=settings.VECTOR_STORE.QDRANT_PREFER_GRPC,
grpc_port=settings.VECTOR_STORE.QDRANT_GRPC_PORT,
https=settings.VECTOR_STORE.QDRANT_HTTPS,
prefix=settings.VECTOR_STORE.QDRANT_PREFIX,
timeout=settings.VECTOR_STORE.QDRANT_TIMEOUT,
)
self._vector_size = settings.VECTOR_STORE.DIMENSIONS
async def _ensure_collection(self, name: str) -> None:
if not await self._client.collection_exists(name):
await self._client.create_collection(
collection_name=name,
vectors_config=models.VectorParams(
size=self._vector_size,
distance=models.Distance.COSINE,
),
)
def _build_filter(self, filters: dict[str, Any]) -> models.Filter | None:
conditions: list[models.Condition] = []
for k, v in filters.items():
if isinstance(v, dict) and "in" in v:
conditions.append(
models.FieldCondition(key=k, match=models.MatchAny(any=v["in"])) # pyright: ignore[reportUnknownArgumentType]
)
elif isinstance(v, list):
conditions.append(
models.FieldCondition(key=k, match=models.MatchAny(any=v)) # pyright: ignore[reportUnknownArgumentType]
)
else:
conditions.append(
models.FieldCondition(key=k, match=models.MatchValue(value=v)) # pyright: ignore[reportArgumentType]
)
return models.Filter(must=conditions) if conditions else None
async def upsert_many(self, namespace: str, vectors: list[VectorRecord]) -> None:
if not vectors:
return
await self._ensure_collection(namespace)
points = [
models.PointStruct(
id=_point_id(v.id),
vector=v.embedding,
payload={**v.metadata, "_id": v.id},
)
for v in vectors
]
try:
await self._client.upsert(collection_name=namespace, points=points)
except Exception as e:
logger.exception(
f"Failed to upsert {len(vectors)} vectors to namespace {namespace}"
)
raise VectorStoreError(
f"Qdrant upsert failed for namespace {namespace}"
) from e
async def query(
self,
namespace: str,
embedding: list[float],
*,
top_k: int = 10,
filters: dict[str, Any] | None = None,
max_distance: float | None = None,
include_attributes: bool | list[str] = True,
) -> list[VectorQueryResult]:
if not await self._client.collection_exists(namespace):
return []
if include_attributes is False:
with_payload: bool | list[str] = ["_id"]
elif isinstance(include_attributes, list):
with_payload = ["_id", *include_attributes]
else:
with_payload = True
response = await self._client.query_points(
collection_name=namespace,
query=embedding,
limit=top_k,
query_filter=self._build_filter(filters) if filters else None,
with_payload=with_payload,
)
results: list[VectorQueryResult] = []
for hit in response.points:
dist = 1.0 - float(hit.score)
if max_distance is not None and dist > max_distance:
continue
payload = dict(hit.payload or {})
results.append(
VectorQueryResult(
id=payload.pop("_id", str(hit.id)),
score=dist,
metadata=payload,
)
)
return results
async def delete_many(self, namespace: str, ids: list[str]) -> None:
if not ids:
return
if not await self._client.collection_exists(namespace):
return
await self._client.delete(
collection_name=namespace,
points_selector=[_point_id(i) for i in ids],
)
async def delete_namespace(self, namespace: str) -> None:
if await self._client.collection_exists(namespace):
await self._client.delete_collection(namespace)
async def close(self) -> None:
await self._client.close()
async def probe_namespace_dim(self, namespace: str) -> int | None:
if not await self._client.collection_exists(namespace):
return None
vectors = (await self._client.get_collection(namespace)).config.params.vectors
if vectors is None:
raise VectorStoreError(
f"Qdrant collection {namespace!r} has no vector configuration"
)
if isinstance(vectors, dict):
if not vectors:
raise VectorStoreError(
f"Qdrant collection {namespace!r} has empty named-vector configuration"
)
vectors = next(iter(vectors.values()))
return int(vectors.size)

View File

@ -0,0 +1,178 @@
from __future__ import annotations
import uuid
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from qdrant_client import models
from src.exceptions import VectorStoreError
from src.vector_store.qdrant import (
QdrantVectorStore,
_point_id, # pyright: ignore[reportPrivateUsage]
)
@pytest.fixture
def store(monkeypatch: pytest.MonkeyPatch) -> QdrantVectorStore:
monkeypatch.setattr("src.vector_store.qdrant.AsyncQdrantClient", MagicMock())
return QdrantVectorStore()
def _mock_client(store: QdrantVectorStore) -> MagicMock:
client = MagicMock()
client.collection_exists = AsyncMock(return_value=True)
client.query_points = AsyncMock(return_value=SimpleNamespace(points=[]))
client.get_collection = AsyncMock()
store._client = client # pyright: ignore[reportPrivateUsage]
return client
def _hit(*, id: str, score: float, payload: dict[str, Any]) -> SimpleNamespace:
return SimpleNamespace(id=id, score=score, payload=payload)
def test_point_id_is_deterministic_and_a_valid_uuid() -> None:
assert _point_id("user_123") == _point_id("user_123")
assert _point_id("user_123") != _point_id("user_456")
uuid.UUID(_point_id("user_123"))
def test_build_filter_membership(store: QdrantVectorStore) -> None:
f = store._build_filter({"session_name": {"in": ["s1", "s2"]}}) # pyright: ignore[reportPrivateUsage]
assert f is not None and f.must is not None
@pytest.mark.asyncio
async def test_query_returns_empty_when_collection_missing(
store: QdrantVectorStore,
) -> None:
client = _mock_client(store)
client.collection_exists = AsyncMock(return_value=False)
results = await store.query("honcho.msg.missing", [0.1, 0.2, 0.3, 0.4])
assert results == []
client.query_points.assert_not_awaited()
@pytest.mark.asyncio
async def test_query_include_attributes_false_still_recovers_id(
store: QdrantVectorStore,
) -> None:
client = _mock_client(store)
await store.query("honcho.msg.test", [0.1, 0.2, 0.3, 0.4], include_attributes=False)
assert client.query_points.await_args.kwargs["with_payload"] == ["_id"]
@pytest.mark.asyncio
async def test_query_attribute_list_projects_id_plus_listed(
store: QdrantVectorStore,
) -> None:
client = _mock_client(store)
await store.query(
"honcho.msg.test",
[0.1, 0.2, 0.3, 0.4],
include_attributes=["message_id"],
)
assert client.query_points.await_args.kwargs["with_payload"] == [
"_id",
"message_id",
]
@pytest.mark.asyncio
async def test_query_converts_hits_to_results_with_distance_and_metadata(
store: QdrantVectorStore,
) -> None:
client = _mock_client(store)
client.query_points = AsyncMock(
return_value=SimpleNamespace(
points=[
_hit(
id="<uuid-1>",
score=0.88,
payload={"_id": "vec_1", "message_id": "msg_1"},
),
_hit(id="<uuid-2>", score=0.66, payload={"_id": "vec_2"}),
]
)
)
results = await store.query("honcho.msg.test", [0.1, 0.2, 0.3, 0.4])
assert [r.id for r in results] == ["vec_1", "vec_2"]
assert results[0].score == 1.0 - 0.88
assert results[1].score == 1.0 - 0.66
assert results[0].metadata == {"message_id": "msg_1"}
assert results[1].metadata == {}
@pytest.mark.asyncio
async def test_query_filters_by_max_distance(store: QdrantVectorStore) -> None:
client = _mock_client(store)
client.query_points = AsyncMock(
return_value=SimpleNamespace(
points=[
_hit(id="<uuid-1>", score=0.95, payload={"_id": "vec_close"}),
_hit(id="<uuid-2>", score=0.1, payload={"_id": "vec_far"}),
]
)
)
results = await store.query(
"honcho.msg.test",
[0.1, 0.2, 0.3, 0.4],
max_distance=0.5,
)
assert [r.id for r in results] == ["vec_close"]
@pytest.mark.asyncio
async def test_probe_returns_none_for_missing_collection(
store: QdrantVectorStore,
) -> None:
client = _mock_client(store)
client.collection_exists = AsyncMock(return_value=False)
assert await store.probe_namespace_dim("does_not_exist") is None
@pytest.mark.asyncio
async def test_probe_returns_declared_dim(store: QdrantVectorStore) -> None:
client = _mock_client(store)
client.get_collection = AsyncMock(
return_value=SimpleNamespace(
config=SimpleNamespace(
params=SimpleNamespace(
vectors=models.VectorParams(
size=768, distance=models.Distance.COSINE
)
)
)
)
)
assert await store.probe_namespace_dim("probe_test") == 768
@pytest.mark.asyncio
async def test_probe_raises_when_vector_config_missing(
store: QdrantVectorStore,
) -> None:
client = _mock_client(store)
client.get_collection = AsyncMock(
return_value=SimpleNamespace(
config=SimpleNamespace(params=SimpleNamespace(vectors=None))
)
)
with pytest.raises(VectorStoreError):
await store.probe_namespace_dim("corrupt")

120
uv.lock
View File

@ -1,6 +1,10 @@
version = 1
revision = 3
requires-python = ">=3.13"
resolution-markers = [
"python_full_version >= '3.14'",
"python_full_version < '3.14'",
]
[options]
exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
@ -813,6 +817,37 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" },
]
[[package]]
name = "grpcio"
version = "1.80.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" },
{ url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" },
{ url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" },
{ url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" },
{ url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" },
{ url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" },
{ url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" },
{ url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" },
{ url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" },
{ url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" },
{ url = "https://files.pythonhosted.org/packages/c5/6d/e65307ce20f5a09244ba9e9d8476e99fb039de7154f37fb85f26978b59c3/grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e", size = 6017376, upload-time = "2026-03-30T08:48:10.005Z" },
{ url = "https://files.pythonhosted.org/packages/69/10/9cef5d9650c72625a699c549940f0abb3c4bfdb5ed45a5ce431f92f31806/grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f", size = 12018133, upload-time = "2026-03-30T08:48:12.927Z" },
{ url = "https://files.pythonhosted.org/packages/04/82/983aabaad82ba26113caceeb9091706a0696b25da004fe3defb5b346e15b/grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9", size = 6574748, upload-time = "2026-03-30T08:48:16.386Z" },
{ url = "https://files.pythonhosted.org/packages/07/d7/031666ef155aa0bf399ed7e19439656c38bbd143779ae0861b038ce82abd/grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14", size = 7277711, upload-time = "2026-03-30T08:48:19.627Z" },
{ url = "https://files.pythonhosted.org/packages/e8/43/f437a78f7f4f1d311804189e8f11fb311a01049b2e08557c1068d470cb2e/grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05", size = 6785372, upload-time = "2026-03-30T08:48:22.373Z" },
{ url = "https://files.pythonhosted.org/packages/93/3d/f6558e9c6296cb4227faa5c43c54a34c68d32654b829f53288313d16a86e/grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1", size = 7395268, upload-time = "2026-03-30T08:48:25.638Z" },
{ url = "https://files.pythonhosted.org/packages/06/21/0fdd77e84720b08843c371a2efa6f2e19dbebf56adc72df73d891f5506f0/grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f", size = 8392000, upload-time = "2026-03-30T08:48:28.974Z" },
{ url = "https://files.pythonhosted.org/packages/f5/68/67f4947ed55d2e69f2cc199ab9fd85e0a0034d813bbeef84df6d2ba4d4b7/grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e", size = 7828477, upload-time = "2026-03-30T08:48:32.054Z" },
{ url = "https://files.pythonhosted.org/packages/44/b6/8d4096691b2e385e8271911a0de4f35f0a6c7d05aff7098e296c3de86939/grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae", size = 4218563, upload-time = "2026-03-30T08:48:34.538Z" },
{ url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
@ -822,6 +857,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "h2"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "hpack" },
{ name = "hyperframe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" },
]
[[package]]
name = "honcho"
version = "3.1.1"
@ -863,6 +911,9 @@ lancedb = [
{ name = "lancedb", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" },
{ name = "pyarrow" },
]
qdrant = [
{ name = "qdrant-client" },
]
[package.dev-dependencies]
dev = [
@ -906,6 +957,7 @@ requires-dist = [
{ name = "pydantic-settings", specifier = ">=2.10.1" },
{ name = "pyjwt", specifier = ">=2.10.0" },
{ name = "python-dotenv", specifier = ">=1.0.0" },
{ name = "qdrant-client", marker = "extra == 'qdrant'", specifier = ">=1.18.0" },
{ name = "redis", specifier = ">=7.0.0,<8.0.0" },
{ name = "rich", specifier = ">=13.7.1" },
{ name = "scikit-learn", specifier = ">=1.6.0" },
@ -916,7 +968,7 @@ requires-dist = [
{ name = "turbopuffer", specifier = ">=1.8.1" },
{ name = "typing-extensions", specifier = ">=4.11.0" },
]
provides-extras = ["lancedb"]
provides-extras = ["lancedb", "qdrant"]
[package.metadata.requires-dev]
dev = [
@ -997,6 +1049,15 @@ requires-dist = [
]
provides-extras = ["dev"]
[[package]]
name = "hpack"
version = "4.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
@ -1047,6 +1108,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[package.optional-dependencies]
http2 = [
{ name = "h2" },
]
[[package]]
name = "hyperframe"
version = "6.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" },
]
[[package]]
name = "identify"
version = "2.6.19"
@ -1776,6 +1851,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "portalocker"
version = "3.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pywin32", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" },
]
[[package]]
name = "pre-commit"
version = "4.6.0"
@ -2378,6 +2465,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl", hash = "sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645", size = 29254, upload-time = "2026-04-27T10:51:24.997Z" },
]
[[package]]
name = "pywin32"
version = "311"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
{ url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
{ url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
{ url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
{ url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
@ -2414,6 +2514,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
name = "qdrant-client"
version = "1.18.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "grpcio" },
{ name = "httpx", extra = ["http2"] },
{ name = "numpy" },
{ name = "portalocker" },
{ name = "protobuf" },
{ name = "pydantic" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/65/45/5b1bdd15a3c7730eefb9c113600829e20d689b82b5a23f9e07d107094004/qdrant_client-1.18.0.tar.gz", hash = "sha256:52e8ece1a7d40519801bf0b70713bfa0f6b7ae28c7275bbe0b0286fbed7f6db4", size = 352580, upload-time = "2026-05-11T14:12:38.702Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d6/10/c437bd2ac41ef30d3019063e6ce537dc111e9214473b337ee88f7fa6359a/qdrant_client-1.18.0-py3-none-any.whl", hash = "sha256:093aa8cf8a420ee3ad2a68b007e1378d7992b2600e0b53c193fc172674f659cd", size = 398126, upload-time = "2026-05-11T14:12:36.998Z" },
]
[[package]]
name = "redis"
version = "7.4.0"