From 256ed8c9ade3d85ca72090cb9d63a89a8df25f53 Mon Sep 17 00:00:00 2001 From: Anush008 Date: Thu, 14 May 2026 18:37:35 +0530 Subject: [PATCH 1/2] feat: Adds Qdrant Vector Search --- .env.template | 11 +- CHANGELOG.md | 3 +- README.md | 2 +- config.toml.example | 9 +- docs/changelog/introduction.mdx | 2 +- docs/v3/contributing/configuration.mdx | 11 +- pyproject.toml | 1 + src/config.py | 13 ++- src/vector_store/__init__.py | 4 + src/vector_store/qdrant.py | 138 ++++++++++++++++++++++++ uv.lock | 141 ++++++++++++++++++++++++- 11 files changed, 326 insertions(+), 9 deletions(-) create mode 100644 src/vector_store/qdrant.py diff --git a/.env.template b/.env.template index 3b5ecfc0..6203b4e5 100644 --- a/.env.template +++ b/.env.template @@ -266,7 +266,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 @@ -289,5 +289,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 diff --git a/CHANGELOG.md b/CHANGELOG.md index ab9302c6..1c8a9fe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Support for using [Qdrant](https://qdrant.tech/) as the vector search provider. - New `src/llm/` package as the single owner of provider runtime: clients, backends, history adapters, tool loop, request builder, credentials, and caching policy - `AttemptPlan` dataclass captures per-retry provider selection (client, model, reasoning_effort, thinking_budget_tokens, selected_config) and pins it across stream-final retries so streaming doesn't bounce back to primary after the tool loop has settled on fallback - Gemini JSON-schema sanitizer for `function_declarations` — strips keywords Gemini's validator rejects (`additionalProperties`, `allOf`, etc.) while preserving semantics for all other backends @@ -169,7 +170,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Prometheus token tracking for deriver and dialectic operations - n8n integration - Cloud Events for auditable telemetry -- External Vector Store support for turbopuffer and lancedb with reconciliation flow +- External Vector Store support for turbopuffer, lancedb, and qdrant with reconciliation flow ### Changed diff --git a/README.md b/README.md index 9a7cf541..0f2da7bb 100644 --- a/README.md +++ b/README.md @@ -412,7 +412,7 @@ Then modify the values as needed. The TOML file is organized into sections: - `[webhook]` - Webhook configuration - `[metrics]` - Prometheus pull-based metrics - `[telemetry]` - CloudEvents telemetry for analytics -- `[vector_store]` - Vector store configuration (pgvector, turbopuffer, or lancedb) +- `[vector_store]` - Vector store configuration (pgvector, turbopuffer, lancedb, or qdrant) - `[sentry]` - Error tracking and monitoring settings ### Using Environment Variables diff --git a/config.toml.example b/config.toml.example index 99e2e2dc..d300ee3e 100644 --- a/config.toml.example +++ b/config.toml.example @@ -250,7 +250,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 @@ -261,4 +261,11 @@ DIMENSIONS = 1536 # 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 diff --git a/docs/changelog/introduction.mdx b/docs/changelog/introduction.mdx index d2d31331..4f5e4963 100644 --- a/docs/changelog/introduction.mdx +++ b/docs/changelog/introduction.mdx @@ -156,7 +156,7 @@ Welcome to the Honcho changelog! This section documents all notable changes to t - Prometheus token tracking for deriver and dialectic operations - n8n integration - Cloud Events for auditable telemetry - - External Vector Store support for turbopuffer and lancedb with reconciliation flow + - External Vector Store support for turbopuffer, lancedb, and qdrant with reconciliation flow ### Changed diff --git a/docs/v3/contributing/configuration.mdx b/docs/v3/contributing/configuration.mdx index 076ec465..623910bc 100644 --- a/docs/v3/contributing/configuration.mdx +++ b/docs/v3/contributing/configuration.mdx @@ -456,7 +456,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 VECTOR_STORE_DIMENSIONS=1536 @@ -467,6 +467,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 ``` ## Monitoring diff --git a/pyproject.toml b/pyproject.toml index 0a2a49ca..1abb2451 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ "json-repair>=0.49.0", "turbopuffer>=1.8.1", "lancedb>=0.25.3", + "qdrant-client>=1.18.0", "pyarrow>=19.0.0", "redis>=7.0.0,<8.0.0", "cashews[redis]==7.4.4", diff --git a/src/config.py b/src/config.py index a4e814f8..dae6502c 100644 --- a/src/config.py +++ b/src/config.py @@ -1166,12 +1166,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 @@ -1197,6 +1197,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 ) diff --git a/src/vector_store/__init__.py b/src/vector_store/__init__.py index 5a22abd9..7b0ed98f 100644 --- a/src/vector_store/__init__.py +++ b/src/vector_store/__init__.py @@ -191,6 +191,10 @@ def _create_store_by_type(store_type: str) -> VectorStore: from src.vector_store.lancedb import LanceDBVectorStore return LanceDBVectorStore() + elif store_type == "qdrant": + from src.vector_store.qdrant import QdrantVectorStore + + return QdrantVectorStore() else: raise ValueError(f"Unknown vector store type: {store_type}") diff --git a/src/vector_store/qdrant.py b/src/vector_store/qdrant.py new file mode 100644 index 00000000..6a06adc9 --- /dev/null +++ b/src/vector_store/qdrant.py @@ -0,0 +1,138 @@ +"""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] + ) + 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={"_id": v.id, **v.metadata}, + ) + 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, + ) -> list[VectorQueryResult]: + if not await self._client.collection_exists(namespace): + return [] + + 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=True, + ) + + 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() diff --git a/uv.lock b/uv.lock index c0bbd9db..568debcc 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,8 @@ requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", - "python_full_version < '3.13'", + "python_full_version == '3.12.*'", + "python_full_version < '3.12'", ] [manifest] @@ -1118,6 +1119,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, ] +[[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/5d/db/1d56e5f5823257b291962d6c0ce106146c6447f405b60b234c4f222a7cde/grpcio-1.80.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:dfab85db094068ff42e2a3563f60ab3dddcc9d6488a35abf0132daec13209c8a", size = 6055009, upload-time = "2026-03-30T08:46:46.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/18/c83f3cad64c5ca63bca7e91e5e46b0d026afc5af9d0a9972472ceba294b3/grpcio-1.80.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5c07e82e822e1161354e32da2662f741a4944ea955f9f580ec8fb409dd6f6060", size = 12035295, upload-time = "2026-03-30T08:46:49.099Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8e/e14966b435be2dda99fbe89db9525ea436edc79780431a1c2875a3582644/grpcio-1.80.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2", size = 6610297, upload-time = "2026-03-30T08:46:52.123Z" }, + { url = "https://files.pythonhosted.org/packages/cc/26/d5eb38f42ce0e3fdc8174ea4d52036ef8d58cc4426cb800f2610f625dd75/grpcio-1.80.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3cb8130ba457d2aa09fa6b7c3ed6b6e4e6a2685fce63cb803d479576c4d80e21", size = 7300208, upload-time = "2026-03-30T08:46:54.859Z" }, + { url = "https://files.pythonhosted.org/packages/25/51/bd267c989f85a17a5b3eea65a6feb4ff672af41ca614e5a0279cc0ea381c/grpcio-1.80.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e5e478b3d14afd23f12e49e8b44c8684ac3c5f08561c43a5b9691c54d136ab", size = 6813442, upload-time = "2026-03-30T08:46:57.056Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d9/d80eef735b19e9169e30164bbf889b46f9df9127598a83d174eb13a48b26/grpcio-1.80.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", size = 7414743, upload-time = "2026-03-30T08:46:59.682Z" }, + { url = "https://files.pythonhosted.org/packages/de/f2/567f5bd5054398ed6b0509b9a30900376dcf2786bd936812098808b49d8d/grpcio-1.80.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8502122a3cc1714038e39a0b071acb1207ca7844208d5ea0d091317555ee7106", size = 8426046, upload-time = "2026-03-30T08:47:02.474Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/73ef0141b4732ff5eacd68430ff2512a65c004696997f70476a83e548e7e/grpcio-1.80.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6", size = 7851641, upload-time = "2026-03-30T08:47:05.462Z" }, + { url = "https://files.pythonhosted.org/packages/46/69/abbfa360eb229a8623bab5f5a4f8105e445bd38ce81a89514ba55d281ad0/grpcio-1.80.0-cp311-cp311-win32.whl", hash = "sha256:51b4a7189b0bef2aa30adce3c78f09c83526cf3dddb24c6a96555e3b97340440", size = 4154368, upload-time = "2026-03-30T08:47:08.027Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d4/ae92206d01183b08613e846076115f5ac5991bae358d2a749fa864da5699/grpcio-1.80.0-cp311-cp311-win_amd64.whl", hash = "sha256:02e64bb0bb2da14d947a49e6f120a75e947250aebe65f9629b62bb1f5c14e6e9", size = 4894235, upload-time = "2026-03-30T08:47:10.839Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" }, + { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, + { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, + { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" }, + { 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" @@ -1127,6 +1179,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.0.6" @@ -1154,6 +1219,7 @@ dependencies = [ { name = "pydantic-settings" }, { name = "pyjwt" }, { name = "python-dotenv" }, + { name = "qdrant-client" }, { name = "redis" }, { name = "rich" }, { name = "scikit-learn" }, @@ -1208,6 +1274,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", 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" }, @@ -1298,6 +1365,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" @@ -1362,6 +1438,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.16" @@ -2280,6 +2370,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.5.1" @@ -3029,6 +3131,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { 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" @@ -3084,6 +3205,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.2.0" From 54ce79a0c917311caebcd496a7f26fd965099156 Mon Sep 17 00:00:00 2001 From: Anush008 Date: Thu, 3 Sep 2026 13:42:45 +0530 Subject: [PATCH 2/2] chore: Review updates --- CHANGELOG.md | 8 +- docs/changelog/introduction.mdx | 2 +- pyproject.toml | 4 +- src/startup/embedding_validator.py | 2 +- src/vector_store/__init__.py | 10 +- src/vector_store/qdrant.py | 33 +++++- tests/vector_store/test_qdrant.py | 178 +++++++++++++++++++++++++++++ uv.lock | 38 ++---- 8 files changed, 239 insertions(+), 36 deletions(-) create mode 100644 tests/vector_store/test_qdrant.py diff --git a/CHANGELOG.md b/CHANGELOG.md index de9d9938..d4a7660d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.0] - 2026-08-25 ### Added @@ -380,7 +386,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Prometheus token tracking for deriver and dialectic operations - n8n integration - Cloud Events for auditable telemetry -- External Vector Store support for turbopuffer, lancedb, and qdrant with reconciliation flow +- External Vector Store support for turbopuffer and lancedb with reconciliation flow ### Changed diff --git a/docs/changelog/introduction.mdx b/docs/changelog/introduction.mdx index 54947cda..a152d16a 100644 --- a/docs/changelog/introduction.mdx +++ b/docs/changelog/introduction.mdx @@ -383,7 +383,7 @@ Welcome to the Honcho changelog! This section documents all notable changes to t - Prometheus token tracking for deriver and dialectic operations - n8n integration - Cloud Events for auditable telemetry - - External Vector Store support for turbopuffer, lancedb, and qdrant with reconciliation flow + - External Vector Store support for turbopuffer and lancedb with reconciliation flow ### Changed diff --git a/pyproject.toml b/pyproject.toml index 2ff7e1fe..efc47336 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,6 @@ dependencies = [ "typing-extensions>=4.11.0", "json-repair>=0.49.0", "turbopuffer>=1.8.1", - "qdrant-client>=1.18.0", "redis>=7.0.0,<8.0.0", "cashews[redis]==7.5.0", "scikit-learn>=1.6.0", @@ -44,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", diff --git a/src/startup/embedding_validator.py b/src/startup/embedding_validator.py index a397c9e2..d0584dff 100644 --- a/src/startup/embedding_validator.py +++ b/src/startup/embedding_validator.py @@ -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) diff --git a/src/vector_store/__init__.py b/src/vector_store/__init__.py index 6f9eb413..e5e6df10 100644 --- a/src/vector_store/__init__.py +++ b/src/vector_store/__init__.py @@ -215,7 +215,15 @@ def _create_store_by_type(store_type: str) -> VectorStore: return LanceDBVectorStore() elif store_type == "qdrant": - from src.vector_store.qdrant import QdrantVectorStore + 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: diff --git a/src/vector_store/qdrant.py b/src/vector_store/qdrant.py index 6a06adc9..56274002 100644 --- a/src/vector_store/qdrant.py +++ b/src/vector_store/qdrant.py @@ -57,6 +57,10 @@ class QdrantVectorStore(VectorStore): 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] @@ -71,7 +75,7 @@ class QdrantVectorStore(VectorStore): models.PointStruct( id=_point_id(v.id), vector=v.embedding, - payload={"_id": v.id, **v.metadata}, + payload={**v.metadata, "_id": v.id}, ) for v in vectors ] @@ -93,16 +97,24 @@ class QdrantVectorStore(VectorStore): 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=True, + with_payload=with_payload, ) results: list[VectorQueryResult] = [] @@ -136,3 +148,20 @@ class QdrantVectorStore(VectorStore): 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) diff --git a/tests/vector_store/test_qdrant.py b/tests/vector_store/test_qdrant.py new file mode 100644 index 00000000..85462503 --- /dev/null +++ b/tests/vector_store/test_qdrant.py @@ -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="", + score=0.88, + payload={"_id": "vec_1", "message_id": "msg_1"}, + ), + _hit(id="", 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="", score=0.95, payload={"_id": "vec_close"}), + _hit(id="", 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") diff --git a/uv.lock b/uv.lock index f3584c35..027e8bd8 100644 --- a/uv.lock +++ b/uv.lock @@ -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. @@ -822,26 +826,6 @@ dependencies = [ ] 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/5d/db/1d56e5f5823257b291962d6c0ce106146c6447f405b60b234c4f222a7cde/grpcio-1.80.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:dfab85db094068ff42e2a3563f60ab3dddcc9d6488a35abf0132daec13209c8a", size = 6055009, upload-time = "2026-03-30T08:46:46.265Z" }, - { url = "https://files.pythonhosted.org/packages/6e/18/c83f3cad64c5ca63bca7e91e5e46b0d026afc5af9d0a9972472ceba294b3/grpcio-1.80.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5c07e82e822e1161354e32da2662f741a4944ea955f9f580ec8fb409dd6f6060", size = 12035295, upload-time = "2026-03-30T08:46:49.099Z" }, - { url = "https://files.pythonhosted.org/packages/0f/8e/e14966b435be2dda99fbe89db9525ea436edc79780431a1c2875a3582644/grpcio-1.80.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2", size = 6610297, upload-time = "2026-03-30T08:46:52.123Z" }, - { url = "https://files.pythonhosted.org/packages/cc/26/d5eb38f42ce0e3fdc8174ea4d52036ef8d58cc4426cb800f2610f625dd75/grpcio-1.80.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3cb8130ba457d2aa09fa6b7c3ed6b6e4e6a2685fce63cb803d479576c4d80e21", size = 7300208, upload-time = "2026-03-30T08:46:54.859Z" }, - { url = "https://files.pythonhosted.org/packages/25/51/bd267c989f85a17a5b3eea65a6feb4ff672af41ca614e5a0279cc0ea381c/grpcio-1.80.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e5e478b3d14afd23f12e49e8b44c8684ac3c5f08561c43a5b9691c54d136ab", size = 6813442, upload-time = "2026-03-30T08:46:57.056Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d9/d80eef735b19e9169e30164bbf889b46f9df9127598a83d174eb13a48b26/grpcio-1.80.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", size = 7414743, upload-time = "2026-03-30T08:46:59.682Z" }, - { url = "https://files.pythonhosted.org/packages/de/f2/567f5bd5054398ed6b0509b9a30900376dcf2786bd936812098808b49d8d/grpcio-1.80.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8502122a3cc1714038e39a0b071acb1207ca7844208d5ea0d091317555ee7106", size = 8426046, upload-time = "2026-03-30T08:47:02.474Z" }, - { url = "https://files.pythonhosted.org/packages/62/29/73ef0141b4732ff5eacd68430ff2512a65c004696997f70476a83e548e7e/grpcio-1.80.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6", size = 7851641, upload-time = "2026-03-30T08:47:05.462Z" }, - { url = "https://files.pythonhosted.org/packages/46/69/abbfa360eb229a8623bab5f5a4f8105e445bd38ce81a89514ba55d281ad0/grpcio-1.80.0-cp311-cp311-win32.whl", hash = "sha256:51b4a7189b0bef2aa30adce3c78f09c83526cf3dddb24c6a96555e3b97340440", size = 4154368, upload-time = "2026-03-30T08:47:08.027Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d4/ae92206d01183b08613e846076115f5ac5991bae358d2a749fa864da5699/grpcio-1.80.0-cp311-cp311-win_amd64.whl", hash = "sha256:02e64bb0bb2da14d947a49e6f120a75e947250aebe65f9629b62bb1f5c14e6e9", size = 4894235, upload-time = "2026-03-30T08:47:10.839Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, - { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, - { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" }, - { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, - { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" }, - { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, - { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" }, - { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" }, { 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" }, @@ -911,7 +895,6 @@ dependencies = [ { name = "pydantic-settings" }, { name = "pyjwt" }, { name = "python-dotenv" }, - { name = "qdrant-client" }, { name = "redis" }, { name = "rich" }, { name = "scikit-learn" }, @@ -928,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 = [ @@ -971,7 +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", specifier = ">=1.18.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" }, @@ -982,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 = [ @@ -2484,12 +2470,6 @@ name = "pywin32" version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, - { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, - { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, { 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" },