diff --git a/docs/docs/integrations/vectorstores/qdrant.ipynb b/docs/docs/integrations/vectorstores/qdrant.ipynb index c256510526d..ed825beb002 100644 --- a/docs/docs/integrations/vectorstores/qdrant.ipynb +++ b/docs/docs/integrations/vectorstores/qdrant.ipynb @@ -30,7 +30,7 @@ }, "outputs": [], "source": [ - "%pip install --upgrade --quiet qdrant-client" + "%pip install --upgrade --quiet langchain-qdrant langchain-openai langchain" ] }, { @@ -79,8 +79,8 @@ "outputs": [], "source": [ "from langchain_community.document_loaders import TextLoader\n", - "from langchain_community.vectorstores import Qdrant\n", "from langchain_openai import OpenAIEmbeddings\n", + "from langchain_qdrant import Qdrant\n", "from langchain_text_splitters import CharacterTextSplitter" ] }, @@ -216,7 +216,7 @@ "source": [ "### Qdrant Cloud\n", "\n", - "If you prefer not to keep yourself busy with managing the infrastructure, you can choose to set up a fully-managed Qdrant cluster on [Qdrant Cloud](https://cloud.qdrant.io/). There is a free forever 1GB cluster included for trying out. The main difference with using a managed version of Qdrant is that you'll need to provide an API key to secure your deployment from being accessed publicly." + "If you prefer not to keep yourself busy with managing the infrastructure, you can choose to set up a fully-managed Qdrant cluster on [Qdrant Cloud](https://cloud.qdrant.io/). There is a free forever 1GB cluster included for trying out. The main difference with using a managed version of Qdrant is that you'll need to provide an API key to secure your deployment from being accessed publicly. The value can also be set in a `QDRANT_API_KEY` environment variable." ] }, { @@ -243,6 +243,36 @@ ")" ] }, + { + "cell_type": "markdown", + "id": "825c7903", + "metadata": {}, + "source": [ + "## Using an existing collection" + ] + }, + { + "cell_type": "markdown", + "id": "3f772575", + "metadata": {}, + "source": [ + "To get an instance of `langchain_qdrant.Qdrant` without loading any new documents or texts, you can use the `Qdrant.from_existing_collection()` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "daf7a6e5", + "metadata": {}, + "outputs": [], + "source": [ + "qdrant = Qdrant.from_existing_collection(\n", + " embeddings=embeddings,\n", + " collection_name=\"my_documents\",\n", + " url=\"http://localhost:6333\",\n", + ")" + ] + }, { "attachments": {}, "cell_type": "markdown", @@ -251,7 +281,7 @@ "source": [ "## Recreating the collection\n", "\n", - "Both `Qdrant.from_texts` and `Qdrant.from_documents` methods are great to start using Qdrant with Langchain. In the previous versions the collection was recreated every time you called any of them. That behaviour has changed. Currently, the collection is going to be reused if it already exists. Setting `force_recreate` to `True` allows to remove the old collection and start from scratch." + "The collection is reused if it already exists. Setting `force_recreate` to `True` allows to remove the old collection and start from scratch." ] }, { @@ -520,7 +550,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "9427195f", "metadata": { "ExecuteTime": { @@ -528,21 +558,9 @@ "start_time": "2023-04-04T10:51:26.018763Z" } }, - "outputs": [ - { - "data": { - "text/plain": [ - "VectorStoreRetriever(vectorstore=, search_type='similarity', search_kwargs={})" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "retriever = qdrant.as_retriever()\n", - "retriever" + "retriever = qdrant.as_retriever()" ] }, { @@ -556,7 +574,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "id": "64348f1b", "metadata": { "ExecuteTime": { @@ -564,21 +582,9 @@ "start_time": "2023-04-04T10:51:26.034284Z" } }, - "outputs": [ - { - "data": { - "text/plain": [ - "VectorStoreRetriever(vectorstore=, search_type='mmr', search_kwargs={})" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "retriever = qdrant.as_retriever(search_type=\"mmr\")\n", - "retriever" + "retriever = qdrant.as_retriever(search_type=\"mmr\")" ] }, { @@ -678,7 +684,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "id": "e4d6baf9", "metadata": { "ExecuteTime": { @@ -686,18 +692,7 @@ "start_time": "2023-04-04T11:08:30.229748Z" } }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "Qdrant.from_documents(\n", " docs,\n", diff --git a/libs/community/langchain_community/vectorstores/qdrant.py b/libs/community/langchain_community/vectorstores/qdrant.py index 5eb546dfee5..799007530b3 100644 --- a/libs/community/langchain_community/vectorstores/qdrant.py +++ b/libs/community/langchain_community/vectorstores/qdrant.py @@ -22,6 +22,7 @@ from typing import ( ) import numpy as np +from langchain_core._api.deprecation import deprecated from langchain_core.embeddings import Embeddings from langchain_core.runnables.config import run_in_executor from langchain_core.vectorstores import VectorStore @@ -65,6 +66,9 @@ def sync_call_fallback(method: Callable) -> Callable: return wrapper +@deprecated( + since="0.0.37", removal="0.3.0", alternative_import="langchain_qdrant.Qdrant" +) class Qdrant(VectorStore): """`Qdrant` vector store. diff --git a/libs/partners/qdrant/.gitignore b/libs/partners/qdrant/.gitignore new file mode 100644 index 00000000000..bee8a64b79a --- /dev/null +++ b/libs/partners/qdrant/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/libs/partners/qdrant/LICENSE b/libs/partners/qdrant/LICENSE new file mode 100644 index 00000000000..fc0602feecd --- /dev/null +++ b/libs/partners/qdrant/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 LangChain, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/partners/qdrant/Makefile b/libs/partners/qdrant/Makefile new file mode 100644 index 00000000000..d0e7767ee5f --- /dev/null +++ b/libs/partners/qdrant/Makefile @@ -0,0 +1,60 @@ +.PHONY: all format lint test tests integration_test integration_tests help + +# Default target executed when no arguments are given to make. +all: help + +# Define a variable for the test file path. +TEST_FILE ?= tests/unit_tests/ + +integration_test integration_tests: TEST_FILE = tests/integration_tests/ + +test tests integration_test integration_tests: + poetry run pytest $(TEST_FILE) + + +###################### +# LINTING AND FORMATTING +###################### + +# Define a variable for Python and notebook files. +PYTHON_FILES=. +MYPY_CACHE=.mypy_cache +lint format: PYTHON_FILES=. +lint_diff format_diff: PYTHON_FILES=$(shell git diff --relative=libs/partners/qdrant --name-only --diff-filter=d master | grep -E '\.py$$|\.ipynb$$') +lint_package: PYTHON_FILES=langchain_qdrant +lint_tests: PYTHON_FILES=tests +lint_tests: MYPY_CACHE=.mypy_cache_test + +lint lint_diff lint_package lint_tests: + poetry run ruff . + poetry run ruff format $(PYTHON_FILES) --diff + poetry run ruff --select I $(PYTHON_FILES) + mkdir $(MYPY_CACHE); poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE) + +format format_diff: + poetry run ruff format $(PYTHON_FILES) + poetry run ruff --select I --fix $(PYTHON_FILES) + +spell_check: + poetry run codespell --toml pyproject.toml + +spell_fix: + poetry run codespell --toml pyproject.toml -w + +check_imports: $(shell find langchain_qdrant -name '*.py') + poetry run python ./scripts/check_imports.py $^ + +###################### +# HELP +###################### + +help: + @echo '----' + @echo 'check_imports - check imports' + @echo 'format - run code formatters' + @echo 'lint - run linters' + @echo 'test - run unit tests' + @echo 'tests - run unit tests' + @echo 'test TEST_FILE= - run all tests in file' + @echo 'integration_test - run integration tests' + @echo 'integration_tests - run integration tests' diff --git a/libs/partners/qdrant/README.md b/libs/partners/qdrant/README.md new file mode 100644 index 00000000000..020b684e066 --- /dev/null +++ b/libs/partners/qdrant/README.md @@ -0,0 +1,25 @@ +# langchain-qdrant + +This package contains the LangChain integration with [Qdrant](https://qdrant.tech/). + +## Installation + +```bash +pip install -U langchain-qdrant +``` + +## Usage + +The `Qdrant` class exposes the connection to the Qdrant vector store. + +```python +from langchain_qdrant import Qdrant + +embeddings = ... # use a LangChain Embeddings class + +vectorstore = Qdrant.from_existing_collection( + embeddings=embeddings, + collection_name="", + url="http://localhost:6333", +) +``` diff --git a/libs/partners/qdrant/langchain_qdrant/__init__.py b/libs/partners/qdrant/langchain_qdrant/__init__.py new file mode 100644 index 00000000000..4c3277bce5e --- /dev/null +++ b/libs/partners/qdrant/langchain_qdrant/__init__.py @@ -0,0 +1,3 @@ +from langchain_qdrant.vectorstores import Qdrant + +__all__ = ["Qdrant"] diff --git a/libs/partners/qdrant/langchain_qdrant/_utils.py b/libs/partners/qdrant/langchain_qdrant/_utils.py new file mode 100644 index 00000000000..0e17cf75bb4 --- /dev/null +++ b/libs/partners/qdrant/langchain_qdrant/_utils.py @@ -0,0 +1,70 @@ +from typing import List, Union + +import numpy as np + +Matrix = Union[List[List[float]], List[np.ndarray], np.ndarray] + + +def maximal_marginal_relevance( + query_embedding: np.ndarray, + embedding_list: list, + lambda_mult: float = 0.5, + k: int = 4, +) -> List[int]: + """Calculate maximal marginal relevance.""" + if min(k, len(embedding_list)) <= 0: + return [] + if query_embedding.ndim == 1: + query_embedding = np.expand_dims(query_embedding, axis=0) + similarity_to_query = cosine_similarity(query_embedding, embedding_list)[0] + most_similar = int(np.argmax(similarity_to_query)) + idxs = [most_similar] + selected = np.array([embedding_list[most_similar]]) + while len(idxs) < min(k, len(embedding_list)): + best_score = -np.inf + idx_to_add = -1 + similarity_to_selected = cosine_similarity(embedding_list, selected) + for i, query_score in enumerate(similarity_to_query): + if i in idxs: + continue + redundant_score = max(similarity_to_selected[i]) + equation_score = ( + lambda_mult * query_score - (1 - lambda_mult) * redundant_score + ) + if equation_score > best_score: + best_score = equation_score + idx_to_add = i + idxs.append(idx_to_add) + selected = np.append(selected, [embedding_list[idx_to_add]], axis=0) + return idxs + + +def cosine_similarity(X: Matrix, Y: Matrix) -> np.ndarray: + """Row-wise cosine similarity between two equal-width matrices.""" + if len(X) == 0 or len(Y) == 0: + return np.array([]) + + X = np.array(X) + Y = np.array(Y) + if X.shape[1] != Y.shape[1]: + raise ValueError( + f"Number of columns in X and Y must be the same. X has shape {X.shape} " + f"and Y has shape {Y.shape}." + ) + try: + import simsimd as simd # type: ignore + + X = np.array(X, dtype=np.float32) + Y = np.array(Y, dtype=np.float32) + Z = 1 - simd.cdist(X, Y, metric="cosine") + if isinstance(Z, float): + return np.array([Z]) + return np.array(Z) + except ImportError: + X_norm = np.linalg.norm(X, axis=1) + Y_norm = np.linalg.norm(Y, axis=1) + # Ignore divide by zero errors run time warnings as those are handled below. + with np.errstate(divide="ignore", invalid="ignore"): + similarity = np.dot(X, Y.T) / np.outer(X_norm, Y_norm) + similarity[np.isnan(similarity) | np.isinf(similarity)] = 0.0 + return similarity diff --git a/libs/partners/qdrant/langchain_qdrant/py.typed b/libs/partners/qdrant/langchain_qdrant/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libs/partners/qdrant/langchain_qdrant/vectorstores.py b/libs/partners/qdrant/langchain_qdrant/vectorstores.py new file mode 100644 index 00000000000..766a70d0fbf --- /dev/null +++ b/libs/partners/qdrant/langchain_qdrant/vectorstores.py @@ -0,0 +1,2253 @@ +from __future__ import annotations + +import functools +import os +import uuid +import warnings +from itertools import islice +from operator import itemgetter +from typing import ( + TYPE_CHECKING, + Any, + AsyncGenerator, + Callable, + Dict, + Generator, + Iterable, + List, + Optional, + Sequence, + Tuple, + Type, + Union, +) + +import numpy as np +from grpc import RpcError # type: ignore +from langchain_core.documents import Document +from langchain_core.embeddings import Embeddings +from langchain_core.runnables.config import run_in_executor +from langchain_core.vectorstores import VectorStore +from qdrant_client import AsyncQdrantClient, QdrantClient +from qdrant_client.http import models +from qdrant_client.http.exceptions import UnexpectedResponse +from qdrant_client.local.async_qdrant_local import AsyncQdrantLocal + +from langchain_qdrant._utils import maximal_marginal_relevance + +if TYPE_CHECKING: + DictFilter = Dict[str, Union[str, int, bool, dict, list]] + MetadataFilter = Union[DictFilter, models.Filter] + + +class QdrantException(Exception): + """`Qdrant` related exceptions.""" + + +def sync_call_fallback(method: Callable) -> Callable: + """ + Decorator to call the synchronous method of the class if the async method is not + implemented. This decorator might be only used for the methods that are defined + as async in the class. + """ + + @functools.wraps(method) + async def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: + try: + return await method(self, *args, **kwargs) + except NotImplementedError: + # If the async method is not implemented, call the synchronous method + # by removing the first letter from the method name. For example, + # if the async method is called ``aaad_texts``, the synchronous method + # will be called ``aad_texts``. + return await run_in_executor( + None, getattr(self, method.__name__[1:]), *args, **kwargs + ) + + return wrapper + + +class Qdrant(VectorStore): + """`Qdrant` vector store. + + Example: + .. code-block:: python + + from qdrant_client import QdrantClient + from langchain_qdrant import Qdrant + + client = QdrantClient() + collection_name = "MyCollection" + qdrant = Qdrant(client, collection_name, embedding_function) + """ + + CONTENT_KEY: str = "page_content" + METADATA_KEY: str = "metadata" + VECTOR_NAME: Optional[str] = None + + def __init__( + self, + client: Any, + collection_name: str, + embeddings: Optional[Embeddings] = None, + content_payload_key: str = CONTENT_KEY, + metadata_payload_key: str = METADATA_KEY, + distance_strategy: str = "COSINE", + vector_name: Optional[str] = VECTOR_NAME, + async_client: Optional[Any] = None, + embedding_function: Optional[Callable] = None, # deprecated + ): + """Initialize with necessary components.""" + if not isinstance(client, QdrantClient): + raise ValueError( + f"client should be an instance of qdrant_client.QdrantClient, " + f"got {type(client)}" + ) + + if async_client is not None and not isinstance(async_client, AsyncQdrantClient): + raise ValueError( + f"async_client should be an instance of qdrant_client.AsyncQdrantClient" + f"got {type(async_client)}" + ) + + if embeddings is None and embedding_function is None: + raise ValueError( + "`embeddings` value can't be None. Pass `Embeddings` instance." + ) + + if embeddings is not None and embedding_function is not None: + raise ValueError( + "Both `embeddings` and `embedding_function` are passed. " + "Use `embeddings` only." + ) + + self._embeddings = embeddings + self._embeddings_function = embedding_function + self.client: QdrantClient = client + self.async_client: Optional[AsyncQdrantClient] = async_client + self.collection_name = collection_name + self.content_payload_key = content_payload_key or self.CONTENT_KEY + self.metadata_payload_key = metadata_payload_key or self.METADATA_KEY + self.vector_name = vector_name or self.VECTOR_NAME + + if embedding_function is not None: + warnings.warn( + "Using `embedding_function` is deprecated. " + "Pass `Embeddings` instance to `embeddings` instead." + ) + + if not isinstance(embeddings, Embeddings): + warnings.warn( + "`embeddings` should be an instance of `Embeddings`." + "Using `embeddings` as `embedding_function` which is deprecated" + ) + self._embeddings_function = embeddings + self._embeddings = None + + self.distance_strategy = distance_strategy.upper() + + @property + def embeddings(self) -> Optional[Embeddings]: + return self._embeddings + + def add_texts( + self, + texts: Iterable[str], + metadatas: Optional[List[dict]] = None, + ids: Optional[Sequence[str]] = None, + batch_size: int = 64, + **kwargs: Any, + ) -> List[str]: + """Run more texts through the embeddings and add to the vectorstore. + + Args: + texts: Iterable of strings to add to the vectorstore. + metadatas: Optional list of metadatas associated with the texts. + ids: + Optional list of ids to associate with the texts. Ids have to be + uuid-like strings. + batch_size: + How many vectors upload per-request. + Default: 64 + + Returns: + List of ids from adding the texts into the vectorstore. + """ + added_ids = [] + for batch_ids, points in self._generate_rest_batches( + texts, metadatas, ids, batch_size + ): + self.client.upsert( + collection_name=self.collection_name, points=points, **kwargs + ) + added_ids.extend(batch_ids) + + return added_ids + + @sync_call_fallback + async def aadd_texts( + self, + texts: Iterable[str], + metadatas: Optional[List[dict]] = None, + ids: Optional[Sequence[str]] = None, + batch_size: int = 64, + **kwargs: Any, + ) -> List[str]: + """Run more texts through the embeddings and add to the vectorstore. + + Args: + texts: Iterable of strings to add to the vectorstore. + metadatas: Optional list of metadatas associated with the texts. + ids: + Optional list of ids to associate with the texts. Ids have to be + uuid-like strings. + batch_size: + How many vectors upload per-request. + Default: 64 + + Returns: + List of ids from adding the texts into the vectorstore. + """ + if self.async_client is None or isinstance( + self.async_client._client, AsyncQdrantLocal + ): + raise NotImplementedError( + "QdrantLocal cannot interoperate with sync and async clients" + ) + + added_ids = [] + async for batch_ids, points in self._agenerate_rest_batches( + texts, metadatas, ids, batch_size + ): + await self.async_client.upsert( + collection_name=self.collection_name, points=points, **kwargs + ) + added_ids.extend(batch_ids) + + return added_ids + + def similarity_search( + self, + query: str, + k: int = 4, + filter: Optional[MetadataFilter] = None, + search_params: Optional[models.SearchParams] = None, + offset: int = 0, + score_threshold: Optional[float] = None, + consistency: Optional[models.ReadConsistency] = None, + **kwargs: Any, + ) -> List[Document]: + """Return docs most similar to query. + + Args: + query: Text to look up documents similar to. + k: Number of Documents to return. Defaults to 4. + filter: Filter by metadata. Defaults to None. + search_params: Additional search params + offset: + Offset of the first result to return. + May be used to paginate results. + Note: large offset values may cause performance issues. + score_threshold: + Define a minimal score threshold for the result. + If defined, less similar results will not be returned. + Score of the returned result might be higher or smaller than the + threshold depending on the Distance function used. + E.g. for cosine similarity only higher scores will be returned. + consistency: + Read consistency of the search. Defines how many replicas should be + queried before returning the result. + Values: + - int - number of replicas to query, values should present in all + queried replicas + - 'majority' - query all replicas, but return values present in the + majority of replicas + - 'quorum' - query the majority of replicas, return values present in + all of them + - 'all' - query all replicas, and return values present in all replicas + **kwargs: + Any other named arguments to pass through to QdrantClient.search() + + Returns: + List of Documents most similar to the query. + """ + results = self.similarity_search_with_score( + query, + k, + filter=filter, + search_params=search_params, + offset=offset, + score_threshold=score_threshold, + consistency=consistency, + **kwargs, + ) + return list(map(itemgetter(0), results)) + + @sync_call_fallback + async def asimilarity_search( + self, + query: str, + k: int = 4, + filter: Optional[MetadataFilter] = None, + **kwargs: Any, + ) -> List[Document]: + """Return docs most similar to query. + Args: + query: Text to look up documents similar to. + k: Number of Documents to return. Defaults to 4. + filter: Filter by metadata. Defaults to None. + Returns: + List of Documents most similar to the query. + """ + results = await self.asimilarity_search_with_score(query, k, filter, **kwargs) + return list(map(itemgetter(0), results)) + + def similarity_search_with_score( + self, + query: str, + k: int = 4, + filter: Optional[MetadataFilter] = None, + search_params: Optional[models.SearchParams] = None, + offset: int = 0, + score_threshold: Optional[float] = None, + consistency: Optional[models.ReadConsistency] = None, + **kwargs: Any, + ) -> List[Tuple[Document, float]]: + """Return docs most similar to query. + + Args: + query: Text to look up documents similar to. + k: Number of Documents to return. Defaults to 4. + filter: Filter by metadata. Defaults to None. + search_params: Additional search params + offset: + Offset of the first result to return. + May be used to paginate results. + Note: large offset values may cause performance issues. + score_threshold: + Define a minimal score threshold for the result. + If defined, less similar results will not be returned. + Score of the returned result might be higher or smaller than the + threshold depending on the Distance function used. + E.g. for cosine similarity only higher scores will be returned. + consistency: + Read consistency of the search. Defines how many replicas should be + queried before returning the result. + Values: + - int - number of replicas to query, values should present in all + queried replicas + - 'majority' - query all replicas, but return values present in the + majority of replicas + - 'quorum' - query the majority of replicas, return values present in + all of them + - 'all' - query all replicas, and return values present in all replicas + **kwargs: + Any other named arguments to pass through to QdrantClient.search() + + Returns: + List of documents most similar to the query text and distance for each. + """ + return self.similarity_search_with_score_by_vector( + self._embed_query(query), + k, + filter=filter, + search_params=search_params, + offset=offset, + score_threshold=score_threshold, + consistency=consistency, + **kwargs, + ) + + @sync_call_fallback + async def asimilarity_search_with_score( + self, + query: str, + k: int = 4, + filter: Optional[MetadataFilter] = None, + search_params: Optional[models.SearchParams] = None, + offset: int = 0, + score_threshold: Optional[float] = None, + consistency: Optional[models.ReadConsistency] = None, + **kwargs: Any, + ) -> List[Tuple[Document, float]]: + """Return docs most similar to query. + + Args: + query: Text to look up documents similar to. + k: Number of Documents to return. Defaults to 4. + filter: Filter by metadata. Defaults to None. + search_params: Additional search params + offset: + Offset of the first result to return. + May be used to paginate results. + Note: large offset values may cause performance issues. + score_threshold: + Define a minimal score threshold for the result. + If defined, less similar results will not be returned. + Score of the returned result might be higher or smaller than the + threshold depending on the Distance function used. + E.g. for cosine similarity only higher scores will be returned. + consistency: + Read consistency of the search. Defines how many replicas should be + queried before returning the result. + Values: + - int - number of replicas to query, values should present in all + queried replicas + - 'majority' - query all replicas, but return values present in the + majority of replicas + - 'quorum' - query the majority of replicas, return values present in + all of them + - 'all' - query all replicas, and return values present in all replicas + **kwargs: + Any other named arguments to pass through to + AsyncQdrantClient.Search(). + + Returns: + List of documents most similar to the query text and distance for each. + """ + query_embedding = await self._aembed_query(query) + return await self.asimilarity_search_with_score_by_vector( + query_embedding, + k, + filter=filter, + search_params=search_params, + offset=offset, + score_threshold=score_threshold, + consistency=consistency, + **kwargs, + ) + + def similarity_search_by_vector( + self, + embedding: List[float], + k: int = 4, + filter: Optional[MetadataFilter] = None, + search_params: Optional[models.SearchParams] = None, + offset: int = 0, + score_threshold: Optional[float] = None, + consistency: Optional[models.ReadConsistency] = None, + **kwargs: Any, + ) -> List[Document]: + """Return docs most similar to embedding vector. + + Args: + embedding: Embedding vector to look up documents similar to. + k: Number of Documents to return. Defaults to 4. + filter: Filter by metadata. Defaults to None. + search_params: Additional search params + offset: + Offset of the first result to return. + May be used to paginate results. + Note: large offset values may cause performance issues. + score_threshold: + Define a minimal score threshold for the result. + If defined, less similar results will not be returned. + Score of the returned result might be higher or smaller than the + threshold depending on the Distance function used. + E.g. for cosine similarity only higher scores will be returned. + consistency: + Read consistency of the search. Defines how many replicas should be + queried before returning the result. + Values: + - int - number of replicas to query, values should present in all + queried replicas + - 'majority' - query all replicas, but return values present in the + majority of replicas + - 'quorum' - query the majority of replicas, return values present in + all of them + - 'all' - query all replicas, and return values present in all replicas + **kwargs: + Any other named arguments to pass through to QdrantClient.search() + + Returns: + List of Documents most similar to the query. + """ + results = self.similarity_search_with_score_by_vector( + embedding, + k, + filter=filter, + search_params=search_params, + offset=offset, + score_threshold=score_threshold, + consistency=consistency, + **kwargs, + ) + return list(map(itemgetter(0), results)) + + @sync_call_fallback + async def asimilarity_search_by_vector( + self, + embedding: List[float], + k: int = 4, + filter: Optional[MetadataFilter] = None, + search_params: Optional[models.SearchParams] = None, + offset: int = 0, + score_threshold: Optional[float] = None, + consistency: Optional[models.ReadConsistency] = None, + **kwargs: Any, + ) -> List[Document]: + """Return docs most similar to embedding vector. + + Args: + embedding: Embedding vector to look up documents similar to. + k: Number of Documents to return. Defaults to 4. + filter: Filter by metadata. Defaults to None. + search_params: Additional search params + offset: + Offset of the first result to return. + May be used to paginate results. + Note: large offset values may cause performance issues. + score_threshold: + Define a minimal score threshold for the result. + If defined, less similar results will not be returned. + Score of the returned result might be higher or smaller than the + threshold depending on the Distance function used. + E.g. for cosine similarity only higher scores will be returned. + consistency: + Read consistency of the search. Defines how many replicas should be + queried before returning the result. + Values: + - int - number of replicas to query, values should present in all + queried replicas + - 'majority' - query all replicas, but return values present in the + majority of replicas + - 'quorum' - query the majority of replicas, return values present in + all of them + - 'all' - query all replicas, and return values present in all replicas + **kwargs: + Any other named arguments to pass through to + AsyncQdrantClient.Search(). + + Returns: + List of Documents most similar to the query. + """ + results = await self.asimilarity_search_with_score_by_vector( + embedding, + k, + filter=filter, + search_params=search_params, + offset=offset, + score_threshold=score_threshold, + consistency=consistency, + **kwargs, + ) + return list(map(itemgetter(0), results)) + + def similarity_search_with_score_by_vector( + self, + embedding: List[float], + k: int = 4, + filter: Optional[MetadataFilter] = None, + search_params: Optional[models.SearchParams] = None, + offset: int = 0, + score_threshold: Optional[float] = None, + consistency: Optional[models.ReadConsistency] = None, + **kwargs: Any, + ) -> List[Tuple[Document, float]]: + """Return docs most similar to embedding vector. + + Args: + embedding: Embedding vector to look up documents similar to. + k: Number of Documents to return. Defaults to 4. + filter: Filter by metadata. Defaults to None. + search_params: Additional search params + offset: + Offset of the first result to return. + May be used to paginate results. + Note: large offset values may cause performance issues. + score_threshold: + Define a minimal score threshold for the result. + If defined, less similar results will not be returned. + Score of the returned result might be higher or smaller than the + threshold depending on the Distance function used. + E.g. for cosine similarity only higher scores will be returned. + consistency: + Read consistency of the search. Defines how many replicas should be + queried before returning the result. + Values: + - int - number of replicas to query, values should present in all + queried replicas + - 'majority' - query all replicas, but return values present in the + majority of replicas + - 'quorum' - query the majority of replicas, return values present in + all of them + - 'all' - query all replicas, and return values present in all replicas + **kwargs: + Any other named arguments to pass through to QdrantClient.search() + + Returns: + List of documents most similar to the query text and distance for each. + """ + if filter is not None and isinstance(filter, dict): + warnings.warn( + "Using dict as a `filter` is deprecated. Please use qdrant-client " + "filters directly: " + "https://qdrant.tech/documentation/concepts/filtering/", + DeprecationWarning, + ) + qdrant_filter = self._qdrant_filter_from_dict(filter) + else: + qdrant_filter = filter + + query_vector = embedding + if self.vector_name is not None: + query_vector = (self.vector_name, embedding) # type: ignore[assignment] + + results = self.client.search( + collection_name=self.collection_name, + query_vector=query_vector, + query_filter=qdrant_filter, + search_params=search_params, + limit=k, + offset=offset, + with_payload=True, + with_vectors=False, # Langchain does not expect vectors to be returned + score_threshold=score_threshold, + consistency=consistency, + **kwargs, + ) + return [ + ( + self._document_from_scored_point( + result, + self.collection_name, + self.content_payload_key, + self.metadata_payload_key, + ), + result.score, + ) + for result in results + ] + + @sync_call_fallback + async def asimilarity_search_with_score_by_vector( + self, + embedding: List[float], + k: int = 4, + filter: Optional[MetadataFilter] = None, + search_params: Optional[models.SearchParams] = None, + offset: int = 0, + score_threshold: Optional[float] = None, + consistency: Optional[models.ReadConsistency] = None, + **kwargs: Any, + ) -> List[Tuple[Document, float]]: + """Return docs most similar to embedding vector. + + Args: + embedding: Embedding vector to look up documents similar to. + k: Number of Documents to return. Defaults to 4. + filter: Filter by metadata. Defaults to None. + search_params: Additional search params + offset: + Offset of the first result to return. + May be used to paginate results. + Note: large offset values may cause performance issues. + score_threshold: + Define a minimal score threshold for the result. + If defined, less similar results will not be returned. + Score of the returned result might be higher or smaller than the + threshold depending on the Distance function used. + E.g. for cosine similarity only higher scores will be returned. + consistency: + Read consistency of the search. Defines how many replicas should be + queried before returning the result. + Values: + - int - number of replicas to query, values should present in all + queried replicas + - 'majority' - query all replicas, but return values present in the + majority of replicas + - 'quorum' - query the majority of replicas, return values present in + all of them + - 'all' - query all replicas, and return values present in all replicas + **kwargs: + Any other named arguments to pass through to + AsyncQdrantClient.Search(). + + Returns: + List of documents most similar to the query text and distance for each. + """ + + if self.async_client is None or isinstance( + self.async_client._client, AsyncQdrantLocal + ): + raise NotImplementedError( + "QdrantLocal cannot interoperate with sync and async clients" + ) + if filter is not None and isinstance(filter, dict): + warnings.warn( + "Using dict as a `filter` is deprecated. Please use qdrant-client " + "filters directly: " + "https://qdrant.tech/documentation/concepts/filtering/", + DeprecationWarning, + ) + qdrant_filter = self._qdrant_filter_from_dict(filter) + else: + qdrant_filter = filter + + query_vector = embedding + if self.vector_name is not None: + query_vector = (self.vector_name, embedding) # type: ignore[assignment] + + results = await self.async_client.search( + collection_name=self.collection_name, + query_vector=query_vector, + query_filter=qdrant_filter, + search_params=search_params, + limit=k, + offset=offset, + with_payload=True, + with_vectors=False, # Langchain does not expect vectors to be returned + score_threshold=score_threshold, + consistency=consistency, + **kwargs, + ) + return [ + ( + self._document_from_scored_point( + result, + self.collection_name, + self.content_payload_key, + self.metadata_payload_key, + ), + result.score, + ) + for result in results + ] + + def max_marginal_relevance_search( + self, + query: str, + k: int = 4, + fetch_k: int = 20, + lambda_mult: float = 0.5, + filter: Optional[MetadataFilter] = None, + search_params: Optional[models.SearchParams] = None, + score_threshold: Optional[float] = None, + consistency: Optional[models.ReadConsistency] = None, + **kwargs: Any, + ) -> List[Document]: + """Return docs selected using the maximal marginal relevance. + + Maximal marginal relevance optimizes for similarity to query AND diversity + among selected documents. + + Args: + query: Text to look up documents similar to. + k: Number of Documents to return. Defaults to 4. + fetch_k: Number of Documents to fetch to pass to MMR algorithm. + Defaults to 20. + lambda_mult: Number between 0 and 1 that determines the degree + of diversity among the results with 0 corresponding + to maximum diversity and 1 to minimum diversity. + Defaults to 0.5. + filter: Filter by metadata. Defaults to None. + search_params: Additional search params + score_threshold: + Define a minimal score threshold for the result. + If defined, less similar results will not be returned. + Score of the returned result might be higher or smaller than the + threshold depending on the Distance function used. + E.g. for cosine similarity only higher scores will be returned. + consistency: + Read consistency of the search. Defines how many replicas should be + queried before returning the result. + Values: + - int - number of replicas to query, values should present in all + queried replicas + - 'majority' - query all replicas, but return values present in the + majority of replicas + - 'quorum' - query the majority of replicas, return values present in + all of them + - 'all' - query all replicas, and return values present in all replicas + **kwargs: + Any other named arguments to pass through to QdrantClient.search() + Returns: + List of Documents selected by maximal marginal relevance. + """ + query_embedding = self._embed_query(query) + return self.max_marginal_relevance_search_by_vector( + query_embedding, + k=k, + fetch_k=fetch_k, + lambda_mult=lambda_mult, + filter=filter, + search_params=search_params, + score_threshold=score_threshold, + consistency=consistency, + **kwargs, + ) + + @sync_call_fallback + async def amax_marginal_relevance_search( + self, + query: str, + k: int = 4, + fetch_k: int = 20, + lambda_mult: float = 0.5, + filter: Optional[MetadataFilter] = None, + search_params: Optional[models.SearchParams] = None, + score_threshold: Optional[float] = None, + consistency: Optional[models.ReadConsistency] = None, + **kwargs: Any, + ) -> List[Document]: + """Return docs selected using the maximal marginal relevance. + + Maximal marginal relevance optimizes for similarity to query AND diversity + among selected documents. + + Args: + query: Text to look up documents similar to. + k: Number of Documents to return. Defaults to 4. + fetch_k: Number of Documents to fetch to pass to MMR algorithm. + Defaults to 20. + lambda_mult: Number between 0 and 1 that determines the degree + of diversity among the results with 0 corresponding + to maximum diversity and 1 to minimum diversity. + Defaults to 0.5. + filter: Filter by metadata. Defaults to None. + search_params: Additional search params + score_threshold: + Define a minimal score threshold for the result. + If defined, less similar results will not be returned. + Score of the returned result might be higher or smaller than the + threshold depending on the Distance function used. + E.g. for cosine similarity only higher scores will be returned. + consistency: + Read consistency of the search. Defines how many replicas should be + queried before returning the result. + Values: + - int - number of replicas to query, values should present in all + queried replicas + - 'majority' - query all replicas, but return values present in the + majority of replicas + - 'quorum' - query the majority of replicas, return values present in + all of them + - 'all' - query all replicas, and return values present in all replicas + **kwargs: + Any other named arguments to pass through to + AsyncQdrantClient.Search(). + Returns: + List of Documents selected by maximal marginal relevance. + """ + query_embedding = await self._aembed_query(query) + return await self.amax_marginal_relevance_search_by_vector( + query_embedding, + k=k, + fetch_k=fetch_k, + lambda_mult=lambda_mult, + filter=filter, + search_params=search_params, + score_threshold=score_threshold, + consistency=consistency, + **kwargs, + ) + + def max_marginal_relevance_search_by_vector( + self, + embedding: List[float], + k: int = 4, + fetch_k: int = 20, + lambda_mult: float = 0.5, + filter: Optional[MetadataFilter] = None, + search_params: Optional[models.SearchParams] = None, + score_threshold: Optional[float] = None, + consistency: Optional[models.ReadConsistency] = None, + **kwargs: Any, + ) -> List[Document]: + """Return docs selected using the maximal marginal relevance. + + Maximal marginal relevance optimizes for similarity to query AND diversity + among selected documents. + + Args: + embedding: Embedding to look up documents similar to. + k: Number of Documents to return. Defaults to 4. + fetch_k: Number of Documents to fetch to pass to MMR algorithm. + lambda_mult: Number between 0 and 1 that determines the degree + of diversity among the results with 0 corresponding + to maximum diversity and 1 to minimum diversity. + Defaults to 0.5. + filter: Filter by metadata. Defaults to None. + search_params: Additional search params + score_threshold: + Define a minimal score threshold for the result. + If defined, less similar results will not be returned. + Score of the returned result might be higher or smaller than the + threshold depending on the Distance function used. + E.g. for cosine similarity only higher scores will be returned. + consistency: + Read consistency of the search. Defines how many replicas should be + queried before returning the result. + Values: + - int - number of replicas to query, values should present in all + queried replicas + - 'majority' - query all replicas, but return values present in the + majority of replicas + - 'quorum' - query the majority of replicas, return values present in + all of them + - 'all' - query all replicas, and return values present in all replicas + **kwargs: + Any other named arguments to pass through to QdrantClient.search() + Returns: + List of Documents selected by maximal marginal relevance. + """ + results = self.max_marginal_relevance_search_with_score_by_vector( + embedding, + k=k, + fetch_k=fetch_k, + lambda_mult=lambda_mult, + filter=filter, + search_params=search_params, + score_threshold=score_threshold, + consistency=consistency, + **kwargs, + ) + return list(map(itemgetter(0), results)) + + @sync_call_fallback + async def amax_marginal_relevance_search_by_vector( + self, + embedding: List[float], + k: int = 4, + fetch_k: int = 20, + lambda_mult: float = 0.5, + filter: Optional[MetadataFilter] = None, + search_params: Optional[models.SearchParams] = None, + score_threshold: Optional[float] = None, + consistency: Optional[models.ReadConsistency] = None, + **kwargs: Any, + ) -> List[Document]: + """Return docs selected using the maximal marginal relevance. + Maximal marginal relevance optimizes for similarity to query AND diversity + among selected documents. + Args: + query: Text to look up documents similar to. + k: Number of Documents to return. Defaults to 4. + fetch_k: Number of Documents to fetch to pass to MMR algorithm. + Defaults to 20. + lambda_mult: Number between 0 and 1 that determines the degree + of diversity among the results with 0 corresponding + to maximum diversity and 1 to minimum diversity. + Defaults to 0.5. + filter: Filter by metadata. Defaults to None. + search_params: Additional search params + score_threshold: + Define a minimal score threshold for the result. + If defined, less similar results will not be returned. + Score of the returned result might be higher or smaller than the + threshold depending on the Distance function used. + E.g. for cosine similarity only higher scores will be returned. + consistency: + Read consistency of the search. Defines how many replicas should be + queried before returning the result. + Values: + - int - number of replicas to query, values should present in all + queried replicas + - 'majority' - query all replicas, but return values present in the + majority of replicas + - 'quorum' - query the majority of replicas, return values present in + all of them + - 'all' - query all replicas, and return values present in all replicas + **kwargs: + Any other named arguments to pass through to + AsyncQdrantClient.Search(). + Returns: + List of Documents selected by maximal marginal relevance and distance for + each. + """ + results = await self.amax_marginal_relevance_search_with_score_by_vector( + embedding, + k=k, + fetch_k=fetch_k, + lambda_mult=lambda_mult, + filter=filter, + search_params=search_params, + score_threshold=score_threshold, + consistency=consistency, + **kwargs, + ) + return list(map(itemgetter(0), results)) + + def max_marginal_relevance_search_with_score_by_vector( + self, + embedding: List[float], + k: int = 4, + fetch_k: int = 20, + lambda_mult: float = 0.5, + filter: Optional[MetadataFilter] = None, + search_params: Optional[models.SearchParams] = None, + score_threshold: Optional[float] = None, + consistency: Optional[models.ReadConsistency] = None, + **kwargs: Any, + ) -> List[Tuple[Document, float]]: + """Return docs selected using the maximal marginal relevance. + Maximal marginal relevance optimizes for similarity to query AND diversity + among selected documents. + Args: + query: Text to look up documents similar to. + k: Number of Documents to return. Defaults to 4. + fetch_k: Number of Documents to fetch to pass to MMR algorithm. + Defaults to 20. + lambda_mult: Number between 0 and 1 that determines the degree + of diversity among the results with 0 corresponding + to maximum diversity and 1 to minimum diversity. + Defaults to 0.5. + filter: Filter by metadata. Defaults to None. + search_params: Additional search params + score_threshold: + Define a minimal score threshold for the result. + If defined, less similar results will not be returned. + Score of the returned result might be higher or smaller than the + threshold depending on the Distance function used. + E.g. for cosine similarity only higher scores will be returned. + consistency: + Read consistency of the search. Defines how many replicas should be + queried before returning the result. + Values: + - int - number of replicas to query, values should present in all + queried replicas + - 'majority' - query all replicas, but return values present in the + majority of replicas + - 'quorum' - query the majority of replicas, return values present in + all of them + - 'all' - query all replicas, and return values present in all replicas + **kwargs: + Any other named arguments to pass through to QdrantClient.search() + Returns: + List of Documents selected by maximal marginal relevance and distance for + each. + """ + query_vector = embedding + if self.vector_name is not None: + query_vector = (self.vector_name, query_vector) # type: ignore[assignment] + + results = self.client.search( + collection_name=self.collection_name, + query_vector=query_vector, + query_filter=filter, + search_params=search_params, + limit=fetch_k, + with_payload=True, + with_vectors=True, + score_threshold=score_threshold, + consistency=consistency, + **kwargs, + ) + embeddings = [ + result.vector.get(self.vector_name) # type: ignore[index, union-attr] + if self.vector_name is not None + else result.vector + for result in results + ] + mmr_selected = maximal_marginal_relevance( + np.array(embedding), embeddings, k=k, lambda_mult=lambda_mult + ) + return [ + ( + self._document_from_scored_point( + results[i], + self.collection_name, + self.content_payload_key, + self.metadata_payload_key, + ), + results[i].score, + ) + for i in mmr_selected + ] + + @sync_call_fallback + async def amax_marginal_relevance_search_with_score_by_vector( + self, + embedding: List[float], + k: int = 4, + fetch_k: int = 20, + lambda_mult: float = 0.5, + filter: Optional[MetadataFilter] = None, + search_params: Optional[models.SearchParams] = None, + score_threshold: Optional[float] = None, + consistency: Optional[models.ReadConsistency] = None, + **kwargs: Any, + ) -> List[Tuple[Document, float]]: + """Return docs selected using the maximal marginal relevance. + Maximal marginal relevance optimizes for similarity to query AND diversity + among selected documents. + Args: + query: Text to look up documents similar to. + k: Number of Documents to return. Defaults to 4. + fetch_k: Number of Documents to fetch to pass to MMR algorithm. + Defaults to 20. + lambda_mult: Number between 0 and 1 that determines the degree + of diversity among the results with 0 corresponding + to maximum diversity and 1 to minimum diversity. + Defaults to 0.5. + Returns: + List of Documents selected by maximal marginal relevance and distance for + each. + """ + if self.async_client is None or isinstance( + self.async_client._client, AsyncQdrantLocal + ): + raise NotImplementedError( + "QdrantLocal cannot interoperate with sync and async clients" + ) + query_vector = embedding + if self.vector_name is not None: + query_vector = (self.vector_name, query_vector) # type: ignore[assignment] + + results = await self.async_client.search( + collection_name=self.collection_name, + query_vector=query_vector, + query_filter=filter, + search_params=search_params, + limit=fetch_k, + with_payload=True, + with_vectors=True, + score_threshold=score_threshold, + consistency=consistency, + **kwargs, + ) + embeddings = [ + result.vector.get(self.vector_name) # type: ignore[index, union-attr] + if self.vector_name is not None + else result.vector + for result in results + ] + mmr_selected = maximal_marginal_relevance( + np.array(embedding), embeddings, k=k, lambda_mult=lambda_mult + ) + return [ + ( + self._document_from_scored_point( + results[i], + self.collection_name, + self.content_payload_key, + self.metadata_payload_key, + ), + results[i].score, + ) + for i in mmr_selected + ] + + def delete(self, ids: Optional[List[str]] = None, **kwargs: Any) -> Optional[bool]: + """Delete by vector ID or other criteria. + + Args: + ids: List of ids to delete. + **kwargs: Other keyword arguments that subclasses might use. + + Returns: + True if deletion is successful, False otherwise. + """ + + result = self.client.delete( + collection_name=self.collection_name, + points_selector=ids, + ) + return result.status == models.UpdateStatus.COMPLETED + + @sync_call_fallback + async def adelete( + self, ids: Optional[List[str]] = None, **kwargs: Any + ) -> Optional[bool]: + """Delete by vector ID or other criteria. + + Args: + ids: List of ids to delete. + **kwargs: Other keyword arguments that subclasses might use. + + Returns: + True if deletion is successful, False otherwise. + """ + if self.async_client is None or isinstance( + self.async_client._client, AsyncQdrantLocal + ): + raise NotImplementedError( + "QdrantLocal cannot interoperate with sync and async clients" + ) + + result = await self.async_client.delete( + collection_name=self.collection_name, + points_selector=ids, + ) + + return result.status == models.UpdateStatus.COMPLETED + + @classmethod + def from_texts( + cls: Type[Qdrant], + texts: List[str], + embedding: Embeddings, + metadatas: Optional[List[dict]] = None, + ids: Optional[Sequence[str]] = None, + location: Optional[str] = None, + url: Optional[str] = None, + port: Optional[int] = 6333, + grpc_port: int = 6334, + prefer_grpc: bool = False, + https: Optional[bool] = None, + api_key: Optional[str] = None, + prefix: Optional[str] = None, + timeout: Optional[int] = None, + host: Optional[str] = None, + path: Optional[str] = None, + collection_name: Optional[str] = None, + distance_func: str = "Cosine", + content_payload_key: str = CONTENT_KEY, + metadata_payload_key: str = METADATA_KEY, + vector_name: Optional[str] = VECTOR_NAME, + batch_size: int = 64, + shard_number: Optional[int] = None, + replication_factor: Optional[int] = None, + write_consistency_factor: Optional[int] = None, + on_disk_payload: Optional[bool] = None, + hnsw_config: Optional[models.HnswConfigDiff] = None, + optimizers_config: Optional[models.OptimizersConfigDiff] = None, + wal_config: Optional[models.WalConfigDiff] = None, + quantization_config: Optional[models.QuantizationConfig] = None, + init_from: Optional[models.InitFrom] = None, + on_disk: Optional[bool] = None, + force_recreate: bool = False, + **kwargs: Any, + ) -> Qdrant: + """Construct Qdrant wrapper from a list of texts. + + Args: + texts: A list of texts to be indexed in Qdrant. + embedding: A subclass of `Embeddings`, responsible for text vectorization. + metadatas: + An optional list of metadata. If provided it has to be of the same + length as a list of texts. + ids: + Optional list of ids to associate with the texts. Ids have to be + uuid-like strings. + location: + If ':memory:' - use in-memory Qdrant instance. + If `str` - use it as a `url` parameter. + If `None` - fallback to relying on `host` and `port` parameters. + url: either host or str of "Optional[scheme], host, Optional[port], + Optional[prefix]". Default: `None` + port: Port of the REST API interface. Default: 6333 + grpc_port: Port of the gRPC interface. Default: 6334 + prefer_grpc: + If true - use gPRC interface whenever possible in custom methods. + Default: False + https: If true - use HTTPS(SSL) protocol. Default: None + api_key: + API key for authentication in Qdrant Cloud. Default: None + Can also be set via environment variable `QDRANT_API_KEY`. + prefix: + If not None - add prefix to the REST URL path. + Example: service/v1 will result in + http://localhost:6333/service/v1/{qdrant-endpoint} for REST API. + Default: None + timeout: + Timeout for REST and gRPC API requests. + Default: 5.0 seconds for REST and unlimited for gRPC + host: + Host name of Qdrant service. If url and host are None, set to + 'localhost'. Default: None + path: + Path in which the vectors will be stored while using local mode. + Default: None + collection_name: + Name of the Qdrant collection to be used. If not provided, + it will be created randomly. Default: None + distance_func: + Distance function. One of: "Cosine" / "Euclid" / "Dot". + Default: "Cosine" + content_payload_key: + A payload key used to store the content of the document. + Default: "page_content" + metadata_payload_key: + A payload key used to store the metadata of the document. + Default: "metadata" + vector_name: + Name of the vector to be used internally in Qdrant. + Default: None + batch_size: + How many vectors upload per-request. + Default: 64 + shard_number: Number of shards in collection. Default is 1, minimum is 1. + replication_factor: + Replication factor for collection. Default is 1, minimum is 1. + Defines how many copies of each shard will be created. + Have effect only in distributed mode. + write_consistency_factor: + Write consistency factor for collection. Default is 1, minimum is 1. + Defines how many replicas should apply the operation for us to consider + it successful. Increasing this number will make the collection more + resilient to inconsistencies, but will also make it fail if not enough + replicas are available. + Does not have any performance impact. + Have effect only in distributed mode. + on_disk_payload: + If true - point`s payload will not be stored in memory. + It will be read from the disk every time it is requested. + This setting saves RAM by (slightly) increasing the response time. + Note: those payload values that are involved in filtering and are + indexed - remain in RAM. + hnsw_config: Params for HNSW index + optimizers_config: Params for optimizer + wal_config: Params for Write-Ahead-Log + quantization_config: + Params for quantization, if None - quantization will be disabled + init_from: + Use data stored in another collection to initialize this collection + force_recreate: + Force recreating the collection + **kwargs: + Additional arguments passed directly into REST client initialization + + This is a user-friendly interface that: + 1. Creates embeddings, one for each text + 2. Initializes the Qdrant database as an in-memory docstore by default + (and overridable to a remote docstore) + 3. Adds the text embeddings to the Qdrant database + + This is intended to be a quick way to get started. + + Example: + .. code-block:: python + + from langchain_qdrant import Qdrant + from langchain_openai import OpenAIEmbeddings + embeddings = OpenAIEmbeddings() + qdrant = Qdrant.from_texts(texts, embeddings, "localhost") + """ + qdrant = cls.construct_instance( + texts, + embedding, + location, + url, + port, + grpc_port, + prefer_grpc, + https, + api_key, + prefix, + timeout, + host, + path, + collection_name, + distance_func, + content_payload_key, + metadata_payload_key, + vector_name, + shard_number, + replication_factor, + write_consistency_factor, + on_disk_payload, + hnsw_config, + optimizers_config, + wal_config, + quantization_config, + init_from, + on_disk, + force_recreate, + **kwargs, + ) + qdrant.add_texts(texts, metadatas, ids, batch_size) + return qdrant + + @classmethod + def from_existing_collection( + cls: Type[Qdrant], + embedding: Embeddings, + path: str, + collection_name: str, + location: Optional[str] = None, + url: Optional[str] = None, + port: Optional[int] = 6333, + grpc_port: int = 6334, + prefer_grpc: bool = False, + https: Optional[bool] = None, + api_key: Optional[str] = None, + prefix: Optional[str] = None, + timeout: Optional[int] = None, + host: Optional[str] = None, + content_payload_key: str = CONTENT_KEY, + metadata_payload_key: str = METADATA_KEY, + distance_strategy: str = "COSINE", + vector_name: Optional[str] = VECTOR_NAME, + **kwargs: Any, + ) -> Qdrant: + """ + Get instance of an existing Qdrant collection. + This method will return the instance of the store without inserting any new + embeddings + """ + client, async_client = cls._generate_clients( + location=location, + url=url, + port=port, + grpc_port=grpc_port, + prefer_grpc=prefer_grpc, + https=https, + api_key=api_key, + prefix=prefix, + timeout=timeout, + host=host, + path=path, + **kwargs, + ) + return cls( + client=client, + async_client=async_client, + collection_name=collection_name, + embeddings=embedding, + content_payload_key=content_payload_key, + metadata_payload_key=metadata_payload_key, + distance_strategy=distance_strategy, + vector_name=vector_name, + ) + + @classmethod + @sync_call_fallback + async def afrom_texts( + cls: Type[Qdrant], + texts: List[str], + embedding: Embeddings, + metadatas: Optional[List[dict]] = None, + ids: Optional[Sequence[str]] = None, + location: Optional[str] = None, + url: Optional[str] = None, + port: Optional[int] = 6333, + grpc_port: int = 6334, + prefer_grpc: bool = False, + https: Optional[bool] = None, + api_key: Optional[str] = None, + prefix: Optional[str] = None, + timeout: Optional[int] = None, + host: Optional[str] = None, + path: Optional[str] = None, + collection_name: Optional[str] = None, + distance_func: str = "Cosine", + content_payload_key: str = CONTENT_KEY, + metadata_payload_key: str = METADATA_KEY, + vector_name: Optional[str] = VECTOR_NAME, + batch_size: int = 64, + shard_number: Optional[int] = None, + replication_factor: Optional[int] = None, + write_consistency_factor: Optional[int] = None, + on_disk_payload: Optional[bool] = None, + hnsw_config: Optional[models.HnswConfigDiff] = None, + optimizers_config: Optional[models.OptimizersConfigDiff] = None, + wal_config: Optional[models.WalConfigDiff] = None, + quantization_config: Optional[models.QuantizationConfig] = None, + init_from: Optional[models.InitFrom] = None, + on_disk: Optional[bool] = None, + force_recreate: bool = False, + **kwargs: Any, + ) -> Qdrant: + """Construct Qdrant wrapper from a list of texts. + + Args: + texts: A list of texts to be indexed in Qdrant. + embedding: A subclass of `Embeddings`, responsible for text vectorization. + metadatas: + An optional list of metadata. If provided it has to be of the same + length as a list of texts. + ids: + Optional list of ids to associate with the texts. Ids have to be + uuid-like strings. + location: + If ':memory:' - use in-memory Qdrant instance. + If `str` - use it as a `url` parameter. + If `None` - fallback to relying on `host` and `port` parameters. + url: either host or str of "Optional[scheme], host, Optional[port], + Optional[prefix]". Default: `None` + port: Port of the REST API interface. Default: 6333 + grpc_port: Port of the gRPC interface. Default: 6334 + prefer_grpc: + If true - use gPRC interface whenever possible in custom methods. + Default: False + https: If true - use HTTPS(SSL) protocol. Default: None + api_key: + API key for authentication in Qdrant Cloud. Default: None + Can also be set via environment variable `QDRANT_API_KEY`. + prefix: + If not None - add prefix to the REST URL path. + Example: service/v1 will result in + http://localhost:6333/service/v1/{qdrant-endpoint} for REST API. + Default: None + timeout: + Timeout for REST and gRPC API requests. + Default: 5.0 seconds for REST and unlimited for gRPC + host: + Host name of Qdrant service. If url and host are None, set to + 'localhost'. Default: None + path: + Path in which the vectors will be stored while using local mode. + Default: None + collection_name: + Name of the Qdrant collection to be used. If not provided, + it will be created randomly. Default: None + distance_func: + Distance function. One of: "Cosine" / "Euclid" / "Dot". + Default: "Cosine" + content_payload_key: + A payload key used to store the content of the document. + Default: "page_content" + metadata_payload_key: + A payload key used to store the metadata of the document. + Default: "metadata" + vector_name: + Name of the vector to be used internally in Qdrant. + Default: None + batch_size: + How many vectors upload per-request. + Default: 64 + shard_number: Number of shards in collection. Default is 1, minimum is 1. + replication_factor: + Replication factor for collection. Default is 1, minimum is 1. + Defines how many copies of each shard will be created. + Have effect only in distributed mode. + write_consistency_factor: + Write consistency factor for collection. Default is 1, minimum is 1. + Defines how many replicas should apply the operation for us to consider + it successful. Increasing this number will make the collection more + resilient to inconsistencies, but will also make it fail if not enough + replicas are available. + Does not have any performance impact. + Have effect only in distributed mode. + on_disk_payload: + If true - point`s payload will not be stored in memory. + It will be read from the disk every time it is requested. + This setting saves RAM by (slightly) increasing the response time. + Note: those payload values that are involved in filtering and are + indexed - remain in RAM. + hnsw_config: Params for HNSW index + optimizers_config: Params for optimizer + wal_config: Params for Write-Ahead-Log + quantization_config: + Params for quantization, if None - quantization will be disabled + init_from: + Use data stored in another collection to initialize this collection + force_recreate: + Force recreating the collection + **kwargs: + Additional arguments passed directly into REST client initialization + + This is a user-friendly interface that: + 1. Creates embeddings, one for each text + 2. Initializes the Qdrant database as an in-memory docstore by default + (and overridable to a remote docstore) + 3. Adds the text embeddings to the Qdrant database + + This is intended to be a quick way to get started. + + Example: + .. code-block:: python + + from langchain_qdrant import Qdrant + from langchain_openai import OpenAIEmbeddings + embeddings = OpenAIEmbeddings() + qdrant = await Qdrant.afrom_texts(texts, embeddings, "localhost") + """ + qdrant = await cls.aconstruct_instance( + texts, + embedding, + location, + url, + port, + grpc_port, + prefer_grpc, + https, + api_key, + prefix, + timeout, + host, + path, + collection_name, + distance_func, + content_payload_key, + metadata_payload_key, + vector_name, + shard_number, + replication_factor, + write_consistency_factor, + on_disk_payload, + hnsw_config, + optimizers_config, + wal_config, + quantization_config, + init_from, + on_disk, + force_recreate, + **kwargs, + ) + await qdrant.aadd_texts(texts, metadatas, ids, batch_size) + return qdrant + + @classmethod + def construct_instance( + cls: Type[Qdrant], + texts: List[str], + embedding: Embeddings, + location: Optional[str] = None, + url: Optional[str] = None, + port: Optional[int] = 6333, + grpc_port: int = 6334, + prefer_grpc: bool = False, + https: Optional[bool] = None, + api_key: Optional[str] = None, + prefix: Optional[str] = None, + timeout: Optional[int] = None, + host: Optional[str] = None, + path: Optional[str] = None, + collection_name: Optional[str] = None, + distance_func: str = "Cosine", + content_payload_key: str = CONTENT_KEY, + metadata_payload_key: str = METADATA_KEY, + vector_name: Optional[str] = VECTOR_NAME, + shard_number: Optional[int] = None, + replication_factor: Optional[int] = None, + write_consistency_factor: Optional[int] = None, + on_disk_payload: Optional[bool] = None, + hnsw_config: Optional[models.HnswConfigDiff] = None, + optimizers_config: Optional[models.OptimizersConfigDiff] = None, + wal_config: Optional[models.WalConfigDiff] = None, + quantization_config: Optional[models.QuantizationConfig] = None, + init_from: Optional[models.InitFrom] = None, + on_disk: Optional[bool] = None, + force_recreate: bool = False, + **kwargs: Any, + ) -> Qdrant: + # Just do a single quick embedding to get vector size + partial_embeddings = embedding.embed_documents(texts[:1]) + vector_size = len(partial_embeddings[0]) + collection_name = collection_name or uuid.uuid4().hex + distance_func = distance_func.upper() + client, async_client = cls._generate_clients( + location=location, + url=url, + port=port, + grpc_port=grpc_port, + prefer_grpc=prefer_grpc, + https=https, + api_key=api_key, + prefix=prefix, + timeout=timeout, + host=host, + path=path, + **kwargs, + ) + try: + # Skip any validation in case of forced collection recreate. + if force_recreate: + raise ValueError + + # Get the vector configuration of the existing collection and vector, if it + # was specified. If the old configuration does not match the current one, + # an exception is being thrown. + collection_info = client.get_collection(collection_name=collection_name) + current_vector_config = collection_info.config.params.vectors + if isinstance(current_vector_config, dict) and vector_name is not None: + if vector_name not in current_vector_config: + raise QdrantException( + f"Existing Qdrant collection {collection_name} does not " + f"contain vector named {vector_name}. Did you mean one of the " + f"existing vectors: {', '.join(current_vector_config.keys())}? " + f"If you want to recreate the collection, set `force_recreate` " + f"parameter to `True`." + ) + current_vector_config = current_vector_config.get(vector_name) # type: ignore[assignment] + elif isinstance(current_vector_config, dict) and vector_name is None: + raise QdrantException( + f"Existing Qdrant collection {collection_name} uses named vectors. " + f"If you want to reuse it, please set `vector_name` to any of the " + f"existing named vectors: " + f"{', '.join(current_vector_config.keys())}." # noqa + f"If you want to recreate the collection, set `force_recreate` " + f"parameter to `True`." + ) + elif ( + not isinstance(current_vector_config, dict) and vector_name is not None + ): + raise QdrantException( + f"Existing Qdrant collection {collection_name} doesn't use named " + f"vectors. If you want to reuse it, please set `vector_name` to " + f"`None`. If you want to recreate the collection, set " + f"`force_recreate` parameter to `True`." + ) + assert isinstance(current_vector_config, models.VectorParams), ( + "Expected current_vector_config to be an instance of " + f"models.VectorParams, but got {type(current_vector_config)}" + ) + # Check if the vector configuration has the same dimensionality. + if current_vector_config.size != vector_size: + raise QdrantException( + f"Existing Qdrant collection is configured for vectors with " + f"{current_vector_config.size} " + f"dimensions. Selected embeddings are {vector_size}-dimensional. " + f"If you want to recreate the collection, set `force_recreate` " + f"parameter to `True`." + ) + + current_distance_func = ( + current_vector_config.distance.name.upper() # type: ignore[union-attr] + ) + if current_distance_func != distance_func: + raise QdrantException( + f"Existing Qdrant collection is configured for " + f"{current_distance_func} similarity, but requested " + f"{distance_func}. Please set `distance_func` parameter to " + f"`{current_distance_func}` if you want to reuse it. " + f"If you want to recreate the collection, set `force_recreate` " + f"parameter to `True`." + ) + except (UnexpectedResponse, RpcError, ValueError): + vectors_config = models.VectorParams( + size=vector_size, + distance=models.Distance[distance_func], + on_disk=on_disk, + ) + + # If vector name was provided, we're going to use the named vectors feature + # with just a single vector. + if vector_name is not None: + vectors_config = { # type: ignore[assignment] + vector_name: vectors_config, + } + + if client.collection_exists(collection_name): + client.delete_collection(collection_name) + client.create_collection( + collection_name=collection_name, + vectors_config=vectors_config, + shard_number=shard_number, + replication_factor=replication_factor, + write_consistency_factor=write_consistency_factor, + on_disk_payload=on_disk_payload, + hnsw_config=hnsw_config, + optimizers_config=optimizers_config, + wal_config=wal_config, + quantization_config=quantization_config, + init_from=init_from, + timeout=timeout, # type: ignore[arg-type] + ) + qdrant = cls( + client=client, + collection_name=collection_name, + embeddings=embedding, + content_payload_key=content_payload_key, + metadata_payload_key=metadata_payload_key, + distance_strategy=distance_func, + vector_name=vector_name, + async_client=async_client, + ) + return qdrant + + @classmethod + async def aconstruct_instance( + cls: Type[Qdrant], + texts: List[str], + embedding: Embeddings, + location: Optional[str] = None, + url: Optional[str] = None, + port: Optional[int] = 6333, + grpc_port: int = 6334, + prefer_grpc: bool = False, + https: Optional[bool] = None, + api_key: Optional[str] = None, + prefix: Optional[str] = None, + timeout: Optional[int] = None, + host: Optional[str] = None, + path: Optional[str] = None, + collection_name: Optional[str] = None, + distance_func: str = "Cosine", + content_payload_key: str = CONTENT_KEY, + metadata_payload_key: str = METADATA_KEY, + vector_name: Optional[str] = VECTOR_NAME, + shard_number: Optional[int] = None, + replication_factor: Optional[int] = None, + write_consistency_factor: Optional[int] = None, + on_disk_payload: Optional[bool] = None, + hnsw_config: Optional[models.HnswConfigDiff] = None, + optimizers_config: Optional[models.OptimizersConfigDiff] = None, + wal_config: Optional[models.WalConfigDiff] = None, + quantization_config: Optional[models.QuantizationConfig] = None, + init_from: Optional[models.InitFrom] = None, + on_disk: Optional[bool] = None, + force_recreate: bool = False, + **kwargs: Any, + ) -> Qdrant: + # Just do a single quick embedding to get vector size + partial_embeddings = await embedding.aembed_documents(texts[:1]) + vector_size = len(partial_embeddings[0]) + collection_name = collection_name or uuid.uuid4().hex + distance_func = distance_func.upper() + client, async_client = cls._generate_clients( + location=location, + url=url, + port=port, + grpc_port=grpc_port, + prefer_grpc=prefer_grpc, + https=https, + api_key=api_key, + prefix=prefix, + timeout=timeout, + host=host, + path=path, + **kwargs, + ) + try: + # Skip any validation in case of forced collection recreate. + if force_recreate: + raise ValueError + + # Get the vector configuration of the existing collection and vector, if it + # was specified. If the old configuration does not match the current one, + # an exception is being thrown. + collection_info = client.get_collection(collection_name=collection_name) + current_vector_config = collection_info.config.params.vectors + if isinstance(current_vector_config, dict) and vector_name is not None: + if vector_name not in current_vector_config: + raise QdrantException( + f"Existing Qdrant collection {collection_name} does not " + f"contain vector named {vector_name}. Did you mean one of the " + f"existing vectors: {', '.join(current_vector_config.keys())}? " + f"If you want to recreate the collection, set `force_recreate` " + f"parameter to `True`." + ) + current_vector_config = current_vector_config.get(vector_name) # type: ignore[assignment] + elif isinstance(current_vector_config, dict) and vector_name is None: + raise QdrantException( + f"Existing Qdrant collection {collection_name} uses named vectors. " + f"If you want to reuse it, please set `vector_name` to any of the " + f"existing named vectors: " + f"{', '.join(current_vector_config.keys())}." # noqa + f"If you want to recreate the collection, set `force_recreate` " + f"parameter to `True`." + ) + elif ( + not isinstance(current_vector_config, dict) and vector_name is not None + ): + raise QdrantException( + f"Existing Qdrant collection {collection_name} doesn't use named " + f"vectors. If you want to reuse it, please set `vector_name` to " + f"`None`. If you want to recreate the collection, set " + f"`force_recreate` parameter to `True`." + ) + + assert isinstance(current_vector_config, models.VectorParams), ( + "Expected current_vector_config to be an instance of " + f"models.VectorParams, but got {type(current_vector_config)}" + ) + + # Check if the vector configuration has the same dimensionality. + if current_vector_config.size != vector_size: + raise QdrantException( + f"Existing Qdrant collection is configured for vectors with " + f"{current_vector_config.size} " + f"dimensions. Selected embeddings are {vector_size}-dimensional. " + f"If you want to recreate the collection, set `force_recreate` " + f"parameter to `True`." + ) + + current_distance_func = ( + current_vector_config.distance.name.upper() # type: ignore[union-attr] + ) + if current_distance_func != distance_func: + raise QdrantException( + f"Existing Qdrant collection is configured for " + f"{current_vector_config.distance} " # type: ignore[union-attr] + f"similarity. Please set `distance_func` parameter to " + f"`{distance_func}` if you want to reuse it. If you want to " + f"recreate the collection, set `force_recreate` parameter to " + f"`True`." + ) + except (UnexpectedResponse, RpcError, ValueError): + vectors_config = models.VectorParams( + size=vector_size, + distance=models.Distance[distance_func], + on_disk=on_disk, + ) + + # If vector name was provided, we're going to use the named vectors feature + # with just a single vector. + if vector_name is not None: + vectors_config = { # type: ignore[assignment] + vector_name: vectors_config, + } + + client.recreate_collection( + collection_name=collection_name, + vectors_config=vectors_config, + shard_number=shard_number, + replication_factor=replication_factor, + write_consistency_factor=write_consistency_factor, + on_disk_payload=on_disk_payload, + hnsw_config=hnsw_config, + optimizers_config=optimizers_config, + wal_config=wal_config, + quantization_config=quantization_config, + init_from=init_from, + timeout=timeout, # type: ignore[arg-type] + ) + qdrant = cls( + client=client, + collection_name=collection_name, + embeddings=embedding, + content_payload_key=content_payload_key, + metadata_payload_key=metadata_payload_key, + distance_strategy=distance_func, + vector_name=vector_name, + async_client=async_client, + ) + return qdrant + + @staticmethod + def _cosine_relevance_score_fn(distance: float) -> float: + """Normalize the distance to a score on a scale [0, 1].""" + return (distance + 1.0) / 2.0 + + def _select_relevance_score_fn(self) -> Callable[[float], float]: + """ + The 'correct' relevance function + may differ depending on a few things, including: + - the distance / similarity metric used by the VectorStore + - the scale of your embeddings (OpenAI's are unit normed. Many others are not!) + - embedding dimensionality + - etc. + """ + + if self.distance_strategy == "COSINE": + return self._cosine_relevance_score_fn + elif self.distance_strategy == "DOT": + return self._max_inner_product_relevance_score_fn + elif self.distance_strategy == "EUCLID": + return self._euclidean_relevance_score_fn + else: + raise ValueError( + "Unknown distance strategy, must be cosine, " + "max_inner_product, or euclidean" + ) + + def _similarity_search_with_relevance_scores( + self, + query: str, + k: int = 4, + **kwargs: Any, + ) -> List[Tuple[Document, float]]: + """Return docs and relevance scores in the range [0, 1]. + + 0 is dissimilar, 1 is most similar. + + Args: + query: input text + k: Number of Documents to return. Defaults to 4. + **kwargs: kwargs to be passed to similarity search. Should include: + score_threshold: Optional, a floating point value between 0 to 1 to + filter the resulting set of retrieved docs + + Returns: + List of Tuples of (doc, similarity_score) + """ + return self.similarity_search_with_score(query, k, **kwargs) + + @classmethod + def _build_payloads( + cls, + texts: Iterable[str], + metadatas: Optional[List[dict]], + content_payload_key: str, + metadata_payload_key: str, + ) -> List[dict]: + payloads = [] + for i, text in enumerate(texts): + if text is None: + raise ValueError( + "At least one of the texts is None. Please remove it before " + "calling .from_texts or .add_texts on Qdrant instance." + ) + metadata = metadatas[i] if metadatas is not None else None + payloads.append( + { + content_payload_key: text, + metadata_payload_key: metadata, + } + ) + + return payloads + + @classmethod + def _document_from_scored_point( + cls, + scored_point: Any, + collection_name: str, + content_payload_key: str, + metadata_payload_key: str, + ) -> Document: + metadata = scored_point.payload.get(metadata_payload_key) or {} + metadata["_id"] = scored_point.id + metadata["_collection_name"] = collection_name + return Document( + page_content=scored_point.payload.get(content_payload_key, ""), + metadata=metadata, + ) + + def _build_condition(self, key: str, value: Any) -> List[models.FieldCondition]: + out = [] + + if isinstance(value, dict): + for _key, value in value.items(): + out.extend(self._build_condition(f"{key}.{_key}", value)) + elif isinstance(value, list): + for _value in value: + if isinstance(_value, dict): + out.extend(self._build_condition(f"{key}[]", _value)) + else: + out.extend(self._build_condition(f"{key}", _value)) + else: + out.append( + models.FieldCondition( + key=f"{self.metadata_payload_key}.{key}", + match=models.MatchValue(value=value), + ) + ) + + return out + + def _qdrant_filter_from_dict( + self, filter: Optional[DictFilter] + ) -> Optional[models.Filter]: + if not filter: + return None + + return models.Filter( + must=[ + condition + for key, value in filter.items() + for condition in self._build_condition(key, value) + ] + ) + + def _embed_query(self, query: str) -> List[float]: + """Embed query text. + + Used to provide backward compatibility with `embedding_function` argument. + + Args: + query: Query text. + + Returns: + List of floats representing the query embedding. + """ + if self.embeddings is not None: + embedding = self.embeddings.embed_query(query) + else: + if self._embeddings_function is not None: + embedding = self._embeddings_function(query) + else: + raise ValueError("Neither of embeddings or embedding_function is set") + return embedding.tolist() if hasattr(embedding, "tolist") else embedding + + async def _aembed_query(self, query: str) -> List[float]: + """Embed query text asynchronously. + + Used to provide backward compatibility with `embedding_function` argument. + + Args: + query: Query text. + + Returns: + List of floats representing the query embedding. + """ + if self.embeddings is not None: + embedding = await self.embeddings.aembed_query(query) + else: + if self._embeddings_function is not None: + embedding = self._embeddings_function(query) + else: + raise ValueError("Neither of embeddings or embedding_function is set") + return embedding.tolist() if hasattr(embedding, "tolist") else embedding + + def _embed_texts(self, texts: Iterable[str]) -> List[List[float]]: + """Embed search texts. + + Used to provide backward compatibility with `embedding_function` argument. + + Args: + texts: Iterable of texts to embed. + + Returns: + List of floats representing the texts embedding. + """ + if self.embeddings is not None: + embeddings = self.embeddings.embed_documents(list(texts)) + if hasattr(embeddings, "tolist"): + embeddings = embeddings.tolist() + elif self._embeddings_function is not None: + embeddings = [] + for text in texts: + embedding = self._embeddings_function(text) + if hasattr(embeddings, "tolist"): + embedding = embedding.tolist() + embeddings.append(embedding) + else: + raise ValueError("Neither of embeddings or embedding_function is set") + + return embeddings + + async def _aembed_texts(self, texts: Iterable[str]) -> List[List[float]]: + """Embed search texts. + + Used to provide backward compatibility with `embedding_function` argument. + + Args: + texts: Iterable of texts to embed. + + Returns: + List of floats representing the texts embedding. + """ + if self.embeddings is not None: + embeddings = await self.embeddings.aembed_documents(list(texts)) + if hasattr(embeddings, "tolist"): + embeddings = embeddings.tolist() + elif self._embeddings_function is not None: + embeddings = [] + for text in texts: + embedding = self._embeddings_function(text) + if hasattr(embeddings, "tolist"): + embedding = embedding.tolist() + embeddings.append(embedding) + else: + raise ValueError("Neither of embeddings or embedding_function is set") + + return embeddings + + def _generate_rest_batches( + self, + texts: Iterable[str], + metadatas: Optional[List[dict]] = None, + ids: Optional[Sequence[str]] = None, + batch_size: int = 64, + ) -> Generator[Tuple[List[str], List[models.PointStruct]], None, None]: + texts_iterator = iter(texts) + metadatas_iterator = iter(metadatas or []) + ids_iterator = iter(ids or [uuid.uuid4().hex for _ in iter(texts)]) + while batch_texts := list(islice(texts_iterator, batch_size)): + # Take the corresponding metadata and id for each text in a batch + batch_metadatas = list(islice(metadatas_iterator, batch_size)) or None + batch_ids = list(islice(ids_iterator, batch_size)) + + # Generate the embeddings for all the texts in a batch + batch_embeddings = self._embed_texts(batch_texts) + + points = [ + models.PointStruct( + id=point_id, + vector=vector + if self.vector_name is None + else {self.vector_name: vector}, + payload=payload, + ) + for point_id, vector, payload in zip( + batch_ids, + batch_embeddings, + self._build_payloads( + batch_texts, + batch_metadatas, + self.content_payload_key, + self.metadata_payload_key, + ), + ) + ] + + yield batch_ids, points + + async def _agenerate_rest_batches( + self, + texts: Iterable[str], + metadatas: Optional[List[dict]] = None, + ids: Optional[Sequence[str]] = None, + batch_size: int = 64, + ) -> AsyncGenerator[Tuple[List[str], List[models.PointStruct]], None]: + texts_iterator = iter(texts) + metadatas_iterator = iter(metadatas or []) + ids_iterator = iter(ids or [uuid.uuid4().hex for _ in iter(texts)]) + while batch_texts := list(islice(texts_iterator, batch_size)): + # Take the corresponding metadata and id for each text in a batch + batch_metadatas = list(islice(metadatas_iterator, batch_size)) or None + batch_ids = list(islice(ids_iterator, batch_size)) + + # Generate the embeddings for all the texts in a batch + batch_embeddings = await self._aembed_texts(batch_texts) + + points = [ + models.PointStruct( + id=point_id, + vector=vector + if self.vector_name is None + else {self.vector_name: vector}, + payload=payload, + ) + for point_id, vector, payload in zip( + batch_ids, + batch_embeddings, + self._build_payloads( + batch_texts, + batch_metadatas, + self.content_payload_key, + self.metadata_payload_key, + ), + ) + ] + + yield batch_ids, points + + @staticmethod + def _generate_clients( + location: Optional[str] = None, + url: Optional[str] = None, + port: Optional[int] = 6333, + grpc_port: int = 6334, + prefer_grpc: bool = False, + https: Optional[bool] = None, + api_key: Optional[str] = None, + prefix: Optional[str] = None, + timeout: Optional[int] = None, + host: Optional[str] = None, + path: Optional[str] = None, + **kwargs: Any, + ) -> Tuple[QdrantClient, Optional[AsyncQdrantClient]]: + if api_key is None: + api_key = os.getenv("QDRANT_API_KEY") + + sync_client = QdrantClient( + location=location, + url=url, + port=port, + grpc_port=grpc_port, + prefer_grpc=prefer_grpc, + https=https, + api_key=api_key, + prefix=prefix, + timeout=timeout, + host=host, + path=path, + **kwargs, + ) + + if location == ":memory:" or path is not None: + # Local Qdrant cannot co-exist with Sync and Async clients + # We fallback to sync operations in this case + async_client = None + else: + async_client = AsyncQdrantClient( + location=location, + url=url, + port=port, + grpc_port=grpc_port, + prefer_grpc=prefer_grpc, + https=https, + api_key=api_key, + prefix=prefix, + timeout=timeout, + host=host, + path=path, + **kwargs, + ) + + return sync_client, async_client diff --git a/libs/partners/qdrant/poetry.lock b/libs/partners/qdrant/poetry.lock new file mode 100644 index 00000000000..f2abdcec68b --- /dev/null +++ b/libs/partners/qdrant/poetry.lock @@ -0,0 +1,1268 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} + +[[package]] +name = "anyio" +version = "4.3.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "codespell" +version = "2.2.6" +description = "Codespell" +optional = false +python-versions = ">=3.8" +files = [ + {file = "codespell-2.2.6-py3-none-any.whl", hash = "sha256:9ee9a3e5df0990604013ac2a9f22fa8e57669c827124a2e961fe8a1da4cacc07"}, + {file = "codespell-2.2.6.tar.gz", hash = "sha256:a8c65d8eb3faa03deabab6b3bbe798bea72e1799c7e9e955d57eca4096abcff9"}, +] + +[package.extras] +dev = ["Pygments", "build", "chardet", "pre-commit", "pytest", "pytest-cov", "pytest-dependency", "ruff", "tomli", "twine"] +hard-encoding-detection = ["chardet"] +toml = ["tomli"] +types = ["chardet (>=5.1.0)", "mypy", "pytest", "pytest-cov", "pytest-dependency"] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "freezegun" +version = "1.5.0" +description = "Let your Python tests travel through time" +optional = false +python-versions = ">=3.7" +files = [ + {file = "freezegun-1.5.0-py3-none-any.whl", hash = "sha256:ec3f4ba030e34eb6cf7e1e257308aee2c60c3d038ff35996d7475760c9ff3719"}, + {file = "freezegun-1.5.0.tar.gz", hash = "sha256:200a64359b363aa3653d8aac289584078386c7c3da77339d257e46a01fb5c77c"}, +] + +[package.dependencies] +python-dateutil = ">=2.7" + +[[package]] +name = "grpcio" +version = "1.63.0" +description = "HTTP/2-based RPC framework" +optional = false +python-versions = ">=3.8" +files = [ + {file = "grpcio-1.63.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:2e93aca840c29d4ab5db93f94ed0a0ca899e241f2e8aec6334ab3575dc46125c"}, + {file = "grpcio-1.63.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:91b73d3f1340fefa1e1716c8c1ec9930c676d6b10a3513ab6c26004cb02d8b3f"}, + {file = "grpcio-1.63.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:b3afbd9d6827fa6f475a4f91db55e441113f6d3eb9b7ebb8fb806e5bb6d6bd0d"}, + {file = "grpcio-1.63.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f3f6883ce54a7a5f47db43289a0a4c776487912de1a0e2cc83fdaec9685cc9f"}, + {file = "grpcio-1.63.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf8dae9cc0412cb86c8de5a8f3be395c5119a370f3ce2e69c8b7d46bb9872c8d"}, + {file = "grpcio-1.63.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:08e1559fd3b3b4468486b26b0af64a3904a8dbc78d8d936af9c1cf9636eb3e8b"}, + {file = "grpcio-1.63.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5c039ef01516039fa39da8a8a43a95b64e288f79f42a17e6c2904a02a319b357"}, + {file = "grpcio-1.63.0-cp310-cp310-win32.whl", hash = "sha256:ad2ac8903b2eae071055a927ef74121ed52d69468e91d9bcbd028bd0e554be6d"}, + {file = "grpcio-1.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:b2e44f59316716532a993ca2966636df6fbe7be4ab6f099de6815570ebe4383a"}, + {file = "grpcio-1.63.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:f28f8b2db7b86c77916829d64ab21ff49a9d8289ea1564a2b2a3a8ed9ffcccd3"}, + {file = "grpcio-1.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:65bf975639a1f93bee63ca60d2e4951f1b543f498d581869922910a476ead2f5"}, + {file = "grpcio-1.63.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:b5194775fec7dc3dbd6a935102bb156cd2c35efe1685b0a46c67b927c74f0cfb"}, + {file = "grpcio-1.63.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4cbb2100ee46d024c45920d16e888ee5d3cf47c66e316210bc236d5bebc42b3"}, + {file = "grpcio-1.63.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ff737cf29b5b801619f10e59b581869e32f400159e8b12d7a97e7e3bdeee6a2"}, + {file = "grpcio-1.63.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cd1e68776262dd44dedd7381b1a0ad09d9930ffb405f737d64f505eb7f77d6c7"}, + {file = "grpcio-1.63.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:93f45f27f516548e23e4ec3fbab21b060416007dbe768a111fc4611464cc773f"}, + {file = "grpcio-1.63.0-cp311-cp311-win32.whl", hash = "sha256:878b1d88d0137df60e6b09b74cdb73db123f9579232c8456f53e9abc4f62eb3c"}, + {file = "grpcio-1.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:756fed02dacd24e8f488f295a913f250b56b98fb793f41d5b2de6c44fb762434"}, + {file = "grpcio-1.63.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:93a46794cc96c3a674cdfb59ef9ce84d46185fe9421baf2268ccb556f8f81f57"}, + {file = "grpcio-1.63.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a7b19dfc74d0be7032ca1eda0ed545e582ee46cd65c162f9e9fc6b26ef827dc6"}, + {file = "grpcio-1.63.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:8064d986d3a64ba21e498b9a376cbc5d6ab2e8ab0e288d39f266f0fca169b90d"}, + {file = "grpcio-1.63.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:219bb1848cd2c90348c79ed0a6b0ea51866bc7e72fa6e205e459fedab5770172"}, + {file = "grpcio-1.63.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2d60cd1d58817bc5985fae6168d8b5655c4981d448d0f5b6194bbcc038090d2"}, + {file = "grpcio-1.63.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9e350cb096e5c67832e9b6e018cf8a0d2a53b2a958f6251615173165269a91b0"}, + {file = "grpcio-1.63.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:56cdf96ff82e3cc90dbe8bac260352993f23e8e256e063c327b6cf9c88daf7a9"}, + {file = "grpcio-1.63.0-cp312-cp312-win32.whl", hash = "sha256:3a6d1f9ea965e750db7b4ee6f9fdef5fdf135abe8a249e75d84b0a3e0c668a1b"}, + {file = "grpcio-1.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:d2497769895bb03efe3187fb1888fc20e98a5f18b3d14b606167dacda5789434"}, + {file = "grpcio-1.63.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:fdf348ae69c6ff484402cfdb14e18c1b0054ac2420079d575c53a60b9b2853ae"}, + {file = "grpcio-1.63.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a3abfe0b0f6798dedd2e9e92e881d9acd0fdb62ae27dcbbfa7654a57e24060c0"}, + {file = "grpcio-1.63.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:6ef0ad92873672a2a3767cb827b64741c363ebaa27e7f21659e4e31f4d750280"}, + {file = "grpcio-1.63.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b416252ac5588d9dfb8a30a191451adbf534e9ce5f56bb02cd193f12d8845b7f"}, + {file = "grpcio-1.63.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b77eaefc74d7eb861d3ffbdf91b50a1bb1639514ebe764c47773b833fa2d91"}, + {file = "grpcio-1.63.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b005292369d9c1f80bf70c1db1c17c6c342da7576f1c689e8eee4fb0c256af85"}, + {file = "grpcio-1.63.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cdcda1156dcc41e042d1e899ba1f5c2e9f3cd7625b3d6ebfa619806a4c1aadda"}, + {file = "grpcio-1.63.0-cp38-cp38-win32.whl", hash = "sha256:01799e8649f9e94ba7db1aeb3452188048b0019dc37696b0f5ce212c87c560c3"}, + {file = "grpcio-1.63.0-cp38-cp38-win_amd64.whl", hash = "sha256:6a1a3642d76f887aa4009d92f71eb37809abceb3b7b5a1eec9c554a246f20e3a"}, + {file = "grpcio-1.63.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:75f701ff645858a2b16bc8c9fc68af215a8bb2d5a9b647448129de6e85d52bce"}, + {file = "grpcio-1.63.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cacdef0348a08e475a721967f48206a2254a1b26ee7637638d9e081761a5ba86"}, + {file = "grpcio-1.63.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:0697563d1d84d6985e40ec5ec596ff41b52abb3fd91ec240e8cb44a63b895094"}, + {file = "grpcio-1.63.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6426e1fb92d006e47476d42b8f240c1d916a6d4423c5258ccc5b105e43438f61"}, + {file = "grpcio-1.63.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48cee31bc5f5a31fb2f3b573764bd563aaa5472342860edcc7039525b53e46a"}, + {file = "grpcio-1.63.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:50344663068041b34a992c19c600236e7abb42d6ec32567916b87b4c8b8833b3"}, + {file = "grpcio-1.63.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:259e11932230d70ef24a21b9fb5bb947eb4703f57865a404054400ee92f42f5d"}, + {file = "grpcio-1.63.0-cp39-cp39-win32.whl", hash = "sha256:a44624aad77bf8ca198c55af811fd28f2b3eaf0a50ec5b57b06c034416ef2d0a"}, + {file = "grpcio-1.63.0-cp39-cp39-win_amd64.whl", hash = "sha256:166e5c460e5d7d4656ff9e63b13e1f6029b122104c1633d5f37eaea348d7356d"}, + {file = "grpcio-1.63.0.tar.gz", hash = "sha256:f3023e14805c61bc439fb40ca545ac3d5740ce66120a678a3c6c2c55b70343d1"}, +] + +[package.extras] +protobuf = ["grpcio-tools (>=1.63.0)"] + +[[package]] +name = "grpcio-tools" +version = "1.63.0" +description = "Protobuf code generator for gRPC" +optional = false +python-versions = ">=3.8" +files = [ + {file = "grpcio_tools-1.63.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:1ab17460a2dfd3433af3120598bc18e705e3092d4d8396d3c06fe93deab19bbb"}, + {file = "grpcio_tools-1.63.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:cb9a0f61cabff426eaf5c0a506de599c9f006b31947ba1159254cc291c1dbdd1"}, + {file = "grpcio_tools-1.63.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:711d9f038c18c2f637b89af70c485018ae437dff5f7d2c631ca6a1eee7563038"}, + {file = "grpcio_tools-1.63.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:847ca8d75090d66e787576049500eb7c230a9997146d5d433da7928baf914273"}, + {file = "grpcio_tools-1.63.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49404876ec70bdae431eac5b1591c32c0bba4047dfd96dc6add03dbcdad5a5fc"}, + {file = "grpcio_tools-1.63.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:376136b9bbd16304a2e550ea0bb2b3340b720a0f623856124987845ef071d479"}, + {file = "grpcio_tools-1.63.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e197d5de49bb024f3d0b9e1ee1a0cce9e39955e17738bfbed72b0cc506a4824c"}, + {file = "grpcio_tools-1.63.0-cp310-cp310-win32.whl", hash = "sha256:acb5cc845942dc0f020eefbe10ad8ac6fe2f96b99c035da738c5d3026d3a5324"}, + {file = "grpcio_tools-1.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:f74a6da9db48296c3e7e34820e96744a0ea9cd58c3fa075ed206f7bb75229324"}, + {file = "grpcio_tools-1.63.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:4374c8beefec84f682c799b8df5ac4b217c09de6d69038ce16fc12dcd862fff8"}, + {file = "grpcio_tools-1.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d58a5aacee102858e49b1cc89b1ba1a020bb04f001df057e2b03fa11e6c636d1"}, + {file = "grpcio_tools-1.63.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:744952a560fdb060a5f9d467d130fde6dbfee2abb07143c87e9b17aae3c19d5a"}, + {file = "grpcio_tools-1.63.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c305274aa111412f5b8858242853e56c16ebcedc25d6a49ad615fd1b3ecd5971"}, + {file = "grpcio_tools-1.63.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e952835e7b8f40204bceb2a96fc7bcb8b07ff45ca9d07266774bc429db1efead"}, + {file = "grpcio_tools-1.63.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:409613bb694308a1945256d1d05c3ef3497f9fbf7fd68bd8bed86d80d97df334"}, + {file = "grpcio_tools-1.63.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b2d246eee3b2a6afe65362c22a98b0e6d805c227c2569c5616ad3bec619621dd"}, + {file = "grpcio_tools-1.63.0-cp311-cp311-win32.whl", hash = "sha256:bc0e6af05f66b36186ad3467d46ecc0f51dc9fa366005e095f4aa7739c7bfcba"}, + {file = "grpcio_tools-1.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:3f138c822090e7c87ef6a5dce0a6c4fdf68a9472e6a936b70ac7be2371184abe"}, + {file = "grpcio_tools-1.63.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:63a975d0457b2db1ee19fe99806091c71ad22f6f3664adc8f4c95943684dc0fd"}, + {file = "grpcio_tools-1.63.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:517ed2b405793e55c527f332296ae92a3e17fdd83772f1569709f2c228acaf54"}, + {file = "grpcio_tools-1.63.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:49af114fed0075025fe243cb3c8405c7a99f0b87f4eb7ccdaaf33ce1f55d8318"}, + {file = "grpcio_tools-1.63.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ef50fa15689f46a2c903f1c9687aa40687d67dcb0469903fff37a63e55f11cd"}, + {file = "grpcio_tools-1.63.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e68d9df9134906cbab1b225b625e11a368ab01b9ff24a4546bddec705ec7fd66"}, + {file = "grpcio_tools-1.63.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7cbf570f7b9badd3bd27be5e057ca466d447c1047bf80c87a53d8bcb2e87bbbe"}, + {file = "grpcio_tools-1.63.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f2cc0b3098ff48811ca821440e03763dcabd11158a11d9ea819c58938a9ea276"}, + {file = "grpcio_tools-1.63.0-cp312-cp312-win32.whl", hash = "sha256:0ca6d5623dadce66fabbd8b04d0572e35fd63b31f1ae7ea1555d662864852d33"}, + {file = "grpcio_tools-1.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:94b52c0dfb6026f69858a10ee3eadf15c343667647b5846cace82f61fe809c88"}, + {file = "grpcio_tools-1.63.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:c759306c04e3d0b3da3bd576e3de8bcbccc31a243a85ad256fd46d3a3ed93402"}, + {file = "grpcio_tools-1.63.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f305a5d61613e7ea3510eab62d65d47dff5206fcbe3b2347a7c1ebc9eff23dc6"}, + {file = "grpcio_tools-1.63.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:b5d74a30409eda2a0cdaa700da23fe3cad5d7ac47ac2d52644abe13a84047aa0"}, + {file = "grpcio_tools-1.63.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:632f78d8730d39363fc5afaf7cb5cf2f56b4e346ca11f550af74cff85e702f79"}, + {file = "grpcio_tools-1.63.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54136ac94eabc45b1b72d5ca379e5a2753f21a654f562838c5a9b706482bc1f0"}, + {file = "grpcio_tools-1.63.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b61682c06f0bcf2c576537819c42d5fb8eec1a0a9c05c905005200a57ff54c1f"}, + {file = "grpcio_tools-1.63.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0f8ce3fc598886a5370f28c86f94d06ddb0d3a251101a5bb8ed9576d9f86a519"}, + {file = "grpcio_tools-1.63.0-cp38-cp38-win32.whl", hash = "sha256:27684446c81bffcd4f20f13bf672ac7cbeaefbd270b3d867cdb58132e4b866bc"}, + {file = "grpcio_tools-1.63.0-cp38-cp38-win_amd64.whl", hash = "sha256:8341846604df00cf1c0a822476d27f4c481f678601a2f0b190e3b9936f857ded"}, + {file = "grpcio_tools-1.63.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:2924747142ebcbbd62acf65936fbc9694afbdfc2c6ae721461015370e27b8d6f"}, + {file = "grpcio_tools-1.63.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1b88be61eaa41eb4deb6b91a1e21d2a789d8567f0a973694fa27c09196f39a9a"}, + {file = "grpcio_tools-1.63.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:b87750347cb024bb74d5139da01ffba9f099ad2e43ba45893dc627ec920d57ab"}, + {file = "grpcio_tools-1.63.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49435413548e019921e125b178f3fd30543aa348c70775669b5ed80f0b39b393"}, + {file = "grpcio_tools-1.63.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df4dc9db9763594ae6ae04b42d6fcf7f163897a072f8fc946b864c9c3f0fbab1"}, + {file = "grpcio_tools-1.63.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6bbf51f334452fcac422509979635f97e2c2c3e71f21480891f2ba280b4b6867"}, + {file = "grpcio_tools-1.63.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c63a0f37b6b64dc31b2f1a0e5f889ae8b6bb7b7b20fe2406c1285321a7c54fdd"}, + {file = "grpcio_tools-1.63.0-cp39-cp39-win32.whl", hash = "sha256:d7142b0162834d3a67df532744a733b0757b11056373bd489a70dc07a3f65829"}, + {file = "grpcio_tools-1.63.0-cp39-cp39-win_amd64.whl", hash = "sha256:32247ac2d575a633aea2536840fd232d56f309bd940081d772081bd71e0626c6"}, + {file = "grpcio_tools-1.63.0.tar.gz", hash = "sha256:2474cffbc8f29404f0e3a2109c0a0423211ba93fe048b144e734f601ff391fc7"}, +] + +[package.dependencies] +grpcio = ">=1.63.0" +protobuf = ">=5.26.1,<6.0dev" +setuptools = "*" + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "h2" +version = "4.1.0" +description = "HTTP/2 State-Machine based protocol implementation" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, + {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, +] + +[package.dependencies] +hpack = ">=4.0,<5" +hyperframe = ">=6.0,<7" + +[[package]] +name = "hpack" +version = "4.0.0" +description = "Pure-Python HPACK header compression" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, + {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, +] + +[[package]] +name = "httpcore" +version = "1.0.5" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.26.0)"] + +[[package]] +name = "httpx" +version = "0.27.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +h2 = {version = ">=3,<5", optional = true, markers = "extra == \"http2\""} +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "hyperframe" +version = "6.0.1" +description = "HTTP/2 framing layer for Python" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, + {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, +] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jsonpatch" +version = "1.33" +description = "Apply JSON-Patches (RFC 6902)" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +files = [ + {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, + {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, +] + +[package.dependencies] +jsonpointer = ">=1.9" + +[[package]] +name = "jsonpointer" +version = "2.4" +description = "Identify specific nodes in a JSON document (RFC 6901)" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +files = [ + {file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"}, + {file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"}, +] + +[[package]] +name = "langchain-core" +version = "0.1.52" +description = "Building applications with LLMs through composability" +optional = false +python-versions = ">=3.8.1,<4.0" +files = [] +develop = true + +[package.dependencies] +jsonpatch = "^1.33" +langsmith = "^0.1.0" +packaging = "^23.2" +pydantic = ">=1,<3" +PyYAML = ">=5.3" +tenacity = "^8.1.0" + +[package.extras] +extended-testing = ["jinja2 (>=3,<4)"] + +[package.source] +type = "directory" +url = "../../core" + +[[package]] +name = "langsmith" +version = "0.1.52" +description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." +optional = false +python-versions = "<4.0,>=3.8.1" +files = [ + {file = "langsmith-0.1.52-py3-none-any.whl", hash = "sha256:4518e269b9a0e10197550f050b6518d1276fe68732f7b8579b3e1302b8471d29"}, + {file = "langsmith-0.1.52.tar.gz", hash = "sha256:f767fddb13c794bea7cc827a77f050a8a1c075ab1d997eb37849b975b0eef1b0"}, +] + +[package.dependencies] +orjson = ">=3.9.14,<4.0.0" +pydantic = ">=1,<3" +requests = ">=2,<3" + +[[package]] +name = "mypy" +version = "0.991" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"}, + {file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"}, + {file = "mypy-0.991-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6"}, + {file = "mypy-0.991-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb"}, + {file = "mypy-0.991-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305"}, + {file = "mypy-0.991-cp310-cp310-win_amd64.whl", hash = "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c"}, + {file = "mypy-0.991-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372"}, + {file = "mypy-0.991-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f"}, + {file = "mypy-0.991-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33"}, + {file = "mypy-0.991-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05"}, + {file = "mypy-0.991-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad"}, + {file = "mypy-0.991-cp311-cp311-win_amd64.whl", hash = "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297"}, + {file = "mypy-0.991-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813"}, + {file = "mypy-0.991-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711"}, + {file = "mypy-0.991-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd"}, + {file = "mypy-0.991-cp37-cp37m-win_amd64.whl", hash = "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"}, + {file = "mypy-0.991-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a"}, + {file = "mypy-0.991-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93"}, + {file = "mypy-0.991-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf"}, + {file = "mypy-0.991-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135"}, + {file = "mypy-0.991-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70"}, + {file = "mypy-0.991-cp38-cp38-win_amd64.whl", hash = "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243"}, + {file = "mypy-0.991-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d"}, + {file = "mypy-0.991-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5"}, + {file = "mypy-0.991-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3"}, + {file = "mypy-0.991-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648"}, + {file = "mypy-0.991-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476"}, + {file = "mypy-0.991-cp39-cp39-win_amd64.whl", hash = "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461"}, + {file = "mypy-0.991-py3-none-any.whl", hash = "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb"}, + {file = "mypy-0.991.tar.gz", hash = "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06"}, +] + +[package.dependencies] +mypy-extensions = ">=0.4.3" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "numpy" +version = "1.24.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, + {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, + {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, + {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, + {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, + {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, + {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, + {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, + {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, + {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, + {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, +] + +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + +[[package]] +name = "orjson" +version = "3.10.2" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +optional = false +python-versions = ">=3.8" +files = [ + {file = "orjson-3.10.2-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:87124c1b3471a072fda422e156dd7ef086d854937d68adc266f17f32a1043c95"}, + {file = "orjson-3.10.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1b79526bd039e775ad0f558800c3cd9f3bde878a1268845f63984d37bcbb5d1"}, + {file = "orjson-3.10.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f6dc97a6b2833a0d77598e7d016b6d964e4b0bc9576c89aa9a16fcf8ac902d"}, + {file = "orjson-3.10.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e427ce004fe15e13dcfdbd6c9dc936abf83d85d2164ec415a8bd90954f6f781"}, + {file = "orjson-3.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f3e05f70ab6225ba38504a2be61935d6ebc09de2b1bc484c30cb96ca4fa24b8"}, + {file = "orjson-3.10.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4e67821e3c1f0ec5dbef9dbd0bc9cd0fe4f0d8ba5d76a07038ee3843c9ac98a"}, + {file = "orjson-3.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24877561fe96a3736224243d6e2e026a674a4ddeff2b02fdeac41801bd261c87"}, + {file = "orjson-3.10.2-cp310-none-win32.whl", hash = "sha256:5da4ce52892b00aa51f5c5781414dc2bcdecc8470d2d60eeaeadbc14c5d9540b"}, + {file = "orjson-3.10.2-cp310-none-win_amd64.whl", hash = "sha256:cee3df171d957e84f568c3920f1f077f7f2a69f8ce4303d4c1404b7aab2f365a"}, + {file = "orjson-3.10.2-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a361e7ad84452416a469cdda7a2efeee8ddc9e06e4b95938b072045e205f86dc"}, + {file = "orjson-3.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b064251af6a2b7fb26e51b9abd3c1e615b53d5d5f87972263233d66d9c736a4"}, + {file = "orjson-3.10.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:464c30c24961cc83b2dc0e5532ed41084624ee1c71d4e7ef1aaec88f7a677393"}, + {file = "orjson-3.10.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4459005982748fda9871f04bce6a304c515afc46c96bef51e2bc81755c0f4ea0"}, + {file = "orjson-3.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abd0cd3a113a6ea0051c4a50cca65161ee50c014a01363554a1417d9f3c4529f"}, + {file = "orjson-3.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9a658ebc5143fbc0a9e3a10aafce4de50b01b1b0a41942038cb4bc6617f1e1d7"}, + {file = "orjson-3.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2fa4addaf6a6b3eb836cf92c4986d5ef9215fbdc87e4891cf8fd97990972bba0"}, + {file = "orjson-3.10.2-cp311-none-win32.whl", hash = "sha256:faff04363bfcff9cb41ab09c0ce8db84b8d4a09a374305ec5b12210dfa3154ea"}, + {file = "orjson-3.10.2-cp311-none-win_amd64.whl", hash = "sha256:7aee7b31a6acecf65a94beef2191081692891b00e8b7e02fbcc0c85002d62d0b"}, + {file = "orjson-3.10.2-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:38d9e9eab01131fdccbe95bff4f1d8ea197d239b5c73396e2079d07730bfa205"}, + {file = "orjson-3.10.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bfd84ecf5ebe8ec334a95950427e7ade40135032b1f00e2b17f351b0ef6dc72b"}, + {file = "orjson-3.10.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2ba009d85c3c98006759e62150d018d622aa79012fdeefbb70a42a542582b45"}, + {file = "orjson-3.10.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eac25b54fab6d9ccbf9dbc57555c2b52bf6d0802ea84bd2bd9670a161bd881dc"}, + {file = "orjson-3.10.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e735d90a90caf746de59becf29642c8358cafcd9b1a906ae3566efcc495324"}, + {file = "orjson-3.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:12feeee9089654904c2c988788eb9d521f5752c83ea410969d1f58d05ea95943"}, + {file = "orjson-3.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:619a7a4df76497afd2e6f1c963cc7e13658b3d58425c3a2ccf0471ad61d71025"}, + {file = "orjson-3.10.2-cp312-none-win32.whl", hash = "sha256:460d221090b451a0e78813196ec9dd28d2e33103048cfd7c1a3312a532fe3b1f"}, + {file = "orjson-3.10.2-cp312-none-win_amd64.whl", hash = "sha256:7efa93a9540e6ac9fe01167389fd7b1f0250cbfe3a8f06fe23e045d2a2d5d6ac"}, + {file = "orjson-3.10.2-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ceb283b8c048fb20bd1c703b10e710783a4f1ba7d5654358a25db99e9df94d5"}, + {file = "orjson-3.10.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201bf2b96ba39941254ef6b02e080660861e1444ec50be55778e1c38446c2d39"}, + {file = "orjson-3.10.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51a7b67c8cddf1a9de72d534244590103b1f17b2105d3bdcb221981bd97ab427"}, + {file = "orjson-3.10.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cde123c227e28ef9bba7092dc88abbd1933a0d7c17c58970c8ed8ec804e7add5"}, + {file = "orjson-3.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09b51caf8720b6df448acf764312d4678aeed6852ebfa6f3aa28b6061155ffef"}, + {file = "orjson-3.10.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f124d7e813e7b3d56bb7841d3d0884fec633f5f889a27a158d004b6b37e5ca98"}, + {file = "orjson-3.10.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e33ac7a6b081688a2167b501c9813aa6ec1f2cc097c47ab5f33cca3e875da9dc"}, + {file = "orjson-3.10.2-cp38-none-win32.whl", hash = "sha256:8f4a91921270d646f50f90a9903f87baae24c6e376ef3c275fcd0ffc051117bb"}, + {file = "orjson-3.10.2-cp38-none-win_amd64.whl", hash = "sha256:148d266e300257ff6d8e8a5895cc1e12766b8db676510b4f1d79b0d07f666fdd"}, + {file = "orjson-3.10.2-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:27158a75e7239145cf385d2318fdb27fbcd1fc494a470ee68287147c8b214cb1"}, + {file = "orjson-3.10.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26302b13e3f542b3e1ad1723e3543caf28e2f372391d21e1642de29c06e6209"}, + {file = "orjson-3.10.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:712cb3aa976311ae53de116a64949392aa5e7dcceda6769d5d7169d303d5ed09"}, + {file = "orjson-3.10.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9db3e6f23a6c9ce6c883a8e10e0eae0e2895327fb6e2286019b13153e59c672f"}, + {file = "orjson-3.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44787769d93d1ef9f25a80644ef020e0f30f37045d6336133e421a414c8fe51"}, + {file = "orjson-3.10.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:53a43b18d280c8d18cb18437921a05ec478b908809f9e89ad60eb2fdf0ba96ac"}, + {file = "orjson-3.10.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99e270b6a13027ed4c26c2b75b06c2cfb950934c8eb0400d70f4e6919bfe24f4"}, + {file = "orjson-3.10.2-cp39-none-win32.whl", hash = "sha256:d6f71486d211db9a01094cdd619ab594156a43ca04fa24e23ee04dac1509cdca"}, + {file = "orjson-3.10.2-cp39-none-win_amd64.whl", hash = "sha256:161f3b4e6364132562af80967ac3211e6681d320a01954da4915af579caab0b2"}, + {file = "orjson-3.10.2.tar.gz", hash = "sha256:47affe9f704c23e49a0fbb9d441af41f602474721e8639e8814640198f9ae32f"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "portalocker" +version = "2.8.2" +description = "Wraps the portalocker recipe for easy usage" +optional = false +python-versions = ">=3.8" +files = [ + {file = "portalocker-2.8.2-py3-none-any.whl", hash = "sha256:cfb86acc09b9aa7c3b43594e19be1345b9d16af3feb08bf92f23d4dce513a28e"}, + {file = "portalocker-2.8.2.tar.gz", hash = "sha256:2b035aa7828e46c58e9b31390ee1f169b98e1066ab10b9a6a861fe7e25ee4f33"}, +] + +[package.dependencies] +pywin32 = {version = ">=226", markers = "platform_system == \"Windows\""} + +[package.extras] +docs = ["sphinx (>=1.7.1)"] +redis = ["redis"] +tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "pytest-timeout (>=2.1.0)", "redis", "sphinx (>=6.0.0)", "types-redis"] + +[[package]] +name = "protobuf" +version = "5.26.1" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-5.26.1-cp310-abi3-win32.whl", hash = "sha256:3c388ea6ddfe735f8cf69e3f7dc7611e73107b60bdfcf5d0f024c3ccd3794e23"}, + {file = "protobuf-5.26.1-cp310-abi3-win_amd64.whl", hash = "sha256:e6039957449cb918f331d32ffafa8eb9255769c96aa0560d9a5bf0b4e00a2a33"}, + {file = "protobuf-5.26.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:38aa5f535721d5bb99861166c445c4105c4e285c765fbb2ac10f116e32dcd46d"}, + {file = "protobuf-5.26.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:fbfe61e7ee8c1860855696e3ac6cfd1b01af5498facc6834fcc345c9684fb2ca"}, + {file = "protobuf-5.26.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:f7417703f841167e5a27d48be13389d52ad705ec09eade63dfc3180a959215d7"}, + {file = "protobuf-5.26.1-cp38-cp38-win32.whl", hash = "sha256:d693d2504ca96750d92d9de8a103102dd648fda04540495535f0fec7577ed8fc"}, + {file = "protobuf-5.26.1-cp38-cp38-win_amd64.whl", hash = "sha256:9b557c317ebe6836835ec4ef74ec3e994ad0894ea424314ad3552bc6e8835b4e"}, + {file = "protobuf-5.26.1-cp39-cp39-win32.whl", hash = "sha256:b9ba3ca83c2e31219ffbeb9d76b63aad35a3eb1544170c55336993d7a18ae72c"}, + {file = "protobuf-5.26.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ee014c2c87582e101d6b54260af03b6596728505c79f17c8586e7523aaa8f8c"}, + {file = "protobuf-5.26.1-py3-none-any.whl", hash = "sha256:da612f2720c0183417194eeaa2523215c4fcc1a1949772dc65f05047e08d5932"}, + {file = "protobuf-5.26.1.tar.gz", hash = "sha256:8ca2a1d97c290ec7b16e4e5dff2e5ae150cc1582f55b5ab300d45cb0dfa90e51"}, +] + +[[package]] +name = "pydantic" +version = "2.7.1" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"}, + {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.18.2" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.18.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"}, + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"}, + {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"}, + {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"}, + {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"}, + {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"}, + {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"}, + {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"}, + {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"}, + {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"}, + {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"}, + {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"}, + {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"}, + {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"}, + {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"}, + {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"}, + {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"}, + {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"}, + {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"}, + {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"}, + {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"}, + {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"}, + {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.21.2" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"}, + {file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "pytest-watcher" +version = "0.3.5" +description = "Automatically rerun your tests on file modifications" +optional = false +python-versions = ">=3.7.0,<4.0.0" +files = [ + {file = "pytest_watcher-0.3.5-py3-none-any.whl", hash = "sha256:af00ca52c7be22dc34c0fd3d7ffef99057207a73b05dc5161fe3b2fe91f58130"}, + {file = "pytest_watcher-0.3.5.tar.gz", hash = "sha256:8896152460ba2b1a8200c12117c6611008ec96c8b2d811f0a05ab8a82b043ff8"}, +] + +[package.dependencies] +tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""} +watchdog = ">=2.0.0" + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "qdrant-client" +version = "1.9.0" +description = "Client library for the Qdrant vector search engine" +optional = false +python-versions = ">=3.8" +files = [ + {file = "qdrant_client-1.9.0-py3-none-any.whl", hash = "sha256:ee02893eab1f642481b1ac1e38eb68ec30bab0f673bef7cc05c19fa5d2cbf43e"}, + {file = "qdrant_client-1.9.0.tar.gz", hash = "sha256:7b1792f616651a6f0a76312f945c13d088e9451726795b82ce0350f7df3b7981"}, +] + +[package.dependencies] +grpcio = ">=1.41.0" +grpcio-tools = ">=1.41.0" +httpx = {version = ">=0.20.0", extras = ["http2"]} +numpy = [ + {version = ">=1.21", markers = "python_version >= \"3.8\" and python_version < \"3.12\""}, + {version = ">=1.26", markers = "python_version >= \"3.12\""}, +] +portalocker = ">=2.7.0,<3.0.0" +pydantic = ">=1.10.8" +urllib3 = ">=1.26.14,<3" + +[package.extras] +fastembed = ["fastembed (==0.2.6)"] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "ruff" +version = "0.1.15" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, + {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, + {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, + {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, + {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, +] + +[[package]] +name = "setuptools" +version = "69.5.1" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, + {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "syrupy" +version = "4.6.1" +description = "Pytest Snapshot Test Utility" +optional = false +python-versions = ">=3.8.1,<4" +files = [ + {file = "syrupy-4.6.1-py3-none-any.whl", hash = "sha256:203e52f9cb9fa749cf683f29bd68f02c16c3bc7e7e5fe8f2fc59bdfe488ce133"}, + {file = "syrupy-4.6.1.tar.gz", hash = "sha256:37a835c9ce7857eeef86d62145885e10b3cb9615bc6abeb4ce404b3f18e1bb36"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9.0.0" + +[[package]] +name = "tenacity" +version = "8.2.3" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"}, + {file = "tenacity-8.2.3.tar.gz", hash = "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a"}, +] + +[package.extras] +doc = ["reno", "sphinx", "tornado (>=4.5)"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.11.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, +] + +[[package]] +name = "urllib3" +version = "2.2.1" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "watchdog" +version = "4.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.8" +files = [ + {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b"}, + {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b"}, + {file = "watchdog-4.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c47bdd680009b11c9ac382163e05ca43baf4127954c5f6d0250e7d772d2b80c"}, + {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8350d4055505412a426b6ad8c521bc7d367d1637a762c70fdd93a3a0d595990b"}, + {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c17d98799f32e3f55f181f19dd2021d762eb38fdd381b4a748b9f5a36738e935"}, + {file = "watchdog-4.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4986db5e8880b0e6b7cd52ba36255d4793bf5cdc95bd6264806c233173b1ec0b"}, + {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257"}, + {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5369136a6474678e02426bd984466343924d1df8e2fd94a9b443cb7e3aa20d19"}, + {file = "watchdog-4.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76ad8484379695f3fe46228962017a7e1337e9acadafed67eb20aabb175df98b"}, + {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:45cc09cc4c3b43fb10b59ef4d07318d9a3ecdbff03abd2e36e77b6dd9f9a5c85"}, + {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eed82cdf79cd7f0232e2fdc1ad05b06a5e102a43e331f7d041e5f0e0a34a51c4"}, + {file = "watchdog-4.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba30a896166f0fee83183cec913298151b73164160d965af2e93a20bbd2ab605"}, + {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d18d7f18a47de6863cd480734613502904611730f8def45fc52a5d97503e5101"}, + {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2895bf0518361a9728773083908801a376743bcc37dfa252b801af8fd281b1ca"}, + {file = "watchdog-4.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87e9df830022488e235dd601478c15ad73a0389628588ba0b028cb74eb72fed8"}, + {file = "watchdog-4.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6e949a8a94186bced05b6508faa61b7adacc911115664ccb1923b9ad1f1ccf7b"}, + {file = "watchdog-4.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6a4db54edea37d1058b08947c789a2354ee02972ed5d1e0dca9b0b820f4c7f92"}, + {file = "watchdog-4.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d31481ccf4694a8416b681544c23bd271f5a123162ab603c7d7d2dd7dd901a07"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8fec441f5adcf81dd240a5fe78e3d83767999771630b5ddfc5867827a34fa3d3"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:6a9c71a0b02985b4b0b6d14b875a6c86ddea2fdbebd0c9a720a806a8bbffc69f"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:557ba04c816d23ce98a06e70af6abaa0485f6d94994ec78a42b05d1c03dcbd50"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0f9bd1fd919134d459d8abf954f63886745f4660ef66480b9d753a7c9d40927"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:73c7a935e62033bd5e8f0da33a4dcb763da2361921a69a5a95aaf6c93aa03a87"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6a80d5cae8c265842c7419c560b9961561556c4361b297b4c431903f8c33b269"}, + {file = "watchdog-4.0.0-py3-none-win32.whl", hash = "sha256:8f9a542c979df62098ae9c58b19e03ad3df1c9d8c6895d96c0d51da17b243b1c"}, + {file = "watchdog-4.0.0-py3-none-win_amd64.whl", hash = "sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245"}, + {file = "watchdog-4.0.0-py3-none-win_ia64.whl", hash = "sha256:9a03e16e55465177d416699331b0f3564138f1807ecc5f2de9d55d8f188d08c7"}, + {file = "watchdog-4.0.0.tar.gz", hash = "sha256:e3e7065cbdabe6183ab82199d7a4f6b3ba0a438c5a512a68559846ccb76a78ec"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.8.1,<4.0" +content-hash = "2101ce01262e65f8aa5d8294b1b2139efee6cb68fdfb5ad2ff0753b006327299" diff --git a/libs/partners/qdrant/pyproject.toml b/libs/partners/qdrant/pyproject.toml new file mode 100644 index 00000000000..68f6a64d382 --- /dev/null +++ b/libs/partners/qdrant/pyproject.toml @@ -0,0 +1,95 @@ +[tool.poetry] +name = "langchain-qdrant" +version = "0.0.1" +description = "An integration package connecting Qdrant and LangChain" +authors = [] +readme = "README.md" +repository = "https://github.com/langchain-ai/langchain" +license = "MIT" + +[tool.poetry.urls] +"Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/qdrant" + +[tool.poetry.dependencies] +python = ">=3.8.1,<4.0" +langchain-core = ">=0.1.52,<0.3" +qdrant-client = "^1.9.0" + +[tool.poetry.group.test] +optional = true + +[tool.poetry.group.test.dependencies] +pytest = "^7.3.0" +freezegun = "^1.2.2" +pytest-mock = "^3.10.0" +syrupy = "^4.0.2" +pytest-watcher = "^0.3.4" +pytest-asyncio = "^0.21.1" +langchain-core = {path = "../../core", develop = true} +requests = "^2.31.0" + +[tool.poetry.group.codespell] +optional = true + +[tool.poetry.group.codespell.dependencies] +codespell = "^2.2.0" + +[tool.poetry.group.test_integration] +optional = true + +[tool.poetry.group.test_integration.dependencies] + +[tool.poetry.group.lint] +optional = true + +[tool.poetry.group.lint.dependencies] +ruff = "^0.1.5" + +[tool.poetry.group.typing.dependencies] +mypy = "^0.991" +langchain-core = {path = "../../core", develop = true} + +[tool.poetry.group.dev] +optional = true + +[tool.poetry.group.dev.dependencies] +langchain-core = {path = "../../core", develop = true} + +[tool.ruff] +select = [ + "E", # pycodestyle + "F", # pyflakes + "I", # isort +] + +[tool.mypy] +disallow_untyped_defs = true + +[tool.coverage.run] +omit = [ + "tests/*", +] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +# --strict-markers will raise errors on unknown marks. +# https://docs.pytest.org/en/7.1.x/how-to/mark.html#raising-errors-on-unknown-marks +# +# https://docs.pytest.org/en/7.1.x/reference/reference.html +# --strict-config any warnings encountered while parsing the `pytest` +# section of the configuration file raise errors. +# +# https://github.com/tophat/syrupy +# --snapshot-warn-unused Prints a warning on unused snapshots rather than fail the test suite. +addopts = "--snapshot-warn-unused --strict-markers --strict-config --durations=5" +# Registering custom markers. +# https://docs.pytest.org/en/7.1.x/example/markers.html#registering-markers +markers = [ + "requires: mark tests as requiring a specific library", + "asyncio: mark tests as requiring asyncio", + "compile: mark placeholder test used to compile integration tests without running them", +] +asyncio_mode = "auto" diff --git a/libs/partners/qdrant/scripts/check_imports.py b/libs/partners/qdrant/scripts/check_imports.py new file mode 100644 index 00000000000..fd21a4975b7 --- /dev/null +++ b/libs/partners/qdrant/scripts/check_imports.py @@ -0,0 +1,17 @@ +import sys +import traceback +from importlib.machinery import SourceFileLoader + +if __name__ == "__main__": + files = sys.argv[1:] + has_failure = False + for file in files: + try: + SourceFileLoader("x", file).load_module() + except Exception: + has_faillure = True + print(file) + traceback.print_exc() + print() + + sys.exit(1 if has_failure else 0) diff --git a/libs/partners/qdrant/scripts/check_pydantic.sh b/libs/partners/qdrant/scripts/check_pydantic.sh new file mode 100755 index 00000000000..06b5bb81ae2 --- /dev/null +++ b/libs/partners/qdrant/scripts/check_pydantic.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# +# This script searches for lines starting with "import pydantic" or "from pydantic" +# in tracked files within a Git repository. +# +# Usage: ./scripts/check_pydantic.sh /path/to/repository + +# Check if a path argument is provided +if [ $# -ne 1 ]; then + echo "Usage: $0 /path/to/repository" + exit 1 +fi + +repository_path="$1" + +# Search for lines matching the pattern within the specified repository +result=$(git -C "$repository_path" grep -E '^import pydantic|^from pydantic') + +# Check if any matching lines were found +if [ -n "$result" ]; then + echo "ERROR: The following lines need to be updated:" + echo "$result" + echo "Please replace the code with an import from langchain_core.pydantic_v1." + echo "For example, replace 'from pydantic import BaseModel'" + echo "with 'from langchain_core.pydantic_v1 import BaseModel'" + exit 1 +fi diff --git a/libs/partners/qdrant/scripts/lint_imports.sh b/libs/partners/qdrant/scripts/lint_imports.sh new file mode 100755 index 00000000000..695613c7ba8 --- /dev/null +++ b/libs/partners/qdrant/scripts/lint_imports.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -eu + +# Initialize a variable to keep track of errors +errors=0 + +# make sure not importing from langchain or langchain_experimental +git --no-pager grep '^from langchain\.' . && errors=$((errors+1)) +git --no-pager grep '^from langchain_experimental\.' . && errors=$((errors+1)) + +# Decide on an exit status based on the errors +if [ "$errors" -gt 0 ]; then + exit 1 +else + exit 0 +fi diff --git a/libs/partners/qdrant/tests/__init__.py b/libs/partners/qdrant/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libs/partners/qdrant/tests/integration_tests/__init__.py b/libs/partners/qdrant/tests/integration_tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libs/partners/qdrant/tests/integration_tests/async_api/__init__.py b/libs/partners/qdrant/tests/integration_tests/async_api/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libs/partners/qdrant/tests/integration_tests/async_api/test_add_texts.py b/libs/partners/qdrant/tests/integration_tests/async_api/test_add_texts.py new file mode 100644 index 00000000000..ca145da6ae0 --- /dev/null +++ b/libs/partners/qdrant/tests/integration_tests/async_api/test_add_texts.py @@ -0,0 +1,123 @@ +import os +import uuid +from typing import Optional + +import pytest + +from langchain_qdrant import Qdrant +from tests.integration_tests.common import ConsistentFakeEmbeddings +from tests.integration_tests.fixtures import qdrant_locations + +API_KEY = os.getenv("QDRANT_API_KEY") + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("qdrant_location", qdrant_locations()) +async def test_qdrant_aadd_texts_returns_all_ids( + batch_size: int, qdrant_location: str +) -> None: + """Test end to end Qdrant.aadd_texts returns unique ids.""" + docsearch: Qdrant = Qdrant.from_texts( + ["foobar"], + ConsistentFakeEmbeddings(), + batch_size=batch_size, + location=qdrant_location, + ) + + ids = await docsearch.aadd_texts(["foo", "bar", "baz"]) + assert 3 == len(ids) + assert 3 == len(set(ids)) + + +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +@pytest.mark.parametrize("qdrant_location", qdrant_locations()) +async def test_qdrant_aadd_texts_stores_duplicated_texts( + vector_name: Optional[str], qdrant_location: str +) -> None: + """Test end to end Qdrant.aadd_texts stores duplicated texts separately.""" + from qdrant_client import QdrantClient + from qdrant_client.http import models as rest + + client = QdrantClient(location=qdrant_location, api_key=API_KEY) + collection_name = uuid.uuid4().hex + vectors_config = rest.VectorParams(size=10, distance=rest.Distance.COSINE) + if vector_name is not None: + vectors_config = {vector_name: vectors_config} # type: ignore[assignment] + client.recreate_collection(collection_name, vectors_config=vectors_config) + + vec_store = Qdrant( + client, + collection_name, + embeddings=ConsistentFakeEmbeddings(), + vector_name=vector_name, + ) + ids = await vec_store.aadd_texts(["abc", "abc"], [{"a": 1}, {"a": 2}]) + + assert 2 == len(set(ids)) + assert 2 == client.count(collection_name).count + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("qdrant_location", qdrant_locations()) +async def test_qdrant_aadd_texts_stores_ids( + batch_size: int, qdrant_location: str +) -> None: + """Test end to end Qdrant.aadd_texts stores provided ids.""" + from qdrant_client import QdrantClient + from qdrant_client.http import models as rest + + ids = [ + "fa38d572-4c31-4579-aedc-1960d79df6df", + "cdc1aa36-d6ab-4fb2-8a94-56674fd27484", + ] + + client = QdrantClient(location=qdrant_location, api_key=API_KEY) + collection_name = uuid.uuid4().hex + client.recreate_collection( + collection_name, + vectors_config=rest.VectorParams(size=10, distance=rest.Distance.COSINE), + ) + + vec_store = Qdrant(client, collection_name, ConsistentFakeEmbeddings()) + returned_ids = await vec_store.aadd_texts( + ["abc", "def"], ids=ids, batch_size=batch_size + ) + + assert all(first == second for first, second in zip(ids, returned_ids)) + assert 2 == client.count(collection_name).count + stored_ids = [point.id for point in client.scroll(collection_name)[0]] + assert set(ids) == set(stored_ids) + + +@pytest.mark.parametrize("vector_name", ["custom-vector"]) +@pytest.mark.parametrize("qdrant_location", qdrant_locations()) +async def test_qdrant_aadd_texts_stores_embeddings_as_named_vectors( + vector_name: str, qdrant_location: str +) -> None: + """Test end to end Qdrant.aadd_texts stores named vectors if name is provided.""" + from qdrant_client import QdrantClient + from qdrant_client.http import models as rest + + collection_name = uuid.uuid4().hex + + client = QdrantClient(location=qdrant_location, api_key=API_KEY) + client.recreate_collection( + collection_name, + vectors_config={ + vector_name: rest.VectorParams(size=10, distance=rest.Distance.COSINE) + }, + ) + + vec_store = Qdrant( + client, + collection_name, + ConsistentFakeEmbeddings(), + vector_name=vector_name, + ) + await vec_store.aadd_texts(["lorem", "ipsum", "dolor", "sit", "amet"]) + + assert 5 == client.count(collection_name).count + assert all( + vector_name in point.vector # type: ignore[operator] + for point in client.scroll(collection_name, with_vectors=True)[0] + ) diff --git a/libs/partners/qdrant/tests/integration_tests/async_api/test_from_texts.py b/libs/partners/qdrant/tests/integration_tests/async_api/test_from_texts.py new file mode 100644 index 00000000000..64696a5ce3a --- /dev/null +++ b/libs/partners/qdrant/tests/integration_tests/async_api/test_from_texts.py @@ -0,0 +1,266 @@ +import os +import uuid +from typing import Optional + +import pytest +from langchain_core.documents import Document + +from langchain_qdrant import Qdrant +from langchain_qdrant.vectorstores import QdrantException +from tests.integration_tests.common import ( + ConsistentFakeEmbeddings, + assert_documents_equals, +) +from tests.integration_tests.fixtures import ( + qdrant_locations, +) + + +@pytest.mark.parametrize("qdrant_location", qdrant_locations()) +async def test_qdrant_from_texts_stores_duplicated_texts(qdrant_location: str) -> None: + """Test end to end Qdrant.afrom_texts stores duplicated texts separately.""" + collection_name = uuid.uuid4().hex + + vec_store = await Qdrant.afrom_texts( + ["abc", "abc"], + ConsistentFakeEmbeddings(), + collection_name=collection_name, + location=qdrant_location, + ) + + client = vec_store.client + assert 2 == client.count(collection_name).count + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +@pytest.mark.parametrize("qdrant_location", qdrant_locations()) +async def test_qdrant_from_texts_stores_ids( + batch_size: int, vector_name: Optional[str], qdrant_location: str +) -> None: + """Test end to end Qdrant.afrom_texts stores provided ids.""" + collection_name = uuid.uuid4().hex + ids = [ + "fa38d572-4c31-4579-aedc-1960d79df6df", + "cdc1aa36-d6ab-4fb2-8a94-56674fd27484", + ] + vec_store = await Qdrant.afrom_texts( + ["abc", "def"], + ConsistentFakeEmbeddings(), + ids=ids, + collection_name=collection_name, + batch_size=batch_size, + vector_name=vector_name, + location=qdrant_location, + ) + + client = vec_store.client + assert 2 == client.count(collection_name).count + stored_ids = [point.id for point in client.scroll(collection_name)[0]] + assert set(ids) == set(stored_ids) + + +@pytest.mark.parametrize("vector_name", ["custom-vector"]) +@pytest.mark.parametrize("qdrant_location", qdrant_locations()) +async def test_qdrant_from_texts_stores_embeddings_as_named_vectors( + vector_name: str, + qdrant_location: str, +) -> None: + """Test end to end Qdrant.afrom_texts stores named vectors if name is provided.""" + collection_name = uuid.uuid4().hex + + vec_store = await Qdrant.afrom_texts( + ["lorem", "ipsum", "dolor", "sit", "amet"], + ConsistentFakeEmbeddings(), + collection_name=collection_name, + vector_name=vector_name, + location=qdrant_location, + ) + + client = vec_store.client + assert 5 == client.count(collection_name).count + assert all( + vector_name in point.vector # type: ignore[operator] + for point in client.scroll(collection_name, with_vectors=True)[0] + ) + + +@pytest.mark.parametrize("location", qdrant_locations(use_in_memory=False)) +@pytest.mark.parametrize("vector_name", [None, "custom-vector"]) +async def test_qdrant_from_texts_reuses_same_collection( + location: str, vector_name: Optional[str] +) -> None: + """Test if Qdrant.afrom_texts reuses the same collection""" + collection_name = uuid.uuid4().hex + embeddings = ConsistentFakeEmbeddings() + + await Qdrant.afrom_texts( + ["lorem", "ipsum", "dolor", "sit", "amet"], + embeddings, + collection_name=collection_name, + vector_name=vector_name, + location=location, + ) + + vec_store = await Qdrant.afrom_texts( + ["foo", "bar"], + embeddings, + collection_name=collection_name, + vector_name=vector_name, + location=location, + ) + + client = vec_store.client + assert 7 == client.count(collection_name).count + + +@pytest.mark.parametrize("location", qdrant_locations(use_in_memory=False)) +@pytest.mark.parametrize("vector_name", [None, "custom-vector"]) +async def test_qdrant_from_texts_raises_error_on_different_dimensionality( + location: str, + vector_name: Optional[str], +) -> None: + """Test if Qdrant.afrom_texts raises an exception if dimensionality does not + match""" + collection_name = uuid.uuid4().hex + + await Qdrant.afrom_texts( + ["lorem", "ipsum", "dolor", "sit", "amet"], + ConsistentFakeEmbeddings(dimensionality=10), + collection_name=collection_name, + vector_name=vector_name, + location=location, + ) + + with pytest.raises(QdrantException): + await Qdrant.afrom_texts( + ["foo", "bar"], + ConsistentFakeEmbeddings(dimensionality=5), + collection_name=collection_name, + vector_name=vector_name, + location=location, + ) + + +@pytest.mark.parametrize("location", qdrant_locations(use_in_memory=False)) +@pytest.mark.parametrize( + ["first_vector_name", "second_vector_name"], + [ + (None, "custom-vector"), + ("custom-vector", None), + ("my-first-vector", "my-second_vector"), + ], +) +async def test_qdrant_from_texts_raises_error_on_different_vector_name( + location: str, + first_vector_name: Optional[str], + second_vector_name: Optional[str], +) -> None: + """Test if Qdrant.afrom_texts raises an exception if vector name does not match""" + collection_name = uuid.uuid4().hex + + await Qdrant.afrom_texts( + ["lorem", "ipsum", "dolor", "sit", "amet"], + ConsistentFakeEmbeddings(dimensionality=10), + collection_name=collection_name, + vector_name=first_vector_name, + location=location, + ) + + with pytest.raises(QdrantException): + await Qdrant.afrom_texts( + ["foo", "bar"], + ConsistentFakeEmbeddings(dimensionality=5), + collection_name=collection_name, + vector_name=second_vector_name, + location=location, + ) + + +@pytest.mark.parametrize("location", qdrant_locations(use_in_memory=False)) +async def test_qdrant_from_texts_raises_error_on_different_distance( + location: str, +) -> None: + """Test if Qdrant.afrom_texts raises an exception if distance does not match""" + collection_name = uuid.uuid4().hex + + await Qdrant.afrom_texts( + ["lorem", "ipsum", "dolor", "sit", "amet"], + ConsistentFakeEmbeddings(dimensionality=10), + collection_name=collection_name, + distance_func="Cosine", + location=location, + ) + + with pytest.raises(QdrantException): + await Qdrant.afrom_texts( + ["foo", "bar"], + ConsistentFakeEmbeddings(dimensionality=5), + collection_name=collection_name, + distance_func="Euclid", + location=location, + ) + + +@pytest.mark.parametrize("location", qdrant_locations(use_in_memory=False)) +@pytest.mark.parametrize("vector_name", [None, "custom-vector"]) +async def test_qdrant_from_texts_recreates_collection_on_force_recreate( + location: str, + vector_name: Optional[str], +) -> None: + """Test if Qdrant.afrom_texts recreates the collection even if config mismatches""" + from qdrant_client import QdrantClient + + collection_name = uuid.uuid4().hex + + await Qdrant.afrom_texts( + ["lorem", "ipsum", "dolor", "sit", "amet"], + ConsistentFakeEmbeddings(dimensionality=10), + collection_name=collection_name, + vector_name=vector_name, + location=location, + ) + + await Qdrant.afrom_texts( + ["foo", "bar"], + ConsistentFakeEmbeddings(dimensionality=5), + collection_name=collection_name, + vector_name=vector_name, + force_recreate=True, + location=location, + ) + + client = QdrantClient(location=location, api_key=os.getenv("QDRANT_API_KEY")) + assert 2 == client.count(collection_name).count + vector_params = client.get_collection(collection_name).config.params.vectors + if vector_name is not None: + vector_params = vector_params[vector_name] # type: ignore[index] + assert 5 == vector_params.size # type: ignore[union-attr] + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("content_payload_key", [Qdrant.CONTENT_KEY, "foo"]) +@pytest.mark.parametrize("metadata_payload_key", [Qdrant.METADATA_KEY, "bar"]) +@pytest.mark.parametrize("qdrant_location", qdrant_locations()) +async def test_qdrant_from_texts_stores_metadatas( + batch_size: int, + content_payload_key: str, + metadata_payload_key: str, + qdrant_location: str, +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + metadatas = [{"page": i} for i in range(len(texts))] + docsearch = await Qdrant.afrom_texts( + texts, + ConsistentFakeEmbeddings(), + metadatas=metadatas, + content_payload_key=content_payload_key, + metadata_payload_key=metadata_payload_key, + batch_size=batch_size, + location=qdrant_location, + ) + output = await docsearch.asimilarity_search("foo", k=1) + assert_documents_equals( + output, [Document(page_content="foo", metadata={"page": 0})] + ) diff --git a/libs/partners/qdrant/tests/integration_tests/async_api/test_max_marginal_relevance.py b/libs/partners/qdrant/tests/integration_tests/async_api/test_max_marginal_relevance.py new file mode 100644 index 00000000000..3a49d4b861a --- /dev/null +++ b/libs/partners/qdrant/tests/integration_tests/async_api/test_max_marginal_relevance.py @@ -0,0 +1,51 @@ +from typing import Optional + +import pytest +from langchain_core.documents import Document + +from langchain_qdrant import Qdrant +from tests.integration_tests.common import ( + ConsistentFakeEmbeddings, + assert_documents_equals, +) +from tests.integration_tests.fixtures import ( + qdrant_locations, +) + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("content_payload_key", [Qdrant.CONTENT_KEY, "test_content"]) +@pytest.mark.parametrize("metadata_payload_key", [Qdrant.METADATA_KEY, "test_metadata"]) +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +@pytest.mark.parametrize("qdrant_location", qdrant_locations()) +async def test_qdrant_max_marginal_relevance_search( + batch_size: int, + content_payload_key: str, + metadata_payload_key: str, + vector_name: Optional[str], + qdrant_location: str, +) -> None: + """Test end to end construction and MRR search.""" + texts = ["foo", "bar", "baz"] + metadatas = [{"page": i} for i in range(len(texts))] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + metadatas=metadatas, + content_payload_key=content_payload_key, + metadata_payload_key=metadata_payload_key, + batch_size=batch_size, + vector_name=vector_name, + location=qdrant_location, + distance_func="EUCLID", # Euclid distance used to avoid normalization + ) + output = await docsearch.amax_marginal_relevance_search( + "foo", k=2, fetch_k=3, lambda_mult=0.0 + ) + assert_documents_equals( + output, + [ + Document(page_content="foo", metadata={"page": 0}), + Document(page_content="baz", metadata={"page": 2}), + ], + ) diff --git a/libs/partners/qdrant/tests/integration_tests/async_api/test_similarity_search.py b/libs/partners/qdrant/tests/integration_tests/async_api/test_similarity_search.py new file mode 100644 index 00000000000..5b86b9bc380 --- /dev/null +++ b/libs/partners/qdrant/tests/integration_tests/async_api/test_similarity_search.py @@ -0,0 +1,305 @@ +from typing import Optional + +import numpy as np +import pytest +from langchain_core.documents import Document + +from langchain_qdrant import Qdrant +from tests.integration_tests.common import ( + ConsistentFakeEmbeddings, + assert_documents_equals, +) +from tests.integration_tests.fixtures import qdrant_locations + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("content_payload_key", [Qdrant.CONTENT_KEY, "foo"]) +@pytest.mark.parametrize("metadata_payload_key", [Qdrant.METADATA_KEY, "bar"]) +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +@pytest.mark.parametrize("qdrant_location", qdrant_locations()) +async def test_qdrant_similarity_search( + batch_size: int, + content_payload_key: str, + metadata_payload_key: str, + vector_name: Optional[str], + qdrant_location: str, +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + content_payload_key=content_payload_key, + metadata_payload_key=metadata_payload_key, + batch_size=batch_size, + vector_name=vector_name, + location=qdrant_location, + ) + output = await docsearch.asimilarity_search("foo", k=1) + assert_documents_equals(output, [Document(page_content="foo")]) + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("content_payload_key", [Qdrant.CONTENT_KEY, "foo"]) +@pytest.mark.parametrize("metadata_payload_key", [Qdrant.METADATA_KEY, "bar"]) +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +@pytest.mark.parametrize("qdrant_location", qdrant_locations()) +async def test_qdrant_similarity_search_by_vector( + batch_size: int, + content_payload_key: str, + metadata_payload_key: str, + vector_name: Optional[str], + qdrant_location: str, +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + content_payload_key=content_payload_key, + metadata_payload_key=metadata_payload_key, + batch_size=batch_size, + vector_name=vector_name, + location=qdrant_location, + ) + embeddings = ConsistentFakeEmbeddings().embed_query("foo") + output = await docsearch.asimilarity_search_by_vector(embeddings, k=1) + assert_documents_equals(output, [Document(page_content="foo")]) + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("content_payload_key", [Qdrant.CONTENT_KEY, "foo"]) +@pytest.mark.parametrize("metadata_payload_key", [Qdrant.METADATA_KEY, "bar"]) +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +@pytest.mark.parametrize("qdrant_location", qdrant_locations()) +async def test_qdrant_similarity_search_with_score_by_vector( + batch_size: int, + content_payload_key: str, + metadata_payload_key: str, + vector_name: Optional[str], + qdrant_location: str, +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + content_payload_key=content_payload_key, + metadata_payload_key=metadata_payload_key, + batch_size=batch_size, + vector_name=vector_name, + location=qdrant_location, + ) + embeddings = ConsistentFakeEmbeddings().embed_query("foo") + output = await docsearch.asimilarity_search_with_score_by_vector(embeddings, k=1) + assert len(output) == 1 + document, score = output[0] + assert_documents_equals([document], [Document(page_content="foo")]) + assert score >= 0 + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +@pytest.mark.parametrize("qdrant_location", qdrant_locations()) +async def test_qdrant_similarity_search_filters( + batch_size: int, vector_name: Optional[str], qdrant_location: str +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + metadatas = [ + {"page": i, "metadata": {"page": i + 1, "pages": [i + 2, -1]}} + for i in range(len(texts)) + ] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + metadatas=metadatas, + batch_size=batch_size, + vector_name=vector_name, + location=qdrant_location, + ) + + output = await docsearch.asimilarity_search( + "foo", k=1, filter={"page": 1, "metadata": {"page": 2, "pages": [3]}} + ) + assert_documents_equals( + output, + [ + Document( + page_content="bar", + metadata={"page": 1, "metadata": {"page": 2, "pages": [3, -1]}}, + ) + ], + ) + + +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +@pytest.mark.parametrize("qdrant_location", qdrant_locations()) +async def test_qdrant_similarity_search_with_relevance_score_no_threshold( + vector_name: Optional[str], + qdrant_location: str, +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + metadatas = [ + {"page": i, "metadata": {"page": i + 1, "pages": [i + 2, -1]}} + for i in range(len(texts)) + ] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + metadatas=metadatas, + vector_name=vector_name, + location=qdrant_location, + ) + output = await docsearch.asimilarity_search_with_relevance_scores( + "foo", k=3, score_threshold=None + ) + assert len(output) == 3 + for i in range(len(output)): + assert round(output[i][1], 2) >= 0 + assert round(output[i][1], 2) <= 1 + + +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +@pytest.mark.parametrize("qdrant_location", qdrant_locations()) +async def test_qdrant_similarity_search_with_relevance_score_with_threshold( + vector_name: Optional[str], + qdrant_location: str, +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + metadatas = [ + {"page": i, "metadata": {"page": i + 1, "pages": [i + 2, -1]}} + for i in range(len(texts)) + ] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + metadatas=metadatas, + vector_name=vector_name, + location=qdrant_location, + ) + + score_threshold = 0.98 + kwargs = {"score_threshold": score_threshold} + output = await docsearch.asimilarity_search_with_relevance_scores( + "foo", k=3, **kwargs + ) + assert len(output) == 1 + assert all([score >= score_threshold for _, score in output]) + + +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +@pytest.mark.parametrize("qdrant_location", qdrant_locations()) +async def test_similarity_search_with_relevance_score_with_threshold_and_filter( + vector_name: Optional[str], + qdrant_location: str, +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + metadatas = [ + {"page": i, "metadata": {"page": i + 1, "pages": [i + 2, -1]}} + for i in range(len(texts)) + ] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + metadatas=metadatas, + vector_name=vector_name, + location=qdrant_location, + ) + score_threshold = 0.99 # for almost exact match + # test negative filter condition + negative_filter = {"page": 1, "metadata": {"page": 2, "pages": [3]}} + kwargs = {"filter": negative_filter, "score_threshold": score_threshold} + output = docsearch.similarity_search_with_relevance_scores("foo", k=3, **kwargs) + assert len(output) == 0 + # test positive filter condition + positive_filter = {"page": 0, "metadata": {"page": 1, "pages": [2]}} + kwargs = {"filter": positive_filter, "score_threshold": score_threshold} + output = await docsearch.asimilarity_search_with_relevance_scores( + "foo", k=3, **kwargs + ) + assert len(output) == 1 + assert all([score >= score_threshold for _, score in output]) + + +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +@pytest.mark.parametrize("qdrant_location", qdrant_locations()) +async def test_qdrant_similarity_search_filters_with_qdrant_filters( + vector_name: Optional[str], + qdrant_location: str, +) -> None: + """Test end to end construction and search.""" + from qdrant_client.http import models as rest + + texts = ["foo", "bar", "baz"] + metadatas = [ + {"page": i, "details": {"page": i + 1, "pages": [i + 2, -1]}} + for i in range(len(texts)) + ] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + metadatas=metadatas, + vector_name=vector_name, + location=qdrant_location, + ) + + qdrant_filter = rest.Filter( + must=[ + rest.FieldCondition( + key="metadata.page", + match=rest.MatchValue(value=1), + ), + rest.FieldCondition( + key="metadata.details.page", + match=rest.MatchValue(value=2), + ), + rest.FieldCondition( + key="metadata.details.pages", + match=rest.MatchAny(any=[3]), + ), + ] + ) + output = await docsearch.asimilarity_search("foo", k=1, filter=qdrant_filter) + assert_documents_equals( + output, + [ + Document( + page_content="bar", + metadata={"page": 1, "details": {"page": 2, "pages": [3, -1]}}, + ) + ], + ) + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("content_payload_key", [Qdrant.CONTENT_KEY, "foo"]) +@pytest.mark.parametrize("metadata_payload_key", [Qdrant.METADATA_KEY, "bar"]) +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +@pytest.mark.parametrize("qdrant_location", qdrant_locations()) +async def test_qdrant_similarity_search_with_relevance_scores( + batch_size: int, + content_payload_key: str, + metadata_payload_key: str, + vector_name: str, + qdrant_location: str, +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + content_payload_key=content_payload_key, + metadata_payload_key=metadata_payload_key, + batch_size=batch_size, + vector_name=vector_name, + location=qdrant_location, + ) + output = await docsearch.asimilarity_search_with_relevance_scores("foo", k=3) + + assert all( + (1 >= score or np.isclose(score, 1)) and score >= 0 for _, score in output + ) diff --git a/libs/partners/qdrant/tests/integration_tests/common.py b/libs/partners/qdrant/tests/integration_tests/common.py new file mode 100644 index 00000000000..8336791f3dc --- /dev/null +++ b/libs/partners/qdrant/tests/integration_tests/common.py @@ -0,0 +1,79 @@ +from typing import List + +import requests # type: ignore +from langchain_core.documents import Document +from langchain_core.embeddings import Embeddings + + +def qdrant_running_locally() -> bool: + """Check if Qdrant is running at http://localhost:6333.""" + + try: + response = requests.get("http://localhost:6333", timeout=10.0) + response_json = response.json() + return response_json.get("title") == "qdrant - vector search engine" + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): + return False + + +def assert_documents_equals(actual: List[Document], expected: List[Document]): # type: ignore[no-untyped-def] + assert len(actual) == len(expected) + + for actual_doc, expected_doc in zip(actual, expected): + assert actual_doc.page_content == expected_doc.page_content + + assert "_id" in actual_doc.metadata + assert "_collection_name" in actual_doc.metadata + + actual_doc.metadata.pop("_id") + actual_doc.metadata.pop("_collection_name") + + assert actual_doc.metadata == expected_doc.metadata + + +class FakeEmbeddings(Embeddings): + """Fake embeddings functionality for testing.""" + + def embed_documents(self, texts: List[str]) -> List[List[float]]: + """Return simple embeddings. + Embeddings encode each text as its index.""" + return [[float(1.0)] * 9 + [float(i)] for i in range(len(texts))] + + async def aembed_documents(self, texts: List[str]) -> List[List[float]]: + return self.embed_documents(texts) + + def embed_query(self, text: str) -> List[float]: + """Return constant query embeddings. + Embeddings are identical to embed_documents(texts)[0]. + Distance to each text will be that text's index, + as it was passed to embed_documents.""" + return [float(1.0)] * 9 + [float(0.0)] + + async def aembed_query(self, text: str) -> List[float]: + return self.embed_query(text) + + +class ConsistentFakeEmbeddings(FakeEmbeddings): + """Fake embeddings which remember all the texts seen so far to return consistent + vectors for the same texts.""" + + def __init__(self, dimensionality: int = 10) -> None: + self.known_texts: List[str] = [] + self.dimensionality = dimensionality + + def embed_documents(self, texts: List[str]) -> List[List[float]]: + """Return consistent embeddings for each text seen so far.""" + out_vectors = [] + for text in texts: + if text not in self.known_texts: + self.known_texts.append(text) + vector = [float(1.0)] * (self.dimensionality - 1) + [ + float(self.known_texts.index(text)) + ] + out_vectors.append(vector) + return out_vectors + + def embed_query(self, text: str) -> List[float]: + """Return consistent embeddings for the text, if seen before, or a constant + one if the text is unknown.""" + return self.embed_documents([text])[0] diff --git a/libs/partners/qdrant/tests/integration_tests/conftest.py b/libs/partners/qdrant/tests/integration_tests/conftest.py new file mode 100644 index 00000000000..b7830c1cd81 --- /dev/null +++ b/libs/partners/qdrant/tests/integration_tests/conftest.py @@ -0,0 +1,15 @@ +import os + +from qdrant_client import QdrantClient + +from tests.integration_tests.fixtures import qdrant_locations + + +def pytest_sessionfinish() -> None: + """Clean up all collections after the test session.""" + for location in qdrant_locations(): + client = QdrantClient(location=location, api_key=os.getenv("QDRANT_API_KEY")) + collections = client.get_collections().collections + + for collection in collections: + client.delete_collection(collection.name) diff --git a/libs/partners/qdrant/tests/integration_tests/fixtures.py b/libs/partners/qdrant/tests/integration_tests/fixtures.py new file mode 100644 index 00000000000..50819050dee --- /dev/null +++ b/libs/partners/qdrant/tests/integration_tests/fixtures.py @@ -0,0 +1,25 @@ +import logging +import os +from typing import List + +from tests.integration_tests.common import qdrant_running_locally + +logger = logging.getLogger(__name__) + + +def qdrant_locations(use_in_memory: bool = True) -> List[str]: + locations = [] + + if use_in_memory: + logger.info("Running Qdrant tests with in-memory mode.") + locations.append(":memory:") + + if qdrant_running_locally(): + logger.info("Running Qdrant tests with local Qdrant instance.") + locations.append("http://localhost:6333") + + if qdrant_url := os.getenv("QDRANT_URL"): + logger.info(f"Running Qdrant tests with Qdrant instance at {qdrant_url}.") + locations.append(qdrant_url) + + return locations diff --git a/libs/partners/qdrant/tests/integration_tests/test_add_texts.py b/libs/partners/qdrant/tests/integration_tests/test_add_texts.py new file mode 100644 index 00000000000..2329c2f6285 --- /dev/null +++ b/libs/partners/qdrant/tests/integration_tests/test_add_texts.py @@ -0,0 +1,135 @@ +import uuid +from typing import Optional + +import pytest +from langchain_core.documents import Document + +from langchain_qdrant import Qdrant +from tests.integration_tests.common import ( + ConsistentFakeEmbeddings, + assert_documents_equals, +) + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +def test_qdrant_add_documents_extends_existing_collection( + batch_size: int, vector_name: Optional[str] +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + docsearch: Qdrant = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + location=":memory:", + batch_size=batch_size, + vector_name=vector_name, + ) + + new_texts = ["foobar", "foobaz"] + docsearch.add_documents( + [Document(page_content=content) for content in new_texts], batch_size=batch_size + ) + output = docsearch.similarity_search("foobar", k=1) + # ConsistentFakeEmbeddings return the same query embedding as the first document + # embedding computed in `embedding.embed_documents`. Thus, "foo" embedding is the + # same as "foobar" embedding + assert_documents_equals(output, [Document(page_content="foobar")]) + + +@pytest.mark.parametrize("batch_size", [1, 64]) +def test_qdrant_add_texts_returns_all_ids(batch_size: int) -> None: + """Test end to end Qdrant.add_texts returns unique ids.""" + docsearch: Qdrant = Qdrant.from_texts( + ["foobar"], + ConsistentFakeEmbeddings(), + location=":memory:", + batch_size=batch_size, + ) + + ids = docsearch.add_texts(["foo", "bar", "baz"]) + assert 3 == len(ids) + assert 3 == len(set(ids)) + + +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +def test_qdrant_add_texts_stores_duplicated_texts(vector_name: Optional[str]) -> None: + """Test end to end Qdrant.add_texts stores duplicated texts separately.""" + from qdrant_client import QdrantClient + from qdrant_client.http import models as rest + + client = QdrantClient(":memory:") + collection_name = uuid.uuid4().hex + vectors_config = rest.VectorParams(size=10, distance=rest.Distance.COSINE) + if vector_name is not None: + vectors_config = {vector_name: vectors_config} # type: ignore[assignment] + client.recreate_collection(collection_name, vectors_config=vectors_config) + + vec_store = Qdrant( + client, + collection_name, + embeddings=ConsistentFakeEmbeddings(), + vector_name=vector_name, + ) + ids = vec_store.add_texts(["abc", "abc"], [{"a": 1}, {"a": 2}]) + + assert 2 == len(set(ids)) + assert 2 == client.count(collection_name).count + + +@pytest.mark.parametrize("batch_size", [1, 64]) +def test_qdrant_add_texts_stores_ids(batch_size: int) -> None: + """Test end to end Qdrant.add_texts stores provided ids.""" + from qdrant_client import QdrantClient + from qdrant_client.http import models as rest + + ids = [ + "fa38d572-4c31-4579-aedc-1960d79df6df", + "cdc1aa36-d6ab-4fb2-8a94-56674fd27484", + ] + + client = QdrantClient(":memory:") + collection_name = uuid.uuid4().hex + client.recreate_collection( + collection_name, + vectors_config=rest.VectorParams(size=10, distance=rest.Distance.COSINE), + ) + + vec_store = Qdrant(client, collection_name, ConsistentFakeEmbeddings()) + returned_ids = vec_store.add_texts(["abc", "def"], ids=ids, batch_size=batch_size) + + assert all(first == second for first, second in zip(ids, returned_ids)) + assert 2 == client.count(collection_name).count + stored_ids = [point.id for point in client.scroll(collection_name)[0]] + assert set(ids) == set(stored_ids) + + +@pytest.mark.parametrize("vector_name", ["custom-vector"]) +def test_qdrant_add_texts_stores_embeddings_as_named_vectors(vector_name: str) -> None: + """Test end to end Qdrant.add_texts stores named vectors if name is provided.""" + from qdrant_client import QdrantClient + from qdrant_client.http import models as rest + + collection_name = uuid.uuid4().hex + + client = QdrantClient(":memory:") + client.recreate_collection( + collection_name, + vectors_config={ + vector_name: rest.VectorParams(size=10, distance=rest.Distance.COSINE) + }, + ) + + vec_store = Qdrant( + client, + collection_name, + ConsistentFakeEmbeddings(), + vector_name=vector_name, + ) + vec_store.add_texts(["lorem", "ipsum", "dolor", "sit", "amet"]) + + assert 5 == client.count(collection_name).count + assert all( + vector_name in point.vector # type: ignore[operator] + for point in client.scroll(collection_name, with_vectors=True)[0] + ) diff --git a/libs/partners/qdrant/tests/integration_tests/test_compile.py b/libs/partners/qdrant/tests/integration_tests/test_compile.py new file mode 100644 index 00000000000..33ecccdfa0f --- /dev/null +++ b/libs/partners/qdrant/tests/integration_tests/test_compile.py @@ -0,0 +1,7 @@ +import pytest + + +@pytest.mark.compile +def test_placeholder() -> None: + """Used for compiling integration tests without running any real tests.""" + pass diff --git a/libs/partners/qdrant/tests/integration_tests/test_embedding_interface.py b/libs/partners/qdrant/tests/integration_tests/test_embedding_interface.py new file mode 100644 index 00000000000..7de6e8a28c6 --- /dev/null +++ b/libs/partners/qdrant/tests/integration_tests/test_embedding_interface.py @@ -0,0 +1,58 @@ +import uuid +from typing import Callable, Optional + +import pytest +from langchain_core.embeddings import Embeddings + +from langchain_qdrant import Qdrant +from tests.integration_tests.common import ConsistentFakeEmbeddings + + +@pytest.mark.parametrize( + ["embeddings", "embedding_function"], + [ + (ConsistentFakeEmbeddings(), None), + (ConsistentFakeEmbeddings().embed_query, None), + (None, ConsistentFakeEmbeddings().embed_query), + ], +) +def test_qdrant_embedding_interface( + embeddings: Optional[Embeddings], embedding_function: Optional[Callable] +) -> None: + """Test Qdrant may accept different types for embeddings.""" + from qdrant_client import QdrantClient + + client = QdrantClient(":memory:") + collection_name = uuid.uuid4().hex + + Qdrant( + client, + collection_name, + embeddings=embeddings, + embedding_function=embedding_function, + ) + + +@pytest.mark.parametrize( + ["embeddings", "embedding_function"], + [ + (ConsistentFakeEmbeddings(), ConsistentFakeEmbeddings().embed_query), + (None, None), + ], +) +def test_qdrant_embedding_interface_raises_value_error( + embeddings: Optional[Embeddings], embedding_function: Optional[Callable] +) -> None: + """Test Qdrant requires only one method for embeddings.""" + from qdrant_client import QdrantClient + + client = QdrantClient(":memory:") + collection_name = uuid.uuid4().hex + + with pytest.raises(ValueError): + Qdrant( + client, + collection_name, + embeddings=embeddings, + embedding_function=embedding_function, + ) diff --git a/libs/partners/qdrant/tests/integration_tests/test_from_existing_collection.py b/libs/partners/qdrant/tests/integration_tests/test_from_existing_collection.py new file mode 100644 index 00000000000..0f681e3ccbd --- /dev/null +++ b/libs/partners/qdrant/tests/integration_tests/test_from_existing_collection.py @@ -0,0 +1,37 @@ +import tempfile +import uuid + +import pytest + +from langchain_qdrant import Qdrant +from tests.integration_tests.common import ConsistentFakeEmbeddings + + +@pytest.mark.parametrize("vector_name", ["custom-vector"]) +def test_qdrant_from_existing_collection_uses_same_collection(vector_name: str) -> None: + """Test if the Qdrant.from_existing_collection reuses the same collection.""" + from qdrant_client import QdrantClient + + collection_name = uuid.uuid4().hex + with tempfile.TemporaryDirectory() as tmpdir: + docs = ["foo"] + qdrant = Qdrant.from_texts( + docs, + embedding=ConsistentFakeEmbeddings(), + path=str(tmpdir), + collection_name=collection_name, + vector_name=vector_name, + ) + del qdrant + + qdrant = Qdrant.from_existing_collection( + embedding=ConsistentFakeEmbeddings(), + path=str(tmpdir), + collection_name=collection_name, + vector_name=vector_name, + ) + qdrant.add_texts(["baz", "bar"]) + del qdrant + + client = QdrantClient(path=str(tmpdir)) + assert 3 == client.count(collection_name).count diff --git a/libs/partners/qdrant/tests/integration_tests/test_from_texts.py b/libs/partners/qdrant/tests/integration_tests/test_from_texts.py new file mode 100644 index 00000000000..175b77783c7 --- /dev/null +++ b/libs/partners/qdrant/tests/integration_tests/test_from_texts.py @@ -0,0 +1,288 @@ +import tempfile +import uuid +from typing import Optional + +import pytest +from langchain_core.documents import Document + +from langchain_qdrant import Qdrant +from langchain_qdrant.vectorstores import QdrantException +from tests.integration_tests.common import ( + ConsistentFakeEmbeddings, + assert_documents_equals, +) +from tests.integration_tests.fixtures import qdrant_locations + + +def test_qdrant_from_texts_stores_duplicated_texts() -> None: + """Test end to end Qdrant.from_texts stores duplicated texts separately.""" + from qdrant_client import QdrantClient + + collection_name = uuid.uuid4().hex + + with tempfile.TemporaryDirectory() as tmpdir: + vec_store = Qdrant.from_texts( + ["abc", "abc"], + ConsistentFakeEmbeddings(), + collection_name=collection_name, + path=str(tmpdir), + ) + del vec_store + + client = QdrantClient(path=str(tmpdir)) + assert 2 == client.count(collection_name).count + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +def test_qdrant_from_texts_stores_ids( + batch_size: int, vector_name: Optional[str] +) -> None: + """Test end to end Qdrant.from_texts stores provided ids.""" + from qdrant_client import QdrantClient + + collection_name = uuid.uuid4().hex + with tempfile.TemporaryDirectory() as tmpdir: + ids = [ + "fa38d572-4c31-4579-aedc-1960d79df6df", + "cdc1aa36-d6ab-4fb2-8a94-56674fd27484", + ] + vec_store = Qdrant.from_texts( + ["abc", "def"], + ConsistentFakeEmbeddings(), + ids=ids, + collection_name=collection_name, + path=str(tmpdir), + batch_size=batch_size, + vector_name=vector_name, + ) + del vec_store + + client = QdrantClient(path=str(tmpdir)) + assert 2 == client.count(collection_name).count + stored_ids = [point.id for point in client.scroll(collection_name)[0]] + assert set(ids) == set(stored_ids) + + +@pytest.mark.parametrize("vector_name", ["custom-vector"]) +def test_qdrant_from_texts_stores_embeddings_as_named_vectors(vector_name: str) -> None: + """Test end to end Qdrant.from_texts stores named vectors if name is provided.""" + from qdrant_client import QdrantClient + + collection_name = uuid.uuid4().hex + with tempfile.TemporaryDirectory() as tmpdir: + vec_store = Qdrant.from_texts( + ["lorem", "ipsum", "dolor", "sit", "amet"], + ConsistentFakeEmbeddings(), + collection_name=collection_name, + path=str(tmpdir), + vector_name=vector_name, + ) + del vec_store + + client = QdrantClient(path=str(tmpdir)) + assert 5 == client.count(collection_name).count + assert all( + vector_name in point.vector # type: ignore[operator] + for point in client.scroll(collection_name, with_vectors=True)[0] + ) + + +@pytest.mark.parametrize("vector_name", [None, "custom-vector"]) +def test_qdrant_from_texts_reuses_same_collection(vector_name: Optional[str]) -> None: + """Test if Qdrant.from_texts reuses the same collection""" + from qdrant_client import QdrantClient + + collection_name = uuid.uuid4().hex + embeddings = ConsistentFakeEmbeddings() + with tempfile.TemporaryDirectory() as tmpdir: + vec_store = Qdrant.from_texts( + ["lorem", "ipsum", "dolor", "sit", "amet"], + embeddings, + collection_name=collection_name, + path=str(tmpdir), + vector_name=vector_name, + ) + del vec_store + + vec_store = Qdrant.from_texts( + ["foo", "bar"], + embeddings, + collection_name=collection_name, + path=str(tmpdir), + vector_name=vector_name, + ) + del vec_store + + client = QdrantClient(path=str(tmpdir)) + assert 7 == client.count(collection_name).count + + +@pytest.mark.parametrize("vector_name", [None, "custom-vector"]) +def test_qdrant_from_texts_raises_error_on_different_dimensionality( + vector_name: Optional[str], +) -> None: + """Test if Qdrant.from_texts raises an exception if dimensionality does not match""" + collection_name = uuid.uuid4().hex + with tempfile.TemporaryDirectory() as tmpdir: + vec_store = Qdrant.from_texts( + ["lorem", "ipsum", "dolor", "sit", "amet"], + ConsistentFakeEmbeddings(dimensionality=10), + collection_name=collection_name, + path=str(tmpdir), + vector_name=vector_name, + ) + del vec_store + + with pytest.raises(QdrantException): + Qdrant.from_texts( + ["foo", "bar"], + ConsistentFakeEmbeddings(dimensionality=5), + collection_name=collection_name, + path=str(tmpdir), + vector_name=vector_name, + ) + + +@pytest.mark.parametrize( + ["first_vector_name", "second_vector_name"], + [ + (None, "custom-vector"), + ("custom-vector", None), + ("my-first-vector", "my-second_vector"), + ], +) +def test_qdrant_from_texts_raises_error_on_different_vector_name( + first_vector_name: Optional[str], + second_vector_name: Optional[str], +) -> None: + """Test if Qdrant.from_texts raises an exception if vector name does not match""" + collection_name = uuid.uuid4().hex + with tempfile.TemporaryDirectory() as tmpdir: + vec_store = Qdrant.from_texts( + ["lorem", "ipsum", "dolor", "sit", "amet"], + ConsistentFakeEmbeddings(dimensionality=10), + collection_name=collection_name, + path=str(tmpdir), + vector_name=first_vector_name, + ) + del vec_store + + with pytest.raises(QdrantException): + Qdrant.from_texts( + ["foo", "bar"], + ConsistentFakeEmbeddings(dimensionality=5), + collection_name=collection_name, + path=str(tmpdir), + vector_name=second_vector_name, + ) + + +def test_qdrant_from_texts_raises_error_on_different_distance() -> None: + """Test if Qdrant.from_texts raises an exception if distance does not match""" + collection_name = uuid.uuid4().hex + with tempfile.TemporaryDirectory() as tmpdir: + vec_store = Qdrant.from_texts( + ["lorem", "ipsum", "dolor", "sit", "amet"], + ConsistentFakeEmbeddings(), + collection_name=collection_name, + path=str(tmpdir), + distance_func="Cosine", + ) + del vec_store + + with pytest.raises(QdrantException) as excinfo: + Qdrant.from_texts( + ["foo", "bar"], + ConsistentFakeEmbeddings(), + collection_name=collection_name, + path=str(tmpdir), + distance_func="Euclid", + ) + + expected_message = ( + "configured for COSINE similarity, but requested EUCLID. Please set " + "`distance_func` parameter to `COSINE`" + ) + assert expected_message in str(excinfo.value) + + +@pytest.mark.parametrize("vector_name", [None, "custom-vector"]) +def test_qdrant_from_texts_recreates_collection_on_force_recreate( + vector_name: Optional[str], +) -> None: + """Test if Qdrant.from_texts recreates the collection even if config mismatches""" + from qdrant_client import QdrantClient + + collection_name = uuid.uuid4().hex + with tempfile.TemporaryDirectory() as tmpdir: + vec_store = Qdrant.from_texts( + ["lorem", "ipsum", "dolor", "sit", "amet"], + ConsistentFakeEmbeddings(dimensionality=10), + collection_name=collection_name, + path=str(tmpdir), + vector_name=vector_name, + ) + del vec_store + + vec_store = Qdrant.from_texts( + ["foo", "bar"], + ConsistentFakeEmbeddings(dimensionality=5), + collection_name=collection_name, + path=str(tmpdir), + vector_name=vector_name, + force_recreate=True, + ) + del vec_store + + client = QdrantClient(path=str(tmpdir)) + assert 2 == client.count(collection_name).count + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("content_payload_key", [Qdrant.CONTENT_KEY, "foo"]) +@pytest.mark.parametrize("metadata_payload_key", [Qdrant.METADATA_KEY, "bar"]) +def test_qdrant_from_texts_stores_metadatas( + batch_size: int, content_payload_key: str, metadata_payload_key: str +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + metadatas = [{"page": i} for i in range(len(texts))] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + metadatas=metadatas, + location=":memory:", + content_payload_key=content_payload_key, + metadata_payload_key=metadata_payload_key, + batch_size=batch_size, + ) + output = docsearch.similarity_search("foo", k=1) + assert_documents_equals( + output, [Document(page_content="foo", metadata={"page": 0})] + ) + + +@pytest.mark.parametrize("location", qdrant_locations(use_in_memory=False)) +def test_from_texts_passed_optimizers_config_and_on_disk_payload(location: str) -> None: + from qdrant_client import models + + collection_name = uuid.uuid4().hex + texts = ["foo", "bar", "baz"] + metadatas = [{"page": i} for i in range(len(texts))] + optimizers_config = models.OptimizersConfigDiff(memmap_threshold=1000) + vec_store = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + metadatas=metadatas, + optimizers_config=optimizers_config, + on_disk_payload=True, + on_disk=True, + collection_name=collection_name, + location=location, + ) + + collection_info = vec_store.client.get_collection(collection_name) + assert collection_info.config.params.vectors.on_disk is True # type: ignore + assert collection_info.config.optimizer_config.memmap_threshold == 1000 + assert collection_info.config.params.on_disk_payload is True diff --git a/libs/partners/qdrant/tests/integration_tests/test_max_marginal_relevance.py b/libs/partners/qdrant/tests/integration_tests/test_max_marginal_relevance.py new file mode 100644 index 00000000000..a3f186b2feb --- /dev/null +++ b/libs/partners/qdrant/tests/integration_tests/test_max_marginal_relevance.py @@ -0,0 +1,67 @@ +from typing import Optional + +import pytest +from langchain_core.documents import Document + +from langchain_qdrant import Qdrant +from tests.integration_tests.common import ( + ConsistentFakeEmbeddings, + assert_documents_equals, +) + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("content_payload_key", [Qdrant.CONTENT_KEY, "test_content"]) +@pytest.mark.parametrize("metadata_payload_key", [Qdrant.METADATA_KEY, "test_metadata"]) +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +def test_qdrant_max_marginal_relevance_search( + batch_size: int, + content_payload_key: str, + metadata_payload_key: str, + vector_name: Optional[str], +) -> None: + """Test end to end construction and MRR search.""" + from qdrant_client import models + + filter = models.Filter( + must=[ + models.FieldCondition( + key=f"{metadata_payload_key}.page", + match=models.MatchValue( + value=2, + ), + ), + ], + ) + + texts = ["foo", "bar", "baz"] + metadatas = [{"page": i} for i in range(len(texts))] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + metadatas=metadatas, + location=":memory:", + content_payload_key=content_payload_key, + metadata_payload_key=metadata_payload_key, + batch_size=batch_size, + vector_name=vector_name, + distance_func="EUCLID", # Euclid distance used to avoid normalization + ) + output = docsearch.max_marginal_relevance_search( + "foo", k=2, fetch_k=3, lambda_mult=0.0 + ) + assert_documents_equals( + output, + [ + Document(page_content="foo", metadata={"page": 0}), + Document(page_content="baz", metadata={"page": 2}), + ], + ) + + output = docsearch.max_marginal_relevance_search( + "foo", k=2, fetch_k=3, lambda_mult=0.0, filter=filter + ) + assert_documents_equals( + output, + [Document(page_content="baz", metadata={"page": 2})], + ) diff --git a/libs/partners/qdrant/tests/integration_tests/test_similarity_search.py b/libs/partners/qdrant/tests/integration_tests/test_similarity_search.py new file mode 100644 index 00000000000..0373ad6dc17 --- /dev/null +++ b/libs/partners/qdrant/tests/integration_tests/test_similarity_search.py @@ -0,0 +1,284 @@ +from typing import Optional + +import numpy as np +import pytest +from langchain_core.documents import Document + +from langchain_qdrant import Qdrant +from tests.integration_tests.common import ( + ConsistentFakeEmbeddings, + assert_documents_equals, +) + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("content_payload_key", [Qdrant.CONTENT_KEY, "foo"]) +@pytest.mark.parametrize("metadata_payload_key", [Qdrant.METADATA_KEY, "bar"]) +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +def test_qdrant_similarity_search( + batch_size: int, + content_payload_key: str, + metadata_payload_key: str, + vector_name: Optional[str], +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + location=":memory:", + content_payload_key=content_payload_key, + metadata_payload_key=metadata_payload_key, + batch_size=batch_size, + vector_name=vector_name, + ) + output = docsearch.similarity_search("foo", k=1) + assert_documents_equals(actual=output, expected=[Document(page_content="foo")]) + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("content_payload_key", [Qdrant.CONTENT_KEY, "foo"]) +@pytest.mark.parametrize("metadata_payload_key", [Qdrant.METADATA_KEY, "bar"]) +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +def test_qdrant_similarity_search_by_vector( + batch_size: int, + content_payload_key: str, + metadata_payload_key: str, + vector_name: Optional[str], +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + location=":memory:", + content_payload_key=content_payload_key, + metadata_payload_key=metadata_payload_key, + batch_size=batch_size, + vector_name=vector_name, + ) + embeddings = ConsistentFakeEmbeddings().embed_query("foo") + output = docsearch.similarity_search_by_vector(embeddings, k=1) + assert_documents_equals(output, [Document(page_content="foo")]) + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("content_payload_key", [Qdrant.CONTENT_KEY, "foo"]) +@pytest.mark.parametrize("metadata_payload_key", [Qdrant.METADATA_KEY, "bar"]) +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +def test_qdrant_similarity_search_with_score_by_vector( + batch_size: int, + content_payload_key: str, + metadata_payload_key: str, + vector_name: Optional[str], +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + location=":memory:", + content_payload_key=content_payload_key, + metadata_payload_key=metadata_payload_key, + batch_size=batch_size, + vector_name=vector_name, + ) + embeddings = ConsistentFakeEmbeddings().embed_query("foo") + output = docsearch.similarity_search_with_score_by_vector(embeddings, k=1) + assert len(output) == 1 + document, score = output[0] + assert_documents_equals(actual=[document], expected=[Document(page_content="foo")]) + assert score >= 0 + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +def test_qdrant_similarity_search_filters( + batch_size: int, vector_name: Optional[str] +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + metadatas = [ + {"page": i, "metadata": {"page": i + 1, "pages": [i + 2, -1]}} + for i in range(len(texts)) + ] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + metadatas=metadatas, + location=":memory:", + batch_size=batch_size, + vector_name=vector_name, + ) + + output = docsearch.similarity_search( + "foo", k=1, filter={"page": 1, "metadata": {"page": 2, "pages": [3]}} + ) + + assert_documents_equals( + actual=output, + expected=[ + Document( + page_content="bar", + metadata={"page": 1, "metadata": {"page": 2, "pages": [3, -1]}}, + ) + ], + ) + + +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +def test_qdrant_similarity_search_with_relevance_score_no_threshold( + vector_name: Optional[str], +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + metadatas = [ + {"page": i, "metadata": {"page": i + 1, "pages": [i + 2, -1]}} + for i in range(len(texts)) + ] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + metadatas=metadatas, + location=":memory:", + vector_name=vector_name, + ) + output = docsearch.similarity_search_with_relevance_scores( + "foo", k=3, score_threshold=None + ) + assert len(output) == 3 + for i in range(len(output)): + assert round(output[i][1], 2) >= 0 + assert round(output[i][1], 2) <= 1 + + +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +def test_qdrant_similarity_search_with_relevance_score_with_threshold( + vector_name: Optional[str], +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + metadatas = [ + {"page": i, "metadata": {"page": i + 1, "pages": [i + 2, -1]}} + for i in range(len(texts)) + ] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + metadatas=metadatas, + location=":memory:", + vector_name=vector_name, + ) + + score_threshold = 0.98 + kwargs = {"score_threshold": score_threshold} + output = docsearch.similarity_search_with_relevance_scores("foo", k=3, **kwargs) + assert len(output) == 1 + assert all([score >= score_threshold for _, score in output]) + + +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +def test_qdrant_similarity_search_with_relevance_score_with_threshold_and_filter( + vector_name: Optional[str], +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + metadatas = [ + {"page": i, "metadata": {"page": i + 1, "pages": [i + 2, -1]}} + for i in range(len(texts)) + ] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + metadatas=metadatas, + location=":memory:", + vector_name=vector_name, + ) + score_threshold = 0.99 # for almost exact match + # test negative filter condition + negative_filter = {"page": 1, "metadata": {"page": 2, "pages": [3]}} + kwargs = {"filter": negative_filter, "score_threshold": score_threshold} + output = docsearch.similarity_search_with_relevance_scores("foo", k=3, **kwargs) + assert len(output) == 0 + # test positive filter condition + positive_filter = {"page": 0, "metadata": {"page": 1, "pages": [2]}} + kwargs = {"filter": positive_filter, "score_threshold": score_threshold} + output = docsearch.similarity_search_with_relevance_scores("foo", k=3, **kwargs) + assert len(output) == 1 + assert all([score >= score_threshold for _, score in output]) + + +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +def test_qdrant_similarity_search_filters_with_qdrant_filters( + vector_name: Optional[str], +) -> None: + """Test end to end construction and search.""" + from qdrant_client.http import models as rest + + texts = ["foo", "bar", "baz"] + metadatas = [ + {"page": i, "details": {"page": i + 1, "pages": [i + 2, -1]}} + for i in range(len(texts)) + ] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + metadatas=metadatas, + location=":memory:", + vector_name=vector_name, + ) + + qdrant_filter = rest.Filter( + must=[ + rest.FieldCondition( + key="metadata.page", + match=rest.MatchValue(value=1), + ), + rest.FieldCondition( + key="metadata.details.page", + match=rest.MatchValue(value=2), + ), + rest.FieldCondition( + key="metadata.details.pages", + match=rest.MatchAny(any=[3]), + ), + ] + ) + output = docsearch.similarity_search("foo", k=1, filter=qdrant_filter) + assert_documents_equals( + actual=output, + expected=[ + Document( + page_content="bar", + metadata={"page": 1, "details": {"page": 2, "pages": [3, -1]}}, + ) + ], + ) + + +@pytest.mark.parametrize("batch_size", [1, 64]) +@pytest.mark.parametrize("content_payload_key", [Qdrant.CONTENT_KEY, "foo"]) +@pytest.mark.parametrize("metadata_payload_key", [Qdrant.METADATA_KEY, "bar"]) +@pytest.mark.parametrize("vector_name", [None, "my-vector"]) +def test_qdrant_similarity_search_with_relevance_scores( + batch_size: int, + content_payload_key: str, + metadata_payload_key: str, + vector_name: Optional[str], +) -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + docsearch = Qdrant.from_texts( + texts, + ConsistentFakeEmbeddings(), + location=":memory:", + content_payload_key=content_payload_key, + metadata_payload_key=metadata_payload_key, + batch_size=batch_size, + vector_name=vector_name, + ) + output = docsearch.similarity_search_with_relevance_scores("foo", k=3) + + assert all( + (1 >= score or np.isclose(score, 1)) and score >= 0 for _, score in output + ) diff --git a/libs/partners/qdrant/tests/unit_tests/__init__.py b/libs/partners/qdrant/tests/unit_tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libs/partners/qdrant/tests/unit_tests/test_imports.py b/libs/partners/qdrant/tests/unit_tests/test_imports.py new file mode 100644 index 00000000000..6e82a2b1a28 --- /dev/null +++ b/libs/partners/qdrant/tests/unit_tests/test_imports.py @@ -0,0 +1,7 @@ +from langchain_qdrant import __all__ + +EXPECTED_ALL = ["Qdrant"] + + +def test_all_imports() -> None: + assert sorted(EXPECTED_ALL) == sorted(__all__) diff --git a/libs/partners/qdrant/tests/unit_tests/test_vectorstores.py b/libs/partners/qdrant/tests/unit_tests/test_vectorstores.py new file mode 100644 index 00000000000..e69de29bb2d