Skip to content
Merged
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
3 changes: 3 additions & 0 deletions pyrit/converter/math_obfuscation_converter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

import hashlib
import logging
import random

Expand Down Expand Up @@ -98,6 +99,8 @@ def _build_identifier(self) -> ComponentIdentifier:
params={
"min_n": self._min_n,
"max_n": self._max_n,
"hint_hash": hashlib.sha256(self._hint.encode("utf-8")).hexdigest()[:16],
"suffix_hash": hashlib.sha256(self._suffix.encode("utf-8")).hexdigest()[:16],
}
)

Expand Down
17 changes: 16 additions & 1 deletion pyrit/converter/negation_trap_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
# Licensed under the MIT license.

import asyncio
import hashlib

from pyrit.converter.converter import Converter, ConverterResult
from pyrit.models import PromptDataType
from pyrit.models import ComponentIdentifier, PromptDataType


class NegationTrapConverter(Converter):
Expand Down Expand Up @@ -58,6 +59,20 @@ def __init__(
if "{prompt}" not in self.trap_template:
raise ValueError("trap_template must contain '{prompt}' placeholder")

def _build_identifier(self) -> ComponentIdentifier:
"""
Build identifier with negation trap parameters.

Returns:
ComponentIdentifier: The identifier for this converter.
"""
return self._create_identifier(
params={
"wrong_value": self.wrong_value,
"trap_template_hash": hashlib.sha256(self.trap_template.encode("utf-8")).hexdigest()[:16],
}
)

async def convert_async(self, *, prompt: str, input_type: PromptDataType = "text") -> ConverterResult:
"""
Convert the prompt into a negation trap.
Expand Down
1 change: 1 addition & 0 deletions pyrit/converter/selective_text_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ def _build_identifier(self) -> ComponentIdentifier:
"preserve_tokens": self._preserve_tokens,
"start_token": self._start_token,
"end_token": self._end_token,
"word_separator": self._word_separator if self._is_word_level else None,
},
sub_converter=self._sub_converter.get_identifier(),
)
Expand Down
1 change: 1 addition & 0 deletions pyrit/converter/zalgo_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def _build_identifier(self) -> ComponentIdentifier:
params={
**selection_params,
"intensity": self._intensity,
"seed": self._seed,
},
)

Expand Down
33 changes: 33 additions & 0 deletions tests/unit/converter/test_math_obfuscation_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pytest

from pyrit.converter.math_obfuscation_converter import MathObfuscationConverter
from pyrit.registry import ConverterRegistry


@pytest.mark.parametrize(
Expand Down Expand Up @@ -151,3 +152,35 @@ def test_math_obfuscation_full_defaults():
# Hint should be on first line only
lines = result.output_text.split(MathObfuscationConverter.DEFAULT_SUFFIX)[0].splitlines()
assert MathObfuscationConverter.DEFAULT_HINT in lines[0]


@pytest.mark.parametrize(
"settings",
[{"hint": "custom hint"}, {"hint": ""}, {"suffix": "custom suffix"}, {"suffix": ""}],
)
def test_math_obfuscation_identifier_distinguishes_hint_and_suffix(settings: dict[str, str]) -> None:
default = MathObfuscationConverter()
other = MathObfuscationConverter(**settings)
assert default.get_identifier().hash != other.get_identifier().hash

# Both configurations get their own default registry name instead of colliding.
instances = ConverterRegistry().instances
instances.register(default)
instances.register(other)
assert instances.get(default.get_identifier().unique_name) is default
assert instances.get(other.get_identifier().unique_name) is other


def test_math_obfuscation_identifier_is_stable_for_equal_settings() -> None:
assert (
MathObfuscationConverter(hint="h", suffix="s").get_identifier().hash
== MathObfuscationConverter(hint="h", suffix="s").get_identifier().hash
)
assert (
MathObfuscationConverter().get_identifier().hash
== MathObfuscationConverter(
hint=MathObfuscationConverter.DEFAULT_HINT, suffix=MathObfuscationConverter.DEFAULT_SUFFIX
)
.get_identifier()
.hash
)
31 changes: 31 additions & 0 deletions tests/unit/converter/test_negation_trap_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import pytest

from pyrit.converter import NegationTrapConverter
from pyrit.registry import ConverterRegistry


async def test_negation_trap_converter_initialization_with_defaults() -> None:
Expand Down Expand Up @@ -255,3 +256,33 @@ async def test_negation_trap_converter_default_template_constant() -> None:
assert hasattr(NegationTrapConverter, "DEFAULT_TEMPLATE")
assert "{prompt}" in NegationTrapConverter.DEFAULT_TEMPLATE
assert "{wrong_value}" in NegationTrapConverter.DEFAULT_TEMPLATE


def test_negation_trap_identifier_includes_wrong_value() -> None:
params = NegationTrapConverter(wrong_value="1234").get_identifier().params
assert params["wrong_value"] == "1234"
assert len(params["trap_template_hash"]) == 16


@pytest.mark.parametrize(
"settings",
[{"wrong_value": "hunter2"}, {"trap_template": "Is {prompt} really '{wrong_value}'?"}],
)
def test_negation_trap_identifier_distinguishes_settings(settings: dict[str, str]) -> None:
default = NegationTrapConverter()
other = NegationTrapConverter(**settings)
assert default.get_identifier().hash != other.get_identifier().hash

# Both configurations get their own default registry name instead of colliding.
instances = ConverterRegistry().instances
instances.register(default)
instances.register(other)
assert instances.get(default.get_identifier().unique_name) is default
assert instances.get(other.get_identifier().unique_name) is other


def test_negation_trap_identifier_is_stable_for_equal_settings() -> None:
assert (
NegationTrapConverter(wrong_value="x", trap_template="{prompt}: {wrong_value}").get_identifier().hash
== NegationTrapConverter(wrong_value="x", trap_template="{prompt}: {wrong_value}").get_identifier().hash
)
38 changes: 38 additions & 0 deletions tests/unit/converter/test_selective_text_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
)
from pyrit.models import Message, PromptDataType
from pyrit.prompt_normalizer import ConverterConfiguration, PromptNormalizer
from pyrit.registry import ConverterRegistry


@pytest.mark.parametrize("token_entry", [False, True])
Expand Down Expand Up @@ -773,3 +774,40 @@ def test_identifier_uses_empty_strategy_params_for_char_level_strategies(self):
params = converter.get_identifier().params
assert params["selection_strategy"] == "IndexSelectionStrategy"
assert params["selection_strategy_params"] == {}


def test_identifier_distinguishes_word_separator_for_word_level_strategies() -> None:
space = SelectiveTextConverter(
sub_converter=Base64Converter(),
selection_strategy=WordIndexSelectionStrategy(indices=[0]),
)
comma = SelectiveTextConverter(
sub_converter=Base64Converter(),
selection_strategy=WordIndexSelectionStrategy(indices=[0]),
word_separator=",",
)
assert space.get_identifier().params["word_separator"] == " "
assert comma.get_identifier().params["word_separator"] == ","
assert space.get_identifier().hash != comma.get_identifier().hash

# Both configurations get their own default registry name instead of colliding.
instances = ConverterRegistry().instances
instances.register(space)
instances.register(comma)
assert instances.get(space.get_identifier().unique_name) is space
assert instances.get(comma.get_identifier().unique_name) is comma


def test_identifier_ignores_word_separator_for_char_level_strategies() -> None:
# Character-level strategies never split on the separator, so it does not change the output.
space = SelectiveTextConverter(
sub_converter=Base64Converter(),
selection_strategy=IndexSelectionStrategy(start=0, end=5),
)
comma = SelectiveTextConverter(
sub_converter=Base64Converter(),
selection_strategy=IndexSelectionStrategy(start=0, end=5),
word_separator=",",
)
assert "word_separator" not in space.get_identifier().params
assert space.get_identifier().hash == comma.get_identifier().hash
24 changes: 24 additions & 0 deletions tests/unit/converter/test_zalgo_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from pyrit.converter import ZalgoConverter
from pyrit.converter.text_selection_strategy import WordProportionSelectionStrategy
from pyrit.registry import ConverterRegistry


async def test_zalgo_output_changes_text():
Expand Down Expand Up @@ -136,3 +137,26 @@ async def test_zalgo_negative_intensity(caplog):
async def test_zalgo_invalid_intensity(bad_intensity):
with pytest.raises(ValueError):
ZalgoConverter(intensity=bad_intensity, seed=1)


def test_zalgo_identifier_includes_seed() -> None:
assert ZalgoConverter(seed=7).get_identifier().params["seed"] == 7


def test_zalgo_identifier_distinguishes_seeds() -> None:
first = ZalgoConverter(seed=1)
second = ZalgoConverter(seed=2)
assert first.get_identifier().hash != second.get_identifier().hash

# Both configurations get their own default registry name instead of colliding.
instances = ConverterRegistry().instances
instances.register(first)
instances.register(second)
assert instances.get(first.get_identifier().unique_name) is first
assert instances.get(second.get_identifier().unique_name) is second


def test_zalgo_identifier_without_seed_is_unchanged_by_the_seed_param() -> None:
# An unseeded converter inherits the configured root, and None params are not hashed.
assert "seed" not in ZalgoConverter().get_identifier().params
assert ZalgoConverter(seed=3).get_identifier().hash == ZalgoConverter(seed=3).get_identifier().hash
Loading