From 9b678024d2ec95ea1110568a26d98afb8d36ce02 Mon Sep 17 00:00:00 2001 From: Punch Date: Thu, 4 Jun 2026 05:52:21 +0000 Subject: [PATCH] Add GET / discovery endpoint for agent navigation --- CHANGELOG.md | 10 ++++++++++ app/main.py | 37 ++++++++++++++++++++++++++++++++++++- tests/test_health.py | 15 +++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba12448..63bd8b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [0.15.7] — 2026-06-04 + +### Added +- **`GET /`** (`app/main.py`): root discovery endpoint. Was 404; now returns API name, version, docs links (`/docs`, `/redoc`, `/openapi.json`), dashboard URL, and a labelled map of primary endpoints (active rounds, specs, leaderboards, SOTA, submit, preview) plus a one-line quickstart. An agent arriving at the bare host can now navigate the API without out-of-band knowledge. + +### Tests +- **1 new test** (`tests/test_health.py::test_root_discovery`). Test count: 138 → 139. + +--- + ## [0.15.6] — 2026-06-04 ### Fixed diff --git a/app/main.py b/app/main.py index 183dc39..6d9e638 100644 --- a/app/main.py +++ b/app/main.py @@ -20,7 +20,7 @@ async def lifespan(app: FastAPI): app = FastAPI( title="Forge API", description="Competitive parametric CAD benchmark — specs, submissions, leaderboard, SOTA.", - version="0.15.6", + version="0.15.7", lifespan=lifespan, ) @@ -84,3 +84,38 @@ def custom_openapi(): @app.get("/health") async def health(): return {"status": "ok"} + + +@app.get("/", tags=["meta"]) +async def root(): + """API entry point — discovery payload for agents arriving at the bare host. + + Lists the API name, version, docs links, and the primary endpoints an agent + needs to start exploring (active rounds, specs, leaderboards, SOTA). + """ + return { + "name": app.title, + "version": app.version, + "description": app.description, + "docs": { + "swagger": "/docs", + "redoc": "/redoc", + "openapi": "/openapi.json", + }, + "dashboard": "https://forge.gittensor.io", + "endpoints": { + "active_rounds": "/rounds/active", + "rounds": "/rounds", + "specs": "/specs", + "overall_leaderboard": "/leaderboard/overall", + "sota": "/sota", + "submit": "POST /submissions", + "preview": "POST /eval/preview", + "health": "/health", + "health_deep": "/health/deep", + }, + "quickstart": ( + "Start at GET /rounds/active to see open competitions, then " + "GET /specs to list problems. Submit agent results to POST /submissions." + ), + } diff --git a/tests/test_health.py b/tests/test_health.py index dc352a9..218f372 100644 --- a/tests/test_health.py +++ b/tests/test_health.py @@ -141,3 +141,18 @@ def test_health_deep_storage_s3_error(deep_client, monkeypatch): assert body["checks"]["storage"]["status"] == "error" assert body["checks"]["storage"]["backend"] == "s3" assert body["status"] == "degraded" + + +def test_root_discovery(client): + """Bare root returns a discovery payload, not 404, so agents can navigate.""" + r = client.get("/") + assert r.status_code == 200 + body = r.json() + assert body["name"] == "Forge API" + assert "version" in body + assert body["docs"]["swagger"] == "/docs" + assert body["docs"]["openapi"] == "/openapi.json" + assert body["endpoints"]["active_rounds"] == "/rounds/active" + assert body["endpoints"]["overall_leaderboard"] == "/leaderboard/overall" + assert body["endpoints"]["submit"].startswith("POST") + assert "quickstart" in body