diff --git a/nemoguardrails/embeddings/basic.py b/nemoguardrails/embeddings/basic.py index ad3109e8f8..56929bcf42 100644 --- a/nemoguardrails/embeddings/basic.py +++ b/nemoguardrails/embeddings/basic.py @@ -17,8 +17,6 @@ import logging from typing import Any, Dict, List, Optional, Union -from annoy import AnnoyIndex - from nemoguardrails.embeddings.cache import cache_embeddings from nemoguardrails.embeddings.index import EmbeddingsIndex, IndexItem from nemoguardrails.embeddings.providers import EmbeddingModel, init_embedding_model @@ -26,17 +24,36 @@ log = logging.getLogger(__name__) +try: + from annoy import AnnoyIndex +except ImportError: + AnnoyIndex = None + import sys + + if sys.version_info >= (3, 13): + log.warning( + "annoy is not supported on Python 3.13+ (SIGILL in the C++ extension); " + "using numpy-based nearest-neighbour search instead." + ) + else: + log.warning( + "annoy is not installed; falling back to numpy-based nearest-neighbour " + "search. Install annoy (or use the [annoy] extra) for faster index " + "lookups on large knowledge bases." + ) + class BasicEmbeddingsIndex(EmbeddingsIndex): """Basic implementation of an embeddings index. It uses the `sentence-transformers/all-MiniLM-L6-v2` model to compute embeddings. - Annoy is employed for efficient nearest-neighbor search. + Annoy is employed for efficient nearest-neighbor search when available; + otherwise a numpy-based brute-force fallback is used. Attributes: embedding_model (str): The model for computing embeddings. embedding_engine (str): The engine for computing embeddings. - index (AnnoyIndex): The current embedding index. + index: The current embedding index (AnnoyIndex or NumpyAnnoyIndex). embedding_size (int): The size of the embeddings. cache_config (EmbeddingsCacheConfig): The cache configuration. embeddings (List[List[float]]): The computed embeddings. @@ -48,7 +65,6 @@ class BasicEmbeddingsIndex(EmbeddingsIndex): embedding_model: str embedding_engine: str embedding_params: Dict[str, Any] - index: AnnoyIndex embedding_size: int cache_config: EmbeddingsCacheConfig embeddings: List[List[float]] @@ -189,8 +205,17 @@ async def add_items(self, items: List[IndexItem]): self._embedding_size = len(self._embeddings[0]) async def build(self): - """Builds the Annoy index.""" - self._index = AnnoyIndex(len(self._embeddings[0]), "angular") + """Builds the embeddings index. + + Uses Annoy when available, otherwise falls back to a numpy-based + brute-force index (sufficient for typical guardrails index sizes). + """ + if AnnoyIndex is not None: + self._index = AnnoyIndex(len(self._embeddings[0]), "angular") + else: + from nemoguardrails.embeddings.numpy_index import NumpyAnnoyIndex + + self._index = NumpyAnnoyIndex(len(self._embeddings[0]), "angular") for i in range(len(self._embeddings)): self._index.add_item(i, self._embeddings[i]) self._index.build(10) diff --git a/nemoguardrails/embeddings/numpy_index.py b/nemoguardrails/embeddings/numpy_index.py new file mode 100644 index 0000000000..84aa74b854 --- /dev/null +++ b/nemoguardrails/embeddings/numpy_index.py @@ -0,0 +1,164 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Numpy-based drop-in replacement for annoy.AnnoyIndex. + +This module provides a pure-numpy alternative to the Annoy library for +nearest-neighbour search over embedding vectors. It is used as a fallback +when annoy is not installed (e.g. on Python 3.13+ where the annoy C++ +extension triggers a SIGILL). + +For the typical guardrails index sizes (tens to hundreds of items) the +brute-force cosine search is more than fast enough. +""" + +from typing import List, Optional, Tuple, Union + +import numpy as np + + +class NumpyAnnoyIndex: + """A numpy-backed nearest-neighbour index that exposes the same API surface + as ``annoy.AnnoyIndex`` for the subset used by NeMo Guardrails. + + Supported operations: + * ``add_item(i, vector)`` + * ``build(n_trees)`` (no-op -- kept for interface compatibility) + * ``get_nns_by_vector(vector, n, include_distances=False)`` + * ``save(path)`` / ``load(path)`` + + The metric is *angular* distance, matching Annoy's default for text + embeddings. Angular distance is defined as + ``sqrt(2 * (1 - cos_sim))`` so that it is ``0`` for identical vectors + and ``2`` for diametrically opposed ones. + """ + + def __init__(self, embedding_size: int, metric: str = "angular"): + if metric != "angular": + raise ValueError( + f"NumpyAnnoyIndex only supports metric='angular', got {metric!r}" + ) + self._embedding_size = embedding_size + self._metric = metric + # Sparse storage during build phase (id -> vector) + self._vectors_dict: dict = {} + # Dense numpy matrix after build() + self._vectors: Optional[np.ndarray] = None + self._built = False + + # ------------------------------------------------------------------ + # Build interface + # ------------------------------------------------------------------ + + def add_item(self, i: int, vector) -> None: + """Add a single vector with integer id *i*.""" + self._vectors_dict[i] = np.asarray(vector, dtype=np.float32) + + def build(self, n_trees: int = 10) -> None: + """Finalise the index. The *n_trees* parameter is ignored (kept + for API compatibility with Annoy).""" + if not self._vectors_dict: + self._vectors = np.empty((0, self._embedding_size), dtype=np.float32) + else: + max_id = max(self._vectors_dict.keys()) + self._vectors = np.zeros( + (max_id + 1, self._embedding_size), dtype=np.float32 + ) + for idx, vec in self._vectors_dict.items(): + self._vectors[idx] = vec + self._vectors_dict = {} # release per-item dict memory now stored in _vectors + self._built = True + + # ------------------------------------------------------------------ + # Query interface + # ------------------------------------------------------------------ + + def get_nns_by_vector( + self, vector, n: int, include_distances: bool = False + ) -> Union[List[int], Tuple[List[int], List[float]]]: + """Return the *n* nearest neighbours of *vector*. + + When *include_distances* is ``True`` the return value is a tuple + ``(ids, distances)``; otherwise just ``ids``. + """ + if self._vectors is None or len(self._vectors) == 0: + return ([], []) if include_distances else [] + + query = np.asarray(vector, dtype=np.float32) + + # Cosine similarity via normalised dot product + norms = np.linalg.norm(self._vectors, axis=1, keepdims=True) + # Avoid division by zero for zero-vectors + safe_norms = np.where(norms == 0, 1.0, norms) + normed = self._vectors / safe_norms + + query_norm = np.linalg.norm(query) + if query_norm == 0: + query_normed = query + else: + query_normed = query / query_norm + + cos_sim = normed @ query_normed # shape: (num_items,) + + # Angular distance (matches Annoy's definition) + cos_sim_clipped = np.clip(cos_sim, -1.0, 1.0) + distances = np.sqrt(2.0 * (1.0 - cos_sim_clipped)) + + # Get top-n indices (lowest distance first) + n = min(n, len(distances)) + if n == len(distances): + # All items requested -- just argsort the whole array + top_indices = np.argsort(distances)[:n] + else: + top_indices = np.argpartition(distances, n)[:n] + top_indices = top_indices[np.argsort(distances[top_indices])] + + ids = top_indices.tolist() + if include_distances: + return ids, distances[top_indices].tolist() + return ids + + # ------------------------------------------------------------------ + # Persistence + # ------------------------------------------------------------------ + + def save(self, path: str) -> None: + """Save the index to disk as a ``.npy`` file. + + If the caller supplies a path ending in ``.ann`` (the annoy + convention), we silently swap the extension to ``.npy`` so that + both backends can coexist in the same cache directory. + + Note: ``numpy.save`` automatically appends ``.npy`` when the + path does not already end with that suffix, so callers should + always pass either an ``.ann`` path (which is converted here) + or an explicit ``.npy`` path. + """ + if not self._built: + raise RuntimeError( + "NumpyAnnoyIndex.save() called before build(); call build() first." + ) + if path.endswith(".ann"): + path = path[:-4] + ".npy" + if self._vectors is not None: + np.save(path, self._vectors) + + def load(self, path: str) -> None: + """Load a previously saved index from disk.""" + if path.endswith(".ann"): + path = path[:-4] + ".npy" + self._vectors_dict = {} # discard any pre-build state + self._vectors = np.load(path).astype(np.float32) + self._built = True diff --git a/nemoguardrails/kb/kb.py b/nemoguardrails/kb/kb.py index 685f8a9a04..877a00ac56 100644 --- a/nemoguardrails/kb/kb.py +++ b/nemoguardrails/kb/kb.py @@ -125,17 +125,31 @@ async def build(self): cache_file = os.path.join(CACHE_FOLDER, f"{hash_value}.ann") embedding_size_file = os.path.join(CACHE_FOLDER, f"{hash_value}.esize") + # Determine which index backend to use + try: + from annoy import AnnoyIndex + + _annoy_available = True + except ImportError: + _annoy_available = False + + # When using the numpy fallback the cache file extension is .npy + # instead of .ann; check for both so that caches from either + # backend are honoured. + npy_cache_file = cache_file[:-4] + ".npy" + + has_ann_cache = os.path.exists(cache_file) and _annoy_available + has_npy_cache = os.path.exists(npy_cache_file) + # If we have already computed this before, we use it if ( self.config.embedding_search_provider.name == "default" - and os.path.exists(cache_file) + and (has_ann_cache or has_npy_cache) and os.path.exists(embedding_size_file) ): - from annoy import AnnoyIndex - from nemoguardrails.embeddings.basic import BasicEmbeddingsIndex - log.info(cache_file) + log.info(cache_file if has_ann_cache else npy_cache_file) self.index = cast( BasicEmbeddingsIndex, self._get_embeddings_search_instance( @@ -146,8 +160,19 @@ async def build(self): with open(embedding_size_file, "r") as f: embedding_size = int(f.read()) - ann_index = AnnoyIndex(embedding_size, "angular") - ann_index.load(cache_file) + if has_ann_cache: + ann_index = AnnoyIndex(embedding_size, "angular") + ann_index.load(cache_file) + else: + # NOTE: if annoy is installed but only a .npy cache exists + # (e.g. first run was on Python 3.13 without annoy, then the + # user installed annoy), we load via the numpy backend rather + # than regenerating an .ann cache. The cache will be rebuilt + # automatically the next time the KB content hash changes. + from nemoguardrails.embeddings.numpy_index import NumpyAnnoyIndex + + ann_index = NumpyAnnoyIndex(embedding_size, "angular") + ann_index.load(npy_cache_file) self.index.embeddings_index = ann_index @@ -159,8 +184,9 @@ async def build(self): await self.index.add_items(index_items) await self.index.build() - # For the default Embedding Search provider, which uses annoy, we also - # persist the index after it's computed. + # For the default Embedding Search provider, which uses annoy + # (or the numpy fallback), we also persist the index after + # it is computed. if self.config.embedding_search_provider.name == "default": from nemoguardrails.embeddings.basic import BasicEmbeddingsIndex diff --git a/poetry.lock b/poetry.lock index b5eedf3d04..8ad5826dc2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -228,7 +228,7 @@ files = [ name = "annoy" version = "1.17.3" description = "Approximate Nearest Neighbors in C++/Python optimized for memory usage and loading/saving to disk." -optional = false +optional = true python-versions = "*" files = [ {file = "annoy-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c33a5d4d344c136c84976bfb2825760142a8bb25335165e24e11c9afbfa8c2e9"}, @@ -1412,6 +1412,8 @@ files = [ {file = "greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d"}, {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5"}, {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f"}, + {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7"}, + {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8"}, {file = "greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c"}, {file = "greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2"}, {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246"}, @@ -1421,6 +1423,8 @@ files = [ {file = "greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8"}, {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52"}, {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa"}, + {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c"}, + {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5"}, {file = "greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9"}, {file = "greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd"}, {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb"}, @@ -1430,6 +1434,8 @@ files = [ {file = "greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0"}, {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0"}, {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f"}, + {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0"}, + {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d"}, {file = "greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02"}, {file = "greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31"}, {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945"}, @@ -1439,6 +1445,8 @@ files = [ {file = "greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671"}, {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b"}, {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae"}, + {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b"}, + {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929"}, {file = "greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b"}, {file = "greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0"}, {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f"}, @@ -1446,6 +1454,8 @@ files = [ {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1"}, {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735"}, {file = "greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337"}, + {file = "greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269"}, + {file = "greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681"}, {file = "greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01"}, {file = "greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c"}, {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d"}, @@ -1455,6 +1465,8 @@ files = [ {file = "greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df"}, {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594"}, {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98"}, + {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:28a3c6b7cd72a96f61b0e4b2a36f681025b60ae4779cc73c1535eb5f29560b10"}, + {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52206cd642670b0b320a1fd1cbfd95bca0e043179c1d8a045f2c6109dfe973be"}, {file = "greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b"}, {file = "greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb"}, {file = "greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d"}, @@ -6456,7 +6468,8 @@ files = [ cffi = ["cffi (>=1.17)"] [extras] -all = ["aiofiles", "google-cloud-language", "langchain-nvidia-ai-endpoints", "langchain-openai", "numpy", "numpy", "numpy", "numpy", "opentelemetry-api", "presidio-analyzer", "presidio-anonymizer", "streamlit", "tqdm", "yara-python"] +all = ["aiofiles", "annoy", "google-cloud-language", "langchain-nvidia-ai-endpoints", "langchain-openai", "numpy", "numpy", "numpy", "numpy", "opentelemetry-api", "presidio-analyzer", "presidio-anonymizer", "streamlit", "tqdm", "yara-python"] +annoy = ["annoy"] eval = ["numpy", "numpy", "numpy", "numpy", "streamlit", "tornado", "tqdm"] gcp = ["google-cloud-language"] jailbreak = ["yara-python"] @@ -6468,4 +6481,4 @@ tracing = ["aiofiles", "opentelemetry-api"] [metadata] lock-version = "2.0" python-versions = ">=3.9,!=3.9.7,<3.14" -content-hash = "313705d475a9cb177efa633c193da9315388aa99832b9c5b429fafb5b3da44b0" +content-hash = "d8d4998c05a6d085ad6bbc64035fe80d05fc387e85102df47315d0e037438f6e" diff --git a/pyproject.toml b/pyproject.toml index a2d094636c..984ba72503 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ nemoguardrails = "nemoguardrails.__main__:app" [tool.poetry.dependencies] python = ">=3.9,!=3.9.7,<3.14" aiohttp = ">=3.10.11" -annoy = ">=1.17.3" +annoy = { version = ">=1.17.3", optional = true, python = "<3.13" } fastapi = ">=0.103.0," fastembed = [{ version = ">=0.2.2, <=0.6.0", python = ">=3.9,<3.14" }] httpx = ">=0.24.1" @@ -113,11 +113,13 @@ openai = ["langchain-openai"] gcp = ["google-cloud-language"] tracing = ["opentelemetry-api", "aiofiles"] nvidia = ["langchain-nvidia-ai-endpoints"] +annoy = ["annoy"] jailbreak = ["yara-python"] # Poetry does not support recursive dependencies, so we need to add all the dependencies here. # I also support their decision. There is no PEP for recursive dependencies, but it has been supported in pip since version 21.2. # It is here for backward compatibility. all = [ + "annoy", "presidio-analyzer", "presidio-anonymizer", "tqdm", diff --git a/tox.ini b/tox.ini index 7516ff6933..986eb6283d 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ # > pyenv local 3.9 3.10 3.11 [tox] -envlist = py39, py310, py311, py312 +envlist = py39, py310, py311, py312, py313 [testenv] description = Run tests with pytest under different Python versions using Poetry