Metadata-Version: 2.4
Name: pydantic-promptmodel
Version: 0.4.1
Summary: Compile typed Python prompt structures into deterministic Markdown or XML
Keywords: llm,markdown,prompt,pydantic,xml
Author: Hillel Twersky
License-Expression: MIT
License-File: LICENSE
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Dist: pydantic>=2.1,<3
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/Thillel/pydantic-promptmodel
Project-URL: Repository, https://github.com/Thillel/pydantic-promptmodel
Project-URL: Issues, https://github.com/Thillel/pydantic-promptmodel/issues
Description-Content-Type: text/markdown

# pydantic-promptmodel

[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Thillel/pydantic-promptmodel)

Compile typed Python prompt structures into deterministic Markdown or XML.

`pydantic-promptmodel` gives prompt code the same properties you expect from other
application code: typed inputs, validation, reusable structure, deterministic output,
and reviewable diffs. Define the meaning once, then choose Markdown or XML without
maintaining parallel templates.

## In 20 seconds

```text
uv add pydantic-promptmodel
```

Or use `pip install pydantic-promptmodel`.

<!-- docs:quick-start:start -->
```python
from pydantic_promptmodel import PromptModel


class ReviewPrompt(PromptModel):
    _title: str = "Code Review"
    _body: str = "Review the supplied change as a senior engineer."
    task: str
    checks: tuple[str, ...] = ("Correctness", "Security", "Maintainability")


prompt = ReviewPrompt(task="Return prioritized findings with concrete fixes.")
```
<!-- docs:quick-start:end -->

<!-- docs:quick-start-output:start -->
```markdown
# Code Review

Review the supplied change as a senior engineer.

**Task:** Return prioritized findings with concrete fixes.

## Checks

- Correctness
- Security
- Maintainability
```
<!-- docs:quick-start-output:end -->

```python
markdown_prompt = prompt.to_markdown()
xml_prompt = prompt.to_xml()
```

Both outputs preserve the same field order, hierarchy, values, and repetition. The
renderer owns heading levels, labels, list markers, XML names, indentation, and
escaping.

## The maintenance problem it removes

An interpolated prompt begins simply, but its structure, escaping, validation, and
alternate formats quickly become application responsibilities:

````python
prompt_text = f"""# Support Triage

Classify the ticket and propose the next action.

## Priorities

- Protect account access
- Avoid unsupported promises

## Ticket

```text
{ticket_text}
```
"""
````

Represent the same meaning as typed data instead:

```python
from typing import Annotated

from pydantic_promptmodel import PromptModel, Slot


class TriagePrompt(PromptModel):
    _title: str = "Support Triage"
    _body: str = "Classify the ticket and propose the next action."
    priorities: tuple[str, ...] = (
        "Protect account access",
        "Avoid unsupported promises",
    )
    ticket: Annotated[str, Slot()]


prompt_text = TriagePrompt(ticket=ticket_text).to_markdown()
```

Pydantic validates the input, the renderer safely expands a Markdown fence when the
ticket contains backticks, and `.to_xml()` remains available without a second prompt
definition.

## Who is this for?

Use `pydantic-promptmodel` when prompts are maintained as application code and you
care about:

- reviewing prompt changes as stable pull-request diffs;
- validating runtime inputs before rendering;
- sharing one prompt structure across model providers or output formats;
- snapshot testing complete prompts;
- composing prompts from existing Pydantic models or dataclasses; or
- making data boundaries and unsupported structures explicit.

It deliberately produces one canonical representation per format. It is not intended
for pixel-level control over arbitrary Markdown/XML layouts or extensive template
control flow.

## What you get

- **One typed source of truth.** Nested models, collections, schemas, literal examples,
  and runtime values compose through normal Python types.
- **Canonical Markdown and XML.** The two renderers lower independently from one shared
  plan; neither format is converted into the other.
- **Deterministic output.** Declaration order and stored sequence order are preserved,
  making snapshots and prompt review useful.
- **Pydantic validation.** Prompt values are checked before they reach a model client.
- **Useful defaults with narrow overrides.** Ordinary models need little or no metadata;
  explicit annotations handle genuine ambiguity such as tables or XML item names.
- **No required inheritance.** Standalone functions render existing Pydantic models and
  standard dataclasses.

## Render an existing model

You do not need to inherit from `PromptModel`:

```python
from pydantic import BaseModel
from pydantic_promptmodel import render_markdown, render_xml


class FlightContext(BaseModel):
    departure_city: str
    arrival_city: str


context = FlightContext(departure_city="Lisbon", arrival_city="Tel Aviv")

markdown_context = render_markdown(context, title="Flight Context")
xml_context = render_xml(context)
```

Standard dataclass instances use the same standalone renderers. If you own an
existing `BaseModel` and prefer method syntax, inherit as
`class FlightContext(BaseModel, PromptModel)`.

## Use it with Pydantic AI

Rendered prompts are ordinary strings, so they can be passed directly to a model
client or agent framework. With [Pydantic AI](https://pydantic.dev/docs/ai/):

```python
from pydantic_ai import Agent


agent = Agent(
    "openai:gpt-5.2",
    system_prompt=prompt.to_markdown(),
)
result = agent.run_sync("Review src/checkout.py.")
```

Pydantic AI remains an independent optional dependency; install it separately when
using this integration. The rendered XML can be supplied in the same way.

## Snapshot-test a prompt

Deterministic rendering makes a checked-in prompt snapshot an exact contract:

```python
from pathlib import Path


def test_code_review_prompt() -> None:
    expected = Path("tests/snapshots/code-review.md").read_text()
    assert prompt.to_markdown() == expected
```

A semantic model change produces an ordinary text diff. Accidental changes to
headings, order, labels, escaping, or runtime boundaries fail the test.

## How it works

```text
PromptModel / Pydantic BaseModel / dataclass
                    |
          reflection and validation
                    |
          shared rendering plan
               /          \
 canonical Markdown    canonical XML
```

The public plan is inspectable, but normal application code renders model instances
directly.

## Complete examples

- [Travel support](https://github.com/Thillel/pydantic-promptmodel/blob/main/examples/travel_support.py)
  models tools, procedures, ordered handling rules, and repeated records.
- [Code review](https://github.com/Thillel/pydantic-promptmodel/blob/main/examples/code_review.py)
  isolates a runtime diff and renders a prompt-visible output schema.
- [Research brief](https://github.com/Thillel/pydantic-promptmodel/blob/main/examples/research_brief.py)
  combines runtime source material with typed research and citation requirements.

Each example is runnable with `uv run python examples/<name>.py`.

## Rich rendering structures

Natural model hierarchy becomes sections and repeated elements. Use explicit local
metadata only where the model alone cannot choose the intended presentation.

For example, `Table()` renders a homogeneous sequence of shallow records as a
Markdown table while XML retains its normal repeated-element sequence:

```python
from typing import Annotated

from pydantic_promptmodel import PromptModel, Table


class Finding(PromptModel):
    severity: str
    line_number: int
    message: str


class ReviewResult(PromptModel):
    findings: Annotated[list[Finding], Table()]
```

```markdown
# Findings

| Severity | Line Number | Message |
| --- | --- | --- |
| high | 42 | Reject malformed input. |
| low | 8 | Clarify the example. |
```

`Table()` supports one-depth records with single-line scalar cells. Optional values
become empty cells, declaration and row order are preserved, and `Ordered()` adds a
numbered first column. Unsupported table shapes fail with a structural location.

Other narrow overrides include `Label`, `XmlName`, `ItemName`, `Inline`, `Block`,
`Ordered`, `LiteralBlock`, and `Slot`.

## Runtime data boundaries

`Slot()` marks validated runtime data. Markdown places its value inside an
escape-aware code fence; XML emits escaped character data. This prevents runtime
content from altering the generated Markdown or XML structure.

The boundary is syntactic, not semantic. A model can still interpret instructions
inside fenced or escaped data, so `Slot()` is not a prompt-injection defense.

`LiteralBlock` provides the same opaque rendering behavior for prompt-authored
examples such as JSON, source code, or logs.

## Document-level controls

Renderer options apply across the complete model graph:

```python
markdown_fragment = render_markdown(
    context,
    naming="verbatim",
    title="flight_context",
    start_level=2,
    heading_overflow="bold",
    scalar_style="auto",
    empty_sequences="marker",
)

xml_prompt = render_xml(
    context,
    naming="verbatim",
    root_name="flight_context",
    fallback_item_name="criterion",
)
```

`naming="verbatim"` preserves identifiers. Markdown scalar and heading policies
control canonical presentation without changing the model. XML root and fallback
item names resolve document-level ambiguity. Field-local metadata takes precedence.

## Design philosophy

The model is the source of truth. The library aims to produce one clear, correct,
and useful representation for each format—not reproduce every possible hand-written
Markdown or XML layout.

Supports Python 3.11+ and Pydantic 2.

## Learn more

- [Rendering reference](https://github.com/Thillel/pydantic-promptmodel/blob/main/docs/rendering.md)
- [Design](https://github.com/Thillel/pydantic-promptmodel/blob/main/docs/design.md)
- [Testing](https://github.com/Thillel/pydantic-promptmodel/blob/main/docs/testing.md)
- [Changelog](https://github.com/Thillel/pydantic-promptmodel/blob/main/CHANGELOG.md)

## Development

```text
make format
make lint
make test
make check
make build
```

Release maintainers should follow the
[trusted-publishing guide](https://github.com/Thillel/pydantic-promptmodel/blob/main/docs/releasing.md).
