fix(hotel_receptionist): pair near-miss rows in the expected-state diff - #6799
fix(hotel_receptionist): pair near-miss rows in the expected-state diff#6799u9g wants to merge 1 commit into
Conversation
A row the agent got almost right was reported twice β once missing, once unexpected β with every column echoed, burying the one field that differed. Greedily pair each missing row with its nearest unexpected row and report only the changed fields.
| pairs: list[tuple[tuple[Any, ...], tuple[Any, ...]]] = [] | ||
| remaining = list(unexpected) | ||
| for want in list(missing): | ||
| if not remaining: | ||
| break | ||
| got = min(remaining, key=lambda row: len(_changed_fields(cols, want, row))) | ||
| remaining.remove(got) | ||
| missing.remove(want) | ||
| unexpected.remove(got) | ||
| pairs.append((want, got)) | ||
| return pairs |
There was a problem hiding this comment.
π‘ Unrelated missing and extra records get merged into a single misleading difference report
Every absent record is paired with some extra record no matter how dissimilar (min(remaining, ...) at examples/hotel_receptionist/benchmark.py:125), so a truly absent record and a completely unrelated extra one are reported as one "row differs" line instead of two separate findings.
Impact: Grading reports can claim a record was merely edited when in fact one record is missing and a different, unrelated one was created, hiding real failures from whoever reads the report.
Greedy pairing has no similarity threshold
_pair_rows (examples/hotel_receptionist/benchmark.py:112-130) pairs while remaining is non-empty, with no bound on len(_changed_fields(...)). If the expected DB contains booking A that the agent never created, and the agent instead created an unrelated booking B for a different guest, missing == [A] and unexpected == [B], so they get paired and emitted as "row differs on guest_name: ... ; check_in: ... ; ..." listing essentially every compared column β the fact that A is absent and B is spurious is lost. The author's tested case ("a genuinely absent row still reports as missing") only holds when the unexpected list happens to be empty.
A threshold (e.g. only pair when the number of differing fields is small relative to the column count, or when key identity columns match) would preserve the intent while avoiding bogus pairings.
Prompt for agents
In examples/hotel_receptionist/benchmark.py, _pair_rows greedily pairs every missing row with the nearest unexpected row without any similarity threshold. When a row is genuinely absent from the agent's DB and an unrelated extra row exists, the two get paired and reported as a single 'row differs on <every column>' line, which conceals that one row is missing and another is spurious. Consider only accepting a pairing when the candidate is actually a near miss β e.g. when the number of differing fields is at most some fraction of the compared columns, or when a set of identity-ish columns matches β and leaving non-matching rows to print as plain missing/unexpected.
Was this helpful? React with π or π to provide feedback.
Summary
The expected-state grader reports a row the agent got almost right twice β once as
missing, once asunexpectedβ each echoing every compared column. The one field that actually differs is buried in two near-identical dicts.Greedily pair each missing row with its nearest unexpected row (fewest differing fields) and report only the changed fields. Unpaired rows still print as plain
missing/unexpected.Multiset differences are now expanded with
Counter.elements()so pairing works per row; theNxmultiplicity prefix is gone and duplicates print one line each. Sorting uses areprkey because rows mixNonewithstr/intin the same column, so tuples aren't directly orderable.Extracted from #6567, which carries this alongside unrelated hotel-scenario work.
Testing
ruff format --check/ruff checkclean;mypy --strictreports nothing inbenchmark.pydiff_databasesagainst synthetic apsw DBs: single-field near miss pairs into onerow differsline; a genuinely absent row still reports asmissing; 2-expected/1-actual reports one pair plus one leftover; identical states and empty tables both return[];None-vs-strcolumns sort without raising