diff --git a/semantic_router/index/pinecone.py b/semantic_router/index/pinecone.py index 6086de1c5a3de8d18023c3488064399220a9f8a5..7b4dba53dba3c36946ff1d01c4441a2dc5f9c310 100644 --- a/semantic_router/index/pinecone.py +++ b/semantic_router/index/pinecone.py @@ -387,7 +387,9 @@ class PineconeIndex(BaseIndex): if sparse_vector is not None: if isinstance(sparse_vector, dict): sparse_vector = SparseEmbedding.from_dict(sparse_vector) - sparse_vector = sparse_vector.to_pinecone() + if isinstance(sparse_vector, SparseEmbedding): + # unnecessary if-statement but mypy didn't like this otherwise + sparse_vector = sparse_vector.to_pinecone() results = self.index.query( vector=[query_vector_list], sparse_vector=sparse_vector, diff --git a/semantic_router/routers/base.py b/semantic_router/routers/base.py index 0172e5d803c6a3e9f73aa4c44a41381f1b5de4d2..5d59ae580c74f1164a71bcf38f762d26f987499c 100644 --- a/semantic_router/routers/base.py +++ b/semantic_router/routers/base.py @@ -413,7 +413,7 @@ class BaseRouter(BaseModel): if vector is None: if text is None: raise ValueError("Either text or vector must be provided") - vector = self._encode(text=text) + vector = self._encode(text=[text]) route, top_class_scores = self._retrieve_top_route(vector, route_filter) passed = self._check_threshold(top_class_scores, route) if passed and route is not None and not simulate_static: @@ -455,7 +455,7 @@ class BaseRouter(BaseModel): if vector is None: if text is None: raise ValueError("Either text or vector must be provided") - vector = await self._async_encode(text=text) + vector = await self._async_encode(text=[text]) route, top_class_scores = await self._async_retrieve_top_route( vector, route_filter @@ -494,7 +494,7 @@ class BaseRouter(BaseModel): if vector is None: if text is None: raise ValueError("Either text or vector must be provided") - vector_arr = self._encode(text=text) + vector_arr = self._encode(text=[text]) else: vector_arr = np.array(vector) print(f"{text=}") @@ -967,26 +967,26 @@ class BaseRouter(BaseModel): return route_names, utterances, function_schemas, metadata return route_names, utterances, function_schemas - def _encode(self, text: str) -> Any: + def _encode(self, text: list[str]) -> Any: """Generates embeddings for a given text. Must be implemented by a subclass. :param text: The text to encode. - :type text: str + :type text: list[str] :return: The embeddings of the text. :rtype: Any """ # TODO: should encode "content" rather than text raise NotImplementedError("This method should be implemented by subclasses.") - async def _async_encode(self, text: str) -> Any: + async def _async_encode(self, text: list[str]) -> Any: """Asynchronously generates embeddings for a given text. Must be implemented by a subclass. :param text: The text to encode. - :type text: str + :type text: list[str] :return: The embeddings of the text. :rtype: Any """ @@ -1029,6 +1029,9 @@ class BaseRouter(BaseModel): def _semantic_classify(self, query_results: List[Dict]) -> Tuple[str, List[float]]: scores_by_class = self.group_scores_by_class(query_results) + if self.aggregation_method is None: + raise ValueError("self.aggregation_method is not set.") + # Calculate total score for each class total_scores = { route: self.aggregation_method(scores) @@ -1048,6 +1051,9 @@ class BaseRouter(BaseModel): ) -> Tuple[str, List[float]]: scores_by_class = await self.async_group_scores_by_class(query_results) + if self.aggregation_method is None: + raise ValueError("self.aggregation_method is not set.") + # Calculate total score for each class total_scores = { route: self.aggregation_method(scores) diff --git a/semantic_router/routers/hybrid.py b/semantic_router/routers/hybrid.py index be3880abbbe9afd7e8f368695eaa6d34e7219ca8..91ecf2ef7b8df89eca5332b63f8d3277f5ab95d2 100644 --- a/semantic_router/routers/hybrid.py +++ b/semantic_router/routers/hybrid.py @@ -1,4 +1,4 @@ -from typing import Any, List, Optional +from typing import List, Optional import asyncio from pydantic.v1 import Field @@ -6,6 +6,7 @@ import numpy as np from semantic_router.encoders import ( DenseEncoder, + SparseEncoder, BM25Encoder, TfidfEncoder, ) @@ -21,13 +22,13 @@ class HybridRouter(BaseRouter): """A hybrid layer that uses both dense and sparse embeddings to classify routes.""" # there are a few additional attributes for hybrid - sparse_encoder: Optional[DenseEncoder] = Field(default=None) + sparse_encoder: Optional[SparseEncoder] = Field(default=None) alpha: float = 0.3 def __init__( self, encoder: DenseEncoder, - sparse_encoder: Optional[DenseEncoder] = None, + sparse_encoder: Optional[SparseEncoder] = None, llm: Optional[BaseLLM] = None, routes: List[Route] = [], index: Optional[HybridLocalIndex] = None, @@ -70,47 +71,49 @@ class HybridRouter(BaseRouter): index = index return index - def _set_sparse_encoder(self, sparse_encoder: Optional[DenseEncoder]): + def _set_sparse_encoder(self, sparse_encoder: Optional[SparseEncoder]): if sparse_encoder is None: logger.warning("No sparse_encoder provided. Using default BM25Encoder.") self.sparse_encoder = BM25Encoder() else: self.sparse_encoder = sparse_encoder - def _encode(self, text: list[str]) -> tuple[np.ndarray, list[dict[int, float]]]: + def _encode(self, text: list[str]) -> tuple[np.ndarray, list[SparseEmbedding]]: """Given some text, generates dense and sparse embeddings, then scales them using the chosen alpha value. """ + if self.sparse_encoder is None: + raise ValueError("self.sparse_encoder is not set.") # TODO: should encode "content" rather than text # TODO: add alpha as a parameter # create dense query vector xq_d = np.array(self.encoder(text)) # xq_d = np.squeeze(xq_d) # Reduce to 1d array. # create sparse query vector dict - xq_s_dict = self.sparse_encoder(text) + xq_s = self.sparse_encoder(text) # xq_s = np.squeeze(xq_s) # convex scaling - xq_d, xq_s_dict = self._convex_scaling(xq_d, xq_s_dict) - return xq_d, xq_s_dict + xq_d, xq_s = self._convex_scaling(dense=xq_d, sparse=xq_s) + return xq_d, xq_s - async def _async_encode(self, text: List[str]) -> Any: + async def _async_encode( + self, text: List[str] + ) -> tuple[np.ndarray, list[SparseEmbedding]]: """Given some text, generates dense and sparse embeddings, then scales them using the chosen alpha value. """ + if self.sparse_encoder is None: + raise ValueError("self.sparse_encoder is not set.") # TODO: should encode "content" rather than text # TODO: add alpha as a parameter # async encode both dense and sparse dense_coro = self.encoder.acall(text) sparse_coro = self.sparse_encoder.acall(text) - dense_vec, sparse_vec = await asyncio.gather(dense_coro, sparse_coro) + dense_vec, xq_s = await asyncio.gather(dense_coro, sparse_coro) # create dense query vector xq_d = np.array(dense_vec) - # xq_d = np.squeeze(xq_d) # reduce to 1d array - # create sparse query vector - xq_s = np.array(sparse_vec) - # xq_s = np.squeeze(xq_s) # convex scaling - xq_d, xq_s = self._convex_scaling(xq_d, xq_s) + xq_d, xq_s = self._convex_scaling(dense=xq_d, sparse=xq_s) return xq_d, xq_s def __call__( @@ -146,12 +149,18 @@ class HybridRouter(BaseRouter): else: return RouteChoice() - def _convex_scaling(self, dense: np.ndarray, sparse: list[dict[int, float]]): + def _convex_scaling( + self, dense: np.ndarray, sparse: list[SparseEmbedding] + ) -> tuple[np.ndarray, list[SparseEmbedding]]: + # TODO: better way to do this? + sparse_dicts = [sparse_vec.to_dict() for sparse_vec in sparse] # scale sparse and dense vecs scaled_dense = np.array(dense) * self.alpha scaled_sparse = [] - for sparse_dict in sparse: + for sparse_dict in sparse_dicts: scaled_sparse.append( - {k: v * (1 - self.alpha) for k, v in sparse_dict.items()} + SparseEmbedding.from_dict( + {k: v * (1 - self.alpha) for k, v in sparse_dict.items()} + ) ) return scaled_dense, scaled_sparse diff --git a/semantic_router/routers/semantic.py b/semantic_router/routers/semantic.py index 59cd8de18319e3049a458da997022dc5cd407aee..ff7f73f2a853563b574641aef98a94eedc94ccaa 100644 --- a/semantic_router/routers/semantic.py +++ b/semantic_router/routers/semantic.py @@ -35,16 +35,16 @@ class SemanticRouter(BaseRouter): if self.auto_sync: self._init_index_state() - def _encode(self, text: str) -> Any: + def _encode(self, text: list[str]) -> Any: """Given some text, encode it.""" # create query vector - xq = np.array(self.encoder([text])) + xq = np.array(self.encoder(text)) xq = np.squeeze(xq) # Reduce to 1d array. return xq - async def _async_encode(self, text: str) -> Any: + async def _async_encode(self, text: list[str]) -> Any: """Given some text, encode it.""" # create query vector - xq = np.array(await self.encoder.acall(docs=[text])) + xq = np.array(await self.encoder.acall(docs=text)) xq = np.squeeze(xq) # Reduce to 1d array. return xq