diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f63a3e..5aafae9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,8 +26,21 @@ not an index. - `GET /api/data/price-series?commodity=&geo=&start_year=&end_year=` — raw series. - `GET /api/data/price-geographies?commodity=` — which geos (US + states) exist. -All grain: corn/soy/wheat, all 50 states + US. (Input-cost trends — diesel, -fertilizer — coming next.) +All grain: corn/soy/wheat, all 50 states + US. + +### Input-cost tools (real $ + change) + +- **`input_cost_trend(item, years=10)`** — real input price with the move. + Currently `item="diesel"` (EIA U.S. retail $/gal, weekly, back to 1994): + latest price + week-over-week and year-over-year change + seasonal + percentile/range. (For current fertilizer $/ton, `current_input_price` (DTN) + still applies; more inputs extend this same tool.) +- **`input_cost_series(item)`** — raw historical series for an input. +- API: `GET /api/data/input-cost-trend?item=&years=`, + `GET /api/data/input-cost-series?item=`. + +USDA stopped publishing dollar input prices in 2014, so these use real-dollar +sources (EIA) rather than an index. ## 2026-05-30 — Source geo + many more locations diff --git a/ag_bids_mcp/client.py b/ag_bids_mcp/client.py index c405a6d..7de688f 100644 --- a/ag_bids_mcp/client.py +++ b/ag_bids_mcp/client.py @@ -80,6 +80,14 @@ def price_series(commodity: str, geo: str = "US", start_year=start_year, end_year=end_year) +def input_cost_trend(item: str, years: int = 10) -> dict: + return _get("/api/data/input-cost-trend", item=item, years=years) + + +def input_cost_series(item: str) -> dict: + return _get("/api/data/input-cost-series", item=item) + + 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 270cdfe..ac0f720 100644 --- a/ag_bids_mcp/format.py +++ b/ag_bids_mcp/format.py @@ -130,14 +130,16 @@ _MONTH_NAMES_3 = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", def fmt_price_trend(payload: dict, label: str = "price received") -> str: - commodity = payload.get("commodity") + name = payload.get("commodity") or payload.get("item") geo = payload.get("geo") + label = payload.get("label") or label unit = payload.get("unit") or "$/bu" t = payload.get("trend") + loc = f" — {geo}" if geo else "" if not t: - return f"### {commodity} — {geo} — {label}\n\nNo data on file.\n" + return f"### {name}{loc} — {label}\n\nNo data on file.\n" when = _ym(t["period"]) - lines = [f"### {commodity} — {geo} — {label}, {when}: {_unit_money(t['value_cents'], unit)}", ""] + lines = [f"### {name}{loc} — {label}, {when}: {_unit_money(t['value_cents'], unit)}", ""] arrow = _delta_arrow(t.get("change_cents")) lines.append(f"- **Change:** {arrow} {_signed(t.get('change_cents'))} ({_pct(t.get('change_pct'))}) vs prior period") if t.get("yoy_cents") is not None: @@ -155,14 +157,15 @@ def fmt_price_trend(payload: dict, label: str = "price received") -> str: def fmt_price_series(payload: dict, max_points: int = 60) -> str: - commodity = payload.get("commodity") + name = payload.get("commodity") or payload.get("item") geo = payload.get("geo") unit = payload.get("unit") or "$/bu" series = payload.get("series") or [] + loc = f" — {geo}" if geo else "" if not series: - return f"### {commodity} — {geo} series\n\nNo data on file.\n" + return f"### {name}{loc} series\n\nNo data on file.\n" shown = series[-max_points:] - head = [f"### {commodity} — {geo} — {payload.get('count', len(series))} periods " + head = [f"### {name}{loc} — {payload.get('count', len(series))} periods " f"(showing last {len(shown)})", "", "| Period | Price |", "|---|---:|"] body = [f"| {_ym(p['period'])} | {_unit_money(p['value_cents'], unit)} |" for p in shown] diff --git a/ag_bids_mcp/server.py b/ag_bids_mcp/server.py index a54f5c4..d135ab0 100644 --- a/ag_bids_mcp/server.py +++ b/ag_bids_mcp/server.py @@ -303,6 +303,42 @@ def price_series( return fmt.fmt_price_series(payload) +VALID_INPUTS = {"diesel"} + + +@mcp.tool() +def input_cost_trend( + item: Annotated[ + str, Field(description="Input to price. Currently: 'diesel' (U.S. retail $/gal).") + ], + years: Annotated[ + int, Field(ge=1, le=120, description="Baseline window for the seasonal normal/percentile."), + ] = 10, +) -> str: + """Real input cost with the change — e.g. U.S. retail diesel ($/gal). + + Latest real price + week-over-week and year-over-year moves + seasonal + percentile/range. Fills the input-cost side for the advisor (fuel). For + current fertilizer $/ton use current_input_price.""" + it = item.strip().lower() + with track("input_cost_trend", item=it, years=years): + if it not in VALID_INPUTS: + return f"`item` must be one of: {sorted(VALID_INPUTS)}" + return fmt.fmt_price_trend(client.input_cost_trend(item=it, years=years)) + + +@mcp.tool() +def input_cost_series( + item: Annotated[str, Field(description="Input: 'diesel'.")], +) -> str: + """Raw historical series for a tracked input cost (diesel, $/gal).""" + it = item.strip().lower() + with track("input_cost_series", item=it): + if it not in VALID_INPUTS: + return f"`item` must be one of: {sorted(VALID_INPUTS)}" + return fmt.fmt_price_series(client.input_cost_series(item=it)) + + @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 7f2f508..804d85a 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -118,6 +118,19 @@ def test_fmt_price_trend_diesel_units(): assert "$5.520/gal" in out # 3-decimal $/gal formatting +def test_fmt_input_cost_payload_shape(): + # Input-cost payload uses item/label and has no geo. + payload = {"item": "diesel", "label": "retail diesel", "unit": "$/gal", "source": "EIA", + "trend": {"period": "2026-05-25", "value_cents": 552, "prev_cents": 560, + "change_cents": -8, "change_pct": -1.4, "yoy_cents": 349, + "yoy_change_cents": 203, "yoy_pct": 58.2, "seasonal": None, + "recent_direction": "down", "baseline_years": 10, "points": 1680}} + out = fmt.fmt_price_trend(payload) + assert "diesel — retail diesel" in out # item/label header, no geo segment + assert "$5.520/gal" in out + assert "+58.2%" in out # YoY surfaced + + def test_fmt_inputs_lime_table(): payload = { "product": "lime", "count": 2,