Metadata-Version: 2.4
Name: pydantic-promptmodel
Version: 0.3.0
Summary: Convention-first typed prompt models with canonical Markdown and 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.13.4,<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

Write prompts as typed Python. Render them as clean Markdown or XML.

`pydantic-promptmodel` turns a natural class hierarchy into a ready-to-send LLM
prompt. Define the structure once, validate values with Pydantic, and switch output
formats with one method call—useful for comparing models, providers, and prompting
strategies without maintaining two templates.

## Quick start

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

Or:

```text
pip install pydantic-promptmodel
```

Define the meaning of your prompt, without specifying heading levels, list markers,
or XML tags:

```python
from pydantic_promptmodel import PromptModel


class CodeReviewPrompt(PromptModel):
    _title: str = "Code Review"
    _body: str = "Review the supplied pull request as a senior engineer."
    focus_areas: tuple[str, ...] = (
        "Correctness and edge cases",
        "Security and data handling",
        "Clear, maintainable design",
    )
    output: str = "Return prioritized findings with a concrete fix for each one."


review = CodeReviewPrompt()

markdown_prompt = review.to_markdown()
xml_prompt = review.to_xml()
```

Choose a format at runtime with your own model-routing policy:

```python
def render_for(model_name: str) -> str:
    if model_name.startswith("claude"):
        return review.to_xml()
    return review.to_markdown()
```

That routing choice belongs to your application; either renderer can be used with
any model provider.

The Markdown is immediately useful:

```markdown
# Code Review

Review the supplied pull request as a senior engineer.

## Focus Areas

- Correctness and edge cases
- Security and data handling
- Clear, maintainable design

**Output:** Return prioritized findings with a concrete fix for each one.
```

The same instance also produces equivalent XML:

```xml
<code-review-prompt title="Code Review">
  <content>Review the supplied pull request as a senior engineer.</content>
  <focus-areas>
    <focus-area>Correctness and edge cases</focus-area>
    <focus-area>Security and data handling</focus-area>
    <focus-area>Clear, maintainable design</focus-area>
  </focus-areas>
  <output>Return prioritized findings with a concrete fix for each one.</output>
</code-review-prompt>
```

## Richer Markdown structures

Typed records, semantic item titles, compact scalar labels, and ordered steps
compose without Markdown-specific layout code:

<!-- docs:travel-support-models:start -->
```python
from typing import Annotated

from pydantic_promptmodel import Ordered, PromptModel


class TravelProcedure(PromptModel):
    _title: str
    customer_goal: str
    required_checks: tuple[str, ...]
    tool_call: str
    completion: str


class TravelSupportPrompt(PromptModel):
    _title: str
    _body: str
    available_tools: tuple[str, ...]
    procedures: list[TravelProcedure]
    handling_order: Annotated[list[str], Ordered()]
```
<!-- docs:travel-support-models:end -->

The [complete runnable example](https://github.com/Thillel/pydantic-promptmodel/blob/main/examples/travel_support.py)
generates compact Markdown while preserving the model hierarchy:

<!-- docs:travel-support-output:start -->
```markdown
# Waypoint Travel Desk

Resolve the request with the documented booking tools and procedures.

## Available Tools

- get_booking
- search_flights
- change_flight

## Procedures

### Change a departure

**Customer Goal:** Move an existing trip to another available flight.

#### Required Checks

- Retrieve the current booking.
- Confirm the requested travel date and destination.

**Tool Call:** search_flights, then change_flight
**Completion:** Return the confirmed itinerary and any fare difference.

### Review an existing trip

**Customer Goal:** Answer a question about a confirmed itinerary.

#### Required Checks

- Retrieve the booking before describing it.
- Use the itinerary returned by the tool.

**Tool Call:** get_booking
**Completion:** Summarize only the requested itinerary details.

## Handling Order

1. Retrieve the booking before making a change.
2. Use the procedure matching the customer request.
3. Summarize the completed action.
```
<!-- docs:travel-support-output:end -->

## Render existing models

You can render an existing Pydantic model without adding `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)
xml_context = render_xml(context)
```

Standard dataclass instances use the same standalone renderers.

If you own an existing `BaseModel` and want method syntax, add `PromptModel` after
the Pydantic base:

```python
from pydantic import BaseModel
from pydantic_promptmodel import PromptModel


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


markdown_context = FlightContext(
    departure_city="Lisbon",
    arrival_city="Tel Aviv",
).to_markdown()
```

## Why use it?

- **One prompt model, multiple formats.** Change `.to_markdown()` to `.to_xml()`
  without rewriting or synchronizing templates.
- **Natural structure.** Nested models become nested sections, `list[str]` becomes
  a readable list, short scalar fields stay compact, and `snake_case` names become
  human-friendly labels.
- **Pydantic validation.** Prompt inputs are typed and validated before they reach
  the model provider.
- **Deterministic output.** The same prompt instance always renders the same text,
  making snapshots, reviews, and A/B tests straightforward.
- **Useful defaults first.** Most prompts need no formatting metadata. Local
  overrides are available when a label, XML name, ordering rule, literal block, or
  runtime slot needs special treatment.

`_title` names the current prompt or nested section, while `_body` is prose owned
directly by it. Ordinary fields named `title` and `body` remain available for your
domain.

## Document-level controls

Renderer options apply across the complete model graph, including unmodified
Pydantic `BaseModel` instances:

```python
from pydantic_promptmodel import render_markdown, render_xml


markdown_fragment = render_markdown(
    existing_model,
    naming="verbatim",
    title="system_instructions",
    start_level=3,
    heading_overflow="bold",
    scalar_style="auto",
    empty_sequences="marker",
)

xml_prompt = render_xml(
    existing_model,
    naming="verbatim",
    root_name="system_instructions",
    fallback_item_name="criterion",
)
```

`naming="verbatim"` preserves field identifiers in Markdown labels and XML field
wrappers. `fallback_item_name` is used only when conservative singularization would
otherwise emit `<item>`. Local `Label`, `XmlName`, and `ItemName` metadata still
take precedence over document-level inference.

Markdown automatically renders single-line scalar values of up to 120 characters
as compact `**Label:** value` lines. Longer and multiline values retain headings.
`scalar_style="block"` restores heading-based scalars for a document;
`Inline()` and `Block()` provide local overrides. Empty sequences render as
`- (none)` by default or can be dropped with `empty_sequences="omit"`.

`PromptModel.to_markdown()` and `.to_xml()` accept the same format-specific options.
Defaults retain the canonical output shown in the quick start.

## Design philosophy

The model is the source of truth. The library aims to produce one beautiful,
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).
