Commit Graph

25 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
f1a8add84b Add OCR debug logging to diagnose receipt extraction quality
Logs per-receipt: OCR text length, first 120 chars of OCR output,
and final parsed vendor/amount/date/product_name.
This will show whether Tesseract is producing usable text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:23:13 -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
9fa391c720 fix: reduce hallucination in receipt extraction — conservative prompts + date injection
Two sources of hallucinated values in receipt parsing:

1. The LLM extraction prompt had no explicit "don't guess" constraint, so
   when Tesseract produced garbled OCR text the LLM substituted plausible-
   looking values (wrong vendor names, wrong totals) instead of returning
   safe defaults.

2. The date field asked the LLM to extract the date from the OCR text even
   when date_hint (from the filename timestamp, e.g. 20260509_180857.jpg)
   was already available — a reliable signal that was being ignored.

expenses_agent._parse_receipt_text:
- LLM path: new prompt leads with "copy values EXACTLY, do NOT guess or
  infer"; adds "if OCR looks corrupted, return safe default rather than
  a more logical value"; injects date_hint directly as an authoritative
  value when available so the LLM never needs to extract the date.
- Vision fast path: normalise "null" string for date the same way as time;
  prefer date_hint over a null date returned by the vision model.

receipt_parser._ocr_image_vision:
- Vision prompt now leads with the same "copy exactly, do not guess"
  constraint and explicitly accepts null for date/time when not clearly
  visible, matching the conservative tone of the LLM extraction prompt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 22:19:20 -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
233f461480 fix: align peer_bus signature, bot presence SQL, XML-RPC timeout
- All specialist agents: handle_peer_request(request_type, params, directive_id)
  replaces handle_peer_request(request: dict) so callers pass structured args
- ab_ai_bot: force-write bus_presence.status via SQL so Odoo 18 WebSocket presence
  shows the correct colour immediately (ORM compute does not trigger on last_poll writes)
- odoo_client: wrap XML-RPC executor calls in asyncio.wait_for to enforce timeout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 23:02:51 -04:00
Carlos Garcia
0bd1810405 fix: create expense report immediately — remove broken confirmation gate
The old flow required a "confirm" reply after showing a parsed-receipt
table, but that follow-up dispatch call carries no receipts (they only
exist in the /upload context). The confirmation gate was architecturally
broken: the second turn would always create nothing.

Fix: create the expense sheet immediately when receipts are present.
Byte-exact and semantic duplicates are auto-skipped; the count of
skipped items is reported in the success message. The report is always
created in Odoo as a draft so users can review amounts and submit
manually via Odoo > Expenses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:58:47 -04:00
Carlos Garcia
af1d27be89 feat: pre-creation confirmation step with inline duplicate warnings
Before writing any expense records the bot now posts a numbered table
of parsed vendor/amount/date for every receipt, with duplicate entries
flagged inline. User replies 'confirm' (skips dups) or 'confirm, keep
all'. This catches OCR amount misreads before they land in Odoo.

Also removes the separate awaiting_dup_approval step; duplicate review
is now part of the single confirmation table.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 16:54:25 -04:00
Carlos Garcia
12576ead1b feat: two-pass dedup catches same-vendor OCR amount misreads
Pass 1 unchanged: same date + amount within 0.05 + vendor similarity 60%.
Pass 2 (new): same vendor (>= 80% similarity) + same date, regardless
of amount, to catch receipts where OCR misread the total.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 16:48:51 -04:00
Carlos Garcia
774c0cc062 fix: tighten receipt amount extraction prompt to reduce OCR misreads
Replaced 'pick the largest one' guidance with 'bottom-most total' and
'return 0 if no clear total found' to avoid picking line items or tips.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 16:47:48 -04:00
Carlos Garcia
9e3fe974dc Fix dup approval flow: preserve raw message, force expenses routing, fix HTML rendering
- master_agent: thread raw user message into extra_context and peer_data so
  expenses_agent can check it directly without relying on LLM intent_summary
- master_agent: when receipts are in extra_context always route to expenses_agent,
  so replies like 'skip duplicates' still trigger expense processing
- expenses_agent: _plan() checks peer_data raw_message alongside task so
  skip/keep keywords are detected even when master rewrites the intent
- ab_ai_mail: wrap clarification message HTML in Markup() so Odoo does not
  re-escape the tags; use <br> instead of <br/>
- ab_ai_mail: convert agent plain-text replies newlines to <br> for proper
  line-break rendering in Discuss

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:55:46 -04:00
Carlos Garcia
462f63d11d Add duplicate approval flow with time-based dedup
- expenses_agent: extract transaction time (HH:MM) from OCR receipt text
- expenses_agent: _find_semantic_duplicate uses time to rule out false positives (>30 min apart = different receipts)
- expenses_agent: pause when duplicates found, set mode=awaiting_dup_approval, ask user before creating sheet
- expenses_agent: _report formats approval message listing each dup pair with vendor/amount/date/times/filenames
- ab_ai_mail: _find_pending_attachments recognises dup-approval bot message so ZIP re-attaches on user reply

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 02:07:37 -04:00
Carlos Garcia
f90a2ee863 feat: semantic deduplication of multiple photos of same receipt
After parsing all receipts, identify photos that are different shots of
the same physical receipt by comparing amount + date + vendor similarity
(difflib ratio >= 0.6). When a duplicate is found, keep whichever photo
produced the most OCR text (clearest shot) and report the skipped ones.

Zero-amount receipts (OCR failed entirely) are excluded from semantic
dedup to avoid false positives.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:56:30 -04:00
Carlos Garcia
c2d1078d79 fix: improve OCR accuracy for rotated/sideways receipt photos
- Dockerfile: add tesseract-ocr-osd for orientation detection data
- receipt_parser: resize large phone photos to 1800px, convert to
  grayscale, sharpen before OCR; use psm 1 (auto + OSD) so rotated
  receipts are correctly oriented before text extraction
- expenses_agent: tighten amount extraction prompt to pick the FINAL
  total, not subtotal or tax line, reducing misreads like 42.90->409.00

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:51:29 -04:00
Carlos Garcia
8a9d772b8e fix: increase timeout and parallelize receipt processing
- ab_ai_bot: raise requests.post timeout 120s -> 600s so long OCR+LLM
  runs don't silently drop the reply in Discuss
- upload: run parse_upload in ThreadPoolExecutor so tesseract OCR
  doesn't block the FastAPI event loop
- expenses_agent: parse all receipts concurrently with asyncio.gather
  (Ollama semaphore caps parallelism at 2); reduces 13-receipt LLM
  time from ~39s sequential to ~20s parallel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:50:12 -04:00
Carlos Garcia
ef6dad5a81 feat: OCR via tesseract, dedup, category selection for expense receipts
- Dockerfile: install tesseract-ocr so Pillow+pytesseract can OCR receipt images
- operational_store: JSON-serialize raw_data before passing to asyncpg JSONB
- receipt_parser: add SHA256 hash + date extracted from filename timestamps
- expenses_agent: deduplicate receipts by hash before creating expense records
- expenses_agent: fetch all expensable Odoo products, pass list to LLM for
  category selection (Meals, Flights, etc.) per receipt
- expenses_agent: pass date_hint from filename (e.g. 20260509_180857.jpg -> 2026-05-09)
  as fallback when OCR text is unavailable
- expenses_tools: add get_expense_products() to fetch all expensable products

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:40:32 -04:00
Carlos Garcia
4b7223a139 feat: file upload + expense report creation from Discuss attachments
- Discuss bot now reads ir.attachment from incoming messages; file-only
  messages no longer silently dropped
- ZIP files are described (contents listed) and bot asks clarifying
  question before acting; user's follow-up reply looks back for pending
  attachments so files don't need to be re-uploaded
- receipt_parser: extracts text from ZIP (recursive), JPG/PNG/etc (OCR),
  PDF (pdfplumber), HTML, TXT
- expenses_agent: full rewrite fixing broken method signatures; adds
  create_expense_sheet / create_expense / attach_receipt flow driven by
  LLM receipt parsing (Ollama, HIPAA-locked)
- master_agent: extra_context threads receipts + user_id into directives
- FastAPI /upload multipart endpoint; registered in main.py
- Odoo /ai/upload controller proxies files to agent service
- ab_ai_bot: dispatch_message_with_files() for multipart uploads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:02:24 -04:00
ActiveBlue Build
fe47f950e4 feat(agents): add 7 specialist agents with tools and system prompts
Agents (all following 6-step contract: _plan/_gather/_reason/_act/_report):
- AccountingAgent: trial balance, chart of accounts, tax summary (HIPAA-locked)
- CrmAgent: pipeline summary, lead/opportunity management, won/lost analysis
- SalesAgent: sales orders, quotations, revenue by rep, expired quote detection
- ProjectAgent: task tracking, blocked/overdue detection, timesheet logging
- ElearningAgent: course completion, low-engagement flagging, next-course suggestion
- ExpensesAgent: expense sheets, pending approvals, policy violations (HIPAA-locked)
- EmployeesAgent: headcount, contracts, leaves, attendance, expired contract sweep (HIPAA-locked)

Tools (one file per domain):
- accounting_tools.py, crm_tools.py, sales_tools.py, project_tools.py
- elearning_tools.py, expenses_tools.py, employees_tools.py

System prompts: each agent has a domain-specific system.txt with rules and output format

All agents implement handle_peer_request() and sweep() for proactive monitoring
HIPAA-locked agents (accounting, expenses, employees) enforced via LLMRouter

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