Files
Odoo-Modules/fusion_accounting_assets/services/useful_life_predictor.py
2026-04-19 16:50:01 -04:00

95 lines
3.6 KiB
Python

"""AI-suggested useful life from invoice context.
Wraps useful_life_prompt + an LLMProvider. Returns a dict per the prompt's
output contract. Templated fallback when no provider configured.
"""
import json
import logging
import re
_logger = logging.getLogger(__name__)
# Templated fallback rules: (regex, years, method, rationale)
FALLBACK_RULES = [
(r'\b(computer|laptop|monitor|server|workstation)\b', 4, 'straight_line', 'Computer hardware'),
(r'\b(furniture|desk|chair|cabinet)\b', 7, 'straight_line', 'Furniture'),
(r'\b(vehicle|truck|car|van)\b', 5, 'declining_balance', 'Vehicle (CRA Class 10)'),
(r'\b(building|warehouse)\b', 30, 'straight_line', 'Building'),
(r'\b(software|license)\b', 4, 'straight_line', 'Software license'),
(r'\b(equipment|machinery|machine)\b', 10, 'straight_line', 'Manufacturing equipment'),
(r'\b(leasehold improvement)\b', 5, 'straight_line', 'Leasehold improvements'),
]
FALLBACK_DEFAULT = (5, 'straight_line', 'Generic fixed asset (default)')
def predict_useful_life(env, *, description: str, amount: float = None,
partner_name: str = None, provider=None) -> dict:
"""Suggest useful life + method via LLM, with templated fallback."""
if provider is None:
provider = _get_provider(env)
if provider is None:
return _templated_fallback(description)
try:
from .useful_life_prompt import build_prompt
system, user = build_prompt(
description=description, amount=amount, partner_name=partner_name,
)
response = provider.complete(
system=system,
messages=[{'role': 'user', 'content': user}],
max_tokens=400, temperature=0.1,
)
content = response.get('content') if isinstance(response, dict) else response
parsed = json.loads(content)
for key in ('useful_life_years', 'depreciation_method', 'rationale'):
if key not in parsed:
raise ValueError(f"Missing key: {key}")
parsed.setdefault('confidence', 0.7)
return parsed
except Exception as e:
_logger.warning("Useful life LLM prediction failed (%s); falling back", e)
return _templated_fallback(description)
def _templated_fallback(description: str) -> dict:
"""Pattern-match keyword rules. Always returns a usable dict."""
desc_lower = description.lower() if description else ''
for pattern, years, method, rationale in FALLBACK_RULES:
if re.search(pattern, desc_lower):
return {
'useful_life_years': years,
'depreciation_method': method,
'rationale': rationale,
'confidence': 0.5,
}
years, method, rationale = FALLBACK_DEFAULT
return {
'useful_life_years': years,
'depreciation_method': method,
'rationale': rationale,
'confidence': 0.3,
}
def _get_provider(env):
"""Look up provider for 'asset_useful_life' feature."""
param = env['ir.config_parameter'].sudo()
name = param.get_param('fusion_accounting.provider.asset_useful_life')
if not name:
name = param.get_param('fusion_accounting.provider.default')
if not name:
return None
try:
from odoo.addons.fusion_accounting_ai.services.adapters.openai_adapter import OpenAIAdapter
from odoo.addons.fusion_accounting_ai.services.adapters.claude import ClaudeAdapter
except ImportError:
return None
if name.startswith('openai'):
return OpenAIAdapter(env)
elif name.startswith('claude'):
return ClaudeAdapter(env)
return None