File size: 12,050 Bytes
622315e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
"""

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()