Add input_cost_trend / input_cost_series MCP tools (EIA diesel)
Real input price + WoW/YoY change + seasonal for diesel ($/gal). Formatters now handle the item/label payload shape. Changelog updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+15
-2
@@ -26,8 +26,21 @@ not an index.
|
|||||||
- `GET /api/data/price-series?commodity=&geo=&start_year=&end_year=` — raw series.
|
- `GET /api/data/price-series?commodity=&geo=&start_year=&end_year=` — raw series.
|
||||||
- `GET /api/data/price-geographies?commodity=` — which geos (US + states) exist.
|
- `GET /api/data/price-geographies?commodity=` — which geos (US + states) exist.
|
||||||
|
|
||||||
All grain: corn/soy/wheat, all 50 states + US. (Input-cost trends — diesel,
|
All grain: corn/soy/wheat, all 50 states + US.
|
||||||
fertilizer — coming next.)
|
|
||||||
|
### 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
|
## 2026-05-30 — Source geo + many more locations
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,14 @@ def price_series(commodity: str, geo: str = "US",
|
|||||||
start_year=start_year, end_year=end_year)
|
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:
|
def futures(commodity: str, delivery: str | None = None) -> dict:
|
||||||
return _get("/api/data/futures", commodity=commodity, delivery=delivery)
|
return _get("/api/data/futures", commodity=commodity, delivery=delivery)
|
||||||
|
|
||||||
|
|||||||
@@ -130,14 +130,16 @@ _MONTH_NAMES_3 = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|||||||
|
|
||||||
|
|
||||||
def fmt_price_trend(payload: dict, label: str = "price received") -> str:
|
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")
|
geo = payload.get("geo")
|
||||||
|
label = payload.get("label") or label
|
||||||
unit = payload.get("unit") or "$/bu"
|
unit = payload.get("unit") or "$/bu"
|
||||||
t = payload.get("trend")
|
t = payload.get("trend")
|
||||||
|
loc = f" — {geo}" if geo else ""
|
||||||
if not t:
|
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"])
|
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"))
|
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")
|
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:
|
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:
|
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")
|
geo = payload.get("geo")
|
||||||
unit = payload.get("unit") or "$/bu"
|
unit = payload.get("unit") or "$/bu"
|
||||||
series = payload.get("series") or []
|
series = payload.get("series") or []
|
||||||
|
loc = f" — {geo}" if geo else ""
|
||||||
if not series:
|
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:]
|
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)})", "",
|
f"(showing last {len(shown)})", "",
|
||||||
"| Period | Price |", "|---|---:|"]
|
"| Period | Price |", "|---|---:|"]
|
||||||
body = [f"| {_ym(p['period'])} | {_unit_money(p['value_cents'], unit)} |" for p in shown]
|
body = [f"| {_ym(p['period'])} | {_unit_money(p['value_cents'], unit)} |" for p in shown]
|
||||||
|
|||||||
@@ -303,6 +303,42 @@ def price_series(
|
|||||||
return fmt.fmt_price_series(payload)
|
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()
|
@mcp.tool()
|
||||||
def list_sources() -> str:
|
def list_sources() -> str:
|
||||||
"""All active scrapers + their last-success timestamps and any pending failures."""
|
"""All active scrapers + their last-success timestamps and any pending failures."""
|
||||||
|
|||||||
@@ -118,6 +118,19 @@ def test_fmt_price_trend_diesel_units():
|
|||||||
assert "$5.520/gal" in out # 3-decimal $/gal formatting
|
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():
|
def test_fmt_inputs_lime_table():
|
||||||
payload = {
|
payload = {
|
||||||
"product": "lime", "count": 2,
|
"product": "lime", "count": 2,
|
||||||
|
|||||||
Reference in New Issue
Block a user