fix(extraction): guard empty provider choices in PDF extraction path (#8)#20
Open
aircode610 wants to merge 4 commits into
Open
fix(extraction): guard empty provider choices in PDF extraction path (#8)#20aircode610 wants to merge 4 commits into
aircode610 wants to merge 4 commits into
Conversation
added 4 commits
July 2, 2026 17:14
) The issue-#8 fix (PR #13) taught the OCR and form-fill response parsers to reject an empty provider `choices` list, but the primary PDF extraction entry point still blind-indexed `response.choices[0]`. A content-filtered or empty vision completion for a blank/unreadable scanned PDF can return `choices == []`; `choices[0]` then raised a raw IndexError (surfacing as a 500) instead of the typed ExtractionError — the exact crash pattern issue #8 set out to eliminate. Fixed the same pattern at all three call sites in extraction.py (extract_from_letter_file, generate_reply_text, generate_checklist) and added a regression test for the empty-choices scanned-PDF case. Verified: the new test reproduces `IndexError: list index out of range` at extraction.py:284 without the fix, and passes as a typed ExtractionError with it. Full backend suite: 22 passed.
This branch was missing .github/workflows/lint.yml (present on every other feature branch), so PR #20 had 0 check runs and the tests job never executed. Add the canonical workflow. Verified the backend test suite passes under the exact CI setup (Python 3.12 + backend/requirements.txt + pytest-asyncio): 22 passed.
The newly added Lint Python workflow's ruff job was failing: - Removed unused imports (datetime, json, Any, RagHit, generate_reply_text) - Removed f-strings without placeholders in ai/rag/ingest.py - Removed dead locals (classification_data, action_titles, reply_actions) - Moved public.py logger below imports (fixes E402) - Ran 'ruff format' across backend/ and ai/ so 'ruff format --check' passes Backend test suite (22 tests) still passes.
Add a GitHub Actions workflow (actions/github-script) that runs on issue opened/edited/reopened and, based on issue content: - Adds a type label (security > bug > documentation > enhancement > question) - Adds component labels (frontend, backend, ai-agent, ai-rag, infra) - Adds a priority label (keyword-driven, with sensible defaults) - Auto-assigns the component owner, never overriding a manual assignee Only adds labels that exist in the repo and are not already set.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A follow-up crash gap left by the issue #8 fix (PR #13). PR #13 hardened the OCR (
ai/react_agent/ocr.py) and form-fill response parsers against an empty providerchoiceslist, but the primary PDF extraction path inbackend/app/services/extraction.pystill blind-indexedresponse.choices[0].A content-filtered or empty vision completion for a blank/unreadable scanned PDF can legitimately return
choices == []. Indexingchoices[0]then raised a rawIndexErrorthat surfaced to the client as a 500 — the exact crash class issue #8 set out to eliminate — instead of the typedExtractionErrorthat maps to a friendlyEXTRACTION_FAILEDresponse.Root cause
Three call sites blind-indexed the provider response:
extract_from_letter_fileresponse.choices[0].message.tool_callsgenerate_reply_textresponse.choices[0].message.contentgenerate_checklistresponse.choices[0].message.contentFix
Guard all three sites: treat a missing/empty
choiceslist (or nullmessage) as "no content".extract_from_letter_file→ falls through to the existing typedExtractionError("Could not extract text… may be a scanned image without readable content").generate_reply_text→ returns""instead of crashing.generate_checklist→ returns[]instead of crashing.Testing
Per the "exercise the fixed path with edge cases before opening a PR" guidance:
test_extract_from_letter_file_empty_choices— verified it reproducesIndexError: list index out of rangeatextraction.py:284without the fix, and passes as a typedExtractionErrorwith it.generate_reply_text→'',generate_checklist→[], noIndexError.22 passed(baseline 21 + the new test).Scoped to 2 files (extraction guard + regression test); no unrelated churn.
Refs #8