Skip to content

Commit 5174d8e

Browse files
committed
Merge remote-tracking branch 'origin/main' into romanlutz/gui-home-landing-page
2 parents 45e91a1 + 6850d2e commit 5174d8e

164 files changed

Lines changed: 8360 additions & 4053 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
applyTo: "pyrit/output/**"
3+
---
4+
5+
# PyRIT Output Module — Coding & Review Guidelines
6+
7+
For full architecture documentation, usage examples, and extension guides, see [doc/code/output/0_output.py](../../../doc/code/output/0_output.py).
8+
9+
This file covers the rules for **writing and reviewing** code in `pyrit/output/`.
10+
11+
## Critical Rules
12+
13+
### Output goes through the sink — never call `print()` directly
14+
15+
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`.
16+
17+
When reviewing: reject any `print()` call outside `StdoutSink`.
18+
19+
### Data fetching belongs in leaf classes only
20+
21+
Format classes (`PrettyAttackResultPrinter`, `MarkdownAttackResultPrinter`) must not import or reference `CentralMemory`. Only `*MemoryPrinter` leaf classes do data I/O.
22+
23+
When reviewing: reject any `CentralMemory` import in a non-leaf file (`pretty.py`, `markdown.py`, `json.py`).
24+
25+
### Sinks must use async I/O
26+
27+
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.
28+
29+
When reviewing: reject synchronous `open()`, `write()`, or network calls inside a sink's `write_async`.
30+
31+
## Three-Layer Hierarchy
32+
33+
Every domain follows this structure. Do not mix responsibilities across layers.
34+
35+
| Layer | File | Responsibility | May import CentralMemory? |
36+
|-------|------|---------------|---------------------------|
37+
| **Base** | `base.py` | Abstract data-fetching methods + abstract `render_async` | No |
38+
| **Format** | `pretty.py`, `markdown.py`, `json.py` | Implements `render_async`, returns `str` | No |
39+
| **Leaf** | Same file as format (e.g., `PrettyAttackResultMemoryPrinter`) | Implements data methods via CentralMemory; forwarding `render_async` | Yes |
40+
41+
### File names = output format
42+
43+
- `pretty.py` — ANSI-colored human-readable
44+
- `markdown.py` — Markdown
45+
- `json.py` — structured JSON
46+
47+
### Memory leaf classes must work with zero args
48+
49+
```python
50+
printer = PrettyAttackResultMemoryPrinter() # defaults: StdoutSink, matching sub-printers
51+
await printer.write_async(result)
52+
```
53+
54+
Pass `sink=` to redirect output. Pass sub-printers only to override defaults.
55+
56+
### Convenience functions live in `helpers.py`
57+
58+
Every new domain printer **must** have a corresponding convenience function added to `helpers.py`. This is the primary entry point most callers use.
59+
60+
```python
61+
from pyrit.output.helpers import output_attack_async
62+
await output_attack_async(result, format="pretty")
63+
```
64+
65+
`helpers.py` resolves `format` → printer class, `sink` → Sink, and calls `write_async`.
66+
67+
When reviewing: if a new domain printer is added without a helper function, request one.

.github/workflows/docs.yml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,24 @@ jobs:
5555
- name: Install PyRIT with uv
5656
run: uv sync --extra all
5757

58-
# Build the book
58+
# LaTeX toolchain needed for the PDF export (jupyter-book --pdf via xelatex).
59+
# Mirrors the ReadTheDocs build so CI catches PDF-only issues (e.g. broken
60+
# image paths) instead of letting them silently break the RTD build.
61+
- name: Install LaTeX (for PDF export)
62+
run: |
63+
sudo apt-get update
64+
sudo apt-get install -y --no-install-recommends \
65+
texlive-xetex \
66+
texlive-fonts-recommended \
67+
texlive-fonts-extra \
68+
texlive-plain-generic \
69+
texlive-latex-extra \
70+
latexmk
71+
72+
# Build the book (HTML site + PDF export)
5973
- name: Build the book
6074
run: |
61-
make docs-build
75+
make docs-build-all
6276
# Upload the book's HTML as an artifact
6377
- name: Upload artifact
6478
uses: actions/upload-pages-artifact@v3

Makefile

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,25 @@ ty:
2020
# Build the full documentation site:
2121
# 1. Generate API reference JSON from Python source (griffe)
2222
# 2. Convert API JSON to MyST markdown pages
23-
# 3. Build the Jupyter Book site
23+
# 3. Build the Jupyter Book site (HTML only — fast, no LaTeX needed)
2424
# 4. Generate RSS feed
2525
docs-build:
2626
uv run python build_scripts/pydoc2json.py pyrit --submodules -o doc/_api/pyrit_all.json
2727
uv run python build_scripts/gen_api_md.py
2828
cd doc && uv run jupyter-book build --all --html
2929
uv run ./build_scripts/generate_rss.py
3030

31+
# Build the full documentation site including the PDF export.
32+
# Mirrors the ReadTheDocs build (.readthedocs.yaml) so CI catches PDF-only issues
33+
# such as missing images that the HTML-only build silently ignores.
34+
# Requires xelatex / latexmk on PATH (texlive-xetex + texlive-fonts-recommended +
35+
# texlive-plain-generic + latexmk on Ubuntu).
36+
docs-build-all:
37+
uv run python build_scripts/pydoc2json.py pyrit --submodules -o doc/_api/pyrit_all.json
38+
uv run python build_scripts/gen_api_md.py
39+
cd doc && uv run jupyter-book build --all --html --pdf
40+
uv run ./build_scripts/generate_rss.py
41+
3142
# Regenerate only the API reference pages (without building the full site)
3243
docs-api:
3344
uv run python build_scripts/pydoc2json.py pyrit --submodules -o doc/_api/pyrit_all.json

doc/code/auxiliary_attacks/0_auxiliary_attacks.ipynb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@
6464
"source": [
6565
"from pyrit.executor.attack import (\n",
6666
" AttackScoringConfig,\n",
67-
" ConsoleAttackResultPrinter,\n",
6867
" PromptSendingAttack,\n",
6968
")\n",
7069
"from pyrit.prompt_target import OpenAIChatTarget\n",
@@ -83,8 +82,7 @@
8382
"attack = PromptSendingAttack(objective_target=target, attack_scoring_config=scoring_config)\n",
8483
"result = await attack.execute_async(objective=objective) # type: ignore\n",
8584
"\n",
86-
"printer = ConsoleAttackResultPrinter()\n",
87-
"await printer.print_conversation_async(result=result) # type: ignore"
85+
"await output_attack_async(result)"
8886
]
8987
},
9088
{
@@ -180,7 +178,7 @@
180178
")\n",
181179
"\n",
182180
"result = await attack.execute_async(objective=objective) # type: ignore\n",
183-
"await printer.print_result_async(result=result) # type: ignore"
181+
"await output_attack_async(result)"
184182
]
185183
}
186184
],

doc/code/auxiliary_attacks/0_auxiliary_attacks.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,24 @@
88
# format_version: '1.3'
99
# jupytext_version: 1.17.3
1010
# ---
11-
1211
# %% [markdown]
1312
# # Auxiliary Attacks
14-
1513
# %% [markdown]
1614
# Auxiliary attacks cover a variety of techniques that do not fit into the core PyRIT functionality.
1715
#
1816
# 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.
19-
2017
# %% [markdown]
2118
# ## GCG Suffixes
22-
2319
# %% [markdown]
2420
# 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.
2521
#
2622
# 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`.
27-
2823
# %%
2924
from pyrit.executor.attack import (
3025
AttackScoringConfig,
31-
ConsoleAttackResultPrinter,
3226
PromptSendingAttack,
3327
)
28+
from pyrit.output import output_attack_async
3429
from pyrit.prompt_target import OpenAIChatTarget
3530
from pyrit.score import SelfAskRefusalScorer, TrueFalseInverterScorer
3631
from pyrit.setup import IN_MEMORY, initialize_pyrit_async
@@ -47,8 +42,7 @@
4742
attack = PromptSendingAttack(objective_target=target, attack_scoring_config=scoring_config)
4843
result = await attack.execute_async(objective=objective) # type: ignore
4944

50-
printer = ConsoleAttackResultPrinter()
51-
await printer.print_conversation_async(result=result) # type: ignore
45+
await output_attack_async(result)
5246

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

7569
result = await attack.execute_async(objective=objective) # type: ignore
76-
await printer.print_result_async(result=result) # type: ignore
70+
await output_attack_async(result)

doc/code/converters/0_converters.ipynb

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,6 @@
279279
"source": [
280280
"from pyrit.executor.attack import (\n",
281281
" AttackConverterConfig,\n",
282-
" ConsoleAttackResultPrinter,\n",
283282
" PromptSendingAttack,\n",
284283
")\n",
285284
"from pyrit.prompt_converter import StringJoinConverter, VariationConverter\n",
@@ -306,8 +305,7 @@
306305
"\n",
307306
"result = await attack.execute_async(objective=objective) # type: ignore\n",
308307
"\n",
309-
"printer = ConsoleAttackResultPrinter()\n",
310-
"await printer.print_conversation_async(result=result) # type: ignore"
308+
"await output_attack_async(result)"
311309
]
312310
},
313311
{
@@ -370,7 +368,6 @@
370368
"source": [
371369
"from pyrit.executor.attack import (\n",
372370
" AttackConverterConfig,\n",
373-
" ConsoleAttackResultPrinter,\n",
374371
" PromptSendingAttack,\n",
375372
")\n",
376373
"from pyrit.prompt_converter import TranslationConverter\n",
@@ -407,8 +404,7 @@
407404
"result = await attack.execute_async(objective=objective) # type: ignore\n",
408405
"\n",
409406
"# Print the conversation showing both original and converted values\n",
410-
"printer = ConsoleAttackResultPrinter()\n",
411-
"await printer.print_conversation_async(result=result) # type: ignore"
407+
"await output_attack_async(result)"
412408
]
413409
}
414410
],

doc/code/converters/0_converters.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@
88
# format_version: '1.3'
99
# jupytext_version: 1.19.1
1010
# ---
11-
1211
# %% [markdown]
1312
# # Converters
14-
1513
# %% [markdown]
1614
# Converters are used to transform prompts before sending them to the target.
1715
#
@@ -24,10 +22,10 @@
2422
# ## Converter Modality Reference Table
2523
#
2624
# The following table shows all available converters organized by their input and output modalities:
27-
2825
# %%
2926
import pandas as pd
3027

28+
from pyrit.output import output_attack_async
3129
from pyrit.prompt_converter import get_converter_modalities
3230
from pyrit.setup import IN_MEMORY, initialize_pyrit_async
3331

@@ -102,7 +100,6 @@
102100
# %%
103101
from pyrit.executor.attack import (
104102
AttackConverterConfig,
105-
ConsoleAttackResultPrinter,
106103
PromptSendingAttack,
107104
)
108105
from pyrit.prompt_converter import StringJoinConverter, VariationConverter
@@ -129,8 +126,7 @@
129126

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

132-
printer = ConsoleAttackResultPrinter()
133-
await printer.print_conversation_async(result=result) # type: ignore
129+
await output_attack_async(result)
134130

135131
# %% [markdown]
136132
# ## Response Converters
@@ -154,7 +150,6 @@
154150
# %%
155151
from pyrit.executor.attack import (
156152
AttackConverterConfig,
157-
ConsoleAttackResultPrinter,
158153
PromptSendingAttack,
159154
)
160155
from pyrit.prompt_converter import TranslationConverter
@@ -191,5 +186,4 @@
191186
result = await attack.execute_async(objective=objective) # type: ignore
192187

193188
# Print the conversation showing both original and converted values
194-
printer = ConsoleAttackResultPrinter()
195-
await printer.print_conversation_async(result=result) # type: ignore
189+
await output_attack_async(result)

doc/code/converters/5_file_converters.ipynb

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@
9090
"from pyrit.common.path import CONVERTER_SEED_PROMPT_PATH\n",
9191
"from pyrit.executor.attack import (\n",
9292
" AttackConverterConfig,\n",
93-
" ConsoleAttackResultPrinter,\n",
9493
" PromptSendingAttack,\n",
9594
")\n",
9695
"from pyrit.models import SeedPrompt\n",
@@ -147,7 +146,7 @@
147146
")\n",
148147
"\n",
149148
"result = await attack.execute_async(objective=prompt) # type: ignore\n",
150-
"await ConsoleAttackResultPrinter().print_conversation_async(result=result) # type: ignore"
149+
"await output_attack_async(result)"
151150
]
152151
},
153152
{
@@ -226,7 +225,7 @@
226225
")\n",
227226
"\n",
228227
"result = await attack.execute_async(objective=prompt) # type: ignore\n",
229-
"await ConsoleAttackResultPrinter().print_conversation_async(result=result) # type: ignore"
228+
"await output_attack_async(result)"
230229
]
231230
},
232231
{
@@ -363,7 +362,7 @@
363362
")\n",
364363
"\n",
365364
"result = await attack.execute_async(objective=\"Modify existing PDF\") # type: ignore\n",
366-
"await ConsoleAttackResultPrinter().print_conversation_async(result=result) # type: ignore"
365+
"await output_attack_async(result)"
367366
]
368367
},
369368
{
@@ -445,7 +444,7 @@
445444
")\n",
446445
"\n",
447446
"result = await attack.execute_async(objective=\"This is a simple test string for Word document generation.\") # type: ignore\n",
448-
"await ConsoleAttackResultPrinter().print_conversation_async(result=result) # type: ignore"
447+
"await output_attack_async(result)"
449448
]
450449
},
451450
{
@@ -529,7 +528,7 @@
529528
")\n",
530529
"\n",
531530
"result = await attack.execute_async(objective=\"AI Red Team Engineer\") # type: ignore\n",
532-
"await ConsoleAttackResultPrinter().print_conversation_async(result=result) # type: ignore"
531+
"await output_attack_async(result)"
533532
]
534533
}
535534
],

0 commit comments

Comments
 (0)