File size: 1,600 Bytes
c55ab5e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
"""A structured calculation result.

Every engine function returns one of these instead of a bare number. It carries:

* ``amount``      — the headline figure (Decimal, rounded);
* ``label``       — what the figure is;
* ``breakdown``   — the step-by-step math, so the model can *explain* without doing
                    arithmetic;
* ``source`` / ``effective_year`` — provenance, so the UI can say "per the 2024
                    SAT tariff" and never present a number without its basis.

This is the contract between the deterministic engine and the LLM: the model reads
``breakdown`` and ``source`` and turns them into prose. It never recomputes.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from decimal import Decimal
from typing import List, Optional, Tuple


@dataclass
class CalcResult:
    amount: Decimal
    label: str
    breakdown: List[Tuple[str, Decimal]] = field(default_factory=list)
    source: Optional[str] = None
    effective_year: Optional[int] = None
    notes: List[str] = field(default_factory=list)

    def explain(self) -> str:
        """A plain-text rendering of the math — handy for logs, tests and the UI."""
        lines = [f"{self.label}: {self.amount}"]
        for desc, val in self.breakdown:
            lines.append(f"  · {desc}: {val}")
        if self.source:
            year = f" ({self.effective_year})" if self.effective_year else ""
            lines.append(f"  source: {self.source}{year}")
        for note in self.notes:
            lines.append(f"  note: {note}")
        return "\n".join(lines)