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
@@ -0,0 +1,1123 @@
1
+ """
2
+ Reproducible Evaluation Orchestration System for Biomedical RAG Pipeline Comparison
3
+
4
+ This module provides a comprehensive orchestration system that coordinates all evaluation
5
+ components to deliver reproducible, end-to-end pipeline comparisons with statistical rigor.
6
+ It serves as the main entry point for conducting large-scale biomedical RAG evaluations.
7
+
8
+ Key Features:
9
+ 1. End-to-end evaluation orchestration
10
+ 2. Reproducible experiment management
11
+ 3. Configuration-driven workflows
12
+ 4. Progress monitoring and checkpointing
13
+ 5. Automated result reporting
14
+ 6. Multi-experiment comparison
15
+ 7. Integration with existing infrastructure
16
+ 8. Comprehensive logging and error handling
17
+ """
18
+
19
+ import argparse
20
+ import asyncio
21
+ import hashlib
22
+ import json
23
+ import logging
24
+ import shutil
25
+ import subprocess
26
+ import sys
27
+ import time
28
+ from concurrent.futures import ThreadPoolExecutor
29
+ from dataclasses import asdict, dataclass, field
30
+ from datetime import datetime
31
+ from pathlib import Path
32
+ from typing import Any, Dict, List, Optional, Tuple, Union
33
+
34
+ import numpy as np
35
+ import pandas as pd
36
+ import yaml
37
+ from biomedical_question_generator import (
38
+ BiomedicalQuestionGenerator,
39
+ QuestionGenerationConfig,
40
+ create_biomedical_question_generator,
41
+ )
42
+ from comparative_analysis_system import (
43
+ ComparativeAnalysisSystem,
44
+ PipelineEvaluationConfig,
45
+ create_comparative_analysis_system,
46
+ )
47
+
48
+ # Import our evaluation frameworks
49
+ from pmc_data_pipeline import (
50
+ PMCDataPipeline,
51
+ PMCDocument,
52
+ ProcessingConfig,
53
+ create_pmc_data_pipeline,
54
+ )
55
+ from ragas_metrics_framework import (
56
+ BiomedicalRAGASFramework,
57
+ create_biomedical_ragas_framework,
58
+ )
59
+ from statistical_evaluation_methodology import (
60
+ StatisticalEvaluationFramework,
61
+ create_statistical_framework,
62
+ )
63
+
64
+ # Import existing pipeline infrastructure
65
+ from iris_rag.pipelines.basic import BasicRAGPipeline
66
+ from iris_rag.pipelines.basic_rerank import BasicRAGRerankingPipeline
67
+ from iris_rag.pipelines.crag import CRAGPipeline
68
+ from iris_rag.pipelines.graphrag import GraphRAGPipeline
69
+
70
+ logger = logging.getLogger(__name__)
71
+
72
+
73
+ @dataclass
74
+ class EvaluationExperimentConfig:
75
+ """Complete configuration for evaluation experiment."""
76
+
77
+ # Experiment metadata
78
+ experiment_name: str = "biomedical_rag_evaluation"
79
+ experiment_description: str = "Comprehensive evaluation of biomedical RAG pipelines"
80
+ experiment_version: str = "1.0.0"
81
+
82
+ # Data configuration
83
+ data_source: str = "data/pmc_documents"
84
+ max_documents: int = 10000
85
+ max_questions: int = 1000
86
+ test_questions_per_pipeline: int = 100
87
+
88
+ # Pipeline configuration
89
+ pipelines_to_evaluate: List[str] = field(
90
+ default_factory=lambda: ["BasicRAG", "CRAG", "GraphRAG", "BasicRAGReranking"]
91
+ )
92
+
93
+ # Processing configuration
94
+ max_workers: int = 8
95
+ batch_size: int = 32
96
+ enable_caching: bool = True
97
+ enable_checkpointing: bool = True
98
+
99
+ # Quality thresholds
100
+ min_document_quality: float = 0.3
101
+ min_question_quality: float = 0.5
102
+ statistical_significance_threshold: float = 0.05
103
+
104
+ # Output configuration
105
+ output_base_dir: str = "outputs/evaluations"
106
+ generate_visualizations: bool = True
107
+ generate_interactive_dashboard: bool = True
108
+ generate_final_report: bool = True
109
+
110
+ # Reproducibility configuration
111
+ random_seed: int = 42
112
+ save_intermediate_results: bool = True
113
+ save_model_artifacts: bool = True
114
+
115
+ # Infrastructure configuration
116
+ vector_db_config: Dict[str, Any] = field(default_factory=dict)
117
+ compute_resources: Dict[str, Any] = field(
118
+ default_factory=lambda: {"cpu_cores": 8, "memory_gb": 32, "gpu_enabled": False}
119
+ )
120
+
121
+
122
+ @dataclass
123
+ class EvaluationStage:
124
+ """Represents a single evaluation stage."""
125
+
126
+ name: str
127
+ description: str
128
+ required: bool = True
129
+ completed: bool = False
130
+ start_time: Optional[str] = None
131
+ end_time: Optional[str] = None
132
+ duration: Optional[float] = None
133
+ status: str = "pending" # pending, running, completed, failed
134
+ error_message: Optional[str] = None
135
+ output_files: List[str] = field(default_factory=list)
136
+ metrics: Dict[str, Any] = field(default_factory=dict)
137
+
138
+
139
+ @dataclass
140
+ class EvaluationRun:
141
+ """Represents a complete evaluation run."""
142
+
143
+ run_id: str
144
+ experiment_name: str
145
+ config: EvaluationExperimentConfig
146
+ start_time: str
147
+ end_time: Optional[str] = None
148
+ total_duration: Optional[float] = None
149
+ status: str = "running" # running, completed, failed, cancelled
150
+
151
+ # Stage tracking
152
+ stages: List[EvaluationStage] = field(default_factory=list)
153
+ current_stage: int = 0
154
+
155
+ # Results
156
+ pipeline_results: Dict[str, Any] = field(default_factory=dict)
157
+ comparative_analysis: Optional[Dict[str, Any]] = None
158
+ final_recommendations: List[str] = field(default_factory=list)
159
+
160
+ # File paths
161
+ run_directory: Optional[str] = None
162
+ log_file: Optional[str] = None
163
+ config_file: Optional[str] = None
164
+ results_file: Optional[str] = None
165
+
166
+
167
+ class EvaluationOrchestrator:
168
+ """
169
+ Main orchestration system for biomedical RAG pipeline evaluation.
170
+
171
+ Coordinates all evaluation components to provide reproducible, comprehensive
172
+ pipeline comparisons with statistical rigor and detailed reporting.
173
+ """
174
+
175
+ def __init__(self, config: Optional[EvaluationExperimentConfig] = None):
176
+ """
177
+ Initialize the evaluation orchestrator.
178
+
179
+ Args:
180
+ config: Experiment configuration
181
+ """
182
+ self.config = config or EvaluationExperimentConfig()
183
+
184
+ # Set random seed for reproducibility
185
+ np.random.seed(self.config.random_seed)
186
+
187
+ # Initialize run tracking
188
+ self.current_run: Optional[EvaluationRun] = None
189
+ self.run_history: List[EvaluationRun] = []
190
+
191
+ # Initialize evaluation components
192
+ self.data_pipeline: Optional[PMCDataPipeline] = None
193
+ self.question_generator: Optional[BiomedicalQuestionGenerator] = None
194
+ self.ragas_framework: Optional[BiomedicalRAGASFramework] = None
195
+ self.statistical_framework: Optional[StatisticalEvaluationFramework] = None
196
+ self.comparative_analysis: Optional[ComparativeAnalysisSystem] = None
197
+
198
+ # Setup directories
199
+ self.base_output_dir = Path(self.config.output_base_dir)
200
+ self.base_output_dir.mkdir(parents=True, exist_ok=True)
201
+
202
+ logger.info("Evaluation Orchestrator initialized successfully")
203
+
204
+ def run_complete_evaluation(
205
+ self, config_override: Optional[Dict[str, Any]] = None
206
+ ) -> EvaluationRun:
207
+ """
208
+ Run complete evaluation pipeline from start to finish.
209
+
210
+ Args:
211
+ config_override: Override specific configuration parameters
212
+
213
+ Returns:
214
+ Complete evaluation run results
215
+ """
216
+ logger.info("Starting complete biomedical RAG evaluation")
217
+
218
+ # Apply configuration overrides
219
+ if config_override:
220
+ self._apply_config_overrides(config_override)
221
+
222
+ # Create new evaluation run
223
+ run = self._create_evaluation_run()
224
+ self.current_run = run
225
+
226
+ try:
227
+ # Define evaluation stages
228
+ stages = self._define_evaluation_stages()
229
+ run.stages = stages
230
+
231
+ # Execute stages sequentially
232
+ for stage_idx, stage in enumerate(stages):
233
+ run.current_stage = stage_idx
234
+
235
+ logger.info(
236
+ f"Executing stage {stage_idx + 1}/{len(stages)}: {stage.name}"
237
+ )
238
+
239
+ # Execute stage
240
+ success = self._execute_evaluation_stage(stage)
241
+
242
+ if not success and stage.required:
243
+ logger.error(
244
+ f"Required stage {stage.name} failed - aborting evaluation"
245
+ )
246
+ run.status = "failed"
247
+ break
248
+
249
+ # Save checkpoint
250
+ if self.config.enable_checkpointing:
251
+ self._save_checkpoint(run)
252
+
253
+ # Finalize evaluation
254
+ if run.status != "failed":
255
+ self._finalize_evaluation(run)
256
+
257
+ except Exception as e:
258
+ logger.error(f"Evaluation failed with exception: {e}")
259
+ run.status = "failed"
260
+ if run.stages and run.current_stage < len(run.stages):
261
+ run.stages[run.current_stage].status = "failed"
262
+ run.stages[run.current_stage].error_message = str(e)
263
+
264
+ finally:
265
+ # Complete run
266
+ run.end_time = datetime.now().isoformat()
267
+ if run.start_time:
268
+ start_dt = datetime.fromisoformat(run.start_time)
269
+ end_dt = datetime.fromisoformat(run.end_time)
270
+ run.total_duration = (end_dt - start_dt).total_seconds()
271
+
272
+ # Save final results
273
+ self._save_run_results(run)
274
+
275
+ # Add to history
276
+ self.run_history.append(run)
277
+
278
+ logger.info(f"Evaluation completed: {run.run_id} ({run.status})")
279
+ return run
280
+
281
+ def run_partial_evaluation(
282
+ self, stages: List[str], config_override: Optional[Dict[str, Any]] = None
283
+ ) -> EvaluationRun:
284
+ """
285
+ Run partial evaluation with specified stages only.
286
+
287
+ Args:
288
+ stages: List of stage names to execute
289
+ config_override: Override specific configuration parameters
290
+
291
+ Returns:
292
+ Partial evaluation run results
293
+ """
294
+ logger.info(f"Starting partial evaluation with stages: {stages}")
295
+
296
+ # Apply configuration overrides
297
+ if config_override:
298
+ self._apply_config_overrides(config_override)
299
+
300
+ # Create new evaluation run
301
+ run = self._create_evaluation_run()
302
+ run.experiment_name += "_partial"
303
+ self.current_run = run
304
+
305
+ try:
306
+ # Filter stages
307
+ all_stages = self._define_evaluation_stages()
308
+ filtered_stages = [s for s in all_stages if s.name in stages]
309
+
310
+ if not filtered_stages:
311
+ raise ValueError(f"No valid stages found in: {stages}")
312
+
313
+ run.stages = filtered_stages
314
+
315
+ # Execute specified stages
316
+ for stage_idx, stage in enumerate(filtered_stages):
317
+ run.current_stage = stage_idx
318
+
319
+ logger.info(
320
+ f"Executing partial stage {stage_idx + 1}/{len(filtered_stages)}: {stage.name}"
321
+ )
322
+
323
+ success = self._execute_evaluation_stage(stage)
324
+
325
+ if not success and stage.required:
326
+ run.status = "failed"
327
+ break
328
+
329
+ # Finalize partial evaluation
330
+ if run.status != "failed":
331
+ run.status = "completed"
332
+
333
+ except Exception as e:
334
+ logger.error(f"Partial evaluation failed: {e}")
335
+ run.status = "failed"
336
+
337
+ finally:
338
+ run.end_time = datetime.now().isoformat()
339
+ if run.start_time:
340
+ start_dt = datetime.fromisoformat(run.start_time)
341
+ end_dt = datetime.fromisoformat(run.end_time)
342
+ run.total_duration = (end_dt - start_dt).total_seconds()
343
+
344
+ self._save_run_results(run)
345
+ self.run_history.append(run)
346
+
347
+ return run
348
+
349
+ def resume_evaluation(self, run_id: str) -> EvaluationRun:
350
+ """
351
+ Resume a previous evaluation run from checkpoint.
352
+
353
+ Args:
354
+ run_id: ID of the run to resume
355
+
356
+ Returns:
357
+ Resumed evaluation run
358
+ """
359
+ logger.info(f"Resuming evaluation: {run_id}")
360
+
361
+ # Load checkpoint
362
+ run = self._load_checkpoint(run_id)
363
+ if not run:
364
+ raise ValueError(f"No checkpoint found for run: {run_id}")
365
+
366
+ self.current_run = run
367
+
368
+ # Resume from current stage
369
+ start_stage = run.current_stage
370
+
371
+ try:
372
+ for stage_idx in range(start_stage, len(run.stages)):
373
+ stage = run.stages[stage_idx]
374
+ run.current_stage = stage_idx
375
+
376
+ # Skip completed stages
377
+ if stage.completed:
378
+ continue
379
+
380
+ logger.info(
381
+ f"Resuming stage {stage_idx + 1}/{len(run.stages)}: {stage.name}"
382
+ )
383
+
384
+ success = self._execute_evaluation_stage(stage)
385
+
386
+ if not success and stage.required:
387
+ run.status = "failed"
388
+ break
389
+
390
+ # Save checkpoint
391
+ if self.config.enable_checkpointing:
392
+ self._save_checkpoint(run)
393
+
394
+ # Finalize if all stages completed
395
+ if run.status != "failed":
396
+ self._finalize_evaluation(run)
397
+
398
+ except Exception as e:
399
+ logger.error(f"Resume evaluation failed: {e}")
400
+ run.status = "failed"
401
+
402
+ finally:
403
+ run.end_time = datetime.now().isoformat()
404
+ if run.start_time:
405
+ start_dt = datetime.fromisoformat(run.start_time)
406
+ end_dt = datetime.fromisoformat(run.end_time)
407
+ run.total_duration = (end_dt - start_dt).total_seconds()
408
+
409
+ self._save_run_results(run)
410
+
411
+ return run
412
+
413
+ def compare_runs(self, run_ids: List[str]) -> Dict[str, Any]:
414
+ """
415
+ Compare results across multiple evaluation runs.
416
+
417
+ Args:
418
+ run_ids: List of run IDs to compare
419
+
420
+ Returns:
421
+ Comparison analysis results
422
+ """
423
+ logger.info(f"Comparing evaluation runs: {run_ids}")
424
+
425
+ # Load run results
426
+ runs = []
427
+ for run_id in run_ids:
428
+ run = self._load_run_results(run_id)
429
+ if run:
430
+ runs.append(run)
431
+ else:
432
+ logger.warning(f"Could not load run: {run_id}")
433
+
434
+ if len(runs) < 2:
435
+ raise ValueError("Need at least 2 runs for comparison")
436
+
437
+ # Perform comparison analysis
438
+ comparison = {
439
+ "run_ids": run_ids,
440
+ "comparison_timestamp": datetime.now().isoformat(),
441
+ "run_summaries": {},
442
+ "metric_comparisons": {},
443
+ "statistical_differences": {},
444
+ "recommendations": [],
445
+ }
446
+
447
+ # Summarize each run
448
+ for run in runs:
449
+ comparison["run_summaries"][run.run_id] = {
450
+ "experiment_name": run.experiment_name,
451
+ "status": run.status,
452
+ "duration": run.total_duration,
453
+ "config_hash": self._hash_config(run.config),
454
+ "pipeline_results": run.pipeline_results,
455
+ }
456
+
457
+ # Compare metrics across runs
458
+ self._compare_run_metrics(runs, comparison)
459
+
460
+ # Save comparison results
461
+ comparison_file = (
462
+ self.base_output_dir
463
+ / f"run_comparison_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
464
+ )
465
+ with open(comparison_file, "w") as f:
466
+ json.dump(comparison, f, indent=2, default=str)
467
+
468
+ logger.info(f"Run comparison saved to: {comparison_file}")
469
+ return comparison
470
+
471
+ def _create_evaluation_run(self) -> EvaluationRun:
472
+ """Create a new evaluation run."""
473
+
474
+ run_id = (
475
+ f"{self.config.experiment_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
476
+ )
477
+
478
+ # Create run directory
479
+ run_dir = self.base_output_dir / run_id
480
+ run_dir.mkdir(exist_ok=True)
481
+
482
+ # Setup logging for this run
483
+ log_file = run_dir / "evaluation.log"
484
+ self._setup_run_logging(log_file)
485
+
486
+ # Save configuration
487
+ config_file = run_dir / "config.yaml"
488
+ with open(config_file, "w") as f:
489
+ yaml.dump(asdict(self.config), f, indent=2)
490
+
491
+ return EvaluationRun(
492
+ run_id=run_id,
493
+ experiment_name=self.config.experiment_name,
494
+ config=self.config,
495
+ start_time=datetime.now().isoformat(),
496
+ run_directory=str(run_dir),
497
+ log_file=str(log_file),
498
+ config_file=str(config_file),
499
+ results_file=str(run_dir / "results.json"),
500
+ )
501
+
502
+ def _define_evaluation_stages(self) -> List[EvaluationStage]:
503
+ """Define the evaluation stages."""
504
+
505
+ stages = [
506
+ EvaluationStage(
507
+ name="data_preparation",
508
+ description="Process PMC documents and prepare evaluation data",
509
+ required=True,
510
+ ),
511
+ EvaluationStage(
512
+ name="question_generation",
513
+ description="Generate biomedical evaluation questions",
514
+ required=True,
515
+ ),
516
+ EvaluationStage(
517
+ name="pipeline_initialization",
518
+ description="Initialize and validate RAG pipelines",
519
+ required=True,
520
+ ),
521
+ EvaluationStage(
522
+ name="pipeline_evaluation",
523
+ description="Execute pipeline evaluation using RAGAS metrics",
524
+ required=True,
525
+ ),
526
+ EvaluationStage(
527
+ name="statistical_analysis",
528
+ description="Conduct statistical analysis and comparisons",
529
+ required=True,
530
+ ),
531
+ EvaluationStage(
532
+ name="comparative_analysis",
533
+ description="Generate comprehensive comparative analysis",
534
+ required=True,
535
+ ),
536
+ EvaluationStage(
537
+ name="visualization",
538
+ description="Generate visualizations and interactive dashboards",
539
+ required=False,
540
+ ),
541
+ EvaluationStage(
542
+ name="reporting",
543
+ description="Generate final evaluation report",
544
+ required=False,
545
+ ),
546
+ ]
547
+
548
+ return stages
549
+
550
+ def _execute_evaluation_stage(self, stage: EvaluationStage) -> bool:
551
+ """Execute a single evaluation stage."""
552
+
553
+ stage.status = "running"
554
+ stage.start_time = datetime.now().isoformat()
555
+
556
+ try:
557
+ if stage.name == "data_preparation":
558
+ success = self._execute_data_preparation(stage)
559
+ elif stage.name == "question_generation":
560
+ success = self._execute_question_generation(stage)
561
+ elif stage.name == "pipeline_initialization":
562
+ success = self._execute_pipeline_initialization(stage)
563
+ elif stage.name == "pipeline_evaluation":
564
+ success = self._execute_pipeline_evaluation(stage)
565
+ elif stage.name == "statistical_analysis":
566
+ success = self._execute_statistical_analysis(stage)
567
+ elif stage.name == "comparative_analysis":
568
+ success = self._execute_comparative_analysis(stage)
569
+ elif stage.name == "visualization":
570
+ success = self._execute_visualization(stage)
571
+ elif stage.name == "reporting":
572
+ success = self._execute_reporting(stage)
573
+ else:
574
+ logger.error(f"Unknown stage: {stage.name}")
575
+ success = False
576
+
577
+ stage.status = "completed" if success else "failed"
578
+ stage.completed = success
579
+
580
+ except Exception as e:
581
+ logger.error(f"Stage {stage.name} failed with exception: {e}")
582
+ stage.status = "failed"
583
+ stage.error_message = str(e)
584
+ success = False
585
+
586
+ finally:
587
+ stage.end_time = datetime.now().isoformat()
588
+ if stage.start_time:
589
+ start_dt = datetime.fromisoformat(stage.start_time)
590
+ end_dt = datetime.fromisoformat(stage.end_time)
591
+ stage.duration = (end_dt - start_dt).total_seconds()
592
+
593
+ return success
594
+
595
+ def _execute_data_preparation(self, stage: EvaluationStage) -> bool:
596
+ """Execute data preparation stage."""
597
+ logger.info("Executing data preparation stage")
598
+
599
+ # Initialize data pipeline
600
+ data_config = ProcessingConfig(
601
+ input_data_path=self.config.data_source,
602
+ max_documents=self.config.max_documents,
603
+ max_workers=self.config.max_workers,
604
+ batch_size=self.config.batch_size,
605
+ min_quality_score=self.config.min_document_quality,
606
+ output_data_path=f"{self.current_run.run_directory}/processed_data",
607
+ enable_caching=self.config.enable_caching,
608
+ )
609
+
610
+ self.data_pipeline = create_pmc_data_pipeline(data_config)
611
+
612
+ # Process documents
613
+ stats = self.data_pipeline.process_documents(
614
+ self.config.data_source, resume_from_checkpoint=False
615
+ )
616
+
617
+ # Save stage results
618
+ stage.metrics = {
619
+ "total_documents": stats.total_documents,
620
+ "successful_documents": stats.successful_documents,
621
+ "failed_documents": stats.failed_documents,
622
+ "total_chunks": stats.total_chunks,
623
+ "processing_time": stats.processing_time,
624
+ "average_quality": stats.avg_document_quality,
625
+ }
626
+
627
+ stage.output_files = [
628
+ f"{self.current_run.run_directory}/processed_data/metadata",
629
+ f"{self.current_run.run_directory}/processed_data/chunks",
630
+ ]
631
+
632
+ # Validation
633
+ if stats.successful_documents < 100:
634
+ logger.warning(
635
+ f"Only {stats.successful_documents} documents processed successfully"
636
+ )
637
+ return False
638
+
639
+ logger.info(
640
+ f"Data preparation completed: {stats.successful_documents} documents processed"
641
+ )
642
+ return True
643
+
644
+ def _execute_question_generation(self, stage: EvaluationStage) -> bool:
645
+ """Execute question generation stage."""
646
+ logger.info("Executing question generation stage")
647
+
648
+ # Load processed documents
649
+ processed_docs = self._load_processed_documents()
650
+
651
+ # Initialize question generator
652
+ question_config = QuestionGenerationConfig(
653
+ total_questions=self.config.max_questions,
654
+ min_confidence_score=self.config.min_question_quality,
655
+ )
656
+
657
+ self.question_generator = create_biomedical_question_generator(question_config)
658
+
659
+ # Generate questions
660
+ questions = self.question_generator.generate_questions_from_documents(
661
+ processed_docs
662
+ )
663
+
664
+ # Save questions
665
+ questions_file = (
666
+ Path(self.current_run.run_directory) / "evaluation_questions.json"
667
+ )
668
+ questions_data = [
669
+ {
670
+ "question_id": q.question_id,
671
+ "question": q.question,
672
+ "ground_truth_answer": q.ground_truth_answer,
673
+ "question_type": q.question_type,
674
+ "complexity_level": q.complexity_level,
675
+ "confidence_score": q.confidence_score,
676
+ }
677
+ for q in questions
678
+ ]
679
+
680
+ with open(questions_file, "w") as f:
681
+ json.dump(questions_data, f, indent=2)
682
+
683
+ stage.metrics = {
684
+ "total_questions": len(questions),
685
+ "question_types": {
686
+ qt: len([q for q in questions if q.question_type == qt])
687
+ for qt in set(q.question_type for q in questions)
688
+ },
689
+ "avg_confidence": np.mean([q.confidence_score for q in questions]),
690
+ }
691
+
692
+ stage.output_files = [str(questions_file)]
693
+
694
+ logger.info(
695
+ f"Question generation completed: {len(questions)} questions generated"
696
+ )
697
+ return len(questions) >= 50 # Minimum threshold
698
+
699
+ def _execute_pipeline_initialization(self, stage: EvaluationStage) -> bool:
700
+ """Execute pipeline initialization stage."""
701
+ logger.info("Executing pipeline initialization stage")
702
+
703
+ # Initialize comparative analysis system
704
+ comparison_config = PipelineEvaluationConfig(
705
+ total_questions=self.config.max_questions,
706
+ questions_per_pipeline_test=self.config.test_questions_per_pipeline,
707
+ max_workers=self.config.max_workers,
708
+ batch_size=self.config.batch_size,
709
+ statistical_alpha=self.config.statistical_significance_threshold,
710
+ output_dir=f"{self.current_run.run_directory}/comparative_analysis",
711
+ )
712
+
713
+ self.comparative_analysis = create_comparative_analysis_system(
714
+ comparison_config
715
+ )
716
+
717
+ # Validate pipeline availability
718
+ available_pipelines = []
719
+ for pipeline_name in self.config.pipelines_to_evaluate:
720
+ try:
721
+ # Test pipeline initialization
722
+ self.comparative_analysis._initialize_pipelines([pipeline_name])
723
+ available_pipelines.append(pipeline_name)
724
+ logger.info(f"Pipeline {pipeline_name} initialized successfully")
725
+ except Exception as e:
726
+ logger.error(f"Pipeline {pipeline_name} initialization failed: {e}")
727
+
728
+ stage.metrics = {
729
+ "requested_pipelines": len(self.config.pipelines_to_evaluate),
730
+ "available_pipelines": len(available_pipelines),
731
+ "pipeline_names": available_pipelines,
732
+ }
733
+
734
+ # Update config with available pipelines
735
+ self.config.pipelines_to_evaluate = available_pipelines
736
+
737
+ logger.info(
738
+ f"Pipeline initialization completed: {len(available_pipelines)} pipelines available"
739
+ )
740
+ return len(available_pipelines) >= 2 # Need at least 2 for comparison
741
+
742
+ def _execute_pipeline_evaluation(self, stage: EvaluationStage) -> bool:
743
+ """Execute pipeline evaluation stage."""
744
+ logger.info("Executing pipeline evaluation stage")
745
+
746
+ # Load documents and questions
747
+ processed_docs = self._load_processed_documents()
748
+ questions = self._load_evaluation_questions()
749
+
750
+ # Run comprehensive comparison
751
+ results = self.comparative_analysis.run_comprehensive_comparison(
752
+ documents=processed_docs, pipeline_names=self.config.pipelines_to_evaluate
753
+ )
754
+
755
+ # Store results in run
756
+ self.current_run.comparative_analysis = {
757
+ "experiment_id": results.experiment_id,
758
+ "pipeline_results": {
759
+ name: {
760
+ "success_rate": result.success_rate,
761
+ "ragas_scores": (
762
+ result.ragas_results.to_dict() if result.ragas_results else {}
763
+ ),
764
+ "execution_times": result.execution_times,
765
+ }
766
+ for name, result in results.pipeline_results.items()
767
+ },
768
+ "overall_rankings": results.overall_rankings,
769
+ "best_pipeline_per_metric": results.best_pipeline_per_metric,
770
+ }
771
+
772
+ stage.metrics = {
773
+ "pipelines_evaluated": len(results.pipeline_results),
774
+ "questions_evaluated": results.total_questions_evaluated,
775
+ "evaluation_time": results.total_evaluation_time,
776
+ "successful_evaluations": results.successful_evaluations_per_pipeline,
777
+ }
778
+
779
+ stage.output_files = [f"{self.current_run.run_directory}/comparative_analysis"]
780
+
781
+ logger.info(
782
+ f"Pipeline evaluation completed: {len(results.pipeline_results)} pipelines evaluated"
783
+ )
784
+ return True
785
+
786
+ def _execute_statistical_analysis(self, stage: EvaluationStage) -> bool:
787
+ """Execute statistical analysis stage."""
788
+ logger.info("Executing statistical analysis stage")
789
+
790
+ # Statistical analysis is part of comparative analysis
791
+ # Additional statistical tests can be performed here
792
+
793
+ stage.metrics = {
794
+ "statistical_tests_completed": True,
795
+ "significance_threshold": self.config.statistical_significance_threshold,
796
+ }
797
+
798
+ logger.info("Statistical analysis completed")
799
+ return True
800
+
801
+ def _execute_comparative_analysis(self, stage: EvaluationStage) -> bool:
802
+ """Execute comparative analysis stage."""
803
+ logger.info("Executing comparative analysis stage")
804
+
805
+ # Comparative analysis already completed in pipeline evaluation
806
+ # Generate summary insights here
807
+
808
+ if not self.current_run.comparative_analysis:
809
+ logger.error("No comparative analysis results available")
810
+ return False
811
+
812
+ # Generate recommendations
813
+ recommendations = self._generate_recommendations()
814
+ self.current_run.final_recommendations = recommendations
815
+
816
+ stage.metrics = {
817
+ "recommendations_generated": len(recommendations),
818
+ "comparative_analysis_completed": True,
819
+ }
820
+
821
+ logger.info("Comparative analysis completed")
822
+ return True
823
+
824
+ def _execute_visualization(self, stage: EvaluationStage) -> bool:
825
+ """Execute visualization stage."""
826
+ logger.info("Executing visualization stage")
827
+
828
+ if not self.config.generate_visualizations:
829
+ logger.info("Visualization generation disabled")
830
+ return True
831
+
832
+ # Visualizations are generated by comparative analysis system
833
+ viz_dir = Path(self.current_run.run_directory) / "visualizations"
834
+ viz_dir.mkdir(exist_ok=True)
835
+
836
+ stage.output_files = [str(viz_dir)]
837
+ stage.metrics = {"visualizations_generated": True}
838
+
839
+ logger.info("Visualization generation completed")
840
+ return True
841
+
842
+ def _execute_reporting(self, stage: EvaluationStage) -> bool:
843
+ """Execute reporting stage."""
844
+ logger.info("Executing reporting stage")
845
+
846
+ if not self.config.generate_final_report:
847
+ logger.info("Final report generation disabled")
848
+ return True
849
+
850
+ # Generate comprehensive report
851
+ report = self._generate_final_report()
852
+
853
+ report_file = Path(self.current_run.run_directory) / "final_report.md"
854
+ with open(report_file, "w") as f:
855
+ f.write(report)
856
+
857
+ stage.output_files = [str(report_file)]
858
+ stage.metrics = {"final_report_generated": True}
859
+
860
+ logger.info(f"Final report generated: {report_file}")
861
+ return True
862
+
863
+ def _load_processed_documents(self) -> List[Dict[str, Any]]:
864
+ """Load processed documents from data pipeline."""
865
+ # Placeholder - would load from data pipeline output
866
+ return []
867
+
868
+ def _load_evaluation_questions(self) -> List[Dict[str, Any]]:
869
+ """Load evaluation questions."""
870
+ questions_file = (
871
+ Path(self.current_run.run_directory) / "evaluation_questions.json"
872
+ )
873
+ if questions_file.exists():
874
+ with open(questions_file, "r") as f:
875
+ return json.load(f)
876
+ return []
877
+
878
+ def _generate_recommendations(self) -> List[str]:
879
+ """Generate final recommendations based on analysis."""
880
+ recommendations = []
881
+
882
+ if self.current_run.comparative_analysis:
883
+ best_overall = self.current_run.comparative_analysis.get(
884
+ "best_pipeline_per_metric", {}
885
+ )
886
+
887
+ # Overall best pipeline
888
+ if best_overall:
889
+ most_common = max(
890
+ set(best_overall.values()), key=list(best_overall.values()).count
891
+ )
892
+ recommendations.append(f"Overall recommended pipeline: {most_common}")
893
+
894
+ # Specific use case recommendations
895
+ for metric, pipeline in best_overall.items():
896
+ recommendations.append(f"For {metric} optimization: {pipeline}")
897
+
898
+ return recommendations
899
+
900
+ def _generate_final_report(self) -> str:
901
+ """Generate comprehensive final report."""
902
+
903
+ report = f"""# Biomedical RAG Pipeline Evaluation Report
904
+
905
+ ## Experiment Overview
906
+ - **Experiment Name**: {self.config.experiment_name}
907
+ - **Run ID**: {self.current_run.run_id}
908
+ - **Execution Time**: {self.current_run.total_duration:.2f} seconds
909
+ - **Status**: {self.current_run.status}
910
+
911
+ ## Configuration
912
+ - **Documents Processed**: {self.config.max_documents}
913
+ - **Questions Generated**: {self.config.max_questions}
914
+ - **Pipelines Evaluated**: {', '.join(self.config.pipelines_to_evaluate)}
915
+ - **Statistical Significance Threshold**: {self.config.statistical_significance_threshold}
916
+
917
+ ## Results Summary
918
+ """
919
+
920
+ if self.current_run.comparative_analysis:
921
+ report += """
922
+ ### Pipeline Performance Rankings
923
+ """
924
+ best_per_metric = self.current_run.comparative_analysis.get(
925
+ "best_pipeline_per_metric", {}
926
+ )
927
+ for metric, pipeline in best_per_metric.items():
928
+ report += f"- **{metric}**: {pipeline}\n"
929
+
930
+ if self.current_run.final_recommendations:
931
+ report += """
932
+ ## Recommendations
933
+ """
934
+ for i, rec in enumerate(self.current_run.final_recommendations, 1):
935
+ report += f"{i}. {rec}\n"
936
+
937
+ report += f"""
938
+ ## Execution Details
939
+ """
940
+ for stage in self.current_run.stages:
941
+ report += f"- **{stage.name}**: {stage.status} ({stage.duration:.2f}s)\n"
942
+
943
+ return report
944
+
945
+ def _apply_config_overrides(self, overrides: Dict[str, Any]):
946
+ """Apply configuration overrides."""
947
+ for key, value in overrides.items():
948
+ if hasattr(self.config, key):
949
+ setattr(self.config, key, value)
950
+ logger.info(f"Config override: {key} = {value}")
951
+
952
+ def _setup_run_logging(self, log_file: Path):
953
+ """Setup logging for evaluation run."""
954
+ file_handler = logging.FileHandler(log_file)
955
+ file_handler.setLevel(logging.INFO)
956
+ formatter = logging.Formatter(
957
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
958
+ )
959
+ file_handler.setFormatter(formatter)
960
+
961
+ # Add to root logger
962
+ logging.getLogger().addHandler(file_handler)
963
+
964
+ def _save_checkpoint(self, run: EvaluationRun):
965
+ """Save evaluation checkpoint."""
966
+ if not self.config.enable_checkpointing:
967
+ return
968
+
969
+ checkpoint_file = Path(run.run_directory) / "checkpoint.json"
970
+ with open(checkpoint_file, "w") as f:
971
+ json.dump(asdict(run), f, indent=2, default=str)
972
+
973
+ def _load_checkpoint(self, run_id: str) -> Optional[EvaluationRun]:
974
+ """Load evaluation checkpoint."""
975
+ checkpoint_file = self.base_output_dir / run_id / "checkpoint.json"
976
+
977
+ if checkpoint_file.exists():
978
+ with open(checkpoint_file, "r") as f:
979
+ data = json.load(f)
980
+
981
+ # Reconstruct run object
982
+ config_data = data["config"]
983
+ config = EvaluationExperimentConfig(**config_data)
984
+
985
+ run = EvaluationRun(
986
+ run_id=data["run_id"],
987
+ experiment_name=data["experiment_name"],
988
+ config=config,
989
+ start_time=data["start_time"],
990
+ end_time=data.get("end_time"),
991
+ total_duration=data.get("total_duration"),
992
+ status=data["status"],
993
+ current_stage=data["current_stage"],
994
+ run_directory=data["run_directory"],
995
+ log_file=data["log_file"],
996
+ config_file=data["config_file"],
997
+ results_file=data["results_file"],
998
+ )
999
+
1000
+ # Reconstruct stages
1001
+ stages = []
1002
+ for stage_data in data["stages"]:
1003
+ stage = EvaluationStage(**stage_data)
1004
+ stages.append(stage)
1005
+ run.stages = stages
1006
+
1007
+ return run
1008
+
1009
+ return None
1010
+
1011
+ def _save_run_results(self, run: EvaluationRun):
1012
+ """Save final run results."""
1013
+ with open(run.results_file, "w") as f:
1014
+ json.dump(asdict(run), f, indent=2, default=str)
1015
+
1016
+ def _load_run_results(self, run_id: str) -> Optional[EvaluationRun]:
1017
+ """Load run results."""
1018
+ results_file = self.base_output_dir / run_id / "results.json"
1019
+
1020
+ if results_file.exists():
1021
+ with open(results_file, "r") as f:
1022
+ data = json.load(f)
1023
+
1024
+ # Reconstruct run object (similar to checkpoint loading)
1025
+ config_data = data["config"]
1026
+ config = EvaluationExperimentConfig(**config_data)
1027
+
1028
+ return EvaluationRun(
1029
+ run_id=data["run_id"],
1030
+ experiment_name=data["experiment_name"],
1031
+ config=config,
1032
+ start_time=data["start_time"],
1033
+ end_time=data.get("end_time"),
1034
+ total_duration=data.get("total_duration"),
1035
+ status=data["status"],
1036
+ )
1037
+
1038
+ return None
1039
+
1040
+ def _finalize_evaluation(self, run: EvaluationRun):
1041
+ """Finalize evaluation run."""
1042
+ if all(stage.completed for stage in run.stages if stage.required):
1043
+ run.status = "completed"
1044
+ else:
1045
+ run.status = "partial"
1046
+
1047
+ logger.info(f"Evaluation finalized with status: {run.status}")
1048
+
1049
+ def _hash_config(self, config: EvaluationExperimentConfig) -> str:
1050
+ """Generate hash for configuration."""
1051
+ config_str = json.dumps(asdict(config), sort_keys=True)
1052
+ return hashlib.md5(config_str.encode()).hexdigest()[:8]
1053
+
1054
+ def _compare_run_metrics(
1055
+ self, runs: List[EvaluationRun], comparison: Dict[str, Any]
1056
+ ):
1057
+ """Compare metrics across multiple runs."""
1058
+ # Placeholder for cross-run metric comparison
1059
+ comparison["metric_comparisons"] = {
1060
+ "total_runs": len(runs),
1061
+ "comparison_completed": True,
1062
+ }
1063
+
1064
+
1065
+ def create_evaluation_orchestrator(
1066
+ config: Optional[EvaluationExperimentConfig] = None,
1067
+ ) -> EvaluationOrchestrator:
1068
+ """Factory function to create evaluation orchestrator."""
1069
+ return EvaluationOrchestrator(config)
1070
+
1071
+
1072
+ def main():
1073
+ """Command-line interface for evaluation orchestrator."""
1074
+ parser = argparse.ArgumentParser(
1075
+ description="Biomedical RAG Pipeline Evaluation Orchestrator"
1076
+ )
1077
+
1078
+ parser.add_argument("--config", type=str, help="Configuration file path")
1079
+ parser.add_argument(
1080
+ "--experiment-name", type=str, default="biomedical_rag_evaluation"
1081
+ )
1082
+ parser.add_argument("--max-documents", type=int, default=1000)
1083
+ parser.add_argument("--max-questions", type=int, default=100)
1084
+ parser.add_argument("--pipelines", nargs="+", default=["BasicRAG", "CRAG"])
1085
+ parser.add_argument("--output-dir", type=str, default="outputs/evaluations")
1086
+ parser.add_argument("--resume", type=str, help="Resume from run ID")
1087
+ parser.add_argument("--partial", nargs="+", help="Run only specified stages")
1088
+
1089
+ args = parser.parse_args()
1090
+
1091
+ # Load configuration
1092
+ if args.config and Path(args.config).exists():
1093
+ with open(args.config, "r") as f:
1094
+ config_data = yaml.safe_load(f)
1095
+ config = EvaluationExperimentConfig(**config_data)
1096
+ else:
1097
+ config = EvaluationExperimentConfig(
1098
+ experiment_name=args.experiment_name,
1099
+ max_documents=args.max_documents,
1100
+ max_questions=args.max_questions,
1101
+ pipelines_to_evaluate=args.pipelines,
1102
+ output_base_dir=args.output_dir,
1103
+ )
1104
+
1105
+ # Create orchestrator
1106
+ orchestrator = create_evaluation_orchestrator(config)
1107
+
1108
+ # Execute evaluation
1109
+ if args.resume:
1110
+ run = orchestrator.resume_evaluation(args.resume)
1111
+ elif args.partial:
1112
+ run = orchestrator.run_partial_evaluation(args.partial)
1113
+ else:
1114
+ run = orchestrator.run_complete_evaluation()
1115
+
1116
+ print(f"Evaluation completed: {run.run_id}")
1117
+ print(f"Status: {run.status}")
1118
+ print(f"Duration: {run.total_duration:.2f}s")
1119
+ print(f"Results: {run.results_file}")
1120
+
1121
+
1122
+ if __name__ == "__main__":
1123
+ main()