Skip to content
Merged
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
2 changes: 1 addition & 1 deletion doc/code/memory/embeddings.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
}
],
"source": [
"embedding_response.to_json()"
"embedding_response.model_dump_json()"
]
},
{
Expand Down
2 changes: 1 addition & 1 deletion doc/code/memory/embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
# To view the json of an embedding

# %%
embedding_response.to_json()
embedding_response.model_dump_json()

# %% [markdown]
# To save an embedding to disk
Expand Down
32 changes: 23 additions & 9 deletions pyrit/models/chat_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,30 +35,37 @@ class ChatMessage(BaseModel):
tool_calls: Optional[list[ToolCall]] = None
tool_call_id: Optional[str] = None

def to_json(self) -> str:
Comment thread
romanlutz marked this conversation as resolved.
def to_dict(self) -> dict[str, Any]:
"""
Serialize the ChatMessage to a JSON string.
Convert the ChatMessage to a dictionary.

Returns:
A JSON string representation of the message.
A dictionary representation of the message, excluding None values.

"""
return self.model_dump_json()
return self.model_dump(exclude_none=True)

def to_dict(self) -> dict[str, Any]:
def to_json(self) -> str:
"""
Convert the ChatMessage to a dictionary.
Serialize the ChatMessage to a JSON string (deprecated, use ``model_dump_json`` instead).

Returns:
A dictionary representation of the message, excluding None values.
A JSON string representation of the message.

"""
return self.model_dump(exclude_none=True)
from pyrit.common.deprecation import print_deprecation_message

print_deprecation_message(
old_item="ChatMessage.to_json",
new_item="ChatMessage.model_dump_json",
removed_in="0.15.0",
)
return self.model_dump_json()

@classmethod
def from_json(cls, json_str: str) -> "ChatMessage":
"""
Deserialize a ChatMessage from a JSON string.
Deserialize a ChatMessage from a JSON string (deprecated, use ``model_validate_json`` instead).

Args:
json_str: A JSON string representation of a ChatMessage.
Expand All @@ -67,6 +74,13 @@ def from_json(cls, json_str: str) -> "ChatMessage":
A ChatMessage instance.

"""
from pyrit.common.deprecation import print_deprecation_message

print_deprecation_message(
old_item="ChatMessage.from_json",
new_item="ChatMessage.model_validate_json",
removed_in="0.15.0",
)
return cls.model_validate_json(json_str)


Expand Down
9 changes: 8 additions & 1 deletion pyrit/models/embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,19 @@ def load_from_file(file_path: Path) -> EmbeddingResponse:

def to_json(self) -> str:
"""
Serialize this embedding response to JSON.
Serialize this embedding response to JSON (deprecated, use ``model_dump_json`` instead).

Returns:
str: JSON-encoded embedding response.

"""
from pyrit.common.deprecation import print_deprecation_message

print_deprecation_message(
old_item="EmbeddingResponse.to_json",
new_item="EmbeddingResponse.model_dump_json",
removed_in="0.15.0",
)
return self.model_dump_json()


Expand Down
46 changes: 41 additions & 5 deletions pyrit/score/scorer_evaluation/scorer_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ class ScorerMetrics:
"""
Base dataclass for storing scorer evaluation metrics.

This class provides methods for serializing metrics to JSON and loading them from JSON files.
This class provides methods for serializing metrics to JSON strings (see
:meth:`to_json`) and loading them from JSON files on disk (see
:meth:`from_json_file`).

Args:
num_responses (int): Total number of responses evaluated.
Expand All @@ -48,23 +50,34 @@ class ScorerMetrics:

def to_json(self) -> str:
"""
Convert the metrics to a JSON string.
Serialize this metrics instance to a JSON string.

This is the canonical serialization entry point for ``ScorerMetrics`` and its
subclasses. Pair it with :meth:`from_json_file` (which reads a JSON file written
from this string, optionally wrapped in a ``"metrics"`` key) for round-trip
(de)serialization.

Returns:
str: The JSON string representation of the metrics.
"""
return json.dumps(asdict(self))

@classmethod
def from_json(cls: type[T], file_path: Union[str, Path]) -> T:
def from_json_file(cls: type[T], file_path: Union[str, Path]) -> T:
"""
Load the metrics from a JSON file.
Load a metrics instance from a JSON file on disk.

This is the canonical deserialization entry point for ``ScorerMetrics`` and its
subclasses. It accepts a *file path* (string or ``Path``), not a JSON string —
the loader opens the file, unwraps a top-level ``"metrics"`` key if present
(as used by evaluation result files), and filters out internal underscore-prefixed
fields (e.g., cached ``init=False`` attributes) before constructing the instance.

Args:
file_path (Union[str, Path]): The path to the JSON file.

Returns:
ScorerMetrics: An instance of ScorerMetrics with the loaded data.
ScorerMetrics: An instance of ScorerMetrics (or subclass) with the loaded data.

Raises:
FileNotFoundError: If the specified file does not exist.
Expand All @@ -82,6 +95,29 @@ def from_json(cls: type[T], file_path: Union[str, Path]) -> T:

return cls(**filtered_data)

@classmethod
def from_json(cls: type[T], file_path: Union[str, Path]) -> T:
"""
Load a metrics instance from a JSON file (deprecated alias for :meth:`from_json_file`).

The name ``from_json`` is misleading because it accepts a *file path*, not a JSON
string. Use :meth:`from_json_file` instead.

Args:
file_path (Union[str, Path]): The path to the JSON file.

Returns:
ScorerMetrics: An instance of ScorerMetrics (or subclass) with the loaded data.
"""
from pyrit.common.deprecation import print_deprecation_message

print_deprecation_message(
old_item=f"{cls.__name__}.from_json",
new_item=f"{cls.__name__}.from_json_file",
removed_in="0.15.0",
)
return cls.from_json_file(file_path)


@dataclass
class HarmScorerMetrics(ScorerMetrics):
Expand Down
29 changes: 22 additions & 7 deletions tests/unit/models/test_chat_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ def test_chat_message_invalid_role():
ChatMessage(role="invalid_role", content="hi")


def test_chat_message_to_json():
def test_chat_message_serializes_with_model_dump_json():
msg = ChatMessage(role="user", content="test")
json_str = msg.to_json()
json_str = msg.model_dump_json()
parsed = json.loads(json_str)
assert parsed["role"] == "user"
assert parsed["content"] == "test"
Expand All @@ -82,18 +82,18 @@ def test_chat_message_to_dict_excludes_none():
assert d["content"] == "test"


def test_chat_message_from_json():
def test_chat_message_model_validate_json_roundtrip():
original = ChatMessage(role="system", content="you are helpful")
json_str = original.to_json()
restored = ChatMessage.from_json(json_str)
json_str = original.model_dump_json()
restored = ChatMessage.model_validate_json(json_str)
assert restored.role == original.role
assert restored.content == original.content


def test_chat_message_from_json_roundtrip_with_tool_calls():
def test_chat_message_model_validate_json_roundtrip_with_tool_calls():
tc = ToolCall(id="c1", type="function", function="fn")
original = ChatMessage(role="assistant", content="ok", tool_calls=[tc], tool_call_id="c1")
restored = ChatMessage.from_json(original.to_json())
restored = ChatMessage.model_validate_json(original.model_dump_json())
assert restored.tool_calls[0].id == "c1"
assert restored.tool_call_id == "c1"

Expand All @@ -104,6 +104,21 @@ def test_chat_message_accepts_all_valid_roles(role):
assert msg.role == role


def test_chat_message_to_json_is_deprecated_alias_for_model_dump_json():
msg = ChatMessage(role="user", content="test")
with pytest.warns(DeprecationWarning, match="ChatMessage.to_json"):
result = msg.to_json()
assert result == msg.model_dump_json()


def test_chat_message_from_json_is_deprecated_alias_for_model_validate_json():
original = ChatMessage(role="system", content="you are helpful")
json_str = original.model_dump_json()
with pytest.warns(DeprecationWarning, match="ChatMessage.from_json"):
restored = ChatMessage.from_json(json_str)
assert restored == original


def test_chat_messages_dataset_init():
msgs = [[ChatMessage(role="user", content="hi"), ChatMessage(role="assistant", content="hello")]]
dataset = ChatMessagesDataset(name="test_ds", description="A test dataset", list_of_chat_messages=msgs)
Expand Down
6 changes: 6 additions & 0 deletions tests/unit/models/test_embedding_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,9 @@ def test_save_load_loop_is_idempotent(my_embedding):
output_file = my_embedding.save_to_file(Path(tmp_dir))
loaded_embedding = EmbeddingResponse.load_from_file(Path(output_file))
assert my_embedding == loaded_embedding


def test_to_json_is_deprecated_alias_for_model_dump_json(my_embedding: EmbeddingResponse):
with pytest.warns(DeprecationWarning, match="EmbeddingResponse.to_json"):
result = my_embedding.to_json()
assert result == my_embedding.model_dump_json()
28 changes: 24 additions & 4 deletions tests/unit/score/test_scorer_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from pathlib import Path
from unittest.mock import patch

import pytest

from pyrit.identifiers import ComponentIdentifier
from pyrit.score import (
HarmScorerMetrics,
Expand All @@ -21,7 +23,7 @@
class TestScorerMetricsSerialization:
"""Tests for ScorerMetrics JSON serialization."""

def test_harm_metrics_to_json_and_from_json(self, tmp_path):
def test_harm_metrics_to_json_and_from_json_file(self, tmp_path):
metrics = HarmScorerMetrics(
num_responses=10,
num_human_raters=3,
Expand All @@ -41,10 +43,10 @@ def test_harm_metrics_to_json_and_from_json(self, tmp_path):
file_path = tmp_path / "metrics.json"
with open(file_path, "w") as f:
f.write(json_str)
loaded = HarmScorerMetrics.from_json(str(file_path))
loaded = HarmScorerMetrics.from_json_file(str(file_path))
assert loaded == metrics

def test_objective_metrics_to_json_and_from_json(self, tmp_path):
def test_objective_metrics_to_json_and_from_json_file(self, tmp_path):
metrics = ObjectiveScorerMetrics(
num_responses=10,
num_human_raters=3,
Expand All @@ -61,7 +63,25 @@ def test_objective_metrics_to_json_and_from_json(self, tmp_path):
file_path = tmp_path / "metrics.json"
with open(file_path, "w") as f:
f.write(json_str)
loaded = ObjectiveScorerMetrics.from_json(str(file_path))
loaded = ObjectiveScorerMetrics.from_json_file(str(file_path))
assert loaded == metrics

def test_from_json_is_deprecated_alias_for_from_json_file(self, tmp_path):
metrics = ObjectiveScorerMetrics(
num_responses=10,
num_human_raters=3,
accuracy=0.9,
accuracy_standard_error=0.05,
f1_score=0.8,
precision=0.85,
recall=0.75,
)
file_path = tmp_path / "metrics.json"
with open(file_path, "w") as f:
f.write(metrics.to_json())

with pytest.warns(DeprecationWarning, match="ObjectiveScorerMetrics.from_json"):
loaded = ObjectiveScorerMetrics.from_json(str(file_path))
assert loaded == metrics


Expand Down
Loading