From e7f2f78957af6e5f462bb804ee852e7c27fb718e Mon Sep 17 00:00:00 2001 From: Maxime Grenu Date: Wed, 11 Mar 2026 22:34:16 +0100 Subject: [PATCH 01/13] feat: add Python 3.14 support with optional annoy dependency Make annoy an optional dependency and provide a numpy-based fallback for the embeddings index. This resolves the SIGILL crash on Python 3.13+ caused by annoy's C++ extension. Changes: - New NumpyAnnoyIndex class as a drop-in replacement for AnnoyIndex - Conditional annoy import in basic.py and kb.py with automatic fallback - Python version upper bound raised from <3.14 to <3.15 - annoy moved from required to optional dependencies (python <3.13) - CI matrices updated to include Python 3.14 - tox envlist updated with py313 and py314 Signed-off-by: Maxime Kawawa-Beaudan Signed-off-by: Maxime Grenu --- .github/workflows/full-tests.yml | 2 +- .github/workflows/latest-deps-tests.yml | 2 +- .github/workflows/pr-tests.yml | 2 +- nemoguardrails/embeddings/basic.py | 30 +++-- nemoguardrails/embeddings/numpy_index.py | 145 +++++++++++++++++++++++ nemoguardrails/kb/kb.py | 35 ++++-- poetry.lock | 21 +++- pyproject.toml | 13 +- tox.ini | 2 +- 9 files changed, 225 insertions(+), 27 deletions(-) create mode 100644 nemoguardrails/embeddings/numpy_index.py diff --git a/.github/workflows/full-tests.yml b/.github/workflows/full-tests.yml index 56375c392d..20abc87d49 100644 --- a/.github/workflows/full-tests.yml +++ b/.github/workflows/full-tests.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: os: [Windows, macOS] # exclude Ubuntu as it is available in pr-tests - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] include: - os: Windows image: windows-2022 diff --git a/.github/workflows/latest-deps-tests.yml b/.github/workflows/latest-deps-tests.yml index 059fbb327e..0deed44c81 100644 --- a/.github/workflows/latest-deps-tests.yml +++ b/.github/workflows/latest-deps-tests.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [Ubuntu] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] include: - os: Ubuntu image: ubuntu-latest diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index ab13c81dec..15b95e9348 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: os: [Ubuntu] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] include: - os: Ubuntu image: ubuntu-latest diff --git a/nemoguardrails/embeddings/basic.py b/nemoguardrails/embeddings/basic.py index ad3109e8f8..4ed0d57b77 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,27 @@ log = logging.getLogger(__name__) +try: + from annoy import AnnoyIndex +except ImportError: + AnnoyIndex = None + log.info( + "annoy is not installed; falling back to numpy-based nearest-neighbour " + "search. Install annoy 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 +56,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 +196,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..a6716f2868 --- /dev/null +++ b/nemoguardrails/embeddings/numpy_index.py @@ -0,0 +1,145 @@ +# 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 + +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"): + 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._built = True + + # ------------------------------------------------------------------ + # Query interface + # ------------------------------------------------------------------ + + def get_nns_by_vector( + self, vector, n: int, include_distances: bool = False + ) -> Tuple[List[int], ...]: + """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)) + 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. + """ + 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 = np.load(path).astype(np.float32) + self._built = True diff --git a/nemoguardrails/kb/kb.py b/nemoguardrails/kb/kb.py index 685f8a9a04..32114faf88 100644 --- a/nemoguardrails/kb/kb.py +++ b/nemoguardrails/kb/kb.py @@ -125,14 +125,28 @@ 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" if cache_file.endswith(".ann") else cache_file + ".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) @@ -146,8 +160,14 @@ 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 and _annoy_available: + ann_index = AnnoyIndex(embedding_size, "angular") + ann_index.load(cache_file) + else: + 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 +179,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..c3f045be49 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"] @@ -6467,5 +6480,5 @@ tracing = ["aiofiles", "opentelemetry-api"] [metadata] lock-version = "2.0" -python-versions = ">=3.9,!=3.9.7,<3.14" -content-hash = "313705d475a9cb177efa633c193da9315388aa99832b9c5b429fafb5b3da44b0" +python-versions = ">=3.9,!=3.9.7,<3.15" +content-hash = "6f7b9232f42b82cf349282c15802333a101dc6e3cd9cf6ee90f8f188e755815b" diff --git a/pyproject.toml b/pyproject.toml index a2d094636c..595ebda548 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] [tool.poetry.urls] @@ -46,11 +47,11 @@ repository = "https://github.com/NVIDIA/NeMo-Guardrails" nemoguardrails = "nemoguardrails.__main__:app" [tool.poetry.dependencies] -python = ">=3.9,!=3.9.7,<3.14" +python = ">=3.9,!=3.9.7,<3.15" 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" }] +fastembed = [{ version = ">=0.2.2, <=0.6.0", python = ">=3.9,<3.15" }] httpx = ">=0.24.1" jinja2 = ">=3.1.6" langchain = ">=0.2.14,<0.4.0" @@ -82,8 +83,8 @@ langchain-openai = { version = ">=0.1.0", optional = true } # eval tqdm = { version = ">=4.65,<5.0", optional = true } -streamlit = { version = "^1.37.0", optional = true, python = ">=3.9,!=3.9.7,<3.14" } -tornado = { version = ">=6.5.0", optional = true, python = ">=3.9,!=3.9.7,<3.14" } +streamlit = { version = "^1.37.0", optional = true, python = ">=3.9,!=3.9.7,<3.15" } +tornado = { version = ">=6.5.0", optional = true, python = ">=3.9,!=3.9.7,<3.15" } pandas = { version = ">=1.4.0,<3", optional = true } numpy = [ { version = ">=1.21", python = ">=3.10,<3.12" }, @@ -113,11 +114,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..4133f266fb 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, py314 [testenv] description = Run tests with pytest under different Python versions using Poetry From e36a1736baa0d39dcf65fd172c2e5f5f908485f7 Mon Sep 17 00:00:00 2001 From: Maxime Grenu Date: Wed, 11 Mar 2026 22:44:11 +0100 Subject: [PATCH 02/13] fix: handle edge case when n >= item count in NumpyAnnoyIndex When max_results equals or exceeds the number of indexed items, np.argpartition raises ValueError because kth must be strictly less than the array length. Fall back to a full argsort in that case. Signed-off-by: Maxime Kawawa-Beaudan Signed-off-by: Maxime Grenu --- nemoguardrails/embeddings/numpy_index.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nemoguardrails/embeddings/numpy_index.py b/nemoguardrails/embeddings/numpy_index.py index a6716f2868..d5e6bd0e42 100644 --- a/nemoguardrails/embeddings/numpy_index.py +++ b/nemoguardrails/embeddings/numpy_index.py @@ -113,8 +113,12 @@ def get_nns_by_vector( # Get top-n indices (lowest distance first) n = min(n, len(distances)) - top_indices = np.argpartition(distances, n)[:n] - top_indices = top_indices[np.argsort(distances[top_indices])] + 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: From ae08b337a09f69f7a5809286c9c1381a87d7fa3d Mon Sep 17 00:00:00 2001 From: Maxime Grenu Date: Wed, 11 Mar 2026 22:53:07 +0100 Subject: [PATCH 03/13] fix: scope to Python 3.13 support (3.14 blocked by langchain) Python 3.14 is currently blocked by langchain 0.3.x which does not yet support it (pydantic type evaluation failures). Scope the version constraint to <3.14 and remove 3.14 from CI matrix. The numpy-based annoy fallback is ready for when langchain adds 3.14 support. Signed-off-by: Maxime Kawawa-Beaudan Signed-off-by: Maxime Grenu --- .github/workflows/full-tests.yml | 2 +- .github/workflows/latest-deps-tests.yml | 2 +- .github/workflows/pr-tests.yml | 2 +- poetry.lock | 4 ++-- pyproject.toml | 9 ++++----- tox.ini | 2 +- 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/full-tests.yml b/.github/workflows/full-tests.yml index 20abc87d49..56375c392d 100644 --- a/.github/workflows/full-tests.yml +++ b/.github/workflows/full-tests.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: os: [Windows, macOS] # exclude Ubuntu as it is available in pr-tests - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] include: - os: Windows image: windows-2022 diff --git a/.github/workflows/latest-deps-tests.yml b/.github/workflows/latest-deps-tests.yml index 0deed44c81..059fbb327e 100644 --- a/.github/workflows/latest-deps-tests.yml +++ b/.github/workflows/latest-deps-tests.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [Ubuntu] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] include: - os: Ubuntu image: ubuntu-latest diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index 15b95e9348..ab13c81dec 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: os: [Ubuntu] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] include: - os: Ubuntu image: ubuntu-latest diff --git a/poetry.lock b/poetry.lock index c3f045be49..8ad5826dc2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6480,5 +6480,5 @@ tracing = ["aiofiles", "opentelemetry-api"] [metadata] lock-version = "2.0" -python-versions = ">=3.9,!=3.9.7,<3.15" -content-hash = "6f7b9232f42b82cf349282c15802333a101dc6e3cd9cf6ee90f8f188e755815b" +python-versions = ">=3.9,!=3.9.7,<3.14" +content-hash = "d8d4998c05a6d085ad6bbc64035fe80d05fc387e85102df47315d0e037438f6e" diff --git a/pyproject.toml b/pyproject.toml index 595ebda548..984ba72503 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,6 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", ] [tool.poetry.urls] @@ -47,11 +46,11 @@ repository = "https://github.com/NVIDIA/NeMo-Guardrails" nemoguardrails = "nemoguardrails.__main__:app" [tool.poetry.dependencies] -python = ">=3.9,!=3.9.7,<3.15" +python = ">=3.9,!=3.9.7,<3.14" aiohttp = ">=3.10.11" 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.15" }] +fastembed = [{ version = ">=0.2.2, <=0.6.0", python = ">=3.9,<3.14" }] httpx = ">=0.24.1" jinja2 = ">=3.1.6" langchain = ">=0.2.14,<0.4.0" @@ -83,8 +82,8 @@ langchain-openai = { version = ">=0.1.0", optional = true } # eval tqdm = { version = ">=4.65,<5.0", optional = true } -streamlit = { version = "^1.37.0", optional = true, python = ">=3.9,!=3.9.7,<3.15" } -tornado = { version = ">=6.5.0", optional = true, python = ">=3.9,!=3.9.7,<3.15" } +streamlit = { version = "^1.37.0", optional = true, python = ">=3.9,!=3.9.7,<3.14" } +tornado = { version = ">=6.5.0", optional = true, python = ">=3.9,!=3.9.7,<3.14" } pandas = { version = ">=1.4.0,<3", optional = true } numpy = [ { version = ">=1.21", python = ">=3.10,<3.12" }, diff --git a/tox.ini b/tox.ini index 4133f266fb..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, py313, py314 +envlist = py39, py310, py311, py312, py313 [testenv] description = Run tests with pytest under different Python versions using Poetry From 636874d8c56d64b083062266168e814f14a7a627 Mon Sep 17 00:00:00 2001 From: Maxime Grenu Date: Thu, 12 Mar 2026 07:20:20 +0100 Subject: [PATCH 04/13] style: fix black formatting in kb.py Signed-off-by: Maxime Kawawa-Beaudan Signed-off-by: Maxime Grenu --- nemoguardrails/kb/kb.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nemoguardrails/kb/kb.py b/nemoguardrails/kb/kb.py index 32114faf88..1749577281 100644 --- a/nemoguardrails/kb/kb.py +++ b/nemoguardrails/kb/kb.py @@ -136,7 +136,11 @@ async def build(self): # 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" if cache_file.endswith(".ann") else cache_file + ".npy" + npy_cache_file = ( + cache_file[:-4] + ".npy" + if cache_file.endswith(".ann") + else cache_file + ".npy" + ) has_ann_cache = os.path.exists(cache_file) and _annoy_available has_npy_cache = os.path.exists(npy_cache_file) From c4c6e9b1ec5dda6f0445ed6dd26faef9fa81ddd3 Mon Sep 17 00:00:00 2001 From: Maxime Grenu Date: Thu, 12 Mar 2026 13:45:45 +0100 Subject: [PATCH 05/13] Address review feedback: promote log level and document edge cases - Upgrade annoy fallback notice from log.info to log.warning so the message is visible before logging handlers are configured. - Document the mixed-backend cache edge case in kb.py (annoy installed but only .npy cache exists). - Add docstring note about numpy.save auto-appending .npy extension. Signed-off-by: Maxime Kawawa-Beaudan Signed-off-by: Maxime Grenu --- nemoguardrails/embeddings/basic.py | 2 +- nemoguardrails/embeddings/numpy_index.py | 5 +++++ nemoguardrails/kb/kb.py | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/nemoguardrails/embeddings/basic.py b/nemoguardrails/embeddings/basic.py index 4ed0d57b77..d2c88084d6 100644 --- a/nemoguardrails/embeddings/basic.py +++ b/nemoguardrails/embeddings/basic.py @@ -28,7 +28,7 @@ from annoy import AnnoyIndex except ImportError: AnnoyIndex = None - log.info( + log.warning( "annoy is not installed; falling back to numpy-based nearest-neighbour " "search. Install annoy for faster index lookups on large knowledge bases." ) diff --git a/nemoguardrails/embeddings/numpy_index.py b/nemoguardrails/embeddings/numpy_index.py index d5e6bd0e42..8adf086298 100644 --- a/nemoguardrails/embeddings/numpy_index.py +++ b/nemoguardrails/embeddings/numpy_index.py @@ -135,6 +135,11 @@ def save(self, path: str) -> None: 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 path.endswith(".ann"): path = path[:-4] + ".npy" diff --git a/nemoguardrails/kb/kb.py b/nemoguardrails/kb/kb.py index 1749577281..9ff284d7f1 100644 --- a/nemoguardrails/kb/kb.py +++ b/nemoguardrails/kb/kb.py @@ -168,6 +168,11 @@ async def build(self): 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") From 86d9aed92949f1dd9e5ffe663f7d8abdeff8f8d3 Mon Sep 17 00:00:00 2001 From: Maxime Grenu <69890511+cluster2600@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:06:59 +0100 Subject: [PATCH 06/13] Update nemoguardrails/kb/kb.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Signed-off-by: Maxime Grenu <69890511+cluster2600@users.noreply.github.com> --- nemoguardrails/kb/kb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemoguardrails/kb/kb.py b/nemoguardrails/kb/kb.py index 9ff284d7f1..07d0eff111 100644 --- a/nemoguardrails/kb/kb.py +++ b/nemoguardrails/kb/kb.py @@ -153,7 +153,7 @@ async def build(self): ): 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( From 7db842b11a9a1d4a586b491290772af8b5c55d16 Mon Sep 17 00:00:00 2001 From: Maxime Grenu Date: Thu, 12 Mar 2026 17:05:01 +0100 Subject: [PATCH 07/13] Address second round of review feedback - Differentiate the annoy fallback warning by Python version: on 3.13+ explain that annoy is unsupported (SIGILL), on older versions suggest installing the [annoy] extra. - Log the actual cache path being loaded (.ann vs .npy) instead of always logging the .ann path. - Reset _vectors_dict in load() to discard stale pre-build state. Signed-off-by: Maxime Kawawa-Beaudan Signed-off-by: Maxime Grenu --- nemoguardrails/embeddings/basic.py | 17 +++++++++++++---- nemoguardrails/embeddings/numpy_index.py | 1 + 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/nemoguardrails/embeddings/basic.py b/nemoguardrails/embeddings/basic.py index d2c88084d6..56929bcf42 100644 --- a/nemoguardrails/embeddings/basic.py +++ b/nemoguardrails/embeddings/basic.py @@ -28,10 +28,19 @@ from annoy import AnnoyIndex except ImportError: AnnoyIndex = None - log.warning( - "annoy is not installed; falling back to numpy-based nearest-neighbour " - "search. Install annoy for faster index lookups on large knowledge bases." - ) + 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): diff --git a/nemoguardrails/embeddings/numpy_index.py b/nemoguardrails/embeddings/numpy_index.py index 8adf086298..28be1c1bd8 100644 --- a/nemoguardrails/embeddings/numpy_index.py +++ b/nemoguardrails/embeddings/numpy_index.py @@ -150,5 +150,6 @@ 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 From 2434ec0216e3ddb3cea60ef6540f5896c81725bf Mon Sep 17 00:00:00 2001 From: Maxime Grenu Date: Thu, 12 Mar 2026 17:40:53 +0100 Subject: [PATCH 08/13] Fix return type annotation and guard save() against unbuilt index - Correct get_nns_by_vector return type from Tuple[List[int], ...] to Union[List[int], Tuple[List[int], List[float]]] to match actual return paths. - Raise RuntimeError in save() when called before build() to surface misuse early instead of silently writing nothing. Signed-off-by: Maxime Kawawa-Beaudan Signed-off-by: Maxime Grenu --- nemoguardrails/embeddings/numpy_index.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nemoguardrails/embeddings/numpy_index.py b/nemoguardrails/embeddings/numpy_index.py index 28be1c1bd8..dd2857cd22 100644 --- a/nemoguardrails/embeddings/numpy_index.py +++ b/nemoguardrails/embeddings/numpy_index.py @@ -24,7 +24,7 @@ brute-force cosine search is more than fast enough. """ -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union import numpy as np @@ -82,7 +82,7 @@ def build(self, n_trees: int = 10) -> None: def get_nns_by_vector( self, vector, n: int, include_distances: bool = False - ) -> Tuple[List[int], ...]: + ) -> 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 @@ -141,6 +141,10 @@ def save(self, path: str) -> None: 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: From c315832b56e868c674db3a791b9b7509900d060a Mon Sep 17 00:00:00 2001 From: Maxime Grenu <69890511+cluster2600@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:50:14 +0100 Subject: [PATCH 09/13] Update nemoguardrails/kb/kb.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Signed-off-by: Maxime Grenu <69890511+cluster2600@users.noreply.github.com> --- nemoguardrails/kb/kb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nemoguardrails/kb/kb.py b/nemoguardrails/kb/kb.py index 07d0eff111..d446ba790d 100644 --- a/nemoguardrails/kb/kb.py +++ b/nemoguardrails/kb/kb.py @@ -164,7 +164,7 @@ async def build(self): with open(embedding_size_file, "r") as f: embedding_size = int(f.read()) - if has_ann_cache and _annoy_available: + if has_ann_cache: ann_index = AnnoyIndex(embedding_size, "angular") ann_index.load(cache_file) else: From 3a29d1927ca5401669b98664d1654164ceb109fb Mon Sep 17 00:00:00 2001 From: Maxime Grenu Date: Thu, 12 Mar 2026 17:56:47 +0100 Subject: [PATCH 10/13] Tighten NumpyAnnoyIndex defensiveness and clean up kb.py - Validate metric parameter in __init__; raise ValueError for unsupported metrics instead of silently returning angular distances. - Clear _vectors_dict after build() to free duplicated memory. - Remove redundant _annoy_available check in kb.py cache branch (already encoded in has_ann_cache). Signed-off-by: Maxime Kawawa-Beaudan Signed-off-by: Maxime Grenu --- nemoguardrails/embeddings/numpy_index.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nemoguardrails/embeddings/numpy_index.py b/nemoguardrails/embeddings/numpy_index.py index dd2857cd22..84aa74b854 100644 --- a/nemoguardrails/embeddings/numpy_index.py +++ b/nemoguardrails/embeddings/numpy_index.py @@ -46,6 +46,10 @@ class NumpyAnnoyIndex: """ 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) @@ -74,6 +78,7 @@ def build(self, n_trees: int = 10) -> None: ) 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 # ------------------------------------------------------------------ From 15f9a70f7b58afce8f3393643d56277aad44df0e Mon Sep 17 00:00:00 2001 From: Maxime Grenu <69890511+cluster2600@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:59:07 +0100 Subject: [PATCH 11/13] Update nemoguardrails/embeddings/numpy_index.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Signed-off-by: Maxime Grenu <69890511+cluster2600@users.noreply.github.com> --- nemoguardrails/embeddings/numpy_index.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nemoguardrails/embeddings/numpy_index.py b/nemoguardrails/embeddings/numpy_index.py index 84aa74b854..0c379ea404 100644 --- a/nemoguardrails/embeddings/numpy_index.py +++ b/nemoguardrails/embeddings/numpy_index.py @@ -66,6 +66,7 @@ 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: def build(self, n_trees: int = 10) -> None: """Finalise the index. The *n_trees* parameter is ignored (kept for API compatibility with Annoy).""" From 9684e21d197ab548dc4f2c44d3d0c480f8448217 Mon Sep 17 00:00:00 2001 From: Maxime Grenu <69890511+cluster2600@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:06:35 +0100 Subject: [PATCH 12/13] Update nemoguardrails/kb/kb.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Signed-off-by: Maxime Grenu <69890511+cluster2600@users.noreply.github.com> --- nemoguardrails/kb/kb.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/nemoguardrails/kb/kb.py b/nemoguardrails/kb/kb.py index d446ba790d..877a00ac56 100644 --- a/nemoguardrails/kb/kb.py +++ b/nemoguardrails/kb/kb.py @@ -136,11 +136,7 @@ async def build(self): # 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" - if cache_file.endswith(".ann") - else cache_file + ".npy" - ) + 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) From 3aacd37bde95d1c11571ab5a2bc0a3762fed5a0c Mon Sep 17 00:00:00 2001 From: Maxime Grenu <69890511+cluster2600@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:06:54 +0100 Subject: [PATCH 13/13] Update nemoguardrails/embeddings/numpy_index.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Signed-off-by: Maxime Grenu <69890511+cluster2600@users.noreply.github.com> --- nemoguardrails/embeddings/numpy_index.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nemoguardrails/embeddings/numpy_index.py b/nemoguardrails/embeddings/numpy_index.py index 0c379ea404..84aa74b854 100644 --- a/nemoguardrails/embeddings/numpy_index.py +++ b/nemoguardrails/embeddings/numpy_index.py @@ -66,7 +66,6 @@ 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: def build(self, n_trees: int = 10) -> None: """Finalise the index. The *n_trees* parameter is ignored (kept for API compatibility with Annoy)."""