fix(curate): guard _salvage_objects against None LLM content
The LLM (Opus score stage / Haiku cards stage) can return None or empty content, causing _salvage_objects to crash with 'NoneType' object has no attribute 'find' — which killed the curation pipeline and returned zero matches to the user. - Guard _salvage_objects and _salvage_kept against None/empty input - Add explicit empty-content checks in both stages with logging - Log content[:200] on unparseable score stage for QA diagnostics
This commit is contained in:
@@ -121,10 +121,12 @@ def _parse_json(text: str) -> dict:
|
||||
return json.loads(t)
|
||||
|
||||
|
||||
def _salvage_objects(text: str, *, after: str | None = None) -> list[dict]:
|
||||
def _salvage_objects(text: str | None, *, after: str | None = None) -> list[dict]:
|
||||
"""Recover valid objects from malformed/truncated model JSON: brace-scan each top-level {...} and
|
||||
json.loads it on its own, skipping the one broken/cut-off object. `after` anchors the scan past a key
|
||||
(e.g. '"kept"'); omit it to scan the whole text (the cards stage uses '"cards"', not '"kept"')."""
|
||||
if not text:
|
||||
return []
|
||||
s = text
|
||||
if after:
|
||||
i = text.find(after)
|
||||
@@ -145,9 +147,7 @@ def _salvage_objects(text: str, *, after: str | None = None) -> list[dict]:
|
||||
pass
|
||||
start = None
|
||||
return out
|
||||
|
||||
|
||||
def _salvage_kept(text: str) -> list[dict]:
|
||||
def _salvage_kept(text: str | None) -> list[dict]:
|
||||
"""Score-stage salvage — recover kept-job objects (anchored after the '"kept"' key)."""
|
||||
return _salvage_objects(text, after='"kept"')
|
||||
|
||||
@@ -216,12 +216,15 @@ def _score_stage(prefs: dict, ctx: dict | None, sifted_jobs: list[dict], s) -> l
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("score stage (Opus) failed, falling back to sift: %s", e)
|
||||
return None
|
||||
if not content or not content.strip():
|
||||
logger.warning("score stage LLM returned empty content — falling back to sift")
|
||||
return None
|
||||
try:
|
||||
kept = _parse_json(content).get("kept") or []
|
||||
except Exception: # noqa: BLE001
|
||||
kept = _salvage_kept(content)
|
||||
if not kept:
|
||||
logger.warning("score stage unparseable — falling back to sift")
|
||||
logger.warning("score stage unparseable — falling back to sift (content[:200]=%r)", content[:200])
|
||||
return None
|
||||
logger.warning("score stage JSON malformed; salvaged %d items", len(kept))
|
||||
# Floor check on the RAW computed score (weak never shown), THEN the transparent presentation
|
||||
@@ -256,6 +259,9 @@ def _cards_stage(prefs: dict, ctx: dict | None, scored: list[dict], by_id: dict,
|
||||
except Exception as e: # noqa: BLE001 — prose is non-critical; degrade to templated cards
|
||||
logger.warning("cards stage (Haiku) call failed, using evidence-based fallback prose: %s", e)
|
||||
return {}
|
||||
if not content or not content.strip():
|
||||
logger.warning("cards stage LLM returned empty content — using evidence-based fallback prose")
|
||||
return {}
|
||||
try:
|
||||
cards = _parse_json(content).get("cards") or []
|
||||
except Exception: # noqa: BLE001
|
||||
|
||||
Reference in New Issue
Block a user