Python 64.6%
TypeScript 33.7%
CSS 0.8%
1"""unit_convert — area / length / price-per-unit conversions incl. Québec units (arpent)."""23from __future__ import annotations45from typing import Any67from pydantic import BaseModel, Field89from app.llm.schemas import ToolResult10from app.tools.coerce import num11from app.tools.registry import ToolContext, registry1213# base: m² for areas, m for lengths14AREA = {15 "m2": 1.0, "m²": 1.0, "metre carre": 1.0, "mètre carré": 1.0,16 "pi2": 0.09290304, "pi²": 0.09290304, "sqft": 0.09290304, "ft2": 0.09290304, "pied carré": 0.09290304,17 "ha": 10000.0, "hectare": 10000.0,18 "acre": 4046.8564224, "ac": 4046.8564224,19 "arpent2": 3418.894, "arpent carré": 3418.894, "arpent²": 3418.894, "arpent": 3418.894,20 "km2": 1e6, "km²": 1e6,21 "vg2": 0.83612736, "verge carrée": 0.83612736, "yd2": 0.83612736,22}23LENGTH = {24 "m": 1.0, "metre": 1.0, "mètre": 1.0, "cm": 0.01, "mm": 0.001, "km": 1000.0,25 "pi": 0.3048, "ft": 0.3048, "pied": 0.3048, "po": 0.0254, "in": 0.0254, "pouce": 0.0254,26 "vg": 0.9144, "yd": 0.9144, "verge": 0.9144, "mi": 1609.344, "mille": 1609.344,27 "arpent_lin": 58.47, "arpent linéaire": 58.47, "perche": 5.847,28}29ALIASES = {"pieds carrés": "pi2", "pi.ca.": "pi2", "pc": "pi2", "sq ft": "pi2", "metres carres": "m2",30 "mètres carrés": "m2", "hectares": "ha", "acres": "acre", "arpents": "arpent", "pieds": "pi",31 "pouces": "po", "metres": "m", "mètres": "m"}323334class Args(BaseModel):35 value: float | str36 from_unit: str = Field(..., description="ex. m2, pi2, acre, ha, arpent, m, pi, po, $/m2, $/pi2")37 to_unit: str383940def _norm(u: str) -> str:41 u = u.strip().lower().replace(" ", " ")42 return ALIASES.get(u, u)434445def convert(value: float, from_unit: str, to_unit: str) -> dict[str, Any]:46 f, t = _norm(from_unit), _norm(to_unit)47 per = f.startswith("$/") and t.startswith("$/")48 if per:49 f, t = f[2:], t[2:]50 for table, kind in ((AREA, "superficie"), (LENGTH, "longueur")):51 if f in table and t in table:52 factor = table[f] / table[t]53 if per: # $/from → $/to : inverse relationship54 factor = table[t] / table[f]55 return {"result": value * factor, "factor": factor, "kind": f"prix par {kind}",56 "formula": f"{value:g} $/{f} × ({table[t]:g} / {table[f]:g}) = {value * factor:,.4f} $/{t}"}57 return {"result": value * factor, "factor": factor, "kind": kind,58 "formula": f"{value:g} {f} × {factor:,.6g} = {value * factor:,.4f} {t}"}59 raise ValueError(f"unités incompatibles ou inconnues : {from_unit} → {to_unit}")606162async def run(args: dict[str, Any], ctx: ToolContext) -> ToolResult:63 v = num(args["value"], None)64 if v is None:65 return ToolResult(content="Valeur numérique invalide.", error=True)66 try:67 out = convert(v, args["from_unit"], args["to_unit"])68 except ValueError as exc:69 return ToolResult(content=f"{exc}. Unités : {', '.join(sorted(set(AREA) | set(LENGTH)))} ; "70 "préfixe $/ pour les prix unitaires.", error=True)71 return ToolResult(content=f"{out['formula']} (facteur {out['factor']:.6g}, {out['kind']}). "72 "Rappels : 1 m² = 10,7639 pi² ; 1 pi = 0,3048 m ; 1 arpent carré ≈ 3 418,9 m² ; 1 acre = 4 046,86 m².",73 payload={"value": v, "from": args["from_unit"], "to": args["to_unit"], **out},74 meta={"summary": out["formula"]})757677registry.register("unit_convert", run, Args)78