Metadata-Version: 2.4
Name: easypaper
Version: 0.1.7
Summary: AI-powered academic paper generation SDK
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: langchain>=1.0.5
Requires-Dist: openai>=2.7.2
Requires-Dist: langgraph>=0.0.26
Requires-Dist: jinja2>=3.1.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: pymupdf>=1.26.7
Requires-Dist: pdf2image>=1.17.0
Requires-Dist: python-dotenv>=1.2.1
Requires-Dist: pyyaml>=6.0
Requires-Dist: pydantic>=2.0
Requires-Dist: pillow>=10.0
Provides-Extra: server
Requires-Dist: fastapi>=0.121.1; extra == "server"
Requires-Dist: uvicorn[standard]<0.31,>=0.30.0; extra == "server"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
Requires-Dist: ipython>=9.7.0; extra == "dev"
Provides-Extra: vlm
Requires-Dist: anthropic>=0.18.0; extra == "vlm"
Dynamic: license-file

# EasyPaper

EasyPaper is a multi-agent academic paper generation system. It turns a small set of metadata
(title, idea, method, data, experiments, references) into a structured LaTeX paper and optionally
compiles it into a PDF through a typesetting agent.

## Features

- **Python SDK** — `pip install easypaper`, then `import easypaper` in your own project
- **Streaming generation** — async generator yields real-time progress events at each phase
- Multi-agent pipeline: planning, writing, review, typesetting, and optional VLM review
- Optional FastAPI server mode with health and agent discovery endpoints
- LaTeX output with citation validation, figure/table injection, and review loop

## Requirements

- Python 3.11+
- LaTeX toolchain (`pdflatex` + `bibtex`) for PDF compilation
- [Poppler](https://poppler.freedesktop.org/) — required by `pdf2image` for PDF-to-image conversion
  - macOS: `brew install poppler`
  - Ubuntu/Debian: `apt install poppler-utils`
- Model API keys configured in YAML (see [Config](#config))

## SDK Usage

Install from PyPI:

```bash
pip install easypaper
```

PDF compilation in SDK mode is self-contained when your config includes a `typesetter` model. The SDK now prefers in-process Typesetter execution and falls back to the HTTP endpoint (`AGENTSYS_SELF_URL`) when needed.

### One-shot generation

**Inline metadata:**

```python
import asyncio
from easypaper import EasyPaper, PaperMetaData

async def main():
    ep = EasyPaper(config_path="config.yaml")

    metadata = PaperMetaData(
        title="My Paper Title",
        idea_hypothesis="...",
        method="...",
        data="...",
        experiments="...",
    )

    result = await ep.generate(metadata)
    print(result.status, result.total_word_count)
    for sec in result.sections:
        print(f"  {sec.section_type}: {sec.word_count} words")

asyncio.run(main())
```

**Load metadata from a JSON file (recommended):**  
Prepare a `metadata.json` (see [`examples/meta.json`](examples/meta.json) for the full schema). The clean SDK flow is to parse as `PaperGenerationRequest`, then split into content metadata and runtime options:

```python
import asyncio
from easypaper import EasyPaper, PaperGenerationRequest

async def main():
    ep = EasyPaper(config_path="config.yaml")

    request = PaperGenerationRequest.model_validate_json_file("metadata.json")
    metadata = request.to_metadata()
    options = request.to_generate_options()

    result = await ep.generate(metadata, **options)
    print(result.status, result.total_word_count)

asyncio.run(main())
```

For a minimal metadata-only JSON (no runtime options), you can use:

```python
metadata = PaperMetaData.model_validate_json_file("metadata.json")
result = await ep.generate(metadata)
```

### Finding the final PDF

When review loop is enabled, EasyPaper may compile multiple PDFs (one per iteration). Use this priority to locate the final artifact:

1. Use `result.pdf_path` first (authoritative final path).
2. If missing, search under `result.output_path` in this order:
   - `iteration_*_final/**/*.pdf` (highest priority)
   - Latest `iteration_*` directory PDF
   - Root `paper.pdf` as last fallback

```python
from pathlib import Path
import re

def resolve_final_pdf(result) -> str | None:
    if getattr(result, "pdf_path", None):
        p = Path(result.pdf_path)
        if p.exists():
            return str(p.resolve())

    out = getattr(result, "output_path", None)
    if not out:
        return None
    base = Path(out)
    if not base.exists():
        return None

    finals = sorted(base.glob("iteration_*_final/**/*.pdf"))
    if finals:
        return str(finals[-1].resolve())

    iter_dirs = []
    for d in base.glob("iteration_*"):
        m = re.match(r"iteration_(\\d+)$", d.name)
        if m and d.is_dir():
            iter_dirs.append((int(m.group(1)), d))
    if iter_dirs:
        iter_dirs.sort(key=lambda x: x[0])
        pdfs = sorted(iter_dirs[-1][1].glob("**/*.pdf"))
        if pdfs:
            return str(pdfs[-1].resolve())

    root_pdf = base / "paper.pdf"
    if root_pdf.exists():
        return str(root_pdf.resolve())
    return None
```

### Streaming generation

Use `generate_stream()` to receive real-time progress events via async generator. Metadata can be built inline or loaded from a JSON file (e.g. `PaperMetaData.model_validate_json_file("metadata.json")`).

```python
import asyncio
from easypaper import EasyPaper, PaperMetaData, EventType

async def main():
    ep = EasyPaper(config_path="config.yaml")
    metadata = PaperMetaData.model_validate_json_file("metadata.json")  # or build inline

    async for event in ep.generate_stream(metadata):
        if event.event_type == EventType.PHASE_START:
            print(f"▶ [{event.phase}] {event.message}")
        elif event.event_type == EventType.SECTION_COMPLETE:
            print(f"  ✎ {event.phase} done")
        elif event.event_type == EventType.COMPLETE:
            result = event.data["result"]
            print(f"Done! {result['total_word_count']} words")

asyncio.run(main())
```

`GenerationEvent` fields:

| Field | Type | Description |
|---|---|---|
| `event_type` | `EventType` | `PHASE_START`, `PHASE_COMPLETE`, `SECTION_COMPLETE`, `PROGRESS`, `WARNING`, `ERROR`, `COMPLETE` |
| `phase` | `str` | Logical phase name (e.g. `"planning"`, `"introduction"`, `"body"`) |
| `message` | `str` | Human-readable description |
| `data` | `dict \| None` | Structured payload (section content, final result, etc.) |
| `timestamp` | `datetime` | When the event was created |

A complete working example is available in [`user_case/`](user_case/).

## Server Mode

To run EasyPaper as a FastAPI service (requires the `server` extra):

```bash
pip install "easypaper[server]"
```

1. Copy the example config and fill in your API keys:

```bash
cp configs/example.yaml configs/dev.yaml
```

2. Start the server:

```bash
uvicorn easypaper.main:app --reload --port 8000
```

3. Generate via API:

```bash
curl -X POST http://localhost:8000/metadata/generate \
  -H "Content-Type: application/json" \
  -d @economist_example/metadata.json
```

## Skills

EasyPaper includes a pluggable **Skills** system that injects writing constraints, venue-specific
formatting rules, and reviewer checkers into the generation pipeline. Built-in skills are bundled
as static assets inside the `easypaper` package:

| Category | Skills | Description |
|---|---|---|
| **Writing** | `anti-ai-style`, `academic-polish`, `latex-conventions` | Style constraints applied to all sections — eliminates AI-flavored phrasing, enforces academic tone, ensures LaTeX best practices |
| **Venues** | `neurips`, `icml`, `iclr`, `acl`, `aaai`, `colm`, `nature` | Conference/journal profiles with page limits, formatting rules, and venue-specific style requirements |
| **Reviewing** | `logic-check`, `style-check` | Reviewer checker prompts — detects logical contradictions, terminology inconsistencies, and style violations |

### Enabling skills

Built-in skills are loaded by default. You only need `enabled`/`active_skills` in config:

```yaml
skills:
  enabled: true
  active_skills:
    - "*"                   # "*" = load all skills; or list specific names
```

If you add `skills.skills_dir`, EasyPaper **merges** bundled skills with that directory:
same skill `name` → your file wins. If the path is missing, bundled skills still load and a warning is logged.

### Venue profiles

To apply venue-specific constraints (e.g. page limits, formatting), set `style_guide` in your
`PaperMetaData` to match a venue profile name:

```python
metadata = PaperMetaData(
    title="...",
    idea_hypothesis="...",
    method="...",
    data="...",
    experiments="...",
    style_guide="neurips",   # activates the neurips venue profile
)
```

### Custom skills

Each skill is a single YAML file with the following structure:

```yaml
name: my-custom-skill
description: "What this skill does"
type: writing_constraint   # writing_constraint | reviewer_checker | venue_profile
target_sections: ["*"]     # ["*"] = all sections, or specific ones
priority: 10               # lower = higher priority

system_prompt_append: |
  ## My Custom Rules
  - Rule 1: ...
  - Rule 2: ...

anti_patterns:
  - "word to avoid"
```

Drop the file into your `skills_dir` and it will be automatically loaded on the next run.
See the built-in skills in `easypaper/assets/skills/` for complete examples.

## Config

The application loads configuration from `AGENT_CONFIG_PATH` (defaults to `./configs/dev.yaml`).
You can also set this variable in a `.env` file at the project root.

See `configs/example.yaml` for a fully commented configuration template. Each agent entry defines
its model and optional agent-specific settings.

Key fields per agent:
- `model_name` — LLM model identifier
- `api_key` — API key for the model provider
- `base_url` — API endpoint URL

Additional top-level sections:
- `skills` — skills system toggle and active skill list (see [Skills](#skills))
- `tools` — ReAct tool configuration (citation validation, paper search, etc.)
- `vlm_service` — shared VLM provider for visual review (supports OpenAI-compatible and Claude)

## Repository Layout

- `easypaper/` — SDK core, agent implementations, event system, shared utilities
- `configs/` — YAML configs for agents and models
- `skills/` — backend YAML skills loaded by the Python service
- `plugins/easypaper/` — Claude/OpenCode plugin root (commands + SKILL.md prompts)
- `.claude-plugin/marketplace.json` — marketplace catalog
- `.opencode/opencode.json` — OpenCode/OpenClaw runtime configuration
- `AGENTS.md` — repository-level instructions for coding agents
- `scripts/` — CLI utilities and demos
- `user_case/` — standalone usage example (independent environment)
- `economist_example/` — sample metadata input

## Claude Code Plugin Market

This repository ships a Claude Code marketplace with one installable plugin located at `plugins/easypaper`.

### Installation

Add the marketplace:

```bash
/plugin marketplace add https://github.com/your-username/easypaper
```

Install the plugin from this marketplace:

```bash
/plugin install easypaper@easypaper
```

### Available Plugin

| Plugin | Source | Description |
|--------|--------|-------------|
| easypaper | `./plugins/easypaper` | Generate AI-powered academic papers from metadata interactively |

### Usage

After installation:

```bash
/easypaper
```

Related commands:

```bash
/paper-from-metadata
/paper-section
```

### Plugin Prerequisites

- Python 3.11+
- `easypaper` package installed (`pip install easypaper`)
- LaTeX toolchain (pdflatex + bibtex) for PDF compilation
- API key for LLM provider (configured via config file)

## OpenCode / OpenClaw Usage

This repository includes native OpenCode/OpenClaw configuration in `.opencode/opencode.json`.

### Run directly in this repository

```bash
opencode
```

The runtime loads:

- Plugin path: `./plugins/easypaper`
- Skills: `plugins/easypaper/skills/*/SKILL.md`
- Commands: `easypaper`, `paper-from-metadata`, `paper-section`

For both Claude Code and OpenCode/OpenClaw workflows, start the EasyPaper API service before generation:

```bash
uv run uvicorn easypaper.main:app --reload --port 8000
```
