citationEdge / scripts /run_batch_test.py
omkarkudalkar222's picture
Fresh deployment without history
0e38162
Raw
History Blame Contribute Delete
18.2 kB
# -*- coding: utf-8 -*-
#!/usr/bin/env python3
import os, sys
os.environ["PYTHONIOENCODING"] = "utf-8"
if sys.stdout.encoding != "utf-8":
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
"""
Batch pipeline runner for test_5_papers.
Runs CitationEdge on each PDF, saves raw JSON results to test_5_papers_sol/,
then writes a human-interpretable analysis report.
Usage:
python scripts/run_batch_test.py
"""
from __future__ import annotations
import asyncio
import json
import sys
import time
from pathlib import Path
from datetime import datetime
sys.path.insert(0, str(Path(__file__).parent.parent))
from orchestrators.custom_orchestrator import CitationEdgeOrchestrator
from utils.logger import get_logger
logger = get_logger("batch_test")
INPUT_DIR = Path(__file__).parent.parent / "test_5_papers"
OUTPUT_DIR = Path(__file__).parent.parent / "test_5_papers_sol"
OUTPUT_DIR.mkdir(exist_ok=True)
# ── Pipeline runner ────────────────────────────────────────────────────────────
async def run_one(orchestrator: CitationEdgeOrchestrator, pdf: Path) -> dict:
job_id = f"batch_{pdf.stem}"
logger.info(f"\n{'='*65}")
logger.info(f">> {pdf.name}")
logger.info(f"{'='*65}")
t0 = time.monotonic()
try:
result = await orchestrator.run(job_id=job_id, pdf_path=str(pdf))
result["_pdf"] = pdf.name
result["_wall_s"] = round(time.monotonic() - t0, 1)
except Exception as exc:
logger.error(f"Pipeline crashed for {pdf.name}: {exc}")
result = {"_pdf": pdf.name, "error": str(exc), "status": "crashed",
"_wall_s": round(time.monotonic() - t0, 1)}
# Persist raw result
out = OUTPUT_DIR / f"{pdf.stem}_result.json"
out.write_text(json.dumps(result, indent=2, default=str), encoding="utf-8")
logger.info(f"βœ“ Saved β†’ {out.name}")
return result
async def main():
pdfs = sorted(INPUT_DIR.glob("*.pdf"))
if not pdfs:
logger.error(f"No PDFs found in {INPUT_DIR}")
return
logger.info(f"Found {len(pdfs)} papers: {[p.name for p in pdfs]}")
orchestrator = CitationEdgeOrchestrator()
all_results = []
for pdf in pdfs:
r = await run_one(orchestrator, pdf)
all_results.append(r)
# Save combined results
combined = OUTPUT_DIR / "_all_results.json"
combined.write_text(json.dumps(all_results, indent=2, default=str), encoding="utf-8")
# Generate human-interpretable analysis report
report = build_analysis_report(all_results)
report_path = OUTPUT_DIR / "_analysis_report.md"
report_path.write_text(report, encoding="utf-8")
logger.info(f"\nβœ“ Analysis report β†’ {report_path}")
print(report)
# ── Analysis report builder ────────────────────────────────────────────────────
SCORE_KEYS = [
"overall_score",
"claim_quality_score",
"citation_health_score",
"argumentation_score",
"novelty_score",
"evidence_score",
]
EXPECTED_RANGES = {
"overall_score": (4.0, 10.0),
"claim_quality_score": (4.0, 10.0),
"citation_health_score":(3.0, 10.0),
"argumentation_score": (3.0, 10.0),
"novelty_score": (3.0, 10.0),
"evidence_score": (3.0, 10.0),
}
VERDICT_PASS = {"supported", "likely_supported", "verified"}
VERDICT_REJECT = {"refuted", "contradicted", "likely_refuted"}
DOMAIN_NOTES = {
"llm_reasoning_divide_conquer": "LLM reasoning β€” expect high novelty, rich claims, moderate citation gaps",
"kg_embedding_survey": "Survey paper β€” expect many citations, low gaps, lower novelty score",
"federated_chest_radiograph": "Applied FL/medical β€” expect strong evidence, moderate claims",
"yolov10_endtoend": "CV/detection paper β€” expect strong empirical claims, high citation health",
"gnn_deeper": "Architecture paper β€” expect high novelty, methodological claims",
}
def _score_badge(val: float | None, key: str) -> str:
if val is None:
return "N/A"
lo, hi = EXPECTED_RANGES.get(key, (4.0, 10.0))
mark = "βœ…" if lo <= val <= hi else ("⚠️" if val < lo else "⬆️")
return f"{val:.1f}/10 {mark}"
def build_analysis_report(results: list[dict]) -> str:
now = datetime.now().strftime("%Y-%m-%d %H:%M")
lines = [
f"# CitationEdge Batch Test β€” Analysis Report",
f"*Generated: {now} | Papers: {len(results)}*",
"",
"---",
"",
"## 1. Pipeline Execution Summary",
"",
f"| Paper | Status | Duration |",
f"|-------|--------|----------|",
]
for r in results:
name = r.get("_pdf", "?")
status = r.get("status", "?")
dur = r.get("_wall_s") or r.get("duration_s", "?")
icon = "βœ…" if "complet" in str(status) else "❌"
lines.append(f"| `{name}` | {icon} {status} | {dur}s |")
# ── Per-paper deep dive ───────────────────────────────────────────────────
lines += ["", "---", "", "## 2. Per-Paper Analysis", ""]
for r in results:
stem = Path(r.get("_pdf", "unknown")).stem
lines += [f"### πŸ“„ {r.get('_pdf', stem)}", ""]
domain_note = DOMAIN_NOTES.get(stem, "")
if domain_note:
lines += [f"> **Domain context:** {domain_note}", ""]
if r.get("error"):
lines += [f"**ERROR:** `{r['error']}`", ""]
continue
scores = r.get("scores", {})
claims = r.get("claims", [])
gaps = r.get("citation_gaps", [])
args = r.get("argument_graph", [])
cf = r.get("counterfactuality", {})
claim_cfs = r.get("claim_counterfactualities", [])
keywords = r.get("keywords", [])
agents = r.get("agents", {})
# Scores table
lines += ["#### Scores", ""]
lines += ["| Metric | Value | Expected? |", "|--------|-------|-----------|"]
for k in SCORE_KEYS:
v = scores.get(k)
lines.append(f"| {k.replace('_', ' ').title()} | {_score_badge(v, k)} | {EXPECTED_RANGES.get(k, '')} |")
# Agent statuses
failed = [a for a, d in agents.items() if d.get("status") == "failed"]
if failed:
lines += ["", f"⚠️ **Failed agents:** {', '.join(failed)}"]
# Keywords
if keywords:
top_kw = ", ".join(f"`{k}`" for k in keywords[:10])
lines += ["", f"**Keywords ({len(keywords)}):** {top_kw}"]
exp_note = _interpret_keywords(keywords, stem)
if exp_note:
lines += [f" β†’ *{exp_note}*"]
# Claims
lines += ["", f"**Claims extracted:** {len(claims)}"]
if claims:
supported = sum(1 for c in claims if c.get("verdict") in VERDICT_PASS)
refuted = sum(1 for c in claims if c.get("verdict") in VERDICT_REJECT)
unverif = len(claims) - supported - refuted
lines += [
f" - Supported/verified: {supported}",
f" - Refuted/contradicted: {refuted}",
f" - Unverified/unknown: {unverif}",
]
top_claims = [c for c in claims if c.get("text")][:3]
if top_claims:
lines += ["", " *Top claims:*"]
for i, c in enumerate(top_claims, 1):
verdict = c.get("verdict", "?")
conf = c.get("verify_confidence") or c.get("confidence") or "?"
text = (c["text"][:120] + "…") if len(c.get("text","")) > 120 else c.get("text","")
lines.append(f" {i}. [{verdict}] ({conf:.2f} conf) β€” {text}" if isinstance(conf, float) else
f" {i}. [{verdict}] β€” {text}")
lines += _interpret_claims(claims, stem)
# Citation gaps
lines += ["", f"**Citation gaps:** {len(gaps)}"]
if gaps:
high_sev = [g for g in gaps if g.get("severity") in ("high", "critical")]
if high_sev:
lines += [f" ⚠️ High-severity gaps: {len(high_sev)}"]
for g in gaps[:3]:
desc = (g.get("gap","")[:100] + "…") if len(g.get("gap","")) > 100 else g.get("gap","")
lines.append(f" - [{g.get('severity','?')}] {desc}")
lines += _interpret_gaps(gaps, stem)
# Counterfactuality
if cf:
cf_score = cf.get("overall_score", "?")
lines += [
"",
f"**Counterfactuality score:** {cf_score}",
f" Common type: {cf.get('common_type','?')}",
]
if cf.get("summary"):
lines += [f" Summary: {cf['summary'][:200]}"]
lines += _interpret_cf(cf, claim_cfs, stem)
# Argumentation
if args:
strengths = args[0].get("strengths", []) if args else []
weaknesses = args[0].get("weaknesses", []) if args else []
lines += ["", "**Argumentation:**"]
if strengths:
lines += [f" Strengths: {'; '.join(str(s) for s in strengths[:2])}"]
if weaknesses:
lines += [f" Weaknesses: {'; '.join(str(w) for w in weaknesses[:2])}"]
# Human verdict
lines += ["", "#### πŸ§‘β€πŸ”¬ Human Interpretation"]
lines += _human_verdict(r, stem)
lines += ["", "---", ""]
# ── Cross-paper comparison ────────────────────────────────────────────────
lines += ["", "## 3. Cross-Paper Comparison", ""]
lines += ["| Paper | Overall | Claims | Gaps | CF Score |",
"|-------|---------|--------|------|----------|"]
for r in results:
if r.get("error"):
lines.append(f"| `{r.get('_pdf','?')}` | ERROR | β€” | β€” | β€” |")
continue
sc = r.get("scores", {})
ov = f"{sc.get('overall_score','?'):.1f}" if isinstance(sc.get("overall_score"), float) else "?"
ncl = len(r.get("claims", []))
ngp = len(r.get("citation_gaps", []))
cf = r.get("counterfactuality", {}).get("overall_score", "?")
lines.append(f"| `{r.get('_pdf','?')}` | {ov}/10 | {ncl} | {ngp} | {cf} |")
# ── Overall verdict ───────────────────────────────────────────────────────
lines += ["", "## 4. Overall Pipeline Quality Assessment", ""]
valid = [r for r in results if not r.get("error")]
if valid:
avg_overall = sum(
r.get("scores", {}).get("overall_score", 0) for r in valid
if isinstance(r.get("scores", {}).get("overall_score"), (int, float))
) / max(len(valid), 1)
crash_count = sum(1 for r in results if r.get("error"))
pass_count = sum(1 for r in results if "complet" in str(r.get("status","")))
lines += [
f"- **Papers processed:** {len(results)}",
f"- **Successful runs:** {pass_count}/{len(results)}",
f"- **Average overall score:** {avg_overall:.1f}/10",
f"- **Crash rate:** {crash_count}/{len(results)}",
"",
]
if avg_overall >= 6.0 and pass_count == len(results):
lines += ["βœ… **Pipeline is performing as expected across all test papers.**"]
elif avg_overall >= 5.0 and pass_count >= len(results) * 0.8:
lines += ["⚠️ **Pipeline mostly functional but some papers show degraded output. Review failed agents.**"]
else:
lines += ["❌ **Pipeline quality is below expected threshold. Investigate failures.**"]
lines += [
"",
"---",
f"*Report auto-generated by `scripts/run_batch_test.py`*",
]
return "\n".join(lines)
# ── Domain-aware interpretation helpers ───────────────────────────────────────
def _interpret_keywords(kws: list, stem: str) -> list[str]:
kw_set = {k.lower() for k in kws}
notes = []
if stem == "kg_embedding_survey" and any("embedding" in k or "knowledge" in k for k in kw_set):
notes.append("Keywords correctly capture KG embedding themes βœ…")
elif stem == "yolov10_endtoend" and any("detection" in k or "yolo" in k for k in kw_set):
notes.append("Keywords correctly capture detection/YOLO themes βœ…")
elif stem == "llm_reasoning_divide_conquer" and any("reasoning" in k or "llm" in k or "language model" in k for k in kw_set):
notes.append("Keywords correctly capture LLM reasoning themes βœ…")
elif stem == "federated_chest_radiograph" and any("federated" in k or "radiograph" in k or "chest" in k for k in kw_set):
notes.append("Keywords correctly capture federated medical imaging themes βœ…")
elif stem == "gnn_deeper" and any("graph" in k or "neural" in k or "gnn" in k for k in kw_set):
notes.append("Keywords correctly capture GNN themes βœ…")
else:
notes.append("Keywords may not fully reflect domain β€” review KBIR output")
return notes
def _interpret_claims(claims: list, stem: str) -> list[str]:
notes = []
if not claims:
return [" ⚠️ No claims extracted β€” check ClaimAgent and section parsing"]
supported = sum(1 for c in claims if c.get("verdict") in VERDICT_PASS)
support_pct = supported / len(claims) * 100 if claims else 0
if support_pct >= 60:
notes.append(f" βœ… {support_pct:.0f}% claim support rate is healthy for a research paper")
elif support_pct >= 30:
notes.append(f" ⚠️ {support_pct:.0f}% support rate β€” moderate; some claims may be speculative")
else:
notes.append(f" ❌ Low support rate ({support_pct:.0f}%) β€” claims may be under-evidenced or verifier struggled")
if stem == "kg_embedding_survey":
notes.append(" Survey papers should have high citation support β€” low rate would indicate verifier gap")
return notes
def _interpret_gaps(gaps: list, stem: str) -> list[str]:
notes = []
high = sum(1 for g in gaps if g.get("severity") in ("high","critical"))
if stem == "kg_embedding_survey" and len(gaps) <= 3:
notes.append(f" βœ… Survey paper with few gaps ({len(gaps)}) β€” expected, surveys are typically well-cited")
elif high >= 3:
notes.append(f" ⚠️ {high} high-severity gaps β€” paper may have weak citation coverage in key areas")
elif len(gaps) == 0:
notes.append(" βœ… No citation gaps found β€” good coverage, or CitationGapAgent may need more context")
return notes
def _interpret_cf(cf: dict, claim_cfs: list, stem: str) -> list[str]:
notes = []
score = cf.get("overall_score")
if isinstance(score, (int, float)):
if score >= 7:
notes.append(" βœ… High counterfactuality score β€” claims are well-grounded and falsifiable")
elif score >= 4:
notes.append(" ⚠️ Moderate counterfactuality β€” some claims lack clear falsifiability")
else:
notes.append(" ❌ Low counterfactuality β€” paper may contain speculative or unfalsifiable claims")
high_cf = [c for c in claim_cfs if isinstance(c.get("counterfactual_score"), (int,float)) and c["counterfactual_score"] >= 0.7]
if high_cf:
notes.append(f" {len(high_cf)} claim(s) flagged as highly counterfactual β€” may warrant manual review")
return notes
def _human_verdict(r: dict, stem: str) -> list[str]:
scores = r.get("scores", {})
claims = r.get("claims", [])
gaps = r.get("citation_gaps", [])
agents = r.get("agents", {})
overall = scores.get("overall_score")
failed = [a for a, d in agents.items() if d.get("status") == "failed"]
notes = []
# Overall score interpretation
if isinstance(overall, float):
if overall >= 7.5:
notes.append(f"Score {overall:.1f}/10 β†’ **Strong paper.** Claims are well-supported and argumentation is solid.")
elif overall >= 5.5:
notes.append(f"Score {overall:.1f}/10 β†’ **Adequate paper.** Reasonable quality with some gaps or weak evidence.")
else:
notes.append(f"Score {overall:.1f}/10 β†’ **Below average.** Significant weaknesses in claims, citations, or argumentation.")
# Domain-specific expected behaviour
expected = {
"kg_embedding_survey": "Survey: expect many citations, broad keyword coverage, moderate novelty.",
"llm_reasoning_divide_conquer": "LLM: expect novel claims, empirical evidence, recent references.",
"federated_chest_radiograph": "Medical FL: expect strong methodological claims, clinical evidence.",
"yolov10_endtoend": "CV: expect quantitative claims (mAP, FPS), strong benchmark citations.",
"gnn_deeper": "GNN architecture: expect theoretical + empirical claims, ablation studies.",
}.get(stem, "")
if expected:
notes.append(f"**Expected profile:** {expected}")
# Failure flags
if failed:
notes.append(f"**⚠️ Pipeline gaps:** Agents `{', '.join(failed)}` failed β€” results may be incomplete.")
if not claims:
notes.append("**⚠️ No claims extracted** β€” ClaimAgent may have failed or sections were not parsed.")
if not gaps and stem != "kg_embedding_survey":
notes.append("CitationGap found nothing β€” could be thorough citations or gap detection needs tuning.")
if not notes:
notes.append("Pipeline ran cleanly with no anomalies detected.")
return [f"- {n}" for n in notes]
if __name__ == "__main__":
asyncio.run(main())