semantic-router 0.1.0.dev3__tar.gz → 0.1.0.dev4__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.
Files changed (50) hide show
  1. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/PKG-INFO +1 -1
  2. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/pyproject.toml +1 -1
  3. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/__init__.py +1 -1
  4. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/encoders/bedrock.py +37 -11
  5. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/index/base.py +76 -9
  6. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/index/pinecone.py +20 -15
  7. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/routers/base.py +49 -18
  8. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/routers/hybrid.py +6 -6
  9. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/routers/semantic.py +0 -2
  10. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/schema.py +6 -5
  11. semantic_router-0.1.0.dev4/semantic_router/utils/__init__.py +0 -0
  12. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/LICENSE +0 -0
  13. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/README.md +0 -0
  14. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/encoders/__init__.py +0 -0
  15. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/encoders/aurelio.py +0 -0
  16. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/encoders/base.py +0 -0
  17. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/encoders/bm25.py +0 -0
  18. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/encoders/clip.py +0 -0
  19. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/encoders/cohere.py +0 -0
  20. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/encoders/fastembed.py +0 -0
  21. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/encoders/google.py +0 -0
  22. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/encoders/huggingface.py +0 -0
  23. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/encoders/mistral.py +0 -0
  24. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/encoders/openai.py +0 -0
  25. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/encoders/tfidf.py +0 -0
  26. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/encoders/vit.py +0 -0
  27. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/encoders/zure.py +0 -0
  28. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/index/__init__.py +0 -0
  29. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/index/hybrid_local.py +0 -0
  30. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/index/local.py +0 -0
  31. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/index/postgres.py +0 -0
  32. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/index/qdrant.py +0 -0
  33. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/linear.py +0 -0
  34. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/llms/__init__.py +0 -0
  35. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/llms/base.py +0 -0
  36. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/llms/cohere.py +0 -0
  37. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/llms/grammars/json.gbnf +0 -0
  38. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/llms/llamacpp.py +0 -0
  39. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/llms/mistral.py +0 -0
  40. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/llms/ollama.py +0 -0
  41. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/llms/openai.py +0 -0
  42. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/llms/openrouter.py +0 -0
  43. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/llms/zure.py +0 -0
  44. /semantic_router-0.1.0.dev3/semantic_router/utils/__init__.py → /semantic_router-0.1.0.dev4/semantic_router/py.typed +0 -0
  45. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/route.py +0 -0
  46. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/routers/__init__.py +0 -0
  47. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/utils/defaults.py +0 -0
  48. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/utils/function_call.py +0 -0
  49. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/utils/llm.py +0 -0
  50. {semantic_router-0.1.0.dev3 → semantic_router-0.1.0.dev4}/semantic_router/utils/logger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: semantic-router
3
- Version: 0.1.0.dev3
3
+ Version: 0.1.0.dev4
4
4
  Summary: Super fast semantic router for AI decision making
5
5
  License: MIT
6
6
  Author: Aurelio AI
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "semantic-router"
3
- version = "0.1.0.dev3"
3
+ version = "0.1.0.dev4"
4
4
  description = "Super fast semantic router for AI decision making"
5
5
  authors = ["Aurelio AI <hello@aurelio.ai>"]
6
6
  readme = "README.md"
@@ -3,4 +3,4 @@ from semantic_router.route import Route
3
3
 
4
4
  __all__ = ["SemanticRouter", "HybridRouter", "Route", "RouterConfig"]
5
5
 
6
- __version__ = "0.1.0.dev3"
6
+ __version__ = "0.1.0.dev4"
@@ -17,7 +17,7 @@ Classes:
17
17
  """
18
18
 
19
19
  import json
20
- from typing import List, Optional, Any
20
+ from typing import Dict, List, Optional, Any, Union
21
21
  import os
22
22
  from time import sleep
23
23
  import tiktoken
@@ -138,11 +138,14 @@ class BedrockEncoder(DenseEncoder):
138
138
  ) from err
139
139
  return bedrock_client
140
140
 
141
- def __call__(self, docs: List[str]) -> List[List[float]]:
141
+ def __call__(
142
+ self, docs: List[Union[str, Dict]], model_kwargs: Optional[Dict] = None
143
+ ) -> List[List[float]]:
142
144
  """Generates embeddings for the given documents.
143
145
 
144
146
  Args:
145
147
  docs: A list of strings representing the documents to embed.
148
+ model_kwargs: A dictionary of model-specific inference parameters.
146
149
 
147
150
  Returns:
148
151
  A list of lists, where each inner list contains the embedding values for a
@@ -168,13 +171,29 @@ class BedrockEncoder(DenseEncoder):
168
171
  embeddings = []
169
172
  if self.name and "amazon" in self.name:
170
173
  for doc in docs:
171
- embedding_body = json.dumps(
172
- {
173
- "inputText": doc,
174
- }
175
- )
174
+
175
+ embedding_body = {}
176
+
177
+ if isinstance(doc, dict):
178
+ embedding_body["inputText"] = doc.get("text")
179
+ embedding_body["inputImage"] = doc.get(
180
+ "image"
181
+ ) # expects a base64-encoded image
182
+ else:
183
+ embedding_body["inputText"] = doc
184
+
185
+ # Add model-specific inference parameters
186
+ if model_kwargs:
187
+ embedding_body = embedding_body | model_kwargs
188
+
189
+ # Clean up null values
190
+ embedding_body = {k: v for k, v in embedding_body.items() if v}
191
+
192
+ # Format payload
193
+ embedding_body_payload: str = json.dumps(embedding_body)
194
+
176
195
  response = self.client.invoke_model(
177
- body=embedding_body,
196
+ body=embedding_body_payload,
178
197
  modelId=self.name,
179
198
  accept="application/json",
180
199
  contentType="application/json",
@@ -184,9 +203,16 @@ class BedrockEncoder(DenseEncoder):
184
203
  elif self.name and "cohere" in self.name:
185
204
  chunked_docs = self.chunk_strings(docs)
186
205
  for chunk in chunked_docs:
187
- chunk = json.dumps(
188
- {"texts": chunk, "input_type": self.input_type}
189
- )
206
+ chunk = {"texts": chunk, "input_type": self.input_type}
207
+
208
+ # Add model-specific inference parameters
209
+ # Note: if specified, input_type will be overwritten by model_kwargs
210
+ if model_kwargs:
211
+ chunk = chunk | model_kwargs
212
+
213
+ # Format payload
214
+ chunk = json.dumps(chunk)
215
+
190
216
  response = self.client.invoke_model(
191
217
  body=chunk,
192
218
  modelId=self.name,
@@ -1,3 +1,5 @@
1
+ from datetime import datetime
2
+ import time
1
3
  from typing import Any, List, Optional, Tuple, Union, Dict
2
4
  import json
3
5
 
@@ -157,26 +159,91 @@ class BaseIndex(BaseModel):
157
159
  logger.warning("This method should be implemented by subclasses.")
158
160
  self.index = None
159
161
 
160
- def _read_hash(self) -> ConfigParameter:
161
- """
162
- Read the hash of the previously written index.
162
+ def _read_config(self, field: str, scope: str | None = None) -> ConfigParameter:
163
+ """Read a config parameter from the index.
163
164
 
164
- This method should be implemented by subclasses.
165
+ :param field: The field to read.
166
+ :type field: str
167
+ :param scope: The scope to read.
168
+ :type scope: str | None
169
+ :return: The config parameter that was read.
170
+ :rtype: ConfigParameter
165
171
  """
166
172
  logger.warning("This method should be implemented by subclasses.")
167
173
  return ConfigParameter(
168
- field="sr_hash",
174
+ field=field,
169
175
  value="",
170
- namespace="",
176
+ scope=scope,
171
177
  )
172
178
 
173
- def _write_config(self, config: ConfigParameter):
179
+ def _read_hash(self) -> ConfigParameter:
180
+ """Read the hash of the previously written index.
181
+
182
+ :return: The config parameter that was read.
183
+ :rtype: ConfigParameter
174
184
  """
175
- Write a config parameter to the index.
185
+ return self._read_config(field="sr_hash")
176
186
 
177
- This method should be implemented by subclasses.
187
+ def _write_config(self, config: ConfigParameter) -> ConfigParameter:
188
+ """Write a config parameter to the index.
189
+
190
+ :param config: The config parameter to write.
191
+ :type config: ConfigParameter
192
+ :return: The config parameter that was written.
193
+ :rtype: ConfigParameter
178
194
  """
179
195
  logger.warning("This method should be implemented by subclasses.")
196
+ return config
197
+
198
+ def lock(
199
+ self, value: bool, wait: int = 0, scope: str | None = None
200
+ ) -> ConfigParameter:
201
+ """Lock/unlock the index for a given scope (if applicable). If index
202
+ already locked/unlocked, raises ValueError.
203
+
204
+ :param scope: The scope to lock.
205
+ :type scope: str | None
206
+ :param wait: The number of seconds to wait for the index to be unlocked, if
207
+ set to 0, will raise an error if index is already locked/unlocked.
208
+ :type wait: int
209
+ :return: The config parameter that was locked.
210
+ :rtype: ConfigParameter
211
+ """
212
+ start_time = datetime.now()
213
+ while True:
214
+ if self._is_locked(scope=scope) != value:
215
+ # in this case, we can set the lock value
216
+ break
217
+ if (datetime.now() - start_time).total_seconds() < wait:
218
+ # wait for 2.5 seconds before checking again
219
+ time.sleep(2.5)
220
+ else:
221
+ raise ValueError(
222
+ f"Index is already {'locked' if value else 'unlocked'}."
223
+ )
224
+ lock_param = ConfigParameter(
225
+ field="sr_lock",
226
+ value=str(value),
227
+ scope=scope,
228
+ )
229
+ self._write_config(lock_param)
230
+ return lock_param
231
+
232
+ def _is_locked(self, scope: str | None = None) -> bool:
233
+ """Check if the index is locked for a given scope (if applicable).
234
+
235
+ :param scope: The scope to check.
236
+ :type scope: str | None
237
+ :return: True if the index is locked, False otherwise.
238
+ :rtype: bool
239
+ """
240
+ lock_config = self._read_config(field="sr_lock", scope=scope)
241
+ if lock_config.value == "True":
242
+ return True
243
+ elif lock_config.value == "False" or not lock_config.value:
244
+ return False
245
+ else:
246
+ raise ValueError(f"Invalid lock value: {lock_config.value}")
180
247
 
181
248
  def _get_all(self, prefix: Optional[str] = None, include_metadata: bool = False):
182
249
  """
@@ -405,39 +405,43 @@ class PineconeIndex(BaseIndex):
405
405
  route_names = [result["metadata"]["sr_route"] for result in results["matches"]]
406
406
  return np.array(scores), route_names
407
407
 
408
- def _read_hash(self) -> ConfigParameter:
408
+ def _read_config(self, field: str, scope: str | None = None) -> ConfigParameter:
409
+ scope = scope or self.namespace
409
410
  if self.index is None:
410
411
  return ConfigParameter(
411
- field="sr_hash",
412
+ field=field,
412
413
  value="",
413
- namespace=self.namespace,
414
+ scope=scope,
414
415
  )
415
- hash_id = f"sr_hash#{self.namespace}"
416
- hash_record = self.index.fetch(
417
- ids=[hash_id],
416
+ config_id = f"{field}#{scope}"
417
+ config_record = self.index.fetch(
418
+ ids=[config_id],
418
419
  namespace="sr_config",
419
420
  )
420
- if hash_record["vectors"]:
421
+ if config_record["vectors"]:
421
422
  return ConfigParameter(
422
- field="sr_hash",
423
- value=hash_record["vectors"][hash_id]["metadata"]["value"],
424
- created_at=hash_record["vectors"][hash_id]["metadata"]["created_at"],
425
- namespace=self.namespace,
423
+ field=field,
424
+ value=config_record["vectors"][config_id]["metadata"]["value"],
425
+ created_at=config_record["vectors"][config_id]["metadata"][
426
+ "created_at"
427
+ ],
428
+ scope=scope,
426
429
  )
427
430
  else:
428
- logger.warning("Configuration for hash parameter not found in index.")
431
+ logger.warning(f"Configuration for {field} parameter not found in index.")
429
432
  return ConfigParameter(
430
- field="sr_hash",
433
+ field=field,
431
434
  value="",
432
- namespace=self.namespace,
435
+ scope=scope,
433
436
  )
434
437
 
435
- def _write_config(self, config: ConfigParameter) -> None:
438
+ def _write_config(self, config: ConfigParameter) -> ConfigParameter:
436
439
  """Method to write a config parameter to the remote Pinecone index.
437
440
 
438
441
  :param config: The config parameter to write to the index.
439
442
  :type config: ConfigParameter
440
443
  """
444
+ config.scope = config.scope or self.namespace
441
445
  if self.index is None:
442
446
  raise ValueError("Index has not been initialized.")
443
447
  if self.dimensions is None:
@@ -446,6 +450,7 @@ class PineconeIndex(BaseIndex):
446
450
  vectors=[config.to_pinecone(dimensions=self.dimensions)],
447
451
  namespace="sr_config",
448
452
  )
453
+ return config
449
454
 
450
455
  async def aquery(
451
456
  self,
@@ -4,6 +4,7 @@ import os
4
4
  import random
5
5
  import hashlib
6
6
  from typing import Any, Callable, Dict, List, Optional, Tuple, Union
7
+ from typing_extensions import deprecated
7
8
  from pydantic import BaseModel, Field
8
9
 
9
10
  import numpy as np
@@ -280,6 +281,20 @@ class RouterConfig:
280
281
  )
281
282
 
282
283
 
284
+ def xq_reshape(xq: List[float] | np.ndarray) -> np.ndarray:
285
+ # convert to numpy array if not already
286
+ if not isinstance(xq, np.ndarray):
287
+ xq = np.array(xq)
288
+ # check if vector is 1D and expand to 2D if necessary
289
+ if len(xq.shape) == 1:
290
+ xq = np.expand_dims(xq, axis=0)
291
+ if xq.shape[0] != 1:
292
+ raise ValueError(
293
+ f"Expected (1, x) dimensional input for query, got {xq.shape}."
294
+ )
295
+ return xq
296
+
297
+
283
298
  class BaseRouter(BaseModel):
284
299
  encoder: DenseEncoder = Field(default_factory=OpenAIEncoder)
285
300
  index: BaseIndex = Field(default_factory=BaseIndex)
@@ -402,7 +417,7 @@ class BaseRouter(BaseModel):
402
417
  def __call__(
403
418
  self,
404
419
  text: Optional[str] = None,
405
- vector: Optional[List[float]] = None,
420
+ vector: Optional[List[float] | np.ndarray] = None,
406
421
  simulate_static: bool = False,
407
422
  route_filter: Optional[List[str]] = None,
408
423
  ) -> RouteChoice:
@@ -411,6 +426,9 @@ class BaseRouter(BaseModel):
411
426
  if text is None:
412
427
  raise ValueError("Either text or vector must be provided")
413
428
  vector = self._encode(text=[text])
429
+ # convert to numpy array if not already
430
+ vector = xq_reshape(vector)
431
+ # calculate semantics
414
432
  route, top_class_scores = self._retrieve_top_route(vector, route_filter)
415
433
  passed = self._check_threshold(top_class_scores, route)
416
434
  if passed and route is not None and not simulate_static:
@@ -444,7 +462,7 @@ class BaseRouter(BaseModel):
444
462
  async def acall(
445
463
  self,
446
464
  text: Optional[str] = None,
447
- vector: Optional[List[float]] = None,
465
+ vector: Optional[List[float] | np.ndarray] = None,
448
466
  simulate_static: bool = False,
449
467
  route_filter: Optional[List[str]] = None,
450
468
  ) -> RouteChoice:
@@ -453,7 +471,9 @@ class BaseRouter(BaseModel):
453
471
  if text is None:
454
472
  raise ValueError("Either text or vector must be provided")
455
473
  vector = await self._async_encode(text=[text])
456
-
474
+ # convert to numpy array if not already
475
+ vector = xq_reshape(vector)
476
+ # calculate semantics
457
477
  route, top_class_scores = await self._async_retrieve_top_route(
458
478
  vector, route_filter
459
479
  )
@@ -483,19 +503,21 @@ class BaseRouter(BaseModel):
483
503
  # if no route passes threshold, return empty route choice
484
504
  return RouteChoice()
485
505
 
506
+ # TODO: add multiple routes return to __call__ and acall
507
+ @deprecated("This method is deprecated. Use `__call__` instead.")
486
508
  def retrieve_multiple_routes(
487
509
  self,
488
510
  text: Optional[str] = None,
489
- vector: Optional[List[float]] = None,
511
+ vector: Optional[List[float] | np.ndarray] = None,
490
512
  ) -> List[RouteChoice]:
491
513
  if vector is None:
492
514
  if text is None:
493
515
  raise ValueError("Either text or vector must be provided")
494
- vector_arr = self._encode(text=[text])
495
- else:
496
- vector_arr = np.array(vector)
516
+ vector = self._encode(text=[text])
517
+ # convert to numpy array if not already
518
+ vector = xq_reshape(vector)
497
519
  # get relevant utterances
498
- results = self._retrieve(xq=vector_arr)
520
+ results = self._retrieve(xq=vector)
499
521
  # decide most relevant routes
500
522
  categories_with_scores = self._semantic_classify_multiple_routes(results)
501
523
  return [
@@ -514,16 +536,14 @@ class BaseRouter(BaseModel):
514
536
  # return route_choices
515
537
 
516
538
  def _retrieve_top_route(
517
- self, vector: List[float], route_filter: Optional[List[str]] = None
539
+ self, vector: np.ndarray, route_filter: Optional[List[str]] = None
518
540
  ) -> Tuple[Optional[Route], List[float]]:
519
541
  """
520
542
  Retrieve the top matching route based on the given vector.
521
543
  Returns a tuple of the route (if any) and the scores of the top class.
522
544
  """
523
545
  # get relevant results (scores and routes)
524
- results = self._retrieve(
525
- xq=np.array(vector), top_k=self.top_k, route_filter=route_filter
526
- )
546
+ results = self._retrieve(xq=vector, top_k=self.top_k, route_filter=route_filter)
527
547
  # decide most relevant routes
528
548
  top_class, top_class_scores = self._semantic_classify(results)
529
549
  # TODO do we need this check?
@@ -531,11 +551,11 @@ class BaseRouter(BaseModel):
531
551
  return route, top_class_scores
532
552
 
533
553
  async def _async_retrieve_top_route(
534
- self, vector: List[float], route_filter: Optional[List[str]] = None
554
+ self, vector: np.ndarray, route_filter: Optional[List[str]] = None
535
555
  ) -> Tuple[Optional[Route], List[float]]:
536
556
  # get relevant results (scores and routes)
537
557
  results = await self._async_retrieve(
538
- xq=np.array(vector), top_k=self.top_k, route_filter=route_filter
558
+ xq=vector, top_k=self.top_k, route_filter=route_filter
539
559
  )
540
560
  # decide most relevant routes
541
561
  top_class, top_class_scores = await self._async_semantic_classify(results)
@@ -543,7 +563,7 @@ class BaseRouter(BaseModel):
543
563
  route = self.check_for_matching_routes(top_class)
544
564
  return route, top_class_scores
545
565
 
546
- def sync(self, sync_mode: str, force: bool = False) -> List[str]:
566
+ def sync(self, sync_mode: str, force: bool = False, wait: int = 0) -> List[str]:
547
567
  """Runs a sync of the local routes with the remote index.
548
568
 
549
569
  :param sync_mode: The mode to sync the routes with the remote index.
@@ -551,6 +571,10 @@ class BaseRouter(BaseModel):
551
571
  :param force: Whether to force the sync even if the local and remote
552
572
  hashes already match. Defaults to False.
553
573
  :type force: bool, optional
574
+ :param wait: The number of seconds to wait for the index to be unlocked
575
+ before proceeding with the sync. If set to 0, will raise an error if
576
+ index is already locked/unlocked.
577
+ :type wait: int
554
578
  :return: A list of diffs describing the addressed differences between
555
579
  the local and remote route layers.
556
580
  :rtype: List[str]
@@ -565,7 +589,9 @@ class BaseRouter(BaseModel):
565
589
  remote_utterances=local_utterances,
566
590
  )
567
591
  return diff.to_utterance_str()
568
- # otherwise we continue with the sync, first creating a diff
592
+ # otherwise we continue with the sync, first locking the index
593
+ _ = self.index.lock(value=True, wait=wait)
594
+ # first creating a diff
569
595
  local_utterances = self.to_config().to_utterances()
570
596
  remote_utterances = self.index.get_utterances()
571
597
  diff = UtteranceDiff.from_utterances(
@@ -576,6 +602,8 @@ class BaseRouter(BaseModel):
576
602
  sync_strategy = diff.get_sync_strategy(sync_mode=sync_mode)
577
603
  # and execute
578
604
  self._execute_sync_strategy(sync_strategy)
605
+ # unlock index after sync
606
+ _ = self.index.lock(value=False)
579
607
  return diff.to_utterance_str()
580
608
 
581
609
  def _execute_sync_strategy(self, strategy: Dict[str, Dict[str, List[Utterance]]]):
@@ -781,6 +809,9 @@ class BaseRouter(BaseModel):
781
809
  :param route_name: the name of the route to be deleted
782
810
  :type str:
783
811
  """
812
+ # ensure index is not locked
813
+ if self.index._is_locked():
814
+ raise ValueError("Index is locked. Cannot delete route.")
784
815
  current_local_hash = self._get_hash()
785
816
  current_remote_hash = self.index._read_hash()
786
817
  if current_remote_hash.value == "":
@@ -928,7 +959,7 @@ class BaseRouter(BaseModel):
928
959
  """Given a query vector, retrieve the top_k most similar records."""
929
960
  # get scores and routes
930
961
  scores, routes = self.index.query(
931
- vector=xq, top_k=top_k, route_filter=route_filter
962
+ vector=xq[0], top_k=top_k, route_filter=route_filter
932
963
  )
933
964
  return [{"route": d, "score": s.item()} for d, s in zip(routes, scores)]
934
965
 
@@ -938,7 +969,7 @@ class BaseRouter(BaseModel):
938
969
  """Given a query vector, retrieve the top_k most similar records."""
939
970
  # get scores and routes
940
971
  scores, routes = await self.index.aquery(
941
- vector=xq, top_k=top_k, route_filter=route_filter
972
+ vector=xq[0], top_k=top_k, route_filter=route_filter
942
973
  )
943
974
  return [{"route": d, "score": s.item()} for d, s in zip(routes, scores)]
944
975
 
@@ -14,7 +14,7 @@ from semantic_router.route import Route
14
14
  from semantic_router.index import BaseIndex, HybridLocalIndex
15
15
  from semantic_router.schema import RouteChoice, SparseEmbedding, Utterance
16
16
  from semantic_router.utils.logger import logger
17
- from semantic_router.routers.base import BaseRouter
17
+ from semantic_router.routers.base import BaseRouter, xq_reshape
18
18
  from semantic_router.llms import BaseLLM
19
19
 
20
20
 
@@ -197,18 +197,19 @@ class HybridRouter(BaseRouter):
197
197
  def __call__(
198
198
  self,
199
199
  text: Optional[str] = None,
200
- vector: Optional[List[float]] = None,
200
+ vector: Optional[List[float] | np.ndarray] = None,
201
201
  simulate_static: bool = False,
202
202
  route_filter: Optional[List[str]] = None,
203
203
  sparse_vector: dict[int, float] | SparseEmbedding | None = None,
204
204
  ) -> RouteChoice:
205
- vector_arr: np.ndarray | None = None
206
205
  potential_sparse_vector: List[SparseEmbedding] | None = None
207
206
  # if no vector provided, encode text to get vector
208
207
  if vector is None:
209
208
  if text is None:
210
209
  raise ValueError("Either text or vector must be provided")
211
- vector_arr, potential_sparse_vector = self._encode(text=[text])
210
+ vector, potential_sparse_vector = self._encode(text=[text])
211
+ # convert to numpy array if not already
212
+ vector = xq_reshape(vector)
212
213
  if sparse_vector is None:
213
214
  if text is None:
214
215
  raise ValueError("Either text or sparse_vector must be provided")
@@ -217,10 +218,9 @@ class HybridRouter(BaseRouter):
217
218
  )
218
219
  if sparse_vector is None:
219
220
  raise ValueError("Sparse vector is required for HybridLocalIndex.")
220
- vector_arr = vector_arr if vector_arr is not None else np.array(vector)
221
221
  # TODO: add alpha as a parameter
222
222
  scores, route_names = self.index.query(
223
- vector=vector_arr,
223
+ vector=vector,
224
224
  top_k=self.top_k,
225
225
  route_filter=route_filter,
226
226
  sparse_vector=sparse_vector,
@@ -40,14 +40,12 @@ class SemanticRouter(BaseRouter):
40
40
  """Given some text, encode it."""
41
41
  # create query vector
42
42
  xq = np.array(self.encoder(text))
43
- xq = np.squeeze(xq) # Reduce to 1d array.
44
43
  return xq
45
44
 
46
45
  async def _async_encode(self, text: list[str]) -> Any:
47
46
  """Given some text, encode it."""
48
47
  # create query vector
49
48
  xq = np.array(await self.encoder.acall(docs=text))
50
- xq = np.squeeze(xq) # Reduce to 1d array.
51
49
  return xq
52
50
 
53
51
  def add(self, routes: List[Route] | Route):
@@ -1,4 +1,4 @@
1
- from datetime import datetime
1
+ from datetime import datetime, timezone
2
2
  from difflib import Differ
3
3
  from enum import Enum
4
4
  import json
@@ -62,12 +62,13 @@ class Message(BaseModel):
62
62
  class ConfigParameter(BaseModel):
63
63
  field: str
64
64
  value: str
65
- namespace: Optional[str] = None
66
- created_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())
65
+ scope: Optional[str] = None
66
+ created_at: str = Field(
67
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
68
+ )
67
69
 
68
70
  def to_pinecone(self, dimensions: int):
69
- if self.namespace is None:
70
- namespace = ""
71
+ namespace = self.scope or ""
71
72
  return {
72
73
  "id": f"{self.field}#{namespace}",
73
74
  "values": [0.1] * dimensions,