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
14 changes: 10 additions & 4 deletions pyrit/score/response_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import abc
import json
import math
from abc import abstractmethod
from collections.abc import Sequence
from typing import TYPE_CHECKING
Expand Down Expand Up @@ -146,7 +147,7 @@ class JsonSchemaResponseHandler(ResponseHandler):
``json.loads`` the text, then read the score value, rationale, optional description,
category, and metadata from configurable keys. It also owns the response contract: the
optional JSON schema handed to the target, and (when ``numeric_value`` is set) validating
that the parsed score value is numeric.
that the parsed score value is finite and numeric.
"""

def __init__(
Expand All @@ -173,7 +174,8 @@ def __init__(
should honor. Exposed via ``response_schema`` and forwarded to the target by the
LLM round-trip. Defaults to None.
numeric_value (bool): When True, ``parse`` requires the parsed score value to be
parsable as a float and raises ``InvalidJsonException`` otherwise. Defaults to False.
parsable as a finite float and raises ``InvalidJsonException`` otherwise. Defaults
to False.
"""
self._score_value_output_key = score_value_output_key
self._rationale_output_key = rationale_output_key
Expand Down Expand Up @@ -219,7 +221,7 @@ def parse(
parsed category is not a string or a list of strings.
InvalidJsonException: If the response is invalid JSON, is not a top-level JSON object,
is missing a required key, or (when this handler is numeric) the score value is not
parsable as a float.
parsable as a finite float.
"""
response_json = remove_markdown_json(response_text)
try:
Expand Down Expand Up @@ -251,11 +253,15 @@ def parse(
try:
# A numeric handler requires the score value to be parsable as a float; a
# well-formed-but-non-numeric value is treated as an invalid response.
float(score.raw_score_value)
parsed_value = float(score.raw_score_value)
except ValueError:
raise InvalidJsonException(
message=f"Invalid JSON response, score_value should be a float not this: {score.raw_score_value}"
) from None
if not math.isfinite(parsed_value):
raise InvalidJsonException(
message=f"Invalid JSON response, score_value must be a finite float: {score.raw_score_value}"
)

return score

Expand Down
12 changes: 12 additions & 0 deletions tests/unit/score/test_response_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ def test_json_schema_response_handler_rejects_non_object_response(response_text:
)


@pytest.mark.parametrize("score_value", ["nan", "NaN", "inf", "-inf", "Infinity"])
def test_json_schema_response_handler_rejects_non_finite_numeric_values(score_value: str) -> None:
handler = JsonSchemaResponseHandler(numeric_value=True)

with pytest.raises(InvalidJsonException, match="finite float"):
handler.parse(
response_text=f'{{"score_value": "{score_value}", "rationale": "test"}}',
scorer_identifier=SCORER_IDENTIFIER,
scored_prompt_id="test-id",
)


@pytest.mark.parametrize(
("json_value", "expected"),
[
Expand Down