MAINT: Refactor printers into lightweight and flexible output module#1732
MAINT: Refactor printers into lightweight and flexible output module#1732rlundeen2 wants to merge 34 commits into
Conversation
Create new pyrit/printer/ module with abstract base classes that contain all formatting logic. Data-fetching operations (CentralMemory calls) are abstract methods implemented by framework subclasses. This enables thin clients to reuse all pretty-printing by subclassing the base printers and implementing data-fetching via REST endpoints. The thin client only needs pyrit.models + pyrit.identifiers + colorama. Changes: - New pyrit/printer/ module with attack_result, scenario_result, scorer subpackages - ConsoleAttackPrinterBase: all attack console formatting, abstract get_conversation/get_scores - ConsoleScenarioPrinterBase: all scenario console formatting - ConsoleScorerPrinterBase: all scorer formatting, abstract get_objective/harm_metrics - Existing framework printers refactored to thin subclasses (backward compatible) - Added to_dict()/from_dict() to AttackResult, ScenarioResult, ScenarioIdentifier, ConversationReference, Score, MessagePiece, Message for serialization round-tripping - Message.to_full_dict() added for rich serialization (to_dict() unchanged for compat) All 675 existing tests pass with no modifications. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move framework CentralMemory implementations into pyrit/printer/ alongside their base classes. CentralMemory is imported lazily inside constructors, so thin clients importing the module never pay the SQLAlchemy cost. - ConsoleAttackResultPrinter now lives in pyrit.printer.attack_result.console - ConsoleScenarioResultPrinter now lives in pyrit.printer.scenario_result.console - ConsoleScorerPrinter now lives in pyrit.printer.scorer.console - Old locations (executor/attack/printer/, scenario/printer/, score/printer/) become pure re-exports for backward compatibility - Updated test patch paths to match new module locations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…6.0) Old locations now use PEP 562 __getattr__ lazy re-exports with DeprecationWarning. Only concrete classes are re-exported (not bases). - pyrit.executor.attack.printer → pyrit.printer.attack_result - pyrit.scenario.printer → pyrit.printer.scenario_result - pyrit.score.printer → pyrit.printer.scorer - Updated all internal callers to new canonical paths - Old ABC files (attack_result_printer.py, scenario_result_printer.py, scorer_printer.py) kept for now but deprecated via __init__.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…of bases Concrete classes that use CentralMemory/scorer registry are now named to clearly indicate their data source: - ConsoleAttackResultPrinter → ConsoleAttackMemoryPrinter - ConsoleScenarioResultPrinter → ConsoleScenarioMemoryPrinter - ConsoleScorerPrinter → ConsoleScorerMemoryPrinter Moved ScorerEvaluationIdentifier (pyrit internal) from base class into the concrete ConsoleScorerMemoryPrinter. Base classes now contain only formatting logic with no pyrit-internal imports beyond models/identifiers. Deprecated re-exports at old paths still work (mapping old names to new), scheduled for removal in 0.16.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Created MarkdownAttackPrinterBase + MarkdownAttackMemoryPrinter in pyrit/printer/attack_result/markdown.py (same pattern as console) - Deleted dead old ABC files: - pyrit/executor/attack/printer/attack_result_printer.py - pyrit/scenario/printer/scenario_result_printer.py - pyrit/score/printer/scorer_printer.py - Old markdown_printer.py now a deprecation re-export shim - Updated all internal imports and test patches Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Changed dict[str, object] to dict[str, Any] in MessagePiece.from_dict() and Message.from_dict() to satisfy pyright (dict.get returns object otherwise) - Added Any import to message_piece.py and message.py - Wrapped get_prompt_scores return in list() for Sequence -> list coercion - Added isinstance check in display_image_async for type safety Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Added return type annotation (-> type) to all __getattr__ deprecation shims - Added noqa: B027 to display_image_async intentional no-op default - Added Returns/Raises sections to short docstrings (DOC201, DOC501) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
objective_target_identifier and objective_scorer_identifier may be None when deserializing from dicts. The printer bases already handle None. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…n warnings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nversation/score printers, refactor display_image_response Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| "prompt_shield_scorer.py", # requires Azure Content Safety API key | ||
| # targets | ||
| "4_non_llm_targets.py", # requires Azure SQL Storage IO for Azure Storage Account (see #4001) | ||
| "4_openai_video_target.py", # requires OpenAI video API key |
There was a problem hiding this comment.
matched integration tests for now. I want to update and include more
| from pyrit.output.sink import OutputFormat, Sink, StdoutSink, get_default_sink | ||
|
|
||
|
|
||
| async def print_attack_result_async( |
There was a problem hiding this comment.
It might be worth updating these to output_attack_result_async. But before doing it would be good to get a thumbs up :)
| format: OutputFormat = "pretty", # noqa: A002 | ||
| sink: Sink | None = None, | ||
| include_auxiliary_scores: bool = False, | ||
| include_pruned_conversations: bool = False, |
There was a problem hiding this comment.
eventually these should also include the source; but right now the only source is "memory"
| from pyrit.output.sink import OutputFormat, Sink, StdoutSink, get_default_sink | ||
|
|
||
|
|
||
| async def print_attack_result_async( |
There was a problem hiding this comment.
nit: this is a bit of a mouth full. I wonder if there's a way to just do print_async and it figures out which one based on what you pass? Maybe that's a bit ambitious....
Also, output_async since it's not necessarily printed?
There was a problem hiding this comment.
I was thinking the same thing re: output.
I think figuring out which one will be tricky because some of the arguments are different, and I think it's nice to know what they are (e.g. attack_result has "include_summary"). Potentially in the future, but I think that makes it more confusing than helpful
| Defaults to False. | ||
| """ | ||
| if format == "markdown": | ||
| from pyrit.output.attack_result.markdown import MarkdownAttackResultMemoryPrinter |
There was a problem hiding this comment.
Are these imports down here to avoid loading unnecessary things or by accident?
There was a problem hiding this comment.
yes, but I should add a comment to this so it's clear as to why.
In the CLI, it doesn't want to import memory, in theory it will get the data from the REST API (I think).
Introduce pyrit/output/ module — format × sink separation for all result rendering
Adds a new outputmodule that decouples what output looks like (pretty ANSI, markdown, JSON) from where it goes
(stdout, file, Jupyter, etc.) and where data comes from (CentralMemory, REST, test fixtures).
Sink(StdoutSink, FileSink, IPythonMarkdownSink) — pluggable output destinationsPrinterBasewithrender_async()→str(abstract) andwrite_async()→None(concrete, routes through sink)await print_attack_result_async(result, format="markdown", sink=FileSink(...))What this replaces: scattered print() calls and format-fused rendering logic across executors and models. The same
printer classes work against memory, REST endpoints, or test fixtures by swapping only the leaf data-fetching layer
All functionality is the same. you can even call the methods imported using the same arguments with the same behavior (except a deprecation message); but this allows us to use the same printing once we replace our cli frontend to use the REST API and more consistently add to output modules
TODO