Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pyrit/backend/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
AttackSummary,
ConversationMessagesResponse,
ConversationSummary,
ConverterConfigurationRequest,
ConverterOptionsResponse,
CreateAttackRequest,
CreateAttackResponse,
Expand Down Expand Up @@ -74,6 +75,7 @@
"UpdateMainConversationResponse": "pyrit.backend.models.attacks",
"ConversationMessagesResponse": "pyrit.backend.models.attacks",
"ConversationSummary": "pyrit.backend.models.attacks",
"ConverterConfigurationRequest": "pyrit.backend.models.attacks",
"ConverterOptionsResponse": "pyrit.backend.models.attacks",
"CreateAttackRequest": "pyrit.backend.models.attacks",
"CreateAttackResponse": "pyrit.backend.models.attacks",
Expand Down
75 changes: 72 additions & 3 deletions pyrit/backend/models/attacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@

import uuid
from datetime import datetime, timezone
from typing import Any, Literal, cast
from typing import Annotated, Any, Literal, cast

from pydantic import BaseModel, Field, computed_field, field_serializer
from pydantic import BaseModel, Field, computed_field, field_serializer, model_validator

from pyrit.backend.models._media import build_filename, infer_mime_type
from pyrit.backend.models.common import PaginationInfo
Expand All @@ -22,6 +22,7 @@
ConversationReference,
Message,
MessagePiece,
PromptDataType,
Score,
)

Expand Down Expand Up @@ -472,6 +473,26 @@ class UpdateMainConversationResponse(BaseModel):
# ============================================================================


class ConverterConfigurationRequest(BaseModel):
"""Registry-backed converter configuration for one ordered pipeline."""

converter_ids: list[str] = Field(
...,
min_length=1,
description="Converter instance IDs to apply in order.",
)
indexes_to_apply: list[Annotated[int, Field(ge=0)]] | None = Field(
None,
min_length=1,
description="Zero-based message piece indexes to which this pipeline applies. Defaults to all indexes.",
)
prompt_data_types_to_apply: list[PromptDataType] | None = Field(
None,
min_length=1,
description="Prompt data types to which this pipeline applies. Defaults to all data types.",
)


class AddMessageRequest(BaseModel):
"""
Request to add a message to an attack.
Expand All @@ -492,7 +513,18 @@ class AddMessageRequest(BaseModel):
description="Target registry name. Required when send=True so the backend knows which target to use.",
)
converter_ids: list[str] | None = Field(
None, description="Converter instance IDs to apply (overrides attack-level)"
None,
description="Deprecated global request converter pipeline. Use request_converter_configurations instead.",
)
request_converter_configurations: list[ConverterConfigurationRequest] | None = Field(
None,
min_length=1,
description="Ordered registry-backed converter pipelines to apply to the request.",
)
response_converter_configurations: list[ConverterConfigurationRequest] | None = Field(
None,
min_length=1,
description="Ordered registry-backed converter pipelines to apply to the response.",
)
target_conversation_id: str = Field(
...,
Expand All @@ -505,6 +537,43 @@ class AddMessageRequest(BaseModel):
"When present, the operator must match the attack result's operator.",
)

@model_validator(mode="after")
def _validate_converter_configurations(self) -> "AddMessageRequest":
"""
Validate converter configuration combinations and request indexes.

Returns:
AddMessageRequest: The validated request.

Raises:
ValueError: If converter fields conflict, cannot run, or contain an out-of-range request index.
"""
if self.converter_ids is not None and self.request_converter_configurations is not None:
raise ValueError("converter_ids and request_converter_configurations cannot both be provided")

has_converter_configurations = any(
configurations is not None
for configurations in (
self.converter_ids,
self.request_converter_configurations,
self.response_converter_configurations,
)
)
if not self.send and has_converter_configurations:
raise ValueError("Converter configurations require send=True")

piece_count = len(self.pieces)
for configuration in self.request_converter_configurations or []:
if configuration.indexes_to_apply is None:
continue
invalid_indexes = [index for index in configuration.indexes_to_apply if index >= piece_count]
if invalid_indexes:
raise ValueError(
f"Request converter indexes {invalid_indexes} are out of range for {piece_count} message pieces"
)

return self


class AddMessageResponse(BaseModel):
"""
Expand Down
8 changes: 4 additions & 4 deletions pyrit/backend/routes/attacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,10 +449,10 @@ async def add_message( # pyrit-async-suffix-exempt
If send=False, just stores the message in memory without sending (useful for
system messages, context injection, or replaying assistant responses).

Converters can be specified at three levels (in priority order):
1. request.converter_ids - per-message converter instances
2. request.converters - inline converter definitions
3. attack.converter_ids - attack-level defaults
Request and response converters can be supplied as ordered, registry-backed
configurations. Each configuration can target message piece indexes, prompt
data types, or both. The global ``converter_ids`` request pipeline remains
available for compatibility but is deprecated.

Returns:
AddMessageResponse: Updated attack with new message(s).
Expand Down
Loading