Sangeetha-Grantha

Metadata Value
Status Completed
Version 1.1.0
Last Updated 2026-07-19
Author Sangeetha Grantha Team

TRACK-128: Gemini Enricher Hardening — Typed Errors, SDK-Native Parsing, Batch Safety

Goal: Bring src/gemini_enricher.py in line with the google-genai 2.x SDK’s intended usage and the project’s structured-output mandate (“structured outputs mapped to Pydantic schemas, never free-text parsing”). The current code detects rate limits by substring matching on exception text, hand-parses fenced JSON despite requesting response_schema, and has silent-truncation and double-spend hazards in batch mode.

Origin: Python best-practices evaluation, 2026-07-19 (item 4, first block). Builds on TRACK-107 (structured output/batch) and TRACK-124 (SDK 2.x).

Definition of Done

Inputs

Out of Scope

Steps

  1. Add respx-based tests pinning current behaviour for: 429 retry, non-429 failure, malformed JSON, batch count mismatch (red where the bug exists).
  2. Swap substring matching for typed exception handling; keep exponential backoff parameters unchanged.
  3. Move to response.parsed; keep text-parsing fallback behind a log warning.
  4. Fix zip(..., strict=True), applied semantics, dead parameter, magic constant.
  5. Guard the batch → sync fallback; document the quota implication in the module docstring.

Deliverables

Verification

Outcome (2026-07-19)

Characterisation tests were written and run before any source change, per the track’s step 1. Three pinned current behaviour and passed unchanged afterwards — that is the evidence the typed-error swap is behaviour-preserving:

Pinned behaviour Before After
429 → 5 backoff sleeps → gemini_error:max_retries_exceeded pass pass (unchanged)
500 → gemini_error:ServerError, no retry pass pass (unchanged)
Malformed JSON → gemini_error:*, metadata untouched pass pass (unchanged)
Batch count mismatch → 1:1 results fail (red) pass

The mismatch test was deliberately red first: 3 inputs returned 2 results, because zip(..., strict=False) truncated silently.

DoD, each verified against the source after the change:

Verification run: ruff check 0, ruff format --check clean (51 files), mypy 0, pytest 216 unit + 18 integration passing.

Note: _sync_fallback is a small extraction of a block that was already duplicated three times verbatim; it exists to carry the required loud log in one place, not as new abstraction.