Skip to content

FEAT: Add remove_seeds_from_memory_async to memory - #2243

Open
blahdeblahde wants to merge 1 commit into
microsoft:mainfrom
blahdeblahde:blahdeblahde-remove-seeds-from-memory-api
Open

FEAT: Add remove_seeds_from_memory_async to memory#2243
blahdeblahde wants to merge 1 commit into
microsoft:mainfrom
blahdeblahde:blahdeblahde-remove-seeds-from-memory-api

Conversation

@blahdeblahde

@blahdeblahde blahdeblahde commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Description

PyRIT memory can add seed prompt datasets (via add_seed_datasets_to_memory_async / add_seeds_to_memory_async) and query them with get_seeds, but there is no counterpart for removing seeds. Today users have to drop into raw SQL, which is error-prone and differs across the supported backends (SQLite, Azure SQL).

This PR adds two removal methods that accept the same filtering parameters as get_seeds (dataset_name, dataset_name_pattern, added_by, harm_categories, authors, groups, source, value_sha256, data_types, seed_type, parameters, metadata, prompt_group_ids), plus an exact flag that controls how value is matched:

  • remove_seeds_from_memory — removes the individual seeds that match the filters and returns the count removed.
  • remove_seed_groups_from_memory — removes entire seed groups: any match is expanded to its whole prompt_group_id so partial groups are never left behind (e.g. filtering by one modality removes the whole multimodal group).

Recommended workflow — preview, then delete with the same filters:

# See what matches before deleting
to_delete = memory.get_seeds(dataset_name="illegal")

# Remove them; returns the count deleted
removed = memory.remove_seeds_from_memory(dataset_name="illegal")

# Or remove whole groups (preserves group integrity)
removed_groups = memory.remove_seed_groups_from_memory(dataset_name="illegal")

Implementation

  • Extracted the filter-building logic out of get_seeds into a shared private helper (_build_seed_filter_conditions) so query and removal build conditions from a single source and cannot drift.
  • Both methods delete in a single transaction using the SQLAlchemy ORM (rollback on error), so they behave consistently across SQLite and Azure SQL.
  • remove_seed_groups_from_memory expands matches to whole groups with a server-side subquery (prompt_group_id.in_(select(...))) rather than materializing the group IDs into Python and sending them back as an IN (...) list. This avoids exceeding the backend bound-parameter limit (e.g. Azure SQL) on broad filters. Seeds with a NULL prompt_group_id (e.g. added individually rather than as a group) are excluded and skipped — use remove_seeds_from_memory for those.
  • The methods are synchronous, mirroring get_seeds: they perform only database work and never await I/O, so making them async would just wrap blocking calls in a coroutine and stall the event loop.

Safety

  • At least one filter must be provided; a no-filter call raises ValueError to prevent accidentally wiping the entire seed database.
  • Value matching is controlled by an exact flag. For deletion, matching by substring is a footgun (a short/common value can over-match), so the remove methods default to exact=True (full-string equality). get_seeds keeps its substring behavior, so its helper default stays exact=False; pass exact=False to the remove methods to opt into substring deletion when you really want it. This intentional difference is documented in the docstrings and docs, along with the group-integrity consequences of deleting individual seeds.
  • Only database records are removed. For file-backed seeds (image_path, audio_path, video_path) the serialized file on disk is left in place; this is documented so callers can delete those files separately if needed.

Tests and Documentation

Teststests/unit/memory/memory_interface/test_interface_remove_seeds.py covers, for remove_seeds_from_memory: every individual filter (dataset_name, dataset_name_pattern, added_by, source, harm_categories, authors, groups, parameters, metadata, data_types, seed_type, value_sha256, prompt_group_ids), exact-by-default matching vs opt-in exact=False substring, multi-filter narrowing, no-filter ValueError, zero-match, and rollback-on-error; and for remove_seed_groups_from_memory: whole-group removal, groups spanning datasets, non-matching groups preserved, ungrouped (NULL group id) matches skipped, no-filter ValueError, zero-match, and rollback-on-error. File-backed deletion semantics are documented rather than unit-tested (testing real serialized-file deletion is brittle); happy to add an explicit file-cleanup implementation + test if preferred.

Documentation — added a "Removing Seeds from the Database" section to doc/code/memory/8_seed_database.py (and the synced .ipynb) demonstrating the preview-then-remove workflow, whole-group removal, the exact-by-default matching (and exact=False opt-in for substring), the ungrouped-seed skip, and the file-backed caveat.

Copilot AI review requested due to automatic review settings July 21, 2026 22:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds first-class support for deleting seed prompts from PyRIT memory by introducing remove_seeds_from_memory_async, mirroring the existing get_seeds filter surface so users no longer need backend-specific SQL for seed cleanup.

Changes:

  • Extracts seed filter construction into a shared private helper (_build_seed_filter_conditions) used by both seed querying and deletion.
  • Adds remove_seeds_from_memory_async to delete matching SeedEntry rows transactionally and return the number removed (with a no-filter safety guard).
  • Adds unit tests and documentation demonstrating a “preview via get_seeds, then delete via remove_seeds_from_memory_async” workflow.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 13 comments.

File Description
pyrit/memory/memory_interface.py Adds shared seed-filter builder and new async seed removal API implemented via SQLAlchemy.
tests/unit/memory/memory_interface/test_interface_remove_seeds.py Adds unit coverage for common filter cases and safety behavior.
doc/code/memory/8_seed_database.py Documents seed removal usage and recommended preview-then-delete workflow.
doc/code/memory/8_seed_database.ipynb Syncs the same documentation updates into the notebook format.

Comment thread tests/unit/memory/memory_interface/test_interface_remove_seeds.py Outdated
Comment thread tests/unit/memory/memory_interface/test_interface_remove_seeds.py Outdated
Comment thread tests/unit/memory/memory_interface/test_interface_remove_seeds.py Outdated
Comment thread tests/unit/memory/memory_interface/test_interface_remove_seeds.py Outdated
Comment thread tests/unit/memory/memory_interface/test_interface_remove_seeds.py Outdated
Comment thread pyrit/memory/memory_interface.py Outdated
Comment thread pyrit/memory/memory_interface.py Outdated
Comment thread pyrit/memory/memory_interface.py Outdated
Comment thread pyrit/memory/memory_interface.py Outdated
Comment thread pyrit/memory/memory_interface.py
Copilot AI review requested due to automatic review settings July 21, 2026 22:38
@blahdeblahde
blahdeblahde force-pushed the blahdeblahde-remove-seeds-from-memory-api branch from 2d45b57 to 0e3ae02 Compare July 21, 2026 22:38
@blahdeblahde

Copy link
Copy Markdown
Contributor Author

Thanks for the review! Addressed all points in the latest push:

  • Removed the @pytest.mark.asyncio decorators from the new tests (asyncio_mode = auto is set globally).
  • Fixed the value_sha256 docstring type to Sequence[str] | None in the shared helper, get_seeds, and remove_seeds_from_memory_async.
  • Corrected the get_seeds return docstring to Sequence[Seed] (not just SeedPrompt).
  • Changed the delete to synchronize_session=False to avoid the extra SELECT on a short-lived session.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment thread pyrit/memory/memory_interface.py
@blahdeblahde

Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree company="Microsoft"

Comment thread pyrit/memory/memory_interface.py Outdated
Comment thread pyrit/memory/memory_interface.py Outdated
Comment thread pyrit/memory/memory_interface.py
Comment thread pyrit/memory/memory_interface.py Outdated
with closing(self.get_session()) as session:
try:
query = session.query(SeedEntry).filter(and_(*conditions))
count = query.delete(synchronize_session=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are we doing to file-backed image, audio, and video seeds? This removes the database row but leaves the serialized file behind. We should either clean up owned artifacts or clearly document that this only removes database records.

Comment thread pyrit/memory/memory_interface.py
Comment thread doc/code/memory/8_seed_database.py Outdated
@blahdeblahde
blahdeblahde force-pushed the blahdeblahde-remove-seeds-from-memory-api branch 3 times, most recently from 622947d to 4f77352 Compare July 29, 2026 02:08
@blahdeblahde

Copy link
Copy Markdown
Contributor Author

@hannahwestra25 Thanks again for the review — I've pushed updates addressing all your comments:

  • DuckDB wording / helper return type — fixed.
  • Async vs sync — converted remove_seeds_from_memory to synchronous (it only does DB work, no awaited I/O, so async would just stall the event loop); rationale in the thread above.
  • Whole-group removal (Add poetry attributes #5) — added remove_seed_groups_from_memory, which expands any filter match to its full prompt_group_id so partial groups are never left behind. Unit tests included.
  • value substring deletion danger (Upgrade transformers to >=4.36.0 to address dependabot alert #4) — documented the over-match risk with preview-first guidance, and proposed a safe exact-match deletion mode (value_exact=) as a separate follow-up ticket, since value matching lives in the shared filter helper used by get_seeds/get_seed_groups.
  • Deletion footguns (Fixed broken link to notebook #7) — documented in the "Removing entire groups" section of the seed-database notebook.

The PR description has been updated with the new method and a "Follow-up work" section. Let me know if you'd like the exact-match mode folded into this PR instead of a follow-up.

Comment thread tests/unit/memory/memory_interface/test_interface_remove_seeds.py
Comment thread pyrit/memory/memory_interface.py Outdated
Comment thread pyrit/memory/memory_interface.py
@blahdeblahde
blahdeblahde force-pushed the blahdeblahde-remove-seeds-from-memory-api branch 2 times, most recently from 50b8a51 to 6652dcf Compare July 30, 2026 19:01
Comment thread pyrit/memory/memory_interface.py Outdated
Comment thread pyrit/memory/memory_interface.py Outdated
Comment thread doc/code/memory/8_seed_database.py Outdated
Comment thread pyrit/memory/memory_interface.py Outdated
Comment thread pyrit/memory/memory_interface.py Outdated
@blahdeblahde
blahdeblahde force-pushed the blahdeblahde-remove-seeds-from-memory-api branch 3 times, most recently from 276576a to 1bfc7cc Compare August 1, 2026 02:13
Adds two memory APIs to remove seed prompts using the same filter
parameters as get_seeds (value, exact, value_sha256, dataset_name,
dataset_name_pattern, data_types, harm_categories, added_by, authors,
groups, source, seed_type, parameters, metadata, prompt_group_ids):

- remove_seeds_from_memory removes the individual seeds that match.
- remove_seed_groups_from_memory expands any match to its whole
  prompt_group_id so partial groups are never left behind.

Implementation:
- Extract filter-building logic from get_seeds into a shared
  _build_seed_filter_conditions helper so query and removal cannot drift.
- value matching is controlled by an exact bool flag (replacing the
  earlier value_exact string, per review). The remove methods default to
  exact=True (full-string equality) so a short or common value cannot
  over-delete; get_seeds keeps substring matching. Pass exact=False to
  opt into substring deletion. The intentional difference is documented.
- Require at least one filter (raises ValueError if none provided).
- Single transaction with rollback on SQLAlchemyError.
- Group removal selects matching prompt_group_ids with a server-side
  subquery (not a materialized IN(...) list) so a broad filter cannot
  exceed the backend bound-parameter limit; NULL group ids are excluded,
  so ungrouped seeds are skipped.
- Methods are synchronous, mirroring get_seeds: they do only DB work and
  never await I/O.

Only database records are removed; file-backed seeds (image_path,
audio_path, video_path) leave the serialized file on disk (documented).

Unit tests cover every individual filter, exact-by-default vs opt-in
substring matching, multi-filter narrowing, no-filter ValueError,
zero-match, rollback-on-error, and for groups: whole-group removal,
groups spanning datasets, non-matching groups preserved, and the
ungrouped (NULL group id) skip behavior.

Docs: add a "Removing Seeds from the Database" section to the seed
database notebook (8_seed_database.py/.ipynb) demonstrating the
preview-then-remove workflow, whole-group removal, exact-by-default
matching, the ungrouped-skip, and the file-backed caveat.

Resolves AB#5688

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f9e3cc10-5c77-4384-92d9-d27dad9baea2
@blahdeblahde
blahdeblahde force-pushed the blahdeblahde-remove-seeds-from-memory-api branch from 1bfc7cc to 7dbf60f Compare August 1, 2026 02:35
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.

3 participants