PocketAccountant: custom ledger UI + deterministic agent (engine, ledger, retrieval, classifier)
c55ab5e verified | """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 | |
| 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) | |