"""unit_convert — area / length / price-per-unit conversions incl. Québec units (arpent).""" from __future__ import annotations from typing import Any from pydantic import BaseModel, Field from app.llm.schemas import ToolResult from app.tools.coerce import num from app.tools.registry import ToolContext, registry # base: m² for areas, m for lengths AREA = { "m2": 1.0, "m²": 1.0, "metre carre": 1.0, "mètre carré": 1.0, "pi2": 0.09290304, "pi²": 0.09290304, "sqft": 0.09290304, "ft2": 0.09290304, "pied carré": 0.09290304, "ha": 10000.0, "hectare": 10000.0, "acre": 4046.8564224, "ac": 4046.8564224, "arpent2": 3418.894, "arpent carré": 3418.894, "arpent²": 3418.894, "arpent": 3418.894, "km2": 1e6, "km²": 1e6, "vg2": 0.83612736, "verge carrée": 0.83612736, "yd2": 0.83612736, } LENGTH = { "m": 1.0, "metre": 1.0, "mètre": 1.0, "cm": 0.01, "mm": 0.001, "km": 1000.0, "pi": 0.3048, "ft": 0.3048, "pied": 0.3048, "po": 0.0254, "in": 0.0254, "pouce": 0.0254, "vg": 0.9144, "yd": 0.9144, "verge": 0.9144, "mi": 1609.344, "mille": 1609.344, "arpent_lin": 58.47, "arpent linéaire": 58.47, "perche": 5.847, } ALIASES = {"pieds carrés": "pi2", "pi.ca.": "pi2", "pc": "pi2", "sq ft": "pi2", "metres carres": "m2", "mètres carrés": "m2", "hectares": "ha", "acres": "acre", "arpents": "arpent", "pieds": "pi", "pouces": "po", "metres": "m", "mètres": "m"} class Args(BaseModel): value: float | str from_unit: str = Field(..., description="ex. m2, pi2, acre, ha, arpent, m, pi, po, $/m2, $/pi2") to_unit: str def _norm(u: str) -> str: u = u.strip().lower().replace(" ", " ") return ALIASES.get(u, u) def convert(value: float, from_unit: str, to_unit: str) -> dict[str, Any]: f, t = _norm(from_unit), _norm(to_unit) per = f.startswith("$/") and t.startswith("$/") if per: f, t = f[2:], t[2:] for table, kind in ((AREA, "superficie"), (LENGTH, "longueur")): if f in table and t in table: factor = table[f] / table[t] if per: # $/from → $/to : inverse relationship factor = table[t] / table[f] return {"result": value * factor, "factor": factor, "kind": f"prix par {kind}", "formula": f"{value:g} $/{f} × ({table[t]:g} / {table[f]:g}) = {value * factor:,.4f} $/{t}"} return {"result": value * factor, "factor": factor, "kind": kind, "formula": f"{value:g} {f} × {factor:,.6g} = {value * factor:,.4f} {t}"} raise ValueError(f"unités incompatibles ou inconnues : {from_unit} → {to_unit}") async def run(args: dict[str, Any], ctx: ToolContext) -> ToolResult: v = num(args["value"], None) if v is None: return ToolResult(content="Valeur numérique invalide.", error=True) try: out = convert(v, args["from_unit"], args["to_unit"]) except ValueError as exc: return ToolResult(content=f"{exc}. Unités : {', '.join(sorted(set(AREA) | set(LENGTH)))} ; " "préfixe $/ pour les prix unitaires.", error=True) return ToolResult(content=f"{out['formula']} (facteur {out['factor']:.6g}, {out['kind']}). " "Rappels : 1 m² = 10,7639 pi² ; 1 pi = 0,3048 m ; 1 arpent carré ≈ 3 418,9 m² ; 1 acre = 4 046,86 m².", payload={"value": v, "from": args["from_unit"], "to": args["to_unit"], **out}, meta={"summary": out["formula"]}) registry.register("unit_convert", run, Args)