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.
Files changed (34) hide show
  1. aiagents4pharma/talk2aiagents4pharma/configs/app/__init__.py +0 -0
  2. aiagents4pharma/talk2aiagents4pharma/configs/app/frontend/__init__.py +0 -0
  3. aiagents4pharma/talk2aiagents4pharma/configs/app/frontend/default.yaml +102 -0
  4. aiagents4pharma/talk2aiagents4pharma/configs/config.yaml +1 -0
  5. aiagents4pharma/talk2aiagents4pharma/tests/test_main_agent.py +144 -54
  6. aiagents4pharma/talk2biomodels/api/__init__.py +1 -1
  7. aiagents4pharma/talk2biomodels/configs/app/__init__.py +0 -0
  8. aiagents4pharma/talk2biomodels/configs/app/frontend/__init__.py +0 -0
  9. aiagents4pharma/talk2biomodels/configs/app/frontend/default.yaml +72 -0
  10. aiagents4pharma/talk2biomodels/configs/config.yaml +1 -0
  11. aiagents4pharma/talk2biomodels/tests/test_api.py +0 -30
  12. aiagents4pharma/talk2biomodels/tests/test_get_annotation.py +1 -1
  13. aiagents4pharma/talk2biomodels/tools/get_annotation.py +1 -10
  14. aiagents4pharma/talk2knowledgegraphs/configs/app/frontend/default.yaml +42 -26
  15. aiagents4pharma/talk2knowledgegraphs/configs/config.yaml +1 -0
  16. aiagents4pharma/talk2knowledgegraphs/configs/tools/multimodal_subgraph_extraction/default.yaml +4 -23
  17. aiagents4pharma/talk2knowledgegraphs/configs/utils/database/milvus/__init__.py +3 -0
  18. aiagents4pharma/talk2knowledgegraphs/configs/utils/database/milvus/default.yaml +61 -0
  19. aiagents4pharma/talk2knowledgegraphs/entrypoint.sh +1 -11
  20. aiagents4pharma/talk2knowledgegraphs/milvus_data_dump.py +11 -10
  21. aiagents4pharma/talk2knowledgegraphs/tests/test_agents_t2kg_agent.py +193 -73
  22. aiagents4pharma/talk2knowledgegraphs/tests/test_tools_milvus_multimodal_subgraph_extraction.py +1375 -667
  23. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_database_milvus_connection_manager.py +812 -0
  24. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_extractions_milvus_multimodal_pcst.py +723 -539
  25. aiagents4pharma/talk2knowledgegraphs/tools/milvus_multimodal_subgraph_extraction.py +474 -58
  26. aiagents4pharma/talk2knowledgegraphs/utils/database/__init__.py +5 -0
  27. aiagents4pharma/talk2knowledgegraphs/utils/database/milvus_connection_manager.py +586 -0
  28. aiagents4pharma/talk2knowledgegraphs/utils/extractions/milvus_multimodal_pcst.py +240 -8
  29. aiagents4pharma/talk2scholars/configs/app/frontend/default.yaml +67 -31
  30. {aiagents4pharma-1.45.1.dist-info → aiagents4pharma-1.46.1.dist-info}/METADATA +10 -1
  31. {aiagents4pharma-1.45.1.dist-info → aiagents4pharma-1.46.1.dist-info}/RECORD +33 -23
  32. aiagents4pharma/talk2biomodels/api/kegg.py +0 -87
  33. {aiagents4pharma-1.45.1.dist-info → aiagents4pharma-1.46.1.dist-info}/WHEEL +0 -0
  34. {aiagents4pharma-1.45.1.dist-info → aiagents4pharma-1.46.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,5 @@
1
+ """Database utilities for Talk2KnowledgeGraphs."""
2
+
3
+ from .milvus_connection_manager import MilvusConnectionManager
4
+
5
+ __all__ = ["MilvusConnectionManager"]
@@ -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