Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0cdc24a
feat: Add bounded Scenario result hierarchy projections (#3145)
Nimit3418 Oct 13, 2026
a7367c0
fix: address all review feedback on bounded scenario hierarchy (#3167)
Nimit3418 Oct 14, 2026
5fde2ca
fix: resolve import NameError, O(N^2) loop, retry consistency, and co…
Nimit3418 Oct 14, 2026
689eef9
fix: resolve remaining edge cases on executed_turns, piece limits, an…
Nimit3418 Oct 14, 2026
f33a5b9
fix: harden related conversation handling against malformed metadata …
Nimit3418 Oct 15, 2026
5d04b4f
fix: use real conversation statistics and safely handle unavailable m…
Nimit3418 Oct 15, 2026
0e7abda
fix: apply SQL piece budgeting and reconcile diagnostic retries with …
Nimit3418 Oct 15, 2026
e4d0ca0
fix: bound cold delta hydration, truncate unresolved child IDs, and b…
Nimit3418 Oct 16, 2026
6af0e8d
fix: decouple run-wide aggregates from bounded hierarchy and enforce …
Nimit3418 Oct 16, 2026
7c489c5
fix: resolve run-scoped roots for truncated deltas and chunk conversa…
Nimit3418 Oct 16, 2026
04d713e
fix: enforce exact cold-warm parity, targeted parent resolution, and …
Nimit3418 Oct 17, 2026
fbf3301
fix: bound parent queries, handle cyclic discovery, and preserve plan…
Nimit3418 Oct 17, 2026
544ccbf
fix: exclude known rows from parent queries, handle multi-layer roots…
Nimit3418 Oct 18, 2026
a1b1108
fix: page parent queries, guard unverified roots, and preserve legacy…
Nimit3418 Oct 18, 2026
dfb3ce3
fix: lightweight recount projection, guard boundary roots, and bound …
Nimit3418 Oct 19, 2026
1d14bd2
fix: keyset-page parent discovery across exclusion fallbacks and pres…
Nimit3418 Oct 20, 2026
294f3de
fix: account for unprocessed page rows in parent query has_more contr…
Nimit3418 Oct 20, 2026
606d4a2
merge: sync upstream/main and resolve scenario progress and memory co…
Nimit3418 Oct 20, 2026
625b470
fix: preserve keyset delta polling, completed units aggregation, and …
Nimit3418 Oct 20, 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
4 changes: 2 additions & 2 deletions pyrit/analytics/scenario_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ class ScenarioAttempt:
attempt_id: str
total_retries: int
result_role: AttackResultRole | None
attempt_index: int | None
child_attack_result_ids: tuple[str, ...]
attempt_index: int | None = None
child_attack_result_ids: tuple[str, ...] = ()

@property
def attack_result_id(self) -> str:
Expand Down
72 changes: 71 additions & 1 deletion pyrit/backend/routes/scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@
get_scenario_run_service,
)
from pyrit.backend.services.scenario_service import get_scenario_service
from pyrit.models import ScenarioQueueSnapshot, ScenarioResult, ScenarioRunProgress, ScenarioRunState
from pyrit.models import (
ScenarioHierarchyNode,
ScenarioHierarchyProjection,
ScenarioQueueSnapshot,
ScenarioResult,
ScenarioRunProgress,
ScenarioRunState,
)
from pyrit.models.catalog import (
RegisteredScenario,
RunScenarioRequest,
Expand Down Expand Up @@ -375,6 +382,69 @@ async def get_scenario_run_progress( # pyrit-async-suffix-exempt
return progress


@router.get(
"/runs/{scenario_result_id}/hierarchy",
response_model=ScenarioHierarchyProjection,
responses={
404: {"model": ProblemDetail, "description": "Run not found"},
},
)
async def get_scenario_run_hierarchy( # pyrit-async-suffix-exempt
*,
scenario_result_id: IdentifierStr,
max_nodes: int = Query(1000, ge=1, le=5000, description="Maximum nodes to assemble in the hierarchy tree"),
) -> ScenarioHierarchyProjection:
"""
Get the bounded result hierarchy projection for a scenario run.

Returns:
ScenarioHierarchyProjection: Bounded hierarchy projection connecting orchestration parents,
target-facing children, and generic read-only conversations.
"""
service = get_scenario_run_service()
hierarchy = await service.get_run_hierarchy_async(
scenario_result_id=scenario_result_id,
max_nodes=max_nodes,
)
if hierarchy is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Scenario run '{scenario_result_id}' not found",
)
return hierarchy


@router.get(
"/runs/{scenario_result_id}/hierarchy/nodes/{attack_result_id}",
response_model=ScenarioHierarchyNode,
responses={
404: {"model": ProblemDetail, "description": "Node or run not found"},
},
)
async def get_scenario_run_hierarchy_node( # pyrit-async-suffix-exempt
*,
scenario_result_id: IdentifierStr,
attack_result_id: IdentifierStr,
) -> ScenarioHierarchyNode:
"""
Get single-node hierarchy details and generic conversation projections without full-run hydration.

Returns:
ScenarioHierarchyNode: The target node with resolved immediate children and conversations.
"""
service = get_scenario_run_service()
node = await service.get_run_hierarchy_node_async(
scenario_result_id=scenario_result_id,
attack_result_id=attack_result_id,
)
if node is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Hierarchy node '{attack_result_id}' not found in scenario run '{scenario_result_id}'",
)
return node


@router.post(
"/runs/{scenario_result_id}/cancel",
response_model=ScenarioRunSummary,
Expand Down
Loading