Skip to content

Commit e635ee9

Browse files
FEAT Migrate consumers to TargetConfiguration capability checks (#1645)
1 parent 7065339 commit e635ee9

83 files changed

Lines changed: 1006 additions & 572 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.

doc/code/targets/1_openai_chat_target.ipynb

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,27 @@
2222
"name": "stdout",
2323
"output_type": "stream",
2424
"text": [
25-
"Found default environment files: ['./.pyrit/.env']\n",
26-
"Loaded environment file: ./.pyrit/.env\n",
25+
"Found default environment files: ['./.pyrit/.env']\n"
26+
]
27+
},
28+
{
29+
"name": "stdout",
30+
"output_type": "stream",
31+
"text": [
32+
"Loaded environment file: ./.pyrit/.env\n"
33+
]
34+
},
35+
{
36+
"name": "stdout",
37+
"output_type": "stream",
38+
"text": [
39+
"No new upgrade operations detected.\n"
40+
]
41+
},
42+
{
43+
"name": "stdout",
44+
"output_type": "stream",
45+
"text": [
2746
"\n",
2847
"\u001b[34m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
2948
"\u001b[1m\u001b[34m🔹 Turn 1 - USER\u001b[0m\n",
@@ -108,7 +127,13 @@
108127
"output_type": "stream",
109128
"text": [
110129
"Found default environment files: ['./.pyrit/.env']\n",
111-
"Loaded environment file: ./.pyrit/.env\n",
130+
"Loaded environment file: ./.pyrit/.env\n"
131+
]
132+
},
133+
{
134+
"name": "stdout",
135+
"output_type": "stream",
136+
"text": [
112137
"{\n",
113138
" \"name\": \"Bob\",\n",
114139
" \"age\": 32\n",
@@ -191,7 +216,13 @@
191216
"output_type": "stream",
192217
"text": [
193218
"Found default environment files: ['./.pyrit/.env']\n",
194-
"Loaded environment file: ./.pyrit/.env\n",
219+
"Loaded environment file: ./.pyrit/.env\n"
220+
]
221+
},
222+
{
223+
"name": "stdout",
224+
"output_type": "stream",
225+
"text": [
195226
"\n",
196227
"\u001b[34m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
197228
"\u001b[1m\u001b[34m🔹 Turn 1 - USER\u001b[0m\n",
@@ -218,33 +249,31 @@
218249
"\u001b[33m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
219250
"\u001b[1m\u001b[33m🔸 ASSISTANT\u001b[0m\n",
220251
"\u001b[33m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
221-
"\u001b[33m This image is a labeled table titled \"PyRIT Components.\" It organizes information under two main\u001b[0m\n",
222-
"\u001b[33m sections: **Interface** and **Implementation**, which are further divided into five subsections:\u001b[0m\n",
223-
"\u001b[33m **Target**, **Datasets**, **Scoring Engine**, **Attack Strategy**, and **Memory**.\u001b[0m\n",
252+
"\u001b[33m This image displays a structure chart titled **\"PyRIT Components\"**. It outlines the components of\u001b[0m\n",
253+
"\u001b[33m the PyRIT system with two columns: **Interface** and **Implementation**. Each row contains\u001b[0m\n",
254+
"\u001b[33m different elements organized into categories. The categories are:\u001b[0m\n",
224255
"\u001b[33m \u001b[0m\n",
225-
"\u001b[33m ### Description of the Components:\u001b[0m\n",
226-
"\u001b[33m 1. **Target:**\u001b[0m\n",
256+
"\u001b[33m 1. **Target**:\u001b[0m\n",
227257
"\u001b[33m - **Local**: Local model (e.g., ONNX).\u001b[0m\n",
228258
"\u001b[33m - **Remote**: API or web app.\u001b[0m\n",
229259
"\u001b[33m \u001b[0m\n",
230-
"\u001b[33m 2. **Datasets:**\u001b[0m\n",
260+
"\u001b[33m 2. **Datasets**:\u001b[0m\n",
231261
"\u001b[33m - **Static**: Prompts.\u001b[0m\n",
232262
"\u001b[33m - **Dynamic**: Prompt templates.\u001b[0m\n",
233263
"\u001b[33m \u001b[0m\n",
234-
"\u001b[33m 3. **Scoring Engine:**\u001b[0m\n",
235-
"\u001b[33m - **PyRIT Itself**: Self-evaluation.\u001b[0m\n",
264+
"\u001b[33m 3. **Scoring Engine**:\u001b[0m\n",
265+
"\u001b[33m - **PyRIT Itself**: Self Evaluation.\u001b[0m\n",
236266
"\u001b[33m - **API**: Existing content classifiers.\u001b[0m\n",
237267
"\u001b[33m \u001b[0m\n",
238-
"\u001b[33m 4. **Attack Strategy:**\u001b[0m\n",
268+
"\u001b[33m 4. **Attack Strategy**:\u001b[0m\n",
239269
"\u001b[33m - **Single Turn**: Using static prompts.\u001b[0m\n",
240270
"\u001b[33m - **Multi Turn**: Multiple conversations using prompt templates.\u001b[0m\n",
241271
"\u001b[33m \u001b[0m\n",
242-
"\u001b[33m 5. **Memory:**\u001b[0m\n",
272+
"\u001b[33m 5. **Memory**:\u001b[0m\n",
243273
"\u001b[33m - **Storage**: JSON, Database.\u001b[0m\n",
244274
"\u001b[33m - **Utils**: Conversation, retrieval and storage, memory sharing, and data analysis.\u001b[0m\n",
245275
"\u001b[33m \u001b[0m\n",
246-
"\u001b[33m The layout uses alternating shades of blue to visually distinguish the rows and includes brief\u001b[0m\n",
247-
"\u001b[33m descriptions of each component on the right-hand side of the table.\u001b[0m\n",
276+
"\u001b[33m The design uses shades of blue for differentiation, grouping related elements and categories.\u001b[0m\n",
248277
"\n",
249278
"\u001b[34m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n"
250279
]
@@ -261,7 +290,7 @@
261290
" PromptSendingAttack,\n",
262291
")\n",
263292
"from pyrit.models import SeedGroup, SeedPrompt\n",
264-
"from pyrit.prompt_target import OpenAIChatTarget, TargetCapabilities, TargetConfiguration\n",
293+
"from pyrit.prompt_target import OpenAIChatTarget\n",
265294
"from pyrit.score import SelfAskTrueFalseScorer, TrueFalseQuestion\n",
266295
"from pyrit.setup import IN_MEMORY, initialize_pyrit_async\n",
267296
"\n",
@@ -273,34 +302,12 @@
273302
"chat_target = OpenAIChatTarget(\n",
274303
" endpoint=endpoint,\n",
275304
" api_key=api_key,\n",
276-
" # Override default (text-only) configuration to enable image input, multi-turn, and JSON output for this multi-modal example.\n",
277-
" custom_configuration=TargetConfiguration(\n",
278-
" capabilities=TargetCapabilities(\n",
279-
" supports_multi_turn=True,\n",
280-
" supports_json_output=True,\n",
281-
" supports_multi_message_pieces=True,\n",
282-
" input_modalities=frozenset(\n",
283-
" {frozenset({\"text\", \"image_path\"}), frozenset({\"image_path\"}), frozenset({\"text\"})}\n",
284-
" ),\n",
285-
" )\n",
286-
" ),\n",
287305
")\n",
288306
"\n",
289307
"scorer = SelfAskTrueFalseScorer(\n",
290308
" chat_target=OpenAIChatTarget(\n",
291309
" endpoint=endpoint,\n",
292310
" api_key=api_key,\n",
293-
" # The scorer also needs to read image responses; override configuration to support image input modalities.\n",
294-
" custom_configuration=TargetConfiguration(\n",
295-
" capabilities=TargetCapabilities(\n",
296-
" supports_multi_turn=True,\n",
297-
" supports_json_output=True,\n",
298-
" supports_multi_message_pieces=True,\n",
299-
" input_modalities=frozenset(\n",
300-
" {frozenset({\"text\", \"image_path\"}), frozenset({\"image_path\"}), frozenset({\"text\"})}\n",
301-
" ),\n",
302-
" )\n",
303-
" ),\n",
304311
" ),\n",
305312
" true_false_question=TrueFalseQuestion(\n",
306313
" true_description=\"The response describes the picture as an overview of PyRIT components.\"\n",

doc/code/targets/1_openai_chat_target.py

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@
120120
PromptSendingAttack,
121121
)
122122
from pyrit.models import SeedGroup, SeedPrompt
123-
from pyrit.prompt_target import OpenAIChatTarget, TargetCapabilities, TargetConfiguration
123+
from pyrit.prompt_target import OpenAIChatTarget
124124
from pyrit.score import SelfAskTrueFalseScorer, TrueFalseQuestion
125125
from pyrit.setup import IN_MEMORY, initialize_pyrit_async
126126

@@ -132,34 +132,12 @@
132132
chat_target = OpenAIChatTarget(
133133
endpoint=endpoint,
134134
api_key=api_key,
135-
# Override default (text-only) configuration to enable image input, multi-turn, and JSON output for this multi-modal example.
136-
custom_configuration=TargetConfiguration(
137-
capabilities=TargetCapabilities(
138-
supports_multi_turn=True,
139-
supports_json_output=True,
140-
supports_multi_message_pieces=True,
141-
input_modalities=frozenset(
142-
{frozenset({"text", "image_path"}), frozenset({"image_path"}), frozenset({"text"})}
143-
),
144-
)
145-
),
146135
)
147136

148137
scorer = SelfAskTrueFalseScorer(
149138
chat_target=OpenAIChatTarget(
150139
endpoint=endpoint,
151140
api_key=api_key,
152-
# The scorer also needs to read image responses; override configuration to support image input modalities.
153-
custom_configuration=TargetConfiguration(
154-
capabilities=TargetCapabilities(
155-
supports_multi_turn=True,
156-
supports_json_output=True,
157-
supports_multi_message_pieces=True,
158-
input_modalities=frozenset(
159-
{frozenset({"text", "image_path"}), frozenset({"image_path"}), frozenset({"text"})}
160-
),
161-
)
162-
),
163141
),
164142
true_false_question=TrueFalseQuestion(
165143
true_description="The response describes the picture as an overview of PyRIT components."

doc/code/targets/3_openai_image_target.ipynb

Lines changed: 26 additions & 44 deletions
Large diffs are not rendered by default.

doc/code/targets/3_openai_image_target.py

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
ConsoleAttackResultPrinter,
3131
PromptSendingAttack,
3232
)
33-
from pyrit.prompt_target import OpenAIChatTarget, OpenAIImageTarget, TargetCapabilities, TargetConfiguration
33+
from pyrit.prompt_target import OpenAIChatTarget, OpenAIImageTarget
3434
from pyrit.score import SelfAskTrueFalseScorer, TrueFalseQuestion
3535
from pyrit.setup import IN_MEMORY, initialize_pyrit_async
3636

@@ -41,35 +41,13 @@
4141
endpoint=image_endpoint,
4242
api_key=get_azure_openai_auth(image_endpoint),
4343
output_format="jpeg",
44-
# Override default configuration to enable multi-turn, JSON output, and multi-modal input for this attack-with-scoring example.
45-
custom_configuration=TargetConfiguration(
46-
capabilities=TargetCapabilities(
47-
supports_multi_turn=True,
48-
supports_json_output=True,
49-
supports_multi_message_pieces=True,
50-
input_modalities=frozenset(
51-
{frozenset({"text", "image_path"}), frozenset({"image_path"}), frozenset({"text"})}
52-
),
53-
)
54-
),
5544
)
5645

5746
chat_endpoint = os.environ["OPENAI_CHAT_ENDPOINT"]
5847
image_scorer = SelfAskTrueFalseScorer(
5948
chat_target=OpenAIChatTarget(
6049
endpoint=chat_endpoint,
6150
api_key=get_azure_openai_auth(chat_endpoint),
62-
# The scorer needs to evaluate image responses; override configuration to support image input modalities.
63-
custom_configuration=TargetConfiguration(
64-
capabilities=TargetCapabilities(
65-
supports_multi_turn=True,
66-
supports_json_output=True,
67-
supports_multi_message_pieces=True,
68-
input_modalities=frozenset(
69-
{frozenset({"text", "image_path"}), frozenset({"image_path"}), frozenset({"text"})}
70-
),
71-
)
72-
),
7351
),
7452
true_false_question=TrueFalseQuestion(true_description="The response matches the objective {{objective}}"),
7553
)

pyrit/auth/copilot_authenticator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -474,8 +474,8 @@ async def response_handler(response: Any) -> None:
474474
finally:
475475
logger.info("Gracefully closing Playwright browser instance...")
476476

477-
if page: # type: ignore[ty:possibly-unresolved-reference]
478-
await page.close() # type: ignore[ty:possibly-unresolved-reference]
477+
if page:
478+
await page.close()
479479
if context:
480480
await context.close()
481481
if browser:

pyrit/executor/attack/component/conversation_manager.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
)
2222
from pyrit.prompt_normalizer.prompt_normalizer import PromptNormalizer
2323
from pyrit.prompt_target import PromptTarget
24-
from pyrit.prompt_target.common.prompt_chat_target import PromptChatTarget
24+
from pyrit.prompt_target.common.target_capabilities import CapabilityName
2525

2626
if TYPE_CHECKING:
2727
from pyrit.executor.attack.core import AttackContext
@@ -250,7 +250,7 @@ def get_last_message(
250250
def set_system_prompt(
251251
self,
252252
*,
253-
target: PromptChatTarget,
253+
target: PromptTarget,
254254
conversation_id: str,
255255
system_prompt: str,
256256
labels: Optional[dict[str, str]] = None, # deprecated
@@ -259,18 +259,24 @@ def set_system_prompt(
259259
Set or update the system prompt for a conversation.
260260
261261
Args:
262-
target: The chat target to set the system prompt on.
262+
target: The target to set the system prompt on. Must handle the
263+
SYSTEM_PROMPT capability (natively or via an ADAPT policy).
263264
conversation_id: Unique identifier for the conversation.
264265
system_prompt: The system prompt text.
265266
labels: Optional labels to associate with the system prompt.
266267
Deprecated: This parameter will be removed in a release 0.16.0.
268+
269+
Raises:
270+
ValueError: If target cannot handle the SYSTEM_PROMPT capability.
267271
"""
268272
if labels is not None:
269273
print_deprecation_message(
270274
old_item="set_system_prompt(..., labels=...)",
271275
new_item="set_system_prompt(...)",
272276
removed_in="0.16.0",
273277
)
278+
target.configuration.ensure_can_handle(capability=CapabilityName.SYSTEM_PROMPT)
279+
274280
target.set_system_prompt(
275281
system_prompt=system_prompt,
276282
conversation_id=conversation_id,
@@ -298,7 +304,7 @@ async def initialize_context_async(
298304
3. Updates context.executed_turns for multi-turn attacks
299305
4. Sets context.next_message if there's an unanswered user message
300306
301-
For PromptChatTarget:
307+
For chat-capable PromptTarget:
302308
- Adds prepended messages to memory with simulated_assistant role
303309
- All messages get new UUIDs
304310
@@ -321,7 +327,7 @@ async def initialize_context_async(
321327
322328
Raises:
323329
ValueError: If conversation_id is empty, or if prepended_conversation
324-
requires a PromptChatTarget but target is not one.
330+
requires a chat-capable PromptTarget but target is not one.
325331
"""
326332
if not conversation_id:
327333
raise ValueError("conversation_id cannot be empty")
@@ -336,8 +342,11 @@ async def initialize_context_async(
336342
logger.debug(f"No prepended conversation for context initialization: {conversation_id}")
337343
return state
338344

339-
# Handle target type compatibility
340-
is_chat_target = isinstance(target, PromptChatTarget)
345+
# Targets that don't natively support editable history cannot consume a
346+
# prepended multi-message conversation as-is — route them to the
347+
# single-string fallback path. Type identity (PromptChatTarget) is a
348+
# legacy signal for this; capability-based routing is the durable form.
349+
is_chat_target = target.configuration.includes(capability=CapabilityName.EDITABLE_HISTORY)
341350
if not is_chat_target:
342351
return await self._handle_non_chat_target_async(
343352
context=context,
@@ -381,8 +390,8 @@ async def _handle_non_chat_target_async(
381390

382391
if config.non_chat_target_behavior == "raise":
383392
raise ValueError(
384-
"prepended_conversation requires the objective target to be a PromptChatTarget. "
385-
"Non-chat objective targets do not support conversation history. "
393+
"prepended_conversation requires the objective target to be a chat-capable "
394+
"PromptTarget. Non-chat objective targets do not support conversation history. "
386395
"Use PrependedConversationConfig with non_chat_target_behavior='normalize_first_turn' "
387396
"to normalize the conversation into the first message instead."
388397
)

pyrit/executor/attack/component/prepended_conversation_config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class PrependedConversationConfig:
2222
This class provides control over:
2323
- Which message roles should have request converters applied
2424
- How to normalize conversation history for non-chat objective targets
25-
- What to do when the objective target is not a PromptChatTarget
25+
- What to do when the objective target is not a chat-capable PromptTarget
2626
"""
2727

2828
# Roles for which request converters should be applied to prepended messages.
@@ -36,13 +36,13 @@ class PrependedConversationConfig:
3636
# ConversationContextNormalizer is used that produces "Turn N: User/Assistant" format.
3737
message_normalizer: Optional[MessageStringNormalizer] = None
3838

39-
# Behavior when the target is a PromptTarget but not a PromptChatTarget:
39+
# Behavior when the target is a PromptTarget but not a chat-capable PromptTarget:
4040
# - "normalize_first_turn": Normalize the prepended conversation into a string and
4141
# store it in ConversationState.normalized_prepended_context. This context will be
4242
# prepended to the first message sent to the target. Uses objective_target_context_normalizer
4343
# if provided, otherwise falls back to ConversationContextNormalizer.
4444
# - "raise": Raise a ValueError. Use this when prepended conversation history must be
45-
# maintained by the target (i.e., target must be a PromptChatTarget).
45+
# maintained by the target (i.e., target must be a chat-capable PromptTarget).
4646
non_chat_target_behavior: Literal["normalize_first_turn", "raise"] = "normalize_first_turn"
4747

4848
def get_message_normalizer(self) -> MessageStringNormalizer:

0 commit comments

Comments
 (0)