diff --git a/pyproject.toml b/pyproject.toml index 76ac7539..9abd9c7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "semantic-router" -version = "0.0.5" +version = "0.0.7" description = "Super fast semantic router for AI decision making" authors = [ "James Briggs ", diff --git a/semantic_router/__init__.py b/semantic_router/__init__.py index 734906f8..b10cc687 100644 --- a/semantic_router/__init__.py +++ b/semantic_router/__init__.py @@ -1,3 +1,3 @@ -from .layer import DecisionLayer, HybridDecisionLayer +from .layer import RouteLayer, HybridRouteLayer -__all__ = ["DecisionLayer", "HybridDecisionLayer"] +__all__ = ["RouteLayer", "HybridRouteLayer"] diff --git a/semantic_router/layer.py b/semantic_router/layer.py index adff961c..c29f190f 100644 --- a/semantic_router/layer.py +++ b/semantic_router/layer.py @@ -9,15 +9,15 @@ BM25Encoder, ) from semantic_router.linear import similarity_matrix, top_scores -from semantic_router.schema import Decision +from semantic_router.schema import Route -class DecisionLayer: +class RouteLayer: index = None categories = None score_threshold = 0.82 - def __init__(self, encoder: BaseEncoder, decisions: list[Decision] = []): + def __init__(self, encoder: BaseEncoder, routes: list[Route] = []): self.encoder = encoder # decide on default threshold based on encoder if isinstance(encoder, OpenAIEncoder): @@ -26,11 +26,11 @@ def __init__(self, encoder: BaseEncoder, decisions: list[Decision] = []): self.score_threshold = 0.3 else: self.score_threshold = 0.82 - # if decisions list has been passed, we initialize index now - if decisions: + # if routes list has been passed, we initialize index now + if routes: # initialize index now - for decision in tqdm(decisions): - self._add_decision(decision=decision) + for route in tqdm(routes): + self._add_route(route=route) def __call__(self, text: str) -> str | None: results = self._query(text) @@ -41,18 +41,18 @@ def __call__(self, text: str) -> str | None: else: return None - def add(self, decision: Decision): - self._add_decision(decision=decision) + def add(self, route: Route): + self._add_route(route=route) - def _add_decision(self, decision: Decision): + def _add_route(self, route: Route): # create embeddings - embeds = self.encoder(decision.utterances) + embeds = self.encoder(route.utterances) - # create decision array + # create route array if self.categories is None: - self.categories = np.array([decision.name] * len(embeds)) + self.categories = np.array([route.name] * len(embeds)) else: - str_arr = np.array([decision.name] * len(embeds)) + str_arr = np.array([route.name] * len(embeds)) self.categories = np.concatenate([self.categories, str_arr]) # create utterance array (the index) if self.index is None: @@ -73,10 +73,10 @@ def _query(self, text: str, top_k: int = 5): # calculate similarity matrix sim = similarity_matrix(xq, self.index) scores, idx = top_scores(sim, top_k) - # get the utterance categories (decision names) - decisions = self.categories[idx] if self.categories is not None else [] + # get the utterance categories (route names) + routes = self.categories[idx] if self.categories is not None else [] return [ - {"decision": d, "score": s.item()} for d, s in zip(decisions, scores) + {"route": d, "score": s.item()} for d, s in zip(routes, scores) ] else: return [] @@ -85,15 +85,15 @@ def _semantic_classify(self, query_results: list[dict]) -> tuple[str, list[float scores_by_class = {} for result in query_results: score = result["score"] - decision = result["decision"] - if decision in scores_by_class: - scores_by_class[decision].append(score) + route = result["route"] + if route in scores_by_class: + scores_by_class[route].append(score) else: - scores_by_class[decision] = [score] + scores_by_class[route] = [score] # Calculate total score for each class total_scores = { - decision: sum(scores) for decision, scores in scores_by_class.items() + route: sum(scores) for route, scores in scores_by_class.items() } top_class = max(total_scores, key=lambda x: total_scores[x], default=None) @@ -107,14 +107,14 @@ def _pass_threshold(self, scores: list[float], threshold: float) -> bool: return False -class HybridDecisionLayer: +class HybridRouteLayer: index = None sparse_index = None categories = None score_threshold = 0.82 def __init__( - self, encoder: BaseEncoder, decisions: list[Decision] = [], alpha: float = 0.3 + self, encoder: BaseEncoder, routes: list[Route] = [], alpha: float = 0.3 ): self.encoder = encoder self.sparse_encoder = BM25Encoder() @@ -126,11 +126,11 @@ def __init__( self.score_threshold = 0.3 else: self.score_threshold = 0.82 - # if decisions list has been passed, we initialize index now - if decisions: + # if routes list has been passed, we initialize index now + if routes: # initialize index now - for decision in tqdm(decisions): - self._add_decision(decision=decision) + for route in tqdm(routes): + self._add_route(route=route) def __call__(self, text: str) -> str | None: results = self._query(text) @@ -141,25 +141,25 @@ def __call__(self, text: str) -> str | None: else: return None - def add(self, decision: Decision): - self._add_decision(decision=decision) + def add(self, route: Route): + self._add_route(route=route) - def _add_decision(self, decision: Decision): + def _add_route(self, route: Route): # create embeddings - dense_embeds = np.array(self.encoder(decision.utterances)) # * self.alpha + dense_embeds = np.array(self.encoder(route.utterances)) # * self.alpha sparse_embeds = np.array( - self.sparse_encoder(decision.utterances) + self.sparse_encoder(route.utterances) ) # * (1 - self.alpha) - # create decision array + # create route array if self.categories is None: - self.categories = np.array([decision.name] * len(decision.utterances)) - self.utterances = np.array(decision.utterances) + self.categories = np.array([route.name] * len(route.utterances)) + self.utterances = np.array(route.utterances) else: - str_arr = np.array([decision.name] * len(decision.utterances)) + str_arr = np.array([route.name] * len(route.utterances)) self.categories = np.concatenate([self.categories, str_arr]) self.utterances = np.concatenate( - [self.utterances, np.array(decision.utterances)] + [self.utterances, np.array(route.utterances)] ) # create utterance array (the dense index) if self.index is None: @@ -199,10 +199,10 @@ def _query(self, text: str, top_k: int = 5): top_k = min(top_k, total_sim.shape[0]) idx = np.argpartition(total_sim, -top_k)[-top_k:] scores = total_sim[idx] - # get the utterance categories (decision names) - decisions = self.categories[idx] if self.categories is not None else [] + # get the utterance categories (route names) + routes = self.categories[idx] if self.categories is not None else [] return [ - {"decision": d, "score": s.item()} for d, s in zip(decisions, scores) + {"route": d, "score": s.item()} for d, s in zip(routes, scores) ] else: return [] @@ -217,15 +217,15 @@ def _semantic_classify(self, query_results: list[dict]) -> tuple[str, list[float scores_by_class = {} for result in query_results: score = result["score"] - decision = result["decision"] - if decision in scores_by_class: - scores_by_class[decision].append(score) + route = result["route"] + if route in scores_by_class: + scores_by_class[route].append(score) else: - scores_by_class[decision] = [score] + scores_by_class[route] = [score] # Calculate total score for each class total_scores = { - decision: sum(scores) for decision, scores in scores_by_class.items() + route: sum(scores) for route, scores in scores_by_class.items() } top_class = max(total_scores, key=lambda x: total_scores[x], default=None) diff --git a/semantic_router/schema.py b/semantic_router/schema.py index 37a43dd4..3763db03 100644 --- a/semantic_router/schema.py +++ b/semantic_router/schema.py @@ -10,7 +10,7 @@ ) -class Decision(BaseModel): +class Route(BaseModel): name: str utterances: list[str] description: str | None = None @@ -45,12 +45,12 @@ def __call__(self, texts: list[str]) -> list[float]: @dataclass class SemanticSpace: id: str - decisions: list[Decision] + routes: list[Route] encoder: str = "" - def __init__(self, decisions: list[Decision] = []): + def __init__(self, routes: list[Route] = []): self.id = "" - self.decisions = decisions + self.routes = routes - def add(self, decision: Decision): - self.decisions.append(decision) + def add(self, route: Route): + self.routes.append(route) diff --git a/walkthrough.ipynb b/walkthrough.ipynb index 81bb3ec2..6731ee0a 100644 --- a/walkthrough.ipynb +++ b/walkthrough.ipynb @@ -11,7 +11,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The Semantic Router library can be used as a super fast decision making layer on top of LLMs. That means rather than waiting on a slow agent to decide what to do, we can use the magic of semantic vector space to make decisions. Cutting decision making time down from seconds to milliseconds." + "The Semantic Router library can be used as a super fast route making layer on top of LLMs. That means rather than waiting on a slow agent to decide what to do, we can use the magic of semantic vector space to make routes. Cutting route making time down from seconds to milliseconds." ] }, { @@ -41,18 +41,28 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We start by defining a dictionary mapping decisions to example phrases that should trigger those decisions." + "We start by defining a dictionary mapping routes to example phrases that should trigger those routes." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from semantic_router.schema import Decision\n", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/jamesbriggs/opt/anaconda3/envs/decision-layer/lib/python3.11/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n", + "None of PyTorch, TensorFlow >= 2.0, or Flax have been found. Models won't be available and only tokenizers, configuration and file/data utilities can be used.\n" + ] + } + ], + "source": [ + "from semantic_router.schema import Route\n", "\n", - "politics = Decision(\n", + "politics = Route(\n", " name=\"politics\",\n", " utterances=[\n", " \"isn't politics the best thing ever\",\n", @@ -74,11 +84,11 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ - "chitchat = Decision(\n", + "chitchat = Route(\n", " name=\"chitchat\",\n", " utterances=[\n", " \"how's the weather today?\",\n", @@ -89,7 +99,7 @@ " ]\n", ")\n", "\n", - "decisions = [politics, chitchat]" + "routes = [politics, chitchat]" ] }, { @@ -101,7 +111,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -109,7 +119,7 @@ "from getpass import getpass\n", "import os\n", "\n", - "os.environ[\"COHERE_API_KEY\"] = os.environ[\"COHERE_API_KEY\"] or \\\n", + "os.environ[\"COHERE_API_KEY\"] = os.getenv(\"COHERE_API_KEY\") or \\\n", " getpass(\"Enter Cohere API Key: \")\n", "\n", "encoder = CohereEncoder()" @@ -119,18 +129,26 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now we define the `DecisionLayer`. When called, the decision layer will consume text (a query) and output the category (`Decision`) it belongs to — to initialize a `DecisionLayer` we need our `encoder` model and a list of `decisions`." + "Now we define the `RouteLayer`. When called, the route layer will consume text (a query) and output the category (`Route`) it belongs to — to initialize a `RouteLayer` we need our `encoder` model and a list of `routes`." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from semantic_router import DecisionLayer\n", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 2/2 [00:01<00:00, 1.04it/s]\n" + ] + } + ], + "source": [ + "from semantic_router.layer import RouteLayer\n", "\n", - "dl = DecisionLayer(encoder=encoder, decisions=decisions)" + "dl = RouteLayer(encoder=encoder, routes=routes)" ] }, { @@ -142,18 +160,40 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'politics'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "dl(\"don't you love politics?\")" ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'chitchat'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "dl(\"how's the weather today?\")" ] @@ -162,12 +202,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Both are classified accurately, what if we send a query that is unrelated to our existing `Decision` objects?" + "Both are classified accurately, what if we send a query that is unrelated to our existing `Route` objects?" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [