semantic-router 0.2.0.dev1__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/.gitignore +5 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/LICENSE +1 -1
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/PKG-INFO +23 -41
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/README.md +12 -1
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/pyproject.toml +36 -24
- semantic_router-0.3.1/semantic_router/__init__.py +11 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/bm25.py +86 -7
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/clip.py +10 -4
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/fastembed.py +15 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/openai.py +25 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/vit.py +7 -5
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/index/__init__.py +2 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/index/local.py +6 -2
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/index/pinecone.py +40 -23
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/index/postgres.py +41 -16
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/index/qdrant.py +44 -49
- semantic_router-0.3.1/semantic_router/index/redis.py +1299 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/llms/__init__.py +10 -0
- semantic_router-0.3.1/semantic_router/llms/atlascloud.py +55 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/llms/base.py +25 -0
- semantic_router-0.3.1/semantic_router/llms/minimax.py +137 -0
- semantic_router-0.3.1/semantic_router/llms/openai_compatible.py +103 -0
- semantic_router-0.3.1/semantic_router/llms/orcarouter.py +52 -0
- semantic_router-0.3.1/semantic_router/llms/requesty.py +53 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/routers/base.py +1 -3
- semantic_router-0.2.0.dev1/semantic_router/__init__.py +0 -6
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/__init__.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/aurelio.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/azure_openai.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/base.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/bedrock.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/cohere.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/encode_input_type.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/google.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/huggingface.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/jina.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/litellm.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/local.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/mistral.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/nvidia_nim.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/ollama.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/tfidf.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/encoders/voyage.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/index/base.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/index/hybrid_local.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/linear.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/llms/cohere.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/llms/grammars/json.gbnf +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/llms/llamacpp.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/llms/mistral.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/llms/ollama.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/llms/openai.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/llms/openrouter.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/llms/zure.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/py.typed +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/route.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/routers/__init__.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/routers/hybrid.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/routers/semantic.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/schema.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/tokenizers.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/utils/__init__.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/utils/defaults.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/utils/function_call.py +0 -0
- {semantic_router-0.2.0.dev1 → semantic_router-0.3.1}/semantic_router/utils/logger.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: semantic-router
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Super fast semantic router for AI decision making
|
|
5
5
|
Author-email: Aurelio AI <hello@aurelio.ai>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
License-File: LICENSE
|
|
8
|
-
Requires-Python: <3.14,>=3.
|
|
8
|
+
Requires-Python: <3.14,>=3.10
|
|
9
9
|
Requires-Dist: aiohttp<4,>=3.10.11
|
|
10
10
|
Requires-Dist: aurelio-sdk>=0.0.19
|
|
11
11
|
Requires-Dist: cohere<6.00,>=5.9.4
|
|
@@ -24,61 +24,30 @@ Requires-Dist: voyageai>=0.4.1
|
|
|
24
24
|
Provides-Extra: all
|
|
25
25
|
Requires-Dist: boto3<2,>=1.34.98; extra == 'all'
|
|
26
26
|
Requires-Dist: botocore<2,>=1.34.110; extra == 'all'
|
|
27
|
-
Requires-Dist:
|
|
28
|
-
Requires-Dist: fastembed<0.4,>=0.3.0; (python_version < '3.13') and extra == 'all'
|
|
27
|
+
Requires-Dist: fastembed<1,>=0.7.0; extra == 'all'
|
|
29
28
|
Requires-Dist: google-cloud-aiplatform<2,>=1.45.0; extra == 'all'
|
|
30
|
-
Requires-Dist:
|
|
31
|
-
Requires-Dist: litellm>=1.83.7; extra == 'all'
|
|
29
|
+
Requires-Dist: litellm>=1.84.0; extra == 'all'
|
|
32
30
|
Requires-Dist: llama-cpp-python<0.2.86,>=0.2.28; (python_version < '3.13') and extra == 'all'
|
|
33
|
-
Requires-Dist: mypy<2,>=1.7.1; extra == 'all'
|
|
34
31
|
Requires-Dist: ollama>=0.1.7; extra == 'all'
|
|
35
|
-
Requires-Dist: pillow
|
|
32
|
+
Requires-Dist: pillow>=10.2.0; (python_version < '3.13') and extra == 'all'
|
|
36
33
|
Requires-Dist: pinecone[asyncio]<8.0.0,>=7.0.0; extra == 'all'
|
|
37
34
|
Requires-Dist: psycopg[binary]<4,>=3.1.0; extra == 'all'
|
|
38
|
-
Requires-Dist: pytest-asyncio<0.25,>=0.24.0; extra == 'all'
|
|
39
|
-
Requires-Dist: pytest-cov<5,>=4.1.0; extra == 'all'
|
|
40
|
-
Requires-Dist: pytest-mock<4,>=3.12.0; extra == 'all'
|
|
41
|
-
Requires-Dist: pytest-timeout; extra == 'all'
|
|
42
|
-
Requires-Dist: pytest-xdist<4,>=3.5.0; extra == 'all'
|
|
43
|
-
Requires-Dist: pytest~=8.2; extra == 'all'
|
|
44
|
-
Requires-Dist: python-dotenv<2,>=1.0.0; extra == 'all'
|
|
45
35
|
Requires-Dist: qdrant-client<2,>=1.11.1; extra == 'all'
|
|
46
|
-
Requires-Dist:
|
|
47
|
-
Requires-Dist: ruff<0.12,>=0.11.2; extra == 'all'
|
|
36
|
+
Requires-Dist: redis<7,>=5.0.0; extra == 'all'
|
|
48
37
|
Requires-Dist: sentence-transformers>=5.0.0; (python_version < '3.13') and extra == 'all'
|
|
49
38
|
Requires-Dist: tokenizers>=0.19; (python_version < '3.13') and extra == 'all'
|
|
50
39
|
Requires-Dist: torch>=2.6.0; (python_version < '3.13') and extra == 'all'
|
|
51
40
|
Requires-Dist: torchvision>=0.17.0; (python_version < '3.13') and extra == 'all'
|
|
52
41
|
Requires-Dist: transformers>=4.36.2; (python_version < '3.13') and extra == 'all'
|
|
53
|
-
Requires-Dist: types-pyyaml<7,>=6.0.12.12; extra == 'all'
|
|
54
|
-
Requires-Dist: types-requests<3,>=2.31.0; extra == 'all'
|
|
55
42
|
Provides-Extra: bedrock
|
|
56
43
|
Requires-Dist: boto3<2,>=1.34.98; extra == 'bedrock'
|
|
57
44
|
Requires-Dist: botocore<2,>=1.34.110; extra == 'bedrock'
|
|
58
|
-
Provides-Extra: dev
|
|
59
|
-
Requires-Dist: dagger-io>=0.1.1; (python_version >= '3.11') and extra == 'dev'
|
|
60
|
-
Requires-Dist: ipykernel<7,>=6.25.0; extra == 'dev'
|
|
61
|
-
Requires-Dist: litellm>=1.83.7; extra == 'dev'
|
|
62
|
-
Requires-Dist: mypy<2,>=1.7.1; extra == 'dev'
|
|
63
|
-
Requires-Dist: pytest-asyncio<0.25,>=0.24.0; extra == 'dev'
|
|
64
|
-
Requires-Dist: pytest-cov<5,>=4.1.0; extra == 'dev'
|
|
65
|
-
Requires-Dist: pytest-mock<4,>=3.12.0; extra == 'dev'
|
|
66
|
-
Requires-Dist: pytest-timeout; extra == 'dev'
|
|
67
|
-
Requires-Dist: pytest-xdist<4,>=3.5.0; extra == 'dev'
|
|
68
|
-
Requires-Dist: pytest~=8.2; extra == 'dev'
|
|
69
|
-
Requires-Dist: python-dotenv<2,>=1.0.0; extra == 'dev'
|
|
70
|
-
Requires-Dist: requests-mock<2,>=1.12.1; extra == 'dev'
|
|
71
|
-
Requires-Dist: ruff<0.12,>=0.11.2; extra == 'dev'
|
|
72
|
-
Requires-Dist: types-pyyaml<7,>=6.0.12.12; extra == 'dev'
|
|
73
|
-
Requires-Dist: types-requests<3,>=2.31.0; extra == 'dev'
|
|
74
|
-
Provides-Extra: docs
|
|
75
|
-
Requires-Dist: pydoc-markdown>=4.8.2; (python_version < '3.12') and extra == 'docs'
|
|
76
45
|
Provides-Extra: fastembed
|
|
77
|
-
Requires-Dist: fastembed<
|
|
46
|
+
Requires-Dist: fastembed<1,>=0.7.0; extra == 'fastembed'
|
|
78
47
|
Provides-Extra: google
|
|
79
48
|
Requires-Dist: google-cloud-aiplatform<2,>=1.45.0; extra == 'google'
|
|
80
49
|
Provides-Extra: litellm
|
|
81
|
-
Requires-Dist: litellm>=1.
|
|
50
|
+
Requires-Dist: litellm>=1.84.0; extra == 'litellm'
|
|
82
51
|
Provides-Extra: local
|
|
83
52
|
Requires-Dist: llama-cpp-python<0.2.86,>=0.2.28; (python_version < '3.13') and extra == 'local'
|
|
84
53
|
Requires-Dist: sentence-transformers>=5.0.0; (python_version < '3.13') and extra == 'local'
|
|
@@ -93,8 +62,10 @@ Provides-Extra: postgres
|
|
|
93
62
|
Requires-Dist: psycopg[binary]<4,>=3.1.0; extra == 'postgres'
|
|
94
63
|
Provides-Extra: qdrant
|
|
95
64
|
Requires-Dist: qdrant-client<2,>=1.11.1; extra == 'qdrant'
|
|
65
|
+
Provides-Extra: redis
|
|
66
|
+
Requires-Dist: redis<7,>=5.0.0; extra == 'redis'
|
|
96
67
|
Provides-Extra: vision
|
|
97
|
-
Requires-Dist: pillow
|
|
68
|
+
Requires-Dist: pillow>=10.2.0; (python_version < '3.13') and extra == 'vision'
|
|
98
69
|
Requires-Dist: torch>=2.6.0; (python_version < '3.13') and extra == 'vision'
|
|
99
70
|
Requires-Dist: torchvision>=0.17.0; (python_version < '3.13') and extra == 'vision'
|
|
100
71
|
Requires-Dist: transformers>=4.36.2; (python_version < '3.13') and extra == 'vision'
|
|
@@ -109,7 +80,6 @@ Description-Content-Type: text/markdown
|
|
|
109
80
|
<img alt="" src="https://img.shields.io/github/repo-size/aurelio-labs/semantic-router" />
|
|
110
81
|
<a href="https://github.com/aurelio-labs/semantic-router/issues"><img alt="GitHub Issues" src="https://img.shields.io/github/issues/aurelio-labs/semantic-router" />
|
|
111
82
|
<a href="https://github.com/aurelio-labs/semantic-router/pulls"><img alt="GitHub Pull Requests" src="https://img.shields.io/github/issues-pr/aurelio-labs/semantic-router" />
|
|
112
|
-
<img src="https://codecov.io/gh/aurelio-labs/semantic-router/graph/badge.svg?token=H8OOMV2TUF" />
|
|
113
83
|
<a href="https://github.com/aurelio-labs/semantic-router/blob/main/LICENSE"><img alt="Github License" src="https://img.shields.io/badge/License-MIT-yellow.svg" />
|
|
114
84
|
</p>
|
|
115
85
|
|
|
@@ -119,6 +89,18 @@ Semantic Router is a superfast decision-making layer for your LLMs and agents. R
|
|
|
119
89
|
|
|
120
90
|
---
|
|
121
91
|
|
|
92
|
+
## Versions
|
|
93
|
+
|
|
94
|
+
Semantic Router has two release lines. The **0.x line** (`v0` branch) is the current stable library and finishes at 0.3.0, after which it receives bug fixes only. The **1.x line** (`main` branch) is a breaking rewrite of the routing layer and ships as 1.0.0. Pin to the line you are on:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
pip install "semantic-router<1" # 0.x
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Details, and the docs for each line, are on the [versions page](https://docs.aurelio.ai/semantic-router/v0/get-started/versions).
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
122
104
|
## Quickstart
|
|
123
105
|
|
|
124
106
|
To get started with _semantic-router_ we install it like so:
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
<img alt="" src="https://img.shields.io/github/repo-size/aurelio-labs/semantic-router" />
|
|
8
8
|
<a href="https://github.com/aurelio-labs/semantic-router/issues"><img alt="GitHub Issues" src="https://img.shields.io/github/issues/aurelio-labs/semantic-router" />
|
|
9
9
|
<a href="https://github.com/aurelio-labs/semantic-router/pulls"><img alt="GitHub Pull Requests" src="https://img.shields.io/github/issues-pr/aurelio-labs/semantic-router" />
|
|
10
|
-
<img src="https://codecov.io/gh/aurelio-labs/semantic-router/graph/badge.svg?token=H8OOMV2TUF" />
|
|
11
10
|
<a href="https://github.com/aurelio-labs/semantic-router/blob/main/LICENSE"><img alt="Github License" src="https://img.shields.io/badge/License-MIT-yellow.svg" />
|
|
12
11
|
</p>
|
|
13
12
|
|
|
@@ -17,6 +16,18 @@ Semantic Router is a superfast decision-making layer for your LLMs and agents. R
|
|
|
17
16
|
|
|
18
17
|
---
|
|
19
18
|
|
|
19
|
+
## Versions
|
|
20
|
+
|
|
21
|
+
Semantic Router has two release lines. The **0.x line** (`v0` branch) is the current stable library and finishes at 0.3.0, after which it receives bug fixes only. The **1.x line** (`main` branch) is a breaking rewrite of the routing layer and ships as 1.0.0. Pin to the line you are on:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
pip install "semantic-router<1" # 0.x
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Details, and the docs for each line, are on the [versions page](https://docs.aurelio.ai/semantic-router/v0/get-started/versions).
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
20
31
|
## Quickstart
|
|
21
32
|
|
|
22
33
|
To get started with _semantic-router_ we install it like so:
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "semantic-router"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
description = "Super fast semantic router for AI decision making"
|
|
5
5
|
authors = [{ name = "Aurelio AI", email = "hello@aurelio.ai" }]
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.10,<3.14"
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
license = "MIT"
|
|
9
9
|
dependencies = [
|
|
@@ -27,8 +27,9 @@ dependencies = [
|
|
|
27
27
|
[project.optional-dependencies]
|
|
28
28
|
# only required by LiteLLMEncoder; the provider-specific encoders use their own SDKs
|
|
29
29
|
litellm = [
|
|
30
|
-
#
|
|
31
|
-
|
|
30
|
+
# 1.84.0 fixes an authentication bypass via Host header injection
|
|
31
|
+
# (GHSA-4xpc-pv4p-pm3w); 1.83.7 was the previous floor for CVE-2026-42208
|
|
32
|
+
"litellm>=1.84.0",
|
|
32
33
|
]
|
|
33
34
|
ollama = ["ollama>=0.1.7"]
|
|
34
35
|
local = [
|
|
@@ -42,10 +43,11 @@ pinecone = ["pinecone[asyncio]>=7.0.0,<8.0.0"]
|
|
|
42
43
|
vision = [
|
|
43
44
|
"torchvision>=0.17.0 ; python_version < '3.13'",
|
|
44
45
|
"transformers>=4.36.2 ; python_version < '3.13'",
|
|
45
|
-
"pillow>=10.2.0
|
|
46
|
+
"pillow>=10.2.0 ; python_version < '3.13'",
|
|
46
47
|
"torch>=2.6.0 ; python_version < '3.13'"
|
|
47
48
|
]
|
|
48
49
|
qdrant = ["qdrant-client>=1.11.1,<2"]
|
|
50
|
+
redis = ["redis>=5.0.0,<7"]
|
|
49
51
|
google = ["google-cloud-aiplatform>=1.45.0,<2"]
|
|
50
52
|
bedrock = [
|
|
51
53
|
"boto3>=1.34.98,<2",
|
|
@@ -54,8 +56,24 @@ bedrock = [
|
|
|
54
56
|
postgres = [
|
|
55
57
|
"psycopg[binary]>=3.1.0,<4",
|
|
56
58
|
]
|
|
57
|
-
fastembed = ["fastembed>=0.
|
|
58
|
-
|
|
59
|
+
fastembed = ["fastembed>=0.7.0,<1"]
|
|
60
|
+
all = [
|
|
61
|
+
"semantic-router[local]",
|
|
62
|
+
"semantic-router[pinecone]",
|
|
63
|
+
"semantic-router[vision]",
|
|
64
|
+
"semantic-router[qdrant]",
|
|
65
|
+
"semantic-router[redis]",
|
|
66
|
+
"semantic-router[google]",
|
|
67
|
+
"semantic-router[bedrock]",
|
|
68
|
+
"semantic-router[postgres]",
|
|
69
|
+
"semantic-router[fastembed]",
|
|
70
|
+
"semantic-router[litellm]",
|
|
71
|
+
"semantic-router[ollama]",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
# Development tooling. `uv sync` and `uv run` install the `dev` group by default,
|
|
75
|
+
# so a fresh clone gets pytest, ruff and mypy without asking for any extra.
|
|
76
|
+
[dependency-groups]
|
|
59
77
|
dev = [
|
|
60
78
|
"ipykernel>=6.25.0,<7",
|
|
61
79
|
"ruff>=0.11.2,<0.12",
|
|
@@ -69,25 +87,9 @@ dev = [
|
|
|
69
87
|
"types-pyyaml>=6.0.12.12,<7",
|
|
70
88
|
"requests-mock>=1.12.1,<2",
|
|
71
89
|
"types-requests>=2.31.0,<3",
|
|
72
|
-
"dagger-io>=0.1.1 ; python_version >= '3.11'",
|
|
73
90
|
"python-dotenv>=1.0.0,<2",
|
|
74
|
-
# needed so `mypy semantic_router/` can resolve the LiteLLMEncoder import
|
|
75
|
-
"semantic-router[litellm]",
|
|
76
|
-
]
|
|
77
|
-
all = [
|
|
78
|
-
"semantic-router[local]",
|
|
79
|
-
"semantic-router[pinecone]",
|
|
80
|
-
"semantic-router[vision]",
|
|
81
|
-
"semantic-router[qdrant]",
|
|
82
|
-
"semantic-router[google]",
|
|
83
|
-
"semantic-router[bedrock]",
|
|
84
|
-
"semantic-router[postgres]",
|
|
85
|
-
"semantic-router[fastembed]",
|
|
86
|
-
"semantic-router[litellm]",
|
|
87
|
-
"semantic-router[dev]",
|
|
88
|
-
"semantic-router[ollama]",
|
|
89
|
-
"pytest-timeout",
|
|
90
91
|
]
|
|
92
|
+
docs = ["pydoc-markdown>=4.8.2 ; python_version < '3.12'"]
|
|
91
93
|
|
|
92
94
|
[tool.hatch.build.targets.sdist]
|
|
93
95
|
include = ["semantic_router"]
|
|
@@ -105,6 +107,9 @@ build-backend = "hatchling.build"
|
|
|
105
107
|
[tool.ruff]
|
|
106
108
|
line-length = 88
|
|
107
109
|
|
|
110
|
+
[tool.ruff.format]
|
|
111
|
+
exclude = ["*.ipynb"]
|
|
112
|
+
|
|
108
113
|
[tool.ruff.lint]
|
|
109
114
|
select = ["E", "F", "I", "T201", "NPY201"]
|
|
110
115
|
ignore = ["E501"]
|
|
@@ -132,3 +137,10 @@ torchvision = [
|
|
|
132
137
|
{ index = "pytorch-nightly", marker = "python_version >= '3.13' and sys_platform == 'darwin'" },
|
|
133
138
|
{ index = "pytorch-cpu", marker = "python_version < '3.13' or sys_platform != 'darwin'" },
|
|
134
139
|
]
|
|
140
|
+
|
|
141
|
+
[tool.pytest.ini_options]
|
|
142
|
+
testpaths = ["tests"]
|
|
143
|
+
addopts = "--strict-markers"
|
|
144
|
+
markers = [
|
|
145
|
+
"live: calls a paid third-party API (OpenAI, Cohere, ...). Needs real keys; excluded from the default `make test` run.",
|
|
146
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from semantic_router.route import Route
|
|
2
|
+
from semantic_router.routers import HybridRouter, RouterConfig, SemanticRouter
|
|
3
|
+
|
|
4
|
+
__all__ = ["SemanticRouter", "HybridRouter", "Route", "RouterConfig"]
|
|
5
|
+
|
|
6
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = version("semantic-router")
|
|
10
|
+
except PackageNotFoundError: # running from a checkout that was never installed
|
|
11
|
+
__version__ = "0.0.0"
|
|
@@ -26,12 +26,52 @@ class BM25Encoder(SparseEncoder, FittableMixin, AsymmetricSparseMixin):
|
|
|
26
26
|
- we train a BM25 encoder's normalization parameters on a sufficiently large corpus to capture target language distribution
|
|
27
27
|
- these trained parameter allow us to balance TF & IDF of query & documents for retrieval (read more on how BM25 fixes issues with TF-IDF)
|
|
28
28
|
|
|
29
|
+
The reference formula is section 4.1 of the ATIRE paper:
|
|
30
|
+
|
|
31
|
+
RSV_d = Σ_{t ∈ q} log(N / df_t) · (k1 + 1) · tf_td
|
|
32
|
+
/ (k1 · ((1 - b) + b · L_d / L_avg) + tf_td)
|
|
33
|
+
|
|
34
|
+
Deviations from the paper
|
|
35
|
+
-------------------------
|
|
36
|
+
This implementation is faithful to the formula above on the document side,
|
|
37
|
+
but deviates from it in three documented ways. Each is deliberate; see the
|
|
38
|
+
inline comments at the point of use for the details.
|
|
39
|
+
|
|
40
|
+
1. IDF smoothing. We compute `log((N + 1) / (df_t + 0.5))` rather than
|
|
41
|
+
`log(N / df_t)`, to avoid a division by zero for unseen terms. This
|
|
42
|
+
preserves the strictly-positive IDF that the ATIRE variant exists for.
|
|
43
|
+
|
|
44
|
+
2. Query weight normalization. The paper produces an unbounded relevance
|
|
45
|
+
score, which is fine for a search engine that only ranks within a single
|
|
46
|
+
query. A router instead compares scores against a fixed
|
|
47
|
+
`score_threshold`, so scores must be comparable *across* queries. We
|
|
48
|
+
therefore L1-normalize the query weights and scale them by
|
|
49
|
+
`1 / (k1 + 1)`, which bounds the query·document dot product to [0, 1] --
|
|
50
|
+
the same range as the cosine similarity returned by the dense encoders,
|
|
51
|
+
so the two can be blended in `HybridRouter` and thresholded alike.
|
|
52
|
+
|
|
53
|
+
This is ranking-neutral within a query (it is a single positive constant
|
|
54
|
+
per query) but it does discard cross-query IDF magnitude: two queries
|
|
55
|
+
with different total IDF mass can produce the same score.
|
|
56
|
+
|
|
57
|
+
3. Parameter defaults. We default to `k1=1.5, b=0.75` rather than the
|
|
58
|
+
paper's `k1=0.9, b=0.4`. See the `k1` and `b` parameter docs below.
|
|
59
|
+
|
|
29
60
|
ATIRE Paper: https://www.cs.otago.ac.nz/research/student-publications/atire-opensource.pdf
|
|
30
61
|
Pinecone Implementation: https://github.com/pinecone-io/pinecone-text/blob/8399f9ff28c4652766c35165c0db9b0eff309077/pinecone_text/sparse/bm25_encoder.py
|
|
31
62
|
|
|
32
|
-
:param k1: normalizer parameter that limits how much a single query term `q_i ∈ q` can affect score for document `D_n
|
|
63
|
+
:param k1: normalizer parameter that limits how much a single query term `q_i ∈ q` can affect score for document `D_n`.
|
|
64
|
+
Defaults to 1.5, the standard Robertson value, *not* the 0.9 reported in the ATIRE paper. The paper's value was
|
|
65
|
+
trained on the INEX 2008 Wikipedia collection, whose documents average 878 words (paper table 5), whereas this
|
|
66
|
+
encoder is fitted on route utterances averaging on the order of ten tokens. Transferring the paper's parameters
|
|
67
|
+
to utterance-length text is not obviously more correct, and changing the default would silently shift the scores
|
|
68
|
+
that existing `score_threshold` values were tuned against.
|
|
33
69
|
:type k1: float
|
|
34
|
-
:param b: normalizer parameter that balances the effect of a single document length compared to the average document
|
|
70
|
+
:param b: normalizer parameter that balances the effect of a single document length compared to the average document
|
|
71
|
+
length. Defaults to 0.75 rather than the paper's 0.4, for the same reason as `k1`. Note that `b` is considerably
|
|
72
|
+
more influential on short text: over utterances spanning 0.3x to 2.2x the average length, `b=0.75` spreads the
|
|
73
|
+
document factor by ~2.2x versus ~1.4x for `b=0.4`. Lower it towards the paper's value if length is over-weighted
|
|
74
|
+
for your route set.
|
|
35
75
|
:type b: float
|
|
36
76
|
:param corpus_size: number of documents in the trained corpus
|
|
37
77
|
:type corpus_size: int, optional
|
|
@@ -193,16 +233,55 @@ class BM25Encoder(SparseEncoder, FittableMixin, AsymmetricSparseMixin):
|
|
|
193
233
|
raise ValueError("No documents provided for encoding")
|
|
194
234
|
|
|
195
235
|
# Convert queries to token counts
|
|
196
|
-
queries_ids = self._tokenizer.tokenize(queries)
|
|
236
|
+
queries_ids = self._tokenizer.tokenize(queries, pad=True)
|
|
197
237
|
df = self._df(queries_ids) # (batch_size, vocab_size)
|
|
198
238
|
N = self.corpus_size
|
|
239
|
+
|
|
240
|
+
# DEVIATION FROM THE ATIRE PAPER: the paper specifies `log(N / df_t)`,
|
|
241
|
+
# whereas we compute `log((N + 1) / (df_t + 0.5))`. The `+ 0.5` avoids a
|
|
242
|
+
# division by zero and the `+ 1` keeps the numerator above the
|
|
243
|
+
# denominator, so the result is strictly positive. This preserves the
|
|
244
|
+
# property the ATIRE variant was introduced for (IDF is never negative,
|
|
245
|
+
# see footnote 1 of the paper) and stays monotonically decreasing in
|
|
246
|
+
# `df_t`, but it is a smoothed variant rather than the paper's formula.
|
|
247
|
+
# It weights common terms slightly less harshly than `log(N / df_t)`.
|
|
199
248
|
df = df + np.where(df > 0, 0.5, 0)
|
|
200
249
|
idf = np.divide(N + 1, df, out=np.zeros_like(df), where=df != 0)
|
|
201
250
|
idf = np.log(
|
|
202
251
|
idf, out=np.zeros_like(df), where=df != 0
|
|
203
252
|
) # (batch_size, vocab_size)
|
|
253
|
+
|
|
254
|
+
# The paper sums over the query terms `t ∈ q`, so a term repeated in the
|
|
255
|
+
# query contributes once per occurrence. Weighting IDF by the query term
|
|
256
|
+
# frequency is equivalent, and keeps the operation vectorised.
|
|
257
|
+
qtf = self._tf(queries_ids) # (batch_size, vocab_size)
|
|
258
|
+
weighted_idf = idf * qtf
|
|
259
|
+
|
|
260
|
+
# DEVIATION FROM THE ATIRE PAPER: the paper's RSV is an unbounded sum,
|
|
261
|
+
# which is fine when only ranking within one query. A router compares
|
|
262
|
+
# scores against a fixed `score_threshold`, so they must be comparable
|
|
263
|
+
# across queries too. Two steps make that so:
|
|
264
|
+
#
|
|
265
|
+
# 1. L1-normalize, so the weights sum to 1 regardless of how long the
|
|
266
|
+
# query is or how rare its terms are.
|
|
267
|
+
# 2. Divide by `k1 + 1`, so the weights sum to `1 / (k1 + 1)`.
|
|
268
|
+
#
|
|
269
|
+
# `encode_documents` returns values in (0, k1 + 1] (it keeps the paper's
|
|
270
|
+
# `(k1 + 1)` numerator), so the dot product of the two is bounded to
|
|
271
|
+
# [0, 1] -- the same range as the cosine similarity produced by the
|
|
272
|
+
# dense encoders. That lets `HybridRouter` blend the two and lets one
|
|
273
|
+
# threshold apply to both. Without the `k1 + 1` divisor the sparse score
|
|
274
|
+
# would reach `k1 + 1` (2.5 at the default `k1`) and would dominate the
|
|
275
|
+
# dense component of the hybrid score.
|
|
276
|
+
#
|
|
277
|
+
# Both steps are a single positive constant per query, so neither
|
|
278
|
+
# changes the ranking of documents for that query.
|
|
279
|
+
denominator = weighted_idf.sum(axis=1)[:, np.newaxis] * (self.k1 + 1.0)
|
|
204
280
|
idf_norm = np.divide(
|
|
205
|
-
|
|
281
|
+
weighted_idf,
|
|
282
|
+
denominator,
|
|
283
|
+
out=np.zeros_like(weighted_idf),
|
|
284
|
+
where=weighted_idf != 0,
|
|
206
285
|
)
|
|
207
286
|
|
|
208
287
|
return self._array_to_sparse_embeddings(idf_norm)
|
|
@@ -215,7 +294,7 @@ class BM25Encoder(SparseEncoder, FittableMixin, AsymmetricSparseMixin):
|
|
|
215
294
|
r"""Returns document term frequency normed by itself & average trained corpus length
|
|
216
295
|
(This is the right-hand side of the BM25 equation, which gets matmul-ed with the query IDF component)
|
|
217
296
|
|
|
218
|
-
LaTeX: $\frac{f(d_i, D)}{f(d_i, D) + k_1 \times (1 - b + b \times \frac{|D|}{avgdl})}$
|
|
297
|
+
LaTeX: $\frac{(k_1 + 1) \times f(d_i, D)}{f(d_i, D) + k_1 \times (1 - b + b \times \frac{|D|}{avgdl})}$
|
|
219
298
|
where:
|
|
220
299
|
f(d_i, D) is frequency of term `d_i ∈ D`
|
|
221
300
|
|D| is the document length
|
|
@@ -244,9 +323,9 @@ class BM25Encoder(SparseEncoder, FittableMixin, AsymmetricSparseMixin):
|
|
|
244
323
|
queries_ids = self._tokenizer.tokenize(documents, pad=True)
|
|
245
324
|
tf = self._tf(queries_ids) # (batch_size, vocab_size)
|
|
246
325
|
tf_sum = tf.sum(axis=1) # (batch_size, 1)
|
|
247
|
-
tf_normed = tf / (
|
|
326
|
+
tf_normed = ((self.k1 + 1.0) * tf) / (
|
|
248
327
|
self.k1
|
|
249
|
-
* (1.0 - self.b
|
|
328
|
+
* ((1.0 - self.b) + self.b * (tf_sum[:, np.newaxis] / self._avg_doc_len))
|
|
250
329
|
+ tf
|
|
251
330
|
) # (batch_size, vocab_size)
|
|
252
331
|
|
|
@@ -96,20 +96,22 @@ class CLIPEncoder(DenseEncoder):
|
|
|
96
96
|
:returns: A tuple of the tokenizer, processor, and model.
|
|
97
97
|
:rtype: Tuple[Any, Any, Any]
|
|
98
98
|
"""
|
|
99
|
+
# torch first: transformers >= 5 imports torch itself, so checking it
|
|
100
|
+
# second would report a missing torch as a missing transformers
|
|
99
101
|
try:
|
|
100
|
-
|
|
102
|
+
import torch
|
|
101
103
|
except ImportError:
|
|
102
104
|
raise ImportError(
|
|
103
|
-
"Please install
|
|
105
|
+
"Please install Pytorch to use CLIPEncoder. "
|
|
104
106
|
"You can install it with: "
|
|
105
107
|
"`pip install semantic-router[vision]`"
|
|
106
108
|
)
|
|
107
109
|
|
|
108
110
|
try:
|
|
109
|
-
import
|
|
111
|
+
from transformers import CLIPModel, CLIPProcessor, CLIPTokenizerFast
|
|
110
112
|
except ImportError:
|
|
111
113
|
raise ImportError(
|
|
112
|
-
"Please install
|
|
114
|
+
"Please install transformers to use CLIPEncoder. "
|
|
113
115
|
"You can install it with: "
|
|
114
116
|
"`pip install semantic-router[vision]`"
|
|
115
117
|
)
|
|
@@ -166,6 +168,9 @@ class CLIPEncoder(DenseEncoder):
|
|
|
166
168
|
).to(self.device)
|
|
167
169
|
with self._torch.no_grad():
|
|
168
170
|
embeds = self._model.get_text_features(**inputs)
|
|
171
|
+
# transformers >= 5 returns a model output whose pooler_output holds the
|
|
172
|
+
# projected features; earlier versions return that tensor directly
|
|
173
|
+
embeds = getattr(embeds, "pooler_output", embeds)
|
|
169
174
|
embeds = embeds.squeeze(0).cpu().detach().numpy()
|
|
170
175
|
return embeds
|
|
171
176
|
|
|
@@ -183,6 +188,7 @@ class CLIPEncoder(DenseEncoder):
|
|
|
183
188
|
].to(self.device)
|
|
184
189
|
with self._torch.no_grad():
|
|
185
190
|
embeds = self._model.get_image_features(pixel_values=inputs)
|
|
191
|
+
embeds = getattr(embeds, "pooler_output", embeds) # see _encode_text
|
|
186
192
|
embeds = embeds.squeeze(0).cpu().detach().numpy()
|
|
187
193
|
return embeds
|
|
188
194
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
from typing import Any, List, Optional
|
|
2
3
|
|
|
3
4
|
import numpy as np
|
|
@@ -71,3 +72,17 @@ class FastEmbedEncoder(DenseEncoder):
|
|
|
71
72
|
return embeddings
|
|
72
73
|
except Exception as e:
|
|
73
74
|
raise ValueError(f"FastEmbed embed failed. Error: {e}") from e
|
|
75
|
+
|
|
76
|
+
async def acall(self, docs: List[str]) -> List[List[float]]:
|
|
77
|
+
"""Embed a list of documents asynchronously. Supports text only.
|
|
78
|
+
|
|
79
|
+
FastEmbed itself is synchronous, so this offloads the embedding call to a
|
|
80
|
+
thread to avoid blocking the event loop.
|
|
81
|
+
|
|
82
|
+
:param docs: The documents to embed.
|
|
83
|
+
:type docs: List[str]
|
|
84
|
+
:raise ValueError: If the embedding fails.
|
|
85
|
+
:return: The vector embeddings of the documents.
|
|
86
|
+
:rtype: List[List[float]]
|
|
87
|
+
"""
|
|
88
|
+
return await asyncio.to_thread(lambda: self.__call__(docs))
|
|
@@ -118,6 +118,27 @@ class OpenAIEncoder(DenseEncoder):
|
|
|
118
118
|
# get token encoder
|
|
119
119
|
self._token_encoder = tiktoken.encoding_for_model(name)
|
|
120
120
|
|
|
121
|
+
def _validate_docs(self, docs: List[str]) -> None:
|
|
122
|
+
"""Raise a clear error for input the embeddings API rejects with a 400.
|
|
123
|
+
Checked before the call, since a 400 is an OpenAIError and gets retried."""
|
|
124
|
+
if isinstance(docs, str):
|
|
125
|
+
raise ValueError("OpenAI encoder input must be a list of strings, got str.")
|
|
126
|
+
if len(docs) == 0:
|
|
127
|
+
raise ValueError("OpenAI encoder input cannot be empty.")
|
|
128
|
+
|
|
129
|
+
for idx, doc in enumerate(docs):
|
|
130
|
+
if not isinstance(doc, str):
|
|
131
|
+
raise ValueError(
|
|
132
|
+
"OpenAI encoder input must contain only strings, got "
|
|
133
|
+
f"{type(doc).__name__} at index {idx}."
|
|
134
|
+
)
|
|
135
|
+
# the API accepts whitespace-only strings, so only "" is rejected, and
|
|
136
|
+
# docs are sent unchanged: stripping would change their embeddings
|
|
137
|
+
if doc == "":
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f"OpenAI encoder input contains an empty string at index {idx}."
|
|
140
|
+
)
|
|
141
|
+
|
|
121
142
|
def __call__(self, docs: List[str], truncate: bool = True) -> List[List[float]]:
|
|
122
143
|
"""Encode a list of text documents into embeddings using OpenAI API.
|
|
123
144
|
|
|
@@ -130,6 +151,8 @@ class OpenAIEncoder(DenseEncoder):
|
|
|
130
151
|
raise ValueError("OpenAI client is not initialized.")
|
|
131
152
|
embeds = None
|
|
132
153
|
|
|
154
|
+
self._validate_docs(docs)
|
|
155
|
+
|
|
133
156
|
if truncate:
|
|
134
157
|
# check if any document exceeds token limit and truncate if so
|
|
135
158
|
docs = [self._truncate(doc) for doc in docs]
|
|
@@ -202,6 +225,8 @@ class OpenAIEncoder(DenseEncoder):
|
|
|
202
225
|
raise ValueError("OpenAI async client is not initialized.")
|
|
203
226
|
embeds = None
|
|
204
227
|
|
|
228
|
+
self._validate_docs(docs)
|
|
229
|
+
|
|
205
230
|
if truncate:
|
|
206
231
|
# check if any document exceeds token limit and truncate if so
|
|
207
232
|
docs = [self._truncate(doc) for doc in docs]
|
|
@@ -41,21 +41,23 @@ class VitEncoder(DenseEncoder):
|
|
|
41
41
|
:return: The processor and model.
|
|
42
42
|
:rtype: tuple
|
|
43
43
|
"""
|
|
44
|
+
# torch first: transformers >= 5 imports torch itself, so checking it
|
|
45
|
+
# second would report a missing torch as a missing transformers
|
|
44
46
|
try:
|
|
45
|
-
|
|
47
|
+
import torch
|
|
48
|
+
import torchvision.transforms as T
|
|
46
49
|
except ImportError:
|
|
47
50
|
raise ImportError(
|
|
48
|
-
"Please install
|
|
51
|
+
"Please install Pytorch to use VitEncoder. "
|
|
49
52
|
"You can install it with: "
|
|
50
53
|
"`pip install semantic-router[vision]`"
|
|
51
54
|
)
|
|
52
55
|
|
|
53
56
|
try:
|
|
54
|
-
import
|
|
55
|
-
import torchvision.transforms as T
|
|
57
|
+
from transformers import ViTImageProcessor, ViTModel
|
|
56
58
|
except ImportError:
|
|
57
59
|
raise ImportError(
|
|
58
|
-
"Please install
|
|
60
|
+
"Please install transformers to use VitEncoder. "
|
|
59
61
|
"You can install it with: "
|
|
60
62
|
"`pip install semantic-router[vision]`"
|
|
61
63
|
)
|
|
@@ -4,6 +4,7 @@ from semantic_router.index.local import LocalIndex
|
|
|
4
4
|
from semantic_router.index.pinecone import PineconeIndex
|
|
5
5
|
from semantic_router.index.postgres import PostgresIndex
|
|
6
6
|
from semantic_router.index.qdrant import QdrantIndex
|
|
7
|
+
from semantic_router.index.redis import RedisIndex
|
|
7
8
|
|
|
8
9
|
__all__ = [
|
|
9
10
|
"BaseIndex",
|
|
@@ -12,4 +13,5 @@ __all__ = [
|
|
|
12
13
|
"QdrantIndex",
|
|
13
14
|
"PineconeIndex",
|
|
14
15
|
"PostgresIndex",
|
|
16
|
+
"RedisIndex",
|
|
15
17
|
]
|
|
@@ -43,6 +43,10 @@ class LocalIndex(BaseIndex):
|
|
|
43
43
|
:param metadata_list: List of metadata to add to the index.
|
|
44
44
|
:type metadata_list: List[Dict[str, Any]]
|
|
45
45
|
"""
|
|
46
|
+
if metadata_list and len(metadata_list) != len(embeddings):
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"metadata_list length ({len(metadata_list)}) must match embeddings length ({len(embeddings)})."
|
|
49
|
+
)
|
|
46
50
|
embeds = np.array(embeddings) # type: ignore
|
|
47
51
|
routes_arr = np.array(routes)
|
|
48
52
|
if isinstance(utterances[0], str):
|
|
@@ -191,7 +195,7 @@ class LocalIndex(BaseIndex):
|
|
|
191
195
|
filtered_index.append(vec)
|
|
192
196
|
filtered_routes.append(route)
|
|
193
197
|
if not filtered_routes:
|
|
194
|
-
|
|
198
|
+
return np.array([]), []
|
|
195
199
|
sim = similarity_matrix(vector, np.array(filtered_index))
|
|
196
200
|
scores, idx = top_scores(sim, top_k)
|
|
197
201
|
route_names = [filtered_routes[i] for i in idx]
|
|
@@ -231,7 +235,7 @@ class LocalIndex(BaseIndex):
|
|
|
231
235
|
filtered_index.append(vec)
|
|
232
236
|
filtered_routes.append(route)
|
|
233
237
|
if not filtered_routes:
|
|
234
|
-
|
|
238
|
+
return np.array([]), []
|
|
235
239
|
sim = similarity_matrix(vector, np.array(filtered_index))
|
|
236
240
|
scores, idx = top_scores(sim, top_k)
|
|
237
241
|
route_names = [filtered_routes[i] for i in idx]
|