From 402d5525cce55204aa79470308e97705dd02a213 Mon Sep 17 00:00:00 2001 From: Anush008 Date: Mon, 1 Jun 2026 21:27:26 +0530 Subject: [PATCH] feat: Adds Qdrant vector search --- AGENTS.md | 1 + docs/llms.txt | 18 +- .../parameter/AgentCPM-Report_parameter.yaml | 7 + .../AgentCPM-Report_web_parameter.yaml | 7 + .../demos/parameter/LLM_memory_parameter.yaml | 7 + .../parameter/LightResearch_parameter.yaml | 7 + .../demos/parameter/RAG_memory_parameter.yaml | 7 + examples/demos/parameter/RAG_parameter.yaml | 7 + .../demos/parameter/RAG_web_parameter.yaml | 7 + .../parameter/milvus_index_parameter.yaml | 7 + examples/demos/qdrant_index.yaml | 10 + examples/experiments/qdrant_index.yaml | 10 + pyproject.toml | 4 +- script/deploy_retriever_config.json | 9 + servers/retriever/parameter.yaml | 9 +- .../retriever/src/index_backends/__init__.py | 1 + .../src/index_backends/qdrant_backend.py | 161 +++++++++++++ servers/retriever/src/retriever.py | 12 +- ui/backend/app.py | 24 +- ui/backend/pipeline_manager.py | 227 +++++++++++++----- uv.lock | 101 +++++++- 21 files changed, 541 insertions(+), 102 deletions(-) create mode 100644 examples/demos/qdrant_index.yaml create mode 100644 examples/experiments/qdrant_index.yaml create mode 100644 servers/retriever/src/index_backends/qdrant_backend.py diff --git a/AGENTS.md b/AGENTS.md index 6f48106d..f5074a06 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -334,6 +334,7 @@ if __name__ == "__main__": - Index backends are pluggable via factory: - `faiss` - `milvus` + - `qdrant` - Web search backends are pluggable via factory: - `exa` - `tavily` diff --git a/docs/llms.txt b/docs/llms.txt index 26592e01..72daa4d8 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -1596,7 +1596,7 @@ async def retriever_init( **Function** - Initializes retrieval service. - Embedding Backend (backend): Responsible for converting text/images into vectors (Infinity, SentenceTransformers, OpenAI, BM25). -- Index Backend (index_backend): Responsible for vector storage and retrieval (FAISS, Milvus). +- Index Backend (index_backend): Responsible for vector storage and retrieval (FAISS, Milvus, Qdrant). - Demo Mode: If is_demo=True, forces OpenAI + Milvus configuration, ignoring some parameters. --- @@ -1634,7 +1634,7 @@ async def retriever_index( **Function** - Builds retrieval index. - FAISS: Reads embedding_path (.npy) to build local index file. -- Milvus / Demo: Reads corpus_path (.jsonl), generates vectors and inserts into specified collection_name. +- Milvus / Qdrant / Demo: Reads corpus_path (.jsonl), generates vectors and inserts into specified collection_name. --- @@ -1652,7 +1652,7 @@ async def retriever_search( **Function** - Retrieves single or multiple queries. -- Automatically handles query vectorization (adds query_instruction) and finds Top-K in specified collection_name (for Milvus) or default index. +- Automatically handles query vectorization (adds query_instruction) and finds Top-K in specified collection_name (for Milvus or Qdrant) or default index. **Output Format (JSON)** ```json @@ -1820,7 +1820,7 @@ backend_configs: save_path: index/bm25 # Index Backend Configuration -index_backend: faiss # options: faiss, milvus +index_backend: faiss # options: faiss, milvus, qdrant index_backend_configs: faiss: index_use_gpu: True @@ -1878,9 +1878,9 @@ Parameter Description: | `model_name_or_path` | str | Retrieval model path or name (e.g., HuggingFace model ID) | | `corpus_path` | str | Input corpus JSONL file path | | `embedding_path` | str | Vector file save path (`.npy`) | -| `collection_name` | str | Milvus collection name | +| `collection_name` | str | Milvus or Qdrant collection name | | `backend` | str | Select retrieval backend: `infinity`, `sentence_transformers`, `openai`, `bm25` | -| `index_backend` | str | Index backend: `faiss`, `milvus` | +| `index_backend` | str | Index backend: `faiss`, `milvus`, `qdrant` | | `backend_configs` | dict | Parameter configuration for each backend (see table below) | | `index_backend_configs` | dict | Parameter configuration for each index backend (see table below) | | `websearch_backend` | str | Web search backend: `tavily`, `exa`, `zhipuai` | @@ -1941,6 +1941,12 @@ Parameter Description: ||index_params| Dict| Index construction parameters (e.g., index_type: AUTOINDEX)| ||search_params |Dict |Retrieval parameters (e.g., nprobe etc.)| ||index_chunk_size| int| Batch size when inserting data| +|qdrant|url| str| Qdrant server URL (e.g., http://localhost:6333); leave null to use local path| +||path| str |Local persistent storage path; used when url is null| +||api_key| str |API key for authentication (if needed)| +||text_field_name| str |Payload field name for document text (default contents)| +||distance| str |Distance metric: Cosine, Dot, Euclid, Manhattan (default Cosine)| +||index_chunk_size| int |Batch size when upserting vectors| ================ File: pages/en/api/router.mdx diff --git a/examples/demos/parameter/AgentCPM-Report_parameter.yaml b/examples/demos/parameter/AgentCPM-Report_parameter.yaml index 46acb66e..f6236de1 100644 --- a/examples/demos/parameter/AgentCPM-Report_parameter.yaml +++ b/examples/demos/parameter/AgentCPM-Report_parameter.yaml @@ -60,6 +60,13 @@ retriever: metric_type: IP params: {} index_chunk_size: 1000 + qdrant: + url: null + path: index/qdrant + api_key: null + text_field_name: contents + distance: Cosine + index_chunk_size: 50000 is_demo: false collection_name: wiki top_k: 5 diff --git a/examples/demos/parameter/AgentCPM-Report_web_parameter.yaml b/examples/demos/parameter/AgentCPM-Report_web_parameter.yaml index ca5e067d..98500f75 100644 --- a/examples/demos/parameter/AgentCPM-Report_web_parameter.yaml +++ b/examples/demos/parameter/AgentCPM-Report_web_parameter.yaml @@ -60,6 +60,13 @@ retriever: metric_type: IP params: {} index_chunk_size: 1000 + qdrant: + url: null + path: index/qdrant + api_key: null + text_field_name: contents + distance: Cosine + index_chunk_size: 50000 is_demo: false collection_name: wiki top_k: 5 diff --git a/examples/demos/parameter/LLM_memory_parameter.yaml b/examples/demos/parameter/LLM_memory_parameter.yaml index d6621f84..0dfd7c69 100644 --- a/examples/demos/parameter/LLM_memory_parameter.yaml +++ b/examples/demos/parameter/LLM_memory_parameter.yaml @@ -62,6 +62,13 @@ retriever: metric_type: IP params: {} index_chunk_size: 1000 + qdrant: + url: null + path: index/qdrant + api_key: null + text_field_name: contents + distance: Cosine + index_chunk_size: 50000 is_demo: false collection_name: wiki top_k: 5 diff --git a/examples/demos/parameter/LightResearch_parameter.yaml b/examples/demos/parameter/LightResearch_parameter.yaml index 310c27cc..800b37df 100644 --- a/examples/demos/parameter/LightResearch_parameter.yaml +++ b/examples/demos/parameter/LightResearch_parameter.yaml @@ -94,6 +94,13 @@ retriever: token: null uri: index/milvus_demo.db vector_field_name: vector + qdrant: + url: null + path: index/qdrant + api_key: null + text_field_name: contents + distance: Cosine + index_chunk_size: 50000 is_demo: false is_multimodal: false model_name_or_path: openbmb/MiniCPM-Embedding-Light diff --git a/examples/demos/parameter/RAG_memory_parameter.yaml b/examples/demos/parameter/RAG_memory_parameter.yaml index 20529d57..277c8b64 100644 --- a/examples/demos/parameter/RAG_memory_parameter.yaml +++ b/examples/demos/parameter/RAG_memory_parameter.yaml @@ -62,6 +62,13 @@ retriever: metric_type: IP params: {} index_chunk_size: 1000 + qdrant: + url: null + path: index/qdrant + api_key: null + text_field_name: contents + distance: Cosine + index_chunk_size: 50000 is_demo: false collection_name: wiki top_k: 5 diff --git a/examples/demos/parameter/RAG_parameter.yaml b/examples/demos/parameter/RAG_parameter.yaml index 3a4c2c13..15a016b3 100644 --- a/examples/demos/parameter/RAG_parameter.yaml +++ b/examples/demos/parameter/RAG_parameter.yaml @@ -90,6 +90,13 @@ retriever: token: null uri: index/milvus_demo.db vector_field_name: vector + qdrant: + url: null + path: index/qdrant + api_key: null + text_field_name: contents + distance: Cosine + index_chunk_size: 50000 is_demo: false is_multimodal: false model_name_or_path: openbmb/MiniCPM-Embedding-Light diff --git a/examples/demos/parameter/RAG_web_parameter.yaml b/examples/demos/parameter/RAG_web_parameter.yaml index c10f5281..a3b41a7c 100644 --- a/examples/demos/parameter/RAG_web_parameter.yaml +++ b/examples/demos/parameter/RAG_web_parameter.yaml @@ -60,6 +60,13 @@ retriever: metric_type: IP params: {} index_chunk_size: 1000 + qdrant: + url: null + path: index/qdrant + api_key: null + text_field_name: contents + distance: Cosine + index_chunk_size: 50000 is_demo: false collection_name: wiki top_k: 5 diff --git a/examples/demos/parameter/milvus_index_parameter.yaml b/examples/demos/parameter/milvus_index_parameter.yaml index 8e99b403..084a6869 100644 --- a/examples/demos/parameter/milvus_index_parameter.yaml +++ b/examples/demos/parameter/milvus_index_parameter.yaml @@ -50,6 +50,13 @@ retriever: metric_type: IP params: {} index_chunk_size: 1000 + qdrant: + url: null + path: index/qdrant + api_key: null + text_field_name: contents + distance: Cosine + index_chunk_size: 50000 is_demo: false collection_name: wiki embedding_path: embedding/embedding.npy diff --git a/examples/demos/qdrant_index.yaml b/examples/demos/qdrant_index.yaml new file mode 100644 index 00000000..71149a40 --- /dev/null +++ b/examples/demos/qdrant_index.yaml @@ -0,0 +1,10 @@ +# Qdrant Embedding and Indexing Demo for UltraRAG UI + +# MCP Server +servers: + retriever: servers/retriever + +# MCP Client Pipeline +pipeline: +- retriever.retriever_init +- retriever.retriever_index diff --git a/examples/experiments/qdrant_index.yaml b/examples/experiments/qdrant_index.yaml new file mode 100644 index 00000000..71149a40 --- /dev/null +++ b/examples/experiments/qdrant_index.yaml @@ -0,0 +1,10 @@ +# Qdrant Embedding and Indexing Demo for UltraRAG UI + +# MCP Server +servers: + retriever: servers/retriever + +# MCP Client Pipeline +pipeline: +- retriever.retriever_init +- retriever.retriever_index diff --git a/pyproject.toml b/pyproject.toml index ed209e10..910b25ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,10 +43,12 @@ retriever = [ "sentence-transformers", "openai", "bm25s", - "faiss-gpu-cu12", + "faiss-gpu-cu12; sys_platform != 'darwin'", + "faiss-cpu; sys_platform == 'darwin'", "exa_py", "tavily-python", "pymilvus", + "qdrant-client", "numba", "torch", "fastapi", diff --git a/script/deploy_retriever_config.json b/script/deploy_retriever_config.json index c9d79596..20b12b14 100644 --- a/script/deploy_retriever_config.json +++ b/script/deploy_retriever_config.json @@ -58,6 +58,15 @@ "params": {} }, "index_chunk_size": 50000 + }, + "qdrant": { + "url": null, + "path": "index/qdrant", + "api_key": null, + "collection_name": "ultrarag_embeddings", + "text_field_name": "contents", + "distance": "Cosine", + "index_chunk_size": 50000 } }, diff --git a/servers/retriever/parameter.yaml b/servers/retriever/parameter.yaml index 8f681848..de6e9bd2 100644 --- a/servers/retriever/parameter.yaml +++ b/servers/retriever/parameter.yaml @@ -31,7 +31,7 @@ backend_configs: tokenizer: auto # options: auto, default, jieba, character; auto uses jieba for Chinese save_path: index/bm25 -index_backend: faiss # options: faiss, milvus +index_backend: faiss # options: faiss, milvus, qdrant index_backend_configs: faiss: index_use_gpu: True @@ -53,6 +53,13 @@ index_backend_configs: metric_type: IP params: {} index_chunk_size: 1000 + qdrant: + url: null # http://localhost:6333 for server, or leave null for local path + path: index/qdrant # local persistent storage; ignored when url is set + api_key: null + text_field_name: contents + distance: Cosine # options: Cosine, Dot, Euclid, Manhattan + index_chunk_size: 50000 websearch_backend: tavily # options: tavily, exa, zhipuai websearch_backend_configs: diff --git a/servers/retriever/src/index_backends/__init__.py b/servers/retriever/src/index_backends/__init__.py index a0388a2b..369d7572 100644 --- a/servers/retriever/src/index_backends/__init__.py +++ b/servers/retriever/src/index_backends/__init__.py @@ -8,6 +8,7 @@ _INDEX_BACKENDS: Dict[str, str] = { "faiss": ".faiss_backend.FaissIndexBackend", "milvus": ".milvus_backend.MilvusIndexBackend", + "qdrant": ".qdrant_backend.QdrantIndexBackend", } diff --git a/servers/retriever/src/index_backends/qdrant_backend.py b/servers/retriever/src/index_backends/qdrant_backend.py new file mode 100644 index 00000000..8356c8be --- /dev/null +++ b/servers/retriever/src/index_backends/qdrant_backend.py @@ -0,0 +1,161 @@ +from __future__ import annotations + +import uuid +from typing import Any, Dict, List, Optional, Sequence + +import numpy as np + +from .base import BaseIndexBackend + +try: + from qdrant_client import QdrantClient + from qdrant_client.models import Distance, PointStruct, QueryRequest, VectorParams +except ImportError: + QdrantClient = Distance = PointStruct = QueryRequest = VectorParams = None + +_INTERNAL_KEYS = frozenset({"text_field_name", "distance", "index_chunk_size", "collection_name"}) + + +def _to_qdrant_id(val: Any) -> uuid.UUID: + return uuid.uuid5(uuid.NAMESPACE_DNS, str(val)) + + +class QdrantIndexBackend(BaseIndexBackend): + """Qdrant-based index backend for vector similarity search.""" + + def __init__( + self, + contents: Sequence[str], + config: Optional[dict[str, Any]], + logger, + **_: Any, + ) -> None: + if QdrantClient is None: + raise ImportError( + "qdrant-client is not installed. " + "Install it with `pip install qdrant-client`." + ) + super().__init__(contents=[], config=config, logger=logger) + + self.collection_name: Optional[str] = self.config.get("collection_name") + self.text_field: str = self.config.get("text_field_name", "contents") + self.chunk_size: int = int(self.config.get("index_chunk_size", 50000)) + self._distance_str: str = self.config.get("distance", "Cosine").lower() + self.client: Optional[QdrantClient] = None + + def _connect(self) -> QdrantClient: + if self.client is None: + kw = {k: v for k, v in self.config.items() if k not in _INTERNAL_KEYS and v is not None} + self.client = QdrantClient(**kw) if kw else QdrantClient(":memory:") + return self.client + + def _distance(self) -> Any: + mapping = { + "cosine": Distance.COSINE, + "dot": Distance.DOT, + "ip": Distance.DOT, + "euclid": Distance.EUCLID, + "l2": Distance.EUCLID, + "manhattan": Distance.MANHATTAN, + } + dist = mapping.get(self._distance_str) + if dist is None: + self.logger.warning( + "[qdrant] Unknown distance '%s'; defaulting to Cosine.", + self._distance_str, + ) + return Distance.COSINE + return dist + + def _ensure_collection(self, name: str, dim: int, overwrite: bool) -> None: + client = self._connect() + if overwrite and client.collection_exists(name): + client.delete_collection(name) + if not client.collection_exists(name): + client.create_collection( + name, vectors_config=VectorParams(size=dim, distance=self._distance()) + ) + + def load_index(self) -> None: + self._connect() + + def build_index( + self, + *, + embeddings: np.ndarray, + ids: np.ndarray, + overwrite: bool = False, + **kwargs: Any, + ) -> None: + col = kwargs.get("collection_name", self.collection_name) + contents = kwargs.get("contents") + metadatas = kwargs.get("metadatas") or [] + + if not col: + raise ValueError("[qdrant] 'collection_name' is required.") + if not contents: + raise ValueError("[qdrant] 'contents' is required.") + + embeddings = np.asarray(embeddings, dtype=np.float32, order="C") + if embeddings.ndim != 2: + raise ValueError("[qdrant] embeddings must be 2-D.") + if len(ids) != len(embeddings): + raise ValueError("[qdrant] ids and embeddings must have the same length.") + + self._ensure_collection(col, embeddings.shape[1], overwrite) + client = self._connect() + + for start in range(0, len(embeddings), self.chunk_size): + end = min(start + self.chunk_size, len(embeddings)) + client.upsert( + collection_name=col, + points=[ + PointStruct( + id=_to_qdrant_id(ids[i]), + vector=embeddings[i].tolist(), + payload={ + self.text_field: contents[i], + **( + metadatas[i] + if i < len(metadatas) and isinstance(metadatas[i], dict) + else {} + ), + }, + ) + for i in range(start, end) + ], + ) + + self.logger.info("[qdrant] Indexed %d vectors into '%s'.", len(embeddings), col) + + def search( + self, query_embeddings: np.ndarray, top_k: int, **kwargs: Any + ) -> List[List[str]]: + col = kwargs.get("collection_name", self.collection_name) + if not col: + raise ValueError("[qdrant] 'collection_name' is required.") + + query_embeddings = np.asarray(query_embeddings, dtype=np.float32, order="C") + if query_embeddings.ndim != 2: + raise ValueError("[qdrant] query embeddings must be 2-D.") + + try: + responses = self._connect().query_batch_points( + collection_name=col, + requests=[ + QueryRequest(query=row.tolist(), limit=top_k, with_payload=True) + for row in query_embeddings + ], + ) + except Exception as exc: + raise RuntimeError(f"[qdrant] Search failed on '{col}': {exc}") from exc + + return [ + [str((hit.payload or {}).get(self.text_field, "")) for hit in resp.points] + for resp in responses + ] + + def close(self) -> None: + if self.client is not None: + self.client.close() + self.client = None diff --git a/servers/retriever/src/retriever.py b/servers/retriever/src/retriever.py index 4dafddc6..6d659a66 100644 --- a/servers/retriever/src/retriever.py +++ b/servers/retriever/src/retriever.py @@ -207,10 +207,10 @@ async def retriever_init( gpu_ids: Comma-separated GPU IDs (e.g., "0,1") is_multimodal: Whether to use multimodal (image) embeddings backend: Backend name ("infinity", "sentence_transformers", "openai", or "bm25") - index_backend: Index backend name ("faiss" or "milvus") + index_backend: Index backend name ("faiss", "milvus", or "qdrant") index_backend_configs: Dictionary of index backend configurations is_demo: Whether to run in demo mode (forces OpenAI + Milvus) - collection_name: Collection name for Milvus backend + collection_name: Collection name for Milvus or Qdrant backend Raises: ImportError: If required dependencies are not installed @@ -461,7 +461,7 @@ async def retriever_init( self.index_backend_name, {} ) - if self.index_backend_name == "milvus": + if self.index_backend_name in ("milvus", "qdrant"): index_backend_cfg["collection_name"] = collection_name self.index_backend = create_index_backend( @@ -694,7 +694,7 @@ async def retriever_index( Args: embedding_path: Path to embeddings file (.npy) for non-demo mode overwrite: Whether to overwrite existing index - collection_name: Collection name for Milvus backend + collection_name: Collection name for Milvus or Qdrant backend corpus_path: Corpus file path (required for demo mode) Raises: @@ -873,7 +873,7 @@ async def retriever_search( query_list: List of query strings top_k: Number of top passages to return per query query_instruction: Optional instruction to prepend to queries - collection_name: Collection name for Milvus backend + collection_name: Collection name for Milvus or Qdrant backend Returns: Dictionary with 'ret_psg' containing retrieved passages @@ -996,7 +996,7 @@ async def retriever_batch_search( batch_query_list: List of query lists (one batch per list) top_k: Number of top passages to return per query query_instruction: Optional instruction to prepend to queries - collection_name: Collection name for Milvus backend + collection_name: Collection name for Milvus or Qdrant backend Returns: Dictionary with 'ret_psg_ls' containing retrieved passages for each batch diff --git a/ui/backend/app.py b/ui/backend/app.py index 9051b007..baf47a0a 100644 --- a/ui/backend/app.py +++ b/ui/backend/app.py @@ -377,7 +377,7 @@ def _run_kb_background( pipeline_name: Name of the pipeline to run target_file: Path to target file output_dir: Output directory path - collection_name: Milvus collection name + collection_name: Milvus or Qdrant collection name index_mode: Index mode ("append" or "overwrite") chunk_params: Optional chunking parameters embedding_params: Optional embedding parameters @@ -397,7 +397,7 @@ def _run_kb_background( ) if ( - pipeline_name == "milvus_index" + pipeline_name in ("milvus_index", "qdrant_index") and index_mode == "new" and visibility_store is not None and owner_user_id @@ -1453,11 +1453,12 @@ def _stream_with_chat_persistence(base_stream): try: kb_config = pm.load_kb_config() - milvus_global_config = kb_config.get("milvus", {}) + index_backend = kb_config.get("index_backend", "milvus") + backend_cfg = kb_config.get(index_backend, {}) retriever_params = { - "index_backend": "milvus", - "index_backend_configs": {"milvus": milvus_global_config}, + "index_backend": index_backend, + "index_backend_configs": {index_backend: backend_cfg}, } if selected_collection: @@ -1686,11 +1687,12 @@ def start_background_chat(name: str) -> Response: dynamic_params["memory"] = memory_params try: kb_config = pm.load_kb_config() - milvus_global_config = kb_config.get("milvus", {}) + index_backend = kb_config.get("index_backend", "milvus") + backend_cfg = kb_config.get(index_backend, {}) retriever_params = { - "index_backend": "milvus", - "index_backend_configs": {"milvus": milvus_global_config}, + "index_backend": index_backend, + "index_backend_configs": {index_backend: backend_cfg}, } if selected_collection: @@ -2133,7 +2135,7 @@ def run_kb_task() -> Response: "use_title": payload.get("use_title", True), } - # Embedding parameters (for milvus_index) + # Embedding parameters (for milvus_index / qdrant_index) embedding_params = { "api_key": payload.get("emb_api_key", ""), "base_url": payload.get("emb_base_url", "https://api.openai.com/v1"), @@ -2143,7 +2145,7 @@ def run_kb_task() -> Response: if not pipeline_name or not target_file: return jsonify({"error": "Missing pipeline_name or target_file"}), 400 - if pipeline_name == "milvus_index": + if pipeline_name in ("milvus_index", "qdrant_index"): normalized_mode = str(index_mode or "").strip().lower() if normalized_mode in {"append", "overwrite"}: target_collection = str(collection_name or "").strip() @@ -2163,7 +2165,7 @@ def run_kb_task() -> Response: output_dir = str(pm.KB_CORPUS_DIR) elif pipeline_name == "corpus_chunk": output_dir = str(pm.KB_CHUNKS_DIR) - elif pipeline_name == "milvus_index": + elif pipeline_name in ("milvus_index", "qdrant_index"): output_dir = "" task_id = str(uuid.uuid4()) diff --git a/ui/backend/pipeline_manager.py b/ui/backend/pipeline_manager.py index 1a1ec84a..ba1bb1ff 100644 --- a/ui/backend/pipeline_manager.py +++ b/ui/backend/pipeline_manager.py @@ -39,6 +39,11 @@ except ImportError: MilvusClient = None +try: + from qdrant_client import QdrantClient as QdrantClientCls +except ImportError: + QdrantClientCls = None + LOGGER = logging.getLogger(__name__) # Suppress noisy "Event loop is closed" errors triggered by fastmcp transport @@ -1805,15 +1810,15 @@ def _report_progress(progress: int, message: str = "") -> None: _write_turn_chunks_jsonl(chunk_file, turn_chunks, normalized_user_id) _report_progress(45, f"Prepared {len(turn_chunks)} turn chunks") - # Use examples/milvus_index.yaml pipeline for indexing. + index_pipeline = load_kb_config().get("index_backend", "milvus") + "_index" run_kb_pipeline_tool( - pipeline_name="milvus_index", + pipeline_name=index_pipeline, target_file_path=str(chunk_file), output_dir="", collection_name=collection_name, index_mode=run_mode, ) - _report_progress(85, "Indexed turn chunks to Milvus") + _report_progress(85, "Indexed turn chunks to vector store") _save_memory_sync_state( normalized_user_id, @@ -1860,18 +1865,20 @@ def clear_user_memory_collection_vectors(user_id: Optional[str]) -> Dict[str, An } client = None + index_backend = load_kb_config().get("index_backend", "milvus") try: - client = _get_milvus_client() + client = _get_qdrant_client() if index_backend == "qdrant" else _get_milvus_client() before_count = 0 - if client.has_collection(collection_name): + if index_backend == "qdrant": + if client.collection_exists(collection_name): + before_count = client.get_collection(collection_name).points_count or 0 + client.delete_collection(collection_name) + LOGGER.info("Dropped memory collection for user %s: %s", normalized_user_id, collection_name) + elif client.has_collection(collection_name): before_count = _get_collection_row_count(client, collection_name) # Drop collection instead of row-by-row delete to avoid stale row_count. client.drop_collection(collection_name) - LOGGER.info( - "Dropped memory collection for user %s: %s", - normalized_user_id, - collection_name, - ) + LOGGER.info("Dropped memory collection for user %s: %s", normalized_user_id, collection_name) # Also clear user working-memory project files so next sync starts clean. # Keep global MEMORY.md untouched. @@ -1909,7 +1916,7 @@ def clear_user_memory_collection_vectors(user_id: Optional[str]) -> Dict[str, An try: client.close() except Exception as exc: - LOGGER.debug("Failed to close Milvus client for user %s: %s", normalized_user_id, exc) + LOGGER.debug("Failed to close vector store client for user %s: %s", normalized_user_id, exc) lock.release() @@ -2141,6 +2148,7 @@ def _sanitize_pipeline_name(name: str) -> str: "build_text_corpus", "corpus_chunk", "milvus_index", + "qdrant_index", } @@ -2224,6 +2232,7 @@ def list_server_tools() -> List[ServerTool]: "build_text_corpus", "corpus_chunk", "milvus_index", + "qdrant_index", "multiturn_chat", } @@ -2872,6 +2881,7 @@ def clear_completed_background_tasks(user_id: str = "") -> int: # Knowledge Base Management def load_kb_config() -> Dict[str, Any]: default_config = { + "index_backend": "milvus", "milvus": { "uri": "tcp://127.0.0.1:19530", "token": "", @@ -2884,7 +2894,15 @@ def load_kb_config() -> Dict[str, Any]: "index_params": {"index_type": "AUTOINDEX", "metric_type": "IP"}, "search_params": {"metric_type": "IP", "params": {}}, "index_chunk_size": 1000, - } + }, + "qdrant": { + "url": None, + "path": "index/qdrant", + "api_key": None, + "text_field_name": "contents", + "distance": "Cosine", + "index_chunk_size": 50000, + }, } if not KB_CONFIG_PATH.exists(): @@ -2892,12 +2910,13 @@ def load_kb_config() -> Dict[str, Any]: try: saved = json.loads(KB_CONFIG_PATH.read_text(encoding="utf-8")) - if "milvus" not in saved: - return default_config - - full_cfg = default_config["milvus"].copy() - full_cfg.update(saved["milvus"]) - return {"milvus": full_cfg} + result = {"index_backend": saved.get("index_backend", "milvus")} + for backend in ("milvus", "qdrant"): + full_cfg = default_config[backend].copy() + if backend in saved: + full_cfg.update(saved[backend]) + result[backend] = full_cfg + return result except Exception: return default_config @@ -2908,32 +2927,39 @@ def save_kb_config(config: Dict[str, str]): def _get_milvus_client() -> Any: - """Get Milvus client instance. - - Returns: - MilvusClient instance - - Raises: - ValueError: If URI is empty - Exception: If connection fails - """ cfg = load_kb_config() try: milvus_cfg = cfg.get("milvus", {}) uri = milvus_cfg.get("uri", "") - if not uri: raise ValueError("URI is empty") - - if not uri.startswith("http") and not Path(uri).is_absolute(): - pass - return MilvusClient(uri=uri, token=milvus_cfg.get("token", "")) except Exception as e: LOGGER.error(f"Failed to connect to Milvus: {e}") raise e +def _get_qdrant_client() -> Any: + cfg = load_kb_config() + try: + qdrant_cfg = cfg.get("qdrant", {}) + url = qdrant_cfg.get("url") + path = qdrant_cfg.get("path") + api_key = qdrant_cfg.get("api_key") + if url: + kw = {"url": url} + if api_key: + kw["api_key"] = api_key + return QdrantClientCls(**kw) + elif path: + return QdrantClientCls(path=path) + else: + return QdrantClientCls(":memory:") + except Exception as e: + LOGGER.error(f"Failed to connect to Qdrant: {e}") + raise e + + def list_kb_files() -> Dict[str, List[Dict[str, Any]]]: """List knowledge base files across all categories. @@ -3010,28 +3036,43 @@ def _scan_files(d: Path, category: str) -> List[Dict[str, Any]]: collections = [] db_status = "unknown" + index_backend = load_kb_config().get("index_backend", "milvus") try: - client = _get_milvus_client() - names = client.list_collections() - for name in names: - res = client.get_collection_stats(name) - count = res.get("row_count", 0) - try: - desc = client.describe_collection(name).get("description", "") - except Exception: - desc = "" - collections.append( - { - "name": name, - "display_name": _extract_display_name_from_desc(desc, name), - "count": count, - "category": "collection", - } - ) - client.close() + if index_backend == "qdrant": + client = _get_qdrant_client() + for col in client.get_collections().collections: + info = client.get_collection(col.name) + collections.append( + { + "name": col.name, + "display_name": col.name, + "count": info.points_count or 0, + "category": "collection", + } + ) + client.close() + else: + client = _get_milvus_client() + names = client.list_collections() + for name in names: + res = client.get_collection_stats(name) + count = res.get("row_count", 0) + try: + desc = client.describe_collection(name).get("description", "") + except Exception: + desc = "" + collections.append( + { + "name": name, + "display_name": _extract_display_name_from_desc(desc, name), + "count": count, + "category": "collection", + } + ) + client.close() db_status = "connected" except Exception as e: - LOGGER.warning(f"Milvus connection failed: {e}") + LOGGER.warning(f"{index_backend.capitalize()} connection failed: {e}") db_status = "error" return { @@ -3256,9 +3297,11 @@ def delete_kb_file(category: str, filename: str) -> Dict[str, str]: elif category == "chunks": base_dir = KB_CHUNKS_DIR elif category == "collection": - return _delete_milvus_collection(filename) + backend = load_kb_config().get("index_backend", "milvus") + return _delete_qdrant_collection(filename) if backend == "qdrant" else _delete_milvus_collection(filename) elif category == "index": - return _delete_milvus_collection(filename) + backend = load_kb_config().get("index_backend", "milvus") + return _delete_qdrant_collection(filename) if backend == "qdrant" else _delete_milvus_collection(filename) if not base_dir: raise ValueError("Invalid category") @@ -3287,17 +3330,6 @@ def delete_kb_file(category: str, filename: str) -> Dict[str, str]: def _delete_milvus_collection(name: str) -> Dict[str, str]: - """Delete a Milvus collection. - - Args: - name: Collection name - - Returns: - Dictionary with deletion status - - Raises: - Exception: If deletion fails - """ try: client = _get_milvus_client() if client.has_collection(name): @@ -3310,6 +3342,19 @@ def _delete_milvus_collection(name: str) -> Dict[str, str]: raise e +def _delete_qdrant_collection(name: str) -> Dict[str, str]: + try: + client = _get_qdrant_client() + if client.collection_exists(name): + client.delete_collection(name) + LOGGER.info(f"Dropped Qdrant collection: {name}") + client.close() + return {"status": "deleted", "collection": name} + except Exception as e: + LOGGER.error(f"Failed to drop Qdrant collection {name}: {e}") + raise e + + def clear_staging_area() -> Dict[str, Any]: """Clear staging area: delete all files in raw, corpus, chunks directories. @@ -3376,7 +3421,7 @@ def run_kb_pipeline_tool( chunk_params: Optional[Dict[str, Any]] = None, # [New] Receive parameters embedding_params: Optional[ Dict[str, Any] - ] = None, # [New] Embedding configuration (for milvus_index) + ] = None, # [New] Embedding configuration (for milvus_index / qdrant_index) progress_callback: Optional[Any] = None, # Progress callback: (progress: int, message: str) -> None ) -> Dict[str, Any]: """Run knowledge base pipeline tool. @@ -3385,7 +3430,7 @@ def run_kb_pipeline_tool( pipeline_name: Name of the pipeline to run target_file_path: Path to target file or directory output_dir: Output directory path - collection_name: Optional Milvus collection name + collection_name: Optional collection name (Milvus or Qdrant) index_mode: Index mode ("append" or "overwrite") chunk_params: Optional chunking parameters embedding_params: Optional embedding parameters @@ -3568,6 +3613,56 @@ def _report_progress(progress: int, message: str = "") -> None: override_params = {"retriever": retriever_override} + elif pipeline_name == "qdrant_index": + requested_name = collection_name if collection_name else stem + + client = None + try: + client = _get_qdrant_client() + existing_collections = {c.name for c in client.get_collections().collections} + except Exception as exc: + raise PipelineManagerError(f"Qdrant connection failed: {exc}") from exc + finally: + try: + if client: + client.close() + except Exception: + pass + + if index_mode == "new" and requested_name in existing_collections: + raise PipelineManagerError( + "Collection name already exists. Choose append or overwrite." + ) + + resolved_collection_name = requested_name + resolved_collection_display_name = requested_name + is_overwrite = index_mode == "overwrite" + + full_kb_cfg = load_kb_config() + qdrant_config_dict = dict(full_kb_cfg["qdrant"]) + + KB_INDEX_DIR.mkdir(parents=True, exist_ok=True) + + retriever_override = { + "corpus_path": str(target_file), + "collection_name": requested_name, + "overwrite": is_overwrite, + "index_backend": "qdrant", + "index_backend_configs": {"qdrant": qdrant_config_dict}, + } + + if embedding_params and embedding_params.get("api_key"): + retriever_override["backend"] = "openai" + retriever_override["backend_configs"] = { + "openai": { + "api_key": embedding_params.get("api_key", ""), + "base_url": embedding_params.get("base_url", "https://api.openai.com/v1"), + "model_name": embedding_params.get("model_name", "text-embedding-3-small"), + } + } + + override_params = {"retriever": retriever_override} + else: raise PipelineManagerError(f"Unsupported KB Pipeline: {pipeline_name}") diff --git a/uv.lock b/uv.lock index b368f553..f23db45f 100644 --- a/uv.lock +++ b/uv.lock @@ -1162,17 +1162,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] +[[package]] +name = "faiss-cpu" +version = "1.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "sys_platform == 'darwin'" }, + { name = "packaging", marker = "sys_platform == 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/8b/5d2cd7c9fd60bc4d1ca6e9f5e8b0eb57254a7b301daaa12d5853fdf48afe/faiss_cpu-1.14.2-cp310-abi3-macosx_14_0_arm64.whl", hash = "sha256:a20011b8a97318e6d5e29143a773277ca71f1c33140024aeec91f05ad56cdd04", size = 4621203, upload-time = "2026-05-22T19:58:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/84/b1/05876aa7ceafd67a8c53667f574c0354e50ebadfdaf0632d7d9f5c80fffe/faiss_cpu-1.14.2-cp310-abi3-macosx_15_0_x86_64.whl", hash = "sha256:6ba528b5803fe5206bbb38e6ea2c537de0d1482a47c5765982af4e4a48c135e2", size = 6681426, upload-time = "2026-05-22T19:58:29.193Z" }, +] + [[package]] name = "faiss-gpu-cu12" version = "1.13.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, - { name = "nvidia-cublas-cu12", version = "12.8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "numpy", marker = "sys_platform != 'darwin'" }, + { name = "nvidia-cublas-cu12", version = "12.8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin' and sys_platform != 'linux'" }, { name = "nvidia-cublas-cu12", version = "12.9.1.4", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, - { name = "nvidia-cuda-runtime-cu12", version = "12.8.90", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", version = "12.8.90", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin' and sys_platform != 'linux'" }, { name = "nvidia-cuda-runtime-cu12", version = "12.9.79", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, - { name = "packaging" }, + { name = "packaging", marker = "sys_platform != 'darwin'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c1/1d/7823598589ba8a67bcaff5fe93d4025991cda04a6ecc93cba862ae89b63a/faiss_gpu_cu12-1.13.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:752fa4044ec10a3a55f4fa0e81472db8915b4fa40bec6166959c69391b3e5142", size = 48399007, upload-time = "2025-12-29T23:26:52.455Z" }, @@ -1734,6 +1747,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, +] + [[package]] name = "hf-transfer" version = "0.1.9" @@ -1770,6 +1796,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, ] +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -1821,6 +1856,9 @@ wheels = [ ] [package.optional-dependencies] +http2 = [ + { name = "h2" }, +] socks = [ { name = "socksio" }, ] @@ -1877,6 +1915,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, ] +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + [[package]] name = "idna" version = "3.15" @@ -3242,10 +3289,8 @@ version = "12.8.4.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'darwin'", "python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'darwin'", "python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", ] wheels = [ @@ -3289,10 +3334,8 @@ version = "12.8.90" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'darwin'", "python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'darwin'", "python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", ] wheels = [ @@ -4010,6 +4053,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "portalocker" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" }, +] + [[package]] name = "prometheus-client" version = "0.24.1" @@ -4707,6 +4762,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, ] +[[package]] +name = "qdrant-client" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "httpx", extra = ["http2"] }, + { name = "numpy" }, + { name = "portalocker" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/45/5b1bdd15a3c7730eefb9c113600829e20d689b82b5a23f9e07d107094004/qdrant_client-1.18.0.tar.gz", hash = "sha256:52e8ece1a7d40519801bf0b70713bfa0f6b7ae28c7275bbe0b0286fbed7f6db4", size = 352580, upload-time = "2026-05-11T14:12:38.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/10/c437bd2ac41ef30d3019063e6ce537dc111e9214473b337ee88f7fa6359a/qdrant_client-1.18.0-py3-none-any.whl", hash = "sha256:093aa8cf8a420ee3ad2a68b007e1378d7992b2600e0b53c193fc172674f659cd", size = 398126, upload-time = "2026-05-11T14:12:36.998Z" }, +] + [[package]] name = "quack-kernels" version = "0.3.7" @@ -5987,7 +6060,8 @@ dependencies = [ all = [ { name = "bm25s" }, { name = "exa-py" }, - { name = "faiss-gpu-cu12" }, + { name = "faiss-cpu", marker = "sys_platform == 'darwin'" }, + { name = "faiss-gpu-cu12", marker = "sys_platform != 'darwin'" }, { name = "fastapi" }, { name = "infinity-emb" }, { name = "mineru", extra = ["core"] }, @@ -5996,6 +6070,7 @@ all = [ { name = "pydantic" }, { name = "pymilvus" }, { name = "pytrec-eval-terrier" }, + { name = "qdrant-client" }, { name = "rouge-score" }, { name = "sentence-transformers" }, { name = "tavily-python" }, @@ -6022,13 +6097,15 @@ generation = [ retriever = [ { name = "bm25s" }, { name = "exa-py" }, - { name = "faiss-gpu-cu12" }, + { name = "faiss-cpu", marker = "sys_platform == 'darwin'" }, + { name = "faiss-gpu-cu12", marker = "sys_platform != 'darwin'" }, { name = "fastapi" }, { name = "infinity-emb" }, { name = "numba" }, { name = "openai" }, { name = "pydantic" }, { name = "pymilvus" }, + { name = "qdrant-client" }, { name = "sentence-transformers" }, { name = "tavily-python" }, { name = "torch", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, @@ -6051,7 +6128,8 @@ requires-dist = [ { name = "charset-normalizer" }, { name = "chonkie" }, { name = "exa-py", marker = "extra == 'retriever'" }, - { name = "faiss-gpu-cu12", marker = "extra == 'retriever'" }, + { name = "faiss-cpu", marker = "sys_platform == 'darwin' and extra == 'retriever'" }, + { name = "faiss-gpu-cu12", marker = "sys_platform != 'darwin' and extra == 'retriever'" }, { name = "fakeredis" }, { name = "fastapi", marker = "extra == 'retriever'" }, { name = "fastmcp", specifier = ">=3.3.1" }, @@ -6079,6 +6157,7 @@ requires-dist = [ { name = "python-dotenv" }, { name = "pytrec-eval-terrier", marker = "extra == 'evaluation'" }, { name = "pyyaml" }, + { name = "qdrant-client", marker = "extra == 'retriever'" }, { name = "requests" }, { name = "rich" }, { name = "rouge-score", marker = "extra == 'evaluation'" },