Skip to content

[BREAKING] FEAT: Normalize simulated conversation prompts to SeedPrompt - #2698

Merged
varunj-msft merged 5 commits into
microsoft:mainfrom
varunj-msft:varunj-msft/12683-Normalize-to-SeedPrompt
Sep 22, 2026
Merged

varunj-msft merged 5 commits into
microsoft:mainfrom
varunj-msft:varunj-msft/12683-Normalize-to-SeedPrompt

Conversation

@varunj-msft

@varunj-msft varunj-msft commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Description

Simulated-conversation techniques carried three YAML file paths through the seed model, the attack parameters, and the async executor, which read those files once per objective in the middle of attack execution. A technique was therefore only inspectable by chasing a file path, and its identity was a set of absolute, machine-specific path strings.

The adversarial, simulated-target, and next-message system prompts are now canonical SeedPrompt values on SeedSimulatedConversation. YAML is loaded at the setup and composition boundaries, and only prompts travel from there, so the executor no longer touches disk. Serialized configuration and identity now describe prompt content instead of file locations, which makes a technique portable across machines and lets an edit to a prompt change its identity.

This follows direction from the review on #2503. That PR was closed unmerged, so none of it was on main — all three prompts needed normalizing, not just the two mentioned in the follow-up work item:

adversarial_chat_system_prompt: SeedPrompt is canonical — not SeedPrompt | Path.
YAML is loaded with SeedPrompt.from_yaml_file(...) at setup/composition boundaries.
Only SeedPrompt is carried through SeedSimulatedConversation, AttackParameters, and the executor.
*_path remains only as a deprecated compatibility adapter and for reading legacy records.
The *_system_prompt_path inputs stay accepted on the model, the technique factory, and the public generator. They are resolved once, emit a DeprecationWarning (removed_in="1.4.0"), and never become model fields — so they never reach persistence or identity. Every shipped technique was migrated off them, so a normal run emits no deprecation warnings.

Composition now reads prompt files where it previously only built paths, so the scenario and initializer entry points that build techniques from an async method run that work in a worker thread.

Defaults, template rendering, response schemas, sequence behavior, and the database schema are unchanged. No Alembic migration is needed — only the JSON payload inside the existing unbounded, unindexed SeedPromptEntries.value column changes shape.

Why this is BREAKING
SeedSimulatedConversation canonical fields are renamed and retyped: adversarial_chat_system_prompt_path: Path becomes adversarial_chat_system_prompt: SeedPrompt, and likewise for the simulated-target and next-message prompts. The *_path spellings still work as deprecated constructor inputs, so existing callers keep working with a warning.
SeedSimulatedConversation.load_simulated_target_system_prompt is deprecated, not removed.

Things reviewers should know
Identity churn. Every simulated-conversation seed's value_sha256 changes. This hash already churned on each release, because pyrit_version is embedded in value.
Row size. Serialized value grows from roughly 200 B to roughly 4–13 KB per technique, since it now carries prompt text. This is the intended cost of carrying prompts instead of paths. SeedEntry.value and AttackResultEntry.atomic_attack_identifier are unbounded, unindexed JSON.
Legacy records with a missing file. A record written earlier names prompt files by absolute path. If those files are not present on the reading machine, get_seed() now fails at read naming the offending record (id, name, dataset) rather than failing later at execution. A silent placeholder was deliberately rejected — it would fabricate prompt content.
Legacy read then re-add. Reading a legacy path-shaped record and re-adding it can hit a pre-existing hash/id collision in add_seeds_to_memory_async, which dedupes purely by hash and reuses the row id. The same collision already occurs on main whenever value drifts across versions. Not fixed here, since it is in shared insert logic and out of scope for this change.
Overlap with #2494. That PR touches six of the same files and adds adversarial_chat_system_prompt_prefix to the same two identity methods (_compute_value, get_identifier). Now that the adversarial prompt is a real SeedPrompt, that prefix can be prepended to prompt.value directly. Worth agreeing on merge order and whether the prefix is still needed. #2551 also touches three of these files and gates on next_message_system_prompt_path, which becomes next_message_system_prompt.

Tests and Documentation

9369 unit tests pass across models, executor/attack, scenario, setup, memory, and datasets.
Ruff check/format, ty, check-async-suffix, validate-docs, and memory-migrations check ("No new upgrade operations detected") all pass.
New coverage: canonical construction and defaults; a guard that *_path never becomes a model field; deprecated-input warnings; supplying both a prompt and its path; content-based identity, including that editing one word changes the hash and that two copies of the same text from different files match; model_dump/model_validate stability in both modes; legacy and canonical record reconstruction; a named error for a legacy record whose file is missing; and a test asserting the async path performs no YAML loading at all.
Regression coverage for template preparation: a prompt is prepared exactly once, so {% raw %} deferred template syntax survives construction, the deprecated path input, and reconstruction from a persisted record.
JupyText: updated both halves of the doc/code/datasets/5_simulated_conversation and doc/code/datasets/2_seed_programming pairs inline, then verified parity by regenerating .py from each .ipynb with jupytext --to py:percent and diffing against the committed .py. Both are in sync. Also updated doc/code/memory/3_memory_data_types.md.
Exercised end to end against live Azure endpoints: output matches the pre-change baseline (4 prepended turns plus a generated next message, with preparation and adversarial conversation references preserved).

Simulated-conversation techniques carried three YAML file paths through the
seed model, the attack parameters, and the async executor, which read those
files once per objective in the middle of attack execution. A technique was
therefore only inspectable by chasing a path, and its identity was a set of
absolute, machine-specific path strings.

The adversarial, simulated-target, and next-message system prompts are now
canonical SeedPrompt values on SeedSimulatedConversation. YAML is loaded at the
setup and composition boundaries, and only prompts travel from there, so the
executor no longer touches disk. Serialized configuration and identity now
describe prompt content instead of file locations, which makes a technique
portable and lets an edit to a prompt change its identity.

The matching *_system_prompt_path inputs stay accepted on the model, the
technique factory, and the public generator for legacy callers and for records
persisted before this change; they are resolved once, warn, and never become
fields. A record written earlier still loads as long as the prompt files it
names are present, and reports which record is at fault when they are not.

Composition now reads prompt files where it previously only built paths, so the
scenario and initializer entry points that build techniques from an async
method run that work in a worker thread.

Defaults, template rendering, response schemas, sequence behavior, and the
database schema are unchanged.
@hannahwestra25 hannahwestra25 self-assigned this Sep 21, 2026
Comment thread pyrit/executor/attack/multi_turn/simulated_conversation.py Outdated
Comment thread pyrit/scenario/core/attack_technique_factory.py
Comment thread pyrit/scenario/core/attack_technique_factory.py Outdated
Comment thread pyrit/models/seeds/seed_simulated_conversation.py Outdated
Resolve the review notes on the simulated-conversation prompt normalization.

Describe the executor's next-message parameter as the SeedPrompt it now is
rather than a bare template, and apply the same wording to the three
SeedSimulatedConversation attribute docs so the terminology is consistent
rather than corrected in one place.

Resolve the simulated-target default inside with_simulated_conversation, so
all three prompts reach SeedSimulatedConversation as concrete SeedPrompt
values instead of one arriving as None and being backfilled by a field
validator. Serialized configuration and identity are unchanged.

Promote resolve_prompt_source and warn_prompt_path_deprecated to public
names and export them from pyrit.models.seeds and pyrit.models, so the
scenario and executor layers no longer reach into a private helper across a
package boundary.

Drop the invented placeholder from __repr__. The prompt name is descriptive
metadata that identity deliberately discards, so a seed rebuilt from a
persisted record never has one; the fragment is now omitted in that case
instead of printing a placeholder or a bare None.
…repr

The representation now omits the adversarial prompt fragment when the prompt
carries no name, which is the usual state for a seed rebuilt from a persisted
record. Assert that branch directly so it is not left to the named case.
@varunj-msft
varunj-msft added this pull request to the merge queue Sep 22, 2026
Merged via the queue into microsoft:main with commit 9af8dd5 Sep 22, 2026
49 checks passed
@varunj-msft
varunj-msft deleted the varunj-msft/12683-Normalize-to-SeedPrompt branch September 22, 2026 16:48
hannahwestra25 pushed a commit to hannahwestra25/PyRIT that referenced this pull request Sep 22, 2026
…dPrompt

Integrates PR microsoft#2698's canonical SeedPrompt design for
SeedSimulatedConversation, replacing the old *_path-only fields with
SeedPrompt fields plus deprecated path shims. Reconciles our
with_layered_prefix() to compose the prefix directly into
SeedPrompt.value (via SeedPrompt.compose_with_prefix) instead of
storing a separate prefix field, per reviewer suggestion.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
hannahwestra25 pushed a commit that referenced this pull request Sep 22, 2026
Resolves a conflict in simulated_conversation.py where main's SeedPrompt
normalization (#2698) renamed the next-message system prompt parameter while
this branch added typed preparation-failure handling around the same call.
The resolution keeps both: the guarded call now uses the resolved
next_message_system_prompt SeedPrompt and retains the
AttackPreparationFailure short-circuit and blocked-response handling.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants