from __future__ import annotations import json from pathlib import Path import typer from .paper_package import load_paper_package from .pipeline import TwoPassAnnotationPipeline app = typer.Typer(help="Run step 8: derive target contributions, enabling contributions, and groundings.") def _default_output_root() -> Path: return Path("runs/two_pass_outputs") @app.command() def run( paper_dir: Path = typer.Option(..., exists=True, file_okay=False, dir_okay=True), provider: str = typer.Option("openai", help="Provider family: openai or gemini."), model: str = typer.Option("openai/gpt-5", help="Reasoning model used for target-contribution derivation and annotation."), formatter_model: str | None = typer.Option( None, help="Optional model override for pass 2 formatting, e.g. openai/gpt-5-mini or openai/gpt-5.4-pro.", ), judge_model: str | None = typer.Option( None, help="Optional model override for pass 1 candidate ranking. Ignored when --candidate-count=1.", ), candidate_count: int = typer.Option( 1, help="Number of reasoning candidates to generate. If set to 1, no judge call is made.", ), formatter_max_attempts: int = typer.Option( 3, help="Formatter-only retry attempts after pass 1 has succeeded.", ), include_reference_examples: bool = typer.Option( True, "--include-reference-examples/--no-include-reference-examples", help="Include the built-in reference examples in the pass-1 reasoning prompt.", ), prompt_profile: str = typer.Option( "full", help="Reasoning prompt profile: full or generic.", ), output_root: Path = typer.Option( _default_output_root(), help="Directory to store run outputs.", ), run_label: str | None = typer.Option(None, help="Optional label to include in the saved run directory name."), annotator_id: str = typer.Option("llm", help="Annotator id to embed in the final UI payload."), extracted_claim: str | None = typer.Option(None, help="Optional override for the extracted target contribution."), ) -> None: paper = load_paper_package(paper_dir, extracted_claim_override=extracted_claim) pipeline = TwoPassAnnotationPipeline( provider=provider, model=model, formatter_model=formatter_model, judge_model=judge_model, output_root=output_root, run_label=run_label, annotator_id=annotator_id, candidate_count=candidate_count, formatter_max_attempts=formatter_max_attempts, include_reference_examples=include_reference_examples, prompt_profile=prompt_profile, progress_callback=typer.echo, ) result = pipeline.run(paper) typer.echo(str(result.run_dir / "run_output.json")) @app.command() def summarize(run_output: Path = typer.Option(..., exists=True, dir_okay=False, file_okay=True)) -> None: data = json.loads(run_output.read_text()) payload = data.get("ui_payload") or {} claims = payload.get("claims") or [] summary = { "paper_id": data.get("paper_id"), "target_contribution_count": len(claims), "target_contributions": [ { "claim_id": claim.get("claim_id"), "rewritten_claim": claim.get("rewritten_claim"), "decision": claim.get("decision"), "enabling_contribution_count": len(claim.get("ingredients") or []), } for claim in claims ], } typer.echo(json.dumps(summary, indent=2)) if __name__ == "__main__": app()