diff --git a/doc/code/converters/3_image_converters.ipynb b/doc/code/converters/3_image_converters.ipynb index 21149e66a4..f1cb3b9e6f 100644 --- a/doc/code/converters/3_image_converters.ipynb +++ b/doc/code/converters/3_image_converters.ipynb @@ -66,6 +66,7 @@ "\n", "from pyrit.prompt_converter import QRCodeConverter\n", "from pyrit.prompt_target.common.target_capabilities import TargetCapabilities\n", + "from pyrit.prompt_target.common.target_configuration import TargetConfiguration\n", "from pyrit.setup import IN_MEMORY, initialize_pyrit_async\n", "\n", "await initialize_pyrit_async(memory_db_type=IN_MEMORY) # type: ignore\n", @@ -409,11 +410,15 @@ "from pyrit.prompt_target import OpenAIChatTarget\n", "\n", "llm_target = OpenAIChatTarget(\n", - " # The target needs to accept a multi-piece message containing an image; override the default text-only capabilities.\n", - " custom_capabilities=TargetCapabilities(\n", - " supports_multi_message_pieces=True,\n", - " supports_multi_turn=True,\n", - " input_modalities=frozenset({frozenset({\"text\", \"image_path\"}), frozenset({\"text\"}), frozenset({\"image_path\"})}),\n", + " # The target needs to accept a multi-piece message containing an image; override the default text-only configuration.\n", + " custom_configuration=TargetConfiguration(\n", + " capabilities=TargetCapabilities(\n", + " supports_multi_message_pieces=True,\n", + " supports_multi_turn=True,\n", + " input_modalities=frozenset(\n", + " {frozenset({\"text\", \"image_path\"}), frozenset({\"text\"}), frozenset({\"image_path\"})}\n", + " ),\n", + " )\n", " )\n", ")\n", "\n", diff --git a/doc/code/converters/3_image_converters.py b/doc/code/converters/3_image_converters.py index 5fbe1801bb..cf2350f464 100644 --- a/doc/code/converters/3_image_converters.py +++ b/doc/code/converters/3_image_converters.py @@ -36,6 +36,7 @@ from pyrit.prompt_converter import QRCodeConverter from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.setup import IN_MEMORY, initialize_pyrit_async await initialize_pyrit_async(memory_db_type=IN_MEMORY) # type: ignore @@ -179,11 +180,15 @@ from pyrit.prompt_target import OpenAIChatTarget llm_target = OpenAIChatTarget( - # The target needs to accept a multi-piece message containing an image; override the default text-only capabilities. - custom_capabilities=TargetCapabilities( - supports_multi_message_pieces=True, - supports_multi_turn=True, - input_modalities=frozenset({frozenset({"text", "image_path"}), frozenset({"text"}), frozenset({"image_path"})}), + # The target needs to accept a multi-piece message containing an image; override the default text-only configuration. + custom_configuration=TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_message_pieces=True, + supports_multi_turn=True, + input_modalities=frozenset( + {frozenset({"text", "image_path"}), frozenset({"text"}), frozenset({"image_path"})} + ), + ) ) ) diff --git a/doc/code/executor/attack/2_red_teaming_attack.ipynb b/doc/code/executor/attack/2_red_teaming_attack.ipynb index 74e63ccacd..d32486c6c8 100644 --- a/doc/code/executor/attack/2_red_teaming_attack.ipynb +++ b/doc/code/executor/attack/2_red_teaming_attack.ipynb @@ -177,6 +177,7 @@ ")\n", "from pyrit.prompt_target import AzureMLChatTarget, OpenAIChatTarget\n", "from pyrit.prompt_target.common.target_capabilities import TargetCapabilities\n", + "from pyrit.prompt_target.common.target_configuration import TargetConfiguration\n", "from pyrit.score import SelfAskTrueFalseScorer, TrueFalseQuestion\n", "from pyrit.setup import IN_MEMORY, initialize_pyrit_async\n", "\n", @@ -1238,7 +1239,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "WARNING:pyrit.prompt_target.common.prompt_target:No known capabilities for model 'gpt-image-1.5'. Falling back to OpenAIImageTarget._DEFAULT_CAPABILITIES.\n" + "WARNING:pyrit.prompt_target.common.prompt_target:No known capabilities for model 'gpt-image-1.5'. Falling back to OpenAIImageTarget._DEFAULT_CONFIGURATION.\n" ] }, { @@ -1394,14 +1395,16 @@ "scoring_config = AttackScoringConfig(\n", " objective_scorer=SelfAskTrueFalseScorer(\n", " chat_target=OpenAIChatTarget(\n", - " # The scorer evaluates image outputs from the attack; override capabilities to support image input modalities.\n", - " custom_capabilities=TargetCapabilities(\n", - " supports_multi_message_pieces=True,\n", - " supports_multi_turn=True,\n", - " supports_json_output=True,\n", - " input_modalities=frozenset(\n", - " {frozenset({\"text\", \"image_path\"}), frozenset({\"text\"}), frozenset({\"image_path\"})}\n", - " ),\n", + " # The scorer evaluates image outputs from the attack; override configuration to support image input modalities.\n", + " custom_configuration=TargetConfiguration(\n", + " capabilities=TargetCapabilities(\n", + " supports_multi_message_pieces=True,\n", + " supports_multi_turn=True,\n", + " supports_json_output=True,\n", + " input_modalities=frozenset(\n", + " {frozenset({\"text\", \"image_path\"}), frozenset({\"text\"}), frozenset({\"image_path\"})}\n", + " ),\n", + " )\n", " )\n", " ),\n", " true_false_question=TrueFalseQuestion(\n", diff --git a/doc/code/executor/attack/2_red_teaming_attack.py b/doc/code/executor/attack/2_red_teaming_attack.py index 149774382f..8612eca14a 100644 --- a/doc/code/executor/attack/2_red_teaming_attack.py +++ b/doc/code/executor/attack/2_red_teaming_attack.py @@ -69,6 +69,7 @@ ) from pyrit.prompt_target import AzureMLChatTarget, OpenAIChatTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.score import SelfAskTrueFalseScorer, TrueFalseQuestion from pyrit.setup import IN_MEMORY, initialize_pyrit_async @@ -279,14 +280,16 @@ scoring_config = AttackScoringConfig( objective_scorer=SelfAskTrueFalseScorer( chat_target=OpenAIChatTarget( - # The scorer evaluates image outputs from the attack; override capabilities to support image input modalities. - custom_capabilities=TargetCapabilities( - supports_multi_message_pieces=True, - supports_multi_turn=True, - supports_json_output=True, - input_modalities=frozenset( - {frozenset({"text", "image_path"}), frozenset({"text"}), frozenset({"image_path"})} - ), + # The scorer evaluates image outputs from the attack; override configuration to support image input modalities. + custom_configuration=TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_message_pieces=True, + supports_multi_turn=True, + supports_json_output=True, + input_modalities=frozenset( + {frozenset({"text", "image_path"}), frozenset({"text"}), frozenset({"image_path"})} + ), + ) ) ), true_false_question=TrueFalseQuestion( diff --git a/doc/code/targets/1_openai_chat_target.ipynb b/doc/code/targets/1_openai_chat_target.ipynb index 80f6ccefc0..a70de4596b 100644 --- a/doc/code/targets/1_openai_chat_target.ipynb +++ b/doc/code/targets/1_openai_chat_target.ipynb @@ -263,6 +263,7 @@ "from pyrit.models import SeedGroup, SeedPrompt\n", "from pyrit.prompt_target import OpenAIChatTarget\n", "from pyrit.prompt_target.common.target_capabilities import TargetCapabilities\n", + "from pyrit.prompt_target.common.target_configuration import TargetConfiguration\n", "from pyrit.score import SelfAskTrueFalseScorer, TrueFalseQuestion\n", "from pyrit.setup import IN_MEMORY, initialize_pyrit_async\n", "\n", @@ -274,12 +275,16 @@ "chat_target = OpenAIChatTarget(\n", " endpoint=endpoint,\n", " api_key=api_key,\n", - " # Override default (text-only) capabilities to enable image input, multi-turn, and JSON output for this multi-modal example.\n", - " custom_capabilities=TargetCapabilities(\n", - " supports_multi_turn=True,\n", - " supports_json_output=True,\n", - " supports_multi_message_pieces=True,\n", - " input_modalities=frozenset({frozenset({\"text\", \"image_path\"}), frozenset({\"image_path\"}), frozenset({\"text\"})}),\n", + " # Override default (text-only) configuration to enable image input, multi-turn, and JSON output for this multi-modal example.\n", + " custom_configuration=TargetConfiguration(\n", + " capabilities=TargetCapabilities(\n", + " supports_multi_turn=True,\n", + " supports_json_output=True,\n", + " supports_multi_message_pieces=True,\n", + " input_modalities=frozenset(\n", + " {frozenset({\"text\", \"image_path\"}), frozenset({\"image_path\"}), frozenset({\"text\"})}\n", + " ),\n", + " )\n", " ),\n", ")\n", "\n", @@ -287,14 +292,16 @@ " chat_target=OpenAIChatTarget(\n", " endpoint=endpoint,\n", " api_key=api_key,\n", - " # The scorer also needs to read image responses; override capabilities to support image input modalities.\n", - " custom_capabilities=TargetCapabilities(\n", - " supports_multi_turn=True,\n", - " supports_json_output=True,\n", - " supports_multi_message_pieces=True,\n", - " input_modalities=frozenset(\n", - " {frozenset({\"text\", \"image_path\"}), frozenset({\"image_path\"}), frozenset({\"text\"})}\n", - " ),\n", + " # The scorer also needs to read image responses; override configuration to support image input modalities.\n", + " custom_configuration=TargetConfiguration(\n", + " capabilities=TargetCapabilities(\n", + " supports_multi_turn=True,\n", + " supports_json_output=True,\n", + " supports_multi_message_pieces=True,\n", + " input_modalities=frozenset(\n", + " {frozenset({\"text\", \"image_path\"}), frozenset({\"image_path\"}), frozenset({\"text\"})}\n", + " ),\n", + " )\n", " ),\n", " ),\n", " true_false_question=TrueFalseQuestion(\n", diff --git a/doc/code/targets/1_openai_chat_target.py b/doc/code/targets/1_openai_chat_target.py index d2c08ab892..d3a285e1a0 100644 --- a/doc/code/targets/1_openai_chat_target.py +++ b/doc/code/targets/1_openai_chat_target.py @@ -126,6 +126,7 @@ from pyrit.models import SeedGroup, SeedPrompt from pyrit.prompt_target import OpenAIChatTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.score import SelfAskTrueFalseScorer, TrueFalseQuestion from pyrit.setup import IN_MEMORY, initialize_pyrit_async @@ -137,12 +138,16 @@ chat_target = OpenAIChatTarget( endpoint=endpoint, api_key=api_key, - # Override default (text-only) capabilities to enable image input, multi-turn, and JSON output for this multi-modal example. - custom_capabilities=TargetCapabilities( - supports_multi_turn=True, - supports_json_output=True, - supports_multi_message_pieces=True, - input_modalities=frozenset({frozenset({"text", "image_path"}), frozenset({"image_path"}), frozenset({"text"})}), + # Override default (text-only) configuration to enable image input, multi-turn, and JSON output for this multi-modal example. + custom_configuration=TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=True, + supports_json_output=True, + supports_multi_message_pieces=True, + input_modalities=frozenset( + {frozenset({"text", "image_path"}), frozenset({"image_path"}), frozenset({"text"})} + ), + ) ), ) @@ -150,14 +155,16 @@ chat_target=OpenAIChatTarget( endpoint=endpoint, api_key=api_key, - # The scorer also needs to read image responses; override capabilities to support image input modalities. - custom_capabilities=TargetCapabilities( - supports_multi_turn=True, - supports_json_output=True, - supports_multi_message_pieces=True, - input_modalities=frozenset( - {frozenset({"text", "image_path"}), frozenset({"image_path"}), frozenset({"text"})} - ), + # The scorer also needs to read image responses; override configuration to support image input modalities. + custom_configuration=TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=True, + supports_json_output=True, + supports_multi_message_pieces=True, + input_modalities=frozenset( + {frozenset({"text", "image_path"}), frozenset({"image_path"}), frozenset({"text"})} + ), + ) ), ), true_false_question=TrueFalseQuestion( diff --git a/doc/code/targets/3_openai_image_target.ipynb b/doc/code/targets/3_openai_image_target.ipynb index 6492c7f249..c9b4eb0245 100644 --- a/doc/code/targets/3_openai_image_target.ipynb +++ b/doc/code/targets/3_openai_image_target.ipynb @@ -115,6 +115,7 @@ ")\n", "from pyrit.prompt_target import OpenAIChatTarget, OpenAIImageTarget\n", "from pyrit.prompt_target.common.target_capabilities import TargetCapabilities\n", + "from pyrit.prompt_target.common.target_configuration import TargetConfiguration\n", "from pyrit.score import SelfAskTrueFalseScorer, TrueFalseQuestion\n", "from pyrit.setup import IN_MEMORY, initialize_pyrit_async\n", "\n", @@ -125,12 +126,16 @@ " endpoint=image_endpoint,\n", " api_key=get_azure_openai_auth(image_endpoint),\n", " output_format=\"jpeg\",\n", - " # Override default capabilities to enable multi-turn, JSON output, and multi-modal input for this attack-with-scoring example.\n", - " custom_capabilities=TargetCapabilities(\n", - " supports_multi_turn=True,\n", - " supports_json_output=True,\n", - " supports_multi_message_pieces=True,\n", - " input_modalities=frozenset({frozenset({\"text\", \"image_path\"}), frozenset({\"image_path\"}), frozenset({\"text\"})}),\n", + " # Override default configuration to enable multi-turn, JSON output, and multi-modal input for this attack-with-scoring example.\n", + " custom_configuration=TargetConfiguration(\n", + " capabilities=TargetCapabilities(\n", + " supports_multi_turn=True,\n", + " supports_json_output=True,\n", + " supports_multi_message_pieces=True,\n", + " input_modalities=frozenset(\n", + " {frozenset({\"text\", \"image_path\"}), frozenset({\"image_path\"}), frozenset({\"text\"})}\n", + " ),\n", + " )\n", " ),\n", ")\n", "\n", @@ -139,14 +144,16 @@ " chat_target=OpenAIChatTarget(\n", " endpoint=chat_endpoint,\n", " api_key=get_azure_openai_auth(chat_endpoint),\n", - " # The scorer needs to evaluate image responses; override capabilities to support image input modalities.\n", - " custom_capabilities=TargetCapabilities(\n", - " supports_multi_turn=True,\n", - " supports_json_output=True,\n", - " supports_multi_message_pieces=True,\n", - " input_modalities=frozenset(\n", - " {frozenset({\"text\", \"image_path\"}), frozenset({\"image_path\"}), frozenset({\"text\"})}\n", - " ),\n", + " # The scorer needs to evaluate image responses; override configuration to support image input modalities.\n", + " custom_configuration=TargetConfiguration(\n", + " capabilities=TargetCapabilities(\n", + " supports_multi_turn=True,\n", + " supports_json_output=True,\n", + " supports_multi_message_pieces=True,\n", + " input_modalities=frozenset(\n", + " {frozenset({\"text\", \"image_path\"}), frozenset({\"image_path\"}), frozenset({\"text\"})}\n", + " ),\n", + " )\n", " ),\n", " ),\n", " true_false_question=TrueFalseQuestion(true_description=\"The response matches the objective {{objective}}\"),\n", diff --git a/doc/code/targets/3_openai_image_target.py b/doc/code/targets/3_openai_image_target.py index 439aa1ea5c..ad3e5f2780 100644 --- a/doc/code/targets/3_openai_image_target.py +++ b/doc/code/targets/3_openai_image_target.py @@ -32,6 +32,7 @@ ) from pyrit.prompt_target import OpenAIChatTarget, OpenAIImageTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.score import SelfAskTrueFalseScorer, TrueFalseQuestion from pyrit.setup import IN_MEMORY, initialize_pyrit_async @@ -42,12 +43,16 @@ endpoint=image_endpoint, api_key=get_azure_openai_auth(image_endpoint), output_format="jpeg", - # Override default capabilities to enable multi-turn, JSON output, and multi-modal input for this attack-with-scoring example. - custom_capabilities=TargetCapabilities( - supports_multi_turn=True, - supports_json_output=True, - supports_multi_message_pieces=True, - input_modalities=frozenset({frozenset({"text", "image_path"}), frozenset({"image_path"}), frozenset({"text"})}), + # Override default configuration to enable multi-turn, JSON output, and multi-modal input for this attack-with-scoring example. + custom_configuration=TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=True, + supports_json_output=True, + supports_multi_message_pieces=True, + input_modalities=frozenset( + {frozenset({"text", "image_path"}), frozenset({"image_path"}), frozenset({"text"})} + ), + ) ), ) @@ -56,14 +61,16 @@ chat_target=OpenAIChatTarget( endpoint=chat_endpoint, api_key=get_azure_openai_auth(chat_endpoint), - # The scorer needs to evaluate image responses; override capabilities to support image input modalities. - custom_capabilities=TargetCapabilities( - supports_multi_turn=True, - supports_json_output=True, - supports_multi_message_pieces=True, - input_modalities=frozenset( - {frozenset({"text", "image_path"}), frozenset({"image_path"}), frozenset({"text"})} - ), + # The scorer needs to evaluate image responses; override configuration to support image input modalities. + custom_configuration=TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=True, + supports_json_output=True, + supports_multi_message_pieces=True, + input_modalities=frozenset( + {frozenset({"text", "image_path"}), frozenset({"image_path"}), frozenset({"text"})} + ), + ) ), ), true_false_question=TrueFalseQuestion(true_description="The response matches the objective {{objective}}"), diff --git a/pyrit/prompt_target/azure_blob_storage_target.py b/pyrit/prompt_target/azure_blob_storage_target.py index 8acdcee470..dcabad099c 100644 --- a/pyrit/prompt_target/azure_blob_storage_target.py +++ b/pyrit/prompt_target/azure_blob_storage_target.py @@ -16,6 +16,7 @@ from pyrit.models import Message, construct_response_from_request from pyrit.prompt_target.common.prompt_target import PromptTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.prompt_target.common.utils import limit_requests_per_minute logger = logging.getLogger(__name__) @@ -50,18 +51,20 @@ class AzureBlobStorageTarget(PromptTarget): AZURE_STORAGE_CONTAINER_ENVIRONMENT_VARIABLE: str = "AZURE_STORAGE_ACCOUNT_CONTAINER_URL" SAS_TOKEN_ENVIRONMENT_VARIABLE: str = "AZURE_STORAGE_ACCOUNT_SAS_TOKEN" - _DEFAULT_CAPABILITIES: TargetCapabilities = TargetCapabilities( - input_modalities=frozenset( - { - frozenset(["text"]), - frozenset(["url"]), - } - ), - output_modalities=frozenset( - { - frozenset(["url"]), - } - ), + _DEFAULT_CONFIGURATION: TargetConfiguration = TargetConfiguration( + capabilities=TargetCapabilities( + input_modalities=frozenset( + { + frozenset(["text"]), + frozenset(["url"]), + } + ), + output_modalities=frozenset( + { + frozenset(["url"]), + } + ), + ) ) def __init__( @@ -71,6 +74,7 @@ def __init__( sas_token: Optional[str] = None, blob_content_type: SupportedContentType = SupportedContentType.PLAIN_TEXT, max_requests_per_minute: Optional[int] = None, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, ) -> None: """ @@ -84,8 +88,10 @@ def __init__( blob_content_type (SupportedContentType): The content type for blobs. Defaults to PLAIN_TEXT. max_requests_per_minute (int, Optional): Maximum number of requests per minute. - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for + custom_configuration (TargetConfiguration, Optional): Override the default configuration for this target instance. Defaults to None. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. """ self._blob_content_type: str = blob_content_type.value @@ -99,6 +105,7 @@ def __init__( super().__init__( endpoint=self._container_url, max_requests_per_minute=max_requests_per_minute, + custom_configuration=custom_configuration, custom_capabilities=custom_capabilities, ) diff --git a/pyrit/prompt_target/azure_ml_chat_target.py b/pyrit/prompt_target/azure_ml_chat_target.py index 4795d42d0d..cd7083c61a 100644 --- a/pyrit/prompt_target/azure_ml_chat_target.py +++ b/pyrit/prompt_target/azure_ml_chat_target.py @@ -21,6 +21,7 @@ ) from pyrit.prompt_target.common.prompt_chat_target import PromptChatTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.prompt_target.common.utils import limit_requests_per_minute, validate_temperature, validate_top_p logger = logging.getLogger(__name__) @@ -41,11 +42,13 @@ class AzureMLChatTarget(PromptChatTarget): endpoint_uri_environment_variable: str = "AZURE_ML_MANAGED_ENDPOINT" api_key_environment_variable: str = "AZURE_ML_KEY" - _DEFAULT_CAPABILITIES: TargetCapabilities = TargetCapabilities( - supports_multi_message_pieces=True, - supports_editable_history=True, - supports_multi_turn=True, - supports_system_prompt=True, + _DEFAULT_CONFIGURATION: TargetConfiguration = TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_message_pieces=True, + supports_editable_history=True, + supports_multi_turn=True, + supports_system_prompt=True, + ) ) def __init__( @@ -60,6 +63,7 @@ def __init__( top_p: float = 1.0, repetition_penalty: float = 1.0, max_requests_per_minute: Optional[int] = None, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, **param_kwargs: Any, ) -> None: @@ -88,8 +92,10 @@ def __init__( max_requests_per_minute (int, Optional): Number of requests the target can handle per minute before hitting a rate limit. The number of requests sent to the target will be capped at the value provided. - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for this target + custom_configuration (TargetConfiguration, Optional): Override the default configuration for this target instance. Useful for targets whose capabilities depend on deployment configuration. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. **param_kwargs: Additional parameters to pass to the model for generating responses. Example parameters can be found here: https://huggingface.co/docs/api-inference/tasks/text-generation. Note that the link above may not be comprehensive, and specific acceptable parameters may be @@ -104,6 +110,7 @@ def __init__( max_requests_per_minute=max_requests_per_minute, endpoint=endpoint_value, model_name=model_name, + custom_configuration=custom_configuration, custom_capabilities=custom_capabilities, ) diff --git a/pyrit/prompt_target/common/conversation_normalization_pipeline.py b/pyrit/prompt_target/common/conversation_normalization_pipeline.py index d81d7c97ae..619f2b59e8 100644 --- a/pyrit/prompt_target/common/conversation_normalization_pipeline.py +++ b/pyrit/prompt_target/common/conversation_normalization_pipeline.py @@ -99,6 +99,8 @@ def from_capabilities( behavior = policy.get_behavior(capability=capability) + # TODO: This ValueError can be removed once TargetConfiguration.ensure_can_handle() + # is called in the full end-to-end request flow, making validation deferred to request time. if behavior == UnsupportedCapabilityBehavior.RAISE: raise ValueError(f"Target does not support '{capability.value}' and the handling policy is RAISE.") diff --git a/pyrit/prompt_target/common/prompt_chat_target.py b/pyrit/prompt_target/common/prompt_chat_target.py index 6b77ac03b1..ce1f254678 100644 --- a/pyrit/prompt_target/common/prompt_chat_target.py +++ b/pyrit/prompt_target/common/prompt_chat_target.py @@ -8,6 +8,7 @@ from pyrit.models.json_response_config import _JsonResponseConfig from pyrit.prompt_target.common.prompt_target import PromptTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration class PromptChatTarget(PromptTarget): @@ -21,10 +22,12 @@ class PromptChatTarget(PromptTarget): Realtime chat targets or OpenAI completions are NOT PromptChatTargets. You don't send the conversation history. """ - _DEFAULT_CAPABILITIES: TargetCapabilities = TargetCapabilities( - supports_multi_turn=True, - supports_multi_message_pieces=True, - supports_system_prompt=True, + _DEFAULT_CONFIGURATION: TargetConfiguration = TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=True, + supports_multi_message_pieces=True, + supports_system_prompt=True, + ) ) def __init__( @@ -34,6 +37,7 @@ def __init__( endpoint: str = "", model_name: str = "", underlying_model: Optional[str] = None, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, ) -> None: """ @@ -46,14 +50,17 @@ def __init__( underlying_model (str, Optional): The underlying model name (e.g., "gpt-4o") for identification purposes. This is useful when the deployment name in Azure differs from the actual model. Defaults to None. - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for - this target instance. If None, uses the class-level defaults. Defaults to None. + custom_configuration (TargetConfiguration, Optional): Override the default configuration + for this target instance. If None, uses the class-level defaults. Defaults to None. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. """ super().__init__( max_requests_per_minute=max_requests_per_minute, endpoint=endpoint, model_name=model_name, underlying_model=underlying_model, + custom_configuration=custom_configuration, custom_capabilities=custom_capabilities, ) diff --git a/pyrit/prompt_target/common/prompt_target.py b/pyrit/prompt_target/common/prompt_target.py index ecaf5e1c39..69ae8a2e9c 100644 --- a/pyrit/prompt_target/common/prompt_target.py +++ b/pyrit/prompt_target/common/prompt_target.py @@ -3,12 +3,14 @@ import abc import logging +import warnings from typing import Any, Optional, Union from pyrit.identifiers import ComponentIdentifier, Identifiable from pyrit.memory import CentralMemory, MemoryInterface from pyrit.models import Message from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration, resolve_configuration_compat logger = logging.getLogger(__name__) @@ -29,16 +31,28 @@ class PromptTarget(Identifiable): _identifier: Optional[ComponentIdentifier] = None - # Class-level default capabilities for this target type. + # Class-level default configuration for this target type. # # Subclasses **should** override this when their capabilities differ from the base # defaults (e.g., to declare multi-turn support or non-text modalities). - # Overriding is *optional* — if a subclass does not define ``_DEFAULT_CAPABILITIES``, + # Overriding is *optional* — if a subclass does not define ``_DEFAULT_CONFIGURATION``, # it inherits the base-class default (text-only, single-turn, no JSON response). # - # Per-instance overrides are also possible via the ``custom_capabilities`` + # Per-instance overrides are also possible via the ``custom_configuration`` # constructor parameter, which takes precedence over the class-level value. - _DEFAULT_CAPABILITIES: TargetCapabilities = TargetCapabilities() + _DEFAULT_CONFIGURATION: TargetConfiguration = TargetConfiguration(capabilities=TargetCapabilities()) + + def __init_subclass__(cls, **kwargs: Any) -> None: + super().__init_subclass__(**kwargs) + # Auto-promote the deprecated _DEFAULT_CAPABILITIES class attribute. + if "_DEFAULT_CAPABILITIES" in cls.__dict__: + warnings.warn( + f"{cls.__name__}._DEFAULT_CAPABILITIES is deprecated and will be removed in v0.14.0. " + "Use _DEFAULT_CONFIGURATION = TargetConfiguration(capabilities=...) instead.", + DeprecationWarning, + stacklevel=2, + ) + cls._DEFAULT_CONFIGURATION = TargetConfiguration(capabilities=cls.__dict__["_DEFAULT_CAPABILITIES"]) def __init__( self, @@ -47,6 +61,7 @@ def __init__( endpoint: str = "", model_name: str = "", underlying_model: Optional[str] = None, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, ) -> None: """ @@ -61,21 +76,27 @@ def __init__( identification purposes. This is useful when the deployment name in Azure differs from the actual model. If not provided, `model_name` will be used for the identifier. Defaults to None. - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for - this target instance. Useful for targets whose capabilities depend on deployment + custom_configuration (TargetConfiguration, Optional): Override the default configuration + for this target instance. Useful for targets whose capabilities depend on deployment configuration (e.g., Playwright, HTTP). If None, uses the class-level - ``_DEFAULT_CAPABILITIES``. Defaults to None. + ``_DEFAULT_CONFIGURATION``. Defaults to None. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. """ + custom_configuration = resolve_configuration_compat( + custom_configuration=custom_configuration, + custom_capabilities=custom_capabilities, + ) self._memory = CentralMemory.get_memory_instance() self._verbose = verbose self._max_requests_per_minute = max_requests_per_minute self._endpoint = endpoint self._model_name = model_name self._underlying_model = underlying_model - self._capabilities = ( - custom_capabilities - if custom_capabilities is not None - else type(self).get_default_capabilities(underlying_model) + self._configuration = ( + custom_configuration + if custom_configuration is not None + else type(self).get_default_configuration(underlying_model) ) if self._verbose: @@ -108,13 +129,13 @@ def _validate_request(self, *, message: Message) -> None: if n_pieces == 0: raise ValueError("Message must contain at least one message piece. Received: 0 pieces.") - custom_capabilities_message = ( - "If your target does support this, set the custom_capabilities parameter accordingly." + custom_configuration_message = ( + "If your target does support this, set the custom_configuration parameter accordingly." ) if not self.capabilities.supports_multi_message_pieces and n_pieces != 1: raise ValueError( f"This target only supports a single message piece. Received: {n_pieces} pieces. " - f"{custom_capabilities_message}" + f"{custom_configuration_message}" ) for piece in message.message_pieces: @@ -124,7 +145,7 @@ def _validate_request(self, *, message: Message) -> None: supported_types = ", ".join(sorted(supported_types_flat)) raise ValueError( f"This target supports only the following data types: {supported_types}. Received: {piece_type}. " - f"{custom_capabilities_message}" + f"{custom_configuration_message}" ) if not self.capabilities.supports_multi_turn: @@ -132,7 +153,9 @@ def _validate_request(self, *, message: Message) -> None: messages = self._memory.get_message_pieces(conversation_id=request.conversation_id) if len(messages) > 0: - raise ValueError(f"This target only supports a single turn conversation. {custom_capabilities_message}") + raise ValueError( + f"This target only supports a single turn conversation. {custom_configuration_message}" + ) def set_model_name(self, *, model_name: str) -> None: """ @@ -188,44 +211,71 @@ def _create_identifier( return ComponentIdentifier.of(self, params=all_params, children=children) @property - def capabilities(self) -> TargetCapabilities: + def configuration(self) -> TargetConfiguration: """ - The capabilities of this target instance. + The configuration of this target instance. - Defaults to the class-level ``_DEFAULT_CAPABILITIES``. Can be overridden - per instance via the ``capabilities`` constructor parameter, which is useful + Defaults to the class-level ``_DEFAULT_CONFIGURATION``. Can be overridden + per instance via the ``custom_configuration`` constructor parameter, which is useful for targets whose capabilities depend on deployment configuration (e.g., Playwright, HTTP). + Returns: + TargetConfiguration: The configuration for this target. + """ + return self._configuration + + @property + def capabilities(self) -> TargetCapabilities: + """ + The capabilities of this target instance. + + Shorthand for ``self.configuration.capabilities``. + Returns: TargetCapabilities: The capabilities for this target. """ - return self._capabilities + return self._configuration.capabilities @classmethod - def get_default_capabilities(cls, underlying_model: Optional[str]) -> TargetCapabilities: + def get_default_configuration(cls, underlying_model: Optional[str] = None) -> TargetConfiguration: """ - Return the capabilities for the given underlying model, falling back to - the class-level ``_DEFAULT_CAPABILITIES`` when the model is not recognized. + Return the configuration for the given underlying model, falling back to + the class-level ``_DEFAULT_CONFIGURATION`` when the model is not recognized. Args: underlying_model (str | None): The underlying model name (e.g., "gpt-4o"), or None if not specified. Returns: - TargetCapabilities: Known capabilities for the model, or the class's own - ``_DEFAULT_CAPABILITIES`` if the model is unrecognized or not provided. + TargetConfiguration: Known configuration for the model, or the class's own + ``_DEFAULT_CONFIGURATION`` if the model is unrecognized or not provided. """ if underlying_model: known = TargetCapabilities.get_known_capabilities(underlying_model) if known is not None: - return known + return TargetConfiguration(capabilities=known) logger.info( - "No known capabilities for model '%s'. Falling back to %s._DEFAULT_CAPABILITIES.", + "No known capabilities for model '%s'. Falling back to %s._DEFAULT_CONFIGURATION.", underlying_model, cls.__name__, ) - return cls._DEFAULT_CAPABILITIES + return cls._DEFAULT_CONFIGURATION + + @classmethod + def get_default_capabilities(cls, underlying_model: Optional[str] = None) -> TargetCapabilities: + """ + **Deprecated.** Use :meth:`get_default_configuration` instead. + + Will be removed in v0.14.0. + """ + warnings.warn( + "get_default_capabilities() is deprecated and will be removed in v0.14.0. " + "Use get_default_configuration() instead.", + DeprecationWarning, + stacklevel=2, + ) + return cls.get_default_configuration(underlying_model).capabilities def _build_identifier(self) -> ComponentIdentifier: """ diff --git a/pyrit/prompt_target/common/target_capabilities.py b/pyrit/prompt_target/common/target_capabilities.py index 7a34222803..58c6a7e05c 100644 --- a/pyrit/prompt_target/common/target_capabilities.py +++ b/pyrit/prompt_target/common/target_capabilities.py @@ -110,7 +110,7 @@ class TargetCapabilities: Describes the capabilities of a PromptTarget so that attacks and other components can adapt their behavior accordingly. - Each target class defines default capabilities via the _DEFAULT_CAPABILITIES + Each target class defines default capabilities via the _DEFAULT_CONFIGURATION class attribute. Users can override individual capabilities per instance through constructor parameters, which is useful for targets whose capabilities depend on deployment configuration (e.g., Playwright, HTTP). diff --git a/pyrit/prompt_target/common/target_configuration.py b/pyrit/prompt_target/common/target_configuration.py index 47abdb55d5..0daabd7fdb 100644 --- a/pyrit/prompt_target/common/target_configuration.py +++ b/pyrit/prompt_target/common/target_configuration.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. import logging +import warnings from pyrit.message_normalizer import MessageListNormalizer from pyrit.models import Message @@ -15,6 +16,47 @@ logger = logging.getLogger(__name__) + +def resolve_configuration_compat( + *, + custom_configuration: "TargetConfiguration | None", + custom_capabilities: TargetCapabilities | None, +) -> "TargetConfiguration | None": + """ + Resolve the deprecated ``custom_capabilities`` parameter. + + If the caller supplied the old ``custom_capabilities`` keyword, emit a + :class:`DeprecationWarning` and wrap the value in a + :class:`TargetConfiguration`. Passing both parameters is an error. + + Args: + custom_configuration (TargetConfiguration | None): The new-style configuration object. + custom_capabilities (TargetCapabilities | None): The deprecated capabilities object. + + Returns: + The resolved :class:`TargetConfiguration`, or *None* when neither + parameter was supplied. + + Raises: + ValueError: If both parameters were supplied. + """ + if custom_capabilities is not None and custom_configuration is not None: + raise ValueError( + "Cannot specify both 'custom_capabilities' and 'custom_configuration'. " + "Use 'custom_configuration' only; 'custom_capabilities' is deprecated and" + " will be removed in v0.14.0." + ) + if custom_capabilities is not None: + warnings.warn( + "'custom_capabilities' is deprecated and will be removed in v0.14.0. " + "Use 'custom_configuration=TargetConfiguration(capabilities=...)' instead.", + DeprecationWarning, + stacklevel=3, + ) + return TargetConfiguration(capabilities=custom_capabilities) + return custom_configuration + + # Default policy: RAISE on all adaptable capabilities. _DEFAULT_POLICY = CapabilityHandlingPolicy() @@ -53,9 +95,6 @@ def __init__( capability. Defaults to RAISE for all adaptable capabilities. normalizer_overrides (dict[CapabilityName, MessageListNormalizer[Message]] | None): Optional overrides for specific capability normalizers. - - Raises: - ValueError: If a required capability is missing and the policy is RAISE. """ self._capabilities = capabilities self._policy = policy or _DEFAULT_POLICY diff --git a/pyrit/prompt_target/gandalf_target.py b/pyrit/prompt_target/gandalf_target.py index a9dfe7170c..f92326e66b 100644 --- a/pyrit/prompt_target/gandalf_target.py +++ b/pyrit/prompt_target/gandalf_target.py @@ -11,6 +11,7 @@ from pyrit.models import Message, construct_response_from_request from pyrit.prompt_target.common.prompt_target import PromptTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.prompt_target.common.utils import limit_requests_per_minute logger = logging.getLogger(__name__) @@ -44,6 +45,7 @@ def __init__( *, level: GandalfLevel, max_requests_per_minute: Optional[int] = None, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, ) -> None: """ @@ -54,12 +56,17 @@ def __init__( max_requests_per_minute (int, Optional): Number of requests the target can handle per minute before hitting a rate limit. The number of requests sent to the target will be capped at the value provided. - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for this + custom_configuration (TargetConfiguration, Optional): Override the default configuration for this target instance. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. """ endpoint = "https://gandalf-api.lakera.ai/api/send-message" super().__init__( - max_requests_per_minute=max_requests_per_minute, endpoint=endpoint, custom_capabilities=custom_capabilities + max_requests_per_minute=max_requests_per_minute, + endpoint=endpoint, + custom_configuration=custom_configuration, + custom_capabilities=custom_capabilities, ) self._defender = level.value diff --git a/pyrit/prompt_target/http_target/http_target.py b/pyrit/prompt_target/http_target/http_target.py index 48e6a24c13..dbc3dcc309 100644 --- a/pyrit/prompt_target/http_target/http_target.py +++ b/pyrit/prompt_target/http_target/http_target.py @@ -18,6 +18,7 @@ ) from pyrit.prompt_target.common.prompt_target import PromptTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.prompt_target.common.utils import limit_requests_per_minute logger = logging.getLogger(__name__) @@ -41,6 +42,7 @@ def __init__( max_requests_per_minute: Optional[int] = None, client: Optional[httpx.AsyncClient] = None, model_name: str = "", + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, **httpx_client_kwargs: Any, ) -> None: @@ -57,8 +59,10 @@ def __init__( max_requests_per_minute (int, Optional): Maximum number of requests per minute. client (httpx.AsyncClient, Optional): Pre-configured httpx client. model_name (str): The model name. Defaults to empty string. - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for + custom_configuration (TargetConfiguration, Optional): Override the default configuration for this target instance. Defaults to None. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. **httpx_client_kwargs: Additional keyword arguments for httpx.AsyncClient. Raises: @@ -76,6 +80,7 @@ def __init__( max_requests_per_minute=max_requests_per_minute, endpoint=endpoint, model_name=model_name, + custom_configuration=custom_configuration, custom_capabilities=custom_capabilities, ) self.http_request = http_request diff --git a/pyrit/prompt_target/http_target/httpx_api_target.py b/pyrit/prompt_target/http_target/httpx_api_target.py index f41ad0a62d..7555038b64 100644 --- a/pyrit/prompt_target/http_target/httpx_api_target.py +++ b/pyrit/prompt_target/http_target/httpx_api_target.py @@ -15,6 +15,7 @@ construct_response_from_request, ) from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.prompt_target.common.utils import limit_requests_per_minute from pyrit.prompt_target.http_target.http_target import HTTPTarget @@ -31,7 +32,9 @@ class HTTPXAPITarget(HTTPTarget): it's a local file path generated by a PromptConverter (like PDFConverter). """ - _DEFAULT_CAPABILITIES: TargetCapabilities = TargetCapabilities(supports_multi_turn=True) + _DEFAULT_CONFIGURATION: TargetConfiguration = TargetConfiguration( + capabilities=TargetCapabilities(supports_multi_turn=True) + ) def __init__( self, @@ -46,7 +49,8 @@ def __init__( http2: Optional[bool] = None, callback_function: Callable[..., Any] | None = None, max_requests_per_minute: Optional[int] = None, - custom_capabilities: Optional["TargetCapabilities"] = None, + custom_configuration: Optional[TargetConfiguration] = None, + custom_capabilities: Optional[TargetCapabilities] = None, **httpx_client_kwargs: Any, ) -> None: """ @@ -64,9 +68,11 @@ def __init__( http2 (bool, Optional): Whether to use HTTP/2. If None, defaults to False. callback_function (Callable, Optional): Function to parse the HTTP response. max_requests_per_minute (int, Optional): Maximum number of requests per minute. - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for this target + custom_configuration (TargetConfiguration, Optional): Override the default configuration for this target instance. Defaults to None. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. **httpx_client_kwargs: Additional keyword arguments to pass to the httpx.AsyncClient constructor. Raises: @@ -79,6 +85,7 @@ def __init__( use_tls=True, callback_function=callback_function, max_requests_per_minute=max_requests_per_minute, + custom_configuration=custom_configuration, custom_capabilities=custom_capabilities, **httpx_client_kwargs, ) diff --git a/pyrit/prompt_target/hugging_face/hugging_face_chat_target.py b/pyrit/prompt_target/hugging_face/hugging_face_chat_target.py index 13152af775..ae02026004 100644 --- a/pyrit/prompt_target/hugging_face/hugging_face_chat_target.py +++ b/pyrit/prompt_target/hugging_face/hugging_face_chat_target.py @@ -21,6 +21,7 @@ from pyrit.models import Message, construct_response_from_request from pyrit.prompt_target.common.prompt_chat_target import PromptChatTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.prompt_target.common.utils import limit_requests_per_minute logger = logging.getLogger(__name__) @@ -32,10 +33,12 @@ class HuggingFaceChatTarget(PromptChatTarget): Inherits from PromptTarget to comply with the current design standards. """ - _DEFAULT_CAPABILITIES: TargetCapabilities = TargetCapabilities( - supports_multi_turn=True, - supports_editable_history=True, - supports_system_prompt=True, + _DEFAULT_CONFIGURATION: TargetConfiguration = TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=True, + supports_editable_history=True, + supports_system_prompt=True, + ) ) # Class-level cache for model and tokenizer @@ -67,6 +70,7 @@ def __init__( torch_dtype: Optional[Any] = None, attn_implementation: Optional[str] = None, max_requests_per_minute: Optional[int] = None, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, ) -> None: """ @@ -88,8 +92,10 @@ def __init__( torch_dtype (Optional[torch.dtype]): Torch data type for model weights. attn_implementation (Optional[str]): Attention implementation type. max_requests_per_minute (Optional[int]): The maximum number of requests per minute. Defaults to None. - custom_capabilities (Optional[TargetCapabilities]): Override the default capabilities for this target + custom_configuration (Optional[TargetConfiguration]): Override the default configuration for this target instance. Defaults to None + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. Raises: ValueError: If neither or both of `model_id` and `model_path` are provided. @@ -100,6 +106,7 @@ def __init__( super().__init__( max_requests_per_minute=max_requests_per_minute, model_name=model_name, + custom_configuration=custom_configuration, custom_capabilities=custom_capabilities, ) diff --git a/pyrit/prompt_target/hugging_face/hugging_face_endpoint_target.py b/pyrit/prompt_target/hugging_face/hugging_face_endpoint_target.py index 16cf311355..5c4e3d9371 100644 --- a/pyrit/prompt_target/hugging_face/hugging_face_endpoint_target.py +++ b/pyrit/prompt_target/hugging_face/hugging_face_endpoint_target.py @@ -9,6 +9,7 @@ from pyrit.models import Message, construct_response_from_request from pyrit.prompt_target.common.prompt_target import PromptTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.prompt_target.common.utils import limit_requests_per_minute, validate_temperature, validate_top_p logger = logging.getLogger(__name__) @@ -32,6 +33,7 @@ def __init__( top_p: float = 1.0, max_requests_per_minute: Optional[int] = None, verbose: bool = False, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, ) -> None: """ @@ -46,13 +48,16 @@ def __init__( top_p (float, Optional): The cumulative probability for nucleus sampling. Defaults to 1.0. max_requests_per_minute (Optional[int]): The maximum number of requests per minute. Defaults to None. verbose (bool, Optional): Flag to enable verbose logging. Defaults to False. - custom_capabilities (Optional[TargetCapabilities]): Custom capabilities for this target instance. + custom_configuration (Optional[TargetConfiguration]): Custom configuration for this target instance. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. """ super().__init__( max_requests_per_minute=max_requests_per_minute, verbose=verbose, endpoint=endpoint, model_name=model_id, + custom_configuration=custom_configuration, custom_capabilities=custom_capabilities, ) diff --git a/pyrit/prompt_target/openai/openai_chat_target.py b/pyrit/prompt_target/openai/openai_chat_target.py index ba0abf7cfa..277ca4c08c 100644 --- a/pyrit/prompt_target/openai/openai_chat_target.py +++ b/pyrit/prompt_target/openai/openai_chat_target.py @@ -26,6 +26,7 @@ from pyrit.models.json_response_config import _JsonResponseConfig from pyrit.prompt_target.common.prompt_chat_target import PromptChatTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration, resolve_configuration_compat from pyrit.prompt_target.common.utils import limit_requests_per_minute, validate_temperature, validate_top_p from pyrit.prompt_target.openai.openai_chat_audio_config import OpenAIChatAudioConfig from pyrit.prompt_target.openai.openai_target import OpenAITarget @@ -65,11 +66,13 @@ class OpenAIChatTarget(OpenAITarget, PromptChatTarget): """ - _DEFAULT_CAPABILITIES: TargetCapabilities = TargetCapabilities( - supports_multi_turn=True, - supports_json_output=True, - supports_multi_message_pieces=True, - supports_system_prompt=True, + _DEFAULT_CONFIGURATION: TargetConfiguration = TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=True, + supports_json_output=True, + supports_multi_message_pieces=True, + supports_system_prompt=True, + ) ) def __init__( @@ -86,6 +89,7 @@ def __init__( is_json_supported: bool = True, audio_response_config: Optional[OpenAIChatAudioConfig] = None, extra_body_parameters: Optional[dict[str, Any]] = None, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, **kwargs: Any, ) -> None: @@ -127,11 +131,13 @@ def __init__( setting the response_format header. Official OpenAI models all support this, but if you are using this target with different models, is_json_supported should be set correctly to avoid issues when using adversarial infrastructure (e.g. Crescendo scorers will set this flag). - This value is now deprecated in favor of `custom_capabilities`. + This value is now deprecated in favor of `custom_configuration`. audio_response_config (OpenAIChatAudioConfig, Optional): Configuration for audio output from models that support it (e.g., gpt-4o-audio-preview). When provided, enables audio modality in responses. extra_body_parameters (dict, Optional): Additional parameters to be included in the request body. - custom_capabilities (TargetCapabilities, Optional): Override the default target capabilities. + custom_configuration (TargetConfiguration, Optional): Override the default target configuration. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. **kwargs: Additional keyword arguments passed to the parent OpenAITarget class. httpx_client_kwargs (dict, Optional): Additional kwargs to be passed to the ``httpx.AsyncClient()`` constructor. For example, to specify a 3 minute timeout: ``httpx_client_kwargs={"timeout": 180}`` @@ -146,18 +152,25 @@ def __init__( json.JSONDecodeError: If the response from the target is not valid JSON. Exception: If the request fails for any other reason. """ - # Resolve capabilities: - # 1. Explicit custom_capabilities always wins. - # 2. If is_json_supported was explicitly set to False (deprecated), apply that override. - # 3. Otherwise, pass None so the parent can resolve via get_default_capabilities(underlying_model), + # Resolve configuration: + # 1. Resolve deprecated custom_capabilities into custom_configuration. + # 2. Explicit custom_configuration always wins. + # 3. If is_json_supported was explicitly set to False (deprecated), apply that override. + # 4. Otherwise, pass None so the parent can resolve via get_default_configuration(underlying_model), # which checks _KNOWN_CAPABILITIES (e.g., gpt-4o gets image input support). - if custom_capabilities is not None: - effective_capabilities: TargetCapabilities | None = custom_capabilities + custom_configuration = resolve_configuration_compat( + custom_configuration=custom_configuration, + custom_capabilities=custom_capabilities, + ) + if custom_configuration is not None: + effective_configuration: TargetConfiguration | None = custom_configuration elif not is_json_supported: - effective_capabilities = replace(type(self)._DEFAULT_CAPABILITIES, supports_json_output=False) + effective_configuration = TargetConfiguration( + capabilities=replace(type(self)._DEFAULT_CONFIGURATION.capabilities, supports_json_output=False) + ) else: - effective_capabilities = None - super().__init__(custom_capabilities=effective_capabilities, **kwargs) + effective_configuration = None + super().__init__(custom_configuration=effective_configuration, custom_capabilities=None, **kwargs) # Validate temperature and top_p validate_temperature(temperature) diff --git a/pyrit/prompt_target/openai/openai_completion_target.py b/pyrit/prompt_target/openai/openai_completion_target.py index 2718929c1d..b1ee2efe25 100644 --- a/pyrit/prompt_target/openai/openai_completion_target.py +++ b/pyrit/prompt_target/openai/openai_completion_target.py @@ -10,6 +10,7 @@ from pyrit.identifiers import ComponentIdentifier from pyrit.models import Message, construct_response_from_request from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.prompt_target.common.utils import limit_requests_per_minute from pyrit.prompt_target.openai.openai_target import OpenAITarget @@ -19,7 +20,7 @@ class OpenAICompletionTarget(OpenAITarget): """A prompt target for OpenAI completion endpoints.""" - _DEFAULT_CAPABILITIES: TargetCapabilities = TargetCapabilities() + _DEFAULT_CONFIGURATION: TargetConfiguration = TargetConfiguration(capabilities=TargetCapabilities()) def __init__( self, @@ -29,6 +30,7 @@ def __init__( presence_penalty: Optional[float] = None, frequency_penalty: Optional[float] = None, n: Optional[int] = None, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, *args: Any, **kwargs: Any, @@ -63,14 +65,18 @@ def __init__( tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. n (int, Optional): How many completions to generate for each prompt. - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for + custom_configuration (TargetConfiguration, Optional): Override the default configuration for this target instance. Defaults to None. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. *args: Variable length argument list passed to the parent class. **kwargs: Additional keyword arguments passed to the parent OpenAITarget class. httpx_client_kwargs (dict, Optional): Additional kwargs to be passed to the ``httpx.AsyncClient()`` constructor. For example, to specify a 3 minute timeout: ``httpx_client_kwargs={"timeout": 180}`` """ - super().__init__(*args, custom_capabilities=custom_capabilities, **kwargs) + super().__init__( + *args, custom_configuration=custom_configuration, custom_capabilities=custom_capabilities, **kwargs + ) self._max_tokens = max_tokens self._temperature = temperature diff --git a/pyrit/prompt_target/openai/openai_image_target.py b/pyrit/prompt_target/openai/openai_image_target.py index ededc85a0c..a99e01dd60 100644 --- a/pyrit/prompt_target/openai/openai_image_target.py +++ b/pyrit/prompt_target/openai/openai_image_target.py @@ -17,6 +17,7 @@ data_serializer_factory, ) from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.prompt_target.common.utils import limit_requests_per_minute from pyrit.prompt_target.openai.openai_target import OpenAITarget @@ -28,20 +29,22 @@ class OpenAIImageTarget(OpenAITarget): # Maximum number of image inputs supported by the OpenAI image API _MAX_INPUT_IMAGES = 16 - _DEFAULT_CAPABILITIES: TargetCapabilities = TargetCapabilities( - supports_multi_message_pieces=True, - input_modalities=frozenset( - { - frozenset(["text"]), - frozenset(["image_path"]), - frozenset(["text", "image_path"]), - } - ), - output_modalities=frozenset( - { - frozenset(["image_path"]), - } - ), + _DEFAULT_CONFIGURATION: TargetConfiguration = TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_message_pieces=True, + input_modalities=frozenset( + { + frozenset(["text"]), + frozenset(["image_path"]), + frozenset(["text", "image_path"]), + } + ), + output_modalities=frozenset( + { + frozenset(["image_path"]), + } + ), + ) ) def __init__( @@ -52,6 +55,7 @@ def __init__( output_format: Optional[Literal["png", "jpeg", "webp"]] = None, quality: Optional[Literal["standard", "hd", "low", "medium", "high"]] = None, style: Optional[Literal["natural", "vivid"]] = None, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, *args: Any, **kwargs: Any, @@ -91,8 +95,10 @@ def __init__( style (Literal["natural", "vivid"], Optional): The style of the generated images. This parameter is only supported for DALL-E-3. Default is to not specify. - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for + custom_configuration (TargetConfiguration, Optional): Override the default configuration for this target instance. Defaults to None. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. *args: Additional positional arguments to be passed to AzureOpenAITarget. **kwargs: Additional keyword arguments to be passed to AzureOpenAITarget. httpx_client_kwargs (dict, Optional): Additional kwargs to be passed to the @@ -104,7 +110,9 @@ def __init__( self.style = style self.image_size = image_size - super().__init__(*args, custom_capabilities=custom_capabilities, **kwargs) + super().__init__( + *args, custom_configuration=custom_configuration, custom_capabilities=custom_capabilities, **kwargs + ) def _set_openai_env_configuration_vars(self) -> None: self.model_name_environment_variable = "OPENAI_IMAGE_MODEL" diff --git a/pyrit/prompt_target/openai/openai_realtime_target.py b/pyrit/prompt_target/openai/openai_realtime_target.py index ffc8d7a2ca..b14a6e823e 100644 --- a/pyrit/prompt_target/openai/openai_realtime_target.py +++ b/pyrit/prompt_target/openai/openai_realtime_target.py @@ -23,6 +23,7 @@ ) from pyrit.prompt_target.common.prompt_chat_target import PromptChatTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.prompt_target.common.utils import limit_requests_per_minute from pyrit.prompt_target.openai.openai_target import OpenAITarget @@ -68,23 +69,25 @@ class RealtimeTarget(OpenAITarget, PromptChatTarget): and https://platform.openai.com/docs/guides/realtime-websocket """ - _DEFAULT_CAPABILITIES: TargetCapabilities = TargetCapabilities( - supports_multi_turn=True, - supports_multi_message_pieces=True, - supports_system_prompt=True, - input_modalities=frozenset( - { - frozenset(["text"]), - frozenset(["text", "audio_path"]), - } - ), - output_modalities=frozenset( - { - frozenset(["text"]), - frozenset(["audio_path"]), - frozenset(["text", "audio_path"]), - } - ), + _DEFAULT_CONFIGURATION: TargetConfiguration = TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=True, + supports_multi_message_pieces=True, + supports_system_prompt=True, + input_modalities=frozenset( + { + frozenset(["text"]), + frozenset(["text", "audio_path"]), + } + ), + output_modalities=frozenset( + { + frozenset(["text"]), + frozenset(["audio_path"]), + frozenset(["text", "audio_path"]), + } + ), + ) ) def __init__( @@ -92,6 +95,7 @@ def __init__( *, voice: Optional[RealTimeVoice] = None, existing_convo: Optional[dict[str, Any]] = None, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, **kwargs: Any, ) -> None: @@ -114,13 +118,15 @@ def __init__( voice (literal str, Optional): The voice to use. Defaults to None. the only supported voices by the AzureOpenAI Realtime API are "alloy", "echo", and "shimmer". existing_convo (dict[str, websockets.WebSocketClientProtocol], Optional): Existing conversations. - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for + custom_configuration (TargetConfiguration, Optional): Override the default configuration for this target instance. Defaults to None. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. **kwargs: Additional keyword arguments passed to the parent OpenAITarget class. httpx_client_kwargs (dict, Optional): Additional kwargs to be passed to the ``httpx.AsyncClient()`` constructor. For example, to specify a 3 minute timeout: ``httpx_client_kwargs={"timeout": 180}`` """ - super().__init__(custom_capabilities=custom_capabilities, **kwargs) + super().__init__(custom_configuration=custom_configuration, custom_capabilities=custom_capabilities, **kwargs) self.voice = voice self._existing_conversation = existing_convo if existing_convo is not None else {} diff --git a/pyrit/prompt_target/openai/openai_response_target.py b/pyrit/prompt_target/openai/openai_response_target.py index b41b40a30f..8e02ce7b42 100644 --- a/pyrit/prompt_target/openai/openai_response_target.py +++ b/pyrit/prompt_target/openai/openai_response_target.py @@ -30,6 +30,7 @@ from pyrit.models.json_response_config import _JsonResponseConfig from pyrit.prompt_target.common.prompt_chat_target import PromptChatTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.prompt_target.common.utils import limit_requests_per_minute, validate_temperature, validate_top_p from pyrit.prompt_target.openai.openai_error_handling import _is_content_filter_error from pyrit.prompt_target.openai.openai_target import OpenAITarget @@ -68,21 +69,23 @@ class OpenAIResponseTarget(OpenAITarget, PromptChatTarget): https://platform.openai.com/docs/api-reference/responses/create """ - _DEFAULT_CAPABILITIES: TargetCapabilities = TargetCapabilities( - supports_multi_turn=True, - supports_json_output=True, - supports_multi_message_pieces=True, - supports_system_prompt=True, - input_modalities=frozenset( - { - frozenset(["text"]), - frozenset(["text", "image_path"]), - frozenset(["function_call"]), - frozenset(["tool_call"]), - frozenset(["function_call_output"]), - frozenset(["reasoning"]), - } - ), + _DEFAULT_CONFIGURATION: TargetConfiguration = TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=True, + supports_json_output=True, + supports_multi_message_pieces=True, + supports_system_prompt=True, + input_modalities=frozenset( + { + frozenset(["text"]), + frozenset(["text", "image_path"]), + frozenset(["function_call"]), + frozenset(["tool_call"]), + frozenset(["function_call_output"]), + frozenset(["reasoning"]), + } + ), + ) ) def __init__( @@ -96,6 +99,7 @@ def __init__( reasoning_summary: Optional[Literal["auto", "concise", "detailed"]] = None, extra_body_parameters: Optional[dict[str, Any]] = None, fail_on_missing_function: bool = False, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, **kwargs: Any, ) -> None: @@ -136,8 +140,10 @@ def __init__( an unknown function or does not output a function; if False, return a structured error so we can wrap it as function_call_output and let the model potentially recover (e.g., pick another tool or ask for clarification). - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for + custom_configuration (TargetConfiguration, Optional): Override the default configuration for this target instance. Defaults to None. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. **kwargs: Additional keyword arguments passed to the parent OpenAITarget class. httpx_client_kwargs (dict, Optional): Additional kwargs to be passed to the ``httpx.AsyncClient()`` constructor. For example, to specify a 3 minute timeout: ``httpx_client_kwargs={"timeout": 180}`` @@ -153,7 +159,7 @@ def __init__( json.JSONDecodeError: If the response from the target is not valid JSON. Exception: If the request fails for any other reason. """ - super().__init__(custom_capabilities=custom_capabilities, **kwargs) + super().__init__(custom_configuration=custom_configuration, custom_capabilities=custom_capabilities, **kwargs) # Validate temperature and top_p validate_temperature(temperature) diff --git a/pyrit/prompt_target/openai/openai_target.py b/pyrit/prompt_target/openai/openai_target.py index 2c9a48fa21..4ec0ff6f65 100644 --- a/pyrit/prompt_target/openai/openai_target.py +++ b/pyrit/prompt_target/openai/openai_target.py @@ -31,6 +31,7 @@ from pyrit.models import Message, MessagePiece from pyrit.prompt_target.common.prompt_target import PromptTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.prompt_target.openai.openai_error_handling import ( _extract_error_payload, _extract_request_id_from_exception, @@ -52,7 +53,9 @@ class OpenAITarget(PromptTarget): """ ADDITIONAL_REQUEST_HEADERS: str = "OPENAI_ADDITIONAL_REQUEST_HEADERS" - _DEFAULT_CAPABILITIES: TargetCapabilities = TargetCapabilities(supports_multi_message_pieces=True) + _DEFAULT_CONFIGURATION: TargetConfiguration = TargetConfiguration( + capabilities=TargetCapabilities(supports_multi_message_pieces=True) + ) model_name_environment_variable: str endpoint_environment_variable: str @@ -71,6 +74,7 @@ def __init__( max_requests_per_minute: Optional[int] = None, httpx_client_kwargs: Optional[dict[str, Any]] = None, underlying_model: Optional[str] = None, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, ) -> None: """ @@ -99,8 +103,10 @@ def __init__( from the actual model. If not provided, will attempt to fetch from environment variable. If it is not there either, the identifier "model_name" attribute will use the model_name. Defaults to None. - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for + custom_configuration (TargetConfiguration, Optional): Override the default configuration for this target instance. If None, uses the class-level defaults. Defaults to None. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. Raises: ValueError: If no API key is provided and the endpoint is not an Azure endpoint. @@ -136,6 +142,7 @@ def __init__( endpoint=endpoint_value, model_name=self._model_name, underlying_model=underlying_model_value, + custom_configuration=custom_configuration, custom_capabilities=custom_capabilities, ) @@ -675,4 +682,4 @@ def is_json_response_supported(self) -> bool: Returns: bool: True if JSON response is supported, False otherwise. """ - return self._capabilities.supports_json_output + return self.capabilities.supports_json_output diff --git a/pyrit/prompt_target/openai/openai_tts_target.py b/pyrit/prompt_target/openai/openai_tts_target.py index fded3739f6..1ec69d3070 100644 --- a/pyrit/prompt_target/openai/openai_tts_target.py +++ b/pyrit/prompt_target/openai/openai_tts_target.py @@ -14,6 +14,7 @@ data_serializer_factory, ) from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.prompt_target.common.utils import limit_requests_per_minute from pyrit.prompt_target.openai.openai_target import OpenAITarget @@ -27,8 +28,10 @@ class OpenAITTSTarget(OpenAITarget): """A prompt target for OpenAI Text-to-Speech (TTS) endpoints.""" - _DEFAULT_CAPABILITIES: TargetCapabilities = TargetCapabilities( - output_modalities=frozenset({frozenset(["audio_path"])}), + _DEFAULT_CONFIGURATION: TargetConfiguration = TargetConfiguration( + capabilities=TargetCapabilities( + output_modalities=frozenset({frozenset(["audio_path"])}), + ) ) def __init__( @@ -38,6 +41,7 @@ def __init__( response_format: TTSResponseFormat = "mp3", language: str = "en", speed: Optional[float] = None, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, **kwargs: Any, ) -> None: @@ -60,13 +64,15 @@ def __init__( response_format (str, Optional): The format of the audio response. Defaults to "mp3". language (str): The language for TTS. Defaults to "en". speed (float, Optional): The speed of the TTS. Select a value from 0.25 to 4.0. 1.0 is normal. - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for + custom_configuration (TargetConfiguration, Optional): Override the default configuration for this target instance. Defaults to None. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. **kwargs: Additional keyword arguments passed to the parent OpenAITarget class. httpx_client_kwargs (dict, Optional): Additional kwargs to be passed to the ``httpx.AsyncClient()`` constructor. For example, to specify a 3 minute timeout: ``httpx_client_kwargs={"timeout": 180}`` """ - super().__init__(custom_capabilities=custom_capabilities, **kwargs) + super().__init__(custom_configuration=custom_configuration, custom_capabilities=custom_capabilities, **kwargs) self._voice = voice self._response_format = response_format diff --git a/pyrit/prompt_target/openai/openai_video_target.py b/pyrit/prompt_target/openai/openai_video_target.py index 2b0be3cefb..8a23e08565 100644 --- a/pyrit/prompt_target/openai/openai_video_target.py +++ b/pyrit/prompt_target/openai/openai_video_target.py @@ -20,6 +20,7 @@ data_serializer_factory, ) from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.prompt_target.common.utils import limit_requests_per_minute from pyrit.prompt_target.openai.openai_error_handling import _is_content_filter_error from pyrit.prompt_target.openai.openai_target import OpenAITarget @@ -52,11 +53,13 @@ class OpenAIVideoTarget(OpenAITarget): SUPPORTED_RESOLUTIONS: list[VideoSize] = ["720x1280", "1280x720", "1024x1792", "1792x1024"] SUPPORTED_DURATIONS: list[VideoSeconds] = ["4", "8", "12"] SUPPORTED_IMAGE_FORMATS: list[str] = ["image/jpeg", "image/png", "image/webp"] - _DEFAULT_CAPABILITIES: TargetCapabilities = TargetCapabilities( - supports_multi_turn=False, - supports_multi_message_pieces=True, - input_modalities=frozenset({frozenset(["text"]), frozenset(["text", "image_path"])}), - output_modalities=frozenset({frozenset(["video_path"])}), + _DEFAULT_CONFIGURATION: TargetConfiguration = TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=False, + supports_multi_message_pieces=True, + input_modalities=frozenset({frozenset(["text"]), frozenset(["text", "image_path"])}), + output_modalities=frozenset({frozenset(["video_path"])}), + ) ) def __init__( @@ -64,6 +67,7 @@ def __init__( *, resolution_dimensions: VideoSize = "1280x720", n_seconds: int | VideoSeconds = 4, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, **kwargs: Any, ) -> None: @@ -90,18 +94,20 @@ def __init__( n_seconds (int | VideoSeconds, Optional): The duration of the generated video. Accepts an int (4, 8, 12) or a VideoSeconds string ("4", "8", "12"). Defaults to 4. - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for + custom_configuration (TargetConfiguration, Optional): Override the default configuration for this target instance. Defaults to None. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. **kwargs: Additional keyword arguments passed to the parent OpenAITarget class. httpx_client_kwargs (dict, Optional): Additional kwargs to be passed to the ``httpx.AsyncClient()`` constructor. For example, to specify a 3 minute timeout: ``httpx_client_kwargs={"timeout": 180}`` - Remix workflow: + Remix workflow: To remix an existing video, set ``prompt_metadata={"video_id": ""}`` on the text MessagePiece. The video_id is returned in the response metadata after any successful - generation (``response.message_pieces[0].prompt_metadata["video_id"]``). + generation (``response.message_pieces[0].prompt_metadata["video_id"]""). """ - super().__init__(custom_capabilities=custom_capabilities, **kwargs) + super().__init__(custom_configuration=custom_configuration, custom_capabilities=custom_capabilities, **kwargs) self._n_seconds: VideoSeconds = ( cast("VideoSeconds", str(n_seconds)) if isinstance(n_seconds, int) else n_seconds diff --git a/pyrit/prompt_target/playwright_copilot_target.py b/pyrit/prompt_target/playwright_copilot_target.py index e1b987eb0f..de317ad7b9 100644 --- a/pyrit/prompt_target/playwright_copilot_target.py +++ b/pyrit/prompt_target/playwright_copilot_target.py @@ -19,6 +19,7 @@ from pyrit.models.literals import PromptDataType from pyrit.prompt_target.common.prompt_target import PromptTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration logger = logging.getLogger(__name__) @@ -79,22 +80,24 @@ class PlaywrightCopilotTarget(PromptTarget): # Supported data types SUPPORTED_DATA_TYPES = {"text", "image_path"} - _DEFAULT_CAPABILITIES: TargetCapabilities = TargetCapabilities( - supports_multi_turn=True, - supports_multi_message_pieces=True, - input_modalities=frozenset( - { - frozenset(["text"]), - frozenset(["text", "image_path"]), - } - ), - output_modalities=frozenset( - { - frozenset(["text"]), - frozenset(["image_path"]), - frozenset(["text", "image_path"]), - } - ), + _DEFAULT_CONFIGURATION: TargetConfiguration = TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=True, + supports_multi_message_pieces=True, + input_modalities=frozenset( + { + frozenset(["text"]), + frozenset(["text", "image_path"]), + } + ), + output_modalities=frozenset( + { + frozenset(["text"]), + frozenset(["image_path"]), + frozenset(["text", "image_path"]), + } + ), + ) ) # Placeholder text constants @@ -125,6 +128,7 @@ def __init__( *, page: "Page", copilot_type: CopilotType = CopilotType.CONSUMER, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, ) -> None: """ @@ -134,14 +138,16 @@ def __init__( page (Page): The Playwright page object for browser interaction. copilot_type (CopilotType): The type of Copilot to interact with. Defaults to CopilotType.CONSUMER. - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for + custom_configuration (TargetConfiguration, Optional): Override the default configuration for this target instance. Defaults to None. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. Raises: RuntimeError: If the Playwright page is not initialized. ValueError: If the page URL doesn't match the specified copilot_type. """ - super().__init__(custom_capabilities=custom_capabilities) + super().__init__(custom_configuration=custom_configuration, custom_capabilities=custom_capabilities) self._page = page self._type = copilot_type diff --git a/pyrit/prompt_target/playwright_target.py b/pyrit/prompt_target/playwright_target.py index c581b3b3c0..c97f5d803b 100644 --- a/pyrit/prompt_target/playwright_target.py +++ b/pyrit/prompt_target/playwright_target.py @@ -9,6 +9,7 @@ ) from pyrit.prompt_target.common.prompt_target import PromptTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.prompt_target.common.utils import limit_requests_per_minute # Avoid errors for users who don't have playwright installed @@ -52,15 +53,17 @@ class PlaywrightTarget(PromptTarget): # Supported data types SUPPORTED_DATA_TYPES = {"text", "image_path"} - _DEFAULT_CAPABILITIES: TargetCapabilities = TargetCapabilities( - supports_multi_turn=True, - supports_multi_message_pieces=True, - input_modalities=frozenset( - { - frozenset(["text"]), - frozenset(["text", "image_path"]), - } - ), + _DEFAULT_CONFIGURATION: TargetConfiguration = TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=True, + supports_multi_message_pieces=True, + input_modalities=frozenset( + { + frozenset(["text"]), + frozenset(["text", "image_path"]), + } + ), + ) ) def __init__( @@ -69,6 +72,7 @@ def __init__( interaction_func: InteractionFunction, page: "Page", max_requests_per_minute: Optional[int] = None, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, ) -> None: """ @@ -80,12 +84,17 @@ def __init__( max_requests_per_minute (int, Optional): Number of requests the target can handle per minute before hitting a rate limit. The number of requests sent to the target will be capped at the value provided. - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for + custom_configuration (TargetConfiguration, Optional): Override the default configuration for this target instance. Defaults to None. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. """ endpoint = page.url if page else "" super().__init__( - max_requests_per_minute=max_requests_per_minute, endpoint=endpoint, custom_capabilities=custom_capabilities + max_requests_per_minute=max_requests_per_minute, + endpoint=endpoint, + custom_configuration=custom_configuration, + custom_capabilities=custom_capabilities, ) self._interaction_func = interaction_func self._page = page diff --git a/pyrit/prompt_target/prompt_shield_target.py b/pyrit/prompt_target/prompt_shield_target.py index 21aaf8a2e5..8bb249f350 100644 --- a/pyrit/prompt_target/prompt_shield_target.py +++ b/pyrit/prompt_target/prompt_shield_target.py @@ -14,6 +14,7 @@ ) from pyrit.prompt_target.common.prompt_target import PromptTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration from pyrit.prompt_target.common.utils import limit_requests_per_minute logger = logging.getLogger(__name__) @@ -61,6 +62,7 @@ def __init__( api_version: Optional[str] = "2024-09-01", field: Optional[PromptShieldEntryField] = None, max_requests_per_minute: Optional[int] = None, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, ) -> None: """ @@ -82,8 +84,10 @@ def __init__( max_requests_per_minute (int, Optional): Number of requests the target can handle per minute before hitting a rate limit. The number of requests sent to the target will be capped at the value provided. - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for + custom_configuration (TargetConfiguration, Optional): Override the default configuration for this target instance. Defaults to None. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. """ endpoint_value = default_values.get_required_value( env_var_name=self.ENDPOINT_URI_ENVIRONMENT_VARIABLE, passed_value=endpoint @@ -91,6 +95,7 @@ def __init__( super().__init__( max_requests_per_minute=max_requests_per_minute, endpoint=endpoint_value, + custom_configuration=custom_configuration, custom_capabilities=custom_capabilities, ) diff --git a/pyrit/prompt_target/text_target.py b/pyrit/prompt_target/text_target.py index 01b29dfa19..4c736daf2d 100644 --- a/pyrit/prompt_target/text_target.py +++ b/pyrit/prompt_target/text_target.py @@ -10,6 +10,7 @@ from pyrit.models import Message, MessagePiece from pyrit.prompt_target.common.prompt_target import PromptTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration class TextTarget(PromptTarget): @@ -25,6 +26,7 @@ def __init__( self, *, text_stream: IO[str] = sys.stdout, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, ) -> None: """ @@ -32,10 +34,12 @@ def __init__( Args: text_stream (IO[str]): The text stream to write prompts to. Defaults to sys.stdout. - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for + custom_configuration (TargetConfiguration, Optional): Override the default configuration for this target instance. Defaults to None. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. """ - super().__init__(custom_capabilities=custom_capabilities) + super().__init__(custom_configuration=custom_configuration, custom_capabilities=custom_capabilities) self._text_stream = text_stream async def send_prompt_async(self, *, message: Message) -> list[Message]: diff --git a/pyrit/prompt_target/websocket_copilot_target.py b/pyrit/prompt_target/websocket_copilot_target.py index 9f8c4b0418..9b56078272 100644 --- a/pyrit/prompt_target/websocket_copilot_target.py +++ b/pyrit/prompt_target/websocket_copilot_target.py @@ -23,6 +23,7 @@ from pyrit.models import DataTypeSerializer, Message, MessagePiece, construct_response_from_request from pyrit.prompt_target import PromptTarget, limit_requests_per_minute from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration logger = logging.getLogger(__name__) @@ -73,15 +74,17 @@ class WebSocketCopilotTarget(PromptTarget): RESPONSE_TIMEOUT_SECONDS: int = 60 CONNECTION_TIMEOUT_SECONDS: int = 30 - _DEFAULT_CAPABILITIES: TargetCapabilities = TargetCapabilities( - supports_multi_turn=True, - supports_multi_message_pieces=True, - input_modalities=frozenset( - { - frozenset(["text"]), - frozenset(["text", "image_path"]), - } - ), + _DEFAULT_CONFIGURATION: TargetConfiguration = TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=True, + supports_multi_message_pieces=True, + input_modalities=frozenset( + { + frozenset(["text"]), + frozenset(["text", "image_path"]), + } + ), + ) ) def __init__( @@ -92,6 +95,7 @@ def __init__( model_name: str = "copilot", response_timeout_seconds: int = RESPONSE_TIMEOUT_SECONDS, authenticator: Optional[Union[CopilotAuthenticator, ManualCopilotAuthenticator]] = None, + custom_configuration: Optional[TargetConfiguration] = None, custom_capabilities: Optional[TargetCapabilities] = None, ) -> None: """ @@ -106,8 +110,10 @@ def __init__( authenticator (Optional[Union[CopilotAuthenticator, ManualCopilotAuthenticator]]): Authenticator instance. Supports both ``CopilotAuthenticator`` and ``ManualCopilotAuthenticator``. If None, a new ``CopilotAuthenticator`` instance will be created with default settings. - custom_capabilities (TargetCapabilities, Optional): Override the default capabilities for + custom_configuration (TargetConfiguration, Optional): Override the default configuration for this target instance. Defaults to None. + custom_capabilities (TargetCapabilities, Optional): **Deprecated.** Use + ``custom_configuration`` instead. Will be removed in v0.14.0. Raises: ValueError: If ``response_timeout_seconds`` is not a positive integer. @@ -130,6 +136,7 @@ def __init__( max_requests_per_minute=max_requests_per_minute, endpoint=self._websocket_base_url, model_name=model_name, + custom_configuration=custom_configuration, custom_capabilities=custom_capabilities, ) diff --git a/tests/integration/targets/test_openai_chat_target_integration.py b/tests/integration/targets/test_openai_chat_target_integration.py index 6e0bf7a4ad..dfac679704 100644 --- a/tests/integration/targets/test_openai_chat_target_integration.py +++ b/tests/integration/targets/test_openai_chat_target_integration.py @@ -19,6 +19,7 @@ from pyrit.models import MessagePiece from pyrit.prompt_target import OpenAIChatAudioConfig, OpenAIChatTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration # Path to sample audio file for testing SAMPLE_AUDIO_FILE = HOME_PATH / "assets" / "converted_audio.wav" @@ -88,10 +89,12 @@ async def test_openai_chat_target_audio_multi_turn(sqlite_instance, platform_ope target = OpenAIChatTarget( **platform_openai_audio_args, audio_response_config=audio_config, - custom_capabilities=TargetCapabilities( - input_modalities=frozenset( - {frozenset({"text", "audio_path"}), frozenset({"text"}), frozenset({"audio_path"})} - ), + custom_configuration=TargetConfiguration( + capabilities=TargetCapabilities( + input_modalities=frozenset( + {frozenset({"text", "audio_path"}), frozenset({"text"}), frozenset({"audio_path"})} + ), + ) ), ) diff --git a/tests/unit/target/test_azure_openai_completion_target.py b/tests/unit/target/test_azure_openai_completion_target.py index aa1c188358..9158449022 100644 --- a/tests/unit/target/test_azure_openai_completion_target.py +++ b/tests/unit/target/test_azure_openai_completion_target.py @@ -56,7 +56,7 @@ async def test_azure_completion_validate_request_length(azure_completion_target: with pytest.raises( ValueError, match="This target only supports a single message piece.*If your target does support this, set the" - " custom_capabilities parameter accordingly", + " custom_configuration parameter accordingly", ): await azure_completion_target.send_prompt_async(message=request) @@ -67,7 +67,7 @@ async def test_azure_completion_validate_prompt_type(azure_completion_target: Op with pytest.raises( ValueError, match="This target supports only the following data types.*If your target does support this, set the" - " custom_capabilities parameter accordingly", + " custom_configuration parameter accordingly", ): await azure_completion_target.send_prompt_async(message=request) diff --git a/tests/unit/target/test_conversation_normalization_pipeline.py b/tests/unit/target/test_conversation_normalization_pipeline.py index 77d69a7e4b..c0428cfdcb 100644 --- a/tests/unit/target/test_conversation_normalization_pipeline.py +++ b/tests/unit/target/test_conversation_normalization_pipeline.py @@ -115,13 +115,13 @@ def test_from_capabilities_normalizers_is_tuple(adapt_all_policy): def test_from_capabilities_raises_when_system_prompt_missing_and_policy_raise(raise_all_policy): caps = TargetCapabilities(supports_system_prompt=False, supports_multi_turn=True) - with pytest.raises(ValueError, match="RAISE"): + with pytest.raises(ValueError, match="system_prompt"): ConversationNormalizationPipeline.from_capabilities(capabilities=caps, policy=raise_all_policy) def test_from_capabilities_raises_when_multi_turn_missing_and_policy_raise(raise_all_policy): caps = TargetCapabilities(supports_system_prompt=True, supports_multi_turn=False) - with pytest.raises(ValueError, match="RAISE"): + with pytest.raises(ValueError, match="multi_turn"): ConversationNormalizationPipeline.from_capabilities(capabilities=caps, policy=raise_all_policy) diff --git a/tests/unit/target/test_gandalf_target.py b/tests/unit/target/test_gandalf_target.py index 4eb326f0fe..6a76b3157d 100644 --- a/tests/unit/target/test_gandalf_target.py +++ b/tests/unit/target/test_gandalf_target.py @@ -35,7 +35,7 @@ async def test_gandalf_validate_request_length(gandalf_target: GandalfTarget): with pytest.raises( ValueError, match="This target only supports a single message piece.*If your target does support this, set the" - " custom_capabilities parameter accordingly", + " custom_configuration parameter accordingly", ): await gandalf_target.send_prompt_async(message=request) @@ -46,6 +46,6 @@ async def test_gandalf_validate_prompt_type(gandalf_target: GandalfTarget): with pytest.raises( ValueError, match="This target supports only the following data types.*If your target does support this, set the" - " custom_capabilities parameter accordingly", + " custom_configuration parameter accordingly", ): await gandalf_target.send_prompt_async(message=request) diff --git a/tests/unit/target/test_image_target.py b/tests/unit/target/test_image_target.py index 064930b853..0d495535c3 100644 --- a/tests/unit/target/test_image_target.py +++ b/tests/unit/target/test_image_target.py @@ -16,6 +16,7 @@ from pyrit.models import Message, MessagePiece from pyrit.prompt_target import OpenAIImageTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration @pytest.fixture @@ -24,16 +25,18 @@ def image_target(patch_central_database) -> OpenAIImageTarget: model_name="dall-e-3", endpoint="test", api_key="test", - custom_capabilities=TargetCapabilities( - supports_multi_turn=False, - supports_multi_message_pieces=True, - input_modalities=frozenset( - { - frozenset(["text"]), - frozenset(["text", "image_path"]), - } - ), - output_modalities=frozenset({frozenset(["image_path"])}), + custom_configuration=TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=False, + supports_multi_message_pieces=True, + input_modalities=frozenset( + { + frozenset(["text"]), + frozenset(["text", "image_path"]), + } + ), + output_modalities=frozenset({frozenset(["image_path"])}), + ) ), ) @@ -526,6 +529,6 @@ async def test_validate_previous_conversations( with pytest.raises( ValueError, match="This target only supports a single turn conversation.*If your target does support this, set the" - " custom_capabilities parameter accordingly", + " custom_configuration parameter accordingly", ): await image_target.send_prompt_async(message=request) diff --git a/tests/unit/target/test_openai_chat_target.py b/tests/unit/target/test_openai_chat_target.py index 56f61b8733..7f05f8d83f 100644 --- a/tests/unit/target/test_openai_chat_target.py +++ b/tests/unit/target/test_openai_chat_target.py @@ -35,6 +35,7 @@ PromptChatTarget, ) from pyrit.prompt_target.common.target_capabilities import TargetCapabilities +from pyrit.prompt_target.common.target_configuration import TargetConfiguration def fake_construct_response_from_request(request, response_text_pieces): @@ -261,16 +262,18 @@ async def test_send_prompt_async_empty_response_adds_to_memory(openai_response_j model_name="gpt-o", endpoint="https://mock.azure.com/", api_key="mock-api-key", - custom_capabilities=TargetCapabilities( - supports_multi_turn=True, - supports_json_output=True, - supports_multi_message_pieces=True, - input_modalities=frozenset( - { - frozenset(["text"]), - frozenset(["text", "image_path"]), - } - ), + custom_configuration=TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=True, + supports_json_output=True, + supports_multi_message_pieces=True, + input_modalities=frozenset( + { + frozenset(["text"]), + frozenset(["text", "image_path"]), + } + ), + ) ), ) mock_memory = MagicMock() @@ -377,16 +380,18 @@ async def test_send_prompt_async(openai_response_json: dict, patch_central_datab model_name="gpt-o", endpoint="https://mock.azure.com/", api_key="mock-api-key", - custom_capabilities=TargetCapabilities( - supports_multi_turn=True, - supports_json_output=True, - supports_multi_message_pieces=True, - input_modalities=frozenset( - { - frozenset(["text"]), - frozenset(["text", "image_path"]), - } - ), + custom_configuration=TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=True, + supports_json_output=True, + supports_multi_message_pieces=True, + input_modalities=frozenset( + { + frozenset(["text"]), + frozenset(["text", "image_path"]), + } + ), + ) ), ) with NamedTemporaryFile(suffix=".jpg", delete=False) as tmp_file: @@ -441,16 +446,18 @@ async def test_send_prompt_async_empty_response_retries(openai_response_json: di model_name="gpt-o", endpoint="https://mock.azure.com/", api_key="mock-api-key", - custom_capabilities=TargetCapabilities( - supports_multi_turn=True, - supports_json_output=True, - supports_multi_message_pieces=True, - input_modalities=frozenset( - { - frozenset(["text"]), - frozenset(["text", "image_path"]), - } - ), + custom_configuration=TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=True, + supports_json_output=True, + supports_multi_message_pieces=True, + input_modalities=frozenset( + { + frozenset(["text"]), + frozenset(["text", "image_path"]), + } + ), + ) ), ) with NamedTemporaryFile(suffix=".jpg", delete=False) as tmp_file: diff --git a/tests/unit/target/test_playwright_copilot_target.py b/tests/unit/target/test_playwright_copilot_target.py index 97d68a7942..8a5aaf1d9c 100644 --- a/tests/unit/target/test_playwright_copilot_target.py +++ b/tests/unit/target/test_playwright_copilot_target.py @@ -153,7 +153,7 @@ def test_validate_request_unsupported_type(self, mock_page): with pytest.raises( ValueError, match=r"This target supports only the following data types.*If your target does support this, set the" - r" custom_capabilities parameter accordingly", + r" custom_configuration parameter accordingly", ): target._validate_request(message=request) diff --git a/tests/unit/target/test_playwright_target.py b/tests/unit/target/test_playwright_target.py index 3720be4179..4df935f09e 100644 --- a/tests/unit/target/test_playwright_target.py +++ b/tests/unit/target/test_playwright_target.py @@ -127,7 +127,7 @@ def test_validate_request_unsupported_type(self, mock_interaction_func, mock_pag with pytest.raises( ValueError, match=r"This target supports only the following data types.*If your target does support this, set the" - r" custom_capabilities parameter accordingly", + r" custom_configuration parameter accordingly", ): target._validate_request(message=request) @@ -343,7 +343,7 @@ def test_validate_request_multiple_unsupported_types(self, mock_interaction_func with pytest.raises( ValueError, match=r"This target supports only the following data types.*If your target does support this, set the" - r" custom_capabilities parameter accordingly", + r" custom_configuration parameter accordingly", ): target._validate_request(message=request) diff --git a/tests/unit/target/test_prompt_shield_target.py b/tests/unit/target/test_prompt_shield_target.py index a474ed9e0c..37cfdfd7ae 100644 --- a/tests/unit/target/test_prompt_shield_target.py +++ b/tests/unit/target/test_prompt_shield_target.py @@ -64,7 +64,7 @@ async def test_prompt_shield_validate_request_length(promptshield_target: Prompt with pytest.raises( ValueError, match="This target only supports a single message piece.*If your target does support this, set the" - " custom_capabilities parameter accordingly", + " custom_configuration parameter accordingly", ): await promptshield_target.send_prompt_async(message=request) diff --git a/tests/unit/target/test_prompt_target_azure_blob_storage.py b/tests/unit/target/test_prompt_target_azure_blob_storage.py index 73d4dabfbd..effc4ba854 100644 --- a/tests/unit/target/test_prompt_target_azure_blob_storage.py +++ b/tests/unit/target/test_prompt_target_azure_blob_storage.py @@ -72,7 +72,7 @@ async def test_azure_blob_storage_validate_request_length( with pytest.raises( ValueError, match="This target only supports a single message piece.*If your target does support this, set the" - " custom_capabilities parameter accordingly", + " custom_configuration parameter accordingly", ): await azure_blob_storage_target.send_prompt_async(message=request) @@ -88,7 +88,7 @@ async def test_azure_blob_storage_validate_prompt_type( with pytest.raises( ValueError, match="This target supports only the following data types.*If your target does support this, set the" - " custom_capabilities parameter accordingly", + " custom_configuration parameter accordingly", ): await azure_blob_storage_target.send_prompt_async(message=request) @@ -108,7 +108,7 @@ async def test_azure_blob_storage_validate_prev_convs( with pytest.raises( ValueError, match="This target only supports a single turn conversation.*If your target does support this, set the" - " custom_capabilities parameter accordingly", + " custom_configuration parameter accordingly", ): await azure_blob_storage_target.send_prompt_async(message=request) diff --git a/tests/unit/target/test_supports_multi_turn.py b/tests/unit/target/test_supports_multi_turn.py index f27b717958..25447fd2bd 100644 --- a/tests/unit/target/test_supports_multi_turn.py +++ b/tests/unit/target/test_supports_multi_turn.py @@ -20,11 +20,11 @@ class TestSupportsMultiTurn: """Test supports_multi_turn capability flag across the target hierarchy.""" def test_prompt_target_defaults_to_false(self): - # PromptTarget is abstract, so we verify via the class default capabilities + # PromptTarget is abstract, so we verify via the class default configuration from pyrit.prompt_target import PromptTarget, TargetCapabilities - assert TargetCapabilities() == PromptTarget._DEFAULT_CAPABILITIES - assert PromptTarget._DEFAULT_CAPABILITIES.supports_multi_turn is False + assert TargetCapabilities() == PromptTarget._DEFAULT_CONFIGURATION.capabilities + assert PromptTarget._DEFAULT_CONFIGURATION.capabilities.supports_multi_turn is False def test_prompt_chat_target_returns_true(self): target = MockPromptTarget() @@ -90,8 +90,8 @@ def test_text_target_returns_false(self): assert target.capabilities.supports_multi_turn is False def test_constructor_override_supports_multi_turn(self): - """Test that capabilities can be overridden via the constructor.""" - from pyrit.prompt_target import OpenAIChatTarget, TargetCapabilities + """Test that configuration can be overridden via the constructor.""" + from pyrit.prompt_target import OpenAIChatTarget, TargetCapabilities, TargetConfiguration # By default, chat targets support multi-turn target = OpenAIChatTarget( @@ -106,13 +106,13 @@ def test_constructor_override_supports_multi_turn(self): model_name="test-model", endpoint="https://mock.azure.com/", api_key="mock-api-key", - custom_capabilities=TargetCapabilities(supports_multi_turn=False), + custom_configuration=TargetConfiguration(capabilities=TargetCapabilities(supports_multi_turn=False)), ) assert target.capabilities.supports_multi_turn is False def test_constructor_override_single_turn_to_multi(self): """Test that a single-turn target can be overridden to multi-turn.""" - from pyrit.prompt_target import OpenAIImageTarget, TargetCapabilities + from pyrit.prompt_target import OpenAIImageTarget, TargetCapabilities, TargetConfiguration target = OpenAIImageTarget( model_name="dall-e-3", @@ -125,7 +125,7 @@ def test_constructor_override_single_turn_to_multi(self): model_name="dall-e-3", endpoint="https://mock.azure.com/", api_key="mock-api-key", - custom_capabilities=TargetCapabilities(supports_multi_turn=True), + custom_configuration=TargetConfiguration(capabilities=TargetCapabilities(supports_multi_turn=True)), ) assert target.capabilities.supports_multi_turn is True @@ -144,13 +144,13 @@ def test_capabilities_property_returns_target_capabilities(self): def test_capabilities_override_via_constructor(self): """Test that capabilities are correctly overridden via the constructor.""" - from pyrit.prompt_target import OpenAIChatTarget, TargetCapabilities + from pyrit.prompt_target import OpenAIChatTarget, TargetCapabilities, TargetConfiguration target = OpenAIChatTarget( model_name="test-model", endpoint="https://mock.azure.com/", api_key="mock-api-key", - custom_capabilities=TargetCapabilities(supports_multi_turn=False), + custom_configuration=TargetConfiguration(capabilities=TargetCapabilities(supports_multi_turn=False)), ) caps = target.capabilities assert isinstance(caps, TargetCapabilities) diff --git a/tests/unit/target/test_target_capabilities.py b/tests/unit/target/test_target_capabilities.py index df33a4f073..7cd58793a2 100644 --- a/tests/unit/target/test_target_capabilities.py +++ b/tests/unit/target/test_target_capabilities.py @@ -12,6 +12,7 @@ TargetCapabilities, UnsupportedCapabilityBehavior, ) +from pyrit.prompt_target.common.target_configuration import TargetConfiguration class TestCapabilityHandlingPolicy: @@ -100,7 +101,7 @@ def test_target_capabilities_includes_helper(self): # Env vars that may leak from .env files loaded by other tests in parallel workers. -# Clear them so that targets use _DEFAULT_CAPABILITIES instead of _KNOWN_CAPABILITIES. +# Clear them so that targets use _DEFAULT_CONFIGURATION instead of _KNOWN_CAPABILITIES. _CLEAN_UNDERLYING_MODEL_ENV = { "OPENAI_VIDEO_UNDERLYING_MODEL": "", "OPENAI_REALTIME_UNDERLYING_MODEL": "", @@ -112,8 +113,8 @@ def test_target_capabilities_includes_helper(self): } -class TestDefaultCapabilitiesDefined: - """Verify that every concrete PromptTarget subclass defines _DEFAULT_CAPABILITIES.""" +class TestDefaultConfigurationDefined: + """Verify that every concrete PromptTarget subclass defines _DEFAULT_CONFIGURATION.""" def _all_concrete_target_classes(self): from pyrit.prompt_target import ( @@ -160,21 +161,21 @@ def _all_concrete_target_classes(self): WebSocketCopilotTarget, ] - def test_all_targets_have_default_capabilities(self): - """Every concrete target must have _DEFAULT_CAPABILITIES as a TargetCapabilities instance.""" + def test_all_targets_have_default_configuration(self): + """Every concrete target must have _DEFAULT_CONFIGURATION as a TargetConfiguration instance.""" for cls in self._all_concrete_target_classes(): - assert hasattr(cls, "_DEFAULT_CAPABILITIES"), ( - f"{cls.__name__} is missing _DEFAULT_CAPABILITIES class attribute" + assert hasattr(cls, "_DEFAULT_CONFIGURATION"), ( + f"{cls.__name__} is missing _DEFAULT_CONFIGURATION class attribute" ) - assert isinstance(cls._DEFAULT_CAPABILITIES, TargetCapabilities), ( - f"{cls.__name__}._DEFAULT_CAPABILITIES must be a TargetCapabilities instance, " - f"got {type(cls._DEFAULT_CAPABILITIES)}" + assert isinstance(cls._DEFAULT_CONFIGURATION, TargetConfiguration), ( + f"{cls.__name__}._DEFAULT_CONFIGURATION must be a TargetConfiguration instance, " + f"got {type(cls._DEFAULT_CONFIGURATION)}" ) @pytest.mark.usefixtures("patch_central_database") class TestTargetCapabilitiesModalities: - """Test that each target declares the correct input/output modalities via _DEFAULT_CAPABILITIES.""" + """Test that each target declares the correct input/output modalities via _DEFAULT_CONFIGURATION.""" def test_default_capabilities_are_text_only(self): caps = TargetCapabilities() @@ -330,7 +331,7 @@ def test_websocket_copilot_target_modalities(self): def test_hugging_face_chat_target_capabilities(self): from pyrit.prompt_target import HuggingFaceChatTarget - caps = HuggingFaceChatTarget._DEFAULT_CAPABILITIES + caps = HuggingFaceChatTarget._DEFAULT_CONFIGURATION.capabilities assert caps.supports_editable_history is True assert caps.supports_multi_turn is True assert caps.supports_system_prompt is True @@ -370,19 +371,21 @@ def test_prompt_chat_targets_support_system_prompt(self): assert openai_response_target.capabilities.supports_system_prompt is True assert realtime_target.capabilities.supports_system_prompt is True - def test_custom_capabilities_override_modalities(self): - from pyrit.prompt_target import OpenAIChatTarget, TargetCapabilities + def test_custom_configuration_override_modalities(self): + from pyrit.prompt_target import OpenAIChatTarget, TargetCapabilities, TargetConfiguration - custom = TargetCapabilities( - supports_multi_turn=True, - input_modalities=frozenset({frozenset(["text"])}), - output_modalities=frozenset({frozenset(["text"])}), + custom = TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=True, + input_modalities=frozenset({frozenset(["text"])}), + output_modalities=frozenset({frozenset(["text"])}), + ) ) target = OpenAIChatTarget( model_name="test-model", endpoint="https://mock.azure.com/", api_key="mock-api-key", - custom_capabilities=custom, + custom_configuration=custom, ) assert target.capabilities.input_modalities == frozenset({frozenset(["text"])}) assert target.capabilities.output_modalities == frozenset({frozenset(["text"])}) @@ -479,16 +482,16 @@ def test_empty_string_returns_none(self): @pytest.mark.usefixtures("patch_central_database") -class TestGetDefaultCapabilities: - """Test PromptTarget.get_default_capabilities classmethod.""" +class TestGetDefaultConfiguration: + """Test PromptTarget.get_default_configuration classmethod.""" - def _make_target_class(self, *, default_caps: "TargetCapabilities"): - """Create a minimal concrete PromptTarget subclass with the given _DEFAULT_CAPABILITIES.""" + def _make_target_class(self, *, default_config: "TargetConfiguration"): + """Create a minimal concrete PromptTarget subclass with the given _DEFAULT_CONFIGURATION.""" from pyrit.models import Message from pyrit.prompt_target.common.prompt_target import PromptTarget class _ConcreteTarget(PromptTarget): - _DEFAULT_CAPABILITIES = default_caps + _DEFAULT_CONFIGURATION = default_config async def send_prompt_async(self, *, message: Message) -> list[Message]: return [] @@ -496,47 +499,49 @@ async def send_prompt_async(self, *, message: Message) -> list[Message]: return _ConcreteTarget def test_returns_class_default_when_underlying_model_is_none(self): - custom_caps = TargetCapabilities(supports_editable_history=True) - cls = self._make_target_class(default_caps=custom_caps) - result = cls.get_default_capabilities(None) - assert result is custom_caps - - def test_returns_known_caps_when_model_is_recognized(self): - custom_caps = TargetCapabilities() - cls = self._make_target_class(default_caps=custom_caps) - result = cls.get_default_capabilities("gpt-4o") + custom_config = TargetConfiguration(capabilities=TargetCapabilities(supports_editable_history=True)) + cls = self._make_target_class(default_config=custom_config) + result = cls.get_default_configuration(None) + assert result is custom_config + + def test_returns_known_config_when_model_is_recognized(self): + custom_config = TargetConfiguration(capabilities=TargetCapabilities()) + cls = self._make_target_class(default_config=custom_config) + result = cls.get_default_configuration("gpt-4o") expected = TargetCapabilities.get_known_capabilities("gpt-4o") - assert result == expected + assert result.capabilities == expected def test_returns_class_default_and_warns_when_model_is_unrecognized(self): - custom_caps = TargetCapabilities(supports_multi_turn=True) - cls = self._make_target_class(default_caps=custom_caps) + custom_config = TargetConfiguration(capabilities=TargetCapabilities(supports_multi_turn=True)) + cls = self._make_target_class(default_config=custom_config) with patch("pyrit.prompt_target.common.prompt_target.logger") as mock_logger: - result = cls.get_default_capabilities("totally-unknown-model") + result = cls.get_default_configuration("totally-unknown-model") mock_logger.info.assert_called_once() warning_args = mock_logger.info.call_args[0] assert "totally-unknown-model" in warning_args[1] - assert result is custom_caps + assert result is custom_config - def test_subclass_default_caps_not_overridden_by_parent_default(self): - custom_caps = TargetCapabilities(supports_json_output=True, supports_multi_turn=True) - cls = self._make_target_class(default_caps=custom_caps) - result = cls.get_default_capabilities(None) - assert result.supports_json_output is True - assert result.supports_multi_turn is True + def test_subclass_default_config_not_overridden_by_parent_default(self): + custom_config = TargetConfiguration( + capabilities=TargetCapabilities(supports_json_output=True, supports_multi_turn=True) + ) + cls = self._make_target_class(default_config=custom_config) + result = cls.get_default_configuration(None) + assert result.capabilities.supports_json_output is True + assert result.capabilities.supports_multi_turn is True def test_recognized_model_overrides_class_default(self): # Class has a minimal default; recognized model should override it - minimal_caps = TargetCapabilities() - cls = self._make_target_class(default_caps=minimal_caps) - result = cls.get_default_capabilities("tts") - assert result.output_modalities == frozenset({frozenset(["audio_path"])}) + minimal_config = TargetConfiguration(capabilities=TargetCapabilities()) + cls = self._make_target_class(default_config=minimal_config) + result = cls.get_default_configuration("tts") + assert result.capabilities.output_modalities == frozenset({frozenset(["audio_path"])}) def test_prompt_chat_target_preserves_system_prompt_for_recognized_model(self): from pyrit.prompt_target.common.prompt_chat_target import PromptChatTarget - result = PromptChatTarget.get_default_capabilities("gpt-4o") + result = PromptChatTarget.get_default_configuration("gpt-4o") - assert result.supports_multi_turn is True - assert result.supports_multi_message_pieces is True - assert result.supports_system_prompt is True + assert result.capabilities.supports_multi_turn is True + assert result.capabilities.supports_multi_message_pieces is True + assert result.capabilities.supports_system_prompt is True diff --git a/tests/unit/target/test_target_configuration.py b/tests/unit/target/test_target_configuration.py index df0dbe3d62..b7e4e4c085 100644 --- a/tests/unit/target/test_target_configuration.py +++ b/tests/unit/target/test_target_configuration.py @@ -71,7 +71,7 @@ def test_init_missing_capability_adapt_builds_pipeline(adapt_all_policy): def test_init_missing_capability_raise_policy_raises(): caps = TargetCapabilities(supports_multi_turn=False, supports_system_prompt=True) - with pytest.raises(ValueError, match="RAISE"): + with pytest.raises(ValueError, match="multi_turn"): TargetConfiguration(capabilities=caps) diff --git a/tests/unit/target/test_tts_target.py b/tests/unit/target/test_tts_target.py index e81498878e..ec595b55b4 100644 --- a/tests/unit/target/test_tts_target.py +++ b/tests/unit/target/test_tts_target.py @@ -68,7 +68,7 @@ async def test_tts_validate_request_length(tts_target: OpenAITTSTarget): with pytest.raises( ValueError, match="This target only supports a single message piece.*If your target does support this, set the" - " custom_capabilities parameter accordingly", + " custom_configuration parameter accordingly", ): await tts_target.send_prompt_async(message=request) @@ -79,7 +79,7 @@ async def test_tts_validate_prompt_type(tts_target: OpenAITTSTarget): with pytest.raises( ValueError, match="This target supports only the following data types.*If your target does support this, set the" - " custom_capabilities parameter accordingly", + " custom_configuration parameter accordingly", ): await tts_target.send_prompt_async(message=request) @@ -103,7 +103,7 @@ async def test_tts_validate_previous_conversations( with pytest.raises( ValueError, match="This target only supports a single turn conversation.*If your target does support this, set the" - " custom_capabilities parameter accordingly", + " custom_configuration parameter accordingly", ): await tts_target.send_prompt_async(message=request) diff --git a/tests/unit/target/test_video_target.py b/tests/unit/target/test_video_target.py index 8eabcd2427..faba7830a0 100644 --- a/tests/unit/target/test_video_target.py +++ b/tests/unit/target/test_video_target.py @@ -13,18 +13,21 @@ from pyrit.models import Message, MessagePiece from pyrit.prompt_target import OpenAIVideoTarget from pyrit.prompt_target.common.target_capabilities import TargetCapabilities - -_VIDEO_PATH_CAPABILITIES = TargetCapabilities( - supports_multi_turn=False, - supports_multi_message_pieces=True, - input_modalities=frozenset( - { - frozenset(["text"]), - frozenset(["text", "image_path"]), - frozenset(["text", "video_path"]), - } - ), - output_modalities=frozenset({frozenset(["video_path"])}), +from pyrit.prompt_target.common.target_configuration import TargetConfiguration + +_VIDEO_PATH_CONFIGURATION = TargetConfiguration( + capabilities=TargetCapabilities( + supports_multi_turn=False, + supports_multi_message_pieces=True, + input_modalities=frozenset( + { + frozenset(["text"]), + frozenset(["text", "image_path"]), + frozenset(["text", "video_path"]), + } + ), + output_modalities=frozenset({frozenset(["video_path"])}), + ) ) @@ -437,7 +440,7 @@ def test_validate_rejects_unsupported_types(self, video_target: OpenAIVideoTarge with pytest.raises( ValueError, match="This target supports only the following data types.*If your target does support this, set the" - " custom_capabilities parameter accordingly", + " custom_configuration parameter accordingly", ): video_target._validate_request(message=Message([msg_text, msg_audio])) @@ -550,7 +553,7 @@ def video_target(self) -> OpenAIVideoTarget: endpoint="https://api.openai.com/v1", api_key="test", model_name="sora-2", - custom_capabilities=_VIDEO_PATH_CAPABILITIES, + custom_configuration=_VIDEO_PATH_CONFIGURATION, ) @pytest.mark.asyncio @@ -1001,7 +1004,7 @@ def test_video_validate_previous_conversations( with pytest.raises( ValueError, match="This target only supports a single turn conversation.*If your target does support this, set the" - " custom_capabilities parameter accordingly", + " custom_configuration parameter accordingly", ): video_target._validate_request(message=request) @@ -1016,7 +1019,7 @@ def video_target(self) -> OpenAIVideoTarget: endpoint="https://api.openai.com/v1", api_key="test", model_name="sora-2", - custom_capabilities=_VIDEO_PATH_CAPABILITIES, + custom_configuration=_VIDEO_PATH_CONFIGURATION, ) def test_validate_accepts_text_and_video_path(self, video_target: OpenAIVideoTarget) -> None: diff --git a/tests/unit/target/test_websocket_copilot_target.py b/tests/unit/target/test_websocket_copilot_target.py index 5c0a8880ff..c6b6ce6c01 100644 --- a/tests/unit/target/test_websocket_copilot_target.py +++ b/tests/unit/target/test_websocket_copilot_target.py @@ -917,6 +917,6 @@ async def test_send_prompt_async_calls_validation(self, mock_authenticator, make with pytest.raises( ValueError, match="This target supports only the following data types.*If your target does support this, set the" - " custom_capabilities parameter accordingly", + " custom_configuration parameter accordingly", ): await target.send_prompt_async(message=message)