diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index cf22ea9b75332a14d994c57a58f946dc7b53b807..017c23c02503e45e36b8580c0b5c6664d59f016f 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -15,7 +15,10 @@ jobs:
     strategy:
       matrix:
         python-version:
+          - "3.9"
+          - "3.10"
           - "3.11"
+          # - "3.12"
     steps:
       - uses: actions/checkout@v4
       - name: Cache Poetry
diff --git a/.gitignore b/.gitignore
index cb4c0022d27d777ec80531b834da365db0729125..cc461499d2d720e715c86c33e513b9c259cb5737 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@ venv/
 **/__pycache__
 **/*.py[cod]
 
+
 # local env files
 .env*.local
 .env
@@ -18,5 +19,11 @@ mac.env
 .coverage
 .coverage.*
 .pytest_cache
+coverage.xml
 test.py
 output
+node_modules
+package-lock.json
+package.json
+
+```
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6a80529022ae36491cbbb22511c1a93a0050351f..b24594f35ab4a09c525946915f027ed267ac78c5 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,5 +1,5 @@
 default_language_version:
-  python: python3.11.3
+  python: python3.9
 repos:
   - repo: meta
     hooks:
@@ -17,14 +17,12 @@ repos:
       - id: blacken-docs
         additional_dependencies: [black==22.10.0]
 
-  - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.0.290
+  - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
+    rev: v9.11.0
     hooks:
-      - id: ruff
-        types_or: [ python, pyi, jupyter ]
-        args: [ --fix ]
-      - id: ruff-format
-        types_or: [ python, pyi, jupyter ]
+      - id: commitlint
+        stages: [commit-msg]
+        additional_dependencies: ['@commitlint/config-conventional']
 
   - repo: https://github.com/codespell-project/codespell
     rev: v2.2.4
@@ -36,13 +34,11 @@ repos:
         args: [ "--write-changes", "--ignore-words-list", "asend" ]
         exclude: "poetry.lock"
 
-
   - repo: https://github.com/pre-commit/pre-commit-hooks
     rev: v4.4.0
     hooks:
       - id: check-vcs-permalinks
       - id: end-of-file-fixer
-        # exclude: "tests/((commands|data)/|test_).+"
       - id: trailing-whitespace
         args: [ --markdown-linebreak-ext=md ]
       - id: debug-statements
@@ -62,8 +58,20 @@ repos:
           - post-commit
           - push
 
-  - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
-    rev: v9.11.0
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.0.290
+    hooks:
+      - id: ruff
+        types_or: [python, pyi, jupyter]
+
+  # - repo: https://github.com/pre-commit/mirrors-mypy
+  #   rev: v1.8.0
+  #   hooks:
+  #     - id: mypy
+  #       args: [--ignore-missing-imports]
+
+  - repo: https://github.com/PyCQA/bandit
+    rev: 1.7.6
     hooks:
-        - id: commitlint
-          stages: [commit-msg]
+      - id: bandit
+        args: ['-lll']
diff --git a/Makefile b/Makefile
index aeb3d3b19ff9262b933b9022f6fc80c240279040..adf4eb0c4079749a2f86a0a862292f3ed0931265 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
 format:
-	poetry run black .
+	poetry run black --target-version py39 .
 	poetry run ruff --select I --fix .
 
 PYTHON_FILES=.
@@ -7,7 +7,7 @@ lint: PYTHON_FILES=.
 lint_diff: PYTHON_FILES=$(shell git diff --name-only --diff-filter=d main | grep -E '\.py$$')
 
 lint lint_diff:
-	poetry run black $(PYTHON_FILES) --check
+	poetry run black --target-version py39 $(PYTHON_FILES) --check
 	poetry run ruff .
 	poetry run mypy $(PYTHON_FILES)
 
diff --git a/README.md b/README.md
index fe5db3433ebcaa5de5766e22923f1db43dc19629..fd4c12a2e5f441e8cb792d6705c976c7e67b55d4 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
 [![Aurelio AI](https://pbs.twimg.com/profile_banners/1671498317455581184/1696285195/1500x500)](https://aurelio.ai)
 
 # Semantic Router
+
 <p>
 <img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/semantic-router?logo=python&logoColor=gold" />
 <img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/aurelio-labs/semantic-router" />
@@ -22,7 +23,7 @@ To get started with _semantic-router_ we install it like so:
 pip install -qU semantic-router
 ```
 
-❗️ _If wanting to use local embeddings you can use `FastEmbedEncoder` (`pip install -qU semantic-router[fastembed]`). To use the `HybridRouteLayer` you must `pip install -qU semantic-router[hybrid]`._
+❗️ _If wanting to use local embeddings you can use `FastEmbedEncoder` (`pip install -qU "semantic-router[fastembed]`"). To use the `HybridRouteLayer` you must `pip install -qU "semantic-router[hybrid]"`._
 
 We begin by defining a set of `Route` objects. These are the decision paths that the semantic router can decide to use, let's try two simple routes for now — one for talk on _politics_ and another for _chitchat_:
 
@@ -114,10 +115,3 @@ rl("I'm interested in learning about llama 2").name
 In this case, no decision could be made as we had no matches — so our route layer returned `None`!
 
 ## 📚 [Resources](https://github.com/aurelio-labs/semantic-router/tree/main/docs)
-
-
-
-
-
-
-
diff --git a/coverage.xml b/coverage.xml
index 6726da20c0072e4a9458817d708f40d897fb84a3..5850def2dfb8bf3ee91ce0e20a28f5312ec8c0b2 100644
--- a/coverage.xml
+++ b/coverage.xml
@@ -1,12 +1,12 @@
 <?xml version="1.0" ?>
-<coverage version="7.4.0" timestamp="1704449872184" lines-valid="676" lines-covered="592" line-rate="0.8757" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0">
+<coverage version="7.4.0" timestamp="1704881517474" lines-valid="893" lines-covered="778" line-rate="0.8712" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0">
 	<!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.4.0 -->
 	<!-- 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.9317" branch-rate="0" complexity="0">
+		<package name="." line-rate="0.9263" branch-rate="0" complexity="0">
 			<classes>
 				<class name="__init__.py" filename="__init__.py" complexity="0" line-rate="1" branch-rate="0">
 					<methods/>
@@ -17,301 +17,308 @@
 						<line number="5" hits="1"/>
 					</lines>
 				</class>
-				<class name="hybrid_layer.py" filename="hybrid_layer.py" complexity="0" line-rate="1" branch-rate="0">
+				<class name="hybrid_layer.py" filename="hybrid_layer.py" complexity="0" line-rate="0.9796" branch-rate="0">
 					<methods/>
 					<lines>
 						<line number="1" hits="1"/>
 						<line number="2" hits="1"/>
 						<line number="4" hits="1"/>
+						<line number="8" hits="1"/>
+						<line number="9" hits="1"/>
 						<line number="10" hits="1"/>
-						<line number="11" hits="1"/>
+						<line number="13" hits="1"/>
 						<line number="14" hits="1"/>
 						<line number="15" hits="1"/>
 						<line number="16" hits="1"/>
 						<line number="17" hits="1"/>
-						<line number="18" hits="1"/>
-						<line number="20" hits="1"/>
-						<line number="23" hits="1"/>
-						<line number="24" hits="1"/>
-						<line number="25" hits="1"/>
+						<line number="19" 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="38" hits="1"/>
-						<line number="40" hits="1"/>
+						<line number="30" hits="0"/>
+						<line number="31" hits="0"/>
+						<line number="33" hits="1"/>
+						<line number="35" hits="1"/>
+						<line number="37" 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="49" hits="1"/>
+						<line number="48" hits="1"/>
 						<line number="50" hits="1"/>
 						<line number="52" hits="1"/>
-						<line number="54" hits="1"/>
+						<line number="53" hits="1"/>
 						<line number="55" hits="1"/>
-						<line number="60" hits="1"/>
-						<line number="61" hits="1"/>
-						<line number="62" hits="1"/>
+						<line number="57" hits="1"/>
+						<line number="58" hits="1"/>
+						<line number="63" hits="1"/>
 						<line number="64" hits="1"/>
 						<line number="65" hits="1"/>
-						<line number="66" hits="1"/>
-						<line number="70" hits="1"/>
-						<line number="71" hits="1"/>
+						<line number="67" hits="1"/>
+						<line number="68" hits="1"/>
+						<line number="69" hits="1"/>
 						<line number="73" hits="1"/>
-						<line number="75" hits="1"/>
+						<line number="74" hits="1"/>
 						<line number="76" hits="1"/>
 						<line number="78" hits="1"/>
-						<line number="80" hits="1"/>
-						<line number="82" hits="1"/>
+						<line number="79" hits="1"/>
+						<line number="81" hits="1"/>
 						<line number="83" hits="1"/>
+						<line number="85" hits="1"/>
 						<line number="86" hits="1"/>
-						<line number="87" hits="1"/>
+						<line number="89" hits="1"/>
 						<line number="90" hits="1"/>
-						<line number="91" hits="1"/>
-						<line number="92" hits="1"/>
-						<line number="99" hits="1"/>
-						<line number="106" hits="1"/>
-						<line number="112" hits="1"/>
-						<line number="117" hits="1"/>
-						<line number="118" hits="1"/>
+						<line number="93" hits="1"/>
+						<line number="94" hits="1"/>
+						<line number="95" hits="1"/>
+						<line number="102" hits="1"/>
+						<line number="109" hits="1"/>
+						<line number="115" hits="1"/>
 						<line number="120" hits="1"/>
 						<line number="121" hits="1"/>
 						<line number="123" hits="1"/>
-						<line number="125" hits="1"/>
-						<line number="127" hits="1"/>
+						<line number="124" hits="1"/>
+						<line number="126" hits="1"/>
 						<line number="128" hits="1"/>
-						<line number="129" 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="137" hits="1"/>
-						<line number="138" hits="1"/>
+						<line number="139" hits="1"/>
 						<line number="140" hits="1"/>
 						<line number="141" hits="1"/>
 						<line number="143" hits="1"/>
 						<line number="144" hits="1"/>
 						<line number="146" hits="1"/>
-						<line number="148" hits="1"/>
+						<line number="147" hits="1"/>
 						<line number="149" hits="1"/>
-						<line number="150" hits="1"/>
+						<line number="151" hits="1"/>
 						<line number="152" hits="1"/>
 						<line number="153" 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="161" hits="1"/>
 						<line number="163" hits="1"/>
-						<line number="164" 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="173" hits="1"/>
 						<line number="174" hits="1"/>
-						<line number="175" hits="1"/>
+						<line number="176" hits="1"/>
 						<line number="177" hits="1"/>
+						<line number="178" hits="1"/>
+						<line number="180" hits="1"/>
 					</lines>
 				</class>
-				<class name="layer.py" filename="layer.py" complexity="0" line-rate="0.8836" branch-rate="0">
+				<class name="layer.py" filename="layer.py" complexity="0" line-rate="0.8827" branch-rate="0">
 					<methods/>
 					<lines>
 						<line number="1" hits="1"/>
 						<line number="2" hits="1"/>
-						<line number="4" hits="1"/>
+						<line number="3" hits="1"/>
 						<line number="5" hits="1"/>
-						<line number="7" hits="1"/>
+						<line number="6" hits="1"/>
+						<line number="8" hits="1"/>
+						<line number="9" hits="1"/>
+						<line number="10" hits="1"/>
+						<line number="11" hits="1"/>
 						<line number="12" hits="1"/>
 						<line number="13" hits="1"/>
-						<line number="14" hits="1"/>
-						<line number="15" 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="23" hits="1"/>
+						<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="30" hits="0"/>
-						<line number="31" hits="0"/>
-						<line number="33" hits="1"/>
-						<line number="34" hits="1"/>
-						<line number="35" hits="0"/>
-						<line number="38" hits="0"/>
-						<line number="40" hits="1"/>
+						<line number="28" hits="0"/>
+						<line number="29" hits="0"/>
+						<line number="31" hits="1"/>
+						<line number="32" hits="1"/>
+						<line number="33" hits="0"/>
+						<line number="36" hits="0"/>
+						<line number="38" hits="1"/>
+						<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="46" hits="1"/>
+						<line number="44" hits="1"/>
+						<line number="50" hits="1"/>
 						<line number="52" hits="1"/>
-						<line number="54" hits="1"/>
-						<line number="60" hits="1"/>
-						<line number="61" hits="1"/>
-						<line number="63" hits="0"/>
+						<line number="58" hits="1"/>
+						<line number="59" hits="1"/>
+						<line number="63" hits="1"/>
 						<line number="64" hits="0"/>
-						<line number="65" hits="0"/>
+						<line number="65" hits="1"/>
 						<line number="66" hits="0"/>
-						<line number="67" hits="0"/>
+						<line number="67" hits="1"/>
 						<line number="68" hits="0"/>
-						<line number="69" hits="0"/>
-						<line number="70" hits="1"/>
+						<line number="69" hits="1"/>
+						<line number="70" hits="0"/>
 						<line number="71" hits="1"/>
+						<line number="72" hits="1"/>
 						<line number="73" hits="1"/>
-						<line number="74" hits="1"/>
+						<line number="75" hits="1"/>
 						<line number="76" hits="1"/>
-						<line number="77" hits="1"/>
 						<line number="78" hits="1"/>
 						<line number="79" hits="1"/>
 						<line number="80" hits="1"/>
 						<line number="81" hits="1"/>
 						<line number="82" hits="1"/>
+						<line number="83" hits="1"/>
 						<line number="84" hits="1"/>
-						<line number="88" hits="1"/>
-						<line number="89" hits="1"/>
+						<line number="86" hits="1"/>
 						<line number="90" hits="1"/>
 						<line number="91" hits="1"/>
 						<line number="92" hits="1"/>
 						<line number="93" hits="1"/>
-						<line number="97" hits="0"/>
+						<line number="94" hits="1"/>
+						<line number="95" hits="1"/>
 						<line number="99" hits="1"/>
-						<line number="100" hits="1"/>
-						<line number="106" hits="1"/>
+						<line number="101" hits="1"/>
+						<line number="102" hits="1"/>
 						<line number="108" hits="1"/>
-						<line number="109" hits="1"/>
-						<line number="112" hits="1"/>
-						<line number="113" hits="1"/>
-						<line number="117" hits="1"/>
-						<line number="120" hits="1"/>
-						<line number="121" hits="0"/>
-						<line number="123" hits="1"/>
-						<line number="124" hits="1"/>
+						<line number="110" hits="1"/>
+						<line number="111" hits="1"/>
+						<line number="114" hits="1"/>
+						<line number="115" hits="1"/>
+						<line number="119" hits="1"/>
+						<line number="122" hits="1"/>
+						<line number="123" hits="0"/>
 						<line number="125" hits="1"/>
 						<line number="126" hits="1"/>
 						<line number="127" hits="1"/>
+						<line number="128" hits="1"/>
 						<line number="129" 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="137" hits="1"/>
 						<line number="138" hits="1"/>
+						<line number="139" hits="1"/>
 						<line number="140" hits="1"/>
-						<line number="141" hits="1"/>
-						<line number="142" hits="0"/>
-						<line number="144" hits="1"/>
-						<line number="145" hits="1"/>
-						<line number="148" hits="1"/>
-						<line number="149" hits="1"/>
+						<line number="142" hits="1"/>
+						<line number="143" hits="1"/>
+						<line number="144" hits="0"/>
+						<line number="146" hits="1"/>
+						<line number="147" hits="1"/>
 						<line number="150" hits="1"/>
 						<line number="151" hits="1"/>
+						<line number="152" hits="1"/>
 						<line number="153" hits="1"/>
+						<line number="154" 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="163" hits="1"/>
 						<line number="164" hits="1"/>
 						<line number="165" hits="1"/>
-						<line number="167" hits="1"/>
-						<line number="169" hits="1"/>
-						<line number="171" hits="1"/>
+						<line number="166" hits="1"/>
+						<line number="170" hits="1"/>
+						<line number="172" hits="1"/>
 						<line number="173" hits="1"/>
 						<line number="174" hits="1"/>
 						<line number="175" hits="1"/>
-						<line number="176" hits="1"/>
 						<line number="177" hits="1"/>
 						<line number="179" hits="1"/>
-						<line number="180" hits="1"/>
+						<line number="181" hits="1"/>
+						<line number="182" hits="1"/>
 						<line number="183" hits="1"/>
+						<line number="184" hits="1"/>
 						<line number="185" hits="1"/>
-						<line number="186" hits="0"/>
-						<line number="192" hits="1"/>
-						<line number="193" hits="1"/>
-						<line number="194" hits="1"/>
-						<line number="195" hits="1"/>
-						<line number="196" hits="1"/>
-						<line number="198" hits="1"/>
+						<line number="187" hits="1"/>
+						<line number="188" hits="1"/>
+						<line number="189" hits="0"/>
+						<line number="190" hits="0"/>
+						<line number="195" hits="0"/>
+						<line number="196" hits="0"/>
+						<line number="198" hits="0"/>
 						<line number="199" hits="1"/>
-						<line number="200" hits="1"/>
-						<line number="201" hits="1"/>
 						<line number="202" hits="1"/>
 						<line number="204" hits="1"/>
-						<line number="205" hits="1"/>
-						<line number="206" hits="1"/>
-						<line number="207" hits="1"/>
-						<line number="209" hits="1"/>
-						<line number="210" hits="1"/>
+						<line number="205" hits="0"/>
+						<line number="211" hits="1"/>
 						<line number="212" hits="1"/>
+						<line number="213" hits="1"/>
+						<line number="214" hits="1"/>
 						<line number="215" hits="1"/>
-						<line number="216" hits="1"/>
 						<line number="217" hits="1"/>
+						<line number="218" hits="1"/>
 						<line number="219" hits="1"/>
 						<line number="220" hits="1"/>
 						<line number="221" hits="1"/>
 						<line number="223" hits="1"/>
 						<line number="224" hits="1"/>
 						<line number="225" hits="1"/>
-						<line number="227" hits="1"/>
+						<line number="226" hits="1"/>
 						<line number="228" hits="1"/>
 						<line number="229" hits="1"/>
 						<line number="231" hits="1"/>
-						<line number="233" hits="1"/>
+						<line number="234" hits="1"/>
 						<line number="235" hits="1"/>
+						<line number="237" hits="1"/>
 						<line number="238" hits="1"/>
+						<line number="240" hits="1"/>
 						<line number="241" hits="1"/>
-						<line number="242" hits="1"/>
 						<line number="243" hits="1"/>
+						<line number="244" hits="1"/>
+						<line number="246" hits="1"/>
+						<line number="248" hits="1"/>
 						<line number="250" hits="1"/>
-						<line number="251" hits="1"/>
+						<line number="253" hits="1"/>
+						<line number="256" hits="1"/>
 						<line number="257" hits="1"/>
-						<line number="262" hits="1"/>
-						<line number="263" hits="1"/>
+						<line number="258" hits="1"/>
 						<line number="265" hits="1"/>
-						<line number="267" hits="1"/>
-						<line number="268" hits="1"/>
-						<line number="270" hits="1"/>
-						<line number="271" hits="1"/>
-						<line number="273" hits="1"/>
-						<line number="274" hits="1"/>
-						<line number="276" hits="1"/>
+						<line number="266" hits="1"/>
+						<line number="272" hits="1"/>
 						<line number="277" hits="1"/>
 						<line number="278" hits="1"/>
-						<line number="279" hits="1"/>
 						<line number="280" hits="1"/>
-						<line number="281" hits="1"/>
 						<line number="282" hits="1"/>
-						<line number="284" hits="1"/>
-						<line number="287" hits="1"/>
+						<line number="283" hits="1"/>
+						<line number="285" hits="1"/>
+						<line number="286" hits="1"/>
 						<line number="288" hits="1"/>
+						<line number="289" hits="1"/>
 						<line number="291" hits="1"/>
 						<line number="292" hits="1"/>
+						<line number="293" hits="1"/>
 						<line number="294" hits="1"/>
 						<line number="295" hits="1"/>
+						<line number="296" hits="1"/>
 						<line number="297" hits="1"/>
-						<line number="298" hits="1"/>
 						<line number="299" hits="1"/>
-						<line number="301" hits="1"/>
+						<line number="302" hits="1"/>
 						<line number="303" hits="1"/>
-						<line number="304" hits="1"/>
+						<line number="306" hits="1"/>
+						<line number="307" hits="1"/>
+						<line number="309" hits="1"/>
 						<line number="310" hits="1"/>
-						<line number="311" hits="1"/>
 						<line number="312" hits="1"/>
+						<line number="313" hits="1"/>
 						<line number="314" hits="1"/>
-						<line number="315" hits="1"/>
 						<line number="316" hits="1"/>
+						<line number="318" hits="1"/>
+						<line number="319" hits="1"/>
+						<line number="325" hits="1"/>
+						<line number="326" hits="1"/>
+						<line number="327" hits="1"/>
+						<line number="329" hits="1"/>
+						<line number="330" hits="1"/>
+						<line number="331" hits="1"/>
 					</lines>
 				</class>
 				<class name="linear.py" filename="linear.py" complexity="0" line-rate="1" branch-rate="0">
@@ -332,7 +339,7 @@
 						<line number="30" hits="1"/>
 					</lines>
 				</class>
-				<class name="route.py" filename="route.py" complexity="0" line-rate="0.9275" branch-rate="0">
+				<class name="route.py" filename="route.py" complexity="0" line-rate="0.9342" branch-rate="0">
 					<methods/>
 					<lines>
 						<line number="1" hits="1"/>
@@ -343,114 +350,131 @@
 						<line number="8" hits="1"/>
 						<line number="9" hits="1"/>
 						<line number="10" hits="1"/>
-						<line number="13" hits="1"/>
+						<line number="11" hits="1"/>
 						<line number="14" hits="1"/>
 						<line number="15" hits="1"/>
 						<line number="16" hits="1"/>
-						<line number="18" 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="25" hits="1"/>
+						<line number="23" hits="1"/>
 						<line number="26" hits="1"/>
-						<line number="28" hits="1"/>
+						<line number="27" hits="1"/>
 						<line number="29" hits="1"/>
 						<line number="30" hits="1"/>
-						<line number="33" hits="1"/>
-						<line number="35" 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="41" 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="46" hits="1"/>
 						<line number="47" hits="1"/>
-						<line number="48" hits="1"/>
-						<line number="50" hits="0"/>
-						<line number="53" hits="0"/>
-						<line number="56" hits="1"/>
-						<line number="57" hits="1"/>
-						<line number="59" hits="1"/>
-						<line number="60" hits="1"/>
-						<line number="62" hits="1"/>
+						<line number="49" hits="1"/>
+						<line number="50" hits="1"/>
+						<line number="51" hits="1"/>
+						<line number="52" hits="1"/>
+						<line number="57" hits="0"/>
+						<line number="60" hits="0"/>
 						<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="72" hits="1"/>
 						<line number="73" hits="1"/>
 						<line number="74" hits="1"/>
-						<line number="76" hits="1"/>
-						<line number="77" hits="1"/>
+						<line number="78" hits="1"/>
 						<line number="79" hits="1"/>
 						<line number="80" hits="1"/>
-						<line number="82" hits="1"/>
+						<line number="81" hits="1"/>
 						<line number="83" hits="1"/>
 						<line number="84" hits="1"/>
-						<line number="86" hits="0"/>
-						<line number="88" 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="117" hits="1"/>
-						<line number="118" hits="1"/>
-						<line number="119" hits="0"/>
-						<line number="121" hits="1"/>
-						<line number="123" hits="1"/>
+						<line number="91" hits="1"/>
+						<line number="93" hits="0"/>
+						<line number="95" hits="1"/>
+						<line number="96" hits="1"/>
+						<line number="97" hits="1"/>
+						<line number="99" hits="1"/>
+						<line number="124" hits="1"/>
 						<line number="125" hits="1"/>
 						<line number="126" hits="1"/>
 						<line number="127" hits="0"/>
+						<line number="129" hits="1"/>
+						<line number="131" hits="1"/>
+						<line number="133" hits="1"/>
+						<line number="134" hits="1"/>
+						<line number="135" hits="1"/>
+						<line number="136" hits="1"/>
+						<line number="137" hits="0"/>
 					</lines>
 				</class>
-				<class name="schema.py" filename="schema.py" complexity="0" line-rate="0.973" branch-rate="0">
+				<class name="schema.py" filename="schema.py" complexity="0" line-rate="0.9574" branch-rate="0">
 					<methods/>
 					<lines>
 						<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="14" hits="1"/>
-						<line number="15" hits="1"/>
+						<line number="12" 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="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="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="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="41" hits="0"/>
+						<line number="40" hits="0"/>
+						<line number="41" hits="1"/>
+						<line number="42" hits="1"/>
 						<line number="43" hits="1"/>
 						<line number="44" hits="1"/>
-						<line number="47" hits="1"/>
+						<line number="46" hits="0"/>
 						<line number="48" hits="1"/>
 						<line number="49" hits="1"/>
 						<line number="52" hits="1"/>
 						<line number="53" hits="1"/>
-						<line number="55" hits="1"/>
+						<line number="54" hits="1"/>
+						<line number="56" hits="1"/>
+						<line number="57" hits="1"/>
+						<line number="58" hits="1"/>
+						<line number="59" hits="1"/>
 						<line number="61" hits="1"/>
 						<line number="62" hits="1"/>
+						<line number="65" hits="1"/>
+						<line number="66" hits="1"/>
+						<line number="68" hits="1"/>
+						<line number="74" hits="1"/>
+						<line number="75" hits="1"/>
 					</lines>
 				</class>
 			</classes>
 		</package>
-		<package name="encoders" line-rate="0.966" branch-rate="0" complexity="0">
+		<package name="encoders" line-rate="0.9369" branch-rate="0" complexity="0">
 			<classes>
 				<class name="__init__.py" filename="encoders/__init__.py" complexity="0" line-rate="1" branch-rate="0">
 					<methods/>
@@ -460,7 +484,8 @@
 						<line number="3" hits="1"/>
 						<line number="4" hits="1"/>
 						<line number="5" hits="1"/>
-						<line number="7" hits="1"/>
+						<line number="6" hits="1"/>
+						<line number="8" hits="1"/>
 					</lines>
 				</class>
 				<class name="base.py" filename="encoders/base.py" complexity="0" line-rate="1" branch-rate="0">
@@ -470,66 +495,76 @@
 						<line number="4" hits="1"/>
 						<line number="5" hits="1"/>
 						<line number="6" hits="1"/>
-						<line number="8" hits="1"/>
+						<line number="7" hits="1"/>
 						<line number="9" hits="1"/>
-						<line number="11" hits="1"/>
+						<line number="10" hits="1"/>
 						<line number="12" hits="1"/>
+						<line number="13" hits="1"/>
 					</lines>
 				</class>
-				<class name="bm25.py" filename="encoders/bm25.py" complexity="0" line-rate="1" branch-rate="0">
+				<class name="bm25.py" filename="encoders/bm25.py" complexity="0" line-rate="0.9574" branch-rate="0">
 					<methods/>
 					<lines>
 						<line number="1" hits="1"/>
 						<line number="3" hits="1"/>
-						<line number="5" hits="1"/>
+						<line number="4" hits="1"/>
+						<line number="7" hits="1"/>
 						<line number="8" hits="1"/>
 						<line number="9" hits="1"/>
 						<line number="10" hits="1"/>
-						<line number="11" hits="1"/>
-						<line number="13" hits="1"/>
-						<line number="14" hits="1"/>
-						<line number="15" hits="1"/>
-						<line number="17" hits="1"/>
+						<line number="12" hits="1"/>
 						<line number="18" hits="1"/>
 						<line number="19" hits="1"/>
 						<line number="20" hits="1"/>
-						<line number="21" hits="1"/>
-						<line number="23" hits="1"/>
-						<line number="25" hits="1"/>
-						<line number="26" hits="1"/>
+						<line number="21" hits="0"/>
+						<line number="22" hits="0"/>
 						<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="33" 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="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="49" hits="1"/>
+						<line number="51" hits="1"/>
+						<line number="53" hits="1"/>
+						<line number="54" hits="1"/>
+						<line number="55" hits="1"/>
+						<line number="56" hits="1"/>
+						<line number="57" hits="1"/>
+						<line number="58" 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="65" hits="1"/>
+						<line number="66" hits="1"/>
+						<line number="67" 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="2" hits="1"/>
+						<line number="4" hits="1"/>
+						<line number="6" hits="1"/>
 						<line number="9" hits="1"/>
 						<line number="10" hits="1"/>
-						<line number="12" hits="1"/>
-						<line number="17" hits="1"/>
-						<line number="18" hits="1"/>
+						<line number="11" hits="1"/>
+						<line number="13" hits="1"/>
 						<line number="19" hits="1"/>
 						<line number="20" hits="1"/>
 						<line number="21" hits="1"/>
@@ -538,22 +573,60 @@
 						<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="31" hits="1"/>
 						<line number="32" hits="1"/>
 						<line number="33" hits="1"/>
 						<line number="34" hits="1"/>
 						<line number="35" hits="1"/>
+						<line number="36" hits="1"/>
+						<line number="37" hits="1"/>
 					</lines>
 				</class>
-				<class name="fastembed.py" filename="encoders/fastembed.py" complexity="0" line-rate="0.8621" branch-rate="0">
+				<class name="fastembed.py" filename="encoders/fastembed.py" complexity="0" line-rate="0.7" branch-rate="0">
 					<methods/>
 					<lines>
 						<line number="1" hits="1"/>
 						<line number="3" hits="1"/>
 						<line number="4" hits="1"/>
+						<line number="6" hits="1"/>
+						<line number="9" hits="1"/>
+						<line number="10" hits="1"/>
+						<line number="11" hits="1"/>
+						<line number="12" hits="1"/>
+						<line number="13" hits="1"/>
+						<line number="14" hits="1"/>
+						<line number="15" hits="1"/>
+						<line number="17" hits="1"/>
+						<line number="20" hits="1"/>
+						<line number="21" hits="1"/>
+						<line number="23" hits="1"/>
+						<line number="24" hits="1"/>
+						<line number="25" hits="1"/>
+						<line number="26" hits="0"/>
+						<line number="27" hits="0"/>
+						<line number="33" hits="1"/>
+						<line number="40" hits="1"/>
+						<line number="42" hits="1"/>
+						<line number="43" hits="0"/>
+						<line number="45" hits="1"/>
+						<line number="46" hits="0"/>
+						<line number="47" hits="0"/>
+						<line number="48" hits="0"/>
+						<line number="49" hits="0"/>
+						<line number="50" hits="0"/>
+						<line number="51" hits="0"/>
+					</lines>
+				</class>
+				<class name="huggingface.py" filename="encoders/huggingface.py" complexity="0" line-rate="0.9667" branch-rate="0">
+					<methods/>
+					<lines>
+						<line number="1" hits="1"/>
+						<line number="2" hits="1"/>
+						<line number="3" hits="1"/>
+						<line number="6" hits="1"/>
 						<line number="7" hits="1"/>
 						<line number="8" hits="1"/>
 						<line number="9" hits="1"/>
@@ -561,83 +634,258 @@
 						<line number="11" hits="1"/>
 						<line number="12" hits="1"/>
 						<line number="13" hits="1"/>
+						<line number="14" hits="1"/>
 						<line number="15" 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="0"/>
-						<line number="23" hits="0"/>
-						<line number="28" hits="1"/>
+						<line number="22" hits="1"/>
+						<line number="23" hits="1"/>
+						<line number="24" hits="1"/>
+						<line number="25" hits="1"/>
+						<line number="31" hits="1"/>
+						<line number="32" hits="1"/>
+						<line number="33" hits="1"/>
+						<line number="34" hits="1"/>
+						<line number="40" hits="1"/>
+						<line number="42" hits="1"/>
+						<line number="47" hits="1"/>
+						<line number="49" hits="1"/>
+						<line number="50" hits="0"/>
+						<line number="53" hits="1"/>
+						<line number="54" hits="1"/>
+						<line number="55" hits="1"/>
+						<line number="57" hits="1"/>
+						<line number="59" hits="1"/>
+						<line number="66" hits="1"/>
+						<line number="67" hits="1"/>
+						<line number="68" hits="1"/>
+						<line number="70" hits="1"/>
+						<line number="74" hits="1"/>
+						<line number="75" hits="1"/>
+						<line number="77" hits="1"/>
+						<line number="78" hits="1"/>
+						<line number="81" hits="1"/>
+						<line number="82" hits="1"/>
+						<line number="86" hits="0"/>
+						<line number="90" hits="1"/>
+						<line number="91" hits="1"/>
+						<line number="93" hits="1"/>
+						<line number="94" hits="1"/>
+						<line number="95" hits="1"/>
+						<line number="97" hits="1"/>
+						<line number="98" hits="1"/>
+						<line number="99" hits="1"/>
+						<line number="102" hits="1"/>
+						<line number="106" hits="1"/>
+						<line number="107" hits="1"/>
+						<line number="108" hits="1"/>
+						<line number="111" hits="1"/>
+						<line number="112" hits="1"/>
+					</lines>
+				</class>
+				<class name="openai.py" filename="encoders/openai.py" complexity="0" line-rate="0.9767" 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="10" hits="1"/>
+						<line number="13" hits="1"/>
+						<line number="14" hits="1"/>
+						<line number="15" hits="1"/>
+						<line number="17" 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="28" hits="0"/>
+						<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="35" hits="1"/>
+						<line number="36" hits="1"/>
 						<line number="37" hits="1"/>
 						<line number="38" 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="0"/>
-						<line number="46" hits="0"/>
+						<line number="45" hits="1"/>
+						<line number="46" hits="1"/>
+						<line number="47" hits="1"/>
+						<line number="48" hits="1"/>
+						<line number="49" hits="1"/>
+						<line number="50" hits="1"/>
+						<line number="51" hits="1"/>
+						<line number="52" hits="1"/>
+						<line number="54" hits="1"/>
+						<line number="59" hits="1"/>
+						<line number="61" hits="1"/>
+						<line number="62" hits="1"/>
+					</lines>
+				</class>
+			</classes>
+		</package>
+		<package name="llms" line-rate="0.9043" branch-rate="0" complexity="0">
+			<classes>
+				<class name="__init__.py" filename="llms/__init__.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="4" hits="1"/>
+						<line number="6" hits="1"/>
+					</lines>
+				</class>
+				<class name="base.py" filename="llms/base.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="12" hits="1"/>
+						<line number="14" hits="1"/>
+						<line number="15" hits="1"/>
 					</lines>
 				</class>
-				<class name="openai.py" filename="encoders/openai.py" complexity="0" line-rate="0.9762" branch-rate="0">
+				<class name="cohere.py" filename="llms/cohere.py" complexity="0" line-rate="0.9655" branch-rate="0">
 					<methods/>
 					<lines>
 						<line number="1" hits="1"/>
 						<line number="2" hits="1"/>
 						<line number="4" hits="1"/>
+						<line number="6" hits="1"/>
+						<line number="7" hits="1"/>
+						<line number="10" hits="1"/>
+						<line number="11" hits="1"/>
+						<line number="13" 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="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="39" hits="1"/>
+						<line number="41" hits="1"/>
+						<line number="42" hits="0"/>
+						<line number="43" hits="1"/>
+						<line number="45" hits="1"/>
+						<line number="46" hits="1"/>
+					</lines>
+				</class>
+				<class name="openai.py" filename="llms/openai.py" complexity="0" line-rate="0.8571" branch-rate="0">
+					<methods/>
+					<lines>
+						<line number="1" hits="1"/>
+						<line number="3" hits="1"/>
 						<line number="5" hits="1"/>
 						<line number="6" hits="1"/>
+						<line number="7" hits="1"/>
 						<line number="8" hits="1"/>
-						<line number="9" hits="1"/>
+						<line number="11" hits="1"/>
 						<line number="12" hits="1"/>
 						<line number="13" hits="1"/>
 						<line number="14" hits="1"/>
 						<line number="16" 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="0"/>
+						<line number="26" hits="1"/>
 						<line number="27" hits="1"/>
-						<line number="28" hits="1"/>
+						<line number="28" hits="0"/>
 						<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="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="47" hits="1"/>
+						<line number="49" hits="1"/>
+						<line number="50" hits="0"/>
+						<line number="51" hits="1"/>
+						<line number="52" hits="0"/>
+						<line number="53" hits="0"/>
+						<line number="54" hits="0"/>
+					</lines>
+				</class>
+				<class name="openrouter.py" filename="llms/openrouter.py" complexity="0" line-rate="0.8649" branch-rate="0">
+					<methods/>
+					<lines>
+						<line number="1" hits="1"/>
+						<line number="3" hits="1"/>
+						<line number="5" hits="1"/>
+						<line number="6" 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="1"/>
+						<line number="14" hits="1"/>
+						<line number="15" hits="1"/>
+						<line number="17" hits="1"/>
+						<line number="25" hits="1"/>
+						<line number="26" 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="0"/>
+						<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="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="49" hits="1"/>
-						<line number="50" hits="1"/>
 						<line number="52" hits="1"/>
-						<line number="57" hits="1"/>
-						<line number="59" hits="1"/>
-						<line number="60" hits="1"/>
+						<line number="54" hits="1"/>
+						<line number="55" hits="0"/>
+						<line number="56" hits="1"/>
+						<line number="57" hits="0"/>
+						<line number="58" hits="0"/>
+						<line number="59" hits="0"/>
 					</lines>
 				</class>
 			</classes>
 		</package>
-		<package name="utils" line-rate="0.5714" branch-rate="0" complexity="0">
+		<package name="utils" line-rate="0.5246" 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.2545" branch-rate="0">
+				<class name="function_call.py" filename="utils/function_call.py" complexity="0" line-rate="0.2456" branch-rate="0">
 					<methods/>
 					<lines>
 						<line number="1" hits="1"/>
@@ -663,57 +911,60 @@
 						<line number="35" hits="1"/>
 						<line number="41" hits="1"/>
 						<line number="44" hits="1"/>
-						<line number="45" hits="0"/>
 						<line number="47" hits="0"/>
-						<line number="76" hits="0"/>
+						<line number="49" hits="0"/>
 						<line number="77" hits="0"/>
 						<line number="78" hits="0"/>
+						<line number="79" hits="0"/>
 						<line number="80" hits="0"/>
 						<line number="82" hits="0"/>
-						<line number="83" hits="0"/>
 						<line number="84" hits="0"/>
 						<line number="85" hits="0"/>
-						<line number="88" hits="1"/>
-						<line number="90" hits="0"/>
+						<line number="86" hits="0"/>
+						<line number="87" hits="0"/>
+						<line number="90" hits="1"/>
 						<line number="92" hits="0"/>
-						<line number="93" hits="0"/>
 						<line number="94" hits="0"/>
 						<line number="95" hits="0"/>
-						<line number="99" hits="0"/>
-						<line number="100" hits="0"/>
+						<line number="96" hits="0"/>
+						<line number="97" 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="106" hits="0"/>
-						<line number="110" hits="1"/>
-						<line number="111" hits="0"/>
-						<line number="113" hits="0"/>
-						<line number="114" hits="0"/>
+						<line number="107" hits="0"/>
+						<line number="108" hits="0"/>
+						<line number="112" hits="1"/>
 						<line number="115" hits="0"/>
-						<line number="116" hits="0"/>
+						<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"/>
 					</lines>
 				</class>
-				<class name="llm.py" filename="utils/llm.py" complexity="0" line-rate="0.2857" branch-rate="0">
+				<class name="llm.py" filename="utils/llm.py" complexity="0" line-rate="0" 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="1" hits="0"/>
+						<line number="3" hits="0"/>
+						<line number="5" hits="0"/>
+						<line number="6" hits="0"/>
 						<line number="9" hits="0"/>
 						<line number="10" hits="0"/>
-						<line number="15" hits="0"/>
-						<line number="27" hits="0"/>
-						<line number="29" hits="0"/>
+						<line number="11" hits="0"/>
+						<line number="16" hits="0"/>
+						<line number="28" hits="0"/>
 						<line number="30" hits="0"/>
 						<line number="31" hits="0"/>
 						<line number="32" hits="0"/>
 						<line number="33" hits="0"/>
 						<line number="34" hits="0"/>
+						<line number="35" hits="0"/>
 					</lines>
 				</class>
 				<class name="logger.py" filename="utils/logger.py" complexity="0" line-rate="1" branch-rate="0">
@@ -746,34 +997,34 @@
 						<line number="1" hits="1"/>
 						<line number="3" hits="1"/>
 						<line number="6" 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="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="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="49" hits="1"/>
+						<line number="50" hits="1"/>
 						<line number="51" hits="1"/>
-						<line number="52" hits="1"/>
+						<line number="53" hits="1"/>
 						<line number="54" hits="1"/>
-						<line number="59" hits="1"/>
-						<line number="60" hits="1"/>
-						<line number="63" hits="1"/>
-						<line number="64" hits="1"/>
-						<line number="67" hits="1"/>
-						<line number="71" hits="1"/>
-						<line number="72" hits="1"/>
+						<line number="56" hits="1"/>
+						<line number="61" hits="1"/>
+						<line number="62" hits="1"/>
+						<line number="65" hits="1"/>
+						<line number="66" hits="1"/>
+						<line number="69" hits="1"/>
+						<line number="74" hits="1"/>
+						<line number="75" hits="1"/>
 					</lines>
 				</class>
 			</classes>
diff --git a/docs/encoders/fastembed.ipynb b/docs/encoders/fastembed.ipynb
index 27bad545385bd76c7c8064f988e84b0b5c0f180e..68b1f69cd7228a25ec2ae97e6ffa4f13ca39a116 100644
--- a/docs/encoders/fastembed.ipynb
+++ b/docs/encoders/fastembed.ipynb
@@ -114,13 +114,13 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 3,
+   "execution_count": 6,
    "metadata": {},
    "outputs": [],
    "source": [
     "from semantic_router.encoders import FastEmbedEncoder\n",
     "\n",
-    "encoder = FastEmbedEncoder()"
+    "encoder = FastEmbedEncoder(name=\"BAAI/bge-small-en-v1.5\")"
    ]
   },
   {
@@ -254,7 +254,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.11.5"
+   "version": "3.9.18"
   }
  },
  "nbformat": 4,
diff --git a/docs/examples/function_calling.ipynb b/docs/examples/function_calling.ipynb
index 401ab04a71296066ad1aa7762d0c4e3f1b7f9d52..5392e54e537cadeb74ec3554c37d8f76c9e4a96c 100644
--- a/docs/examples/function_calling.ipynb
+++ b/docs/examples/function_calling.ipynb
@@ -512,7 +512,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.11.3"
+   "version": "3.9.18"
   }
  },
  "nbformat": 4,
diff --git a/poetry.lock b/poetry.lock
index 68a8e12ef90ffa9c6ed31e8a94d14f58544e3a74..26e392884612b9b043d9234a858b83554444b278 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
 
 [[package]]
 name = "aiohttp"
@@ -250,6 +250,17 @@ d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
 jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
 uvloop = ["uvloop (>=0.15.2)"]
 
+[[package]]
+name = "cachetools"
+version = "5.3.2"
+description = "Extensible memoizing collections and decorators"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"},
+    {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"},
+]
+
 [[package]]
 name = "certifi"
 version = "2023.11.17"
@@ -325,6 +336,17 @@ files = [
 [package.dependencies]
 pycparser = "*"
 
+[[package]]
+name = "chardet"
+version = "5.2.0"
+description = "Universal encoding detector for Python 3"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"},
+    {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"},
+]
+
 [[package]]
 name = "charset-normalizer"
 version = "3.3.2"
@@ -440,13 +462,13 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
 
 [[package]]
 name = "cohere"
-version = "4.40"
+version = "4.41"
 description = "Python SDK for the Cohere API"
 optional = false
 python-versions = ">=3.8,<4.0"
 files = [
-    {file = "cohere-4.40-py3-none-any.whl", hash = "sha256:75dac8369d97fadc05901352d9db64a0ca6cd40c08423f3c4691f57eb7b131e7"},
-    {file = "cohere-4.40.tar.gz", hash = "sha256:d9e5c1fa7f80a193c03330a634954b927bf188ead7dcfdb51865480f73aebda8"},
+    {file = "cohere-4.41-py3-none-any.whl", hash = "sha256:39470cc412fa96a1c612f522d48d7d86b34b3163a04030cff83ec48ebbaff32f"},
+    {file = "cohere-4.41.tar.gz", hash = "sha256:8509ca196dc038eca81e474d3cd5896da2ea168a4d3c578b4cb6969994be34ef"},
 ]
 
 [package.dependencies]
@@ -624,6 +646,17 @@ files = [
     {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
 ]
 
+[[package]]
+name = "distlib"
+version = "0.3.8"
+description = "Distribution utilities"
+optional = false
+python-versions = "*"
+files = [
+    {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
+    {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
+]
+
 [[package]]
 name = "distro"
 version = "1.9.0"
@@ -679,42 +712,42 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth
 
 [[package]]
 name = "fastavro"
-version = "1.9.2"
+version = "1.9.3"
 description = "Fast read/write of AVRO files"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "fastavro-1.9.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:223cecf135fd29b83ca6a30035b15b8db169aeaf8dc4f9a5d34afadc4b31638a"},
-    {file = "fastavro-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e08c9be8c6f7eed2cf30f8b64d50094cba38a81b751c7db9f9c4be2656715259"},
-    {file = "fastavro-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:394f06cc865c6fbae3bbca323633a28a5d914c55dc2c1cdefb75432456ef8f6f"},
-    {file = "fastavro-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7a7caadd47bdd04bda534ff70b4b98d2823800c488fd911918115aec4c4dc09b"},
-    {file = "fastavro-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:68478a1b8a583d83ad6550e9dceac6cbb148a99a52c3559a0413bf4c0b9c8786"},
-    {file = "fastavro-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:b59a1123f1d534743af33fdbda80dd7b9146685bdd7931eae12bee6203065222"},
-    {file = "fastavro-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:887c20dc527a549764c91f9e48ece071f2f26d217af66ebcaeb87bf29578fee5"},
-    {file = "fastavro-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46458f78b481c12db62d3d8a81bae09cb0b5b521c0d066c6856fc2746908d00d"},
-    {file = "fastavro-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f4a2a4bed0e829f79fa1e4f172d484b2179426e827bcc80c0069cc81328a5af"},
-    {file = "fastavro-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6167f9bbe1c5a28fbc2db767f97dbbb4981065e6eeafd4e613f6fe76c576ffd4"},
-    {file = "fastavro-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d574bc385f820da0404528157238de4e5fdd775d2cb3d05b3b0f1b475d493837"},
-    {file = "fastavro-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:ec600eb15b3ec931904c5bf8da62b3b725cb0f369add83ba47d7b5e9322f92a0"},
-    {file = "fastavro-1.9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c82b0761503420cd45f7f50bc31975ac1c75b5118e15434c1d724b751abcc249"},
-    {file = "fastavro-1.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db62d9b8c944b8d9c481e5f980d5becfd034bdd58c72e27c9333bd504b06bda0"},
-    {file = "fastavro-1.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65e61f040bc9494646f42a466e9cd428783b82d7161173f3296710723ba5a453"},
-    {file = "fastavro-1.9.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6278b93cdd5bef1778c0232ce1f265137f90bc6be97a5c1dd7e0d99a406c0488"},
-    {file = "fastavro-1.9.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cd003ddea5d89720194b6e57011c37221d9fc4ddc750e6f4723516eb659be686"},
-    {file = "fastavro-1.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:43f09d100a26e8b59f30dde664d93e423b648e008abfc43132608a18fe8ddcc2"},
-    {file = "fastavro-1.9.2-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:3ddffeff5394f285c69f9cd481f47b6cf62379840cdbe6e0dc74683bd589b56e"},
-    {file = "fastavro-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e75a2b2ec697d2058a7d96522e921f03f174cf9049ace007c24be7ab58c5370"},
-    {file = "fastavro-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd2e8fd0567483eb0fdada1b979ad4d493305dfdd3f351c82a87df301f0ae1f"},
-    {file = "fastavro-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c652dbe3f087c943a5b89f9a50a574e64f23790bfbec335ce2b91a2ae354a443"},
-    {file = "fastavro-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bba73e9a1822162f1b3a43de0362f29880014c5c4d49d63ad7fcce339ef73ea2"},
-    {file = "fastavro-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:beeef2964bbfd09c539424808539b956d7425afbb7055b89e2aa311374748b56"},
-    {file = "fastavro-1.9.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:d5fa48266d75e057b27d8586b823d6d7d7c94593fd989d75033eb4c8078009fb"},
-    {file = "fastavro-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b69aeb0d063f5955a0e412f9779444fc452568a49db75a90a8d372f9cb4a01c8"},
-    {file = "fastavro-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ce336c59fb40fdb8751bda8cc6076cfcdf9767c3c107f6049e049166b26c61f"},
-    {file = "fastavro-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:581036e18661f045415a51ad528865e1d7ba5a9690a3dede9e6ea50f94ed6c4c"},
-    {file = "fastavro-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b6b5c3cda569c0a130fd2d08d4c53a326ede7e05174a24eda08f7698f70eda"},
-    {file = "fastavro-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:d33e40f246bf07f106f9d2da68d0234efcc62276b6e35bde00ff920ea7f871fd"},
-    {file = "fastavro-1.9.2.tar.gz", hash = "sha256:5c1ffad986200496bd69b5c4748ae90b5d934d3b1456f33147bee3a0bb17f89b"},
+    {file = "fastavro-1.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5e9b2e1427fb84c0754bc34923d10cabcf2ed23230201208a1371ab7b6027674"},
+    {file = "fastavro-1.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ef82f86ae276309abc0072598474b6be68105a0b28f8d7cc0398d1d353d7de"},
+    {file = "fastavro-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:280ef7ab7232ecb2097038d6842416ec717d0e1c314b80ff245f85201f3396a4"},
+    {file = "fastavro-1.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a36cfc0421ed7576ecb1c22de7bd1dedcce62aebbffcc597379d59171e5d76e"},
+    {file = "fastavro-1.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d80f2e20199140eb8c036b4393e9bc9eff325543311b958c72318999499d4279"},
+    {file = "fastavro-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:a435f7edd7c5b52cee3f23ca950cd9373ab35cf2aa3d269b3d6aca7e2fc1372c"},
+    {file = "fastavro-1.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2a7053ed10194ec53754f5337b57b3273a74b48505edcd6edb79fe3c4cd259c0"},
+    {file = "fastavro-1.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853e01f13534d1baa0a3d493a8573e665e93ffa35b4bf1d125e21764d343af8e"},
+    {file = "fastavro-1.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5a279cda25d876e6f120950cadf184a307fd8998f9a22a90bb62e6749f88d1e"},
+    {file = "fastavro-1.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:63d6f928840f3fb1f2e1fe20bc8b7d0e1a51ba4bb0e554ecb837a669fba31288"},
+    {file = "fastavro-1.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8807046edc78f50b3ea5f55f6a534c87b2a13538e7c56fec3532ef802bcae333"},
+    {file = "fastavro-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:e502579da4a51c5630eadbd811a1b3d262d6e783bf19998cfb33d2ea0cf6f516"},
+    {file = "fastavro-1.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6b665efe442061df8d9608c2fb692847df85d52ad825b776c441802f0dfa6571"},
+    {file = "fastavro-1.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b8c96d81f0115633489d7f1133a03832922629a61ca81c1d47b482ddcda3b94"},
+    {file = "fastavro-1.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:338c7ec94dd2474c4679e44d2560a1922cb6fa99acbb7b18957264baf8eadfc7"},
+    {file = "fastavro-1.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a509b34c9af71a109c633631ac2f6d2209830e13200d0048f7e9c057fd563f8f"},
+    {file = "fastavro-1.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:967edefab470987c024cd5a1fcd04744a50a91e740c7bdf325181043a47f1083"},
+    {file = "fastavro-1.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:033c15e8ed02f80f01d58be1cd880b09fd444faf277263d563a727711d47a98a"},
+    {file = "fastavro-1.9.3-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:6b38723327603d77080aec56628e13a739415f8596ca0cc41a905615977c6d6b"},
+    {file = "fastavro-1.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:046d75c4400941fd08f0a6855a34ae63bf02ea01f366b5b749942abe10640056"},
+    {file = "fastavro-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87ab312b8baf0e61ee717878d390022ee1b713d70b244d69efbf3325680f9749"},
+    {file = "fastavro-1.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c562fcf8f5091a2446aafd0c2a0da590c24e0b53527a0100d33908e32f20eea8"},
+    {file = "fastavro-1.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2aa0111e7ebd076d2a094862bbdf8ea175cebba148fcce6c89ff46b625e334b4"},
+    {file = "fastavro-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:652072e0f455ca19a1ee502b527e603389783657c130d81f89df66775979d6f5"},
+    {file = "fastavro-1.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:0a57cdd4edaee36d4216faf801ebc7f53f45e4e1518bdd9832d6f6f1d6e2d88f"},
+    {file = "fastavro-1.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b46a18ebed61573b0823c28eda2716485d283258a83659c7fe6ad3aaeacfed4"},
+    {file = "fastavro-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f756f0723f3bd97db20437d0a8e45712839e6ccd7c82f4d82469533be48b4c7"},
+    {file = "fastavro-1.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d98d5a08063f5b6d7ac5016a0dfe0698b50d9987cb74686f7dfa8288b7b09e0b"},
+    {file = "fastavro-1.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:00698e60db58a2d52cb709df882d451fb7664ebb2f8cb37d9171697e060dc767"},
+    {file = "fastavro-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:d021bbc135023194688e88a7431fb0b5e3ce20e27153bf258f2ce08ee1a0106b"},
+    {file = "fastavro-1.9.3.tar.gz", hash = "sha256:a30d3d2353f6d3b4f6dcd6a97ae937b3775faddd63f5856fe11ba3b0dbb1756a"},
 ]
 
 [package.extras]
@@ -746,7 +779,7 @@ tqdm = ">=4.65,<5.0"
 name = "filelock"
 version = "3.13.1"
 description = "A platform independent file lock."
-optional = true
+optional = false
 python-versions = ">=3.8"
 files = [
     {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"},
@@ -1069,13 +1102,13 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio"
 
 [[package]]
 name = "ipython"
-version = "8.20.0"
+version = "8.18.1"
 description = "IPython: Productive Interactive Computing"
 optional = false
-python-versions = ">=3.10"
+python-versions = ">=3.9"
 files = [
-    {file = "ipython-8.20.0-py3-none-any.whl", hash = "sha256:bc9716aad6f29f36c449e30821c9dd0c1c1a7b59ddcc26931685b87b4c569619"},
-    {file = "ipython-8.20.0.tar.gz", hash = "sha256:2f21bd3fc1d51550c89ee3944ae04bbc7bc79e129ea0937da6e6c68bfdbf117a"},
+    {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"},
+    {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"},
 ]
 
 [package.dependencies]
@@ -1089,19 +1122,20 @@ prompt-toolkit = ">=3.0.41,<3.1.0"
 pygments = ">=2.4.0"
 stack-data = "*"
 traitlets = ">=5"
+typing-extensions = {version = "*", markers = "python_version < \"3.10\""}
 
 [package.extras]
-all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.23)", "pandas", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"]
+all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"]
 black = ["black"]
-doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"]
+doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"]
 kernel = ["ipykernel"]
 nbconvert = ["nbconvert"]
 nbformat = ["nbformat"]
 notebook = ["ipywidgets", "notebook"]
 parallel = ["ipyparallel"]
 qtconsole = ["qtconsole"]
-test = ["pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"]
-test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath", "trio"]
+test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"]
+test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"]
 
 [[package]]
 name = "jedi"
@@ -1162,6 +1196,7 @@ files = [
 ]
 
 [package.dependencies]
+importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""}
 jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0"
 python-dateutil = ">=2.8.2"
 pyzmq = ">=23.0"
@@ -1219,16 +1254,6 @@ files = [
     {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
     {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
     {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
-    {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
     {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
     {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
     {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
@@ -1790,13 +1815,13 @@ sympy = "*"
 
 [[package]]
 name = "openai"
-version = "1.6.1"
+version = "1.7.0"
 description = "The official Python library for the openai API"
 optional = false
 python-versions = ">=3.7.1"
 files = [
-    {file = "openai-1.6.1-py3-none-any.whl", hash = "sha256:bc9f774838d67ac29fb24cdeb2d58faf57de8b311085dcd1348f7aa02a96c7ee"},
-    {file = "openai-1.6.1.tar.gz", hash = "sha256:d553ca9dbf9486b08e75b09e8671e4f638462aaadccfced632bf490fc3d75fa2"},
+    {file = "openai-1.7.0-py3-none-any.whl", hash = "sha256:2282e8e15acb05df79cccba330c025b8e84284c7ec1f3fa31f167a8479066333"},
+    {file = "openai-1.7.0.tar.gz", hash = "sha256:f2a8dcb739e8620c9318a2c6304ea72aebb572ba02fa1d586344405e80d567d3"},
 ]
 
 [package.dependencies]
@@ -2080,6 +2105,25 @@ files = [
 plugins = ["importlib-metadata"]
 windows-terminal = ["colorama (>=0.4.6)"]
 
+[[package]]
+name = "pyproject-api"
+version = "1.6.1"
+description = "API to interact with the python pyproject.toml based projects"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pyproject_api-1.6.1-py3-none-any.whl", hash = "sha256:4c0116d60476b0786c88692cf4e325a9814965e2469c5998b830bba16b183675"},
+    {file = "pyproject_api-1.6.1.tar.gz", hash = "sha256:1817dc018adc0d1ff9ca1ed8c60e1623d5aaca40814b953af14a9cf9a5cae538"},
+]
+
+[package.dependencies]
+packaging = ">=23.1"
+tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
+
+[package.extras]
+docs = ["furo (>=2023.8.19)", "sphinx (<7.2)", "sphinx-autodoc-typehints (>=1.24)"]
+testing = ["covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "setuptools (>=68.1.2)", "wheel (>=0.41.2)"]
+
 [[package]]
 name = "pyreadline3"
 version = "3.4.1"
@@ -2217,7 +2261,6 @@ 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"},
@@ -2225,15 +2268,8 @@ 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"},
@@ -2250,7 +2286,6 @@ 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"},
@@ -2258,7 +2293,6 @@ 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"},
@@ -2902,6 +2936,33 @@ files = [
     {file = "tornado-6.4.tar.gz", hash = "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee"},
 ]
 
+[[package]]
+name = "tox"
+version = "4.11.4"
+description = "tox is a generic virtualenv management and test command line tool"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "tox-4.11.4-py3-none-any.whl", hash = "sha256:2adb83d68f27116812b69aa36676a8d6a52249cb0d173649de0e7d0c2e3e7229"},
+    {file = "tox-4.11.4.tar.gz", hash = "sha256:73a7240778fabf305aeb05ab8ea26e575e042ab5a18d71d0ed13e343a51d6ce1"},
+]
+
+[package.dependencies]
+cachetools = ">=5.3.1"
+chardet = ">=5.2"
+colorama = ">=0.4.6"
+filelock = ">=3.12.3"
+packaging = ">=23.1"
+platformdirs = ">=3.10"
+pluggy = ">=1.3"
+pyproject-api = ">=1.6.1"
+tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
+virtualenv = ">=20.24.3"
+
+[package.extras]
+docs = ["furo (>=2023.8.19)", "sphinx (>=7.2.4)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.24)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.1.1)", "devpi-process (>=1)", "diff-cover (>=7.7)", "distlib (>=0.3.7)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.18)", "psutil (>=5.9.5)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-xdist (>=3.3.1)", "re-assert (>=1.1)", "time-machine (>=2.12)", "wheel (>=0.41.2)"]
+
 [[package]]
 name = "tqdm"
 version = "4.66.1"
@@ -3068,6 +3129,26 @@ brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
 socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
 zstd = ["zstandard (>=0.18.0)"]
 
+[[package]]
+name = "virtualenv"
+version = "20.25.0"
+description = "Virtual Python Environment builder"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"},
+    {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"},
+]
+
+[package.dependencies]
+distlib = ">=0.3.7,<1"
+filelock = ">=3.12.2,<4"
+platformdirs = ">=3.9.1,<5"
+
+[package.extras]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
+
 [[package]]
 name = "wcwidth"
 version = "0.2.13"
@@ -3214,5 +3295,5 @@ local = ["torch", "transformers"]
 
 [metadata]
 lock-version = "2.0"
-python-versions = ">=3.10,<3.12"
-content-hash = "9cfa7fae3109942320a1e65fa30ee94e5b355beda5e1b285228b179bc462b4ac"
+python-versions = "^3.9"
+content-hash = "4ceb0344e006fc7657c66548321ff51d34a297ee6f9ab069fdc53e1256024a12"
diff --git a/pyproject.toml b/pyproject.toml
index 02cde056bb3452f0aa72dc68b320ff27b7727819..45f105fd9ac63138f7b5d4d82b6300d1052f2ccb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,15 +14,15 @@ readme = "README.md"
 packages = [{include = "semantic_router"}]
 
 [tool.poetry.dependencies]
-python = ">=3.10,<3.12"
+python = "^3.9"
 pydantic = "^1.8.2"
 openai = "^1.3.9"
 cohere = "^4.32"
 numpy = "^1.25.2"
-pinecone-text = {version = "^0.7.0", optional = true}
 colorlog = "^6.8.0"
 pyyaml = "^6.0.1"
-fastembed = {version = "^0.1.3", optional = true}
+pinecone-text = {version = "^0.7.1", optional = true}
+fastembed = {version = "^0.1.3", optional = true, python = "<3.12"}
 torch = {version = "^2.1.2", optional = true}
 transformers = {version = "^4.36.2", optional = true}
 
@@ -32,7 +32,7 @@ fastembed = ["fastembed"]
 local = ["torch", "transformers"]
 
 [tool.poetry.group.dev.dependencies]
-ipykernel = "^6.26.0"
+ipykernel = "^6.25.0"
 ruff = "^0.1.5"
 black = {extras = ["jupyter"], version = "^23.12.0"}
 pytest = "^7.4.3"
diff --git a/replace.py b/replace.py
new file mode 100644
index 0000000000000000000000000000000000000000..508c3f33f32c42600dc9aa25c21f62bbaf119dd3
--- /dev/null
+++ b/replace.py
@@ -0,0 +1,28 @@
+import os
+import re
+
+
+def replace_type_hints(file_path):
+    with open(file_path, "rb") as file:
+        file_data = file.read()
+
+    # Decode the file data with error handling
+    file_data = file_data.decode("utf-8", errors="ignore")
+
+    # Regular expression pattern to find 'Optional[dict[int, int]]' and replace with 'Optional[dict[int, int]]'
+    file_data = re.sub(
+        r"dict\[(\w+), (\w+)\]\s*\|\s*None", r"Optional[dict[\1, \2]]", file_data
+    )
+
+    with open(file_path, "w") as file:
+        file.write(file_data)
+
+
+# Directory path
+dir_path = "/Users/jakit/customers/aurelio/semantic-router"
+
+# Traverse the directory
+for root, dirs, files in os.walk(dir_path):
+    for file in files:
+        if file.endswith(".py"):
+            replace_type_hints(os.path.join(root, file))
diff --git a/semantic_router/encoders/bm25.py b/semantic_router/encoders/bm25.py
index b1298b373e2420aacee1621634333e3f33c3c7f2..451273cdcc78899861c7b64baea4eb4e1cc6b33b 100644
--- a/semantic_router/encoders/bm25.py
+++ b/semantic_router/encoders/bm25.py
@@ -1,12 +1,12 @@
-from typing import Any
+from typing import Any, Optional
 
 from semantic_router.encoders import BaseEncoder
 from semantic_router.utils.logger import logger
 
 
 class BM25Encoder(BaseEncoder):
-    model: Any | None = None
-    idx_mapping: dict[int, int] | None = None
+    model: Optional[Any] = None
+    idx_mapping: Optional[dict[int, int]] = None
     type: str = "sparse"
 
     def __init__(
@@ -21,7 +21,7 @@ class BM25Encoder(BaseEncoder):
         except ImportError:
             raise ImportError(
                 "Please install pinecone-text to use BM25Encoder. "
-                "You can install it with: `pip install semantic-router[hybrid]`"
+                "You can install it with: `pip install 'semantic-router[hybrid]'`"
             )
 
         self.model = encoder()
diff --git a/semantic_router/encoders/cohere.py b/semantic_router/encoders/cohere.py
index 2f80aaafdc5b5554f86851e6dfc3b12fb3ca08b0..ec8ee0f8fcebd39444f3689d7bdffc2b7e98c812 100644
--- a/semantic_router/encoders/cohere.py
+++ b/semantic_router/encoders/cohere.py
@@ -1,4 +1,5 @@
 import os
+from typing import Optional
 
 import cohere
 
@@ -6,13 +7,13 @@ from semantic_router.encoders import BaseEncoder
 
 
 class CohereEncoder(BaseEncoder):
-    client: cohere.Client | None = None
+    client: Optional[cohere.Client] = None
     type: str = "cohere"
 
     def __init__(
         self,
-        name: str | None = None,
-        cohere_api_key: str | None = None,
+        name: Optional[str] = None,
+        cohere_api_key: Optional[str] = None,
         score_threshold: float = 0.3,
     ):
         if name is None:
diff --git a/semantic_router/encoders/fastembed.py b/semantic_router/encoders/fastembed.py
index 413e3a6ada8e956fcbb8cbdfdd640a29883647df..98cfc6cc529ff4c06e0a3e7f0d0af779df7f71dd 100644
--- a/semantic_router/encoders/fastembed.py
+++ b/semantic_router/encoders/fastembed.py
@@ -27,7 +27,7 @@ class FastEmbedEncoder(BaseEncoder):
             raise ImportError(
                 "Please install fastembed to use FastEmbedEncoder. "
                 "You can install it with: "
-                "`pip install semantic-router[fastembed]`"
+                "`pip install 'semantic-router[fastembed]'`"
             )
 
         embedding_args = {
diff --git a/semantic_router/encoders/huggingface.py b/semantic_router/encoders/huggingface.py
index f84b402ab04c509d21ed266d0df309cf7139ee03..ace189213b76aed940dd8b4280ce1505339f656f 100644
--- a/semantic_router/encoders/huggingface.py
+++ b/semantic_router/encoders/huggingface.py
@@ -1,5 +1,7 @@
-from typing import Any
+from typing import Any, Optional
+
 from pydantic import PrivateAttr
+
 from semantic_router.encoders import BaseEncoder
 
 
@@ -9,7 +11,7 @@ class HuggingFaceEncoder(BaseEncoder):
     score_threshold: float = 0.5
     tokenizer_kwargs: dict = {}
     model_kwargs: dict = {}
-    device: str | None = None
+    device: Optional[str] = None
     _tokenizer: Any = PrivateAttr()
     _model: Any = PrivateAttr()
     _torch: Any = PrivateAttr()
@@ -20,7 +22,7 @@ class HuggingFaceEncoder(BaseEncoder):
 
     def _initialize_hf_model(self):
         try:
-            from transformers import AutoTokenizer, AutoModel
+            from transformers import AutoModel, AutoTokenizer
         except ImportError:
             raise ImportError(
                 "Please install transformers to use HuggingFaceEncoder. "
diff --git a/semantic_router/encoders/openai.py b/semantic_router/encoders/openai.py
index 4ec87638cf9fc2c9d33c3b7c6a77d9d38ecf9b5b..169761afa8f726a72439a534a69bac3ebf73de29 100644
--- a/semantic_router/encoders/openai.py
+++ b/semantic_router/encoders/openai.py
@@ -1,5 +1,6 @@
 import os
 from time import sleep
+from typing import Optional
 
 import openai
 from openai import OpenAIError
@@ -10,13 +11,13 @@ from semantic_router.utils.logger import logger
 
 
 class OpenAIEncoder(BaseEncoder):
-    client: openai.Client | None
+    client: Optional[openai.Client]
     type: str = "openai"
 
     def __init__(
         self,
-        name: str | None = None,
-        openai_api_key: str | None = None,
+        name: Optional[str] = None,
+        openai_api_key: Optional[str] = None,
         score_threshold: float = 0.82,
     ):
         if name is None:
diff --git a/semantic_router/hybrid_layer.py b/semantic_router/hybrid_layer.py
index 5273f531c90bc28167017ae5b86366268408cb3b..d4c81b13c87749f070a2177896ed6c702484fde0 100644
--- a/semantic_router/hybrid_layer.py
+++ b/semantic_router/hybrid_layer.py
@@ -1,3 +1,5 @@
+from typing import Optional
+
 import numpy as np
 from numpy.linalg import norm
 
@@ -18,7 +20,7 @@ class HybridRouteLayer:
     def __init__(
         self,
         encoder: BaseEncoder,
-        sparse_encoder: BM25Encoder | None = None,
+        sparse_encoder: Optional[BM25Encoder] = None,
         routes: list[Route] = [],
         alpha: float = 0.3,
     ):
@@ -39,7 +41,7 @@ class HybridRouteLayer:
             #     self._add_route(route=route)
             self._add_routes(routes)
 
-    def __call__(self, text: str) -> str | None:
+    def __call__(self, text: str) -> Optional[str]:
         results = self._query(text)
         top_class, top_class_scores = self._semantic_classify(results)
         passed = self._pass_threshold(top_class_scores, self.score_threshold)
diff --git a/semantic_router/layer.py b/semantic_router/layer.py
index 082617566edb395c282491c04c9d38da8ae2ed16..cf546bfc1d791c6843bd62ce5ad4f178b9f0254b 100644
--- a/semantic_router/layer.py
+++ b/semantic_router/layer.py
@@ -1,5 +1,6 @@
 import json
 import os
+from typing import Optional
 
 import numpy as np
 import yaml
@@ -52,7 +53,7 @@ class LayerConfig:
         self,
         routes: list[Route] = [],
         encoder_type: str = "openai",
-        encoder_name: str | None = None,
+        encoder_name: Optional[str] = None,
     ):
         self.encoder_type = encoder_type
         if encoder_name is None:
@@ -131,7 +132,7 @@ class LayerConfig:
         self.routes.append(route)
         logger.info(f"Added route `{route.name}`")
 
-    def get(self, name: str) -> Route | None:
+    def get(self, name: str) -> Optional[Route]:
         for route in self.routes:
             if route.name == name:
                 return route
@@ -147,16 +148,17 @@ class LayerConfig:
 
 
 class RouteLayer:
-    index: np.ndarray | None = None
-    categories: np.ndarray | None = None
+    index: Optional[np.ndarray] = None
+    categories: Optional[np.ndarray] = None
     score_threshold: float
     encoder: BaseEncoder
 
     def __init__(
         self,
-        encoder: BaseEncoder | None = None,
-        llm: BaseLLM | None = None,
-        routes: list[Route] | None = None,
+        encoder: Optional[BaseEncoder] = None,
+        llm: Optional[BaseLLM] = None,
+        routes: Optional[list[Route]] = None,
+        top_k_routes: int = 3,
     ):
         logger.info("Initializing RouteLayer")
         self.index = None
diff --git a/semantic_router/llms/base.py b/semantic_router/llms/base.py
index 51db1fd0e5f317ba7a436cecaac891e8628a161e..bf5f29b6005daaa76abc4674971dc8f775f4af80 100644
--- a/semantic_router/llms/base.py
+++ b/semantic_router/llms/base.py
@@ -1,3 +1,5 @@
+from typing import Optional
+
 from pydantic import BaseModel
 
 from semantic_router.schema import Message
@@ -9,5 +11,5 @@ class BaseLLM(BaseModel):
     class Config:
         arbitrary_types_allowed = True
 
-    def __call__(self, messages: list[Message]) -> str | None:
+    def __call__(self, messages: list[Message]) -> Optional[str]:
         raise NotImplementedError("Subclasses must implement this method")
diff --git a/semantic_router/llms/cohere.py b/semantic_router/llms/cohere.py
index 775817001f22a4474f8705b2335c697730035d47..0ec21f354c090f0d1d00da7c20b89c5b233c3a89 100644
--- a/semantic_router/llms/cohere.py
+++ b/semantic_router/llms/cohere.py
@@ -1,4 +1,5 @@
 import os
+from typing import Optional
 
 import cohere
 
@@ -7,12 +8,12 @@ from semantic_router.schema import Message
 
 
 class CohereLLM(BaseLLM):
-    client: cohere.Client | None = None
+    client: Optional[cohere.Client] = None
 
     def __init__(
         self,
-        name: str | None = None,
-        cohere_api_key: str | None = None,
+        name: Optional[str] = None,
+        cohere_api_key: Optional[str] = None,
     ):
         if name is None:
             name = os.getenv("COHERE_CHAT_MODEL_NAME", "command")
diff --git a/semantic_router/llms/openai.py b/semantic_router/llms/openai.py
index 43ddd642bd42702461da56fd5de87dea01635dfb..8b3442c742c2f268773bf551fb49ce0cd24645af 100644
--- a/semantic_router/llms/openai.py
+++ b/semantic_router/llms/openai.py
@@ -1,4 +1,5 @@
 import os
+from typing import Optional
 
 import openai
 
@@ -8,14 +9,14 @@ from semantic_router.utils.logger import logger
 
 
 class OpenAILLM(BaseLLM):
-    client: openai.OpenAI | None
-    temperature: float | None
-    max_tokens: int | None
+    client: Optional[openai.OpenAI]
+    temperature: Optional[float]
+    max_tokens: Optional[int]
 
     def __init__(
         self,
-        name: str | None = None,
-        openai_api_key: str | None = None,
+        name: Optional[str] = None,
+        openai_api_key: Optional[str] = None,
         temperature: float = 0.01,
         max_tokens: int = 200,
     ):
diff --git a/semantic_router/llms/openrouter.py b/semantic_router/llms/openrouter.py
index 587eeb121e60c4e6f63dac289e390dd5c9086a1a..4cc15d6bfedbfa67fb5957129d1ce901544dcb38 100644
--- a/semantic_router/llms/openrouter.py
+++ b/semantic_router/llms/openrouter.py
@@ -1,4 +1,5 @@
 import os
+from typing import Optional
 
 import openai
 
@@ -8,15 +9,15 @@ from semantic_router.utils.logger import logger
 
 
 class OpenRouterLLM(BaseLLM):
-    client: openai.OpenAI | None
-    base_url: str | None
-    temperature: float | None
-    max_tokens: int | None
+    client: Optional[openai.OpenAI]
+    base_url: Optional[str]
+    temperature: Optional[float]
+    max_tokens: Optional[int]
 
     def __init__(
         self,
-        name: str | None = None,
-        openrouter_api_key: str | None = None,
+        name: Optional[str] = None,
+        openrouter_api_key: Optional[str] = None,
         base_url: str = "https://openrouter.ai/api/v1",
         temperature: float = 0.01,
         max_tokens: int = 200,
diff --git a/semantic_router/route.py b/semantic_router/route.py
index 0d8269f0a24df70cb71da172781154de64bf1425..2289825071a39652aeaa467395103d71dc623140 100644
--- a/semantic_router/route.py
+++ b/semantic_router/route.py
@@ -1,6 +1,6 @@
 import json
 import re
-from typing import Any, Callable, Union
+from typing import Any, Callable, Optional, Union
 
 from pydantic import BaseModel
 
@@ -41,9 +41,9 @@ def is_valid(route_config: str) -> bool:
 class Route(BaseModel):
     name: str
     utterances: list[str]
-    description: str | None = None
-    function_schema: dict[str, Any] | None = None
-    llm: BaseLLM | None = None
+    description: Optional[str] = None
+    function_schema: Optional[dict[str, Any]] = None
+    llm: Optional[BaseLLM] = None
 
     def __call__(self, query: str) -> RouteChoice:
         if self.function_schema:
@@ -96,29 +96,29 @@ class Route(BaseModel):
         logger.info("Generating dynamic route...")
 
         prompt = f"""
-        You are tasked to generate a JSON configuration based on the provided
-        function schema. Please follow the template below, no other tokens allowed:
-
-        <config>
-        {{
-            "name": "<function_name>",
-            "utterances": [
-                "<example_utterance_1>",
-                "<example_utterance_2>",
-                "<example_utterance_3>",
-                "<example_utterance_4>",
-                "<example_utterance_5>"]
-        }}
-        </config>
-
-        Only include the "name" and "utterances" keys in your answer.
-        The "name" should match the function name and the "utterances"
-        should comprise a list of 5 example phrases that could be used to invoke
-        the function. Use real values instead of placeholders.
-
-        Input schema:
-        {function_schema}
-        """
+You are tasked to generate a JSON configuration based on the provided
+function schema. Please follow the template below, no other tokens allowed:
+
+<config>
+{{
+    "name": "<function_name>",
+    "utterances": [
+        "<example_utterance_1>",
+        "<example_utterance_2>",
+        "<example_utterance_3>",
+        "<example_utterance_4>",
+        "<example_utterance_5>"]
+}}
+</config>
+
+Only include the "name" and "utterances" keys in your answer.
+The "name" should match the function name and the "utterances"
+should comprise a list of 5 example phrases that could be used to invoke
+the function. Use real values instead of placeholders.
+
+Input schema:
+{function_schema}
+"""
 
         llm_input = [Message(role="user", content=prompt)]
         output = llm(llm_input)
diff --git a/semantic_router/schema.py b/semantic_router/schema.py
index 5e94c23b13f8c9d9359609e46c790e18dd860ab4..bb1a4c6ac26819ef7b13bec1f293fe6e51a66a7a 100644
--- a/semantic_router/schema.py
+++ b/semantic_router/schema.py
@@ -1,4 +1,5 @@
 from enum import Enum
+from typing import Optional
 
 from pydantic import BaseModel
 from pydantic.dataclasses import dataclass
@@ -20,17 +21,19 @@ class EncoderType(Enum):
 
 
 class RouteChoice(BaseModel):
-    name: str | None = None
-    function_call: dict | None = None
+    name: Optional[str] = None
+    function_call: Optional[dict] = None
+    similarity_score: Optional[float] = None
+    trigger: Optional[bool] = None
 
 
 @dataclass
 class Encoder:
     type: EncoderType
-    name: str | None
+    name: Optional[str]
     model: BaseEncoder
 
-    def __init__(self, type: str, name: str | None):
+    def __init__(self, type: str, name: Optional[str]):
         self.type = EncoderType(type)
         self.name = name
         if self.type == EncoderType.HUGGINGFACE:
diff --git a/semantic_router/utils/function_call.py b/semantic_router/utils/function_call.py
index cedd9b6ecd86131b630cf6d4921848604dc88fa0..3c8b3277b3b4cdd8d3c2ebc1849ff3da4cbd1ca7 100644
--- a/semantic_router/utils/function_call.py
+++ b/semantic_router/utils/function_call.py
@@ -47,33 +47,36 @@ def extract_function_inputs(
     logger.info("Extracting function input...")
 
     prompt = f"""
-    You are a helpful assistant designed to output JSON.
-    Given the following function schema
-    << {function_schema} >>
-    and query
-    << {query} >>
-    extract the parameters values from the query, in a valid JSON format.
-    Example:
-    Input:
-    query: "How is the weather in Hawaii right now in International units?"
-    schema:
-    {{
-        "name": "get_weather",
-        "description": "Useful to get the weather in a specific location",
-        "signature": "(location: str, degree: str) -> str",
-        "output": "<class 'str'>",
-    }}
-
-    Result: {{
-        "location": "London",
-        "degree": "Celsius",
-    }}
-
-    Input:
-    query: {query}
-    schema: {function_schema}
-    Result:
-    """
+You are a helpful assistant designed to output JSON.
+Given the following function schema
+<< {function_schema} >>
+and query
+<< {query} >>
+extract the parameters values from the query, in a valid JSON format.
+Example:
+Input:
+query: "How is the weather in Hawaii right now in International units?"
+schema:
+{{
+    "name": "get_weather",
+    "description": "Useful to get the weather in a specific location",
+    "signature": "(location: str, degree: str) -> float",
+    "output": "<class 'float'>",
+}}
+
+Result:
+{{
+    "location": "Hawaii",
+    "degree": "Kelvin",
+}}
+
+Input:
+query: \"{query}\"
+schema:
+{json.dumps(function_schema, indent=4)}
+
+Result:
+"""
     llm_input = [Message(role="user", content=prompt)]
     output = llm(llm_input)
     if not output:
diff --git a/semantic_router/utils/llm.py b/semantic_router/utils/llm.py
index e92c1bcf7752b5fce5a071dde41da4a24d0851a9..4f89566f79df700ec3ae8103b5b8cdfd84700627 100644
--- a/semantic_router/utils/llm.py
+++ b/semantic_router/utils/llm.py
@@ -1,11 +1,12 @@
 import os
+from typing import Optional
 
 import openai
 
 from semantic_router.utils.logger import logger
 
 
-def llm(prompt: str) -> str | None:
+def llm(prompt: str) -> Optional[str]:
     try:
         client = openai.OpenAI(
             base_url="https://openrouter.ai/api/v1",
@@ -35,7 +36,7 @@ def llm(prompt: str) -> str | None:
 
 
 # TODO integrate async LLM function
-# async def allm(prompt: str) -> str | None:
+# async def allm(prompt: str) -> Optional[str]:
 #     try:
 #         client = openai.AsyncOpenAI(
 #             base_url="https://openrouter.ai/api/v1",
diff --git a/test_output.txt b/test_output.txt
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/tests/unit/encoders/test_huggingface.py b/tests/unit/encoders/test_huggingface.py
index 0aa8cb79be8cb7f2be76720501eb9f030fdb3f2b..1e916f3eca2956b32c6147db6cf5c42695720f14 100644
--- a/tests/unit/encoders/test_huggingface.py
+++ b/tests/unit/encoders/test_huggingface.py
@@ -1,8 +1,9 @@
-import pytest
-import numpy as np
 from unittest.mock import patch
-from semantic_router.encoders.huggingface import HuggingFaceEncoder
 
+import numpy as np
+import pytest
+
+from semantic_router.encoders.huggingface import HuggingFaceEncoder
 
 encoder = HuggingFaceEncoder()