aiagents4pharma 1.45.1__py3-none-any.whl → 1.46.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aiagents4pharma/talk2aiagents4pharma/configs/app/__init__.py +0 -0
- aiagents4pharma/talk2aiagents4pharma/configs/app/frontend/__init__.py +0 -0
- aiagents4pharma/talk2aiagents4pharma/configs/app/frontend/default.yaml +102 -0
- aiagents4pharma/talk2aiagents4pharma/configs/config.yaml +1 -0
- aiagents4pharma/talk2aiagents4pharma/tests/test_main_agent.py +144 -54
- aiagents4pharma/talk2biomodels/api/__init__.py +1 -1
- aiagents4pharma/talk2biomodels/configs/app/__init__.py +0 -0
- aiagents4pharma/talk2biomodels/configs/app/frontend/__init__.py +0 -0
- aiagents4pharma/talk2biomodels/configs/app/frontend/default.yaml +72 -0
- aiagents4pharma/talk2biomodels/configs/config.yaml +1 -0
- aiagents4pharma/talk2biomodels/tests/test_api.py +0 -30
- aiagents4pharma/talk2biomodels/tests/test_get_annotation.py +1 -1
- aiagents4pharma/talk2biomodels/tools/get_annotation.py +1 -10
- aiagents4pharma/talk2knowledgegraphs/configs/app/frontend/default.yaml +42 -26
- aiagents4pharma/talk2knowledgegraphs/configs/config.yaml +1 -0
- aiagents4pharma/talk2knowledgegraphs/configs/tools/multimodal_subgraph_extraction/default.yaml +4 -23
- aiagents4pharma/talk2knowledgegraphs/configs/utils/database/milvus/__init__.py +3 -0
- aiagents4pharma/talk2knowledgegraphs/configs/utils/database/milvus/default.yaml +61 -0
- aiagents4pharma/talk2knowledgegraphs/entrypoint.sh +1 -11
- aiagents4pharma/talk2knowledgegraphs/milvus_data_dump.py +11 -10
- aiagents4pharma/talk2knowledgegraphs/tests/test_agents_t2kg_agent.py +193 -73
- aiagents4pharma/talk2knowledgegraphs/tests/test_tools_milvus_multimodal_subgraph_extraction.py +1375 -667
- aiagents4pharma/talk2knowledgegraphs/tests/test_utils_database_milvus_connection_manager.py +812 -0
- aiagents4pharma/talk2knowledgegraphs/tests/test_utils_extractions_milvus_multimodal_pcst.py +723 -539
- aiagents4pharma/talk2knowledgegraphs/tools/milvus_multimodal_subgraph_extraction.py +474 -58
- aiagents4pharma/talk2knowledgegraphs/utils/database/__init__.py +5 -0
- aiagents4pharma/talk2knowledgegraphs/utils/database/milvus_connection_manager.py +586 -0
- aiagents4pharma/talk2knowledgegraphs/utils/extractions/milvus_multimodal_pcst.py +240 -8
- aiagents4pharma/talk2scholars/configs/app/frontend/default.yaml +67 -31
- {aiagents4pharma-1.45.1.dist-info → aiagents4pharma-1.46.1.dist-info}/METADATA +10 -1
- {aiagents4pharma-1.45.1.dist-info → aiagents4pharma-1.46.1.dist-info}/RECORD +33 -23
- aiagents4pharma/talk2biomodels/api/kegg.py +0 -87
- {aiagents4pharma-1.45.1.dist-info → aiagents4pharma-1.46.1.dist-info}/WHEEL +0 -0
- {aiagents4pharma-1.45.1.dist-info → aiagents4pharma-1.46.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,586 @@
|
|
1
|
+
"""
|
2
|
+
Milvus Connection Manager for Talk2KnowledgeGraphs.
|
3
|
+
|
4
|
+
This module provides centralized connection management for Milvus database,
|
5
|
+
removing the dependency on frontend session state and enabling proper
|
6
|
+
separation of concerns between frontend and backend.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import asyncio
|
10
|
+
import concurrent.futures
|
11
|
+
import logging
|
12
|
+
import threading
|
13
|
+
from dataclasses import dataclass
|
14
|
+
from typing import Any
|
15
|
+
|
16
|
+
import hydra
|
17
|
+
from pymilvus import AsyncMilvusClient, Collection, MilvusClient, connections, db
|
18
|
+
from pymilvus.exceptions import MilvusException
|
19
|
+
|
20
|
+
# Initialize logger
|
21
|
+
logging.basicConfig(level=logging.INFO)
|
22
|
+
logger = logging.getLogger(__name__)
|
23
|
+
|
24
|
+
|
25
|
+
@dataclass
|
26
|
+
class SearchParams:
|
27
|
+
"""Parameters for search operations."""
|
28
|
+
|
29
|
+
collection_name: str
|
30
|
+
data: list
|
31
|
+
anns_field: str
|
32
|
+
search_params: dict
|
33
|
+
limit: int
|
34
|
+
output_fields: list | None = None
|
35
|
+
|
36
|
+
|
37
|
+
@dataclass
|
38
|
+
class QueryParams:
|
39
|
+
"""Parameters for query operations."""
|
40
|
+
|
41
|
+
collection_name: str
|
42
|
+
expr: str
|
43
|
+
output_fields: list | None = None
|
44
|
+
limit: int | None = None
|
45
|
+
|
46
|
+
|
47
|
+
class MilvusConnectionManager:
|
48
|
+
"""
|
49
|
+
Centralized Milvus connection manager for backend tools with singleton pattern.
|
50
|
+
|
51
|
+
This class handles:
|
52
|
+
- Connection establishment and management
|
53
|
+
- Database switching
|
54
|
+
- Connection health checks
|
55
|
+
- Graceful error handling
|
56
|
+
- Thread-safe singleton pattern
|
57
|
+
|
58
|
+
Args:
|
59
|
+
cfg: Configuration object containing Milvus connection parameters
|
60
|
+
"""
|
61
|
+
|
62
|
+
_instances = {}
|
63
|
+
_lock = threading.Lock()
|
64
|
+
|
65
|
+
def __new__(cls, cfg: dict[str, Any]):
|
66
|
+
"""
|
67
|
+
Create singleton instance based on database configuration.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
cfg: Configuration dictionary containing Milvus DB settings
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
MilvusConnectionManager: Singleton instance for the given config
|
74
|
+
"""
|
75
|
+
# Create a unique key based on connection parameters
|
76
|
+
config_key = (
|
77
|
+
cfg.milvus_db.host,
|
78
|
+
int(cfg.milvus_db.port),
|
79
|
+
cfg.milvus_db.user,
|
80
|
+
cfg.milvus_db.database_name,
|
81
|
+
cfg.milvus_db.alias,
|
82
|
+
)
|
83
|
+
|
84
|
+
if config_key not in cls._instances:
|
85
|
+
with cls._lock:
|
86
|
+
# Double-check locking pattern
|
87
|
+
if config_key not in cls._instances:
|
88
|
+
instance = super().__new__(cls)
|
89
|
+
cls._instances[config_key] = instance
|
90
|
+
logger.info(
|
91
|
+
"Created new MilvusConnectionManager singleton for database: %s",
|
92
|
+
cfg.milvus_db.database_name,
|
93
|
+
)
|
94
|
+
else:
|
95
|
+
logger.debug(
|
96
|
+
"Reusing existing MilvusConnectionManager singleton for database: %s",
|
97
|
+
cfg.milvus_db.database_name,
|
98
|
+
)
|
99
|
+
|
100
|
+
return cls._instances[config_key]
|
101
|
+
|
102
|
+
def __init__(self, cfg: dict[str, Any]):
|
103
|
+
"""
|
104
|
+
Initialize the Milvus connection manager.
|
105
|
+
|
106
|
+
Args:
|
107
|
+
cfg: Configuration dictionary containing Milvus DB settings
|
108
|
+
"""
|
109
|
+
# Prevent re-initialization of singleton instance
|
110
|
+
if hasattr(self, "_initialized"):
|
111
|
+
return
|
112
|
+
|
113
|
+
self.cfg = cfg
|
114
|
+
self.alias = cfg.milvus_db.alias
|
115
|
+
self.host = cfg.milvus_db.host
|
116
|
+
self.port = int(cfg.milvus_db.port) # Ensure port is integer
|
117
|
+
self.user = cfg.milvus_db.user
|
118
|
+
self.password = cfg.milvus_db.password
|
119
|
+
self.database_name = cfg.milvus_db.database_name
|
120
|
+
|
121
|
+
# Thread lock for connection operations
|
122
|
+
self._connection_lock = threading.Lock()
|
123
|
+
|
124
|
+
# Initialize both sync and async clients
|
125
|
+
self._sync_client = None
|
126
|
+
self._async_client = None
|
127
|
+
|
128
|
+
# Mark as initialized
|
129
|
+
self._initialized = True
|
130
|
+
|
131
|
+
logger.info("MilvusConnectionManager initialized for database: %s", self.database_name)
|
132
|
+
|
133
|
+
def get_sync_client(self) -> MilvusClient:
|
134
|
+
"""
|
135
|
+
Get or create a synchronous MilvusClient.
|
136
|
+
|
137
|
+
Returns:
|
138
|
+
MilvusClient: Configured synchronous client
|
139
|
+
"""
|
140
|
+
if self._sync_client is None:
|
141
|
+
self._sync_client = MilvusClient(
|
142
|
+
uri=f"http://{self.host}:{self.port}",
|
143
|
+
token=f"{self.user}:{self.password}",
|
144
|
+
db_name=self.database_name,
|
145
|
+
)
|
146
|
+
logger.info("Created synchronous MilvusClient for database: %s", self.database_name)
|
147
|
+
return self._sync_client
|
148
|
+
|
149
|
+
def get_async_client(self) -> AsyncMilvusClient:
|
150
|
+
"""
|
151
|
+
Get or create an asynchronous AsyncMilvusClient.
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
AsyncMilvusClient: Configured asynchronous client
|
155
|
+
"""
|
156
|
+
if self._async_client is None:
|
157
|
+
try:
|
158
|
+
self._async_client = AsyncMilvusClient(
|
159
|
+
uri=f"http://{self.host}:{self.port}",
|
160
|
+
token=f"{self.user}:{self.password}",
|
161
|
+
db_name=self.database_name,
|
162
|
+
)
|
163
|
+
logger.info(
|
164
|
+
"Created asynchronous AsyncMilvusClient for database: %s",
|
165
|
+
self.database_name,
|
166
|
+
)
|
167
|
+
except (MilvusException, RuntimeError, ConnectionError, OSError) as e:
|
168
|
+
logger.error("Failed to create async client: %s", str(e))
|
169
|
+
# Don't raise here, let the calling method handle the fallback
|
170
|
+
return None
|
171
|
+
return self._async_client
|
172
|
+
|
173
|
+
def ensure_connection(self) -> bool:
|
174
|
+
"""
|
175
|
+
Ensure Milvus connection exists, create if not.
|
176
|
+
|
177
|
+
This method checks if a connection with the specified alias exists,
|
178
|
+
and creates one if it doesn't. It also switches to the correct database.
|
179
|
+
Thread-safe implementation with connection locking.
|
180
|
+
|
181
|
+
Returns:
|
182
|
+
bool: True if connection is established, False otherwise
|
183
|
+
|
184
|
+
Raises:
|
185
|
+
MilvusException: If connection cannot be established
|
186
|
+
"""
|
187
|
+
with self._connection_lock:
|
188
|
+
try:
|
189
|
+
# Check if connection already exists
|
190
|
+
if not connections.has_connection(self.alias):
|
191
|
+
logger.info("Creating new Milvus connection with alias: %s", self.alias)
|
192
|
+
connections.connect(
|
193
|
+
alias=self.alias,
|
194
|
+
host=self.host,
|
195
|
+
port=self.port,
|
196
|
+
user=self.user,
|
197
|
+
password=self.password,
|
198
|
+
)
|
199
|
+
logger.info(
|
200
|
+
"Successfully connected to Milvus at %s:%s",
|
201
|
+
self.host,
|
202
|
+
self.port,
|
203
|
+
)
|
204
|
+
else:
|
205
|
+
logger.debug("Milvus connection already exists with alias: %s", self.alias)
|
206
|
+
|
207
|
+
# Switch to the correct database
|
208
|
+
db.using_database(self.database_name)
|
209
|
+
logger.debug("Using Milvus database: %s", self.database_name)
|
210
|
+
|
211
|
+
return True
|
212
|
+
|
213
|
+
except MilvusException as e:
|
214
|
+
logger.error("Failed to establish Milvus connection: %s", str(e))
|
215
|
+
raise
|
216
|
+
except Exception as e:
|
217
|
+
logger.error("Unexpected error during Milvus connection: %s", str(e))
|
218
|
+
raise MilvusException(f"Connection failed: {str(e)}") from e
|
219
|
+
|
220
|
+
def get_connection_info(self) -> dict[str, Any]:
|
221
|
+
"""
|
222
|
+
Get current connection information.
|
223
|
+
|
224
|
+
Returns:
|
225
|
+
Dict containing connection details
|
226
|
+
"""
|
227
|
+
try:
|
228
|
+
if connections.has_connection(self.alias):
|
229
|
+
conn_addr = connections.get_connection_addr(self.alias)
|
230
|
+
return {
|
231
|
+
"alias": self.alias,
|
232
|
+
"host": self.host,
|
233
|
+
"port": self.port,
|
234
|
+
"database": self.database_name,
|
235
|
+
"connected": True,
|
236
|
+
"connection_address": conn_addr,
|
237
|
+
}
|
238
|
+
return {
|
239
|
+
"alias": self.alias,
|
240
|
+
"host": self.host,
|
241
|
+
"port": self.port,
|
242
|
+
"database": self.database_name,
|
243
|
+
"connected": False,
|
244
|
+
"connection_address": None,
|
245
|
+
}
|
246
|
+
except (MilvusException, RuntimeError, ConnectionError, OSError) as e:
|
247
|
+
logger.error("Error getting connection info: %s", str(e))
|
248
|
+
return {"alias": self.alias, "connected": False, "error": str(e)}
|
249
|
+
|
250
|
+
def test_connection(self) -> bool:
|
251
|
+
"""
|
252
|
+
Test the connection by attempting to list collections.
|
253
|
+
|
254
|
+
Returns:
|
255
|
+
bool: True if connection is healthy, False otherwise
|
256
|
+
"""
|
257
|
+
try:
|
258
|
+
self.ensure_connection()
|
259
|
+
|
260
|
+
# Try to get a collection to test the connection
|
261
|
+
test_collection_name = f"{self.database_name}_nodes"
|
262
|
+
Collection(name=test_collection_name)
|
263
|
+
|
264
|
+
logger.debug("Connection test successful")
|
265
|
+
return True
|
266
|
+
|
267
|
+
except (MilvusException, RuntimeError, ConnectionError, OSError) as e:
|
268
|
+
logger.error("Connection test failed: %s", str(e))
|
269
|
+
return False
|
270
|
+
|
271
|
+
def disconnect(self) -> bool:
|
272
|
+
"""
|
273
|
+
Disconnect from Milvus (both sync and async clients).
|
274
|
+
|
275
|
+
Returns:
|
276
|
+
bool: True if disconnected successfully, False otherwise
|
277
|
+
"""
|
278
|
+
try:
|
279
|
+
success = True
|
280
|
+
|
281
|
+
# Disconnect sync client
|
282
|
+
if connections.has_connection(self.alias):
|
283
|
+
connections.disconnect(self.alias)
|
284
|
+
logger.info("Disconnected sync connection with alias: %s", self.alias)
|
285
|
+
|
286
|
+
# Close async client if it exists
|
287
|
+
if self._async_client is not None:
|
288
|
+
try:
|
289
|
+
# Check if we can close the async client properly
|
290
|
+
try:
|
291
|
+
loop = asyncio.get_running_loop()
|
292
|
+
# If there's a running loop, create a task
|
293
|
+
loop.create_task(self._async_client.close())
|
294
|
+
except RuntimeError:
|
295
|
+
# No running loop, use asyncio.run in a thread
|
296
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
297
|
+
executor.submit(lambda: asyncio.run(self._async_client.close())).result(
|
298
|
+
timeout=5
|
299
|
+
)
|
300
|
+
|
301
|
+
self._async_client = None
|
302
|
+
logger.info("Closed async client for database: %s", self.database_name)
|
303
|
+
except (TimeoutError, RuntimeError) as e:
|
304
|
+
logger.warning("Error closing async client: %s", str(e))
|
305
|
+
# Still clear the reference even if close failed
|
306
|
+
self._async_client = None
|
307
|
+
success = False
|
308
|
+
|
309
|
+
# Clear sync client reference
|
310
|
+
if self._sync_client is not None:
|
311
|
+
self._sync_client = None
|
312
|
+
logger.info("Cleared sync client reference")
|
313
|
+
|
314
|
+
return success
|
315
|
+
|
316
|
+
except (MilvusException, RuntimeError, ConnectionError, OSError) as e:
|
317
|
+
logger.error("Error disconnecting from Milvus: %s", str(e))
|
318
|
+
return False
|
319
|
+
|
320
|
+
def get_collection(self, collection_name: str) -> Collection:
|
321
|
+
"""
|
322
|
+
Get a Milvus collection, ensuring connection is established.
|
323
|
+
Thread-safe implementation.
|
324
|
+
|
325
|
+
Args:
|
326
|
+
collection_name: Name of the collection to retrieve
|
327
|
+
|
328
|
+
Returns:
|
329
|
+
Collection: The requested Milvus collection
|
330
|
+
|
331
|
+
Raises:
|
332
|
+
MilvusException: If collection cannot be retrieved
|
333
|
+
"""
|
334
|
+
try:
|
335
|
+
self.ensure_connection()
|
336
|
+
collection = Collection(name=collection_name)
|
337
|
+
collection.load() # Load collection data
|
338
|
+
logger.debug("Successfully loaded collection: %s", collection_name)
|
339
|
+
return collection
|
340
|
+
|
341
|
+
except Exception as e:
|
342
|
+
logger.error("Failed to get collection %s: %s", collection_name, str(e))
|
343
|
+
raise MilvusException(f"Failed to get collection {collection_name}: {str(e)}") from e
|
344
|
+
|
345
|
+
async def async_search(self, params: SearchParams) -> list:
|
346
|
+
"""
|
347
|
+
Perform asynchronous vector search.
|
348
|
+
|
349
|
+
Args:
|
350
|
+
params: SearchParams object containing all search parameters
|
351
|
+
|
352
|
+
Returns:
|
353
|
+
List of search results
|
354
|
+
"""
|
355
|
+
try:
|
356
|
+
async_client = self.get_async_client()
|
357
|
+
if async_client is None:
|
358
|
+
raise MilvusException("Failed to create async client")
|
359
|
+
|
360
|
+
# Ensure collection is loaded before searching
|
361
|
+
await async_client.load_collection(collection_name=params.collection_name)
|
362
|
+
|
363
|
+
results = await async_client.search(
|
364
|
+
collection_name=params.collection_name,
|
365
|
+
data=params.data,
|
366
|
+
anns_field=params.anns_field,
|
367
|
+
search_params=params.search_params,
|
368
|
+
limit=params.limit,
|
369
|
+
output_fields=params.output_fields or [],
|
370
|
+
)
|
371
|
+
logger.debug("Async search completed for collection: %s", params.collection_name)
|
372
|
+
return results
|
373
|
+
except MilvusException as e:
|
374
|
+
logger.warning(
|
375
|
+
"Async search failed for collection %s: %s, falling back to sync",
|
376
|
+
params.collection_name,
|
377
|
+
str(e),
|
378
|
+
)
|
379
|
+
# Fallback to sync operation
|
380
|
+
return await asyncio.to_thread(self._sync_search, params)
|
381
|
+
|
382
|
+
def _sync_search(self, params: SearchParams) -> list:
|
383
|
+
"""Sync fallback for search operations."""
|
384
|
+
try:
|
385
|
+
collection = Collection(name=params.collection_name)
|
386
|
+
collection.load()
|
387
|
+
results = collection.search(
|
388
|
+
data=params.data,
|
389
|
+
anns_field=params.anns_field,
|
390
|
+
param=params.search_params,
|
391
|
+
limit=params.limit,
|
392
|
+
output_fields=params.output_fields or [],
|
393
|
+
)
|
394
|
+
logger.debug(
|
395
|
+
"Sync fallback search completed for collection: %s",
|
396
|
+
params.collection_name,
|
397
|
+
)
|
398
|
+
return results
|
399
|
+
except Exception as e:
|
400
|
+
logger.error(
|
401
|
+
"Sync fallback search failed for collection %s: %s",
|
402
|
+
params.collection_name,
|
403
|
+
str(e),
|
404
|
+
)
|
405
|
+
raise MilvusException(f"Search failed (sync fallback): {str(e)}") from e
|
406
|
+
|
407
|
+
async def async_query(self, params: QueryParams) -> list:
|
408
|
+
"""
|
409
|
+
Perform asynchronous query with sync fallback.
|
410
|
+
|
411
|
+
Args:
|
412
|
+
params: QueryParams object containing all query parameters
|
413
|
+
|
414
|
+
Returns:
|
415
|
+
List of query results
|
416
|
+
"""
|
417
|
+
try:
|
418
|
+
async_client = self.get_async_client()
|
419
|
+
if async_client is None:
|
420
|
+
raise MilvusException("Failed to create async client")
|
421
|
+
|
422
|
+
# Ensure collection is loaded before querying
|
423
|
+
await async_client.load_collection(collection_name=params.collection_name)
|
424
|
+
|
425
|
+
results = await async_client.query(
|
426
|
+
collection_name=params.collection_name,
|
427
|
+
filter=params.expr,
|
428
|
+
output_fields=params.output_fields or [],
|
429
|
+
limit=params.limit,
|
430
|
+
)
|
431
|
+
logger.debug("Async query completed for collection: %s", params.collection_name)
|
432
|
+
return results
|
433
|
+
except MilvusException as e:
|
434
|
+
logger.warning(
|
435
|
+
"Async query failed for collection %s: %s, falling back to sync",
|
436
|
+
params.collection_name,
|
437
|
+
str(e),
|
438
|
+
)
|
439
|
+
# Fallback to sync operation
|
440
|
+
return await asyncio.to_thread(self._sync_query, params)
|
441
|
+
|
442
|
+
def _sync_query(self, params: QueryParams) -> list:
|
443
|
+
"""Sync fallback for query operations."""
|
444
|
+
try:
|
445
|
+
collection = Collection(name=params.collection_name)
|
446
|
+
collection.load()
|
447
|
+
results = collection.query(
|
448
|
+
expr=params.expr,
|
449
|
+
output_fields=params.output_fields or [],
|
450
|
+
limit=params.limit,
|
451
|
+
)
|
452
|
+
logger.debug(
|
453
|
+
"Sync fallback query completed for collection: %s",
|
454
|
+
params.collection_name,
|
455
|
+
)
|
456
|
+
return results
|
457
|
+
except Exception as e:
|
458
|
+
logger.error(
|
459
|
+
"Sync fallback query failed for collection %s: %s",
|
460
|
+
params.collection_name,
|
461
|
+
str(e),
|
462
|
+
)
|
463
|
+
raise MilvusException(f"Query failed (sync fallback): {str(e)}") from e
|
464
|
+
|
465
|
+
async def async_load_collection(self, collection_name: str) -> bool:
|
466
|
+
"""
|
467
|
+
Asynchronously load a collection.
|
468
|
+
|
469
|
+
Args:
|
470
|
+
collection_name: Name of the collection to load
|
471
|
+
|
472
|
+
Returns:
|
473
|
+
bool: True if loaded successfully
|
474
|
+
"""
|
475
|
+
try:
|
476
|
+
async_client = self.get_async_client()
|
477
|
+
await async_client.load_collection(collection_name=collection_name)
|
478
|
+
logger.debug("Async load completed for collection: %s", collection_name)
|
479
|
+
return True
|
480
|
+
except Exception as e:
|
481
|
+
logger.error("Async load failed for collection %s: %s", collection_name, str(e))
|
482
|
+
raise MilvusException(f"Async load failed: {str(e)}") from e
|
483
|
+
|
484
|
+
async def async_get_collection_stats(self, collection_name: str) -> dict:
|
485
|
+
"""
|
486
|
+
Get collection statistics asynchronously.
|
487
|
+
|
488
|
+
Args:
|
489
|
+
collection_name: Name of the collection
|
490
|
+
|
491
|
+
Returns:
|
492
|
+
dict: Collection statistics
|
493
|
+
"""
|
494
|
+
try:
|
495
|
+
# Note: Using sync client methods through asyncio.to_thread as fallback
|
496
|
+
# since AsyncMilvusClient might not have all stat methods
|
497
|
+
stats = await asyncio.to_thread(lambda: Collection(name=collection_name).num_entities)
|
498
|
+
return {"num_entities": stats}
|
499
|
+
except Exception as e:
|
500
|
+
logger.error(
|
501
|
+
"Failed to get async collection stats for %s: %s",
|
502
|
+
collection_name,
|
503
|
+
str(e),
|
504
|
+
)
|
505
|
+
raise MilvusException(f"Failed to get collection stats: {str(e)}") from e
|
506
|
+
|
507
|
+
@classmethod
|
508
|
+
def get_instance(cls, cfg: dict[str, Any]) -> "MilvusConnectionManager":
|
509
|
+
"""
|
510
|
+
Get singleton instance for the given configuration.
|
511
|
+
|
512
|
+
Args:
|
513
|
+
cfg: Configuration dictionary containing Milvus DB settings
|
514
|
+
|
515
|
+
Returns:
|
516
|
+
MilvusConnectionManager: Singleton instance for the given config
|
517
|
+
"""
|
518
|
+
return cls(cfg)
|
519
|
+
|
520
|
+
@classmethod
|
521
|
+
def clear_instances(cls):
|
522
|
+
"""
|
523
|
+
Clear all singleton instances. Useful for testing or cleanup.
|
524
|
+
"""
|
525
|
+
with cls._lock:
|
526
|
+
# Disconnect all existing connections before clearing
|
527
|
+
for instance in cls._instances.values():
|
528
|
+
instance.disconnect()
|
529
|
+
cls._instances.clear()
|
530
|
+
logger.info("Cleared all MilvusConnectionManager singleton instances")
|
531
|
+
|
532
|
+
@classmethod
|
533
|
+
def from_config(cls, cfg: dict[str, Any]) -> "MilvusConnectionManager":
|
534
|
+
"""
|
535
|
+
Create a MilvusConnectionManager from configuration.
|
536
|
+
|
537
|
+
Args:
|
538
|
+
cfg: Configuration object or dictionary
|
539
|
+
|
540
|
+
Returns:
|
541
|
+
MilvusConnectionManager: Configured connection manager instance
|
542
|
+
"""
|
543
|
+
return cls(cfg)
|
544
|
+
|
545
|
+
@classmethod
|
546
|
+
def from_hydra_config(
|
547
|
+
cls,
|
548
|
+
config_path: str = "../configs",
|
549
|
+
config_name: str = "config",
|
550
|
+
overrides: list | None = None,
|
551
|
+
) -> "MilvusConnectionManager":
|
552
|
+
"""
|
553
|
+
Create a MilvusConnectionManager from Hydra configuration.
|
554
|
+
|
555
|
+
This method loads the Milvus database configuration using Hydra,
|
556
|
+
providing complete backend separation from frontend configs.
|
557
|
+
|
558
|
+
Args:
|
559
|
+
config_path: Path to the configs directory
|
560
|
+
config_name: Name of the main config file
|
561
|
+
overrides: List of config overrides
|
562
|
+
|
563
|
+
Returns:
|
564
|
+
MilvusConnectionManager: Configured connection manager instance
|
565
|
+
|
566
|
+
Example:
|
567
|
+
# Load with default database config
|
568
|
+
conn_manager = MilvusConnectionManager.from_hydra_config()
|
569
|
+
|
570
|
+
# Load with specific overrides
|
571
|
+
conn_manager = MilvusConnectionManager.from_hydra_config(
|
572
|
+
overrides=["utils/database/milvus=default"]
|
573
|
+
)
|
574
|
+
"""
|
575
|
+
if overrides is None:
|
576
|
+
overrides = ["utils/database/milvus=default"]
|
577
|
+
|
578
|
+
try:
|
579
|
+
with hydra.initialize(version_base=None, config_path=config_path):
|
580
|
+
cfg_all = hydra.compose(config_name=config_name, overrides=overrides)
|
581
|
+
cfg = cfg_all.utils.database.milvus # Extract utils.database.milvus section
|
582
|
+
logger.info("Loaded Milvus config from Hydra with overrides: %s", overrides)
|
583
|
+
return cls(cfg)
|
584
|
+
except Exception as e:
|
585
|
+
logger.error("Failed to load Hydra configuration: %s", str(e))
|
586
|
+
raise MilvusException(f"Configuration loading failed: {str(e)}") from e
|