diff --git a/pyrit/converter/nato_converter.py b/pyrit/converter/nato_converter.py index 7065feea8a..551cc64ff1 100644 --- a/pyrit/converter/nato_converter.py +++ b/pyrit/converter/nato_converter.py @@ -2,18 +2,18 @@ # Licensed under the MIT license. -from pyrit.converter.converter import Converter, ConverterResult -from pyrit.models import PromptDataType +from pyrit.converter.word_level_converter import WordLevelConverter -class NatoConverter(Converter): +class NatoConverter(WordLevelConverter): """ Converts text into NATO phonetic alphabet representation. This converter transforms standard text into NATO phonetic alphabet format, - where each letter is replaced with its corresponding NATO phonetic code word - (e.g., "A" becomes "Alfa", "B" becomes "Bravo"). Only alphabetic characters - are converted; non-alphabetic characters are ignored. + where each ASCII letter is replaced with its corresponding NATO phonetic code + word (e.g., "A" becomes "Alfa", "B" becomes "Bravo"). Characters outside the + ASCII alphabet are preserved with their original casing. Spaces are represented + by ```` so word boundaries remain distinct from code-word separators. The NATO phonetic alphabet is the most widely used spelling alphabet, designed to improve clarity of voice communication. This converter can be used to test @@ -23,13 +23,15 @@ class NatoConverter(Converter): Reference: https://en.wikipedia.org/wiki/NATO_phonetic_alphabet Example: - Input: "Hello" - Output: "Hotel Echo Lima Lima Oscar" + Input: "Hello world" + Output: "Hotel Echo Lima Lima Oscar Whiskey Oscar Romeo Lima Delta" """ SUPPORTED_INPUT_TYPES = ("text",) SUPPORTED_OUTPUT_TYPES = ("text",) + _WORD_SEPARATOR = "" + _NATO_MAP = { "A": "Alfa", "B": "Bravo", @@ -59,37 +61,34 @@ class NatoConverter(Converter): "Z": "Zulu", } - async def convert_async(self, *, prompt: str, input_type: PromptDataType = "text") -> ConverterResult: + async def convert_word_async(self, word: str) -> str: """ - Convert the given text into NATO phonetic alphabet representation. + Convert one word into NATO phonetic alphabet representation. Args: - prompt (str): The text to be converted to NATO phonetic alphabet. - input_type (PromptDataType, optional): Type of input data. Defaults to "text". + word (str): The word to convert. Returns: - ConverterResult: The text converted to NATO phonetic alphabet format. - - Raises: - ValueError: If the input type is not supported (only "text" is supported). + str: The converted word, with code words separated by spaces. """ - if not self.input_supported(input_type): - raise ValueError("Input type not supported") - - nato_text = self._convert_to_nato(prompt) - - return ConverterResult(output_text=nato_text, output_type="text") + output = [self._NATO_MAP.get(char.upper(), char) if char.isascii() else char for char in word] + return " ".join(output) - def _convert_to_nato(self, text: str) -> str: + def join_words(self, words: list[str]) -> str: """ - Convert text to NATO phonetic alphabet representation. + Join converted words while preserving every source-space boundary. Args: - text (str): The text to convert. + words (list[str]): The converted words. Returns: - str: The NATO phonetic alphabet representation, with code words separated by spaces. + str: The converted words separated by explicit space tokens. """ - output = [self._NATO_MAP[char] for char in text.upper() if char in self._NATO_MAP] + output: list[str] = [] + for index, word in enumerate(words): + if index: + output.append(self._WORD_SEPARATOR) + if word: + output.append(word) return " ".join(output) diff --git a/tests/unit/converter/test_nato_converter.py b/tests/unit/converter/test_nato_converter.py index 2dbe2c0eda..72f43dd50c 100644 --- a/tests/unit/converter/test_nato_converter.py +++ b/tests/unit/converter/test_nato_converter.py @@ -4,6 +4,7 @@ import pytest from pyrit.converter import ConverterResult, NatoConverter +from pyrit.converter.text_selection_strategy import WordIndexSelectionStrategy async def test_nato_converter_simple_text(): @@ -43,7 +44,7 @@ async def test_nato_converter_mixed_case(): async def test_nato_converter_with_numbers(): - """Test that numbers are ignored in NATO conversion.""" + """Test that numbers are preserved in NATO conversion.""" converter = NatoConverter() prompt = "a1b2c3" @@ -51,12 +52,12 @@ async def test_nato_converter_with_numbers(): assert isinstance(result, ConverterResult) assert result.output_type == "text" - # Only letters should be converted - assert result.output_text == "Alfa Bravo Charlie" + # Digits are preserved as-is so the encoded prompt keeps its full content + assert result.output_text == "Alfa 1 Bravo 2 Charlie 3" async def test_nato_converter_with_spaces(): - """Test that spaces are ignored in NATO conversion.""" + """Test that word boundaries remain distinct from code-word separators.""" converter = NatoConverter() prompt = "a b c" @@ -64,12 +65,11 @@ async def test_nato_converter_with_spaces(): assert isinstance(result, ConverterResult) assert result.output_type == "text" - # Spaces should be ignored - assert result.output_text == "Alfa Bravo Charlie" + assert result.output_text == "Alfa Bravo Charlie" async def test_nato_converter_with_punctuation(): - """Test that punctuation is ignored in NATO conversion.""" + """Test that punctuation is preserved in NATO conversion.""" converter = NatoConverter() prompt = "Hello, world!" @@ -77,8 +77,8 @@ async def test_nato_converter_with_punctuation(): assert isinstance(result, ConverterResult) assert result.output_type == "text" - # Only letters should be converted - assert result.output_text == "Hotel Echo Lima Lima Oscar Whiskey Oscar Romeo Lima Delta" + # Punctuation is preserved as-is so the encoded prompt keeps its full content + assert result.output_text == "Hotel Echo Lima Lima Oscar , Whiskey Oscar Romeo Lima Delta !" async def test_nato_converter_empty_string(): @@ -94,7 +94,11 @@ async def test_nato_converter_empty_string(): async def test_nato_converter_no_letters(): - """Test NATO conversion with no alphabetic characters.""" + """Test NATO conversion with no alphabetic characters. + + Regression: non-empty input must never convert to an empty prompt + (digits/punctuation are preserved rather than erased). + """ converter = NatoConverter() prompt = "123!@#" @@ -102,7 +106,35 @@ async def test_nato_converter_no_letters(): assert isinstance(result, ConverterResult) assert result.output_type == "text" - assert result.output_text == "" + assert result.output_text == "1 2 3 ! @ #" + + +async def test_nato_converter_space_only_prompt(): + """Test that a non-empty whitespace prompt remains non-empty.""" + converter = NatoConverter() + + result = await converter.convert_async(prompt=" ", input_type="text") + + assert result.output_text == " " + + +@pytest.mark.parametrize("prompt", ["é", "ß", "ı", "ñ"]) +async def test_nato_converter_preserves_unmapped_unicode(prompt: str): + """Test that Unicode characters are preserved without case conversion.""" + converter = NatoConverter() + + result = await converter.convert_async(prompt=prompt, input_type="text") + + assert result.output_text == prompt + + +async def test_nato_converter_word_selection_strategy(): + """Test that NATO conversion supports the shared word selection strategies.""" + converter = NatoConverter(word_selection_strategy=WordIndexSelectionStrategy(indices=[1])) + + result = await converter.convert_async(prompt="abc def", input_type="text") + + assert result.output_text == "abc Delta Echo Foxtrot" async def test_nato_converter_all_letters(): @@ -126,7 +158,7 @@ async def test_nato_converter_input_type_not_supported(): """Test that non-text input types raise ValueError.""" converter = NatoConverter() - with pytest.raises(ValueError, match="Input type not supported"): + with pytest.raises(ValueError, match="Input type image_path not supported"): await converter.convert_async(prompt="test", input_type="image_path")