diff --git a/doc/code/memory/embeddings.ipynb b/doc/code/memory/embeddings.ipynb index c48682e61..bd2db1e12 100644 --- a/doc/code/memory/embeddings.ipynb +++ b/doc/code/memory/embeddings.ipynb @@ -77,7 +77,7 @@ } ], "source": [ - "embedding_response.to_json()" + "embedding_response.model_dump_json()" ] }, { diff --git a/doc/code/memory/embeddings.py b/doc/code/memory/embeddings.py index 03d897864..4b2edd88d 100644 --- a/doc/code/memory/embeddings.py +++ b/doc/code/memory/embeddings.py @@ -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 diff --git a/pyrit/models/chat_message.py b/pyrit/models/chat_message.py index 6a1e7c2ff..c2f801862 100644 --- a/pyrit/models/chat_message.py +++ b/pyrit/models/chat_message.py @@ -35,30 +35,37 @@ class ChatMessage(BaseModel): tool_calls: Optional[list[ToolCall]] = None tool_call_id: Optional[str] = None - def to_json(self) -> str: + 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. @@ -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) diff --git a/pyrit/models/embeddings.py b/pyrit/models/embeddings.py index a71d636b8..e51ae48f8 100644 --- a/pyrit/models/embeddings.py +++ b/pyrit/models/embeddings.py @@ -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() diff --git a/pyrit/score/scorer_evaluation/scorer_metrics.py b/pyrit/score/scorer_evaluation/scorer_metrics.py index 0dbccec57..528f664d6 100644 --- a/pyrit/score/scorer_evaluation/scorer_metrics.py +++ b/pyrit/score/scorer_evaluation/scorer_metrics.py @@ -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. @@ -48,7 +50,12 @@ 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. @@ -56,15 +63,21 @@ def to_json(self) -> str: 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. @@ -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): diff --git a/tests/unit/models/test_chat_message.py b/tests/unit/models/test_chat_message.py index b3726b650..8391e9340 100644 --- a/tests/unit/models/test_chat_message.py +++ b/tests/unit/models/test_chat_message.py @@ -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" @@ -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" @@ -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) diff --git a/tests/unit/models/test_embedding_response.py b/tests/unit/models/test_embedding_response.py index fe39fa102..03e338f44 100644 --- a/tests/unit/models/test_embedding_response.py +++ b/tests/unit/models/test_embedding_response.py @@ -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() diff --git a/tests/unit/score/test_scorer_metrics.py b/tests/unit/score/test_scorer_metrics.py index 7000a9e12..ba01fb267 100644 --- a/tests/unit/score/test_scorer_metrics.py +++ b/tests/unit/score/test_scorer_metrics.py @@ -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, @@ -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, @@ -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, @@ -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