fix(language): always raise on ambiguous resolver matches

`_select_one` previously skipped its ambiguity check whenever any of
`role`/`tool_name`/`camera` was set, on the assumption that the caller
had already pinned down a unique row. That left a real ambiguity hole
for VQA: with two cameras emitting `(vqa, assistant)` at the same
frame, `emitted_at(..., role="assistant")` silently picked the first
sorted row instead of telling the recipe to add `camera=...`. The
existing `test_emitted_at_raises_on_ambiguous_per_camera_vqa` test
already encoded the desired behavior.

Tighten the check: any time `len(rows) > 1` we now raise with the
selectors echoed back, so users see exactly which fields they passed
and that more is needed to disambiguate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pepijn
2026-05-06 14:00:45 +02:00
parent e8327b8e62
commit a356b12c41
+11 -8
View File
@@ -468,20 +468,23 @@ def _select_one(
tool_name: str | None, tool_name: str | None,
camera: str | None, camera: str | None,
) -> LanguageRow | None: ) -> LanguageRow | None:
"""Return the single matching row, or raise if the selectors are ambiguous. """Return the single matching row, or raise if the resolver is ambiguous.
Ties are broken deterministically by ``_row_sort_key`` so that Multiple matches always raise — even when the caller already passed
multiple rows with identical ``(style, role, tool_name, camera)`` still some selectors — because remaining ambiguity means the data has
resolve to a stable choice. several rows that look identical to the resolver and the caller
needs to pin down a specific one (e.g. add ``camera=...`` for VQA
rows shared across cameras).
""" """
if not rows: if not rows:
return None return None
if len(rows) > 1 and role is None and tool_name is None and camera is None: if len(rows) > 1:
raise ValueError( raise ValueError(
f"Ambiguous resolver for style={style!r}; add role=..., tool_name=..., " f"Ambiguous resolver for style={style!r} role={role!r} "
f"or camera=... to disambiguate." f"tool_name={tool_name!r} camera={camera!r}: {len(rows)} matching rows. "
f"Add a selector that distinguishes them."
) )
return sorted(rows, key=_row_sort_key)[0] return rows[0]
def _row_sort_key(row: LanguageRow) -> tuple[float, str, str]: def _row_sort_key(row: LanguageRow) -> tuple[float, str, str]: