Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7926cc4
Refactor printers: extract formatting into lightweight base classes
rlundeen2 May 14, 2026
30f6151
Consolidate all printers into pyrit/printer/ module
rlundeen2 May 14, 2026
de61795
Add deprecation warnings for old printer import paths (removed in 0.1…
rlundeen2 May 14, 2026
837ed3f
Rename concrete printers to *MemoryPrinter, move pyrit internals out …
rlundeen2 May 14, 2026
788eceb
Refactor markdown printer, delete dead old ABC files
rlundeen2 May 14, 2026
91a417a
Add missing __all__ to scenario printer deprecation shim
rlundeen2 May 14, 2026
f31d0d0
Fix type checker errors in from_dict methods and MemoryPrinter types
rlundeen2 May 14, 2026
86172c9
Fix ruff lint errors: return types, docstrings, noqa
rlundeen2 May 14, 2026
d117af2
Fix ty type check: make ScenarioResult identifier params optional
rlundeen2 May 14, 2026
65becc5
pr feedback
rlundeen2 May 14, 2026
1eeb7a3
pre-commit
rlundeen2 May 14, 2026
4f29026
fixing test
rlundeen2 May 14, 2026
69ccff3
fixing test
rlundeen2 May 15, 2026
bf32513
self-review
rlundeen2 May 15, 2026
e6a939b
Rename console→pretty, add Sink/PrinterBase plumbing
rlundeen2 May 15, 2026
ae13cdb
Route all output through sinks, add write_async and convenience methods
rlundeen2 May 15, 2026
35f8e9d
Move convenience functions into their domain modules
rlundeen2 May 15, 2026
4720ff7
Move convenience functions to printer/helpers.py
rlundeen2 May 15, 2026
65fedab
Add copilot instructions for printer module
rlundeen2 May 15, 2026
fdfabcc
Add render_async/write_async contract, rename to→sink, add deprecatio…
rlundeen2 May 15, 2026
bd6f021
Extract conversation and score printers, slim attack_result/pretty.py
rlundeen2 May 15, 2026
1bdc53d
Add render_async to leaf classes, update printer instructions
rlundeen2 May 15, 2026
17b8bc7
Add IPythonMarkdownSink, get_default_sink auto-detection, markdown co…
rlundeen2 May 15, 2026
62efab5
adding docs
rlundeen2 May 15, 2026
582f34a
updating help
rlundeen2 May 15, 2026
52c4259
Rename pyrit.printer module to pyrit.output
rlundeen2 May 15, 2026
d4a94af
Revert model to_dict/from_dict changes (moved to separate PR)
rlundeen2 May 15, 2026
712bfd8
Update docs to use pyrit.output, add __init__ exports, fix test
rlundeen2 May 15, 2026
c4d5341
Sync pct_to_ipynb skip list with integration test exclusions
rlundeen2 May 15, 2026
29c3e4d
updating tests
rlundeen2 May 15, 2026
1cbac18
pre-commit
rlundeen2 May 15, 2026
3e1be4b
pre-commit
rlundeen2 May 15, 2026
a00cd0d
updating shims
rlundeen2 May 15, 2026
23973e3
pre-commit
rlundeen2 May 15, 2026
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
67 changes: 67 additions & 0 deletions .github/instructions/output.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
applyTo: "pyrit/output/**"
---

# PyRIT Output Module — Coding & Review Guidelines

For full architecture documentation, usage examples, and extension guides, see [doc/code/output/0_output.py](../../../doc/code/output/0_output.py).

This file covers the rules for **writing and reviewing** code in `pyrit/output/`.

## Critical Rules

### Output goes through the sink — never call `print()` directly

All rendering methods return `str`. The inherited `write_async` calls `render_async` then `_write_async(content)`. No bare `print()` calls anywhere in the output module except inside `StdoutSink`.

When reviewing: reject any `print()` call outside `StdoutSink`.

### Data fetching belongs in leaf classes only

Format classes (`PrettyAttackResultPrinter`, `MarkdownAttackResultPrinter`) must not import or reference `CentralMemory`. Only `*MemoryPrinter` leaf classes do data I/O.

When reviewing: reject any `CentralMemory` import in a non-leaf file (`pretty.py`, `markdown.py`, `json.py`).

### Sinks must use async I/O

Sink implementations must not block the event loop. Use `asyncio.to_thread()` or native async libraries for I/O operations. `FileSink` uses an `asyncio.Lock` to prevent concurrent write races.

When reviewing: reject synchronous `open()`, `write()`, or network calls inside a sink's `write_async`.

## Three-Layer Hierarchy

Every domain follows this structure. Do not mix responsibilities across layers.

| Layer | File | Responsibility | May import CentralMemory? |
|-------|------|---------------|---------------------------|
| **Base** | `base.py` | Abstract data-fetching methods + abstract `render_async` | No |
| **Format** | `pretty.py`, `markdown.py`, `json.py` | Implements `render_async`, returns `str` | No |
| **Leaf** | Same file as format (e.g., `PrettyAttackResultMemoryPrinter`) | Implements data methods via CentralMemory; forwarding `render_async` | Yes |

### File names = output format

- `pretty.py` — ANSI-colored human-readable
- `markdown.py` — Markdown
- `json.py` — structured JSON

### Memory leaf classes must work with zero args

```python
printer = PrettyAttackResultMemoryPrinter() # defaults: StdoutSink, matching sub-printers
await printer.write_async(result)
```

Pass `sink=` to redirect output. Pass sub-printers only to override defaults.

### Convenience functions live in `helpers.py`

Every new domain printer **must** have a corresponding convenience function added to `helpers.py`. This is the primary entry point most callers use.

```python
from pyrit.output.helpers import print_attack_result_async
await print_attack_result_async(result, format="pretty")
```

`helpers.py` resolves `format` → printer class, `sink` → Sink, and calls `write_async`.

When reviewing: if a new domain printer is added without a helper function, request one.
6 changes: 2 additions & 4 deletions doc/code/auxiliary_attacks/0_auxiliary_attacks.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
"source": [
"from pyrit.executor.attack import (\n",
" AttackScoringConfig,\n",
" ConsoleAttackResultPrinter,\n",
" PromptSendingAttack,\n",
")\n",
"from pyrit.prompt_target import OpenAIChatTarget\n",
Expand All @@ -83,8 +82,7 @@
"attack = PromptSendingAttack(objective_target=target, attack_scoring_config=scoring_config)\n",
"result = await attack.execute_async(objective=objective) # type: ignore\n",
"\n",
"printer = ConsoleAttackResultPrinter()\n",
"await printer.print_conversation_async(result=result) # type: ignore"
"await print_attack_result_async(result)"
]
},
{
Expand Down Expand Up @@ -180,7 +178,7 @@
")\n",
"\n",
"result = await attack.execute_async(objective=objective) # type: ignore\n",
"await printer.print_result_async(result=result) # type: ignore"
"await print_attack_result_async(result)"
]
}
],
Expand Down
12 changes: 3 additions & 9 deletions doc/code/auxiliary_attacks/0_auxiliary_attacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,24 @@
# format_version: '1.3'
# jupytext_version: 1.17.3
# ---

# %% [markdown]
# # Auxiliary Attacks

# %% [markdown]
# Auxiliary attacks cover a variety of techniques that do not fit into the core PyRIT functionality.
#
# These attack pipelines may be useful to run before orchestrating other attacks. For example, we provide an Azure Machine Learning (AML) pipeline for generating suffixes using the greedy coordinate gradient (GCG) [@zou2023gcg] algorithm.

# %% [markdown]
# ## GCG Suffixes

# %% [markdown]
# The [GCG demo notebook](1_gcg_azure_ml.ipynb) shows how to create an AML environment and submit a job that generates GCG suffixes, which can be appended to a base prompt to jailbreak a language model. In the example below, we compare the response generated by Phi-3-mini with and without a GCG suffix trained on that model.
#
# First, we send a harmful prompt to Phi-3-mini without a GCG suffix. If the environment variables `PHI3_MINI_ENDPOINT` and `PHI3_MINI_KEY` are not set in your .env file, the target will default to the model with `AZURE_ML_MANAGED_ENDPOINT` and `AZURE_ML_MANAGED_KEY`.

# %%
from pyrit.executor.attack import (
AttackScoringConfig,
ConsoleAttackResultPrinter,
PromptSendingAttack,
)
from pyrit.output import print_attack_result_async
from pyrit.prompt_target import OpenAIChatTarget
from pyrit.score import SelfAskRefusalScorer, TrueFalseInverterScorer
from pyrit.setup import IN_MEMORY, initialize_pyrit_async
Expand All @@ -47,8 +42,7 @@
attack = PromptSendingAttack(objective_target=target, attack_scoring_config=scoring_config)
result = await attack.execute_async(objective=objective) # type: ignore

printer = ConsoleAttackResultPrinter()
await printer.print_conversation_async(result=result) # type: ignore
await print_attack_result_async(result)

# %% [markdown]
# Next, let's apply a GCG suffix trained on Phi-3-mini to the base prompt using the `SuffixAppendConverter`.
Expand All @@ -73,4 +67,4 @@
)

result = await attack.execute_async(objective=objective) # type: ignore
await printer.print_result_async(result=result) # type: ignore
await print_attack_result_async(result)
8 changes: 2 additions & 6 deletions doc/code/converters/0_converters.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,6 @@
"source": [
"from pyrit.executor.attack import (\n",
" AttackConverterConfig,\n",
" ConsoleAttackResultPrinter,\n",
" PromptSendingAttack,\n",
")\n",
"from pyrit.prompt_converter import StringJoinConverter, VariationConverter\n",
Expand All @@ -306,8 +305,7 @@
"\n",
"result = await attack.execute_async(objective=objective) # type: ignore\n",
"\n",
"printer = ConsoleAttackResultPrinter()\n",
"await printer.print_conversation_async(result=result) # type: ignore"
"await print_attack_result_async(result)"
]
},
{
Expand Down Expand Up @@ -370,7 +368,6 @@
"source": [
"from pyrit.executor.attack import (\n",
" AttackConverterConfig,\n",
" ConsoleAttackResultPrinter,\n",
" PromptSendingAttack,\n",
")\n",
"from pyrit.prompt_converter import TranslationConverter\n",
Expand Down Expand Up @@ -407,8 +404,7 @@
"result = await attack.execute_async(objective=objective) # type: ignore\n",
"\n",
"# Print the conversation showing both original and converted values\n",
"printer = ConsoleAttackResultPrinter()\n",
"await printer.print_conversation_async(result=result) # type: ignore"
"await print_attack_result_async(result)"
]
}
],
Expand Down
12 changes: 3 additions & 9 deletions doc/code/converters/0_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
# format_version: '1.3'
# jupytext_version: 1.19.1
# ---

# %% [markdown]
# # Converters

# %% [markdown]
# Converters are used to transform prompts before sending them to the target.
#
Expand All @@ -24,10 +22,10 @@
# ## Converter Modality Reference Table
#
# The following table shows all available converters organized by their input and output modalities:

# %%
import pandas as pd

from pyrit.output import print_attack_result_async
from pyrit.prompt_converter import get_converter_modalities
from pyrit.setup import IN_MEMORY, initialize_pyrit_async

Expand Down Expand Up @@ -102,7 +100,6 @@
# %%
from pyrit.executor.attack import (
AttackConverterConfig,
ConsoleAttackResultPrinter,
PromptSendingAttack,
)
from pyrit.prompt_converter import StringJoinConverter, VariationConverter
Expand All @@ -129,8 +126,7 @@

result = await attack.execute_async(objective=objective) # type: ignore

printer = ConsoleAttackResultPrinter()
await printer.print_conversation_async(result=result) # type: ignore
await print_attack_result_async(result)

# %% [markdown]
# ## Response Converters
Expand All @@ -154,7 +150,6 @@
# %%
from pyrit.executor.attack import (
AttackConverterConfig,
ConsoleAttackResultPrinter,
PromptSendingAttack,
)
from pyrit.prompt_converter import TranslationConverter
Expand Down Expand Up @@ -191,5 +186,4 @@
result = await attack.execute_async(objective=objective) # type: ignore

# Print the conversation showing both original and converted values
printer = ConsoleAttackResultPrinter()
await printer.print_conversation_async(result=result) # type: ignore
await print_attack_result_async(result)
11 changes: 5 additions & 6 deletions doc/code/converters/5_file_converters.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@
"from pyrit.common.path import CONVERTER_SEED_PROMPT_PATH\n",
"from pyrit.executor.attack import (\n",
" AttackConverterConfig,\n",
" ConsoleAttackResultPrinter,\n",
" PromptSendingAttack,\n",
")\n",
"from pyrit.models import SeedPrompt\n",
Expand Down Expand Up @@ -147,7 +146,7 @@
")\n",
"\n",
"result = await attack.execute_async(objective=prompt) # type: ignore\n",
"await ConsoleAttackResultPrinter().print_conversation_async(result=result) # type: ignore"
"await print_attack_result_async(result)"
]
},
{
Expand Down Expand Up @@ -226,7 +225,7 @@
")\n",
"\n",
"result = await attack.execute_async(objective=prompt) # type: ignore\n",
"await ConsoleAttackResultPrinter().print_conversation_async(result=result) # type: ignore"
"await print_attack_result_async(result)"
]
},
{
Expand Down Expand Up @@ -363,7 +362,7 @@
")\n",
"\n",
"result = await attack.execute_async(objective=\"Modify existing PDF\") # type: ignore\n",
"await ConsoleAttackResultPrinter().print_conversation_async(result=result) # type: ignore"
"await print_attack_result_async(result)"
]
},
{
Expand Down Expand Up @@ -445,7 +444,7 @@
")\n",
"\n",
"result = await attack.execute_async(objective=\"This is a simple test string for Word document generation.\") # type: ignore\n",
"await ConsoleAttackResultPrinter().print_conversation_async(result=result) # type: ignore"
"await print_attack_result_async(result)"
]
},
{
Expand Down Expand Up @@ -529,7 +528,7 @@
")\n",
"\n",
"result = await attack.execute_async(objective=\"AI Red Team Engineer\") # type: ignore\n",
"await ConsoleAttackResultPrinter().print_conversation_async(result=result) # type: ignore"
"await print_attack_result_async(result)"
]
}
],
Expand Down
16 changes: 6 additions & 10 deletions doc/code/converters/5_file_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
# format_version: '1.3'
# jupytext_version: 1.17.3
# ---

# %% [markdown]
# # 5. File Converters
#
Expand All @@ -20,7 +19,6 @@
#
# - **PDFConverter**: Convert text to PDF documents with templates or direct generation
# - **WordDocConverter**: Convert text to Word (.docx) documents with optional placeholder injection

# %% [markdown]
# ## PDFConverter
#
Expand All @@ -29,22 +27,20 @@
# 1. **Template-Based PDF Generation**: Use YAML templates to render dynamic content into PDFs
# 2. **Direct Prompt PDF Generation**: Convert plain text strings into PDFs without templates
# 3. **Modify Existing PDFs**: Inject text into existing PDF documents

# %% [markdown]
# ### Template-Based PDF Generation
#
# This mode populates placeholders in a YAML-based template and converts the rendered content into a PDF.

# %%
import pathlib

from pyrit.common.path import CONVERTER_SEED_PROMPT_PATH
from pyrit.executor.attack import (
AttackConverterConfig,
ConsoleAttackResultPrinter,
PromptSendingAttack,
)
from pyrit.models import SeedPrompt
from pyrit.output import print_attack_result_async
from pyrit.prompt_converter import PDFConverter
from pyrit.prompt_normalizer import PromptConverterConfiguration
from pyrit.prompt_target import TextTarget
Expand Down Expand Up @@ -98,7 +94,7 @@
)

result = await attack.execute_async(objective=prompt) # type: ignore
await ConsoleAttackResultPrinter().print_conversation_async(result=result) # type: ignore
await print_attack_result_async(result)

# %% [markdown]
# ### Direct Prompt PDF Generation (No Template)
Expand Down Expand Up @@ -133,7 +129,7 @@
)

result = await attack.execute_async(objective=prompt) # type: ignore
await ConsoleAttackResultPrinter().print_conversation_async(result=result) # type: ignore
await print_attack_result_async(result)

# %% [markdown]
# ### Modifying Existing PDFs with Injection Items
Expand Down Expand Up @@ -205,7 +201,7 @@
)

result = await attack.execute_async(objective="Modify existing PDF") # type: ignore
await ConsoleAttackResultPrinter().print_conversation_async(result=result) # type: ignore
await print_attack_result_async(result)

# %% [markdown]
# ## WordDocConverter
Expand Down Expand Up @@ -238,7 +234,7 @@
)

result = await attack.execute_async(objective="This is a simple test string for Word document generation.") # type: ignore
await ConsoleAttackResultPrinter().print_conversation_async(result=result) # type: ignore
await print_attack_result_async(result)

# %% [markdown]
# ### Placeholder-Based Injection into Existing Word Documents
Expand Down Expand Up @@ -278,4 +274,4 @@
)

result = await attack.execute_async(objective="AI Red Team Engineer") # type: ignore
await ConsoleAttackResultPrinter().print_conversation_async(result=result) # type: ignore
await print_attack_result_async(result)
Loading
Loading