diff --git a/poetry.lock b/poetry.lock index d5e1153b3e2611cb158cf0184a4b1a37c36e982d..361cedea80e4a8bf5fe4bd6af59df6819c15ba2d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3218,7 +3218,7 @@ files = [ name = "types-requests" version = "2.31.0.20240125" description = "Typing stubs for requests" -optional = true +optional = false python-versions = ">=3.8" files = [ {file = "types-requests-2.31.0.20240125.tar.gz", hash = "sha256:03a28ce1d7cd54199148e043b2079cdded22d6795d19a2c2a6791a4b2b5e2eb5"}, @@ -3404,4 +3404,4 @@ pinecone = ["pinecone-client"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "52ce34492a7d4827a3c2b96332e7285369209dbe9ec2a9488d8eac2c13d4d0c6" +content-hash = "23b8995b7eee4df19bb2242d6a81de8557a855053b4346a532efa63be2ea303f" diff --git a/pyproject.toml b/pyproject.toml index 1491b6d7a267fa493e93162e8c55e924b7003ee8..e1137235108ab96511a5b6dc3102da0784715331 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ pytest-cov = "^4.1.0" pytest-xdist = "^3.5.0" mypy = "^1.7.1" types-pyyaml = "^6.0.12.12" +types-requests = "^2.31.0" [build-system] requires = ["poetry-core"] diff --git a/semantic_router/index/base.py b/semantic_router/index/base.py index 37550281e77acd87ad180dc025452fcfba5fe21d..c3c9ed0fb740e7195e723965a23dddedeb2948bc 100644 --- a/semantic_router/index/base.py +++ b/semantic_router/index/base.py @@ -18,7 +18,9 @@ class BaseIndex(BaseModel): dimensions: Union[int, None] = None type: str = "base" - def add(self, embeddings: List[float], routes: List[str], utterances: List[str]): + def add( + self, embeddings: List[List[float]], routes: List[str], utterances: List[str] + ): """ Add embeddings to the index. This method should be implemented by subclasses. @@ -32,20 +34,20 @@ class BaseIndex(BaseModel): """ raise NotImplementedError("This method should be implemented by subclasses.") - def describe(self) -> bool: + def describe(self) -> dict: """ Returns a dictionary with index details such as type, dimensions, and total vector count. This method should be implemented by subclasses. """ raise NotImplementedError("This method should be implemented by subclasses.") - def query(self, vector: np.ndarray, top_k: int = 5) -> Tuple[np.ndarray, np.ndarray]: + def query(self, vector: np.ndarray, top_k: int = 5) -> Tuple[np.ndarray, List[str]]: """ Search the index for the query_vector and return top_k results. This method should be implemented by subclasses. """ raise NotImplementedError("This method should be implemented by subclasses.") - + def delete_index(self): """ Deletes or resets the index. @@ -53,6 +55,5 @@ class BaseIndex(BaseModel): """ raise NotImplementedError("This method should be implemented by subclasses.") - class Config: arbitrary_types_allowed = True diff --git a/semantic_router/index/local.py b/semantic_router/index/local.py index 72c474ae7de395d19a9b472e03d6b2c57a1804c0..02a1f209fc17052e1c8fc52c6198347a347e80de 100644 --- a/semantic_router/index/local.py +++ b/semantic_router/index/local.py @@ -1,19 +1,23 @@ import numpy as np -from typing import List, Any, Tuple, Optional +from typing import List, Tuple, Optional from semantic_router.linear import similarity_matrix, top_scores from semantic_router.index.base import BaseIndex class LocalIndex(BaseIndex): - def __init__(self): - super().__init__() + super().__init__() self.type = "local" + self.index: Optional[np.ndarray] = None + self.routes: Optional[np.ndarray] = None + self.utterances: Optional[np.ndarray] = None class Config: # Stop pydantic from complaining about Optional[np.ndarray] type hints. arbitrary_types_allowed = True - def add(self, embeddings: List[List[float]], routes: List[str], utterances: List[str]): + def add( + self, embeddings: List[List[float]], routes: List[str], utterances: List[str] + ): embeds = np.array(embeddings) # type: ignore routes_arr = np.array(routes) utterances_arr = np.array(utterances) @@ -27,44 +31,50 @@ class LocalIndex(BaseIndex): self.utterances = np.concatenate([self.utterances, utterances_arr]) def _get_indices_for_route(self, route_name: str): - """Gets an array of indices for a specific route. - """ - idx = [ - i for i, route in enumerate(self.routes) - if route == route_name - ] + """Gets an array of indices for a specific route.""" + if self.routes is None: + raise ValueError("Routes are not populated.") + idx = [i for i, route in enumerate(self.routes) if route == route_name] return idx def delete(self, route_name: str): """ Delete all records of a specific route from the index. """ - if self.index is not None: + if ( + self.index is not None + and self.routes is not None + and self.utterances is not None + ): delete_idx = self._get_indices_for_route(route_name=route_name) self.index = np.delete(self.index, delete_idx, axis=0) self.routes = np.delete(self.routes, delete_idx, axis=0) self.utterances = np.delete(self.utterances, delete_idx, axis=0) + else: + raise ValueError( + "Attempted to delete route records but either indx, routes or utterances is None." + ) - def describe(self): + def describe(self) -> dict: return { "type": self.type, "dimensions": self.index.shape[1] if self.index is not None else 0, - "vectors": self.index.shape[0] if self.index is not None else 0 + "vectors": self.index.shape[0] if self.index is not None else 0, } def query(self, vector: np.ndarray, top_k: int = 5) -> Tuple[np.ndarray, List[str]]: """ Search the index for the query and return top_k results. """ - if self.index is None: - raise ValueError("Index is not populated.") + if self.index is None or self.routes is None: + raise ValueError("Index or routes are not populated.") sim = similarity_matrix(vector, self.index) # extract the index values of top scoring vectors scores, idx = top_scores(sim, top_k) # get routes from index values route_names = self.routes[idx].copy() return scores, route_names - + def delete_index(self): """ Deletes the index, effectively clearing it and setting it to None. diff --git a/semantic_router/index/pinecone.py b/semantic_router/index/pinecone.py index 391fb9a65b5ceb46627006b3b666e1328784cfe1..d2b21d95f9ff176493f91d91050eed204125aa45 100644 --- a/semantic_router/index/pinecone.py +++ b/semantic_router/index/pinecone.py @@ -12,6 +12,7 @@ import numpy as np def clean_route_name(route_name: str) -> str: return route_name.strip().replace(" ", "-") + class PineconeRecord(BaseModel): id: str = "" values: List[float] @@ -29,10 +30,7 @@ class PineconeRecord(BaseModel): return { "id": self.id, "values": self.values, - "metadata": { - "sr_route": self.route, - "sr_utterance": self.utterance - } + "metadata": {"sr_route": self.route, "sr_utterance": self.utterance}, } @@ -42,14 +40,14 @@ class PineconeIndex(BaseIndex): dimensions: Union[int, None] = None metric: str = "cosine" cloud: str = "aws" - region: str = "us-west-2" + region: str = "us-west-2" host: str = "" client: Any = Field(default=None, exclude=True) index: Optional[Any] = Field(default=None, exclude=True) ServerlessSpec: Any = Field(default=None, exclude=True) def __init__(self, **data): - super().__init__(**data) + super().__init__(**data) self._initialize_client() self.type = "pinecone" @@ -62,6 +60,7 @@ class PineconeIndex(BaseIndex): def _initialize_client(self, api_key: Optional[str] = None): try: from pinecone import Pinecone, ServerlessSpec + self.ServerlessSpec = ServerlessSpec except ImportError: raise ImportError( @@ -81,13 +80,10 @@ class PineconeIndex(BaseIndex): # if the index doesn't exist and we have dimension value # we create the index self.client.create_index( - name=self.index_name, - dimension=self.dimensions, + name=self.index_name, + dimension=self.dimensions, metric=self.metric, - spec=self.ServerlessSpec( - cloud=self.cloud, - region=self.region - ) + spec=self.ServerlessSpec(cloud=self.cloud, region=self.region), ) # wait for index to be created while not self.client.describe_index(self.index_name).status["ready"]: @@ -100,7 +96,9 @@ class PineconeIndex(BaseIndex): # grab the dimensions from the index self.dimensions = index.describe_index_stats()["dimension"] elif force_create and not dimensions_given: - raise ValueError("Cannot create an index without specifying the dimensions.") + raise ValueError( + "Cannot create an index without specifying the dimensions." + ) else: # if the index doesn't exist and we don't have the dimensions # we return None @@ -109,8 +107,10 @@ class PineconeIndex(BaseIndex): if index is not None: self.host = self.client.describe_index(self.index_name)["host"] return index - - def add(self, embeddings: List[List[float]], routes: List[str], utterances: List[str]): + + def add( + self, embeddings: List[List[float]], routes: List[str], utterances: List[str] + ): if self.index is None: self.dimensions = self.dimensions or len(embeddings[0]) # we set force_create to True as we MUST have an index to add data @@ -119,35 +119,46 @@ class PineconeIndex(BaseIndex): for vector, route, utterance in zip(embeddings, routes, utterances): record = PineconeRecord(values=vector, route=route, utterance=utterance) vectors_to_upsert.append(record.to_dict()) - self.index.upsert(vectors=vectors_to_upsert) + if self.index is not None: + self.index.upsert(vectors=vectors_to_upsert) + else: + raise ValueError("Index is None could not upsert.") def _get_route_vecs(self, route_name: str): clean_route = clean_route_name(route_name) res = requests.get( f"https://{self.host}/vectors/list?prefix={clean_route}#", - headers={"Api-Key": os.environ["PINECONE_API_KEY"]} + headers={"Api-Key": os.environ["PINECONE_API_KEY"]}, ) return [vec["id"] for vec in res.json()["vectors"]] def delete(self, route_name: str): route_vec_ids = self._get_route_vecs(route_name=route_name) - self.index.delete(ids=route_vec_ids) + if self.index is not None: + self.index.delete(ids=route_vec_ids) + else: + raise ValueError("Index is None, could not delete.") def delete_all(self): self.index.delete(delete_all=True) - def describe(self) -> bool: - stats = self.index.describe_index_stats() - return { - "type": self.type, - "dimensions": stats["dimension"], - "vectors": stats["total_vector_count"] - } - + def describe(self) -> dict: + if self.index is not None: + stats = self.index.describe_index_stats() + return { + "type": self.type, + "dimensions": stats["dimension"], + "vectors": stats["total_vector_count"], + } + else: + raise ValueError("Index is None, cannot describe index stats.") + def query(self, vector: np.ndarray, top_k: int = 5) -> Tuple[np.ndarray, List[str]]: + if self.index is None: + raise ValueError("Index is not populated.") query_vector_list = vector.tolist() results = self.index.query( - vector=[query_vector_list], + vector=[query_vector_list], top_k=top_k, include_metadata=True, ) @@ -159,4 +170,4 @@ class PineconeIndex(BaseIndex): self.client.delete_index(self.index_name) def __len__(self): - return self.index.describe_index_stats()["total_vector_count"] \ No newline at end of file + return self.index.describe_index_stats()["total_vector_count"] diff --git a/semantic_router/layer.py b/semantic_router/layer.py index 21d208f1d5a2c6813d5c24190f8c3bed4d24c2ae..573c24d669c69726d7091aee30e1d4f3da22d081 100644 --- a/semantic_router/layer.py +++ b/semantic_router/layer.py @@ -161,10 +161,10 @@ class RouteLayer: encoder: Optional[BaseEncoder] = None, llm: Optional[BaseLLM] = None, routes: Optional[List[Route]] = None, - index: Optional[BaseIndex] = LocalIndex, + index: Optional[BaseIndex] = LocalIndex, # type: ignore ): logger.info("local") - self.index: BaseIndex = index + self.index: BaseIndex = index if index is not None else LocalIndex() if encoder is None: logger.warning( "No encoder provided. Using default OpenAIEncoder. Ensure " @@ -283,13 +283,13 @@ class RouteLayer: def list_route_names(self) -> List[str]: return [route.name for route in self.routes] - + def update(self, route_name: str, utterances: List[str]): raise NotImplementedError("This method has not yet been implemented.") def delete(self, route_name: str): """Deletes a route given a specific route name. - + :param route_name: the name of the route to be deleted :type str: """ @@ -303,7 +303,9 @@ class RouteLayer: def _add_routes(self, routes: List[Route]): # create embeddings for all routes - all_utterances = [utterance for route in routes for utterance in route.utterances] + all_utterances = [ + utterance for route in routes for utterance in route.utterances + ] embedded_utterances = self.encoder(all_utterances) # create route array route_names = [route.name for route in routes for _ in route.utterances] @@ -311,7 +313,7 @@ class RouteLayer: self.index.add( embeddings=embedded_utterances, routes=route_names, - utterances=all_utterances + utterances=all_utterances, ) def _encode(self, text: str) -> Any: diff --git a/semantic_router/schema.py b/semantic_router/schema.py index 2ac7280a8c25398047b916bbbb495b24dcc22b72..46ee7f590c3275b314b9ecb3e52adc54f16012fe 100644 --- a/semantic_router/schema.py +++ b/semantic_router/schema.py @@ -11,9 +11,6 @@ from semantic_router.encoders import ( OpenAIEncoder, ) -from semantic_router.index.local import LocalIndex -from semantic_router.index.pinecone import PineconeIndex -from semantic_router.index.base import BaseIndex class EncoderType(Enum): HUGGINGFACE = "huggingface"