Plan a compact, weather-aware day itinerary for any city. The system:
- Classifies the request with Gemini
- Calls MCP tools (geo, weather, air, calendar, route, fx)
- Streams a concise Markdown itinerary with travel legs, guardrails (rain/AQI), and optional currency conversion
- Docker and docker-compose
- Python 3.11+ for the local CLI (optional)
- API keys (place these in a
.envfile at the repo root):- MCP_API_KEY=dev-key (or your own)
- GEMINI_API_KEY=...
- EXCHANGERATE_API_KEY=...
- OPENAQ_API_KEY=...
Create your .env from the example and fill in keys:
cp .env.example .env
# edit .env with your keysOptional overrides (default values shown):
- GEMINI_MODEL=gemini-2.5-flash-preview-09-2025
- CLASSIFICATION_DEFAULT_DATE=2025-11-10
- HTTP_TIMEOUT_SEC=15
- NEARBY_RADIUS_M=3000
- NEARBY_LIMIT=1
- ROUTE_PROFILE_DEFAULT=foot
- ORCHESTRATOR_URL=http://localhost:3002 (for the CLI)
start:
docker-compose up --build -d
restart:
docker-compose down && docker-compose up --build -d
stop:
docker-compose down
ps:
docker-compose ps
logs:
docker-compose logs -f mcp_tools orchestratorServices:
- MCP Tools API on http://localhost:3001 (requires header
X-API-KEY) - Orchestrator API on http://localhost:3002
graph LR
U["Client CLI"]
O["Orchestrator (FastAPI /plan-day SSE)"]
G["Gemini API (LLM)"]
subgraph MCP["MCP Tools (FastAPI)"]
GEO["geo"]
WEA["weather"]
AIR["air"]
RTE["route"]
CAL["calendar"]
FX["fx"]
end
subgraph EXT["External APIs"]
NOM["Nominatim"]
OM["Open-Meteo"]
AQ["OpenAQ"]
OSRM["OSRM"]
NAGER["Nager.Date"]
FXAPI["exchangerate.host"]
end
U -->|"POST /plan-day (SSE)"| O
O -->|"Classify / Plan / Summarize"| G
O -->|"HTTP JSON"| MCP
GEO --> NOM
WEA --> OM
AIR --> AQ
AIR --> OM
RTE --> OSRM
CAL --> NAGER
FX --> FXAPI
Create a virtualenv and install the CLI dependencies with uv:
python3 -m venv .venv
source .venv/bin/activate
uv pip install -r client_cli/requirements.txtFirst, start the services (MCP Tools and Orchestrator) in Docker:
docker-compose up -d --buildInteractive mode (multi‑turn session with refinement/comparison and caching):
source .venv/bin/activate
python3 client_cli/main.py -i
# Then type prompts like:
# Plan a museum-first day in Amsterdam on 2025-11-22. Bike preferred.
# add a specialty coffee stop near the second venue
# compare two options if it rains after 3 PM
# convert 500000 Japanese Yen to Indian Rupee
# EXPORT # saves the entire session's streamed output (all turns) to ./itinerary-YYYYmmdd-HHMMSS.mdYou will see streaming tool traces in stderr and Markdown in stdout.
Run the CLI with structured args (single turn):
source .venv/bin/activate
python3 client_cli/main.py "Kyoto" "2025-12-12" --prefer "temples" --prefer "walkable"Run the CLI with a free-form prompt (single turn):
source .venv/bin/activate
python3 client_cli/main.py --prompt "Plan 10:00–18:00 in Kyoto on 2025-12-12. Prefer temples and walkable"Include a currency conversion in the prompt (orchestrator will call MCP FX):
source .venv/bin/activate
python3 client_cli/main.py --prompt "Plan a day in Tokyo on 2025-11-20. Also convert 200 USD to JPY"Notes:
- EXPORT is available only in interactive mode and saves to the current working directory.
- Interactive sessions automatically use a session_id so follow‑ups refine/compare the same plan.
- Pure FX follow‑ups (e.g., “convert 200 USD to JPY”) short‑circuit planning and only return the conversion.
- Pure AQI/weather/holiday follow‑ups (e.g., “What’s the AQI in Kyoto?”, “Weather in Kyoto on 2025‑12‑12”, “Public holidays in JP this year”) also short‑circuit and return compact tables without generating an itinerary.
- You can combine these (e.g., “AQI in Kyoto and public holidays in JP this year?”); the orchestrator will return multiple compact sections in one response, still without generating an itinerary.
- Determinism: for the same prompt/context, stop order and ETAs are stable (models at temperature 0, deterministic nearby sorting, and a plan/ETA cache).
curl -s -H "X-API-KEY: ${MCP_API_KEY:-dev-key}" http://localhost:3001/ | jq
curl -s http://localhost:3002/plan-day -X POST \
-H "Content-Type: application/json" \
-d '{"prompt":"Plan a museum-first day in Amsterdam on 2025-11-22. Bike preferred."}'- Open-Meteo (weather forecast):
- Base:
https://api.open-meteo.com/v1/forecast - Example:
curl -G "https://api.open-meteo.com/v1/forecast" \ --data-urlencode "latitude=35.0116" \ --data-urlencode "longitude=135.7681" \ --data-urlencode "hourly=temperature_2m,precipitation_probability,windspeed_10m" \ --data-urlencode "timezone=UTC"
- Base:
- OpenAQ v3 (air quality observations):
- Base:
https://api.openaq.org/v3 - Example (latest near coords, repeated parameters):
curl -G "https://api.openaq.org/v3/latest" \ -H "X-API-Key: $OPENAQ_API_KEY" \ --data-urlencode "coordinates=40.7128,-74.0060" \ --data-urlencode "radius=25000" \ --data-urlencode "limit=50" \ --data-urlencode "parameter=pm25" \ --data-urlencode "parameter=pm10" \ --data-urlencode "parameter=no2" \ --data-urlencode "parameter=o3"
- Base:
- Nominatim / OpenStreetMap (geocoding + places):
https://nominatim.openstreetmap.org/searchhttps://nominatim.openstreetmap.org/reverse
- OSRM demo (routing and ETAs):
https://router.project-osrm.org/route/v1/{profile}/{lon},{lat};{lon},{lat}
- Nager.Date (public holidays):
https://date.nager.at/api/v3/PublicHolidays/{year}/{countryCode}
- exchangerate.host (FX rates):
- Latest:
https://api.exchangerate.host/latest - Convert:
https://api.exchangerate.host/convert?from=USD&to=JPY&amount=200
- Latest:
- Open‑Meteo Air Quality (fallback if OpenAQ has no nearby observations):
https://air-quality-api.open-meteo.com/v1/air-quality
All MCP endpoints require X-API-KEY header.
- Geocoding
- POST
http://localhost:3001/geo/geocode - Body:
{ "city": "Kyoto", "country_hint": "JP" }
- POST
- Nearby places (Nominatim search)
- POST
http://localhost:3001/geo/nearby - Body:
{ "lat": 35.0116, "lon": 135.7681, "query": "coffee", "radius_m": 3000, "limit": 3 } - Deterministic: results are ranked by distance from center then name for stable ordering.
- POST
- Weather forecast (Open-Meteo)
- POST
http://localhost:3001/weather/forecast - Body:
{ "lat": 35.0116, "lon": 135.7681, "date": "2025-12-12" }
- POST
- Air quality (OpenAQ v3 with Open‑Meteo fallback)
- POST
http://localhost:3001/air/aqi - Body:
{ "lat": 40.7128, "lon": -74.0060 }
- POST
- Routing/ETA (OSRM)
- POST
http://localhost:3001/route/eta - Body:
{ "points": [{"lat": 35.0,"lon":135.7},{"lat":35.02,"lon":135.76}], "profile": "foot" }
- POST
- Public holidays (Nager.Date)
- POST
http://localhost:3001/calendar/holidays - Body:
{ "country_code": "JP", "year": 2025 }
- POST
- FX conversion (exchangerate.host)
- POST
http://localhost:3001/fx/convert - Body:
{ "amount": 200, "from": "USD", "to": "JPY" } - Response:
{ "rate": <number>, "converted": <number> }
- POST
Orchestrator (streams SSE):
- POST
http://localhost:3002/plan-day - Body:
{ "prompt": "Plan 10:00–18:00 in Kyoto on 2025-12-12. Prefer temples and walkable", "session_id": "<optional UUID>" } - Emits:
tool_traceentries for each tool call with durationsplan_chunkentries with Markdown segments- a final “Tool trace summary” Markdown table with durations
[DONE]when complete
- Short‑circuit prompts:
- FX-only (e.g., “convert 200 USD to JPY”) → compact FX table
- AQI-only (e.g., “AQI in Kyoto on 2025‑12‑12?”) → compact AQI table
- Weather-only (e.g., “Weather in Kyoto on 2025‑12‑12?”) → compact weather table
- Holidays-only (e.g., “Public holidays in JP in 2025?”) → compact holidays table
- Combos of any of the above (e.g., “AQI in Kyoto and convert 200 USD to JPY”) → multiple compact sections, no itinerary
System instruction:
- “You are an assistant that classifies user requests. Respond only with a JSON object.” Model must produce:
{
"intent": "plan_day | refine_plan | compare_options",
"city": "string",
"date": "YYYY-MM-DD",
"country_code": "2-letter ISO",
"preferences": ["..."]
}If classification is not plan_day but the prompt contains a currency conversion (e.g., “Convert 200 USD to JPY”), the orchestrator short-circuits to the FX tool and returns a small Markdown block with the rate and converted amount. Currency phrases like “Japanese Yen” or “Indian Rupee” are normalized to ISO codes.
Refinement and comparison:
refine_plan: re-plans venues based on the refinement instruction using prior session context (geocode/weather/air/holidays), recomputes nearby+ETA deterministically, and summarizes the refined itinerary.compare_options: generates two alternative Markdown itineraries (“Option A” and “Option B”) using the existing session context without re-calling external tools.
Inputs to the planner include:
- Weather summary, air-quality summary, holidays, preferences, basic geocoding Planner returns a JSON array of 4–6 venue names or types. The orchestrator then queries nearby places for each item and builds a point list for routing. Determinism: models are run with temperature 0, nearby results are ranked deterministically, and a per-context plan cache (city/date/country/preferences/coords/profile/radius/limit) ensures identical stop order and ETAs for repeated identical prompts.
When there are at least two points, the orchestrator calls the route ETA tool (OSRM) using a default profile (foot/bike/car). The result is included in the itinerary between stops.
The final system instruction requires a Markdown itinerary and explicitly mandates:
- Insert travel legs with ETAs between itinerary points
- If rain probability > 60%, add an indoor-heavy alternative section
- If PM2.5 > 75 μg/m³, append “mask recommended” to outdoor segments or propose indoor swaps
- Add a holiday caution when relevant
- Include a “Currency Conversion” section when FX is present
For the final streaming output, the orchestrator uses a text-configured model instance (temperature 0) to ensure stable Markdown text. Each response ends with a “Tool trace summary” table for visibility into which tools were called and their durations.
- “This demo only supports plan_day intent.”
Use a plan prompt, or include a currency expression like “convert 200 USD to JPY” for FX-only. - Air data absent for a future date
OpenAQ is observational only. The air tool automatically drops future dates and fetches latest measurements near the coordinates; it also includes an Open‑Meteo fallback. - No FX section in the output
Ensure your prompt contains a recognizable conversion phrase (e.g., “convert 200 USD to JPY”) and thatEXCHANGERATE_API_KEYis set if your account requires it. - 401/403 from OpenAQ
EnsureOPENAQ_API_KEYis valid in.envand visible in the container.
Minimal acceptance tests are included to validate core behaviors end‑to‑end via the orchestrator, without deep mocking.
How to run:
- Start services:
docker-compose up -d --build- (Optional) Create/activate venv and install dev deps:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt- Run the test file:
pytest -v -rA tests/test_acceptance.pyWhat the tests do (brief summary):
- Basic plan: checks that a Kyoto plan includes weather info, at least one ETA/travel leg, and multiple stops.
- Rain fallback: on a known rainy test date, verifies an indoor‑heavy alternative is included.
- Air guardrail: on a high‑PM2.5 test date, verifies “mask recommended” or an indoor swap is suggested.
- Holiday awareness: on a holiday, verifies there’s a caution about closures/crowds.
- FX only: verifies “Convert 200 USD to JPY” returns only a currency conversion (no itinerary).
- Trace visibility: verifies a final “Tool trace summary” table is present listing tool calls and durations.
- “Plan a museum-first day in Amsterdam on 2025-11-22. Bike preferred.”
- “Refine: add a specialty coffee stop near the second venue.”
- “Compare two options if it rains after 3pm.”
- “Plan a day in Tokyo on 2025-11-20. Also convert 200 USD to JPY.”