diff --git a/plugins/asta-preview/skills/literature-report/SKILL.md b/plugins/asta-preview/skills/literature-report/SKILL.md index 141a2d4..c9d9c66 100644 --- a/plugins/asta-preview/skills/literature-report/SKILL.md +++ b/plugins/asta-preview/skills/literature-report/SKILL.md @@ -2,14 +2,15 @@ name: Asta Literature Reports description: Create or update literature reviews/reports. Use whenever you need to research, summarize, or synthesize the literature. allowed-tools: + - Bash(asta literature find *) + - Bash(asta literature report *) - Read(.asta/literature/*) - TaskOutput - - Bash(jq *) --- # Literature Reports -Create or update literature reviews/reports as markdown documents with citations. +Generate comprehensive literature reports by finding relevant papers and synthesizing them into a structured written report with citations. ## Finding Papers @@ -23,6 +24,35 @@ Create or update literature reviews/reports as markdown documents with citations - **When updating an existing report**, read it first to understand the current structure and content. - **When using Find Literature results**, go through ALL highly relevant papers — extract relevance criteria, snippets, and citation contexts from each. +**If the user provides an existing paper-finder results file:** +- Skip to step 3, using that file as `-i` + +**If no existing results file:** +- Proceed to step 2 + +### 2. Find Papers + +```bash +asta literature find 'query' --timeout 300 -o .asta/literature/find/YYYY-MM-DD-topic-slug.json +``` + +Note the output path from the command. + +### 3. Generate the Report + +```bash +asta literature report \ + -i .asta/literature/find/YYYY-MM-DD-topic-slug.json \ + -o .asta/literature/report/YYYY-MM-DD-topic-slug.md +``` + +The command prints progress to stderr and the report path when done. + +### 4. Confirm and Share + +- Read the saved report file and share the key findings with the user +- Tell the user the file path so they can open it + ## Citation Conventions **YAML frontmatter** — required for citation resolution: @@ -46,3 +76,9 @@ Generate a BibTeX entry from the result. Conventions: - **Append** to the `.bib` — never rewrite existing entries. **Verification**: Use the **Preview** skill to render and verify all citations resolve. + +## Error Handling + +- **Authentication error**: Run `asta auth login` and retry +- **Timeout**: Pass a larger `--timeout` value (default is 360s) +- **No papers found**: Suggest refining the query and re-running `asta literature find` diff --git a/skills/literature-report/SKILL.md b/skills/literature-report/SKILL.md index 141a2d4..037636e 100644 --- a/skills/literature-report/SKILL.md +++ b/skills/literature-report/SKILL.md @@ -2,47 +2,58 @@ name: Asta Literature Reports description: Create or update literature reviews/reports. Use whenever you need to research, summarize, or synthesize the literature. allowed-tools: + - Bash(asta literature find *) + - Bash(asta literature report *) + - Bash(asta papers get *) + - Bash(jq *) + - Bash(mkdir *) - Read(.asta/literature/*) + - Read(*) - TaskOutput - - Bash(jq *) --- -# Literature Reports - -Create or update literature reviews/reports as markdown documents with citations. + +Generate comprehensive literature reports by finding relevant papers and synthesizing them into structured written reports with citations. Route to the correct workflow based on what the user already has. + -## Finding Papers + +- **Check before searching.** Always check `.asta/literature/find/` for existing search results before running new searches. +- **Reports are user-facing.** New reports go in user-visible locations (e.g., `docs/`, project root), never inside `.asta/`. +- **Read before updating.** When updating an existing report, read it first to understand structure and content. +- **Exhaust the evidence.** When using find-literature results, go through ALL highly relevant papers — extract relevance criteria, snippets, and citation contexts from each. +- **Delegate search to the right tool.** Use the **Find Literature** skill for comprehensive searches. Use the **Semantic Scholar Lookup** skill for targeted queries (specific papers, citations, author searches). + -- Check `.asta/literature/find/` for existing search results before running new searches. -- Use the **Find Literature** skill for comprehensive searches and for the result schema and jq access patterns. -- Use the **Semantic Scholar Lookup** skill for targeted queries: specific papers, citations, and author searches. + +Determine which workflow to follow based on what the user has: -## Workflow Guidance +**Route A — No papers yet** → The user wants a literature report but hasn't searched yet. +Read `workflows/find-and-report.md`. -- **Identify or create the report file first**. Check if there's an existing report to update. New reports go in user-visible locations (e.g., `docs/`), not inside `.asta/`. -- **When updating an existing report**, read it first to understand the current structure and content. -- **When using Find Literature results**, go through ALL highly relevant papers — extract relevance criteria, snippets, and citation contexts from each. +**Route B — Has a find-literature result** → The user provides or references an existing `.json` results file from `asta literature find`. +Read `workflows/generate-report.md`. -## Citation Conventions - -**YAML frontmatter** — required for citation resolution: -```yaml ---- -bibliography: ../references.bib ---- -``` +**Route C — Has papers in another form** → The user has a collection of papers as a list of titles, DOIs, arXiv IDs, URLs, a BibTeX file, or any non-find-literature format. +Read `workflows/convert-and-report.md`. -**Citations**: Use `[@key]` format. Every key must exist in the `.bib` file — unresolved keys won't render. +**Route D — Merge into existing report** → The user has an existing report and wants to incorporate new papers or findings into it. +Read `workflows/merge-report.md`. -**Adding papers to `.bib`**: As you cite a paper, fetch its metadata and append a BibTeX entry: +When the user's intent maps to multiple routes (e.g., "find papers on X and add them to my existing report"), chain the relevant workflows in sequence. + -```bash -asta papers get CorpusId: --fields title,authors,year,venue,externalIds,journal,publicationDate -``` + +**Search results:** `.asta/literature/find/YYYY-MM-DD-topic-slug.json` +**Generated reports:** `.asta/literature/report/YYYY-MM-DD-topic-slug.md` +**User-facing reports:** User-specified path or `docs/` directory + -Generate a BibTeX entry from the result. Conventions: -- **Key**: `{firstAuthorSurname}{year}` lowercased, ASCII only (e.g., `wei2022`). Append a letter for conflicts (`wei2022a`). -- **`corpus_id`**: Include `corpus_id = {}` for stable identification (survives key renames). -- **Append** to the `.bib` — never rewrite existing entries. + +Read `references/citation-conventions.md` when the report requires formal citations with a `.bib` file and `[@key]` references. + -**Verification**: Use the **Preview** skill to render and verify all citations resolve. + +- **Authentication error** → Run `asta auth login` and retry. +- **Timeout** → Pass a larger `--timeout` value (default is 360s). +- **No papers found** → Suggest refining the query and re-running `asta literature find`. + diff --git a/skills/literature-report/references/citation-conventions.md b/skills/literature-report/references/citation-conventions.md new file mode 100644 index 0000000..66322d3 --- /dev/null +++ b/skills/literature-report/references/citation-conventions.md @@ -0,0 +1,37 @@ +# Citation Conventions + +Follow these conventions when a report requires formal citations with a `.bib` file and `[@key]` Pandoc-style references. + +## YAML Frontmatter + +Every report with citations must include: + +```yaml +--- +bibliography: ../references.bib +--- +``` + +Adjust the relative path to point to the `.bib` file from the report's location. + +## Citation Format + +Use `[@key]` inline. Every key must exist in the `.bib` file — unresolved keys won't render. + +## Adding Papers to the `.bib` File + +As you cite a paper, fetch its metadata and append a BibTeX entry: + +```bash +asta papers get CorpusId: --fields title,authors,year,venue,externalIds,journal,publicationDate +``` + +Generate a BibTeX entry from the result. Conventions: + +- **Key format**: `{firstAuthorSurname}{year}` lowercased, ASCII only (e.g., `wei2022`). Append a letter for conflicts (`wei2022a`, `wei2022b`). +- **`corpus_id` field**: Always include `corpus_id = {}` for stable identification (survives key renames). +- **Append only**: Add new entries to the end of the `.bib` file. Never rewrite or remove existing entries. + +## Verification + +Use the **Preview** skill to render the report and verify all citations resolve. Unresolved `[@key]` references appear as literal text in the output — scan for any that didn't render as proper citations. diff --git a/skills/literature-report/workflows/convert-and-report.md b/skills/literature-report/workflows/convert-and-report.md new file mode 100644 index 0000000..c69d4f7 --- /dev/null +++ b/skills/literature-report/workflows/convert-and-report.md @@ -0,0 +1,81 @@ +# Convert External Papers to Find-Literature Format + +Use when the user has papers in a non-standard form — titles, DOIs, arXiv IDs, URLs, a BibTeX file, CSV, or any other format — and wants a literature report from them. + + + +## Step 1: Identify the papers + +Determine what the user has provided. Common forms: +- A list of paper titles +- DOIs or arXiv IDs +- Semantic Scholar corpus IDs or URLs +- A `.bib` file +- A CSV or spreadsheet +- Inline references in a document + +## Step 2: Resolve each paper via Semantic Scholar + +For each paper, look it up to get full metadata: + +```bash +# By arXiv ID +asta papers get ARXIV:2005.14165 --fields title,abstract,year,authors,venue,citationCount,corpusId + +# By DOI +asta papers get DOI:10.1234/example --fields title,abstract,year,authors,venue,citationCount,corpusId + +# By corpus ID +asta papers get CorpusId:12345 --fields title,abstract,year,authors,venue,citationCount,corpusId + +# By title search (when only title is available) +asta papers search "exact paper title" --limit 5 --fields title,abstract,year,authors,venue,citationCount,corpusId +``` + +For title searches, verify the match is correct by checking authors and year. + +## Step 3: Build the results JSON + +Construct a `LiteratureSearchResult` JSON file matching the find-literature schema: + +```json +{ + "query": "", + "results": [ + { + "corpus_id": 12345, + "title": "Paper Title", + "abstract": "...", + "year": 2023, + "authors": [{"name": "Author Name", "authorId": "123"}], + "venue": "Conference Name", + "citation_count": 50, + "relevance_score": 0.9, + "relevance_judgement": { + "relevance": 9, + "relevanceSummary": "Brief explanation of why this paper is relevant", + "relevanceCriteriaJudgements": [] + }, + "snippets": [], + "citation_contexts": [] + } + ] +} +``` + +Key points: +- Set `relevance_score` between 0 and 1 based on your judgement of relevance to the user's topic. If no clear ranking, use 0.8 for all. +- The `query` field should describe the research topic, not just repeat the user's input. +- `snippets` and `citation_contexts` will be empty since we don't have full-text access — this is fine, the report will rely on abstracts. + +Save to: `.asta/literature/find/YYYY-MM-DD-topic-slug.json` + +## Step 4: Generate the report + +Now follow `workflows/generate-report.md` using the converted results file. + + + + +If the user has a large collection (20+ papers), consider whether `asta literature find` with a well-crafted query would produce better results than manual conversion — it provides relevance scores, snippets, and citation contexts that improve report quality. + diff --git a/skills/literature-report/workflows/find-and-report.md b/skills/literature-report/workflows/find-and-report.md new file mode 100644 index 0000000..2e2bd8a --- /dev/null +++ b/skills/literature-report/workflows/find-and-report.md @@ -0,0 +1,50 @@ +# Find Papers and Generate Report + +Use when the user wants a literature report but has no papers yet. + + + +## Step 1: Clarify the query + +Ask the user to describe their research topic if the request is vague. The search query can be long and complex — a well-scoped query produces better results. + +## Step 2: Check for existing searches + +```bash +asta documents --root .asta/literature/find search --summary '' 2>/dev/null +``` +If relevant results already exist, confirm with the user whether to reuse them or run a fresh search. + +## Step 3: Run the search + +Generate a slug from the topic (e.g., `transformer-efficiency`). + +```bash +asta literature find '' --timeout 300 -o .asta/literature/find/YYYY-MM-DD-topic-slug.json +``` + +Run this in the background — it takes 30–60 seconds. When done, add it to the Asta document index +```bash +asta documents --root .asta/literature/find \ + add .asta/literature/find/YYYY-MM-DD-topic-slug.json + --name="Literature Report: " \ + --summary="" \ + --tags="find-literature,..." \ + --mime-type="application/json" +``` + +## Step 4: Review results + +After the search completes, inspect the top results: + +```bash +jq '[.results | sort_by(-.relevanceScore) | .[0:10][] | {title, year, venue, corpusId, score: .relevanceScore, summary: .relevanceJudgement.relevanceSummary}]' .asta/literature/find/YYYY-MM-DD-topic-slug.json +``` + +Share a brief summary of what was found (paper count, top themes) with the user before generating the report. If the results look thin or off-topic, suggest refining the query. + +## Step 5: Generate the report + +Now follow `workflows/generate-report.md` using the results file from step 3. + + diff --git a/skills/literature-report/workflows/generate-report.md b/skills/literature-report/workflows/generate-report.md new file mode 100644 index 0000000..91ce42a --- /dev/null +++ b/skills/literature-report/workflows/generate-report.md @@ -0,0 +1,39 @@ +# Generate Report from Find-Literature Results + +Use when the user already has a `.json` results file from `asta literature find`. + + + +## Step 1: Identify the input file + +The user provides or references a results file. If it was created by an Asta skill, +it will be in .asta/literature/find. Confirm the file exists. + +## Step 2: Generate the report + +```bash +asta literature report \ + -i .asta/literature/find/YYYY-MM-DD-topic-slug.json \ + -o .asta/literature/report/YYYY-MM-DD-topic-slug.md +``` + +Options: +- `--query "custom framing"` — override the query used as the report title/framing +- `--max-papers 30` — include more papers (default: 20, highest-relevance first) + +The command prints progress to stderr and the report path when done. + +## Step 3: Review and deliver + +Read the saved report file. Share the key findings with the user, including: +- The main themes and conclusions +- Number of papers synthesized +- The file path so they can open it + +If the user wants the report in a specific location (e.g., `docs/literature-review.md`), copy or move it there. + +## Step 4: Citations (if needed) + +If the report needs formal citations with a `.bib` file, follow `references/citation-conventions.md`. + + diff --git a/skills/literature-report/workflows/merge-report.md b/skills/literature-report/workflows/merge-report.md new file mode 100644 index 0000000..6555149 --- /dev/null +++ b/skills/literature-report/workflows/merge-report.md @@ -0,0 +1,64 @@ +# Merge New Findings into an Existing Report + +Use when the user has an existing literature report and wants to incorporate new papers or updated findings. + + + +## Step 1: Read the existing report + +Read the report file to understand: +- Current structure (sections, headings) +- Papers already cited +- The narrative arc and argument +- Citation format (inline `[@key]`, footnotes, or informal) + +## Step 2: Obtain new papers + +Determine the source of new papers. This may require chaining with another workflow: + +- **New search needed** → Follow `workflows/find-and-report.md` steps 1–4 to get a results file, then return here. +- **User provided a results file** → Use it directly. +- **User has papers in another form** → Follow `workflows/convert-and-report.md` steps 1–3 to create a results file, then return here. + +## Step 3: Generate a draft report from new papers + +```bash +asta literature report \ + -i .asta/literature/find/YYYY-MM-DD-new-topic-slug.json \ + -o .asta/literature/report/YYYY-MM-DD-new-topic-slug.md +``` + +Read the generated draft to understand what new content is available. + +## Step 4: Identify what to merge + +Compare the new draft against the existing report: +- **New themes** not covered in the existing report → add as new sections or subsections +- **Supporting evidence** for existing claims → weave into existing sections with new citations +- **Contradictory findings** → add as counterpoints or discussion within relevant sections +- **Duplicate coverage** → skip papers already represented + +## Step 5: Merge into the existing report + +Edit the existing report directly. Preserve the original structure and voice. For each addition: + +1. Place new content in the most relevant existing section, or create a new section if the theme is distinct. +2. Integrate citations naturally into the narrative — don't just append a list. +3. Update any summary or conclusion sections to reflect the expanded evidence base. +4. If using a `.bib` file, add new BibTeX entries following `references/citation-conventions.md`. + +## Step 6: Verify + +- Read the merged report end-to-end to check for coherence. +- Ensure no duplicate citations or contradictory statements. +- Confirm section flow still makes sense with the additions. +- If using formal citations, use the **Preview** skill to verify all `[@key]` references resolve. + + + + +- Existing report structure and voice are preserved +- New findings are integrated contextually, not just appended +- No duplicate papers or contradictory unsupported claims +- Citations are consistent with the existing format + diff --git a/src/asta/cli.py b/src/asta/cli.py index 9884c70..22bb058 100644 --- a/src/asta/cli.py +++ b/src/asta/cli.py @@ -7,6 +7,7 @@ from asta.documents import documents from asta.experiment import experiment from asta.literature.find import find +from asta.literature.report import report from asta.papers.author import author from asta.papers.citations import citations from asta.papers.get import get @@ -46,6 +47,7 @@ def papers(): # Register literature subcommands literature.add_command(find) +literature.add_command(report) # Register papers subcommands papers.add_command(get) diff --git a/src/asta/literature/report.py b/src/asta/literature/report.py new file mode 100644 index 0000000..8e323db --- /dev/null +++ b/src/asta/literature/report.py @@ -0,0 +1,132 @@ +"""Generate literature report command""" + +import json +from pathlib import Path + +import click + +from asta.literature.models import LiteratureSearchResult +from asta.literature.report_client import NoraReportClient + + +def _papers_to_documents(papers: list, max_papers: int) -> list[dict]: + """Convert Paper model instances to the Document dicts expected by the API.""" + sorted_papers = sorted(papers, key=lambda p: p.relevanceScore, reverse=True) + selected = sorted_papers[:max_papers] + + documents = [] + for paper in selected: + doc: dict = { + "corpus_id": str(paper.corpusId), + "title": paper.title, + "abstract": paper.abstract or "", + "authors": [{"name": a.name} for a in paper.authors], + "snippets": [ + {"text": s.text, "section_title": s.sectionTitle} + for s in paper.snippets + ], + "year": paper.year, + "venue": paper.venue, + "relevance_judgement": paper.relevanceScore, + "final_agent_score": paper.relevanceScore, + "citation_count": paper.citationCount, + } + documents.append(doc) + + return documents + + +def _format_report(response: dict, query: str) -> str: + """Render a TaskResult as a markdown document.""" + title = response.get("report_title") or query or "Literature Report" + sections = response.get("sections", []) + + lines: list[str] = [f"# {title}", ""] + for section in sections: + if section.get("title"): + lines.append(f"## {section['title']}") + lines.append("") + if section.get("text"): + lines.append(section["text"]) + lines.append("") + + return "\n".join(lines) + + +@click.command() +@click.option( + "-i", + "--input", + "input_file", + type=click.Path(exists=True, dir_okay=False), + required=True, + help="Paper-finder results JSON file (from 'asta literature find')", +) +@click.option( + "-o", + "--output", + type=click.Path(dir_okay=False), + required=True, + help="Output file path for the generated report (markdown)", +) +@click.option( + "--query", + default=None, + help="Override the research query (defaults to the query stored in the results file)", +) +@click.option( + "--max-papers", + type=int, + default=20, + show_default=True, + help="Maximum number of papers to include (highest-relevance first)", +) +def report( + input_file: str, + output: str, + query: str | None, + max_papers: int, +): + """Generate a literature report from paper-finder results. + + Reads a results file produced by 'asta literature find', sends the papers + to the Nora report generation API, and writes a markdown summary. + + Examples: + + # Generate report from an existing results file + asta literature report -i results.json -o report.md + + # Override the query used as the report title/framing + asta literature report -i results.json -o report.md --query "transformers for NLP" + + # Include more papers + asta literature report -i results.json -o report.md --max-papers 30 + """ + try: + with open(input_file) as f: + raw = json.load(f) + + search_result = LiteratureSearchResult.model_validate(raw) + effective_query = query or search_result.query + + click.echo( + f"Loaded {len(search_result.results)} papers from {input_file}", err=True + ) + + documents = _papers_to_documents(search_result.results, max_papers) + click.echo(f"Sending {len(documents)} papers to report API…", err=True) + + client = NoraReportClient() + response = client.generate_report(documents=documents, query=effective_query) + + markdown = _format_report(response, effective_query) + + output_path = Path(output) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(markdown) + + click.echo(f"Report saved to: {output_path}", err=True) + + except Exception as e: + raise click.ClickException(str(e)) diff --git a/src/asta/literature/report_client.py b/src/asta/literature/report_client.py new file mode 100644 index 0000000..71c8449 --- /dev/null +++ b/src/asta/literature/report_client.py @@ -0,0 +1,81 @@ +"""Nora Report Generation API client""" + +import json +import urllib.error +import urllib.request +from typing import Any + +from asta.utils.auth_helper import get_access_token +from asta.utils.config import get_api_config, get_config_path + + +class NoraReportClient: + """Client for the Nora report generation API.""" + + def __init__(self, base_url: str | None = None, access_token: str | None = None): + if base_url is None: + try: + config = get_api_config("report") + base_url = config.get("base_url") + except (KeyError, FileNotFoundError): + base_url = None + + if not base_url: + raise ValueError( + f"No value for apis.report.base_url in {get_config_path()}. " + "base_url is required. Either provide it as a parameter or configure it in asta.conf" + ) + + if access_token is None: + access_token = get_access_token() + + self.base_url = base_url.rstrip("/") + self.access_token = access_token + self.headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.access_token}", + } + + def _request(self, data: dict) -> dict[str, Any]: + """POST to the generate-report endpoint and return parsed JSON.""" + url = f"{self.base_url}/generate-report" + body = json.dumps(data).encode() + req = urllib.request.Request(url, data=body, headers=self.headers, method="POST") + try: + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read()) + except urllib.error.HTTPError as e: + error_body = e.read().decode("utf-8") + try: + error_msg = json.loads(error_body).get("detail", str(e)) + except json.JSONDecodeError: + error_msg = error_body or str(e) + raise Exception(f"API request failed ({e.code}): {error_msg}") from e + + def generate_report( + self, + documents: list[dict], + query: str = "", + ) -> dict[str, Any]: + """Generate a literature report from pre-retrieved documents. + + Args: + documents: List of document dicts matching the GenerateReportRequest + Document schema (corpus_id, title, abstract, authors, snippets, + and optional metadata fields). + query: The research question / topic for the report. + + Returns: + A TaskResult dict with report_title, sections, report_id, event_id, + and cost. + + Raises: + Exception: On API errors. + """ + payload = { + "query": query, + "documents": documents, + "truncated_result": False, + } + + return self._request(payload) diff --git a/src/asta/utils/asta.conf b/src/asta/utils/asta.conf index 84196be..f917275 100644 --- a/src/asta/utils/asta.conf +++ b/src/asta/utils/asta.conf @@ -39,6 +39,11 @@ apis { model = "allenai/olmocr-2-7b-1025" } + # Nora report generation API + report { + base_url = ${auth.gateway_url}"/api/corpus-qa" + } + # Remote OCR API for PDF text extraction remote-ocr { base_url = ${auth.gateway_url}"/api/remote-ocr" diff --git a/tests/test_cli.py b/tests/test_cli.py index fee0509..2a30911 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -200,6 +200,184 @@ def test_find_with_mode_option(self, runner, tmp_path): ) +class TestReportCommand: + """Test 'asta literature report' command.""" + + RESULTS_JSON = { + "query": "transformers for NLP", + "results": [ + { + "corpus_id": 111, + "title": "Attention Is All You Need", + "abstract": "We propose a new network architecture, the Transformer.", + "year": 2017, + "authors": [{"name": "Vaswani, A.", "authorId": "1"}], + "relevance_score": 0.95, + "snippets": [{"text": "The Transformer follows an encoder-decoder structure."}], + }, + { + "corpus_id": 222, + "title": "BERT: Pre-training of Deep Bidirectional Transformers", + "abstract": "We introduce BERT.", + "year": 2019, + "authors": [{"name": "Devlin, J.", "authorId": "2"}], + "relevance_score": 0.90, + "snippets": [], + }, + ], + } + + API_RESPONSE = { + "report_title": "Transformers for NLP", + "sections": [ + {"title": "Introduction", "text": "Transformers have revolutionized NLP."}, + {"title": "Key Works", "text": "Vaswani et al. introduced the Transformer."}, + ], + "report_id": "report-abc-123", + "cost": 0.01, + } + + def test_report_missing_input(self, runner, tmp_path): + """Test report command fails without --input.""" + output_file = tmp_path / "report.md" + result = runner.invoke(cli, ["literature", "report", "-o", str(output_file)]) + assert result.exit_code != 0 + + def test_report_missing_output(self, runner, tmp_path): + """Test report command fails without --output.""" + input_file = tmp_path / "results.json" + input_file.write_text(json.dumps(self.RESULTS_JSON)) + result = runner.invoke(cli, ["literature", "report", "-i", str(input_file)]) + assert result.exit_code != 0 + + def test_report_success(self, runner, tmp_path): + """Test successful report generation writes markdown file.""" + input_file = tmp_path / "results.json" + input_file.write_text(json.dumps(self.RESULTS_JSON)) + output_file = tmp_path / "report.md" + + with patch("asta.literature.report.NoraReportClient") as MockClient: + mock_instance = MagicMock() + mock_instance.generate_report.return_value = self.API_RESPONSE + MockClient.return_value = mock_instance + + result = runner.invoke( + cli, + ["literature", "report", "-i", str(input_file), "-o", str(output_file)], + ) + + assert result.exit_code == 0, result.output + assert output_file.exists() + content = output_file.read_text() + assert "# Transformers for NLP" in content + assert "## Introduction" in content + assert "Transformers have revolutionized NLP." in content + + def test_report_passes_query_from_results(self, runner, tmp_path): + """Test that the query from the results file is passed to the client.""" + input_file = tmp_path / "results.json" + input_file.write_text(json.dumps(self.RESULTS_JSON)) + output_file = tmp_path / "report.md" + + with patch("asta.literature.report.NoraReportClient") as MockClient: + mock_instance = MagicMock() + mock_instance.generate_report.return_value = self.API_RESPONSE + MockClient.return_value = mock_instance + + runner.invoke( + cli, + ["literature", "report", "-i", str(input_file), "-o", str(output_file)], + ) + + mock_instance.generate_report.assert_called_once() + call_kwargs = mock_instance.generate_report.call_args[1] + assert call_kwargs["query"] == "transformers for NLP" + + def test_report_query_override(self, runner, tmp_path): + """Test --query overrides the query from the results file.""" + input_file = tmp_path / "results.json" + input_file.write_text(json.dumps(self.RESULTS_JSON)) + output_file = tmp_path / "report.md" + + with patch("asta.literature.report.NoraReportClient") as MockClient: + mock_instance = MagicMock() + mock_instance.generate_report.return_value = self.API_RESPONSE + MockClient.return_value = mock_instance + + runner.invoke( + cli, + [ + "literature", + "report", + "-i", + str(input_file), + "-o", + str(output_file), + "--query", + "custom query", + ], + ) + + call_kwargs = mock_instance.generate_report.call_args[1] + assert call_kwargs["query"] == "custom query" + + def test_report_max_papers_option(self, runner, tmp_path): + """Test --max-papers limits the number of documents sent.""" + input_file = tmp_path / "results.json" + input_file.write_text(json.dumps(self.RESULTS_JSON)) + output_file = tmp_path / "report.md" + + with patch("asta.literature.report.NoraReportClient") as MockClient: + mock_instance = MagicMock() + mock_instance.generate_report.return_value = self.API_RESPONSE + MockClient.return_value = mock_instance + + runner.invoke( + cli, + [ + "literature", + "report", + "-i", + str(input_file), + "-o", + str(output_file), + "--max-papers", + "1", + ], + ) + + call_kwargs = mock_instance.generate_report.call_args[1] + assert len(call_kwargs["documents"]) == 1 + # Should be the highest-relevance paper + assert call_kwargs["documents"][0]["corpus_id"] == "111" + + def test_report_api_error(self, runner, tmp_path): + """Test report command exits with code 1 on API error.""" + input_file = tmp_path / "results.json" + input_file.write_text(json.dumps(self.RESULTS_JSON)) + output_file = tmp_path / "report.md" + + with patch("asta.literature.report.NoraReportClient") as MockClient: + mock_instance = MagicMock() + mock_instance.generate_report.side_effect = Exception("API error") + MockClient.return_value = mock_instance + + result = runner.invoke( + cli, + ["literature", "report", "-i", str(input_file), "-o", str(output_file)], + ) + + assert result.exit_code == 1 + + def test_report_config(self): + """Test that the report API is configured in asta.conf.""" + from asta.utils.config import get_api_config + + config = get_api_config("report") + assert "base_url" in config + assert "/api/corpus-qa" in config["base_url"] + + class TestPassthroughUtility: """Test generic passthrough utility functions."""