Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

WebSocket Target

WebsocketTarget connects PyRIT to services that use a custom WebSocket protocol. Supply the service-specific initialization messages, prompt builder, and response parser. The protocol_identifier is a non-secret name for this complete protocol configuration.

This example starts a local PyRIT WebSocket service. It exercises the real WebSocket transport without credentials or an external endpoint.

import json
import uuid

from websockets.asyncio.client import ClientConnection
from websockets.asyncio.server import ServerConnection, serve

from pyrit.models import Message, MessagePiece
from pyrit.prompt_target import WebsocketTarget
from pyrit.setup import IN_MEMORY, initialize_pyrit_async

await initialize_pyrit_async(memory_db_type=IN_MEMORY, load_defaults=False, silent=True)  # type: ignore


async def pyrit_websocket_handler(websocket: ServerConnection) -> None:
    initialization = json.loads(await websocket.recv())
    if initialization != {"type": "initialize", "client": "PyRIT"}:
        await websocket.close(code=1002, reason="Invalid initialization message")
        return

    await websocket.send(json.dumps({"message": "PyRIT WebSocket target ready"}))

    async for raw_message in websocket:
        request = json.loads(raw_message)
        if request["type"] == "restore":
            await websocket.send(json.dumps({"type": "restored"}))
            continue

        await websocket.send(json.dumps({"event": "processing"}))
        await websocket.send(json.dumps({"message": f"PyRIT received: {request['prompt']}"}))


def response_parser(message: str | bytes) -> str | None:
    if isinstance(message, bytes):
        message = message.decode()
    return json.loads(message).get("message")


def message_builder(prompt: str) -> str:
    return json.dumps({"type": "prompt", "prompt": prompt})


async def restore_conversation_async(
    websocket: ClientConnection,
    conversation_history: list[Message],
) -> None:
    history = [
        {
            "role": message.message_pieces[0].role,
            "content": message.get_value(),
        }
        for message in conversation_history
    ]
    await websocket.send(json.dumps({"type": "restore", "history": history}))
    acknowledgement = json.loads(await websocket.recv())
    if acknowledgement != {"type": "restored"}:
        raise ConnectionError("The WebSocket service did not restore the conversation.")

Start the local service on an available loopback port, then configure the target for its protocol.

The restore callback is service-specific. PyRIT calls it when a multi-turn conversation needs a replacement connection. Without this callback, the target fails instead of silently losing history.

server = await serve(pyrit_websocket_handler, "127.0.0.1", 0)  # type: ignore
port = server.sockets[0].getsockname()[1]

target = WebsocketTarget(
    endpoint=f"ws://127.0.0.1:{port}",
    protocol_identifier="local-pyrit-echo-v1",
    initialization_strings=[json.dumps({"type": "initialize", "client": "PyRIT"})],
    response_parser=response_parser,
    message_builder=message_builder,
    conversation_restore_callback=restore_conversation_async,
    discard_initial_messages=1,
)

Send a prompt through the target and close both sides of the connection.

Cleanup is terminal for this target instance. If a connection fails while a prompt is in progress, the target discards that connection and raises the error instead of retrying the prompt.

request = MessagePiece(
    role="user",
    original_value="Hello",
    original_value_data_type="text",
    conversation_id=str(uuid.uuid4()),
).to_message()

try:
    response = await target.send_prompt_async(message=request)  # type: ignore
    print(response[0].get_value())
finally:
    await target.cleanup_target_async()  # type: ignore
    server.close()
    await server.wait_closed()  # type: ignore
PyRIT received: Hello