assess_trip
Assess one trip: current disruption status for its destinations and dates.
Use this tool when the user asks whether a specific trip is affected by
strikes, weather, transport disruptions or other travel risks.
Give the destinations and the travel window (date_from/date_to,
YYYY-MM-DD). Destinations are the EU-27 ISO2 codes (Greece = "EL") PLUS the
non-EU27 countries we actively monitor: Norway ("NO", rail via Entur, live),
the United Kingdom ("UK" or "GB", transit via TfL, live), and Switzerland
("CH", rail via SBB — key-pending, so it is reported as a declared blind
spot until the feed is keyed, never a false all-clear). A code we do not
monitor is rejected with {"error": "unknown_country"} rather than silently
all-cleared. Returns a Decision-Support answer, not raw data:
* travel_status: NORMAL | MINOR_DISRUPTION | MAJOR_DISRUPTION;
* actionable_lines: per-event DECISION-IMPACT guidance — what the
disruption means for THIS trip and what to do (e.g. "affects regional
trains, not airports -> take a road airport transfer, leave ~30 min
earlier"), or a clearly-labelled "nothing material" line when calm;
* confidence: a LABELLED model output (coverage/corroboration/recency/
blind-spots blend, not a probability) — read its caveats;
* sources_checked: proof of what was monitored (sources_ok, blind spots);
* events + caveats.
Sub-floor noise (a deep, far-field seismic blip) is omitted; calm is a
monitoring result for the window, never an invented forecast. Invalid
inputs return an explicit {"error": ...}; nothing is fabricated.
Top-level MCP-facing structure (additive; existing fields preserved):
* presentation: a three-section block — affects_your_trip[] (each
item with verified_sources[] as display-ready names, source_count,
corroborated flag (≥2 distinct sources), an honest for_you line
bound to destinations+dates only, report_url, first_detected_at,
last_verified_at); doesnt_affect_your_trip (the proof-of-work
pile — shown[] of {headline, reason_excluded}, additional_checked_count,
summary_line, total_checked); next_steps[] (deterministic — re-check
date, aviation-handoff watch when blind spot, per-active monitor URLs);
* track_record_ref: lean {window_days, flagged, ended, still_active,
monitoring_since, url} — numbers + URL only, no narrative;
* suggested_next_call: {tool, context} — the suggested follow-up
(watch_trip) when the user wants continued monitoring.
These exist so an LLM consumer can quote verbatim — every fact is
traceable to a named source or an input field, never invented.
Destinations also accept natural input: country names ('Spain', 'España',
'Spania'), IATA airport or city codes ('ALC', 'TSR', 'LON'), city names in
any common spelling ('Alicante'/'Alacant', 'București'/'Bucharest',
'München'/'Munich') and major rail stations ('Alicante Terminal', 'Gare de
Lyon'), resolved deterministically from local datasets (exact match after
Unicode folding — never fuzzy). The response discloses every mapping in
'resolved_locations' ([{input, canonical_name, country, lat, lon, type,
source, confidence}]); the legacy 'resolved' list is kept. A token that
cannot be placed is returned in 'unresolved_locations' with suggestions;
if at least one other token resolved, the verdict is still computed on the
resolved countries and marked 'degraded' (reason + missing tokens, plus a
caveat) — never a silent downgrade. When nothing resolves the call is
rejected with {'error': 'unknown_destination', 'unresolved_locations':
[...]}; a token in a country we do not monitor is rejected with
{'error': 'unknown_country'}.
Regional scope (weather): when the source publishes warning AREAS
(Meteoalarm areaDesc + EMMA_ID), each cited event carries
region_scope {level: region | no-match | country, matched[], places_checked,
method} — a NAME match between the areas and your resolved places / their
region and province (no geometry: Meteoalarm publishes no polygons). The
country-level travel_status is unchanged; travel_status_regional is the
status without the events whose areas name none of your places, and a
caveat says so. Weather events without area detail say "country level".
Optional `legs`: the trip as segments with a travel MODE —
[{"from": "OTP", "to": "Valencia", "mode": "air", "date_from": "...",
"date_to": "..."}, {"from": "Valencia", "to": "Alicante", "mode": "rail"}]
(mode: air | rail | road | sea; leg dates optional, default = the trip
window, must sit inside it). Endpoints are resolved like destinations and
are ADDED to them, so you may pass destinations: [] with legs only. The
answer then carries legs[]: per leg the resolved endpoints, countries,
domains_applicable for its mode (rail sources apply to rail legs, aviation
to air legs; weather/disaster/health/geopolitical/public order to all),
the events that hit THAT leg (a weather event whose areas name neither
endpoint is listed apart in events_not_in_your_regions), its own
travel_status, its own blind_spots and — for air legs — the
aviation_handoff. An unresolved endpoint is an
explicit {"error": "unresolved_leg_endpoint"} with suggestions.
Pass `lang` (e.g. "de", "ro", "pl", "fr", "es", "it"; default English) to
answer in the traveller's language — useful for a traveller
in a country whose language they do not speak. The response then carries a
`localized` block with the status sentence, an honest reassurance line
(calm ONLY when status is NORMAL), the decision-impact lines, AND — never
dropped — the localized caveats + blind_spots. Source-derived free text the
traveller cannot read (an event headline in the source language) is
AI-translated via Gemini and carries the label "AI-translated — verify
against the linked official source"; when no GEMINI_API_KEY is set or a
translation fails, the original source text is kept with an honest note —
never a fake translation. Our own wording falls back to English (flagged in
`localized.fallback_lang_parts`) when no template exists for `lang`; an
unknown `lang` answers in English and says so (`is_known_lang=false`).
Localization NEVER becomes a false all-clear and the aviation handoff is a
SIGNPOST that DISCLOSES the blind spot, not coverage.
Pass `audience` for role-specific operational actions (B2B travel-risk /
duty-of-care): one of "tmc" (travel management company / corporate travel
risk), "hotel", "ota", "tour_operator". The response then carries a
`persona` block: {audience, actions[]} where each action ties an affecting
event to that role's recommended steps (e.g. TMC: flexible-rebooking policy,
reroute inventory, proactive guest comms) — a PURE PROJECTION of the
audience-tagged recommendations already computed per event, each carrying a
`based_on` disclosure of the inputs it used. An unknown audience is reported
honestly with the valid set, never guessed. Omit `audience` for the default
(no persona block).