From bb4219da873ad09e21e9ea45c0e5a97b72db42da Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 4 Jun 2026 15:58:59 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20nutrient=5Fcost=20tool=20=E2=80=94=20ch?= =?UTF-8?q?eapest=20fertilizer=20per=20lb=20of=20N/P/K=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: claude Co-committed-by: claude --- ag_bids_mcp/client.py | 4 +++ ag_bids_mcp/format.py | 57 +++++++++++++++++++++++++++++++++++++++++++ ag_bids_mcp/server.py | 21 ++++++++++++++++ tests/test_format.py | 41 +++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+) diff --git a/ag_bids_mcp/client.py b/ag_bids_mcp/client.py index 35955ae..e09053d 100644 --- a/ag_bids_mcp/client.py +++ b/ag_bids_mcp/client.py @@ -97,6 +97,10 @@ def input_cost_geographies(item: str) -> dict: return _get("/api/data/input-cost-geographies", item=item) +def nutrient_cost(geo: str | None = None) -> dict: + return _get("/api/data/nutrient-cost", geo=geo) + + def futures(commodity: str, delivery: str | None = None) -> dict: return _get("/api/data/futures", commodity=commodity, delivery=delivery) diff --git a/ag_bids_mcp/format.py b/ag_bids_mcp/format.py index 89dc503..1e78510 100644 --- a/ag_bids_mcp/format.py +++ b/ag_bids_mcp/format.py @@ -22,6 +22,12 @@ def _ton(cents: Optional[int]) -> str: return f"${cents / 100:.2f}" +def _per_lb(cents: Optional[int]) -> str: + if cents is None: + return "—" + return f"${cents / 100:.2f}" + + def _basis(cents: Optional[int]) -> str: if cents is None: return "—" @@ -207,6 +213,57 @@ def fmt_price_series(payload: dict, max_points: int = 60) -> str: return "\n".join(head + body) + "\n" +_NUTRIENT_LABEL = {"n": "Nitrogen (N)", "p2o5": "Phosphate (P₂O₅)", "k2o": "Potash (K₂O)"} + + +def fmt_nutrient_cost(payload: dict) -> str: + """Cheapest fertilizer per pound of N / P2O5 / K2O for a region.""" + geo = payload.get("geo") or "Cornbelt" + src = payload.get("source") or "USDA AgTransport" + products = payload.get("products") or [] + cheapest = payload.get("cheapest") or {} + if not products: + return f"### Fertilizer value per nutrient — {geo}\n\nNo data on file.\n" + by_item = {p["item"]: p for p in products} + + lines = [f"### Fertilizer value per pound of nutrient — {geo}", ""] + for nut in ("n", "p2o5", "k2o"): + it = cheapest.get(nut) + prod = by_item.get(it or "") + if prod is not None: + c = (prod.get("cost_per_lb") or {}).get(nut) + lines.append( + f"- **Cheapest {_NUTRIENT_LABEL[nut]}:** " + f"{prod.get('label') or it} at {_per_lb(c)}/lb" + ) + lines += ["", "| Product | Grade | $/ton | $/lb N | $/lb P₂O₅ | $/lb K₂O |", + "|---|---|---:|---:|---:|---:|"] + + def _nkey(p: dict): + v = (p.get("cost_per_lb") or {}).get("n") + return (v is None, v if v is not None else 0) + + for p in sorted(products, key=_nkey): # cheapest N first, blanks last + a = p.get("analysis") or {} + cpl = p.get("cost_per_lb") or {} + grade = (a.get("grade") or "") + ("*" if a.get("grade_assumed") else "") + lines.append( + f"| {p.get('label') or p['item']} | {grade} | {_ton(p.get('price_cents_per_ton'))} | " + f"{_per_lb(cpl.get('n'))} | {_per_lb(cpl.get('p2o5'))} | {_per_lb(cpl.get('k2o'))} |" + ) + + period = next((p.get("period") for p in products if p.get("period")), None) + assumed = any((p.get("analysis") or {}).get("grade_assumed") for p in products) + foot = f"_Source: {src}" + if period: + foot += f" · as of {_ym(period)}" + foot += " · $/lb = $/ton ÷ (analysis% × 2000 lb)" + if assumed: + foot += " · *UAN grade assumed 32-0-0" + lines.append("\n" + foot + "_") + return "\n".join(lines) + "\n" + + # ---------- futures quote + change ---------- diff --git a/ag_bids_mcp/server.py b/ag_bids_mcp/server.py index 05f29e5..0f435df 100644 --- a/ag_bids_mcp/server.py +++ b/ag_bids_mcp/server.py @@ -411,6 +411,27 @@ def input_cost_series( return fmt.fmt_price_series(client.input_cost_series(item=it, geo=g)) +@mcp.tool() +def nutrient_cost( + geo: Annotated[ + str | None, + Field(description="Fertilizer region (default 'Cornbelt'). Same regions as " + "input_cost_trend (e.g. 'U.S. Gulf NOLA', 'Northern Plains')."), + ] = None, +) -> str: + """Cheapest fertilizer per POUND of nutrient — "what's the best value for N (or P/K)?". + + Converts each fertilizer's regional $/ton (USDA AgTransport, latest month) into + $/lb of N, P2O5, and K2O from its grade, ranks them, and names the cheapest + source of each nutrient (anhydrous usually wins on N; DAP/MAP are phosphate + buys; potash is the K source). Use THIS — not input_cost_trend ($/ton only) — + whenever the grower asks which fertilizer is the best buy / best nitrogen value. + UAN grade is assumed 32-0-0.""" + g = geo.strip() if geo else None + with track("nutrient_cost", geo=g or ""): + return fmt.fmt_nutrient_cost(client.nutrient_cost(geo=g)) + + @mcp.tool() def list_sources() -> str: """All active scrapers + their last-success timestamps and any pending failures.""" diff --git a/tests/test_format.py b/tests/test_format.py index 9846117..4cc86b5 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -390,3 +390,44 @@ def test_fmt_summary_includes_best_for_today(): assert "Mercer Landmark — St Henry" in out assert "$4.5800" in out # corn last assert "No current-month local bid posted" in out # soy fallback + + +def test_fmt_nutrient_cost(): + payload = { + "geo": "Cornbelt", + "source": "USDA AgTransport", + "products": [ + {"item": "anhydrous", "label": "anhydrous ammonia", "unit": "$/ton", + "geo": "Cornbelt", "period": "2026-03-01", "price_cents_per_ton": 88062, + "analysis": {"grade": "82-0-0", "grade_assumed": False}, + "cost_per_lb": {"n": 54, "p2o5": None, "k2o": None}}, + {"item": "urea", "label": "urea", "unit": "$/ton", "geo": "Cornbelt", + "period": "2026-03-01", "price_cents_per_ton": 69812, + "analysis": {"grade": "46-0-0", "grade_assumed": False}, + "cost_per_lb": {"n": 76, "p2o5": None, "k2o": None}}, + {"item": "uan", "label": "UAN (28-32%)", "unit": "$/ton", "geo": "Cornbelt", + "period": "2026-03-01", "price_cents_per_ton": 47612, + "analysis": {"grade": "32-0-0", "grade_assumed": True}, + "cost_per_lb": {"n": 74, "p2o5": None, "k2o": None}}, + {"item": "potash", "label": "potash (0-0-60)", "unit": "$/ton", + "geo": "Cornbelt", "period": "2026-03-01", "price_cents_per_ton": 35938, + "analysis": {"grade": "0-0-60", "grade_assumed": False}, + "cost_per_lb": {"n": None, "p2o5": None, "k2o": 30}}, + ], + "cheapest": {"n": "anhydrous", "p2o5": None, "k2o": "potash"}, + } + out = fmt.fmt_nutrient_cost(payload) + assert "Cornbelt" in out + assert "Cheapest Nitrogen (N):** anhydrous ammonia at $0.54/lb" in out + assert "Cheapest Potash (K₂O):** potash (0-0-60) at $0.30/lb" in out + # anhydrous (cheapest N) sorts above urea in the table + assert out.index("anhydrous ammonia |") < out.index("| urea |") + # UAN grade-assumed marker + footnote + assert "32-0-0*" in out + assert "UAN grade assumed 32-0-0" in out + # potash supplies no N -> em-dash in the N column + assert "—" in out + + +def test_fmt_nutrient_cost_empty(): + assert "No data on file" in fmt.fmt_nutrient_cost({"geo": "Cornbelt", "products": []})