Skip to content

Commit 83caadd

Browse files
rlundeen2Copilot
andauthored
MAINT: Refactoring Identifiers to be Pydantic classes (#1881)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7df5493 commit 83caadd

14 files changed

Lines changed: 558 additions & 290 deletions

File tree

pyrit/backend/services/attack_service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -756,7 +756,7 @@ async def _update_attack_after_message_async(
756756
children=new_children,
757757
)
758758
if ar.atomic_attack_identifier:
759-
atomic = ComponentIdentifier.from_dict(ar.atomic_attack_identifier.to_dict())
759+
atomic = ComponentIdentifier.model_validate(ar.atomic_attack_identifier.model_dump())
760760
atomic_children = dict(atomic.children)
761761
# Navigate into attack_technique child to update the nested attack child.
762762
technique = atomic_children.get("attack_technique")
@@ -778,7 +778,7 @@ async def _update_attack_after_message_async(
778778
params=dict(atomic.params),
779779
children=atomic_children,
780780
)
781-
update_fields["atomic_attack_identifier"] = new_atomic.to_dict()
781+
update_fields["atomic_attack_identifier"] = new_atomic.model_dump()
782782

783783
self._memory.update_attack_result_by_id(
784784
attack_result_id=attack_result_id,

pyrit/memory/memory_models.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
# Default pyrit_version for database records created before version tracking was added
5858
LEGACY_PYRIT_VERSION = "<0.10.0"
5959

60-
# Maximum length for string values in ComponentIdentifier.to_dict() when storing to the database.
60+
# Maximum length for string values in ComponentIdentifier.model_dump() when storing to the database.
6161
# Longer values are truncated with a "..." suffix.
6262
MAX_IDENTIFIER_VALUE_LENGTH: int = 80
6363

@@ -231,16 +231,17 @@ def __init__(self, *, entry: MessagePiece) -> None:
231231
self.prompt_metadata = entry.prompt_metadata
232232
self.targeted_harm_categories = entry.targeted_harm_categories
233233
self.converter_identifiers = [
234-
conv.to_dict(max_value_length=MAX_IDENTIFIER_VALUE_LENGTH) for conv in entry.converter_identifiers
234+
conv.model_dump(context={"max_value_length": MAX_IDENTIFIER_VALUE_LENGTH})
235+
for conv in entry.converter_identifiers
235236
]
236237
# Normalize prompt_target_identifier and convert to dict for JSON serialization
237238
self.prompt_target_identifier = (
238-
entry.prompt_target_identifier.to_dict(max_value_length=MAX_IDENTIFIER_VALUE_LENGTH)
239+
entry.prompt_target_identifier.model_dump(context={"max_value_length": MAX_IDENTIFIER_VALUE_LENGTH})
239240
if entry.prompt_target_identifier
240241
else {}
241242
)
242243
self.attack_identifier = (
243-
entry.attack_identifier.to_dict(max_value_length=MAX_IDENTIFIER_VALUE_LENGTH)
244+
entry.attack_identifier.model_dump(context={"max_value_length": MAX_IDENTIFIER_VALUE_LENGTH})
244245
if entry.attack_identifier
245246
else {}
246247
)
@@ -270,21 +271,21 @@ def get_message_piece(self) -> MessagePiece:
270271
stored_version = self.pyrit_version or LEGACY_PYRIT_VERSION
271272
if self.converter_identifiers:
272273
converter_ids = [
273-
ComponentIdentifier.from_dict({**c, "pyrit_version": stored_version})
274+
ComponentIdentifier.model_validate({**c, "pyrit_version": stored_version})
274275
for c in self.converter_identifiers
275276
]
276277

277278
# Reconstruct ComponentIdentifier with the stored pyrit_version
278279
target_id: ComponentIdentifier | None = None
279280
if self.prompt_target_identifier:
280-
target_id = ComponentIdentifier.from_dict(
281+
target_id = ComponentIdentifier.model_validate(
281282
{**self.prompt_target_identifier, "pyrit_version": stored_version}
282283
)
283284

284285
# Reconstruct ComponentIdentifier with the stored pyrit_version
285286
attack_id: ComponentIdentifier | None = None
286287
if self.attack_identifier:
287-
attack_id = ComponentIdentifier.from_dict({**self.attack_identifier, "pyrit_version": stored_version})
288+
attack_id = ComponentIdentifier.model_validate({**self.attack_identifier, "pyrit_version": stored_version})
288289

289290
message_piece = MessagePiece(
290291
role=self.role,
@@ -405,8 +406,8 @@ def __init__(self, *, entry: Score) -> None:
405406
normalized_scorer = normalized_scorer.with_eval_hash(
406407
ScorerEvaluationIdentifier(normalized_scorer).eval_hash
407408
)
408-
self.scorer_class_identifier = normalized_scorer.to_dict(
409-
max_value_length=MAX_IDENTIFIER_VALUE_LENGTH,
409+
self.scorer_class_identifier = normalized_scorer.model_dump(
410+
context={"max_value_length": MAX_IDENTIFIER_VALUE_LENGTH},
410411
)
411412
self.prompt_request_response_id = entry.message_piece_id if entry.message_piece_id else None
412413
self.timestamp = entry.timestamp
@@ -427,7 +428,7 @@ def get_score(self) -> Score:
427428
scorer_identifier = None
428429
stored_version = self.pyrit_version or LEGACY_PYRIT_VERSION
429430
if self.scorer_class_identifier:
430-
scorer_identifier = ComponentIdentifier.from_dict(
431+
scorer_identifier = ComponentIdentifier.model_validate(
431432
{**self.scorer_class_identifier, "pyrit_version": stored_version}
432433
)
433434
return Score(
@@ -779,16 +780,18 @@ def __init__(self, *, entry: AttackResult) -> None:
779780
# Will be removed in 0.15.0.
780781
_attack_strategy_id = entry.get_attack_strategy_identifier()
781782
self.attack_identifier = (
782-
_attack_strategy_id.to_dict(max_value_length=MAX_IDENTIFIER_VALUE_LENGTH) if _attack_strategy_id else {}
783+
_attack_strategy_id.model_dump(context={"max_value_length": MAX_IDENTIFIER_VALUE_LENGTH})
784+
if _attack_strategy_id
785+
else {}
783786
)
784787
# Ensure eval_hash is set before truncation so it survives the DB round-trip
785788
if entry.atomic_attack_identifier and entry.atomic_attack_identifier.eval_hash is None:
786789
entry.atomic_attack_identifier = entry.atomic_attack_identifier.with_eval_hash(
787790
AtomicAttackEvaluationIdentifier(entry.atomic_attack_identifier).eval_hash
788791
)
789792
self.atomic_attack_identifier = (
790-
entry.atomic_attack_identifier.to_dict(
791-
max_value_length=MAX_IDENTIFIER_VALUE_LENGTH,
793+
entry.atomic_attack_identifier.model_dump(
794+
context={"max_value_length": MAX_IDENTIFIER_VALUE_LENGTH},
792795
)
793796
if entry.atomic_attack_identifier
794797
else None
@@ -911,13 +914,13 @@ def get_attack_result(self) -> AttackResult:
911914
# Reconstruct atomic_attack_identifier, with backward compatibility for
912915
# legacy rows that only have the attack_identifier column.
913916
atomic_id = (
914-
ComponentIdentifier.from_dict(self.atomic_attack_identifier) if self.atomic_attack_identifier else None
917+
ComponentIdentifier.model_validate(self.atomic_attack_identifier) if self.atomic_attack_identifier else None
915918
)
916919
if atomic_id is None and self.attack_identifier:
917920
from pyrit.models import build_atomic_attack_identifier
918921

919922
atomic_id = build_atomic_attack_identifier(
920-
attack_identifier=ComponentIdentifier.from_dict(self.attack_identifier),
923+
attack_identifier=ComponentIdentifier.model_validate(self.attack_identifier),
921924
)
922925

923926
# Deserialize retry events from JSON
@@ -1037,8 +1040,8 @@ def __init__(self, *, entry: ScenarioResult) -> None:
10371040
self.scenario_init_data = entry.scenario_identifier.init_data
10381041
# Convert ComponentIdentifier to dict for JSON storage
10391042
self.objective_target_identifier = (
1040-
entry.objective_target_identifier.to_dict(
1041-
max_value_length=MAX_IDENTIFIER_VALUE_LENGTH,
1043+
entry.objective_target_identifier.model_dump(
1044+
context={"max_value_length": MAX_IDENTIFIER_VALUE_LENGTH},
10421045
)
10431046
if entry.objective_target_identifier
10441047
else None
@@ -1050,8 +1053,8 @@ def __init__(self, *, entry: ScenarioResult) -> None:
10501053
)
10511054

10521055
self.objective_scorer_identifier = (
1053-
entry.objective_scorer_identifier.to_dict(
1054-
max_value_length=MAX_IDENTIFIER_VALUE_LENGTH,
1056+
entry.objective_scorer_identifier.model_dump(
1057+
context={"max_value_length": MAX_IDENTIFIER_VALUE_LENGTH},
10551058
)
10561059
if entry.objective_scorer_identifier
10571060
else None
@@ -1104,12 +1107,12 @@ def get_scenario_result(self) -> ScenarioResult:
11041107
# Convert dict back to ComponentIdentifier with the stored pyrit_version
11051108
scorer_identifier = None
11061109
if self.objective_scorer_identifier:
1107-
scorer_identifier = ComponentIdentifier.from_dict(
1110+
scorer_identifier = ComponentIdentifier.model_validate(
11081111
{**self.objective_scorer_identifier, "pyrit_version": stored_version}
11091112
)
11101113

11111114
# Convert dict back to ComponentIdentifier for reconstruction
1112-
target_identifier = ComponentIdentifier.from_dict(self.objective_target_identifier)
1115+
target_identifier = ComponentIdentifier.model_validate(self.objective_target_identifier)
11131116

11141117
# Deserialize display_group_map if stored
11151118
display_group_map: dict[str, str] | None = None

pyrit/models/attack_result.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ def to_dict(self) -> dict[str, Any]:
240240
"objective": self.objective,
241241
"attack_result_id": self.attack_result_id,
242242
"atomic_attack_identifier": (
243-
self.atomic_attack_identifier.to_dict() if self.atomic_attack_identifier else None
243+
self.atomic_attack_identifier.model_dump() if self.atomic_attack_identifier else None
244244
),
245245
"last_response": self.last_response.model_dump(mode="json") if self.last_response else None,
246246
"last_score": self.last_score.to_dict() if self.last_score else None,
@@ -278,7 +278,7 @@ def from_dict(cls, data: dict[str, Any]) -> AttackResult:
278278
objective=data["objective"],
279279
attack_result_id=data.get("attack_result_id", str(uuid.uuid4())),
280280
atomic_attack_identifier=(
281-
ComponentIdentifier.from_dict(data["atomic_attack_identifier"])
281+
ComponentIdentifier.model_validate(data["atomic_attack_identifier"])
282282
if data.get("atomic_attack_identifier")
283283
else None
284284
),

0 commit comments

Comments
 (0)