From 3530c854ed049a80e597bd7126b2a39c522651ae Mon Sep 17 00:00:00 2001 From: James Briggs <35938317+jamescalam@users.noreply.github.com> Date: Tue, 26 Dec 2023 21:30:24 +0100 Subject: [PATCH 01/17] restructure for how we use config object and fix circular imports --- semantic_router/__init__.py | 6 +- semantic_router/encoders/__init__.py | 8 +- semantic_router/hybrid_layer.py | 2 +- semantic_router/layer.py | 165 +++++++++++++++++++++++-- semantic_router/route.py | 113 +++-------------- semantic_router/schema.py | 21 +--- semantic_router/utils/function_call.py | 4 +- semantic_router/utils/llm.py | 31 ++++- tests/unit/test_schema.py | 18 --- 9 files changed, 216 insertions(+), 152 deletions(-) diff --git a/semantic_router/__init__.py b/semantic_router/__init__.py index 2659bfe3..fd3198cf 100644 --- a/semantic_router/__init__.py +++ b/semantic_router/__init__.py @@ -1,5 +1,5 @@ -from .hybrid_layer import HybridRouteLayer -from .layer import RouteLayer -from .route import Route, RouteConfig +from semantic_router.hybrid_layer import HybridRouteLayer +from semantic_router.layer import RouteLayer +from semantic_router.route import Route, RouteConfig __all__ = ["RouteLayer", "HybridRouteLayer", "Route", "RouteConfig"] diff --git a/semantic_router/encoders/__init__.py b/semantic_router/encoders/__init__.py index 30ad624a..ac27ebb4 100644 --- a/semantic_router/encoders/__init__.py +++ b/semantic_router/encoders/__init__.py @@ -1,6 +1,6 @@ -from .base import BaseEncoder -from .bm25 import BM25Encoder -from .cohere import CohereEncoder -from .openai import OpenAIEncoder +from semantic_router.encoders.base import BaseEncoder +from semantic_router.encoders.bm25 import BM25Encoder +from semantic_router.encoders.cohere import CohereEncoder +from semantic_router.encoders.openai import OpenAIEncoder __all__ = ["BaseEncoder", "CohereEncoder", "OpenAIEncoder", "BM25Encoder"] diff --git a/semantic_router/hybrid_layer.py b/semantic_router/hybrid_layer.py index 475a12f0..22f6573c 100644 --- a/semantic_router/hybrid_layer.py +++ b/semantic_router/hybrid_layer.py @@ -9,7 +9,7 @@ ) from semantic_router.utils.logger import logger -from .route import Route +from semantic_router.route import Route class HybridRouteLayer: diff --git a/semantic_router/layer.py b/semantic_router/layer.py index 2fa3b863..00dde5c2 100644 --- a/semantic_router/layer.py +++ b/semantic_router/layer.py @@ -1,4 +1,5 @@ import json +import os import numpy as np import yaml @@ -11,7 +12,121 @@ from semantic_router.linear import similarity_matrix, top_scores from semantic_router.utils.logger import logger -from .route import Route +from semantic_router.route import Route +from semantic_router.schema import Encoder, EncoderType, RouteChoice + + +def is_valid(route_config: str) -> bool: + try: + output_json = json.loads(route_config) + required_keys = ["name", "utterances"] + + if isinstance(output_json, list): + for item in output_json: + missing_keys = [key for key in required_keys if key not in item] + if missing_keys: + logger.warning( + f"Missing keys in route config: {', '.join(missing_keys)}" + ) + return False + return True + else: + missing_keys = [key for key in required_keys if key not in output_json] + if missing_keys: + logger.warning( + f"Missing keys in route config: {', '.join(missing_keys)}" + ) + return False + else: + return True + except json.JSONDecodeError as e: + logger.error(e) + return False + + +class LayerConfig: + """ + Generates a LayerConfig object that can be used for initializing a + RouteLayer. + """ + + routes: list[Route] = [] + + def __init__( + self, + routes: list[Route] = [], + encoder_type: EncoderType = "openai", + encoder_name: str | None = None, + ): + self.encoder_type = encoder_type + if encoder_name is None: + # if encoder_name is not provided, use the default encoder for type + if encoder_type == EncoderType.OPENAI: + encoder_name = "text-embedding-ada-002" + elif encoder_type == EncoderType.COHERE: + encoder_name = "embed-english-v3.0" + elif encoder_type == EncoderType.HUGGINGFACE: + raise NotImplementedError + logger.info(f"Using default {encoder_type} encoder: {encoder_name}") + self.encoder_name = encoder_name + self.routes = routes + + @classmethod + def from_file(cls, path: str): + """Load the routes from a file in JSON or YAML format""" + logger.info(f"Loading route config from {path}") + _, ext = os.path.splitext(path) + with open(path, "r") as f: + if ext == ".json": + routes = json.load(f) + elif ext in [".yaml", ".yml"]: + routes = yaml.safe_load(f) + else: + raise ValueError( + "Unsupported file type. Only .json and .yaml are supported" + ) + + route_config_str = json.dumps(routes) + if is_valid(route_config_str): + routes = [Route.from_dict(route) for route in routes] + return cls(routes=routes) + else: + raise Exception("Invalid config JSON or YAML") + + def to_dict(self): + return [route.to_dict() for route in self.routes] + + def to_file(self, path: str): + """Save the routes to a file in JSON or YAML format""" + logger.info(f"Saving route config to {path}") + _, ext = os.path.splitext(path) + with open(path, "w") as f: + if ext == ".json": + json.dump(self.to_dict(), f) + elif ext in [".yaml", ".yml"]: + yaml.safe_dump(self.to_dict(), f) + else: + raise ValueError( + "Unsupported file type. Only .json and .yaml are supported" + ) + + def add(self, route: Route): + self.routes.append(route) + logger.info(f"Added route `{route.name}`") + + def get(self, name: str) -> Route | None: + for route in self.routes: + if route.name == name: + return route + logger.error(f"Route `{name}` not found") + return None + + def remove(self, name: str): + if name not in [route.name for route in self.routes]: + logger.error(f"Route `{name}` not found") + else: + self.routes = [route for route in self.routes if route.name != name] + logger.info(f"Removed route `{name}`") class RouteLayer: @@ -34,28 +149,52 @@ def __init__(self, encoder: BaseEncoder | None = None, routes: list[Route] = []) # initialize index now self._add_routes(routes=routes) - def __call__(self, text: str) -> str | None: + def __call__(self, text: str) -> RouteChoice: results = self._query(text) top_class, top_class_scores = self._semantic_classify(results) passed = self._pass_threshold(top_class_scores, self.score_threshold) if passed: - return top_class + # get chosen route object + route = [route for route in self.routes if route.name == top_class][0] + return route(text) else: - return None + # if no route passes threshold, return empty route choice + return RouteChoice() @classmethod def from_json(cls, file_path: str): - with open(file_path, "r") as f: - routes_data = json.load(f) - routes = [Route.from_dict(route_data) for route_data in routes_data] - return cls(routes=routes) + config = LayerConfig.from_file(file_path) + encoder = Encoder( + encoder_type=config.encoder_type, + encoder_name=config.encoder_name + ) + return cls( + encoder=encoder, + routes=config.routes + ) @classmethod def from_yaml(cls, file_path: str): - with open(file_path, "r") as f: - routes_data = yaml.load(f, Loader=yaml.FullLoader) - routes = [Route.from_dict(route_data) for route_data in routes_data] - return cls(routes=routes) + config = LayerConfig.from_file(file_path) + encoder = Encoder( + encoder_type=config.encoder_type, + encoder_name=config.encoder_name + ) + return cls( + encoder=encoder, + routes=config.routes + ) + + @classmethod + def from_config(cls, config: LayerConfig): + encoder = Encoder( + encoder_type=config.encoder_type, + encoder_name=config.encoder_name + ) + return cls( + encoder=encoder, + routes=config.routes + ) def add(self, route: Route): # create embeddings @@ -73,6 +212,8 @@ def add(self, route: Route): else: embed_arr = np.array(embeds) self.index = np.concatenate([self.index, embed_arr]) + # add route to routes list + self.routes.append(route) def _add_routes(self, routes: list[Route]): # create embeddings for all routes diff --git a/semantic_router/route.py b/semantic_router/route.py index 99a7945b..be520da9 100644 --- a/semantic_router/route.py +++ b/semantic_router/route.py @@ -1,48 +1,35 @@ import json -import os import re from typing import Any, Callable, Union -import yaml from pydantic import BaseModel from semantic_router.utils import function_call from semantic_router.utils.llm import llm from semantic_router.utils.logger import logger - - -def is_valid(route_config: str) -> bool: - try: - output_json = json.loads(route_config) - required_keys = ["name", "utterances"] - - if isinstance(output_json, list): - for item in output_json: - missing_keys = [key for key in required_keys if key not in item] - if missing_keys: - logger.warning( - f"Missing keys in route config: {', '.join(missing_keys)}" - ) - return False - return True - else: - missing_keys = [key for key in required_keys if key not in output_json] - if missing_keys: - logger.warning( - f"Missing keys in route config: {', '.join(missing_keys)}" - ) - return False - else: - return True - except json.JSONDecodeError as e: - logger.error(e) - return False +from semantic_router.schema import RouteChoice class Route(BaseModel): name: str utterances: list[str] description: str | None = None + function_schema: dict[str, Any] | None = None + + def __call__(self, query: str) -> RouteChoice: + if self.function_schema: + # if a function schema is provided we generate the inputs + extracted_inputs = function_call.extract_function_inputs( + query=query, function_schema=self.function_schema + ) + function_call = extracted_inputs + else: + # otherwise we just pass None for the call + function_call = None + return RouteChoice( + name=self.name, + function_call=function_call + ) def to_dict(self): return self.dict() @@ -114,69 +101,3 @@ async def _generate_dynamic_route(cls, function_schema: dict[str, Any]): raise Exception("No config generated") -class RouteConfig: - """ - Generates a RouteConfig object from a list of Route objects - """ - - routes: list[Route] = [] - - def __init__(self, routes: list[Route] = []): - self.routes = routes - - @classmethod - def from_file(cls, path: str): - """Load the routes from a file in JSON or YAML format""" - logger.info(f"Loading route config from {path}") - _, ext = os.path.splitext(path) - with open(path, "r") as f: - if ext == ".json": - routes = json.load(f) - elif ext in [".yaml", ".yml"]: - routes = yaml.safe_load(f) - else: - raise ValueError( - "Unsupported file type. Only .json and .yaml are supported" - ) - - route_config_str = json.dumps(routes) - if is_valid(route_config_str): - routes = [Route.from_dict(route) for route in routes] - return cls(routes=routes) - else: - raise Exception("Invalid config JSON or YAML") - - def to_dict(self): - return [route.to_dict() for route in self.routes] - - def to_file(self, path: str): - """Save the routes to a file in JSON or YAML format""" - logger.info(f"Saving route config to {path}") - _, ext = os.path.splitext(path) - with open(path, "w") as f: - if ext == ".json": - json.dump(self.to_dict(), f) - elif ext in [".yaml", ".yml"]: - yaml.safe_dump(self.to_dict(), f) - else: - raise ValueError( - "Unsupported file type. Only .json and .yaml are supported" - ) - - def add(self, route: Route): - self.routes.append(route) - logger.info(f"Added route `{route.name}`") - - def get(self, name: str) -> Route | None: - for route in self.routes: - if route.name == name: - return route - logger.error(f"Route `{name}` not found") - return None - - def remove(self, name: str): - if name not in [route.name for route in self.routes]: - logger.error(f"Route `{name}` not found") - else: - self.routes = [route for route in self.routes if route.name != name] - logger.info(f"Removed route `{name}`") diff --git a/semantic_router/schema.py b/semantic_router/schema.py index 4646a637..a3d786db 100644 --- a/semantic_router/schema.py +++ b/semantic_router/schema.py @@ -1,8 +1,8 @@ from enum import Enum from pydantic.dataclasses import dataclass +from pydantic import BaseModel -from semantic_router import Route from semantic_router.encoders import ( BaseEncoder, CohereEncoder, @@ -16,6 +16,11 @@ class EncoderType(Enum): COHERE = "cohere" +class RouteChoice(BaseModel): + name: str | None = None + function_call: dict | None = None + + @dataclass class Encoder: type: EncoderType @@ -34,17 +39,3 @@ def __init__(self, type: str, name: str): def __call__(self, texts: list[str]) -> list[list[float]]: return self.model(texts) - - -@dataclass -class SemanticSpace: - id: str - routes: list[Route] - encoder: str = "" - - def __init__(self, routes: list[Route] = []): - self.id = "" - self.routes = routes - - def add(self, route: Route): - self.routes.append(route) diff --git a/semantic_router/utils/function_call.py b/semantic_router/utils/function_call.py index c1b4fcee..9504dfb8 100644 --- a/semantic_router/utils/function_call.py +++ b/semantic_router/utils/function_call.py @@ -40,7 +40,7 @@ def get_schema(item: Union[BaseModel, Callable]) -> dict[str, Any]: return schema -async def extract_function_inputs(query: str, function_schema: dict[str, Any]) -> dict: +def extract_function_inputs(query: str, function_schema: dict[str, Any]) -> dict: logger.info("Extracting function input...") prompt = f""" @@ -72,7 +72,7 @@ async def extract_function_inputs(query: str, function_schema: dict[str, Any]) - Result: """ - output = await llm(prompt) + output = llm(prompt) if not output: raise Exception("No output generated for extract function input") diff --git a/semantic_router/utils/llm.py b/semantic_router/utils/llm.py index e912ee1f..0d22b9a6 100644 --- a/semantic_router/utils/llm.py +++ b/semantic_router/utils/llm.py @@ -5,7 +5,36 @@ from semantic_router.utils.logger import logger -async def llm(prompt: str) -> str | None: +def llm(prompt: str) -> str | None: + try: + client = openai.OpenAI( + base_url="https://openrouter.ai/api/v1", + api_key=os.getenv("OPENROUTER_API_KEY"), + ) + + completion = client.chat.completions.create( + model="mistralai/mistral-7b-instruct", + messages=[ + { + "role": "user", + "content": prompt, + }, + ], + temperature=0.01, + max_tokens=200, + ) + + output = completion.choices[0].message.content + + if not output: + raise Exception("No output generated") + return output + except Exception as e: + logger.error(f"LLM error: {e}") + raise Exception(f"LLM error: {e}") + + +async def allm(prompt: str) -> str | None: try: client = openai.AsyncOpenAI( base_url="https://openrouter.ai/api/v1", diff --git a/tests/unit/test_schema.py b/tests/unit/test_schema.py index 27c73c9f..46799ee8 100644 --- a/tests/unit/test_schema.py +++ b/tests/unit/test_schema.py @@ -6,7 +6,6 @@ Encoder, EncoderType, OpenAIEncoder, - SemanticSpace, ) @@ -40,20 +39,3 @@ def test_encoder_call_method(self, mocker): encoder = Encoder(type="openai", name="test-engine") result = encoder(["test"]) assert result == [0.1, 0.2, 0.3] - - -class TestSemanticSpaceDataclass: - def test_semanticspace_initialization(self): - semantic_space = SemanticSpace() - assert semantic_space.id == "" - assert semantic_space.routes == [] - - def test_semanticspace_add_route(self): - route = Route(name="test", utterances=["hello", "hi"], description="greeting") - semantic_space = SemanticSpace() - semantic_space.add(route) - - assert len(semantic_space.routes) == 1 - assert semantic_space.routes[0].name == "test" - assert semantic_space.routes[0].utterances == ["hello", "hi"] - assert semantic_space.routes[0].description == "greeting" From 21b275121266c1d33bab085b7baa0d495811e726 Mon Sep 17 00:00:00 2001 From: James Briggs <35938317+jamescalam@users.noreply.github.com> Date: Tue, 26 Dec 2023 23:53:08 +0100 Subject: [PATCH 02/17] restructure and example docs --- .../00_introduction.ipynb | 6 +- docs/01_save_load_from_file.ipynb | 253 ++++++++++++++ docs/02_dynamic_routes.ipynb | 309 ++++++++++++++++++ docs/examples/route_config.json | 1 - semantic_router/__init__.py | 6 +- semantic_router/encoders/base.py | 1 + semantic_router/encoders/bm25.py | 1 + semantic_router/encoders/cohere.py | 1 + semantic_router/encoders/openai.py | 1 + semantic_router/layer.py | 67 ++-- semantic_router/route.py | 33 +- 11 files changed, 648 insertions(+), 31 deletions(-) rename walkthrough.ipynb => docs/00_introduction.ipynb (97%) create mode 100644 docs/01_save_load_from_file.ipynb create mode 100644 docs/02_dynamic_routes.ipynb delete mode 100644 docs/examples/route_config.json diff --git a/walkthrough.ipynb b/docs/00_introduction.ipynb similarity index 97% rename from walkthrough.ipynb rename to docs/00_introduction.ipynb index d008739c..7a9b5283 100644 --- a/walkthrough.ipynb +++ b/docs/00_introduction.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Semantic Router Walkthrough" + "# Semantic Router Intro" ] }, { @@ -34,7 +34,7 @@ "metadata": {}, "outputs": [], "source": [ - "!pip install -qU semantic-router==0.0.8" + "!pip install -qU semantic-router==0.0.13" ] }, { @@ -198,7 +198,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.3" + "version": "3.11.5" } }, "nbformat": 4, diff --git a/docs/01_save_load_from_file.ipynb b/docs/01_save_load_from_file.ipynb new file mode 100644 index 00000000..925878f2 --- /dev/null +++ b/docs/01_save_load_from_file.ipynb @@ -0,0 +1,253 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Route Layers from File\n", + "\n", + "Here we will show how to save routers to YAML or JSON files, and how to load a route layer from file." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Getting Started" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We start by installing the library:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install -qU semantic-router==0.0.13" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Saving to JSON" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First let's create a list of routes:" + ] + }, + { + "cell_type": "code", + "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 import Route\n", + "\n", + "politics = Route(\n", + " name=\"politics\",\n", + " utterances=[\n", + " \"isn't politics the best thing ever\",\n", + " \"why don't you tell me about your political opinions\",\n", + " \"don't you just love the president\" \"don't you just hate the president\",\n", + " \"they're going to destroy this country!\",\n", + " \"they will save the country!\",\n", + " ],\n", + ")\n", + "chitchat = Route(\n", + " name=\"chitchat\",\n", + " utterances=[\n", + " \"how's the weather today?\",\n", + " \"how are things going?\",\n", + " \"lovely weather today\",\n", + " \"the weather is horrendous\",\n", + " \"let's go to the chippy\",\n", + " ],\n", + ")\n", + "\n", + "routes = [politics, chitchat]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We define a route layer using these routes and using the default Cohere encoder." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from semantic_router import RouteLayer\n", + "\n", + "# dashboard.cohere.ai\n", + "os.environ[\"COHERE_API_KEY\"] = \"\"\n", + "\n", + "layer = RouteLayer(routes=routes)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To save our route layer we call the `to_json` method:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m2023-12-26 23:38:56 INFO semantic_router.utils.logger Saving route config to layer.json\u001b[0m\n" + ] + } + ], + "source": [ + "layer.to_json(\"layer.json\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Loading from JSON" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can view the router file we just saved to see what information is stored." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'encoder_type': 'cohere', 'encoder_name': 'embed-english-v3.0', 'routes': [{'name': 'politics', 'utterances': [\"isn't politics the best thing ever\", \"why don't you tell me about your political opinions\", \"don't you just love the presidentdon't you just hate the president\", \"they're going to destroy this country!\", 'they will save the country!'], 'description': None, 'function_schema': None}, {'name': 'chitchat', 'utterances': [\"how's the weather today?\", 'how are things going?', 'lovely weather today', 'the weather is horrendous', \"let's go to the chippy\"], 'description': None, 'function_schema': None}]}\n" + ] + } + ], + "source": [ + "import json\n", + "\n", + "with open(\"layer.json\", \"r\") as f:\n", + " router_json = json.load(f)\n", + "\n", + "print(router_json)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It tells us our encoder type, encoder name, and routes. This is everything we need to initialize a new router. To do so, we use the `from_json` method." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m2023-12-26 23:38:56 INFO semantic_router.utils.logger Loading route config from layer.json\u001b[0m\n" + ] + } + ], + "source": [ + "layer = RouteLayer.from_json(\"layer.json\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can confirm that our layer has been initialized with the expected attributes by viewing the `RouteLayer` object:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "RouteLayer(encoder=Encoder(type=, name='embed-english-v3.0', model=CohereEncoder(name='embed-english-v3.0', type='cohere', client=)), routes=[Route(name='politics', utterances=[\"isn't politics the best thing ever\", \"why don't you tell me about your political opinions\", \"don't you just love the presidentdon't you just hate the president\", \"they're going to destroy this country!\", 'they will save the country!'], description=None, function_schema=None), Route(name='chitchat', utterances=[\"how's the weather today?\", 'how are things going?', 'lovely weather today', 'the weather is horrendous', \"let's go to the chippy\"], description=None, function_schema=None)])" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "layer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "decision-layer", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/02_dynamic_routes.ipynb b/docs/02_dynamic_routes.ipynb new file mode 100644 index 00000000..61456a07 --- /dev/null +++ b/docs/02_dynamic_routes.ipynb @@ -0,0 +1,309 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Dynamic Routes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In semantic-router there are two types of routes that can be chosen. Both routes belong to the `Route` object, the only difference between them is that _static_ routes return a `Route.name` when chosen, whereas _dynamic_ routes use an LLM call to produce parameter input values.\n", + "\n", + "For example, a _static_ route will tell us if a query is talking about mathematics by returning the route name (which could be `\"math\"` for example). A _dynamic_ route can generate additional values, so it may decide a query is talking about maths, but it can also generate Python code that we can later execute to answer the user's query, this output may look like `\"math\", \"import math; output = math.sqrt(64)`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initializing Routes and RouteLayer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Dynamic routes are treated in the same way as static routes, let's begin by initializing a `RouteLayer` consisting of static routes." + ] + }, + { + "cell_type": "code", + "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 import Route\n", + "\n", + "politics = Route(\n", + " name=\"politics\",\n", + " utterances=[\n", + " \"isn't politics the best thing ever\",\n", + " \"why don't you tell me about your political opinions\",\n", + " \"don't you just love the president\" \"don't you just hate the president\",\n", + " \"they're going to destroy this country!\",\n", + " \"they will save the country!\",\n", + " ],\n", + ")\n", + "chitchat = Route(\n", + " name=\"chitchat\",\n", + " utterances=[\n", + " \"how's the weather today?\",\n", + " \"how are things going?\",\n", + " \"lovely weather today\",\n", + " \"the weather is horrendous\",\n", + " \"let's go to the chippy\",\n", + " ],\n", + ")\n", + "\n", + "routes = [politics, chitchat]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from semantic_router import RouteLayer\n", + "\n", + "os.environ[\"COHERE_API_KEY\"] = \"\"\n", + "\n", + "layer = RouteLayer(routes=routes)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We run the solely static routes layer:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "RouteChoice(name='chitchat', function_call=None)" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "layer(\"how's the weather today?\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating a Dynamic Route" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As with static routes, we must create a dynamic route before adding it to our route layer. To make a route dynamic, we need to provide a `function_schema`. The function schema provides instructions on what a function is, so that an LLM can decide how to use it correctly." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime\n", + "from zoneinfo import ZoneInfo\n", + "\n", + "def get_time(timezone: str) -> str:\n", + " \"\"\"Finds the current time in a specific timezone.\n", + " \n", + " :param timezone: The timezone to find the current time in, should\n", + " be a valid timezone from the IANA Time Zone Database like\n", + " \"America/New_York\" or \"Europe/London\".\n", + " :type timezone: str\n", + " :return: The current time in the specified timezone.\"\"\"\n", + " now = datetime.now(ZoneInfo(timezone))\n", + " return now.strftime(\"%H:%M\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'17:50'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "get_time(\"America/New_York\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To get the function schema we can use the `get_schema` function from the `function_call` module." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'name': 'get_time',\n", + " 'description': 'Finds the current time in a specific timezone.\\n\\n:param timezone: The timezone to find the current time in, should\\n be a valid timezone from the IANA Time Zone Database like\\n \"America/New_York\" or \"Europe/London\".\\n:type timezone: str\\n:return: The current time in the specified timezone.',\n", + " 'signature': '(timezone: str) -> str',\n", + " 'output': \"\"}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from semantic_router.utils.function_call import get_schema\n", + "\n", + "schema = get_schema(get_time)\n", + "schema" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We use this to define our dynamic route:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "time_route = Route(\n", + " name=\"get_time\",\n", + " utterances=[\n", + " \"what is the time in new york city?\",\n", + " \"what is the time in london?\",\n", + " \"I live in Rome, what time is it?\"\n", + " ],\n", + " function_schema=schema\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Add the new route to our `layer`:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "layer.add(time_route)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can ask our layer a time related question to trigger our new dynamic route." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m2023-12-26 23:50:55 INFO semantic_router.utils.logger Extracting function input...\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "RouteChoice(name='get_time', function_call={'timezone': 'America/New_York'})" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "os.environ[\"OPENROUTER_API_KEY\"] = \"\"\n", + "\n", + "layer(\"what is the time in new york city?\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "decision-layer", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/examples/route_config.json b/docs/examples/route_config.json deleted file mode 100644 index f76a7385..00000000 --- a/docs/examples/route_config.json +++ /dev/null @@ -1 +0,0 @@ -[{"name": "get_time", "utterances": ["What's the time in New York?", "Can you tell me the time in Tokyo?", "What's the current time in London?", "Can you give me the time in Sydney?", "What's the time in Paris?"], "description": null}, {"name": "get_news", "utterances": ["Tell me the latest news from the United States", "What's happening in India today?", "Can you give me the top stories from Japan", "Get me the breaking news from the UK", "What's the latest in Germany?"], "description": null}] diff --git a/semantic_router/__init__.py b/semantic_router/__init__.py index fd3198cf..07735312 100644 --- a/semantic_router/__init__.py +++ b/semantic_router/__init__.py @@ -1,5 +1,5 @@ from semantic_router.hybrid_layer import HybridRouteLayer -from semantic_router.layer import RouteLayer -from semantic_router.route import Route, RouteConfig +from semantic_router.layer import RouteLayer, LayerConfig +from semantic_router.route import Route -__all__ = ["RouteLayer", "HybridRouteLayer", "Route", "RouteConfig"] +__all__ = ["RouteLayer", "HybridRouteLayer", "Route", "LayerConfig"] diff --git a/semantic_router/encoders/base.py b/semantic_router/encoders/base.py index 632ebc79..4e9d02a0 100644 --- a/semantic_router/encoders/base.py +++ b/semantic_router/encoders/base.py @@ -3,6 +3,7 @@ class BaseEncoder(BaseModel): name: str + type: str class Config: arbitrary_types_allowed = True diff --git a/semantic_router/encoders/bm25.py b/semantic_router/encoders/bm25.py index c9da628e..f43e1780 100644 --- a/semantic_router/encoders/bm25.py +++ b/semantic_router/encoders/bm25.py @@ -8,6 +8,7 @@ class BM25Encoder(BaseEncoder): model: Any | None = None idx_mapping: dict[int, int] | None = None + type: str = "sparse" def __init__(self, name: str = "bm25"): super().__init__(name=name) diff --git a/semantic_router/encoders/cohere.py b/semantic_router/encoders/cohere.py index 9cddcb58..ae0db7a2 100644 --- a/semantic_router/encoders/cohere.py +++ b/semantic_router/encoders/cohere.py @@ -7,6 +7,7 @@ class CohereEncoder(BaseEncoder): client: cohere.Client | None = None + type: str = "cohere" def __init__( self, diff --git a/semantic_router/encoders/openai.py b/semantic_router/encoders/openai.py index c6d4cc96..9744401f 100644 --- a/semantic_router/encoders/openai.py +++ b/semantic_router/encoders/openai.py @@ -11,6 +11,7 @@ class OpenAIEncoder(BaseEncoder): client: openai.Client | None + type: str = "openai" def __init__( self, diff --git a/semantic_router/layer.py b/semantic_router/layer.py index 00dde5c2..5079c452 100644 --- a/semantic_router/layer.py +++ b/semantic_router/layer.py @@ -16,17 +16,17 @@ from semantic_router.schema import Encoder, EncoderType, RouteChoice -def is_valid(route_config: str) -> bool: +def is_valid(layer_config: str) -> bool: try: - output_json = json.loads(route_config) - required_keys = ["name", "utterances"] + output_json = json.loads(layer_config) + required_keys = ["encoder_name", "encoder_type", "routes"] if isinstance(output_json, list): for item in output_json: missing_keys = [key for key in required_keys if key not in item] if missing_keys: logger.warning( - f"Missing keys in route config: {', '.join(missing_keys)}" + f"Missing keys in layer config: {', '.join(missing_keys)}" ) return False return True @@ -34,7 +34,7 @@ def is_valid(route_config: str) -> bool: missing_keys = [key for key in required_keys if key not in output_json] if missing_keys: logger.warning( - f"Missing keys in route config: {', '.join(missing_keys)}" + f"Missing keys in layer config: {', '.join(missing_keys)}" ) return False else: @@ -78,23 +78,33 @@ def from_file(cls, path: str): _, ext = os.path.splitext(path) with open(path, "r") as f: if ext == ".json": - routes = json.load(f) + layer = json.load(f) elif ext in [".yaml", ".yml"]: - routes = yaml.safe_load(f) + layer = yaml.safe_load(f) else: raise ValueError( "Unsupported file type. Only .json and .yaml are supported" ) - route_config_str = json.dumps(routes) + route_config_str = json.dumps(layer) if is_valid(route_config_str): - routes = [Route.from_dict(route) for route in routes] - return cls(routes=routes) + encoder_type = layer["encoder_type"] + encoder_name = layer["encoder_name"] + routes = [Route.from_dict(route) for route in layer["routes"]] + return cls( + encoder_type=encoder_type, + encoder_name=encoder_name, + routes=routes + ) else: raise Exception("Invalid config JSON or YAML") def to_dict(self): - return [route.to_dict() for route in self.routes] + return { + "encoder_type": self.encoder_type, + "encoder_name": self.encoder_name, + "routes": [route.to_dict() for route in self.routes] + } def to_file(self, path: str): """Save the routes to a file in JSON or YAML format""" @@ -102,7 +112,7 @@ def to_file(self, path: str): _, ext = os.path.splitext(path) with open(path, "w") as f: if ext == ".json": - json.dump(self.to_dict(), f) + json.dump(self.to_dict(), f, indent=4) elif ext in [".yaml", ".yml"]: yaml.safe_dump(self.to_dict(), f) else: @@ -160,13 +170,18 @@ def __call__(self, text: str) -> RouteChoice: else: # if no route passes threshold, return empty route choice return RouteChoice() + + def __str__(self): + return (f"RouteLayer(encoder={self.encoder}, " + f"score_threshold={self.score_threshold}, " + f"routes={self.routes})") @classmethod def from_json(cls, file_path: str): config = LayerConfig.from_file(file_path) encoder = Encoder( - encoder_type=config.encoder_type, - encoder_name=config.encoder_name + type=config.encoder_type, + name=config.encoder_name ) return cls( encoder=encoder, @@ -177,8 +192,8 @@ def from_json(cls, file_path: str): def from_yaml(cls, file_path: str): config = LayerConfig.from_file(file_path) encoder = Encoder( - encoder_type=config.encoder_type, - encoder_name=config.encoder_name + type=config.encoder_type, + name=config.encoder_name ) return cls( encoder=encoder, @@ -188,8 +203,8 @@ def from_yaml(cls, file_path: str): @classmethod def from_config(cls, config: LayerConfig): encoder = Encoder( - encoder_type=config.encoder_type, - encoder_name=config.encoder_name + type=config.encoder_type, + name=config.encoder_name ) return cls( encoder=encoder, @@ -284,8 +299,18 @@ def _pass_threshold(self, scores: list[float], threshold: float) -> bool: return max(scores) > threshold else: return False + + def to_config(self) -> LayerConfig: + return LayerConfig( + encoder_type=self.encoder.type, + encoder_name=self.encoder.name, + routes=self.routes + ) def to_json(self, file_path: str): - routes = [route.to_dict() for route in self.routes] - with open(file_path, "w") as f: - json.dump(routes, f, indent=4) + config = self.to_config() + config.to_file(file_path) + + def to_yaml(self, file_path: str): + config = self.to_config() + config.to_file(file_path) diff --git a/semantic_router/route.py b/semantic_router/route.py index be520da9..af75b211 100644 --- a/semantic_router/route.py +++ b/semantic_router/route.py @@ -10,6 +10,33 @@ from semantic_router.schema import RouteChoice +def is_valid(route_config: str) -> bool: + try: + output_json = json.loads(route_config) + required_keys = ["name", "utterances"] + + if isinstance(output_json, list): + for item in output_json: + missing_keys = [key for key in required_keys if key not in item] + if missing_keys: + logger.warning( + f"Missing keys in route config: {', '.join(missing_keys)}" + ) + return False + return True + else: + missing_keys = [key for key in required_keys if key not in output_json] + if missing_keys: + logger.warning( + f"Missing keys in route config: {', '.join(missing_keys)}" + ) + return False + else: + return True + except json.JSONDecodeError as e: + logger.error(e) + return False + class Route(BaseModel): name: str utterances: list[str] @@ -22,13 +49,13 @@ def __call__(self, query: str) -> RouteChoice: extracted_inputs = function_call.extract_function_inputs( query=query, function_schema=self.function_schema ) - function_call = extracted_inputs + func_call = extracted_inputs else: # otherwise we just pass None for the call - function_call = None + func_call = None return RouteChoice( name=self.name, - function_call=function_call + function_call=func_call ) def to_dict(self): From 27e2982bdb15015c046408962f8d31124d3c9381 Mon Sep 17 00:00:00 2001 From: James Briggs <35938317+jamescalam@users.noreply.github.com> Date: Wed, 27 Dec 2023 09:25:52 +0100 Subject: [PATCH 03/17] fix issue where RouteLayer was overwriting previous states of self --- semantic_router/layer.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/semantic_router/layer.py b/semantic_router/layer.py index 5079c452..da25f828 100644 --- a/semantic_router/layer.py +++ b/semantic_router/layer.py @@ -140,13 +140,20 @@ def remove(self, name: str): class RouteLayer: - index = None - categories = None - score_threshold = 0.82 + index: np.ndarray | None = None + categories: np.ndarray | None = None + score_threshold: float = 0.82 - def __init__(self, encoder: BaseEncoder | None = None, routes: list[Route] = []): + def __init__( + self, + encoder: BaseEncoder | None = None, + routes: list[Route] | None = None + ): + logger.info("Initializing RouteLayer") + self.index = None + self.categories = None self.encoder = encoder if encoder is not None else CohereEncoder() - self.routes: list[Route] = routes + self.routes: list[Route] = routes if routes is not None else [] # decide on default threshold based on encoder if isinstance(encoder, OpenAIEncoder): self.score_threshold = 0.82 @@ -155,9 +162,9 @@ def __init__(self, encoder: BaseEncoder | None = None, routes: list[Route] = []) else: self.score_threshold = 0.82 # if routes list has been passed, we initialize index now - if routes: + if len(self.routes) > 0: # initialize index now - self._add_routes(routes=routes) + self._add_routes(routes=self.routes) def __call__(self, text: str) -> RouteChoice: results = self._query(text) @@ -212,19 +219,24 @@ def from_config(cls, config: LayerConfig): ) def add(self, route: Route): + print(f"Adding route `{route.name}`") # create embeddings embeds = self.encoder(route.utterances) # create route array if self.categories is None: + print("Initializing categories array") self.categories = np.array([route.name] * len(embeds)) else: + print("Adding route to categories") 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: + print("Initializing index array") self.index = np.array(embeds) else: + print("Adding route to index") embed_arr = np.array(embeds) self.index = np.concatenate([self.index, embed_arr]) # add route to routes list From 92e8f8423bd944cd5eea4802c6af34c9e033bb56 Mon Sep 17 00:00:00 2001 From: James Briggs <35938317+jamescalam@users.noreply.github.com> Date: Wed, 27 Dec 2023 09:30:31 +0100 Subject: [PATCH 04/17] update tests and added type to encoder --- semantic_router/encoders/base.py | 4 +- tests/unit/test_layer.py | 138 +++++++++++++++++++++++++++++-- tests/unit/test_route.py | 77 +---------------- 3 files changed, 136 insertions(+), 83 deletions(-) diff --git a/semantic_router/encoders/base.py b/semantic_router/encoders/base.py index 4e9d02a0..bd952403 100644 --- a/semantic_router/encoders/base.py +++ b/semantic_router/encoders/base.py @@ -1,9 +1,9 @@ -from pydantic import BaseModel +from pydantic import BaseModel, Field class BaseEncoder(BaseModel): name: str - type: str + type: str = Field(default="base") class Config: arbitrary_types_allowed = True diff --git a/tests/unit/test_layer.py b/tests/unit/test_layer.py index 21b48917..873e488a 100644 --- a/tests/unit/test_layer.py +++ b/tests/unit/test_layer.py @@ -1,7 +1,9 @@ +import os import pytest +from unittest.mock import mock_open, patch from semantic_router.encoders import BaseEncoder, CohereEncoder, OpenAIEncoder -from semantic_router.layer import RouteLayer +from semantic_router.layer import LayerConfig, RouteLayer from semantic_router.route import Route @@ -17,6 +19,50 @@ def mock_encoder_call(utterances): return [mock_responses.get(u, [0, 0, 0]) for u in utterances] +def layer_json(): + return """{ + "encoder_type": "cohere", + "encoder_name": "embed-english-v3.0", + "routes": [ + { + "name": "politics", + "utterances": [ + "isn't politics the best thing ever", + "why don't you tell me about your political opinions" + ], + "description": null, + "function_schema": null + }, + { + "name": "chitchat", + "utterances": [ + "how's the weather today?", + "how are things going?" + ], + "description": null, + "function_schema": null + } + ] +}""" + +def layer_yaml(): + return """encoder_name: embed-english-v3.0 +encoder_type: cohere +routes: +- description: null + function_schema: null + name: politics + utterances: + - isn't politics the best thing ever + - why don't you tell me about your political opinions +- description: null + function_schema: null + name: chitchat + utterances: + - how's the weather today? + - how are things going? + """ + @pytest.fixture def base_encoder(): return BaseEncoder(name="test-encoder") @@ -67,30 +113,31 @@ def test_add_route(self, openai_encoder): route_layer.add(route=route1) assert route_layer.index is not None and route_layer.categories is not None - assert len(route_layer.index) == 2 + assert route_layer.index.shape[0] == 2 assert len(set(route_layer.categories)) == 1 assert set(route_layer.categories) == {"Route 1"} route_layer.add(route=route2) - assert len(route_layer.index) == 4 + assert route_layer.index.shape[0] == 4 assert len(set(route_layer.categories)) == 2 assert set(route_layer.categories) == {"Route 1", "Route 2"} + del route_layer def test_add_multiple_routes(self, openai_encoder, routes): route_layer = RouteLayer(encoder=openai_encoder) route_layer._add_routes(routes=routes) assert route_layer.index is not None and route_layer.categories is not None - assert len(route_layer.index) == 5 + assert route_layer.index.shape[0] == 5 assert len(set(route_layer.categories)) == 2 def test_query_and_classification(self, openai_encoder, routes): route_layer = RouteLayer(encoder=openai_encoder, routes=routes) - query_result = route_layer("Hello") + query_result = route_layer("Hello").name assert query_result in ["Route 1", "Route 2"] def test_query_with_no_index(self, openai_encoder): route_layer = RouteLayer(encoder=openai_encoder) - assert route_layer("Anything") is None + assert route_layer("Anything").name is None def test_semantic_classify(self, openai_encoder, routes): route_layer = RouteLayer(encoder=openai_encoder, routes=routes) @@ -126,3 +173,82 @@ def test_failover_score_threshold(self, base_encoder): # Add more tests for edge cases and error handling as needed. + + +class TestLayerConfig: + def test_init(self): + layer_config = LayerConfig() + assert layer_config.routes == [] + + def test_to_file_json(self): + route = Route(name="test", utterances=["utterance"]) + layer_config = LayerConfig(routes=[route]) + with patch("builtins.open", mock_open()) as mocked_open: + layer_config.to_file("data/test_output.json") + mocked_open.assert_called_once_with("data/test_output.json", "w") + + def test_to_file_yaml(self): + route = Route(name="test", utterances=["utterance"]) + layer_config = LayerConfig(routes=[route]) + with patch("builtins.open", mock_open()) as mocked_open: + layer_config.to_file("data/test_output.yaml") + mocked_open.assert_called_once_with("data/test_output.yaml", "w") + + def test_to_file_invalid(self): + route = Route(name="test", utterances=["utterance"]) + layer_config = LayerConfig(routes=[route]) + with pytest.raises(ValueError): + layer_config.to_file("test_output.txt") + + def test_from_file_json(self): + mock_json_data = layer_json() + with patch("builtins.open", mock_open(read_data=mock_json_data)) as mocked_open: + layer_config = LayerConfig.from_file("data/test.json") + mocked_open.assert_called_once_with("data/test.json", "r") + assert isinstance(layer_config, LayerConfig) + + def test_from_file_yaml(self): + mock_yaml_data = layer_yaml() + with patch("builtins.open", mock_open(read_data=mock_yaml_data)) as mocked_open: + layer_config = LayerConfig.from_file("data/test.yaml") + mocked_open.assert_called_once_with("data/test.yaml", "r") + assert isinstance(layer_config, LayerConfig) + + def test_from_file_invalid(self): + with open("test.txt", "w") as f: + f.write("dummy content") + with pytest.raises(ValueError): + LayerConfig.from_file("test.txt") + os.remove("test.txt") + + def test_to_dict(self): + route = Route(name="test", utterances=["utterance"]) + layer_config = LayerConfig(routes=[route]) + assert layer_config.to_dict()["routes"] == [route.to_dict()] + + def test_add(self): + route = Route(name="test", utterances=["utterance"]) + route2 = Route(name="test2", utterances=["utterance2"]) + layer_config = LayerConfig() + layer_config.add(route) + # confirm route added + assert layer_config.routes == [route] + # add second route and check updates + layer_config.add(route2) + assert layer_config.routes == [route, route2] + + def test_get(self): + route = Route(name="test", utterances=["utterance"]) + layer_config = LayerConfig(routes=[route]) + assert layer_config.get("test") == route + + def test_get_not_found(self): + route = Route(name="test", utterances=["utterance"]) + layer_config = LayerConfig(routes=[route]) + assert layer_config.get("not_found") is None + + def test_remove(self): + route = Route(name="test", utterances=["utterance"]) + layer_config = LayerConfig(routes=[route]) + layer_config.remove("test") + assert layer_config.routes == [] diff --git a/tests/unit/test_route.py b/tests/unit/test_route.py index 1de3f0e5..4e19db24 100644 --- a/tests/unit/test_route.py +++ b/tests/unit/test_route.py @@ -3,7 +3,7 @@ import pytest -from semantic_router.route import Route, RouteConfig, is_valid +from semantic_router.route import Route, is_valid # Is valid test: @@ -78,6 +78,7 @@ def test_to_dict(self): "name": "test", "utterances": ["utterance"], "description": None, + "function_schema": None, } assert route.to_dict() == expected_dict @@ -146,77 +147,3 @@ def test_parse_route_config(self): } """ assert Route._parse_route_config(config).strip() == expected_config.strip() - - -class TestRouteConfig: - def test_init(self): - route_config = RouteConfig() - assert route_config.routes == [] - - def test_to_file_json(self): - route = Route(name="test", utterances=["utterance"]) - route_config = RouteConfig(routes=[route]) - with patch("builtins.open", mock_open()) as mocked_open: - route_config.to_file("data/test_output.json") - mocked_open.assert_called_once_with("data/test_output.json", "w") - - def test_to_file_yaml(self): - route = Route(name="test", utterances=["utterance"]) - route_config = RouteConfig(routes=[route]) - with patch("builtins.open", mock_open()) as mocked_open: - route_config.to_file("data/test_output.yaml") - mocked_open.assert_called_once_with("data/test_output.yaml", "w") - - def test_to_file_invalid(self): - route = Route(name="test", utterances=["utterance"]) - route_config = RouteConfig(routes=[route]) - with pytest.raises(ValueError): - route_config.to_file("test_output.txt") - - def test_from_file_json(self): - mock_json_data = '[{"name": "test", "utterances": ["utterance"]}]' - with patch("builtins.open", mock_open(read_data=mock_json_data)) as mocked_open: - route_config = RouteConfig.from_file("data/test.json") - mocked_open.assert_called_once_with("data/test.json", "r") - assert isinstance(route_config, RouteConfig) - - def test_from_file_yaml(self): - mock_yaml_data = "- name: test\n utterances:\n - utterance" - with patch("builtins.open", mock_open(read_data=mock_yaml_data)) as mocked_open: - route_config = RouteConfig.from_file("data/test.yaml") - mocked_open.assert_called_once_with("data/test.yaml", "r") - assert isinstance(route_config, RouteConfig) - - def test_from_file_invalid(self): - with open("test.txt", "w") as f: - f.write("dummy content") - with pytest.raises(ValueError): - RouteConfig.from_file("test.txt") - os.remove("test.txt") - - def test_to_dict(self): - route = Route(name="test", utterances=["utterance"]) - route_config = RouteConfig(routes=[route]) - assert route_config.to_dict() == [route.to_dict()] - - def test_add(self): - route = Route(name="test", utterances=["utterance"]) - route_config = RouteConfig() - route_config.add(route) - assert route_config.routes == [route] - - def test_get(self): - route = Route(name="test", utterances=["utterance"]) - route_config = RouteConfig(routes=[route]) - assert route_config.get("test") == route - - def test_get_not_found(self): - route = Route(name="test", utterances=["utterance"]) - route_config = RouteConfig(routes=[route]) - assert route_config.get("not_found") is None - - def test_remove(self): - route = Route(name="test", utterances=["utterance"]) - route_config = RouteConfig(routes=[route]) - route_config.remove("test") - assert route_config.routes == [] From ccdef6d37c322dda75e3a0c850775f6ece45590f Mon Sep 17 00:00:00 2001 From: James Briggs <35938317+jamescalam@users.noreply.github.com> Date: Wed, 27 Dec 2023 09:41:39 +0100 Subject: [PATCH 05/17] lint --- docs/02_dynamic_routes.ipynb | 7 +++-- semantic_router/layer.py | 56 ++++++++++++------------------------ semantic_router/route.py | 8 ++---- tests/unit/test_layer.py | 2 ++ tests/unit/test_route.py | 3 +- tests/unit/test_schema.py | 1 - 6 files changed, 27 insertions(+), 50 deletions(-) diff --git a/docs/02_dynamic_routes.ipynb b/docs/02_dynamic_routes.ipynb index 61456a07..2ffc0ce0 100644 --- a/docs/02_dynamic_routes.ipynb +++ b/docs/02_dynamic_routes.ipynb @@ -136,9 +136,10 @@ "from datetime import datetime\n", "from zoneinfo import ZoneInfo\n", "\n", + "\n", "def get_time(timezone: str) -> str:\n", " \"\"\"Finds the current time in a specific timezone.\n", - " \n", + "\n", " :param timezone: The timezone to find the current time in, should\n", " be a valid timezone from the IANA Time Zone Database like\n", " \"America/New_York\" or \"Europe/London\".\n", @@ -219,9 +220,9 @@ " utterances=[\n", " \"what is the time in new york city?\",\n", " \"what is the time in london?\",\n", - " \"I live in Rome, what time is it?\"\n", + " \"I live in Rome, what time is it?\",\n", " ],\n", - " function_schema=schema\n", + " function_schema=schema,\n", ")" ] }, diff --git a/semantic_router/layer.py b/semantic_router/layer.py index da25f828..bd48f77f 100644 --- a/semantic_router/layer.py +++ b/semantic_router/layer.py @@ -92,9 +92,7 @@ def from_file(cls, path: str): encoder_name = layer["encoder_name"] routes = [Route.from_dict(route) for route in layer["routes"]] return cls( - encoder_type=encoder_type, - encoder_name=encoder_name, - routes=routes + encoder_type=encoder_type, encoder_name=encoder_name, routes=routes ) else: raise Exception("Invalid config JSON or YAML") @@ -103,7 +101,7 @@ def to_dict(self): return { "encoder_type": self.encoder_type, "encoder_name": self.encoder_name, - "routes": [route.to_dict() for route in self.routes] + "routes": [route.to_dict() for route in self.routes], } def to_file(self, path: str): @@ -145,9 +143,7 @@ class RouteLayer: score_threshold: float = 0.82 def __init__( - self, - encoder: BaseEncoder | None = None, - routes: list[Route] | None = None + self, encoder: BaseEncoder | None = None, routes: list[Route] | None = None ): logger.info("Initializing RouteLayer") self.index = None @@ -177,46 +173,30 @@ def __call__(self, text: str) -> RouteChoice: else: # if no route passes threshold, return empty route choice return RouteChoice() - + def __str__(self): - return (f"RouteLayer(encoder={self.encoder}, " - f"score_threshold={self.score_threshold}, " - f"routes={self.routes})") + return ( + f"RouteLayer(encoder={self.encoder}, " + f"score_threshold={self.score_threshold}, " + f"routes={self.routes})" + ) @classmethod def from_json(cls, file_path: str): config = LayerConfig.from_file(file_path) - encoder = Encoder( - type=config.encoder_type, - name=config.encoder_name - ) - return cls( - encoder=encoder, - routes=config.routes - ) + encoder = Encoder(type=config.encoder_type, name=config.encoder_name) + return cls(encoder=encoder, routes=config.routes) @classmethod def from_yaml(cls, file_path: str): config = LayerConfig.from_file(file_path) - encoder = Encoder( - type=config.encoder_type, - name=config.encoder_name - ) - return cls( - encoder=encoder, - routes=config.routes - ) - + encoder = Encoder(type=config.encoder_type, name=config.encoder_name) + return cls(encoder=encoder, routes=config.routes) + @classmethod def from_config(cls, config: LayerConfig): - encoder = Encoder( - type=config.encoder_type, - name=config.encoder_name - ) - return cls( - encoder=encoder, - routes=config.routes - ) + encoder = Encoder(type=config.encoder_type, name=config.encoder_name) + return cls(encoder=encoder, routes=config.routes) def add(self, route: Route): print(f"Adding route `{route.name}`") @@ -311,12 +291,12 @@ def _pass_threshold(self, scores: list[float], threshold: float) -> bool: return max(scores) > threshold else: return False - + def to_config(self) -> LayerConfig: return LayerConfig( encoder_type=self.encoder.type, encoder_name=self.encoder.name, - routes=self.routes + routes=self.routes, ) def to_json(self, file_path: str): diff --git a/semantic_router/route.py b/semantic_router/route.py index af75b211..1fa3291a 100644 --- a/semantic_router/route.py +++ b/semantic_router/route.py @@ -37,6 +37,7 @@ def is_valid(route_config: str) -> bool: logger.error(e) return False + class Route(BaseModel): name: str utterances: list[str] @@ -53,10 +54,7 @@ def __call__(self, query: str) -> RouteChoice: else: # otherwise we just pass None for the call func_call = None - return RouteChoice( - name=self.name, - function_call=func_call - ) + return RouteChoice(name=self.name, function_call=func_call) def to_dict(self): return self.dict() @@ -126,5 +124,3 @@ async def _generate_dynamic_route(cls, function_schema: dict[str, Any]): if is_valid(route_config): return Route.from_dict(json.loads(route_config)) raise Exception("No config generated") - - diff --git a/tests/unit/test_layer.py b/tests/unit/test_layer.py index 873e488a..386edf6d 100644 --- a/tests/unit/test_layer.py +++ b/tests/unit/test_layer.py @@ -45,6 +45,7 @@ def layer_json(): ] }""" + def layer_yaml(): return """encoder_name: embed-english-v3.0 encoder_type: cohere @@ -63,6 +64,7 @@ def layer_yaml(): - how are things going? """ + @pytest.fixture def base_encoder(): return BaseEncoder(name="test-encoder") diff --git a/tests/unit/test_route.py b/tests/unit/test_route.py index 4e19db24..2843ae40 100644 --- a/tests/unit/test_route.py +++ b/tests/unit/test_route.py @@ -1,5 +1,4 @@ -import os -from unittest.mock import AsyncMock, mock_open, patch +from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/unit/test_schema.py b/tests/unit/test_schema.py index 46799ee8..97b5028e 100644 --- a/tests/unit/test_schema.py +++ b/tests/unit/test_schema.py @@ -1,6 +1,5 @@ import pytest -from semantic_router.route import Route from semantic_router.schema import ( CohereEncoder, Encoder, From 4c49baf521f11ae11226740e3126b0a4c0eabae8 Mon Sep 17 00:00:00 2001 From: James Briggs <35938317+jamescalam@users.noreply.github.com> Date: Wed, 27 Dec 2023 19:14:58 +0100 Subject: [PATCH 06/17] lint --- semantic_router/layer.py | 8 ++++---- semantic_router/route.py | 2 +- semantic_router/schema.py | 10 ++++++---- semantic_router/utils/function_call.py | 4 ++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/semantic_router/layer.py b/semantic_router/layer.py index bd48f77f..dae040a5 100644 --- a/semantic_router/layer.py +++ b/semantic_router/layer.py @@ -55,7 +55,7 @@ class LayerConfig: def __init__( self, routes: list[Route] = [], - encoder_type: EncoderType = "openai", + encoder_type: str = "openai", encoder_name: str | None = None, ): self.encoder_type = encoder_type @@ -184,18 +184,18 @@ def __str__(self): @classmethod def from_json(cls, file_path: str): config = LayerConfig.from_file(file_path) - encoder = Encoder(type=config.encoder_type, name=config.encoder_name) + encoder = Encoder(type=config.encoder_type, name=config.encoder_name).model return cls(encoder=encoder, routes=config.routes) @classmethod def from_yaml(cls, file_path: str): config = LayerConfig.from_file(file_path) - encoder = Encoder(type=config.encoder_type, name=config.encoder_name) + encoder = Encoder(type=config.encoder_type, name=config.encoder_name).model return cls(encoder=encoder, routes=config.routes) @classmethod def from_config(cls, config: LayerConfig): - encoder = Encoder(type=config.encoder_type, name=config.encoder_name) + encoder = Encoder(type=config.encoder_type, name=config.encoder_name).model return cls(encoder=encoder, routes=config.routes) def add(self, route: Route): diff --git a/semantic_router/route.py b/semantic_router/route.py index 1fa3291a..06ebf8f3 100644 --- a/semantic_router/route.py +++ b/semantic_router/route.py @@ -113,7 +113,7 @@ async def _generate_dynamic_route(cls, function_schema: dict[str, Any]): {function_schema} """ - output = await llm(prompt) + output = llm(prompt) if not output: raise Exception("No output generated for dynamic route") diff --git a/semantic_router/schema.py b/semantic_router/schema.py index a3d786db..63233322 100644 --- a/semantic_router/schema.py +++ b/semantic_router/schema.py @@ -23,12 +23,12 @@ class RouteChoice(BaseModel): @dataclass class Encoder: - type: EncoderType - name: str + type: str + name: str | None model: BaseEncoder - def __init__(self, type: str, name: str): - self.type = EncoderType(type) + def __init__(self, type: str, name: str | None): + self.type = type self.name = name if self.type == EncoderType.HUGGINGFACE: raise NotImplementedError @@ -36,6 +36,8 @@ def __init__(self, type: str, name: str): self.model = OpenAIEncoder(name) elif self.type == EncoderType.COHERE: self.model = CohereEncoder(name) + else: + raise NotImplementedError def __call__(self, texts: list[str]) -> list[list[float]]: return self.model(texts) diff --git a/semantic_router/utils/function_call.py b/semantic_router/utils/function_call.py index 9504dfb8..2ead3ab5 100644 --- a/semantic_router/utils/function_call.py +++ b/semantic_router/utils/function_call.py @@ -117,11 +117,11 @@ async def route_and_execute(query: str, functions: list[Callable], route_layer): function_name = route_layer(query) if not function_name: logger.warning("No function found, calling LLM...") - return await llm(query) + return llm(query) for function in functions: if function.__name__ == function_name: print(f"Calling function: {function.__name__}") schema = get_schema(function) - inputs = await extract_function_inputs(query, schema) + inputs = extract_function_inputs(query, schema) call_function(function, inputs) From b6d5c4f71ffeca62713a8c8c12653df16ab92028 Mon Sep 17 00:00:00 2001 From: James Briggs <35938317+jamescalam@users.noreply.github.com> Date: Wed, 27 Dec 2023 19:31:47 +0100 Subject: [PATCH 07/17] fix for Encoder class --- semantic_router/schema.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/semantic_router/schema.py b/semantic_router/schema.py index 63233322..be486888 100644 --- a/semantic_router/schema.py +++ b/semantic_router/schema.py @@ -23,12 +23,12 @@ class RouteChoice(BaseModel): @dataclass class Encoder: - type: str + type: EncoderType name: str | None model: BaseEncoder def __init__(self, type: str, name: str | None): - self.type = type + self.type = EncoderType(type) self.name = name if self.type == EncoderType.HUGGINGFACE: raise NotImplementedError @@ -37,7 +37,7 @@ def __init__(self, type: str, name: str | None): elif self.type == EncoderType.COHERE: self.model = CohereEncoder(name) else: - raise NotImplementedError + raise ValueError def __call__(self, texts: list[str]) -> list[list[float]]: return self.model(texts) From 8b243bb6f12353631e66f83f67aeec82bb1d78d5 Mon Sep 17 00:00:00 2001 From: James Briggs <35938317+jamescalam@users.noreply.github.com> Date: Thu, 28 Dec 2023 09:05:33 +0100 Subject: [PATCH 08/17] sync tests by default, will add async back in later --- poetry.lock | 12 ++++- pyproject.toml | 2 +- semantic_router/encoders/cohere.py | 4 +- semantic_router/encoders/openai.py | 4 +- semantic_router/route.py | 6 +-- tests/unit/encoders/test_openai.py | 1 - tests/unit/test_route.py | 79 ++++++++++++++++++++++++++---- 7 files changed, 91 insertions(+), 17 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7efeda7e..63248ed2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiohttp" @@ -1716,6 +1716,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1723,8 +1724,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {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_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"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1741,6 +1749,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1748,6 +1757,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, diff --git a/pyproject.toml b/pyproject.toml index 0741dac0..71ef163b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "semantic-router" -version = "0.0.12" +version = "0.0.13" description = "Super fast semantic router for AI decision making" authors = [ "James Briggs ", diff --git a/semantic_router/encoders/cohere.py b/semantic_router/encoders/cohere.py index ae0db7a2..f7aef0e6 100644 --- a/semantic_router/encoders/cohere.py +++ b/semantic_router/encoders/cohere.py @@ -11,9 +11,11 @@ class CohereEncoder(BaseEncoder): def __init__( self, - name: str = os.getenv("COHERE_MODEL_NAME", "embed-english-v3.0"), + name: str | None = None, cohere_api_key: str | None = None, ): + if name is None: + name = os.getenv("COHERE_MODEL_NAME", "embed-english-v3.0") super().__init__(name=name) cohere_api_key = cohere_api_key or os.getenv("COHERE_API_KEY") if cohere_api_key is None: diff --git a/semantic_router/encoders/openai.py b/semantic_router/encoders/openai.py index 9744401f..173fe94a 100644 --- a/semantic_router/encoders/openai.py +++ b/semantic_router/encoders/openai.py @@ -15,9 +15,11 @@ class OpenAIEncoder(BaseEncoder): def __init__( self, - name: str = os.getenv("OPENAI_MODEL_NAME", "text-embedding-ada-002"), + name: str | None = None, openai_api_key: str | None = None, ): + if name is None: + name = os.getenv("OPENAI_MODEL_NAME", "text-embedding-ada-002") super().__init__(name=name) api_key = openai_api_key or os.getenv("OPENAI_API_KEY") if api_key is None: diff --git a/semantic_router/route.py b/semantic_router/route.py index 06ebf8f3..30c20887 100644 --- a/semantic_router/route.py +++ b/semantic_router/route.py @@ -64,12 +64,12 @@ def from_dict(cls, data: dict): return cls(**data) @classmethod - async def from_dynamic_route(cls, entity: Union[BaseModel, Callable]): + def from_dynamic_route(cls, entity: Union[BaseModel, Callable]): """ Generate a dynamic Route object from a function or Pydantic model using LLM """ schema = function_call.get_schema(item=entity) - dynamic_route = await cls._generate_dynamic_route(function_schema=schema) + dynamic_route = cls._generate_dynamic_route(function_schema=schema) return dynamic_route @classmethod @@ -85,7 +85,7 @@ def _parse_route_config(cls, config: str) -> str: raise ValueError("No tags found in the output.") @classmethod - async def _generate_dynamic_route(cls, function_schema: dict[str, Any]): + def _generate_dynamic_route(cls, function_schema: dict[str, Any]): logger.info("Generating dynamic route...") prompt = f""" diff --git a/tests/unit/encoders/test_openai.py b/tests/unit/encoders/test_openai.py index cc79d272..fb0e604f 100644 --- a/tests/unit/encoders/test_openai.py +++ b/tests/unit/encoders/test_openai.py @@ -22,7 +22,6 @@ def test_openai_encoder_init_no_api_key(self, mocker): mocker.patch("os.getenv", return_value=None) with pytest.raises(ValueError) as e: OpenAIEncoder() - assert "OpenAI API key cannot be 'None'." in str(e.value) def test_openai_encoder_call_uninitialized_client(self, openai_encoder): # Set the client to None to simulate an uninitialized client diff --git a/tests/unit/test_route.py b/tests/unit/test_route.py index 2843ae40..0a9a6eba 100644 --- a/tests/unit/test_route.py +++ b/tests/unit/test_route.py @@ -1,4 +1,4 @@ -from unittest.mock import AsyncMock, patch +from unittest.mock import Mock, AsyncMock, patch import pytest @@ -43,9 +43,8 @@ def test_is_valid_with_invalid_json(): class TestRoute: - @pytest.mark.asyncio - @patch("semantic_router.route.llm", new_callable=AsyncMock) - async def test_generate_dynamic_route(self, mock_llm): + @patch("semantic_router.route.llm", new_callable=Mock) + def test_generate_dynamic_route(self, mock_llm): print(f"mock_llm: {mock_llm}") mock_llm.return_value = """ @@ -61,7 +60,7 @@ async def test_generate_dynamic_route(self, mock_llm): """ function_schema = {"name": "test_function", "type": "function"} - route = await Route._generate_dynamic_route(function_schema) + route = Route._generate_dynamic_route(function_schema) assert route.name == "test_function" assert route.utterances == [ "example_utterance_1", @@ -71,6 +70,35 @@ async def test_generate_dynamic_route(self, mock_llm): "example_utterance_5", ] + # TODO add async version + # @pytest.mark.asyncio + # @patch("semantic_router.route.allm", new_callable=Mock) + # async def test_generate_dynamic_route_async(self, mock_llm): + # print(f"mock_llm: {mock_llm}") + # mock_llm.return_value = """ + # + # { + # "name": "test_function", + # "utterances": [ + # "example_utterance_1", + # "example_utterance_2", + # "example_utterance_3", + # "example_utterance_4", + # "example_utterance_5"] + # } + # + # """ + # function_schema = {"name": "test_function", "type": "function"} + # route = await Route._generate_dynamic_route(function_schema) + # assert route.name == "test_function" + # assert route.utterances == [ + # "example_utterance_1", + # "example_utterance_2", + # "example_utterance_3", + # "example_utterance_4", + # "example_utterance_5", + # ] + def test_to_dict(self): route = Route(name="test", utterances=["utterance"]) expected_dict = { @@ -87,9 +115,8 @@ def test_from_dict(self): assert route.name == "test" assert route.utterances == ["utterance"] - @pytest.mark.asyncio - @patch("semantic_router.route.llm", new_callable=AsyncMock) - async def test_from_dynamic_route(self, mock_llm): + @patch("semantic_router.route.llm", new_callable=Mock) + def test_from_dynamic_route(self, mock_llm): # Mock the llm function mock_llm.return_value = """ @@ -109,7 +136,7 @@ def test_function(input: str): """Test function docstring""" pass - dynamic_route = await Route.from_dynamic_route(test_function) + dynamic_route = Route.from_dynamic_route(test_function) assert dynamic_route.name == "test_function" assert dynamic_route.utterances == [ @@ -120,6 +147,40 @@ def test_function(input: str): "example_utterance_5", ] + # TODO add async functions + # @pytest.mark.asyncio + # @patch("semantic_router.route.allm", new_callable=AsyncMock) + # async def test_from_dynamic_route_async(self, mock_llm): + # # Mock the llm function + # mock_llm.return_value = """ + # + # { + # "name": "test_function", + # "utterances": [ + # "example_utterance_1", + # "example_utterance_2", + # "example_utterance_3", + # "example_utterance_4", + # "example_utterance_5"] + # } + # + # """ + + # def test_function(input: str): + # """Test function docstring""" + # pass + + # dynamic_route = await Route.from_dynamic_route(test_function) + + # assert dynamic_route.name == "test_function" + # assert dynamic_route.utterances == [ + # "example_utterance_1", + # "example_utterance_2", + # "example_utterance_3", + # "example_utterance_4", + # "example_utterance_5", + # ] + def test_parse_route_config(self): config = """ From 6bc16ca1456d60f3ff155ce2cc2bd1da678e2a02 Mon Sep 17 00:00:00 2001 From: James Briggs <35938317+jamescalam@users.noreply.github.com> Date: Thu, 28 Dec 2023 09:26:04 +0100 Subject: [PATCH 09/17] lint --- tests/unit/encoders/test_openai.py | 2 +- tests/unit/test_route.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/encoders/test_openai.py b/tests/unit/encoders/test_openai.py index fb0e604f..4679ee93 100644 --- a/tests/unit/encoders/test_openai.py +++ b/tests/unit/encoders/test_openai.py @@ -20,7 +20,7 @@ def test_openai_encoder_init_success(self, mocker): def test_openai_encoder_init_no_api_key(self, mocker): mocker.patch("os.getenv", return_value=None) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as _: OpenAIEncoder() def test_openai_encoder_call_uninitialized_client(self, openai_encoder): diff --git a/tests/unit/test_route.py b/tests/unit/test_route.py index 0a9a6eba..44cf2276 100644 --- a/tests/unit/test_route.py +++ b/tests/unit/test_route.py @@ -1,6 +1,6 @@ -from unittest.mock import Mock, AsyncMock, patch +from unittest.mock import Mock, patch # , AsyncMock -import pytest +# import pytest from semantic_router.route import Route, is_valid From d96ec2ea3ac8cbe88b3376cad3ecea0e1fe3e47c Mon Sep 17 00:00:00 2001 From: James Briggs <35938317+jamescalam@users.noreply.github.com> Date: Thu, 28 Dec 2023 09:46:10 +0100 Subject: [PATCH 10/17] added tests to cover route layer save/load --- semantic_router/utils/llm.py | 56 ++++++++++++++++++------------------ test_output.json | 1 - test_output.yaml | 4 --- tests/unit/test_layer.py | 26 +++++++++++++++++ 4 files changed, 54 insertions(+), 33 deletions(-) delete mode 100644 test_output.json delete mode 100644 test_output.yaml diff --git a/semantic_router/utils/llm.py b/semantic_router/utils/llm.py index 0d22b9a6..6ce28ff8 100644 --- a/semantic_router/utils/llm.py +++ b/semantic_router/utils/llm.py @@ -33,31 +33,31 @@ def llm(prompt: str) -> str | None: logger.error(f"LLM error: {e}") raise Exception(f"LLM error: {e}") - -async def allm(prompt: str) -> str | None: - try: - client = openai.AsyncOpenAI( - base_url="https://openrouter.ai/api/v1", - api_key=os.getenv("OPENROUTER_API_KEY"), - ) - - completion = await client.chat.completions.create( - model="mistralai/mistral-7b-instruct", - messages=[ - { - "role": "user", - "content": prompt, - }, - ], - temperature=0.01, - max_tokens=200, - ) - - output = completion.choices[0].message.content - - if not output: - raise Exception("No output generated") - return output - except Exception as e: - logger.error(f"LLM error: {e}") - raise Exception(f"LLM error: {e}") +# TODO integrate async LLM function +# async def allm(prompt: str) -> str | None: +# try: +# client = openai.AsyncOpenAI( +# base_url="https://openrouter.ai/api/v1", +# api_key=os.getenv("OPENROUTER_API_KEY"), +# ) + +# completion = await client.chat.completions.create( +# model="mistralai/mistral-7b-instruct", +# messages=[ +# { +# "role": "user", +# "content": prompt, +# }, +# ], +# temperature=0.01, +# max_tokens=200, +# ) + +# output = completion.choices[0].message.content + +# if not output: +# raise Exception("No output generated") +# return output +# except Exception as e: +# logger.error(f"LLM error: {e}") +# raise Exception(f"LLM error: {e}") diff --git a/test_output.json b/test_output.json deleted file mode 100644 index 1f930085..00000000 --- a/test_output.json +++ /dev/null @@ -1 +0,0 @@ -[{"name": "test", "utterances": ["utterance"], "description": null}] diff --git a/test_output.yaml b/test_output.yaml deleted file mode 100644 index b7167647..00000000 --- a/test_output.yaml +++ /dev/null @@ -1,4 +0,0 @@ -- description: null - name: test - utterances: - - utterance diff --git a/tests/unit/test_layer.py b/tests/unit/test_layer.py index 386edf6d..c6898235 100644 --- a/tests/unit/test_layer.py +++ b/tests/unit/test_layer.py @@ -173,6 +173,32 @@ def test_failover_score_threshold(self, base_encoder): route_layer = RouteLayer(encoder=base_encoder) assert route_layer.score_threshold == 0.82 + def test_json(self, openai_encoder, routes): + route_layer = RouteLayer(encoder=openai_encoder, routes=routes) + route_layer.to_json("test_output.json") + assert os.path.exists("test_output.json") + route_layer_from_file = RouteLayer.from_json("test_output.json") + assert route_layer_from_file.index is not None and route_layer_from_file.categories is not None + os.remove("test_output.json") + + def test_yaml(self, openai_encoder, routes): + route_layer = RouteLayer(encoder=openai_encoder, routes=routes) + route_layer.to_yaml("test_output.yaml") + assert os.path.exists("test_output.yaml") + route_layer_from_file = RouteLayer.from_yaml("test_output.yaml") + assert route_layer_from_file.index is not None and route_layer_from_file.categories is not None + os.remove("test_output.yaml") + + def test_config(self, openai_encoder, routes): + route_layer = RouteLayer(encoder=openai_encoder, routes=routes) + # confirm route creation functions as expected + layer_config = route_layer.to_config() + assert layer_config.routes == routes + # now load from config and confirm it's the same + route_layer_from_config = RouteLayer.from_config(layer_config) + assert (route_layer_from_config.index == route_layer.index).all() + assert (route_layer_from_config.categories == route_layer.categories).all() + assert route_layer_from_config.score_threshold == route_layer.score_threshold # Add more tests for edge cases and error handling as needed. From 9a9249a243c2c962e50371a99aea60c006bdb2bd Mon Sep 17 00:00:00 2001 From: James Briggs <35938317+jamescalam@users.noreply.github.com> Date: Thu, 28 Dec 2023 09:54:06 +0100 Subject: [PATCH 11/17] fix tests env var and lint --- .github/workflows/pr_agent.yml | 2 +- semantic_router/utils/llm.py | 1 + tests/unit/test_layer.py | 11 +++++++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr_agent.yml b/.github/workflows/pr_agent.yml index 4e86dfbc..e9db72d8 100644 --- a/.github/workflows/pr_agent.yml +++ b/.github/workflows/pr_agent.yml @@ -14,5 +14,5 @@ jobs: id: pragent uses: Codium-ai/pr-agent@main env: - OPENAI_KEY: ${{ secrets.OPENAI_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/semantic_router/utils/llm.py b/semantic_router/utils/llm.py index 6ce28ff8..e92c1bcf 100644 --- a/semantic_router/utils/llm.py +++ b/semantic_router/utils/llm.py @@ -33,6 +33,7 @@ def llm(prompt: str) -> str | None: logger.error(f"LLM error: {e}") raise Exception(f"LLM error: {e}") + # TODO integrate async LLM function # async def allm(prompt: str) -> str | None: # try: diff --git a/tests/unit/test_layer.py b/tests/unit/test_layer.py index c6898235..4520adf4 100644 --- a/tests/unit/test_layer.py +++ b/tests/unit/test_layer.py @@ -178,7 +178,10 @@ def test_json(self, openai_encoder, routes): route_layer.to_json("test_output.json") assert os.path.exists("test_output.json") route_layer_from_file = RouteLayer.from_json("test_output.json") - assert route_layer_from_file.index is not None and route_layer_from_file.categories is not None + assert ( + route_layer_from_file.index is not None + and route_layer_from_file.categories is not None + ) os.remove("test_output.json") def test_yaml(self, openai_encoder, routes): @@ -186,7 +189,10 @@ def test_yaml(self, openai_encoder, routes): route_layer.to_yaml("test_output.yaml") assert os.path.exists("test_output.yaml") route_layer_from_file = RouteLayer.from_yaml("test_output.yaml") - assert route_layer_from_file.index is not None and route_layer_from_file.categories is not None + assert ( + route_layer_from_file.index is not None + and route_layer_from_file.categories is not None + ) os.remove("test_output.yaml") def test_config(self, openai_encoder, routes): @@ -200,6 +206,7 @@ def test_config(self, openai_encoder, routes): assert (route_layer_from_config.categories == route_layer.categories).all() assert route_layer_from_config.score_threshold == route_layer.score_threshold + # Add more tests for edge cases and error handling as needed. From 9a4bea83668f15e3b8c0eeb80718ab941223c195 Mon Sep 17 00:00:00 2001 From: James Briggs <35938317+jamescalam@users.noreply.github.com> Date: Thu, 28 Dec 2023 10:14:36 +0100 Subject: [PATCH 12/17] add mock for encoder in new tests --- tests/unit/test_layer.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/tests/unit/test_layer.py b/tests/unit/test_layer.py index 4520adf4..3d1b6604 100644 --- a/tests/unit/test_layer.py +++ b/tests/unit/test_layer.py @@ -1,6 +1,6 @@ import os import pytest -from unittest.mock import mock_open, patch +from unittest.mock import Mock, mock_open, patch from semantic_router.encoders import BaseEncoder, CohereEncoder, OpenAIEncoder from semantic_router.layer import LayerConfig, RouteLayer @@ -177,22 +177,24 @@ def test_json(self, openai_encoder, routes): route_layer = RouteLayer(encoder=openai_encoder, routes=routes) route_layer.to_json("test_output.json") assert os.path.exists("test_output.json") - route_layer_from_file = RouteLayer.from_json("test_output.json") - assert ( - route_layer_from_file.index is not None - and route_layer_from_file.categories is not None - ) + with patch("semantic_router.schema.Encoder", new_callable=Mock): + route_layer_from_file = RouteLayer.from_json("test_output.json") + assert ( + route_layer_from_file.index is not None + and route_layer_from_file.categories is not None + ) os.remove("test_output.json") def test_yaml(self, openai_encoder, routes): route_layer = RouteLayer(encoder=openai_encoder, routes=routes) route_layer.to_yaml("test_output.yaml") assert os.path.exists("test_output.yaml") - route_layer_from_file = RouteLayer.from_yaml("test_output.yaml") - assert ( - route_layer_from_file.index is not None - and route_layer_from_file.categories is not None - ) + with patch("semantic_router.schema.Encoder", new_callable=Mock): + route_layer_from_file = RouteLayer.from_yaml("test_output.yaml") + assert ( + route_layer_from_file.index is not None + and route_layer_from_file.categories is not None + ) os.remove("test_output.yaml") def test_config(self, openai_encoder, routes): @@ -200,11 +202,12 @@ def test_config(self, openai_encoder, routes): # confirm route creation functions as expected layer_config = route_layer.to_config() assert layer_config.routes == routes - # now load from config and confirm it's the same - route_layer_from_config = RouteLayer.from_config(layer_config) - assert (route_layer_from_config.index == route_layer.index).all() - assert (route_layer_from_config.categories == route_layer.categories).all() - assert route_layer_from_config.score_threshold == route_layer.score_threshold + with patch("semantic_router.schema.Encoder", new_callable=Mock): + # now load from config and confirm it's the same + route_layer_from_config = RouteLayer.from_config(layer_config) + assert (route_layer_from_config.index == route_layer.index).all() + assert (route_layer_from_config.categories == route_layer.categories).all() + assert route_layer_from_config.score_threshold == route_layer.score_threshold # Add more tests for edge cases and error handling as needed. From ce2867c237c9372040cf0475e13902208adc0f7b Mon Sep 17 00:00:00 2001 From: James Briggs <35938317+jamescalam@users.noreply.github.com> Date: Thu, 28 Dec 2023 10:19:18 +0100 Subject: [PATCH 13/17] update encoder mock --- tests/unit/test_layer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_layer.py b/tests/unit/test_layer.py index 3d1b6604..f94c84d0 100644 --- a/tests/unit/test_layer.py +++ b/tests/unit/test_layer.py @@ -177,7 +177,7 @@ def test_json(self, openai_encoder, routes): route_layer = RouteLayer(encoder=openai_encoder, routes=routes) route_layer.to_json("test_output.json") assert os.path.exists("test_output.json") - with patch("semantic_router.schema.Encoder", new_callable=Mock): + with patch("semantic_router.encoders.OpenAIEncoder", new_callable=Mock): route_layer_from_file = RouteLayer.from_json("test_output.json") assert ( route_layer_from_file.index is not None @@ -189,7 +189,7 @@ def test_yaml(self, openai_encoder, routes): route_layer = RouteLayer(encoder=openai_encoder, routes=routes) route_layer.to_yaml("test_output.yaml") assert os.path.exists("test_output.yaml") - with patch("semantic_router.schema.Encoder", new_callable=Mock): + with patch("semantic_router.encoders.OpenAIEncoder", new_callable=Mock): route_layer_from_file = RouteLayer.from_yaml("test_output.yaml") assert ( route_layer_from_file.index is not None @@ -202,7 +202,7 @@ def test_config(self, openai_encoder, routes): # confirm route creation functions as expected layer_config = route_layer.to_config() assert layer_config.routes == routes - with patch("semantic_router.schema.Encoder", new_callable=Mock): + with patch("semantic_router.encoders.OpenAIEncoder", new_callable=Mock): # now load from config and confirm it's the same route_layer_from_config = RouteLayer.from_config(layer_config) assert (route_layer_from_config.index == route_layer.index).all() From 726bc11fe7b19581657cd555aa3b3f8741a7eb64 Mon Sep 17 00:00:00 2001 From: James Briggs <35938317+jamescalam@users.noreply.github.com> Date: Thu, 28 Dec 2023 10:47:11 +0100 Subject: [PATCH 14/17] add placeholder api key --- tests/unit/test_layer.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/unit/test_layer.py b/tests/unit/test_layer.py index f94c84d0..3a4adcc7 100644 --- a/tests/unit/test_layer.py +++ b/tests/unit/test_layer.py @@ -174,27 +174,26 @@ def test_failover_score_threshold(self, base_encoder): assert route_layer.score_threshold == 0.82 def test_json(self, openai_encoder, routes): + os.environ["OPENAI_API_KEY"] = "test_api_key" route_layer = RouteLayer(encoder=openai_encoder, routes=routes) route_layer.to_json("test_output.json") assert os.path.exists("test_output.json") - with patch("semantic_router.encoders.OpenAIEncoder", new_callable=Mock): - route_layer_from_file = RouteLayer.from_json("test_output.json") - assert ( - route_layer_from_file.index is not None - and route_layer_from_file.categories is not None - ) + route_layer_from_file = RouteLayer.from_json("test_output.json") + assert ( + route_layer_from_file.index is not None + and route_layer_from_file.categories is not None + ) os.remove("test_output.json") def test_yaml(self, openai_encoder, routes): route_layer = RouteLayer(encoder=openai_encoder, routes=routes) route_layer.to_yaml("test_output.yaml") assert os.path.exists("test_output.yaml") - with patch("semantic_router.encoders.OpenAIEncoder", new_callable=Mock): - route_layer_from_file = RouteLayer.from_yaml("test_output.yaml") - assert ( - route_layer_from_file.index is not None - and route_layer_from_file.categories is not None - ) + route_layer_from_file = RouteLayer.from_yaml("test_output.yaml") + assert ( + route_layer_from_file.index is not None + and route_layer_from_file.categories is not None + ) os.remove("test_output.yaml") def test_config(self, openai_encoder, routes): From d16f5891c023d3da3e30b3a97fb8db905f896ad5 Mon Sep 17 00:00:00 2001 From: James Briggs <35938317+jamescalam@users.noreply.github.com> Date: Thu, 28 Dec 2023 10:47:41 +0100 Subject: [PATCH 15/17] lint --- tests/unit/test_layer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_layer.py b/tests/unit/test_layer.py index 3a4adcc7..43f8a0c1 100644 --- a/tests/unit/test_layer.py +++ b/tests/unit/test_layer.py @@ -206,7 +206,9 @@ def test_config(self, openai_encoder, routes): route_layer_from_config = RouteLayer.from_config(layer_config) assert (route_layer_from_config.index == route_layer.index).all() assert (route_layer_from_config.categories == route_layer.categories).all() - assert route_layer_from_config.score_threshold == route_layer.score_threshold + assert ( + route_layer_from_config.score_threshold == route_layer.score_threshold + ) # Add more tests for edge cases and error handling as needed. From 7e4fe6d5cb2eeee3d0fb0e6bec93a93289840a57 Mon Sep 17 00:00:00 2001 From: James Briggs <35938317+jamescalam@users.noreply.github.com> Date: Thu, 28 Dec 2023 10:54:15 +0100 Subject: [PATCH 16/17] add env var to other tests --- tests/unit/test_layer.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/unit/test_layer.py b/tests/unit/test_layer.py index 43f8a0c1..1c8c18f2 100644 --- a/tests/unit/test_layer.py +++ b/tests/unit/test_layer.py @@ -1,6 +1,6 @@ import os import pytest -from unittest.mock import Mock, mock_open, patch +from unittest.mock import mock_open, patch from semantic_router.encoders import BaseEncoder, CohereEncoder, OpenAIEncoder from semantic_router.layer import LayerConfig, RouteLayer @@ -186,6 +186,7 @@ def test_json(self, openai_encoder, routes): os.remove("test_output.json") def test_yaml(self, openai_encoder, routes): + os.environ["OPENAI_API_KEY"] = "test_api_key" route_layer = RouteLayer(encoder=openai_encoder, routes=routes) route_layer.to_yaml("test_output.yaml") assert os.path.exists("test_output.yaml") @@ -197,18 +198,18 @@ def test_yaml(self, openai_encoder, routes): os.remove("test_output.yaml") def test_config(self, openai_encoder, routes): + os.environ["OPENAI_API_KEY"] = "test_api_key" route_layer = RouteLayer(encoder=openai_encoder, routes=routes) # confirm route creation functions as expected layer_config = route_layer.to_config() assert layer_config.routes == routes - with patch("semantic_router.encoders.OpenAIEncoder", new_callable=Mock): - # now load from config and confirm it's the same - route_layer_from_config = RouteLayer.from_config(layer_config) - assert (route_layer_from_config.index == route_layer.index).all() - assert (route_layer_from_config.categories == route_layer.categories).all() - assert ( - route_layer_from_config.score_threshold == route_layer.score_threshold - ) + # now load from config and confirm it's the same + route_layer_from_config = RouteLayer.from_config(layer_config) + assert (route_layer_from_config.index == route_layer.index).all() + assert (route_layer_from_config.categories == route_layer.categories).all() + assert ( + route_layer_from_config.score_threshold == route_layer.score_threshold + ) # Add more tests for edge cases and error handling as needed. From e8f0b2345ffdd89350fb59e0c1341ab66ced4df4 Mon Sep 17 00:00:00 2001 From: James Briggs <35938317+jamescalam@users.noreply.github.com> Date: Thu, 28 Dec 2023 11:03:51 +0100 Subject: [PATCH 17/17] lint --- tests/unit/test_layer.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/unit/test_layer.py b/tests/unit/test_layer.py index 1c8c18f2..b8f31793 100644 --- a/tests/unit/test_layer.py +++ b/tests/unit/test_layer.py @@ -207,9 +207,7 @@ def test_config(self, openai_encoder, routes): route_layer_from_config = RouteLayer.from_config(layer_config) assert (route_layer_from_config.index == route_layer.index).all() assert (route_layer_from_config.categories == route_layer.categories).all() - assert ( - route_layer_from_config.score_threshold == route_layer.score_threshold - ) + assert route_layer_from_config.score_threshold == route_layer.score_threshold # Add more tests for edge cases and error handling as needed.