@@ -211,13 +211,55 @@ def _build_filter(self, base_filter: Optional[Any] = None) -> Optional[Any]:
211211
212212 return models .Filter (must = [ns_condition , base_filter ])
213213
214+ def _point_ids_for_utterances (self , routes_to_delete : dict ) -> list [str ]:
215+ """Compute deterministic point IDs for the given route→utterance mapping.
216+
217+ Uses the same uuid5 scheme as ``add()`` so IDs are derived without
218+ a round-trip to Qdrant.
219+ """
220+ ids = []
221+ for route , utterances in routes_to_delete .items ():
222+ for utterance in utterances :
223+ key = (
224+ f"{ self .namespace } :{ route } :{ utterance } "
225+ if self .namespace is not None
226+ else f"{ route } :{ utterance } "
227+ )
228+ ids .append (str (uuid .uuid5 (uuid .NAMESPACE_DNS , key )))
229+ return ids
230+
214231 def _remove_and_sync (self , routes_to_delete : dict ):
215- """Remove and sync the index.
232+ """Remove specific utterances from the index.
216233
217- :param routes_to_delete: The routes to delete .
234+ :param routes_to_delete: Dict mapping route name to list of utterances to remove .
218235 :type routes_to_delete: dict
219236 """
220- logger .error ("Sync remove is not implemented for QdrantIndex." )
237+ from qdrant_client import models
238+
239+ ids = self ._point_ids_for_utterances (routes_to_delete )
240+ if ids :
241+ self .client .delete (
242+ self .index_name ,
243+ points_selector = models .PointIdsList (points = ids ),
244+ )
245+
246+ async def _async_remove_and_sync (self , routes_to_delete : dict ):
247+ """Asynchronously remove specific utterances from the index.
248+
249+ :param routes_to_delete: Dict mapping route name to list of utterances to remove.
250+ :type routes_to_delete: dict
251+ """
252+ from qdrant_client import models
253+
254+ ids = self ._point_ids_for_utterances (routes_to_delete )
255+ if ids :
256+ if self .aclient is not None :
257+ await self .aclient .delete (
258+ self .index_name ,
259+ points_selector = models .PointIdsList (points = ids ),
260+ )
261+ else :
262+ self ._remove_and_sync (routes_to_delete )
221263
222264 def add (
223265 self ,
@@ -283,57 +325,107 @@ def add(
283325 batch_size = batch_size ,
284326 )
285327
286- def get_utterances (self , include_metadata : bool = False ) -> List [Utterance ]:
287- """Gets a list of route and utterance objects currently stored in the index.
328+ def _get_all (
329+ self , prefix : Optional [str ] = None , include_metadata : bool = False
330+ ) -> tuple [list [str ], list [dict ]]:
331+ """Retrieves all vector IDs (and optionally payloads) via scroll.
288332
289- :param include_metadata: Whether to include function schemas and metadata in
290- the returned Utterance objects - QdrantIndex does not currently support this
291- parameter so it is ignored. If required for your use-case please reach out to
292- semantic-router maintainers on GitHub via an issue or PR.
293- :type include_metadata: bool
294- :return: A list of Utterance objects.
295- :rtype: List[Utterance]
333+ :param prefix: Unused for Qdrant; filtering is handled by namespace.
334+ :param include_metadata: Whether to include payload dicts in the return value.
335+ :return: Tuple of (ids, metadata_list).
296336 """
297- # Check if collection exists first
298337 if not self .client .collection_exists (self .index_name ):
299- return []
338+ return [], []
300339
301340 from qdrant_client import grpc
302341
303342 results = []
304343 next_offset = None
305344 stop_scrolling = False
306- scroll_filter = self ._build_filter ()
307345 try :
308346 while not stop_scrolling :
309347 records , next_offset = self .client .scroll (
310348 self .index_name ,
311349 limit = SCROLL_SIZE ,
312350 offset = next_offset ,
313351 with_payload = True ,
314- scroll_filter = scroll_filter ,
352+ scroll_filter = self . _build_filter () ,
315353 )
316354 stop_scrolling = next_offset is None or (
317355 isinstance (next_offset , grpc .PointId )
318356 and next_offset .num == 0
319357 and next_offset .uuid == ""
320358 )
321-
322359 results .extend (records )
360+ except ValueError as e :
361+ logger .warning (f"Index likely empty, error: { e } " )
362+ return [], []
363+
364+ ids = [str (r .id ) for r in results ]
365+ metadata = [r .payload or {} for r in results ] if include_metadata else []
366+ return ids , metadata
367+
368+ async def _async_get_all (
369+ self , prefix : Optional [str ] = None , include_metadata : bool = False
370+ ) -> tuple [list [str ], list [dict ]]:
371+ """Async version of _get_all.
372+
373+ :param prefix: Unused for Qdrant; filtering is handled by namespace.
374+ :param include_metadata: Whether to include payload dicts in the return value.
375+ :return: Tuple of (ids, metadata_list).
376+ """
377+ if self .aclient is None :
378+ return self ._get_all (prefix = prefix , include_metadata = include_metadata )
323379
324- utterances : List [Utterance ] = [
325- Utterance (
326- route = x .payload [SR_ROUTE_PAYLOAD_KEY ],
327- utterance = x .payload [SR_UTTERANCE_PAYLOAD_KEY ],
328- function_schemas = None ,
329- metadata = x .payload .get ("metadata" , {}),
380+ if not await self .aclient .collection_exists (self .index_name ):
381+ return [], []
382+
383+ from qdrant_client import grpc
384+
385+ results = []
386+ next_offset = None
387+ stop_scrolling = False
388+ try :
389+ while not stop_scrolling :
390+ records , next_offset = await self .aclient .scroll (
391+ self .index_name ,
392+ limit = SCROLL_SIZE ,
393+ offset = next_offset ,
394+ with_payload = True ,
395+ scroll_filter = self ._build_filter (),
396+ )
397+ stop_scrolling = next_offset is None or (
398+ isinstance (next_offset , grpc .PointId )
399+ and next_offset .num == 0
400+ and next_offset .uuid == ""
330401 )
331- for x in results
332- ]
402+ results .extend (records )
333403 except ValueError as e :
334404 logger .warning (f"Index likely empty, error: { e } " )
335- return []
336- return utterances
405+ return [], []
406+
407+ ids = [str (r .id ) for r in results ]
408+ metadata = [r .payload or {} for r in results ] if include_metadata else []
409+ return ids , metadata
410+
411+ def get_utterances (self , include_metadata : bool = False ) -> List [Utterance ]:
412+ """Gets a list of route and utterance objects currently stored in the index.
413+
414+ :param include_metadata: Whether to include metadata in the returned Utterance objects.
415+ :type include_metadata: bool
416+ :return: A list of Utterance objects.
417+ :rtype: List[Utterance]
418+ """
419+ _ , payloads = self ._get_all (include_metadata = True )
420+ return [
421+ Utterance (
422+ route = p .get (SR_ROUTE_PAYLOAD_KEY , "" ),
423+ utterance = p .get (SR_UTTERANCE_PAYLOAD_KEY , "" ),
424+ function_schemas = None ,
425+ metadata = p .get ("metadata" , {}) if include_metadata else {},
426+ )
427+ for p in payloads
428+ ]
337429
338430 def delete (self , route_name : str ):
339431 """Delete records from the index.
@@ -479,13 +571,28 @@ async def aquery(
479571 ]
480572 return np .array (scores ), route_names
481573
482- def aget_routes (self ):
483- """Asynchronously get all routes from the index .
574+ def get_routes_tuples (self ) -> list [ tuple ] :
575+ """Synchronously get route and utterance objects as tuples .
484576
485- :return: A list of routes .
486- :rtype: List[str ]
577+ :return: A list of (route_name, utterance, function_schemas, metadata) tuples .
578+ :rtype: list[tuple ]
487579 """
488- logger .error ("Sync remove is not implemented for QdrantIndex." )
580+ utterances = self .get_utterances (include_metadata = True )
581+ return [
582+ (u .route , u .utterance , u .function_schemas , u .metadata ) for u in utterances
583+ ]
584+
585+ async def aget_routes (self ) -> list [tuple ]:
586+ """Asynchronously get a list of route and utterance objects currently
587+ stored in the index.
588+
589+ :return: A list of (route_name, utterance, function_schemas, metadata) tuples.
590+ :rtype: list[tuple]
591+ """
592+ utterances = await self .aget_utterances (include_metadata = True )
593+ return [
594+ (u .route , u .utterance , u .function_schemas , u .metadata ) for u in utterances
595+ ]
489596
490597 def delete_index (self ):
491598 """Delete the index.
@@ -767,43 +874,20 @@ async def aadd(
767874 )
768875
769876 async def aget_utterances (self , include_metadata : bool = False ) -> List [Utterance ]:
770- """Asynchronously gets a list of route and utterance objects currently stored in the index, including metadata."""
771- if self .aclient is None :
772- logger .warning (
773- "Cannot use async get_utterances with an in-memory Qdrant instance; falling back to sync get_utterances."
774- )
775- return self .get_utterances (include_metadata = include_metadata )
776- from qdrant_client import grpc
877+ """Asynchronously gets a list of route and utterance objects currently stored in the index.
777878
778- results = []
779- next_offset = None
780- stop_scrolling = False
781- scroll_filter = self ._build_filter ()
782- try :
783- while not stop_scrolling :
784- records , next_offset = await self .aclient .scroll (
785- self .index_name ,
786- limit = SCROLL_SIZE ,
787- offset = next_offset ,
788- with_payload = True ,
789- scroll_filter = scroll_filter ,
790- )
791- stop_scrolling = next_offset is None or (
792- isinstance (next_offset , grpc .PointId )
793- and next_offset .num == 0
794- and next_offset .uuid == ""
795- )
796- results .extend (records )
797- utterances : List [Utterance ] = [
798- Utterance (
799- route = x .payload [SR_ROUTE_PAYLOAD_KEY ],
800- utterance = x .payload [SR_UTTERANCE_PAYLOAD_KEY ],
801- function_schemas = None ,
802- metadata = x .payload .get ("metadata" , {}),
803- )
804- for x in results
805- ]
806- except ValueError as e :
807- logger .warning (f"Index likely empty, error: { e } " )
808- return []
809- return utterances
879+ :param include_metadata: Whether to include metadata in the returned Utterance objects.
880+ :type include_metadata: bool
881+ :return: A list of Utterance objects.
882+ :rtype: List[Utterance]
883+ """
884+ _ , payloads = await self ._async_get_all (include_metadata = True )
885+ return [
886+ Utterance (
887+ route = p .get (SR_ROUTE_PAYLOAD_KEY , "" ),
888+ utterance = p .get (SR_UTTERANCE_PAYLOAD_KEY , "" ),
889+ function_schemas = None ,
890+ metadata = p .get ("metadata" , {}) if include_metadata else {},
891+ )
892+ for p in payloads
893+ ]
0 commit comments