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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions pyrit/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"""
Common utilities and helpers for PyRIT.
Heavy submodules (data_url_converter, display_response, download_hf_model,
net_utility) are intentionally NOT re-exported here to keep ``import pyrit``
fast. Import them directly, e.g.::
Heavy submodules (display_response, download_hf_model, net_utility) are
intentionally NOT re-exported here to keep ``import pyrit`` fast. Import them
directly, e.g.::
from pyrit.common.net_utility import get_httpx_client
"""
Expand Down
91 changes: 34 additions & 57 deletions pyrit/common/data_url_converter.py
Original file line number Diff line number Diff line change
@@ -1,60 +1,37 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

from pyrit.common.deprecation import print_deprecation_message
from pyrit.memory import DataTypeSerializer, data_serializer_factory

# Supported image formats for Azure OpenAI GPT-4o,
# https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/use-your-image-data
AZURE_OPENAI_GPT4O_SUPPORTED_IMAGE_FORMATS = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif"]


async def convert_local_image_to_data_url_async(image_path: str) -> str:
"""
Convert a local image file to a data URL encoded in base64.

Args:
image_path (str): The file system path to the image file.

Returns:
str: A string containing the MIME type and the base64-encoded data of the image, formatted as a data URL.

Raises:
FileNotFoundError: If no file is found at the specified `image_path`.
ValueError: If the image file's extension is not in the supported formats list.
"""
ext = DataTypeSerializer.get_extension(image_path)
if ext is None or ext.lower() not in AZURE_OPENAI_GPT4O_SUPPORTED_IMAGE_FORMATS:
raise ValueError(
f"Unsupported image format: {ext}. Supported formats are: {AZURE_OPENAI_GPT4O_SUPPORTED_IMAGE_FORMATS}"
)

mime_type = DataTypeSerializer.get_mime_type(image_path)
if not mime_type:
mime_type = "application/octet-stream"

image_serializer = data_serializer_factory(
category="prompt-memory-entries", value=image_path, data_type="image_path", extension=ext
)
base64_encoded_data = await image_serializer.read_data_base64_async()
# Azure OpenAI documentation doesn't specify the local image upload format for API.
# GPT-4o image upload format is determined using "view code" functionality in Azure OpenAI deployments
# The image upload format is same as GPT-4 Turbo.
# Construct the data URL, as per Azure OpenAI GPT-4 Turbo local image format
# https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/gpt-with-vision?tabs=rest%2Csystem-assigned%2Cresource#call-the-chat-completion-apis
return f"data:{mime_type};base64,{base64_encoded_data}"


async def convert_local_image_to_data_url(image_path: str) -> str: # pyrit-async-suffix-exempt
"""
Delegate to ``convert_local_image_to_data_url_async`` (deprecated alias).

Returns:
str: A string containing the MIME type and the base64-encoded data of the image, formatted as a data URL.
"""
print_deprecation_message(
old_item="pyrit.common.data_url_converter.convert_local_image_to_data_url",
new_item="pyrit.common.data_url_converter.convert_local_image_to_data_url_async",
removed_in="0.16.0",
)
return await convert_local_image_to_data_url_async(image_path)
"""
Deprecation shim — the data-URL conversion helpers now live in
``pyrit.memory.storage``.

Importing names from ``pyrit.common.data_url_converter`` still works for one
release but emits a one-time ``DeprecationWarning`` per name. Import from
``pyrit.memory.storage`` instead. This shim will be removed in 0.16.0.

NOTE: When this shim is removed, also drop the ``pyrit.common.data_url_converter``
entry from ``KNOWN_COMMON_VIOLATIONS`` in
``tests/unit/models/test_import_boundary.py`` if it has not already been removed,
so the reverse-guard ratchet bookkeeping is not missed.
"""

from __future__ import annotations

from pyrit.common.deprecation import module_deprecation_getattr

__all__ = [
"AZURE_OPENAI_GPT4O_SUPPORTED_IMAGE_FORMATS",
"convert_local_image_to_data_url",
"convert_local_image_to_data_url_async",
]

__getattr__ = module_deprecation_getattr(
old_module="pyrit.common.data_url_converter",
target_module="pyrit.memory.storage.data_url_converter",
names=__all__,
removed_in="0.16.0",
)


def __dir__() -> list[str]:
return sorted(__all__)
40 changes: 29 additions & 11 deletions pyrit/common/question_answer_helpers.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

from pyrit.models import QuestionAnsweringEntry
"""
Deprecation shim — the question-answering scoring helpers now live in
``pyrit.score``.

Importing names from ``pyrit.common.question_answer_helpers`` still works for one
release but emits a one-time ``DeprecationWarning`` per name. Import from
``pyrit.score.question_answer_helpers`` instead. This shim will be removed in
0.16.0.

def construct_evaluation_prompt(entry: QuestionAnsweringEntry) -> str:
"""
From question and choices in entry, creates prompt to be send to target.
NOTE: When this shim is removed, also drop the
``pyrit.common.question_answer_helpers`` entry from ``KNOWN_COMMON_VIOLATIONS`` in
``tests/unit/models/test_import_boundary.py`` if it has not already been removed,
so the reverse-guard ratchet bookkeeping is not missed.
"""

Args:
entry (QuestionAnsweringEntry): A single entry from which the prompt is constructed.
from __future__ import annotations

Returns:
str: The constructed prompt string.
"""
available_choices = "\n".join(f"index={c.index}, value={c.text}" for c in entry.choices)
return f"Question:\n{entry.question}\n\nChoices:\n{available_choices}"
from pyrit.common.deprecation import module_deprecation_getattr

__all__ = [
"construct_evaluation_prompt",
]

__getattr__ = module_deprecation_getattr(
old_module="pyrit.common.question_answer_helpers",
target_module="pyrit.score.question_answer_helpers",
names=__all__,
removed_in="0.16.0",
)


def __dir__() -> list[str]:
return sorted(__all__)
6 changes: 6 additions & 0 deletions pyrit/memory/storage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
holds the blob payloads those records point to.
"""

from pyrit.memory.storage.data_url_converter import (
convert_local_image_to_data_url,
convert_local_image_to_data_url_async,
)
from pyrit.memory.storage.serializers import (
AllowedCategories,
AudioPathDataTypeSerializer,
Expand All @@ -41,6 +45,8 @@
"AudioPathDataTypeSerializer",
"AzureBlobStorageIO",
"BinaryPathDataTypeSerializer",
"convert_local_image_to_data_url",
"convert_local_image_to_data_url_async",
"DataTypeSerializer",
"data_serializer_factory",
"DiskStorageIO",
Expand Down
60 changes: 60 additions & 0 deletions pyrit/memory/storage/data_url_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

from pyrit.common.deprecation import print_deprecation_message
from pyrit.memory.storage.serializers import DataTypeSerializer, data_serializer_factory

# Supported image formats for Azure OpenAI GPT-4o,
# https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/use-your-image-data
AZURE_OPENAI_GPT4O_SUPPORTED_IMAGE_FORMATS = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif"]


async def convert_local_image_to_data_url_async(image_path: str) -> str:
"""
Convert a local image file to a data URL encoded in base64.

Args:
image_path (str): The file system path to the image file.

Returns:
str: A string containing the MIME type and the base64-encoded data of the image, formatted as a data URL.

Raises:
FileNotFoundError: If no file is found at the specified `image_path`.
ValueError: If the image file's extension is not in the supported formats list.
"""
ext = DataTypeSerializer.get_extension(image_path)
if ext is None or ext.lower() not in AZURE_OPENAI_GPT4O_SUPPORTED_IMAGE_FORMATS:
raise ValueError(
f"Unsupported image format: {ext}. Supported formats are: {AZURE_OPENAI_GPT4O_SUPPORTED_IMAGE_FORMATS}"
)

mime_type = DataTypeSerializer.get_mime_type(image_path)
if not mime_type:
mime_type = "application/octet-stream"

image_serializer = data_serializer_factory(
category="prompt-memory-entries", value=image_path, data_type="image_path", extension=ext
)
base64_encoded_data = await image_serializer.read_data_base64_async()
# Azure OpenAI documentation doesn't specify the local image upload format for API.
# GPT-4o image upload format is determined using "view code" functionality in Azure OpenAI deployments
# The image upload format is same as GPT-4 Turbo.
# Construct the data URL, as per Azure OpenAI GPT-4 Turbo local image format
# https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/gpt-with-vision?tabs=rest%2Csystem-assigned%2Cresource#call-the-chat-completion-apis
return f"data:{mime_type};base64,{base64_encoded_data}"


async def convert_local_image_to_data_url(image_path: str) -> str: # pyrit-async-suffix-exempt
"""
Delegate to ``convert_local_image_to_data_url_async`` (deprecated alias).

Returns:
str: A string containing the MIME type and the base64-encoded data of the image, formatted as a data URL.
"""
print_deprecation_message(
old_item="pyrit.memory.storage.data_url_converter.convert_local_image_to_data_url",
new_item="pyrit.memory.storage.data_url_converter.convert_local_image_to_data_url_async",
removed_in="0.16.0",
)
return await convert_local_image_to_data_url_async(image_path)
2 changes: 1 addition & 1 deletion pyrit/message_normalizer/chat_message_normalizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

import aiofiles

from pyrit.common.data_url_converter import convert_local_image_to_data_url_async
from pyrit.memory import DataTypeSerializer
from pyrit.memory.storage import convert_local_image_to_data_url_async
from pyrit.message_normalizer.message_normalizer import (
MessageListNormalizer,
MessageStringNormalizer,
Expand Down
8 changes: 6 additions & 2 deletions pyrit/prompt_target/openai/openai_chat_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
from collections.abc import MutableSequence
from typing import Any

from pyrit.common.data_url_converter import convert_local_image_to_data_url_async
from pyrit.exceptions import (
EmptyResponseException,
PyritException,
pyrit_target_retry,
)
from pyrit.memory import DataTypeSerializer, data_serializer_factory
from pyrit.memory.storage import convert_local_image_to_data_url_async
from pyrit.models import (
ChatMessage,
ComponentIdentifier,
Expand All @@ -24,7 +24,11 @@
from pyrit.prompt_target.common.json_response_config import _JsonResponseConfig
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.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

Expand Down
8 changes: 6 additions & 2 deletions pyrit/prompt_target/openai/openai_response_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@

from openai.types.shared import ReasoningEffort

from pyrit.common.data_url_converter import convert_local_image_to_data_url_async
from pyrit.exceptions import (
EmptyResponseException,
PyritException,
pyrit_target_retry,
)
from pyrit.memory.storage import convert_local_image_to_data_url_async
from pyrit.models import (
ComponentIdentifier,
Message,
Expand All @@ -29,7 +29,11 @@
from pyrit.prompt_target.common.json_response_config import _JsonResponseConfig
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.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

Expand Down
9 changes: 7 additions & 2 deletions pyrit/prompt_target/websocket_copilot_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@
from websockets.exceptions import InvalidStatus

from pyrit.auth import CopilotAuthenticator, ManualCopilotAuthenticator
from pyrit.common.data_url_converter import convert_local_image_to_data_url_async
from pyrit.exceptions import (
EmptyResponseException,
pyrit_target_retry,
)
from pyrit.memory import DataTypeSerializer
from pyrit.models import ComponentIdentifier, Message, MessagePiece, construct_response_from_request
from pyrit.memory.storage import convert_local_image_to_data_url_async
from pyrit.models import (
ComponentIdentifier,
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
Expand Down
18 changes: 18 additions & 0 deletions pyrit/score/question_answer_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

from pyrit.models import QuestionAnsweringEntry


def construct_evaluation_prompt(entry: QuestionAnsweringEntry) -> str:
"""
From question and choices in entry, creates prompt to be send to target.

Args:
entry (QuestionAnsweringEntry): A single entry from which the prompt is constructed.

Returns:
str: The constructed prompt string.
"""
available_choices = "\n".join(f"index={c.index}, value={c.text}" for c in entry.choices)
return f"Question:\n{entry.question}\n\nChoices:\n{available_choices}"
Loading
Loading