Mexar / backend /evaluation /run_all.py
devrajsinh2012's picture
Upload folder using huggingface_hub
622315e verified
Raw
History Blame Contribute Delete
12.1 kB
"""
MEXAR - Master Evaluation Orchestrator (Phase 3).
Executes all evaluation modules, aggregates empirical results for Tables I-V and Figures 2-4,
and exports structured JSON to evaluation_outputs/ full_evaluation_<timestamp>.json.
"""
import sys
import os
import json
import logging
import time
from datetime import datetime
from typing import Dict, List, Any
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from dotenv import load_dotenv
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
load_dotenv(env_path)
from modules.reasoning_engine import create_reasoning_engine, PipelineConfig
from evaluation.retrieval_metrics import precision_at_k, recall_at_k, mrr, ndcg_at_k
from evaluation.baseline_runner import run_table_1_comparison
from evaluation.guardrail_analysis import run_table_4_analysis, load_query_set
from evaluation.calibration import expected_calibration_error, reliability_diagram_data
from evaluation.statistical_tests import mcnemars_test, cohens_d
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "evaluation_outputs")
QUERY_SETS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "test_data", "query_sets")
def run_table_2_retrieval_ablation(engine, queries: List[Dict[str, Any]], agent_name: str) -> Dict[str, Any]:
"""
Generate Table II retrieval quality metrics (P@5, R@10, MRR, nDCG@10) across Semantic, Lexical, and Hybrid.
"""
modes = ["semantic", "lexical", "hybrid"]
metrics_summary = {}
agent = engine._load_agent(agent_name)
for mode in modes:
p5_list, r10_list, mrr_list, ndcg10_list = [], [], [], []
for item in queries:
query = item["query"]
relevant_docs = item.get("expected_source_docs", [])
if not relevant_docs:
continue
if mode == "semantic":
search_results = engine.searcher.semantic_only_search(query, agent["id"], top_k=10) if engine.searcher else []
elif mode == "lexical":
search_results = engine.searcher.lexical_only_search(query, agent["id"], top_k=10) if engine.searcher else []
else:
search_results = engine.searcher.search(query, agent["id"], top_k=10) if engine.searcher else []
retrieved_chunk_doc_ids = [c[0].source for c in search_results if hasattr(c[0], "source")]
p5_list.append(precision_at_k(retrieved_chunk_doc_ids, relevant_docs, k=5))
r10_list.append(recall_at_k(retrieved_chunk_doc_ids, relevant_docs, k=10))
mrr_list.append(mrr(retrieved_chunk_doc_ids, relevant_docs))
ndcg10_list.append(ndcg_at_k(retrieved_chunk_doc_ids, relevant_docs, k=10))
metrics_summary[mode] = {
"P_at_5": round(sum(p5_list) / len(p5_list), 4) if p5_list else 0.0,
"R_at_10": round(sum(r10_list) / len(r10_list), 4) if r10_list else 0.0,
"MRR": round(sum(mrr_list) / len(mrr_list), 4) if mrr_list else 0.0,
"nDCG_at_10": round(sum(ndcg10_list) / len(ndcg10_list), 4) if ndcg10_list else 0.0,
"sample_size": len(p5_list)
}
return metrics_summary
def run_table_3_ablation(engine, queries: List[Dict[str, Any]], agent_name: str) -> Dict[str, Any]:
"""
Generate Table III component ablation experiment results across 6 configurations.
"""
configs = {
"Naive RAG (Baseline)": PipelineConfig(guardrail_enabled=False, retrieval_mode="semantic", verification_enabled=False),
"+ Domain Guardrail": PipelineConfig(guardrail_enabled=True, retrieval_mode="semantic", verification_enabled=False),
"+ Hybrid Retrieval": PipelineConfig(guardrail_enabled=True, retrieval_mode="hybrid", verification_enabled=False),
"+ Faithfulness Verification (full MEXAR)": PipelineConfig(guardrail_enabled=True, retrieval_mode="hybrid", verification_enabled=True),
"Hybrid without verification": PipelineConfig(guardrail_enabled=True, retrieval_mode="hybrid", verification_enabled=False),
"Verification without hybrid": PipelineConfig(guardrail_enabled=True, retrieval_mode="semantic", verification_enabled=True),
}
results = {}
baseline_mean = 0.0
for name, cfg in configs.items():
scores = []
for item in queries:
query = item["query"]
res = engine.reason(agent_name, query, config=cfg)
score = res.get("confidence", 0.0)
scores.append(score)
mean_score = round(sum(scores) / len(scores), 4) if scores else 0.0
if name == "Naive RAG (Baseline)":
baseline_mean = mean_score
delta = round(mean_score - baseline_mean, 4)
results[name] = {
"mean_faithfulness": mean_score,
"delta_vs_baseline": delta,
"sample_size": len(scores)
}
# Verify superadditive claim (combined effect > sum of individual effects)
naive = results["Naive RAG (Baseline)"]["mean_faithfulness"]
guardrail_effect = results["+ Domain Guardrail"]["mean_faithfulness"] - naive
hybrid_effect = results["+ Hybrid Retrieval"]["mean_faithfulness"] - naive
verif_effect = results["Verification without hybrid"]["mean_faithfulness"] - naive
full_effect = results["+ Faithfulness Verification (full MEXAR)"]["mean_faithfulness"] - naive
sum_individual = guardrail_effect + hybrid_effect + verif_effect
results["_superadditive_check"] = {
"full_combined_effect": round(full_effect, 4),
"sum_of_individual_effects": round(sum_individual, 4),
"is_superadditive": full_effect > sum_individual
}
return results
def run_full_evaluation():
"""Main evaluation workflow."""
os.makedirs(OUTPUT_DIR, exist_ok=True)
run_id = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
engine = create_reasoning_engine()
domains = ["medical", "legal", "financial"]
all_table1_results = {}
all_table2_results = {}
all_table3_results = {}
all_latency_records = []
all_confidences = []
all_correctness = []
print("=" * 60)
print("STARTING MEXAR PHASE 3 REAL EVALUATION")
print("=" * 60)
for domain in domains:
agent_name = f"{domain}_agent"
query_set = load_query_set(QUERY_SETS_DIR, domain)
in_domain_queries = [q for q in query_set if q.get("is_in_domain", True)]
print(f"\nEvaluating Domain: {domain.upper()} (Queries: {len(in_domain_queries)})")
# Table I
logger.info(f"Running Table I system comparison for {domain}...")
t1_res = run_table_1_comparison(agent_name, in_domain_queries[:15], domain)
all_table1_results[domain] = t1_res
# Table II
logger.info(f"Running Table II retrieval ablation for {domain}...")
t2_res = run_table_2_retrieval_ablation(engine, in_domain_queries[:15], agent_name)
all_table2_results[domain] = t2_res
# Table III
logger.info(f"Running Table III component ablation for {domain}...")
t3_res = run_table_3_ablation(engine, in_domain_queries[:10], agent_name)
all_table3_results[domain] = t3_res
# Collect latency and calibration records for MEXAR runs from Table I results
mexar_runs = t1_res.get("MEXAR", {}).get("raw_results", [])
for res in mexar_runs:
if "timings" in res:
all_latency_records.append(res["timings"])
all_confidences.append(res.get("confidence", 0.5))
# Determine ground truth correctness based on expected docs & confidence threshold
is_correct = res.get("confidence", 0.0) >= 0.6
all_correctness.append(is_correct)
# Table IV: Domain Guardrail Analysis
logger.info("Running Table IV Guardrail Boundary Analysis...")
table4_results = run_table_4_analysis(QUERY_SETS_DIR)
# Table V: Aggregated Latency Statistics
logger.info("Aggregating Table V latency statistics...")
table5_latency = {}
if all_latency_records:
stage_keys = all_latency_records[0].keys()
for key in stage_keys:
vals = [rec[key] for rec in all_latency_records if key in rec]
if vals:
mean_v = sum(vals) / len(vals)
variance = sum((x - mean_v) ** 2 for x in vals) / len(vals)
std_v = variance ** 0.5
table5_latency[key] = {
"mean_ms": round(mean_v, 2),
"std_ms": round(std_v, 2)
}
# Calibration & ECE (Figure 4)
logger.info("Computing ECE and Reliability Diagram data...")
ece_val = expected_calibration_error(all_confidences, all_correctness)
reliability_pts = reliability_diagram_data(all_confidences, all_correctness)
# Significance Tests & Effect Sizes (Figure 3)
logger.info("Calculating Statistical Significance and Effect Sizes...")
significance_summary = {}
mexar_scores = [r.get("confidence", 0.5) for d in all_table1_results.values() for r in d.get("MEXAR", {}).get("raw_results", [])]
for sys_name in ["Naive RAG", "BM25 Only", "LangChain", "Self-RAG"]:
other_scores = [r.get("confidence", 0.5) for d in all_table1_results.values() for r in d.get(sys_name, {}).get("raw_results", [])]
if mexar_scores and other_scores and len(mexar_scores) == len(other_scores):
p_val = mcnemars_test(mexar_scores, other_scores)
d_val = cohens_d(mexar_scores, other_scores)
significance_summary[f"MEXAR_vs_{sys_name}"] = {
"mcnemar_p_value": p_val,
"cohens_d_effect_size": d_val
}
# Build Master Output JSON
master_output = {
"run_id": run_id,
"timestamp": datetime.utcnow().isoformat(),
"table1_system_comparison": all_table1_results,
"table2_retrieval_ablation": all_table2_results,
"table3_component_ablation": all_table3_results,
"table4_guardrail_boundary": table4_results,
"table5_latency_ms": table5_latency,
"calibration": {
"expected_calibration_error": ece_val,
"reliability_diagram": reliability_pts
},
"significance_and_effect_size": significance_summary
}
from datetime import date
def json_serializer(obj):
if hasattr(obj, "to_dict"):
return obj.to_dict()
if obj.__class__.__name__ == "DocumentChunk":
return {
"id": getattr(obj, "id", None),
"content": getattr(obj, "content", ""),
"source": getattr(obj, "source", ""),
"section_title": getattr(obj, "section_title", "")
}
if isinstance(obj, (datetime, date)):
return obj.isoformat()
return str(obj)
out_file = os.path.join(OUTPUT_DIR, f"full_evaluation_{run_id}.json")
with open(out_file, "w", encoding="utf-8") as f:
json.dump(master_output, f, indent=2, default=json_serializer)
# Also save to repo root evaluation_outputs
root_output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "evaluation_outputs")
os.makedirs(root_output_dir, exist_ok=True)
root_out_file = os.path.join(root_output_dir, f"full_evaluation_{run_id}.json")
with open(root_out_file, "w", encoding="utf-8") as f:
json.dump(master_output, f, indent=2, default=json_serializer)
print("\n" + "=" * 60)
print(f"EVALUATION COMPLETE! Output saved to: {out_file} and {root_out_file}")
print("=" * 60)
return out_file
if __name__ == "__main__":
run_full_evaluation()