Skip to content

fix(extraction): guard empty provider choices in PDF extraction path (#8)#20

Open
aircode610 wants to merge 4 commits into
mainfrom
fix/issue-8-extraction-empty-choices
Open

fix(extraction): guard empty provider choices in PDF extraction path (#8)#20
aircode610 wants to merge 4 commits into
mainfrom
fix/issue-8-extraction-empty-choices

Conversation

@aircode610

Copy link
Copy Markdown
Owner

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 provider choices list, but the primary PDF extraction path in backend/app/services/extraction.py still blind-indexed response.choices[0].

A content-filtered or empty vision completion for a blank/unreadable scanned PDF can legitimately return choices == []. Indexing choices[0] then raised a raw IndexError that surfaced to the client as a 500 — the exact crash class issue #8 set out to eliminate — instead of the typed ExtractionError that maps to a friendly EXTRACTION_FAILED response.

Root cause

Three call sites blind-indexed the provider response:

Function Old code
extract_from_letter_file response.choices[0].message.tool_calls
generate_reply_text response.choices[0].message.content
generate_checklist response.choices[0].message.content

Fix

Guard all three sites: treat a missing/empty choices list (or null message) as "no content".

  • extract_from_letter_file → falls through to the existing typed ExtractionError ("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:

  • New regression test test_extract_from_letter_file_empty_choices — verified it reproduces IndexError: list index out of range at extraction.py:284 without the fix, and passes as a typed ExtractionError with it.
  • Manually exercised the other two patched sites with an empty-choices response: generate_reply_text'', generate_checklist[], no IndexError.
  • Full backend suite: 22 passed (baseline 21 + the new test).

Scoped to 2 files (extraction guard + regression test); no unrelated churn.

Refs #8

aircode610 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant