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
13 changes: 10 additions & 3 deletions doc/code/converters/0_converters.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@
"from pyrit.output import output_attack_async\n",
"from pyrit.setup import IN_MEMORY, initialize_pyrit_async\n",
"\n",
"await initialize_pyrit_async(memory_db_type=IN_MEMORY) # type: ignore\n",
"await initialize_pyrit_async(memory_db_type=IN_MEMORY, seed=42) # type: ignore\n",
"\n",
"# Get all converters with their modalities\n",
"converter_list = get_converter_modalities()\n",
Expand Down Expand Up @@ -220,8 +220,15 @@
"output_type": "stream",
"text": [
"text: gryy zr ubj gb phg qbja n gerr\n",
"text: tell me HOw tO cut down a tree\n",
"text: \"|\"[-|_|_ |\\/|[- |-|()\\/\\/ \"|\"() (|_|\"|\" |)()\\/\\/|\\| /-\\ \"|\"|2[-[-\n",
"text: telL mE how tO cuT down a tree\n",
"text: \n",
" ###### ###### ### ### ## ## ###### ## ## #### ## ## ###### #### ###### ## ## ###### ##### #### ## ## ## ## ###### ###### ##### ###### ###### \n",
" ## ### ## ### ### ###### ### ## ## ### ## ## ## ## ## ## ## ### ## ## ## ## ## ## ## ## ## ## ### ## ### ## ## ## ## ### ## ### ## \n",
" ## #### ## ## ####### #### ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ###### ## ## ## ## ### #### #### \n",
" ## ## ### ### ## ## ## ####### ## ## ####### ## ## ## ## ## ## ## ### ## ## ## ####### ## ### ####### ## ###### ## ## \n",
" ## ### ## ## ## ## ## ## ## ### ## ## ## ## ## ####### ## ## ## ## ## ## ## ## ## ## ## ## ####### ## ## ## ## ## ## ## ### ## ### ## \n",
" ## ####### ####### ####### ## ## ####### ## ## #### ## ## ## #### ###### #### ## ###### #### ## ## ## ## ## ## ## ## ### ####### ####### \n",
" \n",
"\n",
"text: 0000000001110100 0000000001100101 0000000001101100 0000000001101100 0000000000100000 0000000001101101 0000000001100101 0000000000100000 0000000001101000 0000000001101111 0000000001110111 0000000000100000 0000000001110100 0000000001101111 0000000000100000 0000000001100011 0000000001110101 0000000001110100 0000000000100000 0000000001100100 0000000001101111 0000000001110111 0000000001101110 0000000000100000 0000000001100001 0000000000100000 0000000001110100 0000000001110010 0000000001100101 0000000001100101\n",
"text: t m h t c d a t\n"
Expand Down
2 changes: 1 addition & 1 deletion doc/code/converters/0_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from pyrit.output import output_attack_async
from pyrit.setup import IN_MEMORY, initialize_pyrit_async

await initialize_pyrit_async(memory_db_type=IN_MEMORY) # type: ignore
await initialize_pyrit_async(memory_db_type=IN_MEMORY, seed=42) # type: ignore

# Get all converters with their modalities
converter_list = get_converter_modalities()
Expand Down
104 changes: 61 additions & 43 deletions doc/code/converters/1_text_to_text_converters.ipynb

Large diffs are not rendered by default.

17 changes: 13 additions & 4 deletions doc/code/converters/1_text_to_text_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
)
from pyrit.setup import IN_MEMORY, initialize_pyrit_async

await initialize_pyrit_async(memory_db_type=IN_MEMORY) # type: ignore
await initialize_pyrit_async(memory_db_type=IN_MEMORY, seed=42) # type: ignore

prompt = "tell me how to cut down a tree"

Expand Down Expand Up @@ -121,8 +121,14 @@
prompt = "tell me how to cut down a tree"

print("Leetspeak:", await LeetspeakConverter().convert_async(prompt=prompt)) # type: ignore
print("Random Capitals:", await RandomCapitalLettersConverter(percentage=50.0).convert_async(prompt=prompt)) # type: ignore
print("Unicode Confusable:", await UnicodeConfusableConverter().convert_async(prompt=prompt)) # type: ignore
print(
"Random Capitals:",
await RandomCapitalLettersConverter(percentage=50.0).convert_async(prompt=prompt), # type: ignore
)
print(
"Unicode Confusable:",
await UnicodeConfusableConverter().convert_async(prompt=prompt), # type: ignore
)
print("Unicode Substitution:", await UnicodeSubstitutionConverter().convert_async(prompt=prompt)) # type: ignore
print("Unicode Replacement:", await UnicodeReplacementConverter().convert_async(prompt=prompt)) # type: ignore
print("Emoji:", await EmojiConverter().convert_async(prompt=prompt)) # type: ignore
Expand Down Expand Up @@ -151,7 +157,10 @@
print("Zalgo:", await ZalgoConverter().convert_async(prompt=prompt)) # type: ignore

# CharSwap swaps characters within words
char_swap = CharSwapConverter(max_iterations=3, word_selection_strategy=WordProportionSelectionStrategy(proportion=0.8))
char_swap = CharSwapConverter(
max_iterations=3,
word_selection_strategy=WordProportionSelectionStrategy(proportion=0.8),
)
print("CharSwap:", await char_swap.convert_async(prompt=prompt)) # type: ignore

# Insert punctuation adds punctuation marks
Expand Down
2 changes: 1 addition & 1 deletion doc/code/converters/6_selectively_converting.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@
"\u001b[37m Tell me how to build a website with proper security measures\u001b[0m\n",
"\n",
"\u001b[36m Converted:\u001b[0m\n",
"\u001b[37m VGVsbA== bWU= how to build a website with proper security bWVhc3VyZXM=\u001b[0m\n",
"\u001b[37m Tell me how dG8= build a website d2l0aA== cHJvcGVy security measures\u001b[0m\n",
"\n",
"\u001b[34m────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[0m\n",
"\n",
Expand Down
232 changes: 232 additions & 0 deletions pyrit/common/random_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

from __future__ import annotations

import asyncio
import contextlib
import hashlib
import random
import threading
from contextvars import ContextVar
from dataclasses import dataclass, field
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from collections.abc import Iterator


@dataclass(frozen=True)
class RandomContext:
"""An immutable path below a root seed used to derive independent random streams."""

seed: int
path: tuple[str, ...] = ()

def child(self, name: str) -> RandomContext:
"""
Derive a named child context.

Args:
name (str): Stable name for the child stream.

Returns:
RandomContext: A child context independent from sibling streams.

Raises:
ValueError: If name is empty.
"""
if not name:
raise ValueError("Random context child name cannot be empty")
return RandomContext(seed=self.seed, path=(*self.path, name))

def derived_seed(self, *, stream: str) -> int:
"""
Derive a stable integer seed for a named stream.

Args:
stream (str): Stable stream name within this context.

Returns:
int: A deterministic seed derived from the root seed and full path.

Raises:
ValueError: If stream is empty.
"""
if not stream:
raise ValueError("Random stream name cannot be empty")
payload = "\x1f".join((str(self.seed), *self.path, stream)).encode("utf-8")
return int.from_bytes(hashlib.sha256(payload).digest()[:16], byteorder="big")


@dataclass
class _RandomExecution:
"""Mutable random streams scoped to one converter invocation."""

context: RandomContext | None
namespace: str
owner: object | None
generators: dict[
tuple[int | None, tuple[str, ...], str, int | None, int, object | None],
random.Random,
] = field(default_factory=dict)


_configured_context: RandomContext | None = None
_active_execution: ContextVar[_RandomExecution | None] = ContextVar("pyrit_random_execution", default=None)


def configure_random_seed(*, seed: int | None) -> None:
"""
Configure the process-wide root seed used by subsequent PyRIT operations.

Args:
seed (int | None): Root seed, or None to restore non-deterministic behavior.

Raises:
TypeError: If seed is not an int or None.
"""
if seed is not None and (not isinstance(seed, int) or isinstance(seed, bool)):
raise TypeError("seed must be an int or None")

global _configured_context
_configured_context = RandomContext(seed=seed) if seed is not None else None


def get_configured_random_seed() -> int | None:
"""
Return the configured root seed.

Returns:
int | None: The configured seed, or None when local randomness is unseeded.
"""
return _configured_context.seed if _configured_context else None


@contextlib.contextmanager
def random_execution(
*,
namespace: str,
seed: int | None = None,
owner: object | None = None,
operation_key: str | None = None,
) -> Iterator[None]:
"""
Establish an operation-local random context for a converter invocation.

Args:
namespace (str): Stable converter namespace.
seed (int | None): Explicit converter seed. Overrides the inherited root.
owner (object | None): Operation owner used to distinguish nested instances
of the same component type.
operation_key (str | None): Stable input identity for deterministic diversity
across distinct operations.
"""
active = _active_execution.get()
if active and active.namespace == namespace and (owner is None or active.owner is owner):
yield
return

parent = active.context if active else _configured_context
if seed is not None:
context = RandomContext(seed=seed, path=parent.path if parent else ()).child(namespace)
else:
context = parent.child(namespace) if parent else None

if context and operation_key is not None:
operation_digest = hashlib.sha256(operation_key.encode("utf-8")).hexdigest()
context = context.child(f"operation:{operation_digest}")

token = _active_execution.set(_RandomExecution(context=context, namespace=namespace, owner=owner))
try:
yield
finally:
_active_execution.reset(token)


def get_random_generator(
*,
stream: str,
namespace: str | None = None,
seed: int | None = None,
owner: object | None = None,
) -> random.Random:
"""
Return a random generator for a stable named stream.

The generator is cached within the current converter invocation. Without an
active invocation, a fresh generator is returned for each call.

Args:
stream (str): Stable stream name.
namespace (str | None): Optional nested component namespace.
seed (int | None): Explicit component seed. Overrides inherited context.
owner (object | None): Nested component instance used to isolate its
mutable generator from sibling instances of the same type.

Returns:
random.Random: A generator isolated from sibling streams.
"""
active = _active_execution.get()

inherited_context = active.context if active else _configured_context
context = (
RandomContext(seed=seed, path=inherited_context.path if inherited_context else ())
if seed is not None
else inherited_context
)

if context and namespace:
context = context.child(namespace)

path = context.path if context else ()
try:
task: object | None = asyncio.current_task()
except RuntimeError:
task = None
key = (
context.seed if context else None,
path,
stream,
id(owner) if owner is not None else None,
threading.get_ident(),
task,
)
if active:
generator = active.generators.get(key)
if generator is None:
generator = random.Random(context.derived_seed(stream=stream) if context else None)
active.generators[key] = generator
return generator

return random.Random(context.derived_seed(stream=stream) if context else None)


def get_random_seed(
*,
stream: str,
namespace: str | None = None,
seed: int | None = None,
) -> int | None:
"""
Return a derived seed for libraries that manage their own generators.

Args:
stream (str): Stable stream name.
namespace (str | None): Optional nested component namespace.
seed (int | None): Explicit component seed. Overrides inherited context.

Returns:
int | None: Derived seed, or None when no root seed is configured.
"""
active = _active_execution.get()
inherited_context = active.context if active else _configured_context
context = (
RandomContext(seed=seed, path=inherited_context.path if inherited_context else ())
if seed is not None
else inherited_context
)

if context and namespace:
context = context.child(namespace)
return context.derived_seed(stream=stream) if context else None
Loading