File size: 3,210 Bytes
056cefb
 
 
 
 
37a21b0
056cefb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3039836
 
 
4a9ccd7
 
 
056cefb
 
 
 
 
 
 
 
 
37a21b0
 
056cefb
3039836
 
 
 
 
 
4a9ccd7
3039836
056cefb
37a21b0
 
 
 
 
 
 
 
056cefb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37a21b0
056cefb
37a21b0
 
 
 
 
056cefb
 
 
 
 
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
"""Typer CLI entrypoint: `python -m src.cli review "topic"`."""

from __future__ import annotations

import os
import time

import typer
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel

from . import utils

app = typer.Typer(add_completion=False, help="Personal research agent.")
console = Console()


@app.command()
def review(
    topic: str = typer.Argument(..., help="Research topic to review."),
    max_papers: int = typer.Option(15, "--max-papers", help="Papers in final review."),
    depth: int = typer.Option(1, "--depth", help="Iterative gap-filling passes."),
    model: str = typer.Option("", "--model", help="Override the model for this run."),
    since: int = typer.Option(0, "--since", help="Only papers from this year onward."),
    comprehensive: bool = typer.Option(
        False, "--comprehensive", help="Longer, more detailed review (costs more)."
    ),
) -> None:
    """Search arXiv, read papers, and write a structured literature review."""
    from .agent import build_graph

    console.print(Panel.fit(f"[bold]Research topic:[/bold] {topic}", border_style="cyan"))

    def progress(msg: str) -> None:
        console.print(f"[dim]路[/dim] {msg}")

    meter: dict = {}
    t0 = time.time()
    try:
        agent = build_graph(
            max_papers=max_papers,
            progress=progress,
            meter=meter,
            model=model or None,
            year_min=since,
            style="comprehensive" if comprehensive else "concise",
        )
        result = agent.invoke(
            {
                "topic": topic,
                "papers": [],
                "review": "",
                "gaps": [],
                "depth": max(1, depth),
                "iteration": 0,
            }
        )
    except Exception as err:
        console.print(f"[bold red]Run failed:[/bold red] {err}")
        raise typer.Exit(code=1)

    papers = result["papers"]
    review_text = result["review"]

    if not review_text.strip():
        console.print("[bold red]No review produced.[/bold red]")
        raise typer.Exit(code=1)

    os.makedirs("reviews", exist_ok=True)
    path = os.path.join("reviews", f"{utils.sanitize_filename(topic)}.md")
    with open(path, "w", encoding="utf-8") as fh:
        fh.write(f"# Literature Review: {topic}\n\n")
        fh.write(review_text.rstrip() + "\n")

    # Validation warnings (non-fatal).
    report = utils.validate_review(review_text, papers[:max_papers])
    if not report["valid"]:
        console.print("[yellow]Validation warnings:[/yellow]")
        for issue in report["issues"]:
            console.print(f"  [yellow]![/yellow] {issue}")

    stats = {"elapsed_s": round(time.time() - t0, 1), **utils.cost_report(meter)}
    console.print(Panel.fit(f"[green]Saved:[/green] {path}", border_style="green"))
    console.print(
        f"[dim]{stats['elapsed_s']}s 路 {len(papers)} papers 路 {stats['llm_calls']} "
        f"LLM calls 路 {stats['in_tok']}+{stats['out_tok']} tok 路 "
        f"~${stats['est_cost_usd']}[/dim]"
    )
    console.print(Markdown(review_text[:500] + ("..." if len(review_text) > 500 else "")))


if __name__ == "__main__":
    app()