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] 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\"] = \"<YOUR_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=<EncoderType.COHERE: 'cohere'>, name='embed-english-v3.0', model=CohereEncoder(name='embed-english-v3.0', type='cohere', client=<cohere.client.Client object at 0x12e40d510>)), 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\"] = \"<YOUR_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': \"<class 'str'>\"}" + ] + }, + "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\"] = \"<YOUR_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 @@ from pydantic import BaseModel 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 @@ from semantic_router.encoders import BaseEncoder 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 @@ from semantic_router.encoders import BaseEncoder 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 @@ from semantic_router.utils.logger import logger 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.route import Route 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 @@ class LayerConfig: _, 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 @@ class LayerConfig: _, 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 @@ class RouteLayer: 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 @@ class RouteLayer: 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 @@ class RouteLayer: @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 @@ class RouteLayer: 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.utils.logger import logger 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 @@ class Route(BaseModel): 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): -- GitLab