feat(eval): golden Q&A matching in core — the producer for GOLDEN_EXPECTED_ANSWER (closes #428) - #432
Conversation
…PECTED_ANSWER GoogleCloudPlatform#378 (U4) gave the SDK the consumer side of answer-key grounding (per_session_context + CategoricalContextSource.GOLDEN_EXPECTED_ANSWER); the producer stayed in scripts/quality_report.py. This extracts it verbatim into bigquery_agent_analytics.golden_matching: - embed_texts(texts, model=None, batch_size=50, max_attempts=5) -- L2-normalised vectors with bounded 429/503 retry (the exact resilience added after an embedding quota burst aborted a live gate run in GoogleCloudPlatform#385); - DEFAULT_GOLDEN_THRESHOLD = 0.92; - match_golden_qa(question_by_key, golden_qa, threshold) -> per-key judge-context mapping (expected answers + out-of-scope decline notes) ready for evaluate_categorical(per_session_context=...), plus per-key match metadata for reporting. Keys are opaque: exact ResolvedTraceSelectors on the BigQuery path, session ids locally. scripts/quality_report.py becomes a thin caller (aliases keep its public/test surface stable); the embedding-retry tests move to tests/test_golden_matching.py alongside new matching tests (threshold, out-of-scope decline note, empty inputs, opaque keys). Follow-up direction (tracked in the promotion issue): a server-side embedding path via ai_ml_integration's AI.EMBED would remove the genai embedding dependency entirely.
Code review: golden Q&A matching in coreVerdict: request changes — one real trap, plus a pattern break. The extraction itself is verbatim (I diffed the moved
Cross-PR note: this PR and #431 both insert their export block at the same anchor in |
caohy1988
left a comment
There was a problem hiding this comment.
Review: Approve with nits
Verified locally: diffed the deleted block in scripts/quality_report.py (normalized for the _embed_texts/_DEFAULT_GOLDEN_THRESHOLD renames) against the new golden_matching.py — the move is verbatim; the only difference is the threshold constant moving above embed_texts(). Nothing was lost in the −202 deletions. Ran both affected test files: 135 passed. The producer output shape matches #378's consumer (_validated_context_mapping in categorical_evaluator.py:263-281 accepts exactly what match_golden_qa emits). Retry logic is correct and bounded.
Findings
MINOR — backward-compat claim slightly overstated: monkeypatch surface moved (scripts/quality_report.py:353)
The script binds embed_texts as _embed_texts, but match_golden_qa now resolves embed_texts in golden_matching's module globals. Before this PR, patching quality_report_module._embed_texts intercepted embeddings for match_golden_qa; now it silently does nothing (the in-repo test was updated at tests/test_quality_report_helpers.py:304-306, so the authors know). Any external host/test patching the script's _embed_texts will break quietly. Suggest a one-line comment on the alias noting it's re-export-only, or a release note.
MINOR — EMBEDDING_MODEL env-var read timing moved earlier
The script used to read EMBEDDING_MODEL at its own import; now it's captured when bigquery_agent_analytics/__init__.py eagerly imports golden_matching (i.e., at SDK import). A host that sets the env var after importing the SDK gets the old default. Also, rebinding quality_report_module.EMBEDDING_MODEL (now an F401 alias) no longer affects the embedding model. Edge-case, but a real semantic change from the "aliases keep the surface stable" claim.
NIT — test_keys_are_opaque uses a key type the consumer would reject (tests/test_golden_matching.py:132)
Uses ("identity", "scope") (a tuple) as the opaque key, but _validated_context_mapping raises TypeError for anything that isn't exactly ResolvedTraceSelector or str (categorical_evaluator.py:271-281). The test proves opacity with a key that would fail downstream; use a real ResolvedTraceSelector to demonstrate the actual contract.
NIT — docstring overclaims key preservation (golden_matching.py:106)
Says "per_session_context preserves each input key", but keys with empty/None questions are silently dropped (:116) and get no metadata entry either. Pre-existing behavior, but now a public API docstring — worth one clause: "keys with empty questions are omitted."
NIT — unused import math left behind (scripts/quality_report.py:59)
math was only used by the extracted _embed_texts; no math. references remain.
NIT — parameter name contradicts the description/issue (golden_matching.py:92)
Both the PR body and #428 specify match_golden_qa(question_by_key, ...); the code keeps question_by_sid. Keeping the old name is defensible for compat, but the description is inaccurate and sid undercuts the "opaque keys" story.
NIT — dead try/except ImportError guard (__init__.py:394-410)
The module imports only stdlib at module level and google-genai (a hard dependency) lazily inside embed_texts — the guard can never fire. Harmless and consistent with the file's style; just noting it buys nothing.
Positives
- Move verified verbatim; threshold semantics (
>=, first-best tie-breaking, 0.92 default,round(score, 4)) unchanged. - Retry logic correct: bounded at
max_attempts, immediate re-raise on non-429/503, backoff capped at 2+4+8+16s per batch; tests cover all three branches with call-count assertions. - The two script call sites still pass
ResolvedTraceSelectorkeys on the BQ path and session-id strings locally, then feedper_session_context+CategoricalContextSource.GOLDEN_EXPECTED_ANSWER— exactly what the consumer validates.
Review by @caohy1988's assistant (Kimi Code CLI).
Code review: current head
|
|
Open-PR sweep recheck at unchanged head No commits have landed since my full current-head review, and the current CI checks remain green. The Ready with fixes verdict still applies: the public matching controls still need validation for positive integer batch/retry values and a finite threshold in Full current-head review: #432 (comment) Coordination note: #431 still edits overlapping evaluator/export surfaces, so whichever PR lands second should be refreshed against the first. |
caohy1988
left a comment
There was a problem hiding this comment.
Requesting changes at exact head a5dfd7e. The focused suites pass (135 tests) and current CI is green, but the public matching controls still need validation.
At src/bigquery_agent_analytics/golden_matching.py:61, batch_size, max_attempts, and threshold flow into range/retry/comparison logic without validation. Zero or negative batch sizes cause implementation-level errors or silent no-work behavior; zero attempts can leave resp unbound; and non-finite or out-of-range thresholds silently produce nonsensical matching.
Please require positive batch_size and max_attempts, plus a finite threshold in [0.0, 1.0], raise ValueError for invalid values, and add boundary/non-finite tests. This was independently revalidated on the current head.
…tching # Conflicts: # src/bigquery_agent_analytics/__init__.py
Reviewer-requested (P2, PR GoogleCloudPlatform#432): embed_texts now rejects batch_size < 1 and max_attempts < 1, and match_golden_qa rejects a non-finite or out-of-range threshold, all with ValueError naming the invalid value. Previously batch_size=-1 silently returned no vectors, batch_size=0 leaked a range() error, max_attempts=0 hit UnboundLocalError, and a NaN/out-of-range threshold silently matched everything or nothing. Adds zero/negative/inf/NaN boundary tests plus accepted-bound tests (batch_size=1, max_attempts=1, threshold 0.0 and 1.0).
|
Both outstanding items from the changes-requested review are addressed at head P2 — validate public matching controls (fixed in
New boundary tests (parametrized) cover every case named in the review: Merge coordination with #431 (done in #431 landed, so the branch is merged up to current Verification at |
caohy1988
left a comment
There was a problem hiding this comment.
LGTM - approved at exact head 0398e24.
I re-reviewed the complete extraction, the merged #431 export block, and the latest public-control fix. The previously requested zero/negative batch and retry cases plus non-finite/out-of-range thresholds now fail early with documented ValueErrors. The combined exact-head suites pass (153 tests), git diff --check is clean, and current CI is green across formatting, build, and Python 3.10-3.14.
One nonblocking public-API edge remains: positive non-integral batch_size or max_attempts values pass the new < 1 checks and later raise TypeError from range(). Please tighten those controls to positive integers (and add 1.5 tests) in this PR if convenient or as a small follow-up. This does not block approval because the documented count controls work correctly for their supported integer usage.
235cdd4
into
GoogleCloudPlatform:main
The release branch was cut one commit before #432 landed on main; the synthetic merge CI builds would already ship golden_matching.py in the 0.5.1 wheel, so the module and its three top-level exports must be in the release section. Merges main and documents the module in Release highlights and Added.
…configurator (#450) * chore(release): 0.5.1 — evalbench reader, canonical rubrics, single-input configurator Version bump 0.5.0 -> 0.5.1 and the changelog cut for everything merged since v0.5.0 (2026-08-11). In the wheel: the EvalBench BigQuery run reader (#444), canonical evaluation rubrics in core (#431), and the CLI judge-feedback escaping fix (#438). Repo/live-template side: the single fully-qualified table-ID configurator entrance (#449), Console table-link paste (#424), the attested external-access contract with its staleness workflow (#446), the end-user manual (#425), Grafana metric and scan-bound fixes (#433) with the one-command local run (#422), and the documentation and CI follow-ups. * chore(release): fold #432 golden-matching into the 0.5.1 cut The release branch was cut one commit before #432 landed on main; the synthetic merge CI builds would already ship golden_matching.py in the 0.5.1 wheel, so the module and its three top-level exports must be in the release section. Merges main and documents the module in Release highlights and Added.
Extracts golden Q&A matching from
scripts/quality_report.pyintobigquery_agent_analytics.golden_matching, per #428 — completing the U4 story: #378 shipped the consumer (per_session_context+CategoricalContextSource.GOLDEN_EXPECTED_ANSWER); this ships the producer.What's in core now:
embed_texts(texts, model=None, batch_size=50, max_attempts=5)— L2-normalised vectors with bounded 429/503 retry (the resilience added after an embedding quota burst aborted a live gate run during Skill-evolution lab: remove the #358/#359 workarounds (U6, closes #360) #385's AE8).DEFAULT_GOLDEN_THRESHOLD = 0.92.match_golden_qa(question_by_key, golden_qa, threshold)→ per-key judge-context mapping (expected answers + out-of-scope decline notes) ready forevaluate_categorical(per_session_context=..., context_source=GOLDEN_EXPECTED_ANSWER), plus per-key match metadata. Keys are opaque: exactResolvedTraceSelectors on the BigQuery path, session ids locally.Same-PR thinning: the script keeps its public/test surface via aliases and delegates everything; the embedding-retry tests move to
tests/test_golden_matching.pyalongside new matching tests (threshold behavior, decline notes, empty inputs, opaque keys).Follow-up (noted on #428): a server-side embedding path via
ai_ml_integration'sAI.EMBED/ML.DISTANCEwould remove the genai embedding dependency entirely. Refs #378, #385, #63.