From 0483b4670c620933e34b397951e1eeb99dc3b60d Mon Sep 17 00:00:00 2001 From: Ismail Ashraq Date: Sun, 18 Aug 2024 16:02:43 +0500 Subject: [PATCH 01/10] add max_retries to openai and azure encoders --- semantic_router/encoders/openai.py | 37 ++++++++++++++++--------- semantic_router/encoders/zure.py | 44 ++++++++++++++++-------------- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/semantic_router/encoders/openai.py b/semantic_router/encoders/openai.py index e4acc5a4..0425d5df 100644 --- a/semantic_router/encoders/openai.py +++ b/semantic_router/encoders/openai.py @@ -42,6 +42,7 @@ class OpenAIEncoder(BaseEncoder): token_limit: int = 8192 # default value, should be replaced by config _token_encoder: Any = PrivateAttr() type: str = "openai" + max_retries: int def __init__( self, @@ -51,9 +52,13 @@ def __init__( openai_org_id: Optional[str] = None, score_threshold: Optional[float] = None, dimensions: Union[int, NotGiven] = NotGiven(), + max_retries: int | None = None, ): if name is None: name = EncoderDefault.OPENAI.value["embedding_model"] + + max_retries = max_retries if max_retries is not None else 3 + if score_threshold is None and name in model_configs: set_score_threshold = model_configs[name].threshold elif score_threshold is None: @@ -66,6 +71,7 @@ def __init__( super().__init__( name=name, score_threshold=set_score_threshold, + max_retries=max_retries, ) api_key = openai_api_key or os.getenv("OPENAI_API_KEY") base_url = openai_base_url or os.getenv("OPENAI_BASE_URL") @@ -109,8 +115,9 @@ def __call__(self, docs: List[str], truncate: bool = True) -> List[List[float]]: docs = [self._truncate(doc) for doc in docs] # Exponential backoff - for j in range(1, 7): + for j in range(self.max_retries + 1): try: + raise OpenAIError("Test") embeds = self.client.embeddings.create( input=docs, model=self.name, @@ -119,12 +126,14 @@ def __call__(self, docs: List[str], truncate: bool = True) -> List[List[float]]: if embeds.data: break except OpenAIError as e: - sleep(2**j) - error_message = str(e) - logger.warning(f"Retrying in {2**j} seconds...") + logger.error("Exception occurred", exc_info=True) + if self.max_retries != 0: + sleep(2**j) + logger.warning(f"Retrying in {2**j} seconds due to OpenAIError: {e}") + except Exception as e: - logger.error(f"OpenAI API call failed. Error: {error_message}") - raise ValueError(f"OpenAI API call failed. Error: {e}") from e + logger.error(f"OpenAI API call failed. Error: {e}") + raise ValueError(f"OpenAI API call failed. Error: {str(e)}") from e if ( not embeds @@ -132,7 +141,7 @@ def __call__(self, docs: List[str], truncate: bool = True) -> List[List[float]]: or not embeds.data ): logger.info(f"Returned embeddings: {embeds}") - raise ValueError(f"No embeddings returned. Error: {error_message}") + raise ValueError(f"No embeddings returned.") embeddings = [embeds_obj.embedding for embeds_obj in embeds.data] return embeddings @@ -161,8 +170,9 @@ async def acall(self, docs: List[str], truncate: bool = True) -> List[List[float docs = [self._truncate(doc) for doc in docs] # Exponential backoff - for j in range(1, 7): + for j in range(self.max_retries + 1): try: + raise OpenAIError("Test") embeds = await self.async_client.embeddings.create( input=docs, model=self.name, @@ -171,11 +181,12 @@ async def acall(self, docs: List[str], truncate: bool = True) -> List[List[float if embeds.data: break except OpenAIError as e: - await asleep(2**j) - error_message = str(e) - logger.warning(f"Retrying in {2**j} seconds...") + logger.error("Exception occurred", exc_info=True) + if self.max_retries != 0: + await asleep(2**j) + logger.warning(f"Retrying in {2**j} seconds due to OpenAIError: {e}") except Exception as e: - logger.error(f"OpenAI API call failed. Error: {error_message}") + logger.error(f"OpenAI API call failed. Error: {e}") raise ValueError(f"OpenAI API call failed. Error: {e}") from e if ( @@ -184,7 +195,7 @@ async def acall(self, docs: List[str], truncate: bool = True) -> List[List[float or not embeds.data ): logger.info(f"Returned embeddings: {embeds}") - raise ValueError(f"No embeddings returned. Error: {error_message}") + raise ValueError(f"No embeddings returned.") embeddings = [embeds_obj.embedding for embeds_obj in embeds.data] return embeddings diff --git a/semantic_router/encoders/zure.py b/semantic_router/encoders/zure.py index dba93600..3c199692 100644 --- a/semantic_router/encoders/zure.py +++ b/semantic_router/encoders/zure.py @@ -23,6 +23,7 @@ class AzureOpenAIEncoder(BaseEncoder): azure_endpoint: Optional[str] = None api_version: Optional[str] = None model: Optional[str] = None + max_retries: int def __init__( self, @@ -33,11 +34,15 @@ def __init__( model: Optional[str] = None, # TODO we should change to `name` JB score_threshold: float = 0.82, dimensions: Union[int, NotGiven] = NotGiven(), + max_retries: int | None = None, ): name = deployment_name if name is None: name = EncoderDefault.AZURE.value["embedding_model"] - super().__init__(name=name, score_threshold=score_threshold) + + max_retries = max_retries if max_retries is not None else 3 + + super().__init__(name=name, score_threshold=score_threshold, max_retries=max_retries) self.api_key = api_key self.deployment_name = deployment_name self.azure_endpoint = azure_endpoint @@ -100,8 +105,9 @@ def __call__(self, docs: List[str]) -> List[List[float]]: error_message = "" # Exponential backoff - for j in range(3): + for j in range(self.max_retries + 1): try: + raise OpenAIError("Test") embeds = self.client.embeddings.create( input=docs, model=str(self.model), @@ -110,15 +116,12 @@ def __call__(self, docs: List[str]) -> List[List[float]]: if embeds.data: break except OpenAIError as e: - # print full traceback - import traceback - - traceback.print_exc() - sleep(2**j) - error_message = str(e) - logger.warning(f"Retrying in {2**j} seconds...") + logger.error("Exception occurred", exc_info=True) + if self.max_retries != 0: + sleep(2**j) + logger.warning(f"Retrying in {2**j} seconds due to OpenAIError: {e}") except Exception as e: - logger.error(f"Azure OpenAI API call failed. Error: {error_message}") + logger.error(f"Azure OpenAI API call failed. Error: {e}") raise ValueError(f"Azure OpenAI API call failed. Error: {e}") from e if ( @@ -126,7 +129,7 @@ def __call__(self, docs: List[str]) -> List[List[float]]: or not isinstance(embeds, CreateEmbeddingResponse) or not embeds.data ): - raise ValueError(f"No embeddings returned. Error: {error_message}") + raise ValueError(f"No embeddings returned.") embeddings = [embeds_obj.embedding for embeds_obj in embeds.data] return embeddings @@ -138,8 +141,9 @@ async def acall(self, docs: List[str]) -> List[List[float]]: error_message = "" # Exponential backoff - for j in range(3): + for j in range(self.max_retries + 1): try: + raise OpenAIError("Test") embeds = await self.async_client.embeddings.create( input=docs, model=str(self.model), @@ -147,16 +151,14 @@ async def acall(self, docs: List[str]) -> List[List[float]]: ) if embeds.data: break - except OpenAIError as e: - # print full traceback - import traceback - traceback.print_exc() - await asleep(2**j) - error_message = str(e) - logger.warning(f"Retrying in {2**j} seconds...") + except OpenAIError as e: + logger.error("Exception occurred", exc_info=True) + if self.max_retries != 0: + await asleep(2**j) + logger.warning(f"Retrying in {2**j} seconds due to OpenAIError: {e}") except Exception as e: - logger.error(f"Azure OpenAI API call failed. Error: {error_message}") + logger.error(f"Azure OpenAI API call failed. Error: {e}") raise ValueError(f"Azure OpenAI API call failed. Error: {e}") from e if ( @@ -164,7 +166,7 @@ async def acall(self, docs: List[str]) -> List[List[float]]: or not isinstance(embeds, CreateEmbeddingResponse) or not embeds.data ): - raise ValueError(f"No embeddings returned. Error: {error_message}") + raise ValueError(f"No embeddings returned.") embeddings = [embeds_obj.embedding for embeds_obj in embeds.data] return embeddings From 57d748dde82b3ceaae0f16b9a1b1cdaf4e88e606 Mon Sep 17 00:00:00 2001 From: Ismail Ashraq Date: Sun, 18 Aug 2024 16:14:17 +0500 Subject: [PATCH 02/10] linting --- semantic_router/encoders/openai.py | 22 +++++++++++----------- semantic_router/encoders/zure.py | 23 ++++++++++++----------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/semantic_router/encoders/openai.py b/semantic_router/encoders/openai.py index 0425d5df..8ba7ee5b 100644 --- a/semantic_router/encoders/openai.py +++ b/semantic_router/encoders/openai.py @@ -42,7 +42,7 @@ class OpenAIEncoder(BaseEncoder): token_limit: int = 8192 # default value, should be replaced by config _token_encoder: Any = PrivateAttr() type: str = "openai" - max_retries: int + max_retries: int = 3 def __init__( self, @@ -56,9 +56,6 @@ def __init__( ): if name is None: name = EncoderDefault.OPENAI.value["embedding_model"] - - max_retries = max_retries if max_retries is not None else 3 - if score_threshold is None and name in model_configs: set_score_threshold = model_configs[name].threshold elif score_threshold is None: @@ -71,13 +68,14 @@ def __init__( super().__init__( name=name, score_threshold=set_score_threshold, - max_retries=max_retries, ) api_key = openai_api_key or os.getenv("OPENAI_API_KEY") base_url = openai_base_url or os.getenv("OPENAI_BASE_URL") openai_org_id = openai_org_id or os.getenv("OPENAI_ORG_ID") if api_key is None: raise ValueError("OpenAI API key cannot be 'None'.") + if max_retries is not None: + self.max_retries = max_retries try: self.client = openai.Client( base_url=base_url, api_key=api_key, organization=openai_org_id @@ -108,7 +106,6 @@ def __call__(self, docs: List[str], truncate: bool = True) -> List[List[float]]: if self.client is None: raise ValueError("OpenAI client is not initialized.") embeds = None - error_message = "" if truncate: # check if any document exceeds token limit and truncate if so @@ -129,7 +126,9 @@ def __call__(self, docs: List[str], truncate: bool = True) -> List[List[float]]: logger.error("Exception occurred", exc_info=True) if self.max_retries != 0: sleep(2**j) - logger.warning(f"Retrying in {2**j} seconds due to OpenAIError: {e}") + logger.warning( + f"Retrying in {2**j} seconds due to OpenAIError: {e}" + ) except Exception as e: logger.error(f"OpenAI API call failed. Error: {e}") @@ -141,7 +140,7 @@ def __call__(self, docs: List[str], truncate: bool = True) -> List[List[float]]: or not embeds.data ): logger.info(f"Returned embeddings: {embeds}") - raise ValueError(f"No embeddings returned.") + raise ValueError("No embeddings returned.") embeddings = [embeds_obj.embedding for embeds_obj in embeds.data] return embeddings @@ -163,7 +162,6 @@ async def acall(self, docs: List[str], truncate: bool = True) -> List[List[float if self.async_client is None: raise ValueError("OpenAI async client is not initialized.") embeds = None - error_message = "" if truncate: # check if any document exceeds token limit and truncate if so @@ -184,7 +182,9 @@ async def acall(self, docs: List[str], truncate: bool = True) -> List[List[float logger.error("Exception occurred", exc_info=True) if self.max_retries != 0: await asleep(2**j) - logger.warning(f"Retrying in {2**j} seconds due to OpenAIError: {e}") + logger.warning( + f"Retrying in {2**j} seconds due to OpenAIError: {e}" + ) except Exception as e: logger.error(f"OpenAI API call failed. Error: {e}") raise ValueError(f"OpenAI API call failed. Error: {e}") from e @@ -195,7 +195,7 @@ async def acall(self, docs: List[str], truncate: bool = True) -> List[List[float or not embeds.data ): logger.info(f"Returned embeddings: {embeds}") - raise ValueError(f"No embeddings returned.") + raise ValueError("No embeddings returned.") embeddings = [embeds_obj.embedding for embeds_obj in embeds.data] return embeddings diff --git a/semantic_router/encoders/zure.py b/semantic_router/encoders/zure.py index 3c199692..e04b55bf 100644 --- a/semantic_router/encoders/zure.py +++ b/semantic_router/encoders/zure.py @@ -23,7 +23,7 @@ class AzureOpenAIEncoder(BaseEncoder): azure_endpoint: Optional[str] = None api_version: Optional[str] = None model: Optional[str] = None - max_retries: int + max_retries: int = 3 def __init__( self, @@ -39,10 +39,7 @@ def __init__( name = deployment_name if name is None: name = EncoderDefault.AZURE.value["embedding_model"] - - max_retries = max_retries if max_retries is not None else 3 - - super().__init__(name=name, score_threshold=score_threshold, max_retries=max_retries) + super().__init__(name=name, score_threshold=score_threshold) self.api_key = api_key self.deployment_name = deployment_name self.azure_endpoint = azure_endpoint @@ -54,6 +51,8 @@ def __init__( self.api_key = os.getenv("AZURE_OPENAI_API_KEY") if self.api_key is None: raise ValueError("No Azure OpenAI API key provided.") + if max_retries is not None: + self.max_retries = max_retries if self.deployment_name is None: self.deployment_name = EncoderDefault.AZURE.value["deployment_name"] # deployment_name may still be None, but it is optional in the API @@ -102,7 +101,6 @@ def __call__(self, docs: List[str]) -> List[List[float]]: if self.client is None: raise ValueError("Azure OpenAI client is not initialized.") embeds = None - error_message = "" # Exponential backoff for j in range(self.max_retries + 1): @@ -119,7 +117,9 @@ def __call__(self, docs: List[str]) -> List[List[float]]: logger.error("Exception occurred", exc_info=True) if self.max_retries != 0: sleep(2**j) - logger.warning(f"Retrying in {2**j} seconds due to OpenAIError: {e}") + logger.warning( + f"Retrying in {2**j} seconds due to OpenAIError: {e}" + ) except Exception as e: logger.error(f"Azure OpenAI API call failed. Error: {e}") raise ValueError(f"Azure OpenAI API call failed. Error: {e}") from e @@ -129,7 +129,7 @@ def __call__(self, docs: List[str]) -> List[List[float]]: or not isinstance(embeds, CreateEmbeddingResponse) or not embeds.data ): - raise ValueError(f"No embeddings returned.") + raise ValueError("No embeddings returned.") embeddings = [embeds_obj.embedding for embeds_obj in embeds.data] return embeddings @@ -138,7 +138,6 @@ async def acall(self, docs: List[str]) -> List[List[float]]: if self.async_client is None: raise ValueError("Azure OpenAI async client is not initialized.") embeds = None - error_message = "" # Exponential backoff for j in range(self.max_retries + 1): @@ -156,7 +155,9 @@ async def acall(self, docs: List[str]) -> List[List[float]]: logger.error("Exception occurred", exc_info=True) if self.max_retries != 0: await asleep(2**j) - logger.warning(f"Retrying in {2**j} seconds due to OpenAIError: {e}") + logger.warning( + f"Retrying in {2**j} seconds due to OpenAIError: {e}" + ) except Exception as e: logger.error(f"Azure OpenAI API call failed. Error: {e}") raise ValueError(f"Azure OpenAI API call failed. Error: {e}") from e @@ -166,7 +167,7 @@ async def acall(self, docs: List[str]) -> List[List[float]]: or not isinstance(embeds, CreateEmbeddingResponse) or not embeds.data ): - raise ValueError(f"No embeddings returned.") + raise ValueError("No embeddings returned.") embeddings = [embeds_obj.embedding for embeds_obj in embeds.data] return embeddings From 91da241317caeadf1e22e24f6c7410b7a174d4fc Mon Sep 17 00:00:00 2001 From: Ismail Ashraq Date: Sun, 18 Aug 2024 16:35:40 +0500 Subject: [PATCH 03/10] remove test code --- semantic_router/encoders/openai.py | 2 -- semantic_router/encoders/zure.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/semantic_router/encoders/openai.py b/semantic_router/encoders/openai.py index 8ba7ee5b..6f80f257 100644 --- a/semantic_router/encoders/openai.py +++ b/semantic_router/encoders/openai.py @@ -114,7 +114,6 @@ def __call__(self, docs: List[str], truncate: bool = True) -> List[List[float]]: # Exponential backoff for j in range(self.max_retries + 1): try: - raise OpenAIError("Test") embeds = self.client.embeddings.create( input=docs, model=self.name, @@ -170,7 +169,6 @@ async def acall(self, docs: List[str], truncate: bool = True) -> List[List[float # Exponential backoff for j in range(self.max_retries + 1): try: - raise OpenAIError("Test") embeds = await self.async_client.embeddings.create( input=docs, model=self.name, diff --git a/semantic_router/encoders/zure.py b/semantic_router/encoders/zure.py index e04b55bf..ffb0d4b3 100644 --- a/semantic_router/encoders/zure.py +++ b/semantic_router/encoders/zure.py @@ -105,7 +105,6 @@ def __call__(self, docs: List[str]) -> List[List[float]]: # Exponential backoff for j in range(self.max_retries + 1): try: - raise OpenAIError("Test") embeds = self.client.embeddings.create( input=docs, model=str(self.model), @@ -142,7 +141,6 @@ async def acall(self, docs: List[str]) -> List[List[float]]: # Exponential backoff for j in range(self.max_retries + 1): try: - raise OpenAIError("Test") embeds = await self.async_client.embeddings.create( input=docs, model=str(self.model), From a45045146f2025c1affccb545d40072cb7e785fe Mon Sep 17 00:00:00 2001 From: Ismail Ashraq Date: Sun, 18 Aug 2024 16:39:57 +0500 Subject: [PATCH 04/10] update unit tests --- tests/unit/encoders/test_azure.py | 2 +- tests/unit/encoders/test_openai.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/encoders/test_azure.py b/tests/unit/encoders/test_azure.py index 93dffb89..3969543e 100644 --- a/tests/unit/encoders/test_azure.py +++ b/tests/unit/encoders/test_azure.py @@ -83,7 +83,7 @@ def test_openai_encoder_call_with_retries(self, openai_encoder, mocker): ) with pytest.raises(ValueError) as e: openai_encoder(["test document"]) - assert "No embeddings returned. Error" in str(e.value) + assert "No embeddings returned." in str(e.value) def test_openai_encoder_call_failure_non_openai_error(self, openai_encoder, mocker): mocker.patch("os.getenv", return_value="fake-api-key") diff --git a/tests/unit/encoders/test_openai.py b/tests/unit/encoders/test_openai.py index 508e9e9e..56ef2b38 100644 --- a/tests/unit/encoders/test_openai.py +++ b/tests/unit/encoders/test_openai.py @@ -77,7 +77,7 @@ def test_openai_encoder_call_with_retries(self, openai_encoder, mocker): ) with pytest.raises(ValueError) as e: openai_encoder(["test document"]) - assert "No embeddings returned. Error" in str(e.value) + assert "No embeddings returned." in str(e.value) def test_openai_encoder_call_failure_non_openai_error(self, openai_encoder, mocker): mocker.patch("os.getenv", return_value="fake-api-key") From 7c6bf69c996da7e533f8c0319801b1e8c141e724 Mon Sep 17 00:00:00 2001 From: Ismail Ashraq Date: Mon, 19 Aug 2024 11:00:39 +0500 Subject: [PATCH 05/10] raise openai errors --- semantic_router/encoders/openai.py | 9 +++++++-- semantic_router/encoders/zure.py | 8 ++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/semantic_router/encoders/openai.py b/semantic_router/encoders/openai.py index 6f80f257..db011956 100644 --- a/semantic_router/encoders/openai.py +++ b/semantic_router/encoders/openai.py @@ -123,11 +123,13 @@ def __call__(self, docs: List[str], truncate: bool = True) -> List[List[float]]: break except OpenAIError as e: logger.error("Exception occurred", exc_info=True) - if self.max_retries != 0: + if self.max_retries != 0 and j < self.max_retries: sleep(2**j) logger.warning( f"Retrying in {2**j} seconds due to OpenAIError: {e}" ) + else: + raise except Exception as e: logger.error(f"OpenAI API call failed. Error: {e}") @@ -178,11 +180,14 @@ async def acall(self, docs: List[str], truncate: bool = True) -> List[List[float break except OpenAIError as e: logger.error("Exception occurred", exc_info=True) - if self.max_retries != 0: + if self.max_retries != 0 and j < self.max_retries: await asleep(2**j) logger.warning( f"Retrying in {2**j} seconds due to OpenAIError: {e}" ) + else: + raise + except Exception as e: logger.error(f"OpenAI API call failed. Error: {e}") raise ValueError(f"OpenAI API call failed. Error: {e}") from e diff --git a/semantic_router/encoders/zure.py b/semantic_router/encoders/zure.py index ffb0d4b3..4caded0f 100644 --- a/semantic_router/encoders/zure.py +++ b/semantic_router/encoders/zure.py @@ -114,11 +114,13 @@ def __call__(self, docs: List[str]) -> List[List[float]]: break except OpenAIError as e: logger.error("Exception occurred", exc_info=True) - if self.max_retries != 0: + if self.max_retries != 0 and j < self.max_retries: sleep(2**j) logger.warning( f"Retrying in {2**j} seconds due to OpenAIError: {e}" ) + else: + raise except Exception as e: logger.error(f"Azure OpenAI API call failed. Error: {e}") raise ValueError(f"Azure OpenAI API call failed. Error: {e}") from e @@ -151,11 +153,13 @@ async def acall(self, docs: List[str]) -> List[List[float]]: except OpenAIError as e: logger.error("Exception occurred", exc_info=True) - if self.max_retries != 0: + if self.max_retries != 0 and j < self.max_retries: await asleep(2**j) logger.warning( f"Retrying in {2**j} seconds due to OpenAIError: {e}" ) + else: + raise except Exception as e: logger.error(f"Azure OpenAI API call failed. Error: {e}") raise ValueError(f"Azure OpenAI API call failed. Error: {e}") from e From 79729e1fe4f881989d499ecde31854314fe79469 Mon Sep 17 00:00:00 2001 From: Ismail Ashraq Date: Mon, 19 Aug 2024 11:02:21 +0500 Subject: [PATCH 06/10] linting --- semantic_router/encoders/openai.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/semantic_router/encoders/openai.py b/semantic_router/encoders/openai.py index db011956..ec39e828 100644 --- a/semantic_router/encoders/openai.py +++ b/semantic_router/encoders/openai.py @@ -187,7 +187,7 @@ async def acall(self, docs: List[str], truncate: bool = True) -> List[List[float ) else: raise - + except Exception as e: logger.error(f"OpenAI API call failed. Error: {e}") raise ValueError(f"OpenAI API call failed. Error: {e}") from e From 8016fa11e6e6dbbafea6a260c0d624820372016f Mon Sep 17 00:00:00 2001 From: Ismail Ashraq Date: Mon, 19 Aug 2024 15:14:28 +0500 Subject: [PATCH 07/10] unit tests for retry logic --- tests/unit/encoders/test_azure.py | 162 ++++++++++++++++++++++++---- tests/unit/encoders/test_openai.py | 163 +++++++++++++++++++++++++---- 2 files changed, 288 insertions(+), 37 deletions(-) diff --git a/tests/unit/encoders/test_azure.py b/tests/unit/encoders/test_azure.py index 3969543e..5a6841d7 100644 --- a/tests/unit/encoders/test_azure.py +++ b/tests/unit/encoders/test_azure.py @@ -1,4 +1,5 @@ import pytest +from unittest.mock import AsyncMock, Mock, patch from openai import OpenAIError from openai.types import CreateEmbeddingResponse, Embedding from openai.types.create_embedding_response import Usage @@ -7,14 +8,26 @@ @pytest.fixture -def openai_encoder(mocker): - mocker.patch("openai.Client") +def mock_openai_client(): + with patch("openai.AzureOpenAI") as mock_client: + yield mock_client + + +@pytest.fixture +def mock_openai_async_client(): + with patch("openai.AsyncAzureOpenAI") as mock_async_client: + yield mock_async_client + + +@pytest.fixture +def openai_encoder(mock_openai_client, mock_openai_async_client): return AzureOpenAIEncoder( api_key="test_api_key", deployment_name="test-deployment", azure_endpoint="test_endpoint", api_version="test_version", model="test_model", + max_retries=2, ) @@ -70,21 +83,10 @@ def test_openai_encoder_call_success(self, openai_encoder, mocker): mocker.patch.object( openai_encoder.client.embeddings, "create", side_effect=responses ) - embeddings = openai_encoder(["test document"]) + with patch("semantic_router.encoders.zure.sleep", return_value=None): + embeddings = openai_encoder(["test document"]) assert embeddings == [[0.1, 0.2]] - def test_openai_encoder_call_with_retries(self, openai_encoder, mocker): - mocker.patch("os.getenv", return_value="fake-api-key") - mocker.patch("time.sleep", return_value=None) # To speed up the test - mocker.patch.object( - openai_encoder.client.embeddings, - "create", - side_effect=OpenAIError("Test error"), - ) - with pytest.raises(ValueError) as e: - openai_encoder(["test document"]) - assert "No embeddings returned." in str(e.value) - def test_openai_encoder_call_failure_non_openai_error(self, openai_encoder, mocker): mocker.patch("os.getenv", return_value="fake-api-key") mocker.patch("time.sleep", return_value=None) # To speed up the test @@ -93,8 +95,9 @@ def test_openai_encoder_call_failure_non_openai_error(self, openai_encoder, mock "create", side_effect=Exception("Non-OpenAIError"), ) - with pytest.raises(ValueError) as e: - openai_encoder(["test document"]) + with patch("semantic_router.encoders.zure.sleep", return_value=None): + with pytest.raises(ValueError) as e: + openai_encoder(["test document"]) assert "OpenAI API call failed. Error: Non-OpenAIError" in str(e.value) @@ -120,5 +123,128 @@ def test_openai_encoder_call_successful_retry(self, openai_encoder, mocker): mocker.patch.object( openai_encoder.client.embeddings, "create", side_effect=responses ) - embeddings = openai_encoder(["test document"]) + with patch("semantic_router.encoders.zure.sleep", return_value=None): + embeddings = openai_encoder(["test document"]) assert embeddings == [[0.1, 0.2]] + + def test_retry_logic_sync(self, openai_encoder, mock_openai_client, mocker): + # Mock the embeddings.create method to raise an error twice, then succeed + mock_create = Mock( + side_effect=[ + OpenAIError("API error"), + OpenAIError("API error"), + CreateEmbeddingResponse( + data=[ + Embedding( + embedding=[0.1, 0.2, 0.3], index=0, object="embedding" + ) + ], + model="text-embedding-3-small", + object="list", + usage={"prompt_tokens": 5, "total_tokens": 5}, + ), + ] + ) + mock_openai_client.return_value.embeddings.create = mock_create + mocker.patch("time.sleep", return_value=None) # To speed up the test + + # Patch the sleep function in the encoder module to avoid actual sleep + with patch("semantic_router.encoders.zure.sleep", return_value=None): + result = openai_encoder(["test document"]) + + assert result == [[0.1, 0.2, 0.3]] + assert mock_create.call_count == 3 + + def test_no_retry_on_max_retries_zero(self, openai_encoder, mock_openai_client): + openai_encoder.max_retries = 0 + # Mock the embeddings.create method to always raise an error + mock_create = Mock(side_effect=OpenAIError("API error")) + mock_openai_client.return_value.embeddings.create = mock_create + + with pytest.raises(OpenAIError): + openai_encoder(["test document"]) + + assert mock_create.call_count == 1 # Only the initial attempt, no retries + + def test_retry_logic_sync_max_retries_exceeded( + self, openai_encoder, mock_openai_client, mocker + ): + # Mock the embeddings.create method to always raise an error + mock_create = Mock(side_effect=OpenAIError("API error")) + mock_openai_client.return_value.embeddings.create = mock_create + mocker.patch("time.sleep", return_value=None) # To speed up the test + + # Patch the sleep function in the encoder module to avoid actual sleep + with patch("semantic_router.encoders.zure.sleep", return_value=None): + with pytest.raises(OpenAIError): + openai_encoder(["test document"]) + + assert mock_create.call_count == 3 # Initial attempt + 2 retries + + @pytest.mark.asyncio + async def test_retry_logic_async( + self, openai_encoder, mock_openai_async_client, mocker + ): + # Set up the mock to fail twice, then succeed + mock_create = AsyncMock( + side_effect=[ + OpenAIError("API error"), + OpenAIError("API error"), + CreateEmbeddingResponse( + data=[ + Embedding( + embedding=[0.1, 0.2, 0.3], index=0, object="embedding" + ) + ], + model="text-embedding-3-small", + object="list", + usage={"prompt_tokens": 5, "total_tokens": 5}, + ), + ] + ) + mock_openai_async_client.return_value.embeddings.create = mock_create + mocker.patch("asyncio.sleep", return_value=None) # To speed up the test + + # Patch the asleep function in the encoder module to avoid actual sleep + with patch("semantic_router.encoders.zure.asleep", return_value=None): + result = await openai_encoder.acall(["test document"]) + + assert result == [[0.1, 0.2, 0.3]] + assert mock_create.call_count == 3 + + @pytest.mark.asyncio + async def test_retry_logic_async_max_retries_exceeded( + self, openai_encoder, mock_openai_async_client, mocker + ): + # Mock the embeddings.create method to always raise an error + async def raise_error(*args, **kwargs): + raise OpenAIError("API error") + + mock_create = Mock(side_effect=raise_error) + mock_openai_async_client.return_value.embeddings.create = mock_create + mocker.patch("asyncio.sleep", return_value=None) # To speed up the test + + # Patch the asleep function in the encoder module to avoid actual sleep + with patch("semantic_router.encoders.zure.asleep", return_value=None): + with pytest.raises(OpenAIError): + await openai_encoder.acall(["test document"]) + + assert mock_create.call_count == 3 # Initial attempt + 2 retries + + @pytest.mark.asyncio + async def test_no_retry_on_max_retries_zero_async( + self, openai_encoder, mock_openai_async_client + ): + openai_encoder.max_retries = 0 + + # Mock the embeddings.create method to always raise an error + async def raise_error(*args, **kwargs): + raise OpenAIError("API error") + + mock_create = AsyncMock(side_effect=raise_error) + mock_openai_async_client.return_value.embeddings.create = mock_create + + with pytest.raises(OpenAIError): + await openai_encoder.acall(["test document"]) + + assert mock_create.call_count == 1 # Only the initial attempt, no retries diff --git a/tests/unit/encoders/test_openai.py b/tests/unit/encoders/test_openai.py index 56ef2b38..538e9692 100644 --- a/tests/unit/encoders/test_openai.py +++ b/tests/unit/encoders/test_openai.py @@ -1,4 +1,5 @@ import pytest +from unittest.mock import AsyncMock, Mock, patch from openai import OpenAIError from openai.types import CreateEmbeddingResponse, Embedding from openai.types.create_embedding_response import Usage @@ -7,9 +8,20 @@ @pytest.fixture -def openai_encoder(mocker): - mocker.patch("openai.Client") - return OpenAIEncoder(openai_api_key="test_api_key") +def mock_openai_client(): + with patch("openai.Client") as mock_client: + yield mock_client + + +@pytest.fixture +def mock_openai_async_client(): + with patch("openai.AsyncClient") as mock_async_client: + yield mock_async_client + + +@pytest.fixture +def openai_encoder(mock_openai_client, mock_openai_async_client): + return OpenAIEncoder(openai_api_key="fake_key", max_retries=2) class TestOpenAIEncoder: @@ -64,21 +76,10 @@ def test_openai_encoder_call_success(self, openai_encoder, mocker): mocker.patch.object( openai_encoder.client.embeddings, "create", side_effect=responses ) - embeddings = openai_encoder(["test document"]) + with patch("semantic_router.encoders.openai.sleep", return_value=None): + embeddings = openai_encoder(["test document"]) assert embeddings == [[0.1, 0.2]] - def test_openai_encoder_call_with_retries(self, openai_encoder, mocker): - mocker.patch("os.getenv", return_value="fake-api-key") - mocker.patch("time.sleep", return_value=None) # To speed up the test - mocker.patch.object( - openai_encoder.client.embeddings, - "create", - side_effect=OpenAIError("Test error"), - ) - with pytest.raises(ValueError) as e: - openai_encoder(["test document"]) - assert "No embeddings returned." in str(e.value) - def test_openai_encoder_call_failure_non_openai_error(self, openai_encoder, mocker): mocker.patch("os.getenv", return_value="fake-api-key") mocker.patch("time.sleep", return_value=None) # To speed up the test @@ -87,8 +88,9 @@ def test_openai_encoder_call_failure_non_openai_error(self, openai_encoder, mock "create", side_effect=Exception("Non-OpenAIError"), ) - with pytest.raises(ValueError) as e: - openai_encoder(["test document"]) + with patch("semantic_router.encoders.openai.sleep", return_value=None): + with pytest.raises(ValueError) as e: + openai_encoder(["test document"]) assert "OpenAI API call failed. Error: Non-OpenAIError" in str(e.value) @@ -114,5 +116,128 @@ def test_openai_encoder_call_successful_retry(self, openai_encoder, mocker): mocker.patch.object( openai_encoder.client.embeddings, "create", side_effect=responses ) - embeddings = openai_encoder(["test document"]) + with patch("semantic_router.encoders.openai.sleep", return_value=None): + embeddings = openai_encoder(["test document"]) assert embeddings == [[0.1, 0.2]] + + def test_retry_logic_sync(self, openai_encoder, mock_openai_client, mocker): + # Mock the embeddings.create method to raise an error twice, then succeed + mock_create = Mock( + side_effect=[ + OpenAIError("API error"), + OpenAIError("API error"), + CreateEmbeddingResponse( + data=[ + Embedding( + embedding=[0.1, 0.2, 0.3], index=0, object="embedding" + ) + ], + model="text-embedding-3-small", + object="list", + usage={"prompt_tokens": 5, "total_tokens": 5}, + ), + ] + ) + mock_openai_client.return_value.embeddings.create = mock_create + mocker.patch("time.sleep", return_value=None) # To speed up the test + + # Patch the sleep function in the encoder module to avoid actual sleep + with patch("semantic_router.encoders.openai.sleep", return_value=None): + result = openai_encoder(["test document"]) + + assert result == [[0.1, 0.2, 0.3]] + assert mock_create.call_count == 3 + + def test_no_retry_on_max_retries_zero(self, openai_encoder, mock_openai_client): + openai_encoder.max_retries = 0 + # Mock the embeddings.create method to always raise an error + mock_create = Mock(side_effect=OpenAIError("API error")) + mock_openai_client.return_value.embeddings.create = mock_create + + with pytest.raises(OpenAIError): + openai_encoder(["test document"]) + + assert mock_create.call_count == 1 # Only the initial attempt, no retries + + def test_retry_logic_sync_max_retries_exceeded( + self, openai_encoder, mock_openai_client, mocker + ): + # Mock the embeddings.create method to always raise an error + mock_create = Mock(side_effect=OpenAIError("API error")) + mock_openai_client.return_value.embeddings.create = mock_create + mocker.patch("time.sleep", return_value=None) # To speed up the test + + # Patch the sleep function in the encoder module to avoid actual sleep + with patch("semantic_router.encoders.openai.sleep", return_value=None): + with pytest.raises(OpenAIError): + openai_encoder(["test document"]) + + assert mock_create.call_count == 3 # Initial attempt + 2 retries + + @pytest.mark.asyncio + async def test_retry_logic_async( + self, openai_encoder, mock_openai_async_client, mocker + ): + # Set up the mock to fail twice, then succeed + mock_create = AsyncMock( + side_effect=[ + OpenAIError("API error"), + OpenAIError("API error"), + CreateEmbeddingResponse( + data=[ + Embedding( + embedding=[0.1, 0.2, 0.3], index=0, object="embedding" + ) + ], + model="text-embedding-3-small", + object="list", + usage={"prompt_tokens": 5, "total_tokens": 5}, + ), + ] + ) + mock_openai_async_client.return_value.embeddings.create = mock_create + mocker.patch("asyncio.sleep", return_value=None) # To speed up the test + + # Patch the asleep function in the encoder module to avoid actual sleep + with patch("semantic_router.encoders.openai.asleep", return_value=None): + result = await openai_encoder.acall(["test document"]) + + assert result == [[0.1, 0.2, 0.3]] + assert mock_create.call_count == 3 + + @pytest.mark.asyncio + async def test_retry_logic_async_max_retries_exceeded( + self, openai_encoder, mock_openai_async_client, mocker + ): + # Mock the embeddings.create method to always raise an error + async def raise_error(*args, **kwargs): + raise OpenAIError("API error") + + mock_create = Mock(side_effect=raise_error) + mock_openai_async_client.return_value.embeddings.create = mock_create + mocker.patch("asyncio.sleep", return_value=None) # To speed up the test + + # Patch the asleep function in the encoder module to avoid actual sleep + with patch("semantic_router.encoders.openai.asleep", return_value=None): + with pytest.raises(OpenAIError): + await openai_encoder.acall(["test document"]) + + assert mock_create.call_count == 3 # Initial attempt + 2 retries + + @pytest.mark.asyncio + async def test_no_retry_on_max_retries_zero_async( + self, openai_encoder, mock_openai_async_client + ): + openai_encoder.max_retries = 0 + + # Mock the embeddings.create method to always raise an error + async def raise_error(*args, **kwargs): + raise OpenAIError("API error") + + mock_create = AsyncMock(side_effect=raise_error) + mock_openai_async_client.return_value.embeddings.create = mock_create + + with pytest.raises(OpenAIError): + await openai_encoder.acall(["test document"]) + + assert mock_create.call_count == 1 # Only the initial attempt, no retries From fb78955154fd04425992810b616e82c5266ddb87 Mon Sep 17 00:00:00 2001 From: Ismail Ashraq Date: Mon, 19 Aug 2024 15:43:28 +0500 Subject: [PATCH 08/10] fix test error --- semantic_router/encoders/openai.py | 2 +- semantic_router/encoders/zure.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/semantic_router/encoders/openai.py b/semantic_router/encoders/openai.py index ec39e828..4bc86ac2 100644 --- a/semantic_router/encoders/openai.py +++ b/semantic_router/encoders/openai.py @@ -52,7 +52,7 @@ def __init__( openai_org_id: Optional[str] = None, score_threshold: Optional[float] = None, dimensions: Union[int, NotGiven] = NotGiven(), - max_retries: int | None = None, + max_retries: int = 3, ): if name is None: name = EncoderDefault.OPENAI.value["embedding_model"] diff --git a/semantic_router/encoders/zure.py b/semantic_router/encoders/zure.py index 4caded0f..d6f65660 100644 --- a/semantic_router/encoders/zure.py +++ b/semantic_router/encoders/zure.py @@ -34,7 +34,7 @@ def __init__( model: Optional[str] = None, # TODO we should change to `name` JB score_threshold: float = 0.82, dimensions: Union[int, NotGiven] = NotGiven(), - max_retries: int | None = None, + max_retries: int = 3, ): name = deployment_name if name is None: From f2936780257e75ed3d974058ddbaa067ab0e40f2 Mon Sep 17 00:00:00 2001 From: Ismail Ashraq Date: Mon, 19 Aug 2024 16:05:03 +0500 Subject: [PATCH 09/10] fix tests --- tests/integration/encoders/test_openai_integration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/encoders/test_openai_integration.py b/tests/integration/encoders/test_openai_integration.py index a8b59281..47e617a5 100644 --- a/tests/integration/encoders/test_openai_integration.py +++ b/tests/integration/encoders/test_openai_integration.py @@ -1,5 +1,6 @@ import os import pytest +from openai import OpenAIError from semantic_router.encoders.base import BaseEncoder from semantic_router.encoders.openai import OpenAIEncoder @@ -40,7 +41,7 @@ def test_openai_encoder_call_truncation(self, openai_encoder): os.environ.get("OPENAI_API_KEY") is None, reason="OpenAI API key required" ) def test_openai_encoder_call_no_truncation(self, openai_encoder): - with pytest.raises(ValueError) as _: + with pytest.raises(OpenAIError) as _: # default truncation is True openai_encoder([long_doc], truncate=False) From e1ef5a035800951b5aac9217ef136b0257b8ab8f Mon Sep 17 00:00:00 2001 From: James Briggs Date: Mon, 19 Aug 2024 14:59:33 +0200 Subject: [PATCH 10/10] fix: test --- semantic_router/utils/function_call.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/semantic_router/utils/function_call.py b/semantic_router/utils/function_call.py index 3ae14043..d03c0798 100644 --- a/semantic_router/utils/function_call.py +++ b/semantic_router/utils/function_call.py @@ -77,7 +77,11 @@ def to_ollama(self): "type": "object", "properties": { param.name: { - "description": param.description, + "description": ( + param.description + if isinstance(param.description, str) + else None + ), "type": self._ollama_type_mapping(param.type), } for param in self.parameters