Skip to main content

bike-routing-agent

CI

An OSM-based bike-routing service: a typed, provider-neutral domain model, a full openrouteservice API client plus routing adapter, and a LangGraph workflow that turns a validated route request into an explainable, exportable cycling route.

The LLM (where used) may parse free text, ask clarification questions, and explain a chosen route. It never invents coordinates, geometry, elevation, or route suitability -- all of that comes from OSM-derived routing engines.

Easy install (no programming needed)

Download pybikerouter-easy-<version>.zip from the Releases page, unzip it, install Docker Desktop, and double-click the Start file for your computer. The first start asks one question -- where you ride -- downloads that area's map data, and opens the planner in your browser. Step by step, in plain words: docs/easy-install.md (and START-HERE.txt in the download).

Documentation

The full documentation suite lives in docs/:

Document Contents
Architecture Graph nodes, state, branching, terminal statuses, error model
API Endpoint contracts, response statuses, artifact serving
Configuration Every environment variable, validation rules, deployment notes
Providers Protocols, ORS client/adapter behaviour, geocoders, BRouter/Valhalla adapters, adding a backend
Backend comparison Measured ORS vs BRouter behaviour per bike type, combining both engines
Geocoding Nominatim vs Pelias, confidence/ambiguity semantics, tuning
Scoring & exports Score math, uncertainty policy, explanation rules, GeoJSON/GPX
Plan from words Describe a ride in a sentence; an optional LLM turns it into the structured request (never coordinates)
Weather Forecast along the route at your departure time: temperature, rain, wind vs. riding direction (free, keyless providers)
Self-hosted stack Run routing and/or geocoding locally from one OSM extract; hardware requirements and small-PC options
Testing Test layout, mocking conventions, live tests, CI
Web planner Putting places into the trip; click a point on the map and choose where in the route it goes
iPhone / Tailscale Use the planner from an iPhone as a home-screen app over your tailnet
Roadmap Planned work and explicit non-goals

Architecture

FastAPI
  |
  v
LangGraph RouteAgentState
  |
  +--> parse_request        (structured input first; LLM extraction is an injectable dependency)
  +--> validate_request      (Pydantic)
  +--> geocode_locations     (Nominatim by default, public or self-hosted; ambiguity -> clarification, never a guess)
  +--> route_with_provider   (openrouteservice; engine-neutral RoutingRequest in, RouteCandidate out)
  +--> score_candidates      (deterministic distance/elevation/warning scoring)
  +--> explain_and_export    (facts-only explanation + GeoJSON/GPX export)
  |
  v
JSON response + GeoJSON/GPX artifacts

Graph branches:

START -> parse_request
      -> validate_request -> (invalid | awaiting_clarification) -> END
      -> geocode_locations -> (awaiting_clarification | provider_failure) -> END
      -> route_with_provider -> (no_route | provider_failure) -> END
      -> score_candidates -> (no_route) -> END
      -> explain_and_export -> END

Every path ends with an explicit status; the graph never calls a routing provider unless validation and geocoding both succeeded, and it never returns without a terminal status.

Setup

Requires Python 3.11+ and Poetry.

poetry install
cp .env.example .env

poetry install creates an in-project virtualenv (.venv/, see poetry.toml) with both runtime and dev dependencies. Run any command inside it with poetry run <command>, or poetry shell to activate it directly.

Edit .env and set ORS_API_KEY to a valid openrouteservice API key.

Environment variables

Variable Purpose Default
ORS_API_KEY openrouteservice API key (required for live routing)
ORS_BASE_URL openrouteservice base URL https://api.openrouteservice.org
ORS_TIMEOUT_S ORS request timeout (seconds) 10.0
ORS_MAX_RETRIES Retries for timeouts/5xx 2
GEOCODER_PROVIDER Geocoder backend: nominatim (public or self-hosted) or pelias (needs a Pelias API behind ORS_BASE_URL, see docs/geocoding.md; refuses the public ORS URL at startup) nominatim
GEOCODER_BASE_URL Nominatim base URL (ignored when GEOCODER_PROVIDER=pelias) https://nominatim.openstreetmap.org
GEOCODER_TIMEOUT_S Geocoder request timeout 5.0
GEOCODER_USER_AGENT Required by Nominatim's usage policy bike-routing-agent/0.1
GEOCODER_CACHE_TTL_S In-memory geocode cache TTL 3600
GEOCODER_AMBIGUITY_MARGIN Confidence gap below which top-2 results are "ambiguous" 0.05
GEOCODER_MIN_CONFIDENCE Minimum confidence to auto-accept a top result 0.3
VALHALLA_BASE_URL Base URL of a self-hosted Valhalla (meili) server http://localhost:8002
VALHALLA_TIMEOUT_S Per-request Valhalla timeout (seconds) 30.0
VALHALLA_MAX_RETRIES Retries for Valhalla timeouts/5xx 1
EXPORT_DIR Local directory for GeoJSON/GPX artifacts exports
DATABASE_URL PostgreSQL/PostGIS URL: records route history, enables /v1/history/* (docs/persistence.md) unset
ARTIFACT_BACKEND local or database (artifacts stored in PostgreSQL) local
LOG_LEVEL Log level INFO

Running the API

poetry run uvicorn bike_routing_agent.api:app --reload

The app also serves a built-in web UI (map-based route planner) at http://localhost:8000/, with the interactive API docs at /docs.

Command-line interface

Installing the package exposes a bike-router command (also runnable as python -m bike_routing_agent.cli) organised into subcommand groups:

bike-router route plan --origin "Berlin Hbf" --destination "Potsdam" \
    --bike-type gravel --target-distance-km 30 --output plan.json
bike-router serve start --port 8000
bike-router docker up            # docker compose -f docker/compose.yaml up -d --build
bike-router docker logs -f
bike-router config show          # effective settings, secrets redacted
bike-router config check         # validate settings against the environment
bike-router providers list       # provider names and bike-type -> ORS profile map
bike-router route plan ... --format text --gpx best.gpx   # alternatives, sight stops, exports
bike-router route show plan.json # a saved plan again; `route export` writes any alternative
bike-router poi along --route plan.json   # sights near a route, ranked by fame
bike-router brouter needed|info|download  # BRouter map tiles (asks before downloading)
bike-router history list|show|stats       # past plans (needs DATABASE_URL)
bike-router status show          # readiness of every component and the active features
bike-router chat start           # plan routes by chatting (docs/chat.md)

Everything the API and the web form do is available from the CLI -- see docs/cli.md.

route plan --text "50 km gravel loop from Braunschweig" uses the optional LLM parser (docs/llm-parser.md).

route plan accepts lat,lon pairs or place text for --origin, --destination and repeated --via waypoints, prints the JSON result and exits 0 on ready, 1 on clarification/provider failure, 2 on usage errors. With DATABASE_URL set it also records the plan in the route history (best effort; plan_id in the output, --no-record to skip), a forecast along the route at --departure-time (docs/weather.md), and bike-router db migrate / db status manage the schema (docs/persistence.md).

Running with Docker

All container assets live in docker/:

cp .env.example .env        # set ORS_API_KEY
docker compose -f docker/compose.yaml up --build

Open http://localhost:8000/ for the web UI. Route artifacts are stored in the named pybikerouter-exports volume. See docs/self-hosted.md for the optional local routing / geocoding stack -- including what it needs and how to run just one half on a small machine.

Example request

curl -X POST http://localhost:8000/v1/route/plan \
  -H 'Content-Type: application/json' \
  -d '{
    "origin": "Braunschweig Hauptbahnhof",
    "destination": "Wolfenbüttel",
    "constraints": {
      "bike_type": "gravel",
      "avoid_high_traffic_roads": true,
      "max_ascent_m": 500
    }
  }'

Example response

{
  "status": "ready",
  "route": {
    "provider": "ors",
    "provider_profile": "cycling-regular",
    "geometry_geojson": {"type": "LineString", "coordinates": []},
    "metrics": {
      "distance_m": 12500,
      "duration_s": 2700,
      "ascent_m": 85,
      "descent_m": 90,
      "surface_coverage": {},
      "unknown_surface_fraction": null
    },
    "score": 0.91,
    "score_breakdown": {"distance_fit": 1.0, "elevation_fit": 1.0, "warning_penalty": 0.0},
    "warnings": [],
    "provenance": {"provider": "ors", "profile": "cycling-regular"}
  },
  "candidates": [
    {"provider": "ors", "provider_profile": "cycling-regular", "score": 0.91},
    {"provider": "brouter", "provider_profile": "trekking", "score": 0.84}
  ],
  "explanation": "...",
  "artifacts": {
    "geojson_url": "/v1/routes/<id>.geojson",
    "gpx_url": "/v1/routes/<id>.gpx"
  },
  "clarification": [],
  "errors": []
}

If origin/destination are ambiguous or unresolved, status is awaiting_clarification and clarification lists the candidate places to choose from -- no route is generated from a guess.

Bike types

constraints.bike_type selects the routing profile per engine and defaults to gravel. Supported values: road, gravel, touring, mountain, city, ebike, commuter (fast, low-traffic), recumbent. ORS ships four cycling profiles, so e.g. ebike uses cycling-electric while commuter rides on cycling-regular; the self-hosted BRouter provider differentiates further -- gravel, touring, mountain, city, commuter and recumbent each get their own profile, while road and ebike share fastbike (BRouter has no e-assist cost model) (see docs/configuration.md).

Make targets

make lists the everyday commands; they use the project's .venv (or the active environment):

make install       # poetry install
make hooks         # the git hooks (commit: lint + types; push: the whole suite)
make run           # the API with auto-reload (HOST=127.0.0.1 PORT=8000)
make check         # lint + types + tests without coverage, stops at the first failure
make ci            # what CI runs: lint, types, tests with the 80 % coverage floor
make test-fast ARGS="tests/nodes -k geocode"     # a part of the suite (no coverage floor)
make deploy        # build and restart the local container (bike-router deploy up)

Linting and type checking

poetry run ruff check .
poetry run mypy src

Both run in CI on every push and pull request (see .github/workflows/ci.yml).

Running tests

poetry run pytest

Coverage is collected on every run and the suite fails below 80% (--cov-fail-under=80 in tool.pytest.ini_options.addopts; currently 97% total, every module ≥84%).

Live tests that hit real external services are marked @pytest.mark.live and excluded by default (see tool.pytest.ini_options.addopts in pyproject.toml). Run them explicitly with:

poetry run pytest -m live

Pre-commit hooks

Quality gates run automatically before each commit/push via pre-commit. Install both git hook stages once after poetry install:

poetry run pre-commit install --hook-type pre-commit --hook-type pre-push

On commit it runs whitespace/EOF/YAML/TOML/merge-conflict/large-file checks, ruff check --fix and mypy src; the full pytest suite runs only in the pre-push stage on git push (mirroring CI). Run the commit stage manually with poetry run pre-commit run --all-files and the pre-push stage with poetry run pre-commit run --hook-stage pre-push --all-files.

Known limitations

  • OSM tag completeness varies by region; surface/access metadata is frequently missing and is represented as uncertainty, not treated as favorable.
  • Routing engines (ORS, Valhalla, BRouter) map bike categories to their own profiles differently; the mapping in config.py is an approximation, especially for gravel riding.
  • Elevation data (ascent/descent) varies in accuracy between providers and is not always available.
  • No route produced by this service is a safety guarantee. Explanations use hedged language ("better aligned with available map metadata") rather than claims like "safe route".
  • BRouter and Valhalla are wired up end to end but only against self-hosted deployments (ROUTING_PROVIDER=brouter / valhalla / all, BRouter via the compose profile in docker/brouter/README.md), and openrouteservice remains the default engine.
  • The Pelias geocoder option needs a Pelias API served at <ORS_BASE_URL>/pelias/v1: neither the public api.openrouteservice.org nor a stock self-hosted ORS provides one, and the adapter has only been tested against mocks (see docs/geocoding.md). For a local geocoder use the self-hosted Nominatim.

Repository layout

src/bike_routing_agent/
├── api.py            FastAPI app (POST /v1/route/plan)
├── config.py          Settings + provider profile mapping
├── models.py          Pydantic v2 domain models
├── state.py            LangGraph RouteAgentState
├── graph.py            LangGraph wiring
├── errors.py           Structured provider/geocoding errors
├── providers/         GeocodeProvider / RoutingProvider adapters
│                      (ors_client.py: full ORS client -- directions, export,
│                       isochrones, matrix, snap, POIs, Vroom, elevation,
│                       Pelias geocoding, health; ors.py: the RoutingProvider;
│                       geocoder.py: Nominatim; pelias.py: Pelias on
│                       self-hosted ORS)
├── nodes/              One module per graph node
├── scoring/            Deterministic route scoring
├── exporters/          GeoJSON / GPX export
├── frontend/           Static web UI served at /
└── cli/                bike-router command line (route/serve/docker/config/providers)
docker/                 Dockerfile + compose.yaml
tests/
├── fixtures/           Sample provider responses
├── nodes/               One test module per graph node
├── providers/          Adapter unit tests (mocked HTTP)
├── graph/              Graph branch tests
├── exporters/          Export format tests
└── live/                Tests marked @pytest.mark.live
docs/                   Documentation suite (see docs/README.md)

Licence

MIT, see LICENSE.

Third-party licences

BRouter (MIT) and the OpenStreetMap data (ODbL) this project builds on are credited, with the full BRouter licence text, in THIRD_PARTY_LICENSES.md.

Metadata

Release files for bike-router 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bike-router 0.3.0
File Size Uploaded Provenance
bike_router-0.3.0.tar.gz 404.9 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for bike-router 0.3.0
File Interpreter ABI Platform
bike_router-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 889.3 kB

Release files / bike_router-0.3.0.tar.gz

Download URL bike_router-0.3.0.tar.gz
Size 404.9 kB
Tags Source
SHA-256 checksum
How to use checksums
18cdbea84e94329df59be1e9b8725277c5ee134c92a9a5ec3641030bb0f59712
BLAKE2b-256 checksum
How to use checksums
db44135fa7955864bea5533dcf92bf7086a66ed08c9922a38d91ccba4947a4f0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.

Transparency log

Release files / bike_router-0.3.0-py3-none-any.whl

Download URL bike_router-0.3.0-py3-none-any.whl
Size 484.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5447a2dd0f4d85ac0b2459e59dc8039a4203c636dca33137bffa17463dbdb3b0
BLAKE2b-256 checksum
How to use checksums
28ce8918f8b173978cc0b19e95f7ec7e28aa7f4f57cdd63eedc4f7a2d7ad6f4f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.

Transparency log

Security for bike-router 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for bike_router-0.3.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page