Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
/.github/ @NVIDIA-NeMo/data_designer_reviewers

# Plugins
/plugins/data-designer-docx/ @NVIDIA-NeMo/data_designer_reviewers @mvansegbroeck
/plugins/data-designer-github/ @NVIDIA-NeMo/data_designer_reviewers @eric-tramel
/plugins/data-designer-retrieval-sdg/ @NVIDIA-NeMo/data_designer_reviewers @shan-nvidia @oliverholworthy
/plugins/data-designer-template/ @NVIDIA-NeMo/data_designer_reviewers
92 changes: 92 additions & 0 deletions docs/plugins/data-designer-docx/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# data-designer-docx

Renders Data Designer rows as Microsoft Word documents. Registers a `docx`
processor that writes one `.docx` per row and records each file's path back into
the dataset, so rows and documents stay joined.

## Installation

```bash
uv add data-designer data-designer-docx
```

## How it works

The plugin ships a Pydantic model, `WordDocument`, that is used twice: as the
`output_format` of an LLM structured column, and as the input contract of the
renderer. Because both ends share one definition, a model that emits something
unrenderable produces a validation error on the column — which Data Designer
already knows how to retry — instead of a parsing failure downstream.

That is also why the package contains no markdown parsing. The LLM generates the
document's *structure*, and the renderer walks it.

```text
llm-structured(document: WordDocument) -> processor(docx) -> documents/<name>/*.docx
```

## Configuration

| Field | Required | Description |
| --- | --- | --- |
| `name` | Yes | Processor name; also the output subfolder. |
| `document_column` | Yes | Column holding a `WordDocument`-shaped value. |
| `output_subdir` | No | Folder under the dataset directory. Defaults to `documents`. Must be relative and must not name a Data Designer-managed directory. |
| `filename_template` | No | Jinja2 template for the file name. Defaults to `document.docx`, which references no dataset columns; duplicates get a numeric suffix. |
| `output_path_column` | No | Column receiving the written path. Defaults to `docx_path`. |
| `metadata_columns` | No | Label to Jinja2 template pairs, rendered as a front-matter table. |
| `core_property_columns` | No | Word core property (`author`, `category`, `subject`, `keywords`) to Jinja2 template pairs. |
| `template_path` | No | A `.docx` supplying corporate styles, header, and footer. |
| `footer_template` | No | Jinja2 template for the page footer, applied to every section. |
| `table_style` | No | Table style name; must exist in the template. Defaults to `Table Grid`. |
| `number_sections` | No | Prefix section headings with `1.`, `2.`, and so on. Defaults to `True`. |

## Implementation notes

**Stage choice.** The processor implements `process_after_batch` rather than
`process_after_generation`. Documents then stream out while the run is still in
progress, the row count stays fixed as the async engine requires at that stage,
and the dataset stays resumable — `process_after_generation` rewrites the final
parquet and marks the dataset terminal for resume.

**Output location.** Documents are written to `<output_subdir>/<name>/`, never
under `processors-files/`. Both components are validated as contained, relative,
non-reserved path segments, and the resolved directory is asserted to sit beneath
the dataset directory before anything is written. Data Designer reads every directory there back as a
parquet dataset, so `.docx` files placed there make `preview()` fail with
*"Parquet magic bytes not found in footer"*. Binary artifacts get their own
folder, the same way generated images live under `images/`. The config validator
rejects the reserved names.

**Ragged tables.** Structured outputs constrain the shape of the JSON, not the
arithmetic inside it — a model asked for a three-column table will occasionally
return a row with two cells. Rows are padded or truncated to the header width
rather than triggering a retry, which would cost a whole document generation.

**Package layout.** `schema.py` holds the contract, `render.py` is pure
python-docx with no Data Designer imports (so layout can be iterated without
spending tokens), `config.py` is user-facing, and `impl.py` is engine-side.

## Templates

`template_path` should point at a `.docx` containing styles, header, and footer
but **no body content** — python-docx appends generated content after anything
already in the file, so a template with a cover page yields a cover page on every
document.

**Structured values are not re-decoded.** Data Designer's recursive JSON decoding
rewrites string leaves that look like scalars, turning `"30"` into `30` and
`"true"` into `True` — precisely the values a key-data table carries, and values
the schema then rejects. The processor therefore validates the document column
from the raw record and only JSON-decodes it when the top-level value is a
string. The recursively decoded copy is used for Jinja templates only.

**Resume safety.** File name collisions are tracked with case-folded keys, since
macOS and Windows treat `A.docx` and `a.docx` as the same file, and the set is
seeded from documents already on disk. A resumed run therefore cannot overwrite a
document written by a batch that completed before the resume.

**Footers.** Generated content is appended after any body content the template
already has, so it lands in the template's final section. `footer_template` is
applied to every section rather than just the first, which would otherwise leave
the generated pages showing the template's own footer.
97 changes: 97 additions & 0 deletions docs/plugins/data-designer-docx/usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Usage

A complete pipeline: samplers describe the corpus, one structured column writes
each document, and the processor renders them.

```python
import data_designer.config as dd
from data_designer.interface import DataDesigner

from data_designer_docx.config import DocxProcessorConfig
from data_designer_docx.schema import WordDocument

MODEL_ALIAS = "doc-writer"

config_builder = dd.DataDesignerConfigBuilder(
model_configs=[
dd.ModelConfig(
alias=MODEL_ALIAS,
model="nvidia/nemotron-3-super-120b-a12b",
provider="nvidia",
# A whole document in one call is a long structured generation.
inference_parameters=dd.ChatCompletionInferenceParams(max_tokens=8192),
)
]
)

config_builder.add_column(
dd.SamplerColumnConfig(
name="doc_id",
sampler_type=dd.SamplerType.UUID,
params=dd.UUIDSamplerParams(prefix="POL-", short_form=True, uppercase=True),
)
)
config_builder.add_column(
dd.SamplerColumnConfig(
name="doc_type",
sampler_type=dd.SamplerType.CATEGORY,
params=dd.CategorySamplerParams(values=["Remote Work Policy", "Incident Response Runbook"]),
)
)

config_builder.add_column(
dd.LLMStructuredColumnConfig(
name="document",
model_alias=MODEL_ALIAS,
output_format=WordDocument,
prompt=(
"Write an internal {{ doc_type }} (document ID {{ doc_id }}). "
"Write like a real corporate policy: flat, procedural, no marketing language."
),
)
)

config_builder.add_processor(
DocxProcessorConfig(
name="word-documents",
document_column="document",
filename_template="{{ doc_id }}-{{ doc_type }}.docx",
metadata_columns={"Document ID": "{{ doc_id }}"},
footer_template="{{ doc_id }}",
)
)

results = DataDesigner().create(config_builder, num_records=10, dataset_name="policies")
dataset = results.load_dataset()
```

## Reading the output

`docx_path` is relative to the dataset directory, which keeps the dataset
portable — move the folder and the paths still resolve.

```python
from docx import Document

path = results.artifact_storage.base_dataset_path / dataset["docx_path"].iloc[0]
rendered = Document(str(path))

print(rendered.core_properties.author)
print(rendered.sections[0].footer.paragraphs[0].text)
```

## Customizing the document shape

`WordDocument` describes a title, subtitle, summary, sections, and one key-data
table. To change what gets generated, subclass or replace it and pass your model
as the column's `output_format`; the renderer only requires the fields it reads.

The `Field(description=...)` strings on that model are not documentation. Data
Designer serializes the JSON Schema into the prompt inside `<response_schema>`
tags, so those descriptions are prompt text the model reads — the fastest lever
for changing output quality.

## Rows without a valid document

A row whose document column fails validation is skipped: its `docx_path` is
null, a warning is logged, and the rest of the batch still renders.
11 changes: 11 additions & 0 deletions docs/plugins/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@
Browse available Data Designer plugins by what they add to your data generation workflow.

<div class="plugin-doc-grid">
<a class="plugin-doc-card" href="data-designer-docx/" aria-label="Open data-designer-docx documentation">
<span class="plugin-doc-card__header">
<span class="plugin-doc-card__title">data-designer-docx</span>
<span class="plugin-doc-card__version">v0.1.0</span>
</span>
<span class="plugin-doc-card__description">Data Designer processor plugin that renders generated rows as Microsoft Word documents</span>
<span class="plugin-doc-card__section">
<span class="plugin-doc-card__label">Entry points</span>
<span class="plugin-doc-card__chips"><span class="plugin-doc-chip">docx</span></span>
</span>
</a>
<a class="plugin-doc-card" href="data-designer-github/" aria-label="Open data-designer-github documentation">
<span class="plugin-doc-card__header">
<span class="plugin-doc-card__title">data-designer-github</span>
Expand Down
3 changes: 3 additions & 0 deletions plugins/data-designer-docx/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Owner(s) of this plugin — used to generate the root CODEOWNERS file.
# GitHub accepts @username, @org/team, or email format.
* @NVIDIA-NeMo/data_designer_reviewers @mvansegbroeck
46 changes: 46 additions & 0 deletions plugins/data-designer-docx/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# data-designer-docx

Render Data Designer rows as Microsoft Word documents.

Data Designer generates rows; document pipelines consume `.docx`. This plugin
closes that gap with a processor that writes one Word document per row — with
headings, tables, a front-matter metadata table, footers, and Word core
properties — while keeping the file path joined to its row in the dataset.

## Installation

```bash
uv add data-designer data-designer-docx
```

## Usage

```python
import data_designer.config as dd
from data_designer_docx.config import DocxProcessorConfig
from data_designer_docx.schema import WordDocument

config_builder.add_column(
dd.LLMStructuredColumnConfig(
name="document",
model_alias="doc-writer",
output_format=WordDocument,
prompt="Write an internal {{ doc_type }} for {{ company }}.",
)
)

config_builder.add_processor(
DocxProcessorConfig(
name="word-documents",
document_column="document",
filename_template="{{ doc_id }}-{{ doc_type }}.docx",
metadata_columns={"Document ID": "{{ doc_id }}", "Company": "{{ company }}"},
footer_template="{{ company }} · {{ doc_id }}",
)
)
```

Files land in `<artifact_path>/<dataset>/documents/word-documents/`, and the
relative path of each one is written back into the dataset as `docx_path`.

See [`docs/`](docs/) for the full field reference and design notes.
92 changes: 92 additions & 0 deletions plugins/data-designer-docx/docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# data-designer-docx

Renders Data Designer rows as Microsoft Word documents. Registers a `docx`
processor that writes one `.docx` per row and records each file's path back into
the dataset, so rows and documents stay joined.

## Installation

```bash
uv add data-designer data-designer-docx
```

## How it works

The plugin ships a Pydantic model, `WordDocument`, that is used twice: as the
`output_format` of an LLM structured column, and as the input contract of the
renderer. Because both ends share one definition, a model that emits something
unrenderable produces a validation error on the column — which Data Designer
already knows how to retry — instead of a parsing failure downstream.

That is also why the package contains no markdown parsing. The LLM generates the
document's *structure*, and the renderer walks it.

```text
llm-structured(document: WordDocument) -> processor(docx) -> documents/<name>/*.docx
```

## Configuration

| Field | Required | Description |
| --- | --- | --- |
| `name` | Yes | Processor name; also the output subfolder. |
| `document_column` | Yes | Column holding a `WordDocument`-shaped value. |
| `output_subdir` | No | Folder under the dataset directory. Defaults to `documents`. Must be relative and must not name a Data Designer-managed directory. |
| `filename_template` | No | Jinja2 template for the file name. Defaults to `document.docx`, which references no dataset columns; duplicates get a numeric suffix. |
| `output_path_column` | No | Column receiving the written path. Defaults to `docx_path`. |
| `metadata_columns` | No | Label to Jinja2 template pairs, rendered as a front-matter table. |
| `core_property_columns` | No | Word core property (`author`, `category`, `subject`, `keywords`) to Jinja2 template pairs. |
| `template_path` | No | A `.docx` supplying corporate styles, header, and footer. |
| `footer_template` | No | Jinja2 template for the page footer, applied to every section. |
| `table_style` | No | Table style name; must exist in the template. Defaults to `Table Grid`. |
| `number_sections` | No | Prefix section headings with `1.`, `2.`, and so on. Defaults to `True`. |

## Implementation notes

**Stage choice.** The processor implements `process_after_batch` rather than
`process_after_generation`. Documents then stream out while the run is still in
progress, the row count stays fixed as the async engine requires at that stage,
and the dataset stays resumable — `process_after_generation` rewrites the final
parquet and marks the dataset terminal for resume.

**Output location.** Documents are written to `<output_subdir>/<name>/`, never
under `processors-files/`. Both components are validated as contained, relative,
non-reserved path segments, and the resolved directory is asserted to sit beneath
the dataset directory before anything is written. Data Designer reads every directory there back as a
parquet dataset, so `.docx` files placed there make `preview()` fail with
*"Parquet magic bytes not found in footer"*. Binary artifacts get their own
folder, the same way generated images live under `images/`. The config validator
rejects the reserved names.

**Ragged tables.** Structured outputs constrain the shape of the JSON, not the
arithmetic inside it — a model asked for a three-column table will occasionally
return a row with two cells. Rows are padded or truncated to the header width
rather than triggering a retry, which would cost a whole document generation.

**Package layout.** `schema.py` holds the contract, `render.py` is pure
python-docx with no Data Designer imports (so layout can be iterated without
spending tokens), `config.py` is user-facing, and `impl.py` is engine-side.

## Templates

`template_path` should point at a `.docx` containing styles, header, and footer
but **no body content** — python-docx appends generated content after anything
already in the file, so a template with a cover page yields a cover page on every
document.

**Structured values are not re-decoded.** Data Designer's recursive JSON decoding
rewrites string leaves that look like scalars, turning `"30"` into `30` and
`"true"` into `True` — precisely the values a key-data table carries, and values
the schema then rejects. The processor therefore validates the document column
from the raw record and only JSON-decodes it when the top-level value is a
string. The recursively decoded copy is used for Jinja templates only.

**Resume safety.** File name collisions are tracked with case-folded keys, since
macOS and Windows treat `A.docx` and `a.docx` as the same file, and the set is
seeded from documents already on disk. A resumed run therefore cannot overwrite a
document written by a batch that completed before the resume.

**Footers.** Generated content is appended after any body content the template
already has, so it lands in the template's final section. `footer_template` is
applied to every section rather than just the first, which would otherwise leave
the generated pages showing the template's own footer.
Loading
Loading