devrajsinh2012 commited on
Commit
90aae1b
·
verified ·
1 Parent(s): 2d35f29

Delete MEXAR_Phase3_Real_Evaluation.md

Browse files
Files changed (1) hide show
  1. MEXAR_Phase3_Real_Evaluation.md +0 -497
MEXAR_Phase3_Real_Evaluation.md DELETED
@@ -1,497 +0,0 @@
1
- # MEXAR Phase 3 — Running the Real Evaluation to Generate Paper Numbers
2
-
3
- **Purpose of this document:** hand this to an AI coding agent (Antigravity or similar) working directly in the `Mexar` repository (`github.com/devrajsinh2012/Mexar`). This is Phase 3 only: turning Phase 1's fixed architecture and Phase 2's real ~500-doc corpus into actual, reproducible numbers for Tables I-V and Figures 2-4. Assumes Phase 1 and Phase 2 are both done.
4
-
5
- **Presentation date:** July 30-31, 2026. Target for this phase: days 12-16 (about 5 days). This is the densest phase, budget real time for it.
6
-
7
- ---
8
-
9
- ## 3.0 Read this first: a finding that changes the scope of this phase
10
-
11
- Before writing any new evaluation code, understand what already exists in `backend/evaluation/` and what it actually measures, because it's not what the paper's Table I needs.
12
-
13
- `backend/evaluation/baseline_runner.py` calls `engine.reason_crag_baseline()` and `engine.reason_raptor_baseline()`. Both route into `_run_baseline()` in `reasoning_engine.py`, whose own docstrings say "simulating CRAG flow" and "Simulates recursive summarization trees." What actually happens: the **same MEXAR hybrid retrieval** runs, and only the system prompt text changes ("You are a Corrective-RAG system..." vs "You are a RAPTOR baseline model..."). This is not a real CRAG implementation (no retrieval evaluator, no web-search fallback, no knowledge refinement) and not a real RAPTOR implementation (no hierarchical clustering, no recursive summarization tree built at index time). It's the same retrieval with a relabeled prompt.
14
-
15
- More importantly: **Table I in the paper compares Naive RAG, BM25 Only, LangChain, Self-RAG, and MEXAR.** None of those four baseline systems exist anywhere in the codebase. `reason_crag_baseline` and `reason_raptor_baseline` aren't even the right systems, CRAG and RAPTOR are discussed in the paper's Related Work prose, not in Table I.
16
-
17
- This phase has to build the four real Table I baselines from close to scratch. That's most of the work below. Don't try to reuse the CRAG/RAPTOR baseline functions for Table I, they measure the wrong thing.
18
-
19
- ---
20
-
21
- ## 3.1 Add per-stage latency instrumentation
22
-
23
- **Why:** Table V reports millisecond-precision latency per pipeline stage (guardrail, embedding, retrieval, generation, verification). Right now those numbers have no instrumentation behind them anywhere in the code. This needs to be real before it can be reported.
24
-
25
- **Modify `reasoning_engine.py`, `reason()` method:** wrap each stage with timing and return the breakdown in the response payload.
26
-
27
- ```python
28
- import time
29
-
30
- def reason(self, agent_name: str, query: str, **kwargs) -> Dict[str, Any]:
31
- timings = {}
32
- t0 = time.perf_counter()
33
-
34
- # Stage 1: Domain guardrail
35
- t_stage = time.perf_counter()
36
- in_domain, guardrail_score = self._check_guardrail(query, domain_signature, prompt_analysis)
37
- timings["domain_guardrail_ms"] = round((time.perf_counter() - t_stage) * 1000, 1)
38
-
39
- if not in_domain:
40
- timings["total_ms"] = round((time.perf_counter() - t0) * 1000, 1)
41
- return self._out_of_scope_response(query, guardrail_score, timings)
42
-
43
- # Stage 2: Query embedding
44
- t_stage = time.perf_counter()
45
- query_embedding = list(self.embedding_model.embed([query]))[0]
46
- timings["query_embedding_ms"] = round((time.perf_counter() - t_stage) * 1000, 1)
47
-
48
- # Stage 3: Hybrid retrieval
49
- t_stage = time.perf_counter()
50
- search_results = self.searcher.search(query, agent["id"], top_k=20)
51
- timings["hybrid_retrieval_ms"] = round((time.perf_counter() - t_stage) * 1000, 1)
52
-
53
- # ... reranking, generation, verification stages follow the same pattern ...
54
-
55
- t_stage = time.perf_counter()
56
- answer = self._generate_answer(query, context, sys_prompt)
57
- timings["llm_generation_ms"] = round((time.perf_counter() - t_stage) * 1000, 1)
58
-
59
- t_stage = time.perf_counter()
60
- faithfulness_result = self.deberta_nli_scorer.score(answer, [c.content for c in top_chunks])
61
- timings["faithfulness_verification_ms"] = round((time.perf_counter() - t_stage) * 1000, 1)
62
-
63
- timings["total_ms"] = round((time.perf_counter() - t0) * 1000, 1)
64
-
65
- return {
66
- "answer": answer,
67
- # ... existing fields ...
68
- "timings": timings,
69
- }
70
- ```
71
-
72
- **Acceptance test:** run 10 in-domain queries and confirm `timings` is present and non-zero on every stage in every response. Table V's mean/std values in the paper should come from aggregating this field across the full query set in Phase 3.5's orchestrator, not from typing in numbers by hand.
73
-
74
- ---
75
-
76
- ## 3.2 Add component toggle flags for ablation
77
-
78
- **Why:** Table III (component ablation) needs six configurations: Naive RAG baseline, +Guardrail, +Hybrid Retrieval, +Faithfulness Verification, Hybrid-without-verification, Verification-without-hybrid. There's currently no way to turn individual components on/off in `reason()`, everything is hardwired on.
79
-
80
- **Modify `reasoning_engine.py`:** add a config dataclass and thread it through `reason()`.
81
-
82
- ```python
83
- from dataclasses import dataclass
84
-
85
- @dataclass
86
- class PipelineConfig:
87
- guardrail_enabled: bool = True
88
- retrieval_mode: str = "hybrid" # "semantic", "lexical", or "hybrid"
89
- verification_enabled: bool = True
90
-
91
- def reason(self, agent_name: str, query: str, config: PipelineConfig = None, **kwargs) -> Dict[str, Any]:
92
- config = config or PipelineConfig()
93
-
94
- if config.guardrail_enabled:
95
- in_domain, guardrail_score = self._check_guardrail(query, domain_signature, prompt_analysis)
96
- if not in_domain:
97
- return self._out_of_scope_response(query, guardrail_score, {})
98
- else:
99
- in_domain, guardrail_score = True, 1.0
100
-
101
- if config.retrieval_mode == "semantic":
102
- search_results = self.searcher._semantic_only_search(db, query_embedding, agent["id"], top_k=20)
103
- elif config.retrieval_mode == "lexical":
104
- search_results = self.searcher.lexical_only_search(query, agent["id"], top_k=20) # see 3.5.2, needs to be added
105
- else:
106
- search_results = self.searcher.search(query, agent["id"], top_k=20)
107
-
108
- # ... generation happens the same way regardless of config ...
109
-
110
- if config.verification_enabled:
111
- faithfulness_result = self.deberta_nli_scorer.score(answer, [c.content for c in top_chunks])
112
- else:
113
- faithfulness_result = None # confidence falls back to retrieval/rerank signal only
114
-
115
- # ...
116
- ```
117
-
118
- **Table III configuration mapping for the agent to use later in 3.8:**
119
-
120
- | Configuration | guardrail_enabled | retrieval_mode | verification_enabled |
121
- |---|---|---|---|
122
- | Naive RAG (Baseline) | False | semantic | False |
123
- | + Domain Guardrail | True | semantic | False |
124
- | + Hybrid Retrieval | True | hybrid | False |
125
- | + Faithfulness Verification (full MEXAR) | True | hybrid | True |
126
- | Hybrid without verification | True | hybrid | False |
127
- | Verification without hybrid | True | semantic | True |
128
-
129
- Note rows 3 and 5 are the same configuration, that's expected, the paper's ablation table reuses that row.
130
-
131
- ---
132
-
133
- ## 3.3 Build the retrieval quality metrics module
134
-
135
- **Why:** Table II (P@5, R@10, MRR, nDCG) needs real relevance judgments. Phase 2's query sets include `expected_source_docs` per query, that's the ground truth to score against.
136
-
137
- **New file: `backend/evaluation/retrieval_metrics.py`**
138
-
139
- ```python
140
- """
141
- MEXAR - Retrieval quality metrics: Precision@k, Recall@k, MRR, nDCG.
142
- Scores retrieved chunk source-document IDs against a query's expected_source_docs.
143
- """
144
- import math
145
- from typing import List
146
-
147
- def precision_at_k(retrieved_doc_ids: List[str], relevant_doc_ids: List[str], k: int) -> float:
148
- top_k = retrieved_doc_ids[:k]
149
- if not top_k:
150
- return 0.0
151
- hits = sum(1 for d in top_k if d in relevant_doc_ids)
152
- return hits / len(top_k)
153
-
154
- def recall_at_k(retrieved_doc_ids: List[str], relevant_doc_ids: List[str], k: int) -> float:
155
- if not relevant_doc_ids:
156
- return 0.0
157
- top_k = retrieved_doc_ids[:k]
158
- hits = sum(1 for d in top_k if d in relevant_doc_ids)
159
- return hits / len(relevant_doc_ids)
160
-
161
- def mrr(retrieved_doc_ids: List[str], relevant_doc_ids: List[str]) -> float:
162
- for i, d in enumerate(retrieved_doc_ids, start=1):
163
- if d in relevant_doc_ids:
164
- return 1.0 / i
165
- return 0.0
166
-
167
- def ndcg_at_k(retrieved_doc_ids: List[str], relevant_doc_ids: List[str], k: int) -> float:
168
- def dcg(doc_ids):
169
- return sum(
170
- (1.0 if d in relevant_doc_ids else 0.0) / math.log2(i + 1)
171
- for i, d in enumerate(doc_ids[:k], start=1)
172
- )
173
- ideal = dcg(relevant_doc_ids[:k] + [None] * max(0, k - len(relevant_doc_ids)))
174
- actual = dcg(retrieved_doc_ids)
175
- return actual / ideal if ideal > 0 else 0.0
176
- ```
177
-
178
- **Wiring:** for each query in the Phase 2 query sets, run retrieval (using the `retrieval_mode` toggle from 3.2 to get semantic-only, lexical-only, and hybrid result sets separately), extract the source document ID for each returned chunk, and score against `expected_source_docs`. This is what produces the real Table II numbers, don't hand-write them.
179
-
180
- ---
181
-
182
- ## 3.4 Add a lexical-only search method
183
-
184
- **Why:** needed for both the BM25 Only Table I baseline (3.5) and the "Lexical Only" row of Table II (3.3). No pure lexical-only Python method currently exists, only inside the fused SQL RPC.
185
-
186
- **Modify `backend/utils/hybrid_search.py`:** add alongside `_semantic_only_search`:
187
-
188
- ```python
189
- def lexical_only_search(
190
- self,
191
- query: str,
192
- agent_id: int,
193
- top_k: int = 20
194
- ) -> List[Tuple[DocumentChunk, float]]:
195
- """Pure lexical (ts_rank) search, no semantic component. Used for the BM25 Only baseline and Table II."""
196
- db = SessionLocal()
197
- try:
198
- result = db.execute(text("""
199
- SELECT id, ts_rank_cd(content_tsv, plainto_tsquery('english', :query_text)) AS score
200
- FROM document_chunks
201
- WHERE agent_id = :agent_id
202
- AND content_tsv @@ plainto_tsquery('english', :query_text)
203
- ORDER BY score DESC
204
- LIMIT :top_k
205
- """), {"query_text": query, "agent_id": agent_id, "top_k": top_k})
206
- rows = result.fetchall()
207
- if not rows:
208
- return []
209
- chunk_ids = [row.id for row in rows]
210
- chunks = db.query(DocumentChunk).filter(DocumentChunk.id.in_(chunk_ids)).all()
211
- chunk_map = {c.id: c for c in chunks}
212
- return [(chunk_map[row.id], row.score) for row in rows if row.id in chunk_map]
213
- finally:
214
- db.close()
215
- ```
216
-
217
- Verify the actual tsvector column name against `backend/migrations/hybrid_search_function.sql`, the skeleton above assumes `content_tsv`, confirm before trusting it.
218
-
219
- ---
220
-
221
- ## 3.5 Build the four real Table I baselines
222
-
223
- **Why:** these don't exist yet. Building them properly is most of this phase's effort.
224
-
225
- ### 3.5.1 Naive RAG
226
-
227
- Semantic-only search (reuse `_semantic_only_search`), no guardrail, no verification, Llama 3 8B generation. This is just `PipelineConfig(guardrail_enabled=False, retrieval_mode="semantic", verification_enabled=False)` from 3.2, but generate the answer with a plain unconstrained system prompt (no citation instructions, no domain constraints) to match the paper's description of it as the true baseline. Add a `reason_naive_rag_baseline()` method to `reasoning_engine.py` that wraps this configuration.
228
-
229
- ### 3.5.2 BM25 Only
230
-
231
- Same idea, `retrieval_mode="lexical"` using the new `lexical_only_search()` from 3.4, no guardrail, no verification. Add `reason_bm25_baseline()`.
232
-
233
- ### 3.5.3 LangChain
234
-
235
- This one is genuinely separate infrastructure, not a config toggle. It needs its own small standalone pipeline, not routed through `ReasoningEngine` at all.
236
-
237
- **New file: `backend/evaluation/langchain_baseline.py`**
238
- ```python
239
- """
240
- MEXAR - LangChain baseline for Table I.
241
- Standard LangChain RetrievalQA pipeline with Chroma vector store and default config,
242
- built from the SAME source documents as the MEXAR corpus, kept separate from the
243
- production Supabase-backed pipeline entirely.
244
- """
245
- from langchain_community.vectorstores import Chroma
246
- from langchain_community.embeddings import HuggingFaceEmbeddings
247
- from langchain.chains import RetrievalQA
248
- from langchain_groq import ChatGroq # or whatever LLM wrapper matches the Groq backbone used elsewhere
249
- import os
250
-
251
- def build_langchain_pipeline(source_documents: list, persist_dir: str = "./chroma_baseline_db"):
252
- embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-en-v1.5") # match MEXAR's embedding model for a fair comparison
253
- vectorstore = Chroma.from_texts(source_documents, embeddings, persist_directory=persist_dir)
254
- llm = ChatGroq(model="llama3-8b-8192", groq_api_key=os.environ["GROQ_API_KEY"])
255
- qa_chain = RetrievalQA.from_chain_type(
256
- llm=llm,
257
- chain_type="stuff",
258
- retriever=vectorstore.as_retriever(search_kwargs={"k": 5}),
259
- return_source_documents=True,
260
- )
261
- return qa_chain
262
-
263
- def run_langchain_baseline(qa_chain, query: str) -> dict:
264
- result = qa_chain.invoke({"query": query})
265
- return {
266
- "answer": result["result"],
267
- "source_documents": [d.page_content for d in result.get("source_documents", [])],
268
- }
269
- ```
270
- Add `langchain`, `langchain-community`, `langchain-groq` to `backend/requirements.txt`, scoped to the evaluation environment if it's kept separate from the production deploy (Hugging Face Spaces doesn't need these, only the evaluation run does).
271
-
272
- **Fairness note for the agent:** embed the same corpus (Phase 2's ~500 docs) into this separate Chroma store so the comparison is apples-to-apples with MEXAR's Supabase-backed corpus, not a different or smaller dataset.
273
-
274
- ### 3.5.4 Self-RAG — decision point, don't skip this
275
-
276
- The paper says "Public checkpoint implementing reflection tokens for retrieval critique." The real Self-RAG checkpoint is `selfrag/selfrag_llama2_7b`, a 7B-parameter model that needs to actually run inference with reflection-token-aware decoding, not just a differently-worded prompt on Llama 3 8B via Groq (that would repeat the exact CRAG/RAPTOR mistake flagged in 3.0).
277
-
278
- Two honest paths, pick one explicitly, don't default silently:
279
-
280
- - **Path A (real):** run the actual `selfrag/selfrag_llama2_7b` checkpoint via a Hugging Face Inference Endpoint or local GPU inference (needs roughly 16GB+ VRAM for reasonable speed, or a quantized version if hardware is constrained). This matches the paper's claim exactly and is the safer choice if a reviewer probes it.
281
- - **Path B (disclosed proxy):** if GPU access isn't available in the timeline, build a prompted proxy (Llama 3 8B instructed to emit reflection-style self-critique tokens before answering) and **change the paper's baseline description** to say so explicitly, e.g. "a prompted approximation of Self-RAG's reflection mechanism, as the original 7B checkpoint was not feasible to deploy at evaluation scale." This is a legitimate disclosed limitation. What's not legitimate is calling it "Self-RAG" in Table I without the caveat.
282
-
283
- Flag this decision to Dev before the agent proceeds, don't have the agent silently pick Path B and let it read as Path A in the paper.
284
-
285
- ### 3.5.5 Update `baseline_runner.py`
286
-
287
- Rewrite it to run all five systems (Naive RAG, BM25 Only, LangChain, Self-RAG, MEXAR) per query and log faithfulness, confidence, and retrieval metrics for each, replacing the current CRAG/RAPTOR comparison entirely:
288
-
289
- ```python
290
- def run_table_1_comparison(agent_name: str, queries: list, domain: str):
291
- engine = create_reasoning_engine()
292
- results = {"Naive RAG": [], "BM25 Only": [], "LangChain": [], "Self-RAG": [], "MEXAR": []}
293
- langchain_chain = build_langchain_pipeline(...) # loaded once per domain
294
-
295
- for q in queries:
296
- results["Naive RAG"].append(engine.reason_naive_rag_baseline(agent_name, q))
297
- results["BM25 Only"].append(engine.reason_bm25_baseline(agent_name, q))
298
- results["LangChain"].append(run_langchain_baseline(langchain_chain, q))
299
- results["Self-RAG"].append(run_self_rag_baseline(q)) # per 3.5.4's chosen path
300
- results["MEXAR"].append(engine.reason(agent_name, q))
301
-
302
- return results
303
- ```
304
- Save full raw output per query, not just averages, Phase 4 needs the per-query pairs for McNemar's test.
305
-
306
- ---
307
-
308
- ## 3.6 Expand guardrail analysis to real Table IV
309
-
310
- **Why:** the current `guardrail_analysis.py` only prints results for 4 hardcoded queries. Table IV needs precision/recall/F1 per domain-pair (Medical→Legal, Medical→Financial, etc.) computed over the full out-of-domain query set from Phase 2.
311
-
312
- **Rewrite `backend/evaluation/guardrail_analysis.py`:**
313
- ```python
314
- def run_table_4_analysis(query_sets_dir: str = "test_data/query_sets"):
315
- engine = create_reasoning_engine()
316
- domain_pairs = [
317
- ("medical", "legal"), ("medical", "financial"),
318
- ("legal", "medical"), ("legal", "financial"),
319
- ("financial", "medical"), ("financial", "legal"),
320
- ]
321
- results = {}
322
- for source_domain, target_agent_domain in domain_pairs:
323
- queries = load_out_of_domain_queries(query_sets_dir, source_domain, target_agent_domain)
324
- tp = fp = fn = tn = 0
325
- for item in queries:
326
- res = engine.reason(f"{target_agent_domain}_agent", item["query"])
327
- rejected = not res["in_domain"]
328
- truly_out_of_scope = not item["is_in_domain"]
329
- if rejected and truly_out_of_scope: tp += 1
330
- elif rejected and not truly_out_of_scope: fp += 1
331
- elif not rejected and truly_out_of_scope: fn += 1
332
- else: tn += 1
333
- precision = tp / (tp + fp) if (tp + fp) else 0
334
- recall = tp / (tp + fn) if (tp + fn) else 0
335
- f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0
336
- results[f"{source_domain}_to_{target_agent_domain}"] = {
337
- "rejected": tp, "total": len(queries), "precision": round(precision, 2),
338
- "recall": round(recall, 2), "f1": round(f1, 2),
339
- }
340
- return results
341
- ```
342
- `load_out_of_domain_queries` needs to filter Phase 2's query set JSON by domain-pair, the agent should implement this against whatever schema Phase 2 actually produced.
343
-
344
- ---
345
-
346
- ## 3.7 Run the Table III ablation
347
-
348
- Using the `PipelineConfig` toggles from 3.2 and the configuration mapping table in that section, run each of the six configurations against the full in-domain query set (all three domains combined, matching how the paper's Table III doesn't split by domain) and record faithfulness scores per configuration. Average per configuration, compute the delta vs. Naive RAG baseline, and check whether the superadditive pattern the paper claims (combined effect > sum of individual effects) actually holds on real data, don't assume it will, that's an empirical claim that needs to survive contact with real numbers.
349
-
350
- ---
351
-
352
- ## 3.8 Build the calibration (ECE) module
353
-
354
- **Why:** Figure 4's reliability diagram and the ECE = 0.06 claim need a real calculator, none exists in the repo currently.
355
-
356
- **New file: `backend/evaluation/calibration.py`**
357
- ```python
358
- """
359
- MEXAR - Expected Calibration Error and reliability diagram data.
360
- """
361
- from typing import List, Tuple
362
- import numpy as np
363
-
364
- def expected_calibration_error(confidences: List[float], correctness: List[bool], n_bins: int = 10) -> float:
365
- """
366
- confidences: predicted confidence per query (0-1)
367
- correctness: whether the answer was actually correct per query (from human/expert review)
368
- """
369
- bins = np.linspace(0, 1, n_bins + 1)
370
- ece = 0.0
371
- n = len(confidences)
372
- for i in range(n_bins):
373
- lo, hi = bins[i], bins[i + 1]
374
- in_bin = [(c, corr) for c, corr in zip(confidences, correctness) if lo <= c < hi or (i == n_bins - 1 and c == hi)]
375
- if not in_bin:
376
- continue
377
- bin_conf = np.mean([c for c, _ in in_bin])
378
- bin_acc = np.mean([1.0 if corr else 0.0 for _, corr in in_bin])
379
- ece += (len(in_bin) / n) * abs(bin_conf - bin_acc)
380
- return round(ece, 4)
381
-
382
- def reliability_diagram_data(confidences: List[float], correctness: List[bool], n_bins: int = 10) -> List[Tuple[float, float]]:
383
- """Returns (mean_predicted_confidence, observed_accuracy) per bin, for plotting Figure 4."""
384
- bins = np.linspace(0, 1, n_bins + 1)
385
- points = []
386
- for i in range(n_bins):
387
- lo, hi = bins[i], bins[i + 1]
388
- in_bin = [(c, corr) for c, corr in zip(confidences, correctness) if lo <= c < hi or (i == n_bins - 1 and c == hi)]
389
- if not in_bin:
390
- continue
391
- bin_conf = float(np.mean([c for c, _ in in_bin]))
392
- bin_acc = float(np.mean([1.0 if corr else 0.0 for _, corr in in_bin]))
393
- points.append((bin_conf, bin_acc))
394
- return points
395
- ```
396
-
397
- **Important:** `correctness` needs to come from actual human judgment of whether each answer was right, not from the faithfulness score itself, otherwise ECE would be measuring the faithfulness scorer's agreement with itself, which is circular in the same way the paper criticizes elsewhere. This means someone (Dev, Om, or Jay) needs to mark each query's MEXAR answer as correct/incorrect by hand, this is a real annotation task, budget time for it in this phase, it can't be automated away.
398
-
399
- ---
400
-
401
- ## 3.9 Effect size and significance summary
402
-
403
- Reuse the existing `statistical_tests.py` McNemar implementation (it's legitimate, no changes needed there) but wrap it to also compute Cohen's d for the forest plot in Figure 3:
404
-
405
- ```python
406
- def cohens_d(scores_a: list, scores_b: list) -> float:
407
- import numpy as np
408
- mean_diff = np.mean(scores_a) - np.mean(scores_b)
409
- pooled_std = np.sqrt((np.std(scores_a, ddof=1)**2 + np.std(scores_b, ddof=1)**2) / 2)
410
- return round(mean_diff / pooled_std, 3) if pooled_std > 0 else 0.0
411
- ```
412
- Run this for every pairwise comparison Figure 3 shows (Hybrid vs Lexical Only, Hybrid vs Semantic Only, MEXAR vs Self-RAG, MEXAR vs LangChain, MEXAR vs BM25 Only, MEXAR vs Naive RAG), using the per-query score arrays saved in 3.5.5.
413
-
414
- ---
415
-
416
- ## 3.10 Orchestrator script
417
-
418
- **New file: `backend/evaluation/run_all.py`**, ties everything above together and writes results to `evaluation_outputs/`:
419
-
420
- ```python
421
- """
422
- MEXAR - Full evaluation orchestrator. Run this once Phase 1-2 and all of Phase 3's
423
- components are in place. Produces every number Phase 4 needs to update the paper.
424
- """
425
- import json
426
- import os
427
- from datetime import datetime
428
-
429
- OUTPUT_DIR = "evaluation_outputs"
430
-
431
- def main():
432
- os.makedirs(OUTPUT_DIR, exist_ok=True)
433
- run_id = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
434
-
435
- table1 = run_table_1_comparison_all_domains() # 3.5
436
- table2 = run_retrieval_ablation_all_domains() # 3.3 + 3.4
437
- table3 = run_table_3_ablation() # 3.7
438
- table4 = run_table_4_analysis() # 3.6
439
- table5 = aggregate_latency_stats() # 3.1
440
- calibration = run_calibration_analysis() # 3.8
441
- significance = run_all_significance_tests(table1) # 3.9
442
-
443
- payload = {
444
- "run_id": run_id,
445
- "table1_system_comparison": table1,
446
- "table2_retrieval_ablation": table2,
447
- "table3_component_ablation": table3,
448
- "table4_guardrail_boundary": table4,
449
- "table5_latency": table5,
450
- "calibration": calibration,
451
- "significance_tests": significance,
452
- }
453
- out_path = os.path.join(OUTPUT_DIR, f"full_evaluation_{run_id}.json")
454
- with open(out_path, "w") as f:
455
- json.dump(payload, f, indent=2)
456
- print(f"Full evaluation saved to {out_path}")
457
-
458
- if __name__ == "__main__":
459
- main()
460
- ```
461
-
462
- ---
463
-
464
- ## 3.11 Sanity checklist before trusting any number
465
-
466
- Before handing anything to Phase 4, check:
467
-
468
- - [ ] Every table's numbers came from `evaluation_outputs/full_evaluation_*.json`, not typed in by hand.
469
- - [ ] Self-RAG's scope (Path A or B from 3.5.4) is explicitly recorded, so Phase 4 knows how to describe it in the paper.
470
- - [ ] The ECE calculation used real human-marked correctness labels, not the faithfulness score used as a stand-in for correctness.
471
- - [ ] The superadditive claim in Table III (35% > 23% + 18%) is checked against the real numbers, if it doesn't hold, the paper's Section V-B prose needs to change too, not just the table.
472
- - [ ] Latency numbers came from actual `timings` fields aggregated across a real run, not the current placeholder table.
473
- - [ ] LangChain baseline used the same ~500-doc corpus as MEXAR, not a smaller or different sample.
474
-
475
- ---
476
-
477
- ## Summary of files touched/created in Phase 3
478
-
479
- | File | Change |
480
- |---|---|
481
- | `backend/modules/reasoning_engine.py` | add timing instrumentation, `PipelineConfig`, `reason_naive_rag_baseline()`, `reason_bm25_baseline()` |
482
- | `backend/utils/hybrid_search.py` | add `lexical_only_search()` |
483
- | `backend/evaluation/retrieval_metrics.py` | **new** — P@k, R@k, MRR, nDCG |
484
- | `backend/evaluation/langchain_baseline.py` | **new** — real LangChain/Chroma pipeline |
485
- | `backend/evaluation/self_rag_baseline.py` | **new** — real or disclosed-proxy Self-RAG (per 3.5.4 decision) |
486
- | `backend/evaluation/baseline_runner.py` | rewritten to run real Table I comparison |
487
- | `backend/evaluation/guardrail_analysis.py` | rewritten to compute real Table IV over Phase 2's out-of-domain queries |
488
- | `backend/evaluation/calibration.py` | **new** — ECE + reliability diagram data |
489
- | `backend/evaluation/run_all.py` | **new** — orchestrator, single entry point for the full evaluation |
490
- | `backend/requirements.txt` | add langchain, langchain-community, langchain-groq (evaluation-only if kept separate from prod) |
491
-
492
- ## What Phase 3 does NOT cover
493
-
494
- - Editing the paper itself, that's Phase 4.
495
- - Preparing or rehearsing the live demo, that's Phase 4.
496
-
497
- Once `evaluation_outputs/full_evaluation_*.json` exists with real numbers, move to the Phase 4 document.