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
38 changes: 38 additions & 0 deletions tests/test_from_dict_malformed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import pytest

from voiceeval.turns import from_dict


def _turn(**overrides) -> dict:
turn = {"speaker": "user", "text": "hello", "start_s": 0.0, "end_s": 1.0}
turn.update(overrides)
return turn


@pytest.mark.parametrize(
"turn,field",
[
({}, "speaker"),
(_turn(speaker="robot"), "speaker"),
(_turn(text=None), "text"),
({"speaker": "user", "text": "hello", "end_s": 1.0}, "start_s"),
(_turn(start_s="soon"), "start_s"),
(_turn(start_s=None), "start_s"),
({"speaker": "user", "text": "hello", "start_s": 0.0}, "end_s"),
(_turn(end_s="gone"), "end_s"),
],
)
def test_from_dict_malformed_turn_raises_descriptive_value_error(turn, field):
with pytest.raises(ValueError, match=f"turn 0.*{field}"):
from_dict({"turns": [turn]})


def test_from_dict_malformed_second_turn_names_turn_index():
turns = [_turn(), {"speaker": "agent", "text": "ok", "start_s": "later", "end_s": 5.0}]
with pytest.raises(ValueError, match="turn 1.*start_s"):
from_dict({"turns": turns})


def test_from_dict_missing_turns_key_is_descriptive():
with pytest.raises(ValueError, match="turns"):
from_dict({})
33 changes: 28 additions & 5 deletions voiceeval/turns.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,35 @@ def load(path: str | Path) -> Interaction:


def from_dict(data: dict) -> Interaction:
if not isinstance(data, dict):
raise ValueError("interaction must be a JSON object with a 'turns' list")
raw_turns = data.get("turns")
if not isinstance(raw_turns, list):
raise ValueError("interaction 'turns' must be a list")

def turn_field(turn, i, field, *, convert=None):
if not isinstance(turn, dict):
raise ValueError(f"turn {i} must be an object")
if field not in turn:
raise ValueError(f"turn {i} missing field '{field}'")
value = turn[field]
if convert is not None:
try:
return convert(value)
except (TypeError, ValueError):
raise ValueError(f"turn {i} field '{field}' must be a number, got {value!r}") from None
if field == "speaker" and value not in ("user", "agent"):
raise ValueError(f"turn {i} field 'speaker' must be 'user' or 'agent', got {value!r}")
if field == "text" and not isinstance(value, str):
raise ValueError(f"turn {i} field 'text' must be a string, got {value!r}")
return value

turns = [
Turn(
speaker=t["speaker"],
text=t["text"],
start_s=float(t["start_s"]),
end_s=float(t["end_s"]),
speaker=turn_field(t, i, "speaker"),
text=turn_field(t, i, "text"),
start_s=turn_field(t, i, "start_s", convert=float),
end_s=turn_field(t, i, "end_s", convert=float),
truth=t.get("truth"),
actions=[
Action(
Expand All @@ -104,7 +127,7 @@ def from_dict(data: dict) -> Interaction:
for a in t.get("actions", [])
],
)
for t in data["turns"]
for i, t in enumerate(raw_turns)
]
return Interaction(
id=data.get("id", "unnamed"),
Expand Down
Loading