synaptoroute 0.1.0__py2.py3-none-any.whl

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.
@@ -0,0 +1,13 @@
1
+ """
2
+ SynaptoRoute
3
+ A high-throughput, local semantic routing engine.
4
+ """
5
+
6
+ __version__ = "0.1.0"
7
+
8
+ from synaptoroute.router import AdaptiveRouter
9
+ from synaptoroute.encoder import Encoder
10
+ from synaptoroute.storage import BaseStorage, SQLiteStorage
11
+ from synaptoroute.models import Route
12
+
13
+ __all__ = ["AdaptiveRouter", "Encoder", "BaseStorage", "SQLiteStorage", "Route", "__version__"]
@@ -0,0 +1,25 @@
1
+ import numpy as np
2
+ import numpy.typing as npt
3
+ from fastembed import TextEmbedding
4
+ from typing import List
5
+
6
+ class Encoder:
7
+ """
8
+ Handles local intent embeddings using fastembed ONNX models.
9
+ """
10
+ def __init__(self, model_name: str = "BAAI/bge-small-en-v1.5", providers: List[str] = None):
11
+ if providers is None:
12
+ providers = ["CPUExecutionProvider"]
13
+ self.model = TextEmbedding(model_name=model_name, providers=providers)
14
+
15
+ def encode(self, text: str) -> npt.NDArray[np.float32]:
16
+ embeddings = list(self.model.embed([text]))
17
+ return embeddings[0]
18
+
19
+ def encode_batch(self, texts: List[str]) -> npt.NDArray[np.float32]:
20
+ if not texts:
21
+ # Return empty array with correct shape for BGE models (384)
22
+ # If using another model, shape mismatches will be caught downstream
23
+ return np.empty((0, 384), dtype=np.float32)
24
+ embeddings = list(self.model.embed(texts))
25
+ return np.array(embeddings)
@@ -0,0 +1,15 @@
1
+ class SynaptoRouteError(Exception):
2
+ """Base exception for SynaptoRoute."""
3
+ pass
4
+
5
+ class RouteNotFoundError(SynaptoRouteError):
6
+ """Raised when a specified route cannot be found."""
7
+ pass
8
+
9
+ class ModelLoadError(SynaptoRouteError):
10
+ """Raised when an embedding model fails to load."""
11
+ pass
12
+
13
+ class RouterOverloadedError(SynaptoRouteError):
14
+ """Raised when the dynamic batching queue is full (DDoS protection)."""
15
+ pass
synaptoroute/models.py ADDED
@@ -0,0 +1,38 @@
1
+ import json
2
+ from typing import List, Optional, Dict, Any
3
+ from pydantic import BaseModel, Field, field_validator, StringConstraints
4
+ from typing_extensions import Annotated
5
+
6
+ NonEmptyString = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
7
+
8
+ class Route(BaseModel):
9
+ """
10
+ Represents a single semantic route or intent.
11
+ """
12
+ name: str = Field(..., min_length=1, pattern=r"^[a-zA-Z0-9_-]+$")
13
+ utterances: List[NonEmptyString] = Field(..., min_length=1)
14
+ threshold: float = Field(0.0, ge=-1.0, le=1.0)
15
+ metadata: Optional[Dict[str, Any]] = None
16
+
17
+ @field_validator('metadata')
18
+ @classmethod
19
+ def validate_metadata_serializable(cls, v: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
20
+ if v is not None:
21
+ try:
22
+ json.dumps(v)
23
+ except (TypeError, ValueError) as e:
24
+ raise ValueError(f"Metadata must be JSON serializable: {e}")
25
+ return v
26
+
27
+ @field_validator('utterances')
28
+ @classmethod
29
+ def deduplicate_utterances(cls, v: List[str]) -> List[str]:
30
+ seen = set()
31
+ deduped = []
32
+ for utt in v:
33
+ if utt not in seen:
34
+ seen.add(utt)
35
+ deduped.append(utt)
36
+ if not deduped:
37
+ raise ValueError("Route must have at least one valid utterance.")
38
+ return deduped
synaptoroute/router.py ADDED
@@ -0,0 +1,271 @@
1
+ import threading
2
+ import numpy as np
3
+ from sklearn.metrics.pairwise import cosine_similarity
4
+ from sklearn.metrics import f1_score
5
+ from typing import Optional
6
+ import asyncio
7
+
8
+ from synaptoroute.models import Route
9
+ from synaptoroute.encoder import Encoder
10
+ from synaptoroute.storage import BaseStorage
11
+ from synaptoroute.exceptions import RouteNotFoundError, RouterOverloadedError
12
+
13
+ class AdaptiveRouter:
14
+ def __init__(self, encoder: Encoder, storage: BaseStorage):
15
+ self.encoder = encoder
16
+ self.storage = storage
17
+ self.lock = threading.Lock()
18
+
19
+ self._vectors = None
20
+ self._uncompiled_vectors = []
21
+ self._meta = []
22
+ self._route_map = {}
23
+
24
+ self._batch_queue = None
25
+ self._worker_task = None
26
+ self.batch_size = 32
27
+ self.batch_timeout = 0.005
28
+
29
+ self._load_routes()
30
+
31
+ async def start(self):
32
+ self._batch_queue = asyncio.Queue(maxsize=10000)
33
+ self._worker_task = asyncio.create_task(self._batch_worker())
34
+
35
+ async def stop(self):
36
+ if self._worker_task:
37
+ self._worker_task.cancel()
38
+ try:
39
+ await self._worker_task
40
+ except asyncio.CancelledError:
41
+ pass
42
+
43
+ def _load_routes(self):
44
+ routes = self.storage.load_all_routes()
45
+
46
+ for route in routes:
47
+ self._route_map[route.name] = route
48
+ if route.utterances:
49
+ embeddings = self.encoder.encode_batch(route.utterances)
50
+ self._uncompiled_vectors.append(embeddings)
51
+ self._meta.extend([route] * len(route.utterances))
52
+
53
+ def _rebuild_memory_locked(self):
54
+ self._vectors = None
55
+ self._uncompiled_vectors = []
56
+ self._meta = []
57
+ self._route_map = {}
58
+ self._load_routes()
59
+
60
+ def add_route(self, route: Route):
61
+ with self.lock:
62
+ is_overwrite = route.name in self._route_map
63
+ self.storage.save_route(route)
64
+
65
+ if is_overwrite:
66
+ # O(1) Memory Replacement: Filter out the old route's vectors without full DB re-encoding
67
+ self._compile_vectors_locked()
68
+ if self._vectors is not None:
69
+ mask = [r.name != route.name for r in self._meta]
70
+ if len(mask) > 0:
71
+ self._vectors = self._vectors[mask]
72
+ self._meta = [r for r, keep in zip(self._meta, mask) if keep]
73
+
74
+ self._route_map[route.name] = route
75
+ if route.utterances:
76
+ embeddings = self.encoder.encode_batch(route.utterances)
77
+ self._uncompiled_vectors.append(embeddings)
78
+ self._meta.extend([route] * len(route.utterances))
79
+
80
+ def add_utterance(self, route_name: str, utterance: str):
81
+ with self.lock:
82
+ if route_name not in self._route_map:
83
+ raise RouteNotFoundError(f"Route '{route_name}' not found.")
84
+
85
+ # Reshape to 2D to ensure safe vstack compatibility
86
+ embedding = self.encoder.encode(utterance).reshape(1, -1)
87
+
88
+ with self.lock:
89
+ self.storage.add_utterance(route_name, utterance)
90
+ self._uncompiled_vectors.append(embedding)
91
+ route = self._route_map[route_name]
92
+ route.utterances.append(utterance)
93
+ self._meta.append(route)
94
+
95
+ def _compile_vectors_locked(self):
96
+ if self._uncompiled_vectors:
97
+ if self._vectors is not None:
98
+ self._vectors = np.vstack([self._vectors] + self._uncompiled_vectors)
99
+ else:
100
+ self._vectors = np.vstack(self._uncompiled_vectors)
101
+ self._uncompiled_vectors = []
102
+
103
+ def __call__(self, query: str) -> Optional[Route]:
104
+ query_embedding = self.encoder.encode(query)
105
+
106
+ with self.lock:
107
+ self._compile_vectors_locked()
108
+
109
+ if self._vectors is None or len(self._vectors) == 0:
110
+ return None
111
+
112
+ similarities = cosine_similarity([query_embedding], self._vectors)[0]
113
+
114
+ best_route = None
115
+ best_score = -1.0
116
+
117
+ for score, route in zip(similarities, self._meta):
118
+ if score >= route.threshold and score > best_score:
119
+ best_score = score
120
+ best_route = route
121
+
122
+ return best_route
123
+
124
+ async def aquery(self, query: str) -> Optional[Route]:
125
+ if self._batch_queue is None:
126
+ raise RuntimeError("Router must be started with `await router.start()` before calling aquery.")
127
+
128
+ loop = asyncio.get_running_loop()
129
+ future = loop.create_future()
130
+ try:
131
+ self._batch_queue.put_nowait((query, future))
132
+ except asyncio.QueueFull:
133
+ raise RouterOverloadedError("Router queue is full (max 10000). Shedding load.")
134
+ return await future
135
+
136
+ async def _batch_worker(self):
137
+ try:
138
+ while True:
139
+ batch = []
140
+ try:
141
+ item = await self._batch_queue.get()
142
+ batch.append(item)
143
+
144
+ while len(batch) < self.batch_size:
145
+ try:
146
+ item = await asyncio.wait_for(self._batch_queue.get(), timeout=self.batch_timeout)
147
+ batch.append(item)
148
+ except asyncio.TimeoutError:
149
+ break
150
+ except asyncio.CancelledError:
151
+ break
152
+ except Exception:
153
+ continue
154
+
155
+ if not batch:
156
+ continue
157
+
158
+ queries = [q for q, _ in batch]
159
+ futures = [f for _, f in batch]
160
+
161
+ try:
162
+ def process_batch(qs):
163
+ query_embeddings = self.encoder.encode_batch(qs)
164
+ with self.lock:
165
+ self._compile_vectors_locked()
166
+ if self._vectors is None or len(self._vectors) == 0:
167
+ return [None] * len(qs)
168
+ similarities = cosine_similarity(query_embeddings, self._vectors)
169
+ results = []
170
+ for i in range(len(qs)):
171
+ best_route = None
172
+ best_score = -1.0
173
+ for score, route in zip(similarities[i], self._meta):
174
+ if score >= route.threshold and score > best_score:
175
+ best_score = score
176
+ best_route = route
177
+ results.append(best_route)
178
+ return results
179
+
180
+ results = await asyncio.to_thread(process_batch, queries)
181
+
182
+ for future, result in zip(futures, results):
183
+ if not future.done():
184
+ future.set_result(result)
185
+ except asyncio.CancelledError:
186
+ break
187
+ except Exception as e:
188
+ for future in futures:
189
+ if not future.done():
190
+ future.set_exception(e)
191
+ finally:
192
+ # Prevent async deadlocks by marking tasks done
193
+ for _ in batch:
194
+ self._batch_queue.task_done()
195
+ finally:
196
+ while not self._batch_queue.empty():
197
+ try:
198
+ _, future = self._batch_queue.get_nowait()
199
+ if not future.done():
200
+ future.set_exception(asyncio.CancelledError("Router worker shutting down."))
201
+ except asyncio.QueueEmpty:
202
+ break
203
+
204
+ def fit_thresholds(self, samples: list[str], labels: list[str]):
205
+ if not samples:
206
+ return
207
+ if len(samples) != len(labels):
208
+ raise ValueError("samples and labels lists must have the exact same length.")
209
+
210
+ query_embeddings = self.encoder.encode_batch(samples)
211
+
212
+ with self.lock:
213
+ self._compile_vectors_locked()
214
+
215
+ if self._vectors is None or len(self._vectors) == 0:
216
+ return
217
+
218
+ if not self._route_map:
219
+ return
220
+
221
+ vectors_snapshot = self._vectors
222
+ meta_snapshot = list(self._meta)
223
+ route_map_snapshot = dict(self._route_map)
224
+
225
+ similarities = cosine_similarity(query_embeddings, vectors_snapshot)
226
+
227
+ best_routes = []
228
+ best_scores = []
229
+
230
+ for i in range(len(samples)):
231
+ best_idx = np.argmax(similarities[i])
232
+ best_score = similarities[i][best_idx]
233
+ best_route = meta_snapshot[best_idx].name
234
+ best_routes.append(best_route)
235
+ best_scores.append(best_score)
236
+
237
+ best_routes = np.array(best_routes)
238
+ best_scores = np.array(best_scores)
239
+ labels_arr = np.array(labels)
240
+
241
+ # Test full cosine similarity range
242
+ thresholds_to_test = np.arange(-1.0, 1.05, 0.05)
243
+ new_thresholds = {}
244
+
245
+ for route_name, route in route_map_snapshot.items():
246
+ best_f1 = -1.0
247
+ best_t = route.threshold
248
+
249
+ y_true = (labels_arr == route_name).astype(int)
250
+
251
+ for t in thresholds_to_test:
252
+ y_pred = ((best_routes == route_name) & (best_scores > t)).astype(int)
253
+ f1 = f1_score(y_true, y_pred, zero_division=0)
254
+
255
+ if f1 >= best_f1:
256
+ best_f1 = f1
257
+ best_t = t
258
+ new_thresholds[route_name] = float(best_t)
259
+
260
+ with self.lock:
261
+ for route_name, t in new_thresholds.items():
262
+ if route_name in self._route_map:
263
+ route = self._route_map[route_name]
264
+ old_t = route.threshold
265
+ try:
266
+ route.threshold = t
267
+ self.storage.save_route(route)
268
+ except Exception as e:
269
+ route.threshold = old_t
270
+ raise e
271
+
@@ -0,0 +1,139 @@
1
+ import sqlite3
2
+ import json
3
+ import os
4
+ from abc import ABC, abstractmethod
5
+ from typing import List
6
+
7
+ from synaptoroute.models import Route
8
+
9
+ class BaseStorage(ABC):
10
+ @abstractmethod
11
+ def save_route(self, route: Route):
12
+ pass
13
+
14
+ @abstractmethod
15
+ def add_utterance(self, route_name: str, utterance: str):
16
+ pass
17
+
18
+ @abstractmethod
19
+ def load_all_routes(self) -> List[Route]:
20
+ pass
21
+
22
+ class SQLiteStorage(BaseStorage):
23
+ def __init__(self, db_path: str):
24
+ self.db_path = db_path
25
+ dirname = os.path.dirname(self.db_path)
26
+ if dirname:
27
+ os.makedirs(dirname, exist_ok=True)
28
+ # timeout=10.0 handles 'database is locked' errors automatically with backoff
29
+ self.conn = sqlite3.connect(self.db_path, check_same_thread=False, timeout=10.0)
30
+ self.conn.execute('PRAGMA journal_mode=WAL;')
31
+ self.conn.execute('PRAGMA foreign_keys = ON')
32
+ self._init_db()
33
+
34
+ def close(self):
35
+ if self.conn:
36
+ self.conn.close()
37
+ self.conn = None
38
+
39
+ def __del__(self):
40
+ self.close()
41
+
42
+ def __enter__(self):
43
+ return self
44
+
45
+ def __exit__(self, exc_type, exc_val, exc_tb):
46
+ self.close()
47
+
48
+ def _get_connection(self):
49
+ return self.conn
50
+
51
+ def _init_db(self):
52
+ try:
53
+ with self._get_connection() as conn:
54
+ cursor = conn.cursor()
55
+ cursor.execute('''
56
+ CREATE TABLE IF NOT EXISTS routes (
57
+ name TEXT PRIMARY KEY,
58
+ threshold REAL,
59
+ metadata TEXT
60
+ )
61
+ ''')
62
+ cursor.execute('''
63
+ CREATE TABLE IF NOT EXISTS utterances (
64
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
65
+ route_name TEXT,
66
+ utterance TEXT,
67
+ FOREIGN KEY(route_name) REFERENCES routes(name) ON DELETE CASCADE,
68
+ UNIQUE(route_name, utterance)
69
+ )
70
+ ''')
71
+ conn.commit()
72
+ except (sqlite3.OperationalError, sqlite3.IntegrityError, sqlite3.DatabaseError) as e:
73
+ raise RuntimeError(f"Failed to initialize database: {e}") from e
74
+
75
+ def save_route(self, route: Route):
76
+ try:
77
+ with self._get_connection() as conn:
78
+ cursor = conn.cursor()
79
+ metadata_str = json.dumps(route.metadata) if route.metadata is not None else None
80
+
81
+ # Insert or replace route
82
+ cursor.execute('''
83
+ INSERT OR REPLACE INTO routes (name, threshold, metadata)
84
+ VALUES (?, ?, ?)
85
+ ''', (route.name, route.threshold, metadata_str))
86
+
87
+ # Delete existing utterances for this route to avoid duplicates on replace
88
+ cursor.execute('''
89
+ DELETE FROM utterances WHERE route_name = ?
90
+ ''', (route.name,))
91
+
92
+ # Insert utterances
93
+ if route.utterances:
94
+ cursor.executemany('''
95
+ INSERT OR IGNORE INTO utterances (route_name, utterance)
96
+ VALUES (?, ?)
97
+ ''', [(route.name, u) for u in route.utterances])
98
+
99
+ conn.commit()
100
+ except (sqlite3.OperationalError, sqlite3.IntegrityError, sqlite3.DatabaseError) as e:
101
+ raise RuntimeError(f"Failed to save route: {e}") from e
102
+
103
+ def add_utterance(self, route_name: str, utterance: str):
104
+ try:
105
+ with self._get_connection() as conn:
106
+ cursor = conn.cursor()
107
+ cursor.execute('''
108
+ INSERT OR IGNORE INTO utterances (route_name, utterance)
109
+ VALUES (?, ?)
110
+ ''', (route_name, utterance))
111
+ conn.commit()
112
+ except (sqlite3.OperationalError, sqlite3.IntegrityError, sqlite3.DatabaseError) as e:
113
+ raise RuntimeError(f"Failed to add utterance: {e}") from e
114
+
115
+ def load_all_routes(self) -> List[Route]:
116
+ routes = []
117
+ try:
118
+ with self._get_connection() as conn:
119
+ cursor = conn.cursor()
120
+ cursor.execute('SELECT name, threshold, metadata FROM routes')
121
+ route_rows = cursor.fetchall()
122
+
123
+ for row in route_rows:
124
+ name, threshold, metadata_str = row
125
+ metadata = json.loads(metadata_str) if metadata_str else None
126
+
127
+ cursor.execute('SELECT utterance FROM utterances WHERE route_name = ?', (name,))
128
+ utterance_rows = cursor.fetchall()
129
+ utterances = [u[0] for u in utterance_rows]
130
+
131
+ routes.append(Route(
132
+ name=name,
133
+ threshold=threshold,
134
+ metadata=metadata,
135
+ utterances=utterances
136
+ ))
137
+ except (sqlite3.OperationalError, sqlite3.IntegrityError, sqlite3.DatabaseError) as e:
138
+ raise RuntimeError(f"Failed to load routes: {e}") from e
139
+ return routes
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: synaptoroute
3
+ Version: 0.1.0
4
+ Summary: A dynamic zero-token semantic router
5
+ Project-URL: Repository, https://github.com/sitanshukr08/SynaptoRoute
6
+ Project-URL: Issues, https://github.com/sitanshukr08/SynaptoRoute/issues
7
+ Author-email: Sitanshu <contact@example.com>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
14
+ Requires-Dist: fastembed>=0.8.0
15
+ Requires-Dist: numpy>=1.24.0
16
+ Requires-Dist: pydantic>=2.10.0
17
+ Requires-Dist: scikit-learn>=1.3.0
18
+ Provides-Extra: api
19
+ Requires-Dist: fastapi>=0.100.0; extra == 'api'
20
+ Requires-Dist: uvicorn>=0.22.0; extra == 'api'
21
+ Provides-Extra: test
22
+ Requires-Dist: pytest>=7.0.0; extra == 'test'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # SynaptoRoute
26
+
27
+ [![PyPI version](https://badge.fury.io/py/synaptoroute.svg)](https://pypi.org/project/synaptoroute/)
28
+ [![CI/CD Pipeline](https://github.com/sitanshukr08/SynaptoRoute/actions/workflows/ci.yml/badge.svg)](https://github.com/sitanshukr08/SynaptoRoute/actions)
29
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
30
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
31
+ [![FastAPI](https://img.shields.io/badge/FastAPI-0.100+-green.svg)](https://fastapi.tiangolo.com)
32
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/sitanshukr08/SynaptoRoute/blob/main/CONTRIBUTING.md)
33
+
34
+ SynaptoRoute is a high-throughput, local semantic routing engine built for production Python microservices. Designed as a mathematically optimal alternative to Large Language Model (LLM) routing chains and slower local routers, it provides zero-token intent classification in under 3 milliseconds on standard cloud hardware.
35
+
36
+ ## Table of Contents
37
+ - [Why SynaptoRoute?](#why-synaptoroute)
38
+ - [Architecture & Optimizations](#architecture--optimizations)
39
+ - [Performance Benchmarks](#performance-benchmarks)
40
+ - [Installation & Deployment](#installation--deployment)
41
+ - [Quick Start Guide](#quick-start-guide)
42
+ - [System Limitations](#system-limitations)
43
+ - [Community & Contributing](#community--contributing)
44
+
45
+ ---
46
+
47
+ ## Why SynaptoRoute?
48
+
49
+ In modern agentic systems, relying on an external API (like OpenAI or Anthropic) to make simple routing decisions—such as determining if a user wants to reset their password or check their balance—introduces unacceptable latency (300ms+) and high token costs.
50
+
51
+ SynaptoRoute solves this by executing intent classification entirely locally using INT8 quantized vector embeddings.
52
+
53
+ SynaptoRoute was engineered specifically to solve the $O(N)$ memory degradation problem during live hot-reloading and to maximize hardware utilization via asynchronous dynamic batching.
54
+
55
+ ## Architecture & Optimizations
56
+
57
+ ### 1. Lazy Memory Compilation
58
+ Traditional routers suffer from severe performance degradation during live updates. When a new route is added, they execute an immediate `numpy.vstack`, copying the entire vector array in memory ($O(N)$ complexity). SynaptoRoute defers this reallocation, appending new vectors to a lightweight list ($O(1)$) and only executing the heavy compilation precisely when the next query arrives, preventing server freezes.
59
+
60
+ ### 2. Dynamic Asynchronous Batching
61
+ Hardware accelerators (GPUs, AVX512 CPUs) are optimized for large matrix multiplications. Sending single queries sequentially incurs massive transfer overhead. SynaptoRoute utilizes a background `asyncio.Queue` worker that traps parallel HTTP requests, waits 5 milliseconds, groups them into a batch, and processes them in a single hardware cycle.
62
+
63
+ ### 3. INT8 Quantization
64
+ By default, SynaptoRoute leverages the `BAAI/bge-small-en-v1.5` model quantized to 8-bit integers via the ONNX runtime, slashing memory bandwidth requirements by 4x and maximizing CPU cache utilization.
65
+
66
+ ---
67
+
68
+ ## Performance Benchmarks
69
+
70
+ The following metrics were captured via automated GitHub Actions CI/CD running on a standard, unaccelerated `ubuntu-latest` 2-core cloud CPU.
71
+
72
+ | Metric | Cloud CPU Latency | Context |
73
+ | :--- | :--- | :--- |
74
+ | **Inference P99** | 3.94 ms | Single sequential query latency. |
75
+ | **Amortized P50** | 2.69 ms | Per-query latency when processing 1,000 concurrent requests via dynamic batching. |
76
+ | **Hot-Reload** | 5.04 ms | Time required to dynamically inject a new utterance into memory without dropping active API requests. |
77
+
78
+ > **📊 View Full Benchmarks:** For detailed analysis including Memory Leak Endurance, GPU Scaling, Classification F1-Scores, and Input Poisoning Survival Metrics, see our official [BENCHMARKS.md](BENCHMARKS.md).
79
+
80
+ ---
81
+
82
+ ## Installation & Deployment
83
+
84
+ ### Method 1: Docker REST API (Recommended)
85
+
86
+ SynaptoRoute ships with a fully asynchronous FastAPI wrapper, designed for immediate drop-in deployment as a scalable microservice.
87
+
88
+ ```bash
89
+ # Build the Docker image
90
+ docker build -t synaptoroute .
91
+
92
+ # Run the container
93
+ docker run -p 8000:8000 synaptoroute
94
+ ```
95
+
96
+ You can interface with the router immediately:
97
+ ```bash
98
+ curl -X POST http://localhost:8000/route \
99
+ -H "Content-Type: application/json" \
100
+ -d '{"query": "I need help resetting my password"}'
101
+ ```
102
+
103
+ ### Method 2: Standard Python Package
104
+
105
+ To embed SynaptoRoute natively into your existing Python pipelines, install directly from pip (or via git if testing the latest main branch):
106
+
107
+ ```bash
108
+ pip install synaptoroute
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Quick Start Guide
114
+
115
+ ```python
116
+ import asyncio
117
+ from synaptoroute.router import AdaptiveRouter
118
+ from synaptoroute.encoder import Encoder
119
+ from synaptoroute.storage import SQLiteStorage
120
+ from synaptoroute.models import Route
121
+
122
+ async def main():
123
+ # 1. Initialize Components
124
+ encoder = Encoder()
125
+ storage = SQLiteStorage("data/memory.sqlite")
126
+ router = AdaptiveRouter(encoder, storage)
127
+
128
+ # 2. Define Routes
129
+ billing_route = Route(
130
+ name="billing",
131
+ utterances=["I need a refund", "Where is my receipt?", "Cancel my subscription"]
132
+ )
133
+ router.add_route(billing_route)
134
+
135
+ # 3. Start the Background Batching Worker
136
+ await router.start()
137
+
138
+ # 4. Execute Async Queries
139
+ result = await router.aquery("How do I get my money back?")
140
+ print(f"Matched Intent: {result.name}") # Output: billing
141
+
142
+ # 5. Graceful Shutdown
143
+ await router.stop()
144
+
145
+ if __name__ == "__main__":
146
+ asyncio.run(main())
147
+ ```
148
+
149
+ ---
150
+
151
+ ## System Limitations
152
+
153
+ **Horizontal Scaling (Kubernetes Split-Brain)**
154
+ SynaptoRoute relies on a highly optimized, local in-memory NumPy matrix to achieve its microsecond latency. As such, it is structurally bound to a single node. If deployed across multiple load-balanced Kubernetes pods, a hot-reload request hitting Pod A will update Pod A's local memory, but Pod B will remain unaware. Scaling horizontally requires implementing an external event bus (e.g., Redis Pub/Sub) to broadcast memory invalidation events across the cluster.
155
+
156
+ ---
157
+
158
+ ## Community & Contributing
159
+
160
+ We welcome contributions of all sizes from the open-source community!
161
+
162
+ - **Contributing:** Please read our [Contributing Guidelines](CONTRIBUTING.md) to learn how to set up your development environment, run the test suite, and submit Pull Requests.
163
+ - **Code of Conduct:** We are committed to fostering a welcoming environment. Please review our [Code of Conduct](CODE_OF_CONDUCT.md).
164
+ - **Issues:** If you discover a bug or have a feature request, please [open an issue](https://github.com/sitanshukr08/SynaptoRoute/issues).
@@ -0,0 +1,10 @@
1
+ synaptoroute/__init__.py,sha256=98YLN8Z0eUBeCUJdUKg86vgeKzy8SsC8qkK8ueaZYU0,378
2
+ synaptoroute/encoder.py,sha256=vMce67SQTZOULezwNslNeCTn8yBvQqXSb9jJ3ohL4fs,1005
3
+ synaptoroute/exceptions.py,sha256=YsJBgWxydamLpPwlejBx9PiVIDHwX2Hdfy1HPcTQmkE,441
4
+ synaptoroute/models.py,sha256=MmnFKf4yL2MtBCWuDah0jZvlQ1YMcr7uZ8jvjtz-t9c,1355
5
+ synaptoroute/router.py,sha256=hg69-8UBLv4Rn1SW1MDhQerB1sRvOV0cWJFh7HbICHY,10485
6
+ synaptoroute/storage.py,sha256=R-0W2qEMokob67u1HJ7VokK6-hezZw6l9gHTfcZWStw,5289
7
+ synaptoroute-0.1.0.dist-info/METADATA,sha256=dgoDsFi7ado1X4FFQJex0vDS5Xkvi6O15IeRugssAAo,7612
8
+ synaptoroute-0.1.0.dist-info/WHEEL,sha256=e22IIVjxDyt0lABi4WpktFIGsmO_ebSDXLnPUbPK0E0,105
9
+ synaptoroute-0.1.0.dist-info/licenses/LICENSE,sha256=d26nZSImxopcsPU80VczjSvLRycVXMT4JbaqbHB8TGo,1065
10
+ synaptoroute-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sitanshu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.