Commit Graph

14 Commits

Author SHA1 Message Date
Carlos Garcia
a736f3352b Add vision LLM path for receipt vendor/category identification
When RECEIPT_VISION_MODE=vision (default), uploaded receipt images are sent
directly to the vision-capable LLM (llama3.2-vision via Ollama) instead of
the OCR text excerpt.  The model can read logos, stylised fonts, and layouts
that Tesseract OCR mangles (Home Depot, HMSHost/Sergio's, etc.).

Architecture:
- amount + date: always from Tesseract regex (deterministic, never LLM)
- vendor + category: vision LLM when image available, text LLM as fallback
- Fallthrough: if vision call fails for any reason, text path is tried next
- PDF/TXT/HTML receipts: always use text path (not visual media)

Revert instantly without a rebuild:
  echo "RECEIPT_VISION_MODE=text" >> /root/odoo/odoo-ai/.env
  docker compose up -d agent-service

config.py: add receipt_vision_mode setting (default 'vision')
expenses_agent.py: _VISION_MIMETYPES, _get_vision_mode() helper,
  dual-path _parse_receipt_text (b64/mimetype params), _act() passes b64
tests: 92 passing — 4 new vision tests, 2 existing prompt tests
  pinned to text mode via _get_vision_mode patch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 01:06:55 -04:00
Carlos Garcia
db06fede5f Fix vendor mis-identification (McDonald's bias), MIA Parking amount, grayscale OCR fallback
- Remove "NeDonald's → McDonald's" from LLM vendor correction examples; the
  example was biasing the model to return McDonald's for any ambiguous receipt
  (Home Depot, Sergio's/HMSHost). Replace with neutral brand examples and add
  an explicit instruction not to substitute a brand name absent from the OCR text.
- Add `net\s*fee` to _TOTAL_RE so MIA Parking kiosk receipts ("net fee: 150.00 USD")
  are captured by Pass 1 rather than the max-scan which could pick a larger line.
- Add Step 5b grayscale fallback in receipt_parser: if all binarized PSM attempts
  yield < 20 chars, retry OCR on the pre-binarization grayscale image. Fixes
  dot-matrix and certain thermal-print fonts destroyed by the 160-threshold.
- Tests: 88 passing (test_net_fee_parking, test_vendor_prompt_does_not_contain_mcdonalds,
  test_vendor_prompt_instructs_not_to_guess_absent_brand).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 00:56:45 -04:00
Carlos Garcia
ece811cccb fix(expenses): LAYAL CAFE $2.80 bug, United Airlines rotation & date
LAYAL CAFE ($2.80 instead of $42.90):
- Add (?!\s*tax) lookahead to _TOTAL_RE so "Total Taxes $2.80" is never
  confused with the receipt total when OCR drops the "Taxes" word
- Change Pass 1 from matches[-1] to max() so the largest labeled amount
  always wins, regardless of line order in the OCR output

United Airlines (Subway/$0/wrong date):
- Add OSD-based rotation correction in receipt_parser.py: after EXIF
  transpose, ask Tesseract's orientation-detection engine (--psm 0) what
  angle to rotate; applies to receipts photographed lying sideways where
  EXIF metadata cannot help
- Add month-name date patterns (DD MON YYYY / MON DD YYYY) to
  _extract_date_from_text for airline/hotel receipts that print dates
  like "05 MAY 2026" instead of "05/07/26"

85 tests, all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 00:46:08 -04:00
Carlos Garcia
ce57d19528 fix(expenses): add 'Total Sale' and 'Net Sale' to labeled-total pattern
Gas station receipts (Costco, Shell, etc.) print "Total Sale $X.XX" — the
word "Sale" between "Total" and the amount prevented _TOTAL_RE from matching,
causing the Costco receipt to fall through to the max-scan heuristic and
return a garbled OCR value instead of the correct total.

Also add "Net Sale" and "Sale Total" variants for broader coverage.
79 tests, all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 00:36:44 -04:00
Carlos Garcia
77fab52475 fix(expenses): detect bank statements, fix default category, improve prompts
- Add _is_likely_bank_statement(): if OCR text has ≥10 lines with dollar
  amounts it is almost certainly a bank/card statement screenshot, not a
  single receipt.  Return skip=True so _act() skips it and adds a note to
  the escalations list instead of creating a $1,699 expense line.
- Fix default product selection in _act(): prefer "Meals" over whatever
  happens to be first in Odoo's expense product list ("Communication"),
  so unrecognised receipts get a sensible fallback category.
- Improve LLM category prompt: remove hardcoded product names (airline →
  Transport) that don't exist in every Odoo install; describe business
  types semantically so the model picks from the actual available list.
- Mention skipped statements in the final summary message.
- 77 tests, all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 00:25:44 -04:00
Carlos Garcia
6287b3bcef fix(expenses): improve receipt amount extraction and vendor naming
- Remove card brands (VISA/MC/Amex) from _SKIP_LINE_RE so card-terminal
  lines like "VISA USD$ 36.78" are no longer skipped
- Replace bottom-50% scan with full-text max scan (Pass 2): scans every
  line in the receipt and returns the largest dollar amount, correctly
  handling display-style receipts that show the charge at the top with
  no label (e.g. LAYAL CAFE $40.10 before the item list)
- Update vendor LLM prompt to ask the model to correct OCR garbling
  (e.g. "NeDonald's" → "McDonald's") and detect bank statements
- Add 4 new tests covering top-amount, card-terminal, max-beats-items,
  and change-exclusion scenarios (71 tests, all passing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 00:11:03 -04:00
Carlos Garcia
1536d83376 Improve OCR preprocessing and amount extraction robustness
Image preprocessing (receipt_parser.py):
- Add ImageOps.exif_transpose() — fixes portrait photos stored with EXIF
  rotation metadata (most phone photos); without this Tesseract reads a
  rotated image and produces garbage
- Upscale images < 600px wide for better character recognition
- Raise binarization threshold 140→160 for faint thermal-print receipts
- Try PSM 6 (single text block) before PSM 4, PSM 11 as fallbacks;
  PSM 6 is better suited to single-column receipt layout

Amount extraction (expenses_agent.py):
- Add Pass 2 bottom-of-receipt line scan when labeled Total: regex fails;
  reads lines bottom-to-top in the last 50% of text, skipping change/tip
  lines — handles 'T0TAL' OCR misread and amount-on-next-line layout
- Add _SKIP_LINE_RE and _ANY_DOLLAR_RE module-level patterns
- 8 new tests covering garbled total, change-skip, USD suffix, etc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:33:38 -04:00
Carlos Garcia
e6c3d08990 Fix receipt parsing quality and approval endpoint
Receipt quality: replace LLM amount/date extraction with regex.
LLM was hallucinating 2021/2022 dates and returning '198.40 USD' strings.
Amounts now use deterministic regex (Total:/Grand Total:/Amount Due:).
Dates: filename timestamp > OCR regex > today (no LLM date guessing).
LLM only asked for vendor name + product category.

Approval: fix GET /approval/pending 500 by using correct column
name 'started_at' instead of 'created_at' (which does not exist).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:02:11 -04:00
Carlos Garcia
0320591344 Remove vision OCR — use Tesseract-only pipeline for receipt parsing
The llama3.2-vision model was producing unreliable structured data
(wrong vendors, amounts, dates) making expense reports worse than
Tesseract + LLM extraction.  Removes _ocr_image_vision(), the
vision JSON fast path in _parse_receipt_text(), _match_category(),
and the vision_ocr_model config setting entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 22:32:26 -04:00
Carlos Garcia
11cc261923 fix: vision OCR receipt extraction — skip second LLM call, fix total truncation
receipt_parser: change _ocr_image_vision() to extract structured JSON
{vendor,amount,date,time,category} directly from the image instead of
transcribing raw text, so the downstream LLM extraction step is
unnecessary and the two-step error-compounding is eliminated.

expenses_agent: add _match_category() helper to map vision category
labels to expense product names via substring/fuzzy match; add fast
path in _parse_receipt_text() that detects pre-extracted vision JSON
(text starts with '{') and skips the second LLM submit call entirely.
Fix text[:2000] truncation that discarded receipt totals — now keeps
first 1500 + last 1500 chars of long receipts so the grand total at
the bottom is always included.

tests: fix stale test_act_enters_awaiting_confirmation_on_first_pass
(confirmation gate was removed); add TestMatchCategory and three new
tests for the vision JSON fast path and LLM fallthrough.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 21:49:31 -04:00
Carlos Garcia
9f38fb013c docs: label test file and add TEST_EXPENSES_AGENT.md
Adds module-level label and cross-reference to the new doc.
TEST_EXPENSES_AGENT.md documents every test group, case, and the
real-world bug each test guards against (e.g. In-N-Out OCR mismatch).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 18:35:07 -04:00
Carlos Garcia
469025b6f2 test: fix bad vendor example in pass2 similarity test
'Restaurant A' vs 'Restaurant Z' differ by 1 char so difflib scores
them at ~91% -- correctly above the 80% threshold. Use clearly
different vendors (Starbucks Coffee vs McDonalds Burger) instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 18:32:38 -04:00
Carlos Garcia
1c5f6e7ca3 test: fix _ext import (only exists in ab_ai_mail, not receipt_parser)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 18:31:30 -04:00
Carlos Garcia
6fcd830e6f test: unit tests for expenses agent dedup, plan, act, and receipt parser
- TestFindSemanticDuplicate: 18 cases covering Pass1 (amount match),
  Pass2 (OCR mismatch / high vendor similarity), time window, filenames,
  zero-amount exclusion, multi-candidate index correctness
- test_plan_*: keyword detection for confirm/skip/keep-all, mode routing
- test_act_*: confirmation gate, byte-dedup, no-employee escalation,
  confirmed creation with mocked Odoo tools
- TestParseUpload: ZIP extraction, directory skipping, filename date
  parsing, SHA256 consistency, b64 round-trip
- TestTextToHtml: escaping, newline to <br>, empty string

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 18:11:32 -04:00