Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions pyrit/backend/services/runtime_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@ async def shutdown_async(self) -> None:

async def _shutdown_runtime_async(self) -> None:
"""Finish retained requests without cancelling their offloaded writes."""
service = peek_scenario_run_service()
if service:
service.stop_admission()
pending = self.operations | self.management_operations
if self.apply_task is not None:
pending.add(self.apply_task)
Expand Down
289 changes: 118 additions & 171 deletions pyrit/backend/services/scenario_run_service.py

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion pyrit/registry/components/scenario_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Literal

from pyrit.common.async_compatibility import run_legacy_sync_async
from pyrit.models import ScenarioRunSizeEstimate, ScenarioTechniqueSummary, class_name_to_snake_case
from pyrit.models.identifiers.scenario_identifier import ScenarioIdentifier
from pyrit.registry.registry import ParamBagRegistry
Expand Down Expand Up @@ -271,6 +272,8 @@ async def create_and_initialize_async(

Prefer this over manually chaining ``create_instance`` +
``set_params_from_args`` + ``initialize_async``.
Synchronous construction and configuration run off-loop; async
initialization runs on the caller's event loop.

Args:
name (str): The registry name of the scenario (e.g. ``"foundry.red_team_agent"``).
Expand All @@ -291,7 +294,9 @@ async def create_and_initialize_async(
constructor_kwargs["scenario_result_id"] = scenario_result_id

merged_args = {**(scenario_params or {}), **initialize_kwargs}
scenario = self._create_and_configure(name, params=merged_args, constructor_kwargs=constructor_kwargs)
scenario = await run_legacy_sync_async(
self._create_and_configure, name, params=merged_args, constructor_kwargs=constructor_kwargs
)
scenario.set_scenario_registry_name(scenario_registry_name=name)
if initial_metadata:
scenario.set_initial_metadata(metadata=initial_metadata)
Expand Down
3 changes: 3 additions & 0 deletions pyrit/scenario/core/matrix_atomic_attack_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ def build_matrix_atomic_attacks(
Scenarios needing extra axes (adversarial targets, caching, converter stacks) call
``MatrixAtomicAttackBuilder`` directly instead.

This synchronous builder can read attack templates from disk. Async callers
must offload construction, not their dataset reads or persistence.

Args:
context (ScenarioContext): The resolved runtime inputs for this run. Supplies the
objective target, memory labels, per-dataset seed groups, selected techniques, and
Expand Down
4 changes: 3 additions & 1 deletion pyrit/scenario/scenarios/airt/cyber.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing import TYPE_CHECKING

from pyrit.common import apply_defaults
from pyrit.common.async_compatibility import run_legacy_sync_async
from pyrit.common.path import SCORER_SEED_PROMPT_PATH
from pyrit.scenario.core.dataset_configuration import DatasetAttackConfiguration
from pyrit.scenario.core.matrix_atomic_attack_builder import build_matrix_atomic_attacks
Expand Down Expand Up @@ -123,7 +124,8 @@ async def _build_atomic_attacks_async(self, *, context: ScenarioContext) -> list
Returns:
list[AtomicAttack]: The generated atomic attacks.
"""
return build_matrix_atomic_attacks(
return await run_legacy_sync_async(
build_matrix_atomic_attacks,
context=context,
objective_scorer=self._objective_scorer,
technique_converters=self._technique_converters,
Expand Down
23 changes: 19 additions & 4 deletions pyrit/scenario/scenarios/airt/jailbreak.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from typing import TYPE_CHECKING, Any, ClassVar

from pyrit.common import apply_defaults
from pyrit.common.async_compatibility import run_legacy_sync_async
from pyrit.converter import TextJailbreakConverter
from pyrit.datasets import TextJailBreak
from pyrit.executor.attack.single_turn.prompt_sending import PromptSendingAttack
Expand Down Expand Up @@ -304,12 +305,14 @@ async def _resolve_templates_async(self) -> list[str]:
" or `jailbreak_names` (specific selection)."
)
if jailbreak_names:
available = set(TextJailBreak.get_jailbreak_templates())
available = set(await run_legacy_sync_async(TextJailBreak.get_jailbreak_templates))
diff = set(jailbreak_names) - available
if diff:
raise ValueError(f"Error: could not find templates `{diff}`!")
return list(jailbreak_names)
return TextJailBreak.get_jailbreak_templates(num_templates=num_jailbreaks or _DEFAULT_NUM_JAILBREAKS)
return await run_legacy_sync_async(
TextJailBreak.get_jailbreak_templates, num_templates=num_jailbreaks or _DEFAULT_NUM_JAILBREAKS
)

def _build_initial_scenario_metadata(self) -> dict[str, Any]:
"""
Expand Down Expand Up @@ -461,14 +464,26 @@ async def _build_atomic_attacks_async(self, *, context: ScenarioContext) -> list
)

self._resolved_jailbreaks = await self._resolve_templates_async()
return await run_legacy_sync_async(self._build_atomic_attacks, context=context)

def _build_atomic_attacks(self, *, context: ScenarioContext) -> list[AtomicAttack]:
"""
Build the synchronous template and delivery matrix off-loop.

Returns:
list[AtomicAttack]: The attacks for the selected templates and delivery methods.

Raises:
ValueError: If only system-prompt delivery is selected for an incompatible target.
"""
num_attempts = self.params["num_jailbreak_attempts"]

technique_factories = resolve_technique_factories(context=context, extra_factories=_extra_default_factories())

prompt_sending_factory = technique_factories.get(_PROMPT_SENDING)
system_selected = _JAILBREAK_SYSTEM_PROMPT in technique_factories

build_system_delivery = system_selected and self._target_supports_system_delivery(self._objective_target)
build_system_delivery = system_selected and self._target_supports_system_delivery(context.objective_target)
if system_selected and not build_system_delivery:
if prompt_sending_factory is None:
raise ValueError(
Expand All @@ -481,7 +496,7 @@ async def _build_atomic_attacks_async(self, *, context: ScenarioContext) -> list
)

builder = MatrixAtomicAttackBuilder(
objective_target=self._objective_target,
objective_target=context.objective_target,
objective_scorer=self._objective_scorer,
memory_labels=context.memory_labels,
)
Expand Down
4 changes: 3 additions & 1 deletion pyrit/scenario/scenarios/airt/leakage.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing import TYPE_CHECKING

from pyrit.common import apply_defaults
from pyrit.common.async_compatibility import run_legacy_sync_async
from pyrit.common.path import SCORER_SEED_PROMPT_PATH
from pyrit.registry.components.attack_technique_registry import AttackTechniqueRegistry
from pyrit.scenario.core.dataset_configuration import DatasetAttackConfiguration
Expand Down Expand Up @@ -139,7 +140,8 @@ async def _build_atomic_attacks_async(self, *, context: ScenarioContext) -> list
Returns:
list[AtomicAttack]: The generated atomic attacks.
"""
return build_matrix_atomic_attacks(
return await run_legacy_sync_async(
build_matrix_atomic_attacks,
context=context,
objective_scorer=self._objective_scorer,
technique_converters=self._technique_converters,
Expand Down
10 changes: 10 additions & 0 deletions pyrit/scenario/scenarios/airt/multilingual.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from typing import TYPE_CHECKING, Any, ClassVar, Literal

from pyrit.common import apply_defaults
from pyrit.common.async_compatibility import run_legacy_sync_async
from pyrit.common.path import DATASETS_PATH
from pyrit.converter import RandomTranslationConverter, TranslationConverter
from pyrit.executor.attack import PromptSendingAttack
Expand Down Expand Up @@ -306,6 +307,15 @@ async def _build_atomic_attacks_async(self, *, context: ScenarioContext) -> list
)

self._resolved_languages = await self._resolve_languages_async()
return await run_legacy_sync_async(self._build_atomic_attacks, context=context)

def _build_atomic_attacks(self, *, context: ScenarioContext) -> list[AtomicAttack]:
"""
Build the synchronous attack and converter matrix off-loop.

Returns:
list[AtomicAttack]: The attacks for the selected languages and translation methods.
"""
adversarial_chat = self._adversarial_chat or get_default_adversarial_target()
strategies = set(self.params.get("translation_strategies") or [_TRANSLATION, _RANDOM_TRANSLATION])
technique_factories = resolve_technique_factories(
Expand Down
11 changes: 8 additions & 3 deletions pyrit/scenario/scenarios/airt/psychosocial.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from typing import TYPE_CHECKING, ClassVar, cast

from pyrit.common import apply_defaults
from pyrit.common.async_compatibility import run_legacy_sync_async
from pyrit.common.path import DATASETS_PATH
from pyrit.converter import (
CharSwapConverter,
Expand Down Expand Up @@ -641,19 +642,23 @@ async def _build_atomic_attacks_async(self, *, context: ScenarioContext) -> list

for technique in techniques:
if technique is PsychosocialTechnique.Crescendo:
attack_technique = self._build_crescendo_technique(
attack_technique = await asyncio.to_thread(
self._build_crescendo_technique,
harm=harm,
objective_target=context.objective_target,
adversarial_chat=adversarial_chat,
scoring_config=scoring_config,
max_turns=max_turns,
)
else:
converter = _converter_for_technique(technique, adversarial_chat=adversarial_chat)
converter = await run_legacy_sync_async(
_converter_for_technique, technique, adversarial_chat=adversarial_chat
)
extra_converters = (
ConverterConfiguration.from_converters(converters=[converter]) if converter else None
)
attack_technique = base_factory.create(
attack_technique = await run_legacy_sync_async(
base_factory.create,
objective_target=context.objective_target,
attack_scoring_config=scoring_config,
adversarial_chat=adversarial_chat,
Expand Down
4 changes: 3 additions & 1 deletion pyrit/scenario/scenarios/airt/rapid_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from typing import TYPE_CHECKING

from pyrit.common import apply_defaults
from pyrit.common.async_compatibility import run_legacy_sync_async
from pyrit.scenario.core.dataset_configuration import CompoundDatasetAttackConfiguration
from pyrit.scenario.core.matrix_atomic_attack_builder import build_matrix_atomic_attacks
from pyrit.scenario.core.scenario import Scenario
Expand Down Expand Up @@ -124,7 +125,8 @@ async def _build_atomic_attacks_async(self, *, context: ScenarioContext) -> list
Returns:
list[AtomicAttack]: The generated atomic attacks.
"""
return build_matrix_atomic_attacks(
return await run_legacy_sync_async(
build_matrix_atomic_attacks,
context=context,
objective_scorer=self._objective_scorer,
display_group_fn=lambda combo: combo.dataset_name,
Expand Down
4 changes: 3 additions & 1 deletion pyrit/scenario/scenarios/benchmark/adversarial.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from pyrit.analytics import get_cached_results_for_technique_async
from pyrit.common import apply_defaults
from pyrit.common.async_compatibility import run_legacy_sync_async
from pyrit.common.path import EXECUTOR_SEED_PROMPT_PATH, SCORER_SEED_PROMPT_PATH
from pyrit.common.utils import to_sha256
from pyrit.models import (
Expand Down Expand Up @@ -522,7 +523,8 @@ async def _build_atomic_attacks_async(self, *, context: ScenarioContext) -> list
# ``--adversarial-targets`` so per-model ASR rolls up naturally — not any internal
# field on the PromptTarget instance (e.g. ``_model_name``). The builder's default
# ``{technique}__{target}_{dataset}`` naming preserves the VERSION=2 cache key shape.
atomic_attacks = builder.build(
atomic_attacks = await run_legacy_sync_async(
builder.build,
technique_factories=technique_factories,
dataset_groups=context.seed_groups_by_dataset,
adversarial_targets=resolved_targets,
Expand Down
4 changes: 3 additions & 1 deletion pyrit/scenario/scenarios/garak/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing import TYPE_CHECKING, ClassVar

from pyrit.common import apply_defaults
from pyrit.common.async_compatibility import run_legacy_sync_async
from pyrit.converter import LeetspeakConverter, PolicyPuppetryConverter, PolicyPuppetryTemplate
from pyrit.executor.attack import AttackConverterConfig, PromptSendingAttack
from pyrit.prompt_normalizer import ConverterConfiguration
Expand Down Expand Up @@ -168,7 +169,8 @@ async def _build_atomic_attacks_async(self, *, context: ScenarioContext) -> list
objective_scorer=self._objective_scorer,
memory_labels=context.memory_labels,
)
return builder.build(
return await run_legacy_sync_async(
builder.build,
technique_factories=technique_factories,
dataset_groups=context.seed_groups_by_dataset,
include_baseline=context.include_baseline,
Expand Down
3 changes: 2 additions & 1 deletion pyrit/setup/initializers/load_default_datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import logging
import textwrap

from pyrit.common.async_compatibility import run_legacy_sync_async
from pyrit.datasets import SeedDatasetFilter, SeedDatasetProvider
from pyrit.memory import CentralMemory
from pyrit.models.parameter import Parameter
Expand Down Expand Up @@ -80,7 +81,7 @@ async def initialize_async(self) -> None:
unique_datasets = list(dict.fromkeys(matched))
logger.info(f"Loading {len(unique_datasets)} dataset(s) matching tags: {sorted(tags)}")
else:
unique_datasets = self._scenario_default_dataset_names()
unique_datasets = await run_legacy_sync_async(self._scenario_default_dataset_names)
logger.info(f"Loading {len(unique_datasets)} unique datasets required by all scenarios")

if not unique_datasets:
Expand Down
5 changes: 3 additions & 2 deletions pyrit/setup/initializers/preload_scenario_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import logging

from pyrit.common.async_compatibility import run_legacy_sync_async
from pyrit.registry import ScenarioRegistry
from pyrit.setup.pyrit_initializer import PyRITInitializer

Expand All @@ -24,6 +25,6 @@ class PreloadScenarioMetadata(PyRITInitializer):

async def initialize_async(self) -> None:
"""Warm the scenario metadata cache."""
registry = ScenarioRegistry.get_registry_singleton()
metadata = registry.get_all_registered_class_metadata()
registry = await run_legacy_sync_async(ScenarioRegistry.get_registry_singleton)
metadata = await run_legacy_sync_async(registry.get_all_registered_class_metadata)
logger.info("Preloaded metadata for %d scenarios", len(metadata))
10 changes: 10 additions & 0 deletions pyrit/setup/initializers/scorers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

from azure.ai.contentsafety.models import TextCategory

from pyrit.common.async_compatibility import run_legacy_sync_async
from pyrit.models import SeedPrompt
from pyrit.models.parameter import Parameter
from pyrit.registry import ScorerRegistry, TargetRegistry
Expand Down Expand Up @@ -187,6 +188,15 @@ async def initialize_async(self) -> None:
Raises:
RuntimeError: If the TargetRegistry is empty or hasn't been initialized.
"""
await run_legacy_sync_async(self._register_scorers)

def _register_scorers(self) -> None:
"""
Construct template-backed scorers without blocking the caller's event loop.

Raises:
RuntimeError: If the target registry is empty.
"""
target_registry = TargetRegistry.get_registry_singleton()

if len(target_registry.instances) == 0:
Expand Down
5 changes: 5 additions & 0 deletions pyrit/setup/initializers/targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from typing import Any

from pyrit.auth import get_azure_openai_auth, get_azure_token_provider
from pyrit.common.async_compatibility import run_legacy_sync_async
from pyrit.models.identifiers import TARGET_EVAL_PARAM_FALLBACKS, TARGET_EVAL_PARAMS
from pyrit.models.parameter import Parameter
from pyrit.prompt_target import (
Expand Down Expand Up @@ -625,6 +626,10 @@ async def initialize_async(self) -> None:
and target class are automatically grouped into ``RoundRobinTarget``
instances for rate-limit distribution and fault tolerance.
"""
await run_legacy_sync_async(self._register_targets)

def _register_targets(self) -> None:
"""Construct and register targets without blocking the caller's event loop."""
tags = self.params.get("tags", ["default"])
if TargetInitializerTags.ALL in tags:
tags = [tag for tag in TargetInitializerTags if tag != TargetInitializerTags.ALL]
Expand Down
Loading
Loading