FIX Handle structured refusals and scenario partial failures - #2283
Merged
romanlutz merged 16 commits intoJul 29, 2026
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a008124c-220e-491c-9ea1-2144867ba53d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a008124c-220e-491c-9ea1-2144867ba53d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a008124c-220e-491c-9ea1-2144867ba53d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a008124c-220e-491c-9ea1-2144867ba53d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a008124c-220e-491c-9ea1-2144867ba53d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a008124c-220e-491c-9ea1-2144867ba53d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a008124c-220e-491c-9ea1-2144867ba53d
Contributor
There was a problem hiding this comment.
Pull request overview
Handles structured model refusals as scorable responses and improves scenario failure-state reporting.
Changes:
- Parses and preserves OpenAI structured refusals.
- Validates true/false scorer output within retry handling.
- Adds typed scenario states and partial-failure details.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
pyrit/backend/services/scenario_run_service.py |
Uses typed run states. |
pyrit/exceptions/__init__.py |
Exports partial-failure exception. |
pyrit/exceptions/exception_classes.py |
Defines structured partial failures. |
pyrit/memory/memory_interface.py |
Persists typed run states. |
pyrit/models/messages/message_piece.py |
Records structured-refusal metadata. |
pyrit/prompt_target/common/chat_completions_response_parser.py |
Parses Chat Completions refusals. |
pyrit/prompt_target/openai/openai_response_target.py |
Parses and serializes Responses API refusals. |
pyrit/scenario/core/scenario.py |
Corrects retries and terminal states. |
pyrit/score/response_handler.py |
Adds true/false domain validation. |
pyrit/score/scorer.py |
Makes refusal explanations scoreable. |
pyrit/score/true_false/self_ask_general_true_false_scorer.py |
Applies domain-validating handler. |
pyrit/score/true_false/self_ask_refusal_scorer.py |
Applies domain-validating handler. |
pyrit/score/true_false/self_ask_true_false_scorer.py |
Applies domain-validating handler. |
tests/unit/backend/test_scenario_run_service.py |
Tests typed service boundaries. |
tests/unit/memory/memory_interface/test_interface_scenario_results.py |
Tests state persistence and cleanup. |
tests/unit/prompt_target/target/test_normalize_async_integration.py |
Updates typed Responses fixtures. |
tests/unit/prompt_target/target/test_openai_chat_target.py |
Tests Chat refusal parsing. |
tests/unit/prompt_target/target/test_openai_response_target.py |
Tests refusal lifecycle behavior. |
tests/unit/prompt_target/target/test_openai_response_target_function_chaining.py |
Updates typed output fixtures. |
tests/unit/scenario/core/test_scenario.py |
Tests empty-run failure state. |
tests/unit/scenario/core/test_scenario_partial_results.py |
Tests retry and partial failures. |
tests/unit/scenario/core/test_scenario_refusals.py |
Adds cross-layer refusal scenarios. |
tests/unit/score/test_response_handler.py |
Tests response-domain validation. |
tests/unit/score/test_scorer.py |
Tests refusal scoring behavior. |
tests/unit/score/test_self_ask_refusal.py |
Tests semantic retry handling. |
spencrr
approved these changes
Jul 28, 2026
jsong468
approved these changes
Jul 29, 2026
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a008124c-220e-491c-9ea1-2144867ba53d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a008124c-220e-491c-9ea1-2144867ba53d
hannahwestra25
approved these changes
Jul 29, 2026
behnam-o
approved these changes
Jul 29, 2026
behnam-o
left a comment
Contributor
There was a problem hiding this comment.
looks good. Just some nits
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a008124c-220e-491c-9ea1-2144867ba53d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a008124c-220e-491c-9ea1-2144867ba53d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a008124c-220e-491c-9ea1-2144867ba53d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a008124c-220e-491c-9ea1-2144867ba53d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a008124c-220e-491c-9ea1-2144867ba53d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a008124c-220e-491c-9ea1-2144867ba53d
romanlutz
pushed a commit
that referenced
this pull request
Jul 30, 2026
PR #2283 made `OpenAIResponseTarget` sort reasoning pieces after the actionable response pieces, so for reasoning models the answer moved from `message_pieces[1]` to `message_pieces[0]`. The GPT-5 Responses integration tests and the paired Responses doc notebook still indexed position 1 and therefore validated the reasoning blob instead of the answer. Select the piece via `Message.get_piece_by_type(data_type="text")` so the assertions no longer depend on piece ordering, while still asserting that a reasoning piece is present. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a008124c-220e-491c-9ea1-2144867ba53d (cherry picked from commit 782eb1a)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
OpenAI Responses and Chat Completions can return structured refusals without ordinary text content. PyRIT treated those valid model responses as parser/runtime failures, which left attack objectives incomplete and could surface a misleading scenario-level
ValueError. Separately, syntactically valid scorer JSON with an out-of-domain true/false value failed only after the JSON retry boundary.This change:
ScenarioPartialFailureExceptionwith structured counts, incomplete objectives, and the original cause for genuine partial execution;ScenarioRunStateat Python boundaries while retaining string persistence at the ORM boundary; and"refusal"raiseInvalidJsonExceptionand participate in the existing clean-history retry flow.The partial-failure exception remains compatible with callers that catch
ValueError. Genuine transport and runtime failures are still incomplete failures rather than success-shaped refusal results. Valid JSON booleans and string booleans remain supported; arbitrary labels are not coerced.N/A - this change is not intended to be breaking.
Tests and Documentation
Added unit and cross-layer scenario regressions for:
ResponseOutputRefusaland Chat Completionsmessage.refusalparsing;Targeted affected suites passed, including 73 true/false scorer tests and 68 refusal/scenario tests. Ruff, Ruff format, and
tychecks passed. The original structured-refusal notebook reproduction also completed without the parser or partial-scenario failure signature.Documentation: N/A - no public API usage or documentation examples changed.
JupyText: N/A - no notebooks or documentation code samples changed.