"""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()