LLM 기반 국책과제계획서·제안서·보고서(HWP/HWPX) CLI 자동 작성 파이프라인
Project description
ReportGen
A CLI pipeline for AI-assisted drafting of Korean national R&D project plans, proposals, and technical reports (HWP/HWPX) — from form analysis to a finished document, in partnership with an external LLM agent.
Overview
ReportGen is a command-line tool, not a hosted service or a model. You give it an HWPX-based form — a national R&D project plan, a proposal, a technical report, any similarly structured Korean institutional document — plus reference material (PDF/TXT/HWPX), and it does two things:
- Analyzes the form's structure and emits a mapping-request artifact that any external LLM agent can read.
- Applies the agent-authored mapping back onto the form to produce a finished HWPX file.
The pipeline itself never calls an LLM API. Content generation is delegated entirely to whichever external agent drives the CLI — Claude Code, OpenCode, Codex, or any other coding agent/harness. This means no API-key management and no lock-in to a single model provider inside the tool, and it's why the same pipeline works unmodified whether you're filling out a dementia-research R&D plan, a public-works proposal, or a mid-term technical report. ReportGen ships through three channels — PyPI, standalone binaries, and a Claude Code plugin — so it installs cleanly in any environment.
Features
- 🔍 Automatic HWPX structure analysis — TOC detection, 5-way table classification (DATA/INSTR/SKIP/SCHED/IDENT), and paragraph placeholder detection in one pass.
- 🤝 Mapping-request artifact generation — emits
request.json+prompt.mdfor any external LLM agent to read. Zero internal LLM calls. - ✍️ FieldMapping apply engine — takes the agent-authored JSON mapping and writes it into the HWPX XML via three precise actions:
replace/fill_cell/insert_after. - 🎯 Adaptive fill — no per-form hardcoding — table cells can be targeted by structural intent instead of raw coordinates: a
labelfield resolves to whatever cell sits next to that label text, and aschedulefield expands a Gantt-chart task into the right monthly cells — both computed fresh from each document's own structure, so the same code works on any form of that shape. - 🔤 Font consistency & line-wrap safety — automatically applies a 12pt non-bold Malgun Gothic
charPr, and strips stale<hp:linesegarray>caches so Hangul Word doesn't render overlapping text after reflow. - 📎 Reference-material ingestion — PDF/TXT files are parsed automatically and folded into the mapping-request payload.
- 🛡️ Structural verification gate — checks for table loss, coordinate anomalies, and paragraph fill rate right after the pipeline runs, and measures accurate fill rate excluding SKIP/SCHED tables.
- 🔁 HWP → HWPX conversion — converts binary HWP to ZIP-based HWPX via a Java fat JAR.
- 📦 Three distribution channels — PyPI (update-notification), standalone binary (true self-update), and the Claude Code plugin marketplace. See Installation for details on each.
Quick Start
pip install reportgen-pipeline
# Phase 1 — analyze the form & emit a mapping request (no LLM call)
reportgen \
--template input/project-plan-form.hwpx \
--reference input/project-summary.txt input/rfp.pdf \
--emit-mapping-request request.json
# (an external LLM agent reads request.json and writes mappings.json)
# Phase 2 — apply the mapping & produce the finished HWPX
reportgen \
--template input/project-plan-form.hwpx \
--mappings mappings.json \
--output output/result.hwpx \
--measure
How It Works
- Form analysis (Phase 1) —
StructureAnalyzerparses the HWPX form's table/paragraph structure, auto-classifies every table into one of 5 types (DATA/INSTR/SKIP/SCHED/IDENT), detects fill targets, and emitsrequest.json(full detail) plus a compact digest fromdigest.py(a few dozen KB, via--emit-digest) and an optionalprompt.md. This phase never calls an LLM API. - An external LLM agent fills it in — Claude Code, OpenCode, or any other agent designs the fill plan from the compact digest (not the full
request.json, especially for large forms), targets only the specific elements it needs fromrequest.json, and writesmappings.jsonfollowing theFieldMapping[]contract. Swapping models or providers never requires touching the pipeline code. - Mapping apply (Phase 2) —
SmartFillEnginewritesmappings.jsoninto the HWPX XML:replace(paragraphs, with stale linesegarray cleared),fill_cell(table cells, coordinates verified against the real document — or resolved dynamically from alabel/scheduleintent),insert_after(new paragraphs, 12pt Malgun Gothic applied). - Verify + measure —
verify_outputgates on table loss and paragraph fill rate, andmeasure_fill_ratecomputes an accurate fill rate against the original template, excluding SKIP/SCHED tables. The finishedoutput.hwpxopens directly in Hangul Word Processor.
Core design principles
| Principle | What it means |
|---|---|
| Form-invariant | Instruction tables ("delete before submission"), diagram-like tables, and identity tables are preserved as-is; only genuine data tables get filled |
| Line-wrap safety | Stale <hp:linesegarray> caches are stripped so Hangul Word reflows cleanly, with no overlapping text |
| Dynamic classification, never hardcoded | Gantt charts, org charts, and empty grids are identified by structural pattern, not by memorizing any one form's layout |
| Content stays external | Content generation is delegated to an external agent — model and provider stay fully swappable |
| Accurate measurement | Fill rate is computed against the original template, excluding SKIP/SCHED tables |
| Digest-first analysis | The design phase reads a compact digest instead of the full request.json — measured example: a 29-table form went from a 1.5MB request.json to a 45KB digest |
LLM Allocation — how model tiers are assigned
ReportGen's own code contains no LLM calls anywhere. It is a deterministic CLI: Phase 1 and Phase 2 are pure structural analysis and XML editing, and --measure is a pure scoring script. Every piece of authored content comes from whatever external agent invokes the CLI — which means you (or your coding agent) decide which model tier does which job. The tool is designed around a specific 3-stage division of labor so that model spend tracks task difficulty instead of being uniform:
| Stage | Tier required | Input it should read | Why |
|---|---|---|---|
| ① Form analysis & fill design | High-tier (e.g. Opus-class, GPT-5.6-class "high"/"solve" reasoning, or an equivalent top-tier reasoning mode) | The compact digest (--emit-digest), not the full request.json |
This is the step that decides what goes in each field, how much content each section needs, and where a large table should be preserved vs. filled — the single highest-leverage decision in the whole run, so it deserves the strongest available model. Reading the full request.json here (which can be hundreds of KB to >1MB on large forms) burns tokens without improving the decision, since the digest already carries everything structural: element indices, previews, table classifications, and fill coordinates. |
| ② Field content generation | Mid-tier (e.g. Sonnet-class, GPT-5.6 standard, or a comparable general-purpose coding-agent model) | The Phase ① design plan, plus targeted lookups into request.json by element_index only when exact source text or merge structure is needed |
Once the design is set, writing the actual sentence/number for each target is comparatively mechanical — a mid-tier model handles it reliably and far more cheaply. |
| ③ Verification & fill-rate measurement | None — deterministic code | --measure output |
This is fully computed by measure_fill_rate.py; no model judgment is involved. Only re-invoke a mid-tier model here if the measured fill rate looks abnormally low or the output content looks suspicious — a high-tier model is never needed for this step. |
A few corollaries that follow directly from this design:
- Never use a lightweight/mini-tier model at any stage. The whole pipeline is calibrated around "one strong analysis pass, one competent fill pass" — a cheap model at either stage tends to produce sparse content or mis-scoped table decisions, which is exactly the failure mode the digest/design split is meant to prevent.
- Simple forms don't need the two-stage split. A form with a handful of tables and under ~20 paragraph targets is fine end-to-end in a single mid-tier session — reserve the high-tier design pass for large forms (10+ tables, multi-year tables, complex row/merge structures), where it earns its cost.
- Cache your research. If you're filling out many proposals in the same domain, save web-research findings (e.g.
input/research_facts.md) and reuse them — re-searching the same domain facts on every run is wasted spend, independent of which model tier you're using. - Prefer intent over coordinates when authoring
mappings.json. Uselabel/schedulefields (see FieldMapping schema) instead of hand-countingtable_row/table_col— this is what makes the same mapping-authoring approach transfer across different forms without new per-form logic.
CLI
--template <HWPX form path> (required)
--output <output HWPX path> (required for Phase 2)
--reference <file1> <file2> ... PDF/TXT/HWPX reference files
--data <user_data.json> additional structured data (optional)
--instructions "extra instructions" additional instructions for the agent (optional)
--emit-mapping-request <request.json> Phase 1: emit the mapping request
--emit-digest <digest.json> Phase 1: emit the compact structural digest (recommended primary input for the design stage)
--prompt-output <prompt.md> Phase 1: emit a Markdown prompt (optional)
--mappings <mappings.json> Phase 2: apply the mapping
--analysis-output <analysis.json> save the structure analysis result (optional)
--measure print the fill rate on completion
--version show version
--upgrade standalone-binary channel: self-upgrade immediately
--no-update-check skip the startup update check
--verbose
After converting HWP → HWPX
java -jar java/hwp2hwpx-fat.jar input/form.hwp input/form.hwpx
reportgen --template input/form.hwpx --emit-mapping-request request.json
Measuring fill rate separately
python scripts/measure_fill_rate.py --file output/result.hwpx --template input/form.hwpx
=======================================================
Fill Rate Report: result.hwpx [template]
=======================================================
TABLE CELLS (DATA tables only, SKIP/SCHED excluded):
Total data cells : 86
Filled cells : 86
Fill rate : 100.0% [##################################################]
PARAGRAPH TARGETS (from template):
Total targets : 66
Filled : 60
Fill rate : 90.9% [#############################################-----]
=======================================================
Installation
| Channel | Command | Notes |
|---|---|---|
| PyPI (recommended) | pip install reportgen-pipeline |
Public distribution; checks for a new version once a day at runtime and prints a pip install -U notice |
| Standalone binary | Download reportgen-<os>-<arch> from Releases |
For internal/collaborator use (private repo, requires auth); true self-update in the OpenCode style |
| Source (for development) | git clone then pip install -e . |
Includes the reportgen CLI, editable install |
Prerequisites
- Python 3.10+
- Java 11+ / Maven (only needed for HWP → HWPX conversion)
PyPI channel details
pip install reportgen-pipeline
pip install -U reportgen-pipeline # upgrade
reportgen --version
pip deliberately never reinstalls itself automatically (this is correct, safe behavior). The CLI only prints a "a new version is available — run pip install -U" notice, the same way gh or npm do (disable with REPORTGEN_NO_UPDATE_CHECK=1). This repository is private, but PyPI is a separate public registry, so this channel is reachable by anyone.
Standalone binary channel details
gh release download v0.3.0 --repo kaismin82/ReportGen-pipeline \
--pattern "reportgen-<os>-<arch>*"
./reportgen-windows-x86_64.exe --template ... --emit-mapping-request request.json
⚠️ Because this repository is private, this channel only works for people with repo access (anonymous requests get a 404 from the GitHub API). Authentication comes from
gh auth tokenor theGITHUB_TOKEN/GH_TOKENenvironment variable. Use the PyPI channel if you need public distribution.
Unlike the pip package, a standalone binary owns nothing but itself (no shared venv or pinned-version environment to break), so on every run, if it detects a newer version it replaces itself and immediately re-executes the same command — the same mechanism OpenCode uses for opencode upgrade. Force it immediately with reportgen --upgrade, or disable it with REPORTGEN_NO_AUTOUPDATE=1.
Source install & conda caveat
git clone https://github.com/kaismin82/ReportGen-pipeline.git
cd ReportGen-pipeline
pip install -r requirements.txt # dependencies only
pip install -e . # or an editable install (includes the reportgen CLI)
bash setup.sh --java # (optional) build the HWP→HWPX converter
conda environments may already have a different package also named hwpx installed (pip install hwpx ❌ ≠ pip install python-hwpx ✅):
pip install python-hwpx --force-reinstall
Architecture
| Module | Role |
|---|---|
universal_pipeline.py |
📌 Main CLI entry point (the reportgen command) |
document_model.py |
Data classes (DocumentElement, TableInfo, FieldMapping) |
structure_analyzer.py |
HWPX structure analysis · table/paragraph classification |
dynamic_template_analyzer.py |
Analysis orchestrator (no LLM calls) |
smart_fill_engine.py |
HWPX XML edit engine (replace/fill_cell/insert_after) |
mapping_resolve.py |
Resolves label/schedule intent mappings into concrete fill_cell mappings |
schedule_resolve.py |
Expands a Gantt-chart task intent into the right monthly cells, from the table's own structure |
label_resolve.py |
Resolves a label intent to its value cell by text match + spatial rule |
identity_fill.py |
Auto-fills IDENT (identity/institution) tables from user_data, with zero fabrication |
table_resolve.py |
Nested-table resolver (passes through layout frames) |
toc_detection.py |
TOC detection · table-classification helpers · blank-detection regex |
reference_reader.py |
PDF/TXT reference-file parser |
schedule_handler.py |
Gantt-chart metadata extraction |
verify_output.py |
Output structural verification gate |
measure_fill_rate.py |
Fill-rate measurement (accurate, computed against the template) |
digest.py |
Compact structural digest generation (computed from elements alone; primary input for the design stage) |
hwpx_compat.py / hwpx_utils.py |
HWPX-package compatibility layer · shared utilities |
update_check.py |
PyPI latest-version check/notice (pip channel) |
self_update.py |
Standalone-binary self-update (private repo, requires auth) |
bump_version.py |
Bulk version bump · release · CI consistency check |
ReportGen-pipeline/
├── scripts/ # all modules listed above + archive/ (legacy scripts)
├── tests/ # pytest suite (103+ passing)
├── java/ # HWP→HWPX converter (Convert.java, hwp2hwpx-fat.jar)
├── .claude-plugin/ # Claude Code marketplace catalog
├── reportgen/ # Claude Code plugin (distribution copy: plugin.json + SKILL.md)
├── .claude/skills/ # project-scoped skill (plugin mirror)
├── .github/workflows/ # ci.yml · publish.yml (PyPI) · build-binaries.yml
├── docs/ # design proposals
├── pyproject.toml # package metadata · build config
└── requirements.txt
Table classification scheme
| Class | Criteria | Handling |
|---|---|---|
DATA |
Ordinary data table | Filled by the agent (fill_cell) |
INSTR |
Contains authoring instructions ("delete before submission") | Preserved (never touched) |
SKIP |
Org chart, WBS, empty grid | Preserved as-is |
SCHED |
Gantt chart (task rows + monthly columns) | Filled via a schedule intent mapping |
IDENT |
PI/institution identity table | Auto-filled from user_data if present, otherwise preserved |
Claude Code Skill
ReportGen ships as a CLI-driven Claude Code skill — invoking /reportgen doesn't run a hosted service; it drives the same reportgen CLI described above from your local shell, with an external agent (Claude Code itself, in this case) doing the analysis and fill-content authoring per the LLM Allocation model. You can use it two ways:
Option 1 — Plugin marketplace (recommended, auto-upgrading)
/plugin marketplace add kaismin82/ReportGen-pipeline
/plugin install reportgen@reportgen-marketplace
New releases are detected and downloaded automatically in the background, and activate automatically the next session (restart) with no command needed (run /reload-plugins once to pick it up immediately in the current session). ⚠️ Since this repository is private, this channel is also collaborator-only.
Option 2 — Directly from the repository
Cloning the repository makes .claude/skills/reportgen/SKILL.md recognized immediately as a project-scoped skill. Update with git pull.
Use the /reportgen skill to draft a project plan matching this form.
- template: input/project-plan-form.hwpx
- reference files: input/project-summary.txt input/rfp.pdf
- output file: output/result.hwpx
The skill runs Phase 1 automatically → drafts mappings.json from reference material and web search → runs Phase 2 → confirms fill rate with --measure.
Versioning & Release Automation
Version strings (pyproject.toml, universal_pipeline.py, plugin.json) are managed as a single source of truth by scripts/bump_version.py:
python scripts/bump_version.py 0.3.0 # bump the version everywhere (incl. SKILL.md mirror sync)
python scripts/bump_version.py 0.3.0 --release # + commit · tag (v & reportgen--v) · push · GitHub release
python scripts/bump_version.py --check # CI consistency check (run automatically by ci.yml)
Once a GitHub Release is published, two workflows follow automatically:
publish.yml→ publishes sdist/wheel to PyPI via Trusted Publishing (OIDC) — no token needed (requires a one-time PyPI pending-publisher + GitHubpypienvironment setup)build-binaries.yml→ builds, verifies, and attaches standalone binaries for the Linux/macOS/Windows matrix as release assets
FAQ
Q: I get ImportError: cannot import name 'HwpxPackage' from 'hwpx'.
A conflicting, unrelated package also named hwpx is installed in your conda environment. Run pip install python-hwpx --force-reinstall.
Q: After Phase 1, which files do I hand to the external agent?
Hand it request.json (from --emit-mapping-request), the compact digest (from --emit-digest), and prompt.md (from --prompt-output) if you generated one. The agent should design the fill plan from the digest, target only the specific elements it needs from request.json, and write a FieldMapping[] JSON array to mappings.json.
Q: The analysis stage burns too many tokens on a large, table-heavy form.
Generate the compact digest with --emit-digest and feed that to the design-stage model instead of request.json. The digest doesn't repeat each element's full text — it carries only indices, previews, table classifications, and fill coordinates, compressing a 29-table form's request.json from 1.5MB down to 45KB in one measured case. Only look up request.json by element_index when you need a specific element's exact source text.
Q: My paragraph fill rate comes out low.
Always pass --template alongside --measure. Fill quality itself depends on the external agent's model and prompt quality — see LLM Allocation.
Q: An instruction table got filled in.
structure_analyzer.py's _is_instruction_table() treats a "delete before submission" marker as a strong signal. If your form doesn't carry that marker, add another instruction signal to _DELETE_RE.
Q: The Gantt chart / schedule table gets filled oddly.
Prefer a schedule intent mapping over hand-picked coordinates — it lets schedule_resolve.py detect the month-header row and task blocks from the table's own structure. Structural metadata is also exposed via schedule_handler.detect_schedule_meta() in request.json if you need to inspect it directly.
Q: Why do the pip channel and the standalone-binary channel self-update differently? A pip package lives inside a shared venv / pinned-version environment, so silently bumping its own version could break a user's lockfile — hence "notify only." A standalone binary owns nothing but itself, so a true self-replace is safe.
Development
pip install -e ".[test]"
pytest tests/ -v # full suite
pytest tests/test_output_qa.py tests/test_smart_fill_engine_p0.py -v # core invariants only
python scripts/bump_version.py --check # version consistency check
| Test file | Verifies |
|---|---|
test_output_qa.py |
Absence of stale linesegarray · cell placement · resolver invariants |
test_smart_fill_engine_p0.py |
replace/fill_cell/linesegarray stripping |
test_structure_analyzer_p1.py |
TOC detection · header extraction · instruction tables |
test_mapping_artifact_handoff.py |
Phase 1/2 CLI round-trip |
test_self_update.py |
Platform → asset-name mapping, version comparison, asset-selection logic |
test_digest.py |
Table-classification precedence, fill-target extraction, large-table coordinate-omission threshold |
test_e2e_pipeline.py |
Full flow from structure analysis through SmartFillEngine |
test_korean_edge_cases.py |
Full-width brackets, ZWSP, full-width-space blank detection |
test_schedule_table.py |
Gantt-table detection · metadata extraction |
test_mapping_resolve.py |
label/schedule intent dispatch and fail-soft skipping |
test_schedule_resolve.py |
Dynamic month/task-block detection across varied Gantt shapes |
test_label_resolve.py |
Label text matching + spatial value-cell resolution |
test_identity_fill.py |
Zero-fabrication identity auto-fill, duplicate-label safety |
test_quality_gates.py |
Section-tier classification and content-length quality gates |
License & Credits
MIT — see LICENSE. Free to use, modify, and distribute.
| Project | Role | License |
|---|---|---|
| python-hwpx | HWPX file parsing/packaging (HwpxPackage) |
MIT |
| hwp2hwpx by neolord0 | HWP → HWPX binary conversion (Java) | Apache 2.0 |
| @ohah/hwpjs by ohah | HWP → JSON/Markdown/HTML (Node.js) | MIT |
| lxml | HWPX XML parsing and manipulation | BSD |
| python-dotenv | .env environment-variable loading |
BSD |
| pypdf | PDF text extraction | BSD |
Reference & inspiration: hwp-pipeline (Yoojin-nam) — the Claude Code skill whose early design informed this project · the HWP/HWPX format specification (Hancom HWPML 2011/2016) · Claude Code by Anthropic
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file reportgen_pipeline-0.3.0.tar.gz.
File metadata
- Download URL: reportgen_pipeline-0.3.0.tar.gz
- Upload date:
- Size: 143.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
94f0b7512d042223130a7185b1581ce8f41b2410de2910f2fc97b3ff7362c173
|
|
| MD5 |
88af48dbc3cba60a14a6d0247684dfd9
|
|
| BLAKE2b-256 |
9181205f972798291f0f003fe2949f0a825784dea21b10729a06599c8d709dd6
|
Provenance
The following attestation bundles were made for reportgen_pipeline-0.3.0.tar.gz:
Publisher:
publish.yml on kaismin82/ReportGen-pipeline
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
reportgen_pipeline-0.3.0.tar.gz -
Subject digest:
94f0b7512d042223130a7185b1581ce8f41b2410de2910f2fc97b3ff7362c173 - Sigstore transparency entry: 2190907910
- Sigstore integration time:
-
Permalink:
kaismin82/ReportGen-pipeline@86ce32e7309f94c06c4eeeebcd0db462d489358d -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/kaismin82
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@86ce32e7309f94c06c4eeeebcd0db462d489358d -
Trigger Event:
release
-
Statement type:
File details
Details for the file reportgen_pipeline-0.3.0-py3-none-any.whl.
File metadata
- Download URL: reportgen_pipeline-0.3.0-py3-none-any.whl
- Upload date:
- Size: 96.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6ba66dc78ccfc92f28a4f1f0bb6acbdda7d02a3fc8cb0cd02586ed67ce14eaa1
|
|
| MD5 |
e25d3b4b9c4a766b4f62b45f1bac3c03
|
|
| BLAKE2b-256 |
38115c631c1a05efda0541c5a422aa63511eb8f37f1dbdc094393f1d845b4e41
|
Provenance
The following attestation bundles were made for reportgen_pipeline-0.3.0-py3-none-any.whl:
Publisher:
publish.yml on kaismin82/ReportGen-pipeline
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
reportgen_pipeline-0.3.0-py3-none-any.whl -
Subject digest:
6ba66dc78ccfc92f28a4f1f0bb6acbdda7d02a3fc8cb0cd02586ed67ce14eaa1 - Sigstore transparency entry: 2190907969
- Sigstore integration time:
-
Permalink:
kaismin82/ReportGen-pipeline@86ce32e7309f94c06c4eeeebcd0db462d489358d -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/kaismin82
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@86ce32e7309f94c06c4eeeebcd0db462d489358d -
Trigger Event:
release
-
Statement type: