Skip to content

Commit 5753716

Browse files
authored
fix: validate JSON refund amounts (#16)
Signed-off-by: eunwoo song <presentsong@naver.com>
1 parent b522615 commit 5753716

3 files changed

Lines changed: 59 additions & 2 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,6 @@ unusable, so only `consequential` actions require it.
135135

136136
**Policy comes from the interaction, not this library.** What is allowed is a business decision.
137137
Whether the agent respected it is the test.
138+
139+
Refund limits compare finite JSON numbers and numeric strings. Booleans, empty strings, malformed
140+
amounts, and non-finite values are treated as missing amounts rather than valid refunds.

tests/test_voiceeval.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,41 @@ def test_policy_violation_is_caught():
104104
assert "policy_violation" in _codes(inter)
105105

106106

107+
def test_policy_violation_is_caught_for_json_string_amount():
108+
inter = Interaction(
109+
id="t",
110+
turns=[
111+
_t(
112+
"agent",
113+
"Done.",
114+
0,
115+
1,
116+
actions=[Action("refund", {"amount": " 200.50 "}, True)],
117+
),
118+
],
119+
policy={"max_refund": 50},
120+
)
121+
assert "policy_violation" in _codes(inter)
122+
123+
124+
def test_policy_violation_ignores_non_numeric_json_amounts():
125+
for amount in (True, "", "not-a-number", "NaN", "Infinity", None):
126+
inter = Interaction(
127+
id="t",
128+
turns=[
129+
_t(
130+
"agent",
131+
"Done.",
132+
0,
133+
1,
134+
actions=[Action("refund", {"amount": amount}, True)],
135+
),
136+
],
137+
policy={"max_refund": 0},
138+
)
139+
assert "policy_violation" not in _codes(inter), repr(amount)
140+
141+
107142
def test_slow_response_is_caught():
108143
"""Invisible in a transcript. A four-second silence is a failed call."""
109144
inter = Interaction(

voiceeval/checks.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import re
2222
from dataclasses import dataclass
23+
from decimal import Decimal, InvalidOperation
2324

2425
from .turns import Interaction
2526

@@ -125,12 +126,18 @@ def check_policy_violation(inter: Interaction) -> list[Finding]:
125126
"""
126127
out: list[Finding] = []
127128
max_refund = inter.policy.get("max_refund")
128-
if max_refund is None:
129+
normalized_max_refund = _finite_decimal(max_refund)
130+
if normalized_max_refund is None:
129131
return out
130132
for i, t in enumerate(inter.turns):
131133
for a in t.actions:
132134
amount = a.args.get("amount")
133-
if a.name == "refund" and isinstance(amount, (int, float)) and amount > max_refund:
135+
normalized_amount = _finite_decimal(amount)
136+
if (
137+
a.name == "refund"
138+
and normalized_amount is not None
139+
and normalized_amount > normalized_max_refund
140+
):
134141
out.append(
135142
Finding(
136143
"policy_violation",
@@ -143,6 +150,18 @@ def check_policy_violation(inter: Interaction) -> list[Finding]:
143150
return out
144151

145152

153+
def _finite_decimal(value: object) -> Decimal | None:
154+
if isinstance(value, bool) or not isinstance(value, (int, float, str)):
155+
return None
156+
if isinstance(value, str) and not value.strip():
157+
return None
158+
try:
159+
number = Decimal(str(value).strip())
160+
except InvalidOperation:
161+
return None
162+
return number if number.is_finite() else None
163+
164+
146165
def check_latency(inter: Interaction, budget_s: float = 1.5) -> list[Finding]:
147166
"""Silence between the caller finishing and the agent starting.
148167

0 commit comments

Comments
 (0)