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
12 changes: 12 additions & 0 deletions pyrit/converter/bijection_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,12 @@ def _build_identifier(self) -> ComponentIdentifier:
# the marker and restores the uppercase letter. Without this, capitalization is
# silently destroyed at encode time (`"25".upper() == "25"`), not just mishandled
# at decode.
#
# _CASE_MARKER is also a plain character that can appear in the plaintext itself
# (contractions, possessives: "it's", "don't"). Since every letter always encodes
# to a digit token, a passed-through marker immediately followed by an encoded
# letter is indistinguishable from a real case marker, so a literal marker is
# doubled on encode and collapsed back on decode.
_CASE_MARKER = "'"

def encode(self, *, prompt: str) -> str:
Expand All @@ -285,6 +291,8 @@ def encode(self, *, prompt: str) -> str:
if char.lower() in self._mapping:
token = self._mapping[char.lower()]
encoded += (self._CASE_MARKER + token) if char.isupper() else token
elif char == self._CASE_MARKER:
encoded += self._CASE_MARKER * 2
Comment on lines +294 to +295

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we update the digit converter's get_teaching_instructions() along with this change? It still tells the target to "preserve spaces/punctuation", without explaining that '' represents a literal apostrophe and ' before a digit token marks uppercase.

With the mapping in these tests, it's encodes to 1829''28. Following the current instructions literally would read that as it''s. In the other direction, a target encoding it's with unchanged punctuation would produce 1829'28, which decode() turns into itS.

Please teach both marker rules, include a contraction example, and add regression coverage for the generated instructions. Am I missing somewhere else these rules are already explained?

else:
encoded += char
return encoded
Expand Down Expand Up @@ -321,6 +329,10 @@ def decode(self, encoded_text: str) -> str:
decoded = ""
i = 0
while i < len(encoded_text):
if encoded_text[i] == self._CASE_MARKER and encoded_text[i + 1 : i + 2] == self._CASE_MARKER:
decoded += self._CASE_MARKER
i += 2
continue
is_upper = encoded_text[i] == self._CASE_MARKER
start = i + 1 if is_upper else i
candidate = encoded_text[start : start + self._num_digits]
Expand Down
20 changes: 20 additions & 0 deletions tests/unit/converter/test_bijection_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,26 @@ async def test_digit_converter_explicit_mapping_round_trip():
assert converter.decode(encoded.output_text) == "abc xyz!"


async def test_digit_converter_literal_apostrophe_round_trip():
custom_mapping = {letter: str(index + 10) for index, letter in enumerate(string.ascii_lowercase)}
converter = DigitBijectionConverter(mapping=custom_mapping)

encoded = await converter.convert_async(prompt="it's")

assert encoded.output_text == "1829''28"
assert converter.decode(encoded.output_text) == "it's"


async def test_digit_converter_uppercase_letter_after_apostrophe_round_trip():
custom_mapping = {letter: str(index + 10) for index, letter in enumerate(string.ascii_lowercase)}
converter = DigitBijectionConverter(mapping=custom_mapping)

encoded = await converter.convert_async(prompt="I'm")

assert encoded.output_text == "'18''22"
assert converter.decode(encoded.output_text) == "I'm"


def test_digit_converter_encodes_letters():
converter = DigitBijectionConverter(num_digits=2)
# encoding "hello" should produce digit strings
Expand Down