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>
This commit is contained in:
@@ -423,88 +423,12 @@ async def test_act_no_employee_returns_empty_and_escalates():
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _match_category
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestMatchCategory:
|
||||
PRODUCTS = [
|
||||
{'id': 1, 'name': 'Meals'},
|
||||
{'id': 2, 'name': 'Fuel'},
|
||||
{'id': 3, 'name': 'Hotel'},
|
||||
{'id': 4, 'name': 'Office Supplies'},
|
||||
{'id': 5, 'name': 'Transport'},
|
||||
{'id': 6, 'name': 'Other'},
|
||||
]
|
||||
|
||||
def test_exact_match(self):
|
||||
assert ExpensesAgent._match_category('Meals', self.PRODUCTS) == 'Meals'
|
||||
|
||||
def test_case_insensitive(self):
|
||||
assert ExpensesAgent._match_category('meals', self.PRODUCTS) == 'Meals'
|
||||
assert ExpensesAgent._match_category('FUEL', self.PRODUCTS) == 'Fuel'
|
||||
|
||||
def test_substring_match(self):
|
||||
# 'office' is a substring of 'Office Supplies'
|
||||
assert ExpensesAgent._match_category('office', self.PRODUCTS) == 'Office Supplies'
|
||||
|
||||
def test_fuzzy_match(self):
|
||||
# 'transport' is close to 'Transport'
|
||||
assert ExpensesAgent._match_category('transport', self.PRODUCTS) == 'Transport'
|
||||
|
||||
def test_no_match_returns_empty(self):
|
||||
assert ExpensesAgent._match_category('zxqwerty', self.PRODUCTS) == ''
|
||||
|
||||
def test_empty_category(self):
|
||||
assert ExpensesAgent._match_category('', self.PRODUCTS) == ''
|
||||
|
||||
def test_empty_products(self):
|
||||
assert ExpensesAgent._match_category('meals', []) == ''
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _parse_receipt_text — vision JSON fast path
|
||||
# _parse_receipt_text — LLM extraction path
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_vision_json_fast_path():
|
||||
"""When text is pre-extracted JSON from vision model, skip LLM call."""
|
||||
agent = _make_agent()
|
||||
agent._llm.submit = AsyncMock() # should NOT be called
|
||||
|
||||
vision_json = ('{"vendor":"McDonald\'s","amount":12.50,'
|
||||
'"date":"2026-05-09","time":"13:30","category":"meals"}')
|
||||
products = [{'id': 1, 'name': 'Meals'}, {'id': 2, 'name': 'Fuel'}]
|
||||
|
||||
result = await agent._parse_receipt_text(vision_json, 'receipt.jpg',
|
||||
expense_products=products)
|
||||
|
||||
assert result['vendor'] == "McDonald's"
|
||||
assert result['amount'] == 12.50
|
||||
assert result['date'] == '2026-05-09'
|
||||
assert result['time'] == '13:30'
|
||||
assert result['product_name'] == 'Meals'
|
||||
agent._llm.submit.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_vision_json_null_time():
|
||||
"""Vision model may return the string 'null' for time — normalise to None."""
|
||||
agent = _make_agent()
|
||||
agent._llm.submit = AsyncMock()
|
||||
|
||||
vision_json = '{"vendor":"Shell","amount":45.00,"date":"2026-05-09","time":"null","category":"fuel"}'
|
||||
products = [{'id': 1, 'name': 'Meals'}, {'id': 2, 'name': 'Fuel'}]
|
||||
|
||||
result = await agent._parse_receipt_text(vision_json, 'shell.jpg',
|
||||
expense_products=products)
|
||||
assert result['time'] is None
|
||||
assert result['product_name'] == 'Fuel'
|
||||
agent._llm.submit.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_non_json_text_falls_through_to_llm():
|
||||
"""Plain OCR text (not JSON) should go through the LLM extraction path."""
|
||||
async def test_parse_plain_ocr_text_uses_llm():
|
||||
"""Plain OCR text should go through the LLM extraction path."""
|
||||
agent = _make_agent()
|
||||
llm_resp = MagicMock()
|
||||
llm_resp.content = '{"vendor":"Acme","amount":9.99,"date":"2026-05-09","time":null,"product_name":"Meals"}'
|
||||
|
||||
Reference in New Issue
Block a user