Skip to content

FIX Handle structured refusals and scenario partial failures - #2283

Merged
romanlutz merged 16 commits into
microsoft:mainfrom
romanlutz:romanlutz-handle-response-refusals
Jul 29, 2026
Merged

FIX Handle structured refusals and scenario partial failures#2283
romanlutz merged 16 commits into
microsoft:mainfrom
romanlutz:romanlutz-handle-response-refusals

Conversation

@romanlutz

Copy link
Copy Markdown
Contributor

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:

  • parses Responses API and Chat Completions structured refusals into persistable, scorable model responses while preserving explicit blocked/refusal semantics;
  • keeps refusal explanations available as response content so refusal scorers can evaluate them and atomic attacks can complete normally;
  • introduces ScenarioPartialFailureException with structured counts, incomplete objectives, and the original cause for genuine partial execution;
  • corrects scenario retry and terminal run-state transitions, using ScenarioRunState at Python boundaries while retaining string persistence at the ORM boundary; and
  • validates true/false scorer values inside response parsing so labels such as "refusal" raise InvalidJsonException and 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:

  • Responses API ResponseOutputRefusal and Chat Completions message.refusal parsing;
  • all-refusal, mixed refusal/text, and refusal plus genuine runtime-failure scenario runs;
  • partial-result exception context, cause preservation, retry states, and typed run-state boundaries; and
  • true/false JSON booleans, string booleans, invalid semantic values, retry recovery, and retry exhaustion.

Targeted affected suites passed, including 73 true/false scorer tests and 68 refusal/scenario tests. Ruff, Ruff format, and ty checks 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.

Copilot AI added 7 commits July 27, 2026 18:42
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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread pyrit/score/scorer.py Outdated
Comment thread pyrit/models/messages/message_piece.py Outdated
Comment thread pyrit/prompt_target/openai/openai_response_target.py
Comment thread tests/unit/prompt_target/target/test_openai_chat_target.py
Copilot AI and others added 3 commits July 29, 2026 06:18
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
Comment thread pyrit/scenario/core/scenario.py
Comment thread pyrit/prompt_target/common/chat_completions_response_parser.py Outdated
Comment thread pyrit/memory/memory_interface.py Outdated
Comment thread pyrit/score/true_false/self_ask_true_false_scorer.py

@behnam-o behnam-o left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good. Just some nits

Comment thread pyrit/memory/memory_interface.py Outdated
Comment thread pyrit/memory/memory_interface.py Outdated
Comment thread pyrit/prompt_target/openai/openai_response_target.py Outdated
Copilot AI added 6 commits July 29, 2026 09:52
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
romanlutz added this pull request to the merge queue Jul 29, 2026
Merged via the queue into microsoft:main with commit 630061a Jul 29, 2026
54 checks passed
@romanlutz
romanlutz deleted the romanlutz-handle-response-refusals branch July 29, 2026 23:25
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants