"""AI-generated follow-up text with templated fallback.""" import json import logging _logger = logging.getLogger(__name__) TEMPLATES = { 'gentle': { 'subject': 'Friendly reminder: invoice payment', 'body': 'Dear {partner_name},\n\nThis is a friendly reminder that you have ' '{currency_code} {total_overdue:,.2f} outstanding on invoices that ' 'are now {longest_overdue_days} days past due. We understand things ' 'happen — please let us know if there is anything we can do to help ' 'resolve this.\n\nBest regards.', }, 'firm': { 'subject': 'Outstanding invoices — action required', 'body': 'Dear {partner_name},\n\nOur records show {currency_code} ' '{total_overdue:,.2f} outstanding on {invoice_count} invoice(s), ' 'with the longest now {longest_overdue_days} days overdue. We ' 'request immediate payment to avoid further action.\n\nRegards.', }, 'legal': { 'subject': 'FINAL NOTICE — outstanding balance', 'body': 'Dear {partner_name},\n\nDespite previous reminders, ' '{currency_code} {total_overdue:,.2f} remains outstanding on your ' 'account, with the longest invoice {longest_overdue_days} days ' 'overdue. If full payment is not received within 7 days, we will ' 'be forced to refer this matter for legal collection.\n\n' 'Regards.', }, } def generate_followup_text(env, *, partner_name: str, total_overdue: float, currency_code: str, longest_overdue_days: int, tone: str, invoice_count: int = 0, last_payment_date: str = None, risk_drivers: list[str] = None, provider=None) -> dict: """Generate follow-up text via LLM, with templated fallback. Returns: {subject, body, tone_used, key_points}""" if provider is None: provider = _get_provider(env) if provider is None: return _templated_fallback( partner_name=partner_name, total_overdue=total_overdue, currency_code=currency_code, longest_overdue_days=longest_overdue_days, tone=tone, invoice_count=invoice_count, ) try: from .followup_text_prompt import build_prompt system, user = build_prompt( partner_name=partner_name, total_overdue=total_overdue, currency_code=currency_code, longest_overdue_days=longest_overdue_days, tone=tone, invoice_count=invoice_count, last_payment_date=last_payment_date, risk_drivers=risk_drivers, ) response = provider.complete( system=system, messages=[{'role': 'user', 'content': user}], max_tokens=800, temperature=0.3, ) content = response.get('content') if isinstance(response, dict) else response parsed = json.loads(content) for key in ('subject', 'body', 'tone_used'): if key not in parsed: raise ValueError(f"Missing key: {key}") parsed.setdefault('key_points', []) return parsed except Exception as e: _logger.warning("Follow-up text LLM generation failed (%s); falling back", e) return _templated_fallback( partner_name=partner_name, total_overdue=total_overdue, currency_code=currency_code, longest_overdue_days=longest_overdue_days, tone=tone, invoice_count=invoice_count, ) def _templated_fallback(*, partner_name, total_overdue, currency_code, longest_overdue_days, tone, invoice_count) -> dict: template = TEMPLATES.get(tone, TEMPLATES['gentle']) return { 'subject': template['subject'], 'body': template['body'].format( partner_name=partner_name, total_overdue=total_overdue, currency_code=currency_code, longest_overdue_days=longest_overdue_days, invoice_count=invoice_count or 0, ), 'tone_used': tone, 'key_points': [ f"${total_overdue:,.2f} outstanding", f"{longest_overdue_days} days overdue", ], } def _get_provider(env): """Look up provider for 'followup_text' feature.""" param = env['ir.config_parameter'].sudo() name = param.get_param('fusion_accounting.provider.followup_text') 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