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.
- adapters/rag_templates_bridge.py +578 -0
- common/__init__.py +1 -0
- common/batch_utils.py +294 -0
- common/config.py +58 -0
- common/connection_manager.py +242 -0
- common/connection_pool.py +445 -0
- common/database.py +141 -0
- common/database_schema_manager.py +289 -0
- common/db_init_with_indexes.py +181 -0
- common/db_vector_utils.py +181 -0
- common/environment_manager.py +220 -0
- common/huggingface_utils.py +206 -0
- common/iris_client.py +92 -0
- common/iris_connection_manager.py +411 -0
- common/iris_connection_pool.py +360 -0
- common/iris_connector.py +71 -0
- common/iris_dbapi_connector.py +323 -0
- common/iris_index_utils.py +195 -0
- common/iris_sql_utils.py +704 -0
- common/iris_stream_reader.py +166 -0
- common/llm_cache_config.py +308 -0
- common/llm_cache_iris.py +671 -0
- common/llm_cache_manager.py +911 -0
- common/utils.py +559 -0
- common/vector_sql_utils.py +584 -0
- evaluation_framework/__init__.py +66 -0
- evaluation_framework/biomedical_question_generator.py +826 -0
- evaluation_framework/comparative_analysis_system.py +1173 -0
- evaluation_framework/empirical_reporting.py +1298 -0
- evaluation_framework/evaluation_orchestrator.py +1123 -0
- evaluation_framework/execute_production_evaluation.py +382 -0
- evaluation_framework/pmc_data_pipeline.py +1332 -0
- evaluation_framework/production_evaluation_config.py +353 -0
- evaluation_framework/ragas_metrics_framework.py +1013 -0
- evaluation_framework/real_production_evaluation.py +544 -0
- evaluation_framework/run_10k_evaluation.py +175 -0
- evaluation_framework/simple_test.py +364 -0
- evaluation_framework/statistical_evaluation_methodology.py +1016 -0
- evaluation_framework/test_framework.py +281 -0
- evaluation_framework/test_iris_connectivity.py +244 -0
- evaluation_framework/test_small_scale_evaluation.py +432 -0
- evaluation_framework/true_e2e_evaluation.py +639 -0
- evaluation_framework/visualization_engine.py +585 -0
- iris_rag/__init__.py +284 -0
- iris_rag/config/default_config.yaml +295 -0
- iris_vector_rag-0.2.1.dist-info/METADATA +499 -0
- iris_vector_rag-0.2.1.dist-info/RECORD +50 -0
- iris_vector_rag-0.2.1.dist-info/WHEEL +5 -0
- iris_vector_rag-0.2.1.dist-info/entry_points.txt +2 -0
- 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()
|