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."""