diff --git a/app/engine/curate.py b/app/engine/curate.py index 4bfc75e..81a413f 100644 --- a/app/engine/curate.py +++ b/app/engine/curate.py @@ -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