iris-vector-rag 0.2.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 (50) hide show
  1. adapters/rag_templates_bridge.py +578 -0
  2. common/__init__.py +1 -0
  3. common/batch_utils.py +294 -0
  4. common/config.py +58 -0
  5. common/connection_manager.py +242 -0
  6. common/connection_pool.py +445 -0
  7. common/database.py +141 -0
  8. common/database_schema_manager.py +289 -0
  9. common/db_init_with_indexes.py +181 -0
  10. common/db_vector_utils.py +181 -0
  11. common/environment_manager.py +220 -0
  12. common/huggingface_utils.py +206 -0
  13. common/iris_client.py +92 -0
  14. common/iris_connection_manager.py +411 -0
  15. common/iris_connection_pool.py +360 -0
  16. common/iris_connector.py +71 -0
  17. common/iris_dbapi_connector.py +323 -0
  18. common/iris_index_utils.py +195 -0
  19. common/iris_sql_utils.py +704 -0
  20. common/iris_stream_reader.py +166 -0
  21. common/llm_cache_config.py +308 -0
  22. common/llm_cache_iris.py +671 -0
  23. common/llm_cache_manager.py +911 -0
  24. common/utils.py +559 -0
  25. common/vector_sql_utils.py +584 -0
  26. evaluation_framework/__init__.py +66 -0
  27. evaluation_framework/biomedical_question_generator.py +826 -0
  28. evaluation_framework/comparative_analysis_system.py +1173 -0
  29. evaluation_framework/empirical_reporting.py +1298 -0
  30. evaluation_framework/evaluation_orchestrator.py +1123 -0
  31. evaluation_framework/execute_production_evaluation.py +382 -0
  32. evaluation_framework/pmc_data_pipeline.py +1332 -0
  33. evaluation_framework/production_evaluation_config.py +353 -0
  34. evaluation_framework/ragas_metrics_framework.py +1013 -0
  35. evaluation_framework/real_production_evaluation.py +544 -0
  36. evaluation_framework/run_10k_evaluation.py +175 -0
  37. evaluation_framework/simple_test.py +364 -0
  38. evaluation_framework/statistical_evaluation_methodology.py +1016 -0
  39. evaluation_framework/test_framework.py +281 -0
  40. evaluation_framework/test_iris_connectivity.py +244 -0
  41. evaluation_framework/test_small_scale_evaluation.py +432 -0
  42. evaluation_framework/true_e2e_evaluation.py +639 -0
  43. evaluation_framework/visualization_engine.py +585 -0
  44. iris_rag/__init__.py +284 -0
  45. iris_rag/config/default_config.yaml +295 -0
  46. iris_vector_rag-0.2.1.dist-info/METADATA +499 -0
  47. iris_vector_rag-0.2.1.dist-info/RECORD +50 -0
  48. iris_vector_rag-0.2.1.dist-info/WHEEL +5 -0
  49. iris_vector_rag-0.2.1.dist-info/entry_points.txt +2 -0
  50. iris_vector_rag-0.2.1.dist-info/top_level.txt +4 -0
common/utils.py ADDED
@@ -0,0 +1,559 @@
1
+ # common/utils.py
2
+ import logging # Added for logger usage in get_llm_func
3
+
4
+ # import sqlalchemy # Removed - not needed for IRIS
5
+ import os
6
+ import time
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path # Added for config path
9
+ from typing import Any, Callable, Dict, List, Optional, Tuple # Added Tuple
10
+
11
+ import numpy as np
12
+ import yaml # Added for config loading
13
+
14
+ logger = logging.getLogger(__name__) # Added for logger usage
15
+
16
+ # --- Config Loading ---
17
+ _config = None
18
+
19
+
20
+ def load_config():
21
+ global _config
22
+ if _config is None:
23
+ try:
24
+ config_path = Path(__file__).parent.parent / "config" / "config.yaml"
25
+ with open(config_path, "r") as f:
26
+ _config = yaml.safe_load(f)
27
+ logger.info(f"Successfully loaded config from {config_path}")
28
+ except Exception as e:
29
+ logger.error(
30
+ f"Failed to load config/config.yaml: {e}. Using default values."
31
+ )
32
+ _config = {} # Ensure _config is not None
33
+ return _config
34
+
35
+
36
+ def get_config_value(key_path: str, default: Any = None) -> Any:
37
+ config = load_config()
38
+ keys = key_path.split(".")
39
+ value = config
40
+ try:
41
+ for key in keys:
42
+ value = value[key]
43
+ return value
44
+ except (KeyError, TypeError):
45
+ logger.warning(
46
+ f"Config key '{key_path}' not found. Returning default: {default}"
47
+ )
48
+ return default
49
+
50
+
51
+ # --- Dataclasses ---
52
+ @dataclass
53
+ class Document:
54
+ id: str
55
+ content: str
56
+ score: Optional[float] = None # For similarity score from retrieval
57
+ metadata: Optional[Dict[str, Any]] = None # For title or other metadata
58
+ embedding: Optional[List[float]] = field(
59
+ default=None, repr=False
60
+ ) # Standard document embedding
61
+
62
+ def to_dict(self, include_embeddings: bool = False) -> Dict[str, Any]:
63
+ data = {
64
+ "id": self.id,
65
+ "content": self.content,
66
+ "score": float(self.score) if self.score is not None else None,
67
+ }
68
+ if include_embeddings:
69
+ data["embedding"] = self.embedding
70
+ return data
71
+
72
+
73
+ # --- Model and Connector Wrappers ---
74
+
75
+ _llm_instance = None
76
+ _current_llm_key = None # For caching LLM instance
77
+
78
+ DEFAULT_EMBEDDING_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
79
+ DEFAULT_EMBEDDING_DIMENSION = 384
80
+
81
+ # New pure HuggingFace embedder
82
+ _hf_embedder_cache = {}
83
+
84
+
85
+ def build_hf_embedder(model_name: str):
86
+ """
87
+ Builds an embedding function using HuggingFace transformers directly.
88
+ Includes tokenization, model inference, mean pooling, and normalization.
89
+ Caches tokenizer and model per model_name.
90
+ """
91
+ global _hf_embedder_cache
92
+ import torch
93
+
94
+ if model_name not in _hf_embedder_cache:
95
+ from common.huggingface_utils import download_huggingface_model
96
+
97
+ print(f"Initializing HF embedder for model: {model_name}")
98
+ tokenizer, model = download_huggingface_model(model_name)
99
+ model.eval() # Set to evaluation mode
100
+ # Consider model.to(device) if GPU is available/desired
101
+ _hf_embedder_cache[model_name] = (tokenizer, model)
102
+ else:
103
+ print(f"Using cached HF embedder for model: {model_name}")
104
+
105
+ tokenizer, model = _hf_embedder_cache[model_name]
106
+
107
+ # Removed LRU cache to prevent memory leaks during bulk operations
108
+ # Cache causes indefinite memory growth when processing thousands of unique texts
109
+ def _embed_single_text(text: str) -> List[float]:
110
+ with torch.no_grad():
111
+ # Validate input text
112
+ if not text or not text.strip():
113
+ logger.warning("Empty or whitespace-only text provided for embedding")
114
+ # Use configured dimension for zero vector
115
+ dimension = get_config_value(
116
+ "embedding_model.dimension", DEFAULT_EMBEDDING_DIMENSION
117
+ )
118
+ return [0.0] * dimension
119
+
120
+ try:
121
+ inputs = tokenizer(
122
+ text,
123
+ return_tensors="pt",
124
+ truncation=True,
125
+ padding=True,
126
+ max_length=512,
127
+ )
128
+ outputs = model(**inputs)
129
+ token_embeddings = outputs.last_hidden_state
130
+ attention_mask = inputs.attention_mask
131
+ input_mask_expanded = (
132
+ attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
133
+ )
134
+ sum_embeddings = torch.sum(token_embeddings * input_mask_expanded, 1)
135
+ sum_mask = torch.clamp(input_mask_expanded.sum(1), min=1e-9)
136
+ pooled_embedding = sum_embeddings / sum_mask
137
+ normalized_embedding = torch.nn.functional.normalize(
138
+ pooled_embedding, p=2, dim=1
139
+ )
140
+
141
+ # Convert to numpy for NaN/inf checking
142
+ embedding_array = normalized_embedding[0].cpu().numpy()
143
+
144
+ # Check for NaN or inf values and fix them
145
+ if np.any(np.isnan(embedding_array)) or np.any(
146
+ np.isinf(embedding_array)
147
+ ):
148
+ logger.warning(
149
+ f"NaN/inf values detected in embedding for text: {text[:50]}..."
150
+ )
151
+ embedding_array = np.nan_to_num(
152
+ embedding_array, nan=0.0, posinf=1.0, neginf=-1.0
153
+ )
154
+ # Re-normalize after fixing
155
+ norm = np.linalg.norm(embedding_array)
156
+ if norm > 0:
157
+ embedding_array = embedding_array / norm
158
+ else:
159
+ embedding_array = np.zeros_like(embedding_array)
160
+
161
+ return embedding_array.tolist()
162
+
163
+ except Exception as e:
164
+ logger.error(
165
+ f"Error generating embedding for text '{text[:50]}...': {e}"
166
+ )
167
+ dimension = get_config_value(
168
+ "embedding_model.dimension", DEFAULT_EMBEDDING_DIMENSION
169
+ )
170
+ return [0.0] * dimension
171
+
172
+ def embedding_func_hf(texts: List[str]) -> List[List[float]]:
173
+ # Add memory pressure detection for bulk operations
174
+ if len(texts) > 10: # Bulk operation detected
175
+ try:
176
+ import gc
177
+
178
+ import psutil
179
+
180
+ process = psutil.Process()
181
+ memory_mb = process.memory_info().rss / 1024 / 1024
182
+
183
+ # If memory usage exceeds 2GB, force cleanup
184
+ if memory_mb > 2048:
185
+ logger.warning(
186
+ f"High memory usage detected: {memory_mb:.1f} MB. Forcing cleanup..."
187
+ )
188
+ gc.collect()
189
+ # Clear torch cache if available
190
+ try:
191
+ import torch
192
+
193
+ if torch.cuda.is_available():
194
+ torch.cuda.empty_cache()
195
+ except ImportError:
196
+ pass
197
+ except ImportError:
198
+ pass # psutil not available
199
+
200
+ return [_embed_single_text(t) for t in texts]
201
+
202
+ return embedding_func_hf
203
+
204
+
205
+ def get_embedding_func(
206
+ model_name_override: Optional[str] = None,
207
+ provider: Optional[str] = None,
208
+ mock: bool = False,
209
+ ) -> Callable:
210
+ """
211
+ Returns a function that takes either a single text string or a list of texts and returns embeddings.
212
+ Reads model name from config/config.yaml, with override option.
213
+ Supports a "stub" provider or mock=True for testing without real models.
214
+
215
+ The returned function handles both:
216
+ - Single string: embedding_func("text") -> List[float]
217
+ - List of strings: embedding_func(["text1", "text2"]) -> List[List[float]]
218
+ """
219
+ effective_model_name = model_name_override or get_config_value(
220
+ "embedding_model.name", DEFAULT_EMBEDDING_MODEL_NAME
221
+ )
222
+ dimension = get_config_value(
223
+ "embedding_model.dimension", DEFAULT_EMBEDDING_DIMENSION
224
+ )
225
+
226
+ if mock or provider == "stub" or effective_model_name == "stub":
227
+ logger.info(f"Using stub embedding function with dimension {dimension}.")
228
+
229
+ def stub_embed_texts(texts) -> List[List[float]]:
230
+ # Handle both single string and list of strings
231
+ if isinstance(texts, str):
232
+ return [(len(texts) % 100) * 0.01] * dimension
233
+ return [[(len(text) % 100) * 0.01] * dimension for text in texts]
234
+
235
+ return stub_embed_texts
236
+
237
+ logger.info(f"Using pure HuggingFace embedder for model: {effective_model_name}")
238
+ base_embedder = build_hf_embedder(effective_model_name)
239
+
240
+ def flexible_embedder(texts):
241
+ """Wrapper that handles both single strings and lists of strings"""
242
+ if isinstance(texts, str):
243
+ # Single string input - return single embedding
244
+ result = base_embedder([texts])
245
+ return result[0] if result else [0.0] * dimension
246
+ else:
247
+ # List input - return list of embeddings
248
+ return base_embedder(texts)
249
+
250
+ return flexible_embedder
251
+
252
+
253
+ def get_llm_func(
254
+ provider: str = "openai",
255
+ model_name: str = "gpt-4.1-mini",
256
+ enable_cache: Optional[bool] = None,
257
+ **kwargs,
258
+ ) -> Callable[[str], str]:
259
+ """
260
+ Returns a function that takes a prompt string and returns an LLM completion string.
261
+ Supports 'openai' or a 'stub' for testing.
262
+
263
+ Args:
264
+ provider: LLM provider (openai, stub, etc.)
265
+ model_name: Model name
266
+ enable_cache: Override cache enable/disable (None = use config default)
267
+ **kwargs: Additional LLM parameters
268
+
269
+ Returns:
270
+ LLM function (automatically cached if enabled)
271
+ """
272
+ global _llm_instance, _current_llm_key
273
+
274
+ # Setup caching if enabled
275
+ cache_enabled = _setup_caching_if_needed(enable_cache)
276
+
277
+ llm_key = f"{provider}_{model_name}_{cache_enabled}"
278
+
279
+ if provider == "openai" and not os.getenv("OPENAI_API_KEY"):
280
+ try:
281
+ from dotenv import find_dotenv, load_dotenv
282
+
283
+ project_root = os.path.abspath(
284
+ os.path.join(os.path.dirname(__file__), "..")
285
+ )
286
+ dotenv_path = os.path.join(project_root, ".env")
287
+ if os.path.exists(dotenv_path):
288
+ logger.info(
289
+ f"Attempting to load .env file from {dotenv_path} in get_llm_func."
290
+ )
291
+ load_dotenv(dotenv_path=dotenv_path, override=True)
292
+ else:
293
+ env_path_found = find_dotenv(usecwd=True)
294
+ if env_path_found:
295
+ logger.info(
296
+ f"Attempting to load .env file from found path {env_path_found} in get_llm_func."
297
+ )
298
+ load_dotenv(dotenv_path=env_path_found, override=True)
299
+ else:
300
+ logger.warning(
301
+ "No .env file found by find_dotenv() in get_llm_func."
302
+ )
303
+ except ImportError:
304
+ if not logger.hasHandlers():
305
+ logging.basicConfig(level=logging.INFO)
306
+ logger.warning(
307
+ "python-dotenv not installed. Cannot load .env file in get_llm_func."
308
+ )
309
+ except Exception as e_dotenv:
310
+ if not logger.hasHandlers():
311
+ logging.basicConfig(level=logging.INFO)
312
+ logger.warning(f"Error loading .env file in get_llm_func: {e_dotenv}")
313
+
314
+ if _llm_instance is None or _current_llm_key != llm_key:
315
+ print(f"Initializing LLM: {provider} - {model_name}")
316
+ if provider == "openai":
317
+ try:
318
+ from langchain_openai import ChatOpenAI
319
+ except ImportError:
320
+ raise ImportError(
321
+ "LangChain OpenAI library not found. Please install with `poetry add langchain-openai`."
322
+ )
323
+
324
+ api_key = os.getenv("OPENAI_API_KEY")
325
+ if not api_key:
326
+ raise ValueError(
327
+ "OPENAI_API_KEY environment variable not set or found in .env file."
328
+ )
329
+
330
+ _llm_instance = ChatOpenAI(
331
+ model_name=model_name, openai_api_key=api_key, **kwargs
332
+ )
333
+ _current_llm_key = llm_key
334
+
335
+ elif provider == "stub":
336
+
337
+ class StubLLM:
338
+ def __init__(self, model_name, **kwargs):
339
+ self.model_name = model_name
340
+
341
+ def invoke(self, prompt: str, **kwargs) -> Any:
342
+ response_content = (
343
+ f"Stub LLM response for prompt: '{prompt[:50]}...'"
344
+ )
345
+
346
+ class AIMessage:
347
+ def __init__(self, content):
348
+ self.content = content
349
+
350
+ return AIMessage(content=response_content)
351
+
352
+ _llm_instance = StubLLM(model_name=model_name, **kwargs)
353
+ _current_llm_key = llm_key
354
+ else:
355
+ raise ValueError(f"Unsupported LLM provider: {provider}")
356
+
357
+ def query_llm(prompt: str) -> str:
358
+ response = _llm_instance.invoke(prompt)
359
+ # Ensure the response is always a string
360
+ if hasattr(response, "content"):
361
+ return str(response.content)
362
+ return str(response)
363
+
364
+ return query_llm
365
+
366
+
367
+ def get_iris_connector(db_url: Optional[str] = None):
368
+ if db_url is None:
369
+ db_url = os.getenv("IRIS_CONNECTION_URL")
370
+ if not db_url:
371
+ raise ValueError(
372
+ "IRIS_CONNECTION_URL environment variable not set and db_url not provided."
373
+ )
374
+
375
+ print(f"Connecting to IRIS at: {db_url}")
376
+ try:
377
+ import sqlalchemy
378
+
379
+ engine = sqlalchemy.create_engine(db_url)
380
+ connection = engine.connect()
381
+ return connection
382
+ except Exception as e:
383
+ print(f"Failed to connect to IRIS: {e}")
384
+ raise
385
+
386
+
387
+ def timing_decorator(func: Callable) -> Callable:
388
+ def wrapper(*args, **kwargs):
389
+ start_time = time.perf_counter()
390
+ result = func(*args, **kwargs)
391
+ end_time = time.perf_counter()
392
+ print(
393
+ f"Function {func.__name__} executed in {end_time - start_time:.4f} seconds"
394
+ )
395
+ if isinstance(result, dict) and "latency_ms" not in result:
396
+ result["latency_ms"] = (end_time - start_time) * 1000
397
+ return result
398
+
399
+ return wrapper
400
+
401
+
402
+ # ... (Embedded Python specific utilities can remain as they are) ...
403
+
404
+ _iris_connector_embedded = None
405
+ _embedding_model_embedded = None
406
+ _llm_embedded = None
407
+
408
+
409
+ def get_iris_connector_for_embedded():
410
+ global _iris_connector_embedded
411
+ if _iris_connector_embedded is None:
412
+ try:
413
+ try:
414
+ import iris
415
+ except ImportError:
416
+ raise ImportError(
417
+ "IRIS Embedded Python module 'iris' not found. Ensure it is installed in your environment."
418
+ )
419
+ _iris_connector_embedded = iris.connect()
420
+ print("IRIS Embedded Python: DBAPI connection established.")
421
+ except ImportError:
422
+ print("IRIS Embedded Python: 'iris' module not found.")
423
+ _iris_connector_embedded = None
424
+ except Exception as e:
425
+ print(f"IRIS Embedded Python: Error connecting to DB: {e}")
426
+ _iris_connector_embedded = None
427
+ return _iris_connector_embedded
428
+
429
+
430
+ def get_embedding_func_for_embedded(model_name_override: Optional[str] = None):
431
+ global _embedding_model_embedded
432
+ if _embedding_model_embedded is None:
433
+ effective_model_name = model_name_override or get_config_value(
434
+ "embedding_model.name", DEFAULT_EMBEDDING_MODEL_NAME
435
+ )
436
+ dimension = get_config_value(
437
+ "embedding_model.dimension", DEFAULT_EMBEDDING_DIMENSION
438
+ )
439
+ print(
440
+ f"IRIS Embedded Python: Loading embedding model {effective_model_name} (dim: {dimension})"
441
+ )
442
+ # This would call build_hf_embedder or similar for embedded context
443
+ _embedding_model_embedded = lambda texts: [[0.1] * dimension for _ in texts]
444
+ return _embedding_model_embedded
445
+
446
+
447
+ def get_llm_func_for_embedded(provider: str = "stub", model_name: str = "stub-model"):
448
+ global _llm_embedded
449
+ if _llm_embedded is None:
450
+ print(f"IRIS Embedded Python: Initializing LLM {provider} - {model_name}")
451
+ if provider == "stub":
452
+ _llm_embedded = lambda prompt: f"Embedded Stub LLM: {prompt[:30]}"
453
+ else:
454
+ _llm_embedded = lambda prompt: "Error: LLM not configured for embedded"
455
+ return _llm_embedded
456
+
457
+
458
+ if __name__ == "__main__":
459
+ print("Testing common.utils...")
460
+ doc = Document(id="test_doc_001", content="This is a test document.")
461
+ print(f"Created Document: {doc}")
462
+
463
+ try:
464
+ embed_func = get_embedding_func()
465
+ config_model_name = get_config_value(
466
+ "embedding_model.name", DEFAULT_EMBEDDING_MODEL_NAME
467
+ )
468
+ config_dimension = get_config_value(
469
+ "embedding_model.dimension", DEFAULT_EMBEDDING_DIMENSION
470
+ )
471
+ print(
472
+ f"Testing with embedding model: {config_model_name}, dimension: {config_dimension}"
473
+ )
474
+ sample_texts = ["Hello world", "This is a test."]
475
+ embeddings = embed_func(sample_texts)
476
+ print(
477
+ f"Embeddings generated for {len(embeddings)} texts. First embedding dim: {len(embeddings[0]) if embeddings else 'N/A'}"
478
+ )
479
+ assert len(embeddings) == 2
480
+ if (
481
+ embeddings and embeddings[0] is not None
482
+ ): # Check if embedding was successful
483
+ assert len(embeddings[0]) == config_dimension
484
+ except ImportError as e:
485
+ print(f"Skipping embedding test: {e}")
486
+ except Exception as e:
487
+ print(f"Error during embedding test: {e}")
488
+
489
+ try:
490
+ llm_stub_func = get_llm_func(provider="stub")
491
+ response_stub = llm_stub_func("Test prompt for stub LLM")
492
+ print(f"Stub LLM Response: {response_stub}")
493
+ assert "Stub LLM response" in response_stub
494
+ except Exception as e:
495
+ print(f"Error during stub LLM test: {e}")
496
+
497
+ if os.getenv("OPENAI_API_KEY"):
498
+ try:
499
+ llm_openai_func = get_llm_func(
500
+ provider="openai", model_name="gpt-3.5-turbo"
501
+ )
502
+ response_openai = llm_openai_func("What is 1+1?")
503
+ print(f"OpenAI LLM Response: {response_openai}")
504
+ assert response_openai is not None
505
+ except ImportError as e:
506
+ print(f"Skipping OpenAI LLM test: {e}")
507
+ except ValueError as e:
508
+ print(f"Skipping OpenAI LLM test due to config error: {e}")
509
+ except Exception as e:
510
+ print(f"Error during OpenAI LLM test: {e}")
511
+ else:
512
+ print("Skipping OpenAI LLM test: OPENAI_API_KEY not set.")
513
+
514
+ @timing_decorator
515
+ def example_timed_function(duration):
516
+ time.sleep(duration)
517
+ return {"status": "complete", "slept_for": duration}
518
+
519
+ timed_result = example_timed_function(0.1)
520
+ print(f"Timed function result: {timed_result}")
521
+ assert "latency_ms" in timed_result
522
+ assert timed_result["latency_ms"] > 0
523
+
524
+ print("common.utils tests finished.")
525
+
526
+
527
+ def _setup_caching_if_needed(enable_cache: Optional[bool] = None) -> bool:
528
+ """
529
+ Setup LLM caching if needed and return whether caching is enabled.
530
+
531
+ Args:
532
+ enable_cache: Override cache enable/disable (None = use config default)
533
+
534
+ Returns:
535
+ True if caching is enabled, False otherwise
536
+ """
537
+ try:
538
+ from common.llm_cache_config import load_cache_config
539
+ from common.llm_cache_manager import (
540
+ is_langchain_cache_configured,
541
+ setup_langchain_cache,
542
+ )
543
+
544
+ # Load configuration
545
+ config = load_cache_config()
546
+
547
+ # Determine if caching should be enabled
548
+ cache_enabled = enable_cache if enable_cache is not None else config.enabled
549
+
550
+ # Setup cache if enabled and not already configured
551
+ if cache_enabled and not is_langchain_cache_configured():
552
+ setup_langchain_cache(config)
553
+ logger.info("LLM caching initialized")
554
+
555
+ return cache_enabled
556
+
557
+ except Exception as e:
558
+ logger.warning(f"Failed to setup LLM caching: {e}")
559
+ return False