Skip to content

Commit aee6afd

Browse files
author
Dylan Huang
committed
test_pytest_stable_row_ids
1 parent c784309 commit aee6afd

5 files changed

Lines changed: 100 additions & 5 deletions

File tree

eval_protocol/human_id/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def generate_id(
7878
return separator.join(parts)
7979

8080

81-
def num_combinations(word_count: int) -> int:
81+
def num_combinations(word_count: int = 5) -> int:
8282
"""
8383
Return the total number of unique IDs possible for the given word_count.
8484

eval_protocol/models.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ class InputMetadata(BaseModel):
209209

210210
model_config = ConfigDict(extra="allow")
211211

212-
row_id: Optional[str] = Field(default_factory=generate_id, description="Unique string to ID the row")
212+
row_id: Optional[str] = Field(None, description="Unique string to ID the row")
213213
completion_params: CompletionParams = Field(
214214
default_factory=dict, description="Completion endpoint parameters used"
215215
)
@@ -422,9 +422,21 @@ def get_termination_reason(self) -> str:
422422
return "unknown"
423423

424424
def __hash__(self) -> int:
425-
json_str = self.model_dump_json(exclude_none=True)
425+
# Use a stable hash by sorting keys and ensuring compact output
426+
json_str = self.stable_json(self)
426427
return hash(json_str)
427428

429+
@classmethod
430+
def stable_json(cls, row: "EvaluationRow") -> int:
431+
json_str = row.model_dump_json(
432+
exclude_none=True,
433+
exclude_defaults=True,
434+
by_alias=True,
435+
indent=None,
436+
exclude=["created_at", "execution_metadata"],
437+
)
438+
return json_str
439+
428440

429441
# Original dataclass-based models for backwards compatibility
430442
# These are deprecated and will be removed in a future version

eval_protocol/pytest/evaluation_test.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from eval_protocol.dataset_logger import default_logger
1717
from eval_protocol.dataset_logger.dataset_logger import DatasetLogger
18-
from eval_protocol.human_id import generate_id
18+
from eval_protocol.human_id import generate_id, num_combinations
1919
from eval_protocol.models import (
2020
CompletionParams,
2121
EvalMetadata,
@@ -294,6 +294,16 @@ def _log_eval_error(
294294
else:
295295
raise ValueError("No input dataset or input messages provided")
296296

297+
for row in data:
298+
# generate a stable row_id for each row
299+
if row.input_metadata.row_id is None:
300+
# Generate a stable, deterministic row_id using the row's hash and num_combinations
301+
index = hash(row)
302+
max_index = num_combinations() - 1
303+
# Ensure index is a non-negative integer within [0, max_index]
304+
index = abs(index) % (max_index + 1)
305+
row.input_metadata.row_id = generate_id(seed=0, index=index)
306+
297307
if "completion_params" not in kwargs or not kwargs["completion_params"]:
298308
raise ValueError(
299309
"No completion parameters provided. Please provide a completion parameters object."

tests/pytest/test_pytest_stable_row_id.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import List
22

3-
from eval_protocol.models import EvaluationRow
3+
from eval_protocol.models import EvaluationRow, Message
44
from eval_protocol.pytest.default_no_op_rollout_processor import NoOpRolloutProcessor
55
from tests.pytest.test_markdown_highlighting import markdown_dataset_to_evaluation_row
66

@@ -37,5 +37,61 @@ def eval_fn(row: EvaluationRow) -> EvaluationRow:
3737
for params in completion_params_list:
3838
await eval_fn(dataset_path=[ds_path], completion_params=params)
3939

40+
# Second invocation to ensure that IDs are stable across multiple invocations
41+
for ds_path in input_dataset:
42+
for params in completion_params_list:
43+
await eval_fn(dataset_path=[ds_path], completion_params=params)
44+
4045
# Assertions on IDs generated by the decorator logic
4146
assert len(row_ids) == 19 # from the markdown dataset
47+
48+
49+
async def test_evaluation_test_generated_row_ids_without_dataset_keys():
50+
from eval_protocol.pytest.evaluation_test import evaluation_test
51+
52+
# Adapter that does NOT set row_id; lets evaluation_test generate IDs
53+
def markdown_dataset_no_row_id_adapter(data: List[dict]) -> List[EvaluationRow]:
54+
return [
55+
EvaluationRow(
56+
messages=[Message(role="user", content=row["prompt"])],
57+
ground_truth=str(row["num_highlights"]),
58+
)
59+
for row in data
60+
]
61+
62+
row_ids = set()
63+
64+
input_dataset = ["tests/pytest/data/markdown_dataset.jsonl", "tests/pytest/data/markdown_dataset.jsonl"]
65+
completion_params = [
66+
{"temperature": 0.0, "model": "dummy/local-model"},
67+
{"temperature": 1.0, "model": "dummy/local-model"},
68+
]
69+
70+
@evaluation_test(
71+
input_dataset=input_dataset,
72+
completion_params=completion_params,
73+
dataset_adapter=markdown_dataset_no_row_id_adapter,
74+
rollout_processor=NoOpRolloutProcessor(),
75+
mode="pointwise",
76+
combine_datasets=False,
77+
num_runs=5,
78+
)
79+
def eval_fn(row: EvaluationRow) -> EvaluationRow:
80+
# row_id should be auto-generated by evaluation_test/InputMetadata
81+
assert row.input_metadata is not None
82+
assert row.input_metadata.row_id is not None and isinstance(row.input_metadata.row_id, str)
83+
row_ids.add(row.input_metadata.row_id)
84+
return row
85+
86+
# Single invocation (one dataset, one param set) with multiple runs
87+
for ds_path in input_dataset:
88+
for params in completion_params:
89+
await eval_fn(dataset_path=[ds_path], completion_params=params)
90+
91+
# Second invocation to ensure that IDs are stable across multiple invocations
92+
for ds_path in input_dataset:
93+
for params in completion_params:
94+
await eval_fn(dataset_path=[ds_path], completion_params=params)
95+
96+
# Even with multiple runs, generated row_ids should be stable within the invocation
97+
assert len(row_ids) == 19 # equals dataset size when IDs are generated once and preserved across runs

tests/test_models.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,23 @@ def test_evaluation_row_creation():
289289
assert not row.is_trajectory_evaluation()
290290

291291

292+
def test_stable_hash():
293+
"""Test the stable hash method."""
294+
row = EvaluationRow(
295+
messages=[Message(role="user", content="What is 2+2?"), Message(role="assistant", content="2+2 equals 4.")],
296+
ground_truth="4",
297+
)
298+
row2 = EvaluationRow(
299+
messages=[Message(role="user", content="What is 2+2?"), Message(role="assistant", content="2+2 equals 4.")],
300+
ground_truth="4",
301+
)
302+
stable_json = EvaluationRow.stable_json(row)
303+
stable_json2 = EvaluationRow.stable_json(row2)
304+
assert stable_json == stable_json2
305+
assert "created_at" not in stable_json
306+
assert "execution_metadata" not in stable_json
307+
308+
292309
def test_evaluation_row_trajectory_evaluation():
293310
"""Test EvaluationRow with trajectory evaluation."""
294311
messages = [

0 commit comments

Comments
 (0)