[BREAKING] FEAT: Normalize simulated conversation prompts to SeedPrompt - #2698
Merged
varunj-msft merged 5 commits intoSep 22, 2026
Merged
varunj-msft merged 5 commits into
varunj-msft merged 5 commits into
Conversation
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
approved these changes
Sep 21, 2026
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
enabled auto-merge
September 22, 2026 16:00
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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).