Skip to content

FIX: Paginate attack results in the database to keep the GUI responsive on large histories#2246

Open
varunj-msft wants to merge 1 commit into
microsoft:mainfrom
varunj-msft:varunj-msft/10107-Optimize-Copyrit-DB-Slowness
Open

FIX: Paginate attack results in the database to keep the GUI responsive on large histories#2246
varunj-msft wants to merge 1 commit into
microsoft:mainfrom
varunj-msft:varunj-msft/10107-Optimize-Copyrit-DB-Slowness

Conversation

@varunj-msft

Copy link
Copy Markdown
Contributor

Description

Listing attack results gets slow when the database holds a lot of records. The fix pushes the paging, dedup, and sorting into the database so only the page you're viewing is loaded:

  • get_attack_results() takes optional limit/offset (plus min_turns/max_turns), so it only loads the requested page instead of the whole history.
  • Per-conversation dedup and recency ordering now happen in SQL via ROW_NUMBER() instead of in Python.
  • Recency ordering is done per backend: SQLite via json_extract, Azure SQL via JSON_VALUE.
  • The service layer returns an opaque pagination cursor tied to the active filters, so a page token can't be replayed against a different filter set.

This is backend-only:

  • No frontend changes and no change to the API response shape, so existing callers keep working.
  • Paging is opt-in — if you don't pass limit/offset, behavior is unchanged.

Tests and Documentation

Added 13 unit tests (in tests/unit/memory/... and tests/unit/backend/test_attack_service.py) covering:

  • Pagination returning correctly ordered, disjoint, and complete pages.
  • Filter-aware dedup and the min_turns/max_turns filters.
  • Recency ordering, same-timestamp tie-breaking, and empty-metadata cases.
  • The pagination cursor and its filter fingerprint.

Verification:

  • Full unit suite passes (213 tests), along with ruff, formatting, and type checks.
  • Ran the paginated query against a live Azure SQL database to confirm the SQL-side dedup and ordering match the previous in-memory results.
  • No documentation or notebook changes, so JupyText wasn't needed.

…nsive on large histories

Listing attack results hydrated every AttackResult into memory and then
deduplicated and sorted them in Python, so response time grew with the total
number of stored attack results. Push that work into the database so only the
requested page is materialized:

- memory_interface.get_attack_results() gains keyword-only limit/offset (and
  min_turns/max_turns) and performs filter-aware deduplication with
  ROW_NUMBER() OVER (PARTITION BY conversation_id ORDER BY <recency>) in SQL,
  returning only the requested page instead of the full history.
- Per-backend recency ordering pushed to the engine: SQLite via json_extract,
  Azure SQL via JSON_VALUE (ORM functions, not raw text).
- The backend attack service now returns an opaque, filter-fingerprinted
  pagination cursor so a page token cannot be replayed against a different set
  of filters.

Backend-only: no frontend changes and no change to the public API response
shape. Covered by unit tests for memory pagination/dedup/ordering and the
service cursor behavior; the paginated query was also verified against live
Azure SQL.
Copilot AI review requested due to automatic review settings July 22, 2026 01:33

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 optimizes attack history listing by pushing per-conversation deduplication, recency ordering, and (optional) pagination into the database layer, so the GUI can remain responsive on large datasets while preserving the existing response shape.

Changes:

  • Added limit/offset and min_turns/max_turns support to MemoryInterface.get_attack_results(), with SQL-side dedup via ROW_NUMBER() and backend-specific JSON recency ordering.
  • Updated AttackService.list_attacks_async to use offset-based pagination with an opaque cursor that includes a filter fingerprint, and to over-fetch by one row to determine has_more.
  • Expanded unit test coverage for paging correctness, filter-aware dedup, turns bounds behavior, cursor behavior, and hydration under pagination.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/unit/memory/memory_interface/test_interface_attack_results.py Adds deterministic pagination/dedup/order tests for get_attack_results() (SQLite).
tests/unit/backend/test_attack_service.py Updates service-layer tests to validate forwarding of turn bounds and new opaque cursor/offset pagination behavior.
pyrit/memory/sqlite_memory.py Implements SQLite recency ordering using json_extract for metadata-based sorting.
pyrit/memory/memory_interface.py Adds pagination/turn-bounds plumbing, SQL paginated query path, and shared helper for turns filtering.
pyrit/memory/azure_sql_memory.py Implements Azure SQL recency ordering using JSON_VALUE (via SQLAlchemy func).
pyrit/backend/services/attack_service.py Switches to DB-paginated results and introduces filter-fingerprinted opaque cursor encoding/decoding.
pyrit/backend/routes/attacks.py Updates API parameter description to reflect opaque cursor semantics.

Comment on lines +878 to +887
def _norm_labels(
raw: dict[str, str | Sequence[str]] | None,
) -> dict[str, str | list[str]] | None:
if not raw:
return None
normalized: dict[str, str | list[str]] = {}
for key in sorted(raw):
value = raw[key]
normalized[key] = value if isinstance(value, str) else sorted(str(v) for v in value)
return normalized
Comment on lines 1075 to +1077
limit (int | None): Maximum number of rows to return. Defaults to None (no limit).
offset (int | None): Number of leading rows to skip, applied after ``limit`` for
pagination. Defaults to None (no offset).
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