Skip to content
4 changes: 2 additions & 2 deletions .github/instructions/converters.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ from pyrit.identifiers import ComponentIdentifier

For LLM-based converters, also import:
```python
from pyrit.prompt_target import PromptChatTarget
from pyrit.prompt_target import PromptTarget
```

## Constructor Pattern
Expand All @@ -77,7 +77,7 @@ from pyrit.common.apply_defaults import apply_defaults

class MyConverter(PromptConverter):
@apply_defaults
def __init__(self, *, target: PromptChatTarget, template: str = "default") -> None:
def __init__(self, *, target: PromptTarget, template: str = "default") -> None:
...
```

Expand Down
2 changes: 1 addition & 1 deletion .github/instructions/scenarios.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class MyScenario(Scenario):
def __init__(
self,
*,
adversarial_chat: PromptChatTarget | None = None,
adversarial_chat: PromptTarget | None = None,
objective_scorer: TrueFalseScorer | None = None,
scenario_result_id: str | None = None,
) -> None:
Expand Down
4 changes: 2 additions & 2 deletions .github/instructions/style-guide.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ In the same module, importing from the specific path is usually necessary to pre

```python
# Correct
from pyrit.prompt_target import PromptChatTarget, OpenAIChatTarget
from pyrit.prompt_target import PromptTarget, OpenAIChatTarget

# Correct
from pyrit.score import (
Expand All @@ -263,7 +263,7 @@ from pyrit.score import (
)

# Incorrect (if importing from a non-target module)
from pyrit.prompt_target.common.prompt_chat_target import PromptChatTarget
from pyrit.prompt_target.common.prompt_target import PromptTarget
from pyrit.prompt_target.openai.openai_chat_target import OpenAIChatTarget

```
Expand Down
2 changes: 1 addition & 1 deletion doc/code/executor/attack/2_red_teaming_attack.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -1546,7 +1546,7 @@
"source": [
"## Other Multi-Turn Attacks\n",
"\n",
"The above examples should work using other multi-turn attacks with minimal modification. Check out attacks under `pyrit.executor.attack.multi_turn` for other examples, like Crescendo and Tree of Attacks. These algorithms are always more effective than `RedTeamingAttack`, which is a simple algorithm. However, `RedTeamingAttack` by its nature supports more targets - because it doesn't modify conversation history it can support any `PromptTarget` and not only `PromptChatTargets`."
"The above examples should work using other multi-turn attacks with minimal modification. Check out attacks under `pyrit.executor.attack.multi_turn` for other examples, like Crescendo and Tree of Attacks. These algorithms are always more effective than `RedTeamingAttack`, which is a simple algorithm. However, `RedTeamingAttack` by its nature supports more targets - because it doesn't modify conversation history it can support any `PromptTarget` even if `supports_editable_history` is false"
]
}
],
Expand Down
2 changes: 1 addition & 1 deletion doc/code/executor/attack/2_red_teaming_attack.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,4 +328,4 @@
# %% [markdown]
# ## Other Multi-Turn Attacks
#
# The above examples should work using other multi-turn attacks with minimal modification. Check out attacks under `pyrit.executor.attack.multi_turn` for other examples, like Crescendo and Tree of Attacks. These algorithms are always more effective than `RedTeamingAttack`, which is a simple algorithm. However, `RedTeamingAttack` by its nature supports more targets - because it doesn't modify conversation history it can support any `PromptTarget` and not only `PromptChatTargets`.
# The above examples should work using other multi-turn attacks with minimal modification. Check out attacks under `pyrit.executor.attack.multi_turn` for other examples, like Crescendo and Tree of Attacks. These algorithms are always more effective than `RedTeamingAttack`, which is a simple algorithm. However, `RedTeamingAttack` by its nature supports more targets - because it doesn't modify conversation history it can support any `PromptTarget` even if `supports_editable_history` is false
2 changes: 1 addition & 1 deletion doc/code/setup/default_values.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ from pyrit.common.apply_defaults import apply_defaults

class MyConverter(PromptConverter):
@apply_defaults
def __init__(self, *, converter_target: Optional[PromptChatTarget] = None, temperature: Optional[float] = None):
def __init__(self, *, converter_target: Optional[PromptTarget] = None, temperature: Optional[float] = None):
self.converter_target = converter_target
self.temperature = temperature
```
Expand Down
29 changes: 20 additions & 9 deletions doc/code/targets/0_prompt_targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,31 @@ async def send_prompt_async(self, *, message: Message) -> Message:

A `Message` object is a normalized object with all the information a target will need to send a prompt, including a way to get a history for that prompt (in the cases that also needs to be sent). This is discussed in more depth [here](../memory/3_memory_data_types.md).

## PromptChatTargets vs PromptTargets
## Target Capabilities

A `PromptTarget` is a generic place to send a prompt. With PyRIT, the idea is that it will eventually be consumed by an AI application, but that doesn't have to be immediate. For example, you could have a SharePoint target. Everything you send a prompt to is a `PromptTarget`. Many attacks work generically with any `PromptTarget` including `RedTeamingAttack` and `PromptSendingAttack`.
Every `PromptTarget` declares a `TargetCapabilities` object that describes what the target supports. Attacks, scorers, and converters use these flags to validate that a target is compatible before execution, raising a clear error at construction time rather than failing mid-run.

With some algorithms, you want to send a prompt, set a system prompt, and modify conversation history (including PAIR [@chao2023pair], TAP [@mehrotra2023tap], and flip attack [@li2024flipattack]). These often require a `PromptChatTarget`, which implies you can modify a conversation history. `PromptChatTarget` is a subclass of `PromptTarget`.
| Capability | Type | Description |
|---|---|---|
| `supports_multi_turn` | `bool` | Target accepts conversation history across multiple turns. Required by multi-turn attacks (e.g., PAIR, TAP, Crescendo). |
| `supports_editable_history` | `bool` | Target allows prepended conversation history to be injected into memory. Required by attacks that seed a conversation before starting (e.g., TAP, FlipAttack, ContextCompliance). |
| `supports_multi_message_pieces` | `bool` | Target accepts a single request with multiple pieces (e.g., text + image in one turn). |
| `supports_json_output` | `bool` | Target can be instructed to return valid JSON (e.g., via a `response_format` parameter). |
| `supports_json_schema` | `bool` | Target can constrain output to a specific JSON schema. |
| `input_modalities` | `frozenset` | The combinations of data types the target accepts as input (e.g., `{"text"}`, `{"text", "image_path"}`). |
| `output_modalities` | `frozenset` | The data types the target can produce as output (e.g., `{"text"}`, `{"audio_path"}`). |

Capabilities are defined at the class level via `_DEFAULT_CAPABILITIES` and can be overridden per instance using the `custom_capabilities` constructor parameter. This is useful for targets like `HTTPTarget` or `PlaywrightTarget` where capabilities depend on the specific deployment being wrapped.

Here are some examples:

| Example | Is `PromptChatTarget`? | Notes |
|-------------------------------------|---------------------------------------|-------------------------------------------------------------------------------------------------|
| **OpenAIChatTarget** (e.g., GPT-4) | **Yes** (`PromptChatTarget`) | Designed for conversational prompts (system messages, conversation history, etc.). |
| **OpenAIImageTarget** | **No** (not a `PromptChatTarget`) | Used for image generation; does not manage conversation history. |
| **HTTPTarget** | **No** (not a `PromptChatTarget`) | Generic HTTP target. Some apps might allow conversation history, but this target doesn't handle it. |
| **AzureBlobStorageTarget** | **No** (not a `PromptChatTarget`) | Used primarily for storage; not for conversation-based AI. |
| Example | `supports_multi_turn` | `supports_editable_history` | Notes |
|---|---|---|---|
| **OpenAIChatTarget** | Yes | Yes | Full chat target; supports multi-turn and injected history. |
| **OpenAIImageTarget** | No | No | Image generation; single-turn only. |
| **OpenAITTSTarget** | No | No | Text-to-speech; single-turn only. |
| **HTTPTarget** | No (default) | No (default) | Configurable via `custom_capabilities` if the wrapped app supports it. |
| **AzureBlobStorageTarget** | No | No | Storage target; not conversational. |

## Multi-Modal Targets

Expand Down
2 changes: 1 addition & 1 deletion doc/code/targets/10_3_websocket_copilot_target.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
"\n",
"The `WebSocketCopilotTarget` supports multi-turn conversations by leveraging Copilot's server-side conversation management. It automatically generates consistent `session_id` and `conversation_id` values for each PyRIT conversation, enabling Copilot to maintain context across multiple turns.\n",
"\n",
"However, this target does not support setting a system prompt nor modifying conversation history. As a result, it cannot be used with attack strategies that require altering prior messages (such as PAIR, TAP, or flip attack) or in contexts where a `PromptChatTarget` is required.\n",
"However, this target does not support setting a system prompt nor modifying conversation history. As a result, it cannot be used with attack strategies that require altering prior messages (such as PAIR, TAP, or flip attack) or in contexts where a `PromptTarget` which supports editable history and mulit turn conversations is required.\n",
"\n",
"Here is a simple multi-turn conversation example:"
]
Expand Down
2 changes: 1 addition & 1 deletion doc/code/targets/10_3_websocket_copilot_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
#
# The `WebSocketCopilotTarget` supports multi-turn conversations by leveraging Copilot's server-side conversation management. It automatically generates consistent `session_id` and `conversation_id` values for each PyRIT conversation, enabling Copilot to maintain context across multiple turns.
#
# However, this target does not support setting a system prompt nor modifying conversation history. As a result, it cannot be used with attack strategies that require altering prior messages (such as PAIR, TAP, or flip attack) or in contexts where a `PromptChatTarget` is required.
# However, this target does not support setting a system prompt nor modifying conversation history. As a result, it cannot be used with attack strategies that require altering prior messages (such as PAIR, TAP, or flip attack) or in contexts where a `PromptTarget` which supports editable history and multi turn conversations is required.
#
# Here is a simple multi-turn conversation example:

Expand Down
5 changes: 3 additions & 2 deletions doc/code/targets/8_non_llm_targets.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"Prompt Targets are most often LLMs, but not always. They should be thought of as anything that you send prompts to.\n",
"\n",
"\n",
"The `AzureBlobStorageTarget` inherits from `PromptTarget`, meaning it has functionality to send prompts. In contrast to `PromptChatTarget`s, `PromptTarget`s do not interact with chat assistants.\n",
"The `AzureBlobStorageTarget` inherits from `PromptTarget`, meaning it has functionality to send prompts. It does not have multi-turn conversation capabilities.\n",
"\n",
"This prompt target in particular will take in a prompt and upload it as a text file to the provided Azure Storage Account Container.\n",
"This could be useful for Cross-Prompt Injection Attack scenarios, for example, where there is a jailbreak within a file.\n",
"\n",
Expand Down Expand Up @@ -81,7 +82,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.11"
"version": "3.11.15"
}
},
"nbformat": 4,
Expand Down
2 changes: 1 addition & 1 deletion doc/code/targets/8_non_llm_targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# Prompt Targets are most often LLMs, but not always. They should be thought of as anything that you send prompts to.
#
#
# The `AzureBlobStorageTarget` inherits from `PromptTarget`, meaning it has functionality to send prompts. In contrast to `PromptChatTarget`s, `PromptTarget`s do not interact with chat assistants.
# The `AzureBlobStorageTarget` inherits from `PromptTarget`, meaning it has functionality to send prompts. It does not have multi-turn conversation capabilities.
# This prompt target in particular will take in a prompt and upload it as a text file to the provided Azure Storage Account Container.
# This could be useful for Cross-Prompt Injection Attack scenarios, for example, where there is a jailbreak within a file.
#
Expand Down
2 changes: 1 addition & 1 deletion doc/cookbooks/2_precomputing_turns.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"\n",
"Here is a scenario; you want to use a powerful attack technique like `Crescendo` [@russinovich2024crescendo] or `TAP` [@mehrotra2023tap]. That's great! These are the most successful attacks in our arsenal. But there's a catch. They are slow.\n",
"\n",
"One way to speed these up is to generate the first N turns in advance, and start these algorithms on a later turn. This is possible on any target where you can modify prompt history (any PromptChatTarget). And it can be extremely useful if you want to test a new model after having tested an old one.\n",
"One way to speed these up is to generate the first N turns in advance, and start these algorithms on a later turn. This is possible on any target where you can modify prompt history (any PromptTarget that supports editable history). And it can be extremely useful if you want to test a new model after having tested an old one.\n",
"\n",
"This cookbook (like all cookbooks in our docs) takes you step by step, tackling this problem using our best practices and in a way that's the most generic. Sometimes there are issues we want to solve, but haven't yet, and we try to note those and we'll try to keep this up to date as we improve. Comments are added around the pieces you may want to configure as you adapt to your scenario.\n",
"\n",
Expand Down
2 changes: 1 addition & 1 deletion doc/cookbooks/2_precomputing_turns.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
#
# Here is a scenario; you want to use a powerful attack technique like `Crescendo` [@russinovich2024crescendo] or `TAP` [@mehrotra2023tap]. That's great! These are the most successful attacks in our arsenal. But there's a catch. They are slow.
#
# One way to speed these up is to generate the first N turns in advance, and start these algorithms on a later turn. This is possible on any target where you can modify prompt history (any PromptChatTarget). And it can be extremely useful if you want to test a new model after having tested an old one.
# One way to speed these up is to generate the first N turns in advance, and start these algorithms on a later turn. This is possible on any target where you can modify prompt history (any PromptTarget that supports editable history). And it can be extremely useful if you want to test a new model after having tested an old one.
#
# This cookbook (like all cookbooks in our docs) takes you step by step, tackling this problem using our best practices and in a way that's the most generic. Sometimes there are issues we want to solve, but haven't yet, and we try to note those and we'll try to keep this up to date as we improve. Comments are added around the pieces you may want to configure as you adapt to your scenario.
#
Expand Down
33 changes: 22 additions & 11 deletions pyrit/executor/attack/component/conversation_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Optional

from pyrit.common.deprecation import print_deprecation_message
from pyrit.common.utils import combine_dict
from pyrit.executor.attack.component.prepended_conversation_config import (
PrependedConversationConfig,
Expand All @@ -20,7 +21,6 @@
)
from pyrit.prompt_normalizer.prompt_normalizer import PromptNormalizer
from pyrit.prompt_target import PromptTarget
from pyrit.prompt_target.common.prompt_chat_target import PromptChatTarget

if TYPE_CHECKING:
from pyrit.executor.attack.core import AttackContext
Expand Down Expand Up @@ -242,21 +242,31 @@ def get_last_message(
def set_system_prompt(
self,
*,
target: PromptChatTarget,
target: PromptTarget,
conversation_id: str,
system_prompt: str,
labels: Optional[dict[str, str]] = None,
) -> None:
"""
Set or update the system prompt for a conversation.

.. deprecated:: 0.14.0
Use ``prepended_conversation`` on the attack context instead. Pass a
``Message.from_system_prompt(system_prompt)`` as the first element of
``AttackParameters.prepended_conversation``. This method will be removed in 0.14.0.

Args:
target: The chat target to set the system prompt on.
conversation_id: Unique identifier for the conversation.
system_prompt: The system prompt text.
labels: Optional labels to associate with the system prompt.
"""
target.set_system_prompt(
print_deprecation_message(
old_item="ConversationManager.set_system_prompt",
new_item="AttackParameters.prepended_conversation",
removed_in="0.14.0",
)
target._set_target_system_prompt(
Copy link
Copy Markdown
Contributor

@rlundeen2 rlundeen2 Mar 24, 2026

Choose a reason for hiding this comment

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

The one thing I want to check is that this flow still works in the few places you can have a system prompt but are not editable_history, like realtime_target.

Because prepended_conversation is essentially how we edit the conversation. But then we also need a way to set the system prompt for that target specifically.

At the same time, we've had these really nasty bugs where attacks will call set_system_prompt on a target and it messes everything up. So I LIKE that we're setting it via conversation history. But we just need to figure out these special cases.

system_prompt=system_prompt,
conversation_id=conversation_id,
attack_identifier=self._attack_identifier,
Expand All @@ -283,11 +293,11 @@ async def initialize_context_async(
3. Updates context.executed_turns for multi-turn attacks
4. Sets context.next_message if there's an unanswered user message

For PromptChatTarget:
For PromptTarget that support editable conversation history & multi-turn conversations:
- Adds prepended messages to memory with simulated_assistant role
- All messages get new UUIDs

For non-chat PromptTarget:
For PromptTarget that do NOT support editable conversation history & multi-turn conversations:
- If `config.non_chat_target_behavior="normalize_first_turn"`: normalizes
conversation to string and prepends to context.next_message
- If `config.non_chat_target_behavior="raise"`: raises ValueError
Expand All @@ -305,8 +315,8 @@ async def initialize_context_async(
ConversationState with turn_count and last_assistant_message_scores.

Raises:
ValueError: If conversation_id is empty, or if prepended_conversation
requires a PromptChatTarget but target is not one.
ValueError: If conversation_id is empty, or if prepended_conversation requires a target that supports
multi-turn conversations & editable history but target does not support these capabilities.
"""
if not conversation_id:
raise ValueError("conversation_id cannot be empty")
Expand All @@ -322,7 +332,7 @@ async def initialize_context_async(
return state

# Handle target type compatibility
is_chat_target = isinstance(target, PromptChatTarget)
is_chat_target = target.capabilities.supports_multi_turn and target.capabilities.supports_editable_history
if not is_chat_target:
return await self._handle_non_chat_target_async(
context=context,
Expand All @@ -348,7 +358,7 @@ async def _handle_non_chat_target_async(
config: Optional["PrependedConversationConfig"],
) -> ConversationState:
"""
Handle prepended conversation for non-chat targets.
Handle prepended conversation for targets that don't support conversation management.

Args:
context: The attack context.
Expand All @@ -366,8 +376,9 @@ async def _handle_non_chat_target_async(

if config.non_chat_target_behavior == "raise":
raise ValueError(
"prepended_conversation requires the objective target to be a PromptChatTarget. "
"Non-chat objective targets do not support conversation history. "
"prepended_conversation requires the objective target to be a PromptTarget "
"that supports multi-turn conversations & editable history. "
"Non-chat targets do not support conversation history. "
"Use PrependedConversationConfig with non_chat_target_behavior='normalize_first_turn' "
"to normalize the conversation into the first message instead."
)
Expand Down
Loading
Loading