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
6 changes: 6 additions & 0 deletions rampart/pyrit_bridge/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

"""PyRIT integration bridge."""

from rampart.pyrit_bridge.converter_bridge import (
PyRITConverterBridge,
adapt_converter,
)
from rampart.pyrit_bridge.llm_bridge import (
create_prompt_target,
send_generation_request_async,
Expand All @@ -11,6 +15,8 @@
)

__all__ = [
"PyRITConverterBridge",
"adapt_converter",
"create_prompt_target",
"send_generation_request_async",
"send_judge_request_async",
Expand Down
82 changes: 82 additions & 0 deletions rampart/pyrit_bridge/converter_bridge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

"""PyRIT converter bridge — adapt PyRIT PromptConverters to RAMPART PayloadConverter.

Enables RAMPART to leverage PyRIT's rich library of prompt converters
(Base64, Atbash, Caesar, Unicode, ROT13, Leetspeak, etc.) while adhering
to RAMPART's ``PayloadConverter`` protocol.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from rampart.core.types import Payload, PayloadFormat

if TYPE_CHECKING:
from pyrit.prompt_converter import PromptConverter


class PyRITConverterBridge:
"""Adapt a PyRIT ``PromptConverter`` to RAMPART's ``PayloadConverter`` protocol.

Runs the underlying PyRIT prompt converter on the payload's text content,
returning a new ``Payload`` with the transformed text, preserved identifier,
format set to TEXT, and updated metadata tagging the converter.

Attributes:
converter: The wrapped PyRIT PromptConverter instance.
"""

def __init__(self, converter: PromptConverter) -> None:
"""Initialize with a PyRIT PromptConverter instance.

Args:
converter (PromptConverter): The PyRIT PromptConverter to adapt.
"""
self.converter = converter
self._name = converter.__class__.__name__

async def convert_async(self, *, payload: Payload) -> Payload:
"""Transform a text payload using the underlying PyRIT converter.

Args:
payload (Payload): A text-format payload to convert.

Returns:
Payload: A new payload with transformed text content.

Raises:
ValueError: If the payload format is not a text format.
"""
if not payload.format.is_text:
msg = f"{self._name} requires a text payload, got {payload.format.value}."
raise ValueError(msg)

result = await self.converter.convert_async(
prompt=payload.content,
input_type="text",
)

metadata = {**payload.metadata, "converter": self._name}

return Payload(
content=result.output_text,
id=payload.id,
format=PayloadFormat.TEXT,
artifact=None,
metadata=metadata,
)


def adapt_converter(converter: PromptConverter) -> PyRITConverterBridge:
"""Adapt a PyRIT PromptConverter to a RAMPART PayloadConverter.

Args:
converter (PromptConverter): The PyRIT PromptConverter to wrap.

Returns:
PyRITConverterBridge: An adapter fulfilling the PayloadConverter protocol.
"""
return PyRITConverterBridge(converter)
107 changes: 107 additions & 0 deletions tests/unit/pyrit_bridge/test_converter_bridge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

"""Tests for the PyRIT converter bridge.

Validates adaptation of PyRIT PromptConverters to RAMPART's
PayloadConverter protocol and verifies text transformation semantics.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest
from pyrit.prompt_converter import Base64Converter, ROT13Converter

from rampart.core.converter import PayloadConverter
from rampart.core.types import Payload, PayloadFormat
from rampart.pyrit_bridge.converter_bridge import (
PyRITConverterBridge,
adapt_converter,
)

if TYPE_CHECKING:
from pathlib import Path


class TestPyRITConverterBridgeProtocol:
"""Verify PyRITConverterBridge satisfies PayloadConverter protocol."""

def test_implements_payload_converter_protocol(self) -> None:
bridge = PyRITConverterBridge(Base64Converter())
assert isinstance(bridge, PayloadConverter)

def test_adapt_converter_helper(self) -> None:
bridge = adapt_converter(ROT13Converter())
assert isinstance(bridge, PayloadConverter)
assert isinstance(bridge, PyRITConverterBridge)


class TestPyRITConverterBridgeConversion:
"""Verify conversion behavior across PyRIT converters."""

@pytest.mark.asyncio
async def test_base64_conversion(self) -> None:
bridge = adapt_converter(Base64Converter())
payload = Payload(
content="Ignore instructions",
id="test-p1",
format=PayloadFormat.TEXT,
metadata={"source": "unit_test"},
)

result = await bridge.convert_async(payload=payload)

assert result.id == "test-p1"
assert result.content == "SWdub3JlIGluc3RydWN0aW9ucw=="
assert result.format == PayloadFormat.TEXT
assert result.artifact is None
assert result.metadata["source"] == "unit_test"
assert result.metadata["converter"] == "Base64Converter"

@pytest.mark.asyncio
async def test_rot13_conversion(self) -> None:
bridge = adapt_converter(ROT13Converter())
payload = Payload(
content="Hello World",
id="test-p2",
format=PayloadFormat.TEXT,
)

result = await bridge.convert_async(payload=payload)

assert result.id == "test-p2"
assert result.content == "Uryyb Jbeyq"
assert result.format == PayloadFormat.TEXT
assert result.metadata["converter"] == "ROT13Converter"

@pytest.mark.asyncio
async def test_empty_content_conversion(self) -> None:
bridge = adapt_converter(Base64Converter())
payload = Payload(
content="",
id="test-empty",
format=PayloadFormat.TEXT,
)

result = await bridge.convert_async(payload=payload)

assert result.id == "test-empty"
assert result.content == ""
assert result.format == PayloadFormat.TEXT

@pytest.mark.asyncio
async def test_non_text_payload_raises_value_error(self, tmp_path: Path) -> None:
artifact = tmp_path / "test.docx"
artifact.touch()
bridge = adapt_converter(Base64Converter())
payload = Payload(
content="Binary content",
id="test-docx",
format=PayloadFormat.DOCX,
artifact=artifact,
)

with pytest.raises(ValueError, match="requires a text payload"):
await bridge.convert_async(payload=payload)