From 2e4a13d66c5fd6b3dc3e55c7019dd2dca5eec468 Mon Sep 17 00:00:00 2001 From: xRidden Date: Tue, 23 Apr 2024 22:51:31 +0530 Subject: [PATCH 01/11] New feature:super fast VoyageAI for embeddings --- semantic_router/encoders/__init__.py | 2 + semantic_router/encoders/voyageai.py | 64 ++++++++++++++++++++++++++++ semantic_router/utils/defaults.py | 3 ++ 3 files changed, 69 insertions(+) create mode 100644 semantic_router/encoders/voyageai.py diff --git a/semantic_router/encoders/__init__.py b/semantic_router/encoders/__init__.py index a79fa605..0649b1bf 100644 --- a/semantic_router/encoders/__init__.py +++ b/semantic_router/encoders/__init__.py @@ -11,6 +11,7 @@ from semantic_router.encoders.tfidf import TfidfEncoder from semantic_router.encoders.vit import VitEncoder from semantic_router.encoders.zure import AzureOpenAIEncoder +from semantic_router.encoders.voyageai import VoyageAIEncoder __all__ = [ "BaseEncoder", @@ -26,4 +27,5 @@ "VitEncoder", "CLIPEncoder", "GoogleEncoder", + "VoyageAIEncoder" ] diff --git a/semantic_router/encoders/voyageai.py b/semantic_router/encoders/voyageai.py new file mode 100644 index 00000000..64b41bc7 --- /dev/null +++ b/semantic_router/encoders/voyageai.py @@ -0,0 +1,64 @@ +import os +from time import sleep +from typing import List, Optional, Union + +import voyageai +from voyageai import VoyageError +from semantic_router.encoders import BaseEncoder +from semantic_router.utils.defaults import EncoderDefault +from semantic_router.utils.logger import logger + + +class VoyageAIEncoder(BaseEncoder): + client: Optional[voyageai.Client] + type: str = "voyageai" + + def __init__( + self, + name: Optional[str] = None, + voyage_api_key: Optional[str] = None, + score_threshold: float = 0.82, + ): + if name is None: + name = EncoderDefault.VOYAGE.value["embedding_model"] + super().__init__(name=name, score_threshold=score_threshold) + api_key = voyage_api_key or os.environ.get("VOYAGE_API_KEY") + if api_key is None: + raise ValueError("VOYAGEAI API key cannot be 'None'.") + try: + self.client = voyageai.Client(api_key) + except Exception as e: + raise ValueError( + f"VOYAGE API client failed to initialize. Error: {e}" + ) from e + + def __call__(self, docs: List[str]) -> List[List[float]]: + if self.client is None: + raise ValueError("VoyageAI client is not initialized.") + embeds = None + error_message = "" + + # Exponential backoff + for j in range(1, 7): + try: + embeds = self.client.embed( + texts=docs, + model=self.name, + input_type="query", #query or document + ) + if embeds.embeddings: + break + except VoyageError as e: + sleep(2**j) + error_message = str(e) + logger.warning(f"Retrying in {2**j} seconds...") + + except Exception as e: + logger.error(f"VoyageAI API call failed. Error: {error_message}") + raise ValueError(f"VoyageAI API call failed. Error: {e}") from e + + if (not embeds + or not embeds.embeddings): + raise ValueError("VoyageAI API call failed. Error: No embeddings found.") + + return embeds.embeddings diff --git a/semantic_router/utils/defaults.py b/semantic_router/utils/defaults.py index 3c9cbb2d..8e873e71 100644 --- a/semantic_router/utils/defaults.py +++ b/semantic_router/utils/defaults.py @@ -3,6 +3,9 @@ class EncoderDefault(Enum): + VOYAGE = { + "embedding_model": "voyage-lite-02-instruct" + } FASTEMBED = { "embedding_model": "BAAI/bge-small-en-v1.5", "language_model": "BAAI/bge-small-en-v1.5", From 1229f07b12004a3acc9b1f7a609cf545773b8da8 Mon Sep 17 00:00:00 2001 From: xRidden Date: Tue, 23 Apr 2024 22:54:13 +0530 Subject: [PATCH 02/11] New feature:super fast VoyageAI for embeddings --- tests/unit/encoders/test_voyageai.py | 91 ++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 tests/unit/encoders/test_voyageai.py diff --git a/tests/unit/encoders/test_voyageai.py b/tests/unit/encoders/test_voyageai.py new file mode 100644 index 00000000..9692cfe1 --- /dev/null +++ b/tests/unit/encoders/test_voyageai.py @@ -0,0 +1,91 @@ +import pytest +from voyageai import VoyageError + +from semantic_router.encoders import VoyageAIEncoder + + +@pytest.fixture +def voyageai_encoder(mocker): + mocker.patch('voyageai.Client') + return VoyageAIEncoder(voyage_api_key='test_api_key') + + +class TestVoyageAIEncoder: + def test_voyageai_encoder_init_success(self, mocker): + side_effect = ["fake-model-name", "fake-api-key"] + mocker.patch("os.getenv", side_effect=side_effect) + encoder = VoyageAIEncoder() + assert encoder.client is not None + + def test_voyageai_encoder_init_no_api_key(self, mocker): + mocker.patch("os.getenv", return_value=None) + with pytest.raises(ValueError) as _: + VoyageAIEncoder() + + def test_voyageai_encoder_call_uninitialized_client(self, voyageai_encoder): + voyageai_encoder.client = None + with pytest.raises(ValueError) as e: + voyageai_encoder(["test document"]) + assert "VoyageAI client is not initialized." in str(e.value) + + def test_voyageai_encoder_init_exception(self, mocker): + mocker.patch("os.getenv", return_value="fake-api-key") + mocker.patch("voyageai.Client", side_effect=Exception("Initialization error")) + with pytest.raises(ValueError) as e: + VoyageAIEncoder() + assert ( + "VOYAGE API client failed to initialize. Error: Initialization error" + in str(e.value) + ) + + def test_voyageai_encoder_call_success(self, voyageai_encoder, mocker): + mock_response = mocker.Mock() + mock_response.embeddings = [[0.1, 0.2]] + + mocker.patch("os.getenv", return_value="fake-api-key", autospec=True) + mocker.patch("time.sleep", return_value=None) + + responses = [VoyageError("VoyageAI error"), mock_response] + mocker.patch.object( + voyageai_encoder.client, "embed", side_effect=responses + ) + embeddings = voyageai_encoder(["test document"]) + assert embeddings == [[0.1, 0.2]] + + def test_voyageai_encoder_call_with_retries(self, voyageai_encoder, mocker): + mocker.patch("os.getenv", return_value="fake-api-key") + mocker.patch("time.sleep", return_value=None) + mocker.patch.object( + voyageai_encoder.client, + "embed", + side_effect=VoyageError("Test error") + ) + with pytest.raises(ValueError) as e: + voyageai_encoder(["test document"]) + assert "VoyageAI API call failed. Error: " in str(e.value) + + def test_voyageai_encoder_call_failure_non_voyage_error(self, voyageai_encoder, mocker): + mocker.patch("os.getenv", return_value="fake-api-key") + mocker.patch("time.sleep", return_value=None) + mocker.patch.object( + voyageai_encoder.client.embeddings, + "embed", + side_effect=Exception("Non-VoyageError") + ) + with pytest.raises(ValueError) as e: + voyageai_encoder(["test document"]) + assert "VoyageAI API call failed. Error: Non-VoyageError" in str(e.value) + + def test_voyageai_encoder_call_successful_retry(self, voyageai_encoder, mocker): + mock_response = mocker.Mock() + mock_response.embeddings = [[0.1, 0.2]] + + mocker.patch("os.getenv", return_value="fake-api-key") + mocker.patch("time.sleep", return_value=None) + + responses = [VoyageError("VoyageAI error"), mock_response] + mocker.patch.object( + voyageai_encoder.client, "embed", side_effect=responses + ) + embeddings = voyageai_encoder(["test document"]) + assert embeddings == [[0.1, 0.2]] From bc5b9754fc5353c853d520b2a84a579482786dd3 Mon Sep 17 00:00:00 2001 From: xRidden Date: Tue, 23 Apr 2024 23:03:28 +0530 Subject: [PATCH 03/11] finished linting successfully --- semantic_router/encoders/__init__.py | 2 +- semantic_router/encoders/voyageai.py | 13 ++++++------- semantic_router/utils/defaults.py | 4 +--- tests/unit/encoders/test_voyageai.py | 22 +++++++++------------- 4 files changed, 17 insertions(+), 24 deletions(-) diff --git a/semantic_router/encoders/__init__.py b/semantic_router/encoders/__init__.py index 0649b1bf..9d31549e 100644 --- a/semantic_router/encoders/__init__.py +++ b/semantic_router/encoders/__init__.py @@ -27,5 +27,5 @@ "VitEncoder", "CLIPEncoder", "GoogleEncoder", - "VoyageAIEncoder" + "VoyageAIEncoder", ] diff --git a/semantic_router/encoders/voyageai.py b/semantic_router/encoders/voyageai.py index 64b41bc7..cbacdd16 100644 --- a/semantic_router/encoders/voyageai.py +++ b/semantic_router/encoders/voyageai.py @@ -1,6 +1,6 @@ import os from time import sleep -from typing import List, Optional, Union +from typing import List, Optional import voyageai from voyageai import VoyageError @@ -44,21 +44,20 @@ def __call__(self, docs: List[str]) -> List[List[float]]: embeds = self.client.embed( texts=docs, model=self.name, - input_type="query", #query or document + input_type="query", # query or document ) if embeds.embeddings: break except VoyageError as e: sleep(2**j) error_message = str(e) - logger.warning(f"Retrying in {2**j} seconds...") - + logger.warning(f"Retrying in {2**j} seconds...") + except Exception as e: logger.error(f"VoyageAI API call failed. Error: {error_message}") raise ValueError(f"VoyageAI API call failed. Error: {e}") from e - - if (not embeds - or not embeds.embeddings): + + if not embeds or not embeds.embeddings: raise ValueError("VoyageAI API call failed. Error: No embeddings found.") return embeds.embeddings diff --git a/semantic_router/utils/defaults.py b/semantic_router/utils/defaults.py index 8e873e71..6f777a48 100644 --- a/semantic_router/utils/defaults.py +++ b/semantic_router/utils/defaults.py @@ -3,9 +3,7 @@ class EncoderDefault(Enum): - VOYAGE = { - "embedding_model": "voyage-lite-02-instruct" - } + VOYAGE = {"embedding_model": "voyage-lite-02-instruct"} FASTEMBED = { "embedding_model": "BAAI/bge-small-en-v1.5", "language_model": "BAAI/bge-small-en-v1.5", diff --git a/tests/unit/encoders/test_voyageai.py b/tests/unit/encoders/test_voyageai.py index 9692cfe1..3c8ea1f2 100644 --- a/tests/unit/encoders/test_voyageai.py +++ b/tests/unit/encoders/test_voyageai.py @@ -6,8 +6,8 @@ @pytest.fixture def voyageai_encoder(mocker): - mocker.patch('voyageai.Client') - return VoyageAIEncoder(voyage_api_key='test_api_key') + mocker.patch("voyageai.Client") + return VoyageAIEncoder(voyage_api_key="test_api_key") class TestVoyageAIEncoder: @@ -46,9 +46,7 @@ def test_voyageai_encoder_call_success(self, voyageai_encoder, mocker): mocker.patch("time.sleep", return_value=None) responses = [VoyageError("VoyageAI error"), mock_response] - mocker.patch.object( - voyageai_encoder.client, "embed", side_effect=responses - ) + mocker.patch.object(voyageai_encoder.client, "embed", side_effect=responses) embeddings = voyageai_encoder(["test document"]) assert embeddings == [[0.1, 0.2]] @@ -56,21 +54,21 @@ def test_voyageai_encoder_call_with_retries(self, voyageai_encoder, mocker): mocker.patch("os.getenv", return_value="fake-api-key") mocker.patch("time.sleep", return_value=None) mocker.patch.object( - voyageai_encoder.client, - "embed", - side_effect=VoyageError("Test error") + voyageai_encoder.client, "embed", side_effect=VoyageError("Test error") ) with pytest.raises(ValueError) as e: voyageai_encoder(["test document"]) assert "VoyageAI API call failed. Error: " in str(e.value) - def test_voyageai_encoder_call_failure_non_voyage_error(self, voyageai_encoder, mocker): + def test_voyageai_encoder_call_failure_non_voyage_error( + self, voyageai_encoder, mocker + ): mocker.patch("os.getenv", return_value="fake-api-key") mocker.patch("time.sleep", return_value=None) mocker.patch.object( voyageai_encoder.client.embeddings, "embed", - side_effect=Exception("Non-VoyageError") + side_effect=Exception("Non-VoyageError"), ) with pytest.raises(ValueError) as e: voyageai_encoder(["test document"]) @@ -84,8 +82,6 @@ def test_voyageai_encoder_call_successful_retry(self, voyageai_encoder, mocker): mocker.patch("time.sleep", return_value=None) responses = [VoyageError("VoyageAI error"), mock_response] - mocker.patch.object( - voyageai_encoder.client, "embed", side_effect=responses - ) + mocker.patch.object(voyageai_encoder.client, "embed", side_effect=responses) embeddings = voyageai_encoder(["test document"]) assert embeddings == [[0.1, 0.2]] From 759c56ce48cbeaa67bd75b23251391609e54af21 Mon Sep 17 00:00:00 2001 From: xRidden Date: Thu, 25 Apr 2024 19:58:25 +0530 Subject: [PATCH 04/11] fixed incorrect import --- semantic_router/encoders/voyageai.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/semantic_router/encoders/voyageai.py b/semantic_router/encoders/voyageai.py index cbacdd16..25da1aee 100644 --- a/semantic_router/encoders/voyageai.py +++ b/semantic_router/encoders/voyageai.py @@ -3,7 +3,6 @@ from typing import List, Optional import voyageai -from voyageai import VoyageError from semantic_router.encoders import BaseEncoder from semantic_router.utils.defaults import EncoderDefault from semantic_router.utils.logger import logger @@ -48,10 +47,10 @@ def __call__(self, docs: List[str]) -> List[List[float]]: ) if embeds.embeddings: break - except VoyageError as e: - sleep(2**j) - error_message = str(e) - logger.warning(f"Retrying in {2**j} seconds...") + else: + sleep(2**j) + error_message = str(e) + logger.warning(f"Retrying in {2**j} seconds...") except Exception as e: logger.error(f"VoyageAI API call failed. Error: {error_message}") From 7a5470c7948584cc5dd685603db588294f110e79 Mon Sep 17 00:00:00 2001 From: xRidden Date: Thu, 25 Apr 2024 20:07:42 +0530 Subject: [PATCH 05/11] add dependency for voyageai --- pyproject.toml | 1 + tests/unit/encoders/test_voyageai.py | 21 +++++++++------------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 06d314d5..427e76b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ matplotlib = { version = "^3.8.3", optional = true} qdrant-client = {version = "^1.8.0", optional = true} google-cloud-aiplatform = {version = "^1.45.0", optional = true} requests-mock = "^1.12.1" +voyageai = "^0.2.2" [tool.poetry.extras] hybrid = ["pinecone-text"] diff --git a/tests/unit/encoders/test_voyageai.py b/tests/unit/encoders/test_voyageai.py index 3c8ea1f2..bcf36a34 100644 --- a/tests/unit/encoders/test_voyageai.py +++ b/tests/unit/encoders/test_voyageai.py @@ -1,5 +1,4 @@ import pytest -from voyageai import VoyageError from semantic_router.encoders import VoyageAIEncoder @@ -45,20 +44,19 @@ def test_voyageai_encoder_call_success(self, voyageai_encoder, mocker): mocker.patch("os.getenv", return_value="fake-api-key", autospec=True) mocker.patch("time.sleep", return_value=None) - responses = [VoyageError("VoyageAI error"), mock_response] - mocker.patch.object(voyageai_encoder.client, "embed", side_effect=responses) + mocker.patch.object(voyageai_encoder.client, "embed", return_value=mock_response) embeddings = voyageai_encoder(["test document"]) assert embeddings == [[0.1, 0.2]] def test_voyageai_encoder_call_with_retries(self, voyageai_encoder, mocker): + error = Exception("Network error") mocker.patch("os.getenv", return_value="fake-api-key") mocker.patch("time.sleep", return_value=None) mocker.patch.object( - voyageai_encoder.client, "embed", side_effect=VoyageError("Test error") + voyageai_encoder.client, "embed", side_effect=[error, error, mocker.Mock(embeddings=[[0.1, 0.2]])] ) - with pytest.raises(ValueError) as e: - voyageai_encoder(["test document"]) - assert "VoyageAI API call failed. Error: " in str(e.value) + embeddings = voyageai_encoder(["test document"]) + assert embeddings == [[0.1, 0.2]] def test_voyageai_encoder_call_failure_non_voyage_error( self, voyageai_encoder, mocker @@ -66,13 +64,12 @@ def test_voyageai_encoder_call_failure_non_voyage_error( mocker.patch("os.getenv", return_value="fake-api-key") mocker.patch("time.sleep", return_value=None) mocker.patch.object( - voyageai_encoder.client.embeddings, - "embed", - side_effect=Exception("Non-VoyageError"), + voyageai_encoder.client, "embed", + side_effect=Exception("General error"), ) with pytest.raises(ValueError) as e: voyageai_encoder(["test document"]) - assert "VoyageAI API call failed. Error: Non-VoyageError" in str(e.value) + assert "VoyageAI API call failed. Error: General error" in str(e.value) def test_voyageai_encoder_call_successful_retry(self, voyageai_encoder, mocker): mock_response = mocker.Mock() @@ -81,7 +78,7 @@ def test_voyageai_encoder_call_successful_retry(self, voyageai_encoder, mocker): mocker.patch("os.getenv", return_value="fake-api-key") mocker.patch("time.sleep", return_value=None) - responses = [VoyageError("VoyageAI error"), mock_response] + responses = [Exception("Temporary error"), mock_response] mocker.patch.object(voyageai_encoder.client, "embed", side_effect=responses) embeddings = voyageai_encoder(["test document"]) assert embeddings == [[0.1, 0.2]] From c2f74d840b7d816f8397821c9e6a418955f3c04c Mon Sep 17 00:00:00 2001 From: xRidden Date: Thu, 25 Apr 2024 20:16:14 +0530 Subject: [PATCH 06/11] lint successful --- semantic_router/encoders/voyageai.py | 1 - tests/unit/encoders/test_voyageai.py | 11 ++++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/semantic_router/encoders/voyageai.py b/semantic_router/encoders/voyageai.py index 25da1aee..ec7f5551 100644 --- a/semantic_router/encoders/voyageai.py +++ b/semantic_router/encoders/voyageai.py @@ -49,7 +49,6 @@ def __call__(self, docs: List[str]) -> List[List[float]]: break else: sleep(2**j) - error_message = str(e) logger.warning(f"Retrying in {2**j} seconds...") except Exception as e: diff --git a/tests/unit/encoders/test_voyageai.py b/tests/unit/encoders/test_voyageai.py index bcf36a34..baff240a 100644 --- a/tests/unit/encoders/test_voyageai.py +++ b/tests/unit/encoders/test_voyageai.py @@ -44,7 +44,9 @@ def test_voyageai_encoder_call_success(self, voyageai_encoder, mocker): mocker.patch("os.getenv", return_value="fake-api-key", autospec=True) mocker.patch("time.sleep", return_value=None) - mocker.patch.object(voyageai_encoder.client, "embed", return_value=mock_response) + mocker.patch.object( + voyageai_encoder.client, "embed", return_value=mock_response + ) embeddings = voyageai_encoder(["test document"]) assert embeddings == [[0.1, 0.2]] @@ -53,7 +55,9 @@ def test_voyageai_encoder_call_with_retries(self, voyageai_encoder, mocker): mocker.patch("os.getenv", return_value="fake-api-key") mocker.patch("time.sleep", return_value=None) mocker.patch.object( - voyageai_encoder.client, "embed", side_effect=[error, error, mocker.Mock(embeddings=[[0.1, 0.2]])] + voyageai_encoder.client, + "embed", + side_effect=[error, error, mocker.Mock(embeddings=[[0.1, 0.2]])], ) embeddings = voyageai_encoder(["test document"]) assert embeddings == [[0.1, 0.2]] @@ -64,7 +68,8 @@ def test_voyageai_encoder_call_failure_non_voyage_error( mocker.patch("os.getenv", return_value="fake-api-key") mocker.patch("time.sleep", return_value=None) mocker.patch.object( - voyageai_encoder.client, "embed", + voyageai_encoder.client, + "embed", side_effect=Exception("General error"), ) with pytest.raises(ValueError) as e: From 4832c27c0c28e6acc34a79b08ba5adfc6046acb8 Mon Sep 17 00:00:00 2001 From: xRidden Date: Fri, 26 Apr 2024 11:36:46 +0530 Subject: [PATCH 07/11] resolving dependencies in poetry --- poetry.lock | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 53b7b5cd..fc0fd853 100644 --- a/poetry.lock +++ b/poetry.lock @@ -96,6 +96,17 @@ yarl = ">=1.0,<2.0" [package.extras] speedups = ["Brotli", "aiodns", "brotlicffi"] +[[package]] +name = "aiolimiter" +version = "1.1.0" +description = "asyncio rate limiter, a leaky bucket implementation" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "aiolimiter-1.1.0-py3-none-any.whl", hash = "sha256:0b4997961fc58b8df40279e739f9cf0d3e255e63e9a44f64df567a8c17241e24"}, + {file = "aiolimiter-1.1.0.tar.gz", hash = "sha256:461cf02f82a29347340d031626c92853645c099cb5ff85577b831a7bd21132b5"}, +] + [[package]] name = "aiosignal" version = "1.3.1" @@ -3508,6 +3519,7 @@ files = [ {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"}, @@ -4113,6 +4125,20 @@ files = [ [package.dependencies] mpmath = ">=0.19" +[[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 = "tiktoken" version = "0.6.0" @@ -4602,6 +4628,24 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "voyageai" +version = "0.2.2" +description = "" +optional = false +python-versions = "<4.0.0,>=3.7.1" +files = [ + {file = "voyageai-0.2.2-py3-none-any.whl", hash = "sha256:caba84fa448bb82eeb39a4c4479978dc08bff9e2473773fea9cdcdd737560e4d"}, + {file = "voyageai-0.2.2.tar.gz", hash = "sha256:e477ea2aa6d54580426c7a4a67ad45cfba5480db2bad4da3eb74007b3984f3a5"}, +] + +[package.dependencies] +aiohttp = ">=3.5,<4.0" +aiolimiter = ">=1.1.0,<2.0.0" +numpy = ">=1.11" +requests = ">=2.20,<3.0" +tenacity = ">=8.0.1" + [[package]] name = "wcwidth" version = "0.2.13" @@ -4769,4 +4813,4 @@ vision = ["pillow", "torch", "torchvision", "transformers"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "9f308d2dd1c067185f9d84721b25d81e7d1e72a239059863bad1f4439a7a26cc" +content-hash = "a8b93545f2fda29525ca5230d4d983c1587e8b93145793fcd45ba2ecbd0f41a7" From c4fe976f378b7b0b67144f3745cad51d9052e3de Mon Sep 17 00:00:00 2001 From: James Briggs Date: Sat, 27 Apr 2024 23:51:26 +0800 Subject: [PATCH 08/11] install requirements and encoder alignment --- poetry.lock | 9 ++++---- pyproject.toml | 3 ++- semantic_router/encoders/voyageai.py | 34 +++++++++++++++++++--------- tests/unit/encoders/test_voyageai.py | 28 ++++++++++++++++------- 4 files changed, 50 insertions(+), 24 deletions(-) diff --git a/poetry.lock b/poetry.lock index fc0fd853..9c684b31 100644 --- a/poetry.lock +++ b/poetry.lock @@ -100,7 +100,7 @@ speedups = ["Brotli", "aiodns", "brotlicffi"] name = "aiolimiter" version = "1.1.0" description = "asyncio rate limiter, a leaky bucket implementation" -optional = false +optional = true python-versions = ">=3.7,<4.0" files = [ {file = "aiolimiter-1.1.0-py3-none-any.whl", hash = "sha256:0b4997961fc58b8df40279e739f9cf0d3e255e63e9a44f64df567a8c17241e24"}, @@ -4129,7 +4129,7 @@ mpmath = ">=0.19" name = "tenacity" version = "8.2.3" description = "Retry code until it succeeds" -optional = false +optional = true python-versions = ">=3.7" files = [ {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"}, @@ -4632,7 +4632,7 @@ zstd = ["zstandard (>=0.18.0)"] name = "voyageai" version = "0.2.2" description = "" -optional = false +optional = true python-versions = "<4.0.0,>=3.7.1" files = [ {file = "voyageai-0.2.2-py3-none-any.whl", hash = "sha256:caba84fa448bb82eeb39a4c4479978dc08bff9e2473773fea9cdcdd737560e4d"}, @@ -4809,8 +4809,9 @@ pinecone = ["pinecone-client"] processing = ["matplotlib"] qdrant = ["qdrant-client"] vision = ["pillow", "torch", "torchvision", "transformers"] +voyageai = ["voyageai"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "a8b93545f2fda29525ca5230d4d983c1587e8b93145793fcd45ba2ecbd0f41a7" +content-hash = "47ff8ab6a9e699117161a0c4a30beb6d47eebb298a3f5020bab6c8575d9f96d9" diff --git a/pyproject.toml b/pyproject.toml index 427e76b1..1f1bdb01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ matplotlib = { version = "^3.8.3", optional = true} qdrant-client = {version = "^1.8.0", optional = true} google-cloud-aiplatform = {version = "^1.45.0", optional = true} requests-mock = "^1.12.1" -voyageai = "^0.2.2" +voyageai = {version = "^0.2.2", optional = true} [tool.poetry.extras] hybrid = ["pinecone-text"] @@ -50,6 +50,7 @@ processing = ["matplotlib"] mistralai = ["mistralai"] qdrant = ["qdrant-client"] google = ["google-cloud-aiplatform"] +voyageai = ["voyageai"] [tool.poetry.group.dev.dependencies] ipykernel = "^6.25.0" diff --git a/semantic_router/encoders/voyageai.py b/semantic_router/encoders/voyageai.py index ec7f5551..d9bd32ca 100644 --- a/semantic_router/encoders/voyageai.py +++ b/semantic_router/encoders/voyageai.py @@ -1,15 +1,16 @@ import os from time import sleep -from typing import List, Optional +from typing import Any, List, Optional + +from pydantic.v1 import PrivateAttr -import voyageai from semantic_router.encoders import BaseEncoder from semantic_router.utils.defaults import EncoderDefault from semantic_router.utils.logger import logger class VoyageAIEncoder(BaseEncoder): - client: Optional[voyageai.Client] + _client: Any = PrivateAttr() type: str = "voyageai" def __init__( @@ -21,18 +22,29 @@ def __init__( if name is None: name = EncoderDefault.VOYAGE.value["embedding_model"] super().__init__(name=name, score_threshold=score_threshold) - api_key = voyage_api_key or os.environ.get("VOYAGE_API_KEY") + self._client = self._initialize_client(api_key=voyage_api_key) + + def _initialize_client(self, api_key: str): + try: + import voyageai + except ImportError: + raise ImportError( + "Please install VoyageAI to use VoyageAIEncoder. " + "You can install it with: " + "`pip install 'semantic-router[voyageai]'`" + ) + + api_key = api_key or os.getenv("VOYAGEAI_API_KEY") if api_key is None: - raise ValueError("VOYAGEAI API key cannot be 'None'.") + raise ValueError("VoyageAI API key not provided") try: - self.client = voyageai.Client(api_key) + client = voyageai.Client(api_key=api_key) except Exception as e: - raise ValueError( - f"VOYAGE API client failed to initialize. Error: {e}" - ) from e + raise ValueError(f"Unable to connect to VoyageAI {e.args}: {e}") from e + return client def __call__(self, docs: List[str]) -> List[List[float]]: - if self.client is None: + if self._client == PrivateAttr(): raise ValueError("VoyageAI client is not initialized.") embeds = None error_message = "" @@ -40,7 +52,7 @@ def __call__(self, docs: List[str]) -> List[List[float]]: # Exponential backoff for j in range(1, 7): try: - embeds = self.client.embed( + embeds = self._client.embed( texts=docs, model=self.name, input_type="query", # query or document diff --git a/tests/unit/encoders/test_voyageai.py b/tests/unit/encoders/test_voyageai.py index baff240a..1e64d1b0 100644 --- a/tests/unit/encoders/test_voyageai.py +++ b/tests/unit/encoders/test_voyageai.py @@ -1,20 +1,32 @@ +from unittest.mock import patch + import pytest +from pydantic.v1 import PrivateAttr + from semantic_router.encoders import VoyageAIEncoder @pytest.fixture def voyageai_encoder(mocker): mocker.patch("voyageai.Client") + mocker.patch("voyageai.Client.embed", return_value=[[0.1, 0.2]]) return VoyageAIEncoder(voyage_api_key="test_api_key") class TestVoyageAIEncoder: + def test_voyageai_encoder_import_error(self): + with patch.dict("sys.modules", {"voyageai": None}): + with pytest.raises(ImportError) as error: + VoyageAIEncoder() + + assert "pip install 'semantic-router[voyageai]'" in str(error.value) + def test_voyageai_encoder_init_success(self, mocker): - side_effect = ["fake-model-name", "fake-api-key"] - mocker.patch("os.getenv", side_effect=side_effect) + #side_effect = ["fake-model-name", "fake-api-key"] + #mocker.patch("os.getenv", side_effect=side_effect) encoder = VoyageAIEncoder() - assert encoder.client is not None + assert encoder._client is not PrivateAttr() def test_voyageai_encoder_init_no_api_key(self, mocker): mocker.patch("os.getenv", return_value=None) @@ -22,7 +34,7 @@ def test_voyageai_encoder_init_no_api_key(self, mocker): VoyageAIEncoder() def test_voyageai_encoder_call_uninitialized_client(self, voyageai_encoder): - voyageai_encoder.client = None + voyageai_encoder._client = PrivateAttr() with pytest.raises(ValueError) as e: voyageai_encoder(["test document"]) assert "VoyageAI client is not initialized." in str(e.value) @@ -45,7 +57,7 @@ def test_voyageai_encoder_call_success(self, voyageai_encoder, mocker): mocker.patch("time.sleep", return_value=None) mocker.patch.object( - voyageai_encoder.client, "embed", return_value=mock_response + voyageai_encoder._client, "embed", return_value=mock_response ) embeddings = voyageai_encoder(["test document"]) assert embeddings == [[0.1, 0.2]] @@ -55,7 +67,7 @@ def test_voyageai_encoder_call_with_retries(self, voyageai_encoder, mocker): mocker.patch("os.getenv", return_value="fake-api-key") mocker.patch("time.sleep", return_value=None) mocker.patch.object( - voyageai_encoder.client, + voyageai_encoder._client, "embed", side_effect=[error, error, mocker.Mock(embeddings=[[0.1, 0.2]])], ) @@ -68,7 +80,7 @@ def test_voyageai_encoder_call_failure_non_voyage_error( mocker.patch("os.getenv", return_value="fake-api-key") mocker.patch("time.sleep", return_value=None) mocker.patch.object( - voyageai_encoder.client, + voyageai_encoder._client, "embed", side_effect=Exception("General error"), ) @@ -84,6 +96,6 @@ def test_voyageai_encoder_call_successful_retry(self, voyageai_encoder, mocker): mocker.patch("time.sleep", return_value=None) responses = [Exception("Temporary error"), mock_response] - mocker.patch.object(voyageai_encoder.client, "embed", side_effect=responses) + mocker.patch.object(voyageai_encoder._client, "embed", side_effect=responses) embeddings = voyageai_encoder(["test document"]) assert embeddings == [[0.1, 0.2]] From 10b9e54d0b626868f4b192ef014841e4b74cb0ef Mon Sep 17 00:00:00 2001 From: James Briggs Date: Sat, 27 Apr 2024 23:58:13 +0800 Subject: [PATCH 09/11] lint --- semantic_router/encoders/voyageai.py | 4 ++-- tests/unit/encoders/test_voyageai.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/semantic_router/encoders/voyageai.py b/semantic_router/encoders/voyageai.py index d9bd32ca..2462fefe 100644 --- a/semantic_router/encoders/voyageai.py +++ b/semantic_router/encoders/voyageai.py @@ -23,8 +23,8 @@ def __init__( name = EncoderDefault.VOYAGE.value["embedding_model"] super().__init__(name=name, score_threshold=score_threshold) self._client = self._initialize_client(api_key=voyage_api_key) - - def _initialize_client(self, api_key: str): + + def _initialize_client(self, api_key: Optional[str] = None): try: import voyageai except ImportError: diff --git a/tests/unit/encoders/test_voyageai.py b/tests/unit/encoders/test_voyageai.py index 1e64d1b0..fd341e98 100644 --- a/tests/unit/encoders/test_voyageai.py +++ b/tests/unit/encoders/test_voyageai.py @@ -10,7 +10,6 @@ @pytest.fixture def voyageai_encoder(mocker): mocker.patch("voyageai.Client") - mocker.patch("voyageai.Client.embed", return_value=[[0.1, 0.2]]) return VoyageAIEncoder(voyage_api_key="test_api_key") From d5d997d40e45ed3f6165d4954485b7b269acefc7 Mon Sep 17 00:00:00 2001 From: James Briggs Date: Sat, 27 Apr 2024 23:59:42 +0800 Subject: [PATCH 10/11] revert my change in unit test --- tests/unit/encoders/test_voyageai.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/encoders/test_voyageai.py b/tests/unit/encoders/test_voyageai.py index fd341e98..174cf11b 100644 --- a/tests/unit/encoders/test_voyageai.py +++ b/tests/unit/encoders/test_voyageai.py @@ -22,8 +22,8 @@ def test_voyageai_encoder_import_error(self): assert "pip install 'semantic-router[voyageai]'" in str(error.value) def test_voyageai_encoder_init_success(self, mocker): - #side_effect = ["fake-model-name", "fake-api-key"] - #mocker.patch("os.getenv", side_effect=side_effect) + side_effect = ["fake-model-name", "fake-api-key"] + mocker.patch("os.getenv", side_effect=side_effect) encoder = VoyageAIEncoder() assert encoder._client is not PrivateAttr() From 966b23a5fb11b04d030e57d030230272e2aa5883 Mon Sep 17 00:00:00 2001 From: James Briggs Date: Sun, 28 Apr 2024 00:02:44 +0800 Subject: [PATCH 11/11] lint --- tests/unit/encoders/test_voyageai.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/encoders/test_voyageai.py b/tests/unit/encoders/test_voyageai.py index 174cf11b..8ae706f4 100644 --- a/tests/unit/encoders/test_voyageai.py +++ b/tests/unit/encoders/test_voyageai.py @@ -18,7 +18,7 @@ def test_voyageai_encoder_import_error(self): with patch.dict("sys.modules", {"voyageai": None}): with pytest.raises(ImportError) as error: VoyageAIEncoder() - + assert "pip install 'semantic-router[voyageai]'" in str(error.value) def test_voyageai_encoder_init_success(self, mocker):