Skip to content

Commit b3b018f

Browse files
rlundeen2Copilot
andauthored
MAINT: Making Score a Pydantic model (#1891)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ac0da01 commit b3b018f

9 files changed

Lines changed: 387 additions & 231 deletions

File tree

pyrit/memory/sqlite_memory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ def export_conversations(
559559
piece_data = piece.model_dump(mode="json")
560560
# Find associated scores
561561
piece_scores = [score for score in scores if score.message_piece_id == piece.id]
562-
piece_data["scores"] = [score.to_dict() for score in piece_scores]
562+
piece_data["scores"] = [score.model_dump(mode="json") for score in piece_scores]
563563
merged_data.append(piece_data)
564564

565565
if not merged_data:

pyrit/models/attack_result.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ def to_dict(self) -> dict[str, Any]:
243243
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,
246-
"last_score": self.last_score.to_dict() if self.last_score else None,
246+
"last_score": self.last_score.model_dump(mode="json") if self.last_score else None,
247247
"executed_turns": self.executed_turns,
248248
"execution_time_ms": self.execution_time_ms,
249249
"outcome": self.outcome.value,
@@ -283,7 +283,7 @@ def from_dict(cls, data: dict[str, Any]) -> AttackResult:
283283
else None
284284
),
285285
last_response=(MessagePiece.model_validate(data["last_response"]) if data.get("last_response") else None),
286-
last_score=Score.from_dict(data["last_score"]) if data.get("last_score") else None,
286+
last_score=Score.model_validate(data["last_score"]) if data.get("last_score") else None,
287287
executed_turns=data.get("executed_turns", 0),
288288
execution_time_ms=data.get("execution_time_ms", 0),
289289
outcome=AttackOutcome(data.get("outcome", "undetermined")),

pyrit/models/messages/message_piece.py

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,28 @@
55

66
import uuid
77
from datetime import datetime, timezone
8-
from typing import TYPE_CHECKING, Annotated, Any, Literal, Optional
8+
from typing import TYPE_CHECKING, Any, Literal, Optional
99
from uuid import uuid4
1010

1111
from pydantic import (
1212
AwareDatetime,
1313
BaseModel,
14-
BeforeValidator,
1514
ConfigDict,
1615
Field,
17-
PlainSerializer,
1816
model_validator,
1917
)
2018

2119
from pyrit.common.deprecation import print_deprecation_message
2220
from pyrit.models.data_type_serializer import data_serializer_factory
23-
from pyrit.models.identifiers.component_identifier import ComponentIdentifier
2421
from pyrit.models.literals import ( # noqa: TC001 (runtime-required by Pydantic field annotations)
2522
ChatMessageRole,
2623
PromptDataType,
2724
PromptResponseError,
2825
)
29-
from pyrit.models.score import Score
26+
from pyrit.models.score import ( # noqa: TC001 (runtime-required by Pydantic field annotations)
27+
ComponentIdentifierField,
28+
Score,
29+
)
3030

3131
if TYPE_CHECKING:
3232
from pyrit.models.messages.message import Message
@@ -48,21 +48,9 @@
4848
)
4949

5050

51-
# Annotated alias that round-trips identifier fields through the flat dict
52-
# storage shape. ``ComponentIdentifier`` is a Pydantic model with a custom
53-
# flat serializer; ``Score`` is still a plain class needing ``from_dict`` /
54-
# ``to_dict``. Drop the ``Score`` alias once it becomes a Pydantic model.
55-
ComponentIdentifierField = Annotated[
56-
ComponentIdentifier,
57-
BeforeValidator(lambda v: ComponentIdentifier.model_validate(v) if isinstance(v, dict) else v),
58-
PlainSerializer(lambda v: v.model_dump() if v is not None else None, return_type=Optional[dict]),
59-
]
60-
61-
ScoreField = Annotated[
62-
Score,
63-
BeforeValidator(lambda v: Score.from_dict(v) if isinstance(v, dict) else v),
64-
PlainSerializer(lambda v: v.to_dict(), return_type=dict),
65-
]
51+
# ``ComponentIdentifierField`` (and ``Score``) are imported from ``pyrit.models.score``
52+
# above. Both round-trip through the flat dict storage shape via their own Pydantic
53+
# serializers, so no local annotated aliases are needed here.
6654

6755

6856
def __getattr__(name: str) -> Any:
@@ -128,7 +116,7 @@ class MessagePiece(BaseModel):
128116
prompt_target_identifier: Optional[ComponentIdentifierField] = None
129117
attack_identifier: Optional[ComponentIdentifierField] = None
130118
scorer_identifier: Optional[ComponentIdentifierField] = None
131-
scores: list[ScoreField] = Field(default_factory=list)
119+
scores: list[Score] = Field(default_factory=list)
132120

133121
# When True, the memory layer skips persisting this piece. Used for ephemeral
134122
# pieces a scorer creates to score arbitrary content; ``exclude=True`` keeps

0 commit comments

Comments
 (0)