PaperRank / main.py
samwjya
Claude Opus 5.5
Repair CLI: main.py runs the full pipeline
b137166
Raw History Blame Contribute Delete
3.36 kB
"""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()