From 7334989dbe1b7edefcafb7af4993e58ec0a62c1f Mon Sep 17 00:00:00 2001
From: Simonas <20096648+simjak@users.noreply.github.com>
Date: Wed, 20 Dec 2023 17:16:35 +0200
Subject: [PATCH] test + lint

---
 Makefile                        |    2 +-
 coverage.xml                    | 1016 +++++++++++++++++--------------
 poetry.lock                     |   31 +-
 pyproject.toml                  |    2 +
 semantic_router/__init__.py     |    3 +-
 semantic_router/layer.py        |    3 +-
 semantic_router/route.py        |    7 +-
 tests/unit/test_hybrid_layer.py |    2 +-
 tests/unit/test_layer.py        |    6 +-
 tests/unit/test_route.py        |  222 +++++++
 tests/unit/test_route_config.py |   80 ---
 tests/unit/test_schema.py       |    2 +-
 12 files changed, 807 insertions(+), 569 deletions(-)
 create mode 100644 tests/unit/test_route.py
 delete mode 100644 tests/unit/test_route_config.py

diff --git a/Makefile b/Makefile
index 8de202fa..aeb3d3b1 100644
--- a/Makefile
+++ b/Makefile
@@ -12,4 +12,4 @@ lint lint_diff:
 	poetry run mypy $(PYTHON_FILES)
 
 test:
-	poetry run pytest -vv -n 20 --cov=semantic_router --cov-report=term-missing --cov-report=xml --cov-fail-under=100
+	poetry run pytest -vv -n 20 --cov=semantic_router --cov-report=term-missing --cov-report=xml --cov-fail-under=80
diff --git a/coverage.xml b/coverage.xml
index 001746f7..628f2950 100644
--- a/coverage.xml
+++ b/coverage.xml
@@ -1,569 +1,637 @@
 <?xml version="1.0" ?>
-<coverage version="7.3.3" timestamp="1702982262095" lines-valid="486" lines-covered="5" line-rate="0.01029" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0">
+<coverage version="7.3.3" timestamp="1703085147401" lines-valid="544" lines-covered="470" line-rate="0.864" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0">
 	<!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.3.3 -->
 	<!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
 	<sources>
 		<source>/Users/jakit/customers/aurelio/semantic-router/semantic_router</source>
 	</sources>
 	<packages>
-		<package name="." line-rate="0.01404" branch-rate="0" complexity="0">
+		<package name="." line-rate="0.9527" branch-rate="0" complexity="0">
 			<classes>
-				<class name="__init__.py" filename="__init__.py" complexity="0" line-rate="0.25" branch-rate="0">
+				<class name="__init__.py" filename="__init__.py" complexity="0" line-rate="1" branch-rate="0">
 					<methods/>
 					<lines>
 						<line number="1" hits="1"/>
-						<line number="2" hits="0"/>
-						<line number="3" hits="0"/>
-						<line number="5" hits="0"/>
+						<line number="2" hits="1"/>
+						<line number="3" hits="1"/>
+						<line number="5" hits="1"/>
 					</lines>
 				</class>
-				<class name="hybrid_layer.py" filename="hybrid_layer.py" complexity="0" line-rate="0.04444" branch-rate="0">
+				<class name="hybrid_layer.py" filename="hybrid_layer.py" complexity="0" line-rate="1" branch-rate="0">
 					<methods/>
 					<lines>
 						<line number="1" hits="1"/>
 						<line number="2" hits="1"/>
 						<line number="3" hits="1"/>
 						<line number="5" hits="1"/>
-						<line number="6" hits="0"/>
-						<line number="12" hits="0"/>
-						<line number="15" hits="0"/>
-						<line number="16" hits="0"/>
-						<line number="17" hits="0"/>
-						<line number="18" hits="0"/>
-						<line number="19" hits="0"/>
-						<line number="21" hits="0"/>
-						<line number="24" hits="0"/>
-						<line number="25" hits="0"/>
-						<line number="26" hits="0"/>
-						<line number="28" hits="0"/>
-						<line number="29" hits="0"/>
-						<line number="30" hits="0"/>
-						<line number="31" hits="0"/>
-						<line number="33" hits="0"/>
-						<line number="35" hits="0"/>
-						<line number="37" hits="0"/>
-						<line number="38" hits="0"/>
-						<line number="40" hits="0"/>
-						<line number="41" hits="0"/>
-						<line number="42" hits="0"/>
-						<line number="43" hits="0"/>
-						<line number="44" hits="0"/>
-						<line number="45" hits="0"/>
-						<line number="47" hits="0"/>
-						<line number="49" hits="0"/>
-						<line number="50" hits="0"/>
-						<line number="52" hits="0"/>
-						<line number="54" hits="0"/>
-						<line number="55" hits="0"/>
-						<line number="60" hits="0"/>
-						<line number="61" hits="0"/>
-						<line number="62" hits="0"/>
-						<line number="64" hits="0"/>
-						<line number="65" hits="0"/>
-						<line number="66" hits="0"/>
-						<line number="70" hits="0"/>
-						<line number="71" hits="0"/>
-						<line number="73" hits="0"/>
-						<line number="75" hits="0"/>
-						<line number="76" hits="0"/>
-						<line number="78" hits="0"/>
-						<line number="80" hits="0"/>
-						<line number="85" hits="0"/>
-						<line number="86" hits="0"/>
-						<line number="88" hits="0"/>
-						<line number="89" hits="0"/>
-						<line number="91" hits="0"/>
-						<line number="93" hits="0"/>
-						<line number="95" hits="0"/>
-						<line number="96" hits="0"/>
-						<line number="97" hits="0"/>
-						<line number="99" hits="0"/>
-						<line number="100" hits="0"/>
-						<line number="101" hits="0"/>
-						<line number="102" hits="0"/>
-						<line number="104" hits="0"/>
-						<line number="105" hits="0"/>
-						<line number="106" hits="0"/>
-						<line number="108" hits="0"/>
-						<line number="109" hits="0"/>
-						<line number="111" hits="0"/>
-						<line number="112" hits="0"/>
-						<line number="114" hits="0"/>
-						<line number="116" hits="0"/>
-						<line number="117" hits="0"/>
-						<line number="118" hits="0"/>
-						<line number="120" hits="0"/>
-						<line number="121" hits="0"/>
-						<line number="122" hits="0"/>
-						<line number="123" hits="0"/>
-						<line number="124" hits="0"/>
-						<line number="125" hits="0"/>
-						<line number="126" hits="0"/>
-						<line number="128" hits="0"/>
-						<line number="131" hits="0"/>
-						<line number="132" hits="0"/>
-						<line number="135" hits="0"/>
-						<line number="136" hits="0"/>
-						<line number="138" hits="0"/>
-						<line number="139" hits="0"/>
-						<line number="141" hits="0"/>
-						<line number="142" hits="0"/>
-						<line number="143" hits="0"/>
-						<line number="145" hits="0"/>
+						<line number="11" hits="1"/>
+						<line number="13" hits="1"/>
+						<line number="16" hits="1"/>
+						<line number="17" hits="1"/>
+						<line number="18" hits="1"/>
+						<line number="19" hits="1"/>
+						<line number="20" hits="1"/>
+						<line number="22" hits="1"/>
+						<line number="25" hits="1"/>
+						<line number="26" hits="1"/>
+						<line number="27" hits="1"/>
+						<line number="29" hits="1"/>
+						<line number="30" hits="1"/>
+						<line number="31" hits="1"/>
+						<line number="32" hits="1"/>
+						<line number="34" hits="1"/>
+						<line number="36" hits="1"/>
+						<line number="38" hits="1"/>
+						<line number="39" hits="1"/>
+						<line number="41" hits="1"/>
+						<line number="42" hits="1"/>
+						<line number="43" hits="1"/>
+						<line number="44" hits="1"/>
+						<line number="45" hits="1"/>
+						<line number="46" hits="1"/>
+						<line number="48" hits="1"/>
+						<line number="50" hits="1"/>
+						<line number="51" hits="1"/>
+						<line number="53" hits="1"/>
+						<line number="55" hits="1"/>
+						<line number="56" hits="1"/>
+						<line number="61" hits="1"/>
+						<line number="62" hits="1"/>
+						<line number="63" hits="1"/>
+						<line number="65" hits="1"/>
+						<line number="66" hits="1"/>
+						<line number="67" hits="1"/>
+						<line number="71" hits="1"/>
+						<line number="72" hits="1"/>
+						<line number="74" hits="1"/>
+						<line number="76" hits="1"/>
+						<line number="77" hits="1"/>
+						<line number="79" hits="1"/>
+						<line number="81" hits="1"/>
+						<line number="86" hits="1"/>
+						<line number="87" hits="1"/>
+						<line number="89" hits="1"/>
+						<line number="90" hits="1"/>
+						<line number="92" hits="1"/>
+						<line number="94" hits="1"/>
+						<line number="96" hits="1"/>
+						<line number="97" hits="1"/>
+						<line number="98" hits="1"/>
+						<line number="100" hits="1"/>
+						<line number="101" hits="1"/>
+						<line number="102" hits="1"/>
+						<line number="103" hits="1"/>
+						<line number="105" hits="1"/>
+						<line number="106" hits="1"/>
+						<line number="107" hits="1"/>
+						<line number="109" hits="1"/>
+						<line number="110" hits="1"/>
+						<line number="112" hits="1"/>
+						<line number="113" hits="1"/>
+						<line number="115" hits="1"/>
+						<line number="117" hits="1"/>
+						<line number="118" hits="1"/>
+						<line number="119" hits="1"/>
+						<line number="121" hits="1"/>
+						<line number="122" hits="1"/>
+						<line number="123" hits="1"/>
+						<line number="124" hits="1"/>
+						<line number="125" hits="1"/>
+						<line number="126" hits="1"/>
+						<line number="127" hits="1"/>
+						<line number="129" hits="1"/>
+						<line number="132" hits="1"/>
+						<line number="133" hits="1"/>
+						<line number="136" hits="1"/>
+						<line number="137" hits="1"/>
+						<line number="139" hits="1"/>
+						<line number="140" hits="1"/>
+						<line number="142" hits="1"/>
+						<line number="143" hits="1"/>
+						<line number="144" hits="1"/>
+						<line number="146" hits="1"/>
 					</lines>
 				</class>
-				<class name="layer.py" filename="layer.py" complexity="0" line-rate="0" branch-rate="0">
+				<class name="layer.py" filename="layer.py" complexity="0" line-rate="0.8791" branch-rate="0">
 					<methods/>
 					<lines>
-						<line number="1" hits="0"/>
-						<line number="3" hits="0"/>
-						<line number="4" hits="0"/>
-						<line number="6" hits="0"/>
-						<line number="7" hits="0"/>
-						<line number="12" hits="0"/>
-						<line number="13" hits="0"/>
-						<line number="16" hits="0"/>
-						<line number="17" hits="0"/>
-						<line number="18" hits="0"/>
-						<line number="19" hits="0"/>
-						<line number="21" hits="0"/>
-						<line number="24" hits="0"/>
-						<line number="25" hits="0"/>
-						<line number="27" hits="0"/>
-						<line number="28" hits="0"/>
-						<line number="29" hits="0"/>
-						<line number="30" hits="0"/>
-						<line number="32" hits="0"/>
-						<line number="34" hits="0"/>
-						<line number="36" hits="0"/>
-						<line number="38" hits="0"/>
-						<line number="39" hits="0"/>
-						<line number="40" hits="0"/>
-						<line number="41" hits="0"/>
-						<line number="42" hits="0"/>
-						<line number="43" hits="0"/>
-						<line number="45" hits="0"/>
-						<line number="47" hits="0"/>
-						<line number="48" hits="0"/>
+						<line number="1" hits="1"/>
+						<line number="3" hits="1"/>
+						<line number="4" hits="1"/>
+						<line number="6" hits="1"/>
+						<line number="11" hits="1"/>
+						<line number="12" hits="1"/>
+						<line number="14" hits="1"/>
+						<line number="17" hits="1"/>
+						<line number="18" hits="1"/>
+						<line number="19" hits="1"/>
+						<line number="20" hits="1"/>
+						<line number="22" hits="1"/>
+						<line number="23" hits="1"/>
+						<line number="24" hits="1"/>
+						<line number="25" hits="1"/>
+						<line number="27" hits="1"/>
+						<line number="28" hits="1"/>
+						<line number="29" hits="1"/>
+						<line number="30" hits="1"/>
+						<line number="32" hits="1"/>
+						<line number="34" hits="1"/>
+						<line number="36" hits="1"/>
+						<line number="38" hits="1"/>
+						<line number="39" hits="1"/>
+						<line number="40" hits="1"/>
+						<line number="41" hits="1"/>
+						<line number="42" hits="1"/>
+						<line number="43" hits="1"/>
+						<line number="45" hits="1"/>
+						<line number="47" hits="1"/>
+						<line number="48" hits="1"/>
 						<line number="49" hits="0"/>
 						<line number="50" hits="0"/>
 						<line number="51" hits="0"/>
 						<line number="52" hits="0"/>
-						<line number="54" hits="0"/>
-						<line number="55" hits="0"/>
+						<line number="54" hits="1"/>
+						<line number="55" hits="1"/>
 						<line number="56" hits="0"/>
 						<line number="57" hits="0"/>
 						<line number="58" hits="0"/>
 						<line number="59" hits="0"/>
-						<line number="61" hits="0"/>
-						<line number="63" hits="0"/>
-						<line number="66" hits="0"/>
-						<line number="67" hits="0"/>
-						<line number="69" hits="0"/>
-						<line number="70" hits="0"/>
-						<line number="72" hits="0"/>
-						<line number="73" hits="0"/>
-						<line number="75" hits="0"/>
-						<line number="76" hits="0"/>
-						<line number="78" hits="0"/>
-						<line number="80" hits="0"/>
-						<line number="83" hits="0"/>
-						<line number="86" hits="0"/>
-						<line number="87" hits="0"/>
-						<line number="88" hits="0"/>
-						<line number="95" hits="0"/>
-						<line number="96" hits="0"/>
-						<line number="102" hits="0"/>
-						<line number="107" hits="0"/>
-						<line number="108" hits="0"/>
-						<line number="110" hits="0"/>
-						<line number="112" hits="0"/>
-						<line number="113" hits="0"/>
-						<line number="115" hits="0"/>
-						<line number="116" hits="0"/>
-						<line number="118" hits="0"/>
-						<line number="119" hits="0"/>
-						<line number="121" hits="0"/>
-						<line number="122" hits="0"/>
-						<line number="123" hits="0"/>
-						<line number="124" hits="0"/>
-						<line number="125" hits="0"/>
-						<line number="126" hits="0"/>
-						<line number="127" hits="0"/>
-						<line number="129" hits="0"/>
-						<line number="132" hits="0"/>
-						<line number="133" hits="0"/>
-						<line number="136" hits="0"/>
-						<line number="137" hits="0"/>
-						<line number="139" hits="0"/>
-						<line number="140" hits="0"/>
-						<line number="142" hits="0"/>
-						<line number="143" hits="0"/>
-						<line number="144" hits="0"/>
-						<line number="146" hits="0"/>
-						<line number="148" hits="0"/>
+						<line number="61" hits="1"/>
+						<line number="63" hits="1"/>
+						<line number="66" hits="1"/>
+						<line number="67" hits="1"/>
+						<line number="69" hits="1"/>
+						<line number="70" hits="1"/>
+						<line number="72" hits="1"/>
+						<line number="73" hits="1"/>
+						<line number="75" hits="1"/>
+						<line number="76" hits="1"/>
+						<line number="78" hits="1"/>
+						<line number="80" hits="1"/>
+						<line number="83" hits="1"/>
+						<line number="86" hits="1"/>
+						<line number="87" hits="1"/>
+						<line number="88" hits="1"/>
+						<line number="95" hits="1"/>
+						<line number="96" hits="1"/>
+						<line number="102" hits="1"/>
+						<line number="107" hits="1"/>
+						<line number="108" hits="1"/>
+						<line number="110" hits="1"/>
+						<line number="112" hits="1"/>
+						<line number="113" hits="1"/>
+						<line number="115" hits="1"/>
+						<line number="116" hits="1"/>
+						<line number="118" hits="1"/>
+						<line number="119" hits="1"/>
+						<line number="121" hits="1"/>
+						<line number="122" hits="1"/>
+						<line number="123" hits="1"/>
+						<line number="124" hits="1"/>
+						<line number="125" hits="1"/>
+						<line number="126" hits="1"/>
+						<line number="127" hits="1"/>
+						<line number="129" hits="1"/>
+						<line number="132" hits="1"/>
+						<line number="133" hits="1"/>
+						<line number="136" hits="1"/>
+						<line number="137" hits="1"/>
+						<line number="139" hits="1"/>
+						<line number="140" hits="1"/>
+						<line number="142" hits="1"/>
+						<line number="143" hits="1"/>
+						<line number="144" hits="1"/>
+						<line number="146" hits="1"/>
+						<line number="148" hits="1"/>
 						<line number="149" hits="0"/>
 						<line number="150" hits="0"/>
 						<line number="151" hits="0"/>
 					</lines>
 				</class>
-				<class name="linear.py" filename="linear.py" complexity="0" line-rate="0" branch-rate="0">
+				<class name="linear.py" filename="linear.py" complexity="0" line-rate="1" branch-rate="0">
 					<methods/>
 					<lines>
-						<line number="1" hits="0"/>
-						<line number="3" hits="0"/>
-						<line number="4" hits="0"/>
-						<line number="7" hits="0"/>
-						<line number="18" hits="0"/>
-						<line number="19" hits="0"/>
-						<line number="20" hits="0"/>
-						<line number="21" hits="0"/>
-						<line number="24" hits="0"/>
-						<line number="26" hits="0"/>
-						<line number="27" hits="0"/>
-						<line number="28" hits="0"/>
-						<line number="30" hits="0"/>
+						<line number="1" hits="1"/>
+						<line number="3" hits="1"/>
+						<line number="4" hits="1"/>
+						<line number="7" hits="1"/>
+						<line number="18" hits="1"/>
+						<line number="19" hits="1"/>
+						<line number="20" hits="1"/>
+						<line number="21" hits="1"/>
+						<line number="24" hits="1"/>
+						<line number="26" hits="1"/>
+						<line number="27" hits="1"/>
+						<line number="28" hits="1"/>
+						<line number="30" hits="1"/>
 					</lines>
 				</class>
-				<class name="route.py" filename="route.py" complexity="0" line-rate="0" branch-rate="0">
+				<class name="route.py" filename="route.py" complexity="0" line-rate="0.9528" branch-rate="0">
 					<methods/>
 					<lines>
-						<line number="1" hits="0"/>
-						<line number="2" hits="0"/>
-						<line number="3" hits="0"/>
-						<line number="4" hits="0"/>
-						<line number="5" hits="0"/>
-						<line number="7" hits="0"/>
-						<line number="8" hits="0"/>
-						<line number="9" hits="0"/>
-						<line number="11" hits="0"/>
-						<line number="14" hits="0"/>
-						<line number="15" hits="0"/>
-						<line number="16" hits="0"/>
-						<line number="17" hits="0"/>
-						<line number="19" hits="0"/>
-						<line number="20" hits="0"/>
-						<line number="21" hits="0"/>
-						<line number="22" hits="0"/>
-						<line number="23" hits="0"/>
-						<line number="26" hits="0"/>
-						<line number="27" hits="0"/>
-						<line number="29" hits="0"/>
-						<line number="30" hits="0"/>
-						<line number="31" hits="0"/>
-						<line number="34" hits="0"/>
-						<line number="36" hits="0"/>
-						<line number="37" hits="0"/>
-						<line number="38" hits="0"/>
-						<line number="39" hits="0"/>
-						<line number="42" hits="0"/>
-						<line number="43" hits="0"/>
-						<line number="44" hits="0"/>
-						<line number="45" hits="0"/>
-						<line number="47" hits="0"/>
-						<line number="48" hits="0"/>
-						<line number="50" hits="0"/>
-						<line number="51" hits="0"/>
-						<line number="53" hits="0"/>
-						<line number="54" hits="0"/>
-						<line number="55" hits="0"/>
-						<line number="57" hits="0"/>
-						<line number="58" hits="0"/>
-						<line number="62" hits="0"/>
-						<line number="63" hits="0"/>
-						<line number="64" hits="0"/>
-						<line number="66" hits="0"/>
-						<line number="67" hits="0"/>
-						<line number="68" hits="0"/>
-						<line number="69" hits="0"/>
-						<line number="70" hits="0"/>
-						<line number="71" hits="0"/>
-						<line number="72" hits="0"/>
-						<line number="74" hits="0"/>
-						<line number="75" hits="0"/>
-						<line number="76" hits="0"/>
-						<line number="80" hits="0"/>
-						<line number="82" hits="0"/>
-						<line number="83" hits="0"/>
-						<line number="84" hits="0"/>
-						<line number="90" hits="0"/>
-						<line number="96" hits="0"/>
-						<line number="98" hits="0"/>
-						<line number="99" hits="0"/>
-						<line number="101" hits="0"/>
-						<line number="102" hits="0"/>
-						<line number="104" hits="0"/>
-						<line number="105" hits="0"/>
+						<line number="1" hits="1"/>
+						<line number="2" hits="1"/>
+						<line number="3" hits="1"/>
+						<line number="4" hits="1"/>
+						<line number="6" hits="1"/>
+						<line number="7" hits="1"/>
+						<line number="9" hits="1"/>
+						<line number="10" hits="1"/>
+						<line number="11" hits="1"/>
+						<line number="14" hits="1"/>
+						<line number="15" hits="1"/>
+						<line number="16" hits="1"/>
+						<line number="17" hits="1"/>
+						<line number="19" hits="1"/>
+						<line number="20" hits="1"/>
+						<line number="21" hits="1"/>
+						<line number="22" hits="1"/>
+						<line number="23" hits="1"/>
+						<line number="26" hits="1"/>
+						<line number="27" hits="1"/>
+						<line number="29" hits="1"/>
+						<line number="30" hits="1"/>
+						<line number="31" hits="1"/>
+						<line number="34" hits="1"/>
+						<line number="36" hits="1"/>
+						<line number="37" hits="1"/>
+						<line number="38" hits="1"/>
+						<line number="39" hits="1"/>
+						<line number="42" hits="1"/>
+						<line number="43" hits="1"/>
+						<line number="44" hits="1"/>
+						<line number="45" hits="1"/>
+						<line number="47" hits="1"/>
+						<line number="48" hits="1"/>
+						<line number="50" hits="1"/>
+						<line number="51" hits="1"/>
+						<line number="52" hits="1"/>
+						<line number="54" hits="1"/>
+						<line number="55" hits="1"/>
+						<line number="59" hits="1"/>
+						<line number="60" hits="1"/>
+						<line number="61" hits="1"/>
+						<line number="63" hits="1"/>
+						<line number="64" hits="1"/>
+						<line number="66" hits="1"/>
+						<line number="67" hits="1"/>
+						<line number="69" hits="1"/>
+						<line number="70" hits="1"/>
+						<line number="71" hits="1"/>
+						<line number="73" hits="0"/>
+						<line number="75" hits="1"/>
+						<line number="76" hits="1"/>
+						<line number="77" hits="1"/>
+						<line number="79" hits="1"/>
+						<line number="104" hits="1"/>
+						<line number="105" hits="1"/>
 						<line number="106" hits="0"/>
-						<line number="108" hits="0"/>
-						<line number="110" hits="0"/>
-						<line number="111" hits="0"/>
-						<line number="112" hits="0"/>
+						<line number="108" hits="1"/>
+						<line number="110" hits="1"/>
+						<line number="112" hits="1"/>
+						<line number="113" hits="1"/>
 						<line number="114" hits="0"/>
-						<line number="139" hits="0"/>
-						<line number="144" hits="0"/>
-						<line number="156" hits="0"/>
-						<line number="157" hits="0"/>
-						<line number="158" hits="0"/>
-						<line number="159" hits="0"/>
-						<line number="161" hits="0"/>
-						<line number="163" hits="0"/>
-						<line number="164" hits="0"/>
-						<line number="165" hits="0"/>
-						<line number="168" hits="0"/>
-						<line number="173" hits="0"/>
-						<line number="175" hits="0"/>
-						<line number="176" hits="0"/>
-						<line number="178" hits="0"/>
+						<line number="117" hits="1"/>
+						<line number="122" hits="1"/>
+						<line number="124" hits="1"/>
+						<line number="125" hits="1"/>
+						<line number="127" hits="1"/>
+						<line number="128" hits="1"/>
+						<line number="130" hits="1"/>
+						<line number="131" hits="1"/>
+						<line number="132" hits="1"/>
+						<line number="133" hits="1"/>
+						<line number="134" hits="1"/>
+						<line number="135" hits="1"/>
+						<line number="136" hits="1"/>
+						<line number="138" hits="1"/>
+						<line number="142" hits="1"/>
+						<line number="143" hits="1"/>
+						<line number="144" hits="1"/>
+						<line number="145" hits="1"/>
+						<line number="147" hits="0"/>
+						<line number="149" hits="1"/>
+						<line number="150" hits="1"/>
+						<line number="152" hits="1"/>
+						<line number="154" hits="1"/>
+						<line number="155" hits="1"/>
+						<line number="156" hits="1"/>
+						<line number="157" hits="1"/>
+						<line number="158" hits="1"/>
+						<line number="159" hits="1"/>
+						<line number="160" hits="1"/>
+						<line number="162" hits="1"/>
+						<line number="166" hits="1"/>
+						<line number="167" hits="1"/>
+						<line number="168" hits="1"/>
+						<line number="170" hits="1"/>
+						<line number="171" hits="1"/>
+						<line number="172" hits="1"/>
+						<line number="173" hits="1"/>
+						<line number="174" hits="1"/>
+						<line number="175" hits="1"/>
+						<line number="177" hits="1"/>
+						<line number="178" hits="1"/>
 						<line number="179" hits="0"/>
-						<line number="181" hits="0"/>
-						<line number="182" hits="0"/>
-						<line number="183" hits="0"/>
-						<line number="184" hits="0"/>
-						<line number="185" hits="0"/>
-						<line number="186" hits="0"/>
-						<line number="187" hits="0"/>
-						<line number="189" hits="0"/>
-						<line number="193" hits="0"/>
-						<line number="194" hits="0"/>
-						<line number="195" hits="0"/>
-						<line number="196" hits="0"/>
-						<line number="198" hits="0"/>
-						<line number="200" hits="0"/>
-						<line number="201" hits="0"/>
-						<line number="203" hits="0"/>
-						<line number="205" hits="0"/>
-						<line number="206" hits="0"/>
-						<line number="207" hits="0"/>
-						<line number="208" hits="0"/>
-						<line number="209" hits="0"/>
-						<line number="210" hits="0"/>
-						<line number="211" hits="0"/>
-						<line number="213" hits="0"/>
-						<line number="217" hits="0"/>
-						<line number="218" hits="0"/>
-						<line number="219" hits="0"/>
-						<line number="221" hits="0"/>
-						<line number="222" hits="0"/>
-						<line number="223" hits="0"/>
-						<line number="224" hits="0"/>
-						<line number="225" hits="0"/>
-						<line number="227" hits="0"/>
-						<line number="228" hits="0"/>
-						<line number="229" hits="0"/>
-						<line number="231" hits="0"/>
-						<line number="232" hits="0"/>
+						<line number="181" hits="1"/>
+						<line number="182" hits="1"/>
 					</lines>
 				</class>
-				<class name="schema.py" filename="schema.py" complexity="0" line-rate="0" branch-rate="0">
+				<class name="schema.py" filename="schema.py" complexity="0" line-rate="1" branch-rate="0">
 					<methods/>
 					<lines>
-						<line number="1" hits="0"/>
-						<line number="3" hits="0"/>
-						<line number="5" hits="0"/>
-						<line number="6" hits="0"/>
-						<line number="13" hits="0"/>
-						<line number="14" hits="0"/>
-						<line number="15" hits="0"/>
-						<line number="16" hits="0"/>
-						<line number="19" hits="0"/>
-						<line number="20" hits="0"/>
-						<line number="21" hits="0"/>
-						<line number="22" hits="0"/>
-						<line number="23" hits="0"/>
-						<line number="25" hits="0"/>
-						<line number="26" hits="0"/>
-						<line number="27" hits="0"/>
-						<line number="28" hits="0"/>
-						<line number="29" hits="0"/>
-						<line number="30" hits="0"/>
-						<line number="31" hits="0"/>
-						<line number="32" hits="0"/>
-						<line number="33" hits="0"/>
-						<line number="35" hits="0"/>
-						<line number="36" hits="0"/>
-						<line number="39" hits="0"/>
-						<line number="40" hits="0"/>
-						<line number="41" hits="0"/>
-						<line number="42" hits="0"/>
-						<line number="43" hits="0"/>
-						<line number="45" hits="0"/>
-						<line number="46" hits="0"/>
-						<line number="47" hits="0"/>
-						<line number="49" hits="0"/>
-						<line number="50" hits="0"/>
+						<line number="1" hits="1"/>
+						<line number="3" hits="1"/>
+						<line number="5" hits="1"/>
+						<line number="6" hits="1"/>
+						<line number="13" hits="1"/>
+						<line number="14" hits="1"/>
+						<line number="15" hits="1"/>
+						<line number="16" hits="1"/>
+						<line number="19" hits="1"/>
+						<line number="20" hits="1"/>
+						<line number="21" hits="1"/>
+						<line number="22" hits="1"/>
+						<line number="23" hits="1"/>
+						<line number="25" hits="1"/>
+						<line number="26" hits="1"/>
+						<line number="27" hits="1"/>
+						<line number="28" hits="1"/>
+						<line number="29" hits="1"/>
+						<line number="30" hits="1"/>
+						<line number="31" hits="1"/>
+						<line number="32" hits="1"/>
+						<line number="33" hits="1"/>
+						<line number="35" hits="1"/>
+						<line number="36" hits="1"/>
+						<line number="39" hits="1"/>
+						<line number="40" hits="1"/>
+						<line number="41" hits="1"/>
+						<line number="42" hits="1"/>
+						<line number="43" hits="1"/>
+						<line number="45" hits="1"/>
+						<line number="46" hits="1"/>
+						<line number="47" hits="1"/>
+						<line number="49" hits="1"/>
+						<line number="50" hits="1"/>
 					</lines>
 				</class>
 			</classes>
 		</package>
-		<package name="encoders" line-rate="0" branch-rate="0" complexity="0">
+		<package name="encoders" line-rate="1" branch-rate="0" complexity="0">
 			<classes>
-				<class name="__init__.py" filename="encoders/__init__.py" complexity="0" line-rate="0" branch-rate="0">
+				<class name="__init__.py" filename="encoders/__init__.py" complexity="0" line-rate="1" branch-rate="0">
 					<methods/>
 					<lines>
-						<line number="1" hits="0"/>
-						<line number="2" hits="0"/>
-						<line number="3" hits="0"/>
-						<line number="4" hits="0"/>
-						<line number="6" hits="0"/>
+						<line number="1" hits="1"/>
+						<line number="2" hits="1"/>
+						<line number="3" hits="1"/>
+						<line number="4" hits="1"/>
+						<line number="6" hits="1"/>
 					</lines>
 				</class>
-				<class name="base.py" filename="encoders/base.py" complexity="0" line-rate="0" branch-rate="0">
+				<class name="base.py" filename="encoders/base.py" complexity="0" line-rate="1" branch-rate="0">
 					<methods/>
 					<lines>
-						<line number="1" hits="0"/>
-						<line number="4" hits="0"/>
-						<line number="5" hits="0"/>
-						<line number="7" hits="0"/>
-						<line number="8" hits="0"/>
-						<line number="10" hits="0"/>
-						<line number="11" hits="0"/>
+						<line number="1" hits="1"/>
+						<line number="4" hits="1"/>
+						<line number="5" hits="1"/>
+						<line number="7" hits="1"/>
+						<line number="8" hits="1"/>
+						<line number="10" hits="1"/>
+						<line number="11" hits="1"/>
 					</lines>
 				</class>
-				<class name="bm25.py" filename="encoders/bm25.py" complexity="0" line-rate="0" branch-rate="0">
+				<class name="bm25.py" filename="encoders/bm25.py" complexity="0" line-rate="1" branch-rate="0">
 					<methods/>
 					<lines>
-						<line number="1" hits="0"/>
-						<line number="3" hits="0"/>
-						<line number="5" hits="0"/>
-						<line number="8" hits="0"/>
-						<line number="9" hits="0"/>
-						<line number="10" hits="0"/>
-						<line number="12" hits="0"/>
+						<line number="1" hits="1"/>
+						<line number="3" hits="1"/>
+						<line number="5" hits="1"/>
+						<line number="8" hits="1"/>
+						<line number="9" hits="1"/>
+						<line number="10" hits="1"/>
+						<line number="12" hits="1"/>
+						<line number="13" hits="1"/>
+						<line number="14" hits="1"/>
+						<line number="16" hits="1"/>
+						<line number="17" hits="1"/>
+						<line number="18" hits="1"/>
+						<line number="19" hits="1"/>
+						<line number="20" hits="1"/>
+						<line number="22" hits="1"/>
+						<line number="24" hits="1"/>
+						<line number="25" hits="1"/>
+						<line number="26" hits="1"/>
+						<line number="27" hits="1"/>
+						<line number="28" hits="1"/>
+						<line number="29" hits="1"/>
+						<line number="30" hits="1"/>
+						<line number="32" hits="1"/>
+						<line number="34" hits="1"/>
+						<line number="35" hits="1"/>
+						<line number="36" hits="1"/>
+						<line number="37" hits="1"/>
+						<line number="38" hits="1"/>
+						<line number="39" hits="1"/>
+						<line number="40" hits="1"/>
+						<line number="41" hits="1"/>
+						<line number="42" hits="1"/>
+						<line number="44" hits="1"/>
+						<line number="45" hits="1"/>
+						<line number="46" hits="1"/>
+						<line number="47" hits="1"/>
+					</lines>
+				</class>
+				<class name="cohere.py" filename="encoders/cohere.py" complexity="0" line-rate="1" branch-rate="0">
+					<methods/>
+					<lines>
+						<line number="1" hits="1"/>
+						<line number="3" hits="1"/>
+						<line number="5" hits="1"/>
+						<line number="8" hits="1"/>
+						<line number="9" hits="1"/>
+						<line number="11" hits="1"/>
+						<line number="16" hits="1"/>
+						<line number="17" hits="1"/>
+						<line number="18" hits="1"/>
+						<line number="19" hits="1"/>
+						<line number="20" hits="1"/>
+						<line number="21" hits="1"/>
+						<line number="22" hits="1"/>
+						<line number="23" hits="1"/>
+						<line number="25" hits="1"/>
+						<line number="26" hits="1"/>
+						<line number="27" hits="1"/>
+						<line number="28" hits="1"/>
+						<line number="29" hits="1"/>
+						<line number="30" hits="1"/>
+						<line number="31" hits="1"/>
+						<line number="32" hits="1"/>
+					</lines>
+				</class>
+				<class name="openai.py" filename="encoders/openai.py" complexity="0" line-rate="1" branch-rate="0">
+					<methods/>
+					<lines>
+						<line number="1" hits="1"/>
+						<line number="2" hits="1"/>
+						<line number="4" hits="1"/>
+						<line number="5" hits="1"/>
+						<line number="6" hits="1"/>
+						<line number="8" hits="1"/>
+						<line number="9" hits="1"/>
+						<line number="12" hits="1"/>
+						<line number="13" hits="1"/>
+						<line number="15" hits="1"/>
+						<line number="20" hits="1"/>
+						<line number="21" hits="1"/>
+						<line number="22" hits="1"/>
+						<line number="23" hits="1"/>
+						<line number="24" hits="1"/>
+						<line number="25" hits="1"/>
+						<line number="26" hits="1"/>
+						<line number="27" hits="1"/>
+						<line number="29" hits="1"/>
+						<line number="30" hits="1"/>
+						<line number="31" hits="1"/>
+						<line number="32" hits="1"/>
+						<line number="33" hits="1"/>
+						<line number="36" hits="1"/>
+						<line number="37" hits="1"/>
+						<line number="38" hits="1"/>
+						<line number="39" hits="1"/>
+						<line number="40" hits="1"/>
+						<line number="41" hits="1"/>
+						<line number="42" hits="1"/>
+						<line number="43" hits="1"/>
+						<line number="44" hits="1"/>
+						<line number="45" hits="1"/>
+						<line number="46" hits="1"/>
+						<line number="47" hits="1"/>
+						<line number="48" hits="1"/>
+						<line number="50" hits="1"/>
+						<line number="55" hits="1"/>
+						<line number="57" hits="1"/>
+						<line number="58" hits="1"/>
+					</lines>
+				</class>
+			</classes>
+		</package>
+		<package name="utils" line-rate="0.3958" branch-rate="0" complexity="0">
+			<classes>
+				<class name="__init__.py" filename="utils/__init__.py" complexity="0" line-rate="1" branch-rate="0">
+					<methods/>
+					<lines/>
+				</class>
+				<class name="function_call.py" filename="utils/function_call.py" complexity="0" line-rate="0.2258" branch-rate="0">
+					<methods/>
+					<lines>
+						<line number="1" hits="1"/>
+						<line number="2" hits="1"/>
+						<line number="3" hits="1"/>
+						<line number="5" hits="1"/>
+						<line number="7" hits="1"/>
+						<line number="8" hits="1"/>
+						<line number="11" hits="1"/>
+						<line number="12" hits="1"/>
 						<line number="13" hits="0"/>
 						<line number="14" hits="0"/>
+						<line number="15" hits="0"/>
 						<line number="16" hits="0"/>
-						<line number="17" hits="0"/>
 						<line number="18" hits="0"/>
 						<line number="19" hits="0"/>
 						<line number="20" hits="0"/>
-						<line number="22" hits="0"/>
 						<line number="24" hits="0"/>
-						<line number="25" hits="0"/>
 						<line number="26" hits="0"/>
 						<line number="27" hits="0"/>
 						<line number="28" hits="0"/>
-						<line number="29" hits="0"/>
-						<line number="30" hits="0"/>
-						<line number="32" hits="0"/>
-						<line number="34" hits="0"/>
-						<line number="35" hits="0"/>
-						<line number="36" hits="0"/>
-						<line number="37" hits="0"/>
-						<line number="38" hits="0"/>
-						<line number="39" hits="0"/>
-						<line number="40" hits="0"/>
-						<line number="41" hits="0"/>
-						<line number="42" hits="0"/>
+						<line number="34" hits="1"/>
+						<line number="40" hits="1"/>
+						<line number="43" hits="1"/>
 						<line number="44" hits="0"/>
-						<line number="45" hits="0"/>
 						<line number="46" hits="0"/>
-						<line number="47" hits="0"/>
-					</lines>
-				</class>
-				<class name="cohere.py" filename="encoders/cohere.py" complexity="0" line-rate="0" branch-rate="0">
-					<methods/>
-					<lines>
-						<line number="1" hits="0"/>
-						<line number="3" hits="0"/>
-						<line number="5" hits="0"/>
-						<line number="8" hits="0"/>
-						<line number="9" hits="0"/>
-						<line number="11" hits="0"/>
-						<line number="16" hits="0"/>
-						<line number="17" hits="0"/>
-						<line number="18" hits="0"/>
-						<line number="19" hits="0"/>
-						<line number="20" hits="0"/>
-						<line number="21" hits="0"/>
-						<line number="22" hits="0"/>
-						<line number="23" hits="0"/>
-						<line number="25" hits="0"/>
-						<line number="26" hits="0"/>
-						<line number="27" hits="0"/>
-						<line number="28" hits="0"/>
-						<line number="29" hits="0"/>
-						<line number="30" hits="0"/>
-						<line number="31" hits="0"/>
-						<line number="32" hits="0"/>
+						<line number="75" hits="0"/>
+						<line number="76" hits="0"/>
+						<line number="77" hits="0"/>
+						<line number="79" hits="0"/>
+						<line number="81" hits="0"/>
+						<line number="82" hits="0"/>
+						<line number="83" hits="0"/>
+						<line number="84" hits="0"/>
+						<line number="87" hits="1"/>
+						<line number="89" hits="0"/>
+						<line number="91" hits="0"/>
+						<line number="92" hits="0"/>
+						<line number="93" hits="0"/>
+						<line number="94" hits="0"/>
+						<line number="98" hits="0"/>
+						<line number="99" hits="0"/>
+						<line number="100" hits="0"/>
+						<line number="101" hits="0"/>
+						<line number="102" hits="0"/>
+						<line number="103" hits="0"/>
+						<line number="104" hits="0"/>
+						<line number="105" hits="0"/>
+						<line number="108" hits="1"/>
+						<line number="109" hits="0"/>
+						<line number="110" hits="0"/>
+						<line number="111" hits="0"/>
+						<line number="112" hits="0"/>
+						<line number="116" hits="1"/>
+						<line number="117" hits="0"/>
+						<line number="118" hits="0"/>
+						<line number="119" hits="0"/>
+						<line number="120" hits="0"/>
+						<line number="122" hits="0"/>
+						<line number="123" hits="0"/>
+						<line number="124" hits="0"/>
+						<line number="125" hits="0"/>
+						<line number="126" hits="0"/>
+						<line number="127" hits="0"/>
 					</lines>
 				</class>
-				<class name="openai.py" filename="encoders/openai.py" complexity="0" line-rate="0" branch-rate="0">
+				<class name="llm.py" filename="utils/llm.py" complexity="0" line-rate="0.2857" branch-rate="0">
 					<methods/>
 					<lines>
-						<line number="1" hits="0"/>
-						<line number="2" hits="0"/>
-						<line number="4" hits="0"/>
-						<line number="5" hits="0"/>
-						<line number="6" hits="0"/>
-						<line number="8" hits="0"/>
+						<line number="1" hits="1"/>
+						<line number="3" hits="1"/>
+						<line number="5" hits="1"/>
+						<line number="8" hits="1"/>
 						<line number="9" hits="0"/>
-						<line number="12" hits="0"/>
-						<line number="13" hits="0"/>
+						<line number="10" hits="0"/>
 						<line number="15" hits="0"/>
-						<line number="20" hits="0"/>
-						<line number="21" hits="0"/>
-						<line number="22" hits="0"/>
-						<line number="23" hits="0"/>
-						<line number="24" hits="0"/>
-						<line number="25" hits="0"/>
-						<line number="26" hits="0"/>
 						<line number="27" hits="0"/>
 						<line number="29" hits="0"/>
 						<line number="30" hits="0"/>
 						<line number="31" hits="0"/>
 						<line number="32" hits="0"/>
 						<line number="33" hits="0"/>
-						<line number="36" hits="0"/>
-						<line number="37" hits="0"/>
-						<line number="38" hits="0"/>
-						<line number="39" hits="0"/>
-						<line number="40" hits="0"/>
-						<line number="41" hits="0"/>
-						<line number="42" hits="0"/>
-						<line number="43" hits="0"/>
-						<line number="44" hits="0"/>
-						<line number="45" hits="0"/>
-						<line number="46" hits="0"/>
-						<line number="47" hits="0"/>
-						<line number="48" hits="0"/>
-						<line number="50" hits="0"/>
-						<line number="55" hits="0"/>
-						<line number="57" hits="0"/>
-						<line number="58" hits="0"/>
+						<line number="34" hits="0"/>
 					</lines>
 				</class>
-			</classes>
-		</package>
-		<package name="utils" line-rate="0" branch-rate="0" complexity="0">
-			<classes>
-				<class name="__init__.py" filename="utils/__init__.py" complexity="0" line-rate="1" branch-rate="0">
-					<methods/>
-					<lines/>
-				</class>
-				<class name="logger.py" filename="utils/logger.py" complexity="0" line-rate="0" branch-rate="0">
+				<class name="logger.py" filename="utils/logger.py" complexity="0" line-rate="1" branch-rate="0">
 					<methods/>
 					<lines>
-						<line number="1" hits="0"/>
-						<line number="3" hits="0"/>
-						<line number="6" hits="0"/>
-						<line number="7" hits="0"/>
-						<line number="8" hits="0"/>
-						<line number="23" hits="0"/>
-						<line number="24" hits="0"/>
-						<line number="26" hits="0"/>
-						<line number="27" hits="0"/>
-						<line number="29" hits="0"/>
-						<line number="35" hits="0"/>
-						<line number="37" hits="0"/>
-						<line number="40" hits="0"/>
-						<line number="41" hits="0"/>
-						<line number="42" hits="0"/>
-						<line number="44" hits="0"/>
-						<line number="46" hits="0"/>
-						<line number="47" hits="0"/>
-						<line number="49" hits="0"/>
-						<line number="52" hits="0"/>
+						<line number="1" hits="1"/>
+						<line number="3" hits="1"/>
+						<line number="6" hits="1"/>
+						<line number="7" hits="1"/>
+						<line number="8" hits="1"/>
+						<line number="23" hits="1"/>
+						<line number="24" hits="1"/>
+						<line number="26" hits="1"/>
+						<line number="27" hits="1"/>
+						<line number="29" hits="1"/>
+						<line number="35" hits="1"/>
+						<line number="37" hits="1"/>
+						<line number="40" hits="1"/>
+						<line number="41" hits="1"/>
+						<line number="42" hits="1"/>
+						<line number="44" hits="1"/>
+						<line number="46" hits="1"/>
+						<line number="47" hits="1"/>
+						<line number="49" hits="1"/>
+						<line number="52" hits="1"/>
 					</lines>
 				</class>
 			</classes>
diff --git a/poetry.lock b/poetry.lock
index 81101378..7efeda7e 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1594,6 +1594,24 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
 [package.extras]
 testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
 
+[[package]]
+name = "pytest-asyncio"
+version = "0.23.2"
+description = "Pytest support for asyncio"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pytest-asyncio-0.23.2.tar.gz", hash = "sha256:c16052382554c7b22d48782ab3438d5b10f8cf7a4bdcae7f0f67f097d95beecc"},
+    {file = "pytest_asyncio-0.23.2-py3-none-any.whl", hash = "sha256:ea9021364e32d58f0be43b91c6233fb8d2224ccef2398d6837559e587682808f"},
+]
+
+[package.dependencies]
+pytest = ">=7.0.0"
+
+[package.extras]
+docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
+testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
+
 [[package]]
 name = "pytest-cov"
 version = "4.1.0"
@@ -2102,6 +2120,17 @@ files = [
 docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
 test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"]
 
+[[package]]
+name = "types-pyyaml"
+version = "6.0.12.12"
+description = "Typing stubs for PyYAML"
+optional = false
+python-versions = "*"
+files = [
+    {file = "types-PyYAML-6.0.12.12.tar.gz", hash = "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062"},
+    {file = "types_PyYAML-6.0.12.12-py3-none-any.whl", hash = "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"},
+]
+
 [[package]]
 name = "typing-extensions"
 version = "4.9.0"
@@ -2271,4 +2300,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.9"
-content-hash = "f9717f2fd983029796c2c6162081f4b195555453f23f8e5d784ca7a7c1034034"
+content-hash = "afd687626ef87dc72424414d7c2333caf360bccb01fab087cfd78b97ea62e04f"
diff --git a/pyproject.toml b/pyproject.toml
index b530d476..47f1307e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,6 +21,7 @@ numpy = "^1.25.2"
 pinecone-text = "^0.7.0"
 colorlog = "^6.8.0"
 pyyaml = "^6.0.1"
+pytest-asyncio = "^0.23.2"
 
 
 [tool.poetry.group.dev.dependencies]
@@ -32,6 +33,7 @@ pytest-mock = "^3.12.0"
 pytest-cov = "^4.1.0"
 pytest-xdist = "^3.5.0"
 mypy = "^1.7.1"
+types-pyyaml = "^6.0.12.12"
 
 [build-system]
 requires = ["poetry-core"]
diff --git a/semantic_router/__init__.py b/semantic_router/__init__.py
index 0c445bea..2659bfe3 100644
--- a/semantic_router/__init__.py
+++ b/semantic_router/__init__.py
@@ -1,4 +1,5 @@
 from .hybrid_layer import HybridRouteLayer
 from .layer import RouteLayer
+from .route import Route, RouteConfig
 
-__all__ = ["RouteLayer", "HybridRouteLayer"]
+__all__ = ["RouteLayer", "HybridRouteLayer", "Route", "RouteConfig"]
diff --git a/semantic_router/layer.py b/semantic_router/layer.py
index a161e353..2fa3b863 100644
--- a/semantic_router/layer.py
+++ b/semantic_router/layer.py
@@ -22,7 +22,6 @@ class RouteLayer:
     def __init__(self, encoder: BaseEncoder | None = None, routes: list[Route] = []):
         self.encoder = encoder if encoder is not None else CohereEncoder()
         self.routes: list[Route] = routes
-        self.encoder = encoder
         # decide on default threshold based on encoder
         if isinstance(encoder, OpenAIEncoder):
             self.score_threshold = 0.82
@@ -58,7 +57,7 @@ class RouteLayer:
         routes = [Route.from_dict(route_data) for route_data in routes_data]
         return cls(routes=routes)
 
-    def add_route(self, route: Route):
+    def add(self, route: Route):
         # create embeddings
         embeds = self.encoder(route.utterances)
 
diff --git a/semantic_router/route.py b/semantic_router/route.py
index 69f9d4e6..99a7945b 100644
--- a/semantic_router/route.py
+++ b/semantic_router/route.py
@@ -47,9 +47,6 @@ class Route(BaseModel):
     def to_dict(self):
         return self.dict()
 
-    def to_yaml(self):
-        return yaml.dump(self.dict())
-
     @classmethod
     def from_dict(cls, data: dict):
         return cls(**data)
@@ -60,7 +57,7 @@ class Route(BaseModel):
         Generate a dynamic Route object from a function or Pydantic model using LLM
         """
         schema = function_call.get_schema(item=entity)
-        dynamic_route = await cls._agenerate_dynamic_route(function_schema=schema)
+        dynamic_route = await cls._generate_dynamic_route(function_schema=schema)
         return dynamic_route
 
     @classmethod
@@ -76,7 +73,7 @@ class Route(BaseModel):
             raise ValueError("No <config></config> tags found in the output.")
 
     @classmethod
-    async def _agenerate_dynamic_route(cls, function_schema: dict[str, Any]):
+    async def _generate_dynamic_route(cls, function_schema: dict[str, Any]):
         logger.info("Generating dynamic route...")
 
         prompt = f"""
diff --git a/tests/unit/test_hybrid_layer.py b/tests/unit/test_hybrid_layer.py
index 94720cd8..06b5d733 100644
--- a/tests/unit/test_hybrid_layer.py
+++ b/tests/unit/test_hybrid_layer.py
@@ -2,7 +2,7 @@ import pytest
 
 from semantic_router.encoders import BaseEncoder, CohereEncoder, OpenAIEncoder
 from semantic_router.hybrid_layer import HybridRouteLayer
-from semantic_router.schema import Route
+from semantic_router.route import Route
 
 
 def mock_encoder_call(utterances):
diff --git a/tests/unit/test_layer.py b/tests/unit/test_layer.py
index 1d9536a7..21b48917 100644
--- a/tests/unit/test_layer.py
+++ b/tests/unit/test_layer.py
@@ -2,7 +2,7 @@ import pytest
 
 from semantic_router.encoders import BaseEncoder, CohereEncoder, OpenAIEncoder
 from semantic_router.layer import RouteLayer
-from semantic_router.schema import Route
+from semantic_router.route import Route
 
 
 def mock_encoder_call(utterances):
@@ -65,13 +65,13 @@ class TestRouteLayer:
         route1 = Route(name="Route 1", utterances=["Yes", "No"])
         route2 = Route(name="Route 2", utterances=["Maybe", "Sure"])
 
-        route_layer.add_route(route=route1)
+        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 len(set(route_layer.categories)) == 1
         assert set(route_layer.categories) == {"Route 1"}
 
-        route_layer.add_route(route=route2)
+        route_layer.add(route=route2)
         assert len(route_layer.index) == 4
         assert len(set(route_layer.categories)) == 2
         assert set(route_layer.categories) == {"Route 1", "Route 2"}
diff --git a/tests/unit/test_route.py b/tests/unit/test_route.py
new file mode 100644
index 00000000..1de3f0e5
--- /dev/null
+++ b/tests/unit/test_route.py
@@ -0,0 +1,222 @@
+import os
+from unittest.mock import AsyncMock, mock_open, patch
+
+import pytest
+
+from semantic_router.route import Route, RouteConfig, is_valid
+
+
+# Is valid test:
+def test_is_valid_with_valid_json():
+    valid_json = '{"name": "test_route", "utterances": ["hello", "hi"]}'
+    assert is_valid(valid_json) is True
+
+
+def test_is_valid_with_missing_keys():
+    invalid_json = '{"name": "test_route"}'  # Missing 'utterances'
+    with patch("semantic_router.route.logger") as mock_logger:
+        assert is_valid(invalid_json) is False
+        mock_logger.warning.assert_called_once()
+
+
+def test_is_valid_with_valid_json_list():
+    valid_json_list = (
+        '[{"name": "test_route1", "utterances": ["hello"]}, '
+        '{"name": "test_route2", "utterances": ["hi"]}]'
+    )
+    assert is_valid(valid_json_list) is True
+
+
+def test_is_valid_with_invalid_json_list():
+    invalid_json_list = (
+        '[{"name": "test_route1"}, {"name": "test_route2", "utterances": ["hi"]}]'
+    )
+    with patch("semantic_router.route.logger") as mock_logger:
+        assert is_valid(invalid_json_list) is False
+        mock_logger.warning.assert_called_once()
+
+
+def test_is_valid_with_invalid_json():
+    invalid_json = '{"name": "test_route", "utterances": ["hello", "hi" invalid json}'
+    with patch("semantic_router.route.logger") as mock_logger:
+        assert is_valid(invalid_json) is False
+        mock_logger.error.assert_called_once()
+
+
+class TestRoute:
+    @pytest.mark.asyncio
+    @patch("semantic_router.route.llm", new_callable=AsyncMock)
+    async def test_generate_dynamic_route(self, mock_llm):
+        print(f"mock_llm: {mock_llm}")
+        mock_llm.return_value = """
+        <config>
+        {
+            "name": "test_function",
+            "utterances": [
+                "example_utterance_1",
+                "example_utterance_2",
+                "example_utterance_3",
+                "example_utterance_4",
+                "example_utterance_5"]
+        }
+        </config>
+        """
+        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 = {
+            "name": "test",
+            "utterances": ["utterance"],
+            "description": None,
+        }
+        assert route.to_dict() == expected_dict
+
+    def test_from_dict(self):
+        route_dict = {"name": "test", "utterances": ["utterance"]}
+        route = Route.from_dict(route_dict)
+        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):
+        # Mock the llm function
+        mock_llm.return_value = """
+        <config>
+        {
+            "name": "test_function",
+            "utterances": [
+                "example_utterance_1",
+                "example_utterance_2",
+                "example_utterance_3",
+                "example_utterance_4",
+                "example_utterance_5"]
+        }
+        </config>
+        """
+
+        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 = """
+        <config>
+        {
+            "name": "test_function",
+            "utterances": [
+                "example_utterance_1",
+                "example_utterance_2",
+                "example_utterance_3",
+                "example_utterance_4",
+                "example_utterance_5"]
+        }
+        </config>
+        """
+        expected_config = """
+        {
+            "name": "test_function",
+            "utterances": [
+                "example_utterance_1",
+                "example_utterance_2",
+                "example_utterance_3",
+                "example_utterance_4",
+                "example_utterance_5"]
+        }
+        """
+        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 == []
diff --git a/tests/unit/test_route_config.py b/tests/unit/test_route_config.py
deleted file mode 100644
index 0c964d82..00000000
--- a/tests/unit/test_route_config.py
+++ /dev/null
@@ -1,80 +0,0 @@
-import os
-from unittest.mock import mock_open, patch
-
-import pytest
-
-from semantic_router.route import Route, RouteConfig
-
-
-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 == []
diff --git a/tests/unit/test_schema.py b/tests/unit/test_schema.py
index f471755c..27c73c9f 100644
--- a/tests/unit/test_schema.py
+++ b/tests/unit/test_schema.py
@@ -1,11 +1,11 @@
 import pytest
 
+from semantic_router.route import Route
 from semantic_router.schema import (
     CohereEncoder,
     Encoder,
     EncoderType,
     OpenAIEncoder,
-    Route,
     SemanticSpace,
 )
 
-- 
GitLab