Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e2da267
add ability to query target capabilities
hannahwestra25 May 7, 2026
7aeef2f
FEAT deprecate prompt chat (#1678)
hannahwestra25 May 8, 2026
a3873b5
merge
hannahwestra25 May 8, 2026
c912285
resolve merge
hannahwestra25 May 8, 2026
026aa54
fix / clarify misc issues
hannahwestra25 May 8, 2026
d5d902a
add more tests
hannahwestra25 May 8, 2026
632a110
pre-commit
hannahwestra25 May 8, 2026
bfe4fbf
add documentation
hannahwestra25 May 8, 2026
84a3736
add retries
hannahwestra25 May 8, 2026
6e061e3
treat empty response as failure and expose test_modalities and capabi…
hannahwestra25 May 8, 2026
9c3fad1
preserve capabilities and debug flag
hannahwestra25 May 8, 2026
cb8fc68
pre-commit
hannahwestra25 May 8, 2026
b47d354
Merge branch 'main' of https://github.com/microsoft/PyRIT into hawest…
hannahwestra25 May 11, 2026
d085ee4
address comments
hannahwestra25 May 11, 2026
d892f06
fix docstring
hannahwestra25 May 11, 2026
287aba1
Merge branch 'main' of https://github.com/microsoft/PyRIT into hawest…
hannahwestra25 May 11, 2026
0f5be0a
clean up comments
hannahwestra25 May 11, 2026
a347b1c
add assets and comments
hannahwestra25 May 11, 2026
2cbae68
fix bad test
hannahwestra25 May 11, 2026
972777c
pre-commit
hannahwestra25 May 11, 2026
79500e0
fix test
hannahwestra25 May 11, 2026
3934ed5
rename to discover
hannahwestra25 May 11, 2026
20551e6
Merge branch 'main' of https://github.com/microsoft/PyRIT into hawest…
hannahwestra25 May 12, 2026
8363beb
add backoff and use fresh config
hannahwestra25 May 12, 2026
55642f8
Merge branch 'main' of https://github.com/microsoft/PyRIT into hawest…
hannahwestra25 May 12, 2026
52c56e6
add logging for classes that don't enforce json capabilities
hannahwestra25 May 12, 2026
b8478a2
ghcp pr review
hannahwestra25 May 12, 2026
18259d2
add apply function and update docs
hannahwestra25 May 13, 2026
8effccc
Merge branch 'main' of https://github.com/microsoft/PyRIT into hawest…
hannahwestra25 May 13, 2026
5fa2572
rename and add apply flag
hannahwestra25 May 13, 2026
96cffc8
move to private functions
hannahwestra25 May 13, 2026
d1b714c
Merge branch 'main' of https://github.com/microsoft/PyRIT into hawest…
hannahwestra25 May 13, 2026
25d0a9f
remove query_target_capabilities
hannahwestra25 May 13, 2026
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
16 changes: 15 additions & 1 deletion doc/code/targets/0_prompt_targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ A `PromptTarget` is a generic place to send a prompt. With PyRIT, the idea is th

With some algorithms, you want to send a prompt, set a system prompt, and modify conversation history (including PAIR [@chao2023pair], TAP [@mehrotra2023tap], and flip attack [@li2024flipattack]). These algorithms require a target whose [`TargetCapabilities`](#target-capabilities) declare both `supports_multi_turn=True` and `supports_editable_history=True` — i.e. you can modify a conversation history. Consumers express this requirement via `CHAT_TARGET_REQUIREMENTS` and validate it against `target.configuration` at construction time. See [Target Capabilities](#target-capabilities) below for the full list of capabilities and how they compose into a `TargetConfiguration`.

Note: The previous `PromptChatTarget` class is **deprecated** as of v0.13.0 and will be removed in v0.15.0. Use `PromptTarget` directly with a `TargetConfiguration` declaring `supports_multi_turn=True` and `supports_editable_history=True`. See [Target Capabilities](#target-capabilities) for details.
Note: The previous `PromptChatTarget` class is **deprecated** as of v0.14.0 and will be removed in v0.16.0. Use `PromptTarget` directly with a `TargetConfiguration` declaring `supports_multi_turn=True` and `supports_editable_history=True`. See [Target Capabilities](#target-capabilities) for details.


Here are some examples:
Expand Down Expand Up @@ -107,6 +107,20 @@ target = MyHTTPTarget(custom_configuration=config, ...)

The full implementation lives in [`pyrit/prompt_target/common/target_capabilities.py`](https://github.com/microsoft/PyRIT/blob/main/pyrit/prompt_target/common/target_capabilities.py) and [`pyrit/prompt_target/common/target_configuration.py`](https://github.com/microsoft/PyRIT/blob/main/pyrit/prompt_target/common/target_configuration.py). For runnable examples — inspecting capabilities on a real target, comparing known model profiles, and `ADAPT` vs `RAISE` in action — see [Target Capabilities](./6_1_target_capabilities.ipynb).

### Discovering live target capabilities

Declared capabilities describe what a target *should* support. For deployments where actual behavior is uncertain — custom OpenAI-compatible endpoints, gateways that strip features, models whose support drifts — you can probe what the target *actually* accepts at runtime:

```python
from pyrit.prompt_target import discover_target_capabilities_async

# Probe boolean capabilities and input modalities, returning a
# best-effort TargetCapabilities:
queried = await discover_target_capabilities_async(target=target)
```

Each probe sends a minimal request (bounded by `per_probe_timeout_s`, default 30s, with one retry on transient errors) and only marks a capability or modality as supported if the call returns cleanly. `discover_target_capabilities_async` returns a merged view: probed where possible, declared where probing is unavailable or out of scope. "Supported" here means *the request was accepted* — a target that silently ignores a system prompt or `response_format` directive is still reported as supporting it, so validate response content out of band when the distinction matters. This function is not safe to call concurrently with other operations on the same target instance: it temporarily mutates `target._configuration` and writes probe rows to memory (rows are tagged with `prompt_metadata["capability_probe"] == "1"` for filtering). See [Target Capabilities](./6_1_target_capabilities.ipynb) for runnable examples.

## Multi-Modal Targets

Like most of PyRIT, targets can be multi-modal.
Expand Down
297 changes: 288 additions & 9 deletions doc/code/targets/6_1_target_capabilities.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"No new upgrade operations detected.\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"No new upgrade operations detected.\n",
"supports_multi_turn: True\n",
"supports_editable_history: True\n",
"supports_system_prompt: True\n",
Expand Down Expand Up @@ -462,12 +456,297 @@
"try:\n",
" no_editable_history.ensure_can_handle(capability=CapabilityName.EDITABLE_HISTORY)\n",
"except ValueError as exc:\n",
" print(exc)\n",
"# ---"
" print(exc)"
]
},
{
"cell_type": "markdown",
"id": "19",
"metadata": {},
"source": [
"## 7. Discovering live target capabilities\n",
"\n",
"Declared capabilities describe what a target *should* support. For deployments where the actual\n",
"behavior is uncertain — custom OpenAI-compatible endpoints, gateways that strip features, models\n",
"whose support drifts over time — you can probe what the target *actually* accepts at runtime with\n",
"`discover_target_capabilities_async`. It runs both the boolean capability probes and the input\n",
"modality probes and returns a best-effort `TargetCapabilities`.\n",
"\n",
"Internally it walks each capability that has a registered probe (currently\n",
"`SYSTEM_PROMPT`, `MULTI_MESSAGE_PIECES`, `MULTI_TURN`, `JSON_OUTPUT`, `JSON_SCHEMA`), sends a\n",
"minimal request, and includes the capability in the result only if the call succeeds.\n",
"During probing the target's configuration is temporarily replaced with a permissive one so\n",
"`ensure_can_handle` does not short-circuit a probe for a capability the target declares as\n",
"unsupported. The original configuration is restored before the function returns. The same\n",
"treatment is applied to each input modality combination declared in\n",
"`capabilities.input_modalities`, sending a small payload built from optional `test_assets`.\n",
"\n",
"Each probe call is bounded by `per_probe_timeout_s` (default 30s) and is retried once on\n",
"transient errors before being declared failed. The returned `TargetCapabilities` is a merged\n",
"view: probed where possible, declared where probing is unavailable or out of scope.\n",
"\"Supported\" here means *the request was accepted* — a target that silently ignores a system\n",
"prompt or `response_format` directive will still be reported as supporting that capability.\n",
"\n",
"This function is **not safe to call concurrently** with other operations on the same target\n",
"instance: it temporarily mutates `target._configuration` and writes probe rows to\n",
"`target._memory`. Probe-written memory rows are tagged with\n",
"`prompt_metadata[\"capability_probe\"] == \"1\"` so consumers can filter them.\n",
"\n",
"Typical usage against a real endpoint:\n",
"\n",
"```python\n",
"from pyrit.prompt_target import discover_target_capabilities_async\n",
"\n",
"queried = await discover_target_capabilities_async(target=target)\n",
"print(queried)\n",
"```\n",
"\n",
"Below we mock the target's underlying transport (`_send_prompt_to_target_async`) so the notebook\n",
"stays self-contained — the result shape is the same as a live run. We mock the protected method\n",
"rather than `send_prompt_async` so the probe still exercises the real validation and memory\n",
"pipeline."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "20",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"queried capabilities:\n",
" - supports_editable_history\n",
" - supports_json_output\n",
" - supports_json_schema\n",
" - supports_multi_message_pieces\n",
" - supports_multi_turn\n",
" - supports_system_prompt\n"
]
}
],
"source": [
"from unittest.mock import AsyncMock\n",
"\n",
"from pyrit.models import MessagePiece\n",
"from pyrit.prompt_target import discover_target_capabilities_async\n",
"\n",
"\n",
"def _ok_response():\n",
" return [\n",
" Message(\n",
" [\n",
" MessagePiece(\n",
" role=\"assistant\",\n",
" original_value=\"ok\",\n",
" original_value_data_type=\"text\",\n",
" conversation_id=\"probe\",\n",
" response_error=\"none\",\n",
" )\n",
" ]\n",
" )\n",
" ]\n",
"\n",
"\n",
"probe_target = OpenAIChatTarget(model_name=\"gpt-4o\", endpoint=\"https://example.invalid/\", api_key=\"sk-not-a-real-key\")\n",
"probe_target._send_prompt_to_target_async = AsyncMock(return_value=_ok_response()) # type: ignore[method-assign]\n",
"\n",
"queried = await discover_target_capabilities_async(target=probe_target, per_probe_timeout_s=5.0) # type: ignore\n",
"print(\"discover_target_capabilities_async result:\")\n",
"print(f\" supports_multi_turn: {queried.supports_multi_turn}\")\n",
"print(f\" supports_system_prompt: {queried.supports_system_prompt}\")\n",
"print(f\" supports_multi_message_pieces: {queried.supports_multi_message_pieces}\")\n",
"print(f\" supports_json_output: {queried.supports_json_output}\")\n",
"print(f\" supports_json_schema: {queried.supports_json_schema}\")\n",
"print(f\" input_modalities: {sorted(sorted(m) for m in queried.input_modalities)}\")"
]
},
{
"cell_type": "markdown",
"id": "21",
"metadata": {},
"source": [
"To narrow the probe to specific capabilities (faster, fewer calls), pass `capabilities=`:\n",
"\n",
"```python\n",
"from pyrit.prompt_target.common.target_capabilities import CapabilityName\n",
"\n",
"queried = await discover_target_capabilities_async(\n",
" target=target,\n",
" capabilities=[CapabilityName.JSON_SCHEMA, CapabilityName.SYSTEM_PROMPT],\n",
")\n",
"```\n",
"\n",
"Similarly, narrow the modality probe set with `test_modalities=` and override the\n",
"packaged default probe assets with `test_assets=`."
]
},
{
"cell_type": "markdown",
"id": "22",
"metadata": {},
"source": [
"### Discovering undeclared modalities\n",
"\n",
"By default `discover_target_capabilities_async` only probes modality combinations the target already\n",
"**declares** in `capabilities.input_modalities`. For an OpenAI-compatible endpoint that\n",
"claims text-only but might actually accept images, pass `test_modalities=` explicitly to\n",
"probe combinations beyond the declared baseline. Provide `test_assets=` as well if you need\n",
"to override the packaged defaults or probe a modality without one:\n",
"\n",
"```python\n",
"queried = await discover_target_capabilities_async(\n",
" target=target,\n",
" test_modalities={frozenset({\"text\"}), frozenset({\"text\", \"image_path\"})},\n",
" test_assets={\"image_path\": \"/path/to/test_image.png\"},\n",
")\n",
"```\n",
"\n",
"Similarly, when narrowing the probe set with `capabilities=`, capabilities NOT in the\n",
"narrowed set are copied from the target's declared values rather than being reset to\n",
"`False` — narrowing controls *what is re-queried*, not what the returned dataclass\n",
"reports. This makes incremental probing safe:\n",
"\n",
"```python\n",
"# Re-query only JSON support; other declared flags pass through unchanged.\n",
"queried = await discover_target_capabilities_async(\n",
" target=target,\n",
" capabilities={CapabilityName.JSON_OUTPUT, CapabilityName.JSON_SCHEMA},\n",
")\n",
"```"
]
},
{
"cell_type": "markdown",
"id": "23",
"metadata": {},
"source": [
"## 8. Applying probed capabilities back onto the target\n",
"\n",
"`discover_target_capabilities_async` is intentionally pure: it returns a `TargetCapabilities` without\n",
"mutating the target. That lets you inspect (or diff against the declared view, log, gate on\n",
"the result) before committing. Once you're satisfied, call `target.apply_capabilities(...)`\n",
"to install the probed view on the instance. The target's existing\n",
"`CapabilityHandlingPolicy` is preserved — policy expresses user intent (ADAPT vs RAISE),\n",
"which is independent of what the probe found.\n",
"\n",
"Why a two-step pattern rather than auto-apply? Probe results are an upper bound\n",
"(\"the request was accepted\"); a target that silently ignores a feature still passes its\n",
"probe. Keeping discovery separate from application lets callers diff, log, persist, or\n",
"reject the result before it affects subsequent sends.\n",
"\n",
"Below is the end-to-end pattern: construct a target whose declared capabilities are\n",
"pessimistic, discover what the endpoint actually accepts, diff the two views, then apply."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "24",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"declared (before probing):\n",
" supports_multi_turn: False\n",
" supports_system_prompt: False\n",
" supports_json_output: False\n",
"\n",
"probed (returned from discover_target_capabilities_async, target NOT yet updated):\n",
" supports_multi_turn: True\n",
" supports_system_prompt: True\n",
" supports_json_output: True\n",
" target.capabilities.supports_multi_turn (still declared): False\n",
"\n",
"flags probed True that were declared False: ['supports_multi_turn', 'supports_system_prompt', 'supports_multi_message_pieces', 'supports_json_output', 'supports_json_schema']\n",
"\n",
"after apply_capabilities:\n",
" supports_multi_turn: True\n",
" supports_system_prompt: True\n",
" supports_json_output: True\n",
" policy preserved: True\n",
"\n",
"CHAT_TARGET_REQUIREMENTS.validate now passes against the probed target\n"
]
}
],
"source": [
"# Start with an instance that declares fewer capabilities than the endpoint actually has,\n",
"# e.g. a custom gateway whose support we're unsure about.\n",
"pessimistic_config = TargetConfiguration(\n",
" capabilities=TargetCapabilities(\n",
" supports_multi_turn=False,\n",
" supports_system_prompt=False,\n",
" supports_multi_message_pieces=False,\n",
" supports_json_output=False,\n",
" supports_json_schema=False,\n",
" # Editable history has no live probe and falls back to the declared value.\n",
" # Declare it True here so the probed view inherits it.\n",
" supports_editable_history=True,\n",
" ),\n",
")\n",
"endpoint_target = OpenAIChatTarget(\n",
" model_name=\"custom-model\",\n",
" endpoint=\"https://example.invalid/\",\n",
" api_key=\"sk-not-a-real-key\",\n",
" custom_configuration=pessimistic_config,\n",
")\n",
"endpoint_target._send_prompt_to_target_async = AsyncMock(return_value=_ok_response()) # type: ignore[method-assign]\n",
"\n",
"print(\"declared (before probing):\")\n",
"print(f\" supports_multi_turn: {endpoint_target.capabilities.supports_multi_turn}\")\n",
"print(f\" supports_system_prompt: {endpoint_target.capabilities.supports_system_prompt}\")\n",
"print(f\" supports_json_output: {endpoint_target.capabilities.supports_json_output}\")\n",
"\n",
"# Step 1: discover. No mutation yet — `endpoint_target.capabilities` is unchanged.\n",
"probed_caps = await discover_target_capabilities_async(target=endpoint_target, per_probe_timeout_s=5.0) # type: ignore\n",
"\n",
"print(\"\\nprobed (returned from discover_target_capabilities_async, target NOT yet updated):\")\n",
"print(f\" supports_multi_turn: {probed_caps.supports_multi_turn}\")\n",
"print(f\" supports_system_prompt: {probed_caps.supports_system_prompt}\")\n",
"print(f\" supports_json_output: {probed_caps.supports_json_output}\")\n",
"print(f\" target.capabilities.supports_multi_turn (still declared): {endpoint_target.capabilities.supports_multi_turn}\")\n",
"\n",
"# Step 2: diff — see exactly what the probe upgraded.\n",
"declared = pessimistic_config.capabilities\n",
"upgraded = [\n",
" name\n",
" for name in (\n",
" \"supports_multi_turn\",\n",
" \"supports_system_prompt\",\n",
" \"supports_multi_message_pieces\",\n",
" \"supports_json_output\",\n",
" \"supports_json_schema\",\n",
" )\n",
" if getattr(probed_caps, name) and not getattr(declared, name)\n",
"]\n",
"print(f\"\\nflags probed True that were declared False: {upgraded}\")\n",
"\n",
"# Step 3: apply. Policy is preserved; the normalization pipeline is rebuilt.\n",
"original_policy = endpoint_target.configuration.policy\n",
"endpoint_target.apply_capabilities(capabilities=probed_caps)\n",
"\n",
"print(\"\\nafter apply_capabilities:\")\n",
"print(f\" supports_multi_turn: {endpoint_target.capabilities.supports_multi_turn}\")\n",
"print(f\" supports_system_prompt: {endpoint_target.capabilities.supports_system_prompt}\")\n",
"print(f\" supports_json_output: {endpoint_target.capabilities.supports_json_output}\")\n",
"print(f\" policy preserved: {endpoint_target.configuration.policy is original_policy}\")\n",
"\n",
"# Subsequent consumer checks now reflect the probed reality — for example, a chat-style\n",
"# requirement that would have failed against the pessimistic declaration now passes.\n",
"CHAT_TARGET_REQUIREMENTS.validate(target=endpoint_target)\n",
"print(\"\\nCHAT_TARGET_REQUIREMENTS.validate now passes against the probed target\")"
]
}
],
"metadata": {
"jupytext": {
"main_language": "python"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
Expand Down
Loading
Loading