File size: 18,162 Bytes
0e38162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# -*- 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())