From 82883a3b8e1924a2484c0d46aa8cfa878870011c Mon Sep 17 00:00:00 2001 From: fancy Date: Fri, 21 Nov 2025 15:58:38 +0800 Subject: [PATCH 1/4] docs: update .env.example with comprehensive variables and comments --- docker/.env.example | 208 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 161 insertions(+), 47 deletions(-) diff --git a/docker/.env.example b/docker/.env.example index 0f4fcb65d..037eb8db8 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -1,60 +1,174 @@ -# MemOS Environment Variables Configuration -TZ=Asia/Shanghai +# MemOS Environment Variables (core runtime) +# Legend: [required] needed for default startup; others are optional or conditional per comments. -MOS_CUBE_PATH="/tmp/data_test" # Path to memory storage (e.g. /tmp/data_test) -MOS_ENABLE_DEFAULT_CUBE_CONFIG="true" # Enable default cube config (true/false) +## Base +TZ=Asia/Shanghai +ENV_NAME=PLAYGROUND_OFFLINE # Tag shown in DingTalk notifications (e.g., PROD_ONLINE/TEST); no runtime effect unless ENABLE_DINGDING_BOT=true +MOS_CUBE_PATH=/tmp/data_test # local data path +MEMOS_BASE_PATH=. # CLI/SDK cache path +MOS_ENABLE_DEFAULT_CUBE_CONFIG=true # enable default cube config +MOS_ENABLE_REORGANIZE=false # enable memory reorg +MOS_TEXT_MEM_TYPE=general_text # general_text | tree_text +ASYNC_MODE=sync # async/sync, used in default cube config -# OpenAI Configuration -OPENAI_API_KEY="sk-xxx" # Your OpenAI API key -OPENAI_API_BASE="http://xxx" # OpenAI API base URL (default: https://api.openai.com/v1) +## User/session defaults +MOS_USER_ID=root +MOS_SESSION_ID=default_session +MOS_MAX_TURNS_WINDOW=20 +MOS_TOP_K=50 -# MemOS Chat Model Configuration +## Chat LLM (main dialogue) MOS_CHAT_MODEL=gpt-4o-mini MOS_CHAT_TEMPERATURE=0.8 MOS_MAX_TOKENS=8000 MOS_TOP_P=0.9 -MOS_TOP_K=50 -MOS_CHAT_MODEL_PROVIDER=openai - -# graph db -# neo4j -NEO4J_BACKEND=xxx -NEO4J_URI=bolt://xxx -NEO4J_USER=xxx -NEO4J_PASSWORD=xxx -MOS_NEO4J_SHARED_DB=xxx -NEO4J_DB_NAME=xxx - -# tetxmem reog -MOS_ENABLE_REORGANIZE=false - -# MemOS User Configuration -MOS_USER_ID=root -MOS_SESSION_ID=default_session -MOS_MAX_TURNS_WINDOW=20 +MOS_CHAT_MODEL_PROVIDER=openai # openai | huggingface | vllm +MOS_MODEL_SCHEMA=memos.configs.llm.VLLMLLMConfig # vllm only: config class path; keep default unless you extend it +OPENAI_API_KEY=sk-xxx # [required] when provider=openai +OPENAI_API_BASE=https://api.openai.com/v1 # [required] base for the key +OPENAI_BASE_URL= # compatibility for eval/scheduler +VLLM_API_KEY= # required when provider=vllm +VLLM_API_BASE=http://localhost:8088/v1 # required when provider=vllm -# MemRader Configuration +## MemReader / retrieval LLM MEMRADER_MODEL=gpt-4o-mini -MEMRADER_API_KEY=sk-xxx -MEMRADER_API_BASE=http://xxx:3000/v1 +MEMRADER_API_KEY=sk-xxx # [required] can reuse OPENAI_API_KEY +MEMRADER_API_BASE=http://localhost:3000/v1 # [required] base for the key MEMRADER_MAX_TOKENS=5000 -#embedding & rerank +## Embedding & rerank EMBEDDING_DIMENSION=1024 -MOS_EMBEDDER_BACKEND=universal_api -MOS_EMBEDDER_MODEL=bge-m3 -MOS_EMBEDDER_API_BASE=http://xxx -MOS_EMBEDDER_API_KEY=EMPTY -MOS_RERANKER_BACKEND=http_bge -MOS_RERANKER_URL=http://xxx -# Ollama Configuration (for embeddings) -#OLLAMA_API_BASE=http://xxx - -# milvus for pref mem -MILVUS_URI=http://xxx -MILVUS_USER_NAME=xxx -MILVUS_PASSWORD=xxx - -# pref mem +MOS_EMBEDDER_BACKEND=universal_api # universal_api | ollama +MOS_EMBEDDER_PROVIDER=openai # required when universal_api +MOS_EMBEDDER_MODEL=bge-m3 # siliconflow → use BAAI/bge-m3 +MOS_EMBEDDER_API_BASE=http://localhost:8000/v1 # required when universal_api +MOS_EMBEDDER_API_KEY=EMPTY # required when universal_api +OLLAMA_API_BASE=http://localhost:11434 # required when backend=ollama +MOS_RERANKER_BACKEND=http_bge # http_bge | http_bge_strategy | cosine_local +MOS_RERANKER_URL=http://localhost:8001 # required when backend=http_bge* +MOS_RERANKER_MODEL=bge-reranker-v2-m3 # siliconflow → use BAAI/bge-reranker-v2-m3 +MOS_RERANKER_HEADERS_EXTRA= # extra headers, JSON string +MOS_RERANKER_STRATEGY=single_turn +MOS_RERANK_SOURCE= # optional rerank scope, e.g., history/stream/custom + +## Internet search & preference memory +ENABLE_INTERNET=false +BOCHA_API_KEY= # required if ENABLE_INTERNET=true +SEARCH_MODE=fast # fast | fine | mixture +FAST_GRAPH=false +BM25_CALL=false +VEC_COT_CALL=false +FINE_STRATEGY=rewrite # rewrite | recreate | deep_search +ENABLE_ACTIVATION_MEMORY=false ENABLE_PREFERENCE_MEMORY=true -RETURN_ORIGINAL_PREF_MEM=true +PREFERENCE_ADDER_MODE=fast # fast | safe +DEDUP_PREF_EXP_BY_TEXTUAL=false + +## Reader chunking +MEM_READER_BACKEND=simple_struct # simple_struct | strategy_struct +MEM_READER_CHAT_CHUNK_TYPE=default # default | content_length +MEM_READER_CHAT_CHUNK_TOKEN_SIZE=1600 # tokens per chunk (default mode) +MEM_READER_CHAT_CHUNK_SESS_SIZE=10 # sessions per chunk (default mode) +MEM_READER_CHAT_CHUNK_OVERLAP=2 # overlap between chunks + +## Scheduler (MemScheduler / API) +MOS_ENABLE_SCHEDULER=false +MOS_SCHEDULER_TOP_K=10 +MOS_SCHEDULER_ACT_MEM_UPDATE_INTERVAL=300 +MOS_SCHEDULER_CONTEXT_WINDOW_SIZE=5 +MOS_SCHEDULER_THREAD_POOL_MAX_WORKERS=10000 +MOS_SCHEDULER_CONSUME_INTERVAL_SECONDS=0.01 +MOS_SCHEDULER_ENABLE_PARALLEL_DISPATCH=true +MOS_SCHEDULER_ENABLE_ACTIVATION_MEMORY=false +API_SCHEDULER_ON=true +API_SEARCH_WINDOW_SIZE=5 +API_SEARCH_HISTORY_TURNS=5 + +## Graph / vector stores +NEO4J_BACKEND=neo4j-community # neo4j-community | neo4j | nebular | polardb +NEO4J_URI=bolt://localhost:7687 # required when backend=neo4j* +NEO4J_USER=neo4j # required when backend=neo4j* +NEO4J_PASSWORD=12345678 # required when backend=neo4j* +NEO4J_DB_NAME=neo4j # required for shared-db mode +MOS_NEO4J_SHARED_DB=false +QDRANT_HOST=localhost +QDRANT_PORT=6333 +MILVUS_URI=http://localhost:19530 # required when ENABLE_PREFERENCE_MEMORY=true +MILVUS_USER_NAME=root # same as above +MILVUS_PASSWORD=12345678 # same as above +NEBULAR_HOSTS=["localhost"] +NEBULAR_USER=root +NEBULAR_PASSWORD=xxxxxx +NEBULAR_SPACE=shared-tree-textual-memory +NEBULAR_WORKING_MEMORY=20 +NEBULAR_LONGTERM_MEMORY=1000000 +NEBULAR_USER_MEMORY=1000000 + +## Relational DB (user manager / PolarDB) +MOS_USER_MANAGER_BACKEND=sqlite # sqlite | mysql +MYSQL_HOST=localhost # required when backend=mysql +MYSQL_PORT=3306 +MYSQL_USERNAME=root +MYSQL_PASSWORD=12345678 +MYSQL_DATABASE=memos_users +MYSQL_CHARSET=utf8mb4 +POLAR_DB_HOST=localhost +POLAR_DB_PORT=5432 +POLAR_DB_USER=root +POLAR_DB_PASSWORD=123456 +POLAR_DB_DB_NAME=shared_memos_db +POLAR_DB_USE_MULTI_DB=false + +## Redis (scheduler queue) — fill only if you want scheduler queues in Redis; otherwise in-memory queue is used +REDIS_HOST=localhost # global Redis endpoint (preferred over MEMSCHEDULER_*) +REDIS_PORT=6379 +REDIS_DB=0 +REDIS_PASSWORD= +REDIS_SOCKET_TIMEOUT= +REDIS_SOCKET_CONNECT_TIMEOUT= +MEMSCHEDULER_REDIS_HOST= # fallback keys if not using the global ones +MEMSCHEDULER_REDIS_PORT= +MEMSCHEDULER_REDIS_DB= +MEMSCHEDULER_REDIS_PASSWORD= +MEMSCHEDULER_REDIS_TIMEOUT= +MEMSCHEDULER_REDIS_CONNECT_TIMEOUT= + +## MemScheduler LLM +MEMSCHEDULER_OPENAI_API_KEY= # LLM key for scheduler’s own calls (OpenAI-compatible); leave empty if scheduler not using LLM +MEMSCHEDULER_OPENAI_BASE_URL= # Base URL for the above; can reuse OPENAI_API_BASE +MEMSCHEDULER_OPENAI_DEFAULT_MODEL=gpt-4o-mini + +## Nacos (optional config center) +NACOS_ENABLE_WATCH=false +NACOS_WATCH_INTERVAL=60 +NACOS_SERVER_ADDR= +NACOS_DATA_ID= +NACOS_GROUP=DEFAULT_GROUP +NACOS_NAMESPACE= +AK= +SK= + +## DingTalk bot & OSS upload +ENABLE_DINGDING_BOT=false # set true -> fields below required +DINGDING_ACCESS_TOKEN_USER= +DINGDING_SECRET_USER= +DINGDING_ACCESS_TOKEN_ERROR= +DINGDING_SECRET_ERROR= +DINGDING_ROBOT_CODE= +DINGDING_APP_KEY= +DINGDING_APP_SECRET= +OSS_ENDPOINT= # bot image upload depends on OSS +OSS_REGION= +OSS_BUCKET_NAME= +OSS_ACCESS_KEY_ID= +OSS_ACCESS_KEY_SECRET= +OSS_PUBLIC_BASE_URL= + +## Logging / external sink +CUSTOM_LOGGER_URL= +CUSTOM_LOGGER_TOKEN= +CUSTOM_LOGGER_WORKERS=2 + +## SDK / external client +MEMOS_API_KEY= +MEMOS_BASE_URL=https://memos.memtensor.cn/api/openmem/v1 From 439ed49e6d6fff5c63ed3576bbb7eaa3c1c915b9 Mon Sep 17 00:00:00 2001 From: fridayL Date: Fri, 21 Nov 2025 17:50:35 +0800 Subject: [PATCH 2/4] hotfix:hotfix --- src/memos/api/product_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/memos/api/product_models.py b/src/memos/api/product_models.py index 30df150ea..f7f0304c7 100644 --- a/src/memos/api/product_models.py +++ b/src/memos/api/product_models.py @@ -201,8 +201,8 @@ class APIADDRequest(BaseRequest): operation: list[PermissionDict] | None = Field( None, description="operation ids for multi cubes" ) - async_mode: Literal["async", "sync"] = Field( - "async", description="Whether to add memory in async mode" + async_mode: Literal["async", "sync"] | None = Field( + None, description="Whether to add memory in async mode" ) From 9e48ac2cdff32b80a35ab0bec5768d2343a2b1e3 Mon Sep 17 00:00:00 2001 From: fancy Date: Mon, 24 Nov 2025 17:38:34 +0800 Subject: [PATCH 3/4] feat(qdrant):support qdrant cloud and add index --- docker/.env.example | 5 ++- docs/product-api-tests.md | 65 ++++++++++++++++++++++++++++++++ src/memos/api/config.py | 3 ++ src/memos/configs/vec_db.py | 5 ++- src/memos/reranker/factory.py | 12 +++++- src/memos/vec_dbs/qdrant.py | 71 ++++++++++++++++++++++++++--------- tests/configs/test_vec_db.py | 21 ++++++++++- tests/vec_dbs/test_qdrant.py | 25 ++++++++++++ 8 files changed, 185 insertions(+), 22 deletions(-) create mode 100644 docs/product-api-tests.md diff --git a/docker/.env.example b/docker/.env.example index 037eb8db8..ddbedfa48 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -47,7 +47,7 @@ OLLAMA_API_BASE=http://localhost:11434 # required when backend=ollama MOS_RERANKER_BACKEND=http_bge # http_bge | http_bge_strategy | cosine_local MOS_RERANKER_URL=http://localhost:8001 # required when backend=http_bge* MOS_RERANKER_MODEL=bge-reranker-v2-m3 # siliconflow → use BAAI/bge-reranker-v2-m3 -MOS_RERANKER_HEADERS_EXTRA= # extra headers, JSON string +MOS_RERANKER_HEADERS_EXTRA= # extra headers, JSON string, e.g. {"Authorization":"Bearer your_token"} MOS_RERANKER_STRATEGY=single_turn MOS_RERANK_SOURCE= # optional rerank scope, e.g., history/stream/custom @@ -93,6 +93,9 @@ NEO4J_DB_NAME=neo4j # required for shared-db mode MOS_NEO4J_SHARED_DB=false QDRANT_HOST=localhost QDRANT_PORT=6333 +# For Qdrant Cloud / remote endpoint (takes priority if set): +QDRANT_URL= +QDRANT_API_KEY= MILVUS_URI=http://localhost:19530 # required when ENABLE_PREFERENCE_MEMORY=true MILVUS_USER_NAME=root # same as above MILVUS_PASSWORD=12345678 # same as above diff --git a/docs/product-api-tests.md b/docs/product-api-tests.md new file mode 100644 index 000000000..cff807e0e --- /dev/null +++ b/docs/product-api-tests.md @@ -0,0 +1,65 @@ +## Product API smoke tests (local 0.0.0.0:8001) + +Source: https://github.com/MemTensor/MemOS/issues/518 + +### Prerequisites +- Service is running: `python -m uvicorn memos.api.server_api:app --host 0.0.0.0 --port 8001` +- `.env` is configured for Redis, embeddings, and the vector DB (current test setup: Redis reachable, Qdrant Cloud connected). + +### 1) /product/add +- Purpose: Write a memory (sync/async). +- Example request (sync): + + ```bash + curl -s -X POST http://127.0.0.1:8001/product/add \ + -H 'Content-Type: application/json' \ + -d '{ + "user_id": "tester", + "mem_cube_id": "default_cube", + "memory_content": "Apple is a fruit rich in fiber.", + "async_mode": "sync" + }' + ``` + +- Observed result: `200`, message: "Memory added successfully", returns the written `memory_id` and related info. + +### 2) /product/get_all +- Purpose: List all memories for the user/type to confirm writes. +- Example request: + + ```bash + curl -s -X POST http://127.0.0.1:8001/product/get_all \ + -H 'Content-Type: application/json' \ + -d '{ + "user_id": "tester", + "memory_type": "text_mem", + "mem_cube_ids": ["default_cube"] + }' + ``` + +- Observed result: `200`, shows the recently written apple memories (WorkingMemory/LongTermMemory/UserMemory present, `vector_sync=success`). + +### 3) /product/search +- Purpose: Vector search memories. +- Example request: + + ```bash + curl -s -X POST http://127.0.0.1:8001/product/search \ + -H 'Content-Type: application/json' \ + -d '{ + "query": "What fruit is rich in fiber?", + "user_id": "tester", + "mem_cube_id": "default_cube", + "top_k": 5, + "pref_top_k": 3, + "include_preference": false + }' + ``` + +- Observed result: previously returned 400 because payload indexes (e.g., `vector_sync`) were missing in Qdrant. Index creation is now automatic during Qdrant initialization (memory_type/status/vector_sync/user_name). +- If results are empty or errors persist, verify indexes exist (auto-created on restart) or recreate/clean the collection. + +### Notes / Next steps +- `/product/add` and `/product/get_all` are healthy. +- `/product/search` still returns empty results even with vectors present; likely related to search filters or vector retrieval. +- Suggested follow-ups: inspect `SearchHandler` flow, filter conditions (user_id/session/cube_name), and vector DB search calls; capture logs or compare with direct `VecDBFactory.search` calls. diff --git a/src/memos/api/config.py b/src/memos/api/config.py index a276fa63d..5f924f1ad 100644 --- a/src/memos/api/config.py +++ b/src/memos/api/config.py @@ -497,6 +497,9 @@ def get_neo4j_community_config(user_id: str | None = None) -> dict[str, Any]: "distance_metric": "cosine", "host": os.getenv("QDRANT_HOST", "localhost"), "port": int(os.getenv("QDRANT_PORT", "6333")), + "path": os.getenv("QDRANT_PATH"), + "url": os.getenv("QDRANT_URL"), + "api_key": os.getenv("QDRANT_API_KEY"), }, }, } diff --git a/src/memos/configs/vec_db.py b/src/memos/configs/vec_db.py index dd1748714..9fdb83a35 100644 --- a/src/memos/configs/vec_db.py +++ b/src/memos/configs/vec_db.py @@ -27,10 +27,13 @@ class QdrantVecDBConfig(BaseVecDBConfig): host: str | None = Field(default=None, description="Host for Qdrant") port: int | None = Field(default=None, description="Port for Qdrant") path: str | None = Field(default=None, description="Path for Qdrant") + url: str | None = Field(default=None, description="Qdrant Cloud/remote endpoint URL") + api_key: str | None = Field(default=None, description="Qdrant Cloud API key") @model_validator(mode="after") def set_default_path(self): - if all(x is None for x in (self.host, self.port, self.path)): + # Only fall back to embedded/local path when no remote host/port/path/url is provided. + if all(x is None for x in (self.host, self.port, self.path, self.url)): logger.warning( "No host, port, or path provided for Qdrant. Defaulting to local path: %s", settings.MEMOS_DIR / "qdrant", diff --git a/src/memos/reranker/factory.py b/src/memos/reranker/factory.py index 57460a4af..d2c50ba5e 100644 --- a/src/memos/reranker/factory.py +++ b/src/memos/reranker/factory.py @@ -1,6 +1,7 @@ # memos/reranker/factory.py from __future__ import annotations +import json from typing import TYPE_CHECKING, Any # Import singleton decorator @@ -28,12 +29,19 @@ def from_config(cfg: RerankerConfigFactory | None) -> BaseReranker | None: backend = (cfg.backend or "").lower() c: dict[str, Any] = cfg.config or {} + headers_extra = c.get("headers_extra") + if isinstance(headers_extra, str): + try: + headers_extra = json.loads(headers_extra) + except Exception: + headers_extra = None + if backend in {"http_bge", "bge"}: return HTTPBGEReranker( reranker_url=c.get("url") or c.get("endpoint") or c.get("reranker_url"), model=c.get("model", "bge-reranker-v2-m3"), timeout=int(c.get("timeout", 10)), - headers_extra=c.get("headers_extra"), + headers_extra=headers_extra, rerank_source=c.get("rerank_source"), ) @@ -51,7 +59,7 @@ def from_config(cfg: RerankerConfigFactory | None) -> BaseReranker | None: reranker_url=c.get("url") or c.get("endpoint") or c.get("reranker_url"), model=c.get("model", "bge-reranker-v2-m3"), timeout=int(c.get("timeout", 10)), - headers_extra=c.get("headers_extra"), + headers_extra=headers_extra, rerank_source=c.get("rerank_source"), reranker_strategy=c.get("reranker_strategy"), ) diff --git a/src/memos/vec_dbs/qdrant.py b/src/memos/vec_dbs/qdrant.py index a0ebf1d80..633cd3580 100644 --- a/src/memos/vec_dbs/qdrant.py +++ b/src/memos/vec_dbs/qdrant.py @@ -23,24 +23,49 @@ def __init__(self, config: QdrantVecDBConfig): from qdrant_client import QdrantClient self.config = config + # Default payload fields we always index because query filters rely on them + self._default_payload_index_fields = [ + "memory_type", + "status", + "vector_sync", + "user_name", + ] - # If both host and port are None, we are running in local mode - if self.config.host is None and self.config.port is None: - logger.warning( - "Qdrant is running in local mode (host and port are both None). " - "In local mode, there may be race conditions during concurrent reads/writes. " - "It is strongly recommended to deploy a standalone Qdrant server " - "(e.g., via Docker: https://qdrant.tech/documentation/quickstart/)." + client_kwargs: dict[str, Any] = {} + if self.config.url: + client_kwargs["url"] = self.config.url + if self.config.api_key: + client_kwargs["api_key"] = self.config.api_key + else: + client_kwargs.update( + { + "host": self.config.host, + "port": self.config.port, + "path": self.config.path, + } ) - self.client = QdrantClient( - host=self.config.host, port=self.config.port, path=self.config.path - ) + # If both host and port are None, we are running in local/embedded mode + if self.config.host is None and self.config.port is None: + logger.warning( + "Qdrant is running in local mode (host and port are both None). " + "In local mode, there may be race conditions during concurrent reads/writes. " + "It is strongly recommended to deploy a standalone Qdrant server " + "(e.g., via Docker: https://qdrant.tech/documentation/quickstart/)." + ) + + self.client = QdrantClient(**client_kwargs) self.create_collection() + # Ensure common payload indexes exist (idempotent) + try: + self.ensure_payload_indexes(self._default_payload_index_fields) + except Exception as e: + logger.warning(f"Failed to ensure default payload indexes: {e}") def create_collection(self) -> None: """Create a new collection with specified parameters.""" from qdrant_client.http import models + from qdrant_client.http.exceptions import UnexpectedResponse if self.collection_exists(self.config.collection_name): collection_info = self.client.get_collection(self.config.collection_name) @@ -57,13 +82,25 @@ def create_collection(self) -> None: "dot": models.Distance.DOT, } - self.client.create_collection( - collection_name=self.config.collection_name, - vectors_config=models.VectorParams( - size=self.config.vector_dimension, - distance=distance_map[self.config.distance_metric], - ), - ) + try: + self.client.create_collection( + collection_name=self.config.collection_name, + vectors_config=models.VectorParams( + size=self.config.vector_dimension, + distance=distance_map[self.config.distance_metric], + ), + ) + except UnexpectedResponse as err: + # Cloud Qdrant returns 409 when the collection already exists; tolerate and continue. + if getattr(err, "status_code", None) == 409 or "already exists" in str(err).lower(): + logger.warning( + f"Collection '{self.config.collection_name}' already exists. Skipping creation." + ) + return + raise + except Exception: + # Bubble up other exceptions so callers can observe failures + raise logger.info( f"Collection '{self.config.collection_name}' created with {self.config.vector_dimension} dimensions." diff --git a/tests/configs/test_vec_db.py b/tests/configs/test_vec_db.py index b41e775af..850ffdd2c 100644 --- a/tests/configs/test_vec_db.py +++ b/tests/configs/test_vec_db.py @@ -40,7 +40,15 @@ def test_qdrant_vec_db_config(): required_fields=[ "collection_name", ], - optional_fields=["vector_dimension", "distance_metric", "host", "port", "path"], + optional_fields=[ + "vector_dimension", + "distance_metric", + "host", + "port", + "path", + "url", + "api_key", + ], ) check_config_instantiation_valid( @@ -53,6 +61,17 @@ def test_qdrant_vec_db_config(): }, ) + check_config_instantiation_valid( + QdrantVecDBConfig, + { + "collection_name": "test_collection", + "vector_dimension": 768, + "distance_metric": "cosine", + "url": "https://cloud.qdrant.example", + "api_key": "dummy", + }, + ) + check_config_instantiation_invalid(QdrantVecDBConfig) diff --git a/tests/vec_dbs/test_qdrant.py b/tests/vec_dbs/test_qdrant.py index 828240ae1..caeef46e4 100644 --- a/tests/vec_dbs/test_qdrant.py +++ b/tests/vec_dbs/test_qdrant.py @@ -113,3 +113,28 @@ def test_get_all(vec_db): results = vec_db.get_all() assert len(results) == 1 assert isinstance(results[0], VecDBItem) + + +def test_qdrant_client_cloud_init(): + config = VectorDBConfigFactory.model_validate( + { + "backend": "qdrant", + "config": { + "collection_name": "cloud_collection", + "vector_dimension": 3, + "distance_metric": "cosine", + "url": "https://cloud.qdrant.example", + "api_key": "secret-key", + }, + } + ) + + with patch("qdrant_client.QdrantClient") as mockclient: + mock_instance = mockclient.return_value + mock_instance.get_collection.side_effect = Exception("Not found") + + VecDBFactory.from_config(config) + + mockclient.assert_called_once_with( + url="https://cloud.qdrant.example", api_key="secret-key" + ) From 8ec9becbdfed9607fd1b1747ea077ae3e357fc17 Mon Sep 17 00:00:00 2001 From: fancy Date: Mon, 24 Nov 2025 17:47:11 +0800 Subject: [PATCH 4/4] chore: format qdrant test --- tests/vec_dbs/test_qdrant.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/vec_dbs/test_qdrant.py b/tests/vec_dbs/test_qdrant.py index caeef46e4..f4bd276c3 100644 --- a/tests/vec_dbs/test_qdrant.py +++ b/tests/vec_dbs/test_qdrant.py @@ -135,6 +135,4 @@ def test_qdrant_client_cloud_init(): VecDBFactory.from_config(config) - mockclient.assert_called_once_with( - url="https://cloud.qdrant.example", api_key="secret-key" - ) + mockclient.assert_called_once_with(url="https://cloud.qdrant.example", api_key="secret-key")