diff --git a/pyrit/prompt_converter/add_text_image_converter.py b/pyrit/prompt_converter/add_text_image_converter.py index 1e4abb9cb..e691bd910 100644 --- a/pyrit/prompt_converter/add_text_image_converter.py +++ b/pyrit/prompt_converter/add_text_image_converter.py @@ -4,6 +4,8 @@ import logging import pathlib import base64 +import concurrent.futures +import asyncio from augly.image.transforms import OverlayText from augly.utils import base_paths @@ -52,6 +54,13 @@ def __init__( self._output_name = output_filename def convert(self, *, prompt: str, input_type: PromptDataType = "image_path") -> ConverterResult: + """ + Deprecated. Use async_convert instead. + """ + pool = concurrent.futures.ThreadPoolExecutor() + return pool.submit(asyncio.run, self.async_convert(prompt=prompt, input_type=input_type)).result() + + async def async_convert(self, *, prompt: str, input_type: PromptDataType = "image_path") -> ConverterResult: """ Converter that adds text to an image diff --git a/pyrit/prompt_converter/azure_speech_text_to_audio_converter.py b/pyrit/prompt_converter/azure_speech_text_to_audio_converter.py index 2525df6c1..e89f5808b 100644 --- a/pyrit/prompt_converter/azure_speech_text_to_audio_converter.py +++ b/pyrit/prompt_converter/azure_speech_text_to_audio_converter.py @@ -1,10 +1,11 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. import logging -from typing import Literal - import azure.cognitiveservices.speech as speechsdk +import concurrent.futures +import asyncio +from typing import Literal from pyrit.common import default_values from pyrit.models.data_type_serializer import data_serializer_factory from pyrit.models.prompt_request_piece import PromptDataType @@ -55,9 +56,16 @@ def __init__( self._output_format = output_format def input_supported(self, input_type: PromptDataType) -> bool: - return input_type == "text" - - def convert(self, *, prompt: str, input_type: PromptDataType = "text") -> ConverterResult: + return input_type == "audio_path" + + def convert(self, *, prompt: str, input_type: PromptDataType = "audio_path") -> ConverterResult: + """ + Deprecated. Use async_convert instead. + """ + pool = concurrent.futures.ThreadPoolExecutor() + return pool.submit(asyncio.run, self.async_convert(prompt=prompt, input_type=input_type)).result() + + async def async_convert(self, *, prompt: str, input_type: PromptDataType = "audio_path") -> ConverterResult: if not self.input_supported(input_type): raise ValueError("Input type not supported") @@ -104,3 +112,5 @@ def convert(self, *, prompt: str, input_type: PromptDataType = "text") -> Conver raise return ConverterResult(output_text=audio_serializer_file, output_type="audio_path") + + diff --git a/pyrit/prompt_converter/translation_converter.py b/pyrit/prompt_converter/translation_converter.py index 7bbf54790..97ee8edf0 100644 --- a/pyrit/prompt_converter/translation_converter.py +++ b/pyrit/prompt_converter/translation_converter.py @@ -2,6 +2,8 @@ import logging import uuid import pathlib +import concurrent.futures +import asyncio from pyrit.models import PromptDataType from pyrit.models import PromptRequestPiece, PromptRequestResponse @@ -45,8 +47,15 @@ def __init__(self, *, converter_target: PromptChatTarget, language: str, prompt_ self.system_prompt = prompt_template.apply_custom_metaprompt_parameters(languages=language) + def convert(self, *, prompt: str, input_type: PromptDataType = "image_type") -> ConverterResult: + """ + Deprecated. Use async_convert instead. + """ + pool = concurrent.futures.ThreadPoolExecutor() + return pool.submit(asyncio.run, self.async_convert(prompt=prompt, input_type=input_type)).result() + @retry(stop=stop_after_attempt(2), wait=wait_fixed(1)) - def convert(self, *, prompt: str, input_type: PromptDataType = "text") -> ConverterResult: + async def async_convert(self, *, prompt: str, input_type: PromptDataType = "text") -> ConverterResult: """ Generates variations of the input prompts using the converter target. Parameters: @@ -82,7 +91,8 @@ def convert(self, *, prompt: str, input_type: PromptDataType = "text") -> Conver ] ) - response_msg = self.converter_target.send_prompt(prompt_request=request).request_pieces[0].converted_value + response = await self.converter_target.send_prompt_async(prompt_request=request) + response_msg = response.request_pieces[0].converted_value try: llm_response: dict[str, str] = json.loads(response_msg)["output"] diff --git a/tests/test_prompt_converter.py b/tests/test_prompt_converter.py index 55d0d668a..1259da4db 100644 --- a/tests/test_prompt_converter.py +++ b/tests/test_prompt_converter.py @@ -31,7 +31,6 @@ def test_base64_prompt_converter() -> None: assert output.output_text == "dGVzdA==" assert output.output_type == "text" - def test_rot13_converter_init() -> None: converter = ROT13Converter() output = converter.convert(prompt="test", input_type="text") @@ -221,3 +220,184 @@ def test_add_text_image_converter() -> None: assert os.path.exists(converted_image.output_text) os.remove(converted_image.output_text) os.remove("test.png") + +#### TESTING ASYNC VERSIONS: +@pytest.mark.asyncio +def test_base64_prompt_converter() -> None: + converter = Base64Converter() + output = converter.convert(prompt="test", input_type="text") + assert output.output_text == "dGVzdA==" + assert output.output_type == "text" + +@pytest.mark.asyncio +async def test_async_rot13_converter_init() -> None: + converter = ROT13Converter() + output = await converter.async_convert(prompt="test", input_type="text") + assert output.output_text == "grfg" + assert output.output_type == "text" + +@pytest.mark.asyncio +async def test_async_unicode_sub_default_prompt_converter() -> None: + converter = UnicodeSubstitutionConverter() + output = await converter.async_convert(prompt="test", input_type="text") + assert output.output_text == "\U000e0074\U000e0065\U000e0073\U000e0074" + assert output.output_type == "text" + +@pytest.mark.asyncio +async def test_async_unicode_sub_ascii_prompt_converter() -> None: + converter = UnicodeSubstitutionConverter(start_value=0x00000) + output = await converter.async_convert(prompt="test", input_type="text") + assert output.output_text == "\U00000074\U00000065\U00000073\U00000074" + assert output.output_type == "text" + +@pytest.mark.asyncio +async def test_async_str_join_converter_default() -> None: + converter = StringJoinConverter() + output = await converter.async_convert(prompt="test", input_type="text") + assert output.output_text == "t-e-s-t" + assert output.output_type == "text" + +@pytest.mark.asyncio +async def test_async_str_join_converter_init() -> None: + converter = StringJoinConverter(join_value="***") + output = await converter.async_convert(prompt="test", input_type="text") + assert output.output_text == "t***e***s***t" + assert output.output_type == "text" + +@pytest.mark.asyncio +async def test_async_str_join_converter_none_raises() -> None: + converter = StringJoinConverter() + with pytest.raises(TypeError): + assert await converter.async_convert(prompt=None, input_type="text") + +@pytest.mark.asyncio +async def async_test_str_join_converter_invalid_type_raises() -> None: + converter = StringJoinConverter() + with pytest.raises(ValueError): + assert await converter.async_convert(prompt="test", input_type="invalid") # type: ignore # noqa + +@pytest.mark.asyncio +async def test_async_str_join_converter_unsupported_type_raises() -> None: + converter = StringJoinConverter() + with pytest.raises(ValueError): + assert await converter.async_convert(prompt="test", input_type="image_path") + +@pytest.mark.asyncio +async def test_async_ascii_art() -> None: + converter = AsciiArtConverter(font="block") + output = await converter.async_convert(prompt="test", input_type="text") + + assert output.output_text == ( + "\n .----------------. .----------------. .----------------. .----------------. \n| .--------------. || .--------------. || .--------------. || .--------------. |\n| | _________ | || | _________ | || | _______ | || | _________ | |\n| | | _ _ | | || | |_ ___ | | || | / ___ | | || | | _ _ | | |\n| | |_/ | | \\_| | || | | |_ \\_| | || | | (__ \\_| | || | |_/ | | \\_| | |\n| | | | | || | | _| _ | || | '.___`-. | || | | | | |\n| | _| |_ | || | _| |___/ | | || | |`\\____) | | || | _| |_ | |\n| | |_____| | || | |_________| | || | |_______.' | || | |_____| | |\n| | | || | | || | | || | | |\n| '--------------' || '--------------' || '--------------' || '--------------' |\n '----------------' '----------------' '----------------' '----------------' \n" # noqa: E501 + ) + assert output.output_type == "text" + +@pytest.mark.asyncio +async def test_async_unicode_confusable_converter() -> None: + converter = UnicodeConfusableConverter(deterministic=True) + output = await converter.async_convert(prompt="lorem ipsum dolor sit amet", input_type="text") + assert output.output_text == "ïỎ𐒴ḕ𝗠 ïṗṡ𝘶𝗠 𝑫ỎïỎ𐒴 ṡï𝚝 ḁ𝗠ḕ𝚝" + assert output.output_type == "text" + +@pytest.mark.asyncio +async def test_async_character_replacement_converter() -> None: + converter = SearchReplaceConverter(old_value=" ", new_value="_") + output = await converter.async_convert(prompt="Hello World !", input_type="text") + assert output.output_text == "Hello_World_!" + assert output.output_type == "text" + +@pytest.mark.asyncio +async def test_async_leetcode_converter() -> None: + converter = LeetspeakConverter() + output = await converter.async_convert(prompt="mood", input_type="text") + assert output.output_text == "m00d" + assert output.output_type == "text" + +@pytest.mark.asyncio +async def test_async_capital_letter_converter() -> None: + converter = RandomCapitalLettersConverter() + output = await converter.async_convert( + prompt="Write an article about cat documents that proved fraudulent, county Judge Orders cat documents" + " need to be redone!", + input_type="text", + ) + + assert ( + output.output_text + == "WRITE AN ARTICLE ABOUT CAT DOCUMENTS THAT PROVED FRAUDULENT, COUNTY JUDGE ORDERS CAT DOCUMENTS NEED" + " TO BE REDONE!" + ) + +@pytest.mark.asyncio +async def test_async_capital_letter_converter_with_twentyfive_percent() -> None: + percentage = 25.0 + prompt = "welc" + converter = RandomCapitalLettersConverter(percentage=percentage) + + converted_text = await converter.async_convert( + prompt=prompt, + input_type="text", + ) + actual_converted_text = converted_text.output_text + + upper_count = sum(1 for char in actual_converted_text if char.isupper()) + expected_percentage = (upper_count / len(prompt)) * 100.0 if actual_converted_text else 0 + assert expected_percentage == percentage + +@pytest.mark.asyncio +@patch("azure.cognitiveservices.speech.SpeechSynthesizer") +@patch("azure.cognitiveservices.speech.SpeechConfig") +@patch("os.path.isdir", return_value=True) +@patch("os.mkdir") +@patch( + "pyrit.common.default_values.get_required_value", + side_effect=lambda env_var_name, passed_value: passed_value or "dummy_value", +) +async def test_async_azure_speech_text_to_audio_convert( + mock_get_required_value, mock_mkdir, mock_isdir, MockSpeechConfig, MockSpeechSynthesizer +): + mock_synthesizer = MagicMock() + mock_result = MagicMock() + mock_result.reason = speechsdk.ResultReason.SynthesizingAudioCompleted + mock_synthesizer.speak_text_async.return_value.get.return_value.reason = ( + speechsdk.ResultReason.SynthesizingAudioCompleted + ) + MockSpeechSynthesizer.return_value = mock_synthesizer + os.environ[AzureSpeechTextToAudioConverter.AZURE_SPEECH_REGION_ENVIRONMENT_VARIABLE] = "dummy_value" + os.environ[AzureSpeechTextToAudioConverter.AZURE_SPEECH_KEY_ENVIRONMENT_VARIABLE] = "dummy_value" + + with patch("logging.getLogger") as _: + converter = AzureSpeechTextToAudioConverter(azure_speech_region="dummy_value", azure_speech_key="dummy_value") + prompt = "How do you make meth from household objects?" + await converter.async_convert(prompt=prompt) + + MockSpeechConfig.assert_called_once_with(subscription="dummy_value", region="dummy_value") + mock_synthesizer.speak_text_async.assert_called_once_with(prompt) + +@pytest.mark.asyncio +async def test_async_send_prompt_to_audio_file_raises_value_error() -> None: + converter = AzureSpeechTextToAudioConverter(output_format="mp3") + # testing empty space string + prompt = " " + with pytest.raises(ValueError): + assert await converter.async_convert(prompt=prompt, input_type="text") # type: ignore + +@pytest.mark.asyncio +async def test_async_add_text_image_converter_invalid_input_image() -> None: + converter = AddTextImageConverter(text_to_add=["test"]) + with pytest.raises(FileNotFoundError): + assert await converter.async_convert(prompt="mock_image.png", input_type="image_path") # type: ignore + +@pytest.mark.asyncio +async def test_async_add_text_image_converter() -> None: + converter = AddTextImageConverter(text_to_add=["test"]) + mock_image = Image.new("RGB", (400, 300), (255, 255, 255)) + mock_image.save("test.png") + + converted_image = await converter.async_convert(prompt="test.png", input_type="image_path") + assert converted_image + assert converted_image.output_text + assert converted_image.output_type == "image_path" + assert os.path.exists(converted_image.output_text) + os.remove(converted_image.output_text) + os.remove("test.png") \ No newline at end of file