diff --git a/pyrit/score/scorer_evaluation/human_labeled_dataset.py b/pyrit/score/scorer_evaluation/human_labeled_dataset.py index 80fe36c99f..5d773c79f4 100644 --- a/pyrit/score/scorer_evaluation/human_labeled_dataset.py +++ b/pyrit/score/scorer_evaluation/human_labeled_dataset.py @@ -15,6 +15,8 @@ from pyrit.score.scorer_evaluation.metrics_type import MetricsType if TYPE_CHECKING: + from collections.abc import Hashable + from pyrit.models.harm_definition import HarmDefinition logger = logging.getLogger(__name__) @@ -271,11 +273,21 @@ def from_csv( if not harm_definition_version and parsed_harm_definition_version: harm_definition_version = parsed_harm_definition_version + # Preserve text before pandas can infer numeric or boolean values. + text_dtypes: dict[Hashable, type[str]] = dict.fromkeys( + ( + cls.STANDARD_ASSISTANT_RESPONSE_COL, + cls.STANDARD_OBJECTIVE_COL, + cls.STANDARD_HARM_COL, + cls.STANDARD_DATA_TYPE_COL, + ), + str, + ) # Try UTF-8 first, fall back to latin-1 for files with special characters try: - eval_df = pd.read_csv(csv_path, comment="#", encoding="utf-8") + eval_df = pd.read_csv(csv_path, comment="#", encoding="utf-8", dtype=text_dtypes) except UnicodeDecodeError: - eval_df = pd.read_csv(csv_path, comment="#", encoding="latin-1") + eval_df = pd.read_csv(csv_path, comment="#", encoding="latin-1", dtype=text_dtypes) # Drop rows where every column is NaN (e.g. trailing blank lines in the CSV) eval_df = eval_df.dropna(how="all") diff --git a/tests/unit/score/test_human_labeled_dataset.py b/tests/unit/score/test_human_labeled_dataset.py index 665a55fe88..328ebc1b21 100644 --- a/tests/unit/score/test_human_labeled_dataset.py +++ b/tests/unit/score/test_human_labeled_dataset.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from pathlib import Path + import pytest from pyrit.common.path import SCORER_EVALS_PATH @@ -13,6 +15,35 @@ ) +@pytest.mark.parametrize("value", ["00123", "1e3", "1.2300", "True"]) +@pytest.mark.parametrize("encoding", ["utf-8", "latin-1"]) +@pytest.mark.parametrize("metrics_type", [MetricsType.OBJECTIVE, MetricsType.HARM]) +def test_from_csv_preserves_numeric_and_boolean_text( + tmp_path: Path, value: str, encoding: str, metrics_type: MetricsType +) -> None: + column = "objective" if metrics_type == MetricsType.OBJECTIVE else "harm_category" + label = "1" if metrics_type == MetricsType.OBJECTIVE else "0.5" + csv_path = tmp_path / "text.csv" + csv_path.write_text( + f"assistant_response,{column},human_score,notes\n{value},00456,{label},café\n", + encoding=encoding, + ) + + dataset = HumanLabeledDataset.from_csv(csv_path=csv_path, metrics_type=metrics_type, version="1.0") + entry = dataset.entries[0] + piece = entry.conversation[0].message_pieces[0] + + assert piece.original_value == value + assert piece.converted_value == value + if isinstance(entry, ObjectiveHumanLabeledEntry): + assert entry.objective == "00456" + assert entry.human_scores == [True] + else: + assert isinstance(entry, HarmHumanLabeledEntry) + assert entry.harm_category == "00456" + assert entry.human_scores == [0.5] + + @pytest.fixture def sample_messages(): return [