File size: 3,362 Bytes
b137166
 
 
 
31ecd3c
 
 
b137166
 
 
 
 
 
 
 
 
 
31ecd3c
 
 
 
 
b137166
31ecd3c
 
b137166
 
 
31ecd3c
 
 
 
b137166
31ecd3c
 
b137166
 
 
 
 
 
 
 
 
31ecd3c
b137166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31ecd3c
 
 
 
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
"""PaperRank command line: the same pipeline as the web app, printed to the terminal.

    python main.py "effects of microplastics on gut microbiome in zebrafish larvae"
"""
import argparse
import sys

from dotenv import load_dotenv

load_dotenv()

from analyzer import LLMError, assess_coverage, decompose, find_gaps
from reranker import AVAILABLE_MODELS, DEFAULT_MODEL, load_model, rerank
from retriever import multi_search


STATUS_MARK = {"covered": "✓", "partial": "~", "missing": "✗"}


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        prog="paperrank",
        description="Find papers for a research question, check aspect coverage, and spot possible gaps",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument("question", type=str, help="Research question in plain language")
    parser.add_argument("--model", default=DEFAULT_MODEL, choices=AVAILABLE_MODELS, help="Reranker model")
    parser.add_argument("--top", type=int, default=5, help="Papers to analyze and display")
    return parser.parse_args()


def main() -> None:
    sys.stdout.reconfigure(encoding="utf-8")  # Windows consoles default to cp1252
    args = parse_args()

    print("[1/4] Breaking the question into aspects ...")
    plan = decompose(args.question)
    print("      Aspects    :", ", ".join(a["name"] for a in plan["aspects"]) or "(none: LLM unavailable)")
    print("      Sub-queries:", *plan["sub_queries"], sep="\n        - ")

    print("[2/4] Searching OpenAlex ...")
    papers = multi_search(plan["sub_queries"])
    if not papers:
        print("No papers with abstracts found. Try rephrasing.", file=sys.stderr)
        sys.exit(1)

    print(f"[3/4] Reranking {len(papers)} candidates with {args.model} ...")
    top = rerank(args.question, papers, load_model(args.model))[:args.top]

    print("[4/4] Checking coverage ...")
    try:
        assessments = assess_coverage(args.question, plan["aspects"], top)
    except LLMError as e:
        print(f"      Coverage unavailable: {e}")
        assessments = [None] * len(top)

    for paper, r in zip(top, assessments):
        coverage = f"{r['coverage']}% coverage" if r and r["coverage"] is not None else "not analyzed"
        print(f"\n#{paper['ce_rank']} [{coverage}, match {paper['ce_score']:.2f}] {paper['title']}")
        print(f"   {paper.get('year') or 'n.d.'} · {paper.get('citationCount') or 0} citations · {paper.get('url', '')}")
        for a in (r or {}).get("aspects", []):
            print(f"   {STATUS_MARK[a['status']]} {a['name']}" + (f'  "{a["evidence"]}"' if a["evidence"] else ""))
        if r and r["note"]:
            print(f"   not addressed: {r['note']}")

    if all(assessments):
        gaps = find_gaps(plan["aspects"], assessments)
        print("\nPossible gaps (leads to verify, not proof)")
        print("  Not fully covered by any top paper:", ", ".join(gaps["uncovered"]) or "none")
        if gaps["full_count"] is not None:
            print(f"  All aspects together ({gaps['full_query']!r}): {gaps['full_count']} papers")
            for b in gaps["bottlenecks"]:
                ratio = b["count_without"] / max(gaps["full_count"], 1)
                print(f"    without '{b['aspect']}': {b['count_without']} papers ({ratio:.0f}x more)")


if __name__ == "__main__":
    main()