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>
102 lines
3.1 KiB
Python
102 lines
3.1 KiB
Python
from __future__ import annotations
|
|
import os
|
|
from functools import lru_cache
|
|
from pydantic_settings import BaseSettings
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
# Odoo
|
|
odoo_url: str = 'http://localhost:8069'
|
|
odoo_db: str = 'odoo'
|
|
odoo_user: str = '__system__'
|
|
odoo_api_key: str = ''
|
|
|
|
# Ollama
|
|
ollama_url: str = 'http://localhost:11434'
|
|
ollama_model: str = 'activeblue-chat'
|
|
ollama_timeout: int = 300
|
|
ollama_max_concurrent: int = 2
|
|
|
|
# Anthropic / Claude
|
|
anthropic_api_key: str = ''
|
|
claude_model: str = 'claude-sonnet-4-6'
|
|
claude_timeout: int = 120
|
|
claude_max_concurrent: int = 2
|
|
|
|
# Privacy
|
|
llm_privacy_mode: str = 'local' # local | hybrid | cloud
|
|
|
|
# Per-agent backend overrides (env: AGENT_BACKEND_FINANCE=claude)
|
|
agent_backend_finance: str = ''
|
|
agent_backend_accounting: str = ''
|
|
agent_backend_crm: str = ''
|
|
agent_backend_sales: str = ''
|
|
agent_backend_project: str = ''
|
|
agent_backend_elearning: str = ''
|
|
agent_backend_expenses: str = ''
|
|
agent_backend_employees: str = ''
|
|
|
|
# Service
|
|
agent_service_port: int = 8001
|
|
webhook_secret: str = ''
|
|
allowed_callback_ip: str = ''
|
|
|
|
# Postgres
|
|
postgres_host: str = 'localhost'
|
|
postgres_port: int = 5432
|
|
postgres_db: str = 'activeblue_ai'
|
|
postgres_user: str = 'activeblue'
|
|
postgres_password: str = ''
|
|
postgres_min_connections: int = 2
|
|
postgres_max_connections: int = 10
|
|
|
|
# Receipt OCR / vision
|
|
# 'vision' — use vision LLM for vendor+category when an image is uploaded (default)
|
|
# 'text' — use Tesseract OCR text only (set RECEIPT_VISION_MODE=text to revert)
|
|
receipt_vision_mode: str = 'vision'
|
|
|
|
# Rate limiting
|
|
dispatch_rate_limit_per_user: int = 30 # requests per minute
|
|
directive_timeout_minutes: int = 10
|
|
|
|
# Logging
|
|
log_level: str = 'INFO'
|
|
log_format: str = 'json'
|
|
loki_url: str = ''
|
|
|
|
class Config:
|
|
env_file = '.env'
|
|
env_file_encoding = 'utf-8'
|
|
|
|
@property
|
|
def postgres_dsn(self) -> str:
|
|
return (
|
|
f'postgresql+asyncpg://{self.postgres_user}:{self.postgres_password}'
|
|
f'@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}'
|
|
)
|
|
|
|
@property
|
|
def postgres_asyncpg_dsn(self) -> str:
|
|
return (
|
|
f'asyncpg://{self.postgres_user}:{self.postgres_password}'
|
|
f'@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}'
|
|
)
|
|
|
|
def agent_backend_override(self, agent_name: str) -> str:
|
|
mapping = {
|
|
'finance_agent': self.agent_backend_finance,
|
|
'accounting_agent': self.agent_backend_accounting,
|
|
'crm_agent': self.agent_backend_crm,
|
|
'sales_agent': self.agent_backend_sales,
|
|
'project_agent': self.agent_backend_project,
|
|
'elearning_agent': self.agent_backend_elearning,
|
|
'expenses_agent': self.agent_backend_expenses,
|
|
'employees_agent': self.agent_backend_employees,
|
|
}
|
|
return mapping.get(agent_name, '')
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def get_settings() -> Settings:
|
|
return Settings()
|