Files
odoo-ai/agent_service/memory/operational_store.py
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

39 lines
1.6 KiB
Python

from __future__ import annotations
import json
import logging
from datetime import datetime, timedelta
logger = logging.getLogger(__name__)
class OperationalStore:
def __init__(self, pool):
self._pool = pool
async def store(self, scope, summary, raw_data=None, ttl_days=90, source_directive_id=None):
expires_at = datetime.utcnow() + timedelta(days=ttl_days)
# asyncpg JSONB column expects a JSON string, not a Python dict
raw_data_json = json.dumps(raw_data) if raw_data is not None else None
async with self._pool.acquire(timeout=10) as conn:
await conn.execute(
"""INSERT INTO ab_operational_memory
(scope, summary, raw_data, source_directive_id, expires_at)
VALUES ($1, $2, $3, $4, $5)""",
scope, summary, raw_data_json, source_directive_id, expires_at)
async def get_recent(self, scope, limit=10):
async with self._pool.acquire(timeout=10) as conn:
rows = await conn.fetch(
"""SELECT id, scope, summary, raw_data, created_at
FROM ab_operational_memory
WHERE scope = $1 AND (expires_at IS NULL OR expires_at > NOW())
ORDER BY created_at DESC LIMIT $2""",
scope, limit)
return [dict(r) for r in rows]
async def prune_expired(self):
async with self._pool.acquire(timeout=10) as conn:
result = await conn.execute(
'DELETE FROM ab_operational_memory WHERE expires_at IS NOT NULL AND expires_at < NOW()')
logger.info('OperationalStore.prune_expired: %s', result)