Skip to content

fix(hotel_receptionist): pair near-miss rows in the expected-state diff - #6799

Open
u9g wants to merge 1 commit into
mainfrom
fix/hotel-diff-pair-rows
Open

fix(hotel_receptionist): pair near-miss rows in the expected-state diff#6799
u9g wants to merge 1 commit into
mainfrom
fix/hotel-diff-pair-rows

Conversation

@u9g

@u9g u9g commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

The expected-state grader reports a row the agent got almost right twice β€” once as missing, once as unexpected β€” 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.

- hotel_bookings: missing 1x {'guest_name': 'Ana Ruiz', 'nights': 2, 'room_type_view': 'king/ocean', ...}
- hotel_bookings: unexpected 1x {'guest_name': 'Ana Ruiz', 'nights': 3, 'room_type_view': 'king/ocean', ...}
+ hotel_bookings: row differs on nights: 2 != 3

Multiset differences are now expanded with Counter.elements() so pairing works per row; the Nx multiplicity prefix is gone and duplicates print one line each. Sorting uses a repr key because rows mix None with str/int in 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 check clean; mypy --strict reports nothing in benchmark.py
  • Exercised diff_databases against synthetic apsw DBs: single-field near miss pairs into one row differs line; a genuinely absent row still reports as missing; 2-expected/1-actual reports one pair plus one leftover; identical states and empty tables both return []; None-vs-str columns sort without raising

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.
@u9g
u9g requested a review from a team as a code owner August 11, 2026 20:03

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Open in Devin Review

Comment on lines +120 to +130
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 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.
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant